Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions app/src/main/java/com/hilingual/app/DeviceInfoProviderImpl.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.hilingual.app

import android.content.Context
import android.os.Build
import com.hilingual.core.common.extension.appVersionName
import com.hilingual.core.common.app.DeviceInfoProvider
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject

internal class DeviceInfoProviderImpl @Inject constructor(
@ApplicationContext private val context: Context
) : DeviceInfoProvider {

override fun getDeviceName(): String = Build.MODEL

override fun getDeviceType(): String =
if (context.resources.configuration.smallestScreenWidthDp >= 600) "TABLET" else "PHONE"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이전에도 있었던 코드이긴 하지만, 이번에 보다가 궁금한 점이 생겨서 댓글 남깁니다..!

smallestscreenWidthDp에 따라 디바이스 타입이 결정되는 것으로 이해했는데요, 코드를 보니 로그인 시점에만 이 부분이 호출되는 것으로 파악했습니다.

이 값이 API 호출 시 단순 통계/로그용으로만 사용되는 것인지 궁금합니다!

Copy link
Member Author

@angryPodo angryPodo Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

현재 앱에서는 넓이를 기준으로 변하는 디자인을 채용하고 있지 않습니다. 해당 정보는 서비스 서버의 응답을 위한 코드입니다. 노션에 명세를 보면 알수있어요👍🏻
서버 자체에서 판단하는 정보이고 앰플리튜드 트래커로 추적하지 않는 정보입니다.


override fun getOsVersion(): String = Build.VERSION.RELEASE

override fun getAppVersion(): String = context.appVersionName

override fun getProvider(): String = "GOOGLE"

override fun getRole(): String = "USER"

override fun getOsType(): String = "Android"
}
20 changes: 20 additions & 0 deletions app/src/main/java/com/hilingual/di/ProviderModule.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.hilingual.di

