diff --git a/core/src/main/java/com/sopt/core/designsystem/component/chip/AvailableUserChips.kt b/core/src/main/java/com/sopt/core/designsystem/component/chip/AvailableUserChips.kt index bbc36c30..2c1b1509 100644 --- a/core/src/main/java/com/sopt/core/designsystem/component/chip/AvailableUserChips.kt +++ b/core/src/main/java/com/sopt/core/designsystem/component/chip/AvailableUserChips.kt @@ -12,7 +12,7 @@ fun AvailableUserChips( myIdentity: IdentityEntity ) { members.forEachIndexed { index, member -> - val isMeAvailable = index == 0 && myIdentity.availability == "available" && myIdentity.name == member + val isMeAvailable = index == 0 && myIdentity.availability == AVAILABLE && myIdentity.name == member NoostakUserChip( text = if (isMeAvailable) stringResource(id = R.string.user_chip_me) else member, textColor = NoostakTheme.colors.black, @@ -21,3 +21,5 @@ fun AvailableUserChips( ) } } + +private const val AVAILABLE = "약속 가능" diff --git a/core/src/main/java/com/sopt/core/designsystem/component/chip/UnavailableUserChips.kt b/core/src/main/java/com/sopt/core/designsystem/component/chip/UnavailableUserChips.kt index 7c8e1f43..72eee296 100644 --- a/core/src/main/java/com/sopt/core/designsystem/component/chip/UnavailableUserChips.kt +++ b/core/src/main/java/com/sopt/core/designsystem/component/chip/UnavailableUserChips.kt @@ -12,7 +12,7 @@ fun UnavailableUserChips( myIdentity: IdentityEntity ) { members.forEachIndexed { index, member -> - val isMeUnavailable = index == 0 && myIdentity.availability == "unavailable" && myIdentity.name == member + val isMeUnavailable = index == 0 && myIdentity.availability == UNAVAILABLE && myIdentity.name == member NoostakUserChip( text = if (isMeUnavailable) stringResource(R.string.user_chip_me) else member, textColor = NoostakTheme.colors.gray800, @@ -21,3 +21,5 @@ fun UnavailableUserChips( ) } } + +private const val UNAVAILABLE = "약속 불가능" diff --git a/data/src/main/java/com/sopt/data/datasource/AppointmentConfirmDataSource.kt b/data/src/main/java/com/sopt/data/datasource/AppointmentConfirmDataSource.kt index 6ab5bb0c..62bdf6a8 100644 --- a/data/src/main/java/com/sopt/data/datasource/AppointmentConfirmDataSource.kt +++ b/data/src/main/java/com/sopt/data/datasource/AppointmentConfirmDataSource.kt @@ -2,7 +2,7 @@ package com.sopt.data.datasource import com.sopt.data.dto.BaseResponse import com.sopt.data.dto.request.RequestPostTimeTableDto -import com.sopt.data.dto.response.ResponseGetConfirmedDto +import com.sopt.data.dto.response.ResponseGetOptionDetailDto import com.sopt.data.dto.response.ResponseGetOptionsDto import com.sopt.data.dto.response.ResponseGetTimeTableDto import com.sopt.data.dto.response.ResponseLikesDto @@ -20,9 +20,9 @@ interface AppointmentConfirmDataSource { suspend fun getOptions(appointmentId: Long): BaseResponse - suspend fun getConfirmed(appointmentOptionId: Long): BaseResponse + suspend fun getOptionDetail(appointmentOptionId: Long): BaseResponse - suspend fun postConfirmed(appointmentOptionId: Long): BaseResponse + suspend fun postOptionConfirm(appointmentOptionId: Long): BaseResponse suspend fun getTimeTable(appointmentId: Long): BaseResponse diff --git a/data/src/main/java/com/sopt/data/datasourceimpl/AppointmentConfirmDataSourceImpl.kt b/data/src/main/java/com/sopt/data/datasourceimpl/AppointmentConfirmDataSourceImpl.kt index b3dc4eeb..a525b5a3 100644 --- a/data/src/main/java/com/sopt/data/datasourceimpl/AppointmentConfirmDataSourceImpl.kt +++ b/data/src/main/java/com/sopt/data/datasourceimpl/AppointmentConfirmDataSourceImpl.kt @@ -3,7 +3,7 @@ package com.sopt.data.datasourceimpl import com.sopt.data.datasource.AppointmentConfirmDataSource import com.sopt.data.dto.BaseResponse import com.sopt.data.dto.request.RequestPostTimeTableDto -import com.sopt.data.dto.response.ResponseGetConfirmedDto +import com.sopt.data.dto.response.ResponseGetOptionDetailDto import com.sopt.data.dto.response.ResponseGetOptionsDto import com.sopt.data.dto.response.ResponseGetTimeTableDto import com.sopt.data.dto.response.ResponseLikesDto @@ -33,12 +33,12 @@ class AppointmentConfirmDataSourceImpl @Inject constructor( return appointmentConfirmApiService.getOptions(appointmentId) } - override suspend fun getConfirmed(appointmentOptionId: Long): BaseResponse { - return appointmentConfirmApiService.getConfirmed(appointmentOptionId) + override suspend fun getOptionDetail(appointmentOptionId: Long): BaseResponse { + return appointmentConfirmApiService.getOptionDetail(appointmentOptionId) } - override suspend fun postConfirmed(appointmentOptionId: Long): BaseResponse { - return appointmentConfirmApiService.postConfirmed(appointmentOptionId) + override suspend fun postOptionConfirm(appointmentOptionId: Long): BaseResponse { + return appointmentConfirmApiService.postOptionConfirm(appointmentOptionId) } override suspend fun getTimeTable(appointmentId: Long): BaseResponse { diff --git a/data/src/main/java/com/sopt/data/dto/request/RequestPostTimeTableDto.kt b/data/src/main/java/com/sopt/data/dto/request/RequestPostTimeTableDto.kt index b02d6cee..98942a23 100644 --- a/data/src/main/java/com/sopt/data/dto/request/RequestPostTimeTableDto.kt +++ b/data/src/main/java/com/sopt/data/dto/request/RequestPostTimeTableDto.kt @@ -6,5 +6,5 @@ import kotlinx.serialization.Serializable @Serializable data class RequestPostTimeTableDto( - @SerialName("availableTimes") val availableTimes: List + @SerialName("appointmentMemberAvailableTimes") val availableTimes: List ) diff --git a/data/src/main/java/com/sopt/data/dto/response/ResponseGetGroupConfirmedDetailDto.kt b/data/src/main/java/com/sopt/data/dto/response/ResponseGetGroupConfirmedDetailDto.kt index a9b30b47..531c3705 100644 --- a/data/src/main/java/com/sopt/data/dto/response/ResponseGetGroupConfirmedDetailDto.kt +++ b/data/src/main/java/com/sopt/data/dto/response/ResponseGetGroupConfirmedDetailDto.kt @@ -26,5 +26,3 @@ data class FriendsDto( @SerialName("count") val count: Int, @SerialName("names") val names: List ) - - diff --git a/data/src/main/java/com/sopt/data/dto/response/ResponseGetGroupOngoingDto.kt b/data/src/main/java/com/sopt/data/dto/response/ResponseGetGroupOngoingDto.kt index 1476103e..fa1731b9 100644 --- a/data/src/main/java/com/sopt/data/dto/response/ResponseGetGroupOngoingDto.kt +++ b/data/src/main/java/com/sopt/data/dto/response/ResponseGetGroupOngoingDto.kt @@ -24,4 +24,4 @@ data class OngoingAppointmentDto( @SerialName("appointmentName") val appointmentName: String, @SerialName("availableGroupMemberCount") val availableGroupMemberCount: Long, @SerialName("appointmentTime") val appointmentTime: BaseTimeDto -) \ No newline at end of file +) diff --git a/data/src/main/java/com/sopt/data/dto/response/ResponseGetConfirmedDto.kt b/data/src/main/java/com/sopt/data/dto/response/ResponseGetOptionDetailDto.kt similarity index 89% rename from data/src/main/java/com/sopt/data/dto/response/ResponseGetConfirmedDto.kt rename to data/src/main/java/com/sopt/data/dto/response/ResponseGetOptionDetailDto.kt index 8a7dd7f9..b2fdb729 100644 --- a/data/src/main/java/com/sopt/data/dto/response/ResponseGetConfirmedDto.kt +++ b/data/src/main/java/com/sopt/data/dto/response/ResponseGetOptionDetailDto.kt @@ -7,8 +7,7 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable -data class ResponseGetConfirmedDto( - @SerialName("isHost") val isHost: Boolean, +data class ResponseGetOptionDetailDto( @SerialName("appointmentTime") val appointmentTime: BaseTimeDto, @SerialName("category") val category: String, @SerialName("appointmentName") val appointmentName: String, diff --git a/data/src/main/java/com/sopt/data/dto/response/base/BaseFriendsDto.kt b/data/src/main/java/com/sopt/data/dto/response/base/BaseFriendsDto.kt index b15177f8..f7330c7e 100644 --- a/data/src/main/java/com/sopt/data/dto/response/base/BaseFriendsDto.kt +++ b/data/src/main/java/com/sopt/data/dto/response/base/BaseFriendsDto.kt @@ -6,5 +6,5 @@ import kotlinx.serialization.Serializable @Serializable data class BaseFriendsDto( @SerialName("count") val count: Int, - @SerialName("names") val names: List + @SerialName("names") val names: List = emptyList() ) diff --git a/data/src/main/java/com/sopt/data/mapper/ResponseGetConfirmedDtoMapper.kt b/data/src/main/java/com/sopt/data/mapper/ResponseGetConfirmedDtoMapper.kt index 929be60a..2b2bd66e 100644 --- a/data/src/main/java/com/sopt/data/mapper/ResponseGetConfirmedDtoMapper.kt +++ b/data/src/main/java/com/sopt/data/mapper/ResponseGetConfirmedDtoMapper.kt @@ -1,18 +1,17 @@ package com.sopt.data.mapper -import com.sopt.data.dto.response.ResponseGetConfirmedDto +import com.sopt.data.dto.response.ResponseGetOptionDetailDto import com.sopt.domain.entity.AppointmentDetailEntity import com.sopt.domain.entity.IdentityEntity -fun ResponseGetConfirmedDto.toAppointmentDetailEntity() = AppointmentDetailEntity( - isHost = isHost, +fun ResponseGetOptionDetailDto.toAppointmentDetailEntity() = AppointmentDetailEntity( myIdentity = myInfo?.toIdentityEntity() ?: IdentityEntity("unavailable", -1, "나"), date = appointmentTime.date, startTime = appointmentTime.startTime, endTime = appointmentTime.endTime, category = category, availableMembersCount = availableFriends?.count ?: 0, - availableMembers = availableFriends?.names ?: emptyList(), + availableMembers = availableFriends?.names?.filterNotNull() ?: emptyList(), unavailableMembersCount = unavailableFriends?.count ?: 0, - unavailableMembers = unavailableFriends?.names ?: emptyList() + unavailableMembers = unavailableFriends?.names ?.filterNotNull() ?: emptyList() ) diff --git a/data/src/main/java/com/sopt/data/mapper/ResponseGetOptionsDtoMapper.kt b/data/src/main/java/com/sopt/data/mapper/ResponseGetOptionsDtoMapper.kt index 91009ed0..51bff556 100644 --- a/data/src/main/java/com/sopt/data/mapper/ResponseGetOptionsDtoMapper.kt +++ b/data/src/main/java/com/sopt/data/mapper/ResponseGetOptionsDtoMapper.kt @@ -46,9 +46,9 @@ fun ResponseGetOptionsOptionDto.toOptionEntity() = OptionEntity( likes = likes, liked = liked, availableMemberCount = availableMemberCount, - availableMembers = availableFriends?.names ?: emptyList(), + availableMembers = availableFriends?.names?.filterNotNull() ?: emptyList(), unavailableMemberCount = unavailableFriends?.count ?: 0, - unavailableMembers = unavailableFriends?.names ?: emptyList() + unavailableMembers = unavailableFriends?.names?.filterNotNull() ?: emptyList() ) fun BaseMyInfoDto.toIdentityEntity() = IdentityEntity( diff --git a/data/src/main/java/com/sopt/data/repositoryimpl/AppointmentConfirmRepositoryImpl.kt b/data/src/main/java/com/sopt/data/repositoryimpl/AppointmentConfirmRepositoryImpl.kt index 12362acd..682c0764 100644 --- a/data/src/main/java/com/sopt/data/repositoryimpl/AppointmentConfirmRepositoryImpl.kt +++ b/data/src/main/java/com/sopt/data/repositoryimpl/AppointmentConfirmRepositoryImpl.kt @@ -41,16 +41,16 @@ class AppointmentConfirmRepositoryImpl @Inject constructor( } } - override suspend fun getConfirmed(appointmentOptionId: Long): Result { + override suspend fun getOptionDetail(appointmentOptionId: Long): Result { return runCatching { - appointmentConfirmDataSource.getConfirmed(appointmentOptionId).result?.toAppointmentDetailEntity() + appointmentConfirmDataSource.getOptionDetail(appointmentOptionId).result?.toAppointmentDetailEntity() ?: throw Exception("getConfirmed failed") } } - override suspend fun postConfirmed(appointmentOptionId: Long): Result { + override suspend fun postOptionConfirm(appointmentOptionId: Long): Result { return runCatching { - appointmentConfirmDataSource.postConfirmed(appointmentOptionId) + appointmentConfirmDataSource.postOptionConfirm(appointmentOptionId) } } diff --git a/data/src/main/java/com/sopt/data/service/ApiKeyStorage.kt b/data/src/main/java/com/sopt/data/service/ApiKeyStorage.kt index 7ec1d797..68e94fe4 100644 --- a/data/src/main/java/com/sopt/data/service/ApiKeyStorage.kt +++ b/data/src/main/java/com/sopt/data/service/ApiKeyStorage.kt @@ -15,7 +15,7 @@ object ApiKeyStorage { const val APPOINTMENT_OPTIONS = "appointment-options" const val APPOINTMENT_OPTION_ID = "appointmentOptionId" const val PROGRESS = "progress" - const val OPTIONS = "options" + const val RECOMMENDED_OPTIONS = "recommended-options" const val LIKE = "like" const val AUTH = "auth" const val WITHDRAW = "withdraw" diff --git a/data/src/main/java/com/sopt/data/service/AppointmentConfirmApiService.kt b/data/src/main/java/com/sopt/data/service/AppointmentConfirmApiService.kt index 03c71564..7d2a6125 100644 --- a/data/src/main/java/com/sopt/data/service/AppointmentConfirmApiService.kt +++ b/data/src/main/java/com/sopt/data/service/AppointmentConfirmApiService.kt @@ -2,7 +2,7 @@ package com.sopt.data.service import com.sopt.data.dto.BaseResponse import com.sopt.data.dto.request.RequestPostTimeTableDto -import com.sopt.data.dto.response.ResponseGetConfirmedDto +import com.sopt.data.dto.response.ResponseGetOptionDetailDto import com.sopt.data.dto.response.ResponseGetOptionsDto import com.sopt.data.dto.response.ResponseGetTimeTableDto import com.sopt.data.dto.response.ResponseLikesDto @@ -13,9 +13,8 @@ import com.sopt.data.service.ApiKeyStorage.APPOINTMENT_MEMBERS import com.sopt.data.service.ApiKeyStorage.APPOINTMENT_OPTIONS import com.sopt.data.service.ApiKeyStorage.APPOINTMENT_OPTION_ID import com.sopt.data.service.ApiKeyStorage.CONFIRM -import com.sopt.data.service.ApiKeyStorage.CONFIRMED import com.sopt.data.service.ApiKeyStorage.LIKE -import com.sopt.data.service.ApiKeyStorage.OPTIONS +import com.sopt.data.service.ApiKeyStorage.RECOMMENDED_OPTIONS import com.sopt.data.service.ApiKeyStorage.TIMETABLE import com.sopt.data.service.ApiKeyStorage.V1 import retrofit2.http.Body @@ -37,18 +36,18 @@ interface AppointmentConfirmApiService { @Path(APPOINTMENT_OPTION_ID) appointmentOptionId: Long ): BaseResponse - @GET("/$API/$V1/$APPOINTMENTS/{$APPOINTMENT_ID}/$OPTIONS") + @GET("/$API/$V1/$APPOINTMENTS/{$APPOINTMENT_ID}/$RECOMMENDED_OPTIONS") suspend fun getOptions( @Path(APPOINTMENT_ID) appointmentId: Long ): BaseResponse - @GET("/$API/$V1/$APPOINTMENT_OPTIONS/{$APPOINTMENT_OPTION_ID}/$CONFIRMED") - suspend fun getConfirmed( + @GET("/$API/$V1/$APPOINTMENT_OPTIONS/{$APPOINTMENT_OPTION_ID}") + suspend fun getOptionDetail( @Path(APPOINTMENT_OPTION_ID) appointmentOptionId: Long - ): BaseResponse + ): BaseResponse @POST("/$API/$V1/$APPOINTMENT_OPTIONS/{$APPOINTMENT_OPTION_ID}/$CONFIRM") - suspend fun postConfirmed( + suspend fun postOptionConfirm( @Path(APPOINTMENT_OPTION_ID) appointmentOptionId: Long ): BaseResponse diff --git a/domain/src/main/java/com/sopt/domain/entity/AppointmentDetailEntity.kt b/domain/src/main/java/com/sopt/domain/entity/AppointmentDetailEntity.kt index 89553262..04ed688c 100644 --- a/domain/src/main/java/com/sopt/domain/entity/AppointmentDetailEntity.kt +++ b/domain/src/main/java/com/sopt/domain/entity/AppointmentDetailEntity.kt @@ -1,7 +1,6 @@ package com.sopt.domain.entity data class AppointmentDetailEntity( - val isHost: Boolean, val myIdentity: IdentityEntity, val date: String, val startTime: String, diff --git a/domain/src/main/java/com/sopt/domain/repository/AppointmentConfirmRepository.kt b/domain/src/main/java/com/sopt/domain/repository/AppointmentConfirmRepository.kt index 549b7df5..0bd74e35 100644 --- a/domain/src/main/java/com/sopt/domain/repository/AppointmentConfirmRepository.kt +++ b/domain/src/main/java/com/sopt/domain/repository/AppointmentConfirmRepository.kt @@ -9,8 +9,8 @@ interface AppointmentConfirmRepository { suspend fun postLike(appointmentId: Long, appointmentOptionId: Long): Result suspend fun deleteLike(appointmentId: Long, appointmentOptionId: Long): Result suspend fun getOptions(appointmentId: Long): Result - suspend fun getConfirmed(appointmentOptionId: Long): Result - suspend fun postConfirmed(appointmentOptionId: Long): Result + suspend fun getOptionDetail(appointmentOptionId: Long): Result + suspend fun postOptionConfirm(appointmentOptionId: Long): Result suspend fun getTimeTable(appointmentId: Long): Result suspend fun postTimeTable(appointmentId: Long, availableTimes: List): Result } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 65a681d2..ca6fbfd6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -184,7 +184,7 @@ junit = { group = "junit", name = "junit", version.ref = "junit" } # JUnit 테 ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } # Compose UI 테스트 매니페스트 라이브러리 ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } # Compose UI JUnit4 테스트 라이브러리 androidx-test-ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-ext-junit" } # AndroidX Test JUnit 확장 라이브러리 -androidx-test-runner = { group = "andrfoidx.test", name = "runner", version.ref = "androidx-test-runner" } # AndroidX Test Runner 라이브러리 +androidx-test-runner = { group = "androidx.test", name = "runner", version.ref = "androidx-test-runner" } # AndroidX Test Runner 라이브러리 androidx-test-core = { group = "androidx.test", name = "core", version.ref = "androidx-test" } # AndroidX Test Core 라이브러리 espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espresso-core" } androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } diff --git a/presentation/src/main/java/com/sopt/presentation/appointment/AppointmentRoute.kt b/presentation/src/main/java/com/sopt/presentation/appointment/AppointmentRoute.kt index 49741e63..09f10b02 100644 --- a/presentation/src/main/java/com/sopt/presentation/appointment/AppointmentRoute.kt +++ b/presentation/src/main/java/com/sopt/presentation/appointment/AppointmentRoute.kt @@ -44,6 +44,7 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.sopt.core.designsystem.component.dialog.NoostakDialog import com.sopt.core.designsystem.component.topappbar.NoostakTopAppBar +import com.sopt.core.designsystem.screen.NoostakFailureScreen import com.sopt.core.designsystem.screen.NoostakLoadingScreen import com.sopt.core.designsystem.theme.NoostakAndroidTheme import com.sopt.core.designsystem.theme.NoostakTheme @@ -54,6 +55,9 @@ import com.sopt.core.state.UiState import com.sopt.core.type.DialogType import com.sopt.domain.entity.AppointmentEntity import com.sopt.domain.entity.AppointmentMembersInfoEntity +import com.sopt.domain.entity.IdentityEntity +import com.sopt.domain.entity.OptionEntity +import com.sopt.domain.entity.RecommendationPriorityEntity import com.sopt.domain.entity.TimeEntity import com.sopt.presentation.R import com.sopt.presentation.appointment.screen.CurrentStatusScreen @@ -66,7 +70,7 @@ fun AppointmentRoute( appointmentName: String, navigateUp: () -> Unit, navigateToAppointmentCheck: (Long, Long, String, List) -> Unit, - navigateToAppointmentConfirm: (Long, Long, Long, String) -> Unit, + navigateToAppointmentConfirm: (Long, Long, Long, String, Boolean) -> Unit, appointmentViewModel: AppointmentViewModel = hiltViewModel() ) { val showDialog by appointmentViewModel.showDialog.collectAsStateWithLifecycle() @@ -90,7 +94,8 @@ fun AppointmentRoute( sideEffect.groupId, sideEffect.appointmentsId, sideEffect.optionId, - sideEffect.appointmentName + sideEffect.appointmentName, + sideEffect.isHost ) } @@ -119,13 +124,6 @@ fun AppointmentRoute( appointmentName, (getTimeTableState as UiState.Success).data.appointmentSchedule.appointmentHostSelectionTimes ) - } else { - navigateToAppointmentCheck( - groupId, - appointmentId, - appointmentName, - mockAvailablePeriods - ) } } }, @@ -137,51 +135,44 @@ fun AppointmentRoute( } ) } - if (getOptionsState is UiState.Success && getTimeTableState is UiState.Success) { - AppointmentScreen( - groupId = groupId, - appointmentsId = appointmentId, - appointmentName = appointmentName, - onBackButtonClick = appointmentViewModel::navigateUp, - onConfirmButtonClick = appointmentViewModel::navigateToAppointmentConfirm, - availablePeriods = (getTimeTableState as UiState.Success).data.appointmentSchedule.appointmentHostSelectionTimes, - availableTimes = (getTimeTableState as UiState.Success).data.appointmentSchedule.appointmentMembersInfo, - recommendations = (getOptionsState as UiState.Success).data, - onLikeClick = { appointmentOptionId, isLiked -> - if (isLiked) { - appointmentViewModel.postLike(appointmentId, appointmentOptionId) - } else { - appointmentViewModel.deleteLike(appointmentId, appointmentOptionId) + + when { + getOptionsState is UiState.Success && getTimeTableState is UiState.Success -> { + val timeTableSuccess = getTimeTableState as UiState.Success + val optionsSuccess = getOptionsState as UiState.Success + + AppointmentScreen( + groupId = groupId, + appointmentsId = appointmentId, + appointmentName = appointmentName, + onBackButtonClick = appointmentViewModel::navigateUp, + onConfirmButtonClick = appointmentViewModel::navigateToAppointmentConfirm, + availablePeriods = timeTableSuccess.data.appointmentSchedule.appointmentHostSelectionTimes, + availableTimes = timeTableSuccess.data.appointmentSchedule.appointmentMembersInfo, + recommendations = optionsSuccess.data, + onLikeClick = { appointmentOptionId, isLiked -> + if (isLiked) { + appointmentViewModel.postLike(appointmentId, appointmentOptionId) + } else { + appointmentViewModel.deleteLike(appointmentId, appointmentOptionId) + } } - } - ) - } else if (getOptionsState is UiState.Loading || getTimeTableState is UiState.Loading) { - NoostakLoadingScreen() - } else if (getOptionsState is UiState.Failure || getTimeTableState is UiState.Failure) { -// NoostakFailureScreen( -// onBackButtonClick = appointmentViewModel::navigateUp, -// onRetryButtonClick = { -// appointmentViewModel.getOptions(appointmentId = appointmentId) -// appointmentViewModel.getTimeTable(appointmentId = appointmentId) -// } -// ) - AppointmentScreen( - groupId = groupId, - appointmentsId = appointmentId, - appointmentName = appointmentName, - onBackButtonClick = appointmentViewModel::navigateUp, - onConfirmButtonClick = appointmentViewModel::navigateToAppointmentConfirm, - availablePeriods = appointmentViewModel.mockAvailablePeriods, - availableTimes = appointmentViewModel.mockAvailableTimes, - recommendations = appointmentViewModel.mockRecommendations, - onLikeClick = { appointmentOptionId, isLiked -> - if (isLiked) { - appointmentViewModel.postLike(appointmentId, appointmentOptionId) - } else { - appointmentViewModel.deleteLike(appointmentId, appointmentOptionId) + ) + } + + getOptionsState is UiState.Loading || getTimeTableState is UiState.Loading -> { + NoostakLoadingScreen() + } + + getOptionsState is UiState.Failure && getTimeTableState is UiState.Failure -> { + NoostakFailureScreen( + onBackButtonClick = appointmentViewModel::navigateUp, + onRetryButtonClick = { + appointmentViewModel.getOptions(appointmentId = appointmentId) + appointmentViewModel.getTimeTable(appointmentId = appointmentId) } - } - ) + ) + } } } @@ -191,7 +182,7 @@ fun AppointmentScreen( appointmentsId: Long, appointmentName: String, onBackButtonClick: () -> Unit, - onConfirmButtonClick: (Long, Long, Long, String) -> Unit, + onConfirmButtonClick: (Long, Long, Long, String, Boolean) -> Unit, availablePeriods: List, availableTimes: List, recommendations: AppointmentEntity, @@ -314,7 +305,13 @@ fun AppointmentScreen( selectedItemIndex = selectedItemIndex, data = recommendations.recommendationPriority, onConfirmButtonClick = { optionId -> - onConfirmButtonClick(groupId, appointmentsId, optionId, appointmentName) + onConfirmButtonClick( + groupId, + appointmentsId, + optionId, + appointmentName, + recommendations.isHost + ) }, onLikeClick = onLikeClick ) @@ -419,16 +416,162 @@ fun RecommendationHeaderItem( @Composable fun AppointmentScreenPreview() { NoostakAndroidTheme { - val appointmentViewModel: AppointmentViewModel = hiltViewModel() AppointmentScreen( groupId = 1, appointmentsId = 1, appointmentName = "3차 회의", onBackButtonClick = {}, - onConfirmButtonClick = { _, _, _, _ -> }, - availablePeriods = appointmentViewModel.mockAvailablePeriods, - availableTimes = appointmentViewModel.mockAvailableTimes, - recommendations = appointmentViewModel.mockRecommendations + onConfirmButtonClick = { _, _, _, _, _ -> }, + availablePeriods = listOf( + TimeEntity( + date = "2024-09-05T10:00:00", + startTime = "2024-09-05T10:00:00", + endTime = "2024-09-05T18:00:00" + ), + TimeEntity( + date = "2024-09-06T10:00:00", + startTime = "2024-09-06T10:00:00", + endTime = "2024-09-06T18:00:00" + ), + TimeEntity( + date = "2024-09-07T10:00:00", + startTime = "2024-09-07T10:00:00", + endTime = "2024-09-07T18:00:00" + ) + ), + availableTimes = listOf( + AppointmentMembersInfoEntity( + memberId = 1, + memberName = "범태하", + appointmentMemberAvailableTimes = listOf( + TimeEntity( + date = "2024-09-05T00:00:00", + startTime = "2024-09-05T10:00:00", + endTime = "2024-09-05T11:00:00" + ), + TimeEntity( + date = "2024-09-05T00:00:00", + startTime = "2024-09-06T14:00:00", + endTime = "2024-09-06T15:00:00" + ), + TimeEntity( + date = "2024-09-06T00:00:00", + startTime = "2024-09-06T10:00:00", + endTime = "2024-09-06T11:00:00" + ), + TimeEntity( + date = "2024-09-07T00:00:00", + startTime = "2024-09-07T10:00:00", + endTime = "2024-09-07T11:00:00" + ) + ) + ), + AppointmentMembersInfoEntity( + memberId = 2, + memberName = "김민수", + appointmentMemberAvailableTimes = listOf( + TimeEntity( + date = "2024-09-05T00:00:00", + startTime = "2024-09-05T10:00:00", + endTime = "2024-09-05T11:00:00" + ), + TimeEntity( + date = "2024-09-05T00:00:00", + startTime = "2024-09-05T11:00:00", + endTime = "2024-09-05T12:00:00" + ) + ) + ) + ), + recommendations = AppointmentEntity( + isHost = true, + recommendationPriority = listOf( + RecommendationPriorityEntity( + priority = 1, + options = listOf( + OptionEntity( + id = 1, + totalMemberCount = 20, + myIdentity = IdentityEntity( + availability = "AVAILABLE", + position = 0, + name = "이가을" + ), + date = "2024-09-27T00:00:00", + startTime = "2024-09-27T11:00:00", + endTime = "2024-09-27T14:00:00", + likes = 15, + liked = true, + availableMemberCount = 10, + availableMembers = listOf( + "이가을", "선우정아", "대한민국만세", "최영희", "정영수", + "이가을", "김언지", "박유진", "임하늘", "변우석" + ), + unavailableMemberCount = 5, + unavailableMembers = listOf("한강", "이영희", "박영수", "최영희", "정영수") + ) + ) + ), + RecommendationPriorityEntity( + priority = 2, + options = listOf( + OptionEntity( + id = 3, + totalMemberCount = 10, + myIdentity = IdentityEntity( + availability = "AVAILABLE", + position = 0, + name = "이가을" + ), + date = "2024-09-27T00:00:00", + startTime = "2024-09-27T11:00:00", + endTime = "2024-09-27T14:00:00", + likes = 15, + liked = true, + availableMemberCount = 5, + availableMembers = listOf( + "이가을", + "선우정아", + "대한민국만세", + "최영희", + "정영수" + ), + unavailableMemberCount = 5, + unavailableMembers = listOf("한강", "이영희", "박영수", "최영희", "정영수") + ) + ) + ), + RecommendationPriorityEntity( + priority = 3, + options = listOf( + OptionEntity( + id = 5, + totalMemberCount = 10, + myIdentity = IdentityEntity( + availability = "AVAILABLE", + position = 0, + name = "이가을" + ), + date = "2024-09-27T00:00:00", + startTime = "2024-09-27T11:00:00", + endTime = "2024-09-27T14:00:00", + likes = 15, + liked = true, + availableMemberCount = 5, + availableMembers = listOf( + "이가을", + "선우정아", + "대한민국만세", + "최영희", + "정영수" + ), + unavailableMemberCount = 5, + unavailableMembers = listOf("한강", "이영희", "박영수", "최영희", "정영수") + ) + ) + ) + ) + ) ) } } diff --git a/presentation/src/main/java/com/sopt/presentation/appointment/AppointmentSideEffect.kt b/presentation/src/main/java/com/sopt/presentation/appointment/AppointmentSideEffect.kt index 5d9f957d..f3ad0ef7 100644 --- a/presentation/src/main/java/com/sopt/presentation/appointment/AppointmentSideEffect.kt +++ b/presentation/src/main/java/com/sopt/presentation/appointment/AppointmentSideEffect.kt @@ -15,7 +15,8 @@ sealed class AppointmentSideEffect { val groupId: Long, val appointmentsId: Long, val optionId: Long, - val appointmentName: String + val appointmentName: String, + val isHost: Boolean ) : AppointmentSideEffect() data class ShowDialog(val show: Boolean) : AppointmentSideEffect() diff --git a/presentation/src/main/java/com/sopt/presentation/appointment/AppointmentViewModel.kt b/presentation/src/main/java/com/sopt/presentation/appointment/AppointmentViewModel.kt index 165c01c6..fd7dd9c1 100644 --- a/presentation/src/main/java/com/sopt/presentation/appointment/AppointmentViewModel.kt +++ b/presentation/src/main/java/com/sopt/presentation/appointment/AppointmentViewModel.kt @@ -4,11 +4,6 @@ import androidx.lifecycle.viewModelScope import com.sopt.core.state.UiState import com.sopt.core.util.BaseViewModel import com.sopt.domain.entity.AppointmentEntity -import com.sopt.domain.entity.AppointmentMembersInfoEntity -import com.sopt.domain.entity.IdentityEntity -import com.sopt.domain.entity.OptionEntity -import com.sopt.domain.entity.RecommendationPriorityEntity -import com.sopt.domain.entity.TimeEntity import com.sopt.domain.entity.TimeTableEntity import com.sopt.domain.repository.AppointmentConfirmRepository import dagger.hilt.android.lifecycle.HiltViewModel @@ -111,14 +106,16 @@ class AppointmentViewModel @Inject constructor( groupId: Long, appointmentId: Long, optionId: Long, - appointmentName: String + appointmentName: String, + isHost: Boolean ) { emitSideEffect( AppointmentSideEffect.NavigateToAppointmentConfirm( groupId, appointmentId, optionId, - appointmentName + appointmentName, + isHost ) ) } @@ -126,231 +123,4 @@ class AppointmentViewModel @Inject constructor( fun showDialog(show: Boolean) { _showDialog.update { show } } - - val mockAvailablePeriods = listOf( - TimeEntity( - date = "2024-09-05T10:00:00", - startTime = "2024-09-05T10:00:00", - endTime = "2024-09-05T18:00:00" - ), - TimeEntity( - date = "2024-09-06T10:00:00", - startTime = "2024-09-06T10:00:00", - endTime = "2024-09-06T18:00:00" - ), - TimeEntity( - date = "2024-09-07T10:00:00", - startTime = "2024-09-07T10:00:00", - endTime = "2024-09-07T18:00:00" - ) - ) - - val mockAvailableTimes = listOf( - AppointmentMembersInfoEntity( - memberId = 1, - memberName = "권장순", - appointmentMemberAvailableTimes = listOf( - TimeEntity( - date = "2024-09-05T00:00:00", - startTime = "2024-09-05T10:00:00", - endTime = "2024-09-05T11:00:00" - ), - TimeEntity( - date = "2024-09-05T00:00:00", - startTime = "2024-09-06T14:00:00", - endTime = "2024-09-06T15:00:00" - ), - TimeEntity( - date = "2024-09-06T00:00:00", - startTime = "2024-09-06T10:00:00", - endTime = "2024-09-06T11:00:00" - ), - TimeEntity( - date = "2024-09-07T00:00:00", - startTime = "2024-09-07T10:00:00", - endTime = "2024-09-07T11:00:00" - ) - ) - ), - AppointmentMembersInfoEntity( - memberId = 2, - memberName = "김민수", - appointmentMemberAvailableTimes = listOf( - TimeEntity( - date = "2024-09-05T00:00:00", - startTime = "2024-09-05T11:00:00", - endTime = "2024-09-05T12:00:00" - ), - TimeEntity( - date = "2024-09-06T00:00:00", - startTime = "2024-09-06T11:00:00", - endTime = "2024-09-06T12:00:00" - ), - TimeEntity( - date = "2024-09-07T00:00:00", - startTime = "2024-09-07T11:00:00", - endTime = "2024-09-07T12:00:00" - ) - ) - ) - ) - - val mockRecommendations = - AppointmentEntity( - isHost = true, - recommendationPriority = listOf( - RecommendationPriorityEntity( - priority = 1, - options = listOf( - OptionEntity( - id = 1, - totalMemberCount = 20, - myIdentity = IdentityEntity( - availability = "available", - position = 0, - name = "이가을" - ), - date = "2024-09-27T00:00:00", - startTime = "2024-09-27T11:00:00", - endTime = "2024-09-27T14:00:00", - likes = 15, - liked = true, - availableMemberCount = 10, - availableMembers = listOf( - "이가을", "선우정아", "대한민국만세", "최영희", "정영수", - "이가을", "김언지", "박유진", "임하늘", "변우석", "김혜윤", "정해인", "카리나", "닝닝", - "지젤", "장원영", "이채연", "김민주", "김채원", "김민주", "김채원", "김민주" - ), - unavailableMemberCount = 5, - unavailableMembers = listOf("한강", "이영희", "박영수", "최영희", "정영수") - ), - OptionEntity( - id = 2, - totalMemberCount = 20, - myIdentity = IdentityEntity( - availability = "available", - position = 0, - name = "이가을" - ), - date = "2024-09-27T00:00:00", - startTime = "2024-09-27T11:00:00", - endTime = "2024-09-27T14:00:00", - likes = 15, - liked = false, - availableMemberCount = 10, - availableMembers = listOf( - "이가을", "선우정아", "대한민국만세", "최영희", "정영수", - "이가을", "김언지", "박유진", "임하늘", "변우석", "김혜윤", "정해인", "카리나", "닝닝", - "지젤", "장원영", "이채연", "김민주", "김채원", "김민주", "김채원", "김민주" - ), - unavailableMemberCount = 5, - unavailableMembers = listOf("한강", "이영희", "박영수", "최영희", "정영수") - ) - ) - ), - RecommendationPriorityEntity( - priority = 2, - options = listOf( - OptionEntity( - id = 3, - totalMemberCount = 10, - myIdentity = IdentityEntity( - availability = "available", - position = 0, - name = "이가을" - ), - date = "2024-09-27T00:00:00", - startTime = "2024-09-27T11:00:00", - endTime = "2024-09-27T14:00:00", - likes = 15, - liked = true, - availableMemberCount = 5, - availableMembers = listOf( - "이가을", "선우정아", "대한민국만세", "최영희", "정영수", - "이가을", "김언지", "박유진", "임하늘", "변우석", "김혜윤", "정해인", "카리나", "닝닝", - "지젤", "장원영", "이채연", "김민주", "김채원", "김민주", "김채원", "김민주" - ), - unavailableMemberCount = 5, - unavailableMembers = listOf("한강", "이영희", "박영수", "최영희", "정영수") - ), - OptionEntity( - id = 4, - totalMemberCount = 10, - myIdentity = IdentityEntity( - availability = "available", - position = 0, - name = "이가을" - ), - date = "2024-09-27T00:00:00", - startTime = "2024-09-27T11:00:00", - endTime = "2024-09-27T14:00:00", - likes = 15, - liked = false, - availableMemberCount = 5, - availableMembers = listOf( - "이가을", "선우정아", "대한민국만세", "최영희", "정영수", - "이가을", "김언지", "박유진", "임하늘", "변우석", "김혜윤", "정해인", "카리나", "닝닝", - "지젤", "장원영", "이채연", "김민주", "김채원", "김민주", "김채원", "김민주" - ), - unavailableMemberCount = 5, - unavailableMembers = listOf("한강", "이영희", "박영수", "최영희", "정영수") - ) - ) - ), - RecommendationPriorityEntity( - priority = 3, - options = listOf( - OptionEntity( - id = 5, - totalMemberCount = 10, - myIdentity = IdentityEntity( - availability = "available", - position = 0, - name = "이가을" - ), - date = "2024-09-27T00:00:00", - startTime = "2024-09-27T11:00:00", - endTime = "2024-09-27T14:00:00", - likes = 15, - liked = true, - availableMemberCount = 5, - availableMembers = listOf( - "이가을", "선우정아", "대한민국만세", "최영희", "정영수", - "이가을", "김언지", "박유진", "임하늘", "변우석", "김혜윤", "정해인", "카리나", "닝닝", - "지젤", "장원영", "이채연", "김민주", "김채원", "김민주", "김채원", "김민주" - ), - unavailableMemberCount = 5, - unavailableMembers = listOf("한강", "이영희", "박영수", "최영희", "정영수") - ) - ) - ), - RecommendationPriorityEntity( - priority = 4, - options = listOf( - OptionEntity( - id = 6, - totalMemberCount = 10, - myIdentity = IdentityEntity( - availability = "available", - position = 0, - name = "이가을" - ), - date = "2024-09-27T00:00:00", - startTime = "2024-09-27T11:00:00", - endTime = "2024-09-27T14:00:00", - likes = 15, - liked = true, - availableMemberCount = 5, - availableMembers = listOf( - "이가을", "선우정아", "대한민국만세", "최영희", "정영수", - "이가을", "김언지", "박유진", "임하늘", "변우석", "김혜윤", "정해인", "카리나", "닝닝", - "지젤", "장원영", "이채연", "김민주", "김채원", "김민주", "김채원", "김민주" - ), - unavailableMemberCount = 5, - unavailableMembers = listOf("한강", "이영희", "박영수", "최영희", "정영수") - ) - ) - ) - ) - ) } diff --git a/presentation/src/main/java/com/sopt/presentation/appointment/appointmentCheck/AppointmentCheckRoute.kt b/presentation/src/main/java/com/sopt/presentation/appointment/appointmentCheck/AppointmentCheckRoute.kt index f8d41063..4b7bf881 100644 --- a/presentation/src/main/java/com/sopt/presentation/appointment/appointmentCheck/AppointmentCheckRoute.kt +++ b/presentation/src/main/java/com/sopt/presentation/appointment/appointmentCheck/AppointmentCheckRoute.kt @@ -13,7 +13,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign @@ -24,12 +23,11 @@ import androidx.constraintlayout.compose.Dimension import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.sopt.core.designsystem.component.button.NoostakBottomButton +import com.sopt.core.designsystem.component.dialog.NoostakDialog import com.sopt.core.designsystem.component.timetable.NoostakEditableTimeTable import com.sopt.core.designsystem.component.topappbar.NoostakTopAppBar import com.sopt.core.designsystem.theme.NoostakAndroidTheme import com.sopt.core.designsystem.theme.NoostakTheme -import com.sopt.core.extension.toast -import com.sopt.core.state.UiState import com.sopt.domain.entity.TimeEntity import com.sopt.presentation.R import timber.log.Timber @@ -45,9 +43,8 @@ fun AppointmentCheckRoute( navigateToGroupDetail: (Long) -> Unit, appointmentCheckViewModel: AppointmentCheckViewModel = hiltViewModel() ) { - val postTimeTableState by appointmentCheckViewModel.postTimeTableState.collectAsStateWithLifecycle() + val showErrorDialog by appointmentCheckViewModel.showErrorDialog.collectAsStateWithLifecycle() var selectedData by remember { mutableStateOf(emptyList()) } - val context = LocalContext.current val rememberedAvailablePeriods = remember { availablePeriods } LaunchedEffect(key1 = appointmentCheckViewModel.sideEffects) { appointmentCheckViewModel.sideEffects.collect { sideEffect -> @@ -65,29 +62,14 @@ fun AppointmentCheckRoute( navigateToGroupDetail(sideEffect.groupId) } - is AppointmentCheckSideEffect.ShowToast -> { - context.toast(sideEffect.message) - } + is AppointmentCheckSideEffect.ShowErrorDialog -> appointmentCheckViewModel.showErrorDialog( + sideEffect.show, + sideEffect.dialogType + ) } } } - LaunchedEffect(key1 = postTimeTableState) { - when (postTimeTableState) { - is UiState.Success -> appointmentCheckViewModel.navigateToAppointment( - groupId, - appointmentId, - appointmentName - ) - - is UiState.Failure -> { - Timber.e("postTimeTable 실패: ${(postTimeTableState as UiState.Failure).msg}") - } - - else -> {} - } - } - AppointmentCheckScreen( groupId = groupId, appointmentName = appointmentName, @@ -95,9 +77,32 @@ fun AppointmentCheckRoute( onSelectedDataChange = { selectedData = it }, onBackButtonClick = appointmentCheckViewModel::navigateToGroupDetail, onConfirmButtonClick = { - appointmentCheckViewModel.postTimeTable(appointmentId, selectedData) + appointmentCheckViewModel.postTimeTable( + groupId, + appointmentId, + appointmentName, + selectedData + ) } ) + + if (showErrorDialog.first) { + NoostakDialog( + dialogType = showErrorDialog.second, + onClick = { + appointmentCheckViewModel.showErrorDialog(false, showErrorDialog.second) + appointmentCheckViewModel.postTimeTable( + groupId, + appointmentId, + appointmentName, + selectedData + ) + }, + onDismissRequest = { + appointmentCheckViewModel.showErrorDialog(false, showErrorDialog.second) + } + ) + } } @Composable diff --git a/presentation/src/main/java/com/sopt/presentation/appointment/appointmentCheck/AppointmentCheckViewModel.kt b/presentation/src/main/java/com/sopt/presentation/appointment/appointmentCheck/AppointmentCheckViewModel.kt index c55aaba1..e0751db6 100644 --- a/presentation/src/main/java/com/sopt/presentation/appointment/appointmentCheck/AppointmentCheckViewModel.kt +++ b/presentation/src/main/java/com/sopt/presentation/appointment/appointmentCheck/AppointmentCheckViewModel.kt @@ -2,40 +2,79 @@ package com.sopt.presentation.appointment.appointmentCheck import androidx.lifecycle.viewModelScope import com.sopt.core.state.UiState +import com.sopt.core.type.DialogType import com.sopt.core.util.BaseViewModel import com.sopt.domain.entity.TimeEntity import com.sopt.domain.repository.AppointmentConfirmRepository -import com.sopt.presentation.R 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.update import kotlinx.coroutines.launch +import java.io.IOException import javax.inject.Inject @HiltViewModel class AppointmentCheckViewModel @Inject constructor( private val appointmentConfirmRepository: AppointmentConfirmRepository ) : BaseViewModel() { + private val _showErrorDialog = MutableStateFlow(Pair(false, DialogType.DATA_FAILURE)) + val showErrorDialog: StateFlow> get() = _showErrorDialog.asStateFlow() + private val _postTimeTableState: MutableStateFlow> = MutableStateFlow(UiState.Empty) - val postTimeTableState: StateFlow> get() = _postTimeTableState.asStateFlow() - fun postTimeTable(appointmentId: Long, availableTimes: List) { + fun postTimeTable( + groupId: Long, + appointmentId: Long, + appointmentName: String, + availableTimes: List + ) { viewModelScope.launch { _postTimeTableState.emit(UiState.Loading) appointmentConfirmRepository.postTimeTable(appointmentId, availableTimes).fold( onSuccess = { _postTimeTableState.emit(UiState.Success(it)) + emitSideEffect( + AppointmentCheckSideEffect.NavigateToAppointment( + groupId, + appointmentId, + appointmentName + ) + ) }, - onFailure = { - _postTimeTableState.emit(UiState.Failure(it.message.toString())) - emitSideEffect(AppointmentCheckSideEffect.ShowToast(R.string.appointment_check_failure)) + onFailure = { throwable -> + when (throwable) { + is IOException -> { + _postTimeTableState.emit(UiState.Failure(throwable.message.toString())) + emitSideEffect( + AppointmentCheckSideEffect.ShowErrorDialog( + true, + DialogType.NETWORK_FAILURE + ) + ) + } + + else -> { + _postTimeTableState.emit(UiState.Failure(throwable.message.toString())) + emitSideEffect( + AppointmentCheckSideEffect.ShowErrorDialog( + true, + DialogType.DATA_FAILURE + ) + ) + } + } } ) } } + fun showErrorDialog(show: Boolean, dialogType: DialogType) { + _showErrorDialog.update { it.copy(first = show, second = dialogType) } + } + fun navigateUp() { emitSideEffect(AppointmentCheckSideEffect.NavigateUp) } @@ -64,5 +103,6 @@ sealed class AppointmentCheckSideEffect { ) : AppointmentCheckSideEffect() data class NavigateToGroupDetail(val groupId: Long) : AppointmentCheckSideEffect() - data class ShowToast(val message: Int) : AppointmentCheckSideEffect() + data class ShowErrorDialog(val show: Boolean, val dialogType: DialogType) : + AppointmentCheckSideEffect() } diff --git a/presentation/src/main/java/com/sopt/presentation/appointment/appointmentConfirm/AppointmentConfirmRoute.kt b/presentation/src/main/java/com/sopt/presentation/appointment/appointmentConfirm/AppointmentConfirmRoute.kt index 51792a98..281a8279 100644 --- a/presentation/src/main/java/com/sopt/presentation/appointment/appointmentConfirm/AppointmentConfirmRoute.kt +++ b/presentation/src/main/java/com/sopt/presentation/appointment/appointmentConfirm/AppointmentConfirmRoute.kt @@ -20,7 +20,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview @@ -33,15 +32,16 @@ import com.sopt.core.designsystem.component.chip.NoostakCategoryChip import com.sopt.core.designsystem.component.chip.UnavailableUserChips import com.sopt.core.designsystem.component.dialog.NoostakDialog import com.sopt.core.designsystem.component.topappbar.NoostakTopAppBar +import com.sopt.core.designsystem.screen.NoostakFailureScreen import com.sopt.core.designsystem.screen.NoostakLoadingScreen import com.sopt.core.designsystem.theme.NoostakAndroidTheme import com.sopt.core.designsystem.theme.NoostakTheme import com.sopt.core.extension.showIf -import com.sopt.core.extension.toast import com.sopt.core.state.UiState import com.sopt.core.util.CalculateTime import com.sopt.core.util.RearrangeList import com.sopt.domain.entity.AppointmentDetailEntity +import com.sopt.domain.entity.IdentityEntity import com.sopt.presentation.R import com.sopt.presentation.groupDetail.confirmedDetail.CompleteDetailInfo import timber.log.Timber @@ -49,17 +49,15 @@ import timber.log.Timber @Composable fun AppointmentConfirmRoute( groupId: Long, - appointmentId: Long, optionId: Long, appointmentName: String, + isHost: Boolean, navigateUp: () -> Unit, navigateToGroupDetail: (Long) -> Unit, appointmentConfirmViewModel: AppointmentConfirmViewModel = hiltViewModel() ) { val showErrorDialog by appointmentConfirmViewModel.showErrorDialog.collectAsStateWithLifecycle() val getConfirmedState by appointmentConfirmViewModel.getConfirmedState.collectAsStateWithLifecycle() - val postConfirmedState by appointmentConfirmViewModel.postConfirmedState.collectAsStateWithLifecycle() - val context = LocalContext.current LaunchedEffect(key1 = appointmentConfirmViewModel.sideEffects) { appointmentConfirmViewModel.sideEffects.collect { sideEffect -> when (sideEffect) { @@ -68,7 +66,6 @@ fun AppointmentConfirmRoute( navigateToGroupDetail(sideEffect.groupId) } - is AppointmentConfirmSideEffect.ShowToast -> context.toast(sideEffect.message) is AppointmentConfirmSideEffect.ShowErrorDialog -> appointmentConfirmViewModel.showErrorDialog( sideEffect.show, sideEffect.dialogType @@ -78,17 +75,7 @@ fun AppointmentConfirmRoute( } LaunchedEffect(key1 = Unit) { - appointmentConfirmViewModel.getConfirmed(optionId) - } - - LaunchedEffect(key1 = postConfirmedState) { - when (postConfirmedState) { - is UiState.Success -> appointmentConfirmViewModel.navigateToGroupDetail( - groupId - ) - - else -> {} - } + appointmentConfirmViewModel.getOptionDetail(optionId) } when (getConfirmedState) { @@ -97,9 +84,10 @@ fun AppointmentConfirmRoute( AppointmentConfirmScreen( groupId = groupId, appointmentName = appointmentName, + isHost = isHost, onBackButtonClick = appointmentConfirmViewModel::navigateUp, onConfirmButtonClick = { - appointmentConfirmViewModel.postConfirmed(optionId) + appointmentConfirmViewModel.postOptionConfirm(groupId, optionId) }, data = (getConfirmedState as UiState.Success).data ) @@ -107,20 +95,11 @@ fun AppointmentConfirmRoute( is UiState.Failure -> { Timber.e("getConfirmedState is failure $getConfirmedState") -// NoostakFailureScreen( -// onBackButtonClick = appointmentConfirmViewModel::navigateUp, -// onRetryButtonClick = { -// appointmentConfirmViewModel.getConfirmed(optionId) -// } -// ) - AppointmentConfirmScreen( - groupId = groupId, - appointmentName = appointmentName, + NoostakFailureScreen( onBackButtonClick = appointmentConfirmViewModel::navigateUp, - onConfirmButtonClick = { - appointmentConfirmViewModel.postConfirmed(optionId) - }, - data = appointmentConfirmViewModel.mockAppointmentDetail + onRetryButtonClick = { + appointmentConfirmViewModel.getOptionDetail(optionId) + } ) } @@ -132,7 +111,7 @@ fun AppointmentConfirmRoute( dialogType = showErrorDialog.second, onClick = { appointmentConfirmViewModel.showErrorDialog(false, showErrorDialog.second) - appointmentConfirmViewModel.postConfirmed(optionId) + appointmentConfirmViewModel.postOptionConfirm(groupId, optionId) }, onDismissRequest = { appointmentConfirmViewModel.showErrorDialog(false, showErrorDialog.second) @@ -146,6 +125,7 @@ fun AppointmentConfirmRoute( fun AppointmentConfirmScreen( groupId: Long, appointmentName: String, + isHost: Boolean = false, onBackButtonClick: () -> Unit, onConfirmButtonClick: (Long) -> Unit, data: AppointmentDetailEntity @@ -267,7 +247,7 @@ fun AppointmentConfirmScreen( } Spacer(modifier = Modifier.weight(1f)) NoostakBottomButton( - modifier = Modifier.showIf(data.isHost), + modifier = Modifier.showIf(isHost), text = stringResource(R.string.btn_appointment_confirm_complete), onButtonClick = { onConfirmButtonClick(groupId) }, isEnabled = true, @@ -280,14 +260,32 @@ fun AppointmentConfirmScreen( @Preview(showBackground = true) @Composable fun AppointmentConfirmScreenPreview() { - val appointmentConfirmViewModel: AppointmentConfirmViewModel = hiltViewModel() NoostakAndroidTheme { AppointmentConfirmScreen( groupId = 1, appointmentName = "약속 이름", + isHost = true, onBackButtonClick = {}, onConfirmButtonClick = {}, - data = appointmentConfirmViewModel.mockAppointmentDetail + data = AppointmentDetailEntity( + myIdentity = IdentityEntity( + availability = "UNAVAILABLE", + position = 2, + name = "박영수" + ), + date = "2025-01-06T00:00:00", + startTime = "2025-01-06T11:00:00", + endTime = "2025-01-06T14:00:00", + category = "기타", + availableMembersCount = 22, + availableMembers = listOf( + "선우정아", "대한민국만세", "최영희", "정영수", + "이가을", "김언지", "박유진", "임하늘", "변우석", "김혜윤", "정해인", "카리나", "닝닝", + "지젤", "장원영", "이채연", "김민주", "김채원", "김민주", "김채원", "김민주" + ), + unavailableMembersCount = 5, + unavailableMembers = listOf("한강", "이영희", "박영수", "최영희", "정영수") + ) ) } } diff --git a/presentation/src/main/java/com/sopt/presentation/appointment/appointmentConfirm/AppointmentConfirmViewModel.kt b/presentation/src/main/java/com/sopt/presentation/appointment/appointmentConfirm/AppointmentConfirmViewModel.kt index 00dfa445..2ecff264 100644 --- a/presentation/src/main/java/com/sopt/presentation/appointment/appointmentConfirm/AppointmentConfirmViewModel.kt +++ b/presentation/src/main/java/com/sopt/presentation/appointment/appointmentConfirm/AppointmentConfirmViewModel.kt @@ -5,11 +5,11 @@ import com.sopt.core.state.UiState import com.sopt.core.type.DialogType import com.sopt.core.util.BaseViewModel import com.sopt.domain.entity.AppointmentDetailEntity -import com.sopt.domain.entity.IdentityEntity import com.sopt.domain.repository.AppointmentConfirmRepository 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.update import kotlinx.coroutines.launch import java.io.IOException @@ -20,20 +20,20 @@ class AppointmentConfirmViewModel @Inject constructor( private val appointmentConfirmRepository: AppointmentConfirmRepository ) : BaseViewModel() { private val _showErrorDialog = MutableStateFlow(Pair(false, DialogType.DATA_FAILURE)) - val showErrorDialog: StateFlow> get() = _showErrorDialog + val showErrorDialog: StateFlow> get() = _showErrorDialog.asStateFlow() private val _getConfirmedState: MutableStateFlow> = MutableStateFlow(UiState.Empty) - val getConfirmedState: MutableStateFlow> = _getConfirmedState + val getConfirmedState: StateFlow> = + _getConfirmedState.asStateFlow() private val _postConfirmedState: MutableStateFlow> = MutableStateFlow(UiState.Empty) - val postConfirmedState: MutableStateFlow> = _postConfirmedState - fun getConfirmed(appointmentOptionId: Long) { + fun getOptionDetail(appointmentOptionId: Long) { viewModelScope.launch { _getConfirmedState.emit(UiState.Loading) - appointmentConfirmRepository.getConfirmed(appointmentOptionId).fold( + appointmentConfirmRepository.getOptionDetail(appointmentOptionId).fold( onSuccess = { _getConfirmedState.emit(UiState.Success(it)) }, @@ -44,22 +44,34 @@ class AppointmentConfirmViewModel @Inject constructor( } } - fun postConfirmed(appointmentOptionId: Long) { + fun postOptionConfirm(groupId: Long, appointmentOptionId: Long) { viewModelScope.launch { _postConfirmedState.emit(UiState.Loading) - appointmentConfirmRepository.postConfirmed(appointmentOptionId).fold( + appointmentConfirmRepository.postOptionConfirm(appointmentOptionId).fold( onSuccess = { _postConfirmedState.emit(UiState.Success(it)) + emitSideEffect(AppointmentConfirmSideEffect.NavigateToGroupDetail(groupId)) }, onFailure = { throwable -> when (throwable) { is IOException -> { // 네트워크 에러 _postConfirmedState.emit(UiState.Failure(throwable.message.toString())) - emitSideEffect(AppointmentConfirmSideEffect.ShowErrorDialog(true, DialogType.NETWORK_FAILURE)) + emitSideEffect( + AppointmentConfirmSideEffect.ShowErrorDialog( + true, + DialogType.NETWORK_FAILURE + ) + ) } + else -> { // 서버 통신 에러 _postConfirmedState.emit(UiState.Failure(throwable.message.toString())) - emitSideEffect(AppointmentConfirmSideEffect.ShowErrorDialog(true, DialogType.DATA_FAILURE)) + emitSideEffect( + AppointmentConfirmSideEffect.ShowErrorDialog( + true, + DialogType.DATA_FAILURE + ) + ) } } } @@ -78,27 +90,6 @@ class AppointmentConfirmViewModel @Inject constructor( fun navigateToGroupDetail(groupId: Long) { emitSideEffect(AppointmentConfirmSideEffect.NavigateToGroupDetail(groupId)) } - - val mockAppointmentDetail = AppointmentDetailEntity( - isHost = true, - myIdentity = IdentityEntity( - availability = "unavailable", - position = 2, - name = "박영수" - ), - date = "2025-01-06T00:00:00", - startTime = "2025-01-06T11:00:00", - endTime = "2025-01-06T14:00:00", - category = "기타", - availableMembersCount = 22, - availableMembers = listOf( - "선우정아", "대한민국만세", "최영희", "정영수", - "이가을", "김언지", "박유진", "임하늘", "변우석", "김혜윤", "정해인", "카리나", "닝닝", - "지젤", "장원영", "이채연", "김민주", "김채원", "김민주", "김채원", "김민주" - ), - unavailableMembersCount = 5, - unavailableMembers = listOf("한강", "이영희", "박영수", "최영희", "정영수") - ) } sealed class AppointmentConfirmSideEffect { @@ -107,6 +98,6 @@ sealed class AppointmentConfirmSideEffect { val groupId: Long ) : AppointmentConfirmSideEffect() - data class ShowToast(val message: Int) : AppointmentConfirmSideEffect() - data class ShowErrorDialog(val show: Boolean, val dialogType: DialogType) : AppointmentConfirmSideEffect() + data class ShowErrorDialog(val show: Boolean, val dialogType: DialogType) : + AppointmentConfirmSideEffect() } diff --git a/presentation/src/main/java/com/sopt/presentation/appointment/navigation/AppointmentNavigation.kt b/presentation/src/main/java/com/sopt/presentation/appointment/navigation/AppointmentNavigation.kt index 5c058fa1..db8b618b 100644 --- a/presentation/src/main/java/com/sopt/presentation/appointment/navigation/AppointmentNavigation.kt +++ b/presentation/src/main/java/com/sopt/presentation/appointment/navigation/AppointmentNavigation.kt @@ -58,14 +58,15 @@ fun NavController.navigateAppointmentConfirm( appointmentId: Long, appointmentName: String, optionId: Long, + isHost: Boolean, navOptions: NavOptions? = null ) { navigate( route = AppointmentConfirm( groupId = groupId, - appointmentId = appointmentId, appointmentName = appointmentName, - optionId = optionId + optionId = optionId, + isHost = isHost ), navOptions = navOptions ) @@ -92,12 +93,13 @@ fun NavGraphBuilder.appointmentNavGraph( appointmentName = appointmentName ) }, - navigateToAppointmentConfirm = { groupId, appointmentId, optionId, appointmentName -> + navigateToAppointmentConfirm = { groupId, appointmentId, optionId, appointmentName, isHost -> navHostController.navigateAppointmentConfirm( groupId = groupId, appointmentId = appointmentId, optionId = optionId, - appointmentName = appointmentName + appointmentName = appointmentName, + isHost = isHost ) } ) @@ -132,9 +134,9 @@ fun NavGraphBuilder.appointmentNavGraph( val args = it.toRoute() AppointmentConfirmRoute( groupId = args.groupId, - appointmentId = args.appointmentId, optionId = args.optionId, appointmentName = args.appointmentName, + isHost = args.isHost, navigateUp = navHostController::navigateUp, navigateToGroupDetail = { groupId -> navHostController.navigateGroupDetail(groupId = groupId) @@ -160,7 +162,7 @@ data class AppointmentCheck( @Serializable data class AppointmentConfirm( val groupId: Long, - val appointmentId: Long, val optionId: Long, - val appointmentName: String + val appointmentName: String, + val isHost: Boolean ) : Route diff --git a/presentation/src/main/java/com/sopt/presentation/appointment/screen/CurrentStatusScreen.kt b/presentation/src/main/java/com/sopt/presentation/appointment/screen/CurrentStatusScreen.kt index 20525792..805e3e8b 100644 --- a/presentation/src/main/java/com/sopt/presentation/appointment/screen/CurrentStatusScreen.kt +++ b/presentation/src/main/java/com/sopt/presentation/appointment/screen/CurrentStatusScreen.kt @@ -59,7 +59,7 @@ fun CurrentStatusScreen( fun CurrentStatusScreenPreview() { val appointmentViewModel: AppointmentViewModel = hiltViewModel() CurrentStatusScreen( - availablePeriods = appointmentViewModel.mockAvailablePeriods, - availableTimes = appointmentViewModel.mockAvailableTimes + availablePeriods = emptyList(), + availableTimes = emptyList() ) } diff --git a/presentation/src/main/java/com/sopt/presentation/appointment/screen/RecommendationScreen.kt b/presentation/src/main/java/com/sopt/presentation/appointment/screen/RecommendationScreen.kt index 9a8ddb44..2dbbfd11 100644 --- a/presentation/src/main/java/com/sopt/presentation/appointment/screen/RecommendationScreen.kt +++ b/presentation/src/main/java/com/sopt/presentation/appointment/screen/RecommendationScreen.kt @@ -20,8 +20,8 @@ import androidx.compose.foundation.shape.RoundedCornerShape 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.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue @@ -33,6 +33,7 @@ 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 com.sopt.core.designsystem.component.button.NoostakBottomButton import com.sopt.core.designsystem.component.chip.AvailableUserChips import com.sopt.core.designsystem.component.chip.UnavailableUserChips @@ -104,10 +105,15 @@ fun RecommendationItem( data: OptionEntity, isSelected: Boolean, onItemClick: () -> Unit, - onLikeClick: (Boolean) -> Unit + onLikeClick: (Boolean) -> Unit, + recommendationViewModel: RecommendationViewModel = hiltViewModel() ) { - var isLiked by remember { mutableStateOf(data.liked) } - var likes by remember { mutableIntStateOf(data.likes) } + val likeState by remember { + derivedStateOf { + recommendationViewModel.likeStates[data.id] ?: (data.liked to data.likes) + } + } + val (isLiked, likes) = likeState val calculateTime = CalculateTime() val date = calculateTime.extractDateWithKorean(data.date) val dayOfWeek = calculateTime.extractDayOfWeekWithBraces(data.date) @@ -172,9 +178,8 @@ fun RecommendationItem( ) { Image( modifier = Modifier.noRippleClickable { - isLiked = !isLiked - likes = if (isLiked) likes + 1 else likes - 1 - onLikeClick(isLiked) + recommendationViewModel.toggleLike(data.id, data.liked, data.likes) + onLikeClick(!isLiked) }, imageVector = if (isLiked) { ImageVector.vectorResource(id = R.drawable.ic_heart_on) diff --git a/presentation/src/main/java/com/sopt/presentation/appointment/screen/RecommendationViewModel.kt b/presentation/src/main/java/com/sopt/presentation/appointment/screen/RecommendationViewModel.kt new file mode 100644 index 00000000..724b2f65 --- /dev/null +++ b/presentation/src/main/java/com/sopt/presentation/appointment/screen/RecommendationViewModel.kt @@ -0,0 +1,17 @@ +package com.sopt.presentation.appointment.screen + +import androidx.compose.runtime.mutableStateMapOf +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject + +@HiltViewModel +class RecommendationViewModel @Inject constructor() : ViewModel() { + private val _likeStates = mutableStateMapOf>() + val likeStates: Map> get() = _likeStates + + fun toggleLike(itemId: Long, defaultLiked: Boolean, defaultLikes: Int) { + val (isLiked, likes) = _likeStates[itemId] ?: (defaultLiked to defaultLikes) + _likeStates[itemId] = !isLiked to if (isLiked) likes - 1 else likes + 1 + } +} diff --git a/presentation/src/main/java/com/sopt/presentation/auth/splash/SplashViewModel.kt b/presentation/src/main/java/com/sopt/presentation/auth/splash/SplashViewModel.kt index 646dce4f..12adab72 100644 --- a/presentation/src/main/java/com/sopt/presentation/auth/splash/SplashViewModel.kt +++ b/presentation/src/main/java/com/sopt/presentation/auth/splash/SplashViewModel.kt @@ -6,6 +6,7 @@ import com.sopt.domain.repository.UserInfoRepository import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject @HiltViewModel @@ -17,6 +18,7 @@ class SplashViewModel @Inject constructor( if (userInfoRepository.getIsAutoLogin().first()) { emitSideEffect(SplashSideEffect.NavigateToHome) } else { + Timber.d("${userInfoRepository.getIsAutoLogin().first()}") emitSideEffect(SplashSideEffect.NavigateToLogin) } } diff --git a/presentation/src/main/java/com/sopt/presentation/groupDetail/GroupDetailRoute.kt b/presentation/src/main/java/com/sopt/presentation/groupDetail/GroupDetailRoute.kt index 79799217..a2d6946e 100644 --- a/presentation/src/main/java/com/sopt/presentation/groupDetail/GroupDetailRoute.kt +++ b/presentation/src/main/java/com/sopt/presentation/groupDetail/GroupDetailRoute.kt @@ -130,6 +130,7 @@ fun GroupDetailRoute( tabs = groupDetailViewModel.tabs, groupName = groupOngoing.groupOngoingInfo.groupName, groupImage = groupOngoing.groupOngoingInfo.groupProfileImageUrl, + groupInvitationCode = groupOngoing.groupOngoingInfo.groupInviteCode, groupMembersCount = groupOngoing.groupOngoingInfo.groupMemberCount.toInt(), progressEntities = groupOngoing.ongoingAppointments.map { ProgressEntity( @@ -162,6 +163,7 @@ fun GroupDetailScreen( tabs: List, groupName: String, groupImage: String?, + groupInvitationCode: String = "", groupMembersCount: Int, progressEntities: List, confirmedEntities: List, @@ -202,7 +204,7 @@ fun GroupDetailScreen( .padding(horizontal = dimensionResource(id = R.dimen.horizontal_padding)) ) { GroupDetailHeader( - groupId = groupId, + groupInvitationCode = groupInvitationCode, groupImage = groupImage, groupName = groupName ) @@ -326,7 +328,7 @@ fun CustomTabPager( @Composable fun GroupDetailHeader( - groupId: Long, + groupInvitationCode: String, groupImage: String?, groupName: String ) { @@ -366,7 +368,7 @@ fun GroupDetailHeader( action = Intent.ACTION_SEND putExtra( Intent.EXTRA_TEXT, - "공유하고자 하는 그룹 아이디: $groupId" + groupInvitationCode ) type = "text/plain" } diff --git a/presentation/src/main/java/com/sopt/presentation/groupDetail/groupMember/GroupMemberRoute.kt b/presentation/src/main/java/com/sopt/presentation/groupDetail/groupMember/GroupMemberRoute.kt index de6d662c..cd08b628 100644 --- a/presentation/src/main/java/com/sopt/presentation/groupDetail/groupMember/GroupMemberRoute.kt +++ b/presentation/src/main/java/com/sopt/presentation/groupDetail/groupMember/GroupMemberRoute.kt @@ -117,7 +117,7 @@ fun GroupMemberScreen( .padding(horizontal = dimensionResource(id = R.dimen.horizontal_padding)) ) { GroupDetailHeader( - groupId = groupId, + groupInvitationCode = groupInfo.groupInvitationCode, groupImage = groupInfo.groupProfileImageUrl, groupName = groupInfo.groupName )