-
Notifications
You must be signed in to change notification settings - Fork 0
[Feat/#248] 프로필 리팩토링 완료, 이슈 일부 수정 #258
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
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
Walkthrough
Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User
participant VM as AreaVerificationViewModel
participant Repo as OnboardingRepository
participant Pref as OnboardingPrefs
participant UI as AreaVerificationScreenContainer
User->>VM: onSkipClicked()
VM->>Repo: getOnboardingPreferences()
Repo-->>VM: Result(preferences)
alt failure or !hasTastePreference
VM-->>UI: SideEffect.NavigateToChooseDislikes
else shouldShowIntroduce
VM-->>UI: SideEffect.NavigateToIntroduce
UI->>Nav: onNavigateToIntroduce()
else
VM-->>UI: SideEffect.NavigateToSpotList
UI->>Nav: onNavigateToSpotList()
end
sequenceDiagram
autonumber
participant OR as OnboardingRepository
participant DS as DataStream(@VerifiedArea)
participant PR as ProfileRepositoryImpl
participant RDS as ProfileRemoteDataSource
participant VM as UserVerifiedAreasViewModel
OR->>DS: notifyDataChanged()
note right of DS: 트리거 방출(replay=1)
VM->>PR: getVerifiedAreas() (collect)
PR->>DS: subscribe { block }
DS-->>PR: trigger event
PR->>RDS: getVerifiedAreas()
RDS-->>PR: VerifiedAreaListResponse
PR-->>VM: Result<List<Area>>
sequenceDiagram
autonumber
participant PR as ProfileRepositoryImpl
participant AR as AconAppRepository
participant CR as ContentResolver
participant RS as Remote Storage
participant API as App API
PR->>AR: uploadImage(ImageType.PROFILE, uri)
AR->>CR: openInputStream(uri), getType(uri)
AR->>API: requestPreSignedUrl(fileName, mime)
API-->>AR: PreSigned URL
AR->>RS: PUT image bytes (Content-Type)
RS-->>AR: 200 OK
AR-->>PR: Result(fileUrl)
PR->>PR: update profile with fileUrl
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests
Comment |
1971123-seongmin
left a 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.
고생하셨습니다
| reduce { | ||
| BookmarkUiState.LoadFailed | ||
| delay(LOADING_DELAY_MILLIS) | ||
| profileRepository.getSavedSpots().collect { result -> |
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.
북마크도 캐시데이터에 추가한건가요??
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.
넹 프로필 들어갈 때마다 안끊기게 하려구여
| longitude = longitude | ||
| ) | ||
| onboardingLocalDataSource.updateHasVerifiedArea(true) | ||
| areaDataStream.notifyDataChanged() |
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.
여기서 DataStream을 쓰는 이유는 무엇인가요??
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.
설정에서 새롭게 지역인증 -> 저장 api 호출해서 지역인증함 -> 이전화면으로 돌아옴
여기서 돌아왔을 때, notify를 해주지 않으면 이전화면에서 새로 저장된 지역이 있는지 알 수 없기 때문입니다
| import kotlinx.coroutines.flow.transformLatest | ||
| import javax.inject.Inject | ||
|
|
||
| interface DataStream { |
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.
이 DataStream을 사용하는 곳을 몇개 봤는데 (프로필? , 인증동네 확인 뷰모델, 온보딩) 왜 이걸 새로 만드셨는지 궁금합니다ㅏ
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.
데이터 변화 알려주는거에요
간단하게, 데이터를 새로 저장하면, 원래 그 데이터를 쓰던곳에선 변화를 감지해야 하잖아요? 그걸 기능화 한것입니다.
| profileRemoteDataSource.getVerifiedAreas().verifiedAreaList | ||
| .map { it.toVerifiedArea() } | ||
| }) | ||
| } |
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.
여기서 DataStream을 이유는 무엇인가요?? (기존 앱 버전에서는 지역인증을 하고, 인증동네 확인 화면으로 돌아왔을 때, 바로 추가가 안되고 나갔다가 들어와야하는 문제가 있는거로 알고있는데 그거 때문인가요??)
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.
네 그거 맞습니다. (위 코멘트와 같은 맥락)
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: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotdetail/composable/SpotDetailViewModel.kt (1)
86-92: getVerifiedAreas 흐름: 예외/미발행 시 로딩 취소·중단 가능성 — 안전 가드 추가 권장Flow가 예외를 던지거나 첫 emission 없이 완료되면 await 시 intent가 실패/중단될 수 있습니다. 기본값을 보장하고 예외를 흡수하도록 수정해주세요.
다음 diff 제안을 적용하면 Result를 항상 non-null로 유지합니다.
- val verifiedAreaListDeferred = viewModelScope.async { - if (signInStatus.value != SignInStatus.GUEST) { - profileRepository.getVerifiedAreas().firstOrNull() - } else { - Result.success(emptyList()) - } - } + val verifiedAreaListDeferred = viewModelScope.async { + if (signInStatus.value != SignInStatus.GUEST) { + try { + profileRepository + .getVerifiedAreas() + .firstOrNull() ?: Result.success(emptyList()) + } catch (t: Throwable) { + Result.success(emptyList()) + } + } else { + Result.success(emptyList()) + } + }feature/settings/src/main/java/com/acon/acon/feature/verification/screen/UserVerifiedAreasViewModel.kt (1)
84-106: Timber 사용법 오류: 로그 메시지가 TAG로 출력되는 버그
Timber.e(TAG, "...")는 TAG를 메시지 포맷으로 해석하여 실제 메시지가 출력되지 않습니다.Timber.tag(TAG).e("...")또는Timber.e("...")로 수정하세요. 예외 포함 시Timber.tag(TAG).e(error, "...").수정 예시:
- Timber.e(TAG, "유효하지 않은 인증 지역입니다.") + Timber.tag(TAG).e("유효하지 않은 인증 지역입니다.") @@ - Timber.e(TAG, "인증 지역은 최소 1개 이상 존재해야 합니다.") + Timber.tag(TAG).e("인증 지역은 최소 1개 이상 존재해야 합니다.") @@ - Timber.e(TAG, "인증일로부터 1주 이상 3개월 미만인 지역은 삭제할 수 없습니다.") + Timber.tag(TAG).e("인증일로부터 1주 이상 3개월 미만인 지역은 삭제할 수 없습니다.") @@ - Timber.e(TAG, "존재하지 않는 인증 동네입니다.") + Timber.tag(TAG).e("존재하지 않는 인증 동네입니다.") @@ - Timber.e(TAG, error.message) + Timber.tag(TAG).e(error, "알 수 없는 오류가 발생했습니다.")
🧹 Nitpick comments (35)
feature/profile/src/main/java/com/acon/feature/profile/savedspot/composable/BookmarkItem.kt (6)
36-40: 접근성: 카드에 Role 부여 및 의미 병합으로 스크린리더 읽기 개선카드 전체가 클릭 가능하지만 Role/Button 지정과 의미 병합이 없어 보이스오버/톡백에서 의도 전달이 약합니다. 아래처럼 role 지정 및 mergeDescendants로 텍스트만 읽히도록 제안합니다.
- modifier = modifier - .clip(RoundedCornerShape(8.dp)) - .clickable { onClickSpotItem() } + modifier = modifier + .clip(RoundedCornerShape(8.dp)) + .semantics(mergeDescendants = true) { role = Role.Button } + .clickable(onClick = onClickSpotItem)추가 import:
import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.mergeDescendants import androidx.compose.ui.semantics.role import androidx.compose.ui.semantics.semantics
43-51: 배경 이미지는 장식용으로 처리: contentDescription = null배경 이미지는 정보 전달 목적이 아니므로 스크린리더에 불필요한 잡음을 유발할 수 있습니다. Empty 브랜치와 동일하게 null 권장.
- contentDescription = stringResource(R.string.store_background_image_content_description), + contentDescription = null,
53-66: 텍스트 수동 절단(Substring) 대신 Ellipsize 사용 + 중앙 정렬수동 절단은 다국어(조합형/서로게이트)에서 글자 깨짐 위험이 있고, 폭 기준이 아닌 문자 수 기준으로 잘려 UX가 들쭉날쭉합니다. 한 줄/말줄임 + 중앙 정렬을 권장합니다.
- Text( - text = spot.spotName.let { name -> - if (name.length > 9) name.take(8) + "…" else name - }, - color = AconTheme.color.White, - style = AconTheme.typography.Title5, - fontWeight = FontWeight.SemiBold, - modifier = Modifier - .align(Alignment.TopCenter) - .fillMaxWidth() - .padding(top = 20.dp) - .padding(horizontal = 20.dp) - ) + Text( + text = spot.spotName, + color = AconTheme.color.White, + style = AconTheme.typography.Title5, + fontWeight = FontWeight.SemiBold, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + textAlign = TextAlign.Center, + modifier = Modifier + .align(Alignment.TopCenter) + .fillMaxWidth() + .padding(top = 20.dp, start = 20.dp, end = 20.dp) + )추가 import:
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow참고: 만약 문자 수 기준 고정이 꼭 필요하다면 조건을
> 8로 두는 편이 명세(8자+말줄임)에 더 부합합니다.
78-90: Empty 브랜치도 동일한 Ellipsize/정렬 적용브랜치 간 UI 일관성을 위해 위와 동일한 변경을 적용하세요.
- Text( - text = spot.spotName.let { name -> - if (name.length > 9) name.take(8) + "…" else name - }, - color = AconTheme.color.White, - style = AconTheme.typography.Title5, - fontWeight = FontWeight.SemiBold, - modifier = Modifier - .fillMaxWidth() - .align(Alignment.TopCenter) - .padding(top = 20.dp) - .padding(horizontal = 20.dp) - ) + Text( + text = spot.spotName, + color = AconTheme.color.White, + style = AconTheme.typography.Title5, + fontWeight = FontWeight.SemiBold, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .align(Alignment.TopCenter) + .padding(top = 20.dp, start = 20.dp, end = 20.dp) + )
53-66: 중복 텍스트 오버레이 추출로 유지보수성 향상타이틀 Text 블록이 두 브랜치에서 중복됩니다. 작은 컴포저블로 추출하면 스타일 변경 시 리스크/비용이 줄어듭니다.
예시(파일 내 임의 위치 추가):
@Composable private fun SpotNameTopOverlay( name: String, modifier: Modifier = Modifier ) { Text( text = name, color = AconTheme.color.White, style = AconTheme.typography.Title5, fontWeight = FontWeight.SemiBold, maxLines = 1, overflow = TextOverflow.Ellipsis, textAlign = TextAlign.Center, modifier = modifier .fillMaxWidth() .padding(top = 20.dp, start = 20.dp, end = 20.dp) ) }사용:
SpotNameTopOverlay( name = spot.spotName, modifier = Modifier.align(Alignment.TopCenter) )Also applies to: 78-90
104-113: 프리뷰: 긴 이름/이미지 존재 케이스 추가 권장긴 이름, 이미지 존재(Exist) 케이스도 함께 보면 회귀를 쉽게 잡을 수 있습니다.
@Preview @Composable private fun BookmarkItemPreview_LongName_Exist() { AconTheme { BookmarkItem( spot = SavedSpot( 0, "매우매우긴스팟이름매우매우긴스팟이름", SpotThumbnailStatus.Exist(url = "https://example.com/image.jpg") ), onClickSpotItem = {} ) } }feature/profile/src/main/java/com/acon/feature/profile/savedspot/viewmodel/BookmarkViewModel.kt (4)
50-53: @immutable + List는 ‘깊은 불변’ 보장이 없습니다. 불변 컬렉션으로 바꾸거나 주석을 제거하세요.현재
@Immutable로 표시했지만List<SavedSpot>는 외부에서 변형 가능해 Compose 안정성 가정이 깨질 수 있습니다.
- 옵션 A(권장):
kotlinx.collections.immutable.PersistentList로 타입을 바꾸고toPersistentList()로 스냅샷 저장.- 옵션 B(최소 변경):
@Immutable제거.가능한 최소 수정 예시:
- @Immutable - data class Success( - val savedSpots: List<SavedSpot> - ) : BookmarkUiState + data class Success( + val savedSpots: List<SavedSpot> // 외부 변형 금지 보장 어려우면 @Immutable 제거 + ) : BookmarkUiState불변 컬렉션을 사용한다면:
+import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.toPersistentList ... - data class Success( - val savedSpots: List<SavedSpot> - ) : BookmarkUiState + @Immutable + data class Success( + val savedSpots: PersistentList<SavedSpot> + ) : BookmarkUiState그리고 reduce 측:
- reduce { BookmarkUiState.Success(savedSpots = it) } + reduce { BookmarkUiState.Success(savedSpots = it.toPersistentList()) }
19-32: 실패 후 재시도 경로가 보이지 않습니다.UI에서
NetworkErrorView(onRetry = LocalOnRetry.current)를 쓰는데 VM에retry()/refresh()가 없습니다. 재시도 시 다시 로딩/재구독되는지 확인이 필요합니다. 필요 시 간단한retry()intent 추가를 권합니다.원하시면 Orbit 패턴에 맞춘
retry()/repeatOnSubscription기반 예시를 드릴게요.
20-20: 의도적 로딩 지연 800ms는 UX에 과합니다.짧은 응답에도 최소 스켈레톤 노출이 목적이면 200~300ms 수준으로 낮추거나 “최소 노출시간 보장”만 하세요.
- private const val LOADING_DELAY_MILLIS = 800L + private const val LOADING_DELAY_MILLIS = 250LAlso applies to: 44-46
21-31: 불필요한 재구성 줄이기: distinctUntilChanged 권장동일 데이터가 반복 방출될 때 불필요한
reduce호출을 줄이세요.- profileRepository.getSavedSpots().collect { result -> + profileRepository.getSavedSpots() + .distinctUntilChanged() + .collect { result ->feature/profile/src/main/java/com/acon/feature/profile/savedspot/composable/BookmarkScreenContainer.kt (1)
28-33: 사이드이펙트 수집 시 재조합 최소화 보장 확인
collectSideEffect는 내부적으로LaunchedEffect이므로 안전하나, 네비게이션 람다 캡처가 최신인지 확인 바랍니다. 필요 시rememberUpdatedState로 감싸는 방법도 있습니다.feature/profile/src/main/java/com/acon/feature/profile/savedspot/composable/BookmarkSkeletonItem.kt (2)
31-32: padding 체이닝 단순화중복 padding 호출을 합치면 가독성이 좋아집니다.
- .padding(top = 20.dp) - .padding(horizontal = 20.dp), + .padding(top = 20.dp, horizontal = 20.dp),
23-25: 이중 스켈레톤 효과 여부 확인
Box.skeleton(...)와 내부SkeletonItem둘 다 스켈레톤 처리를 하는지 확인 필요합니다. 중복이면 한쪽만 유지하세요.Also applies to: 27-34
feature/profile/src/main/java/com/acon/feature/profile/savedspot/composable/BookmarkScreen.kt (3)
166-181: 스크롤 성능: Column + chunked 대신 LazyVerticalGrid 사용 권장데이터가 많아지면 전체 컴포지션/측정 비용이 커집니다.
LazyVerticalGrid(columns = GridCells.Fixed(2))로 전환하고key(spot.spotId)를 지정하세요.간단 예시:
LazyVerticalGrid( columns = GridCells.Fixed(2), verticalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp), contentPadding = PaddingValues(top = 72.dp, start = 16.dp, end = 16.dp, bottom = 16.dp) ) { items( items = state.savedSpots, key = { it.spotId } ) { spot -> BookmarkItem( spot = spot, onClickSpotItem = { onSpotClick(spot.spotId) }, modifier = Modifier .aspectRatio(160f / 231f) ) } }
104-111: 불필요 변수명 사용로딩 스켈레톤 루프에서
spot변수를 쓰지 않습니다. 의미 없는 식별자는_로 교체하세요.- rowItems.forEach { spot -> + rowItems.forEach { _ -> BookmarkSkeletonItem(
64-87: TopBar 중복 코드 추출 제안Loading/Success에 동일 TopBar가 반복됩니다.
@Composable private fun BookmarkTopBar(onBack: ()->Unit)로 추출하여 중복 제거하세요.Also applies to: 133-156
feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotdetail/composable/SpotDetailViewModel.kt (1)
99-101: nullable Result 체인 단순화위 수정으로 항상 non-null Result를 보장하면 안전연산자가 불필요합니다.
- val isAreaVerified = verifiedAreaListResult - ?.getOrNull() - .orEmpty() - .isNotEmpty() + val isAreaVerified = verifiedAreaListResult + .getOrNull() + .orEmpty() + .isNotEmpty()app/src/main/java/com/acon/acon/navigation/nested/ProfileNavigation.kt (2)
48-56: SpotList로의 복귀가 백스택에 없을 때 무효(no-op)일 수 있음현재 popBackStack(SpotRoute.SpotList, inclusive = false)은 대상이 백스택에 없으면 이동하지 않습니다. 없을 때는 navigateAndClear로 폴백하는 게 안전합니다.
- onNavigateToSpotList = { - navController.popBackStack( - route = SpotRoute.SpotList, - inclusive = false - ) - }, + onNavigateToSpotList = { + if (!navController.popBackStack(route = SpotRoute.SpotList, inclusive = false)) { + navController.navigateAndClear(SpotRoute.SpotList) + } + },
73-86: SpotNavigationParameter는 이름 있는 인자를 권장생성자 인자 순서 변화에 취약합니다. 이름 있는 인자로 전환하면 회귀를 줄일 수 있습니다.
- SpotRoute.SpotDetail( - SpotNavigationParameter( - it, - emptyList(), - null, - null, - null, - true - ) - ) + SpotRoute.SpotDetail( + SpotNavigationParameter( + spotId = it, + tags = emptyList(), + addr = null, + category = null, + distance = null, + fromProfile = true + ) + )domain/src/main/java/com/acon/acon/domain/repository/AconAppRepository.kt (1)
5-8: uploadImage 매개변수 의미가 모호합니다(로컬 Uri vs. 프리사인드 URL). 명확한 네이밍/KDoc 제안현재 url 이름만으로는 의도가 불분명합니다. 실제 의도에 따라 아래 둘 중 하나로 명시해 주세요. 반환값의 의미(K/V/절대 URL 등)도 KDoc으로 명확히 해두면 호출부 혼선을 줄일 수 있습니다.
옵션 A: 로컬 콘텐츠 Uri를 업로드
interface AconAppRepository { suspend fun shouldUpdateApp(currentVersion: String): Result<Boolean> - suspend fun uploadImage(imageType: ImageType, url: String): Result<String> + /** + * 주어진 로컬 콘텐츠 URI의 이미지를 업로드합니다. + * @param imageType 업로드 대상 이미지 종류(예: PROFILE, SPOT 등) + * @param contentUri ContentResolver로 읽을 수 있는 로컬 콘텐츠 URI 문자열 + * @return 업로드 결과 리소스 식별자(예: 버킷 키 또는 공개 URL) + */ + suspend fun uploadImage(imageType: ImageType, contentUri: String): Result<String> }옵션 B: 서버가 발급한 프리사인드 URL로 업로드
- suspend fun uploadImage(imageType: ImageType, url: String): Result<String> + /** + * 프리사인드 URL을 통해 이미지를 업로드합니다. + * @param presignedUrl 업로드 대상 프리사인드 URL + * @return 업로드 성공 시 서버에서 사용하는 리소스 식별자(예: 버킷 키) + */ + suspend fun uploadImage(imageType: ImageType, presignedUrl: String): Result<String>app/src/main/java/com/acon/acon/navigation/nested/AreaVerificationNavigation.kt (1)
35-39: skippable 계산은 LocalNavController 대신 인자 navController 사용 권장동일 인스턴스일 가능성이 높지만, 조합 가능한 중첩 네비 구조에서는 CompositionLocal보다 명시 인자를 사용하는 편이 안전합니다.
- skippable = LocalNavController.current.hasPreviousBackStackEntry().not(), + skippable = navController.hasPreviousBackStackEntry().not(),feature/onboarding/src/main/java/com/acon/feature/onboarding/area/viewmodel/AreaVerificationViewModel.kt (1)
15-18: 건너뛰기 분기 로직 설계 적절(선호도/인트로/스팟리스트 순)실패 시 ChooseDislikes 폴백, 선호도 미설정 우선, 그다음 Introduce, 마지막 SpotList로의 분기 흐름이 명확합니다.
해당 분기(성공/실패, 각 flag 조합)에 대한 단위 테스트 추가를 원하시면 케이스 스켈레톤을 만들어 드릴게요.
Also applies to: 32-45, 71-74
core/data/src/test/java/com/acon/core/data/repository/ProfileRepositoryTest.kt (1)
52-54: DataStream 목을 relaxed로 두어 불필요한 스텁/검증 이슈 예방구현체가 내부에서 notify 등을 호출해도 테스트가 깨지지 않도록 relaxed mock을 권장합니다.
- @MockK - private lateinit var dataStream: DataStream + @MockK(relaxed = true) + private lateinit var dataStream: DataStream또한 setUp에서 별도 스텁이 필요 없다면 현 상태가 가장 단순합니다.
Also applies to: 65-68
core/data/src/main/kotlin/com/acon/core/data/repository/AconAppRepositoryImpl.kt (4)
23-26: MIME 타입 허용 범위 보완 제안
- Android가 반환할 수 있는
image/heif가 누락되어 HEIF가 JPEG로 업로드될 수 있습니다.image/jpg는 비표준이므로image/jpeg로 충분하나, 유지해도 해는 없습니다.권장 수정:
- private val availableImageMimeTypes by lazy { - setOf("image/jpg", "image/jpeg", "image/png", "image/webp", "image/heic") - } + private val availableImageMimeTypes by lazy { + setOf("image/jpeg", "image/jpg", "image/png", "image/webp", "image/heic", "image/heif") + }
44-46: MIME 결정 로직 보강 제안
getType()이 null/엉뚱한 값을 줄 때 무조건image/jpeg로 강제하면 원본 포맷 손실 위험이 있습니다. 파일명 확장자(DocumentFile.name)로 보정하는 fallback을 한 단계 추가하는 것을 권장합니다. 필요 시 간단한 확장자→MIME 매핑을 두세요.이 로직이 서버측 콘텐츠 검증/표시에 영향을 주지 않는지 확인 부탁드립니다.
33-33: 파라미터 명확성 개선
uploadImage(imageType, url)의url은 실제로는 로컬content://URI입니다. API 오해를 줄이기 위해localUri(또는contentUri)로 변경을 권장합니다.
16-16: 불필요한 import 제거
kotlin.collections.contains명시적 import는 불필요합니다. 제거해도 동작 동일합니다.core/data/src/main/kotlin/com/acon/core/data/api/remote/ProfileApi.kt (1)
30-41: 신규 Verified Area API 추가: 인터페이스 정합성 LGTM엔드포인트/어노테이션 사용이 적절합니다. 서버가 200/204를 혼용할 수 있으니, 상태코드 기반 분기 필요 시
Response<Unit>로 받는 방안도 고려해 주세요.서버 스펙 상
/api/v1/verified-areas/replacement의 응답 바디가 없는지(204) 확인 부탁드립니다.core/data/src/main/kotlin/com/acon/core/data/datasource/remote/ProfileRemoteDataSource.kt (1)
3-6: API 표면과 DS 표면 일치시키기
ProfileApi에는replaceVerifiedArea(...)가 존재하나 DataSource에는 부재합니다. 레이어 간 표면 불일치를 줄이기 위해 DS에도 추가하는 것을 권장합니다.예시:
interface ProfileRemoteDataSource { @@ suspend fun getSavedSpots() : List<SavedSpotResponse> suspend fun getVerifiedAreas(): VerifiedAreaListResponse suspend fun deleteVerifiedArea(verifiedAreaId: Long) + suspend fun replaceVerifiedArea(request: ReplaceVerifiedAreaRequest) } @@ override suspend fun deleteVerifiedArea(verifiedAreaId: Long) { return profileApi.deleteVerifiedArea(verifiedAreaId) } + + override suspend fun replaceVerifiedArea(request: ReplaceVerifiedAreaRequest) { + profileApi.replaceVerifiedArea(request) + }feature/settings/src/main/java/com/acon/acon/feature/verification/screen/UserVerifiedAreasViewModel.kt (2)
41-45: retry 시 신규 Job 참조를 갱신하지 않아 이후 cancel 불가
retry()에서 새 로드 Job을 반환받지만loadVerifiedAreasJob에 재할당하지 않습니다. 이후 추가retry()에서 신규 Job을 cancel할 수 없습니다.fun retry() = intent { reduce { UserVerifiedAreasUiState.Loading } loadVerifiedAreasJob?.cancel() - loadVerifiedAreas() + loadVerifiedAreasJob = loadVerifiedAreas() }
119-121: TAG 상수 명 정합성
"LocalVerificationViewModel"은 파일/클래스명과 불일치합니다."UserVerifiedAreasViewModel"로 정렬을 권장합니다.- const val TAG = "LocalVerificationViewModel" + const val TAG = "UserVerifiedAreasViewModel"core/data/src/main/kotlin/com/acon/core/data/stream/DataStream.kt (1)
14-28: 간결하고 의도 명확한 트리거 스트림: LGTM초기
tryEmit과transformLatest구성이 용도에 적합합니다.빠른 연속 트리거에서 불필요한 서스펜션을 줄이려면 버퍼 용량을 약간 부여하는 것도 고려해 보세요.
- private val trigger = MutableSharedFlow<Unit>(replay = 1) + private val trigger = MutableSharedFlow<Unit>(replay = 1, extraBufferCapacity = 1)core/data/src/main/kotlin/com/acon/core/data/repository/ProfileRepositoryImpl.kt (3)
3-3: 미사용 DI 의존성(Context) 제거 제안Context가 더 이상 사용되지 않습니다. DI/생성자에서 제거해 빌드/경고 노이즈를 줄이는 편이 좋습니다.
적용 diff:
-import android.content.Context @@ -import dagger.hilt.android.qualifiers.ApplicationContext @@ class ProfileRepositoryImpl @Inject constructor( @@ private val aconAppRepository: AconAppRepository, - @VerifiedArea private val areaDataStream: DataStream, - @ApplicationContext private val context: Context + @VerifiedArea private val areaDataStream: DataStream ) : ProfileRepository {Also applies to: 20-20, 27-33
62-71: 로컬 이미지 판별 보강(파일/콘텐츠 스킴 지원) 및 한 줄 정리content://만 체크하면 file:// 또는 스킴 누락(로컬 경로 문자열) 케이스를 놓칠 수 있습니다. 스킴 기반 판별로 일반화하고 불필요한 임시 변수를 제거하세요.
적용 diff:
+import android.net.Uri @@ - if (imageStatus is ProfileImageStatus.Custom) { - if (imageStatus.url.startsWith("content://")) { - val uploadUrlResult = aconAppRepository.uploadImage(ImageType.PROFILE, imageStatus.url) - val fileUrl = uploadUrlResult.getOrThrow() + if (imageStatus is ProfileImageStatus.Custom) { + val scheme = Uri.parse(imageStatus.url).scheme + val isLocal = scheme == null || scheme == "content" || scheme == "file" + if (isLocal) { + val fileUrl = aconAppRepository + .uploadImage(ImageType.PROFILE, imageStatus.url) + .getOrThrow() profileToUpdate = newProfile.copy( image = ProfileImageStatus.Custom(fileUrl) ) } else { profileToUpdate = newProfile }참고: ThirFir의 실용적 선호(이전 PR의 업로드 로직 단순화)와도 일관됩니다.
115-121: runCatchingWith 호출 형태 통일 및 subscribe DSL 확인runCatchingWith는 괄호 없는 형태(runCatchingWith { ... })로 통일.
- return areaDataStream.subscribe { - emit(runCatchingWith() { + return areaDataStream.subscribe { + emit(runCatchingWith { profileRemoteDataSource.getVerifiedAreas().verifiedAreaList .map { it.toVerifiedArea() } }) }DataStream.subscribe 시그니처가 fun subscribe(block: suspend FlowCollector.() -> Unit): Flow이므로 subscribe 블록 내 emit 사용은 유효합니다.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (65)
app/src/main/java/com/acon/acon/navigation/nested/AreaVerificationNavigation.kt(1 hunks)app/src/main/java/com/acon/acon/navigation/nested/ProfileNavigation.kt(1 hunks)app/src/main/java/com/acon/acon/navigation/nested/SettingsNavigation.kt(2 hunks)core/data/src/main/kotlin/com/acon/core/data/api/remote/ProfileApi.kt(2 hunks)core/data/src/main/kotlin/com/acon/core/data/api/remote/auth/ProfileAuthApiLegacy.kt(0 hunks)core/data/src/main/kotlin/com/acon/core/data/api/remote/auth/SpotAuthApi.kt(0 hunks)core/data/src/main/kotlin/com/acon/core/data/cache/ProfileInfoCacheLegacy.kt(0 hunks)core/data/src/main/kotlin/com/acon/core/data/datasource/remote/ProfileRemoteDataSource.kt(3 hunks)core/data/src/main/kotlin/com/acon/core/data/datasource/remote/ProfileRemoteDataSourceLegacy.kt(0 hunks)core/data/src/main/kotlin/com/acon/core/data/datasource/remote/SpotRemoteDataSource.kt(0 hunks)core/data/src/main/kotlin/com/acon/core/data/di/ApiModule.kt(0 hunks)core/data/src/main/kotlin/com/acon/core/data/di/CacheModule.kt(0 hunks)core/data/src/main/kotlin/com/acon/core/data/di/DataStreamModule.kt(1 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/request/UpdateProfileRequestLegacy.kt(0 hunks)core/data/src/main/kotlin/com/acon/core/data/dto/request/profile/UpdateProfileRequest.kt(1 hunks)core/data/src/main/kotlin/com/acon/core/data/dto/response/profile/ProfileResponseLegacy.kt(0 hunks)core/data/src/main/kotlin/com/acon/core/data/dto/response/profile/SavedSpotsResponseLegacy.kt(0 hunks)core/data/src/main/kotlin/com/acon/core/data/repository/AconAppRepositoryImpl.kt(1 hunks)core/data/src/main/kotlin/com/acon/core/data/repository/OnboardingRepositoryImpl.kt(2 hunks)core/data/src/main/kotlin/com/acon/core/data/repository/ProfileRepositoryImpl.kt(3 hunks)core/data/src/main/kotlin/com/acon/core/data/repository/ProfileRepositoryLegacyImpl.kt(0 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(0 hunks)core/data/src/main/kotlin/com/acon/core/data/stream/DataStream.kt(1 hunks)core/data/src/test/java/com/acon/core/data/repository/ProfileRepositoryImplTest.kt(0 hunks)core/data/src/test/java/com/acon/core/data/repository/ProfileRepositoryTest.kt(3 hunks)core/data/src/test/java/com/acon/core/data/repository/SpotRepositoryImplTest.kt(0 hunks)core/data/src/test/java/com/acon/core/data/stream/DataStreamImplTest.kt(1 hunks)core/model/src/main/java/com/acon/acon/core/model/model/profile/PresignedUrl.kt(1 hunks)core/model/src/main/java/com/acon/acon/core/model/model/profile/ProfileInfoLegacy.kt(0 hunks)core/model/src/main/java/com/acon/acon/core/model/model/profile/SavedSpotLegacy.kt(0 hunks)domain/src/main/java/com/acon/acon/domain/error/profile/ValidateNicknameErrorLegacy.kt(0 hunks)domain/src/main/java/com/acon/acon/domain/repository/AconAppRepository.kt(1 hunks)domain/src/main/java/com/acon/acon/domain/repository/ProfileRepository.kt(2 hunks)domain/src/main/java/com/acon/acon/domain/repository/ProfileRepositoryLegacy.kt(0 hunks)domain/src/main/java/com/acon/acon/domain/repository/SpotRepository.kt(0 hunks)feature/onboarding/src/main/java/com/acon/feature/onboarding/area/composable/AreaVerificationScreenContainer.kt(2 hunks)feature/onboarding/src/main/java/com/acon/feature/onboarding/area/viewmodel/AreaVerificationViewModel.kt(4 hunks)feature/profile/src/main/java/com/acon/acon/feature/profile/composable/component/GallerySelectBottomSheet.kt(0 hunks)feature/profile/src/main/java/com/acon/acon/feature/profile/composable/component/NicknameValidMessageRow.kt(0 hunks)feature/profile/src/main/java/com/acon/acon/feature/profile/composable/component/ProfilePhotoBox.kt(0 hunks)feature/profile/src/main/java/com/acon/acon/feature/profile/composable/component/ProfileTextField.kt(0 hunks)feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/MockSavedSpotList.kt(0 hunks)feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profile/ProfileViewModelLegacy.kt(0 hunks)feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profile/composable/BookmarkItemLegacy.kt(0 hunks)feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profile/composable/ProfileScreenContainerLegacy.kt(0 hunks)feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profile/composable/ProfileScreenLegacy.kt(0 hunks)feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profileMod/ProfileModViewModelLegacy.kt(0 hunks)feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profileMod/composable/ProfileModScreenContainerLegacy.kt(0 hunks)feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profileMod/composable/ProfileModScreenLegacy.kt(0 hunks)feature/profile/src/main/java/com/acon/acon/feature/profile/composable/type/FocusType.kt(0 hunks)feature/profile/src/main/java/com/acon/acon/feature/profile/composable/type/NicknameErrorType.kt(0 hunks)feature/profile/src/main/java/com/acon/acon/feature/profile/composable/type/ProfileInfoType.kt(0 hunks)feature/profile/src/main/java/com/acon/acon/feature/profile/composable/type/ProfileUpdateResult.kt(0 hunks)feature/profile/src/main/java/com/acon/acon/feature/profile/composable/type/ValidationStatus.kt(0 hunks)feature/profile/src/main/java/com/acon/acon/feature/profile/composable/utils/BirthdayTransformation.kt(0 hunks)feature/profile/src/main/java/com/acon/acon/feature/profile/composable/utils/StringUtils.kt(0 hunks)feature/profile/src/main/java/com/acon/feature/profile/savedspot/composable/BookmarkItem.kt(1 hunks)feature/profile/src/main/java/com/acon/feature/profile/savedspot/composable/BookmarkScreen.kt(4 hunks)feature/profile/src/main/java/com/acon/feature/profile/savedspot/composable/BookmarkScreenContainer.kt(1 hunks)feature/profile/src/main/java/com/acon/feature/profile/savedspot/composable/BookmarkSkeletonItem.kt(2 hunks)feature/profile/src/main/java/com/acon/feature/profile/savedspot/viewmodel/BookmarkViewModel.kt(3 hunks)feature/settings/src/main/java/com/acon/acon/feature/verification/screen/UserVerifiedAreasViewModel.kt(3 hunks)feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotdetail/composable/SpotDetailViewModel.kt(5 hunks)
💤 Files with no reviewable changes (38)
- core/model/src/main/java/com/acon/acon/core/model/model/profile/SavedSpotLegacy.kt
- feature/profile/src/main/java/com/acon/acon/feature/profile/composable/type/FocusType.kt
- feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profileMod/composable/ProfileModScreenContainerLegacy.kt
- feature/profile/src/main/java/com/acon/acon/feature/profile/composable/utils/BirthdayTransformation.kt
- feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profile/ProfileViewModelLegacy.kt
- feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profile/composable/ProfileScreenContainerLegacy.kt
- feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/MockSavedSpotList.kt
- core/data/src/main/kotlin/com/acon/core/data/api/remote/auth/ProfileAuthApiLegacy.kt
- core/data/src/main/kotlin/com/acon/core/data/api/remote/auth/SpotAuthApi.kt
- feature/profile/src/main/java/com/acon/acon/feature/profile/composable/component/NicknameValidMessageRow.kt
- feature/profile/src/main/java/com/acon/acon/feature/profile/composable/type/ProfileInfoType.kt
- core/model/src/main/java/com/acon/acon/core/model/model/profile/ProfileInfoLegacy.kt
- feature/profile/src/main/java/com/acon/acon/feature/profile/composable/type/ProfileUpdateResult.kt
- core/data/src/main/kotlin/com/acon/core/data/repository/UserRepositoryImpl.kt
- feature/profile/src/main/java/com/acon/acon/feature/profile/composable/type/ValidationStatus.kt
- core/data/src/main/kotlin/com/acon/core/data/dto/request/UpdateProfileRequestLegacy.kt
- feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profile/composable/BookmarkItemLegacy.kt
- domain/src/main/java/com/acon/acon/domain/repository/SpotRepository.kt
- feature/profile/src/main/java/com/acon/acon/feature/profile/composable/component/ProfilePhotoBox.kt
- core/data/src/main/kotlin/com/acon/core/data/cache/ProfileInfoCacheLegacy.kt
- domain/src/main/java/com/acon/acon/domain/error/profile/ValidateNicknameErrorLegacy.kt
- feature/profile/src/main/java/com/acon/acon/feature/profile/composable/component/GallerySelectBottomSheet.kt
- core/data/src/main/kotlin/com/acon/core/data/di/ApiModule.kt
- core/data/src/main/kotlin/com/acon/core/data/repository/ProfileRepositoryLegacyImpl.kt
- core/data/src/test/java/com/acon/core/data/repository/ProfileRepositoryImplTest.kt
- feature/profile/src/main/java/com/acon/acon/feature/profile/composable/utils/StringUtils.kt
- feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profileMod/composable/ProfileModScreenLegacy.kt
- core/data/src/main/kotlin/com/acon/core/data/dto/response/profile/ProfileResponseLegacy.kt
- core/data/src/main/kotlin/com/acon/core/data/dto/response/profile/SavedSpotsResponseLegacy.kt
- core/data/src/main/kotlin/com/acon/core/data/datasource/remote/SpotRemoteDataSource.kt
- core/data/src/test/java/com/acon/core/data/repository/SpotRepositoryImplTest.kt
- core/data/src/main/kotlin/com/acon/core/data/di/CacheModule.kt
- domain/src/main/java/com/acon/acon/domain/repository/ProfileRepositoryLegacy.kt
- feature/profile/src/main/java/com/acon/acon/feature/profile/composable/type/NicknameErrorType.kt
- feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profileMod/ProfileModViewModelLegacy.kt
- core/data/src/main/kotlin/com/acon/core/data/datasource/remote/ProfileRemoteDataSourceLegacy.kt
- feature/profile/src/main/java/com/acon/acon/feature/profile/composable/component/ProfileTextField.kt
- feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profile/composable/ProfileScreenLegacy.kt
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: ThirFir
PR: AconInc/ACON-Android#255
File: core/data/src/main/kotlin/com/acon/core/data/datasource/remote/AconAppRemoteDataSource.kt:12-14
Timestamp: 2025-09-19T07:16:52.945Z
Learning: ThirFir prefers pragmatic engineering decisions over strict architectural separation when the practical benefits don't justify the implementation complexity, such as keeping FileUploadApi with the same client configuration rather than creating separate upload-specific clients.
Learnt from: ThirFir
PR: AconInc/ACON-Android#256
File: feature/profile/src/main/java/com/acon/feature/profile/update/composable/ProfileUpdateScreen.kt:86-99
Timestamp: 2025-09-20T06:53:43.571Z
Learning: ThirFir prefers practical engineering decisions over theoretical best practices when the actual risk is minimal, such as questioning the need for remember keys when the captured values are demonstrably stable.
Learnt from: ThirFir
PR: AconInc/ACON-Android#255
File: feature/profile/src/main/java/com/acon/feature/profile/update/viewmodel/ProfileUpdateViewModel.kt:38-57
Timestamp: 2025-09-19T07:40:37.778Z
Learning: ThirFir prefers pragmatic engineering solutions that balance user experience with implementation effort, such as choosing snackbar + retry button over more complex custom retry UI when design resources are limited.
📚 Learning: 2025-09-19T09:06:29.860Z
Learnt from: ThirFir
PR: AconInc/ACON-Android#255
File: core/data/src/main/kotlin/com/acon/core/data/repository/ProfileRepositoryImpl.kt:64-77
Timestamp: 2025-09-19T09:06:29.860Z
Learning: ThirFir moved image upload logic from ProfileRepository to AconAppRepository for better separation of concerns, demonstrating preference for proper architectural boundaries over keeping all profile-related operations in one repository.
Applied to files:
domain/src/main/java/com/acon/acon/domain/repository/AconAppRepository.ktcore/data/src/main/kotlin/com/acon/core/data/repository/ProfileRepositoryImpl.ktcore/data/src/main/kotlin/com/acon/core/data/repository/AconAppRepositoryImpl.kt
📚 Learning: 2025-09-19T04:52:02.675Z
Learnt from: ThirFir
PR: AconInc/ACON-Android#254
File: app/src/main/java/com/acon/acon/navigation/nested/ProfileNavigationLegacy.kt:38-40
Timestamp: 2025-09-19T04:52:02.675Z
Learning: In ProfileNavigationLegacy.kt, the onNavigateToProfileUpdate callback is intentionally left as a placeholder (just returning navController) and will be implemented in a future PR according to ThirFir.
Applied to files:
feature/onboarding/src/main/java/com/acon/feature/onboarding/area/composable/AreaVerificationScreenContainer.ktapp/src/main/java/com/acon/acon/navigation/nested/ProfileNavigation.ktapp/src/main/java/com/acon/acon/navigation/nested/AreaVerificationNavigation.ktfeature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotdetail/composable/SpotDetailViewModel.kt
🔇 Additional comments (32)
feature/profile/src/main/java/com/acon/feature/profile/savedspot/composable/BookmarkItem.kt (1)
30-35: 구조는 전반적으로 적절합니다 (internal 가시성, 파라미터 구성).간결한 API와 프리뷰 포함이 좋아요. 아래 몇 가지 소소한 개선만 제안합니다.
feature/profile/src/main/java/com/acon/feature/profile/savedspot/viewmodel/BookmarkViewModel.kt (2)
38-41: 상태 조건부 사이드이펙트 전송 패턴 좋습니다.
runOn<Success>로 클릭 시점 상태를 보장한 점 👍
4-4: 패키지 경로 확인 —model.model중복은 코드베이스에서 일관되게 사용됩니다core/model/src/main/java/com/acon/acon/core/model/model/profile/SavedSpot.kt 등 다수 파일이 package com.acon.acon.core.model.model.profile로 선언되어 있고 여러 파일이 동일 경로를 import하고 있으므로 현재 상태는 의도된 구조입니다. 중복
model제거는 전역 리팩터(패키지 선언과 모든 import 수정)가 필요합니다.feature/profile/src/main/java/com/acon/feature/profile/savedspot/composable/BookmarkScreenContainer.kt (1)
7-9: 패키지/의존성 경로 정리 LGTM컨테이너는 Orbit
collectAsState/collectSideEffect조합으로 간결하고 안전합니다.feature/profile/src/main/java/com/acon/feature/profile/savedspot/composable/BookmarkScreen.kt (1)
192-197: Retry 배선 확인 — LocalOnRetry → ViewModel 연결 검증 필요feature/profile/src/main/java/com/acon/feature/profile/savedspot/composable/BookmarkScreen.kt(192–197)에서 NetworkErrorView(onRetry = LocalOnRetry.current)로 재시도 콜백이 전달됩니다; 실행한 rg 스크립트가 출력이 없어 LocalOnRetry가 ViewModel의 재시도 로직을 호출하는지를 확인할 수 없습니다.
- ViewModel에 retry() 또는 동등한 공개 재시도 메서드가 존재하는지 확인.
- LocalOnRetry.current가 해당 메서드를 호출하도록 바인딩(람다 또는 멤버참조)되어 있는지 확인.
- 레거시 네이밍(SavedSpotLegacy, BookmarkItemLegacy, BookmarkSkeletonItemLegacy, com.acon.acon.feature.profile.composable.screen.bookmark) 잔여 여부를 재스캔하거나 수동 확인; 원본 rg 명령 재실행 권장.
feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotdetail/composable/SpotDetailViewModel.kt (5)
10-10: SignInStatus import 정리 OKFQCN 사용 대신 단일 import로 가독성 향상되었습니다.
19-19: firstOrNull 사용은 적절하나, 업스트림 예외 전파에 유의업스트림에서 예외가 발생하면 intent가 취소될 수 있습니다. 아래 소비 지점(verified areas 조회)에서 try/catch로 감싸는 수정을 권장드립니다.
29-29: 생성자 의존성 주입 변경 LGTMSpotRepository + ProfileRepository 구성으로 명확합니다.
50-50: GUEST 분기 처리 OK — 신규 상태 추가 시 동작만 확인 요청SignInStatus에 UNKNOWN 등 신규 값이 생겨도 else로 상세 조회가 트리거됩니다. 의도된 동작인지 확인 부탁드립니다.
14-14: ProfileRepository 전환 확인 — DI 바인딩 등록됨core/data/src/main/kotlin/com/acon/core/data/di/RepositoryModule.kt에서 ProfileRepositoryImpl을 ProfileRepository로 @BINDS 바인딩하고 있습니다.
core/model/src/main/java/com/acon/acon/core/model/model/profile/PresignedUrl.kt (1)
1-6: 코드 변경 사항이 적절합니다.새로운
PreSignedUrl데이터 클래스가 명확하고 간결하게 정의되어 있으며, 프로필 레거시 코드 제거의 일환으로 새로운 아키텍처에 맞게 도입된 모델로 보입니다.core/data/src/main/kotlin/com/acon/core/data/di/DataStreamModule.kt (2)
12-23: DI 모듈 구성이 적절합니다.
DataStreamModule이 올바르게 구성되어 있으며,@VerifiedArea한정자를 사용하여 특정 용도의DataStream인스턴스를 바인딩하고 있습니다.SingletonComponent에 설치되어 싱글톤 스코프로 관리됩니다.
25-27: 한정자 애노테이션이 적절히 정의되었습니다.
@VerifiedArea한정자가 올바르게 정의되어 있으며, 지역 인증과 관련된DataStream인스턴스를 구분하는 용도로 사용됩니다.core/data/src/test/java/com/acon/core/data/stream/DataStreamImplTest.kt (3)
11-31: 초기 방출 테스트가 적절합니다.
DataStream의 초기 방출 동작을 검증하는 테스트가 올바르게 작성되어 있습니다.MutableSharedFlow의replay = 1설정으로 인한 초기 방출이 제대로 테스트됩니다.
33-47: 데이터 변경 알림 테스트가 적절합니다.
notifyDataChanged()호출이 구독자들에게 올바르게 전파되는지 검증하는 테스트가 잘 작성되어 있습니다. 순차적인 값 증가를 통해 각 알림이 제대로 처리되는지 확인합니다.
49-75: 멀티 구독자 테스트가 철저합니다.여러 구독자가 있을 때 단일
notifyDataChanged()호출이 모든 구독자에게 동시에 전파되는지 검증하는 테스트가 잘 구현되어 있습니다.turbine을 활용한 테스트 패턴이 적절합니다.core/data/src/main/kotlin/com/acon/core/data/repository/SpotRepositoryImpl.kt (1)
108-113: 북마크 삭제 후 캐시 갱신 로직이 적절합니다.북마크 삭제 후에 로컬 캐시가 존재하는 경우에만 원격에서 최신 데이터를 가져와 캐시를 갱신하는 로직이 올바르게 구현되어 있습니다. 이는 불필요한 네트워크 호출을 방지하면서도 데이터 일관성을 보장합니다.
app/src/main/java/com/acon/acon/navigation/nested/SettingsNavigation.kt (1)
17-17: 중앙화된 네비게이션 유틸리티 사용이 좋습니다.
navigateAndClear유틸리티 함수를 사용하여 기존의 수동적인 백스택 클리어링 코드를 대체한 것이 좋습니다. 이는 네비게이션 로직을 중앙화하고 코드 중복을 줄입니다.Also applies to: 48-48
domain/src/main/java/com/acon/acon/domain/repository/ProfileRepository.kt (1)
3-3: 지역 인증 관련 API 확장이 적절합니다.
getVerifiedAreas()와deleteVerifiedArea()메서드가 추가되어 지역 인증 기능을 지원합니다.Flow<Result<List<Area>>>를 통해 반응형 데이터 스트림을 제공하는 것이 적절합니다.Also applies to: 14-15
core/data/src/main/kotlin/com/acon/core/data/repository/OnboardingRepositoryImpl.kt (2)
8-8: DataStream 의존성 주입이 적절합니다.
@VerifiedArea한정자를 사용하여 특정 용도의DataStream을 주입받고 있으며, 이는 지역 인증과 관련된 데이터 스트림을 적절히 구분합니다.Also applies to: 11-11, 19-19
41-41: 지역 인증 후 데이터 스트림 알림이 적절합니다.지역 인증 성공 후
areaDataStream.notifyDataChanged()를 호출하여 다른 컴포넌트들에게 지역 데이터 변경을 알리는 것이 올바른 구현입니다. 이를 통해 반응형 UI 업데이트가 가능합니다.core/data/src/main/kotlin/com/acon/core/data/dto/request/profile/UpdateProfileRequest.kt (1)
20-20: 날짜 포맷팅 개선이 좋습니다.기존의 문자열 보간 방식에서
String.format()을 사용한 방식으로 변경하여 더 명확하고 안전한 날짜 포맷팅을 구현했습니다.%04d.%02d.%02d형식은 제로 패딩을 보장하여 일관된 날짜 형식을 제공합니다.feature/onboarding/src/main/java/com/acon/feature/onboarding/area/composable/AreaVerificationScreenContainer.kt (2)
25-27: 사이드이펙트→콜백 매핑 추가 적절Introduce/SpotList에 대한 사이드이펙트 매핑이 자연스럽고, 시그니처 확장도 호출부 변경과 일치합니다.
호출부 전체가 새 파라미터 순서에 맞게 업데이트되었는지 한 번 더 빌드 확인 부탁드립니다.
Also applies to: 78-80
85-86: BackHandler 동작은 기존 navigateUp과 실질적으로 동등skippable=false(이전 스택 존재)에서만 popBackStack을 호출하는 현재 로직은 기존 navigateUp 동작과 동일한 사용자 경험을 제공합니다. 그대로 가도 무방합니다.
app/src/main/java/com/acon/acon/navigation/nested/AreaVerificationNavigation.kt (1)
37-39: 새 콜백 배선 LGTMIntroduce/SpotList로의 navigateAndClear 배선이 컨셉과 일치합니다.
core/data/src/main/kotlin/com/acon/core/data/di/RepositoryModule.kt (2)
12-23: 레거시 ProfileRepository DI 제거 — 전역 잔존 참조 재검증 필요제공하신 rg 명령의 출력이 비어있어 검증이 불확실합니다. 아래 전역 검색을 실행해 잔존 참조가 없는지 확인해 주세요.
#!/bin/bash set -euo pipefail # 레거시 심볼(정확명) 검색 rg -n --hidden -S -C2 '\b(ProfileRepositoryLegacy|bindsProfileRepositoryLegacy|ProfileRemoteDataSourceLegacy|ProfileInfoCacheLegacy)\b' || true # 일반적인 Profile 관련 리포지토/데이터소스 참조(브로드) 검색 rg -n --hidden -S -C2 '\b(ProfileRepository|ProfileRemoteDataSource|ProfileInfoCache|ProfileRepo)\b' || true # DI 바인딩/어노테이션·import 검사 rg -n --hidden -S -C2 '@Binds.*Profile|binds.*Profile|import\s+.*Profile' || true
33-38: 확인: AconAppRepositoryImpl은 @Inject 생성자(@ApplicationContext 포함)로 주입 가능 — @provides 불필요AconAppRepositoryImpl이 core/data/src/main/kotlin/com/acon/core/data/repository/AconAppRepositoryImpl.kt 에서 @Inject constructor(@ApplicationContext private val context: Context, ...)로 정의되어 있어 Hilt의 @BINDS 바인딩만으로 인스턴스화됩니다. 모듈이 ApplicationContext를 제공하는 컴포넌트(예: @Installin(SingletonComponent::class))에 설치되어 있는지 확인하세요.
core/data/src/main/kotlin/com/acon/core/data/repository/AconAppRepositoryImpl.kt (1)
52-55: 업로드 결과 검증 권장S3류 presigned 업로드의 경우 200/204 외 상태가 올 수 있습니다.
uploadFile(...)내부에서 상태코드 검증 및 재시도/로깅이 있는지 확인 부탁드립니다. 필요 시 5xx에 한해 지수 백오프로 재시도 권장.core/data/src/main/kotlin/com/acon/core/data/datasource/remote/ProfileRemoteDataSource.kt (1)
17-19: Verified Areas 조회/삭제 추가: 정합성 OK
getVerifiedAreas/deleteVerifiedArea추가와 구현 위임이 일관성 있습니다.feature/settings/src/main/java/com/acon/acon/feature/verification/screen/UserVerifiedAreasViewModel.kt (1)
27-39: 삭제 성공 시 목록 갱신 트리거 확인 필요UI가 getVerifiedAreas() Flow를 수집합니다. deleteVerifiedArea() 성공 이후 Repository가 Flow를 재발행(또는 notifyDataChanged 같은 트리거)하여 구독자에게 변경을 통지하는지 확인하세요. 미트리거면 ViewModel에서 삭제 성공 시 수동으로 목록 재로딩을 구현해야 합니다.
대상: feature/settings/src/main/java/com/acon/acon/feature/verification/screen/UserVerifiedAreasViewModel.kt (27-39, 79-109)
자동 검사 스크립트가 환경에서 실패했습니다(--type=kt 미인식). 로컬에서 아래 명령을 실행한 출력 결과를 첨부하세요:
rg -n -C2 -P '\bdeleteVerifiedArea\s*\(' --glob '*.kt' rg -n -C3 -P '\bnotifyDataChanged\s*\(' --glob '*.kt' rg -n -C3 -P '\bgetVerifiedAreas\s*\(' --glob '*.kt'core/data/src/main/kotlin/com/acon/core/data/repository/ProfileRepositoryImpl.kt (2)
30-31: AconAppRepository 주입으로 업로드 로직 집약: 방향 좋습니다프로필 이미지 업로드를 AconAppRepository로 이관한 결정이 명확하고, 이전 레거시 제거 취지와도 부합합니다. 👍
124-129: 삭제 후 스트림 갱신 처리 LGTM원격 삭제 성공 시에만 areaDataStream.notifyDataChanged()를 호출하는 흐름이 올바릅니다. 의도대로 getVerifiedAreas() 구독 측이 자동 새로고침되는지만 한번 확인 부탁드립니다.
Summary by CodeRabbit