import com.hilingual.core.common.app.DeviceInfoProvider
import com.hilingual.app.DeviceInfoProviderImpl
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
internal abstract class ProviderModule {

@Binds
@Singleton
abstract fun bindDeviceInfoProvider(
impl: DeviceInfoProviderImpl
): DeviceInfoProvider
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.hilingual.data.auth.datasource
package com.hilingual.core.common.app

interface SystemDataSource {
interface DeviceInfoProvider {
fun getDeviceName(): String

fun getDeviceType(): String
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.hilingual.core.common.constant

const val STABLE_VERSION = "2.0.0"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

버전 관련 상수를 모아두는 용도로 VersionConstant로 이름붙이신 것 같은데, STABLE_VERSION으로 설정된 이유가 있는지 궁금해요! 버전 관리를 위해 앞으로 어떤 상수들이 또 생기게 될 수 있을까요? 기존의 DEFAULT_VERSION을 이번에 다 STABLE_VERSION으로 대체하신 것 같길래 여쭤봅니다!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

기존 DEFAULT_VERSION이라는 이름은 '0.0.0' 같은 단순히 빈 값을 채우는 의미로만 느껴져서, 앱이 정상적으로 동작한다고 보장할 수 있는 최소한의 기준점을 명시하고자 STABLE_VERSION으로 변경했습니다.

그리고 버전 추출 실패 시 비정상적인 버전 값 때문에 강제 업데이트 로직이 잘못 실행되는 등의 부수 효과를 방지하는 안전장치 역할을 합니다. 앞으로는 MIN_SUPPORTED_VERSION처럼 정책적인 상수들이 추가될 수 있을 것 같아요.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

앞으로 버전 계속 올라갈텐데 STABLE_VERSION은 고정인가요? 아님 메이저만 반영하는 등의 기준이 있을까요?

Copy link
Member Author

@angryPodo angryPodo Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 값은 고정이라기보다는 프로젝트의 기준이 되는 '최소 보장 버전'의 성격입니다. 버전 정보를 가져오지 못하는 예외 상황에서도 앱의 비즈니스 로직이 깨지지 않게 하기 위한 최소한의 가이드라인이라고 봐주시면 될 것 같습니다. 말씀하신 대로 메이저 업데이트나 중요한 정책 변경이 있을 때 이 상수도 함께 관리할 예정입니다!

Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,25 @@
package com.hilingual.core.common.extension

import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import androidx.browser.customtabs.CustomTabsIntent
import androidx.core.net.toUri
import com.hilingual.core.common.constant.STABLE_VERSION

fun Context.launchCustomTabs(url: String) {
CustomTabsIntent.Builder().build().launchUrl(this, url.toUri())
}

val Context.appVersionName: String
get() = runCatching {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기에서 suspendRuncatching이 아닌 runcatching을 사용하신 이유가 있나요?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

appVersionName을 확장 프로퍼티로 선언했기 때문에 호출하는 쪽에서 별도의 코루틴 스코프 없이도 동기적으로 값을 바로 얻을 수 있도록 runCatching을 사용했습니다.
PackageManager를 통한 정보 조회는 I/O나 네트워크 작업이 아닌 가벼운 시스템 API 호출이라 suspend로 만들지 않아도 성능상 영향이 미비하다고 판단했고 우리 프로젝트 컨벤션인 suspendRunCatching은 비동기 처리가 필요한 Repository 레이어의 로직에서 적극 활용하는게 의도니까요ㅎㅎ

val packageInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
packageManager.getPackageInfo(
packageName,
PackageManager.PackageInfoFlags.of(0)
)
} else {
packageManager.getPackageInfo(packageName, 0)
}
packageInfo.versionName
}.getOrNull() ?: STABLE_VERSION
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DeviceInfoProviderImpl에서 사용하는 걸로 보이는데, 이외에 또 해당 확장함수가 사용될 곳이 있을까요??
디바이스 관련 정보를 앞으로 DeviceInfoProvider로 관리한다고 하면, 구현체에 한 번만 쓰일 함수이지 않을까 싶은데 Ext 파일로 로직을 분리하신 이유가 궁금합니다

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

음 버전을 UI에서 불러올때 사용하는걸 의도했습니다. 정말 단순하게 버전을 불러올 뿐이라면 해당 함수를 사용하는게 더 간결하니까요! 해당 의도를 더 전달하기 위해서 Mypage는 Provider없이 사용하도록 수정하겠습니다

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,8 @@ import android.content.Context
import androidx.credentials.CredentialManager
import com.hilingual.data.auth.datasource.AuthRemoteDataSource
import com.hilingual.data.auth.datasource.GoogleAuthDataSource
import com.hilingual.data.auth.datasource.SystemDataSource
import com.hilingual.data.auth.datasourceimpl.AuthRemoteDataSourceImpl
import com.hilingual.data.auth.datasourceimpl.GoogleAuthDataSourceImpl
import com.hilingual.data.auth.datasourceimpl.SystemDataSourceImpl
import dagger.Binds
import dagger.Module
import dagger.Provides
Expand All @@ -46,12 +44,6 @@ internal abstract class DataSourceModule {
authRemoteDataSourceImpl: AuthRemoteDataSourceImpl
): AuthRemoteDataSource

@Binds
@Singleton
abstract fun bindSystemDataSource(
systemDataSourceImpl: SystemDataSourceImpl
): SystemDataSource

internal companion object {
@Provides
@Singleton
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@
package com.hilingual.data.auth.repositoryimpl

import android.content.Context
import com.hilingual.core.common.app.DeviceInfoProvider
import com.hilingual.core.common.util.suspendRunCatching
import com.hilingual.core.localstorage.TokenManager
import com.hilingual.core.localstorage.UserInfoManager
import com.hilingual.data.auth.datasource.AuthRemoteDataSource
import com.hilingual.data.auth.datasource.GoogleAuthDataSource
import com.hilingual.data.auth.datasource.SystemDataSource
import com.hilingual.data.auth.dto.request.LoginRequestDto
import com.hilingual.data.auth.dto.request.VerifyCodeRequestDto
import com.hilingual.data.auth.model.LoginModel
Expand All @@ -31,7 +31,7 @@ import javax.inject.Inject
internal class AuthRepositoryImpl @Inject constructor(
private val authRemoteDataSource: AuthRemoteDataSource,
private val googleAuthDataSource: GoogleAuthDataSource,
private val systemDataSource: SystemDataSource,
private val deviceInfoProvider: DeviceInfoProvider,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Manager과 Provider 명칭을 구분하시는 기준이 있으실까요??
아래에는 TokenManager, UserInfoManager를 사용하셨기에 궁금해서 여쭤봅니다:)
디바이스 정보를 가져오는 것도 어쩄든 관리의 영역이라고 볼 수도 있을 것 같아서, DeviceInfoProvider 말고 DeviceInfoManager로 쓸 수도 있지 않을까 싶어서요!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Manager와 Provider 명칭에 대해 저도 고민을 많이 했었는데요! 제가 생각하는 기준은 '상태 관리나 저장/수정 로직이 포함되는가'입니다.

TokenManager나 UserInfoManager는 데이터를 로컬에 저장하고 갱신하거나 삭제하는 등 데이터의 생명주기를 CRUD하는 성격이 강합니다.
DeviceInfoProvider는 시스템으로부터 기기 정보를 단순히 provide 받는 읽기 전용 역할이기에 Provider가 더 적합하다고 생각해요. 관리보다는 시스템 정보를 안전하게 읽어오는 통로 역할에 집중했다고 봐주시면 좋을 것 같습니다👍🏻

private val tokenManager: TokenManager,
private val userInfoManager: UserInfoManager
) : AuthRepository {
Expand All @@ -41,7 +41,7 @@ internal class AuthRepositoryImpl @Inject constructor(
override suspend fun login(providerToken: String): Result<LoginModel> = suspendRunCatching {
val loginResponse = authRemoteDataSource.login(
providerToken = providerToken,
loginRequestDto = systemDataSource.toLoginRequestDto()
loginRequestDto = deviceInfoProvider.toLoginRequestDto()
).data!!

tokenManager.saveTokens(loginResponse.accessToken, loginResponse.refreshToken)
Expand Down Expand Up @@ -70,7 +70,7 @@ internal class AuthRepositoryImpl @Inject constructor(
userInfoManager.clear()
}

private fun SystemDataSource.toLoginRequestDto() = LoginRequestDto(
private fun DeviceInfoProvider.toLoginRequestDto() = LoginRequestDto(
provider = this.getProvider(),
role = this.getRole(),
deviceName = this.getDeviceName(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,3 @@ const val MIN_FETCH_INTERVAL_SECONDS = 3600L

const val KEY_MIN_FORCE_VERSION = "min_force_version"
const val KEY_LATEST_VERSION = "latest_version"

const val DEFAULT_VERSION = "0.0.0"
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.hilingual.data.config.datasourceimpl

import com.google.firebase.remoteconfig.FirebaseRemoteConfig
import com.hilingual.core.common.extension.onLogFailure
import com.hilingual.data.config.constant.DEFAULT_VERSION
import com.hilingual.core.common.constant.STABLE_VERSION
import com.hilingual.data.config.constant.KEY_LATEST_VERSION
import com.hilingual.data.config.constant.KEY_MIN_FORCE_VERSION
import com.hilingual.data.config.datasource.ConfigRemoteDataSource
Expand All @@ -31,14 +31,12 @@ internal class RemoteConfigDataSourceImpl @Inject constructor(
) : ConfigRemoteDataSource {

override suspend fun getAppVersionInfo(): AppVersionInfo {
runCatching {
remoteConfig.fetchAndActivate().await()
}.onLogFailure { }
remoteConfig.fetchAndActivate().await()

val minForceVersionStr = remoteConfig.getString(KEY_MIN_FORCE_VERSION)
.takeIf { it.isNotBlank() } ?: DEFAULT_VERSION
.takeIf { it.isNotBlank() } ?: STABLE_VERSION
val latestVersionStr = remoteConfig.getString(KEY_LATEST_VERSION)
.takeIf { it.isNotBlank() } ?: DEFAULT_VERSION
.takeIf { it.isNotBlank() } ?: STABLE_VERSION

return AppVersionInfo(
minForceVersion = AppVersion(minForceVersionStr),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@ package com.hilingual.data.config.di

import com.hilingual.data.config.datasource.ConfigRemoteDataSource
import com.hilingual.data.config.datasourceimpl.RemoteConfigDataSourceImpl
import com.hilingual.data.config.repository.ConfigRepository
import com.hilingual.data.config.repositoryimpl.ConfigRepositoryImpl
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
Expand All @@ -27,17 +25,11 @@ import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
internal abstract class ConfigModule {
internal abstract class DataSourceModule {

@Binds
@Singleton
abstract fun bindConfigRemoteDataSource(
impl: RemoteConfigDataSourceImpl
): ConfigRemoteDataSource

@Binds
@Singleton
abstract fun bindConfigRepository(
impl: ConfigRepositoryImpl
): ConfigRepository
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import com.google.firebase.Firebase
import com.google.firebase.remoteconfig.FirebaseRemoteConfig
import com.google.firebase.remoteconfig.remoteConfig
import com.google.firebase.remoteconfig.remoteConfigSettings
import com.hilingual.data.config.constant.DEFAULT_VERSION
import com.hilingual.core.common.constant.STABLE_VERSION
import com.hilingual.data.config.constant.KEY_LATEST_VERSION
import com.hilingual.data.config.constant.KEY_MIN_FORCE_VERSION
import com.hilingual.data.config.constant.MIN_FETCH_INTERVAL_SECONDS
Expand All @@ -44,8 +44,8 @@ object FirebaseModule {

remoteConfig.setDefaultsAsync(
mapOf(
KEY_MIN_FORCE_VERSION to DEFAULT_VERSION,
KEY_LATEST_VERSION to DEFAULT_VERSION
KEY_MIN_FORCE_VERSION to STABLE_VERSION,
KEY_LATEST_VERSION to STABLE_VERSION
)
)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.hilingual.data.config.di

import com.hilingual.data.config.repository.ConfigRepository
import com.hilingual.data.config.repositoryimpl.ConfigRepositoryImpl
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
internal abstract class RepositoryModule {

@Binds
@Singleton
abstract fun bindConfigRepository(
impl: ConfigRepositoryImpl
): ConfigRepository
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,12 @@ import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.hilingual.core.common.provider.LocalAppRestarter
import com.hilingual.core.common.constant.UrlConstant
import com.hilingual.core.common.extension.collectSideEffect
import com.hilingual.core.common.extension.launchCustomTabs
import com.hilingual.core.common.extension.statusBarColor
import com.hilingual.core.common.model.HilingualMessage
import com.hilingual.core.common.provider.LocalAppRestarter
import com.hilingual.core.common.trigger.LocalDialogTrigger
import com.hilingual.core.common.trigger.LocalMessageController
import com.hilingual.core.common.util.UiState
Expand Down Expand Up @@ -87,6 +87,7 @@ internal fun MyPageRoute(
paddingValues = paddingValues,
profileImageUrl = state.data.profileImageUrl,
profileNickname = state.data.profileNickname,
appVersion = state.data.appVersion,
onProfileEditClick = navigateToProfileEdit,
onMyFeedClick = navigateToMyFeedProfile,
onAlarmClick = navigateToAlarm,
Expand All @@ -107,6 +108,7 @@ private fun MyPageScreen(
paddingValues: PaddingValues,
profileImageUrl: String,
profileNickname: String,
appVersion: String,
onProfileEditClick: () -> Unit,
onMyFeedClick: () -> Unit,
onAlarmClick: () -> Unit,
Expand Down Expand Up @@ -191,7 +193,7 @@ private fun MyPageScreen(
title = "버전 정보",
trailingContent = {
Text(
text = context.packageManager.getPackageInfo(context.packageName, 0).versionName ?: "",
text = appVersion,
color = HilingualTheme.colors.gray400,
style = HilingualTheme.typography.bodyR14,
modifier = Modifier.padding(end = 4.dp)
Expand Down Expand Up @@ -240,6 +242,7 @@ private fun MyPageScreenPreview() {
paddingValues = PaddingValues(),
profileImageUrl = "",
profileNickname = "하링이",
appVersion = "1.0.0",
onProfileEditClick = {},
onMyFeedClick = {},
onAlarmClick = {},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@
package com.hilingual.presentation.mypage

import androidx.compose.runtime.Immutable
import com.hilingual.core.common.constant.STABLE_VERSION

@Immutable
internal data class MyPageUiState(
val profileImageUrl: String = "",
val profileNickname: String = "",
val profileProvider: String = ""
val profileProvider: String = "",
val appVersion: String = STABLE_VERSION
)
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import android.net.Uri
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.hilingual.core.common.extension.onLogFailure
import com.hilingual.core.common.app.DeviceInfoProvider
import com.hilingual.core.common.util.UiState
import com.hilingual.data.auth.repository.AuthRepository
import com.hilingual.data.user.repository.UserRepository
Expand All @@ -36,7 +37,8 @@ import javax.inject.Inject
@HiltViewModel
internal class MyPageViewModel @Inject constructor(
private val userRepository: UserRepository,
private val authRepository: AuthRepository
private val authRepository: AuthRepository,
private val deviceInfoProvider: DeviceInfoProvider
) : ViewModel() {
private val _uiState = MutableStateFlow<UiState<MyPageUiState>>(UiState.Loading)
val uiState: StateFlow<UiState<MyPageUiState>> = _uiState.asStateFlow()
Expand All @@ -57,7 +59,8 @@ internal class MyPageViewModel @Inject constructor(
MyPageUiState(
profileImageUrl = userInfo.profileImg,
profileNickname = userInfo.nickname,
profileProvider = userInfo.provider
profileProvider = userInfo.provider,
appVersion = deviceInfoProvider.getAppVersion()
)
)
}
Expand Down
Loading
Loading