Skip to content

[NDGL-98] 내 여행 관련 API 연동#34

Merged
mj010504 merged 20 commits into
developfrom
feature/NDGL-98
Feb 23, 2026
Merged

[NDGL-98] 내 여행 관련 API 연동#34
mj010504 merged 20 commits into
developfrom
feature/NDGL-98

Conversation

@mj010504
Copy link
Copy Markdown
Contributor

@mj010504 mj010504 commented Feb 23, 2026

개요

  • 내 여행 관련 API 연동

변경사항

  • 내 여행 생성 API 연동
  • 내 여행 일정 조회 API 연동
  • 내 여행 상단 카드 조회 API 연동
  • 내 여행 일정 전체 수정 API 연동(해당 API 수정 예정)
  • 일정 추가하기 시 UI에 표시(Event-Bus 패턴 - SharedFlow 활용)
  • 두 장소간의 교통수단 찾는 API 연동(Google Routes API)
  • 교통수단 리스트 추가
  • Navigator에 navigateAndPopUpTo() 함수 추가
  • NDGLModal에 dismissOnBackPress, dismissOnClickOutside 속성 추가
  • 홈 화면 및 내 여행 탭 화면에서 FollowTravelScreen, TravelDetailScreen 연결
  • 앱 아이콘 배경색 수정

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • 여행 템플릿으로부터 여행 생성 기능 추가
    • 다양한 교통 수단 옵션 확대 (도보, 자동차, 대중교통, 자전거, 택시, 이륜차, 페리, 항공)
    • 여행 일정별 장소 추가 및 경로 계산 기능
    • 출발 시간 설정 기능 개선
  • UI/UX 개선

    • 새로운 교통 수단 아이콘 추가
    • 색상 테마 업데이트 (녹색 강조)
    • 모달 및 시간 설정 인터페이스 개선

@mj010504
Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 23, 2026

✅ Actions performed

Review triggered.

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

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 23, 2026

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

이 PR은 여행 계획 앱의 여행 생성, 일정 수정 및 경로 계산 기능을 추가합니다. 템플릿 기반 여행 생성, 다중 날짜 일정 관리, 장소 추가 시 경로 계산, 시간대별 교통편 제안 기능을 구현하고 관련 데이터 모델 및 API를 확장합니다.

Changes

