Skip to content

[NDGL-86] 내 여행 화면 장소 재배치, 교통수단 변경 추가#22

Merged
mj010504 merged 5 commits intodevelopfrom
feature/NDGL-86
Feb 16, 2026
Merged

[NDGL-86] 내 여행 화면 장소 재배치, 교통수단 변경 추가#22
mj010504 merged 5 commits intodevelopfrom
feature/NDGL-86

Conversation

@mj010504
Copy link
Copy Markdown
Contributor

@mj010504 mj010504 commented Feb 16, 2026

개요

  • 내 여행 화면 장소 재배치, 교통수단 변경 추가

디자인

내 여행 편집

영상

변경사항

  • 내 여행 화면 장소 재배치 로직 및 UI/UX
  • 내 여행 화면 교통수단 변경 로직 및 UI/UX

추후 작업

  • 두 장소간의 교통수단 구하는 API 연동

테스트 체크 리스트

  • 장소 재배치 로직 동작
  • 장소 재배치 시 지도에 UI 반영
  • 교통수단 변경 시 장소마다 시간 반영

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • 여행 일정의 장소 드래그 앤 드롭 재정렬 기능 추가
    • 이동수단 선택 바텀시트 추가 (도보, 자동차, 버스, 기차)
    • 이동수단별 소요 시간 및 거리 표시
  • 개선 사항

    • 바텀시트 헤더 디자인 개선 (제목 및 닫기 버튼 추가)
    • 장소 아이템 배경색 투명화
    • 지도 마커 성능 최적화

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 16, 2026

개요

여행 상세 화면의 장소 재정렬 기능과 교통 수단 선택 기능을 추가합니다. 새로운 드래그 앤 드롭 유틸리티, 교통 수단 바텀시트 컴포넌트, 데이터 모델 재구성을 통해 장소별 교통 세그먼트 관리를 개선합니다.

변경사항

Cohort / File(s) Summary
바텀시트 및 UI 컴포넌트
core/ui/src/main/java/com/yapp/ndgl/core/ui/designsystem/NDGLBottomSheet.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/PlaceBottomSheet.kt, feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/component/TimelineContent.kt
NDGLBottomSheet에 SheetState 파라미터와 선택적 title 파라미터 추가; 새로운 TransportBottomSheet 컴포넌트로 교통 수단 선택 UI 제공; TimelineContent에서 onDismissRequest 파라미터 제거
재정렬 가능한 리스트 유틸리티
core/ui/src/main/java/com/yapp/ndgl/core/ui/util/ReorderableList.kt
드래그 앤 드롭 재정렬을 지원하는 새로운 ReorderableState, Modifier.reorderable, rememberReorderableState 추가; 리스트 요소 인덱스 변경 및 자동 스크롤 기능 포함
여행 상세 데이터 모델
feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/TravelDetailContract.kt
Itinerary에서 budget 및 transportSegments 제거; TravelPlace에 transportToNext 속성 추가; TravelDetailState에 showTransportBottomSheet 플래그 추가; TransportType에 CAR, BUS, TRAIN 추가; ReorderPlaces에 dayIndex 파라미터 추가
여행 상세 화면 UI
feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/TravelDetailScreen.kt, feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/component/PlaceItem.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/TravelMap.kt
TravelDetailScreen에 드래그 재정렬 및 교통 수단 변경 콜백 통합; PlaceItem의 배경색을 투명으로 변경; TransportSegment의 clickable을 noRippleClickable로 변경; TravelMap의 마커에 key 래핑 추가
여행 상세 비즈니스 로직
feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/TravelDetailViewModel.kt
장소별 transportToNext 모델 지원; 새로운 clickTransportSegment, dismissTransportBottomSheet, confirmChangeTransportSegment 인텐트 처리; 재정렬 후 교통 세그먼트 재계산
여행 보조 화면
feature/travel-helper/src/main/java/com/yapp/ndgl/feature/travelhelper/TravelHelperScreen.kt
TravelHelperRoute 및 TravelHelperScreen에서 innerPadding 파라미터 제거
문자열 리소스 및 유틸리티
core/ui/src/main/res/values/strings.xml, core/util/src/main/java/com/yapp/ndgl/core/util/IntUtil.kt
transport_bottom_sheet_title, transport_bottom_sheet_button 문자열 리소스 추가; Int.formatDistance() 확장 함수로 거리 포맷팅 기능 추가

코드 검토 난이도

🎯 4 (복잡함) | ⏱️ ~45분

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.18% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목은 변경의 핵심 내용인 '내 여행 화면에서 장소 재배치 및 교통수단 변경 기능 추가'를 명확하게 요약하고 있습니다.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into develop

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

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

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 marked this pull request as ready for review February 16, 2026 07:39
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: 3

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/traveldetail/TravelDetailViewModel.kt (2)

462-485: ⚠️ Potential issue | 🔴 Critical

confirmDuration에서 endTime 계산에 이전 duration 값이 사용됩니다.

itinerary.totalDurationitinerary.places(업데이트 전 원본)를 기반으로 계산되는 computed property입니다. durationUpdatedPlaces에 반영된 새로운 duration이 아닌, 변경 전 값으로 endTime이 설정됩니다.

🐛 수정 제안: 업데이트된 itinerary로부터 totalDuration 계산
                 itinerary.copy(
                     places = calculatePlaceStartTimes(durationUpdatedPlaces, itinerary.startTime ?: DEFAULT_START_TIME.hours),
-                    endTime = (itinerary.startTime ?: DEFAULT_START_TIME.hours) + itinerary.totalDuration,
                 )
+                val baseStartTime = itinerary.startTime ?: DEFAULT_START_TIME.hours
+                val updatedItinerary = itinerary.copy(
+                    places = calculatePlaceStartTimes(durationUpdatedPlaces, baseStartTime),
+                )
+                updatedItinerary.copy(endTime = baseStartTime + updatedItinerary.totalDuration)

311-318: ⚠️ Potential issue | 🟡 Minor

longClickPlaceItem에서 tempItineraries를 동기화하지 않습니다.

clickEditTravel(라인 222-229)에서는 tempItineraries = itineraries로 최신 상태를 동기화하지만, longClickPlaceItem에서는 동기화하지 않습니다. 교통수단 변경이나 시간 수정 후 롱프레스로 편집 모드에 진입하면 이전 데이터가 표시될 수 있습니다.

🐛 수정 제안: clickEditTravel과 동일하게 tempItineraries 동기화
     private fun longClickPlaceItem() {
         reduce {
             copy(
                 isEditMode = true,
                 selectedPlaceIds = emptySet(),
+                tempItineraries = itineraries,
             )
         }
     }
🤖 Fix all issues with AI agents
In `@core/ui/src/main/java/com/yapp/ndgl/core/ui/util/ReorderableList.kt`:
- Around line 173-179: onDragEnd() currently just nulls autoScrollJob but
doesn't cancel the running coroutine; modify onDragEnd() to call
autoScrollJob?.cancel() (and optionally autoScrollJob?.cancelAndJoin() if you
need to wait for termination) before setting autoScrollJob = null so any running
auto-scroll coroutine is stopped; update the method that references
autoScrollJob (the onDragEnd() function in ReorderableList.kt) to perform this
cancellation.

In
`@feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/component/TransportBottomSheet.kt`:
- Around line 88-96: The Row's clickable modifier is placed after padding so the
touch target is reduced; in TransportBottomSheet (the Row that sets
selectedTransport) move the .noRippleClickable modifier to be before .padding
(i.e., apply it earlier in the modifier chain, ahead of
.padding/.height/.background) so the entire Row area becomes touchable and still
updates selectedTransport when tapped.

In
`@feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/TravelDetailViewModel.kt`:
- Around line 393-416: confirmChangeTransportSegment updates itineraries but
never closes the transport BottomSheet or clears the selectedPlace; update the
state returned by the reduce call so that after computing updatedItineraries you
return copy(itineraries = updatedItineraries, showTransportBottomSheet = false,
selectedPlace = null) (i.e., modify the reduce result inside
confirmChangeTransportSegment to explicitly set showTransportBottomSheet to
false and selectedPlace to null).
🧹 Nitpick comments (7)
feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/component/PlaceItem.kt (1)

93-96: Color.Transparent 배경은 불필요한 코드입니다.

투명 색상으로 배경을 설정하는 것은 시각적으로 아무 효과가 없으므로, .background(...) 호출 자체를 제거하는 것이 더 깔끔합니다.

