Conversation
Walkthrough새로운 여행 일정 추가(Add Itinerary) 기능을 구현하며, 장소 검색 및 선택을 위한 UI 컴포넌트, 데이터 계층, 뷰 모델을 추가합니다. Google Places API 통합과 non-modal bottom sheet 디자인 시스템을 도입하고, 네비게이션 및 상태 관리를 업데이트합니다. Changes
Sequence Diagram(s)sequenceDiagram
participant User as 사용자
participant AddItineraryScreen as AddItinerary<br/>Screen
participant AddItineraryVM as AddItinerary<br/>ViewModel
participant PlaceRepository as PlaceRepository
participant PlacesAPI as Google Places<br/>API
participant BottomSheet as Bottom Sheet<br/>Components
User->>AddItineraryScreen: 검색창 포커스
AddItineraryScreen->>AddItineraryVM: focusSearch()
AddItineraryVM->>AddItineraryVM: 상태 업데이트<br/>(isSearchFocused)
User->>AddItineraryScreen: 키워드 입력
AddItineraryScreen->>AddItineraryVM: updateKeyword(keyword)
AddItineraryVM->>AddItineraryVM: 디바운스 처리
User->>AddItineraryScreen: 검색 실행
AddItineraryScreen->>AddItineraryVM: searchKeyword()
AddItineraryVM->>PlaceRepository: searchKeyword(keyword, country)
PlaceRepository->>PlaceRepository: SessionToken 초기화
PlaceRepository->>PlacesAPI: FindAutocompletePredictionsRequest
PlacesAPI-->>PlaceRepository: 예측 결과
PlaceRepository-->>AddItineraryVM: SearchKeywordResponse
AddItineraryVM->>AddItineraryVM: 상태 업데이트<br/>(searchResults)
AddItineraryVM-->>AddItineraryScreen: 상태 변경
AddItineraryScreen->>BottomSheet: SearchResultItem<br/>렌더링
User->>AddItineraryScreen: 검색 결과 선택
AddItineraryScreen->>AddItineraryVM: selectSearchResult(result)
AddItineraryVM->>PlaceRepository: getPlace(placeId)
PlaceRepository->>PlacesAPI: FetchPlaceRequest
PlacesAPI-->>PlaceRepository: Place 상세정보
AddItineraryVM->>AddItineraryVM: PlaceInfo 생성 및<br/>사진 로드
AddItineraryVM-->>AddItineraryScreen: 상태 업데이트
AddItineraryScreen->>BottomSheet: SearchedPlaceBottomSheet<br/>렌더링 (expanded)
User->>BottomSheet: AddItinerary 버튼 클릭
BottomSheet->>AddItineraryVM: clickAddItinerary()
AddItineraryVM-->>AddItineraryScreen: NavigateToAddPlace<br/>Side Effect
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 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: 8
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
feature/travel/src/main/java/com/yapp/ndgl/feature/travel/placedetail/PlaceDetailViewModel.kt (1)
27-27:⚠️ Potential issue | 🟠 Major생성자와 Factory 인터페이스의
alternativePlaces타입 불일치 수정 필요Line 27의 생성자 파라미터는
List<RouteAlternativePlace>(non-nullable)이지만, Line 152의 Factory 인터페이스 메서드는List<RouteAlternativePlace>?(nullable)로 선언되어 있습니다. Kotlin에서T와T?는 별개의 타입이며, Line 68의alternativePlaces.map { ... }호출은 null이 아님을 가정하고 있습니다. 같은 패턴의FollowPlaceDetailViewModel에서는 생성자와 Factory 메서드 모두 non-nullable로 올바르게 구현되어 있으므로, 이에 맞춰 수정해야 합니다.Factory 파라미터를 non-nullable로 변경하세요:
수정안
- alternativePlaces: List<RouteAlternativePlace>?, + alternativePlaces: List<RouteAlternativePlace>,🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@feature/travel/src/main/java/com/yapp/ndgl/feature/travel/placedetail/PlaceDetailViewModel.kt` at line 27, The constructor parameter alternativePlaces in PlaceDetailViewModel is non-nullable but the Factory interface method declares it as nullable; update the Factory method signature (the Factory interface that creates PlaceDetailViewModel) to accept alternativePlaces: List<RouteAlternativePlace> (non-nullable) so it matches the `@Assisted` constructor parameter, and then remove or avoid any null-handling around alternativePlaces (e.g., the alternativePlaces.map { ... } call can remain as-is). Ensure the Factory method name and parameter list (Factory.create or similar) uses the non-nullable type to keep constructor and factory consistent.navigation/src/main/java/com/yapp/ndgl/navigation/Route.kt (1)
29-40:⚠️ Potential issue | 🟡 Minor
PlaceDetail과FollowPlaceDetail간alternativePlacesnull 처리 방식이 불일치합니다.
FollowPlaceDetail은 TravelEntry.kt에서alternativePlaces?.map { ... } ?: emptyList()로 항상 non-null 값을 전달하고, 라우트 정의도List<RouteAlternativePlace> = emptyList()입니다. 반면PlaceDetail은 TravelEntry.kt에서 null을 직접 허용하고, 라우트 정의도List<RouteAlternativePlace>? = null입니다. 두 라우트가 유사한 용도로 사용되지만 null 처리 패턴이 다르므로,PlaceDetail도FollowPlaceDetail과 동일하게?: emptyList()패턴을 적용하여 일관성을 맞추는 것이 좋습니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@navigation/src/main/java/com/yapp/ndgl/navigation/Route.kt` around lines 29 - 40, PlaceDetail's alternativePlaces is nullable while FollowPlaceDetail uses a non-null List with default emptyList(), causing inconsistent null handling; change PlaceDetail's declaration (data class PlaceDetail) to make alternativePlaces: List<RouteAlternativePlace> = emptyList() to match FollowPlaceDetail and ensure callers (e.g., mapping in TravelEntry.kt) rely on a non-null list rather than nullable handling.data/travel/src/main/java/com/yapp/ndgl/data/travel/repository/PlaceRepository.kt (1)
47-61:⚠️ Potential issue | 🟡 Minor장소 선택 후 세션 토큰이 자동으로 초기화되지 않습니다
Google Places API에서 세션 토큰은 사용자 검색의 쿼리 및 선택 단계를 단일 세션으로 묶는 역할을 하며, 세션은 사용자가 쿼리 입력을 시작할 때 시작되고 장소를 선택할 때 종료됩니다. 현재
getPlace()메서드는resetSessionToken()을 호출하지 않으므로, 호출자가 이를 직접 관리하지 않으면 이후의 자동완성 요청들이 계속 동일한 세션 토큰을 재사용하게 됩니다.getPlace()내부 또는 그 이후에 토큰을 자동으로 초기화하거나, KDoc으로 호출 계약을 명시해야 합니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@data/travel/src/main/java/com/yapp/ndgl/data/travel/repository/PlaceRepository.kt` around lines 47 - 61, getPlace() currently never clears the Google Places session token, so calls reuse sessionToken; modify getPlace(googlePlaceId: String) to call resetSessionToken() after a successful place retrieval (both when placeApi.getPlaceDetail(googlePlaceId).getData() succeeds and when you recover via placeApi.savePlace(SavePlaceRequest(googlePlaceId)).getData()), e.g. ensure resetSessionToken() is invoked in the success path or a finally-equivalent flow so the token is cleared after selection; alternatively add KDoc on getPlace/resetSessionToken to require callers to manage the token if you prefer manual control.
🟡 Minor comments (17)
feature/travel/src/main/java/com/yapp/ndgl/feature/travel/additinerary/component/SearchComponents.kt-88-124 (1)
88-124:⚠️ Potential issue | 🟡 Minor
SearchResultItem: 수직 패딩 누락 및 trailing 아이콘 크기 미지정두 가지 레이아웃 이슈가 있습니다:
- 수직 패딩 없음 (Line 91):
padding(horizontal = 24.dp)만 적용되어 있어, 리스트에서 아이템 간 간격이 지나치게 좁아질 수 있습니다.- trailing 아이콘 크기 미지정 (Lines 119-123): 화살표 아이콘에
Modifier.size()가 없어 기본 크기로 렌더링됩니다. 디자인 명세에 맞는 명시적 크기 지정이 필요합니다.🐛 수정 제안
Row( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 24.dp) + .padding(horizontal = 24.dp, vertical = 12.dp) .noRippleClickable { onClick() }, verticalAlignment = Alignment.CenterVertically, ) {Icon( imageVector = ImageVector.vectorResource(R.drawable.ic_24_arrow_up_right), contentDescription = null, tint = NDGLTheme.colors.black400, + modifier = Modifier.size(24.dp), )🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@feature/travel/src/main/java/com/yapp/ndgl/feature/travel/additinerary/component/SearchComponents.kt` around lines 88 - 124, The Row for the search result should include vertical padding and the trailing arrow Icon needs an explicit size: update the Row's Modifier.padding to include a vertical value (e.g., padding(horizontal = 24.dp, vertical = <appropriate dp>)) to provide item spacing, and add a Modifier.size(<appropriate dp>) to the trailing Icon (the ImageVector.vectorResource(R.drawable.ic_24_arrow_up_right) Icon) so the arrow renders at the design-specified size; adjust values to match design tokens used elsewhere (e.g., 20.dp or 24.dp).feature/travel/src/main/java/com/yapp/ndgl/feature/travel/additinerary/component/SearchComponents.kt-36-80 (1)
36-80:⚠️ Potential issue | 🟡 Minor두 빈 상태 컴포넌트 모두 수직 중앙 정렬이 누락되어 있습니다
SearchEmptyContent와NoSearchResultContent모두fillMaxSize()를 사용하지만, 수직 방향으로 콘텐츠를 중앙에 배치하는 설정이 없습니다. 결과적으로 아이콘과 텍스트가 화면 상단에 붙어 렌더링됩니다.
SearchEmptyContent:Arrangement.spacedBy(16.dp)→Arrangement.spacedBy(16.dp, Alignment.CenterVertically)NoSearchResultContent:verticalArrangement = Arrangement.Center추가🐛 수정 제안
`@Composable` internal fun SearchEmptyContent() { Column( modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterVertically), ) {`@Composable` internal fun NoSearchResultContent() { Column( modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, ) {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@feature/travel/src/main/java/com/yapp/ndgl/feature/travel/additinerary/component/SearchComponents.kt` around lines 36 - 80, SearchEmptyContent과 NoSearchResultContent에서 Column이 수직 중앙 정렬되지 않아 화면 상단에 붙어 표시됩니다; SearchEmptyContent의 Column에서 Arrangement.spacedBy(16.dp)를 Arrangement.spacedBy(16.dp, Alignment.CenterVertically)로 바꾸고, NoSearchResultContent의 Column에 verticalArrangement = Arrangement.Center를 추가하여 두 컴포넌트 내 콘텐츠가 수직으로 중앙에 배치되도록 수정하세요 (참조: 함수 이름 SearchEmptyContent, NoSearchResultContent 및 해당 Column 선언).core/ui/src/main/res/drawable/ic_140_serach.xml-44-53 (1)
44-53:⚠️ Potential issue | 🟡 Minor
strokeAlpha단독 사용 —strokeColor/strokeWidth미정의로 인해 무효 속성두 path(Lines 44-48, 49-53) 모두
android:strokeAlpha="0.5"를 선언하고 있으나android:strokeColor와android:strokeWidth가 없습니다. Android VectorDrawable은 스트로크가 정의되지 않은 상태에서strokeAlpha를 무시하므로, 해당 속성은 현재 아무 효과도 없는 dead code입니다. 의도가 스트로크를 그리는 것이었다면 strokeColor/strokeWidth를 추가하고, 그렇지 않다면 strokeAlpha를 제거하세요.🛠️ strokeAlpha 제거 제안 (스트로크 의도가 없을 경우)
<path android:pathData="M70.44,50.35C61.86,53.95 56.53,65.45 60.38,74.6C64.22,83.76 75.79,87.1 84.37,83.5C92.94,79.9 98.78,68.22 94.94,59.07C91.09,49.91 79.02,46.75 70.44,50.35Z" - android:strokeAlpha="0.5" android:fillColor="#F6F6F6" android:fillAlpha="0.5"/> <path android:pathData="M77.11,85.15C74.89,85.15 ..." - android:strokeAlpha="0.5" android:fillColor="#F6F6F6" android:fillAlpha="0.5"/>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@core/ui/src/main/res/drawable/ic_140_serach.xml` around lines 44 - 53, The two <path> elements (identify by their android:pathData values "M70.44,50.35C61.86,53.95 56.53,65.45 60.38,74.6C64.22,83.76 75.79,87.1 84.37,83.5C92.94,79.9 98.78,68.22 94.94,59.07C91.09,49.91 79.02,46.75 70.44,50.35Z" and "M77.11,85.15C74.89,85.15 72.65,84.79 70.52,84.05...") declare android:strokeAlpha="0.5" without any strokeColor or strokeWidth, so strokeAlpha is ignored; either remove the strokeAlpha attributes from those <path> elements if no stroke was intended, or add explicit android:strokeColor and android:strokeWidth to actually render a semi-transparent stroke (set strokeColor with desired color and strokeWidth in dp), then remove or keep strokeAlpha as appropriate.core/ui/src/main/res/drawable/ic_140_serach.xml-1-5 (1)
1-5:⚠️ Potential issue | 🟡 Minor파일명 오타 수정 필요:
serach→search파일명이
ic_140_serach.xml로 되어 있어 이미 코드베이스 2곳에서R.drawable.ic_140_serach로 참조되고 있습니다 (feature/travel/src/main/java/com/yapp/ndgl/feature/travel/additinerary/component/SearchComponents.kt42줄, 62줄). 머지 전에 파일명을ic_140_search.xml로 수정하고 관련 참조도 함께 변경해야 합니다. 머지 후 수정 시 모든 참조 변경이 필요한 breaking change가 발생합니다.추가로 46줄과 51줄의
strokeAlpha="0.5"속성은strokeColor와strokeWidth가 없어 Android에서 무시되므로 제거하는 것이 좋습니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@core/ui/src/main/res/drawable/ic_140_serach.xml` around lines 1 - 5, Rename the drawable file ic_140_serach.xml to ic_140_search.xml and update all usages of R.drawable.ic_140_serach to R.drawable.ic_140_search (e.g., the references in SearchComponents.kt) to avoid broken resource lookups; also remove the redundant strokeAlpha="0.5" attributes from the vector drawable (they have no effect without strokeColor/strokeWidth) so the XML contains only valid stroke-related attributes.feature/travel/src/main/java/com/yapp/ndgl/feature/travel/additinerary/AddItineraryViewModel.kt-195-203 (1)
195-203:⚠️ Potential issue | 🟡 Minor
reduce블록 내에서state.value대신this(현재 상태)를 사용해야 합니다.Line 199에서
state.value.searchResults를 사용하고 있지만,reduce람다의 리시버(this)는 현재 업데이트 대상인 상태 객체입니다.state.value는 동시 업데이트가 있을 경우 이미 변경된 값을 읽을 수 있어 불일치가 발생할 수 있습니다.🔧 수정 제안
private fun focusSearch() { reduce { copy( isSearchFocused = true, - isSearched = state.value.searchResults.isNotEmpty(), + isSearched = searchResults.isNotEmpty(), selectedPlaceDetail = null, ) } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@feature/travel/src/main/java/com/yapp/ndgl/feature/travel/additinerary/AddItineraryViewModel.kt` around lines 195 - 203, In focusSearch(), the reduce lambda must read the current state's fields via the lambda receiver instead of accessing state.value; replace state.value.searchResults.isNotEmpty() with this.searchResults.isNotEmpty() (or just searchResults.isNotEmpty()) so the copy(...) sets isSearched based on the receiver state, keeping isSearchFocused and selectedPlaceDetail updates the same.feature/travel/src/main/java/com/yapp/ndgl/feature/travel/additinerary/AddItineraryContract.kt-16-51 (1)
16-51:⚠️ Potential issue | 🟡 Minor검색 상태 파생 프로퍼티에 UI 갭이 있습니다.
showSearchEmptyArea는isSearchFocused && searchResults.isEmpty()로 정의되어 있으므로, 사용자가 검색어를 입력한 순간 true가 됩니다. 그러나 API 응답 전까지는:
showSearchEmptyContent= false (keyword.isNotEmpty()이므로)showNoSearchResultContent= false (isSearched = false이므로)이 때문에 AddItineraryScreen.kt의 138-143번 줄에서 Box 내부에 렌더링될 콘텐츠가 없어 빈 화면이 표시됩니다. 300ms 지연이 있지만 사용자 경험상 입력 중 빈 영역이 깜박일 수 있으므로, 로딩 상태 추가 또는 상태 조건 재정의를 고려해주세요.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@feature/travel/src/main/java/com/yapp/ndgl/feature/travel/additinerary/AddItineraryContract.kt` around lines 16 - 51, The derived UI shows an empty box during typing because showSearchEmptyArea is true as soon as isSearchFocused even before API returns; update AddItineraryState to either add a search-loading flag (e.g., isSearchLoading: Boolean) and exclude the empty area while loading, or simplest: require isSearched for showSearchEmptyArea (change its getter to isSearchFocused && isSearched && searchResults.isEmpty()), and adjust showSearchEmptyContent/showNoSearchResultContent logic accordingly so the Box only renders content when results/state are settled; update callers that set isSearched or set the new flag (e.g., where search starts/finishes) to reflect loading.feature/travel/src/main/java/com/yapp/ndgl/feature/travel/additinerary/component/AddItineraryTopBar.kt-82-90 (1)
82-90:⚠️ Potential issue | 🟡 Minor인터랙티브 아이콘에 접근성
contentDescription이 누락되어 있습니다.뒤로 가기 버튼(Line 84)과 검색 아이콘(Line 155) 모두 클릭 가능한 요소이지만
contentDescription = null로 설정되어 있어 TalkBack 사용자가 해당 버튼의 역할을 파악할 수 없습니다.♿ 수정 제안
Icon( imageVector = ImageVector.vectorResource(R.drawable.ic_28_chevron_left), - contentDescription = null, + contentDescription = stringResource(R.string.content_description_back), tint = NDGLTheme.colors.black600, ... )Icon( imageVector = ImageVector.vectorResource(R.drawable.ic_28_search), - contentDescription = null, + contentDescription = stringResource(R.string.content_description_search), tint = NDGLTheme.colors.black600, ... )Also applies to: 153-163
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@feature/travel/src/main/java/com/yapp/ndgl/feature/travel/additinerary/component/AddItineraryTopBar.kt` around lines 82 - 90, The interactive Icon composables in AddItineraryTopBar (the back button that calls clickBackButton() and the search icon) are missing accessible descriptions; replace contentDescription = null with meaningful descriptions using string resources via stringResource(...) (e.g., R.string.back or R.string.search) so TalkBack can announce their purpose, and add the necessary import for androidx.compose.ui.res.stringResource; ensure you update both the back Icon and the search Icon (the ones invoking clickBackButton() and the search click handler) to use these stringResource values.feature/travel/src/main/java/com/yapp/ndgl/feature/travel/additinerary/component/AddItineraryTopBar.kt-142-150 (1)
142-150:⚠️ Potential issue | 🟡 Minorplaceholder 노출 조건이
keyword대신textFieldValue.text를 기준으로 해야 합니다.
onValueChange에서textFieldValue는 동기적으로 업데이트되지만,updateKeyword(newValue.text)는 ViewModel을 통해 비동기로 전파됩니다. 첫 글자를 입력하는 순간,textFieldValue.text = "a"임에도keyword가 아직""인 중간 recompose 시점에 placeholder가 잠깐 표시될 수 있습니다.🐛 수정 제안
decorationBox = { innerTextField -> - if (keyword.isEmpty()) { + if (textFieldValue.text.isEmpty()) { Text( text = stringResource(R.string.add_itinerary_search_placeholder), style = NDGLTheme.typography.bodyLgRegular, color = NDGLTheme.colors.black400, ) } innerTextField() },🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@feature/travel/src/main/java/com/yapp/ndgl/feature/travel/additinerary/component/AddItineraryTopBar.kt` around lines 142 - 150, The placeholder visibility in AddItineraryTopBar's decorationBox should be based on the local TextField state instead of the ViewModel keyword to avoid flicker; change the condition from using keyword to using textFieldValue.text (the value updated synchronously in onValueChange) so the placeholder is shown only when textFieldValue.text.isEmpty(); update the check inside decorationBox (where innerTextField() is invoked) and keep onValueChange calling updateKeyword(newValue.text) as-is.feature/travel/src/main/java/com/yapp/ndgl/feature/travel/additinerary/component/AddItineraryTopBar.kt-57-61 (1)
57-61:⚠️ Potential issue | 🟡 Minor
keyword동기화 시 커서 위치(selection)를 함께 리셋해야 합니다.
copy(text = keyword)는 기존selection을 그대로 유지합니다. 예를 들어 사용자가 "hello"를 입력해TextRange(5)가 된 상태에서 ViewModel이keyword를""로 초기화하면,TextFieldValue("", selection = TextRange(5))처럼 텍스트 범위를 벗어난 selection이 생성됩니다. Compose가 내부적으로 클램핑하더라도, 명시적으로 selection을 재설정하는 것이 안전합니다.🐛 수정 제안
LaunchedEffect(keyword) { if (textFieldValue.text != keyword) { - textFieldValue = textFieldValue.copy(text = keyword) + textFieldValue = textFieldValue.copy( + text = keyword, + selection = TextRange(keyword.length), + ) } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@feature/travel/src/main/java/com/yapp/ndgl/feature/travel/additinerary/component/AddItineraryTopBar.kt` around lines 57 - 61, When syncing keyword inside AddItineraryTopBar's LaunchedEffect(keyword), avoid copying only text into textFieldValue because copy(text = keyword) preserves the old selection; instead reset the selection to a valid position (e.g., collapsed at keyword.length) when you update textFieldValue so selection won't point outside the new text. Update the LaunchedEffect block that compares textFieldValue.text and keyword to assign textFieldValue with both text and a safe selection (use TextRange(keyword.length) or equivalent) rather than only copy(text = keyword).feature/travel/src/main/java/com/yapp/ndgl/feature/travel/addplace/component/AddPlacePhotoTab.kt-40-42 (1)
40-42:⚠️ Potential issue | 🟡 Minor
photo.aspectRatio가 0 또는 무한대일 경우 런타임 크래시 발생 가능성Compose의
aspectRatio()modifier는 양수 값을 요구합니다. API 응답의widthPx또는heightPx가 0이면aspectRatio가 0 또는 무한대가 되어IllegalArgumentException이 발생합니다.PlacePhoto 생성 시(AddPlaceViewModel.kt:80, AddItineraryViewModel.kt:274 등) 유효성 검사를 추가하거나,
aspectRatio계산에서 기본값을 제공해야 합니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@feature/travel/src/main/java/com/yapp/ndgl/feature/travel/addplace/component/AddPlacePhotoTab.kt` around lines 40 - 42, The aspectRatio modifier in AddPlacePhotoTab.kt uses photo.aspectRatio which can be 0 or infinite if widthPx/heightPx are 0, causing a runtime crash; fix by ensuring a positive finite aspect ratio before passing to Modifier.aspectRatio—compute a safeAspectRatio (e.g., if photo.widthPx>0 and photo.heightPx>0 then widthPx.toFloat()/heightPx else a default like 1f) and use that in Modifier.aspectRatio(safeAspectRatio); additionally add validation when constructing PlacePhoto in AddPlaceViewModel and AddItineraryViewModel to prevent widthPx/heightPx from being zero or to fallback to the default aspect ratio.core/ui/src/main/res/values/strings.xml-116-124 (1)
116-124:⚠️ Potential issue | 🟡 Minor사용되지 않는 문자열 리소스를 제거하세요.
다음 문자열이 코드에서 참조되지 않습니다:
add_itinerary_chip_day_format(Line 119)add_itinerary_chip_recently_saved(Line 120)실제로 사용되는 키는
add_itinerary_chip_recommended와add_itinerary_chip_recent입니다. 사용되지 않는 리소스를 정리하세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@core/ui/src/main/res/values/strings.xml` around lines 116 - 124, Remove the two unused string resources add_itinerary_chip_day_format and add_itinerary_chip_recently_saved from strings.xml; keep the actual used keys add_itinerary_chip_recommended and add_itinerary_chip_recent, and verify no code references remain (search for add_itinerary_chip_day_format and add_itinerary_chip_recently_saved before committing).feature/travel/src/main/java/com/yapp/ndgl/feature/travel/addplace/component/AddPlaceTabRow.kt-32-38 (1)
32-38:⚠️ Potential issue | 🟡 Minor
SecondaryTabRow는 이 프로젝트의 Compose BOM 버전(2026.01.00)에서@OptIn(ExperimentalMaterial3Api::class)어노테이션이 필요합니다.함수 또는 파일 레벨에
@OptIn(ExperimentalMaterial3Api::class)를 추가하세요. 다른 TabRow 관련 컴포넌트들(SearchedPlaceBottomSheet, AddItineraryBottomSheet 등)에서도 동일한 패턴으로 사용하고 있습니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@feature/travel/src/main/java/com/yapp/ndgl/feature/travel/addplace/component/AddPlaceTabRow.kt` around lines 32 - 38, Add the `@OptIn`(ExperimentalMaterial3Api::class) annotation at the top of the composable or file that contains SecondaryTabRow usage so the call to SecondaryTabRow(selectedTabIndex = selectedIndex, modifier = Modifier.fillMaxWidth(), ...) compiles under the project Compose BOM; you can annotate the composable function that renders this UI (or the file) just like other TabRow components (e.g., SearchedPlaceBottomSheet, AddItineraryBottomSheet) to keep the pattern consistent.feature/travel/src/main/java/com/yapp/ndgl/feature/travel/additinerary/component/AddItineraryBottomSheet.kt-131-131 (1)
131-131:⚠️ Potential issue | 🟡 Minor
sheetState.anchoredDraggableState.offset을 Spacer 높이로 사용 시 NaN 크래시 가능성.
offset이NaN일 경우toDp()가NaNdp를 반환하며,Modifier.height(NaN.dp)는 예측 불가능한 동작을 유발합니다. 방어적으로 처리해주세요.🛡️ NaN 방어 처리 제안
- Spacer(modifier = Modifier.height(with(density) { sheetState.anchoredDraggableState.offset.toDp() })) + val offsetDp = with(density) { + val offset = sheetState.anchoredDraggableState.offset + if (offset.isNaN() || offset < 0f) 0.dp else offset.toDp() + } + Spacer(modifier = Modifier.height(offsetDp))🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@feature/travel/src/main/java/com/yapp/ndgl/feature/travel/additinerary/component/AddItineraryBottomSheet.kt` at line 131, In AddItineraryBottomSheet.kt, guard against NaN when using sheetState.anchoredDraggableState.offset for the Spacer height: compute a finite fallback (e.g., 0f) if offset.isNaN() or !isFinite() and then convert that finite float to Dp with with(density) { ...toDp() }; replace direct use of sheetState.anchoredDraggableState.offset in Spacer(modifier = Modifier.height(...)) with the validated/coerced value so Modifier.height never receives NaN.dp.feature/travel/src/main/java/com/yapp/ndgl/feature/travel/additinerary/component/SearchedPlaceBottomSheet.kt-256-256 (1)
256-256:⚠️ Potential issue | 🟡 Minor
anchoredDraggableState.offset이 초기화 전에NaN일 수 있습니다.
AnchoredDraggableState의offset은 앵커가 아직 설정되기 전에Float.NaN을 반환할 수 있습니다. 이 경우NaN.toDp()를 사용하면 레이아웃에서 예기치 않은 동작이 발생할 수 있습니다.🛡️ 제안하는 수정
- Spacer(modifier = Modifier.height(with(density) { sheetState.anchoredDraggableState.offset.toDp() })) + val offsetDp = with(density) { + val offset = sheetState.anchoredDraggableState.offset + if (offset.isNaN()) 0.dp else offset.toDp() + } + Spacer(modifier = Modifier.height(offsetDp.coerceAtLeast(0.dp)))🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@feature/travel/src/main/java/com/yapp/ndgl/feature/travel/additinerary/component/SearchedPlaceBottomSheet.kt` at line 256, The Spacer uses sheetState.anchoredDraggableState.offset directly and that offset may be Float.NaN before anchors are set; guard against NaN/Infinite before calling toDp by reading sheetState.anchoredDraggableState.offset into a local, check offset.isFinite() (or !offset.isNaN()) and only convert to Dp when finite, otherwise use a safe fallback (e.g., 0.dp or a minimum value); update the Spacer to use that safe Dp value so NaN.toDp() is never called (references: SearchedPlaceBottomSheet, sheetState, anchoredDraggableState.offset, Spacer).feature/travel/src/main/java/com/yapp/ndgl/feature/travel/additinerary/component/SearchedPlaceBottomSheet.kt-439-471 (1)
439-471:⚠️ Potential issue | 🟡 Minor클릭 핸들러가 두 번 호출될 수 있습니다.
SearchedPlaceInfoRow에서onClick이 null이 아닌 경우, 행 전체에noRippleClickable(라인 448)이 적용되고, 동시에 우측 chevron 아이콘에도 별도의clickable(라인 464)이 적용됩니다. 사용자가 chevron 아이콘을 탭하면 이벤트 버블링으로 인해onClick이 두 번 호출될 수 있습니다.🐛 제안하는 수정
chevron 아이콘의 별도 클릭 핸들러를 제거하거나, 행 전체의 클릭을 제거하고 chevron만 클릭 가능하게 하세요:
if (onClick != null) { Icon( modifier = Modifier - .size(24.dp) - .clip(CircleShape) - .clickable { onClick() }, + .size(24.dp), imageVector = ImageVector.vectorResource(R.drawable.ic_24_chevron_right), tint = NDGLTheme.colors.black600, contentDescription = null, ) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@feature/travel/src/main/java/com/yapp/ndgl/feature/travel/additinerary/component/SearchedPlaceBottomSheet.kt` around lines 439 - 471, SearchedPlaceInfoRow applies onClick both to the whole Row (via noRippleClickable) and again to the chevron Icon (clickable), which can cause the handler to fire twice; fix by removing the chevron's separate clickable when onClick is provided (or alternatively remove the Row-level noRippleClickable and keep only the chevron clickable) so that onClick is only invoked once—update the logic around noRippleClickable, the onClick parameter, and the chevron Icon to ensure only a single click target invokes onClick.data/travel/src/main/java/com/yapp/ndgl/data/travel/di/TravelNetworkModule.kt-27-29 (1)
27-29:⚠️ Potential issue | 🟡 Minor빈 API 키로 초기화 시 런타임에서야 실패합니다
BuildConfig.PLACE_API_KEY가 빈 문자열인 경우(예: CI/CD 환경 또는local.properties키 누락),Places.initializeWithNewPlacesApiEnabled는 초기화를 성공적으로 완료하지만 실제 API 호출 시점에 인증 오류가 발생합니다. 앱 시작 시 즉시 실패하도록 검증을 추가하면 디버깅이 훨씬 쉬워집니다.🛡️ 빠른 실패(fail-fast) 검증 추가 제안
if (!Places.isInitialized()) { + require(BuildConfig.PLACE_API_KEY.isNotEmpty()) { + "PLACE_API_KEY is not configured. Check local.properties." + } Places.initializeWithNewPlacesApiEnabled(context, BuildConfig.PLACE_API_KEY) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@data/travel/src/main/java/com/yapp/ndgl/data/travel/di/TravelNetworkModule.kt` around lines 27 - 29, Before calling Places.initializeWithNewPlacesApiEnabled, validate BuildConfig.PLACE_API_KEY is non-blank and fail-fast if it is empty: check BuildConfig.PLACE_API_KEY (used where Places.isInitialized() and Places.initializeWithNewPlacesApiEnabled are called) and if blank, log an explicit error (or throw an IllegalStateException) with a clear message about the missing Places API key so initialization does not silently succeed and later API calls fail; keep the existing Places.isInitialized() guard and only call initializeWithNewPlacesApiEnabled when the key is valid.data/travel/src/main/java/com/yapp/ndgl/data/travel/repository/PlaceRepository.kt-23-28 (1)
23-28:⚠️ Potential issue | 🟡 Minor
sessionToken의 동시 접근이 스레드 안전하지 않습니다
sessionToken은@Volatile없이 선언된var이며, suspend 함수에서 null 체크 후 할당하는 패턴이 원자적이지 않습니다. 두 코루틴이 동시에searchKeyword()를 호출하면 둘 다null을 보고 각자 다른 토큰을 생성하여 청구 세션이 분리될 수 있습니다.@Volatile을 추가하거나, lazy initialization에synchronized를 사용하세요.🔒 `@Volatile` 추가 제안
- private var sessionToken: AutocompleteSessionToken? = null + `@Volatile` + private var sessionToken: AutocompleteSessionToken? = null🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@data/travel/src/main/java/com/yapp/ndgl/data/travel/repository/PlaceRepository.kt` around lines 23 - 28, The sessionToken field in PlaceRepository is not thread-safe: multiple coroutines calling searchKeyword(...) can observe null and create multiple AutocompleteSessionToken instances; make sessionToken volatile (add `@Volatile` to the var sessionToken: AutocompleteSessionToken? declaration) or perform a synchronized/lazy initialization inside searchKeyword (wrap the null-check and assignment around AutocompleteSessionToken.newInstance() in a synchronized block or use Kotlin's lazy/threadSafe construct) so only one token is created and shared safely across coroutines.
f14bccc to
dc2d99d
Compare
dc2d99d to
d662ade
Compare
개요
디자인
https://www.figma.com/design/qHn9o58ENLeHjiBWNuZFJx/Design_-YAPP-1%ED%8C%80-?node-id=2384-22520&t=eyU74Nuy00pF2YLK-0
영상
default.mp4
변경사항
참고 문서
추후 작업사항
테스트 체크 리스트