diff --git a/Prezel/app/build.gradle.kts b/Prezel/app/build.gradle.kts index f4751910..6f0a4a50 100644 --- a/Prezel/app/build.gradle.kts +++ b/Prezel/app/build.gradle.kts @@ -61,6 +61,8 @@ dependencies { implementation(projects.featureHomeImpl) implementation(projects.featureHistoryApi) implementation(projects.featureHistoryImpl) + implementation(projects.featureFeedbackApi) + implementation(projects.featureFeedbackImpl) implementation(projects.featureMyApi) implementation(projects.featureMyImpl) diff --git a/Prezel/core/data/src/main/java/com/team/prezel/core/data/error/AppErrorExt.kt b/Prezel/core/data/src/main/java/com/team/prezel/core/data/error/AppErrorExt.kt index 8e94c8b9..eac35399 100644 --- a/Prezel/core/data/src/main/java/com/team/prezel/core/data/error/AppErrorExt.kt +++ b/Prezel/core/data/src/main/java/com/team/prezel/core/data/error/AppErrorExt.kt @@ -57,13 +57,17 @@ private fun ServerErrorCode.toDomainError(): AppError = ServerErrorCode.TERMS_NOT_FOUND, ServerErrorCode.PRESENTATION_NOT_FOUND, + ServerErrorCode.PRESENTATION_REVIEW_NOT_FOUND, ServerErrorCode.ANALYSIS_RESULT_NOT_FOUND, -> AppError.NOT_FOUND - ServerErrorCode.DUPLICATE_NICKNAME -> AppError.DUPLICATE + ServerErrorCode.DUPLICATE_NICKNAME, + ServerErrorCode.SELF_FEEDBACK_ALREADY_WRITTEN, + -> AppError.DUPLICATE ServerErrorCode.UNAUTHORIZED, ServerErrorCode.FORBIDDEN, + ServerErrorCode.PRESENTATION_REVIEW_FORBIDDEN, ServerErrorCode.INVALID_TOKEN, ServerErrorCode.TOKEN_STOLEN, ServerErrorCode.USER_NOT_FOUND, diff --git a/Prezel/core/data/src/main/java/com/team/prezel/core/data/mapper/PresentationMapper.kt b/Prezel/core/data/src/main/java/com/team/prezel/core/data/mapper/PresentationMapper.kt index 085758ec..6d71c932 100644 --- a/Prezel/core/data/src/main/java/com/team/prezel/core/data/mapper/PresentationMapper.kt +++ b/Prezel/core/data/src/main/java/com/team/prezel/core/data/mapper/PresentationMapper.kt @@ -47,7 +47,7 @@ internal fun PresentationSummaryResponse.toDomain(): PresentationAnalysisSummary spellErrorCount = spellErrorCount, grammarErrorCount = grammarErrorCount, totalErrorCount = totalErrorCount, - growth = growthGraph.map { item -> item.toDomain() }, + growth = growthGraph?.map { item -> item.toDomain() }.orEmpty(), expectedQuestions = expectedQuestions.map { item -> item.toDomain() }, selfFeedback = reviewContent, ) diff --git a/Prezel/core/data/src/main/java/com/team/prezel/core/data/repository/PresentationRepositoryImpl.kt b/Prezel/core/data/src/main/java/com/team/prezel/core/data/repository/PresentationRepositoryImpl.kt index 0787871e..e1e2e491 100644 --- a/Prezel/core/data/src/main/java/com/team/prezel/core/data/repository/PresentationRepositoryImpl.kt +++ b/Prezel/core/data/src/main/java/com/team/prezel/core/data/repository/PresentationRepositoryImpl.kt @@ -125,4 +125,15 @@ internal class PresentationRepositoryImpl @Inject constructor( }.mapCatching { response -> response.map(GetMainDataResponse::toDomain) }.mapDomainFailure() + + override suspend fun writeSelfFeedback( + presentationId: Long, + content: String, + ): Result = + runCatching { + presentationRemoteDataSource.writeSelfFeedback( + presentationId = presentationId, + content = content, + ) + }.mapDomainFailure() } diff --git a/Prezel/core/domain/src/main/kotlin/com/team/prezel/core/domain/repository/presentation/PresentationRepository.kt b/Prezel/core/domain/src/main/kotlin/com/team/prezel/core/domain/repository/presentation/PresentationRepository.kt index 6e0e3b3e..9379e68f 100644 --- a/Prezel/core/domain/src/main/kotlin/com/team/prezel/core/domain/repository/presentation/PresentationRepository.kt +++ b/Prezel/core/domain/src/main/kotlin/com/team/prezel/core/domain/repository/presentation/PresentationRepository.kt @@ -48,4 +48,9 @@ interface PresentationRepository { suspend fun getPracticeRecords(presentationId: Long): Result suspend fun getMainData(): Result> + + suspend fun writeSelfFeedback( + presentationId: Long, + content: String, + ): Result } diff --git a/Prezel/core/domain/src/main/kotlin/com/team/prezel/core/domain/usecase/presentation/WriteSelfFeedbackUseCase.kt b/Prezel/core/domain/src/main/kotlin/com/team/prezel/core/domain/usecase/presentation/WriteSelfFeedbackUseCase.kt new file mode 100644 index 00000000..ccf93568 --- /dev/null +++ b/Prezel/core/domain/src/main/kotlin/com/team/prezel/core/domain/usecase/presentation/WriteSelfFeedbackUseCase.kt @@ -0,0 +1,17 @@ +package com.team.prezel.core.domain.usecase.presentation + +import com.team.prezel.core.domain.repository.presentation.PresentationRepository +import javax.inject.Inject + +class WriteSelfFeedbackUseCase @Inject constructor( + private val presentationRepository: PresentationRepository, +) { + suspend operator fun invoke( + presentationId: Long, + content: String, + ): Result = + presentationRepository.writeSelfFeedback( + presentationId = presentationId, + content = content, + ) +} diff --git a/Prezel/core/network/src/main/java/com/team/prezel/core/network/datasource/PresentationRemoteDataSource.kt b/Prezel/core/network/src/main/java/com/team/prezel/core/network/datasource/PresentationRemoteDataSource.kt index 6ab70f33..3d5fb6e0 100644 --- a/Prezel/core/network/src/main/java/com/team/prezel/core/network/datasource/PresentationRemoteDataSource.kt +++ b/Prezel/core/network/src/main/java/com/team/prezel/core/network/datasource/PresentationRemoteDataSource.kt @@ -44,4 +44,9 @@ interface PresentationRemoteDataSource { suspend fun getPracticeRecords(presentationId: Long): GetPracticeRecordsResponse suspend fun getMainData(): List + + suspend fun writeSelfFeedback( + presentationId: Long, + content: String, + ) } diff --git a/Prezel/core/network/src/main/java/com/team/prezel/core/network/datasource/PresentationRemoteDataSourceImpl.kt b/Prezel/core/network/src/main/java/com/team/prezel/core/network/datasource/PresentationRemoteDataSourceImpl.kt index ec62b5cf..3fd9c211 100644 --- a/Prezel/core/network/src/main/java/com/team/prezel/core/network/datasource/PresentationRemoteDataSourceImpl.kt +++ b/Prezel/core/network/src/main/java/com/team/prezel/core/network/datasource/PresentationRemoteDataSourceImpl.kt @@ -2,10 +2,12 @@ package com.team.prezel.core.network.datasource import com.team.prezel.core.network.model.presentation.GetMainDataResponse import com.team.prezel.core.network.model.presentation.GetPracticeRecordsResponse +import com.team.prezel.core.network.model.presentation.GetPresentationDetailResponse import com.team.prezel.core.network.model.presentation.GetPresentationsResponse import com.team.prezel.core.network.model.presentation.PresentationScriptDetailResponse import com.team.prezel.core.network.model.presentation.PresentationSummaryResponse import com.team.prezel.core.network.model.presentation.PresentationWordDetailResponse +import com.team.prezel.core.network.model.presentation.review.SelfFeedbackRequest import com.team.prezel.core.network.model.requireData import com.team.prezel.core.network.model.requireSuccess import com.team.prezel.core.network.service.PresentationService @@ -96,15 +98,26 @@ internal class PresentationRemoteDataSourceImpl @Inject constructor( override suspend fun getPastPresentations(): List = presentationService.getPastPresentations().requireData() override suspend fun getUpcomingPresentationDetail(presentationId: Long): PresentationSummaryResponse = - presentationService.getUpcomingPresentationDetail(presentationId = presentationId).requireData().analysisResult + presentationService.getUpcomingPresentationDetail(presentationId = presentationId).requireData().toPresentationSummaryResponse() override suspend fun getPastPresentationDetail(presentationId: Long): PresentationSummaryResponse = - presentationService.getPastPresentationDetail(presentationId = presentationId).requireData().analysisResult + presentationService.getPastPresentationDetail(presentationId = presentationId).requireData().toPresentationSummaryResponse() override suspend fun getPracticeRecords(presentationId: Long): GetPracticeRecordsResponse = presentationService.getPracticeRecords(presentationId = presentationId).requireData() override suspend fun getMainData(): List = presentationService.getMainData().requireData() + + override suspend fun writeSelfFeedback( + presentationId: Long, + content: String, + ) { + presentationService + .writeSelfFeedback( + presentationId = presentationId, + request = SelfFeedbackRequest(content = content), + ).requireSuccess() + } } private fun FormBuilder.appendAudioPart(audioFilePath: String) { @@ -119,6 +132,11 @@ private fun FormBuilder.appendAudioPart(audioFilePath: String) { ) } +private fun GetPresentationDetailResponse.toPresentationSummaryResponse(): PresentationSummaryResponse = + analysisResult.copy( + reviewContent = reviewContent ?: analysisResult.reviewContent, + ) + private fun FormBuilder.appendScriptPart(scriptFilePath: String) { val file = File(scriptFilePath) diff --git a/Prezel/core/network/src/main/java/com/team/prezel/core/network/model/BaseResponse.kt b/Prezel/core/network/src/main/java/com/team/prezel/core/network/model/BaseResponse.kt index e6a18ef6..6037d95b 100644 --- a/Prezel/core/network/src/main/java/com/team/prezel/core/network/model/BaseResponse.kt +++ b/Prezel/core/network/src/main/java/com/team/prezel/core/network/model/BaseResponse.kt @@ -55,9 +55,12 @@ enum class ServerErrorCode( INVALID_ID_TOKEN("T003"), TERMS_NOT_FOUND("TR001"), REQUIRED_TERMS_DISAGREED("TR002"), + SELF_FEEDBACK_ALREADY_WRITTEN("R002"), FILE_IS_EMPTY("F001"), FILE_UPLOAD_FAILED("F002"), UNSUPPORTED_FILE_FORMAT("F004"), + PRESENTATION_REVIEW_NOT_FOUND("PR001"), + PRESENTATION_REVIEW_FORBIDDEN("PR002"), PRESENTATION_NOT_FOUND("P001"), ANALYSIS_RESULT_NOT_FOUND("A001"), SENTENCE_NOT_FOUND("S001"), diff --git a/Prezel/core/network/src/main/java/com/team/prezel/core/network/model/presentation/GetPresentationDetailResponse.kt b/Prezel/core/network/src/main/java/com/team/prezel/core/network/model/presentation/GetPresentationDetailResponse.kt index 2a78412c..741590e4 100644 --- a/Prezel/core/network/src/main/java/com/team/prezel/core/network/model/presentation/GetPresentationDetailResponse.kt +++ b/Prezel/core/network/src/main/java/com/team/prezel/core/network/model/presentation/GetPresentationDetailResponse.kt @@ -7,4 +7,6 @@ import kotlinx.serialization.Serializable data class GetPresentationDetailResponse( @SerialName("analysisResult") val analysisResult: PresentationSummaryResponse, + @SerialName("reviewContent") + val reviewContent: String? = null, ) diff --git a/Prezel/core/network/src/main/java/com/team/prezel/core/network/model/presentation/PresentationSummaryResponse.kt b/Prezel/core/network/src/main/java/com/team/prezel/core/network/model/presentation/PresentationSummaryResponse.kt index 54876839..6784a02c 100644 --- a/Prezel/core/network/src/main/java/com/team/prezel/core/network/model/presentation/PresentationSummaryResponse.kt +++ b/Prezel/core/network/src/main/java/com/team/prezel/core/network/model/presentation/PresentationSummaryResponse.kt @@ -42,7 +42,7 @@ data class PresentationSummaryResponse( @SerialName("totalErrorCount") val totalErrorCount: Int, @SerialName("growthGraph") - val growthGraph: List, + val growthGraph: List? = null, @SerialName("expectedQuestions") val expectedQuestions: List, @SerialName("reviewContent") diff --git a/Prezel/core/network/src/main/java/com/team/prezel/core/network/model/presentation/review/SelfFeedbackRequest.kt b/Prezel/core/network/src/main/java/com/team/prezel/core/network/model/presentation/review/SelfFeedbackRequest.kt new file mode 100644 index 00000000..9eb77ac5 --- /dev/null +++ b/Prezel/core/network/src/main/java/com/team/prezel/core/network/model/presentation/review/SelfFeedbackRequest.kt @@ -0,0 +1,10 @@ +package com.team.prezel.core.network.model.presentation.review + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class SelfFeedbackRequest( + @SerialName("content") + val content: String, +) diff --git a/Prezel/core/network/src/main/java/com/team/prezel/core/network/service/PresentationService.kt b/Prezel/core/network/src/main/java/com/team/prezel/core/network/service/PresentationService.kt index ed5fbfa5..ddf1a13d 100644 --- a/Prezel/core/network/src/main/java/com/team/prezel/core/network/service/PresentationService.kt +++ b/Prezel/core/network/src/main/java/com/team/prezel/core/network/service/PresentationService.kt @@ -8,6 +8,7 @@ import com.team.prezel.core.network.model.presentation.GetPresentationsResponse import com.team.prezel.core.network.model.presentation.PresentationScriptDetailResponse import com.team.prezel.core.network.model.presentation.PresentationSummaryResponse import com.team.prezel.core.network.model.presentation.PresentationWordDetailResponse +import com.team.prezel.core.network.model.presentation.review.SelfFeedbackRequest import de.jensklingenberg.ktorfit.http.Body import de.jensklingenberg.ktorfit.http.DELETE import de.jensklingenberg.ktorfit.http.GET @@ -65,4 +66,10 @@ interface PresentationService { @GET("main") suspend fun getMainData(): BaseResponse> + + @POST("recording/{presentationId}/review") + suspend fun writeSelfFeedback( + @Path("presentationId") presentationId: Long, + @Body request: SelfFeedbackRequest, + ): BaseResponse } diff --git a/Prezel/detekt-config.yml b/Prezel/detekt-config.yml index 5589ae0d..9c9dbffc 100644 --- a/Prezel/detekt-config.yml +++ b/Prezel/detekt-config.yml @@ -37,7 +37,7 @@ complexity: active: true thresholdInFiles: 20 thresholdInClasses: 15 - thresholdInInterfaces: 12 + thresholdInInterfaces: 15 thresholdInObjects: 20 thresholdInEnums: 10 ignoreAnnotatedFunctions: diff --git a/Prezel/feature/analysis/impl/src/main/java/com/team/prezel/feature/analysis/impl/navigation/AnalysisEntryBuilder.kt b/Prezel/feature/analysis/impl/src/main/java/com/team/prezel/feature/analysis/impl/navigation/AnalysisEntryBuilder.kt index 01e480fc..e1cb486b 100644 --- a/Prezel/feature/analysis/impl/src/main/java/com/team/prezel/feature/analysis/impl/navigation/AnalysisEntryBuilder.kt +++ b/Prezel/feature/analysis/impl/src/main/java/com/team/prezel/feature/analysis/impl/navigation/AnalysisEntryBuilder.kt @@ -21,7 +21,6 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.components.ActivityRetainedComponent import dagger.multibindings.IntoSet -import java.util.UUID internal fun EntryProviderScope.featureAnalysisEntryBuilder() { analysisEntry { key -> key.enterStep(AnalysisFlowStep.PRESENTATION_SCHEDULE) } @@ -84,7 +83,6 @@ private fun AnalysisRoute( navigator.navigate( key = ReportNavKey( presentationId = presentationId, - refreshKey = newReportRefreshKey(), ), clearStack = true, ) @@ -127,5 +125,3 @@ object FeatureAnalysisModule { featureAnalysisEntryBuilder() } } - -private fun newReportRefreshKey(): String = UUID.randomUUID().toString() diff --git a/Prezel/feature/feedback/api/build.gradle.kts b/Prezel/feature/feedback/api/build.gradle.kts new file mode 100644 index 00000000..cc170aa8 --- /dev/null +++ b/Prezel/feature/feedback/api/build.gradle.kts @@ -0,0 +1,7 @@ +plugins { + alias(libs.plugins.prezel.android.feature.api) +} + +android { + namespace = "com.team.prezel.feature.feedback.api" +} diff --git a/Prezel/feature/feedback/api/consumer-rules.pro b/Prezel/feature/feedback/api/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/Prezel/feature/feedback/api/proguard-rules.pro b/Prezel/feature/feedback/api/proguard-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/Prezel/feature/feedback/api/src/main/java/com/team/prezel/feature/feedback/api/FeedbackNavKey.kt b/Prezel/feature/feedback/api/src/main/java/com/team/prezel/feature/feedback/api/FeedbackNavKey.kt new file mode 100644 index 00000000..7df01fc0 --- /dev/null +++ b/Prezel/feature/feedback/api/src/main/java/com/team/prezel/feature/feedback/api/FeedbackNavKey.kt @@ -0,0 +1,11 @@ +package com.team.prezel.feature.feedback.api + +import androidx.navigation3.runtime.NavKey +import kotlinx.serialization.Serializable + +@Serializable +data class FeedbackNavKey( + val presentationId: Long, + val title: String, + val isPast: Boolean = false, +) : NavKey diff --git a/Prezel/feature/feedback/api/src/main/res/values/strings.xml b/Prezel/feature/feedback/api/src/main/res/values/strings.xml new file mode 100644 index 00000000..545704f2 --- /dev/null +++ b/Prezel/feature/feedback/api/src/main/res/values/strings.xml @@ -0,0 +1,2 @@ + + diff --git a/Prezel/feature/feedback/impl/build.gradle.kts b/Prezel/feature/feedback/impl/build.gradle.kts new file mode 100644 index 00000000..024a012a --- /dev/null +++ b/Prezel/feature/feedback/impl/build.gradle.kts @@ -0,0 +1,15 @@ +plugins { + alias(libs.plugins.prezel.android.feature.impl) +} + +android { + namespace = "com.team.prezel.feature.feedback.impl" +} + +dependencies { + implementation(projects.coreCommon) + implementation(projects.coreModel) + implementation(projects.coreDomain) + + implementation(projects.featureFeedbackApi) +} diff --git a/Prezel/feature/feedback/impl/consumer-rules.pro b/Prezel/feature/feedback/impl/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/Prezel/feature/feedback/impl/proguard-rules.pro b/Prezel/feature/feedback/impl/proguard-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/Prezel/feature/feedback/impl/src/main/java/com/team/prezel/feature/feedback/impl/FeedbackScreen.kt b/Prezel/feature/feedback/impl/src/main/java/com/team/prezel/feature/feedback/impl/FeedbackScreen.kt new file mode 100644 index 00000000..04bf67ee --- /dev/null +++ b/Prezel/feature/feedback/impl/src/main/java/com/team/prezel/feature/feedback/impl/FeedbackScreen.kt @@ -0,0 +1,250 @@ +package com.team.prezel.feature.feedback.impl + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.platform.LocalResources +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.team.prezel.core.designsystem.component.PrezelTopAppBar +import com.team.prezel.core.designsystem.component.actions.area.PrezelButtonArea +import com.team.prezel.core.designsystem.component.actions.button.PrezelButton +import com.team.prezel.core.designsystem.component.actions.button.config.ButtonHierarchy +import com.team.prezel.core.designsystem.component.actions.button.config.ButtonType +import com.team.prezel.core.designsystem.component.feedback.dialog.PrezelDialog +import com.team.prezel.core.designsystem.component.feedback.dialog.PrezelDialogScope.ActionType +import com.team.prezel.core.designsystem.component.feedback.snackbar.showPrezelSnackbar +import com.team.prezel.core.designsystem.component.textfield.PrezelTextArea +import com.team.prezel.core.designsystem.icon.PrezelIcons +import com.team.prezel.core.designsystem.preview.BasicPreview +import com.team.prezel.core.designsystem.theme.PrezelTheme +import com.team.prezel.core.ui.state.LocalSnackbarHostState +import com.team.prezel.core.ui.util.advancedImePadding +import com.team.prezel.feature.feedback.impl.contract.FeedbackUiEffect +import com.team.prezel.feature.feedback.impl.contract.FeedbackUiIntent +import com.team.prezel.feature.feedback.impl.contract.FeedbackUiState +import com.team.prezel.feature.feedback.impl.model.FeedbackUiMessage + +@Composable +internal fun FeedbackScreen( + title: String, + navigateBack: () -> Unit, + onSaveComplete: () -> Unit, + modifier: Modifier = Modifier, + viewModel: FeedbackViewModel = hiltViewModel(), +) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val snackbarHostState = LocalSnackbarHostState.current + val resources = LocalResources.current + + LaunchedEffect(Unit) { + viewModel.uiEffect.collect { effect -> + when (effect) { + FeedbackUiEffect.NavigateBack -> navigateBack() + FeedbackUiEffect.SaveComplete -> onSaveComplete() + is FeedbackUiEffect.ShowMessage -> { + val resId = when (effect.message) { + FeedbackUiMessage.FETCH_FEEDBACK_FAILED -> + R.string.feature_feedback_impl_fetch_feedback_failed + FeedbackUiMessage.PRESENTATION_FORBIDDEN -> + R.string.feature_feedback_impl_presentation_forbidden + FeedbackUiMessage.PRESENTATION_NOT_FOUND -> + R.string.feature_feedback_impl_presentation_not_found + FeedbackUiMessage.SELF_FEEDBACK_ALREADY_WRITTEN -> + R.string.feature_feedback_impl_self_feedback_already_written + FeedbackUiMessage.SAVE_FAILED -> R.string.feature_feedback_impl_save_failed + } + snackbarHostState.showPrezelSnackbar( + message = resources.getString(resId), + ) + } + } + } + } + + BackHandler { + viewModel.onIntent(FeedbackUiIntent.ClickClose) + } + + FeedbackScreen( + title = title, + uiState = uiState, + onContentChanged = { content -> viewModel.onIntent(FeedbackUiIntent.ChangeContent(content)) }, + onClickClose = { viewModel.onIntent(FeedbackUiIntent.ClickClose) }, + onClickSave = { viewModel.onIntent(FeedbackUiIntent.ClickSave) }, + onDismissExitDialog = { viewModel.onIntent(FeedbackUiIntent.ClickDialogClose) }, + onConfirmExit = { viewModel.onIntent(FeedbackUiIntent.ClickDialogExit) }, + modifier = modifier, + ) +} + +@Composable +private fun FeedbackScreen( + title: String, + uiState: FeedbackUiState, + onContentChanged: (String) -> Unit, + onClickClose: () -> Unit, + onClickSave: () -> Unit, + onDismissExitDialog: () -> Unit, + onConfirmExit: () -> Unit, + modifier: Modifier = Modifier, +) { + if (uiState.isExitDialogVisible) { + FeedbackExitDialog( + onDismiss = onDismissExitDialog, + onConfirmExit = onConfirmExit, + ) + } + + Column( + modifier = modifier + .fillMaxSize() + .background(PrezelTheme.colors.bgRegular), + ) { + FeedbackTopAppBar(onClickClose = onClickClose) + + FeedbackContent( + title = title, + content = uiState.content, + onContentChanged = onContentChanged, + modifier = Modifier.weight(1f), + ) + + PrezelButtonArea( + modifier = Modifier.advancedImePadding(), + mainButton = { buttonModifier -> + PrezelButton( + text = stringResource(R.string.feature_feedback_impl_save), + onClick = onClickSave, + enabled = uiState.isSaveEnabled, + type = ButtonType.FILLED, + hierarchy = ButtonHierarchy.PRIMARY, + modifier = buttonModifier, + ) + }, + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun FeedbackTopAppBar( + onClickClose: () -> Unit, + modifier: Modifier = Modifier, +) { + PrezelTopAppBar( + title = { Text(text = stringResource(R.string.feature_feedback_impl_top_app_bar_title)) }, + trailingIcons = { + IconButton(onClick = onClickClose) { + Icon( + painter = painterResource(PrezelIcons.Cancel), + contentDescription = stringResource(R.string.feature_feedback_impl_close), + tint = PrezelTheme.colors.iconRegular, + ) + } + }, + modifier = modifier, + ) +} + +@Composable +private fun FeedbackContent( + title: String, + content: String, + onContentChanged: (String) -> Unit, + modifier: Modifier = Modifier, +) { + val focusManager = LocalFocusManager.current + + Column( + modifier = modifier + .fillMaxWidth() + .pointerInput(Unit) { detectTapGestures(onTap = { focusManager.clearFocus() }) } + .padding(horizontal = PrezelTheme.spacing.V20) + .padding(top = PrezelTheme.spacing.V32), + ) { + Text( + text = stringResource(R.string.feature_feedback_impl_question, title), + style = PrezelTheme.typography.title2Bold, + color = PrezelTheme.colors.textLarge, + ) + + Spacer(modifier = Modifier.height(PrezelTheme.spacing.V16)) + + PrezelTextArea( + value = content, + onValueChange = onContentChanged, + placeholder = stringResource(R.string.feature_feedback_impl_placeholder), + maxLength = 200, + showCount = true, + ) + } +} + +@Composable +private fun FeedbackExitDialog( + onDismiss: () -> Unit, + onConfirmExit: () -> Unit, +) { + PrezelDialog( + title = stringResource(R.string.feature_feedback_impl_exit_dialog_title), + description = stringResource(R.string.feature_feedback_impl_exit_dialog_description), + onDismiss = onDismiss, + ) { + Action(label = stringResource(R.string.feature_feedback_impl_exit_dialog_cancel)) { onDismiss() } + Action( + label = stringResource(R.string.feature_feedback_impl_exit_dialog_confirm), + type = ActionType.BAD, + ) { + onConfirmExit() + } + } +} + +@BasicPreview +@Composable +private fun FeedbackScreenPreview() { + PrezelTheme { + FeedbackScreen( + title = "제 7회 컨셉발표회", + uiState = FeedbackUiState(), + onContentChanged = {}, + onClickClose = {}, + onClickSave = {}, + onDismissExitDialog = {}, + onConfirmExit = {}, + ) + } +} + +@BasicPreview +@Composable +private fun FeedbackExitDialogPreview() { + PrezelTheme { + Box(modifier = Modifier.fillMaxSize()) { + FeedbackExitDialog( + onDismiss = {}, + onConfirmExit = {}, + ) + } + } +} diff --git a/Prezel/feature/feedback/impl/src/main/java/com/team/prezel/feature/feedback/impl/FeedbackViewModel.kt b/Prezel/feature/feedback/impl/src/main/java/com/team/prezel/feature/feedback/impl/FeedbackViewModel.kt new file mode 100644 index 00000000..c63a91b7 --- /dev/null +++ b/Prezel/feature/feedback/impl/src/main/java/com/team/prezel/feature/feedback/impl/FeedbackViewModel.kt @@ -0,0 +1,126 @@ +package com.team.prezel.feature.feedback.impl + +import androidx.lifecycle.viewModelScope +import com.team.prezel.core.common.error.AppError +import com.team.prezel.core.common.error.AppException +import com.team.prezel.core.domain.usecase.presentation.FetchPresentationDetailUseCase +import com.team.prezel.core.domain.usecase.presentation.WriteSelfFeedbackUseCase +import com.team.prezel.core.ui.base.BaseViewModel +import com.team.prezel.feature.feedback.api.FeedbackNavKey +import com.team.prezel.feature.feedback.impl.contract.FeedbackUiEffect +import com.team.prezel.feature.feedback.impl.contract.FeedbackUiIntent +import com.team.prezel.feature.feedback.impl.contract.FeedbackUiState +import com.team.prezel.feature.feedback.impl.model.FeedbackUiMessage +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import timber.log.Timber + +@HiltViewModel(assistedFactory = FeedbackViewModel.Factory::class) +internal class FeedbackViewModel @AssistedInject constructor( + @Assisted private val navKey: FeedbackNavKey, + private val fetchPresentationDetailUseCase: FetchPresentationDetailUseCase, + private val writeSelfFeedbackUseCase: WriteSelfFeedbackUseCase, +) : BaseViewModel(FeedbackUiState()) { + @AssistedFactory + interface Factory { + fun create(navKey: FeedbackNavKey): FeedbackViewModel + } + + private var initialContent: String = "" + + init { + fetchInitialContent() + } + + override fun onIntent(intent: FeedbackUiIntent) { + when (intent) { + is FeedbackUiIntent.ChangeContent -> changeContent(intent.content) + FeedbackUiIntent.ClickClose -> handleClose() + FeedbackUiIntent.ClickSave -> save() + FeedbackUiIntent.ClickDialogClose -> updateState { copy(isExitDialogVisible = false) } + FeedbackUiIntent.ClickDialogExit -> navigateBack() + } + } + + private fun fetchInitialContent() { + viewModelScope.launch { + fetchPresentationDetailUseCase( + presentationId = navKey.presentationId, + isPast = navKey.isPast, + ).onSuccess { result -> + val selfFeedback = result.analysisSummary.selfFeedback + .orEmpty() + initialContent = selfFeedback + + if (currentState.content.isBlank()) { + updateState { copy(content = selfFeedback) } + } + }.onFailure { throwable -> + sendEffect(FeedbackUiEffect.ShowMessage(FeedbackUiMessage.FETCH_FEEDBACK_FAILED)) + Timber.e(throwable) + } + } + } + + private fun changeContent(content: String) { + updateState { copy(content = content) } + } + + private fun handleClose() { + if (hasUnsavedContent()) { + updateState { copy(isExitDialogVisible = true) } + return + } + + navigateBack() + } + + private fun hasUnsavedContent(): Boolean = currentState.content != initialContent + + private fun save() { + val content = currentState.content.trim() + if (content.isBlank() || currentState.isSaving) return + + updateState { copy(isSaving = true) } + + viewModelScope.launch { + writeSelfFeedbackUseCase( + presentationId = navKey.presentationId, + content = content, + ).onSuccess { + updateState { copy(isSaving = false) } + sendEffect(FeedbackUiEffect.SaveComplete) + }.onFailure { throwable -> + updateState { copy(isSaving = false) } + sendEffect(FeedbackUiEffect.ShowMessage(throwable.toFeedbackUiMessage())) + Timber.e(throwable) + } + } + } + + private fun navigateBack() { + viewModelScope.launch { sendEffect(FeedbackUiEffect.NavigateBack) } + } +} + +private fun Throwable.toFeedbackUiMessage(): FeedbackUiMessage { + val error = (this as? AppException)?.error + + return when (error) { + AppError.UNAUTHORIZED -> FeedbackUiMessage.PRESENTATION_FORBIDDEN + AppError.NOT_FOUND -> FeedbackUiMessage.PRESENTATION_NOT_FOUND + AppError.DUPLICATE -> FeedbackUiMessage.SELF_FEEDBACK_ALREADY_WRITTEN + AppError.INVALID_REQUEST, + AppError.SERVER_ERROR, + AppError.VOICE_RECOGNITION_FAILED, + AppError.VOICE_ANALYSIS_FAILED, + AppError.SCRIPT_FILE_RECOGNITION_FAILED, + AppError.NETWORK, + AppError.UNKNOWN, + null, + -> FeedbackUiMessage.SAVE_FAILED + } +} diff --git a/Prezel/feature/feedback/impl/src/main/java/com/team/prezel/feature/feedback/impl/contract/FeedbackUiEffect.kt b/Prezel/feature/feedback/impl/src/main/java/com/team/prezel/feature/feedback/impl/contract/FeedbackUiEffect.kt new file mode 100644 index 00000000..bf6feb08 --- /dev/null +++ b/Prezel/feature/feedback/impl/src/main/java/com/team/prezel/feature/feedback/impl/contract/FeedbackUiEffect.kt @@ -0,0 +1,14 @@ +package com.team.prezel.feature.feedback.impl.contract + +import com.team.prezel.core.ui.base.UiEffect +import com.team.prezel.feature.feedback.impl.model.FeedbackUiMessage + +internal sealed interface FeedbackUiEffect : UiEffect { + data object NavigateBack : FeedbackUiEffect + + data object SaveComplete : FeedbackUiEffect + + data class ShowMessage( + val message: FeedbackUiMessage, + ) : FeedbackUiEffect +} diff --git a/Prezel/feature/feedback/impl/src/main/java/com/team/prezel/feature/feedback/impl/contract/FeedbackUiIntent.kt b/Prezel/feature/feedback/impl/src/main/java/com/team/prezel/feature/feedback/impl/contract/FeedbackUiIntent.kt new file mode 100644 index 00000000..7072ec0b --- /dev/null +++ b/Prezel/feature/feedback/impl/src/main/java/com/team/prezel/feature/feedback/impl/contract/FeedbackUiIntent.kt @@ -0,0 +1,17 @@ +package com.team.prezel.feature.feedback.impl.contract + +import com.team.prezel.core.ui.base.UiIntent + +internal sealed interface FeedbackUiIntent : UiIntent { + data class ChangeContent( + val content: String, + ) : FeedbackUiIntent + + data object ClickClose : FeedbackUiIntent + + data object ClickSave : FeedbackUiIntent + + data object ClickDialogClose : FeedbackUiIntent + + data object ClickDialogExit : FeedbackUiIntent +} diff --git a/Prezel/feature/feedback/impl/src/main/java/com/team/prezel/feature/feedback/impl/contract/FeedbackUiState.kt b/Prezel/feature/feedback/impl/src/main/java/com/team/prezel/feature/feedback/impl/contract/FeedbackUiState.kt new file mode 100644 index 00000000..9476a13f --- /dev/null +++ b/Prezel/feature/feedback/impl/src/main/java/com/team/prezel/feature/feedback/impl/contract/FeedbackUiState.kt @@ -0,0 +1,14 @@ +package com.team.prezel.feature.feedback.impl.contract + +import androidx.compose.runtime.Immutable +import com.team.prezel.core.ui.base.UiState + +@Immutable +internal data class FeedbackUiState( + val content: String = "", + val isSaving: Boolean = false, + val isExitDialogVisible: Boolean = false, +) : UiState { + val isSaveEnabled: Boolean + get() = content.isNotBlank() && !isSaving +} diff --git a/Prezel/feature/feedback/impl/src/main/java/com/team/prezel/feature/feedback/impl/model/FeedbackUiMessage.kt b/Prezel/feature/feedback/impl/src/main/java/com/team/prezel/feature/feedback/impl/model/FeedbackUiMessage.kt new file mode 100644 index 00000000..f408ef84 --- /dev/null +++ b/Prezel/feature/feedback/impl/src/main/java/com/team/prezel/feature/feedback/impl/model/FeedbackUiMessage.kt @@ -0,0 +1,9 @@ +package com.team.prezel.feature.feedback.impl.model + +internal enum class FeedbackUiMessage { + FETCH_FEEDBACK_FAILED, + PRESENTATION_FORBIDDEN, + PRESENTATION_NOT_FOUND, + SELF_FEEDBACK_ALREADY_WRITTEN, + SAVE_FAILED, +} diff --git a/Prezel/feature/feedback/impl/src/main/java/com/team/prezel/feature/feedback/impl/navigation/FeedbackEntryBuilder.kt b/Prezel/feature/feedback/impl/src/main/java/com/team/prezel/feature/feedback/impl/navigation/FeedbackEntryBuilder.kt new file mode 100644 index 00000000..95a1bc4c --- /dev/null +++ b/Prezel/feature/feedback/impl/src/main/java/com/team/prezel/feature/feedback/impl/navigation/FeedbackEntryBuilder.kt @@ -0,0 +1,40 @@ +package com.team.prezel.feature.feedback.impl.navigation + +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.navigation3.runtime.EntryProviderScope +import androidx.navigation3.runtime.NavKey +import com.team.prezel.core.navigation.LocalNavigator +import com.team.prezel.feature.feedback.api.FeedbackNavKey +import com.team.prezel.feature.feedback.impl.FeedbackScreen +import com.team.prezel.feature.feedback.impl.FeedbackViewModel +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ActivityRetainedComponent +import dagger.multibindings.IntoSet + +internal fun EntryProviderScope.featureFeedbackEntryBuilder() { + entry { key -> + val navigator = LocalNavigator.current + + FeedbackScreen( + title = key.title, + navigateBack = { navigator.goBack() }, + onSaveComplete = { navigator.goBack() }, + viewModel = hiltViewModel( + creationCallback = { factory -> factory.create(key) }, + ), + ) + } +} + +@Module +@InstallIn(ActivityRetainedComponent::class) +object FeatureFeedbackModule { + @IntoSet + @Provides + fun provideFeatureFeedbackEntryBuilder(): EntryProviderScope.() -> Unit = + { + featureFeedbackEntryBuilder() + } +} diff --git a/Prezel/feature/feedback/impl/src/main/res/values/strings.xml b/Prezel/feature/feedback/impl/src/main/res/values/strings.xml new file mode 100644 index 00000000..c5db6ad0 --- /dev/null +++ b/Prezel/feature/feedback/impl/src/main/res/values/strings.xml @@ -0,0 +1,16 @@ + + 셀프 피드백 + 닫기 + \'%1$s\'는 어떠셨나요? + 다음 발표에는 어떤 부분을 신경쓰고 싶나요? + 저장하기 + 셀프 피드백을 불러오지 못했어요. + 해당 데이터에 접근할 권한이 없습니다. + 존재하지 않는 발표입니다. + 이미 회고를 작성한 발표입니다. + 셀프 피드백 저장에 실패했어요. + 저장하지 않고 나가시겠어요? + 작성된 셀프 피드백이 모두 삭제됩니다. + 닫기 + 나가기 + diff --git a/Prezel/feature/home/impl/build.gradle.kts b/Prezel/feature/home/impl/build.gradle.kts index c8d37800..d8bd2026 100644 --- a/Prezel/feature/home/impl/build.gradle.kts +++ b/Prezel/feature/home/impl/build.gradle.kts @@ -11,6 +11,7 @@ dependencies { implementation(projects.coreDomain) implementation(projects.featureAnalysisApi) + implementation(projects.featureFeedbackApi) implementation(projects.featureHomeApi) implementation(projects.featurePracticeApi) diff --git a/Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/main/HomeScreen.kt b/Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/main/HomeScreen.kt index 7996c65c..30dd9b66 100644 --- a/Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/main/HomeScreen.kt +++ b/Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/main/HomeScreen.kt @@ -34,6 +34,8 @@ internal fun HomeScreen( navigateToPracticeRecording: (presentationId: Long) -> Unit, navigateToFileUploadAnalysis: () -> Unit, navigateToVoiceRecordingAnalysis: () -> Unit, + navigateToAnalyzePresentation: (presentationId: Long, isPast: Boolean) -> Unit, + navigateToFeedback: (presentationId: Long, title: String, isPast: Boolean) -> Unit, modifier: Modifier = Modifier, viewModel: HomeViewModel = hiltViewModel(), ) { @@ -62,8 +64,16 @@ internal fun HomeScreen( pagerState = pagerState, onClickAddPresentation = navigateToVoiceRecordingAnalysis, onClickPracticeRecording = { presentationId -> navigateToPracticeRecording(presentationId) }, - onClickAnalyzePresentation = { }, - onClickWriteFeedback = { }, + onClickAnalyzePresentation = { presentation -> + navigateToAnalyzePresentation(presentation.id, presentation.isPastPresentation) + }, + onClickWriteFeedback = { presentation -> + navigateToFeedback( + presentation.id, + presentation.title, + presentation.isPastPresentation, + ) + }, onClickVoiceRecordingAnalysis = navigateToVoiceRecordingAnalysis, onClickFileUploadAnalysis = navigateToFileUploadAnalysis, onClickCardGraphItemIndex = { presentationId, index -> diff --git a/Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/main/component/HomeScreenContent.kt b/Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/main/component/HomeScreenContent.kt index b2c1cc92..03c266b8 100644 --- a/Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/main/component/HomeScreenContent.kt +++ b/Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/main/component/HomeScreenContent.kt @@ -189,9 +189,9 @@ private fun HomeMultipleContent( modifier = Modifier.fillMaxSize(), overscrollEffect = null, userScrollEnabled = false, - key = { pageIndex -> uiState.presentations[pageIndex].id }, + key = { pageIndex -> uiState.presentations.getOrNull(pageIndex)?.id ?: pageIndex }, ) { pageIndex -> - val presentation = uiState.presentations[pageIndex] + val presentation = uiState.presentations.getOrNull(pageIndex) ?: return@HorizontalPager HomePresentationContent( presentation = presentation, diff --git a/Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/main/component/title/PresentationHero.kt b/Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/main/component/title/PresentationHero.kt index b4ad0866..922bed9c 100644 --- a/Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/main/component/title/PresentationHero.kt +++ b/Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/main/component/title/PresentationHero.kt @@ -2,17 +2,17 @@ package com.team.prezel.feature.home.impl.main.component.title import androidx.annotation.DrawableRes import androidx.annotation.StringRes +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.res.stringResource -import com.team.prezel.core.designsystem.component.chip.chip.ChipSize -import com.team.prezel.core.designsystem.component.chip.chip.ChipType -import com.team.prezel.core.designsystem.component.chip.chip.PrezelChip import com.team.prezel.core.designsystem.preview.BasicPreview import com.team.prezel.core.designsystem.theme.PrezelTheme import com.team.prezel.core.model.presentation.Category @@ -34,10 +34,8 @@ internal fun PresentationHero( backgroundResId = presentation.category.backgroundResId(), modifier = modifier, ) { - PrezelChip( + HomeCategoryChip( text = stringResource(id = presentation.category.labelResId()), - type = ChipType.OUTLINED, - size = ChipSize.SMALL, ) Spacer(modifier = Modifier.weight(1f)) @@ -69,6 +67,25 @@ internal fun PresentationHero( } } +@Composable +private fun HomeCategoryChip( + text: String, + modifier: Modifier = Modifier, +) { + Text( + text = text, + color = PrezelTheme.colors.interactiveRegular, + style = PrezelTheme.typography.caption2Regular, + modifier = modifier + .clip(PrezelTheme.shapes.V4) + .background(PrezelTheme.colors.bgRegular) + .padding( + horizontal = PrezelTheme.spacing.V6, + vertical = PrezelTheme.spacing.V4, + ), + ) +} + @Composable private fun HomePresentationDate(date: LocalDate) { Text( @@ -128,7 +145,7 @@ private fun HomePresentationPagePreview() { category = Category.OFFER, title = "설득하는 발표", date = LocalDate(2026, 10, 1), - dDay = "-3", + dDay = "D-3", practiceRecords = PracticeRecordsUiModel( practicedDates = listOf(LocalDate(2026, 9, 28)), startDate = LocalDate(2026, 9, 26), @@ -151,7 +168,7 @@ private fun HomePresentationPagePastPreview() { category = Category.EDUCATION, title = "교육 발표", date = LocalDate(2026, 9, 20), - dDay = "+5", + dDay = "D+5", practiceRecords = PracticeRecordsUiModel( practicedDates = listOf(LocalDate(2026, 9, 18), LocalDate(2026, 9, 19)), startDate = LocalDate(2026, 9, 15), diff --git a/Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/navigation/HomeEntryBuilder.kt b/Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/navigation/HomeEntryBuilder.kt index 5cf4ddaf..f7de6e70 100644 --- a/Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/navigation/HomeEntryBuilder.kt +++ b/Prezel/feature/home/impl/src/main/java/com/team/prezel/feature/home/impl/navigation/HomeEntryBuilder.kt @@ -5,6 +5,7 @@ import androidx.navigation3.runtime.NavKey import com.team.prezel.core.navigation.LocalNavigator import com.team.prezel.feature.analysis.api.AnalysisNavKey import com.team.prezel.feature.analysis.api.AnalysisStartType +import com.team.prezel.feature.feedback.api.FeedbackNavKey import com.team.prezel.feature.home.api.HomeNavKey import com.team.prezel.feature.home.impl.main.HomeScreen import com.team.prezel.feature.practice.api.PracticeNavKey @@ -28,6 +29,18 @@ internal fun EntryProviderScope.featureHomeEntryBuilder() { navigateToVoiceRecordingAnalysis = { navigator.navigate(AnalysisNavKey.Schedule(startType = AnalysisStartType.VOICE_RECORDING)) }, + navigateToAnalyzePresentation = { presentationId, isPast -> + navigator.navigate(AnalysisNavKey.ReRecording(presentationId = presentationId, isPast = isPast)) + }, + navigateToFeedback = { presentationId, title, isPast -> + navigator.navigate( + FeedbackNavKey( + presentationId = presentationId, + title = title, + isPast = isPast, + ), + ) + }, ) } } diff --git a/Prezel/feature/report/api/src/main/java/com/team/prezel/feature/report/api/ReportNavKey.kt b/Prezel/feature/report/api/src/main/java/com/team/prezel/feature/report/api/ReportNavKey.kt index 2825db54..5cb4de82 100644 --- a/Prezel/feature/report/api/src/main/java/com/team/prezel/feature/report/api/ReportNavKey.kt +++ b/Prezel/feature/report/api/src/main/java/com/team/prezel/feature/report/api/ReportNavKey.kt @@ -7,5 +7,4 @@ import kotlinx.serialization.Serializable data class ReportNavKey( val presentationId: Long, val isPast: Boolean = false, - val refreshKey: String = "", ) : NavKey diff --git a/Prezel/feature/report/impl/build.gradle.kts b/Prezel/feature/report/impl/build.gradle.kts index 9b714254..ab9dc26c 100644 --- a/Prezel/feature/report/impl/build.gradle.kts +++ b/Prezel/feature/report/impl/build.gradle.kts @@ -11,6 +11,7 @@ dependencies { implementation(projects.coreDomain) implementation(projects.coreUi) implementation(projects.featureAnalysisApi) + implementation(projects.featureFeedbackApi) implementation(projects.featureReportApi) implementation(libs.kotlinx.datetime) diff --git a/Prezel/feature/report/impl/src/main/java/com/team/prezel/feature/report/impl/AnalysisReportScreen.kt b/Prezel/feature/report/impl/src/main/java/com/team/prezel/feature/report/impl/AnalysisReportScreen.kt index d6b531af..8af05514 100644 --- a/Prezel/feature/report/impl/src/main/java/com/team/prezel/feature/report/impl/AnalysisReportScreen.kt +++ b/Prezel/feature/report/impl/src/main/java/com/team/prezel/feature/report/impl/AnalysisReportScreen.kt @@ -33,7 +33,7 @@ internal fun AnalysisReportScreen( onBack: () -> Unit, navigateToAnalysisScript: (presentationId: Long, isPast: Boolean) -> Unit, navigateToAnalysisRecording: (presentationId: Long, isPast: Boolean) -> Unit, - navigateToSelfFeedbackWrite: (presentationId: Long) -> Unit, + navigateToSelfFeedbackWrite: (presentationId: Long, title: String, isPast: Boolean) -> Unit, modifier: Modifier = Modifier, viewModel: AnalysisReportViewModel = hiltViewModel(), ) { @@ -41,6 +41,10 @@ internal fun AnalysisReportScreen( val snackbarHostState = LocalSnackbarHostState.current val resources = LocalResources.current + LaunchedEffect(Unit) { + viewModel.onIntent(AnalysisReportUiIntent.FetchData) + } + LaunchedEffect(Unit) { viewModel.uiEffect.collect { effect -> when (effect) { @@ -54,7 +58,9 @@ internal fun AnalysisReportScreen( is AnalysisReportUiEffect.NavigateToAnalysisScript -> navigateToAnalysisScript(effect.presentationId, effect.isPast) is AnalysisReportUiEffect.NavigateToAnalysisRecording -> navigateToAnalysisRecording(effect.presentationId, effect.isPast) - is AnalysisReportUiEffect.NavigateToSelfFeedbackWrite -> navigateToSelfFeedbackWrite(effect.presentationId) + is AnalysisReportUiEffect.NavigateToSelfFeedbackWrite -> { + navigateToSelfFeedbackWrite(effect.presentationId, effect.title, effect.isPast) + } } } } diff --git a/Prezel/feature/report/impl/src/main/java/com/team/prezel/feature/report/impl/AnalysisReportViewModel.kt b/Prezel/feature/report/impl/src/main/java/com/team/prezel/feature/report/impl/AnalysisReportViewModel.kt index 8a5e3b62..d2b0fdda 100644 --- a/Prezel/feature/report/impl/src/main/java/com/team/prezel/feature/report/impl/AnalysisReportViewModel.kt +++ b/Prezel/feature/report/impl/src/main/java/com/team/prezel/feature/report/impl/AnalysisReportViewModel.kt @@ -28,16 +28,14 @@ internal class AnalysisReportViewModel @AssistedInject constructor( fun create(navKey: ReportNavKey): AnalysisReportViewModel } + private val requestedPresentationId: Long = navKey.presentationId private var presentationId: Long? = null private var analysisResultId: Long? = null private val isPast: Boolean = navKey.isPast - init { - fetchData(presentationId = navKey.presentationId, isPast = navKey.isPast) - } - override fun onIntent(intent: AnalysisReportUiIntent) { when (intent) { + AnalysisReportUiIntent.FetchData -> fetchData() AnalysisReportUiIntent.ClickDelete -> updateContent { copy(reportDialog = AnalysisReportDialog.DELETE_REPORT) } is AnalysisReportUiIntent.ClickGrowthGraphItem -> updateGrowthGraphSelectedItem(index = intent.index) AnalysisReportUiIntent.ClickDialogConform -> handleClickDialogConform() @@ -48,12 +46,9 @@ internal class AnalysisReportViewModel @AssistedInject constructor( } } - private fun fetchData( - presentationId: Long, - isPast: Boolean, - ) { + private fun fetchData() { viewModelScope.launch { - fetchPresentationDetailUseCase(presentationId = presentationId, isPast = isPast) + fetchPresentationDetailUseCase(presentationId = requestedPresentationId, isPast = isPast) .onSuccess { result -> this@AnalysisReportViewModel.presentationId = result.analysisSummary.presentationId analysisResultId = result.analysisSummary.analysisResultId @@ -117,7 +112,13 @@ internal class AnalysisReportViewModel @AssistedInject constructor( } private fun navigateToSelfFeedbackWrite() { - val effect = presentationId?.let(AnalysisReportUiEffect::NavigateToSelfFeedbackWrite) ?: return + val presentationId = presentationId ?: return + val title = contentState?.presentationInfo?.title ?: return + val effect = AnalysisReportUiEffect.NavigateToSelfFeedbackWrite( + presentationId = presentationId, + title = title, + isPast = isPast, + ) viewModelScope.launch { sendEffect(effect) } } diff --git a/Prezel/feature/report/impl/src/main/java/com/team/prezel/feature/report/impl/contract/AnalysisReportUiEffect.kt b/Prezel/feature/report/impl/src/main/java/com/team/prezel/feature/report/impl/contract/AnalysisReportUiEffect.kt index c5b26bde..9e50bc37 100644 --- a/Prezel/feature/report/impl/src/main/java/com/team/prezel/feature/report/impl/contract/AnalysisReportUiEffect.kt +++ b/Prezel/feature/report/impl/src/main/java/com/team/prezel/feature/report/impl/contract/AnalysisReportUiEffect.kt @@ -22,5 +22,7 @@ internal sealed interface AnalysisReportUiEffect : UiEffect { data class NavigateToSelfFeedbackWrite( val presentationId: Long, + val title: String, + val isPast: Boolean, ) : AnalysisReportUiEffect } diff --git a/Prezel/feature/report/impl/src/main/java/com/team/prezel/feature/report/impl/contract/AnalysisReportUiIntent.kt b/Prezel/feature/report/impl/src/main/java/com/team/prezel/feature/report/impl/contract/AnalysisReportUiIntent.kt index 85f3480f..34c4dc16 100644 --- a/Prezel/feature/report/impl/src/main/java/com/team/prezel/feature/report/impl/contract/AnalysisReportUiIntent.kt +++ b/Prezel/feature/report/impl/src/main/java/com/team/prezel/feature/report/impl/contract/AnalysisReportUiIntent.kt @@ -3,6 +3,8 @@ package com.team.prezel.feature.report.impl.contract import com.team.prezel.core.ui.base.UiIntent internal sealed interface AnalysisReportUiIntent : UiIntent { + data object FetchData : AnalysisReportUiIntent + data object ClickDelete : AnalysisReportUiIntent data class ClickGrowthGraphItem( diff --git a/Prezel/feature/report/impl/src/main/java/com/team/prezel/feature/report/impl/navigation/ReportEntryBuilder.kt b/Prezel/feature/report/impl/src/main/java/com/team/prezel/feature/report/impl/navigation/ReportEntryBuilder.kt index 9bf39fc6..6b74eee9 100644 --- a/Prezel/feature/report/impl/src/main/java/com/team/prezel/feature/report/impl/navigation/ReportEntryBuilder.kt +++ b/Prezel/feature/report/impl/src/main/java/com/team/prezel/feature/report/impl/navigation/ReportEntryBuilder.kt @@ -5,6 +5,7 @@ import androidx.navigation3.runtime.EntryProviderScope import androidx.navigation3.runtime.NavKey import com.team.prezel.core.navigation.LocalNavigator import com.team.prezel.feature.analysis.api.AnalysisNavKey +import com.team.prezel.feature.feedback.api.FeedbackNavKey import com.team.prezel.feature.report.api.ReportNavKey import com.team.prezel.feature.report.impl.AnalysisReportScreen import com.team.prezel.feature.report.impl.AnalysisReportViewModel @@ -36,7 +37,15 @@ internal fun EntryProviderScope.featureAnalysisReportEntryBuilder() { ), ) }, - navigateToSelfFeedbackWrite = {}, + navigateToSelfFeedbackWrite = { presentationId, title, isPast -> + navigator.navigate( + FeedbackNavKey( + presentationId = presentationId, + title = title, + isPast = isPast, + ), + ) + }, viewModel = hiltViewModel( creationCallback = { factory -> factory.create(key) }, ), diff --git a/Prezel/settings.gradle.kts b/Prezel/settings.gradle.kts index 83ede2fb..f7b56c39 100644 --- a/Prezel/settings.gradle.kts +++ b/Prezel/settings.gradle.kts @@ -58,6 +58,8 @@ includeAuto( ":feature:analysis:impl", ":feature:history:api", ":feature:history:impl", + ":feature:feedback:api", + ":feature:feedback:impl", ":feature:my:api", ":feature:my:impl", ":feature:setting:api",