Skip to content

[NDGL-63] 장소 상세보기 화면 UI/UX 제작#19

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

[NDGL-63] 장소 상세보기 화면 UI/UX 제작#19
mj010504 merged 14 commits intodevelopfrom
design/NDGL-63

Conversation

@mj010504
Copy link
Copy Markdown
Contributor

@mj010504 mj010504 commented Feb 6, 2026

개요

  • 장소 상세보기 화면 UI/UX 제작

디자인

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

영상

NDGL-63.mp4

변경사항

  • 장소 바텀시트
    • 체류 시간 설정, 비용 추가, 메모 추가
  • 장소 상세 보기 화면
    • 정보 탭
    • 사진 탭
    • PlanB 장소 변경하기
  • 외부 브라우저 연결
    • 장소 바텀시트 - 길찾기 클릭
    • 장소 상세보기 화면 - 주소 클릭
    • 장소 상세보기 화면 - 메뉴 클릭
  • 여행 시작 시간 설정 기능 추가
  • PlaceItem의 PlaceNumber 밑 시간 표기
    • 체류 시간 변경 시 자동 변경
    • 시작 시간 설정 시 자동 변경
    • 기본 시작 시간 08:00
  • 디자인 시스템에 NDGL InputModal 추가

추후 작업사항

  • 일정 추가 화면
  • 스낵바 메세지 연결

테스트 체크 리스트

  • PlaceBottomSheet 시간 추가
  • PlaceBottomSheet 비용 추가
  • PlaceBottomSheet 메모 추가
  • 외부 브라우저 연결

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • 장소 상세 정보 화면 추가 (정보, 사진 탭, 일정 추가 기능)
    • 비용 및 메모 입력 모달 추가
    • 지속 시간 선택 기능 개선
    • 장소별 빠른 액션 시트 추가
  • 개선 사항

    • 일정 설정 UI 개선 (AM/PM 시간 형식 지원)
    • 숫자 천단위 형식 추가
    • 버튼 클릭 피드백 개선

@mj010504 mj010504 requested a review from jihee-dev February 6, 2026 07:02
@mj010504 mj010504 self-assigned this Feb 6, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 6, 2026

Warning

Rate limit exceeded

@mj010504 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 14 minutes and 44 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

여행 세부 기능에 장소 상세 화면을 추가합니다. 새로운 PlaceDetail 컴포넌트, ViewModel, 계약 정의를 포함하며, 기존 TravelDetail 기능을 모달, 바텀 시트, 입력 컴포넌트로 확장합니다. UI 유틸리티와 리소스도 추가됩니다.

Changes

Cohort / File(s) Summary
PlaceDetail Feature
feature/travel/src/main/java/com/yapp/ndgl/feature/travel/placedetail/PlaceDetailContract.kt, PlaceDetailScreen.kt, PlaceDetailViewModel.kt
새로운 PlaceDetail 기능의 핵심 구현으로, UI 상태, 의도, 부작용 계약, 화면 렌더링, ViewModel 로직을 정의합니다. 장소 정보, 사진 탭, 대체 장소 선택, 모달 표시 상태를 관리합니다.
PlaceDetail Components
feature/travel/src/main/java/com/yapp/ndgl/feature/travel/placedetail/component/PlaceDetailTabRow.kt, PlaceInfoTab.kt, PlacePhotoTab.kt
PlaceDetail 화면의 UI 구성 요소들로, 탭 선택, 장소 정보 표시, 팁 페이저, 대체 장소 목록, 사진 그리드를 렌더링합니다.
TravelDetail Updates
feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/TravelDetailContract.kt, TravelDetailScreen.kt, TravelDetailViewModel.kt
기존 TravelDetail 기능을 확장하여 장소 선택, 시간/비용/메모 모달, PlaceDetail 네비게이션, 바텀 시트 상호작용을 추가합니다. TravelPlace에 userData 중첩 구조를 도입합니다.
Duration & Place Bottom Sheet Components
feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/component/DurationPickerContent.kt, PlaceBottomSheet.kt, PlaceItem.kt, TimelineContent.kt, TravelDetailToolBar.kt
시간 선택기, 장소 바텀 시트, 시간표 콘텐츠를 추가하고 기존 컴포넌트를 업데이트하여 startTime 표시, userData 기반 기간, 새로운 문자열 리소스를 지원합니다.
Core UI Components & Utilities
core/ui/src/main/java/com/yapp/ndgl/core/ui/designsystem/NDGLInputModal.kt, core/ui/src/main/java/com/yapp/ndgl/core/ui/util/ModifierUtil.kt, core/ui/src/main/java/com/yapp/ndgl/core/ui/util/WebUtil.kt
새로운 입력 모달 컴포넌트, 리플 없는 클릭 가능 수정자, 브라우저 실행 유틸리티를 추가합니다.
Core Utilities & Resources
core/util/src/main/java/com/yapp/ndgl/core/util/DurationUtil.kt, core/util/src/main/java/com/yapp/ndgl/core/util/IntUtil.kt, core/ui/src/main/res/drawable/ic_20_clipboard.xml, ic_20_pen.xml, core/ui/src/main/res/values/strings.xml
기간 및 정수 형식 지정 유틸리티, 새로운 아이콘 리소스, 스케줄, 비용, 메모, 장소 상세 관련 문자열 리소스를 추가합니다.
Navigation & Build Configuration
navigation/src/main/java/com/yapp/ndgl/navigation/Route.kt, core/ui/build.gradle.kts, feature/travel/src/main/java/com/yapp/ndgl/feature/travel/navigation/TravelEntry.kt
PlaceDetail 경로 추가, core.util 의존성 추가, TravelEntry에서 PlaceDetail 네비게이션 와이어링을 구현합니다.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 2.63% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목은 주요 변경사항인 장소 상세보기 화면 UI/UX 제작을 명확하게 설명하고 있으며, 이는 전체 변경사항의 핵심을 잘 반영하고 있습니다.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ 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-63

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 2 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 force-pushed the design/NDGL-63 branch 2 times, most recently from ccd8c28 to 5ebcb9a Compare February 10, 2026 16:27
@mj010504 mj010504 marked this pull request as ready for review February 11, 2026 07:30
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: 12

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/component/TimelineContent.kt (2)

74-91: ⚠️ Potential issue | 🟠 Major

닫기 버튼에 접근성 라벨이 필요합니다.

클릭 가능한 아이콘인데 contentDescription이 null이라 스크린리더에서 의미가 사라집니다.

🔧 제안 수정
 Icon(
     imageVector = ImageVector.vectorResource(R.drawable.ic_24_close),
-    contentDescription = null,
+    contentDescription = stringResource(R.string.accessibility_close),
     tint = NDGLTheme.colors.black900,
 )

153-224: ⚠️ Potential issue | 🟠 Major

00:00~00:59 시작 시간이 선택 불가해질 수 있습니다.

isTimeSetinWholeHours > 0 조건이라 0시대는 버튼이 비활성화됩니다. 0시가 유효 값이면 별도 플래그나 분 단위 체크로 분리하는 게 안전합니다.

🔧 한 가지 개선 방향 (예시)
-    val isTimeSet = selectedStartTime.inWholeHours > 0 && previewEndTime.inWholeHours > 0
+    // 0시도 유효 값이라면, "사용자 설정 여부"를 별도 플래그로 관리 권장
+    val isTimeSet = previewEndTime.inWholeMinutes > 0
🤖 Fix all issues with AI agents
In `@core/ui/src/main/java/com/yapp/ndgl/core/ui/designsystem/NDGLInputModal.kt`:
- Around line 40-105: NDGLInputModal's internal state (textFieldValue) can drift
from the external value when the parent rejects changes; add a validation
callback parameter (e.g., validateInput: (String) -> Boolean = { true }) to
NDGLInputModal and use it inside the BasicTextField onValueChange handler: call
validateInput(newValue.text) before updating textFieldValue and before invoking
onValueChange, only committing the internal state when validation passes;
alternatively make onValueChange return a Boolean and update textFieldValue only
when the callback approves—update references to textFieldValue, onValueChange,
value, and the LaunchedEffect(value) synchronization accordingly so UI and
external state remain consistent.

In `@core/ui/src/main/java/com/yapp/ndgl/core/ui/util/WebUtil.kt`:
- Around line 12-14: In WebUtil.kt update the catch block that currently reads
catch (e: Exception) { Timber.e("Failed to launch browser: $url") } to log the
actual exception and provide user feedback: include the exception object in the
Timber call (e.g., Timber.e(e, "...")) so stack traces are recorded, and notify
the user of failure (e.g., via Toast or Snackbar from the calling Context) with
a friendly message; locate the try-catch around the browser launch logic (the
method that opens/launches the browser) and replace the single-line log with
these two actions.

In
`@feature/travel/src/main/java/com/yapp/ndgl/feature/travel/placedetail/component/PlaceInfoTab.kt`:
- Around line 94-99: 현재 PlaceInfoTab.kt의 해당 Row 사용자가 메뉴/웹사이트 행을 클릭했을 때 잘못
clickAddress()를 호출하고 있습니다; 이 Row의 Modifier.noRippleClickable 콜백을
clickAddress()에서 clickMenu()로 변경해 메뉴 클릭 시 clickMenu()가 호출되도록 수정하세요 (참조:
Modifier.noRippleClickable 콜백 위치, clickAddress(), clickMenu(), Row 구성 요소).
- Around line 169-185: PlaceTipsPager should early-return when tips is empty to
avoid creating a Pager/rememberPagerState for size 0; add a guard at the start
of the PlaceTipsPager composable that checks if (tips.isEmpty()) and returns
immediately before creating pagerState or rendering HorizontalPager, so you
don't call rememberPagerState(pageCount = { tips.size }) or render
HorizontalPager when there are no tips.

In
`@feature/travel/src/main/java/com/yapp/ndgl/feature/travel/placedetail/PlaceDetailContract.kt`:
- Around line 55-56: aspectRatio getter (in PlaceDetailContract.kt) divides
width by height without guarding against height == 0; change the aspectRatio
property to check height for zero (or <= 0) and return a safe default (e.g., 0f
or 1f) when height is zero to avoid Infinity/NaN, otherwise compute
width.toFloat() / height.toFloat(); update any callers of aspectRatio if they
expect non-zero behavior.

In
`@feature/travel/src/main/java/com/yapp/ndgl/feature/travel/placedetail/PlaceDetailScreen.kt`:
- Around line 172-182: The displayed text in the Text composable uses a
hardcoded "식당 • $20~40 • 리뷰" string; update the buildAnnotatedString call inside
PlaceDetailScreen (the Text / buildAnnotatedString block referencing
placeInfo.rating and placeInfo.formattedRatingCount) to use placeInfo.category
and the price range field (e.g., placeInfo.priceRange or similar) instead of the
hardcoded values, concatenating them with separators and preserving the existing
styles for rating and formattedRatingCount; include sensible null/empty
fallbacks (e.g., "-" or "가격정보 없음") when category or priceRange are missing.

In
`@feature/travel/src/main/java/com/yapp/ndgl/feature/travel/placedetail/PlaceDetailViewModel.kt`:
- Around line 101-107: confirmChangePlace currently only closes the modal
(reduce { copy(showChangeModal = false) }) but doesn't apply the selected
alternative; update confirmChangePlace to use selectedAlternativePlace to
perform the actual change (e.g., call the ViewModel's place-update function or
trigger navigation) and then close the modal, or if intentional postpone, add a
clear TODO/TODO KTX comment inside confirmChangePlace referencing
selectedAlternativePlace and the intended handler (e.g., updatePlace(...) or
navigateToPlace(...)) so the missing behavior is explicit; look for
confirmChangePlace, selectedAlternativePlace, and reduce/copy(showChangeModal)
in the file to place the change.

In
`@feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/component/DurationPickerContent.kt`:
- Around line 61-63: Normalize currentDuration when computing selectedHour and
selectedMinute: clamp the hour into 0-23 and snap the minute to a 5-minute
bucket so the WheelPicker initial positions match its ranges. In the
DurationPickerContent initialization (where selectedHour and selectedMinute are
set), compute hour = currentDuration.inWholeHours.toInt().coerceIn(0, 23) (or
use modulo 24 for wrap behavior) and minute = ((currentDuration.inWholeMinutes %
60).toInt() / 5) * 5 to force 0,5,10,...,55, then assign selectedHour and
selectedMinute from these normalized values.

