From 4ce92ad222c12ecc80caeb4c85ac23dcd587012c Mon Sep 17 00:00:00 2001 From: Ojongseok Date: Mon, 29 Dec 2025 08:41:43 +0900 Subject: [PATCH 01/19] =?UTF-8?q?[chore]=20DataStoreRepository.kt=20?= =?UTF-8?q?=EB=B0=8F=20DataStoreModule.kt=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dataapi/repository/DataStoreRepository.kt | 13 +++++ ...aStoreRepository.kt => DataStoreModule.kt} | 0 .../data/repository/di/RepositoryModule.kt | 10 ++++ .../impl/DataStoreRepositoryImpl.kt | 48 +++++++++++++++++++ 4 files changed, 71 insertions(+) create mode 100644 core/data-api/src/main/java/com/neki/android/core/dataapi/repository/DataStoreRepository.kt rename core/data/src/main/java/com/neki/android/core/data/local/di/{DataStoreRepository.kt => DataStoreModule.kt} (100%) create mode 100644 core/data/src/main/java/com/neki/android/core/data/repository/impl/DataStoreRepositoryImpl.kt diff --git a/core/data-api/src/main/java/com/neki/android/core/dataapi/repository/DataStoreRepository.kt b/core/data-api/src/main/java/com/neki/android/core/dataapi/repository/DataStoreRepository.kt new file mode 100644 index 000000000..e8831db1a --- /dev/null +++ b/core/data-api/src/main/java/com/neki/android/core/dataapi/repository/DataStoreRepository.kt @@ -0,0 +1,13 @@ +package com.neki.android.core.dataapi.repository + +import kotlinx.coroutines.flow.Flow + +interface DataStoreRepository { + suspend fun saveTokens( + accessToken: String, + refreshToken: String + ) + fun getAccessToken(): Flow + fun getRefreshToken(): Flow + suspend fun clearTokens() +} \ No newline at end of file diff --git a/core/data/src/main/java/com/neki/android/core/data/local/di/DataStoreRepository.kt b/core/data/src/main/java/com/neki/android/core/data/local/di/DataStoreModule.kt similarity index 100% rename from core/data/src/main/java/com/neki/android/core/data/local/di/DataStoreRepository.kt rename to core/data/src/main/java/com/neki/android/core/data/local/di/DataStoreModule.kt diff --git a/core/data/src/main/java/com/neki/android/core/data/repository/di/RepositoryModule.kt b/core/data/src/main/java/com/neki/android/core/data/repository/di/RepositoryModule.kt index a6c88f9da..d59620aa0 100644 --- a/core/data/src/main/java/com/neki/android/core/data/repository/di/RepositoryModule.kt +++ b/core/data/src/main/java/com/neki/android/core/data/repository/di/RepositoryModule.kt @@ -1,20 +1,30 @@ package com.neki.android.core.data.repository.di +import com.neki.android.core.data.repository.impl.DataStoreRepositoryImpl import com.neki.android.core.data.repository.impl.SampleRepositoryImpl +import com.neki.android.core.dataapi.repository.DataStoreRepository import com.neki.android.core.dataapi.repository.SampleRepository 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 bindSampleRepositoryImpl( sampleRepositoryImpl: SampleRepositoryImpl ): SampleRepository + @Binds + @Singleton + abstract fun bindDataStoreRepositoryImpl( + dataStoreRepositoryImpl: DataStoreRepositoryImpl + ): DataStoreRepository + } \ No newline at end of file diff --git a/core/data/src/main/java/com/neki/android/core/data/repository/impl/DataStoreRepositoryImpl.kt b/core/data/src/main/java/com/neki/android/core/data/repository/impl/DataStoreRepositoryImpl.kt new file mode 100644 index 000000000..6a5aa5fa9 --- /dev/null +++ b/core/data/src/main/java/com/neki/android/core/data/repository/impl/DataStoreRepositoryImpl.kt @@ -0,0 +1,48 @@ +package com.neki.android.core.data.repository.impl + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringPreferencesKey +import com.neki.android.core.common.crypto.CryptoManager +import com.neki.android.core.dataapi.repository.DataStoreRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +class DataStoreRepositoryImpl @Inject constructor( + private val dataStore: DataStore +): DataStoreRepository { + companion object { + private val ACCESS_TOKEN = stringPreferencesKey("access_token") + private val REFRESH_TOKEN = stringPreferencesKey("refresh_token") + } + + override suspend fun saveTokens( + accessToken: String, + refreshToken: String + ) { + dataStore.edit { preferences -> + preferences[ACCESS_TOKEN] = CryptoManager.encrypt(accessToken) + preferences[REFRESH_TOKEN] = CryptoManager.encrypt(refreshToken) + } + } + + override fun getAccessToken(): Flow { + return dataStore.data.map { preferences -> + preferences[ACCESS_TOKEN]?.let { CryptoManager.decrypt(it) } +// preferences[ACCESS_TOKEN] + } + } + + override fun getRefreshToken(): Flow { + return dataStore.data.map { preferences -> + preferences[REFRESH_TOKEN]?.let { CryptoManager.decrypt(it) } + } + } + + override suspend fun clearTokens() { + dataStore.edit { it.clear() } + } + +} \ No newline at end of file From ea9260aca673a953e85a425ce380ab22c36f563b Mon Sep 17 00:00:00 2001 From: Ojongseok Date: Mon, 29 Dec 2025 08:42:11 +0900 Subject: [PATCH 02/19] =?UTF-8?q?[chore]=20DataStore=20=EC=95=94=ED=98=B8?= =?UTF-8?q?=ED=99=94/=EB=B3=B5=ED=98=B8=ED=99=94=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EA=B5=AC=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/common/build.gradle.kts | 1 + .../core/common/crypto/CryptoManager.kt | 67 +++++++++++++++++++ core/data-api/build.gradle.kts | 2 + gradle/libs.versions.toml | 4 ++ 4 files changed, 74 insertions(+) create mode 100644 core/common/src/main/java/com/neki/android/core/common/crypto/CryptoManager.kt diff --git a/core/common/build.gradle.kts b/core/common/build.gradle.kts index 8250c3e29..7a26d84e2 100644 --- a/core/common/build.gradle.kts +++ b/core/common/build.gradle.kts @@ -9,5 +9,6 @@ android { dependencies { api(libs.timber) + implementation(libs.androidx.security.crypto) } \ No newline at end of file diff --git a/core/common/src/main/java/com/neki/android/core/common/crypto/CryptoManager.kt b/core/common/src/main/java/com/neki/android/core/common/crypto/CryptoManager.kt new file mode 100644 index 000000000..f94d06467 --- /dev/null +++ b/core/common/src/main/java/com/neki/android/core/common/crypto/CryptoManager.kt @@ -0,0 +1,67 @@ +package com.neki.android.core.common.crypto + +import android.annotation.SuppressLint +import android.security.keystore.KeyGenParameterSpec +import android.security.keystore.KeyProperties +import android.util.Base64 +import java.security.KeyStore +import javax.crypto.Cipher +import javax.crypto.KeyGenerator +import javax.crypto.SecretKey +import javax.crypto.spec.GCMParameterSpec + +object CryptoManager { + private const val ALGORITHM = "AES/GCM/NoPadding" + private const val KEY_ALIAS = "token_encryption_key" + + private val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { + load(null) + } + + // 키가 없으면 새로 생성, 있으면 가져오기 + private fun getSecretKey(): SecretKey { + val existingKey = keyStore.getEntry(KEY_ALIAS, null) as? KeyStore.SecretKeyEntry + return existingKey?.secretKey ?: createKey() + } + + @SuppressLint("NewApi") + private fun createKey(): SecretKey { + return KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore").apply { + init( + KeyGenParameterSpec.Builder( + KEY_ALIAS, + KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT + ) + .setBlockModes(KeyProperties.BLOCK_MODE_GCM) + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) + .build() + ) + }.generateKey() + } + + // 암호화: IV + Encrypted Data를 합쳐서 반환 + @SuppressLint("NewApi") + fun encrypt(text: String): String { + val cipher = Cipher.getInstance(ALGORITHM) + cipher.init(Cipher.ENCRYPT_MODE, getSecretKey()) + val encryptedBytes = cipher.doFinal(text.toByteArray()) + + // IV(초기화 벡터)와 암호문을 합쳐서 Base64로 인코딩 + val combined = cipher.iv + encryptedBytes + return Base64.encodeToString(combined, Base64.NO_WRAP) + } + + // 복호화 + @SuppressLint("NewApi") + fun decrypt(encryptedText: String): String { + val combined = Base64.decode(encryptedText, Base64.NO_WRAP) + val iv = combined.sliceArray(0 until 12) // GCM IV 기본 길이는 12바이트 + val encryptedData = combined.sliceArray(12 until combined.size) + + val cipher = Cipher.getInstance(ALGORITHM) + val spec = GCMParameterSpec(128, iv) + cipher.init(Cipher.DECRYPT_MODE, getSecretKey(), spec) + + return String(cipher.doFinal(encryptedData)) + } +} \ No newline at end of file diff --git a/core/data-api/build.gradle.kts b/core/data-api/build.gradle.kts index ed411b848..8ff0f0e1f 100644 --- a/core/data-api/build.gradle.kts +++ b/core/data-api/build.gradle.kts @@ -4,4 +4,6 @@ plugins { dependencies { implementation(projects.core.model) + implementation(libs.kotlinx.coroutines.core) + } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2b17f43a6..0db4249f8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,6 +17,8 @@ jetbrainsKotlinJvmVersion = "2.1.0" hilt = "2.51.1" ktor = "2.3.12" androidxDatastore = "1.1.2" +kotlinxCoroutines = "1.8.1" +securityCrypto = "1.1.0" timber = "5.0.1" [libraries] @@ -36,6 +38,7 @@ androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-te androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" } androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } +kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "kotlinxCoroutines" } kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" } hilt-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" } @@ -49,6 +52,7 @@ androidx-annotation-experimental = { module = "androidx.annotation:annotation-ex androidx-datastore-core = { group = "androidx.datastore", name = "datastore", version.ref = "androidxDatastore" } androidx-datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "androidxDatastore" } +androidx-security-crypto = { module = "androidx.security:security-crypto", version.ref = "securityCrypto" } timber = { group = "com.jakewharton.timber", name = "timber", version.ref = "timber" } From 5164727d61d10035931dfe0cd43e66adfee901f3 Mon Sep 17 00:00:00 2001 From: Ojongseok Date: Mon, 29 Dec 2025 08:44:06 +0900 Subject: [PATCH 03/19] =?UTF-8?q?[chore]=20feature=20=EB=8B=A8=EC=9C=84=20?= =?UTF-8?q?Repository=20=EA=B5=AC=ED=98=84=EC=B2=B4=20=EB=82=B4=20dataStor?= =?UTF-8?q?e=20=EC=A3=BC=EC=9E=85=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../android/core/data/repository/impl/SampleRepositoryImpl.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/core/data/src/main/java/com/neki/android/core/data/repository/impl/SampleRepositoryImpl.kt b/core/data/src/main/java/com/neki/android/core/data/repository/impl/SampleRepositoryImpl.kt index 9e257fb62..0534cffbe 100644 --- a/core/data/src/main/java/com/neki/android/core/data/repository/impl/SampleRepositoryImpl.kt +++ b/core/data/src/main/java/com/neki/android/core/data/repository/impl/SampleRepositoryImpl.kt @@ -9,7 +9,6 @@ import javax.inject.Inject class SampleRepositoryImpl @Inject constructor( private val apiService: ApiService, - private val dataStore: DataStore ): SampleRepository { override suspend fun getPosts(): List { return apiService.getPosts() From 17badae507884aafe9f737afee06ae253cc02033 Mon Sep 17 00:00:00 2001 From: Ojongseok Date: Mon, 29 Dec 2025 08:52:39 +0900 Subject: [PATCH 04/19] =?UTF-8?q?[chore]=20CryptoManager.kt=20=EB=B6=88?= =?UTF-8?q?=ED=95=84=EC=9A=94=ED=95=9C=20=EC=A3=BC=EC=84=9D=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../neki/android/core/common/crypto/CryptoManager.kt | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/core/common/src/main/java/com/neki/android/core/common/crypto/CryptoManager.kt b/core/common/src/main/java/com/neki/android/core/common/crypto/CryptoManager.kt index f94d06467..f216e0758 100644 --- a/core/common/src/main/java/com/neki/android/core/common/crypto/CryptoManager.kt +++ b/core/common/src/main/java/com/neki/android/core/common/crypto/CryptoManager.kt @@ -12,13 +12,12 @@ import javax.crypto.spec.GCMParameterSpec object CryptoManager { private const val ALGORITHM = "AES/GCM/NoPadding" - private const val KEY_ALIAS = "token_encryption_key" + private const val KEY_ALIAS = "neki_token_encryption_key" private val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) } - // 키가 없으면 새로 생성, 있으면 가져오기 private fun getSecretKey(): SecretKey { val existingKey = keyStore.getEntry(KEY_ALIAS, null) as? KeyStore.SecretKeyEntry return existingKey?.secretKey ?: createKey() @@ -39,26 +38,24 @@ object CryptoManager { }.generateKey() } - // 암호화: IV + Encrypted Data를 합쳐서 반환 @SuppressLint("NewApi") fun encrypt(text: String): String { val cipher = Cipher.getInstance(ALGORITHM) cipher.init(Cipher.ENCRYPT_MODE, getSecretKey()) - val encryptedBytes = cipher.doFinal(text.toByteArray()) - // IV(초기화 벡터)와 암호문을 합쳐서 Base64로 인코딩 + val encryptedBytes = cipher.doFinal(text.toByteArray()) val combined = cipher.iv + encryptedBytes + return Base64.encodeToString(combined, Base64.NO_WRAP) } - // 복호화 @SuppressLint("NewApi") fun decrypt(encryptedText: String): String { val combined = Base64.decode(encryptedText, Base64.NO_WRAP) - val iv = combined.sliceArray(0 until 12) // GCM IV 기본 길이는 12바이트 val encryptedData = combined.sliceArray(12 until combined.size) val cipher = Cipher.getInstance(ALGORITHM) + val iv = combined.sliceArray(0 until 12) val spec = GCMParameterSpec(128, iv) cipher.init(Cipher.DECRYPT_MODE, getSecretKey(), spec) From 33f276f982160b34e62faa98f8b532b359e12650 Mon Sep 17 00:00:00 2001 From: Ojongseok Date: Thu, 1 Jan 2026 11:01:07 +0900 Subject: [PATCH 05/19] =?UTF-8?q?[chore]=20RepositoryModule=20=EC=84=A0?= =?UTF-8?q?=EC=96=B8=EB=B6=80=20interface=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../android/core/data/repository/di/RepositoryModule.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/data/src/main/java/com/neki/android/core/data/repository/di/RepositoryModule.kt b/core/data/src/main/java/com/neki/android/core/data/repository/di/RepositoryModule.kt index d59620aa0..1a8e48f9b 100644 --- a/core/data/src/main/java/com/neki/android/core/data/repository/di/RepositoryModule.kt +++ b/core/data/src/main/java/com/neki/android/core/data/repository/di/RepositoryModule.kt @@ -12,17 +12,17 @@ import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) -internal abstract class RepositoryModule { +internal interface RepositoryModule { @Binds @Singleton - abstract fun bindSampleRepositoryImpl( + fun bindSampleRepositoryImpl( sampleRepositoryImpl: SampleRepositoryImpl ): SampleRepository @Binds @Singleton - abstract fun bindDataStoreRepositoryImpl( + fun bindDataStoreRepositoryImpl( dataStoreRepositoryImpl: DataStoreRepositoryImpl ): DataStoreRepository From b7b54199810dead8c855b18c8376c9ddf76413ab Mon Sep 17 00:00:00 2001 From: Ojongseok Date: Thu, 1 Jan 2026 11:29:36 +0900 Subject: [PATCH 06/19] =?UTF-8?q?[chore]=20#4=20RunSuspendCatching=20?= =?UTF-8?q?=ED=99=95=EC=9E=A5=ED=95=A8=EC=88=98=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/data/util/RunSuspendCatching.kt | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 core/data/src/main/java/com/neki/android/core/data/util/RunSuspendCatching.kt diff --git a/core/data/src/main/java/com/neki/android/core/data/util/RunSuspendCatching.kt b/core/data/src/main/java/com/neki/android/core/data/util/RunSuspendCatching.kt new file mode 100644 index 000000000..f2772a438 --- /dev/null +++ b/core/data/src/main/java/com/neki/android/core/data/util/RunSuspendCatching.kt @@ -0,0 +1,20 @@ +package com.neki.android.core.data.util + +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract +import kotlin.coroutines.cancellation.CancellationException + +@OptIn(ExperimentalContracts::class) +internal inline fun runSuspendCatching(block: () -> T): Result { + // Kotlin 의 contract(계약) 시스템을 이용해 block 이 정확히 한번만 호출 되어야 함을 나타냄 + contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } + + return runCatching(block).also { result -> + // 만약 람다에서 예외가 발생하면, Result 객체는 실패를 나타내고 해당 예외를 포함, 추가적인 작업을 실행 + val maybeException = result.exceptionOrNull() + // 만약 예외가 CancellationException 이면 예외를 던져 코루틴 계층 구조에 따라 상위 코루틴까지 취소 신호를 전파 + // 이를 통해, 상위 코루틴에서 적절한 예외 처리 루틴을 수행할 수 있음 + if (maybeException is CancellationException) throw maybeException + } +} \ No newline at end of file From 992dadd9928408e88808f52d4c15fbe679bc2450 Mon Sep 17 00:00:00 2001 From: Ojongseok Date: Mon, 29 Dec 2025 08:41:43 +0900 Subject: [PATCH 07/19] =?UTF-8?q?[chore]=20DataStoreRepository.kt=20?= =?UTF-8?q?=EB=B0=8F=20DataStoreModule.kt=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dataapi/repository/DataStoreRepository.kt | 13 +++++ ...aStoreRepository.kt => DataStoreModule.kt} | 0 .../data/repository/di/RepositoryModule.kt | 14 +++++- .../impl/DataStoreRepositoryImpl.kt | 48 +++++++++++++++++++ 4 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 core/data-api/src/main/java/com/neki/android/core/dataapi/repository/DataStoreRepository.kt rename core/data/src/main/java/com/neki/android/core/data/local/di/{DataStoreRepository.kt => DataStoreModule.kt} (100%) create mode 100644 core/data/src/main/java/com/neki/android/core/data/repository/impl/DataStoreRepositoryImpl.kt diff --git a/core/data-api/src/main/java/com/neki/android/core/dataapi/repository/DataStoreRepository.kt b/core/data-api/src/main/java/com/neki/android/core/dataapi/repository/DataStoreRepository.kt new file mode 100644 index 000000000..e8831db1a --- /dev/null +++ b/core/data-api/src/main/java/com/neki/android/core/dataapi/repository/DataStoreRepository.kt @@ -0,0 +1,13 @@ +package com.neki.android.core.dataapi.repository + +import kotlinx.coroutines.flow.Flow + +interface DataStoreRepository { + suspend fun saveTokens( + accessToken: String, + refreshToken: String + ) + fun getAccessToken(): Flow + fun getRefreshToken(): Flow + suspend fun clearTokens() +} \ No newline at end of file diff --git a/core/data/src/main/java/com/neki/android/core/data/local/di/DataStoreRepository.kt b/core/data/src/main/java/com/neki/android/core/data/local/di/DataStoreModule.kt similarity index 100% rename from core/data/src/main/java/com/neki/android/core/data/local/di/DataStoreRepository.kt rename to core/data/src/main/java/com/neki/android/core/data/local/di/DataStoreModule.kt diff --git a/core/data/src/main/java/com/neki/android/core/data/repository/di/RepositoryModule.kt b/core/data/src/main/java/com/neki/android/core/data/repository/di/RepositoryModule.kt index 5c3686db4..7c00ff063 100644 --- a/core/data/src/main/java/com/neki/android/core/data/repository/di/RepositoryModule.kt +++ b/core/data/src/main/java/com/neki/android/core/data/repository/di/RepositoryModule.kt @@ -1,18 +1,30 @@ package com.neki.android.core.data.repository.di +import com.neki.android.core.data.repository.impl.DataStoreRepositoryImpl import com.neki.android.core.data.repository.impl.SampleRepositoryImpl +import com.neki.android.core.dataapi.repository.DataStoreRepository import com.neki.android.core.dataapi.repository.SampleRepository 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 bindSampleRepositoryImpl( - sampleRepositoryImpl: SampleRepositoryImpl, + sampleRepositoryImpl: SampleRepositoryImpl ): SampleRepository + + @Binds + @Singleton + abstract fun bindDataStoreRepositoryImpl( + dataStoreRepositoryImpl: DataStoreRepositoryImpl + ): DataStoreRepository + + } diff --git a/core/data/src/main/java/com/neki/android/core/data/repository/impl/DataStoreRepositoryImpl.kt b/core/data/src/main/java/com/neki/android/core/data/repository/impl/DataStoreRepositoryImpl.kt new file mode 100644 index 000000000..6a5aa5fa9 --- /dev/null +++ b/core/data/src/main/java/com/neki/android/core/data/repository/impl/DataStoreRepositoryImpl.kt @@ -0,0 +1,48 @@ +package com.neki.android.core.data.repository.impl + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringPreferencesKey +import com.neki.android.core.common.crypto.CryptoManager +import com.neki.android.core.dataapi.repository.DataStoreRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +class DataStoreRepositoryImpl @Inject constructor( + private val dataStore: DataStore +): DataStoreRepository { + companion object { + private val ACCESS_TOKEN = stringPreferencesKey("access_token") + private val REFRESH_TOKEN = stringPreferencesKey("refresh_token") + } + + override suspend fun saveTokens( + accessToken: String, + refreshToken: String + ) { + dataStore.edit { preferences -> + preferences[ACCESS_TOKEN] = CryptoManager.encrypt(accessToken) + preferences[REFRESH_TOKEN] = CryptoManager.encrypt(refreshToken) + } + } + + override fun getAccessToken(): Flow { + return dataStore.data.map { preferences -> + preferences[ACCESS_TOKEN]?.let { CryptoManager.decrypt(it) } +// preferences[ACCESS_TOKEN] + } + } + + override fun getRefreshToken(): Flow { + return dataStore.data.map { preferences -> + preferences[REFRESH_TOKEN]?.let { CryptoManager.decrypt(it) } + } + } + + override suspend fun clearTokens() { + dataStore.edit { it.clear() } + } + +} \ No newline at end of file From 27371e947c154b5cadd1453a168c72ff18d24dd9 Mon Sep 17 00:00:00 2001 From: Ojongseok Date: Mon, 29 Dec 2025 08:42:11 +0900 Subject: [PATCH 08/19] =?UTF-8?q?[chore]=20DataStore=20=EC=95=94=ED=98=B8?= =?UTF-8?q?=ED=99=94/=EB=B3=B5=ED=98=B8=ED=99=94=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EA=B5=AC=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/common/build.gradle.kts | 1 + .../core/common/crypto/CryptoManager.kt | 67 +++++++++++++++++++ core/data-api/build.gradle.kts | 2 + gradle/libs.versions.toml | 2 + 4 files changed, 72 insertions(+) create mode 100644 core/common/src/main/java/com/neki/android/core/common/crypto/CryptoManager.kt diff --git a/core/common/build.gradle.kts b/core/common/build.gradle.kts index 8250c3e29..7a26d84e2 100644 --- a/core/common/build.gradle.kts +++ b/core/common/build.gradle.kts @@ -9,5 +9,6 @@ android { dependencies { api(libs.timber) + implementation(libs.androidx.security.crypto) } \ No newline at end of file diff --git a/core/common/src/main/java/com/neki/android/core/common/crypto/CryptoManager.kt b/core/common/src/main/java/com/neki/android/core/common/crypto/CryptoManager.kt new file mode 100644 index 000000000..f94d06467 --- /dev/null +++ b/core/common/src/main/java/com/neki/android/core/common/crypto/CryptoManager.kt @@ -0,0 +1,67 @@ +package com.neki.android.core.common.crypto + +import android.annotation.SuppressLint +import android.security.keystore.KeyGenParameterSpec +import android.security.keystore.KeyProperties +import android.util.Base64 +import java.security.KeyStore +import javax.crypto.Cipher +import javax.crypto.KeyGenerator +import javax.crypto.SecretKey +import javax.crypto.spec.GCMParameterSpec + +object CryptoManager { + private const val ALGORITHM = "AES/GCM/NoPadding" + private const val KEY_ALIAS = "token_encryption_key" + + private val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { + load(null) + } + + // 키가 없으면 새로 생성, 있으면 가져오기 + private fun getSecretKey(): SecretKey { + val existingKey = keyStore.getEntry(KEY_ALIAS, null) as? KeyStore.SecretKeyEntry + return existingKey?.secretKey ?: createKey() + } + + @SuppressLint("NewApi") + private fun createKey(): SecretKey { + return KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore").apply { + init( + KeyGenParameterSpec.Builder( + KEY_ALIAS, + KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT + ) + .setBlockModes(KeyProperties.BLOCK_MODE_GCM) + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) + .build() + ) + }.generateKey() + } + + // 암호화: IV + Encrypted Data를 합쳐서 반환 + @SuppressLint("NewApi") + fun encrypt(text: String): String { + val cipher = Cipher.getInstance(ALGORITHM) + cipher.init(Cipher.ENCRYPT_MODE, getSecretKey()) + val encryptedBytes = cipher.doFinal(text.toByteArray()) + + // IV(초기화 벡터)와 암호문을 합쳐서 Base64로 인코딩 + val combined = cipher.iv + encryptedBytes + return Base64.encodeToString(combined, Base64.NO_WRAP) + } + + // 복호화 + @SuppressLint("NewApi") + fun decrypt(encryptedText: String): String { + val combined = Base64.decode(encryptedText, Base64.NO_WRAP) + val iv = combined.sliceArray(0 until 12) // GCM IV 기본 길이는 12바이트 + val encryptedData = combined.sliceArray(12 until combined.size) + + val cipher = Cipher.getInstance(ALGORITHM) + val spec = GCMParameterSpec(128, iv) + cipher.init(Cipher.DECRYPT_MODE, getSecretKey(), spec) + + return String(cipher.doFinal(encryptedData)) + } +} \ No newline at end of file diff --git a/core/data-api/build.gradle.kts b/core/data-api/build.gradle.kts index ed411b848..8ff0f0e1f 100644 --- a/core/data-api/build.gradle.kts +++ b/core/data-api/build.gradle.kts @@ -4,4 +4,6 @@ plugins { dependencies { implementation(projects.core.model) + implementation(libs.kotlinx.coroutines.core) + } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ea615c9de..63d3e48be 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,6 +17,8 @@ jetbrainsKotlinJvmVersion = "2.1.0" hilt = "2.54" ktor = "2.3.12" androidxDatastore = "1.1.2" +kotlinxCoroutines = "1.8.1" +securityCrypto = "1.1.0" timber = "5.0.1" androidxNavigation3 = "1.0.0" androidxLifecycleViewModelNavigation3 = "2.10.0" From 722b61bbaa453a8753fb706a635bfa95fd93798a Mon Sep 17 00:00:00 2001 From: Ojongseok Date: Mon, 29 Dec 2025 08:52:39 +0900 Subject: [PATCH 09/19] =?UTF-8?q?[chore]=20CryptoManager.kt=20=EB=B6=88?= =?UTF-8?q?=ED=95=84=EC=9A=94=ED=95=9C=20=EC=A3=BC=EC=84=9D=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../neki/android/core/common/crypto/CryptoManager.kt | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/core/common/src/main/java/com/neki/android/core/common/crypto/CryptoManager.kt b/core/common/src/main/java/com/neki/android/core/common/crypto/CryptoManager.kt index f94d06467..f216e0758 100644 --- a/core/common/src/main/java/com/neki/android/core/common/crypto/CryptoManager.kt +++ b/core/common/src/main/java/com/neki/android/core/common/crypto/CryptoManager.kt @@ -12,13 +12,12 @@ import javax.crypto.spec.GCMParameterSpec object CryptoManager { private const val ALGORITHM = "AES/GCM/NoPadding" - private const val KEY_ALIAS = "token_encryption_key" + private const val KEY_ALIAS = "neki_token_encryption_key" private val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) } - // 키가 없으면 새로 생성, 있으면 가져오기 private fun getSecretKey(): SecretKey { val existingKey = keyStore.getEntry(KEY_ALIAS, null) as? KeyStore.SecretKeyEntry return existingKey?.secretKey ?: createKey() @@ -39,26 +38,24 @@ object CryptoManager { }.generateKey() } - // 암호화: IV + Encrypted Data를 합쳐서 반환 @SuppressLint("NewApi") fun encrypt(text: String): String { val cipher = Cipher.getInstance(ALGORITHM) cipher.init(Cipher.ENCRYPT_MODE, getSecretKey()) - val encryptedBytes = cipher.doFinal(text.toByteArray()) - // IV(초기화 벡터)와 암호문을 합쳐서 Base64로 인코딩 + val encryptedBytes = cipher.doFinal(text.toByteArray()) val combined = cipher.iv + encryptedBytes + return Base64.encodeToString(combined, Base64.NO_WRAP) } - // 복호화 @SuppressLint("NewApi") fun decrypt(encryptedText: String): String { val combined = Base64.decode(encryptedText, Base64.NO_WRAP) - val iv = combined.sliceArray(0 until 12) // GCM IV 기본 길이는 12바이트 val encryptedData = combined.sliceArray(12 until combined.size) val cipher = Cipher.getInstance(ALGORITHM) + val iv = combined.sliceArray(0 until 12) val spec = GCMParameterSpec(128, iv) cipher.init(Cipher.DECRYPT_MODE, getSecretKey(), spec) From 8db2485b151f1d3fd5c891aa935c7ce67eeb4f57 Mon Sep 17 00:00:00 2001 From: Ojongseok Date: Thu, 1 Jan 2026 11:01:07 +0900 Subject: [PATCH 10/19] =?UTF-8?q?[chore]=20RepositoryModule=20=EC=84=A0?= =?UTF-8?q?=EC=96=B8=EB=B6=80=20interface=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../android/core/data/repository/di/RepositoryModule.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/data/src/main/java/com/neki/android/core/data/repository/di/RepositoryModule.kt b/core/data/src/main/java/com/neki/android/core/data/repository/di/RepositoryModule.kt index 7c00ff063..c4706838c 100644 --- a/core/data/src/main/java/com/neki/android/core/data/repository/di/RepositoryModule.kt +++ b/core/data/src/main/java/com/neki/android/core/data/repository/di/RepositoryModule.kt @@ -12,17 +12,17 @@ import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) -internal abstract class RepositoryModule { +internal interface RepositoryModule { @Binds @Singleton - abstract fun bindSampleRepositoryImpl( + fun bindSampleRepositoryImpl( sampleRepositoryImpl: SampleRepositoryImpl ): SampleRepository @Binds @Singleton - abstract fun bindDataStoreRepositoryImpl( + fun bindDataStoreRepositoryImpl( dataStoreRepositoryImpl: DataStoreRepositoryImpl ): DataStoreRepository From ba03dcaebe086d8f3723927d936d1bd752b56609 Mon Sep 17 00:00:00 2001 From: Ojongseok Date: Thu, 1 Jan 2026 11:29:36 +0900 Subject: [PATCH 11/19] =?UTF-8?q?[chore]=20#4=20RunSuspendCatching=20?= =?UTF-8?q?=ED=99=95=EC=9E=A5=ED=95=A8=EC=88=98=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/data/util/RunSuspendCatching.kt | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 core/data/src/main/java/com/neki/android/core/data/util/RunSuspendCatching.kt diff --git a/core/data/src/main/java/com/neki/android/core/data/util/RunSuspendCatching.kt b/core/data/src/main/java/com/neki/android/core/data/util/RunSuspendCatching.kt new file mode 100644 index 000000000..f2772a438 --- /dev/null +++ b/core/data/src/main/java/com/neki/android/core/data/util/RunSuspendCatching.kt @@ -0,0 +1,20 @@ +package com.neki.android.core.data.util + +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract +import kotlin.coroutines.cancellation.CancellationException + +@OptIn(ExperimentalContracts::class) +internal inline fun runSuspendCatching(block: () -> T): Result { + // Kotlin 의 contract(계약) 시스템을 이용해 block 이 정확히 한번만 호출 되어야 함을 나타냄 + contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } + + return runCatching(block).also { result -> + // 만약 람다에서 예외가 발생하면, Result 객체는 실패를 나타내고 해당 예외를 포함, 추가적인 작업을 실행 + val maybeException = result.exceptionOrNull() + // 만약 예외가 CancellationException 이면 예외를 던져 코루틴 계층 구조에 따라 상위 코루틴까지 취소 신호를 전파 + // 이를 통해, 상위 코루틴에서 적절한 예외 처리 루틴을 수행할 수 있음 + if (maybeException is CancellationException) throw maybeException + } +} \ No newline at end of file From ba9c1fdd10022628a7e1a2ded799e92525dcfbe2 Mon Sep 17 00:00:00 2001 From: Ojongseok Date: Fri, 2 Jan 2026 12:20:41 +0900 Subject: [PATCH 12/19] =?UTF-8?q?[chore]=20#4=20develop=20=EB=B8=8C?= =?UTF-8?q?=EB=9E=9C=EC=B9=98=20=EB=B3=91=ED=95=A9=20=EA=B3=BC=EC=A0=95=20?= =?UTF-8?q?=EC=A4=91=20conflict=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../android/core/data/repository/impl/SampleRepositoryImpl.kt | 1 - gradle/libs.versions.toml | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/core/data/src/main/java/com/neki/android/core/data/repository/impl/SampleRepositoryImpl.kt b/core/data/src/main/java/com/neki/android/core/data/repository/impl/SampleRepositoryImpl.kt index bcafcc53f..d40ba5ef5 100644 --- a/core/data/src/main/java/com/neki/android/core/data/repository/impl/SampleRepositoryImpl.kt +++ b/core/data/src/main/java/com/neki/android/core/data/repository/impl/SampleRepositoryImpl.kt @@ -7,7 +7,6 @@ import javax.inject.Inject class SampleRepositoryImpl @Inject constructor( private val apiService: ApiService, -// private val dataStore: DataStore, ) : SampleRepository { override suspend fun getPosts(): List { return apiService.getPosts() diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 63d3e48be..bc5cc4e08 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -47,6 +47,7 @@ androidx-navigation3-runtime = { group = "androidx.navigation3", name = "navigat androidx-navigation3-ui = { group = "androidx.navigation3", name = "navigation3-ui", version.ref = "androidxNavigation3" } androidx-lifecycle-viewModel-navigation3 = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-navigation3", version.ref = "androidxLifecycleViewModelNavigation3" } +kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "kotlinxCoroutines" } kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" } hilt-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" } @@ -61,6 +62,7 @@ androidx-annotation-experimental = { module = "androidx.annotation:annotation-ex androidx-datastore-core = { group = "androidx.datastore", name = "datastore", version.ref = "androidxDatastore" } androidx-datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "androidxDatastore" } +androidx-security-crypto = { module = "androidx.security:security-crypto", version.ref = "securityCrypto" } timber = { group = "com.jakewharton.timber", name = "timber", version.ref = "timber" } From e61e08e7816db29fd321ef0eb5ee88056d286e54 Mon Sep 17 00:00:00 2001 From: Ojongseok Date: Sat, 3 Jan 2026 12:14:03 +0900 Subject: [PATCH 13/19] =?UTF-8?q?[chore]=20#4=20:feature:sample=20?= =?UTF-8?q?=EB=AA=A8=EB=93=88=20=EC=82=AD=EC=A0=9C=20=EB=B0=8F=20=EC=9D=98?= =?UTF-8?q?=EC=A1=B4=EC=84=B1=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 4 +-- feature/sample/api/.gitignore | 1 - feature/sample/api/build.gradle.kts | 7 ---- .../feature/sample/api/SampleNavKey.kt | 15 -------- feature/sample/impl/.gitignore | 1 - feature/sample/impl/build.gradle.kts | 14 -------- .../sample/impl/SampleEntryProvider.kt | 36 ------------------- .../feature/sample/impl/SampleScreen.kt | 18 ---------- .../feature/sample/impl/SampleViewModel.kt | 21 ----------- settings.gradle.kts | 2 -- 10 files changed, 1 insertion(+), 118 deletions(-) delete mode 100644 feature/sample/api/.gitignore delete mode 100644 feature/sample/api/build.gradle.kts delete mode 100644 feature/sample/api/src/main/java/com/neki/android/feature/sample/api/SampleNavKey.kt delete mode 100644 feature/sample/impl/.gitignore delete mode 100644 feature/sample/impl/build.gradle.kts delete mode 100644 feature/sample/impl/src/main/java/com/neki/android/feature/sample/impl/SampleEntryProvider.kt delete mode 100644 feature/sample/impl/src/main/java/com/neki/android/feature/sample/impl/SampleScreen.kt delete mode 100644 feature/sample/impl/src/main/java/com/neki/android/feature/sample/impl/SampleViewModel.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a5644595c..a6ce4e1a8 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -19,8 +19,6 @@ dependencies { implementation(projects.core.domain) implementation(projects.core.model) implementation(projects.core.navigation) - implementation(projects.feature.sample.impl) - implementation(projects.feature.sample.api) implementation(projects.feature.pose.api) implementation(projects.feature.pose.impl) implementation(projects.feature.archive.api) @@ -32,4 +30,4 @@ dependencies { implementation(libs.androidx.activity.compose) implementation(libs.androidx.navigation3.ui) -} \ No newline at end of file +} diff --git a/feature/sample/api/.gitignore b/feature/sample/api/.gitignore deleted file mode 100644 index 42afabfd2..000000000 --- a/feature/sample/api/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/feature/sample/api/build.gradle.kts b/feature/sample/api/build.gradle.kts deleted file mode 100644 index 128f6619a..000000000 --- a/feature/sample/api/build.gradle.kts +++ /dev/null @@ -1,7 +0,0 @@ -plugins { - alias(libs.plugins.neki.android.feature.api) -} - -android { - namespace = "com.neki.android.feature.sample.api" -} \ No newline at end of file diff --git a/feature/sample/api/src/main/java/com/neki/android/feature/sample/api/SampleNavKey.kt b/feature/sample/api/src/main/java/com/neki/android/feature/sample/api/SampleNavKey.kt deleted file mode 100644 index c5a5d3763..000000000 --- a/feature/sample/api/src/main/java/com/neki/android/feature/sample/api/SampleNavKey.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.neki.android.feature.sample.api - -import androidx.navigation3.runtime.NavKey -import com.neki.android.core.navigation.Navigator -import kotlinx.serialization.Serializable - -sealed interface SampleNavKey : NavKey { - - @Serializable - data class Sample(val id: Long) : SampleNavKey -} - -fun Navigator.navigateToSample(id: Long) { - navigate(SampleNavKey.Sample(id)) -} diff --git a/feature/sample/impl/.gitignore b/feature/sample/impl/.gitignore deleted file mode 100644 index 42afabfd2..000000000 --- a/feature/sample/impl/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/feature/sample/impl/build.gradle.kts b/feature/sample/impl/build.gradle.kts deleted file mode 100644 index 17361a44a..000000000 --- a/feature/sample/impl/build.gradle.kts +++ /dev/null @@ -1,14 +0,0 @@ -plugins { - alias(libs.plugins.neki.android.feature.impl) -} - -android { - namespace = "com.neki.android.feature.sample.impl" -} - -dependencies { - implementation(libs.androidx.activity.compose) - implementation(libs.androidx.appcompat) - implementation(libs.androidx.core.ktx) - implementation(projects.feature.sample.api) -} \ No newline at end of file diff --git a/feature/sample/impl/src/main/java/com/neki/android/feature/sample/impl/SampleEntryProvider.kt b/feature/sample/impl/src/main/java/com/neki/android/feature/sample/impl/SampleEntryProvider.kt deleted file mode 100644 index 9adf4e256..000000000 --- a/feature/sample/impl/src/main/java/com/neki/android/feature/sample/impl/SampleEntryProvider.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.neki.android.feature.sample.impl - -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel -import androidx.navigation3.runtime.EntryProviderScope -import androidx.navigation3.runtime.NavKey -import com.neki.android.core.navigation.EntryProviderInstaller -import com.neki.android.core.navigation.Navigator -import com.neki.android.feature.sample.api.SampleNavKey -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.components.ActivityRetainedComponent -import dagger.multibindings.IntoSet - -@Module -@InstallIn(ActivityRetainedComponent::class) -object SampleEntryProviderModule { - - @IntoSet - @Provides - fun provideSampleEntryBuilder(navigator: Navigator): EntryProviderInstaller = { - sampleEntry(navigator) - } -} - -private fun EntryProviderScope.sampleEntry(navigator: Navigator) { - entry { key -> - navigator - val viewModel = hiltViewModel( - creationCallback = { factory -> - factory.create(key) - }, - ) - SampleScreen(viewModel = viewModel) - } -} diff --git a/feature/sample/impl/src/main/java/com/neki/android/feature/sample/impl/SampleScreen.kt b/feature/sample/impl/src/main/java/com/neki/android/feature/sample/impl/SampleScreen.kt deleted file mode 100644 index fc24a677b..000000000 --- a/feature/sample/impl/src/main/java/com/neki/android/feature/sample/impl/SampleScreen.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.neki.android.feature.sample.impl - -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel - -@Composable -fun SampleScreen( - modifier: Modifier = Modifier, - viewModel: SampleViewModel = hiltViewModel(), -) { - viewModel - Text( - modifier = modifier, - text = "Sample", - ) -} diff --git a/feature/sample/impl/src/main/java/com/neki/android/feature/sample/impl/SampleViewModel.kt b/feature/sample/impl/src/main/java/com/neki/android/feature/sample/impl/SampleViewModel.kt deleted file mode 100644 index 91021e222..000000000 --- a/feature/sample/impl/src/main/java/com/neki/android/feature/sample/impl/SampleViewModel.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.neki.android.feature.sample.impl - -import androidx.lifecycle.ViewModel -import com.neki.android.feature.sample.api.SampleNavKey -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import dagger.hilt.android.lifecycle.HiltViewModel - -@HiltViewModel(assistedFactory = SampleViewModel.Factory::class) -class SampleViewModel @AssistedInject constructor( - @Assisted val navKey: SampleNavKey.Sample, -) : ViewModel() { - - val id = navKey.id - - @AssistedFactory - interface Factory { - fun create(navKey: SampleNavKey.Sample): SampleViewModel - } -} diff --git a/settings.gradle.kts b/settings.gradle.kts index 79c5f5252..7efad2502 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -30,8 +30,6 @@ include(":core:domain") include(":core:data") include(":core:data-api") include(":core:model") -include(":feature:sample:api") -include(":feature:sample:impl") include(":core:navigation") include(":feature:pose:api") include(":feature:pose:impl") From b3553f9ef4c226e48c6d6e924ebc4d6a2c34db14 Mon Sep 17 00:00:00 2001 From: Ojongseok Date: Sat, 3 Jan 2026 22:06:31 +0900 Subject: [PATCH 14/19] =?UTF-8?q?[chore]=20#4=20:=20Repository=20=EB=B0=98?= =?UTF-8?q?=ED=99=98=20=ED=83=80=EC=9E=85=20Result=20=EA=B0=9D=EC=B2=B4?= =?UTF-8?q?=EB=A1=9C=20=EB=A7=A4=ED=95=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/dataapi/repository/SampleRespository.kt | 6 ++---- .../data/repository/impl/SampleRepositoryImpl.kt | 13 +++++-------- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/core/data-api/src/main/java/com/neki/android/core/dataapi/repository/SampleRespository.kt b/core/data-api/src/main/java/com/neki/android/core/dataapi/repository/SampleRespository.kt index 4ce138ea9..f44a2fad0 100644 --- a/core/data-api/src/main/java/com/neki/android/core/dataapi/repository/SampleRespository.kt +++ b/core/data-api/src/main/java/com/neki/android/core/dataapi/repository/SampleRespository.kt @@ -3,8 +3,6 @@ package com.neki.android.core.dataapi.repository import com.neki.android.core.model.Post interface SampleRepository { - suspend fun getPosts(): List - suspend fun getPost( - id: Int, - ): Post + suspend fun getPosts(): Result> + suspend fun getPost(id: Int): Result } diff --git a/core/data/src/main/java/com/neki/android/core/data/repository/impl/SampleRepositoryImpl.kt b/core/data/src/main/java/com/neki/android/core/data/repository/impl/SampleRepositoryImpl.kt index d40ba5ef5..deb6c6a5f 100644 --- a/core/data/src/main/java/com/neki/android/core/data/repository/impl/SampleRepositoryImpl.kt +++ b/core/data/src/main/java/com/neki/android/core/data/repository/impl/SampleRepositoryImpl.kt @@ -1,6 +1,7 @@ package com.neki.android.core.data.repository.impl import com.neki.android.core.data.remote.api.ApiService +import com.neki.android.core.data.util.runSuspendCatching import com.neki.android.core.dataapi.repository.SampleRepository import com.neki.android.core.model.Post import javax.inject.Inject @@ -8,15 +9,11 @@ import javax.inject.Inject class SampleRepositoryImpl @Inject constructor( private val apiService: ApiService, ) : SampleRepository { - override suspend fun getPosts(): List { - return apiService.getPosts() - .map { it.toModel() } + override suspend fun getPosts(): Result> = runSuspendCatching { + apiService.getPosts().map { it.toModel() } } - override suspend fun getPost( - id: Int, - ): Post { - return apiService.getPost(id = id) - .toModel() + override suspend fun getPost(id: Int): Result = runSuspendCatching { + apiService.getPost(id).toModel() } } From dbb910d57a33cc63ccfaf43e095d6c4bb06039a4 Mon Sep 17 00:00:00 2001 From: Ojongseok Date: Sat, 3 Jan 2026 22:26:38 +0900 Subject: [PATCH 15/19] =?UTF-8?q?[chore]=20#4=20:=20Detekt=20=EB=A3=B0=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/dataapi/repository/DataStoreRepository.kt | 5 +++-- .../android/core/data/repository/di/RepositoryModule.kt | 4 ++-- .../core/data/repository/impl/DataStoreRepositoryImpl.kt | 8 ++++---- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/core/data-api/src/main/java/com/neki/android/core/dataapi/repository/DataStoreRepository.kt b/core/data-api/src/main/java/com/neki/android/core/dataapi/repository/DataStoreRepository.kt index e8831db1a..1e50840fb 100644 --- a/core/data-api/src/main/java/com/neki/android/core/dataapi/repository/DataStoreRepository.kt +++ b/core/data-api/src/main/java/com/neki/android/core/dataapi/repository/DataStoreRepository.kt @@ -5,9 +5,10 @@ import kotlinx.coroutines.flow.Flow interface DataStoreRepository { suspend fun saveTokens( accessToken: String, - refreshToken: String + refreshToken: String, ) + fun getAccessToken(): Flow fun getRefreshToken(): Flow suspend fun clearTokens() -} \ No newline at end of file +} diff --git a/core/data/src/main/java/com/neki/android/core/data/repository/di/RepositoryModule.kt b/core/data/src/main/java/com/neki/android/core/data/repository/di/RepositoryModule.kt index c4706838c..23e5539b0 100644 --- a/core/data/src/main/java/com/neki/android/core/data/repository/di/RepositoryModule.kt +++ b/core/data/src/main/java/com/neki/android/core/data/repository/di/RepositoryModule.kt @@ -17,13 +17,13 @@ internal interface RepositoryModule { @Binds @Singleton fun bindSampleRepositoryImpl( - sampleRepositoryImpl: SampleRepositoryImpl + sampleRepositoryImpl: SampleRepositoryImpl, ): SampleRepository @Binds @Singleton fun bindDataStoreRepositoryImpl( - dataStoreRepositoryImpl: DataStoreRepositoryImpl + dataStoreRepositoryImpl: DataStoreRepositoryImpl, ): DataStoreRepository diff --git a/core/data/src/main/java/com/neki/android/core/data/repository/impl/DataStoreRepositoryImpl.kt b/core/data/src/main/java/com/neki/android/core/data/repository/impl/DataStoreRepositoryImpl.kt index 6a5aa5fa9..a043ae018 100644 --- a/core/data/src/main/java/com/neki/android/core/data/repository/impl/DataStoreRepositoryImpl.kt +++ b/core/data/src/main/java/com/neki/android/core/data/repository/impl/DataStoreRepositoryImpl.kt @@ -11,8 +11,8 @@ import kotlinx.coroutines.flow.map import javax.inject.Inject class DataStoreRepositoryImpl @Inject constructor( - private val dataStore: DataStore -): DataStoreRepository { + private val dataStore: DataStore, +) : DataStoreRepository { companion object { private val ACCESS_TOKEN = stringPreferencesKey("access_token") private val REFRESH_TOKEN = stringPreferencesKey("refresh_token") @@ -20,7 +20,7 @@ class DataStoreRepositoryImpl @Inject constructor( override suspend fun saveTokens( accessToken: String, - refreshToken: String + refreshToken: String, ) { dataStore.edit { preferences -> preferences[ACCESS_TOKEN] = CryptoManager.encrypt(accessToken) @@ -45,4 +45,4 @@ class DataStoreRepositoryImpl @Inject constructor( dataStore.edit { it.clear() } } -} \ No newline at end of file +} From 2d0021754aff5248d214431df5ec8a0fab322fd6 Mon Sep 17 00:00:00 2001 From: Ojongseok Date: Sat, 3 Jan 2026 22:34:09 +0900 Subject: [PATCH 16/19] =?UTF-8?q?[chore]=20#4=20:=20detekt=20=EB=A3=B0=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9=20=EB=B0=8F=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EC=A3=BC=EC=84=9D=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/neki/android/core/common/crypto/CryptoManager.kt | 6 +++--- .../android/core/data/repository/di/RepositoryModule.kt | 2 -- .../core/data/repository/impl/DataStoreRepositoryImpl.kt | 2 -- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/core/common/src/main/java/com/neki/android/core/common/crypto/CryptoManager.kt b/core/common/src/main/java/com/neki/android/core/common/crypto/CryptoManager.kt index f216e0758..c18f5203f 100644 --- a/core/common/src/main/java/com/neki/android/core/common/crypto/CryptoManager.kt +++ b/core/common/src/main/java/com/neki/android/core/common/crypto/CryptoManager.kt @@ -29,11 +29,11 @@ object CryptoManager { init( KeyGenParameterSpec.Builder( KEY_ALIAS, - KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT + KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT, ) .setBlockModes(KeyProperties.BLOCK_MODE_GCM) .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) - .build() + .build(), ) }.generateKey() } @@ -61,4 +61,4 @@ object CryptoManager { return String(cipher.doFinal(encryptedData)) } -} \ No newline at end of file +} diff --git a/core/data/src/main/java/com/neki/android/core/data/repository/di/RepositoryModule.kt b/core/data/src/main/java/com/neki/android/core/data/repository/di/RepositoryModule.kt index 23e5539b0..e8258332d 100644 --- a/core/data/src/main/java/com/neki/android/core/data/repository/di/RepositoryModule.kt +++ b/core/data/src/main/java/com/neki/android/core/data/repository/di/RepositoryModule.kt @@ -25,6 +25,4 @@ internal interface RepositoryModule { fun bindDataStoreRepositoryImpl( dataStoreRepositoryImpl: DataStoreRepositoryImpl, ): DataStoreRepository - - } diff --git a/core/data/src/main/java/com/neki/android/core/data/repository/impl/DataStoreRepositoryImpl.kt b/core/data/src/main/java/com/neki/android/core/data/repository/impl/DataStoreRepositoryImpl.kt index a043ae018..49cbced43 100644 --- a/core/data/src/main/java/com/neki/android/core/data/repository/impl/DataStoreRepositoryImpl.kt +++ b/core/data/src/main/java/com/neki/android/core/data/repository/impl/DataStoreRepositoryImpl.kt @@ -31,7 +31,6 @@ class DataStoreRepositoryImpl @Inject constructor( override fun getAccessToken(): Flow { return dataStore.data.map { preferences -> preferences[ACCESS_TOKEN]?.let { CryptoManager.decrypt(it) } -// preferences[ACCESS_TOKEN] } } @@ -44,5 +43,4 @@ class DataStoreRepositoryImpl @Inject constructor( override suspend fun clearTokens() { dataStore.edit { it.clear() } } - } From 21ae92989578eb3adf07016d52a8b328e27706e1 Mon Sep 17 00:00:00 2001 From: Ojongseok Date: Sat, 3 Jan 2026 22:47:24 +0900 Subject: [PATCH 17/19] =?UTF-8?q?[chore]=20#4=20:=20RunSuspendCatching.kt?= =?UTF-8?q?=20detekt=20=EB=A3=B0=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/neki/android/core/data/util/RunSuspendCatching.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/data/src/main/java/com/neki/android/core/data/util/RunSuspendCatching.kt b/core/data/src/main/java/com/neki/android/core/data/util/RunSuspendCatching.kt index f2772a438..9fb0130c8 100644 --- a/core/data/src/main/java/com/neki/android/core/data/util/RunSuspendCatching.kt +++ b/core/data/src/main/java/com/neki/android/core/data/util/RunSuspendCatching.kt @@ -17,4 +17,4 @@ internal inline fun runSuspendCatching(block: () -> T): Result { // 이를 통해, 상위 코루틴에서 적절한 예외 처리 루틴을 수행할 수 있음 if (maybeException is CancellationException) throw maybeException } -} \ No newline at end of file +} From 182f30552a947cd73933b305af359577334ec1c4 Mon Sep 17 00:00:00 2001 From: Ojongseok Date: Sun, 4 Jan 2026 01:38:36 +0900 Subject: [PATCH 18/19] =?UTF-8?q?[chore]=20#4=20:=20SampleRepository,=20Sa?= =?UTF-8?q?mpleRepositoryImpl=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dataapi/repository/SampleRespository.kt | 8 -------- .../data/repository/di/RepositoryModule.kt | 9 +-------- .../repository/impl/SampleRepositoryImpl.kt | 19 ------------------- 3 files changed, 1 insertion(+), 35 deletions(-) delete mode 100644 core/data-api/src/main/java/com/neki/android/core/dataapi/repository/SampleRespository.kt delete mode 100644 core/data/src/main/java/com/neki/android/core/data/repository/impl/SampleRepositoryImpl.kt diff --git a/core/data-api/src/main/java/com/neki/android/core/dataapi/repository/SampleRespository.kt b/core/data-api/src/main/java/com/neki/android/core/dataapi/repository/SampleRespository.kt deleted file mode 100644 index f44a2fad0..000000000 --- a/core/data-api/src/main/java/com/neki/android/core/dataapi/repository/SampleRespository.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.neki.android.core.dataapi.repository - -import com.neki.android.core.model.Post - -interface SampleRepository { - suspend fun getPosts(): Result> - suspend fun getPost(id: Int): Result -} diff --git a/core/data/src/main/java/com/neki/android/core/data/repository/di/RepositoryModule.kt b/core/data/src/main/java/com/neki/android/core/data/repository/di/RepositoryModule.kt index e8258332d..2a721a9b1 100644 --- a/core/data/src/main/java/com/neki/android/core/data/repository/di/RepositoryModule.kt +++ b/core/data/src/main/java/com/neki/android/core/data/repository/di/RepositoryModule.kt @@ -1,9 +1,7 @@ package com.neki.android.core.data.repository.di import com.neki.android.core.data.repository.impl.DataStoreRepositoryImpl -import com.neki.android.core.data.repository.impl.SampleRepositoryImpl import com.neki.android.core.dataapi.repository.DataStoreRepository -import com.neki.android.core.dataapi.repository.SampleRepository import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -14,15 +12,10 @@ import javax.inject.Singleton @InstallIn(SingletonComponent::class) internal interface RepositoryModule { - @Binds - @Singleton - fun bindSampleRepositoryImpl( - sampleRepositoryImpl: SampleRepositoryImpl, - ): SampleRepository - @Binds @Singleton fun bindDataStoreRepositoryImpl( dataStoreRepositoryImpl: DataStoreRepositoryImpl, ): DataStoreRepository + } diff --git a/core/data/src/main/java/com/neki/android/core/data/repository/impl/SampleRepositoryImpl.kt b/core/data/src/main/java/com/neki/android/core/data/repository/impl/SampleRepositoryImpl.kt deleted file mode 100644 index deb6c6a5f..000000000 --- a/core/data/src/main/java/com/neki/android/core/data/repository/impl/SampleRepositoryImpl.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.neki.android.core.data.repository.impl - -import com.neki.android.core.data.remote.api.ApiService -import com.neki.android.core.data.util.runSuspendCatching -import com.neki.android.core.dataapi.repository.SampleRepository -import com.neki.android.core.model.Post -import javax.inject.Inject - -class SampleRepositoryImpl @Inject constructor( - private val apiService: ApiService, -) : SampleRepository { - override suspend fun getPosts(): Result> = runSuspendCatching { - apiService.getPosts().map { it.toModel() } - } - - override suspend fun getPost(id: Int): Result = runSuspendCatching { - apiService.getPost(id).toModel() - } -} From 448a732d515083a3caae72d900d97111ea2066bf Mon Sep 17 00:00:00 2001 From: Ojongseok Date: Sun, 4 Jan 2026 01:41:40 +0900 Subject: [PATCH 19/19] =?UTF-8?q?[chore]=20#4=20:=20detekt=20=EB=A3=B0=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/neki/android/core/data/repository/di/RepositoryModule.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/core/data/src/main/java/com/neki/android/core/data/repository/di/RepositoryModule.kt b/core/data/src/main/java/com/neki/android/core/data/repository/di/RepositoryModule.kt index 2a721a9b1..78ca2cc8c 100644 --- a/core/data/src/main/java/com/neki/android/core/data/repository/di/RepositoryModule.kt +++ b/core/data/src/main/java/com/neki/android/core/data/repository/di/RepositoryModule.kt @@ -17,5 +17,4 @@ internal interface RepositoryModule { fun bindDataStoreRepositoryImpl( dataStoreRepositoryImpl: DataStoreRepositoryImpl, ): DataStoreRepository - }