From d65a167e681d2f682b759737f1dc173202da4b1a Mon Sep 17 00:00:00 2001 From: oungsi2000 Date: Mon, 1 Dec 2025 23:45:51 +0900 Subject: [PATCH 01/10] =?UTF-8?q?feat(PlaceMap):=20NaverMap=EC=9D=84=20Com?= =?UTF-8?q?pose=EB=A1=9C=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 기존 `MapFragment`를 사용하던 지도 표시 방식을 `Compose` 환경에 맞게 `NaverMapContent` 컴포저블로 마이그레이션했습니다. 이를 통해 지도 관련 UI 로직을 `Compose` 내에서 통합 관리할 수 있도록 개선했습니다. - **`NaverMapContent.kt` 컴포저블 추가:** - `AndroidView`를 사용해 `MapView`를 래핑하는 `NaverMapContent` 컴포저블을 새로 추가했습니다. - `DisposableEffect`를 활용하여 컴포저블의 생명주기와 `MapView`의 생명주기(`onCreate`, `onStart`, `onResume`, `onPause`, `onStop`, `onDestroy`)를 동기화했습니다. - `onDispose` 블록 내에서 `MapView`의 생명주기 메서드를 순차적으로 호출하여 메모리 누수를 방지하도록 처리했습니다. - 지도 드래그 이벤트를 감지하기 위해 `pointerInput` Modifier를 사용하여 드래그 인터셉터(`dragInterceptor`)를 구현했습니다. - **`PlaceMapScreen.kt` 컴포저블 추가:** - `NaverMapContent`와 시간대 선택 메뉴인 `TimeTagMenu`를 결합한 `PlaceMapScreen` 컴포저블을 추가하여 지도 화면의 전체 레이아웃을 구성했습니다. - **`PlaceMapFragment.kt` 리팩토링:** - 기존의 `MapFragment`와 `TimeTagMenu`를 직접 관리하던 XML 및 Fragment 코드를 제거했습니다. - `ComposeView`를 통해 새로 추가된 `PlaceMapScreen`을 화면에 표시하도록 변경했습니다. - `onMapReady` 콜백을 통해 `NaverMap` 객체를 받아와 지도 관련 로직을 설정하도록 수정했습니다. --- .../presentation/placeMap/PlaceMapFragment.kt | 90 +++++----- .../placeMap/component/NaverMapContent.kt | 158 ++++++++++++++++++ .../placeMap/component/PlaceMapScreen.kt | 46 +++++ 3 files changed, 242 insertions(+), 52 deletions(-) create mode 100644 app/src/main/java/com/daedan/festabook/presentation/placeMap/component/NaverMapContent.kt create mode 100644 app/src/main/java/com/daedan/festabook/presentation/placeMap/component/PlaceMapScreen.kt diff --git a/app/src/main/java/com/daedan/festabook/presentation/placeMap/PlaceMapFragment.kt b/app/src/main/java/com/daedan/festabook/presentation/placeMap/PlaceMapFragment.kt index 773b642..6f0aeba 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/placeMap/PlaceMapFragment.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/placeMap/PlaceMapFragment.kt @@ -3,19 +3,14 @@ package com.daedan.festabook.presentation.placeMap import android.content.Context import android.os.Bundle import android.view.View -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.padding import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ViewCompositionStrategy -import androidx.compose.ui.unit.dp import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentTransaction import androidx.fragment.app.commit import androidx.fragment.app.viewModels import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.lifecycleScope import com.daedan.festabook.R import com.daedan.festabook.databinding.FragmentPlaceMapBinding import com.daedan.festabook.di.fragment.FragmentKey @@ -25,6 +20,7 @@ import com.daedan.festabook.presentation.common.BaseFragment import com.daedan.festabook.presentation.common.OnMenuItemReClickListener import com.daedan.festabook.presentation.common.showErrorSnackBar import com.daedan.festabook.presentation.common.toPx +import com.daedan.festabook.presentation.placeMap.component.PlaceMapScreen import com.daedan.festabook.presentation.placeMap.logging.CurrentLocationChecked import com.daedan.festabook.presentation.placeMap.logging.PlaceFragmentEnter import com.daedan.festabook.presentation.placeMap.logging.PlaceMarkerClick @@ -36,10 +32,7 @@ import com.daedan.festabook.presentation.placeMap.placeCategory.PlaceCategoryFra import com.daedan.festabook.presentation.placeMap.placeDetailPreview.PlaceDetailPreviewFragment import com.daedan.festabook.presentation.placeMap.placeDetailPreview.PlaceDetailPreviewSecondaryFragment import com.daedan.festabook.presentation.placeMap.placeList.PlaceListFragment -import com.daedan.festabook.presentation.placeMap.timeTagSpinner.component.TimeTagMenu -import com.daedan.festabook.presentation.theme.FestabookColor import com.daedan.festabook.presentation.theme.FestabookTheme -import com.naver.maps.map.MapFragment import com.naver.maps.map.NaverMap import com.naver.maps.map.OnMapReadyCallback import com.naver.maps.map.util.FusedLocationSource @@ -48,7 +41,6 @@ import dev.zacsweers.metro.ContributesIntoMap import dev.zacsweers.metro.Inject import dev.zacsweers.metro.binding import dev.zacsweers.metro.createGraphFactory -import kotlinx.coroutines.launch import timber.log.Timber @ContributesIntoMap( @@ -62,7 +54,6 @@ class PlaceMapFragment( placeDetailPreviewFragment: PlaceDetailPreviewFragment, placeCategoryFragment: PlaceCategoryFragment, placeDetailPreviewSecondaryFragment: PlaceDetailPreviewSecondaryFragment, - mapFragment: MapFragment, ) : BaseFragment(), OnMenuItemReClickListener { override val layoutId: Int = R.layout.fragment_place_map @@ -80,8 +71,6 @@ class PlaceMapFragment( placeDetailPreviewSecondaryFragment, ) } - private val mapFragment by lazy { getIfExists(mapFragment) } - private val locationSource by lazy { FusedLocationSource(this, LOCATION_PERMISSION_REQUEST_CODE) } @@ -96,7 +85,6 @@ class PlaceMapFragment( super.onViewCreated(view, savedInstanceState) if (savedInstanceState == null) { childFragmentManager.commit { - addWithSimpleTag(R.id.fcv_map_container, mapFragment) addWithSimpleTag(R.id.fcv_place_list_container, placeListFragment) addWithSimpleTag(R.id.fcv_map_container, placeDetailPreviewFragment) addWithSimpleTag(R.id.fcv_place_category_container, placeCategoryFragment) @@ -105,11 +93,9 @@ class PlaceMapFragment( hide(placeDetailPreviewSecondaryFragment) } } - lifecycleScope.launch { - setUpMapManager() - setupComposeView() - setUpObserver() - } + + setupComposeView() + binding.logger.log( PlaceFragmentEnter( baseLogData = binding.logger.getBaseLogData(), @@ -130,8 +116,39 @@ class PlaceMapFragment( mapManager?.moveToPosition() } - private suspend fun setUpMapManager() { - naverMap = mapFragment.getMap() + override fun onTimeTagSelected(item: TimeTag) { + viewModel.unselectPlace() + viewModel.onDaySelected(item) + binding.logger.log( + PlaceTimeTagSelected( + baseLogData = binding.logger.getBaseLogData(), + timeTagName = item.name, + ), + ) + } + + override fun onNothingSelected() = Unit + + private fun setupComposeView() { + binding.cvPlaceMap.apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + setContent { + val timeTags by viewModel.timeTags.collectAsStateWithLifecycle(viewLifecycleOwner) + FestabookTheme { + PlaceMapScreen( + onMapReady = { setupMap(it) }, + timeTags = timeTags, + onTimeTagSelected = { + onTimeTagSelected(it) + }, + ) + } + } + } + } + + private fun setupMap(map: NaverMap) { + naverMap = map naverMap.addOnLocationChangeListener { binding.logger.log( CurrentLocationChecked( @@ -144,38 +161,7 @@ class PlaceMapFragment( binding.viewMapTouchEventIntercept.setOnMapDragListener { viewModel.onMapViewClick() } - } - - private fun setupComposeView() { - binding.cvPlaceMap.apply { - setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) - setContent { - FestabookTheme { - val timeTags by viewModel.timeTags.collectAsStateWithLifecycle() - val title by viewModel.selectedTimeTagFlow.collectAsStateWithLifecycle() - if (timeTags.isNotEmpty()) { - TimeTagMenu( - title = title.name, - timeTags = timeTags, - onTimeTagClick = { timeTag -> - viewModel.onDaySelected(timeTag) - binding.logger.log( - PlaceTimeTagSelected( - baseLogData = binding.logger.getBaseLogData(), - timeTagName = timeTag.name, - ), - ) - }, - modifier = - Modifier - .background( - FestabookColor.white, - ).padding(horizontal = 24.dp), - ) - } - } - } - } + setUpObserver() } private fun setUpObserver() { diff --git a/app/src/main/java/com/daedan/festabook/presentation/placeMap/component/NaverMapContent.kt b/app/src/main/java/com/daedan/festabook/presentation/placeMap/component/NaverMapContent.kt new file mode 100644 index 0000000..095e563 --- /dev/null +++ b/app/src/main/java/com/daedan/festabook/presentation/placeMap/component/NaverMapContent.kt @@ -0,0 +1,158 @@ +package com.daedan.festabook.presentation.placeMap.component + +import android.content.ComponentCallbacks2 +import android.content.res.Configuration +import android.os.Bundle +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.pointer.PointerEventPass +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.viewinterop.AndroidView +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.compose.LocalLifecycleOwner +import com.naver.maps.map.MapView +import com.naver.maps.map.NaverMap + +@Composable +fun NaverMapContent( + modifier: Modifier = Modifier, + onMapDrag: () -> Unit = {}, + onMapReady: (NaverMap) -> Unit = {}, + content: @Composable () -> Unit, +) { + val context = LocalContext.current + val mapView by remember { mutableStateOf(MapView(context)) } + AndroidView( + factory = { + mapView.apply { + getMapAsync(onMapReady) + } + }, + modifier = modifier.dragInterceptor(onMapDrag), + ) + RegisterMapLifeCycle(mapView) + content() +} + +private fun Modifier.dragInterceptor(onMapDrag: () -> Unit): Modifier = + this.then( + Modifier.pointerInput(Unit) { + val touchSlop = viewConfiguration.touchSlop // 시스템이 정의한 드래그 판단 기준 거리 + + awaitPointerEventScope { + while (true) { + // 1. 첫 번째 터치(Down)를 기다립니다. + // pass = PointerEventPass.Initial : 자식보다 먼저 이벤트를 봅니다. + // requireUnconsumed = false : 이미 다른 곳에서 처리했더라도 신경 쓰지 않고 받습니다. + val downEvent = awaitPointerEvent(pass = PointerEventPass.Initial) + val downChange = downEvent.changes.firstOrNull { it.pressed } ?: continue + + // 터치 시작 지점 저장 + val startPosition = downChange.position + var isDragEmitted = false // 이번 드래그 세션에서 콜백을 호출했는지 체크 + + // 2. 터치가 유지되는 동안(드래그 중) 계속 감시합니다. + do { + val event = awaitPointerEvent(pass = PointerEventPass.Initial) + val change = event.changes.firstOrNull { it.id == downChange.id } + + if (change != null && change.pressed) { + // 현재 위치와 시작 위치 사이의 거리 계산 + val currentPosition = change.position + val distance = (currentPosition - startPosition).getDistance() + + // 3. 이동 거리가 touchSlop보다 크고, 아직 콜백을 안 불렀다면 호출 + if (!isDragEmitted && distance > touchSlop) { + onMapDrag() + isDragEmitted = true + } + } + } while (event.changes.any { it.pressed }) // 손을 뗄 때까지 루프 + } + } + }, + ) + +@Composable +private fun RegisterMapLifeCycle(mapView: MapView) { + val context = LocalContext.current + val lifecycle = LocalLifecycleOwner.current.lifecycle + val previousState = remember { mutableStateOf(Lifecycle.Event.ON_CREATE) } + val savedInstanceState = rememberSaveable { Bundle() } + + DisposableEffect(context, lifecycle, mapView, savedInstanceState) { + val mapLifecycleObserver = + mapView.lifecycleObserver( + savedInstanceState.takeUnless { it.isEmpty }, + previousState, + ) + + val callbacks = + object : ComponentCallbacks2 { + override fun onConfigurationChanged(config: Configuration) = Unit + + @Deprecated("This callback is superseded by onTrimMemory") + override fun onLowMemory() { + mapView.onLowMemory() + } + + override fun onTrimMemory(level: Int) { + mapView.onLowMemory() + } + } + + lifecycle.addObserver(mapLifecycleObserver) + context.registerComponentCallbacks(callbacks) + onDispose { + mapView.onSaveInstanceState(savedInstanceState) + lifecycle.removeObserver(mapLifecycleObserver) + context.unregisterComponentCallbacks(callbacks) + + // dispose 시점에 Lifecycle.Event가 끝까지 진행되지 않아 발생되는 + // MapView Memory Leak 수정합니다. + when (previousState.value) { + Lifecycle.Event.ON_CREATE, Lifecycle.Event.ON_STOP -> { + mapView.onDestroy() + } + + Lifecycle.Event.ON_START, Lifecycle.Event.ON_PAUSE -> { + mapView.onStop() + mapView.onDestroy() + } + + Lifecycle.Event.ON_RESUME -> { + mapView.onPause() + mapView.onStop() + mapView.onDestroy() + } + + else -> {} + } + } + } +} + +private fun MapView.lifecycleObserver( + savedInstanceState: Bundle?, + previousState: MutableState, +): LifecycleEventObserver = + LifecycleEventObserver { _, event -> + when (event) { + Lifecycle.Event.ON_CREATE -> this.onCreate(savedInstanceState) + Lifecycle.Event.ON_START -> this.onStart() + Lifecycle.Event.ON_RESUME -> this.onResume() + Lifecycle.Event.ON_PAUSE -> this.onPause() + Lifecycle.Event.ON_STOP -> this.onStop() + Lifecycle.Event.ON_DESTROY -> this.onDestroy() + else -> throw IllegalStateException() + } + previousState.value = event + } diff --git a/app/src/main/java/com/daedan/festabook/presentation/placeMap/component/PlaceMapScreen.kt b/app/src/main/java/com/daedan/festabook/presentation/placeMap/component/PlaceMapScreen.kt new file mode 100644 index 0000000..2541420 --- /dev/null +++ b/app/src/main/java/com/daedan/festabook/presentation/placeMap/component/PlaceMapScreen.kt @@ -0,0 +1,46 @@ +package com.daedan.festabook.presentation.placeMap.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.daedan.festabook.domain.model.TimeTag +import com.daedan.festabook.presentation.placeMap.timeTagSpinner.component.TimeTagMenu +import com.daedan.festabook.presentation.theme.FestabookColor +import com.naver.maps.map.NaverMap + +@Composable +fun PlaceMapScreen( + onMapReady: (NaverMap) -> Unit, + timeTags: List, + onTimeTagSelected: (TimeTag) -> Unit, +) { + NaverMapContent( + modifier = Modifier.fillMaxSize(), + onMapReady = onMapReady, + ) { + Column( + modifier = Modifier.wrapContentSize(), + ) { + if (!timeTags.isEmpty()) { + val initialTitle = timeTags.first().name + TimeTagMenu( + initialTitle = initialTitle, + timeTags = timeTags, + onTimeTagClick = { + onTimeTagSelected(it) + }, + modifier = + Modifier + .background( + FestabookColor.white, + ).padding(horizontal = 24.dp), + ) + } + } + } +} From f1b3a11d5fa15cf66366c9611a586c126eb98c86 Mon Sep 17 00:00:00 2001 From: oungsi2000 Date: Tue, 2 Dec 2025 15:00:59 +0900 Subject: [PATCH 02/10] =?UTF-8?q?refactor:=20timeTagSpinner,=20PlaceMap?= =?UTF-8?q?=EC=9D=84=20=EB=B3=84=EB=8F=84=EC=9D=98=20ComposeView=EB=A1=9C?= =?UTF-8?q?=20=EB=B6=84=EB=A6=AC=20=EA=B8=B0=EC=A1=B4=EC=97=90=20NaverMap?= =?UTF-8?q?=EA=B3=BC=20=ED=95=A8=EA=BB=98=20=ED=95=98=EB=82=98=EC=9D=98=20?= =?UTF-8?q?`ComposeView`(`cv=5Fplace=5Fmap`)=EC=97=90=EC=84=9C=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=EB=90=98=EB=8D=98=20=EC=8B=9C=EA=B0=84=EB=8C=80=20?= =?UTF-8?q?=EC=84=A0=ED=83=9D=20=EB=A9=94=EB=89=B4=EB=A5=BC=20=EB=B3=84?= =?UTF-8?q?=EB=8F=84=EC=9D=98=20`ComposeView`(`cv=5Ftime=5Ftag=5Fspinner`)?= =?UTF-8?q?=EB=A1=9C=20=EB=B6=84=EB=A6=AC=ED=96=88=EC=8A=B5=EB=8B=88?= =?UTF-8?q?=EB=8B=A4.=20=EC=9D=B4=EB=A5=BC=20=ED=86=B5=ED=95=B4=20NaverMap?= =?UTF-8?q?=EC=9D=80=20=EC=A7=80=EB=8F=84=20=EA=B4=80=EB=A0=A8=20UI?= =?UTF-8?q?=EB=A7=8C=20=EB=8B=B4=EB=8B=B9=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=97=AD=ED=95=A0=EC=9D=84=20=EB=AA=85=ED=99=95=ED=9E=88=20?= =?UTF-8?q?=ED=95=98=EA=B3=A0,=20=EC=8B=9C=EA=B0=84=EB=8C=80=20=EC=84=A0?= =?UTF-8?q?=ED=83=9D=20=EB=A9=94=EB=89=B4=EC=9D=98=20=EB=8F=85=EB=A6=BD?= =?UTF-8?q?=EC=84=B1=EC=9D=84=20=EB=86=92=EC=98=80=EC=8A=B5=EB=8B=88?= =?UTF-8?q?=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - **`fragment_place_map.xml` 레이아웃 변경** - 시간대 선택 메뉴를 위한 `ComposeView`(`cv_time_tag_spinner`)를 추가했습니다. - `cv_time_tag_spinner`에 `translationZ="1dp"` 속성을 부여하여 지도 위에 표시되도록 했습니다. - NaverMap을 담는 `ComposeView`는 `cv_place_map`으로 유지하고, 내부에서 지도만 렌더링하도록 수정했습니다. - **`PlaceMapFragment.kt` 수정** - `cv_place_map`에서는 `NaverMapContent`를 호출하여 지도 초기화(`setupMap`)만 수행하도록 변경했습니다. - 새로 추가된 `cv_time_tag_spinner`에서 `TimeTagMenu` 컴포저블을 설정하여 시간대 선택 UI를 표시합니다. - `timeTags` 상태를 관찰하여 `TimeTagMenu`에 데이터를 전달하고, 태그 선택 시 `onTimeTagSelected` 콜백을 호출하는 로직은 그대로 유지됩니다. --- .../presentation/placeMap/PlaceMapFragment.kt | 42 +++++++++++++++---- .../main/res/layout/fragment_place_map.xml | 27 ++++++++---- 2 files changed, 52 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/com/daedan/festabook/presentation/placeMap/PlaceMapFragment.kt b/app/src/main/java/com/daedan/festabook/presentation/placeMap/PlaceMapFragment.kt index 6f0aeba..0ab93e4 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/placeMap/PlaceMapFragment.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/placeMap/PlaceMapFragment.kt @@ -3,8 +3,13 @@ package com.daedan.festabook.presentation.placeMap import android.content.Context import android.os.Bundle import android.view.View +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.compose.ui.unit.dp import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentTransaction import androidx.fragment.app.commit @@ -20,7 +25,7 @@ import com.daedan.festabook.presentation.common.BaseFragment import com.daedan.festabook.presentation.common.OnMenuItemReClickListener import com.daedan.festabook.presentation.common.showErrorSnackBar import com.daedan.festabook.presentation.common.toPx -import com.daedan.festabook.presentation.placeMap.component.PlaceMapScreen +import com.daedan.festabook.presentation.placeMap.component.NaverMapContent import com.daedan.festabook.presentation.placeMap.logging.CurrentLocationChecked import com.daedan.festabook.presentation.placeMap.logging.PlaceFragmentEnter import com.daedan.festabook.presentation.placeMap.logging.PlaceMarkerClick @@ -32,6 +37,8 @@ import com.daedan.festabook.presentation.placeMap.placeCategory.PlaceCategoryFra import com.daedan.festabook.presentation.placeMap.placeDetailPreview.PlaceDetailPreviewFragment import com.daedan.festabook.presentation.placeMap.placeDetailPreview.PlaceDetailPreviewSecondaryFragment import com.daedan.festabook.presentation.placeMap.placeList.PlaceListFragment +import com.daedan.festabook.presentation.placeMap.timeTagSpinner.component.TimeTagMenu +import com.daedan.festabook.presentation.theme.FestabookColor import com.daedan.festabook.presentation.theme.FestabookTheme import com.naver.maps.map.NaverMap import com.naver.maps.map.OnMapReadyCallback @@ -133,15 +140,34 @@ class PlaceMapFragment( binding.cvPlaceMap.apply { setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent { - val timeTags by viewModel.timeTags.collectAsStateWithLifecycle(viewLifecycleOwner) FestabookTheme { - PlaceMapScreen( + NaverMapContent( + modifier = Modifier.fillMaxSize(), onMapReady = { setupMap(it) }, - timeTags = timeTags, - onTimeTagSelected = { - onTimeTagSelected(it) - }, - ) + ) {} + } + } + } + binding.cvTimeTagSpinner.apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + setContent { + val timeTags by viewModel.timeTags.collectAsStateWithLifecycle(viewLifecycleOwner) + FestabookTheme { + if (!timeTags.isEmpty()) { + val initialTitle = timeTags.first().name + TimeTagMenu( + initialTitle = initialTitle, + timeTags = timeTags, + onTimeTagClick = { + onTimeTagSelected(it) + }, + modifier = + Modifier + .background( + FestabookColor.white, + ).padding(horizontal = 24.dp), + ) + } } } } diff --git a/app/src/main/res/layout/fragment_place_map.xml b/app/src/main/res/layout/fragment_place_map.xml index c2df411..2fabeab 100644 --- a/app/src/main/res/layout/fragment_place_map.xml +++ b/app/src/main/res/layout/fragment_place_map.xml @@ -10,11 +10,28 @@ android:layout_width="match_parent" android:layout_height="match_parent"> + + + + - - + app:layout_constraintTop_toBottomOf="@id/cv_time_tag_spinner" /> Date: Tue, 2 Dec 2025 15:48:03 +0900 Subject: [PATCH 03/10] =?UTF-8?q?refactor(PlaceMap):=20=EC=A7=80=EB=8F=84?= =?UTF-8?q?=20=EA=B4=80=EB=A0=A8=20=EC=BB=B4=ED=8F=AC=EC=A0=80=EB=B8=94=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 지도 화면에 사용되는 `NaverMapContent`와 `NaverMapLogo` 컴포저블의 코드를 개선하고 리팩토링했습니다. - **`NaverMapContent.kt` 수정:** - `mapView`를 `remember`로만 감싸도록 변경하여 불필요한 `by mutableStateOf` 위임을 제거했습니다. - `DisposableEffect`의 `key`에서 `context`와 `savedInstanceState`를 제거하여 불필요한 재구성을 방지했습니다. - `pointerInput` Modifier 내에서 `awaitPointerEvent` 호출 시 사용되던 불필요한 주석을 삭제했습니다. - **`NaverMapLogo.kt` 추가:** - 네이버 지도 로고(`LogoView`)를 표시하기 위한 `NaverMapLogo` 컴포저블을 새로 추가했습니다. - `AndroidView`를 사용하여 기존 뷰 시스템의 `LogoView`를 Compose에서 사용할 수 있도록 래핑했습니다. --- .../placeMap/component/NaverMapContent.kt | 9 +++------ .../placeMap/component/NaverMapLogo.kt | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 6 deletions(-) create mode 100644 app/src/main/java/com/daedan/festabook/presentation/placeMap/component/NaverMapLogo.kt diff --git a/app/src/main/java/com/daedan/festabook/presentation/placeMap/component/NaverMapContent.kt b/app/src/main/java/com/daedan/festabook/presentation/placeMap/component/NaverMapContent.kt index 095e563..2be5150 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/placeMap/component/NaverMapContent.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/placeMap/component/NaverMapContent.kt @@ -6,7 +6,6 @@ import android.os.Bundle import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.MutableState -import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable @@ -29,7 +28,7 @@ fun NaverMapContent( content: @Composable () -> Unit, ) { val context = LocalContext.current - val mapView by remember { mutableStateOf(MapView(context)) } + val mapView = remember { MapView(context) } AndroidView( factory = { mapView.apply { @@ -50,8 +49,6 @@ private fun Modifier.dragInterceptor(onMapDrag: () -> Unit): Modifier = awaitPointerEventScope { while (true) { // 1. 첫 번째 터치(Down)를 기다립니다. - // pass = PointerEventPass.Initial : 자식보다 먼저 이벤트를 봅니다. - // requireUnconsumed = false : 이미 다른 곳에서 처리했더라도 신경 쓰지 않고 받습니다. val downEvent = awaitPointerEvent(pass = PointerEventPass.Initial) val downChange = downEvent.changes.firstOrNull { it.pressed } ?: continue @@ -88,7 +85,7 @@ private fun RegisterMapLifeCycle(mapView: MapView) { val previousState = remember { mutableStateOf(Lifecycle.Event.ON_CREATE) } val savedInstanceState = rememberSaveable { Bundle() } - DisposableEffect(context, lifecycle, mapView, savedInstanceState) { + DisposableEffect(lifecycle, mapView) { val mapLifecycleObserver = mapView.lifecycleObserver( savedInstanceState.takeUnless { it.isEmpty }, @@ -134,7 +131,7 @@ private fun RegisterMapLifeCycle(mapView: MapView) { mapView.onDestroy() } - else -> {} + else -> Unit } } } diff --git a/app/src/main/java/com/daedan/festabook/presentation/placeMap/component/NaverMapLogo.kt b/app/src/main/java/com/daedan/festabook/presentation/placeMap/component/NaverMapLogo.kt new file mode 100644 index 0000000..4fab146 --- /dev/null +++ b/app/src/main/java/com/daedan/festabook/presentation/placeMap/component/NaverMapLogo.kt @@ -0,0 +1,18 @@ +package com.daedan.festabook.presentation.placeMap.component + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.viewinterop.AndroidView +import com.naver.maps.map.widget.LogoView + +@Composable +fun NaverMapLogo(modifier: Modifier = Modifier) { + val context = LocalContext.current + val logoView = remember { LogoView(context) } + AndroidView( + factory = { logoView }, + modifier = modifier, + ) +} From 2a0b37ec66f4cbf0a6eba6a9fbf2b2b99140fcbd Mon Sep 17 00:00:00 2001 From: oungsi2000 Date: Tue, 2 Dec 2025 22:25:07 +0900 Subject: [PATCH 04/10] =?UTF-8?q?feat(PlaceMap):=20=EC=9E=A5=EC=86=8C=20?= =?UTF-8?q?=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20=EC=84=A0=ED=83=9D=20Com?= =?UTF-8?q?pose=20View=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 장소 지도 화면에 표시될 장소의 카테고리를 선택할 수 있는 `PlaceCategoryScreen` 컴포저블을 새로 추가했습니다. 이 컴포저블은 `FilterChip`을 사용하여 '전체' 및 개별 카테고리 필터링 기능을 제공합니다. - **`PlaceCategoryScreen.kt` 신규 추가** - `Row`와 `horizontalScroll`을 사용하여 가로로 스크롤 가능한 칩 그룹을 구현했습니다. - '전체' 칩과 `PlaceCategoryUiModel`에 정의된 각 카테고리 칩들을 표시합니다. - `FilterChip`을 사용하여 `CategoryChip` 컴포저블을 구현했으며, 선택 상태에 따라 다른 스타일(배경색, 테두리)을 적용했습니다. - 각 카테고리 칩에는 `PlaceCategoryUiModel`에 정의된 아이콘과 텍스트를 표시합니다. - 칩 클릭 시 `onDisplayAllClick` 또는 `onCategoryClick` 콜백을 호출하여 선택된 상태를 외부로 전달합니다. - **`NaverMapContent.kt` 수정** - `pointerInput` Modifier 내 불필요한 주석을 삭제하여 코드를 정리했습니다. --- .../placeMap/component/NaverMapContent.kt | 1 - .../component/PlaceCategoryScreen.kt | 149 ++++++++++++++++++ 2 files changed, 149 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/com/daedan/festabook/presentation/placeMap/placeCategory/component/PlaceCategoryScreen.kt diff --git a/app/src/main/java/com/daedan/festabook/presentation/placeMap/component/NaverMapContent.kt b/app/src/main/java/com/daedan/festabook/presentation/placeMap/component/NaverMapContent.kt index 2be5150..aa72f6e 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/placeMap/component/NaverMapContent.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/placeMap/component/NaverMapContent.kt @@ -45,7 +45,6 @@ private fun Modifier.dragInterceptor(onMapDrag: () -> Unit): Modifier = this.then( Modifier.pointerInput(Unit) { val touchSlop = viewConfiguration.touchSlop // 시스템이 정의한 드래그 판단 기준 거리 - awaitPointerEventScope { while (true) { // 1. 첫 번째 터치(Down)를 기다립니다. diff --git a/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeCategory/component/PlaceCategoryScreen.kt b/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeCategory/component/PlaceCategoryScreen.kt new file mode 100644 index 0000000..13ce560 --- /dev/null +++ b/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeCategory/component/PlaceCategoryScreen.kt @@ -0,0 +1,149 @@ +package com.daedan.festabook.presentation.placeMap.placeCategory.component + +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.material3.FilterChip +import androidx.compose.material3.FilterChipDefaults +import androidx.compose.material3.Icon +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.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.placeMap.model.PlaceCategoryUiModel +import com.daedan.festabook.presentation.placeMap.model.getIconId +import com.daedan.festabook.presentation.placeMap.model.getTextId +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 PlaceCategoryScreen( + modifier: Modifier = Modifier, + onDisplayAllClick: () -> Unit = {}, + onCategoryClick: (PlaceCategoryUiModel) -> Unit = {}, + categories: List = PlaceCategoryUiModel.entries, +) { + val scrollState = rememberScrollState() + var selectedCategories by remember { + mutableStateOf(emptySet()) + } + + Row( + modifier = + modifier + .horizontalScroll(scrollState) + .padding( + vertical = festabookSpacing.paddingBody2, + horizontal = festabookSpacing.paddingScreenGutter, + ), + horizontalArrangement = Arrangement.spacedBy(festabookSpacing.paddingBody2), + ) { + CategoryChip( + text = stringResource(R.string.map_category_all), + selected = selectedCategories.isEmpty(), + onClick = { + selectedCategories = emptySet() + onDisplayAllClick() + }, + ) + + categories.forEach { category -> + val text = stringResource(category.getTextId()) + CategoryChip( + text = text, + selected = selectedCategories.contains(category), + icon = { + Icon( + painter = painterResource(category.getIconId()), + contentDescription = text, + tint = Color.Unspecified, + modifier = Modifier.size(FilterChipDefaults.IconSize), + ) + }, + onClick = { + selectedCategories = + if (selectedCategories.contains(category)) { + selectedCategories + .filter { it != category } + .toSet() + } else { + selectedCategories + setOf(category) + } + onCategoryClick(category) + }, + ) + } + } +} + +@Composable +private fun CategoryChip( + text: String, + modifier: Modifier = Modifier, + selected: Boolean = false, + icon: @Composable (() -> Unit)? = null, + onClick: () -> Unit = {}, +) { + FilterChip( + selected = selected, + onClick = { + onClick() + }, + modifier = modifier, + label = { + Text( + text = text, + style = FestabookTypography.bodyLarge, + ) + }, + shape = festabookShapes.radiusFull, + colors = + FilterChipDefaults.filterChipColors( + containerColor = FestabookColor.white, + selectedContainerColor = FestabookColor.gray200, + labelColor = FestabookColor.black, + selectedLabelColor = FestabookColor.black, + ), + border = + FilterChipDefaults.filterChipBorder( + enabled = true, + selected = selected, + borderColor = FestabookColor.gray200, + selectedBorderColor = FestabookColor.black, + borderWidth = 2.dp, + selectedBorderWidth = 2.dp, + ), + leadingIcon = icon, + ) +} + +@Composable +@Preview(showBackground = true) +private fun CategoryChipPreview() { + FestabookTheme { + CategoryChip("전체") + } +} + +@Composable +@Preview(showBackground = true) +private fun PlaceCategoryScreenPreview() { + FestabookTheme { + PlaceCategoryScreen() + } +} From d9f23b784627047d2bf2e739f2d4863e4aa7df08 Mon Sep 17 00:00:00 2001 From: oungsi2000 Date: Tue, 2 Dec 2025 23:01:08 +0900 Subject: [PATCH 05/10] =?UTF-8?q?refactor(PlaceCategory):=20=EC=8B=9C?= =?UTF-8?q?=EC=84=A4=20=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20=EC=84=A0?= =?UTF-8?q?=ED=83=9D=20UI=EB=A5=BC=20Compose=EB=A1=9C=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 기존의 ChipGroup과 Chip View를 사용하던 시설 카테고리 선택 UI를 Compose 기반의 `PlaceCategoryScreen`으로 전면 교체했습니다. 이를 통해 UI 로직을 선언적으로 관리하고, 코드의 재사용성과 유지보수성을 향상시켰습니다. - **`PlaceCategoryFragment.kt` 리팩토링:** - 기존의 View(`FragmentPlaceCategoryBinding`) 및 Chip 관련 로직을 모두 제거했습니다. - `onCreateView`에서 `ComposeView`를 반환하도록 변경하고, 내부에 `PlaceCategoryScreen` 컴포저블을 설정했습니다. - 카테고리 클릭(`onCategoryClick`)과 '전체보기' 클릭(`onDisplayAllClick`) 이벤트 발생 시, `PlaceMapViewModel`의 상태를 업데이트하는 로직을 구현했습니다. - 여러 카테고리를 동시에 선택할 수 있도록 콜백 로직을 `List` 타입으로 수정했습니다. - **`PlaceCategoryScreen.kt` 수정:** - 카테고리 클릭 시 단일 `PlaceCategoryUiModel`이 아닌, 선택된 카테고리 목록(`List`)을 반환하도록 `onCategoryClick` 콜백의 시그니처를 변경했습니다. - **`PlaceCategoryUiModel.kt` 수정:** - `enum`으로 정의된 카테고리들의 순서를 논리적으로 재정렬했습니다. - **`FestaBookApp.kt` 수정:** - 개발 중 전역 예외 핸들러(`setGlobalExceptionHandler`) 설정을 임시로 주석 처리했습니다. --- .../placeMap/model/PlaceCategoryUiModel.kt | 14 ++-- .../placeCategory/PlaceCategoryFragment.kt | 70 +++++++++---------- .../component/PlaceCategoryScreen.kt | 4 +- 3 files changed, 44 insertions(+), 44 deletions(-) diff --git a/app/src/main/java/com/daedan/festabook/presentation/placeMap/model/PlaceCategoryUiModel.kt b/app/src/main/java/com/daedan/festabook/presentation/placeMap/model/PlaceCategoryUiModel.kt index bad82c6..5b63718 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/placeMap/model/PlaceCategoryUiModel.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/placeMap/model/PlaceCategoryUiModel.kt @@ -9,14 +9,18 @@ enum class PlaceCategoryUiModel { FOOD_TRUCK, BOOTH, BAR, - TRASH_CAN, - TOILET, - SMOKING_AREA, - PRIMARY, - PARKING, + STAGE, PHOTO_BOOTH, + PRIMARY, + EXTRA, + PARKING, + TOILET, + + SMOKING_AREA, + + TRASH_CAN, ; companion object { diff --git a/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeCategory/PlaceCategoryFragment.kt b/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeCategory/PlaceCategoryFragment.kt index 44db4b8..7b1bf14 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeCategory/PlaceCategoryFragment.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeCategory/PlaceCategoryFragment.kt @@ -1,20 +1,24 @@ package com.daedan.festabook.presentation.placeMap.placeCategory import android.os.Bundle +import android.view.LayoutInflater import android.view.View -import androidx.core.view.children +import android.view.ViewGroup +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 com.daedan.festabook.R import com.daedan.festabook.databinding.FragmentPlaceCategoryBinding +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.placeMap.PlaceMapViewModel import com.daedan.festabook.presentation.placeMap.logging.PlaceCategoryClick import com.daedan.festabook.presentation.placeMap.model.PlaceCategoryUiModel -import com.google.android.material.chip.Chip +import com.daedan.festabook.presentation.placeMap.placeCategory.component.PlaceCategoryScreen +import com.daedan.festabook.presentation.theme.FestabookTheme import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.ContributesIntoMap import dev.zacsweers.metro.Inject @@ -30,42 +34,34 @@ class PlaceCategoryFragment : BaseFragment() { override lateinit var defaultViewModelProviderFactory: ViewModelProvider.Factory private val viewModel: PlaceMapViewModel by viewModels({ requireParentFragment() }) - override fun onViewCreated( - view: View, + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, savedInstanceState: Bundle?, - ) { - super.onViewCreated(view, savedInstanceState) - setUpBinding() - } - - private fun setUpBinding() { - binding.cgCategories.setOnCheckedStateChangeListener { group, checkedIds -> - val selectedCategories = - checkedIds.mapNotNull { - val category = group.findViewById(it).tag - category as? PlaceCategoryUiModel + ): View = + ComposeView(requireContext()).apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + setContent { + FestabookTheme { + val categories = PlaceCategoryUiModel.entries + PlaceCategoryScreen( + categories = categories, + onCategoryClick = { categories -> + viewModel.unselectPlace() + viewModel.setSelectedCategories(categories) + appGraph.defaultFirebaseLogger.log( + PlaceCategoryClick( + baseLogData = appGraph.defaultFirebaseLogger.getBaseLogData(), + currentCategories = categories.joinToString(",") { it.toString() }, + ), + ) + }, + onDisplayAllClick = { + viewModel.unselectPlace() + viewModel.setSelectedCategories(categories) + }, + ) } - - viewModel.unselectPlace() - viewModel.setSelectedCategories(selectedCategories) - binding.chipCategoryAll.isChecked = selectedCategories.isEmpty() - binding.logger.log( - PlaceCategoryClick( - baseLogData = binding.logger.getBaseLogData(), - currentCategories = selectedCategories.joinToString(",") { it.toString() }, - ), - ) - } - - setUpChipCategoryAllListener() - } - - private fun setUpChipCategoryAllListener() { - binding.chipCategoryAll.setOnClickListener { - binding.cgCategories.children.forEach { - val chip = (it as? Chip) ?: return@forEach - chip.isChecked = chip.id == binding.chipCategoryAll.id } } - } } diff --git a/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeCategory/component/PlaceCategoryScreen.kt b/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeCategory/component/PlaceCategoryScreen.kt index 13ce560..16afe36 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeCategory/component/PlaceCategoryScreen.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeCategory/component/PlaceCategoryScreen.kt @@ -35,7 +35,7 @@ import com.daedan.festabook.presentation.theme.festabookSpacing fun PlaceCategoryScreen( modifier: Modifier = Modifier, onDisplayAllClick: () -> Unit = {}, - onCategoryClick: (PlaceCategoryUiModel) -> Unit = {}, + onCategoryClick: (List) -> Unit = {}, categories: List = PlaceCategoryUiModel.entries, ) { val scrollState = rememberScrollState() @@ -84,7 +84,7 @@ fun PlaceCategoryScreen( } else { selectedCategories + setOf(category) } - onCategoryClick(category) + onCategoryClick(selectedCategories.toList()) }, ) } From ba322fa16eaca316929da958ecf350c78628e827 Mon Sep 17 00:00:00 2001 From: oungsi2000 Date: Wed, 3 Dec 2025 00:48:25 +0900 Subject: [PATCH 06/10] =?UTF-8?q?refactor(PlaceCategory):=20=EC=B9=B4?= =?UTF-8?q?=ED=85=8C=EA=B3=A0=EB=A6=AC=20=EC=83=81=ED=83=9C=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=20=EB=B0=A9=EC=8B=9D=EC=9D=84=20=EC=99=B8=EB=B6=80=20?= =?UTF-8?q?=EC=A3=BC=EC=9E=85=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `PlaceCategoryScreen` 컴포저블의 상태 관리 방식을 개선하여, 내부에서 `remember`로 관리하던 `selectedCategories` 상태를 외부에서 주입받도록 변경했습니다. 이를 통해 컴포저블의 재사용성을 높이고, 상태 관리를 상위 컴포저블 또는 Fragment/ViewModel로 위임하여 단일 책임 원칙을 강화했습니다. - **`PlaceCategoryScreen.kt` (컴포저블)** - 내부에서 `mutableStateOf`로 관리하던 `selectedCategories` 상태를 제거했습니다. - `selectedCategories`와 `initialCategories`를 파라미터로 직접 전달받도록 시그니처를 수정했습니다. - 카테고리 클릭(`onCategoryClick`) 및 전체 보기 클릭(`onDisplayAllClick`) 콜백에서 변경된 카테고리 셋(`Set`)을 상위로 전달하도록 수정했습니다. - **`PlaceCategoryFragment.kt` (프래그먼트)** - `PlaceCategoryScreen`에 표시할 `selectedCategoriesState`를 `remember`를 통해 상태로 관리하도록 추가했습니다. - ViewModel의 `selectedTimeTag`가 변경될 때마다, `selectedCategoriesState`가 빈 상태(`emptySet()`)로 초기화되도록 `remember`의 `key`를 설정했습니다. 이는 시간대가 바뀌면 카테고리 선택도 초기화되어야 하는 비즈니스 로직을 반영합니다. - `onCategoryClick`과 `onDisplayAllClick` 콜백을 구현하여, `PlaceCategoryScreen`에서 전달받은 값으로 `selectedCategoriesState`를 업데이트하고 ViewModel의 관련 메서드를 호출하도록 수정했습니다. --- .../placeCategory/PlaceCategoryFragment.kt | 34 +++++++++++++++---- .../component/PlaceCategoryScreen.kt | 27 +++++---------- 2 files changed, 36 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeCategory/PlaceCategoryFragment.kt b/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeCategory/PlaceCategoryFragment.kt index 7b1bf14..4c0e18b 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeCategory/PlaceCategoryFragment.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeCategory/PlaceCategoryFragment.kt @@ -4,11 +4,17 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue 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.asFlow +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.daedan.festabook.R import com.daedan.festabook.databinding.FragmentPlaceCategoryBinding import com.daedan.festabook.di.appGraph @@ -43,22 +49,36 @@ class PlaceCategoryFragment : BaseFragment() { setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent { FestabookTheme { - val categories = PlaceCategoryUiModel.entries + val initialCategories = PlaceCategoryUiModel.entries + // StateFlow로 변경 시 asFlow 제거 예정 + val timeTagChanged = + viewModel.selectedTimeTag + .asFlow() + .collectAsStateWithLifecycle(viewLifecycleOwner) + var selectedCategoriesState by remember(timeTagChanged.value) { + mutableStateOf( + emptySet(), + ) + } + PlaceCategoryScreen( - categories = categories, - onCategoryClick = { categories -> + initialCategories = initialCategories, + selectedCategories = selectedCategoriesState, + onCategoryClick = { selectedCategories -> + selectedCategoriesState = selectedCategories viewModel.unselectPlace() - viewModel.setSelectedCategories(categories) + viewModel.setSelectedCategories(selectedCategories.toList()) appGraph.defaultFirebaseLogger.log( PlaceCategoryClick( baseLogData = appGraph.defaultFirebaseLogger.getBaseLogData(), - currentCategories = categories.joinToString(",") { it.toString() }, + currentCategories = selectedCategories.joinToString(",") { it.toString() }, ), ) }, - onDisplayAllClick = { + onDisplayAllClick = { selectedCategories -> + selectedCategoriesState = selectedCategories viewModel.unselectPlace() - viewModel.setSelectedCategories(categories) + viewModel.setSelectedCategories(initialCategories) }, ) } diff --git a/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeCategory/component/PlaceCategoryScreen.kt b/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeCategory/component/PlaceCategoryScreen.kt index 16afe36..723992d 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeCategory/component/PlaceCategoryScreen.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeCategory/component/PlaceCategoryScreen.kt @@ -11,10 +11,6 @@ import androidx.compose.material3.FilterChipDefaults import androidx.compose.material3.Icon 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.res.painterResource @@ -34,14 +30,12 @@ import com.daedan.festabook.presentation.theme.festabookSpacing @Composable fun PlaceCategoryScreen( modifier: Modifier = Modifier, - onDisplayAllClick: () -> Unit = {}, - onCategoryClick: (List) -> Unit = {}, - categories: List = PlaceCategoryUiModel.entries, + selectedCategories: Set = emptySet(), + onDisplayAllClick: (selectedCategories: Set) -> Unit = {}, + onCategoryClick: (selectedCategories: Set) -> Unit = {}, + initialCategories: List = PlaceCategoryUiModel.entries, ) { val scrollState = rememberScrollState() - var selectedCategories by remember { - mutableStateOf(emptySet()) - } Row( modifier = @@ -57,12 +51,11 @@ fun PlaceCategoryScreen( text = stringResource(R.string.map_category_all), selected = selectedCategories.isEmpty(), onClick = { - selectedCategories = emptySet() - onDisplayAllClick() + onDisplayAllClick(emptySet()) }, ) - categories.forEach { category -> + initialCategories.forEach { category -> val text = stringResource(category.getTextId()) CategoryChip( text = text, @@ -76,15 +69,13 @@ fun PlaceCategoryScreen( ) }, onClick = { - selectedCategories = + val newSelectedCategories = if (selectedCategories.contains(category)) { - selectedCategories - .filter { it != category } - .toSet() + selectedCategories.filter { it != category } } else { selectedCategories + setOf(category) } - onCategoryClick(selectedCategories.toList()) + onCategoryClick(newSelectedCategories.toSet()) }, ) } From 9f6edb01a756b531e960dc453b5fa8ed57d699f2 Mon Sep 17 00:00:00 2001 From: oungsi2000 Date: Wed, 3 Dec 2025 21:32:55 +0900 Subject: [PATCH 07/10] =?UTF-8?q?refactor(PlaceMap):=20Develop=20=EB=B8=8C?= =?UTF-8?q?=EB=9E=9C=EC=B9=98=20=EB=B0=98=EC=98=81,=20MapInterceptorView?= =?UTF-8?q?=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 기존에 `Fragment`에서 콜백 인터페이스(`OnTimeTagSelectedListener`)를 통해 처리하던 시간대(`TimeTag`) 선택 로직을 `ViewModel`이 직접 상태를 관리하고 Compose UI가 이를 구독하는 단방향 데이터 흐름(UDF) 방식으로 리팩토링했습니다. - **`PlaceMapFragment.kt` 리팩토링:** - `OnTimeTagSelectedListener` 인터페이스와 관련 콜백 메서드(`onTimeTagSelected`, `onNothingSelected`)를 제거했습니다. - `TimeTagMenu` 컴포저블의 `onTimeTagClick` 람다 내에서 `ViewModel`의 `onDaySelected`를 직접 호출하도록 변경하여 `Fragment`의 역할을 줄였습니다. - `ViewModel`의 `StateFlow`(`selectedTimeTagFlow`)를 구독하여 `TimeTagMenu`의 제목(`title`)을 동적으로 업데이트하도록 개선했습니다. - 더 이상 사용되지 않는 `MapTouchEventInterceptView`를 XML 레이아웃에서 삭제했습니다. - **`PlaceMapScreen.kt` 수정:** - 외부에서 `title`을 직접 전달받도록 파라미터를 변경하여, 컴포저블이 상태 비저장(Stateless) 방식으로 동작하도록 수정했습니다. - `onTimeTagSelected` 콜백의 이름을 `onTimeTagClick`으로 변경하여 일관성을 높였습니다. --- .../placeMap/MapTouchEventInterceptView.kt | 66 ------------------- .../presentation/placeMap/MapUtil.kt | 14 ---- .../placeMap/OnMapDragListener.kt | 5 -- .../placeMap/OnTimeTagSelectedListener.kt | 9 --- .../presentation/placeMap/PlaceMapFragment.kt | 39 +++++------ .../placeMap/component/PlaceMapScreen.kt | 14 ++-- .../main/res/layout/fragment_place_map.xml | 32 ++++----- 7 files changed, 36 insertions(+), 143 deletions(-) delete mode 100644 app/src/main/java/com/daedan/festabook/presentation/placeMap/MapTouchEventInterceptView.kt delete mode 100644 app/src/main/java/com/daedan/festabook/presentation/placeMap/MapUtil.kt delete mode 100644 app/src/main/java/com/daedan/festabook/presentation/placeMap/OnMapDragListener.kt delete mode 100644 app/src/main/java/com/daedan/festabook/presentation/placeMap/OnTimeTagSelectedListener.kt diff --git a/app/src/main/java/com/daedan/festabook/presentation/placeMap/MapTouchEventInterceptView.kt b/app/src/main/java/com/daedan/festabook/presentation/placeMap/MapTouchEventInterceptView.kt deleted file mode 100644 index 01c1e1a..0000000 --- a/app/src/main/java/com/daedan/festabook/presentation/placeMap/MapTouchEventInterceptView.kt +++ /dev/null @@ -1,66 +0,0 @@ -package com.daedan.festabook.presentation.placeMap - -import android.content.Context -import android.util.AttributeSet -import android.view.GestureDetector -import android.view.MotionEvent -import android.widget.FrameLayout - -class MapTouchEventInterceptView( - context: Context, - attrs: AttributeSet? = null, -) : FrameLayout( - context, - attrs, - ) { - private var onMapDragListener: OnMapDragListener? = null - - private var isMapDragging = false - - private val gestureDetector by lazy { - GestureDetector( - context, - object : GestureDetector.SimpleOnGestureListener() { - override fun onFling( - e1: MotionEvent?, - e2: MotionEvent, - velocityX: Float, - velocityY: Float, - ): Boolean { - if (!isMapDragging) { - onMapDragListener?.onDrag() - isMapDragging = true - } - return super.onFling(e1, e2, velocityX, velocityY) - } - - override fun onScroll( - e1: MotionEvent?, - e2: MotionEvent, - distanceX: Float, - distanceY: Float, - ): Boolean { - if ((distanceY > 0 || distanceX > 0) && !isMapDragging) { - isMapDragging = true - onMapDragListener?.onDrag() - } - return super.onScroll(e1, e2, distanceX, distanceY) - } - }, - ) - } - - override fun onInterceptTouchEvent(event: MotionEvent?): Boolean { - event?.let { - if (it.action == MotionEvent.ACTION_UP) { - isMapDragging = false - } - gestureDetector.onTouchEvent(it) - } - return false - } - - fun setOnMapDragListener(listener: OnMapDragListener) { - onMapDragListener = listener - } -} diff --git a/app/src/main/java/com/daedan/festabook/presentation/placeMap/MapUtil.kt b/app/src/main/java/com/daedan/festabook/presentation/placeMap/MapUtil.kt deleted file mode 100644 index a193bee..0000000 --- a/app/src/main/java/com/daedan/festabook/presentation/placeMap/MapUtil.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.daedan.festabook.presentation.placeMap - -import com.naver.maps.map.MapFragment -import com.naver.maps.map.NaverMap -import kotlinx.coroutines.suspendCancellableCoroutine - -suspend fun MapFragment.getMap() = - suspendCancellableCoroutine { cont -> - getMapAsync { map -> - cont.resumeWith( - Result.success(map), - ) - } - } diff --git a/app/src/main/java/com/daedan/festabook/presentation/placeMap/OnMapDragListener.kt b/app/src/main/java/com/daedan/festabook/presentation/placeMap/OnMapDragListener.kt deleted file mode 100644 index aa98139..0000000 --- a/app/src/main/java/com/daedan/festabook/presentation/placeMap/OnMapDragListener.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.daedan.festabook.presentation.placeMap - -fun interface OnMapDragListener { - fun onDrag() -} diff --git a/app/src/main/java/com/daedan/festabook/presentation/placeMap/OnTimeTagSelectedListener.kt b/app/src/main/java/com/daedan/festabook/presentation/placeMap/OnTimeTagSelectedListener.kt deleted file mode 100644 index fb5eb58..0000000 --- a/app/src/main/java/com/daedan/festabook/presentation/placeMap/OnTimeTagSelectedListener.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.daedan.festabook.presentation.placeMap - -import com.daedan.festabook.domain.model.TimeTag - -interface OnTimeTagSelectedListener { - fun onTimeTagSelected(item: TimeTag) - - fun onNothingSelected() -} diff --git a/app/src/main/java/com/daedan/festabook/presentation/placeMap/PlaceMapFragment.kt b/app/src/main/java/com/daedan/festabook/presentation/placeMap/PlaceMapFragment.kt index 0ab93e4..e14d4c1 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/placeMap/PlaceMapFragment.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/placeMap/PlaceMapFragment.kt @@ -123,19 +123,6 @@ class PlaceMapFragment( mapManager?.moveToPosition() } - override fun onTimeTagSelected(item: TimeTag) { - viewModel.unselectPlace() - viewModel.onDaySelected(item) - binding.logger.log( - PlaceTimeTagSelected( - baseLogData = binding.logger.getBaseLogData(), - timeTagName = item.name, - ), - ) - } - - override fun onNothingSelected() = Unit - private fun setupComposeView() { binding.cvPlaceMap.apply { setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) @@ -143,23 +130,32 @@ class PlaceMapFragment( FestabookTheme { NaverMapContent( modifier = Modifier.fillMaxSize(), + onMapDrag = { viewModel.onMapViewClick() }, onMapReady = { setupMap(it) }, - ) {} + ) { + // TODO 흩어져있는 ComposeView 통합, 추후 PlaceMapScreen 사용 + } } } } binding.cvTimeTagSpinner.apply { setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent { - val timeTags by viewModel.timeTags.collectAsStateWithLifecycle(viewLifecycleOwner) + val timeTags by viewModel.timeTags.collectAsStateWithLifecycle() + val title by viewModel.selectedTimeTagFlow.collectAsStateWithLifecycle() FestabookTheme { - if (!timeTags.isEmpty()) { - val initialTitle = timeTags.first().name + if (timeTags.isNotEmpty()) { TimeTagMenu( - initialTitle = initialTitle, + title = title.name, timeTags = timeTags, - onTimeTagClick = { - onTimeTagSelected(it) + onTimeTagClick = { timeTag -> + viewModel.onDaySelected(timeTag) + binding.logger.log( + PlaceTimeTagSelected( + baseLogData = binding.logger.getBaseLogData(), + timeTagName = timeTag.name, + ), + ) }, modifier = Modifier @@ -184,9 +180,6 @@ class PlaceMapFragment( } (placeListFragment as? OnMapReadyCallback)?.onMapReady(naverMap) naverMap.locationSource = locationSource - binding.viewMapTouchEventIntercept.setOnMapDragListener { - viewModel.onMapViewClick() - } setUpObserver() } diff --git a/app/src/main/java/com/daedan/festabook/presentation/placeMap/component/PlaceMapScreen.kt b/app/src/main/java/com/daedan/festabook/presentation/placeMap/component/PlaceMapScreen.kt index 2541420..af17816 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/placeMap/component/PlaceMapScreen.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/placeMap/component/PlaceMapScreen.kt @@ -15,9 +15,10 @@ import com.naver.maps.map.NaverMap @Composable fun PlaceMapScreen( - onMapReady: (NaverMap) -> Unit, timeTags: List, - onTimeTagSelected: (TimeTag) -> Unit, + title: String, + onMapReady: (NaverMap) -> Unit, + onTimeTagClick: (TimeTag) -> Unit, ) { NaverMapContent( modifier = Modifier.fillMaxSize(), @@ -26,13 +27,12 @@ fun PlaceMapScreen( Column( modifier = Modifier.wrapContentSize(), ) { - if (!timeTags.isEmpty()) { - val initialTitle = timeTags.first().name + if (timeTags.isNotEmpty()) { TimeTagMenu( - initialTitle = initialTitle, + title = title, timeTags = timeTags, - onTimeTagClick = { - onTimeTagSelected(it) + onTimeTagClick = { timeTag -> + onTimeTagClick(timeTag) }, modifier = Modifier diff --git a/app/src/main/res/layout/fragment_place_map.xml b/app/src/main/res/layout/fragment_place_map.xml index 2fabeab..6734ddf 100644 --- a/app/src/main/res/layout/fragment_place_map.xml +++ b/app/src/main/res/layout/fragment_place_map.xml @@ -19,25 +19,19 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> - - - - + - + + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/fcv_place_category_container" /> Date: Wed, 3 Dec 2025 22:19:09 +0900 Subject: [PATCH 08/10] =?UTF-8?q?fix:=20=ED=95=AD=EB=AA=A9=20=EC=97=86?= =?UTF-8?q?=EB=8A=94=20=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20=EC=84=A0?= =?UTF-8?q?=ED=83=9D=20=ED=9B=84,=20=EB=8B=A4=EB=A5=B8=20=EC=B9=B4?= =?UTF-8?q?=ED=85=8C=EA=B3=A0=EB=A6=AC=20=EC=84=A0=ED=83=9D=EC=8B=9C=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=EB=A9=94=EC=8B=9C=EC=A7=80=EA=B0=80=20?= =?UTF-8?q?=EC=82=AC=EB=9D=BC=EC=A7=80=EC=A7=80=20=EC=95=8A=EB=8A=94=20?= =?UTF-8?q?=EB=B2=84=EA=B7=B8=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `PlaceMapFragment`와 `PlaceListFragment`에서 카테고리 선택에 따른 장소 필터링 로직을 리팩토링했습니다. 기존에는 선택된 카테고리(`selectedCategories`)가 비어있는 경우와 그렇지 않은 경우를 분기 처리하여 각각 필터 초기화(`clearFilter`, `clearPlacesFilter`) 또는 필터 적용(`filterMarkersByCategories`, `updatePlacesByCategories`) 메서드를 호출했습니다. 개선된 코드에서는 이 분기 로직을 제거하고, `filterMarkersByCategories`와 `updatePlacesByCategories` 메서드만 호출하도록 변경했습니다. 이는 해당 메서드들이 빈 카테고리 목록을 인자로 받았을 때 '전체 보기'와 동일하게 동작하도록 내부 로직이 구현되어 있음을 전제로 합니다. 이를 통해 코드의 중복을 줄이고 가독성을 높였습니다. - **`PlaceMapFragment.kt` 수정:** - `selectedCategories` 관찰 시, 조건 분기 없이 `mapManager?.filterMarkersByCategories(selectedCategories)`를 직접 호출하도록 변경했습니다. - **`PlaceListFragment.kt` 수정:** - `selectedCategories` 관찰 시, 조건 분기 없이 `childViewModel.updatePlacesByCategories(selectedCategories)`를 직접 호출하도록 변경했습니다. - 장소 목록(`places`)이 비어있지 않을 경우, 에러 메시지(`tvErrorToLoadPlaceInfo`)를 숨기는 로직을 추가했습니다. --- .../festabook/presentation/placeMap/PlaceMapFragment.kt | 6 +----- .../presentation/placeMap/placeList/PlaceListFragment.kt | 8 +++----- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/daedan/festabook/presentation/placeMap/PlaceMapFragment.kt b/app/src/main/java/com/daedan/festabook/presentation/placeMap/PlaceMapFragment.kt index e14d4c1..5cd3835 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/placeMap/PlaceMapFragment.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/placeMap/PlaceMapFragment.kt @@ -228,11 +228,7 @@ class PlaceMapFragment( } viewModel.selectedCategories.observe(viewLifecycleOwner) { selectedCategories -> - if (selectedCategories.isEmpty()) { - mapManager?.clearFilter() - } else { - mapManager?.filterMarkersByCategories(selectedCategories) - } + mapManager?.filterMarkersByCategories(selectedCategories) } viewModel.selectedPlace.observe(viewLifecycleOwner) { selectedPlace -> diff --git a/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeList/PlaceListFragment.kt b/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeList/PlaceListFragment.kt index 8c17bcf..78871db 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeList/PlaceListFragment.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeList/PlaceListFragment.kt @@ -136,6 +136,8 @@ class PlaceListFragment : placeAdapter.submitList(places.value) { if (places.value.isEmpty()) { binding.tvErrorToLoadPlaceInfo.visibility = View.VISIBLE + } else { + binding.tvErrorToLoadPlaceInfo.visibility = View.GONE } binding.rvPlaces.scrollToPosition(0) } @@ -165,11 +167,7 @@ class PlaceListFragment : } viewModel.selectedCategories.observe(viewLifecycleOwner) { selectedCategories -> - if (selectedCategories.isEmpty()) { - childViewModel.clearPlacesFilter() - } else { - childViewModel.updatePlacesByCategories(selectedCategories) - } + childViewModel.updatePlacesByCategories(selectedCategories) } viewModel.isExceededMaxLength.observe(viewLifecycleOwner) { isExceededMaxLength -> From 1b64a1d154eb3141ab2f836e74ad08a553519a41 Mon Sep 17 00:00:00 2001 From: oungsi2000 Date: Wed, 3 Dec 2025 22:42:26 +0900 Subject: [PATCH 09/10] =?UTF-8?q?fix:=20=ED=83=80=EC=9E=84=ED=83=9C?= =?UTF-8?q?=EA=B7=B8=EA=B0=80=20=EC=97=86=EC=9D=84=20=EB=95=8C=20=EC=B9=B4?= =?UTF-8?q?=ED=85=8C=EA=B3=A0=EB=A6=AC=20=EC=84=A0=ED=83=9D=20=EC=8B=9C=20?= =?UTF-8?q?=EB=A7=88=EC=BB=A4=EA=B0=80=20=EB=B3=B4=EC=9D=B4=EC=A7=80=20?= =?UTF-8?q?=EC=95=8A=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 기존에는 선택된 카테고리(`selectedCategories`)가 비어있는 경우와 그렇지 않은 경우를 분기 처리하여 각각 필터 초기화(`clearFilter`, `clearPlacesFilter`) 또는 필터 적용(`filterMarkersByCategories`, `updatePlacesByCategories`) 메서드를 호출했습니다. 개선된 코드에서는 이 분기 로직을 제거하고, `filterMarkersByCategories`와 `updatePlacesByCategories` 메서드만 호출하도록 변경했습니다. 이는 해당 메서드들이 빈 카테고리 목록을 인자로 받았을 때 '전체 보기'와 동일하게 동작하도록 내부 로직이 구현되어 있음을 전제로 합니다. 이를 통해 코드의 중복을 줄이고 가독성을 높였습니다. - **`PlaceMapFragment.kt` 수정:** - `selectedCategories` 관찰 시, 조건 분기 없이 `mapManager?.filterMarkersByCategories(selectedCategories)`를 직접 호출하도록 변경했습니다. - **`PlaceListFragment.kt` 수정:** - `selectedCategories` 관찰 시, 조건 분기 없이 `childViewModel.updatePlacesByCategories(selectedCategories)`를 직접 호출하도록 변경했습니다. - 장소 목록(`places`)이 비어있지 않을 경우, 에러 메시지(`tvErrorToLoadPlaceInfo`)를 숨기는 로직을 추가했습니다. --- .../placeMap/mapManager/MapFilterManager.kt | 2 +- .../internal/MapFilterManagerImpl.kt | 21 ++++++++++++------- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/daedan/festabook/presentation/placeMap/mapManager/MapFilterManager.kt b/app/src/main/java/com/daedan/festabook/presentation/placeMap/mapManager/MapFilterManager.kt index c62a906..ddccdc1 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/placeMap/mapManager/MapFilterManager.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/placeMap/mapManager/MapFilterManager.kt @@ -20,7 +20,7 @@ interface MapFilterManager { * * @param selectedTimeTagId 필터링에 사용할 시간 태그의 ID입니다. null 또는 특정 ID가 될 수 있습니다. */ - fun filterMarkersByTimeTag(selectedTimeTagId: Long?) + fun filterMarkersByTimeTag(selectedTimeTagId: Long) /** * 모든 필터링 조건을 해제하고 마커를 초기 상태로 복원합니다. diff --git a/app/src/main/java/com/daedan/festabook/presentation/placeMap/mapManager/internal/MapFilterManagerImpl.kt b/app/src/main/java/com/daedan/festabook/presentation/placeMap/mapManager/internal/MapFilterManagerImpl.kt index d934fbd..1283ecb 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/placeMap/mapManager/internal/MapFilterManagerImpl.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/placeMap/mapManager/internal/MapFilterManagerImpl.kt @@ -20,25 +20,32 @@ class MapFilterManagerImpl( ) : MapFilterManager { private var selectedMarker: Marker? = null - private var selectedTimeTagId: Long? = null + private var selectedTimeTagId: Long = TimeTag.EMTPY_TIME_TAG_ID override fun filterMarkersByCategories(categories: List) { markers.forEach { marker -> val place = marker.tag as? PlaceCoordinateUiModel ?: return@forEach val isSelectedMarker = marker == selectedMarker - // 필터링된 마커이거나 선택된 마커인 경우에만 보이게 처리 - marker.isVisible = - place.category in categories && - place.timeTagIds.contains(selectedTimeTagId) || - isSelectedMarker + // 필터링된 마커이거나 선택된 마커인 경우에만 보이게 처리, + // 타임태그가 없다면, 타임태그 검사 생략 + if (selectedTimeTagId == TimeTag.EMTPY_TIME_TAG_ID) { + marker.isVisible = + place.category in categories || + isSelectedMarker + } else { + marker.isVisible = + place.category in categories && + place.timeTagIds.contains(selectedTimeTagId) || + isSelectedMarker + } // 선택된 마커는 크기를 유지하고, 필터링되지 않은 마커는 원래 크기로 되돌림 markerManager.setMarkerIcon(marker, isSelectedMarker) } } - override fun filterMarkersByTimeTag(selectedTimeTagId: Long?) { + override fun filterMarkersByTimeTag(selectedTimeTagId: Long) { if (selectedTimeTagId == TimeTag.EMTPY_TIME_TAG_ID) { markers.forEach { it.isVisible = true } return From 39f37c14c512c26fee410de77b7db455148f2501 Mon Sep 17 00:00:00 2001 From: oungsi2000 Date: Wed, 3 Dec 2025 22:55:27 +0900 Subject: [PATCH 10/10] =?UTF-8?q?fix:=20=EC=B9=B4=ED=85=8C=EA=B3=A0?= =?UTF-8?q?=EB=A6=AC=20=EC=84=A0=ED=83=9D=20=ED=9B=84=20=EC=B9=B4=ED=85=8C?= =?UTF-8?q?=EA=B3=A0=EB=A6=AC=20=ED=95=B4=EC=A0=9C=20=EC=8B=9C=20=EB=A7=88?= =?UTF-8?q?=EC=BB=A4,=20=ED=95=9C=20=EB=88=88=EC=97=90=20=EB=B3=B4?= =?UTF-8?q?=EA=B8=B0=EA=B0=80=20=EC=B4=88=EA=B8=B0=ED=99=94=EB=90=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20=ED=95=B4?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 기존에는 선택된 카테고리(`selectedCategories`)가 비어있는 경우와 그렇지 않은 경우를 분기 처리하여 각각 필터 초기화(`clearFilter`, `clearPlacesFilter`) 또는 필터 적용(`filterMarkersByCategories`, `updatePlacesByCategories`) 메서드를 호출했습니다. 개선된 코드에서는 이 분기 로직을 제거하고, `filterMarkersByCategories`와 `updatePlacesByCategories` 메서드만 호출하도록 변경했습니다. 이는 해당 메서드들이 빈 카테고리 목록을 인자로 받았을 때 '전체 보기'와 동일하게 동작하도록 내부 로직이 구현되어 있음을 전제로 합니다. 이를 통해 코드의 중복을 줄이고 가독성을 높였습니다. - **`PlaceMapFragment.kt` 수정:** - `selectedCategories` 관찰 시, 조건 분기 없이 `mapManager?.filterMarkersByCategories(selectedCategories)`를 직접 호출하도록 변경했습니다. - **`PlaceListFragment.kt` 수정:** - `selectedCategories` 관찰 시, 조건 분기 없이 `childViewModel.updatePlacesByCategories(selectedCategories)`를 직접 호출하도록 변경했습니다. - 장소 목록(`places`)이 비어있지 않을 경우, 에러 메시지(`tvErrorToLoadPlaceInfo`)를 숨기는 로직을 추가했습니다. --- .../festabook/presentation/placeMap/PlaceMapFragment.kt | 6 +++++- .../presentation/placeMap/placeList/PlaceListFragment.kt | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/daedan/festabook/presentation/placeMap/PlaceMapFragment.kt b/app/src/main/java/com/daedan/festabook/presentation/placeMap/PlaceMapFragment.kt index 5cd3835..e14d4c1 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/placeMap/PlaceMapFragment.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/placeMap/PlaceMapFragment.kt @@ -228,7 +228,11 @@ class PlaceMapFragment( } viewModel.selectedCategories.observe(viewLifecycleOwner) { selectedCategories -> - mapManager?.filterMarkersByCategories(selectedCategories) + if (selectedCategories.isEmpty()) { + mapManager?.clearFilter() + } else { + mapManager?.filterMarkersByCategories(selectedCategories) + } } viewModel.selectedPlace.observe(viewLifecycleOwner) { selectedPlace -> diff --git a/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeList/PlaceListFragment.kt b/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeList/PlaceListFragment.kt index 78871db..9d62862 100644 --- a/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeList/PlaceListFragment.kt +++ b/app/src/main/java/com/daedan/festabook/presentation/placeMap/placeList/PlaceListFragment.kt @@ -167,7 +167,11 @@ class PlaceListFragment : } viewModel.selectedCategories.observe(viewLifecycleOwner) { selectedCategories -> - childViewModel.updatePlacesByCategories(selectedCategories) + if (selectedCategories.isEmpty()) { + childViewModel.clearPlacesFilter() + } else { + childViewModel.updatePlacesByCategories(selectedCategories) + } } viewModel.isExceededMaxLength.observe(viewLifecycleOwner) { isExceededMaxLength ->