Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
4ce92ad
[chore] DataStoreRepository.kt 및 DataStoreModule.kt 정의
Ojongseok Dec 28, 2025
ea9260a
[chore] DataStore 암호화/복호화 로직 구성
Ojongseok Dec 28, 2025
5164727
[chore] feature 단위 Repository 구현체 내 dataStore 주입 제거
Ojongseok Dec 28, 2025
17badae
[chore] CryptoManager.kt 불필요한 주석 제거
Ojongseok Dec 28, 2025
33f276f
[chore] RepositoryModule 선언부 interface로 변경
Ojongseok Jan 1, 2026
b7b5419
[chore] #4 RunSuspendCatching 확장함수 정의
Ojongseok Jan 1, 2026
992dadd
[chore] DataStoreRepository.kt 및 DataStoreModule.kt 정의
Ojongseok Dec 28, 2025
27371e9
[chore] DataStore 암호화/복호화 로직 구성
Ojongseok Dec 28, 2025
722b61b
[chore] CryptoManager.kt 불필요한 주석 제거
Ojongseok Dec 28, 2025
8db2485
[chore] RepositoryModule 선언부 interface로 변경
Ojongseok Jan 1, 2026
ba03dca
[chore] #4 RunSuspendCatching 확장함수 정의
Ojongseok Jan 1, 2026
1f5b96d
Merge remote-tracking branch 'origin/init/#4-setup-api-library' into …
Ojongseok Jan 2, 2026
ba9c1fd
[chore] #4 develop 브랜치 병합 과정 중 conflict 해결
Ojongseok Jan 2, 2026
e61e08e
[chore] #4 :feature:sample 모듈 삭제 및 의존성 제거
Ojongseok Jan 3, 2026
b3553f9
[chore] #4 : Repository 반환 타입 Result 객체로 매핑
Ojongseok Jan 3, 2026
dbb910d
[chore] #4 : Detekt 룰 적용
Ojongseok Jan 3, 2026
2d00217
[chore] #4 : detekt 룰 적용 및 불필요한 주석 제거
Ojongseok Jan 3, 2026
21ae929
[chore] #4 : RunSuspendCatching.kt detekt 룰 적용
Ojongseok Jan 3, 2026
182f305
[chore] #4 : SampleRepository, SampleRepositoryImpl 제거
Ojongseok Jan 3, 2026
448a732
[chore] #4 : detekt 룰 적용
Ojongseok Jan 3, 2026
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
4 changes: 1 addition & 3 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -32,4 +30,4 @@ dependencies {

implementation(libs.androidx.activity.compose)
implementation(libs.androidx.navigation3.ui)
}
}
1 change: 1 addition & 0 deletions core/common/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@ android {

dependencies {
api(libs.timber)
implementation(libs.androidx.security.crypto)

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
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 = "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()
}

@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()
}

@SuppressLint("NewApi")
fun encrypt(text: String): String {
val cipher = Cipher.getInstance(ALGORITHM)
cipher.init(Cipher.ENCRYPT_MODE, getSecretKey())

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 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)

return String(cipher.doFinal(encryptedData))
}
Comment thread
Ojongseok marked this conversation as resolved.
}
2 changes: 2 additions & 0 deletions core/data-api/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ plugins {

dependencies {
implementation(projects.core.model)
implementation(libs.kotlinx.coroutines.core)

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.neki.android.core.dataapi.repository

import kotlinx.coroutines.flow.Flow

interface DataStoreRepository {
suspend fun saveTokens(
accessToken: String,
refreshToken: String,
)

fun getAccessToken(): Flow<String?>
fun getRefreshToken(): Flow<String?>
suspend fun clearTokens()
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
package com.neki.android.core.data.repository.di

import com.neki.android.core.data.repository.impl.SampleRepositoryImpl
import com.neki.android.core.dataapi.repository.SampleRepository
import com.neki.android.core.data.repository.impl.DataStoreRepositoryImpl
import com.neki.android.core.dataapi.repository.DataStoreRepository
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 {
internal interface RepositoryModule {

@Binds
abstract fun bindSampleRepositoryImpl(
sampleRepositoryImpl: SampleRepositoryImpl,
): SampleRepository
@Singleton
fun bindDataStoreRepositoryImpl(
dataStoreRepositoryImpl: DataStoreRepositoryImpl,
): DataStoreRepository
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
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<Preferences>,
) : 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)
}
}
Comment thread
Ojongseok marked this conversation as resolved.

override fun getAccessToken(): Flow<String?> {
return dataStore.data.map { preferences ->
preferences[ACCESS_TOKEN]?.let { CryptoManager.decrypt(it) }
}
}
Comment thread
Ojongseok marked this conversation as resolved.

override fun getRefreshToken(): Flow<String?> {
return dataStore.data.map { preferences ->
preferences[REFRESH_TOKEN]?.let { CryptoManager.decrypt(it) }
}
}

override suspend fun clearTokens() {
dataStore.edit { it.clear() }
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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 <T> runSuspendCatching(block: () -> T): Result<T> {
// 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
}
Comment thread
Ojongseok marked this conversation as resolved.
}
1 change: 0 additions & 1 deletion feature/sample/api/.gitignore

This file was deleted.

7 changes: 0 additions & 7 deletions feature/sample/api/build.gradle.kts

This file was deleted.

This file was deleted.

1 change: 0 additions & 1 deletion feature/sample/impl/.gitignore

This file was deleted.

14 changes: 0 additions & 14 deletions feature/sample/impl/build.gradle.kts

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

4 changes: 4 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Comment thread
Ojongseok marked this conversation as resolved.
timber = "5.0.1"
androidxNavigation3 = "1.0.0"
androidxLifecycleViewModelNavigation3 = "2.10.0"
Expand Down Expand Up @@ -45,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" }
Expand All @@ -59,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" }

Expand Down
Loading