Skip to content

Commit c9c0269

Browse files
authored
Merge branch 'develop' into design/NDGL-49
2 parents a5c1da5 + f5b0d68 commit c9c0269

File tree

14 files changed

+268
-25
lines changed

14 files changed

+268
-25
lines changed

.claude/settings.local.json

Lines changed: 0 additions & 9 deletions
This file was deleted.

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ render.experimental.xml
3333
# Google Services (e.g. APIs or Firebase)
3434
google-services.json
3535

36+
# Claude Code local settings
37+
.claude/settings.local.json
38+
3639
# Android Profiling
3740
*.hprof
3841

app/src/main/AndroidManifest.xml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
33
xmlns:tools="http://schemas.android.com/tools">
44

5+
<uses-permission android:name="android.permission.INTERNET" />
6+
57
<application
68
android:name=".NDGLApplication"
79
android:allowBackup="true"
@@ -11,8 +13,9 @@
1113
android:label="@string/app_name"
1214
android:roundIcon="@mipmap/ic_launcher_round"
1315
android:supportsRtl="true"
14-
android:theme="@style/Theme.NDGL">
15-
16+
android:theme="@style/Theme.NDGL"
17+
android:usesCleartextTraffic="true">
18+
1619
<meta-data
1720
android:name="com.google.android.geo.API_KEY"
1821
android:value="${MAPS_API_KEY}" />

build-logic/src/main/kotlin/NDGLDataPlugin.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ class NDGLDataPlugin : Plugin<Project> {
2424
}
2525

2626
dependencies {
27+
"implementation"(project(":core:util"))
2728
"implementation"(libs.findLibrary("retrofit").get())
2829
"implementation"(libs.findLibrary("retrofit-kotlinx-serialization-json").get())
2930
"implementation"(libs.findLibrary("kotlinx-serialization-json").get())

core/ui/src/main/java/com/yapp/ndgl/core/ui/designsystem/NDGLNavigationBar.kt

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.yapp.ndgl.core.ui.designsystem
33
import androidx.annotation.DrawableRes
44
import androidx.compose.foundation.clickable
55
import androidx.compose.foundation.layout.Arrangement
6+
import androidx.compose.foundation.layout.Box
67
import androidx.compose.foundation.layout.Row
78
import androidx.compose.foundation.layout.RowScope
89
import androidx.compose.foundation.layout.Spacer
@@ -45,8 +46,8 @@ fun NDGLNavigationBar(
4546
modifier = modifier
4647
.fillMaxWidth()
4748
.height(48.dp)
48-
.padding(horizontal = 24.dp, vertical = 12.dp),
49-
horizontalArrangement = Arrangement.spacedBy(8.dp),
49+
.padding(horizontal = 24.dp, vertical = 4.dp),
50+
horizontalArrangement = Arrangement.spacedBy(4.dp),
5051
verticalAlignment = Alignment.CenterVertically,
5152
) {
5253
leadingIcon?.let { icon ->
@@ -69,8 +70,10 @@ fun NDGLNavigationBar(
6970
)
7071
} ?: Spacer(modifier = Modifier.weight(1f))
7172

72-
trailingContents?.let { contents ->
73-
contents()
73+
if (trailingContents != null) {
74+
trailingContents()
75+
} else {
76+
Box(modifier = Modifier.size(40.dp))
7477
}
7578
}
7679
}
@@ -84,10 +87,11 @@ fun NDGLNavigationIcon(
8487
imageVector = ImageVector.vectorResource(icon),
8588
contentDescription = null,
8689
modifier = Modifier
87-
.size(28.dp)
90+
.size(40.dp)
8891
.clip(CircleShape)
89-
.clickable(onClick = onClick),
90-
tint = NDGLTheme.colors.black700,
92+
.clickable(onClick = onClick)
93+
.padding(6.dp),
94+
tint = NDGLTheme.colors.black600,
9195
)
9296
}
9397

@@ -113,6 +117,36 @@ private fun NDGLNavigationBarCenterPreview() {
113117
}
114118
}
115119

