Skip to content

Conversation

@ThirFir
Copy link
Collaborator

@ThirFir ThirFir commented Sep 19, 2025

💻 Work Description

  • 프로필 정보(프로필 메인) 페이지 UI와 뷰모델: 구현 및 테스트
    • Kotest + orbit-test
    • compose-test
  • 프로필 Response 매핑 테스트 추가
  • 소셜 로그인 구현체 EntryPoint 잘못된 사용 이슈 수정
  • 프로필 실제 API 연결

Summary by CodeRabbit

  • 개선

    • 로그인 상태 모델 정비로 게스트/사용자 분기 안정화(프로필/스팟/설정 화면 전반)
    • 저장한 장소 캐시 및 동기화로 목록 최신화 및 응답성 향상
    • 하단 바 고정 스크롤 동작 안정화
  • 테스트

    • 프로필 정보 화면 UI/동작 테스트 추가
    • 데이터 매핑/세션 상태 테스트 보강
  • 작업

    • 의존성 업데이트(Compose Compiler, Orbit, 테스트 라이브러리 등)

- Kotest, Orbit-test 라이브러리 추가
- UserType -> SignInStatus 네이밍 수정
- Compose Compiler version upgrade to 1.5.15
- Orbit upgrade to 10.0.0
- 저장한 장소 캐싱 로직 추가
@ThirFir ThirFir self-assigned this Sep 19, 2025
@coderabbitai
Copy link

coderabbitai bot commented Sep 19, 2025

Walkthrough

전역적으로 UserType를 SignInStatus로 대체하고, 세션/리포지토리/Compose Local/뷰모델/화면 전반을 이에 맞게 수정. 프로필 정보 화면(컨테이너/뷰모델/컴포저블)과 저장한 장소 캐시(로컬/리모트/리포지토리) 추가. Profile API 엔드포인트/DTO 정비. 내비게이션 프로필 레거시 시그니처 변경. 테스트·그라들 의존성 갱신.

Changes

Cohort / File(s) Summary
SignInStatus 전환 (모델/세션/도메인/앱 상태)
core/model/src/main/java/.../SignInStatus.kt, core/model/src/main/java/.../UserType.kt, core/data/src/main/kotlin/.../session/SessionHandler.kt, core/data/src/main/kotlin/.../repository/UserRepositoryImpl.kt, domain/src/main/java/.../UserRepository.kt, app/src/main/java/com/acon/acon/MainViewModel.kt, app/src/main/java/com/acon/acon/MainActivity.kt, core/ui/src/main/java/.../base/BaseContainerHost.kt, core/ui/src/main/java/.../compose/LocalCompositions.kt
UserType 삭제 및 SignInStatus 추가/치환. 세션 흐름(getUserType→getSignInStatus), 상태 플로우/초기화 변경. 앱 상태 필드(userType→signInStatus). Compose Local(LocalUserType→LocalSignInStatus) 및 BaseContainerHost 훅(useUserType→useSignInStatus) 교체.
인증 DI 변경
app/src/main/java/com/acon/acon/MainActivity.kt, ...core/social/di/AuthClientEntryPoint(사용), ...
GoogleAuthClient 접근을 AuthClientEntryPoint 통해 획득하도록 수정.
프로필 기능 신규 화면/뷰모델
feature/profile/src/main/java/.../info/composable/ProfileInfoScreen.kt, .../ProfileInfoScreenContainer.kt, .../ProfileView.kt, .../SavedSpotsView.kt, .../info/viewmodel/ProfileInfoViewModel.kt, feature/profile/src/main/java/com/acon/feature/profile/TestTags.kt
프로필 정보 화면/컨테이너/뷰모델 및 뷰 추가. 액션/사이드이펙트 정의, 저장한 장소 섹션/아이템 렌더링, 테스트 태그 제공.
프로필 네비 레거시 업데이트
app/src/main/java/com/acon/acon/navigation/AconNavigation.kt, app/src/main/java/com/acon/acon/navigation/nested/ProfileNavigationLegacy.kt, feature/profile/src/main/java/.../profile/...
profileNavigationLegacy에서 SnackbarHostState 제거. ProfileInfoScreenContainer로 교체 및 콜백 기반 내비게이션으로 재배선. 레거시 컨테이너는 useSignInStatus로 갱신.
프로필 데이터 레이어(API/DTO/DS/Repo)
core/data/src/main/kotlin/.../api/remote/ProfileApi.kt, .../dto/response/profile/ProfileResponse.kt, .../dto/response/profile/SavedSpotResponse.kt, .../dto/response/profile/SavedSpotsResponse.kt, .../datasource/local/ProfileLocalDataSource.kt, .../datasource/remote/ProfileRemoteDataSource.kt, .../repository/ProfileRepositoryImpl.kt, domain/src/main/java/.../ProfileRepository.kt
Retrofit 경로/어노테이션 추가, getSavedSpots 응답을 래퍼 DTO(SavedSpotsResponse)로 변경. DTO에 @Serializable/@SerialName 추가. 로컬 DS에 저장한 장소 캐시 플로우 추가. 리모트 DS는 래퍼 언랩. 리포지토리 getSavedSpots를 Flow<Result<...>>로 전환하고 캐시-리모트 결합 로직 추가.
스팟 기능 SignInStatus 반영
feature/spot/src/main/java/.../spotdetail/...kt, .../spotlist/SpotListViewModel.kt, .../spotlist/composable/SpotEmptyView.kt, .../spotlist/composable/SpotItem.kt, .../spotlist/composable/SpotListScreen.kt, .../spotlist/composable/SpotListScreenContainer.kt, .../spotlist/composable/SpotListSuccessView.kt, core/data/src/main/kotlin/.../datasource/remote/SpotRemoteDataSource.kt
LocalUserType→LocalSignInStatus, 게스트 분기 UserType→SignInStatus. SpotEmptyView/SpotListSuccessView 파라미터 시그니처 변경(userType→signInStatus). Remote DS fetchSpotList 인자 타입 변경.
스팟 리포지토리 - 저장 장소 동기화
core/data/src/main/kotlin/.../repository/SpotRepositoryImpl.kt
북마크 추가 후 로컬 캐시가 존재하면 리모트 저장 장소 재조회 후 로컬 캐시 갱신 로직 추가. 프로필 로컬/리모트 DS 의존성 주입.
DI 모듈 조정
core/data/src/main/kotlin/.../di/ApiModule.kt, core/data/src/main/kotlin/.../di/RepositoryModule.kt
ProfileApi 신규 프로바이더 추가, 레거시 프로바이더 이름 변경. 리포지토리 바인딩을 레거시 구현으로 교체.
디자인 시스템
core/designsystem/src/main/java/.../component/image/DefaultProfileImage.kt
기본 프로필 이미지 컴포저블 추가.
사인인 기능 반영
feature/signin/src/main/java/.../SignInScreen.kt, .../SignInScreenContainer.kt, .../SignInViewModel.kt
Local/훅/비교 로직을 SignInStatus 기반으로 교체.
테스트 추가/갱신
core/data/src/test/java/.../ProfileRemoteDataSourceTest.kt, .../mapping/ProfileMappingTest.kt, .../session/SessionHandlerImplTest.kt, feature/profile/src/test/.../ProfileInfoViewModelTest.kt, feature/profile/src/androidTest/.../ProfileInfoScreenTest.kt, feature/profile/src/androidTest/AndroidManifest.xml
SavedSpotsResponse 대응, 프로필 매핑/세션 상태/프로필 VM 동작/프로필 UI 상호작용 테스트 추가·수정. 안드로이드 테스트 매니페스트 추가.
Gradle/의존성
feature/profile/build.gradle.kts, gradle/libs.versions.toml
테스트 의존성(kotest, orbit-test, espresso 등) 추가 및 JUnit Platform 활성화. Compose Compiler/Orbit 버전 상승, KSP 버전 키 추가.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor UI
  participant Compose as LocalSignInStatus
  participant VM as ViewModel (e.g., ProfileInfoViewModel)
  participant UserRepo
  participant Session as SessionHandler

  UI->>Compose: read LocalSignInStatus.current
  VM->>UserRepo: getSignInStatus()
  UserRepo->>Session: getUserType() -> Flow<SignInStatus>
  Session-->>UserRepo: Flow(USER/GUEST)
  UserRepo-->>VM: Flow(SignInStatus)
  VM-->>UI: render based on status
Loading
sequenceDiagram
  autonumber
  actor UI
  participant VM as ProfileInfoViewModel
  participant ProfRepo as ProfileRepository
  participant ProfLocal as ProfileLocalDS
  participant ProfRemote as ProfileRemoteDS

  Note over VM: loadState()
  VM->>UserRepo: getSignInStatus()
  alt SignInStatus.USER
    par SavedSpots
      VM->>ProfRepo: getSavedSpots() : Flow<Result<List<SavedSpot>>>
      ProfRepo->>ProfLocal: getSavedSpots() (Flow<List<SavedSpot>?>)
      alt Cache hit
        ProfLocal-->>ProfRepo: List<SavedSpot>
        ProfRepo-->>VM: Flow(Result.success(cache))
      else Cache miss
        ProfRepo->>ProfRemote: getSavedSpots()
        ProfRemote-->>ProfRepo: List<SavedSpotResponse>
        ProfRepo->>ProfLocal: cacheSavedSpots(mapped)
        ProfRepo-->>VM: Flow(Result(mapped))
      end
    and Profile
      VM->>ProfRepo: getProfile()
      ProfRepo-->>VM: Result<Profile>
    end
    VM-->>UI: User(profile, savedSpots) or LoadFailed
  else SignInStatus.GUEST
    VM-->>UI: Guest
  end
