From 243d1b01805aac51ba378b750ff9ab781f248c59 Mon Sep 17 00:00:00 2001 From: sonms Date: Wed, 9 Jul 2025 12:10:00 +0900 Subject: [PATCH 01/27] =?UTF-8?q?setting/#11:=20=EC=B9=B4=EC=B9=B4?= =?UTF-8?q?=EC=98=A4=EB=A7=B5=20=EA=B4=80=EB=A0=A8=20gradle,=20toml,=20per?= =?UTF-8?q?mission=20=EC=84=B8=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 13 +++++++++++++ app/src/main/AndroidManifest.xml | 10 ++++++++++ gradle/libs.versions.toml | 16 ++++++++++++++++ settings.gradle.kts | 3 +++ 4 files changed, 42 insertions(+) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 20e670ff..cc91936f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -26,6 +26,10 @@ android { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 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()) + + manifestPlaceholders["KAKAO_NATIVE_KEY"] = properties["kakao.native.key"].toString() } buildTypes { @@ -79,4 +83,13 @@ dependencies { implementation(libs.timber) implementation(libs.accompanist.systemuicontroller) + + implementation(libs.androidx.datastore.preferences) + + //카카오 + implementation(libs.kakaoMaps) + implementation(libs.v2.all) + + //실시간 위치 + implementation(libs.play.services.location) } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 10ff34a1..dc0efb69 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,6 +3,12 @@ xmlns:tools="http://schemas.android.com/tools"> + + + + + + + + \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a1cf2b32..d50a497c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -49,9 +49,20 @@ coil = "2.6.0" # Timber timber = "5.0.1" +# Kakao +kakaoMaps = "2.9.5" +v2All = "2.20.1" + +# ServiceLocation +playServicesLocation = "21.3.0" + +# DataStore +datastorePreferences = "1.1.7" + [libraries] # Test accompanist-systemuicontroller = { module = "com.google.accompanist:accompanist-systemuicontroller", version.ref = "accompanistSystemuicontroller" } +androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" } junit = { group = "junit", name = "junit", version.ref = "junit" } androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } @@ -81,6 +92,7 @@ androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", versi okhttp-bom = { group = "com.squareup.okhttp3", name = "okhttp-bom", version.ref = "okhttp" } okhttp = { group = "com.squareup.okhttp3", name = "okhttp" } okhttp-logging-interceptor = { group = "com.squareup.okhttp3", name = "logging-interceptor" } +play-services-location = { module = "com.google.android.gms:play-services-location", version.ref = "playServicesLocation" } retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" } retrofit-kotlin-serialization-converter = { group = "com.jakewharton.retrofit", name = "retrofit2-kotlinx-serialization-converter", version.ref = "retrofitKotlinSerializationConverter" } @@ -100,6 +112,10 @@ material = { group = "com.google.android.material", name = "material", version.r # Timber timber = { group = "com.jakewharton.timber", name = "timber", version.ref = "timber" } +# Kakao +kakaoMaps = { group = "com.kakao.maps.open", name = "android", version.ref = "kakaoMaps" } +v2-all = { module = "com.kakao.sdk:v2-all", version.ref = "v2All" } + [plugins] android-application = { id = "com.android.application", version.ref = "agp" } jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 6ed3e114..a8dfd20e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -16,6 +16,9 @@ dependencyResolutionManagement { repositories { google() mavenCentral() + maven("https://jitpack.io") + maven("https://devrepo.kakao.com/nexus/content/groups/public/") + maven("https://devrepo.kakao.com/nexus/repository/kakaomap-releases/") } } From 3527e6171da6f294124570c93517143fa37da8e2 Mon Sep 17 00:00:00 2001 From: sonms Date: Wed, 9 Jul 2025 12:11:36 +0900 Subject: [PATCH 02/27] =?UTF-8?q?feat/#11:=20=EC=82=B0=EC=B1=85=ED=95=98?= =?UTF-8?q?=EA=B8=B0=20=EB=A9=94=EC=9D=B8=20=ED=83=AD=EC=9D=98=20=EC=83=81?= =?UTF-8?q?=EC=9C=84=20=ED=83=AD=20=ED=91=9C=EC=8B=9C=EB=A5=BC=20=EC=9C=84?= =?UTF-8?q?=ED=95=9C=20contract,=20viewmodel=20=EA=B5=AC=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../entire/state/EntireCourseContract.kt | 36 +++++++++++++++++++ .../entire/viewmodel/EntireCourseViewModel.kt | 30 ++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 app/src/main/java/com/paw/key/presentation/ui/course/entire/state/EntireCourseContract.kt create mode 100644 app/src/main/java/com/paw/key/presentation/ui/course/entire/viewmodel/EntireCourseViewModel.kt diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/entire/state/EntireCourseContract.kt b/app/src/main/java/com/paw/key/presentation/ui/course/entire/state/EntireCourseContract.kt new file mode 100644 index 00000000..f51cf000 --- /dev/null +++ b/app/src/main/java/com/paw/key/presentation/ui/course/entire/state/EntireCourseContract.kt @@ -0,0 +1,36 @@ +package com.paw.key.presentation.ui.course.entire.state + +import androidx.annotation.StringRes +import androidx.compose.runtime.Immutable +import com.paw.key.R + +class EntireCourseContract { + @Immutable + data class EntireCourseState( + val pagerState : Int = 2, + val selectedTabIndex : Int = 0, + val courseTabs : List = listOf( + CourseTab.MapTab, + CourseTab.ListTab, + ), + val isEnabled : Boolean = false, + + val isLocationPermissionGranted: Boolean = false, + val isRecognitionPermissionGranted: Boolean = false, + val isLocationServiceEnabled: Boolean = false, + ) + + sealed class EntireCourseSideEffect { + data class ShowSnackBar(val message: String) : EntireCourseSideEffect() + data object NavigateUp: EntireCourseSideEffect() + data object NavigateNext: EntireCourseSideEffect() + } + + sealed class CourseTab ( + @StringRes val titleResId: Int + ) { + data object MapTab : CourseTab(R.string.course_tab_title_map) + + data object ListTab : CourseTab(R.string.course_tab_title_list) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/entire/viewmodel/EntireCourseViewModel.kt b/app/src/main/java/com/paw/key/presentation/ui/course/entire/viewmodel/EntireCourseViewModel.kt new file mode 100644 index 00000000..7e163d72 --- /dev/null +++ b/app/src/main/java/com/paw/key/presentation/ui/course/entire/viewmodel/EntireCourseViewModel.kt @@ -0,0 +1,30 @@ +package com.paw.key.presentation.ui.course.entire.viewmodel + +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import javax.inject.Inject +import com.paw.key.presentation.ui.course.entire.state.EntireCourseContract.EntireCourseState +import com.paw.key.presentation.ui.course.entire.state.EntireCourseContract.EntireCourseSideEffect +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update + +@HiltViewModel +class EntireCourseViewModel @Inject constructor( + +) : ViewModel() { + private val _state = MutableStateFlow(EntireCourseState()) + val state : StateFlow + get() = _state.asStateFlow() + + private val _sideEffect = MutableStateFlow(null) + val sideEffect : StateFlow + get() = _sideEffect.asStateFlow() + + fun updateState(reducer: EntireCourseState.() -> EntireCourseState) { + _state.update { + it.reducer() + } + } +} \ No newline at end of file From c9a5afac7291c561de057fb60a4f8d3142f4d1c5 Mon Sep 17 00:00:00 2001 From: sonms Date: Wed, 9 Jul 2025 12:13:07 +0900 Subject: [PATCH 03/27] =?UTF-8?q?feat/#11:=20=EC=82=B0=EC=B1=85=ED=95=98?= =?UTF-8?q?=EA=B8=B0=20=EC=A0=84=EC=B2=B4=EC=9D=98=20TabRow=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../entire/component/EntireCourseTabRow.kt | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 app/src/main/java/com/paw/key/presentation/ui/course/entire/component/EntireCourseTabRow.kt diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/entire/component/EntireCourseTabRow.kt b/app/src/main/java/com/paw/key/presentation/ui/course/entire/component/EntireCourseTabRow.kt new file mode 100644 index 00000000..fdf6b819 --- /dev/null +++ b/app/src/main/java/com/paw/key/presentation/ui/course/entire/component/EntireCourseTabRow.kt @@ -0,0 +1,68 @@ +package com.paw.key.presentation.ui.course.entire.component + +import androidx.compose.foundation.layout.height +import androidx.compose.material3.ScrollableTabRow +import androidx.compose.material3.Tab +import androidx.compose.material3.TabRowDefaults +import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +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.presentation.ui.course.entire.state.EntireCourseContract.CourseTab + +@Composable +fun EntireCourseTabRow( + selectedTabIndex: Int, + onTabSelected: (Int) -> Unit, + tabs : List, + modifier: Modifier = Modifier, +) { + ScrollableTabRow ( + selectedTabIndex = selectedTabIndex, + modifier = modifier, + edgePadding = 16.dp, + contentColor = PawKeyTheme.colors.gray950, + containerColor = PawKeyTheme.colors.gray0, + indicator = { tabPositions -> + TabRowDefaults.SecondaryIndicator( + modifier = modifier + .tabIndicatorOffset(tabPositions[selectedTabIndex]) + .height(6.dp), + color = PawKeyTheme.colors.gray950 + ) + }, + divider = {} + ) { + tabs.forEachIndexed { index, tab -> + Tab( + text = { + Text( + text = stringResource(id = tab.titleResId), + color = PawKeyTheme.colors.gray950, + //style = PawKeyTheme.typography.body2M15, + ) + }, + selected = index == selectedTabIndex, + onClick = { + onTabSelected(index) + }, + ) + } + } +} + +@Preview +@Composable +private fun EntireCourseTabRowPreview() { + PawKeyTheme { + EntireCourseTabRow( + selectedTabIndex = 0, + onTabSelected = {}, + tabs = listOf(CourseTab.MapTab, CourseTab.ListTab), + ) + } +} \ No newline at end of file From c3130df90a694085af780081ad7687c0a57c2901 Mon Sep 17 00:00:00 2001 From: sonms Date: Wed, 9 Jul 2025 12:14:25 +0900 Subject: [PATCH 04/27] =?UTF-8?q?feat/#11:=20=EC=82=B0=EC=B1=85=ED=95=98?= =?UTF-8?q?=EA=B8=B0=20=EB=A9=94=EC=9D=B8=20=ED=83=AD=20=EB=B7=B0=20?= =?UTF-8?q?=EA=B5=AC=EC=84=B1=20-=20=ED=8D=BC=EB=AF=B8=EC=85=98=20?= =?UTF-8?q?=EC=B2=B4=ED=81=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/course/entire/EntireCourseScreen.kt | 224 ++++++++++++++++++ 1 file changed, 224 insertions(+) create mode 100644 app/src/main/java/com/paw/key/presentation/ui/course/entire/EntireCourseScreen.kt diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/entire/EntireCourseScreen.kt b/app/src/main/java/com/paw/key/presentation/ui/course/entire/EntireCourseScreen.kt new file mode 100644 index 00000000..77c7c98c --- /dev/null +++ b/app/src/main/java/com/paw/key/presentation/ui/course/entire/EntireCourseScreen.kt @@ -0,0 +1,224 @@ +package com.paw.key.presentation.ui.course.entire + +import android.Manifest +import android.content.Context +import android.content.pm.PackageManager +import android.os.Build +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.annotation.RequiresApi +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview +import androidx.core.content.ContextCompat +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.paw.key.core.designsystem.theme.PawKeyTheme +import com.paw.key.presentation.ui.course.entire.component.EntireCourseTabRow +import com.paw.key.presentation.ui.course.entire.state.EntireCourseContract.CourseTab +import com.paw.key.presentation.ui.course.entire.tab.map.TapMapRoute +import com.paw.key.presentation.ui.course.entire.viewmodel.EntireCourseViewModel +import kotlinx.coroutines.launch + +@RequiresApi(Build.VERSION_CODES.Q) +@Composable +fun EntireCourseRoute( + paddingValues: PaddingValues, + navigateUp: () -> Unit, + navigateNext: () -> Unit, + snackBarHostState: SnackbarHostState, + setOnVisibleRecord: (Boolean) -> Unit, + modifier: Modifier = Modifier, + viewModel : EntireCourseViewModel = hiltViewModel(), +) { + val state by viewModel.state.collectAsStateWithLifecycle() + + val scope = rememberCoroutineScope() + val context = LocalContext.current + + val pagerState = rememberPagerState(pageCount = { 2 }) + + val requestPermissionLauncher = rememberLauncherForActivityResult( + ActivityResultContracts.RequestMultiplePermissions() + ) { permissions -> + val isGranted = checkPermissionResults(permissions) + + if (!isGranted) { + scope.launch { + snackBarHostState.showSnackbar("위치 권한이 필요합니다.") + } + } else { + viewModel.updateState { + copy( + isLocationPermissionGranted = true, + isRecognitionPermissionGranted = true, + isLocationServiceEnabled = true + ) + } + } + } + + LaunchedEffect(Unit) { + val isGranted = hasAllRequiredPermissions(context) + + if (isGranted && state.isLocationPermissionGranted && state.isRecognitionPermissionGranted) { + viewModel.updateState { + copy( + isLocationPermissionGranted = true, + isRecognitionPermissionGranted = true, + isLocationServiceEnabled = true + ) + } + } else { + requestPermissionLauncher.launch( + arrayOf( + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION, + Manifest.permission.ACTIVITY_RECOGNITION + ) + ) + } + } + + LaunchedEffect(state.selectedTabIndex) { + if (pagerState.currentPage != state.selectedTabIndex) { + pagerState.animateScrollToPage(state.selectedTabIndex) + } + } + + LaunchedEffect(pagerState.currentPage) { + snapshotFlow { pagerState.currentPage } + .collect { page -> + if (state.selectedTabIndex != page) { + viewModel.updateState { + copy(selectedTabIndex = page) + } + } + } + } + + EntireCourseScreen( + paddingValues = paddingValues, + navigateUp = navigateUp, + navigateNext = navigateNext, + snackBarHostState = snackBarHostState, + currentPage = state.selectedTabIndex, + onTabSelected = { + viewModel.updateState { + copy(selectedTabIndex = it) + } + }, + setOnVisibleRecord = { + viewModel.updateState { + copy( + isEnabled = !this.isEnabled + ) + } + setOnVisibleRecord(it) + }, + isGranted = state.isLocationPermissionGranted, + tabs = state.courseTabs, + modifier = modifier, + ) +} + +@RequiresApi(Build.VERSION_CODES.Q) +@Composable +fun EntireCourseScreen( + paddingValues: PaddingValues, + snackBarHostState: SnackbarHostState, + tabs : List, + isGranted : Boolean, + currentPage : Int, + navigateUp: () -> Unit, + navigateNext: () -> Unit, + setOnVisibleRecord : (Boolean) -> Unit, + onTabSelected : (Int) -> Unit, + modifier: Modifier = Modifier, +) { + Column ( + modifier = modifier + .padding(paddingValues) + .fillMaxSize() + ) { + EntireCourseTabRow( + selectedTabIndex = currentPage, + onTabSelected = { + onTabSelected(it) + }, + tabs = tabs, + modifier = Modifier, + ) + + when (currentPage) { + 0 -> { + TapMapRoute( + paddingValues = paddingValues, + navigateUp = {}, + navigateNext = { + navigateNext() + }, + isGranted = isGranted, + snackBarHostState = snackBarHostState, + ) + } + + 1 -> {} + } + } +} + +@RequiresApi(Build.VERSION_CODES.Q) +fun hasAllRequiredPermissions(context: Context): Boolean { + val fine = ContextCompat.checkSelfPermission( + context, Manifest.permission.ACCESS_FINE_LOCATION + ) == PackageManager.PERMISSION_GRANTED + + val coarse = ContextCompat.checkSelfPermission( + context, Manifest.permission.ACCESS_COARSE_LOCATION + ) == PackageManager.PERMISSION_GRANTED + + val activity = ContextCompat.checkSelfPermission( + context, Manifest.permission.ACTIVITY_RECOGNITION + ) == PackageManager.PERMISSION_GRANTED + + return fine || coarse || activity +} + +@RequiresApi(Build.VERSION_CODES.Q) +fun checkPermissionResults(permissions: Map): Boolean { + return permissions[Manifest.permission.ACCESS_FINE_LOCATION] == true || + permissions[Manifest.permission.ACCESS_COARSE_LOCATION] == true || + permissions[Manifest.permission.ACTIVITY_RECOGNITION] == true +} + +@RequiresApi(Build.VERSION_CODES.Q) +@Preview +@Composable +private fun EntireCourseScreenPreview() { + PawKeyTheme { + EntireCourseScreen( + paddingValues = PaddingValues(), + navigateUp = {}, + navigateNext = {}, + snackBarHostState = SnackbarHostState(), + tabs = listOf(CourseTab.MapTab, CourseTab.ListTab), + currentPage = 0, + onTabSelected = {}, + isGranted = true, + setOnVisibleRecord = {}, + modifier = Modifier, + ) + } +} From 9288cfccb29ad92a941595687fc75e1970b5d749 Mon Sep 17 00:00:00 2001 From: sonms Date: Wed, 9 Jul 2025 12:16:15 +0900 Subject: [PATCH 05/27] =?UTF-8?q?feat/#11:=20=EC=A7=80=EB=8F=84=20?= =?UTF-8?q?=ED=83=AD=20contract,=20viewmodel=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../entire/tab/map/state/TapMapContract.kt | 21 ++++++++++ .../tab/map/viewmodel/TapMapViewModel.kt | 39 +++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/state/TapMapContract.kt create mode 100644 app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/viewmodel/TapMapViewModel.kt 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 new file mode 100644 index 00000000..8f314513 --- /dev/null +++ b/app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/state/TapMapContract.kt @@ -0,0 +1,21 @@ +package com.paw.key.presentation.ui.course.entire.tab.map.state + +import androidx.compose.runtime.Immutable +import com.kakao.vectormap.LatLng +import com.paw.key.core.util.UiState + +class TapMapContract { + @Immutable + data class TapMapState( + val initialLocationState : UiState = UiState.Loading, + val currentLocation: LatLng? = null, + val isLocationTracking: Boolean = false, + val isTrackingEnabled: Boolean = false, + ) + + sealed class TapMapSideEffect { + data class ShowSnackBar(val message: String) : TapMapSideEffect() + data object NavigateUp: TapMapSideEffect() + data object NavigateNext: TapMapSideEffect() + } +} \ No newline at end of file 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 new file mode 100644 index 00000000..9b9504ba --- /dev/null +++ b/app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/viewmodel/TapMapViewModel.kt @@ -0,0 +1,39 @@ +package com.paw.key.presentation.ui.course.entire.tab.map.viewmodel + +import androidx.lifecycle.ViewModel +import com.kakao.vectormap.LatLng +import com.paw.key.core.util.UiState +import com.paw.key.presentation.ui.course.entire.tab.map.state.TapMapContract.TapMapState +import com.paw.key.presentation.ui.course.entire.tab.map.state.TapMapContract.TapMapSideEffect +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import javax.inject.Inject + +@HiltViewModel +class TapMapViewModel @Inject constructor( + +) : ViewModel() { + private val _state = MutableStateFlow(TapMapState()) + val state : StateFlow + get() = _state.asStateFlow() + + private val _sideEffect = MutableSharedFlow() + val sideEffect: MutableSharedFlow + get() = _sideEffect + + fun updateInitialLocationState(newState: UiState) { + _state.value = _state.value.copy( + initialLocationState = newState + ) + } + + fun updateState(reducer: TapMapState.() -> TapMapState) { + _state.update { + it.reducer() + } + } +} \ No newline at end of file From 094ea0d3ec6986b716d19c6dec4f2f2e8808c0a1 Mon Sep 17 00:00:00 2001 From: sonms Date: Wed, 9 Jul 2025 12:17:16 +0900 Subject: [PATCH 06/27] =?UTF-8?q?feat/#11:=20=EC=A7=80=EB=8F=84=20?= =?UTF-8?q?=ED=83=AD=20=EB=84=A4=EB=B9=84=EA=B2=8C=EC=9D=B4=EC=85=98=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tab/map/navigation/TapMapNavigation.kt | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/navigation/TapMapNavigation.kt diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/navigation/TapMapNavigation.kt b/app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/navigation/TapMapNavigation.kt new file mode 100644 index 00000000..91f796ce --- /dev/null +++ b/app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/navigation/TapMapNavigation.kt @@ -0,0 +1,39 @@ +package com.paw.key.presentation.ui.course.entire.tab.map.navigation + +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.material3.SnackbarHostState +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import androidx.navigation.compose.composable +import com.paw.key.core.navigation.Route +import com.paw.key.presentation.ui.course.walk.WalkCourseRoute +import kotlinx.serialization.Serializable + +fun NavController.navigateWalkCourse( + navOptions: NavOptions? +) { + navigate(WalkCourse, navOptions) +} + +@RequiresApi(Build.VERSION_CODES.Q) +fun NavGraphBuilder.walkCourseNavGraph( + paddingValues: PaddingValues, + navigateUp: () -> Unit, + navigateNext: () -> Unit, + snackBarHostState: SnackbarHostState, +) { + composable { + WalkCourseRoute( + paddingValues = paddingValues, + navigateUp = navigateUp, + navigateNext = navigateNext, + snackBarHostState = snackBarHostState, + ) + } +} + +@Serializable +data object WalkCourse : Route \ No newline at end of file From ee2b21f37c127f4350a3258677cddd77f909c6fd Mon Sep 17 00:00:00 2001 From: sonms Date: Wed, 9 Jul 2025 12:17:40 +0900 Subject: [PATCH 07/27] =?UTF-8?q?feat/#11:=20=EC=A7=80=EB=8F=84=20?= =?UTF-8?q?=ED=83=AD=EC=9A=A9=20=EC=A7=80=EB=8F=84=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?-=20=EC=8B=A4=EC=8B=9C=EA=B0=84=20=EC=9C=84=EC=B9=98=20?= =?UTF-8?q?=ED=99=95=EC=9D=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../entire/tab/map/component/tapMapView.kt | 144 ++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/component/tapMapView.kt 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 new file mode 100644 index 00000000..c205e589 --- /dev/null +++ b/app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/component/tapMapView.kt @@ -0,0 +1,144 @@ +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.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, + onDispose : () -> Unit, +) : MapView { + val mapView = remember { + MapView(context) + } + + var kakaoMapState by remember { + mutableStateOf(null) + } + + var centerLabel by remember { + mutableStateOf(null) + } + + // ------------------------------------------------ + // 트래킹 + /*var trackingManager 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 + //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) + onDispose() + /*sensorManager.unregisterListener(stepSensorEventListener) + isWalking(false)*/ + } + } + + LaunchedEffect(isTrackingEnabled, centerLabel) { + if (currentUserLocation != null && centerLabel != null) { + centerLabel?.moveTo(currentUserLocation) + kakaoMapState?.moveCamera( + CameraUpdateFactory.newCenterPosition( + currentUserLocation, 21 + ) + ) + } + } + + return mapView +} \ No newline at end of file From 4522f4664df0fba105a1b6b3e2d38b351458839e Mon Sep 17 00:00:00 2001 From: sonms Date: Wed, 9 Jul 2025 12:18:00 +0900 Subject: [PATCH 08/27] =?UTF-8?q?feat/#11:=20=EC=A7=80=EB=8F=84=20?= =?UTF-8?q?=ED=83=AD=20=EC=A0=84=EC=B2=B4=20=EB=B7=B0=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/course/entire/tab/map/TapMapScreen.kt | 266 ++++++++++++++++++ 1 file changed, 266 insertions(+) create mode 100644 app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/TapMapScreen.kt 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 new file mode 100644 index 00000000..448261da --- /dev/null +++ b/app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/TapMapScreen.kt @@ -0,0 +1,266 @@ +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.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.LocationOn +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.LaunchedEffect +import androidx.compose.runtime.getValue +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.platform.LocalContext +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.core.designsystem.component.LoadingScreen +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.presentation.ui.course.entire.tab.map.viewmodel.TapMapViewModel +import com.paw.key.presentation.ui.course.walk.getCurrentLocation +import timber.log.Timber + +@Composable +fun TapMapRoute( + paddingValues: PaddingValues, + navigateUp: () -> Unit, + navigateNext: () -> Unit, + isGranted: Boolean, + snackBarHostState: SnackbarHostState, + modifier: Modifier = Modifier, + viewModel: TapMapViewModel = hiltViewModel(), +) { + val state by viewModel.state.collectAsStateWithLifecycle() + val lifecycleOwner = LocalLifecycleOwner.current + val context = LocalContext.current + + val fusedLocationClient = remember { + LocationServices.getFusedLocationProviderClient(context) + } + + val locationCallback = remember(viewModel) { + object : LocationCallback() { + override fun onLocationResult(locationResult: LocationResult) { + locationResult.lastLocation?.let { location -> + val newLocation = LatLng.from(location.latitude, location.longitude) + //viewModel.updateCurrentLocation(newLocation) // ViewModel의 상태 업데이트 + viewModel.updateState { + copy( + currentLocation = newLocation + ) + } + + Log.d("TapMapRoute", "Updated location: ${newLocation.latitude}, ${newLocation.longitude}, accuracy: ${location.accuracy}") + } + } + } + } + + LaunchedEffect(isGranted) { + if (isGranted) { + Log.e("TapMapRoute", "isGranted: $isGranted") + val currentLocation = getCurrentLocation( + context, + fusedLocationClient, + ) + + Log.e("TapMapRoute", "currentLocation: $currentLocation") + + viewModel.updateState { + copy( + initialLocationState = UiState.Success(currentLocation), + currentLocation = currentLocation + ) + } + } + } + + LaunchedEffect(isGranted) { + if (isGranted) { + val locationRequest = LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 1000) // 1초마다, 높은 정확도 + .setWaitForAccurateLocation(true) + .build() + + try { + fusedLocationClient.requestLocationUpdates( + locationRequest, + locationCallback, + Looper.getMainLooper() + ) + Log.d("TapMapRoute", "Location updates requested.") + } catch (e: SecurityException) { + snackBarHostState.showSnackbar("위치 권한이 필요합니다.") + + viewModel.updateState { + copy( + isTrackingEnabled = false + ) + } + } + } else { + fusedLocationClient.removeLocationUpdates(locationCallback) + } + } + + when (state.initialLocationState) { + is UiState.Loading -> { + LoadingScreen() + } + + is UiState.Success -> { + val mapView = tapMapView( + lifeCycle = lifecycleOwner.lifecycle, + context = context, + currentUserLocation = state.currentLocation, + isTrackingEnabled = state.isTrackingEnabled, + onDispose = { + fusedLocationClient.removeLocationUpdates(locationCallback) + } + ) + + TapMapScreen( + paddingValues = paddingValues, + navigateUp = navigateUp, + navigateNext = navigateNext, + snackBarHostState = snackBarHostState, + mapView = mapView, + onClickTracking = { + viewModel.updateState { + copy( + isTrackingEnabled = !this.isTrackingEnabled + ) + } + }, + modifier = modifier, + ) + } + + UiState.Empty -> {} + is UiState.Failure -> {} + } +} + +@Composable +fun TapMapScreen( + paddingValues: PaddingValues, + navigateUp: () -> Unit, + navigateNext: () -> Unit, + snackBarHostState: SnackbarHostState, + onClickTracking: () -> Unit, + mapView: MapView, + modifier: Modifier = Modifier, +) { + Scaffold( + modifier = modifier + .padding(paddingValues), + snackbarHost = { + SnackbarHost( + hostState = snackBarHostState, + ) + } + ) { pv -> + Box( + modifier = Modifier + .padding(pv) + ) { + AndroidView( + factory = { mapView }, + modifier = Modifier + .align(Alignment.Center) + ) + + Text( + text = "강남구 역삼동", + modifier = Modifier + .align(Alignment.TopStart) + .padding(top = 18.dp, start = 18.dp) + .clip(RoundedCornerShape(36.dp)) + .background(Color.White) + .border( + width = 1.dp, + color = Color(0xFF00C853), + shape = RoundedCornerShape(36.dp) + ) + .padding(horizontal = 16.dp, vertical = 8.dp), + color = Color(0xFF00C853), + fontSize = 16.sp, + textAlign = TextAlign.Center, + ) + + Row( + modifier = Modifier + .fillMaxWidth() + .align(Alignment.BottomCenter) + .padding(bottom = 100.dp) + .navigationBarsPadding(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Box( + modifier = Modifier + .padding(horizontal = 10.dp) + .noRippleClickable { + navigateNext() + } + ) { + Text( + text = "산책 기록 시작하기", + color = Color.White, + fontSize = 16.sp, + modifier = Modifier + .clip(RoundedCornerShape(12.dp)) + .background(Color(0xFF00C73C)) + .padding(vertical = 16.dp, horizontal = 20.dp), + textAlign = TextAlign.Center + ) + } + + FloatingActionButton( + onClick = onClickTracking, + shape = CircleShape, + containerColor = Color.White + ) { + Icon( + imageVector = Icons.Default.LocationOn, + contentDescription = "내 위치", + tint = Color.Black + ) + } + } + } + } +} \ No newline at end of file From e2260fdc21142339f0710cbd9fef6f6798266269 Mon Sep 17 00:00:00 2001 From: sonms Date: Wed, 9 Jul 2025 12:18:51 +0900 Subject: [PATCH 09/27] =?UTF-8?q?feat/#11:=20=EC=A7=80=EB=8F=84=20?= =?UTF-8?q?=ED=83=AD=EC=97=90=EC=84=9C=20=EC=82=B0=EC=B1=85=ED=95=98?= =?UTF-8?q?=EA=B8=B0=20contract,=20viewmodel=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../course/walk/state/WalkCourseContract.kt | 53 ++++++ .../walk/viewmodel/WalkCourseViewModel.kt | 170 ++++++++++++++++++ 2 files changed, 223 insertions(+) create mode 100644 app/src/main/java/com/paw/key/presentation/ui/course/walk/state/WalkCourseContract.kt create mode 100644 app/src/main/java/com/paw/key/presentation/ui/course/walk/viewmodel/WalkCourseViewModel.kt 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 new file mode 100644 index 00000000..80b3a7c4 --- /dev/null +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walk/state/WalkCourseContract.kt @@ -0,0 +1,53 @@ +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 + +class WalkCourseContract { + @Immutable + data class WalkCourseState( + val uiState: UiState> = UiState.Loading, + val poiPoints: PersistentList = persistentListOf(), + + // 현재 걸음 수 + 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 + ) + + sealed class WalkCourseSideEffect { + data class ShowSnackBar(val message: String) : WalkCourseSideEffect() + data object NavigateUp: WalkCourseSideEffect() + data object NavigateNext: WalkCourseSideEffect() + } + + sealed class WalkCourseRecord ( + @StringRes val titleResId: Int + ) { + data object DistanceRecord : WalkCourseRecord(R.string.course_record_distance) + + data object TimeRecord : WalkCourseRecord(R.string.course_record_time) + + data object StepsRecord : WalkCourseRecord(R.string.course_record_step) + } +} \ No newline at end of file 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 new file mode 100644 index 00000000..100f5857 --- /dev/null +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walk/viewmodel/WalkCourseViewModel.kt @@ -0,0 +1,170 @@ +package com.paw.key.presentation.ui.course.walk.viewmodel + +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.domain.repository.BitmapRepository +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 kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject + +@HiltViewModel +class WalkCourseViewModel @Inject constructor( + private val bitmapRepository: BitmapRepository +) : ViewModel() { + private val _state = MutableStateFlow(WalkCourseState()) + val state : StateFlow + get() = _state.asStateFlow() + + private val _sideEffect = MutableSharedFlow() + val sideEffect: MutableSharedFlow + get() = _sideEffect + + private val _totalTime = MutableStateFlow(0L) + val totalTime: StateFlow = _totalTime.asStateFlow() + + fun incrementTotalTime() { + _totalTime.update { + it + 1000L + } + } + + fun addInitLocation(location: LatLng) { + val currentList = state.value.poiPoints.toMutableList() + currentList.add(location) + _state.value = _state.value.copy( + poiPoints = currentList.toPersistentList() + ) + } + + // Todo : = updateState 로 일관되게 정리하기 + fun updateLocationAndCalculateDistance(newLocation: LatLng, accuracy: Float) { + // GPS 정확도가 너무 낮은 경우 (예: 10m 이상) 무시 + 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 + } + } + + val newTotalDistance = currentUiState.totalDistance + distanceIncrement + + val updatedPoiPoints: PersistentList = if (distanceIncrement > 0) { + currentUiState.poiPoints.add(newLocation) + } else { + currentUiState.poiPoints + } + + 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 + ) + } + } + } + + fun updateState(reducer: WalkCourseState.() -> WalkCourseState) { + Log.e("updateState", "updateState called") + _state.update { + it.reducer() + } + } + + fun mapCaptureCompleted() { + updateState { + copy(shouldCaptureMap = false) + } + } + + fun onMapCaptured(bitmap: Bitmap?) { + if (bitmap == null) { + Log.e("WalkCourseViewModel", "Captured bitmap is null, cannot save.") + return + } + + viewModelScope.launch { + try { + bitmapRepository.saveBitmap(bitmap) + _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() // 캡처 시도 후, 성공/실패 여부와 관계없이 플래그 리셋 + } + } + } +} \ No newline at end of file From a8e60bf5e1d3df80e2aeac35f33309eba557ed4e Mon Sep 17 00:00:00 2001 From: sonms Date: Wed, 9 Jul 2025 12:19:38 +0900 Subject: [PATCH 10/27] =?UTF-8?q?feat/#11:=20=EC=82=B0=EC=B1=85=ED=95=98?= =?UTF-8?q?=EA=B8=B0=20record=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20-=20=EA=B1=B0=EB=A6=AC,=20=EC=8B=9C?= =?UTF-8?q?=EA=B0=84,=20=EA=B1=B8=EC=9D=8C=EC=88=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../course/walk/component/WalkRecordItem.kt | 57 +++++++++++++++ .../ui/course/walk/component/WalkRecordRow.kt | 69 +++++++++++++++++++ 2 files changed, 126 insertions(+) create mode 100644 app/src/main/java/com/paw/key/presentation/ui/course/walk/component/WalkRecordItem.kt create mode 100644 app/src/main/java/com/paw/key/presentation/ui/course/walk/component/WalkRecordRow.kt diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walk/component/WalkRecordItem.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walk/component/WalkRecordItem.kt new file mode 100644 index 00000000..b53f0580 --- /dev/null +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walk/component/WalkRecordItem.kt @@ -0,0 +1,57 @@ +package com.paw.key.presentation.ui.course.walk.component + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.paw.key.R +import com.paw.key.core.designsystem.theme.PawKeyTheme + +@Composable +fun WalkRecordItem( + recordTitle : Int, + recordContent : String, + modifier: Modifier = Modifier, +) { + Column ( + modifier = modifier + .padding(vertical = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = stringResource(recordTitle), + fontSize = 14.sp, + color = Color.Black + ) + + Spacer(modifier = Modifier.height(4.dp)) + + Text( + text = recordContent, + fontSize = 20.sp, + color = Color(0xFF00C853) + ) + } +} + +@Preview +@Composable +private fun WalkRecordItemPreview() { + PawKeyTheme { + WalkRecordItem( + recordTitle = R.string.course_record_distance, + recordContent = "10km" + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walk/component/WalkRecordRow.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walk/component/WalkRecordRow.kt new file mode 100644 index 00000000..ab8c9143 --- /dev/null +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walk/component/WalkRecordRow.kt @@ -0,0 +1,69 @@ +package com.paw.key.presentation.ui.course.walk.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +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 + +@Composable +fun WalkRecordRow( + totalDistance: String, + totalTime: String, + currentSteps: Int, + modifier: Modifier = Modifier, +) { + Row ( + modifier = modifier + .padding(top = 16.dp, start = 20.dp, end = 20.dp) + .fillMaxWidth() + .background(Color.White, shape = RoundedCornerShape(8.dp)) + .border( + width = 1.dp, + color = Color(0xFF00C853), + shape = RoundedCornerShape(8.dp) + ) + .padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically + ){ + val recordItems = listOf( + DistanceRecord, + TimeRecord, + StepsRecord, + ) + + recordItems.forEach { record -> + when (record) { + DistanceRecord -> WalkRecordItem( + recordTitle = record.titleResId, + recordContent = totalDistance, + modifier = Modifier + .weight(1f), + ) + + TimeRecord -> WalkRecordItem( + recordTitle = record.titleResId, + recordContent = totalTime, + modifier = Modifier + .weight(1f), + ) + + StepsRecord -> WalkRecordItem( + recordTitle = record.titleResId, + recordContent = currentSteps.toString(), + modifier = Modifier + .weight(1f), + ) + } + } + } +} \ No newline at end of file From 0fc283c204ec9807c727516c3142e936db612cf1 Mon Sep 17 00:00:00 2001 From: sonms Date: Wed, 9 Jul 2025 12:20:26 +0900 Subject: [PATCH 11/27] =?UTF-8?q?feat/#11:=20=EC=82=B0=EC=B1=85=ED=95=98?= =?UTF-8?q?=EA=B8=B0=EC=9A=A9=20=EC=A7=80=EB=8F=84=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/course/walk/component/courseMapView.kt | 227 ++++++++++++++++++ 1 file changed, 227 insertions(+) create mode 100644 app/src/main/java/com/paw/key/presentation/ui/course/walk/component/courseMapView.kt 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 new file mode 100644 index 00000000..9f09bd77 --- /dev/null +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walk/component/courseMapView.kt @@ -0,0 +1,227 @@ +package com.paw.key.presentation.ui.course.walk.component + +import android.Manifest +import android.content.Context +import android.content.pm.PackageManager +import android.hardware.SensorManager +import android.os.Looper +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.Modifier +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.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.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.kakao.vectormap.shape.DotPoints +import com.kakao.vectormap.shape.PolygonOptions +import com.kakao.vectormap.shape.PolygonStyles +import com.kakao.vectormap.shape.PolygonStylesSet +import com.paw.key.R + +@Composable +fun courseMapView( + lifeCycle : Lifecycle, + context : Context, + currentUserLocation : LatLng?, + isTrackingEnabled : Boolean, + isPauseTracking : Boolean, + isStopTracking : Boolean, + poiPoints : List, + onLabelClick : (LatLng, String) -> Unit, + updateLocationAndCalculateDistance : (LatLng, Float) -> Unit, +) : MapView { + val mapView = remember { + MapView(context) + } + + var kakaoMapState by remember { + mutableStateOf(null) + } + + var currentLocation by remember { + mutableStateOf(currentUserLocation) + } + + val stableCallback = rememberUpdatedState(onLabelClick) + + var centerLabel by remember { + mutableStateOf(null) + } + + // ------------------------------------------------ + // 트래킹 + + var dimScreenLayer by remember { + mutableStateOf(null) + } + + var currentDrawnRouteLine by remember { + mutableStateOf(null) + } + + val drawRouteOnMap: (KakaoMap, List) -> Unit = { kakaoMap, pointsToDraw -> + if (pointsToDraw.isNotEmpty()) { + currentDrawnRouteLine?.remove() + currentDrawnRouteLine = null + + val routeLineStyle = RouteLineStyle.from( + 12f, + ContextCompat.getColor(context, R.color.teal_200) + ) + + 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() + + /*kakaoMap.moveCamera( + CameraUpdateFactory.fitMapPoints( + pointsToDraw.toTypedArray(), 100 + ) + )*/ + } + } + + 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 + + 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() + } + + 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) + } + } + + LaunchedEffect(isTrackingEnabled, centerLabel) { + if (currentUserLocation != null && centerLabel != null) { + centerLabel?.moveTo(currentUserLocation) + kakaoMapState?.moveCamera( + CameraUpdateFactory.newCenterPosition( + currentUserLocation, 18 + ) + ) + } + } + + LaunchedEffect(isPauseTracking) { + if (!isPauseTracking) { + mapView.isClickable = false + dimScreenLayer?.setColor(Color.LightGray.copy(alpha = 0.5f).toArgb()) + dimScreenLayer?.setVisible(true) + } else { + mapView.isClickable = true + dimScreenLayer?.setVisible(false) + } + } + + return mapView +} + From 8e9dd4e7cf199a906ba69f139f3f6f6518f9b3ae Mon Sep 17 00:00:00 2001 From: sonms Date: Wed, 9 Jul 2025 12:20:49 +0900 Subject: [PATCH 12/27] =?UTF-8?q?feat/#11:=20=EC=82=B0=EC=B1=85=ED=95=98?= =?UTF-8?q?=EA=B8=B0=20=EB=B7=B0=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/course/walk/WalkCourseScreen.kt | 746 ++++++++++++++++++ 1 file changed, 746 insertions(+) create mode 100644 app/src/main/java/com/paw/key/presentation/ui/course/walk/WalkCourseScreen.kt 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 new file mode 100644 index 00000000..7c33e523 --- /dev/null +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walk/WalkCourseScreen.kt @@ -0,0 +1,746 @@ +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.Looper +import android.util.Log +import androidx.annotation.RequiresApi +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +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.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.LocationOn +import androidx.compose.material3.Button +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.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +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.platform.LocalContext +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.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.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.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 + + +@RequiresApi(Build.VERSION_CODES.Q) +@Composable +fun WalkCourseRoute( + paddingValues: PaddingValues, + navigateUp: () -> Unit, + navigateNext: () -> Unit, + snackBarHostState: SnackbarHostState, + modifier: Modifier = Modifier, + viewModel: WalkCourseViewModel = hiltViewModel(), +) { + val state by viewModel.state.collectAsStateWithLifecycle() + val scope = rememberCoroutineScope() + val lifecycleOwner = LocalLifecycleOwner.current + val context = LocalContext.current + + val totalTime by viewModel.totalTime.collectAsStateWithLifecycle() + + val formattedTotalTime by remember(totalTime) { + derivedStateOf { + formatTime(totalTime) + } + } + + val formatDistance by remember(state.totalDistance) { + derivedStateOf { + formatDistance(state.totalDistance) + } + } + + // 0~9 = 0, 10~19 = 1 을 감지 + val distanceInTens by remember(state.totalDistance) { // ViewModel의 totalDistance를 참조 + derivedStateOf { + (state.totalDistance / 10).toInt() // Float을 Int로 변환 + } + } + + // 이전 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) + } + } + + 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}") + } + } + } + } + } + + LaunchedEffect(Unit) { + val currentLocation = getCurrentLocation( + context, + fusedLocationClient, + ) + + viewModel.updateState { + copy( + isRecording = true, + currentLocation = currentLocation, + initialLocationState = UiState.Success(currentLocation) + ) + } + + 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 + ) + + WalkCourseSideEffect.NavigateNext -> navigateNext() + WalkCourseSideEffect.NavigateUp -> navigateUp() + } + } + } + + 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) + } + } + } else { + fusedLocationClient.removeLocationUpdates(locationCallback) + Log.d("WalkCourseRoute", "Location updates removed.") + } + } + + /*LaunchedEffect(distanceInTens) { + // 거리가 10m씩 변경되었을 경우 + if (distanceInTens > 0 && distanceInTens > lastRecordedDistanceInTens) { // 0m 제외, 새로운 단위일 때만 + println("새로운 10m 단위 도달! 현재 거리: ${state.totalDistance}m") + lastRecordedDistanceInTens = distanceInTens + } + + state.currentLocation?.let { viewModel.addLocation(it) } + + viewModel.updateState { + copy( + currentLocation = state.currentLocation + ) + } + + 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) + } + } + } + + when (state.initialLocationState) { + is UiState.Empty -> Unit + is UiState.Failure -> Unit + + is UiState.Loading -> { + LoadingScreen() + } + + is UiState.Success -> { + val mapView = courseMapView( + lifeCycle = lifecycleOwner.lifecycle, + context = context, + onLabelClick = { _, _ -> }, + currentUserLocation = state.currentLocation, + poiPoints = state.poiPoints, + isTrackingEnabled = state.isTrackingEnabled, + isPauseTracking = state.isRecording, // true = 잠시 중단, false = 시작 + isStopTracking = state.isLocationTracking, // true = 진짜 중단 + updateLocationAndCalculateDistance = { newLatLng, accuracy -> + // 정확한 거리를 계산하여 거리를 기록하는 함수 + viewModel.updateLocationAndCalculateDistance(newLatLng,accuracy) + }, + ) + + LaunchedEffect(Unit) { + state.currentLocation?.let { + viewModel.addInitLocation( + location = it + ) + } + } + + LaunchedEffect(state.shouldCaptureMap, state.poiPoints) { + if (state.shouldCaptureMap) { + delay(500L) + + val glSurfaceView = mapView.surfaceView as? GLSurfaceView + if (glSurfaceView != null) { + withContext(Dispatchers.IO) { + captureMapToBitmap(glSurfaceView) { capturedBitmap -> + capturedBitmap?.let { + viewModel.onMapCaptured(it) // 캡처된 비트맵을 ViewModel로 전달 + Log.d("WalkCourseRoute", "맵 캡처 성공! (triggered by shouldCaptureMap)") + } ?: run { + Log.e("WalkCourseRoute", "맵 캡처 실패: 비트맵이 null입니다.") + viewModel.mapCaptureCompleted() + } + } + } + } else { + Log.e("WalkCourseRoute", "GLSurfaceView not available for capture.") + viewModel.mapCaptureCompleted() + } + } + } + + WalkCourseScreen( + paddingValues = paddingValues, + navigateUp = navigateUp, + navigateNext = navigateNext, + scope = scope, + snackBarHostState = snackBarHostState, + mapView = mapView, + totalDistance = formatDistance, + currentSteps = state.steps, + totalTime = formattedTotalTime, + isTracking = state.isRecording, // true = 잠시 중단, false = dim + onClickTracking = { + viewModel.updateState { + copy( + isTrackingEnabled = !this.isTrackingEnabled + ) + } + }, + onPauseTracking = { + viewModel.updateState { + copy( + isRecording = !this.isRecording, + shouldCaptureMap = true + ) + } + }, + onStartTracking = { + viewModel.updateState { + copy( + isRecording = !this.isRecording, + ) + } + }, + onStopTracking = { + viewModel.updateState { + copy( + isLocationTracking = !this.isLocationTracking + ) + } + }, + onCaptured = { bitmap -> + //viewModel.onMapCaptured(bitmap) + }, + modifier = modifier, + ) + } + } +} + +@Composable +fun WalkCourseScreen( + paddingValues: PaddingValues, + navigateUp: () -> Unit, + navigateNext: () -> Unit, + scope: CoroutineScope, + snackBarHostState: SnackbarHostState, + totalDistance: String, + currentSteps: Long, + totalTime: String, + isTracking: Boolean, // 버튼 상태 + onClickTracking: () -> Unit, + onStartTracking: () -> Unit, // 계속하기 + onPauseTracking: () -> Unit, // 잠시 중단 + onStopTracking: () -> Unit, // 종료하기 + onCaptured: (Bitmap?) -> Unit, + mapView: MapView, + modifier: Modifier = Modifier, +) { + Scaffold( + modifier = modifier + .padding(paddingValues), + snackbarHost = { + SnackbarHost( + hostState = snackBarHostState, + ) + } + ) { pv -> + Box( + modifier = Modifier + .padding(pv) + ) { + AndroidView( + factory = { mapView }, + modifier = Modifier + .align(Alignment.Center) + ) + + // Todo : 공통컴포넌트용 + Column ( + modifier = modifier + .fillMaxSize(), + verticalArrangement = Arrangement.SpaceBetween, + horizontalAlignment = Alignment.CenterHorizontally + ) { + WalkRecordRow( + totalDistance = totalDistance, + totalTime = totalTime, + currentSteps = currentSteps.toInt(), + modifier = Modifier + .fillMaxWidth() + .padding(top = 16.dp) + ) + + if (isTracking) { + Spacer(modifier = Modifier.weight(1f)) + } else { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = "산책이 중단되었어요!", + fontSize = 20.sp, + textAlign = TextAlign.Center, + color = Color.White, + modifier = Modifier.fillMaxWidth() + ) + + Text( + text = "산책을 정말 종료?", + fontSize = 12.sp, + textAlign = TextAlign.Center, + color = Color.White, + modifier = Modifier.fillMaxWidth() + ) + } + } + + // Todo : 나중에 버튼 들어갈자리 + Column ( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 16.dp, start = 16.dp, end = 16.dp) + .navigationBarsPadding(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Row ( + modifier = Modifier + .fillMaxWidth() + ) { + Spacer(modifier = Modifier.weight(1f)) + + FloatingActionButton( + shape = CircleShape, + onClick = onClickTracking, + containerColor = Color.White + ) { + Icon( + imageVector = Icons.Default.LocationOn, + contentDescription = "내 위치",//stringResource(id = R.string.lo) + tint = Color.Black + ) + } + } + + if (isTracking) { + Button( + onClick = { + onPauseTracking() + + scope.launch { + val glSurfaceView = mapView.surfaceView as? GLSurfaceView + if (glSurfaceView != null) { + withContext(Dispatchers.IO) { + captureMapToBitmap(glSurfaceView) { capturedBitmap -> + capturedBitmap?.let { + onCaptured(it) + Log.d("WalkCourseScreen", "맵 캡처 성공!") + } ?: run { + Log.e("WalkCourseScreen", "맵 캡처 실패: 비트맵이 null입니다.") + } + } + } + } + } + } + ) { + Text(text = "산책 기록 종료") + } + } else { + Row ( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ){ + Text( + text = "계속 산책하기", + modifier = Modifier + .clip(RoundedCornerShape(8.dp)) + .background( + Color.White, + RoundedCornerShape(8.dp) + ) + .noRippleClickable { + onStartTracking() + } + .border( + width = 1.dp, + color = Color(0xFF00C853), + shape = RoundedCornerShape(8.dp) + ) + .padding(horizontal = 24.dp, vertical = 16.dp) + ) + + Spacer(modifier = Modifier.weight(1f)) + + Text( + text = "산책 종료하기", + modifier = Modifier + .clip(RoundedCornerShape(8.dp)) + .background( + Color(0xFF00C853), + RoundedCornerShape(8.dp) + ) + .noRippleClickable { + navigateNext() + onStopTracking() + } + .padding(horizontal = 24.dp, vertical = 16.dp), + color = Color.White + ) + } + } + } + } + } + } +} + +fun captureMapToBitmap(surfaceView: GLSurfaceView, onCaptured: (Bitmap?) -> Unit) { + surfaceView.queueEvent { + val egl = EGLContext.getEGL() as EGL10 + val gl = egl.eglGetCurrentContext().gl as GL10 + val bitmap = createBitmapFromGLSurface(0, 0, surfaceView.width, surfaceView.height, gl) + + onCaptured(bitmap) + } +} + +private fun createBitmapFromGLSurface(x: Int, y: Int, w: Int, h: Int, gl: GL10): Bitmap? { + val bitmapBuffer = IntArray(w * h) + val bitmapSource = IntArray(w * h) + val intBuffer = IntBuffer.wrap(bitmapBuffer) + intBuffer.position(0) + + try { + gl.glReadPixels(x, y, w, h, GL10.GL_RGBA, GL10.GL_UNSIGNED_BYTE, intBuffer) + var offset1: Int + var offset2: Int + + for (i in 0 until h) { + offset1 = i * w + offset2 = (h - i - 1) * w + + for (j in 0 until w) { + val texturePixel = bitmapBuffer[offset1 + j] + val blue = (texturePixel shr 16) and 0xff + val red = (texturePixel shl 16) and 0x00ff0000 + val pixel = (texturePixel and 0xff00ff00.toInt()) or red or blue + bitmapSource[offset2 + j] = pixel + } + } + } catch (e: GLException) { + return null + } catch (e: OutOfMemoryError) { + return null + } + + // 전체 비트맵 생성 + val fullBitmap = Bitmap.createBitmap(bitmapSource, w, h, Bitmap.Config.ARGB_8888) + + // 화면의 aspectRatio 계산 (Modifier.aspectRatio(340f / 150f) 와 동일하게) + val targetAspectRatio = 16f / 11f // 사용하고자 하는 화면의 aspectRatio를 여기에 설정합니다. + + var cropWidth: Int + var cropHeight: Int + + val currentAspectRatio = w.toFloat() / h.toFloat() + + if (currentAspectRatio > targetAspectRatio) { + // 현재 비트맵이 목표보다 가로로 더 길면, 높이를 기준으로 너비를 계산하여 자름 (가로 양쪽 여백 발생) + cropHeight = h + cropWidth = (h * targetAspectRatio).toInt() + } else { + // 현재 비트맵이 목표보다 세로로 더 길거나 같으면, 너비를 기준으로 높이를 계산하여 자름 (세로 양쪽 여백 발생) + cropWidth = w + cropHeight = (w / targetAspectRatio).toInt() + } + + val startX = ((w - cropWidth) / 2).coerceAtLeast(0) + val startY = ((h - cropHeight) / 2).coerceAtLeast(0) + + val safeWidth = minOf(cropWidth, w - startX) + val safeHeight = minOf(cropHeight, h - startY) + + // 잘라낸 비트맵 반환 + 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) + val minutes = TimeUnit.SECONDS.toMinutes(totalSeconds) % 60 + val seconds = totalSeconds % 60 + + return String.format(Locale.getDefault(), "%02d:%02d", minutes, seconds) +} + +fun formatDistance(distance: Float): String { + val distanceToKm = distance / 1000 + return String.format(Locale.getDefault(), "%.1f km", distanceToKm) +} + +@Preview(showBackground = true) +@Composable +private fun WalkCourseScreenPreview() { + PawKeyTheme { + Row ( + modifier = Modifier + .background(Color.White, shape = RoundedCornerShape(12.dp)) + .border( + width = 1.dp, + color = Color(0xFF00C853), + shape = RoundedCornerShape(12.dp) + ) + .fillMaxWidth() + .padding(horizontal = 24.dp, vertical = 16.dp) + //.align(Alignment.CenterHorizontally) + ){ + val recordItems = listOf( + DistanceRecord, + TimeRecord, + StepsRecord, + ) + + recordItems.forEach { record -> + /*if (record == TimeRecord) { + WalkRecordItem( + recordTitle = record.titleResId, + recordContent = "00:00", + modifier = Modifier + .weight(1f), + ) + }*/ + WalkRecordItem( + recordTitle = record.titleResId, + recordContent = "00:00", + modifier = Modifier + .weight(1f), + ) + } + } + } +} \ No newline at end of file From 847528cf29e2d1fa44dca0b0ecf9225d68dcb9e9 Mon Sep 17 00:00:00 2001 From: sonms Date: Wed, 9 Jul 2025 12:28:02 +0900 Subject: [PATCH 13/27] =?UTF-8?q?feat/#11:=20=EC=82=B0=EC=B1=85=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C=20=ED=9B=84=20=EB=B7=B0=EB=AA=A8=EB=8D=B8=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20-=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20?= =?UTF-8?q?=EC=8A=A4=ED=86=A0=EC=96=B4=20di=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?=EB=B0=8F=20=EB=B9=84=ED=8A=B8=EB=A7=B5=20=EC=A0=80=EC=9E=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/paw/key/data/di/AppModule.kt | 33 +++++++++++ .../repositoryimpl/BitmapRepositoryImpl.kt | 59 +++++++++++++++++++ .../key/domain/repository/BitmapRepository.kt | 10 ++++ .../viewmodel/WalkCompleteViewModel.kt | 36 +++++++++++ 4 files changed, 138 insertions(+) create mode 100644 app/src/main/java/com/paw/key/data/di/AppModule.kt create mode 100644 app/src/main/java/com/paw/key/data/repositoryimpl/BitmapRepositoryImpl.kt create mode 100644 app/src/main/java/com/paw/key/domain/repository/BitmapRepository.kt create mode 100644 app/src/main/java/com/paw/key/presentation/ui/course/walkcomplete/viewmodel/WalkCompleteViewModel.kt diff --git a/app/src/main/java/com/paw/key/data/di/AppModule.kt b/app/src/main/java/com/paw/key/data/di/AppModule.kt new file mode 100644 index 00000000..f7ec4cfd --- /dev/null +++ b/app/src/main/java/com/paw/key/data/di/AppModule.kt @@ -0,0 +1,33 @@ +package com.paw.key.data.di + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.preferencesDataStore +import com.paw.key.BuildConfig +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Named +import javax.inject.Singleton + +val Context.bitmapDataStore: DataStore by preferencesDataStore(name = "captured_bitmap_prefs") + +@Module +@InstallIn(SingletonComponent::class) +object AppModule { + + @Provides + @Named("kakao.native.key") + fun provideKakaoNativeKey(): String { + return BuildConfig.KAKAO_NATIVE_KEY + } + + @Singleton + @Provides + fun provideBitmapDataStore(@ApplicationContext context: Context): DataStore { + return context.bitmapDataStore + } +} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/data/repositoryimpl/BitmapRepositoryImpl.kt b/app/src/main/java/com/paw/key/data/repositoryimpl/BitmapRepositoryImpl.kt new file mode 100644 index 00000000..48e13f58 --- /dev/null +++ b/app/src/main/java/com/paw/key/data/repositoryimpl/BitmapRepositoryImpl.kt @@ -0,0 +1,59 @@ +package com.paw.key.data.repositoryimpl + +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.util.Base64 +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringPreferencesKey +import com.paw.key.domain.repository.BitmapRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import java.io.ByteArrayOutputStream +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class BitmapRepositoryImpl @Inject constructor( + private val dataStore: DataStore +) : BitmapRepository { + private object PreferencesKeys { + val CAPTURED_BITMAP_BASE64 = stringPreferencesKey("captured_bitmap_base64") + } + + override suspend fun saveBitmap(bitmap: Bitmap) { + dataStore.edit { preferences -> + val outputStream = ByteArrayOutputStream() + bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream) + + val byteArray = outputStream.toByteArray() + val base64String = Base64.encodeToString(byteArray, Base64.DEFAULT) + + preferences[PreferencesKeys.CAPTURED_BITMAP_BASE64] = base64String + } + } + + override fun getSavedBitmap(): Flow { + return dataStore.data.map { preferences -> + val base64String = preferences[PreferencesKeys.CAPTURED_BITMAP_BASE64] + + if (!base64String.isNullOrEmpty()) { + try { + val byteArray = Base64.decode(base64String, Base64.DEFAULT) + BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size) + } catch (e: IllegalArgumentException) { + null + } + } else { + null + } + } + } + + override suspend fun clearSavedBitmap() { + dataStore.edit { preferences -> + preferences.remove(PreferencesKeys.CAPTURED_BITMAP_BASE64) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/domain/repository/BitmapRepository.kt b/app/src/main/java/com/paw/key/domain/repository/BitmapRepository.kt new file mode 100644 index 00000000..29449d46 --- /dev/null +++ b/app/src/main/java/com/paw/key/domain/repository/BitmapRepository.kt @@ -0,0 +1,10 @@ +package com.paw.key.domain.repository + +import android.graphics.Bitmap +import kotlinx.coroutines.flow.Flow + +interface BitmapRepository { + suspend fun saveBitmap(bitmap: Bitmap) + fun getSavedBitmap(): Flow + suspend fun clearSavedBitmap() +} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walkcomplete/viewmodel/WalkCompleteViewModel.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walkcomplete/viewmodel/WalkCompleteViewModel.kt new file mode 100644 index 00000000..e5e6ba4f --- /dev/null +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walkcomplete/viewmodel/WalkCompleteViewModel.kt @@ -0,0 +1,36 @@ +package com.paw.key.presentation.ui.course.walkcomplete.viewmodel + +import android.graphics.Bitmap +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.paw.key.domain.repository.BitmapRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class WalkCompleteViewModel @Inject constructor( + private val bitmapRepository: BitmapRepository +) : ViewModel() { + private val _savedMapBitmap = MutableStateFlow(null) + val savedMapBitmap: StateFlow + get() = _savedMapBitmap.asStateFlow() + + init { + viewModelScope.launch { + bitmapRepository.getSavedBitmap().collectLatest { bitmap -> + _savedMapBitmap.value = bitmap + } + } + } + + fun clearSavedBitmap() { + viewModelScope.launch { + bitmapRepository.clearSavedBitmap() + } + } +} \ No newline at end of file From 84c2b54df4c7013c9ba67b9d0f6a60cc88682eed Mon Sep 17 00:00:00 2001 From: sonms Date: Wed, 9 Jul 2025 12:29:32 +0900 Subject: [PATCH 14/27] =?UTF-8?q?feat/#11:=20=EC=A0=84=EC=B2=B4=EC=BD=94?= =?UTF-8?q?=EC=8A=A4=20=EB=A3=A8=ED=8A=B8=20=EB=84=A4=EB=B9=84=EA=B2=8C?= =?UTF-8?q?=EC=9D=B4=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{ => entire}/navigation/CourseNavigation.kt | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) rename app/src/main/java/com/paw/key/presentation/ui/course/{ => entire}/navigation/CourseNavigation.kt (72%) diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/navigation/CourseNavigation.kt b/app/src/main/java/com/paw/key/presentation/ui/course/entire/navigation/CourseNavigation.kt similarity index 72% rename from app/src/main/java/com/paw/key/presentation/ui/course/navigation/CourseNavigation.kt rename to app/src/main/java/com/paw/key/presentation/ui/course/entire/navigation/CourseNavigation.kt index 95cc92f6..49f0ac48 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/course/navigation/CourseNavigation.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/course/entire/navigation/CourseNavigation.kt @@ -1,5 +1,7 @@ -package com.paw.key.presentation.ui.course.navigation +package com.paw.key.presentation.ui.course.entire.navigation +import android.os.Build +import androidx.annotation.RequiresApi import androidx.compose.foundation.layout.PaddingValues import androidx.compose.material3.SnackbarHostState import androidx.navigation.NavController @@ -7,7 +9,7 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import androidx.navigation.compose.composable import com.paw.key.core.navigation.MainTabRoute -import com.paw.key.presentation.ui.course.CourseRoute +import com.paw.key.presentation.ui.course.entire.EntireCourseRoute import kotlinx.serialization.Serializable fun NavController.navigateCourse( @@ -16,17 +18,20 @@ fun NavController.navigateCourse( navigate(Course, navOptions) } +@RequiresApi(Build.VERSION_CODES.Q) fun NavGraphBuilder.courseNavGraph( paddingValues: PaddingValues, navigateUp: () -> Unit, navigateNext: () -> Unit, + setOnVisibleRecord: (Boolean) -> Unit, snackBarHostState: SnackbarHostState, ) { composable { - CourseRoute( + EntireCourseRoute( paddingValues = paddingValues, navigateUp = navigateUp, navigateNext = navigateNext, + setOnVisibleRecord = setOnVisibleRecord, snackBarHostState = snackBarHostState, ) } From 31a9e7cba857f57440a80a919441eaabf83527f4 Mon Sep 17 00:00:00 2001 From: sonms Date: Wed, 9 Jul 2025 12:29:59 +0900 Subject: [PATCH 15/27] =?UTF-8?q?delete/#11:=20=EC=A0=84=EC=B2=B4=20?= =?UTF-8?q?=EB=A3=A8=ED=8A=B8=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/ui/course/CourseScreen.kt | 52 ------------------- 1 file changed, 52 deletions(-) delete mode 100644 app/src/main/java/com/paw/key/presentation/ui/course/CourseScreen.kt diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/CourseScreen.kt b/app/src/main/java/com/paw/key/presentation/ui/course/CourseScreen.kt deleted file mode 100644 index 2a0793af..00000000 --- a/app/src/main/java/com/paw/key/presentation/ui/course/CourseScreen.kt +++ /dev/null @@ -1,52 +0,0 @@ -package com.paw.key.presentation.ui.course - -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.material3.SnackbarHostState -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import com.paw.key.R - -@Composable -fun CourseRoute( - paddingValues: PaddingValues, - navigateUp: () -> Unit, - navigateNext: () -> Unit, - snackBarHostState: SnackbarHostState, - modifier: Modifier = Modifier, -) { - CourseScreen( - paddingValues = paddingValues, - navigateUp = navigateUp, - navigateNext = navigateNext, - snackBarHostState = snackBarHostState, - modifier = modifier, - ) -} - -@Composable -fun CourseScreen( - paddingValues: PaddingValues, - navigateUp: () -> Unit, - navigateNext: () -> Unit, - snackBarHostState: SnackbarHostState, - modifier: Modifier = Modifier, -) { - Text( - text = stringResource(R.string.ic_course_description), - modifier = modifier, - ) -} - -@Preview -@Composable -private fun CourseScreenPreview() { - CourseScreen( - paddingValues = PaddingValues(), - navigateUp = {}, - navigateNext = {}, - snackBarHostState = SnackbarHostState(), - ) -} \ No newline at end of file From bc6c6efb39440d7325a95b7829cf144cfa932c31 Mon Sep 17 00:00:00 2001 From: sonms Date: Wed, 9 Jul 2025 12:30:26 +0900 Subject: [PATCH 16/27] =?UTF-8?q?feat/#11:=20=EB=A1=9C=EB=94=A9=20?= =?UTF-8?q?=EC=8A=A4=ED=81=AC=EB=A6=B0=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../designsystem/component/LoadingScreen.kt | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 app/src/main/java/com/paw/key/core/designsystem/component/LoadingScreen.kt diff --git a/app/src/main/java/com/paw/key/core/designsystem/component/LoadingScreen.kt b/app/src/main/java/com/paw/key/core/designsystem/component/LoadingScreen.kt new file mode 100644 index 00000000..4813afd1 --- /dev/null +++ b/app/src/main/java/com/paw/key/core/designsystem/component/LoadingScreen.kt @@ -0,0 +1,44 @@ +package com.paw.key.core.designsystem.component + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.paw.key.core.designsystem.theme.PawKeyTheme + +@Composable +fun LoadingScreen() { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + CircularProgressIndicator( + color = Color.Green + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text("현재 위치를 가져오는 중...") + } + } +} + +@Preview +@Composable +private fun LoadingScreenPreview() { + PawKeyTheme { + LoadingScreen() + } +} \ No newline at end of file From a4d83002927da3f2eccdacbab2a95eb5fab7f92c Mon Sep 17 00:00:00 2001 From: sonms Date: Wed, 9 Jul 2025 12:31:15 +0900 Subject: [PATCH 17/27] =?UTF-8?q?feat/#11:=20=EC=82=B0=EC=B1=85=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C=20=ED=9B=84=20=ED=97=A4=EB=8D=94=20-=20?= =?UTF-8?q?=ED=94=84=EB=A1=9C=ED=95=84=20=EC=82=AC=EC=A7=84,=20=EC=8B=9C?= =?UTF-8?q?=EA=B0=84,=20=EB=82=A0=EC=A7=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../component/WalkCompletionHeader.kt | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 app/src/main/java/com/paw/key/presentation/ui/course/walkcomplete/component/WalkCompletionHeader.kt diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walkcomplete/component/WalkCompletionHeader.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walkcomplete/component/WalkCompletionHeader.kt new file mode 100644 index 00000000..3c5585fb --- /dev/null +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walkcomplete/component/WalkCompletionHeader.kt @@ -0,0 +1,61 @@ +package com.paw.key.presentation.ui.course.walkcomplete.component + +import android.graphics.Bitmap +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import com.paw.key.core.designsystem.theme.PawKeyTheme + +@Composable +fun WalkCompleteHeader( + bitmap : Bitmap?, + modifier: Modifier = Modifier, +) { + Row ( + modifier = modifier + ){ + AsyncImage( + model = bitmap, + contentDescription = "profile", + modifier = Modifier + .background( + color = Color.LightGray, + shape = androidx.compose.foundation.shape.CircleShape + ) + .size(45.dp) + ) + + Column { + Text( + text = "포비", + color = Color.Black + ) + + Text( + text = "2025.06.26(금) | 오후 11:50", + color = Color.LightGray + ) + } + } +} + +@Preview +@Composable +private fun WalkCompleteHeaderPreview() { + PawKeyTheme { + WalkCompleteHeader( + bitmap = null + ) + } +} \ No newline at end of file From 2a89453d394293882c1395aeeafed69abe5f632e Mon Sep 17 00:00:00 2001 From: sonms Date: Wed, 9 Jul 2025 12:32:34 +0900 Subject: [PATCH 18/27] =?UTF-8?q?feat/#11:=20=EC=82=B0=EC=B1=85=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C=20=EB=84=A4=EB=B9=84=EA=B2=8C=EC=9D=B4?= =?UTF-8?q?=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../navigation/WalkCompletionNavigation.kt | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 app/src/main/java/com/paw/key/presentation/ui/course/walkcomplete/navigation/WalkCompletionNavigation.kt diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walkcomplete/navigation/WalkCompletionNavigation.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walkcomplete/navigation/WalkCompletionNavigation.kt new file mode 100644 index 00000000..3a162834 --- /dev/null +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walkcomplete/navigation/WalkCompletionNavigation.kt @@ -0,0 +1,40 @@ +package com.paw.key.presentation.ui.course.walkcomplete.navigation + +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.material3.SnackbarHostState +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import androidx.navigation.compose.composable +import com.paw.key.core.navigation.Route +import com.paw.key.presentation.ui.course.walk.WalkCourseRoute +import com.paw.key.presentation.ui.course.walkcomplete.WalkCompleteRoute +import kotlinx.serialization.Serializable + +fun NavController.navigateWalkCompletion( + navOptions: NavOptions? +) { + navigate(WalkCompletion, navOptions) +} + +@RequiresApi(Build.VERSION_CODES.Q) +fun NavGraphBuilder.walkCompletionNavGraph( + paddingValues: PaddingValues, + navigateUp: () -> Unit, + navigateNext: () -> Unit, + snackBarHostState: SnackbarHostState, +) { + composable { + WalkCompleteRoute( + paddingValues = paddingValues, + navigateUp = navigateUp, + navigateNext = navigateNext, + snackBarHostState = snackBarHostState, + ) + } +} + +@Serializable +data object WalkCompletion : Route \ No newline at end of file From 272909115c65360d5b0b44e2ee239023973ca12d Mon Sep 17 00:00:00 2001 From: sonms Date: Wed, 9 Jul 2025 12:32:50 +0900 Subject: [PATCH 19/27] =?UTF-8?q?feat/#11:=20=EC=82=B0=EC=B1=85=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C=20=EB=B7=B0=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../walkcomplete/WalkCompletionScreen.kt | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 app/src/main/java/com/paw/key/presentation/ui/course/walkcomplete/WalkCompletionScreen.kt diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walkcomplete/WalkCompletionScreen.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walkcomplete/WalkCompletionScreen.kt new file mode 100644 index 00000000..b539a15d --- /dev/null +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walkcomplete/WalkCompletionScreen.kt @@ -0,0 +1,108 @@ +package com.paw.key.presentation.ui.course.walkcomplete + +import android.graphics.Bitmap +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.paw.key.presentation.ui.course.walk.component.WalkRecordRow +import com.paw.key.presentation.ui.course.walkcomplete.component.WalkCompleteHeader +import com.paw.key.presentation.ui.course.walkcomplete.viewmodel.WalkCompleteViewModel + +@Composable +fun WalkCompleteRoute( + paddingValues: PaddingValues, + navigateUp: () -> Unit, + navigateNext: () -> Unit, + snackBarHostState: SnackbarHostState, + modifier: Modifier = Modifier, + viewModel: WalkCompleteViewModel = hiltViewModel(), +) { + val bitmap by viewModel.savedMapBitmap.collectAsStateWithLifecycle() + + WalkCompleteScreen( + paddingValues = paddingValues, + navigateUp = navigateUp, + navigateNext = navigateNext, + snackBarHostState = snackBarHostState, + bitmap = bitmap, + modifier = modifier, + ) +} + +@Composable +fun WalkCompleteScreen( + paddingValues: PaddingValues, + navigateUp: () -> Unit, + navigateNext: () -> Unit, + snackBarHostState: SnackbarHostState, + bitmap: Bitmap?, + modifier: Modifier = Modifier, +) { + Column ( + modifier = modifier + .padding(paddingValues) + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ){ + Text( + text = "산책 완료", + modifier = modifier + .align(Alignment.Start) + .fillMaxWidth() + ) + + Text( + text = "포비와 함께한 산책한 루트에요.", + modifier = modifier + .fillMaxWidth() + ) + + WalkCompleteHeader( + bitmap = null, + modifier = modifier + .padding(top = 10.dp) + ) + + bitmap?.asImageBitmap()?.let { + Image( + bitmap = it, + contentDescription = "My Image", + modifier = Modifier + .padding(top = 6.dp) + ) + } + + WalkRecordRow( + totalDistance = "2.2", + totalTime = "30:00", + currentSteps = 12345, + modifier = modifier + .padding(top = 10.dp) + ) + + Spacer(modifier = Modifier.weight(1f)) + + // Todo : 공통컴포넌트 + Button( + onClick = navigateNext, + modifier = Modifier + .fillMaxWidth() + ) { + Text(text = "산책 기록하기") + } + } +} \ No newline at end of file From f7a975b004a1df297f544269032a9ed7045a247d Mon Sep 17 00:00:00 2001 From: sonms Date: Wed, 9 Jul 2025 12:36:09 +0900 Subject: [PATCH 20/27] =?UTF-8?q?chore/#11:=20=EB=A9=94=EC=9D=B8=20?= =?UTF-8?q?=EB=B0=94=ED=85=80=20visible=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/paw/key/presentation/ui/main/MainActivity.kt | 3 +++ .../paw/key/presentation/ui/main/component/MainBottomBar.kt | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) 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 e0372f9d..48b1977c 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,15 +1,18 @@ package com.paw.key.presentation.ui.main +import android.os.Build import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge +import androidx.annotation.RequiresApi import androidx.core.view.WindowCompat import com.paw.key.core.designsystem.theme.PawKeyTheme import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint class MainActivity : ComponentActivity() { + @RequiresApi(Build.VERSION_CODES.Q) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() 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 8fa79e34..63b6cf3b 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 @@ -43,7 +43,7 @@ fun MainBottomBar( modifier: Modifier = Modifier, ) { AnimatedVisibility ( - visible = true, + visible = isVisible, enter = EnterTransition.None, exit = ExitTransition.None, modifier = modifier @@ -56,7 +56,7 @@ fun MainBottomBar( contentAlignment = Alignment.Center ) { Surface( - color = Color.Black, + color = PawKeyTheme.colors.gray950, shape = RoundedCornerShape(200.dp), shadowElevation = 10.dp, modifier = Modifier From b226db20d0341329458541dc89246a9f37ab79a4 Mon Sep 17 00:00:00 2001 From: sonms Date: Wed, 9 Jul 2025 12:36:46 +0900 Subject: [PATCH 21/27] =?UTF-8?q?chore/#11:=20=EB=A9=94=EC=9D=B8=20?= =?UTF-8?q?=EB=84=A4=EB=B9=84=EA=B2=8C=EC=9D=B4=ED=84=B0=20=EC=88=A8?= =?UTF-8?q?=EA=B9=80=20=EB=B6=84=EA=B8=B0=20=EC=B6=94=EA=B0=80=EB=A1=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../key/presentation/ui/main/MainNavigator.kt | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/paw/key/presentation/ui/main/MainNavigator.kt b/app/src/main/java/com/paw/key/presentation/ui/main/MainNavigator.kt index eefe1b9e..c557c9d6 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/main/MainNavigator.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/main/MainNavigator.kt @@ -10,7 +10,9 @@ import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController import androidx.navigation.navOptions import com.paw.key.presentation.ui.community.navigation.navigateCommunity -import com.paw.key.presentation.ui.course.navigation.navigateCourse +import com.paw.key.presentation.ui.course.entire.navigation.navigateCourse +import com.paw.key.presentation.ui.course.entire.tab.map.navigation.navigateWalkCourse +import com.paw.key.presentation.ui.course.walkcomplete.navigation.navigateWalkCompletion import com.paw.key.presentation.ui.dummy.navigation.navigateDummy import com.paw.key.presentation.ui.dummy.next.navigateDummyNext import com.paw.key.presentation.ui.home.navigation.Home @@ -31,6 +33,9 @@ class MainNavigator ( currentDestination?.hasRoute(tab::class) == true } + var isRecordVisible: Boolean = false + private set + fun navigate(tab: MainTab) { val navOptions = navOptions { navController.currentDestination?.route?.let { @@ -51,15 +56,22 @@ class MainNavigator ( } } - // 더미용 Todo : 나중에 위에거로 교환예정 - fun navigateToDummy(navOptions: NavOptions? = null) { - navController.navigateDummy(navOptions = navOptions) + fun setOnVisibleRecord(visible: Boolean) { + isRecordVisible = visible } fun navigateDummyNext(navOptions: NavOptions? = null) { navController.navigateDummyNext(navOptions = navOptions) } + fun navigateWalkCourse(navOptions: NavOptions? = null) { + navController.navigateWalkCourse(navOptions = navOptions) + } + + fun navigateWalkCompletion(navOptions: NavOptions? = null) { + navController.navigateWalkCompletion(navOptions = navOptions) + } + fun navigateUp() { navController.navigateUp() } @@ -67,7 +79,7 @@ class MainNavigator ( @Composable fun showBottomBar() = MainTab.contains { currentDestination?.hasRoute(it::class) == true - } + } && !isRecordVisible } @Composable From cf61a1b08ab503d600a119bdf48282abeb4f95a9 Mon Sep 17 00:00:00 2001 From: sonms Date: Wed, 9 Jul 2025 12:42:45 +0900 Subject: [PATCH 22/27] =?UTF-8?q?chore/#11:=20Main=20=EB=B2=84=EC=A0=84=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/com/paw/key/presentation/ui/main/MainScreen.kt | 4 ++++ app/src/main/java/com/paw/key/presentation/ui/main/MainTab.kt | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/paw/key/presentation/ui/main/MainScreen.kt b/app/src/main/java/com/paw/key/presentation/ui/main/MainScreen.kt index 5e44e405..84a5809e 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/main/MainScreen.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/main/MainScreen.kt @@ -1,5 +1,7 @@ package com.paw.key.presentation.ui.main +import android.os.Build +import androidx.annotation.RequiresApi import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.WindowInsets @@ -17,6 +19,7 @@ import androidx.compose.ui.Modifier import com.paw.key.presentation.ui.main.component.MainBottomBar import kotlinx.collections.immutable.toImmutableList +@RequiresApi(Build.VERSION_CODES.Q) @Composable fun MainScreen( navigator: MainNavigator = rememberMainNavigator(), @@ -29,6 +32,7 @@ fun MainScreen( ) } +@RequiresApi(Build.VERSION_CODES.Q) @Composable private fun MainScreenContent( navigator: MainNavigator, diff --git a/app/src/main/java/com/paw/key/presentation/ui/main/MainTab.kt b/app/src/main/java/com/paw/key/presentation/ui/main/MainTab.kt index 847fd7b9..90752cb2 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/main/MainTab.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/main/MainTab.kt @@ -6,7 +6,7 @@ import androidx.compose.runtime.Composable import com.paw.key.R import com.paw.key.core.navigation.MainTabRoute import com.paw.key.presentation.ui.home.navigation.Home -import com.paw.key.presentation.ui.course.navigation.Course +import com.paw.key.presentation.ui.course.entire.navigation.Course import com.paw.key.presentation.ui.community.navigation.Community import com.paw.key.presentation.ui.mypage.navigation.MyPage import com.paw.key.R.string.ic_home_description From 2b3f0fd74073f78018556895e102bbbfac3df649 Mon Sep 17 00:00:00 2001 From: sonms Date: Wed, 9 Jul 2025 12:44:11 +0900 Subject: [PATCH 23/27] =?UTF-8?q?chore/#11:=20Application=20=EC=88=98?= =?UTF-8?q?=EC=A4=80=EC=97=90=EC=84=9C=20=EC=B9=B4=EC=B9=B4=EC=98=A4=20sdk?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/java/com/paw/key/PawKeyApplication.kt | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/src/main/java/com/paw/key/PawKeyApplication.kt b/app/src/main/java/com/paw/key/PawKeyApplication.kt index cddaf6e2..54735d3f 100644 --- a/app/src/main/java/com/paw/key/PawKeyApplication.kt +++ b/app/src/main/java/com/paw/key/PawKeyApplication.kt @@ -2,16 +2,25 @@ package com.paw.key import android.app.Application import androidx.appcompat.app.AppCompatDelegate +import com.kakao.vectormap.KakaoMapSdk import dagger.hilt.android.HiltAndroidApp import timber.log.Timber +import javax.inject.Inject +import javax.inject.Named @HiltAndroidApp class PawKeyApplication : Application() { + @Inject + @Named("kakao.native.key") + lateinit var kakaoNativeKey: String + override fun onCreate() { super.onCreate() setTimber() setDarkMode() + + KakaoMapSdk.init(this, kakaoNativeKey) } private fun setTimber() { From 4db6fc70ed9445c4d512f237508dd8fd3ee5bdfe Mon Sep 17 00:00:00 2001 From: sonms Date: Wed, 9 Jul 2025 12:44:45 +0900 Subject: [PATCH 24/27] =?UTF-8?q?feat/#11:=20=EB=8D=B0=EC=9D=B4=ED=84=B0?= =?UTF-8?q?=20=EC=8A=A4=ED=86=A0=EC=96=B4=20=EB=A0=88=ED=8F=AC=EC=A7=80?= =?UTF-8?q?=ED=86=A0=EB=A6=AC=20=EB=AA=A8=EB=93=88=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/java/com/paw/key/data/di/RepositoryModule.kt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/src/main/java/com/paw/key/data/di/RepositoryModule.kt b/app/src/main/java/com/paw/key/data/di/RepositoryModule.kt index f3659c82..3d66968d 100644 --- a/app/src/main/java/com/paw/key/data/di/RepositoryModule.kt +++ b/app/src/main/java/com/paw/key/data/di/RepositoryModule.kt @@ -1,6 +1,8 @@ package com.paw.key.data.di +import com.paw.key.data.repositoryimpl.BitmapRepositoryImpl import com.paw.key.data.repositoryimpl.DummyRepositoryImpl +import com.paw.key.domain.repository.BitmapRepository import com.paw.key.domain.repository.DummyRepository import dagger.Binds import dagger.Module @@ -16,4 +18,9 @@ interface RepositoryModule { dummyRepositoryImpl: DummyRepositoryImpl ): DummyRepository + @Binds + fun bindsBitmapRepository( + bitmapRepositoryImpl: BitmapRepositoryImpl + ): BitmapRepository + } \ No newline at end of file From 5bbbb1a6179d50185b65f573542634a0c93dde58 Mon Sep 17 00:00:00 2001 From: sonms Date: Wed, 9 Jul 2025 12:45:11 +0900 Subject: [PATCH 25/27] =?UTF-8?q?feat/#11:=20=EC=82=B0=EC=B1=85=ED=95=98?= =?UTF-8?q?=EA=B8=B0=20=ED=99=94=EB=A9=B4=20=EC=9D=B4=EB=8F=99=20navhost?= =?UTF-8?q?=20=EC=84=B8=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../key/presentation/ui/main/PawKeyNavHost.kt | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) 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 52b36222..124fd472 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 @@ -1,5 +1,7 @@ package com.paw.key.presentation.ui.main +import android.os.Build +import androidx.annotation.RequiresApi import androidx.compose.animation.EnterTransition import androidx.compose.animation.ExitTransition import androidx.compose.foundation.layout.PaddingValues @@ -8,12 +10,15 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.navigation.compose.NavHost import com.paw.key.presentation.ui.community.navigation.communityNavGraph -import com.paw.key.presentation.ui.course.navigation.courseNavGraph +import com.paw.key.presentation.ui.course.entire.navigation.courseNavGraph +import com.paw.key.presentation.ui.course.entire.tab.map.navigation.walkCourseNavGraph +import com.paw.key.presentation.ui.course.walkcomplete.navigation.walkCompletionNavGraph import com.paw.key.presentation.ui.dummy.navigation.dummyNavGraph import com.paw.key.presentation.ui.dummy.next.dummyNextNavGraph import com.paw.key.presentation.ui.home.navigation.homeNavGraph import com.paw.key.presentation.ui.mypage.navigation.myPageNavGraph +@RequiresApi(Build.VERSION_CODES.Q) @Composable fun PawKeyNavHost ( navigator: MainNavigator, @@ -38,6 +43,21 @@ fun PawKeyNavHost ( ) courseNavGraph( + paddingValues = paddingValues, + navigateUp = navigator::navigateUp, + navigateNext = navigator::navigateWalkCourse, + setOnVisibleRecord = navigator::setOnVisibleRecord, + snackBarHostState = snackbarHostState + ) + + walkCourseNavGraph( + paddingValues = paddingValues, + navigateUp = navigator::navigateUp, + navigateNext = navigator::navigateWalkCompletion, + snackBarHostState = snackbarHostState + ) + + walkCompletionNavGraph( paddingValues = paddingValues, navigateUp = navigator::navigateUp, navigateNext = navigator::navigateDummyNext, From eaed79f37a1058ff58ed2aaf236cf15767b89154 Mon Sep 17 00:00:00 2001 From: sonms Date: Wed, 9 Jul 2025 12:45:34 +0900 Subject: [PATCH 26/27] =?UTF-8?q?setting/#11:=20=EC=82=B0=EC=B1=85?= =?UTF-8?q?=ED=95=98=EA=B8=B0=20=EA=B4=80=EB=A0=A8=20strings=20=EC=84=B8?= =?UTF-8?q?=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/res/values/strings.xml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 32fe0835..03313617 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -16,4 +16,12 @@ mypage + + 지도 + 리스트 + + 거리 (km) + 시간 (분) + 걸음 수 (걸음) + \ No newline at end of file From 719ac83bdf42fab65c283e8de79d6f7a28f7188e Mon Sep 17 00:00:00 2001 From: sonms Date: Wed, 9 Jul 2025 12:46:01 +0900 Subject: [PATCH 27/27] =?UTF-8?q?setting/#11:=20centerLabel=20-=20?= =?UTF-8?q?=EC=9C=A0=EC=A0=80=20=EC=84=B8=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/res/drawable/user_poi.png | Bin 0 -> 583 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 app/src/main/res/drawable/user_poi.png diff --git a/app/src/main/res/drawable/user_poi.png b/app/src/main/res/drawable/user_poi.png new file mode 100644 index 0000000000000000000000000000000000000000..212842d489a74489ca41cc6c8f45ad22b00803af GIT binary patch literal 583 zcmV-N0=WH&P)$4yV~xYUAhq@~GDJlcmAOQ~<8{X=Y?Iu6+{0>m4NjlWOA^~WEwI=%WA*tenmEsG z&~OxYjl(gbj>#m`XAAI}%T~NngW`vko_U+hB1En+ZsNGSaqeQdDv5wtTh-T`SDawP zrh@$@8LH?3YT@+d9Imn%nMq^R6^Z1<8W2JhfguL}1R_GcFbVsEbtlSSf!-*^NW(;N zF$lel-8D&CNiq3Qdgl~FFgN5)Qyr7wCyA>;tSxoUIHDM{e)ituVVq0;x_!Tg*~?|P zb0{dP6$iC>Tcjx0qs@b1izsO%Lpff-BBWx8MI3J^EaLYmYQz^w+os(o(|-Qdv>q~T z