From acf370db8cf15c7543d130d7b66eef077b21709c Mon Sep 17 00:00:00 2001 From: sonms Date: Mon, 5 Jan 2026 12:48:35 +0900 Subject: [PATCH 01/47] =?UTF-8?q?delete/#154=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20=ED=8C=8C=EC=9D=BC=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/course/entire/EntireCourseScreen.kt | 251 ------- .../entire/component/EntireCourseTabRow.kt | 90 --- .../entire/navigation/CourseNavigation.kt | 51 -- .../entire/state/EntireCourseContract.kt | 36 - .../tab/map/List/CourseOptionBottomSheet.kt | 638 ----------------- .../entire/tab/map/List/TabListScreen.kt | 242 ------- .../tab/map/List/state/TapListContract.kt | 50 -- .../map/List/viewmodel/TapListViewModel.kt | 422 ----------- .../ui/course/entire/tab/map/TapMapScreen.kt | 191 ----- .../entire/tab/map/state/TapMapContract.kt | 21 - .../tab/map/viewmodel/TapMapViewModel.kt | 61 -- .../entire/viewmodel/EntireCourseViewModel.kt | 30 - .../complete/SharedWalkCompletionScreen.kt | 181 ----- .../navigation/SharedWalkCompletionRoute.kt | 49 -- .../review/SharedWalkReviewScreen.kt | 237 ------- .../navigation/SharedWalkReviewNavigation.kt | 51 -- .../review/state/SharedWalkReviewContract.kt | 31 - .../viewmodel/SharedWalkReviewViewModel.kt | 139 ---- .../sharedroute/SharedWalkCourseScreen.kt | 657 ------------------ .../navigation/SharedWalkNavigation.kt | 49 -- .../state/SharedWalkCourseContract.kt | 53 -- .../viewmodel/SharedWalkViewModel.kt | 238 ------- .../ui/course/walk/component/WalkRecordRow.kt | 70 -- .../walk/navigation/WalkCourseNavigation.kt | 41 -- .../walkcomplete/WalkCompletionScreen.kt | 171 ----- .../component/WalkCompletionHeader.kt | 71 -- .../component/WalkCompletionItem.kt | 55 -- .../component/WalkCompletionRecordRow.kt | 62 -- .../navigation/WalkCompletionNavigation.kt | 41 -- .../state/WalkCompleteContract.kt | 16 - .../viewmodel/WalkCompleteViewModel.kt | 75 -- .../ui/course/walkreview/WalkReviewUiModel.kt | 14 - .../component/WalkReviewFeedbackForm.kt | 91 --- .../component/WalkReviewFeedbackHeader.kt | 46 -- .../component/WalkReviewImageItem.kt | 118 ---- .../component/WalkReviewTextField.kt | 73 -- .../course/walkreview/util/WalkReviewUtil.kt | 22 - .../main/res/drawable/ic_community_fill.xml | 18 - .../main/res/drawable/ic_community_linear.xml | 34 - 39 files changed, 4786 deletions(-) delete mode 100644 app/src/main/java/com/paw/key/presentation/ui/course/entire/EntireCourseScreen.kt delete mode 100644 app/src/main/java/com/paw/key/presentation/ui/course/entire/component/EntireCourseTabRow.kt delete mode 100644 app/src/main/java/com/paw/key/presentation/ui/course/entire/navigation/CourseNavigation.kt delete mode 100644 app/src/main/java/com/paw/key/presentation/ui/course/entire/state/EntireCourseContract.kt delete mode 100644 app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/List/CourseOptionBottomSheet.kt delete mode 100644 app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/List/TabListScreen.kt delete mode 100644 app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/List/state/TapListContract.kt delete mode 100644 app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/List/viewmodel/TapListViewModel.kt delete mode 100644 app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/TapMapScreen.kt delete mode 100644 app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/state/TapMapContract.kt delete mode 100644 app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/viewmodel/TapMapViewModel.kt delete mode 100644 app/src/main/java/com/paw/key/presentation/ui/course/entire/viewmodel/EntireCourseViewModel.kt delete mode 100644 app/src/main/java/com/paw/key/presentation/ui/course/sharedwalk/complete/SharedWalkCompletionScreen.kt delete mode 100644 app/src/main/java/com/paw/key/presentation/ui/course/sharedwalk/complete/navigation/SharedWalkCompletionRoute.kt delete mode 100644 app/src/main/java/com/paw/key/presentation/ui/course/sharedwalk/review/SharedWalkReviewScreen.kt delete mode 100644 app/src/main/java/com/paw/key/presentation/ui/course/sharedwalk/review/navigation/SharedWalkReviewNavigation.kt delete mode 100644 app/src/main/java/com/paw/key/presentation/ui/course/sharedwalk/review/state/SharedWalkReviewContract.kt delete mode 100644 app/src/main/java/com/paw/key/presentation/ui/course/sharedwalk/review/viewmodel/SharedWalkReviewViewModel.kt delete mode 100644 app/src/main/java/com/paw/key/presentation/ui/course/sharedwalk/sharedroute/SharedWalkCourseScreen.kt delete mode 100644 app/src/main/java/com/paw/key/presentation/ui/course/sharedwalk/sharedroute/navigation/SharedWalkNavigation.kt delete mode 100644 app/src/main/java/com/paw/key/presentation/ui/course/sharedwalk/sharedroute/state/SharedWalkCourseContract.kt delete mode 100644 app/src/main/java/com/paw/key/presentation/ui/course/sharedwalk/sharedroute/viewmodel/SharedWalkViewModel.kt delete mode 100644 app/src/main/java/com/paw/key/presentation/ui/course/walk/component/WalkRecordRow.kt delete mode 100644 app/src/main/java/com/paw/key/presentation/ui/course/walk/navigation/WalkCourseNavigation.kt delete mode 100644 app/src/main/java/com/paw/key/presentation/ui/course/walkcomplete/WalkCompletionScreen.kt delete mode 100644 app/src/main/java/com/paw/key/presentation/ui/course/walkcomplete/component/WalkCompletionHeader.kt delete mode 100644 app/src/main/java/com/paw/key/presentation/ui/course/walkcomplete/component/WalkCompletionItem.kt delete mode 100644 app/src/main/java/com/paw/key/presentation/ui/course/walkcomplete/component/WalkCompletionRecordRow.kt delete mode 100644 app/src/main/java/com/paw/key/presentation/ui/course/walkcomplete/navigation/WalkCompletionNavigation.kt delete mode 100644 app/src/main/java/com/paw/key/presentation/ui/course/walkcomplete/state/WalkCompleteContract.kt delete mode 100644 app/src/main/java/com/paw/key/presentation/ui/course/walkcomplete/viewmodel/WalkCompleteViewModel.kt delete mode 100644 app/src/main/java/com/paw/key/presentation/ui/course/walkreview/WalkReviewUiModel.kt delete mode 100644 app/src/main/java/com/paw/key/presentation/ui/course/walkreview/component/WalkReviewFeedbackForm.kt delete mode 100644 app/src/main/java/com/paw/key/presentation/ui/course/walkreview/component/WalkReviewFeedbackHeader.kt delete mode 100644 app/src/main/java/com/paw/key/presentation/ui/course/walkreview/component/WalkReviewImageItem.kt delete mode 100644 app/src/main/java/com/paw/key/presentation/ui/course/walkreview/component/WalkReviewTextField.kt delete mode 100644 app/src/main/java/com/paw/key/presentation/ui/course/walkreview/util/WalkReviewUtil.kt delete mode 100644 app/src/main/res/drawable/ic_community_fill.xml delete mode 100644 app/src/main/res/drawable/ic_community_linear.xml 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 deleted file mode 100644 index 86e3638f..00000000 --- a/app/src/main/java/com/paw/key/presentation/ui/course/entire/EntireCourseScreen.kt +++ /dev/null @@ -1,251 +0,0 @@ -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 android.util.Log -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.compose.ui.unit.dp -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.List.TapListRoute -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, - navigateToDetail: (Int, Int) -> Unit, - routeIndex: Int, - 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 }) - - LaunchedEffect(Unit) { - viewModel.updateState { - copy(selectedTabIndex = routeIndex) - } - } - - 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) - }, - navigateToDetail = { postId, routeId -> - navigateToDetail(postId, routeId) - }, - 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, - navigateToDetail : (Int, Int) -> Unit, - setOnVisibleRecord : (Boolean) -> Unit, - onTabSelected : (Int) -> Unit, - modifier: Modifier = Modifier, -) { - val scope = rememberCoroutineScope() - - Column ( - modifier = modifier - .padding(paddingValues) - .fillMaxSize() - ) { - EntireCourseTabRow( - selectedTabIndex = currentPage, - onTabSelected = { - onTabSelected(it) - }, - tabs = tabs, - modifier = Modifier - .padding(top = 8.dp), - ) - - when (currentPage) { - 0 -> { - TapMapRoute( - paddingValues = paddingValues, - navigateUp = {}, - navigateNext = { - navigateNext() - }, - isGranted = isGranted, - snackBarHostState = snackBarHostState, - ) - } - - 1 -> { - TapListRoute( - navigateToDetail = { postId, routeId -> - Log.e("TabListScreen", "postId: $postId, routeId: $routeId") - navigateToDetail(postId, routeId) - } - ) - } - } - } -} - -@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 = {}, - navigateToDetail = { postId, routeId -> - }, - 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 deleted file mode 100644 index 0dc5db92..00000000 --- a/app/src/main/java/com/paw/key/presentation/ui/course/entire/component/EntireCourseTabRow.kt +++ /dev/null @@ -1,90 +0,0 @@ -package com.paw.key.presentation.ui.course.entire.component - -import androidx.compose.foundation.interaction.MutableInteractionSource -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.runtime.remember -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.core.util.NoRippleInteractionSource -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.white1, - indicator = { tabPositions -> - TabRowDefaults.SecondaryIndicator( - modifier = modifier - .tabIndicatorOffset(tabPositions[selectedTabIndex]) - .height(4.dp), - color = PawKeyTheme.colors.black - ) - /*val currentTabPosition = tabPositions[selectedTabIndex] - val indicatorWidth = currentTabPosition.contentWidth - val indicatorOffset = currentTabPosition.left - - Box( - modifier = Modifier - .fillMaxWidth() - .wrapContentSize(Alignment.BottomStart) - .offset(x = indicatorOffset) - .width(indicatorWidth) - .height(4.dp) - .background( - color = PawKeyTheme.colors.black, - shape = RoundedCornerShape(2.dp) - ) - )*/ - }, - divider = {} - ) { - tabs.forEachIndexed { index, tab -> - val isSelected = index == selectedTabIndex - - Tab( - text = { - Text( - text = stringResource(id = tab.titleResId), - color = if (isSelected) PawKeyTheme.colors.black else PawKeyTheme.colors.gray200, - style = PawKeyTheme.typography.head22B, - ) - }, - selected = index == selectedTabIndex, - onClick = { - onTabSelected(index) - }, - interactionSource = remember { NoRippleInteractionSource() }, - ) - } - } -} - -@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/entire/navigation/CourseNavigation.kt b/app/src/main/java/com/paw/key/presentation/ui/course/entire/navigation/CourseNavigation.kt deleted file mode 100644 index 7259a667..00000000 --- a/app/src/main/java/com/paw/key/presentation/ui/course/entire/navigation/CourseNavigation.kt +++ /dev/null @@ -1,51 +0,0 @@ -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 -import androidx.navigation.NavGraphBuilder -import androidx.navigation.NavOptions -import androidx.navigation.compose.composable -import androidx.navigation.toRoute -import com.paw.key.core.navigation.MainTabRoute -import com.paw.key.presentation.ui.course.entire.EntireCourseRoute -import kotlinx.serialization.Serializable - -fun NavController.navigateCourse( - navOptions: NavOptions? = null, - index: Int = 0, -) = navigate(Course(index), navOptions) - - -@RequiresApi(Build.VERSION_CODES.Q) -fun NavGraphBuilder.courseNavGraph( - paddingValues: PaddingValues, - navigateUp: () -> Unit, - navigateNext: () -> Unit, - navigateToDetail: (Int, Int) -> Unit, - setOnVisibleRecord: (Boolean) -> Unit, - snackBarHostState: SnackbarHostState, -) { - composable { backStackEntry -> - val courseDestination = backStackEntry.toRoute() - val receivedIndex = courseDestination.index - - EntireCourseRoute( - paddingValues = paddingValues, - navigateUp = navigateUp, - navigateNext = navigateNext, - navigateToDetail = { postId, routeId -> - navigateToDetail(postId, routeId) - }, - routeIndex = receivedIndex, - setOnVisibleRecord = setOnVisibleRecord, - snackBarHostState = snackBarHostState, - ) - } -} - -@Serializable -data class Course(val index: Int = 0) : MainTabRoute - 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 deleted file mode 100644 index f51cf000..00000000 --- a/app/src/main/java/com/paw/key/presentation/ui/course/entire/state/EntireCourseContract.kt +++ /dev/null @@ -1,36 +0,0 @@ -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/List/CourseOptionBottomSheet.kt b/app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/List/CourseOptionBottomSheet.kt deleted file mode 100644 index fc5954dd..00000000 --- a/app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/List/CourseOptionBottomSheet.kt +++ /dev/null @@ -1,638 +0,0 @@ -package com.paw.key.presentation.ui.course.entire.tab.map.List - -import android.util.Log -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Checkbox -import androidx.compose.material3.CheckboxDefaults -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.ModalBottomSheet -import androidx.compose.material3.Text -import androidx.compose.material3.rememberModalBottomSheetState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.res.vectorResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.paw.key.R -import com.paw.key.core.designsystem.component.PawkeyButton -import com.paw.key.core.designsystem.theme.PawKeyTheme -import com.paw.key.core.extension.noRippleClickable -import com.paw.key.presentation.ui.course.entire.tab.map.List.state.TapListContract -import com.paw.key.presentation.ui.course.entire.tab.map.List.viewmodel.TapListViewModel - -@Preview(showBackground = true) -@Composable -fun PreviewCourseOptionBottomSheet() { - PawKeyTheme { - Column { - Text("Bottom Sheet Preview") - } - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun CourseOptionBottomSheet( - modifier: Modifier = Modifier, - viewModel: TapListViewModel, - onDismissRequest: () -> Unit, -) { - val sheetState = rememberModalBottomSheetState( - skipPartiallyExpanded = false - ) - val listState by viewModel.state.collectAsStateWithLifecycle() - - LaunchedEffect(Unit) { - if (listState.filterOptions == null && !listState.isLoading) { - viewModel.loadFilterOptions() - } - } - - ModalBottomSheet( - onDismissRequest = onDismissRequest, - sheetState = sheetState, - containerColor = PawKeyTheme.colors.white1, - shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp), - ) { - Column( - modifier = modifier - .fillMaxWidth() - .padding(bottom = 34.dp) - ) { - BottomSheetHeader() - - when { - listState.isLoading -> { - LoadingContent( - listState = listState, - viewModel = viewModel, - modifier = Modifier.weight(1f) - ) - } - - listState.filterOptions != null -> { - SuccessContent( - listState = listState, - viewModel = viewModel, - modifier = Modifier.weight(1f) - ) - } - - else -> { - ErrorContent( - listState = listState, - viewModel = viewModel, - modifier = Modifier.weight(1f) - ) - } - } - - BottomButtons( - viewModel = viewModel, - onDismissRequest = onDismissRequest - ) - } - } -} - -@Composable -private fun BottomSheetHeader() { - Column( - modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp) - ) { - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = stringResource(id = R.string.course_list_option_title), - color = PawKeyTheme.colors.black, - style = PawKeyTheme.typography.head20B1 - ) - Spacer(modifier = Modifier.weight(1f)) - } - - Spacer(modifier = Modifier.height(16.dp)) - - Text( - text = stringResource(id = R.string.course_list_option_sort), - color = PawKeyTheme.colors.green500, - style = PawKeyTheme.typography.caption12Sb1 - ) - } -} - -@Composable -private fun LoadingContent( - listState: TapListContract.TapListState, - viewModel: TapListViewModel, - modifier: Modifier = Modifier -) { - LazyColumn( - modifier = modifier - .fillMaxWidth() - .padding(horizontal = 16.dp) - ) { - items(TapListContract.Options.sortOptions) { option -> - SortOptionItem( - title = option, - isSelected = listState.selectedSortOption == option, - onSelect = { viewModel.updateSortOption(option) } - ) - } - - item { - Spacer(modifier = Modifier.height(32.dp)) - Box( - modifier = Modifier.fillMaxWidth(), - contentAlignment = Alignment.Center - ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally - ) { - CircularProgressIndicator( - color = PawKeyTheme.colors.green500, - modifier = Modifier.size(24.dp) - ) - Spacer(modifier = Modifier.height(16.dp)) - Text( - text = "필터 옵션을 불러오는 중...", - color = PawKeyTheme.colors.gray500, - style = PawKeyTheme.typography.body14R, - textAlign = TextAlign.Center - ) - } - } - Spacer(modifier = Modifier.height(80.dp)) - } - } -} - -@Composable -private fun ErrorContent( - listState: TapListContract.TapListState, - viewModel: TapListViewModel, - modifier: Modifier = Modifier -) { - LazyColumn( - modifier = modifier - .fillMaxWidth() - .padding(horizontal = 16.dp) - ) { - items(TapListContract.Options.sortOptions) { option -> - SortOptionItem( - title = option, - isSelected = listState.selectedSortOption == option, - onSelect = { viewModel.updateSortOption(option) } - ) - } - - item { - Spacer(modifier = Modifier.height(32.dp)) - Box( - modifier = Modifier.fillMaxWidth(), - contentAlignment = Alignment.Center - ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text( - text = "필터 옵션을 불러올 수 없습니다", - color = PawKeyTheme.colors.gray500, - style = PawKeyTheme.typography.body14R, - textAlign = TextAlign.Center - ) - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = "다시 시도해보세요", - color = PawKeyTheme.colors.gray400, - style = PawKeyTheme.typography.caption12R, - textAlign = TextAlign.Center, - modifier = Modifier.noRippleClickable { - viewModel.loadFilterOptions() - } - ) - } - } - Spacer(modifier = Modifier.height(80.dp)) - } - } -} - -@Composable -private fun SuccessContent( - listState: TapListContract.TapListState, - viewModel: TapListViewModel, - modifier: Modifier = Modifier -) { - val filterOptions = listState.filterOptions!! - - val timeOptions = filterOptions.selectList?.find { it.selectName == "산책 소요 시간" } - Log.e("timeoptions", timeOptions.toString()) - val moodOptions = filterOptions.categoryList?.find { it.categoryName == "분위기" } - val dogFriendOptions = filterOptions.categoryList?.find { it.categoryName == "강아지 친구" } - val safetyOptions = filterOptions.categoryList?.find { it.categoryName == "안전" } - val convenienceOptions = filterOptions.categoryList?.find { it.categoryName == "편의성" } - val environmentOptions = filterOptions.categoryList?.find { it.categoryName == "환경" } - - LazyColumn( - modifier = modifier - .fillMaxWidth() - .padding(horizontal = 16.dp) - ) { - items(TapListContract.Options.sortOptions) { option -> - SortOptionItem( - title = option, - isSelected = listState.selectedSortOption == option, - onSelect = { viewModel.updateSortOption(option) } - ) - } - - item { - Spacer(modifier = Modifier.height(16.dp)) - Text( - text = stringResource(id = R.string.course_list_option_single), - color = PawKeyTheme.colors.green500, - style = PawKeyTheme.typography.caption12Sb1 - ) - } - - timeOptions?.let { selectList -> - item { - CourseOptionToggle( - title = selectList.selectName, - selecttitle = listState.selectedSortTime, - isExpanded = listState.isTimeExpanded, - onClick = { viewModel.toggleTimeExpanded() } - ) - } - - if (listState.isTimeExpanded) { - val options = filterOptions.selectList.flatMap { - it.options ?: emptyList() - } - - items(options) { option -> - SingleOptionItem( - title = option.selectText, - isSelected = listState.selectedSortTime == option.selectText, - onSelect = { viewModel.updateSortTime(option.selectText) } - ) - } - } - - item { - HorizontalDivider(color = PawKeyTheme.colors.gray50, thickness = 1.dp) - } - } - - moodOptions?.let { mood -> - item { - CourseOptionToggle( - title = mood.categoryName, - selecttitle = listState.selectedMood, - isExpanded = listState.isMoodExpanded, - onClick = { viewModel.toggleMoodExpanded() } - ) - } - - if (listState.isMoodExpanded) { - val options = mood.categoryOptions ?: emptyList() - - items(options) { option -> - SingleOptionItem( - title = option.categoryOptionText, - isSelected = listState.selectedMood == option.categoryOptionText, - onSelect = { viewModel.updateMood(option.categoryOptionText) } - ) - } - } - - item { - HorizontalDivider(color = PawKeyTheme.colors.gray50, thickness = 1.dp) - } - } - - dogFriendOptions?.let { dogFriend -> - item { - CourseOptionToggle( - title = dogFriend.categoryName, - selecttitle = listState.selectedDogFriend, - isExpanded = listState.isDogFriendExpanded, - onClick = { viewModel.toggleDogFriendExpanded() } - ) - } - - if (listState.isDogFriendExpanded) { - val options = dogFriend.categoryOptions ?: emptyList() - items(options) { option -> - SingleOptionItem( - title = option.categoryOptionText, - isSelected = listState.selectedDogFriend == option.categoryOptionText, - onSelect = { viewModel.updateDogFriend(option.categoryOptionText) } - ) - } - } - - item { - HorizontalDivider(color = PawKeyTheme.colors.gray50, thickness = 1.dp) - } - } - - item { - Spacer(modifier = Modifier.height(16.dp)) - Text( - text = stringResource(id = R.string.course_list_option_multiple), - color = PawKeyTheme.colors.green500, - style = PawKeyTheme.typography.caption12Sb1 - ) - } - - safetyOptions?.let { safety -> - item { - CourseOptionToggle( - title = safety.categoryName, - selecttitle = if (listState.selectedSafety.isNotEmpty()) "${listState.selectedSafety.size}개 선택" else "", - isExpanded = listState.isSafetyExpanded, - onClick = { viewModel.toggleSafetyExpanded() } - ) - } - - if (listState.isSafetyExpanded) { - val options = safety.categoryOptions ?: emptyList() - items(options) { option -> - MultipleOptionItem( - title = option.categoryOptionText, - isSelected = listState.selectedSafety.contains(option.categoryOptionText), - onSelect = { viewModel.updateSafety(option.categoryOptionText) } - ) - } - } - - item { - HorizontalDivider(color = PawKeyTheme.colors.gray50, thickness = 1.dp) - } - } - - convenienceOptions?.let { convenience -> - item { - CourseOptionToggle( - title = convenience.categoryName, - selecttitle = if (listState.selectedConvenience.isNotEmpty()) "${listState.selectedConvenience.size}개 선택" else "", - isExpanded = listState.isConvenienceExpanded, - onClick = { viewModel.toggleConvenienceExpanded() } - ) - } - - if (listState.isConvenienceExpanded) { - val options = convenience.categoryOptions ?: emptyList() - items(options) { option -> - MultipleOptionItem( - title = option.categoryOptionText, - isSelected = listState.selectedConvenience.contains(option.categoryOptionText), - onSelect = { viewModel.updateConvenience(option.categoryOptionText) } - ) - } - } - - item { - HorizontalDivider(color = PawKeyTheme.colors.gray50, thickness = 1.dp) - } - } - - environmentOptions?.let { environment -> - item { - CourseOptionToggle( - title = environment.categoryName, - selecttitle = if (listState.selectedEnvironment.isNotEmpty()) "${listState.selectedEnvironment.size}개 선택" else "", - isExpanded = listState.isEnvironmentExpanded, - onClick = { viewModel.toggleEnvironmentExpanded() } - ) - } - - if (listState.isEnvironmentExpanded) { - val options = environment.categoryOptions ?: emptyList() - items(options) { option -> - MultipleOptionItem( - title = option.categoryOptionText, - isSelected = listState.selectedEnvironment.contains(option.categoryOptionText), - onSelect = { viewModel.updateEnvironment(option.categoryOptionText) } - ) - } - } - - item { - HorizontalDivider(color = PawKeyTheme.colors.gray50, thickness = 1.dp) - } - } - - item { - Spacer(modifier = Modifier.height(80.dp)) - } - } -} - -@Composable -private fun BottomButtons( - viewModel: TapListViewModel, - onDismissRequest: () -> Unit -) { - Row( - horizontalArrangement = Arrangement.spacedBy(10.dp), - modifier = Modifier.padding(horizontal = 16.dp) - ) { - PawkeyButton( - text = stringResource(id = R.string.course_list_option_apply), - enabled = viewModel.isAllOptionsSelected(), - onClick = { - viewModel.applyOptions() - onDismissRequest() - }, - modifier = Modifier - .weight(0.7f) - ) - - IconButton( - onClick = { viewModel.resetAllOptions() }, - modifier = Modifier.size(56.dp).weight(0.3f) - ) { - Icon( - imageVector = ImageVector.vectorResource(R.drawable.ic_course_list_refresh), - contentDescription = "초기화", - tint = Color.Unspecified, - modifier = Modifier.size(56.dp) - ) - } - } -} - -@Composable -private fun SortOptionItem( - title: String, - isSelected: Boolean, - onSelect: () -> Unit, - modifier: Modifier = Modifier, -) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = modifier - .fillMaxWidth() - .padding(vertical = 20.dp) - .noRippleClickable { onSelect() } - ) { - Text( - text = title, - color = if (isSelected) PawKeyTheme.colors.green500 else PawKeyTheme.colors.black, - style = PawKeyTheme.typography.body14R - ) - - Spacer(modifier = Modifier.weight(1F)) - - Checkbox( - checked = isSelected, - onCheckedChange = { onSelect() }, - colors = CheckboxDefaults.colors( - checkedColor = PawKeyTheme.colors.green500, - checkmarkColor = PawKeyTheme.colors.white1, - uncheckedColor = PawKeyTheme.colors.gray100, - ), - modifier = Modifier.size(20.dp) - ) - } -} - -@Composable -private fun SingleOptionItem( - title: String, - isSelected: Boolean, - onSelect: () -> Unit, - modifier: Modifier = Modifier, -) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = modifier - .fillMaxWidth() - .padding(vertical = 20.dp) - .noRippleClickable { onSelect() } - ) { - Text( - text = title, - color = if (isSelected) PawKeyTheme.colors.green500 else PawKeyTheme.colors.black, - style = PawKeyTheme.typography.body14R - ) - - Spacer(modifier = Modifier.weight(1F)) - - Checkbox( - checked = isSelected, - onCheckedChange = { onSelect() }, - colors = CheckboxDefaults.colors( - checkedColor = PawKeyTheme.colors.green500, - checkmarkColor = PawKeyTheme.colors.white1, - uncheckedColor = PawKeyTheme.colors.gray100, - ), - modifier = Modifier.size(20.dp) - ) - } -} - -@Composable -private fun MultipleOptionItem( - title: String, - isSelected: Boolean, - onSelect: () -> Unit, - modifier: Modifier = Modifier, -) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = modifier - .fillMaxWidth() - .padding(vertical = 20.dp) - .noRippleClickable { onSelect() } - ) { - Text( - text = title, - color = if (isSelected) PawKeyTheme.colors.green500 else PawKeyTheme.colors.black, - style = PawKeyTheme.typography.body14R - ) - - Spacer(modifier = Modifier.weight(1F)) - - Checkbox( - checked = isSelected, - onCheckedChange = { onSelect() }, - colors = CheckboxDefaults.colors( - checkedColor = PawKeyTheme.colors.green500, - checkmarkColor = PawKeyTheme.colors.white1, - uncheckedColor = PawKeyTheme.colors.gray100, - ), - modifier = Modifier.size(20.dp) - ) - } -} - -@Composable -private fun CourseOptionToggle( - title: String, - selecttitle: String, - isExpanded: Boolean, - onClick: () -> Unit, - modifier: Modifier = Modifier, -) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = modifier - .fillMaxWidth() - .padding(vertical = 20.dp) - .noRippleClickable { onClick() } - ) { - Text( - text = title, - color = PawKeyTheme.colors.black, - style = PawKeyTheme.typography.body16Sb - ) - - Spacer(modifier = Modifier.weight(1F)) - - if (selecttitle.isNotEmpty()) { - Text( - text = selecttitle, - color = PawKeyTheme.colors.green500, - style = PawKeyTheme.typography.caption12Sb1, - modifier = Modifier.padding(end = 12.dp) - ) - } - - Icon( - imageVector = ImageVector.vectorResource( - if (isExpanded) R.drawable.ic_arrow_up else R.drawable.ic_arrow_down - ), - contentDescription = "toggle", - tint = Color.Unspecified, - modifier = Modifier.size(24.dp) - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/List/TabListScreen.kt b/app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/List/TabListScreen.kt deleted file mode 100644 index 85e41c28..00000000 --- a/app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/List/TabListScreen.kt +++ /dev/null @@ -1,242 +0,0 @@ -package com.paw.key.presentation.ui.course.entire.tab.map.List - -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.Row -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.Icon -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -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.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.res.vectorResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.paw.key.R -import com.paw.key.core.designsystem.component.CourseCard -import com.paw.key.core.designsystem.theme.PawKeyTheme -import com.paw.key.core.extension.noRippleClickable -import com.paw.key.presentation.ui.course.entire.tab.map.List.viewmodel.TapListViewModel - -@Preview(showBackground = true) -@Composable -private fun PreviewTabListScreen() { - PawKeyTheme { - } -} - -@Composable -fun TapListRoute( - navigateToDetail: (Int, Int) -> Unit, - modifier: Modifier = Modifier, - viewModel: TapListViewModel = hiltViewModel(), -) { - LaunchedEffect(Unit) { - viewModel.loadInitialPosts() - viewModel.loadFilterOptions() - } - - TabListScreen( - modifier = modifier, - navigateToDetail = { postId, routeId -> - navigateToDetail(postId, routeId) - }, - viewModel = viewModel - ) -} - -@Composable -fun TabListScreen( - navigateToDetail: (Int, Int) -> Unit, - modifier: Modifier = Modifier, - viewModel: TapListViewModel = hiltViewModel(), -) { - var showBottomSheet by remember { mutableStateOf(false) } - val listState by viewModel.state.collectAsStateWithLifecycle() - val filterValid = listState.isValid - - Column( - modifier = modifier - .fillMaxSize() - ) { - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .background(color = PawKeyTheme.colors.white1) - .padding(horizontal = 16.dp, vertical = 11.dp) - ) { - Icon( - imageVector = ImageVector.vectorResource(R.drawable.ic_course_optin_filter), - contentDescription = "filter", - tint = Color.Unspecified, - modifier = Modifier - .noRippleClickable { - showBottomSheet = true - } - ) - - if (filterValid) { - if (listState.selectedSortTime.isNotEmpty()) { - OptionChip(text = listState.selectedSortTime) - } - if (listState.selectedMood.isNotEmpty()) { - OptionChip(text = listState.selectedMood) - } - if (listState.selectedDogFriend.isNotEmpty()) { - OptionChip(text = listState.selectedDogFriend) - } - listState.selectedSafety.forEach { - OptionChip(text = it) - } - listState.selectedConvenience.forEach { - OptionChip(text = it) - } - listState.selectedEnvironment.forEach { - OptionChip(text = it) - } - } else { - OptionChip( - text = "선택한 옵션이 없어요", - isActionChip = true - ) - } - } - - LazyColumn( - modifier = Modifier - .fillMaxSize() - .background(PawKeyTheme.colors.white2) - .padding(bottom = 36.dp) - ) { - if (listState.isLoading) { - item { - Box( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - contentAlignment = Alignment.Center - ) { - CircularProgressIndicator( - color = PawKeyTheme.colors.green500 - ) - } - } - } else { - listState.postsResult?.let { postsResult -> - val posts = postsResult.posts - if (posts.isNotEmpty()) { - items( - items = posts, - key = { post -> post.postId } - ) { post -> - CourseCard( - title = post.title, - petName = post.writer.petName, - representativeImageUrl = post.representativeImageUrl, - petProfileImageUrl = post.writer.petProfileImageUrl, - descriptionTags = post.descriptionTags, - postId = post.postId, - createdAt = post.createdAt, - isMine = post.isMine, - isLiked = post.isLike, - onClickItem = { - navigateToDetail(post.postId, post.routeId) - }, - onClickLike = { newLikeState -> - viewModel.toggleLike(post.postId, newLikeState) - } - ) - } - } else { - item { - Box( - modifier = Modifier - .fillMaxWidth() - .padding(32.dp), - contentAlignment = Alignment.Center - ) { - Text( - text = "조건에 맞는 게시물이 없습니다", - style = PawKeyTheme.typography.body14R, - color = PawKeyTheme.colors.gray500 - ) - } - } - } - } ?: run { - item { - Box( - modifier = Modifier - .fillMaxWidth() - .padding(32.dp), - contentAlignment = Alignment.Center - ) { - Text( - text = "데이터를 불러오는 중...", - style = PawKeyTheme.typography.body14R, - color = PawKeyTheme.colors.gray500 - ) - } - } - } - } - } - - if (showBottomSheet) { - CourseOptionBottomSheet( - viewModel = viewModel, - onDismissRequest = { showBottomSheet = false } - ) - } - } -} - -@Composable -private fun OptionChip( - text: String, - modifier: Modifier = Modifier, - onClick: () -> Unit = {}, - isActionChip: Boolean = false, -) { - Box( - modifier = modifier - .background( - color = PawKeyTheme.colors.white1, - shape = RoundedCornerShape(60.dp) - ) - .border( - width = 1.dp, - color = if (isActionChip) PawKeyTheme.colors.gray200 else PawKeyTheme.colors.gray50, - shape = RoundedCornerShape(60.dp) - ) - .clickable(onClick = onClick) - .padding(horizontal = 14.dp, vertical = 7.dp) - ) { - Text( - text = text, - color = if (isActionChip) PawKeyTheme.colors.black else PawKeyTheme.colors.gray200, - style = PawKeyTheme.typography.caption12R - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/List/state/TapListContract.kt b/app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/List/state/TapListContract.kt deleted file mode 100644 index adb53f18..00000000 --- a/app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/List/state/TapListContract.kt +++ /dev/null @@ -1,50 +0,0 @@ -package com.paw.key.presentation.ui.course.entire.tab.map.List.state - -import androidx.compose.runtime.Immutable -import com.paw.key.domain.model.entity.filter.FilterEntity -import com.paw.key.domain.model.entity.filter.SelectOption -import com.paw.key.domain.model.entity.list.ListEntity - -class TapListContract { - @Immutable - data class TapListState( - val isLoading: Boolean = false, - val filterTopOptions : SelectOption? = null, - val filterOptions: FilterEntity? = null, - val postsResult: ListEntity? = null, - - val selectedSortOption: String = "", - - val selectedSortTimeStart : Int? = 0, - val selectedSortTimeEnd : Int? = 0, - val selectedSortTime : String = "", - val selectedMood: String = "", - val selectedDogFriend: String = "", - - val selectedSafety: List = emptyList(), - val selectedConvenience: List = emptyList(), - val selectedEnvironment: List = emptyList(), - - val isTimeExpanded: Boolean = false, - val isMoodExpanded: Boolean = false, - val isDogFriendExpanded: Boolean = false, - val isSafetyExpanded: Boolean = false, - val isConvenienceExpanded: Boolean = false, - val isEnvironmentExpanded: Boolean = false, - ) { - val isValid: Boolean - get() = selectedSortTime.isNotEmpty() - || selectedMood.isNotEmpty() - || selectedDogFriend.isNotEmpty() - || selectedSafety.isNotEmpty() - || selectedConvenience.isNotEmpty() - || selectedEnvironment.isNotEmpty() - } - - object Options { - val sortOptions = listOf( - "최신순", - "인기순" - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/List/viewmodel/TapListViewModel.kt b/app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/List/viewmodel/TapListViewModel.kt deleted file mode 100644 index 36931d8d..00000000 --- a/app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/List/viewmodel/TapListViewModel.kt +++ /dev/null @@ -1,422 +0,0 @@ -package com.paw.key.presentation.ui.course.entire.tab.map.List.viewmodel - -import android.util.Log -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.paw.key.core.util.PreferenceDataStore -import com.paw.key.data.dto.request.list.PostsListRequestDto -import com.paw.key.data.dto.request.list.TraitList -import com.paw.key.domain.repository.LikeRepository -import com.paw.key.domain.repository.filter.FilterOptionRepository -import com.paw.key.domain.repository.list.PostsListRepository -import com.paw.key.presentation.ui.course.entire.tab.map.List.state.TapListContract -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharedFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import javax.inject.Inject - -@HiltViewModel -class TapListViewModel @Inject constructor( - private val filterOptionRepository: FilterOptionRepository, - private val postsListRepository: PostsListRepository, - private val likeRepository: LikeRepository, -) : ViewModel() { - private val _state = MutableStateFlow(TapListContract.TapListState()) - val state: StateFlow = _state.asStateFlow() - - private val _sideEffect = MutableSharedFlow() - val sideEffect: SharedFlow = _sideEffect.asSharedFlow() - - private val userId = PreferenceDataStore.getUserId() - private val _likingPosts = MutableStateFlow>(emptySet()) - - // SideEffect 정의 - sealed class TapListSideEffect { - data class ShowToast(val message: String) : TapListSideEffect() - data class ShowSnackBar(val message: String) : TapListSideEffect() - } - - fun loadInitialPosts() { - viewModelScope.launch { - _state.update { it.copy(isLoading = true) } - println("POST 요청 시작 - 초기 데이터 로딩 (null 값들)") - - try { - val request = PostsListRequestDto( - durationStart = null, - durationEnd = null, - selectedOptions = listOf( - TraitList( - categoryId = null, - optionIds = null - ) - ) - ) - - println("요청 데이터: $request") - - postsListRepository.postList( - userId = userId.first(), - request = request - ).onSuccess { listEntity -> - val filteredPosts = listEntity.posts//.filter { !it.isMine } - println("응답 성공 - 내 게시물 제외된 posts 개수: ${filteredPosts.size}") - - _state.update { - it.copy( - isLoading = false, - postsResult = listEntity.copy(posts = filteredPosts) - ) - } - }.onFailure { exception -> - println("응답 실패: ${exception.message}") - _state.update { it.copy(isLoading = false) } - exception.printStackTrace() - } - } catch (e: Exception) { - println("예외 발생: ${e.message}") - _state.update { it.copy(isLoading = false) } - e.printStackTrace() - } - } - } - - fun loadFilterOptions() { - viewModelScope.launch { - filterOptionRepository.getFilterOptions(userId = userId.first()) - .onSuccess { filterEntity -> - _state.update { - it.copy( - filterOptions = filterEntity - ) - } - Log.e("filterOptions", filterEntity.toString()) - } - .onFailure { exception -> - exception.printStackTrace() - } - } - } - - fun updateSortOption(option: String) { - _state.update { it.copy(selectedSortOption = option) } - } - - fun updateSortTime(option: String) { - Log.e("updateSortTime", option) - when (option) { - "20분 이내" -> { - _state.update { - it.copy( - selectedSortTimeStart = 0, - selectedSortTimeEnd = 20 - ) - } - } - - "21~40분" -> { - _state.update { - it.copy( - selectedSortTimeStart = 20, - selectedSortTimeEnd = 40 - ) - } - } - - "41~60분" -> { - _state.update { - it.copy( - selectedSortTimeStart = 40, - selectedSortTimeEnd = 60 - ) - } - } - - "1시간 이상" -> { - _state.update { - it.copy( - selectedSortTimeStart = 60, - selectedSortTimeEnd = null - ) - } - } - - else -> { - _state.update { - it.copy( - selectedSortTimeStart = null, - selectedSortTimeEnd = null - ) - } - } - } - - _state.update { - it.copy( - selectedSortTime = if (it.selectedSortTime == option) "" else option - ) - } - } - - - fun toggleLike(postId: Int, newLikeState: Boolean) { - if (_likingPosts.value.contains(postId)) { - return - } - - viewModelScope.launch { - try { - _likingPosts.update { it + postId } - - val result = if (newLikeState) { - likeRepository.likeCourse(userId = userId.first(), postId = postId) - } else { - likeRepository.unlikeCourse(userId = userId.first(), postId = postId) - } - - result.onSuccess { - updateLocalLikeState(postId, newLikeState) - Log.d("TapListViewModel", "좋아요 상태 변경 성공: postId=$postId, isLiked=$newLikeState") - }.onFailure { exception -> - Log.e("TapListViewModel", "좋아요 상태 변경 실패: ${exception.message}") - } - } catch (e: Exception) { - Log.e("TapListViewModel", "toggleLike Exception: ${e.message}") - } finally { - _likingPosts.update { it - postId } - } - } - } - - private fun updateLocalLikeState(postId: Int, isLiked: Boolean) { - _state.update { currentState -> - val updatedPostsResult = currentState.postsResult?.let { postsResult -> - postsResult.copy( - posts = postsResult.posts.map { post -> - if (post.postId == postId) { - post.copy(isLike = isLiked) - } else { - post - } - } - ) - } - currentState.copy(postsResult = updatedPostsResult) - } - } - - - fun updateMood(option: String) { - _state.update { - it.copy( - selectedMood = if (it.selectedMood == option) "" else option - ) - } - } - - fun updateDogFriend(option: String) { - _state.update { - it.copy( - selectedDogFriend = if (it.selectedDogFriend == option) "" else option - ) - } - } - - fun updateSafety(option: String) { - _state.update { currentState -> - val newSafety = if (currentState.selectedSafety.contains(option)) { - currentState.selectedSafety.filter { it != option } - } else { - currentState.selectedSafety + option - } - currentState.copy(selectedSafety = newSafety) - } - } - - fun updateConvenience(option: String) { - _state.update { currentState -> - val newConvenience = if (currentState.selectedConvenience.contains(option)) { - currentState.selectedConvenience.filter { it != option } - } else { - currentState.selectedConvenience + option - } - currentState.copy(selectedConvenience = newConvenience) - } - } - - fun updateEnvironment(option: String) { - _state.update { currentState -> - val newEnvironment = if (currentState.selectedEnvironment.contains(option)) { - currentState.selectedEnvironment.filter { it != option } - } else { - currentState.selectedEnvironment + option - } - currentState.copy(selectedEnvironment = newEnvironment) - } - } - - fun toggleTimeExpanded() { - _state.update { it.copy(isTimeExpanded = !it.isTimeExpanded) } - } - - fun toggleMoodExpanded() { - _state.update { it.copy(isMoodExpanded = !it.isMoodExpanded) } - } - - fun toggleDogFriendExpanded() { - _state.update { it.copy(isDogFriendExpanded = !it.isDogFriendExpanded) } - } - - fun toggleSafetyExpanded() { - _state.update { it.copy(isSafetyExpanded = !it.isSafetyExpanded) } - } - - fun toggleConvenienceExpanded() { - _state.update { it.copy(isConvenienceExpanded = !it.isConvenienceExpanded) } - } - - fun toggleEnvironmentExpanded() { - _state.update { it.copy(isEnvironmentExpanded = !it.isEnvironmentExpanded) } - } - - fun resetAllOptions() { - _state.update { currentState -> - currentState.copy( - selectedSortOption = "", - selectedMood = "", - selectedDogFriend = "", - selectedSortTime = "", - selectedSafety = emptyList(), - selectedConvenience = emptyList(), - selectedEnvironment = emptyList(), - isMoodExpanded = false, - isDogFriendExpanded = false, - isSafetyExpanded = false, - isConvenienceExpanded = false, - isEnvironmentExpanded = false, - isTimeExpanded = false - ) - } - loadInitialPosts() - } - - fun applyOptions() { - val currentState = _state.value - - viewModelScope.launch { - _state.update { it.copy(isLoading = true) } - - try { - val selectedOptions = buildSelectedOptionsList(currentState) - - val request = PostsListRequestDto( - durationStart = state.value.selectedSortTimeStart, - durationEnd = state.value.selectedSortTimeEnd, - selectedOptions = selectedOptions.ifEmpty { null } - ) - - Log.e("TapListViewModel", "요청 데이터: $request") - - postsListRepository.postList( - userId = userId.first(), - request = request - ).onSuccess { listEntity -> - _state.update { - it.copy( - isLoading = false, - postsResult = listEntity - ) - } - println("필터링된 게시물 로드 성공: ${listEntity.posts.size}개") - }.onFailure { exception -> - _state.update { it.copy(isLoading = false) } - exception.printStackTrace() - println("필터링된 게시물 로드 실패: ${exception.message}") - } - } catch (e: Exception) { - _state.update { it.copy(isLoading = false) } - e.printStackTrace() - } - } - } - - private fun buildSelectedOptionsList(state: TapListContract.TapListState): List { - val selectedOptions = mutableListOf() - val filterOptions = state.filterOptions ?: return emptyList() - - - - filterOptions.categoryList?.forEach { category -> - val selectedOptionIds = mutableListOf() - - when (category.categoryName) { - "분위기" -> { - if (state.selectedMood.isNotEmpty()) { - category.categoryOptions?.find { it.categoryOptionText == state.selectedMood } - ?.let { selectedOptionIds.add(it.categoryOptionId) } - } - } - - "강아지 친구" -> { - if (state.selectedDogFriend.isNotEmpty()) { - category.categoryOptions?.find { it.categoryOptionText == state.selectedDogFriend } - ?.let { selectedOptionIds.add(it.categoryOptionId) } - } - } - - "안전" -> { - state.selectedSafety.forEach { selectedSafety -> - category.categoryOptions?.find { it.categoryOptionText == selectedSafety } - ?.let { selectedOptionIds.add(it.categoryOptionId) } - } - } - - "편의성" -> { - state.selectedConvenience.forEach { selectedConvenience -> - category.categoryOptions?.find { it.categoryOptionText == selectedConvenience } - ?.let { selectedOptionIds.add(it.categoryOptionId) } - } - } - - "환경" -> { - state.selectedEnvironment.forEach { selectedEnvironment -> - category.categoryOptions?.find { it.categoryOptionText == selectedEnvironment } - ?.let { selectedOptionIds.add(it.categoryOptionId) } - } - } - } - - if (selectedOptionIds.isNotEmpty()) { - selectedOptions.add( - TraitList( - categoryId = category.categoryId, - optionIds = selectedOptionIds - ) - ) - } - } - - return selectedOptions - } - - fun isAllOptionsSelected(): Boolean { - val currentState = _state.value - return currentState.selectedSortOption.isNotEmpty() || - currentState.selectedMood.isNotEmpty() || - currentState.selectedDogFriend.isNotEmpty() || - currentState.selectedSafety.isNotEmpty() || - currentState.selectedConvenience.isNotEmpty() || - currentState.selectedEnvironment.isNotEmpty() - } - - fun isFilterApplied(): Boolean { - return isAllOptionsSelected() - } -} \ 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 deleted file mode 100644 index 57fbe1e8..00000000 --- a/app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/TapMapScreen.kt +++ /dev/null @@ -1,191 +0,0 @@ -package com.paw.key.presentation.ui.course.entire.tab.map - -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.FloatingActionButton -import androidx.compose.material3.Icon -import androidx.compose.material3.Scaffold -import androidx.compose.material3.SnackbarHost -import androidx.compose.material3.SnackbarHostState -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.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.graphics.vector.ImageVector -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.res.vectorResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.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.LocationResult -import com.google.android.gms.location.LocationServices -import com.paw.key.R -import com.paw.key.core.designsystem.component.LoadingScreen -import com.paw.key.core.designsystem.theme.PawKeyTheme -import com.paw.key.core.util.UiState -import com.paw.key.core.extension.noRippleClickable -import com.paw.key.presentation.ui.course.entire.tab.map.viewmodel.TapMapViewModel - -@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 -> - - } - } - } - } - - LaunchedEffect(Unit) { - viewModel.loadInitialLocation() - } - - when (state.initialLocationState) { - is UiState.Loading -> { - LoadingScreen() - } - - is UiState.Success -> { - TapMapScreen( - paddingValues = paddingValues, - navigateUp = navigateUp, - navigateNext = navigateNext, - snackBarHostState = snackBarHostState, - regionName = state.currentRegion ?: "영등포구 여의도동", - 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, - regionName: String, - onClickTracking: () -> Unit, - modifier: Modifier = Modifier, -) { - Scaffold( - modifier = modifier - .padding(paddingValues), - snackbarHost = { - SnackbarHost( - hostState = snackBarHostState, - ) - } - ) { pv -> - Box( - modifier = Modifier - .padding(pv) - ) { - Text( - text = regionName, - modifier = Modifier - .align(Alignment.TopStart) - .padding(top = 18.dp, start = 18.dp) - .clip(RoundedCornerShape(36.dp)) - .background(Color.White) - .border( - width = 1.dp, - color = PawKeyTheme.colors.gray50, - shape = RoundedCornerShape(36.dp) - ) - .padding(horizontal = 16.dp, vertical = 8.dp), - color = PawKeyTheme.colors.green500, - style = PawKeyTheme.typography.body14Sb, - textAlign = TextAlign.Center, - ) - - Box( - modifier = modifier - .fillMaxWidth() - .navigationBarsPadding() - .align(Alignment.BottomCenter) - .padding(horizontal = 16.dp) - .padding(bottom = 100.dp), - contentAlignment = Alignment.BottomCenter - ) { - Text( - text = "산책 기록하기", - color = PawKeyTheme.colors.white1, - fontSize = 16.sp, - modifier = Modifier - .clip(RoundedCornerShape(12.dp)) - .background( - color = PawKeyTheme.colors.green500 - ) - .padding(vertical = 16.dp, horizontal = 20.dp) - .noRippleClickable { - navigateNext() - }, - textAlign = TextAlign.Center, - style = PawKeyTheme.typography.body16Sb - ) - - FloatingActionButton( - onClick = onClickTracking, - shape = CircleShape, - containerColor = PawKeyTheme.colors.white1, - modifier = Modifier - .align(Alignment.CenterEnd) - .size(44.dp) - ) { - Icon( - imageVector = ImageVector.vectorResource(R.drawable.ic_course_map_tap_location_on), - contentDescription = stringResource(R.string.course_tap_location_description), - tint = Color.Unspecified - ) - } - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/state/TapMapContract.kt b/app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/state/TapMapContract.kt deleted file mode 100644 index 1d7a4c0c..00000000 --- a/app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/state/TapMapContract.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.paw.key.presentation.ui.course.entire.tab.map.state - -import androidx.compose.runtime.Immutable -import com.naver.maps.geometry.LatLng -import com.paw.key.core.util.UiState - -@Immutable -data class TapMapState( - val initialLocationState : UiState = UiState.Loading, - val currentLocation: LatLng? = null, - val isLocationTracking: Boolean = false, - val isTrackingEnabled: Boolean = false, - - val currentRegion : String? = null, -) - -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 deleted file mode 100644 index 19175849..00000000 --- a/app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/viewmodel/TapMapViewModel.kt +++ /dev/null @@ -1,61 +0,0 @@ -package com.paw.key.presentation.ui.course.entire.tab.map.viewmodel - -import android.util.Log -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.naver.maps.geometry.LatLng -import com.paw.key.core.util.PreferenceDataStore -import com.paw.key.core.util.UiState -import com.paw.key.presentation.ui.course.entire.tab.map.state.TapMapSideEffect -import com.paw.key.presentation.ui.course.entire.tab.map.state.TapMapState -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.first -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -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 - - private val savedRegion = PreferenceDataStore.getActiveRegion() - - fun loadInitialLocation() { - viewModelScope.launch { - Log.e("TapMapViewModel", "savedRegion: $savedRegion") - _state.update { - it.copy( - currentRegion = savedRegion.first(), - initialLocationState = UiState.Success( - LatLng(37.4979000000, 127.0276000000) - ) - ) - } - } - } - - 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 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 deleted file mode 100644 index 7e163d72..00000000 --- a/app/src/main/java/com/paw/key/presentation/ui/course/entire/viewmodel/EntireCourseViewModel.kt +++ /dev/null @@ -1,30 +0,0 @@ -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 diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/sharedwalk/complete/SharedWalkCompletionScreen.kt b/app/src/main/java/com/paw/key/presentation/ui/course/sharedwalk/complete/SharedWalkCompletionScreen.kt deleted file mode 100644 index 61faa8e3..00000000 --- a/app/src/main/java/com/paw/key/presentation/ui/course/sharedwalk/complete/SharedWalkCompletionScreen.kt +++ /dev/null @@ -1,181 +0,0 @@ -package com.paw.key.presentation.ui.course.sharedwalk.complete - -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.PaddingValues -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.asImageBitmap -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import coil.compose.AsyncImage -import coil.request.ImageRequest -import com.paw.key.R -import com.paw.key.core.designsystem.component.PawkeyButton -import com.paw.key.core.designsystem.component.TopBar -import com.paw.key.core.designsystem.theme.PawKeyTheme -import com.paw.key.presentation.ui.course.walk.formatDistance -import com.paw.key.presentation.ui.course.walk.formatTime -import com.paw.key.presentation.ui.course.walkcomplete.component.WalkCompleteHeader -import com.paw.key.presentation.ui.course.walkcomplete.component.WalkCompletionRecordRow -import com.paw.key.presentation.ui.course.walkcomplete.viewmodel.WalkCompleteViewModel - -@Composable -fun SharedWalkCompletionRoute( - paddingValues: PaddingValues, - navigateUp: () -> Unit, - navigateNext: (Int, Int) -> Unit, - routeId: Int, - pageId: Int, - modifier: Modifier = Modifier, - viewModel: WalkCompleteViewModel = hiltViewModel(), - isSharedWalk : Boolean = true -) { - val state by viewModel.state.collectAsStateWithLifecycle() - - val walkRecordList = listOf(R.string.course_record_distance, R.string.course_record_time, R.string.course_record_step) - - LaunchedEffect(Unit) { - viewModel.loadWalkResult() - // 디버깅용 - viewModel.debugRepositoryState() - } - - SharedWalkCompletionScreen( - paddingValues = paddingValues, - navigateUp = navigateUp, - navigateNext = { - navigateNext(routeId, pageId) - }, - bitmap = state.bitmap, - walkRecordList = walkRecordList, - totalDistance = state.totalDistance, - totalTime = state.totalTime, - totalSteps = state.totalSteps, - isSharedWalk = isSharedWalk, - modifier = modifier, - ) -} - -@Composable -fun SharedWalkCompletionScreen( - paddingValues: PaddingValues, - navigateUp: () -> Unit, - navigateNext: () -> Unit, - bitmap: Bitmap?, - walkRecordList: List, - totalDistance: Float, - totalTime: Long, - totalSteps: Int, - isSharedWalk: Boolean, - modifier: Modifier = Modifier, -) { - Column ( - modifier = modifier - .background(PawKeyTheme.colors.white2), - horizontalAlignment = Alignment.CenterHorizontally, - ){ - TopBar( - title = "산책 완료", - onBackClick = navigateUp, - modifier = Modifier - .background(PawKeyTheme.colors.white1), - isBackVisible = false - ) - - Text( - text = "산책 결과를 확인해보세요.", - color = PawKeyTheme.colors.black, - style = PawKeyTheme.typography.head18Sb, - modifier = Modifier - .padding(top = 36.dp, bottom = 16.dp) - .padding(horizontal = 16.dp) - .fillMaxWidth() - ) - - Column ( - modifier = modifier - .padding(start = 16.dp, end = 16.dp) - .background( - color = PawKeyTheme.colors.white1, - shape = RoundedCornerShape(12.dp) - ) - ) { - // Todo : 사진 받아올 곳 - /*WalkCompleteHeader( - bitmap = null, - modifier = Modifier - .padding(top = 16.dp, start = 16.dp, end = 16.dp) - )*/ - - /*bitmap?.asImageBitmap()?.let { - Image( - bitmap = it, - contentDescription = "My Image", - modifier = Modifier - .padding(start = 8.dp, end = 8.dp) - .padding(top = 12.dp) - .clip(RoundedCornerShape(8.dp)) - ) - }*/ - AsyncImage( - model = ImageRequest.Builder(LocalContext.current) - .data("https://pawkey-bucket.s3.ap-northeast-2.amazonaws.com/route/69a9c758-csnapshot.jpg") - .crossfade(true) - .build(), - contentDescription = "My Image", - modifier = Modifier - .padding(start = 8.dp, end = 8.dp) - .padding(top = 12.dp) - ) - - HorizontalDivider( - thickness = 1.dp, - color = PawKeyTheme.colors.gray50, - modifier = Modifier - .fillMaxWidth() - .padding(top = 15.dp, bottom = 10.dp) - ) - - WalkCompletionRecordRow( - totalDistance = formatDistance(totalDistance), - totalTime = formatTime(totalTime), - currentSteps = totalSteps, - modifier = Modifier - .fillMaxWidth() - ) - } - - val buttonTextRes = if (isSharedWalk) { - R.string.course_shared_complete_button_text - } else { - R.string.course_complete_button_text - } - - Spacer(modifier = Modifier.weight(1f)) - - PawkeyButton( - text = stringResource(buttonTextRes), - onClick = navigateNext, - enabled = true, - modifier = Modifier - .fillMaxWidth() - .padding(start = 16.dp, end = 16.dp, bottom = 60.dp) - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/sharedwalk/complete/navigation/SharedWalkCompletionRoute.kt b/app/src/main/java/com/paw/key/presentation/ui/course/sharedwalk/complete/navigation/SharedWalkCompletionRoute.kt deleted file mode 100644 index 9823cd87..00000000 --- a/app/src/main/java/com/paw/key/presentation/ui/course/sharedwalk/complete/navigation/SharedWalkCompletionRoute.kt +++ /dev/null @@ -1,49 +0,0 @@ -package com.paw.key.presentation.ui.course.sharedwalk.complete.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 androidx.navigation.toRoute -import com.paw.key.core.navigation.Route -import com.paw.key.presentation.ui.course.sharedwalk.complete.SharedWalkCompletionRoute -import com.paw.key.presentation.ui.course.sharedwalk.sharedroute.SharedWalkCourseRoute -import com.paw.key.presentation.ui.course.walk.navigation.WalkCourse -import kotlinx.serialization.Serializable - -fun NavController.navigateSharedWalkCompletion( - pageId: Int, - routeId: Int, - navOptions: NavOptions?, -) { - navigate(SharedWalkCompletion(routeId, pageId), navOptions) -} - -@RequiresApi(Build.VERSION_CODES.Q) -fun NavGraphBuilder.sharedWalkCompletionNavGraph( - paddingValues: PaddingValues, - navigateUp: () -> Unit, - navigateNext: (Int, Int) -> Unit, - snackBarHostState: SnackbarHostState, -) { - composable { backStackEntry -> - val ids = backStackEntry.toRoute() - - SharedWalkCompletionRoute( - paddingValues = paddingValues, - navigateUp = navigateUp, - navigateNext = { routeId, pageId -> - navigateNext(routeId, pageId) - }, - routeId = ids.routeId, - pageId = ids.pageId - ) - } -} - -@Serializable -data class SharedWalkCompletion(val routeId: Int, val pageId: Int) : Route \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/sharedwalk/review/SharedWalkReviewScreen.kt b/app/src/main/java/com/paw/key/presentation/ui/course/sharedwalk/review/SharedWalkReviewScreen.kt deleted file mode 100644 index 0900c687..00000000 --- a/app/src/main/java/com/paw/key/presentation/ui/course/sharedwalk/review/SharedWalkReviewScreen.kt +++ /dev/null @@ -1,237 +0,0 @@ -package com.paw.key.presentation.ui.course.sharedwalk.review - -import android.Manifest -import android.net.Uri -import android.os.Build -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.PickVisualMediaRequest -import androidx.activity.result.contract.ActivityResultContracts -import androidx.annotation.RequiresApi -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.heightIn -import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material3.HorizontalDivider -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.rememberCoroutineScope -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.compose.LocalLifecycleOwner -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.flowWithLifecycle -import coil.compose.AsyncImage -import com.paw.key.R -import com.paw.key.core.designsystem.component.PawkeyButton -import com.paw.key.core.designsystem.component.SubChip -import com.paw.key.core.designsystem.component.TopBar -import com.paw.key.core.designsystem.theme.PawKeyTheme -import com.paw.key.core.util.PreferenceDataStore -import com.paw.key.presentation.ui.course.sharedwalk.review.state.SharedWalkReviewSideEffect -import com.paw.key.presentation.ui.course.sharedwalk.review.viewmodel.SharedWalkReviewViewModel -import com.paw.key.presentation.ui.course.walkreview.WalkReviewCategoryUiModel -import com.paw.key.presentation.ui.course.walkreview.component.WalkReviewDialog -import com.paw.key.presentation.ui.course.walkreview.component.WalkReviewFeedbackForm -import com.paw.key.presentation.ui.course.walkreview.component.WalkReviewFeedbackHeader -import com.paw.key.presentation.ui.course.walkreview.component.WalkReviewImageRow -import com.paw.key.presentation.ui.course.walkreview.component.WalkReviewInfoHolder -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.launch - -@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) -@Composable -fun SharedWalkReviewRoute( - navigateUp: () -> Unit, - navigateNext: () -> Unit, - routeId : Int, - pageId : Int, - snackBarHostState: SnackbarHostState, - modifier: Modifier = Modifier, - viewModel: SharedWalkReviewViewModel = hiltViewModel(), - isSharedWalk : Boolean = true -) { - val state by viewModel.state.collectAsStateWithLifecycle() - val scope = rememberCoroutineScope() - val isValid = state.isValidForm - val userId = PreferenceDataStore.getUserId() - - val lifecycleOwner = LocalLifecycleOwner.current - - LaunchedEffect(Unit) { - viewModel.getSharedWalkReviewCategory(userId.first()) - viewModel.getSharedWalkReviewInfo( - routeId = routeId, - userId = userId.first() - ) - } - - LaunchedEffect(viewModel.sideEffect, lifecycleOwner) { - viewModel.sideEffect.flowWithLifecycle(lifecycleOwner.lifecycle) - .collect { sideEffect -> - when (sideEffect) { - is SharedWalkReviewSideEffect.ShowSnackBar -> snackBarHostState.showSnackbar( - sideEffect.message - ) - - SharedWalkReviewSideEffect.NavigateNext -> navigateNext() - SharedWalkReviewSideEffect.NavigateUp -> navigateUp() - } - } - } - - SharedWalkReviewScreen( - navigateUp = navigateUp, - navigateNext = navigateNext, - onClickFeedback = { index, content -> - viewModel.onClickFeedback(index, content) - }, - isDialogVisible = state.isDialogVisible, - isFormValid = isValid, - isSharedWalk = isSharedWalk, - petName = state.petName, - feedbackList = state.categoryList, - onClickSharedReview = { - // 다이얼로그용 - scope.launch { - viewModel.onClickSharedReview( - routeId = routeId, - userId = userId.first() - ) - } - }, - modifier = modifier, - ) -} - -@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) -@Composable -fun SharedWalkReviewScreen( - navigateUp: () -> Unit, - navigateNext: () -> Unit, - onClickSharedReview : () -> Unit, - onClickFeedback : (Int, Int) -> Unit, - isDialogVisible : Boolean, - isFormValid : Boolean, - isSharedWalk : Boolean, - petName : String, - feedbackList : List, - modifier: Modifier = Modifier, -) { - Column ( - modifier = modifier - .fillMaxSize() - ) { - TopBar( - title = "산책 기록하기", - onBackClick = navigateUp, - modifier = Modifier - .background(PawKeyTheme.colors.white1), - isBackVisible = true - ) - - HorizontalDivider( - thickness = 1.dp, - color = PawKeyTheme.colors.gray50, - modifier = Modifier - .fillMaxWidth() - ) - - LazyColumn ( - modifier = modifier - .background(PawKeyTheme.colors.white1) - .padding(bottom = 16.dp) - ) { - item { - WalkReviewFeedbackHeader( - petName = petName, - modifier = Modifier - .padding(top = 12.dp, start = 16.dp, end = 16.dp) - .background(PawKeyTheme.colors.white1) - ) - } - - item { - val emoji = listOf( - "\uD83D\uDE0C", - "\uD83D\uDC36", - "\uD83D\uDEB8", - "\uD83E\uDDFA", - "\uD83C\uDF3F" - ) - - feedbackList.forEachIndexed { index, category -> - WalkReviewFeedbackForm( - icon = R.drawable.ic_walk_review_location, - title = "${emoji[index]} ${category.categoryDescription}", - selectedFeedbackItems = category.options.filter { it.isSelected }.map { it.optionText }, - feedbackList = category.options.map { it.optionText }, - onClickFeedback = { selectedText -> - val selectedOption = category.options.find { it.optionText == selectedText } - if (selectedOption != null) { - onClickFeedback(category.categoryId, selectedOption.optionId) - } - }, - modifier = Modifier - .padding(top = 12.dp, bottom = 12.dp, start = 16.dp, end = 16.dp), - selectedFeedbackItem = category.options.find { it.isSelected }?.optionText - ) - } - } - - item { - HorizontalDivider( - thickness = 10.dp, - color = PawKeyTheme.colors.gray50, - modifier = Modifier - .fillMaxWidth() - .padding(top = 12.dp, bottom = 12.dp) - ) - } - - item { - val buttonTextRes = if (isSharedWalk) { - // 후기 - R.string.course_review_shared_button - } else { - R.string.course_review_shared_all_button - } - - // Todo : 후기 남기고 course의 리스트로 이동 - 애니메이션 - PawkeyButton( - text = stringResource(buttonTextRes), - onClick = { - onClickSharedReview() - }, - enabled = isFormValid, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp) - ) - } - } - - if (isDialogVisible && isSharedWalk) { - WalkReviewDialog( - onClickOk = { - // 리스트로 이동 - navigateNext() - } - ) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/sharedwalk/review/navigation/SharedWalkReviewNavigation.kt b/app/src/main/java/com/paw/key/presentation/ui/course/sharedwalk/review/navigation/SharedWalkReviewNavigation.kt deleted file mode 100644 index 58943955..00000000 --- a/app/src/main/java/com/paw/key/presentation/ui/course/sharedwalk/review/navigation/SharedWalkReviewNavigation.kt +++ /dev/null @@ -1,51 +0,0 @@ -package com.paw.key.presentation.ui.course.sharedwalk.review.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 androidx.navigation.toRoute -import com.paw.key.core.navigation.Route -import com.paw.key.presentation.ui.course.sharedwalk.complete.SharedWalkCompletionRoute -import com.paw.key.presentation.ui.course.sharedwalk.review.SharedWalkReviewRoute -import com.paw.key.presentation.ui.course.walk.navigation.WalkCourse -import kotlinx.serialization.Serializable - -fun NavController.navigateSharedWalkReview( - pageId: Int, - routeId: Int, - navOptions: NavOptions?, -) { - navigate(SharedWalkReview( - routeId = routeId, - pageId = pageId - ), navOptions) -} - -@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) -fun NavGraphBuilder.sharedWalkReviewNavGraph( - paddingValues: PaddingValues, - navigateUp: () -> Unit, - navigateNext: () -> Unit, - snackBarHostState: SnackbarHostState, -) { - composable { backStackEntry -> - val ids = backStackEntry.toRoute() - - SharedWalkReviewRoute( - navigateUp = navigateUp, - navigateNext = navigateNext, - routeId = ids.routeId, - pageId = ids.pageId, - snackBarHostState = snackBarHostState, - ) - } -} - -@Serializable -data class SharedWalkReview(val routeId : Int, val pageId : Int) : Route \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/sharedwalk/review/state/SharedWalkReviewContract.kt b/app/src/main/java/com/paw/key/presentation/ui/course/sharedwalk/review/state/SharedWalkReviewContract.kt deleted file mode 100644 index fab8d2f4..00000000 --- a/app/src/main/java/com/paw/key/presentation/ui/course/sharedwalk/review/state/SharedWalkReviewContract.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.paw.key.presentation.ui.course.sharedwalk.review.state - -import androidx.compose.runtime.Immutable -import com.paw.key.presentation.ui.course.walkreview.WalkReviewCategoryUiModel -import kotlinx.collections.immutable.PersistentList -import kotlinx.collections.immutable.persistentListOf - -@Immutable -data class SharedWalkReviewState( - val location : String = "", - val date : String = "", - val time : String = "", - - val petName : String = "포비", - - val isDialogVisible : Boolean = false, - - val categoryList: List = emptyList(), - - val tags : PersistentList = persistentListOf(), -){ - val isValidForm get() = categoryList.all { category -> - category.options.any { it.isSelected } - } -} - -sealed class SharedWalkReviewSideEffect { - data class ShowSnackBar(val message: String) : SharedWalkReviewSideEffect() - data object NavigateUp: SharedWalkReviewSideEffect() - data object NavigateNext: SharedWalkReviewSideEffect() -} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/sharedwalk/review/viewmodel/SharedWalkReviewViewModel.kt b/app/src/main/java/com/paw/key/presentation/ui/course/sharedwalk/review/viewmodel/SharedWalkReviewViewModel.kt deleted file mode 100644 index 3ad85131..00000000 --- a/app/src/main/java/com/paw/key/presentation/ui/course/sharedwalk/review/viewmodel/SharedWalkReviewViewModel.kt +++ /dev/null @@ -1,139 +0,0 @@ -package com.paw.key.presentation.ui.course.sharedwalk.review.viewmodel - -import android.util.Log -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.paw.key.domain.model.entity.sharedwalk.SharedWalkReviewCategory -import com.paw.key.domain.model.entity.sharedwalk.SharedWalkReviewEntity -import com.paw.key.domain.model.entity.walkreview.WalkReviewRecordCategory -import com.paw.key.domain.repository.sharedwalk.SharedWalkRepository -import com.paw.key.domain.repository.walkreview.WalkReviewRepository -import com.paw.key.presentation.ui.course.sharedwalk.review.state.SharedWalkReviewSideEffect -import com.paw.key.presentation.ui.course.sharedwalk.review.state.SharedWalkReviewState -import com.paw.key.presentation.ui.course.walkreview.util.toUiModel -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.collections.immutable.toPersistentList -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharedFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import javax.inject.Inject - -@HiltViewModel -class SharedWalkReviewViewModel @Inject constructor( - private val repository: WalkReviewRepository, - private val sharedRepository : SharedWalkRepository -) : ViewModel() { - private val _state = MutableStateFlow(SharedWalkReviewState()) - val state : StateFlow - get() = _state.asStateFlow() - - private val _sideEffect = MutableSharedFlow() - val sideEffect : SharedFlow - get() = _sideEffect.asSharedFlow() - - fun postSharedWalkReview(userId: Int, routeId: Int) { - viewModelScope.launch { - val categoryList = state.value.categoryList.map { category -> - SharedWalkReviewCategory( - reviewCategoryId = category.categoryId, - selectedReviewOptionIds = category.options.filter { it.isSelected }.map { it.optionId } - ) - } - - val review = SharedWalkReviewEntity ( - routeId = routeId, - selectedReviewSetList = categoryList, - ) - Log.e("sharedWalkReview", "${routeId}, ${userId}") - Log.d("SharedWalkReviewSideEffect", "리뷰 : $review") - sharedRepository.postSharedWalkReviewRegister( - userId = userId, - review = review - ).onSuccess { - _sideEffect.emit(SharedWalkReviewSideEffect.NavigateNext) - Log.d("SharedWalkReviewSideEffect", "리뷰 등록 성공!") - }.onFailure { - _sideEffect.emit(SharedWalkReviewSideEffect.ShowSnackBar("리뷰 등록 실패!")) - Log.e("SharedWalkReviewSideEffect", "리뷰 등록 실패!") - } - } - } - - fun getSharedWalkReviewCategory(userId: Int) { - viewModelScope.launch { - repository.getWalkReviewCategory( - userId = userId - ).onSuccess { - _state.update { currentState -> - currentState.copy( - categoryList = it.categoryList.map { entity -> entity.toUiModel() } - ) - } - Log.d("SharedWalk", "카테고리 가져오기 성공!") - }.onFailure { - _sideEffect.emit(SharedWalkReviewSideEffect.ShowSnackBar("카테고리 가져오기 실패!")) - Log.e("SharedWalk", "카테고리 가져오기 실패!") - } - } - } - - fun getSharedWalkReviewInfo(routeId: Int, userId: Int) { - viewModelScope.launch { - repository.getWalkReviewInfo( - userId = userId, - routeId = routeId - ).onSuccess { - _state.update { currentState -> - currentState.copy( - location = it.routeDto.locationDescription, - time = it.routeDto.dateDescription, - tags = it.routeDto.descriptionTags.toPersistentList(), - petName = it.petName - ) - } - Log.d("SharedWalkReviewSideEffect", "산책 정보 가져오기 성공!") - }.onFailure { - _sideEffect.emit(SharedWalkReviewSideEffect.ShowSnackBar("산책 정보 가져오기 실패!")) - Log.e("SharedWalkReviewSideEffect", "산책 정보 가져오기 실패!") - } - } - } - - fun onClickFeedback(categoryId: Int, optionId: Int) { - val updatedCategories = state.value.categoryList.map { category -> - if (category.categoryId == categoryId) { - val updatedOptions = category.options.map { option -> - if (option.optionId == optionId) { - option.copy(isSelected = !option.isSelected) - } else { - option - } - } - category.copy(options = updatedOptions) - } else { - category - } - } - - _state.update { it.copy(categoryList = updatedCategories) } - } - - - fun onClickSharedReview(userId: Int, routeId: Int) { - _state.update { - it.copy( - isDialogVisible = true - ) - } - - postSharedWalkReview( - userId = userId, - routeId = routeId - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/sharedwalk/sharedroute/SharedWalkCourseScreen.kt b/app/src/main/java/com/paw/key/presentation/ui/course/sharedwalk/sharedroute/SharedWalkCourseScreen.kt deleted file mode 100644 index 5bef3da8..00000000 --- a/app/src/main/java/com/paw/key/presentation/ui/course/sharedwalk/sharedroute/SharedWalkCourseScreen.kt +++ /dev/null @@ -1,657 +0,0 @@ -package com.paw.key.presentation.ui.course.sharedwalk.sharedroute - -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.layout.size -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.FloatingActionButton -import androidx.compose.material3.Icon -import androidx.compose.material3.Scaffold -import androidx.compose.material3.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.graphics.vector.ImageVector -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.vectorResource -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.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.graphics.gl.GLSurfaceView -import com.naver.maps.geometry.LatLng -import com.paw.key.R -import com.paw.key.core.designsystem.component.LoadingScreen -import com.paw.key.core.designsystem.component.PawkeyButton -import com.paw.key.core.designsystem.theme.PawKeyTheme -import com.paw.key.core.util.UiState -import com.paw.key.core.extension.noRippleClickable -import com.paw.key.presentation.ui.course.sharedwalk.sharedroute.state.SharedWalkCourseSideEffect -import com.paw.key.presentation.ui.course.sharedwalk.sharedroute.viewmodel.SharedWalkCourseViewModel -import com.paw.key.presentation.ui.course.walk.component.WalkRecordItem -import com.paw.key.presentation.ui.course.walk.component.WalkRecordRow -import com.paw.key.presentation.ui.course.walk.formatDistance -import com.paw.key.presentation.ui.course.walk.formatTime -import com.paw.key.presentation.ui.course.walk.state.WalkCourseContract.WalkCourseRecord.DistanceRecord -import com.paw.key.presentation.ui.course.walk.state.WalkCourseContract.WalkCourseRecord.StepsRecord -import com.paw.key.presentation.ui.course.walk.state.WalkCourseContract.WalkCourseRecord.TimeRecord -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.delay -import kotlinx.coroutines.suspendCancellableCoroutine -import java.nio.IntBuffer -import javax.microedition.khronos.egl.EGL10 -import javax.microedition.khronos.egl.EGLContext -import javax.microedition.khronos.opengles.GL10 -import kotlin.coroutines.resumeWithException - - -@RequiresApi(Build.VERSION_CODES.Q) -@Composable -fun SharedWalkCourseRoute( - paddingValues: PaddingValues, - navigateUp: () -> Unit, - navigateNext: (Int, Int) -> Unit, - routeId : Int, - pageId : Int, - snackBarHostState: SnackbarHostState, - modifier: Modifier = Modifier, - isSharedWalk : Boolean = true, - viewModel: SharedWalkCourseViewModel = hiltViewModel(), -) { - val state by viewModel.state.collectAsStateWithLifecycle() - val scope = rememberCoroutineScope() - val lifecycleOwner = LocalLifecycleOwner.current - val context = LocalContext.current - - LaunchedEffect(Unit) { - viewModel.getWalkSharedTrack(routeId) - } - - 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) { - } - } - } - - LaunchedEffect(Unit) { - val currentLocation = sharedGetCurrentLocation( - 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 SharedWalkCourseSideEffect.ShowSnackBar -> snackBarHostState.showSnackbar( - sideEffect.message - ) - - SharedWalkCourseSideEffect.NavigateNext -> navigateNext(routeId, pageId) - SharedWalkCourseSideEffect.NavigateUp -> navigateUp() - } - } - } - - - /*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 -> { - SharedWalkCourseScreen( - paddingValues = paddingValues, - navigateUp = navigateUp, - navigateNext = { - navigateNext(routeId, pageId) - }, - scope = scope, - snackBarHostState = snackBarHostState, - totalDistance = formatDistance, - isSharedWalk = isSharedWalk, - currentSteps = state.steps, - totalTime = formattedTotalTime, - isTracking = state.isRecording, // true = 잠시 중단, false = dim - onClickTracking = { - viewModel.updateState { - copy( - isTrackingEnabled = !this.isTrackingEnabled - ) - } - }, - onPauseTracking = { - viewModel.onStopTrackingEvent() - 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 SharedWalkCourseScreen( - paddingValues: PaddingValues, - navigateUp: () -> Unit, - navigateNext: () -> Unit, - scope: CoroutineScope, - snackBarHostState: SnackbarHostState, - totalDistance: String, - currentSteps: Long, - totalTime: String, - isSharedWalk: Boolean, - isTracking: Boolean, // 버튼 상태 - onClickTracking: () -> Unit, - onStartTracking: () -> Unit, // 계속하기 - onPauseTracking: () -> Unit, // 잠시 중단 - onStopTracking: () -> Unit, // 종료하기 - onCaptured: (Bitmap?) -> Unit, - modifier: Modifier = Modifier, -) { - Scaffold( - modifier = modifier - .padding(paddingValues), - snackbarHost = { - - } - ) { pv -> - Box( - modifier = Modifier - .padding(pv) - ) { - - 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 - ) { - if (!isSharedWalk) { - Text( - text = "산책이 중단되었어요!", - textAlign = TextAlign.Center, - style = PawKeyTheme.typography.head24B, - color = PawKeyTheme.colors.white1, - modifier = Modifier.fillMaxWidth() - ) - - Text( - text = "산책을 정말 종료하시겠어요?", - fontSize = 12.sp, - textAlign = TextAlign.Center, - style = PawKeyTheme.typography.body16M, - color = PawKeyTheme.colors.white2, - modifier = Modifier - .padding(top = 12.dp) - .fillMaxWidth() - ) - } else { - Text( - text = "산책이 중단되었어요.", - textAlign = TextAlign.Center, - style = PawKeyTheme.typography.head22B, - color = PawKeyTheme.colors.white1, - modifier = Modifier.fillMaxWidth() - ) - - Text( - text = "산책을 정말 종료하시겠어요?", - fontSize = 12.sp, - textAlign = TextAlign.Center, - style = PawKeyTheme.typography.body16M, - color = PawKeyTheme.colors.white2, - modifier = Modifier.fillMaxWidth() - ) - } - } - } - - Column ( - modifier = Modifier - .fillMaxWidth() - .padding(bottom = 16.dp, start = 16.dp, end = 16.dp) - .navigationBarsPadding(), - horizontalAlignment = Alignment.CenterHorizontally - ) { - if (isTracking) { - Row ( - modifier = Modifier - .fillMaxWidth() - ) { - Spacer(modifier = Modifier.weight(1f)) - - FloatingActionButton( - shape = CircleShape, - onClick = onClickTracking, - containerColor = PawKeyTheme.colors.white1, - modifier = Modifier - .size(44.dp) - ) { - Icon( - imageVector = ImageVector.vectorResource(R.drawable.ic_course_map_tap_location_on), - contentDescription = "내 위치",//stringResource(id = R.string.lo) - tint = Color.Unspecified - ) - } - } - - PawkeyButton( - text = "중지하기", - enabled = true, - onClick = { - - }, - modifier = Modifier - .padding(top = 16.dp) - .padding(bottom = 44.dp) - ) - } 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 = PawKeyTheme.colors.green500, - shape = RoundedCornerShape(8.dp) - ) - .padding(horizontal = 24.dp, vertical = 16.dp), - color = PawKeyTheme.colors.green500, - style = PawKeyTheme.typography.body16Sb - ) - - Spacer(modifier = Modifier.weight(1f)) - - Text( - text = "산책 종료하기", - modifier = Modifier - .clip(RoundedCornerShape(8.dp)) - .background( - color = PawKeyTheme.colors.green500, - shape = RoundedCornerShape(8.dp) - ) - .noRippleClickable { - navigateNext() - onStopTracking() - } - .padding(horizontal = 28.dp, vertical = 16.dp), - color = PawKeyTheme.colors.white1, - style = PawKeyTheme.typography.body16Sb - ) - } - } - } - } - } - } -} - -fun sharedCaptureMapToBitmap(surfaceView: GLSurfaceView, onCaptured: (Bitmap?) -> Unit) { - surfaceView.queueEvent { - val egl = EGLContext.getEGL() as EGL10 - val gl = egl.eglGetCurrentContext().gl as GL10 - - // 원하는 최종 크기를 먼저 계산 - val screenWidth = surfaceView.context.resources.displayMetrics.widthPixels - val contentWidth = (screenWidth - 32) - val targetHeight = (156 * surfaceView.context.resources.displayMetrics.density).toInt() - - val bitmap = sharedCreateBitmapFromGLSurface(0, 0, surfaceView.width, surfaceView.height, gl, contentWidth, targetHeight) - onCaptured(bitmap) - } -} - -fun sharedCreateBitmapFromGLSurface(x: Int, y: Int, w: Int, h: Int, gl: GL10, targetWidth: Int, targetHeight: Int): 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) - - val targetAspectRatio = 16f / 11f - - 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 sharedGetCurrentLocation( - 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}") -} - -@Preview(showBackground = true) -@Composable -private fun SharedWalkCourseScreenPreview() { - PawKeyTheme { - Row ( - modifier = Modifier - .background(Color.White, shape = RoundedCornerShape(12.dp)) - .border( - width = 1.dp, - color = PawKeyTheme.colors.green500, - 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 diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/sharedwalk/sharedroute/navigation/SharedWalkNavigation.kt b/app/src/main/java/com/paw/key/presentation/ui/course/sharedwalk/sharedroute/navigation/SharedWalkNavigation.kt deleted file mode 100644 index 9127b0ce..00000000 --- a/app/src/main/java/com/paw/key/presentation/ui/course/sharedwalk/sharedroute/navigation/SharedWalkNavigation.kt +++ /dev/null @@ -1,49 +0,0 @@ -package com.paw.key.presentation.ui.course.sharedwalk.sharedroute.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 androidx.navigation.toRoute -import com.paw.key.core.navigation.Route -import com.paw.key.presentation.ui.course.sharedwalk.sharedroute.SharedWalkCourseRoute -import com.paw.key.presentation.ui.course.walk.navigation.WalkCourse -import kotlinx.serialization.Serializable - -fun NavController.navigateSharedWalkCourse( - routeId: Int, - pageId : Int, - navOptions: NavOptions?, -) { - navigate(SharedWalkCourse(routeId = routeId, pageId = pageId), navOptions) -} - -@RequiresApi(Build.VERSION_CODES.Q) -fun NavGraphBuilder.sharedWalkCourseNavGraph( - paddingValues: PaddingValues, - navigateUp: () -> Unit, - navigateNext: (Int, Int) -> Unit, - snackBarHostState: SnackbarHostState, -) { - composable { backStackEntry -> - val ids = backStackEntry.toRoute() - - SharedWalkCourseRoute( - paddingValues = paddingValues, - navigateUp = navigateUp, - navigateNext = { routeId, pageId -> - navigateNext(routeId, pageId) - }, - routeId = ids.routeId, - pageId = ids.pageId, - snackBarHostState = snackBarHostState, - ) - } -} - -@Serializable -data class SharedWalkCourse(val pageId: Int, val routeId: Int) : Route \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/sharedwalk/sharedroute/state/SharedWalkCourseContract.kt b/app/src/main/java/com/paw/key/presentation/ui/course/sharedwalk/sharedroute/state/SharedWalkCourseContract.kt deleted file mode 100644 index 901c7301..00000000 --- a/app/src/main/java/com/paw/key/presentation/ui/course/sharedwalk/sharedroute/state/SharedWalkCourseContract.kt +++ /dev/null @@ -1,53 +0,0 @@ -package com.paw.key.presentation.ui.course.sharedwalk.sharedroute.state - -import android.graphics.Bitmap -import androidx.annotation.StringRes -import androidx.compose.runtime.Immutable -import com.naver.maps.geometry.LatLng -import com.paw.key.R -import com.paw.key.core.util.UiState -import kotlinx.collections.immutable.PersistentList -import kotlinx.collections.immutable.persistentListOf - -@Immutable -data class SharedWalkCourseState( - val uiState: UiState> = UiState.Loading, - val poiPoints: PersistentList = persistentListOf(), - - val bitmap: Bitmap? = null, - - // 현재 걸음 수 - val steps: Long = 0, - val totalDistance: Float = 0f, - - val initialSensorSteps: Long? = null, - val prevSteps: Long = 0, - val isWalking: Boolean = false, - - val initialLocationState : UiState = UiState.Loading, - val currentLocation: LatLng? = null, - val lastLocation: LatLng? = null, - val cameraState : Boolean = false, - val isLocationTracking: Boolean = false, - - val isTrackingEnabled : Boolean = false, - val isRecording : Boolean = false, // 기록 중 상태관리 - - val shouldCaptureMap: Boolean = false -) - -sealed class SharedWalkCourseSideEffect { - data class ShowSnackBar(val message: String) : SharedWalkCourseSideEffect() - data object NavigateUp: SharedWalkCourseSideEffect() - data object NavigateNext: SharedWalkCourseSideEffect() -} - -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/sharedwalk/sharedroute/viewmodel/SharedWalkViewModel.kt b/app/src/main/java/com/paw/key/presentation/ui/course/sharedwalk/sharedroute/viewmodel/SharedWalkViewModel.kt deleted file mode 100644 index a654e466..00000000 --- a/app/src/main/java/com/paw/key/presentation/ui/course/sharedwalk/sharedroute/viewmodel/SharedWalkViewModel.kt +++ /dev/null @@ -1,238 +0,0 @@ -package com.paw.key.presentation.ui.course.sharedwalk.sharedroute.viewmodel - -import android.content.Context -import android.graphics.Bitmap -import android.location.Location -import android.util.Log -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.naver.maps.geometry.LatLng -import com.paw.key.core.util.PreferenceDataStore -import com.paw.key.domain.repository.WalkSharedResultRepository -import com.paw.key.domain.repository.sharedwalk.SharedWalkRepository -import com.paw.key.presentation.ui.course.sharedwalk.sharedroute.state.SharedWalkCourseSideEffect -import com.paw.key.presentation.ui.course.sharedwalk.sharedroute.state.SharedWalkCourseState -import com.paw.key.presentation.ui.course.walk.state.WalkCourseContract.WalkCourseSideEffect -import com.paw.key.presentation.ui.course.walk.state.WalkCourseContract.WalkCourseState -import dagger.hilt.android.lifecycle.HiltViewModel -import dagger.hilt.android.qualifiers.ApplicationContext -import kotlinx.collections.immutable.PersistentList -import kotlinx.collections.immutable.toPersistentList -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import javax.inject.Inject - -@HiltViewModel -class SharedWalkCourseViewModel @Inject constructor( - private val walkSharedResultRepository : WalkSharedResultRepository, - private val sharedWalkRepository: SharedWalkRepository -) : ViewModel() { - private val _state = MutableStateFlow(SharedWalkCourseState()) - val state : StateFlow - get() = _state.asStateFlow() - - private val _sideEffect = MutableSharedFlow() - val sideEffect: MutableSharedFlow - get() = _sideEffect - - private val userId = PreferenceDataStore.getUserId() - - private val _totalTime = MutableStateFlow(0L) - val totalTime: StateFlow = _totalTime.asStateFlow() - - fun getWalkSharedTrack(routeId : Int) { - viewModelScope.launch { - sharedWalkRepository.getSharedWalkTrack( - userId = userId.first(), - routeId = routeId - ).onSuccess { - // Todo : 공유용 수정 예정 - Log.d("SharedWalkCourseViewModel", "success getWalkSharedTrack: ${state.value.poiPoints}") - }.onFailure { - Log.e("SharedWalkCourseViewModel", "failure getWalkSharedTrack: $it") - _sideEffect.emit(SharedWalkCourseSideEffect.ShowSnackBar("산책 경로를 불러오는데 실패했습니다.")) - } - } - } - - - - 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 - }*/ - distanceIncrement = oldAndroidLocation.distanceTo(newAndroidLocation) - } - - val updatedPoiPoints: PersistentList = - if (currentUiState.poiPoints.isEmpty() && currentUiState.lastLocation == null) { - // 첫 위치일 경우 무조건 추가 - currentUiState.poiPoints.add(newLocation) - } else if (distanceIncrement > 0) { // (이동이 있었으면) 추가 - currentUiState.poiPoints.add(newLocation) - } else { - // 이동 거리가 0이거나 이전 위치가 없는 경우 (첫 위치가 이미 추가된 후) - currentUiState.poiPoints - } - - val newTotalDistance = currentUiState.totalDistance + distanceIncrement - - currentUiState.copy( - lastLocation = newLocation, - currentLocation = newLocation, - totalDistance = newTotalDistance, - ) - } - } - - 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: SharedWalkCourseState.() -> SharedWalkCourseState) { - Log.e("updateState", "updateState called") - _state.update { - it.reducer() - } - } - - fun mapCaptureCompleted() { - updateState { - copy(shouldCaptureMap = false) - } - } - - fun onMapCaptured(bitmap: Bitmap?) { - if (bitmap == null) { - return - } - updateState { - copy(bitmap = bitmap) - } - - viewModelScope.launch { - try { - walkSharedResultRepository.saveResult( - bitmap = state.value.bitmap, - totalTime = _totalTime.value, - distance = state.value.totalDistance, - steps = state.value.steps.toInt(), - points = state.value.poiPoints.toList() - ) - - Log.d("WalkCourseViewModel", "Map captured bitmap saved to DataStore.") - } catch (e: Exception) { - _sideEffect.emit(SharedWalkCourseSideEffect.ShowSnackBar("산책 지도 이미지 저장 실패: ${e.localizedMessage}")) - Log.e("WalkCourseViewModel", "Error saving captured bitmap: ${e.localizedMessage}") - } finally { - mapCaptureCompleted() // 캡처 시도 후, 성공/실패 여부와 관계없이 플래그 리셋 - } - } - } - - fun onStopTrackingEvent() { - viewModelScope.launch { - val currentWalkState = _state.value - - try { - walkSharedResultRepository.saveResult( - bitmap = currentWalkState.bitmap, - totalTime = _totalTime.value, - distance = currentWalkState.totalDistance, - steps = currentWalkState.steps.toInt(), - points = currentWalkState.poiPoints.toList() - ) - Log.d("WalkCourseViewModel", "All walk summary data saved successfully using PreferenceDataStore.") - //Log.e("WalkCourseViewModel", PreferenceDataStore.getTotalTime(context).toString()) - _sideEffect.emit(SharedWalkCourseSideEffect.ShowSnackBar("산책 기록이 성공적으로 저장되었습니다.")) - } catch (e: Exception) { - Log.e("WalkCourseViewModel", "Error saving all walk summary data: ${e.message}", e) - _sideEffect.emit(SharedWalkCourseSideEffect.ShowSnackBar("산책 기록 저장 실패: ${e.localizedMessage}")) - } finally { - walkSharedResultRepository.saveResult( - bitmap = currentWalkState.bitmap, - totalTime = _totalTime.value, - distance = currentWalkState.totalDistance, - steps = currentWalkState.steps.toInt(), - points = currentWalkState.poiPoints.toList() - ) - } - } - } -} \ 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 deleted file mode 100644 index 7f6fc7f7..00000000 --- a/app/src/main/java/com/paw/key/presentation/ui/course/walk/component/WalkRecordRow.kt +++ /dev/null @@ -1,70 +0,0 @@ -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.core.designsystem.theme.PawKeyTheme -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 = PawKeyTheme.colors.green500, - 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 diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walk/navigation/WalkCourseNavigation.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walk/navigation/WalkCourseNavigation.kt deleted file mode 100644 index fa95115d..00000000 --- a/app/src/main/java/com/paw/key/presentation/ui/course/walk/navigation/WalkCourseNavigation.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.paw.key.presentation.ui.course.walk.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: (routeId : Int) -> Unit, - snackBarHostState: SnackbarHostState, -) { - composable { - WalkCourseRoute( - paddingValues = paddingValues, - navigateUp = navigateUp, - navigateNext = { routeId -> - navigateNext(routeId) - }, - snackBarHostState = snackBarHostState, - ) - } -} - -@Serializable -data object WalkCourse : Route \ No newline at end of file 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 deleted file mode 100644 index 83c4501e..00000000 --- a/app/src/main/java/com/paw/key/presentation/ui/course/walkcomplete/WalkCompletionScreen.kt +++ /dev/null @@ -1,171 +0,0 @@ -package com.paw.key.presentation.ui.course.walkcomplete - -import android.graphics.Bitmap -import androidx.activity.compose.BackHandler -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -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.foundation.shape.RoundedCornerShape -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.asImageBitmap -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.paw.key.R -import com.paw.key.core.designsystem.component.PawkeyButton -import com.paw.key.core.designsystem.component.TopBar -import com.paw.key.core.designsystem.theme.PawKeyTheme -import com.paw.key.presentation.ui.course.walk.formatDistance -import com.paw.key.presentation.ui.course.walk.formatTime -import com.paw.key.presentation.ui.course.walkcomplete.component.WalkCompleteHeader -import com.paw.key.presentation.ui.course.walkcomplete.component.WalkCompletionRecordRow -import com.paw.key.presentation.ui.course.walkcomplete.viewmodel.WalkCompleteViewModel - -@Composable -fun WalkCompletionRoute( - paddingValues: PaddingValues, - navigateUp: () -> Unit, - navigateNext: () -> Unit, - modifier: Modifier = Modifier, - viewModel: WalkCompleteViewModel = hiltViewModel(), - isSharedWalk : Boolean = false -) { - val state by viewModel.state.collectAsStateWithLifecycle() - - val walkRecordList = listOf(R.string.course_record_distance, R.string.course_record_time, R.string.course_record_step) - - BackHandler(enabled = true) { - // 뒤로 가기 막기 - } - - - LaunchedEffect(Unit) { - viewModel.loadWalkResult() - // 디버깅용 - viewModel.debugRepositoryState() - } - - WalkCompletionScreen( - paddingValues = paddingValues, - navigateUp = navigateUp, - navigateNext = navigateNext, - bitmap = state.bitmap, - walkRecordList = walkRecordList, - totalDistance = state.totalDistance, - totalTime = state.totalTime, - totalSteps = state.totalSteps, - isSharedWalk = isSharedWalk, - modifier = modifier, - ) -} - -@Composable -fun WalkCompletionScreen( - paddingValues: PaddingValues, - navigateUp: () -> Unit, - navigateNext: () -> Unit, - bitmap: Bitmap?, - walkRecordList: List, - totalDistance: Float, - totalTime: Long, - totalSteps: Int, - isSharedWalk: Boolean, - modifier: Modifier = Modifier, -) { - Column ( - modifier = modifier - .background(PawKeyTheme.colors.white2), - horizontalAlignment = Alignment.CenterHorizontally, - ){ - TopBar( - title = "산책 완료", - onBackClick = navigateUp, - modifier = Modifier - .padding(8.dp) - .background(PawKeyTheme.colors.white1), - isBackVisible = false - ) - - Text( - text = "산책 결과를 확인해보세요.", - color = PawKeyTheme.colors.black, - style = PawKeyTheme.typography.head18Sb, - modifier = Modifier - .padding(top = 36.dp, bottom = 16.dp) - .padding(horizontal = 16.dp) - .fillMaxWidth() - ) - - Column ( - modifier = modifier - .padding(start = 16.dp, end = 16.dp) - .background( - color = PawKeyTheme.colors.white1, - shape = RoundedCornerShape(12.dp) - ) - ) { - /*// Todo : 사진 받아올 곳 - WalkCompleteHeader( - bitmap = null, - modifier = Modifier - .padding(top = 16.dp, start = 16.dp, end = 16.dp) - )*/ - - bitmap?.asImageBitmap()?.let { - Image( - bitmap = it, - contentDescription = "My Image", - modifier = Modifier - .padding(start = 8.dp, end = 8.dp) - .padding(top = 32.dp) - .clip(RoundedCornerShape(8.dp)) - ) - } - - HorizontalDivider( - thickness = 1.dp, - color = PawKeyTheme.colors.gray50, - modifier = Modifier - .fillMaxWidth() - .padding(top = 15.dp, bottom = 10.dp) - ) - - WalkCompletionRecordRow( - totalDistance = formatDistance(totalDistance), - totalTime = formatTime(totalTime), - currentSteps = totalSteps, - modifier = Modifier - .fillMaxWidth() - ) - } - - val buttonTextRes = if (isSharedWalk) { - R.string.course_shared_complete_button_text - } else { - R.string.course_complete_button_text - } - - Spacer(modifier = Modifier.weight(1f)) - - PawkeyButton( - text = stringResource(buttonTextRes), - onClick = navigateNext, - enabled = true, - modifier = Modifier - .fillMaxWidth() - .padding(start = 16.dp, end = 16.dp, bottom = 60.dp) - ) - } -} \ No newline at end of file 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 deleted file mode 100644 index ef64ae5a..00000000 --- a/app/src/main/java/com/paw/key/presentation/ui/course/walkcomplete/component/WalkCompletionHeader.kt +++ /dev/null @@ -1,71 +0,0 @@ -package com.paw.key.presentation.ui.course.walkcomplete.component - -import android.graphics.Bitmap -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -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 - .fillMaxWidth() - ) { - AsyncImage( - model = bitmap, - contentDescription = "profile", - modifier = Modifier - .size(48.dp) - .background( - color = PawKeyTheme.colors.gray50, - shape = CircleShape - ) - .clip(CircleShape) - ) - - Column ( - modifier = Modifier - .padding(start = 10.dp) - ) { - Text( - text = "포비", - color = PawKeyTheme.colors.black, - style = PawKeyTheme.typography.head20B1 - ) - - Text( - text = "2025.06.26(금) | 오후 11:50", - color = PawKeyTheme.colors.gray300, - style = PawKeyTheme.typography.caption12Sb1, - modifier = Modifier - .padding(top = 6.dp) - ) - } - } -} - -@Preview(showBackground = true) -@Composable -private fun WalkCompleteHeaderPreview() { - PawKeyTheme { - WalkCompleteHeader( - bitmap = null - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walkcomplete/component/WalkCompletionItem.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walkcomplete/component/WalkCompletionItem.kt deleted file mode 100644 index fbc568a5..00000000 --- a/app/src/main/java/com/paw/key/presentation/ui/course/walkcomplete/component/WalkCompletionItem.kt +++ /dev/null @@ -1,55 +0,0 @@ -package com.paw.key.presentation.ui.course.walkcomplete.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.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.paw.key.R -import com.paw.key.core.designsystem.theme.PawKeyTheme - -@Composable -fun WalkCompletionRecordItem( - recordTitle : Int, - recordContent : String, - modifier: Modifier = Modifier, -) { - Column ( - modifier = modifier - .padding(vertical = 16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - Text( - text = stringResource(recordTitle), - color = PawKeyTheme.colors.gray500, - style = PawKeyTheme.typography.caption12Sb2 - ) - - Spacer(modifier = Modifier.height(4.dp)) - - Text( - text = recordContent, - color = PawKeyTheme.colors.green500, - style = PawKeyTheme.typography.head20B2 - ) - } -} - -@Preview -@Composable -private fun WalkCompletionRecordItemPreview() { - PawKeyTheme { - WalkCompletionRecordItem( - 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/walkcomplete/component/WalkCompletionRecordRow.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walkcomplete/component/WalkCompletionRecordRow.kt deleted file mode 100644 index aac98dc9..00000000 --- a/app/src/main/java/com/paw/key/presentation/ui/course/walkcomplete/component/WalkCompletionRecordRow.kt +++ /dev/null @@ -1,62 +0,0 @@ -package com.paw.key.presentation.ui.course.walkcomplete.component - -import androidx.compose.foundation.background -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 WalkCompletionRecordRow( - totalDistance: String, - totalTime: String, - currentSteps: Int, - modifier: Modifier = Modifier, -) { - Row ( - modifier = modifier - .fillMaxWidth() - .background(color = Color.White, shape = RoundedCornerShape(8.dp)) - .padding(horizontal = 16.dp), - verticalAlignment = Alignment.CenterVertically - ) { - val recordItems = listOf( - DistanceRecord, - TimeRecord, - StepsRecord, - ) - - recordItems.forEach { record -> - when (record) { - DistanceRecord -> WalkCompletionRecordItem( - recordTitle = record.titleResId, - recordContent = totalDistance, - modifier = Modifier - .weight(1f), - ) - - TimeRecord -> WalkCompletionRecordItem( - recordTitle = record.titleResId, - recordContent = totalTime, - modifier = Modifier - .weight(1f), - ) - - StepsRecord -> WalkCompletionRecordItem( - recordTitle = record.titleResId, - recordContent = currentSteps.toString(), - modifier = Modifier - .weight(1f), - ) - } - } - } -} \ No newline at end of file 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 deleted file mode 100644 index 3dff0c39..00000000 --- a/app/src/main/java/com/paw/key/presentation/ui/course/walkcomplete/navigation/WalkCompletionNavigation.kt +++ /dev/null @@ -1,41 +0,0 @@ -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.walkcomplete.WalkCompletionRoute -import kotlinx.serialization.Serializable - -fun NavController.navigateWalkCompletion( - navOptions: NavOptions?, - routeId: Int -) { - navigate(WalkCompletion(routeId), navOptions) -} - -@RequiresApi(Build.VERSION_CODES.Q) -fun NavGraphBuilder.walkCompletionNavGraph( - paddingValues: PaddingValues, - navigateUp: () -> Unit, - navigateNext: (routeId : Int) -> Unit, -) { - composable { backStackEntry -> - val routeId = backStackEntry.arguments?.getInt("routeId") ?: 0 - WalkCompletionRoute( - paddingValues = paddingValues, - navigateUp = navigateUp, - navigateNext = { - navigateNext(routeId) - }, - ) - } -} - -@Serializable -data class WalkCompletion(val routeId : Int) : Route \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walkcomplete/state/WalkCompleteContract.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walkcomplete/state/WalkCompleteContract.kt deleted file mode 100644 index f6ed3c94..00000000 --- a/app/src/main/java/com/paw/key/presentation/ui/course/walkcomplete/state/WalkCompleteContract.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.paw.key.presentation.ui.course.walkcomplete.state - -import android.graphics.Bitmap -import androidx.compose.runtime.Immutable -import com.naver.maps.geometry.LatLng - -class WalkCompleteContract { - @Immutable - data class WalkCompleteState( - val bitmap: Bitmap? = null, - val poiPoints: List = emptyList(), - val totalDistance: Float = 0f, - val totalTime: Long = 0L, - val totalSteps : Int = 0, - ) -} \ 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 deleted file mode 100644 index dfae0149..00000000 --- a/app/src/main/java/com/paw/key/presentation/ui/course/walkcomplete/viewmodel/WalkCompleteViewModel.kt +++ /dev/null @@ -1,75 +0,0 @@ -package com.paw.key.presentation.ui.course.walkcomplete.viewmodel - -import android.util.Log -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.paw.key.domain.repository.WalkSharedResultRepository -import com.paw.key.presentation.ui.course.walkcomplete.state.WalkCompleteContract -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.flow.firstOrNull -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import javax.inject.Inject - -@HiltViewModel -class WalkCompleteViewModel @Inject constructor( - private val walkSharedResultRepository: WalkSharedResultRepository, -) : ViewModel() { - private val _state = MutableStateFlow(WalkCompleteContract.WalkCompleteState()) - val state: StateFlow - get() = _state.asStateFlow() - - init { - viewModelScope.launch { - walkSharedResultRepository.getResult().collectLatest { result -> - Log.d("WalkCompleteViewModel", "Result received: $result") - - if (result != null) { - _state.update { - it.copy( - bitmap = result.bitmap, - poiPoints = result.points, - totalDistance = result.distance, - totalTime = result.totalTime, - totalSteps = result.steps - ) - } - } - } - } - } - - fun loadWalkResult() { - viewModelScope.launch { - try { - val result = walkSharedResultRepository.getResult().firstOrNull() - - if (result != null) { - _state.update { - it.copy( - bitmap = result.bitmap, - poiPoints = result.points, - totalDistance = result.distance, - totalTime = result.totalTime, - totalSteps = result.steps - ) - } - } - } catch (e: Exception) { - Log.e("WalkCompleteViewModel", "Error loading walk result", e) - } - } - } - - // 디버깅을 위한 함수 - fun debugRepositoryState() { - viewModelScope.launch { - val result = walkSharedResultRepository.getResult().firstOrNull() - Log.d("WalkCompleteViewModel", "Debug - Repository state: $result") - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/WalkReviewUiModel.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/WalkReviewUiModel.kt deleted file mode 100644 index cdb989b6..00000000 --- a/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/WalkReviewUiModel.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.paw.key.presentation.ui.course.walkreview - -data class WalkReviewCategoryUiModel( - val categoryId: Int, - val categoryName: String, - val categoryDescription: String, - val options: List -) - -data class WalkReviewOptionUiModel( - val optionId: Int, - val optionText: String, - val isSelected: Boolean = false -) \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/component/WalkReviewFeedbackForm.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/component/WalkReviewFeedbackForm.kt deleted file mode 100644 index 82714d62..00000000 --- a/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/component/WalkReviewFeedbackForm.kt +++ /dev/null @@ -1,91 +0,0 @@ -package com.paw.key.presentation.ui.course.walkreview.component - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.ExperimentalLayoutApi -import androidx.compose.foundation.layout.FlowRow -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -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.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.paw.key.core.designsystem.component.FeedbackItem -import com.paw.key.core.designsystem.theme.PawKeyTheme -import com.paw.key.presentation.ui.course.walkreview.state.WalkReviewContract - -@OptIn(ExperimentalLayoutApi::class) -@Composable -fun WalkReviewFeedbackForm( - icon: Int, - title: String, - selectedFeedbackItems: List, - selectedFeedbackItem: String?, // 선택된 옵션 텍스트 - feedbackList: List, - onClickFeedback: (String) -> Unit, - modifier: Modifier = Modifier -) { - Row( - modifier = modifier - .padding(top = 24.dp) - .fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically - ) { - // TODO: 아이콘 변경 예정 - /* Icon( - imageVector = ImageVector.vectorResource(icon), - contentDescription = null - ) */ - - Text( - text = title, - style = PawKeyTheme.typography.body16M, - color = PawKeyTheme.colors.black, - modifier = Modifier - ) - } - - FlowRow( - modifier = Modifier - .padding(start = 16.dp, end = 16.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - feedbackList.forEach { itemText -> - val isSelected = selectedFeedbackItems.contains(itemText) - val textColor = if (isSelected) PawKeyTheme.colors.green500 else PawKeyTheme.colors.gray400 - val borderColor = if (isSelected) PawKeyTheme.colors.green500 else PawKeyTheme.colors.gray50 - - FeedbackItem( - item = itemText, - textColor = textColor, - borderColor = borderColor, - onClickFeedback = { onClickFeedback(itemText) } - ) - } - } -} - - -@Preview(showBackground = true) -@Composable -private fun WalkReviewFeedbackFormPreview() { - PawKeyTheme { - WalkReviewFeedbackForm( - feedbackList = listOf( - "킥보드나 자전거가 거의 없어요", - "차량이 거의 다니지 않아요", - "야간 조명이 잘 되어 있어요", - "보도와 차도가 구분되어 있어요", - "보도가 넓어서 산책하기 편했어요" - ), - icon = com.paw.key.R.drawable.ic_walk_review_location, - title = "위치", - selectedFeedbackItem = null, - onClickFeedback = {}, - selectedFeedbackItems = listOf() - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/component/WalkReviewFeedbackHeader.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/component/WalkReviewFeedbackHeader.kt deleted file mode 100644 index d69eb1fd..00000000 --- a/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/component/WalkReviewFeedbackHeader.kt +++ /dev/null @@ -1,46 +0,0 @@ -package com.paw.key.presentation.ui.course.walkreview.component - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.paw.key.core.designsystem.theme.PawKeyTheme - -@Composable -fun WalkReviewFeedbackHeader ( - petName : String, - modifier: Modifier = Modifier -) { - Column ( - modifier = modifier - .fillMaxWidth() - ) { - Text( - text = "${petName}와의 산책 어땠나요?", - style = PawKeyTheme.typography.head18Sb, - color = PawKeyTheme.colors.black - ) - - Text( - text = "카테고리 별로 1개 이상의 키워드를 선물해주세요.", - style = PawKeyTheme.typography.body14R, - color = PawKeyTheme.colors.gray300, - modifier = Modifier - .padding(top = 10.dp) - ) - } -} - -@Preview(showBackground = true) -@Composable -private fun WalkReviewFeedbackHeaderPreview() { - PawKeyTheme { - WalkReviewFeedbackHeader( - petName = "뽀삐" - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/component/WalkReviewImageItem.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/component/WalkReviewImageItem.kt deleted file mode 100644 index 5ed9f63c..00000000 --- a/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/component/WalkReviewImageItem.kt +++ /dev/null @@ -1,118 +0,0 @@ -package com.paw.key.presentation.ui.course.walkreview.component - -import android.net.Uri -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Card -import androidx.compose.material3.Icon -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.res.vectorResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import coil.compose.AsyncImage -import com.paw.key.R -import com.paw.key.core.designsystem.theme.PawKeyTheme -import com.paw.key.core.extension.noRippleClickable - -@Composable -fun WalkReviewItem( - image : Uri?, - onClickCard: () -> Unit, - onImageDelete : (Uri?) -> Unit, - modifier: Modifier = Modifier, -) { - Card( - shape = RoundedCornerShape(16.dp), - modifier = modifier - .width(LocalConfiguration.current.screenHeightDp.dp * 0.25f) - .height(LocalConfiguration.current.screenHeightDp.dp * 0.25f) - .noRippleClickable { - if (image == null) { - onClickCard() - } - } - ) { - Box( - modifier = Modifier - .weight(1f) - .fillMaxWidth() - .background(if (image == null) PawKeyTheme.colors.gray50 else Color.Transparent) - .clip(RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)) - ) { - when { - image != null -> { - AsyncImage( - model = image, - contentDescription = stringResource(R.string.course_review_image_description), - modifier = Modifier.fillMaxSize(), - contentScale = ContentScale.Crop - ) - } - - else -> { - Icon( - imageVector = ImageVector.vectorResource(id = R.drawable.ic_walk_review_add_image), - contentDescription = stringResource(R.string.course_review_image_description), - tint = Color.Unspecified, - modifier = Modifier - .align(Alignment.Center) - .size(48.dp) - ) - } - } - - if (image != null) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .align(Alignment.TopStart) - .fillMaxWidth() - .padding(8.dp) - .background(color = Color.Transparent) - ) { - Spacer(modifier = Modifier.weight(1f)) - - Icon( - imageVector = ImageVector.vectorResource(R.drawable.ic_walk_review_cancel), - tint = Color.Unspecified, - contentDescription = null, - modifier = Modifier - .size(24.dp) - .noRippleClickable { - onImageDelete(image) - } - ) - } - } - } - } -} - -@Preview -@Composable -private fun WalkReviewItemPreview() { - PawKeyTheme { - WalkReviewItem( - image = null, - onClickCard = {}, - onImageDelete = {} - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/component/WalkReviewTextField.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/component/WalkReviewTextField.kt deleted file mode 100644 index ddba1349..00000000 --- a/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/component/WalkReviewTextField.kt +++ /dev/null @@ -1,73 +0,0 @@ -package com.paw.key.presentation.ui.course.walkreview.component - -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.BasicTextField -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.paw.key.core.designsystem.theme.PawKeyTheme - -@Composable -fun WalkReviewTextField( - textValue: String, - placeHolder : String, - onTextChanged: (String) -> Unit, - - modifier: Modifier = Modifier -) { - BasicTextField( - value = textValue, - onValueChange = { - onTextChanged(it) - }, - singleLine = false, - textStyle = PawKeyTheme.typography.body14R, - decorationBox = { innerTextField -> - Box( - modifier = modifier - .fillMaxWidth() - .background( - color = PawKeyTheme.colors.white2, - shape = RoundedCornerShape(8.dp) - ) - .border( - width = 1.dp, - color = PawKeyTheme.colors.gray50, - shape = RoundedCornerShape(8.dp) - ) - .padding(horizontal = 16.dp, vertical = 14.dp), - contentAlignment = Alignment.TopStart - ) { - if (textValue.isEmpty()) { - Text( - text = placeHolder, - color = PawKeyTheme.colors.gray200, - style = PawKeyTheme.typography.body14R - ) - } - innerTextField() - } - } - ) -} - - -@Preview -@Composable -private fun WalkReviewTextFieldPreview() { - PawKeyTheme { - WalkReviewTextField( - textValue = "", - placeHolder = "제목을 입력해주세요.", - onTextChanged = {} - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/util/WalkReviewUtil.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/util/WalkReviewUtil.kt deleted file mode 100644 index b889ed8c..00000000 --- a/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/util/WalkReviewUtil.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.paw.key.presentation.ui.course.walkreview.util - -import com.paw.key.domain.model.entity.walkreview.WalkReviewCategoryEntity -import com.paw.key.domain.model.entity.walkreview.WalkReviewOptionOptionsResponseEntity -import com.paw.key.presentation.ui.course.walkreview.WalkReviewCategoryUiModel -import com.paw.key.presentation.ui.course.walkreview.WalkReviewOptionUiModel - -fun WalkReviewCategoryEntity.toUiModel(): WalkReviewCategoryUiModel { - return WalkReviewCategoryUiModel( - categoryId = categoryId, - categoryName = categoryName, - categoryDescription = categoryDescription, - options = options.map { it.toUiModel() } - ) -} - -fun WalkReviewOptionOptionsResponseEntity.toUiModel(): WalkReviewOptionUiModel { - return WalkReviewOptionUiModel( - optionId = categoryOptionId, - optionText = optionText - ) -} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_community_fill.xml b/app/src/main/res/drawable/ic_community_fill.xml deleted file mode 100644 index da82e6bc..00000000 --- a/app/src/main/res/drawable/ic_community_fill.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/ic_community_linear.xml b/app/src/main/res/drawable/ic_community_linear.xml deleted file mode 100644 index 61d28de7..00000000 --- a/app/src/main/res/drawable/ic_community_linear.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - From 532c3b543a30cfd0e5b2d2892b8caa7e7535d254 Mon Sep 17 00:00:00 2001 From: sonms Date: Mon, 5 Jan 2026 12:51:36 +0900 Subject: [PATCH 02/47] =?UTF-8?q?add/#154=20=EC=82=AC=EC=9A=A9=20=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=20=ED=8C=8C=EC=9D=BC,=20=EC=95=84=EC=9D=B4?= =?UTF-8?q?=EC=BD=98=20=ED=8C=8C=EC=9D=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/course/walk/WalkCourseScreen.kt | 762 ------------------ .../course/walk/state/WalkCourseContract.kt | 34 - app/src/main/res/drawable/ic_home_fill.xml | 7 +- app/src/main/res/drawable/ic_home_linear.xml | 7 +- app/src/main/res/drawable/ic_mypage_fill.xml | 20 +- .../main/res/drawable/ic_mypage_linear.xml | 24 +- .../res/drawable/ic_route_recommand_fill.xml | 13 + .../drawable/ic_route_recommand_linear.xml | 12 + .../res/drawable/ic_walk_course_check.xml | 13 + app/src/main/res/drawable/ic_walk_fill.xml | 50 +- app/src/main/res/drawable/ic_walk_linear.xml | 34 +- .../drawable/ic_walk_review_dialog_paw.xml | 28 + app/src/main/res/drawable/img_walk_info.png | Bin 0 -> 10143 bytes 13 files changed, 125 insertions(+), 879 deletions(-) delete mode 100644 app/src/main/java/com/paw/key/presentation/ui/course/walk/WalkCourseScreen.kt delete mode 100644 app/src/main/java/com/paw/key/presentation/ui/course/walk/state/WalkCourseContract.kt create mode 100644 app/src/main/res/drawable/ic_route_recommand_fill.xml create mode 100644 app/src/main/res/drawable/ic_route_recommand_linear.xml create mode 100644 app/src/main/res/drawable/ic_walk_course_check.xml create mode 100644 app/src/main/res/drawable/ic_walk_review_dialog_paw.xml create mode 100644 app/src/main/res/drawable/img_walk_info.png 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 deleted file mode 100644 index 5117cda1..00000000 --- a/app/src/main/java/com/paw/key/presentation/ui/course/walk/WalkCourseScreen.kt +++ /dev/null @@ -1,762 +0,0 @@ -package com.paw.key.presentation.ui.course.walk - -import android.Manifest -import android.content.Context -import android.graphics.Bitmap -import android.opengl.GLException -import android.os.Build -import android.os.Handler -import android.os.Looper -import android.util.Log -import android.view.Gravity -import android.view.PixelCopy -import android.widget.Toast -import androidx.annotation.RequiresApi -import androidx.compose.foundation.background -import androidx.compose.foundation.border -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.material3.FloatingActionButton -import androidx.compose.material3.Icon -import androidx.compose.material3.Scaffold -import androidx.compose.material3.SnackbarHostState -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import androidx.compose.runtime.snapshotFlow -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.vectorResource -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.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.compose.LocalLifecycleOwner -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.flowWithLifecycle -import com.kakao.vectormap.graphics.gl.GLSurfaceView -import com.naver.maps.geometry.LatLng -import com.naver.maps.geometry.LatLngBounds -import com.naver.maps.map.CameraUpdate -import com.naver.maps.map.compose.CameraPositionState -import com.naver.maps.map.compose.CameraUpdateReason -import com.naver.maps.map.compose.ExperimentalNaverMapApi -import com.naver.maps.map.compose.LocationOverlay -import com.naver.maps.map.compose.LocationTrackingMode -import com.naver.maps.map.compose.MapProperties -import com.naver.maps.map.compose.MapUiSettings -import com.naver.maps.map.compose.NaverMap -import com.naver.maps.map.compose.PathOverlay -import com.naver.maps.map.compose.rememberCameraPositionState -import com.naver.maps.map.overlay.OverlayImage -import com.paw.key.R -import com.paw.key.core.designsystem.component.LoadingScreen -import com.paw.key.core.designsystem.component.PawkeyButton -import com.paw.key.core.designsystem.theme.PawKeyTheme -import com.paw.key.core.util.PreferenceDataStore -import com.paw.key.core.util.UiState -import com.paw.key.core.extension.noRippleClickable -import com.paw.key.presentation.ui.course.util.FusedLocationSource -import com.paw.key.core.util.PermissionRequestEffect -import com.paw.key.presentation.ui.course.util.StepCountListener -import com.paw.key.presentation.ui.course.util.rememberCustomFusedLocationSource -import com.paw.key.presentation.ui.course.util.rememberStepCounter -import com.paw.key.presentation.ui.course.walk.component.WalkRecordItem -import com.paw.key.presentation.ui.course.walk.component.WalkRecordRow -import com.paw.key.presentation.ui.course.walk.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.collections.immutable.ImmutableList -import kotlinx.coroutines.flow.drop -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 - - -@OptIn(ExperimentalNaverMapApi::class) -@RequiresApi(Build.VERSION_CODES.Q) -@Composable -fun WalkCourseRoute( - paddingValues: PaddingValues, - navigateUp: () -> Unit, - navigateNext: (routeId : Int) -> Unit, - snackBarHostState: SnackbarHostState, - modifier: Modifier = Modifier, - isSharedWalk : Boolean = false, - viewModel: WalkCourseViewModel = hiltViewModel(), -) { - val lifecycleOwner = LocalLifecycleOwner.current - val context = LocalContext.current - val scope = rememberCoroutineScope() - - val state by viewModel.state.collectAsStateWithLifecycle() - - val cameraPositionState = rememberCameraPositionState() - - val userId = PreferenceDataStore.getUserId() - - var hasLocationPermission by remember { mutableStateOf(false) } - - val fusedLocationClient = rememberCustomFusedLocationSource( - useTestPoints = false, - cameraPositionState = cameraPositionState, - hasLocationPermission = hasLocationPermission - ) - - val stepCounter = rememberStepCounter() - - LaunchedEffect(viewModel.sideEffect, lifecycleOwner) { - viewModel.sideEffect.flowWithLifecycle(lifecycleOwner.lifecycle) - .collect { sideEffect -> - when (sideEffect) { - is WalkCourseSideEffect.ShowSnackBar -> snackBarHostState.showSnackbar( - sideEffect.message - ) - - is WalkCourseSideEffect.NavigateNext -> navigateNext(sideEffect.regionId) - WalkCourseSideEffect.NavigateUp -> navigateUp() - } - } - } - - val requiredPermissions = remember { - mutableListOf( - Manifest.permission.ACCESS_FINE_LOCATION, - Manifest.permission.ACCESS_COARSE_LOCATION - ).apply { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - add(Manifest.permission.ACTIVITY_RECOGNITION) - } - }.toTypedArray() - } - - PermissionRequestEffect( - permissions = requiredPermissions, - onResult = { isGranted -> - hasLocationPermission = isGranted - if (isGranted) { - viewModel.onPermissionsGranted() - fusedLocationClient.setRealTimeLocationListener(viewModel) - /*fusedLocationClient.activate { - if (state.recordingState.isRecording) { - viewModel.startTracking() - } - }*/ - } else { - Toast.makeText(context, "산책 기록을 위해 권한이 필요합니다.", Toast.LENGTH_SHORT).show() - } - } - ) - - val formattedTotalTime by remember { - derivedStateOf { - formatTime(state.totalTimeMillis) - } - } - - val formatDistance by remember { - derivedStateOf { - formatDistance(state.mapState.totalDistance) - } - } - - var mapProperties by remember { - mutableStateOf(MapProperties()) - } - - /*// 0~9 = 0, 10~19 = 1 을 감지 - val distanceInTens by remember(state.totalDistance) { // ViewModel의 totalDistance를 참조 - derivedStateOf { - (state.totalDistance / 10).toInt() // Float을 Int로 변환 - } - } - - // 이전 10m 단위 값을 저장하여 중복 호출 방지 - var lastRecordedDistanceInTens by remember { - mutableIntStateOf(-1) - }*/ - - - - LaunchedEffect(state.recordingState.isRecording, stepCounter) { - if (state.recordingState.isRecording) { - stepCounter.setStepCountListener(object : StepCountListener { - override fun onStepCountChanged(sessionSteps: Long) { - viewModel.onRawStepData(sessionSteps) - } - override fun onSensorNotFound() { - Toast.makeText(context, "걸음 수 측정 센서가 없는 기기입니다.", Toast.LENGTH_SHORT).show() - } - }) - stepCounter.activate() - } else { - stepCounter.deactivate() - } - } - - LaunchedEffect(state.mapState.poiPoints.size) { - if (state.mapState.poiPoints.size >= 2) { - val bounds = LatLngBounds.from(state.mapState.poiPoints) - cameraPositionState.animate( - CameraUpdate.fitBounds(bounds, 300) - ) - } - } - - LaunchedEffect(state.mapState.isTrackingEnabled) { - mapProperties = mapProperties.copy( - locationTrackingMode = if (state.mapState.isTrackingEnabled) { - LocationTrackingMode.Follow - } else { - LocationTrackingMode.NoFollow - } - ) - } - - LaunchedEffect(cameraPositionState) { - snapshotFlow { cameraPositionState.cameraUpdateReason } - .drop(1) // flow 가 시작될 때의 이전 값 무시 - .collect { reason -> - if (reason == CameraUpdateReason.GESTURE && state.mapState.isTrackingEnabled) { - viewModel.disableTracking() - } - } - } - - - /*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}") - }*/ - - when (state.mapState.initialState) { - is UiState.Empty -> Unit - is UiState.Failure -> Unit - - is UiState.Loading -> { - LoadingScreen() - } - - is UiState.Success -> { - WalkCourseScreen( - paddingValues = paddingValues, - navigateUp = navigateUp, - cameraPositionState = cameraPositionState, - currentLocation = state.mapState.currentLocation, - routeLineCoords = state.mapState.poiPoints, - locationSource = fusedLocationClient, - context = context, - totalDistance = formatDistance, - mapProperties = mapProperties, - isSharedWalk = isSharedWalk, - currentSteps = state.stepCounterState.sessionSteps, - totalTime = formattedTotalTime, - isRecording = state.recordingState.isRecording, // 산책 중단, 계속 여부 - isTracking = state.mapState.isTrackingEnabled, // 산책 포커싱 - onClickTracking = { - viewModel.fetchTrackingEnable() - Log.d("WalkCourseRoute", "onClickTracking ${state.mapState.isTrackingEnabled}") - }, - onPauseTracking = { // 일시정지 - - }, - onStartTracking = { // 계속하기 - - }, - onStopTracking = { - /*scope.launch { - viewModel.postWalkCourseData(userId = userId.first()) - }*/ - }, - onCaptured = { bitmap -> - // Todo : bitmap 안쓸거임 - }, - modifier = modifier, - ) - } - } -} - -@OptIn(ExperimentalNaverMapApi::class) -@Composable -fun WalkCourseScreen( - paddingValues: PaddingValues, - navigateUp: () -> Unit, - cameraPositionState: CameraPositionState, - locationSource: FusedLocationSource, - context: Context, - currentLocation : LatLng?, - routeLineCoords : ImmutableList, - totalDistance: String, - currentSteps: Long, - totalTime: String, - mapProperties: MapProperties, - isSharedWalk: Boolean, - isTracking: Boolean, // 포커싱 여부 - isRecording: Boolean, // 산책 중단, 계속 여부 - onClickTracking: () -> Unit, // 따라다니기 - onStartTracking: () -> Unit, // 계속하기 - onPauseTracking: () -> Unit, // 잠시 중단 - onStopTracking: () -> Unit, // 종료하기 - onCaptured: (Bitmap?) -> Unit, - modifier: Modifier = Modifier, -) { - var mapUiSettings by remember { - mutableStateOf( - MapUiSettings( - logoGravity = Gravity.BOTTOM or Gravity.START, - ) - ) - } - - Scaffold( - modifier = modifier - .padding(paddingValues), - snackbarHost = { - } - ) { pv -> - Box( - modifier = Modifier - .padding(pv) - ) { - NaverMap ( - modifier = Modifier - .fillMaxSize(), - cameraPositionState = cameraPositionState, - locationSource = locationSource, - locale = Locale.KOREA, - uiSettings = mapUiSettings, - properties = mapProperties, - ) { - if (currentLocation != null) { - LocationOverlay( - position = currentLocation , - icon = OverlayImage.fromResource(R.drawable.user_poi), - ) - } - - if (routeLineCoords.isNotEmpty() && routeLineCoords.size >= 2) { - PathOverlay( - coords = routeLineCoords, - width = 5.dp, - color = PawKeyTheme.colors.green500, - outlineWidth = 0.dp - ) - } - } - - Column ( - modifier = modifier - .fillMaxSize(), - verticalArrangement = Arrangement.SpaceBetween, - horizontalAlignment = Alignment.CenterHorizontally - ) { - WalkRecordRow( - totalDistance = totalDistance, - totalTime = totalTime, - currentSteps = currentSteps.toInt(), - modifier = Modifier - .fillMaxWidth() - .padding(top = 16.dp) - ) - - if (isRecording) { - Spacer(modifier = Modifier.weight(1f)) - } else { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - if (!isSharedWalk) { - Text( - text = "산책이 중단되었어요!", - textAlign = TextAlign.Center, - style = PawKeyTheme.typography.head24B, - color = PawKeyTheme.colors.white1, - modifier = Modifier.fillMaxWidth() - ) - - Text( - text = "산책을 정말 종료하시겠어요?", - fontSize = 12.sp, - textAlign = TextAlign.Center, - style = PawKeyTheme.typography.body16M, - color = PawKeyTheme.colors.white2, - modifier = Modifier - .fillMaxWidth() - .padding(top = 12.dp) - ) - } else { - Text( - text = "산책이 중단되었어요", - textAlign = TextAlign.Center, - style = PawKeyTheme.typography.head22B, - color = PawKeyTheme.colors.white1, - modifier = Modifier.fillMaxWidth() - ) - - Text( - text = "산책을 정말 종료하시겠어요?", - fontSize = 12.sp, - textAlign = TextAlign.Center, - style = PawKeyTheme.typography.body16M, - color = PawKeyTheme.colors.white2, - modifier = Modifier.fillMaxWidth() - ) - } - } - } - - Column ( - modifier = Modifier - .fillMaxWidth() - .padding(bottom = 16.dp, start = 16.dp, end = 16.dp) - .navigationBarsPadding(), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Row ( - modifier = Modifier - .fillMaxWidth() - ) { - FloatingActionButton( - shape = CircleShape, - onClick = onClickTracking, - containerColor = if (isTracking) PawKeyTheme.colors.green500 else Color.White, - ) { - Icon( - imageVector = ImageVector.vectorResource(R.drawable.ic_course_map_tap_location_on), - contentDescription = "내 위치",//stringResource(id = R.string.lo) - tint = Color.Black - ) - } - } - - if (isRecording) { - PawkeyButton( - text = "중지하기", - enabled = true, - onClick = { - onPauseTracking() - - // Todo : 맵 캡처 로직 변경 예정 - /*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입니다." - ) - } - } - } - } - }*/ - }, - modifier = Modifier - .padding(top = 16.dp) - .padding(bottom = 44.dp) - ) - } 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 = PawKeyTheme.colors.green500, - shape = RoundedCornerShape(8.dp) - ) - .padding(horizontal = 24.dp, vertical = 16.dp), - color = PawKeyTheme.colors.green500, - style = PawKeyTheme.typography.body16Sb - ) - - Spacer(modifier = Modifier.weight(1f)) - - Text( - text = "산책 종료하기", - modifier = Modifier - .clip(RoundedCornerShape(8.dp)) - .background( - color = PawKeyTheme.colors.green500, - shape = RoundedCornerShape(8.dp) - ) - .noRippleClickable { - onStopTracking() - } - .padding(horizontal = 28.dp, vertical = 16.dp), - color = PawKeyTheme.colors.white1, - style = PawKeyTheme.typography.body16Sb - ) - } - } - } - } - } - } -} - -fun captureMapToBitmap(surfaceView: GLSurfaceView, onCaptured: (Bitmap?) -> Unit) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - captureUsingPixelCopy(surfaceView, onCaptured) - } else { - surfaceView.queueEvent { - val egl = EGLContext.getEGL() as EGL10 - val gl = egl.eglGetCurrentContext().gl as GL10 - - val context = surfaceView.context - val density = context.resources.displayMetrics.density - val screenWidth = context.resources.displayMetrics.widthPixels - - val contentWidth = (screenWidth - 32) - val targetHeight = (156 * density).toInt() - - // OpenGL로 전체 비트맵 캡처 - val fullBitmap = createBitmapFromGLSurface(0, 0, surfaceView.width, surfaceView.height, gl) - - val croppedBitmap = fullBitmap?.let { bitmap -> - val centerX = bitmap.width / 2 - val centerY = bitmap.height / 2 - - val cropStartX = (centerX - contentWidth / 2).coerceAtLeast(0) - val cropStartY = (centerY - targetHeight / 2).coerceAtLeast(0) - - val safeWidth = minOf(contentWidth, bitmap.width - cropStartX) - val safeHeight = minOf(targetHeight, bitmap.height - cropStartY) - - Bitmap.createBitmap(bitmap, cropStartX, cropStartY, safeWidth, safeHeight) - } - - onCaptured(croppedBitmap) - } - } -} - -@RequiresApi(Build.VERSION_CODES.O) -fun captureUsingPixelCopy( - surfaceView: GLSurfaceView, - onCaptured: (Bitmap?) -> Unit -) { - val rawBitmap = Bitmap.createBitmap(surfaceView.width, surfaceView.height, Bitmap.Config.ARGB_8888) - - try { - PixelCopy.request(surfaceView, rawBitmap, { copyResult -> - if (copyResult == PixelCopy.SUCCESS) { - val croppedBitmap = cropCenterWithAspectRatio(rawBitmap, 16f / 11f) - onCaptured(croppedBitmap) - } else { - Log.e("PixelCopy", "PixelCopy 실패: $copyResult") - onCaptured(null) - } - }, Handler(Looper.getMainLooper())) - } catch (e: IllegalArgumentException) { - e.printStackTrace() - onCaptured(null) - } -} - -fun cropCenterWithAspectRatio( - bitmap: Bitmap, - targetAspectRatio: Float -): Bitmap { - val width = bitmap.width - val height = bitmap.height - val currentAspectRatio = width.toFloat() / height.toFloat() - - val cropWidth: Int - val cropHeight: Int - - if (currentAspectRatio > targetAspectRatio) { - // 현재 이미지가 더 넓음 → 좌우 잘라야 함 - cropHeight = height - cropWidth = (height * targetAspectRatio).toInt() - } else { - // 현재 이미지가 더 높음 → 위아래 잘라야 함 - cropWidth = width - cropHeight = (width / targetAspectRatio).toInt() - } - - val startX = ((width - cropWidth) / 2).coerceAtLeast(0) - val startY = ((height - cropHeight) / 2).coerceAtLeast(0) - - val safeWidth = minOf(cropWidth, width - startX) - val safeHeight = minOf(cropHeight, height - startY) - - return Bitmap.createBitmap(bitmap, startX, startY, safeWidth, safeHeight) -} - - -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) - - val targetAspectRatio = 16f / 11f - - 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) -} - -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 = PawKeyTheme.colors.green500, - 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 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 deleted file mode 100644 index 3d471ad0..00000000 --- a/app/src/main/java/com/paw/key/presentation/ui/course/walk/state/WalkCourseContract.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.paw.key.presentation.ui.course.walk.state - -import androidx.annotation.StringRes -import androidx.compose.runtime.Immutable -import com.paw.key.R -import com.paw.key.presentation.ui.course.walk.model.MapState -import com.paw.key.presentation.ui.course.walk.model.RecordingState -import com.paw.key.presentation.ui.course.walk.model.StepCounterState - -class WalkCourseContract { - @Immutable - data class WalkCourseState( - val recordingState: RecordingState = RecordingState(), - val mapState: MapState = MapState(), - val stepCounterState: StepCounterState = StepCounterState(), - val totalTimeMillis: Long = 0L, - ) - - sealed class WalkCourseSideEffect { - data class ShowSnackBar(val message: String) : WalkCourseSideEffect() - data object NavigateUp: WalkCourseSideEffect() - data class NavigateNext(val regionId: Int): 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/res/drawable/ic_home_fill.xml b/app/src/main/res/drawable/ic_home_fill.xml index 25f0d87e..81a55c0a 100644 --- a/app/src/main/res/drawable/ic_home_fill.xml +++ b/app/src/main/res/drawable/ic_home_fill.xml @@ -4,6 +4,7 @@ android:viewportWidth="24" android:viewportHeight="24"> - + android:strokeColor="#FF00D281" + android:strokeWidth="2" + android:pathData="M9.4 3.7c1.17-0.9 3.1-0.94 4.3-0.1v0.01l5.77 4.03c0.39 0.27 0.78 0.74 1.08 1.32 0.3 0.57 0.46 1.16 0.46 1.64v6.78c0 2-1.62 3.62-3.62 3.62H6.61c-2 0-3.62-1.63-3.62-3.63v-6.9-0.17c0.04-0.4 0.18-0.9 0.42-1.39 0.24-0.48 0.54-0.9 0.85-1.18L4.4 7.62l5-3.91Zm2.6 9.55c-0.96 0-1.75 0.79-1.75 1.75v3c0 0.96 0.79 1.75 1.75 1.75s1.75-0.79 1.75-1.75v-3c0-0.9-0.7-1.65-1.57-1.74H12Z"/> + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_home_linear.xml b/app/src/main/res/drawable/ic_home_linear.xml index 6daf1a99..0feb4162 100644 --- a/app/src/main/res/drawable/ic_home_linear.xml +++ b/app/src/main/res/drawable/ic_home_linear.xml @@ -4,8 +4,7 @@ android:viewportWidth="24" android:viewportHeight="24"> - + android:pathData="M9.25 3.51c1.25-0.97 3.3-1.02 4.6-0.1l5.76 4.03c0.43 0.3 0.85 0.8 1.16 1.4 0.32 0.6 0.49 1.23 0.49 1.76v6.78c0 2.14-1.73 3.87-3.87 3.87H6.61c-2.13 0-3.87-1.74-3.87-3.88v-6.9c0-0.49 0.16-1.09 0.44-1.67 0.29-0.58 0.67-1.07 1.06-1.38l5-3.9h0.01Zm2.75 10c-0.82 0-1.5 0.67-1.5 1.5V18c0 0.82 0.68 1.5 1.5 1.5s1.5-0.68 1.5-1.5v-3c0-0.82-0.68-1.5-1.5-1.5Z"/> + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_mypage_fill.xml b/app/src/main/res/drawable/ic_mypage_fill.xml index c6468d24..7c108fac 100644 --- a/app/src/main/res/drawable/ic_mypage_fill.xml +++ b/app/src/main/res/drawable/ic_mypage_fill.xml @@ -1,12 +1,14 @@ + android:width="22dp" + android:height="22dp" + android:viewportWidth="22" + android:viewportHeight="22"> + android:strokeColor="#FF00D281" + android:strokeWidth="2" + android:pathData="M2.2 18.8c0-3.46 2.9-6.26 8.8-6.26s8.8 2.8 8.8 6.26c0 0.55-0.4 1-0.9 1H3.1c-0.5 0-0.9-0.45-0.9-1Z"/> - + android:strokeColor="#FF00D281" + android:strokeWidth="2" + android:pathData="M14.3 5.5c0 1.82-1.48 3.3-3.3 3.3-1.82 0-3.3-1.48-3.3-3.3 0-1.82 1.48-3.3 3.3-3.3 1.82 0 3.3 1.48 3.3 3.3Z"/> + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_mypage_linear.xml b/app/src/main/res/drawable/ic_mypage_linear.xml index 49f81c65..f7010a05 100644 --- a/app/src/main/res/drawable/ic_mypage_linear.xml +++ b/app/src/main/res/drawable/ic_mypage_linear.xml @@ -1,20 +1,14 @@ + android:width="22dp" + android:height="22dp" + android:viewportWidth="22" + android:viewportHeight="22"> + android:pathData="M2.2 18.8c0-3.46 2.9-6.26 8.8-6.26s8.8 2.8 8.8 6.26c0 0.55-0.4 1-0.9 1H3.1c-0.5 0-0.9-0.45-0.9-1Z"/> - + android:pathData="M14.3 5.5c0 1.82-1.48 3.3-3.3 3.3-1.82 0-3.3-1.48-3.3-3.3 0-1.82 1.48-3.3 3.3-3.3 1.82 0 3.3 1.48 3.3 3.3Z"/> + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_route_recommand_fill.xml b/app/src/main/res/drawable/ic_route_recommand_fill.xml new file mode 100644 index 00000000..20603456 --- /dev/null +++ b/app/src/main/res/drawable/ic_route_recommand_fill.xml @@ -0,0 +1,13 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_route_recommand_linear.xml b/app/src/main/res/drawable/ic_route_recommand_linear.xml new file mode 100644 index 00000000..c7ad76b4 --- /dev/null +++ b/app/src/main/res/drawable/ic_route_recommand_linear.xml @@ -0,0 +1,12 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_walk_course_check.xml b/app/src/main/res/drawable/ic_walk_course_check.xml new file mode 100644 index 00000000..877b45f2 --- /dev/null +++ b/app/src/main/res/drawable/ic_walk_course_check.xml @@ -0,0 +1,13 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_walk_fill.xml b/app/src/main/res/drawable/ic_walk_fill.xml index 7aca239a..2b03f698 100644 --- a/app/src/main/res/drawable/ic_walk_fill.xml +++ b/app/src/main/res/drawable/ic_walk_fill.xml @@ -4,38 +4,28 @@ android:viewportWidth="24" android:viewportHeight="24"> + android:pathData="M11.838,12.3C12.161,12.271 12.461,12.288 12.693,12.354C12.925,12.42 13.157,12.535 13.369,12.691C13.837,13.035 14.218,13.54 14.638,14.177C15.03,14.772 15.5,15.551 16.122,16.193C16.495,16.577 16.939,16.926 17.257,17.196C17.604,17.491 17.864,17.739 18.051,18.012C18.482,18.642 18.541,19.538 18.194,20.23C17.848,20.921 17.096,21.42 16.315,21.475C15.649,21.522 15.049,21.318 14.073,21.114C12.888,20.866 11.6,20.776 10.33,21.04C9.921,21.124 9.471,21.258 9.153,21.34C8.798,21.432 8.506,21.491 8.232,21.5C7.374,21.526 6.492,20.978 6.1,20.19L6.027,20.03C5.71,19.23 5.881,18.22 6.452,17.532C6.499,17.476 6.548,17.422 6.599,17.372C6.744,17.227 6.916,17.09 7.135,16.922C7.339,16.766 7.601,16.571 7.844,16.352C8.325,15.918 8.703,15.425 9.025,14.964C9.377,14.461 9.606,14.088 9.921,13.664C10.295,13.161 10.691,12.759 11.128,12.514C11.337,12.397 11.578,12.324 11.838,12.3Z" + android:strokeWidth="2" + android:fillColor="#00000000" + android:strokeColor="#00D281"/> + android:pathData="M2.959,9.618C3.257,9.349 3.531,9.314 3.82,9.389C4.161,9.477 4.554,9.737 4.891,10.128H4.892C5.257,10.553 5.424,10.958 5.58,11.367C5.788,11.913 5.922,12.415 5.928,12.875C5.935,13.478 5.697,13.944 5.388,14.131C4.979,14.379 4.433,14.427 3.949,14.248C3.433,14.058 3.065,13.562 2.819,12.87C2.58,12.196 2.528,11.531 2.51,11.309C2.476,10.893 2.534,10.599 2.572,10.448C2.674,10.04 2.753,9.803 2.959,9.618Z" + android:strokeWidth="2" + android:fillColor="#00000000" + android:strokeColor="#00D281"/> + android:pathData="M20.336,9.374C20.62,9.314 20.798,9.364 20.908,9.444C21.017,9.522 21.143,9.691 21.267,10.007C21.392,10.329 21.466,10.667 21.49,11.01C21.568,12.1 21.143,13.223 20.365,14.002C20.017,14.35 19.571,14.426 19.143,14.29C18.696,14.148 18.32,13.786 18.188,13.31C18.058,12.843 18.106,12.281 18.31,11.708C18.59,10.925 18.986,10.237 19.512,9.789C19.739,9.595 20.046,9.434 20.336,9.374Z" + android:strokeWidth="2" + android:fillColor="#00000000" + android:strokeColor="#00D281"/> + android:pathData="M9.314,3.352C9.617,3.422 9.931,3.684 10.178,4.144C10.539,4.819 10.607,5.637 10.558,6.612C10.507,7.603 10.393,8.353 9.986,8.877C9.429,9.595 8.407,9.701 7.845,9.208C7.777,9.148 7.711,9.078 7.649,9C7.071,8.264 6.79,7.115 6.85,6.251C6.906,5.43 7.388,4.375 8.043,3.775C8.44,3.411 8.94,3.265 9.314,3.352Z" + android:strokeWidth="2" + android:fillColor="#00000000" + android:strokeColor="#00D281"/> + android:pathData="M14.672,2.505C15.02,2.475 15.418,2.598 15.747,2.841L15.884,2.952C16.523,3.53 16.957,4.557 17.028,5.455C17.125,6.669 16.636,8.02 15.788,8.611C15.388,8.89 14.795,8.879 14.327,8.531C13.988,8.28 13.722,7.873 13.569,7.383C13.374,6.755 13.333,6.038 13.352,5.258C13.369,4.603 13.473,3.863 13.725,3.304C13.964,2.773 14.26,2.54 14.672,2.505Z" + android:strokeWidth="2" + android:fillColor="#00000000" + android:strokeColor="#00D281"/> diff --git a/app/src/main/res/drawable/ic_walk_linear.xml b/app/src/main/res/drawable/ic_walk_linear.xml index 0ec37a63..6b083c10 100644 --- a/app/src/main/res/drawable/ic_walk_linear.xml +++ b/app/src/main/res/drawable/ic_walk_linear.xml @@ -1,41 +1,31 @@ + android:strokeColor="#9C9C9C"/> + android:strokeColor="#9C9C9C"/> + android:strokeColor="#9C9C9C"/> + android:strokeColor="#9C9C9C"/> + android:strokeColor="#9C9C9C"/> diff --git a/app/src/main/res/drawable/ic_walk_review_dialog_paw.xml b/app/src/main/res/drawable/ic_walk_review_dialog_paw.xml new file mode 100644 index 00000000..ae7c9395 --- /dev/null +++ b/app/src/main/res/drawable/ic_walk_review_dialog_paw.xml @@ -0,0 +1,28 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/img_walk_info.png b/app/src/main/res/drawable/img_walk_info.png new file mode 100644 index 0000000000000000000000000000000000000000..fde0e3a890820bb84653b71d21cade9b93e6aef9 GIT binary patch literal 10143 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 Date: Mon, 5 Jan 2026 12:53:05 +0900 Subject: [PATCH 03/47] =?UTF-8?q?mod/#154=20=EB=A9=94=EC=9D=B8=20=EB=B7=B0?= =?UTF-8?q?=20=EC=88=98=EC=A0=95=20-=20=EB=B0=9C=EC=9E=90=EA=B5=ADx,=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=20=ED=8C=A8=EB=94=A9,=20=EB=B0=94=ED=85=80?= =?UTF-8?q?=EB=B0=94=20=EA=B5=AC=EC=A1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../key/presentation/ui/main/MainNavigator.kt | 71 +-------- .../key/presentation/ui/main/MainScreen.kt | 85 +++------- .../paw/key/presentation/ui/main/MainTab.kt | 23 ++- .../ui/main/component/MainBottomBar.kt | 145 ++++++------------ .../ui/main/viewmodel/MainViewModel.kt | 1 + 5 files changed, 93 insertions(+), 232 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 09fdbe64..1d135083 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,12 +10,8 @@ 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.entire.navigation.navigateCourse -import com.paw.key.presentation.ui.course.sharedwalk.complete.navigation.navigateSharedWalkCompletion -import com.paw.key.presentation.ui.course.sharedwalk.review.navigation.navigateSharedWalkReview -import com.paw.key.presentation.ui.course.sharedwalk.sharedroute.navigation.navigateSharedWalkCourse -import com.paw.key.presentation.ui.course.walk.navigation.navigateWalkCourse -import com.paw.key.presentation.ui.course.walkcomplete.navigation.navigateWalkCompletion +import com.paw.key.presentation.ui.course.navigation.navigateWalkCourse +import com.paw.key.presentation.ui.course.navigation.navigateWalkPrepare import com.paw.key.presentation.ui.course.walkreview.navigation.navigateWalkReview import com.paw.key.presentation.ui.dummy.next.navigateDummyNext import com.paw.key.presentation.ui.home.navigation.navigateHome @@ -64,8 +60,8 @@ class MainNavigator( when (tab) { MainTab.HOME -> navController.navigateHome(navOptions) - MainTab.COURSE -> navController.navigateCourse(navOptions) - MainTab.COMMUNITY -> navController.navigateCommunity(navOptions) + MainTab.COURSE -> navController.navigateWalkPrepare(navOptions) + MainTab.ROUTERECOMMAND -> navController.navigateCommunity(navOptions) MainTab.MYPAGE -> navController.navigateMyPage(navOptions) } } @@ -121,12 +117,6 @@ class MainNavigator( navController.navigateArchivedCourse(navOptions = navOptions) } - fun navigateCourse(index : Int = 0, navOptions: NavOptions? = null) { - navController.navigateCourse( - index = index, - navOptions = navOptions - ) - } fun navigateHome(navOptions: NavOptions? = null) { navController.navigateHome(navOptions = navOptions) @@ -136,46 +126,6 @@ class MainNavigator( navController.navigateHomeLocationSetting(navOptions = navOptions) } - /*메인 탭 산택 기준 - 산책하기, 완료, 리뷰*/ -// fun navigateRegional(navOptions: NavOptions? = null) { -// navController.navigateRegional(navOptions = navOptions) -// } - fun navigateSharedWalkCourse( - routeId: Int, - pageId : Int, - navOptions: NavOptions? = null) - { - navController.navigateSharedWalkCourse( - routeId = routeId, - pageId = pageId, - navOptions = navOptions - ) - } - - fun navigateSharedWalkReview( - pageId: Int, - routeId: Int, - navOptions: NavOptions? = null - ) { - navController.navigateSharedWalkReview( - pageId = pageId, - routeId = routeId, - navOptions = navOptions - ) - } - - fun navigateSharedWalkCompletion( - pageId: Int, - routeId: Int, - navOptions: NavOptions? = null - ) { - navController.navigateSharedWalkCompletion( - pageId = pageId, - routeId = routeId, - navOptions = navOptions - ) - } - fun navigateArchivedDetail( routeId: Int, pageId : Int, @@ -192,20 +142,15 @@ class MainNavigator( navController.navigateWalkCourse(navOptions = navOptions) } - fun navigateWalkCompletion(routeId : Int, navOptions: NavOptions? = null) { - navController.navigateWalkCompletion( - routeId = routeId, - navOptions = navOptions - ) - } - - fun navigateWalkReview(routeId: Int, navOptions: NavOptions? = null) { + fun navigateWalkReview( + navOptions: NavOptions? = null + ) { navController.navigateWalkReview( - routeId = routeId, navOptions = navOptions ) } + fun navigateDummyNext(navOptions: NavOptions? = null) { navController.navigateDummyNext(navOptions = navOptions) } 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 0eb37309..90ae2dce 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 @@ -5,56 +5,30 @@ import android.os.Build import android.widget.Toast import androidx.activity.compose.BackHandler import androidx.annotation.RequiresApi -import androidx.compose.foundation.gestures.detectTapGestures -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.only -import androidx.compose.foundation.layout.systemBars -import androidx.compose.foundation.layout.systemBarsPadding -import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableLongStateOf -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalContext -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.paw.key.presentation.animation.FootprintAnimationScreen import com.paw.key.presentation.ui.main.component.MainBottomBar -import com.paw.key.presentation.ui.main.state.MainContract -import com.paw.key.presentation.ui.main.viewmodel.MainViewModel import kotlinx.collections.immutable.toImmutableList -import kotlinx.collections.immutable.toPersistentList @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) @Composable -fun MainRoute( - viewModel: MainViewModel = hiltViewModel(), -) { - val state by viewModel.state.collectAsStateWithLifecycle() - - MainScreen( - footprints = state.footprint, - addFootprint = viewModel::addFootprint, - removeFootprint = viewModel::removeFootprint - ) +fun MainRoute() { + MainScreen() } @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) @Composable fun MainScreen( - footprints: List, - addFootprint: (MainContract.Footprint) -> Unit, - removeFootprint: (MainContract.Footprint) -> Unit, navigator: MainNavigator = rememberMainNavigator(), ) { val context = LocalContext.current @@ -81,13 +55,6 @@ fun MainScreen( MainScreenContent( navigator = navigator, snackBarHostState = snackBarHostState, - footprints = footprints, - addFootprint = { footprint -> - addFootprint(footprint) - }, - removeFootprint = { footprint -> - removeFootprint(footprint) - } ) } @@ -96,42 +63,34 @@ fun MainScreen( private fun MainScreenContent( navigator: MainNavigator, snackBarHostState: SnackbarHostState, - footprints: List, - addFootprint: (MainContract.Footprint) -> Unit, - removeFootprint: (MainContract.Footprint) -> Unit, ) { - Box( + Scaffold ( + bottomBar = { + MainBottomBar( + isVisible = navigator.showBottomBar(), + tabs = MainTab.entries.toImmutableList(), + currentTab = navigator.currentTab, + onTabSelected = navigator::navigate, + ) + }, modifier = Modifier - .windowInsetsPadding(WindowInsets.systemBars.only(WindowInsetsSides.Top)) // 시스템 바들 중 현재는 탑만, 유연성을 위해 사용 - .systemBarsPadding() .fillMaxSize() - .pointerInput(Unit) { - detectTapGestures { offset -> - addFootprint(MainContract.Footprint(position = offset)) - } - } - ) { + .navigationBarsPadding() + .statusBarsPadding() + ) { innerPadding -> PawKeyNavHost( navigator = navigator, - paddingValues = PaddingValues(), + paddingValues = innerPadding, snackbarHostState = snackBarHostState ) + } +} - MainBottomBar( - isVisible = navigator.showBottomBar(), - tabs = MainTab.entries.toImmutableList(), - currentTab = navigator.currentTab, - onTabSelected = navigator::navigate, - modifier = Modifier.align(Alignment.BottomCenter) - ) - - FootprintAnimationScreen( +/*FootprintAnimationScreen( footprints = footprints.toPersistentList(), onAnimationFinished = { finishedFootprint -> removeFootprint(finishedFootprint) }, modifier = Modifier .fillMaxSize() - ) - } -} \ No newline at end of file + )*/ \ No newline at end of file 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 cff8dab9..6adb4d70 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 @@ -4,16 +4,15 @@ import androidx.annotation.DrawableRes import androidx.annotation.StringRes 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.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 import com.paw.key.R.string.ic_course_description -import com.paw.key.R.string.ic_community_description +import com.paw.key.R.string.ic_home_description import com.paw.key.R.string.ic_mypage_description +import com.paw.key.core.navigation.MainTabRoute import com.paw.key.core.navigation.Route +import com.paw.key.presentation.ui.community.navigation.Community +import com.paw.key.presentation.ui.course.navigation.WalkPrepare +import com.paw.key.presentation.ui.home.navigation.Home +import com.paw.key.presentation.ui.mypage.navigation.MyPage enum class MainTab( @@ -32,12 +31,12 @@ enum class MainTab( selectedIcon = R.drawable.ic_walk_fill, unselectedIcon = R.drawable.ic_walk_linear, contentDescription = ic_course_description, - route = Course(), + route = WalkPrepare, ), - COMMUNITY( - selectedIcon = R.drawable.ic_community_fill, - unselectedIcon = R.drawable.ic_community_linear, - contentDescription = ic_community_description, + ROUTERECOMMAND( + selectedIcon = R.drawable.ic_route_recommand_fill, + unselectedIcon = R.drawable.ic_route_recommand_linear, + contentDescription = R.string.ic_route_recommand_description, route = Community, ), MYPAGE( 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 1bca3d2a..55c1739a 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 @@ -1,29 +1,30 @@ package com.paw.key.presentation.ui.main.component import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.EnterTransition -import androidx.compose.animation.ExitTransition -import androidx.compose.animation.core.animateDpAsState -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideIn +import androidx.compose.animation.slideOut +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.selection.selectableGroup import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.material3.Surface +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf 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.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.layout.onGloballyPositioned -import androidx.compose.ui.layout.positionInParent -import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview @@ -35,7 +36,6 @@ import com.paw.key.presentation.ui.main.MainTab import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList -import com.paw.key.R @Composable fun MainBottomBar( @@ -45,83 +45,32 @@ fun MainBottomBar( onTabSelected: (MainTab) -> Unit, modifier: Modifier = Modifier, ) { - val tabPositions = remember { - mutableStateListOf() - } - - val density = LocalDensity.current - AnimatedVisibility( visible = isVisible, - enter = EnterTransition.None, - exit = ExitTransition.None, + enter = fadeIn() + slideIn { IntOffset(0, it.height) }, + exit = fadeOut() + slideOut { IntOffset(0, it.height) }, modifier = modifier ) { - Box( - modifier = Modifier - .background(Color.Transparent) - .fillMaxWidth() - .padding(bottom = 12.dp), - contentAlignment = Alignment.Center + Surface( + shape = RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp), + color = Color.White, + shadowElevation = 10.dp, + modifier = Modifier.fillMaxWidth() ) { - Surface( - color = PawKeyTheme.colors.gray950, - shape = RoundedCornerShape(200.dp), - shadowElevation = 10.dp, + Row( modifier = Modifier + .padding(top = 16.dp, bottom = 10.dp, start = 32.dp, end = 32.dp) + .selectableGroup(), + horizontalArrangement = Arrangement.spacedBy(40.dp, Alignment.CenterHorizontally), + verticalAlignment = Alignment.CenterVertically ) { - val selectedIndex = tabs.indexOf(currentTab) - - // 애니메이션 - density로 - val targetOffsetX by animateDpAsState( - targetValue = with(density) { - tabPositions.getOrNull(selectedIndex)?.x?.toDp() ?: 0.dp - }, - label = stringResource(R.string.main_bottom_bar_animation_label) - ) - - Box( - modifier = Modifier - .padding(horizontal = 12.dp, vertical = 8.dp) - .wrapContentSize() - ) { - // 물방울 모양 - if (selectedIndex != -1 && tabPositions.size == tabs.size) { - Box( - modifier = Modifier - .offset { - IntOffset(targetOffsetX.roundToPx(), 0) - } - .size(48.dp) - .clip(RoundedCornerShape(50)) - .background(Color.White) - ) - } - - Row( - modifier = Modifier - .selectableGroup(), - horizontalArrangement = Arrangement.SpaceAround, - verticalAlignment = Alignment.CenterVertically - ) { - tabs.forEachIndexed { index, tab -> - MainNavigationBarItem( - selected = tab == currentTab, - tab = tab, - onClick = { onTabSelected(tab) }, - modifier = Modifier - .padding(horizontal = 6.dp) - .onGloballyPositioned { - val pos = it.positionInParent() - if (tabPositions.size <= index) { - tabPositions.add(pos) - } else { - tabPositions[index] = pos - } - } - ) - } - } + tabs.forEach { tab -> + MainNavigationBarItem( + tab = tab, + selected = tab == currentTab, + onClick = { onTabSelected(tab) }, + modifier = Modifier.weight(1f) + ) } } } @@ -136,21 +85,29 @@ private fun MainNavigationBarItem( modifier: Modifier = Modifier, ) { val iconRes = if (selected) tab.selectedIcon else tab.unselectedIcon - - Box( + + Column ( modifier = modifier - .noRippleClickable(onClick) - .clip(RoundedCornerShape(24.dp)) - .size(48.dp), - contentAlignment = Alignment.Center + .noRippleClickable(onClick), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center ) { Icon( imageVector = ImageVector.vectorResource(iconRes), - contentDescription = stringResource(tab.contentDescription), - modifier = Modifier - .padding(12.dp) - .size(24.dp), - tint = Color.Unspecified + contentDescription = tab.contentDescription.toString(), + tint = Color.Unspecified, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = stringResource(tab.contentDescription), + style = PawKeyTheme.typography.buttonSmall, + color = if (selected) { + PawKeyTheme.colors.primary + } else { + PawKeyTheme.colors.defaultMiddle + } ) } } @@ -162,7 +119,7 @@ private fun MainBottomBarPreview() { val dummyTabs = persistentListOf( MainTab.HOME, MainTab.COURSE, - MainTab.COMMUNITY, + MainTab.ROUTERECOMMAND, MainTab.MYPAGE ) diff --git a/app/src/main/java/com/paw/key/presentation/ui/main/viewmodel/MainViewModel.kt b/app/src/main/java/com/paw/key/presentation/ui/main/viewmodel/MainViewModel.kt index 6a3fbcb0..88f4b4fa 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/main/viewmodel/MainViewModel.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/main/viewmodel/MainViewModel.kt @@ -8,6 +8,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import javax.inject.Inject +@Deprecated("발자국 안쓸거임") @HiltViewModel class MainViewModel @Inject constructor( ) : ViewModel() { From 1961553b2db971b46d75ef5235c79f7f712511aa Mon Sep 17 00:00:00 2001 From: sonms Date: Mon, 5 Jan 2026 12:53:44 +0900 Subject: [PATCH 04/47] =?UTF-8?q?add/#154=20strings,=20=EC=83=89=EC=83=81?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../paw/key/core/designsystem/theme/Color.kt | 27 ++++++++++++++++--- app/src/main/res/values/strings.xml | 7 ++--- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/paw/key/core/designsystem/theme/Color.kt b/app/src/main/java/com/paw/key/core/designsystem/theme/Color.kt index d689c2cd..663670f1 100644 --- a/app/src/main/java/com/paw/key/core/designsystem/theme/Color.kt +++ b/app/src/main/java/com/paw/key/core/designsystem/theme/Color.kt @@ -1,6 +1,5 @@ package com.paw.key.core.designsystem.theme -import android.annotation.SuppressLint import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -47,7 +46,6 @@ val System_green = Color(0xFF40C927) /*------------------------------------------------*/ // Brand -@SuppressLint("InvalidColorHexValue") val Opacity5Primary = Color(0x0D00D281) val Opacity25Primary = Color(0x4000D281) @@ -60,6 +58,8 @@ val Primary = Color(0xFF00D281) val PrimaryGra6 = Color(0xFF00A86B) val PrimaryGra7 = Color(0xFF007A50) +val DokiRed = Color(0xFFFF646C) + // contents val Contents = Color(0xFF171717) @@ -68,11 +68,13 @@ val Background = Color(0xFFFFFFFF) // default val DefaultButton = Color(0xFFEEEEEE) + +val DefaultBright = Color(0xFFF2F2F2) + val DefaultMiddle = Color(0xFF9C9C9C) val DefaultDark = Color(0xFF555555) - @Stable class PawKeyColors( green50: Color, @@ -119,9 +121,11 @@ class PawKeyColors( primaryGra6: Color, primaryGra7: Color, primary: Color, + dokiRed: Color, contents: Color, background: Color, defaultButton: Color, + defaultBright: Color, defaultMiddle: Color, defaultDark: Color, ) { @@ -207,12 +211,17 @@ class PawKeyColors( private set var primary: Color by mutableStateOf(primary) private set + + var dokiRed: Color by mutableStateOf(dokiRed) + private set var contents: Color by mutableStateOf(contents) private set var background: Color by mutableStateOf(background) private set var defaultButton: Color by mutableStateOf(defaultButton) private set + var defaultBright: Color by mutableStateOf(defaultBright) + private set var defaultMiddle: Color by mutableStateOf(defaultMiddle) private set var defaultDark: Color by mutableStateOf(defaultDark) @@ -251,9 +260,11 @@ class PawKeyColors( primaryGra6: Color = this.primaryGra6, primaryGra7: Color = this.primaryGra7, primary: Color = this.primary, + dokiRed: Color = this.dokiRed, contents: Color = this.contents, background: Color = this.background, defaultButton: Color = this.defaultButton, + defaultBright: Color = this.defaultBright, defaultMiddle: Color = this.defaultMiddle, defaultDark: Color = this.defaultDark, @@ -303,11 +314,13 @@ class PawKeyColors( primaryGra6 = primaryGra6, primaryGra7 = primaryGra7, primary = primary, + dokiRed = dokiRed, contents = contents, background = background, defaultButton = defaultButton, defaultMiddle = defaultMiddle, - defaultDark = defaultDark, + defaultBright = defaultBright, + defaultDark = defaultDark ) fun update(other: PawKeyColors) { @@ -354,9 +367,11 @@ class PawKeyColors( primaryGra6 = other.primaryGra6 primaryGra7 = other.primaryGra7 primary = other.primary + dokiRed = other.dokiRed contents = other.contents background = other.background defaultButton = other.defaultButton + defaultBright = other.defaultBright defaultMiddle = other.defaultMiddle defaultDark = other.defaultDark } @@ -407,9 +422,11 @@ fun pawKeyColors( primaryGra6: Color = PrimaryGra6, primaryGra7: Color = PrimaryGra7, primary: Color = Primary, + dokiRed: Color = DokiRed, contents: Color = Contents, background: Color = Background, defaultButton: Color = DefaultButton, + defaultBright: Color = DefaultBright, defaultMiddle: Color = DefaultMiddle, defaultDark: Color = DefaultDark, ) = PawKeyColors( @@ -457,9 +474,11 @@ fun pawKeyColors( primaryGra6 = primaryGra6, primaryGra7 = primaryGra7, primary = primary, + dokiRed = dokiRed, contents = contents, background = background, defaultButton = defaultButton, + defaultBright = defaultBright, defaultMiddle = defaultMiddle, defaultDark = defaultDark, ) \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 93b322c1..45a126b7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -59,12 +59,12 @@ - home + 연속산책 내 동네 설정 - course + 산책하기 산책 기록하기 후기 작성하기 @@ -100,7 +100,8 @@ 둑닥둑닥ing... - mypage + 마이페이지 profile + 루트 추천 \ No newline at end of file From 5069d2c6a7b31826dc49ae56c785623365366263 Mon Sep 17 00:00:00 2001 From: sonms Date: Mon, 5 Jan 2026 12:55:38 +0900 Subject: [PATCH 05/47] =?UTF-8?q?mod/#154=20=EA=B3=B5=ED=86=B5=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20?= =?UTF-8?q?=EA=B8=B0=EC=A1=B4=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=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 --- .../component/DokiBorderButton.kt | 69 +++++++++++++++++++ .../{DogkyButton.kt => DokiButton.kt} | 28 +++++--- .../core/designsystem/component/InfoChip.kt | 63 +++++++++++++++++ .../key/core/designsystem/component/TopBar.kt | 47 ++++++++----- .../java/com/paw/key/core/util/UiState.kt | 3 + 5 files changed, 183 insertions(+), 27 deletions(-) create mode 100644 app/src/main/java/com/paw/key/core/designsystem/component/DokiBorderButton.kt rename app/src/main/java/com/paw/key/core/designsystem/component/{DogkyButton.kt => DokiButton.kt} (66%) create mode 100644 app/src/main/java/com/paw/key/core/designsystem/component/InfoChip.kt diff --git a/app/src/main/java/com/paw/key/core/designsystem/component/DokiBorderButton.kt b/app/src/main/java/com/paw/key/core/designsystem/component/DokiBorderButton.kt new file mode 100644 index 00000000..0d071584 --- /dev/null +++ b/app/src/main/java/com/paw/key/core/designsystem/component/DokiBorderButton.kt @@ -0,0 +1,69 @@ +package com.paw.key.core.designsystem.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.paw.key.core.designsystem.theme.PawKeyTheme +import com.paw.key.core.extension.noRippleClickable + +@Composable +fun DokiBorderButton( + text: String, + enabled: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + val textColor = when { + enabled -> PawKeyTheme.colors.primary + else -> PawKeyTheme.colors.defaultMiddle + } + + Box( + modifier = modifier + .fillMaxWidth() + .border( + width = 1.dp, + color = textColor, + shape = RoundedCornerShape(8.dp) + ) + .background( + color = PawKeyTheme.colors.background, + shape = RoundedCornerShape(8.dp) + ) + .noRippleClickable { + if (enabled) onClick() + } + .padding(vertical = 18.dp), + contentAlignment = Alignment.Center + ) { + Text( + text = text, + style = PawKeyTheme.typography.mainButtonActive, + color = textColor, + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center + ) + } +} + +@Preview +@Composable +private fun DokiBorderButtonPreview() { + PawKeyTheme { + DokiBorderButton( + text = "산책 기록하기", + enabled = true, + onClick = {} + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/core/designsystem/component/DogkyButton.kt b/app/src/main/java/com/paw/key/core/designsystem/component/DokiButton.kt similarity index 66% rename from app/src/main/java/com/paw/key/core/designsystem/component/DogkyButton.kt rename to app/src/main/java/com/paw/key/core/designsystem/component/DokiButton.kt index a2379142..a7541bd2 100644 --- a/app/src/main/java/com/paw/key/core/designsystem/component/DogkyButton.kt +++ b/app/src/main/java/com/paw/key/core/designsystem/component/DokiButton.kt @@ -2,6 +2,7 @@ package com.paw.key.core.designsystem.component import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape @@ -15,11 +16,12 @@ import com.paw.key.core.designsystem.theme.PawKeyTheme import com.paw.key.core.extension.noRippleClickable @Composable -fun DogkyButton( +fun DokiButton( text: String, enabled: Boolean, onClick: () -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, + isDialog: Boolean = false, ) { val backgroundColor = when { enabled -> PawKeyTheme.colors.primary @@ -28,22 +30,26 @@ fun DogkyButton( val textColor = when { enabled -> PawKeyTheme.colors.background - else -> PawKeyTheme.colors.defaultMiddle + else -> PawKeyTheme.colors.defaultDark } Box( modifier = modifier .fillMaxWidth() .background(backgroundColor, shape = RoundedCornerShape(8.dp)) - .noRippleClickable { - if (enabled) onClick() - } - .padding(vertical = 14.dp), + .noRippleClickable(onClick = onClick) + .padding( + if (isDialog) { + PaddingValues(horizontal = 6.dp, vertical = 17.dp) + } else { + PaddingValues(vertical = 18.dp) + } + ), contentAlignment = Alignment.Center ) { Text( text = text, - style = PawKeyTheme.typography.mainButtonDefault, + style = if (isDialog) PawKeyTheme.typography.subTitle else PawKeyTheme.typography.mainButtonDefault, color = textColor ) } @@ -53,9 +59,9 @@ fun DogkyButton( @Composable private fun DogkyButtonPreview() { PawKeyTheme { - DogkyButton( - text = "", - enabled = true, + DokiButton( + text = "산책 종료하기", + enabled = false, onClick = {} ) } diff --git a/app/src/main/java/com/paw/key/core/designsystem/component/InfoChip.kt b/app/src/main/java/com/paw/key/core/designsystem/component/InfoChip.kt new file mode 100644 index 00000000..b8d47080 --- /dev/null +++ b/app/src/main/java/com/paw/key/core/designsystem/component/InfoChip.kt @@ -0,0 +1,63 @@ +package com.paw.key.core.designsystem.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.paw.key.core.designsystem.theme.PawKeyTheme + +@Composable +fun InfoChip( + text: String, + modifier: Modifier = Modifier, + isActionChip: Boolean = false, +) { + val backGroundColor = if (isActionChip) { + PawKeyTheme.colors.primary + } else { + PawKeyTheme.colors.primaryGra1 + } + + val textColor = if (isActionChip) { + PawKeyTheme.colors.background + } else { + PawKeyTheme.colors.primary + } + + val fontStyle = if (isActionChip) { + PawKeyTheme.typography.bodySmall + } else { + PawKeyTheme.typography.subButtonDefault + } + + Box ( + modifier = modifier + .background( + color = backGroundColor, + shape = RoundedCornerShape(8.dp) + ) + .padding(horizontal = 8.dp, vertical = 4.dp) + ) { + Text( + text = text, + color = textColor, + style = fontStyle + ) + } +} + +@Preview +@Composable +private fun InfoChipPreview() { + PawKeyTheme { + InfoChip( + text = "2.2 km", + isActionChip = true + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/core/designsystem/component/TopBar.kt b/app/src/main/java/com/paw/key/core/designsystem/component/TopBar.kt index c419932c..6294d862 100644 --- a/app/src/main/java/com/paw/key/core/designsystem/component/TopBar.kt +++ b/app/src/main/java/com/paw/key/core/designsystem/component/TopBar.kt @@ -2,8 +2,10 @@ package com.paw.key.core.designsystem.component import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -20,33 +22,46 @@ import com.paw.key.core.extension.noRippleClickable @Composable fun TopBar( title: String, - onBackClick: () -> Unit, modifier: Modifier = Modifier, + onBackClick: () -> Unit = {}, onClickTitle : () -> Unit = {}, isBackVisible: Boolean = true, ) { - Box( + Column ( modifier = modifier - .fillMaxWidth() - .background(color = PawKeyTheme.colors.white1) - .padding(vertical = 12.dp, horizontal = 16.dp) ) { - if (isBackVisible) { - Icon( - imageVector = ImageVector.vectorResource(R.drawable.ic_arrow_left_black), - contentDescription = "뒤로가기", + Box( + modifier = Modifier + .fillMaxWidth() + .background(color = PawKeyTheme.colors.background) + .padding(vertical = 12.dp, horizontal = 16.dp) + ) { + if (isBackVisible) { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_arrow_left_black), + contentDescription = "뒤로가기", + modifier = Modifier + .align(Alignment.CenterStart) + .noRippleClickable(onClick = onBackClick) + ) + } + + Text( + text = title, + style = PawKeyTheme.typography.subTitle, modifier = Modifier - .align(Alignment.CenterStart) - .noRippleClickable { onBackClick() } + .align(Alignment.Center) + .noRippleClickable(onClickTitle) ) } - Text( - text = title, - style = PawKeyTheme.typography.head18Sb, + HorizontalDivider( + thickness = 1.dp, modifier = Modifier - .align(Alignment.Center) - .noRippleClickable(onClickTitle) + .fillMaxWidth() + .background( + color = PawKeyTheme.colors.defaultButton + ) ) } } diff --git a/app/src/main/java/com/paw/key/core/util/UiState.kt b/app/src/main/java/com/paw/key/core/util/UiState.kt index c8731e34..945009b9 100644 --- a/app/src/main/java/com/paw/key/core/util/UiState.kt +++ b/app/src/main/java/com/paw/key/core/util/UiState.kt @@ -1,5 +1,8 @@ package com.paw.key.core.util +import androidx.compose.runtime.Stable + +@Stable sealed interface UiState { data object Empty : UiState From 6fa2dca7296b0c84092269f430d5a9632e04cdbb Mon Sep 17 00:00:00 2001 From: sonms Date: Mon, 5 Jan 2026 12:56:35 +0900 Subject: [PATCH 06/47] =?UTF-8?q?mod/#154=20=ED=8C=8C=EC=9D=BC=20=EB=94=94?= =?UTF-8?q?=EB=A0=89=ED=86=A0=EB=A6=AC=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20?= =?UTF-8?q?=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{walk => walkcourse}/model/MapState.kt | 2 +- .../model/RecordingState.kt | 2 +- .../model/StepCounterState.kt | 2 +- .../key/presentation/ui/main/PawKeyNavHost.kt | 98 ++----------------- .../ui/onboard/OnboardingScreen.kt | 4 +- .../presentation/ui/signup/SignUpScreen.kt | 4 +- 6 files changed, 16 insertions(+), 96 deletions(-) rename app/src/main/java/com/paw/key/presentation/ui/course/{walk => walkcourse}/model/MapState.kt (91%) rename app/src/main/java/com/paw/key/presentation/ui/course/{walk => walkcourse}/model/RecordingState.kt (82%) rename app/src/main/java/com/paw/key/presentation/ui/course/{walk => walkcourse}/model/StepCounterState.kt (75%) diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walk/model/MapState.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/model/MapState.kt similarity index 91% rename from app/src/main/java/com/paw/key/presentation/ui/course/walk/model/MapState.kt rename to app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/model/MapState.kt index 3a876f5d..1a66f2b7 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/course/walk/model/MapState.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/model/MapState.kt @@ -1,4 +1,4 @@ -package com.paw.key.presentation.ui.course.walk.model +package com.paw.key.presentation.ui.course.walkcourse.model import android.graphics.Bitmap import androidx.compose.runtime.Immutable diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walk/model/RecordingState.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/model/RecordingState.kt similarity index 82% rename from app/src/main/java/com/paw/key/presentation/ui/course/walk/model/RecordingState.kt rename to app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/model/RecordingState.kt index 4bf4b5eb..571050f7 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/course/walk/model/RecordingState.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/model/RecordingState.kt @@ -1,4 +1,4 @@ -package com.paw.key.presentation.ui.course.walk.model +package com.paw.key.presentation.ui.course.walkcourse.model import androidx.compose.runtime.Immutable diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walk/model/StepCounterState.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/model/StepCounterState.kt similarity index 75% rename from app/src/main/java/com/paw/key/presentation/ui/course/walk/model/StepCounterState.kt rename to app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/model/StepCounterState.kt index d0b6d054..90e53ea3 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/course/walk/model/StepCounterState.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/model/StepCounterState.kt @@ -1,4 +1,4 @@ -package com.paw.key.presentation.ui.course.walk.model +package com.paw.key.presentation.ui.course.walkcourse.model import androidx.compose.runtime.Immutable 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 efd65292..f0b0faf3 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 @@ -12,13 +12,7 @@ import androidx.compose.ui.Modifier import androidx.navigation.compose.NavHost import androidx.navigation.navOptions import com.paw.key.presentation.ui.community.navigation.communityNavGraph -import com.paw.key.presentation.ui.course.entire.navigation.courseNavGraph -import com.paw.key.presentation.ui.course.entire.navigation.navigateCourse -import com.paw.key.presentation.ui.course.sharedwalk.complete.navigation.sharedWalkCompletionNavGraph -import com.paw.key.presentation.ui.course.sharedwalk.review.navigation.sharedWalkReviewNavGraph -import com.paw.key.presentation.ui.course.sharedwalk.sharedroute.navigation.sharedWalkCourseNavGraph -import com.paw.key.presentation.ui.course.walk.navigation.walkCourseNavGraph -import com.paw.key.presentation.ui.course.walkcomplete.navigation.walkCompletionNavGraph +import com.paw.key.presentation.ui.course.navigation.walkCourseGraph import com.paw.key.presentation.ui.course.walkreview.navigation.walkReviewNavGraph import com.paw.key.presentation.ui.dummy.navigation.dummyNavGraph import com.paw.key.presentation.ui.dummy.next.dummyNextNavGraph @@ -76,7 +70,7 @@ fun PawKeyNavHost( homeNavGraph( paddingValues = paddingValues, navigateUp = navigator::navigateUp, - navigateNext = navigator::navigateCourse, + navigateNext = navigator::navigateWalkCourse, navigateHomeLocationSetting = navigator::navigateHomeLocationSetting, modifier = modifier, ) @@ -94,87 +88,16 @@ fun PawKeyNavHost( modifier = modifier, ) - courseNavGraph( - paddingValues = paddingValues, - navigateUp = navigator::navigateUp, - navigateNext = navigator::navigateWalkCourse, - navigateToDetail = { postId, routeId -> - // Todo : 마찬가지로 이것도 그냥 넣어놓음 나중에 리스트 연결 후 예쩡 / 리스트 아이템이동 - navigator.navigateArchivedDetail( - pageId = postId, - routeId = routeId - ) - }, - setOnVisibleRecord = navigator::setOnVisibleRecord, - snackBarHostState = snackbarHostState - ) - - sharedWalkCourseNavGraph( - paddingValues = paddingValues, - navigateUp = navigator::navigateUp, - navigateNext = { routeId, pageId -> - navigator.navigateSharedWalkCompletion( - routeId = routeId, - pageId = pageId - ) - }, - snackBarHostState = snackbarHostState - ) - - sharedWalkCompletionNavGraph( - paddingValues = paddingValues, - navigateUp = navigator::navigateUp, - navigateNext = { routeId, pageId -> - // Todo : 마찬가지로 이것도 그냥 넣어놓음 나중에 리스트 연결 후 예쩡 - navigator.navigateSharedWalkReview( - routeId = routeId, - pageId = pageId - ) - }, - snackBarHostState = snackbarHostState - ) - - // Todo : 리스트로 돌아갈 수 있게 - 다이얼로그 - sharedWalkReviewNavGraph( - paddingValues = paddingValues, - navigateUp = navigator::navigateUp, - navigateNext = { - navigator.navController.navigateCourse(index = 1, navOptions = null) - }, - snackBarHostState = snackbarHostState - ) - - walkCourseNavGraph( - paddingValues = paddingValues, - navigateUp = navigator::navigateUp, - navigateNext = { - navigator.navigateWalkCompletion( - routeId = it, - ) - }, - snackBarHostState = snackbarHostState - ) - - walkCompletionNavGraph( + walkCourseGraph( paddingValues = paddingValues, - navigateUp = navigator::navigateUp, - navigateNext = { routeId -> - navigator.navigateWalkReview( - routeId = routeId, - ) - }, + navController = navigator.navController, + navigateWalkReview = navigator::navigateWalkReview ) walkReviewNavGraph( - navigateUp = navigator::navigateUp, - navigateNext = navigator::navigateCourse, - navigateShared = { routeId, pageId -> - navigator.navigateArchivedDetail( - pageId = pageId, - routeId = routeId - ) - }, - snackBarHostState = snackbarHostState + paddingValues = paddingValues, + navigateHome = navigator::navigateHome, + navigateWalkDetail = navigator::navigateWalkCourse, // Todo 상세 정보 뷰로 ) communityNavGraph( @@ -230,10 +153,7 @@ fun PawKeyNavHost( navigator.navController.navigateCourse(index = 1, navOptions = null) },*/ navigateToSharedWalk = { routeId, pageId -> - navigator.navigateSharedWalkCourse( - routeId = routeId, - pageId = pageId - ) + }, modifier = modifier ) diff --git a/app/src/main/java/com/paw/key/presentation/ui/onboard/OnboardingScreen.kt b/app/src/main/java/com/paw/key/presentation/ui/onboard/OnboardingScreen.kt index 92972678..79325e30 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/onboard/OnboardingScreen.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/onboard/OnboardingScreen.kt @@ -20,7 +20,7 @@ import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.paw.key.R -import com.paw.key.core.designsystem.component.DogkyButton +import com.paw.key.core.designsystem.component.DokiButton import com.paw.key.core.designsystem.theme.PawKeyTheme import com.paw.key.presentation.ui.onboard.component.OnboardPager import com.paw.key.presentation.ui.onboard.component.OnboardingPosting @@ -110,7 +110,7 @@ fun OnboardingScreen( ) ) - DogkyButton( + DokiButton( text = stringResource(id = R.string.ic_onboarding_button), enabled = true, onClick = navigateNext, diff --git a/app/src/main/java/com/paw/key/presentation/ui/signup/SignUpScreen.kt b/app/src/main/java/com/paw/key/presentation/ui/signup/SignUpScreen.kt index 70ffad7a..437ef3ea 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/signup/SignUpScreen.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/signup/SignUpScreen.kt @@ -18,7 +18,7 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.flowWithLifecycle -import com.paw.key.core.designsystem.component.DogkyButton +import com.paw.key.core.designsystem.component.DokiButton import com.paw.key.core.designsystem.component.LoadingScreen import com.paw.key.core.util.UiState import com.paw.key.presentation.ui.signup.component.SignUpHeader @@ -238,7 +238,7 @@ fun SignUpScreen( Spacer(modifier = Modifier.weight(1f)) - DogkyButton( + DokiButton( text = buttonText, onClick = onNextClick, enabled = isNextEnabled, From 612725243c3594d21d4b1cace313d875bfe54e7e Mon Sep 17 00:00:00 2001 From: sonms Date: Mon, 5 Jan 2026 12:56:48 +0900 Subject: [PATCH 07/47] =?UTF-8?q?mod/#154=20=ED=83=80=EC=9D=B4=ED=8F=AC?= =?UTF-8?q?=EA=B7=B8=EB=9E=98=ED=94=BC=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/core/designsystem/theme/Type.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/paw/key/core/designsystem/theme/Type.kt b/app/src/main/java/com/paw/key/core/designsystem/theme/Type.kt index 027b9c64..a19dc9b5 100644 --- a/app/src/main/java/com/paw/key/core/designsystem/theme/Type.kt +++ b/app/src/main/java/com/paw/key/core/designsystem/theme/Type.kt @@ -419,10 +419,10 @@ fun pawKeyTypography(): PawKeyTypography { letterSpacing = 0.em ), mainButtonActive = pawKeyTextStyle( - fontFamily = PretendardRegular, - fontWeight = FontWeight.Normal, + fontFamily = PretendardSemiBold, + fontWeight = FontWeight.SemiBold, fontSize = 18.sp, - lineHeight = 16.sp, + lineHeight = 20.sp, letterSpacing = 0.em ), subButtonDefault = pawKeyTextStyle( @@ -433,8 +433,8 @@ fun pawKeyTypography(): PawKeyTypography { letterSpacing = 0.em ), subButtonActive = pawKeyTextStyle( - fontFamily = PretendardSemiBold, - fontWeight = FontWeight.SemiBold, + fontFamily = PretendardMedium, + fontWeight = FontWeight.Medium, fontSize = 12.sp, lineHeight = 16.sp, letterSpacing = 0.em From 0adc21cfb02103c58d3a8d7f1b576b0a583262bc Mon Sep 17 00:00:00 2001 From: sonms Date: Mon, 5 Jan 2026 13:00:00 +0900 Subject: [PATCH 08/47] =?UTF-8?q?mod/#154=20=EC=BD=94=EC=8A=A4=EB=A1=9C=20?= =?UTF-8?q?=ED=95=98=EB=82=98=EC=9D=98=20feature=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=20=EB=B0=8F=20=EC=A7=80=EB=8F=84=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B5=AC=ED=98=84,=20=EB=84=A4=EB=B9=84=EC=97=B0?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/course/navigation/WalkCourseGraph.kt | 44 ++ .../course/navigation/WalkCourseNavigation.kt | 22 + .../ui/course/navigation/WalkCourseRoute.kt | 19 + .../ui/course/walkcourse/WalkCourseScreen.kt | 497 ++++++++++++++++++ .../walkcourse/state/WalkCourseContract.kt | 33 ++ .../course/walkcourse/util/WalkCourseUtil.kt | 110 ++++ .../viewmodel/WalkCourseViewModel.kt | 29 +- .../walkcourse/walkcomplete/WalkComplete.kt | 110 ++++ .../state/WalkCompleteContract.kt | 14 + 9 files changed, 863 insertions(+), 15 deletions(-) create mode 100644 app/src/main/java/com/paw/key/presentation/ui/course/navigation/WalkCourseGraph.kt create mode 100644 app/src/main/java/com/paw/key/presentation/ui/course/navigation/WalkCourseNavigation.kt create mode 100644 app/src/main/java/com/paw/key/presentation/ui/course/navigation/WalkCourseRoute.kt create mode 100644 app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/WalkCourseScreen.kt create mode 100644 app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/state/WalkCourseContract.kt create mode 100644 app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/util/WalkCourseUtil.kt rename app/src/main/java/com/paw/key/presentation/ui/course/{walk => walkcourse}/viewmodel/WalkCourseViewModel.kt (90%) create mode 100644 app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkcomplete/WalkComplete.kt create mode 100644 app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkcomplete/state/WalkCompleteContract.kt diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/navigation/WalkCourseGraph.kt b/app/src/main/java/com/paw/key/presentation/ui/course/navigation/WalkCourseGraph.kt new file mode 100644 index 00000000..f6ed276b --- /dev/null +++ b/app/src/main/java/com/paw/key/presentation/ui/course/navigation/WalkCourseGraph.kt @@ -0,0 +1,44 @@ +package com.paw.key.presentation.ui.course.navigation + +import androidx.compose.foundation.layout.PaddingValues +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable +import androidx.navigation.navigation +import com.paw.key.presentation.ui.course.walkcourse.WalkCourseRoute +import com.paw.key.presentation.ui.course.walkcourse.walkcomplete.WalkCompleteRoute +import com.paw.key.presentation.ui.course.walkcourse.walkprepare.WalkPrepareRoute + + +fun NavGraphBuilder.walkCourseGraph( + paddingValues: PaddingValues, + navController: NavController, + navigateWalkReview : () -> Unit +) { + navigation( + startDestination = WalkPrepare + ) { + composable { + WalkPrepareRoute( + paddingValues = paddingValues, + navigateWalkCourse = navController::navigateWalkCourse + ) + } + + composable { + WalkCourseRoute( + paddingValues = paddingValues, + navigateUp = navController::navigateUp, + //navigateWalkComplete = navController::navigateWalkComplete, + navigateReview = navigateWalkReview + ) + } + + composable { + WalkCompleteRoute( + paddingValues = paddingValues, + //navigateReview = navigateWalkReview + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/navigation/WalkCourseNavigation.kt b/app/src/main/java/com/paw/key/presentation/ui/course/navigation/WalkCourseNavigation.kt new file mode 100644 index 00000000..2584b0e2 --- /dev/null +++ b/app/src/main/java/com/paw/key/presentation/ui/course/navigation/WalkCourseNavigation.kt @@ -0,0 +1,22 @@ +package com.paw.key.presentation.ui.course.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavOptions + +fun NavController.navigateWalkCourse( + navOptions: NavOptions? = null, +) { + navigate(WalkCourse, navOptions) +} + +fun NavController.navigateWalkPrepare( + navOptions: NavOptions?, +) { + navigate(WalkPrepare, navOptions) +} + +fun NavController.navigateWalkComplete( + navOptions: NavOptions?, +) { + navigate(WalkComplete, navOptions) +} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/navigation/WalkCourseRoute.kt b/app/src/main/java/com/paw/key/presentation/ui/course/navigation/WalkCourseRoute.kt new file mode 100644 index 00000000..c72c66da --- /dev/null +++ b/app/src/main/java/com/paw/key/presentation/ui/course/navigation/WalkCourseRoute.kt @@ -0,0 +1,19 @@ +package com.paw.key.presentation.ui.course.navigation + +import com.paw.key.core.navigation.MainTabRoute +import kotlinx.serialization.Serializable + +sealed interface WalkRoute : MainTabRoute + +@Serializable +data object WalkCourseGraph : WalkRoute + +@Serializable +data object WalkPrepare: WalkRoute + +@Serializable +data object WalkCourse: WalkRoute + +@Serializable +data object WalkComplete: WalkRoute + diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/WalkCourseScreen.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/WalkCourseScreen.kt new file mode 100644 index 00000000..14ab85c3 --- /dev/null +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/WalkCourseScreen.kt @@ -0,0 +1,497 @@ +package com.paw.key.presentation.ui.course.walkcourse + +import android.Manifest +import android.graphics.Bitmap +import android.os.Build +import android.view.Gravity +import android.widget.Toast +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.height +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.flowWithLifecycle +import com.naver.maps.geometry.LatLng +import com.naver.maps.geometry.LatLngBounds +import com.naver.maps.map.CameraUpdate +import com.naver.maps.map.compose.CameraPositionState +import com.naver.maps.map.compose.CameraUpdateReason +import com.naver.maps.map.compose.ExperimentalNaverMapApi +import com.naver.maps.map.compose.LocationOverlay +import com.naver.maps.map.compose.LocationTrackingMode +import com.naver.maps.map.compose.MapProperties +import com.naver.maps.map.compose.MapUiSettings +import com.naver.maps.map.compose.NaverMap +import com.naver.maps.map.compose.PathOverlay +import com.naver.maps.map.compose.rememberCameraPositionState +import com.naver.maps.map.overlay.OverlayImage +import com.paw.key.R +import com.paw.key.core.designsystem.component.DokiBorderButton +import com.paw.key.core.designsystem.component.DokiButton +import com.paw.key.core.designsystem.component.LoadingScreen +import com.paw.key.core.designsystem.theme.PawKeyTheme +import com.paw.key.core.extension.noRippleClickable +import com.paw.key.core.util.PermissionRequestEffect +import com.paw.key.core.util.UiState +import com.paw.key.presentation.ui.course.util.FusedLocationSource +import com.paw.key.presentation.ui.course.util.StepCountListener +import com.paw.key.presentation.ui.course.util.rememberCustomFusedLocationSource +import com.paw.key.presentation.ui.course.util.rememberStepCounter +import com.paw.key.presentation.ui.course.walkcourse.component.WalkRecordItem +import com.paw.key.presentation.ui.course.walkcourse.state.WalkCourseSideEffect +import com.paw.key.presentation.ui.course.walkcourse.util.formatDistance +import com.paw.key.presentation.ui.course.walkcourse.util.formatTime +import com.paw.key.presentation.ui.course.walkcourse.viewmodel.WalkCourseViewModel +import kotlinx.collections.immutable.ImmutableList +import kotlinx.coroutines.flow.drop +import java.util.Locale + +private val REQUIRED_PERMISSIONS = mutableListOf( + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION +).apply { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + add(Manifest.permission.ACTIVITY_RECOGNITION) + } +}.toTypedArray() + +@OptIn(ExperimentalNaverMapApi::class) +@Composable +fun WalkCourseRoute( + paddingValues: PaddingValues, + navigateUp: () -> Unit = {}, + navigateReview: () -> Unit = {}, + viewModel: WalkCourseViewModel = hiltViewModel(), +) { + val lifecycleOwner = LocalLifecycleOwner.current + val context = LocalContext.current + + val state by viewModel.state.collectAsStateWithLifecycle() + + val cameraPositionState = rememberCameraPositionState() + + var hasLocationPermission by remember { mutableStateOf(false) } + + val fusedLocationClient = rememberCustomFusedLocationSource( + useTestPoints = false, + cameraPositionState = cameraPositionState, + hasLocationPermission = hasLocationPermission + ) + + val stepCounter = rememberStepCounter() + + val formattedTotalTime by remember(state.totalTimeMillis) { + derivedStateOf { formatTime(state.totalTimeMillis) } + } + + val formattedDistance by remember(state.mapState.totalDistance) { + derivedStateOf { formatDistance(state.mapState.totalDistance) } + } + + var mapProperties by remember { + mutableStateOf(MapProperties()) + } + + LaunchedEffect(Unit) { + viewModel.sideEffect.flowWithLifecycle(lifecycleOwner.lifecycle) + .collect { sideEffect -> + when (sideEffect) { + is WalkCourseSideEffect.NavigateNext -> navigateReview() + + WalkCourseSideEffect.NavigateUp -> navigateUp() + + is WalkCourseSideEffect.ShowToastMessage -> { + Toast.makeText(context, sideEffect.message, Toast.LENGTH_SHORT).show() + } + else -> { + + } + } + } + } + + PermissionRequestEffect( + permissions = REQUIRED_PERMISSIONS, + onResult = { isGranted -> + hasLocationPermission = isGranted + if (isGranted) { + viewModel.onPermissionsGranted() + fusedLocationClient.setRealTimeLocationListener(viewModel) + } else { + viewModel.showToastMessage("산책 기록을 위해 권한이 필요합니다.") + } + } + ) + + /*// 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) + }*/ + + + + LaunchedEffect(state.recordingState.isRecording, stepCounter) { + if (state.recordingState.isRecording) { + stepCounter.setStepCountListener(object : StepCountListener { + override fun onStepCountChanged(sessionSteps: Long) { + viewModel.onRawStepData(sessionSteps) + } + override fun onSensorNotFound() { + Toast.makeText(context, "걸음 수 측정 센서가 없는 기기입니다.", Toast.LENGTH_SHORT).show() + } + }) + stepCounter.activate() + } else { + stepCounter.deactivate() + } + } + + LaunchedEffect(state.mapState.poiPoints.size) { + if (state.mapState.poiPoints.size >= 2) { + val bounds = LatLngBounds.from(state.mapState.poiPoints) + cameraPositionState.animate( + CameraUpdate.fitBounds(bounds, 300) + ) + } + } + + LaunchedEffect(state.mapState.isTrackingEnabled) { + mapProperties = mapProperties.copy( + locationTrackingMode = if (state.mapState.isTrackingEnabled) { + LocationTrackingMode.Follow + } else { + LocationTrackingMode.NoFollow + } + ) + } + + LaunchedEffect(cameraPositionState) { + snapshotFlow { cameraPositionState.cameraUpdateReason } + .drop(1) + .collect { reason -> + if (reason == CameraUpdateReason.GESTURE && state.mapState.isTrackingEnabled) { + viewModel.disableTracking() + } + } + } + + when (state.mapState.initialState) { + is UiState.Empty -> Unit + is UiState.Failure -> Unit + + is UiState.Loading -> { + LoadingScreen() + } + + is UiState.Success -> { + WalkCourseScreen( + paddingValues = paddingValues, + cameraPositionState = cameraPositionState, + currentLocation = state.mapState.currentLocation, + routeLineCoords = state.mapState.poiPoints, + locationSource = fusedLocationClient, + totalDistance = formattedDistance, + mapProperties = mapProperties, + currentSteps = state.stepCounterState.sessionSteps, + totalTime = formattedTotalTime, + isRecording = state.recordingState.isRecording, // 산책 중단, 계속 여부 + isTracking = state.mapState.isTrackingEnabled, // 산책 포커싱 + onClickTracking = { + viewModel.fetchTrackingEnable() + }, + onPauseTracking = { // 일시정지 + viewModel.pauseTracking() + }, + onStartTracking = { // 계속하기 + viewModel.startTracking() + }, + onStopTracking = { // 종료 후 넘어가기 -> 서버 전송 후 완료 뷰로 넘어가기 + /*scope.launch { + viewModel.postWalkCourseData(userId = userId.first()) + }*/ + //viewModel.onStopTrackingEvent() + //navigateToWalkComplete() + navigateReview() + }, + onCaptured = { bitmap -> + // Todo : bitmap 안쓸거임 + }, + ) + } + } +} + +@OptIn(ExperimentalNaverMapApi::class) +@Composable +fun WalkCourseScreen( + paddingValues: PaddingValues, + cameraPositionState: CameraPositionState, + locationSource: FusedLocationSource, + currentLocation : LatLng?, + routeLineCoords : ImmutableList, + totalDistance: String, + currentSteps: Long, + totalTime: String, + mapProperties: MapProperties, + isTracking: Boolean, // 포커싱 여부 + isRecording: Boolean, // 산책 중단, 계속 여부 + onClickTracking: () -> Unit, // 따라다니기 + onStartTracking: () -> Unit, // 계속하기 + onPauseTracking: () -> Unit, // 잠시 중단 + onStopTracking: () -> Unit, // 종료하기 + onCaptured: (Bitmap?) -> Unit, +) { + var mapUiSettings by remember { + mutableStateOf( + MapUiSettings( + logoGravity = Gravity.BOTTOM or Gravity.START, + isZoomControlEnabled = false + ) + ) + } + + Box( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + ) { + NaverMap( + modifier = Modifier + .fillMaxSize(), + cameraPositionState = cameraPositionState, + locationSource = locationSource, + locale = Locale.KOREA, + uiSettings = mapUiSettings, + properties = mapProperties, + ) { + if (currentLocation != null) { + LocationOverlay( + position = currentLocation, + icon = OverlayImage.fromResource(R.drawable.user_poi), + ) + } + + if (routeLineCoords.isNotEmpty() && routeLineCoords.size >= 2) { + PathOverlay( + coords = routeLineCoords, + width = 5.dp, + color = PawKeyTheme.colors.green500, + outlineWidth = 0.dp + ) + } + } + + Column( + modifier = Modifier + .fillMaxSize(), + verticalArrangement = Arrangement.SpaceBetween, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Spacer(modifier = Modifier.weight(1f)) + + Column( + modifier = Modifier + .fillMaxWidth() + .navigationBarsPadding(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + horizontalArrangement = Arrangement.End + ) { + FloatingActionButton( + shape = CircleShape, + onClick = onClickTracking, + containerColor = Color.White, + modifier = Modifier + .border( + width = 2.dp, + color = if (isTracking) PawKeyTheme.colors.primary else Color.Transparent, + shape = CircleShape + ), + ) { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_course_map_tap_location_on), + contentDescription = "내 위치", + tint = if (isTracking) PawKeyTheme.colors.primary else Color.Unspecified + ) + } + } + + Surface( + shape = RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp), + color = PawKeyTheme.colors.background, + shadowElevation = 10.dp, + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight(Alignment.Bottom) + ) { + Column { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 32.dp, start = 16.dp, end = 16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceAround + ) { + WalkRecordItem( + recordTitle = R.string.course_record_distance, + recordContent = totalDistance + ) + WalkRecordItem( + recordTitle = R.string.course_record_time, + recordContent = totalTime + ) + WalkRecordItem( + recordTitle = R.string.course_record_step, + recordContent = currentSteps.toString() + ) + } + + Spacer(modifier = Modifier.height(20.dp)) + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .padding(bottom = 12.dp) + ) { + DokiBorderButton( + text = "산책 중단하기", + enabled = true, + onClick = onPauseTracking, + modifier = Modifier + .weight(1f) + ) + + Spacer(modifier = Modifier.width(16.dp)) + + DokiButton( + text = "산책 종료하기", + enabled = true, + onClick = onStopTracking, + modifier = Modifier + .weight(1f) + ) + } + } + } + } + } + + if (!isRecording) { + Box( + modifier = Modifier + .fillMaxSize() + .background(Color.Black.copy(alpha = 0.6f)) + .noRippleClickable { } + ) + + Column( + modifier = Modifier + .align(Alignment.Center) + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = "산책이 중단되었어요!", + textAlign = TextAlign.Center, + style = PawKeyTheme.typography.header2, + color = PawKeyTheme.colors.background + ) + + Spacer(modifier = Modifier.height(4.dp)) + + Text( + text = "산책을 정말 종료하시겠어요?", + textAlign = TextAlign.Center, + style = PawKeyTheme.typography.subTitle, + color = PawKeyTheme.colors.background + ) + } + + Column( + modifier = Modifier.align(Alignment.BottomCenter) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .padding(bottom = 12.dp) + .navigationBarsPadding() + ) { + DokiBorderButton( + text = "산책 재개하기", + enabled = true, + onClick = onStartTracking, + modifier = Modifier + .weight(1f) + .background(Color.White, RoundedCornerShape(8.dp)) + ) + + Spacer(modifier = Modifier.width(16.dp)) + + DokiButton( + text = "산책 종료하기", + enabled = true, + onClick = onStopTracking, + modifier = Modifier.weight(1f) + ) + } + } + } + } + +} + +@Preview(showBackground = true) +@Composable +private fun WalkCourseBottomPreview() { + PawKeyTheme {} +} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/state/WalkCourseContract.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/state/WalkCourseContract.kt new file mode 100644 index 00000000..f4ff78df --- /dev/null +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/state/WalkCourseContract.kt @@ -0,0 +1,33 @@ +package com.paw.key.presentation.ui.course.walkcourse.state + +import androidx.annotation.StringRes +import androidx.compose.runtime.Immutable +import com.paw.key.R +import com.paw.key.presentation.ui.course.walkcourse.model.MapState +import com.paw.key.presentation.ui.course.walkcourse.model.RecordingState +import com.paw.key.presentation.ui.course.walkcourse.model.StepCounterState + +@Immutable +data class WalkCourseState( + val recordingState: RecordingState = RecordingState(), + val mapState: MapState = MapState(), + val stepCounterState: StepCounterState = StepCounterState(), + val totalTimeMillis: Long = 0L, +) + +sealed class WalkCourseSideEffect { + data class ShowSnackBar(val message: String) : WalkCourseSideEffect() + data class ShowToastMessage(val message: String) : WalkCourseSideEffect() + data object NavigateUp: WalkCourseSideEffect() + data class NavigateNext(val regionId: Int): 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/walkcourse/util/WalkCourseUtil.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/util/WalkCourseUtil.kt new file mode 100644 index 00000000..0ca58d12 --- /dev/null +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/util/WalkCourseUtil.kt @@ -0,0 +1,110 @@ +package com.paw.key.presentation.ui.course.walkcourse.util + +import android.graphics.Bitmap +import android.opengl.GLException +import java.nio.IntBuffer +import java.util.Locale +import java.util.concurrent.TimeUnit +import javax.microedition.khronos.opengles.GL10 + +fun cropCenterWithAspectRatio( + bitmap: Bitmap, + targetAspectRatio: Float +): Bitmap { + val width = bitmap.width + val height = bitmap.height + val currentAspectRatio = width.toFloat() / height.toFloat() + + val cropWidth: Int + val cropHeight: Int + + if (currentAspectRatio > targetAspectRatio) { + // 현재 이미지가 더 넓음 → 좌우 잘라야 함 + cropHeight = height + cropWidth = (height * targetAspectRatio).toInt() + } else { + // 현재 이미지가 더 높음 → 위아래 잘라야 함 + cropWidth = width + cropHeight = (width / targetAspectRatio).toInt() + } + + val startX = ((width - cropWidth) / 2).coerceAtLeast(0) + val startY = ((height - cropHeight) / 2).coerceAtLeast(0) + + val safeWidth = minOf(cropWidth, width - startX) + val safeHeight = minOf(cropHeight, height - startY) + + return Bitmap.createBitmap(bitmap, startX, startY, safeWidth, safeHeight) +} + + +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) + + val targetAspectRatio = 16f / 11f + + 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) +} + +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) +} \ 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/walkcourse/viewmodel/WalkCourseViewModel.kt similarity index 90% rename from app/src/main/java/com/paw/key/presentation/ui/course/walk/viewmodel/WalkCourseViewModel.kt rename to app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/viewmodel/WalkCourseViewModel.kt index c43d5eb5..fe334091 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/course/walk/viewmodel/WalkCourseViewModel.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/viewmodel/WalkCourseViewModel.kt @@ -1,19 +1,19 @@ -package com.paw.key.presentation.ui.course.walk.viewmodel +package com.paw.key.presentation.ui.course.walkcourse.viewmodel import android.location.Location import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.paw.key.core.extension.toLatLng import com.paw.key.core.util.PhotoUtils import com.paw.key.core.util.UiState -import com.paw.key.core.extension.toLatLng import com.paw.key.domain.model.entity.walkcourse.CoordinateEntity import com.paw.key.domain.model.entity.walkcourse.WalkCourseEntity import com.paw.key.domain.repository.WalkSharedResultRepository import com.paw.key.domain.repository.walkcourse.WalkCourseRepository import com.paw.key.presentation.ui.course.util.RealTimeLocationListener -import com.paw.key.presentation.ui.course.walk.state.WalkCourseContract.WalkCourseSideEffect -import com.paw.key.presentation.ui.course.walk.state.WalkCourseContract.WalkCourseState +import com.paw.key.presentation.ui.course.walkcourse.state.WalkCourseSideEffect +import com.paw.key.presentation.ui.course.walkcourse.state.WalkCourseState import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Job import kotlinx.coroutines.delay @@ -34,12 +34,10 @@ class WalkCourseViewModel @Inject constructor( private val walkCourseRepository: WalkCourseRepository ) : ViewModel(), RealTimeLocationListener { private val _state = MutableStateFlow(WalkCourseState()) - val state: StateFlow - get() = _state.asStateFlow() + val state: StateFlow = _state.asStateFlow() private val _sideEffect = MutableSharedFlow() - val sideEffect: SharedFlow - get() = _sideEffect.asSharedFlow() + val sideEffect: SharedFlow = _sideEffect.asSharedFlow() private var timerJob: Job? = null @@ -59,6 +57,14 @@ class WalkCourseViewModel @Inject constructor( } } + fun showToastMessage( + message: String + ) { + viewModelScope.launch { + _sideEffect.emit(WalkCourseSideEffect.ShowSnackBar(message)) + } + } + fun startTracking() { _state.update { currentState -> val newRecordingState = currentState.recordingState.copy( @@ -188,15 +194,12 @@ class WalkCourseViewModel @Inject constructor( result.onSuccess { response -> _sideEffect.emit(WalkCourseSideEffect.NavigateNext(response.regionId)) - Log.d("WalkCourseViewModel", "routeId = ${response}") }.onFailure { throwable -> _sideEffect.emit(WalkCourseSideEffect.ShowSnackBar("업로드 실패: ${throwable.message}")) - Log.e("WalkCourseViewModel", "업로드 실패", throwable) } } catch (e: Exception) { _sideEffect.emit(WalkCourseSideEffect.ShowSnackBar("오류 발생: ${e.localizedMessage}")) - Log.e("WalkCourseViewModel", "예외 발생", e) } } @@ -214,7 +217,6 @@ class WalkCourseViewModel @Inject constructor( ) _sideEffect.emit(WalkCourseSideEffect.ShowSnackBar("산책 기록이 성공적으로 저장되었습니다.")) } catch (e: Exception) { - Log.e("WalkCourseViewModel", "Error saving walk summary data: ${e.message}", e) _sideEffect.emit(WalkCourseSideEffect.ShowSnackBar("산책 기록 저장 실패: ${e.localizedMessage}")) } } @@ -222,11 +224,9 @@ class WalkCourseViewModel @Inject constructor( override fun onLocationChanged(location: Location) { if (location.accuracy > LOCATION_ACCURACY_THRESHOLD) { - Log.e("onLocationChanged", "onLocationChanged: $location") return } - Log.e("onLocationChanged", "onLocationChanged: $location") val newLatLng = location.toLatLng() val lastPoint = lastLocation @@ -273,7 +273,6 @@ class WalkCourseViewModel @Inject constructor( } companion object { - private const val TIMER_INTERVAL_MS = 1000L private const val LOCATION_ACCURACY_THRESHOLD = 25f } } \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkcomplete/WalkComplete.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkcomplete/WalkComplete.kt new file mode 100644 index 00000000..0033ac19 --- /dev/null +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkcomplete/WalkComplete.kt @@ -0,0 +1,110 @@ +package com.paw.key.presentation.ui.course.walkcourse.walkcomplete + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import com.paw.key.core.designsystem.component.TopBar +import com.paw.key.core.designsystem.theme.PawKeyTheme + +// Todo : 나중에 서버에서 줌 +@Composable +fun WalkCompleteRoute( + paddingValues: PaddingValues +) { + WalkCompleteScreen( + paddingValues = paddingValues + ) +} + +@Composable +private fun WalkCompleteScreen( + paddingValues: PaddingValues +) { + Column ( + modifier = Modifier + .fillMaxSize() + .background(color = PawKeyTheme.colors.background) + .padding(paddingValues) + ) { + TopBar( + title = "산책 완료", + isBackVisible = false + ) + + Spacer(modifier = Modifier.height(20.dp)) + + Column ( + modifier = Modifier + .fillMaxSize() + .clip(RoundedCornerShape(16.dp)) + .shadow( + elevation = 10.dp, + spotColor = PawKeyTheme.colors.defaultMiddle, + ) + ) { + Row ( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + AsyncImage( + model = "", + contentDescription = null, + modifier = Modifier + .clip(CircleShape) + .background( + color = PawKeyTheme.colors.defaultMiddle + ) + ) + + Spacer(modifier = Modifier.width(10.dp)) + + Column { + Text( + text = "단지", + style = PawKeyTheme.typography.subTitle, + color = PawKeyTheme.colors.contents + ) + + Text( + text = "2025.06.26(금) | 오후 11:50", + style = PawKeyTheme.typography.subButtonDefault, + color = PawKeyTheme.colors.contents + ) + } + } + + + + } + } +} + +@Preview +@Composable +private fun WalkCompletePreview() { + PawKeyTheme { + WalkCompleteScreen( + paddingValues = PaddingValues() + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkcomplete/state/WalkCompleteContract.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkcomplete/state/WalkCompleteContract.kt new file mode 100644 index 00000000..e661d166 --- /dev/null +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkcomplete/state/WalkCompleteContract.kt @@ -0,0 +1,14 @@ +package com.paw.key.presentation.ui.course.walkcourse.walkcomplete.state + +import androidx.compose.runtime.Immutable +import com.paw.key.presentation.ui.course.walkcourse.model.WalkInfoState + +@Immutable +data class WalkCompleteState( + val userProfile: String = "", + val petName : String = "", + val dateTime : String = "", + val mapImage: String = "", + + val walkInfo: WalkInfoState = WalkInfoState() +) \ No newline at end of file From f052af6262ca4028b22cacd3b883170a6a048315 Mon Sep 17 00:00:00 2001 From: sonms Date: Mon, 5 Jan 2026 13:00:36 +0900 Subject: [PATCH 09/47] =?UTF-8?q?feat/#154=20=EC=A4=80=EB=B9=84=20?= =?UTF-8?q?=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 --- .../course/walkcourse/model/WalkInfoState.kt | 10 + .../walkprepare/WalkPrepareScreen.kt | 97 +++++++++ .../walkprepare/WalkPrepareViewModel.kt | 18 ++ .../walkprepare/component/WalkPrepareBody.kt | 196 ++++++++++++++++++ .../component/WalkPrepareWeatherInfo.kt | 77 +++++++ .../walkprepare/model/WalkPrepareItemModel.kt | 8 + .../walkprepare/state/WalkPrepareContract.kt | 17 ++ 7 files changed, 423 insertions(+) create mode 100644 app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/model/WalkInfoState.kt create mode 100644 app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkprepare/WalkPrepareScreen.kt create mode 100644 app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkprepare/WalkPrepareViewModel.kt create mode 100644 app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkprepare/component/WalkPrepareBody.kt create mode 100644 app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkprepare/component/WalkPrepareWeatherInfo.kt create mode 100644 app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkprepare/model/WalkPrepareItemModel.kt create mode 100644 app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkprepare/state/WalkPrepareContract.kt diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/model/WalkInfoState.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/model/WalkInfoState.kt new file mode 100644 index 00000000..ee99c01d --- /dev/null +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/model/WalkInfoState.kt @@ -0,0 +1,10 @@ +package com.paw.key.presentation.ui.course.walkcourse.model + +import androidx.compose.runtime.Immutable + +@Immutable +data class WalkInfoState( + val distanceMeters: Float = 0f, // 산책 거리 (기존 MapState에서 이동) + val timeMillis: Long = 0L, // 산책 시간 (기존 WalkCourseState에서 이동) + val stepCount: Long = 0 // 걸음 수 (기존 StepCounterState에서 이동) +) \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkprepare/WalkPrepareScreen.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkprepare/WalkPrepareScreen.kt new file mode 100644 index 00000000..7e6b36df --- /dev/null +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkprepare/WalkPrepareScreen.kt @@ -0,0 +1,97 @@ +package com.paw.key.presentation.ui.course.walkcourse.walkprepare + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.paw.key.core.designsystem.component.DokiBorderButton +import com.paw.key.core.designsystem.component.TopBar +import com.paw.key.core.designsystem.theme.PawKeyTheme +import com.paw.key.presentation.ui.course.walkcourse.walkprepare.component.WalkPrepareBody +import com.paw.key.presentation.ui.course.walkcourse.walkprepare.component.WalkPrepareWeatherInfo +import com.paw.key.presentation.ui.course.walkcourse.walkprepare.state.WalkPrepareState + +@Composable +fun WalkPrepareRoute( + paddingValues: PaddingValues, + navigateWalkCourse: () -> Unit = {}, + viewModel: WalkPrepareViewModel = hiltViewModel() +) { + val state by viewModel.state.collectAsStateWithLifecycle() + + WalkPrepareScreen( + paddingValues = paddingValues, + state = state, + navigateWalkCourse = navigateWalkCourse + ) +} + +@Composable +private fun WalkPrepareScreen( + paddingValues: PaddingValues, + state: WalkPrepareState, + navigateWalkCourse: () -> Unit = {} +) { + Column ( + modifier = Modifier + .fillMaxSize() + .background( + color = PawKeyTheme.colors.defaultButton + ) + .padding(paddingValues) + ) { + TopBar( + title = "산책", + isBackVisible = false + ) + + Spacer(modifier = Modifier.height(20.dp)) + + // Todo : 안에 내용은 수정하기 + WalkPrepareWeatherInfo( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + ) + + Spacer(modifier = Modifier.height(16.dp)) + + WalkPrepareBody( + itemList = state.dummyWalkPrepare, + modifier = Modifier + .padding(horizontal = 16.dp) + ) + + Spacer(modifier = Modifier.height(16.dp)) + + DokiBorderButton( + text = "산책 기록하기", + enabled = true, + onClick = navigateWalkCourse, + modifier = Modifier + .padding(horizontal = 16.dp) + ) + } +} + +@Preview +@Composable +private fun WalkPrepareScreenPreview() { + PawKeyTheme { + WalkPrepareScreen( + paddingValues = PaddingValues(), + state = WalkPrepareState() + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkprepare/WalkPrepareViewModel.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkprepare/WalkPrepareViewModel.kt new file mode 100644 index 00000000..47d7df65 --- /dev/null +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkprepare/WalkPrepareViewModel.kt @@ -0,0 +1,18 @@ +package com.paw.key.presentation.ui.course.walkcourse.walkprepare + +import androidx.lifecycle.ViewModel +import com.paw.key.presentation.ui.course.walkcourse.walkprepare.state.WalkPrepareState +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import javax.inject.Inject + +@HiltViewModel +class WalkPrepareViewModel @Inject constructor( + +) : ViewModel() { + private val _state = MutableStateFlow(WalkPrepareState()) + val state = _state.asStateFlow() + + +} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkprepare/component/WalkPrepareBody.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkprepare/component/WalkPrepareBody.kt new file mode 100644 index 00000000..1729a43b --- /dev/null +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkprepare/component/WalkPrepareBody.kt @@ -0,0 +1,196 @@ +package com.paw.key.presentation.ui.course.walkcourse.walkprepare.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check +import androidx.compose.material3.Checkbox +import androidx.compose.material3.CheckboxDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.material3.TriStateCheckbox +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.paw.key.R +import com.paw.key.core.designsystem.theme.PawKeyTheme +import com.paw.key.core.extension.noRippleClickable +import com.paw.key.presentation.ui.course.walkcourse.walkprepare.model.WalkPrepareItemModel +import com.paw.key.presentation.ui.course.walkcourse.walkprepare.state.WalkPrepareState +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +@Composable +fun WalkPrepareBody( + modifier: Modifier = Modifier, + itemList : ImmutableList = persistentListOf(), +) { + var selectedIds by remember { mutableStateOf(setOf()) } + + Column ( + modifier = modifier + .clip(RoundedCornerShape(16.dp)) + .background( + color = PawKeyTheme.colors.background + ) + .padding(16.dp) + ) { + Text( + text = "산책 필수템", + style = PawKeyTheme.typography.subTitle, + color = PawKeyTheme.colors.defaultDark + ) + + Spacer(modifier = Modifier.height(16.dp)) + + LazyColumn ( + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + itemsIndexed( + items = itemList, + key = { _, item -> item.id } + ) { index, item -> + WalkPrepareItem( + itemModel = item, + isSelected = selectedIds.contains(item.id), + onCheckBoxClick = { isSelectedNew -> + selectedIds = if (isSelectedNew) { + selectedIds + item.id + } else { + selectedIds - item.id + } + }, + ) + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + Box( + modifier = Modifier + .fillMaxWidth() + .background( + color = PawKeyTheme.colors.primaryGra1, + shape = RoundedCornerShape(8.dp) + ) + ) { + Text( + text = "+ 추가하기", + style = PawKeyTheme.typography.subButtonActive, + color = PawKeyTheme.colors.primary, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp) + ) + } + } +} + +@Composable +private fun WalkPrepareItem( + isSelected : Boolean, + onCheckBoxClick: (Boolean) -> Unit, + itemModel: WalkPrepareItemModel, + modifier: Modifier = Modifier +) { + Row ( + modifier = modifier + .background( + color = if (isSelected) { + PawKeyTheme.colors.primary + } else { + PawKeyTheme.colors.defaultButton + }, + shape = RoundedCornerShape(8.dp) + ) + .noRippleClickable { + onCheckBoxClick(!isSelected) + } + .padding(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + CustomCheckBox( + isSelected = isSelected, + onCheckClick = onCheckBoxClick, + ) + + Spacer(modifier = Modifier.width(8.dp)) + + Text( + text = itemModel.walkItem, + style = PawKeyTheme.typography.subButtonDefault, + color = if (isSelected) { + PawKeyTheme.colors.background + } else { + PawKeyTheme.colors.defaultMiddle + }, + textAlign = TextAlign.Start + ) + } +} + +@Composable +private fun CustomCheckBox( + isSelected: Boolean, + onCheckClick: (Boolean) -> Unit, + modifier: Modifier = Modifier +) { + val checkMarkTint = if (isSelected) { + PawKeyTheme.colors.primary + } else { + PawKeyTheme.colors.defaultButton + } + + Box( + modifier = modifier + .size(15.dp) + .background( + color = PawKeyTheme.colors.background, + shape = RoundedCornerShape(1.dp) + ) + .noRippleClickable { + onCheckClick(!isSelected) + }, + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_walk_course_check), + contentDescription = null, + tint = checkMarkTint, + ) + } +} + +@Preview +@Composable +private fun WalkPrepareBodyPreview() { + PawKeyTheme { + WalkPrepareBody( + itemList = WalkPrepareState().dummyWalkPrepare + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkprepare/component/WalkPrepareWeatherInfo.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkprepare/component/WalkPrepareWeatherInfo.kt new file mode 100644 index 00000000..e77087e8 --- /dev/null +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkprepare/component/WalkPrepareWeatherInfo.kt @@ -0,0 +1,77 @@ +package com.paw.key.presentation.ui.course.walkcourse.walkprepare.component + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.paw.key.R +import com.paw.key.core.designsystem.theme.PawKeyTheme + +@Composable +fun WalkPrepareWeatherInfo( + modifier: Modifier = Modifier, + title: String = "숨이 얼어붙어요...오늘은 나가지말아요", + subTitle: String = "실외 금지! 실내 놀이로 대체", +) { + Box( + modifier = modifier + .fillMaxWidth() + .clip(RoundedCornerShape(16.dp)) + .background(color = PawKeyTheme.colors.primary) + ) { + Image( + painter = painterResource(R.drawable.img_walk_info), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .align(Alignment.BottomStart) + .padding(start = 20.dp) + .size(100.dp) + ) + + Column( + modifier = Modifier + .align(Alignment.CenterStart) + .fillMaxWidth() + .padding(start = 110.dp, end = 20.dp, top = 24.dp, bottom = 24.dp) + ) { + // Todo: body bold로 변경 + Text( + text = title, + style = PawKeyTheme.typography.bodyActive, + color = PawKeyTheme.colors.background, + ) + + Spacer(modifier = Modifier.height(4.dp)) + + Text( + text = subTitle, + style = PawKeyTheme.typography.subButtonDefault, + color = PawKeyTheme.colors.defaultBright + ) + } + } +} + +@Preview(showBackground = true) +@Composable +private fun WalkPrepareWeatherInfoPreview() { + PawKeyTheme { + WalkPrepareWeatherInfo() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkprepare/model/WalkPrepareItemModel.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkprepare/model/WalkPrepareItemModel.kt new file mode 100644 index 00000000..871259e8 --- /dev/null +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkprepare/model/WalkPrepareItemModel.kt @@ -0,0 +1,8 @@ +package com.paw.key.presentation.ui.course.walkcourse.walkprepare.model + +import okhttp3.internal.toImmutableList + +data class WalkPrepareItemModel( + val id : Int = 0, + val walkItem: String = "" +) diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkprepare/state/WalkPrepareContract.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkprepare/state/WalkPrepareContract.kt new file mode 100644 index 00000000..f47e7b1e --- /dev/null +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkprepare/state/WalkPrepareContract.kt @@ -0,0 +1,17 @@ +package com.paw.key.presentation.ui.course.walkcourse.walkprepare.state + +import com.paw.key.presentation.ui.course.walkcourse.walkprepare.model.WalkPrepareItemModel +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList + +data class WalkPrepareState( + val walkPrepareItemList: ImmutableList = persistentListOf(), +) { + val dummyWalkPrepare = listOf( + WalkPrepareItemModel(1, "배변봉투"), + WalkPrepareItemModel(2, "리드줄"), + WalkPrepareItemModel(3, "물"), + WalkPrepareItemModel(4, "간식"), + ).toImmutableList() +} \ No newline at end of file From fe22bbbcc27bbd5387eb2ba6fd3661b5bf837075 Mon Sep 17 00:00:00 2001 From: sonms Date: Mon, 5 Jan 2026 13:01:00 +0900 Subject: [PATCH 10/47] =?UTF-8?q?feat/#154=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=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/walkreview/WalkReviewScreen.kt | 633 ++++++++---------- .../walkreview/component/WalkReviewDialog.kt | 103 +-- .../component/WalkReviewImageRow.kt | 12 +- .../component/WalkReviewInfoHolder.kt | 12 +- .../walkreview/component/WalkReviewItem.kt | 117 ++++ .../component/WalkReviewMultipleFilter.kt | 148 ++++ .../component/WalkReviewSingleFilter.kt | 116 ++++ .../walkreview/model/WalkReviewFilterModel.kt | 29 + .../navigation/WalkReviewNavigation.kt | 37 +- .../walkreview/state/WalkReviewContract.kt | 60 +- .../viewmodel/WalkReviewViewModel.kt | 181 +---- 11 files changed, 810 insertions(+), 638 deletions(-) create mode 100644 app/src/main/java/com/paw/key/presentation/ui/course/walkreview/component/WalkReviewItem.kt create mode 100644 app/src/main/java/com/paw/key/presentation/ui/course/walkreview/component/WalkReviewMultipleFilter.kt create mode 100644 app/src/main/java/com/paw/key/presentation/ui/course/walkreview/component/WalkReviewSingleFilter.kt create mode 100644 app/src/main/java/com/paw/key/presentation/ui/course/walkreview/model/WalkReviewFilterModel.kt diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/WalkReviewScreen.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/WalkReviewScreen.kt index 3af5fb97..b3b00296 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/WalkReviewScreen.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/WalkReviewScreen.kt @@ -3,14 +3,14 @@ package com.paw.key.presentation.ui.course.walkreview import android.Manifest import android.net.Uri import android.os.Build -import android.util.Log -import android.widget.Toast import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.PickVisualMediaRequest import androidx.activity.result.contract.ActivityResultContracts -import androidx.annotation.RequiresApi import androidx.compose.foundation.background +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 @@ -18,64 +18,42 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.SnackbarHostState +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -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.platform.LocalContext -import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.flowWithLifecycle -import coil.compose.AsyncImage import com.paw.key.R -import com.paw.key.core.designsystem.component.DataLoadingScreen -import com.paw.key.core.designsystem.component.PawkeyButton -import com.paw.key.core.designsystem.component.SubChip +import com.paw.key.core.designsystem.component.DokiBorderButton +import com.paw.key.core.designsystem.component.DokiButton +import com.paw.key.core.designsystem.component.InfoChip import com.paw.key.core.designsystem.component.TopBar import com.paw.key.core.designsystem.theme.PawKeyTheme -import com.paw.key.presentation.ui.course.walkreview.component.WalkReviewFeedbackForm -import com.paw.key.presentation.ui.course.walkreview.component.WalkReviewFeedbackHeader +import com.paw.key.presentation.ui.course.walkreview.component.WalkReviewDialog import com.paw.key.presentation.ui.course.walkreview.component.WalkReviewImageRow import com.paw.key.presentation.ui.course.walkreview.component.WalkReviewInfoHolder -import com.paw.key.presentation.ui.course.walkreview.component.WalkReviewTextField -import com.paw.key.presentation.ui.course.walkreview.state.WalkReviewContract +import com.paw.key.presentation.ui.course.walkreview.component.WalkReviewMultipleFilter +import com.paw.key.presentation.ui.course.walkreview.component.WalkReviewSingleFilter +import com.paw.key.presentation.ui.course.walkreview.state.WalkReviewState import com.paw.key.presentation.ui.course.walkreview.viewmodel.WalkReviewViewModel -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) @Composable fun WalkReviewRoute( - navigateUp: () -> Unit, - navigateNext: (routeId : Int) -> Unit, - navigateShared : (routeId : Int, pageId : Int) -> Unit, - routeId : Int, - snackBarHostState: SnackbarHostState, - modifier: Modifier = Modifier, - viewModel: WalkReviewViewModel = hiltViewModel(), - isSharedWalk : Boolean = false + paddingValues: PaddingValues, + navigateUp: () -> Unit = {}, + navigateHome: () -> Unit = {}, + navigateWalkDetail: () -> Unit = {}, + viewModel: WalkReviewViewModel = hiltViewModel() ) { val state by viewModel.state.collectAsStateWithLifecycle() - val isValid = state.isValidForm - val context = LocalContext.current - var isLoading by remember { mutableStateOf(false) } - val coroutineScope = rememberCoroutineScope() - - val lifecycleOwner = LocalLifecycleOwner.current val imagePermission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { Manifest.permission.READ_MEDIA_IMAGES @@ -84,10 +62,12 @@ fun WalkReviewRoute( } val pickMultipleMediaLauncher = rememberLauncherForActivityResult( - ActivityResultContracts.PickMultipleVisualMedia(5) + ActivityResultContracts.PickMultipleVisualMedia(3) ) { uris -> if (uris.isNotEmpty()) { - viewModel.onImagesSelected(uris) + uris.forEach { + viewModel.updateImageList(it) + } } } @@ -95,8 +75,11 @@ fun WalkReviewRoute( ActivityResultContracts.GetMultipleContents() ) { uris: List -> if (uris.isNotEmpty()) { - val limitedUris = uris.take(5) - viewModel.onImagesSelected(limitedUris) + val limitedUris = uris.take(3) + + limitedUris.forEach { + viewModel.updateImageList(it) + } } } @@ -108,53 +91,13 @@ fun WalkReviewRoute( } } - LaunchedEffect(routeId) { - viewModel.getWalkReviewCategory() - viewModel.getWalkReviewInfo(routeId) - } - - LaunchedEffect(viewModel.sideEffect, lifecycleOwner) { - viewModel.sideEffect.flowWithLifecycle(lifecycleOwner.lifecycle) - .collect { sideEffect -> - when (sideEffect) { - is WalkReviewContract.WalkReviewSideEffect.ShowSnackBar -> snackBarHostState.showSnackbar( - sideEffect.message - ) - - is WalkReviewContract.WalkReviewSideEffect.SHowToastMessage -> { - Toast.makeText(context, sideEffect.message, Toast.LENGTH_SHORT).show() - } - - is WalkReviewContract.WalkReviewSideEffect.NavigateNext -> { - Log.d("WalkReviewRoute", "navigateNext") - navigateShared(sideEffect.routeId, sideEffect.pageId) - } - WalkReviewContract.WalkReviewSideEffect.NavigateUp -> navigateUp() - } - } - } WalkReviewScreen( + paddingValues = paddingValues, navigateUp = navigateUp, - onClickFeedback = { categoryId, optionId -> - viewModel.onOptionSelected(categoryId, optionId) - }, - locationDescription = state.location, - timeDescription = state.time, - tags = state.tags, - isFormValid = isValid, - isSharedWalk = isSharedWalk, - imageList = state.images, - petName = state.petName, - titleText = state.title, - contentText = state.content, - feedbackList = state.categoryList, - onTitleTextChanged = { - viewModel.onTitleTextChanged(it) - }, - onContentTextChanged = { - viewModel.onContentTextChanged(it) - }, + navigateHome = navigateHome, + navigateWalkDetail = navigateWalkDetail, + state = state, onClickImage = { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { pickMultipleMediaLauncher.launch( @@ -164,309 +107,283 @@ fun WalkReviewRoute( permissionLauncher.launch(imagePermission) } }, - onImageDelete = { - viewModel.onImageDelete(it) - }, - onClickPublic = { isShare -> - coroutineScope.launch { - isLoading = false - delay(3000L) - isLoading = true - viewModel.postWalkReview( - routeId = routeId, - isShare = isShare - ) - } - }, - /*navigateShared = { - navigateShared(routeId) - },*/ - modifier = modifier, + onImageDelete = viewModel::deleteImage, + onFilterClick = viewModel::onFilterClick, + onTitleValueChange = viewModel::updateReviewTitle, + onContentValueChange = viewModel::updateReviewContent, + onClickComplete = viewModel::completeWalkReview ) - - if (isLoading) { - DataLoadingScreen() - } } -@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) @Composable -fun WalkReviewScreen( - navigateUp: () -> Unit, - onClickFeedback : (Int, Int) -> Unit, // 카테고리, 옵션 - onTitleTextChanged : (String) -> Unit, - onContentTextChanged : (String) -> Unit, - onClickImage : () -> Unit, - onImageDelete : (Uri?) -> Unit, - onClickPublic : (Boolean) -> Unit, // true = 공개 / false = 비공개 - imageList: List, - locationDescription : String, - timeDescription : String, - tags : List, - isFormValid : Boolean, - isSharedWalk : Boolean, - petName : String, - titleText : String, - contentText : String, - feedbackList : List, - modifier: Modifier = Modifier, +private fun WalkReviewScreen( + paddingValues: PaddingValues, + state: WalkReviewState, + navigateUp: () -> Unit = {}, + navigateHome: () -> Unit = {}, + navigateWalkDetail: () -> Unit = {}, + onClickImage: () -> Unit = {}, + onImageDelete: (Int) -> Unit = {}, + onFilterClick: (String, List, Boolean) -> Unit = { _, _, _ -> }, + onTitleValueChange: (String) -> Unit = {}, + onContentValueChange: (String) -> Unit = {}, + onClickComplete: (Boolean) -> Unit = {} ) { Column ( - modifier = modifier + modifier = Modifier .fillMaxSize() + .verticalScroll(rememberScrollState()) + .background( + color = PawKeyTheme.colors.background + ) + .padding(paddingValues) ) { TopBar( title = "산책 기록하기", - onBackClick = navigateUp, + isBackVisible = true, + onBackClick = navigateUp + ) + + WalkReviewImageRow( + imageList = state.walkReviewImageList, + onClickCard = { index, _ -> + if (index != 0) { + onClickImage() + } + }, + onImageDelete = onImageDelete, modifier = Modifier - .background(PawKeyTheme.colors.white1), - isBackVisible = true + .padding(vertical = 12.dp) ) - HorizontalDivider( - thickness = 1.dp, - color = PawKeyTheme.colors.gray50, + // Todo : 서버 내용으로 변경 + Column ( modifier = Modifier .fillMaxWidth() - ) + .padding(16.dp) + ) { + WalkReviewInfoHolder( + icon = R.drawable.ic_walk_review_location, + content = "서울시 강남구 역삼동" + ) + + WalkReviewInfoHolder( + icon = R.drawable.ic_walk_review_time, + content = "2025.10.11 | 오후 11:30" + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Row ( + modifier = Modifier + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + InfoChip( + text = "2.2 km", + isActionChip = true + ) - LazyColumn ( - modifier = modifier - .background(PawKeyTheme.colors.white1) - .padding(bottom = 16.dp) - ){ - if (!isSharedWalk) { - item { - WalkReviewImageRow( - imageList = imageList, - onClickCard = { index, _ -> - if (index != 0) { - onClickImage() - } - }, - onImageDelete = { - onImageDelete(it) - }, - modifier = Modifier - .background(PawKeyTheme.colors.white1), + InfoChip( + text = "2.2 km", + isActionChip = true + ) + + InfoChip( + text = "2.2 km", + isActionChip = true + ) + } + + Spacer(modifier = Modifier.height(40.dp)) + + WalkReviewSingleFilter( + title = "혼잡도", + filterList = state.walkReviewFilterModel.confusionSingleFilterList, + selectedItem = state.getSingleFilterSelection(state.walkReviewFilterModel.confusionSingleFilterList), + onItemSelected = { + onFilterClick( + it, + state.walkReviewFilterModel.confusionSingleFilterList, + true ) } - } else { - item { - Text( - text = "제목", - style = PawKeyTheme.typography.head20Sb, - color = PawKeyTheme.colors.green500, - modifier = Modifier - .padding(bottom = 10.dp) + ) + + Spacer(modifier = Modifier.height(40.dp)) + + WalkReviewSingleFilter( + title = "강아지 교류 빈도", + filterList = state.walkReviewFilterModel.frequencySingleFilterList, + selectedItem = state.getSingleFilterSelection(state.walkReviewFilterModel.frequencySingleFilterList), + onItemSelected = { + onFilterClick( + it, + state.walkReviewFilterModel.frequencySingleFilterList, + true ) - - Row ( - modifier = Modifier - .fillMaxWidth() - .padding(bottom = 10.dp) - ) { - AsyncImage( - model = "", - contentDescription = "profile", - modifier = Modifier - .size(48.dp) - .background( - color = PawKeyTheme.colors.gray50, - shape = CircleShape - ) - .clip(CircleShape) - .padding(end = 10.dp) - ) - - Text( - text = "강아지 이름 작성", - style = PawKeyTheme.typography.body16Sb, - color = PawKeyTheme.colors.gray600 - ) - } } - } - - item { - Column( - modifier = Modifier - .padding(bottom = 12.dp, start = 16.dp, end = 16.dp) - .background(PawKeyTheme.colors.white1) - ) { - WalkReviewInfoHolder( - icon = R.drawable.ic_walk_review_location, - content = locationDescription + ) + + Spacer(modifier = Modifier.height(40.dp)) + + // Todo : 어떻게 필터값을 받을 지 몰라서 보류 + WalkReviewMultipleFilter( + title = "안전", + filterList = state.walkReviewFilterModel.safetyMultipleFilterList, + selectedItems = state.walkReviewSelectedFilterData, + onItemClick = { + onFilterClick( + it, + state.walkReviewFilterModel.safetyMultipleFilterList, + false ) - - WalkReviewInfoHolder( - icon = R.drawable.ic_walk_review_time, - content = timeDescription + } + ) + + Spacer(modifier = Modifier.height(40.dp)) + + WalkReviewMultipleFilter( + title = "편의성", + filterList = state.walkReviewFilterModel.comfortMultipleFilterList, + selectedItems = state.walkReviewSelectedFilterData, + onItemClick = { + onFilterClick( + it, + state.walkReviewFilterModel.comfortMultipleFilterList, + false ) } - } - - item { - Row ( - modifier = Modifier - .padding(bottom = 12.dp, start = 16.dp, end = 16.dp) - .fillMaxWidth() - .background(PawKeyTheme.colors.white1) - ) { - tags.forEach { - SubChip( - text = it, - modifier = Modifier - .padding(end = 6.dp) - ) - } + ) + + Spacer(modifier = Modifier.height(40.dp)) + + WalkReviewMultipleFilter( + title = "환경", + filterList = state.walkReviewFilterModel.environmentMultipleFilterList, + selectedItems = state.walkReviewSelectedFilterData, + onItemClick = { + onFilterClick( + it, + state.walkReviewFilterModel.environmentMultipleFilterList, + false + ) } - } - - item { - HorizontalDivider( - thickness = 10.dp, - color = PawKeyTheme.colors.gray50, - modifier = Modifier - .fillMaxWidth() - .padding(bottom = 12.dp) - ) - } - - item { - WalkReviewFeedbackHeader( - petName = petName, - modifier = Modifier - .padding(top = 12.dp, start = 16.dp, end = 16.dp) - .background(PawKeyTheme.colors.white1) - ) - } - - item { - val emoji = listOf( - "\uD83D\uDE0C", - "\uD83D\uDC36", - "\uD83D\uDEB8", - "\uD83E\uDDFA", - "\uD83C\uDF3F" - ) - - feedbackList.forEachIndexed { index, category -> - WalkReviewFeedbackForm( - icon = R.drawable.ic_walk_review_location, - title = "${emoji[index]} ${category.categoryDescription}", - selectedFeedbackItems = category.options.filter { it.isSelected }.map { it.optionText }, - feedbackList = category.options.map { it.optionText }, - onClickFeedback = { selectedText -> - val selectedOption = category.options.find { it.optionText == selectedText } - if (selectedOption != null) { - onClickFeedback(category.categoryId, selectedOption.optionId) - } - }, + ) + + Spacer(modifier = Modifier.height(40.dp)) + + Text( + text = "산책에 대한 후기를 작성해주세요", + style = PawKeyTheme.typography.subTitle, + color = PawKeyTheme.colors.contents + ) + + Spacer(modifier = Modifier.height(16.dp)) + + // 텍필 넣기 + BasicTextField( + value = state.walkReviewTitle, + onValueChange = onTitleValueChange, + textStyle = PawKeyTheme.typography.bodyActive.copy(color = PawKeyTheme.colors.contents), + modifier = Modifier.fillMaxWidth(), + decorationBox = { innerTextField -> + Box( modifier = Modifier - .padding(top = 12.dp, bottom = 12.dp, start = 16.dp, end = 16.dp), - selectedFeedbackItem = category.options.find { it.isSelected }?.optionText - ) + .fillMaxSize() + .background( + color = PawKeyTheme.colors.defaultBright, + shape = RoundedCornerShape(8.dp) + ) + .padding(16.dp), + contentAlignment = Alignment.CenterStart + ) { + if (state.walkReviewTitle.isEmpty()) { + Text( + text = "후기 제목을 입력해주세요", + style = PawKeyTheme.typography.bodyDefault, + color = PawKeyTheme.colors.defaultMiddle + ) + } + innerTextField() + } } - } - - item { - HorizontalDivider( - thickness = 10.dp, - color = PawKeyTheme.colors.gray50, - modifier = Modifier - .fillMaxWidth() - .padding(top = 12.dp, bottom = 12.dp) - ) - } - - if (!isSharedWalk) { - item { - Text( - text = "산책에 대한 감상을 들려주시겠어요?", - style = PawKeyTheme.typography.body16M, - color = PawKeyTheme.colors.black, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + BasicTextField( + value = state.walkReviewContent, + onValueChange = onContentValueChange, + textStyle = PawKeyTheme.typography.bodyActive.copy(color = PawKeyTheme.colors.contents), + modifier = Modifier + .fillMaxWidth() + .heightIn(min = 216.dp), + decorationBox = { innerTextField -> + Box( modifier = Modifier - .padding(top = 12.dp, start = 16.dp, end = 16.dp) - ) + .fillMaxSize() + .background( + color = PawKeyTheme.colors.defaultBright, + shape = RoundedCornerShape(8.dp) + ) + .padding(16.dp), + contentAlignment = Alignment.TopStart + ) { + if (state.walkReviewContent.isEmpty()) { + Text( + text = "산책에 대한 내용을 작성해주세요", + style = PawKeyTheme.typography.bodyDefault, + color = PawKeyTheme.colors.defaultMiddle + ) + } + innerTextField() + } + } + ) - WalkReviewTextField( - textValue = titleText, - placeHolder = "후기 제목을 입력해주세요.", - onTextChanged = { - onTitleTextChanged(it) - }, - modifier = Modifier - .padding(top = 10.dp, start = 16.dp, end = 16.dp) - ) + Spacer(modifier = Modifier.height(40.dp)) - WalkReviewTextField( - textValue = contentText, - placeHolder = "산책 후기를 간단하게 적어주세요!", - onTextChanged = { - onContentTextChanged(it) - }, - modifier = Modifier - .heightIn(min = 200.dp, max = 400.dp) - .padding(top = 10.dp, bottom = 24.dp, start = 16.dp, end = 16.dp) - ) + DokiBorderButton( + text = "산책 기록 나만보기", + enabled = true, + onClick = { + onClickComplete(false) } - } + ) - item { - HorizontalDivider( - thickness = 10.dp, - color = PawKeyTheme.colors.gray50, - modifier = Modifier - .fillMaxWidth() - .padding(bottom = 24.dp) - ) - } + Spacer(modifier = Modifier.height(8.dp)) - item { - val buttonTextRes = if (isSharedWalk) { - // 공유됨 - R.string.course_review_shared_button - } else { - R.string.course_review_shared_all_button + DokiButton( + text = "산책 기록 공유하기", + enabled = true, + onClick = { + onClickComplete(true) } + ) + } + } - // 공유된 거면 산책후기남기기 / 공유 안된거면 산책 기록 공개하기 - PawkeyButton( - text = stringResource(buttonTextRes), - onClick = { - // 공유뷰 아님 / 현재 그냥 리뷰 - if (!isSharedWalk) { - onClickPublic(true) - Log.d("TAG", "WalkReviewScreen: 공유 안됨") - } else { - onClickPublic(false) - Log.d("TAG", "WalkReviewScreen: 공유 됨") - } - }, - enabled = isFormValid, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp) - ) + if (state.isComplete) { + WalkReviewDialog( + navigateHome = { - if (!isSharedWalk) { - Spacer(modifier = Modifier.height(10.dp)) + }, + navigateWalkDetail = { - PawkeyButton( - text = stringResource(R.string.course_review_saved_button), - onClick = { - onClickPublic(false) - }, - enabled = isFormValid, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - isBackGround = true, - isBorder = true, - ) - } } - } + ) + } +} + +@Preview +@Composable +private fun WalkReviewPreview() { + PawKeyTheme { + WalkReviewScreen( + paddingValues = PaddingValues(), + state = WalkReviewState(), + ) } } \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/component/WalkReviewDialog.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/component/WalkReviewDialog.kt index 54cc94c9..d267bfa2 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/component/WalkReviewDialog.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/component/WalkReviewDialog.kt @@ -1,9 +1,12 @@ package com.paw.key.presentation.ui.course.walkreview.component -import androidx.compose.foundation.background +import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape @@ -11,34 +14,24 @@ import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults 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.draw.clip -import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties -import com.airbnb.lottie.compose.LottieAnimation -import com.airbnb.lottie.compose.LottieCompositionSpec -import com.airbnb.lottie.compose.animateLottieCompositionAsState -import com.airbnb.lottie.compose.rememberLottieComposition +import com.paw.key.R +import com.paw.key.core.designsystem.component.DokiButton import com.paw.key.core.designsystem.theme.PawKeyTheme -import com.paw.key.core.extension.noRippleClickable @Composable fun WalkReviewDialog( - onClickOk: () -> Unit, + navigateHome: () -> Unit, + navigateWalkDetail: () -> Unit, modifier: Modifier = Modifier ) { - val composition by rememberLottieComposition(LottieCompositionSpec.Asset("dialog_animation.json")) - - val progress by animateLottieCompositionAsState( - composition, - iterations = 1, - ) - Dialog ( onDismissRequest = { }, @@ -48,60 +41,67 @@ fun WalkReviewDialog( ), ) { Card ( + modifier = modifier, shape = RoundedCornerShape(8.dp), - modifier = modifier - .padding(8.dp), colors = CardDefaults.cardColors( containerColor = PawKeyTheme.colors.white1 ) ) { Column ( - modifier = Modifier.padding(8.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { - LottieAnimation( - composition = composition, - progress = { - progress }, + Spacer(modifier = Modifier.height(35.dp)) + + Image( + imageVector = ImageVector.vectorResource(R.drawable.ic_walk_review_dialog_paw), + contentDescription = null, modifier = Modifier .size(90.dp) - .padding(bottom = 8.dp) ) + Spacer(modifier = Modifier.height(35.dp)) + Text( - text = "후기가 등록되었어요!", - style = PawKeyTheme.typography.head18Sb, - color = PawKeyTheme.colors.black, - modifier = Modifier.padding(bottom = 6.dp) + text = "후기가 등록이 완료되었어요!", + style = PawKeyTheme.typography.mainButtonActive, + color = PawKeyTheme.colors.contents, ) + Spacer(modifier = Modifier.height(8.dp)) + Text( - text = " 덕분에 PAWKEY가 보호자님을 더 잘 알게 됐어요. \n이 정보로 다음엔 더 완벽한 경로를 추천해 드릴게요.", - style = PawKeyTheme.typography.caption12R, - color = PawKeyTheme.colors.gray300, - modifier = Modifier - .padding(bottom = 32.dp) + text = " 덕분에 DOKI가 보호자님을 더 잘 알게 됐어요.\n" + + "이 정보로 다음엔 더 완벽한 경로를 추천해 드릴게요.", + style = PawKeyTheme.typography.bodySmall, + color = PawKeyTheme.colors.defaultMiddle, ) - Text( - text = "확인", - style = PawKeyTheme.typography.body14Sb, - color = PawKeyTheme.colors.white1, + Spacer(modifier = Modifier.height(32.dp)) + + Row ( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 24.dp, vertical = 8.dp) - .background( - color = PawKeyTheme.colors.green500, - shape = RoundedCornerShape(8.dp) - ) - .clip(RoundedCornerShape(8.dp)) - .padding(vertical = 8.dp) - .noRippleClickable { - onClickOk() - }, - textAlign = TextAlign.Center, - ) + .padding(bottom = 20.dp, start = 16.dp, end = 16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + DokiButton( + text = "홈으로 돌아가기", + enabled = false, + onClick = navigateHome, + modifier = Modifier.weight(1f), + isDialog = true + ) + + DokiButton( + text = "자세히 보러가기", + enabled = true, + onClick = navigateWalkDetail, + modifier = Modifier.weight(1f), + isDialog = true + ) + } } } } @@ -112,7 +112,8 @@ fun WalkReviewDialog( private fun WalkReviewDialogPreview() { PawKeyTheme { WalkReviewDialog( - onClickOk = {} + navigateHome = {}, + navigateWalkDetail = {} ) } } \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/component/WalkReviewImageRow.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/component/WalkReviewImageRow.kt index 0a5f8150..74930e99 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/component/WalkReviewImageRow.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/component/WalkReviewImageRow.kt @@ -4,7 +4,6 @@ import android.net.Uri import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyRow import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -16,16 +15,15 @@ import com.paw.key.core.designsystem.theme.PawKeyTheme fun WalkReviewImageRow ( imageList : List, onClickCard: (Int, Uri?) -> Unit, - onImageDelete : (Uri?) -> Unit, + onImageDelete : (Int) -> Unit, modifier: Modifier = Modifier ) { - val maxImages = 5 + val maxImages = 3 val totalCardCount = (imageList.size).coerceAtMost(maxImages) LazyRow ( modifier = modifier - .fillMaxWidth() - .padding(top = 24.dp, bottom = 24.dp), + .fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(10.dp), contentPadding = PaddingValues(horizontal = 16.dp) ) { @@ -38,9 +36,8 @@ fun WalkReviewImageRow ( onClickCard(index, currentImageUri) }, onImageDelete = { - onImageDelete(currentImageUri) + onImageDelete(index) }, - modifier = Modifier ) } @@ -54,7 +51,6 @@ fun WalkReviewImageRow ( onImageDelete = { }, - modifier = Modifier ) } } diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/component/WalkReviewInfoHolder.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/component/WalkReviewInfoHolder.kt index c5c7007b..9d8c1fd0 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/component/WalkReviewInfoHolder.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/component/WalkReviewInfoHolder.kt @@ -1,5 +1,6 @@ package com.paw.key.presentation.ui.course.walkreview.component +import androidx.annotation.DrawableRes import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth @@ -19,29 +20,28 @@ import com.paw.key.core.designsystem.theme.PawKeyTheme @Composable fun WalkReviewInfoHolder( - icon : Int, + @DrawableRes icon : Int, content : String, modifier: Modifier = Modifier ) { Row ( modifier = modifier - .fillMaxWidth() - .padding(top = 12.dp), + .fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start ) { Icon( imageVector = ImageVector.vectorResource(id = icon), contentDescription = stringResource(R.string.course_review_location_icon_description), - tint = PawKeyTheme.colors.green500, + tint = PawKeyTheme.colors.primary, modifier = Modifier .padding(end = 8.dp) ) Text( text = content, - color = PawKeyTheme.colors.gray400, - style = PawKeyTheme.typography.body14M, + color = PawKeyTheme.colors.defaultDark, + style = PawKeyTheme.typography.bodyActive, modifier = Modifier .weight(1f) ) diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/component/WalkReviewItem.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/component/WalkReviewItem.kt new file mode 100644 index 00000000..a4d2b92f --- /dev/null +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/component/WalkReviewItem.kt @@ -0,0 +1,117 @@ +package com.paw.key.presentation.ui.course.walkreview.component + +import android.net.Uri +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalWindowInfo +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import com.paw.key.R +import com.paw.key.core.designsystem.theme.PawKeyTheme +import com.paw.key.core.extension.noRippleClickable + +@Composable +fun WalkReviewItem( + image : Uri?, + onClickCard: () -> Unit, + onImageDelete : (Uri?) -> Unit, + modifier: Modifier = Modifier, +) { + Card( + shape = RoundedCornerShape(8.dp), + modifier = modifier + .width(LocalWindowInfo.current.containerSize.height.dp * 0.07f) + .height(LocalWindowInfo.current.containerSize.height.dp * 0.07f) + .noRippleClickable { + if (image == null) { + onClickCard() + } + } + ) { + Box( + modifier = Modifier + .weight(1f) + .fillMaxWidth() + .background(if (image == null) PawKeyTheme.colors.defaultButton else Color.Transparent) + ) { + when { + image != null -> { + AsyncImage( + model = image, + contentDescription = stringResource(R.string.course_review_image_description), + modifier = Modifier.fillMaxSize(), + contentScale = ContentScale.Crop + ) + } + + else -> { + Icon( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_walk_review_add_image), + contentDescription = stringResource(R.string.course_review_image_description), + tint = PawKeyTheme.colors.defaultMiddle, + modifier = Modifier + .align(Alignment.Center) + .size(48.dp) + ) + } + } + + if (image != null) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .align(Alignment.TopStart) + .fillMaxWidth() + .padding(8.dp) + .background(color = Color.Transparent) + ) { + Spacer(modifier = Modifier.weight(1f)) + + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_walk_review_cancel), + tint = Color.Unspecified, + contentDescription = null, + modifier = Modifier + .size(24.dp) + .noRippleClickable { + onImageDelete(image) + } + ) + } + } + } + } +} + +@Preview +@Composable +private fun WalkReviewItemPreview() { + PawKeyTheme { + WalkReviewItem( + image = null, + onClickCard = {}, + onImageDelete = {} + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/component/WalkReviewMultipleFilter.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/component/WalkReviewMultipleFilter.kt new file mode 100644 index 00000000..e2383b7f --- /dev/null +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/component/WalkReviewMultipleFilter.kt @@ -0,0 +1,148 @@ +package com.paw.key.presentation.ui.course.walkreview.component + +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.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.paw.key.core.designsystem.theme.PawKeyTheme +import com.paw.key.core.extension.noRippleClickable +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toPersistentList + +@Composable +fun WalkReviewMultipleFilter( + title: String, + filterList: ImmutableList, + selectedItems: ImmutableList, + onItemClick: (String) -> Unit, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .align(Alignment.Start), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + Text( + text = title, + style = PawKeyTheme.typography.subTitle, + color = PawKeyTheme.colors.contents + ) + + Text( + text = "(복수 선택 가능)", + style = PawKeyTheme.typography.subButtonDefault, + color = PawKeyTheme.colors.defaultMiddle + ) + } + + Column ( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + val rows = filterList.chunked(2) + + rows.forEach { rowItems -> + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + rowItems.forEach { item -> + Box(modifier = Modifier.weight(1f)) { + MultipleFilterItem( + text = item, + isSelected = selectedItems.contains(item), + onClick = { onItemClick(item) } + ) + } + } + + if (rowItems.size < 2) { + Spacer(modifier = Modifier.weight(1f)) + } + } + } + } + } +} + +@Composable +private fun MultipleFilterItem( + text: String, + isSelected: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + val borderColor = if (isSelected) PawKeyTheme.colors.primary else PawKeyTheme.colors.defaultMiddle + val backgroundColor = if (isSelected) PawKeyTheme.colors.opacity5Primary else Color.White + val textColor = if (isSelected) PawKeyTheme.colors.primary else PawKeyTheme.colors.defaultMiddle + val fontStyle = if (isSelected) PawKeyTheme.typography.subButtonActive else PawKeyTheme.typography.subButtonDefault + + Box( + modifier = modifier + .fillMaxWidth() + .clip(RoundedCornerShape(8.dp)) + .background(backgroundColor) + .border( + width = 1.dp, + color = borderColor, + shape = RoundedCornerShape(8.dp) + ) + .noRippleClickable(onClick = onClick) + .padding(vertical = 12.dp), + contentAlignment = Alignment.Center + ) { + Text( + text = text, + style = fontStyle, + color = textColor + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun WalkReviewMultipleFilterPreview() { + val dummyFilters = listOf("차량 적음", "보도 넓음", "야간 밝음", "보도/차도 분리", "킥보드/자전거 적음").toPersistentList() + var selectedItems by remember { mutableStateOf(listOf("차량 적음", "킥보드/자전거 적음").toPersistentList()) } + + PawKeyTheme { + Box(modifier = Modifier.padding(16.dp).background(Color.White)) { + WalkReviewMultipleFilter( + title = "산책로 특징", + filterList = dummyFilters, + selectedItems = selectedItems, + onItemClick = { clickedItem -> + selectedItems = if (selectedItems.contains(clickedItem)) { + selectedItems.remove(clickedItem) + } else { + selectedItems.add(clickedItem) + } + } + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/component/WalkReviewSingleFilter.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/component/WalkReviewSingleFilter.kt new file mode 100644 index 00000000..0a0a3037 --- /dev/null +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/component/WalkReviewSingleFilter.kt @@ -0,0 +1,116 @@ +package com.paw.key.presentation.ui.course.walkreview.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.paw.key.core.designsystem.theme.PawKeyTheme +import com.paw.key.core.extension.noRippleClickable +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +@Composable +fun WalkReviewSingleFilter( + title : String, + filterList: ImmutableList, + modifier: Modifier = Modifier, + selectedItem: String = "", + onItemSelected: (String) -> Unit = {}, +) { + Column ( + modifier = modifier + .fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Row ( + modifier = Modifier + .fillMaxWidth() + .align(Alignment.Start), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + Text( + text = title, + style = PawKeyTheme.typography.subTitle, + color = PawKeyTheme.colors.contents + ) + + Text( + text = "(단일 선택 가능)", + style = PawKeyTheme.typography.subButtonDefault, + color = PawKeyTheme.colors.defaultMiddle + ) + } + + Row( + modifier = Modifier + .fillMaxWidth() + .background( + color = PawKeyTheme.colors.defaultBright, + shape = RoundedCornerShape(8.dp) + ), + horizontalArrangement = Arrangement.spacedBy(0.dp) + ) { + filterList.forEach { item -> + FilterItem( + text = item, + isSelected = item == selectedItem, + onClickFilter = { onItemSelected(item) }, + modifier = Modifier.weight(1f) + ) + } + } + } +} + +@Composable +private fun FilterItem( + text: String, + isSelected: Boolean, + onClickFilter: () -> Unit, + modifier: Modifier = Modifier +) { + val backgroundColor = if (isSelected) PawKeyTheme.colors.primary else Color.Transparent + val textColor = if (isSelected) PawKeyTheme.colors.background else PawKeyTheme.colors.defaultMiddle + + Box( + modifier = modifier + .clip(RoundedCornerShape(6.dp)) + .background(color = backgroundColor) + .noRippleClickable( + onClick = onClickFilter + ) + .padding(vertical = 12.dp), + contentAlignment = Alignment.Center + ) { + Text( + text = text, + style = PawKeyTheme.typography.subButtonActive, + color = textColor + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun WalkReviewSingleFilterPreview() { + PawKeyTheme { + WalkReviewSingleFilter( + title = "필터", + filterList = persistentListOf("적음", "평법", "많음"), + selectedItem = "적음" + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/model/WalkReviewFilterModel.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/model/WalkReviewFilterModel.kt new file mode 100644 index 00000000..2ec5da32 --- /dev/null +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/model/WalkReviewFilterModel.kt @@ -0,0 +1,29 @@ +package com.paw.key.presentation.ui.course.walkreview.model + +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +data class WalkReviewFilterModel( + val confusionSingleFilterList: ImmutableList = persistentListOf("적음", "평범", "많음"), + val frequencySingleFilterList: ImmutableList = persistentListOf("교류 없음", "보통", "교류 활발"), + + val safetyMultipleFilterList: ImmutableList = persistentListOf( + "차량 적음", + "보도/차도 분리", + "보도 넓음", + "킥보드/자전거 적음 ", + "야간 밝음" + ), + val comfortMultipleFilterList: ImmutableList = persistentListOf( + "벤치", + "배변 봉투 쓰레기통", + "편의점", + "반려견 동반 카페" + ), + val environmentMultipleFilterList: ImmutableList = persistentListOf( + "잔디길", + "흙길", + "포장길", + "놀이터/공터" + ) +) diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/navigation/WalkReviewNavigation.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/navigation/WalkReviewNavigation.kt index 650816d1..2a86e94e 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/navigation/WalkReviewNavigation.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/navigation/WalkReviewNavigation.kt @@ -1,47 +1,36 @@ package com.paw.key.presentation.ui.course.walkreview.navigation -import android.os.Build -import androidx.annotation.RequiresApi -import androidx.compose.material3.SnackbarHostState +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.ui.Modifier import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import androidx.navigation.compose.composable -import androidx.navigation.toRoute +import com.paw.key.core.navigation.MainTabRoute import com.paw.key.core.navigation.Route import com.paw.key.presentation.ui.course.walkreview.WalkReviewRoute +import com.paw.key.presentation.ui.home.HomeRoute import kotlinx.serialization.Serializable fun NavController.navigateWalkReview( navOptions: NavOptions?, - routeId: Int, ) { - navigate(WalkReview(routeId), navOptions) + navigate(WalkReview, navOptions) } -@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) fun NavGraphBuilder.walkReviewNavGraph( - navigateUp: () -> Unit, - navigateNext: (routeId : Int) -> Unit, - navigateShared : (routeId : Int, pageId : Int) -> Unit, - snackBarHostState: SnackbarHostState, + paddingValues: PaddingValues, + navigateHome: () -> Unit, + navigateWalkDetail: () -> Unit, ) { - composable { backStackEntry -> - val ids = backStackEntry.toRoute() - + composable { WalkReviewRoute( - navigateUp = navigateUp, - navigateNext = { - navigateNext(ids.routeId) - }, - navigateShared = { routeId, pageId -> - navigateShared(routeId, pageId) - }, - snackBarHostState = snackBarHostState, - routeId = ids.routeId + paddingValues = paddingValues, + navigateHome = navigateHome, + navigateWalkDetail = navigateWalkDetail ) } } @Serializable -data class WalkReview(val routeId : Int) : Route \ No newline at end of file +data object WalkReview : Route diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/state/WalkReviewContract.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/state/WalkReviewContract.kt index fee4731c..d2ef0c39 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/state/WalkReviewContract.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/state/WalkReviewContract.kt @@ -2,42 +2,38 @@ package com.paw.key.presentation.ui.course.walkreview.state import android.net.Uri import androidx.compose.runtime.Immutable -import com.paw.key.presentation.ui.course.walkreview.WalkReviewCategoryUiModel +import com.paw.key.presentation.ui.course.walkreview.model.WalkReviewFilterModel import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf -class WalkReviewContract { - - @Immutable - data class WalkReviewState( - val images: PersistentList = persistentListOf(), - val tags : PersistentList = persistentListOf(), - val location: String = "", - val date: String = "", - val time: String = "", - - val title: String = "", - val content: String = "", - - val petName: String = "포비", - - val isPublic: Boolean = false, - val isMine: Boolean = false, - - val categoryList: List = emptyList() - ) { - val isValidForm: Boolean - get() = title.isNotBlank() && - content.isNotBlank() && - categoryList.all { category -> - category.options.any { it.isSelected } - } +@Immutable +data class WalkReviewState( + val walkReviewImageList : PersistentList = persistentListOf(), + val walkReviewFilterModel: WalkReviewFilterModel = WalkReviewFilterModel(), + val walkReviewSelectedFilterData: PersistentList = persistentListOf(), + val walkReviewTitle : String = "", + val walkReviewContent: String = "", + val isComplete : Boolean = false +) { + fun getSingleFilterSelection(categoryList: List): String { + return walkReviewSelectedFilterData.firstOrNull { categoryList.contains(it) } ?: "" } - sealed class WalkReviewSideEffect { - data class ShowSnackBar(val message: String) : WalkReviewSideEffect() - data class SHowToastMessage(val message: String) : WalkReviewSideEffect() - data object NavigateUp: WalkReviewSideEffect() - data class NavigateNext(val routeId : Int, val pageId : Int): WalkReviewSideEffect() + fun getUpdatedFilterList( + selectedItem: String, + categoryList: List, + isSingleSelect: Boolean + ): PersistentList { + return if (isSingleSelect) { + walkReviewSelectedFilterData + .removeAll(categoryList) + .add(selectedItem) + } else { + if (walkReviewSelectedFilterData.contains(selectedItem)) { + walkReviewSelectedFilterData.remove(selectedItem) + } else { + walkReviewSelectedFilterData.add(selectedItem) + } + } } } \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/viewmodel/WalkReviewViewModel.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/viewmodel/WalkReviewViewModel.kt index c84a079e..43102316 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/viewmodel/WalkReviewViewModel.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/viewmodel/WalkReviewViewModel.kt @@ -1,208 +1,71 @@ package com.paw.key.presentation.ui.course.walkreview.viewmodel -import android.content.Context import android.net.Uri -import android.util.Log import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.paw.key.core.util.PhotoUtils -import com.paw.key.core.util.PreferenceDataStore -import com.paw.key.data.dto.request.walkreview.WalkCourseReviewRequestDto -import com.paw.key.domain.model.entity.walkreview.WalkReviewRecordCategory -import com.paw.key.domain.model.entity.walkreview.WalkReviewRecordEntity -import com.paw.key.domain.repository.walkreview.WalkReviewRepository -import com.paw.key.presentation.ui.course.walkreview.state.WalkReviewContract.WalkReviewSideEffect -import com.paw.key.presentation.ui.course.walkreview.state.WalkReviewContract.WalkReviewState -import com.paw.key.presentation.ui.course.walkreview.util.toUiModel +import com.paw.key.presentation.ui.course.walkreview.state.WalkReviewState import dagger.hilt.android.lifecycle.HiltViewModel -import dagger.hilt.android.qualifiers.ApplicationContext -import kotlinx.collections.immutable.toPersistentList -import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharedFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class WalkReviewViewModel @Inject constructor( - @ApplicationContext private val context: Context, - private val repository: WalkReviewRepository ) : ViewModel() { private val _state = MutableStateFlow(WalkReviewState()) - val state : StateFlow - get() = _state.asStateFlow() + val state = _state.asStateFlow() - private val _sideEffect = MutableSharedFlow() - val sideEffect : SharedFlow - get() = _sideEffect.asSharedFlow() - - private val userId = PreferenceDataStore.getUserId() - - fun postWalkReview(routeId : Int, isShare : Boolean) { - viewModelScope.launch { - Log.d("WalkReviewViewModel", "${userId.first()}") - val category = state.value.categoryList.map { category -> - WalkReviewRecordCategory( - categoryId = category.categoryId, - selectedOptionIds = category.options.filter { it.isSelected }.map { it.optionId } - ) - } - - val requestEntity = WalkReviewRecordEntity( - title = state.value.title, - description = state.value.content, - isPublic = isShare, - categories = category, - routeId = routeId.toLong(), - isMine = state.value.isMine - ) - - val imageFiles = PhotoUtils.uriListToMultipartParts( - uris = state.value.images, - contentResolver = context.contentResolver + fun updateImageList(uri : Uri) { + _state.update { + it.copy( + walkReviewImageList = it.walkReviewImageList.add(uri) ) - - repository.postWalkReview( - userId = userId.first(), - imageFiles = imageFiles, - walkReviewRequest = requestEntity - ).onSuccess { response -> - _sideEffect.emit(WalkReviewSideEffect.NavigateNext(response.routeId, response.postId)) - Log.d("WalkReviewViewModel", "리뷰 전송 성공!") - Log.d("WalkReviewViewModel", "routeId : ${response.routeId}, postId : ${response.postId}") - }.onFailure { respon -> - _sideEffect.emit(WalkReviewSideEffect.SHowToastMessage("${respon.message}")) - _sideEffect.emit(WalkReviewSideEffect.ShowSnackBar("리뷰 전송 실패!")) - - - Log.e("WalkReviewViewModel", "리뷰 전송 실패!") - } - } - } - - fun getWalkReviewCategory() { - viewModelScope.launch { - repository.getWalkReviewCategory( - userId = userId.first() - ).onSuccess { - _state.update { currentState -> - currentState.copy( - categoryList = it.categoryList.map { entity -> entity.toUiModel() } - ) - } - Log.d("WalkReviewViewModel", "카테고리 가져오기 성공!") - }.onFailure { - _sideEffect.emit(WalkReviewSideEffect.ShowSnackBar("카테고리 가져오기 실패!")) - Log.e("WalkReviewViewModel", "카테고리 가져오기 실패!") - } - } - } - - fun getWalkReviewInfo(routeId: Int) { - viewModelScope.launch { - repository.getWalkReviewInfo( - userId = userId.first(), - routeId = routeId - ).onSuccess { - _state.update { currentState -> - currentState.copy( - location = it.routeDto.locationDescription, - time = it.routeDto.dateDescription, - tags = it.routeDto.descriptionTags.toPersistentList(), - petName = it.petName - ) - } - Log.d("WalkReviewViewModel", "산책 정보 가져오기 성공!") - }.onFailure { - _sideEffect.emit(WalkReviewSideEffect.ShowSnackBar("산책 정보 가져오기 실패!")) - Log.e("WalkReviewViewModel", "산책 정보 가져오기 실패!") - } } } - fun onClickPublic(isShare : Boolean) { + fun deleteImage(index : Int) { _state.update { it.copy( - isPublic = isShare + walkReviewImageList = it.walkReviewImageList.removeAt(index) ) } } - fun onOptionSelected(categoryId: Int, optionId: Int) { - val updatedList = state.value.categoryList.map { category -> - if (category.categoryId == categoryId) { - val newOptions = category.options.map { option -> - if (option.optionId == optionId) { - option.copy(isSelected = !option.isSelected) - } else { - option - } - } - category.copy(options = newOptions) - } else { - category - } - } - - _state.update { it.copy(categoryList = updatedList) } - } - - fun onTitleTextChanged(text : String) { + fun updateReviewTitle(title : String) { _state.update { it.copy( - title = text + walkReviewTitle = title ) } } - fun onContentTextChanged(text : String) { + fun updateReviewContent(content : String) { _state.update { it.copy( - content = text + walkReviewContent = content ) } } - fun onSelectCategoryOption(categoryId: Int, selectedOptionId: Int) { - _state.update { current -> - val updatedList = current.categoryList.map { category -> - if (category.categoryId == categoryId) { - category.copy( - options = category.options.map { option -> - option.copy( - isSelected = option.optionId == selectedOptionId - ) - } - ) - } else category - } - - current.copy(categoryList = updatedList) - } - } - - fun onImagesSelected(uris: List) { - val currentImages = _state.value.images.toMutableList() - currentImages.addAll(uris) + fun onFilterClick( + item: String, + categoryList: List, + isSingle: Boolean + ) { + val newFilterList = _state.value.getUpdatedFilterList(item, categoryList, isSingle) _state.update { it.copy( - images = currentImages.toPersistentList() + walkReviewSelectedFilterData = newFilterList ) } } - fun onImageDelete(uri : Uri?) { + fun completeWalkReview(isPublic : Boolean) { + // Todo 나중에 검증 로직 추가 - 예를 들면 데이터가 다 필요한지 등 - 필터 _state.update { it.copy( - images = it.images.filter { currentUri -> - currentUri != uri - }.toPersistentList() + isComplete = true ) } } From c9cfdbaa5904519476a3f725e390513e36e75bb8 Mon Sep 17 00:00:00 2001 From: sonms Date: Mon, 5 Jan 2026 13:01:12 +0900 Subject: [PATCH 11/47] =?UTF-8?q?feat/#154=20=EC=83=81=EC=84=B8=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=EB=B7=B0=20=EA=B5=AC=ED=98=84=20=EC=A4=80?= =?UTF-8?q?=EB=B9=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../component/WalkRecordItem.kt | 17 +++++----- .../presentation/ui/detail/DetailScreen.kt | 32 +++++++++++++++++++ 2 files changed, 40 insertions(+), 9 deletions(-) rename app/src/main/java/com/paw/key/presentation/ui/course/{walk => walkcourse}/component/WalkRecordItem.kt (74%) create mode 100644 app/src/main/java/com/paw/key/presentation/ui/detail/DetailScreen.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/walkcourse/component/WalkRecordItem.kt similarity index 74% rename from app/src/main/java/com/paw/key/presentation/ui/course/walk/component/WalkRecordItem.kt rename to app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/component/WalkRecordItem.kt index 0459b36b..acd77bab 100644 --- 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/walkcourse/component/WalkRecordItem.kt @@ -1,10 +1,10 @@ -package com.paw.key.presentation.ui.course.walk.component +package com.paw.key.presentation.ui.course.walkcourse.component +import androidx.annotation.StringRes 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 @@ -17,28 +17,27 @@ import com.paw.key.core.designsystem.theme.PawKeyTheme @Composable fun WalkRecordItem( - recordTitle : Int, + @StringRes recordTitle : Int, recordContent : String, modifier: Modifier = Modifier, ) { Column ( - modifier = modifier - .padding(vertical = 16.dp), + modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { Text( text = stringResource(recordTitle), - color = PawKeyTheme.colors.gray500, - style = PawKeyTheme.typography.caption12Sb2 + color = PawKeyTheme.colors.defaultDark, + style = PawKeyTheme.typography.subButtonActive ) Spacer(modifier = Modifier.height(4.dp)) Text( text = recordContent, - color = PawKeyTheme.colors.green500, - style = PawKeyTheme.typography.head20B2 + color = PawKeyTheme.colors.primary, + style = PawKeyTheme.typography.header3 ) } } diff --git a/app/src/main/java/com/paw/key/presentation/ui/detail/DetailScreen.kt b/app/src/main/java/com/paw/key/presentation/ui/detail/DetailScreen.kt new file mode 100644 index 00000000..5567b727 --- /dev/null +++ b/app/src/main/java/com/paw/key/presentation/ui/detail/DetailScreen.kt @@ -0,0 +1,32 @@ +package com.paw.key.presentation.ui.detail + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.Preview +import com.paw.key.core.designsystem.theme.PawKeyTheme + +@Composable +fun DetailRoute( + paddingValues: PaddingValues, +) { + DetailScreen( + paddingValues = paddingValues + ) +} + +@Composable +private fun DetailScreen( + paddingValues: PaddingValues, +) { + +} + +@Preview +@Composable +private fun DetailScreenPreview() { + PawKeyTheme { + DetailScreen( + paddingValues = PaddingValues() + ) + } +} \ No newline at end of file From 8350d0ecda02adb459d0d2e1f0a9b4b7c53343db Mon Sep 17 00:00:00 2001 From: minseong-PC Date: Mon, 5 Jan 2026 18:00:58 +0900 Subject: [PATCH 12/47] =?UTF-8?q?add/#154=20drawable=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../drawable/ic_walk_review_course_info.xml | 16 +++++++++++ .../res/drawable/ic_walk_review_location.xml | 25 ++++++++++++------ .../main/res/drawable/ic_walk_review_time.xml | 9 ++++--- app/src/main/res/drawable/user_poi.jpg | Bin 0 -> 36223 bytes app/src/main/res/drawable/user_poi.png | Bin 4391 -> 0 bytes 5 files changed, 39 insertions(+), 11 deletions(-) create mode 100644 app/src/main/res/drawable/ic_walk_review_course_info.xml create mode 100644 app/src/main/res/drawable/user_poi.jpg delete mode 100644 app/src/main/res/drawable/user_poi.png diff --git a/app/src/main/res/drawable/ic_walk_review_course_info.xml b/app/src/main/res/drawable/ic_walk_review_course_info.xml new file mode 100644 index 00000000..1ad91cc3 --- /dev/null +++ b/app/src/main/res/drawable/ic_walk_review_course_info.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_walk_review_location.xml b/app/src/main/res/drawable/ic_walk_review_location.xml index 24194683..3d2d43f1 100644 --- a/app/src/main/res/drawable/ic_walk_review_location.xml +++ b/app/src/main/res/drawable/ic_walk_review_location.xml @@ -1,12 +1,21 @@ - - - - \ No newline at end of file + android:viewportHeight="19"> + + + + + diff --git a/app/src/main/res/drawable/ic_walk_review_time.xml b/app/src/main/res/drawable/ic_walk_review_time.xml index 192f394e..89f6c0a4 100644 --- a/app/src/main/res/drawable/ic_walk_review_time.xml +++ b/app/src/main/res/drawable/ic_walk_review_time.xml @@ -5,6 +5,9 @@ android:viewportWidth="18" android:viewportHeight="18"> - \ No newline at end of file + android:fillColor="#FF9C9C9C" + android:pathData="M17 8.5c0 4.14-3.36 7.5-7.5 7.5C5.36 16 2 12.64 2 8.5 2 4.36 5.36 1 9.5 1 13.64 1 17 4.36 17 8.5Z"/> + + diff --git a/app/src/main/res/drawable/user_poi.jpg b/app/src/main/res/drawable/user_poi.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3aa40b03aa9c6c689f7b8747e067712b5ec6e98c GIT binary patch literal 36223 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

Zfy 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 h+WG9kjrOxO{udC+votGySS Date: Mon, 5 Jan 2026 18:01:19 +0900 Subject: [PATCH 13/47] =?UTF-8?q?refactor:=20=EC=82=B0=EC=B1=85=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=20UI=20=EB=B0=8F=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `WalkCourseState`에 산책 시간과 거리를 포맷팅하는 로직을 통합하여, 컴포저블 내에서 `derivedStateOf`를 사용하던 기존 방식을 제거했습니다. - `WalkCourseScreen` 컴포저블의 파라미터를 `WalkCourseState`로 통합하여 의존성을 줄이고 코드를 단순화했습니다. - NaverMap의 로고 위치를 `BOTTOM or START`에서 `TOP or END`로 변경하고, 사용자 위치 표시 아이콘의 크기를 지정했습니다. - `WalkCourseGraph` 객체의 파일 위치를 `WalkCourseRoute.kt`에서 `WalkCourseGraph.kt`로 이동하여 코드 구조를 개선했습니다. - 불필요한 주석과 `mapUiSettings` 관련 로직을 정리했습니다. --- .../ui/course/navigation/WalkCourseGraph.kt | 5 +- .../ui/course/navigation/WalkCourseRoute.kt | 3 - .../ui/course/walkcourse/WalkCourseScreen.kt | 58 +++++-------------- .../walkcourse/state/WalkCourseContract.kt | 12 +++- 4 files changed, 28 insertions(+), 50 deletions(-) diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/navigation/WalkCourseGraph.kt b/app/src/main/java/com/paw/key/presentation/ui/course/navigation/WalkCourseGraph.kt index f6ed276b..211a7719 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/course/navigation/WalkCourseGraph.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/course/navigation/WalkCourseGraph.kt @@ -8,7 +8,10 @@ import androidx.navigation.navigation import com.paw.key.presentation.ui.course.walkcourse.WalkCourseRoute import com.paw.key.presentation.ui.course.walkcourse.walkcomplete.WalkCompleteRoute import com.paw.key.presentation.ui.course.walkcourse.walkprepare.WalkPrepareRoute +import kotlinx.serialization.Serializable +@Serializable +data object WalkCourseGraph : WalkRoute fun NavGraphBuilder.walkCourseGraph( paddingValues: PaddingValues, @@ -41,4 +44,4 @@ fun NavGraphBuilder.walkCourseGraph( ) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/navigation/WalkCourseRoute.kt b/app/src/main/java/com/paw/key/presentation/ui/course/navigation/WalkCourseRoute.kt index c72c66da..70b0f6f8 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/course/navigation/WalkCourseRoute.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/course/navigation/WalkCourseRoute.kt @@ -5,9 +5,6 @@ import kotlinx.serialization.Serializable sealed interface WalkRoute : MainTabRoute -@Serializable -data object WalkCourseGraph : WalkRoute - @Serializable data object WalkPrepare: WalkRoute diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/WalkCourseScreen.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/WalkCourseScreen.kt index 14ab85c3..2b91b429 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/WalkCourseScreen.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/WalkCourseScreen.kt @@ -28,7 +28,6 @@ import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -75,8 +74,7 @@ import com.paw.key.presentation.ui.course.util.rememberCustomFusedLocationSource import com.paw.key.presentation.ui.course.util.rememberStepCounter import com.paw.key.presentation.ui.course.walkcourse.component.WalkRecordItem import com.paw.key.presentation.ui.course.walkcourse.state.WalkCourseSideEffect -import com.paw.key.presentation.ui.course.walkcourse.util.formatDistance -import com.paw.key.presentation.ui.course.walkcourse.util.formatTime +import com.paw.key.presentation.ui.course.walkcourse.state.WalkCourseState import com.paw.key.presentation.ui.course.walkcourse.viewmodel.WalkCourseViewModel import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.flow.drop @@ -116,13 +114,6 @@ fun WalkCourseRoute( val stepCounter = rememberStepCounter() - val formattedTotalTime by remember(state.totalTimeMillis) { - derivedStateOf { formatTime(state.totalTimeMillis) } - } - - val formattedDistance by remember(state.mapState.totalDistance) { - derivedStateOf { formatDistance(state.mapState.totalDistance) } - } var mapProperties by remember { mutableStateOf(MapProperties()) @@ -159,20 +150,6 @@ fun WalkCourseRoute( } ) - /*// 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) - }*/ - - - LaunchedEffect(state.recordingState.isRecording, stepCounter) { if (state.recordingState.isRecording) { stepCounter.setStepCountListener(object : StepCountListener { @@ -229,14 +206,12 @@ fun WalkCourseRoute( is UiState.Success -> { WalkCourseScreen( paddingValues = paddingValues, + state = state, cameraPositionState = cameraPositionState, currentLocation = state.mapState.currentLocation, routeLineCoords = state.mapState.poiPoints, locationSource = fusedLocationClient, - totalDistance = formattedDistance, mapProperties = mapProperties, - currentSteps = state.stepCounterState.sessionSteps, - totalTime = formattedTotalTime, isRecording = state.recordingState.isRecording, // 산책 중단, 계속 여부 isTracking = state.mapState.isTrackingEnabled, // 산책 포커싱 onClickTracking = { @@ -268,13 +243,11 @@ fun WalkCourseRoute( @Composable fun WalkCourseScreen( paddingValues: PaddingValues, + state: WalkCourseState, cameraPositionState: CameraPositionState, locationSource: FusedLocationSource, currentLocation : LatLng?, routeLineCoords : ImmutableList, - totalDistance: String, - currentSteps: Long, - totalTime: String, mapProperties: MapProperties, isTracking: Boolean, // 포커싱 여부 isRecording: Boolean, // 산책 중단, 계속 여부 @@ -284,15 +257,6 @@ fun WalkCourseScreen( onStopTracking: () -> Unit, // 종료하기 onCaptured: (Bitmap?) -> Unit, ) { - var mapUiSettings by remember { - mutableStateOf( - MapUiSettings( - logoGravity = Gravity.BOTTOM or Gravity.START, - isZoomControlEnabled = false - ) - ) - } - Box( modifier = Modifier .fillMaxSize() @@ -304,13 +268,19 @@ fun WalkCourseScreen( cameraPositionState = cameraPositionState, locationSource = locationSource, locale = Locale.KOREA, - uiSettings = mapUiSettings, + uiSettings = MapUiSettings( + logoGravity = Gravity.TOP or Gravity.END, + isZoomControlEnabled = false, + isLogoClickEnabled = true + ), properties = mapProperties, ) { if (currentLocation != null) { LocationOverlay( position = currentLocation, icon = OverlayImage.fromResource(R.drawable.user_poi), + iconWidth = 24, + iconHeight = 24, ) } @@ -381,15 +351,15 @@ fun WalkCourseScreen( ) { WalkRecordItem( recordTitle = R.string.course_record_distance, - recordContent = totalDistance + recordContent = state.formattedDistance ) WalkRecordItem( recordTitle = R.string.course_record_time, - recordContent = totalTime + recordContent = state.formattedTime ) WalkRecordItem( recordTitle = R.string.course_record_step, - recordContent = currentSteps.toString() + recordContent = state.stepCounterState.sessionSteps.toString() ) } @@ -494,4 +464,4 @@ fun WalkCourseScreen( @Composable private fun WalkCourseBottomPreview() { PawKeyTheme {} -} \ No newline at end of file +} diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/state/WalkCourseContract.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/state/WalkCourseContract.kt index f4ff78df..24b1c8dc 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/state/WalkCourseContract.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/state/WalkCourseContract.kt @@ -6,6 +6,8 @@ import com.paw.key.R import com.paw.key.presentation.ui.course.walkcourse.model.MapState import com.paw.key.presentation.ui.course.walkcourse.model.RecordingState import com.paw.key.presentation.ui.course.walkcourse.model.StepCounterState +import com.paw.key.presentation.ui.course.walkcourse.util.formatDistance +import com.paw.key.presentation.ui.course.walkcourse.util.formatTime @Immutable data class WalkCourseState( @@ -13,7 +15,13 @@ data class WalkCourseState( val mapState: MapState = MapState(), val stepCounterState: StepCounterState = StepCounterState(), val totalTimeMillis: Long = 0L, -) +) { + val formattedTime: String + get() = formatTime(this.totalTimeMillis) + + val formattedDistance: String + get() = formatDistance(this.mapState.totalDistance) +} sealed class WalkCourseSideEffect { data class ShowSnackBar(val message: String) : WalkCourseSideEffect() @@ -30,4 +38,4 @@ sealed class WalkCourseRecord ( data object TimeRecord : WalkCourseRecord(R.string.course_record_time) data object StepsRecord : WalkCourseRecord(R.string.course_record_step) -} \ No newline at end of file +} From 6f8b5dc8624d22f274d98643a01159e09c2c4a74 Mon Sep 17 00:00:00 2001 From: minseong-PC Date: Mon, 5 Jan 2026 18:01:41 +0900 Subject: [PATCH 14/47] =?UTF-8?q?feat/#154=20=EC=82=B0=EC=B1=85=20?= =?UTF-8?q?=EA=B8=B0=EB=A1=9D=20=ED=99=94=EB=A9=B4=20UI=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EB=B0=8F=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 산책 기록 화면의 레이아웃과 컴포넌트를 개선했습니다. - 키보드 활성화 시 화면이 가려지지 않도록 `imePadding()`을 적용했습니다. - 산책 코스 정보 표시를 위해 `WalkReviewInfoHolder` 컴포넌트를 사용하도록 변경하고, 아이콘 틴트가 적용되지 않도록 수정했습니다. - `WalkReviewState`에 산책 코스 정보를 담는 `walkReviewCourseInfo`를 추가했습니다. - 산책 기록 완료 다이얼로그의 네비게이션 로직을 연결했습니다. - 불필요한 `InfoChip` 사용 코드를 제거했습니다. --- .../component/DokiBorderButton.kt | 2 +- .../ui/course/walkreview/WalkReviewScreen.kt | 47 ++++++------------- .../component/WalkReviewInfoHolder.kt | 10 ++-- .../walkreview/state/WalkReviewContract.kt | 8 ++-- 4 files changed, 25 insertions(+), 42 deletions(-) diff --git a/app/src/main/java/com/paw/key/core/designsystem/component/DokiBorderButton.kt b/app/src/main/java/com/paw/key/core/designsystem/component/DokiBorderButton.kt index 0d071584..f5114bbc 100644 --- a/app/src/main/java/com/paw/key/core/designsystem/component/DokiBorderButton.kt +++ b/app/src/main/java/com/paw/key/core/designsystem/component/DokiBorderButton.kt @@ -66,4 +66,4 @@ private fun DokiBorderButtonPreview() { onClick = {} ) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/WalkReviewScreen.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/WalkReviewScreen.kt index b3b00296..18cfb12e 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/WalkReviewScreen.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/WalkReviewScreen.kt @@ -17,6 +17,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape @@ -132,10 +133,11 @@ private fun WalkReviewScreen( Column ( modifier = Modifier .fillMaxSize() - .verticalScroll(rememberScrollState()) .background( color = PawKeyTheme.colors.background ) + .imePadding() + .verticalScroll(rememberScrollState()) .padding(paddingValues) ) { TopBar( @@ -144,6 +146,8 @@ private fun WalkReviewScreen( onBackClick = navigateUp ) + Spacer(modifier = Modifier.height(14.dp)) + WalkReviewImageRow( imageList = state.walkReviewImageList, onClickCard = { index, _ -> @@ -152,10 +156,10 @@ private fun WalkReviewScreen( } }, onImageDelete = onImageDelete, - modifier = Modifier - .padding(vertical = 12.dp) ) + Spacer(modifier = Modifier.height(20.dp)) + // Todo : 서버 내용으로 변경 Column ( modifier = Modifier @@ -172,29 +176,10 @@ private fun WalkReviewScreen( content = "2025.10.11 | 오후 11:30" ) - Spacer(modifier = Modifier.height(8.dp)) - - Row ( - modifier = Modifier - .fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { - InfoChip( - text = "2.2 km", - isActionChip = true - ) - - InfoChip( - text = "2.2 km", - isActionChip = true - ) - - InfoChip( - text = "2.2 km", - isActionChip = true - ) - } + WalkReviewInfoHolder( + icon = R.drawable.ic_walk_review_course_info, + content = "2025.10.11 | 오후 11:30 | 걸음수" + ) Spacer(modifier = Modifier.height(40.dp)) @@ -367,12 +352,8 @@ private fun WalkReviewScreen( if (state.isComplete) { WalkReviewDialog( - navigateHome = { - - }, - navigateWalkDetail = { - - } + navigateHome = navigateHome, + navigateWalkDetail = navigateWalkDetail ) } } @@ -386,4 +367,4 @@ private fun WalkReviewPreview() { state = WalkReviewState(), ) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/component/WalkReviewInfoHolder.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/component/WalkReviewInfoHolder.kt index 9d8c1fd0..a7b84217 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/component/WalkReviewInfoHolder.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/component/WalkReviewInfoHolder.kt @@ -10,8 +10,8 @@ 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.graphics.vector.ImageVector -import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -32,8 +32,8 @@ fun WalkReviewInfoHolder( ) { Icon( imageVector = ImageVector.vectorResource(id = icon), - contentDescription = stringResource(R.string.course_review_location_icon_description), - tint = PawKeyTheme.colors.primary, + contentDescription = null, + tint = Color.Unspecified, modifier = Modifier .padding(end = 8.dp) ) @@ -53,8 +53,8 @@ fun WalkReviewInfoHolder( private fun WalkReviewInfoHolderPreview() { PawKeyTheme { WalkReviewInfoHolder( - icon = R.drawable.ic_walk_review_add_image, + icon = R.drawable.ic_walk_review_time, content = "서울시 강남구 역삼동" ) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/state/WalkReviewContract.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/state/WalkReviewContract.kt index d2ef0c39..b5c40eb2 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/state/WalkReviewContract.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/state/WalkReviewContract.kt @@ -2,6 +2,7 @@ package com.paw.key.presentation.ui.course.walkreview.state import android.net.Uri import androidx.compose.runtime.Immutable +import com.paw.key.presentation.ui.course.walkcourse.model.WalkInfoState import com.paw.key.presentation.ui.course.walkreview.model.WalkReviewFilterModel import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf @@ -13,10 +14,11 @@ data class WalkReviewState( val walkReviewSelectedFilterData: PersistentList = persistentListOf(), val walkReviewTitle : String = "", val walkReviewContent: String = "", - val isComplete : Boolean = false + val walkReviewCourseInfo: WalkInfoState = WalkInfoState(), + val isComplete : Boolean = false, ) { fun getSingleFilterSelection(categoryList: List): String { - return walkReviewSelectedFilterData.firstOrNull { categoryList.contains(it) } ?: "" + return walkReviewSelectedFilterData.firstOrNull { categoryList.contains(it) }.orEmpty() } fun getUpdatedFilterList( @@ -36,4 +38,4 @@ data class WalkReviewState( } } } -} \ No newline at end of file +} From f3c615d0aed8e1140a56a73c55b5a6e46cf15359 Mon Sep 17 00:00:00 2001 From: minseong-PC Date: Wed, 4 Feb 2026 17:49:48 +0900 Subject: [PATCH 15/47] =?UTF-8?q?mod/#154:=20=EC=82=B0=EC=B1=85=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C=20=ED=99=94=EB=A9=B4=20UI=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20=EB=B0=8F=20=EA=B8=B0=EB=8A=A5=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Compose BOM 버전을 `2024.12.01`에서 `2026.01.01`로 업데이트했습니다. - `WalkCompleteRoute`에 `ViewModel`을 연결하고 `UiState`를 수집하여 화면에 표시하도록 수정했습니다. - `WalkCompleteScreen`에 산책 기록(거리, 시간, 걸음 수)을 표시하는 `WalkRecordItem` 컴포넌트를 추가했습니다. - "후기 작성하기" 버튼(`DokiButton`)을 추가했습니다. - 그림자 효과를 `shadow`에서 `dropShadow`로 변경하여 UI를 개선했습니다. - `ViewModel`의 상태를 반영하여 프리뷰 코드를 업데이트했습니다. --- .../walkcourse/walkcomplete/WalkComplete.kt | 90 +++++++++++++++---- gradle/libs.versions.toml | 2 +- 2 files changed, 76 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkcomplete/WalkComplete.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkcomplete/WalkComplete.kt index 0033ac19..7a0594f4 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkcomplete/WalkComplete.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkcomplete/WalkComplete.kt @@ -1,6 +1,7 @@ package com.paw.key.presentation.ui.course.walkcourse.walkcomplete import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row @@ -14,51 +15,72 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape 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.draw.clip +import androidx.compose.ui.draw.dropShadow import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.shadow.Shadow import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil.compose.AsyncImage +import com.paw.key.R +import com.paw.key.core.designsystem.component.DokiButton import com.paw.key.core.designsystem.component.TopBar import com.paw.key.core.designsystem.theme.PawKeyTheme +import com.paw.key.presentation.ui.course.walkcourse.component.WalkRecordItem +import com.paw.key.presentation.ui.course.walkcourse.model.WalkInfoState +import com.paw.key.presentation.ui.course.walkcourse.walkcomplete.state.WalkCompleteState // Todo : 나중에 서버에서 줌 @Composable fun WalkCompleteRoute( - paddingValues: PaddingValues + paddingValues: PaddingValues, + viewModel: WalkCompleteViewModel = hiltViewModel() ) { + val state by viewModel.state.collectAsStateWithLifecycle() + WalkCompleteScreen( - paddingValues = paddingValues + paddingValues = paddingValues, + state = state ) } @Composable private fun WalkCompleteScreen( - paddingValues: PaddingValues + paddingValues: PaddingValues, + state: WalkCompleteState, ) { Column ( modifier = Modifier .fillMaxSize() - .background(color = PawKeyTheme.colors.background) .padding(paddingValues) + .background(color = PawKeyTheme.colors.background) ) { TopBar( title = "산책 완료", isBackVisible = false ) - Spacer(modifier = Modifier.height(20.dp)) + Spacer(modifier = Modifier.height(22.dp)) Column ( modifier = Modifier - .fillMaxSize() - .clip(RoundedCornerShape(16.dp)) - .shadow( - elevation = 10.dp, - spotColor = PawKeyTheme.colors.defaultMiddle, + .weight(1f) + .padding(horizontal = 16.dp) + .dropShadow( + shape = RoundedCornerShape(16.dp), + shadow = Shadow( + radius = 4f.dp, + color = Color(0xff000000).copy(alpha = 0.25f), + ) ) + .background(Color.White, RoundedCornerShape(16.dp)) ) { Row ( modifier = Modifier @@ -66,7 +88,8 @@ private fun WalkCompleteScreen( .padding(16.dp), verticalAlignment = Alignment.CenterVertically ) { - AsyncImage( + // 프로필사진 + /*AsyncImage( model = "", contentDescription = null, modifier = Modifier @@ -74,7 +97,7 @@ private fun WalkCompleteScreen( .background( color = PawKeyTheme.colors.defaultMiddle ) - ) + )*/ Spacer(modifier = Modifier.width(10.dp)) @@ -93,9 +116,39 @@ private fun WalkCompleteScreen( } } - + // 지도 사진 + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 32.dp, start = 16.dp, end = 16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceAround + ) { + WalkRecordItem( + recordTitle = R.string.course_record_distance, + recordContent = state.walkInfo.distanceMeters.toString() + ) + WalkRecordItem( + recordTitle = R.string.course_record_time, + recordContent = state.walkInfo.timeMillis.toString() + ) + WalkRecordItem( + recordTitle = R.string.course_record_step, + recordContent = state.walkInfo.stepCount.toString() + ) + } } + + Spacer(modifier = Modifier.height(43.dp)) + + DokiButton( + text = "후기 작성하기", + enabled = true, + onClick = {}, + modifier = Modifier + .padding(horizontal = 16.dp) + ) } } @@ -104,7 +157,14 @@ private fun WalkCompleteScreen( private fun WalkCompletePreview() { PawKeyTheme { WalkCompleteScreen( - paddingValues = PaddingValues() + paddingValues = PaddingValues(), + state = WalkCompleteState( + walkInfo = WalkInfoState( + distanceMeters = 1000f, + timeMillis = 1000L, + stepCount = 1 + ) + ) ) } -} \ No newline at end of file +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1aa8c176..6fc3e5fb 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -23,7 +23,7 @@ espressoCore = "3.6.1" coreKtx = "1.15.0" lifecycleRuntimeKtx = "2.9.0" activityCompose = "1.10.1" -composeBom = "2024.12.01" +composeBom = "2026.01.01" androidxAppCompat = "1.7.0" navigation = "2.8.5" security = "1.1.0-alpha06" From 5f48a730e9120465275964f2c71452d864dc8f71 Mon Sep 17 00:00:00 2001 From: minseong-PC Date: Wed, 4 Feb 2026 17:50:11 +0900 Subject: [PATCH 16/47] =?UTF-8?q?feat/#154:=20=EC=82=B0=EC=B1=85=20?= =?UTF-8?q?=EC=A2=85=EB=A3=8C=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 산책 일시정지 상태와 완전히 종료하는 상태를 구분하기 위해 `isStopTracking` 상태를 `WalkCourseState`에 추가했습니다. - 사용자가 '산책 종료하기' 버튼을 누르면, `isStopTracking` 상태가 `true`로 변경되고 확인 다이얼로그가 표시됩니다. - 산책 준비 화면의 애견용품 체크박스 UI 스타일을 수정했습니다. - 산책 종료 후 리뷰 화면으로 이동하는 `NavigateReview` সাই드 이펙트를 추가했습니다. - 산책 종료 로직을 처리할 `WalkCompleteViewModel`을 추가했습니다. --- .../ui/course/walkcourse/WalkCourseScreen.kt | 22 ++++++------- .../walkcourse/state/WalkCourseContract.kt | 13 +++++--- .../viewmodel/WalkCourseViewModel.kt | 14 ++++++-- .../walkcomplete/WalkCompleteViewModel.kt | 18 +++++++++++ .../walkprepare/component/WalkPrepareBody.kt | 32 +++++++++---------- 5 files changed, 61 insertions(+), 38 deletions(-) create mode 100644 app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkcomplete/WalkCompleteViewModel.kt diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/WalkCourseScreen.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/WalkCourseScreen.kt index 2b91b429..880b4f55 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/WalkCourseScreen.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/WalkCourseScreen.kt @@ -130,9 +130,10 @@ fun WalkCourseRoute( is WalkCourseSideEffect.ShowToastMessage -> { Toast.makeText(context, sideEffect.message, Toast.LENGTH_SHORT).show() } - else -> { - } + WalkCourseSideEffect.NavigateReview -> navigateReview() + + else -> {} } } } @@ -224,12 +225,7 @@ fun WalkCourseRoute( viewModel.startTracking() }, onStopTracking = { // 종료 후 넘어가기 -> 서버 전송 후 완료 뷰로 넘어가기 - /*scope.launch { - viewModel.postWalkCourseData(userId = userId.first()) - }*/ - //viewModel.onStopTrackingEvent() - //navigateToWalkComplete() - navigateReview() + viewModel.stopTracking() }, onCaptured = { bitmap -> // Todo : bitmap 안쓸거임 @@ -394,7 +390,7 @@ fun WalkCourseScreen( } } - if (!isRecording) { + if (!isRecording || state.isStopTracking) { Box( modifier = Modifier .fillMaxSize() @@ -410,7 +406,7 @@ fun WalkCourseScreen( verticalArrangement = Arrangement.Center ) { Text( - text = "산책이 중단되었어요!", + text = if (state.isStopTracking) "산책을 종료하시겠어요?" else "산책이 중단되었어요", textAlign = TextAlign.Center, style = PawKeyTheme.typography.header2, color = PawKeyTheme.colors.background @@ -419,7 +415,7 @@ fun WalkCourseScreen( Spacer(modifier = Modifier.height(4.dp)) Text( - text = "산책을 정말 종료하시겠어요?", + text = if (state.isStopTracking) "종료 후에는 산책 기록을 이어갈 수 없어요!" else "정비 후에 다시 산책을 시작해보세요!", // stop 버튼 클릭시 , pause 버튼 클릭 시 textAlign = TextAlign.Center, style = PawKeyTheme.typography.subTitle, color = PawKeyTheme.colors.background @@ -437,7 +433,7 @@ fun WalkCourseScreen( .navigationBarsPadding() ) { DokiBorderButton( - text = "산책 재개하기", + text = if (state.isStopTracking) "아니오" else "이어서 하기", enabled = true, onClick = onStartTracking, modifier = Modifier @@ -448,7 +444,7 @@ fun WalkCourseScreen( Spacer(modifier = Modifier.width(16.dp)) DokiButton( - text = "산책 종료하기", + text = if (state.isStopTracking) "예" else "산책 종료하기", enabled = true, onClick = onStopTracking, modifier = Modifier.weight(1f) diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/state/WalkCourseContract.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/state/WalkCourseContract.kt index 24b1c8dc..7cb71b72 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/state/WalkCourseContract.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/state/WalkCourseContract.kt @@ -15,6 +15,7 @@ data class WalkCourseState( val mapState: MapState = MapState(), val stepCounterState: StepCounterState = StepCounterState(), val totalTimeMillis: Long = 0L, + val isStopTracking: Boolean = false // true는 stop됨, false는 다시 시작 ) { val formattedTime: String get() = formatTime(this.totalTimeMillis) @@ -23,11 +24,13 @@ data class WalkCourseState( get() = formatDistance(this.mapState.totalDistance) } -sealed class WalkCourseSideEffect { - data class ShowSnackBar(val message: String) : WalkCourseSideEffect() - data class ShowToastMessage(val message: String) : WalkCourseSideEffect() - data object NavigateUp: WalkCourseSideEffect() - data class NavigateNext(val regionId: Int): WalkCourseSideEffect() +sealed interface WalkCourseSideEffect { + data class ShowSnackBar(val message: String) : WalkCourseSideEffect + data class ShowToastMessage(val message: String) : WalkCourseSideEffect + data object NavigateUp: WalkCourseSideEffect + data class NavigateNext(val regionId: Int): WalkCourseSideEffect + + data object NavigateReview: WalkCourseSideEffect } sealed class WalkCourseRecord ( diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/viewmodel/WalkCourseViewModel.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/viewmodel/WalkCourseViewModel.kt index fe334091..2b19b9bc 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/viewmodel/WalkCourseViewModel.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/viewmodel/WalkCourseViewModel.kt @@ -203,9 +203,10 @@ class WalkCourseViewModel @Inject constructor( } } - fun onStopTrackingEvent() { + // Todo: 서버 내용 확인하고 넘기기 + fun stopTracking() { viewModelScope.launch { - val currentWalkState = _state.value + /*val currentWalkState = _state.value try { walkSharedResultRepository.saveResult( @@ -216,8 +217,15 @@ class WalkCourseViewModel @Inject constructor( points = currentWalkState.mapState.poiPoints.toList() ) _sideEffect.emit(WalkCourseSideEffect.ShowSnackBar("산책 기록이 성공적으로 저장되었습니다.")) + _sideEffect.emit(WalkCourseSideEffect.NavigateReview) } catch (e: Exception) { _sideEffect.emit(WalkCourseSideEffect.ShowSnackBar("산책 기록 저장 실패: ${e.localizedMessage}")) + }*/ + + _state.update { + it.copy( + isStopTracking = true + ) } } } @@ -275,4 +283,4 @@ class WalkCourseViewModel @Inject constructor( companion object { private const val LOCATION_ACCURACY_THRESHOLD = 25f } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkcomplete/WalkCompleteViewModel.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkcomplete/WalkCompleteViewModel.kt new file mode 100644 index 00000000..2a4c6876 --- /dev/null +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkcomplete/WalkCompleteViewModel.kt @@ -0,0 +1,18 @@ +package com.paw.key.presentation.ui.course.walkcourse.walkcomplete + +import androidx.lifecycle.ViewModel +import com.paw.key.presentation.ui.course.walkcourse.walkcomplete.state.WalkCompleteState +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import javax.inject.Inject + +@HiltViewModel +class WalkCompleteViewModel @Inject constructor( +) : ViewModel() { + private val _state = MutableStateFlow(WalkCompleteState()) + val state: StateFlow = _state.asStateFlow() + + +} diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkprepare/component/WalkPrepareBody.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkprepare/component/WalkPrepareBody.kt index 1729a43b..3838f977 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkprepare/component/WalkPrepareBody.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkprepare/component/WalkPrepareBody.kt @@ -11,17 +11,11 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width -import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Check -import androidx.compose.material3.Checkbox -import androidx.compose.material3.CheckboxDefaults import androidx.compose.material3.Icon import androidx.compose.material3.Text -import androidx.compose.material3.TriStateCheckbox import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -119,14 +113,6 @@ private fun WalkPrepareItem( ) { Row ( modifier = modifier - .background( - color = if (isSelected) { - PawKeyTheme.colors.primary - } else { - PawKeyTheme.colors.defaultButton - }, - shape = RoundedCornerShape(8.dp) - ) .noRippleClickable { onCheckBoxClick(!isSelected) } @@ -150,6 +136,12 @@ private fun WalkPrepareItem( }, textAlign = TextAlign.Start ) + + Spacer(modifier = Modifier.weight(1f)) + + /*Icon( + imageVector = + )*/ } } @@ -162,14 +154,20 @@ private fun CustomCheckBox( val checkMarkTint = if (isSelected) { PawKeyTheme.colors.primary } else { - PawKeyTheme.colors.defaultButton + PawKeyTheme.colors.defaultMiddle + } + + val checkBackground = if (isSelected) { + PawKeyTheme.colors.primary + } else { + PawKeyTheme.colors.defaultBright } Box( modifier = modifier .size(15.dp) .background( - color = PawKeyTheme.colors.background, + color = checkBackground, shape = RoundedCornerShape(1.dp) ) .noRippleClickable { @@ -193,4 +191,4 @@ private fun WalkPrepareBodyPreview() { itemList = WalkPrepareState().dummyWalkPrepare ) } -} \ No newline at end of file +} From e7841139ab342b0980c1e3751cb6c4aaa8b0326d Mon Sep 17 00:00:00 2001 From: minseong-PC Date: Wed, 4 Feb 2026 17:50:23 +0900 Subject: [PATCH 17/47] =?UTF-8?q?mod/#154:=20=EC=9D=B4=EB=AF=B8=EC=A7=80?= =?UTF-8?q?=20=EB=AA=A8=EB=8B=AC=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20?= =?UTF-8?q?=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `ImageModal` 컴포넌트에서 사용되지 않는 import 문들을 제거했습니다. - 코드 가독성을 위해 컴포저블 내의 코드 순서를 조정했습니다. --- .../key/core/designsystem/component/ImageModal.kt | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/com/paw/key/core/designsystem/component/ImageModal.kt b/app/src/main/java/com/paw/key/core/designsystem/component/ImageModal.kt index 7f402f94..a02b96ee 100644 --- a/app/src/main/java/com/paw/key/core/designsystem/component/ImageModal.kt +++ b/app/src/main/java/com/paw/key/core/designsystem/component/ImageModal.kt @@ -1,28 +1,17 @@ package com.paw.key.core.designsystem.component -import androidx.compose.foundation.Image -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentSize -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Close -import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import coil.compose.AsyncImage - import androidx.compose.ui.window.Dialog -import coil.request.ImageRequest -import com.paw.key.R +import coil.compose.AsyncImage import com.paw.key.core.designsystem.theme.PawKeyTheme @Composable @@ -67,4 +56,4 @@ fun ImageModalPreview() { onDismiss = {} ) } -} \ No newline at end of file +} From 3b6adc0df3cf3e0df0dab7c53570dfa3c6d93dd8 Mon Sep 17 00:00:00 2001 From: minseong-PC Date: Thu, 5 Feb 2026 18:20:45 +0900 Subject: [PATCH 18/47] =?UTF-8?q?feat#154:=20=EC=82=B0=EC=B1=85=20?= =?UTF-8?q?=EC=A4=80=EB=B9=84=EB=AC=BC=20=EC=B6=94=EA=B0=80/=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `WalkPrepareViewModel`에 준비물 목록을 관리하는 로직(`addWalkItem`, `deleteWalkItem`, `updateWalkItem`)을 추가했습니다. - `WalkPrepareItemModel`의 `walkItem` 타입을 `String`에서 `TextFieldState`로 변경하여 사용자 입력을 처리하도록 수정했습니다. - "준비물 추가하기" 버튼을 누르면 새로운 준비물 항목이 목록에 추가됩니다. - 각 준비물 항목에 삭제 아이콘(`ic_cancel.xml`)을 추가하여 항목을 제거할 수 있는 기능을 구현했습니다. - `WalkPrepareScreen`에 ViewModel의 함수들(`addWalkItem`, `deleteWalkItem`)을 연결했습니다. - `BasicTextField`를 사용하여 준비물 내용을 직접 수정할 수 있도록 하고, 내용이 없을 경우 "준비물을 작성해주세요"라는 플레이스홀더를 표시하도록 개선했습니다. - 준비물 목록 사이에 `HorizontalDivider`를 추가하여 UI 가독성을 높였습니다. --- .../walkprepare/WalkPrepareScreen.kt | 16 ++-- .../walkprepare/WalkPrepareViewModel.kt | 32 ++++++- .../walkprepare/component/WalkPrepareBody.kt | 86 +++++++++++++------ .../walkprepare/model/WalkPrepareItemModel.kt | 4 +- .../walkprepare/state/WalkPrepareContract.kt | 14 +-- app/src/main/res/drawable/ic_cancel.xml | 12 +++ 6 files changed, 123 insertions(+), 41 deletions(-) create mode 100644 app/src/main/res/drawable/ic_cancel.xml diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkprepare/WalkPrepareScreen.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkprepare/WalkPrepareScreen.kt index 7e6b36df..ed610a92 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkprepare/WalkPrepareScreen.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkprepare/WalkPrepareScreen.kt @@ -33,7 +33,9 @@ fun WalkPrepareRoute( WalkPrepareScreen( paddingValues = paddingValues, state = state, - navigateWalkCourse = navigateWalkCourse + navigateWalkCourse = navigateWalkCourse, + addWalkItem = viewModel::addWalkItem, + deleteWalkItem = viewModel::deleteWalkItem ) } @@ -41,7 +43,9 @@ fun WalkPrepareRoute( private fun WalkPrepareScreen( paddingValues: PaddingValues, state: WalkPrepareState, - navigateWalkCourse: () -> Unit = {} + navigateWalkCourse: () -> Unit = {}, + addWalkItem : () -> Unit = {}, + deleteWalkItem : (Int) -> Unit = {} ) { Column ( modifier = Modifier @@ -68,9 +72,11 @@ private fun WalkPrepareScreen( Spacer(modifier = Modifier.height(16.dp)) WalkPrepareBody( - itemList = state.dummyWalkPrepare, + itemList = state.walkPrepareItemList, modifier = Modifier - .padding(horizontal = 16.dp) + .padding(horizontal = 16.dp), + addWalkItem = addWalkItem, + deleteWalkItem = deleteWalkItem ) Spacer(modifier = Modifier.height(16.dp)) @@ -94,4 +100,4 @@ private fun WalkPrepareScreenPreview() { state = WalkPrepareState() ) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkprepare/WalkPrepareViewModel.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkprepare/WalkPrepareViewModel.kt index 47d7df65..20ea84be 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkprepare/WalkPrepareViewModel.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkprepare/WalkPrepareViewModel.kt @@ -1,10 +1,14 @@ package com.paw.key.presentation.ui.course.walkcourse.walkprepare +import androidx.compose.foundation.text.input.TextFieldState import androidx.lifecycle.ViewModel +import com.paw.key.presentation.ui.course.walkcourse.walkprepare.model.WalkPrepareItemModel import com.paw.key.presentation.ui.course.walkcourse.walkprepare.state.WalkPrepareState import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update import javax.inject.Inject @HiltViewModel @@ -14,5 +18,31 @@ class WalkPrepareViewModel @Inject constructor( private val _state = MutableStateFlow(WalkPrepareState()) val state = _state.asStateFlow() + fun addWalkItem() { + val currentList = _state.value.walkPrepareItemList + val newId = (currentList.maxOfOrNull { it.id } ?: 0) + 1 -} \ No newline at end of file + val newItem = WalkPrepareItemModel(id = newId, walkItem = TextFieldState("")) + + _state.update { + it.copy(walkPrepareItemList = it.walkPrepareItemList.add(newItem)) + } + } + + fun deleteWalkItem(id : Int) { + val currentList = _state.value.walkPrepareItemList + + _state.update { + it.copy(walkPrepareItemList = currentList.removeAt(id)) + } + } + + fun updateWalkItem(id: Int, newText: String) { + _state.update { state -> + val newList = state.walkPrepareItemList.map { item -> + if (item.id == id) item.copy(walkItem = TextFieldState(newText)) else item + }.toPersistentList() + state.copy(walkPrepareItemList = newList) + } + } +} diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkprepare/component/WalkPrepareBody.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkprepare/component/WalkPrepareBody.kt index 3838f977..efc8303c 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkprepare/component/WalkPrepareBody.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkprepare/component/WalkPrepareBody.kt @@ -1,7 +1,6 @@ package com.paw.key.presentation.ui.course.walkcourse.walkprepare.component import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -14,6 +13,8 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -24,6 +25,7 @@ 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.graphics.vector.ImageVector import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.style.TextAlign @@ -40,6 +42,8 @@ import kotlinx.collections.immutable.persistentListOf @Composable fun WalkPrepareBody( modifier: Modifier = Modifier, + addWalkItem : () -> Unit = {}, + deleteWalkItem : (Int) -> Unit = {}, itemList : ImmutableList = persistentListOf(), ) { var selectedIds by remember { mutableStateOf(setOf()) } @@ -60,24 +64,32 @@ fun WalkPrepareBody( Spacer(modifier = Modifier.height(16.dp)) - LazyColumn ( - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { + LazyColumn { itemsIndexed( items = itemList, key = { _, item -> item.id } ) { index, item -> - WalkPrepareItem( - itemModel = item, - isSelected = selectedIds.contains(item.id), - onCheckBoxClick = { isSelectedNew -> - selectedIds = if (isSelectedNew) { - selectedIds + item.id - } else { - selectedIds - item.id - } - }, - ) + Column { + WalkPrepareItem( + itemModel = item, + isSelected = selectedIds.contains(item.id), + onCheckBoxClick = { isSelectedNew -> + selectedIds = if (isSelectedNew) { + selectedIds + item.id + } else { + selectedIds - item.id + } + }, + deleteWalkItem = deleteWalkItem + ) + + if (index < itemList.lastIndex) { + HorizontalDivider( + thickness = 1.dp, + color = PawKeyTheme.colors.defaultBright + ) + } + } } } @@ -86,6 +98,7 @@ fun WalkPrepareBody( Box( modifier = Modifier .fillMaxWidth() + .noRippleClickable(onClick = addWalkItem) .background( color = PawKeyTheme.colors.primaryGra1, shape = RoundedCornerShape(8.dp) @@ -108,6 +121,7 @@ fun WalkPrepareBody( private fun WalkPrepareItem( isSelected : Boolean, onCheckBoxClick: (Boolean) -> Unit, + deleteWalkItem : (Int) -> Unit, itemModel: WalkPrepareItemModel, modifier: Modifier = Modifier ) { @@ -116,7 +130,7 @@ private fun WalkPrepareItem( .noRippleClickable { onCheckBoxClick(!isSelected) } - .padding(8.dp), + .padding(10.dp), verticalAlignment = Alignment.CenterVertically ) { CustomCheckBox( @@ -126,22 +140,40 @@ private fun WalkPrepareItem( Spacer(modifier = Modifier.width(8.dp)) - Text( - text = itemModel.walkItem, - style = PawKeyTheme.typography.subButtonDefault, - color = if (isSelected) { - PawKeyTheme.colors.background - } else { - PawKeyTheme.colors.defaultMiddle + BasicTextField( + state = itemModel.walkItem, + textStyle = PawKeyTheme.typography.subButtonDefault.copy( + color = if (isSelected) PawKeyTheme.colors.background else PawKeyTheme.colors.defaultMiddle + ), + decorator = { innerTextField -> + Box( + contentAlignment = Alignment.CenterStart, + modifier = Modifier.fillMaxWidth() + ) { + if (itemModel.walkItem.text.isEmpty()) { + Text( + text = "준비물을 작성해주세요", + style = PawKeyTheme.typography.subButtonDefault, + color = PawKeyTheme.colors.defaultMiddle + ) + } + innerTextField() + } }, - textAlign = TextAlign.Start + modifier = Modifier.weight(1f) ) Spacer(modifier = Modifier.weight(1f)) - /*Icon( - imageVector = - )*/ + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_cancel), + contentDescription = null, + tint = Color.Unspecified, + modifier = Modifier + .noRippleClickable(onClick = { + deleteWalkItem(itemModel.id) + }) + ) } } diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkprepare/model/WalkPrepareItemModel.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkprepare/model/WalkPrepareItemModel.kt index 871259e8..4f41bcec 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkprepare/model/WalkPrepareItemModel.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkprepare/model/WalkPrepareItemModel.kt @@ -1,8 +1,8 @@ package com.paw.key.presentation.ui.course.walkcourse.walkprepare.model -import okhttp3.internal.toImmutableList +import androidx.compose.foundation.text.input.TextFieldState data class WalkPrepareItemModel( val id : Int = 0, - val walkItem: String = "" + val walkItem: TextFieldState = TextFieldState() ) diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkprepare/state/WalkPrepareContract.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkprepare/state/WalkPrepareContract.kt index f47e7b1e..797c204d 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkprepare/state/WalkPrepareContract.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkprepare/state/WalkPrepareContract.kt @@ -1,17 +1,19 @@ package com.paw.key.presentation.ui.course.walkcourse.walkprepare.state +import androidx.compose.foundation.text.input.TextFieldState import com.paw.key.presentation.ui.course.walkcourse.walkprepare.model.WalkPrepareItemModel import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList data class WalkPrepareState( - val walkPrepareItemList: ImmutableList = persistentListOf(), + val walkPrepareItemList: PersistentList = persistentListOf(), ) { val dummyWalkPrepare = listOf( - WalkPrepareItemModel(1, "배변봉투"), - WalkPrepareItemModel(2, "리드줄"), - WalkPrepareItemModel(3, "물"), - WalkPrepareItemModel(4, "간식"), + WalkPrepareItemModel(1, TextFieldState("")), + WalkPrepareItemModel(2, TextFieldState("리드줄")), + WalkPrepareItemModel(3, TextFieldState("물")), + WalkPrepareItemModel(4, TextFieldState("간식")), ).toImmutableList() -} \ No newline at end of file +} diff --git a/app/src/main/res/drawable/ic_cancel.xml b/app/src/main/res/drawable/ic_cancel.xml new file mode 100644 index 00000000..908147d2 --- /dev/null +++ b/app/src/main/res/drawable/ic_cancel.xml @@ -0,0 +1,12 @@ + + + From 03413b13784ddbc11018f2ed898da182261c09c6 Mon Sep 17 00:00:00 2001 From: sonms Date: Mon, 9 Feb 2026 09:34:00 +0900 Subject: [PATCH 19/47] =?UTF-8?q?chore/#154=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EB=B3=80=EC=88=98=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/com/paw/key/presentation/ui/login/LoginScreen.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/com/paw/key/presentation/ui/login/LoginScreen.kt b/app/src/main/java/com/paw/key/presentation/ui/login/LoginScreen.kt index c7e409f5..c34eb3f6 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/login/LoginScreen.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/login/LoginScreen.kt @@ -57,7 +57,6 @@ fun LoginRoute( ) { val state by viewModel.state.collectAsStateWithLifecycle() val isLoginFormValid = viewModel.state.collectAsStateWithLifecycle().value.isLoginValid - val context = LocalContext.current val coroutineScope = rememberCoroutineScope() LoginScreen( From a82e151e2fa267e994ce2649b4afc6cdeee0e3de Mon Sep 17 00:00:00 2001 From: sonms Date: Mon, 9 Feb 2026 09:34:31 +0900 Subject: [PATCH 20/47] =?UTF-8?q?feat/#154=20detail=20=EB=B7=B0=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EB=B0=8F=20preview=EC=97=90=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=ED=95=A0=20=EC=88=98=20=EC=9E=88=EB=8A=94=20=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/designsystem/component/UriImage.kt | 47 ++++++++ .../presentation/ui/detail/DetailScreen.kt | 104 +++++++++++++++++- app/src/main/res/drawable/img_fake_red.xml | 33 ++++++ 3 files changed, 183 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/com/paw/key/core/designsystem/component/UriImage.kt create mode 100644 app/src/main/res/drawable/img_fake_red.xml diff --git a/app/src/main/java/com/paw/key/core/designsystem/component/UriImage.kt b/app/src/main/java/com/paw/key/core/designsystem/component/UriImage.kt new file mode 100644 index 00000000..5d9b446f --- /dev/null +++ b/app/src/main/java/com/paw/key/core/designsystem/component/UriImage.kt @@ -0,0 +1,47 @@ +package com.paw.key.core.designsystem.component + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalInspectionMode +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import com.paw.key.R + +@Composable +fun UrlImage( + url: String, + modifier: Modifier = Modifier, + contentScale: ContentScale = ContentScale.Fit, + contentDescription: String? = null, +) { + if (LocalInspectionMode.current) { + Image( + imageVector = ImageVector.vectorResource(R.drawable.img_fake_red), + contentDescription = contentDescription, + contentScale = contentScale, + modifier = modifier + ) + } else { + AsyncImage( + model = url, + contentDescription = contentDescription, + contentScale = contentScale, + modifier = modifier + ) + } +} + +@Preview +@Composable +fun UrlImagePreview() { + UrlImage( + url = "", + modifier = Modifier.size(100.dp), + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/detail/DetailScreen.kt b/app/src/main/java/com/paw/key/presentation/ui/detail/DetailScreen.kt index 5567b727..774105ef 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/detail/DetailScreen.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/detail/DetailScreen.kt @@ -1,8 +1,32 @@ package com.paw.key.presentation.ui.detail +import androidx.compose.foundation.background +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.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.dropShadow +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.shadow.Shadow import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.paw.key.core.designsystem.component.TopBar +import com.paw.key.core.designsystem.component.UrlImage import com.paw.key.core.designsystem.theme.PawKeyTheme @Composable @@ -13,12 +37,90 @@ fun DetailRoute( paddingValues = paddingValues ) } - @Composable private fun DetailScreen( paddingValues: PaddingValues, ) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + ) { + TopBar( + title = "루트 상세정보", + thickness = 2 + ) + + Box( + modifier = Modifier.fillMaxSize() + ) { + UrlImage( + url = "", + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight(0.4f) + .align(Alignment.TopCenter) + ) + + Column( + modifier = Modifier + .align(Alignment.BottomCenter) + .fillMaxWidth() + .fillMaxHeight(0.75f) + .dropShadow( + shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp), + shadow = Shadow( + radius = 19f.dp, + alpha = 0.1f, + color = Color(0xff000000), + ) + ) + .background( + color = PawKeyTheme.colors.background, + shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp) + ) + .padding(16.dp) + ) { + Text( + text = "단지와 룰루랄라 룰루랄라 룰루랄라", + style = PawKeyTheme.typography.header3, + color = PawKeyTheme.colors.contents, + modifier = Modifier.padding(bottom = 16.dp) + ) + HorizontalDivider( + thickness = 1.dp, + color = PawKeyTheme.colors.defaultButton, + modifier = Modifier.fillMaxWidth() + ) + + Row( + modifier = Modifier.padding(vertical = 16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(10.dp), + ) { + UrlImage( + url = "", + modifier = Modifier + .size(43.dp) + .clip(RoundedCornerShape(50.dp)) + ) + + Text( + text = "단지", + style = PawKeyTheme.typography.subTitle, + color = PawKeyTheme.colors.defaultDark + ) + } + + HorizontalDivider( + thickness = 1.dp, + color = PawKeyTheme.colors.defaultButton, + modifier = Modifier.fillMaxWidth() + ) + } + } + } } @Preview diff --git a/app/src/main/res/drawable/img_fake_red.xml b/app/src/main/res/drawable/img_fake_red.xml new file mode 100644 index 00000000..70143b61 --- /dev/null +++ b/app/src/main/res/drawable/img_fake_red.xml @@ -0,0 +1,33 @@ + + + + + From b7748e896761accd86256a8a3b2637ee467f7ac0 Mon Sep 17 00:00:00 2001 From: sonms Date: Mon, 9 Feb 2026 09:35:11 +0900 Subject: [PATCH 21/47] =?UTF-8?q?mod/#154=20drop=20shadow=20=EC=9D=98=20al?= =?UTF-8?q?pha=20=EA=B0=92=20=EC=82=AC=EC=9A=A9=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20=EB=B0=8F=20thickness=20=EB=B3=80=EC=88=98?= =?UTF-8?q?=20=EB=9A=AB=EC=96=B4=EB=86=93=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/paw/key/core/designsystem/component/TopBar.kt | 3 ++- .../ui/course/walkcourse/walkcomplete/WalkComplete.kt | 3 ++- .../key/presentation/ui/course/walkreview/WalkReviewScreen.kt | 1 + 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/paw/key/core/designsystem/component/TopBar.kt b/app/src/main/java/com/paw/key/core/designsystem/component/TopBar.kt index 6294d862..0799dbd0 100644 --- a/app/src/main/java/com/paw/key/core/designsystem/component/TopBar.kt +++ b/app/src/main/java/com/paw/key/core/designsystem/component/TopBar.kt @@ -26,6 +26,7 @@ fun TopBar( onBackClick: () -> Unit = {}, onClickTitle : () -> Unit = {}, isBackVisible: Boolean = true, + thickness : Int = 1 ) { Column ( modifier = modifier @@ -56,7 +57,7 @@ fun TopBar( } HorizontalDivider( - thickness = 1.dp, + thickness = thickness.dp, modifier = Modifier .fillMaxWidth() .background( diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkcomplete/WalkComplete.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkcomplete/WalkComplete.kt index 7a0594f4..dbe7885d 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkcomplete/WalkComplete.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkcomplete/WalkComplete.kt @@ -77,7 +77,8 @@ private fun WalkCompleteScreen( shape = RoundedCornerShape(16.dp), shadow = Shadow( radius = 4f.dp, - color = Color(0xff000000).copy(alpha = 0.25f), + alpha = 0.25f, + color = Color(0xff000000), ) ) .background(Color.White, RoundedCornerShape(16.dp)) diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/WalkReviewScreen.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/WalkReviewScreen.kt index 18cfb12e..e121c895 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/WalkReviewScreen.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/WalkReviewScreen.kt @@ -143,6 +143,7 @@ private fun WalkReviewScreen( TopBar( title = "산책 기록하기", isBackVisible = true, + thickness = 2, onBackClick = navigateUp ) From 6aeb3b5bf7abbbc766b0e30fcf265f6ba878492f Mon Sep 17 00:00:00 2001 From: sonms Date: Mon, 16 Feb 2026 01:45:11 +0900 Subject: [PATCH 22/47] =?UTF-8?q?feat/#154=20=EB=A3=A8=ED=8A=B8=20?= =?UTF-8?q?=EC=B6=94=EC=B2=9C=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=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 --- .../component/CommunityTopImageHolder.kt | 65 +++++++ .../ui/community/component/FilterScreen.kt | 162 ++++++++++++++++++ 2 files changed, 227 insertions(+) create mode 100644 app/src/main/java/com/paw/key/presentation/ui/community/component/CommunityTopImageHolder.kt create mode 100644 app/src/main/java/com/paw/key/presentation/ui/community/component/FilterScreen.kt diff --git a/app/src/main/java/com/paw/key/presentation/ui/community/component/CommunityTopImageHolder.kt b/app/src/main/java/com/paw/key/presentation/ui/community/component/CommunityTopImageHolder.kt new file mode 100644 index 00000000..847ceb5e --- /dev/null +++ b/app/src/main/java/com/paw/key/presentation/ui/community/component/CommunityTopImageHolder.kt @@ -0,0 +1,65 @@ +package com.paw.key.presentation.ui.community.component + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.paw.key.core.designsystem.component.PageIndicator +import com.paw.key.core.designsystem.component.UrlImage +import com.paw.key.core.designsystem.theme.PawKeyTheme +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +@Composable +fun CommunityTopImageHolder( + imageList: ImmutableList, + modifier: Modifier = Modifier +) { + val pagerState = rememberPagerState(pageCount = { imageList.size }) + + Box( + modifier = modifier + ) { + HorizontalPager( + state = pagerState, + modifier = Modifier + .fillMaxWidth() + .aspectRatio(360f/141f) + ) { page -> + UrlImage( + url = imageList[page], + modifier = Modifier.fillMaxSize(), + contentScale = ContentScale.Crop + ) + } + + PageIndicator( + numberOfPages = imageList.size, + selectedPage = pagerState.currentPage, + selectedColor = PawKeyTheme.colors.primary, + defaultColor = PawKeyTheme.colors.defaultMiddle, + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(bottom = 8.dp) + ) + } +} + +@Preview +@Composable +private fun CommunityTopImageHolderPreview() { + PawKeyTheme { + CommunityTopImageHolder( + imageList = persistentListOf("", "", "") + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/community/component/FilterScreen.kt b/app/src/main/java/com/paw/key/presentation/ui/community/component/FilterScreen.kt new file mode 100644 index 00000000..da9b487c --- /dev/null +++ b/app/src/main/java/com/paw/key/presentation/ui/community/component/FilterScreen.kt @@ -0,0 +1,162 @@ +package com.paw.key.presentation.ui.community.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.paw.key.R +import com.paw.key.core.designsystem.component.DokiButton +import com.paw.key.core.designsystem.component.TopBar +import com.paw.key.core.designsystem.theme.PawKeyTheme +import com.paw.key.presentation.ui.community.CommunityState +import com.paw.key.presentation.ui.course.walkreview.component.WalkReviewMultipleFilter +import com.paw.key.presentation.ui.course.walkreview.component.WalkReviewSingleFilter + +@Composable +fun FilterScreen( + paddingValues: PaddingValues, + state: CommunityState, + onFilterClick: (String, List, Boolean) -> Unit, + onCompleted: () -> Unit, + onBackClick: () -> Unit, + onClickSuffix: () -> Unit, + modifier: Modifier = Modifier +) { + // Todo : 서버 내용으로 변경 + Column ( + modifier = modifier + .fillMaxSize() + .background(PawKeyTheme.colors.background) + .padding(paddingValues) + ) { + TopBar( + title = "필터", + isBackVisible = true, + thickness = 2, + onBackClick = onBackClick, + onClickSuffix = onClickSuffix, + suffix = R.drawable.ic_course_list_refresh + ) + + Column ( + modifier = Modifier + .weight(1f) + .padding(horizontal = 16.dp) + .verticalScroll(rememberScrollState()) + ) { + Spacer(modifier = Modifier.height(26.dp)) + + WalkReviewSingleFilter( + title = "혼잡도", + filterList = state.communityFilterModel.confusionSingleFilterList, + selectedItem = state.getSingleFilterSelection(state.communityFilterModel.confusionSingleFilterList), + onItemSelected = { + onFilterClick( + it, + state.communityFilterModel.confusionSingleFilterList, + true + ) + } + ) + + Spacer(modifier = Modifier.height(40.dp)) + + WalkReviewSingleFilter( + title = "강아지 교류 빈도", + filterList = state.communityFilterModel.frequencySingleFilterList, + selectedItem = state.getSingleFilterSelection(state.communityFilterModel.frequencySingleFilterList), + onItemSelected = { + onFilterClick( + it, + state.communityFilterModel.frequencySingleFilterList, + true + ) + } + ) + + Spacer(modifier = Modifier.height(40.dp)) + + // Todo : 어떻게 필터값을 받을 지 몰라서 보류 + WalkReviewMultipleFilter( + title = "안전", + filterList = state.communityFilterModel.safetyMultipleFilterList, + selectedItems = state.communitySelectedFilterData, + onItemClick = { + onFilterClick( + it, + state.communityFilterModel.safetyMultipleFilterList, + false + ) + } + ) + + Spacer(modifier = Modifier.height(40.dp)) + + WalkReviewMultipleFilter( + title = "편의성", + filterList = state.communityFilterModel.comfortMultipleFilterList, + selectedItems = state.communitySelectedFilterData, + onItemClick = { + onFilterClick( + it, + state.communityFilterModel.comfortMultipleFilterList, + false + ) + } + ) + + Spacer(modifier = Modifier.height(40.dp)) + + WalkReviewMultipleFilter( + title = "환경", + filterList = state.communityFilterModel.environmentMultipleFilterList, + selectedItems = state.communitySelectedFilterData, + onItemClick = { + onFilterClick( + it, + state.communityFilterModel.environmentMultipleFilterList, + false + ) + } + ) + + Spacer(modifier = Modifier.height(24.dp)) + } + + + DokiButton( + text = "적용하기", + enabled = true, + onClick = onCompleted, + modifier = Modifier + .padding(horizontal = 16.dp) + .padding(bottom = 24.dp) + ) + } + + +} + +@Preview +@Composable +private fun FilterScreenPreview() { + PawKeyTheme { + FilterScreen( + state = CommunityState(), + onFilterClick = { _, _, _ -> }, + onCompleted = {}, + onBackClick = {}, + onClickSuffix = {}, + paddingValues = PaddingValues() + ) + } +} \ No newline at end of file From f86a2b4a9e8eaaa7718db0c26979c5f79c26f12c Mon Sep 17 00:00:00 2001 From: sonms Date: Mon, 16 Feb 2026 01:45:23 +0900 Subject: [PATCH 23/47] =?UTF-8?q?feat/#154=20=EB=A3=A8=ED=8A=B8=20?= =?UTF-8?q?=EC=B6=94=EC=B2=9C=20=EB=B7=B0=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/community/CommunityContract.kt | 39 +++ .../ui/community/CommunityScreen.kt | 284 +++++++++++++++--- .../ui/community/CommunityViewModel.kt | 43 +++ .../navigation/CommunityNavigation.kt | 3 - 4 files changed, 324 insertions(+), 45 deletions(-) create mode 100644 app/src/main/java/com/paw/key/presentation/ui/community/CommunityContract.kt create mode 100644 app/src/main/java/com/paw/key/presentation/ui/community/CommunityViewModel.kt diff --git a/app/src/main/java/com/paw/key/presentation/ui/community/CommunityContract.kt b/app/src/main/java/com/paw/key/presentation/ui/community/CommunityContract.kt new file mode 100644 index 00000000..e65a3e07 --- /dev/null +++ b/app/src/main/java/com/paw/key/presentation/ui/community/CommunityContract.kt @@ -0,0 +1,39 @@ +package com.paw.key.presentation.ui.community + +import com.paw.key.core.model.WalkingRouteUiModel +import com.paw.key.presentation.ui.community.model.SortedType +import com.paw.key.presentation.ui.course.walkreview.model.WalkReviewFilterModel +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.persistentListOf + +data class CommunityState( + val filterList: ImmutableList = persistentListOf(), + val communityRouteList: ImmutableList = persistentListOf(), + val selectedSortedType: SortedType = SortedType.LATEST, + + val communityFilterModel: WalkReviewFilterModel = WalkReviewFilterModel(), + val communitySelectedFilterData: PersistentList = persistentListOf(), +) { + fun getSingleFilterSelection(categoryList: List): String { + return communitySelectedFilterData.firstOrNull { categoryList.contains(it) }.orEmpty() + } + + fun getUpdatedFilterList( + selectedItem: String, + categoryList: List, + isSingleSelect: Boolean + ): PersistentList { + return if (isSingleSelect) { + communitySelectedFilterData + .removeAll(categoryList) + .add(selectedItem) + } else { + if (communitySelectedFilterData.contains(selectedItem)) { + communitySelectedFilterData.remove(selectedItem) + } else { + communitySelectedFilterData.add(selectedItem) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/community/CommunityScreen.kt b/app/src/main/java/com/paw/key/presentation/ui/community/CommunityScreen.kt index 060ec9f6..91715602 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/community/CommunityScreen.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/community/CommunityScreen.kt @@ -1,88 +1,288 @@ package com.paw.key.presentation.ui.community -import androidx.compose.foundation.Image +import androidx.activity.compose.BackHandler +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.height -import androidx.compose.material3.SnackbarHostState +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.paw.key.R +import com.paw.key.core.designsystem.component.TopBar +import com.paw.key.core.designsystem.component.routeitem.RouteItem import com.paw.key.core.designsystem.theme.PawKeyTheme +import com.paw.key.core.extension.noRippleClickable +import com.paw.key.core.model.WalkingRouteUiModel +import com.paw.key.presentation.ui.community.component.CommunityTopImageHolder +import com.paw.key.presentation.ui.community.component.FilterScreen +import com.paw.key.presentation.ui.community.model.SortedType +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +/** + * [루트 추천] 화면 + */ @Composable -fun CommunityRoute( +fun CommunityRoute( // 루트 추천 paddingValues: PaddingValues, - navigateUp: () -> Unit, - navigateNext: () -> Unit, - snackBarHostState: SnackbarHostState, - modifier: Modifier = Modifier, + viewModel: CommunityViewModel = hiltViewModel() ) { - CommunityScreen( - paddingValues = paddingValues, - navigateUp = navigateUp, - navigateNext = navigateNext, - snackBarHostState = snackBarHostState, - modifier = modifier, - ) + val state by viewModel.state.collectAsStateWithLifecycle() + var isFilterSheetVisible by remember { mutableStateOf(false) } + + BackHandler { + if (isFilterSheetVisible) { + isFilterSheetVisible = false + viewModel.onRefreshFilter() + } + } + + if (isFilterSheetVisible) { + FilterScreen( + paddingValues = paddingValues, + state = state, + onFilterClick = viewModel::onFilterClick, + onCompleted = { + isFilterSheetVisible = false + viewModel.postFilter() + }, + onBackClick = { isFilterSheetVisible = false }, + onClickSuffix = { + viewModel.onRefreshFilter() + } + ) + } else { + CommunityScreen( + paddingValues = paddingValues, + state = state, + onShowFilterSheet = { isFilterSheetVisible = true } + ) + } } @Composable fun CommunityScreen( paddingValues: PaddingValues, - navigateUp: () -> Unit, - navigateNext: () -> Unit, - snackBarHostState: SnackbarHostState, - modifier: Modifier = Modifier, + state: CommunityState, + onShowFilterSheet: () -> Unit = {}, ) { + // Todo: 서버 내용으로 수정 + val filterList = listOf( + "산책 소요 시간", "혼잡도", "강아지 교류 빈도", "안전") + + var selectedFilters by remember { mutableStateOf(setOf()) } + var isSortMenuExpanded by remember { mutableStateOf(false) } + Column( - modifier = modifier - .fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center + modifier = Modifier + .fillMaxSize() + .background(color = PawKeyTheme.colors.background) + .padding(paddingValues), ) { - Image( - imageVector = ImageVector.vectorResource(R.drawable.ic_community), - contentDescription = stringResource(R.string.ic_community_description) + TopBar( + title = "루트 추천", + onBackClick = {}, + isBackVisible = false, + thickness = 0 + ) + + CommunityTopImageHolder( + imageList = persistentListOf() ) - Spacer(modifier = Modifier.height(4.dp)) + Spacer(modifier = Modifier.height(17.dp)) - Column( - horizontalAlignment = Alignment.CenterHorizontally + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .padding(top = 19.dp, bottom = 8.dp) ) { - Text( - text = "커뮤니티 기능은", - style = PawKeyTheme.typography.body16Sb, - color = PawKeyTheme.colors.gray300 + Icon( + imageVector = if (selectedFilters.isNotEmpty()) { + ImageVector.vectorResource(R.drawable.ic_course_option_selected_filter) + } else { + ImageVector.vectorResource(R.drawable.ic_course_optin_filter) + }, + contentDescription = "filter", + tint = Color.Unspecified, + modifier = Modifier + .noRippleClickable(onClick = onShowFilterSheet) ) + + LazyRow ( + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + items(filterList.size) { + val filter = filterList[it] + val isSelected = selectedFilters.contains(filter) + + Box( + modifier = Modifier + .border( + width = 1.dp, + color = if (isSelected) { + PawKeyTheme.colors.primary + } else { + PawKeyTheme.colors.defaultButton + }, + shape = RoundedCornerShape(8.dp) + ) + .background( + color = if (isSelected) { + PawKeyTheme.colors.opacity5Primary + } else { + PawKeyTheme.colors.background + }, + shape = RoundedCornerShape(8.dp) + ) + .padding(horizontal = 10.dp, vertical = 9.dp) + ) { + Text( + text = filterList[it], + style = PawKeyTheme.typography.subButtonDefault, + color = if (isSelected) { + PawKeyTheme.colors.primary + } else { + PawKeyTheme.colors.defaultMiddle + } + ) + } + } + } + } + + Row ( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .padding(horizontal = 16.dp, vertical = 8.dp) + ) { Text( - text = "아직 준비중이에요", - style = PawKeyTheme.typography.body16Sb, - color = PawKeyTheme.colors.gray300 + text = "${state.communityRouteList.size}개의 루트", + style = PawKeyTheme.typography.subButtonDefault, + color = PawKeyTheme.colors.defaultDark ) + + Spacer(modifier = Modifier.weight(1f)) + + Box { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.noRippleClickable { + isSortMenuExpanded = true + } + ) { + Text( + text = state.selectedSortedType.label, + style = PawKeyTheme.typography.subButtonDefault, + color = PawKeyTheme.colors.defaultDark + ) + + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_arrow_down), + contentDescription = "sort", + tint = Color.Unspecified + ) + } + + DropdownMenu( + expanded = isSortMenuExpanded, + onDismissRequest = { isSortMenuExpanded = false }, + modifier = Modifier + .background(PawKeyTheme.colors.background) + ) { + SortedType.entries.forEach { option -> + DropdownMenuItem( + modifier = Modifier + .clip(RoundedCornerShape(8.dp)) + .background(if (option == state.selectedSortedType) { + PawKeyTheme.colors.opacity5Primary + } else { + PawKeyTheme.colors.background + }), + text = { + Text( + text = option.label, + style = PawKeyTheme.typography.subButtonDefault, + color = if (option == state.selectedSortedType) { + PawKeyTheme.colors.primary + } else { + PawKeyTheme.colors.defaultMiddle + } + ) + }, + onClick = { + // Todo: 정렬 타입 변경 로직 + isSortMenuExpanded = false + } + ) + } + } + } + } + + LazyVerticalGrid ( + columns = GridCells.Fixed(2), + modifier = Modifier.weight(1f), + contentPadding = PaddingValues(horizontal = 16.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + items(state.communityRouteList.size) { + RouteItem( + routeTitle = state.communityRouteList[it].title, + routeTime = state.communityRouteList[it].time, + routeDate = state.communityRouteList[it].date, + location = state.communityRouteList[it].location, + routeImage = state.communityRouteList[it].imageUri, + onClickHeart = {}, + onClick = {} + ) + } } } } + @Preview @Composable private fun CommunityScreenPreview() { - CommunityScreen( - paddingValues = PaddingValues(), - navigateUp = {}, - navigateNext = {}, - snackBarHostState = SnackbarHostState(), - ) + PawKeyTheme { + CommunityScreen( + paddingValues = PaddingValues(), + state = CommunityState(communityRouteList = WalkingRouteUiModel.Fake.toImmutableList()) + ) + } } \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/community/CommunityViewModel.kt b/app/src/main/java/com/paw/key/presentation/ui/community/CommunityViewModel.kt new file mode 100644 index 00000000..04606734 --- /dev/null +++ b/app/src/main/java/com/paw/key/presentation/ui/community/CommunityViewModel.kt @@ -0,0 +1,43 @@ +package com.paw.key.presentation.ui.community + +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import javax.inject.Inject + +@HiltViewModel +class CommunityViewModel @Inject constructor( + +) : ViewModel() { + private val _state = MutableStateFlow(CommunityState()) + val state = _state.asStateFlow() + + fun onFilterClick( + item: String, + categoryList: List, + isSingle: Boolean + ) { + val newFilterList = _state.value.getUpdatedFilterList(item, categoryList, isSingle) + + _state.update { + it.copy( + communitySelectedFilterData = newFilterList + ) + } + } + + fun onRefreshFilter() { + _state.update { + it.copy( + communitySelectedFilterData = persistentListOf() + ) + } + } + + fun postFilter() { + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/community/navigation/CommunityNavigation.kt b/app/src/main/java/com/paw/key/presentation/ui/community/navigation/CommunityNavigation.kt index e0a1522a..d325a49b 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/community/navigation/CommunityNavigation.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/community/navigation/CommunityNavigation.kt @@ -25,9 +25,6 @@ fun NavGraphBuilder.communityNavGraph( composable { CommunityRoute( paddingValues = paddingValues, - navigateUp = navigateUp, - navigateNext = navigateNext, - snackBarHostState = snackBarHostState, ) } } From a4a5d1eed0a690dcff8873d9f8fcadd56dc87260 Mon Sep 17 00:00:00 2001 From: sonms Date: Mon, 16 Feb 2026 01:45:47 +0900 Subject: [PATCH 24/47] =?UTF-8?q?feat/#154=20=EC=83=81=EC=84=B8=20?= =?UTF-8?q?=EB=B3=B4=EA=B8=B0=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=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 --- .../ui/detail/component/DetailImageHolder.kt | 55 +++++++ .../ui/detail/component/DetailTopReview.kt | 141 ++++++++++++++++++ 2 files changed, 196 insertions(+) create mode 100644 app/src/main/java/com/paw/key/presentation/ui/detail/component/DetailImageHolder.kt create mode 100644 app/src/main/java/com/paw/key/presentation/ui/detail/component/DetailTopReview.kt diff --git a/app/src/main/java/com/paw/key/presentation/ui/detail/component/DetailImageHolder.kt b/app/src/main/java/com/paw/key/presentation/ui/detail/component/DetailImageHolder.kt new file mode 100644 index 00000000..0472c8d1 --- /dev/null +++ b/app/src/main/java/com/paw/key/presentation/ui/detail/component/DetailImageHolder.kt @@ -0,0 +1,55 @@ +package com.paw.key.presentation.ui.detail.component + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.paw.key.core.designsystem.component.UrlImage +import com.paw.key.core.designsystem.theme.PawKeyTheme +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +@Composable +fun DetailImageHolder( + imageUrls: ImmutableList, + modifier: Modifier = Modifier +) { + BoxWithConstraints(modifier = modifier.fillMaxWidth()) { + val imageWidth = maxWidth * 0.35f + + LazyRow( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(6.dp) + ) { + items(imageUrls.size) { + UrlImage( + url = imageUrls[it], + contentScale = ContentScale.Crop, + modifier = Modifier + .size(imageWidth) + .aspectRatio(3f / 3f) + .clip(RoundedCornerShape(4.dp)) + ) + } + } + } +} + +@Preview +@Composable +private fun DetailImageHolderPreview() { + PawKeyTheme { + DetailImageHolder( + imageUrls = persistentListOf("", "", "") + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/detail/component/DetailTopReview.kt b/app/src/main/java/com/paw/key/presentation/ui/detail/component/DetailTopReview.kt new file mode 100644 index 00000000..656869cd --- /dev/null +++ b/app/src/main/java/com/paw/key/presentation/ui/detail/component/DetailTopReview.kt @@ -0,0 +1,141 @@ +package com.paw.key.presentation.ui.detail.component + +import androidx.annotation.ColorRes +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.paw.key.R +import com.paw.key.core.designsystem.theme.PawKeyTheme + +@Composable +fun DetailTopReview( + reviewCount: Int, + modifier: Modifier = Modifier, + isShared: Boolean = false, // 공유 여부 +) { + val emptyReviewText = if (isShared) "아직은 후기가 없어요." else "현재는 비공개 상태에요.\n" + + "공개로 전환해 산책 루트를 공유해보세요." + + Column( + modifier = modifier + ) { + Row ( + modifier = Modifier + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "이런 점이 좋았어요", + style = PawKeyTheme.typography.mainButtonActive, + color = PawKeyTheme.colors.contents + ) + + Spacer(modifier = Modifier.width(12.dp)) + + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_edit), + tint = Color.Unspecified, + contentDescription = null + ) + + Spacer(modifier = Modifier.width(4.dp)) + + Text( + text = "$reviewCount", + style = PawKeyTheme.typography.bodySmall, + color = PawKeyTheme.colors.defaultMiddle + ) + } + + // Todo: 서버 기준으로 변경 + if (reviewCount == 0) { + Text( + text = emptyReviewText, + style = PawKeyTheme.typography.subTitle, + color = PawKeyTheme.colors.defaultMiddle, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 24.dp) + ) + } else { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + PercentageItem( + text = "~가 좋아요", + widthPercent = 1f, + backgroundColor = PawKeyTheme.colors.primaryGra5 + ) + + PercentageItem( + text = "후기 업선", + widthPercent = 0.75f, + backgroundColor = PawKeyTheme.colors.primaryGra2 + ) + + PercentageItem( + text = "후기 업선", + widthPercent = 0.55f, + backgroundColor = PawKeyTheme.colors.primaryGra2 + ) + } + } + } +} + +@Composable +private fun PercentageItem( + text: String, + widthPercent: Float, + @ColorRes backgroundColor: Color +) { + Box( + modifier = Modifier + .fillMaxWidth(widthPercent) + .background( + color = backgroundColor, + shape = RoundedCornerShape(6.dp) + ) + .padding(vertical = 11.dp, horizontal = 14.dp), + contentAlignment = Alignment.CenterStart + ) { + Text( + text = text, + style = PawKeyTheme.typography.subButtonActive, + color = PawKeyTheme.colors.contents, + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Start + ) + } +} + +@Preview +@Composable +private fun DetailTopReviewPreview() { + PawKeyTheme { + DetailTopReview( + reviewCount = 30 + ) + } +} \ No newline at end of file From 062d08eff4087637069754e3ea51fb361369778a Mon Sep 17 00:00:00 2001 From: sonms Date: Mon, 16 Feb 2026 01:45:56 +0900 Subject: [PATCH 25/47] =?UTF-8?q?feat/#154=20=EC=83=81=EC=84=B8=20?= =?UTF-8?q?=EB=B3=B4=EA=B8=B0=20=EB=B7=B0=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/ui/detail/DetailContract.kt | 9 + .../presentation/ui/detail/DetailScreen.kt | 198 +++++++++++++++++- .../presentation/ui/detail/DetailViewModel.kt | 15 ++ .../ui/detail/navigation/DetailNavigation.kt | 29 +++ 4 files changed, 242 insertions(+), 9 deletions(-) create mode 100644 app/src/main/java/com/paw/key/presentation/ui/detail/DetailContract.kt create mode 100644 app/src/main/java/com/paw/key/presentation/ui/detail/DetailViewModel.kt create mode 100644 app/src/main/java/com/paw/key/presentation/ui/detail/navigation/DetailNavigation.kt diff --git a/app/src/main/java/com/paw/key/presentation/ui/detail/DetailContract.kt b/app/src/main/java/com/paw/key/presentation/ui/detail/DetailContract.kt new file mode 100644 index 00000000..43e1b3d7 --- /dev/null +++ b/app/src/main/java/com/paw/key/presentation/ui/detail/DetailContract.kt @@ -0,0 +1,9 @@ +package com.paw.key.presentation.ui.detail + +data class DetailState( + val isMine: Boolean = false +) + +sealed interface DetailSideEffect { + +} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/detail/DetailScreen.kt b/app/src/main/java/com/paw/key/presentation/ui/detail/DetailScreen.kt index 774105ef..b23613ab 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/detail/DetailScreen.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/detail/DetailScreen.kt @@ -1,46 +1,84 @@ package com.paw.key.presentation.ui.detail +import androidx.compose.animation.animateContentSize import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.dropShadow import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.shadow.Shadow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.paw.key.R +import com.paw.key.core.designsystem.component.DokiButton +import com.paw.key.core.designsystem.component.SubChip import com.paw.key.core.designsystem.component.TopBar import com.paw.key.core.designsystem.component.UrlImage +import com.paw.key.core.designsystem.component.walk.WalkReviewInfoHolder import com.paw.key.core.designsystem.theme.PawKeyTheme +import com.paw.key.presentation.ui.detail.component.DetailImageHolder +import com.paw.key.presentation.ui.detail.component.DetailTopReview +import com.paw.key.presentation.ui.detail.component.DokiDeleteButton +import com.paw.key.presentation.ui.detail.component.FilterChipDivider +import kotlinx.collections.immutable.persistentListOf @Composable fun DetailRoute( paddingValues: PaddingValues, + viewModel: DetailViewModel = hiltViewModel() ) { + val state by viewModel.state.collectAsStateWithLifecycle() + DetailScreen( - paddingValues = paddingValues + paddingValues = paddingValues, + state = state ) } @Composable private fun DetailScreen( paddingValues: PaddingValues, + state: DetailState ) { + var isExpanded by remember { mutableStateOf(false) } + + // Todo : 서버 내용으로 변경 + val allItems = listOf( + "중요도 낮음", "교육행정", "보도/자료 분석", "보도 일정", + "키보드/자판자 공지", "의전 방문", "행사", "비밀 방문 쓰여기둥", + "현미경", "번역본 용어 번역", "간단한", "공연", "프로젝트", + "농이티/관리" + ) + + val maxVisibleItems = 5 + val visibleItems = if (isExpanded) allItems else allItems.take(maxVisibleItems) + val hiddenCount = allItems.size - maxVisibleItems + Column( modifier = Modifier .fillMaxSize() @@ -52,7 +90,8 @@ private fun DetailScreen( ) Box( - modifier = Modifier.fillMaxSize() + modifier = Modifier + .fillMaxSize() ) { UrlImage( url = "", @@ -67,6 +106,7 @@ private fun DetailScreen( .align(Alignment.BottomCenter) .fillMaxWidth() .fillMaxHeight(0.75f) + .verticalScroll(rememberScrollState()) .dropShadow( shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp), shadow = Shadow( @@ -79,13 +119,12 @@ private fun DetailScreen( color = PawKeyTheme.colors.background, shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp) ) - .padding(16.dp) ) { Text( text = "단지와 룰루랄라 룰루랄라 룰루랄라", style = PawKeyTheme.typography.header3, color = PawKeyTheme.colors.contents, - modifier = Modifier.padding(bottom = 16.dp) + modifier = Modifier.padding(16.dp) ) HorizontalDivider( @@ -95,7 +134,7 @@ private fun DetailScreen( ) Row( - modifier = Modifier.padding(vertical = 16.dp), + modifier = Modifier.padding(vertical = 16.dp, horizontal = 16.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp), ) { @@ -116,19 +155,160 @@ private fun DetailScreen( HorizontalDivider( thickness = 1.dp, color = PawKeyTheme.colors.defaultButton, - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp) + ) + + WalkReviewInfoHolder( + icon = R.drawable.ic_walk_review_location, + content = "서울시 강남구 역삼동", + modifier = Modifier + .padding(top = 8.dp) + .padding(horizontal = 16.dp) + ) + + WalkReviewInfoHolder( + icon = R.drawable.ic_walk_review_time, + content = "2025.10.11 | 오후 11:30", + modifier = Modifier + .padding(horizontal = 16.dp) + ) + + WalkReviewInfoHolder( + icon = R.drawable.ic_walk_review_course_info, + content = "2025.10.11 | 오후 11:30 | 걸음수", + modifier = Modifier + .padding(bottom = 8.dp) + .padding(horizontal = 16.dp) + ) + + Spacer(modifier = Modifier.height(10.dp)) + + FlowRow ( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .animateContentSize(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(9.dp) + ) { + visibleItems.forEach { item -> + SubChip( + text = item, + isActionChip = true, + ) + } + } + + Spacer(modifier = Modifier.height(11.dp)) + + + if (!isExpanded) { + FilterChipDivider( + hiddenCount = hiddenCount, + onClick = { isExpanded = !isExpanded }, + modifier = Modifier.padding(horizontal = 16.dp) + ) + } + + Spacer( + modifier = Modifier + .fillMaxWidth() + .height(8.dp) + .background( + color = PawKeyTheme.colors.defaultButton, + ) ) + + DetailImageHolder( + imageUrls = persistentListOf("", "", "", ""), + modifier = Modifier + .padding(horizontal = 16.dp) + .padding(top = 20.dp, bottom = 12.dp) + ) + + Text( + text = "후기 글 본문 후기 글 본문 후기 글 본문ㅇ\n" + + "후기 글 본문 후기 글 본문 후기 글 본문ㅇ후기 글 본문 후기 글 본문 후기 글 본문ㅇ후기 글 본문 후기 글 본문 후기 글 본문ㅇ후기 글 본문 후기 글 본문 후기 글 본문ㅇ후기 글 본문 후기 글 본문 후기 글 본문ㅇ후기 글 본문 후기 글 본문 후기 글 본문ㅇ후기 글 본문 후기 글 본문 후기 글 본문ㅇ후기 글 본문 후기 글 본문 후기 글 본문ㅇ후기 글 본문 후기 글 본문 후기 글 본문ㅇ", + color = PawKeyTheme.colors.contents, + style = PawKeyTheme.typography.bodyDefault, + modifier = Modifier.padding(horizontal = 16.dp) + ) + + Spacer( + modifier = Modifier.height(20.dp) + ) + + Spacer( + modifier = Modifier + .fillMaxWidth() + .height(8.dp) + .background( + color = PawKeyTheme.colors.defaultButton, + ) + ) + + DetailTopReview( + reviewCount = 30, + isShared = true + ) + + Spacer( + modifier = Modifier + .fillMaxWidth() + .height(8.dp) + .background( + color = PawKeyTheme.colors.defaultButton, + ) + ) + + Spacer( + modifier = Modifier.height(40.dp) + ) + + Row ( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .padding(bottom = 20.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + // Todo : State의 isMine으로 설정하기 + if (state.isMine) { + DokiDeleteButton( + text = "삭제하기", + onClick = {}, + modifier = Modifier.weight(1f) + ) + DokiButton( + text = "수정하기", + enabled = true, + onClick = {}, + modifier = Modifier.weight(1f) + ) + } else { + DokiButton( + text = "해당 루트로 산책하기", + enabled = true, + onClick = {}, + modifier = Modifier.weight(1f) + ) + } + } } } } } + + @Preview @Composable private fun DetailScreenPreview() { PawKeyTheme { DetailScreen( - paddingValues = PaddingValues() + paddingValues = PaddingValues(), + state = DetailState() ) } } \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/detail/DetailViewModel.kt b/app/src/main/java/com/paw/key/presentation/ui/detail/DetailViewModel.kt new file mode 100644 index 00000000..5c0352a0 --- /dev/null +++ b/app/src/main/java/com/paw/key/presentation/ui/detail/DetailViewModel.kt @@ -0,0 +1,15 @@ +package com.paw.key.presentation.ui.detail + +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import javax.inject.Inject + +@HiltViewModel +class DetailViewModel @Inject constructor( +) : ViewModel() { + private val _state = MutableStateFlow(DetailState()) + val state = _state.asStateFlow() + +} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/detail/navigation/DetailNavigation.kt b/app/src/main/java/com/paw/key/presentation/ui/detail/navigation/DetailNavigation.kt new file mode 100644 index 00000000..8315c106 --- /dev/null +++ b/app/src/main/java/com/paw/key/presentation/ui/detail/navigation/DetailNavigation.kt @@ -0,0 +1,29 @@ +package com.paw.key.presentation.ui.detail.navigation + +import androidx.compose.foundation.layout.PaddingValues +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.detail.DetailRoute +import kotlinx.serialization.Serializable + +fun NavController.navigateDetail( + navOptions: NavOptions? +) { + navigate(Detail, navOptions) +} + +fun NavGraphBuilder.detailNavGraph( + paddingValues: PaddingValues, +) { + composable { + DetailRoute( + paddingValues = paddingValues, + ) + } +} + +@Serializable +data object Detail : Route \ No newline at end of file From c901620602628f96cd307b92f101b6c847aa631b Mon Sep 17 00:00:00 2001 From: sonms Date: Mon, 16 Feb 2026 01:46:59 +0900 Subject: [PATCH 26/47] =?UTF-8?q?mod/#154=20=EB=94=94=EC=9E=90=EC=9D=B8=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20=EC=82=AC=ED=95=AD=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/designsystem/component/DogkyFilterBadge.kt | 10 ++++++---- .../core/designsystem/component/DokiBorderButton.kt | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/paw/key/core/designsystem/component/DogkyFilterBadge.kt b/app/src/main/java/com/paw/key/core/designsystem/component/DogkyFilterBadge.kt index df189714..e71935bf 100644 --- a/app/src/main/java/com/paw/key/core/designsystem/component/DogkyFilterBadge.kt +++ b/app/src/main/java/com/paw/key/core/designsystem/component/DogkyFilterBadge.kt @@ -39,14 +39,14 @@ fun DogkyFilterBadge( ) .clickable( onClick = onLocationClick - ), + ) + .padding(horizontal = horizontalPadding.dp, vertical = verticalPadding.dp), contentAlignment = Alignment.Center ) { Text( text = location, style = PawKeyTheme.typography.buttonSmall, color = PawKeyTheme.colors.primary, - modifier = Modifier.padding(horizontal = horizontalPadding.dp, vertical = verticalPadding.dp) ) } } @@ -56,8 +56,10 @@ fun DogkyFilterBadge( private fun RegionBadgePreview() { PawKeyTheme { DogkyFilterBadge( - location = "w적음", - onLocationClick = {} + location = "강남구 역삼동", + onLocationClick = {}, + horizontalPadding = 6, + verticalPadding = 5, ) } } diff --git a/app/src/main/java/com/paw/key/core/designsystem/component/DokiBorderButton.kt b/app/src/main/java/com/paw/key/core/designsystem/component/DokiBorderButton.kt index f5114bbc..258d0d4f 100644 --- a/app/src/main/java/com/paw/key/core/designsystem/component/DokiBorderButton.kt +++ b/app/src/main/java/com/paw/key/core/designsystem/component/DokiBorderButton.kt @@ -21,7 +21,7 @@ fun DokiBorderButton( text: String, enabled: Boolean, onClick: () -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { val textColor = when { enabled -> PawKeyTheme.colors.primary From 34627dbca957e7143ca8617ee14a6979c729d457 Mon Sep 17 00:00:00 2001 From: sonms Date: Mon, 16 Feb 2026 01:47:25 +0900 Subject: [PATCH 27/47] =?UTF-8?q?add/#154=20drawable=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=20=EB=B0=8F=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/res/drawable/ic_arrow_down.xml | 11 ++- .../res/drawable/ic_course_list_refresh.xml | 43 +++------- .../res/drawable/ic_course_optin_filter.xml | 79 +++++++++--------- .../ic_course_option_selected_filter.xml | 56 +++++++++++++ app/src/main/res/drawable/ic_edit.xml | 4 +- .../main/res/drawable/ic_heart_default.xml | 19 ++--- app/src/main/res/drawable/img_home_empty.png | Bin 0 -> 72789 bytes app/src/main/res/drawable/img_walk_info.png | Bin 10143 -> 49963 bytes 8 files changed, 121 insertions(+), 91 deletions(-) create mode 100644 app/src/main/res/drawable/ic_course_option_selected_filter.xml create mode 100644 app/src/main/res/drawable/img_home_empty.png diff --git a/app/src/main/res/drawable/ic_arrow_down.xml b/app/src/main/res/drawable/ic_arrow_down.xml index 57125726..74fb93ca 100644 --- a/app/src/main/res/drawable/ic_arrow_down.xml +++ b/app/src/main/res/drawable/ic_arrow_down.xml @@ -4,10 +4,9 @@ android:viewportWidth="24" android:viewportHeight="24"> - + android:pathData="M16.8 9.6L12 14.4 7.2 9.6"/> + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_course_list_refresh.xml b/app/src/main/res/drawable/ic_course_list_refresh.xml index 5bcef1f2..73c6737b 100644 --- a/app/src/main/res/drawable/ic_course_list_refresh.xml +++ b/app/src/main/res/drawable/ic_course_list_refresh.xml @@ -1,35 +1,12 @@ - - - - - + - + android:pathData="M2.65 12.5C3.7 15.7 6.7 18 10.25 18c4.42 0 8-3.58 8-8s-3.58-8-8-8C7.29 2 4.7 3.6 3.32 6m2.43 1h-4V3"/> + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_course_optin_filter.xml b/app/src/main/res/drawable/ic_course_optin_filter.xml index c3ef1b39..b6d54fc1 100644 --- a/app/src/main/res/drawable/ic_course_optin_filter.xml +++ b/app/src/main/res/drawable/ic_course_optin_filter.xml @@ -1,56 +1,55 @@ + android:width="30dp" + android:height="30dp" + android:viewportWidth="30" + android:viewportHeight="30"> + android:fillColor="#FFFFFFFF" + android:pathData="M0.5 8c0-4.14 3.36-7.5 7.5-7.5h14c4.14 0 7.5 3.36 7.5 7.5v14c0 4.14-3.36 7.5-7.5 7.5h-14c-4.14 0-7.5-3.36-7.5-7.5z"/> + android:pathData="M0.5 8c0-4.14 3.36-7.5 7.5-7.5h14c4.14 0 7.5 3.36 7.5 7.5v14c0 4.14-3.36 7.5-7.5 7.5h-14c-4.14 0-7.5-3.36-7.5-7.5z"/> + android:strokeMiterLimit="10" + android:pathData="M22.71 10.76H18.1"/> + android:strokeMiterLimit="10" + android:pathData="M10.37 10.76H7.3"/> + android:strokeMiterLimit="10" + android:pathData="M13.46 13.46c1.49 0 2.7-1.21 2.7-2.7 0-1.5-1.21-2.7-2.7-2.7-1.5 0-2.7 1.2-2.7 2.7 0 1.49 1.2 2.7 2.7 2.7Z"/> + android:strokeMiterLimit="10" + android:pathData="M22.71 19.24h-3.08"/> + android:strokeMiterLimit="10" + android:pathData="M11.91 19.24H7.3"/> - + android:strokeMiterLimit="10" + android:pathData="M16.54 21.94c1.5 0 2.7-1.2 2.7-2.7 0-1.49-1.2-2.7-2.7-2.7-1.49 0-2.7 1.21-2.7 2.7 0 1.5 1.21 2.7 2.7 2.7Z"/> + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_course_option_selected_filter.xml b/app/src/main/res/drawable/ic_course_option_selected_filter.xml new file mode 100644 index 00000000..01ef3a4e --- /dev/null +++ b/app/src/main/res/drawable/ic_course_option_selected_filter.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_edit.xml b/app/src/main/res/drawable/ic_edit.xml index ac7efe46..18ad7207 100644 --- a/app/src/main/res/drawable/ic_edit.xml +++ b/app/src/main/res/drawable/ic_edit.xml @@ -4,6 +4,6 @@ android:viewportWidth="18" android:viewportHeight="18"> + android:fillColor="#FF9C9C9C" + android:pathData="M12.14 1.5H5.86C3.13 1.5 1.5 3.13 1.5 5.86v6.28c0 2.73 1.63 4.36 4.36 4.36h6.27c2.73 0 4.36-1.63 4.36-4.36V5.86c0.01-2.73-1.62-4.36-4.35-4.36ZM8.21 13.13C8 13.35 7.58 13.56 7.28 13.61l-1.84 0.26-0.2 0.01c-0.31 0-0.6-0.1-0.8-0.3-0.25-0.25-0.35-0.61-0.3-1l0.27-1.85C4.45 10.42 4.66 10 4.88 9.8l3.35-3.35 0.2 0.5 0.25 0.47C8.75 7.55 8.84 7.67 8.9 7.76 8.98 7.88 9.08 8 9.14 8.07l0.08 0.1c0.2 0.23 0.41 0.44 0.6 0.6l0.1 0.09 0.32 0.25c0.12 0.09 0.24 0.17 0.37 0.24 0.15 0.09 0.31 0.17 0.48 0.25l0.48 0.2-3.36 3.33Zm4.82-4.81l-0.7 0.7c-0.04 0.04-0.1 0.06-0.16 0.06h-0.06C10.58 8.64 9.37 7.43 8.93 5.9 8.91 5.82 8.93 5.73 9 5.68l0.7-0.7c1.14-1.14 2.23-1.12 3.35 0 0.57 0.57 0.84 1.12 0.84 1.69 0 0.54-0.28 1.08-0.85 1.65Z"/> diff --git a/app/src/main/res/drawable/ic_heart_default.xml b/app/src/main/res/drawable/ic_heart_default.xml index 37445066..d687c761 100644 --- a/app/src/main/res/drawable/ic_heart_default.xml +++ b/app/src/main/res/drawable/ic_heart_default.xml @@ -1,13 +1,12 @@ + android:width="20dp" + android:height="20dp" + android:viewportWidth="20" + android:viewportHeight="20"> - + android:pathData="M10.52 17.34c-0.29 0.1-0.75 0.1-1.04 0-2.41-0.82-7.81-4.27-7.81-10.1 0-2.57 2.07-4.66 4.63-4.66 1.52 0 2.86 0.74 3.7 1.87 0.84-1.13 2.2-1.87 3.7-1.87 2.56 0 4.63 2.09 4.63 4.66 0 5.83-5.4 9.28-7.81 10.1Z"/> + \ No newline at end of file diff --git a/app/src/main/res/drawable/img_home_empty.png b/app/src/main/res/drawable/img_home_empty.png new file mode 100644 index 0000000000000000000000000000000000000000..0e1a9a6e13770cfa200a24005911a52054e91c0d GIT binary patch literal 72789 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/img_walk_info.png b/app/src/main/res/drawable/img_walk_info.png index fde0e3a890820bb84653b71d21cade9b93e6aef9..b49aa6bc5ca672171eed7ba2e45aecdba32e34e3 100644 GIT binary patch literal 49963 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�n5+Clu=VL~U58>J1ry_3_KdgFjfwrQ?gH5KHK}z~8 z1~W%6I11_@h~0wg4c>Mbsf8+UV(2m>BGl#|O*mWmqFi2$1H-kA192>a;gRjX1?DkN z_&M~%b|n$Bdr-KIVZGf5UKW_Rh-UfqkK5QF;nd?(EC+*NZKPHURN_GmDJU!!Ia(?_ z{|$)V!YZ-ad@gB|ii?6brRNb{3Ko8M zS@2C9MV=Ap@a6V9Nhsg%Iy89q&RD5UqhC(Ey!p`oIuCIL@CtV1P|`_IYY`naRtH^} z$29XkVlulUh4fTbh$CJy12e0<6F8ZrW%g2r0v8wMXAPJ%1)vS@)P0_7L1T>W}SH$mSP~#m3A{(xg?>biTagb!D}-hyXp7b%i%GB=p$rGFr99@L;qcZhdLo| zj9%>+E|5G}Yo${7{&PN#yh6BiVttonSicD(&jK#3?stg}v0vrvN2UvV;d#Tqv6SmH zIBTm`!$wBnqsc_DzmBMg_wK7ZthdDihg?#}+4(Jsd#`7{UHN26jul1C1ozvFgvBt` zA5J=(HeYmPd21W$Z=>NRr{txhh9qK9*A<(g*gopoR#$@ShRXl}xoo~h(H2F%`oVT* zfo$O;1fKXzT5<85w->0E^y*K#85b9v8-K4$H)=;ySTyae>gtg%*>4m{4>kKaT!HYoEik{KghRAZw#8 zn^yy}4L_;!j(|*bQwz|1wQjFz;5osGoj`H5$x_&<{m7Yt zOm#hWimCGR)s-Q}Zf$Qb606#*Aw0&P{U9o4!&^8Sy*4vL6W!}?X;T2dm*Eqd&egmE z_r3*q{mfjyH-1io8dAmRo9Rqdb=3$_gFC^ly9;8U<869&%YCs`#l!72$|AoQ{)~~F zX_NyHxcC!wtu*!_x&m%idYx8RrASW# z`3i#wRelePRf`n<%k&Ob+N=<}K#S~nNVw4|c`)i`{^sfQFl+cQ9%$^ll? z`S_=8+n);2ypo!d#jo8D>&J(OyP?`f{mA1kx?5d}6h)#dLr> ztq51AR>9iHk&xm_Qy3^(FwJ{HH{w&~B9nrKO?i6Tdt~_T*!n!BC1=LfVn%W&jk=VT z?(O)qS-dwK9}Wir`hwO9{nS2)1`e-2IbJeefdS-SJT~Lbr1on> zsaOf^~D7Z*?2R1-dd?&hjcET_NH7c=O{azqW-eF zP98hz)SnR(Or0jvP93?s-$d!E6Snkm!Clit15>Bo1O_E}j8cDzM!Y zaE`P6IQjGaY_$hN={?-tS%}84nz_IVg-dzu&4g?{k5Lcbo`dvvm|xKs*Y**S-c%tk_hEZp>IN-UZz+kBz{x#b=z7zD%cKc2K?TvBu!ceBk zp;$i)UFdCK68M&7OWQaswH1iuev%eE4lvpkwvrdx4+~rV_nqo z$w?c<0Uy=qhw>yC)PnVg-+tha$*zLaaPd23;G&YAN0XVnd}0=M{SJS@;ules0&pB!H&a6D42$a7lF8m3g9yeHY#fgpUvvu`xB3w$CfAiv#mSgJqiB=Dal|j18*p$Nrm8PEnzzi z1P<39J@4+ys@A2!z91>yYwuP;cc?L$4=Zzaw>>-EmYlo1u1#P59`hqX_^(C^#~=Cwy%9d-GXuVEBdRa4BkxPadcGrrbA zv-)$X5nI|w{=NNM;ROkp%!fnUMsx>0F68`q;E|tuKT65^E1D~>m%}g3X87q3Rv+C} z=W205)iwG6t(wb)IUSkYl@O1huB4c~y0!3u^E=v0luzQvn|sBLb13uH=lRwGj`+NE zP2358RAQ4P&;{Y=T|inZhqmL_y=tbm4As|O*PBB$T7VL0nsM#P)v|53N$^yt11d!5 z%N#|#ZfkAEYj651-yQ3hnKb%v@BDTvNYpDbpD-qe;$!>%@y5M_a#8o^GOa?J5hmzq z+(G>pST}rX!2f{HUJfe{$5V#j???6?b$9ye{ zKN=a&6~Fwxf97yp_jrs0(o$w@)ag006D+olvP=r6*C?(N)}>U)vp%C3VYbf^uAHwK zwi^`J@bmJBV)mfG{w4{|k0>xIAqRq>9v=A3O?-UWiwRq5+l3f0HVI=I54orebhQHg z2XbCV+c>@(*`b0{{F*dLW+*I{c0IJXa&TUyVW})XxWB`4-7@3$i)=w>yc`fJ15FK? zE8233Cg+bnjJXAR@uNOsCK{SCCWz15L?4QcCO|A9NdSIeH|+QZ7YJC$Jfm=YBAf|-vy>9#DVw>xM zNuyF9QDU=hFmVUp?6zWFxJpR1z8I4MSh}rj`;F7>$q_Q!z1gWsz%#5SVGz zd0&$S4g%&h4o4!rEu$hFpSOoDqbX5WvKS>5y-SzEdBldszqY0xbTO1~U3;=NbReuJ zlKf7C8p^0;`i=)aQWr~$y9h;3wQq4=*eh@Q@0f=S*}$|f1}Uv(OYmKP7r=+8j`?ym zfTJ@Fm+`PRguv%l=XnF;Tq8?Hn+3jV9#mpZ@sPz5uD0ad{=+Y>wvmuO$z7shln^PT z6#v*c?u@Zj3xx^>{glVKvuNRxf3nPn!$YoI9HAsaS(u&_{U@%A72H0j#q{HZ0$2(0 zL|Y4Zh|l^Qpu>K?zfE6N4K=d?(-IFjz&RNA$JxMTa|1eNICL_h5RZ4+hL(gSXy|&f z(Kx3tw}QB3!=85YbBJUcgolqOQN(;7uf}L+k!0jMn(Hiyqf`bfmiH3N4CkQRN_M~F zkFPB3`Vl7QtclrfnV2HVBh>Et!4Sh_Y*)<2R`}{DTdLi*mOQZIOd!NBM%aXM1<}2$ zu1qI{<8v{i4UcX1%mCFb_H8j?S^I7fN|RBoc4pZgG;0z55q472Mn|QAIk?A4kw_Hf zz3p*!wZ+%H6`QeF`>Q*bP&LcL%T}xF>0>G-q}onu7#SgS5?>;d2g=X|l6XCm9z;PSlcdK=A2TE39As!)}aBXsbz*mfn?@=rpLx;z; zC8%1C1)7s@GrT!{n%tfRLg$SbqTgF{{NQ}kSwmi8DsY;JDHYg^GMY@QUiQCb0Z;fG z4BE9ecMOXr%pEq)?jA0>rRMCvX?^6WkxWn6!>0X!yT(lfE@T%qrvM>6;pSTlI;;f* zGVtH<|6QKs&#chAJMeGw*KWj9)QV-i~=!yIi^Kh%)DY?Th(05_8{8 z-*)01amSQZ*?(~~M5kfac93Mi=UNqO^dhm~QY} zv7-eMVX#n1A22}(5w7cYLuQBwz?~cDzfQ?_ImSV?z3c$&J3v8haJqXkDe9fHKU34( z2~pB))@iJVIg-30Zq-6U8U9l@VB0wG1!q~MGvzo1z_CjoO%@|+{3^)IcVxjw(jE~? z93i_|p&9U<(!DHLBQR# z?UY;;YdVDkt4XWpf%MCx%F5ma7i<1~4a7A7=XwhWcw#k8le1G2kZw+dgFdZMbG(j~ zQ|Urp5wansY13Bo*k6*+w;Z{d6Cmv_1VEtLfaL1-G3ZlQ^_6d?EELI^5s^vl?h|Y4 z`4r+wqyJ|URSyF3$PMoy*Z)d_m6W}QStRT=_92>_C*BWUT1QFxTCa6B?oz92w9}Yn zkX}K%pr8RDfq>RGJ1dMpPwI|45+fi>A_{P#qpDT*@>WkEGZ9BaIFhUpe19U}bXmH* zJ{L8DS_K0MwZci;AKME~rG!jlJ`YU`vKmv+1b|~DT#*t~2>@V1x}=Xr&L04o>0U5R z;6+)q`R+oXtG2_d!| zD-oGm?fAow_%4*|C)fsVy77^fvHn+f^8r}`G(F%qY63o|93R!FPAnuI+SU@`+%eh& zyZBoUL@MyOB{EGrA+$q4?1VKs$Ufe5W$*M`Zl8baJHEH58^)iulYn1KEB(psgV{Jcg zH%F5}r#GZQO>6RBTV@6(0KA7Q?uHPeg=NBd)ejrXe$fm42UzIE&Yr!o64>DU?Vob3 z?eY~Q;3`=Mcx48horL1xHbnb_oCXfPPv7(oW1**`@Jdn+7JWHueHcs*9C}h>x*ls( zNm%sbM$Z;Pr-n}{VBq~a=BBvb74O;@uFA*k+^@ABI4A$EXEBz%*RR|TDbT9h}Hx=DDZ z&^w4H{qH^RS_3A9=(xVKNTk#y>p+X+(Q1=z<`q{RdEYq-wcsQxbF=ASn8|W}Bm3oH zqpFTUSY2(!XlNtn&(H|!c%LrRKgLn^Ga|7xjlsiCEV3ifv*a}73KR(>TRr!cW&~px za{IWFIGyW7R>~*JrOx2IeGWqOM>XU2kK7x}5w5mWy3~TLKT%Ljw>rh>D^9lx7D!F4Cv{v+EQBjctv%G0jH*xcynJH(NNgPTd)4G8f{S~?E-|@GHVK(Q!a{8GO1?R4~x?aGRFpmI=;smHW(#yYk zway97cAquhyEL)Y?`!5CCor>H!fl>6YAH*QID&Pd&;8t)Ip#T_EM`L;JPQ94Q@4u3Wd z=(sVop5ntruS8$VvZs&}j7JoNO^*ESp||U%8;aL)l|9^c7{qn8YtEKrslOIH8%h(6 zzz@{KE@B!{iXPN5m!l2gc7fHcG!wG3%tf2@F|p6h8PXK-^&VZT=z?%Detts`2_%VW zHXpZ$x%g`u92~2rUk}jK*QVlX#Vv*%op38$92oRV+_*?gQTD^d5{P%|7GSd>+Gd1Ku~b_eGny*#yNDz zdhZ!knv4JSyzXUkg~R0*)#B(5e?-FjN3SuENSc8F*K>aOq+qzRre-_BCq_i%%7CF&%61h@Y%UH2WD~k{{ADzoOH&J(*d>CiEX-ON`CxaM1 zm*eio(lgf#Wkas#hSV0EKl~f69X}~59}iC$49Dar1Xb)OO7dOEcm)JDL_g23MkNeS zb&AK-EgV?LMyM}%^k8bz;M#`6`9B+yy{AoiCAxCPSF_-NKeA=ye-B6OB%4OVy8>Q! zB@R8;&lg<8KeQ~LNiH4d#;1P`a)Rp>jfvC|m&Xpn_7AL_Ubkny6-j37@VVY+IQ_0U zv2%f1Sy?;!FE*bQ0e_jkJQ&IEYE_qk6o^ijM7!69S}F_M{oMfL!zzdbXu(X@@t&*G zo6KeX+K{A!86|X>yn`%ueemN8X0m2=1m(}=jFxDXHMA2+5Jqa2-?48S^ahRuy+ z$bVvnRw`)fqj3@~XdU{;HIXYHA?)M$)Z51SZ zywUGj3iH{PmAXzy^Y<8^{KlVsQnwe*PPuWaRdgZIE;KDMBXjInSmGf@3IG9mRS#ZV z>_~*Au!(Fmy866QQgQ79z?WgQPo(J_FyzB77EJ$Wk`P1`BbWn{%kUrBj^ zy8sK`IM?(5184SphLrLdK7)uzGOYwAWtEfbLNq@$Zx*YA4YNY_0W@S$U0(iUWf3t` zbEoD%_~+Gk}b!a z3jFltrsR;cM$o_SKjQIsAl`+CfC2iu*`LhiVkWSKmUd0S~&_x%<0JBEh8Yh~EPa;a}^G+K2FmsS`9HypH> z->+q^ybQ9ZPGRYc(A^hlKEIc%){Dl6$A8S^agskKn-j0$PDfR`ySwY+zQjxFoyB^p z(5xJ&x4G31rQv(^-dJ|L>L8XR>W+d%>0yOW%2uSuNnv>=bMpQ`r|%VRK8``-^FmPV zdz*y8Vml>!g2l6$dvn5u2zMez+XEFQQv$tb@pU5 ziab?9MwtT-0i8t4y2--qe0iGhdX?IBe3rGp>J-6z&wh5$VOGgr!LTU;Oz84l9XRA1 z;K|YAw5FKLZ!9bL8C|Feo_e|VHG!lc;ZS@sztX7nL<*XlkP~;VuzeUa`+0uNF1ITC z_1j~zwyhQvl((ZR?)FhdsAl3rHj3Kof`yclC+%^rUzDsAIlIY+FjUeg!8d;d@%}IC z$3l^Yp|tqD9T)0d(g!w66PkS24>;E^ix`9Z3|gb|L|>de_Ji-Y@mU~==xPnWCeQVZ zq}k*jAW0!TKBp@7<3Kj;w__d;WB+T~mmH#Y7;_kdp&gs?5*81v+hY=fDP@RayQGkE z;>RdRK*y{N&NiOs!XNrJhq~<`IO{B^x(Fssv80HWmJ^3xAV>jd)71Gm-_*-;Y0rK{$J}6#=_g>kmL5z<7>6f)2ljeDlS+Wi&`dTXy^T(x8L&-Ra%wqR>qgY z0^vhfi-G;&+`dzfsAroVdFjUOR*P|!%sKF!wv>5!MaiG-&8jMWeg`$m11a$Kz<6nP z5~AUv>0k%>)$4-gLu6J7@p3J`YcorAwIr+b;o)Ik;QPlk&{cmL&tLX7&M0Ue2@}0z z>EWmwF57u|{N4M$SJK~0`9WPT-*LSO>3%y<)G?Y$P)Wf#?zi2Vjle^i6@Kr=*7C)R z^BB58(stNeQ{ffJKWI+Zj**3>e+k+>kas%(Ls@X;LXGMrb_X@+NFFLwiF$u49`Vm1VD`qzYajGWDHmBKwWp&Thm3GRtx-l zj&$ekE<*23U;6Ate#9rc#g0_QR8A)wn*qYoY?^`npH6(9&7OSEn{d#Nt-$cQ+8TW> zYRxfrJ@=4x=QZo!_obx0+0cnvo!-UiRXi#wq2Xae*8 zZ<|Yd7PQNdHYN&hew<3X6T)T1bSyTBD}<)aMw#C0z%_iZ;?--->SVpyp5LN=1!C%- zVJjr^PFh}FMuLuDXJ?;~naRZL7Fw0uP~kBAO^D1jQ%;7`?#K(tvA)s0(RDF~*K{TU zp|GI<#_C%RQBMnki{{{Z2B^K&df;?QFjvBAw&eLGTl(0KB_;*NM;4&l3BvP$M^HRQ zZQtL0Bg?D_)#a&hF06Xm#wY{9G1{-bSwJ>~XqlwXwBR-j)$`s5Yv6 z&l=kdHVT7Qt}=#m1Ky*D9UL_Nm@tO6PVD!G49_@DZWd))TUjL(+T)@N-(oJzJO?v> z133^w2}y||tlmV4bG62zR4BsALT^4oPkn}xXS4;*}Fl6+%95)m2oRqnL7%% zg1+b0_#&+7LH^Kj9g(eGNCnuFy`}A9#hQ|^HT~CDL<_Z!+^IFCJ ztTtloZiS9#gb!LPiA#qq(8}F540n?IDFggqHO7md`&L8=cy{LqlQ>-FD2DH8EiG1w z$pVkFg~s3=Yu82YrL3?lL~Iwv0??qJ%|8AjIo)Q-$8y`Nt27zHo8NZcp)?w@{;I*) z?_$LH<2qny8!FSl;0My1<-R-=^O35->qB?vgWvZel1NxjVQ6mqgh?0}dDshw!bZGS z%9+u4qAZ$8N?tG`JJLxgi`G+^i2w=QWh(XFTk8|C1p=G%3kUmwdW0)`G%Ng93)9oY zOPA@|z}Q?vmwX+H85+; z{LQ~sQkt^ZKY084!o`Xl+id#${XOs*^)d6G4c-K&0tskg^KL|_>gg24z-@k*U4M#Z z(V|JoDFu_qxqmvaosd4ugH<;Ouc9Oa=8tEG+ z71Owl15ieoG8m##r&BFAY5c`QD@n3; z+JXtI!_d%>MI7C^=Lp(HRygj@k@B6>{FvBUHh4Qd(ks*qA+PlQm!RazbLZP%@2;!O zzYv*361H_v;t`+vSvQ7gaVQO}x<;}=2TIqZoz3lD*`T#46vq^Wg*Q9pVJG_L7iu|~ z!g7Ii>8apgOzmMc_t0)lroFa)#u(3ZRqt0Zc&wCp-KY={HW}b4Ox3Svn(wG`EKrD~ zC#-FEu$>w3goI+91``l7o=Dl@YxsOVd38#kJFn$zvOs0MZ;JAE!^ApRocfx>{EB?B zYS8K&gfE2yb^6DT_}>R!O0V@b6gfjDWzH-!J1LYpP5_z9Y6jY!42vZ#y&zkM2KReh zR(z(MyYbBywwiEb*w`AhAyG=7dae1-oJ$OuJs|NMHYzn3Q7DH36uLV(?mR#kTAJmB z%yKn~BZiSboI#z#H;`OQ>%AVC2t1MTJc24i=0|;Ao3+)9jpMq6){tf{p_Gs>BB{C$ zlaAhtFh8SmI?B?zw;E`CZEtL^}2VEFUA|Or#;0 z^-?+N3DtwJg!-IMmtLKSZiNgCh*QEcT-QRE^Q!oqNG{(K>OGOfw_~>;AHG|u# z%R0m~ZMzn+wtD+}X@E{7Y9pLgw-&HNdmC)LAwP3fQbM=t$dg|SZR1SM#gw$o^huO| zYRifDKk8Xf-~BmGtaCtjIDi0cE8xCR1q-R^i`2cCbJJew(WJ{Z|B2aEe1B9IV0Ol1 zgxvRu#p>RuG@A*+q)4T80G{E0yDhQW`CC;t{u!*Lp=aPS17qO%o5eyZ`BD!)0r>E8 ze4fvnDLEvcmrn*8mmb{ju1u}&yuEj-C<%}uYjH3DbDiWyWo;T@t<=N|$c#;;y9+tJ zOp@L{5tYUF<4=6iyM6V3$MfFy<%OR|51-YygIL?eP5!3-+VU=l8`G~KSgxK5%Z2dM z41mQSX<>sFHYU*8r-FX<++!H4d8ag1uRCU3Acy_Ut`LQ`kWU;O6kCKVb+(uLYqr04 zjgyU;q3=MY7m@V>AI&?EE4&98s)I-1{}&OQ)XAlGpLP6=1|Sl+CrFi0rmc4p-NQNI z0qrlkJj-X}Qdhi0OrI_>iJ!qzcYQFrXY4JUGS}qYL(8K>vf5=s`MF02wU9a?i-L;7 zjPIc}km{7SZE@v#cVB=EYPOa%@KY=~acGu7y!*(-j@`3bk$ zV9OogJ*}zJXjL80xq71<7GB~}utO$P1}oXeT#h9gO&5BQ!itN#*b3;u|L<4hHwaIiMR?zsF&GaU9xA6fLSf0^J?D#;gy} zo=LP-Ht^PO$BE7UZ$oM>={DMuaR^lNhOB9dSJF^dj~0s7V%md+JKQpGOgDS|=Q$Cb z{kH54dLT~#hS41V4+hLDlH$4+OF(OI_k z5HBycG%Ed*rg-}pfyJMt#=iH0w{Ewg^Cj(v7hmakTX_74@ogiBSa429Vu+!?R{l6GN7 z2T3MR4gEPUjHvFOQ5=4p=v Cc}cFJwvnteO!M?fe`Qk=68gzcEo(-EFHb=&#|XC za=(NwsK#yeiM8P1S+3HWJ~PrHuPRFjm4xrZSM;1PXn2wokSo!n-s-YY5s>gFRLeSq z4Rcs6eEN%=oG9%i>&2aHdvbd9d$Ne99S9NX$cZ1 zxjjFR(C`~qmZ9?E$69C+{#3sB7bt+h+HUg^%BH4}M`izb4f9n~GnA041gBh7HU-3@ z(G*VAc-6j;h#_Ge?=8PcpQupyb~I912idxpT!=S|Tmij4JpQcbv-pBkAuRHE+9@FN z$`=j)3+wUS^LnK&$+A{OvfHJkbkKCvovoKTmO{?-Njulax#7iJ1nX`{t5g1P3nO7p zYUVach0oyUiOFvRWo7g7;WIX3o@r=5R^IEc-~7>Hw?&?S;fkc9ZB2pw0=cvJ+KAoF z&2Z!aH>BcCe> z^nVe_n45*2Zr*oQuwvy`%BJZD(!JDdF_}K&9vio9{FVO8dO9O= zfTa+xKlasmutG~|vxNc?O4 zxm(L?7$XG+L^dqnbS3QbRHoavpa#4=dh>16t_X;KozLXbAA%FwzZ$P?YVv`%u|V`a z=s2$(gkp7N4^R_CoFs-%$P2C3yz4BEzO)}RCxi%-;VNdyC-E_OgP{_~FB2Jt(ea%h zGq(eA?2SRl8zPU0o;!izN{4)RT8?7>>gL&34I;u@cR=>oYq6$TJ~uFaa+j0EA6M5# zVHc0wOoyX1b?z3iNkqLr`iqEaT#%F}usRSDkAKee9CHwQ96G35a~K~bwXoGr^n4pR ziz%ZHw+tJmF`L!pY9+k8N^0pTUW?&fcIcaAS{am{VT96=+#z7%|mnLnUg5dUrK zo>eN@SH{O4&%_AWdo2Ak`hNk9Kytss4?Tq9k`g5cAZ9S=bfIwQqUT=y=p*zIGH3MW z5viFvc(7yG-_*C6a` z6cboyFo*(A6<>dZ4R5_B;*1S;hx|IW80mMEG}f@kjq#UWj?BTMNIq%V>BQ$uHoYku zmtJ%RRcLa-pZ`8gmU_c3+Ty85g|O6Y*;upcC!D+BOyn07B*R2S zU!jC!)JwWMUk(R779>K@f_s6+h~TxtGDpfnv7ThHzu)#Z%$c_U0Z)}$-#(2XlX)RN zT=WXAzUmqgZ=T3m5Gf|PIk_J^^u#Mip%3Whsp!qAYf|ahlCxUftewi~n5>;&aMAgY zQ@E8DV-Z+$^AHOL+FnWbhgw>ZH)aeuN&N{jHwo>{pG4;8Ampu9$vll97H=Z+Pd{3StA70(3>rKrnSjQ-wam=Sy1q!}j9#6K zY&BXgkP<>P6K^mO#DcRgLL{X|ODq~h>7hp;5)2_8i3lq@;`5P~?n3FzV?@M)&}w6P ze0|%W;V$0>b5=I?)JN2smyeZCK1TX_9kd41PM^c19*Z>L-iMyS*l}Y;>bQ~-Sx}IV zCEqQ@RaaaHqcu-W8}9&k*Aw(LAuHR3_dfaxC!c&W)~;PEGCGAbb^920$Q-U%@F8-3 zR#sNRKqPb-YU7snW5*N=x%IkwQCY_=Vr4B&%ONq4oO5DC+=&g#fm2%LAbRQuKr~hbUdhxT75OuSaRn-VaqQNvPy?P1naiVO&D_GNhqFnI1zH|E}tutdJg{YXA7`u z__vqlu)j7EvWrb$X@`IJ8)2$i%;H`xBnyikAKQrC8=}xER`A9UDD2-Z!zu*3M%sTWiny|i|zbIK=HE~)b z4pl6`l6!6?%e`52wb@%p!s`wk{JXyjsb_a_%1qumV>Yh5{Cq53x{PF(f_9er=+UEb z?}Ja1JXEh*>U#mjisK?)I-@gD;Zlwv7pfzd;*A9mjfB+mnr7gD08aP|hoZRhic7HU z`yWKB$5fdmHzy-nQCBr`(W37+pbzTispz%hpuwWgjV89#afT4fL6ywY4eZD!iDk1Wa5sn|kX-*hN#-1ynCq-qzpb+D%Vn## zLRK64ycXGO86m^rXkn|IAt#^2VKP~v+$SK&+=-_3D@h~&2Y!6?L410{)o9tco^)56 zNHO=;RuRb?u+IfnSfA%Io=T`>2OwLL_4v=-kKmvwlgU!AOMVB%QosDtb7@DBsYYO^ z=v|OxXLhS6fL~vJA@026PNA18IR(t*27_K>wVD@i*yb%;ym+b7s8!uwp%)W+tKXBI z=v0zuI-wComr|t)z8&#z-@oKP1Iy@ zI-Ttrhh#WxffjdQ?sG4Ha4p)1TqW}q^ja1eP?rB`BqGZziequZ#*JjnrZ_$|tc`1A zqD_%hv}V%Ltr>#IwCUloh#v(cXK(~dEF2Q|d26aM z^rZP@oCigN0!}j8=ip9JLZAix4?ha=h<~3geRdK>{581!j{7lV#*FPO^*oUcdFe%G zk!<85QrAO;igbpkp=8NITx-KKPy7#a=NyNRKKek^-{C@asR0N!neV)nqUvGkMsl!sTjH>y^jJf;ZWnnEb12U~*+35(;A2siz}v z^b|z6WNtJ9TS2j?%_5~bQ#%NbLD$>tbiCMey)g}1s zvoBCoRG7>t)n#dMyAwn-K$ivw2Q$p{V}uE4htX2V@g|y|=c8q?^-_J7y|r;Ocg=ooQ-vBmt*CM6~cMMlY+Oam6cWabnyo`W&U~e*&}M{T@?oaT(VdzLW1?iEYwzS z!K07dkAK{D8$SKy6Btb z=jhq9DqC7TMp11s7(z)&G46Zl2_o*+4j+dHX5+e_@WTW5!d|fZdM-?$ezufPI0*v| zn}ZNpB^qt#ubUj#ZXiqjx@&(y`s2qaE-p@5>RdLqUui!)_Vi1%aJ44nB2{`+(e*Hx z%(TmO(Hx23hoxWQsi&U8(xpqqoMBldH5pD(KQo**DG-jlU%Rd9{Kem`s)34qMz6Ne zz22~Y!ua8n0{*~oCUlNVU%zfWMvWSZ0fUB;mS zQ7b%Pqdp(qufvEl&PK`9BM|mAbYVClQ94=bM;(6#IeS-Q#g9KFEp_hL!_hFGeflBh zoxFf;deu@_(PhvH$&#&cCt2fL@xt>@;kH|E!HX}xh?eGNF)pn?}ci4>=%0d$(uZR_^HFn`n}}y%MuG$UCheN#N*Gt1S!HLZ98@f zIJD8|$c3+FD^@@EBs^7eoou$gxikPLB;|}6Lst1QB1Calor*ditf?2^kH7yV-hcmt z9WC{KrFis7`mQmL2%%J=V*eo}3dwQ)h7rEjI($t&rZ--D1FKf87EQHXF6RzqHh4Ce z;@X&Ar_*YkhL;w9{==WQZL3)c6`h7Yu^OI#=CR9LJ^rzLn@N`Yc;0;TpK;vW(-HF3 zcbIrZIaYGw=#7~OdFtV-ts+Y!Cfd;1a|?;s&E$-16{-8(T_1!UrPu#fmS2jBic%b< z4Ja!cK&1XW?MS>TsjJurNOB1agNc6ZB7dW)p$6Z6`!zoM_(S}#{72Dv-A?nsVzDHf zcPrrL+?Kc&VU2{1W*;`0gXc6q^V8E%(dp`!5{>|ipssEUmVWa!KK=Al{IKi?nnSH{ketF2U#f$X zLEH_aq=kuonhjB`HF>bbh`d_o1Rw(Ps$#@adBV zmGsNm;PwRMR+2(`slXquyAG$Eem;?Ub*~^ah174k@z;3sjkm~Bm+MFKj)HrtvQ;&3 z;6O~AI02InItZi2PC!ORK21)Z#6jAvc!V`3#|EpVWqTXgisl7g{vnN9Z=!2#7Ltm$ zwHE8vt-<$8zsHXxqio)?Mf77e=nb&itRmYw^*9Rjkr+(As5nOL02ek)%tht6Ttr+J zfD8)#7ptLPybO}dCZ4ac>JeVka$Wqh%D+KHr=qXSR*{%6_xPDN81?#FqOrD!4{i$l z+zZd6Y|wCo11-J8Qsq9t=q~GpmD*Soj5ARWHJiJre-G%(RI-{jAV(&$x3f3wqonZ zY*dfRhBwC|2Y=CHV57iD?_JdZmsq77{ZfR#+;}zKc>Q&hru1NBq<%-f%_Pr$1)tX| z`f%pu=3?0JVHh!T6b26+ic(q>@(N3d{IigZc9lXT>G(x?;aD`%*1=CcC{%P8#=stI&9v&S(J`o>zl8c)oKw*X+|zLow#n> z6Sy`;Mgqwxx=0)mlMc;!7Lu>+Pg z4x=XcU3C$bHdI4Jr=rizy6Mx$<_#NAQqk1n(Q9OorRs;-Y{E;gy+h>W?=_JiQlf>| z&42n0-hJmC6c!e?BlYdk*2M=xdsh}a4nq@mKM{9EW`;=FW~b|rVM8%=@DLRBD@9gT z0UVCZL~$^UAbTlJ7CqwRgbj-OxVZ>h_em>X#U7zZB+9j6OIxEi)8Aa{Ujtr^4cN44lSr{|X=*_*6cjqSiN-nW-<*@7*#&tsG+fY*$lDN%5t+x) znr%VtKqo4PWTI{W89tc_Yder2@tsx@)d(%#{NI-$WxHgHJq}5mXK4uB`9mJml>Saf zUn6uefKE7J))5Yq=>vZtENVx{G22ZjD=Wi8k3CIi(I^lRHABg| z?@zmejI2x)78Jr@vcP7|pzXYlek_S2D=i>nH1DJI6BN@s7E3H39R8W8Dk(lgyC@=! z97>V)Ls;P}P4aJfQImu;rXzwDgOLBzDpH51-5!Gwp&b6S1$h&4( z7CiZ6g_ENbMDhs7WfG4|yFUGVuq+3RFD-_4OEn~8q9-rCCZ1>izsQ~6-wzd?j=s1r z7DM*LV-NYc&1SqW(2mp_g~RaSC!VJX)J%lZJF+WzE*mU``1y4|!}4#IB9dXkDvKN0 zK|S)y1|zT14`;0preIV&Icy@Qm6=F~9H~jply-L9_Ogv=a!rK7zdatWSTxu&RC>Dd zF|x9=h-3`N&CL_r{Ji{pS_m@5B2ZLR01H>A%q@i8sE5OzLGq7QmJmojfQFls@IuSG z049Ay;_t-5AU^8ag|LSr%ePAlJCpaxHv~oLeC7Wtz^!i~VmGvq`TWtlZ`@qQYP5|MSMl+VbYh1;lwvR@mcg`RvYHmEm=Lo zhY!btk3CBcLw7HU*_JCb&i>1p_+;fb!ck?A$TD<$5xwDDFxhBAF<5BgW^MJLU{fpd zx47Y~4?s(kSVs$6lq~CrSr3jni;<-!rOxXPki9}Sv6$R7ERr~{Y~l0k1HpiJES6YA zZC18WwaCuNMkvIYhYljbn5YNBq>`E85?Q)7j=nIOU?Um9Oh0xT{kGa*B9gOM?L=G} zdW}mE9>+}c?!s)g&_WZ1mA;QeL>!9+V5S{}p9q!%If5ZclzU+3V1PcKLC3`tv?RMhyd_Tz{W^rz~RUs3$)nXQQ|R0yx0-aEumD-p44XehB`%h()jnV5%QzKsq* z_tRsN()?}b!wtx^!|=xE(5FMF=UfV$+ulU{46 zZ=NT~SwUKnh|}CGCjav);fxt@TuUATTDeW4 zlqesqi%Kw)POc-8cawF!naF*^)B^bFnBXd=ULyBssyCcJh}iPN^e?i`$tfzVbK2x+ zGR@jx-EtFN6k8V6I`0DXWkP4jq<-Gq_v;#(kJM_(rjcY)_jsU!1qNg1~+i73>s1!;0M>@r@l%hxv-jJp?;^ToO36jhKxEC~hqN^icd%5# zPw{D^2z+GTN#6c1dKb`e%*1e;NAEyoG`(Blan0H+CW~P=f zj%dn(Hq)ByhL!w2Le|#^i=Qobmgao4Fl-r}gLOpip8ihxw76+OrOzo9WWn^vTR?pa zuX2emME_g#j|CrGc_CEmyvxwn37sJZ*Mj*+y&DWhj-l;toYfbejBEu~S5@PoM<2qt zi8B%K)a+1js3(9OZ=0LyFyy=`Fz5GU^_Mh`5TesYA$IU6VufJG4wN*+q=ZGEFvFSb z$vPK5WaVnfdN=UIC+p4D9D;5e3JY2OCXS#Wf?AXg0x5P3@YgrDw?q-CxmcwsiB99|TWFF%+r;&D8bS-hr z6e8}3Oy2y%i^=vrS%%FF)Bj#e>ftfcPevCF#Gi(rfCrv^N;q517OU`W@UBAa=292J zg!OKs>uRQJ8X`$$>xe9@nO=mJ;n~3Q_0Yfi1@xI2vOj|^qsD5`1|L|VO?(p7I`1N= z4JZ@S{+SowKlZ#ck9*PQ3!D~>#f50V&frkD zRpsmXMsi|nNYjnyX9-QT!$`bVQf|Z(liZ&;MuI@%8ZAOZ%%N-@+<7+H++@p=pHI3q z?+*BHO<3M37>UsJL=0wsTv+_Py>~SRgf(bMWFVRYF+pCu7;un|iQZEpOVUh>23!3` zew==@CCkY!OqeqH%ZrbQ;z=w7dfGWeh%ogmO&B!%f#t=4p3jR46DrFDOt>*UA1j8q zUfyXiDaXj_ztGlO=(dzkvc-=ZpnYyNk+D%u(+9`E=%BA_MdZkX z5Suj)P)eg{cD!coz%~oXB{0gVY-$~=6n)L);D9wvRXtPHdITkSeKoHT998vFU55+tyYSMuZL=tcR5r- zmmx^~iRa##f8m0;H~53TTY|xGoUC-VK`~^=5Zw638?kBq2K@TBw;&R%Js=eo_Z1u) zzNMx@+)i3r{Mj=dvcB1AZA*%~HGk zdbav`0Wj&33jjj=n_oZ@5)q?B_j9KkiFISvr6aNfmn8-x5qdN(>%Xj7+x&9k6USDw zHOyMG(5^W(oef$hczRxx7K5l3!p{)l=W%cblQ8Satk2W)I7);Uf;edr26|1HKBJc8 z3(2Sv3mGpYQ6jo1`xE$klH4RQv2)0u2szTqgc?g&@6mW$=4~`CpEv%S7d$IDec$;W z`nSK9qeW6EQPb>3qI){f2DlFxTgrK(`8MLU(JM|2l zH2)$*BVH`|<}rwT6&( zjmsO(UuO8nQu*x_>0g3M@SP4-a%m3{{W5>m~r@~X(#sd7p} z*GI~)gw zJLKakx6vlUB#*np_m{IQ^&c;Ve(?{GvYbiFSiHQ|gYYS{5g$1~CUb2Er!q@o%{~~P zd>4|#D&9O)Ux^2={tte3ZhJ~ z*eHk7`crdrvLC2Mah7tt`lF9Ojxpn>lSNY{7dc8}Frm-A^+GIM@)6y3$aj+D61s9e zS`6QKZwv7M?fko1X{cuCvyhLMY{Fn zFtEi>gwFO#uzUl)`rrFFXy8c1Vv(JGAX!JJmzyon5xMgpn>YT5mtTAt@4x@PNX&CO z9mzIxa(HUW0Z7w|$0U)O?R2npAKPlS+V0=Db<>lJ7ynq1c%Dk``w5lMcMd`7b7qdo z%pWjdeM4h&woa!N5im;R!j>&taPE2M;WsS7gz6Aal%P#Fuv5xpFTwC%&cw!s8j(;U zw%K|;!WW-F`gaE%qAI!vJcLLJGraN%bSt(%%CNUt=Ddpth7rEtSUDVUUlwD#1%~&( zqhoJ|WHySsLM`=Zcy=Y6x_F0}RB@8(jb{4c9iI*rv)x8D(s%I$U=GRY~* z<1H@5Qz=?X9D*rc!?E@ji?L3xH$1Uo-KK}X`s$}ODQ7}?zKRY)CG=fGY}zMHnq-(X zw*P1K^^H?$ODS}9evloe+_!T;|Nf%sw9{!vz}J+1Q81jPX~{#uh2zm2Ate#csW)&ZuMCmR+fY}Ix8Hh=$o*}sU$+h>lQ|hNvtunG zDK3+{q~WA6`Z%qbS=leG*|_1J4?bA3EaizR!FK@aOx`^-iJ$n{ryccMonCiaORG1o z)rvi?hOBn@ynZw_H{suR{2Mb4IT0aWrToG)AxWNWCR+ir&ln01O(+p67l|TXoQKH# zLjkuRD$)QPSwj}H@u~Nqt!ad0w5z2Wha^CI7lqHaGd$Rjw@O;w2f{UAAcvQ%SW_vjPkos zSP{W%AALx^^&Mi}#h>CF zXt=b3&1#UEn!R6yBJn#yQRC~27cW+-=cy4hDNqT$;}E*~O*cVXwdtg9>zkS;(RgFe zkI-0n;Igxnok34N^Bnq@4JL2;mUjF3^-34f|y-)X0e4A8_^%pV-E&As)c^Q zkg%9F9E<)HyL)9y(s%0c+-ql9)YN8;?qlqIWQ|IrfYmv3y-&vj88*IWd5 zO#?3dzl(9+MZfCwmW&En^TyU-bU>qbh;kasmwkg*UwH|O7k`RiFht~@1&712Q*xIR zRY^W1P2Otdn(xw&;b`=(WgjnpVQpJ)(mqe_DxvQkk`l~C=gj?0I2`(y+v6t*My`3P zWS4Fwhv{QaJcbDe9Z8ltIZPu5&SA<6iaQcV-nkQ@w`ItmJ`_Q0BoX>CQ)S5|tww=H z4)J41QPQ=HvNW*Yb985^od*!2vjh~ip zyBsgQ@H{^L_!BfYHH*6Y95JI*@l5#|k`i^p(x+Xt#?!8Ix3=EfF-{i`gpnTLxNnjdf=#5ax{yWN^gMF1t#WT6$v+6KC6iNo z_MX_8$YKG~7C0h=Tk6HgZ|4-fG-Y-;0P~$clhX=KO%seyz7LJr07jIh5J z-9ZmZyt~e5k|SnF)Zems4PJZY1uS}Z5o&5`1+gpPklV>YiI)rOUt=WcDkIamwXUJ% zj%d}|2No`^_9MjzLmz-j==+XjJ?FFLAN6i55<4awi6lv#$(t+mu?+LCfByrs4m+79 zN!3n^IdvuUv_-9Sx1#?=(_wUR`#6n=V~$K6h1kr|fcL6I z<6vjPg>0EGZ7s(!U*3);CVYbp9W%P*1wuH+i1Y7(c}4=rTDaZ86r#s zv{1yQ7`hGo(TU7CipBW+M}WgZj!s?DGG{W6P92HpakP7CaChDeb=hEec`>xx>V#%5 zN*9ORm>uYc?6^r3=whnBf1-H;gAM7Vg^RkRBghWuf2%3-hKyLHf@5_ z=@K>dd46qA?ji&rE@{Z|N^7JZTBGq>NTDm9e*VMn63-Ea7IfNKI>VliOJwe%h2p!z zZpL6s&Q70m9jD}Gvovh@u@Y!0VhJ`4lu-Sg~)fs?3-cKv@b2B?3Kj{rsv%| z=3H8Un)Y~1t+37`*n0GTc1q?_;=emboQe-;{{?Xl)k7B}LFT$|SHbx5C)<%ZL{0ke zi8DH%%o*eW)vnnriijzzWXRo&i)NpKlg>JeH0ze6fuW}{vFPnZw9n*>;sYl?^SF?^ zOU21a+lZjI91fcfr=N8mo_*;(eEQXQxbFJvL<5wqTegxkQzHTr65oUUSz4V=tD)CR ztsbAPy0-Db`6nLgJ?qpX&PN-W_d5Pl3B7Y5`nSCJ)mOoXpLpY-;zGxj7K=q2mji7j zc9beko>S+}#qEE+0cJ-b8Jd<(3lZBT66Hfgnc2i$La&XaN6*xF*pFWfIbNwUzSHL@ zq<%>D5YeS*Pmvz+h!6ia@F+M*w%KU|bF$=H zexKDpeGQ}8igbfQ?u>At1tD(*jhXCZ0}{aUwe-G*o+nrPoA zYB6y}YP_zl(G~OuKDy+*d5f@PvQm$RO6Z*rVXZG*_*K;-PrXr;<+49)v6yvIqH{5C z3-j~yN&jJ*4VN%0WO&I~$!!Yb#XtCXbe3b3>j_VK=+ z&IXqo*v&B0nea~iD*0u*IvixOziGqU^n45UIQK}pF=$QbC|m|2E}gma6$V})$Qn1i z{3&!x)*{Z9xw44z4k;Ky=)7YgmFCH0-q{O^h^Dc@_{QguTyo|qUyDd>C9XT;3fek) z=~DS{!kptpn?9vZ5%*ku`|WpW@Ywgc<5G7(BeS&l&2jCVrOk;^3(ox+-u>tsJon1W zC@3lrJ}lmWXp@CgHJWfF8mp;mIQoJGbDCz)p319#XT%a6aHxddHAt@dk394C#jSqt zgq-XQv4`boF{QoPuwlc(*TTutm{TdK;-MJgl#YbO&5M8S%IX;JsIEdtmoa9cCt0sQ<;fG}1 z2764+e}?i|L;fa`Wh&9Xv>zUP^kuxZa3Qj@vaxB?rqn1iGTe1iW0TuenByp4Fn`V@ z^a4^>3BBu}NHE&B-~RH4ho4+%wmYmZ*liYFJjGhi$;m-}K|U_~#T7Vz!3m`Q1w`$Y z&XQqxaWEKcu$!!Hk_HcG&OB5aE_4EPyl4a(an69lP<7-#@XYw1;l{zo;r)ZIK*I?S zf&(TI+eOT~H5Z3`dJAq`{}w{q5r0Ln1;1YXGLE6gwbmTbE^Uv{Tkt{S>ODS9sV#W* zsu!TOWTVp{>av(&eDoa=CMdB3RI$thVe(-Z5WbXjdy8@3ifnZRBC^E>!|R_T&W%hG z)_JI=0{^@6b|EowojIWq(&stysH5TW$O)HRi>0BV2|s=RC5#q3dKzR|Mk`8iu-;xm zR{CSlyn*LmdI^z87|4yBaS)z5d7=58)3HP!eDakzYHV1LV64+C_yZe zh&iXhtmSY;B`dKL5RZp3EOQW^9Csm1WT7hwJN*5!Nz(XY)(y0XwCv(D(sE$Q!p*Cm zhxN^$q3HdqG2yeDk^Aly==b(7aOc|B0XiQYUgGp-d|b5@KY6O;&$g2}D(_r0`0J+k z@MF`u4uwK?2%;{F7N&>agtop#ZX%(aXMYG1Ng&~0%oFYN#G=|&7O;t|^G(&z);6&% zD(>QO=5jc3;gRPfK#Q+zsgleR_lU4s#WBjy&B1PK1 z*3dCYjrI0eG>o999D@fB!CUWrfs;=;2^+~;R}%eLf^mEND9F!QcFJ+HOVER%5_tgIZ>-|_3!?QfBpO4LV`&?PNZI4TW=UNwCtV4 zT|H8I>3J>edp3kdw~Bm#Kds#OVo724v$;8$CxpY%EJ+qvIc`=|pW~lbUw#z}-*^Ed zMh-{mfbnD~1%+dCZ*)y2R$dUkUiUq|T=^YY{Z>IFT5kZc34@c75BrU12xGI#a8&m2 zoqq-*ltrY2Z}!*8&8qkCsi6=~>Lg7rYJ6V|nxwN;iC9lL0__q)^xFTfeHrgHZ^o8j z6V?*BKT-WHZrk)euKD3ntoBquLzaK1rT>#PZhZCwXgx&c7E@dHXNwQ9!NrIyIFc69 zKwB@-t^ya~`g$d4_gf)ZWlNT`VROi`ee~-45%ATB(8V@L!YSX}REs4`mWZ*(l0`j9 z6Vs>9L}6hOo%-$#bLAbB(VBr!OEcEI@(k9#_%v#leuw4_>(RV%J!+SKkMd7HLiuMO z!e3VnSHFQUyK)f?`*%4%lte#nvo~bqWK5he63;&SoCwuq(07)aTUzpm^ec|8+f@E3 zdLUFnKQIv5`ufe4jX(Uf{?6$Kk197BjC1||Act15r&2H#$F4J;eEbP4{^%VXG<6zs z3kDPQ2llE-wN7us>dK9H_uKbLUQUFfk#!RrR!XLrrE|g;>9YiAF%ltwJA7G5kos*K z-bO<-s8mPTIanj7#eqW)$zvy~w`j>?chT|7NJBRg(HH8CaL^(pMf?bHBu&IiR&T3p z(UBgkNo+j1L?1g*w~;meY0Zy#t7a*_ZQOvZMC>p+gx20s%U`p{$Qt<{{o-XJkw-Zv z+{igPelVgZA1aqB?rOIizbE@mo_b$y0?w)QWSs^#Z^o;)K7q2FLb@xm-S^RmHP&P_ z;H|gbg2iGH_XbH4;xzloQx7G%uKTKTGMSrf`KbKr1N`{d19WLyV05}*q+Nr~WRmN? z&^R{G-&)$$`fF>j@trrwE#O7|*r{#O;x53}d?e^b|3TxBo9z;fIGj$Wc!;l$A;UiH z*w)~`zjnLRBUxiFpofYB1!a$~d-#dB9+^FR_7g+;Wxdqm_Rb51Ba%)_(=r|xW$vmmO z5q3}JOpFqJZY%jR{Jg->)3w%Yu{`Zy)$^ig)FVEiKyDqEWrv0cT&~Mps zqWfQmhHJzmb~hoP2V*Br#ZeQEhu^bJ)c4sQv2Xx`hYdw;ZZ0j7A>kP1XqP45eS@EO zqn}W2nV5@>i=M~U_uqp#CnqVVwEGk(C3czQou&Oy`OQ}dy4@Ii{^j(Y8`k2`22T#mVz8z!#r?j^)dj!){MByAFjd-we#V9u*ZgpogJX1k?8dBrC$r zpFexBS!;eR6b_FHg(7i{;< zZj>z;19R~}Idm%!L}c;^#Ac4|yuPADu0>?|{y6();RsbwO;+^ik8eOT)8O{jIfo3= zeW#FRe{S(q9G*Q4PQ7`jhi_XXq}^J~Q6l2cn>XQla;AP+y_CIa(1{{k9j~s)i-$Jo z!IHyn#wdFsg0yIS+Oz@lzr6!~CIpj19Irhzq9m^V%b%d#T8rq|L2~vdH*)TdnMS)U z{qhaazxz$HeH}|H{w*8O@a7gcOSe;{k8b~v(FWaRWwC^@#UAuNowoT zlDYz1@7(M}{hDR?@!p(o2e7M906OOPxr&7E@sic5-<(Paj`zUsx-KPpBKgDSbC4}=V}YSoseA6IVp z-^5Wv-p$N%o~z^}n`!ge#Ul06!;y z=#0uUMF^uN*o^ZB9fj8q`Xw&yH;b(C0udpz%i|}1@0=YIaW%pLIDj0iKP#P%>xZ9= zh(?dks=nr$0K%WJCsOC*OQbWuI{o)JG;5Hs^o;bmhT8IQ{m{91w_!CZ=(}NO3lBLv z<7I`2O&$h`vqHI+d3VHev!k0XyXZ!gkXi(pS8l=oet!oJ88r=&uxFPtov3nXFd{^| zxc3%)APAkG%Ne92Mh(Zn!J~-KyS6f?NU4s-@$G;923vmN-aG8sP@(fc7B=$6O{QyY z!q!g~VeL!Li|-d|ZX(V7V-!s}m=JpBI1R9bWgtg-Z%F@_Ign)8aX~EJ2NOr$<7ZVB8H<7xQxKswd-))(TC%r zbLJ7rZ-rSz#aM*Ku7D3{r2B^U8;WQoQC-oXg|^u%lX)i*j;N4F@`jFx`Kj`+@VC{k z;hn0bh>>;7DIef1Hg(f>zA9DvD8+$(bSvw~NT;9rP+KPHHNzr%v+prTMo%{^0 z89EQK=8dGu@4>Om);XQ86Y@qSb^~oY1O9LEw5yR#at=hjlO10x!V$TSxi8o~WG)JS zIbeG7J<()ZS=z!x&V$Q_;TOmLoYc13-Dd3a`Cy%V3i)Y7uud{-)Qqn_|B~)7cUnZ$ zuD*l8mV*roUxLZu#J)mHi;|aiuyG>lC|!pQZ@dg^0m)zn1L^J-as9S_@*yo=ayY3N z2a*qm9d;P}{Y$+e=a0Ru8!ooJ-f@Iw7bj7?T=E@qm}SQ9$()tTSnG>u*;pf5x*N%-+fI($F|Rl zhv;xbpIrk3{Ph(g5=xVDEY8G^g%aN-agE~9047YD2!qj>e4fbv&|(C77*s+}Z-}}u z^rLunuOs zT&iOFka470`=HEBE}_e1Ta}h%T?r=7xVsXU4L=1BjX5_FpWZ!4t*%%?&e2Ur{sU)~ z9f6p;JXz>zyB8pk^4gtYiV{=bUBtaJ@ChX6r}n3f(0#p9BrPSSmWs`|^;fqdFC$0D z3LSy`O|&?8+QIFzM$5NtBmdwQS`<1d`D!QN$d!h*KM6~Ef9l6@+Caya83PXOUS9H?6$f9TAc}vt5(2ZvF_J7mB>0bWo9PIEXOhB z*Vl;%m@qka^(2c={MFU;cuqTKEB7WanKQ(BQob|WN*ONF4tp5Xu*IH(c4am6cJ0nmLzZkN$sCW2IZu$O_5cT0$<}%tHw`%fHq!Qg`~3sZIEvaVLn7+6wJY!? z>FqPK2DgiX=a$Z6nmIq~I}U*VL~&ufcC?L^20vyAYO_N_A*u|E)Dj}8t|jU^+$ z#$WtPvd$&pl;<1{w$4+1C~|^tOgNU6;$5*gJBd+`9tPDy?|D!HsCh){UVdZoQx87= zT3$F9IVL;XRcqwvblzj}#z6*Ht%ksq>D>JMOQ;;zMpk zI)SY*-YHyLb|mHwID$0w?Rse1@^JWZ6kD+AOaz1s~bg9 zOqYB}DT;v^12Kjhc}TmDAMb8x<-|a|KI!Keo;gU)P)$HXj?bvxg3<3?h50``jO9e^ zbs;xCZdi?xAO8^&7a%(jUqwndL0|Y-l*Ls_3~;hgaO(!V`uArrsGuJ?F}-_LA|iQs z{)b*RVd6xQ^T1$|YiepnCF|&huG*ED5DNx{2ENar&l&c4Q8IO!Nbgs`)g*V<>59ul zxZT{^ux&AkMq{*QW^6+bgG%T<9}2NEbPE@Kw(OoqUOv3MCTP_fwEvf#>Ds2(8@R_Q z!kT!zAZ(V}?U{)PbQ>$%($zcr!v&ggPMCNakp1pc<ZtOW7kJy`+p05iIWaK zg>>_}{c1ukkt{KB!gx_?HdRyp@yF#ve&){q-m;4y*8F_o(A-zV0s&-{4T6<(G7`1^ zxgJcE`~eZK2L_X!)U+Kxzm68}WlO#z9Y1N*NEVt8>nkc318HP|bO8SkTx^D?F>0)m=dMqEVA6Y|2(D?`sb^Xe)`)yG5gA<1ANS0EP+ zgA6U0UL+{CAdntcO_)i)?C-B*EhebN1yF0IwKV-^ZySV z%wa<>rx(|sn5z6$8QRJ6yqj#yxE>R);vM(!d&=f|>odG=-5(48X}|Hle^lT9`AX$C zo^5R8DKDDwTo>TCauCmVsWlFs_xYe?_G<^*NYl-_Lti)10NmM+SI4{&@1hK|I-hb(kk6UOz)2{-Z~<-aN?D z^rmg;^rq9qC2+a*^*VB=JgkD!OkI|h;38KnP?9A<-XI??lf1=z{5SN<%OwLl9M6}(`P^5SHC$<|Lxjb+xG2EHnD^K6*n8-=Gkzg-<-Qb zdx{hbA5^zm;8|zEDzl}3yT^n!v3$*X?}j8wmLXC=C!XT_>+WLNT!uGWPVjD;%XJRx z#|_c0)wc}f66-L?Ikrs~bhlWqy@f@lw`+#g-YFuc+O6l?Df|iFT?YkwWL|ym6GOnQC$TIPJuQpMx#w>W@sSTw(gYA6t+o)7TBH^V} zCG+?0gb+C+3pP{9x6KniB5&Ub$cKi$1lLmnYGc8ug{wvZ2KHNOdyQw~u{ zJ#I=M{wU5T$gzEnHwU961}{MT*2*rV8LwFoUP`P)8H^RTG6F&cL0&J9Ws^$6&WCdj zM=n*b`hwZJg>@3|Q0Cm}V_G)2tTCRcpi$;MU_sm6jE0NlM|Pz8U?Lr3)}uzXY-J$v zcC-IQ))nq#X}ONB?qPn%{dB+Lud{89=>(Zl+s+?R18&AgYz?m3`v}u66OwuCi)H=8 zpmZyha5PfpTJaE%_xDbJlfPa;%N3xD8pI6tfld`~#n5TBaUy)-Oo-B(P;3Q~oE$PS zM;$78s5JOd%ebO7Mx#(^ABC>4V}apYa}2E-X@3ZhYkcqf_Z`@-P`Y}rTOy&SeeOKw zKA^?3Z|p=f2?hsn>`+s03Q@ODm(x=vRp!o<^^X^ni(4o=Tip9I9%82PyjHY@@jONB zN;#UX)lIUy^1s=ucdFN&5Y#3w4<#{`T7I_CNPIX*$sII+lM8+d9O}>y;zP(1$f&5% zo1JgsFje^RQIC^x3XRrDP`YmTehi&0s0ZHIHR127=>^5womk_2hF*PB8eh@i#>6wQc7UlmsQ>DSL@$50lbSTwrN1lvqTg~IPvFkH4o zqQGv&U(ApjshEorQ>=~Suc(3(14nBx&wcBgGc{g}sMY5`K)Xv7S5Db1iW; zp(pE9x;$3dlDe3$W2X#Jxg3}#v>Wq=|u=bSlb8f`=Tw$PoO`Q1O1nKF*u zouW#iWRnf;TvfZ-w;n15O2Lw}M*H2lk8TGq>64pP1g~S#;}+@*F{&ZAiNWZ_Eyp|J za>7t`oWf!`_Lp_AM52{F?wx`$Ha1a}9>Ym$E>T z@ZUsep;~C*0~GAhgaqbH`Kk+fNCOC>iA?!fLh2JPj~=7KdP<3AjmRd!@>6aQ*}^gk zZb9WGl2o`+hLs$F#b=M$YvnGGxuZtLrEP`Qe`q~CN?6Lef{W$j#N7)AqBI2LUR4{P z?21PRm7>6-KqM+6jCTFZ*0aN3J996c;U3*X$jg*dA?8mJck?N@pb|YU7Vc{)7*TA{ zg+CE4PQ*c@&eB|JQn>2OU9%(-C!WlXwvSnC&xS6`8MVC|$sDQl^=q4y#5s1-2)>A2 zja3>_FVoNvRwB=PIyPC$7Qq4N{vC3f9*dZ%Or747*()Wy>psU2Gk+Q6PsWhK&p0^sU>h4i(6o^j}L@vkA1mKi~eS7*xfhr8Md{b)gkirstuZ;6g_{ z5n0T_dzPV5S{kQ0qdZg!yrZ+w5>Tt}_T7R|lV$nS%aik-eCXsh=~YvIi}|qWEaCIg zEP!IPqZmhWTr`10xpS(+r%VP{!OcsH6UL3iazK@-`#o~VcnWx_STBYm%Q4WxDfsn1 zN7e{K9W{MCE}RX#Rw}ZW{6|}kwG%5F%(Ukt1*+sKOoddk)1r)XqM%VJQ@6D#$OXb; z^6f`Gf>10jl&?gxN1J>(v)5TmWm8;5Gl-$2U?rl>b^<4hWN@RGNjDRgG=@`-e7_Ux z;(`e_P%qcR0r#295Q{!`%b2>gTI510PGKRyD7r|fm=-q#9<&ld%rbSM#TO&k#Q|Ja z&%9_Oe6zVHs_9zd3@c4x@trxEc6FX|Ccw%34*YHkq9=bYPUfi}Z^Q}jY>4^L?@nMG zmqK=!r)rEm;xR%A$NHI#?dW!GN5o7`1R&W)yqQQ@_h;dCu|T-vG?|dK@(>(FQp2gv zuO2|gI1);!Bc=|5nsBv}8~>km;!`l%1=1gKNcMuOXT9N6TNWd`J(OHsjk<%bqA1{w z5fq+4B7N;3?U>s);j$|g{bmV*muBtk;Tsr*?_*47$bK6qk;18Hi?3 zdh!*9qz8T%Qj{jCEknP{RxwG|Lc5?6ShH)CVT(?h#3CZA2yjR;Eg90W627l$to5Io z;+q)~_{J<$ygvRA2LMV#rMBntx7KXlpFR4Yd2E#Ew5v{kEkX+@j!o~e#^Afovhb|3 z$5&y!V16b+20c1L8qzYR)6s_`rZ_D%VlF3>0MzgEF_m0{`ppb0TASrLWDj+CUd=~k zIiuo@}(C`mS!Ce`JT@k2C%Pl z#|`yIPI4U_2%-E8#=s8V-@2pHBe!j=<`~_j=-0a`6}u!xiB)y3~?t^XmAz&b(kh77ffc6$R26q@}e>k#V(&{)!s>J{BH5^s;e}tqA;@` zC1R>Mnc589s6?TmJPGj)2eyFK++g6=DLblOIEY9IPWS=!qz>l4F5>j>>^)`1^uXW` zvFqMS0 zxSY_iz&Koih8XSui};?CaM5?lOS6Hvpb| zQ7Nk;zD)|K=K>>J5(j3zGuo(Cp_Hxin@_HlRo+?uw}K$1BvIQqsQ+c9NTRoZXSrTq zd_L`ehfuU92q%s%>;;7@Zn?nLb>2{6@r%JL^j-I2ASVhP21>P)pq-Rhaaq}$3&(Wk z+>@2Qq3-3uYnXndeMHVXALVgFbLsmbWLeCTTMc3G$W;}~S^AF%RgdZ27|YEsEOA3~ zFsh>jKKF-@u3$zca8g6%7i7+^x=Uem&_0$125GMBwJ~i%yS`>x*~t%1S|%u>?mB+m z@O6Aq)1iaMN-ja_5@MF@$TgMpdDd08S%nMmFZ||Za(}waksLl2dbK|Gh(eEl&DL2T zKWsRSt@=mj)iNbpGmY=>$e17h2#yX2W4*c+ARN1}vh9K7s7a+)SMcwDOvQG*o8%!m zZbX~U%oa*Cl*N)M`%SbCo(nkW?s$w37(JCD{&lq?5H|z=W;KvWP&*91d@U3&?5neK zq9topQpJu|sLYhScU9Gume`MYXYKP{CFlP9r8>VkAD`oBtf+`O`e;AQ$Mi|k_ZSkf zgDW}F0+%NGqE|WX#7NiM|3jE5%YLS^`GHui*}u68^7mVP*JDh$;k_Cnx*NiCs!k~y zR9>}e6dCJVo5@62`_A$M_+Q5g%?f`jQTty{!qmcBJkTp19$;*=RWYfOye=J@q|3RF zrd37u6kj@Kfh0NyoeTiJQh?+Mg%T>5c{8KSln>;HK zFlS&c6bz`!Ll$8wuUBAFz;u1;gD$-CJ&3=%)`TkKaGl+pvMwVK*{@nalW2!cq||9gMU09ZY6%<_J15;YOFWfHO+s=`n%sxx)b39dva?oo zy=Cy0yqZQ%YVK<3@xnZFrc9VPSt(VKRIm1vv`*1v9$r|Z>T=l+c5nND47R((4hpBA zuQ^WG7^<|xJ(I2(irzA(41`&ny0L697qBrAyLMP_0so=G`w@+TY~bK3|@cE%g&2iPa#)3@Zn+q)j-@O zU76;V%8Nv=U+1xn*JF|tNgc|sMBjbeHL@Kv5aUFeWs^$UC1K4PiXiL{EOH&X z*RINpiCM?e+<56+)%Wlxv`NXX9o4~1B;LF$9_!u9Fn}`xs+ZlmA(!Cm%(HTwsXicZ z!=dyYm^y7p0%=uS$+W@oD;AZcj92&9g-pI;ymwkY@=$?(#@m;x*L$zae_D1y_r!lf zXnI!`cqNJ!3zk;41mG3|=Ex26hzwZ)Q2~cfRV~*7yG4;Ia!%Uj5+j zn%__YoWO6n8zdxD1KGqu({<@w#;*%cdw1zMu^DW!tGS$7SW%1)^gF6wmXu{c4*(N; zk}I6|&)qAbekh-KW_*Z+s)QCPoXW{2GXrXeuj9B3zxSwdu%*l$zun(wvcCJ1Ipg+h z;kx~QMVHLHwLnQ+{rHm`sJiY`GI15v(hb1^L_d8Z)21L9FY4pde>oYS*+T6;9S5Oz z>;EmP*vzQ+=RPIHJU<&jGtDB^5mB<$C5#czeVUz5?tGClAqIE^!x@BQTZXU5qmK?F zFX?&UW`}3?q?uYQ4th$~(l@ck*>m9LvaSP83-+ySCim3VwEbr$& zhC%p_DHAH1h7s=lU(o&!!`(^%*?*C^gXbQ71eyjd$mKa5-z4d^5&ZeLkVi*s>bhW% zG2V3G)DxfTwtEXUJXce{=$fn-sqzXrQmfRgYopjsV^NnAbsN#>g?_)6nFK>AsiVf< zP#?L(L|y)%T(r?*k!Zu^8Bum*7GeI|kBh7Kfq-QnF{uN)kL2Z~fgTlFN&toz&~3QM z4d}p``s>~Y@~PG32}4IspOxRB;B7)=h*0NE>>rVk5zl4xWJ)1e~rD^-UqMwQ6n+2t+CTy$ej0%gY$zXCV18}7Zmg)i_IY)an5OgX|azR(oX-=Nnhoa%1u!t20 z^xrx3a#{_=VaM9$Vd8A zY{{~lnWcKY>}!m&_s1Mm^#51W$x%A4@{p) zA7!Ii+$k%QA3@Q~iC_w_rDXkgFlm4=;){MIU@G|6eKs?NJRF97nf$DR620c+zDHQO zQO?&X>fM<_11znt(i(bvh2}=^%~Gy!c^;yxD_L;_!fe2=m^cVjMOulNp|(X_7PVz{ zJWAV|gm1zZ5^f0soIxIHUnPb&$TV9Uje&upU!OvI9UgkoW%2k>6xlrAgY~vR5cpb$})v9F1A-2_Ft#`F4KX1otbhtg8m?R^> z1+Js?B|3cL5pCm5FuQ(1vcyTn*_EZ+X^=nNxq1_@`(93Gd!LHoN@)qye0QQ_HnOu- zEpMSf%d^TWGrzm=>2@qK?b09PqbgXqcq8>J@Zde{2M-?lwo;)Rq%u=bDHv>}wc=F?A(_@)ZA33`!Ie!oJbIKT z;_8Zd3Q!T9eB&)oi5g;{QX5e&TS|Y(-V}rBO;i1pZfzOjBEJ#Xe!l^Gd8A~~MW~Ch zeOTssHD2c(lq+0i3A!TK640VoT3G30lTMzeTQyd!6XNUrkn89UT!?%<mVJmALBsyXYg`%HJbAb7avX5GwH`0Ek8&c>@z|_}+Svt?oyP;wwKY~s@gQR*o zpAflVidk>J6`yY4329#&XJuz2(vx>ACadV_oIok}sLmI?pV_rhFa+~5`>Tq*>&nWJ zgUJ8Bl1@{g*O}NxsY+v(=O9IOXilityMSHrj}^>=JU*5><~;@LeyUU1`iK~rM%o02k-zdd4hfa;;3quERp~7tUy9Xcj%@`C4`Ea zKE#sMj@%Z=AsTD@2zegB*Y`ZC94i*g#JB?#@^j$Si}loraBI!-_z|iXs}Oy|;r3GT zH;qGC^qOI?=d)nyKJ{FxP>Zv=Iy<{FLHAhZncBWHF3;c?k($hdv|)lY_fLguIN2)@ zbI;!>Np-$4G^RBj!H{8RR~!V%Sxtwd?oFUdIjlp7y2D}XPvAYE6&oM6n_Xbi_v6TK;ttQGb% z^!CCgA671yn+pAKI`6>HXG18zr{04k29R16G;zD%bp-c{`tW&rYg_l3|B`4d7Pth! zZE=g-USddtd|`w&H{h+W#!2KZM~TZ|vjj}{37O3O#uN0pamj#@3@%#mWJgx{v9uV_ zT*jU>CtuLW7!P`IJ_k0-JVk+IL!JL&TaqqwOQZp$E9EA2(K|D{R3U#KPM~s*GP@P0 zgQzm-sJ2`!j1Nq0nchGBz`enMw5s5VChNO2a>9vUrk1(w?}xrr_HcdDkv4n*YDc_o zEfvSj1v_>LYpOZ`sn|d(2U!!VRVuxXb31mGJbY?ZKyg91pJ@_=ovnj-RbjlZ8IZvp zoxkM+O_q`%-LL@$iF;CEDTd0$p0(tOR0U^Q{mDS7>W+>OE<8`Kr*ib(TK{=DD~`+W zocQyBbg4s)P(E$@)7U=1%WZ!i>A%CNjx1kMs=1dzgG)8?vco?OR!2A$&h`TKA>?Wl zdM&YyvN=f_q2Sf_i@~PFInfj=flJYyaSoYHJ6Aa*47ETFK8oj#uhtLlD69S{CSPia zI)%u@6Ai-Po*-37_ex2(89|~MRm-TK@ul!;`_)_p2|jhk3vod`XXVw5UH_)G{m>kt zc&Ln;Y65l-f*O5a-^z1cZbvH1UfcLnJI(wMR0A9u=F*fa%bXOHiL?u&*OCp0p}YjK zlMH=cwCU~aQgWO2m}5?J zDo1hXecw8<)Z9r#z`BtI(rs?v3gm6Hnw*m*-1IZXBO16$I}dhXxwc-Mdt~-Af+m`b zhY$C-`h2dQW(`_)c8Zd5^|J~A6ftgbZ+$C2%8PjN2!8wVew)uSkvyCC+I05(cyYI) z64HCbTAe)1@K_aPiTQvg@9O`yLMuJyR0Js{7M*j$N`iR(4~W~lt8YUTd!EPHI2;Y2y@q*b!XNvaNVm4{9^n9! z{K?vhZ`EC5$p5K&6nnhuA<9x=ZCVaV1b<uxRVxET1zPR0n)i-*-9fXHr6&EmL1KqZj$e>*&vu5>q2YQ*zPH)IZarwfM$DQ<$Xr zGkeoWeBO`~bXK1^4mz_`UH$u|Ir!|K7oVX-^!IG z1K>EsLjB^O_39j^3j>b^t}Z3?r$srh3cdzNkuhcfy!X6zWEJ5Nk_l8M#&eJM+!xTq zWwy^W78OkrpI)&v!yfV%!LCl}Fh(y)ruu$2FRPcEt8dNrd{4{t)=C#)VqZcxQxjc( zU0p6lzZad;PySFzQK2Yn8+3F`)KAZ!KDy~91^iC`NhSgDjv`S`#j)l1^6b>(lZ*Yu|+1=b)vLcnw&aQPa$&T3G+Rj+kL*`3ckpWogd#j2KcBe zT+V-nx2>n&4Vw-_`<(so{!nHUd0bjyh($J2f;gn~hqIQ6I?D_r+XQAR13cMug^R4n z&3!UPUua;Jj1Rqbk(j0i9#yOvqypxLDi<4!M>KC1ChH5w-|$m_ES0X?KG3l@#`l8V zh!Rj9?75?{u1sROjks)w)4ziJdjB+?s88$pH{LIf>XgCP$|@0{s?uVA7xEce)65M6 zr2H3#abF)`4@S}lYb!8SBmYXMyF@g}nuyERqDp_mPL)94cxPH^1T_$RyEEumlfJn` zdO<*o|YR zwAVg^Y~=a2nP;ZsfomtY@ZBh5EPvee`+OMMQjsEgCZC*jhWzC?`_ogG+MeYNq06g2 zGPD@AH~B0A^Aj*-p|*JcSh@Kl0nuWTJ`xbK7W=2PvRh|S;| z{bVrZ+tsM^xC3cL4W`yOBQ>p6X$I z`Jzd(ps<_@OTBxJx>e;3HoJ?)c-hHun$+t#RExSbF@65MyWigToN4X&`}t$-R?-*$ ztE`%_lQLbfhRco=mW+h#SL3I}y7hA~VX0AZBr^7lyhAPwt*so4u$<%O)I}yh*s$+e zxm&E7(o$)>O4q?xPeTp0?;Zwm?j1PqN5I_55k{Xme~;!dXt|ScLU^(kR!*!H1#6iT zf;r0{G2P1G#q{LLSK3p9ijq~doaVs!o$&N-+i$RG;|9l$;HCbT(C&U|zDtLIc-lXF zjKtRxFR!ZqGHgZW&r$<~A<$i!D@!E6IW~}TpwnhBhrwgut_$mDJ+C(h|JLk@$kmg% zv>sQ5rdH_a3OEuS zR7bX{PcNWRKzdR}@k$N1=ZV8wxrC!s(dus}El|nVul(xe3v@Xl1TV57lLj{`2p9W& zmW%El0_`|Rj2{q5i?};GQ?}z6B?JAwN;DPuA+*Y7cIloIK@p>6RLgh6y2d=Cm!nBEC-!kt$A>e&TfuAL{7iVRc9+lU$z)wlIJE0*CW+EE@RWg-;Bny1I& z!}mNL32@ggKZIP=zV`Er@yP7~1fu`d(1j{etZ}K)C}8`kXyk&sSQ?0dWE0wI(KJ0c zmBqvp_%okfW-lha*z&_M3Jb5yZCql$gp5l$SdyDe_<*MWLdP>YW3d2h^b==cqdMT5 zaJFYFpUj`}gP3k5Q^?!pOrKIWoZ2PCz2j*isP^>>xl za*P7-95ny=x^Ub%mP}l%?Oia(Xe4i{lJWkE;Yg^?yZIO z8y<usk_%^&g2<=S2VR|FW{WS-9sRN=;`+@0N27TI*!^3olVL7MY>?Ppu)ntf9$L{@q|8x*C5 Date: Mon, 16 Feb 2026 01:47:44 +0900 Subject: [PATCH 28/47] =?UTF-8?q?mod/#154=20=EA=B3=B5=ED=86=B5=20item?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/ui/mypage/courseinfo/CourseInfoScreen.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/paw/key/presentation/ui/mypage/courseinfo/CourseInfoScreen.kt b/app/src/main/java/com/paw/key/presentation/ui/mypage/courseinfo/CourseInfoScreen.kt index 9b8ebc0f..27d1cdfa 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/mypage/courseinfo/CourseInfoScreen.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/mypage/courseinfo/CourseInfoScreen.kt @@ -15,8 +15,8 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import com.paw.key.core.designsystem.component.TopBar +import com.paw.key.core.designsystem.component.routeitem.RouteItem import com.paw.key.core.designsystem.theme.PawKeyTheme -import com.paw.key.presentation.ui.mypage.courseinfo.component.CourseRouteItem import com.paw.key.presentation.ui.mypage.courseinfo.model.CourseData import com.paw.key.presentation.ui.mypage.courseinfo.viewmodel.CourseInfoViewModel @@ -70,14 +70,14 @@ fun CourseInfoScreen( ) { items(courses.size) { index -> val course = courses[index] - CourseRouteItem( + RouteItem( location = course.location, routeTitle = course.title, routeImage = course.imageUrl, - routeDistance = course.distance, routeTime = course.time, routeDate = course.date, - modifier = Modifier + onClick = {}, + onClickHeart = {} ) } } From 7a571fdf5f110fa8f06419b394f821b6bf871eef Mon Sep 17 00:00:00 2001 From: sonms Date: Mon, 16 Feb 2026 01:47:59 +0900 Subject: [PATCH 29/47] =?UTF-8?q?mod/#154=20=EA=B3=B5=ED=86=B5=20=EB=B2=84?= =?UTF-8?q?=ED=8A=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../paw/key/core/designsystem/component/DokiButton.kt | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/paw/key/core/designsystem/component/DokiButton.kt b/app/src/main/java/com/paw/key/core/designsystem/component/DokiButton.kt index a7541bd2..a0e0995f 100644 --- a/app/src/main/java/com/paw/key/core/designsystem/component/DokiButton.kt +++ b/app/src/main/java/com/paw/key/core/designsystem/component/DokiButton.kt @@ -30,7 +30,13 @@ fun DokiButton( val textColor = when { enabled -> PawKeyTheme.colors.background - else -> PawKeyTheme.colors.defaultDark + else -> PawKeyTheme.colors.defaultMiddle + } + + val typo = when { + isDialog -> PawKeyTheme.typography.subTitle + enabled -> PawKeyTheme.typography.mainButtonActive + else -> PawKeyTheme.typography.mainButtonDefault } Box( @@ -49,7 +55,7 @@ fun DokiButton( ) { Text( text = text, - style = if (isDialog) PawKeyTheme.typography.subTitle else PawKeyTheme.typography.mainButtonDefault, + style = typo, color = textColor ) } From 92d22de2cc703604cc90b7be925da22c79203d8f Mon Sep 17 00:00:00 2001 From: sonms Date: Mon, 16 Feb 2026 01:48:18 +0900 Subject: [PATCH 30/47] =?UTF-8?q?feat/#154=20=EC=83=81=EC=84=B8=EB=B3=B4?= =?UTF-8?q?=EA=B8=B0=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=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 --- .../ui/detail/component/DokiDeleteButton.kt | 60 +++++++++++++++++++ .../ui/detail/component/FilterChipDivider.kt | 60 +++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 app/src/main/java/com/paw/key/presentation/ui/detail/component/DokiDeleteButton.kt create mode 100644 app/src/main/java/com/paw/key/presentation/ui/detail/component/FilterChipDivider.kt diff --git a/app/src/main/java/com/paw/key/presentation/ui/detail/component/DokiDeleteButton.kt b/app/src/main/java/com/paw/key/presentation/ui/detail/component/DokiDeleteButton.kt new file mode 100644 index 00000000..e52906e9 --- /dev/null +++ b/app/src/main/java/com/paw/key/presentation/ui/detail/component/DokiDeleteButton.kt @@ -0,0 +1,60 @@ +package com.paw.key.presentation.ui.detail.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.paw.key.core.designsystem.theme.PawKeyTheme +import com.paw.key.core.extension.noRippleClickable + +@Composable +fun DokiDeleteButton( + text: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier + .fillMaxWidth() + .border( + width = 1.dp, + color = PawKeyTheme.colors.dokiRed, + shape = RoundedCornerShape(8.dp) + ) + .background( + color = PawKeyTheme.colors.background, + shape = RoundedCornerShape(8.dp) + ) + .noRippleClickable(onClick = onClick) + .padding(vertical = 18.dp), + contentAlignment = Alignment.Center + ) { + Text( + text = text, + style = PawKeyTheme.typography.subTitle, + color = PawKeyTheme.colors.dokiRed, + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center + ) + } +} + +@Preview +@Composable +private fun DokiBorderButtonPreview() { + PawKeyTheme { + DokiDeleteButton( + text = "삭제하기", + onClick = {} + ) + } +} diff --git a/app/src/main/java/com/paw/key/presentation/ui/detail/component/FilterChipDivider.kt b/app/src/main/java/com/paw/key/presentation/ui/detail/component/FilterChipDivider.kt new file mode 100644 index 00000000..87359041 --- /dev/null +++ b/app/src/main/java/com/paw/key/presentation/ui/detail/component/FilterChipDivider.kt @@ -0,0 +1,60 @@ +package com.paw.key.presentation.ui.detail.component + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.HorizontalDivider +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.paw.key.core.designsystem.component.SubChip +import com.paw.key.core.designsystem.theme.PawKeyTheme + +@Composable +fun FilterChipDivider( + hiddenCount: Int, + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + Row( + modifier = modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + HorizontalDivider( + modifier = Modifier.weight(1f), + thickness = 2.dp, + color = PawKeyTheme.colors.defaultButton, + ) + + SubChip( + modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp), + text = "+ $hiddenCount", + isActionChip = false, + isDividerChip = true, + onClick = onClick + ) + + HorizontalDivider( + modifier = Modifier.weight(1f), + thickness = 2.dp, + color = PawKeyTheme.colors.defaultButton, + ) + } +} + +@Preview +@Composable +private fun FilterChipDividerPreview() { + PawKeyTheme { + FilterChipDivider( + hiddenCount = 10, + onClick = {} + ) + } +} \ No newline at end of file From ca21acf820c5a40e2659334a573e1b424b9a88ee Mon Sep 17 00:00:00 2001 From: sonms Date: Mon, 16 Feb 2026 01:48:56 +0900 Subject: [PATCH 31/47] =?UTF-8?q?mod/#154=20=ED=99=88=20=EB=B7=B0=20?= =?UTF-8?q?=EA=B3=B5=ED=86=B5=20=EC=95=84=EC=9D=B4=ED=85=9C=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=20=EB=B0=8F=20=EB=94=94=EC=9E=90=EC=9D=B8=EC=97=90=20?= =?UTF-8?q?=EB=A7=9E=EA=B2=8C=20=EC=9E=AC=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../key/presentation/ui/home/HomeScreen.kt | 136 +++++++++++-- .../ui/home/component/HomeRouteItem.kt | 190 ------------------ .../ui/home/component/HomeStartWalkingRow.kt | 5 +- .../ui/home/navigation/HomeNavigation.kt | 7 +- .../ui/home/state/HomeContract.kt | 4 +- .../ui/home/viewmodel/HomeViewModel.kt | 2 +- 6 files changed, 124 insertions(+), 220 deletions(-) delete mode 100644 app/src/main/java/com/paw/key/presentation/ui/home/component/HomeRouteItem.kt diff --git a/app/src/main/java/com/paw/key/presentation/ui/home/HomeScreen.kt b/app/src/main/java/com/paw/key/presentation/ui/home/HomeScreen.kt index 28ef8dff..c132bd16 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/home/HomeScreen.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/home/HomeScreen.kt @@ -1,5 +1,7 @@ package com.paw.key.presentation.ui.home +import androidx.compose.foundation.Image +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -8,6 +10,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.rememberScrollState @@ -17,32 +20,37 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.paw.key.core.designsystem.component.LoadingScreen +import com.paw.key.R +import com.paw.key.core.designsystem.component.routeitem.RouteItem import com.paw.key.core.designsystem.theme.PawKeyTheme +import com.paw.key.core.model.WalkingRouteUiModel import com.paw.key.core.util.UiState -import com.paw.key.presentation.ui.home.component.HomeRouteItem import com.paw.key.presentation.ui.home.component.HomeStartWalkingRow import com.paw.key.presentation.ui.home.component.HomeTopBar import com.paw.key.presentation.ui.home.component.HomeWalkingInfoHolder -import com.paw.key.presentation.ui.home.model.WalkingRouteUiModel import com.paw.key.presentation.ui.home.state.HomeState import com.paw.key.presentation.ui.home.viewmodel.HomeViewModel +import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @Composable fun HomeRoute( paddingValues: PaddingValues, + navigateToCourse: () -> Unit, viewModel: HomeViewModel = hiltViewModel() ) { val state by viewModel.state.collectAsStateWithLifecycle() HomeScreen( paddingValues = paddingValues, + navigateToCourse = navigateToCourse, state = state ) } @@ -50,13 +58,18 @@ fun HomeRoute( @Composable private fun HomeScreen( paddingValues: PaddingValues, + navigateToCourse: () -> Unit, state: HomeState, ) { + val itemWidth = (LocalConfiguration.current.screenWidthDp.dp - 40.dp) / 2.5f + Column ( modifier = Modifier .fillMaxSize() + .background(color = PawKeyTheme.colors.background) + .padding(horizontal = 16.dp) + .padding(top = 16.dp) .padding(paddingValues) - .padding(16.dp) .verticalScroll(rememberScrollState()), horizontalAlignment = Alignment.CenterHorizontally, ) { @@ -75,7 +88,7 @@ private fun HomeScreen( HomeStartWalkingRow( petName = "보리", - onClick = {} + onClick = navigateToCourse ) Spacer(modifier = Modifier.height(32.dp)) @@ -92,24 +105,105 @@ private fun HomeScreen( when (val uiState = state.walkingPopularData) { is UiState.Success -> { - LazyRow( + if (uiState.data.isEmpty()) { + Column ( + modifier = Modifier + .fillMaxWidth(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image( + painter = painterResource(R.drawable.img_home_empty), + contentDescription = null + ) + + Text( + text = "곧 추천 루트가 채워질 예정이에요\n" + + "추후에 인기루트를 확인하실 수 있어요!", + color = PawKeyTheme.colors.defaultDark, + style = PawKeyTheme.typography.subTitle, + textAlign = TextAlign.Center + ) + } + } else { + LazyRow( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + itemsIndexed( + items = uiState.data, + key = { _, item -> item.id } + ) { _, item -> + RouteItem( + routeTitle = item.title, + routeTime = item.time, + routeDate = item.date, + routeImage = item.imageUri, + location = item.location, + onClick = {}, + onClickHeart = {}, + modifier = Modifier.width(itemWidth) + ) + } + } + } + + Spacer(modifier = Modifier.height(23.dp)) + + Text( + text = "비슷한 이용자 루트 추천", + style = PawKeyTheme.typography.header3, + color = PawKeyTheme.colors.contents, modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - itemsIndexed( - items = uiState.data, - key = { _, item -> item.id } - ) { _, item -> - HomeRouteItem( - routeTitle = item.title, - routeDistance = item.distance, - routeTime = item.time, - routeDate = item.date, - routeImage = item.imageUri, - location = item.location + textAlign = TextAlign.Start + ) + + Spacer(modifier = Modifier.height(16.dp)) + + if (state.walkingRecommendedData.isEmpty()) { + Column ( + modifier = Modifier + .fillMaxWidth(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image( + painter = painterResource(R.drawable.img_home_empty), + contentDescription = null ) + + Text( + text = "곧 추천 루트가 채워질 예정이에요\n" + + "추후에 인기루트를 확인하실 수 있어요!", + color = PawKeyTheme.colors.defaultDark, + style = PawKeyTheme.typography.subTitle, + textAlign = TextAlign.Center + ) + } + } else { + LazyRow( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + itemsIndexed( + items = state.walkingRecommendedData, + key = { _, item -> item.id } + ) { _, item -> + RouteItem( + routeTitle = item.title, + routeTime = item.time, + routeDate = item.date, + routeImage = item.imageUri, + location = item.location, + onClick = {}, + onClickHeart = {}, + modifier = Modifier.width(itemWidth) + ) + } } } + + Spacer(modifier = Modifier.height(24.dp)) } is UiState.Loading -> {} @@ -125,8 +219,10 @@ private fun HomePreview() { PawKeyTheme { HomeScreen( paddingValues = PaddingValues(), + navigateToCourse = {}, state = HomeState( - walkingPopularData = UiState.Success(WalkingRouteUiModel.Fake.toImmutableList()) + walkingPopularData = UiState.Success(WalkingRouteUiModel.Fake.toImmutableList()), + walkingRecommendedData = persistentListOf() ) ) } diff --git a/app/src/main/java/com/paw/key/presentation/ui/home/component/HomeRouteItem.kt b/app/src/main/java/com/paw/key/presentation/ui/home/component/HomeRouteItem.kt deleted file mode 100644 index 23bb89e0..00000000 --- a/app/src/main/java/com/paw/key/presentation/ui/home/component/HomeRouteItem.kt +++ /dev/null @@ -1,190 +0,0 @@ -package com.paw.key.presentation.ui.home.component - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.res.vectorResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import coil.compose.AsyncImage -import com.paw.key.R -import com.paw.key.core.designsystem.component.DogkyFilterBadge -import com.paw.key.core.designsystem.theme.PawKeyTheme -import com.paw.key.core.designsystem.theme.PretendardBold - -@Composable -fun HomeRouteItem( - location: String, - routeTitle: String, - routeImage: String, - routeDistance: String, - routeTime: String, - routeDate: String, - modifier: Modifier = Modifier -) { - Column( - modifier = modifier - ) { - Box( - modifier = Modifier - .fillMaxWidth() - .aspectRatio(9f/16f) - .background( - color = PawKeyTheme.colors.gray25, - shape = RoundedCornerShape(8.dp) - ) - .clip(RoundedCornerShape(8.dp)) - ) { - AsyncImage( - model = routeImage, - contentDescription = "routeImage", - contentScale = ContentScale.Crop, - modifier = Modifier.fillMaxSize() - ) - - DogkyFilterBadge( - location = location, - onLocationClick = {}, - horizontalPadding = 6, - verticalPadding = 5, - modifier = Modifier - .align(Alignment.TopStart) - .padding(8.dp) - ) - - IconButton( - onClick = { /*TODO*/ }, - modifier = Modifier - .align(Alignment.TopEnd) - ) { - Icon( - imageVector = ImageVector.vectorResource(R.drawable.ic_heart_default), - contentDescription = "heart", - tint = Color.Unspecified - ) - } - } - - HomeRouteItemInfo( - routeTitle = routeTitle, - routeDistance = routeDistance, - routeTime = routeTime, - routeDate = routeDate - ) - } -} - -@Composable -fun HomeRouteItemInfo( - routeTitle: String, - routeDistance: String, - routeTime: String, - routeDate: String, - modifier: Modifier = Modifier -) { - Column ( - modifier = modifier - .padding( - top = 12.dp, - start = 4.dp, - end = 4.dp - ) - ) { - Text( - text = routeTitle, - style = PawKeyTheme.typography.bodyActive, - fontFamily = PretendardBold, - color = PawKeyTheme.colors.contents - ) - - Spacer(modifier = Modifier.height(4.dp)) - - Text( - text = "현재 거리로부터 ${routeDistance}km", - style = PawKeyTheme.typography.subButtonDefault, - color = PawKeyTheme.colors.defaultMiddle - ) - - Spacer(modifier = Modifier.height(8.dp)) - - Row ( - modifier = Modifier - .fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween - ) { - HomeIconText( - icon = ImageVector.vectorResource(R.drawable.ic_calendar), - text = routeDate - ) - - HomeIconText( - icon = ImageVector.vectorResource(R.drawable.ic_alarm), - text = "${routeTime}min" - ) - } - } -} - -@Composable -fun HomeIconText( - icon: ImageVector, - text: String, - modifier: Modifier = Modifier -) { - Row ( - modifier = modifier, - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center - ) { - Icon( - imageVector = icon, - contentDescription = text, - tint = Color.Unspecified - ) - - Spacer(modifier = Modifier.width(16.dp)) - - Text( - text = text, - style = PawKeyTheme.typography.buttonSmall, - color = PawKeyTheme.colors.defaultMiddle - ) - } -} - -@Preview -@Composable -private fun HomeRouteItemPreview() { - PawKeyTheme { - HomeRouteItem( - routeImage = "", - routeDistance = "10", - routeTime = "10", - routeTitle = "강남구 역삼동", - routeDate = "2025/11/06", - location = "강남구 역삼동" - ) - } - -} diff --git a/app/src/main/java/com/paw/key/presentation/ui/home/component/HomeStartWalkingRow.kt b/app/src/main/java/com/paw/key/presentation/ui/home/component/HomeStartWalkingRow.kt index 4d2262ff..970bd1d7 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/home/component/HomeStartWalkingRow.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/home/component/HomeStartWalkingRow.kt @@ -7,7 +7,6 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -34,7 +33,7 @@ fun HomeStartWalkingRow( Column { Text( text = "${petName}와 함께", - style = PawKeyTheme.typography.bodyActive, + style = PawKeyTheme.typography.subTitle, color = PawKeyTheme.colors.contents ) @@ -57,7 +56,7 @@ fun HomeStartWalkingRow( ) { Text( text = "산책 시작", - style = PawKeyTheme.typography.mainButtonDefault, + style = PawKeyTheme.typography.bodyBold, color = PawKeyTheme.colors.background, modifier = Modifier .padding(horizontal = 16.dp, vertical = 10.dp) diff --git a/app/src/main/java/com/paw/key/presentation/ui/home/navigation/HomeNavigation.kt b/app/src/main/java/com/paw/key/presentation/ui/home/navigation/HomeNavigation.kt index 2df475d5..c26deb58 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/home/navigation/HomeNavigation.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/home/navigation/HomeNavigation.kt @@ -1,7 +1,6 @@ package com.paw.key.presentation.ui.home.navigation import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.ui.Modifier import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions @@ -19,14 +18,12 @@ fun NavController.navigateHome( fun NavGraphBuilder.homeNavGraph( paddingValues: PaddingValues, navigateUp: () -> Unit, - navigateNext: () -> Unit, - navigateHomeLocationSetting: () -> Unit, - modifier: Modifier = Modifier, + navigateToCourse: () -> Unit, ) { composable { HomeRoute( paddingValues = paddingValues, - + navigateToCourse = navigateToCourse, ) } } diff --git a/app/src/main/java/com/paw/key/presentation/ui/home/state/HomeContract.kt b/app/src/main/java/com/paw/key/presentation/ui/home/state/HomeContract.kt index 5f77d848..3edf8d7f 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/home/state/HomeContract.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/home/state/HomeContract.kt @@ -2,11 +2,13 @@ package com.paw.key.presentation.ui.home.state import com.paw.key.core.util.UiState import com.paw.key.presentation.ui.home.model.WalkingInfo -import com.paw.key.presentation.ui.home.model.WalkingRouteUiModel +import com.paw.key.core.model.WalkingRouteUiModel import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf data class HomeState( val walkingPopularData : UiState> = UiState.Loading, + val walkingRecommendedData: ImmutableList = persistentListOf(), val walkingInfo: WalkingInfo = WalkingInfo() ) diff --git a/app/src/main/java/com/paw/key/presentation/ui/home/viewmodel/HomeViewModel.kt b/app/src/main/java/com/paw/key/presentation/ui/home/viewmodel/HomeViewModel.kt index 0f276dbf..aa96977d 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/home/viewmodel/HomeViewModel.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/home/viewmodel/HomeViewModel.kt @@ -2,7 +2,7 @@ package com.paw.key.presentation.ui.home.viewmodel import androidx.lifecycle.ViewModel import com.paw.key.core.util.UiState -import com.paw.key.presentation.ui.home.model.WalkingRouteUiModel +import com.paw.key.core.model.WalkingRouteUiModel import com.paw.key.presentation.ui.home.state.HomeSideEffect import com.paw.key.presentation.ui.home.state.HomeState import dagger.hilt.android.lifecycle.HiltViewModel From 91d8de55eeb859840c85e7027453a3820a634e24 Mon Sep 17 00:00:00 2001 From: sonms Date: Mon, 16 Feb 2026 01:49:21 +0900 Subject: [PATCH 32/47] =?UTF-8?q?mod/#154=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20=ED=95=A8=EC=88=98=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0=20=EB=B0=8F=20dropshadow=20=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 | 29 ++++------ .../paw/key/presentation/ui/main/MainTab.kt | 2 +- .../ui/main/component/MainBottomBar.kt | 53 +++++++++++++------ 3 files changed, 46 insertions(+), 38 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 70e6810a..138ac299 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 @@ -107,14 +107,6 @@ class MainNavigator( navController.navigateSignUp(navOptions) } - - fun navigateCourse(index: Int = 0, navOptions: NavOptions? = null) { - navController.navigateCourse( - index = index, - navOptions = navOptions - ) - } - fun navigateHome(navOptions: NavOptions? = null) { navController.navigateHome(navOptions = navOptions) } @@ -123,18 +115,7 @@ class MainNavigator( navController.navigateHomeLocationSetting(navOptions = navOptions) } - fun navigateArchivedDetail( - routeId: Int, - pageId : Int, - navOptions: NavOptions? = null - ) { - navController.navigateArchivedDetail( - pageId = pageId, - routeId = routeId, - navOptions = navOptions - ) - } - + // walk course fun navigateWalkCourse(navOptions: NavOptions? = null) { navController.navigateWalkCourse(navOptions = navOptions) } @@ -147,6 +128,14 @@ class MainNavigator( ) } + fun navigateWalkPrepare( + navOptions: NavOptions? = null + ) { + navController.navigateWalkPrepare( + navOptions = navOptions + ) + } + fun navigateDummyNext(navOptions: NavOptions? = null) { navController.navigateDummyNext(navOptions = navOptions) 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 6adb4d70..9d5e0e03 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 @@ -12,7 +12,7 @@ import com.paw.key.core.navigation.Route import com.paw.key.presentation.ui.community.navigation.Community import com.paw.key.presentation.ui.course.navigation.WalkPrepare import com.paw.key.presentation.ui.home.navigation.Home -import com.paw.key.presentation.ui.mypage.navigation.MyPage +import com.paw.key.presentation.ui.mypage.main.navigation.MyPage enum class MainTab( 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 55c1739a..cc1a64cc 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 @@ -6,6 +6,7 @@ import androidx.compose.animation.fadeOut import androidx.compose.animation.slideIn import androidx.compose.animation.slideOut import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -23,7 +24,9 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.dropShadow import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.shadow.Shadow import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource @@ -51,26 +54,42 @@ fun MainBottomBar( exit = fadeOut() + slideOut { IntOffset(0, it.height) }, modifier = modifier ) { - Surface( - shape = RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp), - color = Color.White, - shadowElevation = 10.dp, - modifier = Modifier.fillMaxWidth() + Box( + modifier = Modifier + .dropShadow( + shape = RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp), + shadow = Shadow( + radius = 7.dp, + alpha = 0.15f, + color = PawKeyTheme.colors.contents, + ) + ) ) { - Row( + Surface( + shape = RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp), + color = Color.White, modifier = Modifier - .padding(top = 16.dp, bottom = 10.dp, start = 32.dp, end = 32.dp) - .selectableGroup(), - horizontalArrangement = Arrangement.spacedBy(40.dp, Alignment.CenterHorizontally), - verticalAlignment = Alignment.CenterVertically + .fillMaxWidth() ) { - tabs.forEach { tab -> - MainNavigationBarItem( - tab = tab, - selected = tab == currentTab, - onClick = { onTabSelected(tab) }, - modifier = Modifier.weight(1f) - ) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 16.dp, bottom = 10.dp, start = 32.dp, end = 32.dp) + .selectableGroup(), + horizontalArrangement = Arrangement.spacedBy( + 40.dp, + Alignment.CenterHorizontally + ), + verticalAlignment = Alignment.CenterVertically + ) { + tabs.forEach { tab -> + MainNavigationBarItem( + tab = tab, + selected = tab == currentTab, + onClick = { onTabSelected(tab) }, + modifier = Modifier.weight(1f) + ) + } } } } From adcc6c393748df2cf23fa245b42ec3a3e507fc44 Mon Sep 17 00:00:00 2001 From: sonms Date: Mon, 16 Feb 2026 01:49:39 +0900 Subject: [PATCH 33/47] =?UTF-8?q?feat/#154=20=EA=B3=B5=ED=86=B5=20item=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 --- .../component/routeitem/RouteItem.kt | 190 ++++++++++++++++++ .../key/presentation/ui/main/PawKeyNavHost.kt | 13 +- 2 files changed, 196 insertions(+), 7 deletions(-) create mode 100644 app/src/main/java/com/paw/key/core/designsystem/component/routeitem/RouteItem.kt diff --git a/app/src/main/java/com/paw/key/core/designsystem/component/routeitem/RouteItem.kt b/app/src/main/java/com/paw/key/core/designsystem/component/routeitem/RouteItem.kt new file mode 100644 index 00000000..7db8f912 --- /dev/null +++ b/app/src/main/java/com/paw/key/core/designsystem/component/routeitem/RouteItem.kt @@ -0,0 +1,190 @@ +package com.paw.key.core.designsystem.component.routeitem + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.paw.key.R +import com.paw.key.core.designsystem.component.DogkyFilterBadge +import com.paw.key.core.designsystem.component.UrlImage +import com.paw.key.core.designsystem.theme.PawKeyTheme +import com.paw.key.core.designsystem.theme.PretendardBold +import com.paw.key.core.extension.noRippleClickable + +@Composable +fun RouteItem( + location: String, + routeTitle: String, + routeImage: String, + routeTime: String, + routeDate: String, + onClickHeart: () -> Unit, + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .aspectRatio(3f/4f) + .background( + color = PawKeyTheme.colors.defaultBright, + shape = RoundedCornerShape(8.dp) + ) + .clip(RoundedCornerShape(8.dp)) + ) { + UrlImage( + url = routeImage, + contentDescription = "routeImage", + contentScale = ContentScale.Crop, + modifier = Modifier.fillMaxSize() + ) + + Row ( + modifier = Modifier + .fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + DogkyFilterBadge( + location = location, + onLocationClick = {}, + horizontalPadding = 6, + verticalPadding = 5, + modifier = Modifier + .padding(8.dp) + ) + + Spacer(modifier = Modifier.weight(1f)) + + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_heart_default), + contentDescription = "heart", + tint = Color.Unspecified, + modifier = Modifier + .padding(8.dp) + .noRippleClickable(onClick = onClickHeart) + ) + } + } + + HomeRouteItemInfo( + routeTitle = routeTitle, + routeTime = routeTime, + routeDate = routeDate + ) + } +} + +@Composable +fun HomeRouteItemInfo( + routeTitle: String, + routeTime: String, + routeDate: String, + modifier: Modifier = Modifier +) { + Column ( + modifier = modifier + .padding( + start = 4.dp, + end = 4.dp + ) + ) { + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = routeTitle, + style = PawKeyTheme.typography.bodyActive, + fontFamily = PretendardBold, + color = PawKeyTheme.colors.contents + ) + + Spacer(modifier = Modifier.height(4.dp)) + + Row ( + modifier = Modifier + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + HomeIconText( + icon = ImageVector.vectorResource(R.drawable.ic_calendar), + text = routeDate + ) + + Spacer(modifier = Modifier.weight(1f)) + + HomeIconText( + icon = ImageVector.vectorResource(R.drawable.ic_alarm), + text = "${routeTime}min" + ) + } + } +} + +@Composable +fun HomeIconText( + icon: ImageVector, + text: String, + modifier: Modifier = Modifier +) { + Row ( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Icon( + imageVector = icon, + contentDescription = text, + tint = Color.Unspecified + ) + + Spacer(modifier = Modifier.width(4.dp)) + + Text( + text = text, + style = PawKeyTheme.typography.buttonSmall, + color = PawKeyTheme.colors.defaultMiddle + ) + } +} + +@Preview +@Composable +private fun RouteItemPreview() { + PawKeyTheme { + RouteItem( + routeImage = "", + routeTime = "10", + routeTitle = "강남구 역삼동", + routeDate = "2025/11/06", + location = "강남구 역삼동", + onClickHeart = {}, + onClick = {}, + ) + } + +} 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 dbf09eed..2254c40b 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 @@ -12,6 +12,7 @@ import androidx.compose.ui.Modifier import androidx.navigation.compose.NavHost import androidx.navigation.navOptions import com.paw.key.presentation.ui.community.navigation.communityNavGraph +import com.paw.key.presentation.ui.course.navigation.navigateWalkPrepare import com.paw.key.presentation.ui.course.navigation.walkCourseGraph import com.paw.key.presentation.ui.course.walkreview.navigation.walkReviewNavGraph import com.paw.key.presentation.ui.dummy.navigation.dummyNavGraph @@ -68,9 +69,7 @@ fun PawKeyNavHost( homeNavGraph( paddingValues = paddingValues, navigateUp = navigator::navigateUp, - navigateNext = navigator::navigateWalkCourse, - navigateHomeLocationSetting = navigator::navigateHomeLocationSetting, - modifier = modifier, + navigateToCourse = navigator::navigateWalkPrepare ) homeLocationSettingNavGraph( @@ -121,13 +120,13 @@ fun PawKeyNavHost( /*navigateDetail = { navigator.navController.navigateCourse(index = 1, navOptions = null) },*/ - navigateToSharedWalk = { routeId, pageId -> - navigator.navigateSharedWalkCourse( + /*navigateToSharedWalk = { routeId, pageId -> + *//*navigator.navigateSharedWalkCourse( routeId = routeId, pageId = pageId - ) + )*//* }, - modifier = modifier + modifier = modifier*/ ) userProfileNavGraph( From c51411f705d12296ccc87918a1cde8506adbd019 Mon Sep 17 00:00:00 2001 From: sonms Date: Mon, 16 Feb 2026 01:50:03 +0900 Subject: [PATCH 34/47] =?UTF-8?q?feat/#154=20=EA=B3=B5=ED=86=B5=20type=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/paw/key/core/designsystem/theme/Type.kt | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/app/src/main/java/com/paw/key/core/designsystem/theme/Type.kt b/app/src/main/java/com/paw/key/core/designsystem/theme/Type.kt index e5366a2a..faa757bd 100644 --- a/app/src/main/java/com/paw/key/core/designsystem/theme/Type.kt +++ b/app/src/main/java/com/paw/key/core/designsystem/theme/Type.kt @@ -48,6 +48,7 @@ class PawKeyTypography internal constructor( bodyDefault: TextStyle, bodyActive: TextStyle, bodySmall: TextStyle, + bodyBold: TextStyle, mainButtonDefault: TextStyle, mainButtonActive: TextStyle, subButtonDefault: TextStyle, @@ -105,6 +106,8 @@ class PawKeyTypography internal constructor( private set var bodySmall: TextStyle by mutableStateOf(bodySmall) private set + var bodyBold: TextStyle by mutableStateOf(bodyBold) + private set var mainButtonDefault: TextStyle by mutableStateOf(mainButtonDefault) private set var mainButtonActive: TextStyle by mutableStateOf(mainButtonActive) @@ -144,6 +147,7 @@ class PawKeyTypography internal constructor( bodyDefault: TextStyle = this.bodyDefault, bodyActive: TextStyle = this.bodyActive, bodySmall: TextStyle = this.bodySmall, + bodyBold: TextStyle = this.bodyBold, mainButtonDefault: TextStyle = this.mainButtonDefault, mainButtonActive: TextStyle = this.mainButtonActive, subButtonDefault: TextStyle = this.subButtonDefault, @@ -183,6 +187,7 @@ class PawKeyTypography internal constructor( subButtonActive, buttonSmall, buttonLink, + bodyBold ) fun update(other: PawKeyTypography) { @@ -217,6 +222,7 @@ class PawKeyTypography internal constructor( subButtonActive = other.subButtonActive buttonSmall = other.buttonSmall buttonLink = other.buttonLink + bodyBold = other.bodyBold } } @@ -411,6 +417,13 @@ fun pawKeyTypography(): PawKeyTypography { lineHeight = 16.sp, letterSpacing = 0.em ), + bodyBold = pawKeyTextStyle( + fontFamily = PretendardBold, + fontWeight = FontWeight.SemiBold, + fontSize = 14.sp, + lineHeight = 20.sp, + letterSpacing = 0.em + ), mainButtonDefault = pawKeyTextStyle( fontFamily = PretendardRegular, fontWeight = FontWeight.Normal, From 736854c0e8cf02c7b749289f13d7c527a28c31b2 Mon Sep 17 00:00:00 2001 From: sonms Date: Mon, 16 Feb 2026 01:50:32 +0900 Subject: [PATCH 35/47] =?UTF-8?q?mod/#154=20=EA=B3=B5=ED=86=B5=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EB=94=94=EC=9E=90=EC=9D=B8=20?= =?UTF-8?q?=EB=B0=98=EC=98=81=EC=9C=BC=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/designsystem/component/SubChip.kt | 16 +++++++++------ .../key/core/designsystem/component/TopBar.kt | 20 +++++++++++++++++-- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/paw/key/core/designsystem/component/SubChip.kt b/app/src/main/java/com/paw/key/core/designsystem/component/SubChip.kt index c56e891b..30bef1c5 100644 --- a/app/src/main/java/com/paw/key/core/designsystem/component/SubChip.kt +++ b/app/src/main/java/com/paw/key/core/designsystem/component/SubChip.kt @@ -16,7 +16,7 @@ import com.paw.key.core.extension.noRippleClickable @Composable private fun PreviewSubChip() { SubChip( - text = "4km", + text = "+ 9", onClick = {} ) } @@ -26,21 +26,25 @@ fun SubChip( text: String, modifier: Modifier = Modifier, onClick : () -> Unit = {}, + isDividerChip: Boolean = false, //true -> detail의 16dp isActionChip: Boolean = false, //true -> 회색 ) { Box( modifier = modifier .background( - color = if (isActionChip) PawKeyTheme.colors.white2 else PawKeyTheme.colors.green50, - shape = RoundedCornerShape(20.dp) + color = if (isActionChip) PawKeyTheme.colors.primaryGra1 else PawKeyTheme.colors.defaultButton, + shape = if (isActionChip) RoundedCornerShape(8.dp) else RoundedCornerShape(36.dp) ) .noRippleClickable(onClick = onClick) - .padding(horizontal = 10.dp, vertical = 4.dp) + .then( + if (isDividerChip) Modifier.padding(horizontal = 16.dp, vertical = 4.dp) + else Modifier.padding(8.dp) + ) ) { Text( text = text, - color = if (isActionChip) PawKeyTheme.colors.gray700 else PawKeyTheme.colors.green600, - style = PawKeyTheme.typography.caption12R + color = if (isActionChip) PawKeyTheme.colors.primary else PawKeyTheme.colors.defaultMiddle, + style = if (isActionChip) PawKeyTheme.typography.subButtonActive else PawKeyTheme.typography.buttonSmall ) } } \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/core/designsystem/component/TopBar.kt b/app/src/main/java/com/paw/key/core/designsystem/component/TopBar.kt index 0799dbd0..b005cc7c 100644 --- a/app/src/main/java/com/paw/key/core/designsystem/component/TopBar.kt +++ b/app/src/main/java/com/paw/key/core/designsystem/component/TopBar.kt @@ -1,5 +1,6 @@ package com.paw.key.core.designsystem.component +import androidx.annotation.DrawableRes import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -11,6 +12,7 @@ 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.graphics.vector.ImageVector import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview @@ -26,7 +28,9 @@ fun TopBar( onBackClick: () -> Unit = {}, onClickTitle : () -> Unit = {}, isBackVisible: Boolean = true, - thickness : Int = 1 + thickness : Int = 1, + onClickSuffix : () -> Unit = {}, + @DrawableRes suffix: Int? = null, ) { Column ( modifier = modifier @@ -54,6 +58,17 @@ fun TopBar( .align(Alignment.Center) .noRippleClickable(onClickTitle) ) + + if (suffix != null) { + Icon( + imageVector = ImageVector.vectorResource(suffix), + contentDescription = null, + tint = Color.Unspecified, + modifier = Modifier + .align(Alignment.CenterEnd) + .noRippleClickable(onClick = onClickSuffix) + ) + } } HorizontalDivider( @@ -76,7 +91,8 @@ private fun TopBarPreview() { onBackClick = {}, isBackVisible = true, modifier = Modifier - .fillMaxWidth() + .fillMaxWidth(), + suffix = R.drawable.ic_course_list_refresh ) } } \ No newline at end of file From 1da21410cdda6286ab50816f0051bb8371f22fca Mon Sep 17 00:00:00 2001 From: sonms Date: Mon, 16 Feb 2026 01:50:54 +0900 Subject: [PATCH 36/47] =?UTF-8?q?mod/#154=20review=20=EB=84=A4=EB=B9=84=20?= =?UTF-8?q?=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/course/walkcourse/walkcomplete/WalkComplete.kt | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkcomplete/WalkComplete.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkcomplete/WalkComplete.kt index dbe7885d..82cb09d9 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkcomplete/WalkComplete.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkcomplete/WalkComplete.kt @@ -41,13 +41,15 @@ import com.paw.key.presentation.ui.course.walkcourse.walkcomplete.state.WalkComp @Composable fun WalkCompleteRoute( paddingValues: PaddingValues, + navigateReview: () -> Unit = {}, viewModel: WalkCompleteViewModel = hiltViewModel() ) { val state by viewModel.state.collectAsStateWithLifecycle() WalkCompleteScreen( paddingValues = paddingValues, - state = state + state = state, + navigateReview = navigateReview ) } @@ -55,6 +57,7 @@ fun WalkCompleteRoute( private fun WalkCompleteScreen( paddingValues: PaddingValues, state: WalkCompleteState, + navigateReview: () -> Unit = {} ) { Column ( modifier = Modifier @@ -146,10 +149,12 @@ private fun WalkCompleteScreen( DokiButton( text = "후기 작성하기", enabled = true, - onClick = {}, + onClick = navigateReview, modifier = Modifier .padding(horizontal = 16.dp) ) + + Spacer(modifier = Modifier.height(16.dp)) } } From 457ccc95e35bb04099ac9dcad3c0ef97f3af83aa Mon Sep 17 00:00:00 2001 From: sonms Date: Mon, 16 Feb 2026 01:51:31 +0900 Subject: [PATCH 37/47] =?UTF-8?q?feat/#154=20=EB=A3=A8=ED=8A=B8=20?= =?UTF-8?q?=EC=B6=94=EC=B2=9C=20=EB=B7=B0=EC=9D=98=20=ED=95=84=ED=84=B0?= =?UTF-8?q?=EC=9A=A9=20enum?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../key/presentation/ui/community/model/SortedType.kt | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 app/src/main/java/com/paw/key/presentation/ui/community/model/SortedType.kt diff --git a/app/src/main/java/com/paw/key/presentation/ui/community/model/SortedType.kt b/app/src/main/java/com/paw/key/presentation/ui/community/model/SortedType.kt new file mode 100644 index 00000000..08876ae3 --- /dev/null +++ b/app/src/main/java/com/paw/key/presentation/ui/community/model/SortedType.kt @@ -0,0 +1,11 @@ +package com.paw.key.presentation.ui.community.model + +enum class SortedType( + val label: String, +) { + // Todo : 서버 내용으로 수정 + LATEST("최신순"), + POPULARITY("인기순"), + DISTANCE("거리순"), + TIME("시간순"), +} \ No newline at end of file From 65e62f24541828fc423798c9012b44e05a7352a2 Mon Sep 17 00:00:00 2001 From: sonms Date: Mon, 16 Feb 2026 01:51:47 +0900 Subject: [PATCH 38/47] =?UTF-8?q?feat/#154=20=EB=A3=A8=ED=8A=B8=20?= =?UTF-8?q?=EC=B6=94=EC=B2=9C=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/navigation/WalkCourseGraph.kt | 4 ++-- .../course/navigation/WalkCourseNavigation.kt | 2 +- .../ui/course/walkcourse/WalkCourseScreen.kt | 3 +++ .../walkcourse/state/WalkCourseContract.kt | 1 + .../viewmodel/WalkCourseViewModel.kt | 23 +++++-------------- 5 files changed, 13 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/navigation/WalkCourseGraph.kt b/app/src/main/java/com/paw/key/presentation/ui/course/navigation/WalkCourseGraph.kt index 211a7719..da8eb79e 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/course/navigation/WalkCourseGraph.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/course/navigation/WalkCourseGraph.kt @@ -32,7 +32,7 @@ fun NavGraphBuilder.walkCourseGraph( WalkCourseRoute( paddingValues = paddingValues, navigateUp = navController::navigateUp, - //navigateWalkComplete = navController::navigateWalkComplete, + navigateWalkComplete = navController::navigateWalkComplete, navigateReview = navigateWalkReview ) } @@ -40,7 +40,7 @@ fun NavGraphBuilder.walkCourseGraph( composable { WalkCompleteRoute( paddingValues = paddingValues, - //navigateReview = navigateWalkReview + navigateReview = navigateWalkReview ) } } diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/navigation/WalkCourseNavigation.kt b/app/src/main/java/com/paw/key/presentation/ui/course/navigation/WalkCourseNavigation.kt index 2584b0e2..0316f456 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/course/navigation/WalkCourseNavigation.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/course/navigation/WalkCourseNavigation.kt @@ -16,7 +16,7 @@ fun NavController.navigateWalkPrepare( } fun NavController.navigateWalkComplete( - navOptions: NavOptions?, + navOptions: NavOptions? = null, ) { navigate(WalkComplete, navOptions) } \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/WalkCourseScreen.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/WalkCourseScreen.kt index 880b4f55..81a46a2e 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/WalkCourseScreen.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/WalkCourseScreen.kt @@ -95,6 +95,7 @@ fun WalkCourseRoute( paddingValues: PaddingValues, navigateUp: () -> Unit = {}, navigateReview: () -> Unit = {}, + navigateWalkComplete: () -> Unit = {}, viewModel: WalkCourseViewModel = hiltViewModel(), ) { val lifecycleOwner = LocalLifecycleOwner.current @@ -133,6 +134,8 @@ fun WalkCourseRoute( WalkCourseSideEffect.NavigateReview -> navigateReview() + WalkCourseSideEffect.NavigateComplete -> navigateWalkComplete() + else -> {} } } diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/state/WalkCourseContract.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/state/WalkCourseContract.kt index 7cb71b72..6845f8e7 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/state/WalkCourseContract.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/state/WalkCourseContract.kt @@ -31,6 +31,7 @@ sealed interface WalkCourseSideEffect { data class NavigateNext(val regionId: Int): WalkCourseSideEffect data object NavigateReview: WalkCourseSideEffect + data object NavigateComplete: WalkCourseSideEffect } sealed class WalkCourseRecord ( diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/viewmodel/WalkCourseViewModel.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/viewmodel/WalkCourseViewModel.kt index 2b19b9bc..b14737c8 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/viewmodel/WalkCourseViewModel.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/viewmodel/WalkCourseViewModel.kt @@ -1,7 +1,6 @@ package com.paw.key.presentation.ui.course.walkcourse.viewmodel import android.location.Location -import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.paw.key.core.extension.toLatLng @@ -33,6 +32,7 @@ class WalkCourseViewModel @Inject constructor( private val walkSharedResultRepository: WalkSharedResultRepository, private val walkCourseRepository: WalkCourseRepository ) : ViewModel(), RealTimeLocationListener { + // Todo : saveStateHandle로 isShared 받아서 처리하기 private val _state = MutableStateFlow(WalkCourseState()) val state: StateFlow = _state.asStateFlow() @@ -73,7 +73,8 @@ class WalkCourseViewModel @Inject constructor( ) currentState.copy( - recordingState = newRecordingState + recordingState = newRecordingState, + isStopTracking = false ) } startTimer() @@ -206,21 +207,9 @@ class WalkCourseViewModel @Inject constructor( // Todo: 서버 내용 확인하고 넘기기 fun stopTracking() { viewModelScope.launch { - /*val currentWalkState = _state.value - - try { - walkSharedResultRepository.saveResult( - bitmap = currentWalkState.mapState.capturedMapBitmap, - totalTime = currentWalkState.totalTimeMillis, - distance = currentWalkState.mapState.totalDistance, - steps = currentWalkState.stepCounterState.sessionSteps.toInt(), - points = currentWalkState.mapState.poiPoints.toList() - ) - _sideEffect.emit(WalkCourseSideEffect.ShowSnackBar("산책 기록이 성공적으로 저장되었습니다.")) - _sideEffect.emit(WalkCourseSideEffect.NavigateReview) - } catch (e: Exception) { - _sideEffect.emit(WalkCourseSideEffect.ShowSnackBar("산책 기록 저장 실패: ${e.localizedMessage}")) - }*/ + if (_state.value.isStopTracking) { + _sideEffect.emit(WalkCourseSideEffect.NavigateComplete) + } _state.update { it.copy( From 4ccdb2789450dc9736c5c0b0fa9a0c05ae3b897f Mon Sep 17 00:00:00 2001 From: sonms Date: Mon, 16 Feb 2026 01:52:12 +0900 Subject: [PATCH 39/47] =?UTF-8?q?feat/#154=20=EB=A3=A8=ED=8A=B8=20item?= =?UTF-8?q?=EC=9A=A9=20=EA=B3=B5=ED=86=B5=20model=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../model/WalkingRouteUiModel.kt | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) rename app/src/main/java/com/paw/key/{presentation/ui/home => core}/model/WalkingRouteUiModel.kt (72%) diff --git a/app/src/main/java/com/paw/key/presentation/ui/home/model/WalkingRouteUiModel.kt b/app/src/main/java/com/paw/key/core/model/WalkingRouteUiModel.kt similarity index 72% rename from app/src/main/java/com/paw/key/presentation/ui/home/model/WalkingRouteUiModel.kt rename to app/src/main/java/com/paw/key/core/model/WalkingRouteUiModel.kt index d69db457..5395ffbf 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/home/model/WalkingRouteUiModel.kt +++ b/app/src/main/java/com/paw/key/core/model/WalkingRouteUiModel.kt @@ -1,4 +1,4 @@ -package com.paw.key.presentation.ui.home.model +package com.paw.key.core.model data class WalkingRouteUiModel( val id: Int, @@ -16,8 +16,8 @@ data class WalkingRouteUiModel( title = "한강 반포공원 코스", distance = "3.2km", time = "45", - date = "2025/11/06", - imageUri = "https://picsum.photos/400/300?random=1", + date = "25/11/06", + imageUri = "https://picsum.photos/300/400?random=1", location = "서울 서초구" ), WalkingRouteUiModel( @@ -25,8 +25,8 @@ data class WalkingRouteUiModel( title = "북서울 꿈의숲 코스", distance = "2.8km", time = "38", - date = "2025/11/06", - imageUri = "https://picsum.photos/400/300?random=2", + date = "25/11/06", + imageUri = "https://picsum.photos/300/400?random=2", location = "서울 강북구" ), WalkingRouteUiModel( @@ -34,8 +34,8 @@ data class WalkingRouteUiModel( title = "성수동 카페거리 산책로", distance = "1.6km", time = "25", - date = "2025/11/06", - imageUri = "https://picsum.photos/400/300?random=3", + date = "25/11/06", + imageUri = "https://picsum.photos/300/400?random=3", location = "서울 성동구" ), WalkingRouteUiModel( @@ -43,8 +43,8 @@ data class WalkingRouteUiModel( title = "올림픽공원 호수길", distance = "4.5km", time = "60", - date = "2025/11/06", - imageUri = "https://picsum.photos/400/300?random=4", + date = "25/11/06", + imageUri = "https://picsum.photos/300/400?random=4", location = "서울 송파구" ), WalkingRouteUiModel( @@ -52,10 +52,10 @@ data class WalkingRouteUiModel( title = "남산 둘레길", distance = "5.2km", time = "75", - date = "2025/11/06", - imageUri = "https://picsum.photos/400/300?random=5", + date = "25/11/06", + imageUri = "https://picsum.photos/300/400?random=5", location = "서울 중구" ) ) } -} +} \ No newline at end of file From b4081814853608c37cded749809801688b2b08a2 Mon Sep 17 00:00:00 2001 From: sonms Date: Mon, 16 Feb 2026 01:52:54 +0900 Subject: [PATCH 40/47] =?UTF-8?q?feat/#154=20=EA=B1=B8=EC=9D=8C=20?= =?UTF-8?q?=EC=A4=80=EB=B9=84=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 --- .../walkprepare/WalkPrepareScreen.kt | 8 ++- .../walkprepare/WalkPrepareViewModel.kt | 16 +++-- .../walkprepare/component/WalkPrepareBody.kt | 59 ++++++++++++--- .../component/WalkPrepareWeatherInfo.kt | 71 ++++++++++++------- .../walkprepare/state/WalkPrepareContract.kt | 2 +- 5 files changed, 115 insertions(+), 41 deletions(-) diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkprepare/WalkPrepareScreen.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkprepare/WalkPrepareScreen.kt index ed610a92..a9baa76e 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkprepare/WalkPrepareScreen.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkprepare/WalkPrepareScreen.kt @@ -35,7 +35,8 @@ fun WalkPrepareRoute( state = state, navigateWalkCourse = navigateWalkCourse, addWalkItem = viewModel::addWalkItem, - deleteWalkItem = viewModel::deleteWalkItem + deleteWalkItem = viewModel::deleteWalkItem, + clearLastAddedItemId = viewModel::clearLastAddedItemId ) } @@ -45,7 +46,8 @@ private fun WalkPrepareScreen( state: WalkPrepareState, navigateWalkCourse: () -> Unit = {}, addWalkItem : () -> Unit = {}, - deleteWalkItem : (Int) -> Unit = {} + deleteWalkItem : (Int) -> Unit = {}, + clearLastAddedItemId: () -> Unit = {} ) { Column ( modifier = Modifier @@ -73,6 +75,8 @@ private fun WalkPrepareScreen( WalkPrepareBody( itemList = state.walkPrepareItemList, + lastAddedItemId = state.lastAddedItemId, + onFocusHandled = clearLastAddedItemId, modifier = Modifier .padding(horizontal = 16.dp), addWalkItem = addWalkItem, diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkprepare/WalkPrepareViewModel.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkprepare/WalkPrepareViewModel.kt index 20ea84be..6b4f7a2d 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkprepare/WalkPrepareViewModel.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkprepare/WalkPrepareViewModel.kt @@ -25,15 +25,17 @@ class WalkPrepareViewModel @Inject constructor( val newItem = WalkPrepareItemModel(id = newId, walkItem = TextFieldState("")) _state.update { - it.copy(walkPrepareItemList = it.walkPrepareItemList.add(newItem)) + it.copy( + walkPrepareItemList = it.walkPrepareItemList.add(newItem), + lastAddedItemId = newId + ) } } fun deleteWalkItem(id : Int) { - val currentList = _state.value.walkPrepareItemList - - _state.update { - it.copy(walkPrepareItemList = currentList.removeAt(id)) + _state.update { state -> + val newList = state.walkPrepareItemList.filter { it.id != id }.toPersistentList() + state.copy(walkPrepareItemList = newList) } } @@ -45,4 +47,8 @@ class WalkPrepareViewModel @Inject constructor( state.copy(walkPrepareItemList = newList) } } + + fun clearLastAddedItemId() { + _state.update { it.copy(lastAddedItemId = null) } + } } diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkprepare/component/WalkPrepareBody.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkprepare/component/WalkPrepareBody.kt index efc8303c..5baf4923 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkprepare/component/WalkPrepareBody.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkprepare/component/WalkPrepareBody.kt @@ -12,21 +12,27 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf 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.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview @@ -38,6 +44,7 @@ import com.paw.key.presentation.ui.course.walkcourse.walkprepare.model.WalkPrepa import com.paw.key.presentation.ui.course.walkcourse.walkprepare.state.WalkPrepareState import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.launch @Composable fun WalkPrepareBody( @@ -45,8 +52,20 @@ fun WalkPrepareBody( addWalkItem : () -> Unit = {}, deleteWalkItem : (Int) -> Unit = {}, itemList : ImmutableList = persistentListOf(), + lastAddedItemId: Int? = null, + onFocusHandled: () -> Unit = {} ) { var selectedIds by remember { mutableStateOf(setOf()) } + val listState = rememberLazyListState() + val coroutineScope = rememberCoroutineScope() + + LaunchedEffect(itemList.size) { + if (itemList.isNotEmpty()) { + coroutineScope.launch { + listState.animateScrollToItem(itemList.lastIndex) + } + } + } Column ( modifier = modifier @@ -64,7 +83,9 @@ fun WalkPrepareBody( Spacer(modifier = Modifier.height(16.dp)) - LazyColumn { + LazyColumn ( + state = listState + ) { itemsIndexed( items = itemList, key = { _, item -> item.id } @@ -80,7 +101,9 @@ fun WalkPrepareBody( selectedIds - item.id } }, - deleteWalkItem = deleteWalkItem + deleteWalkItem = deleteWalkItem, + shouldFocus = lastAddedItemId == item.id, + onFocusHandled = onFocusHandled ) if (index < itemList.lastIndex) { @@ -123,8 +146,28 @@ private fun WalkPrepareItem( onCheckBoxClick: (Boolean) -> Unit, deleteWalkItem : (Int) -> Unit, itemModel: WalkPrepareItemModel, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, + shouldFocus: Boolean = false, + onFocusHandled: () -> Unit = {} ) { + val focusRequester = remember { FocusRequester() } + val keyboardController = LocalSoftwareKeyboardController.current + + val textStyle = when { + isSelected -> PawKeyTheme.typography.subButtonActive + .copy(color = PawKeyTheme.colors.defaultDark) + else -> PawKeyTheme.typography.subButtonDefault + .copy(color = PawKeyTheme.colors.defaultMiddle) + } + + LaunchedEffect(shouldFocus) { + if (shouldFocus) { + focusRequester.requestFocus() + keyboardController?.show() + onFocusHandled() + } + } + Row ( modifier = modifier .noRippleClickable { @@ -142,9 +185,7 @@ private fun WalkPrepareItem( BasicTextField( state = itemModel.walkItem, - textStyle = PawKeyTheme.typography.subButtonDefault.copy( - color = if (isSelected) PawKeyTheme.colors.background else PawKeyTheme.colors.defaultMiddle - ), + textStyle = textStyle, decorator = { innerTextField -> Box( contentAlignment = Alignment.CenterStart, @@ -160,7 +201,9 @@ private fun WalkPrepareItem( innerTextField() } }, - modifier = Modifier.weight(1f) + modifier = Modifier + .weight(1f) + .focusRequester(focusRequester) ) Spacer(modifier = Modifier.weight(1f)) @@ -184,7 +227,7 @@ private fun CustomCheckBox( modifier: Modifier = Modifier ) { val checkMarkTint = if (isSelected) { - PawKeyTheme.colors.primary + PawKeyTheme.colors.background } else { PawKeyTheme.colors.defaultMiddle } diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkprepare/component/WalkPrepareWeatherInfo.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkprepare/component/WalkPrepareWeatherInfo.kt index e77087e8..d81ca577 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkprepare/component/WalkPrepareWeatherInfo.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkprepare/component/WalkPrepareWeatherInfo.kt @@ -2,13 +2,15 @@ package com.paw.key.presentation.ui.course.walkcourse.walkprepare.component import androidx.compose.foundation.Image import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -25,46 +27,63 @@ import com.paw.key.core.designsystem.theme.PawKeyTheme @Composable fun WalkPrepareWeatherInfo( modifier: Modifier = Modifier, - title: String = "숨이 얼어붙어요...오늘은 나가지말아요", + title: String = "발이 차가워요.. 잠깐 다녀와요!", subTitle: String = "실외 금지! 실내 놀이로 대체", ) { - Box( + Row( modifier = modifier .fillMaxWidth() .clip(RoundedCornerShape(16.dp)) - .background(color = PawKeyTheme.colors.primary) + .background( + color = PawKeyTheme.colors.background, + shape = RoundedCornerShape(16.dp) + ) + .border( + width = 1.dp, + color = PawKeyTheme.colors.primary, + shape = RoundedCornerShape(16.dp) + ), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween ) { - Image( - painter = painterResource(R.drawable.img_walk_info), - contentDescription = null, - contentScale = ContentScale.Crop, - modifier = Modifier - .align(Alignment.BottomStart) - .padding(start = 20.dp) - .size(100.dp) - ) - Column( modifier = Modifier - .align(Alignment.CenterStart) - .fillMaxWidth() - .padding(start = 110.dp, end = 20.dp, top = 24.dp, bottom = 24.dp) + .padding(vertical = 16.dp) + .padding(start = 16.dp), + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.Center ) { - // Todo: body bold로 변경 + Text( + text = "오늘의 산책 TIP", + style = PawKeyTheme.typography.buttonSmall, + color = PawKeyTheme.colors.defaultMiddle, + ) + + Spacer(modifier = Modifier.height(2.dp)) + Text( text = title, - style = PawKeyTheme.typography.bodyActive, - color = PawKeyTheme.colors.background, + style = PawKeyTheme.typography.subTitle, + color = PawKeyTheme.colors.contents, ) - Spacer(modifier = Modifier.height(4.dp)) Text( text = subTitle, - style = PawKeyTheme.typography.subButtonDefault, - color = PawKeyTheme.colors.defaultBright + style = PawKeyTheme.typography.bodySmall, + color = PawKeyTheme.colors.contents ) } + + Spacer(modifier = Modifier.width(12.dp)) + + Image( + painter = painterResource(R.drawable.img_walk_info), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .padding(top = 8.dp, end = 16.dp) + ) } } @@ -72,6 +91,8 @@ fun WalkPrepareWeatherInfo( @Composable private fun WalkPrepareWeatherInfoPreview() { PawKeyTheme { - WalkPrepareWeatherInfo() + WalkPrepareWeatherInfo( + subTitle = "10분 내 짧은 산책 / 패딩과 신발 필수" + ) } } \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkprepare/state/WalkPrepareContract.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkprepare/state/WalkPrepareContract.kt index 797c204d..3e6ed08f 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkprepare/state/WalkPrepareContract.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/walkprepare/state/WalkPrepareContract.kt @@ -2,13 +2,13 @@ package com.paw.key.presentation.ui.course.walkcourse.walkprepare.state import androidx.compose.foundation.text.input.TextFieldState import com.paw.key.presentation.ui.course.walkcourse.walkprepare.model.WalkPrepareItemModel -import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList data class WalkPrepareState( val walkPrepareItemList: PersistentList = persistentListOf(), + val lastAddedItemId: Int? = null, ) { val dummyWalkPrepare = listOf( WalkPrepareItemModel(1, TextFieldState("")), From b436bde721b8fccd69bdde882fd1d8b2382f8dd6 Mon Sep 17 00:00:00 2001 From: sonms Date: Mon, 16 Feb 2026 01:53:12 +0900 Subject: [PATCH 41/47] =?UTF-8?q?feat/#154=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=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 --- .../component/walk}/WalkReviewInfoHolder.kt | 2 +- .../ui/course/walkreview/WalkReviewScreen.kt | 11 ++++------- .../course/walkreview/model/WalkReviewFilterModel.kt | 1 + .../walkreview/viewmodel/WalkReviewViewModel.kt | 12 +++++++----- 4 files changed, 13 insertions(+), 13 deletions(-) rename app/src/main/java/com/paw/key/{presentation/ui/course/walkreview/component => core/designsystem/component/walk}/WalkReviewInfoHolder.kt (96%) diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/component/WalkReviewInfoHolder.kt b/app/src/main/java/com/paw/key/core/designsystem/component/walk/WalkReviewInfoHolder.kt similarity index 96% rename from app/src/main/java/com/paw/key/presentation/ui/course/walkreview/component/WalkReviewInfoHolder.kt rename to app/src/main/java/com/paw/key/core/designsystem/component/walk/WalkReviewInfoHolder.kt index a7b84217..f341d784 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/component/WalkReviewInfoHolder.kt +++ b/app/src/main/java/com/paw/key/core/designsystem/component/walk/WalkReviewInfoHolder.kt @@ -1,4 +1,4 @@ -package com.paw.key.presentation.ui.course.walkreview.component +package com.paw.key.core.designsystem.component.walk import androidx.annotation.DrawableRes import androidx.compose.foundation.layout.Arrangement diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/WalkReviewScreen.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/WalkReviewScreen.kt index e121c895..32a9a69d 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/WalkReviewScreen.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/WalkReviewScreen.kt @@ -7,11 +7,9 @@ import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.PickVisualMediaRequest import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.background -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 @@ -35,12 +33,11 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.paw.key.R import com.paw.key.core.designsystem.component.DokiBorderButton import com.paw.key.core.designsystem.component.DokiButton -import com.paw.key.core.designsystem.component.InfoChip import com.paw.key.core.designsystem.component.TopBar import com.paw.key.core.designsystem.theme.PawKeyTheme import com.paw.key.presentation.ui.course.walkreview.component.WalkReviewDialog import com.paw.key.presentation.ui.course.walkreview.component.WalkReviewImageRow -import com.paw.key.presentation.ui.course.walkreview.component.WalkReviewInfoHolder +import com.paw.key.core.designsystem.component.walk.WalkReviewInfoHolder import com.paw.key.presentation.ui.course.walkreview.component.WalkReviewMultipleFilter import com.paw.key.presentation.ui.course.walkreview.component.WalkReviewSingleFilter import com.paw.key.presentation.ui.course.walkreview.state.WalkReviewState @@ -287,7 +284,7 @@ private fun WalkReviewScreen( ) { if (state.walkReviewTitle.isEmpty()) { Text( - text = "후기 제목을 입력해주세요", + text = "후기 제목을 14글자 이내로 입력해주세요", style = PawKeyTheme.typography.bodyDefault, color = PawKeyTheme.colors.defaultMiddle ) @@ -319,7 +316,7 @@ private fun WalkReviewScreen( ) { if (state.walkReviewContent.isEmpty()) { Text( - text = "산책에 대한 내용을 작성해주세요", + text = "산책에 대한 내용을 250자 이내로 작성해주세요", style = PawKeyTheme.typography.bodyDefault, color = PawKeyTheme.colors.defaultMiddle ) @@ -343,7 +340,7 @@ private fun WalkReviewScreen( DokiButton( text = "산책 기록 공유하기", - enabled = true, + enabled = state.walkReviewTitle.isNotEmpty() && state.walkReviewContent.isNotEmpty(), onClick = { onClickComplete(true) } diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/model/WalkReviewFilterModel.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/model/WalkReviewFilterModel.kt index 2ec5da32..9ff59b16 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/model/WalkReviewFilterModel.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/model/WalkReviewFilterModel.kt @@ -4,6 +4,7 @@ import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf data class WalkReviewFilterModel( + // Todo : 서버 내용으로 변경 val confusionSingleFilterList: ImmutableList = persistentListOf("적음", "평범", "많음"), val frequencySingleFilterList: ImmutableList = persistentListOf("교류 없음", "보통", "교류 활발"), diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/viewmodel/WalkReviewViewModel.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/viewmodel/WalkReviewViewModel.kt index 43102316..716ff797 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/viewmodel/WalkReviewViewModel.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/viewmodel/WalkReviewViewModel.kt @@ -31,18 +31,20 @@ class WalkReviewViewModel @Inject constructor( } } - fun updateReviewTitle(title : String) { + fun updateReviewTitle(title: String) { + val limitedTitle = title.take(14) + _state.update { - it.copy( - walkReviewTitle = title - ) + it.copy(walkReviewTitle = limitedTitle) } } fun updateReviewContent(content : String) { + val limitedContent = content.take(250) + _state.update { it.copy( - walkReviewContent = content + walkReviewContent = limitedContent ) } } From 62339bf514bc1ced84cb015a69a660d2429e2ebe Mon Sep 17 00:00:00 2001 From: sonms Date: Thu, 19 Feb 2026 23:57:28 +0900 Subject: [PATCH 42/47] =?UTF-8?q?delete/#154=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20=ED=8C=8C=EC=9D=BC=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../paw/key/core/util/PreferenceDataStore.kt | 348 ------------------ .../onboarding/OnboardingInfoRequest.kt | 48 --- .../onboarding/OnboardingInfoResponse.kt | 29 -- .../onboarding/OnboardingPetsResponse.kt | 68 ---- .../onboarding/OnboardingRegionResponse.kt | 47 --- .../paw/key/data/mapper/DummyUserMapper.kt | 28 -- .../data/remote/datasource/DummyDataSource.kt | 10 - .../datasource/OnboardingInfoDataSource.kt | 27 -- .../datasource/OnboardingPetsDataSource.kt | 10 - .../datasource/OnboardingRegionDataSource.kt | 11 - .../repositoryimpl/DummyRepositoryImpl.kt | 16 - .../OnboardingInfoRepositoryImpl.kt | 27 -- .../OnboardingRegionRepositoryImpl.kt | 20 - .../onboarding/OnboardingRepositoryImpl.kt | 22 -- .../com/paw/key/data/service/DummyService.kt | 15 - .../com/paw/key/data/service/RegionService.kt | 19 - .../onboarding/OnboardingInfoService.kt | 20 - .../onboarding/OnboardingPetsService.kt | 13 - .../onboarding/OnboardingRegionService.kt | 13 - .../paw/key/domain/model/entity/DummyUser.kt | 8 - .../entity/onboarding/OnboardingEntity.kt | 35 -- .../key/domain/repository/DummyRepository.kt | 7 - .../onboarding/OnboardingInfoRepository.kt | 14 - .../onboarding/OnboardingRegionRepository.kt | 7 - .../onboarding/OnboardingRepository.kt | 7 - .../key/presentation/ui/dummy/DummyScreen.kt | 146 -------- .../ui/dummy/component/DummyItem.kt | 73 ---- .../ui/dummy/navigation/DummyNavigation.kt | 36 -- .../ui/dummy/next/DummyNextNavigation.kt | 28 -- .../ui/dummy/next/DummyNextScreen.kt | 37 -- .../ui/dummy/state/DummyContract.kt | 19 - .../ui/dummy/viewmodel/DummyViewModel.kt | 57 --- .../ui/home/HomeLocationSettingScreen.kt | 173 --------- 33 files changed, 1438 deletions(-) delete mode 100644 app/src/main/java/com/paw/key/core/util/PreferenceDataStore.kt delete mode 100644 app/src/main/java/com/paw/key/data/dto/request/onboarding/OnboardingInfoRequest.kt delete mode 100644 app/src/main/java/com/paw/key/data/dto/response/onboarding/OnboardingInfoResponse.kt delete mode 100644 app/src/main/java/com/paw/key/data/dto/response/onboarding/OnboardingPetsResponse.kt delete mode 100644 app/src/main/java/com/paw/key/data/dto/response/onboarding/OnboardingRegionResponse.kt delete mode 100644 app/src/main/java/com/paw/key/data/mapper/DummyUserMapper.kt delete mode 100644 app/src/main/java/com/paw/key/data/remote/datasource/DummyDataSource.kt delete mode 100644 app/src/main/java/com/paw/key/data/remote/datasource/OnboardingInfoDataSource.kt delete mode 100644 app/src/main/java/com/paw/key/data/remote/datasource/OnboardingPetsDataSource.kt delete mode 100644 app/src/main/java/com/paw/key/data/remote/datasource/OnboardingRegionDataSource.kt delete mode 100644 app/src/main/java/com/paw/key/data/repositoryimpl/DummyRepositoryImpl.kt delete mode 100644 app/src/main/java/com/paw/key/data/repositoryimpl/onboarding/OnboardingInfoRepositoryImpl.kt delete mode 100644 app/src/main/java/com/paw/key/data/repositoryimpl/onboarding/OnboardingRegionRepositoryImpl.kt delete mode 100644 app/src/main/java/com/paw/key/data/repositoryimpl/onboarding/OnboardingRepositoryImpl.kt delete mode 100644 app/src/main/java/com/paw/key/data/service/DummyService.kt delete mode 100644 app/src/main/java/com/paw/key/data/service/RegionService.kt delete mode 100644 app/src/main/java/com/paw/key/data/service/onboarding/OnboardingInfoService.kt delete mode 100644 app/src/main/java/com/paw/key/data/service/onboarding/OnboardingPetsService.kt delete mode 100644 app/src/main/java/com/paw/key/data/service/onboarding/OnboardingRegionService.kt delete mode 100644 app/src/main/java/com/paw/key/domain/model/entity/DummyUser.kt delete mode 100644 app/src/main/java/com/paw/key/domain/model/entity/onboarding/OnboardingEntity.kt delete mode 100644 app/src/main/java/com/paw/key/domain/repository/DummyRepository.kt delete mode 100644 app/src/main/java/com/paw/key/domain/repository/onboarding/OnboardingInfoRepository.kt delete mode 100644 app/src/main/java/com/paw/key/domain/repository/onboarding/OnboardingRegionRepository.kt delete mode 100644 app/src/main/java/com/paw/key/domain/repository/onboarding/OnboardingRepository.kt delete mode 100644 app/src/main/java/com/paw/key/presentation/ui/dummy/DummyScreen.kt delete mode 100644 app/src/main/java/com/paw/key/presentation/ui/dummy/component/DummyItem.kt delete mode 100644 app/src/main/java/com/paw/key/presentation/ui/dummy/navigation/DummyNavigation.kt delete mode 100644 app/src/main/java/com/paw/key/presentation/ui/dummy/next/DummyNextNavigation.kt delete mode 100644 app/src/main/java/com/paw/key/presentation/ui/dummy/next/DummyNextScreen.kt delete mode 100644 app/src/main/java/com/paw/key/presentation/ui/dummy/state/DummyContract.kt delete mode 100644 app/src/main/java/com/paw/key/presentation/ui/dummy/viewmodel/DummyViewModel.kt delete mode 100644 app/src/main/java/com/paw/key/presentation/ui/home/HomeLocationSettingScreen.kt diff --git a/app/src/main/java/com/paw/key/core/util/PreferenceDataStore.kt b/app/src/main/java/com/paw/key/core/util/PreferenceDataStore.kt deleted file mode 100644 index 3a16757a..00000000 --- a/app/src/main/java/com/paw/key/core/util/PreferenceDataStore.kt +++ /dev/null @@ -1,348 +0,0 @@ -package com.paw.key.core.util - -import android.content.Context -import android.content.SharedPreferences -import androidx.datastore.preferences.core.edit -import androidx.datastore.preferences.core.floatPreferencesKey -import androidx.datastore.preferences.core.intPreferencesKey -import androidx.datastore.preferences.core.longPreferencesKey -import androidx.datastore.preferences.core.stringPreferencesKey -import androidx.datastore.preferences.preferencesDataStore -import androidx.security.crypto.EncryptedSharedPreferences -import androidx.security.crypto.MasterKey -import com.naver.maps.geometry.LatLng -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map - -private val Context.summaryStore by preferencesDataStore(name = "summaryStore_pref") - -private val POINTS_KEY = stringPreferencesKey("points") -private val TOTAL_DISTANCE_KEY = floatPreferencesKey("total_distance") -private val TOTAL_TIME_KEY = longPreferencesKey("total_time") -private val TOTAL_STEPS_KEY = intPreferencesKey("total_steps") - -private val LOGIN_EMAIL_KEY = stringPreferencesKey("login_email") -private val LOGIN_PASSWORD_KEY = stringPreferencesKey("login_password") -private val USER_ID_KEY = intPreferencesKey("user_id") -private val USER_NAME_KEY = stringPreferencesKey("user_name") -private val PET_ID_KEY = intPreferencesKey("pet_id") -private val PET_NAME_KEY = stringPreferencesKey("pet_name") - -// 위치 정보를 위한 새로운 키들 -private val SELECTED_GU_ID_KEY = intPreferencesKey("selected_gu_id") -private val SELECTED_DONG_ID_KEY = intPreferencesKey("selected_dong_id") -private val SELECTED_GU_NAME_KEY = stringPreferencesKey("selected_gu_name") -private val SELECTED_DONG_NAME_KEY = stringPreferencesKey("selected_dong_name") -private val ACTIVE_REGION_KEY = stringPreferencesKey("active_region") - -private fun List.toPreferenceString(): String = - joinToString(";") { "${it.latitude},${it.longitude}" } - -object PreferenceDataStore { - - private lateinit var appContext: Context - - fun init(context: Context) { - appContext = context.applicationContext - } - - private val summaryStore - get() = appContext.summaryStore - - suspend fun saveWalkSummary( - points: List, - totalDistance: Float, - totalTime: Long, - totalSteps: Int, - ) { - summaryStore.edit { preferences -> - preferences[POINTS_KEY] = points.toPreferenceString() - preferences[TOTAL_DISTANCE_KEY] = totalDistance - preferences[TOTAL_TIME_KEY] = totalTime - preferences[TOTAL_STEPS_KEY] = totalSteps - } - } - - fun getTotalDistance(): Flow = summaryStore.data.map { - it[TOTAL_DISTANCE_KEY] ?: 0f - } - - fun getTotalTime(): Flow = summaryStore.data.map { - it[TOTAL_TIME_KEY] ?: 0L - } - - fun getTotalSteps(): Flow = summaryStore.data.map { - it[TOTAL_STEPS_KEY] ?: 0 - } - - suspend fun clearWalkSummary() { - summaryStore.edit { - it.remove(POINTS_KEY) - it.remove(TOTAL_DISTANCE_KEY) - it.remove(TOTAL_TIME_KEY) - it.remove(TOTAL_STEPS_KEY) - } - } - - suspend fun saveLoginInfo(email: String, password: String) { - summaryStore.edit { - it[LOGIN_EMAIL_KEY] = email - it[LOGIN_PASSWORD_KEY] = password - } - } - - fun getLoginEmail(): Flow = summaryStore.data.map { - it[LOGIN_EMAIL_KEY] ?: "" - } - - fun getLoginPassword(): Flow = summaryStore.data.map { - it[LOGIN_PASSWORD_KEY] ?: "" - } - - data class LoginInfo(val email: String, val password: String) - - fun getLoginInfo(): Flow = summaryStore.data.map { - LoginInfo( - email = it[LOGIN_EMAIL_KEY] ?: "", - password = it[LOGIN_PASSWORD_KEY] ?: "" - ) - } - - suspend fun clearLoginInfo() { - summaryStore.edit { - it.remove(LOGIN_EMAIL_KEY) - it.remove(LOGIN_PASSWORD_KEY) - } - } - - suspend fun saveUserInfo( - userId: Int, - userName: String, - petId: Int, - petName: String, - ) { - summaryStore.edit { - it[USER_ID_KEY] = userId - it[USER_NAME_KEY] = userName - it[PET_ID_KEY] = petId - it[PET_NAME_KEY] = petName - } - } - - fun getUserId(): Flow = summaryStore.data.map { - it[USER_ID_KEY] ?: 43 - } - - fun getUserName(): Flow = summaryStore.data.map { - it[USER_NAME_KEY] ?: "" - } - - fun getPetId(): Flow = summaryStore.data.map { - it[PET_ID_KEY] ?: 0 - } - - fun getPetName(): Flow = summaryStore.data.map { - it[PET_NAME_KEY] ?: "" - } - - data class UserInfo( - val userId: Int, - val userName: String, - val petId: Int, - val petName: String, - ) - - fun getUserInfo(): Flow = summaryStore.data.map { - UserInfo( - userId = it[USER_ID_KEY] ?: 0, - userName = it[USER_NAME_KEY] ?: "", - petId = it[PET_ID_KEY] ?: 0, - petName = it[PET_NAME_KEY] ?: "" - ) - } - - suspend fun clearUserInfo() { - summaryStore.edit { - it.remove(USER_ID_KEY) - it.remove(USER_NAME_KEY) - it.remove(PET_ID_KEY) - it.remove(PET_NAME_KEY) - } - } - - // ===== 위치 정보 관련 새로운 함수들 ===== - - /** - * 선택된 위치 정보를 저장합니다 - */ - suspend fun saveLocationInfo( - guId: Int, - dongId: Int, - guName: String, - dongName: String, - ) { - summaryStore.edit { preferences -> - preferences[SELECTED_GU_ID_KEY] = guId - preferences[SELECTED_DONG_ID_KEY] = dongId - preferences[SELECTED_GU_NAME_KEY] = guName - preferences[SELECTED_DONG_NAME_KEY] = dongName - } - } - - /** - * 구 정보만 저장합니다 (동 선택 전) - */ - suspend fun saveGuInfo(guId: Int, guName: String) { - summaryStore.edit { preferences -> - preferences[SELECTED_GU_ID_KEY] = guId - preferences[SELECTED_GU_NAME_KEY] = guName - // 구가 변경되면 기존 동 정보 초기화 - preferences.remove(SELECTED_DONG_ID_KEY) - preferences.remove(SELECTED_DONG_NAME_KEY) - } - } - - /** - * 동 정보만 저장합니다 - */ - suspend fun saveDongInfo(dongId: Int, dongName: String) { - summaryStore.edit { preferences -> - preferences[SELECTED_DONG_ID_KEY] = dongId - preferences[SELECTED_DONG_NAME_KEY] = dongName - } - } - - /** - * 활동 지역 정보를 저장합니다 (activeRegion) - */ - suspend fun saveActiveRegion(activeRegion: String) { - summaryStore.edit { preferences -> - preferences[ACTIVE_REGION_KEY] = activeRegion - } - } - - // 개별 조회 함수들 - fun getSelectedGuId(): Flow = summaryStore.data.map { - it[SELECTED_GU_ID_KEY] ?: 0 - } - - fun getSelectedDongId(): Flow = summaryStore.data.map { - it[SELECTED_DONG_ID_KEY] ?: 0 - } - - fun getSelectedGuName(): Flow = summaryStore.data.map { - it[SELECTED_GU_NAME_KEY] ?: "" - } - - fun getSelectedDongName(): Flow = summaryStore.data.map { - it[SELECTED_DONG_NAME_KEY] ?: "" - } - - fun getActiveRegion(): Flow = summaryStore.data.map { - it[ACTIVE_REGION_KEY] ?: "" - } - - // 위치 정보 통합 조회 - data class LocationInfo( - val guId: Int, - val dongId: Int, - val guName: String, - val dongName: String, - val activeRegion: String, - ) { - val displayLocation: String - get() = if (guName.isNotEmpty() && dongName.isNotEmpty()) { - "$guName $dongName" - } else if (guName.isNotEmpty()) { - guName - } else { - "위치를 선택해주세요" - } - - val isLocationSelected: Boolean - get() = guId != 0 && dongId != 0 - } - - /** - * 모든 위치 정보를 한번에 조회합니다 - */ - fun getLocationInfo(): Flow = summaryStore.data.map { preferences -> - LocationInfo( - guId = preferences[SELECTED_GU_ID_KEY] ?: 0, - dongId = preferences[SELECTED_DONG_ID_KEY] ?: 0, - guName = preferences[SELECTED_GU_NAME_KEY] ?: "", - dongName = preferences[SELECTED_DONG_NAME_KEY] ?: "", - activeRegion = preferences[ACTIVE_REGION_KEY] ?: "" - ) - } - - /** - * 위치 정보를 초기화합니다 - */ - suspend fun clearLocationInfo() { - summaryStore.edit { preferences -> - preferences.remove(SELECTED_GU_ID_KEY) - preferences.remove(SELECTED_DONG_ID_KEY) - preferences.remove(SELECTED_GU_NAME_KEY) - preferences.remove(SELECTED_DONG_NAME_KEY) - preferences.remove(ACTIVE_REGION_KEY) - } - } -} - -object UserDataStore { - private val ACCESS_TOKEN = "ACCESS_TOKEN" - private val REFRESH_TOKEN = "REFRESH_TOKEN" - private val PREFERENCES_NAME = "user_preferences" - - private fun getSharedPreferences(context: Context): SharedPreferences { - val masterKey = MasterKey.Builder(context) - .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) - .build() - - return EncryptedSharedPreferences.create( - context, - PREFERENCES_NAME, - masterKey, - EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, - EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM - ) - } - - fun saveAcessToken(context: Context, token: String) { - val sharedPreferences = getSharedPreferences(context) - with(sharedPreferences.edit()) { - putString(ACCESS_TOKEN, token) - commit() - } - } - - fun saveRefreshToken(context: Context, token: String) { - val sharedPreferences = getSharedPreferences(context) - with(sharedPreferences.edit()) { - putString(REFRESH_TOKEN, token) - commit() - } - } - - fun getAccessToken(context: Context): String { - val sharedPreferences = getSharedPreferences(context) - return sharedPreferences.getString(ACCESS_TOKEN, "") ?: "" - } - - fun getRefreshToken(context: Context): String { - val sharedPreferences = getSharedPreferences(context) - return sharedPreferences.getString(REFRESH_TOKEN, "") ?: "" - } - - fun removeToken(context: Context) { - val sharedPreferences = getSharedPreferences(context) - with(sharedPreferences.edit()) { - remove(ACCESS_TOKEN) - remove(REFRESH_TOKEN) - commit() - } - } -} - - diff --git a/app/src/main/java/com/paw/key/data/dto/request/onboarding/OnboardingInfoRequest.kt b/app/src/main/java/com/paw/key/data/dto/request/onboarding/OnboardingInfoRequest.kt deleted file mode 100644 index 427ada0f..00000000 --- a/app/src/main/java/com/paw/key/data/dto/request/onboarding/OnboardingInfoRequest.kt +++ /dev/null @@ -1,48 +0,0 @@ -package com.paw.key.data.dto.request.onboarding - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -data class OnboardingInfoRequest( - @SerialName("loginId") - val loginId: String, - @SerialName("password") - val password: String, - @SerialName("name") - val name: String, - @SerialName("gender") - val gender: String, - @SerialName("age") - val age: Int, - @SerialName("regionId") - val regionId: Int, - @SerialName("pet") - val pet: PetInfoDto -) - -@Serializable -data class PetInfoDto( - @SerialName("name") - val name: String, - @SerialName("gender") - val gender: String, - @SerialName("age") - val age: Int, - @SerialName("isAgeKnown") - val isAgeKnown: Boolean, - @SerialName("isNeutered") - val isNeutered: Boolean, - @SerialName("breed") - val breed: String, - @SerialName("petTraits") - val petTraits: List -) - -@Serializable -data class PetTraitDto( - @SerialName("traitCategoryId") - val traitCategoryId: Int, - @SerialName("traitOptionId") - val traitOptionId: Int -) diff --git a/app/src/main/java/com/paw/key/data/dto/response/onboarding/OnboardingInfoResponse.kt b/app/src/main/java/com/paw/key/data/dto/response/onboarding/OnboardingInfoResponse.kt deleted file mode 100644 index 6a9a8406..00000000 --- a/app/src/main/java/com/paw/key/data/dto/response/onboarding/OnboardingInfoResponse.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.paw.key.data.dto.response.onboarding - -import com.paw.key.domain.model.entity.onboarding.OnboardingInfo -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -data class OnboardingInfoResponse( - @SerialName("userId") - val userId: Int, - @SerialName("userName") - val userName: String, - @SerialName("loginId") - val loginId: String, - @SerialName("petId") - val petId: Int, - @SerialName("petName") - val petName: String -) - -fun OnboardingInfoResponse.toDomain(): OnboardingInfo { - return OnboardingInfo( - userId = this.userId, - userName = this.userName, - loginId = this.loginId, - petId = this.petId, - petName = this.petName - ) -} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/data/dto/response/onboarding/OnboardingPetsResponse.kt b/app/src/main/java/com/paw/key/data/dto/response/onboarding/OnboardingPetsResponse.kt deleted file mode 100644 index 0fdef6db..00000000 --- a/app/src/main/java/com/paw/key/data/dto/response/onboarding/OnboardingPetsResponse.kt +++ /dev/null @@ -1,68 +0,0 @@ -package com.paw.key.data.dto.response - -import com.paw.key.domain.model.entity.onboarding.OnboardingPets -import com.paw.key.domain.model.entity.onboarding.PetTraitCategory -import com.paw.key.domain.model.entity.onboarding.PetTraitCategoryOption -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -data class OnboardingPetsResponse( - @SerialName("code") - val code: String, - - @SerialName("message") - val message: String, - - @SerialName("data") - val data: PetTraitCategoryDataDto -) - -@Serializable -data class PetTraitCategoryDataDto( - @SerialName("petTraitCategoryList") - val petTraitCategoryList: List -) - -@Serializable -data class PetTraitCategoryDto( - @SerialName("petTraitCategoryId") - val petTraitCategoryId: Int, - - @SerialName("petTraitCategoryName") - val petTraitCategoryName: String, - - @SerialName("petTraitCategoryOptions") - val petTraitCategoryOptions: List -) - -@Serializable -data class PetTraitCategoryOptionDto( - @SerialName("petTraitCategoryOptionId") - val petTraitCategoryOptionId: Int, - - @SerialName("petTraitCategoryOptionText") - val petTraitCategoryOptionText: String -) - - -fun OnboardingPetsResponse.toDomain(): OnboardingPets { - return OnboardingPets( - petTraitCategoryList = this.data.petTraitCategoryList.map { it.toDomain() } - ) -} - -fun PetTraitCategoryDto.toDomain(): PetTraitCategory { - return PetTraitCategory( - petTraitCategoryId = this.petTraitCategoryId, - petTraitCategoryName = this.petTraitCategoryName, - petTraitCategoryOptions = this.petTraitCategoryOptions.map { it.toDomain() } - ) -} - -fun PetTraitCategoryOptionDto.toDomain(): PetTraitCategoryOption { - return PetTraitCategoryOption( - petTraitCategoryOptionId = this.petTraitCategoryOptionId, - petTraitCategoryOptionText = this.petTraitCategoryOptionText - ) -} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/data/dto/response/onboarding/OnboardingRegionResponse.kt b/app/src/main/java/com/paw/key/data/dto/response/onboarding/OnboardingRegionResponse.kt deleted file mode 100644 index a7aef12a..00000000 --- a/app/src/main/java/com/paw/key/data/dto/response/onboarding/OnboardingRegionResponse.kt +++ /dev/null @@ -1,47 +0,0 @@ -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -data class DistrictResponse( - @SerialName("code") - val code: String, - - @SerialName("message") - val message: String, - - @SerialName("data") - val data: DistrictDataDto -) - -@Serializable -data class DistrictDataDto( - @SerialName("districtDtos") - val districtDtos: List -) - -@Serializable -data class DistrictDto( - @SerialName("gu") - val gu: GuDto, - - @SerialName("dongs") - val dongs: List -) - -@Serializable -data class GuDto( - @SerialName("id") - val id: Int, - - @SerialName("name") - val name: String -) - -@Serializable -data class DongDto( - @SerialName("id") - val id: Int, - - @SerialName("name") - val name: String -) \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/data/mapper/DummyUserMapper.kt b/app/src/main/java/com/paw/key/data/mapper/DummyUserMapper.kt deleted file mode 100644 index 0d80142e..00000000 --- a/app/src/main/java/com/paw/key/data/mapper/DummyUserMapper.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.paw.key.data.mapper - -import com.paw.key.data.dto.response.DummyResponseDto -import com.paw.key.domain.model.entity.DummyUser -import javax.inject.Inject - -class DummyUserMapper @Inject constructor() { - //dto -> entity : 서버에서 받아온 데이터를 맵핑할 때 - fun mapDtoToEntity(dto : DummyResponseDto) : DummyUser { - return DummyUser( - profile = dto.avatar, - firstName = dto.firstName, - id = dto.id, - lastName = dto.lastName, - ) - } - - //entity -> dto : 서버에 데이터 보낼 때 - fun mapEntityToDto(entity : DummyUser) : DummyResponseDto { - return DummyResponseDto( - avatar = entity.profile, - firstName = entity.firstName, - id = entity.id, - lastName = entity.lastName, - email = "" - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/data/remote/datasource/DummyDataSource.kt b/app/src/main/java/com/paw/key/data/remote/datasource/DummyDataSource.kt deleted file mode 100644 index 4b038259..00000000 --- a/app/src/main/java/com/paw/key/data/remote/datasource/DummyDataSource.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.paw.key.data.remote.datasource - -import com.paw.key.data.service.DummyService -import javax.inject.Inject - -class DummyDataSource @Inject constructor ( - private val dummyService: DummyService -) { - suspend fun getDummyList() = dummyService.getDummyLists() -} diff --git a/app/src/main/java/com/paw/key/data/remote/datasource/OnboardingInfoDataSource.kt b/app/src/main/java/com/paw/key/data/remote/datasource/OnboardingInfoDataSource.kt deleted file mode 100644 index c8259f31..00000000 --- a/app/src/main/java/com/paw/key/data/remote/datasource/OnboardingInfoDataSource.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.paw.key.data.remote.datasource - -import com.paw.key.data.dto.request.onboarding.OnboardingInfoRequest -import com.paw.key.data.dto.response.BaseResponse -import com.paw.key.data.dto.response.onboarding.OnboardingInfoResponse -import com.paw.key.data.service.onboarding.OnboardingInfoService -import com.google.gson.Gson -import okhttp3.MediaType.Companion.toMediaType -import okhttp3.MultipartBody -import okhttp3.RequestBody.Companion.toRequestBody -import javax.inject.Inject - -class OnboardingInfoDataSource @Inject constructor( - private val service: OnboardingInfoService -) { - suspend fun postOnboardingInfo( - userId: Int, - file: MultipartBody.Part, - onboardingInfoRequest: OnboardingInfoRequest - ): BaseResponse { - val gson = Gson() - val jsonString = gson.toJson(onboardingInfoRequest) - val requestBody = jsonString.toRequestBody("application/json".toMediaType()) - - return service.postInfo(userId, requestBody, file) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/data/remote/datasource/OnboardingPetsDataSource.kt b/app/src/main/java/com/paw/key/data/remote/datasource/OnboardingPetsDataSource.kt deleted file mode 100644 index 801ac6a3..00000000 --- a/app/src/main/java/com/paw/key/data/remote/datasource/OnboardingPetsDataSource.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.paw.key.data.remote.datasource - -import com.paw.key.data.service.onboarding.OnboardingPetsService -import javax.inject.Inject - -class OnboardingPetsDataSource @Inject constructor( - private val service: OnboardingPetsService -) { - suspend fun getOnboardingPets(userId: Int) = service.getPetsCaegories(userId) -} diff --git a/app/src/main/java/com/paw/key/data/remote/datasource/OnboardingRegionDataSource.kt b/app/src/main/java/com/paw/key/data/remote/datasource/OnboardingRegionDataSource.kt deleted file mode 100644 index 0d4dd335..00000000 --- a/app/src/main/java/com/paw/key/data/remote/datasource/OnboardingRegionDataSource.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.paw.key.data.remote.datasource - -import com.paw.key.data.service.onboarding.OnboardingRegionService -import javax.inject.Inject - -class OnboardingRegionDataSource @Inject constructor( - private val service: OnboardingRegionService -) { - suspend fun getOnboardingRegion(userId: Int) = service.getRegion(userId) -} - diff --git a/app/src/main/java/com/paw/key/data/repositoryimpl/DummyRepositoryImpl.kt b/app/src/main/java/com/paw/key/data/repositoryimpl/DummyRepositoryImpl.kt deleted file mode 100644 index 926957f5..00000000 --- a/app/src/main/java/com/paw/key/data/repositoryimpl/DummyRepositoryImpl.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.paw.key.data.repositoryimpl - -import com.paw.key.data.mapper.DummyUserMapper -import com.paw.key.data.remote.datasource.DummyDataSource -import com.paw.key.domain.model.entity.DummyUser -import com.paw.key.domain.repository.DummyRepository -import javax.inject.Inject - -class DummyRepositoryImpl @Inject constructor( - private val dummyDataSource: DummyDataSource, - private val mapper: DummyUserMapper -): DummyRepository { - override suspend fun getDummyList(): Result> = runCatching { - dummyDataSource.getDummyList().data.map { mapper.mapDtoToEntity(it) } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/data/repositoryimpl/onboarding/OnboardingInfoRepositoryImpl.kt b/app/src/main/java/com/paw/key/data/repositoryimpl/onboarding/OnboardingInfoRepositoryImpl.kt deleted file mode 100644 index b3233ae4..00000000 --- a/app/src/main/java/com/paw/key/data/repositoryimpl/onboarding/OnboardingInfoRepositoryImpl.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.paw.key.data.repositoryimpl.onboarding - -import com.paw.key.data.dto.request.onboarding.OnboardingInfoRequest -import com.paw.key.data.dto.response.BaseResponse -import com.paw.key.data.dto.response.onboarding.OnboardingInfoResponse -import com.paw.key.data.remote.datasource.OnboardingInfoDataSource -import com.paw.key.domain.repository.onboarding.OnboardingInfoRepository -import okhttp3.MultipartBody -import javax.inject.Inject - -class OnboardingInfoRepositoryImpl @Inject constructor( - private val dataSource: OnboardingInfoDataSource, -) : OnboardingInfoRepository { - - override suspend fun postOnboardingInfo( - userId: Int, - image: MultipartBody.Part, - onboardingInfoRequest: OnboardingInfoRequest - ): Result> = runCatching { - val response = dataSource.postOnboardingInfo(userId, image, onboardingInfoRequest) - if (response.code == "S000") { - response - } else { - throw Exception(response.message) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/data/repositoryimpl/onboarding/OnboardingRegionRepositoryImpl.kt b/app/src/main/java/com/paw/key/data/repositoryimpl/onboarding/OnboardingRegionRepositoryImpl.kt deleted file mode 100644 index 29b0393e..00000000 --- a/app/src/main/java/com/paw/key/data/repositoryimpl/onboarding/OnboardingRegionRepositoryImpl.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.paw.key.data.repositoryimpl.onboarding - -import DistrictResponse -import com.paw.key.data.remote.datasource.OnboardingRegionDataSource -import com.paw.key.domain.repository.onboarding.OnboardingRegionRepository -import javax.inject.Inject - -class OnboardingRegionRepositoryImpl @Inject constructor( - private val dataSource: OnboardingRegionDataSource, -) : OnboardingRegionRepository { - - override suspend fun getOnboardingRegion(userId: Int): Result = runCatching { - val response = dataSource.getOnboardingRegion(userId) - if (response.code == "S000") { - response - } else { - throw Exception(response.message) - } - } -} diff --git a/app/src/main/java/com/paw/key/data/repositoryimpl/onboarding/OnboardingRepositoryImpl.kt b/app/src/main/java/com/paw/key/data/repositoryimpl/onboarding/OnboardingRepositoryImpl.kt deleted file mode 100644 index 0263f91a..00000000 --- a/app/src/main/java/com/paw/key/data/repositoryimpl/onboarding/OnboardingRepositoryImpl.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.paw.key.data.repositoryimpl.onboarding - -import com.paw.key.data.dto.response.OnboardingPetsResponse -import com.paw.key.data.remote.datasource.OnboardingPetsDataSource -import com.paw.key.domain.repository.onboarding.OnboardingRepository -import javax.inject.Inject - -class OnboardingRepositoryImpl @Inject constructor( - private val dataSource: OnboardingPetsDataSource, -) : OnboardingRepository { - - override suspend fun getOnboardingPets(userId: Int): Result { - return runCatching { - val response = dataSource.getOnboardingPets(userId) - if (response.code == "S000") { - response - } else { - throw Exception(response.message) - } - } - } -} diff --git a/app/src/main/java/com/paw/key/data/service/DummyService.kt b/app/src/main/java/com/paw/key/data/service/DummyService.kt deleted file mode 100644 index 5ffdb72b..00000000 --- a/app/src/main/java/com/paw/key/data/service/DummyService.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.paw.key.data.service - -import com.paw.key.data.dto.response.DummyBaseResponse -import com.paw.key.data.dto.response.DummyResponseDto -import retrofit2.http.GET -import retrofit2.http.Header -import retrofit2.http.Query - -interface DummyService { - @GET("api/users") - suspend fun getDummyLists( - @Header("x-api-key") apiKey: String = "reqres-free-v1", - @Query("page") page: Int = 2 - ): DummyBaseResponse -} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/data/service/RegionService.kt b/app/src/main/java/com/paw/key/data/service/RegionService.kt deleted file mode 100644 index ad3e1d3c..00000000 --- a/app/src/main/java/com/paw/key/data/service/RegionService.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.paw.key.data.service - -import DistrictDataDto -import com.paw.key.data.dto.response.BaseResponse -import com.paw.key.data.dto.response.region.RegionResponseDto -import retrofit2.http.GET -import retrofit2.http.Header -import retrofit2.http.Path - -interface RegionService { - @GET("regions/{regionId}/geometry") - suspend fun getRegionGeometry( - @Header("X-USER-ID") userId: Int, - @Path("regionId") regionId: Int, - ): BaseResponse - - @GET("regions") - suspend fun getRegionsList(): BaseResponse -} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/data/service/onboarding/OnboardingInfoService.kt b/app/src/main/java/com/paw/key/data/service/onboarding/OnboardingInfoService.kt deleted file mode 100644 index ccc77218..00000000 --- a/app/src/main/java/com/paw/key/data/service/onboarding/OnboardingInfoService.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.paw.key.data.service.onboarding - -import com.paw.key.data.dto.response.BaseResponse -import com.paw.key.data.dto.response.onboarding.OnboardingInfoResponse -import okhttp3.MultipartBody -import okhttp3.RequestBody -import retrofit2.http.Header -import retrofit2.http.Multipart -import retrofit2.http.POST -import retrofit2.http.Part - -interface OnboardingInfoService { - @Multipart - @POST("users") - suspend fun postInfo( - @Header("X-USER-ID") userId: Int, - @Part("data") data: RequestBody, - @Part petProfile: MultipartBody.Part - ): BaseResponse -} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/data/service/onboarding/OnboardingPetsService.kt b/app/src/main/java/com/paw/key/data/service/onboarding/OnboardingPetsService.kt deleted file mode 100644 index 95e1541f..00000000 --- a/app/src/main/java/com/paw/key/data/service/onboarding/OnboardingPetsService.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.paw.key.data.service.onboarding - - -import com.paw.key.data.dto.response.OnboardingPetsResponse -import retrofit2.http.GET -import retrofit2.http.Header - -interface OnboardingPetsService { - @GET("pets/traits/categories") - suspend fun getPetsCaegories( - @Header("X-USER-ID") userId: Int, - ): OnboardingPetsResponse -} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/data/service/onboarding/OnboardingRegionService.kt b/app/src/main/java/com/paw/key/data/service/onboarding/OnboardingRegionService.kt deleted file mode 100644 index f9a94312..00000000 --- a/app/src/main/java/com/paw/key/data/service/onboarding/OnboardingRegionService.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.paw.key.data.service.onboarding - -import DistrictResponse -import retrofit2.http.GET -import retrofit2.http.Header - - -interface OnboardingRegionService { - @GET("regions") - suspend fun getRegion( - @Header("X-USER-ID") userId: Int, - ): DistrictResponse -} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/domain/model/entity/DummyUser.kt b/app/src/main/java/com/paw/key/domain/model/entity/DummyUser.kt deleted file mode 100644 index 7cdd9b6a..00000000 --- a/app/src/main/java/com/paw/key/domain/model/entity/DummyUser.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.paw.key.domain.model.entity - -data class DummyUser( - val profile: String, - val firstName: String, - val id: Int, - val lastName: String, -) diff --git a/app/src/main/java/com/paw/key/domain/model/entity/onboarding/OnboardingEntity.kt b/app/src/main/java/com/paw/key/domain/model/entity/onboarding/OnboardingEntity.kt deleted file mode 100644 index 8cf30fc5..00000000 --- a/app/src/main/java/com/paw/key/domain/model/entity/onboarding/OnboardingEntity.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.paw.key.domain.model.entity.onboarding - -import com.paw.key.domain.model.entity.signup.DistrictEntity - -data class OnboardingInfo( - val userId: Int, - val userName: String, - val loginId: String, - val petId: Int, - val petName: String -) - -data class OnboardingPets( - val petTraitCategoryList: List -) - -data class PetTraitCategory( - val petTraitCategoryId: Int, - val petTraitCategoryName: String, - val petTraitCategoryOptions: List -) - -data class PetTraitCategoryOption( - val petTraitCategoryOptionId: Int, - val petTraitCategoryOptionText: String -) - -data class OnboardingRegion( - val districtList: List -) - -data class PetTraitDto( - val traitCategoryId: Int, - val traitOptionId: List -) diff --git a/app/src/main/java/com/paw/key/domain/repository/DummyRepository.kt b/app/src/main/java/com/paw/key/domain/repository/DummyRepository.kt deleted file mode 100644 index 7ce8d4d9..00000000 --- a/app/src/main/java/com/paw/key/domain/repository/DummyRepository.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.paw.key.domain.repository - -import com.paw.key.domain.model.entity.DummyUser - -interface DummyRepository { - suspend fun getDummyList(): Result> -} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/domain/repository/onboarding/OnboardingInfoRepository.kt b/app/src/main/java/com/paw/key/domain/repository/onboarding/OnboardingInfoRepository.kt deleted file mode 100644 index ce18265c..00000000 --- a/app/src/main/java/com/paw/key/domain/repository/onboarding/OnboardingInfoRepository.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.paw.key.domain.repository.onboarding - -import com.paw.key.data.dto.request.onboarding.OnboardingInfoRequest -import com.paw.key.data.dto.response.BaseResponse -import com.paw.key.data.dto.response.onboarding.OnboardingInfoResponse -import okhttp3.MultipartBody - -interface OnboardingInfoRepository { - suspend fun postOnboardingInfo( - userId: Int, - image: MultipartBody.Part, - onboardingInfoRequest: OnboardingInfoRequest - ): Result> -} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/domain/repository/onboarding/OnboardingRegionRepository.kt b/app/src/main/java/com/paw/key/domain/repository/onboarding/OnboardingRegionRepository.kt deleted file mode 100644 index 3d5479ec..00000000 --- a/app/src/main/java/com/paw/key/domain/repository/onboarding/OnboardingRegionRepository.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.paw.key.domain.repository.onboarding - -import DistrictResponse - -interface OnboardingRegionRepository { - suspend fun getOnboardingRegion(userId: Int): Result -} diff --git a/app/src/main/java/com/paw/key/domain/repository/onboarding/OnboardingRepository.kt b/app/src/main/java/com/paw/key/domain/repository/onboarding/OnboardingRepository.kt deleted file mode 100644 index 23116f97..00000000 --- a/app/src/main/java/com/paw/key/domain/repository/onboarding/OnboardingRepository.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.paw.key.domain.repository.onboarding - -import com.paw.key.data.dto.response.OnboardingPetsResponse - -interface OnboardingRepository { - suspend fun getOnboardingPets(userId: Int): Result -} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/dummy/DummyScreen.kt b/app/src/main/java/com/paw/key/presentation/ui/dummy/DummyScreen.kt deleted file mode 100644 index c2538f0c..00000000 --- a/app/src/main/java/com/paw/key/presentation/ui/dummy/DummyScreen.kt +++ /dev/null @@ -1,146 +0,0 @@ -package com.paw.key.presentation.ui.dummy - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -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.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.sp -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.compose.LocalLifecycleOwner -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.flowWithLifecycle -import com.paw.key.presentation.ui.dummy.viewmodel.DummyViewModel -import kotlinx.collections.immutable.PersistentList -import com.paw.key.presentation.ui.dummy.state.DummyContract.DummySideEffect -import com.paw.key.R -import com.paw.key.core.designsystem.theme.PawKeyTheme -import com.paw.key.core.util.UiState -import com.paw.key.core.extension.noRippleClickable -import com.paw.key.domain.model.entity.DummyUser -import com.paw.key.presentation.ui.dummy.component.DummyItem - - -@Composable -fun DummyRoute( - paddingValues: PaddingValues, - navigateUp: () -> Unit, - navigateNext: () -> Unit, - snackBarHostState: SnackbarHostState, - viewModel: DummyViewModel = hiltViewModel() -) { - val state by viewModel.state.collectAsStateWithLifecycle() - val lifecycleOwner = LocalLifecycleOwner.current - - LaunchedEffect(Unit) { - viewModel.getDummyList() - } - - LaunchedEffect(viewModel.sideEffect, lifecycleOwner) { - viewModel.sideEffect.flowWithLifecycle(lifecycleOwner.lifecycle) - .collect { sideEffect -> - when (sideEffect) { - is DummySideEffect.ShowSnackBar -> snackBarHostState.showSnackbar(sideEffect.message) - DummySideEffect.NavigateNext -> navigateNext() - DummySideEffect.NavigateUp -> navigateUp() - } - } - } - - DummyScreen( - paddingValues = paddingValues, - navigateUp = viewModel::navigateUp, - navigateNext = viewModel::navigateNext, - state = state.uiState - ) -} - -@Composable -fun DummyScreen( - paddingValues: PaddingValues, - navigateUp: () -> Unit, - navigateNext: () -> Unit, - state: UiState>, - modifier: Modifier = Modifier -) { - LazyColumn( - modifier = modifier - .fillMaxSize() - .padding(paddingValues), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - when (state) { - is UiState.Loading -> { - item { - Text( - modifier = modifier - .noRippleClickable { navigateUp() }, - textAlign = TextAlign.Center, - text = stringResource(R.string.loading_string), - fontSize = 30.sp - ) - } - } - - is UiState.Empty -> { - item { - Text( - modifier = modifier - .noRippleClickable { navigateUp() }, - textAlign = TextAlign.Center, - text = stringResource(R.string.empty_string), - fontSize = 30.sp - ) - } - } - - is UiState.Failure -> { - item { - Text( - modifier = modifier - .noRippleClickable { navigateUp() }, - textAlign = TextAlign.Center, - text = state.message, - ) - } - } - - is UiState.Success -> { - items(state.data){ - DummyItem( - id = it.id, - firstName = it.firstName, - lastName = it.lastName, - profileUrl = it.profile, - navigateNext = navigateNext - ) - } - } - } - } -} - -@Preview -@Composable -private fun DummyScreenPreview() { - PawKeyTheme { - DummyScreen( - paddingValues = PaddingValues(), - navigateUp = {}, - navigateNext = {}, - state = UiState.Loading - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/dummy/component/DummyItem.kt b/app/src/main/java/com/paw/key/presentation/ui/dummy/component/DummyItem.kt deleted file mode 100644 index 03b9d2c7..00000000 --- a/app/src/main/java/com/paw/key/presentation/ui/dummy/component/DummyItem.kt +++ /dev/null @@ -1,73 +0,0 @@ -package com.paw.key.presentation.ui.dummy.component - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import coil.compose.AsyncImage -//import com.paw.key.R.string.profile -import com.paw.key.core.designsystem.theme.PawKeyTheme -import com.paw.key.core.extension.noRippleClickable - -@Composable -fun DummyItem( - id: Int, - firstName: String, - lastName: String, - profileUrl: String, - navigateNext: () -> Unit, - modifier: Modifier = Modifier -) { - Column( - modifier = modifier - .padding(16.dp) - .fillMaxWidth() - ) { - AsyncImage( - model = profileUrl, - contentDescription = "프로필 이미지", - //stringResource(profile), - modifier = Modifier - .size(80.dp) - .clip(CircleShape) - .noRippleClickable(navigateNext), - contentScale = ContentScale.Crop - ) - - Spacer(modifier = Modifier.height(8.dp)) - - Text( - text = "$firstName $lastName", - ) - - Spacer(modifier = Modifier.height(8.dp)) - - Text( - text = "ID: $id", - ) - } -} - -@Preview -@Composable -private fun DummyItemPreview() { - PawKeyTheme { - DummyItem( - id = 1, - firstName = "minseong", - lastName = "Son", - profileUrl = "", - navigateNext = {} - ) - } -} diff --git a/app/src/main/java/com/paw/key/presentation/ui/dummy/navigation/DummyNavigation.kt b/app/src/main/java/com/paw/key/presentation/ui/dummy/navigation/DummyNavigation.kt deleted file mode 100644 index 811e09a0..00000000 --- a/app/src/main/java/com/paw/key/presentation/ui/dummy/navigation/DummyNavigation.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.paw.key.presentation.ui.dummy.navigation - -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.dummy.DummyRoute -import kotlinx.serialization.Serializable - -fun NavController.navigateDummy( - navOptions: NavOptions? -) { - navigate(Dummy, navOptions) -} - -fun NavGraphBuilder.dummyNavGraph( - paddingValues: PaddingValues, - navigateUp: () -> Unit, - navigateNext: () -> Unit, - snackBarHostState: SnackbarHostState -) { - composable { - DummyRoute( - paddingValues = paddingValues, - navigateUp = navigateUp, - navigateNext = navigateNext, - snackBarHostState = snackBarHostState, - ) - } -} - -@Serializable -data object Dummy : Route \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/dummy/next/DummyNextNavigation.kt b/app/src/main/java/com/paw/key/presentation/ui/dummy/next/DummyNextNavigation.kt deleted file mode 100644 index 55141569..00000000 --- a/app/src/main/java/com/paw/key/presentation/ui/dummy/next/DummyNextNavigation.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.paw.key.presentation.ui.dummy.next - -import androidx.compose.foundation.layout.PaddingValues -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 kotlinx.serialization.Serializable - -fun NavController.navigateDummyNext( - navOptions: NavOptions? -) { - navigate(DummyNext, navOptions) -} - -fun NavGraphBuilder.dummyNextNavGraph( - paddingValues: PaddingValues, -) { - composable { - DummyNextRoute( - paddingValues = paddingValues, - ) - } -} - -@Serializable -data object DummyNext : Route \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/dummy/next/DummyNextScreen.kt b/app/src/main/java/com/paw/key/presentation/ui/dummy/next/DummyNextScreen.kt deleted file mode 100644 index 88a7fceb..00000000 --- a/app/src/main/java/com/paw/key/presentation/ui/dummy/next/DummyNextScreen.kt +++ /dev/null @@ -1,37 +0,0 @@ -package com.paw.key.presentation.ui.dummy.next - -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview - -@Composable -fun DummyNextRoute( - paddingValues: PaddingValues -) { - DummyNextScreen( - paddingValues = paddingValues - ) -} - -@Composable -fun DummyNextScreen( - paddingValues: PaddingValues, - modifier: Modifier = Modifier -) { - Text( - text = "dummy next", - modifier = modifier - .padding(paddingValues), - ) -} - -@Preview -@Composable -private fun DummyNextScreenPreview() { - DummyNextScreen( - paddingValues = PaddingValues() - ) -} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/dummy/state/DummyContract.kt b/app/src/main/java/com/paw/key/presentation/ui/dummy/state/DummyContract.kt deleted file mode 100644 index 8e365f02..00000000 --- a/app/src/main/java/com/paw/key/presentation/ui/dummy/state/DummyContract.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.paw.key.presentation.ui.dummy.state - -import androidx.compose.runtime.Immutable -import com.paw.key.core.util.UiState -import com.paw.key.domain.model.entity.DummyUser -import kotlinx.collections.immutable.PersistentList - -class DummyContract { - @Immutable - data class DummyState( - val uiState: UiState> = UiState.Loading - ) - - sealed class DummySideEffect { - data class ShowSnackBar(val message: String) : DummySideEffect() - data object NavigateUp: DummySideEffect() - data object NavigateNext: DummySideEffect() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/dummy/viewmodel/DummyViewModel.kt b/app/src/main/java/com/paw/key/presentation/ui/dummy/viewmodel/DummyViewModel.kt deleted file mode 100644 index ebdf6978..00000000 --- a/app/src/main/java/com/paw/key/presentation/ui/dummy/viewmodel/DummyViewModel.kt +++ /dev/null @@ -1,57 +0,0 @@ -package com.paw.key.presentation.ui.dummy.viewmodel - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.paw.key.core.util.UiState -import com.paw.key.core.util.handleError -import com.paw.key.domain.repository.DummyRepository -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.collections.immutable.toPersistentList -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.launch -import com.paw.key.presentation.ui.dummy.state.DummyContract.DummyState -import com.paw.key.presentation.ui.dummy.state.DummyContract.DummySideEffect -import javax.inject.Inject - -@HiltViewModel -class DummyViewModel @Inject constructor( - private val dummyRepository: DummyRepository -): ViewModel() { - private val _state = MutableStateFlow(DummyState()) - val state: StateFlow - get() = _state.asStateFlow() - - private val _sideEffect = MutableSharedFlow() - val sideEffect: MutableSharedFlow - get() = _sideEffect - - fun getDummyList() = viewModelScope.launch { - dummyRepository.getDummyList() - .onSuccess { - _state.value = _state.value.copy( - uiState = UiState.Success(it.toPersistentList()) - ) - }.onFailure { throwable -> - val errorMessage = handleError(throwable) - _state.value = _state.value.copy( - uiState = UiState.Failure(errorMessage) - ) - showSnackBar(errorMessage) - } - } - - private fun showSnackBar(message: String) = viewModelScope.launch { - _sideEffect.emit(DummySideEffect.ShowSnackBar(message)) - } - - fun navigateUp() = viewModelScope.launch { - _sideEffect.emit(DummySideEffect.NavigateUp) - } - - fun navigateNext() = viewModelScope.launch { - _sideEffect.emit(DummySideEffect.NavigateNext) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/home/HomeLocationSettingScreen.kt b/app/src/main/java/com/paw/key/presentation/ui/home/HomeLocationSettingScreen.kt deleted file mode 100644 index d9fd1742..00000000 --- a/app/src/main/java/com/paw/key/presentation/ui/home/HomeLocationSettingScreen.kt +++ /dev/null @@ -1,173 +0,0 @@ -/* -package com.paw.key.presentation.ui.home - -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.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.material3.Icon -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.res.vectorResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.paw.key.R -import com.paw.key.core.designsystem.component.PawkeyButton -import com.paw.key.core.designsystem.theme.PawKeyTheme -import com.paw.key.core.extension.noRippleClickable -import com.paw.key.presentation.ui.home.viewmodel.HomeViewModel -import com.paw.key.presentation.ui.signup.component.FormField -import com.paw.key.presentation.ui.signup.component.LocationItem -import com.paw.key.presentation.ui.signup.component.LocationItemList -import com.paw.key.presentation.ui.signup.component.LocationList - -@Preview(showBackground = true) -@Composable -private fun PreviewHomeLocationSettingScreen() { - PawKeyTheme { - HomeLocationSettingScreen( - paddingValues = PaddingValues(), - navigateUp = {}, - navigateNext = {}, - navigateHomeLocationSetting = {} - ) - } -} - -@Composable -fun HomeLocationSettingRoute( - paddingValues: PaddingValues, - navigateUp: () -> Unit, - navigateNext: (Int) -> Unit, - navigateHomeLocationSetting: () -> Unit, - modifier: Modifier = Modifier, -) { - HomeLocationSettingScreen( - paddingValues = paddingValues, - navigateUp = navigateUp, - navigateNext = navigateNext, - navigateHomeLocationSetting = navigateHomeLocationSetting, - modifier = modifier - ) -} - -@Composable -fun HomeLocationSettingScreen( - paddingValues: PaddingValues, - navigateUp: () -> Unit, - navigateNext: (Int) -> Unit, - navigateHomeLocationSetting: () -> Unit, - modifier: Modifier = Modifier, - viewModel: HomeViewModel = hiltViewModel(), -) { - val state by viewModel.state.collectAsStateWithLifecycle() - val regionList by viewModel.regionList.collectAsStateWithLifecycle() - - val selectedGu = state.selectedLocation.selectedGu - val selectedDong = state.selectedLocation.selectedDong - - val guOptions = regionList.map { it.gu.name } - - val dongOptions = if (selectedGu.isNotEmpty()) { - regionList.find { it.gu.name == selectedGu }?.dongs?.map { - LocationItem(id = it.id, name = it.name) - } ?: emptyList() - } else { - emptyList() - } - - Column( - modifier = modifier - .fillMaxSize() - .padding(paddingValues) - .padding(horizontal = 16.dp) - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .height(60.dp) - .padding(vertical = 16.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - imageVector = ImageVector.vectorResource(R.drawable.ic_arrow_left_black), - contentDescription = "뒤로가기", - modifier = Modifier.noRippleClickable { navigateUp() } - ) - Box( - modifier = Modifier.weight(1f), - contentAlignment = Alignment.Center - ) { - Text( - text = stringResource(id = R.string.ic_home_location_title), - style = PawKeyTheme.typography.body16Sb - ) - } - Spacer(modifier = Modifier.width(24.dp)) - } - - Spacer(modifier = Modifier.height(27.dp)) - - FormField( - label = stringResource(id = R.string.ic_onboarding_signup_main_location), - content = { - LocationList( - selected = selectedGu, - locations = guOptions, - onLocationSelected = { guName -> - val selectedGuItem = regionList.find { it.gu.name == guName } - selectedGuItem?.let { - viewModel.onGuSelected(it.gu.name, it.gu.id) - } - } - ) - } - ) - - Spacer(modifier = Modifier.height(46.dp)) - - if (selectedGu.isNotEmpty()) { - FormField( - label = stringResource(id = R.string.ic_onboarding_signup_sub_location), - content = { - LocationItemList( - selected = selectedDong, - locations = dongOptions, - onLocationSelected = { locationItem -> - viewModel.onDongSelected(locationItem.name, locationItem.id) - } - ) - } - ) - } - - Spacer(modifier = Modifier.weight(1f)) - - val isFormValid = selectedGu.isNotEmpty() && selectedDong.isNotEmpty() - - PawkeyButton( - text = stringResource(id = R.string.ic_onboarding_signup_button), - enabled = isFormValid, - onClick = { - if (isFormValid) { - navigateNext(state.selectedLocation.selectedDongId) - } - } - ) - - Spacer(modifier = Modifier.height(46.dp)) - } -}*/ From 1324d43dc63e114aa261b929f8820c25c9290aa3 Mon Sep 17 00:00:00 2001 From: sonms Date: Thu, 19 Feb 2026 23:58:41 +0900 Subject: [PATCH 43/47] =?UTF-8?q?mod/#154=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EB=B0=A9=EC=8B=9D=20interceptor=20=EB=B0=8F=20repository?= =?UTF-8?q?=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/paw/key/data/di/AuthInterceptor.kt | 25 ++++ .../java/com/paw/key/data/di/NetworkModule.kt | 4 +- .../LocalStorageRepositoryImpl.kt | 118 ++++++++++++++++++ .../login/AuthRepositoryImpl.kt | 18 +-- .../localstorage/LocalStorageRepository.kt | 24 ++++ .../paw/key/domain/usecase/LoginUseCase.kt | 40 ++++++ 6 files changed, 212 insertions(+), 17 deletions(-) create mode 100644 app/src/main/java/com/paw/key/data/di/AuthInterceptor.kt create mode 100644 app/src/main/java/com/paw/key/data/repositoryimpl/localstorage/LocalStorageRepositoryImpl.kt create mode 100644 app/src/main/java/com/paw/key/domain/repository/localstorage/LocalStorageRepository.kt create mode 100644 app/src/main/java/com/paw/key/domain/usecase/LoginUseCase.kt diff --git a/app/src/main/java/com/paw/key/data/di/AuthInterceptor.kt b/app/src/main/java/com/paw/key/data/di/AuthInterceptor.kt new file mode 100644 index 00000000..331d888b --- /dev/null +++ b/app/src/main/java/com/paw/key/data/di/AuthInterceptor.kt @@ -0,0 +1,25 @@ +package com.paw.key.data.di + +import com.paw.key.domain.repository.localstorage.LocalStorageRepository +import kotlinx.coroutines.runBlocking +import okhttp3.Interceptor +import okhttp3.Response +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class AuthInterceptor @Inject constructor( + private val localStorageRepository: LocalStorageRepository +) : Interceptor { + override fun intercept(chain: Interceptor.Chain): Response { + val token = runBlocking { + localStorageRepository.getAccessToken() + } + + val newRequest = chain.request().newBuilder().apply { + header("Authorization", "Bearer $token") + }.build() + + return chain.proceed(newRequest) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/data/di/NetworkModule.kt b/app/src/main/java/com/paw/key/data/di/NetworkModule.kt index e640c843..7a79f1ed 100644 --- a/app/src/main/java/com/paw/key/data/di/NetworkModule.kt +++ b/app/src/main/java/com/paw/key/data/di/NetworkModule.kt @@ -1,7 +1,7 @@ package com.paw.key.data.di -import com.paw.key.BuildConfig import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory +import com.paw.key.BuildConfig import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -33,9 +33,11 @@ object NetworkModule { @Singleton fun providesOkHttpClient( loggingInterceptor: HttpLoggingInterceptor, + authInterceptor: AuthInterceptor ): OkHttpClient = OkHttpClient.Builder() .connectTimeout(30, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) + .addInterceptor(authInterceptor) .addInterceptor(loggingInterceptor) .build() diff --git a/app/src/main/java/com/paw/key/data/repositoryimpl/localstorage/LocalStorageRepositoryImpl.kt b/app/src/main/java/com/paw/key/data/repositoryimpl/localstorage/LocalStorageRepositoryImpl.kt new file mode 100644 index 00000000..57fccf98 --- /dev/null +++ b/app/src/main/java/com/paw/key/data/repositoryimpl/localstorage/LocalStorageRepositoryImpl.kt @@ -0,0 +1,118 @@ +package com.paw.key.data.repositoryimpl.localstorage + +import android.content.Context +import android.content.SharedPreferences +import androidx.security.crypto.EncryptedSharedPreferences +import androidx.security.crypto.MasterKey +import com.paw.key.domain.repository.localstorage.LocalStorageRepository +import dagger.hilt.android.qualifiers.ApplicationContext +import java.util.UUID +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class LocalStorageRepositoryImpl @Inject constructor( + @ApplicationContext private val context: Context +) : LocalStorageRepository { + private val sharedPreferences: SharedPreferences by lazy { + val masterKey = MasterKey.Builder(context) + .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) + .build() + + EncryptedSharedPreferences.create( + context, + PREFERENCES_NAME, + masterKey, + EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, + EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM + ) + } + + override suspend fun saveTokens(accessToken: String, refreshToken: String) { + sharedPreferences.edit().apply { + putString(ACCESS_TOKEN, accessToken) + putString(REFRESH_TOKEN, refreshToken) + apply() + } + } + + override suspend fun getAccessToken(): String { + return sharedPreferences.getString(ACCESS_TOKEN, "") ?: "" + } + + override suspend fun getRefreshToken(): String { + return sharedPreferences.getString(REFRESH_TOKEN, "") ?: "" + } + + override suspend fun removeTokens() { + sharedPreferences.edit().apply { + remove(ACCESS_TOKEN) + remove(REFRESH_TOKEN) + apply() + } + } + + override suspend fun saveUserId(userId: Int) { + sharedPreferences.edit().apply { + putInt(USER_ID, userId) + apply() + } + } + + override suspend fun getUserId(): Int { + return sharedPreferences.getInt(USER_ID, -1) + } + + override suspend fun savePetId(petId: Int) { + sharedPreferences.edit().apply { + putInt(PET_ID, petId) + apply() + } + } + + override suspend fun getPetId(): Int { + return sharedPreferences.getInt(PET_ID, -1) + } + + override suspend fun saveDeviceId(deviceId: String) { + sharedPreferences.edit().apply { + putString(DEVICE_ID, deviceId) + apply() + } + } + + override suspend fun getDeviceId(): String { + val savedId = sharedPreferences.getString(DEVICE_ID, null) + if (!savedId.isNullOrEmpty()) { + return savedId + } + + // ID가 없으면 새로 생성해서 저장 + val newId = UUID.randomUUID().toString() + sharedPreferences.edit().apply { + putString(DEVICE_ID, newId) + apply() + } + return newId + } + + override suspend fun clearInfo() { + sharedPreferences.edit().apply { + remove(USER_ID) + remove(PET_ID) + remove(DEVICE_ID) + remove(ACCESS_TOKEN) + remove(REFRESH_TOKEN) + apply() + } + } + + companion object { + private const val PREFERENCES_NAME = "user_preferences" + private const val ACCESS_TOKEN = "ACCESS_TOKEN" + private const val REFRESH_TOKEN = "REFRESH_TOKEN" + private const val DEVICE_ID = "device_id" + private const val USER_ID = "user_id" + private const val PET_ID = "pet_id" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/data/repositoryimpl/login/AuthRepositoryImpl.kt b/app/src/main/java/com/paw/key/data/repositoryimpl/login/AuthRepositoryImpl.kt index 79718d8b..5eae4cec 100644 --- a/app/src/main/java/com/paw/key/data/repositoryimpl/login/AuthRepositoryImpl.kt +++ b/app/src/main/java/com/paw/key/data/repositoryimpl/login/AuthRepositoryImpl.kt @@ -1,24 +1,19 @@ package com.paw.key.data.repositoryimpl.login import android.content.Context -import com.paw.key.core.util.UserDataStore import com.paw.key.core.util.suspendRunCatching import com.paw.key.data.dto.response.LoginResponseDto import com.paw.key.data.remote.datasource.login.AuthRemoteDataSource import com.paw.key.data.remote.datasource.login.GoogleAuthDataSource import com.paw.key.data.remote.datasource.login.KakaoAuthDataSource import com.paw.key.domain.repository.login.AuthRepository -import dagger.hilt.android.qualifiers.ApplicationContext -import timber.log.Timber import javax.inject.Inject class AuthRepositoryImpl @Inject constructor( private val authRemoteDataSource: AuthRemoteDataSource, private val googleAuthDataSource: GoogleAuthDataSource, private val kakaoAuthDataSource: KakaoAuthDataSource, - @ApplicationContext private val context: Context ) : AuthRepository { - override suspend fun signInWithGoogle(context: Context): Result = googleAuthDataSource.signIn(context).map { it.idToken } @@ -27,20 +22,11 @@ class AuthRepositoryImpl @Inject constructor( override suspend fun login(idToken: String, deviceId: String): Result = suspendRunCatching { - val loginResponse = authRemoteDataSource.login(idToken, deviceId) - saveTokens(loginResponse) - loginResponse + authRemoteDataSource.login(idToken, deviceId) } override suspend fun loginKakao(idToken: String, deviceId: String): Result = suspendRunCatching { - val loginResponse = authRemoteDataSource.loginKakao(idToken, deviceId) - saveTokens(loginResponse) - loginResponse + authRemoteDataSource.loginKakao(idToken, deviceId) } - - private suspend fun saveTokens(loginResponse: LoginResponseDto) { - UserDataStore.saveAcessToken(context, loginResponse.accessToken) - UserDataStore.saveRefreshToken(context, loginResponse.refreshToken) - } } \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/domain/repository/localstorage/LocalStorageRepository.kt b/app/src/main/java/com/paw/key/domain/repository/localstorage/LocalStorageRepository.kt new file mode 100644 index 00000000..eaa37ea6 --- /dev/null +++ b/app/src/main/java/com/paw/key/domain/repository/localstorage/LocalStorageRepository.kt @@ -0,0 +1,24 @@ +package com.paw.key.domain.repository.localstorage + +interface LocalStorageRepository { + // 토큰 관련 + suspend fun saveTokens(accessToken: String, refreshToken: String) + suspend fun getAccessToken(): String + suspend fun getRefreshToken(): String + suspend fun removeTokens() + + // 사용자 정보 관련 + suspend fun saveUserId(userId: Int) + suspend fun getUserId(): Int + + // 펫 정보 관련 + suspend fun savePetId(petId: Int) + suspend fun getPetId(): Int + + // 기기 정보 관련 + suspend fun saveDeviceId(deviceId: String) + suspend fun getDeviceId(): String + + // 전체 초기화 (로그아웃/탈퇴 시) + suspend fun clearInfo() +} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/domain/usecase/LoginUseCase.kt b/app/src/main/java/com/paw/key/domain/usecase/LoginUseCase.kt new file mode 100644 index 00000000..eb97aae8 --- /dev/null +++ b/app/src/main/java/com/paw/key/domain/usecase/LoginUseCase.kt @@ -0,0 +1,40 @@ +package com.paw.key.domain.usecase + +import android.content.Context +import com.paw.key.domain.repository.localstorage.LocalStorageRepository +import com.paw.key.domain.repository.login.AuthRepository +import javax.inject.Inject + +class LoginUseCase @Inject constructor( + private val authRepository: AuthRepository, + private val localStorageRepository: LocalStorageRepository +) { + suspend fun invokeGoogleLogin(context: Context): Result { + val deviceId = localStorageRepository.getDeviceId() + + return authRepository.signInWithGoogle(context) + .mapCatching { idToken -> + val loginResponse = authRepository.login(idToken, deviceId).getOrThrow() + + localStorageRepository.saveTokens( + accessToken = loginResponse.accessToken, + refreshToken = loginResponse.refreshToken + ) + + loginResponse.isNewUser + } + } + + suspend fun invokeKakaoLogin(context: Context): Result { + val deviceId = localStorageRepository.getDeviceId() + + return authRepository.signInWithKakao(context) + .mapCatching { idToken -> + val response = authRepository.loginKakao(idToken, deviceId).getOrThrow() + + localStorageRepository.saveTokens(response.accessToken, response.refreshToken) + + response.isNewUser + } + } +} \ No newline at end of file From f3b82c6244103f165245f141416eae0c0f1dfbbe Mon Sep 17 00:00:00 2001 From: sonms Date: Thu, 19 Feb 2026 23:59:47 +0900 Subject: [PATCH 44/47] =?UTF-8?q?feat/#154=20=EC=9D=B4=EB=AF=B8=EC=A7=80?= =?UTF-8?q?=20presigned=20url=EC=9D=B4=EB=9E=91=20image=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EB=93=B1=EB=A1=9D=20api=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 --- .../presigned/ImagePresignedRequestDto.kt | 19 ++++ .../presigned/ImagePresignedResponseDto.kt | 19 ++++ .../image/register/ImageRegisterRequestDto.kt | 31 +++++++ .../register/ImageRegisterResponseDto.kt | 15 ++++ .../datasource/image/ImageDataSource.kt | 31 +++++++ .../datasource/image/ImageLocalDataSource.kt | 87 +++++++++++++++++++ .../image/ImageRepositoryImpl.kt | 73 ++++++++++++++++ .../key/data/service/image/ImageService.kt | 31 +++++++ .../domain/entity/image/ImageDomainType.kt | 10 +++ .../entity/image/ImagePresignedEntity.kt | 11 +++ .../entity/image/ImageRegisterEntity.kt | 14 +++ .../repository/image/ImageRepository.kt | 22 +++++ 12 files changed, 363 insertions(+) create mode 100644 app/src/main/java/com/paw/key/data/dto/image/presigned/ImagePresignedRequestDto.kt create mode 100644 app/src/main/java/com/paw/key/data/dto/image/presigned/ImagePresignedResponseDto.kt create mode 100644 app/src/main/java/com/paw/key/data/dto/image/register/ImageRegisterRequestDto.kt create mode 100644 app/src/main/java/com/paw/key/data/dto/image/register/ImageRegisterResponseDto.kt create mode 100644 app/src/main/java/com/paw/key/data/remote/datasource/image/ImageDataSource.kt create mode 100644 app/src/main/java/com/paw/key/data/remote/datasource/image/ImageLocalDataSource.kt create mode 100644 app/src/main/java/com/paw/key/data/repositoryimpl/image/ImageRepositoryImpl.kt create mode 100644 app/src/main/java/com/paw/key/data/service/image/ImageService.kt create mode 100644 app/src/main/java/com/paw/key/domain/entity/image/ImageDomainType.kt create mode 100644 app/src/main/java/com/paw/key/domain/entity/image/ImagePresignedEntity.kt create mode 100644 app/src/main/java/com/paw/key/domain/entity/image/ImageRegisterEntity.kt create mode 100644 app/src/main/java/com/paw/key/domain/repository/image/ImageRepository.kt diff --git a/app/src/main/java/com/paw/key/data/dto/image/presigned/ImagePresignedRequestDto.kt b/app/src/main/java/com/paw/key/data/dto/image/presigned/ImagePresignedRequestDto.kt new file mode 100644 index 00000000..b6c1fe20 --- /dev/null +++ b/app/src/main/java/com/paw/key/data/dto/image/presigned/ImagePresignedRequestDto.kt @@ -0,0 +1,19 @@ +package com.paw.key.data.dto.image.presigned + +import com.google.gson.annotations.SerializedName +import com.paw.key.domain.entity.image.ImagePresignedEntity +import kotlinx.serialization.Serializable + +@Serializable +data class ImagePresignedRequestDto( + @SerializedName("domain") + val domain: String, + + @SerializedName("contentType") + val contentType: String +) + +fun ImagePresignedEntity.toDto() = ImagePresignedRequestDto( + domain = domain.name, + contentType = contentType +) diff --git a/app/src/main/java/com/paw/key/data/dto/image/presigned/ImagePresignedResponseDto.kt b/app/src/main/java/com/paw/key/data/dto/image/presigned/ImagePresignedResponseDto.kt new file mode 100644 index 00000000..c8abe279 --- /dev/null +++ b/app/src/main/java/com/paw/key/data/dto/image/presigned/ImagePresignedResponseDto.kt @@ -0,0 +1,19 @@ +package com.paw.key.data.dto.image.presigned + +import com.google.gson.annotations.SerializedName +import com.paw.key.domain.entity.image.ImagePresignedResultEntity +import kotlinx.serialization.Serializable + +@Serializable +data class ImagePresignedResponseDto( + @SerializedName("uploadUrl") + val uploadUrl: String, + + @SerializedName("imageUrl") + val imageUrl: String +) { + fun toEntity() = ImagePresignedResultEntity( + uploadUrl = uploadUrl, + imageUrl = imageUrl + ) +} diff --git a/app/src/main/java/com/paw/key/data/dto/image/register/ImageRegisterRequestDto.kt b/app/src/main/java/com/paw/key/data/dto/image/register/ImageRegisterRequestDto.kt new file mode 100644 index 00000000..c6e15e49 --- /dev/null +++ b/app/src/main/java/com/paw/key/data/dto/image/register/ImageRegisterRequestDto.kt @@ -0,0 +1,31 @@ +package com.paw.key.data.dto.image.register + +import com.google.gson.annotations.SerializedName +import com.paw.key.domain.entity.image.ImageRegisterEntity +import kotlinx.serialization.Serializable + +@Serializable +data class ImageRegisterRequestDto( + @SerializedName("imageUrl") + val imageUrl: String, + + @SerializedName("contentType") + val contentType: String, + + @SerializedName("width") + val width: Int, + + @SerializedName("height") + val height: Int, + + @SerializedName("domain") + val domain: String +) + +fun ImageRegisterEntity.toDto() = ImageRegisterRequestDto( + imageUrl = imageUrl, + contentType = contentType, + width = width, + height = height, + domain = domain.name +) \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/data/dto/image/register/ImageRegisterResponseDto.kt b/app/src/main/java/com/paw/key/data/dto/image/register/ImageRegisterResponseDto.kt new file mode 100644 index 00000000..3c1f9299 --- /dev/null +++ b/app/src/main/java/com/paw/key/data/dto/image/register/ImageRegisterResponseDto.kt @@ -0,0 +1,15 @@ +package com.paw.key.data.dto.image.register + +import com.google.gson.annotations.SerializedName +import com.paw.key.domain.entity.image.ImageRegisterResultEntity +import kotlinx.serialization.Serializable + +@Serializable +data class ImageRegisterResponseDto( + @SerializedName("imageId") + val imageId: Int +) { + fun toEntity() = ImageRegisterResultEntity( + imageId = imageId + ) +} diff --git a/app/src/main/java/com/paw/key/data/remote/datasource/image/ImageDataSource.kt b/app/src/main/java/com/paw/key/data/remote/datasource/image/ImageDataSource.kt new file mode 100644 index 00000000..ff36c21f --- /dev/null +++ b/app/src/main/java/com/paw/key/data/remote/datasource/image/ImageDataSource.kt @@ -0,0 +1,31 @@ +package com.paw.key.data.remote.datasource.image + +import com.paw.key.data.dto.image.presigned.ImagePresignedRequestDto +import com.paw.key.data.dto.image.register.ImageRegisterRequestDto +import com.paw.key.data.service.image.ImageService +import okhttp3.RequestBody +import javax.inject.Inject + +class ImageDataSource @Inject constructor( + private val imageService: ImageService +) { + suspend fun registerImage( + dto : ImageRegisterRequestDto + ) = imageService.registerImage( + body = dto + ) + + suspend fun presignedImage( + dto : ImagePresignedRequestDto + ) = imageService.presignedImage( + body = dto + ) + + suspend fun uploadS3( + presignedUrl : String, + file : RequestBody + ) = imageService.uploadToS3( + presignedUrl = presignedUrl, + file = file + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/data/remote/datasource/image/ImageLocalDataSource.kt b/app/src/main/java/com/paw/key/data/remote/datasource/image/ImageLocalDataSource.kt new file mode 100644 index 00000000..3f3e53e4 --- /dev/null +++ b/app/src/main/java/com/paw/key/data/remote/datasource/image/ImageLocalDataSource.kt @@ -0,0 +1,87 @@ +package com.paw.key.data.remote.datasource.image + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.ImageDecoder +import android.net.Uri +import android.os.Build +import androidx.core.net.toUri +import dagger.hilt.android.qualifiers.ApplicationContext +import java.io.ByteArrayOutputStream +import java.io.File +import java.io.FileOutputStream +import java.util.UUID +import javax.inject.Inject +import kotlin.math.max + +class ImageLocalDataSource @Inject constructor( + @ApplicationContext private val context: Context +) { + fun getOptimizedFile(uriString: String): File { + val uri = uriString.toUri() + val dir = getDirectory() + + return compressToWebP(uri, dir) + } + + fun clearCache() { + getDirectory().listFiles()?.forEach { it.delete() } + } + + private fun getDirectory(): File { + return File(context.cacheDir, DIRECTORY).apply { + if (!exists()) mkdirs() + } + } + + private fun compressToWebP(uri: Uri, dir: File): File { + val source = ImageDecoder.createSource(context.contentResolver, uri) + val bitmap = ImageDecoder.decodeBitmap(source) { decoder, info, _ -> + decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE + decoder.isMutableRequired = true + + val size = info.size + val targetSize = calculateTargetSize(size.width, size.height) + decoder.setTargetSize(targetSize.first, targetSize.second) + } + + + val format = if (Build.VERSION.SDK_INT >= 30) { + Bitmap.CompressFormat.WEBP_LOSSY + } else { + Bitmap.CompressFormat.WEBP + } + + val byteArray = ByteArrayOutputStream().use { stream -> + bitmap.compress(format, WEBP_QUALITY, stream) + bitmap.recycle() + stream.toByteArray() + } + + val tempFile = File(dir, "${UUID.randomUUID()}.webp") + FileOutputStream(tempFile).use { it.write(byteArray) } + + return tempFile + } + + private fun calculateTargetSize(width: Int, height: Int): Pair { + if (width <= MAX_SIZE && height <= MAX_SIZE) return width to height + + val ratio = max(width.toFloat() / MAX_SIZE, height.toFloat() / MAX_SIZE) + return (width / ratio).toInt() to (height / ratio).toInt() + } + + fun getImageSize(file: File): Pair { + val options = android.graphics.BitmapFactory.Options().apply { + inJustDecodeBounds = true + } + android.graphics.BitmapFactory.decodeFile(file.absolutePath, options) + return options.outWidth to options.outHeight + } + + companion object { + private const val DIRECTORY = "image_cache" + private const val MAX_SIZE = 1024 + private const val WEBP_QUALITY = 80 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/data/repositoryimpl/image/ImageRepositoryImpl.kt b/app/src/main/java/com/paw/key/data/repositoryimpl/image/ImageRepositoryImpl.kt new file mode 100644 index 00000000..436f6709 --- /dev/null +++ b/app/src/main/java/com/paw/key/data/repositoryimpl/image/ImageRepositoryImpl.kt @@ -0,0 +1,73 @@ +package com.paw.key.data.repositoryimpl.image + +import com.paw.key.core.util.suspendRunCatching +import com.paw.key.data.dto.image.presigned.toDto +import com.paw.key.data.dto.image.register.toDto +import com.paw.key.data.remote.datasource.image.ImageDataSource +import com.paw.key.data.remote.datasource.image.ImageLocalDataSource +import com.paw.key.domain.entity.image.ImageDomainType +import com.paw.key.domain.entity.image.ImagePresignedEntity +import com.paw.key.domain.entity.image.ImagePresignedResultEntity +import com.paw.key.domain.entity.image.ImageRegisterEntity +import com.paw.key.domain.entity.image.ImageRegisterResultEntity +import com.paw.key.domain.repository.image.ImageRepository +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.RequestBody.Companion.asRequestBody +import javax.inject.Inject + +class ImageRepositoryImpl @Inject constructor( + private val imageDataSource: ImageDataSource, + private val imageLocalDataSource: ImageLocalDataSource +) : ImageRepository { + override suspend fun registerImage( + uriString : String, + domainType: ImageDomainType, + ): Result = + suspendRunCatching{ + val optimizedFile = imageLocalDataSource.getOptimizedFile(uriString.split("#").last()) + val (width, height) = imageLocalDataSource.getImageSize(optimizedFile) + + try { + val registerEntity = ImageRegisterEntity( + imageUrl = uriString.split("#").first(), + contentType = optimizedFile.extension, + width = width, + height = height, + domain = domainType + ) + + imageDataSource.registerImage( + dto = registerEntity + .copy().toDto() + ).data.toEntity() + + } finally { + imageLocalDataSource.clearCache() + } + } + + override suspend fun presignedImage(presignedEntity: ImagePresignedEntity): Result = + suspendRunCatching{ + imageDataSource.presignedImage( + dto = presignedEntity.toDto() + ).data.toEntity() + } + + override suspend fun uploadS3( + presignedUrl: String, + uriString: String + ): Result = suspendRunCatching{ + val file = imageLocalDataSource.getOptimizedFile(uriString) + + val requestBody = file.asRequestBody("image/webp".toMediaTypeOrNull()) + + val response = imageDataSource.uploadS3(presignedUrl, requestBody) + + imageLocalDataSource.clearCache() + + if (!response.isSuccessful) { + throw Exception("S3 Upload Failed: ${response.code()}") + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/data/service/image/ImageService.kt b/app/src/main/java/com/paw/key/data/service/image/ImageService.kt new file mode 100644 index 00000000..8fa38b7e --- /dev/null +++ b/app/src/main/java/com/paw/key/data/service/image/ImageService.kt @@ -0,0 +1,31 @@ +package com.paw.key.data.service.image + +import com.paw.key.data.dto.image.presigned.ImagePresignedRequestDto +import com.paw.key.data.dto.image.presigned.ImagePresignedResponseDto +import com.paw.key.data.dto.image.register.ImageRegisterRequestDto +import com.paw.key.data.dto.image.register.ImageRegisterResponseDto +import com.paw.key.data.dto.response.BaseResponse +import okhttp3.RequestBody +import retrofit2.Response +import retrofit2.http.Body +import retrofit2.http.POST +import retrofit2.http.PUT +import retrofit2.http.Url + +interface ImageService { + @POST("images/register") + suspend fun registerImage( + @Body body: ImageRegisterRequestDto + ): BaseResponse + + @POST("images/presigned") + suspend fun presignedImage( + @Body body: ImagePresignedRequestDto + ): BaseResponse + + @PUT + suspend fun uploadToS3( + @Url presignedUrl: String, + @Body file: RequestBody + ): Response +} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/domain/entity/image/ImageDomainType.kt b/app/src/main/java/com/paw/key/domain/entity/image/ImageDomainType.kt new file mode 100644 index 00000000..334d725f --- /dev/null +++ b/app/src/main/java/com/paw/key/domain/entity/image/ImageDomainType.kt @@ -0,0 +1,10 @@ +package com.paw.key.domain.entity.image + +enum class ImageDomainType ( + val label : String +) { + ROUTE("루트 지도 이미지"), + PET_PROFILE("강아지 프로필 이미지"), + WALK_POST("산책 이미지"), + ETC("기타 등등") +} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/domain/entity/image/ImagePresignedEntity.kt b/app/src/main/java/com/paw/key/domain/entity/image/ImagePresignedEntity.kt new file mode 100644 index 00000000..24f069ca --- /dev/null +++ b/app/src/main/java/com/paw/key/domain/entity/image/ImagePresignedEntity.kt @@ -0,0 +1,11 @@ +package com.paw.key.domain.entity.image + +data class ImagePresignedEntity( + val domain: ImageDomainType, + val contentType: String +) + +data class ImagePresignedResultEntity( + val uploadUrl: String, + val imageUrl: String +) \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/domain/entity/image/ImageRegisterEntity.kt b/app/src/main/java/com/paw/key/domain/entity/image/ImageRegisterEntity.kt new file mode 100644 index 00000000..aa81f353 --- /dev/null +++ b/app/src/main/java/com/paw/key/domain/entity/image/ImageRegisterEntity.kt @@ -0,0 +1,14 @@ +package com.paw.key.domain.entity.image + +data class ImageRegisterEntity( + val imageUrl: String, + val contentType: String, + val width: Int, + val height: Int, + val domain: ImageDomainType +) + +data class ImageRegisterResultEntity( + val imageId: Int +) + diff --git a/app/src/main/java/com/paw/key/domain/repository/image/ImageRepository.kt b/app/src/main/java/com/paw/key/domain/repository/image/ImageRepository.kt new file mode 100644 index 00000000..cc451eb0 --- /dev/null +++ b/app/src/main/java/com/paw/key/domain/repository/image/ImageRepository.kt @@ -0,0 +1,22 @@ +package com.paw.key.domain.repository.image + +import com.paw.key.domain.entity.image.ImageDomainType +import com.paw.key.domain.entity.image.ImagePresignedEntity +import com.paw.key.domain.entity.image.ImagePresignedResultEntity +import com.paw.key.domain.entity.image.ImageRegisterResultEntity + +interface ImageRepository { + suspend fun registerImage( + uriString : String, + domainType: ImageDomainType, + ) : Result + + suspend fun presignedImage( + presignedEntity : ImagePresignedEntity + ) : Result + + suspend fun uploadS3( + presignedUrl : String, + uriString : String + ) : Result +} \ No newline at end of file From df4a0cb921e8b1bd486ef71efe3d0676bd5e9c58 Mon Sep 17 00:00:00 2001 From: sonms Date: Fri, 20 Feb 2026 00:02:00 +0900 Subject: [PATCH 45/47] =?UTF-8?q?feat/#154=20=EC=9C=A0=EC=A0=80=20?= =?UTF-8?q?=EC=84=9C=EB=B9=84=EC=8A=A4=20-=20=EA=B2=AC=EC=A2=85=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C,=20=EC=A7=80=EC=97=AD=EA=B5=AC=EC=A1=B0=ED=9A=8C,=20c?= =?UTF-8?q?reateUser=20=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/core/extension/String.kt | 11 ++++ .../dto/request/user/PetInfoRequestDto.kt | 39 ++++++++++++ .../dto/request/user/UserInfoRequestDto.kt | 36 +++++++++++ .../dto/response/user/PetBreedsResponseDto.kt | 29 +++++++++ .../dto/response/user/UserInfoResponseDto.kt | 18 ++++++ .../remote/datasource/user/UserDataSource.kt | 13 ++++ .../repositoryimpl/user/UserRepositoryImpl.kt | 27 ++++++++ .../key/data/service/region/RegionService.kt | 19 ++++++ .../paw/key/data/service/user/UserService.kt | 20 ++++++ .../key/domain/entity/user/PetInfoEntity.kt | 19 ++++++ .../key/domain/entity/user/UserInfoEntity.kt | 14 +++++ .../domain/repository/user/UserRepository.kt | 13 ++++ .../domain/usecase/PostCreateUserUseCase.kt | 63 +++++++++++++++++++ 13 files changed, 321 insertions(+) create mode 100644 app/src/main/java/com/paw/key/core/extension/String.kt create mode 100644 app/src/main/java/com/paw/key/data/dto/request/user/PetInfoRequestDto.kt create mode 100644 app/src/main/java/com/paw/key/data/dto/request/user/UserInfoRequestDto.kt create mode 100644 app/src/main/java/com/paw/key/data/dto/response/user/PetBreedsResponseDto.kt create mode 100644 app/src/main/java/com/paw/key/data/dto/response/user/UserInfoResponseDto.kt create mode 100644 app/src/main/java/com/paw/key/data/remote/datasource/user/UserDataSource.kt create mode 100644 app/src/main/java/com/paw/key/data/repositoryimpl/user/UserRepositoryImpl.kt create mode 100644 app/src/main/java/com/paw/key/data/service/region/RegionService.kt create mode 100644 app/src/main/java/com/paw/key/data/service/user/UserService.kt create mode 100644 app/src/main/java/com/paw/key/domain/entity/user/PetInfoEntity.kt create mode 100644 app/src/main/java/com/paw/key/domain/entity/user/UserInfoEntity.kt create mode 100644 app/src/main/java/com/paw/key/domain/repository/user/UserRepository.kt create mode 100644 app/src/main/java/com/paw/key/domain/usecase/PostCreateUserUseCase.kt diff --git a/app/src/main/java/com/paw/key/core/extension/String.kt b/app/src/main/java/com/paw/key/core/extension/String.kt new file mode 100644 index 00000000..939b468e --- /dev/null +++ b/app/src/main/java/com/paw/key/core/extension/String.kt @@ -0,0 +1,11 @@ +package com.paw.key.core.extension + +fun String.toBirthDateFormat(): String { + val digits = this.filter { it.isDigit() }.take(8) + return buildString { + digits.forEachIndexed { index, c -> + if (index == 4 || index == 6) append('-') + append(c) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/data/dto/request/user/PetInfoRequestDto.kt b/app/src/main/java/com/paw/key/data/dto/request/user/PetInfoRequestDto.kt new file mode 100644 index 00000000..6ce995ab --- /dev/null +++ b/app/src/main/java/com/paw/key/data/dto/request/user/PetInfoRequestDto.kt @@ -0,0 +1,39 @@ +package com.paw.key.data.dto.request.user + +import com.paw.key.domain.entity.user.PetInfoEntity +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class PetInfoRequestDto( + @SerialName("name") + val name: String, + @SerialName("gender") + val gender: String, + @SerialName("birth") + val birth: String, + @SerialName("isNeutered") + val isNeutered: Boolean, + @SerialName("breedId") + val breedId: Int, + @SerialName("imageId") + val imageId: Int +) { + fun toEntity() = PetInfoEntity( + name = name, + gender = gender, + birth = birth, + isNeutered = isNeutered, + breedId = breedId, + imageId = imageId + ) +} + +fun PetInfoEntity.toDto() = PetInfoRequestDto( + name = name, + gender = gender, + birth = birth, + isNeutered = isNeutered, + breedId = breedId, + imageId = imageId +) \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/data/dto/request/user/UserInfoRequestDto.kt b/app/src/main/java/com/paw/key/data/dto/request/user/UserInfoRequestDto.kt new file mode 100644 index 00000000..f6c871ff --- /dev/null +++ b/app/src/main/java/com/paw/key/data/dto/request/user/UserInfoRequestDto.kt @@ -0,0 +1,36 @@ +package com.paw.key.data.dto.request.user + +import com.paw.key.domain.entity.user.UserInfoEntity +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class UserInfoRequestDto( + @SerialName("name") + val name: String, + @SerialName("birth") + val birth: String, // "YYYY-MM-DD" 형식 + @SerialName("gender") + val gender: String, + @SerialName("dongId") + val dongId: Int, + @SerialName("pet") + val pet: PetInfoRequestDto +) { + fun toEntity() = UserInfoEntity( + name = name, + birth = birth, + gender = gender, + dongId = dongId, + pet = pet.toEntity() + ) +} + + +fun UserInfoEntity.toDto() = UserInfoRequestDto( + name = name, + birth = birth, + gender = gender, + dongId = dongId, + pet = pet.toDto() +) \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/data/dto/response/user/PetBreedsResponseDto.kt b/app/src/main/java/com/paw/key/data/dto/response/user/PetBreedsResponseDto.kt new file mode 100644 index 00000000..fe360da0 --- /dev/null +++ b/app/src/main/java/com/paw/key/data/dto/response/user/PetBreedsResponseDto.kt @@ -0,0 +1,29 @@ +package com.paw.key.data.dto.response.user + +import com.paw.key.domain.entity.user.PetBreedsEntity +import com.paw.key.domain.entity.user.PetBreedsItemEntity +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class PetBreedsResponseDto( + @SerialName("breedList") + val breedList: List +) { + fun toEntity() = PetBreedsEntity( + breedList = breedList.map { it.toEntity() } + ) +} + +@Serializable +data class PetBreedsItemDto( + @SerialName("id") + val id: Int, + @SerialName("name") + val name: String, +) { + fun toEntity() = PetBreedsItemEntity( + id = id, + name = name + ) +} diff --git a/app/src/main/java/com/paw/key/data/dto/response/user/UserInfoResponseDto.kt b/app/src/main/java/com/paw/key/data/dto/response/user/UserInfoResponseDto.kt new file mode 100644 index 00000000..c827d411 --- /dev/null +++ b/app/src/main/java/com/paw/key/data/dto/response/user/UserInfoResponseDto.kt @@ -0,0 +1,18 @@ +package com.paw.key.data.dto.response.user + +import com.paw.key.domain.entity.user.UserInfoResultEntity +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class UserInfoResponseDto( + @SerialName("userId") + val userId: Int, + @SerialName("petId") + val petId: Int, +) { + fun toEntity() = UserInfoResultEntity( + userId = userId, + petId = petId + ) +} diff --git a/app/src/main/java/com/paw/key/data/remote/datasource/user/UserDataSource.kt b/app/src/main/java/com/paw/key/data/remote/datasource/user/UserDataSource.kt new file mode 100644 index 00000000..3570805a --- /dev/null +++ b/app/src/main/java/com/paw/key/data/remote/datasource/user/UserDataSource.kt @@ -0,0 +1,13 @@ +package com.paw.key.data.remote.datasource.user + +import com.paw.key.data.dto.request.user.UserInfoRequestDto +import com.paw.key.data.service.user.UserService +import javax.inject.Inject + +class UserDataSource @Inject constructor( + private val userService: UserService +) { + suspend fun createUser(dto: UserInfoRequestDto) = userService.createUser(dto) + + suspend fun getPetBreeds() = userService.getPetBreeds() +} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/data/repositoryimpl/user/UserRepositoryImpl.kt b/app/src/main/java/com/paw/key/data/repositoryimpl/user/UserRepositoryImpl.kt new file mode 100644 index 00000000..18834e85 --- /dev/null +++ b/app/src/main/java/com/paw/key/data/repositoryimpl/user/UserRepositoryImpl.kt @@ -0,0 +1,27 @@ +package com.paw.key.data.repositoryimpl.user + +import com.paw.key.core.util.suspendRunCatching +import com.paw.key.data.dto.request.user.toDto +import com.paw.key.data.remote.datasource.user.UserDataSource +import com.paw.key.domain.entity.user.PetBreedsEntity +import com.paw.key.domain.entity.user.UserInfoEntity +import com.paw.key.domain.entity.user.UserInfoResultEntity +import com.paw.key.domain.repository.user.UserRepository +import javax.inject.Inject + +class UserRepositoryImpl @Inject constructor( + private val userDataSource: UserDataSource +) : UserRepository { + override suspend fun createUser(userInfoEntity: UserInfoEntity): Result = + suspendRunCatching{ + userDataSource.createUser( + dto = userInfoEntity.toDto() + ).data.toEntity() + } + + override suspend fun getPetBreeds(): Result = + suspendRunCatching { + userDataSource.getPetBreeds().data.toEntity() + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/data/service/region/RegionService.kt b/app/src/main/java/com/paw/key/data/service/region/RegionService.kt new file mode 100644 index 00000000..ee1102a5 --- /dev/null +++ b/app/src/main/java/com/paw/key/data/service/region/RegionService.kt @@ -0,0 +1,19 @@ +package com.paw.key.data.service.region + +import com.paw.key.data.dto.response.BaseResponse +import com.paw.key.data.dto.response.region.DistrictDataDto +import com.paw.key.data.dto.response.region.RegionResponseDto +import retrofit2.http.GET +import retrofit2.http.Path +import retrofit2.http.Query + +interface RegionService { + @GET("regions/{regionId}/geometry") + suspend fun getRegionGeometry( + @Query("userId") userId: Int, + @Path("regionId") regionId: Int, + ): BaseResponse + + @GET("regions") + suspend fun getRegionsList(): BaseResponse +} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/data/service/user/UserService.kt b/app/src/main/java/com/paw/key/data/service/user/UserService.kt new file mode 100644 index 00000000..a2ac3095 --- /dev/null +++ b/app/src/main/java/com/paw/key/data/service/user/UserService.kt @@ -0,0 +1,20 @@ +package com.paw.key.data.service.user + +import com.paw.key.data.dto.request.user.UserInfoRequestDto +import com.paw.key.data.dto.response.BaseResponse +import com.paw.key.data.dto.response.user.PetBreedsResponseDto +import com.paw.key.data.dto.response.user.UserInfoResponseDto +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.POST + +interface UserService { + @POST("users") + suspend fun createUser( + @Body body: UserInfoRequestDto + ): BaseResponse + + @GET("pets/breeds") + suspend fun getPetBreeds(): BaseResponse + +} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/domain/entity/user/PetInfoEntity.kt b/app/src/main/java/com/paw/key/domain/entity/user/PetInfoEntity.kt new file mode 100644 index 00000000..16fe17cb --- /dev/null +++ b/app/src/main/java/com/paw/key/domain/entity/user/PetInfoEntity.kt @@ -0,0 +1,19 @@ +package com.paw.key.domain.entity.user + +data class PetInfoEntity( + val name: String, + val gender: String, + val birth: String, + val isNeutered: Boolean, + val breedId: Int, + val imageId: Int +) + +data class PetBreedsEntity( + val breedList: List +) + +data class PetBreedsItemEntity( + val id: Int, + val name: String, +) diff --git a/app/src/main/java/com/paw/key/domain/entity/user/UserInfoEntity.kt b/app/src/main/java/com/paw/key/domain/entity/user/UserInfoEntity.kt new file mode 100644 index 00000000..ecd4e4e5 --- /dev/null +++ b/app/src/main/java/com/paw/key/domain/entity/user/UserInfoEntity.kt @@ -0,0 +1,14 @@ +package com.paw.key.domain.entity.user + +data class UserInfoEntity( + val name: String, + val birth: String, + val gender: String, + val dongId: Int, + val pet: PetInfoEntity +) + +data class UserInfoResultEntity( + val userId: Int, + val petId: Int, +) diff --git a/app/src/main/java/com/paw/key/domain/repository/user/UserRepository.kt b/app/src/main/java/com/paw/key/domain/repository/user/UserRepository.kt new file mode 100644 index 00000000..09211200 --- /dev/null +++ b/app/src/main/java/com/paw/key/domain/repository/user/UserRepository.kt @@ -0,0 +1,13 @@ +package com.paw.key.domain.repository.user + +import com.paw.key.domain.entity.user.PetBreedsEntity +import com.paw.key.domain.entity.user.UserInfoEntity +import com.paw.key.domain.entity.user.UserInfoResultEntity + +interface UserRepository { + suspend fun createUser( + userInfoEntity: UserInfoEntity + ): Result + + suspend fun getPetBreeds(): Result +} diff --git a/app/src/main/java/com/paw/key/domain/usecase/PostCreateUserUseCase.kt b/app/src/main/java/com/paw/key/domain/usecase/PostCreateUserUseCase.kt new file mode 100644 index 00000000..96905c58 --- /dev/null +++ b/app/src/main/java/com/paw/key/domain/usecase/PostCreateUserUseCase.kt @@ -0,0 +1,63 @@ +package com.paw.key.domain.usecase + +import com.paw.key.core.util.suspendRunCatching +import com.paw.key.domain.entity.image.ImageDomainType +import com.paw.key.domain.entity.image.ImagePresignedEntity +import com.paw.key.domain.entity.user.UserInfoEntity +import com.paw.key.domain.repository.image.ImageRepository +import com.paw.key.domain.repository.localstorage.LocalStorageRepository +import com.paw.key.domain.repository.user.UserRepository +import timber.log.Timber +import javax.inject.Inject + +class PostCreateUserUseCase @Inject constructor( + private val imageRepository: ImageRepository, + private val userRepository: UserRepository, + private val localRepository: LocalStorageRepository +) { + suspend operator fun invoke( + userInfoEntity: UserInfoEntity, + petImageUri: String? + ): Result = suspendRunCatching { + val finalImageId: Int = if (petImageUri != null) { + val presignedResult = imageRepository.presignedImage( + presignedEntity = ImagePresignedEntity( + domain = ImageDomainType.PET_PROFILE, + contentType = "image/webp" + ) + ).getOrThrow() + + /*imageRepository.uploadS3( + presignedUrl = presignedInfo.imageUrl, + uriString = petImageUri + ).getOrThrow()*/ + val registerImage = imageRepository.registerImage( + uriString = "${presignedResult.imageUrl}#${petImageUri}", + domainType = ImageDomainType.PET_PROFILE, + ).onFailure(Timber::e) + + if (!registerImage.isSuccess) { + throw Exception("이미지 업로드에 실패했습니다.") + } + + registerImage.getOrThrow().imageId + } else { + -1 + } + + val finalPetInfo = userInfoEntity.pet.copy( + imageId = finalImageId + ) + + val finalUserInfo = userInfoEntity.copy( + pet = finalPetInfo + ) + + userRepository.createUser( + userInfoEntity = finalUserInfo + ).onSuccess { + localRepository.saveUserId(userId = it.userId) + localRepository.savePetId(petId = it.petId) + } + } +} \ No newline at end of file From 9a1dc6d32d58cd2f1638d672cef923445464e2e9 Mon Sep 17 00:00:00 2001 From: sonms Date: Fri, 20 Feb 2026 00:03:10 +0900 Subject: [PATCH 46/47] =?UTF-8?q?feat/#154=20=EC=84=9C=EB=B2=84=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EC=A0=84=EB=8B=AC=EB=B0=9B=EC=9D=80=20=EB=82=B4?= =?UTF-8?q?=EC=9A=A9=EC=9C=BC=EB=A1=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/response/region/DistrictResponse.kt | 62 +++++ .../paw/key/data/local/datasource/.gitkeep | 0 .../domain/entity/signup/DistrictEntity.kt | 17 ++ .../model/entity/signup/DistrictEntity.kt | 41 --- .../ui/signup/SignUpLocationInfoScreen.kt | 31 ++- .../ui/signup/SignUpPetInfoScreen.kt | 52 ++-- .../presentation/ui/signup/SignUpScreen.kt | 94 ++++--- .../ui/signup/component/SignUpHeader.kt | 2 + .../ui/signup/component/SignUpSubHeader.kt | 47 ++-- .../ui/signup/model/SignUpLocationInfo.kt | 46 +++- .../ui/signup/model/SignUpPetInfo.kt | 14 +- .../ui/signup/state/SignUpContract.kt | 15 +- .../ui/signup/viewmodel/SignUpViewModel.kt | 236 ++++++++++++------ 13 files changed, 454 insertions(+), 203 deletions(-) create mode 100644 app/src/main/java/com/paw/key/data/dto/response/region/DistrictResponse.kt delete mode 100644 app/src/main/java/com/paw/key/data/local/datasource/.gitkeep create mode 100644 app/src/main/java/com/paw/key/domain/entity/signup/DistrictEntity.kt delete mode 100644 app/src/main/java/com/paw/key/domain/model/entity/signup/DistrictEntity.kt diff --git a/app/src/main/java/com/paw/key/data/dto/response/region/DistrictResponse.kt b/app/src/main/java/com/paw/key/data/dto/response/region/DistrictResponse.kt new file mode 100644 index 00000000..b4107c27 --- /dev/null +++ b/app/src/main/java/com/paw/key/data/dto/response/region/DistrictResponse.kt @@ -0,0 +1,62 @@ +package com.paw.key.data.dto.response.region + +import com.paw.key.domain.entity.signup.DistrictEntity +import com.paw.key.domain.entity.signup.Dong +import com.paw.key.domain.entity.signup.Gu +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + + +@Serializable +data class DistrictDataDto( + @SerialName("districtDtos") + val districtDtos: List +) + +@Serializable +data class DistrictDto( + @SerialName("gu") + val gu: GuDto, + + @SerialName("dongs") + val dongs: List +) + +@Serializable +data class GuDto( + @SerialName("id") + val id: Int, + + @SerialName("name") + val name: String +) + +@Serializable +data class DongDto( + @SerialName("id") + val id: Int, + + @SerialName("name") + val name: String +) + +fun DistrictDto.toEntity(): DistrictEntity { + return DistrictEntity( + gu = gu.toEntity(), + dongs = dongs.map { it.toEntity() } + ) +} + +fun GuDto.toEntity(): Gu { + return Gu( + id = id, + name = name + ) +} + +fun DongDto.toEntity(): Dong { + return Dong( + id = id, + name = name + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/data/local/datasource/.gitkeep b/app/src/main/java/com/paw/key/data/local/datasource/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/app/src/main/java/com/paw/key/domain/entity/signup/DistrictEntity.kt b/app/src/main/java/com/paw/key/domain/entity/signup/DistrictEntity.kt new file mode 100644 index 00000000..89daa5e1 --- /dev/null +++ b/app/src/main/java/com/paw/key/domain/entity/signup/DistrictEntity.kt @@ -0,0 +1,17 @@ +package com.paw.key.domain.entity.signup + + +data class DistrictEntity( + val gu: Gu, + val dongs: List +) + +data class Gu( + val id: Int, + val name: String +) + +data class Dong( + val id: Int, + val name: String +) \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/domain/model/entity/signup/DistrictEntity.kt b/app/src/main/java/com/paw/key/domain/model/entity/signup/DistrictEntity.kt deleted file mode 100644 index 85833a26..00000000 --- a/app/src/main/java/com/paw/key/domain/model/entity/signup/DistrictEntity.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.paw.key.domain.model.entity.signup - -import DistrictDto -import DongDto -import GuDto - -data class DistrictEntity( - val gu: Gu, - val dongs: List -) - -data class Gu( - val id: Int, - val name: String -) - -data class Dong( - val id: Int, - val name: String -) - -fun DistrictDto.toEntity(): DistrictEntity { - return DistrictEntity( - gu = gu.toEntity(), - dongs = dongs.map { it.toEntity() } - ) -} - -fun GuDto.toEntity(): Gu { - return Gu( - id = id, - name = name - ) -} - -fun DongDto.toEntity(): Dong { - return Dong( - id = id, - name = name - ) -} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/signup/SignUpLocationInfoScreen.kt b/app/src/main/java/com/paw/key/presentation/ui/signup/SignUpLocationInfoScreen.kt index 7a642951..37293b74 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/signup/SignUpLocationInfoScreen.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/signup/SignUpLocationInfoScreen.kt @@ -6,6 +6,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -22,20 +23,29 @@ import com.paw.key.core.extension.noRippleClickable import com.paw.key.presentation.ui.signup.component.FormField import com.paw.key.presentation.ui.signup.component.RegionSearchContent import com.paw.key.presentation.ui.signup.component.SignUpTextField +import com.paw.key.presentation.ui.signup.model.DongModel +import com.paw.key.presentation.ui.signup.model.GuModel +import com.paw.key.presentation.ui.signup.model.SignUpLocationInfo import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @Composable fun SignUpLocationInfoScreen( - gu : String, - dong : String, - onSelectedLocation : (gu : String, dong : String) -> Unit, + locationInfo: SignUpLocationInfo, + getRegions: () -> Unit, + onRegionSelected: (GuModel, DongModel) -> Unit, modifier: Modifier = Modifier ) { val scope = rememberCoroutineScope() val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) var isSheetOpen by remember { mutableStateOf(false) } + if (locationInfo.regionList.isEmpty()) { + LaunchedEffect(Unit) { + getRegions() + } + } + Column ( modifier = modifier .padding( @@ -48,7 +58,7 @@ fun SignUpLocationInfoScreen( label = "선택 지역", content = { SignUpTextField( - value = if (gu.isNotEmpty() && dong.isNotEmpty()) "$gu $dong" else "", + value = if (locationInfo.selectedGu.name.isNotEmpty() && locationInfo.selectedDong.name.isNotEmpty()) "${locationInfo.selectedGu.name} ${locationInfo.selectedDong.name}" else "", onValueChange = {}, enabled = false, placeholder = "주로 산책하는 지역을 검색해보세요", @@ -76,11 +86,16 @@ fun SignUpLocationInfoScreen( onDismissRequest = { isSheetOpen = false }, ) { RegionSearchContent( - selectedGu = gu, - selectedDong = dong, + regionList = locationInfo.regionList, + selectedGu = locationInfo.selectedGu, + selectedDong = locationInfo.selectedDong, onRegionSelected = { gu, dong -> - isSheetOpen = false - onSelectedLocation(gu, dong) + scope.launch { sheetState.hide() }.invokeOnCompletion { + if (!sheetState.isVisible) { + isSheetOpen = false + onRegionSelected(gu, dong) + } + } } ) } diff --git a/app/src/main/java/com/paw/key/presentation/ui/signup/SignUpPetInfoScreen.kt b/app/src/main/java/com/paw/key/presentation/ui/signup/SignUpPetInfoScreen.kt index 71f44d03..d7ebbcea 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/signup/SignUpPetInfoScreen.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/signup/SignUpPetInfoScreen.kt @@ -6,6 +6,7 @@ import android.os.Build import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.PickVisualMediaRequest import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height @@ -16,6 +17,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -26,6 +28,7 @@ import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.input.ImeAction @@ -41,24 +44,24 @@ import com.paw.key.presentation.ui.signup.component.PetBreedSearchContent import com.paw.key.presentation.ui.signup.component.SignUpNeuteringCheckRadio import com.paw.key.presentation.ui.signup.component.SignUpPetImageHolder import com.paw.key.presentation.ui.signup.component.SignUpTextField +import com.paw.key.presentation.ui.signup.model.PetInfoItemModel +import com.paw.key.presentation.ui.signup.model.SignUpPetInfo import com.paw.key.presentation.ui.signup.state.Gender +import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @Composable fun SignUpPetInfoScreen( - petName : String, - petBirthDate : String, - petGender : Gender, - petNeutered : Boolean, - petBreed : String, - selectedImageUri: Uri?, + petInfo: SignUpPetInfo, + petBreedList: ImmutableList, + requestPetInfo : () -> Unit, deniedPermission: () -> Unit, onPetNameChanged : (String) -> Unit, onPetBirthDateChanged : (String) -> Unit, onPetGenderChanged : (Gender) -> Unit, onPetNeuteredChanged : (Boolean) -> Unit, - onPetBreedChanged : (String) -> Unit, + onPetBreedChanged : (PetInfoItemModel) -> Unit, onSelectedImage: (Uri?) -> Unit, modifier: Modifier = Modifier ) { @@ -95,6 +98,12 @@ fun SignUpPetInfoScreen( } ) + if (petBreedList.isEmpty()) { + LaunchedEffect(Unit) { + requestPetInfo() + } + } + Column( modifier = modifier .padding( @@ -104,7 +113,7 @@ fun SignUpPetInfoScreen( ) ) { SignUpPetImageHolder( - uri = selectedImageUri, + uri = petInfo.petImage, modifier = Modifier .noRippleClickable { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { @@ -123,7 +132,7 @@ fun SignUpPetInfoScreen( label = "이름", content = { SignUpTextField( - value = petName, + value = petInfo.petName, onValueChange = { if (it.length <= 8) { onPetNameChanged(it) @@ -150,7 +159,7 @@ fun SignUpPetInfoScreen( SignUpTextField( modifier = Modifier .focusRequester(petBirthDateFocusRequester), - value = petBirthDate, + value = petInfo.petBirthDate, onValueChange = { if (it.length <= 8) { onPetBirthDateChanged(it) @@ -177,7 +186,7 @@ fun SignUpPetInfoScreen( label = "성별", content = { GenderSelector( - selectedGender = petGender, + selectedGender = petInfo.petGender, onGenderSelected = onPetGenderChanged, type = "반려 동물" ) @@ -185,8 +194,8 @@ fun SignUpPetInfoScreen( ) SignUpNeuteringCheckRadio( - isNeutered = petNeutered, - onToggle = { onPetNeuteredChanged(!petNeutered) }, + isNeutered = petInfo.petNeutered, + onToggle = { onPetNeuteredChanged(!petInfo.petNeutered) }, modifier = Modifier .padding(top = 8.dp) ) @@ -197,8 +206,8 @@ fun SignUpPetInfoScreen( label = "견종", content = { SignUpTextField( - value = petBreed, - onValueChange = onPetBreedChanged, + value = petInfo.petBreed.name, + onValueChange = {}, enabled = false, placeholder = "견종을 검색해보세요", suffix = { @@ -224,13 +233,18 @@ fun SignUpPetInfoScreen( PawKeyBottomSheet( onDismissRequest = { isSheetOpen = false }, sheetState = sheetState, - //sheetGesturesEnabled = false, + modifier = Modifier + .pointerInput(Unit) { + detectTapGestures { + } + } ) { sheetState -> PetBreedSearchContent( + petBreedList = petBreedList, sheetState = sheetState, - selectedBreed = petBreed, - onBreedSelected = { - onPetBreedChanged(it) + selectedBreed = petInfo.petBreed.name, + onBreedSelected = { selectedModel: PetInfoItemModel -> + onPetBreedChanged(selectedModel) scope.launch { sheetState.hide() diff --git a/app/src/main/java/com/paw/key/presentation/ui/signup/SignUpScreen.kt b/app/src/main/java/com/paw/key/presentation/ui/signup/SignUpScreen.kt index 437ef3ea..9b1e4d6e 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/signup/SignUpScreen.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/signup/SignUpScreen.kt @@ -2,6 +2,8 @@ package com.paw.key.presentation.ui.signup import android.net.Uri import androidx.activity.compose.BackHandler +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -20,15 +22,20 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.flowWithLifecycle import com.paw.key.core.designsystem.component.DokiButton 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.presentation.ui.signup.component.SignUpHeader import com.paw.key.presentation.ui.signup.component.SignUpSubHeader +import com.paw.key.presentation.ui.signup.model.DongModel +import com.paw.key.presentation.ui.signup.model.GuModel +import com.paw.key.presentation.ui.signup.model.PetInfoItemModel import com.paw.key.presentation.ui.signup.model.SignUpLocationInfo import com.paw.key.presentation.ui.signup.model.SignUpMapInfo import com.paw.key.presentation.ui.signup.model.SignUpPetInfo import com.paw.key.presentation.ui.signup.model.SignUpUserInfo import com.paw.key.presentation.ui.signup.state.Gender import com.paw.key.presentation.ui.signup.state.SignUpSideEffect +import com.paw.key.presentation.ui.signup.state.SignUpState import com.paw.key.presentation.ui.signup.state.SignUpStateType import com.paw.key.presentation.ui.signup.viewmodel.SignUpViewModel @@ -65,39 +72,53 @@ fun SignUpRoute( } } - SignUpScreen( - currentStep = state.currentStep, - type = state.signUpState, - isNextEnabled = state.isNextEnabled, + Box( + modifier = Modifier + .fillMaxSize() + .background(color = PawKeyTheme.colors.background) + ) { + SignUpScreen( + state = state, + currentStep = state.currentStep, + type = state.signUpState, + isNextEnabled = state.isNextEnabled, + + onNextClick = viewModel::onNextClick, + onBackClick = viewModel::onBackPressed, - onNextClick = viewModel::onNextClick, - onBackClick = viewModel::onBackPressed, + userInfo = state.userInfo, + onNickNameChanged = { viewModel.updateNickname(it) }, + onBirthDateChanged = { viewModel.updateBirthDate(it) }, + onGenderChanged = viewModel::updateGender, - userInfo = state.userInfo, - onNickNameChanged = { viewModel.updateNickname(it) }, - onBirthDateChanged = { viewModel.updateBirthDate(it) }, - onGenderChanged = viewModel::updateGender, + petInfo = state.petInfo, + onPetNameChanged = { viewModel.updatePetName(it) }, + onPetBirthDateChanged = { viewModel.updatePetBirthDate(it) }, + onPetGenderChanged = viewModel::updatePetGender, + onPetNeuteredChanged = viewModel::updatePetNeutered, + onPetBreedChanged = { viewModel.updatePetBreed(it) }, + deniedPermission = viewModel::deniedPermission, + onSelectedImage = viewModel::updatePetImage, + requestPetInfo = viewModel::getPetInfo, - petInfo = state.petInfo, - onPetNameChanged = { viewModel.updatePetName(it) }, - onPetBirthDateChanged = { viewModel.updatePetBirthDate(it) }, - onPetGenderChanged = viewModel::updatePetGender, - onPetNeuteredChanged = viewModel::updatePetNeutered, - onPetBreedChanged = { viewModel.updatePetBreed(it) }, - deniedPermission = viewModel::deniedPermission, - onSelectedImage = viewModel::updatePetImage, + locationInfo = state.locationInfo, + getRegions = viewModel::getRegions, + onRegionSelected = { gu, dong -> + viewModel.updateLocation(gu, dong) + }, - locationInfo = state.locationInfo, - onSelectedLocation = { gu, dong -> - viewModel.onRegionSelected(gu, dong) - }, + mapInfo = state.mapInfo, + ) - mapInfo = state.mapInfo, - ) + if (state.isLoading) { + LoadingScreen() + } + } } @Composable fun SignUpScreen( + state: SignUpState, userInfo : SignUpUserInfo, onNickNameChanged: (String) -> Unit, onBirthDateChanged: (String) -> Unit, @@ -109,11 +130,13 @@ fun SignUpScreen( onPetBirthDateChanged : (String) -> Unit, onPetGenderChanged : (Gender) -> Unit, onPetNeuteredChanged : (Boolean) -> Unit, - onPetBreedChanged : (String) -> Unit, + onPetBreedChanged : (PetInfoItemModel) -> Unit, onSelectedImage: (Uri?) -> Unit, + requestPetInfo : () -> Unit, locationInfo : SignUpLocationInfo, - onSelectedLocation : (gu : String, dong : String) -> Unit, + getRegions: () -> Unit, + onRegionSelected: (GuModel, DongModel) -> Unit, mapInfo: SignUpMapInfo, @@ -150,6 +173,7 @@ fun SignUpScreen( Column ( modifier = Modifier .fillMaxSize() + .background(color = PawKeyTheme.colors.background) ) { SignUpHeader( title = title, @@ -185,6 +209,7 @@ fun SignUpScreen( Column( modifier = Modifier .weight(1f) + .background(color = PawKeyTheme.colors.background) .verticalScroll(rememberScrollState()) ) { when (type) { @@ -203,12 +228,8 @@ fun SignUpScreen( SignUpStateType.PET_INFO -> { SignUpPetInfoScreen( - petName = petInfo.petName, - petBirthDate = petInfo.petBirthDate, - petGender = petInfo.petGender, - petNeutered = petInfo.petNeutered, - petBreed = petInfo.petBreed, - selectedImageUri = petInfo.petImage, + petInfo = petInfo, + petBreedList = state.petBreedList, onPetNameChanged = onPetNameChanged, onPetBirthDateChanged = onPetBirthDateChanged, onPetGenderChanged = onPetGenderChanged, @@ -218,17 +239,16 @@ fun SignUpScreen( onSelectedImage = { onSelectedImage(it) }, + requestPetInfo = requestPetInfo, modifier = Modifier ) } SignUpStateType.LOCATION_INFO -> { SignUpLocationInfoScreen( - gu = locationInfo.selectedGu, - dong = locationInfo.selectedDong, - onSelectedLocation = { gu, dong -> - onSelectedLocation(gu, dong) - }, + locationInfo = locationInfo, + getRegions = getRegions, + onRegionSelected = onRegionSelected, modifier = Modifier ) } diff --git a/app/src/main/java/com/paw/key/presentation/ui/signup/component/SignUpHeader.kt b/app/src/main/java/com/paw/key/presentation/ui/signup/component/SignUpHeader.kt index 184897ed..79d8d5fb 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/signup/component/SignUpHeader.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/signup/component/SignUpHeader.kt @@ -3,6 +3,7 @@ package com.paw.key.presentation.ui.signup.component import androidx.compose.animation.core.FastOutSlowInEasing import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth @@ -58,6 +59,7 @@ fun SignUpHeader( Column ( modifier = modifier .fillMaxWidth() + .background(color = PawKeyTheme.colors.background) ){ Box ( modifier = Modifier diff --git a/app/src/main/java/com/paw/key/presentation/ui/signup/component/SignUpSubHeader.kt b/app/src/main/java/com/paw/key/presentation/ui/signup/component/SignUpSubHeader.kt index d1bef213..0b1419e3 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/signup/component/SignUpSubHeader.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/signup/component/SignUpSubHeader.kt @@ -1,5 +1,6 @@ package com.paw.key.presentation.ui.signup.component +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -11,28 +12,34 @@ import androidx.compose.ui.unit.dp import com.paw.key.core.designsystem.theme.PawKeyTheme @Composable -fun SignUpSubHeader() { - Text( - text = "산책하기 전 \n간단한 정보를 입력해주세요", - color = PawKeyTheme.colors.contents, - style = PawKeyTheme.typography.header2, - modifier = Modifier - .padding( - top = 20.dp, - start = 16.dp, - end = 16.dp - ) - ) +fun SignUpSubHeader( + modifier: Modifier = Modifier +) { + Column ( + modifier = modifier + ){ + Text( + text = "산책하기 전 \n간단한 정보를 입력해주세요", + color = PawKeyTheme.colors.contents, + style = PawKeyTheme.typography.header2, + modifier = Modifier + .padding( + top = 20.dp, + start = 16.dp, + end = 16.dp + ) + ) - Spacer(modifier = Modifier.height(4.dp)) + Spacer(modifier = Modifier.height(4.dp)) - Text( - text = "서비스 시작을 위해 간단한 정보를 입력해주세요!", - color = PawKeyTheme.colors.defaultMiddle, - style = PawKeyTheme.typography.bodyDefault, - modifier = Modifier - .padding(horizontal = 16.dp) - ) + Text( + text = "서비스 시작을 위해 간단한 정보를 입력해주세요!", + color = PawKeyTheme.colors.defaultMiddle, + style = PawKeyTheme.typography.bodyDefault, + modifier = Modifier + .padding(horizontal = 16.dp) + ) + } } @Preview diff --git a/app/src/main/java/com/paw/key/presentation/ui/signup/model/SignUpLocationInfo.kt b/app/src/main/java/com/paw/key/presentation/ui/signup/model/SignUpLocationInfo.kt index 1f0fa190..56475867 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/signup/model/SignUpLocationInfo.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/signup/model/SignUpLocationInfo.kt @@ -1,9 +1,49 @@ package com.paw.key.presentation.ui.signup.model import androidx.compose.runtime.Immutable +import com.paw.key.domain.entity.signup.DistrictEntity +import com.paw.key.domain.entity.signup.Dong +import com.paw.key.domain.entity.signup.Gu +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList @Immutable -data class SignUpLocationInfo( // 바텀시트 지역 선택 시 보여주기 위함 - val selectedGu: String = "", - val selectedDong: String = "", +data class SignUpLocationInfo( + val regionList: ImmutableList = persistentListOf(), + val selectedGu: GuModel = GuModel(0, ""), + val selectedDong: DongModel = DongModel(0, ""), +) + +@Immutable +data class DistrictModel( + val gu: GuModel, + val dongs: ImmutableList = persistentListOf() +) + +@Immutable +data class GuModel( + val id: Int, + val name: String +) + +@Immutable +data class DongModel( + val id: Int, + val name: String +) + +fun Dong.toState() = DongModel( + id = id, + name = name +) + +fun Gu.toState() = GuModel( + id = id, + name = name +) + +fun DistrictEntity.toState() = DistrictModel( + gu = gu.toState(), + dongs = dongs.map { it.toState() }.toImmutableList() ) \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/signup/model/SignUpPetInfo.kt b/app/src/main/java/com/paw/key/presentation/ui/signup/model/SignUpPetInfo.kt index 8e81a8dd..898ecc01 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/signup/model/SignUpPetInfo.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/signup/model/SignUpPetInfo.kt @@ -2,6 +2,7 @@ package com.paw.key.presentation.ui.signup.model import android.net.Uri import androidx.compose.runtime.Immutable +import com.paw.key.domain.entity.user.PetBreedsItemEntity import com.paw.key.presentation.ui.signup.state.Gender @Immutable @@ -11,5 +12,16 @@ data class SignUpPetInfo( val petBirthDate : String = "", val petGender : Gender = Gender.UNKNOWN, val petNeutered : Boolean = false, - val petBreed : String = "", + val petBreed : PetInfoItemModel = PetInfoItemModel(), +) + +@Immutable +data class PetInfoItemModel( + val id : Int = 0, + val name: String = "" +) + +fun PetBreedsItemEntity.toState() = PetInfoItemModel( + id = id, + name = name ) \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/signup/state/SignUpContract.kt b/app/src/main/java/com/paw/key/presentation/ui/signup/state/SignUpContract.kt index 0767f40b..c08de92b 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/signup/state/SignUpContract.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/signup/state/SignUpContract.kt @@ -1,21 +1,26 @@ package com.paw.key.presentation.ui.signup.state import androidx.compose.runtime.Immutable +import com.paw.key.presentation.ui.signup.model.PetInfoItemModel import com.paw.key.presentation.ui.signup.model.SignUpLocationInfo import com.paw.key.presentation.ui.signup.model.SignUpMapInfo import com.paw.key.presentation.ui.signup.model.SignUpPetInfo import com.paw.key.presentation.ui.signup.model.SignUpUserInfo +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf @Immutable data class SignUpState( val userInfo: SignUpUserInfo = SignUpUserInfo(), val petInfo: SignUpPetInfo = SignUpPetInfo(), + val petBreedList : ImmutableList = persistentListOf(), val locationInfo: SignUpLocationInfo = SignUpLocationInfo(), val mapInfo: SignUpMapInfo = SignUpMapInfo(), val signUpState: SignUpStateType = SignUpStateType.USER_INFO, val currentStep: Float = 1f, val isNextEnabled: Boolean = false, val isRegionComplete: Boolean = false, + val isLoading: Boolean = false, ) sealed class SignUpSideEffect { @@ -32,8 +37,10 @@ enum class SignUpStateType { REGION_MANAGEMENT, } -enum class Gender { - MALE, - FEMALE, - UNKNOWN +enum class Gender( + val value: String +) { + MALE("M"), + FEMALE("F"), + UNKNOWN("U") } diff --git a/app/src/main/java/com/paw/key/presentation/ui/signup/viewmodel/SignUpViewModel.kt b/app/src/main/java/com/paw/key/presentation/ui/signup/viewmodel/SignUpViewModel.kt index 8a9bd399..98b552e5 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/signup/viewmodel/SignUpViewModel.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/signup/viewmodel/SignUpViewModel.kt @@ -1,32 +1,34 @@ package com.paw.key.presentation.ui.signup.viewmodel -import android.content.ContentResolver import android.net.Uri import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.paw.key.core.util.PreferenceDataStore +import com.paw.key.core.extension.toBirthDateFormat import com.paw.key.core.util.UiState import com.paw.key.core.util.flattenCoordinatesToLatLng import com.paw.key.core.util.handleError +import com.paw.key.domain.entity.user.PetInfoEntity +import com.paw.key.domain.entity.user.UserInfoEntity import com.paw.key.domain.repository.RegionRepository -import com.paw.key.domain.repository.onboarding.OnboardingInfoRepository -import com.paw.key.domain.repository.onboarding.OnboardingRegionRepository -import com.paw.key.domain.repository.onboarding.OnboardingRepository +import com.paw.key.domain.repository.localstorage.LocalStorageRepository +import com.paw.key.domain.repository.user.UserRepository +import com.paw.key.domain.usecase.PostCreateUserUseCase import com.paw.key.presentation.ui.region.state.DrawType +import com.paw.key.presentation.ui.signup.model.DongModel +import com.paw.key.presentation.ui.signup.model.GuModel +import com.paw.key.presentation.ui.signup.model.PetInfoItemModel +import com.paw.key.presentation.ui.signup.model.toState import com.paw.key.presentation.ui.signup.state.Gender import com.paw.key.presentation.ui.signup.state.SignUpSideEffect import com.paw.key.presentation.ui.signup.state.SignUpState import com.paw.key.presentation.ui.signup.state.SignUpStateType import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import timber.log.Timber @@ -36,11 +38,10 @@ import javax.inject.Inject @HiltViewModel class SignUpViewModel @Inject constructor( - private val contentResolver : ContentResolver, - private val locationRegionRepository: RegionRepository, - private val repository: OnboardingRepository, - private val regionRepository: OnboardingRegionRepository, - private val infoRepository: OnboardingInfoRepository, + private val regionRepository: RegionRepository, + private val userRepository: UserRepository, + private val localRepository: LocalStorageRepository, + private val postCreateUserUseCase: PostCreateUserUseCase, ) : ViewModel() { private val _state = MutableStateFlow(SignUpState()) val state: StateFlow = _state.asStateFlow() @@ -48,13 +49,6 @@ class SignUpViewModel @Inject constructor( private val _sideEffect = MutableSharedFlow() val sideEffect: MutableSharedFlow = _sideEffect - private val userId : StateFlow = PreferenceDataStore.getUserId() - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5000), - initialValue = -1 - ) - fun deniedPermission() { viewModelScope.launch { _sideEffect.emit(SignUpSideEffect.ShowSnackBar("갤러리 접근 권한을 허용해주세요")) @@ -92,10 +86,6 @@ class SignUpViewModel @Inject constructor( currentState.copy( signUpState = SignUpStateType.LOCATION_INFO, isRegionComplete = false, - locationInfo = currentState.locationInfo.copy( - selectedGu = "", - selectedDong = "" - ) ) } } @@ -135,19 +125,17 @@ class SignUpViewModel @Inject constructor( } SignUpStateType.LOCATION_INFO -> { - // TODO: 서버에 값 전송 후 Home으로 - //submitRegistration() - // Todo : 서버 전송 시 uri를 contentresolver로 실제 사진 가져와서 전송하기 그리고 꼭 close하기 - //val inputStream = contentResolver.openInputStream(_state.value.petInfo.petImage) - if (_state.value.isRegionComplete && _state.value.locationInfo.selectedGu.isNotBlank() && _state.value.locationInfo.selectedDong.isNotBlank()) { - _sideEffect.emit(SignUpSideEffect.NavigateHome) + if (_state.value.isRegionComplete && _state.value.locationInfo.selectedGu.name.isNotBlank() && _state.value.locationInfo.selectedDong.name.isNotBlank()) { + postCreateUser() } else { - updateState { + // Todo: 좌표값이 없어서 우선 여기서 종료 + /*updateState { it.copy( signUpState = SignUpStateType.REGION_MANAGEMENT, ) } - _sideEffect.emit(SignUpSideEffect.NavigateNext) + _sideEffect.emit(SignUpSideEffect.NavigateNext)*/ + postCreateUser() } } @@ -187,6 +175,87 @@ class SignUpViewModel @Inject constructor( } } + fun postCreateUser() { + viewModelScope.launch { + _state.update { it.copy(isLoading = true) } + postCreateUserUseCase( + userInfoEntity = UserInfoEntity( + name = _state.value.userInfo.nickName, + birth = _state.value.userInfo.birthDate.toBirthDateFormat(), + gender = _state.value.userInfo.gender.value, + dongId = _state.value.locationInfo.selectedDong.id, + pet = PetInfoEntity( + name = _state.value.petInfo.petName, + birth = _state.value.petInfo.petBirthDate.toBirthDateFormat(), + gender = _state.value.petInfo.petGender.value, + isNeutered = _state.value.petInfo.petNeutered, + breedId = _state.value.petInfo.petBreed.id, + imageId = -1 + ) + ), + petImageUri = _state.value.petInfo.petImage?.toString() + ).onSuccess { + + _state.update { it.copy(isLoading = false) } + _sideEffect.emit(SignUpSideEffect.NavigateHome) + }.onFailure(Timber::e) + + /*suspendRunCatching { + val currentState = _state.value + val petImageUri = currentState.petInfo.petImage + + val finalImageId: Int = if (petImageUri != null) { + val presignedResult = imageRepository.presignedImage( + presignedEntity = ImagePresignedEntity( + domain = ImageDomainType.PET_PROFILE, + contentType = "image/webp" + ) + ).getOrThrow() + + val registerImage = imageRepository.registerImage( + uriString = "${presignedResult.imageUrl}#${_state.value.petInfo.petImage}", + domainType = ImageDomainType.PET_PROFILE, + ).onFailure(Timber::e) + + if (!registerImage.isSuccess) { + throw Exception("이미지 업로드에 실패했습니다.") + } + + registerImage.getOrThrow().imageId + } else { + -1 + } + + userRepository.createUser( + userInfoEntity = UserInfoEntity( + name = _state.value.userInfo.nickName, + birth = _state.value.userInfo.birthDate.toBirthDateFormat(), + gender = _state.value.userInfo.gender.value, + dongId = _state.value.locationInfo.selectedDong.id, + pet = PetInfoEntity( + name = _state.value.petInfo.petName, + birth = _state.value.petInfo.petBirthDate.toBirthDateFormat(), + gender = _state.value.petInfo.petGender.value, + isNeutered = _state.value.petInfo.petNeutered, + breedId = _state.value.petInfo.petBreed.id, + imageId = finalImageId + ) + ) + ).onSuccess { + UserDataStore.saveUserId(context, it.userId) + UserDataStore.savePetId(context, it.petId) + } + }.onSuccess { + Timber.e("postCreateUser success") + _sideEffect.emit(SignUpSideEffect.NavigateHome) + }.onFailure { + Timber.e(it) + _sideEffect.emit(SignUpSideEffect.ShowSnackBar(it.message ?: "알 수 없는 오류가 발생했습니다.")) + } + }*/ + } + } + // userinfo fun updateNickname(nickname: String) { viewModelScope.launch { @@ -221,6 +290,19 @@ class SignUpViewModel @Inject constructor( } // petinfo + fun getPetInfo() { + viewModelScope.launch { + userRepository.getPetBreeds() + .onSuccess { + updateState { currentState -> + currentState.copy( + petBreedList = it.breedList.map { it.toState() }.toImmutableList() + ) + } + } + .onFailure(Timber::e) + } + } fun updatePetName(name: String) { viewModelScope.launch { updateState { currentState -> @@ -263,7 +345,7 @@ class SignUpViewModel @Inject constructor( } } - fun updatePetBreed(breed: String) { + fun updatePetBreed(breed: PetInfoItemModel) { viewModelScope.launch { updateState { currentState -> currentState.copy( @@ -284,44 +366,62 @@ class SignUpViewModel @Inject constructor( } // location - fun onRegionSelected(gu: String, dong: String) { - updateState { currentState -> - // 원래 선택한 구와 동이 같으면 완료로 아니라면 다시 지도뷰 - if (gu != currentState.locationInfo.selectedGu || dong != currentState.locationInfo.selectedDong) { - currentState.copy( - locationInfo = currentState.locationInfo.copy( - selectedGu = gu, - selectedDong = dong, - ), - isRegionComplete = false, - signUpState = SignUpStateType.LOCATION_INFO - ) - } else { - currentState.copy( - locationInfo = currentState.locationInfo.copy( - selectedGu = gu, - selectedDong = dong, - ), - signUpState = SignUpStateType.LOCATION_INFO - ) + + fun getRegions() { + viewModelScope.launch { + regionRepository.getRegionList() + .onSuccess { result -> + updateState { currentState -> + currentState.copy( + locationInfo = currentState.locationInfo.copy( + regionList = result.map { it.toState() }.toImmutableList() + ) + ) + } + }.onFailure(Timber::e) + } + } + + fun updateLocation(gu: GuModel, dong: DongModel) { + viewModelScope.launch { + updateState { currentState -> + // 원래 선택한 구와 동이 같으면 완료로 아니라면 다시 지도뷰 + if (gu != currentState.locationInfo.selectedGu || dong != currentState.locationInfo.selectedDong) { + currentState.copy( + locationInfo = currentState.locationInfo.copy( + selectedGu = gu, + selectedDong = dong, + ), + isRegionComplete = false, + signUpState = SignUpStateType.LOCATION_INFO + ) + } else { + currentState.copy( + locationInfo = currentState.locationInfo.copy( + selectedGu = gu, + selectedDong = dong, + ), + signUpState = SignUpStateType.LOCATION_INFO + ) + } } + getRegionGeometryList() } - getRegion() } - fun getRegion() { + fun getRegionGeometryList() { viewModelScope.launch { - val validUserId = userId.filter { it != -1 }.first() + val dongId = _state.value.locationInfo.selectedDong.id getRegionGeometry( - userId = validUserId, - regionId = 39, + userId = localRepository.getUserId(), + regionId = dongId, ) onNextClick() } } fun getRegionGeometry(userId: Int, regionId: Int?) = viewModelScope.launch { - locationRegionRepository.getRegionGeometry(userId, regionId!!) + regionRepository.getRegionGeometry(userId, regionId!!) .onSuccess { data -> val coordinates = data.geometry.coordinates val flattenedLatLng = flattenCoordinatesToLatLng(coordinates) @@ -391,19 +491,15 @@ class SignUpViewModel @Inject constructor( state.petInfo.petName.isNotBlank() && state.petInfo.petName.length <= 8 && state.petInfo.petBirthDate.length == 8 && state.petInfo.petBirthDate.isValidDate() && state.petInfo.petGender != Gender.UNKNOWN && - state.petInfo.petBreed.isNotBlank() && + state.petInfo.petBreed.name.isNotBlank() && state.petInfo.petImage != null } - SignUpStateType.LOCATION_INFO -> { - // LocationInfo 확인 - state.locationInfo.selectedGu.isNotBlank() && - state.locationInfo.selectedDong.isNotBlank() - } - - SignUpStateType.REGION_MANAGEMENT -> { - state.locationInfo.selectedGu.isNotBlank() && - state.locationInfo.selectedDong.isNotBlank() + SignUpStateType.LOCATION_INFO, SignUpStateType.REGION_MANAGEMENT -> { + state.locationInfo.selectedGu.id != 0 && + state.locationInfo.selectedGu.name.isNotBlank() && + state.locationInfo.selectedDong.id != 0 && + state.locationInfo.selectedDong.name.isNotBlank() } } } From fa508302339d2da5636b360bfc52bb1da780ebc0 Mon Sep 17 00:00:00 2001 From: sonms Date: Fri, 20 Feb 2026 00:03:33 +0900 Subject: [PATCH 47/47] =?UTF-8?q?mod/#154=20=ED=8C=8C=EC=9D=BC=20=EC=9D=B4?= =?UTF-8?q?=EB=8F=99=20=EB=B0=8F=20=EC=82=AD=EC=A0=9C=EB=A1=9C=20=EC=9D=B8?= =?UTF-8?q?=ED=95=9C=20import=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../designsystem/component/CourseDetail.kt | 2 +- .../designsystem/component/LoadingScreen.kt | 11 +- .../com/paw/key/core/extension/Modifier.kt | 26 +++ .../com/paw/key/data/di/RepositoryModule.kt | 65 ++++--- .../java/com/paw/key/data/di/ServiceModule.kt | 32 ++-- .../walkcourse/WalkCourseRequestDto.kt | 4 +- .../dto/response/ArchivedListResponseDto.kt | 6 +- .../key/data/dto/response/LoginResponseDto.kt | 4 +- .../data/dto/response/SavedListResponseDto.kt | 9 +- .../response/filter/FilterOptionResponse.kt | 11 +- .../response/home/RegionCurrentResponseDto.kt | 2 +- .../dto/response/list/PostsListResponseDto.kt | 6 +- .../petprofile/PetProfileResponseDto.kt | 4 +- .../sharedwalk/SharedWalkResponseDto.kt | 4 +- .../walkcourse/WalkCourseResponseDto.kt | 2 +- .../walklist/WalkReviewDetailResponseDto.kt | 6 +- .../walklist/WalkReviewSummaryResponseDto.kt | 4 +- .../WalkReviewCategoryResponseDto.kt | 6 +- .../walkreview/WalkReviewInfoResponseDto.kt | 4 +- .../walkreview/WalkReviewResponseDto.kt | 2 +- .../com/paw/key/data/mapper/RegionMapper.kt | 4 +- .../remote/datasource/PetProfileDataSource.kt | 4 - .../remote/datasource/RegionDataSource.kt | 2 +- .../datasource/UserProfileDataSource.kt | 5 - .../datasource/home/HomeRegionDataSource.kt | 1 - .../sharedwalk/SharedWalkDataSource.kt | 1 - .../ArchivedListRepositoryImpl.kt | 3 +- .../PetProfileRepositoryImpl.kt | 2 +- .../repositoryimpl/RegionRepositoryImpl.kt | 6 +- .../repositoryimpl/SavedListRepositoryImpl.kt | 3 +- .../WalkCourseRepositoryImpl.kt | 2 +- .../WalkSharedResultRepositoryImpl.kt | 7 +- .../filter/FilterOptionRepositoryImpl.kt | 4 +- .../home/HomeRegionRepositoryImpl.kt | 2 +- .../home/RegionCurrentRepositoryImpl.kt | 2 +- .../list/PostsListRepositoryImpl.kt | 2 +- .../sharedwalk/SharedWalkRepositoryImpl.kt | 5 +- .../walklist/WalkListDetailRepositoryImpl.kt | 5 +- .../walkreview/WalkReviewRepositoryImpl.kt | 8 +- .../entity/archivedlist/ArchivedListEntity.kt | 2 +- .../{model => }/entity/filter/FilterEntity.kt | 2 +- .../{model => }/entity/home/HomeEntity.kt | 2 +- .../{model => }/entity/list/ListEntity.kt | 2 +- .../{model => }/entity/login/LoginModel.kt | 2 +- .../entity/petprofile/PetProfileEntity.kt | 2 +- .../{model => }/entity/region/RegionEntity.kt | 2 +- .../entity/savedlist/SavedListEntity.kt | 2 +- .../entity/sharedresult/WalkResult.kt | 2 +- .../entity/sharedwalk/SharedWalkEntity.kt | 2 +- .../sharedwalk/SharedWalkReviewEntity.kt | 2 +- .../entity/userprofile/UserProfileEntity.kt | 0 .../entity/walkcourse/WalkCourseEntity.kt | 2 +- .../entity/walklist/WalkListDetailEntity.kt | 2 +- .../walklist/WalkReviewSummaryEntity.kt | 2 +- .../walkreview/WalkReviewCategoryEntity.kt | 2 +- .../entity/walkreview/WalkReviewEntity.kt | 2 +- .../entity/walkreview/WalkReviewIdEntity.kt | 2 +- .../walkreview/WalkReviewRecordEntity.kt | 2 +- .../repository/ArchivedListRepository.kt | 3 +- .../key/domain/repository/RegionRepository.kt | 4 +- .../domain/repository/SavedListRepository.kt | 3 +- .../repository/WalkSharedResultRepository.kt | 2 +- .../filter/FilterOptionRepository.kt | 2 +- .../repository/home/HomeRegionRepository.kt | 2 +- .../home/RegionCurrentRepository.kt | 2 +- .../repository/list/PostsListRepository.kt | 2 +- .../petprofile/PetProfileRepository.kt | 2 +- .../sharedwalk/SharedWalkRepository.kt | 4 +- .../walkcourse/WalkCourseRepository.kt | 2 +- .../repository/walklist/WalkListRepository.kt | 4 +- .../walkreview/WalkReviewRepository.kt | 10 +- .../viewmodel/WalkCourseViewModel.kt | 4 +- .../key/presentation/ui/login/LoginScreen.kt | 89 ++++------ .../ui/login/navigation/LoginNavigation.kt | 2 + .../ui/login/state/LoginContract.kt | 12 +- .../ui/login/viewmodel/LoginViewModel.kt | 48 ++---- .../key/presentation/ui/main/MainActivity.kt | 2 - .../key/presentation/ui/main/MainNavigator.kt | 6 - .../key/presentation/ui/main/PawKeyNavHost.kt | 34 ++-- .../ui/mypage/main/MyPageScreen.kt | 9 - .../mypage/main/viewmodel/MyPageViewModel.kt | 10 ++ .../ui/mypage/petinfo/PetProfileScreen.kt | 14 +- .../petinfo/viewmodel/PetProfileViewModel.kt | 11 +- .../ui/mypage/userinfo/UserProfileScreen.kt | 8 - .../viewmodel/UserProfileViewModel.kt | 14 +- .../ui/region/viewmodel/RegionViewModel.kt | 26 +-- .../signup/component/PetBreedSearchContent.kt | 80 +++++---- .../signup/component/RegionSearchContent.kt | 163 +++++++----------- 88 files changed, 393 insertions(+), 495 deletions(-) rename app/src/main/java/com/paw/key/domain/{model => }/entity/archivedlist/ArchivedListEntity.kt (90%) rename app/src/main/java/com/paw/key/domain/{model => }/entity/filter/FilterEntity.kt (93%) rename app/src/main/java/com/paw/key/domain/{model => }/entity/home/HomeEntity.kt (78%) rename app/src/main/java/com/paw/key/domain/{model => }/entity/list/ListEntity.kt (91%) rename app/src/main/java/com/paw/key/domain/{model => }/entity/login/LoginModel.kt (64%) rename app/src/main/java/com/paw/key/domain/{model => }/entity/petprofile/PetProfileEntity.kt (88%) rename app/src/main/java/com/paw/key/domain/{model => }/entity/region/RegionEntity.kt (86%) rename app/src/main/java/com/paw/key/domain/{model => }/entity/savedlist/SavedListEntity.kt (90%) rename app/src/main/java/com/paw/key/domain/{model => }/entity/sharedresult/WalkResult.kt (80%) rename app/src/main/java/com/paw/key/domain/{model => }/entity/sharedwalk/SharedWalkEntity.kt (77%) rename app/src/main/java/com/paw/key/domain/{model => }/entity/sharedwalk/SharedWalkReviewEntity.kt (94%) rename app/src/main/java/com/paw/key/domain/{model => }/entity/userprofile/UserProfileEntity.kt (100%) rename app/src/main/java/com/paw/key/domain/{model => }/entity/walkcourse/WalkCourseEntity.kt (94%) rename app/src/main/java/com/paw/key/domain/{model => }/entity/walklist/WalkListDetailEntity.kt (92%) rename app/src/main/java/com/paw/key/domain/{model => }/entity/walklist/WalkReviewSummaryEntity.kt (87%) rename app/src/main/java/com/paw/key/domain/{model => }/entity/walkreview/WalkReviewCategoryEntity.kt (88%) rename app/src/main/java/com/paw/key/domain/{model => }/entity/walkreview/WalkReviewEntity.kt (84%) rename app/src/main/java/com/paw/key/domain/{model => }/entity/walkreview/WalkReviewIdEntity.kt (60%) rename app/src/main/java/com/paw/key/domain/{model => }/entity/walkreview/WalkReviewRecordEntity.kt (95%) diff --git a/app/src/main/java/com/paw/key/core/designsystem/component/CourseDetail.kt b/app/src/main/java/com/paw/key/core/designsystem/component/CourseDetail.kt index a68fa864..64cc47df 100644 --- a/app/src/main/java/com/paw/key/core/designsystem/component/CourseDetail.kt +++ b/app/src/main/java/com/paw/key/core/designsystem/component/CourseDetail.kt @@ -46,7 +46,7 @@ import com.paw.key.R import com.paw.key.core.designsystem.theme.Gray100 import com.paw.key.core.designsystem.theme.PawKeyTheme import com.paw.key.core.extension.noRippleClickable -import com.paw.key.domain.model.entity.walklist.CategoryTop3Entity +import com.paw.key.domain.entity.walklist.CategoryTop3Entity @OptIn(ExperimentalLayoutApi::class) @Composable 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 index c8a143b9..f5e7bf29 100644 --- 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 @@ -1,12 +1,12 @@ package com.paw.key.core.designsystem.component +import androidx.compose.foundation.background 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 @@ -17,19 +17,22 @@ import com.paw.key.core.designsystem.theme.PawKeyTheme @Composable fun LoadingScreen() { Box( - modifier = Modifier.fillMaxSize(), + modifier = Modifier + .fillMaxSize() + .background( + color = PawKeyTheme.colors.contents.copy(alpha = 0.5f) + ), contentAlignment = Alignment.Center ) { Column( horizontalAlignment = Alignment.CenterHorizontally ) { CircularProgressIndicator( - color = PawKeyTheme.colors.green500 + color = PawKeyTheme.colors.primary ) Spacer(modifier = Modifier.height(8.dp)) - Text("현재 위치를 가져오는 중...") } } } diff --git a/app/src/main/java/com/paw/key/core/extension/Modifier.kt b/app/src/main/java/com/paw/key/core/extension/Modifier.kt index 03de9e45..e30752d9 100644 --- a/app/src/main/java/com/paw/key/core/extension/Modifier.kt +++ b/app/src/main/java/com/paw/key/core/extension/Modifier.kt @@ -5,6 +5,10 @@ import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.composed +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.NestedScrollSource +import androidx.compose.ui.input.nestedscroll.nestedScroll fun Modifier.noRippleClickable(onClick: () -> Unit): Modifier = composed { this.clickable( @@ -13,4 +17,26 @@ fun Modifier.noRippleClickable(onClick: () -> Unit): Modifier = composed { ) { onClick() } +} + +fun Modifier.disableNestedScroll(): Modifier = composed { + val connection = remember { + object : NestedScrollConnection { + override fun onPreScroll( + available: Offset, + source: NestedScrollSource + ): Offset { + return Offset.Zero + } + + override fun onPostScroll( + consumed: Offset, + available: Offset, + source: NestedScrollSource + ): Offset { + return available + } + } + } + this.nestedScroll(connection) } \ 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 9733454a..f7115e10 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,47 +1,45 @@ package com.paw.key.data.di +import com.paw.key.domain.repository.localstorage.LocalStorageRepository +import com.paw.key.data.repositoryimpl.localstorage.LocalStorageRepositoryImpl +import com.paw.key.data.remote.datasource.datasourceimpl.AuthRemoteDataSourceImpl +import com.paw.key.data.remote.datasource.datasourceimpl.GoogleAuthDataSourceImpl +import com.paw.key.data.remote.datasource.datasourceimpl.KakaoAuthDataSourceImpl +import com.paw.key.data.remote.datasource.login.AuthRemoteDataSource +import com.paw.key.data.remote.datasource.login.GoogleAuthDataSource +import com.paw.key.data.remote.datasource.login.KakaoAuthDataSource import com.paw.key.data.repositoryimpl.ArchivedListRepositoryImpl -import com.paw.key.data.repositoryimpl.DummyRepositoryImpl import com.paw.key.data.repositoryimpl.LikeRepositoryImpl import com.paw.key.data.repositoryimpl.PetProfileRepositoryImpl -import com.paw.key.data.repositoryimpl.onboarding.OnboardingInfoRepositoryImpl -import com.paw.key.data.repositoryimpl.onboarding.OnboardingRegionRepositoryImpl -import com.paw.key.data.repositoryimpl.onboarding.OnboardingRepositoryImpl import com.paw.key.data.repositoryimpl.RegionRepositoryImpl import com.paw.key.data.repositoryimpl.SavedListRepositoryImpl import com.paw.key.data.repositoryimpl.UserProfileRepositoryImpl import com.paw.key.data.repositoryimpl.WalkCourseRepositoryImpl import com.paw.key.data.repositoryimpl.WalkSharedResultRepositoryImpl import com.paw.key.data.repositoryimpl.filter.FilterOptionRepositoryImpl -import com.paw.key.data.repositoryimpl.sharedwalk.SharedWalkRepositoryImpl import com.paw.key.data.repositoryimpl.home.HomeRegionRepositoryImpl import com.paw.key.data.repositoryimpl.home.RegionCurrentRepositoryImpl +import com.paw.key.data.repositoryimpl.image.ImageRepositoryImpl import com.paw.key.data.repositoryimpl.list.PostsListRepositoryImpl import com.paw.key.data.repositoryimpl.login.AuthRepositoryImpl +import com.paw.key.data.repositoryimpl.sharedwalk.SharedWalkRepositoryImpl +import com.paw.key.data.repositoryimpl.user.UserRepositoryImpl import com.paw.key.data.repositoryimpl.walklist.WalkListDetailRepositoryImpl import com.paw.key.data.repositoryimpl.walkreview.WalkReviewRepositoryImpl -import com.paw.key.data.remote.datasource.datasourceimpl.AuthRemoteDataSourceImpl -import com.paw.key.data.remote.datasource.datasourceimpl.GoogleAuthDataSourceImpl -import com.paw.key.data.remote.datasource.datasourceimpl.KakaoAuthDataSourceImpl -import com.paw.key.data.remote.datasource.login.AuthRemoteDataSource -import com.paw.key.data.remote.datasource.login.GoogleAuthDataSource -import com.paw.key.data.remote.datasource.login.KakaoAuthDataSource import com.paw.key.domain.repository.ArchivedListRepository -import com.paw.key.domain.repository.DummyRepository import com.paw.key.domain.repository.LikeRepository -import com.paw.key.domain.repository.onboarding.OnboardingInfoRepository -import com.paw.key.domain.repository.onboarding.OnboardingRegionRepository -import com.paw.key.domain.repository.onboarding.OnboardingRepository import com.paw.key.domain.repository.RegionRepository import com.paw.key.domain.repository.SavedListRepository import com.paw.key.domain.repository.WalkSharedResultRepository import com.paw.key.domain.repository.filter.FilterOptionRepository -import com.paw.key.domain.repository.sharedwalk.SharedWalkRepository import com.paw.key.domain.repository.home.HomeRegionRepository import com.paw.key.domain.repository.home.RegionCurrentRepository +import com.paw.key.domain.repository.image.ImageRepository import com.paw.key.domain.repository.list.PostsListRepository import com.paw.key.domain.repository.login.AuthRepository import com.paw.key.domain.repository.petprofile.PetProfileRepository +import com.paw.key.domain.repository.sharedwalk.SharedWalkRepository +import com.paw.key.domain.repository.user.UserRepository import com.paw.key.domain.repository.userprofile.UserProfileRepository import com.paw.key.domain.repository.walkcourse.WalkCourseRepository import com.paw.key.domain.repository.walklist.WalkListRepository @@ -73,11 +71,6 @@ interface RepositoryModule { impl: KakaoAuthDataSourceImpl ): KakaoAuthDataSource - @Binds - fun bindsDummyRepository( - dummyRepositoryImpl: DummyRepositoryImpl - ): DummyRepository - @Binds @Singleton fun bindsSharedWalkResultRepository( @@ -99,21 +92,9 @@ interface RepositoryModule { @Binds @Singleton - fun bindOnboardingRepository( - impl: OnboardingRepositoryImpl - ): OnboardingRepository - - @Binds - @Singleton - fun bindOnboardingRegionRepository( - impl: OnboardingRegionRepositoryImpl - ): OnboardingRegionRepository - - @Binds - @Singleton - fun bindOnboardingInfoRepository( - impl: OnboardingInfoRepositoryImpl - ): OnboardingInfoRepository + fun bindsUserRepository( + impl: UserRepositoryImpl + ): UserRepository /*공유 코스*/ @Binds @@ -196,4 +177,16 @@ interface RepositoryModule { fun bindLoginRepository( impl: AuthRepositoryImpl ) : AuthRepository + + @Binds + @Singleton + fun bindImageRepository( + impl: ImageRepositoryImpl + ) : ImageRepository + + @Binds + @Singleton + abstract fun bindLocalStorageRepository( + impl: LocalStorageRepositoryImpl + ): LocalStorageRepository } \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/data/di/ServiceModule.kt b/app/src/main/java/com/paw/key/data/di/ServiceModule.kt index 8d5f9f23..f65ecd71 100644 --- a/app/src/main/java/com/paw/key/data/di/ServiceModule.kt +++ b/app/src/main/java/com/paw/key/data/di/ServiceModule.kt @@ -1,20 +1,18 @@ package com.paw.key.data.di import com.paw.key.data.service.ArchivedListService -import com.paw.key.data.service.DummyService import com.paw.key.data.service.LikeService import com.paw.key.data.service.PetProfileService -import com.paw.key.data.service.RegionService +import com.paw.key.data.service.region.RegionService import com.paw.key.data.service.SavedListService import com.paw.key.data.service.UserProfileService import com.paw.key.data.service.filter.FilterOptionService import com.paw.key.data.service.home.HomeRegionService +import com.paw.key.data.service.image.ImageService import com.paw.key.data.service.list.PostsListService import com.paw.key.data.service.login.LoginService -import com.paw.key.data.service.onboarding.OnboardingInfoService -import com.paw.key.data.service.onboarding.OnboardingPetsService -import com.paw.key.data.service.onboarding.OnboardingRegionService import com.paw.key.data.service.sharedwalk.SharedWalkService +import com.paw.key.data.service.user.UserService import com.paw.key.data.service.walkcourse.WalkCourseService import com.paw.key.data.service.walklist.WalkListDetailService import com.paw.key.data.service.walkreview.WalkReviewService @@ -30,11 +28,6 @@ import javax.inject.Singleton @InstallIn(SingletonComponent::class) object ServiceModule { - @Provides - @Singleton - fun providesDummyService(retrofit: Retrofit ): DummyService = - retrofit.create() - @Provides @Singleton fun providesRegionService(retrofit: Retrofit ): RegionService = @@ -47,18 +40,8 @@ object ServiceModule { @Provides @Singleton - fun provideOnboardingPetsService(retrofit: Retrofit): OnboardingPetsService = - retrofit.create(OnboardingPetsService::class.java) - - @Provides - @Singleton - fun provideOnboardingRegionService(retrofit: Retrofit): OnboardingRegionService = - retrofit.create(OnboardingRegionService::class.java) - - @Provides - @Singleton - fun provideOnboardingInfoService(retrofit: Retrofit): OnboardingInfoService = - retrofit.create(OnboardingInfoService::class.java) + fun provideUserInfoService(retrofit: Retrofit): UserService = + retrofit.create() @Provides @Singleton @@ -121,4 +104,9 @@ object ServiceModule { @Singleton fun provideLoginService(retrofit: Retrofit): LoginService = retrofit.create() + + @Provides + @Singleton + fun provideImageService(retrofit: Retrofit): ImageService = + retrofit.create() } \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/data/dto/request/walkcourse/WalkCourseRequestDto.kt b/app/src/main/java/com/paw/key/data/dto/request/walkcourse/WalkCourseRequestDto.kt index 269d90c2..ae9992e8 100644 --- a/app/src/main/java/com/paw/key/data/dto/request/walkcourse/WalkCourseRequestDto.kt +++ b/app/src/main/java/com/paw/key/data/dto/request/walkcourse/WalkCourseRequestDto.kt @@ -1,7 +1,7 @@ package com.paw.key.data.dto.request.walkcourse -import com.paw.key.domain.model.entity.walkcourse.CoordinateEntity -import com.paw.key.domain.model.entity.walkcourse.WalkCourseEntity +import com.paw.key.domain.entity.walkcourse.CoordinateEntity +import com.paw.key.domain.entity.walkcourse.WalkCourseEntity import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable diff --git a/app/src/main/java/com/paw/key/data/dto/response/ArchivedListResponseDto.kt b/app/src/main/java/com/paw/key/data/dto/response/ArchivedListResponseDto.kt index 5e57f361..7314c75f 100644 --- a/app/src/main/java/com/paw/key/data/dto/response/ArchivedListResponseDto.kt +++ b/app/src/main/java/com/paw/key/data/dto/response/ArchivedListResponseDto.kt @@ -1,8 +1,8 @@ package com.paw.key.data.dto.response -import com.paw.key.domain.model.entity.archivedlist.ArchivedListEntity -import com.paw.key.domain.model.entity.archivedlist.ArchivedListPostsEntity -import com.paw.key.domain.model.entity.archivedlist.WriterEntity +import com.paw.key.domain.entity.archivedlist.ArchivedListEntity +import com.paw.key.domain.entity.archivedlist.ArchivedListPostsEntity +import com.paw.key.domain.entity.archivedlist.WriterEntity import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable diff --git a/app/src/main/java/com/paw/key/data/dto/response/LoginResponseDto.kt b/app/src/main/java/com/paw/key/data/dto/response/LoginResponseDto.kt index 89213588..fb13a9ec 100644 --- a/app/src/main/java/com/paw/key/data/dto/response/LoginResponseDto.kt +++ b/app/src/main/java/com/paw/key/data/dto/response/LoginResponseDto.kt @@ -8,5 +8,7 @@ data class LoginResponseDto ( @SerialName("accessToken") val accessToken: String, @SerialName("refreshToken") - val refreshToken: String + val refreshToken: String, + @SerialName("isNewUser") + val isNewUser: Boolean ) diff --git a/app/src/main/java/com/paw/key/data/dto/response/SavedListResponseDto.kt b/app/src/main/java/com/paw/key/data/dto/response/SavedListResponseDto.kt index 0f251a0d..726777cb 100644 --- a/app/src/main/java/com/paw/key/data/dto/response/SavedListResponseDto.kt +++ b/app/src/main/java/com/paw/key/data/dto/response/SavedListResponseDto.kt @@ -1,11 +1,8 @@ package com.paw.key.data.dto.response -import com.paw.key.domain.model.entity.archivedlist.ArchivedListEntity -import com.paw.key.domain.model.entity.archivedlist.ArchivedListPostsEntity -import com.paw.key.domain.model.entity.archivedlist.WriterEntity -import com.paw.key.domain.model.entity.savedlist.SavedListEntity -import com.paw.key.domain.model.entity.savedlist.SavedListPostEntity -import com.paw.key.domain.model.entity.savedlist.SavedWriterEntity +import com.paw.key.domain.entity.savedlist.SavedListEntity +import com.paw.key.domain.entity.savedlist.SavedListPostEntity +import com.paw.key.domain.entity.savedlist.SavedWriterEntity import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable diff --git a/app/src/main/java/com/paw/key/data/dto/response/filter/FilterOptionResponse.kt b/app/src/main/java/com/paw/key/data/dto/response/filter/FilterOptionResponse.kt index 56c2240b..8a90f83e 100644 --- a/app/src/main/java/com/paw/key/data/dto/response/filter/FilterOptionResponse.kt +++ b/app/src/main/java/com/paw/key/data/dto/response/filter/FilterOptionResponse.kt @@ -1,11 +1,10 @@ package com.paw.key.data.dto.response.filter -import com.paw.key.domain.model.entity.filter.Category -import com.paw.key.domain.model.entity.filter.CategoryOption -import com.paw.key.domain.model.entity.filter.FilterEntity -import com.paw.key.domain.model.entity.filter.SelectOption -import com.paw.key.domain.model.entity.filter.SelectOptionItem -import com.paw.key.domain.model.entity.walklist.CategoryTagsEntity +import com.paw.key.domain.entity.filter.Category +import com.paw.key.domain.entity.filter.CategoryOption +import com.paw.key.domain.entity.filter.FilterEntity +import com.paw.key.domain.entity.filter.SelectOption +import com.paw.key.domain.entity.filter.SelectOptionItem import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable diff --git a/app/src/main/java/com/paw/key/data/dto/response/home/RegionCurrentResponseDto.kt b/app/src/main/java/com/paw/key/data/dto/response/home/RegionCurrentResponseDto.kt index 5583b420..1f406e38 100644 --- a/app/src/main/java/com/paw/key/data/dto/response/home/RegionCurrentResponseDto.kt +++ b/app/src/main/java/com/paw/key/data/dto/response/home/RegionCurrentResponseDto.kt @@ -1,6 +1,6 @@ package com.paw.key.data.dto.response.home -import com.paw.key.domain.model.entity.home.RegionCurrentDataEntity +import com.paw.key.domain.entity.home.RegionCurrentDataEntity import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable diff --git a/app/src/main/java/com/paw/key/data/dto/response/list/PostsListResponseDto.kt b/app/src/main/java/com/paw/key/data/dto/response/list/PostsListResponseDto.kt index a6b8985a..e062d822 100644 --- a/app/src/main/java/com/paw/key/data/dto/response/list/PostsListResponseDto.kt +++ b/app/src/main/java/com/paw/key/data/dto/response/list/PostsListResponseDto.kt @@ -1,8 +1,8 @@ package com.paw.key.data.dto.response.list -import com.paw.key.domain.model.entity.list.ListEntity -import com.paw.key.domain.model.entity.list.PostEntity -import com.paw.key.domain.model.entity.list.WriterEntity +import com.paw.key.domain.entity.list.ListEntity +import com.paw.key.domain.entity.list.PostEntity +import com.paw.key.domain.entity.list.WriterEntity import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable diff --git a/app/src/main/java/com/paw/key/data/dto/response/petprofile/PetProfileResponseDto.kt b/app/src/main/java/com/paw/key/data/dto/response/petprofile/PetProfileResponseDto.kt index bcb74c27..791c21af 100644 --- a/app/src/main/java/com/paw/key/data/dto/response/petprofile/PetProfileResponseDto.kt +++ b/app/src/main/java/com/paw/key/data/dto/response/petprofile/PetProfileResponseDto.kt @@ -1,8 +1,8 @@ package com.paw.key.data.dto.response.petprofile import androidx.core.net.toUri -import com.paw.key.domain.model.entity.petprofile.PetProfileEntity -import com.paw.key.domain.model.entity.petprofile.TraitEntity +import com.paw.key.domain.entity.petprofile.PetProfileEntity +import com.paw.key.domain.entity.petprofile.TraitEntity import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable diff --git a/app/src/main/java/com/paw/key/data/dto/response/sharedwalk/SharedWalkResponseDto.kt b/app/src/main/java/com/paw/key/data/dto/response/sharedwalk/SharedWalkResponseDto.kt index fe703f1a..5d5c0489 100644 --- a/app/src/main/java/com/paw/key/data/dto/response/sharedwalk/SharedWalkResponseDto.kt +++ b/app/src/main/java/com/paw/key/data/dto/response/sharedwalk/SharedWalkResponseDto.kt @@ -1,7 +1,7 @@ package com.paw.key.data.dto.response.sharedwalk -import com.paw.key.domain.model.entity.sharedwalk.GeometryEntity -import com.paw.key.domain.model.entity.sharedwalk.SharedWalkEntity +import com.paw.key.domain.entity.sharedwalk.GeometryEntity +import com.paw.key.domain.entity.sharedwalk.SharedWalkEntity import kotlinx.serialization.Serializable @Serializable diff --git a/app/src/main/java/com/paw/key/data/dto/response/walkcourse/WalkCourseResponseDto.kt b/app/src/main/java/com/paw/key/data/dto/response/walkcourse/WalkCourseResponseDto.kt index 9e22659f..10cd790d 100644 --- a/app/src/main/java/com/paw/key/data/dto/response/walkcourse/WalkCourseResponseDto.kt +++ b/app/src/main/java/com/paw/key/data/dto/response/walkcourse/WalkCourseResponseDto.kt @@ -1,6 +1,6 @@ package com.paw.key.data.dto.response.walkcourse -import com.paw.key.domain.model.entity.walkcourse.WalkCourseRegionIdEntity +import com.paw.key.domain.entity.walkcourse.WalkCourseRegionIdEntity import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable diff --git a/app/src/main/java/com/paw/key/data/dto/response/walklist/WalkReviewDetailResponseDto.kt b/app/src/main/java/com/paw/key/data/dto/response/walklist/WalkReviewDetailResponseDto.kt index eded82e7..552db9ca 100644 --- a/app/src/main/java/com/paw/key/data/dto/response/walklist/WalkReviewDetailResponseDto.kt +++ b/app/src/main/java/com/paw/key/data/dto/response/walklist/WalkReviewDetailResponseDto.kt @@ -1,8 +1,8 @@ package com.paw.key.data.dto.response.walklist -import com.paw.key.domain.model.entity.walklist.AuthorInfoEntity -import com.paw.key.domain.model.entity.walklist.CategoryTagsEntity -import com.paw.key.domain.model.entity.walklist.WalkListDetailEntity +import com.paw.key.domain.entity.walklist.AuthorInfoEntity +import com.paw.key.domain.entity.walklist.CategoryTagsEntity +import com.paw.key.domain.entity.walklist.WalkListDetailEntity import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable diff --git a/app/src/main/java/com/paw/key/data/dto/response/walklist/WalkReviewSummaryResponseDto.kt b/app/src/main/java/com/paw/key/data/dto/response/walklist/WalkReviewSummaryResponseDto.kt index 076a120f..77a9fe13 100644 --- a/app/src/main/java/com/paw/key/data/dto/response/walklist/WalkReviewSummaryResponseDto.kt +++ b/app/src/main/java/com/paw/key/data/dto/response/walklist/WalkReviewSummaryResponseDto.kt @@ -1,7 +1,7 @@ package com.paw.key.data.dto.response.walklist -import com.paw.key.domain.model.entity.walklist.CategoryTop3Entity -import com.paw.key.domain.model.entity.walklist.WalkReviewSummaryEntity +import com.paw.key.domain.entity.walklist.CategoryTop3Entity +import com.paw.key.domain.entity.walklist.WalkReviewSummaryEntity import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable diff --git a/app/src/main/java/com/paw/key/data/dto/response/walkreview/WalkReviewCategoryResponseDto.kt b/app/src/main/java/com/paw/key/data/dto/response/walkreview/WalkReviewCategoryResponseDto.kt index da65415b..2deec624 100644 --- a/app/src/main/java/com/paw/key/data/dto/response/walkreview/WalkReviewCategoryResponseDto.kt +++ b/app/src/main/java/com/paw/key/data/dto/response/walkreview/WalkReviewCategoryResponseDto.kt @@ -1,8 +1,8 @@ package com.paw.key.data.dto.response.walkreview -import com.paw.key.domain.model.entity.walkreview.WalkReviewCategoryEntity -import com.paw.key.domain.model.entity.walkreview.WalkReviewCategoryListEntity -import com.paw.key.domain.model.entity.walkreview.WalkReviewOptionOptionsResponseEntity +import com.paw.key.domain.entity.walkreview.WalkReviewCategoryEntity +import com.paw.key.domain.entity.walkreview.WalkReviewCategoryListEntity +import com.paw.key.domain.entity.walkreview.WalkReviewOptionOptionsResponseEntity import kotlinx.serialization.Serializable @Serializable diff --git a/app/src/main/java/com/paw/key/data/dto/response/walkreview/WalkReviewInfoResponseDto.kt b/app/src/main/java/com/paw/key/data/dto/response/walkreview/WalkReviewInfoResponseDto.kt index 28ebe603..1af4e895 100644 --- a/app/src/main/java/com/paw/key/data/dto/response/walkreview/WalkReviewInfoResponseDto.kt +++ b/app/src/main/java/com/paw/key/data/dto/response/walkreview/WalkReviewInfoResponseDto.kt @@ -1,7 +1,7 @@ package com.paw.key.data.dto.response.walkreview -import com.paw.key.domain.model.entity.walkreview.WalkReviewInfoEntity -import com.paw.key.domain.model.entity.walkreview.WalkReviewRouteInfoEntity +import com.paw.key.domain.entity.walkreview.WalkReviewInfoEntity +import com.paw.key.domain.entity.walkreview.WalkReviewRouteInfoEntity import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable diff --git a/app/src/main/java/com/paw/key/data/dto/response/walkreview/WalkReviewResponseDto.kt b/app/src/main/java/com/paw/key/data/dto/response/walkreview/WalkReviewResponseDto.kt index e5a8ba32..00db2f2a 100644 --- a/app/src/main/java/com/paw/key/data/dto/response/walkreview/WalkReviewResponseDto.kt +++ b/app/src/main/java/com/paw/key/data/dto/response/walkreview/WalkReviewResponseDto.kt @@ -1,6 +1,6 @@ package com.paw.key.data.dto.response.walkreview -import com.paw.key.domain.model.entity.walkreview.WalkReviewIdEntity +import com.paw.key.domain.entity.walkreview.WalkReviewIdEntity import kotlinx.serialization.Serializable @Serializable diff --git a/app/src/main/java/com/paw/key/data/mapper/RegionMapper.kt b/app/src/main/java/com/paw/key/data/mapper/RegionMapper.kt index e383bd5f..7ed05404 100644 --- a/app/src/main/java/com/paw/key/data/mapper/RegionMapper.kt +++ b/app/src/main/java/com/paw/key/data/mapper/RegionMapper.kt @@ -2,8 +2,8 @@ package com.paw.key.data.mapper import com.paw.key.data.dto.response.region.GeometryDto import com.paw.key.data.dto.response.region.RegionResponseDto -import com.paw.key.domain.model.entity.region.GeometryEntity -import com.paw.key.domain.model.entity.region.RegionDataEntity +import com.paw.key.domain.entity.region.GeometryEntity +import com.paw.key.domain.entity.region.RegionDataEntity import javax.inject.Inject class RegionMapper @Inject constructor() { diff --git a/app/src/main/java/com/paw/key/data/remote/datasource/PetProfileDataSource.kt b/app/src/main/java/com/paw/key/data/remote/datasource/PetProfileDataSource.kt index c12bdbe6..8f16027f 100644 --- a/app/src/main/java/com/paw/key/data/remote/datasource/PetProfileDataSource.kt +++ b/app/src/main/java/com/paw/key/data/remote/datasource/PetProfileDataSource.kt @@ -1,10 +1,6 @@ package com.paw.key.data.remote.datasource -import com.paw.key.data.dto.response.onboarding.OnboardingInfoResponse import com.paw.key.data.service.PetProfileService -import com.paw.key.data.service.onboarding.OnboardingInfoService -import okhttp3.MultipartBody -import okhttp3.RequestBody import javax.inject.Inject class PetProfileDataSource @Inject constructor( diff --git a/app/src/main/java/com/paw/key/data/remote/datasource/RegionDataSource.kt b/app/src/main/java/com/paw/key/data/remote/datasource/RegionDataSource.kt index d3be31e8..1885f479 100644 --- a/app/src/main/java/com/paw/key/data/remote/datasource/RegionDataSource.kt +++ b/app/src/main/java/com/paw/key/data/remote/datasource/RegionDataSource.kt @@ -1,6 +1,6 @@ package com.paw.key.data.remote.datasource -import com.paw.key.data.service.RegionService +import com.paw.key.data.service.region.RegionService import javax.inject.Inject class RegionDataSource @Inject constructor ( diff --git a/app/src/main/java/com/paw/key/data/remote/datasource/UserProfileDataSource.kt b/app/src/main/java/com/paw/key/data/remote/datasource/UserProfileDataSource.kt index 7bbec381..23b60071 100644 --- a/app/src/main/java/com/paw/key/data/remote/datasource/UserProfileDataSource.kt +++ b/app/src/main/java/com/paw/key/data/remote/datasource/UserProfileDataSource.kt @@ -1,11 +1,6 @@ package com.paw.key.data.remote.datasource -import com.paw.key.data.dto.response.onboarding.OnboardingInfoResponse -import com.paw.key.data.service.PetProfileService import com.paw.key.data.service.UserProfileService -import com.paw.key.data.service.onboarding.OnboardingInfoService -import okhttp3.MultipartBody -import okhttp3.RequestBody import javax.inject.Inject class UserProfileDataSource @Inject constructor( diff --git a/app/src/main/java/com/paw/key/data/remote/datasource/home/HomeRegionDataSource.kt b/app/src/main/java/com/paw/key/data/remote/datasource/home/HomeRegionDataSource.kt index 5273263c..4af6c97e 100644 --- a/app/src/main/java/com/paw/key/data/remote/datasource/home/HomeRegionDataSource.kt +++ b/app/src/main/java/com/paw/key/data/remote/datasource/home/HomeRegionDataSource.kt @@ -2,7 +2,6 @@ package com.paw.key.data.remote.datasource.home import com.paw.key.data.dto.request.home.HomeRegionRequest import com.paw.key.data.service.home.HomeRegionService -import com.paw.key.data.service.onboarding.OnboardingPetsService import javax.inject.Inject class HomeRegionDataSource @Inject constructor( diff --git a/app/src/main/java/com/paw/key/data/remote/datasource/sharedwalk/SharedWalkDataSource.kt b/app/src/main/java/com/paw/key/data/remote/datasource/sharedwalk/SharedWalkDataSource.kt index 6c2aebf2..95197b36 100644 --- a/app/src/main/java/com/paw/key/data/remote/datasource/sharedwalk/SharedWalkDataSource.kt +++ b/app/src/main/java/com/paw/key/data/remote/datasource/sharedwalk/SharedWalkDataSource.kt @@ -2,7 +2,6 @@ package com.paw.key.data.remote.datasource.sharedwalk import com.paw.key.data.dto.request.sharedwalk.SharedWalkReviewRequestDto import com.paw.key.data.service.sharedwalk.SharedWalkService -import com.paw.key.domain.model.entity.sharedwalk.SharedWalkReviewEntity import javax.inject.Inject class SharedWalkDataSource @Inject constructor( diff --git a/app/src/main/java/com/paw/key/data/repositoryimpl/ArchivedListRepositoryImpl.kt b/app/src/main/java/com/paw/key/data/repositoryimpl/ArchivedListRepositoryImpl.kt index 6559c866..b3793372 100644 --- a/app/src/main/java/com/paw/key/data/repositoryimpl/ArchivedListRepositoryImpl.kt +++ b/app/src/main/java/com/paw/key/data/repositoryimpl/ArchivedListRepositoryImpl.kt @@ -1,8 +1,7 @@ package com.paw.key.data.repositoryimpl import com.paw.key.data.remote.datasource.ArchivedListDataSource -import com.paw.key.domain.model.entity.archivedlist.ArchivedListEntity -import com.paw.key.domain.model.entity.archivedlist.ArchivedListPostsEntity +import com.paw.key.domain.entity.archivedlist.ArchivedListPostsEntity import com.paw.key.domain.repository.ArchivedListRepository import javax.inject.Inject diff --git a/app/src/main/java/com/paw/key/data/repositoryimpl/PetProfileRepositoryImpl.kt b/app/src/main/java/com/paw/key/data/repositoryimpl/PetProfileRepositoryImpl.kt index 9ecafac2..6f4b0254 100644 --- a/app/src/main/java/com/paw/key/data/repositoryimpl/PetProfileRepositoryImpl.kt +++ b/app/src/main/java/com/paw/key/data/repositoryimpl/PetProfileRepositoryImpl.kt @@ -1,7 +1,7 @@ package com.paw.key.data.repositoryimpl import com.paw.key.data.remote.datasource.PetProfileDataSource -import com.paw.key.domain.model.entity.petprofile.PetProfileEntity +import com.paw.key.domain.entity.petprofile.PetProfileEntity import com.paw.key.domain.repository.petprofile.PetProfileRepository import javax.inject.Inject diff --git a/app/src/main/java/com/paw/key/data/repositoryimpl/RegionRepositoryImpl.kt b/app/src/main/java/com/paw/key/data/repositoryimpl/RegionRepositoryImpl.kt index 8c435d2b..337b235c 100644 --- a/app/src/main/java/com/paw/key/data/repositoryimpl/RegionRepositoryImpl.kt +++ b/app/src/main/java/com/paw/key/data/repositoryimpl/RegionRepositoryImpl.kt @@ -1,10 +1,10 @@ package com.paw.key.data.repositoryimpl +import com.paw.key.data.dto.response.region.toEntity import com.paw.key.data.mapper.RegionMapper import com.paw.key.data.remote.datasource.RegionDataSource -import com.paw.key.domain.model.entity.region.RegionDataEntity -import com.paw.key.domain.model.entity.signup.DistrictEntity -import com.paw.key.domain.model.entity.signup.toEntity +import com.paw.key.domain.entity.region.RegionDataEntity +import com.paw.key.domain.entity.signup.DistrictEntity import com.paw.key.domain.repository.RegionRepository import javax.inject.Inject diff --git a/app/src/main/java/com/paw/key/data/repositoryimpl/SavedListRepositoryImpl.kt b/app/src/main/java/com/paw/key/data/repositoryimpl/SavedListRepositoryImpl.kt index d35c979b..47447d45 100644 --- a/app/src/main/java/com/paw/key/data/repositoryimpl/SavedListRepositoryImpl.kt +++ b/app/src/main/java/com/paw/key/data/repositoryimpl/SavedListRepositoryImpl.kt @@ -1,8 +1,7 @@ package com.paw.key.data.repositoryimpl import com.paw.key.data.remote.datasource.SavedListDataSource -import com.paw.key.domain.model.entity.savedlist.SavedListEntity -import com.paw.key.domain.model.entity.savedlist.SavedListPostEntity +import com.paw.key.domain.entity.savedlist.SavedListPostEntity import com.paw.key.domain.repository.SavedListRepository import javax.inject.Inject diff --git a/app/src/main/java/com/paw/key/data/repositoryimpl/WalkCourseRepositoryImpl.kt b/app/src/main/java/com/paw/key/data/repositoryimpl/WalkCourseRepositoryImpl.kt index 8e3b1f65..19c47cab 100644 --- a/app/src/main/java/com/paw/key/data/repositoryimpl/WalkCourseRepositoryImpl.kt +++ b/app/src/main/java/com/paw/key/data/repositoryimpl/WalkCourseRepositoryImpl.kt @@ -2,7 +2,7 @@ package com.paw.key.data.repositoryimpl import com.paw.key.data.dto.request.walkcourse.WalkCourseRequestDto import com.paw.key.data.remote.datasource.WalkCourseDataSource -import com.paw.key.domain.model.entity.walkcourse.WalkCourseRegionIdEntity +import com.paw.key.domain.entity.walkcourse.WalkCourseRegionIdEntity import com.paw.key.domain.repository.walkcourse.WalkCourseRepository import okhttp3.MultipartBody import javax.inject.Inject diff --git a/app/src/main/java/com/paw/key/data/repositoryimpl/WalkSharedResultRepositoryImpl.kt b/app/src/main/java/com/paw/key/data/repositoryimpl/WalkSharedResultRepositoryImpl.kt index a5ca284a..8ce487c5 100644 --- a/app/src/main/java/com/paw/key/data/repositoryimpl/WalkSharedResultRepositoryImpl.kt +++ b/app/src/main/java/com/paw/key/data/repositoryimpl/WalkSharedResultRepositoryImpl.kt @@ -1,9 +1,8 @@ package com.paw.key.data.repositoryimpl import android.graphics.Bitmap -import android.util.Log import com.naver.maps.geometry.LatLng -import com.paw.key.domain.model.entity.sharedresult.WalkResult +import com.paw.key.domain.entity.sharedresult.WalkResult import com.paw.key.domain.repository.WalkSharedResultRepository import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -24,10 +23,6 @@ class WalkSharedResultRepositoryImpl @Inject constructor( points: List ) { _walkResult.value = WalkResult(bitmap, totalTime, distance, steps, points) - Log.d("WalkSharedResultRepositoryImpl", "Result saved: $bitmap") - Log.d("WalkSharedResultRepositoryImpl", "Result saved: $totalTime") - Log.d("WalkSharedResultRepositoryImpl", "Result saved: $distance") - Log.d("WalkSharedResultRepositoryImpl", "Result saved: $steps") } override fun getResult(): Flow = _walkResult.asStateFlow() diff --git a/app/src/main/java/com/paw/key/data/repositoryimpl/filter/FilterOptionRepositoryImpl.kt b/app/src/main/java/com/paw/key/data/repositoryimpl/filter/FilterOptionRepositoryImpl.kt index 5b825ac2..5b4ef0ed 100644 --- a/app/src/main/java/com/paw/key/data/repositoryimpl/filter/FilterOptionRepositoryImpl.kt +++ b/app/src/main/java/com/paw/key/data/repositoryimpl/filter/FilterOptionRepositoryImpl.kt @@ -1,9 +1,7 @@ package com.paw.key.data.repositoryimpl.filter import com.paw.key.data.remote.datasource.filter.FilterOptionDataSource -import com.paw.key.domain.model.entity.filter.Category -import com.paw.key.domain.model.entity.filter.CategoryOption -import com.paw.key.domain.model.entity.filter.FilterEntity +import com.paw.key.domain.entity.filter.FilterEntity import com.paw.key.domain.repository.filter.FilterOptionRepository import javax.inject.Inject diff --git a/app/src/main/java/com/paw/key/data/repositoryimpl/home/HomeRegionRepositoryImpl.kt b/app/src/main/java/com/paw/key/data/repositoryimpl/home/HomeRegionRepositoryImpl.kt index 46cd85e3..b429c859 100644 --- a/app/src/main/java/com/paw/key/data/repositoryimpl/home/HomeRegionRepositoryImpl.kt +++ b/app/src/main/java/com/paw/key/data/repositoryimpl/home/HomeRegionRepositoryImpl.kt @@ -1,7 +1,7 @@ package com.paw.key.data.repositoryimpl.home import com.paw.key.data.remote.datasource.home.HomeRegionDataSource -import com.paw.key.domain.model.entity.home.HomeRegionDataEntity +import com.paw.key.domain.entity.home.HomeRegionDataEntity import com.paw.key.domain.repository.home.HomeRegionRepository import javax.inject.Inject diff --git a/app/src/main/java/com/paw/key/data/repositoryimpl/home/RegionCurrentRepositoryImpl.kt b/app/src/main/java/com/paw/key/data/repositoryimpl/home/RegionCurrentRepositoryImpl.kt index 451e61a6..b662bebd 100644 --- a/app/src/main/java/com/paw/key/data/repositoryimpl/home/RegionCurrentRepositoryImpl.kt +++ b/app/src/main/java/com/paw/key/data/repositoryimpl/home/RegionCurrentRepositoryImpl.kt @@ -1,7 +1,7 @@ package com.paw.key.data.repositoryimpl.home import com.paw.key.data.remote.datasource.home.RegionCurrentDataSource -import com.paw.key.domain.model.entity.home.RegionCurrentDataEntity +import com.paw.key.domain.entity.home.RegionCurrentDataEntity import com.paw.key.domain.repository.home.RegionCurrentRepository import javax.inject.Inject diff --git a/app/src/main/java/com/paw/key/data/repositoryimpl/list/PostsListRepositoryImpl.kt b/app/src/main/java/com/paw/key/data/repositoryimpl/list/PostsListRepositoryImpl.kt index 32401584..e4cc77c7 100644 --- a/app/src/main/java/com/paw/key/data/repositoryimpl/list/PostsListRepositoryImpl.kt +++ b/app/src/main/java/com/paw/key/data/repositoryimpl/list/PostsListRepositoryImpl.kt @@ -3,7 +3,7 @@ package com.paw.key.data.repositoryimpl.list import com.paw.key.data.dto.request.list.PostsListRequestDto import com.paw.key.data.dto.response.list.toEntity import com.paw.key.data.remote.datasource.list.PostsListDataSource -import com.paw.key.domain.model.entity.list.ListEntity +import com.paw.key.domain.entity.list.ListEntity import com.paw.key.domain.repository.list.PostsListRepository import javax.inject.Inject diff --git a/app/src/main/java/com/paw/key/data/repositoryimpl/sharedwalk/SharedWalkRepositoryImpl.kt b/app/src/main/java/com/paw/key/data/repositoryimpl/sharedwalk/SharedWalkRepositoryImpl.kt index 42b332cf..ef7e5d69 100644 --- a/app/src/main/java/com/paw/key/data/repositoryimpl/sharedwalk/SharedWalkRepositoryImpl.kt +++ b/app/src/main/java/com/paw/key/data/repositoryimpl/sharedwalk/SharedWalkRepositoryImpl.kt @@ -1,9 +1,8 @@ package com.paw.key.data.repositoryimpl.sharedwalk import com.paw.key.data.remote.datasource.sharedwalk.SharedWalkDataSource -import com.paw.key.domain.model.entity.region.RegionDataEntity -import com.paw.key.domain.model.entity.sharedwalk.SharedWalkEntity -import com.paw.key.domain.model.entity.sharedwalk.SharedWalkReviewEntity +import com.paw.key.domain.entity.sharedwalk.SharedWalkEntity +import com.paw.key.domain.entity.sharedwalk.SharedWalkReviewEntity import com.paw.key.domain.repository.sharedwalk.SharedWalkRepository import javax.inject.Inject diff --git a/app/src/main/java/com/paw/key/data/repositoryimpl/walklist/WalkListDetailRepositoryImpl.kt b/app/src/main/java/com/paw/key/data/repositoryimpl/walklist/WalkListDetailRepositoryImpl.kt index eee8151a..c326b663 100644 --- a/app/src/main/java/com/paw/key/data/repositoryimpl/walklist/WalkListDetailRepositoryImpl.kt +++ b/app/src/main/java/com/paw/key/data/repositoryimpl/walklist/WalkListDetailRepositoryImpl.kt @@ -1,9 +1,8 @@ package com.paw.key.data.repositoryimpl.walklist import com.paw.key.data.remote.datasource.walklist.WalkListDetailDataSource -import com.paw.key.data.service.walklist.WalkListDetailService -import com.paw.key.domain.model.entity.walklist.WalkListDetailEntity -import com.paw.key.domain.model.entity.walklist.WalkReviewSummaryEntity +import com.paw.key.domain.entity.walklist.WalkListDetailEntity +import com.paw.key.domain.entity.walklist.WalkReviewSummaryEntity import com.paw.key.domain.repository.walklist.WalkListRepository import javax.inject.Inject diff --git a/app/src/main/java/com/paw/key/data/repositoryimpl/walkreview/WalkReviewRepositoryImpl.kt b/app/src/main/java/com/paw/key/data/repositoryimpl/walkreview/WalkReviewRepositoryImpl.kt index 746647b6..e353cdb8 100644 --- a/app/src/main/java/com/paw/key/data/repositoryimpl/walkreview/WalkReviewRepositoryImpl.kt +++ b/app/src/main/java/com/paw/key/data/repositoryimpl/walkreview/WalkReviewRepositoryImpl.kt @@ -1,10 +1,10 @@ package com.paw.key.data.repositoryimpl.walkreview import com.paw.key.data.remote.datasource.walkreview.WalkReviewDataSource -import com.paw.key.domain.model.entity.walkreview.WalkReviewCategoryListEntity -import com.paw.key.domain.model.entity.walkreview.WalkReviewIdEntity -import com.paw.key.domain.model.entity.walkreview.WalkReviewInfoEntity -import com.paw.key.domain.model.entity.walkreview.WalkReviewRecordEntity +import com.paw.key.domain.entity.walkreview.WalkReviewCategoryListEntity +import com.paw.key.domain.entity.walkreview.WalkReviewIdEntity +import com.paw.key.domain.entity.walkreview.WalkReviewInfoEntity +import com.paw.key.domain.entity.walkreview.WalkReviewRecordEntity import com.paw.key.domain.repository.walkreview.WalkReviewRepository import okhttp3.MultipartBody import javax.inject.Inject diff --git a/app/src/main/java/com/paw/key/domain/model/entity/archivedlist/ArchivedListEntity.kt b/app/src/main/java/com/paw/key/domain/entity/archivedlist/ArchivedListEntity.kt similarity index 90% rename from app/src/main/java/com/paw/key/domain/model/entity/archivedlist/ArchivedListEntity.kt rename to app/src/main/java/com/paw/key/domain/entity/archivedlist/ArchivedListEntity.kt index 0991d661..76f6a22e 100644 --- a/app/src/main/java/com/paw/key/domain/model/entity/archivedlist/ArchivedListEntity.kt +++ b/app/src/main/java/com/paw/key/domain/entity/archivedlist/ArchivedListEntity.kt @@ -1,4 +1,4 @@ -package com.paw.key.domain.model.entity.archivedlist +package com.paw.key.domain.entity.archivedlist data class ArchivedListPostsEntity( val posts: List diff --git a/app/src/main/java/com/paw/key/domain/model/entity/filter/FilterEntity.kt b/app/src/main/java/com/paw/key/domain/entity/filter/FilterEntity.kt similarity index 93% rename from app/src/main/java/com/paw/key/domain/model/entity/filter/FilterEntity.kt rename to app/src/main/java/com/paw/key/domain/entity/filter/FilterEntity.kt index 39e1fdab..89cf115b 100644 --- a/app/src/main/java/com/paw/key/domain/model/entity/filter/FilterEntity.kt +++ b/app/src/main/java/com/paw/key/domain/entity/filter/FilterEntity.kt @@ -1,4 +1,4 @@ -package com.paw.key.domain.model.entity.filter +package com.paw.key.domain.entity.filter data class FilterEntity( val selectList: List? = null, diff --git a/app/src/main/java/com/paw/key/domain/model/entity/home/HomeEntity.kt b/app/src/main/java/com/paw/key/domain/entity/home/HomeEntity.kt similarity index 78% rename from app/src/main/java/com/paw/key/domain/model/entity/home/HomeEntity.kt rename to app/src/main/java/com/paw/key/domain/entity/home/HomeEntity.kt index 7dcf031c..42cfea31 100644 --- a/app/src/main/java/com/paw/key/domain/model/entity/home/HomeEntity.kt +++ b/app/src/main/java/com/paw/key/domain/entity/home/HomeEntity.kt @@ -1,4 +1,4 @@ -package com.paw.key.domain.model.entity.home +package com.paw.key.domain.entity.home data class HomeRegionDataEntity( val success: Boolean = true diff --git a/app/src/main/java/com/paw/key/domain/model/entity/list/ListEntity.kt b/app/src/main/java/com/paw/key/domain/entity/list/ListEntity.kt similarity index 91% rename from app/src/main/java/com/paw/key/domain/model/entity/list/ListEntity.kt rename to app/src/main/java/com/paw/key/domain/entity/list/ListEntity.kt index d3a6e1fc..3514c634 100644 --- a/app/src/main/java/com/paw/key/domain/model/entity/list/ListEntity.kt +++ b/app/src/main/java/com/paw/key/domain/entity/list/ListEntity.kt @@ -1,4 +1,4 @@ -package com.paw.key.domain.model.entity.list +package com.paw.key.domain.entity.list data class ListEntity( val posts: List diff --git a/app/src/main/java/com/paw/key/domain/model/entity/login/LoginModel.kt b/app/src/main/java/com/paw/key/domain/entity/login/LoginModel.kt similarity index 64% rename from app/src/main/java/com/paw/key/domain/model/entity/login/LoginModel.kt rename to app/src/main/java/com/paw/key/domain/entity/login/LoginModel.kt index 90f23c22..67a3f565 100644 --- a/app/src/main/java/com/paw/key/domain/model/entity/login/LoginModel.kt +++ b/app/src/main/java/com/paw/key/domain/entity/login/LoginModel.kt @@ -1,4 +1,4 @@ -package com.paw.key.domain.model.entity.login +package com.paw.key.domain.entity.login data class LoginModel ( val AccessToken: String, diff --git a/app/src/main/java/com/paw/key/domain/model/entity/petprofile/PetProfileEntity.kt b/app/src/main/java/com/paw/key/domain/entity/petprofile/PetProfileEntity.kt similarity index 88% rename from app/src/main/java/com/paw/key/domain/model/entity/petprofile/PetProfileEntity.kt rename to app/src/main/java/com/paw/key/domain/entity/petprofile/PetProfileEntity.kt index e151587d..01958ec7 100644 --- a/app/src/main/java/com/paw/key/domain/model/entity/petprofile/PetProfileEntity.kt +++ b/app/src/main/java/com/paw/key/domain/entity/petprofile/PetProfileEntity.kt @@ -1,4 +1,4 @@ -package com.paw.key.domain.model.entity.petprofile +package com.paw.key.domain.entity.petprofile import android.net.Uri diff --git a/app/src/main/java/com/paw/key/domain/model/entity/region/RegionEntity.kt b/app/src/main/java/com/paw/key/domain/entity/region/RegionEntity.kt similarity index 86% rename from app/src/main/java/com/paw/key/domain/model/entity/region/RegionEntity.kt rename to app/src/main/java/com/paw/key/domain/entity/region/RegionEntity.kt index ca4012a1..6d2e2331 100644 --- a/app/src/main/java/com/paw/key/domain/model/entity/region/RegionEntity.kt +++ b/app/src/main/java/com/paw/key/domain/entity/region/RegionEntity.kt @@ -1,4 +1,4 @@ -package com.paw.key.domain.model.entity.region +package com.paw.key.domain.entity.region data class RegionDataEntity( val regionName: String, diff --git a/app/src/main/java/com/paw/key/domain/model/entity/savedlist/SavedListEntity.kt b/app/src/main/java/com/paw/key/domain/entity/savedlist/SavedListEntity.kt similarity index 90% rename from app/src/main/java/com/paw/key/domain/model/entity/savedlist/SavedListEntity.kt rename to app/src/main/java/com/paw/key/domain/entity/savedlist/SavedListEntity.kt index 9c62d099..0bb6266a 100644 --- a/app/src/main/java/com/paw/key/domain/model/entity/savedlist/SavedListEntity.kt +++ b/app/src/main/java/com/paw/key/domain/entity/savedlist/SavedListEntity.kt @@ -1,4 +1,4 @@ -package com.paw.key.domain.model.entity.savedlist +package com.paw.key.domain.entity.savedlist data class SavedListPostEntity( diff --git a/app/src/main/java/com/paw/key/domain/model/entity/sharedresult/WalkResult.kt b/app/src/main/java/com/paw/key/domain/entity/sharedresult/WalkResult.kt similarity index 80% rename from app/src/main/java/com/paw/key/domain/model/entity/sharedresult/WalkResult.kt rename to app/src/main/java/com/paw/key/domain/entity/sharedresult/WalkResult.kt index 9db7a0b3..048970d6 100644 --- a/app/src/main/java/com/paw/key/domain/model/entity/sharedresult/WalkResult.kt +++ b/app/src/main/java/com/paw/key/domain/entity/sharedresult/WalkResult.kt @@ -1,4 +1,4 @@ -package com.paw.key.domain.model.entity.sharedresult +package com.paw.key.domain.entity.sharedresult import android.graphics.Bitmap import com.naver.maps.geometry.LatLng diff --git a/app/src/main/java/com/paw/key/domain/model/entity/sharedwalk/SharedWalkEntity.kt b/app/src/main/java/com/paw/key/domain/entity/sharedwalk/SharedWalkEntity.kt similarity index 77% rename from app/src/main/java/com/paw/key/domain/model/entity/sharedwalk/SharedWalkEntity.kt rename to app/src/main/java/com/paw/key/domain/entity/sharedwalk/SharedWalkEntity.kt index 461ee4e4..e5a0d79a 100644 --- a/app/src/main/java/com/paw/key/domain/model/entity/sharedwalk/SharedWalkEntity.kt +++ b/app/src/main/java/com/paw/key/domain/entity/sharedwalk/SharedWalkEntity.kt @@ -1,4 +1,4 @@ -package com.paw.key.domain.model.entity.sharedwalk +package com.paw.key.domain.entity.sharedwalk data class SharedWalkEntity( val routeId: Int, diff --git a/app/src/main/java/com/paw/key/domain/model/entity/sharedwalk/SharedWalkReviewEntity.kt b/app/src/main/java/com/paw/key/domain/entity/sharedwalk/SharedWalkReviewEntity.kt similarity index 94% rename from app/src/main/java/com/paw/key/domain/model/entity/sharedwalk/SharedWalkReviewEntity.kt rename to app/src/main/java/com/paw/key/domain/entity/sharedwalk/SharedWalkReviewEntity.kt index 484915d0..dd12353e 100644 --- a/app/src/main/java/com/paw/key/domain/model/entity/sharedwalk/SharedWalkReviewEntity.kt +++ b/app/src/main/java/com/paw/key/domain/entity/sharedwalk/SharedWalkReviewEntity.kt @@ -1,4 +1,4 @@ -package com.paw.key.domain.model.entity.sharedwalk +package com.paw.key.domain.entity.sharedwalk import com.paw.key.data.dto.request.sharedwalk.SharedWalkReviewCategoryDto import com.paw.key.data.dto.request.sharedwalk.SharedWalkReviewRequestDto diff --git a/app/src/main/java/com/paw/key/domain/model/entity/userprofile/UserProfileEntity.kt b/app/src/main/java/com/paw/key/domain/entity/userprofile/UserProfileEntity.kt similarity index 100% rename from app/src/main/java/com/paw/key/domain/model/entity/userprofile/UserProfileEntity.kt rename to app/src/main/java/com/paw/key/domain/entity/userprofile/UserProfileEntity.kt diff --git a/app/src/main/java/com/paw/key/domain/model/entity/walkcourse/WalkCourseEntity.kt b/app/src/main/java/com/paw/key/domain/entity/walkcourse/WalkCourseEntity.kt similarity index 94% rename from app/src/main/java/com/paw/key/domain/model/entity/walkcourse/WalkCourseEntity.kt rename to app/src/main/java/com/paw/key/domain/entity/walkcourse/WalkCourseEntity.kt index b96e201b..57b5d3c0 100644 --- a/app/src/main/java/com/paw/key/domain/model/entity/walkcourse/WalkCourseEntity.kt +++ b/app/src/main/java/com/paw/key/domain/entity/walkcourse/WalkCourseEntity.kt @@ -1,4 +1,4 @@ -package com.paw.key.domain.model.entity.walkcourse +package com.paw.key.domain.entity.walkcourse import com.paw.key.data.dto.request.walkcourse.CoordinateDto import com.paw.key.data.dto.request.walkcourse.WalkCourseRequestDto diff --git a/app/src/main/java/com/paw/key/domain/model/entity/walklist/WalkListDetailEntity.kt b/app/src/main/java/com/paw/key/domain/entity/walklist/WalkListDetailEntity.kt similarity index 92% rename from app/src/main/java/com/paw/key/domain/model/entity/walklist/WalkListDetailEntity.kt rename to app/src/main/java/com/paw/key/domain/entity/walklist/WalkListDetailEntity.kt index 0c5c9fba..679deac7 100644 --- a/app/src/main/java/com/paw/key/domain/model/entity/walklist/WalkListDetailEntity.kt +++ b/app/src/main/java/com/paw/key/domain/entity/walklist/WalkListDetailEntity.kt @@ -1,4 +1,4 @@ -package com.paw.key.domain.model.entity.walklist +package com.paw.key.domain.entity.walklist data class WalkListDetailEntity( val postId: Int, diff --git a/app/src/main/java/com/paw/key/domain/model/entity/walklist/WalkReviewSummaryEntity.kt b/app/src/main/java/com/paw/key/domain/entity/walklist/WalkReviewSummaryEntity.kt similarity index 87% rename from app/src/main/java/com/paw/key/domain/model/entity/walklist/WalkReviewSummaryEntity.kt rename to app/src/main/java/com/paw/key/domain/entity/walklist/WalkReviewSummaryEntity.kt index 76d8b4c7..9dcaff7e 100644 --- a/app/src/main/java/com/paw/key/domain/model/entity/walklist/WalkReviewSummaryEntity.kt +++ b/app/src/main/java/com/paw/key/domain/entity/walklist/WalkReviewSummaryEntity.kt @@ -1,4 +1,4 @@ -package com.paw.key.domain.model.entity.walklist +package com.paw.key.domain.entity.walklist data class WalkReviewSummaryEntity( val postId: Int, diff --git a/app/src/main/java/com/paw/key/domain/model/entity/walkreview/WalkReviewCategoryEntity.kt b/app/src/main/java/com/paw/key/domain/entity/walkreview/WalkReviewCategoryEntity.kt similarity index 88% rename from app/src/main/java/com/paw/key/domain/model/entity/walkreview/WalkReviewCategoryEntity.kt rename to app/src/main/java/com/paw/key/domain/entity/walkreview/WalkReviewCategoryEntity.kt index e3ede7bb..e560408d 100644 --- a/app/src/main/java/com/paw/key/domain/model/entity/walkreview/WalkReviewCategoryEntity.kt +++ b/app/src/main/java/com/paw/key/domain/entity/walkreview/WalkReviewCategoryEntity.kt @@ -1,4 +1,4 @@ -package com.paw.key.domain.model.entity.walkreview +package com.paw.key.domain.entity.walkreview data class WalkReviewCategoryListEntity( val categoryList : List diff --git a/app/src/main/java/com/paw/key/domain/model/entity/walkreview/WalkReviewEntity.kt b/app/src/main/java/com/paw/key/domain/entity/walkreview/WalkReviewEntity.kt similarity index 84% rename from app/src/main/java/com/paw/key/domain/model/entity/walkreview/WalkReviewEntity.kt rename to app/src/main/java/com/paw/key/domain/entity/walkreview/WalkReviewEntity.kt index ffa03203..539f70eb 100644 --- a/app/src/main/java/com/paw/key/domain/model/entity/walkreview/WalkReviewEntity.kt +++ b/app/src/main/java/com/paw/key/domain/entity/walkreview/WalkReviewEntity.kt @@ -1,4 +1,4 @@ -package com.paw.key.domain.model.entity.walkreview +package com.paw.key.domain.entity.walkreview data class WalkReviewInfoEntity( val routeDto : WalkReviewRouteInfoEntity, diff --git a/app/src/main/java/com/paw/key/domain/model/entity/walkreview/WalkReviewIdEntity.kt b/app/src/main/java/com/paw/key/domain/entity/walkreview/WalkReviewIdEntity.kt similarity index 60% rename from app/src/main/java/com/paw/key/domain/model/entity/walkreview/WalkReviewIdEntity.kt rename to app/src/main/java/com/paw/key/domain/entity/walkreview/WalkReviewIdEntity.kt index 0b7044aa..7bb79b92 100644 --- a/app/src/main/java/com/paw/key/domain/model/entity/walkreview/WalkReviewIdEntity.kt +++ b/app/src/main/java/com/paw/key/domain/entity/walkreview/WalkReviewIdEntity.kt @@ -1,4 +1,4 @@ -package com.paw.key.domain.model.entity.walkreview +package com.paw.key.domain.entity.walkreview data class WalkReviewIdEntity( val postId: Int, diff --git a/app/src/main/java/com/paw/key/domain/model/entity/walkreview/WalkReviewRecordEntity.kt b/app/src/main/java/com/paw/key/domain/entity/walkreview/WalkReviewRecordEntity.kt similarity index 95% rename from app/src/main/java/com/paw/key/domain/model/entity/walkreview/WalkReviewRecordEntity.kt rename to app/src/main/java/com/paw/key/domain/entity/walkreview/WalkReviewRecordEntity.kt index e98c9f42..6609ebf1 100644 --- a/app/src/main/java/com/paw/key/domain/model/entity/walkreview/WalkReviewRecordEntity.kt +++ b/app/src/main/java/com/paw/key/domain/entity/walkreview/WalkReviewRecordEntity.kt @@ -1,4 +1,4 @@ -package com.paw.key.domain.model.entity.walkreview +package com.paw.key.domain.entity.walkreview import com.paw.key.data.dto.request.walkreview.SelectedCategoryDto import com.paw.key.data.dto.request.walkreview.WalkCourseReviewRequestDto diff --git a/app/src/main/java/com/paw/key/domain/repository/ArchivedListRepository.kt b/app/src/main/java/com/paw/key/domain/repository/ArchivedListRepository.kt index 4ec9cbfc..f5f53e7e 100644 --- a/app/src/main/java/com/paw/key/domain/repository/ArchivedListRepository.kt +++ b/app/src/main/java/com/paw/key/domain/repository/ArchivedListRepository.kt @@ -1,7 +1,6 @@ package com.paw.key.domain.repository -import com.paw.key.domain.model.entity.archivedlist.ArchivedListEntity -import com.paw.key.domain.model.entity.archivedlist.ArchivedListPostsEntity +import com.paw.key.domain.entity.archivedlist.ArchivedListPostsEntity interface ArchivedListRepository { suspend fun getArchivedList(userId: Int): Result diff --git a/app/src/main/java/com/paw/key/domain/repository/RegionRepository.kt b/app/src/main/java/com/paw/key/domain/repository/RegionRepository.kt index b74830df..a72c8386 100644 --- a/app/src/main/java/com/paw/key/domain/repository/RegionRepository.kt +++ b/app/src/main/java/com/paw/key/domain/repository/RegionRepository.kt @@ -1,7 +1,7 @@ package com.paw.key.domain.repository -import com.paw.key.domain.model.entity.region.RegionDataEntity -import com.paw.key.domain.model.entity.signup.DistrictEntity +import com.paw.key.domain.entity.region.RegionDataEntity +import com.paw.key.domain.entity.signup.DistrictEntity interface RegionRepository { suspend fun getRegionGeometry(userId: Int, regionId: Int): Result diff --git a/app/src/main/java/com/paw/key/domain/repository/SavedListRepository.kt b/app/src/main/java/com/paw/key/domain/repository/SavedListRepository.kt index f59e0b95..7a04944a 100644 --- a/app/src/main/java/com/paw/key/domain/repository/SavedListRepository.kt +++ b/app/src/main/java/com/paw/key/domain/repository/SavedListRepository.kt @@ -1,7 +1,6 @@ package com.paw.key.domain.repository -import com.paw.key.domain.model.entity.savedlist.SavedListEntity -import com.paw.key.domain.model.entity.savedlist.SavedListPostEntity +import com.paw.key.domain.entity.savedlist.SavedListPostEntity interface SavedListRepository { suspend fun getSavedList(userId: Int): Result diff --git a/app/src/main/java/com/paw/key/domain/repository/WalkSharedResultRepository.kt b/app/src/main/java/com/paw/key/domain/repository/WalkSharedResultRepository.kt index 1915d788..567f141b 100644 --- a/app/src/main/java/com/paw/key/domain/repository/WalkSharedResultRepository.kt +++ b/app/src/main/java/com/paw/key/domain/repository/WalkSharedResultRepository.kt @@ -2,7 +2,7 @@ package com.paw.key.domain.repository import android.graphics.Bitmap import com.naver.maps.geometry.LatLng -import com.paw.key.domain.model.entity.sharedresult.WalkResult +import com.paw.key.domain.entity.sharedresult.WalkResult import kotlinx.coroutines.flow.Flow interface WalkSharedResultRepository { diff --git a/app/src/main/java/com/paw/key/domain/repository/filter/FilterOptionRepository.kt b/app/src/main/java/com/paw/key/domain/repository/filter/FilterOptionRepository.kt index 3c177169..01fe2fb1 100644 --- a/app/src/main/java/com/paw/key/domain/repository/filter/FilterOptionRepository.kt +++ b/app/src/main/java/com/paw/key/domain/repository/filter/FilterOptionRepository.kt @@ -1,6 +1,6 @@ package com.paw.key.domain.repository.filter -import com.paw.key.domain.model.entity.filter.FilterEntity +import com.paw.key.domain.entity.filter.FilterEntity interface FilterOptionRepository { diff --git a/app/src/main/java/com/paw/key/domain/repository/home/HomeRegionRepository.kt b/app/src/main/java/com/paw/key/domain/repository/home/HomeRegionRepository.kt index b342e271..5f563ace 100644 --- a/app/src/main/java/com/paw/key/domain/repository/home/HomeRegionRepository.kt +++ b/app/src/main/java/com/paw/key/domain/repository/home/HomeRegionRepository.kt @@ -1,6 +1,6 @@ package com.paw.key.domain.repository.home -import com.paw.key.domain.model.entity.home.HomeRegionDataEntity +import com.paw.key.domain.entity.home.HomeRegionDataEntity interface HomeRegionRepository { suspend fun patchRegion(userId: Int, regionId: Int): Result diff --git a/app/src/main/java/com/paw/key/domain/repository/home/RegionCurrentRepository.kt b/app/src/main/java/com/paw/key/domain/repository/home/RegionCurrentRepository.kt index 7c999e84..f82affc7 100644 --- a/app/src/main/java/com/paw/key/domain/repository/home/RegionCurrentRepository.kt +++ b/app/src/main/java/com/paw/key/domain/repository/home/RegionCurrentRepository.kt @@ -1,6 +1,6 @@ package com.paw.key.domain.repository.home -import com.paw.key.domain.model.entity.home.RegionCurrentDataEntity +import com.paw.key.domain.entity.home.RegionCurrentDataEntity interface RegionCurrentRepository { suspend fun regionCurrent(userId: Int): Result diff --git a/app/src/main/java/com/paw/key/domain/repository/list/PostsListRepository.kt b/app/src/main/java/com/paw/key/domain/repository/list/PostsListRepository.kt index b2c485d2..c7cea381 100644 --- a/app/src/main/java/com/paw/key/domain/repository/list/PostsListRepository.kt +++ b/app/src/main/java/com/paw/key/domain/repository/list/PostsListRepository.kt @@ -1,7 +1,7 @@ package com.paw.key.domain.repository.list import com.paw.key.data.dto.request.list.PostsListRequestDto -import com.paw.key.domain.model.entity.list.ListEntity +import com.paw.key.domain.entity.list.ListEntity interface PostsListRepository { suspend fun postList(userId: Int, request: PostsListRequestDto): Result diff --git a/app/src/main/java/com/paw/key/domain/repository/petprofile/PetProfileRepository.kt b/app/src/main/java/com/paw/key/domain/repository/petprofile/PetProfileRepository.kt index b7818e8d..03de6217 100644 --- a/app/src/main/java/com/paw/key/domain/repository/petprofile/PetProfileRepository.kt +++ b/app/src/main/java/com/paw/key/domain/repository/petprofile/PetProfileRepository.kt @@ -1,6 +1,6 @@ package com.paw.key.domain.repository.petprofile -import com.paw.key.domain.model.entity.petprofile.PetProfileEntity +import com.paw.key.domain.entity.petprofile.PetProfileEntity interface PetProfileRepository { suspend fun getPetProfiles(userId: Int): Result> diff --git a/app/src/main/java/com/paw/key/domain/repository/sharedwalk/SharedWalkRepository.kt b/app/src/main/java/com/paw/key/domain/repository/sharedwalk/SharedWalkRepository.kt index 39a06bde..a5ae998b 100644 --- a/app/src/main/java/com/paw/key/domain/repository/sharedwalk/SharedWalkRepository.kt +++ b/app/src/main/java/com/paw/key/domain/repository/sharedwalk/SharedWalkRepository.kt @@ -1,7 +1,7 @@ package com.paw.key.domain.repository.sharedwalk -import com.paw.key.domain.model.entity.sharedwalk.SharedWalkEntity -import com.paw.key.domain.model.entity.sharedwalk.SharedWalkReviewEntity +import com.paw.key.domain.entity.sharedwalk.SharedWalkEntity +import com.paw.key.domain.entity.sharedwalk.SharedWalkReviewEntity interface SharedWalkRepository { suspend fun getSharedWalkTrack(userId: Int, routeId: Int): Result diff --git a/app/src/main/java/com/paw/key/domain/repository/walkcourse/WalkCourseRepository.kt b/app/src/main/java/com/paw/key/domain/repository/walkcourse/WalkCourseRepository.kt index 03e786eb..4901fef0 100644 --- a/app/src/main/java/com/paw/key/domain/repository/walkcourse/WalkCourseRepository.kt +++ b/app/src/main/java/com/paw/key/domain/repository/walkcourse/WalkCourseRepository.kt @@ -1,7 +1,7 @@ package com.paw.key.domain.repository.walkcourse import com.paw.key.data.dto.request.walkcourse.WalkCourseRequestDto -import com.paw.key.domain.model.entity.walkcourse.WalkCourseRegionIdEntity +import com.paw.key.domain.entity.walkcourse.WalkCourseRegionIdEntity import okhttp3.MultipartBody interface WalkCourseRepository { diff --git a/app/src/main/java/com/paw/key/domain/repository/walklist/WalkListRepository.kt b/app/src/main/java/com/paw/key/domain/repository/walklist/WalkListRepository.kt index a63569dc..e8f3b884 100644 --- a/app/src/main/java/com/paw/key/domain/repository/walklist/WalkListRepository.kt +++ b/app/src/main/java/com/paw/key/domain/repository/walklist/WalkListRepository.kt @@ -1,7 +1,7 @@ package com.paw.key.domain.repository.walklist -import com.paw.key.domain.model.entity.walklist.WalkListDetailEntity -import com.paw.key.domain.model.entity.walklist.WalkReviewSummaryEntity +import com.paw.key.domain.entity.walklist.WalkListDetailEntity +import com.paw.key.domain.entity.walklist.WalkReviewSummaryEntity interface WalkListRepository { suspend fun getWalkListDetail(userId: Int, postId: Int): Result diff --git a/app/src/main/java/com/paw/key/domain/repository/walkreview/WalkReviewRepository.kt b/app/src/main/java/com/paw/key/domain/repository/walkreview/WalkReviewRepository.kt index f0e7553b..8d8b69e2 100644 --- a/app/src/main/java/com/paw/key/domain/repository/walkreview/WalkReviewRepository.kt +++ b/app/src/main/java/com/paw/key/domain/repository/walkreview/WalkReviewRepository.kt @@ -1,11 +1,9 @@ package com.paw.key.domain.repository.walkreview -import com.paw.key.data.dto.request.walkreview.WalkCourseReviewRequestDto -import com.paw.key.data.dto.response.walkcourse.WalkCourseResponseDto -import com.paw.key.domain.model.entity.walkreview.WalkReviewCategoryListEntity -import com.paw.key.domain.model.entity.walkreview.WalkReviewIdEntity -import com.paw.key.domain.model.entity.walkreview.WalkReviewInfoEntity -import com.paw.key.domain.model.entity.walkreview.WalkReviewRecordEntity +import com.paw.key.domain.entity.walkreview.WalkReviewCategoryListEntity +import com.paw.key.domain.entity.walkreview.WalkReviewIdEntity +import com.paw.key.domain.entity.walkreview.WalkReviewInfoEntity +import com.paw.key.domain.entity.walkreview.WalkReviewRecordEntity import okhttp3.MultipartBody interface WalkReviewRepository { diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/viewmodel/WalkCourseViewModel.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/viewmodel/WalkCourseViewModel.kt index b14737c8..8ab52bfb 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/viewmodel/WalkCourseViewModel.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walkcourse/viewmodel/WalkCourseViewModel.kt @@ -6,8 +6,8 @@ import androidx.lifecycle.viewModelScope import com.paw.key.core.extension.toLatLng import com.paw.key.core.util.PhotoUtils import com.paw.key.core.util.UiState -import com.paw.key.domain.model.entity.walkcourse.CoordinateEntity -import com.paw.key.domain.model.entity.walkcourse.WalkCourseEntity +import com.paw.key.domain.entity.walkcourse.CoordinateEntity +import com.paw.key.domain.entity.walkcourse.WalkCourseEntity import com.paw.key.domain.repository.WalkSharedResultRepository import com.paw.key.domain.repository.walkcourse.WalkCourseRepository import com.paw.key.presentation.ui.course.util.RealTimeLocationListener diff --git a/app/src/main/java/com/paw/key/presentation/ui/login/LoginScreen.kt b/app/src/main/java/com/paw/key/presentation/ui/login/LoginScreen.kt index c34eb3f6..d9173b0c 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/login/LoginScreen.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/login/LoginScreen.kt @@ -22,8 +22,8 @@ import androidx.compose.material3.Icon 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.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -36,14 +36,14 @@ import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.flowWithLifecycle import com.paw.key.R import com.paw.key.core.designsystem.theme.PawKeyTheme -import com.paw.key.core.util.PreferenceDataStore import com.paw.key.presentation.ui.login.component.LoginSocialButton +import com.paw.key.presentation.ui.login.state.LoginSideEffect import com.paw.key.presentation.ui.login.viewmodel.LoginViewModel -import kotlinx.coroutines.launch -import timber.log.Timber @Composable fun LoginRoute( @@ -51,35 +51,36 @@ fun LoginRoute( navigateUp: () -> Unit, navigateNext: () -> Unit, navigateHome: () -> Unit, + navigateSignUp: () -> Unit, snackBarHostState: SnackbarHostState, modifier: Modifier = Modifier, viewModel: LoginViewModel = hiltViewModel(), ) { val state by viewModel.state.collectAsStateWithLifecycle() val isLoginFormValid = viewModel.state.collectAsStateWithLifecycle().value.isLoginValid - val coroutineScope = rememberCoroutineScope() + val context = LocalContext.current + val lifeCycle = LocalLifecycleOwner.current + + LaunchedEffect(Unit) { + viewModel.sideEffect.flowWithLifecycle(lifeCycle.lifecycle) + .collect { sideEffect -> + when (sideEffect) { + is LoginSideEffect.NavigateToHome -> navigateHome() + is LoginSideEffect.NavigateToSignUp -> navigateSignUp() + else -> {} + } + + } + } LoginScreen( paddingValues = paddingValues, - navigateUp = navigateUp, - navigateNext = { - coroutineScope.launch { - PreferenceDataStore.saveLoginInfo( - email = state.email, - password = state.password - ) - navigateNext() - } + onGoogleSignIn = { + viewModel.onGoogleSignIn(context = context, onSuccess = navigateHome) + }, + onKakaoSignIn = { + viewModel.onKakaoSignIn(context = context) }, - snackBarHostState = snackBarHostState, - email = state.email, - password = state.password, - isPasswordVisible = state.isPasswordVisible, - isLoginFormValid = isLoginFormValid, - modifier = modifier, - onEmailChanged = viewModel::onEmailChanged, - onPasswordChanged = viewModel::onPasswordChanged, - onClickIcon = viewModel::onPasswordVisibilityChanged, navigateHome = navigateHome ) } @@ -88,27 +89,18 @@ fun LoginRoute( @Composable fun LoginScreen( paddingValues: PaddingValues, - navigateUp: () -> Unit, - navigateNext: () -> Unit, navigateHome: () -> Unit, - onEmailChanged: (String) -> Unit, - onPasswordChanged: (String) -> Unit, - onClickIcon: () -> Unit, - snackBarHostState: SnackbarHostState, - email: String, - password: String, - isPasswordVisible: Boolean, - isLoginFormValid: Boolean, + onGoogleSignIn: () -> Unit, + onKakaoSignIn: () -> Unit, modifier: Modifier = Modifier, - viewModel: LoginViewModel = hiltViewModel(), ) { - val context = LocalContext.current val scrollState = rememberScrollState() Box( modifier = modifier .fillMaxSize() - .background(PawKeyTheme.colors.white1) + .background(PawKeyTheme.colors.background) + .padding(paddingValues) ) { Column( modifier = Modifier @@ -154,10 +146,7 @@ fun LoginScreen( logo = R.drawable.ic_login_kakao, loginText = stringResource(R.string.ic_login_kakao), onClick = { - viewModel.onKakaoSignIn( - context = context, - onSuccess = navigateHome - ) + onKakaoSignIn() }, modifier = Modifier .background( @@ -170,11 +159,7 @@ fun LoginScreen( logo = R.drawable.ic_login_google, loginText = stringResource(R.string.ic_login_google), onClick = { - Timber.e("onClick LoginSocialButton") - viewModel.onGoogleSignIn( - context = context, - onSuccess = navigateHome - ) + onGoogleSignIn() }, modifier = Modifier .background( @@ -205,17 +190,9 @@ private fun PreviewLoginScreen() { PawKeyTheme { LoginScreen( paddingValues = PaddingValues(), - navigateUp = {}, - navigateNext = {}, - onEmailChanged = {}, - onPasswordChanged = {}, - onClickIcon = {}, - navigateHome = {}, - snackBarHostState = SnackbarHostState(), - email = "", - password = "", - isPasswordVisible = false, - isLoginFormValid = true, + onGoogleSignIn = {}, + onKakaoSignIn = {}, + navigateHome = {} ) } } diff --git a/app/src/main/java/com/paw/key/presentation/ui/login/navigation/LoginNavigation.kt b/app/src/main/java/com/paw/key/presentation/ui/login/navigation/LoginNavigation.kt index 9b7df2e8..c0250a9f 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/login/navigation/LoginNavigation.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/login/navigation/LoginNavigation.kt @@ -24,6 +24,7 @@ fun NavGraphBuilder.loginNavGraph( navigateUp: () -> Unit, navigateNext: () -> Unit, navigateHome: () -> Unit, + navigateSignUp: () -> Unit, snackBarHostState: SnackbarHostState ) { composable { @@ -32,6 +33,7 @@ fun NavGraphBuilder.loginNavGraph( navigateUp = navigateUp, navigateNext = navigateNext, navigateHome = navigateHome, + navigateSignUp = navigateSignUp, snackBarHostState = snackBarHostState ) } diff --git a/app/src/main/java/com/paw/key/presentation/ui/login/state/LoginContract.kt b/app/src/main/java/com/paw/key/presentation/ui/login/state/LoginContract.kt index fe31c3c4..50dae0da 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/login/state/LoginContract.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/login/state/LoginContract.kt @@ -12,11 +12,11 @@ data class LoginState( val isLoginValid get() = email.isNotBlank() && password.isNotBlank() } -sealed class LoginSideEffect { - data class ShowSnackBar(val message: String) : LoginSideEffect() - data object NavigateUp : LoginSideEffect() - data object NavigateNext : LoginSideEffect() +sealed interface LoginSideEffect { + data class ShowSnackBar(val message: String) : LoginSideEffect + data object NavigateUp : LoginSideEffect + data object NavigateNext : LoginSideEffect - data object SignInSucceed : LoginSideEffect() - data object SignInFailed : LoginSideEffect() + data object NavigateToHome : LoginSideEffect + data object NavigateToSignUp : LoginSideEffect } \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/login/viewmodel/LoginViewModel.kt b/app/src/main/java/com/paw/key/presentation/ui/login/viewmodel/LoginViewModel.kt index a5678f62..0ef57ced 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/login/viewmodel/LoginViewModel.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/login/viewmodel/LoginViewModel.kt @@ -3,7 +3,7 @@ package com.paw.key.presentation.ui.login.viewmodel import android.content.Context import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.paw.key.domain.repository.login.AuthRepository +import com.paw.key.domain.usecase.LoginUseCase import com.paw.key.presentation.ui.login.state.LoginSideEffect import com.paw.key.presentation.ui.login.state.LoginState import dagger.hilt.android.lifecycle.HiltViewModel @@ -17,7 +17,7 @@ import javax.inject.Inject @HiltViewModel class LoginViewModel @Inject constructor( - private val authRepository: AuthRepository, + private val loginUseCase: LoginUseCase ) : ViewModel() { private val _state = MutableStateFlow(LoginState()) val state: StateFlow @@ -32,17 +32,10 @@ class LoginViewModel @Inject constructor( onSuccess: () -> Unit, ) { viewModelScope.launch { - authRepository.signInWithGoogle(context) - .onSuccess { idToken -> - val deviceId = getDeviceId(context) - - authRepository.login(idToken, deviceId) - .onSuccess { response -> - onSuccess() - } - .onFailure { e -> - Timber.e(e, "Backend login failed") - } + loginUseCase.invokeGoogleLogin(context) + .onSuccess { + // Todo : isNew확인 + _sideEffect.emit(LoginSideEffect.NavigateToHome) } .onFailure { e -> Timber.e(e, "Google sign-in failed") @@ -52,33 +45,20 @@ class LoginViewModel @Inject constructor( fun onKakaoSignIn( context: Context, - onSuccess: () -> Unit, ) { viewModelScope.launch { - authRepository.signInWithKakao(context) - .onSuccess { accessToken -> - val deviceId = getDeviceId(context) - authRepository.loginKakao(accessToken, deviceId) - .onSuccess { response -> - onSuccess() - } - .onFailure { e -> - Timber.e(e, "Full stack trace:") - } - } - .onFailure { e -> - Timber.e("[KAKAO_VM] Step 2: SDK login FAILED") + loginUseCase.invokeKakaoLogin(context) + .onSuccess { + // Todo: isNewUser가 true이면이니 signup false는 home + if (it) { + _sideEffect.emit(LoginSideEffect.NavigateToSignUp) + } else { + _sideEffect.emit(LoginSideEffect.NavigateToSignUp) + } } } } - private fun getDeviceId(context: Context): String { - return android.provider.Settings.Secure.getString( - context.contentResolver, - android.provider.Settings.Secure.ANDROID_ID - ) - } - fun onEmailChanged(email: String) { _state.update { it.copy(email = email) } } 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 9b2643a6..cc1a7a27 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 @@ -10,7 +10,6 @@ import androidx.activity.enableEdgeToEdge import androidx.annotation.RequiresApi import androidx.core.view.WindowCompat import com.paw.key.core.designsystem.theme.PawKeyTheme -import com.paw.key.core.util.PreferenceDataStore import dagger.hilt.android.AndroidEntryPoint @@ -22,7 +21,6 @@ class MainActivity : ComponentActivity() { super.onCreate(savedInstanceState) enableEdgeToEdge() requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT - PreferenceDataStore.init(this) WindowCompat.getInsetsController(window, window.decorView).apply { isAppearanceLightStatusBars = true 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 138ac299..89df215a 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 @@ -13,7 +13,6 @@ import com.paw.key.presentation.ui.community.navigation.navigateCommunity import com.paw.key.presentation.ui.course.navigation.navigateWalkCourse import com.paw.key.presentation.ui.course.navigation.navigateWalkPrepare import com.paw.key.presentation.ui.course.walkreview.navigation.navigateWalkReview -import com.paw.key.presentation.ui.dummy.next.navigateDummyNext import com.paw.key.presentation.ui.home.navigation.navigateHome import com.paw.key.presentation.ui.home.navigation.navigateHomeLocationSetting import com.paw.key.presentation.ui.login.navigation.navigateLogin @@ -136,11 +135,6 @@ class MainNavigator( ) } - - fun navigateDummyNext(navOptions: NavOptions? = null) { - navController.navigateDummyNext(navOptions = navOptions) - } - fun navigateUp() { navController.navigateUp() } 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 2254c40b..7370380e 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 @@ -8,15 +8,13 @@ import androidx.compose.animation.slideOutHorizontally import androidx.compose.foundation.layout.PaddingValues import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.navigation.compose.NavHost import androidx.navigation.navOptions import com.paw.key.presentation.ui.community.navigation.communityNavGraph -import com.paw.key.presentation.ui.course.navigation.navigateWalkPrepare import com.paw.key.presentation.ui.course.navigation.walkCourseGraph import com.paw.key.presentation.ui.course.walkreview.navigation.walkReviewNavGraph -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.homeLocationSettingNavGraph import com.paw.key.presentation.ui.home.navigation.homeNavGraph import com.paw.key.presentation.ui.login.navigation.loginNavGraph @@ -38,6 +36,15 @@ fun PawKeyNavHost( snackbarHostState: SnackbarHostState, modifier: Modifier = Modifier, ) { + val clearStackNavOptions = remember { + navOptions { + popUpTo(0) { + inclusive = true + } + launchSingleTop = true + } + } + NavHost( navController = navigator.navController, startDestination = navigator.startDestination, @@ -100,7 +107,7 @@ fun PawKeyNavHost( communityNavGraph( paddingValues = paddingValues, navigateUp = navigator::navigateUp, - navigateNext = navigator::navigateDummyNext, + navigateNext = {}, snackBarHostState = snackbarHostState ) @@ -132,7 +139,7 @@ fun PawKeyNavHost( userProfileNavGraph( paddingValues = paddingValues, navigateUp = navigator::navigateMyPage, - navigateNext = navigator::navigateDummyNext, + navigateNext = {}, snackBarHostState = snackbarHostState ) @@ -145,17 +152,6 @@ fun PawKeyNavHost( navigatePetProfile = navigator::navigatePetProfile, ) - dummyNavGraph( - paddingValues = paddingValues, - navigateUp = navigator::navigateUp, - navigateNext = navigator::navigateDummyNext, - snackBarHostState = snackbarHostState - ) - - dummyNextNavGraph( - paddingValues = paddingValues - ) - splashNavGraph( paddingValues = paddingValues, navigateLogin = { @@ -186,8 +182,12 @@ fun PawKeyNavHost( navigateNext = { //navigator.navigateSignUpFlow() }, + navigateSignUp = { + navigator.navigateSignUp(clearStackNavOptions) + }, + // Todo: Home 으로 수정 navigateHome = { - navigator.navigateHome() + navigator.navigateSignUp(clearStackNavOptions) }, snackBarHostState = snackbarHostState ) diff --git a/app/src/main/java/com/paw/key/presentation/ui/mypage/main/MyPageScreen.kt b/app/src/main/java/com/paw/key/presentation/ui/mypage/main/MyPageScreen.kt index 64da9762..898f83ad 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/mypage/main/MyPageScreen.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/mypage/main/MyPageScreen.kt @@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview @@ -17,7 +16,6 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.paw.key.core.designsystem.component.TopBar import com.paw.key.core.designsystem.theme.PawKeyTheme -import com.paw.key.core.util.PreferenceDataStore import com.paw.key.presentation.ui.mypage.courseinfo.model.CourseType import com.paw.key.presentation.ui.mypage.main.component.MyList import com.paw.key.presentation.ui.mypage.main.component.MyPageCard @@ -26,7 +24,6 @@ import com.paw.key.presentation.ui.mypage.main.component.SettingList import com.paw.key.presentation.ui.mypage.main.model.MyListState import com.paw.key.presentation.ui.mypage.main.model.MyPageState import com.paw.key.presentation.ui.mypage.main.viewmodel.MyPageViewModel -import kotlinx.coroutines.flow.first @Composable fun MyPageRoute( @@ -40,12 +37,6 @@ fun MyPageRoute( viewModel: MyPageViewModel = hiltViewModel(), ) { val state by viewModel.state.collectAsStateWithLifecycle() - val userId = PreferenceDataStore.getUserId() - - LaunchedEffect(Unit) { - viewModel.getUserProfiles(userId = userId.first()) - viewModel.getPetProfiles(userId = userId.first()) - } MyPageScreen( state = state, diff --git a/app/src/main/java/com/paw/key/presentation/ui/mypage/main/viewmodel/MyPageViewModel.kt b/app/src/main/java/com/paw/key/presentation/ui/mypage/main/viewmodel/MyPageViewModel.kt index fa0f1059..18c50a11 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/mypage/main/viewmodel/MyPageViewModel.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/mypage/main/viewmodel/MyPageViewModel.kt @@ -2,6 +2,7 @@ package com.paw.key.presentation.ui.mypage.main.viewmodel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.paw.key.domain.repository.localstorage.LocalStorageRepository import com.paw.key.domain.repository.petprofile.PetProfileRepository import com.paw.key.domain.repository.userprofile.UserProfileRepository import com.paw.key.presentation.ui.mypage.main.model.MyPageSideEffect @@ -19,6 +20,7 @@ import javax.inject.Inject class MyPageViewModel @Inject constructor( private val petProfileRepository: PetProfileRepository, private val userProfileRepository: UserProfileRepository, + private val localRepository: LocalStorageRepository ) : ViewModel() { private val _state = MutableStateFlow(MyPageState()) val state: StateFlow @@ -27,6 +29,14 @@ class MyPageViewModel @Inject constructor( private val _sideEffect = MutableSharedFlow() val sideEffect: MutableSharedFlow = _sideEffect + init { + viewModelScope.launch { + val userId = localRepository.getUserId() + getUserProfiles(userId) + getPetProfiles(userId) + } + } + fun getUserProfiles(userId: Int) { viewModelScope.launch { userProfileRepository.getUserProfiles(userId) diff --git a/app/src/main/java/com/paw/key/presentation/ui/mypage/petinfo/PetProfileScreen.kt b/app/src/main/java/com/paw/key/presentation/ui/mypage/petinfo/PetProfileScreen.kt index 93a73a54..afdbf9eb 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/mypage/petinfo/PetProfileScreen.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/mypage/petinfo/PetProfileScreen.kt @@ -22,7 +22,6 @@ import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -47,7 +46,6 @@ import com.paw.key.core.designsystem.component.PawkeyButton import com.paw.key.core.designsystem.component.TopBar import com.paw.key.core.designsystem.theme.PawKeyTheme import com.paw.key.core.extension.noRippleClickable -import com.paw.key.core.util.PreferenceDataStore import com.paw.key.presentation.ui.mypage.petinfo.viewmodel.PetProfileViewModel import com.paw.key.presentation.ui.signup.component.FormField import com.paw.key.presentation.ui.signup.component.GenderSelector @@ -55,8 +53,9 @@ import com.paw.key.presentation.ui.signup.component.PetBreedSearchContent import com.paw.key.presentation.ui.signup.component.SignUpNeuteringCheckRadio import com.paw.key.presentation.ui.signup.component.SignUpPetImageHolder import com.paw.key.presentation.ui.signup.component.SignUpTextField +import com.paw.key.presentation.ui.signup.model.PetInfoItemModel import com.paw.key.presentation.ui.signup.state.Gender -import kotlinx.coroutines.flow.first +import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.launch @Composable @@ -65,11 +64,7 @@ fun PetProfileRoute( viewModel: PetProfileViewModel = hiltViewModel(), ) { val state = viewModel.state.collectAsStateWithLifecycle() - val userId = PreferenceDataStore.getUserId() - LaunchedEffect(Unit) { - viewModel.getPetProfiles(userId.first()) - } PetProfileScreen( petName = state.value.name, @@ -104,7 +99,7 @@ fun PetProfileScreen( onPetBirthDateChanged: (String) -> Unit, onPetGenderChanged: (Gender) -> Unit, onPetNeuteredChanged: (Boolean) -> Unit, - onPetBreedChanged: (String) -> Unit, + onPetBreedChanged: (PetInfoItemModel) -> Unit, onSelectedImage: (Uri?) -> Unit, modifier: Modifier = Modifier, ) { @@ -268,7 +263,7 @@ fun PetProfileScreen( content = { SignUpTextField( value = petBreed, - onValueChange = onPetBreedChanged, + onValueChange = {}, enabled = false, placeholder = "견종을 검색해보세요", suffix = { @@ -294,6 +289,7 @@ fun PetProfileScreen( //sheetGesturesEnabled = false, ) { sheetState -> PetBreedSearchContent( + petBreedList = persistentListOf(), sheetState = sheetState, selectedBreed = petBreed, onBreedSelected = { diff --git a/app/src/main/java/com/paw/key/presentation/ui/mypage/petinfo/viewmodel/PetProfileViewModel.kt b/app/src/main/java/com/paw/key/presentation/ui/mypage/petinfo/viewmodel/PetProfileViewModel.kt index 67500236..afa14118 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/mypage/petinfo/viewmodel/PetProfileViewModel.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/mypage/petinfo/viewmodel/PetProfileViewModel.kt @@ -3,6 +3,7 @@ package com.paw.key.presentation.ui.mypage.petinfo.viewmodel import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.paw.key.domain.repository.localstorage.LocalStorageRepository import com.paw.key.domain.repository.petprofile.PetProfileRepository import com.paw.key.presentation.ui.mypage.petinfo.model.PetProfileSideEffect import com.paw.key.presentation.ui.mypage.petinfo.model.PetProfileState @@ -17,7 +18,8 @@ import javax.inject.Inject @HiltViewModel class PetProfileViewModel @Inject constructor( - private val petProfileRepository: PetProfileRepository + private val petProfileRepository: PetProfileRepository, + private val localRepository: LocalStorageRepository ) : ViewModel() { private val _state = MutableStateFlow(PetProfileState()) @@ -26,6 +28,13 @@ class PetProfileViewModel @Inject constructor( private val _sideEffect = MutableSharedFlow() // 필요 시 따로 Contract로 분리 가능 val sideEffect: MutableSharedFlow = _sideEffect + init { + viewModelScope.launch { + val userId = localRepository.getUserId() + getPetProfiles(userId) + } + } + fun getPetProfiles(userId: Int) { viewModelScope.launch { petProfileRepository.getPetProfiles(userId) diff --git a/app/src/main/java/com/paw/key/presentation/ui/mypage/userinfo/UserProfileScreen.kt b/app/src/main/java/com/paw/key/presentation/ui/mypage/userinfo/UserProfileScreen.kt index d6cc9a60..e97fd315 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/mypage/userinfo/UserProfileScreen.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/mypage/userinfo/UserProfileScreen.kt @@ -9,7 +9,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -18,12 +17,10 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.paw.key.core.designsystem.component.PawkeyButton import com.paw.key.core.designsystem.component.TopBar import com.paw.key.core.designsystem.theme.PawKeyTheme -import com.paw.key.core.util.PreferenceDataStore import com.paw.key.presentation.ui.mypage.userinfo.component.UserEditTextField import com.paw.key.presentation.ui.mypage.userinfo.component.UserGenderButton import com.paw.key.presentation.ui.mypage.userinfo.component.UserProfileItem import com.paw.key.presentation.ui.mypage.userinfo.viewmodel.UserProfileViewModel -import kotlinx.coroutines.flow.first @Composable fun UserProfileRoute( @@ -32,11 +29,6 @@ fun UserProfileRoute( viewModel: UserProfileViewModel = hiltViewModel(), ) { val state = viewModel.state.collectAsStateWithLifecycle() - val userId = PreferenceDataStore.getUserId() - - LaunchedEffect(Unit) { - viewModel.getUserProfiles(userId = userId.first()) - } UserProfileScreen( name = state.value.name, diff --git a/app/src/main/java/com/paw/key/presentation/ui/mypage/userinfo/viewmodel/UserProfileViewModel.kt b/app/src/main/java/com/paw/key/presentation/ui/mypage/userinfo/viewmodel/UserProfileViewModel.kt index ed1bdf6b..01b5d7a3 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/mypage/userinfo/viewmodel/UserProfileViewModel.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/mypage/userinfo/viewmodel/UserProfileViewModel.kt @@ -3,7 +3,7 @@ package com.paw.key.presentation.ui.mypage.userinfo.viewmodel import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.paw.key.core.util.PreferenceDataStore +import com.paw.key.domain.repository.localstorage.LocalStorageRepository import com.paw.key.domain.repository.userprofile.UserProfileRepository import com.paw.key.presentation.ui.mypage.userinfo.model.UserProfileSideEffect import com.paw.key.presentation.ui.mypage.userinfo.model.UserProfileState @@ -18,7 +18,8 @@ import javax.inject.Inject @HiltViewModel class UserProfileViewModel @Inject constructor( - private val userProfileRepository: UserProfileRepository + private val userProfileRepository: UserProfileRepository, + private val localRepository: LocalStorageRepository ) : ViewModel() { private val _state = MutableStateFlow(UserProfileState()) @@ -27,6 +28,13 @@ class UserProfileViewModel @Inject constructor( private val _sideEffect = MutableSharedFlow() val sideEffect: MutableSharedFlow = _sideEffect + init { + viewModelScope.launch { + val userId = localRepository.getUserId() + getUserProfiles(userId) + } + } + fun getUserProfiles(userId: Int) { viewModelScope.launch { userProfileRepository.getUserProfiles(userId) @@ -44,7 +52,7 @@ class UserProfileViewModel @Inject constructor( } try { - PreferenceDataStore.saveActiveRegion(result.activeRegion) + //PreferenceDataStore.saveActiveRegion(result.activeRegion) Log.d("UserProfileViewModel", "activeRegion 저장 완료: ${result.activeRegion}") } catch (e: Exception) { Log.e("UserProfileViewModel", "activeRegion 저장 실패: ${e.message}") diff --git a/app/src/main/java/com/paw/key/presentation/ui/region/viewmodel/RegionViewModel.kt b/app/src/main/java/com/paw/key/presentation/ui/region/viewmodel/RegionViewModel.kt index 331ec7d1..ac8df4ac 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/region/viewmodel/RegionViewModel.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/region/viewmodel/RegionViewModel.kt @@ -5,12 +5,12 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.navigation.toRoute -import com.paw.key.core.util.PreferenceDataStore import com.paw.key.core.util.UiState import com.paw.key.core.util.flattenCoordinatesToLatLng import com.paw.key.core.util.handleError import com.paw.key.domain.repository.RegionRepository import com.paw.key.domain.repository.home.HomeRegionRepository +import com.paw.key.domain.repository.localstorage.LocalStorageRepository import com.paw.key.presentation.ui.region.navigation.Regional import com.paw.key.presentation.ui.region.state.DrawType import com.paw.key.presentation.ui.region.state.RegionSideEffect @@ -19,12 +19,8 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import timber.log.Timber @@ -34,7 +30,8 @@ import javax.inject.Inject class RegionViewModel @Inject constructor( savedStateHandle: SavedStateHandle, private val regionRepository: RegionRepository, - private val homeRepository: HomeRegionRepository + private val homeRepository: HomeRegionRepository, + private val localStorageRepository: LocalStorageRepository ) : ViewModel() { private val _state = MutableStateFlow(RegionState()) val state: StateFlow = _state.asStateFlow() @@ -43,30 +40,23 @@ class RegionViewModel @Inject constructor( val sideEffect : MutableSharedFlow get() = _sideEffect - private val regionIdState = savedStateHandle.toRoute() - private val userId : StateFlow = PreferenceDataStore.getUserId() - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5000), - initialValue = -1 - ) + + private val regionIdState = savedStateHandle.toRoute() init { if (regionIdState.regionId != -1) { viewModelScope.launch { - val validUserId = userId.filter { it != -1 }.first() getRegionGeometry( - userId = validUserId, + userId = localStorageRepository.getUserId(), regionId = regionIdState.regionId, ) } } else { viewModelScope.launch { Timber.e("RegionViewModel test용 regionId: ${regionIdState.regionId}") - val validUserId = userId.filter { it != -1 }.first() getRegionGeometry( - userId = validUserId, + userId = localStorageRepository.getUserId(), regionId = 39, ) } @@ -125,7 +115,7 @@ class RegionViewModel @Inject constructor( fun patchRegion() { if (regionIdState.regionId != -1) { viewModelScope.launch { - homeRepository.patchRegion(userId.value, regionIdState.regionId!!) + homeRepository.patchRegion(localStorageRepository.getUserId(), regionIdState.regionId!!) .onSuccess { data -> Log.d("RegionViewModel", "API 응답 성공: $data") _sideEffect.emit( diff --git a/app/src/main/java/com/paw/key/presentation/ui/signup/component/PetBreedSearchContent.kt b/app/src/main/java/com/paw/key/presentation/ui/signup/component/PetBreedSearchContent.kt index db48ec76..8483ac2b 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/signup/component/PetBreedSearchContent.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/signup/component/PetBreedSearchContent.kt @@ -1,6 +1,7 @@ package com.paw.key.presentation.ui.signup.component import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth @@ -24,34 +25,32 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.paw.key.R import com.paw.key.core.designsystem.theme.PawKeyTheme +import com.paw.key.core.extension.disableNestedScroll import com.paw.key.core.extension.noRippleClickable +import com.paw.key.presentation.ui.signup.model.PetInfoItemModel +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.launch -private val dummyPetBreeds = listOf( - "닥스훈트", "달마시안", "말라뮤트", "말티즈", "믹스견", - "미니핀", "보스턴 테리어", "불독", "비글", "비숑 프리제", - "사모예드", "시바견", "시츄", "요크셔 테리어", "진돗개", - "치와와", "포메라니안", "푸들" -) - @OptIn(ExperimentalMaterial3Api::class) @Composable fun PetBreedSearchContent( + petBreedList : ImmutableList, sheetState: SheetState, selectedBreed : String, - onBreedSelected : (String) -> Unit, + onBreedSelected : (PetInfoItemModel) -> Unit, modifier: Modifier = Modifier ) { // 바텀 시트용 - var bottomPetBreed by remember { - mutableStateOf("") - } + var searchText by remember { mutableStateOf("") } val scope = rememberCoroutineScope() @@ -65,6 +64,10 @@ fun PetBreedSearchContent( shape = RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp) ) .padding(horizontal = 16.dp) + .pointerInput(Unit) { + detectTapGestures { + } + } ) { Text( text = "견종 검색", @@ -80,10 +83,8 @@ fun PetBreedSearchContent( ) SignUpTextField( - value = bottomPetBreed, - onValueChange = { - bottomPetBreed = it - }, + value = searchText, + onValueChange = { searchText = it }, placeholder = "견종을 검색해보세요", suffix = { Icon( @@ -102,8 +103,8 @@ fun PetBreedSearchContent( ) PetBreedSearchList( - breedList = dummyPetBreeds, - petBreed = bottomPetBreed, + breedList = petBreedList, + searchText = searchText, selectedBreed = selectedBreed, onBreedSelected = onBreedSelected, modifier = Modifier @@ -114,29 +115,35 @@ fun PetBreedSearchContent( @Composable fun PetBreedSearchList( - breedList : List, - petBreed : String, + breedList : ImmutableList, + searchText : String, selectedBreed : String, - onBreedSelected : (String) -> Unit, + onBreedSelected : (PetInfoItemModel) -> Unit, modifier: Modifier = Modifier ) { - val filteredList = if (petBreed.isBlank()) { - breedList - } else { - breedList.filter { it.contains(petBreed, ignoreCase = true) } + val filteredList = remember(searchText, breedList) { + if (searchText.isBlank()) { + breedList + } else { + breedList.filter { + it.name.contains(searchText, ignoreCase = true) + }.toImmutableList() + } } LazyColumn( modifier = modifier + .disableNestedScroll() .padding(top = 8.dp) ) { itemsIndexed( - items = filteredList + items = filteredList, + key = { _, item -> item.id } ) { index, item -> PetBreedSearchItem( - petBreed = item, - onBreedSelected = onBreedSelected, - isPetBreedSelected = petBreed == item || selectedBreed == item, + petBreedItem = item, + isSelected = item.name == selectedBreed, + onBreedSelected = onBreedSelected ) } } @@ -144,25 +151,25 @@ fun PetBreedSearchList( @Composable fun PetBreedSearchItem( - petBreed : String, - onBreedSelected : (String) -> Unit, - isPetBreedSelected : Boolean, + petBreedItem: PetInfoItemModel, + isSelected: Boolean, + onBreedSelected: (PetInfoItemModel) -> Unit, modifier: Modifier = Modifier ) { - val textColor = if (isPetBreedSelected) { + val textColor = if (isSelected) { PawKeyTheme.colors.background } else { PawKeyTheme.colors.contents } - val backgroundColor = if (isPetBreedSelected) { + val backgroundColor = if (isSelected) { PawKeyTheme.colors.primary } else { PawKeyTheme.colors.background } Text( - text = petBreed, + text = petBreedItem.name, style = PawKeyTheme.typography.bodyActive, color = textColor, modifier = modifier @@ -176,7 +183,7 @@ fun PetBreedSearchItem( vertical = 11.dp ) .noRippleClickable { - onBreedSelected(petBreed) + onBreedSelected(petBreedItem) }, textAlign = TextAlign.Start ) @@ -190,7 +197,8 @@ private fun PetPetBreedSearchContentPreview() { PetBreedSearchContent( selectedBreed = "", onBreedSelected = {}, - sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true), + petBreedList = persistentListOf() ) } } \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/signup/component/RegionSearchContent.kt b/app/src/main/java/com/paw/key/presentation/ui/signup/component/RegionSearchContent.kt index 27c9f402..2ad25ab2 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/signup/component/RegionSearchContent.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/signup/component/RegionSearchContent.kt @@ -28,87 +28,42 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.paw.key.R import com.paw.key.core.designsystem.theme.PawKeyTheme +import com.paw.key.core.extension.disableNestedScroll import com.paw.key.core.extension.noRippleClickable - -private val dummySeoulRegions = listOf( - Pair( - "강남구", listOf( - "개포동", "논현동", "대치동", "도곡동", "삼성동", "세곡동", "수서동", - "신사동", "압구정동", "역삼동", "율현동", "일원동", "자곡동", "청담동" - ) - ), - Pair( - "구로구", listOf( - "가리봉동", "개봉동", "고척동", "구로동", "궁동", "신도림동", "오류동", - "온수동", "천왕동", "항동" - ) - ), - Pair( - "금천구", listOf( - "가산동", "독산동", "시흥동" - ) - ), - Pair( - "노원구", listOf( - "공릉동", "상계동", "월계동", "중계동", "하계동" - ) - ), - Pair( - "도봉구", listOf( - "도봉동", "방학동", "쌍문동", "창동" - ) - ), - Pair( - "동대문구", listOf( - "답십리동", "신설동", "용두동", "이문동", "장안동", "전농동", "제기동", - "청량리동", "회기동", "휘경동" - ) - ), - Pair( - "동작구", listOf( - "노량진동", "대방동", "동작동", "본동", "사당동", "상도1동", "상도동", - "신대방동", "흑석동" - ) - ) -) +import com.paw.key.presentation.ui.signup.model.DistrictModel +import com.paw.key.presentation.ui.signup.model.DongModel +import com.paw.key.presentation.ui.signup.model.GuModel +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList @Composable fun RegionSearchContent( - selectedGu: String, - selectedDong: String, - onRegionSelected: (gu: String, dong: String) -> Unit, + regionList: ImmutableList, + selectedGu: GuModel, + selectedDong: DongModel, + onRegionSelected: (GuModel, DongModel) -> Unit, modifier: Modifier = Modifier, ) { - var searchQuery by remember { mutableStateOf("") } - - var selectedGu by remember { - mutableStateOf(selectedGu) - } + var searchText by remember { mutableStateOf("") } - var selectedDong by remember { - mutableStateOf(selectedDong) + var currentGu by remember(selectedGu) { + mutableStateOf(if (selectedGu.id != 0) selectedGu else regionList.firstOrNull()?.gu ?: GuModel(0, "")) } - val filteredRegions = remember(searchQuery) { - if (searchQuery.isBlank()) { - dummySeoulRegions + val filteredRegionList = remember(searchText, regionList) { + if (searchText.isBlank()) { + regionList } else { - dummySeoulRegions.mapNotNull { (gu, dongList) -> - val matchingDongs = dongList.filter { dong -> - "$gu $dong".contains(searchQuery, ignoreCase = true) - } - - if (matchingDongs.isNotEmpty()) { - Pair(gu, matchingDongs) - } else { - null - } - } + regionList.filter { + it.gu.name.contains(searchText, ignoreCase = true) + }.toImmutableList() } } - val dongListForSelectedGu = remember(selectedGu, filteredRegions) { - filteredRegions.find { it.first == selectedGu }?.second ?: emptyList() + val currentDongList = remember(currentGu, filteredRegionList) { + // 전체 리스트에서 찾아야 동 정보가 유실되지 않음 + regionList.find { it.gu.id == currentGu.id }?.dongs ?: persistentListOf() } Column( @@ -136,9 +91,9 @@ fun RegionSearchContent( ) SignUpTextField( - value = searchQuery, + value = searchText, onValueChange = { - searchQuery = it + searchText = it }, placeholder = "지역을 검색해보세요", suffix = { @@ -151,18 +106,10 @@ fun RegionSearchContent( ) RegionSearchList( - guList = filteredRegions.map { it.first }, - dongList = dongListForSelectedGu, + regionList = filteredRegionList, selectedGu = selectedGu, selectedDong = selectedDong, - onGuSelected = { gu -> - selectedGu = gu - selectedDong = "" - }, - onDongSelected = { dong -> - selectedDong = dong - onRegionSelected(selectedGu, selectedDong) - }, + onRegionSelected = onRegionSelected, modifier = Modifier .weight(1f) ) @@ -171,31 +118,40 @@ fun RegionSearchContent( @Composable private fun RegionSearchList( - guList: List, - dongList: List, - selectedGu: String, - selectedDong: String, - onGuSelected: (String) -> Unit, - onDongSelected: (String) -> Unit, + regionList: ImmutableList, + selectedGu: GuModel, + selectedDong: DongModel, + onRegionSelected: (GuModel, DongModel) -> Unit, modifier: Modifier = Modifier ) { + var currentGu by remember(selectedGu) { + mutableStateOf(if (selectedGu.id != 0) selectedGu else regionList.firstOrNull()?.gu ?: GuModel(0,"")) + } + + val currentDongList = remember(currentGu, regionList) { + regionList.find { it.gu.id == currentGu.id }?.dongs ?: persistentListOf() + } + Row( modifier = modifier .padding(vertical = 8.dp), ) { LazyColumn( modifier = Modifier + .disableNestedScroll() .weight(0.45f), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { - items(guList) { gu -> + items( + items = regionList, + key = { it.gu.id } + ) { item -> RegionItem( - name = gu, - isSelected = gu == selectedGu, - onClick = { - onGuSelected(gu) - } + name = item.gu.name, + isSelected = item.gu.id == currentGu.id, + onClick = { currentGu = item.gu }, + textAlign = TextAlign.Center ) } } @@ -212,17 +168,22 @@ private fun RegionSearchList( ) LazyColumn( - modifier = Modifier.weight(1f), + modifier = Modifier + .disableNestedScroll() + .weight(1f), horizontalAlignment = Alignment.Start ) { - items(dongList) { dong -> + items( + items = currentDongList, + key = { it.id } + ) { dong -> RegionItem( - name = dong, - isSelected = dong == selectedDong, + name = dong.name, + isSelected = (dong.id == selectedDong.id) && (currentGu.id == selectedGu.id), onClick = { - onDongSelected(dong) + onRegionSelected(currentGu, dong) }, - textAlign = TextAlign.Start + textAlign = TextAlign.Center ) } } @@ -269,10 +230,10 @@ private fun RegionItem( private fun RegionSearchContentPreview() { PawKeyTheme { RegionSearchContent( - selectedGu = "", - selectedDong = "", + regionList = persistentListOf(), + selectedGu = GuModel(0, ""), + selectedDong = DongModel(0, ""), onRegionSelected = { _, _ -> } ) - } } \ No newline at end of file