120+
@Preview(showBackground = true)
121+
@Composable
122+
private fun NDGLNavigationBarPreview() {
123+
NDGLTheme {
124+
NDGLNavigationBar(
125+
textAlignType = NDGLNavigationBarAttr.TextAlignType.CENTER,
126+
headline = "미리보기",
127+
leadingIcon = R.drawable.ic_28_chevron_left,
128+
trailingContents = {
129+
NDGLNavigationIcon(
130+
icon = R.drawable.ic_28_search,
131+
onClick = {},
132+
)
133+
},
134+
)
135+
}
136+
}
137+
138+
@Preview(showBackground = true)
139+
@Composable
140+
private fun NDGLNavigationBarNoTrailingPreview() {
141+
NDGLTheme {
142+
NDGLNavigationBar(
143+
textAlignType = NDGLNavigationBarAttr.TextAlignType.CENTER,
144+
headline = "미리보기",
145+
leadingIcon = R.drawable.ic_28_chevron_left,
146+
)
147+
}
148+
}
149+
116150
@Preview(showBackground = true)
117151
@Composable
118152
private fun NDGLNavigationBarStartPreview() {

data/auth/build.gradle.kts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,19 @@ plugins {
44

55
android {
66
namespace = "com.yapp.ndgl.data.auth"
7+
8+
buildFeatures {
9+
buildConfig = true
10+
}
11+
12+
defaultConfig {
13+
buildConfigField("String", "VERSION_NAME", "\"${Configuration.VERSION_NAME}\"")
14+
}
715
}
816

917
dependencies {
18+
implementation(project(":data:core"))
19+
implementation(platform(libs.firebase.bom))
20+
implementation(libs.firebase.messaging)
1021
implementation(libs.androidx.datastore)
1122
}

data/auth/src/main/java/com/yapp/ndgl/data/auth/local/LocalAuthDataSource.kt

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import androidx.datastore.core.DataStore
44
import androidx.datastore.preferences.core.Preferences
55
import androidx.datastore.preferences.core.edit
66
import androidx.datastore.preferences.core.stringPreferencesKey
7+
import com.yapp.ndgl.data.auth.local.security.CryptoManager
78
import com.yapp.ndgl.data.auth.local.util.handleException
89
import kotlinx.coroutines.flow.Flow
910
import kotlinx.coroutines.flow.first
@@ -14,17 +15,18 @@ import javax.inject.Singleton
1415
@Singleton
1516
class LocalAuthDataSource @Inject constructor(
1617
private val dataStore: DataStore<Preferences>,
18+
private val cryptoManager: CryptoManager,
1719
) {
1820
private val accessToken: Flow<String> = dataStore.data
1921
.handleException()
2022
.map { preferences ->
21-
preferences[ACCESS_TOKEN_KEY] ?: ""
23+
cryptoManager.decrypt(preferences[ACCESS_TOKEN_KEY] ?: "")
2224
}
2325

2426
private val uuid: Flow<String> = dataStore.data
2527
.handleException()
2628
.map { preferences ->
27-
preferences[UUID_KEY] ?: ""
29+
cryptoManager.decrypt(preferences[UUID_KEY] ?: "")
2830
}
2931

3032
suspend fun getAccessToken(): String = accessToken.first()
@@ -33,13 +35,13 @@ class LocalAuthDataSource @Inject constructor(
3335

3436
suspend fun setAccessToken(token: String) {
3537
dataStore.edit { preferences ->
36-
preferences[ACCESS_TOKEN_KEY] = token
38+
preferences[ACCESS_TOKEN_KEY] = cryptoManager.encrypt(token)
3739
}
3840
}
3941

4042
suspend fun setUuid(uuid: String) {
4143
dataStore.edit { preferences ->
42-
preferences[UUID_KEY] = uuid
44+
preferences[UUID_KEY] = cryptoManager.encrypt(uuid)
4345
}
4446
}
4547

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package com.yapp.ndgl.data.auth.local.security
2+
3+
import android.security.keystore.KeyGenParameterSpec
4+
import android.security.keystore.KeyProperties
5+
import android.util.Base64
6+
import timber.log.Timber
7+
import java.security.KeyStore
8+
import javax.crypto.Cipher
9+
import javax.crypto.KeyGenerator
10+
import javax.crypto.SecretKey
11+
import javax.crypto.spec.GCMParameterSpec
12+
import javax.inject.Inject
13+
import javax.inject.Singleton
14+
15+
@Singleton
16+
class CryptoManager @Inject constructor() {
17+
private val keyStore = KeyStore.getInstance(KEYSTORE_PROVIDER).apply {
18+
load(null)
19+
}
20+
21+
private fun getKey(): SecretKey {
22+
return keyStore.getKey(KEY_ALIAS, null) as? SecretKey ?: createKey()
23+
}
24+
25+
private fun createKey(): SecretKey =
26+
KeyGenerator.getInstance(
27+
KeyProperties.KEY_ALGORITHM_AES,
28+
KEYSTORE_PROVIDER,
29+
).apply {
30+
init(
31+
KeyGenParameterSpec.Builder(
32+
KEY_ALIAS,
33+
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT,
34+
)
35+
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
36+
.setEncryptionPaddings(PADDING)
37+
.setUserAuthenticationRequired(false)
38+
.build(),
39+
)
40+
}.generateKey()
41+
42+
fun encrypt(plainText: String): String {
43+
check(plainText.isNotEmpty()) { "Plain text is empty" }
44+
45+
val cipher = Cipher.getInstance(TRANSFORMATION)
46+
cipher.init(Cipher.ENCRYPT_MODE, getKey())
47+
48+
val encryptedBytes = cipher.doFinal(plainText.toByteArray())
49+
val combined = cipher.iv + encryptedBytes
50+
51+
return Base64.encodeToString(combined, Base64.NO_WRAP)
52+
}
53+
54+
fun decrypt(encryptedText: String): String {
55+
try {
56+
check(encryptedText.isNotEmpty()) { "Encrypted text is empty" }
57+
58+
val combined = Base64.decode(encryptedText, Base64.NO_WRAP)
59+
val iv = combined.copyOfRange(0, IV_SIZE)
60+
val encryptedBytes = combined.copyOfRange(IV_SIZE, combined.size)
61+
62+
val cipher = Cipher.getInstance(TRANSFORMATION)
63+
val spec = GCMParameterSpec(GCM_TAG_LENGTH, iv)
64+
cipher.init(Cipher.DECRYPT_MODE, getKey(), spec)
65+
66+
val decryptedBytes = cipher.doFinal(encryptedBytes)
67+
return String(decryptedBytes)
68+
} catch (e: Exception) {
69+
Timber.e(e, "Failed to decrypt")
70+
return ""
71+
}
72+
}
73+
74+
companion object {
75+
private const val KEYSTORE_PROVIDER = "AndroidKeyStore"
76+
private const val KEY_ALIAS = "ndgl_encryption_key"
77+
private const val ALGORITHM = KeyProperties.KEY_ALGORITHM_AES
78+
private const val BLOCK_MODE = KeyProperties.BLOCK_MODE_GCM
79+
private const val PADDING = KeyProperties.ENCRYPTION_PADDING_NONE
80+
private const val TRANSFORMATION = "$ALGORITHM/$BLOCK_MODE/$PADDING"
81+
private const val IV_SIZE = 12
82+
private const val GCM_TAG_LENGTH = 128
83+
}
84+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package com.yapp.ndgl.data.auth.repository
2+
3+
import com.google.android.gms.tasks.Tasks
4+
import com.google.firebase.messaging.FirebaseMessaging
5+
import com.yapp.ndgl.core.util.suspendRunCatching
6+
import com.yapp.ndgl.data.auth.BuildConfig
7+
import com.yapp.ndgl.data.auth.api.AuthApi
8+
import com.yapp.ndgl.data.auth.local.LocalAuthDataSource
9+
import com.yapp.ndgl.data.auth.model.AuthResponse
10+
import com.yapp.ndgl.data.auth.model.CreateUserRequest
11+
import com.yapp.ndgl.data.auth.model.LoginRequest
12+
import com.yapp.ndgl.data.auth.util.DeviceInfoUtil
13+
import com.yapp.ndgl.data.core.model.getData
14+
import kotlinx.coroutines.Dispatchers
15+
import kotlinx.coroutines.withContext
16+
import javax.inject.Inject
17+
import javax.inject.Singleton
18+
19+
@Singleton
20+
class AuthRepository @Inject constructor(
21+
private val api: AuthApi,
22+
private val localAuthDataSource: LocalAuthDataSource,
23+
) {
24+
suspend fun initSession() {
25+
val uuid = localAuthDataSource.getUuid()
26+
val response = if (uuid.isNotEmpty()) {
27+
suspendRunCatching {
28+
login(uuid)
29+
}.getOrElse {
30+
localAuthDataSource.clearSession()
31+
createUser()
32+
}
33+
} else {
34+
createUser()
35+
}
36+
37+
localAuthDataSource.setAccessToken(response.accessToken)
38+
localAuthDataSource.setUuid(response.uuid)
39+
}
40+
41+
private suspend fun createUser(): AuthResponse {
42+
return api.createUser(
43+
CreateUserRequest(
44+
fcmToken = getFCMToken(),
45+
deviceModel = DeviceInfoUtil.deviceModel,
46+
deviceOs = DeviceInfoUtil.DEVICE_OS,
47+
deviceOsVersion = DeviceInfoUtil.deviceOsVersion,
48+
appVersion = BuildConfig.VERSION_NAME,
49+
),
50+
).getData()
51+
}
52+
53+
private suspend fun login(uuid: String): AuthResponse {
54+
return api.login(LoginRequest(uuid)).getData()
55+
}
56+
57+
private suspend fun getFCMToken(): String = withContext(Dispatchers.IO) {
58+
try {
59+
Tasks.await(FirebaseMessaging.getInstance().token)
60+
} catch (e: Exception) {
61+
throw IllegalStateException("Failed to get FCM token", e)
62+
}
63+
}
64+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.yapp.ndgl.data.auth.util
2+
3+
import android.os.Build
4+
5+
object DeviceInfoUtil {
6+
const val DEVICE_OS: String = "Android"
7+
val deviceModel: String = Build.MODEL
8+
val deviceOsVersion: String = Build.VERSION.RELEASE
9+
}

0 commit comments

Comments
 (0)