Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
39f8a13
[chore] #51 드래그 관련 상수 네이밍 변경
Ojongseok Jan 24, 2026
99149f5
[design] #51 BottomNavigationBar 디자인 변경사항 반영
Ojongseok Jan 24, 2026
0c2d70e
[refactor] #51 AnchoredDraggablePanel 드래그 레벨별 높이 조절 방식 로직 변경
Ojongseok Jan 24, 2026
a1b3208
[feat] #51 '현 위치에서 탐색' 컴포넌트 정의
Ojongseok Jan 24, 2026
60e8997
[feat] #51 지도 버튼 컴포넌트 .noRippleClickableSingle로 변경
Ojongseok Jan 24, 2026
bf53d05
[feat] #51 하단 패널 DragLevel.THIRD 상태 높이 화면 90%로 변경
Ojongseok Jan 24, 2026
d1240e8
[chore] #51 불필요한 패널 높이 변수 제거
Ojongseok Jan 24, 2026
d2fd2c9
[feat] #51 DragLevel First, Second 상태의 '현 위치에서 탐색' 버튼 노출
Ojongseok Jan 24, 2026
01c3df2
[feat] #51 NaverMap isZoomControlEnabled 속성 false로 설정
Ojongseok Jan 24, 2026
3ba09fa
[feat] #51 '현 위치에서 탐색' 버튼 UI 로직 구성
Ojongseok Jan 24, 2026
8c906cc
[feat] #51 현위치에 따른 길찾기 앱 이동 로직 수정
Ojongseok Jan 24, 2026
8c5f0d5
[feat] #51 core:data-api dataStore 의존성 추가
Ojongseok Jan 24, 2026
38a0713
[feat] #51 feat:map:impl activity-compose 의존성 추가
Ojongseok Jan 24, 2026
3cbe6c9
[feat] #51 Boolean 타입을 저장할 DataStoreRepository 인터페이스 작성
Ojongseok Jan 24, 2026
6f5c983
[feat] #51 DataStore Key 정의 경로 변경
Ojongseok Jan 24, 2026
39d7e54
[feat] #51 accessToken, refreshToken 조회 로직 nonNull 타입으로 변경
Ojongseok Jan 24, 2026
c601a61
[feat] #51 네컷지도 위치권한 확인 로직 추가
Ojongseok Jan 24, 2026
c815bc7
[feat] #51 권한 관련 공통 로직을 작성하는 PermissionManager.kt 정의
Ojongseok Jan 24, 2026
329983b
[feat] #51 위치 권한 확인 시점 추가(네컷지도 화면 진입 시, 길찾기 버튼 선택 시)
Ojongseok Jan 24, 2026
00b046a
[build] #51 detekt 룰 적용
Ojongseok Jan 24, 2026
7baf582
[fix] #51 clearTokens()가 accessToken과 refreshToken만 지우도록 수정
Ojongseok Jan 24, 2026
33e368f
[fix] #51 MapContract 내 현위치 nullable하도록 수정
Ojongseok Jan 24, 2026
593a1b8
[fix] #51 일부 Brand~ -> PhotoBooth 네이밍 변경 및 CloseButton 분리
Ojongseok Jan 24, 2026
eb2980e
[build] #51 detekt 룰 적용
Ojongseok Jan 24, 2026
9d6cd4b
[fix] #51 권한 관련 매니저 클래스 권한 단위로 분리
Ojongseok Jan 25, 2026
f6ff2f6
[fix] #51 드래그 관련 높이 상수 네이밍 변경
Ojongseok Jan 25, 2026
f51fa03
[fix] #51 MapRefreshChip.kt 수평 패딩 수정
Ojongseok Jan 25, 2026
34a6471
[fix] #51 가까운 네컷 사진 브랜드 더미데이터 조회 로직 수정
Ojongseok Jan 25, 2026
6a1ce94
[fix] #51 @Qualifier 활용해 Token / Auth DataStore 인스턴스 분리
Ojongseok Jan 25, 2026
0739538
[fix] #51 현위치 조회하지 못한 경우 토스트메시지 NekiToast로 변경
Ojongseok Jan 25, 2026
d409dd7
[fix] #51 위치 권한 여부에 따른 Intent, Effect 처리 로직 개선
Ojongseok Jan 25, 2026
dbf64bb
[fix] #51 위치권한 여부 체크 프로세스 변경
Ojongseok Jan 25, 2026
d62e281
[fix] #51 패널 관련 상수 네이밍 변경
Ojongseok Jan 25, 2026
42ea247
[fix] #51 토스트메시지 remember 변수 정의 제거
Ojongseok Jan 25, 2026
7647462
[fix] #51 위치 권한 이전 shouldShowRationale 확인 로직 추가
Ojongseok Jan 25, 2026
68da56c
[build] #51 detekt 룰 적용
Ojongseok Jan 25, 2026
10b6dca
[design] #51 pinShadow() 확장함수 추가 및 PhotoBoothMarker shadow 효과 적용
Ojongseok Jan 25, 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
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,9 @@ fun BottomNavigationBarItem(
color = NekiTheme.colorScheme.white,
) {
Column(
modifier = Modifier.padding(vertical = 8.dp),
modifier = Modifier.padding(vertical = 4.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(1.dp),
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
Icon(
modifier = Modifier.size(24.dp),
Expand All @@ -99,7 +99,7 @@ fun BottomNavigationBarItem(
Text(
text = stringResource(tab.iconStringRes),
color = textColor,
style = NekiTheme.typography.caption11SemiBold,
style = NekiTheme.typography.caption12SemiBold,
)
Comment thread
ikseong00 marked this conversation as resolved.
}
}
Expand Down
3 changes: 2 additions & 1 deletion core/common/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ android {
dependencies {
api(libs.timber)
implementation(libs.androidx.security.crypto)
implementation(libs.androidx.core.ktx)

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.neki.android.core.common.permission

import android.Manifest
import android.app.Activity
import android.content.Context
import androidx.core.content.ContextCompat
import androidx.core.content.PermissionChecker

object LocationPermissionManager {
val LOCATION_PERMISSIONS = arrayOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION,
)

fun hasLocationPermission(context: Context): Boolean {
return ContextCompat.checkSelfPermission(
context,
Manifest.permission.ACCESS_FINE_LOCATION,
) == PermissionChecker.PERMISSION_GRANTED ||
ContextCompat.checkSelfPermission(
context,
Manifest.permission.ACCESS_COARSE_LOCATION,
) == PermissionChecker.PERMISSION_GRANTED
}

fun shouldShowLocationRationale(activity: Activity): Boolean {
return activity.shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.neki.android.core.common.permission

import android.content.Context
import android.content.Intent
import android.net.Uri
import android.provider.Settings

fun navigateToAppSettings(context: Context) {
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.fromParts("package", context.packageName, null)
}
context.startActivity(intent)
Comment thread
Ojongseok marked this conversation as resolved.
}
2 changes: 1 addition & 1 deletion core/data-api/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ plugins {
dependencies {
implementation(projects.core.model)
implementation(libs.kotlinx.coroutines.core)

api(libs.androidx.datastore.preferences)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.neki.android.core.dataapi.datastore

import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey

object DataStoreKey {
Comment thread
ikseong00 marked this conversation as resolved.
val ACCESS_TOKEN = stringPreferencesKey("access_token")
val REFRESH_TOKEN = stringPreferencesKey("refresh_token")

val IS_FIRST_LOCATION_PERMISSION = booleanPreferencesKey("is_first_location_permission")
Comment thread
Ojongseok marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,9 @@
package com.neki.android.core.dataapi.repository

import androidx.datastore.preferences.core.Preferences
import kotlinx.coroutines.flow.Flow

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

fun isSavedJwtTokens(): Flow<Boolean>
fun getAccessToken(): Flow<String?>
fun getRefreshToken(): Flow<String?>
suspend fun clearTokens()
suspend fun setBoolean(key: Preferences.Key<Boolean>, value: Boolean)
fun getBoolean(key: Preferences.Key<Boolean>): Flow<Boolean>
}
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 TokenRepository {
suspend fun saveTokens(
accessToken: String,
refreshToken: String,
)
fun isSavedTokens(): Flow<Boolean>
fun getAccessToken(): Flow<String>
fun getRefreshToken(): Flow<String>
suspend fun clearTokens()
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,32 @@ package com.neki.android.core.data.local.di

import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.emptyPreferences
import androidx.datastore.preferences.preferencesDataStoreFile
import androidx.datastore.preferences.preferencesDataStore
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton

private const val AUTH_DATASTORE = "auth_datastore"
private val Context.authDataStore: DataStore<Preferences> by preferencesDataStore(name = AUTH_DATASTORE)

private const val TOKEN_DATASTORE = "token_datastore"
private val Context.tokenDataStore: DataStore<Preferences> by preferencesDataStore(name = TOKEN_DATASTORE)

@InstallIn(SingletonComponent::class)
@Module
internal object DataStoreModule {
private const val DATASTORE_NAME = "neki-datastore"

@AuthDataStore
@Singleton
@Provides
fun provideAuthDataStore(@ApplicationContext context: Context): DataStore<Preferences> = context.authDataStore

@TokenDataStore
@Singleton
@Provides
fun provideDataStore(
@ApplicationContext context: Context,
): DataStore<Preferences> {
return PreferenceDataStoreFactory.create(
corruptionHandler = ReplaceFileCorruptionHandler(
produceNewData = { emptyPreferences() },
),
produceFile = { context.preferencesDataStoreFile(DATASTORE_NAME) },
)
}
fun provideTokenDataStore(@ApplicationContext context: Context): DataStore<Preferences> = context.tokenDataStore
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.neki.android.core.data.local.di

import javax.inject.Qualifier

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class AuthDataStore

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class TokenDataStore
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import com.neki.android.core.data.remote.model.response.AuthResponse
import com.neki.android.core.data.remote.model.response.BasicResponse
import com.neki.android.core.data.remote.qualifier.UploadHttpClient
import com.neki.android.core.dataapi.auth.AuthEventManager
import com.neki.android.core.dataapi.repository.DataStoreRepository
import com.neki.android.core.dataapi.repository.TokenRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
Expand All @@ -33,7 +33,6 @@ import io.ktor.http.HttpHeaders
import io.ktor.http.encodedPath
import io.ktor.serialization.kotlinx.json.json
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.serialization.json.Json
import timber.log.Timber
import javax.inject.Singleton
Expand All @@ -46,7 +45,7 @@ internal object NetworkModule {
const val TIME_OUT = 5000L
const val UPLOAD_TIME_OUT = 10_000L

val sendWithoutJwtUrls = listOf(
val sendWithoutAuthUrls = listOf(
"/api/auth/kakao/login",
"/api/auth/refresh",
)
Expand All @@ -67,7 +66,7 @@ internal object NetworkModule {
@Provides
@Singleton
fun provideHttpClient(
dataStoreRepository: DataStoreRepository,
tokenRepository: TokenRepository,
authEventManager: AuthEventManager,
): HttpClient {
return HttpClient(Android) {
Expand All @@ -80,10 +79,10 @@ internal object NetworkModule {
bearer {
loadTokens {
Timber.d("BearerAuth - loadTokens")
if (dataStoreRepository.isSavedJwtTokens().first()) {
if (tokenRepository.isSavedTokens().first()) {
BearerTokens(
accessToken = dataStoreRepository.getAccessToken().firstOrNull() ?: "",
refreshToken = dataStoreRepository.getRefreshToken().firstOrNull() ?: "",
accessToken = tokenRepository.getAccessToken().first(),
refreshToken = tokenRepository.getRefreshToken().first(),
)
} else null
}
Expand All @@ -95,12 +94,12 @@ internal object NetworkModule {
val response = client.post("/api/auth/refresh") {
setBody(
RefreshTokenRequest(
refreshToken = dataStoreRepository.getRefreshToken().firstOrNull() ?: "",
refreshToken = tokenRepository.getRefreshToken().first(),
),
)
}.body<BasicResponse<AuthResponse>>()

dataStoreRepository.saveJwtTokens(
tokenRepository.saveTokens(
accessToken = response.data.accessToken,
refreshToken = response.data.refreshToken,
)
Expand All @@ -111,20 +110,20 @@ internal object NetworkModule {
)
} catch (e: Exception) {
Timber.e(e)
dataStoreRepository.clearTokens()
tokenRepository.clearTokens()
authEventManager.emitTokenExpired()
null
}
} else null
}

sendWithoutRequest { request ->
val shouldNotJwt = sendWithoutJwtUrls.any {
val shouldNotAuth = sendWithoutAuthUrls.any {
request.url.encodedPath == it
}

Timber.d("Bearer 인증 필요 API 여부 : $shouldNotJwt")
!shouldNotJwt
Timber.d("Bearer 인증 필요 API 여부 : $shouldNotAuth")
!shouldNotAuth
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import com.neki.android.core.data.repository.impl.AuthRepositoryImpl
import com.neki.android.core.data.repository.impl.DataStoreRepositoryImpl
import com.neki.android.core.data.repository.impl.MediaUploadRepositoryImpl
import com.neki.android.core.data.repository.impl.PhotoRepositoryImpl
import com.neki.android.core.data.repository.impl.TokenRepositoryImpl
import com.neki.android.core.dataapi.auth.AuthEventManager
import com.neki.android.core.dataapi.repository.AuthRepository
import com.neki.android.core.dataapi.repository.DataStoreRepository
import com.neki.android.core.dataapi.repository.MediaUploadRepository
import com.neki.android.core.dataapi.repository.PhotoRepository
import com.neki.android.core.dataapi.repository.TokenRepository
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
Expand All @@ -32,6 +34,12 @@ internal interface RepositoryModule {
authRepositoryImpl: AuthRepositoryImpl,
): AuthRepository

@Binds
@Singleton
fun bindTokenRepositoryImpl(
tokenRepositoryImpl: TokenRepositoryImpl,
): TokenRepository

@Binds
@Singleton
fun bindAuthEventManagerImpl(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,53 +3,25 @@ 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 com.neki.android.core.data.local.di.AuthDataStore
import javax.inject.Inject

class DataStoreRepositoryImpl @Inject constructor(
private val dataStore: DataStore<Preferences>,
@AuthDataStore 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 saveJwtTokens(
accessToken: String,
refreshToken: String,
) {
override suspend fun setBoolean(key: Preferences.Key<Boolean>, value: Boolean) {
dataStore.edit { preferences ->
preferences[ACCESS_TOKEN] = CryptoManager.encrypt(accessToken)
preferences[REFRESH_TOKEN] = CryptoManager.encrypt(refreshToken)
}
}

override fun isSavedJwtTokens(): Flow<Boolean> {
return dataStore.data.map { preferences ->
val accessToken = preferences[ACCESS_TOKEN]?.let { CryptoManager.decrypt(it) }
val refreshToken = preferences[REFRESH_TOKEN]?.let { CryptoManager.decrypt(it) }

!accessToken.isNullOrBlank() && !refreshToken.isNullOrBlank()
preferences[key] = value
}
}

override fun getAccessToken(): Flow<String?> {
override fun getBoolean(key: Preferences.Key<Boolean>): Flow<Boolean> {
return dataStore.data.map { preferences ->
preferences[ACCESS_TOKEN]?.let { CryptoManager.decrypt(it) }
preferences[key] ?: false
}
}

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