diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index e16dbe44..89507517 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -68,6 +68,7 @@ kotlin { implementation(project(":shared:data:home")) implementation(project(":shared:data:community")) implementation(project(":shared:data:detail")) + implementation(project(":shared:data:evaluate")) implementation(project(":shared:feature:community")) implementation(project(":shared:feature:draw")) @@ -88,6 +89,8 @@ kotlin { implementation(project(":shared:domain:home")) implementation(project(":shared:domain:model")) implementation(project(":shared:domain:detail")) + implementation(project(":shared:domain:evaluate")) + implementation(project(":shared:domain:model")) } commonTest.dependencies { implementation(libs.kotlin.test) diff --git a/composeApp/src/commonMain/kotlin/com/kus/kustaurant/App.kt b/composeApp/src/commonMain/kotlin/com/kus/kustaurant/App.kt index 24f75e0a..9c0a4988 100644 --- a/composeApp/src/commonMain/kotlin/com/kus/kustaurant/App.kt +++ b/composeApp/src/commonMain/kotlin/com/kus/kustaurant/App.kt @@ -1,6 +1,5 @@ package com.kus.kustaurant -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize @@ -118,7 +117,7 @@ fun SetNavigation() { val showBottomBar = shouldShowBottomBar(destination) val currentRoute = destination?.route val selectedKey = BottomTab.fromRoute(currentRoute).key - + val applySystemBarsPadding = !isEdgeToEdgeScreen(currentRoute) val isWriter = navBackStackEntry?.destination?.hasRoute() == true || @@ -136,7 +135,7 @@ fun SetNavigation() { navController = navController, ) }, - modifier = if (applySystemBarsPadding) Modifier.systemBarsPadding() else Modifier, + modifier = if (applySystemBarsPadding) Modifier.systemBarsPadding() else Modifier, contentWindowInsets = WindowInsets.systemBars, ) { padding -> Box( @@ -170,6 +169,7 @@ fun SetNavigation() { } } } + } } diff --git a/composeApp/src/commonMain/kotlin/com/kus/kustaurant/di/initKoin.kt b/composeApp/src/commonMain/kotlin/com/kus/kustaurant/di/initKoin.kt index bb677dd0..20bb955f 100644 --- a/composeApp/src/commonMain/kotlin/com/kus/kustaurant/di/initKoin.kt +++ b/composeApp/src/commonMain/kotlin/com/kus/kustaurant/di/initKoin.kt @@ -10,6 +10,7 @@ import com.kus.domain.auth.di.authDomainModule import com.kus.domain.community.di.communityDomainModule import com.kus.feature.community.di.communityFeatureModule import com.kus.feature.detail.di.detailFeatureModule +import com.kus.feature.evaluate.di.evaluateFeatureModule import com.kus.feature.home.di.homeFeatureModule import com.kus.feature.login.di.featureLoginModule import com.kus.feature.onboarding.di.onboardingFeatureModule @@ -19,8 +20,10 @@ import com.kus.shared.domain.tier.di.tierDomainModule import org.koin.core.KoinApplication import com.kus.feature.splash.di.splashFeatureModule import com.kus.kustaurant.detail.di.detailDataModule +import com.kus.kustaurant.evaluate.di.evaluateDataModule import com.kus.kustaurant.home.di.homeDataModule import com.kus.shared.domain.detail.di.detailDomainModule +import com.kus.shared.domain.evaluate.di.evaluateDomainModule import com.kus.shared.domain.home.di.homeDomainModule import org.koin.core.context.startKoin import org.koin.core.module.Module @@ -44,6 +47,7 @@ fun initKoin( homeDomainModule, communityDomainModule, detailDomainModule, + evaluateDomainModule, // data (repository 등 공통) networkModule, @@ -53,6 +57,7 @@ fun initKoin( homeDataModule, communityDataModule, detailDataModule, + evaluateDataModule, // feature splashFeatureModule, @@ -60,8 +65,9 @@ fun initKoin( featureLoginModule, tierFeatureModule, homeFeatureModule, - communityFeatureModule - detailFeatureModule + communityFeatureModule, + detailFeatureModule, + evaluateFeatureModule ) modules(additionalModules) diff --git a/composeApp/src/commonMain/kotlin/com/kus/kustaurant/navigation/KusNavHost.kt b/composeApp/src/commonMain/kotlin/com/kus/kustaurant/navigation/KusNavHost.kt index d13b2019..477c4218 100644 --- a/composeApp/src/commonMain/kotlin/com/kus/kustaurant/navigation/KusNavHost.kt +++ b/composeApp/src/commonMain/kotlin/com/kus/kustaurant/navigation/KusNavHost.kt @@ -21,6 +21,7 @@ import com.kus.feature.community.navigation.CommunityWrite import com.kus.feature.community.navigation.CommunityWriteModify import com.kus.feature.community.navigation.communityNavGraph import com.kus.feature.detail.navigation.Detail +import com.kus.feature.detail.config.DetailKeys.DETAIL_EVALUATE_REFRESH import com.kus.feature.detail.navigation.detailNavGraph import com.kus.feature.draw.navigation.drawNavGraph import com.kus.feature.evaluate.navigation.Evaluate @@ -154,7 +155,7 @@ fun KusNavHost( navController.navigate(TierCategorySelect) }, - navigateToDetail = { navController.navigate(Detail) }, + navigateToDetail = { restaurantId -> navController.navigate(Detail(restaurantId)) }, popBackStackWithResult = { result -> val json = KusJson.json.encodeToString(result) navController.previousBackStackEntry @@ -228,11 +229,32 @@ fun KusNavHost( detailNavGraph( navigateToUp = navController::popBackStack, - navigateToEvaluate = { navController.navigate(Evaluate) }, + navigateToEvaluate = { restaurant -> + navController.navigate( + Evaluate( + restaurantId = restaurant.restaurantId, + restaurantName = restaurant.restaurantName, + mainTier = restaurant.mainTier, + restaurantCuisine = restaurant.restaurantCuisine, + restaurantCuisineImgUrl = restaurant.restaurantCuisineImgUrl, + restaurantPosition = restaurant.restaurantPosition, + restaurantAddress = restaurant.restaurantAddress, + situationList = restaurant.situationList, + partnershipInfo = restaurant.partnershipInfo, + ) + ) + }, ) evaluateNavGraph( - onBackClick = { navController.popBackStack() } + onBackClick = { navController.popBackStack() }, + onSubmitSuccess = { + navController.previousBackStackEntry + ?.savedStateHandle + ?.set(DETAIL_EVALUATE_REFRESH, true) + + navController.popBackStack() + } ) searchNavGraph( @@ -240,4 +262,4 @@ fun KusNavHost( navigateToRestDetail = { /* Todo: 상세화면 연결 */ }, ) } -} \ No newline at end of file +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 1f5d50f0..40162200 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -54,8 +54,9 @@ include( ":shared:data:tier", ":shared:data:auth", ":shared:data:home", - ":shared:data:community" - ":shared:data:detail" + ":shared:data:community", + ":shared:data:detail", + ":shared:data:evaluate" ) include( @@ -65,7 +66,8 @@ include( ":shared:domain:auth", ":shared:domain:home", ":shared:domain:community", - ":shared:domain:detail" + ":shared:domain:detail", + ":shared:domain:evaluate" ) include( diff --git a/shared/core/designSystem/src/commonMain/kotlin/com/kus/designsystem/component/KusButton.kt b/shared/core/designSystem/src/commonMain/kotlin/com/kus/designsystem/component/KusButton.kt index f3b05f82..288334af 100644 --- a/shared/core/designSystem/src/commonMain/kotlin/com/kus/designsystem/component/KusButton.kt +++ b/shared/core/designSystem/src/commonMain/kotlin/com/kus/designsystem/component/KusButton.kt @@ -58,6 +58,8 @@ fun KusButton( isShadowVisible: Boolean = false, onClick: () -> Unit, ) { + val resolvedBorderColor = if (enabled) borderColor else KusTheme.colors.c_E0E0E0 + Box( modifier = modifier ) { @@ -84,7 +86,7 @@ fun KusButton( disabledContainerColor = KusTheme.colors.c_E0E0E0, disabledContentColor = KusTheme.colors.c_AAAAAA, ), - border = BorderStroke(1.dp, borderColor), + border = BorderStroke(1.dp, resolvedBorderColor), contentPadding = contentPadding, ) { Row( diff --git a/shared/data/evaluate/build.gradle.kts b/shared/data/evaluate/build.gradle.kts new file mode 100644 index 00000000..5cae16cb --- /dev/null +++ b/shared/data/evaluate/build.gradle.kts @@ -0,0 +1,88 @@ +plugins { + alias(libs.plugins.kotlinMultiplatform) + alias(libs.plugins.androidKotlinMultiplatformLibrary) + alias(libs.plugins.androidLint) + alias(libs.plugins.kotlin.serialization) +} + +kotlin { + androidLibrary { + namespace = "com.kus.shared.data.evaluate" + compileSdk = 36 + minSdk = 26 + + withHostTestBuilder { + } + + withDeviceTestBuilder { + sourceSetTreeName = "test" + }.configure { + instrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + } + + val xcfName = "shared:data:evaluateKit" + + iosX64 { + binaries.framework { + baseName = xcfName + } + } + + iosArm64 { + binaries.framework { + baseName = xcfName + } + } + + iosSimulatorArm64 { + binaries.framework { + baseName = xcfName + } + } + + jvm("desktop") + + sourceSets { + commonMain { + dependencies { + implementation(libs.koin.core) + implementation(libs.kotlinx.coroutines.core) + + implementation(libs.kotlinx.serialization.json) + implementation(libs.bundles.ktor) + + implementation(project(":shared:domain:model")) + implementation(project(":shared:domain:evaluate")) + implementation(project(":shared:data:network")) + } + } + + commonTest { + dependencies { + implementation(libs.kotlin.test) + } + } + + androidMain { + dependencies { + implementation(libs.koin.android) + implementation(libs.datastore.preferences) + } + } + + getByName("androidDeviceTest") { + dependencies { + implementation(libs.androidx.runner) + implementation(libs.androidx.core) + implementation(libs.androidx.testExt.junit) + } + } + + iosMain { + dependencies { + } + } + } + +} diff --git a/shared/data/evaluate/src/androidMain/AndroidManifest.xml b/shared/data/evaluate/src/androidMain/AndroidManifest.xml new file mode 100644 index 00000000..8bdb7e14 --- /dev/null +++ b/shared/data/evaluate/src/androidMain/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/shared/data/evaluate/src/commonMain/kotlin/com/kus/kustaurant/evaluate/api/EvaluateApi.kt b/shared/data/evaluate/src/commonMain/kotlin/com/kus/kustaurant/evaluate/api/EvaluateApi.kt new file mode 100644 index 00000000..a9a9f9cb --- /dev/null +++ b/shared/data/evaluate/src/commonMain/kotlin/com/kus/kustaurant/evaluate/api/EvaluateApi.kt @@ -0,0 +1,56 @@ +package com.kus.kustaurant.evaluate.api + +import com.kus.kustaurant.evaluate.remote.request.EvaluationRequest +import com.kus.kustaurant.evaluate.remote.response.EvaluationResponse +import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.request.forms.MultiPartFormDataContent +import io.ktor.client.request.forms.formData +import io.ktor.client.request.get +import io.ktor.client.request.post +import io.ktor.client.request.setBody +import io.ktor.http.Headers +import io.ktor.http.HttpHeaders + +class EvaluateApi( + private val client: HttpClient, +) { + suspend fun getEvaluation(restaurantId: Long): EvaluationResponse { + return client.get("/api/v2/auth/restaurants/$restaurantId/evaluation").body() + } + + suspend fun postEvaluation( + restaurantId: Long, + request: EvaluationRequest, + imageBytes: ByteArray?, + ) { + client.post("/api/v2/auth/restaurants/$restaurantId/evaluation") { + setBody( + MultiPartFormDataContent( + formData { + append("evaluationScore", request.evaluationScore.toString()) + + request.evaluationSituations?.forEach { situation -> + append("evaluationSituations", situation.toString()) + } + + request.evaluationComment?.let { + append("evaluationComment", it) + } + + imageBytes?.let { + append( + key = "newImage", + value = it, + headers = Headers.build { + append(HttpHeaders.ContentType, "image/jpeg") + append(HttpHeaders.ContentDisposition, "filename=evaluation.jpg") + } + ) + } + } + ) + ) + } + } +} diff --git a/shared/data/evaluate/src/commonMain/kotlin/com/kus/kustaurant/evaluate/di/evaluateDataModule.kt b/shared/data/evaluate/src/commonMain/kotlin/com/kus/kustaurant/evaluate/di/evaluateDataModule.kt new file mode 100644 index 00000000..af404846 --- /dev/null +++ b/shared/data/evaluate/src/commonMain/kotlin/com/kus/kustaurant/evaluate/di/evaluateDataModule.kt @@ -0,0 +1,13 @@ +package com.kus.kustaurant.evaluate.di + +import com.kus.kustaurant.evaluate.api.EvaluateApi +import com.kus.kustaurant.evaluate.repositoryimpl.EvaluateRepositoryImpl +import com.kus.shared.domain.evaluate.repository.EvaluateRepository +import org.koin.core.module.dsl.singleOf +import org.koin.dsl.bind +import org.koin.dsl.module + +val evaluateDataModule = module { + singleOf(::EvaluateApi) + singleOf(::EvaluateRepositoryImpl) bind EvaluateRepository::class +} diff --git a/shared/data/evaluate/src/commonMain/kotlin/com/kus/kustaurant/evaluate/remote/mapper/EvaluateMapper.kt b/shared/data/evaluate/src/commonMain/kotlin/com/kus/kustaurant/evaluate/remote/mapper/EvaluateMapper.kt new file mode 100644 index 00000000..7cbf8ca5 --- /dev/null +++ b/shared/data/evaluate/src/commonMain/kotlin/com/kus/kustaurant/evaluate/remote/mapper/EvaluateMapper.kt @@ -0,0 +1,21 @@ +package com.kus.kustaurant.evaluate.remote.mapper + +import com.kus.kustaurant.evaluate.remote.response.EvaluationResponse +import com.kus.kustaurant.evaluate.remote.response.StarCommentResponse +import com.kus.shared.domain.model.evaluate.PreviousEvaluation +import com.kus.shared.domain.model.evaluate.PreviousStarComment + +fun EvaluationResponse.toDomain(): PreviousEvaluation = + PreviousEvaluation( + evaluationScore = evaluationScore, + evaluationSituations = evaluationSituations ?: emptyList(), + evaluationImgUrl = evaluationImgUrl, + evaluationComment = evaluationComment, + starComments = starComments.map { it.toDomain() }, + ) + +fun StarCommentResponse.toDomain(): PreviousStarComment = + PreviousStarComment( + star = star, + comment = comment, + ) diff --git a/shared/data/evaluate/src/commonMain/kotlin/com/kus/kustaurant/evaluate/remote/request/EvaluationRequest.kt b/shared/data/evaluate/src/commonMain/kotlin/com/kus/kustaurant/evaluate/remote/request/EvaluationRequest.kt new file mode 100644 index 00000000..3028f86f --- /dev/null +++ b/shared/data/evaluate/src/commonMain/kotlin/com/kus/kustaurant/evaluate/remote/request/EvaluationRequest.kt @@ -0,0 +1,10 @@ +package com.kus.kustaurant.evaluate.remote.request + +import kotlinx.serialization.Serializable + +@Serializable +data class EvaluationRequest( + val evaluationScore: Double, + val evaluationSituations: List? = null, + val evaluationComment: String? = null, +) diff --git a/shared/data/evaluate/src/commonMain/kotlin/com/kus/kustaurant/evaluate/remote/response/EvaluationResponse.kt b/shared/data/evaluate/src/commonMain/kotlin/com/kus/kustaurant/evaluate/remote/response/EvaluationResponse.kt new file mode 100644 index 00000000..1c157d33 --- /dev/null +++ b/shared/data/evaluate/src/commonMain/kotlin/com/kus/kustaurant/evaluate/remote/response/EvaluationResponse.kt @@ -0,0 +1,19 @@ +package com.kus.kustaurant.evaluate.remote.response + +import kotlinx.serialization.Serializable + +@Serializable +data class EvaluationResponse( + val evaluationScore: Double? = null, + val evaluationSituations: List? = null, + val evaluationImgUrl: String? = null, + val evaluationComment: String? = null, + val starComments: List = emptyList(), + val newImage: String? = null, +) + +@Serializable +data class StarCommentResponse( + val star: Double, + val comment: String, +) diff --git a/shared/data/evaluate/src/commonMain/kotlin/com/kus/kustaurant/evaluate/repositoryimpl/EvaluateRepositoryImpl.kt b/shared/data/evaluate/src/commonMain/kotlin/com/kus/kustaurant/evaluate/repositoryimpl/EvaluateRepositoryImpl.kt new file mode 100644 index 00000000..ce373c9f --- /dev/null +++ b/shared/data/evaluate/src/commonMain/kotlin/com/kus/kustaurant/evaluate/repositoryimpl/EvaluateRepositoryImpl.kt @@ -0,0 +1,33 @@ +package com.kus.kustaurant.evaluate.repositoryimpl + +import com.kus.kustaurant.evaluate.api.EvaluateApi +import com.kus.kustaurant.evaluate.remote.mapper.toDomain +import com.kus.kustaurant.evaluate.remote.request.EvaluationRequest +import com.kus.shared.domain.evaluate.repository.EvaluateRepository +import com.kus.shared.domain.model.evaluate.PreviousEvaluation + +class EvaluateRepositoryImpl( + private val api: EvaluateApi, +) : EvaluateRepository { + override suspend fun getEvaluation(restaurantId: Long): PreviousEvaluation = + api.getEvaluation(restaurantId).toDomain() + + override suspend fun postEvaluation( + restaurantId: Long, + evaluationScore: Double, + evaluationSituations: List?, + evaluationComment: String?, + imageBytes: ByteArray?, + ) { + val request = EvaluationRequest( + evaluationScore = evaluationScore, + evaluationSituations = evaluationSituations?.takeIf { it.isNotEmpty() }, + evaluationComment = evaluationComment?.takeIf { it.isNotBlank() }, + ) + api.postEvaluation( + restaurantId = restaurantId, + request = request, + imageBytes = imageBytes, + ) + } +} diff --git a/shared/domain/evaluate/build.gradle.kts b/shared/domain/evaluate/build.gradle.kts new file mode 100644 index 00000000..d77d1d0a --- /dev/null +++ b/shared/domain/evaluate/build.gradle.kts @@ -0,0 +1,78 @@ +plugins { + alias(libs.plugins.kotlinMultiplatform) + alias(libs.plugins.androidKotlinMultiplatformLibrary) + alias(libs.plugins.androidLint) +} + +kotlin { + androidLibrary { + namespace = "com.kus.shared.domain.evaluate" + compileSdk = 36 + minSdk = 26 + + withHostTestBuilder { + } + + withDeviceTestBuilder { + sourceSetTreeName = "test" + }.configure { + instrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + } + + val xcfName = "shared:domain:evaluateKit" + + iosX64 { + binaries.framework { + baseName = xcfName + } + } + + iosArm64 { + binaries.framework { + baseName = xcfName + } + } + + iosSimulatorArm64 { + binaries.framework { + baseName = xcfName + } + } + + jvm("desktop") + + sourceSets { + commonMain { + dependencies { + implementation(libs.kotlinx.coroutines.core) + api(libs.koin.core) + implementation(project(":shared:domain:model")) + } + } + + commonTest { + dependencies { + implementation(libs.kotlin.test) + } + } + + androidMain { + dependencies { + } + } + + getByName("androidDeviceTest") { + dependencies { + implementation(libs.androidx.runner) + implementation(libs.androidx.core) + implementation(libs.androidx.testExt.junit) + } + } + + iosMain { + dependencies { + } + } + } +} diff --git a/shared/domain/evaluate/src/androidMain/AndroidManifest.xml b/shared/domain/evaluate/src/androidMain/AndroidManifest.xml new file mode 100644 index 00000000..8bdb7e14 --- /dev/null +++ b/shared/domain/evaluate/src/androidMain/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/shared/domain/evaluate/src/commonMain/kotlin/com/kus/shared/domain/evaluate/di/evaluateDomainModule.kt b/shared/domain/evaluate/src/commonMain/kotlin/com/kus/shared/domain/evaluate/di/evaluateDomainModule.kt new file mode 100644 index 00000000..51988fa5 --- /dev/null +++ b/shared/domain/evaluate/src/commonMain/kotlin/com/kus/shared/domain/evaluate/di/evaluateDomainModule.kt @@ -0,0 +1,11 @@ +package com.kus.shared.domain.evaluate.di + +import com.kus.shared.domain.evaluate.usecase.GetEvaluationUseCase +import com.kus.shared.domain.evaluate.usecase.PostEvaluationUseCase +import org.koin.core.module.dsl.singleOf +import org.koin.dsl.module + +val evaluateDomainModule = module { + singleOf(::GetEvaluationUseCase) + singleOf(::PostEvaluationUseCase) +} diff --git a/shared/domain/evaluate/src/commonMain/kotlin/com/kus/shared/domain/evaluate/repository/EvaluateRepository.kt b/shared/domain/evaluate/src/commonMain/kotlin/com/kus/shared/domain/evaluate/repository/EvaluateRepository.kt new file mode 100644 index 00000000..b35d52ae --- /dev/null +++ b/shared/domain/evaluate/src/commonMain/kotlin/com/kus/shared/domain/evaluate/repository/EvaluateRepository.kt @@ -0,0 +1,15 @@ +package com.kus.shared.domain.evaluate.repository + +import com.kus.shared.domain.model.evaluate.PreviousEvaluation + +interface EvaluateRepository { + suspend fun getEvaluation(restaurantId: Long): PreviousEvaluation + + suspend fun postEvaluation( + restaurantId: Long, + evaluationScore: Double, + evaluationSituations: List?, + evaluationComment: String?, + imageBytes: ByteArray?, + ) +} diff --git a/shared/domain/evaluate/src/commonMain/kotlin/com/kus/shared/domain/evaluate/usecase/GetEvaluationUseCase.kt b/shared/domain/evaluate/src/commonMain/kotlin/com/kus/shared/domain/evaluate/usecase/GetEvaluationUseCase.kt new file mode 100644 index 00000000..2ff279dd --- /dev/null +++ b/shared/domain/evaluate/src/commonMain/kotlin/com/kus/shared/domain/evaluate/usecase/GetEvaluationUseCase.kt @@ -0,0 +1,12 @@ +package com.kus.shared.domain.evaluate.usecase + +import com.kus.shared.domain.evaluate.repository.EvaluateRepository +import com.kus.shared.domain.model.evaluate.PreviousEvaluation + +class GetEvaluationUseCase( + private val evaluateRepository: EvaluateRepository, +) { + suspend operator fun invoke(restaurantId: Long): PreviousEvaluation { + return evaluateRepository.getEvaluation(restaurantId) + } +} diff --git a/shared/domain/evaluate/src/commonMain/kotlin/com/kus/shared/domain/evaluate/usecase/PostEvaluationUseCase.kt b/shared/domain/evaluate/src/commonMain/kotlin/com/kus/shared/domain/evaluate/usecase/PostEvaluationUseCase.kt new file mode 100644 index 00000000..8516a15f --- /dev/null +++ b/shared/domain/evaluate/src/commonMain/kotlin/com/kus/shared/domain/evaluate/usecase/PostEvaluationUseCase.kt @@ -0,0 +1,23 @@ +package com.kus.shared.domain.evaluate.usecase + +import com.kus.shared.domain.evaluate.repository.EvaluateRepository + +class PostEvaluationUseCase( + private val evaluateRepository: EvaluateRepository, +) { + suspend operator fun invoke( + restaurantId: Long, + evaluationScore: Double, + evaluationSituations: List?, + evaluationComment: String?, + imageBytes: ByteArray?, + ) { + evaluateRepository.postEvaluation( + restaurantId = restaurantId, + evaluationScore = evaluationScore, + evaluationSituations = evaluationSituations, + evaluationComment = evaluationComment, + imageBytes = imageBytes, + ) + } +} diff --git a/shared/domain/model/src/commonMain/kotlin/com/kus/shared/domain/model/evaluate/PreviousEvaluation.kt b/shared/domain/model/src/commonMain/kotlin/com/kus/shared/domain/model/evaluate/PreviousEvaluation.kt new file mode 100644 index 00000000..6ed7776f --- /dev/null +++ b/shared/domain/model/src/commonMain/kotlin/com/kus/shared/domain/model/evaluate/PreviousEvaluation.kt @@ -0,0 +1,14 @@ +package com.kus.shared.domain.model.evaluate + +data class PreviousEvaluation( + val evaluationScore: Double?, + val evaluationSituations: List, + val evaluationImgUrl: String?, + val evaluationComment: String?, + val starComments: List, +) + +data class PreviousStarComment( + val star: Double, + val comment: String, +) diff --git a/shared/feature/detail/src/commonMain/kotlin/com/kus/feature/detail/config/DetailKeys.kt b/shared/feature/detail/src/commonMain/kotlin/com/kus/feature/detail/config/DetailKeys.kt new file mode 100644 index 00000000..9b5cfbaf --- /dev/null +++ b/shared/feature/detail/src/commonMain/kotlin/com/kus/feature/detail/config/DetailKeys.kt @@ -0,0 +1,5 @@ +package com.kus.feature.detail.config + +object DetailKeys { + const val DETAIL_EVALUATE_REFRESH = "detail_evaluate_refresh" +} diff --git a/shared/feature/detail/src/commonMain/kotlin/com/kus/feature/detail/navigation/DetailNavGraph.kt b/shared/feature/detail/src/commonMain/kotlin/com/kus/feature/detail/navigation/DetailNavGraph.kt index c4af0a3b..62a1374f 100644 --- a/shared/feature/detail/src/commonMain/kotlin/com/kus/feature/detail/navigation/DetailNavGraph.kt +++ b/shared/feature/detail/src/commonMain/kotlin/com/kus/feature/detail/navigation/DetailNavGraph.kt @@ -1,21 +1,36 @@ package com.kus.feature.detail.navigation +import androidx.compose.runtime.getValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable +import androidx.navigation.toRoute +import com.kus.feature.detail.config.DetailKeys import com.kus.feature.detail.ui.DetailRoute +import com.kus.shared.domain.model.detail.RestaurantDetail import kotlinx.serialization.Serializable @Serializable -data object Detail +data class Detail(val restaurantId: Long) fun NavGraphBuilder.detailNavGraph( navigateToUp: () -> Unit, - navigateToEvaluate: () -> Unit, + navigateToEvaluate: (RestaurantDetail) -> Unit, ) { - composable { + composable { backStackEntry -> + val route = backStackEntry.toRoute() + val shouldRefreshFromEvaluate by backStackEntry.savedStateHandle + .getStateFlow(DetailKeys.DETAIL_EVALUATE_REFRESH, false) + .collectAsStateWithLifecycle() + DetailRoute( + restaurantId = route.restaurantId, navigateToEvaluate = navigateToEvaluate, navigateToUp = navigateToUp, + shouldRefreshFromEvaluate = shouldRefreshFromEvaluate, + clearEvaluateRefreshFlag = { + backStackEntry.savedStateHandle[DetailKeys.DETAIL_EVALUATE_REFRESH] = false + }, ) } } diff --git a/shared/feature/detail/src/commonMain/kotlin/com/kus/feature/detail/ui/DetailScreen.kt b/shared/feature/detail/src/commonMain/kotlin/com/kus/feature/detail/ui/DetailScreen.kt index 809c977e..57b5e33c 100644 --- a/shared/feature/detail/src/commonMain/kotlin/com/kus/feature/detail/ui/DetailScreen.kt +++ b/shared/feature/detail/src/commonMain/kotlin/com/kus/feature/detail/ui/DetailScreen.kt @@ -65,8 +65,10 @@ import org.koin.compose.viewmodel.koinViewModel @Composable fun DetailRoute( restaurantId: Long = 510L, - navigateToEvaluate: () -> Unit, + navigateToEvaluate: (RestaurantDetail) -> Unit, navigateToUp: () -> Unit, + shouldRefreshFromEvaluate: Boolean = false, + clearEvaluateRefreshFlag: () -> Unit = {}, viewModel: DetailViewModel = koinViewModel(), ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() @@ -75,6 +77,12 @@ fun DetailRoute( viewModel.getRestaurantDetail(restaurantId) } + LaunchedEffect(shouldRefreshFromEvaluate) { + if (!shouldRefreshFromEvaluate) return@LaunchedEffect + viewModel.refreshAfterEvaluation() + clearEvaluateRefreshFlag() + } + when (val restaurantState = uiState.restaurant) { is UiState.Loading -> { Box( @@ -90,7 +98,7 @@ fun DetailRoute( restaurant = restaurantState.data, reviewsState = uiState.reviews, reviewSort = uiState.reviewSort, - navigateToEvaluate = navigateToEvaluate, + navigateToEvaluate = { navigateToEvaluate(restaurantState.data) }, onBackClick = navigateToUp, onFavoriteClick = { viewModel.onFavoriteClick() }, onSortSelected = { sort -> viewModel.getRestaurantReviews(sort) }, diff --git a/shared/feature/detail/src/commonMain/kotlin/com/kus/feature/detail/ui/DetailViewModel.kt b/shared/feature/detail/src/commonMain/kotlin/com/kus/feature/detail/ui/DetailViewModel.kt index 2ea21636..3bf34744 100644 --- a/shared/feature/detail/src/commonMain/kotlin/com/kus/feature/detail/ui/DetailViewModel.kt +++ b/shared/feature/detail/src/commonMain/kotlin/com/kus/feature/detail/ui/DetailViewModel.kt @@ -86,6 +86,15 @@ class DetailViewModel( } } + fun refreshAfterEvaluation() { + if (currentRestaurantId == 0L) return + getRestaurantDetail(currentRestaurantId) + + if (_uiState.value.reviews !is UiState.Idle) { + getRestaurantReviews(_uiState.value.reviewSort) + } + } + fun onReviewLikeClick(evalId: Int) { onReviewReactionClick(evalId, targetReaction = "LIKE") } diff --git a/shared/feature/evaluate/build.gradle.kts b/shared/feature/evaluate/build.gradle.kts index 058a3f29..eec25ff4 100644 --- a/shared/feature/evaluate/build.gradle.kts +++ b/shared/feature/evaluate/build.gradle.kts @@ -53,7 +53,10 @@ kotlin { implementation(libs.kamel.image.default) implementation(project(":shared:core:designSystem")) + implementation(project(":shared:core:presentation")) implementation(project(":shared:data:network")) + implementation(project(":shared:domain:evaluate")) + implementation(project(":shared:domain:model")) resources.srcDir("src/commonMain/composeResources") } diff --git a/shared/feature/evaluate/src/commonMain/kotlin/com/kus/feature/evaluate/component/EvaluationRestInfoCard.kt b/shared/feature/evaluate/src/commonMain/kotlin/com/kus/feature/evaluate/component/EvaluationRestInfoCard.kt index a0d6cd2c..535deda8 100644 --- a/shared/feature/evaluate/src/commonMain/kotlin/com/kus/feature/evaluate/component/EvaluationRestInfoCard.kt +++ b/shared/feature/evaluate/src/commonMain/kotlin/com/kus/feature/evaluate/component/EvaluationRestInfoCard.kt @@ -16,7 +16,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.kus.designsystem.theme.KusTheme -import com.kus.feature.evaluate.ui.EvaluateRestaurant +import com.kus.feature.evaluate.model.EvaluateRestaurant import kustaurant.shared.core.designsystem.generated.resources.Res import kustaurant.shared.core.designsystem.generated.resources.ic_location import org.jetbrains.compose.resources.painterResource diff --git a/shared/feature/evaluate/src/commonMain/kotlin/com/kus/feature/evaluate/di/evaluateFeatureModule.kt b/shared/feature/evaluate/src/commonMain/kotlin/com/kus/feature/evaluate/di/evaluateFeatureModule.kt new file mode 100644 index 00000000..0e04bac9 --- /dev/null +++ b/shared/feature/evaluate/src/commonMain/kotlin/com/kus/feature/evaluate/di/evaluateFeatureModule.kt @@ -0,0 +1,9 @@ +package com.kus.feature.evaluate.di + +import com.kus.feature.evaluate.ui.EvaluateViewModel +import org.koin.core.module.dsl.viewModelOf +import org.koin.dsl.module + +val evaluateFeatureModule = module { + viewModelOf(::EvaluateViewModel) +} diff --git a/shared/feature/evaluate/src/commonMain/kotlin/com/kus/feature/evaluate/model/EvaluateRestaurant.kt b/shared/feature/evaluate/src/commonMain/kotlin/com/kus/feature/evaluate/model/EvaluateRestaurant.kt new file mode 100644 index 00000000..e9efe694 --- /dev/null +++ b/shared/feature/evaluate/src/commonMain/kotlin/com/kus/feature/evaluate/model/EvaluateRestaurant.kt @@ -0,0 +1,27 @@ +package com.kus.feature.evaluate.model + +data class EvaluateRestaurant( + val restaurantId: Long, + val restaurantName: String, + val mainTier: Int, + val restaurantCuisine: String, + val restaurantCuisineImgUrl: String, + val restaurantPosition: String, + val restaurantAddress: String, + val situationList: ArrayList, + val partnershipInfo: String, +) { + companion object { + fun empty() = EvaluateRestaurant( + restaurantId = 0, + restaurantName = "", + mainTier = 0, + restaurantCuisine = "", + restaurantCuisineImgUrl = "", + restaurantPosition = "", + restaurantAddress = "", + situationList = arrayListOf(), + partnershipInfo = "", + ) + } +} diff --git a/shared/feature/evaluate/src/commonMain/kotlin/com/kus/feature/evaluate/model/Evaluation.kt b/shared/feature/evaluate/src/commonMain/kotlin/com/kus/feature/evaluate/model/Evaluation.kt new file mode 100644 index 00000000..5fbd13f7 --- /dev/null +++ b/shared/feature/evaluate/src/commonMain/kotlin/com/kus/feature/evaluate/model/Evaluation.kt @@ -0,0 +1,26 @@ +package com.kus.feature.evaluate.model + +data class Evaluation( + val evaluationScore: Double, + val evaluationSituations: List, + val evaluationImgUrl: String, + val evaluationComment: String, + val starComments: List, + val imageBytes: ByteArray? = null, +) { + companion object { + fun empty() = Evaluation( + evaluationScore = 0.0, + evaluationSituations = emptyList(), + evaluationImgUrl = "", + evaluationComment = "", + starComments = emptyList(), + imageBytes = null, + ) + } +} + +data class StarComment( + val star: Double, + val comment: String, +) diff --git a/shared/feature/evaluate/src/commonMain/kotlin/com/kus/feature/evaluate/navigation/EvaluateNavGraph.kt b/shared/feature/evaluate/src/commonMain/kotlin/com/kus/feature/evaluate/navigation/EvaluateNavGraph.kt index b720547c..617ee290 100644 --- a/shared/feature/evaluate/src/commonMain/kotlin/com/kus/feature/evaluate/navigation/EvaluateNavGraph.kt +++ b/shared/feature/evaluate/src/commonMain/kotlin/com/kus/feature/evaluate/navigation/EvaluateNavGraph.kt @@ -2,17 +2,45 @@ package com.kus.feature.evaluate.navigation import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable +import androidx.navigation.toRoute +import com.kus.feature.evaluate.model.EvaluateRestaurant +import com.kus.feature.evaluate.ui.EvaluateRoute import kotlinx.serialization.Serializable @Serializable -data object Evaluate +data class Evaluate( + val restaurantId: Long, + val restaurantName: String, + val mainTier: Int, + val restaurantCuisine: String, + val restaurantCuisineImgUrl: String, + val restaurantPosition: String, + val restaurantAddress: String, + val situationList: List, + val partnershipInfo: String, +) fun NavGraphBuilder.evaluateNavGraph( onBackClick: () -> Unit, + onSubmitSuccess: () -> Unit, ) { - composable { + composable { backStackEntry -> + val evaluate = backStackEntry.toRoute() EvaluateRoute( + restaurantId = evaluate.restaurantId, + restaurant = EvaluateRestaurant( + restaurantId = evaluate.restaurantId, + restaurantName = evaluate.restaurantName, + mainTier = evaluate.mainTier, + restaurantCuisine = evaluate.restaurantCuisine, + restaurantCuisineImgUrl = evaluate.restaurantCuisineImgUrl, + restaurantPosition = evaluate.restaurantPosition, + restaurantAddress = evaluate.restaurantAddress, + situationList = ArrayList(evaluate.situationList), + partnershipInfo = evaluate.partnershipInfo, + ), onBackClick = onBackClick, + onSubmitSuccess = onSubmitSuccess, ) } } diff --git a/shared/feature/evaluate/src/commonMain/kotlin/com/kus/feature/evaluate/navigation/EvaluateRoute.kt b/shared/feature/evaluate/src/commonMain/kotlin/com/kus/feature/evaluate/navigation/EvaluateRoute.kt deleted file mode 100644 index 7ee2480c..00000000 --- a/shared/feature/evaluate/src/commonMain/kotlin/com/kus/feature/evaluate/navigation/EvaluateRoute.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.kus.feature.evaluate.navigation - -import androidx.compose.runtime.Composable -import com.kus.feature.evaluate.ui.EvaluateScreen - -@Composable -fun EvaluateRoute( - onBackClick: () -> Unit, -) { - EvaluateScreen( - onBackClick = onBackClick, - ) -} diff --git a/shared/feature/evaluate/src/commonMain/kotlin/com/kus/feature/evaluate/state/EvaluateUiState.kt b/shared/feature/evaluate/src/commonMain/kotlin/com/kus/feature/evaluate/state/EvaluateUiState.kt new file mode 100644 index 00000000..04e8ce90 --- /dev/null +++ b/shared/feature/evaluate/src/commonMain/kotlin/com/kus/feature/evaluate/state/EvaluateUiState.kt @@ -0,0 +1,11 @@ +package com.kus.feature.evaluate.state + +import UiState +import com.kus.feature.evaluate.model.Evaluation +import com.kus.feature.evaluate.model.EvaluateRestaurant + +data class EvaluateUiState( + val restaurant: EvaluateRestaurant = EvaluateRestaurant.empty(), + val evaluation: UiState = UiState.Loading, + val submitState: UiState = UiState.Idle, +) diff --git a/shared/feature/evaluate/src/commonMain/kotlin/com/kus/feature/evaluate/ui/EvaluateContract.kt b/shared/feature/evaluate/src/commonMain/kotlin/com/kus/feature/evaluate/ui/EvaluateContract.kt deleted file mode 100644 index db831e8a..00000000 --- a/shared/feature/evaluate/src/commonMain/kotlin/com/kus/feature/evaluate/ui/EvaluateContract.kt +++ /dev/null @@ -1,57 +0,0 @@ -package com.kus.feature.evaluate.ui - -data class EvaluateUiState( - val restaurant: EvaluateRestaurant = EvaluateRestaurant.empty(), - val evaluation: Evaluation = Evaluation.empty(), -) - -data class EvaluateRestaurant( - val restaurantId: Int, - val restaurantName: String, - val mainTier: Int, - val restaurantCuisine: String, - val restaurantCuisineImgUrl: String, - val restaurantPosition: String, - val restaurantAddress: String, - val situationList: ArrayList, - val partnershipInfo: String, -) { - companion object { - fun empty() = EvaluateRestaurant( - restaurantId = 0, - restaurantName = "", - mainTier = 0, - restaurantCuisine = "", - restaurantCuisineImgUrl = "", - restaurantPosition = "", - restaurantAddress = "", - situationList = arrayListOf(), - partnershipInfo = "", - ) - } -} - -data class Evaluation( - val evaluationScore: Double, - val evaluationSituations: List, - val evaluationImgUrl: String, - val evaluationComment: String, - val starComments: List, - val imageBytes: ByteArray? = null, -) { - companion object { - fun empty() = Evaluation( - evaluationScore = 0.0, - evaluationSituations = emptyList(), - evaluationImgUrl = "", - evaluationComment = "", - starComments = emptyList(), - imageBytes = null, - ) - } -} - -data class StarComment( - val star: Double, - val comment: String, -) diff --git a/shared/feature/evaluate/src/commonMain/kotlin/com/kus/feature/evaluate/ui/EvaluateScreen.kt b/shared/feature/evaluate/src/commonMain/kotlin/com/kus/feature/evaluate/ui/EvaluateScreen.kt index 8cabd40c..c4087dfa 100644 --- a/shared/feature/evaluate/src/commonMain/kotlin/com/kus/feature/evaluate/ui/EvaluateScreen.kt +++ b/shared/feature/evaluate/src/commonMain/kotlin/com/kus/feature/evaluate/ui/EvaluateScreen.kt @@ -1,90 +1,210 @@ package com.kus.feature.evaluate.ui +import UiState import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel import com.kus.designsystem.component.KusButton import com.kus.designsystem.component.KusTopBar import com.kus.designsystem.theme.KusTheme -import com.kus.designsystem.util.noRippleClickable import com.kus.feature.evaluate.component.EvaluationImage import com.kus.feature.evaluate.component.EvaluationKeyword import com.kus.feature.evaluate.component.EvaluationRestInfoCard import com.kus.feature.evaluate.component.EvaluationReview import com.kus.feature.evaluate.component.EvaluationStar +import com.kus.feature.evaluate.model.Evaluation +import com.kus.feature.evaluate.model.EvaluateRestaurant import kustaurant.shared.core.designsystem.generated.resources.Res import kustaurant.shared.core.designsystem.generated.resources.ic_arrow_back import org.jetbrains.compose.resources.painterResource - +import org.koin.compose.viewmodel.koinViewModel @Composable -fun EvaluateScreen( +fun EvaluateRoute( + restaurantId: Long, + restaurant: EvaluateRestaurant, onBackClick: () -> Unit, - viewModel: EvaluateViewModel = viewModel { EvaluateViewModel() }, + onSubmitSuccess: () -> Unit, + viewModel: EvaluateViewModel = koinViewModel(), ) { val uiState by viewModel.uiState.collectAsState() - val restaurant = uiState.restaurant - val evaluation = uiState.evaluation - val isRatingSelected = evaluation.evaluationScore != 0.0 - val submitButtonColor = if (isRatingSelected) { - KusTheme.colors.c_43AB38 - } else { - KusTheme.colors.c_E0E0E0 + + LaunchedEffect(restaurantId) { + viewModel.initRestaurant(restaurant) + viewModel.getPreviousEvaluation(restaurantId) + } + + LaunchedEffect(uiState.submitState) { + if (uiState.submitState is UiState.Success) { + onSubmitSuccess() + } + } + + Column( + modifier = Modifier + .fillMaxSize() + .background(color = KusTheme.colors.c_FFFFFF) + ) { + EvaluateTopBar(onBackClick = onBackClick) + + Box( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + ) { + when (val evaluationState = uiState.evaluation) { + is UiState.Loading, UiState.Idle -> { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + CircularProgressIndicator(color = KusTheme.colors.c_43AB38) + } + } + + is UiState.Success -> { + EvaluateSuccessScreen( + restaurant = uiState.restaurant, + evaluation = evaluationState.data, + submitState = uiState.submitState, + onScoreChanged = { viewModel.updateEvaluationScore(it) }, + onSituationsChanged = { viewModel.updateEvaluationSituations(it) }, + onCommentChanged = { viewModel.updateEvaluationComment(it) }, + onImageSelected = { viewModel.updateImageBytes(it) }, + onSubmitClick = { viewModel.submitEvaluation() }, + ) + } + + is UiState.Failure -> { + EvaluateFailureScreen( + onRetryClick = { viewModel.getPreviousEvaluation(restaurantId) } + ) + } + } + } + } +} + +@Composable +private fun EvaluateTopBar(onBackClick: () -> Unit) { + Column( + modifier = Modifier + .fillMaxWidth() + ) { + KusTopBar( + leftIcon = painterResource(Res.drawable.ic_arrow_back), + onLeftClicked = onBackClick, + leftIconModifier = Modifier.padding(all = 5.dp), + iconTint = KusTheme.colors.c_000000, + modifier = Modifier + .fillMaxWidth() + .background(KusTheme.colors.c_FFFFFF), + content = { + Text( + text = "평가하기", + style = KusTheme.typography.type17sb + ) + } + ) + + HorizontalDivider( + thickness = 1.dp, + color = KusTheme.colors.c_EAEAEA + ) } +} + +@Composable +private fun EvaluateFailureScreen(onRetryClick: () -> Unit) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 20.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Text( + text = "서버 연결이 불안정합니다. 다시 시도해주세요.", + style = KusTheme.typography.type16sb, + color = KusTheme.colors.c_666666, + ) + + Box( + modifier = Modifier + .padding(top = 16.dp) + ) { + KusButton( + enabled = true, + buttonName = "다시 시도", + roundedCornerShape = RoundedCornerShape(50.dp), + onClick = onRetryClick, + ) + } + } +} + +@Composable +private fun EvaluateSuccessScreen( + restaurant: EvaluateRestaurant, + evaluation: Evaluation, + submitState: UiState, + onScoreChanged: (Double) -> Unit, + onSituationsChanged: (List) -> Unit, + onCommentChanged: (String) -> Unit, + onImageSelected: (ByteArray) -> Unit, + onSubmitClick: () -> Unit, +) { + val isRatingSelected = evaluation.evaluationScore != 0.0 + val isSubmitting = submitState is UiState.Loading + val submitButtonColor = if (isRatingSelected) KusTheme.colors.c_43AB38 else KusTheme.colors.c_E0E0E0 Box( - modifier = Modifier.fillMaxSize() + modifier = Modifier + .fillMaxSize() .background(color = KusTheme.colors.c_FFFFFF) ) { LazyColumn( modifier = Modifier.fillMaxWidth() ) { item { - EvaluationRestInfoCard( - restaurant = restaurant - ) + EvaluationRestInfoCard(restaurant = restaurant) } item { EvaluationStar( initialRating = evaluation.evaluationScore, - onRatingChanged = { newRating -> - viewModel.updateEvaluationScore(newRating) - } + onRatingChanged = onScoreChanged, ) } item { EvaluationKeyword( selectedSituations = evaluation.evaluationSituations, - onSituationChanged = { situation -> - viewModel.updateEvaluationSituations(situation) - - } + onSituationChanged = onSituationsChanged, ) } item { EvaluationReview( evaluationComment = evaluation.evaluationComment, - onCommentChange = { comment -> - viewModel.updateEvaluationComment(comment) - } + onCommentChange = onCommentChanged, ) } @@ -92,65 +212,36 @@ fun EvaluateScreen( EvaluationImage( imageUrl = evaluation.evaluationImgUrl, imageBytes = evaluation.imageBytes, - onImageSelected = { imageBytes -> - viewModel.updateImageBytes(imageBytes) - } + onImageSelected = onImageSelected, ) } item { Box( - modifier = Modifier.fillMaxWidth() + modifier = Modifier + .fillMaxWidth() .height(72.dp) ) } - - } - - Column( - modifier = Modifier - .fillMaxWidth() - .align(Alignment.TopCenter) - ) { - KusTopBar( - leftIcon = painterResource(Res.drawable.ic_arrow_back), - leftIconModifier = Modifier.noRippleClickable { onBackClick() } - .padding(all = 5.dp), - iconTint = KusTheme.colors.c_000000, - modifier = Modifier - .fillMaxWidth() - .background(KusTheme.colors.c_FFFFFF), - content = { - Text( - text = "평가하기", - style = KusTheme.typography.type17sb - ) - } - ) - - HorizontalDivider( - thickness = 1.dp, - color = KusTheme.colors.c_EAEAEA - ) } Box( - modifier = Modifier.background(color = KusTheme.colors.c_FFFFFF) + modifier = Modifier + .background(color = KusTheme.colors.c_FFFFFF) .padding(horizontal = 20.dp, vertical = 10.dp) .align(Alignment.BottomCenter), ) { KusButton( - enabled = isRatingSelected, - buttonName = "평가 제출하기", + enabled = isRatingSelected && !isSubmitting, + buttonName = if (isSubmitting) "제출 중..." else "평가 제출하기", roundedCornerShape = RoundedCornerShape(50.dp), containerColor = submitButtonColor, borderColor = submitButtonColor, onClick = { - if (!isRatingSelected) return@KusButton - viewModel.submitEvaluation() - onBackClick() - } + if (!isRatingSelected || isSubmitting) return@KusButton + onSubmitClick() + }, ) } } -} \ No newline at end of file +} diff --git a/shared/feature/evaluate/src/commonMain/kotlin/com/kus/feature/evaluate/ui/EvaluateViewModel.kt b/shared/feature/evaluate/src/commonMain/kotlin/com/kus/feature/evaluate/ui/EvaluateViewModel.kt index a0b523c3..64e1db68 100644 --- a/shared/feature/evaluate/src/commonMain/kotlin/com/kus/feature/evaluate/ui/EvaluateViewModel.kt +++ b/shared/feature/evaluate/src/commonMain/kotlin/com/kus/feature/evaluate/ui/EvaluateViewModel.kt @@ -1,96 +1,109 @@ package com.kus.feature.evaluate.ui +import UiError +import UiState import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.kus.feature.evaluate.model.Evaluation +import com.kus.feature.evaluate.model.EvaluateRestaurant +import com.kus.feature.evaluate.model.StarComment +import com.kus.feature.evaluate.state.EvaluateUiState +import com.kus.shared.domain.evaluate.usecase.GetEvaluationUseCase +import com.kus.shared.domain.evaluate.usecase.PostEvaluationUseCase import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch -class EvaluateViewModel : ViewModel() { +class EvaluateViewModel( + private val getEvaluationUseCase: GetEvaluationUseCase, + private val postEvaluationUseCase: PostEvaluationUseCase, +) : ViewModel() { private val _uiState = MutableStateFlow(EvaluateUiState()) val uiState: StateFlow = _uiState.asStateFlow() - init { - _uiState.value = EvaluateUiState( - restaurant = EvaluateRestaurant( - restaurantId = 1, - restaurantName = "제주곤이칼국수 건대점", - mainTier = 1, - restaurantCuisine = "한식", - restaurantCuisineImgUrl = "https://kustaurant.s3.ap-northeast-2.amazonaws.com/common/cuisine-icon/카페디저트.svg", - restaurantPosition = "건입~중문", - restaurantAddress = "서울시 광진구 어딘가 222-22, 304호", - situationList = arrayListOf("혼밥", "배달"), - partnershipInfo = "학생증 제시 시에 전메뉴 10% 할인 대박!!!! 학생증 제시 시에 전메뉴 10% 할인 대박!!!! 학생증 제시 시에 전메뉴 10% 할인 대박!!!! 학생증 제시 시에 전메뉴 10% 할인 대박!!!!" - ), - evaluation = Evaluation( - evaluationScore = 4.5, - evaluationSituations = listOf(1, 3, 7), - evaluationImgUrl = "https://picsum.photos/seed/evaluation1/400/300", - evaluationComment = "오 좀 맛있는데?", - starComments = listOf( - StarComment( - star = 4.5, - comment = "인생 최고의 식당입니다." - ), - StarComment( - star = 5.0, - comment = "다시 와도 좋을 것 같아요." - ), - StarComment( - star = 4.0, - comment = "가격 대비 훌륭합니다." + fun getPreviousEvaluation(restaurantId: Long) = viewModelScope.launch { + _uiState.update { it.copy(evaluation = UiState.Loading) } + runCatching { + getEvaluationUseCase(restaurantId) + }.onSuccess { previous -> + _uiState.update { + it.copy( + evaluation = UiState.Success( + Evaluation( + evaluationScore = previous.evaluationScore ?: 0.0, + evaluationSituations = previous.evaluationSituations, + evaluationImgUrl = previous.evaluationImgUrl ?: "", + evaluationComment = previous.evaluationComment ?: "", + starComments = previous.starComments.map { StarComment(it.star, it.comment) }, + imageBytes = null, + ) ) ) - ) - ) + } + }.onFailure { + _uiState.update { it.copy(evaluation = UiState.Failure(UiError.Network)) } + } + } + + fun initRestaurant(restaurant: EvaluateRestaurant) { + _uiState.update { it.copy(restaurant = restaurant) } + } + + private fun updateEvaluation(transform: (Evaluation) -> Evaluation) { + val current = _uiState.value.evaluation + if (current !is UiState.Success) return + _uiState.update { it.copy(evaluation = UiState.Success(transform(current.data))) } } fun updateEvaluationScore(score: Double) { - _uiState.value = _uiState.value.copy( - evaluation = _uiState.value.evaluation.copy(evaluationScore = score) - ) + updateEvaluation { it.copy(evaluationScore = score) } } fun updateEvaluationComment(comment: String) { - _uiState.value = _uiState.value.copy( - evaluation = _uiState.value.evaluation.copy(evaluationComment = comment) - ) + updateEvaluation { it.copy(evaluationComment = comment) } } fun updateEvaluationSituations(situations: List) { - _uiState.value = _uiState.value.copy( - evaluation = _uiState.value.evaluation.copy(evaluationSituations = situations) - ) + updateEvaluation { it.copy(evaluationSituations = situations) } } fun updateEvaluationImage(imageUrl: String) { - _uiState.value = _uiState.value.copy( - evaluation = _uiState.value.evaluation.copy(evaluationImgUrl = imageUrl) - ) + updateEvaluation { it.copy(evaluationImgUrl = imageUrl) } } fun updateImageBytes(imageBytes: ByteArray) { - _uiState.value = _uiState.value.copy( - evaluation = _uiState.value.evaluation.copy(imageBytes = imageBytes.copyOf()) - ) + updateEvaluation { it.copy(imageBytes = imageBytes.copyOf()) } } fun addStarComment(starComment: StarComment) { - val updatedComments = _uiState.value.evaluation.starComments + starComment - _uiState.value = _uiState.value.copy( - evaluation = _uiState.value.evaluation.copy(starComments = updatedComments) - ) + updateEvaluation { it.copy(starComments = it.starComments + starComment) } } fun removeStarComment(index: Int) { - val updatedComments = _uiState.value.evaluation.starComments.filterIndexed { i, _ -> i != index } - _uiState.value = _uiState.value.copy( - evaluation = _uiState.value.evaluation.copy(starComments = updatedComments) - ) + updateEvaluation { it.copy(starComments = it.starComments.filterIndexed { i, _ -> i != index }) } } - fun submitEvaluation() { - // TODO: API 호출 로직 구현 - // 평가 데이터를 서버로 전송 + fun submitEvaluation() = viewModelScope.launch { + val evaluationState = _uiState.value.evaluation + if (evaluationState !is UiState.Success) return@launch + val evaluation = evaluationState.data + val restaurantId = _uiState.value.restaurant.restaurantId.toLong() + + _uiState.update { it.copy(submitState = UiState.Loading) } + runCatching { + postEvaluationUseCase( + restaurantId = restaurantId, + evaluationScore = evaluation.evaluationScore, + evaluationSituations = evaluation.evaluationSituations.takeIf { it.isNotEmpty() }, + evaluationComment = evaluation.evaluationComment.takeIf { it.isNotBlank() }, + imageBytes = evaluation.imageBytes, + ) + }.onSuccess { + _uiState.update { it.copy(submitState = UiState.Success(Unit)) } + }.onFailure { + _uiState.update { it.copy(submitState = UiState.Failure(UiError.Network)) } + } } } diff --git a/shared/feature/tier/src/commonMain/kotlin/com/kus/feature/tier/navigation/TierGraph.kt b/shared/feature/tier/src/commonMain/kotlin/com/kus/feature/tier/navigation/TierGraph.kt index 3176bfdc..2a3c21ed 100644 --- a/shared/feature/tier/src/commonMain/kotlin/com/kus/feature/tier/navigation/TierGraph.kt +++ b/shared/feature/tier/src/commonMain/kotlin/com/kus/feature/tier/navigation/TierGraph.kt @@ -20,7 +20,7 @@ fun NavGraphBuilder.tierNavGraph( onShowMessage: (String) -> Unit, initialProvider: () -> TierFilterState, navigateToTierCategorySelect: (TierFilterState) -> Unit, - navigateToDetail: () -> Unit, + navigateToDetail: (Long) -> Unit, popBackStackWithResult: (TierFilterState) -> Unit, onBackButtonClick: () -> Unit = {}, ) { diff --git a/shared/feature/tier/src/commonMain/kotlin/com/kus/feature/tier/navigation/TierRoute.kt b/shared/feature/tier/src/commonMain/kotlin/com/kus/feature/tier/navigation/TierRoute.kt index c73199ab..4ec5e2c9 100644 --- a/shared/feature/tier/src/commonMain/kotlin/com/kus/feature/tier/navigation/TierRoute.kt +++ b/shared/feature/tier/src/commonMain/kotlin/com/kus/feature/tier/navigation/TierRoute.kt @@ -14,7 +14,7 @@ import org.koin.compose.viewmodel.koinViewModel fun TierRoute( navigateToTierCategorySelect: (TierFilterState) -> Unit, initialFilter: () -> TierFilterState, - navigateToDetail: () -> Unit, + navigateToDetail: (Long) -> Unit, resultFilter: TierFilterState?, consumeResult: () -> Unit, onShowMessage: (String) -> Unit, diff --git a/shared/feature/tier/src/commonMain/kotlin/com/kus/feature/tier/ui/TierScreen.kt b/shared/feature/tier/src/commonMain/kotlin/com/kus/feature/tier/ui/TierScreen.kt index 8f6a4e47..203b557d 100644 --- a/shared/feature/tier/src/commonMain/kotlin/com/kus/feature/tier/ui/TierScreen.kt +++ b/shared/feature/tier/src/commonMain/kotlin/com/kus/feature/tier/ui/TierScreen.kt @@ -77,7 +77,7 @@ fun TierScreen( onAlarmClick: () -> Unit = {}, onFilterClick: () -> Unit = {}, onShowMessage: (String) -> Unit, - onNavigateRestaurantDetail: () -> Unit = {}, + onNavigateRestaurantDetail: (Long) -> Unit = {}, ) { val tabs = remember { TierTab.entries } val pagerState = rememberPagerState { tabs.size } @@ -90,7 +90,7 @@ fun TierScreen( LaunchedEffect(Unit) { viewModel.fetchFirstRestaurants() - } + } LaunchedEffect(uiState.toastMessage) { uiState.toastMessage?.let { @@ -134,7 +134,7 @@ fun TierScreen( TierListScreen( viewModel = viewModel, listState = listState, - onRestaurantClick = { onNavigateRestaurantDetail() }, + onRestaurantClick = { restaurant -> onNavigateRestaurantDetail(restaurant.restaurantId) }, modifier = Modifier .fillMaxSize() .padding(top = 44.dp)