From 5d8cdd1281e0ade3293d7c9ad177e105c3261d3e Mon Sep 17 00:00:00 2001 From: Son Juwan Date: Thu, 17 Jul 2025 01:14:12 +0900 Subject: [PATCH 1/3] =?UTF-8?q?mod/#108:=20Signup=20Ux=20=EB=94=94?= =?UTF-8?q?=ED=85=8C=EC=9D=BC=20=EC=9E=A1=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/ui/signup/SignUpScreen.kt | 182 ++++++++++++------ .../ui/signup/component/SignUpTextField.kt | 26 ++- 2 files changed, 136 insertions(+), 72 deletions(-) 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 f157601e..e63e76b3 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 @@ -7,12 +7,22 @@ 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.imePadding import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel @@ -62,79 +72,125 @@ fun SignUpScreen( viewModel: SignUpViewModel ) { val state by viewModel.state.collectAsState() + val keyboardController = LocalSoftwareKeyboardController.current + val focusManager = LocalFocusManager.current - Column( + // FocusRequester들 생성 + val nameFocusRequester = remember { FocusRequester() } + val ageFocusRequester = remember { FocusRequester() } + + LazyColumn( modifier = modifier .fillMaxSize() + .imePadding() ) { - SignUpHeader( - title = stringResource(R.string.ic_onboarding_signup), - subtitle = stringResource(id = R.string.ic_onboarding_signup_subtitle_step1), - progress = step, - ) - Column( - modifier = Modifier - .fillMaxSize() - .padding(horizontal = 16.dp) - ) { - Spacer(modifier = Modifier.height(42.dp)) - - FormField( - label = stringResource(id = R.string.ic_onboarding_signup_name), - content = { - SignUpTextField( - value = state.name, - onValueChange = viewModel::onNameChanged, - placeholder = "이름을 입력해주세요" - ) - } - ) - - Spacer(modifier = Modifier.height(33.dp)) - - FormField( - label = stringResource(id = R.string.ic_onboarding_signup_gender), - content = { - GenderSelector( - selectedGender = state.selectedGender, - onGenderSelected = viewModel::selectGender - ) - } - ) - - Spacer(modifier = Modifier.height(33.dp)) - - FormField( - label = stringResource(id = R.string.ic_onboarding_signup_age), - content = { - SignUpTextField( - value = state.age, - onValueChange = viewModel::onAgeChanged, - placeholder = "나이를 입력해주세요" - ) - } + item { + SignUpHeader( + title = stringResource(R.string.ic_onboarding_signup), + subtitle = stringResource(id = R.string.ic_onboarding_signup_subtitle_step1), + progress = step, ) + } - Spacer(modifier = Modifier.weight(1f)) - - val isFormValid = state.name.isNotBlank() && - state.age.isNotBlank() && - state.selectedGender != SignUpContract.Gender.UNKNOWN - - PawkeyButton( - text = stringResource(id = R.string.ic_onboarding_signup_button), - enabled = isFormValid, - onClick = { - if (isFormValid) { - navigateSignUpActivity() + item { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + ) { + Spacer(modifier = Modifier.height(42.dp)) + + FormField( + label = stringResource(id = R.string.ic_onboarding_signup_name), + content = { + SignUpTextField( + value = state.name, + onValueChange = viewModel::onNameChanged, + placeholder = "이름을 입력해주세요", + modifier = Modifier.focusRequester(nameFocusRequester), + keyboardOptions = KeyboardOptions( + imeAction = androidx.compose.ui.text.input.ImeAction.Next, + keyboardType = KeyboardType.Text + ), + keyboardActions = KeyboardActions( + onNext = { + focusManager.clearFocus() + } + ) + ) } - } - ) + ) + + Spacer(modifier = Modifier.height(33.dp)) + + FormField( + label = stringResource(id = R.string.ic_onboarding_signup_gender), + content = { + GenderSelector( + selectedGender = state.selectedGender, + onGenderSelected = { gender -> + viewModel.selectGender(gender) + // 성별 선택 후 나이 입력으로 포커싱 + ageFocusRequester.requestFocus() + } + ) + } + ) + + Spacer(modifier = Modifier.height(33.dp)) + + FormField( + label = stringResource(id = R.string.ic_onboarding_signup_age), + content = { + SignUpTextField( + value = state.age, + onValueChange = viewModel::onAgeChanged, + placeholder = "나이를 입력해주세요", + modifier = Modifier.focusRequester(ageFocusRequester), + keyboardOptions = KeyboardOptions( + imeAction = androidx.compose.ui.text.input.ImeAction.Done, + keyboardType = KeyboardType.Number + ), + keyboardActions = KeyboardActions( + onDone = { + keyboardController?.hide() + focusManager.clearFocus() + + // 폼이 유효하면 다음 단계로 + val isFormValid = state.name.isNotBlank() && + state.age.isNotBlank() && + state.selectedGender != SignUpContract.Gender.UNKNOWN + + if (isFormValid) { + navigateSignUpActivity() + } + } + ) + ) + } + ) + + Spacer(modifier = Modifier.height(60.dp)) + + val isFormValid = state.name.isNotBlank() && + state.age.isNotBlank() && + state.selectedGender != SignUpContract.Gender.UNKNOWN + + PawkeyButton( + text = stringResource(id = R.string.ic_onboarding_signup_button), + enabled = isFormValid, + onClick = { + if (isFormValid) { + keyboardController?.hide() + navigateSignUpActivity() + } + } + ) - Spacer(modifier = Modifier.height(46.dp)) + Spacer(modifier = Modifier.height(46.dp)) + } } } - } @Composable diff --git a/app/src/main/java/com/paw/key/presentation/ui/signup/component/SignUpTextField.kt b/app/src/main/java/com/paw/key/presentation/ui/signup/component/SignUpTextField.kt index 8e217255..c4f23248 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/signup/component/SignUpTextField.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/signup/component/SignUpTextField.kt @@ -7,17 +7,21 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.unit.dp import com.paw.key.core.designsystem.theme.PawKeyTheme import com.paw.key.core.util.noRippleClickable - @Composable fun SignUpTextField( value: String, @@ -25,16 +29,18 @@ fun SignUpTextField( modifier: Modifier = Modifier, placeholder: String, enabled: Boolean = true, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + keyboardActions: KeyboardActions = KeyboardActions.Default, + singleLine: Boolean = true, ) { - val isClicked = remember { mutableStateOf(false) } + val isFocused = remember { mutableStateOf(false) } val borderColor = when { !enabled -> PawKeyTheme.colors.gray100 - isClicked.value -> PawKeyTheme.colors.green500 + isFocused.value -> PawKeyTheme.colors.green500 else -> PawKeyTheme.colors.gray200 } - BasicTextField( value = value, onValueChange = onValueChange, @@ -42,8 +48,8 @@ fun SignUpTextField( .fillMaxWidth() .height(52.dp) .clip(RoundedCornerShape(8.dp)) - .noRippleClickable { - isClicked.value = true + .onFocusChanged { focusState -> + isFocused.value = focusState.isFocused } .background(color = PawKeyTheme.colors.white1) .border( @@ -52,8 +58,11 @@ fun SignUpTextField( shape = RoundedCornerShape(8.dp) ), textStyle = PawKeyTheme.typography.body14R, - maxLines = 1, + maxLines = if (singleLine) 1 else Int.MAX_VALUE, + singleLine = singleLine, enabled = enabled, + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, decorationBox = { innerTextField -> androidx.compose.foundation.layout.Box( modifier = Modifier @@ -73,5 +82,4 @@ fun SignUpTextField( } } ) -} - +} \ No newline at end of file From e8438de87098b0745ee10f016a8b0f67b86ffeca Mon Sep 17 00:00:00 2001 From: Son Juwan Date: Thu, 17 Jul 2025 01:14:45 +0900 Subject: [PATCH 2/3] =?UTF-8?q?mod/#108:=20=EA=B3=B5=ED=86=B5=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20conflict=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/designsystem/component/CourseCard.kt | 51 ++++++++----------- .../entire/tab/map/List/TabListScreen.kt | 3 ++ .../map/List/viewmodel/TapListViewModel.kt | 32 ++++++++++++ .../ui/mypage/ArchivedCourseListScreen.kt | 6 ++- .../ui/mypage/SavedCourseListScreen.kt | 11 +++- 5 files changed, 70 insertions(+), 33 deletions(-) diff --git a/app/src/main/java/com/paw/key/core/designsystem/component/CourseCard.kt b/app/src/main/java/com/paw/key/core/designsystem/component/CourseCard.kt index 44a540dd..1abdc3c6 100644 --- a/app/src/main/java/com/paw/key/core/designsystem/component/CourseCard.kt +++ b/app/src/main/java/com/paw/key/core/designsystem/component/CourseCard.kt @@ -2,6 +2,7 @@ package com.paw.key.core.designsystem.component import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape @@ -35,22 +36,19 @@ fun CourseCard( createdAt: String, isLiked: Boolean, onClickItem: () -> Unit, + onClickLike: (Boolean) -> Unit, petName: String, - representativeImageUrl: String? = null, // 추가 - petProfileImageUrl: String? = null, // 추가 - descriptionTags: List = emptyList(), // 추가 modifier: Modifier = Modifier, - isShared: Boolean = false, - isRecord: Boolean = false + representativeImageUrl: String? = null, + petProfileImageUrl: String? = null, + descriptionTags: List = emptyList() ) { - // 날짜 포맷 변환 함수 fun formatDate(dateString: String): String { return try { - // "2025-07-15T21:27:03.54498" -> "2025/07/15" - val datePart = dateString.split("T")[0] // "2025-07-15" - datePart.replace("-", "/") // "2025/07/15" + val datePart = dateString.split("T")[0] + datePart.replace("-", "/") } catch (e: Exception) { - dateString // 실패하면 원본 반환 + dateString } } @@ -62,16 +60,12 @@ fun CourseCard( .background(Color.White, shape = RoundedCornerShape(20.dp)) .noRippleClickable { onClickItem() } ) { - // 지도 썸네일 Box( modifier = Modifier .fillMaxWidth() .aspectRatio(343f / 172f) .clip(RoundedCornerShape(10.dp)) ) { - - - // 서버 이미지 또는 기본 이미지 if (representativeImageUrl != null) { AsyncImage( model = ImageRequest.Builder(LocalContext.current) @@ -86,7 +80,6 @@ fun CourseCard( contentScale = ContentScale.Crop ) } else { - // 기본 이미지 Image( painter = painterResource(id = R.drawable.dummy_map), contentDescription = null, @@ -98,7 +91,6 @@ fun CourseCard( ) } - // 하단 그라데이션 오버레이 Box( modifier = Modifier .fillMaxWidth() @@ -123,7 +115,6 @@ fun CourseCard( .align(Alignment.BottomStart) .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) ) { - // 반려견 프로필 이미지 if (petProfileImageUrl != null) { AsyncImage( model = ImageRequest.Builder(LocalContext.current) @@ -137,7 +128,6 @@ fun CourseCard( contentScale = ContentScale.Crop ) } else { - // 기본 프로필 이미지 Box( modifier = Modifier .size(40.dp) @@ -155,6 +145,7 @@ fun CourseCard( } Spacer(modifier = Modifier.width(10.dp)) + Column { Text( text = title, @@ -170,31 +161,32 @@ fun CourseCard( ) Spacer(modifier = Modifier.width(8.dp)) Text( - text = createdAt, + text = formatDate(createdAt), style = PawKeyTheme.typography.caption12R, color = PawKeyTheme.colors.gray100 ) } } + Spacer(modifier = Modifier.weight(1f)) + Icon( imageVector = if (isLiked) ImageVector.vectorResource(id = R.drawable.ic_heart_filled) else ImageVector.vectorResource(id = R.drawable.ic_heart_default), contentDescription = "좋아요", - tint = Color.Unspecified + tint = Color.Unspecified, + modifier = Modifier.clickable { onClickLike(!isLiked) } //클릭하면 외부에 알려줌 ) } } Spacer(modifier = Modifier.height(12.dp)) - // 서버에서 받은 태그들 사용 ChipRow( tags = descriptionTags, - modifier = Modifier - .padding(start = 16.dp, end = 16.dp) + modifier = Modifier.padding(horizontal = 16.dp) ) Spacer(modifier = Modifier.height(12.dp)) @@ -202,7 +194,7 @@ fun CourseCard( HorizontalDivider( color = PawKeyTheme.colors.gray50, thickness = 1.dp, - modifier = Modifier.padding(start = 16.dp, end = 16.dp) + modifier = Modifier.padding(horizontal = 16.dp) ) } } @@ -214,13 +206,14 @@ fun CourseCardPreview() { CourseCard( postId = 1, title = "홍대 주변 좋은 산책 코스", - createdAt = "2025/07/16", - representativeImageUrl = "https://pawkey-server.com/image.jpg", + createdAt = "2025-07-16T21:27:03.54498", + isLiked = true, + onClickItem = {}, + onClickLike = {}, petName = "후추", + representativeImageUrl = "https://pawkey-server.com/image.jpg", petProfileImageUrl = "https://pawkey-server.com/profile.jpg", - descriptionTags = listOf("이륜차 거의 없음", "물그릇 비치", "쉴 곳 있음"), - isLiked = true, - onClickItem = {} + descriptionTags = listOf("이륜차 거의 없음", "물그릇 비치", "쉴 곳 있음") ) } } \ 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 index 758a862e..c10bb98e 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/List/TabListScreen.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/List/TabListScreen.kt @@ -132,6 +132,9 @@ fun TabListScreen( petProfileImageUrl = post.writer.petProfileImageUrl, descriptionTags = post.descriptionTags, isLiked = post.isLike, + onClickLike = { isLiked -> + viewModel.toggleLike(post.postId, isLiked) + }, onClickItem = { navigateToDetail() } ) } diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/List/viewmodel/TapListViewModel.kt b/app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/List/viewmodel/TapListViewModel.kt index 385434c4..d0ee6b8d 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/List/viewmodel/TapListViewModel.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/List/viewmodel/TapListViewModel.kt @@ -90,6 +90,38 @@ class TapListViewModel @Inject constructor( _state.update { it.copy(selectedSortOption = option) } } + fun toggleLike(postId: Int, isLiked: Boolean) { + viewModelScope.launch { + _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 + } + } + ) + } + +// val updatedCourseList = currentState.courseList.map { course -> +// if (course.postId == postId) { +// // ArchivedListEntity의 실제 프로퍼티명에 맞게 수정 +// // isLiked 대신 isLike 또는 liked 등의 프로퍼티를 확인하고 사용 +// course.copy(isLike = isLiked) // 또는 course.copy(liked = isLiked) +// } else { +// course +// } +// } + + currentState.copy( + postsResult = updatedPostsResult, +// courseList = updatedCourseList + ) + } + } + } fun updateMood(option: String) { _state.update { it.copy( diff --git a/app/src/main/java/com/paw/key/presentation/ui/mypage/ArchivedCourseListScreen.kt b/app/src/main/java/com/paw/key/presentation/ui/mypage/ArchivedCourseListScreen.kt index f4b16637..0929baea 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/mypage/ArchivedCourseListScreen.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/mypage/ArchivedCourseListScreen.kt @@ -67,7 +67,8 @@ fun ArchivedCourseListScreen( petProfileImageUrl = item.writer.first().petProfileImageUrl, descriptionTags = item.descriptionTags, isLiked = item.isLiked, - onClickItem = navigateNext + onClickItem = navigateNext, + onClickLike = {} ) } } @@ -80,7 +81,8 @@ fun ArchivedCourseListScreenPreview() { PawKeyTheme { SavedCourseListScreen(state = SavedListState(), navigateUp = {}, - navigateNext = {} + navigateNext = {}, + onClickLike = {} ) } } \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/mypage/SavedCourseListScreen.kt b/app/src/main/java/com/paw/key/presentation/ui/mypage/SavedCourseListScreen.kt index d92c3f2f..b5d1e8aa 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/mypage/SavedCourseListScreen.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/mypage/SavedCourseListScreen.kt @@ -29,6 +29,9 @@ fun SavedCourseRoute( state = state.value, navigateUp = navigateUp, navigateNext = navigateNext, + onClickLike = { + //viewModel.onClickLike() + }, modifier = modifier ) } @@ -38,6 +41,7 @@ fun SavedCourseListScreen( state: SavedListState, navigateUp: () -> Unit, navigateNext: () -> Unit, + onClickLike: () -> Unit, modifier: Modifier = Modifier ) { Column { @@ -65,7 +69,9 @@ fun SavedCourseListScreen( descriptionTags = item.descriptionTags, isLiked = item.isLiked, onClickItem = navigateNext, - isRecord = true + onClickLike = { + onClickLike() + } ) } } @@ -78,7 +84,8 @@ fun SavedCourseListScreenPreview() { PawKeyTheme { SavedCourseListScreen(state = SavedListState(), navigateUp = {}, - navigateNext = {} + navigateNext = {}, + onClickLike = {} ) } } \ No newline at end of file From f58f80fb3ddd5e87eeace4403ab9b9cb77e29bfc Mon Sep 17 00:00:00 2001 From: Son Juwan Date: Thu, 17 Jul 2025 01:15:05 +0900 Subject: [PATCH 3/3] =?UTF-8?q?mod/#108:=20Home=20=EC=AA=BD=20conflict=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../key/presentation/ui/home/HomeScreen.kt | 166 +++++++++++------- .../ui/home/state/HomeContract.kt | 57 +++++- .../ui/home/viewmodel/HomeViewModel.kt | 119 +++++++++++-- 3 files changed, 259 insertions(+), 83 deletions(-) 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 2bb1bcee..bf298271 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/home/HomeScreen.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/home/HomeScreen.kt @@ -15,6 +15,8 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.SideEffect @@ -42,8 +44,6 @@ import com.paw.key.presentation.ui.home.component.SettingButton import com.paw.key.presentation.ui.home.component.TrackingCard import com.paw.key.presentation.ui.home.component.WeatherCard import com.paw.key.presentation.ui.home.viewmodel.HomeViewModel -import kotlin.String - @Preview @Composable @@ -51,9 +51,11 @@ private fun HomeScreenPreview() { PawKeyTheme { HomeScreen( paddingValues = PaddingValues(), + onClickLike = { _, _ -> }, navigateUp = {}, navigateNext = {}, - navigateHomeLocationSetting = {} + navigateHomeLocationSetting = {}, + viewModel = hiltViewModel() ) } } @@ -67,14 +69,16 @@ fun HomeRoute( modifier: Modifier = Modifier, viewModel: HomeViewModel = hiltViewModel(), ) { - HomeScreen( paddingValues = paddingValues, navigateUp = navigateUp, navigateNext = navigateNext, navigateHomeLocationSetting = navigateHomeLocationSetting, modifier = modifier, - viewModel = viewModel + viewModel = viewModel, + onClickLike = { postId, isLiked -> + viewModel.toggleLike(postId = postId, isLiked = isLiked) + } ) } @@ -86,92 +90,120 @@ fun HomeScreen( navigateHomeLocationSetting: () -> Unit, modifier: Modifier = Modifier, viewModel: HomeViewModel = hiltViewModel(), + onClickLike: (postId: Int, isLiked: Boolean) -> Unit ) { val state by viewModel.state.collectAsStateWithLifecycle() val view = LocalView.current val window = (view.context as? Activity)?.window + val postsResult = state.postsResult + val posts = postsResult?.posts SideEffect { window?.let { it.statusBarColor = Color.Black.toArgb() - ViewCompat.getWindowInsetsController(view)?.let { controller -> - controller.isAppearanceLightStatusBars = false - } + ViewCompat.getWindowInsetsController(view)?.isAppearanceLightStatusBars = false } } Column( modifier = modifier .padding(paddingValues) - .background(color = PawKeyTheme.colors.white2) + .background(PawKeyTheme.colors.white2) .fillMaxSize() ) { - HomeTopBar(location = "강남구 역삼동", onLocationClick = { viewModel.toggleLocationMenu() }) - - LazyColumn ( - verticalArrangement = Arrangement.spacedBy(12.dp), - modifier = Modifier - .padding(horizontal = 16.dp) - .background(color = PawKeyTheme.colors.white2), - ) { - item{ - Spacer(modifier = Modifier.height(12.dp)) + HomeTopBar( + location = state.selectedLocation.displayLocation, + onLocationClick = { viewModel.toggleLocationMenu() } + ) - WeatherCard( - weathertitle = "35°", - weathersub1 = "35°", - weathersub2 = "21°", - rating = "0", - weatherIcon = R.drawable.ic_home_weather, - ) + // 로딩 상태 표시 + if (state.uiState.isLoading) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() } - item{ - Spacer(modifier = Modifier.height(12.dp)) + } else { + LazyColumn( + verticalArrangement = Arrangement.spacedBy(12.dp), + modifier = Modifier + .padding(horizontal = 16.dp) + .background(PawKeyTheme.colors.white2), + ) { + item { + Spacer(modifier = Modifier.height(12.dp)) + WeatherCard( + weathertitle = "35°", + weathersub1 = "35°", + weathersub2 = "21°", + rating = "0", + weatherIcon = R.drawable.ic_home_weather, + ) + } + + item { + Spacer(modifier = Modifier.height(12.dp)) + Row( + modifier = Modifier.fillMaxWidth() + ) { + DaytimeCard(daytime = "05:06", daystate = "일출") + Spacer(modifier = Modifier.weight(1F)) + TrackingCard(onClick = { navigateNext() }) + } + } - Row( - modifier = Modifier - .fillMaxWidth() - ) { - DaytimeCard( - daytime = "05:06", - daystate = "일출", + item { RowCalendar(date = "7월") } + + item { + Spacer(modifier = Modifier.height(12.dp)) + Text( + text = stringResource(R.string.ic_home_current_word), + color = PawKeyTheme.colors.black, + style = PawKeyTheme.typography.head18Sb, ) + } - Spacer(modifier = Modifier.weight(1F)) + // 에러 상태 표시 + state.uiState.error?.let { error -> + item { + Text( + text = "오류: $error", + color = Color.Red, + modifier = Modifier.padding(16.dp) + ) + } + } - TrackingCard(onClick = { navigateNext() }) + // posts가 null이 아닐 때만 items를 표시 + posts?.let { postList -> + items( + items = postList, + key = { post -> post.postId } + ) { post -> + CourseCard( + postId = post.postId, + title = post.title, + petName = post.writer.petName, + createdAt = post.createdAt, + representativeImageUrl = post.representativeImageUrl, + petProfileImageUrl = post.writer.petProfileImageUrl, + descriptionTags = post.descriptionTags, + isLiked = post.isLike, + onClickLike = { isLiked -> + onClickLike(post.postId, isLiked) + }, + onClickItem = { navigateNext() } + ) + } } - } - item{ - RowCalendar(date = "7월") - } - item{ - Spacer(modifier = Modifier.height(12.dp)) - Text( - text = stringResource(R.string.ic_home_current_word), - color = PawKeyTheme.colors.black, - style = PawKeyTheme.typography.head18Sb, - ) + item { Spacer(modifier = Modifier.height(48.dp)) } } - item{ - CourseCard( - title = "제목을 입력해주세요", - petName = "반려견 이름", - createdAt = "년도/월/일", - representativeImageUrl = "시:분", - petProfileImageUrl = "", - descriptionTags = listOf("이륜차 거의 없음", "물그릇 비치", "쉴 곳 있음"), - onClickItem = {}, - isLiked = true, - postId = 1 - ) - Spacer(modifier = Modifier.height(48.dp)) - } - } - } + + // 위치 메뉴 오버레이 if (state.isLocationMenuVisible) { Box( modifier = Modifier @@ -180,9 +212,7 @@ fun HomeScreen( .clickable( indication = null, interactionSource = remember { MutableInteractionSource() } - ) { - viewModel.toggleLocationMenu() - } + ) { viewModel.toggleLocationMenu() } ) { SettingButton( modifier = Modifier @@ -195,4 +225,4 @@ fun HomeScreen( ) } } -} +} \ No newline at end of file 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 f44db7b9..fbcda2da 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 @@ -1,19 +1,66 @@ + package com.paw.key.presentation.ui.home.state import androidx.compose.runtime.Immutable - +import com.paw.key.domain.model.entity.archivedlist.ArchivedListEntity +import com.paw.key.domain.model.entity.list.ListEntity class HomeContract { + @Immutable data class HomeState( val isLocationMenuVisible: Boolean = false, - val isVisible: Boolean = false, - val selectedLocation: String = "", + val postsResult: ListEntity? = null, + val selectedLocation: LocationInfo = LocationInfo(), + val courseList: List = emptyList(), + val uiState: HomeUiState = HomeUiState(), + val selectedGuId: Int = 0, + val selectedDongId: Int = 0, + val selectedGu: String = "", + val selectedDong: String = "" + ) + @Immutable + data class LocationInfo( val selectedGuId: Int = 0, val selectedDongId: Int = 0, val selectedGu: String = "", - val selectedDong: String = "", + val selectedDong: String = "" + ) { + val displayLocation: String + get() = if (selectedGu.isNotEmpty() && selectedDong.isNotEmpty()) { + "$selectedGu $selectedDong" + } else if (selectedGu.isNotEmpty()) { + selectedGu + } else { + "위치를 선택해주세요" + } + val isLocationSelected: Boolean + get() = selectedGuId != 0 && selectedDongId != 0 + } + + @Immutable + data class HomeUiState( + val isVisible: Boolean = false, + val isLoading: Boolean = false, + val error: String? = null ) -} + + // 액션들을 정의하는 sealed class + sealed class HomeAction { + object ToggleLocationMenu : HomeAction() + data class SelectGu(val guName: String, val guId: Int) : HomeAction() + data class SelectDong(val dongName: String, val dongId: Int) : HomeAction() + data class ToggleLike(val postId: Int, val isLiked: Boolean) : HomeAction() + object RefreshPosts : HomeAction() + object ClearError : HomeAction() + } + + // 사이드 이펙트를 정의하는 sealed class + sealed class HomeSideEffect { + object NavigateToLocationSetting : HomeSideEffect() + data class ShowError(val message: String) : HomeSideEffect() + data class ShowToast(val message: String) : HomeSideEffect() + } +} \ No newline at end of file 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 418019a0..189c2df7 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 @@ -1,16 +1,13 @@ package com.paw.key.presentation.ui.home.viewmodel import DistrictDto -import android.content.Context 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.home.HomeRegionRepository import com.paw.key.domain.repository.onboarding.OnboardingRegionRepository import com.paw.key.presentation.ui.home.state.HomeContract import dagger.hilt.android.lifecycle.HiltViewModel -import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -23,6 +20,7 @@ import javax.inject.Inject class HomeViewModel @Inject constructor( private val regionRepository: OnboardingRegionRepository, ) : ViewModel() { + private val _state = MutableStateFlow(HomeContract.HomeState()) val state: StateFlow = _state.asStateFlow() @@ -46,11 +44,13 @@ class HomeViewModel @Inject constructor( fun onGuSelected(guName: String, guId: Int) { _state.update { currentState -> currentState.copy( - selectedGu = guName, - selectedGuId = guId, - // 구를 새로 선택하면 기존 동 선택 초기화 - selectedDong = "", - selectedDongId = 0, + selectedLocation = currentState.selectedLocation.copy( + selectedGu = guName, + selectedGuId = guId, + // 구를 새로 선택하면 기존 동 선택 초기화 + selectedDong = "", + selectedDongId = 0 + ), // 구 선택 후 메뉴 닫기 isLocationMenuVisible = false ) @@ -60,24 +60,123 @@ class HomeViewModel @Inject constructor( fun onDongSelected(dongName: String, dongId: Int) { _state.update { currentState -> currentState.copy( - selectedDong = dongName, - selectedDongId = dongId + selectedLocation = currentState.selectedLocation.copy( + selectedDong = dongName, + selectedDongId = dongId + ) ) } } private fun fetchRegion() { + _state.update { it.copy(uiState = it.uiState.copy(isLoading = true)) } + viewModelScope.launch { try { val result = regionRepository.getOnboardingRegion(userId.first()) result.onSuccess { response -> Log.d("HomeViewModel", "Region loaded: ${response.data.districtDtos.size}") _regionList.value = response.data.districtDtos + _state.update { + it.copy( + uiState = it.uiState.copy( + isLoading = false, + error = null + ) + ) + } }.onFailure { exception -> Log.e("HomeViewModel", "구/동 가져오기 실패: ${exception.message}") + _state.update { + it.copy( + uiState = it.uiState.copy( + isLoading = false, + error = exception.message + ) + ) + } } } catch (e: Exception) { Log.e("HomeViewModel", "fetchRegion Exception: ${e.message}") + _state.update { + it.copy( + uiState = it.uiState.copy( + isLoading = false, + error = e.message + ) + ) + } + } + } + } + + fun toggleLike(postId: Int, isLiked: Boolean) { + viewModelScope.launch { + _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 + } + } + ) + } + +// val updatedCourseList = currentState.courseList.map { course -> +// if (course.postId == postId) { +// // ArchivedListEntity의 실제 프로퍼티명에 맞게 수정 +// // isLiked 대신 isLike 또는 liked 등의 프로퍼티를 확인하고 사용 +// course.copy(isLike = isLiked) // 또는 course.copy(liked = isLiked) +// } else { +// course +// } +// } + + currentState.copy( + postsResult = updatedPostsResult, +// courseList = updatedCourseList + ) + } + } + } + + fun clearError() { + _state.update { + it.copy( + uiState = it.uiState.copy(error = null) + ) + } + } + + fun refreshPosts() { + _state.update { it.copy(uiState = it.uiState.copy(isLoading = true)) } + + viewModelScope.launch { + try { + // 여기에 실제 포스트 데이터를 가져오는 로직 추가 + // val result = postsRepository.getPosts(...) + + _state.update { + it.copy( + uiState = it.uiState.copy( + isLoading = false, + error = null + ) + ) + } + } catch (e: Exception) { + Log.e("HomeViewModel", "refreshPosts Exception: ${e.message}") + _state.update { + it.copy( + uiState = it.uiState.copy( + isLoading = false, + error = e.message + ) + ) + } } } }