Cohort / File(s) Summary
리소스 및 테마 업데이트
app/src/main/res/values/ic_launcher_background.xml, core/ui/src/main/res/values/strings.xml, core/ui/src/main/res/drawable/ic_24_*.xml
앱 아이콘 배경색을 업데이트하고, 교통 수단 문자열(walking, driving, transit 등)을 확장하며, 8가지 새로운 교통 수단 아이콘 리소스를 추가합니다.
UI 컴포넌트 확장
core/ui/src/main/java/com/yapp/ndgl/core/ui/designsystem/NDGLModal.kt
NDGLModal에 modifier, description, dismissOnBackPress, dismissOnClickOutside 파라미터를 추가하여 대화상자 커스터마이징 옵션을 확장합니다.
유틸리티 함수 추가/제거
core/util/src/main/java/com/yapp/ndgl/core/util/CountryCodeUtil.kt, core/util/src/main/java/com/yapp/ndgl/core/util/DurationUtil.kt
CountryCodeUtil의 toCountryName 함수를 제거하고, DurationUtil에 시간 문자열 파싱 및 포맷팅 함수를 추가합니다.
네트워크 계층 확장
data/core/src/main/java/com/yapp/ndgl/data/core/di/NetworkModule.kt, data/core/src/main/java/com/yapp/ndgl/data/core/interceptor/RouteInterceptor.kt
RouteInterceptor 및 RouteClient를 도입하여 경로 API 요청에 필요한 헤더(API 키, Content-Type)를 추가합니다.
여행 API 인터페이스
data/travel/src/main/java/com/yapp/ndgl/data/travel/api/RouteApi.kt, data/travel/src/main/java/com/yapp/ndgl/data/travel/api/TravelTemplateApi.kt, data/travel/src/main/java/com/yapp/ndgl/data/travel/api/UserTravelApi.kt
경로 계산 API와 템플릿 기반 여행 생성, 사용자 여행 조회 및 수정 엔드포인트를 추가합니다.
여행 데이터 모델
data/travel/src/main/java/com/yapp/ndgl/data/travel/model/*.kt
ComputeRoutesRequest/Response, TravelMode, CreateTravelFromTemplateRequest/Response, BulkUpdateStartTimeRequest, UpdateItineraryRequest, UserTravelTemplateContentInfo, UserTravelTemplateItinerary, AddPlaceEvent 등 새로운 데이터 클래스를 추가합니다.
예외 및 리포지토리 업그레이드
data/travel/src/main/java/com/yapp/ndgl/data/travel/exception/DuplicateTravelPeriodException.kt, data/travel/src/main/java/com/yapp/ndgl/data/travel/repository/RouteRepository.kt, data/travel/src/main/java/com/yapp/ndgl/data/travel/repository/TravelTemplateRepository.kt, data/travel/src/main/java/com/yapp/ndgl/data/travel/repository/UserTravelRepository.kt, data/travel/src/main/java/com/yapp/ndgl/data/travel/repository/PlaceRepository.kt
DuplicateTravelPeriodException을 추가하고, RouteRepository를 신규 작성하며, 템플릿 여행 생성, 사용자 여행 조회/수정, AddPlaceEvent 스트림 등의 기능을 리포지토리에 추가합니다.
DI 모듈 확장
data/travel/src/main/java/com/yapp/ndgl/data/travel/di/TravelNetworkModule.kt, data/travel/build.gradle.kts
RouteApi DI 설정 및 ROUTE_API_KEY 빌드 설정을 추가합니다.
홈 화면 네비게이션
feature/home/src/main/java/com/yapp/ndgl/feature/home/main/HomeContract.kt, feature/home/src/main/java/com/yapp/ndgl/feature/home/main/HomeScreen.kt, feature/home/src/main/java/com/yapp/ndgl/feature/home/main/HomeViewModel.kt, feature/home/src/main/java/com/yapp/ndgl/feature/home/main/PopularTravelSection.kt, feature/home/src/main/java/com/yapp/ndgl/feature/home/main/RecommendedContentSection.kt, feature/home/src/main/java/com/yapp/ndgl/feature/home/main/UpcomingTravelCardSection.kt, feature/home/src/main/java/com/yapp/ndgl/feature/home/navigation/HomeEntry.kt
여행 클릭 시 travelId와 days 파라미터를 함께 전달하도록 업데이트하고, 나의 여행 카드 클릭 핸들링 및 여행 상세 네비게이션을 추가합니다.
Add Itinerary 기능
feature/travel/src/main/java/com/yapp/ndgl/feature/travel/additinerary/AddItineraryContract.kt, feature/travel/src/main/java/com/yapp/ndgl/feature/travel/additinerary/AddItineraryViewModel.kt
country 파라미터를 countryCode로 이름 변경하고, UserTravelRepository를 주입하여 AddPlaceEvent 발행 기능을 추가합니다.
DatePicker 흐름 개선
feature/travel/src/main/java/com/yapp/ndgl/feature/travel/datepicker/DatePickerContract.kt, feature/travel/src/main/java/com/yapp/ndgl/feature/travel/datepicker/DatePickerScreen.kt, feature/travel/src/main/java/com/yapp/ndgl/feature/travel/datepicker/DatePickerViewModel.kt, feature/travel/src/main/java/com/yapp/ndgl/feature/travel/datepicker/component/CalendarView.kt
DatePicker 상태를 showDialog에서 showDatePickerModal로 변경하고, 템플릿으로부터 여행 생성 로직을 구현하며, 중복 여행 기간 예외 처리를 추가합니다.
Follow Travel 기능
feature/travel/src/main/java/com/yapp/ndgl/feature/travel/followtravel/FollowTravelContract.kt, feature/travel/src/main/java/com/yapp/ndgl/feature/travel/followtravel/FollowTravelScreen.kt, feature/travel/src/main/java/com/yapp/ndgl/feature/travel/followtravel/FollowTravelViewModel.kt, feature/travel/src/main/java/com/yapp/ndgl/feature/travel/followtravel/component/ContentCard.kt
creatorName 필드를 추가하고, countryCode/countryName 처리를 개선하며, 색상 테마를 green500으로 업데이트합니다.
교통 수단 모델
feature/travel/src/main/java/com/yapp/ndgl/feature/travel/model/TransportType.kt, feature/travel/src/main/java/com/yapp/ndgl/feature/travel/model/TravelModeMapper.kt, feature/travel/src/main/java/com/yapp/ndgl/feature/travel/model/PlaceType.kt, feature/travel/src/main/java/com/yapp/ndgl/feature/travel/model/TransportSegment.kt, feature/travel/src/main/java/com/yapp/ndgl/feature/travel/model/AlternativePlace.kt, feature/travel/src/main/java/com/yapp/ndgl/feature/travel/model/ContentInfo.kt
TransportType 열거형을 WALK/CAR/BUS/TRAIN에서 WALKING/DRIVING/TRANSIT/BICYCLING/TAXI/TWO_WHEELER/FERRY/FLIGHT로 확장하고, TravelModeMapper를 추가하며, TransportSegment에 googlePlaceId를 추가합니다.
My Travel 목록 화면
feature/travel/src/main/java/com/yapp/ndgl/feature/travel/mytravel/MyTravelContract.kt, feature/travel/src/main/java/com/yapp/ndgl/feature/travel/mytravel/MyTravelScreen.kt, feature/travel/src/main/java/com/yapp/ndgl/feature/travel/mytravel/MyTravelViewModel.kt, feature/travel/src/main/java/com/yapp/ndgl/feature/travel/mytravel/RecommendedTravelSection.kt, feature/travel/src/main/java/com/yapp/ndgl/feature/travel/mytravel/UpcomingTravelCardSection.kt, feature/travel/src/main/java/com/yapp/ndgl/feature/travel/mytravel/UpcomingTravelListSection.kt
여행 클릭 이벤트에 days 파라미터를 추가하고, 예정된 여행과 진행 중인 여행의 일수 계산을 구현합니다.
Travel Detail 상세 화면
feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/TravelDetailContract.kt, feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/TravelDetailScreen.kt, feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/TravelDetailViewModel.kt, feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/component/TimelineContent.kt, feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/component/TransportBottomSheet.kt, feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/component/TransportSegment.kt, feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/component/ContentCard.kt
days 필드 추가, 비동기 일정/내용 정보 로딩, AddPlaceEvent 구독, 경로 계산 및 교통편 추천, 시간대별 업데이트 등의 기능을 구현합니다.
네비게이션 및 라우팅
feature/travel/src/main/java/com/yapp/ndgl/feature/travel/navigation/TravelEntry.kt, navigation/src/main/java/com/yapp/ndgl/navigation/Route.kt, navigation/src/main/java/com/yapp/ndgl/navigation/Navigator.kt, navigation/src/main/java/com/yapp/ndgl/navigation/model/RouteAlternativePlace.kt
Route 데이터 클래스에 days/templateId 파라미터를 추가하고, 새로운 navigateAndPopUpTo 메서드를 구현하며, countryCode 이름 변경을 적용합니다.
설정 변경
detekt-config.yml
LongMethod 복잡도 임계값을 150에서 180으로 업데이트합니다.

Sequence Diagram(s)

sequenceDiagram
    participant User as 사용자
    participant Home as Home 화면
    participant Template as FollowTravel<br/>(템플릿)
    participant DatePicker as DatePicker<br/>뷰모델
    participant API as TravelTemplate<br/>API
    participant Repo as TravelTemplate<br/>리포지토리
    participant Detail as TravelDetail<br/>화면

    User->>Home: 여행 템플릿 클릭
    Home->>Template: 템플릿 표시
    User->>Template: 날짜 선택 후 확인
    Template->>DatePicker: 템플릿 ID, 날짜 전달
    DatePicker->>Repo: createTravelFromTemplate 호출
    Repo->>API: CreateTravelFromTemplateRequest 전송
    API-->>Repo: userTravelId 반환
    alt 성공
        Repo-->>DatePicker: CreateTravelFromTemplateResponse
        DatePicker->>Detail: 생성된 여행 ID로 네비게이션
    else 중복 기간
        Repo-->>DatePicker: DuplicateTravelPeriodException
        DatePicker->>User: 오류 메시지 표시
    end
Loading
sequenceDiagram
    participant User as 사용자
    participant Detail as TravelDetail<br/>화면
    participant ViewModel as TravelDetail<br/>뷰모델
    participant Repo as User/Route<br/>리포지토리
    participant API as Route/Place<br/>API
    participant Map as 지도 표시

    User->>Detail: 장소 추가 선택
    Detail->>ViewModel: AddPlace 이벤트 수신
    ViewModel->>ViewModel: 새 장소 생성
    ViewModel->>Repo: 출발지→도착지 경로 계산<br/>(여러 교통 수단)
    Repo->>API: ComputeRoutesRequest<br/>(다중 TravelMode)
    API-->>Repo: RouteInfo 배열 반환
    Repo-->>ViewModel: 최적 경로 선택
    ViewModel->>ViewModel: 일정 시간 조정
    ViewModel->>Repo: updateItinerary 호출
    Repo->>API: UpdateItineraryRequest 전송
    API-->>Repo: 성공 응답
    ViewModel->>Detail: UI 상태 업데이트
    Detail->>Map: 새 장소/경로 표시
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목 'NDGL-98 내 여행 관련 API 연동'은 주요 변경 사항인 '내 여행(My Travel) 관련 API 통합'을 명확하게 요약하고 있으며, 이는 변경 집합의 핵심 내용과 잘 일치합니다.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/NDGL-98

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

❤️ Share

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

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 14

Caution

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

⚠️ Outside diff range comments (2)
feature/travel/src/main/java/com/yapp/ndgl/feature/travel/datepicker/DatePickerScreen.kt (1)

124-137: ⚠️ Potential issue | 🟠 Major

isLoading 상태가 UI에 반영되지 않습니다.

DatePickerStateisLoading이 존재하지만, 완료 버튼의 statusisDateSelected만 확인합니다 (line 130-134). API 호출 중에도 버튼이 활성 상태로 유지되어 DatePickerViewModel에서 지적한 중복 클릭 문제와 연결됩니다.

로딩 중일 때 버튼을 비활성화하거나 로딩 인디케이터를 표시하는 것을 권장합니다.

🐛 로딩 상태 반영 제안
                     NDGLCTAButton(
                         modifier = Modifier
                             .fillMaxWidth(),
                         type = NDGLCTAButtonAttr.Type.PRIMARY,
                         size = NDGLCTAButtonAttr.Size.LARGE,
-                        status = if (state.isDateSelected) {
+                        status = if (state.isDateSelected && !state.isLoading) {
                             NDGLCTAButtonAttr.Status.ACTIVE
                         } else {
                             NDGLCTAButtonAttr.Status.DISABLED
                         },
                         label = stringResource(R.string.date_picker_complete),
                         onClick = onCompleteClick,
                     )
🤖 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/datepicker/DatePickerScreen.kt`
around lines 124 - 137, The CTA button currently uses only
DatePickerState.isDateSelected to determine NDGLCTAButtonAttr.Status, so
isLoading is ignored; update the NDGLCTAButton usage in DatePickerScreen to
consider state.isLoading (and/or state.isDateSelected) when setting the status
so the button becomes DISABLED during loading, and additionally either show a
loading indicator inside NDGLCTAButton or short-circuit onClick
(onCompleteClick) when state.isLoading to prevent duplicate clicks (aligns with
DatePickerViewModel's duplicate-click protection).
feature/travel/src/main/java/com/yapp/ndgl/feature/travel/datepicker/DatePickerViewModel.kt (1)

96-103: ⚠️ Potential issue | 🟠 Major

완료 버튼 중복 클릭 시 여행이 중복 생성될 수 있습니다.

clickCompleteButton()에서 isLoading 상태를 확인하지 않기 때문에, API 호출 중에 사용자가 완료 버튼을 다시 누르면 createTravelFromTemplate이 중복 호출될 수 있습니다. DatePickerScreen.kt에서도 isLoading에 따라 버튼을 비활성화하는 로직이 없습니다.

🐛 ViewModel에서 중복 호출 방지 제안
     private fun clickCompleteButton() {
         val startDate = state.value.startDate
         val endDate = state.value.endDate
 
-        if (startDate != null && endDate != null) {
+        if (startDate != null && endDate != null && !state.value.isLoading) {
             createTravelFromTemplate(startDate, endDate)
         }
     }

Also applies to: 120-151

🤖 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/datepicker/DatePickerViewModel.kt`
around lines 96 - 103, Prevent duplicate travel creation by guarding
clickCompleteButton() with the ViewModel's isLoading flag: in
clickCompleteButton(), return early if state.value.isLoading is true, set
isLoading = true before calling createTravelFromTemplate(startDate, endDate) and
ensure it is reset to false once the API call completes or fails
(createTravelFromTemplate should update isLoading accordingly). Also update
DatePickerScreen to disable the complete button based on state.isLoading so the
UI cannot invoke clickCompleteButton() while a request is in flight.
🧹 Nitpick comments (23)
core/ui/src/main/res/drawable/ic_24_car.xml (1)

12-18: 불필요한 중첩 <group> + <clip-path> 제거 권장

새로 추가된 <group> (12번째 줄)의 <clip-path> 경로 데이터(M24,0H0V24H24V0Z)가 부모 <group><clip-path> (10–11번째 줄)와 완전히 동일합니다. 이미 동일한 사각형으로 클리핑된 영역에 같은 사각형을 다시 클리핑하는 것은 시각적·논리적으로 아무 효과가 없으며, XML 구조만 복잡해집니다. Figma 등 디자인 툴 익스포트 시 자주 발생하는 패턴이므로 불필요한 레이어를 제거하는 것이 좋습니다.

♻️ 불필요한 중첩 그룹 제거 제안
     <group>
       <clip-path
           android:pathData="M24,0H0V24H24V0Z"/>
-      <group>
-        <clip-path
-            android:pathData="M24,0H0V24H24V0Z"/>
-        <path
-            android:pathData="M18.92,6.01C18.72,5.42 18.16,5 17.5,5H6.5C5.84,5 5.29,5.42 5.08,6.01L3,12V20C3,20.55 3.45,21 4,21H5C5.55,21 6,20.55 6,20V19H18V20C18,20.55 18.45,21 19,21H20C20.55,21 21,20.55 21,20V12L18.92,6.01ZM6.5,16C5.67,16 5,15.33 5,14.5C5,13.67 5.67,13 6.5,13C7.33,13 8,13.67 8,14.5C8,15.33 7.33,16 6.5,16ZM17.5,16C16.67,16 16,15.33 16,14.5C16,13.67 16.67,13 17.5,13C18.33,13 19,13.67 19,14.5C19,15.33 18.33,16 17.5,16ZM5,11L6.5,6.5H17.5L19,11H5Z"
-            android:fillColor="#757575"/>
-      </group>
+      <path
+          android:pathData="M18.92,6.01C18.72,5.42 18.16,5 17.5,5H6.5C5.84,5 5.29,5.42 5.08,6.01L3,12V20C3,20.55 3.45,21 4,21H5C5.55,21 6,20.55 6,20V19H18V20C18,20.55 18.45,21 19,21H20C20.55,21 21,20.55 21,20V12L18.92,6.01ZM6.5,16C5.67,16 5,15.33 5,14.5C5,13.67 5.67,13 6.5,13C7.33,13 8,13.67 8,14.5C8,15.33 7.33,16 6.5,16ZM17.5,16C16.67,16 16,15.33 16,14.5C16,13.67 16.67,13 17.5,13C18.33,13 19,13.67 19,14.5C19,15.33 18.33,16 17.5,16ZM5,11L6.5,6.5H17.5L19,11H5Z"
+          android:fillColor="#757575"/>
     </group>

또한 fillColor="#757575" 변경은 PR 전반의 교통 아이콘 그레이톤 통일 작업과 일치하며, 사용 시 tint가 적용되므로 색상 하드코딩 자체는 문제 없습니다. Based on learnings, ic_24_bag.xml 아이콘과 동일하게 이 프로젝트의 아이콘들은 tint로 색상이 제어되므로 fillColor의 일치 여부는 엄격하게 검사하지 않아도 됩니다.

🤖 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_24_car.xml` around lines 12 - 18, The nested
<group> contains a duplicate <clip-path> with
android:pathData="M24,0H0V24H24V0Z" which is redundant; remove that inner
<group> (and its <clip-path>) and place the <path> element directly under the
parent group that already defines the same clip-path so the clip is applied
once, keeping the existing android:fillColor="#757575" on the <path>.
feature/travel/src/main/java/com/yapp/ndgl/feature/travel/mytravel/MyTravelScreen.kt (1)

54-56: 불필요한 이중 괄호 제거

onIntent((...)) 의 외부 괄호가 불필요합니다. Line 46의 ClickTravelDetail 호출과 비교하면 스타일 불일치이며, Detekt의 UnnecessaryParentheses 규칙에도 걸릴 수 있습니다.

✨ 불필요한 괄호 제거 제안
-        onTravelTemplateClick = { travelId, days ->
-            viewModel.onIntent((MyTravelIntent.ClickTravel(travelId = travelId, days = days)))
-        },
+        onTravelTemplateClick = { travelId, days ->
+            viewModel.onIntent(MyTravelIntent.ClickTravel(travelId = travelId, days = days))
+        },
🤖 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/mytravel/MyTravelScreen.kt`
around lines 54 - 56, The call
viewModel.onIntent((MyTravelIntent.ClickTravel(...))) uses unnecessary outer
parentheses; remove the extra parentheses so the invocation is
viewModel.onIntent(MyTravelIntent.ClickTravel(travelId = travelId, days = days))
to match the style used for ClickTravelDetail and avoid Detekt
UnnecessaryParentheses warnings (refer to MyTravelScreen.kt and the
MyTravelIntent.ClickTravel usage).
core/ui/src/main/java/com/yapp/ndgl/core/ui/designsystem/NDGLModal.kt (1)

43-44: modifier 체이닝 순서 검토 권장

Line 44: modifier = modifier.wrapContentHeight()는 외부에서 전달된 modifier 뒤에 wrapContentHeight()를 체이닝합니다. 호출부에서 Modifier.fillMaxSize() 같은 크기 제약 modifier를 전달하면 wrapContentHeight()가 의도대로 동작하지 않을 수 있습니다. 현재 다이얼로그 특성상 문제될 가능성은 낮지만, 명확성을 위해 wrapContentHeight()를 먼저 적용하는 방향을 고려할 수 있습니다.

♻️ 제안 수정
-            modifier = modifier.wrapContentHeight(),
+            modifier = Modifier.wrapContentHeight().then(modifier),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/ui/src/main/java/com/yapp/ndgl/core/ui/designsystem/NDGLModal.kt` around
lines 43 - 44, The current Surface in NDGLModal.kt uses modifier =
modifier.wrapContentHeight(), which chains the incoming modifier before
wrapContentHeight and can be overridden by size-constraining modifiers (e.g.,
fillMaxSize) passed by callers; change the chaining so wrapContentHeight() is
applied first (e.g., wrapContentHeight().then(modifier) or modifier =
Modifier.wrapContentHeight().then(modifier)) to ensure the dialog's height
behavior is applied predictably; update the Surface modifier usage in the
NDGLModal composable accordingly.
core/util/src/main/java/com/yapp/ndgl/core/util/DurationUtil.kt (1)

43-44: 정규식을 매 호출마다 컴파일하고 있습니다 — 상수로 추출하세요.

toRegex()를 함수 본문 내부에서 호출하면 parseTimeStringToDuration이 호출될 때마다 정규식이 재컴파일됩니다. 파일 최상단의 val 상수로 추출하면 불필요한 오버헤드를 제거할 수 있습니다.

♻️ 정규식 상수 추출 제안
+private val TIME_STRING_REGEX = """^(\d+):([0-5]?\d):([0-5]?\d)$""".toRegex()
+
 fun parseTimeStringToDuration(timeString: String?): Duration? {
     if (timeString.isNullOrBlank()) return null

-    val regex = """^(\d+):([0-5]?\d):([0-5]?\d)$""".toRegex()
-    val matchResult = regex.find(timeString) ?: return null
+    val matchResult = TIME_STRING_REGEX.find(timeString) ?: return null
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/util/src/main/java/com/yapp/ndgl/core/util/DurationUtil.kt` around lines
43 - 44, The regex in parseTimeStringToDuration is being recompiled on every
call; extract the pattern """^(\d+):([0-5]?\d):([0-5]?\d)$""" to a top-level
immutable val (e.g., TIME_STRING_REGEX or TIME_STRING_PATTERN) and replace the
local regex = ... .toRegex() with a reference to that constant so
parseTimeStringToDuration uses the precompiled Regex instance instead of
recompiling each invocation.
core/ui/src/main/res/values/strings.xml (1)

39-46: start_time_settingstart_time_setting_title의 중복 값 검토

62번 줄의 start_time_setting 키와 83번 줄의 start_time_setting_title 키가 동일한 문자열값 "여행 시작 시간 설정"을 가지고 있습니다. 두 키의 용도와 필요성을 검토하여 중복을 제거하거나 의도를 명확히 하세요.

🤖 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 39 - 46, 두 키
start_time_setting과 start_time_setting_title이 동일한 문자열 "여행 시작 시간 설정"으로 중복되어 있으니
의도를 명확히 하여 중복을 제거하거나 값/키를 분리하세요; 예를 들어 start_time_setting은 설정 항목의 레이블로,
start_time_setting_title은 해당 화면의 타이틀로 쓰인다면 각각 "시작 시간", "여행 시작 시간 설정"처럼 역할에 맞는 다른
문자열값을 설정하거나 실제로 하나만 사용되는 경우 불필요한 키(start_time_setting 또는
start_time_setting_title)를 삭제하고 리소스 참조를 모두 하나의 키로 통일하도록 수정하세요.
data/travel/src/main/java/com/yapp/ndgl/data/travel/model/BulkUpdateStartTimeRequest.kt (1)

11-14: startTime 포맷을 KDoc 또는 주석으로 명시 권장

startTime: String이 서버에 그대로 전달되는 필드이므로, 기대 포맷(예: "HH:mm", "yyyy-MM-dd'T'HH:mm:ss" 등)을 KDoc 또는 인라인 주석으로 문서화해두면 이후 유지보수 시 오포맷 전달로 인한 API 오류를 방지할 수 있습니다.

📝 포맷 명시 예시
 `@Serializable`
 data class StartTimeUpdateItem(
     val id: Long,
+    /** 시작 시각. 서버 API 포맷: "HH:mm" (예: "09:30") */
     val startTime: String,
 )
