Skip to content

Conversation

@ThirFir
Copy link
Collaborator

@ThirFir ThirFir commented Sep 20, 2025

  • 프로필 레거시 제거
  • 지역인증 건너뛰기 로직 이슈 수정
  • presigned url 로직 위치 이동

Summary by CodeRabbit

  • New Features
    • 지역 인증 완료 후 사용자 설정에 따라 소개 화면 또는 스팟 목록으로 이동.
    • 설정의 인증 지역 관리: 목록 조회 및 삭제 지원, 변경 사항이 즉시 반영.
  • Improvements
    • 프로필 이미지 업로드 안정화 및 포맷 지원 확대(jpg/jpeg/png/webp/heic).
    • 특정 화면 이동 시 뒤로가기 기록 정리로 더 예측 가능한 탐색 경험.
  • Refactor
    • 북마크 화면 구조 개편 및 로딩 스켈레톤 개선.
  • Chores
    • 레거시 프로필 관련 코드와 의존성 정리로 안정성 향상.

@ThirFir ThirFir self-assigned this Sep 20, 2025
@ThirFir
Copy link
Collaborator Author

ThirFir commented Sep 20, 2025

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Sep 20, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link

coderabbitai bot commented Sep 20, 2025

Walkthrough

  • 네비게이션에 Introduce/SpotList 분기 추가 및 Settings→SignIn 전환 방식 변경
  • 프로필 레거시 계층(Repository/Remote/DTO/Cache/DI/UI/테스트) 대거 제거
  • Verified Area용 API/Remote/Repository/Stream 도입 및 ViewModel 이관
  • 이미지 업로드 로직 Repository 계층으로 집약
  • 북마크 화면/VM 리팩터링 및 패키지 이동

Changes

