Skip to content

[feat] 내 튜립 화면 compose로 마이그레이션#604

Open
jerry8282 wants to merge 131 commits intodevelopfrom
feature/#576
Open

[feat] 내 튜립 화면 compose로 마이그레이션#604
jerry8282 wants to merge 131 commits intodevelopfrom
feature/#576

Conversation

@jerry8282
Copy link
Contributor

@jerry8282 jerry8282 commented Feb 22, 2026

Issues

✔️ Check-list

  • : Label을 지정해 주세요.
  • : Merge할 브랜치를 확인해 주세요.

🗒️ Work Description

📷 Screenshot

폴더 화면 폴더 추가 및 삭제

  • 폴더를 Long클릭 할 시에 폴더 삭제 화면으로 전환
Screen_recording_20260225_104301.mp4

폴더 화면 Tab조작

Screen_recording_20260225_104324.mp4

폴더 내부 지도 조작

  • 지도 숨김 애니메이션 적용
  • 해당 아이템 클릭시 해당 마커로 이동
Screen_recording_20260225_104433.mp4

폴더 내부 moreOption창 제어

  • 기본 폴더 화면은 이름 변경과 삭제가 안되게 버튼 막기
Screen_recording_20260225_104455.mp4

📚 Reference

Summary by CodeRabbit

릴리스 노트

  • New Features

    • 공유된 튜립 및 멤버 수 관리 기능 추가
    • 개선된 폴더 및 튜립 관리 UI 제공
    • 튜립 상세 화면에서 드래그 앤 드롭으로 장소 순서 변경 가능
    • 새로운 하단 시트를 통한 튜립 이름 변경, 공유, 삭제 기능
  • Chores

    • Android 빌드 도구 및 Google Maps 라이브러리 업데이트

@coderabbitai
Copy link

coderabbitai bot commented Feb 22, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

이번 변경사항은 내 튜립 화면을 Compose로 마이그레이션하고, 멤버 수 및 공유 여부 필드를 데이터 모델에 추가하며, 단일 튜립 조회를 위한 API 엔드포인트를 추가합니다. 새로운 UI 화면, 뷰모델, 컴포넌트들이 도입되었습니다.

Changes

Cohort / File(s) Summary
Build Configuration & Dependencies
android/app/build.gradle.kts, android/gradle/libs.versions.toml
compileSdk를 35에서 36으로 업그레이드하고, Google Maps 의존성을 libs.play.services.maps에서 libs.google.map(maps-compose 8.1.0)으로 변경.
Domain & Data Models
android/app/src/main/java/com/on/turip/domain/turip/Turip.kt, android/app/src/main/java/com/on/turip/data/turip/dto/TuripResponse.kt, android/app/src/main/java/com/on/turip/data/turip/TuripMapper.kt
Turip 데이터 클래스에 memberCount와 isShared 필드를 추가하고, TuripResponse와 매핑 로직을 업데이트하여 모든 응답 타입에서 새 필드를 초기화.
UI Models & Components
android/app/src/main/java/com/on/turip/ui/compose/folder/component/MyTuripModel.kt, android/app/src/main/java/com/on/turip/ui/compose/folder/component/TuripType.kt, android/app/src/main/java/com/on/turip/ui/compose/folder/mapper/MyTuripMapper.kt, android/app/src/main/res/drawable/ic_individual_folder.xml, android/app/src/main/res/drawable/ic_together_folder.xml
튜립 폴더 UI를 위한 새로운 모델과 유형 열거형을 추가하고, 타입별 아이콘(SOLO/TOGETHER)을 매핑하는 확장 함수 정의.
Folder Screen & Components
android/app/src/main/java/com/on/turip/ui/compose/folder/MyTuripScreen.kt, android/app/src/main/java/com/on/turip/ui/compose/folder/MyTuripUiState.kt, android/app/src/main/java/com/on/turip/ui/compose/folder/MyTuripUiEffect.kt, android/app/src/main/java/com/on/turip/ui/compose/folder/MyTuripViewModel.kt, android/app/src/main/java/com/on/turip/ui/compose/folder/component/MyTuripCard.kt, android/app/src/main/java/com/on/turip/ui/compose/folder/component/MyTuripTabRow.kt, android/app/src/main/java/com/on/turip/ui/compose/folder/component/FolderAddBottomSheet.kt
새로운 MyTuripScreen 컴포저블과 관련 UI 상태, 이펙트, 뷰모델을 추가하여 튜립 폴더 관리 기능을 구현 (탭 필터링, 추가/삭제, 모달 시트).
Place Detail Screen & Components
android/app/src/main/java/com/on/turip/ui/compose/favorite/TuripPlaceScreen.kt, android/app/src/main/java/com/on/turip/ui/compose/favorite/TuripPlaceUiState.kt, android/app/src/main/java/com/on/turip/ui/compose/favorite/TuripPlaceViewModel.kt, android/app/src/main/java/com/on/turip/ui/compose/favorite/TuripDetail.kt, android/app/src/main/java/com/on/turip/ui/compose/favorite/component/MoreOptionBottomSheet.kt, android/app/src/main/java/com/on/turip/ui/compose/favorite/component/TuripMapContent.kt
새로운 TuripPlaceScreen 및 관련 컴포넌트들을 추가하여 개별 튜립의 장소들을 표시하고, 드래그-드롭 재정렬, 공유, 삭제 등의 기능 지원.
Repository & Service Layer
android/app/src/main/java/com/on/turip/domain/turip/repository/TuripRepository.kt, android/app/src/main/java/com/on/turip/data/turip/repository/DefaultTuripRepository.kt, android/app/src/main/java/com/on/turip/data/turip/datasource/TuripRemoteDataSource.kt, android/app/src/main/java/com/on/turip/data/turip/datasource/DefaultTuripRemoteDataSource.kt, android/app/src/main/java/com/on/turip/data/turip/service/TuripService.kt
단일 튜립 조회를 위한 loadTurip(turipId) 메서드를 저장소, 데이터소스, 서비스 계층에 추가.
Fragment Refactoring
android/app/src/main/java/com/on/turip/ui/main/favorite/TuripPlaceFragment.kt, android/app/src/main/java/com/on/turip/ui/main/favorite/TuripMapper.kt, android/app/src/main/java/com/on/turip/ui/main/favorite/model/TuripPlaceUiState.kt, android/app/src/main/java/com/on/turip/ui/main/favorite/model/PlaceLatLngUiModel.kt, android/app/src/main/java/com/on/turip/ui/main/favorite/model/TuripPlaceUiEffect.kt
기존 Fragment 기반 UI를 Compose 기반 ComposeView로 마이그레이션하고, 지도 관련 로직과 레거시 바인딩을 제거하며 새로운 UI 이펙트 추가.
Minor Formatting & Resources
android/app/src/main/java/com/on/turip/ui/compose/login/LoginScreen.kt, android/app/src/main/java/com/on/turip/ui/compose/trip/TripDetailScreen.kt, android/app/src/main/java/com/on/turip/ui/compose/trip/component/PlaceItem.kt, android/app/src/main/java/com/on/turip/ui/compose/trip/turipselection/component/TuripPlaceItem.kt, android/app/src/main/res/drawable/ic_people_fill.xml, android/app/src/main/res/drawable/ic_text_area.xml, android/app/src/main/res/values/strings.xml
Compose 수정자 체이닝 포맷팅, 새 드로어블 리소스 추가, UI 텍스트 문자열 추가 (더보기 옵션, 튜립 삭제/추가 메시지).

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant MyTuripScreen
    participant MyTuripViewModel
    participant TuripRepository
    participant TuripService
    participant UI State

    User->>MyTuripScreen: 화면 진입
    MyTuripScreen->>MyTuripViewModel: loadTuripFolders()
    MyTuripViewModel->>TuripRepository: loadTurips()
    TuripRepository->>TuripService: getTurips()
    TuripService-->>TuripRepository: List<TuripResponse>
    TuripRepository->>MyTuripViewModel: List<Turip>
    MyTuripViewModel->>MyTuripViewModel: 도메인 모델을 UI 모델로 변환
    MyTuripViewModel->>UI State: turips 업데이트
    UI State-->>MyTuripScreen: 상태 변경 통지
    MyTuripScreen->>MyTuripScreen: 튜립 목록 렌더링

    User->>MyTuripScreen: 튜립 추가 클릭
    MyTuripScreen->>MyTuripViewModel: showAddBottomSheet()
    MyTuripViewModel->>UI State: showAddBottomSheet = true
    MyTuripScreen->>MyTuripScreen: FolderAddBottomSheet 표시

    User->>MyTuripScreen: 이름 입력 및 확인
    MyTuripScreen->>MyTuripViewModel: updateInputName(), addTurip()
    MyTuripViewModel->>TuripRepository: createTurip()
    TuripRepository->>TuripService: createTurip()
    TuripService-->>TuripRepository: TuripResponse
    TuripRepository-->>MyTuripViewModel: 성공
    MyTuripViewModel->>MyTuripViewModel: TuripAdded 이펙트 방출
    MyTuripViewModel->>MyTuripViewModel: loadTuripFolders() 재호출
    UI State-->>MyTuripScreen: 목록 새로고침
Loading
sequenceDiagram
    actor User
    participant TuripPlaceScreen
    participant TuripPlaceViewModel
    participant TuripRepository
    participant TuripService
    participant TuripDetail
    participant UI State

    User->>TuripPlaceScreen: 튜립 클릭하여 상세화면 진입
    TuripPlaceScreen->>TuripPlaceViewModel: loadSelectedTurip(selectedTuripId)
    TuripPlaceViewModel->>TuripRepository: loadTurip(turipId)
    TuripRepository->>TuripService: getTurip(turipId)
    TuripService-->>TuripRepository: TuripResponse
    TuripRepository-->>TuripPlaceViewModel: Turip 도메인 모델
    
    TuripPlaceScreen->>TuripPlaceViewModel: loadPlaces(selectedTuripId)
    TuripPlaceViewModel->>TuripRepository: loadPlaces(selectedTuripId)
    TuripRepository->>TuripService: getPlaces(turipId)
    TuripService-->>TuripRepository: List<TuripPlaceResponse>
    TuripPlaceViewModel->>TuripPlaceViewModel: UI 모델로 변환
    TuripPlaceViewModel->>UI State: selectedTurip, places, placesLatLng 업데이트
    
    UI State-->>TuripPlaceScreen: 상태 변경 통지
    TuripPlaceScreen->>TuripDetail: places 전달
    TuripDetail->>TuripDetail: 드래그-드롭 가능한 장소 목록 렌더링

    User->>TuripDetail: 장소 드래그하여 순서 변경
    TuripDetail->>TuripPlaceViewModel: onDragPlace(from, to)
    TuripPlaceViewModel->>UI State: 스냅샷으로 상태 업데이트
    
    User->>TuripPlaceScreen: 재정렬 확인
    TuripPlaceViewModel->>TuripRepository: updateTuripPlacesOrder()
    TuripRepository->>TuripService: updateOrder()
    TuripService-->>TuripRepository: 성공 응답
    TuripRepository-->>TuripPlaceViewModel: 완료
    TuripPlaceViewModel->>UI State: 스냅샷 제거, 최종 상태 확정
    
    User->>TuripPlaceScreen: 공유/삭제 옵션 클릭
    TuripPlaceScreen->>TuripPlaceViewModel: shareTurip() 또는 deleteTurip()
    TuripPlaceViewModel->>TuripRepository: 해당 API 호출
    TuripRepository->>TuripService: 요청
    TuripService-->>TuripPlaceViewModel: 응답
    TuripPlaceViewModel->>TuripPlaceViewModel: UI 이펙트 방출
    UI State-->>TuripPlaceScreen: 완료 상태 업데이트
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 Pre-merge checks | ✅ 1 | ❌ 4