🤖 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/model/BulkUpdateStartTimeRequest.kt`
around lines 11 - 14, The StartTimeUpdateItem data class's startTime property
lacks a documented expected string format; update StartTimeUpdateItem (the
startTime field) to include a KDoc or inline comment specifying the exact
expected format (e.g., "HH:mm" or ISO-8601 "yyyy-MM-dd'T'HH:mm:ss"), include any
timezone expectations, and mention validation behavior so callers know what to
send and maintainers know how to parse/validate that field.
feature/travel/src/main/java/com/yapp/ndgl/feature/travel/followtravel/FollowTravelContract.kt (1)

16-16: creatorName 중복 여부 검토 권장

FollowTravelStatecreatorName: String = ""이 추가되었는데, 이미 contentInfo.videoInfo.creatorName으로 동일 정보에 접근 가능합니다. 두 필드가 서로 다른 시점에 채워지거나(예: contentInfo 로드 전에 creatorName이 필요한 경우) 다른 목적(TipContent 등)으로 쓰인다면 유지가 타당하지만, 동일한 값을 이중으로 관리한다면 단일 소스로 통합하는 것이 좋습니다.

🤖 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/followtravel/FollowTravelContract.kt`
at line 16, FollowTravelState currently defines creatorName: String = "" which
duplicates contentInfo.videoInfo.creatorName; inspect usages of
FollowTravelState.creatorName and contentInfo.videoInfo.creatorName to decide
single source of truth: if they always represent the same value, remove
creatorName from FollowTravelState and refactor call sites to read
contentInfo.videoInfo.creatorName (or pass contentInfo earlier), otherwise keep
creatorName but add a comment documenting why it diverges and ensure it is
populated consistently (e.g., initialize in FollowTravelState constructor or
update in the code path that loads contentInfo); reference FollowTravelState,
creatorName, and contentInfo.videoInfo.creatorName when making changes.
data/core/src/main/java/com/yapp/ndgl/data/core/interceptor/RouteInterceptor.kt (2)

10-17: addHeader("Content-Type", ...)는 Retrofit이 자동으로 설정하는 Content-Type과 중복될 수 있습니다.

@Body와 JSON 컨버터 사용 시 Retrofit/OkHttp가 이미 Content-Type: application/json을 설정합니다. addHeader는 기존 헤더를 덮어쓰지 않고 추가하므로, 동일한 헤더가 2개 전송될 수 있습니다. 일부 서버에서는 중복 Content-Type 헤더를 거부할 수 있습니다.

Content-Type 라인을 제거하거나, 꼭 필요하다면 addHeader 대신 .header()(기존 값 교체)를 사용하세요.

