diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f177843b..4ff3eb0e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -28,6 +28,8 @@ android { buildConfigField("String", "BASE_URL", properties["base.url"].toString()) buildConfigField("String", "KAKAO_NATIVE_KEY", properties["kakao.native.key"].toString()) buildConfigField("String", "KAKAO_REST_API_KEY", properties["kakao.rest.api"].toString()) + buildConfigField("String", "NAVERMAP_CLIENT_SECRET", properties["NAVERMAP_CLIENT_SECRET"].toString()) + buildConfigField("String", "NAVERMAP_CLIENT_ID", properties["NAVERMAP_CLIENT_ID"].toString()) manifestPlaceholders["KAKAO_NATIVE_KEY"] = properties["kakao.native.key"].toString() } @@ -103,4 +105,7 @@ dependencies { //로띠 - 애니메이션 implementation(libs.lottie.compose) + + // 네이버 + implementation(libs.bundles.naverMaps) } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c135515d..8aecc629 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -28,7 +28,6 @@ diff --git a/app/src/main/java/com/paw/key/PawKeyApplication.kt b/app/src/main/java/com/paw/key/PawKeyApplication.kt index 54735d3f..589170b0 100644 --- a/app/src/main/java/com/paw/key/PawKeyApplication.kt +++ b/app/src/main/java/com/paw/key/PawKeyApplication.kt @@ -3,6 +3,7 @@ package com.paw.key import android.app.Application import androidx.appcompat.app.AppCompatDelegate import com.kakao.vectormap.KakaoMapSdk +import com.naver.maps.map.NaverMapSdk import dagger.hilt.android.HiltAndroidApp import timber.log.Timber import javax.inject.Inject @@ -12,7 +13,7 @@ import javax.inject.Named class PawKeyApplication : Application() { @Inject @Named("kakao.native.key") - lateinit var kakaoNativeKey: String + lateinit var kakaoNativeKey: String // BuildConfig는 컴파일 타임에 생성되는 정적 클래스이기 때문에 Mocking이 불가능 = 테스트 용이성 override fun onCreate() { super.onCreate() @@ -21,6 +22,8 @@ class PawKeyApplication : Application() { setDarkMode() KakaoMapSdk.init(this, kakaoNativeKey) + NaverMapSdk.getInstance(this).client = + NaverMapSdk.NcpKeyClient(BuildConfig.NAVERMAP_CLIENT_ID) } private fun setTimber() { diff --git a/app/src/main/java/com/paw/key/core/designsystem/component/CourseCard.kt b/app/src/main/java/com/paw/key/core/designsystem/component/CourseCard.kt index ebd078b4..37ce3272 100644 --- a/app/src/main/java/com/paw/key/core/designsystem/component/CourseCard.kt +++ b/app/src/main/java/com/paw/key/core/designsystem/component/CourseCard.kt @@ -27,7 +27,7 @@ import coil.compose.AsyncImage import coil.request.ImageRequest import com.paw.key.R import com.paw.key.core.designsystem.theme.PawKeyTheme -import com.paw.key.core.util.noRippleClickable +import com.paw.key.core.extension.noRippleClickable @Composable fun CourseCard( diff --git a/app/src/main/java/com/paw/key/core/designsystem/component/FeedbackItem.kt b/app/src/main/java/com/paw/key/core/designsystem/component/FeedbackItem.kt index e57dc5c0..48c9c887 100644 --- a/app/src/main/java/com/paw/key/core/designsystem/component/FeedbackItem.kt +++ b/app/src/main/java/com/paw/key/core/designsystem/component/FeedbackItem.kt @@ -9,7 +9,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp import com.paw.key.core.designsystem.theme.PawKeyTheme -import com.paw.key.core.util.noRippleClickable +import com.paw.key.core.extension.noRippleClickable @Composable fun FeedbackItem( diff --git a/app/src/main/java/com/paw/key/core/designsystem/component/PawkeyButton.kt b/app/src/main/java/com/paw/key/core/designsystem/component/PawkeyButton.kt index 3c5d0765..aeb4181a 100644 --- a/app/src/main/java/com/paw/key/core/designsystem/component/PawkeyButton.kt +++ b/app/src/main/java/com/paw/key/core/designsystem/component/PawkeyButton.kt @@ -16,7 +16,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.paw.key.core.designsystem.theme.PawKeyTheme -import com.paw.key.core.util.noRippleClickable +import com.paw.key.core.extension.noRippleClickable @Preview(showBackground = true) diff --git a/app/src/main/java/com/paw/key/core/designsystem/component/SubChip.kt b/app/src/main/java/com/paw/key/core/designsystem/component/SubChip.kt index 1a31903c..c56e891b 100644 --- a/app/src/main/java/com/paw/key/core/designsystem/component/SubChip.kt +++ b/app/src/main/java/com/paw/key/core/designsystem/component/SubChip.kt @@ -1,7 +1,6 @@ package com.paw.key.core.designsystem.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.shape.RoundedCornerShape @@ -11,7 +10,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.paw.key.core.designsystem.theme.PawKeyTheme -import com.paw.key.core.util.noRippleClickable +import com.paw.key.core.extension.noRippleClickable @Preview @Composable diff --git a/app/src/main/java/com/paw/key/core/designsystem/component/TopBar.kt b/app/src/main/java/com/paw/key/core/designsystem/component/TopBar.kt index ec89be07..c419932c 100644 --- a/app/src/main/java/com/paw/key/core/designsystem/component/TopBar.kt +++ b/app/src/main/java/com/paw/key/core/designsystem/component/TopBar.kt @@ -15,13 +15,14 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.paw.key.R import com.paw.key.core.designsystem.theme.PawKeyTheme -import com.paw.key.core.util.noRippleClickable +import com.paw.key.core.extension.noRippleClickable @Composable fun TopBar( title: String, onBackClick: () -> Unit, modifier: Modifier = Modifier, + onClickTitle : () -> Unit = {}, isBackVisible: Boolean = true, ) { Box( @@ -45,6 +46,7 @@ fun TopBar( style = PawKeyTheme.typography.head18Sb, modifier = Modifier .align(Alignment.Center) + .noRippleClickable(onClickTitle) ) } } diff --git a/app/src/main/java/com/paw/key/core/extension/LatLngExtension.kt b/app/src/main/java/com/paw/key/core/extension/LatLngExtension.kt new file mode 100644 index 00000000..83ba8a0e --- /dev/null +++ b/app/src/main/java/com/paw/key/core/extension/LatLngExtension.kt @@ -0,0 +1,8 @@ +package com.paw.key.core.extension + +import android.location.Location +import com.naver.maps.geometry.LatLng + +fun Location.toLatLng(): LatLng { + return LatLng(this.latitude, this.longitude) +} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/core/util/Modifier.kt b/app/src/main/java/com/paw/key/core/extension/Modifier.kt similarity index 92% rename from app/src/main/java/com/paw/key/core/util/Modifier.kt rename to app/src/main/java/com/paw/key/core/extension/Modifier.kt index 38cf313f..03de9e45 100644 --- a/app/src/main/java/com/paw/key/core/util/Modifier.kt +++ b/app/src/main/java/com/paw/key/core/extension/Modifier.kt @@ -1,4 +1,4 @@ -package com.paw.key.core.util +package com.paw.key.core.extension import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource diff --git a/app/src/main/java/com/paw/key/core/util/PreferenceDataStore.kt b/app/src/main/java/com/paw/key/core/util/PreferenceDataStore.kt index 1daebdca..843e3b9f 100644 --- a/app/src/main/java/com/paw/key/core/util/PreferenceDataStore.kt +++ b/app/src/main/java/com/paw/key/core/util/PreferenceDataStore.kt @@ -7,7 +7,7 @@ import androidx.datastore.preferences.core.intPreferencesKey import androidx.datastore.preferences.core.longPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.preferencesDataStore -import com.kakao.vectormap.LatLng +import com.naver.maps.geometry.LatLng import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map @@ -35,15 +35,6 @@ private val ACTIVE_REGION_KEY = stringPreferencesKey("active_region") private fun List.toPreferenceString(): String = joinToString(";") { "${it.latitude},${it.longitude}" } -private fun String.toLatLngList(): List = - split(";").mapNotNull { - val parts = it.split(",") - if (parts.size == 2) { - LatLng.from(parts[0].toDoubleOrNull() ?: return@mapNotNull null, parts[1].toDoubleOrNull() ?: return@mapNotNull null) - } - else null - } - object PreferenceDataStore { private lateinit var appContext: Context @@ -70,10 +61,6 @@ object PreferenceDataStore { } } - fun getPoints(): Flow> = summaryStore.data.map { - it[POINTS_KEY]?.toLatLngList() ?: emptyList() - } - fun getTotalDistance(): Flow = summaryStore.data.map { it[TOTAL_DISTANCE_KEY] ?: 0f } @@ -141,7 +128,7 @@ object PreferenceDataStore { } fun getUserId(): Flow = summaryStore.data.map { - it[USER_ID_KEY] ?: 41 + it[USER_ID_KEY] ?: 43 } fun getUserName(): Flow = summaryStore.data.map { diff --git a/app/src/main/java/com/paw/key/core/util/UiState.kt b/app/src/main/java/com/paw/key/core/util/UiState.kt index 2dc486e0..c8731e34 100644 --- a/app/src/main/java/com/paw/key/core/util/UiState.kt +++ b/app/src/main/java/com/paw/key/core/util/UiState.kt @@ -1,15 +1,15 @@ package com.paw.key.core.util -sealed class UiState { - data object Empty : UiState() +sealed interface UiState { + data object Empty : UiState - data object Loading : UiState() + data object Loading : UiState data class Success( val data: T, - ) : UiState() + ) : UiState data class Failure( val message: String, - ) : UiState() + ) : UiState } \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/data/repositoryimpl/WalkSharedResultRepositoryImpl.kt b/app/src/main/java/com/paw/key/data/repositoryimpl/WalkSharedResultRepositoryImpl.kt index 184f8c33..a5ca284a 100644 --- a/app/src/main/java/com/paw/key/data/repositoryimpl/WalkSharedResultRepositoryImpl.kt +++ b/app/src/main/java/com/paw/key/data/repositoryimpl/WalkSharedResultRepositoryImpl.kt @@ -2,13 +2,11 @@ package com.paw.key.data.repositoryimpl import android.graphics.Bitmap import android.util.Log -import com.kakao.vectormap.LatLng +import com.naver.maps.geometry.LatLng import com.paw.key.domain.model.entity.sharedresult.WalkResult import com.paw.key.domain.repository.WalkSharedResultRepository import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import javax.inject.Inject import javax.inject.Singleton diff --git a/app/src/main/java/com/paw/key/domain/model/entity/sharedresult/WalkResult.kt b/app/src/main/java/com/paw/key/domain/model/entity/sharedresult/WalkResult.kt index 01c010cb..9db7a0b3 100644 --- a/app/src/main/java/com/paw/key/domain/model/entity/sharedresult/WalkResult.kt +++ b/app/src/main/java/com/paw/key/domain/model/entity/sharedresult/WalkResult.kt @@ -1,7 +1,7 @@ package com.paw.key.domain.model.entity.sharedresult import android.graphics.Bitmap -import com.kakao.vectormap.LatLng +import com.naver.maps.geometry.LatLng data class WalkResult( val bitmap: Bitmap?, diff --git a/app/src/main/java/com/paw/key/domain/repository/WalkSharedResultRepository.kt b/app/src/main/java/com/paw/key/domain/repository/WalkSharedResultRepository.kt index f9e264e0..1915d788 100644 --- a/app/src/main/java/com/paw/key/domain/repository/WalkSharedResultRepository.kt +++ b/app/src/main/java/com/paw/key/domain/repository/WalkSharedResultRepository.kt @@ -1,7 +1,7 @@ package com.paw.key.domain.repository import android.graphics.Bitmap -import com.kakao.vectormap.LatLng +import com.naver.maps.geometry.LatLng import com.paw.key.domain.model.entity.sharedresult.WalkResult import kotlinx.coroutines.flow.Flow diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/List/TabListScreen.kt b/app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/List/TabListScreen.kt index 3e580bb8..85e41c28 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/List/TabListScreen.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/List/TabListScreen.kt @@ -12,7 +12,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon @@ -35,7 +34,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.paw.key.R import com.paw.key.core.designsystem.component.CourseCard import com.paw.key.core.designsystem.theme.PawKeyTheme -import com.paw.key.core.util.noRippleClickable +import com.paw.key.core.extension.noRippleClickable import com.paw.key.presentation.ui.course.entire.tab.map.List.viewmodel.TapListViewModel @Preview(showBackground = true) diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/List/viewmodel/TapListViewModel.kt b/app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/List/viewmodel/TapListViewModel.kt index 55cab478..36931d8d 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/List/viewmodel/TapListViewModel.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/List/viewmodel/TapListViewModel.kt @@ -110,12 +110,13 @@ class TapListViewModel @Inject constructor( } fun updateSortTime(option: String) { + Log.e("updateSortTime", option) when (option) { - "21분 이내" -> { + "20분 이내" -> { _state.update { it.copy( selectedSortTimeStart = 0, - selectedSortTimeEnd = 21 + selectedSortTimeEnd = 20 ) } } @@ -123,7 +124,7 @@ class TapListViewModel @Inject constructor( "21~40분" -> { _state.update { it.copy( - selectedSortTimeStart = 21, + selectedSortTimeStart = 20, selectedSortTimeEnd = 40 ) } @@ -132,7 +133,7 @@ class TapListViewModel @Inject constructor( "41~60분" -> { _state.update { it.copy( - selectedSortTimeStart = 41, + selectedSortTimeStart = 40, selectedSortTimeEnd = 60 ) } @@ -141,7 +142,7 @@ class TapListViewModel @Inject constructor( "1시간 이상" -> { _state.update { it.copy( - selectedSortTimeStart = 61, + selectedSortTimeStart = 60, selectedSortTimeEnd = null ) } @@ -321,6 +322,8 @@ class TapListViewModel @Inject constructor( selectedOptions = selectedOptions.ifEmpty { null } ) + Log.e("TapListViewModel", "요청 데이터: $request") + postsListRepository.postList( userId = userId.first(), request = request diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/TapMapScreen.kt b/app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/TapMapScreen.kt index 693536ec..57fbe1e8 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/TapMapScreen.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/TapMapScreen.kt @@ -1,7 +1,5 @@ package com.paw.key.presentation.ui.course.entire.tab.map -import android.os.Looper -import android.util.Log import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.layout.Box @@ -33,25 +31,18 @@ import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.compose.ui.viewinterop.AndroidView import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.google.android.gms.location.LocationCallback -import com.google.android.gms.location.LocationRequest import com.google.android.gms.location.LocationResult import com.google.android.gms.location.LocationServices -import com.google.android.gms.location.Priority -import com.kakao.vectormap.LatLng -import com.kakao.vectormap.MapView import com.paw.key.R import com.paw.key.core.designsystem.component.LoadingScreen import com.paw.key.core.designsystem.theme.PawKeyTheme import com.paw.key.core.util.UiState -import com.paw.key.core.util.noRippleClickable -import com.paw.key.presentation.ui.course.entire.tab.map.component.tapMapView +import com.paw.key.core.extension.noRippleClickable import com.paw.key.presentation.ui.course.entire.tab.map.viewmodel.TapMapViewModel -import com.paw.key.presentation.ui.course.walk.getCurrentLocation @Composable fun TapMapRoute( @@ -75,54 +66,9 @@ fun TapMapRoute( object : LocationCallback() { override fun onLocationResult(locationResult: LocationResult) { locationResult.lastLocation?.let { location -> - val newLocation = LatLng.from(location.latitude, location.longitude) - viewModel.updateState { - copy( - currentLocation = newLocation - ) - } - } - } - } - } - - LaunchedEffect(isGranted) { - if (isGranted) { - val currentLocation = getCurrentLocation( - context, - fusedLocationClient, - ) - - Log.e("TapMapRoute", "currentLocation: $currentLocation") - - viewModel.updateState { - copy( - initialLocationState = UiState.Success(currentLocation), - currentLocation = currentLocation - ) - } - - val locationRequest = LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 1000) // 1초마다, 높은 정확도 - .setWaitForAccurateLocation(true) - .build() - try { - fusedLocationClient.requestLocationUpdates( - locationRequest, - locationCallback, - Looper.getMainLooper() - ) - } catch (e: SecurityException) { - snackBarHostState.showSnackbar("위치 권한이 필요합니다.") - - viewModel.updateState { - copy( - isTrackingEnabled = false - ) } } - } else { - fusedLocationClient.removeLocationUpdates(locationCallback) } } @@ -136,23 +82,12 @@ fun TapMapRoute( } is UiState.Success -> { - val mapView = tapMapView( - lifeCycle = lifecycleOwner.lifecycle, - context = context, - currentUserLocation = state.currentLocation, - isTrackingEnabled = state.isTrackingEnabled, - onDisposeCallback = { - fusedLocationClient.removeLocationUpdates(locationCallback) - } - ) - TapMapScreen( paddingValues = paddingValues, navigateUp = navigateUp, navigateNext = navigateNext, snackBarHostState = snackBarHostState, regionName = state.currentRegion ?: "영등포구 여의도동", - mapView = mapView, onClickTracking = { viewModel.updateState { copy( @@ -177,7 +112,6 @@ fun TapMapScreen( snackBarHostState: SnackbarHostState, regionName: String, onClickTracking: () -> Unit, - mapView: MapView, modifier: Modifier = Modifier, ) { Scaffold( @@ -193,12 +127,6 @@ fun TapMapScreen( modifier = Modifier .padding(pv) ) { - AndroidView( - factory = { mapView }, - modifier = Modifier - .align(Alignment.Center) - ) - Text( text = regionName, modifier = Modifier diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/component/tapMapView.kt b/app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/component/tapMapView.kt deleted file mode 100644 index a4e2ac19..00000000 --- a/app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/component/tapMapView.kt +++ /dev/null @@ -1,147 +0,0 @@ -package com.paw.key.presentation.ui.course.entire.tab.map.component - -import android.content.Context -import android.util.Log -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberUpdatedState -import androidx.compose.runtime.setValue -import androidx.lifecycle.DefaultLifecycleObserver -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleOwner -import com.kakao.vectormap.KakaoMap -import com.kakao.vectormap.KakaoMapReadyCallback -import com.kakao.vectormap.LatLng -import com.kakao.vectormap.MapLifeCycleCallback -import com.kakao.vectormap.MapView -import com.kakao.vectormap.camera.CameraUpdateFactory -import com.kakao.vectormap.label.Label -import com.kakao.vectormap.label.LabelOptions -import com.kakao.vectormap.label.LabelStyle -import com.paw.key.R - -@Composable -fun tapMapView( - lifeCycle : Lifecycle, - context : Context, - currentUserLocation : LatLng?, - isTrackingEnabled : Boolean, - onDisposeCallback : () -> Unit, -) : MapView { - val mapView = remember { - MapView(context) - } - - var kakaoMapState by remember { - mutableStateOf(null) - } - - var centerLabel by remember { - mutableStateOf(null) - } - - val stableOnDisposeCallback = rememberUpdatedState(onDisposeCallback) - - DisposableEffect(lifeCycle) { - val observer = object : DefaultLifecycleObserver { - override fun onCreate(owner: LifecycleOwner) { - mapView.start( - object : MapLifeCycleCallback() { - override fun onMapDestroy() { - } - - override fun onMapError(error: Exception) { - Log.e("MapView", "지도 오류 발생: $error") - } - }, - object : KakaoMapReadyCallback() { - override fun onMapReady(kakaoMap: KakaoMap) { - kakaoMapState = kakaoMap - //trackingManager = kakaoMap.trackingManager - - centerLabel = kakaoMap.labelManager?.layer?.addLabel( - // userLocation이 null일 경우 - LabelOptions.from("dotLabel", currentUserLocation ?: LatLng.from(37.497942, 127.027619)) - .setStyles( - LabelStyle.from( - R.drawable.user_poi - ).setAnchorPoint(0.5f, 0.5f) - ) - .setRank(5) - ) - - /*kakaoMap.shapeManager?.layer?.addPolygon( - PolygonOptions.from("circlePolygon") - .setDotPoints(DotPoints.fromCircle(currentUserLocation, 1.0f)) - .setStylesSet( - PolygonStylesSet.from( - PolygonStyles.from(context.getColor(R.color.purple_700)) - ) - ) - )*/ - - val initialCameraPosition = currentUserLocation ?: LatLng.from(37.497942, 127.027619) - - kakaoMap.moveCamera( - CameraUpdateFactory.newCenterPosition( - initialCameraPosition, 21 - ) - ) - } - - override fun getPosition(): LatLng { - //userLocation = LatLng.from(locationY, locationX) - return currentUserLocation ?: LatLng.from(37.497942, 127.027619) - } - } - ) - } - - override fun onResume(owner: LifecycleOwner) { - mapView.resume() - } - - override fun onPause(owner: LifecycleOwner) { - mapView.pause() - //fusedLocationClient.removeLocationUpdates(locationCallback) - /*sensorManager.unregisterListener(stepSensorEventListener) - fusedLocationClient.removeLocationUpdates(locationCallback) - initialSensorSteps = null - isWalking(false)*/ - } - } - - lifeCycle.addObserver(observer) - - onDispose { - lifeCycle.removeObserver(observer) - //fusedLocationClient.removeLocationUpdates(locationCallback) - //onDisposeCallback() - stableOnDisposeCallback.value() - /*sensorManager.unregisterListener(stepSensorEventListener) - isWalking(false)*/ - } - } - - LaunchedEffect(currentUserLocation, centerLabel, kakaoMapState) { - if (currentUserLocation != null && centerLabel != null && kakaoMapState != null) { - centerLabel?.moveTo(currentUserLocation) - Log.d("TapMapView", "카메라 $currentUserLocation (Tracking Enabled)") - } - } - - LaunchedEffect(isTrackingEnabled) { - kakaoMapState?.moveCamera( - CameraUpdateFactory.newCenterPosition( - currentUserLocation, 21 // 줌 레벨을 유지하거나 필요에 따라 변경 - ) - ) - } - - - return mapView -} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/state/TapMapContract.kt b/app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/state/TapMapContract.kt index fea2cee1..1d7a4c0c 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/state/TapMapContract.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/state/TapMapContract.kt @@ -1,7 +1,7 @@ package com.paw.key.presentation.ui.course.entire.tab.map.state import androidx.compose.runtime.Immutable -import com.kakao.vectormap.LatLng +import com.naver.maps.geometry.LatLng import com.paw.key.core.util.UiState @Immutable diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/viewmodel/TapMapViewModel.kt b/app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/viewmodel/TapMapViewModel.kt index 30d44609..19175849 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/viewmodel/TapMapViewModel.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/viewmodel/TapMapViewModel.kt @@ -3,7 +3,7 @@ package com.paw.key.presentation.ui.course.entire.tab.map.viewmodel import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.kakao.vectormap.LatLng +import com.naver.maps.geometry.LatLng import com.paw.key.core.util.PreferenceDataStore import com.paw.key.core.util.UiState import com.paw.key.presentation.ui.course.entire.tab.map.state.TapMapSideEffect @@ -37,7 +37,10 @@ class TapMapViewModel @Inject constructor( Log.e("TapMapViewModel", "savedRegion: $savedRegion") _state.update { it.copy( - currentRegion = savedRegion.first() + currentRegion = savedRegion.first(), + initialLocationState = UiState.Success( + LatLng(37.4979000000, 127.0276000000) + ) ) } } diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/sharedwalk/sharedroute/SharedWalkCourseScreen.kt b/app/src/main/java/com/paw/key/presentation/ui/course/sharedwalk/sharedroute/SharedWalkCourseScreen.kt index c01efc2e..5bef3da8 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/course/sharedwalk/sharedroute/SharedWalkCourseScreen.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/course/sharedwalk/sharedroute/SharedWalkCourseScreen.kt @@ -31,7 +31,6 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.Scaffold -import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -54,7 +53,6 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.compose.ui.viewinterop.AndroidView import androidx.core.content.ContextCompat import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.LocalLifecycleOwner @@ -66,16 +64,14 @@ import com.google.android.gms.location.LocationRequest import com.google.android.gms.location.LocationResult import com.google.android.gms.location.LocationServices import com.google.android.gms.location.Priority -import com.kakao.vectormap.LatLng -import com.kakao.vectormap.MapView import com.kakao.vectormap.graphics.gl.GLSurfaceView +import com.naver.maps.geometry.LatLng import com.paw.key.R import com.paw.key.core.designsystem.component.LoadingScreen import com.paw.key.core.designsystem.component.PawkeyButton import com.paw.key.core.designsystem.theme.PawKeyTheme import com.paw.key.core.util.UiState -import com.paw.key.core.util.noRippleClickable -import com.paw.key.presentation.ui.course.sharedwalk.sharedroute.component.sharedWalkCourseMapView +import com.paw.key.core.extension.noRippleClickable import com.paw.key.presentation.ui.course.sharedwalk.sharedroute.state.SharedWalkCourseSideEffect import com.paw.key.presentation.ui.course.sharedwalk.sharedroute.viewmodel.SharedWalkCourseViewModel import com.paw.key.presentation.ui.course.walk.component.WalkRecordItem @@ -85,21 +81,13 @@ import com.paw.key.presentation.ui.course.walk.formatTime import com.paw.key.presentation.ui.course.walk.state.WalkCourseContract.WalkCourseRecord.DistanceRecord import com.paw.key.presentation.ui.course.walk.state.WalkCourseContract.WalkCourseRecord.StepsRecord import com.paw.key.presentation.ui.course.walk.state.WalkCourseContract.WalkCourseRecord.TimeRecord -import com.paw.key.presentation.ui.course.walk.state.WalkCourseContract.WalkCourseSideEffect -import com.paw.key.presentation.ui.course.walk.viewmodel.WalkCourseViewModel import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay -import kotlinx.coroutines.launch import kotlinx.coroutines.suspendCancellableCoroutine -import kotlinx.coroutines.withContext import java.nio.IntBuffer -import java.util.Locale -import java.util.concurrent.TimeUnit import javax.microedition.khronos.egl.EGL10 import javax.microedition.khronos.egl.EGLContext import javax.microedition.khronos.opengles.GL10 -import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException @@ -180,26 +168,6 @@ fun SharedWalkCourseRoute( } } - val locationRequest = remember { - LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 1000) // 1초마다, 높은 정확도 - .setWaitForAccurateLocation(true) // false = 가장 빠른 위치 / true = 가장 정확한 위치 - .build() - } - - val locationCallback = remember(viewModel) { - object : LocationCallback() { - override fun onLocationResult(locationResult: LocationResult) { - locationResult.lastLocation?.let { location -> - if (state.isRecording) { - val newLatLng = LatLng.from(location.latitude, location.longitude) - viewModel.updateLocationAndCalculateDistance(newLatLng, location.accuracy) - Log.d("WalkCourseRoute", "Updated location: $newLatLng, accuracy: ${location.accuracy}") - } - } - } - } - } - LaunchedEffect(Unit) { val currentLocation = sharedGetCurrentLocation( context, @@ -231,27 +199,6 @@ fun SharedWalkCourseRoute( } } - LaunchedEffect(state.isRecording) { - if (state.isRecording) { - try { - fusedLocationClient.requestLocationUpdates( - locationRequest, - locationCallback, - Looper.getMainLooper() - ) - Log.d("WalkCourseRoute", "Location updates requested.") - } catch (e: SecurityException) { - Log.e("WalkCourseRoute", "위치 권한 없음: ${e.message}") - snackBarHostState.showSnackbar("위치 권한이 필요합니다.") - viewModel.updateState { - copy(isLocationTracking = false) - } - } - } else { - fusedLocationClient.removeLocationUpdates(locationCallback) - Log.d("WalkCourseRoute", "Location updates removed.") - } - } /*LaunchedEffect(distanceInTens) { // 거리가 10m씩 변경되었을 경우 @@ -292,7 +239,6 @@ fun SharedWalkCourseRoute( onDispose { if (stepCounterSensor != null) { sensorManager.unregisterListener(stepSensorEventListener) - fusedLocationClient.removeLocationUpdates(locationCallback) } } } @@ -306,45 +252,6 @@ fun SharedWalkCourseRoute( } is UiState.Success -> { - val mapView = sharedWalkCourseMapView ( - lifeCycle = lifecycleOwner.lifecycle, - context = context, - onLabelClick = { _, _ -> }, - currentUserLocation = state.currentLocation, - poiPoints = state.poiPoints, // 공유용 뷰 - isTrackingEnabled = state.isTrackingEnabled, - isPauseTracking = state.isRecording, // true = 잠시 중단, false = 시작 - isStopTracking = state.isLocationTracking, // true = 진짜 중단 - onDisposeCallback = { - fusedLocationClient.removeLocationUpdates(locationCallback) - viewModel.mapCaptureCompleted() - } - ) - - // 캡처 부분 - LaunchedEffect(state.shouldCaptureMap, state.poiPoints) { - delay(500) - - if (state.shouldCaptureMap) { - val glSurfaceView = mapView.surfaceView as? GLSurfaceView - if (glSurfaceView != null) { - withContext(Dispatchers.IO) { - sharedCaptureMapToBitmap(glSurfaceView) { capturedBitmap -> - capturedBitmap?.let { - viewModel.onMapCaptured(it) - Log.d("WalkCourseRoute", "맵 캡처 성공! (triggered by shouldCaptureMap)") - } ?: run { - Log.e("WalkCourseRoute", "맵 캡처 실패: 비트맵이 null입니다.") - viewModel.mapCaptureCompleted() - } - } - } - } else { - viewModel.mapCaptureCompleted() - } - } - } - SharedWalkCourseScreen( paddingValues = paddingValues, navigateUp = navigateUp, @@ -353,7 +260,6 @@ fun SharedWalkCourseRoute( }, scope = scope, snackBarHostState = snackBarHostState, - mapView = mapView, totalDistance = formatDistance, isSharedWalk = isSharedWalk, currentSteps = state.steps, @@ -393,6 +299,7 @@ fun SharedWalkCourseRoute( viewModel.onMapCaptured(bitmap) }, modifier = modifier, + ) } } @@ -415,7 +322,6 @@ fun SharedWalkCourseScreen( onPauseTracking: () -> Unit, // 잠시 중단 onStopTracking: () -> Unit, // 종료하기 onCaptured: (Bitmap?) -> Unit, - mapView: MapView, modifier: Modifier = Modifier, ) { Scaffold( @@ -429,11 +335,6 @@ fun SharedWalkCourseScreen( modifier = Modifier .padding(pv) ) { - AndroidView( - factory = { mapView }, - modifier = Modifier - .align(Alignment.Center) - ) Column ( modifier = modifier @@ -533,23 +434,7 @@ fun SharedWalkCourseScreen( text = "중지하기", enabled = true, onClick = { - onPauseTracking() - - scope.launch { - val glSurfaceView = mapView.surfaceView as? GLSurfaceView - if (glSurfaceView != null) { - withContext(Dispatchers.IO) { - sharedCaptureMapToBitmap(glSurfaceView) { capturedBitmap -> - capturedBitmap?.let { - onCaptured(it) - Log.d("WalkCourseScreen", "맵 캡처 성공!") - } ?: run { - Log.e("WalkCourseScreen", "맵 캡처 실패: 비트맵이 null입니다.") - } - } - } - } - } + }, modifier = Modifier .padding(top = 16.dp) @@ -695,7 +580,7 @@ suspend fun sharedGetCurrentLocation( override fun onLocationResult(locationResult: LocationResult) { val location = locationResult.lastLocation if (location != null) { - continuation.resume(LatLng.from(location.latitude, location.longitude)) + //continuation.resume(LatLng.from(location.latitude, location.longitude)) fusedLocationClient.removeLocationUpdates(this) } else { continuation.resumeWithException(IllegalStateException("위치 정보를 가져올 수 없습니다")) diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/sharedwalk/sharedroute/component/sharedCourseMapView.kt b/app/src/main/java/com/paw/key/presentation/ui/course/sharedwalk/sharedroute/component/sharedCourseMapView.kt deleted file mode 100644 index bd634640..00000000 --- a/app/src/main/java/com/paw/key/presentation/ui/course/sharedwalk/sharedroute/component/sharedCourseMapView.kt +++ /dev/null @@ -1,278 +0,0 @@ -package com.paw.key.presentation.ui.course.sharedwalk.sharedroute.component - -import android.content.Context -import android.util.Log -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberUpdatedState -import androidx.compose.runtime.setValue -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.toArgb -import androidx.core.content.ContextCompat -import androidx.lifecycle.DefaultLifecycleObserver -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleOwner -import com.kakao.vectormap.KakaoMap -import com.kakao.vectormap.KakaoMapReadyCallback -import com.kakao.vectormap.LatLng -import com.kakao.vectormap.MapLifeCycleCallback -import com.kakao.vectormap.MapView -import com.kakao.vectormap.camera.CameraUpdateFactory -import com.kakao.vectormap.label.Label -import com.kakao.vectormap.label.LabelOptions -import com.kakao.vectormap.label.LabelStyle -import com.kakao.vectormap.label.TrackingManager -import com.kakao.vectormap.route.RouteLine -import com.kakao.vectormap.route.RouteLineOptions -import com.kakao.vectormap.route.RouteLineSegment -import com.kakao.vectormap.route.RouteLineStyle -import com.kakao.vectormap.route.RouteLineStylesSet -import com.kakao.vectormap.shape.DimScreenLayer -import com.paw.key.R - -@Composable -fun sharedWalkCourseMapView( - lifeCycle : Lifecycle, - context : Context, - currentUserLocation : LatLng?, - isTrackingEnabled : Boolean, - isPauseTracking : Boolean, - isStopTracking : Boolean, - poiPoints : List, - onLabelClick : (LatLng, String) -> Unit, - onDisposeCallback : () -> Unit -) : MapView { - val mapView = remember { - MapView(context) - } - - var kakaoMapState by remember { - mutableStateOf(null) - } - - val stableCallback = rememberUpdatedState(onLabelClick) - val stableOnDisposeCallback = rememberUpdatedState(onDisposeCallback) - - var centerLabel by remember { - mutableStateOf(null) - } - - // ------------------------------------------------ - // 트래킹 - - var trackingManager by remember { - mutableStateOf(null) - } - - var dimScreenLayer by remember { - mutableStateOf(null) - } - - var currentDrawnRouteLine by remember { - mutableStateOf(null) - } - - /*val yeoksamCoordinates = listOf( - LatLng.from(37.50097, 127.03734), // 역삼동 중심 :contentReference[oaicite:1]{index=1} - LatLng.from(37.50079, 127.03689), // 역삼역 (L2) 정문 인근 :contentReference[oaicite:2]{index=2} - LatLng.from(37.50001, 127.03549), // 역삼역 지하철역 (GPS 웹 기준) :contentReference[oaicite:3]{index=3} - LatLng.from(37.49950, 127.03322), // 역삼1동 중심 지역 :contentReference[oaicite:4]{index=4} - LatLng.from(37.49900, 127.03856), // 역삼동 중심 북동쪽 :contentReference[oaicite:5]{index=5} - LatLng.from(37.49999, 127.03719), // 테헤란로 중심가 (중간 위치) ← 위도/경도 참고 위 :contentReference[oaicite:6]{index=6} - LatLng.from(37.49850, 127.03800), // 강남대로 인근 - LatLng.from(37.49800, 127.03450), // 논현로 인근 - LatLng.from(37.50150, 127.03700), // 삼성역 방면 경계 지역 - LatLng.from(37.50050, 127.03900), // 국기원/코엑스 방향 경계 - )*/ - - /*val fusedLocationClient = remember { LocationServices.getFusedLocationProviderClient(context) } - - val locationRequest = remember { - LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 1000) // 1초마다, 높은 정확도 - .setWaitForAccurateLocation(true) - .build() - } - - val locationCallback = remember(isPauseTracking) { - object : LocationCallback() { - override fun onLocationResult(locationResult: LocationResult) { - locationResult.lastLocation?.let { location -> - if (isPauseTracking) { // isRecording 상태를 직접 사용 - val newLatLng = LatLng.from(location.latitude, location.longitude) - currentLocation = newLatLng - Log.d("CourseMapView", "Updated location: $newLatLng, accuracy: ${location.accuracy}") - } - } - } - } - }*/ - val drawRouteOnMap: (KakaoMap, List) -> Unit = { kakaoMap, pointsToDraw -> - if (pointsToDraw.isNotEmpty()) { - currentDrawnRouteLine?.remove() - currentDrawnRouteLine = null - - val routeLineStyle = RouteLineStyle.from( - 12f, - ContextCompat.getColor(context, R.color.green_500) - ) - - val routeStylesSet = RouteLineStylesSet.from(routeLineStyle) - - val routeSegments = listOf( - RouteLineSegment.from(pointsToDraw).setStyles(routeLineStyle) - ) - - val routeLineOptions = RouteLineOptions.from(routeSegments) - .setStylesSet(routeStylesSet) - - currentDrawnRouteLine = kakaoMap.routeLineManager?.layer?.addRouteLine(routeLineOptions) - currentDrawnRouteLine?.show() - - kakaoMapState?.moveCamera( - CameraUpdateFactory.fitMapPoints( - poiPoints.toTypedArray(), 150, 15 - ) - ) - } - } - - LaunchedEffect(poiPoints) { - kakaoMapState?.moveCamera( - CameraUpdateFactory.fitMapPoints( - poiPoints.toTypedArray(), 150, 15 - ) - ) - } - - /*LaunchedEffect(poiPoints) { - kakaoMapState?.let { map -> - drawRouteOnMap(map, poiPoints) - } - Log.d("CourseMapView", "POI Points: $poiPoints") - - kakaoMapState?.moveCamera( - CameraUpdateFactory.fitMapPoints( - poiPoints.toTypedArray(), 150, 15 - ) - ) - }*/ - - DisposableEffect(lifeCycle) { - val observer = object : DefaultLifecycleObserver { - override fun onCreate(owner: LifecycleOwner) { - mapView.start( - object : MapLifeCycleCallback() { - override fun onMapDestroy() { - // 지도 종료 처리 - currentDrawnRouteLine = null - } - - override fun onMapError(error: Exception) { - Log.e("MapView", "지도 오류 발생: $error") - } - }, - object : KakaoMapReadyCallback() { - override fun onMapReady(kakaoMap: KakaoMap) { - kakaoMapState = kakaoMap - trackingManager = kakaoMap.trackingManager - dimScreenLayer = kakaoMap.dimScreenManager?.dimScreenLayer - - centerLabel = kakaoMap.labelManager?.layer?.addLabel( - // userLocation이 null일 경우 - LabelOptions.from("dotLabel", currentUserLocation ?: LatLng.from(37.497942, 127.027619)) - .setStyles( - LabelStyle.from( - R.drawable.user_poi - ).setAnchorPoint(0.5f, 0.5f) - ) - .setRank(5) - ) - - val initialCameraPosition = currentUserLocation ?: LatLng.from(37.497942, 127.027619) - - kakaoMap.moveCamera( - CameraUpdateFactory.newCenterPosition( - initialCameraPosition, 19 - ) - ) - - drawRouteOnMap(kakaoMap, poiPoints) - } - - override fun getPosition(): LatLng { - //userLocation = LatLng.from(locationY, locationX) - return currentUserLocation ?: LatLng.from(37.497942, 127.027619) - } - } - ) - } - - override fun onResume(owner: LifecycleOwner) { - mapView.resume() - - kakaoMapState?.moveCamera( - CameraUpdateFactory.newCenterPosition( - currentUserLocation, 19 - ) - ) - } - - override fun onPause(owner: LifecycleOwner) { - mapView.pause() - kakaoMapState?.moveCamera( - CameraUpdateFactory.fitMapPoints( - poiPoints.toTypedArray(), 150, 15 - ) - ) - } - } - - lifeCycle.addObserver(observer) - - onDispose { - lifeCycle.removeObserver(observer) - //fusedLocationClient.removeLocationUpdates(locationCallback) - onDisposeCallback() - stableOnDisposeCallback.value() - } - } - - LaunchedEffect(currentUserLocation, isTrackingEnabled, centerLabel, kakaoMapState) { - if (currentUserLocation != null && centerLabel != null && kakaoMapState != null) { - centerLabel?.moveTo(currentUserLocation) - } - } - - LaunchedEffect(isTrackingEnabled) { - kakaoMapState?.moveCamera( - CameraUpdateFactory.newCenterPosition( - currentUserLocation, 19 - ) - ) - } - - /*LaunchedEffect(centerLabel, trackingManager) { - if (centerLabel != null && trackingManager != null) { - trackingManager?.startTracking(centerLabel) - } - }*/ - - LaunchedEffect(isPauseTracking) { - if (!isPauseTracking) { - //trackingManager?.stopTracking() - mapView.isClickable = false - dimScreenLayer?.setColor(Color.Black.copy(alpha = 0.5f).toArgb()) - dimScreenLayer?.setVisible(true) - } else { - //trackingManager?.stopTracking() - mapView.isClickable = true - dimScreenLayer?.setVisible(false) - } - } - - return mapView -} diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/sharedwalk/sharedroute/state/SharedWalkCourseContract.kt b/app/src/main/java/com/paw/key/presentation/ui/course/sharedwalk/sharedroute/state/SharedWalkCourseContract.kt index 54e1dfff..901c7301 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/course/sharedwalk/sharedroute/state/SharedWalkCourseContract.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/course/sharedwalk/sharedroute/state/SharedWalkCourseContract.kt @@ -3,7 +3,7 @@ package com.paw.key.presentation.ui.course.sharedwalk.sharedroute.state import android.graphics.Bitmap import androidx.annotation.StringRes import androidx.compose.runtime.Immutable -import com.kakao.vectormap.LatLng +import com.naver.maps.geometry.LatLng import com.paw.key.R import com.paw.key.core.util.UiState import kotlinx.collections.immutable.PersistentList diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/sharedwalk/sharedroute/viewmodel/SharedWalkViewModel.kt b/app/src/main/java/com/paw/key/presentation/ui/course/sharedwalk/sharedroute/viewmodel/SharedWalkViewModel.kt index 15236366..a654e466 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/course/sharedwalk/sharedroute/viewmodel/SharedWalkViewModel.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/course/sharedwalk/sharedroute/viewmodel/SharedWalkViewModel.kt @@ -6,7 +6,7 @@ import android.location.Location import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.kakao.vectormap.LatLng +import com.naver.maps.geometry.LatLng import com.paw.key.core.util.PreferenceDataStore import com.paw.key.domain.repository.WalkSharedResultRepository import com.paw.key.domain.repository.sharedwalk.SharedWalkRepository @@ -51,13 +51,7 @@ class SharedWalkCourseViewModel @Inject constructor( userId = userId.first(), routeId = routeId ).onSuccess { - _state.update { state -> - state.copy( - poiPoints = it.geometry.coordinates.map { coord -> - LatLng.from(coord[1], coord[0]) - }.toPersistentList() - ) - } + // Todo : 공유용 수정 예정 Log.d("SharedWalkCourseViewModel", "success getWalkSharedTrack: ${state.value.poiPoints}") }.onFailure { Log.e("SharedWalkCourseViewModel", "failure getWalkSharedTrack: $it") diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/util/PermissionRequestEffect.kt b/app/src/main/java/com/paw/key/presentation/ui/course/util/PermissionRequestEffect.kt new file mode 100644 index 00000000..3e3c683e --- /dev/null +++ b/app/src/main/java/com/paw/key/presentation/ui/course/util/PermissionRequestEffect.kt @@ -0,0 +1,24 @@ +package com.paw.key.presentation.ui.course.util + +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect + +@Composable +fun PermissionRequestEffect( + permissions: Array, + onResult: (isGranted: Boolean) -> Unit +) { + val permissionLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.RequestMultiplePermissions(), + onResult = { permissionsMap -> + val allPermissionsGranted = permissionsMap.values.all { it } + onResult(allPermissionsGranted) + } + ) + + LaunchedEffect(Unit) { + permissionLauncher.launch(permissions) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/util/rememberFusedLocationSource.kt b/app/src/main/java/com/paw/key/presentation/ui/course/util/rememberFusedLocationSource.kt new file mode 100644 index 00000000..bfb392f9 --- /dev/null +++ b/app/src/main/java/com/paw/key/presentation/ui/course/util/rememberFusedLocationSource.kt @@ -0,0 +1,232 @@ +package com.paw.key.presentation.ui.course.util + +import android.Manifest +import android.content.Context +import android.content.pm.PackageManager +import android.location.Location +import android.os.Looper +import android.util.Log +import androidx.annotation.UiThread +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext +import androidx.core.app.ActivityCompat +import com.google.android.gms.location.FusedLocationProviderClient +import com.google.android.gms.location.LocationCallback +import com.google.android.gms.location.LocationRequest +import com.google.android.gms.location.LocationResult +import com.google.android.gms.location.LocationServices +import com.google.android.gms.location.Priority +import com.naver.maps.geometry.LatLng +import com.naver.maps.map.CameraUpdate +import com.naver.maps.map.LocationSource +import com.naver.maps.map.compose.CameraPositionState +import com.naver.maps.map.compose.ExperimentalNaverMapApi +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +// 실시간 위치가 변경되었음을 알려주는 인터페이스 +interface RealTimeLocationListener { + fun onLocationChanged(location: Location) +} + +// 3m 씩 이동한다고 가정하고 실시간 테스트를 위한 좌표 +private val testPoints = listOf( + LatLng(37.4979000000, 127.0276000000), // 시작점 + + // 북쪽으로 직진 (3m × 3번) + LatLng(37.4979269500, 127.0276000000), + LatLng(37.4979539000, 127.0276000000), + LatLng(37.4979808500, 127.0276000000), + + // 동쪽으로 꺾음 (3m × 2번) + LatLng(37.4979808500, 127.0276340000), + LatLng(37.4979808500, 127.0276680000), + + // 남쪽으로 직진 (3m × 3번) + LatLng(37.4979539000, 127.0276680000), + LatLng(37.4979269500, 127.0276680000), + LatLng(37.4979000000, 127.0276680000), + + // 서쪽으로 꺾어 시작점으로 복귀 + LatLng(37.4979000000, 127.0276340000), + LatLng(37.4979000000, 127.0276000000) // 시작점 +) + +@OptIn(ExperimentalNaverMapApi::class) +@Composable +fun rememberCustomFusedLocationSource( + // 테스트 모드를 제어하기 위한 파라미터 + useTestPoints: Boolean = false, + cameraPositionState: CameraPositionState, + hasLocationPermission: Boolean +): FusedLocationSource { + val context = LocalContext.current + val fusedLocationClient = remember { + LocationServices.getFusedLocationProviderClient(context) + } + + val locationSource = remember(useTestPoints) { + FusedLocationSource(context, fusedLocationClient, useTestPoints) + } + + LaunchedEffect(hasLocationPermission) { + if (hasLocationPermission) { + Log.d("rememberCustomFusedLocationSource", "hasLocationPermission: $hasLocationPermission") + if (ActivityCompat.checkSelfPermission( + context, + Manifest.permission.ACCESS_FINE_LOCATION + ) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission( + context, + Manifest.permission.ACCESS_COARSE_LOCATION + ) != PackageManager.PERMISSION_GRANTED + ) { + return@LaunchedEffect + } + + fusedLocationClient.lastLocation.addOnSuccessListener { location -> + location?.let { + Log.d("rememberCustomFusedLocationSource", "lastLocation: $it") + val latLng = LatLng(it.latitude, it.longitude) + cameraPositionState.move(CameraUpdate.scrollTo(latLng)) + } + } + } + } + + DisposableEffect(Unit) { + onDispose { + locationSource.deactivate() + } + } + return locationSource +} + +class FusedLocationSource( + private val context: Context, + private val fusedLocationClient: FusedLocationProviderClient, + private val useTestPoints: Boolean = false +) : LocationSource { + private var listener: LocationSource.OnLocationChangedListener? = null + private var realTimeListener: RealTimeLocationListener? = null + private var isListening = false + + // 테스트를 위한 코루틴 + private val coroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob()) + private var simulationJob: Job? = null + + private val locationCallback = object : LocationCallback() { + override fun onLocationResult(locationResult: LocationResult) { + val lastLocation: Location? = locationResult.lastLocation + if (lastLocation != null) { + Log.d("FusedLocationSource", "onLocationResult: $lastLocation") + // 지도에 현 위치 업데이트 + listener?.onLocationChanged(lastLocation) + // 이건 실시간으로 위치 전달 업데이트 + realTimeListener?.onLocationChanged(lastLocation) + } + } + } + + fun setRealTimeLocationListener(listener: RealTimeLocationListener) { + this.realTimeListener = listener + } + + /*@UiThread + override fun activate(listener: LocationSource.OnLocationChangedListener) { + this.listener = listener + if (!isListening) { + // 권한 확인 + if (ActivityCompat.checkSelfPermission( + context, + Manifest.permission.ACCESS_FINE_LOCATION + ) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission( + context, + Manifest.permission.ACCESS_COARSE_LOCATION + ) != PackageManager.PERMISSION_GRANTED + ) { + return + } + fusedLocationClient.requestLocationUpdates( + locationRequest, + locationCallback, + Looper.getMainLooper() + ) + isListening = true + } + }*/ + @UiThread + override fun activate(listener: LocationSource.OnLocationChangedListener) { + Log.d("FusedLocationSource", "activate() called!") + this.listener = listener + if (!isListening) { + isListening = true + if (useTestPoints) { + startSimulation() + } else { + startRealLocationUpdates() + } + } + } + + private fun startSimulation() { + simulationJob = coroutineScope.launch { + for (point in testPoints) { + val mockLocation = Location("TestProvider").apply { + latitude = point.latitude + longitude = point.longitude + } + + listener?.onLocationChanged(mockLocation) + realTimeListener?.onLocationChanged(mockLocation) + + delay(3000L) + } + } + } + + private fun startRealLocationUpdates() { + // 권한 확인 + if (ActivityCompat.checkSelfPermission( + context, + Manifest.permission.ACCESS_FINE_LOCATION + ) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission( + context, + Manifest.permission.ACCESS_COARSE_LOCATION + ) != PackageManager.PERMISSION_GRANTED + ) { + return + } + fusedLocationClient.requestLocationUpdates( + locationRequest, + locationCallback, + Looper.getMainLooper() + ) + } + + override fun deactivate() { + if (isListening) { + if (useTestPoints) { + simulationJob?.cancel() + } else { + fusedLocationClient.removeLocationUpdates(locationCallback) + } + isListening = false + listener = null + realTimeListener = null + } + } + + companion object { + private val locationRequest = + LocationRequest.Builder(1000) + .setPriority(Priority.PRIORITY_HIGH_ACCURACY) + .build() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/util/rememberStepCounter.kt b/app/src/main/java/com/paw/key/presentation/ui/course/util/rememberStepCounter.kt new file mode 100644 index 00000000..e0d5789d --- /dev/null +++ b/app/src/main/java/com/paw/key/presentation/ui/course/util/rememberStepCounter.kt @@ -0,0 +1,76 @@ +package com.paw.key.presentation.ui.course.util// StepCounter.kt + +import android.content.Context +import android.hardware.Sensor +import android.hardware.SensorEvent +import android.hardware.SensorEventListener +import android.hardware.SensorManager +import android.util.Log +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext + +// 걸음 수 변경을 외부에 알리기 위한 인터페이스 +interface StepCountListener { + fun onStepCountChanged(sessionSteps: Long) + fun onSensorNotFound() +} + +@Composable +fun rememberStepCounter(): StepCounter { + val context = LocalContext.current + + val stepCounter = remember { + StepCounter(context) + } + + DisposableEffect(Unit) { + onDispose { + stepCounter.deactivate() + } + } + + return stepCounter +} + +class StepCounter(context: Context) : SensorEventListener { + private val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager + private val stepCounterSensor: Sensor? = sensorManager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER) + private var listener: StepCountListener? = null + private var isListening = false + + fun setStepCountListener(listener: StepCountListener) { + this.listener = listener + } + + fun activate() { + if (stepCounterSensor == null) { + Log.e("StepCounter", "걸음 수 측정 센서를 찾을 수 없습니다.") + listener?.onSensorNotFound() + return + } + if (!isListening) { + sensorManager.registerListener(this, stepCounterSensor, SensorManager.SENSOR_DELAY_NORMAL) + isListening = true + Log.d("StepCounter", "걸음 수 측정 리스너 등록") + } + } + + fun deactivate() { + if (isListening) { + sensorManager.unregisterListener(this) + isListening = false + Log.d("StepCounter", "걸음 수 측정 리스너 해제") + } + } + + override fun onSensorChanged(event: SensorEvent?) { + if (event?.sensor?.type == Sensor.TYPE_STEP_COUNTER) { + val totalStepsFromSensor = event.values[0].toLong() + listener?.onStepCountChanged(totalStepsFromSensor) + } + } + + override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) { } +} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walk/WalkCourseScreen.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walk/WalkCourseScreen.kt index 97e6a7e7..ef198e8a 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/course/walk/WalkCourseScreen.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walk/WalkCourseScreen.kt @@ -2,18 +2,15 @@ package com.paw.key.presentation.ui.course.walk import android.Manifest import android.content.Context -import android.content.pm.PackageManager import android.graphics.Bitmap -import android.hardware.Sensor -import android.hardware.SensorEvent -import android.hardware.SensorEventListener -import android.hardware.SensorManager import android.opengl.GLException import android.os.Build import android.os.Handler import android.os.Looper import android.util.Log +import android.view.Gravity import android.view.PixelCopy +import android.widget.Toast import androidx.annotation.RequiresApi import androidx.compose.foundation.background import androidx.compose.foundation.border @@ -32,18 +29,17 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.Scaffold -import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -55,54 +51,55 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.compose.ui.viewinterop.AndroidView -import androidx.core.content.ContextCompat import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.flowWithLifecycle -import com.google.android.gms.location.FusedLocationProviderClient -import com.google.android.gms.location.LocationCallback -import com.google.android.gms.location.LocationRequest -import com.google.android.gms.location.LocationResult -import com.google.android.gms.location.LocationServices -import com.google.android.gms.location.Priority -import com.kakao.vectormap.LatLng -import com.kakao.vectormap.MapView import com.kakao.vectormap.graphics.gl.GLSurfaceView +import com.naver.maps.geometry.LatLng +import com.naver.maps.geometry.LatLngBounds +import com.naver.maps.map.CameraUpdate +import com.naver.maps.map.compose.CameraPositionState +import com.naver.maps.map.compose.CameraUpdateReason +import com.naver.maps.map.compose.ExperimentalNaverMapApi +import com.naver.maps.map.compose.LocationOverlay +import com.naver.maps.map.compose.LocationTrackingMode +import com.naver.maps.map.compose.MapProperties +import com.naver.maps.map.compose.MapUiSettings +import com.naver.maps.map.compose.NaverMap +import com.naver.maps.map.compose.PathOverlay +import com.naver.maps.map.compose.rememberCameraPositionState +import com.naver.maps.map.overlay.OverlayImage import com.paw.key.R import com.paw.key.core.designsystem.component.LoadingScreen import com.paw.key.core.designsystem.component.PawkeyButton import com.paw.key.core.designsystem.theme.PawKeyTheme import com.paw.key.core.util.PreferenceDataStore import com.paw.key.core.util.UiState -import com.paw.key.core.util.noRippleClickable +import com.paw.key.core.extension.noRippleClickable +import com.paw.key.presentation.ui.course.util.FusedLocationSource +import com.paw.key.presentation.ui.course.util.PermissionRequestEffect +import com.paw.key.presentation.ui.course.util.StepCountListener +import com.paw.key.presentation.ui.course.util.rememberCustomFusedLocationSource +import com.paw.key.presentation.ui.course.util.rememberStepCounter import com.paw.key.presentation.ui.course.walk.component.WalkRecordItem import com.paw.key.presentation.ui.course.walk.component.WalkRecordRow -import com.paw.key.presentation.ui.course.walk.component.courseMapView import com.paw.key.presentation.ui.course.walk.state.WalkCourseContract.WalkCourseRecord.DistanceRecord import com.paw.key.presentation.ui.course.walk.state.WalkCourseContract.WalkCourseRecord.StepsRecord import com.paw.key.presentation.ui.course.walk.state.WalkCourseContract.WalkCourseRecord.TimeRecord import com.paw.key.presentation.ui.course.walk.state.WalkCourseContract.WalkCourseSideEffect import com.paw.key.presentation.ui.course.walk.viewmodel.WalkCourseViewModel -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.launch -import kotlinx.coroutines.suspendCancellableCoroutine -import kotlinx.coroutines.withContext +import kotlinx.collections.immutable.ImmutableList +import kotlinx.coroutines.flow.drop import java.nio.IntBuffer -import java.time.LocalDateTime import java.util.Locale import java.util.concurrent.TimeUnit import javax.microedition.khronos.egl.EGL10 import javax.microedition.khronos.egl.EGLContext import javax.microedition.khronos.opengles.GL10 -import kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException +@OptIn(ExperimentalNaverMapApi::class) @RequiresApi(Build.VERSION_CODES.Q) @Composable fun WalkCourseRoute( @@ -114,28 +111,86 @@ fun WalkCourseRoute( isSharedWalk : Boolean = false, viewModel: WalkCourseViewModel = hiltViewModel(), ) { - val state by viewModel.state.collectAsStateWithLifecycle() - val scope = rememberCoroutineScope() val lifecycleOwner = LocalLifecycleOwner.current val context = LocalContext.current + val scope = rememberCoroutineScope() + + val state by viewModel.state.collectAsStateWithLifecycle() + + val cameraPositionState = rememberCameraPositionState() val userId = PreferenceDataStore.getUserId() - val totalTime by viewModel.totalTime.collectAsStateWithLifecycle() + var hasLocationPermission by remember { mutableStateOf(false) } + + val fusedLocationClient = rememberCustomFusedLocationSource( + useTestPoints = false, + cameraPositionState = cameraPositionState, + hasLocationPermission = hasLocationPermission + ) + + val stepCounter = rememberStepCounter() + + LaunchedEffect(viewModel.sideEffect, lifecycleOwner) { + viewModel.sideEffect.flowWithLifecycle(lifecycleOwner.lifecycle) + .collect { sideEffect -> + when (sideEffect) { + is WalkCourseSideEffect.ShowSnackBar -> snackBarHostState.showSnackbar( + sideEffect.message + ) + + is WalkCourseSideEffect.NavigateNext -> navigateNext(sideEffect.regionId) + WalkCourseSideEffect.NavigateUp -> navigateUp() + } + } + } + + val requiredPermissions = remember { + mutableListOf( + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION + ).apply { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + add(Manifest.permission.ACTIVITY_RECOGNITION) + } + }.toTypedArray() + } - val formattedTotalTime by remember(totalTime) { + PermissionRequestEffect( + permissions = requiredPermissions, + onResult = { isGranted -> + hasLocationPermission = isGranted + if (isGranted) { + viewModel.onPermissionsGranted() + fusedLocationClient.setRealTimeLocationListener(viewModel) + /*fusedLocationClient.activate { + if (state.recordingState.isRecording) { + viewModel.startTracking() + } + }*/ + } else { + Toast.makeText(context, "산책 기록을 위해 권한이 필요합니다.", Toast.LENGTH_SHORT).show() + } + } + ) + + val formattedTotalTime by remember { derivedStateOf { - formatTime(totalTime) + formatTime(state.totalTimeMillis) } } - val formatDistance by remember(state.totalDistance) { + val formatDistance by remember { derivedStateOf { - formatDistance(state.totalDistance) + formatDistance(state.mapState.totalDistance) } } - // 0~9 = 0, 10~19 = 1 을 감지 + var mapProperties by remember { + mutableStateOf(MapProperties()) + } + + /*// 0~9 = 0, 10~19 = 1 을 감지 val distanceInTens by remember(state.totalDistance) { // ViewModel의 totalDistance를 참조 derivedStateOf { (state.totalDistance / 10).toInt() // Float을 Int로 변환 @@ -145,112 +200,56 @@ fun WalkCourseRoute( // 이전 10m 단위 값을 저장하여 중복 호출 방지 var lastRecordedDistanceInTens by remember { mutableIntStateOf(-1) - } - - val fusedLocationClient = remember { - LocationServices.getFusedLocationProviderClient(context) - } - - // --- 걸음 수 + 이동거리 - val sensorManager = remember { - context.getSystemService(Context.SENSOR_SERVICE) as SensorManager - } + }*/ - val stepCounterSensor: Sensor? = remember { - sensorManager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER) - } - val stepSensorEventListener = remember { - object : SensorEventListener { - override fun onSensorChanged(event: SensorEvent?) { - if (event?.sensor?.type == Sensor.TYPE_STEP_COUNTER && state.isRecording) { - val totalStepsFromSensor = event.values[0].toLong() - Log.d("StepCounter", "Raw Steps from SensorEventListener: $totalStepsFromSensor") - viewModel.onSensorDataChanged(totalStepsFromSensor) + LaunchedEffect(state.recordingState.isRecording, stepCounter) { + if (state.recordingState.isRecording) { + stepCounter.setStepCountListener(object : StepCountListener { + override fun onStepCountChanged(sessionSteps: Long) { + viewModel.onRawStepData(sessionSteps) } - } - - override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) { - } - } - } - - val locationRequest = remember { - LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 1000) // 1초마다, 높은 정확도 - .setWaitForAccurateLocation(true) // false = 가장 빠른 위치 / true = 가장 정확한 위치 - .build() - } - - val locationCallback = remember(viewModel) { - object : LocationCallback() { - override fun onLocationResult(locationResult: LocationResult) { - locationResult.lastLocation?.let { location -> - if (state.isRecording) { - val newLatLng = LatLng.from(location.latitude, location.longitude) - viewModel.updateLocationAndCalculateDistance(newLatLng, location.accuracy) - Log.d("WalkCourseRoute", "Updated location: $newLatLng, accuracy: ${location.accuracy}") - } + override fun onSensorNotFound() { + Toast.makeText(context, "걸음 수 측정 센서가 없는 기기입니다.", Toast.LENGTH_SHORT).show() } - } + }) + stepCounter.activate() + } else { + stepCounter.deactivate() } } - LaunchedEffect(Unit) { - val currentLocation = getCurrentLocation( - context, - fusedLocationClient, - ) - - viewModel.updateState { - copy( - isRecording = true, - currentLocation = currentLocation, - initialLocationState = UiState.Success(currentLocation), - startedAt = LocalDateTime.now().toString(), + LaunchedEffect(state.mapState.poiPoints.size) { + if (state.mapState.poiPoints.size >= 2) { + val bounds = LatLngBounds.from(state.mapState.poiPoints) + cameraPositionState.animate( + CameraUpdate.fitBounds(bounds, 300) ) } - - Log.e("SearchMapRoute", "Current Location: ${state.currentLocation}") } - LaunchedEffect(viewModel.sideEffect, lifecycleOwner) { - viewModel.sideEffect.flowWithLifecycle(lifecycleOwner.lifecycle) - .collect { sideEffect -> - when (sideEffect) { - is WalkCourseSideEffect.ShowSnackBar -> snackBarHostState.showSnackbar( - sideEffect.message - ) - - is WalkCourseSideEffect.NavigateNext -> navigateNext(sideEffect.regionId) - WalkCourseSideEffect.NavigateUp -> navigateUp() - } + LaunchedEffect(state.mapState.isTrackingEnabled) { + mapProperties = mapProperties.copy( + locationTrackingMode = if (state.mapState.isTrackingEnabled) { + LocationTrackingMode.Follow + } else { + LocationTrackingMode.NoFollow } + ) } - LaunchedEffect(state.isRecording) { - Log.d("WalkCourseRoute", "isRecording: ${state.isRecording}") - if (state.isRecording) { - try { - fusedLocationClient.requestLocationUpdates( - locationRequest, - locationCallback, - Looper.getMainLooper() - ) - Log.d("WalkCourseRoute", "Location updates requested.") - } catch (e: SecurityException) { - Log.e("WalkCourseRoute", "위치 권한 없음: ${e.message}") - snackBarHostState.showSnackbar("위치 권한이 필요합니다.") - viewModel.updateState { - copy(isLocationTracking = false) + LaunchedEffect(cameraPositionState) { + snapshotFlow { cameraPositionState.cameraUpdateReason } + .drop(1) // flow 가 시작될 때의 이전 값 무시 + .collect { reason -> + if (reason == CameraUpdateReason.GESTURE && state.mapState.isTrackingEnabled) { + viewModel.disableTracking() } } - } else { - fusedLocationClient.removeLocationUpdates(locationCallback) - Log.d("WalkCourseRoute", "Location updates removed.") - } } + /*LaunchedEffect(distanceInTens) { // 거리가 10m씩 변경되었을 경우 if (distanceInTens > 0 && distanceInTens > lastRecordedDistanceInTens) { // 0m 제외, 새로운 단위일 때만 @@ -269,33 +268,7 @@ fun WalkCourseRoute( Log.e("SearchMapRoute", "Added POI at 10m interval: ${state.poiPoints}") }*/ - LaunchedEffect(state.isRecording) { - if (state.isRecording) { - while (true) { - delay(1000L) - viewModel.incrementTotalTime() - } - } - } - - DisposableEffect(stepCounterSensor) { - if (stepCounterSensor != null) { - sensorManager.registerListener( - stepSensorEventListener, - stepCounterSensor, - SensorManager.SENSOR_DELAY_NORMAL - ) - } - - onDispose { - if (stepCounterSensor != null) { - sensorManager.unregisterListener(stepSensorEventListener) - fusedLocationClient.removeLocationUpdates(locationCallback) - } - } - } - - when (state.initialLocationState) { + when (state.mapState.initialState) { is UiState.Empty -> Unit is UiState.Failure -> Unit @@ -304,111 +277,38 @@ fun WalkCourseRoute( } is UiState.Success -> { - val mapView = courseMapView( - lifeCycle = lifecycleOwner.lifecycle, - context = context, - onLabelClick = { _, _ -> }, - currentUserLocation = state.currentLocation, - poiPoints = if (isSharedWalk) { - listOf() - } else { - state.poiPoints - }, - isTrackingEnabled = state.isTrackingEnabled, - isPauseTracking = state.isRecording, // true = 잠시 중단, false = 시작 - isStopTracking = state.isLocationTracking, // true = 진짜 중단 - onDisposeCallback = { - fusedLocationClient.removeLocationUpdates(locationCallback) - viewModel.mapCaptureCompleted() - } - ) - - LaunchedEffect(Unit) { - state.currentLocation?.let { - viewModel.addInitLocation( - location = it - ) - } - } - - // 캡처 부분 - LaunchedEffect(state.shouldCaptureMap, state.poiPoints) { - if (state.shouldCaptureMap) { - val glSurfaceView = mapView.surfaceView as? GLSurfaceView - if (glSurfaceView != null) { - withContext(Dispatchers.IO) { - captureMapToBitmap( - glSurfaceView - ) { capturedBitmap -> - capturedBitmap?.let { - viewModel.onMapCaptured(it) - Log.d( - "WalkCourseRoute", - "맵 캡처 성공! (triggered by shouldCaptureMap)" - ) - - viewModel.onMapCaptured(it) // 캡처된 비트맵을 ViewModel로 전달 - Log.d("WalkCourseRoute", "맵 캡처 성공! (triggered by shouldCaptureMap)") - } ?: run { - Log.e("WalkCourseRoute", "맵 캡처 실패: 비트맵이 null입니다.") - viewModel.mapCaptureCompleted() - } - } - } - } else { - viewModel.mapCaptureCompleted() - } - } - } - WalkCourseScreen( paddingValues = paddingValues, navigateUp = navigateUp, - scope = scope, - snackBarHostState = snackBarHostState, - mapView = mapView, + cameraPositionState = cameraPositionState, + currentLocation = state.mapState.currentLocation, + routeLineCoords = state.mapState.poiPoints, + locationSource = fusedLocationClient, + context = context, totalDistance = formatDistance, + mapProperties = mapProperties, isSharedWalk = isSharedWalk, - currentSteps = state.steps, + currentSteps = state.stepCounterState.sessionSteps, totalTime = formattedTotalTime, - isTracking = state.isRecording, // true = 잠시 중단, false = dim + isRecording = state.recordingState.isRecording, // 산책 중단, 계속 여부 + isTracking = state.mapState.isTrackingEnabled, // 산책 포커싱 onClickTracking = { - viewModel.updateState { - copy( - isTrackingEnabled = !this.isTrackingEnabled - ) - } + viewModel.fetchTrackingEnable() + Log.d("WalkCourseRoute", "onClickTracking ${state.mapState.isTrackingEnabled}") }, - onPauseTracking = { - viewModel.onStopTrackingEvent() - viewModel.updateState { - copy( - isRecording = !this.isRecording, - shouldCaptureMap = true, - ) - } + onPauseTracking = { // 일시정지 + }, - onStartTracking = { - viewModel.updateState { - copy( - isRecording = !this.isRecording, - ) - } + onStartTracking = { // 계속하기 + }, onStopTracking = { - viewModel.updateState { - copy( - isLocationTracking = !this.isLocationTracking, - endedAt = LocalDateTime.now().toString() - ) - } - - scope.launch { + /*scope.launch { viewModel.postWalkCourseData(userId = userId.first()) - } + }*/ }, onCaptured = { bitmap -> - viewModel.onMapCaptured(bitmap) + // Todo : bitmap 안쓸거임 }, modifier = modifier, ) @@ -416,25 +316,38 @@ fun WalkCourseRoute( } } +@OptIn(ExperimentalNaverMapApi::class) @Composable fun WalkCourseScreen( paddingValues: PaddingValues, navigateUp: () -> Unit, - scope: CoroutineScope, - snackBarHostState: SnackbarHostState, + cameraPositionState: CameraPositionState, + locationSource: FusedLocationSource, + context: Context, + currentLocation : LatLng?, + routeLineCoords : ImmutableList, totalDistance: String, currentSteps: Long, totalTime: String, + mapProperties: MapProperties, isSharedWalk: Boolean, - isTracking: Boolean, // 버튼 상태 - onClickTracking: () -> Unit, + isTracking: Boolean, // 포커싱 여부 + isRecording: Boolean, // 산책 중단, 계속 여부 + onClickTracking: () -> Unit, // 따라다니기 onStartTracking: () -> Unit, // 계속하기 onPauseTracking: () -> Unit, // 잠시 중단 onStopTracking: () -> Unit, // 종료하기 onCaptured: (Bitmap?) -> Unit, - mapView: MapView, modifier: Modifier = Modifier, ) { + var mapUiSettings by remember { + mutableStateOf( + MapUiSettings( + logoGravity = Gravity.BOTTOM or Gravity.START, + ) + ) + } + Scaffold( modifier = modifier .padding(paddingValues), @@ -445,11 +358,31 @@ fun WalkCourseScreen( modifier = Modifier .padding(pv) ) { - AndroidView( - factory = { mapView }, + NaverMap ( modifier = Modifier - .align(Alignment.Center) - ) + .fillMaxSize(), + cameraPositionState = cameraPositionState, + locationSource = locationSource, + locale = Locale.KOREA, + uiSettings = mapUiSettings, + properties = mapProperties, + ) { + if (currentLocation != null) { + LocationOverlay( + position = currentLocation , + icon = OverlayImage.fromResource(R.drawable.user_poi), + ) + } + + if (routeLineCoords.isNotEmpty() && routeLineCoords.size >= 2) { + PathOverlay( + coords = routeLineCoords, + width = 5.dp, + color = PawKeyTheme.colors.green500, + outlineWidth = 0.dp + ) + } + } Column ( modifier = modifier @@ -466,7 +399,7 @@ fun WalkCourseScreen( .padding(top = 16.dp) ) - if (isTracking) { + if (isRecording) { Spacer(modifier = Modifier.weight(1f)) } else { Column( @@ -491,7 +424,8 @@ fun WalkCourseScreen( textAlign = TextAlign.Center, style = PawKeyTheme.typography.body16M, color = PawKeyTheme.colors.white2, - modifier = Modifier.fillMaxWidth() + modifier = Modifier + .fillMaxWidth() .padding(top = 12.dp) ) } else { @@ -526,31 +460,28 @@ fun WalkCourseScreen( modifier = Modifier .fillMaxWidth() ) { - Spacer(modifier = Modifier.weight(1f)) - - if (isTracking) { - FloatingActionButton( - shape = CircleShape, - onClick = onClickTracking, - containerColor = Color.White - ) { - Icon( - imageVector = ImageVector.vectorResource(R.drawable.ic_course_map_tap_location_on), - contentDescription = "내 위치",//stringResource(id = R.string.lo) - tint = Color.Black - ) - } + FloatingActionButton( + shape = CircleShape, + onClick = onClickTracking, + containerColor = if (isTracking) PawKeyTheme.colors.green500 else Color.White, + ) { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_course_map_tap_location_on), + contentDescription = "내 위치",//stringResource(id = R.string.lo) + tint = Color.Black + ) } } - if (isTracking) { + if (isRecording) { PawkeyButton( text = "중지하기", enabled = true, onClick = { onPauseTracking() - scope.launch { + // Todo : 맵 캡처 로직 변경 예정 + /*scope.launch { val glSurfaceView = mapView.surfaceView as? GLSurfaceView if (glSurfaceView != null) { withContext(Dispatchers.IO) { @@ -569,7 +500,7 @@ fun WalkCourseScreen( } } } - } + }*/ }, modifier = Modifier .padding(top = 16.dp) @@ -580,7 +511,7 @@ fun WalkCourseScreen( modifier = Modifier .fillMaxWidth() .padding(16.dp) - ){ + ) { Text( text = "계속 산책하기", modifier = Modifier @@ -774,53 +705,6 @@ fun createBitmapFromGLSurface(x: Int, y: Int, w: Int, h: Int, gl: GL10): Bitmap? return Bitmap.createBitmap(fullBitmap, startX, startY, safeWidth, safeHeight) } -suspend fun getCurrentLocation( - context: Context, - fusedLocationClient: FusedLocationProviderClient -): LatLng = suspendCancellableCoroutine { continuation -> - val locationRequest = LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 1000L) - .setWaitForAccurateLocation(false) - .setMaxUpdates(1) // 한 번만 업데이트 받음 - .build() - - val locationCallback = object : LocationCallback() { - override fun onLocationResult(locationResult: LocationResult) { - val location = locationResult.lastLocation - if (location != null) { - continuation.resume(LatLng.from(location.latitude, location.longitude)) - fusedLocationClient.removeLocationUpdates(this) - } else { - continuation.resumeWithException(IllegalStateException("위치 정보를 가져올 수 없습니다")) - fusedLocationClient.removeLocationUpdates(this) - } - } - } - - if (ContextCompat.checkSelfPermission( - context, - Manifest.permission.ACCESS_FINE_LOCATION - ) == PackageManager.PERMISSION_GRANTED || - ContextCompat.checkSelfPermission( - context, - Manifest.permission.ACCESS_COARSE_LOCATION - ) == PackageManager.PERMISSION_GRANTED - ) { - fusedLocationClient.requestLocationUpdates( - locationRequest, - locationCallback, - Looper.getMainLooper() - ) - } else { - continuation.resumeWithException(SecurityException("위치 권한을 확인해주세요")) - } - - continuation.invokeOnCancellation { - fusedLocationClient.removeLocationUpdates(locationCallback) - } - - Log.e("getCurrentLocation", "getCurrentLocation ${continuation}") -} - fun formatTime(millis: Long): String { val totalSeconds = TimeUnit.MILLISECONDS.toSeconds(millis) //val hours = TimeUnit.SECONDS.toHours(totalSeconds) diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walk/component/courseMapView.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walk/component/courseMapView.kt deleted file mode 100644 index 4d790049..00000000 --- a/app/src/main/java/com/paw/key/presentation/ui/course/walk/component/courseMapView.kt +++ /dev/null @@ -1,288 +0,0 @@ -package com.paw.key.presentation.ui.course.walk.component - -import android.content.Context -import android.util.Log -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberUpdatedState -import androidx.compose.runtime.setValue -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.toArgb -import androidx.core.content.ContextCompat -import androidx.lifecycle.DefaultLifecycleObserver -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleOwner -import com.kakao.vectormap.KakaoMap -import com.kakao.vectormap.KakaoMapReadyCallback -import com.kakao.vectormap.LatLng -import com.kakao.vectormap.MapLifeCycleCallback -import com.kakao.vectormap.MapView -import com.kakao.vectormap.camera.CameraUpdateFactory -import com.kakao.vectormap.graphics.gl.GLSurfaceView -import com.kakao.vectormap.label.Label -import com.kakao.vectormap.label.LabelOptions -import com.kakao.vectormap.label.LabelStyle -import com.kakao.vectormap.label.TrackingManager -import com.kakao.vectormap.route.RouteLine -import com.kakao.vectormap.route.RouteLineOptions -import com.kakao.vectormap.route.RouteLineSegment -import com.kakao.vectormap.route.RouteLineStyle -import com.kakao.vectormap.route.RouteLineStylesSet -import com.kakao.vectormap.shape.DimScreenLayer -import com.paw.key.R -import java.lang.Math.toDegrees -import java.lang.Math.toRadians -import kotlin.math.atan2 -import kotlin.math.cos -import kotlin.math.sin -import kotlin.math.sqrt - -@Composable -fun courseMapView( - lifeCycle : Lifecycle, - context : Context, - currentUserLocation : LatLng?, - isTrackingEnabled : Boolean, - isPauseTracking : Boolean, - isStopTracking : Boolean, - poiPoints : List, - onLabelClick : (LatLng, String) -> Unit, - onDisposeCallback : () -> Unit -) : MapView { - val mapView = remember { - MapView(context) - } - - var kakaoMapState by remember { - mutableStateOf(null) - } - - val stableCallback = rememberUpdatedState(onLabelClick) - val stableOnDisposeCallback = rememberUpdatedState(onDisposeCallback) - - var centerLabel by remember { - mutableStateOf(null) - } - - // ------------------------------------------------ - // 트래킹 - var trackingManager by remember { - mutableStateOf(null) - } - - var dimScreenLayer by remember { - mutableStateOf(null) - } - - var currentDrawnRouteLine by remember { - mutableStateOf(null) - } - - /*val yeoksamCoordinates = listOf( - LatLng.from(37.50097, 127.03734), // 역삼동 중심 :contentReference[oaicite:1]{index=1} - LatLng.from(37.50079, 127.03689), // 역삼역 (L2) 정문 인근 :contentReference[oaicite:2]{index=2} - LatLng.from(37.50001, 127.03549), // 역삼역 지하철역 (GPS 웹 기준) :contentReference[oaicite:3]{index=3} - LatLng.from(37.49950, 127.03322), // 역삼1동 중심 지역 :contentReference[oaicite:4]{index=4} - LatLng.from(37.49900, 127.03856), // 역삼동 중심 북동쪽 :contentReference[oaicite:5]{index=5} - LatLng.from(37.49999, 127.03719), // 테헤란로 중심가 (중간 위치) ← 위도/경도 참고 위 :contentReference[oaicite:6]{index=6} - LatLng.from(37.49850, 127.03800), // 강남대로 인근 - LatLng.from(37.49800, 127.03450), // 논현로 인근 - LatLng.from(37.50150, 127.03700), // 삼성역 방면 경계 지역 - LatLng.from(37.50050, 127.03900), // 국기원/코엑스 방향 경계 - )*/ - - val drawRouteOnMap: (KakaoMap, List) -> Unit = { kakaoMap, pointsToDraw -> - if (pointsToDraw.isNotEmpty()) { - currentDrawnRouteLine?.remove() - currentDrawnRouteLine = null - - val routeLineStyle = RouteLineStyle.from( - 12f, - ContextCompat.getColor(context, R.color.green_500) - ) - - val routeStylesSet = RouteLineStylesSet.from(routeLineStyle) - - val routeSegments = listOf( - RouteLineSegment.from(pointsToDraw).setStyles(routeLineStyle) - ) - - val routeLineOptions = RouteLineOptions.from(routeSegments) - .setStylesSet(routeStylesSet) - - currentDrawnRouteLine = kakaoMap.routeLineManager?.layer?.addRouteLine(routeLineOptions) - currentDrawnRouteLine?.show() - - kakaoMapState?.moveCamera( - CameraUpdateFactory.fitMapPoints( - poiPoints.toTypedArray(), 150, 15 - ) - ) - } - } - - - LaunchedEffect(poiPoints) { - kakaoMapState?.moveCamera( - CameraUpdateFactory.fitMapPoints( - poiPoints.toTypedArray(), 150, 15 - ) - ) - } - - /*val fusedLocationClient = remember { LocationServices.getFusedLocationProviderClient(context) } - - val locationRequest = remember { - LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 1000) // 1초마다, 높은 정확도 - .setWaitForAccurateLocation(true) - .build() - } - - val locationCallback = remember(isPauseTracking) { - object : LocationCallback() { - override fun onLocationResult(locationResult: LocationResult) { - locationResult.lastLocation?.let { location -> - if (isPauseTracking) { // isRecording 상태를 직접 사용 - val newLatLng = LatLng.from(location.latitude, location.longitude) - currentLocation = newLatLng - Log.d("CourseMapView", "Updated location: $newLatLng, accuracy: ${location.accuracy}") - } - } - } - } - }*/ - - LaunchedEffect(poiPoints) { - kakaoMapState?.let { map -> - drawRouteOnMap(map, poiPoints) - } - } - - DisposableEffect(lifeCycle) { - val observer = object : DefaultLifecycleObserver { - override fun onCreate(owner: LifecycleOwner) { - mapView.start( - object : MapLifeCycleCallback() { - override fun onMapDestroy() { - // 지도 종료 처리 - currentDrawnRouteLine = null - } - - override fun onMapError(error: Exception) { - Log.e("MapView", "지도 오류 발생: $error") - } - }, - object : KakaoMapReadyCallback() { - override fun onMapReady(kakaoMap: KakaoMap) { - kakaoMapState = kakaoMap - dimScreenLayer = kakaoMap.dimScreenManager?.dimScreenLayer - trackingManager = kakaoMap.trackingManager - - centerLabel = kakaoMap.labelManager?.layer?.addLabel( - // userLocation이 null일 경우 - LabelOptions.from("dotLabel", currentUserLocation ?: LatLng.from(37.497942, 127.027619)) - .setStyles( - LabelStyle.from( - R.drawable.user_poi - ).setAnchorPoint(0.5f, 0.5f) - ) - .setRank(5) - ) - - val initialCameraPosition = currentUserLocation ?: LatLng.from(37.497942, 127.027619) - - kakaoMap.moveCamera( - CameraUpdateFactory.newCenterPosition( - initialCameraPosition, 19 - ) - ) - - drawRouteOnMap(kakaoMap, poiPoints) - - kakaoMap.setOnPoiClickListener { _, latLng, _, name -> //name = poi id - stableCallback.value(latLng, name) - } - } - - override fun getPosition(): LatLng { - //userLocation = LatLng.from(locationY, locationX) - return currentUserLocation ?: LatLng.from(37.497942, 127.027619) - } - } - ) - } - - override fun onResume(owner: LifecycleOwner) { - mapView.resume() - - kakaoMapState?.moveCamera( - CameraUpdateFactory.fitMapPoints( - poiPoints.toTypedArray(), 150, 15 - ) - ) - - kakaoMapState?.moveCamera( - CameraUpdateFactory.newCenterPosition( - currentUserLocation, 19 - ) - ) - } - - override fun onPause(owner: LifecycleOwner) { - mapView.pause() - - kakaoMapState?.moveCamera( - CameraUpdateFactory.fitMapPoints( - poiPoints.toTypedArray(), 150, 15 - ) - ) - } - } - - lifeCycle.addObserver(observer) - - onDispose { - lifeCycle.removeObserver(observer) - //fusedLocationClient.removeLocationUpdates(locationCallback) - onDisposeCallback() - stableOnDisposeCallback.value() - } - } - - LaunchedEffect(currentUserLocation, isTrackingEnabled, centerLabel, kakaoMapState) { - if (currentUserLocation != null && centerLabel != null && kakaoMapState != null) { - centerLabel?.moveTo(currentUserLocation) - - /*kakaoMapState?.moveCamera( - CameraUpdateFactory.newCenterPosition( - currentUserLocation, 18 - ) - )*/ - } - } - - LaunchedEffect(centerLabel, trackingManager) { - if (centerLabel != null && trackingManager != null) { - trackingManager?.startTracking(centerLabel) - } - } - - LaunchedEffect(isPauseTracking) { - if (!isPauseTracking) { - trackingManager?.stopTracking() - mapView.isClickable = false - dimScreenLayer?.setColor(Color.Black.copy(alpha = 0.5f).toArgb()) - dimScreenLayer?.setVisible(true) - } else { - trackingManager?.startTracking(centerLabel) - mapView.isClickable = true - dimScreenLayer?.setVisible(false) - } - } - - return mapView -} diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walk/model/MapState.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walk/model/MapState.kt new file mode 100644 index 00000000..3a876f5d --- /dev/null +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walk/model/MapState.kt @@ -0,0 +1,19 @@ +package com.paw.key.presentation.ui.course.walk.model + +import android.graphics.Bitmap +import androidx.compose.runtime.Immutable +import com.naver.maps.geometry.LatLng +import com.paw.key.core.util.UiState +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.persistentListOf + +@Immutable +data class MapState( + val initialState: UiState = UiState.Loading, // 권한 획득 state + val currentLocation: LatLng? = null, + val poiPoints: PersistentList = persistentListOf(), + val totalDistance: Float = 0f, + val isTrackingEnabled: Boolean = true, // 카메라 추적 모드 + val shouldCaptureMap: Boolean = false, + val capturedMapBitmap: Bitmap? = null +) \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walk/model/RecordingState.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walk/model/RecordingState.kt new file mode 100644 index 00000000..4bf4b5eb --- /dev/null +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walk/model/RecordingState.kt @@ -0,0 +1,11 @@ +package com.paw.key.presentation.ui.course.walk.model + +import androidx.compose.runtime.Immutable + +// 산책 기록 상태 - 기록여부, 종료, 시간 +@Immutable +data class RecordingState( + val isRecording: Boolean = false, // 산책 일시정지, 계속하기 + val startedAt: String = "", + val endedAt: String = "" +) \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walk/model/StepCounterState.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walk/model/StepCounterState.kt new file mode 100644 index 00000000..d0b6d054 --- /dev/null +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walk/model/StepCounterState.kt @@ -0,0 +1,10 @@ +package com.paw.key.presentation.ui.course.walk.model + +import androidx.compose.runtime.Immutable + +@Immutable +data class StepCounterState( + // 현재 걸음 수 + val sessionSteps: Long = 0, + val isSensorAvailable: Boolean = true +) \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walk/state/WalkCourseContract.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walk/state/WalkCourseContract.kt index 7b926bc2..3d471ad0 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/course/walk/state/WalkCourseContract.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walk/state/WalkCourseContract.kt @@ -1,43 +1,19 @@ package com.paw.key.presentation.ui.course.walk.state -import android.graphics.Bitmap import androidx.annotation.StringRes import androidx.compose.runtime.Immutable -import com.kakao.vectormap.LatLng import com.paw.key.R -import com.paw.key.core.util.UiState -import kotlinx.collections.immutable.PersistentList -import kotlinx.collections.immutable.persistentListOf +import com.paw.key.presentation.ui.course.walk.model.MapState +import com.paw.key.presentation.ui.course.walk.model.RecordingState +import com.paw.key.presentation.ui.course.walk.model.StepCounterState class WalkCourseContract { @Immutable data class WalkCourseState( - val uiState: UiState> = UiState.Loading, - val poiPoints: PersistentList = persistentListOf(), - - val startedAt: String = "", - val endedAt: String = "", - - val bitmap: Bitmap? = null, - - // 현재 걸음 수 - val steps: Long = 0, - val totalDistance: Float = 0f, - - val initialSensorSteps: Long? = null, - val prevSteps: Long = 0, - val isWalking: Boolean = false, - - val initialLocationState : UiState = UiState.Loading, - val currentLocation: LatLng? = null, - val lastLocation: LatLng? = null, - val cameraState : Boolean = false, - val isLocationTracking: Boolean = false, - - val isTrackingEnabled : Boolean = false, - val isRecording : Boolean = false, // 기록 중 상태관리 - - val shouldCaptureMap: Boolean = false + val recordingState: RecordingState = RecordingState(), + val mapState: MapState = MapState(), + val stepCounterState: StepCounterState = StepCounterState(), + val totalTimeMillis: Long = 0L, ) sealed class WalkCourseSideEffect { diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walk/viewmodel/WalkCourseViewModel.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walk/viewmodel/WalkCourseViewModel.kt index 00a4692f..c43d5eb5 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/course/walk/viewmodel/WalkCourseViewModel.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walk/viewmodel/WalkCourseViewModel.kt @@ -1,69 +1,157 @@ package com.paw.key.presentation.ui.course.walk.viewmodel -import android.content.Context -import android.graphics.Bitmap import android.location.Location import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.kakao.vectormap.LatLng import com.paw.key.core.util.PhotoUtils -import com.paw.key.core.util.PreferenceDataStore +import com.paw.key.core.util.UiState +import com.paw.key.core.extension.toLatLng import com.paw.key.domain.model.entity.walkcourse.CoordinateEntity import com.paw.key.domain.model.entity.walkcourse.WalkCourseEntity import com.paw.key.domain.repository.WalkSharedResultRepository import com.paw.key.domain.repository.walkcourse.WalkCourseRepository +import com.paw.key.presentation.ui.course.util.RealTimeLocationListener import com.paw.key.presentation.ui.course.walk.state.WalkCourseContract.WalkCourseSideEffect import com.paw.key.presentation.ui.course.walk.state.WalkCourseContract.WalkCourseState import dagger.hilt.android.lifecycle.HiltViewModel -import dagger.hilt.android.qualifiers.ApplicationContext -import kotlinx.collections.immutable.PersistentList -import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.sample import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import java.time.LocalDateTime import javax.inject.Inject @HiltViewModel class WalkCourseViewModel @Inject constructor( - @ApplicationContext private val context: Context, - private val walkSharedResultRepository : WalkSharedResultRepository, + private val walkSharedResultRepository: WalkSharedResultRepository, private val walkCourseRepository: WalkCourseRepository -) : ViewModel() { +) : ViewModel(), RealTimeLocationListener { private val _state = MutableStateFlow(WalkCourseState()) - val state : StateFlow + val state: StateFlow get() = _state.asStateFlow() private val _sideEffect = MutableSharedFlow() val sideEffect: SharedFlow get() = _sideEffect.asSharedFlow() - private val _totalTime = MutableStateFlow(0L) - val totalTime: StateFlow = _totalTime.asStateFlow() + private var timerJob: Job? = null - fun incrementTotalTime() { - _totalTime.update { - it + 1000L + private var initialSensorSteps: Long = -1L + private var lastLocation: Location? = null + + fun onPermissionsGranted() { + if (_state.value.mapState.initialState is UiState.Loading) { + _state.update { + it.copy( + mapState = it.mapState.copy( + initialState = UiState.Success(true) + ) + ) + } + startTracking() + } + } + + fun startTracking() { + _state.update { currentState -> + val newRecordingState = currentState.recordingState.copy( + isRecording = true, + startedAt = LocalDateTime.now().toString() + ) + + currentState.copy( + recordingState = newRecordingState + ) + } + startTimer() + } + + fun pauseTracking() { + _state.update { currentState -> + val newRecordingState = currentState.recordingState.copy( + isRecording = false, + endedAt = LocalDateTime.now().toString() + ) + + currentState.copy( + recordingState = newRecordingState + ) + } + stopTimer() + } + + private fun startTimer() { + if (timerJob?.isActive == true) return + timerJob = viewModelScope.launch { + while (true) { + delay(1000L) + _state.update { currentState -> + val newTimeMills = currentState.totalTimeMillis + 1000L + currentState.copy( + totalTimeMillis = newTimeMills + ) + } + } + } + } + + private fun stopTimer() { + timerJob?.cancel() + } + + override fun onCleared() { + super.onCleared() + stopTimer() + } + + fun fetchTrackingEnable() { + _state.update { currentState -> + currentState.copy( + mapState = currentState.mapState.copy( + isTrackingEnabled = !currentState.mapState.isTrackingEnabled + ) + ) } } - fun addInitLocation(location: LatLng) { - val currentList = state.value.poiPoints.toMutableList() - currentList.add(location) - _state.value = _state.value.copy( - poiPoints = currentList.toPersistentList() - ) + fun disableTracking() { + _state.update { currentState -> + if (currentState.mapState.isTrackingEnabled) { + currentState.copy( + mapState = currentState.mapState.copy(isTrackingEnabled = false) + ) + } else { + currentState + } + } + } + + fun onRawStepData(totalStepsFromSensor: Long) { + if (initialSensorSteps == -1L) { + initialSensorSteps = totalStepsFromSensor + } + + val sessionSteps = totalStepsFromSensor - initialSensorSteps + + _state.update { currentState -> + currentState.copy( + stepCounterState = currentState.stepCounterState.copy( + sessionSteps = sessionSteps + ) + ) + } } // 서버 통신 fun postWalkCourseData(userId: Int) = viewModelScope.launch { - val bitmap = state.value.bitmap + val bitmap = _state.value.mapState.capturedMapBitmap if (bitmap == null) { _sideEffect.emit(WalkCourseSideEffect.ShowSnackBar("산책 이미지가 없습니다.")) return@launch @@ -82,14 +170,14 @@ class WalkCourseViewModel @Inject constructor( } val routeEntity = WalkCourseEntity( - coordinates = state.value.poiPoints.map { // la, lo + coordinates = _state.value.mapState.poiPoints.map { CoordinateEntity(it.latitude, it.longitude) }, - distance = state.value.totalDistance.toInt(), - duration = (_totalTime.value / 1000).toInt(), - startedAt = state.value.startedAt, - endedAt = state.value.endedAt, - stepCount = state.value.steps.toInt() + distance = _state.value.mapState.totalDistance.toInt(), + duration = (_state.value.totalTimeMillis / 1000).toInt(), + startedAt = _state.value.recordingState.startedAt, + endedAt = _state.value.recordingState.endedAt, + stepCount = _state.value.stepCounterState.sessionSteps.toInt() ) val result = walkCourseRepository.postWalkCourse( @@ -112,165 +200,80 @@ class WalkCourseViewModel @Inject constructor( } } + fun onStopTrackingEvent() { + viewModelScope.launch { + val currentWalkState = _state.value - // Todo : = updateState 로 일관되게 정리하기 - fun updateLocationAndCalculateDistance(newLocation: LatLng, accuracy: Float) { - val MIN_ACCURACY_THRESHOLD = 25f // 미터 단위 (이보다 높은 정확도일 때만 사용) - if (accuracy > MIN_ACCURACY_THRESHOLD) { - return - } - - _state.update { currentUiState -> - val oldLocation = currentUiState.lastLocation - var distanceIncrement = 0f - - if (oldLocation != null) { - val oldAndroidLocation = Location("prev_location").apply { - latitude = oldLocation.latitude - longitude = oldLocation.longitude - } - - val newAndroidLocation = Location("current_location").apply { - latitude = newLocation.latitude - longitude = newLocation.longitude - } - - /*val calculatedDistance = oldAndroidLocation.distanceTo(newAndroidLocation) - - val MIN_DISTANCE_THRESHOLD = 1f // 미터 단위 - if (calculatedDistance >= MIN_DISTANCE_THRESHOLD) { - distanceIncrement = calculatedDistance - }*/ - distanceIncrement = oldAndroidLocation.distanceTo(newAndroidLocation) - } - - val updatedPoiPoints: PersistentList = - if (currentUiState.poiPoints.isEmpty() && currentUiState.lastLocation == null) { - // 첫 위치일 경우 무조건 추가 - currentUiState.poiPoints.add(newLocation) - } else if (distanceIncrement > 0) { // (이동이 있었으면) 추가 - currentUiState.poiPoints.add(newLocation) - } else { - // 이동 거리가 0이거나 이전 위치가 없는 경우 (첫 위치가 이미 추가된 후) - currentUiState.poiPoints - } - - val newTotalDistance = currentUiState.totalDistance + distanceIncrement - - currentUiState.copy( - lastLocation = newLocation, - currentLocation = newLocation, - totalDistance = newTotalDistance, - poiPoints = updatedPoiPoints - ) - } - } - - fun onSensorDataChanged(totalStepsFromSensor: Long) { - updateState { - val initial = initialSensorSteps - val currentCalculatedSteps: Long - val currentIsWalking: Boolean - - if (initial == null) { - currentCalculatedSteps = 0L - currentIsWalking = false - - copy( - initialSensorSteps = totalStepsFromSensor, - steps = currentCalculatedSteps, - prevSteps = currentCalculatedSteps, - isWalking = currentIsWalking - ) - } else { - currentCalculatedSteps = totalStepsFromSensor - initial - - currentIsWalking = if (currentCalculatedSteps > prevSteps) { - true - } else if (currentCalculatedSteps == prevSteps && prevSteps > 0) { - isWalking - } else { - false - } - - copy( - steps = currentCalculatedSteps, - prevSteps = currentCalculatedSteps, // 현재 걸음 수를 이전 걸음 수로 저장 - isWalking = currentIsWalking + try { + walkSharedResultRepository.saveResult( + bitmap = currentWalkState.mapState.capturedMapBitmap, + totalTime = currentWalkState.totalTimeMillis, + distance = currentWalkState.mapState.totalDistance, + steps = currentWalkState.stepCounterState.sessionSteps.toInt(), + points = currentWalkState.mapState.poiPoints.toList() ) + _sideEffect.emit(WalkCourseSideEffect.ShowSnackBar("산책 기록이 성공적으로 저장되었습니다.")) + } catch (e: Exception) { + Log.e("WalkCourseViewModel", "Error saving walk summary data: ${e.message}", e) + _sideEffect.emit(WalkCourseSideEffect.ShowSnackBar("산책 기록 저장 실패: ${e.localizedMessage}")) } } } - fun updateState(reducer: WalkCourseState.() -> WalkCourseState) { - Log.e("updateState", "updateState called") - _state.update { - it.reducer() + override fun onLocationChanged(location: Location) { + if (location.accuracy > LOCATION_ACCURACY_THRESHOLD) { + Log.e("onLocationChanged", "onLocationChanged: $location") + return } - } - fun mapCaptureCompleted() { - updateState { - copy(shouldCaptureMap = false) - } - } + Log.e("onLocationChanged", "onLocationChanged: $location") - fun onMapCaptured(bitmap: Bitmap?) { - if (bitmap == null) { - return - } - updateState { - copy(bitmap = bitmap) - } + val newLatLng = location.toLatLng() + val lastPoint = lastLocation - viewModelScope.launch { - try { - walkSharedResultRepository.saveResult( - bitmap = state.value.bitmap, - totalTime = _totalTime.value, - distance = state.value.totalDistance, - steps = state.value.steps.toInt(), - points = state.value.poiPoints.toList() + // 최초 위치 수신 시 + if (lastPoint == null) { + _state.update { + it.copy( + mapState = it.mapState.copy( + currentLocation = newLatLng, + poiPoints = it.mapState.poiPoints.add(newLatLng) + ) ) - Log.d("WalkCourseViewModel", "state : ${state.value}") - - _sideEffect.emit(WalkCourseSideEffect.ShowSnackBar("산책 지도 이미지가 저장되었습니다.")) - Log.d("WalkCourseViewModel", "Map captured bitmap saved to DataStore.") - } catch (e: Exception) { - _sideEffect.emit(WalkCourseSideEffect.ShowSnackBar("산책 지도 이미지 저장 실패: ${e.localizedMessage}")) - Log.e("WalkCourseViewModel", "Error saving captured bitmap: ${e.localizedMessage}") - } finally { - mapCaptureCompleted() // 캡처 시도 후, 성공/실패 여부와 관계없이 플래그 리셋 } + lastLocation = location // 마지막 기록 위치로 설정 + return } - } - - fun onStopTrackingEvent() { - viewModelScope.launch { - val currentWalkState = _state.value - try { - walkSharedResultRepository.saveResult( - bitmap = currentWalkState.bitmap, - totalTime = _totalTime.value, - distance = currentWalkState.totalDistance, - steps = currentWalkState.steps.toInt(), - points = currentWalkState.poiPoints.toList() + // 이후 위치 수신 시 + val distance = lastPoint.distanceTo(location) + + // Todo : 3m 이상 이동 시 경로 추가로 되어있는데 추후 어떻게 할 건지 확인 + if (distance >= 3.0f) { + _state.update { currentState -> + val newTotalDistance = currentState.mapState.totalDistance + distance + currentState.copy( + mapState = currentState.mapState.copy( + currentLocation = newLatLng, + totalDistance = newTotalDistance, + poiPoints = currentState.mapState.poiPoints.add(newLatLng) + ) ) - //Log.e("WalkCourseViewModel", PreferenceDataStore.getTotalTime(context).toString()) - _sideEffect.emit(WalkCourseSideEffect.ShowSnackBar("산책 기록이 성공적으로 저장되었습니다.")) - } catch (e: Exception) { - Log.e("WalkCourseViewModel", "Error saving all walk summary data: ${e.message}", e) - _sideEffect.emit(WalkCourseSideEffect.ShowSnackBar("산책 기록 저장 실패: ${e.localizedMessage}")) - } finally { - walkSharedResultRepository.saveResult( - bitmap = currentWalkState.bitmap, - totalTime = _totalTime.value, - distance = currentWalkState.totalDistance, - steps = currentWalkState.steps.toInt(), - points = currentWalkState.poiPoints.toList() + } + lastLocation = location + } else { + _state.update { + it.copy( + mapState = it.mapState.copy( + currentLocation = newLatLng + ) ) } } } + + companion object { + private const val TIMER_INTERVAL_MS = 1000L + private const val LOCATION_ACCURACY_THRESHOLD = 25f + } } \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walkcomplete/state/WalkCompleteContract.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walkcomplete/state/WalkCompleteContract.kt index d5564049..f6ed3c94 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/course/walkcomplete/state/WalkCompleteContract.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walkcomplete/state/WalkCompleteContract.kt @@ -2,7 +2,7 @@ package com.paw.key.presentation.ui.course.walkcomplete.state import android.graphics.Bitmap import androidx.compose.runtime.Immutable -import com.kakao.vectormap.LatLng +import com.naver.maps.geometry.LatLng class WalkCompleteContract { @Immutable diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/component/WalkReviewDialog.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/component/WalkReviewDialog.kt index 2f9a4f9f..54cc94c9 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/component/WalkReviewDialog.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/component/WalkReviewDialog.kt @@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Card -import androidx.compose.material3.CardColors import androidx.compose.material3.CardDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -26,7 +25,7 @@ import com.airbnb.lottie.compose.LottieCompositionSpec import com.airbnb.lottie.compose.animateLottieCompositionAsState import com.airbnb.lottie.compose.rememberLottieComposition import com.paw.key.core.designsystem.theme.PawKeyTheme -import com.paw.key.core.util.noRippleClickable +import com.paw.key.core.extension.noRippleClickable @Composable fun WalkReviewDialog( diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/component/WalkReviewImageItem.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/component/WalkReviewImageItem.kt index 4d887659..5ed9f63c 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/component/WalkReviewImageItem.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/component/WalkReviewImageItem.kt @@ -29,7 +29,7 @@ import androidx.compose.ui.unit.dp import coil.compose.AsyncImage import com.paw.key.R import com.paw.key.core.designsystem.theme.PawKeyTheme -import com.paw.key.core.util.noRippleClickable +import com.paw.key.core.extension.noRippleClickable @Composable fun WalkReviewItem( diff --git a/app/src/main/java/com/paw/key/presentation/ui/dummy/DummyScreen.kt b/app/src/main/java/com/paw/key/presentation/ui/dummy/DummyScreen.kt index 7f009749..c2538f0c 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/dummy/DummyScreen.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/dummy/DummyScreen.kt @@ -27,7 +27,7 @@ import com.paw.key.presentation.ui.dummy.state.DummyContract.DummySideEffect import com.paw.key.R import com.paw.key.core.designsystem.theme.PawKeyTheme import com.paw.key.core.util.UiState -import com.paw.key.core.util.noRippleClickable +import com.paw.key.core.extension.noRippleClickable import com.paw.key.domain.model.entity.DummyUser import com.paw.key.presentation.ui.dummy.component.DummyItem diff --git a/app/src/main/java/com/paw/key/presentation/ui/dummy/component/DummyItem.kt b/app/src/main/java/com/paw/key/presentation/ui/dummy/component/DummyItem.kt index da638368..03b9d2c7 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/dummy/component/DummyItem.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/dummy/component/DummyItem.kt @@ -12,13 +12,12 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import coil.compose.AsyncImage //import com.paw.key.R.string.profile import com.paw.key.core.designsystem.theme.PawKeyTheme -import com.paw.key.core.util.noRippleClickable +import com.paw.key.core.extension.noRippleClickable @Composable fun DummyItem( diff --git a/app/src/main/java/com/paw/key/presentation/ui/home/HomeLocationSettingScreen.kt b/app/src/main/java/com/paw/key/presentation/ui/home/HomeLocationSettingScreen.kt index 86484e8f..1f19a583 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/home/HomeLocationSettingScreen.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/home/HomeLocationSettingScreen.kt @@ -1,7 +1,5 @@ package com.paw.key.presentation.ui.home -import DistrictDto -import android.util.Log import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -15,8 +13,6 @@ import androidx.compose.foundation.layout.width import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -30,7 +26,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.paw.key.R import com.paw.key.core.designsystem.component.PawkeyButton import com.paw.key.core.designsystem.theme.PawKeyTheme -import com.paw.key.core.util.noRippleClickable +import com.paw.key.core.extension.noRippleClickable import com.paw.key.presentation.ui.home.viewmodel.HomeViewModel import com.paw.key.presentation.ui.signup.component.FormField import com.paw.key.presentation.ui.signup.component.LocationItem diff --git a/app/src/main/java/com/paw/key/presentation/ui/home/HomeScreen.kt b/app/src/main/java/com/paw/key/presentation/ui/home/HomeScreen.kt index ab68be4f..fe81fe91 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/home/HomeScreen.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/home/HomeScreen.kt @@ -15,7 +15,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -36,7 +35,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.paw.key.R import com.paw.key.core.designsystem.component.CourseCard import com.paw.key.core.designsystem.theme.PawKeyTheme -import com.paw.key.core.util.noRippleClickable +import com.paw.key.core.extension.noRippleClickable import com.paw.key.presentation.ui.home.component.DaytimeCard import com.paw.key.presentation.ui.home.component.HomeTopBar import com.paw.key.presentation.ui.home.component.RowCalendar @@ -145,11 +144,22 @@ fun HomeScreen( item { Spacer(modifier = Modifier.height(12.dp)) Row( - modifier = Modifier.fillMaxWidth() + modifier = Modifier + .fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp) ) { - DaytimeCard(daytime = "05:06", daystate = "일출") - Spacer(modifier = Modifier.weight(1F)) - TrackingCard(onClick = { navigateNext() }) + DaytimeCard( + daytime = "05:06", + daystate = "일출", + modifier = Modifier + .weight(0.3f) + ) + + TrackingCard( + onClick = { navigateNext() }, + modifier = Modifier + .weight(0.7f) + ) } } diff --git a/app/src/main/java/com/paw/key/presentation/ui/login/LoginScreen.kt b/app/src/main/java/com/paw/key/presentation/ui/login/LoginScreen.kt index ea22b5c0..a4576ec4 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/login/LoginScreen.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/login/LoginScreen.kt @@ -2,7 +2,6 @@ package com.paw.key.presentation.ui.login import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer @@ -26,7 +25,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester @@ -44,7 +42,7 @@ import com.paw.key.core.designsystem.component.PawkeyButton import com.paw.key.core.designsystem.component.TopBar import com.paw.key.core.designsystem.theme.PawKeyTheme import com.paw.key.core.util.PreferenceDataStore -import com.paw.key.core.util.noRippleClickable +import com.paw.key.core.extension.noRippleClickable import com.paw.key.presentation.ui.login.component.LoginTextField import com.paw.key.presentation.ui.login.viewmodel.LoginViewModel import kotlinx.coroutines.launch @@ -54,6 +52,7 @@ fun LoginRoute( paddingValues: PaddingValues, navigateUp: () -> Unit, navigateNext: () -> Unit, + navigateHome: () -> Unit, snackBarHostState: SnackbarHostState, modifier: Modifier = Modifier, viewModel: LoginViewModel = hiltViewModel() @@ -83,7 +82,8 @@ fun LoginRoute( modifier = modifier, onEmailChanged = viewModel::onEmailChanged, onPasswordChanged = viewModel::onPasswordChanged, - onClickIcon = viewModel::onPasswordVisibilityChanged + onClickIcon = viewModel::onPasswordVisibilityChanged, + navigateHome = navigateHome ) } @@ -93,6 +93,7 @@ fun LoginScreen( paddingValues: PaddingValues, navigateUp: () -> Unit, navigateNext: () -> Unit, + navigateHome : () -> Unit, onEmailChanged: (String) -> Unit, onPasswordChanged: (String) -> Unit, onClickIcon: () -> Unit, @@ -119,6 +120,7 @@ fun LoginScreen( TopBar( title = "기존 계정으로 로그인", onBackClick = navigateUp, + onClickTitle = navigateHome, modifier = Modifier.padding( top = paddingValues.calculateTopPadding() ) @@ -201,14 +203,14 @@ fun LoginScreen( .navigationBarsPadding() .padding(bottom = 60.dp) ) { - PawkeyButton( + /* PawkeyButton( text = "신규 계정으로 회원가입", onClick = navigateUp, enabled = true, isBackGround = true, isBorder = false, modifier = Modifier.fillMaxWidth() - ) + )*/ Spacer(modifier = Modifier.height(12.dp)) @@ -233,6 +235,7 @@ private fun PreviewLoginScreen(){ onEmailChanged = {}, onPasswordChanged = {}, onClickIcon = {}, + navigateHome = {}, snackBarHostState = SnackbarHostState(), email = "", password = "", diff --git a/app/src/main/java/com/paw/key/presentation/ui/main/MainActivity.kt b/app/src/main/java/com/paw/key/presentation/ui/main/MainActivity.kt index da6a454b..9b2643a6 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/main/MainActivity.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/main/MainActivity.kt @@ -1,5 +1,7 @@ package com.paw.key.presentation.ui.main +import android.annotation.SuppressLint +import android.content.pm.ActivityInfo import android.os.Build import android.os.Bundle import androidx.activity.ComponentActivity @@ -11,12 +13,15 @@ import com.paw.key.core.designsystem.theme.PawKeyTheme import com.paw.key.core.util.PreferenceDataStore import dagger.hilt.android.AndroidEntryPoint + @AndroidEntryPoint class MainActivity : ComponentActivity() { + @SuppressLint("SourceLockedOrientationActivity") @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() + requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT PreferenceDataStore.init(this) WindowCompat.getInsetsController(window, window.decorView).apply { diff --git a/app/src/main/java/com/paw/key/presentation/ui/main/PawKeyNavHost.kt b/app/src/main/java/com/paw/key/presentation/ui/main/PawKeyNavHost.kt index 4e701e0b..a39e1e39 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/main/PawKeyNavHost.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/main/PawKeyNavHost.kt @@ -129,7 +129,6 @@ fun PawKeyNavHost( paddingValues = paddingValues, navigateUp = navigator::navigateUp, navigateNext = { - Log.e("navigateNext", "navigateNext : $it") navigator.navigateWalkCompletion( routeId = it, ) @@ -206,9 +205,12 @@ fun PawKeyNavHost( ) archivedDetailNavGraph( - navigateUp = navigator::navigateHome, + // Todo 그냥 리스트에서 상세보기 후 뒤로가기 + navigateUp = navigator::navigateUp, + /*navigateDetail = { + navigator.navController.navigateCourse(index = 1, navOptions = null) + },*/ navigateToSharedWalk = { routeId, pageId -> - Log.e("navigateNext", "navigateNext : $routeId") navigator.navigateSharedWalkCourse( routeId = routeId, pageId = pageId @@ -265,9 +267,14 @@ fun PawKeyNavHost( loginNavGraph( paddingValues = paddingValues, navigateUp = { + navigator.navigateUp() + }, + navigateNext = { navigator.navigateSignUpFlow() }, - navigateNext = navigator::navigateHome, + navigateHome = { + navigator.navigateHome() + }, snackBarHostState = snackbarHostState ) diff --git a/app/src/main/java/com/paw/key/presentation/ui/main/component/MainBottomBar.kt b/app/src/main/java/com/paw/key/presentation/ui/main/component/MainBottomBar.kt index be3fac56..1bca3d2a 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/main/component/MainBottomBar.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/main/component/MainBottomBar.kt @@ -30,7 +30,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import com.paw.key.core.designsystem.theme.PawKeyTheme -import com.paw.key.core.util.noRippleClickable +import com.paw.key.core.extension.noRippleClickable import com.paw.key.presentation.ui.main.MainTab import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf diff --git a/app/src/main/java/com/paw/key/presentation/ui/mypage/MyPageScreen.kt b/app/src/main/java/com/paw/key/presentation/ui/mypage/MyPageScreen.kt index aac4caff..9723e9d7 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/mypage/MyPageScreen.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/mypage/MyPageScreen.kt @@ -27,10 +27,9 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil.compose.AsyncImage import coil.request.ImageRequest import com.paw.key.R -import com.paw.key.core.designsystem.component.SubChip import com.paw.key.core.designsystem.theme.PawKeyTheme import com.paw.key.core.util.PreferenceDataStore -import com.paw.key.core.util.noRippleClickable +import com.paw.key.core.extension.noRippleClickable import com.paw.key.presentation.ui.mypage.component.GrayChip import com.paw.key.presentation.ui.mypage.state.MyPageState import com.paw.key.presentation.ui.mypage.viewmodel.MyPageViewModel diff --git a/app/src/main/java/com/paw/key/presentation/ui/mypage/component/GrayChip.kt b/app/src/main/java/com/paw/key/presentation/ui/mypage/component/GrayChip.kt index ae236708..08e8e598 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/mypage/component/GrayChip.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/mypage/component/GrayChip.kt @@ -10,7 +10,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.paw.key.core.designsystem.theme.PawKeyTheme -import com.paw.key.core.util.noRippleClickable +import com.paw.key.core.extension.noRippleClickable @Preview @Composable diff --git a/app/src/main/java/com/paw/key/presentation/ui/region/RegionalManagementScreen.kt b/app/src/main/java/com/paw/key/presentation/ui/region/RegionalManagementScreen.kt index 4ef74b6e..71f0626f 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/region/RegionalManagementScreen.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/region/RegionalManagementScreen.kt @@ -1,6 +1,5 @@ package com.paw.key.presentation.ui.region -import android.util.Log import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -8,12 +7,11 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState @@ -21,31 +19,39 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember 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.layout.onSizeChanged import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.compose.ui.viewinterop.AndroidView import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.flowWithLifecycle -import com.kakao.vectormap.LatLng -import com.kakao.vectormap.MapView +import com.naver.maps.geometry.LatLng +import com.naver.maps.geometry.LatLngBounds +import com.naver.maps.map.CameraUpdate +import com.naver.maps.map.compose.CameraPositionState +import com.naver.maps.map.compose.ExperimentalNaverMapApi +import com.naver.maps.map.compose.NaverMap +import com.naver.maps.map.compose.PolygonOverlay +import com.naver.maps.map.compose.rememberCameraPositionState import com.paw.key.core.designsystem.component.CustomSnackBar import com.paw.key.core.designsystem.component.PawkeyButton import com.paw.key.core.designsystem.theme.PawKeyTheme -import com.paw.key.core.util.PreferenceDataStore import com.paw.key.core.util.UiState -import com.paw.key.presentation.ui.region.component.regionalMapView -import com.paw.key.presentation.ui.region.state.RegionContract +import com.paw.key.presentation.ui.region.state.DrawType +import com.paw.key.presentation.ui.region.state.RegionSideEffect import com.paw.key.presentation.ui.region.viewmodel.RegionViewModel -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.launch +import kotlinx.collections.immutable.ImmutableList +@OptIn(ExperimentalNaverMapApi::class) @Composable fun RegionalManagementRoute( paddingValues: PaddingValues, @@ -57,61 +63,63 @@ fun RegionalManagementRoute( viewModel: RegionViewModel = hiltViewModel() ) { val state by viewModel.state.collectAsStateWithLifecycle() - val lifecycleOwner = LocalLifecycleOwner.current - val context = LocalContext.current - val userId = PreferenceDataStore.getUserId() - val scope = rememberCoroutineScope() + val cameraPositionState = rememberCameraPositionState() - LaunchedEffect(Unit) { - viewModel.getRegionGeometry( - userId = userId.first(), - regionId = regionId, - ) + val bottomPanelHeightPx = remember { + mutableIntStateOf(0) } - LaunchedEffect(viewModel.sideEffect, lifecycleOwner) { + val density = LocalDensity.current + + LaunchedEffect(Unit) { viewModel.sideEffect.flowWithLifecycle(lifecycleOwner.lifecycle) .collect { sideEffect -> when (sideEffect) { - is RegionContract.RegionSideEffect.ShowSnackBar -> { + is RegionSideEffect.ShowSnackBar -> { snackBarHostState.showSnackbar( sideEffect.message ) navigateNext() } - RegionContract.RegionSideEffect.NavigateNext -> navigateNext() - RegionContract.RegionSideEffect.NavigateUp -> navigateUp() + RegionSideEffect.NavigateNext -> navigateNext() + RegionSideEffect.NavigateUp -> navigateUp() } } } - when (state.uiState) { + when (val uiState = state.uiState) { is UiState.Success -> { - val mapView = regionalMapView( - lifeCycle = lifecycleOwner.lifecycle, - context = context, - currentUserLocation = state.centerLocation, - polyPoints = (state.uiState as UiState.Success>>).data - ) + LaunchedEffect(state.entireCoordinates.size) { + if (state.entireCoordinates.size >= 2 && bottomPanelHeightPx.intValue > 0) { + val bounds = LatLngBounds.from(state.entireCoordinates) + + val bottomPadding = + bottomPanelHeightPx.intValue + with(density) { 100.dp.roundToPx() } + + cameraPositionState.move( + CameraUpdate.fitBounds(bounds, 100, 100, 100, bottomPadding) + ) + } + } RegionalManagementScreen( paddingValues = paddingValues, snackBarHostState = snackBarHostState, - mapView = mapView, + cameraPositionState = cameraPositionState, + type = state.drawType, + regionCoordinates = uiState.data, selectedRegion = state.selectedRegion, preRegionName = state.preRegionName, regionName = state.regionName, onClickButton = { - scope.launch { - viewModel.patchRegion( - userId = userId.first(), - regionId = regionId - ) - } + viewModel.patchRegion() + }, + onSizeChanged = { + bottomPanelHeightPx.intValue = it }, - modifier = modifier + modifier = Modifier ) } @@ -123,61 +131,187 @@ fun RegionalManagementRoute( } } +@OptIn(ExperimentalNaverMapApi::class) @Composable fun RegionalManagementScreen( paddingValues: PaddingValues, snackBarHostState: SnackbarHostState, - mapView: MapView, - selectedRegion : String?, - preRegionName : String?, - regionName : String?, - onClickButton : () -> Unit, + cameraPositionState: CameraPositionState, + type: DrawType, + regionCoordinates: ImmutableList>, + selectedRegion: String?, + preRegionName: String?, + regionName: String?, + onClickButton: () -> Unit, + onSizeChanged: (Int) -> Unit, modifier: Modifier = Modifier, ) { Scaffold( - modifier = modifier - .padding(paddingValues), + modifier = modifier, snackbarHost = { SnackbarHost( hostState = snackBarHostState, - snackbar = { - CustomSnackBar( - data = it, - modifier = Modifier - .padding(bottom = LocalConfiguration.current.screenHeightDp.dp * 0.4f + 16.dp) - ) - } - ) + modifier = Modifier.padding( + bottom = LocalConfiguration.current.screenHeightDp.dp * 0.4f + ) + ) { data -> + CustomSnackBar( + data = data + ) + } } - ) { pv -> + ) { innerPadding -> Box( modifier = Modifier - .padding(pv) + .fillMaxSize() + .padding(innerPadding) ) { - AndroidView( - factory = { mapView }, + NaverMap( modifier = Modifier - .align(Alignment.Center) - ) + .align(Alignment.Center), + cameraPositionState = cameraPositionState + ) { + when (type) { + DrawType.SINGLE -> { + val singlePolygonCoords = regionCoordinates.first() + if (singlePolygonCoords.isNotEmpty()) { + PolygonOverlay( + coords = singlePolygonCoords, + color = PawKeyTheme.colors.green500.copy(alpha = 0.3f), + outlineWidth = 1.dp, + outlineColor = PawKeyTheme.colors.green500 + ) + } + } + + DrawType.MULTIPLE -> { + regionCoordinates.forEach { + PolygonOverlay( + coords = it, + color = PawKeyTheme.colors.green500.copy(alpha = 0.3f), + outlineWidth = 1.dp, + outlineColor = PawKeyTheme.colors.green500 + ) + } + } + } + } - Box( + Column( modifier = Modifier .fillMaxWidth() - .height(LocalConfiguration.current.screenHeightDp.dp * 0.4f) .align(Alignment.BottomCenter) - .clip( - RoundedCornerShape( - topStart = 12.dp, - topEnd = 12.dp - ) - ) + .clip(RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)) .background( - color = PawKeyTheme.colors.white1 + color = PawKeyTheme.colors.white1, + shape = RoundedCornerShape( + topStart = 16.dp, topEnd = 16.dp + ) ) .padding(horizontal = 16.dp, vertical = 24.dp) + .onSizeChanged { size -> + onSizeChanged(size.height) + } + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp, bottom = 16.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "선택한 위치", + style = PawKeyTheme.typography.head20B2, + color = PawKeyTheme.colors.black + ) + + Text( + text = regionName ?: "강남구 역삼동", + style = PawKeyTheme.typography.head20B2, + color = PawKeyTheme.colors.green500 + ) + } + + Spacer(modifier = Modifier.height(12.dp)) + + if (regionName == preRegionName) { + Text( + text = "기존에 산책하던 지역은\n" + + "기존 지역과 같은 동네에요.", + style = PawKeyTheme.typography.body14M, + color = PawKeyTheme.colors.gray500, + modifier = Modifier + .padding(bottom = 12.dp) + ) + + Spacer(modifier = Modifier.height(12.dp)) + + PawkeyButton( + text = "지역 변경하기", + onClick = { + onClickButton() + }, + modifier = Modifier + .fillMaxWidth(), + enabled = false, + ) + } else { + Text( + text = "기존에 산책하던 지역은 ${preRegionName}이에요.\n선택한 위치로 산책 지역을 변경하시겠어요?", + style = PawKeyTheme.typography.body14M, + color = PawKeyTheme.colors.gray500, + modifier = Modifier + .padding(bottom = 12.dp) + ) + + Spacer(modifier = Modifier.height(12.dp)) + + PawkeyButton( + text = "지역 변경하기", + onClick = { + onClickButton() + }, + modifier = Modifier + .fillMaxWidth(), + enabled = true, + ) + } + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun RadiusTestPreview() { + PawKeyTheme { + Scaffold( + modifier = Modifier + .fillMaxSize() + .background(Color.Blue) + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .background(Color.Blue) + .padding(paddingValues) ) { + Box( + modifier = Modifier + .weight(1f), + ) + Column( - modifier = Modifier.fillMaxWidth() + modifier = Modifier + .clip(RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)) + .background( + color = PawKeyTheme.colors.black, + shape = RoundedCornerShape( + topStart = 16.dp, topEnd = 16.dp + ) + ) + .padding(horizontal = 16.dp, vertical = 24.dp) ) { Row( modifier = Modifier @@ -191,9 +325,9 @@ fun RegionalManagementScreen( style = PawKeyTheme.typography.head20B2, color = PawKeyTheme.colors.black ) - + Text( - text = selectedRegion ?: "강남구 역삼동", + text = "강남구 역삼동", style = PawKeyTheme.typography.head20B2, color = PawKeyTheme.colors.green500 ) @@ -201,51 +335,27 @@ fun RegionalManagementScreen( Spacer(modifier = Modifier.height(12.dp)) - if (selectedRegion == preRegionName) { - Text( - text = "기존에 산책하던 지역은\n" + - "기존 지역과 같은 동네에요.", - style = PawKeyTheme.typography.body14M, - color = PawKeyTheme.colors.gray500, - modifier = Modifier - .padding(bottom = 12.dp) - ) - - Spacer(modifier = Modifier.height(12.dp)) + Text( + text = "기존에 산책하던 지역은이에요.\n선택한 위치로 산책 지역을 변경하시겠어요?", + style = PawKeyTheme.typography.body14M, + color = PawKeyTheme.colors.gray500, + modifier = Modifier + .padding(bottom = 12.dp) + ) - PawkeyButton( - text = "지역 변경하기", - onClick = { - //onClickButton() - }, - modifier = Modifier - .fillMaxWidth(), - enabled = false, - ) - } else { - Text( - text = "기존에 산책하던 지역은 ${preRegionName}이에요.\n선택한 위치로 산책 지역을 변경하시겠어요?", - style = PawKeyTheme.typography.body14M, - color = PawKeyTheme.colors.gray500, - modifier = Modifier - .padding(bottom = 12.dp) - ) + Spacer(modifier = Modifier.height(12.dp)) - Spacer(modifier = Modifier.height(12.dp)) + PawkeyButton( + text = "지역 변경하기", + onClick = { - PawkeyButton( - text = "지역 변경하기", - onClick = { - onClickButton() - }, - modifier = Modifier - .fillMaxWidth(), - enabled = true, - ) - } + }, + modifier = Modifier + .fillMaxWidth(), + enabled = true, + ) } } } } } - diff --git a/app/src/main/java/com/paw/key/presentation/ui/region/component/regionalMapView.kt b/app/src/main/java/com/paw/key/presentation/ui/region/component/regionalMapView.kt deleted file mode 100644 index f64e0af8..00000000 --- a/app/src/main/java/com/paw/key/presentation/ui/region/component/regionalMapView.kt +++ /dev/null @@ -1,157 +0,0 @@ -package com.paw.key.presentation.ui.region.component - -import android.content.Context -import android.util.Log -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.lifecycle.DefaultLifecycleObserver -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleOwner -import com.kakao.vectormap.KakaoMap -import com.kakao.vectormap.KakaoMapReadyCallback -import com.kakao.vectormap.LatLng -import com.kakao.vectormap.MapLifeCycleCallback -import com.kakao.vectormap.MapView -import com.kakao.vectormap.camera.CameraUpdateFactory -import com.kakao.vectormap.label.Label -import com.kakao.vectormap.label.LabelOptions -import com.kakao.vectormap.label.LabelStyle -import com.kakao.vectormap.shape.MapPoints -import com.kakao.vectormap.shape.PolygonOptions -import com.kakao.vectormap.shape.PolygonStyle -import com.kakao.vectormap.shape.PolygonStyles -import com.kakao.vectormap.shape.PolygonStylesSet -import com.paw.key.R - -@Composable -fun regionalMapView( - lifeCycle: Lifecycle, - context: Context, - currentUserLocation: LatLng?, - polyPoints: List>, -) : MapView { - val mapView = remember { - MapView(context) - } - - var kakaoMapState by remember { - mutableStateOf(null) - } - - var currentLocation by remember { - mutableStateOf(currentUserLocation) - } - - var centerLabel by remember { - mutableStateOf(null) - } - - // ------------------------------------------------ - // - DisposableEffect(lifeCycle) { - val observer = object : DefaultLifecycleObserver { - override fun onCreate(owner: LifecycleOwner) { - mapView.start( - object : MapLifeCycleCallback() { - override fun onMapDestroy() { - } - - override fun onMapError(error: Exception) { - Log.e("MapView", "지도 오류 발생: $error") - } - }, - object : KakaoMapReadyCallback() { - override fun onMapReady(kakaoMap: KakaoMap) { - kakaoMapState = kakaoMap - - centerLabel = kakaoMap.labelManager?.layer?.addLabel( - // userLocation이 null일 경우 - LabelOptions.from("dotLabel", currentUserLocation ?: LatLng.from(37.497942, 127.027619)) - .setStyles( - LabelStyle.from( - R.drawable.user_poi - ).setAnchorPoint(0.5f, 0.5f) - ) - .setRank(5) - ) - - val initialCameraPosition = currentUserLocation ?: LatLng.from(37.497942, 127.027619) - - kakaoMap.moveCamera( - CameraUpdateFactory.newCenterPosition( - initialCameraPosition, 19 - ) - ) - - val fillColor = 0x5039BA28 - - val strokeColor = 0xFF39BA28.toInt() - - polyPoints.forEach { - kakaoMap.shapeManager?.layer?.addPolygon( - PolygonOptions.from() - .setMapPoints(MapPoints.fromLatLng(it)) - .setStylesSet( - PolygonStylesSet.from( - PolygonStyles.from( - PolygonStyle.from (//(int zoomLevel, int color, float strokeWidth, int strokeColor) - 13, - fillColor, - 10f, - strokeColor - ) - ) - ) - ) - )?.show() - } - } - - override fun getPosition(): LatLng { - //userLocation = LatLng.from(locationY, locationX) - return currentUserLocation ?: LatLng.from(37.497942, 127.027619) - } - } - ) - } - - override fun onResume(owner: LifecycleOwner) { - mapView.resume() - } - - override fun onPause(owner: LifecycleOwner) { - mapView.pause() - //fusedLocationClient.removeLocationUpdates(locationCallback) - /*sensorManager.unregisterListener(stepSensorEventListener) - fusedLocationClient.removeLocationUpdates(locationCallback) - initialSensorSteps = null - isWalking(false)*/ - } - } - - lifeCycle.addObserver(observer) - - onDispose { - lifeCycle.removeObserver(observer) - } - } - - LaunchedEffect(centerLabel) { - if (currentUserLocation != null && centerLabel != null) { - centerLabel?.moveTo(currentUserLocation) - kakaoMapState?.moveCamera( - CameraUpdateFactory.newCenterPosition( - currentUserLocation, 18 - ) - ) - } - } - - return mapView -} - diff --git a/app/src/main/java/com/paw/key/presentation/ui/region/state/RegionContract.kt b/app/src/main/java/com/paw/key/presentation/ui/region/state/RegionContract.kt index ef643f4d..d3032f8b 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/region/state/RegionContract.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/region/state/RegionContract.kt @@ -1,22 +1,28 @@ package com.paw.key.presentation.ui.region.state import androidx.compose.runtime.Immutable -import com.kakao.vectormap.LatLng +import com.naver.maps.geometry.LatLng import com.paw.key.core.util.UiState +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf -class RegionContract { - @Immutable - data class RegionState( - val uiState: UiState>> = UiState.Loading, - val preRegionName: String? = null, - val regionName: String? = null, - val selectedRegion: String? = null, - val centerLocation: LatLng? = null, - ) +@Immutable +data class RegionState( + val uiState: UiState>> = UiState.Loading, + val entireCoordinates: ImmutableList = persistentListOf(), + val preRegionName: String? = null, + val regionName: String? = null, + val selectedRegion: String? = null, + val centerLocation: LatLng? = null, + val drawType: DrawType = DrawType.SINGLE +) - sealed class RegionSideEffect { - data class ShowSnackBar(val message: String) : RegionSideEffect() - data object NavigateUp: RegionSideEffect() - data object NavigateNext: RegionSideEffect() - } +sealed class RegionSideEffect { + data class ShowSnackBar(val message: String) : RegionSideEffect() + data object NavigateUp: RegionSideEffect() + data object NavigateNext: RegionSideEffect() +} + +enum class DrawType { + SINGLE, MULTIPLE } \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/region/viewmodel/RegionViewModel.kt b/app/src/main/java/com/paw/key/presentation/ui/region/viewmodel/RegionViewModel.kt index 7f635194..e12a3dc3 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/region/viewmodel/RegionViewModel.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/region/viewmodel/RegionViewModel.kt @@ -1,71 +1,103 @@ package com.paw.key.presentation.ui.region.viewmodel import android.util.Log +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.kakao.vectormap.LatLng +import androidx.navigation.toRoute +import com.naver.maps.geometry.LatLng +import com.paw.key.core.util.PreferenceDataStore import com.paw.key.core.util.UiState import com.paw.key.core.util.handleError import com.paw.key.domain.repository.RegionRepository import com.paw.key.domain.repository.home.HomeRegionRepository -import com.paw.key.presentation.ui.region.state.RegionContract +import com.paw.key.presentation.ui.region.navigation.Regional +import com.paw.key.presentation.ui.region.state.DrawType +import com.paw.key.presentation.ui.region.state.RegionSideEffect +import com.paw.key.presentation.ui.region.state.RegionState import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class RegionViewModel @Inject constructor( + savedStateHandle: SavedStateHandle, private val regionRepository: RegionRepository, private val homeRepository: HomeRegionRepository ) : ViewModel() { - private val _state = MutableStateFlow(RegionContract.RegionState()) - val state : StateFlow - get() = _state.asStateFlow() + private val _state = MutableStateFlow(RegionState()) + val state: StateFlow = _state.asStateFlow() - private val _sideEffect = MutableSharedFlow() - val sideEffect : MutableSharedFlow + private val _sideEffect = MutableSharedFlow() + val sideEffect : MutableSharedFlow get() = _sideEffect + private val regionIdState = savedStateHandle.toRoute() + + private val userId : StateFlow = PreferenceDataStore.getUserId() + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5000), + initialValue = -1 + ) + + init { + viewModelScope.launch { + Log.e("RegionViewModel", "regionId: ${regionIdState.regionId}") + val validUserId = userId.filter { it != -1 }.first() + getRegionGeometry( + userId = validUserId, + regionId = regionIdState.regionId, + ) + } + } + fun getRegionGeometry(userId: Int, regionId: Int) = viewModelScope.launch { regionRepository.getRegionGeometry(userId, regionId) .onSuccess { data -> val coordinates = data.geometry.coordinates val flattenedLatLng = flattenCoordinatesToLatLng(coordinates) - Log.d("RegionViewModel", "flattenedLatLng size: ${flattenedLatLng}") - - _state.update { - it.copy( - uiState = UiState.Success(flattenedLatLng), - preRegionName = data.preRegionName, - regionName = data.regionName - ) + if (flattenedLatLng.isEmpty() || flattenedLatLng.first().isEmpty()) { + _state.update { + it.copy(uiState = UiState.Failure("좌표 데이터가 올바르지 않습니다")) + } + return@launch } - val firstPoint = coordinates - .firstOrNull() // 첫 번째 Polygon - ?.firstOrNull() // 첫 번째 Ring (외부 경계) - ?.firstOrNull() // 첫 번째 Point - - Log.d("RegionViewModel", "First point: $firstPoint") + val allPoints = flattenedLatLng.flatten().toPersistentList() - if (firstPoint != null) { - val latLng = LatLng.from(firstPoint.first, firstPoint.second) + if (flattenedLatLng.size == 1) { + // 폴리곤이 하나일 경우 _state.update { it.copy( - centerLocation = latLng, - selectedRegion = data.regionName + uiState = UiState.Success(flattenedLatLng), + entireCoordinates = allPoints, + drawType = DrawType.SINGLE, + preRegionName = data.preRegionName, + regionName = data.regionName ) } } else { + // 폴리곤이 여러 개일 경우 _state.update { it.copy( - uiState = UiState.Failure("좌표 데이터가 올바르지 않습니다") + uiState = UiState.Success(flattenedLatLng), + entireCoordinates = allPoints, + drawType = DrawType.MULTIPLE, + preRegionName = data.preRegionName, + regionName = data.regionName ) } } @@ -81,21 +113,23 @@ class RegionViewModel @Inject constructor( } } - fun patchRegion(userId: Int, regionId: Int) = viewModelScope.launch { - homeRepository.patchRegion(userId, regionId) - .onSuccess { data -> - Log.d("RegionViewModel", "API 응답 성공: $data") - _sideEffect.emit( - RegionContract.RegionSideEffect.ShowSnackBar("지역을 ${state.value.selectedRegion ?: "역삼동"}으로 변경했어요.") - ) - } - .onFailure { throwable -> - Log.e("RegionViewModel", "API 호출 실패", throwable) - val errorMessage = handleError(throwable) - _sideEffect.emit( - RegionContract.RegionSideEffect.ShowSnackBar(errorMessage) - ) - } + fun patchRegion() { + viewModelScope.launch { + homeRepository.patchRegion(userId.value, regionIdState.regionId) + .onSuccess { data -> + Log.d("RegionViewModel", "API 응답 성공: $data") + _sideEffect.emit( + RegionSideEffect.ShowSnackBar("지역을 ${state.value.regionName ?: "역삼동"}으로 변경했어요.") + ) + } + .onFailure { throwable -> + Log.e("RegionViewModel", "API 호출 실패", throwable) + val errorMessage = handleError(throwable) + _sideEffect.emit( + RegionSideEffect.ShowSnackBar(errorMessage) + ) + } + } } /*fun onChangeRegion() { @@ -107,12 +141,25 @@ class RegionViewModel @Inject constructor( }*/ } +/* +private fun flattenCoordinatesToLatLng( + coordinates: List>>> +): ImmutableList { + return coordinates.flatMap { polygon -> + val outerRing = polygon.firstOrNull().orEmpty() + outerRing.map { point -> + LatLng(point.first, point.second) + } + }.toImmutableList() +} +*/ + private fun flattenCoordinatesToLatLng( coordinates: List>>> -): List> { +): ImmutableList> { return coordinates.map { polygon -> // 각 Polygon polygon.firstOrNull()?.map { point -> - LatLng.from(point.first, point.second) - }.orEmpty() - } + LatLng(point.first, point.second) + }.orEmpty().toPersistentList() + }.toPersistentList() } \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/signup/component/LocationButton.kt b/app/src/main/java/com/paw/key/presentation/ui/signup/component/LocationButton.kt index 41dce16b..9a4d79f7 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/signup/component/LocationButton.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/signup/component/LocationButton.kt @@ -14,7 +14,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.paw.key.core.designsystem.theme.PawKeyTheme -import com.paw.key.core.util.noRippleClickable +import com.paw.key.core.extension.noRippleClickable @Preview(showBackground = true) @Composable diff --git a/app/src/main/java/com/paw/key/presentation/ui/signup/component/SignUpTextField.kt b/app/src/main/java/com/paw/key/presentation/ui/signup/component/SignUpTextField.kt index c4f23248..acb1ac65 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/signup/component/SignUpTextField.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/signup/component/SignUpTextField.kt @@ -16,11 +16,8 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.focus.onFocusChanged -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.unit.dp import com.paw.key.core.designsystem.theme.PawKeyTheme -import com.paw.key.core.util.noRippleClickable @Composable fun SignUpTextField( diff --git a/app/src/main/java/com/paw/key/presentation/ui/signup/component/SignUpUserSelectButton.kt b/app/src/main/java/com/paw/key/presentation/ui/signup/component/SignUpUserSelectButton.kt index e9b0a966..3b91fdf0 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/signup/component/SignUpUserSelectButton.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/signup/component/SignUpUserSelectButton.kt @@ -15,7 +15,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.paw.key.core.designsystem.theme.PawKeyTheme -import com.paw.key.core.util.noRippleClickable +import com.paw.key.core.extension.noRippleClickable @Preview(showBackground = true) @Composable diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index abe56acc..810d2f11 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -60,6 +60,11 @@ playServicesLocation = "21.3.0" # DataStore datastorePreferences = "1.1.7" +# Naver Maps Versions +naverMapCompose = "1.8.2" +naverMapLocation = "21.0.2" +naverMapSdk = "3.22.0" + [libraries] # Test accompanist-systemuicontroller = { module = "com.google.accompanist:accompanist-systemuicontroller", version.ref = "accompanistSystemuicontroller" } @@ -118,6 +123,11 @@ timber = { group = "com.jakewharton.timber", name = "timber", version.ref = "tim kakaoMaps = { group = "com.kakao.maps.open", name = "android", version.ref = "kakaoMaps" } v2-all = { module = "com.kakao.sdk:v2-all", version.ref = "v2All" } +# Naver Libraries +naver-map-compose = { group = "io.github.fornewid", name = "naver-map-compose", version.ref = "naverMapCompose" } +naver-map-location = { group = "io.github.fornewid", name = "naver-map-location", version.ref = "naverMapLocation" } +naver-map-sdk = { group = "com.naver.maps", name = "map-sdk", version.ref = "naverMapSdk" } + [plugins] android-application = { id = "com.android.application", version.ref = "agp" } jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } @@ -170,3 +180,8 @@ hilt = [ "hilt-navigation-compose" ] +naverMaps = [ + "naver-map-compose", + "naver-map-location", + "naver-map-sdk" +] diff --git a/settings.gradle.kts b/settings.gradle.kts index a8dfd20e..16dc05ec 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -19,6 +19,7 @@ dependencyResolutionManagement { maven("https://jitpack.io") maven("https://devrepo.kakao.com/nexus/content/groups/public/") maven("https://devrepo.kakao.com/nexus/repository/kakaomap-releases/") + maven("https://repository.map.naver.com/archive/maven") } }