❌ Failed checks (2 warnings, 2 inconclusive)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning 대부분의 변경사항이 MyTuripScreen 마이그레이션과 관련된 UI/데이터 구조 변경이나, 일부 변경사항(LoginScreen 포매팅, TripDetailScreen 포매팅, PlaceItem 포매팅)은 마이그레이션 범위 외의 개선사항으로 보입니다. LoginScreen.kt, TripDetailScreen.kt, PlaceItem.kt의 포매팅 변경과 같은 범위 외 변경사항을 제거하거나, 별도 PR로 분리하세요.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Linked Issues check ❓ Inconclusive 연결된 이슈 #576의 TO-DO 섹션이 비어있어 구현 요구사항이 명확하지 않으나, PR의 변경사항(MyTuripScreen Compose 마이그레이션, 관련 UI 컴포넌트 추가, 데이터 처리 로직)이 이슈 제목 'Compose로 마이그레이션'과 일치합니다. 이슈 #576에 세부 구현 요구사항과 수용 기준을 명시하거나, PR에서 구현된 기능이 마이그레이션 목표를 충족하는지 명확히 문서화하세요.
Description check ❓ Inconclusive PR 설명에는 체크리스트와 스크린샷이 포함되어 있으나, 작업 설명과 참고 사항 섹션이 비어있습니다. 🗒️ Work Description 섹션에 구체적인 작업 내용을 작성하고, 📚 Reference 섹션에 관련 참고 자료나 기술 사항을 추가해 주세요.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목 '[feat] 내 튜립 화면 compose로 마이그레이션'은 변경 사항의 주요 내용을 명확하게 요약하고 있습니다. 'MyTuripScreen' 화면이 Jetpack Compose로 마이그레이션된 것이 핵심 변경사항이며, PR 제목이 이를 정확하게 반영하고 있습니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/#576

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

🧹 Nitpick comments (15)
android/gradle/libs.versions.toml (2)

31-31: googleMap 버전 앨리어스의 위치를 # Google / Firebase 섹션으로 이동 권장

googleMap = "8.1.0"# Compose 섹션 아래에 위치하고 있으나, Google Maps는 Compose 전용 라이브러리가 아닌 Google/Firebase 범주에 속합니다. 버전 카탈로그의 일관성을 위해 # Google / Firebase 섹션으로 이동하고, 같은 이유로 Line 106의 google-map 라이브러리 항목도 해당 섹션의 play-services-maps 근처로 옮기는 것이 좋습니다.

♻️ 제안 변경사항
 # Compose
 composeBom = "2026.02.00"
 activityCompose = "1.10.1"
 material = "1.12.0"
-googleMap = "8.1.0"
 # Google / Firebase
 playServicesMaps = "19.2.0"
+googleMap = "8.1.0"
 credentials = "1.5.0"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@android/gradle/libs.versions.toml` at line 31, The googleMap version alias
(googleMap = "8.1.0") is placed under the # Compose section but belongs in the #
Google / Firebase section; move the googleMap alias into the # Google / Firebase
block and also relocate the corresponding library entry named google-map (the
play-services-maps dependency) near play-services-maps in that same section so
the version alias and library declaration are grouped consistently under Google
/ Firebase.

51-51: playServicesMaps 버전과 play-services-maps 라이브러리 제거

build.gradle.kts에서 maps 의존성이 google-map(maps-compose 8.1.0)으로만 사용되고 있습니다. playServicesMaps 버전 앨리어스(Line 51)와 play-services-maps 라이브러리 정의(Line 130)는 다른 모듈에서도 참조되지 않으므로 정리하여도 됩니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@android/gradle/libs.versions.toml` at line 51, Remove the unused
playServicesMaps version alias and the corresponding play-services-maps library
entry from libs.versions.toml: delete the "playServicesMaps" version alias (the
playServicesMaps = "19.2.0" entry) and remove the library definition that
references it (the play-services-maps entry under [libraries]); then
sync/refresh Gradle to ensure no other modules reference these symbols and
everything builds with the existing google-map / maps-compose 8.1.0 dependency.
android/app/src/main/java/com/on/turip/ui/compose/favorite/TuripDetail.kt (4)

224-228: TuripDetailPreviewStatename, turipName 필드가 프리뷰에서 미사용

TuripDetailPreview(Line 289~299)에서 state.namestate.turipName이 전혀 참조되지 않습니다. 향후 TopAppBar 등 타이틀 표시 영역이 프리뷰에 포함될 예정이 아니라면, 두 필드를 제거해 불필요한 유지 비용을 줄일 수 있습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@android/app/src/main/java/com/on/turip/ui/compose/favorite/TuripDetail.kt`
around lines 224 - 228, The TuripDetailPreviewState data class defines unused
fields name and turipName for the preview; update the preview state to remove
those unused fields (delete name and turipName from TuripDetailPreviewState) and
adjust TuripDetailPreview construction/usages to only provide the remaining
places (ImmutableList<TuripPlaceModel>) or add the fields to the preview UI if
you intend to render them later; ensure all references to
TuripDetailPreviewState in TuripDetailPreview and any preview providers are
updated to the new constructor shape.

82-86: fillMaxSize() 중복 — weight(1f)로 충분

Column 내부에서 weight(1f)이 높이 분배를 담당하므로, 앞서 적용된 fillMaxSize()는 height 축에 대해 불필요합니다. fillMaxWidth()는 이미 부모 Column(Line 65)에 적용되어 있어 width도 중복입니다.

♻️ 리팩토링 제안
             EmptyTuripPlaces(
                 modifier =
                     Modifier
-                        .fillMaxSize()
                         .weight(1f),
             )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@android/app/src/main/java/com/on/turip/ui/compose/favorite/TuripDetail.kt`
around lines 82 - 86, The modifier on the composable currently uses
Modifier.fillMaxSize().weight(1f) which is redundant inside a Column (weight
already controls height and parent Column already applies fillMaxWidth()), so
remove fillMaxSize() and keep .weight(1f) (optionally chain .fillMaxWidth() only
if you need explicit width) in the modifier used in TuripDetail (the composable
where Modifier.fillMaxSize().weight(1f) is applied) to avoid unnecessary
full-size expansion.

169-169: animateFloatAsStatelabel 파라미터 추가 권장

label이 없으면 Compose Animation Inspector에서 애니메이션을 식별하기 어렵습니다. 공식 문서의 예시에서도 animateFloatAsState(if (enabled) 1f else 0.5f, label = "alpha")처럼 label을 명시하고 있습니다.

♻️ 리팩토링 제안
-                val elevation by animateFloatAsState(if (isDragging) 8.0f else 0.0f)
+                val elevation by animateFloatAsState(
+                    targetValue = if (isDragging) 8.0f else 0.0f,
+                    label = "dragging elevation",
+                )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@android/app/src/main/java/com/on/turip/ui/compose/favorite/TuripDetail.kt` at
line 169, The call to animateFloatAsState in TuripDetail.kt (val elevation by
animateFloatAsState(if (isDragging) 8.0f else 0.0f)) should include a label so
the Compose Animation Inspector can identify it; update the animateFloatAsState
invocation to pass a descriptive label (e.g., "elevation" or "dragElevation")
while leaving the existing condition on isDragging unchanged so behavior remains
the same.

195-198: verticalArrangement = Arrangement.Center가 실질적으로 무효

Column은 높이를 명시하지 않아 컨텐츠 크기에 맞게 wrap됩니다. Arrangement.Center는 여분의 수직 공간이 있을 때만 동작하므로, 이 경우 효과가 없습니다. 수직 방향 중앙 정렬은 이미 외부 Box(contentAlignment = Alignment.Center)가 담당합니다.

♻️ 리팩토링 제안
         Column(
             horizontalAlignment = Alignment.CenterHorizontally,
-            verticalArrangement = Arrangement.Center,
         ) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@android/app/src/main/java/com/on/turip/ui/compose/favorite/TuripDetail.kt`
around lines 195 - 198, The Column's verticalArrangement = Arrangement.Center is
ineffective because the Column has no explicit height (it wraps content) and
vertical centering is already handled by the outer Box(contentAlignment =
Alignment.Center); remove the verticalArrangement parameter from the Column
(keep horizontalAlignment) to simplify and avoid misleading code, i.e., delete
the verticalArrangement = Arrangement.Center entry in the Column declaration.
android/app/src/main/java/com/on/turip/ui/compose/favorite/TuripPlaceScreen.kt (1)

260-261: TuripMapContentpublic으로 노출되어 있음.

