From 1d56f67fa28264d7d7734d0610c41750d4c8451a Mon Sep 17 00:00:00 2001 From: Son Juwan Date: Thu, 17 Jul 2025 20:50:41 +0900 Subject: [PATCH 1/3] =?UTF-8?q?mod/#118:=20SignUpDogScreen=20Ux=20?= =?UTF-8?q?=EA=B8=B0=EB=B0=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/ui/signup/SignUpDogScreen.kt | 135 ++++++++++++++---- 1 file changed, 109 insertions(+), 26 deletions(-) diff --git a/app/src/main/java/com/paw/key/presentation/ui/signup/SignUpDogScreen.kt b/app/src/main/java/com/paw/key/presentation/ui/signup/SignUpDogScreen.kt index 4d8843de..dc71f9e3 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/signup/SignUpDogScreen.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/signup/SignUpDogScreen.kt @@ -21,25 +21,35 @@ 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.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.Icon import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState 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.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.input.ImeAction +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 @@ -58,7 +68,7 @@ import com.paw.key.presentation.ui.signup.viewmodel.SignUpViewModel @Composable private fun PreviewSignUpDogScreen() { PawKeyTheme { - + // Preview content } } @@ -96,6 +106,13 @@ fun SignUpDogScreen( viewModel: SignUpViewModel ) { val state by viewModel.state.collectAsState() + val keyboardController = LocalSoftwareKeyboardController.current + val focusManager = LocalFocusManager.current + + val dogNameFocusRequester = remember { FocusRequester() } + val dogBreedFocusRequester = remember { FocusRequester() } + val dogAgeFocusRequester = remember { FocusRequester() } + val animatedProgress by animateFloatAsState( targetValue = progress, animationSpec = tween( @@ -145,9 +162,40 @@ fun SignUpDogScreen( } } - Box(modifier = modifier.fillMaxSize()) { - Column(modifier = Modifier.fillMaxSize()) { + val isFormValid = remember(state.dogName, state.dogGender, state.dogBreed, state.ageKnown, state.dogAge) { + state.dogName.isNotEmpty() && + state.dogGender != SignUpContract.DogGender.UNKNOWN && + state.dogBreed.isNotEmpty() && + isAgeValid(state.ageKnown, state.dogAge) + } + + val hideKeyboardAndClearFocus = { + keyboardController?.hide() + focusManager.clearFocus() + } + + val proceedToNext = { + if (isFormValid) { + hideKeyboardAndClearFocus() + navigateNext() + } + } + + val requestFocusSafely = { focusRequester: FocusRequester -> + try { + focusRequester.requestFocus() + } catch (e: Exception) { + + } + } + Box( + modifier = modifier + .fillMaxSize() + .imePadding() + ) { + Column(modifier = Modifier.fillMaxSize()) { + // 헤더 Column( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, @@ -156,8 +204,7 @@ fun SignUpDogScreen( text = stringResource(id = R.string.ic_onboarding_signup), color = PawKeyTheme.colors.black, style = PawKeyTheme.typography.body16Sb, - modifier = Modifier - .padding(top = 16.dp), + modifier = Modifier.padding(top = 16.dp), ) Spacer(modifier = Modifier.height(16.dp)) @@ -180,15 +227,15 @@ fun SignUpDogScreen( contentPadding = PaddingValues(16.dp), modifier = Modifier.fillMaxSize() ) { - item{ + item { Text( text = stringResource(id = R.string.ic_onboarding_signup_subtitle_step3), color = PawKeyTheme.colors.black, style = PawKeyTheme.typography.head22Sb, - modifier = Modifier - .padding(top = 20.dp) + modifier = Modifier.padding(top = 20.dp) ) } + item { DogProfileImage( dogImage = state.dogImage, @@ -199,16 +246,20 @@ fun SignUpDogScreen( item { DogNameField( value = state.dogName, - onValueChange = viewModel::onDogNameChanged + onValueChange = viewModel::onDogNameChanged, + focusRequester = dogNameFocusRequester, + onNext = { requestFocusSafely(dogBreedFocusRequester) } ) } item { - Column( - ) { + Column { DogGenderSection( selectedGender = state.dogGender, - onGenderSelected = viewModel::selectDogGender + onGenderSelected = { gender -> + viewModel.selectDogGender(gender) + requestFocusSafely(dogBreedFocusRequester) + } ) Spacer(modifier = Modifier.height(10.dp)) NeuteringCheckbox( @@ -221,7 +272,9 @@ fun SignUpDogScreen( item { DogBreedField( value = state.dogBreed, - onValueChange = viewModel::onDogBreedChanged + onValueChange = viewModel::onDogBreedChanged, + focusRequester = dogBreedFocusRequester, + onNext = { focusManager.clearFocus() } ) } @@ -229,8 +282,15 @@ fun SignUpDogScreen( DogAgeSection( ageKnown = state.ageKnown, dogAge = state.dogAge, - onAgeKnownSelected = viewModel::selectAgeKnown, - onDogAgeChanged = viewModel::onDogAgeChanged + onAgeKnownSelected = { ageKnown -> + viewModel.selectAgeKnown(ageKnown) + if (ageKnown == SignUpContract.AgeKnown.KNOWN) { + requestFocusSafely(dogAgeFocusRequester) + } + }, + onDogAgeChanged = viewModel::onDogAgeChanged, + focusRequester = dogAgeFocusRequester, + onDone = proceedToNext ) } @@ -238,21 +298,15 @@ fun SignUpDogScreen( } } - val isFormValid = state.dogName.isNotEmpty() && - state.dogGender != SignUpContract.DogGender.UNKNOWN && - state.dogBreed.isNotEmpty() && - isAgeValid(state.ageKnown, state.dogAge) - PawkeyButton( text = "다음으로", enabled = isFormValid, - onClick = navigateNext, + onClick = proceedToNext, modifier = Modifier .align(Alignment.BottomCenter) .fillMaxWidth() .padding(horizontal = 16.dp, vertical = 46.dp) ) - } } @@ -306,6 +360,8 @@ private fun DogProfileImage( private fun DogNameField( value: String, onValueChange: (String) -> Unit, + focusRequester: FocusRequester, + onNext: () -> Unit ) { FormField( label = stringResource(id = R.string.ic_onboarding_signup_dog_name), @@ -313,7 +369,15 @@ private fun DogNameField( SignUpTextField( value = value, onValueChange = onValueChange, - placeholder = "강아지 이름을 입력해주세요" + placeholder = "강아지 이름을 입력해주세요", + modifier = Modifier.focusRequester(focusRequester), + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Next, + keyboardType = KeyboardType.Text + ), + keyboardActions = KeyboardActions( + onNext = { onNext() } + ) ) } ) @@ -373,7 +437,6 @@ private fun NeuteringCheckbox( PawKeyTheme.colors.black else PawKeyTheme.colors.gray300, style = PawKeyTheme.typography.body14Sb - ) } } @@ -382,6 +445,8 @@ private fun NeuteringCheckbox( private fun DogBreedField( value: String, onValueChange: (String) -> Unit, + focusRequester: FocusRequester, + onNext: () -> Unit ) { FormField( label = stringResource(id = R.string.ic_onboarding_signup_dog_breed), @@ -389,7 +454,15 @@ private fun DogBreedField( SignUpTextField( value = value, onValueChange = onValueChange, - placeholder = "견종을 입력해주세요" + placeholder = "견종을 입력해주세요", + modifier = Modifier.focusRequester(focusRequester), + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Next, + keyboardType = KeyboardType.Text + ), + keyboardActions = KeyboardActions( + onNext = { onNext() } + ) ) } ) @@ -401,6 +474,8 @@ private fun DogAgeSection( dogAge: String, onAgeKnownSelected: (SignUpContract.AgeKnown) -> Unit, onDogAgeChanged: (String) -> Unit, + focusRequester: FocusRequester, + onDone: () -> Unit ) { FormField( label = stringResource(id = R.string.ic_onboarding_signup_age), @@ -437,7 +512,15 @@ private fun DogAgeSection( SignUpTextField( value = dogAge, onValueChange = onDogAgeChanged, - placeholder = "나이를 입력해주세요" + placeholder = "나이를 입력해주세요", + modifier = Modifier.focusRequester(focusRequester), + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Done, + keyboardType = KeyboardType.Number + ), + keyboardActions = KeyboardActions( + onDone = { onDone() } + ) ) } } From 7fcd5ad22b0d8f433c945cfbbcb8100a40f3e569 Mon Sep 17 00:00:00 2001 From: Son Juwan Date: Thu, 17 Jul 2025 20:51:00 +0900 Subject: [PATCH 2/3] =?UTF-8?q?mod/#118:=20=EC=A2=8B=EC=95=84=EC=9A=94=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5,=20=EC=B7=A8=EC=86=8C=20api?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data/remote/datasource/LikeDataSource.kt | 8 +- .../data/repositoryimpl/LikeRepositoryImpl.kt | 82 ++++++++++++++++--- .../com/paw/key/data/service/LikeService.kt | 4 +- 3 files changed, 76 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/com/paw/key/data/remote/datasource/LikeDataSource.kt b/app/src/main/java/com/paw/key/data/remote/datasource/LikeDataSource.kt index ea298d91..6df2da2d 100644 --- a/app/src/main/java/com/paw/key/data/remote/datasource/LikeDataSource.kt +++ b/app/src/main/java/com/paw/key/data/remote/datasource/LikeDataSource.kt @@ -6,9 +6,9 @@ import javax.inject.Inject class LikeDataSource @Inject constructor( private val likeService: LikeService ) { - suspend fun likeCourse(userId: Int, courseId: Int) = - likeService.likeCourse(userId, courseId) + suspend fun likeCourse(userId: Int, postId: Int) = + likeService.likeCourse(userId, postId) - suspend fun unlikeCourse(userId: Int, courseId: Int) = - likeService.unlikeCourse(userId, courseId) + suspend fun unlikeCourse(userId: Int, postId: Int) = + likeService.unlikeCourse(userId, postId) } \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/data/repositoryimpl/LikeRepositoryImpl.kt b/app/src/main/java/com/paw/key/data/repositoryimpl/LikeRepositoryImpl.kt index b124c658..0ecebdeb 100644 --- a/app/src/main/java/com/paw/key/data/repositoryimpl/LikeRepositoryImpl.kt +++ b/app/src/main/java/com/paw/key/data/repositoryimpl/LikeRepositoryImpl.kt @@ -2,27 +2,85 @@ package com.paw.key.data.repositoryimpl import com.paw.key.data.remote.datasource.LikeDataSource import com.paw.key.domain.repository.LikeRepository +import retrofit2.HttpException +import java.net.HttpURLConnection import javax.inject.Inject class LikeRepositoryImpl @Inject constructor( private val dataSource: LikeDataSource ) : LikeRepository { - override suspend fun likeCourse(userId: Int, courseId: Int): Result { - return try { - dataSource.likeCourse(userId, courseId) - Result.success(Unit) - } catch (e: Exception) { - Result.failure(e) + override suspend fun likeCourse(userId: Int, postId: Int): Result { + return runCatching { + val response = dataSource.likeCourse(userId = userId, postId = postId) + if (response.code == "S000") { + Unit + } else { + throw Exception(response.message ?: "좋아요 처리에 실패했습니다") + } + }.recoverCatching { exception -> + when (exception) { + is HttpException -> { + when (exception.code()) { + HttpURLConnection.HTTP_BAD_REQUEST -> + throw Exception("본인이 작성한 게시글에는 좋아요를 할 수 없습니다") + HttpURLConnection.HTTP_CONFLICT -> + throw Exception("이미 좋아요를 누른 게시글입니다") + HttpURLConnection.HTTP_INTERNAL_ERROR -> + throw Exception("서버 오류가 발생했습니다. 잠시 후 다시 시도해주세요") + else -> + throw Exception("좋아요 처리에 실패했습니다 (${exception.code()})") + } + } + else -> { + if (isJsonParsingError(exception)) { + Unit + } else { + throw exception + } + } + } } } - override suspend fun unlikeCourse(userId: Int, courseId: Int): Result { - return try { - dataSource.unlikeCourse(userId, courseId) - Result.success(Unit) - } catch (e: Exception) { - Result.failure(e) + override suspend fun unlikeCourse(userId: Int, postId: Int): Result { + return runCatching { + val response = dataSource.unlikeCourse(userId = userId, postId = postId) + if (response.code == "S000") { + Unit + } else { + throw Exception(response.message ?: "좋아요 취소 처리에 실패했습니다") + } + }.recoverCatching { exception -> + when (exception) { + is HttpException -> { + when (exception.code()) { + HttpURLConnection.HTTP_BAD_REQUEST -> + throw Exception("잘못된 요청입니다") + HttpURLConnection.HTTP_CONFLICT -> + throw Exception("이미 처리된 요청입니다") + HttpURLConnection.HTTP_INTERNAL_ERROR -> + throw Exception("서버 오류가 발생했습니다. 잠시 후 다시 시도해주세요") + else -> + throw Exception("좋아요 취소 처리에 실패했습니다 (${exception.code()})") + } + } + else -> { + if (isJsonParsingError(exception)) { + Unit + } else { + throw exception + } + } + } } } + + private fun isJsonParsingError(exception: Throwable): Boolean { + val message = exception.message ?: return false + return message.contains("Unexpected JSON token") || + message.contains("Expected start of the object") || + message.contains("JSON input") || + message.contains("SerializationException") + } } \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/data/service/LikeService.kt b/app/src/main/java/com/paw/key/data/service/LikeService.kt index 952a0489..7ce80597 100644 --- a/app/src/main/java/com/paw/key/data/service/LikeService.kt +++ b/app/src/main/java/com/paw/key/data/service/LikeService.kt @@ -12,11 +12,11 @@ interface LikeService { suspend fun likeCourse( @Header("X-USER-ID") userId: Int, @Path("postId") postId: Int - ): BaseResponse + ): BaseResponse @DELETE("/api/v1/likes/{postId}") suspend fun unlikeCourse( @Header("X-USER-ID") userId: Int, @Path("postId") postId: Int - ): BaseResponse + ): BaseResponse } \ No newline at end of file From 17f55abbb9c6796363b5ccd968f0af4c6bc2aa66 Mon Sep 17 00:00:00 2001 From: Son Juwan Date: Thu, 17 Jul 2025 20:51:12 +0900 Subject: [PATCH 3/3] =?UTF-8?q?mod/#118:=20=EC=A2=8B=EC=95=84=EC=9A=94=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5,=20=EC=B7=A8=EC=86=8C=20api=20List=EC=97=90?= =?UTF-8?q?=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../entire/tab/map/List/TabListScreen.kt | 10 +- .../map/List/viewmodel/TapListViewModel.kt | 385 ++++++++++-------- 2 files changed, 228 insertions(+), 167 deletions(-) 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 74284162..e65daf4e 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 @@ -147,7 +147,10 @@ fun TabListScreen( listState.postsResult?.let { postsResult -> val posts = postsResult.posts if (posts.isNotEmpty()) { - items(posts) { post -> + items( + items = posts, + key = { post -> post.postId } + ) { post -> CourseCard( title = post.title, petName = post.writer.petName, @@ -158,11 +161,10 @@ fun TabListScreen( createdAt = post.createdAt, isLiked = post.isLike, onClickItem = { - //showBottomSheet = true navigateToDetail(post.postId, post.routeId) }, - onClickLike = { - //viewModel.toggleLike(postId = post.postId) + onClickLike = { newLikeState -> + viewModel.toggleLike(post.postId, newLikeState) } ) } 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 2b9ff6a0..afe26214 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 @@ -11,8 +11,11 @@ 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 @@ -23,12 +26,22 @@ import javax.inject.Inject class TapListViewModel @Inject constructor( private val filterOptionRepository: FilterOptionRepository, private val postsListRepository: PostsListRepository, - private val likeRepository: LikeRepository + 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 { @@ -106,6 +119,7 @@ class TapListViewModel @Inject constructor( ) } } + "21~40분" -> { _state.update { it.copy( @@ -114,6 +128,7 @@ class TapListViewModel @Inject constructor( ) } } + "41~60분" -> { _state.update { it.copy( @@ -122,6 +137,7 @@ class TapListViewModel @Inject constructor( ) } } + "1시간 이상" -> { _state.update { it.copy( @@ -130,6 +146,7 @@ class TapListViewModel @Inject constructor( ) } } + else -> { _state.update { it.copy( @@ -147,214 +164,256 @@ class TapListViewModel @Inject constructor( } } - fun toggleLike(postId: Int, isLiked: Boolean) { - viewModelScope.launch { - if (isLiked) { - likeRepository.unlikeCourse(userId = userId.first(), postId = postId) - } else { - likeRepository.likeCourse(userId = userId.first(), postId = postId) - } - } - } - fun updateMood(option: String) { - _state.update { - it.copy( - selectedMood = if (it.selectedMood == option) "" else option - ) + fun toggleLike(postId: Int, newLikeState: Boolean) { + if (_likingPosts.value.contains(postId)) { + return } - } - fun updateDogFriend(option: String) { - _state.update { - it.copy( - selectedDogFriend = if (it.selectedDogFriend == option) "" else option - ) - } - } + viewModelScope.launch { + try { + _likingPosts.update { it + postId } - 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) - } - } + val result = if (newLikeState) { + likeRepository.likeCourse(userId = userId.first(), postId = postId) + } else { + likeRepository.unlikeCourse(userId = userId.first(), postId = postId) + } - fun updateConvenience(option: String) { - _state.update { currentState -> - val newConvenience = if (currentState.selectedConvenience.contains(option)) { - currentState.selectedConvenience.filter { it != option } - } else { - currentState.selectedConvenience + option + 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 } } - currentState.copy(selectedConvenience = newConvenience) } } - fun updateEnvironment(option: String) { + private fun updateLocalLikeState(postId: Int, isLiked: Boolean) { _state.update { currentState -> - val newEnvironment = if (currentState.selectedEnvironment.contains(option)) { - currentState.selectedEnvironment.filter { it != option } - } else { - currentState.selectedEnvironment + option + 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(selectedEnvironment = newEnvironment) + currentState.copy(postsResult = updatedPostsResult) } } - 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 updateMood(option: String) { + _state.update { + it.copy( + selectedMood = if (it.selectedMood == option) "" else option + ) } +} - fun toggleSafetyExpanded() { - _state.update { it.copy(isSafetyExpanded = !it.isSafetyExpanded) } +fun updateDogFriend(option: String) { + _state.update { + it.copy( + selectedDogFriend = if (it.selectedDogFriend == option) "" else option + ) } - - fun toggleConvenienceExpanded() { - _state.update { it.copy(isConvenienceExpanded = !it.isConvenienceExpanded) } +} + +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 toggleEnvironmentExpanded() { - _state.update { it.copy(isEnvironmentExpanded = !it.isEnvironmentExpanded) } +} + +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 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 - ) +} + +fun updateEnvironment(option: String) { + _state.update { currentState -> + val newEnvironment = if (currentState.selectedEnvironment.contains(option)) { + currentState.selectedEnvironment.filter { it != option } + } else { + currentState.selectedEnvironment + option } - loadInitialPosts() + 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 +fun applyOptions() { + val currentState = _state.value - viewModelScope.launch { - _state.update { it.copy(isLoading = true) } + viewModelScope.launch { + _state.update { it.copy(isLoading = true) } - try { - val selectedOptions = buildSelectedOptionsList(currentState) + try { + val selectedOptions = buildSelectedOptionsList(currentState) - val request = PostsListRequestDto( - durationStart = state.value.selectedSortTimeStart, - durationEnd = state.value.selectedSortTimeEnd, - selectedOptions = selectedOptions.ifEmpty { null } - ) + val request = PostsListRequestDto( + durationStart = state.value.selectedSortTimeStart, + durationEnd = state.value.selectedSortTimeEnd, + selectedOptions = selectedOptions.ifEmpty { null } + ) - 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}") + postsListRepository.postList( + userId = userId.first(), + request = request + ).onSuccess { listEntity -> + _state.update { + it.copy( + isLoading = false, + postsResult = listEntity + ) } - } catch (e: Exception) { + println("필터링된 게시물 로드 성공: ${listEntity.posts.size}개") + }.onFailure { exception -> _state.update { it.copy(isLoading = false) } - e.printStackTrace() + 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() +private fun buildSelectedOptionsList(state: TapListContract.TapListState): List { + val selectedOptions = mutableListOf() + val filterOptions = state.filterOptions ?: return emptyList() - filterOptions.categoryList?.forEach { category -> - val selectedOptionIds = mutableListOf() + 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) } - } + when (category.categoryName) { + "분위기" -> { + if (state.selectedMood.isNotEmpty()) { + category.categoryOptions?.find { it.categoryOptionText == state.selectedMood } + ?.let { selectedOptionIds.add(it.categoryOptionId) } } - "안전" -> { - state.selectedSafety.forEach { selectedSafety -> - category.categoryOptions?.find { it.categoryOptionText == selectedSafety } - ?.let { selectedOptionIds.add(it.categoryOptionId) } - } + } + + "강아지 친구" -> { + if (state.selectedDogFriend.isNotEmpty()) { + category.categoryOptions?.find { it.categoryOptionText == state.selectedDogFriend } + ?.let { selectedOptionIds.add(it.categoryOptionId) } } - "편의성" -> { - state.selectedConvenience.forEach { selectedConvenience -> - category.categoryOptions?.find { it.categoryOptionText == selectedConvenience } - ?.let { selectedOptionIds.add(it.categoryOptionId) } - } + } + + "안전" -> { + state.selectedSafety.forEach { selectedSafety -> + category.categoryOptions?.find { it.categoryOptionText == selectedSafety } + ?.let { selectedOptionIds.add(it.categoryOptionId) } } - "환경" -> { - state.selectedEnvironment.forEach { selectedEnvironment -> - category.categoryOptions?.find { it.categoryOptionText == selectedEnvironment } - ?.let { selectedOptionIds.add(it.categoryOptionId) } - } + } + + "편의성" -> { + state.selectedConvenience.forEach { selectedConvenience -> + category.categoryOptions?.find { it.categoryOptionText == selectedConvenience } + ?.let { selectedOptionIds.add(it.categoryOptionId) } } } - if (selectedOptionIds.isNotEmpty()) { - selectedOptions.add( - TraitList( - categoryId = category.categoryId, - optionIds = selectedOptionIds - ) - ) + "환경" -> { + state.selectedEnvironment.forEach { selectedEnvironment -> + category.categoryOptions?.find { it.categoryOptionText == selectedEnvironment } + ?.let { selectedOptionIds.add(it.categoryOptionId) } + } } } - 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() + if (selectedOptionIds.isNotEmpty()) { + selectedOptions.add( + TraitList( + categoryId = category.categoryId, + optionIds = selectedOptionIds + ) + ) + } } - fun isFilterApplied(): Boolean { - return isAllOptionsSelected() - } + 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