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/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() {
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
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/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
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/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
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,
+ )
+ }
+}
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
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,
)
}
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/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
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