Loading
sequenceDiagram
  autonumber
  actor UI as Spot Detail/List
  participant SpotRepo
  participant SpotRemote

  UI->>SpotRepo: fetchSpotList(request, signInStatus)
  alt signInStatus == GUEST
    SpotRepo->>SpotRemote: fetch no-auth list
  else
    SpotRepo->>SpotRemote: fetch auth list
  end
  SpotRemote-->>SpotRepo: SpotListResponse
  SpotRepo-->>UI: SpotListResponse
Loading
sequenceDiagram
  autonumber
  actor UI as Bookmark Action
  participant SpotRepo
  participant ProfLocal
  participant ProfRemote

  UI->>SpotRepo: addBookmark(spotId)
  SpotRepo-->>UI: Result
  SpotRepo->>ProfLocal: getSavedSpots().firstOrNull()
  alt Cached exists
    SpotRepo->>ProfRemote: getSavedSpots()
    ProfRemote-->>SpotRepo: List<SavedSpotResponse>
    SpotRepo->>ProfLocal: cacheSavedSpots(mapped)
  else No cache
    Note over SpotRepo: skip refresh
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

귀 훌쩍 깃 세우고 코드밭을 뛰었지 🐇
타입이 바뀌었대, UserType 안녕—SignInStatus 반가워!
프로필엔 새 길, 캐시가 살포시 도왔고,
저장한 발자국들은 흐름 따라 춤추네.
북마크 찍고 깡총! 테스트도 모두 통과했당 ✨

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 제목 "[Refactor] 프로필 정보 Presentation"은 PR의 핵심 의도인 프로필 정보 화면의 프레젠테이션(화면·ViewModel 구현 및 리팩터링, 테스트 추가, API 연결 등)과 직접적으로 연관되어 있어 변경의 주요 목적을 간결하게 요약합니다.
✨ 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-info-presentation

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

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: 13

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotlist/composable/SpotItem.kt (1)

127-135: 컴파일 에러: Modifier.padding() 인자 누락

  • Modifier.padding()은 매개변수가 필수입니다. 현재 형태는 컴파일 실패합니다. 의도에 따라 제거하거나 값(예: start = ...)을 지정해주세요.

수정 예시(패딩 불필요 시 제거):

-                modifier = Modifier.padding()
+                modifier = Modifier

또는(패딩 유지 시):

-                modifier = Modifier.padding()
+                modifier = Modifier.padding(horizontal = 0.dp)
core/data/src/main/kotlin/com/acon/core/data/repository/UserRepositoryImpl.kt (1)

91-95: 계정 삭제 시 프로필 캐시 정리가 누락되었습니다.

deleteAccount에서도 로그아웃과 마찬가지로 프로필 캐시를 정리해야 합니다.

다음과 같이 수정하세요:

         ).onSuccess {
+            profileLocalDataSource.clearCache()
             onboardingRepository.updateHasVerifiedArea(false)
             onboardingRepository.updateHasTastePreference(false)
             clearSession()
         }
app/src/main/java/com/acon/acon/MainActivity.kt (2)

182-186: 권한 체크 오타: FINE을 두 번 체크

COARSE 권한을 누락해 항상 FINE만 중복 확인합니다. AND 조건의 두 번째를 COARSE로 교체하세요.

적용 diff:

-        emit(
-            checkSelfPermission(android.Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
-                    && checkSelfPermission(android.Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
-        )
+        emit(
+            checkSelfPermission(android.Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED &&
+            checkSelfPermission(android.Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED
+        )

416-418: 권한 체크 오타(중복) — COARSE로 교체

아래 위치에서도 동일한 오타가 있습니다.

적용 diff:

-        if (checkSelfPermission(android.Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
-            && checkSelfPermission(android.Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
-        ) return
+        if (checkSelfPermission(android.Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED &&
+            checkSelfPermission(android.Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED
+        ) return
🧹 Nitpick comments (54)
core/data/src/main/kotlin/com/acon/core/data/dto/response/profile/ProfileResponse.kt (4)

6-14: 직렬화 적용은 적절합니다. 단, 불필요한 @SerialName 제거 권장

nickname/birthDate는 필드명과 JSON 키가 동일해 @SerialName이 불필요합니다. 유지보수성 위해 profileImage만 남기고 나머지 제거를 제안합니다.

다음 패치 제안:

-    @SerialName("nickname") val nickname: String,
-    @SerialName("birthDate") val birthDate: String? = null,
+    val nickname: String,
+    val birthDate: String? = null,
     @SerialName("profileImage") val image: String? = null,

19-26: 생일 파싱을 엄격/명시적으로 처리하세요 (포맷, 트리밍, 예외 범위 축소)

split + toInt는 공백/자릿수 오류에 취약하고 광범위 예외를 삼킵니다. STRICT 모드 DateTimeFormatter를 사용해 포맷을 한눈에 드러내고, trim/빈값 거르기를 추가하세요.

-        val birthDateOfModel = birthDate?.let { dateString ->
-            try {
-                val (year, month, day) = dateString.split(".").map { it.toInt() }
-                BirthDateStatus.Specified(LocalDate.of(year, month, day))
-            } catch (_: Exception) {
-                BirthDateStatus.NotSpecified
-            }
-        } ?: BirthDateStatus.NotSpecified
+        val birthDateOfModel = birthDate
+            ?.trim()
+            ?.takeIf { it.isNotEmpty() }
+            ?.let { ds ->
+                runCatching { LocalDate.parse(ds, BIRTHDATE_FMT) }
+                    .getOrNull()
+            }?.let { BirthDateStatus.Specified(it) }
+            ?: BirthDateStatus.NotSpecified

파일 상단(임포트 옆)에 다음 상수를 추가하세요:

import java.time.format.DateTimeFormatter
import java.time.format.ResolverStyle

private val BIRTHDATE_FMT: DateTimeFormatter =
    DateTimeFormatter.ofPattern("uuuu.MM.dd").withResolverStyle(ResolverStyle.STRICT)

27-29: image가 빈 문자열/공백인 경우도 Default로 처리 필요

현재 null만 Default 처리합니다. 공백/빈 문자열은 Custom("")가 되어 다운스트림에서 URL 검증 이슈를 유발할 수 있습니다.

-        val imageOfModel =
-            if (image == null) ProfileImageStatus.Default else ProfileImageStatus.Custom(image)
+        val imageOfModel = image
+            ?.trim()
+            ?.takeIf { it.isNotEmpty() }
+            ?.let(ProfileImageStatus::Custom)
+            ?: ProfileImageStatus.Default

18-31: 사소한 정리: 불필요한 지역 변수 제거

nicknameOfModel은 단순 전달이므로 인라인하면 가독성이 좋아집니다.

-        val nicknameOfModel = nickname
...
-        return Profile(nicknameOfModel, birthDateOfModel, imageOfModel)
+        return Profile(nickname, birthDateOfModel, imageOfModel)
core/data/src/test/java/com/acon/core/data/mapping/ProfileMappingTest.kt (4)

13-14: MockKExtension 미사용 — 제거 권장

이 테스트는 목 객체를 사용하지 않습니다. 불필요한 확장을 제거해 노이즈를 줄이세요.

-@ExtendWith(MockKExtension::class)
-class ProfileMappingTest {
+class ProfileMappingTest {

31-31: assertEquals 인자 순서 교정

일관된 메시지/리포트를 위해 assertEquals(expected, actual) 순서를 사용하세요.

-        assertEquals(actualNickname, expectedNickname)
+        assertEquals(expectedNickname, actualNickname)

78-94: 유효하지 않은 날짜 포맷 케이스는 적절합니다. 추가 경계 케이스 제안

"1999.13.01"(월 범위 초과), "1999.02.29"(평년 2/29), 공백/빈 문자열(" ", "")도 커버하면 회귀 방지에 도움 됩니다.

원하시면 위 케이스를 포함한 파라미터라이즈드 테스트 초안을 드리겠습니다.


96-113: 이미지 커스텀 매핑 테스트는 적절합니다. 소소한 정리 제안

변수명에 URL 약어 대문자 사용 및 불필요한 괄호 제거로 읽기 쉬워집니다.

-        val expectedImageUrl = "Custom Profile Image Url"
+        val expectedImageURL = "Custom Profile Image Url"
...
-        assertIs<ProfileImageStatus.Custom>(actualProfileImageStatus)
-        assertEquals(expectedImageUrl, (actualProfileImageStatus).url)
+        assertIs<ProfileImageStatus.Custom>(actualProfileImageStatus)
+        assertEquals(expectedImageURL, actualProfileImageStatus.url)
core/model/src/main/java/com/acon/acon/core/model/type/SignInStatus.kt (1)

1-5: 세미콜론 제거(스타일 니트픽) 및 직렬화 필요 여부 확인

  • enum 끝의 ;는 본문이 없으므로 불필요합니다(가독성).
  • 이 타입이 저장/전송에 쓰이면 @Serializable 부착을 검토해주세요.

적용 예시:

 enum class SignInStatus {
-    USER, GUEST;
+    USER, GUEST
 }
feature/profile/src/androidTest/AndroidManifest.xml (1)

5-13: androidTest Activity exported 설정 보수화 권장

  • 테스트 전용 매니페스트이긴 하나, 외부에서 인텐트로 호출될 필요가 없다면 exported="false"가 더 안전합니다.
  • AdMob 테스트 앱 ID 사용은 적절합니다(샘플 ID).

제안 diff:

-        <activity
+        <activity
             android:name="androidx.activity.ComponentActivity"
-            android:exported="true"
+            android:exported="false"
             android:theme="@android:style/Theme.Material.Light.NoActionBar.Fullscreen" />
feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotlist/composable/SpotItem.kt (1)

118-119: 네이밍 정리(가독성 니트픽): userType → signInStatus

  • 현재 값이 SignInStatus이므로 변수명을 signInStatus로 맞추면 코드 의도가 더 명확합니다. 동작 변화는 없습니다.

적용 예시:

-    val userType = LocalSignInStatus.current
+    val signInStatus = LocalSignInStatus.current
...
-                if (userType == com.acon.acon.core.model.type.SignInStatus.GUEST)
+                if (signInStatus == com.acon.acon.core.model.type.SignInStatus.GUEST)

Also applies to: 204-208

feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotdetail/composable/SpotDetailScreen.kt (4)

129-130: 로그인 상태 비교를 일반화(!= GUEST)하여 미래 상태 추가에 내성 있게 만들기

USER 고정 비교는 신규 상태(예: PREMIUM, ADMIN 등) 추가 시 분기 누락 위험이 있습니다. 비게스트만 허용하는 의도라면 GUEST 여부만 판정하세요.

-} else if (deepLinkHandler.hasDeepLink.value && userType == com.acon.acon.core.model.type.SignInStatus.USER) {
+} else if (deepLinkHandler.hasDeepLink.value && userType != com.acon.acon.core.model.type.SignInStatus.GUEST) {
-} else if (deepLinkHandler.hasDeepLink.value && userType == com.acon.acon.core.model.type.SignInStatus.USER) {
+} else if (deepLinkHandler.hasDeepLink.value && userType != com.acon.acon.core.model.type.SignInStatus.GUEST) {

Also applies to: 258-259


93-93: 변수명 정리 제안: userType → signInStatus

의미를 직관적으로 맞춰 가독성을 높여주세요.

-    val userType = LocalSignInStatus.current
+    val signInStatus = LocalSignInStatus.current

413-419: 비로그인 북마크 클릭 시 딥링크 초기화(deepLinkHandler.clear()) 재검토

로그인 유도 직후 딥링크 컨텍스트를 지우면 로그인 완료 후 원 상세로의 원복이 어려울 수 있습니다. 초기화 시점 또는 복구 전략(복귀용 파라미터 저장) 확인 부탁드립니다.


420-421: isBookmarkSelected 계산식 단순화

게스트 여부만 배제하고 나머지는 상태 그대로 반영하면 의도가 더 명확합니다.

- isBookmarkSelected = if (userType == com.acon.acon.core.model.type.SignInStatus.GUEST) false else state.spotDetail.isSaved,
+ isBookmarkSelected = userType != com.acon.acon.core.model.type.SignInStatus.GUEST && state.spotDetail.isSaved,
feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotdetail/composable/SpotDetailViewModel.kt (3)

46-61: signInStatus 수집 시 중복 네트워크 호출 가능성 — distinctUntilChanged() 적용 권장

로그인 상태가 동일 값으로 재방출되면 fetchedSpotDetail()이 반복 실행될 수 있습니다. 중복 호출을 방지하세요.

-            signInStatus.collect {
+            signInStatus
+                .distinctUntilChanged()
+                .collect { 

64-64: UI 지연용 delay(800) 제거 또는 빌드플래그/디버그 전용으로 한정

지연은 UX에 악영향을 줄 수 있습니다. 스켈레톤 연출 목적이면 디버그 빌드에서만 적용하거나, 로딩 상태 지속시간을 UI 레이어에서 제어하세요.


83-90: signInStatus.value 직접 접근 최소화

동일 함수 내에서 collect와 value 혼용은 경쟁 상태 가독성을 떨어뜨립니다. collect 블록의 현재 it 값을 캡처해 사용하거나 snapshot을 인자로 넘기세요.

feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotdetail/composable/SpotDetailScreenContainer.kt (1)

44-44: useSignInStatus 호출 시점 조정(선 초기화 권장)

로그인 상태 의존 초기 로딩 타이밍을 안정화하려면 state 수집 이전에 호출하는 편이 안전합니다.

-    val state by viewModel.collectAsState()
+    viewModel.useSignInStatus()
+    val state by viewModel.collectAsState()
@@
-    viewModel.useSignInStatus()

호출 순서가 변화해도 UI 테스트가 통과하는지 확인 부탁드립니다.

core/data/src/main/kotlin/com/acon/core/data/session/SessionHandler.kt (1)

28-29: 변수명이 혼동을 일으킬 수 있습니다.

_signInStatus를 노출하는 프로퍼티명이 userType으로 되어있어 혼란을 줄 수 있습니다.

명확성을 위해 다음과 같이 수정하는 것을 권장합니다:

     private val _signInStatus = MutableStateFlow(SignInStatus.GUEST)
-    private val userType = _signInStatus.asStateFlow()
+    private val signInStatus = _signInStatus.asStateFlow()

그리고 Line 42에서:

     override fun getUserType(): Flow<SignInStatus> {
-        return userType
+        return signInStatus
     }
core/data/src/test/java/com/acon/core/data/datasource/remote/ProfileRemoteDataSourceTest.kt (1)

134-151: SavedSpotsResponse 래퍼 언래핑 테스트: OK. 케이스 보강 제안

래퍼 → 리스트 언래핑을 정확히 검증하고 있습니다. 추가로 빈 리스트(0개) 케이스를 하나 더 넣으면 회귀에 강해집니다. 변수명 actualSavedSpotsResponse는 실제 타입(List)에 맞춰 actualSavedSpots로 정리하면 가독성이 좋아집니다.

Also applies to: 160-160

feature/settings/src/main/java/com/acon/acon/feature/settings/screen/SettingsViewModel.kt (1)

20-24: 수집 최적화 및 미래 확장 대비 가드 추가 제안

중복 방출 방지와 향후 상태 추가에 대비해 distinctUntilChanged와 else 가드를 권장합니다.

-            userRepository.getSignInStatus().collectLatest { signInStatus ->
-                when (signInStatus) {
-                    SignInStatus.GUEST -> reduce { SettingsUiState.Guest }
-                    SignInStatus.USER -> reduce { SettingsUiState.User() }
-                }
-            }
+            userRepository.getSignInStatus()
+                .distinctUntilChanged()
+                .collectLatest { signInStatus ->
+                    when (signInStatus) {
+                        SignInStatus.GUEST -> reduce { SettingsUiState.Guest }
+                        SignInStatus.USER -> reduce { SettingsUiState.User() }
+                        else -> reduce { SettingsUiState.Guest } // forward-compat 가드
+                    }
+                }

추가로 필요한 import:

import kotlinx.coroutines.flow.distinctUntilChanged
core/data/src/main/kotlin/com/acon/core/data/repository/ProfileRepositoryImpl.kt (1)

72-82: 캐시 실패로 네트워크 결과까지 실패하지 않도록 방어 코드 추가

캐시 저장 실패가 전체 Result를 실패로 만들지 않도록 캐시 단계만 개별적으로 보호하세요.

     private fun getSavedSpotsFromRemote(): Flow<Result<List<SavedSpot>>> {
         return flow {
             emit(runCatchingWith {
                 val savedSpotResponses = profileRemoteDataSource.getSavedSpots()
                 val savedSpots = savedSpotResponses.map { it.toSavedSpot() }
-
-                profileLocalDataSource.cacheSavedSpots(savedSpots)
+                // 캐시 오류는 경고만 남기고 진행
+                runCatching { profileLocalDataSource.cacheSavedSpots(savedSpots) }
+                    .onFailure { Timber.w(it, "Failed to cache saved spots; proceeding with network result") }
 
                 savedSpots
             })
         }
     }

추가 import:

import timber.log.Timber
app/src/main/java/com/acon/acon/MainViewModel.kt (2)

25-26: 불필요한 재방출 처리와 오류 내구성 개선 제안

distinctUntilChanged와 catch로 불필요한 state 갱신과 스트림 중단을 방지하세요.

-            userRepository.getSignInStatus().collectLatest {
-                _state.value = state.value.copy(signInStatus = it)
-            }
+            userRepository.getSignInStatus()
+                .distinctUntilChanged()
+                .catch { Timber.e(it, "getSignInStatus stream error") }
+                .collectLatest { status ->
+                    _state.value = state.value.copy(signInStatus = status)
+                }

추가 import:

import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.catch
import timber.log.Timber

59-59: FQCN 대신 import로 간결화 권장

데이터 클래스에서 SignInStatus를 FQCN으로 사용 중입니다. 상단 import로 정리하면 가독성이 좋아집니다.

-    val signInStatus: com.acon.acon.core.model.type.SignInStatus = com.acon.acon.core.model.type.SignInStatus.GUEST,
+    val signInStatus: SignInStatus = SignInStatus.GUEST,

추가 import:

import com.acon.acon.core.model.type.SignInStatus
core/data/src/main/kotlin/com/acon/core/data/di/ApiModule.kt (1)

85-91: ProfileApi 프로바이더 마이그레이션 확인 완료, 네이밍 통일 권장
ProfileRemoteDataSource/Repository 등에서 모든 ProfileApi 주입이 일관되게 전환된 것을 확인했습니다. ApiModule 내 다른 @provides 함수명(‘provideXxxApi’)과 일치하도록 providesProfileApiprovideProfileApi로 수정해주세요.

feature/profile/src/main/java/com/acon/feature/profile/info/composable/ProfileInfoScreen.kt (3)

71-72: 상단 바가 상태바와 겹칠 수 있음 — statusBarsPadding 추가 권장

Edge‑to‑edge 환경에서 상단 바가 시스템 상태바와 시각적으로 겹칠 수 있습니다. Acon의 다른 화면(예: SpotListScreen)과 동일하게 statusBarsPadding을 적용해 주세요.

적용 diff:

@@
 import androidx.compose.foundation.rememberScrollState
 import androidx.compose.foundation.verticalScroll
+import androidx.compose.foundation.layout.statusBarsPadding
@@
-                modifier = Modifier.padding(vertical = 14.dp)
+                modifier = Modifier
+                    .statusBarsPadding()
+                    .padding(vertical = 14.dp)
             )

Also applies to: 3-10


41-49: verticalScroll 컨테이너 내부에서 fillMaxSize 사용 주의

verticalScroll이 적용된 Column 내부에서 자식이 fillMaxSize를 사용하면 무한 높이 제약과 충돌해 레이아웃 경고 또는 의도치 않은 확장/스크롤 이슈가 발생할 수 있습니다. 실패/로딩 UI는 스크롤 영역 밖(Box/Column.weight)에서 전개하거나, 최소 변경으로는 가로만 채우도록 조정해 주세요.

적용(최소 변경) diff:

@@
                 is ProfileInfoUiState.LoadFailed -> {
                     NetworkErrorView(
                         onRetry = actions.retryOnError,
-                        modifier = Modifier.fillMaxSize()
+                        modifier = Modifier
+                            .fillMaxWidth()
+                            .padding(vertical = 48.dp)
                     )
                 }

권장(구조 변경): 스크롤 Column은 성공/게스트 상태에만 적용하고, Loading/LoadFailed는 상위 Column에서 weight(1f) Box로 분리 렌더링.

Also applies to: 112-117


108-111: Loading 상태에 비가시 컨텐츠 — 최소 로딩 인디케이터 표시 권장

로딩일 때 아무것도 렌더링하지 않으면 UX 가이드와 불일치합니다. 디자인시스템의 공통 로딩(스켈레톤/인디케이터)이 있다면 여기서 노출해 주세요.

feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotlist/composable/SpotListScreenContainer.kt (2)

41-41: 네이밍 정합성: userType → signInStatus로 변경

프로젝트 전반의 마이그레이션 방향(SignInStatus)에 맞춰 변수명을 일관되게 유지해 주세요.

적용 diff:

-    val userType = LocalSignInStatus.current
+    val signInStatus = LocalSignInStatus.current
@@
-                if (userType == SignInStatus.GUEST)
+                if (signInStatus == SignInStatus.GUEST)
                     onSignInRequired("click_detail_guest?")
                 else
                     viewModel.onSpotClicked(spot, rank)

Also applies to: 67-71


67-69: 앰플리튜드 프로퍼티 키 통일

"click_detail_guest?"처럼 '?'가 섞여 있습니다. 다른 키들과 통일(예: click_detail_guest)해 주세요.

적용 diff:

-                if (signInStatus == SignInStatus.GUEST)
-                    onSignInRequired("click_detail_guest?")
+                if (signInStatus == SignInStatus.GUEST)
+                    onSignInRequired("click_detail_guest")
feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotlist/composable/SpotListScreen.kt (3)

88-90: PagerState 재생성으로 포지션 리셋/재구성이 발생할 수 있음 — pageCount만 동적 제공

pagerState를 분기 내에서 rememberPagerState로 재할당하면 스크롤 포지션이 리셋되고 재구성 비용이 커집니다. 상단에서 pageCount를 계산하고 단일 rememberPagerState에 공급하세요.

적용 diff:

@@
-    var pagerState = rememberPagerState { 0 }
+    val pageCount = when (state) {
+        is SpotListUiStateV2.Success -> {
+            val size = state.spotList.size
+            when {
+                size >= 11 -> size + 2
+                size >= 5  -> size + 1
+                else       -> size
+            }
+        }
+        else -> 0
+    }
+    val pagerState = rememberPagerState { pageCount }
@@
-                            SpotType.RESTAURANT -> {
-                                pagerState = rememberPagerState {
-                                    val size = state.spotList.size
-                                    when {
-                                        size >= 11 -> size + 2
-                                        size >= 5  -> size + 1
-                                        else       -> size
-                                    }
-                                }
+                            SpotType.RESTAURANT -> {
@@
-                            SpotType.CAFE -> {
-                                pagerState = rememberPagerState {
-                                    val size = state.spotList.size
-                                    when {
-                                        size >= 11 -> size + 2
-                                        size >= 5  -> size + 1
-                                        else       -> size
-                                    }
-                                }
+                            SpotType.CAFE -> {

Also applies to: 171-181, 202-212


91-93: 네이밍/참조 일관성 및 게스트 액션 키 보완

  • 변수명: userType → signInStatus
  • fully‑qualified SignInStatus 제거(이미 import됨)
  • 게스트의 필터 클릭 시 프로퍼티 키 누락 보완

적용 diff:

@@
-    val userType = LocalSignInStatus.current
+    val signInStatus = LocalSignInStatus.current
@@
-                    if (userType == SignInStatus.GUEST)
+                    if (signInStatus == SignInStatus.GUEST)
                         onSignInRequired("click_toggle_guest?")
                     else
                         onSpotTypeChanged(it)
@@
-                        if (userType == com.acon.acon.core.model.type.SignInStatus.GUEST)
-                            onSignInRequired("")
+                        if (signInStatus == SignInStatus.GUEST)
+                            onSignInRequired("click_filter_guest")
                         else
                             onFilterButtonClick()
@@
-                                    signInStatus = userType,
+                                    signInStatus = signInStatus,
@@
-                                    signInStatus = userType,
+                                    signInStatus = signInStatus,
@@
-                        if (userType == com.acon.acon.core.model.type.SignInStatus.GUEST) {
+                        if (signInStatus == SignInStatus.GUEST) {
                             onSignInRequired("click_upload_guest?")
                         } else {
                             onNavigateToUploadScreen()
                         }

Also applies to: 121-126, 137-142, 192-200, 222-230, 286-294


323-324: Preview의 불필요한 FQCN 제거

이미 SpotType을 import하고 있으므로 FQCN 대신 심볼을 직접 사용해 가독성을 높이세요.

적용 diff:

-        state = SpotListUiStateV2.Loading(com.acon.acon.core.model.type.SpotType.RESTAURANT),
+        state = SpotListUiStateV2.Loading(SpotType.RESTAURANT),
feature/profile/src/test/kotlin/com/acon/feature/profile/info/viewmodel/ProfileInfoViewModelTest.kt (1)

150-157: 게스트 업로드 클릭 케이스 테스트 미구현(TODO)

의도대로라면 게스트에서 onUploadClicked 호출 시 로그인 유도용 사이드이펙트가 방출되어야 합니다. 테스트를 추가해 주세요.

원하시면 ViewModel의 사이드이펙트 명세에 맞춰 테스트 본문을 생성해 드리겠습니다(예: RequestSignIn("click_upload_guest")).

feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotlist/composable/SpotListSuccessView.kt (5)

75-86: 광고 아이템 삽입 인덱스 계산 안정화 필요

현재는 size>=11 삽입 후 size>=5 삽입을 수행하여, 원래 11번째 위치에 삽입하려던 광고가 12로 밀릴 수 있습니다. 또한 삽입 순서만 바꾸면 원소 수 판단이 왜곡됩니다. 원본 크기를 보존해 조건을 평가하고, 그 다음 삽입을 수행하도록 수정해 주세요.

적용 예시:

-    val adInsertedSpot = remember(state.spotList) {
-        val list: MutableList<com.acon.acon.core.model.model.spot.Spot?> = state.spotList.toMutableList()
-
-        if (list.size >= 11) {
-            list.add(11, null)
-        }
-        if (list.size >= 5) {
-            list.add(5, null)
-        }
-
-        list
-    }
+    val adInsertedSpot = remember(state.spotList) {
+        val list: MutableList<com.acon.acon.core.model.model.spot.Spot?> = state.spotList.toMutableList()
+        val originalSize = list.size
+        if (originalSize >= 5) list.add(5, null)
+        if (originalSize >= 11) list.add(11, null)
+        list
+    }

207-220: 예외 삼키기 지양

graphicsLayer 블록에서 빈 catch로 예외를 삼키면 원인 파악이 어렵습니다. 최소한 로그를 남기거나, 사전에 page 범위를 점검해 예외를 회피하세요.

적용 예시:

-                    .graphicsLayer {
-                        try {
+                    .graphicsLayer {
+                        kotlin.runCatching {
                             val pageOffset =
                                 pagerState.getOffsetDistanceInPages(page).absoluteValue
                             val ratio = lerp(
                                 start = 1.1f,
                                 stop = 0.9f,
                                 fraction = pageOffset.coerceIn(0f, 1f)
                             )
                             scaleX = ratio
                             scaleY = ratio
-                        } catch (_: Exception) {
-                        }
+                        }

131-151: 하드코딩된 문자열 리소스화

"네이버 지도", "카카오 지도"는 문자열 리소스로 분리해야 i18n/접근성/번역 워크플로우에 유리합니다. stringResource를 사용하도록 교체해 주세요.

적용 예시(리소스 키는 예시):

-                        Text(
-                            text = "네이버 지도",
+                        Text(
+                            text = stringResource(R.string.naver_map),
...
-                        Text(
-                            text = "카카오 지도",
+                        Text(
+                            text = stringResource(R.string.kakao_map),

Also applies to: 152-171


279-283: LaunchedEffect 키 고정으로 색상 갱신 누락 가능성

키를 Unit으로 두면 동일 페이지에서 spot이 갱신되어도 색상이 갱신되지 않습니다. spot(혹은 spot.image)을 키로 사용하세요.

-            LaunchedEffect(Unit) {
+            LaunchedEffect(spot?.image) {
                 if (spot != null) {
                     spotFogColor = if (spot.image.isBlank()) Color(0xFFE17651) else spot.image.getOverlayColor(context)
                 }
             }

59-60: 게스트 노출 개수 상수 중복 관리 제안

동일 상수(MAX_GUEST_AVAILABLE_COUNT)가 SpotEmptyView에도 존재합니다. 공용 모듈의 object/const로 집약해 단일 출처로 관리하는 것을 권장합니다.

core/data/src/main/kotlin/com/acon/core/data/datasource/local/ProfileLocalDataSource.kt (2)

39-46: 널 대신 빈 리스트 사용 고려

Flow<List?>로 널 가능을 열어두면 소비자 측 분기가 늘어납니다. 가능하다면 빈 리스트를 기본값으로 사용해 Flow<List>로 단순화하세요. 외부 계약 영향이 크면 추후 마이그레이션 계획만 수립해도 좋습니다.

적용 방향:

  • _savedSpots 초기값을 emptyList()로 변경
  • 인터페이스를 fun getSavedSpots(): Flow<List>로 변경

47-50: 인메모리 캐시인 점에 대한 운영 고려

clearCache로 세션 종료 시 정리는 잘 되어 있습니다. 다만 프로세스 재시작 시 캐시가 소실됩니다. 콜드 스타트 UX 개선이 필요하다면 DataStore/Room 등 영속 캐시를 고려하세요.

feature/profile/src/main/java/com/acon/feature/profile/info/composable/ProfileView.kt (1)

79-86: 벡터 리소스에는 Icon 사용 권장

현재 벡터를 Image로 그립니다. 의미적으로 Icon이 더 적합하며, 일관성도 좋아집니다.

-                Image(
-                    imageVector = ImageVector.vectorResource(R.drawable.ic_edit),
+                Icon(
+                    imageVector = ImageVector.vectorResource(R.drawable.ic_edit),
                     contentDescription = stringResource(R.string.content_description_edit_profile),
                     modifier = Modifier
                         .testTag(TestTags.PROFILE_UPDATE_ICON)
                         .padding(start = 4.dp)
                         .noRippleClickable { onProfileUpdateIconClick() }
                 )
feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotlist/composable/SpotEmptyView.kt (3)

52-55: 화면 회전 등 밀도 변화 시 아이템 높이 불일치 가능

itemHeightPx를 remember에 의존성 없이 저장하면 화면 높이/밀도 변경 시 갱신되지 않습니다. screenHeightPx를 키로 포함하세요.

-    val itemHeightPx by remember {
-        mutableFloatStateOf(screenHeightPx * .3f)
-    }
+    val itemHeightPx by remember(screenHeightPx) {
+        mutableFloatStateOf(screenHeightPx * .3f)
+    }

130-133: 토스트 문구 하드코딩 제거

사용자 문구는 stringResource로 관리하세요.

-                        context.showToast("웹사이트 접속에 실패했어요")
+                        context.showToast(context.getString(R.string.fail_to_open_website))

37-38: 게스트 노출 상수 중복

SpotListSuccessView와 동일한 상수가 중복됩니다. 공용 상수로 추출해 단일 출처로 관리하는 것을 권장합니다.

feature/profile/src/androidTest/kotlin/com/acon/feature/profile/info/composable/ProfileInfoScreenTest.kt (2)

165-173: 부동소수점 비교 안정화

레이아웃 좌표는 소수 오차가 발생할 수 있어 절대 동일 비교가 불안정합니다. 허용 오차를 두어 검증하세요.

-        assertEquals(expectedBottomBarTop, actualBottomBarTop)
+        assertEquals(expectedBottomBarTop, actualBottomBarTop, absoluteTolerance = 0.5f)

151-159: UI 안정화 대기 추가

setContent 직후 즉시 측정/동작 시 플래키해질 수 있습니다. waitForIdle을 추가해 안정성을 높이세요.

         composeTestRule.setContent {
             ProfileInfoScreen(
                 state = ProfileInfoUiState.User(
                     profile = dummyProfile,
                     savedSpots = emptyList()
                 ),
                 actions = mockActions
             )
         }
+        composeTestRule.waitForIdle()
feature/profile/src/main/java/com/acon/feature/profile/info/composable/SavedSpotsView.kt (3)

89-95: onClick 시그니처 단순화

SavedSpotItem은 spot을 이미 인자로 받아 내부에서 spotId에 접근 가능합니다. onClick 타입을 () -> Unit으로 단순화하면 호출부/구현부 모두 명확해집니다.

-                    SavedSpotItem(
+                    SavedSpotItem(
                         spot = spot,
-                        onClick = { onSavedSpotItemClick(spot.spotId) },
+                        onClick = { onSavedSpotItemClick(spot.spotId) },
                         modifier = Modifier
                             .aspectRatio(150f / 217f)
                             .testTag(TestTags.SAVED_SPOT_ITEM + spot.spotId)
                     )
...
-private fun SavedSpotItem(
-    spot: SavedSpot,
-    onClick: (spotId: Long) -> Unit,
+private fun SavedSpotItem(
+    spot: SavedSpot,
+    onClick: () -> Unit,
     modifier: Modifier = Modifier
 ) {
     Box(
         modifier = modifier
             .clip(RoundedCornerShape(8.dp))
-            .clickable { onClick(spot.spotId) }
+            .clickable { onClick() }
     ) {

Also applies to: 109-118


131-142: 수동 문자열 자르기 대신 TextOverflow.Ellipsis 사용

수동 substring은 다국어/이모지 조합에서 깨질 수 있습니다. maxLines+overflow로 대체하세요.

-                Text(
-                    text = if (spot.spotName.length > 9) spot.spotName.take(8) + stringResource(R.string.ellipsis) else spot.spotName,
+                Text(
+                    text = spot.spotName,
                     color = AconTheme.color.White,
                     style = AconTheme.typography.Title5,
                     fontWeight = FontWeight.SemiBold,
-                    maxLines = 1,
+                    maxLines = 1,
+                    overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis,
                     modifier = Modifier
                         .align(Alignment.TopCenter)
                         .fillMaxWidth()
                         .padding(top = 20.dp)
                         .padding(horizontal = 20.dp)
                 )
...
-                Text(
-                    text = if (spot.spotName.length > 9) spot.spotName.take(8) + stringResource(R.string.ellipsis) else spot.spotName,
+                Text(
+                    text = spot.spotName,
                     color = AconTheme.color.White,
                     style = AconTheme.typography.Title5,
                     fontWeight = FontWeight.SemiBold,
+                    maxLines = 1,
+                    overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis,
                     modifier = Modifier
                         .fillMaxWidth()
                         .align(Alignment.TopCenter)
                         .padding(top = 20.dp)
                         .padding(horizontal = 20.dp)
                 )

Also applies to: 155-165


80-83: 사소한 정리: 불필요한 Modifier 인자

LazyRow에 modifier = Modifier는 불필요합니다. 제거해 가독성을 높일 수 있습니다.

-            LazyRow(
-                modifier = Modifier,
+            LazyRow(
                 horizontalArrangement = Arrangement.spacedBy(10.dp)
             ) {
core/data/src/main/kotlin/com/acon/core/data/repository/SpotRepositoryImpl.kt (1)

125-134: deleteBookmark에도 캐시 동기화 로직 추가 필요

addBookmark에는 새로운 캐시 동기화 로직이 추가되었지만, deleteBookmark에는 추가되지 않았습니다. 일관성을 위해 삭제 시에도 동일한 캐시 업데이트가 필요할 수 있습니다.

 override suspend fun deleteBookmark(spotId: Long): Result<Unit> {
     return runCatchingWith(DeleteBookmarkError()) {
         spotRemoteDataSource.deleteBookmark(spotId)
 
+        val cachedSavedSpots = profileLocalDataSource.getSavedSpots().firstOrNull()
+        if (cachedSavedSpots != null)
+            profileLocalDataSource.cacheSavedSpots(profileRemoteDataSource.getSavedSpots().map {
+                it.toSavedSpot()
+            })
+
         profileRepositoryLegacy.fetchSavedSpots().onSuccess { fetched ->
             (profileInfoCacheLegacy.data.value.getOrNull()
                 ?: return@onSuccess).let { profileInfo ->
                 profileInfoCacheLegacy.updateData(profileInfo.copy(savedSpotLegacies = fetched))
             }
         }
     }
 }
feature/profile/src/main/java/com/acon/feature/profile/info/viewmodel/ProfileInfoViewModel.kt (2)

108-109: 비동기 작업에서 동기 호출 사용

userRepository.getSignInStatus().first()는 suspend 함수인데 intent 블록 내에서 직접 호출하면 블로킹될 수 있습니다.

 fun onUploadClicked() = intent {
-    if (userRepository.getSignInStatus().first() == SignInStatus.GUEST) {
+    if (signInStatus.value == SignInStatus.GUEST) {
         onRequestSignIn()
     } else {
         postSideEffect(ProfileInfoSideEffect.NavigateToUpload)
     }
 }

115-117: 하드코딩된 문자열 상수화 필요

"click_upload_guest?" 문자열이 하드코딩되어 있습니다. 상수로 분리하는 것이 좋습니다.

+    companion object {
+        private const val SIGN_IN_HINT_UPLOAD_GUEST = "click_upload_guest?"
+    }
+
     fun onRequestSignIn() {
-        super.onRequestSignIn?.invoke("click_upload_guest?")
+        super.onRequestSignIn?.invoke(SIGN_IN_HINT_UPLOAD_GUEST)
     }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 081db49 and 94564f4.

📒 Files selected for processing (54)
  • app/src/main/java/com/acon/acon/MainActivity.kt (4 hunks)
  • app/src/main/java/com/acon/acon/MainViewModel.kt (2 hunks)
  • app/src/main/java/com/acon/acon/navigation/AconNavigation.kt (1 hunks)
  • app/src/main/java/com/acon/acon/navigation/nested/ProfileNavigationLegacy.kt (1 hunks)
  • core/data/src/main/kotlin/com/acon/core/data/api/remote/ProfileApi.kt (1 hunks)
  • core/data/src/main/kotlin/com/acon/core/data/datasource/local/ProfileLocalDataSource.kt (2 hunks)
  • core/data/src/main/kotlin/com/acon/core/data/datasource/remote/ProfileRemoteDataSource.kt (1 hunks)
  • core/data/src/main/kotlin/com/acon/core/data/datasource/remote/SpotRemoteDataSource.kt (2 hunks)
  • core/data/src/main/kotlin/com/acon/core/data/di/ApiModule.kt (2 hunks)
  • core/data/src/main/kotlin/com/acon/core/data/di/RepositoryModule.kt (1 hunks)
  • core/data/src/main/kotlin/com/acon/core/data/dto/response/profile/ProfileResponse.kt (1 hunks)
  • core/data/src/main/kotlin/com/acon/core/data/dto/response/profile/SavedSpotResponse.kt (1 hunks)
  • core/data/src/main/kotlin/com/acon/core/data/dto/response/profile/SavedSpotsResponse.kt (1 hunks)
  • core/data/src/main/kotlin/com/acon/core/data/repository/ProfileRepositoryImpl.kt (1 hunks)
  • core/data/src/main/kotlin/com/acon/core/data/repository/SpotRepositoryImpl.kt (2 hunks)
  • core/data/src/main/kotlin/com/acon/core/data/repository/UserRepositoryImpl.kt (3 hunks)
  • core/data/src/main/kotlin/com/acon/core/data/session/SessionHandler.kt (3 hunks)
  • core/data/src/test/java/com/acon/core/data/datasource/remote/ProfileRemoteDataSourceTest.kt (3 hunks)
  • core/data/src/test/java/com/acon/core/data/mapping/ProfileMappingTest.kt (1 hunks)
  • core/data/src/test/java/com/acon/core/data/session/SessionHandlerImplTest.kt (5 hunks)
  • core/designsystem/src/main/java/com/acon/acon/core/designsystem/component/image/DefaultProfileImage.kt (1 hunks)
  • core/model/src/main/java/com/acon/acon/core/model/type/SignInStatus.kt (1 hunks)
  • core/model/src/main/java/com/acon/acon/core/model/type/UserType.kt (0 hunks)
  • core/ui/src/main/java/com/acon/acon/core/ui/base/BaseContainerHost.kt (3 hunks)
  • core/ui/src/main/java/com/acon/acon/core/ui/compose/LocalCompositions.kt (2 hunks)
  • domain/src/main/java/com/acon/acon/domain/repository/ProfileRepository.kt (1 hunks)
  • domain/src/main/java/com/acon/acon/domain/repository/UserRepository.kt (1 hunks)
  • feature/profile/build.gradle.kts (2 hunks)
  • feature/profile/src/androidTest/AndroidManifest.xml (1 hunks)
  • feature/profile/src/androidTest/kotlin/com/acon/feature/profile/info/composable/ProfileInfoScreenTest.kt (1 hunks)
  • feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profile/ProfileViewModelLegacy.kt (2 hunks)
  • feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profile/composable/ProfileScreenContainerLegacy.kt (1 hunks)
  • feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profile/composable/ProfileScreenLegacy.kt (3 hunks)
  • feature/profile/src/main/java/com/acon/feature/profile/TestTags.kt (1 hunks)
  • feature/profile/src/main/java/com/acon/feature/profile/info/composable/ProfileInfoScreen.kt (1 hunks)
  • feature/profile/src/main/java/com/acon/feature/profile/info/composable/ProfileInfoScreenContainer.kt (1 hunks)
  • feature/profile/src/main/java/com/acon/feature/profile/info/composable/ProfileView.kt (1 hunks)
  • feature/profile/src/main/java/com/acon/feature/profile/info/composable/SavedSpotsView.kt (1 hunks)
  • feature/profile/src/main/java/com/acon/feature/profile/info/viewmodel/ProfileInfoViewModel.kt (1 hunks)
  • feature/profile/src/test/kotlin/com/acon/feature/profile/info/viewmodel/ProfileInfoViewModelTest.kt (1 hunks)
  • feature/settings/src/main/java/com/acon/acon/feature/settings/screen/SettingsViewModel.kt (2 hunks)
  • feature/signin/src/main/java/com/acon/acon/feature/signin/screen/SignInScreen.kt (2 hunks)
  • feature/signin/src/main/java/com/acon/acon/feature/signin/screen/SignInScreenContainer.kt (1 hunks)
  • feature/signin/src/main/java/com/acon/acon/feature/signin/screen/SignInViewModel.kt (3 hunks)
  • feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotdetail/composable/SpotDetailScreen.kt (5 hunks)
  • feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotdetail/composable/SpotDetailScreenContainer.kt (1 hunks)
  • feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotdetail/composable/SpotDetailViewModel.kt (2 hunks)
  • feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotlist/SpotListViewModel.kt (2 hunks)
  • feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotlist/composable/SpotEmptyView.kt (4 hunks)
  • feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotlist/composable/SpotItem.kt (3 hunks)
  • feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotlist/composable/SpotListScreen.kt (7 hunks)
  • feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotlist/composable/SpotListScreenContainer.kt (4 hunks)
  • feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotlist/composable/SpotListSuccessView.kt (3 hunks)
  • gradle/libs.versions.toml (6 hunks)
💤 Files with no reviewable changes (1)
  • core/model/src/main/java/com/acon/acon/core/model/type/UserType.kt
🧰 Additional context used
🧬 Code graph analysis (10)
core/data/src/main/kotlin/com/acon/core/data/repository/ProfileRepositoryImpl.kt (1)
core/data/src/main/kotlin/com/acon/core/data/error/ErrorUtils.kt (2)
  • runCatchingWith (7-24)
  • runCatchingWith (26-40)
feature/profile/src/main/java/com/acon/feature/profile/info/composable/ProfileInfoScreen.kt (5)
core/designsystem/src/main/java/com/acon/acon/core/designsystem/component/topbar/AconTopBar.kt (1)
  • AconTopBar (22-68)
feature/profile/src/main/java/com/acon/feature/profile/info/composable/ProfileView.kt (2)
  • UserProfileView (30-90)
  • GuestProfileView (92-130)
feature/profile/src/main/java/com/acon/feature/profile/info/composable/SavedSpotsView.kt (1)
  • SavedSpotsView (41-106)
core/designsystem/src/main/java/com/acon/acon/core/designsystem/component/error/NetworkErrorView.kt (1)
  • NetworkErrorView (23-65)
core/designsystem/src/main/java/com/acon/acon/core/designsystem/component/bottombar/AconBottomBar.kt (1)
  • AconBottomBar (35-55)
feature/profile/src/main/java/com/acon/feature/profile/info/composable/ProfileView.kt (1)
core/designsystem/src/main/java/com/acon/acon/core/designsystem/component/image/DefaultProfileImage.kt (1)
  • DefaultProfileImage (11-20)
app/src/main/java/com/acon/acon/navigation/AconNavigation.kt (1)
app/src/main/java/com/acon/acon/navigation/nested/ProfileNavigationLegacy.kt (1)
  • profileNavigationLegacy (24-103)
feature/profile/src/main/java/com/acon/feature/profile/info/composable/ProfileInfoScreenContainer.kt (2)
feature/profile/src/main/java/com/acon/feature/profile/info/composable/ProfileInfoScreen.kt (1)
  • ProfileInfoScreen (34-140)
feature/profile/src/main/java/com/acon/feature/profile/info/viewmodel/ProfileInfoViewModel.kt (1)
  • loadState (35-75)
feature/profile/src/main/java/com/acon/feature/profile/info/composable/SavedSpotsView.kt (2)
feature/profile/src/main/java/com/acon/feature/profile/info/composable/ProfileInfoScreenContainer.kt (1)
  • onSavedSpotItemClick (61-83)
core/designsystem/src/main/java/com/acon/acon/core/designsystem/image/ImageUtils.kt (1)
  • rememberDefaultLoadImageErrorPainter (116-143)
feature/profile/src/androidTest/kotlin/com/acon/feature/profile/info/composable/ProfileInfoScreenTest.kt (1)
feature/profile/src/main/java/com/acon/feature/profile/info/composable/ProfileInfoScreen.kt (1)
  • ProfileInfoScreen (34-140)
feature/profile/build.gradle.kts (1)
build-logic/convention/src/main/kotlin/utils/DependencyExtensions.kt (2)
  • testImplementation (29-31)
  • androidTestImplementation (25-27)
app/src/main/java/com/acon/acon/MainActivity.kt (1)
core/ui/src/main/java/com/acon/acon/core/ui/EntryPointUtils.kt (1)
  • activityComponentEntryPoint (38-44)
app/src/main/java/com/acon/acon/navigation/nested/ProfileNavigationLegacy.kt (1)
feature/profile/src/main/java/com/acon/feature/profile/info/composable/ProfileInfoScreenContainer.kt (1)
  • ProfileInfoScreenContainer (13-59)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (42)
core/data/src/main/kotlin/com/acon/core/data/dto/response/profile/ProfileResponse.kt (1)

30-30: 도메인 Profile 생성자 시그니처 수동 확인 필요
Profile(nickname, birthDateStatus, imageStatus) 순서가 실제 데이터 클래스 정의와 정확히 일치하는지 직접 검토해주세요. 순서가 어긋날 경우 런타임 매핑 오류가 발생할 수 있습니다.

core/designsystem/src/main/java/com/acon/acon/core/designsystem/component/image/DefaultProfileImage.kt (1)

11-20: 심플하고 충분합니다

  • 벡터 리소스 + contentDescription 제공 OK. 접근성 기본 충족합니다.
core/data/src/main/kotlin/com/acon/core/data/dto/response/profile/SavedSpotResponse.kt (2)

8-13: 직렬화 어노테이션 및 키 매핑 적절

  • @serializable + @SerialName 명시로 API 스키마와 안전하게 매칭됩니다.

15-22: 널/공백 썸네일 처리 로직 합리적

  • 공백/널을 Empty로, 그 외 Exist로 분기 명확합니다.
feature/profile/build.gradle.kts (1)

40-42: JUnit Platform 활성화 LGTM

  • Kotest/Orbit 테스트 실행을 위한 설정으로 적절합니다.
feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotlist/SpotListViewModel.kt (1)

21-21: SignInStatus 마이그레이션은 적절 — ViewModel 주입·쿨다운 단위 검증 필요

  • 게스트 제외 조건(signInStatus.value != SignInStatus.GUEST)은 기존 의도와 일치합니다. (파일: feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotlist/SpotListViewModel.kt:21)
  • isCooldownExpiredUseCase(UserActionType.SKIP_AREA_VERIFICATION, 24 * 60 * 60)의 시간 단위를 명확히 하세요(초/분/밀리초). 단위가 틀리면 영역 인증 모달 노출 빈도에 영향이 있습니다.
  • 자동 검사 스크립트가 'unrecognized file type: kt' 에러로 실패했습니다. 아래 수정된 명령을 실행하거나 해당 심볼의 정의/사용 위치(파일 + 코드 스니펫)를 제공하세요:
#!/bin/bash
rg -n -C2 "signInStatus" feature/spot
rg -n -C2 "IsCooldownExpiredUseCase" .
rg -n -C2 "SKIP_AREA_VERIFICATION" .
core/data/src/main/kotlin/com/acon/core/data/datasource/remote/SpotRemoteDataSource.kt (1)

20-24: 모든 fetchSpotList 호출부가 새 시그니처(SpotListRequest, SignInStatus)로 제대로 업데이트되었습니다

feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profile/ProfileViewModelLegacy.kt (2)

6-6: LGTM! SignInStatus로 타입 변경

UserType에서 SignInStatus로의 전역적인 타입 마이그레이션이 올바르게 적용되었습니다.


22-24: LGTM! SignInStatus 사용으로 업데이트

컨테이너 초기화에서 signInStatus.collect와 SignInStatus.GUEST 사용이 올바르게 적용되었습니다.

feature/signin/src/main/java/com/acon/acon/feature/signin/screen/SignInScreenContainer.kt (1)

40-40: LGTM! useSignInStatus 메소드 호출

UserType에서 SignInStatus로의 마이그레이션에 맞춰 메소드 호출이 올바르게 업데이트되었습니다.

feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profile/composable/ProfileScreenContainerLegacy.kt (1)

69-69: LGTM! useSignInStatus 메소드 호출

프로필 화면에서도 SignInStatus 사용으로 일관성 있게 업데이트되었습니다.

feature/signin/src/main/java/com/acon/acon/feature/signin/screen/SignInScreen.kt (2)

43-43: LGTM! LocalSignInStatus 컴포지션 로컬 사용

LocalUserType에서 LocalSignInStatus로의 변경이 올바르게 적용되었습니다.


84-84: LGTM! SignInStatus 타입 사용

변수명과 비교 조건에서 SignInStatus 타입 사용이 일관성 있게 적용되었습니다.

Also applies to: 91-91

domain/src/main/java/com/acon/acon/domain/repository/UserRepository.kt (1)

6-6: LGTM! 도메인 레포지토리 API 업데이트

UserType에서 SignInStatus로의 마이그레이션이 도메인 레이어에서 올바르게 적용되었습니다. 메소드명도 의미에 맞게 getSignInStatus로 변경되었습니다.

Also applies to: 14-14

feature/signin/src/main/java/com/acon/acon/feature/signin/screen/SignInViewModel.kt (2)

7-7: LGTM! SignInStatus 임포트 추가

UserType 대신 SignInStatus 임포트로 올바르게 변경되었습니다.


28-28: LGTM! SignInStatus 사용 로직

signIn 메소드에서 SignInStatus.GUEST 사용과 signInStatus.collectLatest 호출이 올바르게 적용되었습니다.

Also applies to: 44-45

core/data/src/main/kotlin/com/acon/core/data/datasource/remote/ProfileRemoteDataSource.kt (1)

34-34: LGTM! API 응답 래퍼 언패킹

SavedSpotsResponse 래퍼에서 savedSpotList를 추출하여 반환하는 로직이 올바르게 구현되었습니다. 이는 API 응답 구조 변경에 맞춘 적절한 데이터 소스 계층 처리입니다.

feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotdetail/composable/SpotDetailViewModel.kt (1)

63-71: 딥링크 판정 로직 변경 의도 확인 요청

주석엔 “딥링크 && GUEST일 때만 isDeepLink = true”라 쓰였으나, 현재는 로그인 상태와 무관하게 isDeepLink가 전달 파라미터만으로 결정됩니다. 서버/캐시 경로 선택(fetchSpotDetail vs fetchSpotDetailFromUser)에 영향이 크니 의도한 정책인지 확인 부탁드립니다.

Also applies to: 76-80

core/ui/src/main/java/com/acon/acon/core/ui/compose/LocalCompositions.kt (1)

22-24: LocalUserType → LocalSignInStatus 전환 — 주석 한 곳만 잔존

LGTM. feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotdetail/composable/SpotDetailViewModel.kt (라인 66–67) 주석에 'UserType.GUEST' 참조만 남아 있습니다. 주석 정리 또는 SignInStatus로 업데이트하세요.

core/data/src/main/kotlin/com/acon/core/data/dto/response/profile/SavedSpotsResponse.kt (1)

6-9: 확인 결과: API, 테스트, 매퍼 반영 완료됨

  • ProfileApi.getSavedSpots() 시그니처가 SavedSpotsResponse로 변경되어 있습니다.
  • ProfileRemoteDataSourceTest 내에 SavedSpotsResponse 생성 및 검증 로직이 정상 작동합니다.
  • 매퍼 코드(*Mapper*.kt 파일)에서 SavedSpotsResponse.savedSpotList를 도메인 모델로 변환하는 로직도 이미 존재합니다.
    따라서 추가 수정 필요 없습니다.
core/data/src/main/kotlin/com/acon/core/data/di/RepositoryModule.kt (1)

73-75: 승인 — 레거시/신규 ProfileRepository 바인딩 분리 적절

core/data/src/main/kotlin/com/acon/core/data/di/RepositoryModule.kt에서 ProfileRepository와 ProfileRepositoryLegacy가 각각 @singleton + @BINDS로 분리 등록되어 있으며, 도메인/피처 소비자들이 각 타입을 올바르게 주입하고 있습니다(예: ValidateNicknameUseCase → ProfileRepository, SpotDetailViewModel 등 → ProfileRepositoryLegacy).

feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profile/composable/ProfileScreenLegacy.kt (1)

46-47: LocalSignInStatus로의 마이그레이션이 적절합니다.

LocalUserType에서 LocalSignInStatus로의 변경이 전체 코드베이스의 리팩토링과 일관성 있게 적용되었습니다.

Also applies to: 67-67, 317-317

core/data/src/main/kotlin/com/acon/core/data/repository/UserRepositoryImpl.kt (2)

31-31: 프로필 캐시 정리 로직이 적절하게 추가되었습니다.

로그아웃 시 profileLocalDataSource.clearCache()를 호출하여 프로필 관련 캐시를 정리하는 것이 올바른 접근입니다.

Also applies to: 75-75


34-34: 메서드 이름 변경이 도메인 계층과 일치합니다.

getUserType()에서 getSignInStatus()로의 변경이 도메인 계층의 인터페이스와 일관성 있게 적용되었습니다.

core/data/src/test/java/com/acon/core/data/session/SessionHandlerImplTest.kt (1)

5-5: 테스트가 SignInStatus 타입 변경을 올바르게 반영합니다.

모든 테스트 케이스가 UserType에서 SignInStatus로의 변경을 일관되게 적용하고 있으며, 기대값과 어서션이 적절합니다.

Also applies to: 69-73, 91-91, 107-107, 125-125

feature/profile/src/main/java/com/acon/feature/profile/TestTags.kt (1)

3-11: 테스트 태그 상수 정의가 적절합니다.

UI 테스트를 위한 태그 상수들을 별도 객체로 중앙화한 것이 유지보수성 측면에서 좋은 접근입니다.

core/data/src/main/kotlin/com/acon/core/data/session/SessionHandler.kt (1)

57-62: onSignInResponse의 사용처 및 목적 확인 필요

core/data/src/main/kotlin/com/acon/core/data/session/SessionHandler.kt(선언: 라인 20, 구현: 라인 57–62)에 선언·구현만 존재하며 프로젝트 내 호출처가 없습니다. 현재 구현은 access/refresh 토큰만 저장해 completeSignIn과 기능이 중복되고 사용자 상태 업데이트가 없습니다.

  • 외부 콜백 의도라면 호출처 또는 문서화 추가
  • 중복이면 completeSignIn으로 통합하거나 onSignInResponse 제거
  • 사용자 상태 갱신이 필요하면 해당 로직 추가
core/data/src/main/kotlin/com/acon/core/data/di/ApiModule.kt (2)

10-10: 새 API 타입 import: OK

ProfileApi 도입에 맞는 import 추가 확인했습니다.


79-83: 레거시 프로필 API 프로바이더 확인 — 중복 바인딩 없음

ProfileAuthApiLegacy를 제공하는 바인딩은 core/data/src/main/kotlin/com/acon/core/data/di/ApiModule.kt의 providesProfileApiLegacy 하나뿐입니다. 변경 승인.

feature/settings/src/main/java/com/acon/acon/feature/settings/screen/SettingsViewModel.kt (1)

3-3: SignInStatus로의 전환: OK

도메인 타입 교체( UserType → SignInStatus ) 반영 적절합니다.

feature/profile/src/main/java/com/acon/feature/profile/info/composable/ProfileInfoScreen.kt (1)

121-138: 업로드 탭 전환 시 게스트 가드 확인 필요

이 화면에서는 업로드 탭 클릭 시 즉시 actions.onUploadTabClick만 호출합니다. 게스트의 경우 로그인 유도(바텀시트) 등 상위 컨테이너에서 가드되는지 확인이 필요합니다.

해당 컨테이너(ProfileInfoScreenContainer)에서 SignInStatus로 가드하고 있는지 한번 점검 부탁드립니다.

feature/profile/src/test/kotlin/com/acon/feature/profile/info/viewmodel/ProfileInfoViewModelTest.kt (1)

52-70: 초기화 시나리오 검증 좋음

프로필/저장소트 조합별 성공/실패 상태를 명확히 검증하고 있어 회귀 방지에 유효합니다.

혹시 초기 진입 시 첫 상태(Loading 등)도 한 번 expectState로 스냅샷해 두면 변경 감지에 도움이 됩니다.

Also applies to: 80-90, 100-111

app/src/main/java/com/acon/acon/MainActivity.kt (1)

345-347: Hilt EntryPoint 바인딩 검증 필요 — 로컬 재검증 요청

AuthClientEntryPoint의 @EntryPoint/@Installin(예: ActivityComponent) 스코프와 googleAuthClient를 제공하는 모듈(@Provides/@BINDS) 연결을 확인하세요. 샌드박스에서 검색이 실패해 로컬에서 아래 명령을 실행한 출력 결과를 붙여 주세요.

# repo 루트에서 실행
grep -nR --exclude-dir=build -E "interface\s+AuthClientEntryPoint\b|@EntryPoint|@InstallIn" || true
grep -nR --exclude-dir=build "googleAuthClient\(" || true
grep -nR --exclude-dir=build -E "(Provides|Binds).*GoogleAuthClient" || true
app/src/main/java/com/acon/acon/navigation/nested/ProfileNavigationLegacy.kt (1)

41-43: 검증 필요 — SpotRoute.SpotDetail이 SpotNavigationParameter를 기대합니다

core/navigation/src/main/java/com/acon/acon/core/navigation/route/SpotRoute.kt에서 SpotRoute.SpotDetail은 com.acon.acon.core.model.model.spot.SpotNavigationParameter 타입의 인자(spotNavigationParameter)를 받습니다.

                onNavigateToSpotDetail = {
                    navController.navigate(SpotRoute.SpotDetail(it))
                },
  • 확인: app/src/main/java/com/acon/acon/navigation/nested/ProfileNavigationLegacy.kt(41–43)에서 onNavigateToSpotDetail이 전달하는 'it'의 타입이 com.acon.acon.core.model.model.spot.SpotNavigationParameter인지 확인.
  • 조치: 'it'이 다른 타입이면 SpotNavigationParameter로 변환/매핑하여 전달하거나 SpotRoute.SpotDetail 생성자(또는 호출부)를 해당 타입에 맞게 수정.

레포지토리에서 onNavigateToSpotDetail(또는 ProfileInfoScreenContainer)의 시그니처를 찾지 못했습니다 — 해당 정의(파일/라인)를 제공하면 재검증합니다.

core/data/src/main/kotlin/com/acon/core/data/repository/SpotRepositoryImpl.kt (1)

109-113: SavedSpots 캐시 동기화 — StateFlow 확인 및 조건·오류 처리 필요

  • 검증: ProfileLocalDataSource.getSavedSpots()는 MutableStateFlow.asStateFlow()로 구현되어 있어 .firstOrNull()은 StateFlow의 현재 값을 즉시 반환합니다. (따라서 “firstOrNull이 최신성 보장 안함” 지적은 부정확)
  • 동작 확인 필요: 현재 if (cachedSavedSpots != null) { … } 로 캐시가 null일 때 원격 동기화를 하지 않습니다. 의도 확인 — 항상 서버에서 갱신해야 하면 null 체크 제거, 호출을 줄이려는 의도면 주석으로 명확히 표시.
  • 오류 처리 추가 필요: profileRemoteDataSource.getSavedSpots() 호출을 try/catch 또는 runCatching으로 감싸서 실패 시 로깅/재시도/사용자 알림 등 처리를 추가하세요.
gradle/libs.versions.toml (7)

11-11: 컴포즈 컴파일러 버전 업그레이드 확인

컴포즈 컴파일러 버전이 1.5.1에서 1.5.15로 업그레이드되었습니다. 이는 최신 안정 버전으로의 적절한 업데이트입니다.


21-21: KSP 버전 추가 확인

KSP(Kotlin Symbol Processing) 플러그인이 프로젝트에 새로 추가되었습니다. Kotlin 2.0.21과 호환되는 KSP 1.0.27 버전이 올바르게 설정되었습니다.


83-83: Kotest 테스팅 프레임워크 추가

Kotest 5.9.1이 새로 추가되어 JUnit 기반 테스트를 보완할 수 있게 되었습니다. 최신 안정 버전으로 적절한 선택입니다.


136-141: 테스트 관련 라이브러리 추가

새로운 테스트 관련 라이브러리들이 추가되었습니다:

  • mockk-android: Android 환경 테스트용 모킹
  • orbit-test: Orbit MVI 테스트 지원
  • Kotest 관련 라이브러리들: runner, assertions, property testing

모든 버전이 적절하게 참조되고 있습니다.


242-242: Kotlin JVM 플러그인 버전 참조 수정

jetbrains-kotlin-jvm 플러그인이 이제 kotlin 버전을 올바르게 참조하도록 수정되었습니다. 이는 일관성 있는 Kotlin 버전 관리를 위한 좋은 개선사항입니다.


274-276: 번들 구성 추가

새로운 테스트 관련 번들들이 추가되었습니다:

  • android-test: Android 테스트 라이브러리 번들
  • orbit-test: Orbit 테스트 번들
  • kotest: Kotest 라이브러리 번들

이러한 번들화는 build.gradle 파일에서의 의존성 관리를 단순화합니다.


38-38: Orbit MVI 10.0.0 업그레이드 — 1차 검증 결과

레포 검색 결과 여러 ViewModel에서 ContainerHost / container(...) 사용 및 orbit-test 사용이 확인되었고, Orbit 10.0.0 공식 문서에 ContainerHost 인터페이스·ViewModel용 container() 팩토리·test() API가 10.0.0에 존재하므로 문서 기준 즉시 API 제거로 인한 깨짐은 보이지 않습니다. (orbit-mvi.org)

  • 예시 위치: core/ui/src/main/java/com/acon/acon/core/ui/base/BaseContainerHost.kt, feature/upload/src/main/java/com/acon/acon/feature/upload/screen/UploadPlaceViewModel.kt, feature/profile/src/test/kotlin/com/acon/feature/profile/info/viewmodel/ProfileInfoViewModelTest.kt.
  • 권장 조치: 전체 빌드 및 단위테스트로 컴파일/런타임 호환성 확인(예: ./gradlew build && ./gradlew test).

@ThirFir ThirFir merged commit 80008ee into develop Sep 19, 2025
2 checks passed
@ThirFir ThirFir deleted the refactor/profile-info-presentation branch September 19, 2025 05:06
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.

2 participants