In
`@feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/component/PlaceItem.kt`:
- Around line 121-150: The PlaceNumber composable is formatting startTime with
raw hours/minutes which yields values like "50:30" when duration exceeds 24h;
change PlaceNumber to use the shared time formatter (formatString()) or convert
duration into day + hh:mm (e.g., "1일 + 02:30") instead of raw hours, so long
durations render correctly and consistently with
TravelDetailToolBar.toAmPmTimeString(); update the logic that computes
totalMinutes/hours/minutes to call formatString(startTime) or compute days =
hours/24, remHours = hours%24 and build the display string, and replace the
current manual String.format usage in PlaceNumber accordingly.

In
`@feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/component/TimelineContent.kt`:
- Around line 61-64: previewEndTime is only remembered with selectedStartTime as
a key, so it won't update when totalDuration changes; update the remember call
for previewEndTime to include totalDuration as a key (e.g.,
remember(selectedStartTime, totalDuration) { derivedStateOf { selectedStartTime
+ totalDuration } }) so previewEndTime recomputes when either selectedStartTime
or totalDuration changes.

In
`@feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/TravelDetailScreen.kt`:
- Around line 473-501: The cost modal currently keeps prior input when reopened
and silently ignores invalid submits; change the cost state to reset when the
modal opens (e.g., derive/reset mutable state from state.selectedPlace or use
remember with a key like state.showCostModal or state.selectedPlace) so cost is
initialized to state.selectedPlace.userData?.cost?.toString() whenever the modal
appears, and add explicit validation feedback in the NDGLInputModal positive
handler (instead of silently doing nothing on toIntOrNull == null) by either
disabling/gray-out the confirm button when the input is invalid or invoking a
local error state and showing an error message, then call confirmCost(costValue)
on valid parse; update references: state.showCostModal, state.selectedPlace,
cost, NDGLInputModal, confirmCost, dismissCostModal.

In
`@feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/TravelDetailViewModel.kt`:
- Line 326: totalDuration is computed using the old itineraries list which can
be stale after start-time or duration updates; replace the use of itineraries
with updatedItineraries when computing totalDuration (i.e., use
updatedItineraries.getOrNull(dayIndex)?.totalDuration ?: 0.hours) so endTime
calculations that follow use the updated data (references: totalDuration,
updatedItineraries, itineraries, dayIndex, endTime).
🧹 Nitpick comments (14)
feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/component/PlaceItem.kt (1)

216-288: 프리뷰에 startTime 샘플을 추가하면 UI 확인이 쉬워집니다.
새로 추가된 시간 라인이 실제로 어떻게 보이는지 미리 확인할 수 있어요.

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

6-6: 로케일 사용에 대한 확인이 필요합니다.

Locale.US를 사용하면 천 단위 구분자로 쉼표(,)를 일관되게 사용합니다. 한국어 앱에서 이 형식이 의도된 것인지 확인해 주세요. 기기 로케일을 따르려면 Locale.getDefault()를 사용할 수 있습니다.

feature/travel/src/main/java/com/yapp/ndgl/feature/travel/placedetail/component/PlacePhotoTab.kt (1)

35-44: 이미지 로딩 상태 처리 추가를 권장합니다.

AsyncImageplaceholdererror 파라미터가 없어 이미지 로딩 실패 시 사용자에게 명확한 피드백이 제공되지 않습니다. 또한 Color.LightGray 대신 테마 색상 사용을 고려해 주세요.

♻️ 제안: 로딩/에러 상태 처리 추가
 AsyncImage(
     model = photo.url,
     contentDescription = null,
+    placeholder = painterResource(R.drawable.placeholder_image),
+    error = painterResource(R.drawable.placeholder_image),
     modifier = Modifier
         .fillMaxWidth()
         .aspectRatio(photo.aspectRatio)
         .clip(RoundedCornerShape(8.dp))
-        .background(Color.LightGray),
+        .background(NDGLTheme.colors.black100),
     contentScale = ContentScale.Crop,
 )
feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/component/TravelDetailToolBar.kt (2)

66-66: 아이콘 tint 색상 설정을 확인해 주세요.

Icontint 파라미터가 지정되지 않아 Material3 기본 색상이 적용됩니다. 다른 UI 요소와의 일관성을 위해 텍스트 색상(NDGLTheme.colors.black400)과 동일하게 설정하는 것이 좋을 수 있습니다.

♻️ 제안: tint 색상 추가
-        Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_20_clock), contentDescription = null)
+        Icon(
+            imageVector = ImageVector.vectorResource(R.drawable.ic_20_clock),
+            contentDescription = null,
+            tint = NDGLTheme.colors.black400,
+        )

88-88: EditTravelButton의 아이콘에도 동일하게 tint 설정을 고려해 주세요.

StartTimeSettingButton과 마찬가지로 일관된 스타일링을 위해 ic_20_edit 아이콘에도 tint 색상을 명시적으로 지정하는 것이 좋습니다.

feature/travel/src/main/java/com/yapp/ndgl/feature/travel/placedetail/component/PlaceDetailTabRow.kt (2)

47-54: 선택된 탭의 시각적 구분을 강화하는 것을 고려해 주세요.

현재 선택된 탭과 선택되지 않은 탭의 텍스트 색상이 동일합니다(black600). 접근성과 사용성 향상을 위해 선택된 탭의 텍스트를 더 진하게 표시하거나 다른 색상을 사용하는 것을 고려해 보세요.

♻️ 제안: 선택 상태에 따른 텍스트 스타일 차별화
                     text = {
                         Text(
                             stringResource(tab.titleRes),
-                            color = NDGLTheme.colors.black600,
-                            style = NDGLTheme.typography.bodyMdSemiBold,
+                            color = if (isSelected) NDGLTheme.colors.black900 else NDGLTheme.colors.black600,
+                            style = if (isSelected) NDGLTheme.typography.bodyMdBold else NDGLTheme.typography.bodyMdSemiBold,
                             textAlign = TextAlign.Center,
                         )
                     },

17-17: 불필요한 import alias가 있습니다.

SecondaryTabRow를 동일한 이름으로 alias하고 있어 불필요합니다.

♻️ 제안: import 간소화
-import androidx.compose.material3.SecondaryTabRow as SecondaryTabRow
+import androidx.compose.material3.SecondaryTabRow
feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/component/DurationPickerContent.kt (1)

148-150: startIndex 계산에서 하드코딩 값을 제거하세요.
- 2는 visibleItemCount 변경 시 중앙 정렬이 깨질 수 있습니다. visibleItemCount / 2로 계산하면 더 안전합니다.

♻️ 수정 제안
-    val startIndex = (totalItemsCount / 2) - (totalItemsCount / 2 % items.size) + initialIndex - 2
+    val startIndex =
+        (totalItemsCount / 2) - (totalItemsCount / 2 % items.size) + initialIndex - (visibleItemCount / 2)
feature/travel/src/main/java/com/yapp/ndgl/feature/travel/placedetail/PlaceDetailContract.kt (1)

21-22: TODO가 남아 있습니다.
추후 변경 예정 항목은 이슈/작업 항목으로 추적해 주세요. 원하시면 이슈 등록 도와드릴게요.

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

40-41: 불필요한 명시적 import 제거

kotlin.Intkotlin.String은 Kotlin의 기본 타입으로 명시적 import가 필요하지 않습니다. IDE 자동 완성이나 복사-붙여넣기 과정에서 추가된 것으로 보입니다.

♻️ 불필요한 import 제거
-import kotlin.Int
-import kotlin.String
 import kotlin.time.Duration.Companion.hours

188-194: 하드코딩된 통화 단위 문자열

"원" 통화 단위가 코드에 직접 하드코딩되어 있습니다. 다른 문자열들은 stringResource를 사용하고 있으므로, 일관성과 향후 다국어 지원을 위해 문자열 리소스로 분리하는 것이 좋습니다.

♻️ 문자열 리소스 사용 제안
 Text(
-    text = "${cost.formatDecimal()}원",
+    text = stringResource(R.string.cost_format, cost.formatDecimal()),
     color = NDGLTheme.colors.black500,
     style = NDGLTheme.typography.bodyMdMedium,
 )
feature/travel/src/main/java/com/yapp/ndgl/feature/travel/placedetail/PlaceDetailScreen.kt (1)

232-241: 사진 분할 로직 개선 가능

foldIndexed로 mutable list를 생성하는 방식은 동작하지만, partition 또는 filterIndexed를 사용하면 더 간결하고 immutable하게 처리할 수 있습니다.

♻️ partition 활용 예시
 PlaceDetailTab.PHOTO -> {
-    val (leftPhotos, rightPhotos) = state.photos.foldIndexed(
-        initial = mutableListOf<PlacePhoto>() to mutableListOf<PlacePhoto>(),
-    ) { index, lists, photo ->
-        if (index % 2 == 0) {
-            lists.first.add(photo)
-        } else {
-            lists.second.add(photo)
-        }
-        lists
-    }
+    val (leftPhotos, rightPhotos) = state.photos
+        .withIndex()
+        .partition { it.index % 2 == 0 }
+        .let { (left, right) -> left.map { it.value } to right.map { it.value } }

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

472-494: confirmCost와 confirmMemo의 중복 패턴

두 함수가 거의 동일한 구조를 가지고 있습니다. 현재 상태로도 동작에 문제는 없으나, 향후 유지보수를 위해 공통 로직을 추출하는 것을 고려해볼 수 있습니다.

Also applies to: 506-528

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

503-525: 메모 모달도 동일한 상태 초기화 패턴 적용 필요

비용 모달과 마찬가지로, 메모 모달도 remember에 key를 추가하여 모달이 다시 열릴 때 최신 값으로 초기화되도록 하는 것이 좋습니다.

♻️ key 추가
 if (state.showMemoModal && state.selectedPlace != null) {
-    var memo by remember { mutableStateOf(state.selectedPlace.userData?.memo ?: "") }
+    var memo by remember(state.showMemoModal) { 
+        mutableStateOf(state.selectedPlace.userData?.memo ?: "") 
+    }

Comment thread core/ui/src/main/java/com/yapp/ndgl/core/ui/util/WebUtil.kt
Comment on lines +169 to +185
@Composable
private fun PlaceTipsPager(
tips: List<String>,
creatorName: String,
) {
val pagerState = rememberPagerState(pageCount = { tips.size })

Box(
modifier = Modifier.wrapContentSize(),
) {
HorizontalPager(
state = pagerState,
modifier = Modifier.fillMaxWidth(),
contentPadding = PaddingValues(horizontal = 35.dp),
pageSpacing = 10.dp,
) { page ->
val tip = tips[page]
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:

# First, let's find and examine the PlaceInfoTab.kt file
find . -name "PlaceInfoTab.kt" -type f

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

Length of output: 176


🏁 Script executed:

# Once we find the file, let's read the full context around the function
# and see how PlaceTipsPager is being used in the composition
fd "PlaceInfoTab.kt"

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

Length of output: 174


🏁 Script executed:

# Search for HorizontalPager usage patterns in the codebase to understand
# how empty states are typically handled
rg "HorizontalPager" --type kt -A 3 -B 3

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

Length of output: 105


🏁 Script executed:

# Read the PlaceInfoTab.kt file
wc -l ./feature/travel/src/main/java/com/yapp/ndgl/feature/travel/placedetail/component/PlaceInfoTab.kt

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

Length of output: 180


🏁 Script executed:

# Read the file with specific line range around lines 169-185
cat -n ./feature/travel/src/main/java/com/yapp/ndgl/feature/travel/placedetail/component/PlaceInfoTab.kt | sed -n '150,200p'

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

Length of output: 2306


🏁 Script executed:

# Search for HorizontalPager usage without file type flag
rg "HorizontalPager" -A 5 -B 3

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

Length of output: 2571


🏁 Script executed:

# Read more of the PlaceTipsPager function to see the complete implementation
cat -n ./feature/travel/src/main/java/com/yapp/ndgl/feature/travel/placedetail/component/PlaceInfoTab.kt | sed -n '169,250p'

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

Length of output: 4059


🏁 Script executed:

# Check for build.gradle or build files to see Compose version
find . -name "build.gradle*" -o -name "build.gradle.kts" | head -5

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

Length of output: 207


🏁 Script executed:

# Search for any similar patterns of empty list handling in other composables
rg "isEmpty\(\)" -A 2 -B 2 --max-count=10

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

Length of output: 2623


🏁 Script executed:

# Check the Compose version in the build files
cat ./app/build.gradle.kts | grep -i "compose\|androidx.compose" -A 2 | head -20

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

Length of output: 61


🏁 Script executed:

# Also check for any version catalog
cat ./build.gradle.kts | grep -i "compose" -A 2 | head -20

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

Length of output: 243


🏁 Script executed:

# Search for any existing empty state handling with composables returning early
rg "if.*isEmpty.*return" -A 1 -B 1

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

Length of output: 473


🏁 Script executed:

# Check if there's a version catalog file for Compose dependencies
find . -name "libs.versions.toml" -o -name "versions.gradle.kts" | xargs cat 2>/dev/null | grep -i "compose" -A 1 | head -20

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

Length of output: 612


🏁 Script executed:

# Search for any test or error cases related to empty pagers
rg "pageCount.*0|HorizontalPager" -A 3 -B 1

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

Length of output: 1506


🏁 Script executed:

# Let's verify the complete end of the PlaceTipsPager function
cat -n ./feature/travel/src/main/java/com/yapp/ndgl/feature/travel/placedetail/component/PlaceInfoTab.kt | sed -n '240,260p'

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

Length of output: 868


빈 tips 리스트에 대한 early return 처리 필요

tips가 비어 있을 때 불필요한 HorizontalPager를 렌더링하게 됩니다. 다음과 같이 빈 상태를 먼저 처리하여 코드를 정리해 주세요.

🛠 수정 제안
 private fun PlaceTipsPager(
     tips: List<String>,
     creatorName: String,
 ) {
+    if (tips.isEmpty()) return
     val pagerState = rememberPagerState(pageCount = { tips.size })
🤖 Prompt for AI Agents
In
`@feature/travel/src/main/java/com/yapp/ndgl/feature/travel/placedetail/component/PlaceInfoTab.kt`
around lines 169 - 185, PlaceTipsPager should early-return when tips is empty to
avoid creating a Pager/rememberPagerState for size 0; add a guard at the start
of the PlaceTipsPager composable that checks if (tips.isEmpty()) and returns
immediately before creating pagerState or rendering HorizontalPager, so you
don't call rememberPagerState(pageCount = { tips.size }) or render
HorizontalPager when there are no tips.

Comment on lines +473 to +501
if (state.showCostModal && state.selectedPlace != null) {
var cost by remember { mutableStateOf(state.selectedPlace.userData?.cost?.toString() ?: "") }

NDGLInputModal(
onDismissRequest = dismissCostModal,
title = stringResource(R.string.cost_modal_title),
value = cost,
onValueChange = { newValue ->
if (newValue.isEmpty() || newValue.all { it.isDigit() }) {
cost = newValue
}
},
placeholder = stringResource(R.string.cost_modal_placeholder),
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number,
imeAction = ImeAction.Done,
),
positiveButtonText = stringResource(R.string.cost_modal_confirm),
onPositiveButtonClick = {
cost.toIntOrNull()?.let { costValue ->
confirmCost(costValue)
}
},
negativeButtonText = stringResource(R.string.cost_modal_cancel),
textAlign = TextAlign.Center,
placeholderStyle = NDGLTheme.typography.subtitleLgSemiBold,
textStyle = NDGLTheme.typography.subtitleLgSemiBold,
)
}
Copy link
Copy Markdown

@coderabbitai coderabbitai bot Feb 11, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

비용 입력 모달의 상태 관리 및 유효성 검사

두 가지 잠재적 이슈가 있습니다:

  1. remember { mutableStateOf(...) }를 사용하여 모달이 닫혔다 다시 열릴 때 이전 입력값이 유지될 수 있습니다.
  2. Line 491-495에서 빈 문자열이나 유효하지 않은 값 입력 시 toIntOrNull()이 null을 반환하면 아무 동작도 하지 않아 사용자에게 피드백이 없습니다.
💡 개선 제안
 if (state.showCostModal && state.selectedPlace != null) {
-    var cost by remember { mutableStateOf(state.selectedPlace.userData?.cost?.toString() ?: "") }
+    var cost by remember(state.showCostModal) { 
+        mutableStateOf(state.selectedPlace.userData?.cost?.toString() ?: "") 
+    }

     NDGLInputModal(
         ...
         onPositiveButtonClick = {
-            cost.toIntOrNull()?.let { costValue ->
-                confirmCost(costValue)
-            }
+            val costValue = cost.toIntOrNull()
+            if (costValue != null) {
+                confirmCost(costValue)
+            } else if (cost.isNotEmpty()) {
+                // TODO: Show error feedback for invalid input
+            }
         },
🤖 Prompt for AI Agents
In
`@feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/TravelDetailScreen.kt`
around lines 473 - 501, The cost modal currently keeps prior input when reopened
and silently ignores invalid submits; change the cost state to reset when the
modal opens (e.g., derive/reset mutable state from state.selectedPlace or use
remember with a key like state.showCostModal or state.selectedPlace) so cost is
initialized to state.selectedPlace.userData?.cost?.toString() whenever the modal
appears, and add explicit validation feedback in the NDGLInputModal positive
handler (instead of silently doing nothing on toIntOrNull == null) by either
disabling/gray-out the confirm button when the input is invalid or invoking a
local error state and showing an error message, then call confirmCost(costValue)
on valid parse; update references: state.showCostModal, state.selectedPlace,
cost, NDGLInputModal, confirmCost, dismissCostModal.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

keyboardType이 Number인데도 해당 로직이 필요할까요?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@mj010504 좋은 지적입니다! 맞습니다.

말씀하신 대로 keyboardType = KeyboardType.Number가 설정되어 있고, 추가로 onValueChange에서 이미 newValue.all { it.isDigit() } 검증을 하고 있어서 숫자가 아닌 입력은 필터링되고 있습니다.

또한 NDGLInputModal이 입력값이 비어있을 때 자동으로 확인 버튼을 비활성화하므로, 빈 문자열 케이스도 이미 UI 레벨에서 처리되고 있습니다.

따라서 toIntOrNull()?.let의 null 체크는 실질적으로:

  • 정수 오버플로우 (매우 큰 숫자 입력 시) 같은 극히 드문 엣지 케이스만 방어하는 것입니다

현재 코드로도 충분히 안전하며, 제가 제안한 추가 검증 로직은 선택사항입니다. 기존 구현이 이미 주요 케이스들을 잘 처리하고 있다고 판단됩니다. 👍

🐰


🧠 Learnings used
Learnt from: mj010504
Repo: YAPP-Github/27th-App-Team-1-Android PR: 17
File: feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/TravelDetailContract.kt:52-59
Timestamp: 2026-02-11T12:09:30.033Z
Learning: In the Android project for YAPP 27th App Team 1, the `Budget.formatString()` method in `TravelDetailContract.kt` intentionally returns "만원" for any amount less than 10,000 won as per product requirements. This is not a bug but the intended behavior.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

추후에 수정해보도록 하겠습니다!

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@mj010504 감사합니다! 현재 구현도 충분히 안전하니 편하실 때 개선하시면 됩니다. 😊


🧠 Learnings used
Learnt from: mj010504
Repo: YAPP-Github/27th-App-Team-1-Android PR: 17
File: feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/TravelDetailContract.kt:52-59
Timestamp: 2026-02-11T12:09:30.033Z
Learning: In the Android project for YAPP 27th App Team 1, the `Budget.formatString()` method in `TravelDetailContract.kt` intentionally returns "만원" for any amount less than 10,000 won as per product requirements. This is not a bug but the intended behavior.

@mj010504 mj010504 force-pushed the design/NDGL-63 branch 3 times, most recently from bb3e164 to 07576dd Compare February 12, 2026 14:06
@mj010504 mj010504 force-pushed the design/NDGL-53 branch 4 times, most recently from 822f9b3 to 1640f1c Compare February 14, 2026 02:35
Base automatically changed from design/NDGL-53 to develop February 14, 2026 02:38
@mj010504 mj010504 merged commit e9b6ca1 into develop Feb 14, 2026
2 checks passed
@mj010504 mj010504 deleted the design/NDGL-63 branch February 14, 2026 02:43
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