-
Notifications
You must be signed in to change notification settings - Fork 1
[Refactor] 프로필 정보 Presentation #254
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
- Kotest, Orbit-test 라이브러리 추가 - UserType -> SignInStatus 네이밍 수정 - Compose Compiler version upgrade to 1.5.15 - Orbit upgrade to 10.0.0
- 저장한 장소 캐싱 로직 추가
Walkthrough전역적으로 UserType를 SignInStatus로 대체하고, 세션/리포지토리/Compose Local/뷰모델/화면 전반을 이에 맞게 수정. 프로필 정보 화면(컨테이너/뷰모델/컴포저블)과 저장한 장소 캐시(로컬/리모트/리포지토리) 추가. Profile API 엔드포인트/DTO 정비. 내비게이션 프로필 레거시 시그니처 변경. 테스트·그라들 의존성 갱신. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor UI
participant Compose as LocalSignInStatus
participant VM as ViewModel (e.g., ProfileInfoViewModel)
participant UserRepo
participant Session as SessionHandler
UI->>Compose: read LocalSignInStatus.current
VM->>UserRepo: getSignInStatus()
UserRepo->>Session: getUserType() -> Flow<SignInStatus>
Session-->>UserRepo: Flow(USER/GUEST)
UserRepo-->>VM: Flow(SignInStatus)
VM-->>UI: render based on status
sequenceDiagram
autonumber
actor UI
participant VM as ProfileInfoViewModel
participant ProfRepo as ProfileRepository
participant ProfLocal as ProfileLocalDS
participant ProfRemote as ProfileRemoteDS
Note over VM: loadState()
VM->>UserRepo: getSignInStatus()
alt SignInStatus.USER
par SavedSpots
VM->>ProfRepo: getSavedSpots() : Flow<Result<List<SavedSpot>>>
ProfRepo->>ProfLocal: getSavedSpots() (Flow<List<SavedSpot>?>)
alt Cache hit
ProfLocal-->>ProfRepo: List<SavedSpot>
ProfRepo-->>VM: Flow(Result.success(cache))
else Cache miss
ProfRepo->>ProfRemote: getSavedSpots()
ProfRemote-->>ProfRepo: List<SavedSpotResponse>
ProfRepo->>ProfLocal: cacheSavedSpots(mapped)
ProfRepo-->>VM: Flow(Result(mapped))
end
and Profile
VM->>ProfRepo: getProfile()
ProfRepo-->>VM: Result<Profile>
end
VM-->>UI: User(profile, savedSpots) or LoadFailed
else SignInStatus.GUEST
VM-->>UI: Guest
end
sequenceDiagram
autonumber
actor UI as Spot Detail/List
participant SpotRepo
participant SpotRemote
UI->>SpotRepo: fetchSpotList(request, signInStatus)
alt signInStatus == GUEST
SpotRepo->>SpotRemote: fetch no-auth list
else
SpotRepo->>SpotRemote: fetch auth list
end
SpotRemote-->>SpotRepo: SpotListResponse
SpotRepo-->>UI: SpotListResponse
sequenceDiagram
autonumber
actor UI as Bookmark Action
participant SpotRepo
participant ProfLocal
participant ProfRemote
UI->>SpotRepo: addBookmark(spotId)
SpotRepo-->>UI: Result
SpotRepo->>ProfLocal: getSavedSpots().firstOrNull()
alt Cached exists
SpotRepo->>ProfRemote: getSavedSpots()
ProfRemote-->>SpotRepo: List<SavedSpotResponse>
SpotRepo->>ProfLocal: cacheSavedSpots(mapped)
else No cache
Note over SpotRepo: skip refresh
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests
Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 13
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (4)
feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotlist/composable/SpotItem.kt (1)
127-135: 컴파일 에러: Modifier.padding() 인자 누락
Modifier.padding()은 매개변수가 필수입니다. 현재 형태는 컴파일 실패합니다. 의도에 따라 제거하거나 값(예:start = ...)을 지정해주세요.수정 예시(패딩 불필요 시 제거):
- modifier = Modifier.padding() + modifier = Modifier또는(패딩 유지 시):
- modifier = Modifier.padding() + modifier = Modifier.padding(horizontal = 0.dp)core/data/src/main/kotlin/com/acon/core/data/repository/UserRepositoryImpl.kt (1)
91-95: 계정 삭제 시 프로필 캐시 정리가 누락되었습니다.
deleteAccount에서도 로그아웃과 마찬가지로 프로필 캐시를 정리해야 합니다.다음과 같이 수정하세요:
).onSuccess { + profileLocalDataSource.clearCache() onboardingRepository.updateHasVerifiedArea(false) onboardingRepository.updateHasTastePreference(false) clearSession() }app/src/main/java/com/acon/acon/MainActivity.kt (2)
182-186: 권한 체크 오타: FINE을 두 번 체크COARSE 권한을 누락해 항상 FINE만 중복 확인합니다. AND 조건의 두 번째를 COARSE로 교체하세요.
적용 diff:
- emit( - checkSelfPermission(android.Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED - && checkSelfPermission(android.Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED - ) + emit( + checkSelfPermission(android.Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED && + checkSelfPermission(android.Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED + )
416-418: 권한 체크 오타(중복) — COARSE로 교체아래 위치에서도 동일한 오타가 있습니다.
적용 diff:
- if (checkSelfPermission(android.Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED - && checkSelfPermission(android.Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED - ) return + if (checkSelfPermission(android.Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED && + checkSelfPermission(android.Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED + ) return
🧹 Nitpick comments (54)
core/data/src/main/kotlin/com/acon/core/data/dto/response/profile/ProfileResponse.kt (4)
6-14: 직렬화 적용은 적절합니다. 단, 불필요한 @SerialName 제거 권장nickname/birthDate는 필드명과 JSON 키가 동일해 @SerialName이 불필요합니다. 유지보수성 위해 profileImage만 남기고 나머지 제거를 제안합니다.
다음 패치 제안:
- @SerialName("nickname") val nickname: String, - @SerialName("birthDate") val birthDate: String? = null, + val nickname: String, + val birthDate: String? = null, @SerialName("profileImage") val image: String? = null,
19-26: 생일 파싱을 엄격/명시적으로 처리하세요 (포맷, 트리밍, 예외 범위 축소)split + toInt는 공백/자릿수 오류에 취약하고 광범위 예외를 삼킵니다. STRICT 모드 DateTimeFormatter를 사용해 포맷을 한눈에 드러내고, trim/빈값 거르기를 추가하세요.
- val birthDateOfModel = birthDate?.let { dateString -> - try { - val (year, month, day) = dateString.split(".").map { it.toInt() } - BirthDateStatus.Specified(LocalDate.of(year, month, day)) - } catch (_: Exception) { - BirthDateStatus.NotSpecified - } - } ?: BirthDateStatus.NotSpecified + val birthDateOfModel = birthDate + ?.trim() + ?.takeIf { it.isNotEmpty() } + ?.let { ds -> + runCatching { LocalDate.parse(ds, BIRTHDATE_FMT) } + .getOrNull() + }?.let { BirthDateStatus.Specified(it) } + ?: BirthDateStatus.NotSpecified파일 상단(임포트 옆)에 다음 상수를 추가하세요:
import java.time.format.DateTimeFormatter import java.time.format.ResolverStyle private val BIRTHDATE_FMT: DateTimeFormatter = DateTimeFormatter.ofPattern("uuuu.MM.dd").withResolverStyle(ResolverStyle.STRICT)
27-29: image가 빈 문자열/공백인 경우도 Default로 처리 필요현재 null만 Default 처리합니다. 공백/빈 문자열은 Custom("")가 되어 다운스트림에서 URL 검증 이슈를 유발할 수 있습니다.
- val imageOfModel = - if (image == null) ProfileImageStatus.Default else ProfileImageStatus.Custom(image) + val imageOfModel = image + ?.trim() + ?.takeIf { it.isNotEmpty() } + ?.let(ProfileImageStatus::Custom) + ?: ProfileImageStatus.Default
18-31: 사소한 정리: 불필요한 지역 변수 제거nicknameOfModel은 단순 전달이므로 인라인하면 가독성이 좋아집니다.
- val nicknameOfModel = nickname ... - return Profile(nicknameOfModel, birthDateOfModel, imageOfModel) + return Profile(nickname, birthDateOfModel, imageOfModel)core/data/src/test/java/com/acon/core/data/mapping/ProfileMappingTest.kt (4)
13-14: MockKExtension 미사용 — 제거 권장이 테스트는 목 객체를 사용하지 않습니다. 불필요한 확장을 제거해 노이즈를 줄이세요.
-@ExtendWith(MockKExtension::class) -class ProfileMappingTest { +class ProfileMappingTest {
31-31: assertEquals 인자 순서 교정일관된 메시지/리포트를 위해 assertEquals(expected, actual) 순서를 사용하세요.
- assertEquals(actualNickname, expectedNickname) + assertEquals(expectedNickname, actualNickname)
78-94: 유효하지 않은 날짜 포맷 케이스는 적절합니다. 추가 경계 케이스 제안"1999.13.01"(월 범위 초과), "1999.02.29"(평년 2/29), 공백/빈 문자열(" ", "")도 커버하면 회귀 방지에 도움 됩니다.
원하시면 위 케이스를 포함한 파라미터라이즈드 테스트 초안을 드리겠습니다.
96-113: 이미지 커스텀 매핑 테스트는 적절합니다. 소소한 정리 제안변수명에 URL 약어 대문자 사용 및 불필요한 괄호 제거로 읽기 쉬워집니다.
- val expectedImageUrl = "Custom Profile Image Url" + val expectedImageURL = "Custom Profile Image Url" ... - assertIs<ProfileImageStatus.Custom>(actualProfileImageStatus) - assertEquals(expectedImageUrl, (actualProfileImageStatus).url) + assertIs<ProfileImageStatus.Custom>(actualProfileImageStatus) + assertEquals(expectedImageURL, actualProfileImageStatus.url)core/model/src/main/java/com/acon/acon/core/model/type/SignInStatus.kt (1)
1-5: 세미콜론 제거(스타일 니트픽) 및 직렬화 필요 여부 확인
- enum 끝의
;는 본문이 없으므로 불필요합니다(가독성).- 이 타입이 저장/전송에 쓰이면
@Serializable부착을 검토해주세요.적용 예시:
enum class SignInStatus { - USER, GUEST; + USER, GUEST }feature/profile/src/androidTest/AndroidManifest.xml (1)
5-13: androidTest Activity exported 설정 보수화 권장
- 테스트 전용 매니페스트이긴 하나, 외부에서 인텐트로 호출될 필요가 없다면
exported="false"가 더 안전합니다.- AdMob 테스트 앱 ID 사용은 적절합니다(샘플 ID).
제안 diff:
- <activity + <activity android:name="androidx.activity.ComponentActivity" - android:exported="true" + android:exported="false" android:theme="@android:style/Theme.Material.Light.NoActionBar.Fullscreen" />feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotlist/composable/SpotItem.kt (1)
118-119: 네이밍 정리(가독성 니트픽): userType → signInStatus
- 현재 값이 SignInStatus이므로 변수명을
signInStatus로 맞추면 코드 의도가 더 명확합니다. 동작 변화는 없습니다.적용 예시:
- val userType = LocalSignInStatus.current + val signInStatus = LocalSignInStatus.current ... - if (userType == com.acon.acon.core.model.type.SignInStatus.GUEST) + if (signInStatus == com.acon.acon.core.model.type.SignInStatus.GUEST)Also applies to: 204-208
feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotdetail/composable/SpotDetailScreen.kt (4)
129-130: 로그인 상태 비교를 일반화(!= GUEST)하여 미래 상태 추가에 내성 있게 만들기USER 고정 비교는 신규 상태(예: PREMIUM, ADMIN 등) 추가 시 분기 누락 위험이 있습니다. 비게스트만 허용하는 의도라면 GUEST 여부만 판정하세요.
-} else if (deepLinkHandler.hasDeepLink.value && userType == com.acon.acon.core.model.type.SignInStatus.USER) { +} else if (deepLinkHandler.hasDeepLink.value && userType != com.acon.acon.core.model.type.SignInStatus.GUEST) {-} else if (deepLinkHandler.hasDeepLink.value && userType == com.acon.acon.core.model.type.SignInStatus.USER) { +} else if (deepLinkHandler.hasDeepLink.value && userType != com.acon.acon.core.model.type.SignInStatus.GUEST) {Also applies to: 258-259
93-93: 변수명 정리 제안: userType → signInStatus의미를 직관적으로 맞춰 가독성을 높여주세요.
- val userType = LocalSignInStatus.current + val signInStatus = LocalSignInStatus.current
413-419: 비로그인 북마크 클릭 시 딥링크 초기화(deepLinkHandler.clear()) 재검토로그인 유도 직후 딥링크 컨텍스트를 지우면 로그인 완료 후 원 상세로의 원복이 어려울 수 있습니다. 초기화 시점 또는 복구 전략(복귀용 파라미터 저장) 확인 부탁드립니다.
420-421: isBookmarkSelected 계산식 단순화게스트 여부만 배제하고 나머지는 상태 그대로 반영하면 의도가 더 명확합니다.
- isBookmarkSelected = if (userType == com.acon.acon.core.model.type.SignInStatus.GUEST) false else state.spotDetail.isSaved, + isBookmarkSelected = userType != com.acon.acon.core.model.type.SignInStatus.GUEST && state.spotDetail.isSaved,feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotdetail/composable/SpotDetailViewModel.kt (3)
46-61: signInStatus 수집 시 중복 네트워크 호출 가능성 — distinctUntilChanged() 적용 권장로그인 상태가 동일 값으로 재방출되면 fetchedSpotDetail()이 반복 실행될 수 있습니다. 중복 호출을 방지하세요.
- signInStatus.collect { + signInStatus + .distinctUntilChanged() + .collect {
64-64: UI 지연용 delay(800) 제거 또는 빌드플래그/디버그 전용으로 한정지연은 UX에 악영향을 줄 수 있습니다. 스켈레톤 연출 목적이면 디버그 빌드에서만 적용하거나, 로딩 상태 지속시간을 UI 레이어에서 제어하세요.
83-90: signInStatus.value 직접 접근 최소화동일 함수 내에서 collect와 value 혼용은 경쟁 상태 가독성을 떨어뜨립니다. collect 블록의 현재 it 값을 캡처해 사용하거나 snapshot을 인자로 넘기세요.
feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotdetail/composable/SpotDetailScreenContainer.kt (1)
44-44: useSignInStatus 호출 시점 조정(선 초기화 권장)로그인 상태 의존 초기 로딩 타이밍을 안정화하려면 state 수집 이전에 호출하는 편이 안전합니다.
- val state by viewModel.collectAsState() + viewModel.useSignInStatus() + val state by viewModel.collectAsState() @@ - viewModel.useSignInStatus()호출 순서가 변화해도 UI 테스트가 통과하는지 확인 부탁드립니다.
core/data/src/main/kotlin/com/acon/core/data/session/SessionHandler.kt (1)
28-29: 변수명이 혼동을 일으킬 수 있습니다.
_signInStatus를 노출하는 프로퍼티명이userType으로 되어있어 혼란을 줄 수 있습니다.명확성을 위해 다음과 같이 수정하는 것을 권장합니다:
private val _signInStatus = MutableStateFlow(SignInStatus.GUEST) - private val userType = _signInStatus.asStateFlow() + private val signInStatus = _signInStatus.asStateFlow()그리고 Line 42에서:
override fun getUserType(): Flow<SignInStatus> { - return userType + return signInStatus }core/data/src/test/java/com/acon/core/data/datasource/remote/ProfileRemoteDataSourceTest.kt (1)
134-151: SavedSpotsResponse 래퍼 언래핑 테스트: OK. 케이스 보강 제안래퍼 → 리스트 언래핑을 정확히 검증하고 있습니다. 추가로 빈 리스트(0개) 케이스를 하나 더 넣으면 회귀에 강해집니다. 변수명 actualSavedSpotsResponse는 실제 타입(List)에 맞춰 actualSavedSpots로 정리하면 가독성이 좋아집니다.
Also applies to: 160-160
feature/settings/src/main/java/com/acon/acon/feature/settings/screen/SettingsViewModel.kt (1)
20-24: 수집 최적화 및 미래 확장 대비 가드 추가 제안중복 방출 방지와 향후 상태 추가에 대비해 distinctUntilChanged와 else 가드를 권장합니다.
- userRepository.getSignInStatus().collectLatest { signInStatus -> - when (signInStatus) { - SignInStatus.GUEST -> reduce { SettingsUiState.Guest } - SignInStatus.USER -> reduce { SettingsUiState.User() } - } - } + userRepository.getSignInStatus() + .distinctUntilChanged() + .collectLatest { signInStatus -> + when (signInStatus) { + SignInStatus.GUEST -> reduce { SettingsUiState.Guest } + SignInStatus.USER -> reduce { SettingsUiState.User() } + else -> reduce { SettingsUiState.Guest } // forward-compat 가드 + } + }추가로 필요한 import:
import kotlinx.coroutines.flow.distinctUntilChangedcore/data/src/main/kotlin/com/acon/core/data/repository/ProfileRepositoryImpl.kt (1)
72-82: 캐시 실패로 네트워크 결과까지 실패하지 않도록 방어 코드 추가캐시 저장 실패가 전체 Result를 실패로 만들지 않도록 캐시 단계만 개별적으로 보호하세요.
private fun getSavedSpotsFromRemote(): Flow<Result<List<SavedSpot>>> { return flow { emit(runCatchingWith { val savedSpotResponses = profileRemoteDataSource.getSavedSpots() val savedSpots = savedSpotResponses.map { it.toSavedSpot() } - - profileLocalDataSource.cacheSavedSpots(savedSpots) + // 캐시 오류는 경고만 남기고 진행 + runCatching { profileLocalDataSource.cacheSavedSpots(savedSpots) } + .onFailure { Timber.w(it, "Failed to cache saved spots; proceeding with network result") } savedSpots }) } }추가 import:
import timber.log.Timberapp/src/main/java/com/acon/acon/MainViewModel.kt (2)
25-26: 불필요한 재방출 처리와 오류 내구성 개선 제안distinctUntilChanged와 catch로 불필요한 state 갱신과 스트림 중단을 방지하세요.
- userRepository.getSignInStatus().collectLatest { - _state.value = state.value.copy(signInStatus = it) - } + userRepository.getSignInStatus() + .distinctUntilChanged() + .catch { Timber.e(it, "getSignInStatus stream error") } + .collectLatest { status -> + _state.value = state.value.copy(signInStatus = status) + }추가 import:
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.catch import timber.log.Timber
59-59: FQCN 대신 import로 간결화 권장데이터 클래스에서 SignInStatus를 FQCN으로 사용 중입니다. 상단 import로 정리하면 가독성이 좋아집니다.
- val signInStatus: com.acon.acon.core.model.type.SignInStatus = com.acon.acon.core.model.type.SignInStatus.GUEST, + val signInStatus: SignInStatus = SignInStatus.GUEST,추가 import:
import com.acon.acon.core.model.type.SignInStatuscore/data/src/main/kotlin/com/acon/core/data/di/ApiModule.kt (1)
85-91: ProfileApi 프로바이더 마이그레이션 확인 완료, 네이밍 통일 권장
ProfileRemoteDataSource/Repository 등에서 모든 ProfileApi 주입이 일관되게 전환된 것을 확인했습니다. ApiModule 내 다른 @provides 함수명(‘provideXxxApi’)과 일치하도록providesProfileApi를provideProfileApi로 수정해주세요.feature/profile/src/main/java/com/acon/feature/profile/info/composable/ProfileInfoScreen.kt (3)
71-72: 상단 바가 상태바와 겹칠 수 있음 — statusBarsPadding 추가 권장Edge‑to‑edge 환경에서 상단 바가 시스템 상태바와 시각적으로 겹칠 수 있습니다. Acon의 다른 화면(예: SpotListScreen)과 동일하게 statusBarsPadding을 적용해 주세요.
적용 diff:
@@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll +import androidx.compose.foundation.layout.statusBarsPadding @@ - modifier = Modifier.padding(vertical = 14.dp) + modifier = Modifier + .statusBarsPadding() + .padding(vertical = 14.dp) )Also applies to: 3-10
41-49: verticalScroll 컨테이너 내부에서 fillMaxSize 사용 주의verticalScroll이 적용된 Column 내부에서 자식이 fillMaxSize를 사용하면 무한 높이 제약과 충돌해 레이아웃 경고 또는 의도치 않은 확장/스크롤 이슈가 발생할 수 있습니다. 실패/로딩 UI는 스크롤 영역 밖(Box/Column.weight)에서 전개하거나, 최소 변경으로는 가로만 채우도록 조정해 주세요.
적용(최소 변경) diff:
@@ is ProfileInfoUiState.LoadFailed -> { NetworkErrorView( onRetry = actions.retryOnError, - modifier = Modifier.fillMaxSize() + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 48.dp) ) }권장(구조 변경): 스크롤 Column은 성공/게스트 상태에만 적용하고, Loading/LoadFailed는 상위 Column에서 weight(1f) Box로 분리 렌더링.
Also applies to: 112-117
108-111: Loading 상태에 비가시 컨텐츠 — 최소 로딩 인디케이터 표시 권장로딩일 때 아무것도 렌더링하지 않으면 UX 가이드와 불일치합니다. 디자인시스템의 공통 로딩(스켈레톤/인디케이터)이 있다면 여기서 노출해 주세요.
feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotlist/composable/SpotListScreenContainer.kt (2)
41-41: 네이밍 정합성: userType → signInStatus로 변경프로젝트 전반의 마이그레이션 방향(SignInStatus)에 맞춰 변수명을 일관되게 유지해 주세요.
적용 diff:
- val userType = LocalSignInStatus.current + val signInStatus = LocalSignInStatus.current @@ - if (userType == SignInStatus.GUEST) + if (signInStatus == SignInStatus.GUEST) onSignInRequired("click_detail_guest?") else viewModel.onSpotClicked(spot, rank)Also applies to: 67-71
67-69: 앰플리튜드 프로퍼티 키 통일"click_detail_guest?"처럼 '?'가 섞여 있습니다. 다른 키들과 통일(예: click_detail_guest)해 주세요.
적용 diff:
- if (signInStatus == SignInStatus.GUEST) - onSignInRequired("click_detail_guest?") + if (signInStatus == SignInStatus.GUEST) + onSignInRequired("click_detail_guest")feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotlist/composable/SpotListScreen.kt (3)
88-90: PagerState 재생성으로 포지션 리셋/재구성이 발생할 수 있음 — pageCount만 동적 제공pagerState를 분기 내에서 rememberPagerState로 재할당하면 스크롤 포지션이 리셋되고 재구성 비용이 커집니다. 상단에서 pageCount를 계산하고 단일 rememberPagerState에 공급하세요.
적용 diff:
@@ - var pagerState = rememberPagerState { 0 } + val pageCount = when (state) { + is SpotListUiStateV2.Success -> { + val size = state.spotList.size + when { + size >= 11 -> size + 2 + size >= 5 -> size + 1 + else -> size + } + } + else -> 0 + } + val pagerState = rememberPagerState { pageCount } @@ - SpotType.RESTAURANT -> { - pagerState = rememberPagerState { - val size = state.spotList.size - when { - size >= 11 -> size + 2 - size >= 5 -> size + 1 - else -> size - } - } + SpotType.RESTAURANT -> { @@ - SpotType.CAFE -> { - pagerState = rememberPagerState { - val size = state.spotList.size - when { - size >= 11 -> size + 2 - size >= 5 -> size + 1 - else -> size - } - } + SpotType.CAFE -> {Also applies to: 171-181, 202-212
91-93: 네이밍/참조 일관성 및 게스트 액션 키 보완
- 변수명: userType → signInStatus
- fully‑qualified SignInStatus 제거(이미 import됨)
- 게스트의 필터 클릭 시 프로퍼티 키 누락 보완
적용 diff:
@@ - val userType = LocalSignInStatus.current + val signInStatus = LocalSignInStatus.current @@ - if (userType == SignInStatus.GUEST) + if (signInStatus == SignInStatus.GUEST) onSignInRequired("click_toggle_guest?") else onSpotTypeChanged(it) @@ - if (userType == com.acon.acon.core.model.type.SignInStatus.GUEST) - onSignInRequired("") + if (signInStatus == SignInStatus.GUEST) + onSignInRequired("click_filter_guest") else onFilterButtonClick() @@ - signInStatus = userType, + signInStatus = signInStatus, @@ - signInStatus = userType, + signInStatus = signInStatus, @@ - if (userType == com.acon.acon.core.model.type.SignInStatus.GUEST) { + if (signInStatus == SignInStatus.GUEST) { onSignInRequired("click_upload_guest?") } else { onNavigateToUploadScreen() }Also applies to: 121-126, 137-142, 192-200, 222-230, 286-294
323-324: Preview의 불필요한 FQCN 제거이미 SpotType을 import하고 있으므로 FQCN 대신 심볼을 직접 사용해 가독성을 높이세요.
적용 diff:
- state = SpotListUiStateV2.Loading(com.acon.acon.core.model.type.SpotType.RESTAURANT), + state = SpotListUiStateV2.Loading(SpotType.RESTAURANT),feature/profile/src/test/kotlin/com/acon/feature/profile/info/viewmodel/ProfileInfoViewModelTest.kt (1)
150-157: 게스트 업로드 클릭 케이스 테스트 미구현(TODO)의도대로라면 게스트에서 onUploadClicked 호출 시 로그인 유도용 사이드이펙트가 방출되어야 합니다. 테스트를 추가해 주세요.
원하시면 ViewModel의 사이드이펙트 명세에 맞춰 테스트 본문을 생성해 드리겠습니다(예: RequestSignIn("click_upload_guest")).
feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotlist/composable/SpotListSuccessView.kt (5)
75-86: 광고 아이템 삽입 인덱스 계산 안정화 필요현재는 size>=11 삽입 후 size>=5 삽입을 수행하여, 원래 11번째 위치에 삽입하려던 광고가 12로 밀릴 수 있습니다. 또한 삽입 순서만 바꾸면 원소 수 판단이 왜곡됩니다. 원본 크기를 보존해 조건을 평가하고, 그 다음 삽입을 수행하도록 수정해 주세요.
적용 예시:
- val adInsertedSpot = remember(state.spotList) { - val list: MutableList<com.acon.acon.core.model.model.spot.Spot?> = state.spotList.toMutableList() - - if (list.size >= 11) { - list.add(11, null) - } - if (list.size >= 5) { - list.add(5, null) - } - - list - } + val adInsertedSpot = remember(state.spotList) { + val list: MutableList<com.acon.acon.core.model.model.spot.Spot?> = state.spotList.toMutableList() + val originalSize = list.size + if (originalSize >= 5) list.add(5, null) + if (originalSize >= 11) list.add(11, null) + list + }
207-220: 예외 삼키기 지양graphicsLayer 블록에서 빈 catch로 예외를 삼키면 원인 파악이 어렵습니다. 최소한 로그를 남기거나, 사전에 page 범위를 점검해 예외를 회피하세요.
적용 예시:
- .graphicsLayer { - try { + .graphicsLayer { + kotlin.runCatching { val pageOffset = pagerState.getOffsetDistanceInPages(page).absoluteValue val ratio = lerp( start = 1.1f, stop = 0.9f, fraction = pageOffset.coerceIn(0f, 1f) ) scaleX = ratio scaleY = ratio - } catch (_: Exception) { - } + }
131-151: 하드코딩된 문자열 리소스화"네이버 지도", "카카오 지도"는 문자열 리소스로 분리해야 i18n/접근성/번역 워크플로우에 유리합니다. stringResource를 사용하도록 교체해 주세요.
적용 예시(리소스 키는 예시):
- Text( - text = "네이버 지도", + Text( + text = stringResource(R.string.naver_map), ... - Text( - text = "카카오 지도", + Text( + text = stringResource(R.string.kakao_map),Also applies to: 152-171
279-283: LaunchedEffect 키 고정으로 색상 갱신 누락 가능성키를 Unit으로 두면 동일 페이지에서 spot이 갱신되어도 색상이 갱신되지 않습니다. spot(혹은 spot.image)을 키로 사용하세요.
- LaunchedEffect(Unit) { + LaunchedEffect(spot?.image) { if (spot != null) { spotFogColor = if (spot.image.isBlank()) Color(0xFFE17651) else spot.image.getOverlayColor(context) } }
59-60: 게스트 노출 개수 상수 중복 관리 제안동일 상수(MAX_GUEST_AVAILABLE_COUNT)가 SpotEmptyView에도 존재합니다. 공용 모듈의 object/const로 집약해 단일 출처로 관리하는 것을 권장합니다.
core/data/src/main/kotlin/com/acon/core/data/datasource/local/ProfileLocalDataSource.kt (2)
39-46: 널 대신 빈 리스트 사용 고려Flow<List?>로 널 가능을 열어두면 소비자 측 분기가 늘어납니다. 가능하다면 빈 리스트를 기본값으로 사용해 Flow<List>로 단순화하세요. 외부 계약 영향이 크면 추후 마이그레이션 계획만 수립해도 좋습니다.
적용 방향:
- _savedSpots 초기값을 emptyList()로 변경
- 인터페이스를 fun getSavedSpots(): Flow<List>로 변경
47-50: 인메모리 캐시인 점에 대한 운영 고려clearCache로 세션 종료 시 정리는 잘 되어 있습니다. 다만 프로세스 재시작 시 캐시가 소실됩니다. 콜드 스타트 UX 개선이 필요하다면 DataStore/Room 등 영속 캐시를 고려하세요.
feature/profile/src/main/java/com/acon/feature/profile/info/composable/ProfileView.kt (1)
79-86: 벡터 리소스에는 Icon 사용 권장현재 벡터를 Image로 그립니다. 의미적으로 Icon이 더 적합하며, 일관성도 좋아집니다.
- Image( - imageVector = ImageVector.vectorResource(R.drawable.ic_edit), + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_edit), contentDescription = stringResource(R.string.content_description_edit_profile), modifier = Modifier .testTag(TestTags.PROFILE_UPDATE_ICON) .padding(start = 4.dp) .noRippleClickable { onProfileUpdateIconClick() } )feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotlist/composable/SpotEmptyView.kt (3)
52-55: 화면 회전 등 밀도 변화 시 아이템 높이 불일치 가능itemHeightPx를 remember에 의존성 없이 저장하면 화면 높이/밀도 변경 시 갱신되지 않습니다. screenHeightPx를 키로 포함하세요.
- val itemHeightPx by remember { - mutableFloatStateOf(screenHeightPx * .3f) - } + val itemHeightPx by remember(screenHeightPx) { + mutableFloatStateOf(screenHeightPx * .3f) + }
130-133: 토스트 문구 하드코딩 제거사용자 문구는 stringResource로 관리하세요.
- context.showToast("웹사이트 접속에 실패했어요") + context.showToast(context.getString(R.string.fail_to_open_website))
37-38: 게스트 노출 상수 중복SpotListSuccessView와 동일한 상수가 중복됩니다. 공용 상수로 추출해 단일 출처로 관리하는 것을 권장합니다.
feature/profile/src/androidTest/kotlin/com/acon/feature/profile/info/composable/ProfileInfoScreenTest.kt (2)
165-173: 부동소수점 비교 안정화레이아웃 좌표는 소수 오차가 발생할 수 있어 절대 동일 비교가 불안정합니다. 허용 오차를 두어 검증하세요.
- assertEquals(expectedBottomBarTop, actualBottomBarTop) + assertEquals(expectedBottomBarTop, actualBottomBarTop, absoluteTolerance = 0.5f)
151-159: UI 안정화 대기 추가setContent 직후 즉시 측정/동작 시 플래키해질 수 있습니다. waitForIdle을 추가해 안정성을 높이세요.
composeTestRule.setContent { ProfileInfoScreen( state = ProfileInfoUiState.User( profile = dummyProfile, savedSpots = emptyList() ), actions = mockActions ) } + composeTestRule.waitForIdle()feature/profile/src/main/java/com/acon/feature/profile/info/composable/SavedSpotsView.kt (3)
89-95: onClick 시그니처 단순화SavedSpotItem은 spot을 이미 인자로 받아 내부에서 spotId에 접근 가능합니다. onClick 타입을 () -> Unit으로 단순화하면 호출부/구현부 모두 명확해집니다.
- SavedSpotItem( + SavedSpotItem( spot = spot, - onClick = { onSavedSpotItemClick(spot.spotId) }, + onClick = { onSavedSpotItemClick(spot.spotId) }, modifier = Modifier .aspectRatio(150f / 217f) .testTag(TestTags.SAVED_SPOT_ITEM + spot.spotId) ) ... -private fun SavedSpotItem( - spot: SavedSpot, - onClick: (spotId: Long) -> Unit, +private fun SavedSpotItem( + spot: SavedSpot, + onClick: () -> Unit, modifier: Modifier = Modifier ) { Box( modifier = modifier .clip(RoundedCornerShape(8.dp)) - .clickable { onClick(spot.spotId) } + .clickable { onClick() } ) {Also applies to: 109-118
131-142: 수동 문자열 자르기 대신 TextOverflow.Ellipsis 사용수동 substring은 다국어/이모지 조합에서 깨질 수 있습니다. maxLines+overflow로 대체하세요.
- Text( - text = if (spot.spotName.length > 9) spot.spotName.take(8) + stringResource(R.string.ellipsis) else spot.spotName, + Text( + text = spot.spotName, color = AconTheme.color.White, style = AconTheme.typography.Title5, fontWeight = FontWeight.SemiBold, - maxLines = 1, + maxLines = 1, + overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis, modifier = Modifier .align(Alignment.TopCenter) .fillMaxWidth() .padding(top = 20.dp) .padding(horizontal = 20.dp) ) ... - Text( - text = if (spot.spotName.length > 9) spot.spotName.take(8) + stringResource(R.string.ellipsis) else spot.spotName, + Text( + text = spot.spotName, color = AconTheme.color.White, style = AconTheme.typography.Title5, fontWeight = FontWeight.SemiBold, + maxLines = 1, + overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis, modifier = Modifier .fillMaxWidth() .align(Alignment.TopCenter) .padding(top = 20.dp) .padding(horizontal = 20.dp) )Also applies to: 155-165
80-83: 사소한 정리: 불필요한 Modifier 인자LazyRow에 modifier = Modifier는 불필요합니다. 제거해 가독성을 높일 수 있습니다.
- LazyRow( - modifier = Modifier, + LazyRow( horizontalArrangement = Arrangement.spacedBy(10.dp) ) {core/data/src/main/kotlin/com/acon/core/data/repository/SpotRepositoryImpl.kt (1)
125-134: deleteBookmark에도 캐시 동기화 로직 추가 필요
addBookmark에는 새로운 캐시 동기화 로직이 추가되었지만,deleteBookmark에는 추가되지 않았습니다. 일관성을 위해 삭제 시에도 동일한 캐시 업데이트가 필요할 수 있습니다.override suspend fun deleteBookmark(spotId: Long): Result<Unit> { return runCatchingWith(DeleteBookmarkError()) { spotRemoteDataSource.deleteBookmark(spotId) + val cachedSavedSpots = profileLocalDataSource.getSavedSpots().firstOrNull() + if (cachedSavedSpots != null) + profileLocalDataSource.cacheSavedSpots(profileRemoteDataSource.getSavedSpots().map { + it.toSavedSpot() + }) + profileRepositoryLegacy.fetchSavedSpots().onSuccess { fetched -> (profileInfoCacheLegacy.data.value.getOrNull() ?: return@onSuccess).let { profileInfo -> profileInfoCacheLegacy.updateData(profileInfo.copy(savedSpotLegacies = fetched)) } } } }feature/profile/src/main/java/com/acon/feature/profile/info/viewmodel/ProfileInfoViewModel.kt (2)
108-109: 비동기 작업에서 동기 호출 사용
userRepository.getSignInStatus().first()는 suspend 함수인데intent블록 내에서 직접 호출하면 블로킹될 수 있습니다.fun onUploadClicked() = intent { - if (userRepository.getSignInStatus().first() == SignInStatus.GUEST) { + if (signInStatus.value == SignInStatus.GUEST) { onRequestSignIn() } else { postSideEffect(ProfileInfoSideEffect.NavigateToUpload) } }
115-117: 하드코딩된 문자열 상수화 필요
"click_upload_guest?"문자열이 하드코딩되어 있습니다. 상수로 분리하는 것이 좋습니다.+ companion object { + private const val SIGN_IN_HINT_UPLOAD_GUEST = "click_upload_guest?" + } + fun onRequestSignIn() { - super.onRequestSignIn?.invoke("click_upload_guest?") + super.onRequestSignIn?.invoke(SIGN_IN_HINT_UPLOAD_GUEST) }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (54)
app/src/main/java/com/acon/acon/MainActivity.kt(4 hunks)app/src/main/java/com/acon/acon/MainViewModel.kt(2 hunks)app/src/main/java/com/acon/acon/navigation/AconNavigation.kt(1 hunks)app/src/main/java/com/acon/acon/navigation/nested/ProfileNavigationLegacy.kt(1 hunks)core/data/src/main/kotlin/com/acon/core/data/api/remote/ProfileApi.kt(1 hunks)core/data/src/main/kotlin/com/acon/core/data/datasource/local/ProfileLocalDataSource.kt(2 hunks)core/data/src/main/kotlin/com/acon/core/data/datasource/remote/ProfileRemoteDataSource.kt(1 hunks)core/data/src/main/kotlin/com/acon/core/data/datasource/remote/SpotRemoteDataSource.kt(2 hunks)core/data/src/main/kotlin/com/acon/core/data/di/ApiModule.kt(2 hunks)core/data/src/main/kotlin/com/acon/core/data/di/RepositoryModule.kt(1 hunks)core/data/src/main/kotlin/com/acon/core/data/dto/response/profile/ProfileResponse.kt(1 hunks)core/data/src/main/kotlin/com/acon/core/data/dto/response/profile/SavedSpotResponse.kt(1 hunks)core/data/src/main/kotlin/com/acon/core/data/dto/response/profile/SavedSpotsResponse.kt(1 hunks)core/data/src/main/kotlin/com/acon/core/data/repository/ProfileRepositoryImpl.kt(1 hunks)core/data/src/main/kotlin/com/acon/core/data/repository/SpotRepositoryImpl.kt(2 hunks)core/data/src/main/kotlin/com/acon/core/data/repository/UserRepositoryImpl.kt(3 hunks)core/data/src/main/kotlin/com/acon/core/data/session/SessionHandler.kt(3 hunks)core/data/src/test/java/com/acon/core/data/datasource/remote/ProfileRemoteDataSourceTest.kt(3 hunks)core/data/src/test/java/com/acon/core/data/mapping/ProfileMappingTest.kt(1 hunks)core/data/src/test/java/com/acon/core/data/session/SessionHandlerImplTest.kt(5 hunks)core/designsystem/src/main/java/com/acon/acon/core/designsystem/component/image/DefaultProfileImage.kt(1 hunks)core/model/src/main/java/com/acon/acon/core/model/type/SignInStatus.kt(1 hunks)core/model/src/main/java/com/acon/acon/core/model/type/UserType.kt(0 hunks)core/ui/src/main/java/com/acon/acon/core/ui/base/BaseContainerHost.kt(3 hunks)core/ui/src/main/java/com/acon/acon/core/ui/compose/LocalCompositions.kt(2 hunks)domain/src/main/java/com/acon/acon/domain/repository/ProfileRepository.kt(1 hunks)domain/src/main/java/com/acon/acon/domain/repository/UserRepository.kt(1 hunks)feature/profile/build.gradle.kts(2 hunks)feature/profile/src/androidTest/AndroidManifest.xml(1 hunks)feature/profile/src/androidTest/kotlin/com/acon/feature/profile/info/composable/ProfileInfoScreenTest.kt(1 hunks)feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profile/ProfileViewModelLegacy.kt(2 hunks)feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profile/composable/ProfileScreenContainerLegacy.kt(1 hunks)feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profile/composable/ProfileScreenLegacy.kt(3 hunks)feature/profile/src/main/java/com/acon/feature/profile/TestTags.kt(1 hunks)feature/profile/src/main/java/com/acon/feature/profile/info/composable/ProfileInfoScreen.kt(1 hunks)feature/profile/src/main/java/com/acon/feature/profile/info/composable/ProfileInfoScreenContainer.kt(1 hunks)feature/profile/src/main/java/com/acon/feature/profile/info/composable/ProfileView.kt(1 hunks)feature/profile/src/main/java/com/acon/feature/profile/info/composable/SavedSpotsView.kt(1 hunks)feature/profile/src/main/java/com/acon/feature/profile/info/viewmodel/ProfileInfoViewModel.kt(1 hunks)feature/profile/src/test/kotlin/com/acon/feature/profile/info/viewmodel/ProfileInfoViewModelTest.kt(1 hunks)feature/settings/src/main/java/com/acon/acon/feature/settings/screen/SettingsViewModel.kt(2 hunks)feature/signin/src/main/java/com/acon/acon/feature/signin/screen/SignInScreen.kt(2 hunks)feature/signin/src/main/java/com/acon/acon/feature/signin/screen/SignInScreenContainer.kt(1 hunks)feature/signin/src/main/java/com/acon/acon/feature/signin/screen/SignInViewModel.kt(3 hunks)feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotdetail/composable/SpotDetailScreen.kt(5 hunks)feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotdetail/composable/SpotDetailScreenContainer.kt(1 hunks)feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotdetail/composable/SpotDetailViewModel.kt(2 hunks)feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotlist/SpotListViewModel.kt(2 hunks)feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotlist/composable/SpotEmptyView.kt(4 hunks)feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotlist/composable/SpotItem.kt(3 hunks)feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotlist/composable/SpotListScreen.kt(7 hunks)feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotlist/composable/SpotListScreenContainer.kt(4 hunks)feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotlist/composable/SpotListSuccessView.kt(3 hunks)gradle/libs.versions.toml(6 hunks)
💤 Files with no reviewable changes (1)
- core/model/src/main/java/com/acon/acon/core/model/type/UserType.kt
🧰 Additional context used
🧬 Code graph analysis (10)
core/data/src/main/kotlin/com/acon/core/data/repository/ProfileRepositoryImpl.kt (1)
core/data/src/main/kotlin/com/acon/core/data/error/ErrorUtils.kt (2)
runCatchingWith(7-24)runCatchingWith(26-40)
feature/profile/src/main/java/com/acon/feature/profile/info/composable/ProfileInfoScreen.kt (5)
core/designsystem/src/main/java/com/acon/acon/core/designsystem/component/topbar/AconTopBar.kt (1)
AconTopBar(22-68)feature/profile/src/main/java/com/acon/feature/profile/info/composable/ProfileView.kt (2)
UserProfileView(30-90)GuestProfileView(92-130)feature/profile/src/main/java/com/acon/feature/profile/info/composable/SavedSpotsView.kt (1)
SavedSpotsView(41-106)core/designsystem/src/main/java/com/acon/acon/core/designsystem/component/error/NetworkErrorView.kt (1)
NetworkErrorView(23-65)core/designsystem/src/main/java/com/acon/acon/core/designsystem/component/bottombar/AconBottomBar.kt (1)
AconBottomBar(35-55)
feature/profile/src/main/java/com/acon/feature/profile/info/composable/ProfileView.kt (1)
core/designsystem/src/main/java/com/acon/acon/core/designsystem/component/image/DefaultProfileImage.kt (1)
DefaultProfileImage(11-20)
app/src/main/java/com/acon/acon/navigation/AconNavigation.kt (1)
app/src/main/java/com/acon/acon/navigation/nested/ProfileNavigationLegacy.kt (1)
profileNavigationLegacy(24-103)
feature/profile/src/main/java/com/acon/feature/profile/info/composable/ProfileInfoScreenContainer.kt (2)
feature/profile/src/main/java/com/acon/feature/profile/info/composable/ProfileInfoScreen.kt (1)
ProfileInfoScreen(34-140)feature/profile/src/main/java/com/acon/feature/profile/info/viewmodel/ProfileInfoViewModel.kt (1)
loadState(35-75)
feature/profile/src/main/java/com/acon/feature/profile/info/composable/SavedSpotsView.kt (2)
feature/profile/src/main/java/com/acon/feature/profile/info/composable/ProfileInfoScreenContainer.kt (1)
onSavedSpotItemClick(61-83)core/designsystem/src/main/java/com/acon/acon/core/designsystem/image/ImageUtils.kt (1)
rememberDefaultLoadImageErrorPainter(116-143)
feature/profile/src/androidTest/kotlin/com/acon/feature/profile/info/composable/ProfileInfoScreenTest.kt (1)
feature/profile/src/main/java/com/acon/feature/profile/info/composable/ProfileInfoScreen.kt (1)
ProfileInfoScreen(34-140)
feature/profile/build.gradle.kts (1)
build-logic/convention/src/main/kotlin/utils/DependencyExtensions.kt (2)
testImplementation(29-31)androidTestImplementation(25-27)
app/src/main/java/com/acon/acon/MainActivity.kt (1)
core/ui/src/main/java/com/acon/acon/core/ui/EntryPointUtils.kt (1)
activityComponentEntryPoint(38-44)
app/src/main/java/com/acon/acon/navigation/nested/ProfileNavigationLegacy.kt (1)
feature/profile/src/main/java/com/acon/feature/profile/info/composable/ProfileInfoScreenContainer.kt (1)
ProfileInfoScreenContainer(13-59)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (42)
core/data/src/main/kotlin/com/acon/core/data/dto/response/profile/ProfileResponse.kt (1)
30-30: 도메인 Profile 생성자 시그니처 수동 확인 필요
Profile(nickname, birthDateStatus, imageStatus) 순서가 실제 데이터 클래스 정의와 정확히 일치하는지 직접 검토해주세요. 순서가 어긋날 경우 런타임 매핑 오류가 발생할 수 있습니다.core/designsystem/src/main/java/com/acon/acon/core/designsystem/component/image/DefaultProfileImage.kt (1)
11-20: 심플하고 충분합니다
- 벡터 리소스 + contentDescription 제공 OK. 접근성 기본 충족합니다.
core/data/src/main/kotlin/com/acon/core/data/dto/response/profile/SavedSpotResponse.kt (2)
8-13: 직렬화 어노테이션 및 키 매핑 적절
- @serializable + @SerialName 명시로 API 스키마와 안전하게 매칭됩니다.
15-22: 널/공백 썸네일 처리 로직 합리적
- 공백/널을 Empty로, 그 외 Exist로 분기 명확합니다.
feature/profile/build.gradle.kts (1)
40-42: JUnit Platform 활성화 LGTM
- Kotest/Orbit 테스트 실행을 위한 설정으로 적절합니다.
feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotlist/SpotListViewModel.kt (1)
21-21: SignInStatus 마이그레이션은 적절 — ViewModel 주입·쿨다운 단위 검증 필요
- 게스트 제외 조건(signInStatus.value != SignInStatus.GUEST)은 기존 의도와 일치합니다. (파일: feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotlist/SpotListViewModel.kt:21)
- isCooldownExpiredUseCase(UserActionType.SKIP_AREA_VERIFICATION, 24 * 60 * 60)의 시간 단위를 명확히 하세요(초/분/밀리초). 단위가 틀리면 영역 인증 모달 노출 빈도에 영향이 있습니다.
- 자동 검사 스크립트가 'unrecognized file type: kt' 에러로 실패했습니다. 아래 수정된 명령을 실행하거나 해당 심볼의 정의/사용 위치(파일 + 코드 스니펫)를 제공하세요:
#!/bin/bash rg -n -C2 "signInStatus" feature/spot rg -n -C2 "IsCooldownExpiredUseCase" . rg -n -C2 "SKIP_AREA_VERIFICATION" .core/data/src/main/kotlin/com/acon/core/data/datasource/remote/SpotRemoteDataSource.kt (1)
20-24: 모든 fetchSpotList 호출부가 새 시그니처(SpotListRequest, SignInStatus)로 제대로 업데이트되었습니다feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profile/ProfileViewModelLegacy.kt (2)
6-6: LGTM! SignInStatus로 타입 변경UserType에서 SignInStatus로의 전역적인 타입 마이그레이션이 올바르게 적용되었습니다.
22-24: LGTM! SignInStatus 사용으로 업데이트컨테이너 초기화에서 signInStatus.collect와 SignInStatus.GUEST 사용이 올바르게 적용되었습니다.
feature/signin/src/main/java/com/acon/acon/feature/signin/screen/SignInScreenContainer.kt (1)
40-40: LGTM! useSignInStatus 메소드 호출UserType에서 SignInStatus로의 마이그레이션에 맞춰 메소드 호출이 올바르게 업데이트되었습니다.
feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profile/composable/ProfileScreenContainerLegacy.kt (1)
69-69: LGTM! useSignInStatus 메소드 호출프로필 화면에서도 SignInStatus 사용으로 일관성 있게 업데이트되었습니다.
feature/signin/src/main/java/com/acon/acon/feature/signin/screen/SignInScreen.kt (2)
43-43: LGTM! LocalSignInStatus 컴포지션 로컬 사용LocalUserType에서 LocalSignInStatus로의 변경이 올바르게 적용되었습니다.
84-84: LGTM! SignInStatus 타입 사용변수명과 비교 조건에서 SignInStatus 타입 사용이 일관성 있게 적용되었습니다.
Also applies to: 91-91
domain/src/main/java/com/acon/acon/domain/repository/UserRepository.kt (1)
6-6: LGTM! 도메인 레포지토리 API 업데이트UserType에서 SignInStatus로의 마이그레이션이 도메인 레이어에서 올바르게 적용되었습니다. 메소드명도 의미에 맞게
getSignInStatus로 변경되었습니다.Also applies to: 14-14
feature/signin/src/main/java/com/acon/acon/feature/signin/screen/SignInViewModel.kt (2)
7-7: LGTM! SignInStatus 임포트 추가UserType 대신 SignInStatus 임포트로 올바르게 변경되었습니다.
28-28: LGTM! SignInStatus 사용 로직signIn 메소드에서 SignInStatus.GUEST 사용과 signInStatus.collectLatest 호출이 올바르게 적용되었습니다.
Also applies to: 44-45
core/data/src/main/kotlin/com/acon/core/data/datasource/remote/ProfileRemoteDataSource.kt (1)
34-34: LGTM! API 응답 래퍼 언패킹SavedSpotsResponse 래퍼에서 savedSpotList를 추출하여 반환하는 로직이 올바르게 구현되었습니다. 이는 API 응답 구조 변경에 맞춘 적절한 데이터 소스 계층 처리입니다.
feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotdetail/composable/SpotDetailViewModel.kt (1)
63-71: 딥링크 판정 로직 변경 의도 확인 요청주석엔 “딥링크 && GUEST일 때만 isDeepLink = true”라 쓰였으나, 현재는 로그인 상태와 무관하게 isDeepLink가 전달 파라미터만으로 결정됩니다. 서버/캐시 경로 선택(fetchSpotDetail vs fetchSpotDetailFromUser)에 영향이 크니 의도한 정책인지 확인 부탁드립니다.
Also applies to: 76-80
core/ui/src/main/java/com/acon/acon/core/ui/compose/LocalCompositions.kt (1)
22-24: LocalUserType → LocalSignInStatus 전환 — 주석 한 곳만 잔존LGTM. feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotdetail/composable/SpotDetailViewModel.kt (라인 66–67) 주석에 'UserType.GUEST' 참조만 남아 있습니다. 주석 정리 또는 SignInStatus로 업데이트하세요.
core/data/src/main/kotlin/com/acon/core/data/dto/response/profile/SavedSpotsResponse.kt (1)
6-9: 확인 결과: API, 테스트, 매퍼 반영 완료됨
ProfileApi.getSavedSpots()시그니처가SavedSpotsResponse로 변경되어 있습니다.ProfileRemoteDataSourceTest내에SavedSpotsResponse생성 및 검증 로직이 정상 작동합니다.- 매퍼 코드(
*Mapper*.kt파일)에서SavedSpotsResponse.savedSpotList를 도메인 모델로 변환하는 로직도 이미 존재합니다.
따라서 추가 수정 필요 없습니다.core/data/src/main/kotlin/com/acon/core/data/di/RepositoryModule.kt (1)
73-75: 승인 — 레거시/신규 ProfileRepository 바인딩 분리 적절core/data/src/main/kotlin/com/acon/core/data/di/RepositoryModule.kt에서 ProfileRepository와 ProfileRepositoryLegacy가 각각 @singleton + @BINDS로 분리 등록되어 있으며, 도메인/피처 소비자들이 각 타입을 올바르게 주입하고 있습니다(예: ValidateNicknameUseCase → ProfileRepository, SpotDetailViewModel 등 → ProfileRepositoryLegacy).
feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profile/composable/ProfileScreenLegacy.kt (1)
46-47:LocalSignInStatus로의 마이그레이션이 적절합니다.
LocalUserType에서LocalSignInStatus로의 변경이 전체 코드베이스의 리팩토링과 일관성 있게 적용되었습니다.Also applies to: 67-67, 317-317
core/data/src/main/kotlin/com/acon/core/data/repository/UserRepositoryImpl.kt (2)
31-31: 프로필 캐시 정리 로직이 적절하게 추가되었습니다.로그아웃 시
profileLocalDataSource.clearCache()를 호출하여 프로필 관련 캐시를 정리하는 것이 올바른 접근입니다.Also applies to: 75-75
34-34: 메서드 이름 변경이 도메인 계층과 일치합니다.
getUserType()에서getSignInStatus()로의 변경이 도메인 계층의 인터페이스와 일관성 있게 적용되었습니다.core/data/src/test/java/com/acon/core/data/session/SessionHandlerImplTest.kt (1)
5-5: 테스트가SignInStatus타입 변경을 올바르게 반영합니다.모든 테스트 케이스가
UserType에서SignInStatus로의 변경을 일관되게 적용하고 있으며, 기대값과 어서션이 적절합니다.Also applies to: 69-73, 91-91, 107-107, 125-125
feature/profile/src/main/java/com/acon/feature/profile/TestTags.kt (1)
3-11: 테스트 태그 상수 정의가 적절합니다.UI 테스트를 위한 태그 상수들을 별도 객체로 중앙화한 것이 유지보수성 측면에서 좋은 접근입니다.
core/data/src/main/kotlin/com/acon/core/data/session/SessionHandler.kt (1)
57-62: onSignInResponse의 사용처 및 목적 확인 필요core/data/src/main/kotlin/com/acon/core/data/session/SessionHandler.kt(선언: 라인 20, 구현: 라인 57–62)에 선언·구현만 존재하며 프로젝트 내 호출처가 없습니다. 현재 구현은 access/refresh 토큰만 저장해 completeSignIn과 기능이 중복되고 사용자 상태 업데이트가 없습니다.
- 외부 콜백 의도라면 호출처 또는 문서화 추가
- 중복이면 completeSignIn으로 통합하거나 onSignInResponse 제거
- 사용자 상태 갱신이 필요하면 해당 로직 추가
core/data/src/main/kotlin/com/acon/core/data/di/ApiModule.kt (2)
10-10: 새 API 타입 import: OKProfileApi 도입에 맞는 import 추가 확인했습니다.
79-83: 레거시 프로필 API 프로바이더 확인 — 중복 바인딩 없음ProfileAuthApiLegacy를 제공하는 바인딩은 core/data/src/main/kotlin/com/acon/core/data/di/ApiModule.kt의 providesProfileApiLegacy 하나뿐입니다. 변경 승인.
feature/settings/src/main/java/com/acon/acon/feature/settings/screen/SettingsViewModel.kt (1)
3-3: SignInStatus로의 전환: OK도메인 타입 교체( UserType → SignInStatus ) 반영 적절합니다.
feature/profile/src/main/java/com/acon/feature/profile/info/composable/ProfileInfoScreen.kt (1)
121-138: 업로드 탭 전환 시 게스트 가드 확인 필요이 화면에서는 업로드 탭 클릭 시 즉시 actions.onUploadTabClick만 호출합니다. 게스트의 경우 로그인 유도(바텀시트) 등 상위 컨테이너에서 가드되는지 확인이 필요합니다.
해당 컨테이너(ProfileInfoScreenContainer)에서 SignInStatus로 가드하고 있는지 한번 점검 부탁드립니다.
feature/profile/src/test/kotlin/com/acon/feature/profile/info/viewmodel/ProfileInfoViewModelTest.kt (1)
52-70: 초기화 시나리오 검증 좋음프로필/저장소트 조합별 성공/실패 상태를 명확히 검증하고 있어 회귀 방지에 유효합니다.
혹시 초기 진입 시 첫 상태(Loading 등)도 한 번 expectState로 스냅샷해 두면 변경 감지에 도움이 됩니다.
Also applies to: 80-90, 100-111
app/src/main/java/com/acon/acon/MainActivity.kt (1)
345-347: Hilt EntryPoint 바인딩 검증 필요 — 로컬 재검증 요청AuthClientEntryPoint의 @EntryPoint/@Installin(예: ActivityComponent) 스코프와 googleAuthClient를 제공하는 모듈(@Provides/@BINDS) 연결을 확인하세요. 샌드박스에서 검색이 실패해 로컬에서 아래 명령을 실행한 출력 결과를 붙여 주세요.
# repo 루트에서 실행 grep -nR --exclude-dir=build -E "interface\s+AuthClientEntryPoint\b|@EntryPoint|@InstallIn" || true grep -nR --exclude-dir=build "googleAuthClient\(" || true grep -nR --exclude-dir=build -E "(Provides|Binds).*GoogleAuthClient" || trueapp/src/main/java/com/acon/acon/navigation/nested/ProfileNavigationLegacy.kt (1)
41-43: 검증 필요 — SpotRoute.SpotDetail이 SpotNavigationParameter를 기대합니다core/navigation/src/main/java/com/acon/acon/core/navigation/route/SpotRoute.kt에서 SpotRoute.SpotDetail은 com.acon.acon.core.model.model.spot.SpotNavigationParameter 타입의 인자(spotNavigationParameter)를 받습니다.
onNavigateToSpotDetail = { navController.navigate(SpotRoute.SpotDetail(it)) },
- 확인: app/src/main/java/com/acon/acon/navigation/nested/ProfileNavigationLegacy.kt(41–43)에서 onNavigateToSpotDetail이 전달하는 'it'의 타입이 com.acon.acon.core.model.model.spot.SpotNavigationParameter인지 확인.
- 조치: 'it'이 다른 타입이면 SpotNavigationParameter로 변환/매핑하여 전달하거나 SpotRoute.SpotDetail 생성자(또는 호출부)를 해당 타입에 맞게 수정.
레포지토리에서 onNavigateToSpotDetail(또는 ProfileInfoScreenContainer)의 시그니처를 찾지 못했습니다 — 해당 정의(파일/라인)를 제공하면 재검증합니다.
core/data/src/main/kotlin/com/acon/core/data/repository/SpotRepositoryImpl.kt (1)
109-113: SavedSpots 캐시 동기화 — StateFlow 확인 및 조건·오류 처리 필요
- 검증: ProfileLocalDataSource.getSavedSpots()는 MutableStateFlow.asStateFlow()로 구현되어 있어 .firstOrNull()은 StateFlow의 현재 값을 즉시 반환합니다. (따라서 “firstOrNull이 최신성 보장 안함” 지적은 부정확)
- 동작 확인 필요: 현재 if (cachedSavedSpots != null) { … } 로 캐시가 null일 때 원격 동기화를 하지 않습니다. 의도 확인 — 항상 서버에서 갱신해야 하면 null 체크 제거, 호출을 줄이려는 의도면 주석으로 명확히 표시.
- 오류 처리 추가 필요: profileRemoteDataSource.getSavedSpots() 호출을 try/catch 또는 runCatching으로 감싸서 실패 시 로깅/재시도/사용자 알림 등 처리를 추가하세요.
gradle/libs.versions.toml (7)
11-11: 컴포즈 컴파일러 버전 업그레이드 확인컴포즈 컴파일러 버전이 1.5.1에서 1.5.15로 업그레이드되었습니다. 이는 최신 안정 버전으로의 적절한 업데이트입니다.
21-21: KSP 버전 추가 확인KSP(Kotlin Symbol Processing) 플러그인이 프로젝트에 새로 추가되었습니다. Kotlin 2.0.21과 호환되는 KSP 1.0.27 버전이 올바르게 설정되었습니다.
83-83: Kotest 테스팅 프레임워크 추가Kotest 5.9.1이 새로 추가되어 JUnit 기반 테스트를 보완할 수 있게 되었습니다. 최신 안정 버전으로 적절한 선택입니다.
136-141: 테스트 관련 라이브러리 추가새로운 테스트 관련 라이브러리들이 추가되었습니다:
mockk-android: Android 환경 테스트용 모킹orbit-test: Orbit MVI 테스트 지원- Kotest 관련 라이브러리들: runner, assertions, property testing
모든 버전이 적절하게 참조되고 있습니다.
242-242: Kotlin JVM 플러그인 버전 참조 수정
jetbrains-kotlin-jvm플러그인이 이제kotlin버전을 올바르게 참조하도록 수정되었습니다. 이는 일관성 있는 Kotlin 버전 관리를 위한 좋은 개선사항입니다.
274-276: 번들 구성 추가새로운 테스트 관련 번들들이 추가되었습니다:
android-test: Android 테스트 라이브러리 번들orbit-test: Orbit 테스트 번들kotest: Kotest 라이브러리 번들이러한 번들화는 build.gradle 파일에서의 의존성 관리를 단순화합니다.
38-38: Orbit MVI 10.0.0 업그레이드 — 1차 검증 결과레포 검색 결과 여러 ViewModel에서 ContainerHost / container(...) 사용 및 orbit-test 사용이 확인되었고, Orbit 10.0.0 공식 문서에 ContainerHost 인터페이스·ViewModel용 container() 팩토리·test() API가 10.0.0에 존재하므로 문서 기준 즉시 API 제거로 인한 깨짐은 보이지 않습니다. (orbit-mvi.org)
- 예시 위치: core/ui/src/main/java/com/acon/acon/core/ui/base/BaseContainerHost.kt, feature/upload/src/main/java/com/acon/acon/feature/upload/screen/UploadPlaceViewModel.kt, feature/profile/src/test/kotlin/com/acon/feature/profile/info/viewmodel/ProfileInfoViewModelTest.kt.
- 권장 조치: 전체 빌드 및 단위테스트로 컴파일/런타임 호환성 확인(예: ./gradlew build && ./gradlew test).
💻 Work Description
Summary by CodeRabbit
개선
테스트
작업