Skip to content

[NDGL-53] 내 여행 템플릿 화면 UI/UX#17

Merged
mj010504 merged 5 commits intodevelopfrom
design/NDGL-53
Feb 14, 2026
Merged

[NDGL-53] 내 여행 템플릿 화면 UI/UX#17
mj010504 merged 5 commits intodevelopfrom
design/NDGL-53

Conversation

@mj010504
Copy link
Copy Markdown
Contributor

@mj010504 mj010504 commented Feb 5, 2026

개요

  • 내 여행 템플릿 화면 UI/UX 제작

디자인

https://www.figma.com/design/qHn9o58ENLeHjiBWNuZFJx/Design_-YAPP-1%ED%8C%80-?node-id=3565-18311&t=ZStfQoOkSrncjY0d-0

영상

NDGL-53.mp4

변경사항

  • 내 여행 상세 화면 UI/UX 제작
  • 타임라인 자동생성 BottomSheet, Custom TimePicker 제작
  • PlaceItem LongClick 또는 편집하기 클릭 시 편집 모드 전환
  • 편집 모드 삭제 기능
  • 디자인 시스템에 NDGLBottomSheet, NDGLCheckBox 추가

참고사항

  • 편집모드 재배치와 교통수단 변경은 다른 PR로 따로 빼겠습니다!
  • 내 여행 탭에서 Go to Travel Detail Button 클릭하시고 해당 화면 리뷰해주시면 됩니다.
  • 다음은 해당 PR보단 다른 PR에서 리뷰해주시면 더 적절한 것들 입니다!
    • 타임라인 자동생성 부분
    • PlaceNumber밑 시간 표시
    • 편집모드 재배치
    • 교통수단 변경
    • 장소 바텀시트, 장소 상세보기 화면
    • 일정 추가하기 화면

테스트 체크 리스트

  • 타임라인 설정
  • 편집 모드 - 선택 삭제
  • 편집 모드 - 전체 선택

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • 여행 따라가기 화면 추가 — 지도, 일자 선택, 일별 장소/교통 표시 및 따라가기 CTA
    • 여행 상세 화면 추가 — 일정 편집, 시간 설정(타임라인), 장소 선택/삭제 지원
    • 날짜 선택(데이트피커) 및 달력 컴포넌트 추가
    • 다양한 디자인시스템 컴포넌트 추가(바텀시트, 체크박스, 콘텐츠 카드, 장소 카드, 맵 뷰 등)
  • UI 개선

    • 하단 네비게이션 및 바텀바 애니메이션/레이아웃 개선
    • 그림자 유틸 및 아이콘/문구 리소스 추가/갱신
  • 의존성 추가

    • 지도 및 이미지 로딩, 날짜/시간 라이브러리 추가
  • 구성

    • 앱에서 지도 사용을 위한 API 키 설정 반영

@mj010504 mj010504 requested a review from jihee-dev February 5, 2026 14:06
@mj010504 mj010504 self-assigned this Feb 5, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 5, 2026

Warning

Rate limit exceeded

@mj010504 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 17 minutes and 5 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

Walkthrough

Google Maps 키 및 관련 라이브러리 추가, 내부 패딩 전파, 여행 팔로우·여행 상세 기능(뷰모델·스크린·컴포넌트) 대규모 추가 및 디자인시스템·유틸·리소스 보강; 기존 TravelDetail 구현은 새 traveldetail 패키지로 이동/교체되었습니다.

Changes

Cohort / File(s) Summary
Build & Gradle 설정
app/build.gradle.kts, feature/travel/build.gradle.kts, gradle/libs.versions.toml, detekt-config.yml
MAPS_API_KEY 로컬 프로퍼티 연결 및 AndroidManifest placeholder 등록, maps/coil/kotlinx-datetime 의존성 추가, LongMethod 임계값 임시 상향.
앱 매니페스트
app/src/main/AndroidManifest.xml
com.google.android.geo.API_KEY meta-data로 ${MAPS_API_KEY} placeholder 추가.
네비게이션/라우트
navigation/src/main/java/.../Route.kt, feature/travel/src/.../navigation/TravelEntry.kt, feature/home/.../navigation/HomeEntry.kt, feature/travel-helper/.../navigation/TravelHelperEntry.kt
FollowTravel 및 DatePicker 라우트 추가, travel/home/travelHelper entry들에 innerPadding: PaddingValues 파라미터 전파 및 호출 사이트 수정.
앱 UI(네비게이션/바텀바)
app/src/main/java/.../BottomNavigationBar.kt, app/src/main/java/.../NDGLApp.kt, app/src/main/java/.../BottomNavTab.kt
커스텀 하단 네비게이션 구현(애니메이션 지표), Scaffold 구조 변경으로 innerPadding 전달 방식 변경, 탭 아이콘 리소스 업데이트.
디자인 시스템 컴포넌트
core/ui/.../NDGLBottomSheet.kt, core/ui/.../NDGLCheckBox.kt, core/ui/.../NDGLNavigationBar.kt
모달 바텀시트·체크박스 컴포넌트 추가 및 NDGLNavigationBar 레이아웃/사이징 조정과 프리뷰 추가.
디자인 리소스(추가/삭제)
core/ui/src/main/res/drawable/ic_140_no_schedule_calendar.xml, .../ic_20_check.xml, core/ui/src/main/res/drawable/ic_24_smarrow_down.xml, core/ui/src/main/res/drawable/ic_nav_*.xml (삭제)
여러 벡터 드로어블 추가 및 기존 네비 아이콘 리소스(ic_nav_helper/home/travel) 삭제.
문자열 리소스
core/ui/src/main/res/values/strings.xml
여러 UI 텍스트(약 40개) 추가 — 날짜/타임라인/편집/삭제 등 문자열 정의.
유틸리티
core/util/src/main/java/.../DurationUtil.kt, core/ui/src/main/java/.../DropShadow.kt
Duration 확장(formatString, toTimeString) 추가 및 Modifier용 dropShadow 유틸 추가.
홈/헬퍼 화면 패딩 적용
feature/home/src/.../HomeScreen.kt, feature/travel-helper/src/.../TravelHelperScreen.kt
HomeRoute/TravelHelperRoute 및 Screen에 innerPadding: PaddingValues 파라미터 추가하고 LazyColumn에 .padding(innerPadding) 적용.
feature/travel: 종합 추가 — FollowTravel
feature/travel/src/.../followtravel/*
FollowTravelContract, FollowTravelViewModel(AssistedFactory), FollowTravelScreen 및 컴포넌트(ContentCard, PlaceItem, TransportSegment, TravelMap) 추가 — 상태/인텐트/사이드이펙트, 지도·리스트·CTA 등 UI 흐름 구현.
feature/travel: Travel 화면 리팩터/네비게이션 변경
feature/travel/src/.../travel/*
TravelContract/TravelViewModel/TravelScreen 시그니처 변경(세부·팔로우 네비게이션 분리), 클릭 인텐트 추가, innerPadding 전파.
feature/travel: TravelDetail (새 패키지)
feature/travel/src/.../traveldetail/*
TravelDetailContract, TravelDetailViewModel(AssistedFactory), TravelDetailScreen 및 다수 컴포넌트(ContentCard, EditControlBar, PlaceItem, TimelineContent, TransportSegment, TravelMap) 대규모 추가 — 편집/타임라인/달력 모달/삭제 등 완전 구현.
feature/travel: DatePicker
feature/travel/src/.../datepicker/*
DatePickerContract, DatePickerViewModel, DatePickerScreen 및 CalendarView 컴포넌트 추가 — 날짜 범위 선택, 유효성 체크, 네비게이션 사이드이펙트 통합.
제거된 파일
feature/travel/src/main/java/.../detail/TravelDetailScreen.kt, .../detail/TravelDetailViewModel.kt, .claude/settings.local.json
구형 TravelDetail 구현 및 로컬 Claude 설정 파일 삭제; .gitignore에 해당 파일 무시 추가.

Sequence Diagram(s)

sequenceDiagram
    participant User as 사용자
    participant TravelScreen as TravelScreen
    participant TravelVM as TravelViewModel
    participant Nav as TravelEntry
    participant FollowRoute as Route.FollowTravel
    participant FollowVM as FollowTravelViewModel
    participant FollowScreen as FollowTravelScreen

    User->>TravelScreen: 여행 항목 클릭
    TravelScreen->>TravelVM: ClickTravelDetail(travelId) 인텐트
    TravelVM->>TravelVM: postSideEffect(NavigateToFollowTravel(travelId))
    TravelScreen->>Nav: navigateToFollowTravel(travelId)
    Nav->>FollowRoute: Route.FollowTravel(travelId) 생성/네비게이션
    FollowRoute->>FollowVM: FollowTravelViewModel.Factory.create(travelId)
    FollowVM->>FollowScreen: 상태(FollowTravelState) 발행
    FollowScreen->>User: FollowTravel 화면 렌더링
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • jihee-dev
🚥 Pre-merge checks | ✅ 2 | ❌ 2
❌ Failed checks (2 warnings)
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.
Merge Conflict Detection ⚠️ Warning ❌ Merge conflicts detected (4 files):

⚔️ core/ui/src/main/res/values/strings.xml (content)
⚔️ core/util/src/main/java/com/yapp/ndgl/core/util/DurationUtil.kt (content)
⚔️ feature/travel/src/main/java/com/yapp/ndgl/feature/travel/navigation/TravelEntry.kt (content)
⚔️ navigation/src/main/java/com/yapp/ndgl/navigation/Route.kt (content)

These conflicts must be resolved before merging into develop.
Resolve conflicts locally and push changes to this branch.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목은 주요 변경사항을 명확하게 나타내며, '내 여행 템플릿 화면 UI/UX' 구현이라는 핵심 목표를 잘 요약하고 있습니다.

✏️ 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 design/NDGL-53

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.

@mj010504 mj010504 force-pushed the design/NDGL-53 branch 5 times, most recently from cadef03 to 4665ab9 Compare February 6, 2026 13:45
@mj010504 mj010504 force-pushed the design/NDGL-53 branch 2 times, most recently from e591261 to e6dc184 Compare February 10, 2026 16:13
@mj010504 mj010504 marked this pull request as ready for review February 11, 2026 07:23
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: 5

Caution

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

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

59-74: ⚠️ Potential issue | 🟠 Major

하드코딩된 travelId로 네비게이션 고정됨

clickTravel(123) / clickTravelDetail(456)는 실제 선택된 여행과 무관하게 항상 고정 ID로 이동합니다. 실제 데이터와 연결된 ID를 전달하거나, 데모/리뷰용 버튼이라면 debug 전용으로 분리하는 편이 안전합니다.

🤖 Fix all issues with AI agents
In
`@feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/component/TimelineContent.kt`:
- Around line 243-251: startIndex calculation currently centers into a huge list
but doesn't account for the visibleItemCount offset, causing the WheelPicker to
select initialIndex+2; update the startIndex expression (that uses
totalItemsCount, items.size and initialIndex) to subtract visibleItemCount / 2
so that the initial visible center equals initialIndex (adjust the computed
startIndex used to create listState via rememberLazyListState); ensure
derivedStateOf/currentIndex logic using visibleItemCount remains consistent with
this change.
- Around line 154-156: isTimeSet currently requires
selectedStartTime.inWholeHours > 0 and selectedEndTime.inWholeHours > 0 which
blocks 00:00; relax the check so midnight is allowed by changing the condition
on selectedStartTime and selectedEndTime to use >= 0 (or otherwise check they
are non-negative/initialized) in the isTimeSet computation (refer to isTimeSet,
selectedStartTime, selectedEndTime, and keep the existing
isEndTimeExceeds24Hours logic intact).

In
`@feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/component/TravelMap.kt`:
- Around line 58-77: The code currently calls places.first() when building
initialPosition and builds LatLngBounds from places in the LaunchedEffect, which
will throw for an empty list; update the TravelMap composable to guard against
empty places by checking places.isEmpty() early and either return a placeholder
UI or use a default CameraPosition (replace use of initialPosition /
places.first()), and modify the LaunchedEffect to skip bounds creation (or use a
default CameraUpdate) when places.isEmpty(); update references to
initialPosition, cameraPositionState, and the LaunchedEffect block so they no
longer assume places has at least one element.

In
`@feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/TravelDetailContract.kt`:
- Around line 52-59: The formatString() in TravelDetailContract incorrectly
returns "만원" for any amount < 10000; update the first branch to produce the
correct small-amount formatting by checking thousands and remainder: when amount
< 10000, if amount % 1000 == 0 return "${amount / 1000}천원", else return
"${amount}원" (or use the existing comma-format convention used elsewhere);
adjust the amount < 10000 branch in formatString() accordingly so small amounts
are rendered as "X천원" or "X원" instead of "만원".

In
`@feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/TravelDetailViewModel.kt`:
- Around line 315-338: reorderPlaces is applying the move to every itinerary
because it iterates tempItineraries unconditionally; restrict the change to only
the currently selected itinerary (e.g., compare itinerary.id or itinerary.date
to the viewmodel's selected identifier like selectedDate or selectedItineraryId)
so other itineraries remain untouched. In the reorderPlaces function, when
mapping tempItineraries, only perform the removeAt/add and sequence
recalculation for the itinerary that matches the selected identifier; for others
return itinerary unchanged. Also ensure the index bounds check
(fromIndex/toIndex in mutablePlaces.indices) uses the selected itinerary's
places so indices are valid for that specific day's list.
🧹 Nitpick comments (7)
core/ui/src/main/java/com/yapp/ndgl/core/ui/designsystem/NDGLCheckBox.kt (2)

22-50: 접근성 및 유연성 개선 필요

현재 구현에서 몇 가지 개선이 필요합니다:

  1. 접근성(Accessibility): contentDescription이 없어 스크린 리더 사용자가 체크박스 상태를 인식할 수 없습니다.
  2. Modifier 파라미터 누락: 외부에서 크기, 패딩 등을 조정할 수 없습니다.
  3. 클릭 피드백: clickable에 indication이 없어 시각적 피드백이 부족할 수 있습니다.
♻️ 접근성 및 유연성 개선 제안
 `@Composable`
 fun NDGLCheckbox(
     checked: Boolean,
     onClick: () -> Unit,
+    modifier: Modifier = Modifier,
+    contentDescription: String? = null,
 ) {
     Box(
-        modifier = Modifier
+        modifier = modifier
             .size(24.dp)
             .clip(RoundedCornerShape(4.dp))
             .background(
                 if (checked) NDGLTheme.colors.green500 else NDGLTheme.colors.white,
             )
             .border(
                 width = 1.25.dp,
                 color = if (checked) Color.Unspecified else NDGLTheme.colors.black400,
                 shape = RoundedCornerShape(4.dp),
             )
-            .clickable { onClick() },
+            .clickable(
+                onClick = onClick,
+                role = Role.Checkbox,
+            )
+            .semantics {
+                this.stateDescription = if (checked) "선택됨" else "선택 안 됨"
+                contentDescription?.let { this.contentDescription = it }
+            },
         contentAlignment = Alignment.Center,
     )

추가 import 필요:

import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.stateDescription

34-38: Color.Unspecified 사용 시 주의

checked 상태일 때 Color.Unspecified를 border color로 사용하고 있습니다. 이 경우 border가 그려지지 않는 것을 의도한 것으로 보이지만, Color.Transparent가 의도를 더 명확하게 표현합니다.

♻️ 명시적인 Color.Transparent 사용 제안
             .border(
                 width = 1.25.dp,
-                color = if (checked) Color.Unspecified else NDGLTheme.colors.black400,
+                color = if (checked) Color.Transparent else NDGLTheme.colors.black400,
                 shape = RoundedCornerShape(4.dp),
             )
core/ui/src/main/java/com/yapp/ndgl/core/ui/designsystem/NDGLBottomSheet.kt (1)

19-42: 외부에서 sheet 상태 제어 불가

현재 sheetState가 함수 내부에서 생성되어 외부에서 프로그래매틱하게 sheet를 닫거나 상태를 제어할 수 없습니다. 예를 들어, 버튼 클릭 시 sheetState.hide()를 호출하여 sheet를 닫는 것이 불가능합니다.

사용 시나리오에 따라 외부에서 sheetState를 전달받을 수 있도록 파라미터화하는 것을 고려해 주세요.

♻️ sheetState 파라미터 추가 제안
 `@OptIn`(ExperimentalMaterial3Api::class)
 `@Composable`
 fun NDGLBottomSheet(
     modifier: Modifier = Modifier,
     onDismissRequest: () -> Unit,
     showDragHandle: Boolean = true,
+    sheetState: SheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true),
     content: `@Composable` ColumnScope.() -> Unit,
 ) {
-    val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
-
     ModalBottomSheet(

추가 import 필요:

import androidx.compose.material3.SheetState
feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/component/TimelineContent.kt (1)

125-135: 시간 휠의 초기 위치를 현재 값으로 맞추면 UX 개선
Line 125-134에서 초기 인덱스를 상수로 고정해 기존 선택값이 반영되지 않습니다. selectedStartTime 기반으로 초기 인덱스를 계산하면 재진입 시 값이 유지됩니다.

♻️ 제안 수정
             val hourItems = remember { (0..23).toList() }
             val minuteItems = remember { (0..55 step 5).toList() }
 
             var selectedHour by remember { mutableIntStateOf(hourItems[0]) }
             var selectedMinute by remember { mutableIntStateOf(minuteItems[0]) }
+            val initialHourIndex = (selectedStartTime.inWholeHours % 24).toInt()
+            val initialMinuteIndex = ((selectedStartTime.inWholeMinutes % 60) / 5).toInt()
 ...
                     WheelPicker(
                         items = hourItems,
                         onItemSelected = { selectedHour = it },
-                        initialIndex = 12,
+                        initialIndex = initialHourIndex,
                     )
                     WheelPicker(
                         items = minuteItems,
                         onItemSelected = { selectedMinute = it },
-                        initialIndex = 0,
+                        initialIndex = initialMinuteIndex,
                         padEnabled = true,
                     )
feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/component/ContentCard.kt (2)

80-88: 접근성: contentDescription에 string resource 사용 권장

contentDescription이 하드코딩된 영어 문자열로 되어 있습니다. 다국어 지원 및 일관성을 위해 strings.xml의 리소스를 사용하는 것이 좋습니다.

♻️ 제안 수정
 AsyncImage(
     model = contentInfo.videoInfo.profileImage,
-    contentDescription = "Profile Image",
+    contentDescription = stringResource(R.string.content_description_profile_image),
     modifier = Modifier
         .size(48.dp)
         .clip(CircleShape)
         .background(Color.LightGray),
     contentScale = ContentScale.Crop,
 )

strings.xml에 추가:

<string name="content_description_profile_image">프로필 이미지</string>
<string name="content_description_video_thumbnail">비디오 썸네일</string>
<string name="content_description_expand">펼치기</string>

74-77: 중복 클릭 핸들러 검토

Row(Line 74-77)와 Icon(Line 127-133) 모두 clickableisExpanded를 토글합니다. Icon 영역을 클릭하면 이벤트가 두 번 처리될 수 있습니다. Row에만 클릭 핸들러를 두거나, Icon에서 클릭 핸들러를 제거하는 것을 권장합니다.

♻️ 제안 수정
 Icon(
     imageVector = ImageVector.vectorResource(R.drawable.ic_28_chevron_down),
     contentDescription = "Expand",
     modifier = Modifier
-        .rotate(rotationState)
-        .clickable { isExpanded = !isExpanded },
+        .rotate(rotationState),
 )

Also applies to: 127-133

feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/TravelDetailViewModel.kt (1)

306-313: FIXME 주석: endTime 계산 로직 후속 작업 필요

endTime = duration + 15.hours 하드코딩에 FIXME가 있습니다. 실제 일정의 총 소요 시간을 기반으로 계산하도록 후속 구현이 필요합니다.

이 작업을 추적하기 위한 이슈를 생성해 드릴까요?

@mj010504 mj010504 force-pushed the design/NDGL-49 branch 2 times, most recently from e6e34e3 to 1b0fc4a Compare February 14, 2026 01:48
Base automatically changed from design/NDGL-49 to develop February 14, 2026 01:52
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

🤖 Fix all issues with AI agents
In `@app/build.gradle.kts`:
- Around line 12-18: The current defaultConfig block reads local.properties with
rootProject.file("local.properties").bufferedReader(), which throws if the file
is missing; update the logic around localProperties creation in defaultConfig so
it first checks for the file's existence (or uses safe file access/try-catch)
and only loads when present, otherwise fall back to an empty Properties
instance; then set manifestPlaceholders["MAPS_API_KEY"] using
localProperties.getProperty("MAPS_API_KEY", "") so builds in CI or fresh clones
won’t fail when local.properties is absent.

In `@core/ui/src/main/res/drawable/ic_20_check.xml`:
- Around line 1-5: The drawable filename ic_20_check does not match the vector
size (android:width/android:height set to 24dp); either rename the asset to
ic_24_check to match the 24dp dimensions or change the vector attributes
android:width and android:height to 20dp to match the filename; update any
references to the drawable if you rename the file so references to ic_20_check
are switched to ic_24_check, or if you change dimensions ensure the file name
remains ic_20_check and the viewport/paths still render correctly at 20dp.

In `@core/ui/src/main/res/values/strings.xml`:
- Line 33: The two string resources timeline_auto_setting and
timeline_auto_setting_title both contain the identical value "타임라인 자동생성";
confirm whether they were intentionally distinct and if not, consolidate them by
keeping one resource (pick a canonical name, e.g., timeline_auto_setting_title
or timeline_auto_setting), update all usages to reference that single resource,
and remove the duplicate entry to avoid redundant resources and maintain a
single source of truth.

In
`@feature/travel/src/main/java/com/yapp/ndgl/feature/travel/followtravel/component/ContentCard.kt`:
- Around line 80-88: The contentDescription values in ContentCard's AsyncImage
usages (e.g., the AsyncImage showing profileImage and other AsyncImage calls
around lines with "Expand" and "Video Thumbnail") are hardcoded English strings;
replace them with stringResource(...) references for localization or set
contentDescription = null for purely decorative images. Locate the AsyncImage
invocations in the ContentCard composable and update each contentDescription to
use the appropriate string resource key (add entries to strings.xml) or null
when decorative, ensuring accessibility and i18n support.

In
`@feature/travel/src/main/java/com/yapp/ndgl/feature/travel/followtravel/FollowTravelContract.kt`:
- Around line 19-70: Several core data models (ContentInfo, VideoInfo, Budget,
TravelPlace, PlaceType, TransportSegment, TransportType) and the
PlaceType.getColor() extension are duplicated between followtravel and
traveldetail; extract these into a single shared package under feature/travel
(e.g., common or model/contract) and replace the duplicated definitions by
importing the shared types. Specifically, create shared definitions for
ContentInfo, VideoInfo, Budget (with formatString), Itinerary, TravelPlace,
TransportSegment, TransportType and the PlaceType enum plus its getColor()
extension, then update all usages and imports in followtravel and traveldetail
to reference the new shared package and remove the old duplicated files. Ensure
public API and data shapes remain unchanged so callers (e.g., functions
referencing Budget.formatString or TravelPlace properties) compile without
further changes.

In
`@feature/travel/src/main/java/com/yapp/ndgl/feature/travel/followtravel/FollowTravelScreen.kt`:
- Around line 222-231: The Preview function FollowTravelScreenPreview is calling
FollowTravelScreen directly without providing the NDGLTheme CompositionLocal,
causing Preview rendering errors; wrap the FollowTravelScreen call with
NDGLTheme in FollowTravelScreenPreview so NDGLTheme.colors and
NDGLTheme.typography are available (mirror how TravelDetailScreen's preview is
wrapped), i.e., change FollowTravelScreenPreview to create an NDGLTheme {
FollowTravelScreen(...) } around the existing parameters.

In
`@feature/travel/src/main/java/com/yapp/ndgl/feature/travel/followtravel/FollowTravelViewModel.kt`:
- Around line 113-149: The mock Itinerary for day 2 has two TransportSegment
entries although there are only two TravelPlace items (so transport segments
should be N-1 = 1); update the Itinerary instance in FollowTravelViewModel (the
Itinerary with places containing TravelPlace id 8 and 9) to remove the extra
TransportSegment so transportSegments contains a single TransportSegment (e.g.,
the WALK or TRAIN segment you want to keep), ensuring the TransportSegment list
length matches places.size - 1.

In
`@feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/component/ContentCard.kt`:
- Around line 52-207: The ContentCard composable and its data models
(ContentInfo, VideoInfo, Budget, PlaceType, TravelPlace) are duplicated across
followtravel and traveldetail; consolidate by moving the ContentCard composable
and the shared models into a single shared module (e.g., core/ui or
feature/travel root) and update both packages to import the single source; also
replace hardcoded contentDescription literals in ContentCard ("Profile Image",
"Expand", "Video Thumbnail") with stringResource lookups (use string keys you
add to resources) so the AsyncImage and Icon contentDescription parameters use
stringResource(R.string.xxx) instead of raw strings.

In
`@feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/component/TimelineContent.kt`:
- Around line 80-93: The close icon Box (Box with Modifier.size(32.dp) ...
.padding(8.dp)) currently both hardcodes some contentDescriptions elsewhere and
causes the 24dp icon to be clipped because 32dp - 8dp*2 = 16dp inner area;
change any hardcoded contentDescription (e.g., "Profile Image") to use
stringResource(R.string.profile_image) and prevent clipping by increasing the
Box size to at least 40.dp and/or reducing padding (e.g., padding(4.dp)), and
explicitly set the Icon modifier to Modifier.size(24.dp) while keeping the
existing ImageVector.vectorResource(R.drawable.ic_24_close) and
stringResource(R.string.close) for the close button; update the Box/Icon
modifiers in TimelineContent.kt (the Box containing onDismissRequest and the
Icon call) accordingly.

In
`@feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/TravelDetailScreen.kt`:
- Around line 371-381: The NDGLModal in TravelDetailScreen is wired with
reversed actions: swap the callbacks so that positiveButtonText's handler calls
confirmCancelEditMode and negativeButtonText's handler calls
dismissCancelEditModal; locate the NDGLModal invocation guarded by
state.showCancelEditModal and update the onNegativeButtonClick and
onPositiveButtonClick assignments accordingly (keep titles/texts unchanged).
- Line 40: The import for hiltViewModel is using the deprecated path
androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel; update the import to
androidx.hilt.navigation.compose.hiltViewModel in this file
(TravelDetailScreen.kt) and the other affected screens (HomeScreen.kt,
TravelScreen.kt, FollowTravelScreen.kt, TravelEntry.kt, TravelHelperScreen.kt,
AuthScreen.kt) so usages of hiltViewModel (e.g., calls like val vm:
TravelDetailViewModel = hiltViewModel()) resolve to the supported package from
hilt-navigation-compose.

In
`@feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/TravelDetailViewModel.kt`:
- Around line 182-184: selectDay currently only updates selectedDay, so when
switching days in edit mode previously selectedPlaceIds from another day persist
and break checkSelectAll and confirmDeleteSelectedPlaces; modify selectDay to,
when isEditMode is true, also reset selectedPlaceIds (e.g., set to emptySet())
inside the same reduce call (update selectedDay and selectedPlaceIds together)
so switching days clears prior selections and prevents cross-day
deletion/selection bugs; reference selectDay, isEditMode, selectedPlaceIds,
reduce, copy, checkSelectAll, and confirmDeleteSelectedPlaces.
- Around line 215-229: The checkSelectAll function compares only sizes which
misdetects full-selection when selectedPlaceIds contains IDs from other days;
update checkSelectAll (in TravelDetailViewModel) to compare sets instead of
sizes by checking selectedPlaceIds.containsAll(currentDayPlaceIds) (or
selectedPlaceIds == currentDayPlaceIds) so the selection is true only when all
currentDayPlaceIds are included; keep current logic for choosing
currentItineraries (tempItineraries vs itineraries) and selectedDay as-is.

In `@gradle/libs.versions.toml`:
- Around line 45-51: Update the dependency versions in libs.versions.toml: set
mapsCompose = "8.1.0" and playServicesMaps = "20.0.0", and upgrade coil to
"3.3.0"; additionally, when moving Coil to 3.x update its Gradle coordinates to
the new group ID (io.coil-kt.coil3) wherever the Coil dependency is declared
(search for the symbol coil and replace the old group/artifact with the new
group for Coil 3.x).
🧹 Nitpick comments (33)
feature/travel-helper/src/main/java/com/yapp/ndgl/feature/travelhelper/TravelHelperScreen.kt (1)

15-21: viewModel이 주입만 되고 사용되지 않고 있습니다.

현재 TravelHelperRoute에서 viewModelhiltViewModel()로 주입받고 있지만, TravelHelperScreen에 전달하거나 사용하는 곳이 없습니다. 향후 사용 예정이라면 괜찮지만, 현재 상태에서는 불필요한 의존성입니다. innerPadding 전파 자체는 문제 없습니다.

detekt-config.yml (1)

3-3: FIXME 임시 변경이 영구적으로 남을 위험이 있습니다.

LongMethod 임계값을 100에서 150으로 올린 것은 #FIXME("임시 테스트용")이라고 명시되어 있지만, 되돌리기 위한 이슈 트래킹이 없으면 그대로 남을 가능성이 높습니다. Composable 함수는 이미 ignoreAnnotated로 제외되므로, 이 변경이 필요한 구체적인 원인을 확인해 주세요.

  • 원인이 Composable이 아닌 함수라면 해당 함수를 리팩터링하는 것이 바람직합니다.
  • 원인이 Composable 함수라면 ignoreAnnotated에 이미 포함되어 있으므로 임계값 변경이 불필요합니다.

되돌리기를 보장하기 위해 관련 이슈를 생성하거나, 이 PR에서 원래 값(100)으로 복원하는 것을 권장합니다.

♻️ 원래 값 복원 제안
-        threshold: 150 `#FIXME`("임시 테스트용")
+        threshold: 100
core/ui/src/main/java/com/yapp/ndgl/core/ui/designsystem/NDGLBottomSheet.kt (1)

21-27: sheetState를 외부에서 주입할 수 있도록 파라미터로 노출하는 것을 권장합니다.

현재 sheetState가 내부에서만 생성되어, 호출부에서 sheetState.hide() 등으로 프로그래밍 방식의 제어가 불가능합니다. 디자인 시스템 컴포넌트로서 재사용성을 높이려면 외부 주입을 허용하되, 기본값을 제공하는 패턴이 적합합니다.

♻️ sheetState 파라미터 노출 제안
 `@OptIn`(ExperimentalMaterial3Api::class)
 `@Composable`
 fun NDGLBottomSheet(
     modifier: Modifier = Modifier,
     onDismissRequest: () -> Unit,
     showDragHandle: Boolean = true,
+    sheetState: SheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true),
     content: `@Composable` ColumnScope.() -> Unit,
 ) {
-    val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
-
     ModalBottomSheet(
core/ui/src/main/res/drawable/ic_140_no_schedule_calendar.xml (1)

1-193: 복잡한 벡터 드로어블의 렌더링 성능 고려

이 파일은 약 60개 이상의 <path> 요소와 다수의 <clip-path>를 포함한 매우 복잡한 벡터 드로어블입니다. 이런 고밀도 벡터는 저사양 기기에서 인플레이션 및 렌더링 시 성능 저하(jank)를 유발할 수 있습니다.

일러스트레이션 스타일의 에셋은 WebP 또는 PNG (density별 제공) 포맷이 렌더링 성능 면에서 더 유리할 수 있습니다. 다만 현재 단일 해상도만 필요하고 empty state 등 빈번히 렌더되지 않는 곳에서만 사용된다면 현재 상태로도 괜찮습니다.

feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/component/TransportSegment.kt (2)

26-58: 컴포저블과 데이터 클래스의 이름 충돌

TransportSegment 컴포저블 함수명이 파라미터 타입인 TransportSegment 데이터 클래스와 동일합니다. Kotlin 컴파일러가 구분할 수 있어 동작에는 문제 없지만, 코드 가독성 측면에서 혼동을 줄 수 있습니다. 예를 들어 TransportSegmentRow 등으로 구분하는 것을 고려해볼 수 있습니다.


31-36: clickable에 접근성 라벨 누락

clickable modifier에 onClickLabel이 없어 TalkBack 등 접근성 도구에서 이 요소의 동작을 설명하지 못합니다. 교통수단 세그먼트의 클릭 목적을 설명하는 라벨 추가를 권장합니다.

core/ui/src/main/java/com/yapp/ndgl/core/ui/designsystem/NDGLCheckBox.kt (1)

27-39: 접근성(Accessibility): clickable 대신 toggleable을 사용하세요.

현재 .clickable를 사용하고 있어 TalkBack 등 스크린 리더에서 이 컴포넌트가 체크박스임을 인식하지 못합니다. Modifier.toggleablerole = Role.Checkbox를 지정하면 접근성 서비스에 체크박스 상태가 올바르게 전달됩니다.

♻️ 접근성 개선 제안
+import androidx.compose.ui.semantics.Role
+import androidx.compose.foundation.selection.toggleable
     Box(
         modifier = Modifier
             .size(24.dp)
             .clip(RoundedCornerShape(4.dp))
             .background(
                 if (checked) NDGLTheme.colors.green500 else NDGLTheme.colors.white,
             )
             .border(
                 width = 1.25.dp,
                 color = if (checked) Color.Unspecified else NDGLTheme.colors.black400,
                 shape = RoundedCornerShape(4.dp),
             )
-            .clickable { onClick() },
+            .toggleable(
+                value = checked,
+                role = Role.Checkbox,
+                onValueChange = { onClick() },
+            ),
         contentAlignment = Alignment.Center,
     ) {
feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/component/TravelDetailToolBar.kt (1)

42-55: 최소 터치 영역 확보를 위한 개선 제안

clickableRow에 직접 적용되어 있어, 콘텐츠 크기에 따라 터치 영역이 Material 권장 최소 크기(48dp)보다 작을 수 있습니다. 동일한 패턴이 EditTravelButton에도 적용됩니다. Modifier.minimumInteractiveComponentSize()를 추가하면 접근성이 향상됩니다.

feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/TravelDetailViewModel.kt (2)

306-313: endTime 하드코딩 FIXME 확인

duration + 15.hours가 하드코딩되어 있습니다. FIXME 주석이 있으므로 인지하고 계신 것으로 보이며, 실제 일정 기반 계산으로 대체가 필요합니다.

이 작업에 대한 GitHub 이슈를 생성해드릴까요?


243-267: 삭제 후 교통수단 처리가 불완전

Line 253-254에서 transportSegments를 단순히 take(remainingPlaces.size - 1)로 잘라내고 있습니다. 장소가 중간에서 삭제되면, 남은 transport segment의 순서가 실제 장소 간 경로와 불일치할 수 있습니다. TODO 주석으로 인지하고 계신 부분이지만, 추후 반드시 올바른 매핑이 필요합니다.

이 작업에 대한 GitHub 이슈를 생성해드릴까요?

feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/component/TravelMap.kt (1)

89-99: clip(RoundedCornerShape) GoogleMap에 시각적으로 적용되지 않을 수 있음

GoogleMap은 내부적으로 SurfaceView 또는 TextureView를 사용하는데, SurfaceView는 Compose의 clip modifier를 지원하지 않습니다. 현재 Maps SDK가 사용하는 렌더링 방식(TextureView 또는 SurfaceView)에 따라 둥근 모서리가 실제 디바이스에서 표시되지 않을 수 있습니다. 시각적으로 확인한 후 필요하면 오버레이를 이용한 마스킹 방식을 고려하세요.

feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/component/TimelineContent.kt (2)

97-151: 시간 설정 모드에서 매직 넘버 15.hours가 사용됩니다.

Line 149의 selectedStartTime + 15.hours에서 15시간이 하드코딩되어 있습니다. 이 값의 의미(예: 기본 일정 길이)를 상수로 추출하면 가독성과 유지보수성이 향상됩니다.

♻️ 제안
+private val DEFAULT_ITINERARY_DURATION = 15.hours
+
 // Line 149 내부:
-                    selectedEndTime = selectedStartTime + 15.hours
+                    selectedEndTime = selectedStartTime + DEFAULT_ITINERARY_DURATION

270-313: WheelPicker의 접근성(Accessibility) 지원이 부족합니다.

현재 WheelPicker는 contentDescription이나 semantics 정보가 없어 TalkBack 사용자가 시간 값을 인식하기 어렵습니다. 접근성 개선을 위해 선택된 항목에 semantics 블록을 추가하는 것을 고려해 주세요.

feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/component/ContentCard.kt (2)

80-88: contentDescription에 하드코딩된 영어 문자열 대신 문자열 리소스를 사용해 주세요.

Line 82 "Profile Image", Line 129 "Expand", Line 146 "Video Thumbnail" 등이 하드코딩되어 있습니다. 접근성 및 다국어 지원을 위해 stringResource()를 사용하거나, 장식용 이미지라면 null로 설정하는 것이 좋습니다.

♻️ 제안
 AsyncImage(
     model = contentInfo.videoInfo.profileImage,
-    contentDescription = "Profile Image",
+    contentDescription = null, // 장식용 이미지
 ...
 Icon(
     imageVector = ImageVector.vectorResource(R.drawable.ic_28_chevron_down),
-    contentDescription = "Expand",
+    contentDescription = null, // 접근성: Row의 clickable에서 처리
 ...
 AsyncImage(
     model = contentInfo.videoInfo.thumbnail,
-    contentDescription = "Video Thumbnail",
+    contentDescription = null, // 장식용 이미지

Also applies to: 144-153, 127-133


209-232: Preview 함수의 가시성을 private로 변경하세요.

PreviewContentCard()public으로 선언되어 있지만, Preview 함수는 외부에서 사용할 필요가 없으므로 private로 제한하는 것이 일반적입니다. traveldetail 패키지의 PlaceItem.kt에서는 이미 private 가시성을 사용하고 있어 일관성이 떨어집니다.

♻️ 제안
 `@Preview`
 `@Composable`
-fun PreviewContentCard() {
+private fun PreviewContentCard() {
feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/component/PlaceItem.kt (3)

74-116: EditablePlaceItemmodifier 파라미터가 루트 컴포저블이 아닌 내부 Icon에 적용됩니다.

Compose 컨벤션에 따르면 modifier 파라미터는 최상위 컴포저블에 적용되어야 합니다. 현재 Line 82에서 Box는 새로운 Modifier를 사용하고, 전달받은 modifier는 Line 109의 드래그 핸들 Icon에만 적용됩니다. 드래그 핸들용 modifier가 필요하다면 별도 파라미터(예: dragModifier)로 분리하세요.

♻️ 제안
 `@Composable`
 internal fun EditablePlaceItem(
-    modifier: Modifier = Modifier,
+    dragHandleModifier: Modifier = Modifier,
     place: TravelPlace,
     checked: Boolean,
     onCheck: () -> Unit,
 ) {
     Box(
-        modifier = Modifier
+        modifier = Modifier  // 또는 외부 modifier 파라미터를 추가하여 적용
             .fillMaxWidth()
             .padding(horizontal = 24.dp),
     ) {
         // ...
             Icon(
-                modifier = modifier,
+                modifier = dragHandleModifier,
                 imageVector = ImageVector.vectorResource(R.drawable.ic_24_menu),

86-96: 불필요한 .padding(horizontal = 0.dp) 제거

Line 93의 .padding(horizontal = 0.dp)는 아무 효과가 없으므로 제거해 주세요.

♻️ 제안
                 .background(
                     color = NDGLTheme.colors.white,
                     shape = RoundedCornerShape(16.dp),
                 )
-                .padding(horizontal = 0.dp),
+                ,

188-196: contentDescription이 하드코딩된 영어 문자열입니다.

Line 190의 "Place Thumbnail"은 리소스 문자열을 사용하거나 장식용 이미지이면 null로 설정하세요.

feature/travel/src/main/java/com/yapp/ndgl/feature/travel/followtravel/FollowTravelContract.kt (2)

91-109: FIXME 주석 — API 미제작 상태로 표시되어 있습니다.

TransportSegment에 FIXME 주석이 있습니다. 이 클래스의 API가 확정되면 업데이트가 필요합니다. 추적을 위해 이슈를 생성해 드릴까요?


97-109: formatDistance()의 부동소수점 비교가 잠재적으로 불안정합니다.

Line 101의 km % 1 == 0.0은 부동소수점 정밀도 문제가 발생할 수 있습니다. distanceInt이므로 정수 나눗셈으로 처리하면 더 안전합니다.

♻️ 제안
     fun formatDistance(): String {
         return when {
             distance >= 1000 -> {
-                val km = distance / 1000.0
-                if (km % 1 == 0.0) {
-                    "${km.toInt()}km"
+                if (distance % 1000 == 0) {
+                    "${distance / 1000}km"
                 } else {
+                    val km = distance / 1000.0
                     String.format(java.util.Locale.getDefault(), "%.1fkm", km)
                 }
             }
             else -> "${distance}m"
         }
     }
feature/travel/src/main/java/com/yapp/ndgl/feature/travel/followtravel/component/TransportSegment.kt (1)

25-47: Composable 함수명이 데이터 클래스명과 동일하여 혼동의 소지가 있습니다.

TransportSegment Composable 함수(Line 26)와 TransportSegment 데이터 클래스(import, Line 21)가 같은 이름을 사용합니다. 현재는 패키지로 구분되지만, import 충돌이나 가독성 저하가 우려됩니다. Composable 함수명을 TransportSegmentItem 등으로 변경하는 것을 고려해 주세요.

feature/travel/src/main/java/com/yapp/ndgl/feature/travel/followtravel/component/PlaceItem.kt (1)

89-150: PlaceCardpublic으로 노출되어 있습니다.

followtravel 패키지의 PlaceCardpublic 가시성인 반면, traveldetail 패키지의 동일 컴포넌트는 private입니다. 외부 모듈에서의 의도치 않은 사용을 방지하려면 internal로 제한하세요.

또한 이 컴포넌트는 traveldetail/component/PlaceCard와 거의 동일합니다. 앞서 FollowTravelContract.kt에서 언급한 대로 공통 모듈로 통합하면 중복을 해소할 수 있습니다.

♻️ 제안
 `@Composable`
-fun PlaceCard(
+internal fun PlaceCard(
     place: TravelPlace,
     interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
 ) {
core/ui/src/main/res/values/strings.xml (1)

3-5: 포맷 문자열에 위치 인자(positional arguments)를 사용해 주세요.

Line 3 %d/%s, Line 4 %s/%d 등이 위치 인자 없이 사용됩니다. 다국어 지원 시 번역가가 인자 순서를 변경해야 할 수 있으므로, Line 11의 %1$s/%2$s처럼 위치 인자를 일관되게 사용하는 것이 좋습니다.

♻️ 제안
-    <string name="budget_bar_format">%d일차 여행 예산: %s</string>
-    <string name="content_card_info_format">%s • %s • %d박%d일</string>
-    <string name="content_card_budget_format">1인 기준 여행 예산 : %s</string>
+    <string name="budget_bar_format">%1$d일차 여행 예산: %2$s</string>
+    <string name="content_card_info_format">%1$s • %2$s • %3$d박%4$d일</string>
+    <string name="content_card_budget_format">1인 기준 여행 예산 : %1$s</string>
feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/component/EditControlBar.kt (1)

33-56: 클릭 가능한 텍스트 요소에 접근성 및 터치 영역 개선을 고려해 주세요.

Text.clickable만 적용하면 기본 ripple가 텍스트 영역에만 나타나고, 터치 영역이 작아 사용성이 떨어질 수 있습니다. "전체 선택" 영역은 체크박스와 텍스트를 하나의 클릭 영역으로 묶고, "선택 삭제"에는 minimumInteractiveComponentSize를 적용하면 더 나은 UX를 제공할 수 있습니다.

♻️ 제안: 체크박스와 텍스트를 하나의 클릭 영역으로 통합
-        Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
-            NDGLCheckbox(
-                checked = isAllSelected,
-                onClick = onSelectAllClick,
-            )
-            Spacer(Modifier.width(8.dp))
-            Text(
-                text = stringResource(R.string.select_all),
-                color = NDGLTheme.colors.black700,
-                style = NDGLTheme.typography.bodyMdMedium,
-                modifier = Modifier.clickable {
-                    onSelectAllClick()
-                },
-            )
+        Row(
+            modifier = Modifier
+                .fillMaxWidth()
+                .clickable { onSelectAllClick() },
+            verticalAlignment = Alignment.CenterVertically,
+        ) {
+            NDGLCheckbox(
+                checked = isAllSelected,
+                onClick = onSelectAllClick,
+            )
+            Spacer(Modifier.width(8.dp))
+            Text(
+                text = stringResource(R.string.select_all),
+                color = NDGLTheme.colors.black700,
+                style = NDGLTheme.typography.bodyMdMedium,
+            )
feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/TravelDetailScreen.kt (2)

262-299: 모든 장소가 단일 LazyColumn item 내에서 렌더링됩니다.

currentPlaces.forEachIndexeditem(key = "places_...") 블록 안에 있어 모든 장소가 한 번에 compose됩니다. 장소 수가 많아지면 성능에 영향을 줄 수 있습니다. 각 장소를 개별 item으로 분리하면 LazyColumn의 lazy 렌더링 이점을 활용할 수 있습니다. (FollowTravelScreen에서는 items/item을 개별적으로 사용하고 있습니다.)


127-132: persistentListOf 생성 방식이 FollowTravelScreen과 다릅니다.

여기서는 .let { persistentListOf(*it.toTypedArray()) }를 사용하고, FollowTravelScreen에서는 .toPersistentList()를 사용하고 있습니다. 일관성을 위해 동일한 방식으로 통일하는 것을 권장합니다.

♻️ 제안
     val tabs = (1..state.contentInfo.days).map { day ->
         NDGLChipTabAttr.Tab(
             tag = "d$day",
             name = stringResource(R.string.day_format, day),
         )
-    }.let { persistentListOf(*it.toTypedArray()) }
+    }.toPersistentList()
feature/travel/src/main/java/com/yapp/ndgl/feature/travel/followtravel/FollowTravelScreen.kt (1)

46-61: 뒤로가기 버튼이 ViewModel Intent를 거치지 않고 직접 navigateBack을 호출합니다.

TravelDetailRoute에서는 clickBackviewModel.onIntent(TravelDetailIntent.ClickBack)을 거쳐 ViewModel에서 사이드이펙트로 네비게이션을 처리합니다. 반면 FollowTravelRoute에서는 clickBackButton = navigateBack으로 직접 연결되어 있습니다. 현재는 편집 모드가 없으므로 문제없지만, 향후 FollowTravel에서도 뒤로가기 전 확인이 필요해질 경우 ViewModel을 거치는 패턴이 더 확장성이 좋습니다. 또한 Line 53의 collectSideEffect TODO가 이 부분과 관련됩니다.

feature/travel/src/main/java/com/yapp/ndgl/feature/travel/followtravel/component/ContentCard.kt (2)

52-53: ContentCard의 가시성을 internal로 제한하는 것을 고려해 주세요.

같은 패키지의 다른 컴포넌트(TransportSegment, PlaceItem 등)는 internal로 선언되어 있습니다. ContentCard도 feature 모듈 외부에서 사용되지 않는다면 internal로 통일하는 것이 좋습니다.

#!/bin/bash
# ContentCard의 모듈 외부 사용 여부 확인
rg -n "ContentCard" --type=kt -g '!**/followtravel/**'

209-211: Preview 함수의 가시성을 private으로 변경해 주세요.

PreviewContent()public으로 선언되어 있어 모듈 외부에서 접근 가능합니다. 다른 파일의 Preview 함수들은 모두 private으로 선언되어 있습니다.

♻️ 수정 제안
 `@Preview`
 `@Composable`
-fun PreviewContent() {
+private fun ContentCardPreview() {
feature/travel/src/main/java/com/yapp/ndgl/feature/travel/followtravel/FollowTravelViewModel.kt (1)

10-14: 테스트 데이터 제거 TODO가 남아 있습니다.

TEST_PROFILE_IMAGE_URLTEST_THUMBNAIL_URL 상수와 loadTravelData()의 모든 mock 데이터가 릴리스 전에 제거되어야 합니다. 이 TODO를 추적하기 위해 이슈를 생성해 드릴까요?

feature/travel/src/main/java/com/yapp/ndgl/feature/travel/followtravel/component/TravelMap.kt (1)

58-61: places가 빈 리스트일 경우 places.first()에서 NoSuchElementException이 발생합니다.

현재 호출부(FollowTravelScreen)에서 places.isNotEmpty() 체크 후 호출하므로 당장 문제는 없지만, 컴포넌트 자체적으로 방어 코드를 추가하면 향후 다른 곳에서 재사용 시 안전합니다.

🛡️ 제안: early return 추가
 `@Composable`
 internal fun TravelMap(
     places: List<TravelPlace>,
     onScrollEnabledChange: (Boolean) -> Unit,
 ) {
+    if (places.isEmpty()) return
+
     val routePoints = remember(places) {
feature/travel/src/main/java/com/yapp/ndgl/feature/travel/travel/TravelScreen.kt (2)

41-46: innerPadding에 기본값 추가를 권장합니다.

다른 파라미터(state, clickTravel, clickTravelDetail)는 모두 기본값이 있지만 innerPadding만 기본값이 없어 불일치합니다. PaddingValues()를 기본값으로 지정하면 Preview 호출 시에도 더 간결해지고, 파라미터 간 일관성이 유지됩니다.

♻️ 제안된 변경
 `@Composable`
 private fun TravelScreen(
     state: TravelState = TravelState(),
     clickTravel: (Int) -> Unit = {},
     clickTravelDetail: (Int) -> Unit = {},
-    innerPadding: PaddingValues,
+    innerPadding: PaddingValues = PaddingValues(),
 ) {

59-74: 하드코딩된 travelId(123, 456)는 임시 값으로 보입니다.

PR 설명에서 테스트용 버튼으로 언급되어 있어 현재 단계에서는 문제없지만, 실제 데이터 연동 시 제거/교체가 필요합니다. TODO 주석을 남겨두면 추후 정리 시 식별이 쉬워집니다.

Comment thread app/build.gradle.kts
Comment on lines +12 to +18
defaultConfig {
val localProperties = Properties().apply {
load(rootProject.file("local.properties").bufferedReader())
}

manifestPlaceholders["MAPS_API_KEY"] = localProperties.getProperty("MAPS_API_KEY", "")
}
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

local.properties 파일 부재 시 빌드 실패 가능성

rootProject.file("local.properties").bufferedReader()는 파일이 없으면 FileNotFoundException을 발생시킵니다. CI 환경이나 fresh clone에서 빌드가 실패할 수 있습니다.

🛡️ 방어 코드 제안
     defaultConfig {
-        val localProperties = Properties().apply {
-            load(rootProject.file("local.properties").bufferedReader())
-        }
+        val localPropertiesFile = rootProject.file("local.properties")
+        val localProperties = Properties().apply {
+            if (localPropertiesFile.exists()) {
+                load(localPropertiesFile.bufferedReader())
+            }
+        }

         manifestPlaceholders["MAPS_API_KEY"] = localProperties.getProperty("MAPS_API_KEY", "")
     }
📝 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
defaultConfig {
val localProperties = Properties().apply {
load(rootProject.file("local.properties").bufferedReader())
}
manifestPlaceholders["MAPS_API_KEY"] = localProperties.getProperty("MAPS_API_KEY", "")
}
defaultConfig {
val localPropertiesFile = rootProject.file("local.properties")
val localProperties = Properties().apply {
if (localPropertiesFile.exists()) {
load(localPropertiesFile.bufferedReader())
}
}
manifestPlaceholders["MAPS_API_KEY"] = localProperties.getProperty("MAPS_API_KEY", "")
}
🤖 Prompt for AI Agents
In `@app/build.gradle.kts` around lines 12 - 18, The current defaultConfig block
reads local.properties with
rootProject.file("local.properties").bufferedReader(), which throws if the file
is missing; update the logic around localProperties creation in defaultConfig so
it first checks for the file's existence (or uses safe file access/try-catch)
and only loads when present, otherwise fall back to an empty Properties
instance; then set manifestPlaceholders["MAPS_API_KEY"] using
localProperties.getProperty("MAPS_API_KEY", "") so builds in CI or fresh clones
won’t fail when local.properties is absent.

Comment on lines +1 to +5
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
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

파일명과 실제 벡터 크기가 불일치합니다.

파일명은 ic_20_check(20dp 암시)이지만, 실제 android:width/android:height는 24dp로 설정되어 있습니다. 다른 아이콘들(ic_24_*)과의 네이밍 컨벤션 일관성을 위해 파일명을 ic_24_check로 변경하거나, 실제로 20dp 크기를 의도했다면 width/height를 20dp로 수정해 주세요.

🤖 Prompt for AI Agents
In `@core/ui/src/main/res/drawable/ic_20_check.xml` around lines 1 - 5, The
drawable filename ic_20_check does not match the vector size
(android:width/android:height set to 24dp); either rename the asset to
ic_24_check to match the 24dp dimensions or change the vector attributes
android:width and android:height to 20dp to match the filename; update any
references to the drawable if you rename the file so references to ic_20_check
are switched to ic_24_check, or if you change dimensions ensure the file name
remains ic_20_check and the viewport/paths still render correctly at 20dp.



<!-- Travel Detail -->
<string name="timeline_auto_setting">타임라인 자동생성</string>
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

중복 문자열 리소스: timeline_auto_settingtimeline_auto_setting_title

Line 33의 timeline_auto_setting과 Line 54의 timeline_auto_setting_title이 동일한 값 "타임라인 자동생성"을 가지고 있습니다. 의도적으로 분리한 것인지 확인해 주세요. 동일 용도라면 하나로 통합하는 것이 좋습니다.

Also applies to: 54-54

🤖 Prompt for AI Agents
In `@core/ui/src/main/res/values/strings.xml` at line 33, The two string resources
timeline_auto_setting and timeline_auto_setting_title both contain the identical
value "타임라인 자동생성"; confirm whether they were intentionally distinct and if not,
consolidate them by keeping one resource (pick a canonical name, e.g.,
timeline_auto_setting_title or timeline_auto_setting), update all usages to
reference that single resource, and remove the duplicate entry to avoid
redundant resources and maintain a single source of truth.

Comment on lines +80 to +88
AsyncImage(
model = contentInfo.videoInfo.profileImage,
contentDescription = "Profile Image",
modifier = Modifier
.size(48.dp)
.clip(CircleShape)
.background(Color.LightGray),
contentScale = ContentScale.Crop,
)
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

contentDescription에 하드코딩된 영문 문자열 대신 stringResource를 사용해 주세요.

"Profile Image", "Expand", "Video Thumbnail" 등이 하드코딩되어 있습니다. 접근성과 다국어 지원을 위해 stringResource를 사용하거나, 장식용 이미지라면 null로 설정하는 것이 좋습니다.

Also applies to: 127-133, 144-153

🤖 Prompt for AI Agents
In
`@feature/travel/src/main/java/com/yapp/ndgl/feature/travel/followtravel/component/ContentCard.kt`
around lines 80 - 88, The contentDescription values in ContentCard's AsyncImage
usages (e.g., the AsyncImage showing profileImage and other AsyncImage calls
around lines with "Expand" and "Video Thumbnail") are hardcoded English strings;
replace them with stringResource(...) references for localization or set
contentDescription = null for purely decorative images. Locate the AsyncImage
invocations in the ContentCard composable and update each contentDescription to
use the appropriate string resource key (add entries to strings.xml) or null
when decorative, ensuring accessibility and i18n support.

Comment on lines +19 to +70
data class ContentInfo(
val travelId: String = "",
val country: String = "",
val city: String = "",
val budgetPerPerson: Budget = Budget(0),
val nights: Int = 0,
val days: Int = 0,
val videoInfo: VideoInfo = VideoInfo(),
)

data class VideoInfo(
val title: String = "",
val name: String = "",
val profileImage: String = "",
val thumbnail: String = "",
val link: String = "",
val summary: String = "",
)

data class Budget(
val amount: Int,
) {
fun formatString(): String {
return when {
amount < 10000 -> "만원"
amount % 10000 == 0 -> "${amount / 10000}만원"
amount % 1000 == 0 -> "${amount / 10000}만 ${(amount % 10000) / 1000}천원"
else -> "${amount}원"
}
}
}

data class Itinerary(
val budget: Budget = Budget(0),
val places: List<TravelPlace> = emptyList(),
val transportSegments: List<TransportSegment> = emptyList(),
)

data class TravelPlace(
val id: Int,
val day: Int,
val sequence: Int,
val estimatedDuration: Duration,
val googlePlaceId: String,
val thumbnail: String,
val latitude: Double,
val longitude: Double,
val name: String,
val regularOpeningHours: String,
val googleMapsUri: String,
val placeType: PlaceType,
)
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 | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Verify duplication of data models between followtravel and traveldetail packages

echo "=== Budget class definitions ==="
rg -n "^data class Budget" --type kotlin

echo ""
echo "=== ContentInfo class definitions ==="
rg -n "^data class ContentInfo" --type kotlin

echo ""
echo "=== VideoInfo class definitions ==="
rg -n "^data class VideoInfo" --type kotlin

echo ""
echo "=== TravelPlace class definitions ==="
rg -n "^data class TravelPlace" --type kotlin

echo ""
echo "=== PlaceType enum definitions ==="
rg -n "^enum class PlaceType" --type kotlin

echo ""
echo "=== TransportSegment definitions ==="
rg -n "^data class TransportSegment" --type kotlin

echo ""
echo "=== TransportType definitions ==="
rg -n "^enum class TransportType" --type kotlin

echo ""
echo "=== Checking imports in ContentCard.kt ==="
fd "ContentCard.kt" --type f -x cat -n {}

echo ""
echo "=== Checking imports in PlaceItem.kt ==="
fd "PlaceItem.kt" --type f -x cat -n {}

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

Length of output: 44768


🏁 Script executed:

#!/bin/bash
# Check for differences between duplicated class definitions

echo "=== Comparing Budget classes ==="
diff <(sed -n '38,46p' feature/travel/src/main/java/com/yapp/ndgl/feature/travel/followtravel/FollowTravelContract.kt) \
     <(sed -n '49,57p' feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/TravelDetailContract.kt) || echo "Differences found or files not identical"

echo ""
echo "=== Comparing PlaceType enums ==="
diff <(sed -n '72,90p' feature/travel/src/main/java/com/yapp/ndgl/feature/travel/followtravel/FollowTravelContract.kt) \
     <(sed -n '83,101p' feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/TravelDetailContract.kt) || echo "Differences found or files not identical"

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

Length of output: 421


🏁 Script executed:

#!/bin/bash
# Read and compare the actual class definitions

echo "=== FollowTravelContract.kt Budget ==="
sed -n '38,46p' feature/travel/src/main/java/com/yapp/ndgl/feature/travel/followtravel/FollowTravelContract.kt

echo ""
echo "=== TravelDetailContract.kt Budget ==="
sed -n '49,57p' feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/TravelDetailContract.kt

echo ""
echo "=== FollowTravelContract.kt PlaceType ==="
sed -n '72,90p' feature/travel/src/main/java/com/yapp/ndgl/feature/travel/followtravel/FollowTravelContract.kt

echo ""
echo "=== TravelDetailContract.kt PlaceType ==="
sed -n '83,101p' feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/TravelDetailContract.kt

echo ""
echo "=== Checking for getColor() function in followtravel ==="
rg -n "fun PlaceType.getColor" feature/travel/src/main/java/com/yapp/ndgl/feature/travel/followtravel/ -A 5

echo ""
echo "=== Checking for getColor() function in traveldetail ==="
rg -n "fun PlaceType.getColor" feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/ -A 5

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

Length of output: 4425


followtraveltraveldetail 패키지 간 데이터 모델 중복 정의 제거 필요

ContentInfo, VideoInfo, Budget, TravelPlace, PlaceType, TransportSegment, TransportType 등 핵심 데이터 모델이 두 패키지에 각각 정의되어 있습니다. 특히 PlaceType.getColor() 확장 함수도 동일한 로직으로 중복 구현되어 있습니다.

동일한 모델이 분산되어 있으면 향후 비즈니스 로직 변경 시 두 곳을 모두 수정해야 하는 유지보수 비용이 발생합니다. 공통 데이터 모델을 feature/travel 루트의 공유 패키지(예: common, model, contract)로 추출하여 단일 소스로 관리하기를 권장합니다.

🤖 Prompt for AI Agents
In
`@feature/travel/src/main/java/com/yapp/ndgl/feature/travel/followtravel/FollowTravelContract.kt`
around lines 19 - 70, Several core data models (ContentInfo, VideoInfo, Budget,
TravelPlace, PlaceType, TransportSegment, TransportType) and the
PlaceType.getColor() extension are duplicated between followtravel and
traveldetail; extract these into a single shared package under feature/travel
(e.g., common or model/contract) and replace the duplicated definitions by
importing the shared types. Specifically, create shared definitions for
ContentInfo, VideoInfo, Budget (with formatString), Itinerary, TravelPlace,
TransportSegment, TransportType and the PlaceType enum plus its getColor()
extension, then update all usages and imports in followtravel and traveldetail
to reference the new shared package and remove the old duplicated files. Ensure
public API and data shapes remain unchanged so callers (e.g., functions
referencing Budget.formatString or TravelPlace properties) compile without
further changes.

import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
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 | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check for hiltViewModel imports in Kotlin files
rg -n "import.*hiltViewModel" --type=kt

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

Length of output: 105


🏁 Script executed:

#!/bin/bash
# Check Hilt dependency version in gradle files
find . -name "*.toml" -o -name "build.gradle*" | head -20 | xargs grep -l -i "hilt" 2>/dev/null

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

Length of output: 125


🏁 Script executed:

#!/bin/bash
# Check the actual Hilt version configuration
rg -A 2 -B 2 "hilt" gradle/libs.versions.toml 2>/dev/null || rg -A 2 -B 2 "hilt" build.gradle.kts 2>/dev/null || rg -A 2 -B 2 "hilt" build.gradle 2>/dev/null

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

Length of output: 874


🏁 Script executed:

#!/bin/bash
# Search for hiltViewModel imports without file type restriction
rg -n "import.*hiltViewModel"

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

Length of output: 1119


🏁 Script executed:

#!/bin/bash
# Check the specific file mentioned in the review
cat -n "feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/TravelDetailScreen.kt" | head -50

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

Length of output: 2832


hiltViewModel import 경로를 수정해 주세요.

이 파일을 포함하여 프로젝트 전체에서 deprecated된 import 경로 androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel을 사용 중입니다. 프로젝트가 보유한 Hilt 2.57.2 및 hilt-navigation-compose 1.3.0 버전에서는 이 import 경로가 더 이상 제공되지 않으므로, 반드시 androidx.hilt.navigation.compose.hiltViewModel로 변경해야 합니다.

영향을 받는 파일들:

  • feature/home/src/main/java/com/yapp/ndgl/feature/home/HomeScreen.kt
  • feature/travel/src/main/java/com/yapp/ndgl/feature/travel/travel/TravelScreen.kt
  • feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/TravelDetailScreen.kt
  • feature/travel/src/main/java/com/yapp/ndgl/feature/travel/followtravel/FollowTravelScreen.kt
  • feature/travel/src/main/java/com/yapp/ndgl/feature/travel/navigation/TravelEntry.kt
  • feature/travel-helper/src/main/java/com/yapp/ndgl/feature/travelhelper/TravelHelperScreen.kt
  • feature/auth/src/main/java/com/yapp/ndgl/feature/auth/AuthScreen.kt
🤖 Prompt for AI Agents
In
`@feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/TravelDetailScreen.kt`
at line 40, The import for hiltViewModel is using the deprecated path
androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel; update the import to
androidx.hilt.navigation.compose.hiltViewModel in this file
(TravelDetailScreen.kt) and the other affected screens (HomeScreen.kt,
TravelScreen.kt, FollowTravelScreen.kt, TravelEntry.kt, TravelHelperScreen.kt,
AuthScreen.kt) so usages of hiltViewModel (e.g., calls like val vm:
TravelDetailViewModel = hiltViewModel()) resolve to the supported package from
hilt-navigation-compose.

Comment on lines +371 to +381
if (state.showCancelEditModal) {
NDGLModal(
onDismissRequest = dismissCancelEditModal,
title = stringResource(R.string.cancel_edit_dialog_title),
body = stringResource(R.string.cancel_edit_dialog_message),
negativeButtonText = stringResource(R.string.cancel_edit_dialog_cancel),
onNegativeButtonClick = confirmCancelEditMode,
positiveButtonText = stringResource(R.string.cancel_edit_dialog_confirm),
onPositiveButtonClick = dismissCancelEditModal,
)
}
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 | 🔴 Critical

편집 취소 모달의 버튼 액션이 뒤바뀌어 있습니다.

negativeButtonText(취소 버튼)에 confirmCancelEditMode가, positiveButtonText(확인 버튼)에 dismissCancelEditModal이 연결되어 있습니다. "편집을 취소하시겠습니까?" 다이얼로그에서 "확인"을 누르면 편집 모드에 머무르고, "취소"를 누르면 편집이 취소되는 역전된 동작이 발생합니다.

🐛 수정 제안
         if (state.showCancelEditModal) {
             NDGLModal(
                 onDismissRequest = dismissCancelEditModal,
                 title = stringResource(R.string.cancel_edit_dialog_title),
                 body = stringResource(R.string.cancel_edit_dialog_message),
                 negativeButtonText = stringResource(R.string.cancel_edit_dialog_cancel),
-                onNegativeButtonClick = confirmCancelEditMode,
+                onNegativeButtonClick = dismissCancelEditModal,
                 positiveButtonText = stringResource(R.string.cancel_edit_dialog_confirm),
-                onPositiveButtonClick = dismissCancelEditModal,
+                onPositiveButtonClick = confirmCancelEditMode,
             )
         }
📝 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
if (state.showCancelEditModal) {
NDGLModal(
onDismissRequest = dismissCancelEditModal,
title = stringResource(R.string.cancel_edit_dialog_title),
body = stringResource(R.string.cancel_edit_dialog_message),
negativeButtonText = stringResource(R.string.cancel_edit_dialog_cancel),
onNegativeButtonClick = confirmCancelEditMode,
positiveButtonText = stringResource(R.string.cancel_edit_dialog_confirm),
onPositiveButtonClick = dismissCancelEditModal,
)
}
if (state.showCancelEditModal) {
NDGLModal(
onDismissRequest = dismissCancelEditModal,
title = stringResource(R.string.cancel_edit_dialog_title),
body = stringResource(R.string.cancel_edit_dialog_message),
negativeButtonText = stringResource(R.string.cancel_edit_dialog_cancel),
onNegativeButtonClick = dismissCancelEditModal,
positiveButtonText = stringResource(R.string.cancel_edit_dialog_confirm),
onPositiveButtonClick = confirmCancelEditMode,
)
}
🤖 Prompt for AI Agents
In
`@feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/TravelDetailScreen.kt`
around lines 371 - 381, The NDGLModal in TravelDetailScreen is wired with
reversed actions: swap the callbacks so that positiveButtonText's handler calls
confirmCancelEditMode and negativeButtonText's handler calls
dismissCancelEditModal; locate the NDGLModal invocation guarded by
state.showCancelEditModal and update the onNegativeButtonClick and
onPositiveButtonClick assignments accordingly (keep titles/texts unchanged).

Comment on lines +182 to +184
private fun selectDay(day: Int) {
reduce { copy(selectedDay = day) }
}
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 | 🟠 Major

버그: 편집 모드에서 날짜 전환 시 selectedPlaceIds가 초기화되지 않음

selectDayisEditMode 상태에서 selectedPlaceIds를 초기화하지 않아, 다른 날짜의 장소 ID가 남아있게 됩니다. 이로 인해 다음 함수들이 오동작합니다:

  1. checkSelectAll (Line 222): selectedPlaceIds.size에 다른 날짜의 ID가 포함되어 전체 선택/해제 토글 로직이 깨집니다.
  2. confirmDeleteSelectedPlaces (Line 245-247): 모든 itinerary에서 selectedPlaceIds 기반으로 삭제하므로, 이전 날짜에서 선택된 장소까지 삭제될 수 있습니다.
🐛 수정 제안: 날짜 전환 시 선택 초기화
 private fun selectDay(day: Int) {
-    reduce { copy(selectedDay = day) }
+    reduce {
+        copy(
+            selectedDay = day,
+            selectedPlaceIds = emptySet(),
+        )
+    }
 }
📝 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
private fun selectDay(day: Int) {
reduce { copy(selectedDay = day) }
}
private fun selectDay(day: Int) {
reduce {
copy(
selectedDay = day,
selectedPlaceIds = emptySet(),
)
}
}
🤖 Prompt for AI Agents
In
`@feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/TravelDetailViewModel.kt`
around lines 182 - 184, selectDay currently only updates selectedDay, so when
switching days in edit mode previously selectedPlaceIds from another day persist
and break checkSelectAll and confirmDeleteSelectedPlaces; modify selectDay to,
when isEditMode is true, also reset selectedPlaceIds (e.g., set to emptySet())
inside the same reduce call (update selectedDay and selectedPlaceIds together)
so switching days clears prior selections and prevents cross-day
deletion/selection bugs; reference selectDay, isEditMode, selectedPlaceIds,
reduce, copy, checkSelectAll, and confirmDeleteSelectedPlaces.

Comment on lines +215 to +229
private fun checkSelectAll() {
reduce {
val currentItineraries = if (isEditMode) tempItineraries else itineraries
val currentDayPlaceIds = currentItineraries.getOrNull(selectedDay - 1)
?.places?.map { it.id }?.toSet() ?: emptySet()

copy(
selectedPlaceIds = if (selectedPlaceIds.size == currentDayPlaceIds.size) {
emptySet()
} else {
currentDayPlaceIds
},
)
}
}
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 | 🟠 Major

checkSelectAll 로직이 ID 집합이 아닌 크기만 비교

현재 selectedPlaceIds.size == currentDayPlaceIds.size로 비교하고 있어, 다른 날짜의 ID가 selectedPlaceIds에 남아 있으면 실제로 현재 날짜의 전체가 선택되지 않았는데도 전체 선택으로 판정될 수 있습니다. selectDay에서 선택을 초기화하더라도, 집합 동등성(containsAll)으로 비교하는 것이 더 안전합니다.

🛡️ 수정 제안
     copy(
-        selectedPlaceIds = if (selectedPlaceIds.size == currentDayPlaceIds.size) {
+        selectedPlaceIds = if (selectedPlaceIds.containsAll(currentDayPlaceIds) && currentDayPlaceIds.isNotEmpty()) {
             emptySet()
         } else {
             currentDayPlaceIds
         },
     )
🤖 Prompt for AI Agents
In
`@feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/TravelDetailViewModel.kt`
around lines 215 - 229, The checkSelectAll function compares only sizes which
misdetects full-selection when selectedPlaceIds contains IDs from other days;
update checkSelectAll (in TravelDetailViewModel) to compare sets instead of
sizes by checking selectedPlaceIds.containsAll(currentDayPlaceIds) (or
selectedPlaceIds == currentDayPlaceIds) so the selection is true only when all
currentDayPlaceIds are included; keep current logic for choosing
currentItineraries (tempItineraries vs itineraries) and selectedDay as-is.

Comment thread gradle/libs.versions.toml
Comment on lines +45 to +51
# Google Maps
mapsCompose = "6.4.1"
playServicesMaps = "19.1.0"

# Coil
coil = "2.7.0"

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 | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

Google Maps Compose latest version 2026

💡 Result:

As of February 14, 2026, the latest Google Maps Compose (artifact com.google.maps.android:maps-compose) release is 8.1.0 (released 2026-02-06). [1][2]

Gradle (Kotlin DSL):

dependencies {
  implementation("com.google.maps.android:maps-compose:8.1.0")
}

Sources: GitHub releases show v8.1.0 as “Latest”. [1] Maven Central listings also show 8.1.0 as the newest published version (Feb 2026). [2]


🌐 Web query:

Play Services Maps latest version 2026

💡 Result:

Latest Google Play services Maps artifact (com.google.android.gms:play-services-maps) version in 2026 is 20.0.0, released January 14, 2026. [1][2]

Gradle:

implementation "com.google.android.gms:play-services-maps:20.0.0"

🌐 Web query:

Coil image loading library latest version 2026

💡 Result:

As of February 14, 2026, the latest released version of Coil (v3) is 3.3.0 (released July 22, 2025). [1] [2] [3]

Gradle examples (common artifacts):

implementation("io.coil-kt.coil3:coil-compose:3.3.0")
implementation("io.coil-kt.coil3:coil-network-okhttp:3.3.0")

[2] [3]

Note: Coil v3 uses the io.coil-kt.coil3 group ID (the old io.coil-kt:coil coordinates are relocated). [3]

Sources: [1] [2] [3]


의존성 버전을 최신 안정 버전으로 업데이트하세요.

현재 지정된 버전들이 상당히 오래되었습니다:

  • mapsCompose: 6.4.1 → 최신 8.1.0 (2026년 2월 6일 출시)
  • playServicesMaps: 19.1.0 → 최신 20.0.0 (2026년 1월 14일 출시)
  • coil: 2.7.0 → 최신 3.3.0 (2025년 7월 22일 출시)

Google Maps Compose는 특히 2개 주요 버전이 뒤떨어져 있습니다. Coil 3.x로 업그레이드하는 경우 그룹 ID를 io.coil-kt.coil3로 변경해야 합니다.

🤖 Prompt for AI Agents
In `@gradle/libs.versions.toml` around lines 45 - 51, Update the dependency
versions in libs.versions.toml: set mapsCompose = "8.1.0" and playServicesMaps =
"20.0.0", and upgrade coil to "3.3.0"; additionally, when moving Coil to 3.x
update its Gradle coordinates to the new group ID (io.coil-kt.coil3) wherever
the Coil dependency is declared (search for the symbol coil and replace the old
group/artifact with the new group for Coil 3.x).

@mj010504 mj010504 force-pushed the design/NDGL-53 branch 2 times, most recently from ae97e34 to 822f9b3 Compare February 14, 2026 02:33
@mj010504 mj010504 merged commit 78cb8ef into develop Feb 14, 2026
1 check passed
@mj010504 mj010504 deleted the design/NDGL-53 branch February 14, 2026 02:38
@coderabbitai coderabbitai bot mentioned this pull request Feb 15, 2026
3 tasks
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