이 파일 내부에서만 사용되는 것으로 보이므로, private으로 변경하여 캡슐화를 개선하는 것을 고려해 주세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@android/app/src/main/java/com/on/turip/ui/compose/favorite/TuripPlaceScreen.kt`
around lines 260 - 261, The TuripMapContent composable is declared public but
appears to be used only within this file; change its declaration from public to
private (make the top-level function private) by adjusting the visibility of
TuripMapContent so it is file-private, and then run a quick project-wide search
for TuripMapContent to ensure no external references must be updated or made
public intentionally before committing.
android/app/src/main/java/com/on/turip/ui/main/favorite/TuripPlaceFragment.kt (2)

34-59: 화면 전환에 매직 넘버 대신 상수 또는 enum 사용 권장.

currentScreen0, 1 같은 매직 넘버를 사용하면 가독성과 유지보수성이 떨어집니다. 또한 when 블록에 else 분기가 없어 예상치 못한 값에 대한 방어가 없습니다.

♻️ enum을 활용한 리팩터링 제안
+private enum class TuripScreen { MY_TURIP, TURIP_PLACE }
+
 setContent {
     TuripTheme {
         var selectedTuripId: Long by rememberSaveable { mutableLongStateOf(0L) }
-        var currentScreen: Int by rememberSaveable { mutableIntStateOf(0) }
+        var currentScreen by rememberSaveable { mutableStateOf(TuripScreen.MY_TURIP) }
 
         when (currentScreen) {
-            0 -> {
+            TuripScreen.MY_TURIP -> {
                 MyTuripScreen(
                     onNavigateToTuripPlace = { newId ->
                         selectedTuripId = newId
-                        currentScreen = 1
+                        currentScreen = TuripScreen.TURIP_PLACE
                     },
                     onNavigateToLogin = ::navigateToLoginScreen,
                 )
             }
 
-            1 -> {
+            TuripScreen.TURIP_PLACE -> {
                 TuripPlaceScreen(
                     onNavigateToLogin = ::navigateToLoginScreen,
                     onShareTurip = ::navigateToShareTurip,
                     selectedTuripId = selectedTuripId,
                     onNavigateToMap = {},
                     goBack = {
-                        currentScreen = 0
+                        currentScreen = TuripScreen.MY_TURIP
                     },
                 )
             }
         }
     }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@android/app/src/main/java/com/on/turip/ui/main/favorite/TuripPlaceFragment.kt`
around lines 34 - 59, Replace the magic-number screen state with a typed
constant/enum and add a defensive branch: create a Screen enum (e.g.,
Screen.LIST, Screen.DETAIL) and change currentScreen from "var currentScreen:
Int by rememberSaveable { mutableIntStateOf(0) }" to a rememberSaveable
enum-backed state (or use mutableStateOf(Screen.LIST) with a Saver) and update
the when to "when (currentScreen) { Screen.LIST -> MyTuripScreen(...),
Screen.DETAIL -> TuripPlaceScreen(...), else -> /* fallback e.g., set
currentScreen = Screen.LIST or show error */ }", update assignments where you
set currentScreen = 1 to use Screen.DETAIL and where you set currentScreen = 0
to use Screen.LIST, and keep existing callbacks (selectedTuripId, MyTuripScreen,
TuripPlaceScreen, navigateToLoginScreen, navigateToShareTurip) unchanged.

73-103: createShareIntent 로직이 TripDetailActivity.kt와 중복됨.

관련 코드 스니펫(TripDetailActivity.kt, Line 84-95)에 동일한 createShareIntent 함수가 존재합니다. 공통 유틸리티로 추출하면 코드 중복을 줄일 수 있습니다. 당장 필수는 아니지만 향후 유지보수를 위해 참고해 주세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@android/app/src/main/java/com/on/turip/ui/main/favorite/TuripPlaceFragment.kt`
around lines 73 - 103, The createShareIntent implementation is duplicated
between TuripPlaceFragment (createShareIntent and navigateToShareTurip) and
TripDetailActivity; extract that logic into a shared utility (e.g., a
singleton/object or top-level function like ShareUtils.createShareIntent or
createShareIntent in a shared utils file) preserving the signature (text:
String, packageName: String? = null) and behavior (ACTION_SEND, type
"text/plain", putExtra EXTRA_TEXT, set `package` when packageName != null), then
replace the local createShareIntent definitions in TuripPlaceFragment and
TripDetailActivity to call the new shared function from both
navigateToShareTurip and the TripDetailActivity share code so duplication is
removed.
android/app/src/main/java/com/on/turip/ui/compose/favorite/TuripPlaceViewModel.kt (2)

289-295: emptyList().toImmutableList() 대신 persistentListOf() 사용 권장.

불필요한 래핑 없이 더 관용적인 표현입니다.

♻️ 개선 제안
             val EMPTY =
                 DeleteTuripPlaceSnapshot(
                     deletePlaceId = NOT_INITIALIZED,
-                    originPlaces = emptyList<TuripPlaceModel>().toImmutableList(),
-                    originPlacesLatLng = emptyList<PlaceLatLngUiModel>().toImmutableList(),
+                    originPlaces = persistentListOf(),
+                    originPlacesLatLng = persistentListOf(),
                 )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@android/app/src/main/java/com/on/turip/ui/compose/favorite/TuripPlaceViewModel.kt`
around lines 289 - 295, The companion object initializer for
DeleteTuripPlaceSnapshot (EMPTY) uses emptyList<T>().toImmutableList(); replace
those with persistentListOf() to avoid unnecessary wrapping—e.g., set
originPlaces = persistentListOf<TuripPlaceModel>() and originPlacesLatLng =
persistentListOf<PlaceLatLngUiModel>() while keeping deletePlaceId =
NOT_INITIALIZED; update the EMPTY constant accordingly.

216-232: _uiState.update {} 람다 내부에서 외부 상태 변경은 부작용(side effect)을 유발할 수 있음.

MutableStateFlow.update의 람다는 CAS 실패 시 재실행될 수 있습니다. 현재 deleteTuripPlaceSnapshot 할당이 람다 내부에서 수행되고 있어 이론적으로 중복 실행 가능성이 있습니다. Main 디스패처에서 경합이 발생할 가능성은 낮지만, 스냅샷을 먼저 저장한 후 update를 호출하는 것이 더 안전합니다.

♻️ 부작용 분리 제안
 private fun applyTuripPlaceDelete(placeId: Long) {
     if (deleteTuripPlaceSnapshot.hasSnapshot()) return
+    val currentState = _uiState.value
+    deleteTuripPlaceSnapshot = DeleteTuripPlaceSnapshot(
+        deletePlaceId = placeId,
+        originPlaces = currentState.places,
+        originPlacesLatLng = currentState.placesLatLng,
+    )
     _uiState.update { state ->
-        deleteTuripPlaceSnapshot =
-            DeleteTuripPlaceSnapshot(
-                deletePlaceId = placeId,
-                originPlaces = state.places,
-                originPlacesLatLng = state.placesLatLng,
-            )
         state.copy(
             places = state.places.filter { it.placeId != placeId }.toImmutableList(),
             placesLatLng =
                 state.placesLatLng
                     .filter { it.placeId != placeId }
                     .toImmutableList(),
         )
     }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@android/app/src/main/java/com/on/turip/ui/compose/favorite/TuripPlaceViewModel.kt`
around lines 216 - 232, The assignment to deleteTuripPlaceSnapshot must be moved
out of the _uiState.update lambda to avoid side effects on CAS retries; in
applyTuripPlaceDelete, first read the current state (via _uiState.value),
construct and assign DeleteTuripPlaceSnapshot(deletePlaceId = placeId,
originPlaces = state.places, originPlacesLatLng = state.placesLatLng) to
deleteTuripPlaceSnapshot, then call _uiState.update { state -> state.copy(...) }
to remove the place (filtering places and placesLatLng) so the update lambda has
no external side effects.
android/app/src/main/java/com/on/turip/ui/compose/folder/component/MyTuripModel.kt (1)

3-9: @Immutable 어노테이션 누락 — PlaceLatLngUiModel과 불일치

같은 PR에서 PlaceLatLngUiModel에는 @Immutable이 적용되어 있지만 MyTuripModel에는 없습니다. 모든 프로퍼티(Long, String, Int, TuripType enum)가 안정적 타입이므로 같은 모듈 내에서는 컴파일러가 안정성을 추론할 수 있지만, 명시적으로 @Immutable을 선언하면 Compose 안정성 계약을 코드 레벨에서 명확히 전달할 수 있습니다.

♻️ 제안하는 수정
+import androidx.compose.runtime.Immutable
+
+@Immutable
 data class MyTuripModel(
     val id: Long,
     val name: String,
     val type: TuripType,
     val memberCount: Int = 0,
     val placeCount: Int = 0,
 )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@android/app/src/main/java/com/on/turip/ui/compose/folder/component/MyTuripModel.kt`
around lines 3 - 9, MyTuripModel 데이터 클래스에 Compose 안정성 계약을 명시적으로 적용하기 위해 클래스 선언에
`@Immutable` 어노테이션을 추가하고 필요한 경우 androidx.compose.runtime.Immutable을 임포트하세요;
MyTuripModel(id: Long, name: String, type: TuripType, memberCount: Int = 0,
placeCount: Int = 0) 모든 프로퍼티가 안정적 타입이므로 `@Immutable을` 붙이면 PlaceLatLngUiModel과 일관된
컴포저블 안정성 표현을 제공합니다.
android/app/src/main/java/com/on/turip/ui/compose/folder/component/MyTuripTabRow.kt (1)

40-68: TabRow 대신 Row 사용 고려 (선택)

indicator = {}, divider = {}로 Material3 TabRow의 모든 기본 동작을 비활성화하고 있습니다. 실질적으로 균등 너비 배분만 사용하므로, 불필요한 TabRow 의존성 없이 각 탭에 Modifier.weight(1f)를 적용한 Row로 동일한 레이아웃을 표현하면 의도가 더 명확해집니다.

♻️ 제안하는 대안
-        TabRow(
-            selectedTabIndex = selectedTabIndex,
-            containerColor = TuripTheme.colors.gray01,
-            contentColor = TuripTheme.colors.black,
-            indicator = {},
-            divider = {},
-        ) {
-            MyTuripTab.entries.forEach { myTuripTab ->
+        Row(
+            modifier = Modifier
+                .fillMaxWidth()
+                .background(TuripTheme.colors.gray01),
+        ) {
+            MyTuripTab.entries.forEach { myTuripTab ->
                 val isSelected = selectedTab == myTuripTab
 
                 Box(
                     modifier =
                         Modifier
+                            .weight(1f)
                             .padding(TuripTheme.spacing.extraSmall)
                             ...
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@android/app/src/main/java/com/on/turip/ui/compose/folder/component/MyTuripTabRow.kt`
around lines 40 - 68, The TabRow is being used only for equal-width layout while
its indicator/divider are disabled; replace TabRow with a plain Row and give
each tab Box a Modifier.weight(1f) to distribute space evenly, keeping the
current selection logic (selectedTab, selectedTabIndex), click handler
(onTabSelected), and styling from MyTuripTab iteration—remove TabRow and apply
Modifier.weight(1f) to the Box (or its immediate child) so layout remains
identical without depending on TabRow.
android/app/src/main/java/com/on/turip/ui/compose/folder/component/MyTuripCard.kt (1)

109-130: TuripTypeChip의 색상 및 문자열 하드코딩

두 가지 문제가 있습니다.

  1. 하드코딩 색상: Color(0xFFD4E157), Color(0xFF90CAF9) 대신 TuripTheme.colors의 디자인 토큰을 사용하면 테마 일관성이 유지됩니다.
  2. 하드코딩 문자열: "함께 튜립", "나홀로 튜립"strings.xml 리소스로 분리해 i18n 지원과 문자열 관리를 개선하세요.
♻️ 제안하는 수정 방향
// strings.xml에 추가
// <string name="turip_type_together">함께 튜립</string>
// <string name="turip_type_solo">나홀로 튜립</string>

// TuripTheme.colors에 타입별 색상 토큰 추가 권장 (예시)
// val turipTogether: Color
// val turipSolo: Color

 val (label: String, backgroundColor: Color) =
     when (type) {
-        TuripType.TOGETHER -> "함께 튜립" to Color(0xFFD4E157)
-        TuripType.SOLO -> "나홀로 튜립" to Color(0xFF90CAF9)
+        TuripType.TOGETHER -> stringResource(R.string.turip_type_together) to TuripTheme.colors.turipTogether
+        TuripType.SOLO -> stringResource(R.string.turip_type_solo) to TuripTheme.colors.turipSolo
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@android/app/src/main/java/com/on/turip/ui/compose/folder/component/MyTuripCard.kt`
around lines 109 - 130, Replace the hardcoded label strings and Color literals
in the destructuring assignment inside MyTuripCard (the val (label,
backgroundColor) = when(type) { ... }) with resources and theme tokens: use
stringResource(R.string.*) for "함께 튜립"/"나홀로 튜립" and replace
Color(0xFFD4E157)/Color(0xFF90CAF9) with the appropriate TuripTheme.colors
design tokens (e.g., TuripTheme.colors.primary/secondary or the semantic token
that fits) so the Box background uses theme colors and the Text uses localized
strings.
android/app/src/main/java/com/on/turip/ui/compose/folder/FolderUiState.kt (1)

8-21: @Immutable 어노테이션 추가를 권장합니다

FolderUiState의 모든 필드가 이미 Compose 컴파일러에 의해 안정적으로 인식됩니다:

  • isLoading: Boolean — 기본 타입 (안정)
  • errorUiState: ErrorUiState — sealed interface (안정)
  • turips: ImmutableList<MyTuripModel> — ImmutableList 안정, MyTuripModel은 모든 필드가 안정 타입 (Long, String, Int, TuripType enum)

@Immutable 어노테이션을 추가하면 이러한 안정성을 명시적으로 문서화하고, 컴파일러가 모든 참조 타입의 안정성을 함께 검증하여 Compose 스킵 최적화를 명확히 보장받을 수 있습니다.

♻️ 제안하는 수정
+import androidx.compose.runtime.Immutable
+
+@Immutable
 data class FolderUiState(
     val isLoading: Boolean,
     val errorUiState: ErrorUiState,
     val turips: ImmutableList<MyTuripModel>,
 ) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@android/app/src/main/java/com/on/turip/ui/compose/folder/FolderUiState.kt`
around lines 8 - 21, Add the `@Immutable` annotation to the FolderUiState data
class to explicitly declare it as Compose-stable: annotate FolderUiState with
`@Immutable` (and add the required import for androidx.compose.runtime.Immutable)
so the Compose compiler can verify and rely on the stability of FolderUiState,
its properties (isLoading, errorUiState, turips) and enable skip optimizations;
ensure the annotation is placed immediately above the FolderUiState declaration.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@android/app/src/main/java/com/on/turip/data/turip/dto/TuripResponse.kt`:
- Around line 18-21: The TuripResponse data class properties memberCount and
isShared currently have no defaults and will cause kotlinx.serialization
MissingFieldException if the backend omits them; update the TuripResponse
declaration to provide safe defaults (e.g., memberCount = 0 and isShared =
false) or make them nullable with default null so deserialization succeeds when
those fields are absent; locate the properties named memberCount and isShared in
TuripResponse and add the chosen default values.

In `@android/app/src/main/java/com/on/turip/ui/compose/favorite/TuripDetail.kt`:
- Around line 104-121: The Row in TuripDetail.kt is missing layout modifiers so
the IconButton isn’t aligned to the trailing edge; update the Row to use
Modifier.fillMaxWidth() and give the Text a Modifier.weight(1f) (or equivalent
spacer) so the Text expands and pushes the IconButton (onShareClick /
IconButton) to the end; locate the Row that wraps Text and IconButton and add
these modifiers to fix trailing alignment.

In
`@android/app/src/main/java/com/on/turip/ui/compose/favorite/TuripPlaceScreen.kt`:
- Around line 275-295: The LaunchedEffect block that recenters the map doesn't
guard against an empty places list, which causes LatLngBounds.Builder().build()
to throw when places is empty; update the LaunchedEffect in
TuripPlaceScreen/TuripMapContent to handle places.isEmpty() (e.g., early return
or a dedicated case for 0) before calling LatLngBounds.Builder().build(),
leaving the existing behaviors for size == 1 and the multi-place branch
unchanged and still using cameraPositionState.animate for valid bounds.
- Around line 339-341: Replace the current use of
AnimatedVisibilityScope.transition.isRunning with a MutableTransitionState-based
visible state: create a remember { MutableTransitionState(initialVisibleValue) }
(e.g., visibleState), pass it into AnimatedVisibility(visibleState =
visibleState, ...), and change the LaunchedEffect to watch
visibleState.currentState and visibleState.isIdle; call
onAnimationFinished(isMapVisible) when visibleState.currentState is false and
visibleState.isIdle (this ensures you detect exit animation completion instead
of using transition.isRunning).

In
`@android/app/src/main/java/com/on/turip/ui/compose/favorite/TuripPlaceUiState.kt`:
- Around line 11-17: The ui state never sets selectedTuripName so sharing reads
an empty string; in TuripPlaceViewModel.loadPlaces() update the state.copy(...)
to set selectedTuripName (pull the name from the loadPlaces parameter if you add
one, or extract it from the API response / MyTuripModel) so
TuripPlaceUiState.selectedTuripName holds the real name; also ensure
TuripPlaceScreen is passed the turip name (not just selectedTuripId) or that
loadPlaces accepts a name argument and uses it to populate selectedTuripName
before shareTurip() reads uiState.value.selectedTuripName.

In
`@android/app/src/main/java/com/on/turip/ui/compose/favorite/TuripPlaceViewModel.kt`:
- Around line 51-80: loadPlaces updates selectedTuripId but never sets
selectedTuripName, so the UI state used by shareTurip (shareTurip) ends up with
an empty name; update TuripPlaceViewModel.loadPlaces to populate
selectedTuripName when updating state: either add a selectedTuripName parameter
to loadPlaces and set state.copy(selectedTuripId = selectedTuripId,
selectedTuripName = selectedTuripName, ...), or fetch the turip name inside
loadPlaces (e.g., call the repository to obtain the name) and set it in
state.copy; ensure the chosen approach updates the same UiState field name
(selectedTuripName) so shareTurip reads the correct value.

In
`@android/app/src/main/java/com/on/turip/ui/compose/folder/component/MyTuripCard.kt`:
- Around line 55-62: MyTuripCard의 Image 호출에서 현재 사용 중인 기본 런처 에셋
R.drawable.ic_launcher_background를 실제 카드 이미지 리소스(예:
R.drawable.turip_card_placeholder 또는 전달받는 이미지 리소스 파라미터)로 교체하고,
contentDescription이 null인 부분을 카드 내용을 설명하는 문자열로 채우세요(예: contentDescription =
"Turip 카드 이미지" 혹은 함수 파라미터로 설명을 받아 설정). 변경 대상 식별자: Image(…),
painterResource(R.drawable.ic_launcher_background), contentDescription 파라미터.

In `@android/app/src/main/java/com/on/turip/ui/compose/folder/FolderScreen.kt`:
- Line 66: The snackbarHostState created in MyTuripScreen is never passed into
MyTuripScreenContent, so the Scaffold inside MyTuripScreenContent never uses it
and the snackbar never renders; update the MyTuripScreenContent signature to
accept a SnackbarHostState parameter (e.g., snackbarHostState:
SnackbarHostState), pass the existing snackbarHostState from MyTuripScreen when
calling MyTuripScreenContent, and wire it into the Scaffold by providing
snackbarHost = { SnackbarHost(hostState = snackbarHostState) } so that calls to
snackbarHostState.showSnackbarWithAction() display correctly.
- Line 1: The package declaration in FolderScreen.kt ("package
com.on.turip.ui.compose.myturip") does not match its folder path (…/folder/) and
the main composable name MyTuripScreen; fix by either moving the file into the
myturip directory and renaming FolderScreen.kt -> MyTuripScreen.kt to match the
package and composable, or update the package line to
"com.on.turip.ui.compose.folder" (and optionally rename the file to
FolderScreen.kt -> keep or change as needed) so the package declaration,
directory structure, and symbol names (FolderScreen.kt, MyTuripScreen, package
declaration) are consistent.
- Around line 130-133: The FloatingActionButton Icon in FolderScreen.kt uses
Icons.Default.Add with contentDescription = null which breaks accessibility;
replace the null with a localized string resource (e.g., R.string.add_folder) by
adding an appropriate entry to strings.xml and passing it via
stringResource(R.string.add_folder) as the contentDescription for the Icon
(ensure you import androidx.compose.ui.res.stringResource and update the Icon
call where Icons.Default.Add is used).
- Around line 144-153: Replace the hardcoded label and direct Color usage in the
Text composable inside FolderScreen: use stringResource(R.string.my_turip)
instead of the literal "내 튜립", and replace Color.Black with the theme color
TuripTheme.colors.black so the Text call (in FolderScreen/its composable
function) uses localized resources and the project's theme color.
- Line 122: FolderScreen.kt uses contentWindowInsets = WindowInsets(0, 0, 0, 0)
to avoid double-padding because enableEdgeToEdge() is handled at the Activity
level, but HomeScreen.kt uses contentWindowInsets = WindowInsets() causing
inconsistency; make the inset policy consistent by updating HomeScreen.kt (the
Scaffold usage where contentWindowInsets is set) to use contentWindowInsets =
WindowInsets(0, 0, 0, 0) so both FolderScreen (contentWindowInsets =
WindowInsets(0, 0, 0, 0)) and HomeScreen follow the same Activity-level inset
handling.

In `@android/app/src/main/java/com/on/turip/ui/compose/folder/FolderViewModel.kt`:
- Around line 40-48: loadTuripFolders currently doesn't set
FolderUiState.isLoading or handle failures; update it to set isLoading = true
before calling turipRepository.loadTurips(), handle both onSuccess (update
turips via .map { it.toUiModel() } and set isLoading = false) and onFailure (set
isLoading = false, update errorUiState and emit FolderUiEffect.ShowError), or
use a try/catch/finally around the repository call to ensure isLoading is reset
in all cases and errors are converted into the errorUiState/FolderUiEffect;
reference loadTuripFolders, FolderUiState.isLoading, errorUiState,
FolderUiEffect.ShowError and turipRepository.loadTurips() when making the
changes.

---

Nitpick comments:
In `@android/app/src/main/java/com/on/turip/ui/compose/favorite/TuripDetail.kt`:
- Around line 224-228: The TuripDetailPreviewState data class defines unused
fields name and turipName for the preview; update the preview state to remove
those unused fields (delete name and turipName from TuripDetailPreviewState) and
adjust TuripDetailPreview construction/usages to only provide the remaining
places (ImmutableList<TuripPlaceModel>) or add the fields to the preview UI if
you intend to render them later; ensure all references to
TuripDetailPreviewState in TuripDetailPreview and any preview providers are
updated to the new constructor shape.
- Around line 82-86: The modifier on the composable currently uses
Modifier.fillMaxSize().weight(1f) which is redundant inside a Column (weight
already controls height and parent Column already applies fillMaxWidth()), so
remove fillMaxSize() and keep .weight(1f) (optionally chain .fillMaxWidth() only
if you need explicit width) in the modifier used in TuripDetail (the composable
where Modifier.fillMaxSize().weight(1f) is applied) to avoid unnecessary
full-size expansion.
- Line 169: The call to animateFloatAsState in TuripDetail.kt (val elevation by
animateFloatAsState(if (isDragging) 8.0f else 0.0f)) should include a label so
the Compose Animation Inspector can identify it; update the animateFloatAsState
invocation to pass a descriptive label (e.g., "elevation" or "dragElevation")
while leaving the existing condition on isDragging unchanged so behavior remains
the same.
- Around line 195-198: The Column's verticalArrangement = Arrangement.Center is
ineffective because the Column has no explicit height (it wraps content) and
vertical centering is already handled by the outer Box(contentAlignment =
Alignment.Center); remove the verticalArrangement parameter from the Column
(keep horizontalAlignment) to simplify and avoid misleading code, i.e., delete
the verticalArrangement = Arrangement.Center entry in the Column declaration.

In
`@android/app/src/main/java/com/on/turip/ui/compose/favorite/TuripPlaceScreen.kt`:
- Around line 260-261: The TuripMapContent composable is declared public but
appears to be used only within this file; change its declaration from public to
private (make the top-level function private) by adjusting the visibility of
TuripMapContent so it is file-private, and then run a quick project-wide search
for TuripMapContent to ensure no external references must be updated or made
public intentionally before committing.

In
`@android/app/src/main/java/com/on/turip/ui/compose/favorite/TuripPlaceViewModel.kt`:
- Around line 289-295: The companion object initializer for
DeleteTuripPlaceSnapshot (EMPTY) uses emptyList<T>().toImmutableList(); replace
those with persistentListOf() to avoid unnecessary wrapping—e.g., set
originPlaces = persistentListOf<TuripPlaceModel>() and originPlacesLatLng =
persistentListOf<PlaceLatLngUiModel>() while keeping deletePlaceId =
NOT_INITIALIZED; update the EMPTY constant accordingly.
- Around line 216-232: The assignment to deleteTuripPlaceSnapshot must be moved
out of the _uiState.update lambda to avoid side effects on CAS retries; in
applyTuripPlaceDelete, first read the current state (via _uiState.value),
construct and assign DeleteTuripPlaceSnapshot(deletePlaceId = placeId,
originPlaces = state.places, originPlacesLatLng = state.placesLatLng) to
deleteTuripPlaceSnapshot, then call _uiState.update { state -> state.copy(...) }
to remove the place (filtering places and placesLatLng) so the update lambda has
no external side effects.

In
`@android/app/src/main/java/com/on/turip/ui/compose/folder/component/MyTuripCard.kt`:
- Around line 109-130: Replace the hardcoded label strings and Color literals in
the destructuring assignment inside MyTuripCard (the val (label,
backgroundColor) = when(type) { ... }) with resources and theme tokens: use
stringResource(R.string.*) for "함께 튜립"/"나홀로 튜립" and replace
Color(0xFFD4E157)/Color(0xFF90CAF9) with the appropriate TuripTheme.colors
design tokens (e.g., TuripTheme.colors.primary/secondary or the semantic token
that fits) so the Box background uses theme colors and the Text uses localized
strings.

In
`@android/app/src/main/java/com/on/turip/ui/compose/folder/component/MyTuripModel.kt`:
- Around line 3-9: MyTuripModel 데이터 클래스에 Compose 안정성 계약을 명시적으로 적용하기 위해 클래스 선언에
`@Immutable` 어노테이션을 추가하고 필요한 경우 androidx.compose.runtime.Immutable을 임포트하세요;
MyTuripModel(id: Long, name: String, type: TuripType, memberCount: Int = 0,
placeCount: Int = 0) 모든 프로퍼티가 안정적 타입이므로 `@Immutable을` 붙이면 PlaceLatLngUiModel과 일관된
컴포저블 안정성 표현을 제공합니다.

In
`@android/app/src/main/java/com/on/turip/ui/compose/folder/component/MyTuripTabRow.kt`:
- Around line 40-68: The TabRow is being used only for equal-width layout while
its indicator/divider are disabled; replace TabRow with a plain Row and give
each tab Box a Modifier.weight(1f) to distribute space evenly, keeping the
current selection logic (selectedTab, selectedTabIndex), click handler
(onTabSelected), and styling from MyTuripTab iteration—remove TabRow and apply
Modifier.weight(1f) to the Box (or its immediate child) so layout remains
identical without depending on TabRow.

In `@android/app/src/main/java/com/on/turip/ui/compose/folder/FolderUiState.kt`:
- Around line 8-21: Add the `@Immutable` annotation to the FolderUiState data
class to explicitly declare it as Compose-stable: annotate FolderUiState with
`@Immutable` (and add the required import for androidx.compose.runtime.Immutable)
so the Compose compiler can verify and rely on the stability of FolderUiState,
its properties (isLoading, errorUiState, turips) and enable skip optimizations;
ensure the annotation is placed immediately above the FolderUiState declaration.

In
`@android/app/src/main/java/com/on/turip/ui/main/favorite/TuripPlaceFragment.kt`:
- Around line 34-59: Replace the magic-number screen state with a typed
constant/enum and add a defensive branch: create a Screen enum (e.g.,
Screen.LIST, Screen.DETAIL) and change currentScreen from "var currentScreen:
Int by rememberSaveable { mutableIntStateOf(0) }" to a rememberSaveable
enum-backed state (or use mutableStateOf(Screen.LIST) with a Saver) and update
the when to "when (currentScreen) { Screen.LIST -> MyTuripScreen(...),
Screen.DETAIL -> TuripPlaceScreen(...), else -> /* fallback e.g., set
currentScreen = Screen.LIST or show error */ }", update assignments where you
set currentScreen = 1 to use Screen.DETAIL and where you set currentScreen = 0
to use Screen.LIST, and keep existing callbacks (selectedTuripId, MyTuripScreen,
TuripPlaceScreen, navigateToLoginScreen, navigateToShareTurip) unchanged.
- Around line 73-103: The createShareIntent implementation is duplicated between
TuripPlaceFragment (createShareIntent and navigateToShareTurip) and
TripDetailActivity; extract that logic into a shared utility (e.g., a
singleton/object or top-level function like ShareUtils.createShareIntent or
createShareIntent in a shared utils file) preserving the signature (text:
String, packageName: String? = null) and behavior (ACTION_SEND, type
"text/plain", putExtra EXTRA_TEXT, set `package` when packageName != null), then
replace the local createShareIntent definitions in TuripPlaceFragment and
TripDetailActivity to call the new shared function from both
navigateToShareTurip and the TripDetailActivity share code so duplication is
removed.

In `@android/gradle/libs.versions.toml`:
- Line 31: The googleMap version alias (googleMap = "8.1.0") is placed under the
# Compose section but belongs in the # Google / Firebase section; move the
googleMap alias into the # Google / Firebase block and also relocate the
corresponding library entry named google-map (the play-services-maps dependency)
near play-services-maps in that same section so the version alias and library
declaration are grouped consistently under Google / Firebase.
- Line 51: Remove the unused playServicesMaps version alias and the
corresponding play-services-maps library entry from libs.versions.toml: delete
the "playServicesMaps" version alias (the playServicesMaps = "19.2.0" entry) and
remove the library definition that references it (the play-services-maps entry
under [libraries]); then sync/refresh Gradle to ensure no other modules
reference these symbols and everything builds with the existing google-map /
maps-compose 8.1.0 dependency.

Comment on lines 104 to 121
Row {
Text(
text = stringResource(R.string.all_total_place_count, places.size),
style = TuripTheme.typography.info1,
color = TuripTheme.colors.gray03,
modifier = Modifier.padding(vertical = TuripTheme.spacing.medium),
)

IconButton(
onClick = onShareClick,
) {
Icon(
painter = painterResource(R.drawable.ic_share),
contentDescription = stringResource(R.string.all_share_description),
tint = TuripTheme.colors.gray03,
)
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

공유 버튼이 Row 끝에 정렬되지 않음

RowModifier.fillMaxWidth()가 없고 TextModifier.weight(1f)가 적용되어 있지 않아, IconButton이 텍스트 바로 오른쪽에 붙어서 표시됩니다. 일반적으로 공유 버튼은 Row의 끝(trailing) 위치에 배치됩니다.

🐛 레이아웃 수정 제안
-        Row {
+        Row(modifier = Modifier.fillMaxWidth()) {
             Text(
                 text = stringResource(R.string.all_total_place_count, places.size),
                 style = TuripTheme.typography.info1,
                 color = TuripTheme.colors.gray03,
-                modifier = Modifier.padding(vertical = TuripTheme.spacing.medium),
+                modifier = Modifier
+                    .weight(1f)
+                    .padding(vertical = TuripTheme.spacing.medium),
             )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@android/app/src/main/java/com/on/turip/ui/compose/favorite/TuripDetail.kt`
around lines 104 - 121, The Row in TuripDetail.kt is missing layout modifiers so
the IconButton isn’t aligned to the trailing edge; update the Row to use
Modifier.fillMaxWidth() and give the Text a Modifier.weight(1f) (or equivalent
spacer) so the Text expands and pushes the IconButton (onShareClick /
IconButton) to the end; locate the Row that wraps Text and IconButton and add
these modifiers to fix trailing alignment.

Comment on lines 339 to 341
LaunchedEffect(transition.isRunning) {
if (!transition.isRunning) onAnimationFinished(isMapVisible)
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

Jetpack Compose AnimatedVisibility LaunchedEffect timing exit animation

💡 Result:

LaunchedEffect runs when its composable enters the composition and is cancelled when that composable leaves (e.g., when AnimatedVisibility finally removes its content after the exit finishes). So putting a LaunchedEffect inside AnimatedVisibility { ... } won’t reliably “run after exit”—it will be cancelled at removal time. [2][1]

How to trigger work after the exit animation ends

Use a MutableTransitionState and wait for the transition to become idle (isIdle == true) while the target is hidden:

`@Composable`
fun HideWithExitThen(onHidden: () -> Unit, visible: Boolean) {
    val vis = remember { MutableTransitionState(false) }

    // drive visibility
    LaunchedEffect(visible) { vis.targetState = visible }

    // run after exit finishes (content is about to be removed)
    LaunchedEffect(vis) {
        snapshotFlow { Triple(vis.currentState, vis.targetState, vis.isIdle) }
            .collect { (current, target, idle) ->
                if (idle && current == false && target == false) onHidden()
            }
    }

    AnimatedVisibility(
        visibleState = vis,
        enter = fadeIn(),
        exit = fadeOut(),
    ) { /* content */ }
}

Why this works:

  • AnimatedVisibility keeps its content until exit animations finish, then removes it. [1][5]
  • MutableTransitionState.isIdle tells you when the transition has finished running. [3]

Notes:

  • If you use ExitTransition.None (and don’t run any exit animations in AnimatedVisibilityScope.transition), the content can be removed immediately, so there may be nothing to “wait for.” [4]
  • If you have custom exit animations, prefer adding them via AnimatedVisibilityScope.transition so AnimatedVisibility will wait for them too. [1][5]

Sources: Android Developers docs on AnimatedVisibility behavior and waiting rules [1][5], LaunchedEffect lifecycle [2], MutableTransitionState.isIdle [3], and ExitTransition.None removal note [4].


🌐 Web query:

Compose AnimatedVisibilityScope transition.isRunning exit animation content removal

💡 Result:

AnimatedVisibilityScope.transition is the one transition AnimatedVisibility tracks to decide when it’s safe to remove content.

  • transition.isRunning == true while the AnimatedVisibility enter/exit is animating and while any custom animations you attach to that same transition (via transition.animate*{}) are still running. AnimatedVisibility waits for all of those to finish before considering itself idle. [1][2]
  • After an exit, AnimatedVisibility removes its content only after it becomes idle (i.e., the transition finishes). [1][2]
  • If you run an “exit animation” outside that transition (e.g., animate*AsState, an Animatable you launch separately, etc.), AnimatedVisibility can’t “see” it and may remove the content before that animation finishes. [1][2]
  • If exit = ExitTransition.None and nothing is animating in the provided transition scope, the content can be removed immediately. [3]

Minimal pattern (custom exit tied to the tracked transition):

val visibleState = remember { MutableTransitionState(true) }

AnimatedVisibility(
    visibleState = visibleState,
    enter = fadeIn(),
    exit = fadeOut()
) { // this: AnimatedVisibilityScope
    val alpha by transition.animateFloat(label = "alpha") { s ->
        if (s == EnterExitState.Visible) 1f else 0f
    }
    Box(Modifier.graphicsLayer { this.alpha = alpha }) {
        Content()
    }
}

// optional: know when exit+removal point is reached
LaunchedEffect(visibleState.isIdle, visibleState.targetState) {
    if (visibleState.isIdle && !visibleState.targetState) {
        // exit finished; AnimatedVisibility is now idle and will remove/has removed content
    }
}

Sources: Android Developers API/docs for AnimatedVisibilityScope, AnimatedVisibility, and ExitTransition. [1][2][3]


🌐 Web query:

Jetpack Compose AnimatedVisibility content lambda execution after exit animation

💡 Result:

AnimatedVisibility keeps its content in the composition while the exit transition is running. That means when you set visible = false, the item is still composed (and can recompose), so the content lambda may execute during/after the moment you triggered the exit, until the exit animation (and any animations attached to its transition) finishes; only then is the content removed/disposed. (developer.android.com)

How to run logic after the exit animation finishes

Use visibleState: MutableTransitionState<Boolean> and wait for it to become idle, then act:

val visibleState = remember { MutableTransitionState(true) }

AnimatedVisibility(
    visibleState = visibleState,
    exit = fadeOut() + shrinkVertically(),
) {
    // UI
}

// trigger exit
visibleState.targetState = false

LaunchedEffect(visibleState.currentState, visibleState.isIdle) {
    if (!visibleState.currentState && visibleState.isIdle) {
        // Exit animation finished; content has been removed
        // Do cleanup / navigate / update state, etc.
    }
}

MutableTransitionState.isIdle is the recommended signal that the enter/exit transition has finished. (developer.android.com)

If you’re trying to prevent “work” during exit recompositions

You generally can’t prevent recomposition while it’s exiting (it’s still composed by design). Instead, gate expensive work based on the transition state (e.g., only run when fully visible), or move side effects into DisposableEffect so cleanup happens on disposal (which occurs after the exit finishes). (developer.android.com)

Citations:


AnimatedVisibilityScope.transition.isRunning 대신 MutableTransitionState.isIdle을 사용하여 exit 애니메이션 완료 감지

현재 코드는 transition.isRunning을 모니터링하지만, Compose 공식 권장 패턴은 MutableTransitionStateisIdle을 사용하는 것입니다. isIdle은 모든 애니메이션이 완료되었음을 명확히 나타내며, 다음과 같이 구현하는 것을 권장합니다:

val visibleState = remember { MutableTransitionState(initialValue) }

LaunchedEffect(visibleState.currentState, visibleState.isIdle) {
    if (!visibleState.currentState && visibleState.isIdle) {
        onAnimationFinished(isMapVisible)
    }
}

AnimatedVisibility(
    visibleState = visibleState,
    enter = fadeIn(),
    exit = fadeOut()
) { /* content */ }

이렇게 하면 exit 애니메이션 완료 후 content 제거 시점을 더 명확하고 안전하게 감지할 수 있습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@android/app/src/main/java/com/on/turip/ui/compose/favorite/TuripPlaceScreen.kt`
around lines 339 - 341, Replace the current use of
AnimatedVisibilityScope.transition.isRunning with a MutableTransitionState-based
visible state: create a remember { MutableTransitionState(initialVisibleValue) }
(e.g., visibleState), pass it into AnimatedVisibility(visibleState =
visibleState, ...), and change the LaunchedEffect to watch
visibleState.currentState and visibleState.isIdle; call
onAnimationFinished(isMapVisible) when visibleState.currentState is false and
visibleState.isIdle (this ensures you detect exit animation completion instead
of using transition.isRunning).

Scaffold(
modifier = modifier.fillMaxSize(),
containerColor = TuripTheme.colors.white,
contentWindowInsets = WindowInsets(0, 0, 0, 0),
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n android/app/src/main/java/com/on/turip/ui/compose/folder/FolderScreen.kt | sed -n '110,140p'

Repository: woowacourse-teams/2025-Turip

Length of output: 1381


🏁 Script executed:

rg -n "contentWindowInsets\s*=\s*WindowInsets" --type=kotlin -B 2 -A 2

Repository: woowacourse-teams/2025-Turip

Length of output: 1155


🏁 Script executed:

rg -n "fun FolderScreen|@Composable" android/app/src/main/java/com/on/turip/ui/compose/folder/FolderScreen.kt | head -20

Repository: woowacourse-teams/2025-Turip

Length of output: 118


🏁 Script executed:

rg -n "enableEdgeToEdge|systemBarsPadding|statusBarsPadding" android/app/src/main/java/com/on/turip/ui/compose/ --type=kotlin | head -20

Repository: woowacourse-teams/2025-Turip

Length of output: 1036


🏁 Script executed:

rg -n "enableEdgeToEdge|WindowInsets\|MainActivity|Activity" android/app/src/main/java/com/on/turip/ --type=kotlin | grep -E "(MainActivity|enableEdgeToEdge|class.*Activity)" | head -10

Repository: woowacourse-teams/2025-Turip

Length of output: 1242


🏁 Script executed:

find android/app/src/main/java -name "*Activity.kt" -o -name "*MainActivity.kt" | head -5

Repository: woowacourse-teams/2025-Turip

Length of output: 416


🏁 Script executed:

rg -n "enableEdgeToEdge" android/app/src/main/java/ --type=kotlin -B 3 -A 3

Repository: woowacourse-teams/2025-Turip

Length of output: 9438


🏁 Script executed:

rg -n "contentWindowInsets\s*=" --type=kotlin -A 1 -B 1 | head -40

Repository: woowacourse-teams/2025-Turip

Length of output: 755


HomeScreen과의 일관성을 위해 contentWindowInsets 설정 확인 필요합니다.

enableEdgeToEdge()가 Activity 수준에서 이미 시스템 인셋을 처리하고 있으므로, contentWindowInsets = WindowInsets(0, 0, 0, 0)은 이중 패딩을 방지하기 위한 의도적인 설계입니다. 다만 HomeScreen.kt는 contentWindowInsets = WindowInsets()을 사용하고 있어 두 화면의 인셋 처리 방식이 일관성이 없습니다. 동일한 Activity 수준의 inset 처리 정책에 맞춰 두 화면의 Scaffold inset 설정을 일관되게 정의해 주세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@android/app/src/main/java/com/on/turip/ui/compose/folder/FolderScreen.kt` at
line 122, FolderScreen.kt uses contentWindowInsets = WindowInsets(0, 0, 0, 0) to
avoid double-padding because enableEdgeToEdge() is handled at the Activity
level, but HomeScreen.kt uses contentWindowInsets = WindowInsets() causing
inconsistency; make the inset policy consistent by updating HomeScreen.kt (the
Scaffold usage where contentWindowInsets is set) to use contentWindowInsets =
WindowInsets(0, 0, 0, 0) so both FolderScreen (contentWindowInsets =
WindowInsets(0, 0, 0, 0)) and HomeScreen follow the same Activity-level inset
handling.

Comment on lines 130 to 133
Icon(
imageVector = Icons.Default.Add,
contentDescription = null,
)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

FAB 아이콘에 contentDescription이 누락되어 접근성을 저해합니다.

TalkBack 사용자는 이 버튼이 무엇을 하는지 알 수 없습니다. 문자열 리소스를 추가하고 전달하세요.

♿ 제안하는 수정
                 Icon(
                     imageVector = Icons.Default.Add,
-                    contentDescription = null,
+                    contentDescription = stringResource(R.string.add_turip),
                 )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@android/app/src/main/java/com/on/turip/ui/compose/folder/FolderScreen.kt`
around lines 130 - 133, The FloatingActionButton Icon in FolderScreen.kt uses
Icons.Default.Add with contentDescription = null which breaks accessibility;
replace the null with a localized string resource (e.g., R.string.add_folder) by
adding an appropriate entry to strings.xml and passing it via
stringResource(R.string.add_folder) as the contentDescription for the Icon
(ensure you import androidx.compose.ui.res.stringResource and update the Icon
call where Icons.Default.Add is used).

Comment on lines 144 to 153
Text(
text = "내 튜립",
style = TuripTheme.typography.display,
color = Color.Black,
modifier =
Modifier.padding(
top = TuripTheme.spacing.extraExtraLarge,
bottom = TuripTheme.spacing.large,
),
)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

하드코딩된 문자열 및 테마 외 색상 사용.

두 가지 일관성 문제가 있습니다:

  1. Line 145: "내 튜립" 문자열이 하드코딩되어 있습니다. stringResource(R.string.my_turip) 형태로 리소스화해야 합니다.
  2. Line 147: Color.Black은 프로젝트의 다른 곳에서 사용하는 TuripTheme.colors.black과 불일치합니다.
♻️ 제안하는 수정
             Text(
-                text = "내 튜립",
+                text = stringResource(R.string.my_turip),
                 style = TuripTheme.typography.display,
-                color = Color.Black,
+                color = TuripTheme.colors.black,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@android/app/src/main/java/com/on/turip/ui/compose/folder/FolderScreen.kt`
around lines 144 - 153, Replace the hardcoded label and direct Color usage in
the Text composable inside FolderScreen: use stringResource(R.string.my_turip)
instead of the literal "내 튜립", and replace Color.Black with the theme color
TuripTheme.colors.black so the Text call (in FolderScreen/its composable
function) uses localized resources and the project's theme color.

Comment on lines 40 to 48
fun loadTuripFolders() {
viewModelScope.launch {
turipRepository.loadTurips().onSuccess { turips: List<Turip> ->
_uiState.update { folderUiState: FolderUiState ->
folderUiState.copy(turips = turips.map { it.toUiModel() }.toImmutableList())
}
}
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cd /tmp/repository 2>/dev/null || cd / && find . -name "FolderViewModel.kt" -o -name "*FolderUiState*" -o -name "*FolderRetryAction*" 2>/dev/null | head -20

Repository: woowacourse-teams/2025-Turip

Length of output: 261


🏁 Script executed:

git ls-files | grep -i folder | grep -E "\.(kt|java)$"

Repository: woowacourse-teams/2025-Turip

Length of output: 4523


🏁 Script executed:

find . -path "*/turip/ui/compose/folder/*" -type f -name "*.kt" 2>/dev/null

Repository: woowacourse-teams/2025-Turip

Length of output: 797


🏁 Script executed:

cat -n android/app/src/main/java/com/on/turip/ui/compose/folder/FolderViewModel.kt

Repository: woowacourse-teams/2025-Turip

Length of output: 2184


🏁 Script executed:

cat -n android/app/src/main/java/com/on/turip/ui/compose/folder/FolderUiState.kt

Repository: woowacourse-teams/2025-Turip

Length of output: 834


🏁 Script executed:

cat -n android/app/src/main/java/com/on/turip/ui/compose/folder/FolderUiEffect.kt

Repository: woowacourse-teams/2025-Turip

Length of output: 581


🏁 Script executed:

cat -n android/app/src/main/java/com/on/turip/ui/compose/folder/FolderScreen.kt

Repository: woowacourse-teams/2025-Turip

Length of output: 8395


loadTuripFolders()에서 로딩 상태 미관리 및 오류 처리 누락

두 가지 심각한 문제가 있습니다.

  1. isLoading 상태 미관리: API 호출 전 isLoading = true로 갱신하지 않고, 완료 후에도 isLoading = false로 복원하지 않습니다. FolderUiState.isLoading이 항상 false를 유지하므로 로딩 인디케이터가 전혀 표시되지 않습니다.

  2. 오류 처리 누락: onSuccess만 처리하고 실패 케이스가 없습니다. 네트워크/서버 오류가 묵살되며 errorUiState도 갱신되지 않고, 정의된 FolderUiEffect.ShowError도 발송되지 않아 사용자가 오류 상황을 인지할 수 없습니다.

제안하는 수정
 fun loadTuripFolders() {
     viewModelScope.launch {
+        _uiState.update { it.copy(isLoading = true) }
         turipRepository.loadTurips()
             .onSuccess { turips: List<Turip> ->
                 _uiState.update { folderUiState: FolderUiState ->
-                    folderUiState.copy(turips = turips.map { it.toUiModel() }.toImmutableList())
+                    folderUiState.copy(
+                        isLoading = false,
+                        turips = turips.map { it.toUiModel() }.toImmutableList(),
+                    )
                 }
             }
+            .onFailure { throwable ->
+                _uiState.update { it.copy(isLoading = false) }
+                _uiEffect.send(FolderUiEffect.ShowError(
+                    errorUiState = /* throwable에서 ErrorUiState 변환 */,
+                    retryAction = FolderRetryAction.UpdateFolder,
+                ))
+            }
     }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fun loadTuripFolders() {
viewModelScope.launch {
turipRepository.loadTurips().onSuccess { turips: List<Turip> ->
_uiState.update { folderUiState: FolderUiState ->
folderUiState.copy(turips = turips.map { it.toUiModel() }.toImmutableList())
}
}
}
}
fun loadTuripFolders() {
viewModelScope.launch {
_uiState.update { it.copy(isLoading = true) }
turipRepository.loadTurips()
.onSuccess { turips: List<Turip> ->
_uiState.update { folderUiState: FolderUiState ->
folderUiState.copy(
isLoading = false,
turips = turips.map { it.toUiModel() }.toImmutableList(),
)
}
}
.onFailure { throwable ->
_uiState.update { it.copy(isLoading = false) }
_uiEffect.send(FolderUiEffect.ShowError(
errorUiState = ErrorUiState(message = throwable.message ?: "Unknown error"),
retryAction = FolderRetryAction.UpdateFolder,
))
}
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@android/app/src/main/java/com/on/turip/ui/compose/folder/FolderViewModel.kt`
around lines 40 - 48, loadTuripFolders currently doesn't set
FolderUiState.isLoading or handle failures; update it to set isLoading = true
before calling turipRepository.loadTurips(), handle both onSuccess (update
turips via .map { it.toUiModel() } and set isLoading = false) and onFailure (set
isLoading = false, update errorUiState and emit FolderUiEffect.ShowError), or
use a try/catch/finally around the repository call to ensure isLoading is reset
in all cases and errors are converted into the errorUiState/FolderUiEffect;
reference loadTuripFolders, FolderUiState.isLoading, errorUiState,
FolderUiEffect.ShowError and turipRepository.loadTurips() when making the
changes.

Copy link
Contributor Author

@jerry8282 jerry8282 left a comment

Choose a reason for hiding this comment

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

정말 좋은 리뷰 감사합니다 리컴퍼지션 최소화와 uistate를 깔끔하게 처리하는 것 까지 너무 좋네요!😊

Comment on lines 175 to 177
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

반영 커밋입니다! 5f6589d

저도 처음에 위에 배치를 했었다가 위에 배치를 하게 되면 컴포저블이 배치되기 전에 focus를 하게 되는 문제가 발생할 수도 있다고 하여 지금과 같이 변경하게 되었습니다.

더 찾아보니 awaitFrame이라는 게 있어서 추가를 하여 UI가 다 그려진 뒤에 호출 되도록 변경하고 상단에 배치하였습니다.😊

@Composable
fun NameEditorSheetContent(
title: String,
folderName: String,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

반영 커밋입니다1817a4e

남아있는 folder들도 전부 Turip으로 변경완료했습니다!

BasicTextField(
value = folderName,
onValueChange = { input ->
if (input.length <= 20) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

반영커밋입니다! 8ae37e8

20의 위치가 애매해서 TuripName이라는 value class를 만들어서 사용하려고 했는데 변경사항이 너무 많이 생겨 다른 PR과 충돌이 생길 거 같아 이번 PR에서는 viewmodel의 companion으로 만들도록 하였습니다!

Text(
text = stringResource(R.string.bottom_sheet_turip_add_turip_name_hint),
color = TuripTheme.colors.gray02,
style = TuripTheme.typography.body2,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

반영 커밋입니다! 8eabb70
body2를 한 건 bold처리 때문에 그랬던 거였는데 같은 크기로 하고 bold만 변경되도록 하였습니다.😊

).background(
color = TuripTheme.colors.white,
shape = TuripTheme.shape.container,
).padding(horizontal = 12.dp, vertical = 6.dp),
Copy link
Contributor Author

Choose a reason for hiding this comment

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

반영 커밋입니다! a8f213f

Comment on lines 57 to 63
enum class MyTuripTab(
val tabName: String,
) {
ALL("전체"),
SOLO("나홀로 튜립"),
TOGETHER("함께 튜립"),
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

반영 완료 커밋입니다! f1cb661

Comment on lines 193 to 198
val filteredTurips: List<MyTuripModel> =
when (selectedTab) {
MyTuripTab.ALL -> turips
MyTuripTab.SOLO -> turips.filter { it.type == TuripType.SOLO }
MyTuripTab.TOGETHER -> turips.filter { it.type == TuripType.TOGETHER }
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

좋은 리뷰 감사합니다! 반영 완료 커밋입니다! 9a7f4f1

Comment on lines 281 to 287
SnackbarHost(
hostState = snackbarHostState,
modifier =
Modifier
.align(Alignment.BottomCenter)
.padding(bottom = TuripTheme.spacing.extraLarge),
)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

반영 완료 커밋입니다! 5a62f36

Scaffold의 snackbarHostState를 사용하지 않고 Box로 감싸 하단에 직접 배치한 이유는,
Scaffold를 사용할 경우 Snackbar가 FloatingActionButton 위쪽에 자동으로 배치되어 위치를 세밀하게 커스터마이징하기 어려웠기 때문입니다.

따라서 Snackbar의 위치를 보다 유연하게 제어하기 위해 별도로 분리하여 처리하였습니다.😊
TuripSnackbar로 쓰도록 수정하였습니다!

Comment on lines 158 to 159
val deletedTuripName: String? =
uiState.turips.find { it.id == uiState.deletedTuripId }?.name
Copy link
Contributor Author

Choose a reason for hiding this comment

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

반영 커밋입니다! cfd30d1

이 부분이 처리하기가 너무 애매했었는데 훨씬 깔끔해지네요 역시 채넛 많이 배웁니당!😊


LaunchedEffect(Unit) {
lifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) {
viewModel.loadTuripFolders()
Copy link
Contributor Author

Choose a reason for hiding this comment

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

만약, 이 화면만 있으면 채넛이 말씀하신 내용이 맞는데 현재는 Detail화면에서의 폴더 추가도 감지해야 되기 때문에 다음과 같이 작성을 하였습니다.😊

Copy link
Contributor

@yrsel yrsel left a comment

Choose a reason for hiding this comment

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

제리, 길었던 리뷰도 거의 막바지 인거 같아요, 고생하셨습니다!
반영 너무 잘해주시고 의견에 동의해주셔서 감사해요!
마지막까지 화이팅 해봅시다 ~ :)
의견이 필요한 부분이 있다면 역시나 편하게 연락바랍니다 🫡

BasicTextField(
value = folderName,
onValueChange = { input ->
if (input.length <= 20) {
Copy link
Contributor

Choose a reason for hiding this comment

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

좋아요, 다음번에 반영하면 좋겠네요 ! : )

Comment on lines 175 to 177
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
Copy link
Contributor

Choose a reason for hiding this comment

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

오, LaunchedEffect 선언위치도 영향을 줄 수 있나요??

컴포저블이 배치되기 전에 포커스 하게 되면 어떤 문제가 발생할 수 있나요 ??
코루틴의 awaitFrame이 있거나 없거나 동일하게 작동하고 있는 거 같은데 어떤 차이가 있게 되는 건지 궁금해요 💭

;

companion object {
fun of(isShared: Boolean): TuripTypeModel = if (isShared) TOGETHER else SOLO
Copy link
Contributor

Choose a reason for hiding this comment

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

c: 정적 팩토리 메서드 패턴의 규칙이 하나의 매개변수라면 from으로 사용하더라구요!

정적 팩토리 메서드 패턴 블로그 글

Copy link
Contributor Author

Choose a reason for hiding this comment

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

반영 커밋입니다! 42d1bd2


LaunchedEffect(Unit) {
lifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) {
viewModel.loadTuripFolders()
Copy link
Contributor

Choose a reason for hiding this comment

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

옴,, 현재 상세 화면에서 폴더 추가를 할 수 없지 않나요 ?!

네비게이션 도입하다보면 프로젝트 전반에 있는 이런 코드들이 제거될 수 있겠네요!


@Preview(name = "MoreOption - 기본폴더(삭제 비활성)", showBackground = true)
@Composable
private fun MoreOptionBottomSheetPreviewMoreOption_Default() {
Copy link
Contributor

Choose a reason for hiding this comment

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

c: preview 함수명 슬쩍 반영 부탁드려요!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

반영 커밋입니다! ca3b113

정말 잘 찾아내시는 구만유..🥹

private val _uiEffect: Channel<MyTuripUiEffect> = Channel(Channel.BUFFERED)
val uiEffect: Flow<MyTuripUiEffect> = _uiEffect.receiveAsFlow()

fun loadTuripFolders() {
Copy link
Contributor

Choose a reason for hiding this comment

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

c: 메서드 실패할 때 처리가 안되어 있어요!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

반영 커밋입니다! b7075ca

현재 낙관적 UI가 적용이 잘 되어 있지 않아 전체적으로 코드를 수정을 하였습니다!🥹

Comment on lines 67 to 72
it.copy(
dialogState =
MyTuripUiState.MyTuripDialogState.RemoveTurip(
turip = myTuripModel,
),
)
Copy link
Contributor

Choose a reason for hiding this comment

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

a: 여기 한 줄로 작성해도 될 것 같아요!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

반영 커밋입니다! fcdf71a

Comment on lines 79 to 80
val editModels: List<TuripEditModel> =
_uiState.value.turips.map { it.toEditModel() }
Copy link
Contributor

Choose a reason for hiding this comment

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

a: 여기도 한 줄로 작성 가능해요!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

반영 커밋입니다! 7425cbf

}.onFailure {
_uiEffect.send(
MyTuripUiEffect.ShowError(
errorUiState = uiState.value.errorUiState,
Copy link
Contributor

Choose a reason for hiding this comment

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

r: onFailure 에서 내려준 에러는 무시되고 uiState.value.errorUiState 는 항상 None 일 것 같아요!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

반영 커밋입니다! 5f863d7

}

is MyTuripRetryAction.AddMyTurip -> {
_uiState.update { it.copy(inputTuripName = action.name) }
Copy link
Contributor

Choose a reason for hiding this comment

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

r: addTurip() 의 onSuccess 블럭에 있는 게 적절해 보여요!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

반영 커밋입니다! 840f673

Copy link
Contributor

Choose a reason for hiding this comment

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

r: Retry 할 때 name을 파라미터로 받지 않고 uiState에 있는 inputTuripName을 사용해도 동일할 것 같아요! 따라서 addTurip 메서드 파라미터를 받지 않아도 될 것 같아요!

그리고 성공 시에 loadTuripFolders() 메서드 호출 없이 uiState에 있는 turips에 api 응답결과로 받아온 Turip 객체를 추가해주면 좋을 것 같아요!

Copy link
Contributor Author

@jerry8282 jerry8282 left a comment

Choose a reason for hiding this comment

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

정말 꼼꼼한 리뷰 감사합니다 채넛..!! 리팩토링을 너무 잘하시는 구만유!!

현재 낙관적 UI에 대한 코드가 반영이 잘 안되어서 TuripDetailViewmodel을 수정하였습니다.
추가적으로, MyTuripViewmodel에도 폴더 삭제에 대한 낙관적 UI를 적용완료하였습니다.😊

}

is MyTuripRetryAction.AddMyTurip -> {
_uiState.update { it.copy(inputTuripName = action.name) }
Copy link
Contributor Author

Choose a reason for hiding this comment

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

반영 커밋입니다! 840f673

}

@Composable
private fun SheetSettingItem(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

반영 커밋입니다! 1a2ce65


LaunchedEffect(Unit) {
lifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) {
viewModel.loadTuripFolders()
Copy link
Contributor Author

Choose a reason for hiding this comment

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

아 상세화면이 튜립 상세 화면이 아니라 트립 상세 화면을 말씀드린거였어요 따라서 네비게이션을 도입하지 않더라도 이 코드는 제거 되지는 않을 거 같아요 나중에 evenbus나 다른 것들을 통해 처리하면 좋을 거 같아요!

Comment on lines 175 to 177
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

찾아보니 같은 composable에서는 선언 위치 문제가 없네요😅 그래도 awaitFrame은 있어야 된다고 생각하는 게 awaitFrame()은 첫 프레임을 한 번 지나가게 해서 UI/포커스 타겟이 준비될 시간을 주는 역할이라 requestFocus()를 더 안정적으로 만들 수 있게 된다고 생각합니다!😊

옛날 글이긴 하지만 requestFocus에 문제가 있어 awaitFrame을 사용했다는 글도 있는데 최근에는 windowInfo를 통해 해결할 수 있다고 하네요! 하지만, 간단한 코드에서는 awiaitFrame으로도 충분하다고 해서 awaitFrame을 그대로 적용할 계획입니다!

;

companion object {
fun of(isShared: Boolean): TuripTypeModel = if (isShared) TOGETHER else SOLO
Copy link
Contributor Author

Choose a reason for hiding this comment

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

반영 커밋입니다! 42d1bd2

},
) {
Text(
text = "내 튜립",
Copy link
Contributor Author

Choose a reason for hiding this comment

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

반영 커밋입니다! 3f20c53

private val _uiEffect: Channel<MyTuripUiEffect> = Channel(Channel.BUFFERED)
val uiEffect: Flow<MyTuripUiEffect> = _uiEffect.receiveAsFlow()

fun loadTuripFolders() {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

반영 커밋입니다! b7075ca

현재 낙관적 UI가 적용이 잘 되어 있지 않아 전체적으로 코드를 수정을 하였습니다!🥹

Comment on lines 67 to 72
it.copy(
dialogState =
MyTuripUiState.MyTuripDialogState.RemoveTurip(
turip = myTuripModel,
),
)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

반영 커밋입니다! fcdf71a

Comment on lines 79 to 80
val editModels: List<TuripEditModel> =
_uiState.value.turips.map { it.toEditModel() }
Copy link
Contributor Author

Choose a reason for hiding this comment

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

반영 커밋입니다! 7425cbf

}.onFailure {
_uiEffect.send(
MyTuripUiEffect.ShowError(
errorUiState = uiState.value.errorUiState,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

반영 커밋입니다! 5f863d7

Copy link
Contributor

@yrsel yrsel left a comment

Choose a reason for hiding this comment

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

메서드 하나에 대해 의견 남겼어요!
의견 확인 후에 머지하면 좋을 것 같습니다.
진짜 고생 많으셨습니다 제리 🐭

}

is MyTuripRetryAction.AddMyTurip -> {
_uiState.update { it.copy(inputTuripName = action.name) }
Copy link
Contributor

Choose a reason for hiding this comment

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

r: Retry 할 때 name을 파라미터로 받지 않고 uiState에 있는 inputTuripName을 사용해도 동일할 것 같아요! 따라서 addTurip 메서드 파라미터를 받지 않아도 될 것 같아요!

그리고 성공 시에 loadTuripFolders() 메서드 호출 없이 uiState에 있는 turips에 api 응답결과로 받아온 Turip 객체를 추가해주면 좋을 것 같아요!

Comment on lines 175 to 177
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
Copy link
Contributor

Choose a reason for hiding this comment

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

아하, 넵 자세하게 설명해주셔서 감사해요 제리 👍
좋습니다!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🐭 제리 우아한 테크코스 7기 안드로이드 제리 🤖 Android 안드로이드얌 📝 feat

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

[feat] 내 튜립 화면 compose로 마이그레이션

2 participants