Conversation
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
Walkthrough이번 변경사항은 내 튜립 화면을 Compose로 마이그레이션하고, 멤버 수 및 공유 여부 필드를 데이터 모델에 추가하며, 단일 튜립 조회를 위한 API 엔드포인트를 추가합니다. 새로운 UI 화면, 뷰모델, 컴포넌트들이 도입되었습니다. Changes
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: 목록 새로고침
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: 완료 상태 업데이트
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes 🚥 Pre-merge checks | ✅ 1 | ❌ 4❌ Failed checks (2 warnings, 2 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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:TuripDetailPreviewState의name,turipName필드가 프리뷰에서 미사용
TuripDetailPreview(Line 289~299)에서state.name과state.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:animateFloatAsState에label파라미터 추가 권장
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:TuripMapContent가public으로 노출되어 있음.이 파일 내부에서만 사용되는 것으로 보이므로,
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 사용 권장.
currentScreen에0,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,TuripTypeenum)가 안정적 타입이므로 같은 모듈 내에서는 컴파일러가 안정성을 추론할 수 있지만, 명시적으로@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 = {}로 Material3TabRow의 모든 기본 동작을 비활성화하고 있습니다. 실질적으로 균등 너비 배분만 사용하므로, 불필요한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의 색상 및 문자열 하드코딩두 가지 문제가 있습니다.
- 하드코딩 색상:
Color(0xFFD4E157),Color(0xFF90CAF9)대신TuripTheme.colors의 디자인 토큰을 사용하면 테마 일관성이 유지됩니다.- 하드코딩 문자열:
"함께 튜립","나홀로 튜립"은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.
| 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, | ||
| ) | ||
| } | ||
| } |
There was a problem hiding this comment.
공유 버튼이 Row 끝에 정렬되지 않음
Row에 Modifier.fillMaxWidth()가 없고 Text에 Modifier.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.
android/app/src/main/java/com/on/turip/ui/compose/favorite/TuripPlaceScreen.kt
Outdated
Show resolved
Hide resolved
| LaunchedEffect(transition.isRunning) { | ||
| if (!transition.isRunning) onAnimationFinished(isMapVisible) | ||
| } |
There was a problem hiding this comment.
🧩 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:
AnimatedVisibilitykeeps its content until exit animations finish, then removes it. [1][5]MutableTransitionState.isIdletells you when the transition has finished running. [3]
Notes:
- If you use
ExitTransition.None(and don’t run any exit animations inAnimatedVisibilityScope.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.transitionsoAnimatedVisibilitywill 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 == truewhile theAnimatedVisibilityenter/exit is animating and while any custom animations you attach to that sametransition(viatransition.animate*{}) are still running.AnimatedVisibilitywaits for all of those to finish before considering itself idle. [1][2]- After an exit,
AnimatedVisibilityremoves 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, anAnimatableyou launch separately, etc.),AnimatedVisibilitycan’t “see” it and may remove the content before that animation finishes. [1][2] - If
exit = ExitTransition.Noneand 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:
- 1: https://developer.android.com/develop/ui/compose/animation/composables-modifiers?utm_source=openai
- 2: https://developer.android.com/reference/kotlin/androidx/compose/animation/ExitTransition?utm_source=openai
- 3: https://developer.android.com/develop/ui/compose/animation/composables-modifiers?utm_source=openai
AnimatedVisibilityScope.transition.isRunning 대신 MutableTransitionState.isIdle을 사용하여 exit 애니메이션 완료 감지
현재 코드는 transition.isRunning을 모니터링하지만, Compose 공식 권장 패턴은 MutableTransitionState의 isIdle을 사용하는 것입니다. 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).
android/app/src/main/java/com/on/turip/ui/compose/turipdetail/TuripPlaceUiState.kt
Show resolved
Hide resolved
android/app/src/main/java/com/on/turip/ui/compose/turip/MyTuripScreen.kt
Show resolved
Hide resolved
| Scaffold( | ||
| modifier = modifier.fillMaxSize(), | ||
| containerColor = TuripTheme.colors.white, | ||
| contentWindowInsets = WindowInsets(0, 0, 0, 0), |
There was a problem hiding this comment.
🧩 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 2Repository: 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 -20Repository: 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 -20Repository: 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 -10Repository: woowacourse-teams/2025-Turip
Length of output: 1242
🏁 Script executed:
find android/app/src/main/java -name "*Activity.kt" -o -name "*MainActivity.kt" | head -5Repository: woowacourse-teams/2025-Turip
Length of output: 416
🏁 Script executed:
rg -n "enableEdgeToEdge" android/app/src/main/java/ --type=kotlin -B 3 -A 3Repository: woowacourse-teams/2025-Turip
Length of output: 9438
🏁 Script executed:
rg -n "contentWindowInsets\s*=" --type=kotlin -A 1 -B 1 | head -40Repository: 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.
| Icon( | ||
| imageVector = Icons.Default.Add, | ||
| contentDescription = null, | ||
| ) |
There was a problem hiding this comment.
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).
| Text( | ||
| text = "내 튜립", | ||
| style = TuripTheme.typography.display, | ||
| color = Color.Black, | ||
| modifier = | ||
| Modifier.padding( | ||
| top = TuripTheme.spacing.extraExtraLarge, | ||
| bottom = TuripTheme.spacing.large, | ||
| ), | ||
| ) |
There was a problem hiding this comment.
하드코딩된 문자열 및 테마 외 색상 사용.
두 가지 일관성 문제가 있습니다:
- Line 145:
"내 튜립"문자열이 하드코딩되어 있습니다.stringResource(R.string.my_turip)형태로 리소스화해야 합니다. - 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.
| fun loadTuripFolders() { | ||
| viewModelScope.launch { | ||
| turipRepository.loadTurips().onSuccess { turips: List<Turip> -> | ||
| _uiState.update { folderUiState: FolderUiState -> | ||
| folderUiState.copy(turips = turips.map { it.toUiModel() }.toImmutableList()) | ||
| } | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
🧩 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 -20Repository: 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/nullRepository: woowacourse-teams/2025-Turip
Length of output: 797
🏁 Script executed:
cat -n android/app/src/main/java/com/on/turip/ui/compose/folder/FolderViewModel.ktRepository: woowacourse-teams/2025-Turip
Length of output: 2184
🏁 Script executed:
cat -n android/app/src/main/java/com/on/turip/ui/compose/folder/FolderUiState.ktRepository: woowacourse-teams/2025-Turip
Length of output: 834
🏁 Script executed:
cat -n android/app/src/main/java/com/on/turip/ui/compose/folder/FolderUiEffect.ktRepository: woowacourse-teams/2025-Turip
Length of output: 581
🏁 Script executed:
cat -n android/app/src/main/java/com/on/turip/ui/compose/folder/FolderScreen.ktRepository: woowacourse-teams/2025-Turip
Length of output: 8395
loadTuripFolders()에서 로딩 상태 미관리 및 오류 처리 누락
두 가지 심각한 문제가 있습니다.
-
isLoading상태 미관리: API 호출 전isLoading = true로 갱신하지 않고, 완료 후에도isLoading = false로 복원하지 않습니다.FolderUiState.isLoading이 항상false를 유지하므로 로딩 인디케이터가 전혀 표시되지 않습니다. -
오류 처리 누락:
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.
| 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.
jerry8282
left a comment
There was a problem hiding this comment.
정말 좋은 리뷰 감사합니다 리컴퍼지션 최소화와 uistate를 깔끔하게 처리하는 것 까지 너무 좋네요!😊
| LaunchedEffect(Unit) { | ||
| focusRequester.requestFocus() | ||
| } |
There was a problem hiding this comment.
반영 커밋입니다! 5f6589d
저도 처음에 위에 배치를 했었다가 위에 배치를 하게 되면 컴포저블이 배치되기 전에 focus를 하게 되는 문제가 발생할 수도 있다고 하여 지금과 같이 변경하게 되었습니다.
더 찾아보니 awaitFrame이라는 게 있어서 추가를 하여 UI가 다 그려진 뒤에 호출 되도록 변경하고 상단에 배치하였습니다.😊
| @Composable | ||
| fun NameEditorSheetContent( | ||
| title: String, | ||
| folderName: String, |
| BasicTextField( | ||
| value = folderName, | ||
| onValueChange = { input -> | ||
| if (input.length <= 20) { |
There was a problem hiding this comment.
반영커밋입니다! 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, |
There was a problem hiding this comment.
반영 커밋입니다! 8eabb70
body2를 한 건 bold처리 때문에 그랬던 거였는데 같은 크기로 하고 bold만 변경되도록 하였습니다.😊
| ).background( | ||
| color = TuripTheme.colors.white, | ||
| shape = TuripTheme.shape.container, | ||
| ).padding(horizontal = 12.dp, vertical = 6.dp), |
| enum class MyTuripTab( | ||
| val tabName: String, | ||
| ) { | ||
| ALL("전체"), | ||
| SOLO("나홀로 튜립"), | ||
| TOGETHER("함께 튜립"), | ||
| } |
| 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 } | ||
| } |
| SnackbarHost( | ||
| hostState = snackbarHostState, | ||
| modifier = | ||
| Modifier | ||
| .align(Alignment.BottomCenter) | ||
| .padding(bottom = TuripTheme.spacing.extraLarge), | ||
| ) |
There was a problem hiding this comment.
반영 완료 커밋입니다! 5a62f36
Scaffold의 snackbarHostState를 사용하지 않고 Box로 감싸 하단에 직접 배치한 이유는,
Scaffold를 사용할 경우 Snackbar가 FloatingActionButton 위쪽에 자동으로 배치되어 위치를 세밀하게 커스터마이징하기 어려웠기 때문입니다.
따라서 Snackbar의 위치를 보다 유연하게 제어하기 위해 별도로 분리하여 처리하였습니다.😊
TuripSnackbar로 쓰도록 수정하였습니다!
| val deletedTuripName: String? = | ||
| uiState.turips.find { it.id == uiState.deletedTuripId }?.name |
There was a problem hiding this comment.
반영 커밋입니다! cfd30d1
이 부분이 처리하기가 너무 애매했었는데 훨씬 깔끔해지네요 역시 채넛 많이 배웁니당!😊
|
|
||
| LaunchedEffect(Unit) { | ||
| lifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) { | ||
| viewModel.loadTuripFolders() |
There was a problem hiding this comment.
만약, 이 화면만 있으면 채넛이 말씀하신 내용이 맞는데 현재는 Detail화면에서의 폴더 추가도 감지해야 되기 때문에 다음과 같이 작성을 하였습니다.😊
yrsel
left a comment
There was a problem hiding this comment.
제리, 길었던 리뷰도 거의 막바지 인거 같아요, 고생하셨습니다!
반영 너무 잘해주시고 의견에 동의해주셔서 감사해요!
마지막까지 화이팅 해봅시다 ~ :)
의견이 필요한 부분이 있다면 역시나 편하게 연락바랍니다 🫡
| BasicTextField( | ||
| value = folderName, | ||
| onValueChange = { input -> | ||
| if (input.length <= 20) { |
| LaunchedEffect(Unit) { | ||
| focusRequester.requestFocus() | ||
| } |
There was a problem hiding this comment.
오, LaunchedEffect 선언위치도 영향을 줄 수 있나요??
컴포저블이 배치되기 전에 포커스 하게 되면 어떤 문제가 발생할 수 있나요 ??
코루틴의 awaitFrame이 있거나 없거나 동일하게 작동하고 있는 거 같은데 어떤 차이가 있게 되는 건지 궁금해요 💭
| ; | ||
|
|
||
| companion object { | ||
| fun of(isShared: Boolean): TuripTypeModel = if (isShared) TOGETHER else SOLO |
There was a problem hiding this comment.
c: 정적 팩토리 메서드 패턴의 규칙이 하나의 매개변수라면 from으로 사용하더라구요!
|
|
||
| LaunchedEffect(Unit) { | ||
| lifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) { | ||
| viewModel.loadTuripFolders() |
There was a problem hiding this comment.
옴,, 현재 상세 화면에서 폴더 추가를 할 수 없지 않나요 ?!
네비게이션 도입하다보면 프로젝트 전반에 있는 이런 코드들이 제거될 수 있겠네요!
|
|
||
| @Preview(name = "MoreOption - 기본폴더(삭제 비활성)", showBackground = true) | ||
| @Composable | ||
| private fun MoreOptionBottomSheetPreviewMoreOption_Default() { |
| private val _uiEffect: Channel<MyTuripUiEffect> = Channel(Channel.BUFFERED) | ||
| val uiEffect: Flow<MyTuripUiEffect> = _uiEffect.receiveAsFlow() | ||
|
|
||
| fun loadTuripFolders() { |
There was a problem hiding this comment.
반영 커밋입니다! b7075ca
현재 낙관적 UI가 적용이 잘 되어 있지 않아 전체적으로 코드를 수정을 하였습니다!🥹
| it.copy( | ||
| dialogState = | ||
| MyTuripUiState.MyTuripDialogState.RemoveTurip( | ||
| turip = myTuripModel, | ||
| ), | ||
| ) |
| val editModels: List<TuripEditModel> = | ||
| _uiState.value.turips.map { it.toEditModel() } |
| }.onFailure { | ||
| _uiEffect.send( | ||
| MyTuripUiEffect.ShowError( | ||
| errorUiState = uiState.value.errorUiState, |
There was a problem hiding this comment.
r: onFailure 에서 내려준 에러는 무시되고 uiState.value.errorUiState 는 항상 None 일 것 같아요!
| } | ||
|
|
||
| is MyTuripRetryAction.AddMyTurip -> { | ||
| _uiState.update { it.copy(inputTuripName = action.name) } |
There was a problem hiding this comment.
r: addTurip() 의 onSuccess 블럭에 있는 게 적절해 보여요!
There was a problem hiding this comment.
r: Retry 할 때 name을 파라미터로 받지 않고 uiState에 있는 inputTuripName을 사용해도 동일할 것 같아요! 따라서 addTurip 메서드 파라미터를 받지 않아도 될 것 같아요!
그리고 성공 시에 loadTuripFolders() 메서드 호출 없이 uiState에 있는 turips에 api 응답결과로 받아온 Turip 객체를 추가해주면 좋을 것 같아요!
jerry8282
left a comment
There was a problem hiding this comment.
정말 꼼꼼한 리뷰 감사합니다 채넛..!! 리팩토링을 너무 잘하시는 구만유!!
현재 낙관적 UI에 대한 코드가 반영이 잘 안되어서 TuripDetailViewmodel을 수정하였습니다.
추가적으로, MyTuripViewmodel에도 폴더 삭제에 대한 낙관적 UI를 적용완료하였습니다.😊
| } | ||
|
|
||
| is MyTuripRetryAction.AddMyTurip -> { | ||
| _uiState.update { it.copy(inputTuripName = action.name) } |
| } | ||
|
|
||
| @Composable | ||
| private fun SheetSettingItem( |
|
|
||
| LaunchedEffect(Unit) { | ||
| lifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) { | ||
| viewModel.loadTuripFolders() |
There was a problem hiding this comment.
아 상세화면이 튜립 상세 화면이 아니라 트립 상세 화면을 말씀드린거였어요 따라서 네비게이션을 도입하지 않더라도 이 코드는 제거 되지는 않을 거 같아요 나중에 evenbus나 다른 것들을 통해 처리하면 좋을 거 같아요!
| LaunchedEffect(Unit) { | ||
| focusRequester.requestFocus() | ||
| } |
There was a problem hiding this comment.
찾아보니 같은 composable에서는 선언 위치 문제가 없네요😅 그래도 awaitFrame은 있어야 된다고 생각하는 게 awaitFrame()은 첫 프레임을 한 번 지나가게 해서 UI/포커스 타겟이 준비될 시간을 주는 역할이라 requestFocus()를 더 안정적으로 만들 수 있게 된다고 생각합니다!😊
옛날 글이긴 하지만 requestFocus에 문제가 있어 awaitFrame을 사용했다는 글도 있는데 최근에는 windowInfo를 통해 해결할 수 있다고 하네요! 하지만, 간단한 코드에서는 awiaitFrame으로도 충분하다고 해서 awaitFrame을 그대로 적용할 계획입니다!
| ; | ||
|
|
||
| companion object { | ||
| fun of(isShared: Boolean): TuripTypeModel = if (isShared) TOGETHER else SOLO |
| }, | ||
| ) { | ||
| Text( | ||
| text = "내 튜립", |
| private val _uiEffect: Channel<MyTuripUiEffect> = Channel(Channel.BUFFERED) | ||
| val uiEffect: Flow<MyTuripUiEffect> = _uiEffect.receiveAsFlow() | ||
|
|
||
| fun loadTuripFolders() { |
There was a problem hiding this comment.
반영 커밋입니다! b7075ca
현재 낙관적 UI가 적용이 잘 되어 있지 않아 전체적으로 코드를 수정을 하였습니다!🥹
| it.copy( | ||
| dialogState = | ||
| MyTuripUiState.MyTuripDialogState.RemoveTurip( | ||
| turip = myTuripModel, | ||
| ), | ||
| ) |
| val editModels: List<TuripEditModel> = | ||
| _uiState.value.turips.map { it.toEditModel() } |
| }.onFailure { | ||
| _uiEffect.send( | ||
| MyTuripUiEffect.ShowError( | ||
| errorUiState = uiState.value.errorUiState, |
yrsel
left a comment
There was a problem hiding this comment.
메서드 하나에 대해 의견 남겼어요!
의견 확인 후에 머지하면 좋을 것 같습니다.
진짜 고생 많으셨습니다 제리 🐭
| } | ||
|
|
||
| is MyTuripRetryAction.AddMyTurip -> { | ||
| _uiState.update { it.copy(inputTuripName = action.name) } |
There was a problem hiding this comment.
r: Retry 할 때 name을 파라미터로 받지 않고 uiState에 있는 inputTuripName을 사용해도 동일할 것 같아요! 따라서 addTurip 메서드 파라미터를 받지 않아도 될 것 같아요!
그리고 성공 시에 loadTuripFolders() 메서드 호출 없이 uiState에 있는 turips에 api 응답결과로 받아온 Turip 객체를 추가해주면 좋을 것 같아요!
| LaunchedEffect(Unit) { | ||
| focusRequester.requestFocus() | ||
| } |
There was a problem hiding this comment.
아하, 넵 자세하게 설명해주셔서 감사해요 제리 👍
좋습니다!
Issues
✔️ Check-list
🗒️ Work Description
📷 Screenshot
폴더 화면 폴더 추가 및 삭제
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
Chores