♻️ 제안
         Row(
             modifier = Modifier
                 .fillMaxWidth()
-                .background(
-                    color = Color.Transparent,
-                    shape = RoundedCornerShape(16.dp),
-                )
                 .padding(horizontal = 0.dp),
core/ui/src/main/java/com/yapp/ndgl/core/ui/util/ReorderableList.kt (1)

65-72: isReorderable 람다가 remember 블록에 캡처되어 업데이트되지 않습니다.

rememberReorderableState에 전달된 isReorderable 람다가 변경되어도 remember 블록 내부의 ReorderableState는 초기 값을 계속 사용합니다. onReorder처럼 rememberUpdatedState로 감싸거나, remember의 키로 추가하는 것을 고려해 주세요.

♻️ 수정 제안
     val scope = rememberCoroutineScope()
     val currentList by rememberUpdatedState(list)
+    val currentIsReorderable by rememberUpdatedState(isReorderable)
     val onReorder: (Int, Int) -> Boolean = { from, to ->
         currentList.reorder(from - offset, to - offset)
     }
 
     return remember {
         ReorderableState(
             scope = scope,
             lazyListState = lazyListState,
             onReorder = onReorder,
-            isReorderable = isReorderable,
+            isReorderable = { currentIsReorderable(it) },
         )
     }
core/ui/src/main/java/com/yapp/ndgl/core/ui/designsystem/NDGLBottomSheet.kt (1)

53-71: 헤더 Row에 verticalAlignment이 지정되지 않았습니다.

Row의 기본 verticalAlignmentAlignment.Top입니다. 제목 텍스트와 닫기 아이콘의 높이가 다를 수 있으므로, Alignment.CenterVertically를 추가하면 더 나은 정렬을 보장할 수 있습니다.

♻️ 수정 제안
             Row(
                 modifier = Modifier
                     .fillMaxWidth()
                     .padding(vertical = 18.dp, horizontal = 24.dp),
+                verticalAlignment = Alignment.CenterVertically,
             ) {
feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/component/TransportBottomSheet.kt (1)

86-86: index 파라미터가 사용되지 않습니다.

itemsIndexed 대신 items를 사용하면 불필요한 파라미터를 제거할 수 있습니다. 또는 index_로 변경하세요.

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

559-571: createTransportSegment가 실제 from/to 파라미터를 사용하지 않고 하드코딩된 값을 반환합니다.

TODO 주석으로 명시되어 있어 인지하고 있는 부분이지만, 현재 모든 재배치에서 동일한 교통 정보(CAR, 15분, 1000m)가 할당됩니다. API 연동 전까지 임시적으로 기존 transportToNext 값을 유지하는 방식도 고려해 볼 수 있습니다.

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

209-219: placesOffset = 3은 LazyColumn 구조에 강하게 결합된 매직 넘버입니다.

LazyColumn 내 장소 아이템 앞에 위치한 아이템 수(헤더, stickyHeader, 맵)에 의존합니다. 향후 아이템 추가/제거 시 동기화가 깨질 수 있으므로, 주석 외에 상수로 추출하거나 동적으로 계산하는 방안을 고려하세요.


497-503: 기본 시작 시간 8.hours가 ViewModel의 DEFAULT_START_TIME과 중복됩니다.

ViewModel에서 DEFAULT_START_TIME = 8companion object로 관리하고 있으나, 여기서는 8.hours를 직접 하드코딩하고 있습니다. 공통 상수로 통일하면 향후 변경 시 누락을 방지할 수 있습니다.

Comment thread core/ui/src/main/java/com/yapp/ndgl/core/ui/util/ReorderableList.kt
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: 1

🤖 Fix all issues with AI agents
In
`@feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/TravelDetailViewModel.kt`:
- Around line 474-477: The endTime is still using itinerary.totalDuration which
reflects the old places; update endTime to use the duration computed from
durationUpdatedPlaces (e.g., recompute totalDuration from durationUpdatedPlaces)
or construct a temporary Itinerary with places = durationUpdatedPlaces, compute
its totalDuration, then set endTime = (itinerary.startTime ?:
DEFAULT_START_TIME.hours) + recomputedTotalDuration; ensure
calculatePlaceStartTimes is used for places and the recomputed totalDuration is
used for endTime instead of itinerary.totalDuration.
🧹 Nitpick comments (4)
core/ui/src/main/java/com/yapp/ndgl/core/ui/designsystem/NDGLBottomSheet.kt (1)

60-70: 닫기 아이콘에 접근성 설명을 추가하는 것을 권장합니다.

contentDescriptionnull로 설정되어 있어 스크린 리더 사용자가 버튼의 기능을 인식하기 어렵습니다.

♻️ 수정 제안
                 Icon(
                     imageVector = ImageVector.vectorResource(R.drawable.ic_24_close),
-                    contentDescription = null,
+                    contentDescription = "닫기",
                     tint = NDGLTheme.colors.black600,
feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/component/TransportBottomSheet.kt (1)

86-86: itemsIndexed 대신 items 사용을 권장합니다.

index 파라미터가 사용되지 않으므로 items(availableTransports)로 변경하면 더 간결합니다.

♻️ 수정 제안
-                itemsIndexed(availableTransports) { index, transport ->
+                items(availableTransports) { transport ->

items import 추가 필요:

import androidx.compose.foundation.lazy.items
core/ui/src/main/java/com/yapp/ndgl/core/ui/util/ReorderableList.kt (1)

104-104: autoScrollJobmutableStateOf 대신 일반 변수 사용을 권장합니다.

autoScrollJob은 UI 렌더링에 사용되지 않는 내부 상태이므로 mutableStateOf로 인한 불필요한 리컴포지션 오버헤드가 발생합니다.

♻️ 수정 제안
-    private var autoScrollJob by mutableStateOf<Job?>(null)
+    private var autoScrollJob: Job? = null
feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/TravelDetailScreen.kt (1)

506-523: Mock 교통수단 목록 생성 로직에서 중복이 발생할 수 있습니다.

mockAvailableTransports의 각 항목은 고정된 duration/distance 값을 가집니다. state.selectedPlace.transportToNext가 다른 값(예: WALK, 10분, 800m)을 가지면 remove()가 일치하는 항목을 찾지 못해 add() 후 같은 타입의 교통수단이 중복될 수 있습니다.

TODO 주석이 있어 실제 API 연동 시 수정될 예정이므로 현재는 큰 문제가 아니지만, 임시 구현 시에도 타입 기반 필터링을 고려해볼 수 있습니다.

♻️ 선택적 개선안
             val mockAvailableTransports = mutableListOf(
                 TransportSegment(TransportType.WALK, 15.minutes, 1200),
                 TransportSegment(TransportType.CAR, 10.minutes, 5400),
                 TransportSegment(TransportType.BUS, 25.minutes, 4800),
                 TransportSegment(TransportType.TRAIN, 40.minutes, 12000),
             )
-            mockAvailableTransports.remove(state.selectedPlace.transportToNext)
-            mockAvailableTransports.add(state.selectedPlace.transportToNext)
+            mockAvailableTransports.removeAll { it.type == state.selectedPlace.transportToNext.type }
+            mockAvailableTransports.add(state.selectedPlace.transportToNext)

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: 4

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/traveldetail/TravelDetailViewModel.kt (1)

374-381: ⚠️ Potential issue | 🟠 Major

confirmEditMode에서 재배치 후 장소별 시작 시간이 재계산되지 않습니다.

reorderPlacesrecalculateTransportSegments만 호출하고 calculatePlaceStartTimes는 호출하지 않습니다. confirmEditMode에서 tempItineraries를 그대로 itineraries에 반영하므로, 재배치된 장소들의 startTime이 원래 순서 기준의 값을 유지하게 됩니다.

🐛 수정 제안
     private fun confirmEditMode() {
         reduce {
+            val recalculatedItineraries = tempItineraries.map { itinerary ->
+                val baseStartTime = itinerary.startTime ?: DEFAULT_START_TIME.hours
+                val updatedPlaces = calculatePlaceStartTimes(itinerary.places, baseStartTime)
+                itinerary.copy(
+                    places = updatedPlaces,
+                    endTime = itinerary.startTime?.let { baseStartTime + itinerary.copy(places = updatedPlaces).totalDuration },
+                )
+            }
             copy(
-                itineraries = tempItineraries,
+                itineraries = recalculatedItineraries,
                 isEditMode = false,
                 selectedPlaceIds = emptySet(),
             )
         }
     }
🤖 Fix all issues with AI agents
In `@core/ui/src/main/java/com/yapp/ndgl/core/ui/util/ReorderableList.kt`:
- Around line 148-162: tryMoveDown is missing the same isReorderable check used
in tryMoveUp, so non-reorderable targets can be moved when dragging down; update
tryMoveDown to verify isReorderable(target.key) before calling onReorder in both
the visible-next-item branch and the else/autoscroll branch (mirror the guard in
tryMoveUp), and only set info when the target passes isReorderable and onReorder
returns true.
- Around line 65-72: The remember block in rememberReorderableState captures
isReorderable once so subsequent changes are ignored; update the remember call
to include isReorderable as a key (e.g., remember(isReorderable) {
ReorderableState(...) }) so a new ReorderableState is created when isReorderable
changes; keep using rememberUpdatedState(for onReorder) as-is and reference the
rememberReorderableState function, ReorderableState class, and the isReorderable
parameter when making this change.

In
`@feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/TravelDetailScreen.kt`:
- Around line 509-526: The mockAvailableTransports construction in
TravelDetailScreen can produce duplicates because
remove(state.selectedPlace.transportToNext) relies on structural equality;
instead remove any entries by transport type before adding the selected
transport. Locate the block building mockAvailableTransports and replace the
remove/add sequence with a type-based filter or removeAll by comparing
TransportSegment.transportType/TransportType (e.g., removeAll { it.type ==
state.selectedPlace.transportToNext.type }) then add the selected transport,
ensuring initialTransport/availableTransports remain consistent for
TransportBottomSheet.

In
`@feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/TravelDetailViewModel.kt`:
- Around line 393-416: confirmChangeTransportSegment updates transportToNext but
doesn't update places' endTime when travel duration changes; after mapping the
selected day's places and recalculating start times via
calculatePlaceStartTimes, also recalculate and set each Place's endTime based on
its startTime plus its totalDuration (including the new transportToNext where
applicable) so totalDuration and endTime stay consistent. Locate
confirmChangeTransportSegment and the block that builds timedPlaces, and either
(A) update calculatePlaceStartTimes to compute and return endTime for each place
using place.startTime + place.totalDuration, or (B) after timedPlaces is
produced, map over timedPlaces and set place.endTime = place.startTime +
place.totalDuration before copying the dayItinerary back into itineraries;
ensure you reference transportToNext, totalDuration, startTime, and endTime when
making the change.
🧹 Nitpick comments (8)
core/util/src/main/java/com/yapp/ndgl/core/util/IntUtil.kt (1)

11-17: 정수 연산으로 정수 킬로미터 여부를 확인하는 것이 더 안전합니다.

km % 1 == 0.0은 부동소수점 비교입니다. IntDouble로 나누기 때문에 현재 범위에서는 실질적으로 문제가 없지만, this % 1000 == 0으로 정수 연산을 사용하면 의도가 더 명확하고 안전합니다.

또한, formatDecimal()Locale.US를 사용하지만, formatDistance()Locale.getDefault()를 사용하고 있습니다. 일부 로케일에서는 소수점 구분자가 쉼표(,)로 표시되어 "5,5km"처럼 출력될 수 있습니다. 의도된 동작인지 확인해 주세요.

♻️ 정수 연산 활용 제안
         this >= 1000 -> {
-            val km = this / 1000.0
-            if (km % 1 == 0.0) {
-                "${km.toInt()}km"
+            if (this % 1000 == 0) {
+                "${this / 1000}km"
             } else {
+                val km = this / 1000.0
                 String.format(getDefault(), "%.1fkm", km)
             }
         }
feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/component/PlaceItem.kt (1)

93-96: 투명 배경에 shape 파라미터가 불필요합니다.

Color.TransparentRoundedCornerShape를 지정해도 시각적 효과가 없으므로, .background(...) 호출 자체를 제거하거나 shape 인자를 제거하는 것을 고려해 주세요.

♻️ 수정 제안
             .fillMaxWidth()
-                .background(
-                    color = Color.Transparent,
-                    shape = RoundedCornerShape(16.dp),
-                )
                 .padding(horizontal = 0.dp),
core/ui/src/main/java/com/yapp/ndgl/core/ui/designsystem/NDGLBottomSheet.kt (2)

75-83: 닫기 아이콘에 contentDescription이 누락되어 접근성이 제한됩니다.

TalkBack 등 스크린 리더 사용자가 닫기 버튼의 용도를 알 수 없습니다. 의미 있는 content description을 추가해 주세요.

♻️ 수정 제안
                 Icon(
                     imageVector = ImageVector.vectorResource(R.drawable.ic_24_close),
-                    contentDescription = null,
+                    contentDescription = "닫기",
                     tint = NDGLTheme.colors.black600,

115-115: 파일 레벨 @OptIn이 이미 선언되어 있어 중복됩니다.

Line 1에 @file:OptIn(ExperimentalMaterial3Api::class)이 이미 적용되어 있으므로, Line 115의 @OptIn은 제거해도 됩니다.

core/ui/src/main/java/com/yapp/ndgl/core/ui/util/ReorderableList.kt (1)

182-199: 오토 스크롤에서 updateItemIndex()가 호출되지 않습니다.

스크롤 중에 새로운 아이템이 뷰포트에 들어와도 인덱스 업데이트가 발생하지 않아, 빠른 드래그 시 아이템 순서가 올바르게 변경되지 않을 수 있습니다. 스크롤 루프 내에서 updateItemIndex()를 호출하는 것을 고려해 주세요.

♻️ 수정 제안
                 scrollOffset?.let {
                     lazyListState.scrollBy(it)
+                    updateItemIndex()
                     delay(8)
                 } ?: break
feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/component/TransportBottomSheet.kt (1)

148-153: onConfirm 호출 후 sheetState.hide() 순서에 주의가 필요합니다.

onConfirm(selectedTransport)이 먼저 호출되면 상위에서 상태 변경으로 이 Composable이 composition에서 제거될 수 있고, 이 경우 sheetState.hide() 애니메이션이 실행되지 않습니다. 순서를 바꿔 먼저 시트를 숨긴 후 콜백을 호출하는 것이 안전합니다.

♻️ 수정 제안
                 onClick = {
                     coroutineScope.launch {
-                        onConfirm(selectedTransport)
                         sheetState.hide()
+                        onConfirm(selectedTransport)
                     }
                 },
feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/TravelDetailViewModel.kt (1)

559-571: 스텁 createTransportSegment에서 사용자 맞춤 교통수단 정보가 재배치 시 유실됩니다.

recalculateTransportSegmentscreateTransportSegment 스텁을 통해 모든 교통수단을 동일한 하드코딩 값으로 대체합니다. 사용자가 변경한 교통수단 선택이 장소 재배치 시 덮어씌워질 수 있습니다. API 연동 전까지는 불가피하지만, 연동 시 이 동작을 반드시 수정해야 합니다.

TODO 항목 추적을 위한 이슈를 생성해 드릴까요?

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

211-222: placesOffset = 3이 LazyColumn 아이템 구조에 강하게 결합되어 있습니다.

아이템 순서가 변경되거나 새로운 아이템이 장소 목록 위에 추가되면 이 오프셋이 맞지 않게 됩니다. 현재로서는 동작하지만, 상수에 대한 설명 주석이 있으면 향후 유지보수에 도움이 됩니다.

Comment on lines +65 to +72
return remember {
ReorderableState(
scope = scope,
lazyListState = lazyListState,
onReorder = onReorder,
isReorderable = isReorderable,
)
}
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

remember 블록에 키가 없어 isReorderable 변경 시 반영되지 않습니다.

rememberReorderableStateisReorderable 파라미터가 변경되어도 remember {} 블록에 키가 없으므로 ReorderableState가 재생성되지 않습니다. onReorderrememberUpdatedState로 최신 list를 참조하지만, isReorderable은 최초 캡처된 값이 계속 사용됩니다.

♻️ 수정 제안
+    val currentIsReorderable by rememberUpdatedState(isReorderable)
+    val currentOnReorder: (Int, Int) -> Boolean = { from, to ->
+        currentList.reorder(from - offset, to - offset)
+    }
+
     return remember {
         ReorderableState(
             scope = scope,
             lazyListState = lazyListState,
-            onReorder = onReorder,
-            isReorderable = isReorderable,
+            onReorder = currentOnReorder,
+            isReorderable = { currentIsReorderable(it) },
         )
     }
🤖 Prompt for AI Agents
In `@core/ui/src/main/java/com/yapp/ndgl/core/ui/util/ReorderableList.kt` around
lines 65 - 72, The remember block in rememberReorderableState captures
isReorderable once so subsequent changes are ignored; update the remember call
to include isReorderable as a key (e.g., remember(isReorderable) {
ReorderableState(...) }) so a new ReorderableState is created when isReorderable
changes; keep using rememberUpdatedState(for onReorder) as-is and reference the
rememberReorderableState function, ReorderableState class, and the isReorderable
parameter when making this change.

Comment on lines +148 to +162
// 아래로 드래그할 때
private fun tryMoveDown(itemInfo: LazyListItemInfo) {
val target = lazyListState.layoutInfo.visibleItemsInfo.firstOrNull { it.index == itemInfo.index + 1 }
if (target != null) {
if (onReorder(itemInfo.index, itemInfo.index + 1)) {
info = target
}
} else {
// 오토 스크롤 중 아이템이 화면에 보이지 않을 때
val lastItem = lazyListState.layoutInfo.visibleItemsInfo.last()
if (onReorder(itemInfo.index, lastItem.index)) {
info = lazyListState.layoutInfo.visibleItemsInfo.last()
}
}
}
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

tryMoveDown에서 isReorderable 체크가 누락되었습니다.

tryMoveUp (Line 136)에서는 대상 아이템의 isReorderable(target.key)를 확인하지만, tryMoveDown에서는 이 검증이 빠져있습니다. 아래로 드래그 시 재배치 불가능한 아이템(헤더, 구분선 등)과 위치가 바뀔 수 있습니다.

🐛 수정 제안
     private fun tryMoveDown(itemInfo: LazyListItemInfo) {
         val target = lazyListState.layoutInfo.visibleItemsInfo.firstOrNull { it.index == itemInfo.index + 1 }
         if (target != null) {
-            if (onReorder(itemInfo.index, itemInfo.index + 1)) {
+            if (isReorderable(target.key) && onReorder(itemInfo.index, itemInfo.index + 1)) {
                 info = target
             }
         } else {
             // 오토 스크롤 중 아이템이 화면에 보이지 않을 때
             val lastItem = lazyListState.layoutInfo.visibleItemsInfo.last()
-            if (onReorder(itemInfo.index, lastItem.index)) {
+            if (isReorderable(lastItem.key) && onReorder(itemInfo.index, lastItem.index)) {
                 info = lazyListState.layoutInfo.visibleItemsInfo.last()
             }
         }
     }
📝 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 tryMoveDown(itemInfo: LazyListItemInfo) {
val target = lazyListState.layoutInfo.visibleItemsInfo.firstOrNull { it.index == itemInfo.index + 1 }
if (target != null) {
if (onReorder(itemInfo.index, itemInfo.index + 1)) {
info = target
}
} else {
// 오토 스크롤 중 아이템이 화면에 보이지 않을 때
val lastItem = lazyListState.layoutInfo.visibleItemsInfo.last()
if (onReorder(itemInfo.index, lastItem.index)) {
info = lazyListState.layoutInfo.visibleItemsInfo.last()
}
}
}
// 아래로 드래그할 때
private fun tryMoveDown(itemInfo: LazyListItemInfo) {
val target = lazyListState.layoutInfo.visibleItemsInfo.firstOrNull { it.index == itemInfo.index + 1 }
if (target != null) {
if (isReorderable(target.key) && onReorder(itemInfo.index, itemInfo.index + 1)) {
info = target
}
} else {
// 오토 스크롤 중 아이템이 화면에 보이지 않을 때
val lastItem = lazyListState.layoutInfo.visibleItemsInfo.last()
if (isReorderable(lastItem.key) && onReorder(itemInfo.index, lastItem.index)) {
info = lazyListState.layoutInfo.visibleItemsInfo.last()
}
}
}
🤖 Prompt for AI Agents
In `@core/ui/src/main/java/com/yapp/ndgl/core/ui/util/ReorderableList.kt` around
lines 148 - 162, tryMoveDown is missing the same isReorderable check used in
tryMoveUp, so non-reorderable targets can be moved when dragging down; update
tryMoveDown to verify isReorderable(target.key) before calling onReorder in both
the visible-next-item branch and the else/autoscroll branch (mirror the guard in
tryMoveUp), and only set info when the target passes isReorderable and onReorder
returns true.

@mj010504 mj010504 merged commit 83c6ce4 into develop Feb 16, 2026
2 checks passed
@mj010504 mj010504 deleted the feature/NDGL-86 branch February 16, 2026 08:36
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