Cohort / File(s) Summary
네비게이션 업데이트
app/src/main/java/com/acon/acon/navigation/nested/AreaVerificationNavigation.kt, .../ProfileNavigation.kt, .../SettingsNavigation.kt
AreaVerification에 Introduce/SpotList 콜백 추가. ProfileNavigation 임포트 경로 정리. Settings의 SignIn 이동을 navigateAndClear로 단순화.
Profile API 신설·정비
core/data/.../api/remote/ProfileApi.kt
Verified Areas용 3개 엔드포인트 추가(GET/POST/DELETE).
레거시 API/DS/캐시 제거
core/data/.../api/remote/auth/ProfileAuthApiLegacy.kt, .../datasource/remote/ProfileRemoteDataSourceLegacy.kt, .../cache/ProfileInfoCacheLegacy.kt, core/data/.../dto/request/UpdateProfileRequestLegacy.kt, .../dto/response/profile/ProfileResponseLegacy.kt, .../dto/response/profile/SavedSpotsResponseLegacy.kt
프로필 관련 레거시 API/데이터소스/캐시/DTO 전면 삭제.
Remote DataSource 확장/정리
core/data/.../datasource/remote/ProfileRemoteDataSource.kt, .../datasource/remote/SpotRemoteDataSource.kt, .../api/remote/auth/SpotAuthApi.kt
ProfileRemoteDataSource에 getVerifiedAreas/deleteVerifiedArea 추가. Spot 측 레거시 saved spots 조회 제거.
DI 모듈 변경
core/data/.../di/ApiModule.kt, .../di/CacheModule.kt, .../di/RepositoryModule.kt, .../di/DataStreamModule.kt
레거시 ProfileAuthApi/Cache/Repository 바인딩 제거. DataStream 바인딩 및 @VerifiedArea 퀄리파이어 추가.
데이터 스트림 도입
core/data/.../stream/DataStream.kt
DataStream 인터페이스/구현 추가(SharedFlow 트리거, subscribe/notify).
레포지토리 변화(앱/온보딩/프로필/스팟/유저)
core/data/.../repository/AconAppRepositoryImpl.kt, .../OnboardingRepositoryImpl.kt, .../ProfileRepositoryImpl.kt, .../ProfileRepositoryLegacyImpl.kt, .../SpotRepositoryImpl.kt, .../UserRepositoryImpl.kt
AconAppRepository에 이미지 업로드 추가(Context 주입). OnboardingRepo에서 areaDataStream notify. ProfileRepo가 이미지 업로드를 AconAppRepository로 위임, Verified Areas 흐름 추가(DataStream 구독). 레거시 프로필 레포 제거. SpotRepo에서 레거시 연계 제거. UserRepo에서 레거시 캐시 의존 제거.
도메인 인터페이스 변경
domain/.../repository/AconAppRepository.kt, .../repository/ProfileRepository.kt, .../repository/ProfileRepositoryLegacy.kt, .../repository/SpotRepository.kt
AconAppRepository에 uploadImage 추가. ProfileRepository에 getVerifiedAreas/deleteVerifiedArea 추가. 레거시 ProfileRepositoryLegacy 제거. SpotRepository에서 saved list API 제거.
모델 정리/추가
core/model/.../profile/PresignedUrl.kt, core/model/.../profile/ProfileInfoLegacy.kt, .../SavedSpotLegacy.kt
새 PreSignedUrl 데이터 클래스 추가. 레거시 ProfileInfoLegacy, SavedSpotLegacy 제거.
업데이트 포맷 수정
core/data/.../dto/request/profile/UpdateProfileRequest.kt
생일 문자열 포맷팅 방식 변경(String.format).
온보딩 뷰모델 분기 추가
feature/onboarding/.../AreaVerificationScreenContainer.kt, .../AreaVerificationViewModel.kt
컨테이너에 Introduce/SpotList 콜백 및 skippable 조건 백네비게이션. ViewModel에서 온보딩 선호도 기반 분기(ChooseDislikes/Introduce/SpotList)와 사이드이펙트 추가.
프로필 레거시 UI/VM 제거
feature/profile/src/main/java/com/acon/acon/feature/profile/... 다수
레거시 프로필 화면/수정 화면/컴포넌트/타입/유틸 전면 삭제.
북마크 화면 리팩터링/이동
feature/profile/src/main/java/com/acon/feature/profile/savedspot/composable/*, .../savedspot/viewmodel/BookmarkViewModel.kt
패키지 이동 및 컴포넌트/이름 정리. ViewModel이 ProfileRepository의 flow(getSavedSpots) 사용, UI 상태 구조 변경(savedSpots).
세팅/스팟 ViewModel 이관
feature/settings/.../UserVerifiedAreasViewModel.kt, feature/spot/.../SpotDetailViewModel.kt
레거시 ProfileRepositoryLegacy → ProfileRepository로 교체. Verified Areas 흐름을 flow 기반으로 변경.
테스트 변경
core/data/src/test/.../ProfileRepositoryImplTest.kt, .../ProfileRepositoryTest.kt, .../SpotRepositoryImplTest.kt, .../stream/DataStreamImplTest.kt
레거시 프로필 테스트 삭제/의존 정리. 새 DataStream 테스트 추가. ProfileRepositoryTest에 DataStream 의존 주입.

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

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested reviewers

  • 1971123-seongmin

Poem

새벽 빛 속 코드밭, 삐약! 아냐, 깡총!
레거시 풀 뽑고, 새 길을 쫀쫀히 공정.
흐름은 Stream, 사진은 훌쩍 업로드!
북마크 새 둥지, 영역 인증은 고도.
깡총깡총 머지길—버그야, 이제 도도! 🐇✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed 제목 "[Feat/#248] 프로필 리팩토링 완료, 이슈 일부 수정"은 변경사항의 핵심인 프로필 리팩토링을 명확히 요약하여 PR의 주요 변경점을 잘 반영하며 간결하고 불필요한 파일 목록이나 이모지 없이 읽기 쉽습니다. 다만 "이슈 일부 수정"은 어떤 이슈가 수정되었는지 구체성이 떨어져 스캔하는 동료가 바로 파악하기 어렵습니다. 전반적으로 변경의 주제를 잘 담고 있어 타임라인 확인 시 이해에 문제가 없습니다.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/profile

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Collaborator

@1971123-seongmin 1971123-seongmin left a 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 ->
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

북마크도 캐시데이터에 추가한건가요??

Copy link
Collaborator Author

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()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기서 DataStream을 쓰는 이유는 무엇인가요??

Copy link
Collaborator Author

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 {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 DataStream을 사용하는 곳을 몇개 봤는데 (프로필? , 인증동네 확인 뷰모델, 온보딩) 왜 이걸 새로 만드셨는지 궁금합니다ㅏ

Copy link
Collaborator Author

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() }
})
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기서 DataStream을 이유는 무엇인가요?? (기존 앱 버전에서는 지역인증을 하고, 인증동네 확인 화면으로 돌아왔을 때, 바로 추가가 안되고 나갔다가 들어와야하는 문제가 있는거로 알고있는데 그거 때문인가요??)

Copy link
Collaborator Author

@ThirFir ThirFir Sep 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네 그거 맞습니다. (위 코멘트와 같은 맥락)

Copy link

@coderabbitai coderabbitai bot left a 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 = 250L

Also 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

초기 tryEmittransformLatest 구성이 용도에 적합합니다.

빠른 연속 트리거에서 불필요한 서스펜션을 줄이려면 버퍼 용량을 약간 부여하는 것도 고려해 보세요.

-    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

📥 Commits

Reviewing files that changed from the base of the PR and between a63d471 and 80ffe65.

📒 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.kt
  • core/data/src/main/kotlin/com/acon/core/data/repository/ProfileRepositoryImpl.kt
  • core/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.kt
  • app/src/main/java/com/acon/acon/navigation/nested/ProfileNavigation.kt
  • app/src/main/java/com/acon/acon/navigation/nested/AreaVerificationNavigation.kt
  • feature/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 정리 OK

FQCN 사용 대신 단일 import로 가독성 향상되었습니다.


19-19: firstOrNull 사용은 적절하나, 업스트림 예외 전파에 유의

업스트림에서 예외가 발생하면 intent가 취소될 수 있습니다. 아래 소비 지점(verified areas 조회)에서 try/catch로 감싸는 수정을 권장드립니다.


29-29: 생성자 의존성 주입 변경 LGTM

SpotRepository + 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의 초기 방출 동작을 검증하는 테스트가 올바르게 작성되어 있습니다. MutableSharedFlowreplay = 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: 새 콜백 배선 LGTM

Introduce/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() 구독 측이 자동 새로고침되는지만 한번 확인 부탁드립니다.

@ThirFir ThirFir merged commit 8cd1b43 into develop Sep 20, 2025
2 checks passed
@ThirFir ThirFir deleted the refactor/profile branch September 20, 2025 09:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants