From 39f8a13d8ef445188ae8208db31a91cf44771efa Mon Sep 17 00:00:00 2001 From: Ojongseok Date: Sat, 24 Jan 2026 23:24:22 +0900 Subject: [PATCH 01/37] =?UTF-8?q?[chore]=20#51=20=EB=93=9C=EB=9E=98?= =?UTF-8?q?=EA=B7=B8=20=EA=B4=80=EB=A0=A8=20=EC=83=81=EC=88=98=20=EB=84=A4?= =?UTF-8?q?=EC=9D=B4=EB=B0=8D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../android/feature/map/impl/MapContract.kt | 6 +-- .../android/feature/map/impl/MapScreen.kt | 8 ++-- .../android/feature/map/impl/MapViewModel.kt | 12 +++--- .../impl/component/AnchoredDraggablePanel.kt | 40 ++++++++----------- 4 files changed, 30 insertions(+), 36 deletions(-) diff --git a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapContract.kt b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapContract.kt index 515741848..af818f7e2 100644 --- a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapContract.kt +++ b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapContract.kt @@ -8,7 +8,7 @@ import kotlinx.collections.immutable.persistentListOf data class MapState( val isLoading: Boolean = false, - val dragState: DragValue = DragValue.Bottom, + val dragLevel: DragLevel = DragLevel.FIRST, val currentLocation: Pair = Pair(0.0, 0.0), val brands: ImmutableList = persistentListOf(), val nearbyBrands: ImmutableList = persistentListOf(), @@ -42,7 +42,7 @@ sealed interface MapIntent { data object ClickCloseBrandCard : MapIntent data object CloseDirectionBottomSheet : MapIntent data class ClickDirectionItem(val app: DirectionApp) : MapIntent - data class ChangeDragValue(val dragValue: DragValue) : MapIntent + data class ChangeDragLevel(val dragLevel: DragLevel) : MapIntent } sealed interface MapEffect { @@ -58,4 +58,4 @@ sealed interface MapEffect { ) : MapEffect } -enum class DragValue { Bottom, Center, Top, Invisible } +enum class DragLevel { FIRST, SECOND, THIRD, INVISIBLE } diff --git a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapScreen.kt b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapScreen.kt index f90e5bded..94f7ff6a6 100644 --- a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapScreen.kt +++ b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapScreen.kt @@ -192,8 +192,8 @@ fun MapScreen( AnchoredDraggablePanel( brands = uiState.brands, nearbyBrands = uiState.nearbyBrands, - dragValue = uiState.dragState, - onDragValueChanged = { onIntent(MapIntent.ChangeDragValue(it)) }, + dragLevel = uiState.dragLevel, + onDragLevelChanged = { onIntent(MapIntent.ChangeDragLevel(it)) }, onClickInfoIcon = { onIntent(MapIntent.ClickInfoIcon) }, isCurrentLocation = locationTrackingMode == LocationTrackingMode.Follow, onClickCurrentLocation = { @@ -203,14 +203,14 @@ fun MapScreen( onClickNearBrand = { onIntent(MapIntent.ClickNearBrand(it)) }, ) - if (uiState.dragState == DragValue.Top) { + if (uiState.dragLevel == DragLevel.THIRD) { ToMapChip( modifier = Modifier .align(Alignment.BottomCenter) .padding(bottom = 32.dp), onClick = { onIntent(MapIntent.ClickToMapChip) }, ) - } else if (uiState.dragState == DragValue.Invisible && uiState.selectedBrandInfo != null) { + } else if (uiState.dragLevel == DragLevel.INVISIBLE && uiState.selectedBrandInfo != null) { PanelInvisibleContent( brandInfo = uiState.selectedBrandInfo, modifier = Modifier.align(Alignment.BottomCenter), diff --git a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapViewModel.kt b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapViewModel.kt index 9bb1214c2..cc832c7df 100644 --- a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapViewModel.kt +++ b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapViewModel.kt @@ -44,7 +44,7 @@ class MapViewModel @Inject constructor() : ViewModel() { } MapIntent.ClickToMapChip -> { - reduce { copy(dragState = DragValue.Bottom) } + reduce { copy(dragLevel = DragLevel.FIRST) } } is MapIntent.ClickBrand -> { @@ -64,7 +64,7 @@ class MapViewModel @Inject constructor() : ViewModel() { is MapIntent.ClickNearBrand -> { reduce { copy( - dragState = DragValue.Invisible, + dragLevel = DragLevel.INVISIBLE, selectedBrandInfo = intent.brandInfo, focusedMarkerPosition = intent.brandInfo.latitude to intent.brandInfo.longitude, ) @@ -73,7 +73,7 @@ class MapViewModel @Inject constructor() : ViewModel() { } MapIntent.ClickCloseBrandCard -> { - reduce { copy(dragState = DragValue.Center, focusedMarkerPosition = Pair(0.0, 0.0), selectedBrandInfo = null) } + reduce { copy(dragLevel = DragLevel.SECOND, focusedMarkerPosition = Pair(0.0, 0.0), selectedBrandInfo = null) } } MapIntent.CloseDirectionBottomSheet -> { @@ -85,15 +85,15 @@ class MapViewModel @Inject constructor() : ViewModel() { postSideEffect(MapEffect.MoveDirectionApp(intent.app)) } - is MapIntent.ChangeDragValue -> { - reduce { copy(dragState = intent.dragValue) } + is MapIntent.ChangeDragLevel -> { + reduce { copy(dragLevel = intent.dragLevel) } } is MapIntent.ClickBrandMarker -> { val selectedBrand = state.nearbyBrands.find { it.latitude == intent.latitude && it.longitude == intent.longitude } reduce { copy( - dragState = DragValue.Invisible, + dragLevel = DragLevel.INVISIBLE, focusedMarkerPosition = intent.latitude to intent.longitude, selectedBrandInfo = selectedBrand, ) diff --git a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/AnchoredDraggablePanel.kt b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/AnchoredDraggablePanel.kt index a7ed170ef..d64c8efaf 100644 --- a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/AnchoredDraggablePanel.kt +++ b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/AnchoredDraggablePanel.kt @@ -54,7 +54,7 @@ import com.neki.android.core.designsystem.ui.theme.NekiTheme import com.neki.android.core.model.Brand import com.neki.android.core.model.BrandInfo import com.neki.android.core.ui.compose.VerticalSpacer -import com.neki.android.feature.map.impl.DragValue +import com.neki.android.feature.map.impl.DragLevel import com.neki.android.feature.map.impl.const.MapConst import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -64,9 +64,9 @@ import kotlin.math.roundToInt internal fun AnchoredDraggablePanel( brands: ImmutableList = persistentListOf(), nearbyBrands: ImmutableList = persistentListOf(), - dragValue: DragValue = DragValue.Bottom, + dragLevel: DragLevel = DragLevel.FIRST, isCurrentLocation: Boolean = false, - onDragValueChanged: (DragValue) -> Unit = {}, + onDragLevelChanged: (DragLevel) -> Unit = {}, onClickCurrentLocation: () -> Unit = {}, onClickInfoIcon: () -> Unit = {}, onClickBrand: (Brand) -> Unit = {}, @@ -101,27 +101,27 @@ internal fun AnchoredDraggablePanel( } AnchoredDraggableState( - initialValue = DragValue.Bottom, + initialValue = DragLevel.FIRST, anchors = anchors, positionalThreshold = { distance -> distance * 0.5f }, velocityThreshold = { with(density) { 100.dp.toPx() } }, snapAnimationSpec = tween(), decayAnimationSpec = splineBasedDecay(density), confirmValueChange = { newValue -> - newValue != DragValue.Invisible || isProgrammaticTransition + newValue != DragLevel.INVISIBLE || isProgrammaticTransition }, ) } LaunchedEffect(state.settledValue) { - if (state.settledValue != dragValue) { - onDragValueChanged(state.settledValue) + if (state.settledValue != dragLevel) { + onDragLevelChanged(state.settledValue) } } - LaunchedEffect(dragValue) { + LaunchedEffect(dragLevel) { isProgrammaticTransition = true - state.animateTo(dragValue) + state.animateTo(dragLevel) isProgrammaticTransition = false } @@ -130,9 +130,9 @@ internal fun AnchoredDraggablePanel( .fillMaxSize() .offset { val currentOffset = state.requireOffset() - val shouldConstrainOffset = state.currentValue == DragValue.Bottom && !isProgrammaticTransition + val shouldConstrainOffset = state.currentValue == DragLevel.FIRST && !isProgrammaticTransition val constrainedOffset = if (shouldConstrainOffset) { - currentOffset.coerceAtMost(state.anchors.positionOf(DragValue.Bottom)) + currentOffset.coerceAtMost(state.anchors.positionOf(DragLevel.FIRST)) } else { currentOffset } @@ -147,15 +147,14 @@ internal fun AnchoredDraggablePanel( CurrentLocationButton( modifier = Modifier .padding(start = 20.dp, bottom = 12.dp) - .alpha(alpha = if (dragValue == DragValue.Top) 0f else 1f), + .alpha(alpha = if (dragLevel == DragLevel.THIRD) 0f else 1f), isActiveCurrentLocation = isCurrentLocation, onClick = onClickCurrentLocation, ) AnchoredPanelContent( brands = brands, nearbyBrands = nearbyBrands, - dragValue = dragValue, - onCollapsedHeightMeasured = { collapsedHeightPx = it }, + dragLevel = dragLevel, onClickInfoIcon = onClickInfoIcon, onClickBrand = onClickBrand, onClickNearBrand = onClickNearBrand, @@ -168,22 +167,17 @@ internal fun AnchoredDraggablePanel( internal fun AnchoredPanelContent( brands: ImmutableList = persistentListOf(), nearbyBrands: ImmutableList = persistentListOf(), - dragValue: DragValue = DragValue.Bottom, - onCollapsedHeightMeasured: (Int) -> Unit = {}, + dragLevel: DragLevel = DragLevel.FIRST, onClickInfoIcon: () -> Unit = {}, onClickBrand: (Brand) -> Unit = {}, onClickNearBrand: (BrandInfo) -> Unit = {}, ) { - val density = LocalDensity.current val configuration = LocalConfiguration.current val screenHeightDp = configuration.screenHeightDp.dp - /** 패널 외부 상단 현위치 버튼 영역 **/ - val additionalHeightPx = with(density) { (24.dp + 12.dp).toPx().toInt() } - - val extraBottomPadding = when (dragValue) { - DragValue.Center -> screenHeightDp * 0.3f - DragValue.Top -> screenHeightDp * 0.05f + val extraBottomPadding = when (dragLevel) { + DragLevel.SECOND -> screenHeightDp * 0.3f + DragLevel.THIRD -> screenHeightDp * 0.05f else -> 0.dp } From 99149f52301e09a070b6ad934a87bac9e362681f Mon Sep 17 00:00:00 2001 From: Ojongseok Date: Sat, 24 Jan 2026 23:24:40 +0900 Subject: [PATCH 02/37] =?UTF-8?q?[design]=20#51=20BottomNavigationBar=20?= =?UTF-8?q?=EB=94=94=EC=9E=90=EC=9D=B8=20=EB=B3=80=EA=B2=BD=EC=82=AC?= =?UTF-8?q?=ED=95=AD=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/neki/android/app/ui/BottomNavigationBar.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/neki/android/app/ui/BottomNavigationBar.kt b/app/src/main/java/com/neki/android/app/ui/BottomNavigationBar.kt index 46696d134..99e2ef3ab 100644 --- a/app/src/main/java/com/neki/android/app/ui/BottomNavigationBar.kt +++ b/app/src/main/java/com/neki/android/app/ui/BottomNavigationBar.kt @@ -86,9 +86,9 @@ fun BottomNavigationBarItem( color = NekiTheme.colorScheme.white, ) { Column( - modifier = Modifier.padding(vertical = 8.dp), + modifier = Modifier.padding(vertical = 4.dp), horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(1.dp), + verticalArrangement = Arrangement.spacedBy(4.dp), ) { Icon( modifier = Modifier.size(24.dp), @@ -99,7 +99,7 @@ fun BottomNavigationBarItem( Text( text = stringResource(tab.iconStringRes), color = textColor, - style = NekiTheme.typography.caption11SemiBold, + style = NekiTheme.typography.caption12SemiBold, ) } } From 0c2d70eb296297db0473898a9b9a9d46ee2a750c Mon Sep 17 00:00:00 2001 From: Ojongseok Date: Sat, 24 Jan 2026 23:26:34 +0900 Subject: [PATCH 03/37] =?UTF-8?q?[refactor]=20#51=20AnchoredDraggablePanel?= =?UTF-8?q?=20=EB=93=9C=EB=9E=98=EA=B7=B8=20=EB=A0=88=EB=B2=A8=EB=B3=84=20?= =?UTF-8?q?=EB=86=92=EC=9D=B4=20=EC=A1=B0=EC=A0=88=20=EB=B0=A9=EC=8B=9D=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../impl/component/AnchoredDraggablePanel.kt | 38 ++++++++----------- 1 file changed, 15 insertions(+), 23 deletions(-) diff --git a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/AnchoredDraggablePanel.kt b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/AnchoredDraggablePanel.kt index d64c8efaf..bdd205956 100644 --- a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/AnchoredDraggablePanel.kt +++ b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/AnchoredDraggablePanel.kt @@ -30,7 +30,6 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue @@ -39,7 +38,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.vectorResource @@ -76,28 +74,27 @@ internal fun AnchoredDraggablePanel( val configuration = LocalConfiguration.current val screenHeightPx = with(density) { configuration.screenHeightDp.dp.toPx() } - var collapsedHeightPx by remember { mutableIntStateOf(0) } val navigationBarHeightPx = with(density) { WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding().toPx() } - val bottomOffsetPx = remember(collapsedHeightPx, navigationBarHeightPx) { - with(density) { - collapsedHeightPx + - // currentLocationButton (36.dp + 12.dp) - 48.dp.toPx() + - MapConst.BOTTOM_NAVIGATION_BAR_HEIGHT.dp.toPx() + - navigationBarHeightPx - } + val bottomPanelHeightPx = with(density) { + (MapConst.BOTTOM_NAVIGATION_BAR_HEIGHT + + MapConst.PANEL_DRAG_LOCATION_HEIGHT + + MapConst.PANEL_DRAG_LEVEL_BOTTOM_HEIGHT).dp.toPx() + navigationBarHeightPx + } + val centerPanelHeightPx = with(density) { + (MapConst.BOTTOM_NAVIGATION_BAR_HEIGHT + + MapConst.PANEL_DRAG_LOCATION_HEIGHT + + MapConst.PANEL_DRAG_LEVEL_CENTER_HEIGHT).dp.toPx() + navigationBarHeightPx } - var isProgrammaticTransition by remember { mutableStateOf(false) } - val state = remember(collapsedHeightPx) { + val state = remember { val anchors = DraggableAnchors { - DragValue.Bottom at screenHeightPx - bottomOffsetPx - DragValue.Center at screenHeightPx * 0.3f - DragValue.Top at screenHeightPx * 0.05f - DragValue.Invisible at screenHeightPx + DragLevel.FIRST at screenHeightPx - bottomPanelHeightPx + DragLevel.SECOND at screenHeightPx - centerPanelHeightPx + DragLevel.THIRD at screenHeightPx * 0.05f + DragLevel.INVISIBLE at screenHeightPx } AnchoredDraggableState( @@ -191,12 +188,7 @@ internal fun AnchoredPanelContent( ), ) { Column( - modifier = Modifier - .fillMaxWidth() - .onGloballyPositioned { - val newHeight = it.size.height + additionalHeightPx - onCollapsedHeightMeasured(newHeight) - }, + modifier = Modifier.fillMaxWidth(), ) { BottomSheetDragHandle() Text( From a1b32086dd36c1d681aa9f1ca06fcce4009706c9 Mon Sep 17 00:00:00 2001 From: Ojongseok Date: Sat, 24 Jan 2026 23:39:57 +0900 Subject: [PATCH 04/37] =?UTF-8?q?[feat]=20#51=20'=ED=98=84=20=EC=9C=84?= =?UTF-8?q?=EC=B9=98=EC=97=90=EC=84=9C=20=ED=83=90=EC=83=89'=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/res/drawable/icon_rotation.xml | 20 ++++++ .../map/impl/component/MapRefreshChip.kt | 68 +++++++++++++++++++ .../feature/map/impl/component/ToMapChip.kt | 6 +- 3 files changed, 92 insertions(+), 2 deletions(-) create mode 100644 core/designsystem/src/main/res/drawable/icon_rotation.xml create mode 100644 feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/MapRefreshChip.kt diff --git a/core/designsystem/src/main/res/drawable/icon_rotation.xml b/core/designsystem/src/main/res/drawable/icon_rotation.xml new file mode 100644 index 000000000..b9ad21315 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/icon_rotation.xml @@ -0,0 +1,20 @@ + + + + diff --git a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/MapRefreshChip.kt b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/MapRefreshChip.kt new file mode 100644 index 000000000..57463b404 --- /dev/null +++ b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/MapRefreshChip.kt @@ -0,0 +1,68 @@ +package com.neki.android.feature.map.impl.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +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.shape.CircleShape +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.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.unit.dp +import com.neki.android.core.designsystem.ComponentPreview +import com.neki.android.core.designsystem.R +import com.neki.android.core.designsystem.modifier.buttonShadow +import com.neki.android.core.designsystem.modifier.clickableSingle +import com.neki.android.core.designsystem.ui.theme.NekiTheme + +@Composable +internal fun MapRefreshChip( + modifier: Modifier = Modifier, + onClick: () -> Unit = {}, +) { + Row( + modifier = modifier + .clickableSingle(onClick = onClick) + .buttonShadow() + .background( + shape = CircleShape, + color = NekiTheme.colorScheme.white + ) + .border( + width = 1.dp, + shape = CircleShape, + color = NekiTheme.colorScheme.gray100 + ) + .padding(horizontal = 16.dp, vertical = 8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + modifier = Modifier.size(16.dp), + imageVector = ImageVector.vectorResource(R.drawable.icon_rotation), + contentDescription = null, + tint = NekiTheme.colorScheme.primary400, + ) + Text( + text = "현 위치에서 탐색", + style = NekiTheme.typography.body14SemiBold, + color = NekiTheme.colorScheme.gray800, + ) + } +} + +@ComponentPreview +@Composable +private fun MapRefreshChipPreview() { + NekiTheme { + MapRefreshChip() + } +} diff --git a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/ToMapChip.kt b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/ToMapChip.kt index 0142dc01d..0b9a3cacb 100644 --- a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/ToMapChip.kt +++ b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/ToMapChip.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -17,6 +18,7 @@ import androidx.compose.ui.res.vectorResource import androidx.compose.ui.unit.dp import com.neki.android.core.designsystem.ComponentPreview import com.neki.android.core.designsystem.R +import com.neki.android.core.designsystem.modifier.buttonShadow import com.neki.android.core.designsystem.modifier.clickableSingle import com.neki.android.core.designsystem.ui.theme.NekiTheme @@ -27,8 +29,8 @@ internal fun ToMapChip( ) { Row( modifier = modifier - .clip(CircleShape) .clickableSingle(onClick = onClick) + .buttonShadow() .background(shape = CircleShape, color = NekiTheme.colorScheme.gray800) .padding(horizontal = 16.dp, vertical = 8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp), @@ -51,6 +53,6 @@ internal fun ToMapChip( @Composable private fun ToMapChipPreview() { NekiTheme { - ToMapChip(modifier = Modifier.padding(8.dp)) + ToMapChip() } } From 60e8997563559b4e22e872ddf453845a251e036a Mon Sep 17 00:00:00 2001 From: Ojongseok Date: Sat, 24 Jan 2026 23:42:43 +0900 Subject: [PATCH 05/37] =?UTF-8?q?[feat]=20#51=20=EC=A7=80=EB=8F=84=20?= =?UTF-8?q?=EB=B2=84=ED=8A=BC=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20.no?= =?UTF-8?q?RippleClickableSingle=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 --- .../neki/android/feature/map/impl/component/MapRefreshChip.kt | 3 ++- .../com/neki/android/feature/map/impl/component/ToMapChip.kt | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/MapRefreshChip.kt b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/MapRefreshChip.kt index 57463b404..6b6f1c9bf 100644 --- a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/MapRefreshChip.kt +++ b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/MapRefreshChip.kt @@ -21,6 +21,7 @@ import com.neki.android.core.designsystem.ComponentPreview import com.neki.android.core.designsystem.R import com.neki.android.core.designsystem.modifier.buttonShadow import com.neki.android.core.designsystem.modifier.clickableSingle +import com.neki.android.core.designsystem.modifier.noRippleClickableSingle import com.neki.android.core.designsystem.ui.theme.NekiTheme @Composable @@ -30,7 +31,7 @@ internal fun MapRefreshChip( ) { Row( modifier = modifier - .clickableSingle(onClick = onClick) + .noRippleClickableSingle(onClick = onClick) .buttonShadow() .background( shape = CircleShape, diff --git a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/ToMapChip.kt b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/ToMapChip.kt index 0b9a3cacb..05c4af520 100644 --- a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/ToMapChip.kt +++ b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/ToMapChip.kt @@ -20,6 +20,7 @@ import com.neki.android.core.designsystem.ComponentPreview import com.neki.android.core.designsystem.R import com.neki.android.core.designsystem.modifier.buttonShadow import com.neki.android.core.designsystem.modifier.clickableSingle +import com.neki.android.core.designsystem.modifier.noRippleClickableSingle import com.neki.android.core.designsystem.ui.theme.NekiTheme @Composable @@ -29,7 +30,7 @@ internal fun ToMapChip( ) { Row( modifier = modifier - .clickableSingle(onClick = onClick) + .noRippleClickableSingle(onClick = onClick) .buttonShadow() .background(shape = CircleShape, color = NekiTheme.colorScheme.gray800) .padding(horizontal = 16.dp, vertical = 8.dp), From bf53d054d012535b75df40da9868b6bba528dfe4 Mon Sep 17 00:00:00 2001 From: Ojongseok Date: Sat, 24 Jan 2026 23:45:04 +0900 Subject: [PATCH 06/37] =?UTF-8?q?[feat]=20#51=20=ED=95=98=EB=8B=A8=20?= =?UTF-8?q?=ED=8C=A8=EB=84=90=20DragLevel.THIRD=20=EC=83=81=ED=83=9C=20?= =?UTF-8?q?=EB=86=92=EC=9D=B4=20=ED=99=94=EB=A9=B4=2090%=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../feature/map/impl/component/AnchoredDraggablePanel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/AnchoredDraggablePanel.kt b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/AnchoredDraggablePanel.kt index bdd205956..1eedb603e 100644 --- a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/AnchoredDraggablePanel.kt +++ b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/AnchoredDraggablePanel.kt @@ -174,7 +174,7 @@ internal fun AnchoredPanelContent( val extraBottomPadding = when (dragLevel) { DragLevel.SECOND -> screenHeightDp * 0.3f - DragLevel.THIRD -> screenHeightDp * 0.05f + DragLevel.THIRD -> screenHeightDp * 0.1f else -> 0.dp } From d1240e8c337540721bd4d1fed8e3131b10fe0dc5 Mon Sep 17 00:00:00 2001 From: Ojongseok Date: Sat, 24 Jan 2026 23:51:22 +0900 Subject: [PATCH 07/37] =?UTF-8?q?[chore]=20#51=20=EB=B6=88=ED=95=84?= =?UTF-8?q?=EC=9A=94=ED=95=9C=20=ED=8C=A8=EB=84=90=20=EB=86=92=EC=9D=B4=20?= =?UTF-8?q?=EB=B3=80=EC=88=98=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../map/impl/component/AnchoredDraggablePanel.kt | 13 +------------ .../neki/android/feature/map/impl/const/MapConst.kt | 5 ++++- 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/AnchoredDraggablePanel.kt b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/AnchoredDraggablePanel.kt index 1eedb603e..9dc80f8da 100644 --- a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/AnchoredDraggablePanel.kt +++ b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/AnchoredDraggablePanel.kt @@ -151,7 +151,6 @@ internal fun AnchoredDraggablePanel( AnchoredPanelContent( brands = brands, nearbyBrands = nearbyBrands, - dragLevel = dragLevel, onClickInfoIcon = onClickInfoIcon, onClickBrand = onClickBrand, onClickNearBrand = onClickNearBrand, @@ -164,20 +163,10 @@ internal fun AnchoredDraggablePanel( internal fun AnchoredPanelContent( brands: ImmutableList = persistentListOf(), nearbyBrands: ImmutableList = persistentListOf(), - dragLevel: DragLevel = DragLevel.FIRST, onClickInfoIcon: () -> Unit = {}, onClickBrand: (Brand) -> Unit = {}, onClickNearBrand: (BrandInfo) -> Unit = {}, ) { - val configuration = LocalConfiguration.current - val screenHeightDp = configuration.screenHeightDp.dp - - val extraBottomPadding = when (dragLevel) { - DragLevel.SECOND -> screenHeightDp * 0.3f - DragLevel.THIRD -> screenHeightDp * 0.1f - else -> 0.dp - } - Column( modifier = Modifier .fillMaxSize() @@ -238,7 +227,7 @@ internal fun AnchoredPanelContent( .weight(1f) .padding(horizontal = 20.dp), verticalArrangement = Arrangement.spacedBy(16.dp), - contentPadding = PaddingValues(bottom = extraBottomPadding), + contentPadding = PaddingValues(bottom = MapConst.BOTTOM_NAVIGATION_BAR_HEIGHT.dp), ) { items(nearbyBrands) { brandInfo -> HorizontalBrandItem( diff --git a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/const/MapConst.kt b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/const/MapConst.kt index 348c103db..359b53e9b 100644 --- a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/const/MapConst.kt +++ b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/const/MapConst.kt @@ -1,7 +1,10 @@ package com.neki.android.feature.map.impl.const internal object MapConst { - internal const val BOTTOM_NAVIGATION_BAR_HEIGHT = 72 + internal const val BOTTOM_NAVIGATION_BAR_HEIGHT = 52 + internal const val PANEL_DRAG_LOCATION_HEIGHT = 48 + internal const val PANEL_DRAG_LEVEL_BOTTOM_HEIGHT = 96 + internal const val PANEL_DRAG_LEVEL_CENTER_HEIGHT = 218 // 마커 internal const val MARKER_BACKGROUND_RADIUS = 20 From d2fd2c98de0b162c3efd0fa890ea2bf755fb446a Mon Sep 17 00:00:00 2001 From: Ojongseok Date: Sat, 24 Jan 2026 23:52:01 +0900 Subject: [PATCH 08/37] =?UTF-8?q?[feat]=20#51=20DragLevel=20First,=20Secon?= =?UTF-8?q?d=20=EC=83=81=ED=83=9C=EC=9D=98=20'=ED=98=84=20=EC=9C=84?= =?UTF-8?q?=EC=B9=98=EC=97=90=EC=84=9C=20=ED=83=90=EC=83=89'=20=EB=B2=84?= =?UTF-8?q?=ED=8A=BC=20=EB=85=B8=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/neki/android/feature/map/impl/MapScreen.kt | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapScreen.kt b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapScreen.kt index 94f7ff6a6..b00d53d11 100644 --- a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapScreen.kt +++ b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapScreen.kt @@ -4,6 +4,7 @@ import android.widget.Toast import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -38,6 +39,7 @@ import com.neki.android.core.ui.compose.collectWithLifecycle import com.neki.android.feature.map.impl.component.AnchoredDraggablePanel import com.neki.android.feature.map.impl.component.BrandMarker import com.neki.android.feature.map.impl.component.DirectionBottomSheet +import com.neki.android.feature.map.impl.component.MapRefreshChip import com.neki.android.feature.map.impl.component.PanelInvisibleContent import com.neki.android.feature.map.impl.component.ToMapChip import com.neki.android.feature.map.impl.const.DirectionApp @@ -203,6 +205,15 @@ fun MapScreen( onClickNearBrand = { onIntent(MapIntent.ClickNearBrand(it)) }, ) + if (uiState.dragLevel == DragLevel.FIRST || uiState.dragLevel == DragLevel.SECOND) { + MapRefreshChip( + modifier = Modifier + .align(Alignment.TopCenter) + .statusBarsPadding() + .padding(top = 12.dp), + ) + } + if (uiState.dragLevel == DragLevel.THIRD) { ToMapChip( modifier = Modifier From 01c3df2738ceb73d3616aad8ee80af55862ce66d Mon Sep 17 00:00:00 2001 From: Ojongseok Date: Sun, 25 Jan 2026 00:08:26 +0900 Subject: [PATCH 09/37] =?UTF-8?q?[feat]=20#51=20NaverMap=20isZoomControlEn?= =?UTF-8?q?abled=20=EC=86=8D=EC=84=B1=20false=EB=A1=9C=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/com/neki/android/feature/map/impl/MapScreen.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapScreen.kt b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapScreen.kt index b00d53d11..f26f4d5a3 100644 --- a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapScreen.kt +++ b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapScreen.kt @@ -158,6 +158,7 @@ fun MapScreen( val mapUiSettings = remember { MapUiSettings( isLocationButtonEnabled = false, + isZoomControlEnabled = false, ) } From 3ba09fa51121a8c517b5bc90a1d18036602140b5 Mon Sep 17 00:00:00 2001 From: Ojongseok Date: Sun, 25 Jan 2026 00:09:08 +0900 Subject: [PATCH 10/37] =?UTF-8?q?[feat]=20#51=20'=ED=98=84=20=EC=9C=84?= =?UTF-8?q?=EC=B9=98=EC=97=90=EC=84=9C=20=ED=83=90=EC=83=89'=20=EB=B2=84?= =?UTF-8?q?=ED=8A=BC=20UI=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../android/feature/map/impl/MapContract.kt | 3 ++ .../android/feature/map/impl/MapScreen.kt | 38 +++++++++++-------- .../android/feature/map/impl/MapViewModel.kt | 4 ++ .../map/impl/component/MapRefreshChip.kt | 3 +- .../feature/map/impl/component/ToMapChip.kt | 8 +++- 5 files changed, 37 insertions(+), 19 deletions(-) diff --git a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapContract.kt b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapContract.kt index af818f7e2..ab4af376b 100644 --- a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapContract.kt +++ b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapContract.kt @@ -32,6 +32,8 @@ sealed interface MapIntent { val longitude: Double, ) : MapIntent + data object ClickRefresh : MapIntent + // in 패널 data object ClickCurrentLocation : MapIntent data object ClickInfoIcon : MapIntent @@ -46,6 +48,7 @@ sealed interface MapIntent { } sealed interface MapEffect { + data object RefreshPhotoBooth : MapEffect data object RefreshCurrentLocation : MapEffect data class ShowToastMessage(val message: String) : MapEffect data class MoveCameraToPosition( diff --git a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapScreen.kt b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapScreen.kt index f26f4d5a3..a57dc07ac 100644 --- a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapScreen.kt +++ b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapScreen.kt @@ -65,6 +65,9 @@ fun MapRoute( viewModel.store.sideEffects.collectWithLifecycle { sideEffect -> when (sideEffect) { + is MapEffect.RefreshPhotoBooth -> { + // TODO: 포토부스 새로고침 로직 구현 + } is MapEffect.RefreshCurrentLocation -> { locationTrackingMode = LocationTrackingMode.Follow } @@ -122,21 +125,6 @@ fun MapRoute( onLocationTrackingModeChange = { locationTrackingMode = it }, cameraPositionState = cameraPositionState, ) - - if (uiState.isShowInfoDialog) { - WarningDialog( - content = "가까운 네컷 사진 브랜드는\n1km 기준으로 표시돼요.", - onDismissRequest = { viewModel.store.onIntent(MapIntent.ClickCloseInfoIcon) }, - properties = DialogProperties(usePlatformDefaultWidth = false), - ) - } - - if (uiState.isShowDirectionBottomSheet) { - DirectionBottomSheet( - onDismissRequest = { viewModel.store.onIntent(MapIntent.CloseDirectionBottomSheet) }, - onClickDirectionItem = { viewModel.store.onIntent(MapIntent.ClickDirectionItem(it)) }, - ) - } } @OptIn(ExperimentalNaverMapApi::class) @@ -206,12 +194,15 @@ fun MapScreen( onClickNearBrand = { onIntent(MapIntent.ClickNearBrand(it)) }, ) - if (uiState.dragLevel == DragLevel.FIRST || uiState.dragLevel == DragLevel.SECOND) { + if ((uiState.dragLevel == DragLevel.FIRST || uiState.dragLevel == DragLevel.SECOND) && + locationTrackingMode != LocationTrackingMode.Follow + ) { MapRefreshChip( modifier = Modifier .align(Alignment.TopCenter) .statusBarsPadding() .padding(top = 12.dp), + onClick = { onIntent(MapIntent.ClickRefresh) }, ) } @@ -233,6 +224,21 @@ fun MapScreen( ) } } + + if (uiState.isShowInfoDialog) { + WarningDialog( + content = "가까운 네컷 사진 브랜드는\n1km 기준으로 표시돼요.", + onDismissRequest = { onIntent(MapIntent.ClickCloseInfoIcon) }, + properties = DialogProperties(usePlatformDefaultWidth = false), + ) + } + + if (uiState.isShowDirectionBottomSheet) { + DirectionBottomSheet( + onDismissRequest = { onIntent(MapIntent.CloseDirectionBottomSheet) }, + onClickDirectionItem = { onIntent(MapIntent.ClickDirectionItem(it)) }, + ) + } } @Preview diff --git a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapViewModel.kt b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapViewModel.kt index cc832c7df..92c51fd63 100644 --- a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapViewModel.kt +++ b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapViewModel.kt @@ -31,6 +31,10 @@ class MapViewModel @Inject constructor() : ViewModel() { loadBrands(reduce) } + MapIntent.ClickRefresh -> { + postSideEffect(MapEffect.RefreshPhotoBooth) + } + MapIntent.ClickCurrentLocation -> { postSideEffect(MapEffect.RefreshCurrentLocation) } diff --git a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/MapRefreshChip.kt b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/MapRefreshChip.kt index 6b6f1c9bf..ce3f9bca3 100644 --- a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/MapRefreshChip.kt +++ b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/MapRefreshChip.kt @@ -31,8 +31,9 @@ internal fun MapRefreshChip( ) { Row( modifier = modifier - .noRippleClickableSingle(onClick = onClick) .buttonShadow() + .clip(CircleShape) + .clickableSingle(onClick = onClick) .background( shape = CircleShape, color = NekiTheme.colorScheme.white diff --git a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/ToMapChip.kt b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/ToMapChip.kt index 05c4af520..c9635dc33 100644 --- a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/ToMapChip.kt +++ b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/ToMapChip.kt @@ -30,9 +30,13 @@ internal fun ToMapChip( ) { Row( modifier = modifier - .noRippleClickableSingle(onClick = onClick) .buttonShadow() - .background(shape = CircleShape, color = NekiTheme.colorScheme.gray800) + .clip(CircleShape) + .clickableSingle(onClick = onClick) + .background( + shape = CircleShape, + color = NekiTheme.colorScheme.gray800 + ) .padding(horizontal = 16.dp, vertical = 8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically, From 8c906ccaa35d58a0f6263d7dab62cbd2e073682d Mon Sep 17 00:00:00 2001 From: Ojongseok Date: Sun, 25 Jan 2026 01:20:33 +0900 Subject: [PATCH 11/37] =?UTF-8?q?[feat]=20#51=20=ED=98=84=EC=9C=84?= =?UTF-8?q?=EC=B9=98=EC=97=90=20=EB=94=B0=EB=A5=B8=20=EA=B8=B8=EC=B0=BE?= =?UTF-8?q?=EA=B8=B0=20=EC=95=B1=20=EC=9D=B4=EB=8F=99=20=EB=A1=9C=EC=A7=81?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../android/feature/map/impl/MapContract.kt | 8 +++++++- .../android/feature/map/impl/MapScreen.kt | 19 ++++++++++++++----- .../android/feature/map/impl/MapViewModel.kt | 12 +++++++++++- 3 files changed, 32 insertions(+), 7 deletions(-) diff --git a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapContract.kt b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapContract.kt index ab4af376b..5f6e52fa0 100644 --- a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapContract.kt +++ b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapContract.kt @@ -8,8 +8,8 @@ import kotlinx.collections.immutable.persistentListOf data class MapState( val isLoading: Boolean = false, - val dragLevel: DragLevel = DragLevel.FIRST, val currentLocation: Pair = Pair(0.0, 0.0), + val dragLevel: DragLevel = DragLevel.FIRST, val brands: ImmutableList = persistentListOf(), val nearbyBrands: ImmutableList = persistentListOf(), val focusedMarkerPosition: Pair = Pair(0.0, 0.0), @@ -33,6 +33,7 @@ sealed interface MapIntent { ) : MapIntent data object ClickRefresh : MapIntent + data class UpdateCurrentLocation(val latitude: Double, val longitude: Double) : MapIntent // in 패널 data object ClickCurrentLocation : MapIntent @@ -58,7 +59,12 @@ sealed interface MapEffect { data class MoveDirectionApp( val app: DirectionApp, + val startLatitude: Double, + val startLongitude: Double, + val endLatitude: Double, + val endLongitude: Double, ) : MapEffect + data object NavigateToAppSettings : MapEffect } enum class DragLevel { FIRST, SECOND, THIRD, INVISIBLE } diff --git a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapScreen.kt b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapScreen.kt index a57dc07ac..ea48295ad 100644 --- a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapScreen.kt +++ b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapScreen.kt @@ -1,5 +1,9 @@ package com.neki.android.feature.map.impl +import android.Manifest +import android.content.Intent +import android.net.Uri +import android.provider.Settings import android.widget.Toast import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize @@ -87,21 +91,26 @@ fun MapRoute( } } is MapEffect.MoveDirectionApp -> { + val startLatitude = sideEffect.startLatitude + val startLongitude = sideEffect.startLongitude + val endLatitude = sideEffect.endLatitude + val endLongitude = sideEffect.endLongitude + when (sideEffect.app) { DirectionApp.GOOGLE_MAP -> { DirectionHelper.moveAppOrStore( context = context, - url = "google.navigation:q=37.5256372,126.8862648(${context.getPlaceName(37.5256372, 126.8861924, "도착지")})&mode=w", + url = "google.navigation:q=$endLatitude,$endLongitude&mode=w", packageName = sideEffect.app.packageName, ) } DirectionApp.NAVER_MAP -> { - val startName = context.getPlaceName(37.5270539, 126.8862648, "출발지") - val destName = context.getPlaceName(37.5256372, 126.8861924, "도착지") + val startName = context.getPlaceName(startLatitude, startLongitude, "출발지") + val destName = context.getPlaceName(endLatitude, endLongitude, "도착지") DirectionHelper.moveAppOrStore( context = context, - url = "nmap://route/walk?slat=37.5270539&slng=126.8862648&sname=$startName&dlat=37.5256372&dlng=126.8861924&dname=$destName", + url = "nmap://route/walk?slat=$startLatitude&slng=$startLongitude&sname=$startName&dlat=$endLatitude&dlng=$endLongitude&dname=$destName", packageName = sideEffect.app.packageName, ) } @@ -109,7 +118,7 @@ fun MapRoute( DirectionApp.KAKAO_MAP -> { DirectionHelper.moveAppOrStore( context = context, - url = "kakaomap://route?sp=37.5270539,126.8862648&ep=37.5256372,126.8861924&by=FOOT", + url = "kakaomap://route?sp=$startLatitude,$startLongitude&ep=$endLatitude,$endLongitude&by=FOOT", packageName = sideEffect.app.packageName, ) } diff --git a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapViewModel.kt b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapViewModel.kt index 92c51fd63..4747aba55 100644 --- a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapViewModel.kt +++ b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapViewModel.kt @@ -86,7 +86,17 @@ class MapViewModel @Inject constructor() : ViewModel() { is MapIntent.ClickDirectionItem -> { reduce { copy(isShowDirectionBottomSheet = false) } - postSideEffect(MapEffect.MoveDirectionApp(intent.app)) + state.selectedBrandInfo?.let { brandInfo -> + postSideEffect( + MapEffect.MoveDirectionApp( + app = intent.app, + startLatitude = state.currentLocation.first, + startLongitude = state.currentLocation.second, + endLatitude = brandInfo.latitude, + endLongitude = brandInfo.longitude, + ), + ) + } } is MapIntent.ChangeDragLevel -> { From 8c5f0d5eb20386311e146621f34fcf2fb0ac1bc9 Mon Sep 17 00:00:00 2001 From: Ojongseok Date: Sun, 25 Jan 2026 01:21:06 +0900 Subject: [PATCH 12/37] =?UTF-8?q?[feat]=20#51=20core:data-api=20dataStore?= =?UTF-8?q?=20=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/data-api/build.gradle.kts | 2 +- .../main/java/com/neki/android/feature/map/impl/MapContract.kt | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/core/data-api/build.gradle.kts b/core/data-api/build.gradle.kts index 8ff0f0e1f..2cbcec861 100644 --- a/core/data-api/build.gradle.kts +++ b/core/data-api/build.gradle.kts @@ -5,5 +5,5 @@ plugins { dependencies { implementation(projects.core.model) implementation(libs.kotlinx.coroutines.core) - + api(libs.androidx.datastore.preferences) } \ No newline at end of file diff --git a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapContract.kt b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapContract.kt index 5f6e52fa0..62a4a13e2 100644 --- a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapContract.kt +++ b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapContract.kt @@ -64,7 +64,9 @@ sealed interface MapEffect { val endLatitude: Double, val endLongitude: Double, ) : MapEffect + data object NavigateToAppSettings : MapEffect + data object RequestLocationPermission : MapEffect } enum class DragLevel { FIRST, SECOND, THIRD, INVISIBLE } From 38a07138ef6e5d741d179f2964cadad50e79bce8 Mon Sep 17 00:00:00 2001 From: Ojongseok Date: Sun, 25 Jan 2026 01:21:34 +0900 Subject: [PATCH 13/37] =?UTF-8?q?[feat]=20#51=20feat:map:impl=20activity-c?= =?UTF-8?q?ompose=20=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- feature/map/impl/build.gradle.kts | 1 + 1 file changed, 1 insertion(+) diff --git a/feature/map/impl/build.gradle.kts b/feature/map/impl/build.gradle.kts index 406a4850a..00053ab7d 100644 --- a/feature/map/impl/build.gradle.kts +++ b/feature/map/impl/build.gradle.kts @@ -13,4 +13,5 @@ dependencies { implementation(libs.kotlinx.collections.immutable) implementation(libs.coil.compose) + implementation(libs.androidx.activity.compose) } From 3cbe6c9721f850f0d9a46db7340305ddb97243cb Mon Sep 17 00:00:00 2001 From: Ojongseok Date: Sun, 25 Jan 2026 01:22:28 +0900 Subject: [PATCH 14/37] =?UTF-8?q?[feat]=20#51=20Boolean=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=EC=9D=84=20=EC=A0=80=EC=9E=A5=ED=95=A0=20DataStoreRep?= =?UTF-8?q?ository=20=EC=9D=B8=ED=84=B0=ED=8E=98=EC=9D=B4=EC=8A=A4=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/dataapi/repository/DataStoreRepository.kt | 10 +++++++++- .../data/repository/impl/DataStoreRepositoryImpl.kt | 12 ++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/core/data-api/src/main/java/com/neki/android/core/dataapi/repository/DataStoreRepository.kt b/core/data-api/src/main/java/com/neki/android/core/dataapi/repository/DataStoreRepository.kt index 74c19b4e5..c3bdac32d 100644 --- a/core/data-api/src/main/java/com/neki/android/core/dataapi/repository/DataStoreRepository.kt +++ b/core/data-api/src/main/java/com/neki/android/core/dataapi/repository/DataStoreRepository.kt @@ -1,5 +1,7 @@ package com.neki.android.core.dataapi.repository +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.booleanPreferencesKey import kotlinx.coroutines.flow.Flow interface DataStoreRepository { @@ -7,9 +9,15 @@ interface DataStoreRepository { accessToken: String, refreshToken: String, ) - fun isSavedJwtTokens(): Flow fun getAccessToken(): Flow fun getRefreshToken(): Flow suspend fun clearTokens() + + suspend fun setBoolean(key: Preferences.Key, value: Boolean) + fun getBoolean(key: Preferences.Key): Flow + + companion object { + val IS_FIRST_LOCATION_PERMISSION = booleanPreferencesKey("is_first_location_permission") + } } diff --git a/core/data/src/main/java/com/neki/android/core/data/repository/impl/DataStoreRepositoryImpl.kt b/core/data/src/main/java/com/neki/android/core/data/repository/impl/DataStoreRepositoryImpl.kt index 3d02411da..4eaea2188 100644 --- a/core/data/src/main/java/com/neki/android/core/data/repository/impl/DataStoreRepositoryImpl.kt +++ b/core/data/src/main/java/com/neki/android/core/data/repository/impl/DataStoreRepositoryImpl.kt @@ -52,4 +52,16 @@ class DataStoreRepositoryImpl @Inject constructor( override suspend fun clearTokens() { dataStore.edit { it.clear() } } + + override suspend fun setBoolean(key: Preferences.Key, value: Boolean) { + dataStore.edit { preferences -> + preferences[key] = value + } + } + + override fun getBoolean(key: Preferences.Key): Flow { + return dataStore.data.map { preferences -> + preferences[key] ?: false + } + } } From 6f5c98356283ce557187ab6689570f6ae7b43774 Mon Sep 17 00:00:00 2001 From: Ojongseok Date: Sun, 25 Jan 2026 01:27:28 +0900 Subject: [PATCH 15/37] =?UTF-8?q?[feat]=20#51=20DataStore=20Key=20?= =?UTF-8?q?=EC=A0=95=EC=9D=98=20=EA=B2=BD=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 --- .../core/dataapi/datastore/DataStoreKey.kt | 11 +++++++++++ .../dataapi/repository/DataStoreRepository.kt | 5 ----- .../repository/impl/DataStoreRepositoryImpl.kt | 18 +++++++----------- 3 files changed, 18 insertions(+), 16 deletions(-) create mode 100644 core/data-api/src/main/java/com/neki/android/core/dataapi/datastore/DataStoreKey.kt diff --git a/core/data-api/src/main/java/com/neki/android/core/dataapi/datastore/DataStoreKey.kt b/core/data-api/src/main/java/com/neki/android/core/dataapi/datastore/DataStoreKey.kt new file mode 100644 index 000000000..c2d3a66b4 --- /dev/null +++ b/core/data-api/src/main/java/com/neki/android/core/dataapi/datastore/DataStoreKey.kt @@ -0,0 +1,11 @@ +package com.neki.android.core.dataapi.datastore + +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.stringPreferencesKey + +object DataStoreKey { + val ACCESS_TOKEN = stringPreferencesKey("access_token") + val REFRESH_TOKEN = stringPreferencesKey("refresh_token") + + val IS_FIRST_LOCATION_PERMISSION = booleanPreferencesKey("is_first_location_permission") +} diff --git a/core/data-api/src/main/java/com/neki/android/core/dataapi/repository/DataStoreRepository.kt b/core/data-api/src/main/java/com/neki/android/core/dataapi/repository/DataStoreRepository.kt index c3bdac32d..4d9139fe0 100644 --- a/core/data-api/src/main/java/com/neki/android/core/dataapi/repository/DataStoreRepository.kt +++ b/core/data-api/src/main/java/com/neki/android/core/dataapi/repository/DataStoreRepository.kt @@ -1,7 +1,6 @@ package com.neki.android.core.dataapi.repository import androidx.datastore.preferences.core.Preferences -import androidx.datastore.preferences.core.booleanPreferencesKey import kotlinx.coroutines.flow.Flow interface DataStoreRepository { @@ -16,8 +15,4 @@ interface DataStoreRepository { suspend fun setBoolean(key: Preferences.Key, value: Boolean) fun getBoolean(key: Preferences.Key): Flow - - companion object { - val IS_FIRST_LOCATION_PERMISSION = booleanPreferencesKey("is_first_location_permission") - } } diff --git a/core/data/src/main/java/com/neki/android/core/data/repository/impl/DataStoreRepositoryImpl.kt b/core/data/src/main/java/com/neki/android/core/data/repository/impl/DataStoreRepositoryImpl.kt index 4eaea2188..2ca093994 100644 --- a/core/data/src/main/java/com/neki/android/core/data/repository/impl/DataStoreRepositoryImpl.kt +++ b/core/data/src/main/java/com/neki/android/core/data/repository/impl/DataStoreRepositoryImpl.kt @@ -3,8 +3,8 @@ package com.neki.android.core.data.repository.impl import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.edit -import androidx.datastore.preferences.core.stringPreferencesKey import com.neki.android.core.common.crypto.CryptoManager +import com.neki.android.core.dataapi.datastore.DataStoreKey import com.neki.android.core.dataapi.repository.DataStoreRepository import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map @@ -13,25 +13,21 @@ import javax.inject.Inject class DataStoreRepositoryImpl @Inject constructor( private val dataStore: DataStore, ) : DataStoreRepository { - companion object { - private val ACCESS_TOKEN = stringPreferencesKey("access_token") - private val REFRESH_TOKEN = stringPreferencesKey("refresh_token") - } override suspend fun saveJwtTokens( accessToken: String, refreshToken: String, ) { dataStore.edit { preferences -> - preferences[ACCESS_TOKEN] = CryptoManager.encrypt(accessToken) - preferences[REFRESH_TOKEN] = CryptoManager.encrypt(refreshToken) + preferences[DataStoreKey.ACCESS_TOKEN] = CryptoManager.encrypt(accessToken) + preferences[DataStoreKey.REFRESH_TOKEN] = CryptoManager.encrypt(refreshToken) } } override fun isSavedJwtTokens(): Flow { return dataStore.data.map { preferences -> - val accessToken = preferences[ACCESS_TOKEN]?.let { CryptoManager.decrypt(it) } - val refreshToken = preferences[REFRESH_TOKEN]?.let { CryptoManager.decrypt(it) } + val accessToken = preferences[DataStoreKey.ACCESS_TOKEN]?.let { CryptoManager.decrypt(it) } + val refreshToken = preferences[DataStoreKey.REFRESH_TOKEN]?.let { CryptoManager.decrypt(it) } !accessToken.isNullOrBlank() && !refreshToken.isNullOrBlank() } @@ -39,13 +35,13 @@ class DataStoreRepositoryImpl @Inject constructor( override fun getAccessToken(): Flow { return dataStore.data.map { preferences -> - preferences[ACCESS_TOKEN]?.let { CryptoManager.decrypt(it) } + preferences[DataStoreKey.ACCESS_TOKEN]?.let { CryptoManager.decrypt(it) } } } override fun getRefreshToken(): Flow { return dataStore.data.map { preferences -> - preferences[REFRESH_TOKEN]?.let { CryptoManager.decrypt(it) } + preferences[DataStoreKey.REFRESH_TOKEN]?.let { CryptoManager.decrypt(it) } } } From 39d7e5408ed49672a9c5d271322c8dad7cbf65a0 Mon Sep 17 00:00:00 2001 From: Ojongseok Date: Sun, 25 Jan 2026 01:30:29 +0900 Subject: [PATCH 16/37] =?UTF-8?q?[feat]=20#51=20accessToken,=20refreshToke?= =?UTF-8?q?n=20=EC=A1=B0=ED=9A=8C=20=EB=A1=9C=EC=A7=81=20nonNull=20?= =?UTF-8?q?=ED=83=80=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 --- .../core/dataapi/repository/DataStoreRepository.kt | 4 ++-- .../core/data/repository/impl/DataStoreRepositoryImpl.kt | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/core/data-api/src/main/java/com/neki/android/core/dataapi/repository/DataStoreRepository.kt b/core/data-api/src/main/java/com/neki/android/core/dataapi/repository/DataStoreRepository.kt index 4d9139fe0..c02b46bbf 100644 --- a/core/data-api/src/main/java/com/neki/android/core/dataapi/repository/DataStoreRepository.kt +++ b/core/data-api/src/main/java/com/neki/android/core/dataapi/repository/DataStoreRepository.kt @@ -9,8 +9,8 @@ interface DataStoreRepository { refreshToken: String, ) fun isSavedJwtTokens(): Flow - fun getAccessToken(): Flow - fun getRefreshToken(): Flow + fun getAccessToken(): Flow + fun getRefreshToken(): Flow suspend fun clearTokens() suspend fun setBoolean(key: Preferences.Key, value: Boolean) diff --git a/core/data/src/main/java/com/neki/android/core/data/repository/impl/DataStoreRepositoryImpl.kt b/core/data/src/main/java/com/neki/android/core/data/repository/impl/DataStoreRepositoryImpl.kt index 2ca093994..7027ee242 100644 --- a/core/data/src/main/java/com/neki/android/core/data/repository/impl/DataStoreRepositoryImpl.kt +++ b/core/data/src/main/java/com/neki/android/core/data/repository/impl/DataStoreRepositoryImpl.kt @@ -33,15 +33,15 @@ class DataStoreRepositoryImpl @Inject constructor( } } - override fun getAccessToken(): Flow { + override fun getAccessToken(): Flow { return dataStore.data.map { preferences -> - preferences[DataStoreKey.ACCESS_TOKEN]?.let { CryptoManager.decrypt(it) } + preferences[DataStoreKey.ACCESS_TOKEN]?.let { CryptoManager.decrypt(it) } ?: "" } } - override fun getRefreshToken(): Flow { + override fun getRefreshToken(): Flow { return dataStore.data.map { preferences -> - preferences[DataStoreKey.REFRESH_TOKEN]?.let { CryptoManager.decrypt(it) } + preferences[DataStoreKey.REFRESH_TOKEN]?.let { CryptoManager.decrypt(it) } ?: "" } } From c601a611166a04652614c1d0ba3c41a234fc848c Mon Sep 17 00:00:00 2001 From: Ojongseok Date: Sun, 25 Jan 2026 01:37:53 +0900 Subject: [PATCH 17/37] =?UTF-8?q?[feat]=20#51=20=EB=84=A4=EC=BB=B7?= =?UTF-8?q?=EC=A7=80=EB=8F=84=20=EC=9C=84=EC=B9=98=EA=B6=8C=ED=95=9C=20?= =?UTF-8?q?=ED=99=95=EC=9D=B8=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/data/remote/di/NetworkModule.kt | 7 +- .../feature/auth/impl/LoginViewModel.kt | 3 +- .../android/feature/map/impl/MapContract.kt | 6 ++ .../android/feature/map/impl/MapScreen.kt | 76 ++++++++++++++++++- .../android/feature/map/impl/MapViewModel.kt | 60 ++++++++++++--- 5 files changed, 135 insertions(+), 17 deletions(-) diff --git a/core/data/src/main/java/com/neki/android/core/data/remote/di/NetworkModule.kt b/core/data/src/main/java/com/neki/android/core/data/remote/di/NetworkModule.kt index 5791ea53b..93751a12b 100644 --- a/core/data/src/main/java/com/neki/android/core/data/remote/di/NetworkModule.kt +++ b/core/data/src/main/java/com/neki/android/core/data/remote/di/NetworkModule.kt @@ -33,7 +33,6 @@ import io.ktor.http.HttpHeaders import io.ktor.http.encodedPath import io.ktor.serialization.kotlinx.json.json import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.firstOrNull import kotlinx.serialization.json.Json import timber.log.Timber import javax.inject.Singleton @@ -82,8 +81,8 @@ internal object NetworkModule { Timber.d("BearerAuth - loadTokens") if (dataStoreRepository.isSavedJwtTokens().first()) { BearerTokens( - accessToken = dataStoreRepository.getAccessToken().firstOrNull() ?: "", - refreshToken = dataStoreRepository.getRefreshToken().firstOrNull() ?: "", + accessToken = dataStoreRepository.getAccessToken().first(), + refreshToken = dataStoreRepository.getRefreshToken().first(), ) } else null } @@ -95,7 +94,7 @@ internal object NetworkModule { val response = client.post("/api/auth/refresh") { setBody( RefreshTokenRequest( - refreshToken = dataStoreRepository.getRefreshToken().firstOrNull() ?: "", + refreshToken = dataStoreRepository.getRefreshToken().first(), ), ) }.body>() diff --git a/feature/auth/impl/src/main/kotlin/com/neki/android/feature/auth/impl/LoginViewModel.kt b/feature/auth/impl/src/main/kotlin/com/neki/android/feature/auth/impl/LoginViewModel.kt index 2f17b3892..f850089c1 100644 --- a/feature/auth/impl/src/main/kotlin/com/neki/android/feature/auth/impl/LoginViewModel.kt +++ b/feature/auth/impl/src/main/kotlin/com/neki/android/feature/auth/impl/LoginViewModel.kt @@ -9,7 +9,6 @@ import com.neki.android.core.ui.MviIntentStore import com.neki.android.core.ui.mviIntentStore import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject @@ -47,7 +46,7 @@ class LoginViewModel @Inject constructor( if (dataStoreRepository.isSavedJwtTokens().first()) { Timber.d("JWT 토큰 O") authRepository.updateAccessToken( - refreshToken = dataStoreRepository.getRefreshToken().firstOrNull() ?: "", + refreshToken = dataStoreRepository.getRefreshToken().first(), ).onSuccess { postSideEffect(LoginSideEffect.NavigateToHome) }.onFailure { diff --git a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapContract.kt b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapContract.kt index 62a4a13e2..5190cb83c 100644 --- a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapContract.kt +++ b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapContract.kt @@ -16,6 +16,7 @@ data class MapState( val selectedBrandInfo: BrandInfo? = null, val isShowInfoDialog: Boolean = false, val isShowDirectionBottomSheet: Boolean = false, + val isShowLocationPermissionDialog: Boolean = false, ) sealed interface MapIntent { @@ -46,6 +47,11 @@ sealed interface MapIntent { data object CloseDirectionBottomSheet : MapIntent data class ClickDirectionItem(val app: DirectionApp) : MapIntent data class ChangeDragLevel(val dragLevel: DragLevel) : MapIntent + + // 위치 권한 + data class RequestLocationPermission(val shouldShowRationale: Boolean) : MapIntent + data object DismissLocationPermissionDialog : MapIntent + data object ConfirmLocationPermissionDialog : MapIntent } sealed interface MapEffect { diff --git a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapScreen.kt b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapScreen.kt index ea48295ad..390c6c518 100644 --- a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapScreen.kt +++ b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapScreen.kt @@ -5,6 +5,9 @@ import android.content.Intent import android.net.Uri import android.provider.Settings import android.widget.Toast +import androidx.activity.compose.LocalActivity +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding @@ -23,6 +26,8 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.window.DialogProperties +import androidx.core.content.ContextCompat +import androidx.core.content.PermissionChecker import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.naver.maps.geometry.LatLng @@ -37,6 +42,7 @@ import com.naver.maps.map.compose.MapUiSettings import com.naver.maps.map.compose.NaverMap import com.naver.maps.map.compose.rememberCameraPositionState import com.naver.maps.map.compose.rememberFusedLocationSource +import com.neki.android.core.designsystem.dialog.SingleButtonAlertDialog import com.neki.android.core.designsystem.dialog.WarningDialog import com.neki.android.core.designsystem.ui.theme.NekiTheme import com.neki.android.core.ui.compose.collectWithLifecycle @@ -57,12 +63,34 @@ fun MapRoute( ) { val uiState by viewModel.store.uiState.collectAsStateWithLifecycle() val context = LocalContext.current + val activity = LocalActivity.current!! val coroutineScope = rememberCoroutineScope() var locationTrackingMode by remember { mutableStateOf(LocationTrackingMode.None) } val cameraPositionState = rememberCameraPositionState { position = CameraPosition(LatLng(37.5269278, 126.886225), 17.0) } + val locationPermissionLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.RequestMultiplePermissions(), + ) { permissions -> + val isGranted = permissions.values.any { it } + if (isGranted) { + locationTrackingMode = LocationTrackingMode.Follow + } + } + + fun hasLocationPermission(): Boolean { + return ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PermissionChecker.PERMISSION_GRANTED || + ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) == PermissionChecker.PERMISSION_GRANTED + } + + fun requestLocationPermission() { + val shouldShowRationale = activity.shouldShowRequestPermissionRationale( + Manifest.permission.ACCESS_FINE_LOCATION, + ) + viewModel.store.onIntent(MapIntent.RequestLocationPermission(shouldShowRationale)) + } + LaunchedEffect(Unit) { viewModel.store.onIntent(MapIntent.EnterMapScreen) } @@ -124,6 +152,22 @@ fun MapRoute( } } } + + is MapEffect.NavigateToAppSettings -> { + val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { + data = Uri.fromParts("package", context.packageName, null) + } + context.startActivity(intent) + } + + is MapEffect.RequestLocationPermission -> { + locationPermissionLauncher.launch( + arrayOf( + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION, + ), + ) + } } } @@ -133,6 +177,8 @@ fun MapRoute( locationTrackingMode = locationTrackingMode, onLocationTrackingModeChange = { locationTrackingMode = it }, cameraPositionState = cameraPositionState, + onRequestLocationPermission = { requestLocationPermission() }, + hasLocationPermission = { hasLocationPermission() }, ) } @@ -146,6 +192,8 @@ fun MapScreen( cameraPositionState: CameraPositionState = rememberCameraPositionState { position = CameraPosition(LatLng(37.5269278, 126.886225), 17.0) }, + onRequestLocationPermission: () -> Unit = {}, + hasLocationPermission: () -> Boolean = { false }, ) { val mapProperties = remember(locationTrackingMode) { MapProperties( @@ -173,6 +221,9 @@ fun MapScreen( onLocationTrackingModeChange(it) } }, + onLocationChange = { location -> + onIntent(MapIntent.UpdateCurrentLocation(location.latitude, location.longitude)) + }, ) { uiState.nearbyBrands.forEachIndexed { index, brandInfo -> val isFocused = uiState.focusedMarkerPosition == (brandInfo.latitude to brandInfo.longitude) @@ -197,7 +248,11 @@ fun MapScreen( onClickInfoIcon = { onIntent(MapIntent.ClickInfoIcon) }, isCurrentLocation = locationTrackingMode == LocationTrackingMode.Follow, onClickCurrentLocation = { - onLocationTrackingModeChange(LocationTrackingMode.Follow) + if (hasLocationPermission()) { + onLocationTrackingModeChange(LocationTrackingMode.Follow) + } else { + onRequestLocationPermission() + } }, onClickBrand = { onIntent(MapIntent.ClickBrand(it)) }, onClickNearBrand = { onIntent(MapIntent.ClickNearBrand(it)) }, @@ -227,7 +282,13 @@ fun MapScreen( brandInfo = uiState.selectedBrandInfo, modifier = Modifier.align(Alignment.BottomCenter), isCurrentLocation = locationTrackingMode == LocationTrackingMode.Follow, - onClickCurrentLocation = { onIntent(MapIntent.ClickCurrentLocation) }, + onClickCurrentLocation = { + if (hasLocationPermission()) { + onIntent(MapIntent.ClickCurrentLocation) + } else { + onRequestLocationPermission() + } + }, onClickCloseCard = { onIntent(MapIntent.ClickCloseBrandCard) }, onClickDirection = { onIntent(MapIntent.ClickDirection(uiState.selectedBrandInfo.latitude, uiState.selectedBrandInfo.longitude)) }, ) @@ -248,6 +309,17 @@ fun MapScreen( onClickDirectionItem = { onIntent(MapIntent.ClickDirectionItem(it)) }, ) } + + if (uiState.isShowLocationPermissionDialog) { + SingleButtonAlertDialog( + title = "위치 권한", + content = "설정에서 위치 권한을 허용해주세요.", + buttonText = "확인", + onDismissRequest = { onIntent(MapIntent.DismissLocationPermissionDialog) }, + onClick = { onIntent(MapIntent.ConfirmLocationPermissionDialog) }, + properties = DialogProperties(usePlatformDefaultWidth = false), + ) + } } @Preview diff --git a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapViewModel.kt b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapViewModel.kt index 4747aba55..31966f0ad 100644 --- a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapViewModel.kt +++ b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapViewModel.kt @@ -2,6 +2,8 @@ package com.neki.android.feature.map.impl import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.neki.android.core.dataapi.datastore.DataStoreKey +import com.neki.android.core.dataapi.repository.DataStoreRepository import com.neki.android.core.designsystem.R import com.neki.android.core.model.Brand import com.neki.android.core.model.BrandInfo @@ -10,11 +12,15 @@ import com.neki.android.core.ui.mviIntentStore import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject @HiltViewModel -class MapViewModel @Inject constructor() : ViewModel() { +class MapViewModel @Inject constructor( + private val dataStoreRepository: DataStoreRepository, +) : ViewModel() { val store: MviIntentStore = mviIntentStore( initialState = MapState(), onIntent = ::onIntent, @@ -35,6 +41,10 @@ class MapViewModel @Inject constructor() : ViewModel() { postSideEffect(MapEffect.RefreshPhotoBooth) } + is MapIntent.UpdateCurrentLocation -> { + reduce { copy(currentLocation = intent.latitude to intent.longitude) } + } + MapIntent.ClickCurrentLocation -> { postSideEffect(MapEffect.RefreshCurrentLocation) } @@ -118,6 +128,40 @@ class MapViewModel @Inject constructor() : ViewModel() { is MapIntent.ClickDirection -> { reduce { copy(isShowDirectionBottomSheet = true) } } + + is MapIntent.RequestLocationPermission -> { + viewModelScope.launch { + val isFirstRequest = dataStoreRepository.getBoolean(DataStoreKey.IS_FIRST_LOCATION_PERMISSION).first().not() + + when { + isFirstRequest -> { + Timber.d("최초 요청 - 시스템 권한 팝업 표시") + dataStoreRepository.setBoolean( + DataStoreKey.IS_FIRST_LOCATION_PERMISSION, + true, + ) + postSideEffect(MapEffect.RequestLocationPermission) + } + intent.shouldShowRationale -> { + Timber.d("1회 거부 상태 - 시스템 권한 팝업 표시") + postSideEffect(MapEffect.RequestLocationPermission) + } + else -> { + Timber.d("2회 이상 거부 (영구 거부) - 설정 이동 다이얼로그 표시") + reduce { copy(isShowLocationPermissionDialog = true) } + } + } + } + } + + MapIntent.DismissLocationPermissionDialog -> { + reduce { copy(isShowLocationPermissionDialog = false) } + } + + MapIntent.ConfirmLocationPermissionDialog -> { + reduce { copy(isShowLocationPermissionDialog = false) } + postSideEffect(MapEffect.NavigateToAppSettings) + } } } @@ -125,7 +169,6 @@ class MapViewModel @Inject constructor() : ViewModel() { viewModelScope.launch { reduce { copy(isLoading = true) } - // TODO: 서버 API 연동 시 교체 val brands = persistentListOf( Brand(isChecked = false, brandName = "인생네컷", brandImageRes = R.drawable.icon_life_four_cut), Brand(isChecked = false, brandName = "포토그레이", brandImageRes = R.drawable.icon_photogray), @@ -135,7 +178,6 @@ class MapViewModel @Inject constructor() : ViewModel() { Brand(isChecked = false, brandName = "포토시그니처", brandImageRes = R.drawable.icon_photo_signature), ) - // TODO: 서버 API 연동 시 교체 // 중심: 37.5270539, 126.8862648 주변 100m 이내 val nearbyBrands = persistentListOf( BrandInfo( @@ -151,7 +193,7 @@ class MapViewModel @Inject constructor() : ViewModel() { brandImageRes = R.drawable.icon_photogray, branchName = "가산역점", distance = "38m", - latitude = 37.5268, + latitude = 37.5248, longitude = 126.8867, ), BrandInfo( @@ -160,23 +202,23 @@ class MapViewModel @Inject constructor() : ViewModel() { branchName = "마리오점", distance = "52m", latitude = 37.5274, - longitude = 126.8858, + longitude = 126.8828, ), BrandInfo( brandName = "하루필름", brandImageRes = R.drawable.icon_haru_film, branchName = "W몰점", distance = "65m", - latitude = 37.5266, - longitude = 126.8859, + latitude = 37.5166, + longitude = 126.8659, ), BrandInfo( brandName = "플랜비스튜디오", brandImageRes = R.drawable.icon_planb_studio, branchName = "대륭포스트점", distance = "72m", - latitude = 37.5276, - longitude = 126.8869, + latitude = 37.5176, + longitude = 126.8969, ), BrandInfo( brandName = "포토시그니처", From c815bc75d1f38ee0448b3a76f481ba2c456160c9 Mon Sep 17 00:00:00 2001 From: Ojongseok Date: Sun, 25 Jan 2026 01:53:32 +0900 Subject: [PATCH 18/37] =?UTF-8?q?[feat]=20#51=20=EA=B6=8C=ED=95=9C=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EA=B3=B5=ED=86=B5=20=EB=A1=9C=EC=A7=81?= =?UTF-8?q?=EC=9D=84=20=EC=9E=91=EC=84=B1=ED=95=98=EB=8A=94=20PermissionMa?= =?UTF-8?q?nager.kt=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/common/build.gradle.kts | 3 +- .../common/permission/PermissionManager.kt | 41 +++++++++++++++ .../android/feature/map/impl/MapScreen.kt | 50 +++++-------------- 3 files changed, 56 insertions(+), 38 deletions(-) create mode 100644 core/common/src/main/java/com/neki/android/core/common/permission/PermissionManager.kt diff --git a/core/common/build.gradle.kts b/core/common/build.gradle.kts index 7a26d84e2..f389ae002 100644 --- a/core/common/build.gradle.kts +++ b/core/common/build.gradle.kts @@ -10,5 +10,6 @@ android { dependencies { api(libs.timber) implementation(libs.androidx.security.crypto) + implementation(libs.androidx.core.ktx) -} \ No newline at end of file +} diff --git a/core/common/src/main/java/com/neki/android/core/common/permission/PermissionManager.kt b/core/common/src/main/java/com/neki/android/core/common/permission/PermissionManager.kt new file mode 100644 index 000000000..8a23c31e6 --- /dev/null +++ b/core/common/src/main/java/com/neki/android/core/common/permission/PermissionManager.kt @@ -0,0 +1,41 @@ +package com.neki.android.core.common.permission + +import android.Manifest +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.provider.Settings +import androidx.core.content.ContextCompat +import androidx.core.content.PermissionChecker + +object PermissionManager { + val LOCATION_PERMISSIONS = arrayOf( + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION, + ) + + fun hasLocationPermission(context: Context): Boolean { + return ContextCompat.checkSelfPermission( + context, + Manifest.permission.ACCESS_FINE_LOCATION, + ) == PermissionChecker.PERMISSION_GRANTED || + ContextCompat.checkSelfPermission( + context, + Manifest.permission.ACCESS_COARSE_LOCATION, + ) == PermissionChecker.PERMISSION_GRANTED + } + + fun shouldShowLocationRationale(activity: Activity): Boolean { + return activity.shouldShowRequestPermissionRationale( + Manifest.permission.ACCESS_FINE_LOCATION, + ) + } + + fun navigateToAppSettings(context: Context) { + val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { + data = Uri.fromParts("package", context.packageName, null) + } + context.startActivity(intent) + } +} diff --git a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapScreen.kt b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapScreen.kt index 390c6c518..b29a5ecd4 100644 --- a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapScreen.kt +++ b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapScreen.kt @@ -1,9 +1,5 @@ package com.neki.android.feature.map.impl -import android.Manifest -import android.content.Intent -import android.net.Uri -import android.provider.Settings import android.widget.Toast import androidx.activity.compose.LocalActivity import androidx.activity.compose.rememberLauncherForActivityResult @@ -26,8 +22,6 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.window.DialogProperties -import androidx.core.content.ContextCompat -import androidx.core.content.PermissionChecker import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.naver.maps.geometry.LatLng @@ -52,6 +46,7 @@ import com.neki.android.feature.map.impl.component.DirectionBottomSheet import com.neki.android.feature.map.impl.component.MapRefreshChip import com.neki.android.feature.map.impl.component.PanelInvisibleContent import com.neki.android.feature.map.impl.component.ToMapChip +import com.neki.android.core.common.permission.PermissionManager import com.neki.android.feature.map.impl.const.DirectionApp import com.neki.android.feature.map.impl.util.DirectionHelper import com.neki.android.feature.map.impl.util.getPlaceName @@ -63,8 +58,8 @@ fun MapRoute( ) { val uiState by viewModel.store.uiState.collectAsStateWithLifecycle() val context = LocalContext.current - val activity = LocalActivity.current!! val coroutineScope = rememberCoroutineScope() + var locationTrackingMode by remember { mutableStateOf(LocationTrackingMode.None) } val cameraPositionState = rememberCameraPositionState { position = CameraPosition(LatLng(37.5269278, 126.886225), 17.0) @@ -79,18 +74,6 @@ fun MapRoute( } } - fun hasLocationPermission(): Boolean { - return ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PermissionChecker.PERMISSION_GRANTED || - ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) == PermissionChecker.PERMISSION_GRANTED - } - - fun requestLocationPermission() { - val shouldShowRationale = activity.shouldShowRequestPermissionRationale( - Manifest.permission.ACCESS_FINE_LOCATION, - ) - viewModel.store.onIntent(MapIntent.RequestLocationPermission(shouldShowRationale)) - } - LaunchedEffect(Unit) { viewModel.store.onIntent(MapIntent.EnterMapScreen) } @@ -154,19 +137,11 @@ fun MapRoute( } is MapEffect.NavigateToAppSettings -> { - val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { - data = Uri.fromParts("package", context.packageName, null) - } - context.startActivity(intent) + PermissionManager.navigateToAppSettings(context) } is MapEffect.RequestLocationPermission -> { - locationPermissionLauncher.launch( - arrayOf( - Manifest.permission.ACCESS_FINE_LOCATION, - Manifest.permission.ACCESS_COARSE_LOCATION, - ), - ) + locationPermissionLauncher.launch(PermissionManager.LOCATION_PERMISSIONS) } } } @@ -177,8 +152,6 @@ fun MapRoute( locationTrackingMode = locationTrackingMode, onLocationTrackingModeChange = { locationTrackingMode = it }, cameraPositionState = cameraPositionState, - onRequestLocationPermission = { requestLocationPermission() }, - hasLocationPermission = { hasLocationPermission() }, ) } @@ -192,9 +165,10 @@ fun MapScreen( cameraPositionState: CameraPositionState = rememberCameraPositionState { position = CameraPosition(LatLng(37.5269278, 126.886225), 17.0) }, - onRequestLocationPermission: () -> Unit = {}, - hasLocationPermission: () -> Boolean = { false }, ) { + val context = LocalContext.current + val activity = LocalActivity.current!! + val mapProperties = remember(locationTrackingMode) { MapProperties( locationTrackingMode = locationTrackingMode, @@ -248,10 +222,11 @@ fun MapScreen( onClickInfoIcon = { onIntent(MapIntent.ClickInfoIcon) }, isCurrentLocation = locationTrackingMode == LocationTrackingMode.Follow, onClickCurrentLocation = { - if (hasLocationPermission()) { + if (PermissionManager.hasLocationPermission(context)) { onLocationTrackingModeChange(LocationTrackingMode.Follow) } else { - onRequestLocationPermission() + val shouldShowRationale = PermissionManager.shouldShowLocationRationale(activity) + onIntent(MapIntent.RequestLocationPermission(shouldShowRationale)) } }, onClickBrand = { onIntent(MapIntent.ClickBrand(it)) }, @@ -283,10 +258,11 @@ fun MapScreen( modifier = Modifier.align(Alignment.BottomCenter), isCurrentLocation = locationTrackingMode == LocationTrackingMode.Follow, onClickCurrentLocation = { - if (hasLocationPermission()) { + if (PermissionManager.hasLocationPermission(context)) { onIntent(MapIntent.ClickCurrentLocation) } else { - onRequestLocationPermission() + val shouldShowRationale = PermissionManager.shouldShowLocationRationale(activity) + onIntent(MapIntent.RequestLocationPermission(shouldShowRationale)) } }, onClickCloseCard = { onIntent(MapIntent.ClickCloseBrandCard) }, From 329983b7cbaff44319b27a3ea812daa9c7a855bb Mon Sep 17 00:00:00 2001 From: Ojongseok Date: Sun, 25 Jan 2026 01:58:11 +0900 Subject: [PATCH 19/37] =?UTF-8?q?[feat]=20#51=20=EC=9C=84=EC=B9=98=20?= =?UTF-8?q?=EA=B6=8C=ED=95=9C=20=ED=99=95=EC=9D=B8=20=EC=8B=9C=EC=A0=90=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80(=EB=84=A4=EC=BB=B7=EC=A7=80=EB=8F=84=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=20=EC=A7=84=EC=9E=85=20=EC=8B=9C,=20?= =?UTF-8?q?=EA=B8=B8=EC=B0=BE=EA=B8=B0=20=EB=B2=84=ED=8A=BC=20=EC=84=A0?= =?UTF-8?q?=ED=83=9D=20=EC=8B=9C)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../neki/android/feature/map/impl/MapScreen.kt | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapScreen.kt b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapScreen.kt index b29a5ecd4..e7616477e 100644 --- a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapScreen.kt +++ b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapScreen.kt @@ -58,6 +58,7 @@ fun MapRoute( ) { val uiState by viewModel.store.uiState.collectAsStateWithLifecycle() val context = LocalContext.current + val activity = LocalActivity.current!! val coroutineScope = rememberCoroutineScope() var locationTrackingMode by remember { mutableStateOf(LocationTrackingMode.None) } @@ -75,6 +76,9 @@ fun MapRoute( } LaunchedEffect(Unit) { + if (!PermissionManager.hasLocationPermission(context)) { + viewModel.store.onIntent(MapIntent.RequestLocationPermission(PermissionManager.shouldShowLocationRationale(activity))) + } viewModel.store.onIntent(MapIntent.EnterMapScreen) } @@ -225,8 +229,7 @@ fun MapScreen( if (PermissionManager.hasLocationPermission(context)) { onLocationTrackingModeChange(LocationTrackingMode.Follow) } else { - val shouldShowRationale = PermissionManager.shouldShowLocationRationale(activity) - onIntent(MapIntent.RequestLocationPermission(shouldShowRationale)) + onIntent(MapIntent.RequestLocationPermission(PermissionManager.shouldShowLocationRationale(activity))) } }, onClickBrand = { onIntent(MapIntent.ClickBrand(it)) }, @@ -261,12 +264,17 @@ fun MapScreen( if (PermissionManager.hasLocationPermission(context)) { onIntent(MapIntent.ClickCurrentLocation) } else { - val shouldShowRationale = PermissionManager.shouldShowLocationRationale(activity) - onIntent(MapIntent.RequestLocationPermission(shouldShowRationale)) + onIntent(MapIntent.RequestLocationPermission(PermissionManager.shouldShowLocationRationale(activity))) } }, onClickCloseCard = { onIntent(MapIntent.ClickCloseBrandCard) }, - onClickDirection = { onIntent(MapIntent.ClickDirection(uiState.selectedBrandInfo.latitude, uiState.selectedBrandInfo.longitude)) }, + onClickDirection = { + if (PermissionManager.hasLocationPermission(context)) { + onIntent(MapIntent.ClickDirection(uiState.selectedBrandInfo.latitude, uiState.selectedBrandInfo.longitude)) + } else { + onIntent(MapIntent.RequestLocationPermission(PermissionManager.shouldShowLocationRationale(activity))) + } + }, ) } } From 00b046a7220ff3085ec9a1f88e0df40adafcaad2 Mon Sep 17 00:00:00 2001 From: Ojongseok Date: Sun, 25 Jan 2026 02:10:28 +0900 Subject: [PATCH 20/37] =?UTF-8?q?[build]=20#51=20detekt=20=EB=A3=B0=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/permission/PermissionManager.kt | 4 +- .../android/feature/map/impl/MapViewModel.kt | 227 +++++++++--------- .../map/impl/component/MapRefreshChip.kt | 6 +- .../feature/map/impl/component/ToMapChip.kt | 4 +- 4 files changed, 117 insertions(+), 124 deletions(-) diff --git a/core/common/src/main/java/com/neki/android/core/common/permission/PermissionManager.kt b/core/common/src/main/java/com/neki/android/core/common/permission/PermissionManager.kt index 8a23c31e6..e16bd1108 100644 --- a/core/common/src/main/java/com/neki/android/core/common/permission/PermissionManager.kt +++ b/core/common/src/main/java/com/neki/android/core/common/permission/PermissionManager.kt @@ -27,9 +27,7 @@ object PermissionManager { } fun shouldShowLocationRationale(activity: Activity): Boolean { - return activity.shouldShowRequestPermissionRationale( - Manifest.permission.ACCESS_FINE_LOCATION, - ) + return activity.shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION) } fun navigateToAppSettings(context: Context) { diff --git a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapViewModel.kt b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapViewModel.kt index 31966f0ad..9e15447e2 100644 --- a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapViewModel.kt +++ b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapViewModel.kt @@ -33,135 +33,134 @@ class MapViewModel @Inject constructor( postSideEffect: (MapEffect) -> Unit, ) { when (intent) { - MapIntent.EnterMapScreen -> { - loadBrands(reduce) - } - - MapIntent.ClickRefresh -> { - postSideEffect(MapEffect.RefreshPhotoBooth) - } - - is MapIntent.UpdateCurrentLocation -> { - reduce { copy(currentLocation = intent.latitude to intent.longitude) } - } - - MapIntent.ClickCurrentLocation -> { - postSideEffect(MapEffect.RefreshCurrentLocation) - } - - MapIntent.ClickInfoIcon -> { - reduce { copy(isShowInfoDialog = true) } + MapIntent.EnterMapScreen -> loadBrands(reduce) + MapIntent.ClickRefresh -> postSideEffect(MapEffect.RefreshPhotoBooth) + is MapIntent.UpdateCurrentLocation -> reduce { copy(currentLocation = intent.latitude to intent.longitude) } + MapIntent.ClickCurrentLocation -> postSideEffect(MapEffect.RefreshCurrentLocation) + MapIntent.ClickInfoIcon -> reduce { copy(isShowInfoDialog = true) } + MapIntent.ClickCloseInfoIcon -> reduce { copy(isShowInfoDialog = false) } + MapIntent.ClickToMapChip -> reduce { copy(dragLevel = DragLevel.FIRST) } + is MapIntent.ClickBrand -> handleClickBrand(state, intent, reduce) + is MapIntent.ClickNearBrand -> handleClickNearBrand(intent, reduce, postSideEffect) + MapIntent.ClickCloseBrandCard -> reduce { + copy( + dragLevel = DragLevel.SECOND, + focusedMarkerPosition = Pair(0.0, 0.0), + selectedBrandInfo = null, + ) } - - MapIntent.ClickCloseInfoIcon -> { - reduce { copy(isShowInfoDialog = false) } + MapIntent.CloseDirectionBottomSheet -> reduce { copy(isShowDirectionBottomSheet = false) } + is MapIntent.ClickDirectionItem -> handleClickDirectionItem(state, intent, reduce, postSideEffect) + is MapIntent.ChangeDragLevel -> reduce { copy(dragLevel = intent.dragLevel) } + is MapIntent.ClickBrandMarker -> handleClickBrandMarker(state, intent, reduce, postSideEffect) + is MapIntent.ClickDirection -> reduce { copy(isShowDirectionBottomSheet = true) } + is MapIntent.RequestLocationPermission -> handleRequestLocationPermission(intent, reduce, postSideEffect) + MapIntent.DismissLocationPermissionDialog -> reduce { copy(isShowLocationPermissionDialog = false) } + MapIntent.ConfirmLocationPermissionDialog -> { + reduce { copy(isShowLocationPermissionDialog = false) } + postSideEffect(MapEffect.NavigateToAppSettings) } + } + } - MapIntent.ClickToMapChip -> { - reduce { copy(dragLevel = DragLevel.FIRST) } - } + private fun handleClickBrand( + state: MapState, + intent: MapIntent.ClickBrand, + reduce: (MapState.() -> MapState) -> Unit, + ) { + reduce { + copy( + brands = state.brands.map { brand -> + if (brand == intent.brand) { + brand.copy(isChecked = !brand.isChecked) + } else { + brand + } + }.toImmutableList(), + ) + } + } - is MapIntent.ClickBrand -> { - reduce { - copy( - brands = state.brands.map { brand -> - if (brand == intent.brand) { - brand.copy(isChecked = !brand.isChecked) - } else { - brand - } - }.toImmutableList(), - ) - } - } + private fun handleClickNearBrand( + intent: MapIntent.ClickNearBrand, + reduce: (MapState.() -> MapState) -> Unit, + postSideEffect: (MapEffect) -> Unit, + ) { + reduce { + copy( + dragLevel = DragLevel.INVISIBLE, + selectedBrandInfo = intent.brandInfo, + focusedMarkerPosition = intent.brandInfo.latitude to intent.brandInfo.longitude, + ) + } + postSideEffect(MapEffect.MoveCameraToPosition(intent.brandInfo.latitude, intent.brandInfo.longitude)) + } - is MapIntent.ClickNearBrand -> { - reduce { - copy( - dragLevel = DragLevel.INVISIBLE, - selectedBrandInfo = intent.brandInfo, - focusedMarkerPosition = intent.brandInfo.latitude to intent.brandInfo.longitude, - ) - } - postSideEffect(MapEffect.MoveCameraToPosition(intent.brandInfo.latitude, intent.brandInfo.longitude)) - } + private fun handleClickDirectionItem( + state: MapState, + intent: MapIntent.ClickDirectionItem, + reduce: (MapState.() -> MapState) -> Unit, + postSideEffect: (MapEffect) -> Unit, + ) { + reduce { copy(isShowDirectionBottomSheet = false) } + state.selectedBrandInfo?.let { brandInfo -> + postSideEffect( + MapEffect.MoveDirectionApp( + app = intent.app, + startLatitude = state.currentLocation.first, + startLongitude = state.currentLocation.second, + endLatitude = brandInfo.latitude, + endLongitude = brandInfo.longitude, + ), + ) + } + } - MapIntent.ClickCloseBrandCard -> { - reduce { copy(dragLevel = DragLevel.SECOND, focusedMarkerPosition = Pair(0.0, 0.0), selectedBrandInfo = null) } - } + private fun handleClickBrandMarker( + state: MapState, + intent: MapIntent.ClickBrandMarker, + reduce: (MapState.() -> MapState) -> Unit, + postSideEffect: (MapEffect) -> Unit, + ) { + val selectedBrand = state.nearbyBrands.find { it.latitude == intent.latitude && it.longitude == intent.longitude } + reduce { + copy( + dragLevel = DragLevel.INVISIBLE, + focusedMarkerPosition = intent.latitude to intent.longitude, + selectedBrandInfo = selectedBrand, + ) + } + postSideEffect(MapEffect.MoveCameraToPosition(intent.latitude, intent.longitude)) + } - MapIntent.CloseDirectionBottomSheet -> { - reduce { copy(isShowDirectionBottomSheet = false) } - } + private fun handleRequestLocationPermission( + intent: MapIntent.RequestLocationPermission, + reduce: (MapState.() -> MapState) -> Unit, + postSideEffect: (MapEffect) -> Unit, + ) { + viewModelScope.launch { + val isFirstRequest = dataStoreRepository.getBoolean(DataStoreKey.IS_FIRST_LOCATION_PERMISSION).first().not() - is MapIntent.ClickDirectionItem -> { - reduce { copy(isShowDirectionBottomSheet = false) } - state.selectedBrandInfo?.let { brandInfo -> - postSideEffect( - MapEffect.MoveDirectionApp( - app = intent.app, - startLatitude = state.currentLocation.first, - startLongitude = state.currentLocation.second, - endLatitude = brandInfo.latitude, - endLongitude = brandInfo.longitude, - ), + when { + isFirstRequest -> { + Timber.d("최초 요청 - 시스템 권한 팝업 표시") + dataStoreRepository.setBoolean( + DataStoreKey.IS_FIRST_LOCATION_PERMISSION, + true, ) + postSideEffect(MapEffect.RequestLocationPermission) } - } - is MapIntent.ChangeDragLevel -> { - reduce { copy(dragLevel = intent.dragLevel) } - } - - is MapIntent.ClickBrandMarker -> { - val selectedBrand = state.nearbyBrands.find { it.latitude == intent.latitude && it.longitude == intent.longitude } - reduce { - copy( - dragLevel = DragLevel.INVISIBLE, - focusedMarkerPosition = intent.latitude to intent.longitude, - selectedBrandInfo = selectedBrand, - ) + intent.shouldShowRationale -> { + Timber.d("1회 거부 상태 - 시스템 권한 팝업 표시") + postSideEffect(MapEffect.RequestLocationPermission) } - postSideEffect(MapEffect.MoveCameraToPosition(intent.latitude, intent.longitude)) - } - - is MapIntent.ClickDirection -> { - reduce { copy(isShowDirectionBottomSheet = true) } - } - - is MapIntent.RequestLocationPermission -> { - viewModelScope.launch { - val isFirstRequest = dataStoreRepository.getBoolean(DataStoreKey.IS_FIRST_LOCATION_PERMISSION).first().not() - when { - isFirstRequest -> { - Timber.d("최초 요청 - 시스템 권한 팝업 표시") - dataStoreRepository.setBoolean( - DataStoreKey.IS_FIRST_LOCATION_PERMISSION, - true, - ) - postSideEffect(MapEffect.RequestLocationPermission) - } - intent.shouldShowRationale -> { - Timber.d("1회 거부 상태 - 시스템 권한 팝업 표시") - postSideEffect(MapEffect.RequestLocationPermission) - } - else -> { - Timber.d("2회 이상 거부 (영구 거부) - 설정 이동 다이얼로그 표시") - reduce { copy(isShowLocationPermissionDialog = true) } - } - } + else -> { + Timber.d("2회 이상 거부 (영구 거부) - 설정 이동 다이얼로그 표시") + reduce { copy(isShowLocationPermissionDialog = true) } } } - - MapIntent.DismissLocationPermissionDialog -> { - reduce { copy(isShowLocationPermissionDialog = false) } - } - - MapIntent.ConfirmLocationPermissionDialog -> { - reduce { copy(isShowLocationPermissionDialog = false) } - postSideEffect(MapEffect.NavigateToAppSettings) - } } } diff --git a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/MapRefreshChip.kt b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/MapRefreshChip.kt index ce3f9bca3..1b7a6cc56 100644 --- a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/MapRefreshChip.kt +++ b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/MapRefreshChip.kt @@ -13,7 +13,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.vectorResource import androidx.compose.ui.unit.dp @@ -21,7 +20,6 @@ import com.neki.android.core.designsystem.ComponentPreview import com.neki.android.core.designsystem.R import com.neki.android.core.designsystem.modifier.buttonShadow import com.neki.android.core.designsystem.modifier.clickableSingle -import com.neki.android.core.designsystem.modifier.noRippleClickableSingle import com.neki.android.core.designsystem.ui.theme.NekiTheme @Composable @@ -36,12 +34,12 @@ internal fun MapRefreshChip( .clickableSingle(onClick = onClick) .background( shape = CircleShape, - color = NekiTheme.colorScheme.white + color = NekiTheme.colorScheme.white, ) .border( width = 1.dp, shape = CircleShape, - color = NekiTheme.colorScheme.gray100 + color = NekiTheme.colorScheme.gray100, ) .padding(horizontal = 16.dp, vertical = 8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp), diff --git a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/ToMapChip.kt b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/ToMapChip.kt index c9635dc33..0914429f9 100644 --- a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/ToMapChip.kt +++ b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/ToMapChip.kt @@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -20,7 +19,6 @@ import com.neki.android.core.designsystem.ComponentPreview import com.neki.android.core.designsystem.R import com.neki.android.core.designsystem.modifier.buttonShadow import com.neki.android.core.designsystem.modifier.clickableSingle -import com.neki.android.core.designsystem.modifier.noRippleClickableSingle import com.neki.android.core.designsystem.ui.theme.NekiTheme @Composable @@ -35,7 +33,7 @@ internal fun ToMapChip( .clickableSingle(onClick = onClick) .background( shape = CircleShape, - color = NekiTheme.colorScheme.gray800 + color = NekiTheme.colorScheme.gray800, ) .padding(horizontal = 16.dp, vertical = 8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp), From 7baf582598dd4d6e6399d8acca22e7152a145fd1 Mon Sep 17 00:00:00 2001 From: Ojongseok Date: Sun, 25 Jan 2026 02:43:45 +0900 Subject: [PATCH 21/37] =?UTF-8?q?[fix]=20#51=20clearTokens()=EA=B0=80=20ac?= =?UTF-8?q?cessToken=EA=B3=BC=20refreshToken=EB=A7=8C=20=EC=A7=80=EC=9A=B0?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/data/repository/impl/DataStoreRepositoryImpl.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/core/data/src/main/java/com/neki/android/core/data/repository/impl/DataStoreRepositoryImpl.kt b/core/data/src/main/java/com/neki/android/core/data/repository/impl/DataStoreRepositoryImpl.kt index 7027ee242..f0efa98f4 100644 --- a/core/data/src/main/java/com/neki/android/core/data/repository/impl/DataStoreRepositoryImpl.kt +++ b/core/data/src/main/java/com/neki/android/core/data/repository/impl/DataStoreRepositoryImpl.kt @@ -46,7 +46,10 @@ class DataStoreRepositoryImpl @Inject constructor( } override suspend fun clearTokens() { - dataStore.edit { it.clear() } + dataStore.edit { preferences -> + preferences.remove(DataStoreKey.ACCESS_TOKEN) + preferences.remove(DataStoreKey.REFRESH_TOKEN) + } } override suspend fun setBoolean(key: Preferences.Key, value: Boolean) { From 33e368fea0428b699dda383aeb15f3e306a9cb74 Mon Sep 17 00:00:00 2001 From: Ojongseok Date: Sun, 25 Jan 2026 03:02:56 +0900 Subject: [PATCH 22/37] =?UTF-8?q?[fix]=20#51=20MapContract=20=EB=82=B4=20?= =?UTF-8?q?=ED=98=84=EC=9C=84=EC=B9=98=20nullable=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/neki/android/feature/map/impl/MapContract.kt | 8 +++----- .../com/neki/android/feature/map/impl/MapViewModel.kt | 6 +++++- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapContract.kt b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapContract.kt index 5190cb83c..924940d33 100644 --- a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapContract.kt +++ b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapContract.kt @@ -8,12 +8,13 @@ import kotlinx.collections.immutable.persistentListOf data class MapState( val isLoading: Boolean = false, - val currentLocation: Pair = Pair(0.0, 0.0), + val currentLocation: Pair? = null, val dragLevel: DragLevel = DragLevel.FIRST, val brands: ImmutableList = persistentListOf(), val nearbyBrands: ImmutableList = persistentListOf(), val focusedMarkerPosition: Pair = Pair(0.0, 0.0), val selectedBrandInfo: BrandInfo? = null, + val focusedMarkerPosition: Pair? = null, val isShowInfoDialog: Boolean = false, val isShowDirectionBottomSheet: Boolean = false, val isShowLocationPermissionDialog: Boolean = false, @@ -58,10 +59,7 @@ sealed interface MapEffect { data object RefreshPhotoBooth : MapEffect data object RefreshCurrentLocation : MapEffect data class ShowToastMessage(val message: String) : MapEffect - data class MoveCameraToPosition( - val latitude: Double, - val longitude: Double, - ) : MapEffect + data class MoveCameraToPosition(val latitude: Double, val longitude: Double) : MapEffect data class MoveDirectionApp( val app: DirectionApp, diff --git a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapViewModel.kt b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapViewModel.kt index 9e15447e2..dd5e96ff1 100644 --- a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapViewModel.kt +++ b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapViewModel.kt @@ -103,7 +103,11 @@ class MapViewModel @Inject constructor( postSideEffect: (MapEffect) -> Unit, ) { reduce { copy(isShowDirectionBottomSheet = false) } - state.selectedBrandInfo?.let { brandInfo -> + if (state.currentLocation == null) { + postSideEffect(MapEffect.ShowToastMessage("현재 위치를 가져올 수 없습니다.")) + return + } + state.selectedPhotoBoothInfo?.let { brandInfo -> postSideEffect( MapEffect.MoveDirectionApp( app = intent.app, From 593a1b88f03a7c48b905492552389d421d3f26ed Mon Sep 17 00:00:00 2001 From: Ojongseok Date: Sun, 25 Jan 2026 03:08:00 +0900 Subject: [PATCH 23/37] =?UTF-8?q?[fix]=20#51=20=EC=9D=BC=EB=B6=80=20Brand~?= =?UTF-8?q?=20->=20PhotoBooth=20=EB=84=A4=EC=9D=B4=EB=B0=8D=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=20=EB=B0=8F=20CloseButton=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../android/feature/map/impl/MapContract.kt | 13 ++--- .../android/feature/map/impl/MapScreen.kt | 34 +++++------- .../android/feature/map/impl/MapViewModel.kt | 22 ++++---- .../feature/map/impl/component/CloseButton.kt | 53 +++++++++++++++++++ .../{BrandCard.kt => PhotoBoothCard.kt} | 6 +-- ...ibleContent.kt => PhotoBoothDetailCard.kt} | 44 ++------------- .../{BrandMarker.kt => PhotoBoothMarker.kt} | 14 ++--- 7 files changed, 96 insertions(+), 90 deletions(-) create mode 100644 feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/CloseButton.kt rename feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/{BrandCard.kt => PhotoBoothCard.kt} (97%) rename feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/{PanelInvisibleContent.kt => PhotoBoothDetailCard.kt} (59%) rename feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/{BrandMarker.kt => PhotoBoothMarker.kt} (96%) diff --git a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapContract.kt b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapContract.kt index 924940d33..29de67bdd 100644 --- a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapContract.kt +++ b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapContract.kt @@ -11,10 +11,9 @@ data class MapState( val currentLocation: Pair? = null, val dragLevel: DragLevel = DragLevel.FIRST, val brands: ImmutableList = persistentListOf(), - val nearbyBrands: ImmutableList = persistentListOf(), - val focusedMarkerPosition: Pair = Pair(0.0, 0.0), - val selectedBrandInfo: BrandInfo? = null, + val nearbyPhotoBooths: ImmutableList = persistentListOf(), val focusedMarkerPosition: Pair? = null, + val selectedPhotoBoothInfo: BrandInfo? = null, val isShowInfoDialog: Boolean = false, val isShowDirectionBottomSheet: Boolean = false, val isShowLocationPermissionDialog: Boolean = false, @@ -24,16 +23,14 @@ sealed interface MapIntent { data object EnterMapScreen : MapIntent // in 지도 - data class ClickBrandMarker( + data class ClickPhotoBoothMarker( val latitude: Double, val longitude: Double, ) : MapIntent - data class ClickDirection( val latitude: Double, val longitude: Double, ) : MapIntent - data object ClickRefresh : MapIntent data class UpdateCurrentLocation(val latitude: Double, val longitude: Double) : MapIntent @@ -43,8 +40,8 @@ sealed interface MapIntent { data object ClickCloseInfoIcon : MapIntent data object ClickToMapChip : MapIntent data class ClickBrand(val brand: Brand) : MapIntent - data class ClickNearBrand(val brandInfo: BrandInfo) : MapIntent - data object ClickCloseBrandCard : MapIntent + data class ClickNearPhotoBooth(val brandInfo: BrandInfo) : MapIntent + data object ClickClosePhotoBoothCard : MapIntent data object CloseDirectionBottomSheet : MapIntent data class ClickDirectionItem(val app: DirectionApp) : MapIntent data class ChangeDragLevel(val dragLevel: DragLevel) : MapIntent diff --git a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapScreen.kt b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapScreen.kt index e7616477e..996c16f7f 100644 --- a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapScreen.kt +++ b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapScreen.kt @@ -19,7 +19,6 @@ import kotlinx.coroutines.launch import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.window.DialogProperties import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel @@ -38,13 +37,12 @@ import com.naver.maps.map.compose.rememberCameraPositionState import com.naver.maps.map.compose.rememberFusedLocationSource import com.neki.android.core.designsystem.dialog.SingleButtonAlertDialog import com.neki.android.core.designsystem.dialog.WarningDialog -import com.neki.android.core.designsystem.ui.theme.NekiTheme import com.neki.android.core.ui.compose.collectWithLifecycle import com.neki.android.feature.map.impl.component.AnchoredDraggablePanel -import com.neki.android.feature.map.impl.component.BrandMarker +import com.neki.android.feature.map.impl.component.PhotoBoothMarker import com.neki.android.feature.map.impl.component.DirectionBottomSheet import com.neki.android.feature.map.impl.component.MapRefreshChip -import com.neki.android.feature.map.impl.component.PanelInvisibleContent +import com.neki.android.feature.map.impl.component.PhotoBoothDetailCard import com.neki.android.feature.map.impl.component.ToMapChip import com.neki.android.core.common.permission.PermissionManager import com.neki.android.feature.map.impl.const.DirectionApp @@ -203,16 +201,16 @@ fun MapScreen( onIntent(MapIntent.UpdateCurrentLocation(location.latitude, location.longitude)) }, ) { - uiState.nearbyBrands.forEachIndexed { index, brandInfo -> + uiState.nearbyPhotoBooths.forEachIndexed { index, brandInfo -> val isFocused = uiState.focusedMarkerPosition == (brandInfo.latitude to brandInfo.longitude) - BrandMarker( + PhotoBoothMarker( keys = arrayOf("$isFocused"), latitude = brandInfo.latitude, longitude = brandInfo.longitude, brandImageRes = brandInfo.brandImageRes, isFocused = isFocused, onClick = { - onIntent(MapIntent.ClickBrandMarker(latitude = brandInfo.latitude, longitude = brandInfo.longitude)) + onIntent(MapIntent.ClickPhotoBoothMarker(latitude = brandInfo.latitude, longitude = brandInfo.longitude)) }, ) } @@ -220,7 +218,7 @@ fun MapScreen( AnchoredDraggablePanel( brands = uiState.brands, - nearbyBrands = uiState.nearbyBrands, + nearbyBrands = uiState.nearbyPhotoBooths, dragLevel = uiState.dragLevel, onDragLevelChanged = { onIntent(MapIntent.ChangeDragLevel(it)) }, onClickInfoIcon = { onIntent(MapIntent.ClickInfoIcon) }, @@ -233,7 +231,7 @@ fun MapScreen( } }, onClickBrand = { onIntent(MapIntent.ClickBrand(it)) }, - onClickNearBrand = { onIntent(MapIntent.ClickNearBrand(it)) }, + onClickNearBrand = { onIntent(MapIntent.ClickNearPhotoBooth(it)) }, ) if ((uiState.dragLevel == DragLevel.FIRST || uiState.dragLevel == DragLevel.SECOND) && @@ -255,9 +253,9 @@ fun MapScreen( .padding(bottom = 32.dp), onClick = { onIntent(MapIntent.ClickToMapChip) }, ) - } else if (uiState.dragLevel == DragLevel.INVISIBLE && uiState.selectedBrandInfo != null) { - PanelInvisibleContent( - brandInfo = uiState.selectedBrandInfo, + } else if (uiState.dragLevel == DragLevel.INVISIBLE && uiState.selectedPhotoBoothInfo != null) { + PhotoBoothDetailCard( + brandInfo = uiState.selectedPhotoBoothInfo, modifier = Modifier.align(Alignment.BottomCenter), isCurrentLocation = locationTrackingMode == LocationTrackingMode.Follow, onClickCurrentLocation = { @@ -267,10 +265,10 @@ fun MapScreen( onIntent(MapIntent.RequestLocationPermission(PermissionManager.shouldShowLocationRationale(activity))) } }, - onClickCloseCard = { onIntent(MapIntent.ClickCloseBrandCard) }, + onClickCloseCard = { onIntent(MapIntent.ClickClosePhotoBoothCard) }, onClickDirection = { if (PermissionManager.hasLocationPermission(context)) { - onIntent(MapIntent.ClickDirection(uiState.selectedBrandInfo.latitude, uiState.selectedBrandInfo.longitude)) + onIntent(MapIntent.ClickDirection(uiState.selectedPhotoBoothInfo.latitude, uiState.selectedPhotoBoothInfo.longitude)) } else { onIntent(MapIntent.RequestLocationPermission(PermissionManager.shouldShowLocationRationale(activity))) } @@ -305,11 +303,3 @@ fun MapScreen( ) } } - -@Preview -@Composable -private fun MapScreenPreview() { - NekiTheme { - MapScreen() - } -} diff --git a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapViewModel.kt b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapViewModel.kt index dd5e96ff1..a65b96464 100644 --- a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapViewModel.kt +++ b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapViewModel.kt @@ -41,18 +41,18 @@ class MapViewModel @Inject constructor( MapIntent.ClickCloseInfoIcon -> reduce { copy(isShowInfoDialog = false) } MapIntent.ClickToMapChip -> reduce { copy(dragLevel = DragLevel.FIRST) } is MapIntent.ClickBrand -> handleClickBrand(state, intent, reduce) - is MapIntent.ClickNearBrand -> handleClickNearBrand(intent, reduce, postSideEffect) - MapIntent.ClickCloseBrandCard -> reduce { + is MapIntent.ClickNearPhotoBooth -> handleClickNearBrand(intent, reduce, postSideEffect) + MapIntent.ClickClosePhotoBoothCard -> reduce { copy( dragLevel = DragLevel.SECOND, - focusedMarkerPosition = Pair(0.0, 0.0), - selectedBrandInfo = null, + focusedMarkerPosition = null, + selectedPhotoBoothInfo = null, ) } MapIntent.CloseDirectionBottomSheet -> reduce { copy(isShowDirectionBottomSheet = false) } is MapIntent.ClickDirectionItem -> handleClickDirectionItem(state, intent, reduce, postSideEffect) is MapIntent.ChangeDragLevel -> reduce { copy(dragLevel = intent.dragLevel) } - is MapIntent.ClickBrandMarker -> handleClickBrandMarker(state, intent, reduce, postSideEffect) + is MapIntent.ClickPhotoBoothMarker -> handleClickBrandMarker(state, intent, reduce, postSideEffect) is MapIntent.ClickDirection -> reduce { copy(isShowDirectionBottomSheet = true) } is MapIntent.RequestLocationPermission -> handleRequestLocationPermission(intent, reduce, postSideEffect) MapIntent.DismissLocationPermissionDialog -> reduce { copy(isShowLocationPermissionDialog = false) } @@ -82,14 +82,14 @@ class MapViewModel @Inject constructor( } private fun handleClickNearBrand( - intent: MapIntent.ClickNearBrand, + intent: MapIntent.ClickNearPhotoBooth, reduce: (MapState.() -> MapState) -> Unit, postSideEffect: (MapEffect) -> Unit, ) { reduce { copy( dragLevel = DragLevel.INVISIBLE, - selectedBrandInfo = intent.brandInfo, + selectedPhotoBoothInfo = intent.brandInfo, focusedMarkerPosition = intent.brandInfo.latitude to intent.brandInfo.longitude, ) } @@ -122,16 +122,16 @@ class MapViewModel @Inject constructor( private fun handleClickBrandMarker( state: MapState, - intent: MapIntent.ClickBrandMarker, + intent: MapIntent.ClickPhotoBoothMarker, reduce: (MapState.() -> MapState) -> Unit, postSideEffect: (MapEffect) -> Unit, ) { - val selectedBrand = state.nearbyBrands.find { it.latitude == intent.latitude && it.longitude == intent.longitude } + val selectedBrand = state.nearbyPhotoBooths.find { it.latitude == intent.latitude && it.longitude == intent.longitude } reduce { copy( dragLevel = DragLevel.INVISIBLE, focusedMarkerPosition = intent.latitude to intent.longitude, - selectedBrandInfo = selectedBrand, + selectedPhotoBoothInfo = selectedBrand, ) } postSideEffect(MapEffect.MoveCameraToPosition(intent.latitude, intent.longitude)) @@ -253,7 +253,7 @@ class MapViewModel @Inject constructor( copy( isLoading = false, brands = brands, - nearbyBrands = nearbyBrands, + nearbyPhotoBooths = nearbyBrands, ) } } diff --git a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/CloseButton.kt b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/CloseButton.kt new file mode 100644 index 000000000..dc7a5f0b0 --- /dev/null +++ b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/CloseButton.kt @@ -0,0 +1,53 @@ +package com.neki.android.feature.map.impl.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.unit.dp +import com.neki.android.core.designsystem.ComponentPreview +import com.neki.android.core.designsystem.R +import com.neki.android.core.designsystem.modifier.buttonShadow +import com.neki.android.core.designsystem.ui.theme.NekiTheme +import com.neki.android.core.model.BrandInfo + +@Composable +internal fun CloseButton( + modifier: Modifier = Modifier, + onClick: () -> Unit = {}, +) { + Box( + modifier = modifier + .buttonShadow() + .background( + shape = CircleShape, + color = NekiTheme.colorScheme.white, + ) + .clickable(onClick = onClick) + .padding(8.dp), + contentAlignment = Alignment.Center, + ) { + Icon( + modifier = Modifier.size(20.dp), + imageVector = ImageVector.vectorResource(R.drawable.icon_close), + contentDescription = null, + tint = NekiTheme.colorScheme.gray800, + ) + } +} + +@ComponentPreview +@Composable +private fun CloseButtonPreview() { + NekiTheme { + CloseButton() + } +} diff --git a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/BrandCard.kt b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/PhotoBoothCard.kt similarity index 97% rename from feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/BrandCard.kt rename to feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/PhotoBoothCard.kt index 69d36a12d..f2b7d7d21 100644 --- a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/BrandCard.kt +++ b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/PhotoBoothCard.kt @@ -30,7 +30,7 @@ import com.neki.android.core.ui.compose.HorizontalSpacer import com.neki.android.core.ui.compose.VerticalSpacer @Composable -internal fun BrandCard( +internal fun PhotoBoothCard( brand: BrandInfo, modifier: Modifier = Modifier, onClickDirection: () -> Unit = {}, @@ -100,9 +100,9 @@ internal fun BrandCard( @ComponentPreview @Composable -private fun BrandCardPreview() { +private fun PhotoBoothCardPreview() { NekiTheme { - BrandCard( + PhotoBoothCard( brand = BrandInfo( brandName = "인생네컷", brandImageRes = R.drawable.icon_life_four_cut, diff --git a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/PanelInvisibleContent.kt b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/PhotoBoothDetailCard.kt similarity index 59% rename from feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/PanelInvisibleContent.kt rename to feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/PhotoBoothDetailCard.kt index 737ff6d0e..2e77e685d 100644 --- a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/PanelInvisibleContent.kt +++ b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/PhotoBoothDetailCard.kt @@ -1,31 +1,22 @@ package com.neki.android.feature.map.impl.component -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column 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.foundation.shape.CircleShape -import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.res.vectorResource import androidx.compose.ui.unit.dp import com.neki.android.core.designsystem.ComponentPreview import com.neki.android.core.designsystem.R -import com.neki.android.core.designsystem.modifier.buttonShadow import com.neki.android.core.designsystem.ui.theme.NekiTheme import com.neki.android.core.model.BrandInfo import com.neki.android.core.ui.compose.VerticalSpacer @Composable -internal fun PanelInvisibleContent( +internal fun PhotoBoothDetailCard( brandInfo: BrandInfo, modifier: Modifier = Modifier, isCurrentLocation: Boolean = false, @@ -47,46 +38,21 @@ internal fun PanelInvisibleContent( isActiveCurrentLocation = isCurrentLocation, onClick = onClickCurrentLocation, ) - BrandCardCloseButton(onClick = onClickCloseCard) + CloseButton(onClick = onClickCloseCard) } VerticalSpacer(12.dp) - BrandCard( + PhotoBoothCard( brand = brandInfo, onClickDirection = onClickDirection, ) } } -@Composable -internal fun BrandCardCloseButton( - modifier: Modifier = Modifier, - onClick: () -> Unit = {}, -) { - Box( - modifier = modifier - .buttonShadow() - .background( - shape = CircleShape, - color = NekiTheme.colorScheme.white, - ) - .clickable(onClick = onClick) - .padding(8.dp), - contentAlignment = Alignment.Center, - ) { - Icon( - modifier = Modifier.size(20.dp), - imageVector = ImageVector.vectorResource(R.drawable.icon_close), - contentDescription = null, - tint = NekiTheme.colorScheme.gray800, - ) - } -} - @ComponentPreview @Composable -private fun PanelInvisibleContentPreview() { +private fun PhotoBoothDetailCardPreview() { NekiTheme { - PanelInvisibleContent( + PhotoBoothDetailCard( brandInfo = BrandInfo( brandName = "인생네컷", brandImageRes = R.drawable.icon_life_four_cut, diff --git a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/BrandMarker.kt b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/PhotoBoothMarker.kt similarity index 96% rename from feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/BrandMarker.kt rename to feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/PhotoBoothMarker.kt index a5f1f2179..4bf802a5b 100644 --- a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/BrandMarker.kt +++ b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/PhotoBoothMarker.kt @@ -40,7 +40,7 @@ import com.neki.android.feature.map.impl.const.MapConst.MARKER_TRIANGLE_WIDTH @OptIn(ExperimentalNaverMapApi::class) @Composable -internal fun BrandMarker( +internal fun PhotoBoothMarker( vararg keys: String, latitude: Double, longitude: Double, @@ -61,7 +61,7 @@ internal fun BrandMarker( true }, ) { - BrandMarkerContent( + PhotoBoothMarkerContent( brandImageRes = brandImageRes, isFocused = isFocused, ) @@ -69,7 +69,7 @@ internal fun BrandMarker( } @Composable -internal fun BrandMarkerContent( +internal fun PhotoBoothMarkerContent( modifier: Modifier = Modifier, brandImageRes: Int, isFocused: Boolean = false, @@ -186,9 +186,9 @@ internal fun BrandMarkerContent( @ComponentPreview @Composable -private fun BrandMarkerContentPreview() { +private fun PhotoBoothMarkerPreview() { NekiTheme { - BrandMarkerContent( + PhotoBoothMarkerContent( brandImageRes = R.drawable.icon_life_four_cut, ) } @@ -196,9 +196,9 @@ private fun BrandMarkerContentPreview() { @ComponentPreview @Composable -private fun BrandMarkerContentSelectedPreview() { +private fun PhotoBoothMarkerSelectedPreview() { NekiTheme { - BrandMarkerContent( + PhotoBoothMarkerContent( brandImageRes = R.drawable.icon_life_four_cut, isFocused = true, ) From eb2980e0e1bfd21462b553894bba0108a2ad97d2 Mon Sep 17 00:00:00 2001 From: Ojongseok Date: Sun, 25 Jan 2026 03:08:53 +0900 Subject: [PATCH 24/37] =?UTF-8?q?[build]=20#51=20detekt=20=EB=A3=B0=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/neki/android/feature/map/impl/component/CloseButton.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/CloseButton.kt b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/CloseButton.kt index dc7a5f0b0..c7105c66d 100644 --- a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/CloseButton.kt +++ b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/CloseButton.kt @@ -17,7 +17,6 @@ import com.neki.android.core.designsystem.ComponentPreview import com.neki.android.core.designsystem.R import com.neki.android.core.designsystem.modifier.buttonShadow import com.neki.android.core.designsystem.ui.theme.NekiTheme -import com.neki.android.core.model.BrandInfo @Composable internal fun CloseButton( From 9d6cd4ba1fe8957f6af489b62050304508cdb37f Mon Sep 17 00:00:00 2001 From: Ojongseok Date: Sun, 25 Jan 2026 11:10:44 +0900 Subject: [PATCH 25/37] =?UTF-8?q?[fix]=20#51=20=EA=B6=8C=ED=95=9C=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EB=A7=A4=EB=8B=88=EC=A0=80=20=ED=81=B4?= =?UTF-8?q?=EB=9E=98=EC=8A=A4=20=EA=B6=8C=ED=95=9C=20=EB=8B=A8=EC=9C=84?= =?UTF-8?q?=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...anager.kt => LocationPermissionManager.kt} | 12 +--------- .../core/common/permission/PermissionUtils.kt | 13 +++++++++++ .../android/feature/map/impl/MapScreen.kt | 23 ++++++++++--------- 3 files changed, 26 insertions(+), 22 deletions(-) rename core/common/src/main/java/com/neki/android/core/common/permission/{PermissionManager.kt => LocationPermissionManager.kt} (72%) create mode 100644 core/common/src/main/java/com/neki/android/core/common/permission/PermissionUtils.kt diff --git a/core/common/src/main/java/com/neki/android/core/common/permission/PermissionManager.kt b/core/common/src/main/java/com/neki/android/core/common/permission/LocationPermissionManager.kt similarity index 72% rename from core/common/src/main/java/com/neki/android/core/common/permission/PermissionManager.kt rename to core/common/src/main/java/com/neki/android/core/common/permission/LocationPermissionManager.kt index e16bd1108..923ce048b 100644 --- a/core/common/src/main/java/com/neki/android/core/common/permission/PermissionManager.kt +++ b/core/common/src/main/java/com/neki/android/core/common/permission/LocationPermissionManager.kt @@ -3,13 +3,10 @@ package com.neki.android.core.common.permission import android.Manifest import android.app.Activity import android.content.Context -import android.content.Intent -import android.net.Uri -import android.provider.Settings import androidx.core.content.ContextCompat import androidx.core.content.PermissionChecker -object PermissionManager { +object LocationPermissionManager { val LOCATION_PERMISSIONS = arrayOf( Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION, @@ -29,11 +26,4 @@ object PermissionManager { fun shouldShowLocationRationale(activity: Activity): Boolean { return activity.shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION) } - - fun navigateToAppSettings(context: Context) { - val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { - data = Uri.fromParts("package", context.packageName, null) - } - context.startActivity(intent) - } } diff --git a/core/common/src/main/java/com/neki/android/core/common/permission/PermissionUtils.kt b/core/common/src/main/java/com/neki/android/core/common/permission/PermissionUtils.kt new file mode 100644 index 000000000..1286aa658 --- /dev/null +++ b/core/common/src/main/java/com/neki/android/core/common/permission/PermissionUtils.kt @@ -0,0 +1,13 @@ +package com.neki.android.core.common.permission + +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.provider.Settings + +fun navigateToAppSettings(context: Context) { + val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { + data = Uri.fromParts("package", context.packageName, null) + } + context.startActivity(intent) +} diff --git a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapScreen.kt b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapScreen.kt index 996c16f7f..d91fe99a8 100644 --- a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapScreen.kt +++ b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapScreen.kt @@ -44,7 +44,8 @@ import com.neki.android.feature.map.impl.component.DirectionBottomSheet import com.neki.android.feature.map.impl.component.MapRefreshChip import com.neki.android.feature.map.impl.component.PhotoBoothDetailCard import com.neki.android.feature.map.impl.component.ToMapChip -import com.neki.android.core.common.permission.PermissionManager +import com.neki.android.core.common.permission.LocationPermissionManager +import com.neki.android.core.common.permission.navigateToAppSettings import com.neki.android.feature.map.impl.const.DirectionApp import com.neki.android.feature.map.impl.util.DirectionHelper import com.neki.android.feature.map.impl.util.getPlaceName @@ -74,8 +75,8 @@ fun MapRoute( } LaunchedEffect(Unit) { - if (!PermissionManager.hasLocationPermission(context)) { - viewModel.store.onIntent(MapIntent.RequestLocationPermission(PermissionManager.shouldShowLocationRationale(activity))) + if (!LocationPermissionManager.hasLocationPermission(context)) { + viewModel.store.onIntent(MapIntent.RequestLocationPermission(LocationPermissionManager.shouldShowLocationRationale(activity))) } viewModel.store.onIntent(MapIntent.EnterMapScreen) } @@ -139,11 +140,11 @@ fun MapRoute( } is MapEffect.NavigateToAppSettings -> { - PermissionManager.navigateToAppSettings(context) + navigateToAppSettings(context) } is MapEffect.RequestLocationPermission -> { - locationPermissionLauncher.launch(PermissionManager.LOCATION_PERMISSIONS) + locationPermissionLauncher.launch(LocationPermissionManager.LOCATION_PERMISSIONS) } } } @@ -224,10 +225,10 @@ fun MapScreen( onClickInfoIcon = { onIntent(MapIntent.ClickInfoIcon) }, isCurrentLocation = locationTrackingMode == LocationTrackingMode.Follow, onClickCurrentLocation = { - if (PermissionManager.hasLocationPermission(context)) { + if (LocationPermissionManager.hasLocationPermission(context)) { onLocationTrackingModeChange(LocationTrackingMode.Follow) } else { - onIntent(MapIntent.RequestLocationPermission(PermissionManager.shouldShowLocationRationale(activity))) + onIntent(MapIntent.RequestLocationPermission(LocationPermissionManager.shouldShowLocationRationale(activity))) } }, onClickBrand = { onIntent(MapIntent.ClickBrand(it)) }, @@ -259,18 +260,18 @@ fun MapScreen( modifier = Modifier.align(Alignment.BottomCenter), isCurrentLocation = locationTrackingMode == LocationTrackingMode.Follow, onClickCurrentLocation = { - if (PermissionManager.hasLocationPermission(context)) { + if (LocationPermissionManager.hasLocationPermission(context)) { onIntent(MapIntent.ClickCurrentLocation) } else { - onIntent(MapIntent.RequestLocationPermission(PermissionManager.shouldShowLocationRationale(activity))) + onIntent(MapIntent.RequestLocationPermission(LocationPermissionManager.shouldShowLocationRationale(activity))) } }, onClickCloseCard = { onIntent(MapIntent.ClickClosePhotoBoothCard) }, onClickDirection = { - if (PermissionManager.hasLocationPermission(context)) { + if (LocationPermissionManager.hasLocationPermission(context)) { onIntent(MapIntent.ClickDirection(uiState.selectedPhotoBoothInfo.latitude, uiState.selectedPhotoBoothInfo.longitude)) } else { - onIntent(MapIntent.RequestLocationPermission(PermissionManager.shouldShowLocationRationale(activity))) + onIntent(MapIntent.RequestLocationPermission(LocationPermissionManager.shouldShowLocationRationale(activity))) } }, ) From f6ff2f6c03e44a12a86211b3cf1b359beed8f5d4 Mon Sep 17 00:00:00 2001 From: Ojongseok Date: Sun, 25 Jan 2026 11:14:48 +0900 Subject: [PATCH 26/37] =?UTF-8?q?[fix]=20#51=20=EB=93=9C=EB=9E=98=EA=B7=B8?= =?UTF-8?q?=20=EA=B4=80=EB=A0=A8=20=EB=86=92=EC=9D=B4=20=EC=83=81=EC=88=98?= =?UTF-8?q?=20=EB=84=A4=EC=9D=B4=EB=B0=8D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/neki/android/feature/map/impl/const/MapConst.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/const/MapConst.kt b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/const/MapConst.kt index 359b53e9b..2c4f77433 100644 --- a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/const/MapConst.kt +++ b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/const/MapConst.kt @@ -3,8 +3,8 @@ package com.neki.android.feature.map.impl.const internal object MapConst { internal const val BOTTOM_NAVIGATION_BAR_HEIGHT = 52 internal const val PANEL_DRAG_LOCATION_HEIGHT = 48 - internal const val PANEL_DRAG_LEVEL_BOTTOM_HEIGHT = 96 - internal const val PANEL_DRAG_LEVEL_CENTER_HEIGHT = 218 + internal const val PANEL_DRAG_LEVEL_FIRST_HEIGHT = 96 + internal const val PANEL_DRAG_LEVEL_SECOND_HEIGHT = 218 // 마커 internal const val MARKER_BACKGROUND_RADIUS = 20 From f51fa03d4574611c92160b95a7166e1e9c555d95 Mon Sep 17 00:00:00 2001 From: Ojongseok Date: Sun, 25 Jan 2026 11:16:27 +0900 Subject: [PATCH 27/37] =?UTF-8?q?[fix]=20#51=20MapRefreshChip.kt=20?= =?UTF-8?q?=EC=88=98=ED=8F=89=20=ED=8C=A8=EB=94=A9=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../neki/android/feature/map/impl/component/MapRefreshChip.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/MapRefreshChip.kt b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/MapRefreshChip.kt index 1b7a6cc56..e86aff9c5 100644 --- a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/MapRefreshChip.kt +++ b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/MapRefreshChip.kt @@ -41,7 +41,7 @@ internal fun MapRefreshChip( shape = CircleShape, color = NekiTheme.colorScheme.gray100, ) - .padding(horizontal = 16.dp, vertical = 8.dp), + .padding(horizontal = 13.09.dp, vertical = 8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically, ) { From 34a647134cf44c8179f34a704982b72072bf28bd Mon Sep 17 00:00:00 2001 From: Ojongseok Date: Sun, 25 Jan 2026 11:18:02 +0900 Subject: [PATCH 28/37] =?UTF-8?q?[fix]=20#51=20=EA=B0=80=EA=B9=8C=EC=9A=B4?= =?UTF-8?q?=20=EB=84=A4=EC=BB=B7=20=EC=82=AC=EC=A7=84=20=EB=B8=8C=EB=9E=9C?= =?UTF-8?q?=EB=93=9C=20=EB=8D=94=EB=AF=B8=EB=8D=B0=EC=9D=B4=ED=84=B0=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/com/neki/android/feature/map/impl/MapScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapScreen.kt b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapScreen.kt index d91fe99a8..9e7f1ad09 100644 --- a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapScreen.kt +++ b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapScreen.kt @@ -202,7 +202,7 @@ fun MapScreen( onIntent(MapIntent.UpdateCurrentLocation(location.latitude, location.longitude)) }, ) { - uiState.nearbyPhotoBooths.forEachIndexed { index, brandInfo -> + uiState.nearbyPhotoBooths.forEach { brandInfo -> val isFocused = uiState.focusedMarkerPosition == (brandInfo.latitude to brandInfo.longitude) PhotoBoothMarker( keys = arrayOf("$isFocused"), From 6a1ce9432be6fcac9d08d11b9570a48fe5ea3f84 Mon Sep 17 00:00:00 2001 From: Ojongseok Date: Sun, 25 Jan 2026 11:45:48 +0900 Subject: [PATCH 29/37] =?UTF-8?q?[fix]=20#51=20@Qualifier=20=ED=99=9C?= =?UTF-8?q?=EC=9A=A9=ED=95=B4=20Token=20/=20Auth=20DataStore=20=EC=9D=B8?= =?UTF-8?q?=EC=8A=A4=ED=84=B4=EC=8A=A4=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dataapi/repository/DataStoreRepository.kt | 9 ---- .../dataapi/repository/TokenRepository.kt | 14 +++++ .../core/data/local/di/DataStoreModule.kt | 29 +++++----- .../core/data/local/di/DataStoreQualifier.kt | 11 ++++ .../core/data/remote/di/NetworkModule.kt | 24 ++++----- .../data/repository/di/RepositoryModule.kt | 8 +++ .../impl/DataStoreRepositoryImpl.kt | 43 +-------------- .../repository/impl/TokenRepositoryImpl.kt | 54 +++++++++++++++++++ .../feature/auth/impl/LoginViewModel.kt | 10 ++-- 9 files changed, 120 insertions(+), 82 deletions(-) create mode 100644 core/data-api/src/main/java/com/neki/android/core/dataapi/repository/TokenRepository.kt create mode 100644 core/data/src/main/java/com/neki/android/core/data/local/di/DataStoreQualifier.kt create mode 100644 core/data/src/main/java/com/neki/android/core/data/repository/impl/TokenRepositoryImpl.kt diff --git a/core/data-api/src/main/java/com/neki/android/core/dataapi/repository/DataStoreRepository.kt b/core/data-api/src/main/java/com/neki/android/core/dataapi/repository/DataStoreRepository.kt index c02b46bbf..2ce25268d 100644 --- a/core/data-api/src/main/java/com/neki/android/core/dataapi/repository/DataStoreRepository.kt +++ b/core/data-api/src/main/java/com/neki/android/core/dataapi/repository/DataStoreRepository.kt @@ -4,15 +4,6 @@ import androidx.datastore.preferences.core.Preferences import kotlinx.coroutines.flow.Flow interface DataStoreRepository { - suspend fun saveJwtTokens( - accessToken: String, - refreshToken: String, - ) - fun isSavedJwtTokens(): Flow - fun getAccessToken(): Flow - fun getRefreshToken(): Flow - suspend fun clearTokens() - suspend fun setBoolean(key: Preferences.Key, value: Boolean) fun getBoolean(key: Preferences.Key): Flow } diff --git a/core/data-api/src/main/java/com/neki/android/core/dataapi/repository/TokenRepository.kt b/core/data-api/src/main/java/com/neki/android/core/dataapi/repository/TokenRepository.kt new file mode 100644 index 000000000..a0227f5df --- /dev/null +++ b/core/data-api/src/main/java/com/neki/android/core/dataapi/repository/TokenRepository.kt @@ -0,0 +1,14 @@ +package com.neki.android.core.dataapi.repository + +import kotlinx.coroutines.flow.Flow + +interface TokenRepository { + suspend fun saveTokens( + accessToken: String, + refreshToken: String, + ) + fun isSavedTokens(): Flow + fun getAccessToken(): Flow + fun getRefreshToken(): Flow + suspend fun clearTokens() +} diff --git a/core/data/src/main/java/com/neki/android/core/data/local/di/DataStoreModule.kt b/core/data/src/main/java/com/neki/android/core/data/local/di/DataStoreModule.kt index 17cc664db..568cc7405 100644 --- a/core/data/src/main/java/com/neki/android/core/data/local/di/DataStoreModule.kt +++ b/core/data/src/main/java/com/neki/android/core/data/local/di/DataStoreModule.kt @@ -2,11 +2,8 @@ package com.neki.android.core.data.local.di import android.content.Context import androidx.datastore.core.DataStore -import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler -import androidx.datastore.preferences.core.PreferenceDataStoreFactory import androidx.datastore.preferences.core.Preferences -import androidx.datastore.preferences.core.emptyPreferences -import androidx.datastore.preferences.preferencesDataStoreFile +import androidx.datastore.preferences.preferencesDataStore import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -14,21 +11,23 @@ import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import javax.inject.Singleton +private const val AUTH_DATASTORE = "auth_datastore" +private val Context.authDataStore: DataStore by preferencesDataStore(name = AUTH_DATASTORE) + +private const val TOKEN_DATASTORE = "token_datastore" +private val Context.tokenDataStore: DataStore by preferencesDataStore(name = TOKEN_DATASTORE) + @InstallIn(SingletonComponent::class) @Module internal object DataStoreModule { - private const val DATASTORE_NAME = "neki-datastore" + @AuthDataStore + @Singleton + @Provides + fun provideAuthDataStore(@ApplicationContext context: Context): DataStore = context.authDataStore + + @TokenDataStore @Singleton @Provides - fun provideDataStore( - @ApplicationContext context: Context, - ): DataStore { - return PreferenceDataStoreFactory.create( - corruptionHandler = ReplaceFileCorruptionHandler( - produceNewData = { emptyPreferences() }, - ), - produceFile = { context.preferencesDataStoreFile(DATASTORE_NAME) }, - ) - } + fun provideTokenDataStore(@ApplicationContext context: Context): DataStore = context.tokenDataStore } diff --git a/core/data/src/main/java/com/neki/android/core/data/local/di/DataStoreQualifier.kt b/core/data/src/main/java/com/neki/android/core/data/local/di/DataStoreQualifier.kt new file mode 100644 index 000000000..87863741f --- /dev/null +++ b/core/data/src/main/java/com/neki/android/core/data/local/di/DataStoreQualifier.kt @@ -0,0 +1,11 @@ +package com.neki.android.core.data.local.di + +import javax.inject.Qualifier + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class AuthDataStore + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class TokenDataStore diff --git a/core/data/src/main/java/com/neki/android/core/data/remote/di/NetworkModule.kt b/core/data/src/main/java/com/neki/android/core/data/remote/di/NetworkModule.kt index 93751a12b..dcc77eca1 100644 --- a/core/data/src/main/java/com/neki/android/core/data/remote/di/NetworkModule.kt +++ b/core/data/src/main/java/com/neki/android/core/data/remote/di/NetworkModule.kt @@ -8,7 +8,7 @@ import com.neki.android.core.data.remote.model.response.AuthResponse import com.neki.android.core.data.remote.model.response.BasicResponse import com.neki.android.core.data.remote.qualifier.UploadHttpClient import com.neki.android.core.dataapi.auth.AuthEventManager -import com.neki.android.core.dataapi.repository.DataStoreRepository +import com.neki.android.core.dataapi.repository.TokenRepository import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -45,7 +45,7 @@ internal object NetworkModule { const val TIME_OUT = 5000L const val UPLOAD_TIME_OUT = 10_000L - val sendWithoutJwtUrls = listOf( + val sendWithoutAuthUrls = listOf( "/api/auth/kakao/login", "/api/auth/refresh", ) @@ -66,7 +66,7 @@ internal object NetworkModule { @Provides @Singleton fun provideHttpClient( - dataStoreRepository: DataStoreRepository, + tokenRepository: TokenRepository, authEventManager: AuthEventManager, ): HttpClient { return HttpClient(Android) { @@ -79,10 +79,10 @@ internal object NetworkModule { bearer { loadTokens { Timber.d("BearerAuth - loadTokens") - if (dataStoreRepository.isSavedJwtTokens().first()) { + if (tokenRepository.isSavedTokens().first()) { BearerTokens( - accessToken = dataStoreRepository.getAccessToken().first(), - refreshToken = dataStoreRepository.getRefreshToken().first(), + accessToken = tokenRepository.getAccessToken().first(), + refreshToken = tokenRepository.getRefreshToken().first(), ) } else null } @@ -94,12 +94,12 @@ internal object NetworkModule { val response = client.post("/api/auth/refresh") { setBody( RefreshTokenRequest( - refreshToken = dataStoreRepository.getRefreshToken().first(), + refreshToken = tokenRepository.getRefreshToken().first(), ), ) }.body>() - dataStoreRepository.saveJwtTokens( + tokenRepository.saveTokens( accessToken = response.data.accessToken, refreshToken = response.data.refreshToken, ) @@ -110,7 +110,7 @@ internal object NetworkModule { ) } catch (e: Exception) { Timber.e(e) - dataStoreRepository.clearTokens() + tokenRepository.clearTokens() authEventManager.emitTokenExpired() null } @@ -118,12 +118,12 @@ internal object NetworkModule { } sendWithoutRequest { request -> - val shouldNotJwt = sendWithoutJwtUrls.any { + val shouldNotAuth = sendWithoutAuthUrls.any { request.url.encodedPath == it } - Timber.d("Bearer 인증 필요 API 여부 : $shouldNotJwt") - !shouldNotJwt + Timber.d("Bearer 인증 필요 API 여부 : $shouldNotAuth") + !shouldNotAuth } } } diff --git a/core/data/src/main/java/com/neki/android/core/data/repository/di/RepositoryModule.kt b/core/data/src/main/java/com/neki/android/core/data/repository/di/RepositoryModule.kt index 75e2f4edb..018f02a4c 100644 --- a/core/data/src/main/java/com/neki/android/core/data/repository/di/RepositoryModule.kt +++ b/core/data/src/main/java/com/neki/android/core/data/repository/di/RepositoryModule.kt @@ -5,11 +5,13 @@ import com.neki.android.core.data.repository.impl.AuthRepositoryImpl import com.neki.android.core.data.repository.impl.DataStoreRepositoryImpl import com.neki.android.core.data.repository.impl.MediaUploadRepositoryImpl import com.neki.android.core.data.repository.impl.PhotoRepositoryImpl +import com.neki.android.core.data.repository.impl.TokenRepositoryImpl import com.neki.android.core.dataapi.auth.AuthEventManager import com.neki.android.core.dataapi.repository.AuthRepository import com.neki.android.core.dataapi.repository.DataStoreRepository import com.neki.android.core.dataapi.repository.MediaUploadRepository import com.neki.android.core.dataapi.repository.PhotoRepository +import com.neki.android.core.dataapi.repository.TokenRepository import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -32,6 +34,12 @@ internal interface RepositoryModule { authRepositoryImpl: AuthRepositoryImpl, ): AuthRepository + @Binds + @Singleton + fun bindTokenRepositoryImpl( + tokenRepositoryImpl: TokenRepositoryImpl, + ): TokenRepository + @Binds @Singleton fun bindAuthEventManagerImpl( diff --git a/core/data/src/main/java/com/neki/android/core/data/repository/impl/DataStoreRepositoryImpl.kt b/core/data/src/main/java/com/neki/android/core/data/repository/impl/DataStoreRepositoryImpl.kt index f0efa98f4..64bd4ca1c 100644 --- a/core/data/src/main/java/com/neki/android/core/data/repository/impl/DataStoreRepositoryImpl.kt +++ b/core/data/src/main/java/com/neki/android/core/data/repository/impl/DataStoreRepositoryImpl.kt @@ -3,55 +3,16 @@ package com.neki.android.core.data.repository.impl import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.edit -import com.neki.android.core.common.crypto.CryptoManager -import com.neki.android.core.dataapi.datastore.DataStoreKey import com.neki.android.core.dataapi.repository.DataStoreRepository import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map +import com.neki.android.core.data.local.di.AuthDataStore import javax.inject.Inject class DataStoreRepositoryImpl @Inject constructor( - private val dataStore: DataStore, + @AuthDataStore private val dataStore: DataStore, ) : DataStoreRepository { - override suspend fun saveJwtTokens( - accessToken: String, - refreshToken: String, - ) { - dataStore.edit { preferences -> - preferences[DataStoreKey.ACCESS_TOKEN] = CryptoManager.encrypt(accessToken) - preferences[DataStoreKey.REFRESH_TOKEN] = CryptoManager.encrypt(refreshToken) - } - } - - override fun isSavedJwtTokens(): Flow { - return dataStore.data.map { preferences -> - val accessToken = preferences[DataStoreKey.ACCESS_TOKEN]?.let { CryptoManager.decrypt(it) } - val refreshToken = preferences[DataStoreKey.REFRESH_TOKEN]?.let { CryptoManager.decrypt(it) } - - !accessToken.isNullOrBlank() && !refreshToken.isNullOrBlank() - } - } - - override fun getAccessToken(): Flow { - return dataStore.data.map { preferences -> - preferences[DataStoreKey.ACCESS_TOKEN]?.let { CryptoManager.decrypt(it) } ?: "" - } - } - - override fun getRefreshToken(): Flow { - return dataStore.data.map { preferences -> - preferences[DataStoreKey.REFRESH_TOKEN]?.let { CryptoManager.decrypt(it) } ?: "" - } - } - - override suspend fun clearTokens() { - dataStore.edit { preferences -> - preferences.remove(DataStoreKey.ACCESS_TOKEN) - preferences.remove(DataStoreKey.REFRESH_TOKEN) - } - } - override suspend fun setBoolean(key: Preferences.Key, value: Boolean) { dataStore.edit { preferences -> preferences[key] = value diff --git a/core/data/src/main/java/com/neki/android/core/data/repository/impl/TokenRepositoryImpl.kt b/core/data/src/main/java/com/neki/android/core/data/repository/impl/TokenRepositoryImpl.kt new file mode 100644 index 000000000..3ce7c6286 --- /dev/null +++ b/core/data/src/main/java/com/neki/android/core/data/repository/impl/TokenRepositoryImpl.kt @@ -0,0 +1,54 @@ +package com.neki.android.core.data.repository.impl + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import com.neki.android.core.common.crypto.CryptoManager +import com.neki.android.core.dataapi.datastore.DataStoreKey +import com.neki.android.core.dataapi.repository.TokenRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import com.neki.android.core.data.local.di.TokenDataStore +import javax.inject.Inject + +class TokenRepositoryImpl @Inject constructor( + @TokenDataStore private val dataStore: DataStore, +) : TokenRepository { + override suspend fun saveTokens( + accessToken: String, + refreshToken: String, + ) { + dataStore.edit { preferences -> + preferences[DataStoreKey.ACCESS_TOKEN] = CryptoManager.encrypt(accessToken) + preferences[DataStoreKey.REFRESH_TOKEN] = CryptoManager.encrypt(refreshToken) + } + } + + override fun isSavedTokens(): Flow { + return dataStore.data.map { preferences -> + val accessToken = preferences[DataStoreKey.ACCESS_TOKEN]?.let { CryptoManager.decrypt(it) } + val refreshToken = preferences[DataStoreKey.REFRESH_TOKEN]?.let { CryptoManager.decrypt(it) } + + !accessToken.isNullOrBlank() && !refreshToken.isNullOrBlank() + } + } + + override fun getAccessToken(): Flow { + return dataStore.data.map { preferences -> + preferences[DataStoreKey.ACCESS_TOKEN]?.let { CryptoManager.decrypt(it) } ?: "" + } + } + + override fun getRefreshToken(): Flow { + return dataStore.data.map { preferences -> + preferences[DataStoreKey.REFRESH_TOKEN]?.let { CryptoManager.decrypt(it) } ?: "" + } + } + + override suspend fun clearTokens() { + dataStore.edit { preferences -> + preferences.remove(DataStoreKey.ACCESS_TOKEN) + preferences.remove(DataStoreKey.REFRESH_TOKEN) + } + } +} diff --git a/feature/auth/impl/src/main/kotlin/com/neki/android/feature/auth/impl/LoginViewModel.kt b/feature/auth/impl/src/main/kotlin/com/neki/android/feature/auth/impl/LoginViewModel.kt index f850089c1..be41c5717 100644 --- a/feature/auth/impl/src/main/kotlin/com/neki/android/feature/auth/impl/LoginViewModel.kt +++ b/feature/auth/impl/src/main/kotlin/com/neki/android/feature/auth/impl/LoginViewModel.kt @@ -4,7 +4,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.neki.android.core.dataapi.auth.AuthEventManager import com.neki.android.core.dataapi.repository.AuthRepository -import com.neki.android.core.dataapi.repository.DataStoreRepository +import com.neki.android.core.dataapi.repository.TokenRepository import com.neki.android.core.ui.MviIntentStore import com.neki.android.core.ui.mviIntentStore import dagger.hilt.android.lifecycle.HiltViewModel @@ -16,7 +16,7 @@ import javax.inject.Inject @HiltViewModel class LoginViewModel @Inject constructor( private val authEventManager: AuthEventManager, - private val dataStoreRepository: DataStoreRepository, + private val tokenRepository: TokenRepository, private val authRepository: AuthRepository, ) : ViewModel() { val store: MviIntentStore = @@ -43,10 +43,10 @@ class LoginViewModel @Inject constructor( reduce: (LoginState.() -> LoginState) -> Unit, postSideEffect: (LoginSideEffect) -> Unit, ) = viewModelScope.launch { - if (dataStoreRepository.isSavedJwtTokens().first()) { + if (tokenRepository.isSavedTokens().first()) { Timber.d("JWT 토큰 O") authRepository.updateAccessToken( - refreshToken = dataStoreRepository.getRefreshToken().first(), + refreshToken = tokenRepository.getRefreshToken().first(), ).onSuccess { postSideEffect(LoginSideEffect.NavigateToHome) }.onFailure { @@ -66,7 +66,7 @@ class LoginViewModel @Inject constructor( reduce { copy(isLoading = true) } authRepository.loginWithKakao(idToken) .onSuccess { - dataStoreRepository.saveJwtTokens( + tokenRepository.saveTokens( accessToken = it.accessToken, refreshToken = it.refreshToken, ) From 073953834a509d06a63ecc5e6921d593e6f9f5a1 Mon Sep 17 00:00:00 2001 From: Ojongseok Date: Sun, 25 Jan 2026 11:50:08 +0900 Subject: [PATCH 30/37] =?UTF-8?q?[fix]=20#51=20=ED=98=84=EC=9C=84=EC=B9=98?= =?UTF-8?q?=20=EC=A1=B0=ED=9A=8C=ED=95=98=EC=A7=80=20=EB=AA=BB=ED=95=9C=20?= =?UTF-8?q?=EA=B2=BD=EC=9A=B0=20=ED=86=A0=EC=8A=A4=ED=8A=B8=EB=A9=94?= =?UTF-8?q?=EC=8B=9C=EC=A7=80=20NekiToast=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 --- .../com/neki/android/feature/map/impl/MapScreen.kt | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapScreen.kt b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapScreen.kt index 9e7f1ad09..57e34d88f 100644 --- a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapScreen.kt +++ b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapScreen.kt @@ -1,6 +1,5 @@ package com.neki.android.feature.map.impl -import android.widget.Toast import androidx.activity.compose.LocalActivity import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts @@ -15,7 +14,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue -import kotlinx.coroutines.launch import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -35,20 +33,22 @@ import com.naver.maps.map.compose.MapUiSettings import com.naver.maps.map.compose.NaverMap import com.naver.maps.map.compose.rememberCameraPositionState import com.naver.maps.map.compose.rememberFusedLocationSource +import com.neki.android.core.common.permission.LocationPermissionManager +import com.neki.android.core.common.permission.navigateToAppSettings import com.neki.android.core.designsystem.dialog.SingleButtonAlertDialog import com.neki.android.core.designsystem.dialog.WarningDialog import com.neki.android.core.ui.compose.collectWithLifecycle +import com.neki.android.core.ui.toast.NekiToast import com.neki.android.feature.map.impl.component.AnchoredDraggablePanel -import com.neki.android.feature.map.impl.component.PhotoBoothMarker import com.neki.android.feature.map.impl.component.DirectionBottomSheet import com.neki.android.feature.map.impl.component.MapRefreshChip import com.neki.android.feature.map.impl.component.PhotoBoothDetailCard +import com.neki.android.feature.map.impl.component.PhotoBoothMarker import com.neki.android.feature.map.impl.component.ToMapChip -import com.neki.android.core.common.permission.LocationPermissionManager -import com.neki.android.core.common.permission.navigateToAppSettings import com.neki.android.feature.map.impl.const.DirectionApp import com.neki.android.feature.map.impl.util.DirectionHelper import com.neki.android.feature.map.impl.util.getPlaceName +import kotlinx.coroutines.launch @OptIn(ExperimentalNaverMapApi::class) @Composable @@ -64,6 +64,7 @@ fun MapRoute( val cameraPositionState = rememberCameraPositionState { position = CameraPosition(LatLng(37.5269278, 126.886225), 17.0) } + val nekiToast = remember { NekiToast(context) } val locationPermissionLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.RequestMultiplePermissions(), @@ -90,7 +91,7 @@ fun MapRoute( locationTrackingMode = LocationTrackingMode.Follow } is MapEffect.ShowToastMessage -> { - Toast.makeText(context, sideEffect.message, Toast.LENGTH_SHORT).show() + nekiToast.showToast(sideEffect.message) } is MapEffect.MoveCameraToPosition -> { coroutineScope.launch { From d409dd729f1d6c54c185469356effd72a3fe7a4f Mon Sep 17 00:00:00 2001 From: Ojongseok Date: Sun, 25 Jan 2026 12:05:04 +0900 Subject: [PATCH 31/37] =?UTF-8?q?[fix]=20#51=20=EC=9C=84=EC=B9=98=20?= =?UTF-8?q?=EA=B6=8C=ED=95=9C=20=EC=97=AC=EB=B6=80=EC=97=90=20=EB=94=B0?= =?UTF-8?q?=EB=A5=B8=20Intent,=20Effect=20=EC=B2=98=EB=A6=AC=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../android/feature/map/impl/MapContract.kt | 7 ++- .../android/feature/map/impl/MapScreen.kt | 49 +++++++++---------- .../android/feature/map/impl/MapViewModel.kt | 3 +- 3 files changed, 29 insertions(+), 30 deletions(-) diff --git a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapContract.kt b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapContract.kt index 29de67bdd..a947780d8 100644 --- a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapContract.kt +++ b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapContract.kt @@ -27,10 +27,7 @@ sealed interface MapIntent { val latitude: Double, val longitude: Double, ) : MapIntent - data class ClickDirection( - val latitude: Double, - val longitude: Double, - ) : MapIntent + data object ClickDirection : MapIntent data object ClickRefresh : MapIntent data class UpdateCurrentLocation(val latitude: Double, val longitude: Double) : MapIntent @@ -42,6 +39,7 @@ sealed interface MapIntent { data class ClickBrand(val brand: Brand) : MapIntent data class ClickNearPhotoBooth(val brandInfo: BrandInfo) : MapIntent data object ClickClosePhotoBoothCard : MapIntent + data object OpenDirectionBottomSheet : MapIntent data object CloseDirectionBottomSheet : MapIntent data class ClickDirectionItem(val app: DirectionApp) : MapIntent data class ChangeDragLevel(val dragLevel: DragLevel) : MapIntent @@ -55,6 +53,7 @@ sealed interface MapIntent { sealed interface MapEffect { data object RefreshPhotoBooth : MapEffect data object RefreshCurrentLocation : MapEffect + data object OpenDirectionBottomSheet : MapEffect data class ShowToastMessage(val message: String) : MapEffect data class MoveCameraToPosition(val latitude: Double, val longitude: Double) : MapEffect diff --git a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapScreen.kt b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapScreen.kt index 57e34d88f..777641e7b 100644 --- a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapScreen.kt +++ b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapScreen.kt @@ -88,7 +88,27 @@ fun MapRoute( // TODO: 포토부스 새로고침 로직 구현 } is MapEffect.RefreshCurrentLocation -> { - locationTrackingMode = LocationTrackingMode.Follow + if (LocationPermissionManager.hasLocationPermission(context)) { + locationTrackingMode = LocationTrackingMode.Follow + } else { + viewModel.store.onIntent( + MapIntent.RequestLocationPermission( + LocationPermissionManager.shouldShowLocationRationale(activity), + ), + ) + } + } + + is MapEffect.OpenDirectionBottomSheet -> { + if (LocationPermissionManager.hasLocationPermission(context)) { + viewModel.store.onIntent(MapIntent.OpenDirectionBottomSheet) + } else { + viewModel.store.onIntent( + MapIntent.RequestLocationPermission( + LocationPermissionManager.shouldShowLocationRationale(activity), + ), + ) + } } is MapEffect.ShowToastMessage -> { nekiToast.showToast(sideEffect.message) @@ -170,9 +190,6 @@ fun MapScreen( position = CameraPosition(LatLng(37.5269278, 126.886225), 17.0) }, ) { - val context = LocalContext.current - val activity = LocalActivity.current!! - val mapProperties = remember(locationTrackingMode) { MapProperties( locationTrackingMode = locationTrackingMode, @@ -225,13 +242,7 @@ fun MapScreen( onDragLevelChanged = { onIntent(MapIntent.ChangeDragLevel(it)) }, onClickInfoIcon = { onIntent(MapIntent.ClickInfoIcon) }, isCurrentLocation = locationTrackingMode == LocationTrackingMode.Follow, - onClickCurrentLocation = { - if (LocationPermissionManager.hasLocationPermission(context)) { - onLocationTrackingModeChange(LocationTrackingMode.Follow) - } else { - onIntent(MapIntent.RequestLocationPermission(LocationPermissionManager.shouldShowLocationRationale(activity))) - } - }, + onClickCurrentLocation = { onIntent(MapIntent.ClickCurrentLocation) }, onClickBrand = { onIntent(MapIntent.ClickBrand(it)) }, onClickNearBrand = { onIntent(MapIntent.ClickNearPhotoBooth(it)) }, ) @@ -260,21 +271,9 @@ fun MapScreen( brandInfo = uiState.selectedPhotoBoothInfo, modifier = Modifier.align(Alignment.BottomCenter), isCurrentLocation = locationTrackingMode == LocationTrackingMode.Follow, - onClickCurrentLocation = { - if (LocationPermissionManager.hasLocationPermission(context)) { - onIntent(MapIntent.ClickCurrentLocation) - } else { - onIntent(MapIntent.RequestLocationPermission(LocationPermissionManager.shouldShowLocationRationale(activity))) - } - }, + onClickCurrentLocation = { onIntent(MapIntent.ClickCurrentLocation) }, onClickCloseCard = { onIntent(MapIntent.ClickClosePhotoBoothCard) }, - onClickDirection = { - if (LocationPermissionManager.hasLocationPermission(context)) { - onIntent(MapIntent.ClickDirection(uiState.selectedPhotoBoothInfo.latitude, uiState.selectedPhotoBoothInfo.longitude)) - } else { - onIntent(MapIntent.RequestLocationPermission(LocationPermissionManager.shouldShowLocationRationale(activity))) - } - }, + onClickDirection = { onIntent(MapIntent.ClickDirection) }, ) } } diff --git a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapViewModel.kt b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapViewModel.kt index a65b96464..e219dd93c 100644 --- a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapViewModel.kt +++ b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapViewModel.kt @@ -49,11 +49,12 @@ class MapViewModel @Inject constructor( selectedPhotoBoothInfo = null, ) } + MapIntent.OpenDirectionBottomSheet -> reduce { copy(isShowDirectionBottomSheet = true) } MapIntent.CloseDirectionBottomSheet -> reduce { copy(isShowDirectionBottomSheet = false) } is MapIntent.ClickDirectionItem -> handleClickDirectionItem(state, intent, reduce, postSideEffect) is MapIntent.ChangeDragLevel -> reduce { copy(dragLevel = intent.dragLevel) } is MapIntent.ClickPhotoBoothMarker -> handleClickBrandMarker(state, intent, reduce, postSideEffect) - is MapIntent.ClickDirection -> reduce { copy(isShowDirectionBottomSheet = true) } + MapIntent.ClickDirection -> postSideEffect(MapEffect.OpenDirectionBottomSheet) is MapIntent.RequestLocationPermission -> handleRequestLocationPermission(intent, reduce, postSideEffect) MapIntent.DismissLocationPermissionDialog -> reduce { copy(isShowLocationPermissionDialog = false) } MapIntent.ConfirmLocationPermissionDialog -> { From dbf64bbace31ca0b193a770b99df609f4fb123ec Mon Sep 17 00:00:00 2001 From: Ojongseok Date: Sun, 25 Jan 2026 12:37:35 +0900 Subject: [PATCH 32/37] =?UTF-8?q?[fix]=20#51=20=EC=9C=84=EC=B9=98=EA=B6=8C?= =?UTF-8?q?=ED=95=9C=20=EC=97=AC=EB=B6=80=20=EC=B2=B4=ED=81=AC=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=EC=84=B8=EC=8A=A4=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../android/feature/map/impl/MapContract.kt | 3 +- .../android/feature/map/impl/MapScreen.kt | 24 ++++++----- .../android/feature/map/impl/MapViewModel.kt | 42 ++----------------- 3 files changed, 18 insertions(+), 51 deletions(-) diff --git a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapContract.kt b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapContract.kt index a947780d8..505ee659f 100644 --- a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapContract.kt +++ b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapContract.kt @@ -45,7 +45,8 @@ sealed interface MapIntent { data class ChangeDragLevel(val dragLevel: DragLevel) : MapIntent // 위치 권한 - data class RequestLocationPermission(val shouldShowRationale: Boolean) : MapIntent + data object RequestLocationPermission : MapIntent + data object ShowLocationPermissionDialog : MapIntent data object DismissLocationPermissionDialog : MapIntent data object ConfirmLocationPermissionDialog : MapIntent } diff --git a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapScreen.kt b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapScreen.kt index 777641e7b..7991fbb06 100644 --- a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapScreen.kt +++ b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapScreen.kt @@ -49,6 +49,7 @@ import com.neki.android.feature.map.impl.const.DirectionApp import com.neki.android.feature.map.impl.util.DirectionHelper import com.neki.android.feature.map.impl.util.getPlaceName import kotlinx.coroutines.launch +import timber.log.Timber @OptIn(ExperimentalNaverMapApi::class) @Composable @@ -70,14 +71,23 @@ fun MapRoute( contract = ActivityResultContracts.RequestMultiplePermissions(), ) { permissions -> val isGranted = permissions.values.any { it } + val shouldShowRationale = LocationPermissionManager.shouldShowLocationRationale(activity) + if (isGranted) { locationTrackingMode = LocationTrackingMode.Follow + } else { + if (!shouldShowRationale) { + Timber.d("영구 거부 상태 → 설정 이동 다이얼로그 표시") + viewModel.store.onIntent(MapIntent.ShowLocationPermissionDialog) + } else { + Timber.d("1회 거부 상태 → 다음에 다시 요청 가능") + } } } LaunchedEffect(Unit) { if (!LocationPermissionManager.hasLocationPermission(context)) { - viewModel.store.onIntent(MapIntent.RequestLocationPermission(LocationPermissionManager.shouldShowLocationRationale(activity))) + viewModel.store.onIntent(MapIntent.RequestLocationPermission) } viewModel.store.onIntent(MapIntent.EnterMapScreen) } @@ -91,11 +101,7 @@ fun MapRoute( if (LocationPermissionManager.hasLocationPermission(context)) { locationTrackingMode = LocationTrackingMode.Follow } else { - viewModel.store.onIntent( - MapIntent.RequestLocationPermission( - LocationPermissionManager.shouldShowLocationRationale(activity), - ), - ) + viewModel.store.onIntent(MapIntent.RequestLocationPermission) } } @@ -103,11 +109,7 @@ fun MapRoute( if (LocationPermissionManager.hasLocationPermission(context)) { viewModel.store.onIntent(MapIntent.OpenDirectionBottomSheet) } else { - viewModel.store.onIntent( - MapIntent.RequestLocationPermission( - LocationPermissionManager.shouldShowLocationRationale(activity), - ), - ) + viewModel.store.onIntent(MapIntent.RequestLocationPermission) } } is MapEffect.ShowToastMessage -> { diff --git a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapViewModel.kt b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapViewModel.kt index e219dd93c..8682e4e6a 100644 --- a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapViewModel.kt +++ b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapViewModel.kt @@ -2,8 +2,6 @@ package com.neki.android.feature.map.impl import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.neki.android.core.dataapi.datastore.DataStoreKey -import com.neki.android.core.dataapi.repository.DataStoreRepository import com.neki.android.core.designsystem.R import com.neki.android.core.model.Brand import com.neki.android.core.model.BrandInfo @@ -12,15 +10,11 @@ import com.neki.android.core.ui.mviIntentStore import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch -import timber.log.Timber import javax.inject.Inject @HiltViewModel -class MapViewModel @Inject constructor( - private val dataStoreRepository: DataStoreRepository, -) : ViewModel() { +class MapViewModel @Inject constructor() : ViewModel() { val store: MviIntentStore = mviIntentStore( initialState = MapState(), onIntent = ::onIntent, @@ -55,7 +49,8 @@ class MapViewModel @Inject constructor( is MapIntent.ChangeDragLevel -> reduce { copy(dragLevel = intent.dragLevel) } is MapIntent.ClickPhotoBoothMarker -> handleClickBrandMarker(state, intent, reduce, postSideEffect) MapIntent.ClickDirection -> postSideEffect(MapEffect.OpenDirectionBottomSheet) - is MapIntent.RequestLocationPermission -> handleRequestLocationPermission(intent, reduce, postSideEffect) + MapIntent.RequestLocationPermission -> postSideEffect(MapEffect.RequestLocationPermission) + MapIntent.ShowLocationPermissionDialog -> reduce { copy(isShowLocationPermissionDialog = true) } MapIntent.DismissLocationPermissionDialog -> reduce { copy(isShowLocationPermissionDialog = false) } MapIntent.ConfirmLocationPermissionDialog -> { reduce { copy(isShowLocationPermissionDialog = false) } @@ -138,37 +133,6 @@ class MapViewModel @Inject constructor( postSideEffect(MapEffect.MoveCameraToPosition(intent.latitude, intent.longitude)) } - private fun handleRequestLocationPermission( - intent: MapIntent.RequestLocationPermission, - reduce: (MapState.() -> MapState) -> Unit, - postSideEffect: (MapEffect) -> Unit, - ) { - viewModelScope.launch { - val isFirstRequest = dataStoreRepository.getBoolean(DataStoreKey.IS_FIRST_LOCATION_PERMISSION).first().not() - - when { - isFirstRequest -> { - Timber.d("최초 요청 - 시스템 권한 팝업 표시") - dataStoreRepository.setBoolean( - DataStoreKey.IS_FIRST_LOCATION_PERMISSION, - true, - ) - postSideEffect(MapEffect.RequestLocationPermission) - } - - intent.shouldShowRationale -> { - Timber.d("1회 거부 상태 - 시스템 권한 팝업 표시") - postSideEffect(MapEffect.RequestLocationPermission) - } - - else -> { - Timber.d("2회 이상 거부 (영구 거부) - 설정 이동 다이얼로그 표시") - reduce { copy(isShowLocationPermissionDialog = true) } - } - } - } - } - private fun loadBrands(reduce: (MapState.() -> MapState) -> Unit) { viewModelScope.launch { reduce { copy(isLoading = true) } From d62e281db8f7b517aaf12f336432ebbfd0dbba53 Mon Sep 17 00:00:00 2001 From: Ojongseok Date: Sun, 25 Jan 2026 12:38:39 +0900 Subject: [PATCH 33/37] =?UTF-8?q?[fix]=20#51=20=ED=8C=A8=EB=84=90=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EC=83=81=EC=88=98=20=EB=84=A4=EC=9D=B4?= =?UTF-8?q?=EB=B0=8D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../feature/map/impl/component/AnchoredDraggablePanel.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/AnchoredDraggablePanel.kt b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/AnchoredDraggablePanel.kt index 9dc80f8da..443dcc068 100644 --- a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/AnchoredDraggablePanel.kt +++ b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/AnchoredDraggablePanel.kt @@ -80,12 +80,12 @@ internal fun AnchoredDraggablePanel( val bottomPanelHeightPx = with(density) { (MapConst.BOTTOM_NAVIGATION_BAR_HEIGHT + MapConst.PANEL_DRAG_LOCATION_HEIGHT + - MapConst.PANEL_DRAG_LEVEL_BOTTOM_HEIGHT).dp.toPx() + navigationBarHeightPx + MapConst.PANEL_DRAG_LEVEL_FIRST_HEIGHT).dp.toPx() + navigationBarHeightPx } val centerPanelHeightPx = with(density) { (MapConst.BOTTOM_NAVIGATION_BAR_HEIGHT + MapConst.PANEL_DRAG_LOCATION_HEIGHT + - MapConst.PANEL_DRAG_LEVEL_CENTER_HEIGHT).dp.toPx() + navigationBarHeightPx + MapConst.PANEL_DRAG_LEVEL_SECOND_HEIGHT).dp.toPx() + navigationBarHeightPx } var isProgrammaticTransition by remember { mutableStateOf(false) } From 42ea2477fdd27cfc1eeac12623e1e6a90b71b96c Mon Sep 17 00:00:00 2001 From: Ojongseok Date: Sun, 25 Jan 2026 16:15:40 +0900 Subject: [PATCH 34/37] =?UTF-8?q?[fix]=20#51=20=ED=86=A0=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EB=A9=94=EC=8B=9C=EC=A7=80=20remember=20=EB=B3=80=EC=88=98=20?= =?UTF-8?q?=EC=A0=95=EC=9D=98=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/neki/android/feature/map/impl/MapScreen.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapScreen.kt b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapScreen.kt index 7991fbb06..b0bc744db 100644 --- a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapScreen.kt +++ b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapScreen.kt @@ -65,7 +65,9 @@ fun MapRoute( val cameraPositionState = rememberCameraPositionState { position = CameraPosition(LatLng(37.5269278, 126.886225), 17.0) } - val nekiToast = remember { NekiToast(context) } + + // 권한 요청 전 shouldShowRationale 상태 저장 + var previousShouldShowRationale by remember { mutableStateOf(false) } val locationPermissionLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.RequestMultiplePermissions(), @@ -113,7 +115,7 @@ fun MapRoute( } } is MapEffect.ShowToastMessage -> { - nekiToast.showToast(sideEffect.message) + NekiToast(context).showToast(sideEffect.message) } is MapEffect.MoveCameraToPosition -> { coroutineScope.launch { From 7647462d319e665bfc665872de7ade10b6db9a10 Mon Sep 17 00:00:00 2001 From: Ojongseok Date: Sun, 25 Jan 2026 16:25:47 +0900 Subject: [PATCH 35/37] =?UTF-8?q?[fix]=20#51=20=EC=9C=84=EC=B9=98=20?= =?UTF-8?q?=EA=B6=8C=ED=95=9C=20=EC=9D=B4=EC=A0=84=20shouldShowRationale?= =?UTF-8?q?=20=ED=99=95=EC=9D=B8=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/neki/android/feature/map/impl/MapScreen.kt | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapScreen.kt b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapScreen.kt index b0bc744db..b4b1d7967 100644 --- a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapScreen.kt +++ b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapScreen.kt @@ -66,23 +66,20 @@ fun MapRoute( position = CameraPosition(LatLng(37.5269278, 126.886225), 17.0) } - // 권한 요청 전 shouldShowRationale 상태 저장 var previousShouldShowRationale by remember { mutableStateOf(false) } val locationPermissionLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.RequestMultiplePermissions(), ) { permissions -> val isGranted = permissions.values.any { it } - val shouldShowRationale = LocationPermissionManager.shouldShowLocationRationale(activity) + val currentShouldShowRationale = LocationPermissionManager.shouldShowLocationRationale(activity) if (isGranted) { locationTrackingMode = LocationTrackingMode.Follow } else { - if (!shouldShowRationale) { - Timber.d("영구 거부 상태 → 설정 이동 다이얼로그 표시") + if (!currentShouldShowRationale && !previousShouldShowRationale) { + // 2회 이상 거부 viewModel.store.onIntent(MapIntent.ShowLocationPermissionDialog) - } else { - Timber.d("1회 거부 상태 → 다음에 다시 요청 가능") } } } @@ -169,6 +166,7 @@ fun MapRoute( } is MapEffect.RequestLocationPermission -> { + previousShouldShowRationale = LocationPermissionManager.shouldShowLocationRationale(activity) locationPermissionLauncher.launch(LocationPermissionManager.LOCATION_PERMISSIONS) } } From 68da56cd4b2741fd099a7fc30d11018490be2562 Mon Sep 17 00:00:00 2001 From: Ojongseok Date: Sun, 25 Jan 2026 16:30:24 +0900 Subject: [PATCH 36/37] =?UTF-8?q?[build]=20#51=20detekt=20=EB=A3=B0=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/com/neki/android/feature/map/impl/MapScreen.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapScreen.kt b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapScreen.kt index b4b1d7967..c5772cd30 100644 --- a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapScreen.kt +++ b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/MapScreen.kt @@ -49,7 +49,6 @@ import com.neki.android.feature.map.impl.const.DirectionApp import com.neki.android.feature.map.impl.util.DirectionHelper import com.neki.android.feature.map.impl.util.getPlaceName import kotlinx.coroutines.launch -import timber.log.Timber @OptIn(ExperimentalNaverMapApi::class) @Composable From 10b6dcadbf03a9c5cfbbec1daed0584f2adb566d Mon Sep 17 00:00:00 2001 From: Ojongseok Date: Sun, 25 Jan 2026 23:00:35 +0900 Subject: [PATCH 37/37] =?UTF-8?q?[design]=20#51=20pinShadow()=20=ED=99=95?= =?UTF-8?q?=EC=9E=A5=ED=95=A8=EC=88=98=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20?= =?UTF-8?q?PhotoBoothMarker=20shadow=20=ED=9A=A8=EA=B3=BC=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/designsystem/modifier/Shadow.kt | 28 +++++++++++++++++++ .../map/impl/component/PhotoBoothMarker.kt | 13 ++++----- 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/core/designsystem/src/main/java/com/neki/android/core/designsystem/modifier/Shadow.kt b/core/designsystem/src/main/java/com/neki/android/core/designsystem/modifier/Shadow.kt index db9ac4e23..1d7131d27 100644 --- a/core/designsystem/src/main/java/com/neki/android/core/designsystem/modifier/Shadow.kt +++ b/core/designsystem/src/main/java/com/neki/android/core/designsystem/modifier/Shadow.kt @@ -124,3 +124,31 @@ fun Modifier.dropdownShadow( canvas.drawOutline(outline, paint) } } + +/** + * Figma pin_shadow 스타일 + * DROP_SHADOW: color #00000066, offset (0, 1), radius 2.5, spread 0 + */ +fun Modifier.pinShadow( + shape: Shape = RectangleShape, + color: Color = Color.Black.copy(alpha = 0.40f), + offsetX: Dp = 0.dp, + offsetY: Dp = 1.dp, + blurRadius: Dp = 2.5.dp, +): Modifier = this.drawBehind { + drawIntoCanvas { canvas -> + val paint = Paint().apply { + asFrameworkPaint().apply { + this.color = Color.Transparent.toArgb() + setShadowLayer( + blurRadius.toPx(), + offsetX.toPx(), + offsetY.toPx(), + color.toArgb(), + ) + } + } + val outline = shape.createOutline(size, layoutDirection, this) + canvas.drawOutline(outline, paint) + } +} diff --git a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/PhotoBoothMarker.kt b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/PhotoBoothMarker.kt index 4bf802a5b..e1b41f377 100644 --- a/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/PhotoBoothMarker.kt +++ b/feature/map/impl/src/main/java/com/neki/android/feature/map/impl/component/PhotoBoothMarker.kt @@ -25,7 +25,7 @@ import com.naver.maps.map.compose.MarkerComposable import com.naver.maps.map.compose.rememberUpdatedMarkerState import com.neki.android.core.designsystem.ComponentPreview import com.neki.android.core.designsystem.R -import com.neki.android.core.designsystem.modifier.dropdownShadow +import com.neki.android.core.designsystem.modifier.pinShadow import com.neki.android.core.designsystem.ui.theme.NekiTheme import com.neki.android.feature.map.impl.const.MapConst.FOCUSED_MARKER_BACKGROUND_RADIUS import com.neki.android.feature.map.impl.const.MapConst.FOCUSED_MARKER_IMAGE_RADIUS @@ -93,13 +93,10 @@ internal fun PhotoBoothMarkerContent( Box( modifier = Modifier .size(bodySize.dp) - .dropdownShadow( - color = Color.Black.copy(alpha = 0.38f), + .pinShadow( shape = RoundedCornerShape( if (isFocused) FOCUSED_MARKER_BACKGROUND_RADIUS.dp else MARKER_BACKGROUND_RADIUS.dp, ), - offsetY = 1.18.dp, - blurRadius = 2.55.dp, ), ) @@ -112,9 +109,9 @@ internal fun PhotoBoothMarkerContent( height = MARKER_TRIANGLE_HEIGHT.dp, ), ) { - val shadowColor = Color.Black.copy(alpha = 0.38f) - val offsetY = 1.18.dp.toPx() - val blurRadius = 2.55.dp.toPx() + val shadowColor = Color.Black.copy(alpha = 0.4f) + val offsetY = 1.dp.toPx() + val blurRadius = 2.5.dp.toPx() val path = Path().apply { moveTo(0f, 0f)