Skip to content

[Feat] PlaceDetailActivity Compose 마이그레이션#25

Merged
oungsi2000 merged 18 commits intodevelopfrom
feat/23
Jan 13, 2026
Merged

[Feat] PlaceDetailActivity Compose 마이그레이션#25
oungsi2000 merged 18 commits intodevelopfrom
feat/23

Conversation

@oungsi2000
Copy link
Contributor

@oungsi2000 oungsi2000 commented Jan 7, 2026

#️⃣ 이슈 번호

#23


🛠️ 작업 내용

  • PlaceDetailActivity를 Compose로 마이그레이션 하였습니다.

🙇🏻 중점 리뷰 요청

  • ImageDialog 내부에 HorizontalPager가 필요하여 FestabookImage와는 별도의 독립적인 레이아웃을 구축했습니다.

📸 이미지 첨부 (Optional)

mobizen_20260108_004750.mp4

Summary by CodeRabbit

Release Notes

  • New Features
    • Redesigned place detail screen with an interactive image gallery featuring swipeable images and a fullscreen image viewer dialog.
    • Enhanced URL detection and click handling in place descriptions.
    • Improved visual organization of place information including category, title, time, location, and host details with expandable descriptions.

✏️ Tip: You can customize this high-level summary in your review settings.

장소의 상세 정보를 표시하는 화면을 구현하고, 이미지 슬라이더 및 확대/축소 기능을 적용했습니다. 또한, 긴 설명 텍스트를 펼쳐볼 수 있도록 기존 텍스트 컴포넌트를 개선했습니다.

- **`PlaceDetailScreen.kt` 추가:**
    - 장소 이미지, 제목, 운영 시간, 위치, 호스트 정보를 표시하는 상세 화면 UI를 구현했습니다.
    - `Landscapist` 라이브러리를 활용하여 이미지 페이징 및 핀치 줌(Zoomable) 기능이 포함된 이미지 상세 보기 다이얼로그를 구성했습니다.
    - 설명 텍스트 클릭 시 내용이 확장되거나 축소되는 애니메이션 인터랙션을 추가했습니다.

- **`URLText.kt` 수정:**
    - 텍스트 영역 클릭 이벤트를 처리할 수 있도록 `onClick` 파라미터와 탭 제스처 감지 로직을 추가했습니다.

- **빌드 설정 수정:**
    - 이미지 로딩 및 줌 기능을 위해 `landscapist-coil3`, `landscapist-zoomable` 라이브러리 의존성을 추가했습니다.
`URLText` 컴포넌트에서 URL 클릭 시 부모의 클릭 이벤트가 함께 발생하는 문제를 해결하고, URL 탐지 정규식을 변경했습니다.

- **`URLText.kt` 수정:**
    - Android의 `Patterns.WEB_URL` 의존성을 제거하고, 한글 등을 포함할 수 있는 커스텀 정규식(`WEB_REGEX`)으로 교체하여 Kotlin Regex API를 사용하도록 변경했습니다.
    - `detectTapGestures` 내부 로직을 수정하여, URL 영역을 클릭했을 때는 `uriHandler.openUri`만 실행되고, 그 외 영역을 클릭했을 때만 `onClick` 콜백이 호출되도록 개선했습니다.
기존 XML 및 DataBinding 기반의 장소 상세 화면을 Jetpack Compose로 마이그레이션하고, 상태 관리 로직을 개선했습니다.

- **`PlaceDetailActivity.kt` 리팩토링:**
    - `AppCompatActivity`를 `ComponentActivity`로 변경하고, `setContent` 블록 내에서 `PlaceDetailScreen`을 호출하도록 수정했습니다.
    - 기존의 `ActivityPlaceDetailBinding`, `ViewPager2`, `Adapter` 등 View 시스템 관련 코드를 모두 제거했습니다.

- **`PlaceDetailViewModel.kt` 수정:**
    - `LiveData`로 관리되던 `placeDetail` 상태를 `StateFlow`로 변경하여 데이터 스트림을 개선했습니다.
    - 장소 상세 정보의 이미지가 비어있을 경우, 기본 `ImageUiModel`을 리스트에 포함하도록 로직을 추가했습니다.

- **`PlaceDetailScreen.kt` 수정:**
    - 컴포저블의 파라미터를 `PlaceDetailUiModel`에서 `PlaceDetailUiState`로 변경했습니다.
    - `UiState`의 상태(`Success`, `Loading`, `Error`)에 따라 `PlaceDetailContent`, `LoadingStateScreen`, `EmptyStateScreen`을 분기하여 렌더링하도록 구조를 변경했습니다.
`PlaceDetailViewModel` 테스트 코드에서 `getOrAwaitValue` 유틸리티 함수 대신 `value` 프로퍼티를 직접 참조하는 방식으로 검증 로직을 단순화했습니다.

- **`PlaceDetailViewModelTest.kt` 수정:**
    - 불필요해진 `getOrAwaitValue` import 구문을 제거했습니다.
    - `placeDetail`의 상태를 검증하는 모든 테스트 케이스에서 `getOrAwaitValue()` 호출을 `value` 프로퍼티 접근으로 대체했습니다.
장소 상세 화면의 이미지 로딩 로직을 공통 컴포넌트(`FestabookImage`)로 교체하여 코드를 간소화하고, 더 이상 사용되지 않는 View 기반 어댑터 코드를 정리했습니다.

- **`PlaceDetailScreen.kt` 수정:**
    - `Landscapist` 라이브러리의 `CoilImage`를 직접 호출하던 코드를 제거하고, `FestabookImage` 컴포넌트로 대체했습니다.
    - 이미지 상세 보기 다이얼로그에 `isZoomable = true` 옵션을 적용하여 줌 기능을 유지했습니다.

- **미사용 코드 삭제:**
    - Compose 마이그레이션 후 불필요해진 `PlaceImageViewPagerAdapter.kt` 및 `PlaceImageViewPagerViewHolder.kt` 파일을 삭제했습니다.
@oungsi2000 oungsi2000 self-assigned this Jan 7, 2026
@oungsi2000 oungsi2000 added the Feat label Jan 7, 2026
@coderabbitai
Copy link

coderabbitai bot commented Jan 7, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

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

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

📝 Walkthrough

Walkthrough

This PR migrates the PlaceDetail feature from traditional View-based architecture with DataBinding to Jetpack Compose. The changes include adopting StateFlow over LiveData for state management, removing legacy ViewPager adapter components, and introducing a new Compose-based UI with enhanced URL handling capabilities.

Changes

Cohort / File(s) Summary
URL Component Enhancement
app/src/main/java/com/daedan/festabook/presentation/common/component/URLText.kt
Added public onClick callback parameter; replaced Java Patterns.WEB_URL with custom WEB_REGEX for URL detection; adjusted annotation handling to use regex results with fallback to onClick when URL not tapped
ViewModel State Management Migration
app/src/main/java/com/daedan/festabook/presentation/placeDetail/PlaceDetailViewModel.kt
Migrated from MutableLiveData/LiveData to MutableStateFlow/StateFlow; added default image injection logic to UI models; updated state emission flow for Success state
Activity & Compose Integration
app/src/main/java/com/daedan/festabook/presentation/placeDetail/PlaceDetailActivity.kt
Switched from AppCompatActivity to ComponentActivity; removed DataBinding and traditional binding lifecycle; replaced with Compose setContent and collectAsStateWithLifecycle; removed legacy event listeners, adapters, and UI binding methods; removed DEFAULT_MAX_LINES constant
Removed View Components
app/src/main/java/com/daedan/festabook/presentation/placeDetail/adapter/PlaceImageViewPagerAdapter.kt,
app/src/main/java/com/daedan/festabook/presentation/placeDetail/adapter/PlaceImageViewPagerViewHolder.kt
Deleted ListAdapter-based image adapter and corresponding ViewHolder with photo dialog builder and binding logic (replaced by Compose implementation)
New Compose UI Screen
app/src/main/java/com/daedan/festabook/presentation/placeDetail/component/PlaceDetailScreen.kt
Introduced comprehensive PlaceDetailScreen composable with image gallery, dialog-based image viewer, pager indicator, detailed content display, and expandable description using URLText; includes helper components: PlaceDetailImageDialog, PlaceDetailImageContent, PlaceDetailContent, BackToPreviousButton, PagerIndicator
Test Updates
app/src/test/java/com/daedan/festabook/placeDetail/PlaceDetailViewModelTest.kt
Updated test cases to access StateFlow values directly via .value instead of getOrAwaitValue()

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • parkjiminnnn

Poem

🐰 A place detail springs to life,
From Views to Compose, no more strife,
State flows like water, StateFlow so bright,
URLs tagged with click delight,
Legacy adapters vanish in sight!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main objective: migrating PlaceDetailActivity to Jetpack Compose.

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


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.

@oungsi2000
Copy link
Contributor Author

@CodeRabbit review

@coderabbitai
Copy link

coderabbitai bot commented Jan 7, 2026

✅ Actions performed

Review triggered.

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

Copy link

@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

🤖 Fix all issues with AI agents
In
@app/src/main/java/com/daedan/festabook/presentation/placeDetail/component/PlaceDetailScreen.kt:
- Around line 369-378: The formattedDate function currently returns "null ~ ..."
when one parameter is null; update formattedDate(startTime: String?, endTime:
String?) to explicitly handle partial nulls: if both null return
stringResource(R.string.place_detail_default_time), if only startTime is null
return endTime, if only endTime is null return startTime, otherwise return
"$startTime ~ $endTime" (or use list join only when both non-null); ensure you
reference the formattedDate function and its startTime/endTime parameters and
keep the stringResource call for the default case.
- Around line 84-91: Replace Modifier.scrollable(...) on the Column with
Modifier.verticalScroll(scrollState) so the scroll offset is applied to the
content (locate the Column that currently uses modifier.scrollable(state =
scrollState, orientation = Orientation.Vertical) and swap to
Modifier.verticalScroll(scrollState)); also remove the now-unused import related
to Orientation/scrollable if it becomes unused.
- Around line 197-211: You’re invoking onPageUpdate(page) directly in the
HorizontalPager content which runs during composition (a side effect); remove
that call from the pager lambda and instead observe pagerState.currentPage in a
side-effect such as LaunchedEffect with snapshotFlow: create a LaunchedEffect
tied to pagerState (or pagerState) that uses snapshotFlow {
pagerState.currentPage }.distinctUntilChanged().collect { onPageUpdate(it) } so
onPageUpdate is only called when the page actually changes.
- Around line 331-339: The Image used as a back button in PlaceDetailScreen (the
Image composable with painterResource(R.drawable.btn_back_to_previous) and
onClick) currently has contentDescription = null; replace this with a meaningful
accessible description (e.g., use a string resource like R.string.back_button or
a localized string such as "Back") so screen readers announce its purpose;
update the Image's contentDescription to reference that string resource (or
accept a parameter passed into the composable) to ensure accessibility without
changing the click behavior.
🧹 Nitpick comments (5)
app/src/main/java/com/daedan/festabook/presentation/common/component/URLText.kt (1)

112-113: Consider matching URLs without explicit protocol prefix.

The current regex requires an explicit protocol (https?|ftp|file), which means common patterns like www.example.com won't be detected as URLs. If users paste or type URLs without the protocol prefix, they won't be clickable.

♻️ Optional: Extended regex to catch www. prefixed URLs
 private val WEB_REGEX =
-    """(https?|ftp|file)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;\uAC00-\uD7AF]*[-a-zA-Z0-9+&@#/%=~_|\uAC00-\uD7AF]""".toRegex()
+    """((https?|ftp|file)://|www\.)[-a-zA-Z0-9+&@#/%?=~_|!:,.;\uAC00-\uD7AF]*[-a-zA-Z0-9+&@#/%=~_|\uAC00-\uD7AF]""".toRegex()

Note: If extending to www. prefix, you'd also need to prepend https:// before passing to uriHandler.openUri() for URLs that don't have a protocol.

app/src/test/java/com/daedan/festabook/placeDetail/PlaceDetailViewModelTest.kt (1)

30-31: Consider removing InstantTaskExecutorRule if LiveData is fully migrated.

InstantTaskExecutorRule is specifically for testing LiveData synchronously. With the migration to StateFlow, this rule may no longer be necessary. If no other tests in this class or the ViewModel use LiveData, consider removing it to clean up the test setup.

app/src/main/java/com/daedan/festabook/presentation/placeDetail/PlaceDetailViewModel.kt (1)

42-48: Consider extracting default image logic to reduce duplication.

The same pattern for injecting a default ImageUiModel when images are empty appears in both the init block and loadPlaceDetail. This could be extracted into a helper function.

♻️ Optional: Extract helper for consistent image list handling
private fun PlaceDetailUiModel.withDefaultImageIfEmpty(): PlaceDetailUiModel =
    if (images.isEmpty()) copy(images = listOf(ImageUiModel())) else this

Then use it:

 init {
     if (receivedPlaceDetail != null) {
-        val placeDetailUiModel =
-            if (receivedPlaceDetail.images.isEmpty()) {
-                receivedPlaceDetail.copy(images = listOf(ImageUiModel()))
-            } else {
-                receivedPlaceDetail
-            }
-        _placeDetail.value = PlaceDetailUiState.Success(placeDetailUiModel)
+        _placeDetail.value = PlaceDetailUiState.Success(
+            receivedPlaceDetail.withDefaultImageIfEmpty()
+        )
     } else if (place != null) {
         loadPlaceDetail(place.id)
     }
 }

Also applies to: 59-66

app/src/main/java/com/daedan/festabook/presentation/placeDetail/PlaceDetailActivity.kt (2)

46-53: Consider wrapping content with FestabookTheme.

With enableEdgeToEdge(), you may need to ensure the Compose content is wrapped with your app's theme and handles system bar insets properly. The PlaceDetailScreen should be wrapped with FestabookTheme for consistent styling.

♻️ Suggested theme wrapper
         enableEdgeToEdge()
         setContent {
+            FestabookTheme {
                 val placeDetailUiState by viewModel.placeDetail.collectAsStateWithLifecycle()
                 PlaceDetailScreen(
                     uiState = placeDetailUiState,
                     onBackToPreviousClick = { finish() },
                 )
+            }
         }

Additionally, ensure PlaceDetailScreen handles window insets for edge-to-edge display (e.g., using Modifier.systemBarsPadding() or Scaffold with proper insets).


55-55: Debug log may not capture meaningful state.

This log statement executes immediately after setContent, at which point placeDetail.value is likely still PlaceDetailUiState.Loading. Consider moving this to a LaunchedEffect or removing it if it's only for debugging.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a417c29 and 87da1c7.

📒 Files selected for processing (7)
  • app/src/main/java/com/daedan/festabook/presentation/common/component/URLText.kt
  • app/src/main/java/com/daedan/festabook/presentation/placeDetail/PlaceDetailActivity.kt
  • app/src/main/java/com/daedan/festabook/presentation/placeDetail/PlaceDetailViewModel.kt
  • app/src/main/java/com/daedan/festabook/presentation/placeDetail/adapter/PlaceImageViewPagerAdapter.kt
  • app/src/main/java/com/daedan/festabook/presentation/placeDetail/adapter/PlaceImageViewPagerViewHolder.kt
  • app/src/main/java/com/daedan/festabook/presentation/placeDetail/component/PlaceDetailScreen.kt
  • app/src/test/java/com/daedan/festabook/placeDetail/PlaceDetailViewModelTest.kt
💤 Files with no reviewable changes (2)
  • app/src/main/java/com/daedan/festabook/presentation/placeDetail/adapter/PlaceImageViewPagerViewHolder.kt
  • app/src/main/java/com/daedan/festabook/presentation/placeDetail/adapter/PlaceImageViewPagerAdapter.kt
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Run-PR-Test
🔇 Additional comments (5)
app/src/main/java/com/daedan/festabook/presentation/common/component/URLText.kt (1)

33-33: LGTM on the onClick fallback pattern.

The addition of onClick parameter with a sensible default allows parent components to handle taps on non-URL areas, fixing the issue where parent click handlers couldn't be invoked when tapping on text. The fallback via ?: onClick() at line 86 correctly differentiates between URL taps and regular text taps.

Also applies to: 86-86

app/src/test/java/com/daedan/festabook/placeDetail/PlaceDetailViewModelTest.kt (1)

68-70: LGTM on StateFlow test pattern.

Direct .value access after advanceUntilIdle() is the correct pattern for testing StateFlow with StandardTestDispatcher. The synchronous access is safe since all coroutines have completed.

Also applies to: 88-90, 106-109, 129-132

app/src/main/java/com/daedan/festabook/presentation/placeDetail/PlaceDetailViewModel.kt (1)

34-38: LGTM on StateFlow migration.

The migration from MutableLiveData to MutableStateFlow with PlaceDetailUiState.Loading as the initial value is correct. StateFlow's non-null guarantee aligns well with the sealed class state pattern.

app/src/main/java/com/daedan/festabook/presentation/placeDetail/component/PlaceDetailScreen.kt (1)

147-147: Verify if beyondViewportPageCount = 5 is necessary.

Preloading 5 pages beyond the viewport could increase memory usage, especially with zoomable images. The default value is typically sufficient unless there's a specific UX requirement for pre-caching multiple pages.

Also applies to: 201-201

app/src/main/java/com/daedan/festabook/presentation/placeDetail/PlaceDetailActivity.kt (1)

47-53: LGTM on Compose migration.

The migration to ComponentActivity with setContent and collectAsStateWithLifecycle follows best practices for lifecycle-aware state collection in Compose. The callback to finish() for onBackToPreviousClick is appropriate.

Comment on lines 331 to 339
Image(
modifier =
modifier
.padding(top = festabookSpacing.paddingBody4)
.size(30.dp)
.clickable { onClick() },
painter = painterResource(id = R.drawable.btn_back_to_previous),
contentDescription = null,
)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add accessibility content description for back button.

contentDescription = null prevents screen readers from announcing this interactive element. Users relying on accessibility services won't understand the button's purpose.

♿ Suggested fix
     Image(
         modifier =
             modifier
                 .padding(top = festabookSpacing.paddingBody4)
                 .size(30.dp)
                 .clickable { onClick() },
         painter = painterResource(id = R.drawable.btn_back_to_previous),
-        contentDescription = null,
+        contentDescription = stringResource(R.string.content_description_back_button),
     )

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In
@app/src/main/java/com/daedan/festabook/presentation/placeDetail/component/PlaceDetailScreen.kt
around lines 331 - 339, The Image used as a back button in PlaceDetailScreen
(the Image composable with painterResource(R.drawable.btn_back_to_previous) and
onClick) currently has contentDescription = null; replace this with a meaningful
accessible description (e.g., use a string resource like R.string.back_button or
a localized string such as "Back") so screen readers announce its purpose;
update the Image's contentDescription to reference that string resource (or
accept a parameter passed into the composable) to ensure accessibility without
changing the click behavior.

Comment on lines 369 to 378
@Composable
private fun formattedDate(
startTime: String?,
endTime: String?,
): String =
if (startTime == null && endTime == null) {
stringResource(R.string.place_detail_default_time)
} else {
listOf(startTime, endTime).joinToString(" ~ ")
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Handle partial null values in date formatting.

When only one of startTime or endTime is null, joinToString will produce strings like "null ~ 18:00" or "09:00 ~ null", which is not user-friendly.

💡 Suggested fix
 @Composable
 private fun formattedDate(
     startTime: String?,
     endTime: String?,
 ): String =
-    if (startTime == null && endTime == null) {
+    when {
+        startTime == null && endTime == null -> 
             stringResource(R.string.place_detail_default_time)
-    } else {
-        listOf(startTime, endTime).joinToString(" ~ ")
+        startTime != null && endTime != null -> 
+            "$startTime ~ $endTime"
+        startTime != null -> startTime
+        else -> endTime!!
     }
📝 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
@Composable
private fun formattedDate(
startTime: String?,
endTime: String?,
): String =
if (startTime == null && endTime == null) {
stringResource(R.string.place_detail_default_time)
} else {
listOf(startTime, endTime).joinToString(" ~ ")
}
@Composable
private fun formattedDate(
startTime: String?,
endTime: String?,
): String =
when {
startTime == null && endTime == null ->
stringResource(R.string.place_detail_default_time)
startTime != null && endTime != null ->
"$startTime ~ $endTime"
startTime != null -> startTime
else -> endTime!!
}
🤖 Prompt for AI Agents
In
@app/src/main/java/com/daedan/festabook/presentation/placeDetail/component/PlaceDetailScreen.kt
around lines 369 - 378, The formattedDate function currently returns "null ~
..." when one parameter is null; update formattedDate(startTime: String?,
endTime: String?) to explicitly handle partial nulls: if both null return
stringResource(R.string.place_detail_default_time), if only startTime is null
return endTime, if only endTime is null return startTime, otherwise return
"$startTime ~ $endTime" (or use list join only when both non-null); ensure you
reference the formattedDate function and its startTime/endTime parameters and
keep the stringResource call for the default case.

장소 상세 화면의 스크롤이 올바르게 동작하도록 수정자를 변경하고, 프리뷰용 더미 텍스트를 일부 수정했습니다.

- **`PlaceDetailScreen.kt` 수정:**
    - `Column`에 사용된 `scrollable` 수정자를 `verticalScroll`로 변경하여 세로 스크롤이 정상적으로 작동하도록 개선했습니다.
    - 프리뷰에 사용되는 `uiState`의 설명(`description`) 텍스트 내 URL 오타(`htt` -> `http`) 및 띄어쓰기를 수정했습니다.
Copy link
Contributor

@etama123 etama123 left a comment

Choose a reason for hiding this comment

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

몇 가지 수정하면 좋을 부분에 리뷰를 달았어요!
고생하셨습니다~ 밀러

Comment on lines +291 to +319
URLText(
modifier =
Modifier
.animateContentSize(
animationSpec =
spring(
dampingRatio = Spring.DampingRatioLowBouncy,
stiffness = Spring.StiffnessMedium,
),
).padding(
top = festabookSpacing.paddingBody3,
),
onClick = {
isDescriptionExpand = !isDescriptionExpand
},
text =
placeDetail.place.description
?: stringResource(R.string.place_list_default_description),
style = FestabookTypography.bodySmall,
maxLines =
if (isDescriptionExpand) {
Int.MAX_VALUE
} else {
1
},
overflow = TextOverflow.Ellipsis,
)
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

설명 부분의 기본값이 펼쳐져 있는 상태로 변경되지 않았나요?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

엇..release 앱 확인해보니, 기본값은 닫힌 상태였습니다 !

Copy link
Contributor

Choose a reason for hiding this comment

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

전에 구두로 논의만 하고 적용하지 않고 넘어가버렸군요😥
혹시 지금이라도 펼쳐진 형태로 변경하는 건 어떨까요?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

반영했습니다 !

Comment on lines 239 to 253
Row(
modifier = Modifier.padding(top = festabookSpacing.paddingBody4),
) {
Icon(
painter = painterResource(R.drawable.ic_place_detail_clock),
contentDescription = stringResource(R.string.content_description_iv_clock),
)

Text(
modifier = Modifier.padding(start = festabookSpacing.paddingBody1),
text = formattedDate(placeDetail.startTime, placeDetail.endTime),
style = FestabookTypography.bodySmall,
color = FestabookColor.gray500,
)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

반복되는 부분을 공통 컴포넌트로 분리하면 어떨까요?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

각각의 Icon 컴포저블과 modifier가 달라 분리를 해도 비슷한 것 같습니다. 다만 저 반복되는 부분을 하나로 묶어 보았습니다 !
refactor(PlaceDetail): 장소 상세 정보 UI 컴포넌트 분리

Copy link
Contributor

@etama123 etama123 Jan 12, 2026

Choose a reason for hiding this comment

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

음 이미지 아이콘과 modifier를 파라미터로 정하면, 각 아이템을 컴포저블로 바꿀 수 있지 않을까요? 아래의 형태처럼요!

데이터가 제각각이라 분리해도 비슷해 보일 수 있다는 점 공감합니다! 다만, 공통 컴포저블로 추출하면 PlaceDetailInfo는 로우 레벨의 UI 구현(Row, Icon, Text 조합)보다는 어떤 데이터를 보여줄 것인지에만 집중할 수 있고, 구조를 더 한 눈에 확인할 수 있어 전체적인 가독성이 훨씬 좋아질 것 같아요.

private fun PlaceDetailInfoItem(
    @DrawableRes iconResId: Int,
    contentDescription: String,
    text: String,
    modifier: Modifier = Modifier,
)

Copy link
Contributor Author

@oungsi2000 oungsi2000 Jan 13, 2026

Choose a reason for hiding this comment

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


when (uiState) {
is PlaceDetailUiState.Success -> {
PlaceDetailImageDialog(
Copy link
Contributor

Choose a reason for hiding this comment

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

해당 화면이 시스템 표시줄과 꽤 많은 영역이 겹쳐, 사진이 가려보여요!
툴바처럼 상단에 여백을 주는 것은 어떨까요?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

흠...혹시 사진이 가려보인다는 부분을 더 자세히 공유해주실 수 있으실까요? 저는 잘 체감이 안되서요 !

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Comment on lines 322 to 333
private fun BackToPreviousButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Image(
modifier =
modifier
.padding(top = festabookSpacing.paddingBody4)
.size(30.dp)
.clickable { onClick() },
painter = painterResource(id = R.drawable.btn_back_to_previous),
contentDescription = null,
Copy link
Contributor

Choose a reason for hiding this comment

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

버튼을 눌렀을 때가 아닌, 버튼 바깥의 여백부분을 눌렀을 때만 동작하네요. 확인 부탁드립니다!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

어라..? 이것도 제 핸드폰에서는 정상 작동하는데 혹시 공유해주실 수 있으실까요?

Copy link
Contributor

@parkjiminnnn parkjiminnnn left a comment

Choose a reason for hiding this comment

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

고생했어요 밀러~~

Comment on lines 20 to 21

class PlaceDetailViewModel @AssistedInject constructor(
Copy link
Contributor

Choose a reason for hiding this comment

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

AssistedInject를 클래스 단위로 옮겨도 좋을 것 같네용

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Comment on lines 42 to 49
val placeDetailUiModel =
if (receivedPlaceDetail.images.isEmpty()) {
receivedPlaceDetail.copy(images = listOf(ImageUiModel()))
} else {
receivedPlaceDetail
}
_placeDetail.value = PlaceDetailUiState.Success(placeDetailUiModel)
} else if (place != null) {
Copy link
Contributor

Choose a reason for hiding this comment

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

init {
       receivedPlaceDetail?.let {
           val placeDetailUiModel =
               if (it.images.isEmpty()) it.copy(images = listOf(ImageUiModel())) else it
           _placeDetail.value = PlaceDetailUiState.Success(placeDetailUiModel)
       }
       place?.let { loadPlaceDetail(it.id) }
   }

if문 분기처리가 많아지면 가독성이 떨어진다고 생각해서 이렇게 리팩토링을 해봤는데 어떠신가요?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

오 확실히 이전보다 가독성이 좋아진 것 같아요 ! 반영했습니다 !
refactor(PlaceDetail): PlaceDetailViewModel 초기화 로직 리팩토링

Comment on lines 71 to 72
var isDialogOpen by remember { mutableStateOf(false) }
var currentPage by remember { mutableIntStateOf(0) }
Copy link
Contributor

Choose a reason for hiding this comment

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

상태를 외부에서 들고 있게 하는건 어떠신가요?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

구조의 변경으로 currentPage는 사라졌습니다.
외부에서 PlaceDetailScreen을 호출한다고 했을 때,
외부에서 해당 상태들을 알면 오히려 불필요한 정보까지 알게 된다고 생각했습니다. (추상화의 이점 상실)

Comment on lines 371 to 373
} else {
listOf(startTime, endTime).joinToString(" ~ ")
}
Copy link
Contributor

Choose a reason for hiding this comment

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

string.xml format_date가 있어요!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Comment on lines 332 to 333
painter = painterResource(id = R.drawable.btn_back_to_previous),
contentDescription = null,
Copy link
Contributor

Choose a reason for hiding this comment

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

기존에도 contentDescription이 null이었을까용?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

추가했습니다 !
d242361

Copy link
Contributor

Choose a reason for hiding this comment

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

settingFragment에도 달았던 내용인데 instantTaskExecutorRule 없어도 될 것 같아용

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Comment on lines 129 to 130
.value
.let { it as PlaceDetailUiState.Success }
Copy link
Contributor

Choose a reason for hiding this comment

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

val actual =
              placeDetailViewModel.placeDetail
                  .value
                  .let {
                      (it as? PlaceDetailUiState.Success)
                          ?: fail { "PlaceDetailUiState 가 성공 상태가 아님" }
                  }.placeDetail
                  .notices

fail 함수를 사용하면 캐스팅 오류가 발생 했을 때 에러 로그를 확인하기 더 수월할 듯 한데 어떠신가요?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

오 이런 좋은 함수가 있었군요 ! 반영했습니다!
test(PlaceDetail): PlaceDetailViewModelTest 검증 로직 안정성 강화

장소 상세 정보(위치, 운영 시간 등)를 표시하는 UI 로직을 `PlaceDetailInfo` 컴포저블로 분리하여 코드 구조를 개선했습니다.

- **`PlaceDetailScreen.kt` 수정:**
    - 기존 `PlaceDetailContent` 내부에 있던 장소 정보(주소, 시간, 전화번호) 관련 `Row` 및 `Text` 컴포저블들을 `PlaceDetailInfo`라는 새로운 컴포저블 함수로 추출했습니다.
    - `PlaceDetailContent`는 `PlaceDetailInfo`를 호출하도록 변경하여 코드의 가독성과 재사용성을 높였습니다.
장소 상세 화면의 이미지 페이저(`Pager`)와 다이얼로그 페이저 간의 페이지 상태를 동기화하고, 상태 관리 로직을 개선했습니다.

- **`PlaceDetailScreen.kt` 수정:**
    - 이미지 페이저의 `PagerState`를 `PlaceDetailScreen`의 `Success` 상태 분기 내에서 생성하도록 변경하여, 다이얼로그와 메인 화면 페이저가 동일한 상태를 공유하도록 수정했습니다.
    - `PlaceDetailImageDialog`는 외부에서 생성된 `pagerState`를 받도록 변경하여, 다이얼로그가 열릴 때 현재 페이지를 정확히 표시하도록 개선했습니다.
    - 메인 화면 페이저(`PlaceDetailImageContent`)의 페이지 변경 시 `scrollToPage`를 호출하도록 수정하고, 페이지가 완전히 변경되었을 때(`settledPage`)만 `onPageUpdate` 콜백이 호출되도록 `LaunchedEffect`를 추가했습니다. 이로써 불필요한 재구성을 줄였습니다.
`PlaceDetailViewModel`의 `@AssistedInject` 어노테이션 위치를 생성자에서 클래스 선언부로 이동하여 코드 스타일을 일관성 있게 수정했습니다.
`PlaceDetailViewModel`의 `init` 블록에서 `if` 조건문을 `let` 스코프 함수로 변경하여 코드를 간소화하고 가독성을 개선했습니다.

- **`PlaceDetailViewModel.kt` 수정:**
    - `receivedPlaceDetail` 및 `place` 객체의 null 체크 로직을 `?.let` 블록으로 대체했습니다.
    - `let` 블록 내에서 삼항 연산자를 사용하여 이미지가 비어있을 경우 기본 이미지 모델을 추가하는 로직을 간결하게 수정했습니다.
장소 상세 화면(`PlaceDetailScreen`)의 이미지 페이저 상태 관리 로직을 개선하고, 불필요한 코드를 정리했습니다.

- **`PlaceDetailScreen.kt` 수정:**
    - 이미지 상세 보기 다이얼로그(`ImageDetailDialog`)가 열릴 때, 페이저의 현재 페이지를 유지하기 위해 사용하던 `currentPage` 상태 변수를 제거했습니다. `rememberPagerState`는 상태를 자동으로 유지하므로 불필요한 로직이었습니다.
    - 장소 운영 시간 표시를 위해 하드코딩된 문자열 `" ~ "` 대신, 형식 문자열 리소스(`R.string.format_date`)를 사용하도록 변경하여 코드의 일관성을 높였습니다.
장소 상세 화면의 뒤로가기 버튼에 콘텐츠 설명(`contentDescription`)을 추가하여 접근성을 개선했습니다.

- **`PlaceDetailScreen.kt` 수정:**
    - 뒤로가기 버튼(`Image`)의 `contentDescription`에 `stringResource`를 사용하여 "상세화면 나가기 아이콘"이라는 설명을 추가했습니다.

- **`strings.xml` 추가:**
    - `content_description_exit_place_detail` 문자열 리소스를 새로 추가했습니다.
장소 상세 화면의 UI 코드를 정리하여 스타일 일관성을 개선했습니다.

- **`PlaceDetailScreen.kt` 수정:**
    - 뒤로가기 버튼(`IcBackToPrevious`)의 `padding` Modifier 적용 순서를 변경하여 Z-Index가 올바르게 작동하도록 수정했습니다.
    - `IcBackToPrevious` 컴포저블 내부에 적용되었던 불필요한 `padding(top)` 속성을 제거했습니다.
`PlaceDetailViewModelTest` 테스트 코드에서 더 이상 필요하지 않은 `InstantTaskExecutorRule` 관련 코드를 삭제했습니다.

- **`PlaceDetailViewModelTest.kt` 수정:**
    - 불필요해진 `InstantTaskExecutorRule` import 구문을 제거했습니다.
    - `@get:Rule` 어노테이션과 함께 선언된 `instantTaskExecutorRule` 프로퍼티를 삭제했습니다.
`PlaceDetailViewModel`의 단위 테스트에서 UI 상태를 검증하는 로직을 더 안전하게 수정했습니다.

- **`PlaceDetailViewModelTest.kt` 수정:**
    - `PlaceDetailUiState.Success` 상태를 검증할 때, `as`를 사용한 강제 타입 캐스팅 대신 `as?`와 `fail` 함수를 조합하여 사용하도록 변경했습니다. 이를 통해 테스트가 예기치 않은 상태(예: `Error`)로 인해 실패할 경우, 더 명확한 실패 메시지를 제공하도록 개선했습니다.
Copy link
Contributor

@parkjiminnnn parkjiminnnn left a comment

Choose a reason for hiding this comment

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

리뷰 반영해주셔서 고마워요~

Copy link
Contributor

@etama123 etama123 left a comment

Choose a reason for hiding this comment

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

리뷰 반영 고생하셨습니다. 추가로 약간의 의견을 적어두었으니 확인해주시고 다시 리뷰 요청바랍니다🙂

장소 상세 화면의 정보 표시 UI를 개선하고 사용자 경험을 위해 상세 설명 섹션의 초기 상태를 변경했습니다.

- **`PlaceDetailScreen.kt` 수정:**
    - 시간, 위치, 주최자 정보를 표시하는 중복된 `Row`와 `Icon`, `Text` 컴포저블을 `PlaceDetailInfoItem`이라는 새로운 컴포저블로 추출하여 코드 중복을 제거하고 재사용성을 높였습니다.
    - 장소 상세 설명(`PlaceDetailDescriptionSection`)이 기본적으로 펼쳐진 상태로 보이도록 `isDescriptionExpand`의 초기값을 `true`로 변경했습니다.
장소 상세 화면의 뒤로가기 버튼이 상태 표시줄(Status Bar) 영역과 겹쳐 보이는 문제를 해결하고, 전체 화면(Edge-to-Edge) UI의 스타일을 명확하게 설정했습니다.

- **`PlaceDetailActivity.kt` 수정:**
    - `enableEdgeToEdge` 호출 시 `SystemBarStyle.light`를 적용하여, 스크롤 시 상태 표시줄의 배경(scrim) 색상을 흰색으로 지정했습니다.
    - 기존에 `setContent` 이전에 호출되던 `enableEdgeToEdge`를 `setContent` 내부로 이동시켜 UI 상태와 함께 관리되도록 수정했습니다.

- **`PlaceDetailScreen.kt` 수정:**
    - 뒤로가기 버튼(`BackToPreviousButton`)에 `windowInsetsPadding(WindowInsets.statusBars)` Modifier를 추가하여, 상태 표시줄 높이만큼의 패딩을 동적으로 적용했습니다. 이로 인해 버튼이 상태 표시줄 아래에 올바르게 배치됩니다.
URL을 감지하는 정규식(`WEB_REGEX`)의 정확도를 높였습니다. URL 끝에 포함될 수 없는 마침표(.), 쉼표(,), 콜론(:), 세미콜론(;)과 같은 문자가 URL의 일부로 잘못 인식되는 문제를 해결했습니다.

- **`URLText.kt` 수정:**
    - 기존 정규식의 마지막에 부정형 후방탐색 `(?<![.,:;])`을 추가했습니다.
    - 이를 통해 URL이 해당 문장 부호로 끝나는 경우, 이를 URL에서 제외하여 링크의 정확성을 개선했습니다.
@oungsi2000
Copy link
Contributor Author

oungsi2000 commented Jan 13, 2026

상태바랑 겹치는 버그 수정했습니다

@oungsi2000 oungsi2000 requested a review from etama123 January 13, 2026 03:04
@oungsi2000 oungsi2000 merged commit 9e8cc52 into develop Jan 13, 2026
6 checks passed
@oungsi2000 oungsi2000 deleted the feat/23 branch January 13, 2026 12:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants