From 0ddde4401c94b4d86aae1e1153ca6f2643a94632 Mon Sep 17 00:00:00 2001 From: oungsi2000 Date: Wed, 17 Dec 2025 22:05:29 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat(PlaceDetail):=20=EC=9E=A5=EC=86=8C=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20=EB=AF=B8=EB=A6=AC=EB=B3=B4=EA=B8=B0=20?= =?UTF-8?q?=EC=B9=B4=EB=93=9C=20UI=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 지도에서 특정 장소를 선택했을 때 하단에 표시되는 장소 상세 정보 미리보기 카드(`PlaceDetailPreviewScreen`)를 새로 구현했습니다. 이 카드는 선택된 장소의 핵심 정보를 요약하여 보여줍니다. - **`PlaceDetailPreviewScreen.kt` 신규 컴포저블 추가:** - 지도에서 선택된 장소(`SelectedPlaceUiState`)의 상세 정보를 표시하는 카드 UI를 구현했습니다. - 장소의 카테고리, 이름, 운영 시간, 위치, 주최자, 대표 이미지를 표시합니다. - 카드가 나타날 때 부드러운 애니메이션(`fadeIn`, `slideInVertically`) 효과를 적용했습니다. - 장소 상세 정보의 상태(`Loading`, `Success`, `Error`, `Empty`)에 따라 분기 처리하도록 설계했습니다. - **`URLText.kt` 신규 컴포저블 추가:** - `Text` 컴포저블을 확장하여, 문자열 내의 URL을 자동으로 감지하고 클릭 가능한 링크로 만들어주는 `URLText`를 구현했습니다. - 링크는 밑줄 스타일과 함께 지정된 색상(`gray500`)으로 표시됩니다. - **`strings.xml` 리소스 추가:** - 미리보기 카드에 사용될 아이콘(운영 시간, 주최자)의 접근성을 위한 콘텐츠 설명 문자열을 추가했습니다. --- .../presentation/common/component/URLText.kt | 112 ++++++++ .../component/PlaceDetailPreviewScreen.kt | 239 ++++++++++++++++++ app/src/main/res/values/strings.xml | 2 + 3 files changed, 353 insertions(+) create mode 100644 app/src/main/java/com/daedan/festabook/presentation/common/component/URLText.kt create mode 100644 app/src/main/java/com/daedan/festabook/presentation/placeMap/placeDetailPreview/component/PlaceDetailPreviewScreen.kt diff --git a/app/src/main/java/com/daedan/festabook/presentation/common/component/URLText.kt b/app/src/main/java/com/daedan/festabook/presentation/common/component/URLText.kt new file mode 100644 index 0000000..f57d577 --- /dev/null +++ b/app/src/main/java/com/daedan/festabook/presentation/common/component/URLText.kt @@ -0,0 +1,112 @@ +package com.daedan.festabook.presentation.common.component + +import android.util.Patterns +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.text.InlineTextContent +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.TextLayoutResult +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.TextUnit +import com.daedan.festabook.presentation.theme.FestabookColor + +@Composable +fun URLText( + text: String, + modifier: Modifier = Modifier, + color: Color = Color.Unspecified, + fontSize: TextUnit = TextUnit.Unspecified, + fontStyle: FontStyle? = null, + fontWeight: FontWeight? = null, + fontFamily: FontFamily? = null, + letterSpacing: TextUnit = TextUnit.Unspecified, + textDecoration: TextDecoration? = null, + textAlign: TextAlign? = null, + lineHeight: TextUnit = TextUnit.Unspecified, + overflow: TextOverflow = TextOverflow.Clip, + softWrap: Boolean = true, + maxLines: Int = Int.MAX_VALUE, + minLines: Int = 1, + inlineContent: Map = mapOf(), + onTextLayout: (TextLayoutResult) -> Unit = {}, + style: TextStyle = LocalTextStyle.current, +) { + val uriHandler = LocalUriHandler.current + var layoutResult by remember { mutableStateOf(null) } + val linkedText = + buildAnnotatedString { + append(text) + val urlPattern = Patterns.WEB_URL + val matcher = urlPattern.matcher(text) + while (matcher.find()) { + addStyle( + style = + SpanStyle( + color = FestabookColor.gray500, + textDecoration = TextDecoration.Underline, + ), + start = matcher.start(), + end = matcher.end(), + ) + addStringAnnotation( + tag = "URL", + annotation = matcher.group(), + start = matcher.start(), + end = matcher.end(), + ) + } + } + Text( + text = linkedText, + modifier = + modifier.pointerInput(Unit) { + detectTapGestures { + layoutResult?.let { result -> + val position = result.getOffsetForPosition(it) + linkedText + .getStringAnnotations("URL", position, position) + .firstOrNull() + ?.let { annotation -> + uriHandler.openUri(annotation.item) + } + } + } + }, + color = color, + fontSize = fontSize, + fontStyle = fontStyle, + fontWeight = fontWeight, + fontFamily = fontFamily, + letterSpacing = letterSpacing, + textDecoration = textDecoration, + textAlign = textAlign, + lineHeight = lineHeight, + overflow = overflow, + softWrap = softWrap, + maxLines = maxLines, + minLines = minLines, + inlineContent = inlineContent, + onTextLayout = { + layoutResult = it + onTextLayout(it) + }, + style = style, + ) +} diff --git a/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeDetailPreview/component/PlaceDetailPreviewScreen.kt b/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeDetailPreview/component/PlaceDetailPreviewScreen.kt new file mode 100644 index 0000000..356c85f --- /dev/null +++ b/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeDetailPreview/component/PlaceDetailPreviewScreen.kt @@ -0,0 +1,239 @@ +package com.daedan.festabook.presentation.placeMap.placeDetailPreview.component + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.MutableTransitionState +import androidx.compose.animation.fadeIn +import androidx.compose.animation.slideInVertically +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.daedan.festabook.R +import com.daedan.festabook.presentation.common.component.CoilImage +import com.daedan.festabook.presentation.common.component.URLText +import com.daedan.festabook.presentation.common.component.cardBackground +import com.daedan.festabook.presentation.common.convertImageUrl +import com.daedan.festabook.presentation.placeDetail.model.PlaceDetailUiModel +import com.daedan.festabook.presentation.placeMap.component.PlaceCategoryLabel +import com.daedan.festabook.presentation.placeMap.model.PlaceCategoryUiModel +import com.daedan.festabook.presentation.placeMap.model.PlaceUiModel +import com.daedan.festabook.presentation.placeMap.model.SelectedPlaceUiState +import com.daedan.festabook.presentation.theme.FestabookColor +import com.daedan.festabook.presentation.theme.FestabookTheme +import com.daedan.festabook.presentation.theme.FestabookTypography +import com.daedan.festabook.presentation.theme.festabookShapes +import com.daedan.festabook.presentation.theme.festabookSpacing + +@Composable +fun PlaceDetailPreviewScreen( + placeUiState: SelectedPlaceUiState, + modifier: Modifier = Modifier, + onClick: (SelectedPlaceUiState) -> Unit = {}, + onError: () -> Unit = {}, + onEmpty: () -> Unit = {}, +) { + val visibleState = + remember { + MutableTransitionState(false).apply { targetState = true } + } + val density = LocalDensity.current + + AnimatedVisibility( + visibleState = visibleState, + enter = + fadeIn( + initialAlpha = 0.3f, // 시작 투명도 0.3f + ) + + slideInVertically( + initialOffsetY = { with(density) { 120.dp.roundToPx() } }, // 120dp 아래에서 시작 + ), + modifier = modifier, + ) { + Box( + modifier = + Modifier + .cardBackground( + backgroundColor = FestabookColor.white, + borderColor = FestabookColor.gray200, + shape = festabookShapes.radius5, + ), + ) { + when (placeUiState) { + is SelectedPlaceUiState.Loading -> Unit + is SelectedPlaceUiState.Success -> { + PlaceDetailPreviewContent(placeDetail = placeUiState.value) + } + + is SelectedPlaceUiState.Error -> onError() + is SelectedPlaceUiState.Empty -> onEmpty() + } + } + } +} + +@Composable +private fun PlaceDetailPreviewContent( + placeDetail: PlaceDetailUiModel, + modifier: Modifier = Modifier, +) { + Column( + modifier = + modifier.padding( + horizontal = festabookSpacing.paddingScreenGutter, + vertical = 20.dp, + ), + ) { + PlaceCategoryLabel( + category = placeDetail.place.category, + ) + + Row(modifier = Modifier.wrapContentSize()) { + Column( + modifier = Modifier.weight(1f), + ) { + Text( + modifier = + Modifier + .padding(top = festabookSpacing.paddingBody1), + style = FestabookTypography.displaySmall, + text = + placeDetail.place.title + ?: stringResource(R.string.place_list_default_title), + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + Row( + modifier = Modifier.padding(top = festabookSpacing.paddingBody3), + ) { + 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, + ) + } + + Row( + modifier = Modifier.padding(top = festabookSpacing.paddingBody1), + ) { + Icon( + painter = painterResource(R.drawable.ic_location), + contentDescription = stringResource(R.string.content_description_iv_location), + ) + + Text( + modifier = Modifier.padding(start = festabookSpacing.paddingBody1), + text = + placeDetail.place.location + ?: stringResource(R.string.place_list_default_location), + style = FestabookTypography.bodySmall, + color = FestabookColor.gray500, + ) + } + + Row( + modifier = Modifier.padding(top = festabookSpacing.paddingBody1), + ) { + Icon( + painter = painterResource(R.drawable.ic_place_detail_host), + contentDescription = stringResource(R.string.content_description_iv_host), + ) + + Text( + modifier = Modifier.padding(start = festabookSpacing.paddingBody1), + text = + placeDetail.host + ?: stringResource(R.string.place_detail_default_host), + style = FestabookTypography.bodySmall, + color = FestabookColor.gray500, + ) + } + } + + CoilImage( + modifier = + Modifier + .size(88.dp) + .clip(festabookShapes.radius2), + url = placeDetail.place.imageUrl.convertImageUrl() ?: "", + contentDescription = stringResource(R.string.content_description_booth_image), + ) + } + + URLText( + modifier = Modifier.padding(top = festabookSpacing.paddingBody3), + text = + placeDetail.place.description + ?: stringResource(R.string.place_list_default_description), + style = FestabookTypography.bodySmall, + ) + } +} + +@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(" ~ ") + } + +@Preview +@Composable +private fun PlaceDetailPreviewScreenPreview() { + FestabookTheme { + PlaceDetailPreviewScreen( + modifier = + Modifier + .padding(festabookSpacing.paddingScreenGutter), + placeUiState = + SelectedPlaceUiState.Success( + value = FAKE_PLACE_DETAIL, + ), + ) + } +} + +private val FAKE_PLACE = + PlaceUiModel( + id = 1, + imageUrl = null, + category = PlaceCategoryUiModel.FOOD_TRUCK, + title = "테스트테스트테스트테스트테스트테스트테스트테스트테스트테스트테스트테스트테스트", + description = "https://onlyfor-me-blog.tistory.com/1190", + location = null, + isBookmarked = false, + timeTagId = listOf(1), + ) + +private val FAKE_PLACE_DETAIL = + PlaceDetailUiModel( + place = FAKE_PLACE, + notices = listOf(), + host = null, + startTime = null, + endTime = null, + images = listOf(), + ) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cca637b..905ce6d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -92,6 +92,8 @@ 고정핀 아이콘 위치 아이콘 부스 이미지 + 운영 시간 아이콘 + 호스트 아이콘 공지사항이 없습니다 새로고침 플로팅 지도 버튼 From 044a68ab6b94319f57c27c91e86f7d4825e12b02 Mon Sep 17 00:00:00 2001 From: oungsi2000 Date: Thu, 18 Dec 2025 00:35:58 +0900 Subject: [PATCH 2/4] =?UTF-8?q?refactor(PlaceDetailPreview):=20Fragment=20?= =?UTF-8?q?Compose=20=EB=A7=88=EC=9D=B4=EA=B7=B8=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 기존의 `View` 시스템 기반으로 구현되었던 장소 상세 정보 미리보기 UI(`PlaceDetailPreviewFragment`)를 Jetpack Compose 기반의 `PlaceDetailPreviewScreen`으로 전면 마이그레이션했습니다. 이를 통해 XML 레이아웃 의존성을 제거하고, 선언적 UI 방식으로 전환하여 코드의 가독성과 유지보수성을 향상시켰습니다. - **`PlaceDetailPreviewFragment.kt` 리팩토링:** - `onCreateView`에서 `ComposeView`를 반환하도록 변경하고, 내부에 `PlaceDetailPreviewScreen` 컴포저블을 설정했습니다. - 기존의 `View` 바인딩, `showBottomAnimation`, UI 업데이트 로직(`updateSelectedPlaceUi`) 등을 모두 제거했습니다. - `ViewModel`의 `LiveData`를 `StateFlow`로 변환하여 `collectAsStateWithLifecycle`로 상태를 구독하도록 수정했습니다. - 화면 클릭, 에러 처리, 비어있는 상태 처리 로직을 `PlaceDetailPreviewScreen`의 콜백으로 위임했습니다. - `onBackPressedCallback`의 활성화 상태를 `LaunchedEffect` 내에서 관리하도록 변경했습니다. - **`PlaceDetailPreviewScreen.kt` (컴포저블) 수정:** - 기존의 `AnimatedVisibility`를 제거하고, `Animatable`과 `graphicsLayer`를 사용한 커스텀 애니메이션을 구현하여 UI가 나타날 때 아래에서 위로 올라오며 서서히 나타나는 효과를 적용했습니다. - 장소 설명(`description`) 텍스트에 최대 두 줄 제한과 `Ellipsis`(줄임표)를 적용했습니다. - **`PlaceMapViewModel.kt` 수정:** - 기존의 `selectedPlace` `LiveData`를 `selectedPlaceFlow`라는 `StateFlow`로 변환하여 Compose 환경에서 상태를 효율적으로 관찰할 수 있도록 했습니다. --- .../placeMap/PlaceMapViewModel.kt | 9 ++ .../PlaceDetailPreviewFragment.kt | 136 ++++++++++-------- .../component/PlaceDetailPreviewScreen.kt | 86 ++++++----- 3 files changed, 132 insertions(+), 99 deletions(-) diff --git a/app/src/main/java/com/daedan/festabook/presentation/placeMap/PlaceMapViewModel.kt b/app/src/main/java/com/daedan/festabook/presentation/placeMap/PlaceMapViewModel.kt index 5eebe62..5cefacf 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/placeMap/PlaceMapViewModel.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/placeMap/PlaceMapViewModel.kt @@ -62,6 +62,15 @@ class PlaceMapViewModel( private val _selectedPlace: MutableLiveData = MutableLiveData() val selectedPlace: LiveData = _selectedPlace + val selectedPlaceFlow: StateFlow = + _selectedPlace + .asFlow() + .stateIn( + scope = viewModelScope, + started = SharingStarted.Lazily, + initialValue = SelectedPlaceUiState.Loading, + ) + private val _navigateToDetail = SingleLiveData() val navigateToDetail: LiveData = _navigateToDetail diff --git a/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeDetailPreview/PlaceDetailPreviewFragment.kt b/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeDetailPreview/PlaceDetailPreviewFragment.kt index 6e7e29d..cf57845 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeDetailPreview/PlaceDetailPreviewFragment.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeDetailPreview/PlaceDetailPreviewFragment.kt @@ -1,26 +1,38 @@ package com.daedan.festabook.presentation.placeMap.placeDetailPreview import android.os.Bundle +import android.view.LayoutInflater import android.view.View +import android.view.ViewGroup import androidx.activity.OnBackPressedCallback +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.daedan.festabook.R import com.daedan.festabook.databinding.FragmentPlaceDetailPreviewBinding import com.daedan.festabook.di.fragment.FragmentKey import com.daedan.festabook.logging.logger import com.daedan.festabook.presentation.common.BaseFragment import com.daedan.festabook.presentation.common.OnMenuItemReClickListener -import com.daedan.festabook.presentation.common.loadImage -import com.daedan.festabook.presentation.common.setFormatDate -import com.daedan.festabook.presentation.common.showBottomAnimation import com.daedan.festabook.presentation.common.showErrorSnackBar import com.daedan.festabook.presentation.placeDetail.PlaceDetailActivity import com.daedan.festabook.presentation.placeDetail.model.PlaceDetailUiModel import com.daedan.festabook.presentation.placeMap.PlaceMapViewModel import com.daedan.festabook.presentation.placeMap.logging.PlacePreviewClick import com.daedan.festabook.presentation.placeMap.model.SelectedPlaceUiState +import com.daedan.festabook.presentation.placeMap.placeDetailPreview.component.PlaceDetailPreviewScreen +import com.daedan.festabook.presentation.theme.FestabookTheme +import com.daedan.festabook.presentation.theme.festabookSpacing import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.ContributesIntoMap import dev.zacsweers.metro.Inject @@ -42,13 +54,70 @@ class PlaceDetailPreviewFragment( } } + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View { + return ComposeView(requireContext()).apply { + super.onCreateView(inflater, container, savedInstanceState) + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + setContent { + FestabookTheme { + val placeDetailUiState by viewModel.selectedPlaceFlow.collectAsStateWithLifecycle() + val visible = placeDetailUiState is SelectedPlaceUiState.Success + + LaunchedEffect(placeDetailUiState) { + backPressedCallback.isEnabled = true + } + + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.BottomCenter, + ) { + PlaceDetailPreviewScreen( + placeUiState = placeDetailUiState, + visible = visible, + modifier = + Modifier + .padding( + vertical = festabookSpacing.paddingBody4, + horizontal = festabookSpacing.paddingScreenGutter, + ), + onClick = { selectedPlace -> + if (selectedPlace !is SelectedPlaceUiState.Success) return@PlaceDetailPreviewScreen + startPlaceDetailActivity(selectedPlace.value) + binding.logger.log( + PlacePreviewClick( + baseLogData = binding.logger.getBaseLogData(), + placeName = + selectedPlace.value.place.title + ?: "undefined", + timeTag = + viewModel.selectedTimeTag.value?.name + ?: "undefined", + category = selectedPlace.value.place.category.name, + ), + ) + }, + onError = { selectedPlace -> + showErrorSnackBar(selectedPlace.throwable) + }, + onEmpty = { + backPressedCallback.isEnabled = false + }, + ) + } + } + } + } + } + override fun onViewCreated( view: View, savedInstanceState: Bundle?, ) { super.onViewCreated(view, savedInstanceState) - setUpObserver() - setupBinding() setUpBackPressedCallback() } @@ -63,63 +132,6 @@ class PlaceDetailPreviewFragment( ) } - private fun setupBinding() { - binding.layoutSelectedPlace.setOnClickListener { - val selectedPlaceState = viewModel.selectedPlace.value - if (selectedPlaceState is SelectedPlaceUiState.Success) { - startPlaceDetailActivity(selectedPlaceState.value) - binding.logger.log( - PlacePreviewClick( - baseLogData = binding.logger.getBaseLogData(), - placeName = selectedPlaceState.value.place.title ?: "undefined", - timeTag = viewModel.selectedTimeTag.value?.name ?: "undefined", - category = selectedPlaceState.value.place.category.name, - ), - ) - } - } - } - - private fun setUpObserver() { - viewModel.selectedPlace.observe(viewLifecycleOwner) { selectedPlace -> - backPressedCallback.isEnabled = true - binding.layoutSelectedPlace.visibility = - if (selectedPlace == SelectedPlaceUiState.Empty) View.GONE else View.VISIBLE - - when (selectedPlace) { - is SelectedPlaceUiState.Loading -> Unit - is SelectedPlaceUiState.Success -> { - binding.layoutSelectedPlace.showBottomAnimation() - updateSelectedPlaceUi(selectedPlace.value) - } - - is SelectedPlaceUiState.Error -> showErrorSnackBar(selectedPlace.throwable) - is SelectedPlaceUiState.Empty -> backPressedCallback.isEnabled = false - } - } - } - - private fun updateSelectedPlaceUi(selectedPlace: PlaceDetailUiModel) { - with(binding) { - layoutSelectedPlace.visibility = View.VISIBLE - tvSelectedPlaceTitle.text = - selectedPlace.place.title ?: getString(R.string.place_list_default_title) - tvSelectedPlaceLocation.text = - selectedPlace.place.location ?: getString(R.string.place_list_default_location) - setFormatDate( - binding.tvSelectedPlaceTime, - selectedPlace.startTime, - selectedPlace.endTime, - ) - tvSelectedPlaceHost.text = - selectedPlace.host ?: getString(R.string.place_detail_default_host) - tvSelectedPlaceDescription.text = selectedPlace.place.description - ?: getString(R.string.place_list_default_description) - cvPlaceCategory.setCategory(selectedPlace.place.category) - ivSelectedPlaceImage.loadImage(selectedPlace.featuredImage) - } - } - private fun startPlaceDetailActivity(placeDetail: PlaceDetailUiModel) { startActivity(PlaceDetailActivity.newIntent(requireContext(), placeDetail)) } diff --git a/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeDetailPreview/component/PlaceDetailPreviewScreen.kt b/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeDetailPreview/component/PlaceDetailPreviewScreen.kt index 356c85f..fe2d95a 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeDetailPreview/component/PlaceDetailPreviewScreen.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeDetailPreview/component/PlaceDetailPreviewScreen.kt @@ -1,9 +1,8 @@ package com.daedan.festabook.presentation.placeMap.placeDetailPreview.component -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.core.MutableTransitionState -import androidx.compose.animation.fadeIn -import androidx.compose.animation.slideInVertically +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.tween +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -13,10 +12,11 @@ import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow @@ -37,50 +37,60 @@ import com.daedan.festabook.presentation.theme.FestabookTheme import com.daedan.festabook.presentation.theme.FestabookTypography import com.daedan.festabook.presentation.theme.festabookShapes import com.daedan.festabook.presentation.theme.festabookSpacing +import kotlinx.coroutines.launch @Composable fun PlaceDetailPreviewScreen( placeUiState: SelectedPlaceUiState, modifier: Modifier = Modifier, + visible: Boolean = false, onClick: (SelectedPlaceUiState) -> Unit = {}, - onError: () -> Unit = {}, + onError: (SelectedPlaceUiState.Error) -> Unit = {}, onEmpty: () -> Unit = {}, ) { - val visibleState = - remember { - MutableTransitionState(false).apply { targetState = true } + val offsetY = remember { Animatable(120f) } + val alpha = remember { Animatable(0.3f) } + + LaunchedEffect(visible) { + if (visible) { + launch { + offsetY.animateTo( + targetValue = 0f, + animationSpec = tween(300), + ) + } + launch { + alpha.animateTo(1f, animationSpec = tween(300)) + } + } else { + // 나갈 때 애니메이션 (위에서 아래로 + 페이드아웃) + launch { offsetY.snapTo(120f) } + launch { alpha.snapTo(0.3f) } } - val density = LocalDensity.current - - AnimatedVisibility( - visibleState = visibleState, - enter = - fadeIn( - initialAlpha = 0.3f, // 시작 투명도 0.3f - ) + - slideInVertically( - initialOffsetY = { with(density) { 120.dp.roundToPx() } }, // 120dp 아래에서 시작 + } + + Box( + modifier = + modifier + .wrapContentSize() + .clickable { onClick(placeUiState) } + .graphicsLayer { + translationY = offsetY.value + this.alpha = alpha.value + }.cardBackground( + backgroundColor = FestabookColor.white, + borderColor = FestabookColor.gray200, + shape = festabookShapes.radius5, ), - modifier = modifier, ) { - Box( - modifier = - Modifier - .cardBackground( - backgroundColor = FestabookColor.white, - borderColor = FestabookColor.gray200, - shape = festabookShapes.radius5, - ), - ) { - when (placeUiState) { - is SelectedPlaceUiState.Loading -> Unit - is SelectedPlaceUiState.Success -> { - PlaceDetailPreviewContent(placeDetail = placeUiState.value) - } - - is SelectedPlaceUiState.Error -> onError() - is SelectedPlaceUiState.Empty -> onEmpty() + when (placeUiState) { + is SelectedPlaceUiState.Loading -> Unit + is SelectedPlaceUiState.Success -> { + PlaceDetailPreviewContent(placeDetail = placeUiState.value) } + + is SelectedPlaceUiState.Error -> onError(placeUiState) + is SelectedPlaceUiState.Empty -> onEmpty() } } } @@ -185,6 +195,8 @@ private fun PlaceDetailPreviewContent( placeDetail.place.description ?: stringResource(R.string.place_list_default_description), style = FestabookTypography.bodySmall, + maxLines = 2, + overflow = TextOverflow.Ellipsis, ) } } From 1043f84ab6300e1a05a9667c1a876e44e01d6407 Mon Sep 17 00:00:00 2001 From: oungsi2000 Date: Thu, 18 Dec 2025 01:40:17 +0900 Subject: [PATCH 3/4] =?UTF-8?q?refactor(PlaceDetailPreview):=20=EC=95=A0?= =?UTF-8?q?=EB=8B=88=EB=A9=94=EC=9D=B4=EC=85=98=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC=20=EB=B0=8F=20Secondary=20Preview=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 기존 `PlaceDetailPreviewScreen`에 포함되어 있던 입장/퇴장 애니메이션 로직을 `PreviewAnimatableBox` 컴포저블로 분리하여 재사용성을 높였습니다. 또한, 장소 이름과 카테고리 아이콘만 간결하게 표시하는 `PlaceDetailPreviewSecondaryScreen`을 새로 추가했습니다. - **`PreviewAnimatableBox.kt` 신규 추가:** - `visible` 상태에 따라 Y축 이동(`translationY`) 및 투명도(`alpha`) 애니메이션을 처리하는 `Box` 래퍼 컴포저블을 구현했습니다. - 배경, 테두리, 모양 등 UI 속성을 파라미터로 받아 커스터마이징할 수 있습니다. - **`PlaceDetailPreviewScreen.kt` 리팩토링:** - 기존에 직접 구현되어 있던 `Animatable`, `LaunchedEffect`를 사용한 애니메이션 코드를 제거했습니다. - 새로 추가된 `PreviewAnimatableBox`를 사용하여 UI와 애니메이션 로직을 분리하고 코드를 간소화했습니다. - **`PlaceDetailPreviewSecondaryScreen.kt` 신규 추가:** - 지도 위에서 선택된 장소의 아이콘과 이름만 표시하는 간단한 미리보기 화면을 구현했습니다. - 이 화면 역시 `PreviewAnimatableBox`를 사용하여 애니메이션 효과를 적용합니다. - **`strings.xml` 수정:** - 카테고리 마커 아이콘에 대한 접근성을 위해 `content_description_iv_category_marker` 문자열 리소스를 추가했습니다. --- .../component/PlaceDetailPreviewScreen.kt | 42 +------ .../PlaceDetailPreviewSecondaryScreen.kt | 114 ++++++++++++++++++ .../component/PreviewAnimatableBox.kt | 67 ++++++++++ app/src/main/res/values/strings.xml | 1 + 4 files changed, 185 insertions(+), 39 deletions(-) create mode 100644 app/src/main/java/com/daedan/festabook/presentation/placeMap/placeDetailPreview/component/PlaceDetailPreviewSecondaryScreen.kt create mode 100644 app/src/main/java/com/daedan/festabook/presentation/placeMap/placeDetailPreview/component/PreviewAnimatableBox.kt diff --git a/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeDetailPreview/component/PlaceDetailPreviewScreen.kt b/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeDetailPreview/component/PlaceDetailPreviewScreen.kt index fe2d95a..0e4c101 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeDetailPreview/component/PlaceDetailPreviewScreen.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeDetailPreview/component/PlaceDetailPreviewScreen.kt @@ -1,9 +1,6 @@ package com.daedan.festabook.presentation.placeMap.placeDetailPreview.component -import androidx.compose.animation.core.Animatable -import androidx.compose.animation.core.tween import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding @@ -12,11 +9,8 @@ import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow @@ -25,7 +19,6 @@ import androidx.compose.ui.unit.dp import com.daedan.festabook.R import com.daedan.festabook.presentation.common.component.CoilImage import com.daedan.festabook.presentation.common.component.URLText -import com.daedan.festabook.presentation.common.component.cardBackground import com.daedan.festabook.presentation.common.convertImageUrl import com.daedan.festabook.presentation.placeDetail.model.PlaceDetailUiModel import com.daedan.festabook.presentation.placeMap.component.PlaceCategoryLabel @@ -37,7 +30,6 @@ import com.daedan.festabook.presentation.theme.FestabookTheme import com.daedan.festabook.presentation.theme.FestabookTypography import com.daedan.festabook.presentation.theme.festabookShapes import com.daedan.festabook.presentation.theme.festabookSpacing -import kotlinx.coroutines.launch @Composable fun PlaceDetailPreviewScreen( @@ -48,40 +40,12 @@ fun PlaceDetailPreviewScreen( onError: (SelectedPlaceUiState.Error) -> Unit = {}, onEmpty: () -> Unit = {}, ) { - val offsetY = remember { Animatable(120f) } - val alpha = remember { Animatable(0.3f) } - - LaunchedEffect(visible) { - if (visible) { - launch { - offsetY.animateTo( - targetValue = 0f, - animationSpec = tween(300), - ) - } - launch { - alpha.animateTo(1f, animationSpec = tween(300)) - } - } else { - // 나갈 때 애니메이션 (위에서 아래로 + 페이드아웃) - launch { offsetY.snapTo(120f) } - launch { alpha.snapTo(0.3f) } - } - } - - Box( + PreviewAnimatableBox( + visible = visible, modifier = modifier .wrapContentSize() - .clickable { onClick(placeUiState) } - .graphicsLayer { - translationY = offsetY.value - this.alpha = alpha.value - }.cardBackground( - backgroundColor = FestabookColor.white, - borderColor = FestabookColor.gray200, - shape = festabookShapes.radius5, - ), + .clickable { onClick(placeUiState) }, ) { when (placeUiState) { is SelectedPlaceUiState.Loading -> Unit diff --git a/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeDetailPreview/component/PlaceDetailPreviewSecondaryScreen.kt b/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeDetailPreview/component/PlaceDetailPreviewSecondaryScreen.kt new file mode 100644 index 0000000..545a615 --- /dev/null +++ b/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeDetailPreview/component/PlaceDetailPreviewSecondaryScreen.kt @@ -0,0 +1,114 @@ +package com.daedan.festabook.presentation.placeMap.placeDetailPreview.component + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.daedan.festabook.R +import com.daedan.festabook.presentation.placeDetail.model.PlaceDetailUiModel +import com.daedan.festabook.presentation.placeMap.model.PlaceCategoryUiModel +import com.daedan.festabook.presentation.placeMap.model.PlaceUiModel +import com.daedan.festabook.presentation.placeMap.model.SelectedPlaceUiState +import com.daedan.festabook.presentation.placeMap.model.getIconId +import com.daedan.festabook.presentation.placeMap.model.getTextId +import com.daedan.festabook.presentation.theme.FestabookTheme +import com.daedan.festabook.presentation.theme.FestabookTypography +import com.daedan.festabook.presentation.theme.festabookSpacing + +@Composable +fun PlaceDetailPreviewSecondaryScreen( + placeUiState: SelectedPlaceUiState, + modifier: Modifier = Modifier, + onError: (SelectedPlaceUiState.Error) -> Unit = {}, + onEmpty: () -> Unit = {}, + visible: Boolean = false, +) { + PreviewAnimatableBox( + visible = visible, + modifier = modifier, + ) { + when (placeUiState) { + is SelectedPlaceUiState.Loading -> Unit + is SelectedPlaceUiState.Error -> onError(placeUiState) + is SelectedPlaceUiState.Empty -> onEmpty() + is SelectedPlaceUiState.Success -> { + Row( + modifier = + Modifier.padding( + horizontal = festabookSpacing.paddingBody4, + vertical = festabookSpacing.paddingBody3, + ), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + modifier = Modifier.size(24.dp), + painter = + painterResource( + placeUiState.value.place.category + .getIconId(), + ), + tint = Color.Unspecified, + contentDescription = stringResource(R.string.content_description_iv_category_marker), + ) + + Text( + modifier = Modifier.padding(start = festabookSpacing.paddingBody2), + text = + placeUiState.value.place.title + ?: stringResource( + placeUiState.value.place.category + .getTextId(), + ), + style = FestabookTypography.displaySmall, + ) + } + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun PlaceDetailPreviewSecondaryScreenPreview() { + FestabookTheme { + PlaceDetailPreviewSecondaryScreen( + visible = true, + modifier = Modifier.padding(horizontal = festabookSpacing.paddingScreenGutter), + placeUiState = + SelectedPlaceUiState.Success( + FAKE_PLACE_DETAIL, + ), + ) + } +} + +private val FAKE_PLACE = + PlaceUiModel( + id = 1, + imageUrl = null, + category = PlaceCategoryUiModel.TOILET, + title = "테스트테스트테스트테스트테스트테스트테스트테스트테스트테스트테스트테스트테스트", + description = "https://onlyfor-me-blog.tistory.com/1190", + location = null, + isBookmarked = false, + timeTagId = listOf(1), + ) + +private val FAKE_PLACE_DETAIL = + PlaceDetailUiModel( + place = FAKE_PLACE, + notices = listOf(), + host = null, + startTime = null, + endTime = null, + images = listOf(), + ) diff --git a/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeDetailPreview/component/PreviewAnimatableBox.kt b/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeDetailPreview/component/PreviewAnimatableBox.kt new file mode 100644 index 0000000..0d478d9 --- /dev/null +++ b/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeDetailPreview/component/PreviewAnimatableBox.kt @@ -0,0 +1,67 @@ +package com.daedan.festabook.presentation.placeMap.placeDetailPreview.component + +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.tween +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.daedan.festabook.presentation.common.component.cardBackground +import com.daedan.festabook.presentation.theme.FestabookColor +import com.daedan.festabook.presentation.theme.festabookShapes +import kotlinx.coroutines.launch + +@Composable +fun PreviewAnimatableBox( + visible: Boolean, + modifier: Modifier = Modifier, + backgroundColor: Color = FestabookColor.white, + borderColor: Color = FestabookColor.gray200, + shape: Shape = festabookShapes.radius5, + borderStroke: Dp = 1.dp, + content: @Composable BoxScope.() -> Unit = {}, +) { + val offsetY = remember { Animatable(120f) } + val alpha = remember { Animatable(0.3f) } + + LaunchedEffect(visible) { + if (visible) { + launch { + offsetY.animateTo( + targetValue = 0f, + animationSpec = tween(300), + ) + } + launch { + alpha.animateTo(1f, animationSpec = tween(300)) + } + } else { + // 나갈 때 애니메이션 (위에서 아래로 + 페이드아웃) + launch { offsetY.snapTo(120f) } + launch { alpha.snapTo(0.3f) } + } + } + + Box( + modifier = + modifier + .graphicsLayer { + translationY = offsetY.value + this.alpha = alpha.value + }.cardBackground( + backgroundColor = backgroundColor, + borderColor = borderColor, + shape = shape, + borderStroke = borderStroke, + ), + ) { + content() + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 905ce6d..398b9f6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -91,6 +91,7 @@ 공지 아이콘 고정핀 아이콘 위치 아이콘 + 카테고리 아이콘 부스 이미지 운영 시간 아이콘 호스트 아이콘 From 38b57357ee8585f685cee0533dc27d526712d028 Mon Sep 17 00:00:00 2001 From: oungsi2000 Date: Thu, 18 Dec 2025 14:51:38 +0900 Subject: [PATCH 4/4] =?UTF-8?q?refactor(PlaceDetailPreview):=20PlaceDetail?= =?UTF-8?q?PreviewSecondaryFragment=20Compose=20=EB=A7=88=EC=9D=B4?= =?UTF-8?q?=EA=B7=B8=EB=A0=88=EC=9D=B4=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 기존 View 시스템 기반의 `PlaceDetailPreviewSecondaryFragment`를 Jetpack Compose로 마이그레이션하여 UI 구현 방식을 변경했습니다. - **`PlaceDetailPreviewSecondaryFragment.kt` 수정:** - `onViewCreated` 및 ViewBinding 관련 로직을 제거하고, `onCreateView`에서 `ComposeView`를 반환하도록 변경했습니다. - `PlaceDetailPreviewSecondaryScreen` 컴포저블을 사용하여 UI를 구성하고, `viewModel.selectedPlaceFlow`를 `collectAsStateWithLifecycle`로 구독하여 상태를 전달했습니다. - 뒤로 가기 콜백(`OnBackPressedCallback`) 활성화 로직을 `LaunchedEffect` 내에서 처리하도록 변경했습니다. - 클릭 시 발생하는 로그 기록 로직(`PlacePreviewClick`)을 컴포저블의 `onClick` 콜백으로 이동시켰습니다. - **`PlaceDetailPreviewSecondaryScreen.kt` 수정:** - 컴포넌트 전체에 클릭 이벤트를 지원하기 위해 `onClick` 파라미터를 추가하고 `clickable` Modifier를 적용했습니다. - `PreviewAnimatableBox`에 `fillMaxWidth` Modifier와 `shape`(`radius2`) 설정을 추가하여 UI 레이아웃을 개선했습니다. --- .../PlaceDetailPreviewSecondaryFragment.kt | 114 ++++++++++-------- .../PlaceDetailPreviewSecondaryScreen.kt | 14 ++- 2 files changed, 77 insertions(+), 51 deletions(-) diff --git a/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeDetailPreview/PlaceDetailPreviewSecondaryFragment.kt b/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeDetailPreview/PlaceDetailPreviewSecondaryFragment.kt index 684f6bd..48edaeb 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeDetailPreview/PlaceDetailPreviewSecondaryFragment.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeDetailPreview/PlaceDetailPreviewSecondaryFragment.kt @@ -1,26 +1,36 @@ package com.daedan.festabook.presentation.placeMap.placeDetailPreview import android.os.Bundle +import android.view.LayoutInflater import android.view.View +import android.view.ViewGroup import androidx.activity.OnBackPressedCallback +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.lifecycle.ViewModelProvider -import coil3.load +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.daedan.festabook.R import com.daedan.festabook.databinding.FragmentPlaceDetailPreviewSecondaryBinding +import com.daedan.festabook.di.appGraph import com.daedan.festabook.di.fragment.FragmentKey -import com.daedan.festabook.logging.logger import com.daedan.festabook.presentation.common.BaseFragment import com.daedan.festabook.presentation.common.OnMenuItemReClickListener -import com.daedan.festabook.presentation.common.showBottomAnimation import com.daedan.festabook.presentation.common.showErrorSnackBar -import com.daedan.festabook.presentation.placeDetail.model.PlaceDetailUiModel import com.daedan.festabook.presentation.placeMap.PlaceMapViewModel import com.daedan.festabook.presentation.placeMap.logging.PlacePreviewClick import com.daedan.festabook.presentation.placeMap.model.SelectedPlaceUiState -import com.daedan.festabook.presentation.placeMap.model.getIconId -import com.daedan.festabook.presentation.placeMap.model.getTextId +import com.daedan.festabook.presentation.placeMap.placeDetailPreview.component.PlaceDetailPreviewSecondaryScreen +import com.daedan.festabook.presentation.theme.FestabookTheme +import com.daedan.festabook.presentation.theme.festabookSpacing import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.ContributesIntoMap import dev.zacsweers.metro.Inject @@ -43,56 +53,62 @@ class PlaceDetailPreviewSecondaryFragment( } } - override fun onViewCreated( - view: View, + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, savedInstanceState: Bundle?, - ) { - super.onViewCreated(view, savedInstanceState) - setUpObserver() - setUpBackPressedCallback() - } - - override fun onMenuItemReClick() { - viewModel.unselectPlace() - } + ): View { + return ComposeView(requireContext()).apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + setContent { + val placeDetailUiState by viewModel.selectedPlaceFlow.collectAsStateWithLifecycle() + val visible = placeDetailUiState is SelectedPlaceUiState.Success - private fun setUpBackPressedCallback() { - requireActivity().onBackPressedDispatcher.addCallback( - viewLifecycleOwner, - backPressedCallback, - ) - } - - private fun setUpObserver() { - viewModel.selectedPlace.observe(viewLifecycleOwner) { selectedPlace -> - backPressedCallback.isEnabled = true - when (selectedPlace) { - is SelectedPlaceUiState.Success -> { - binding.layoutSelectedPlace.visibility = View.VISIBLE - binding.layoutSelectedPlace.showBottomAnimation() - updateSelectedPlaceUi(selectedPlace.value) - binding.logger.log( - PlacePreviewClick( - baseLogData = binding.logger.getBaseLogData(), - placeName = selectedPlace.value.place.title ?: "undefined", - timeTag = viewModel.selectedTimeTag.value?.name ?: "undefined", - category = selectedPlace.value.place.category.name, - ), - ) + LaunchedEffect(placeDetailUiState) { + backPressedCallback.isEnabled = true } - is SelectedPlaceUiState.Error -> showErrorSnackBar(selectedPlace.throwable) - is SelectedPlaceUiState.Loading -> Unit - is SelectedPlaceUiState.Empty -> backPressedCallback.isEnabled = false + FestabookTheme { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.BottomCenter, + ) { + PlaceDetailPreviewSecondaryScreen( + visible = visible, + placeUiState = placeDetailUiState, + modifier = + Modifier + .padding( + vertical = festabookSpacing.paddingBody4, + horizontal = festabookSpacing.paddingScreenGutter, + ), + onError = { + showErrorSnackBar(it.throwable) + }, + onEmpty = { + backPressedCallback.isEnabled = false + }, + onClick = { + if (it !is SelectedPlaceUiState.Success) return@PlaceDetailPreviewSecondaryScreen + appGraph.defaultFirebaseLogger.log( + PlacePreviewClick( + baseLogData = appGraph.defaultFirebaseLogger.getBaseLogData(), + placeName = it.value.place.title ?: "undefined", + timeTag = + viewModel.selectedTimeTag.value?.name + ?: "undefined", + category = it.value.place.category.name, + ), + ) + }, + ) + } + } } } } - private fun updateSelectedPlaceUi(selectedPlace: PlaceDetailUiModel) { - with(binding) { - ivSecondaryCategoryItem.load(selectedPlace.place.category.getIconId()) - tvSelectedPlaceTitle.text = - selectedPlace.place.title ?: getString(selectedPlace.place.category.getTextId()) - } + override fun onMenuItemReClick() { + viewModel.unselectPlace() } } diff --git a/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeDetailPreview/component/PlaceDetailPreviewSecondaryScreen.kt b/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeDetailPreview/component/PlaceDetailPreviewSecondaryScreen.kt index 545a615..f6a3ba2 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeDetailPreview/component/PlaceDetailPreviewSecondaryScreen.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeDetailPreview/component/PlaceDetailPreviewSecondaryScreen.kt @@ -1,6 +1,8 @@ package com.daedan.festabook.presentation.placeMap.placeDetailPreview.component +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material3.Icon @@ -22,6 +24,7 @@ import com.daedan.festabook.presentation.placeMap.model.getIconId import com.daedan.festabook.presentation.placeMap.model.getTextId import com.daedan.festabook.presentation.theme.FestabookTheme import com.daedan.festabook.presentation.theme.FestabookTypography +import com.daedan.festabook.presentation.theme.festabookShapes import com.daedan.festabook.presentation.theme.festabookSpacing @Composable @@ -30,11 +33,18 @@ fun PlaceDetailPreviewSecondaryScreen( modifier: Modifier = Modifier, onError: (SelectedPlaceUiState.Error) -> Unit = {}, onEmpty: () -> Unit = {}, + onClick: (SelectedPlaceUiState) -> Unit = {}, visible: Boolean = false, ) { PreviewAnimatableBox( visible = visible, - modifier = modifier, + modifier = + modifier + .fillMaxWidth() + .clickable { + onClick(placeUiState) + }, + shape = festabookShapes.radius2, ) { when (placeUiState) { is SelectedPlaceUiState.Loading -> Unit @@ -96,7 +106,7 @@ private val FAKE_PLACE = id = 1, imageUrl = null, category = PlaceCategoryUiModel.TOILET, - title = "테스트테스트테스트테스트테스트테스트테스트테스트테스트테스트테스트테스트테스트", + title = "테스트테스", description = "https://onlyfor-me-blog.tistory.com/1190", location = null, isBookmarked = false,