🔧 수정 제안
     override fun intercept(chain: Interceptor.Chain): Response {
         val request = chain.request().newBuilder()
-            .addHeader("Content-Type", "application/json")
             .addHeader("X-Goog-Api-Key", apiKey)
             .addHeader("X-Goog-FieldMask", "routes.duration,routes.distanceMeters")
             .build()
         return chain.proceed(request)
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@data/core/src/main/java/com/yapp/ndgl/data/core/interceptor/RouteInterceptor.kt`
around lines 10 - 17, The interceptor RouteInterceptor.intercept currently uses
.addHeader("Content-Type", "application/json") which can duplicate
Retrofit/OkHttp-managed Content-Type headers; remove that addHeader line
entirely or replace it with .header("Content-Type", "application/json") to
overwrite instead of add; keep the other .addHeader calls for "X-Goog-Api-Key"
and "X-Goog-FieldMask" as-is and ensure the modified request building in
intercept uses the chosen approach.

14-14: X-Goog-FieldMask가 인터셉터에 하드코딩되어 있어, 향후 다른 Route API 엔드포인트 추가 시 유연성이 떨어집니다.

현재 하나의 엔드포인트만 사용하므로 당장 문제는 아니지만, 추후 엔드포인트별로 다른 필드가 필요하면 인터셉터 레벨보다 Retrofit @Headers 어노테이션을 활용하는 것이 더 유연합니다.

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

In
`@data/core/src/main/java/com/yapp/ndgl/data/core/interceptor/RouteInterceptor.kt`
at line 14, The interceptor currently hardcodes the "X-Goog-FieldMask" header in
RouteInterceptor (the .addHeader("X-Goog-FieldMask",
"routes.duration,routes.distanceMeters") line), reducing flexibility for future
endpoints; remove that hardcoded addHeader and instead make the header provided
at the Retrofit call level (use Retrofit's `@Headers` on the specific Route API
methods or an `@Header` parameter on functions that need different field masks) so
each endpoint can declare its own fields; update RouteInterceptor to no longer
inject X-Goog-FieldMask and ensure service interfaces (route-related methods)
add the appropriate `@Headers` or `@Header` usage.
data/travel/src/main/java/com/yapp/ndgl/data/travel/model/UpdateItineraryRequest.kt (1)

11-22: memo 필드에 기본값 추가를 고려해 주세요.

distanceKmtransportation에는 = null 기본값이 있지만 memo에는 없습니다. memo도 선택적 필드라면 기본값을 추가하면 호출 측에서 생략할 수 있어 편리합니다.

제안
     `@SerialName`("travelerTip")
-    val memo: String?,
+    val memo: String? = 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/model/UpdateItineraryRequest.kt`
around lines 11 - 22, The ItineraryUpdateItem data class has an optional memo
property declared as val memo: String? but lacks a default value; update the
ItineraryUpdateItem declaration to give memo a default of null (e.g., val memo:
String? = null) so callers can omit it like distanceKm and transportation;
ensure the `@SerialName`("travelerTip") annotation remains on the memo property.
feature/travel/src/main/java/com/yapp/ndgl/feature/travel/datepicker/DatePickerContract.kt (1)

23-24: TODO 주석 확인: 로딩 인디케이터 UI 표시

추후 isLoading 상태에 대한 UI 반영이 필요합니다. 필요하시면 로딩 인디케이터 구현을 도와드릴 수 있습니다.

🤖 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/datepicker/DatePickerContract.kt`
around lines 23 - 24, The TODO notes that the isLoading Boolean in
DatePickerContract is not yet reflected in the UI; update the feature by wiring
that state through your presentation layer: ensure DatePickerContract
(isLoading) is exposed from DatePickerViewModel, emit loading changes during
async operations, and make DatePickerFragment/DatePickerView observe the
ViewModel’s isLoading to show/hide a loading indicator (e.g., ProgressBar) or
remove the TODO if you intentionally plan no UI. Use the symbol names
DatePickerContract.isLoading, DatePickerViewModel (the state producer), and
DatePickerFragment/DatePickerView (the consumers) when making the changes.
feature/travel/src/main/java/com/yapp/ndgl/feature/travel/mytravel/UpcomingTravelCardSection.kt (1)

50-73: days 계산 로직이 두 곳에서 중복됩니다.

UpcomingInProgress 분기에서 동일한 ChronoUnit.DAYS.between(startDate, endDate).toInt() + 1 계산이 반복됩니다. UpcomingTravel sealed class에 공통 프로퍼티 startDateendDate가 정의되어 있으므로, 확장 함수로 추출하거나 when 블록 앞에서 한 번만 계산하면 유지보수가 용이합니다.

예시: 확장 함수로 추출
+private fun MyTravelState.UpcomingTravel.calculateDays(): Int =
+    java.time.temporal.ChronoUnit.DAYS.between(startDate, endDate).toInt() + 1

 `@Composable`
 internal fun UpcomingTravelCardSection(
     modifier: Modifier,
     upcomingTravel: UpcomingTravel,
     onTravelClick: (Long, Int) -> Unit,
     onPlaceClick: (String) -> Unit,
 ) {
+    val days = upcomingTravel.calculateDays()
     when (upcomingTravel) {
         is UpcomingTravel.Upcoming -> {
-            val days = java.time.temporal.ChronoUnit.DAYS.between(
-                upcomingTravel.startDate,
-                upcomingTravel.endDate,
-            ).toInt() + 1
             UpcomingTravelCard(
                 modifier = modifier,
                 travel = upcomingTravel,
                 onCardClick = { onTravelClick(upcomingTravel.travelId, days) },
             )
         }
         is UpcomingTravel.InProgress -> {
-            val days = java.time.temporal.ChronoUnit.DAYS.between(
-                upcomingTravel.startDate,
-                upcomingTravel.endDate,
-            ).toInt() + 1
             InProgressTravelCard(
                 travel = upcomingTravel,
                 onTravelClick = { travelId -> onTravelClick(travelId, days) },
                 onPlaceClick = onPlaceClick,
             )
         }
     }
 }
🤖 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/mytravel/UpcomingTravelCardSection.kt`
around lines 50 - 73, The days calculation is duplicated in the
UpcomingTravel.Upcoming and UpcomingTravel.InProgress branches; extract it to a
single place by computing it once before the when or adding an extension on
UpcomingTravel (e.g., a function using startDate and endDate to return days =
ChronoUnit.DAYS.between(startDate, endDate).toInt() + 1) and then pass that
single computed value into UpcomingTravelCard and InProgressTravelCard
(references: UpcomingTravel, UpcomingTravel.Upcoming, UpcomingTravel.InProgress,
startDate, endDate, days, UpcomingTravelCard, InProgressTravelCard,
onTravelClick).
feature/travel/src/main/java/com/yapp/ndgl/feature/travel/mytravel/UpcomingTravelListSection.kt (1)

85-88: days 계산 로직을 UI 컴포저블이 아닌 데이터 모델로 이동하는 것을 고려해 주세요.

UpcomingTravel 컴포저블 내부에서 days를 계산하고 있는데, 이 로직은 UpcomingTravelItem 데이터 클래스의 프로퍼티(또는 확장 프로퍼티)로 두는 것이 더 적절합니다. 이렇게 하면 데이터 매핑 계층에서 검증/테스트가 용이하고, 다른 곳에서도 재사용할 수 있습니다.

또한 startDateendDate보다 뒤인 경우 days가 0 이하가 되는 엣지 케이스에 대한 방어 로직이 없습니다.

♻️ 제안

UpcomingTravelItem에 프로퍼티 추가:

data class UpcomingTravelItem(
    // ... existing fields
) {
    val days: Int
        get() = maxOf(
            java.time.temporal.ChronoUnit.DAYS.between(startDate, endDate).toInt() + 1,
            1
        )
}

컴포저블에서는 직접 사용:

-    val days = java.time.temporal.ChronoUnit.DAYS.between(
-        travel.startDate,
-        travel.endDate,
-    ).toInt() + 1
     // ...
-            .clickable(onClick = { onUserTravelClick(travel.travelId, days) })
+            .clickable(onClick = { onUserTravelClick(travel.travelId, travel.days) })
🤖 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/mytravel/UpcomingTravelListSection.kt`
around lines 85 - 88, Move the days calculation out of the UpcomingTravel
composable into the data layer by adding a derived property on
UpcomingTravelItem (e.g., val days) that computes days =
maxOf(ChronoUnit.DAYS.between(startDate, endDate).toInt() + 1, 1); update the
composable to use UpcomingTravelItem.days instead of computing it inline; ensure
the property guards against startDate > endDate by returning at least 1 so edge
cases are handled and the logic is testable and reusable.
feature/travel/src/main/java/com/yapp/ndgl/feature/travel/followtravel/FollowTravelViewModel.kt (1)

38-54: 병렬 일차별 로딩 시 에러 처리가 누락되어 있습니다.

loadTravelTemplateItinerary에서 각 day를 개별 코루틴(launch)으로 로딩하고 있는데, onFailure 블록이 // FIXME 코멘트만 있고 실제 에러 처리가 없습니다. 특정 일차의 로딩이 실패해도 사용자에게 피드백이 없으며, 해당 일차는 빈 Itinerary()로 남게 됩니다.

이 부분은 FollowTravelViewModelTravelDetailViewModel 양쪽 모두 동일한 패턴이므로, 에러 상태를 Itinerary 또는 state에 반영하는 방안을 검토해 주세요.

해당 FIXME 처리를 위한 에러 상태 설계를 도와드릴까요? 새 이슈로 트래킹하시겠습니까?

🤖 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/followtravel/FollowTravelViewModel.kt`
around lines 38 - 54, loadTravelTemplateItinerary currently launches per-day
coroutines but leaves onFailure as a FIXME, so failed days remain as empty
Itinerary() with no user feedback; update the error handling in
loadTravelTemplateItinerary (in FollowTravelViewModel) to reflect per-day errors
in state by setting a distinct error marker for that day—either add fields to
Itinerary (e.g., isError:Boolean and errorMessage:String?) or extend the
ViewModel state with a parallel per-day LoadState list—and in the onFailure
block call reduce to update itineraries[day-1] (or the corresponding LoadState)
with the error info so the UI can render an error view for that specific day;
ensure the change mirrors the same pattern in TravelDetailViewModel for
consistency.
feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/TravelDetailContract.kt (1)

61-75: Itinerary.startTime이 body 프로퍼티이므로 equals()/hashCode()에 포함되지 않습니다.

startTime은 data class의 주 생성자 파라미터가 아닌 body에서 초기화되므로, equals()hashCode()에 포함되지 않습니다. places가 동일하면 startTime도 동일하게 파생되므로 실질적으로 문제가 되진 않지만, 만약 startTime이 별도로 비교되어야 하는 상황이 생기면 주의가 필요합니다. 현재 설계 의도와 맞는다면 문제 없습니다.

🤖 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/traveldetail/TravelDetailContract.kt`
around lines 61 - 75, The startTime property is declared in the class body so
it's excluded from the data class generated equals()/hashCode(); if you need
startTime to participate in equality, move it into the primary constructor
(e.g., add a constructor parameter val startTime: Duration =
places.firstOrNull()?.startTime ?: DEFAULT_START_TIME.hours and remove the
body-backed startTime), otherwise keep the body property but add a clarifying
comment near Itinerary explaining that startTime is derived from places and
intentionally excluded from equals()/hashCode(); reference symbols: Itinerary,
startTime, places, DEFAULT_START_TIME.
data/travel/src/main/java/com/yapp/ndgl/data/travel/exception/DuplicateTravelPeriodException.kt (1)

3-5: 명시적 예외 메시지 추가 고려

현재 Exception(cause)만 전달되어 causenull일 경우 messagenull이 됩니다. 로그나 디버깅 시 예외 출처를 즉시 파악하기 어려우므로 기본 메시지를 추가하는 것을 권장합니다.

💡 메시지 추가 제안
-class DuplicateTravelPeriodException(
-    cause: Throwable? = null,
-) : Exception(cause)
+class DuplicateTravelPeriodException(
+    cause: Throwable? = null,
+) : Exception("여행 기간이 중복됩니다.", cause)
🤖 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/exception/DuplicateTravelPeriodException.kt`
around lines 3 - 5, DuplicateTravelPeriodException currently only forwards the
cause so message may be null; update the class DuplicateTravelPeriodException to
provide a clear default message and pass both message and cause to the
superclass (e.g., Exception("Duplicate travel period", cause)) so logs and stack
traces always include a helpful message; ensure the constructor signature still
accepts an optional cause parameter and use a descriptive constant or literal
for the message.
navigation/src/main/java/com/yapp/ndgl/navigation/Navigator.kt (1)

17-24: removeAll을 활용한 코드 간소화 제안

filter → forEach remove의 2단계 패턴은 removeAll로 간결하게 대체할 수 있습니다.

♻️ 리팩토링 제안
 fun navigateAndPopUpTo(destination: NavKey, vararg popRoutes: KClass<out NavKey>) {
-    popRoutes.forEach { routeClass ->
-        val toRemove = state.currentSubStack.filter { stackItem ->
-            routeClass.isInstance(stackItem)
-        }
-        toRemove.forEach { route ->
-            state.currentSubStack.remove(route)
-        }
-    }
-
+    state.currentSubStack.removeAll { item -> popRoutes.any { it.isInstance(item) } }
     navigate(destination)
 }
🤖 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/Navigator.kt` around lines
17 - 24, The loop removes items from state.currentSubStack matching any class in
popRoutes using a two-step filter+forEach; replace this with a single removeAll
call to simplify and avoid manual removal: use state.currentSubStack.removeAll
with a predicate that checks each stack item against popRoutes (e.g., test
routeClass.isInstance for any popRoutes entry) so all matching items are removed
in one operation; update the block around popRoutes and state.currentSubStack
accordingly.
data/travel/src/main/java/com/yapp/ndgl/data/travel/repository/TravelTemplateRepository.kt (1)

60-63: 오류 코드 문자열을 상수로 추출 권장

"TRAVEL-04-003" 매직 스트링을 companion object 상수로 추출하면 재사용성 및 추적 가능성이 향상됩니다.

♻️ 리팩토링 제안
 `@Singleton`
 class TravelTemplateRepository `@Inject` constructor(
     private val travelTemplateApi: TravelTemplateApi,
 ) {
+    companion object {
+        private const val ERROR_CODE_DUPLICATE_TRAVEL_PERIOD = "TRAVEL-04-003"
+    }
+
     ...

         } catch (e: HttpResponseException) {
-            if (e.code == "TRAVEL-04-003") {
+            if (e.code == ERROR_CODE_DUPLICATE_TRAVEL_PERIOD) {
                 throw DuplicateTravelPeriodException(e)
             }
🤖 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/TravelTemplateRepository.kt`
around lines 60 - 63, 현재 catch 블록에서 HttpResponseException의 오류 코드 문자열
"TRAVEL-04-003"을 하드코딩하고 있는데, 이 매직 스트링을 TravelTemplateRepository 클래스의 companion
object에 상수로 추출해 사용하도록 변경하세요: 새 상수 이름은 예를 들어 DUPLICATE_TRAVEL_PERIOD_CODE로 선언하고
catch (e: HttpResponseException) { if (e.code == DUPLICATE_TRAVEL_PERIOD_CODE) {
throw DuplicateTravelPeriodException(e) } }처럼 비교하도록 수정해 재사용성과 추적 가능성을 높이세요.
data/travel/src/main/java/com/yapp/ndgl/data/travel/model/ComputeRoutesRequest.kt (1)

18-25: Location·LatLng 클래스명이 Android SDK 및 Google Maps SDK 동명 클래스와 충돌할 수 있습니다.

  • Locationandroid.location.Location과, LatLngcom.google.android.gms.maps.model.LatLng(places SDK 의존성 존재)과 동일한 이름입니다.
  • 이 파일 자체는 문제없지만, 두 클래스를 함께 사용하는 소비 파일에서는 import alias가 필요해져 혼란을 초래합니다.

RouteLatLng, RouteLocation처럼 접두사를 붙이는 것을 고려해 보세요.

♻️ 이름 충돌 방지를 위한 리팩터링 제안
-@Serializable
-data class RouteLocation(
-    val location: Location,
-)
-
-@Serializable
-data class Location(
-    val latLng: LatLng,
-)
-
-@Serializable
-data class LatLng(
-    val latitude: Double,
-    val longitude: Double,
-)
+@Serializable
+data class RouteLocation(
+    val location: RouteLocationCoords,
+)
+
+@Serializable
+data class RouteLocationCoords(
+    val latLng: RouteLatLng,
+)
+
+@Serializable
+data class RouteLatLng(
+    val latitude: Double,
+    val longitude: Double,
+)
🤖 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/model/ComputeRoutesRequest.kt`
around lines 18 - 25, The types Location and LatLng conflict with Android/Google
Maps SDK classes; rename them to avoid import collisions (e.g., RouteLocation
for Location and RouteLatLng for LatLng) and update all references/usages
accordingly (constructors, serializers, parameter/return types and any JSON
annotations) so code compiles without requiring import aliases; ensure
`@Serializable` stays on the renamed LatLng class (now RouteLatLng) and update any
tests or DTO mappings that reference Location or LatLng.
data/travel/src/main/java/com/yapp/ndgl/data/travel/repository/UserTravelRepository.kt (1)

27-36: Repository에서 SharedFlow를 이벤트 버스로 사용하는 것은 동작하지만, 관심사 분리 측면에서 고려해 볼 사항이 있습니다.

UserTravelRepository@Singleton이므로 이벤트 스트림의 생명주기 관리에는 적합합니다. 다만 Repository가 데이터 접근과 UI 이벤트 전달을 동시에 담당하게 됩니다. 향후 이벤트 버스 역할을 별도 클래스(예: AddPlaceEventBus)로 분리하면 테스트와 유지보수가 더 용이해질 수 있습니다.

MutableSharedFlow 설정(replay=0, extraBufferCapacity=1, DROP_OLDEST)은 일회성 UI 이벤트에 적합합니다.

🤖 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/UserTravelRepository.kt`
around lines 27 - 36, The repository currently hosts the MutableSharedFlow event
bus (_addPlaceEvent, addPlaceEvent and emitAddPlaceEvent) which mixes data
access and UI-event responsibilities; extract this into a dedicated singleton
class (e.g., AddPlaceEventBus) that exposes a SharedFlow<AddPlaceEvent> and an
emit function, move the MutableSharedFlow and its configuration (replay=0,
extraBufferCapacity=1, onBufferOverflow=DROP_OLDEST) into that class, update
UserTravelRepository to inject/use AddPlaceEventBus instead of owning
_addPlaceEvent/emitAddPlaceEvent, and update any callers to use the new bus so
the repository retains only data-access concerns and event handling is testable
and reusable.
feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/component/TransportBottomSheet.kt (1)

89-93: LazyColumn의 최대 높이를 명시적으로 제한하는 것을 검토해 주세요.

heightIn(min = itemHeight * 4)로 설정하면 최소 높이(224.dp)는 보장하지만, 최대 높이가 명시되지 않아 내용이 많을 때 예상보다 커질 수 있습니다. Material3의 ModalBottomSheet이 기본적으로 화면 높이에 맞춰 제약하긴 하지만, heightIn(min = itemHeight * 4, max = itemHeight * 6) 등으로 상한을 명시하면 의도가 더 명확해집니다.

🤖 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/traveldetail/component/TransportBottomSheet.kt`
around lines 89 - 93, The LazyColumn currently uses heightIn(min = itemHeight *
4) so its maximum height is unbounded; update the LazyColumn configuration (the
LazyColumn call that sets modifier with fillMaxWidth(), heightIn(...),
nestedScroll(nestedScrollConnection)) to include an explicit max height such as
heightIn(min = itemHeight * 4, max = itemHeight * 6) (or another appropriate
cap) so the sheet does not grow beyond the intended size even with many items
while keeping integration with ModalBottomSheet intact.
feature/travel/src/main/java/com/yapp/ndgl/feature/travel/additinerary/AddItineraryViewModel.kt (1)

22-23: TODO/테스트용 상수 TEST_THUMBNAIL_URL이 남아 있습니다.

TODO("테스트용으로 지워야함") 코멘트와 함께 테스트용 썸네일 URL 상수가 존재합니다. 이 상수는 loadInitialData() (lines 44-93)의 스텁 데이터에서 사용되고 있습니다. 릴리스 전에 제거해야 할 항목으로 보입니다.

이 테스트 코드 제거를 추적하기 위한 이슈를 생성해 드릴까요?

🤖 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 22 - 23, Remove the leftover test constant TEST_THUMBNAIL_URL and
any usages in the stubbed loadInitialData() method: delete the
TEST_THUMBNAIL_URL declaration and replace references in loadInitialData() with
real data sources or remove the stub entries entirely so no test-only URLs
remain; ensure no other methods reference TEST_THUMBNAIL_URL and run
compile/tests to confirm removal.
feature/travel/src/main/java/com/yapp/ndgl/feature/travel/navigation/TravelEntry.kt (1)

144-152: 디버그용 Timber.d() 로그 3개가 남아 있습니다.

navigateToTravelDetail 콜백 내에 FQN(timber.log.Timber.d)으로 호출되는 디버그 로그 3개가 존재합니다. 디버깅 목적으로 추가된 것으로 보이며, 머지 전에 제거하는 것이 좋습니다. 또한 다른 파일(예: DatePickerViewModel.kt)에서는 Timber를 import하여 사용하고 있으므로 스타일 일관성도 고려해 주세요.

♻️ 디버그 로그 제거 제안
             navigateToTravelDetail = { travelId, days ->
-                timber.log.Timber.d("TravelEntry navigateToTravelDetail called: travelId=$travelId, days=$days")
-                timber.log.Timber.d("TravelEntry calling navigateAndPopUpTo with Route.TravelDetail($travelId, $days)")
                 navigator.navigateAndPopUpTo(
                     destination = Route.TravelDetail(travelId, days),
                     popRoutes = arrayOf(Route.DatePicker::class, Route.FollowTravel::class),
                 )
-                timber.log.Timber.d("TravelEntry navigateAndPopUpTo completed")
             },
🤖 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/navigation/TravelEntry.kt`
around lines 144 - 152, navigateToTravelDetail contains three temporary debug
Timber.d calls using the FQN; remove those three debug log lines and keep the
callback minimal (only perform navigator.navigateAndPopUpTo with
Route.TravelDetail), and if logging is needed follow project style by importing
timber.log.Timber and using Timber.* rather than FQN. Target symbols:
navigateToTravelDetail, navigator.navigateAndPopUpTo, and Timber.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@core/util/src/main/java/com/yapp/ndgl/core/util/DurationUtil.kt`:
- Around line 51-56: The function Duration.toTimeStringWithSeconds() misleads
because it hardcodes seconds as "00"; rename it to a name reflecting the actual
behavior (e.g., toHHMMSSString or toTimeStringForApi) and update all call sites
accordingly; specifically locate the Duration.toTimeStringWithSeconds function
and change its name, adjust any imports/usages (tests, callers, serializers) to
the new identifier, and ensure the formatting logic remains the same so behavior
is unchanged while the name accurately conveys intent.
- Around line 46-48: The regex captures hours, minutes and seconds but the code
only destructures hours and minutes from matchResult.destructured; either handle
seconds explicitly or make the ignore explicit: update the destructuring to
include a third variable (e.g., seconds) and add seconds.toInt().seconds into
the returned Duration (in the same function in DurationUtil.kt that uses
matchResult), or if seconds are intentionally irrelevant, destructure as val
(hours, minutes, _) = matchResult.destructured (or add a clear comment) so the
dropped group is explicit.

In `@data/travel/src/main/java/com/yapp/ndgl/data/travel/api/RouteApi.kt`:
- Around line 8-13: RouteApi.computeRoutes의 반환 타입이 NDGLCallAdapterFactory가 기대하는
BaseResponse가 아니므로 호출 어댑터가 적용되지 않습니다; ComputeRoutesResponse를 그대로 반환하는 대신
RouteApi.computeRoutes의 시그니처를 BaseResponse<ComputeRoutesResponse>로 래핑해 변경하고 필요한
import를 추가해 NDGLCallAdapterFactory와 NDGLCall에서 정상적으로 처리되도록 수정하세요; 만약 래핑이 불가능하면
NDGLCallAdapterFactory에 대응하는 별도의 커스텀 콜 어댑터를 구현(혹은 기존 팩토리에 매핑 추가)하여
HttpResponseException 파싱 로직이 작동하게 하세요.

In
`@data/travel/src/main/java/com/yapp/ndgl/data/travel/repository/UserTravelRepository.kt`:
- Around line 54-72: Both repository calls (bulkUpdateStartTime and
updateItinerary) are missing concrete success/failure handling in the ViewModel;
capture pre-mutation state, apply optimistic updates, call userTravelApi via
bulkUpdateStartTime/updateItinerary, and on failure roll back the local state
and surface an error. Concretely: in TravelDetailViewModel take a snapshot of
affected itinerary items before making the optimistic change, then inside the
suspendRunCatching that calls bulkUpdateStartTime (using
StartTimeUpdateItem/BulkUpdateStartTimeRequest) or updateItinerary (using
ItineraryUpdateItem/UpdateItineraryRequest) implement onSuccess to confirm state
and clear any pending flags and onFailure to restore the snapshot, log the error
and emit a user-facing error event; also replace the "// TODO: Handle success"
with the success-path logic and remove the "FIXME: API 실패 시 롤백 로직 필요" by
implementing this rollback pattern.

In `@detekt-config.yml`:
- Line 3: The temporary increase of the detekt "threshold" to 180 with the
`#FIXME("임시 테스트용")` comment must not be merged as-is; revert the `threshold`
value back to the original (e.g., 150) and remove the FIXME, and if the PR
legitimately causes long methods, either refactor the large ViewModel/Screen
methods or create a tracked issue; for unavoidable long functions, add them to
detekt's `ignoreAnnotated` list or annotate specific functions with
`@Suppress("LongMethod")` so the global threshold is not relaxed.

In
`@feature/travel/src/main/java/com/yapp/ndgl/feature/travel/additinerary/AddItineraryViewModel.kt`:
- Around line 279-312: The clickAddItinerary handler currently wraps
emitAddPlaceEvent in suspendRunCatching but ignores the result, always calling
postSideEffect(AddItinerarySideEffect.NavigateBack); modify clickAddItinerary to
handle success/failure from suspendRunCatching (or use try/catch): on success
call postSideEffect(NavigateBack), on failure log the error and emit an error
side effect (e.g., AddItinerarySideEffect.ShowError or similar) so the user is
notified and navigation is suppressed; reference the suspendRunCatching block,
userTravelRepository.emitAddPlaceEvent(AddPlaceEvent(...)) and
postSideEffect(AddItinerarySideEffect.NavigateBack) when making the change.

In
`@feature/travel/src/main/java/com/yapp/ndgl/feature/travel/datepicker/component/CalendarView.kt`:
- Line 107: Replace the hardcoded English accessibility labels used in
CalendarView (the contentDescription parameters for the previous/next month
buttons) with localized resources; specifically, swap the literal "Previous
Month" and "Next Month" strings passed to contentDescription for
stringResource(R.string.previous_month) and stringResource(R.string.next_month),
and add corresponding entries to your strings.xml (previous_month, next_month).
Locate the usages in CalendarView (and any IconButton/Image/Icon composables
passing contentDescription) and update them to use stringResource so
TalkBack/localization uses the Korean strings.
- Around line 104-116: The two adjacent Icon composables used for month
navigation (the Icon with clickable onPreviousMonth and the Icon with clickable
onNextMonth in CalendarView.kt) have overlapping 48dp touch targets because
their center spacing is only 40dp; fix by ensuring at least 24.dp gap between
them (e.g., change Spacer(Modifier.width(16.dp)) to
Spacer(Modifier.width(24.dp)) or, alternatively, replace the Icon(...)
clickables with Material IconButton wrappers for each icon so the framework
enforces non-overlapping touch targets).

In
`@feature/travel/src/main/java/com/yapp/ndgl/feature/travel/model/PlaceType.kt`:
- Around line 38-44: The conversion is lossy: PlaceCategory.AIRPORT is mapped to
PlaceType.TRANSPORT by toPlaceType() and back to PlaceCategory.TRANSPORT by
PlaceType.toPlaceCategory(), so AIRPORT is permanently lost when
emitAddPlaceEvent() sends data; fix by making the mapping bijective—add a
distinct PlaceType.AIRPORT (or carry an explicit airport flag) and update
PlaceType.toPlaceCategory() and the inverse toPlaceType() to map AIRPORT <->
PlaceCategory.AIRPORT, and ensure AddItineraryViewModel.emitAddPlaceEvent() uses
that preserved type so the original AIRPORT category is not overwritten.

In
`@feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/TravelDetailScreen.kt`:
- Around line 134-147: Replace the magic number 3 in the ScrollToPlace handler
with the shared placesOffset constant (or PLACES_OFFSET companion constant) so
targetIndex is computed as placesOffset + placeIndex rather than 3 + placeIndex;
update the handler referencing state.itineraries to use state.currentPlaces (or
confirm they are equivalent) when computing places and placeIndex, and ensure
both the LazyColumn setup and the ScrollToPlace branch reference the same
placesOffset symbol used elsewhere so future changes stay consistent.

In
`@feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/TravelDetailViewModel.kt`:
- Around line 98-161: handleAddPlace uses a temporary ID generated by
event.googlePlaceId.hashCode().toLong() for TravelPlace.id which can break later
calls (like updateItinerary()) when the server returns a real ID; replace the
ad-hoc hash-based ID with a clear temporary-ID strategy (e.g., generate a
UUID-based negative or namespaced temporary ID) and store a mapping from tempId
-> pending state in the ViewModel (or repository) so you can reconcile it when
the server returns the real itinerary item ID; ensure handleAddPlace
emits/retains the tempId (used by postSideEffect(ScrollToPlace)) and
updateItinerary reads the mapping to translate tempId to serverId, add a clear
TODO/FIXME comment and create a tracked issue to remove the temp-ID logic once
the place-add API returns authoritative IDs.
- Around line 684-722: The getTransportOptions function is public and calls
routeRepository.computeRoute sequentially causing latency; make
getTransportOptions private and run computeRoute calls in parallel using
coroutines (e.g., wrap in coroutineScope and launch async per travel mode), use
suspendRunCatching or runCatching inside each async to capture failures,
awaitAll the Deferred results, then map/filter non-null RouteInfo into
TransportSegment (preserving conversion via mode.toTransportTypeFromMode(),
duration parsing, and excluding zero-distance results). Ensure you reference
getTransportOptions, routeRepository.computeRoute, TravelMode list, and
TransportSegment/TransportType conversions when applying the changes.
- Around line 418-446: Capture and keep the previous state before performing the
optimistic UI update in confirmEditMode (and similarly in
confirmChangeTransportSegment and confirmDuration): store state.value (or the
specific previous itineraries/selectedPlaceIds) in a local val before calling
reduce, then call the API (updateItinerary or respective network function) and
on failure restore the saved state via reduce to roll back; ensure the restore
happens inside the coroutine's onFailure branch (or catch block) and surface an
error message/log so the user knows the update failed.
- Around line 763-797: The current updateItinerary() builds and sends per-day
requests inside the forEachIndexed loop; instead, collect ItineraryUpdateItem
objects for all days (iterate state.value.itineraries, mapping to
ItineraryUpdateItem with day = index+1, sequence, startTime, estimatedDuration,
memo, distanceKm, transportation) but only include items that actually changed
(compare to previous state or original payload you have) and then call
userTravelRepository.updateItinerary(travelId = travelId, itineraries =
allItems) once; handle the single suspendRunCatching result from that call and
implement proper onSuccess (e.g., update local state/acknowledge) and onFailure
(retry/report error) logic rather than per-day TODOs so success/failure of the
whole update is tracked.

---

Outside diff comments:
In
`@feature/travel/src/main/java/com/yapp/ndgl/feature/travel/datepicker/DatePickerScreen.kt`:
- Around line 124-137: The CTA button currently uses only
DatePickerState.isDateSelected to determine NDGLCTAButtonAttr.Status, so
isLoading is ignored; update the NDGLCTAButton usage in DatePickerScreen to
consider state.isLoading (and/or state.isDateSelected) when setting the status
so the button becomes DISABLED during loading, and additionally either show a
loading indicator inside NDGLCTAButton or short-circuit onClick
(onCompleteClick) when state.isLoading to prevent duplicate clicks (aligns with
DatePickerViewModel's duplicate-click protection).

In
`@feature/travel/src/main/java/com/yapp/ndgl/feature/travel/datepicker/DatePickerViewModel.kt`:
- Around line 96-103: Prevent duplicate travel creation by guarding
clickCompleteButton() with the ViewModel's isLoading flag: in
clickCompleteButton(), return early if state.value.isLoading is true, set
isLoading = true before calling createTravelFromTemplate(startDate, endDate) and
ensure it is reset to false once the API call completes or fails
(createTravelFromTemplate should update isLoading accordingly). Also update
DatePickerScreen to disable the complete button based on state.isLoading so the
UI cannot invoke clickCompleteButton() while a request is in flight.

---

Nitpick comments:
In `@core/ui/src/main/java/com/yapp/ndgl/core/ui/designsystem/NDGLModal.kt`:
- Around line 43-44: The current Surface in NDGLModal.kt uses modifier =
modifier.wrapContentHeight(), which chains the incoming modifier before
wrapContentHeight and can be overridden by size-constraining modifiers (e.g.,
fillMaxSize) passed by callers; change the chaining so wrapContentHeight() is
applied first (e.g., wrapContentHeight().then(modifier) or modifier =
Modifier.wrapContentHeight().then(modifier)) to ensure the dialog's height
behavior is applied predictably; update the Surface modifier usage in the
NDGLModal composable accordingly.

In `@core/ui/src/main/res/drawable/ic_24_car.xml`:
- Around line 12-18: The nested <group> contains a duplicate <clip-path> with
android:pathData="M24,0H0V24H24V0Z" which is redundant; remove that inner
<group> (and its <clip-path>) and place the <path> element directly under the
parent group that already defines the same clip-path so the clip is applied
once, keeping the existing android:fillColor="#757575" on the <path>.

In `@core/ui/src/main/res/values/strings.xml`:
- Around line 39-46: 두 키 start_time_setting과 start_time_setting_title이 동일한 문자열
"여행 시작 시간 설정"으로 중복되어 있으니 의도를 명확히 하여 중복을 제거하거나 값/키를 분리하세요; 예를 들어
start_time_setting은 설정 항목의 레이블로, start_time_setting_title은 해당 화면의 타이틀로 쓰인다면 각각
"시작 시간", "여행 시작 시간 설정"처럼 역할에 맞는 다른 문자열값을 설정하거나 실제로 하나만 사용되는 경우 불필요한
키(start_time_setting 또는 start_time_setting_title)를 삭제하고 리소스 참조를 모두 하나의 키로 통일하도록
수정하세요.

In `@core/util/src/main/java/com/yapp/ndgl/core/util/DurationUtil.kt`:
- Around line 43-44: The regex in parseTimeStringToDuration is being recompiled
on every call; extract the pattern """^(\d+):([0-5]?\d):([0-5]?\d)$""" to a
top-level immutable val (e.g., TIME_STRING_REGEX or TIME_STRING_PATTERN) and
replace the local regex = ... .toRegex() with a reference to that constant so
parseTimeStringToDuration uses the precompiled Regex instance instead of
recompiling each invocation.

In
`@data/core/src/main/java/com/yapp/ndgl/data/core/interceptor/RouteInterceptor.kt`:
- Around line 10-17: The interceptor RouteInterceptor.intercept currently uses
.addHeader("Content-Type", "application/json") which can duplicate
Retrofit/OkHttp-managed Content-Type headers; remove that addHeader line
entirely or replace it with .header("Content-Type", "application/json") to
overwrite instead of add; keep the other .addHeader calls for "X-Goog-Api-Key"
and "X-Goog-FieldMask" as-is and ensure the modified request building in
intercept uses the chosen approach.
- Line 14: The interceptor currently hardcodes the "X-Goog-FieldMask" header in
RouteInterceptor (the .addHeader("X-Goog-FieldMask",
"routes.duration,routes.distanceMeters") line), reducing flexibility for future
endpoints; remove that hardcoded addHeader and instead make the header provided
at the Retrofit call level (use Retrofit's `@Headers` on the specific Route API
methods or an `@Header` parameter on functions that need different field masks) so
each endpoint can declare its own fields; update RouteInterceptor to no longer
inject X-Goog-FieldMask and ensure service interfaces (route-related methods)
add the appropriate `@Headers` or `@Header` usage.

In
`@data/travel/src/main/java/com/yapp/ndgl/data/travel/exception/DuplicateTravelPeriodException.kt`:
- Around line 3-5: DuplicateTravelPeriodException currently only forwards the
cause so message may be null; update the class DuplicateTravelPeriodException to
provide a clear default message and pass both message and cause to the
superclass (e.g., Exception("Duplicate travel period", cause)) so logs and stack
traces always include a helpful message; ensure the constructor signature still
accepts an optional cause parameter and use a descriptive constant or literal
for the message.

In
`@data/travel/src/main/java/com/yapp/ndgl/data/travel/model/BulkUpdateStartTimeRequest.kt`:
- Around line 11-14: The StartTimeUpdateItem data class's startTime property
lacks a documented expected string format; update StartTimeUpdateItem (the
startTime field) to include a KDoc or inline comment specifying the exact
expected format (e.g., "HH:mm" or ISO-8601 "yyyy-MM-dd'T'HH:mm:ss"), include any
timezone expectations, and mention validation behavior so callers know what to
send and maintainers know how to parse/validate that field.

In
`@data/travel/src/main/java/com/yapp/ndgl/data/travel/model/ComputeRoutesRequest.kt`:
- Around line 18-25: The types Location and LatLng conflict with Android/Google
Maps SDK classes; rename them to avoid import collisions (e.g., RouteLocation
for Location and RouteLatLng for LatLng) and update all references/usages
accordingly (constructors, serializers, parameter/return types and any JSON
annotations) so code compiles without requiring import aliases; ensure
`@Serializable` stays on the renamed LatLng class (now RouteLatLng) and update any
tests or DTO mappings that reference Location or LatLng.

In
`@data/travel/src/main/java/com/yapp/ndgl/data/travel/model/UpdateItineraryRequest.kt`:
- Around line 11-22: The ItineraryUpdateItem data class has an optional memo
property declared as val memo: String? but lacks a default value; update the
ItineraryUpdateItem declaration to give memo a default of null (e.g., val memo:
String? = null) so callers can omit it like distanceKm and transportation;
ensure the `@SerialName`("travelerTip") annotation remains on the memo property.

In
`@data/travel/src/main/java/com/yapp/ndgl/data/travel/repository/TravelTemplateRepository.kt`:
- Around line 60-63: 현재 catch 블록에서 HttpResponseException의 오류 코드 문자열
"TRAVEL-04-003"을 하드코딩하고 있는데, 이 매직 스트링을 TravelTemplateRepository 클래스의 companion
object에 상수로 추출해 사용하도록 변경하세요: 새 상수 이름은 예를 들어 DUPLICATE_TRAVEL_PERIOD_CODE로 선언하고
catch (e: HttpResponseException) { if (e.code == DUPLICATE_TRAVEL_PERIOD_CODE) {
throw DuplicateTravelPeriodException(e) } }처럼 비교하도록 수정해 재사용성과 추적 가능성을 높이세요.

In
`@data/travel/src/main/java/com/yapp/ndgl/data/travel/repository/UserTravelRepository.kt`:
- Around line 27-36: The repository currently hosts the MutableSharedFlow event
bus (_addPlaceEvent, addPlaceEvent and emitAddPlaceEvent) which mixes data
access and UI-event responsibilities; extract this into a dedicated singleton
class (e.g., AddPlaceEventBus) that exposes a SharedFlow<AddPlaceEvent> and an
emit function, move the MutableSharedFlow and its configuration (replay=0,
extraBufferCapacity=1, onBufferOverflow=DROP_OLDEST) into that class, update
UserTravelRepository to inject/use AddPlaceEventBus instead of owning
_addPlaceEvent/emitAddPlaceEvent, and update any callers to use the new bus so
the repository retains only data-access concerns and event handling is testable
and reusable.

In
`@feature/travel/src/main/java/com/yapp/ndgl/feature/travel/additinerary/AddItineraryViewModel.kt`:
- Around line 22-23: Remove the leftover test constant TEST_THUMBNAIL_URL and
any usages in the stubbed loadInitialData() method: delete the
TEST_THUMBNAIL_URL declaration and replace references in loadInitialData() with
real data sources or remove the stub entries entirely so no test-only URLs
remain; ensure no other methods reference TEST_THUMBNAIL_URL and run
compile/tests to confirm removal.

In
`@feature/travel/src/main/java/com/yapp/ndgl/feature/travel/datepicker/DatePickerContract.kt`:
- Around line 23-24: The TODO notes that the isLoading Boolean in
DatePickerContract is not yet reflected in the UI; update the feature by wiring
that state through your presentation layer: ensure DatePickerContract
(isLoading) is exposed from DatePickerViewModel, emit loading changes during
async operations, and make DatePickerFragment/DatePickerView observe the
ViewModel’s isLoading to show/hide a loading indicator (e.g., ProgressBar) or
remove the TODO if you intentionally plan no UI. Use the symbol names
DatePickerContract.isLoading, DatePickerViewModel (the state producer), and
DatePickerFragment/DatePickerView (the consumers) when making the changes.

In
`@feature/travel/src/main/java/com/yapp/ndgl/feature/travel/followtravel/FollowTravelContract.kt`:
- Line 16: FollowTravelState currently defines creatorName: String = "" which
duplicates contentInfo.videoInfo.creatorName; inspect usages of
FollowTravelState.creatorName and contentInfo.videoInfo.creatorName to decide
single source of truth: if they always represent the same value, remove
creatorName from FollowTravelState and refactor call sites to read
contentInfo.videoInfo.creatorName (or pass contentInfo earlier), otherwise keep
creatorName but add a comment documenting why it diverges and ensure it is
populated consistently (e.g., initialize in FollowTravelState constructor or
update in the code path that loads contentInfo); reference FollowTravelState,
creatorName, and contentInfo.videoInfo.creatorName when making changes.

In
`@feature/travel/src/main/java/com/yapp/ndgl/feature/travel/followtravel/FollowTravelViewModel.kt`:
- Around line 38-54: loadTravelTemplateItinerary currently launches per-day
coroutines but leaves onFailure as a FIXME, so failed days remain as empty
Itinerary() with no user feedback; update the error handling in
loadTravelTemplateItinerary (in FollowTravelViewModel) to reflect per-day errors
in state by setting a distinct error marker for that day—either add fields to
Itinerary (e.g., isError:Boolean and errorMessage:String?) or extend the
ViewModel state with a parallel per-day LoadState list—and in the onFailure
block call reduce to update itineraries[day-1] (or the corresponding LoadState)
with the error info so the UI can render an error view for that specific day;
ensure the change mirrors the same pattern in TravelDetailViewModel for
consistency.

In
`@feature/travel/src/main/java/com/yapp/ndgl/feature/travel/mytravel/MyTravelScreen.kt`:
- Around line 54-56: The call
viewModel.onIntent((MyTravelIntent.ClickTravel(...))) uses unnecessary outer
parentheses; remove the extra parentheses so the invocation is
viewModel.onIntent(MyTravelIntent.ClickTravel(travelId = travelId, days = days))
to match the style used for ClickTravelDetail and avoid Detekt
UnnecessaryParentheses warnings (refer to MyTravelScreen.kt and the
MyTravelIntent.ClickTravel usage).

In
`@feature/travel/src/main/java/com/yapp/ndgl/feature/travel/mytravel/UpcomingTravelCardSection.kt`:
- Around line 50-73: The days calculation is duplicated in the
UpcomingTravel.Upcoming and UpcomingTravel.InProgress branches; extract it to a
single place by computing it once before the when or adding an extension on
UpcomingTravel (e.g., a function using startDate and endDate to return days =
ChronoUnit.DAYS.between(startDate, endDate).toInt() + 1) and then pass that
single computed value into UpcomingTravelCard and InProgressTravelCard
(references: UpcomingTravel, UpcomingTravel.Upcoming, UpcomingTravel.InProgress,
startDate, endDate, days, UpcomingTravelCard, InProgressTravelCard,
onTravelClick).

In
`@feature/travel/src/main/java/com/yapp/ndgl/feature/travel/mytravel/UpcomingTravelListSection.kt`:
- Around line 85-88: Move the days calculation out of the UpcomingTravel
composable into the data layer by adding a derived property on
UpcomingTravelItem (e.g., val days) that computes days =
maxOf(ChronoUnit.DAYS.between(startDate, endDate).toInt() + 1, 1); update the
composable to use UpcomingTravelItem.days instead of computing it inline; ensure
the property guards against startDate > endDate by returning at least 1 so edge
cases are handled and the logic is testable and reusable.

In
`@feature/travel/src/main/java/com/yapp/ndgl/feature/travel/navigation/TravelEntry.kt`:
- Around line 144-152: navigateToTravelDetail contains three temporary debug
Timber.d calls using the FQN; remove those three debug log lines and keep the
callback minimal (only perform navigator.navigateAndPopUpTo with
Route.TravelDetail), and if logging is needed follow project style by importing
timber.log.Timber and using Timber.* rather than FQN. Target symbols:
navigateToTravelDetail, navigator.navigateAndPopUpTo, and Timber.

In
`@feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/component/TransportBottomSheet.kt`:
- Around line 89-93: The LazyColumn currently uses heightIn(min = itemHeight *
4) so its maximum height is unbounded; update the LazyColumn configuration (the
LazyColumn call that sets modifier with fillMaxWidth(), heightIn(...),
nestedScroll(nestedScrollConnection)) to include an explicit max height such as
heightIn(min = itemHeight * 4, max = itemHeight * 6) (or another appropriate
cap) so the sheet does not grow beyond the intended size even with many items
while keeping integration with ModalBottomSheet intact.

In
`@feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/TravelDetailContract.kt`:
- Around line 61-75: The startTime property is declared in the class body so
it's excluded from the data class generated equals()/hashCode(); if you need
startTime to participate in equality, move it into the primary constructor
(e.g., add a constructor parameter val startTime: Duration =
places.firstOrNull()?.startTime ?: DEFAULT_START_TIME.hours and remove the
body-backed startTime), otherwise keep the body property but add a clarifying
comment near Itinerary explaining that startTime is derived from places and
intentionally excluded from equals()/hashCode(); reference symbols: Itinerary,
startTime, places, DEFAULT_START_TIME.

In `@navigation/src/main/java/com/yapp/ndgl/navigation/Navigator.kt`:
- Around line 17-24: The loop removes items from state.currentSubStack matching
any class in popRoutes using a two-step filter+forEach; replace this with a
single removeAll call to simplify and avoid manual removal: use
state.currentSubStack.removeAll with a predicate that checks each stack item
against popRoutes (e.g., test routeClass.isInstance for any popRoutes entry) so
all matching items are removed in one operation; update the block around
popRoutes and state.currentSubStack accordingly.

Comment on lines +46 to +48
val (hours, minutes) = matchResult.destructured

return hours.toInt().hours + minutes.toInt().minutes
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Feb 23, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

정규식에서 캡처된 seconds 그룹이 묵시적으로 버려집니다.

정규식은 (hours):(minutes):(seconds) 3개 그룹을 캡처하지만, val (hours, minutes) = matchResult.destructured에서 세 번째 그룹(초)은 아무런 경고 없이 무시됩니다. 초를 의도적으로 무시하는 것이라면 이를 명시적으로 드러내거나, 초가 항상 0이 아닐 수 있는 입력을 고려하여 seconds 처리 방식을 문서화하는 것이 좋습니다.

🛡️ 초(seconds) 처리를 명시적으로 만드는 제안
-    val (hours, minutes) = matchResult.destructured
+    val (hours, minutes, _) = matchResult.destructured  // 초(seconds)는 의도적으로 무시
📝 Committable suggestion

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

Suggested change
val (hours, minutes) = matchResult.destructured
return hours.toInt().hours + minutes.toInt().minutes
val (hours, minutes, _) = matchResult.destructured // 초(seconds)는 의도적으로 무시
return hours.toInt().hours + minutes.toInt().minutes
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/util/src/main/java/com/yapp/ndgl/core/util/DurationUtil.kt` around lines
46 - 48, The regex captures hours, minutes and seconds but the code only
destructures hours and minutes from matchResult.destructured; either handle
seconds explicitly or make the ignore explicit: update the destructuring to
include a third variable (e.g., seconds) and add seconds.toInt().seconds into
the returned Duration (in the same function in DurationUtil.kt that uses
matchResult), or if seconds are intentionally irrelevant, destructure as val
(hours, minutes, _) = matchResult.destructured (or add a clear comment) so the
dropped group is explicit.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

seconds값은 필요없습니다.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@mj010504, 네, 이해했습니다! 초(seconds) 값이 필요하지 않다는 점을 확인했습니다.

현재 코드가 의도한 대로 동작하고 있습니다. 다만 향후 코드 유지보수성을 위해, 세 번째 그룹을 명시적으로 무시하는 것을 고려해보실 수 있습니다:

val (hours, minutes, _) = matchResult.destructured  // 초는 의도적으로 무시

이렇게 하면 다른 개발자들이 코드를 읽을 때 초를 의도적으로 무시한 것임을 명확히 알 수 있습니다. 하지만 현재 구현도 올바르게 작동하고 있으므로 선택 사항입니다.


🧠 Learnings used
Learnt from: jihee-dev
Repo: YAPP-Github/27th-App-Team-1-Android PR: 25
File: feature/travel/src/main/java/com/yapp/ndgl/feature/travel/mytravel/MyTravelContract.kt:52-54
Timestamp: 2026-02-17T22:21:31.141Z
Learning: Use the naming convention estimatedDuration for duration-related fields across Kotlin files to align with server API response parameter names and maintain consistency throughout the codebase. When introducing or renaming duration fields, replace generic names (e.g., duration, totalDuration) with estimatedDuration and update all references (data classes, mappings, JSON parsing) accordingly to avoid mismatch with server payloads.

Learnt from: jihee-dev
Repo: YAPP-Github/27th-App-Team-1-Android PR: 27
File: feature/home/src/main/java/com/yapp/ndgl/feature/home/search/TemplateSearchScreen.kt:0-0
Timestamp: 2026-02-18T18:16:40.700Z
Learning: Use the correct Hilt Compose ViewModel import across Kotlin Android files: import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel (from androidx.hilt:hilt-lifecycle-viewmodel-compose:1.3.0). The old import androidx.hilt.navigation.compose.hiltViewModel is deprecated in Hilt 1.3.0. Ensure the hilt-lifecycle-viewmodel-compose dependency is added to Gradle (both Gradle catalog and module dependencies) and update imports accordingly to avoid runtime issues or build failures.

Comment on lines +51 to +56
fun Duration.toTimeStringWithSeconds(): String {
val totalMinutes = this.inWholeMinutes
val hours = totalMinutes / 60
val minutes = totalMinutes % 60
return String.format(getDefault(), "%02d:%02d:00", hours, minutes)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

toTimeStringWithSeconds 네이밍이 실제 동작과 맞지 않습니다.

함수 이름의 "WithSeconds"는 초(seconds) 값을 포함한다는 의미를 내포하지만, 실제로는 초를 항상 "00"으로 하드코딩합니다. parseTimeStringToDuration과의 왕복 변환 컨텍스트에서는 일관성이 있지만, 이름만 보고 "초 단위까지 정밀하게 포맷팅한다"고 오해할 수 있습니다. toTimeStringForApi 또는 toHHMMSSString 등 실제 의도를 반영하는 이름으로 변경을 고려해보세요.

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

In `@core/util/src/main/java/com/yapp/ndgl/core/util/DurationUtil.kt` around lines
51 - 56, The function Duration.toTimeStringWithSeconds() misleads because it
hardcodes seconds as "00"; rename it to a name reflecting the actual behavior
(e.g., toHHMMSSString or toTimeStringForApi) and update all call sites
accordingly; specifically locate the Duration.toTimeStringWithSeconds function
and change its name, adjust any imports/usages (tests, callers, serializers) to
the new identifier, and ensure the formatting logic remains the same so behavior
is unchanged while the name accurately conveys intent.

Comment thread data/travel/src/main/java/com/yapp/ndgl/data/travel/api/RouteApi.kt
Comment on lines +54 to +72
suspend fun bulkUpdateStartTime(
travelId: Long,
updates: List<StartTimeUpdateItem>,
) {
userTravelApi.bulkUpdateStartTime(
id = travelId,
request = BulkUpdateStartTimeRequest(updates = updates),
).getData()
}

suspend fun updateItinerary(
travelId: Long,
itineraries: List<ItineraryUpdateItem>,
) {
userTravelApi.updateItinerary(
id = travelId,
request = UpdateItineraryRequest(itineraries = itineraries),
).getData()
}
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Feb 23, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# bulkUpdateStartTime과 updateItinerary 호출부에서 에러 핸들링이 되어 있는지 확인
rg -nP --type=kotlin -C5 'bulkUpdateStartTime|updateItinerary' -g '!UserTravelApi.kt' -g '!UserTravelRepository.kt'

Repository: YAPP-Github/27th-App-Team-1-Android

Length of output: 8521


bulkUpdateStartTimeupdateItinerary 호출부에서 에러 핸들링이 완성되지 않았습니다.

두 메서드 모두 호출 시점에서 suspendRunCatching 블록으로 감싸져 있어 기본적인 에러 핸들링은 적용되어 있습니다. 다만 TravelDetailViewModel.kt에서 다음과 같은 미완성 부분이 있습니다:

  • bulkUpdateStartTime 호출 후 성공 처리는 진행되나, API 실패 시 상태 복구 로직이 명시되지 않음
  • updateItinerary 호출 후 // TODO: Handle success 주석만 있고 실제 구현이 없음
  • 함수 내부에 "FIXME: API 실패 시 롤백 로직 필요" 주석이 여러 곳(444, 517, 606줄)에 기록되어 있음

쓰기 작업의 성공/실패 처리를 완성하고 필요시 상태 복구 로직을 추가하기 바랍니다.

🤖 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/UserTravelRepository.kt`
around lines 54 - 72, Both repository calls (bulkUpdateStartTime and
updateItinerary) are missing concrete success/failure handling in the ViewModel;
capture pre-mutation state, apply optimistic updates, call userTravelApi via
bulkUpdateStartTime/updateItinerary, and on failure roll back the local state
and surface an error. Concretely: in TravelDetailViewModel take a snapshot of
affected itinerary items before making the optimistic change, then inside the
suspendRunCatching that calls bulkUpdateStartTime (using
StartTimeUpdateItem/BulkUpdateStartTimeRequest) or updateItinerary (using
ItineraryUpdateItem/UpdateItineraryRequest) implement onSuccess to confirm state
and clear any pending flags and onFailure to restore the snapshot, log the error
and emit a user-facing error event; also replace the "// TODO: Handle success"
with the success-path logic and remove the "FIXME: API 실패 시 롤백 로직 필요" by
implementing this rollback pattern.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

추후 에러 핸들링 로직 반영

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@mj010504, 네, 이해했습니다! 추후 PR에서 에러 핸들링을 반영하시는 것으로 확인했습니다.

필요하시다면 후속 작업 추적을 위해 이슈를 생성하실 수 있습니다. 생성을 원하시면 말씀해 주세요!


🧠 Learnings used
Learnt from: jihee-dev
Repo: YAPP-Github/27th-App-Team-1-Android PR: 25
File: data/travel/src/main/java/com/yapp/ndgl/data/travel/repository/UserTravelRepository.kt:28-30
Timestamp: 2026-02-17T22:18:31.734Z
Learning: In UserTravelRepository.kt, follow the API contract: getUpcomingTravelList() returns an empty result instead of HTTP 204. Do not add separate 204 error handling. Keep returning getUpcomingTravelList().getData(); ensure null-safety for getData() if needed and prefer this streamlined path unless the API contract changes.

Learnt from: mj010504
Repo: YAPP-Github/27th-App-Team-1-Android PR: 26
File: feature/travel/src/main/java/com/yapp/ndgl/feature/travel/addplace/AddPlaceViewModel.kt:71-90
Timestamp: 2026-02-18T12:08:25.163Z
Learning: In the travel feature's place photo loading logic (AddPlaceViewModel.kt, AddItineraryViewModel.kt), a 1-second delay before each photo fetch attempt is intentional and necessary. For newly searched places, the backend needs time to save place data to the database before photos can be retrieved. Without this delay, photo fetching returns empty results for first-time searches.

Learnt from: jihee-dev
Repo: YAPP-Github/27th-App-Team-1-Android PR: 25
File: feature/travel/src/main/java/com/yapp/ndgl/feature/travel/mytravel/MyTravelContract.kt:52-54
Timestamp: 2026-02-17T22:21:31.141Z
Learning: Use the naming convention estimatedDuration for duration-related fields across Kotlin files to align with server API response parameter names and maintain consistency throughout the codebase. When introducing or renaming duration fields, replace generic names (e.g., duration, totalDuration) with estimatedDuration and update all references (data classes, mappings, JSON parsing) accordingly to avoid mismatch with server payloads.

Learnt from: jihee-dev
Repo: YAPP-Github/27th-App-Team-1-Android PR: 27
File: feature/home/src/main/java/com/yapp/ndgl/feature/home/search/TemplateSearchScreen.kt:0-0
Timestamp: 2026-02-18T18:16:40.700Z
Learning: Use the correct Hilt Compose ViewModel import across Kotlin Android files: import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel (from androidx.hilt:hilt-lifecycle-viewmodel-compose:1.3.0). The old import androidx.hilt.navigation.compose.hiltViewModel is deprecated in Hilt 1.3.0. Ensure the hilt-lifecycle-viewmodel-compose dependency is added to Gradle (both Gradle catalog and module dependencies) and update imports accordingly to avoid runtime issues or build failures.

Learnt from: mj010504
Repo: YAPP-Github/27th-App-Team-1-Android PR: 34
File: data/travel/src/main/java/com/yapp/ndgl/data/travel/api/RouteApi.kt:8-13
Timestamp: 2026-02-23T11:42:30.498Z
Learning: In the YAPP-Github/27th-App-Team-1-Android project, for external APIs (e.g., Google's Route API) do not wrap responses with the internal BaseResponse or use NDGLCallAdapterFactory. Create a separate Retrofit client instance for external APIs that omits the custom call adapter factory so they can use the native response format and error handling. Apply this pattern to Kotlin files under data/travel/src/main/java (e.g., RouteApi.kt and similar external-API API interfaces).

Comment thread detekt-config.yml
Comment on lines +98 to +161
private suspend fun handleAddPlace(event: AddPlaceEvent) {
val dayIndex = event.day - 1
val currentItinerary = state.value.itineraries.getOrNull(dayIndex) ?: return

val newSequence = currentItinerary.places.size + 1

val newPlaceInfo = PlaceInfo(
googlePlaceId = event.googlePlaceId,
name = event.name,
placeType = event.placeType.toPlaceType(),
day = event.day,
sequence = newSequence,
thumbnail = event.thumbnail,
latitude = event.latitude,
longitude = event.longitude,
address = event.address,
phoneNumber = event.phoneNumber,
googleMapsUri = event.googleMapsUri,
websiteUrl = event.websiteUrl,
rating = event.rating,
userRatingCount = event.userRatingCount,
estimatedDuration = event.estimatedDuration.minutes,
)

val newPlace = TravelPlace(
id = event.googlePlaceId.hashCode().toLong(), // FIXME: 임시 ID (googlePlaceId 해시), API 응답에서 실제 itinerary item ID 필요
placeInfo = newPlaceInfo,
regularOpeningHours = null,
userData = TravelPlace.UserData(
estimatedDuration = event.estimatedDuration.minutes,
),
startTime = 0.hours,
transportToNext = null,
)

val updatedPlaces = if (currentItinerary.places.isNotEmpty()) {
val lastPlace = currentItinerary.places.last()
val transportSegment = calculateTransport(lastPlace, newPlace)

val placesWithTransport = currentItinerary.places.dropLast(1) +
lastPlace.copy(transportToNext = transportSegment)
placesWithTransport + newPlace
} else {
listOf(newPlace)
}

val firstPlaceStartTime = currentItinerary.places.firstOrNull()?.startTime
?: Itinerary.DEFAULT_START_TIME.hours
val timedPlaces = calculatePlaceStartTimes(updatedPlaces, firstPlaceStartTime)

reduce {
copy(
contentInfo = ContentInfo(
country = "태국",
city = "방콕",
budgetPerPerson = Budget(1200000),
nights = 1,
days = 2,
videoInfo = VideoInfo(
title = "방콕 풀코스, 동남아 안 가본 곽튜브와 함께 【방콕】",
creatorName = "빠니보틀",
profileImage = TEST_PROFILE_IMAGE_URL,
thumbnail = TEST_THUMBNAIL_URL,
link = "https://www.youtube.com/watch?v=F2utz6L76D0",
summary = "빠니보틀은 주말을 이용해 직장인들도 충분히 다녀올 수 있는 '금요일 퇴근 후 방콕 여행'의 가능성을 보여주며, 곽튜브와의 티격태격 케미를 통해 방콕의 매력을 소개합니다",
),
),
itineraries = loadedItineraries,
tempItineraries = loadedItineraries,
val updatedItineraries = itineraries.mapIndexed { index, itinerary ->
if (index == dayIndex) {
itinerary.copy(places = timedPlaces)
} else {
itinerary
}
}
copy(itineraries = updatedItineraries)
}

postSideEffect(TravelDetailSideEffect.ScrollToPlace(newPlace.id))
// FIXME: 일정 추가 API 연동
}
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Feb 23, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

handleAddPlace — 임시 ID 생성과 후속 동기화에 대한 우려.

Line 123의 event.googlePlaceId.hashCode().toLong()는 FIXME로 표시되어 있지만, 이 임시 ID가 이후 updateItinerary()placeId로 사용됩니다 (Line 768). 서버가 이 hashCode 기반 ID를 인식하지 못하면 업데이트가 실패할 수 있습니다. 장소 추가 API가 연동되기 전까지 이 흐름을 잘 관리해 주세요.

또한 Line 129에서 startTime = 0.hours로 초기화한 뒤 Line 146에서 calculatePlaceStartTimes로 재계산하므로, 초기값은 덮어씌워집니다 — 이 부분은 문제 없습니다.

해당 FIXME 항목을 트래킹하는 이슈를 생성해 드릴까요?

🤖 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/traveldetail/TravelDetailViewModel.kt`
around lines 98 - 161, handleAddPlace uses a temporary ID generated by
event.googlePlaceId.hashCode().toLong() for TravelPlace.id which can break later
calls (like updateItinerary()) when the server returns a real ID; replace the
ad-hoc hash-based ID with a clear temporary-ID strategy (e.g., generate a
UUID-based negative or namespaced temporary ID) and store a mapping from tempId
-> pending state in the ViewModel (or repository) so you can reconcile it when
the server returns the real itinerary item ID; ensure handleAddPlace
emits/retains the tempId (used by postSideEffect(ScrollToPlace)) and
updateItinerary reads the mapping to translate tempId to serverId, add a clear
TODO/FIXME comment and create a tracked issue to remove the temp-ID logic once
the place-add API returns authoritative IDs.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

추후 수정할 것

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@mj010504, 알겠습니다! 임시 ID 방식은 API 연동 전까지 유지하고 추후 수정하시겠다는 점 확인했습니다.

필요하시다면 이 항목을 추적하는 GitHub 이슈를 생성해 드릴 수 있습니다. 이슈 생성을 원하시면 말씀해 주세요.


🧠 Learnings used
Learnt from: mj010504
Repo: YAPP-Github/27th-App-Team-1-Android PR: 26
File: feature/travel/src/main/java/com/yapp/ndgl/feature/travel/addplace/AddPlaceViewModel.kt:71-90
Timestamp: 2026-02-18T12:08:18.209Z
Learning: In the travel feature (place photo loading logic in AddPlaceViewModel.kt and AddItineraryViewModel.kt), retain a deliberate 1-second delay before each photo fetch when loading data for newly searched places. This accounts for backend latency needed to save place data before photos are retrievable. Ensure the delay is configurable (e.g., via feature flag or constant) and documented, so it can be adjusted or removed if backend performance improves. This guidance applies to all travel feature view models handling similar photo fetch flows.

Learnt from: mj010504
Repo: YAPP-Github/27th-App-Team-1-Android PR: 17
File: feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/component/TimelineContent.kt:243-251
Timestamp: 2026-02-11T12:08:02.377Z
Learning: In feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/component/TimelineContent.kt, the startIndex calculation for the WheelPicker intentionally does NOT subtract visibleItemCount/2 offset. This design choice preserves showing 5 items initially and creates an offset from the initialIndex parameter. Reviewers should not request centering the active item unless the UI design is changed; keep this behavior as the intended UX for this component.

Learnt from: jihee-dev
Repo: YAPP-Github/27th-App-Team-1-Android PR: 25
File: feature/travel/src/main/java/com/yapp/ndgl/feature/travel/mytravel/MyTravelContract.kt:52-54
Timestamp: 2026-02-17T22:21:31.141Z
Learning: Use the naming convention estimatedDuration for duration-related fields across Kotlin files to align with server API response parameter names and maintain consistency throughout the codebase. When introducing or renaming duration fields, replace generic names (e.g., duration, totalDuration) with estimatedDuration and update all references (data classes, mappings, JSON parsing) accordingly to avoid mismatch with server payloads.

Learnt from: jihee-dev
Repo: YAPP-Github/27th-App-Team-1-Android PR: 27
File: feature/home/src/main/java/com/yapp/ndgl/feature/home/search/TemplateSearchScreen.kt:0-0
Timestamp: 2026-02-18T18:16:40.700Z
Learning: Use the correct Hilt Compose ViewModel import across Kotlin Android files: import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel (from androidx.hilt:hilt-lifecycle-viewmodel-compose:1.3.0). The old import androidx.hilt.navigation.compose.hiltViewModel is deprecated in Hilt 1.3.0. Ensure the hilt-lifecycle-viewmodel-compose dependency is added to Gradle (both Gradle catalog and module dependencies) and update imports accordingly to avoid runtime issues or build failures.

Comment on lines +763 to +797
private fun updateItinerary() = viewModelScope.launch {
state.value.itineraries.forEachIndexed { dayIndex, itinerary ->
val day = dayIndex + 1
val itineraryItems = itinerary.places.map { place ->
ItineraryUpdateItem(
placeId = place.id,
day = day,
sequence = place.placeInfo.sequence,
startTime = place.startTime.toTimeStringWithSeconds(),
estimatedDuration = place.userData.estimatedDuration.inWholeMinutes.toInt(),
memo = place.userData.memo,
distanceKm = place.transportToNext?.let { it.distance / 1000.0 },
transportation = place.transportToNext?.let {
listOf(
TransportationItem(
mode = it.type.toTransportCategory(),
timeMin = it.duration.inWholeMinutes.toInt(),
),
)
},
)
}

suspendRunCatching {
userTravelRepository.updateItinerary(
travelId = travelId,
itineraries = itineraryItems,
)
}.onSuccess {
// TODO: Handle success
}.onFailure {
// TODO: Handle failure
}
}
}
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Feb 23, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

ast-grep --pattern 'fun updateItinerary($$$)'

Repository: YAPP-Github/27th-App-Team-1-Android

Length of output: 61


🏁 Script executed:

rg -n 'updateItinerary' --type=kt -C3

Repository: YAPP-Github/27th-App-Team-1-Android

Length of output: 105


🏁 Script executed:

# Find the repository structure and locate the repository interface/implementation
fd -e kt -type f | grep -i repository | head -20

Repository: YAPP-Github/27th-App-Team-1-Android

Length of output: 249


🏁 Script executed:

# Check userTravelRepository definition
rg -n 'interface.*TravelRepository|class.*TravelRepository' --type=kt -A 5

Repository: YAPP-Github/27th-App-Team-1-Android

Length of output: 105


🏁 Script executed:

# Find all Kotlin files related to travel repository
find . -name "*TravelRepository*" -o -name "*Travel*Dto*" | grep -E "\.(kt|java)$"

Repository: YAPP-Github/27th-App-Team-1-Android

Length of output: 167


🏁 Script executed:

# Search for updateItinerary without type specification
rg -n "updateItinerary" --max-count 20

Repository: YAPP-Github/27th-App-Team-1-Android

Length of output: 1131


🏁 Script executed:

# Check the file structure
git ls-files | grep -i travel | grep -i repository | head -10

Repository: YAPP-Github/27th-App-Team-1-Android

Length of output: 510


🏁 Script executed:

# Look at the TravelDetailViewModel file
git ls-files | grep -i "TravelDetailViewModel"

Repository: YAPP-Github/27th-App-Team-1-Android

Length of output: 174


🏁 Script executed:

# Check UserTravelRepository updateItinerary implementation
sed -n '60,75p' ./data/travel/src/main/java/com/yapp/ndgl/data/travel/repository/UserTravelRepository.kt

Repository: YAPP-Github/27th-App-Team-1-Android

Length of output: 525


🏁 Script executed:

# Check UserTravelApi updateItinerary definition
sed -n '40,55p' ./data/travel/src/main/java/com/yapp/ndgl/data/travel/api/UserTravelApi.kt

Repository: YAPP-Github/27th-App-Team-1-Android

Length of output: 367


🏁 Script executed:

# Check the DTO/Request structure used in updateItinerary
rg -n "ItineraryUpdateItem|data class.*Itinerary" --max-count 10

Repository: YAPP-Github/27th-App-Team-1-Android

Length of output: 3776


🏁 Script executed:

# Check the full UpdateItineraryRequest and ItineraryUpdateItem structure
cat ./data/travel/src/main/java/com/yapp/ndgl/data/travel/model/UpdateItineraryRequest.kt

Repository: YAPP-Github/27th-App-Team-1-Android

Length of output: 724


🏁 Script executed:

# Check the full TravelDetailViewModel updateItinerary() implementation
sed -n '763,797p' ./feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/TravelDetailViewModel.kt

Repository: YAPP-Github/27th-App-Team-1-Android

Length of output: 1546


🏁 Script executed:

# Check state.value.itineraries structure to understand data
rg -n "state.value.itineraries" -B5 -A5

Repository: YAPP-Github/27th-App-Team-1-Android

Length of output: 8155


API를 하나의 요청으로 일괄 전송하도록 리팩토링이 필요합니다.

현재 구현은 forEachIndexed 루프 내에서 일차별로 개별 API 요청을 보내고 있습니다. UpdateItineraryRequestList<ItineraryUpdateItem>을 받으므로 모든 일차를 단일 요청으로 전송할 수 있습니다. 추가로:

  • 변경된 일차만 감지하여 전송하는 로직이 없어 매번 모든 일차가 전송됩니다
  • 각 요청의 성공/실패를 개별적으로 추적하지 않으며 TODO 주석만 있습니다
  • 전체 요청의 성공/실패 처리 전략이 명확하지 않습니다

모든 일차를 하나의 요청으로 통합하고, 변경 감지 및 에러 처리 로직을 구현하십시오.

🤖 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/traveldetail/TravelDetailViewModel.kt`
around lines 763 - 797, The current updateItinerary() builds and sends per-day
requests inside the forEachIndexed loop; instead, collect ItineraryUpdateItem
objects for all days (iterate state.value.itineraries, mapping to
ItineraryUpdateItem with day = index+1, sequence, startTime, estimatedDuration,
memo, distanceKm, transportation) but only include items that actually changed
(compare to previous state or original payload you have) and then call
userTravelRepository.updateItinerary(travelId = travelId, itineraries =
allItems) once; handle the single suspendRunCatching result from that call and
implement proper onSuccess (e.g., update local state/acknowledge) and onFailure
(retry/report error) logic rather than per-day TODOs so success/failure of the
whole update is tracked.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

반영했습니다!

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check the current implementation of updateItinerary in TravelDetailViewModel.kt
ast-grep --pattern 'private fun updateItinerary() = $$$' feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/TravelDetailViewModel.kt

Length of output: 4967


🏁 Script executed:

#!/bin/bash
# Get the full updateItinerary function implementation with context
rg -A 40 "private fun updateItinerary\(\)" feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/TravelDetailViewModel.kt

Length of output: 1754


@mj010504, 확인해보니 아직 여러 번의 API 요청이 발생하는 구조가 남아있습니다. 현재 코드는 Line 764의 forEachIndexed 루프 안에서 Line 786-790의 API 호출이 일어나고 있어, 일차별로 개별 요청이 전송됩니다.

제안드린 리팩토링은 다음과 같이 모든 일차의 아이템을 먼저 수집한 후, 단일 API 요청으로 보내는 것입니다:

private fun updateItinerary() = viewModelScope.launch {
    // 1. 모든 일차의 ItineraryUpdateItem을 단일 리스트로 수집
    val allItineraryItems = state.value.itineraries.flatMapIndexed { dayIndex, itinerary ->
        val day = dayIndex + 1
        itinerary.places.map { place ->
            ItineraryUpdateItem(
                placeId = place.id,
                day = day,
                sequence = place.placeInfo.sequence,
                startTime = place.startTime.toTimeStringWithSeconds(),
                estimatedDuration = place.userData.estimatedDuration.inWholeMinutes.toInt(),
                memo = place.userData.memo,
                distanceKm = place.transportToNext?.let { it.distance / 1000.0 },
                transportation = place.transportToNext?.let {
                    listOf(
                        TransportationItem(
                            mode = it.type.toTransportCategory(),
                            timeMin = it.duration.inWholeMinutes.toInt(),
                        ),
                    )
                },
            )
        }
    }

    // 2. 단일 API 요청으로 전체 업데이트
    suspendRunCatching {
        userTravelRepository.updateItinerary(
            travelId = travelId,
            itineraries = allItineraryItems,
        )
    }.onSuccess {
        // 전체 업데이트 성공 처리
    }.onFailure {
        // 전체 업데이트 실패 처리 (롤백 등)
    }
}

주요 차이점:

  • forEachIndexedflatMapIndexed로 변경하여 모든 아이템을 단일 리스트로 수집
  • API 호출을 루프 으로 이동하여 한 번만 실행
  • 전체 요청의 성공/실패를 한 번에 처리

이렇게 하면 네트워크 오버헤드가 줄고, 에러 처리도 단순해집니다. 혹시 백엔드 API가 일차별 개별 요청을 요구한다면 말씀해주세요!


🧬 Code Graph Analysis Results

<no_relevant_code_snippets>


🧠 Learnings used
Learnt from: jihee-dev
Repo: YAPP-Github/27th-App-Team-1-Android PR: 25
File: data/travel/src/main/java/com/yapp/ndgl/data/travel/repository/UserTravelRepository.kt:28-30
Timestamp: 2026-02-17T22:18:43.706Z
Learning: `getUpcomingTravelList()` API는 `getUpcomingTravel()`과 달리 빈 목록일 때 HTTP 204를 반환하지 않으므로, 별도의 204 에러 핸들링이 필요하지 않음. 현재 구현(`return userTravelApi.getUpcomingTravelList().getData()`)이 올바른 방식임.

Learnt from: mj010504
Repo: YAPP-Github/27th-App-Team-1-Android PR: 26
File: feature/travel/src/main/java/com/yapp/ndgl/feature/travel/addplace/AddPlaceViewModel.kt:71-90
Timestamp: 2026-02-18T12:08:18.209Z
Learning: In the travel feature (place photo loading logic in AddPlaceViewModel.kt and AddItineraryViewModel.kt), retain a deliberate 1-second delay before each photo fetch when loading data for newly searched places. This accounts for backend latency needed to save place data before photos are retrievable. Ensure the delay is configurable (e.g., via feature flag or constant) and documented, so it can be adjusted or removed if backend performance improves. This guidance applies to all travel feature view models handling similar photo fetch flows.

Learnt from: mj010504
Repo: YAPP-Github/27th-App-Team-1-Android PR: 17
File: feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/component/TimelineContent.kt:243-251
Timestamp: 2026-02-11T12:08:02.377Z
Learning: In feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/component/TimelineContent.kt, the startIndex calculation for the WheelPicker intentionally does NOT subtract visibleItemCount/2 offset. This design choice preserves showing 5 items initially and creates an offset from the initialIndex parameter. Reviewers should not request centering the active item unless the UI design is changed; keep this behavior as the intended UX for this component.

Learnt from: jihee-dev
Repo: YAPP-Github/27th-App-Team-1-Android PR: 25
File: feature/travel/src/main/java/com/yapp/ndgl/feature/travel/mytravel/MyTravelContract.kt:52-54
Timestamp: 2026-02-17T22:21:31.141Z
Learning: Use the naming convention estimatedDuration for duration-related fields across Kotlin files to align with server API response parameter names and maintain consistency throughout the codebase. When introducing or renaming duration fields, replace generic names (e.g., duration, totalDuration) with estimatedDuration and update all references (data classes, mappings, JSON parsing) accordingly to avoid mismatch with server payloads.

Learnt from: jihee-dev
Repo: YAPP-Github/27th-App-Team-1-Android PR: 27
File: feature/home/src/main/java/com/yapp/ndgl/feature/home/search/TemplateSearchScreen.kt:0-0
Timestamp: 2026-02-18T18:16:40.700Z
Learning: Use the correct Hilt Compose ViewModel import across Kotlin Android files: import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel (from androidx.hilt:hilt-lifecycle-viewmodel-compose:1.3.0). The old import androidx.hilt.navigation.compose.hiltViewModel is deprecated in Hilt 1.3.0. Ensure the hilt-lifecycle-viewmodel-compose dependency is added to Gradle (both Gradle catalog and module dependencies) and update imports accordingly to avoid runtime issues or build failures.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

다시 확인 부탁드립니다.

@mj010504 mj010504 merged commit 2af1983 into develop Feb 23, 2026
4 checks passed
@mj010504 mj010504 deleted the feature/NDGL-98 branch February 23, 2026 12:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant