From d922a8de677d7f03c2edba57377ffd96b9768439 Mon Sep 17 00:00:00 2001 From: Jae Woong Date: Tue, 24 Jun 2025 11:05:33 +0900 Subject: [PATCH 1/7] =?UTF-8?q?feat=20:=20ExitConfirmDialog=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../QuizBookSolvingRepositoryTest.kt | 2 +- .../QuizBookSolvingRepositoryImpl.kt | 12 ++ .../database/dao/quizBook/QuizBookGradeDao.kt | 2 +- .../repository/QuizBookSolvingRepository.kt | 3 + .../solving/DeleteQuizBookGradeUseCase.kt | 14 +++ .../feature/quiz/solve/QuizSolveRoute.kt | 31 ++++- .../feature/quiz/solve/QuizSolveScreen.kt | 4 + .../quiz/solve/component/ExitConfirmDialog.kt | 114 ++++++++++++++++++ .../quiz/solve/viewmodel/QuizSolveEffect.kt | 3 +- .../quiz/solve/viewmodel/QuizSolveIntent.kt | 2 + .../solve/viewmodel/QuizSolveViewModel.kt | 40 ++++-- app/src/main/res/values/strings.xml | 9 ++ 12 files changed, 220 insertions(+), 16 deletions(-) create mode 100644 app/src/main/java/com/android/quizcafe/core/domain/usecase/solving/DeleteQuizBookGradeUseCase.kt create mode 100644 app/src/main/java/com/android/quizcafe/feature/quiz/solve/component/ExitConfirmDialog.kt diff --git a/app/src/androidTest/java/com/android/quizcafe/repository/QuizBookSolvingRepositoryTest.kt b/app/src/androidTest/java/com/android/quizcafe/repository/QuizBookSolvingRepositoryTest.kt index e33995f6..a40a5782 100644 --- a/app/src/androidTest/java/com/android/quizcafe/repository/QuizBookSolvingRepositoryTest.kt +++ b/app/src/androidTest/java/com/android/quizcafe/repository/QuizBookSolvingRepositoryTest.kt @@ -204,7 +204,7 @@ class QuizBookSolvingRepositoryTest { // When val solveResults = mutableListOf>() - repository.solveQuizBook(quizBookGradeLocalId).collect(solveResults::add) + repository.solveQuizBook(quizBookGradeLocalId,1L).collect(solveResults::add) // Then assertEquals(2, solveResults.size) diff --git a/app/src/main/java/com/android/quizcafe/core/data/repository/QuizBookSolvingRepositoryImpl.kt b/app/src/main/java/com/android/quizcafe/core/data/repository/QuizBookSolvingRepositoryImpl.kt index 89cf2bd1..d0d3f8ca 100644 --- a/app/src/main/java/com/android/quizcafe/core/data/repository/QuizBookSolvingRepositoryImpl.kt +++ b/app/src/main/java/com/android/quizcafe/core/data/repository/QuizBookSolvingRepositoryImpl.kt @@ -76,6 +76,18 @@ class QuizBookSolvingRepositoryImpl @Inject constructor( emit(Resource.Failure(errorMessage = "퀴즈북 풀이 기록 조회 중 오류: ${e.message}", code = LocalErrorCode.ROOM_ERROR)) } + override fun deleteQuizBookGrade(id: QuizBookGradeLocalId): Flow> = flow { + emit(Resource.Loading) + val deletedCnt = quizBookGradeDao.deleteQuizBookGrade(id.value) + if (deletedCnt == 1) { + emit(Resource.Success(Unit)) + }else{ + emit(Resource.Failure(errorMessage = "퀴즈북 풀이 기록 삭제 중 오류 : deletedCnt = $deletedCnt", code = LocalErrorCode.ROOM_ERROR)) + } + }.catch { e -> + emit(Resource.Failure(errorMessage = "퀴즈북 풀이 기록 삭제 중 오류: ${e.message}", code = LocalErrorCode.ROOM_ERROR)) + } + override fun getQuizBookSolving(id: QuizBookGradeServerId): Flow> = flow { val quizBookSolvingId = id.value if (quizBookSolvingId == null) { diff --git a/app/src/main/java/com/android/quizcafe/core/database/dao/quizBook/QuizBookGradeDao.kt b/app/src/main/java/com/android/quizcafe/core/database/dao/quizBook/QuizBookGradeDao.kt index efea1800..611d571d 100644 --- a/app/src/main/java/com/android/quizcafe/core/database/dao/quizBook/QuizBookGradeDao.kt +++ b/app/src/main/java/com/android/quizcafe/core/database/dao/quizBook/QuizBookGradeDao.kt @@ -19,5 +19,5 @@ interface QuizBookGradeDao { suspend fun getQuizBookGrade(localId: Long): QuizBookGradeWithQuizGradesRelation? @Query("DELETE FROM QuizBookGradeEntity WHERE localId = :localId") - suspend fun deleteQuizBookGrade(localId: Long) + suspend fun deleteQuizBookGrade(localId: Long) : Int } diff --git a/app/src/main/java/com/android/quizcafe/core/domain/repository/QuizBookSolvingRepository.kt b/app/src/main/java/com/android/quizcafe/core/domain/repository/QuizBookSolvingRepository.kt index 23054b71..fceb6422 100644 --- a/app/src/main/java/com/android/quizcafe/core/domain/repository/QuizBookSolvingRepository.kt +++ b/app/src/main/java/com/android/quizcafe/core/domain/repository/QuizBookSolvingRepository.kt @@ -16,6 +16,9 @@ interface QuizBookSolvingRepository { // 퀴즈북 풀이 로컬 기록 가져오기 fun getQuizBookGrade(id: QuizBookGradeLocalId): Flow> + // 퀴즈북 풀이 로컬 기록 삭제하기 + fun deleteQuizBookGrade(id : QuizBookGradeLocalId) : Flow> + // 퀴즈북 풀이 완료 기록 가져오기 (서버) fun getQuizBookSolving(id: QuizBookGradeServerId): Flow> diff --git a/app/src/main/java/com/android/quizcafe/core/domain/usecase/solving/DeleteQuizBookGradeUseCase.kt b/app/src/main/java/com/android/quizcafe/core/domain/usecase/solving/DeleteQuizBookGradeUseCase.kt new file mode 100644 index 00000000..992177a5 --- /dev/null +++ b/app/src/main/java/com/android/quizcafe/core/domain/usecase/solving/DeleteQuizBookGradeUseCase.kt @@ -0,0 +1,14 @@ +package com.android.quizcafe.core.domain.usecase.solving + +import com.android.quizcafe.core.domain.model.Resource +import com.android.quizcafe.core.domain.model.value.QuizBookGradeLocalId +import com.android.quizcafe.core.domain.repository.QuizBookSolvingRepository +import kotlinx.coroutines.flow.Flow +import javax.inject.Inject + +class DeleteQuizBookGradeUseCase @Inject constructor( + private val quizBookSolvingRepository: QuizBookSolvingRepository +) { + operator fun invoke(quizBookGradeLocalId: QuizBookGradeLocalId): Flow> = + quizBookSolvingRepository.deleteQuizBookGrade(quizBookGradeLocalId) +} diff --git a/app/src/main/java/com/android/quizcafe/feature/quiz/solve/QuizSolveRoute.kt b/app/src/main/java/com/android/quizcafe/feature/quiz/solve/QuizSolveRoute.kt index 74fd43ad..f4824ce9 100644 --- a/app/src/main/java/com/android/quizcafe/feature/quiz/solve/QuizSolveRoute.kt +++ b/app/src/main/java/com/android/quizcafe/feature/quiz/solve/QuizSolveRoute.kt @@ -1,14 +1,19 @@ package com.android.quizcafe.feature.quiz.solve import android.widget.Toast +import androidx.activity.compose.BackHandler import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalContext import androidx.hilt.navigation.compose.hiltViewModel import com.android.quizcafe.R import com.android.quizcafe.core.domain.model.value.QuizBookGradeServerId +import com.android.quizcafe.feature.quiz.solve.component.ExitConfirmDialog import com.android.quizcafe.feature.quiz.solve.viewmodel.QuizSolveEffect import com.android.quizcafe.feature.quiz.solve.viewmodel.QuizSolveIntent import com.android.quizcafe.feature.quiz.solve.viewmodel.QuizSolveViewModel @@ -22,6 +27,25 @@ fun QuizSolveRoute( ) { val context = LocalContext.current val state by viewModel.state.collectAsState() + var showExitDialog by remember { mutableStateOf(false)} + + BackHandler(enabled = !showExitDialog) { + viewModel.sendIntent(QuizSolveIntent.OnBackClick) + } + + if (showExitDialog) { + ExitConfirmDialog( + onDismissRequest = { + showExitDialog = false + }, + onExitWithDelete = { + viewModel.sendIntent(QuizSolveIntent.ExitWithDelete) + }, + onExitWithSave = { + viewModel.sendIntent(QuizSolveIntent.ExitWithSave) + }, + ) + } LaunchedEffect(Unit) { viewModel.sendIntent(QuizSolveIntent.LoadQuizBook(quizBookId)) @@ -30,13 +54,16 @@ fun QuizSolveRoute( LaunchedEffect(Unit) { viewModel.effect.collect { effect -> when (effect) { - QuizSolveEffect.NavigatePopBack -> { + QuizSolveEffect.ShowExitDialog -> { + showExitDialog = true + } + QuizSolveEffect.NavigateToBack -> { + if(showExitDialog) showExitDialog = false navigateToBack() } is QuizSolveEffect.NavigateToQuizBookSolvingResult -> { navigateToQuizBookSolvingResult(effect.quizBookGradeServerId) } - is QuizSolveEffect.ShowErrorDialog -> { Toast.makeText( context, diff --git a/app/src/main/java/com/android/quizcafe/feature/quiz/solve/QuizSolveScreen.kt b/app/src/main/java/com/android/quizcafe/feature/quiz/solve/QuizSolveScreen.kt index b4f02d7e..2406a2f9 100644 --- a/app/src/main/java/com/android/quizcafe/feature/quiz/solve/QuizSolveScreen.kt +++ b/app/src/main/java/com/android/quizcafe/feature/quiz/solve/QuizSolveScreen.kt @@ -14,6 +14,10 @@ import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource diff --git a/app/src/main/java/com/android/quizcafe/feature/quiz/solve/component/ExitConfirmDialog.kt b/app/src/main/java/com/android/quizcafe/feature/quiz/solve/component/ExitConfirmDialog.kt new file mode 100644 index 00000000..a715cb87 --- /dev/null +++ b/app/src/main/java/com/android/quizcafe/feature/quiz/solve/component/ExitConfirmDialog.kt @@ -0,0 +1,114 @@ +package com.android.quizcafe.feature.quiz.solve.component + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import com.android.quizcafe.R +import com.android.quizcafe.core.designsystem.theme.QuizCafeTheme + +@Composable +fun ExitConfirmDialog( + onDismissRequest: () -> Unit, + onExitWithDelete: () -> Unit, + onExitWithSave: () -> Unit +) { + Dialog( + onDismissRequest = onDismissRequest, + properties = DialogProperties( + dismissOnClickOutside = false + ) + ) { + Card( + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surface, + ), + modifier = Modifier.fillMaxWidth() + ) { + Box(contentAlignment = Alignment.TopEnd) { + IconButton(onClick = onDismissRequest) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = stringResource(R.string.dialog_close) + ) + } + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp) + .padding(top = 32.dp, bottom = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + text = stringResource(R.string.dialog_exit_title), + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurface + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = stringResource(R.string.dialog_exit_description), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.height(16.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically + ) { + TextButton( + onClick = onExitWithDelete, + colors = ButtonDefaults.textButtonColors( + contentColor = MaterialTheme.colorScheme.error + ) + ) { + Text(text = stringResource(R.string.dialog_exit_with_delete)) + } + + Spacer(modifier = Modifier.width(8.dp)) + + TextButton( + onClick = onExitWithSave, + colors = ButtonDefaults.textButtonColors( + contentColor = MaterialTheme.colorScheme.onSurface + ) + ) { + Text(text = stringResource(R.string.dialog_exit_with_save)) + } + } + + } + } + } + } +} + + +@Preview(name = "Exit Confirmation Dialog", showBackground = true) +@Composable +fun ExitConfirmationDialogPreview() { + QuizCafeTheme { + ExitConfirmDialog ( + onDismissRequest = {}, + onExitWithDelete = {}, + onExitWithSave = {} + ) + } +} diff --git a/app/src/main/java/com/android/quizcafe/feature/quiz/solve/viewmodel/QuizSolveEffect.kt b/app/src/main/java/com/android/quizcafe/feature/quiz/solve/viewmodel/QuizSolveEffect.kt index 8efd682a..c428f8ed 100644 --- a/app/src/main/java/com/android/quizcafe/feature/quiz/solve/viewmodel/QuizSolveEffect.kt +++ b/app/src/main/java/com/android/quizcafe/feature/quiz/solve/viewmodel/QuizSolveEffect.kt @@ -5,6 +5,7 @@ import com.android.quizcafe.core.ui.base.BaseContract sealed class QuizSolveEffect : BaseContract.ViewEffect { data class ShowErrorDialog(val message: String) : QuizSolveEffect() - data object NavigatePopBack : QuizSolveEffect() + data object ShowExitDialog : QuizSolveEffect() + data object NavigateToBack : QuizSolveEffect() data class NavigateToQuizBookSolvingResult(val quizBookGradeServerId: QuizBookGradeServerId) : QuizSolveEffect() } diff --git a/app/src/main/java/com/android/quizcafe/feature/quiz/solve/viewmodel/QuizSolveIntent.kt b/app/src/main/java/com/android/quizcafe/feature/quiz/solve/viewmodel/QuizSolveIntent.kt index 8ebeffed..b368459b 100644 --- a/app/src/main/java/com/android/quizcafe/feature/quiz/solve/viewmodel/QuizSolveIntent.kt +++ b/app/src/main/java/com/android/quizcafe/feature/quiz/solve/viewmodel/QuizSolveIntent.kt @@ -25,4 +25,6 @@ sealed class QuizSolveIntent : BaseContract.ViewIntent { data object OnBackClick : QuizSolveIntent() data object TickTime : QuizSolveIntent() + data object ExitWithDelete : QuizSolveIntent() + data object ExitWithSave : QuizSolveIntent() } diff --git a/app/src/main/java/com/android/quizcafe/feature/quiz/solve/viewmodel/QuizSolveViewModel.kt b/app/src/main/java/com/android/quizcafe/feature/quiz/solve/viewmodel/QuizSolveViewModel.kt index 42d733ba..79559d50 100644 --- a/app/src/main/java/com/android/quizcafe/feature/quiz/solve/viewmodel/QuizSolveViewModel.kt +++ b/app/src/main/java/com/android/quizcafe/feature/quiz/solve/viewmodel/QuizSolveViewModel.kt @@ -6,6 +6,7 @@ import com.android.quizcafe.core.domain.model.Resource import com.android.quizcafe.core.domain.model.value.QuizBookGradeLocalId import com.android.quizcafe.core.domain.model.value.QuizBookId import com.android.quizcafe.core.domain.usecase.quizbook.GetQuizBookUseCase +import com.android.quizcafe.core.domain.usecase.solving.DeleteQuizBookGradeUseCase import com.android.quizcafe.core.domain.usecase.solving.GetQuizBookGradeUseCase import com.android.quizcafe.core.domain.usecase.solving.GetQuizBookLocalIdUseCase import com.android.quizcafe.core.domain.usecase.solving.GradeQuizUseCase @@ -22,7 +23,8 @@ class QuizSolveViewModel @Inject constructor( private val getQuizBookLocalIdUseCase: GetQuizBookLocalIdUseCase, private val getQuizBookGradeUseCase: GetQuizBookGradeUseCase, private val gradeQuizUseCase: GradeQuizUseCase, - private val solveQuizBookUseCase: SolveQuizBookUseCase + private val solveQuizBookUseCase: SolveQuizBookUseCase, + private val deleteQuizBookGradeUseCase: DeleteQuizBookGradeUseCase ) : BaseViewModel( initialState = QuizSolveUiState() ) { @@ -38,7 +40,7 @@ class QuizSolveViewModel @Inject constructor( override suspend fun handleIntent(intent: QuizSolveIntent) { when (intent) { QuizSolveIntent.OnBackClick -> { - emitEffect(QuizSolveEffect.NavigatePopBack) + emitEffect(QuizSolveEffect.ShowExitDialog) } is QuizSolveIntent.LoadQuizBook -> { @@ -50,17 +52,22 @@ class QuizSolveViewModel @Inject constructor( } is QuizSolveIntent.GetQuizBookGradeResult -> { - getQuizBookGradeResult(intent.quizBookLocalId) + getQuizBookGrade(intent.quizBookLocalId) } is QuizSolveIntent.SubmitNext -> { saveQuizToLocal() } - is QuizSolveIntent.SubmitAnswer -> { submitQuizAnswer() } + is QuizSolveIntent.ExitWithDelete -> { + deleteQuizBookGrade() + } + is QuizSolveIntent.ExitWithSave -> { + emitEffect(QuizSolveEffect.NavigateToBack) + } is QuizSolveIntent.SolveQuizSuccess -> { emitEffect(QuizSolveEffect.NavigateToQuizBookSolvingResult(intent.quizBookGradeServerId)) } @@ -68,7 +75,6 @@ class QuizSolveViewModel @Inject constructor( is QuizSolveIntent.GradeQuizError -> { emitEffect(QuizSolveEffect.ShowErrorDialog(intent.message ?: "")) } - else -> Unit } } @@ -140,7 +146,6 @@ class QuizSolveViewModel @Inject constructor( currentState.copy( quizGrades = intent.quizBookGrade.quizGrades ) - QuizSolveIntent.GradeQuizSuccess -> { if (!currentState.isLastQuestion) { currentState.copy( @@ -150,7 +155,6 @@ class QuizSolveViewModel @Inject constructor( } else currentState } - else -> currentState } } @@ -197,8 +201,7 @@ class QuizSolveViewModel @Inject constructor( } } } - - private suspend fun getQuizBookGradeResult(id: QuizBookGradeLocalId) { + private suspend fun getQuizBookGrade(id: QuizBookGradeLocalId) { getQuizBookGradeUseCase( id ).collect { @@ -218,7 +221,23 @@ class QuizSolveViewModel @Inject constructor( } } } - + private suspend fun deleteQuizBookGrade() { + val quizBookLocalId = state.value.quizBookLocalId + quizBookLocalId?.let { + deleteQuizBookGradeUseCase(it).collect { + when (it) { + is Resource.Success -> { + Log.d("deleteQuizBookGradeUseCase", "Get QuizBookDetail Success") + emitEffect(QuizSolveEffect.NavigateToBack) + } + is Resource.Failure -> { + Log.d("deleteQuizBookGradeUseCase", it.errorMessage) + } + else -> Unit + } + } + }?: Log.d("deleteQuizBookGrade", "quizBookLocalId is null") + } private suspend fun saveQuizToLocal() { val uiState = state.value val quiz = uiState.currentQuiz @@ -257,7 +276,6 @@ class QuizSolveViewModel @Inject constructor( sendIntent(QuizSolveIntent.GradeQuizError("문제 발생")) } } - private suspend fun submitQuizAnswer() { val localId = state.value.quizBookLocalId val elapsedTimeInSeconds: Long = state.value.timer.elapsedSeconds.toLong() diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 37f44e83..c051da8b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -93,4 +93,13 @@ 오류 확인 + + 닫기 + 기록을 저장하시겠습니까? + + 기록을 저장하지 않고 종료하면 현재까지의 풀이 기록은 삭제됩니다. + + 삭제 + 저장 + \ No newline at end of file From cab92fd57f1316f37c6ab6bddd28a29c79cb9924 Mon Sep 17 00:00:00 2001 From: Jae Woong Date: Tue, 24 Jun 2025 16:59:24 +0900 Subject: [PATCH 2/7] =?UTF-8?q?feat=20:=20ResumeSolvingDialog=20UI=20?= =?UTF-8?q?=EB=B0=8F=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../1.json | 16 ++- .../QuizBookSolvingRepositoryTest.kt | 6 +- .../data/mapper/quizbook/QuizBookMapper.kt | 9 +- .../QuizBookSolvingRepositoryImpl.kt | 42 +++--- .../database/dao/quizBook/QuizBookGradeDao.kt | 9 +- .../model/grading/QuizBookGradeEntity.kt | 10 +- .../model/quizbook/response/QuizBook.kt | 3 +- .../repository/QuizBookSolvingRepository.kt | 5 +- .../GetOrCreateQuizBookGradeUseCase.kt | 15 +++ .../solving/GetQuizBookGradeUseCase.kt | 15 --- .../solving/GetQuizBookLocalIdUseCase.kt | 12 -- .../quizcafe/feature/main/MainScreen.kt | 3 +- .../feature/quiz/solve/QuizSolveRoute.kt | 63 +++++---- ...tConfirmDialog.kt => ExitSolvingDialog.kt} | 8 +- .../solve/component/ResumeSolvingDialog.kt | 121 ++++++++++++++++++ .../quiz/solve/viewmodel/QuizSolveEffect.kt | 2 + .../quiz/solve/viewmodel/QuizSolveIntent.kt | 11 +- .../quiz/solve/viewmodel/QuizSolveUiState.kt | 9 +- .../solve/viewmodel/QuizSolveViewModel.kt | 112 ++++++++-------- .../quizbooklist/QuizBookListContent.kt | 9 +- .../quizbooklist/QuizBookListContract.kt | 5 +- .../feature/quizbooklist/QuizBookListRoute.kt | 3 +- app/src/main/res/values/strings.xml | 7 + 23 files changed, 320 insertions(+), 175 deletions(-) create mode 100644 app/src/main/java/com/android/quizcafe/core/domain/usecase/solving/GetOrCreateQuizBookGradeUseCase.kt delete mode 100644 app/src/main/java/com/android/quizcafe/core/domain/usecase/solving/GetQuizBookGradeUseCase.kt delete mode 100644 app/src/main/java/com/android/quizcafe/core/domain/usecase/solving/GetQuizBookLocalIdUseCase.kt rename app/src/main/java/com/android/quizcafe/feature/quiz/solve/component/{ExitConfirmDialog.kt => ExitSolvingDialog.kt} (96%) create mode 100644 app/src/main/java/com/android/quizcafe/feature/quiz/solve/component/ResumeSolvingDialog.kt diff --git a/app/schemas/com.android.quizcafe.core.database.QuizCafeDatabase/1.json b/app/schemas/com.android.quizcafe.core.database.QuizCafeDatabase/1.json index 6f92283b..2b7f4fc7 100644 --- a/app/schemas/com.android.quizcafe.core.database.QuizCafeDatabase/1.json +++ b/app/schemas/com.android.quizcafe.core.database.QuizCafeDatabase/1.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 1, - "identityHash": "33db5bc2ece01377a4dad8b52e0c573b", + "identityHash": "f3458608bcd684e447bc7970cc8cdfdd", "entities": [ { "tableName": "quiz", @@ -241,7 +241,17 @@ "localId" ] }, - "indices": [], + "indices": [ + { + "name": "index_QuizBookGradeEntity_quizBookId", + "unique": true, + "columnNames": [ + "quizBookId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_QuizBookGradeEntity_quizBookId` ON `${TABLE_NAME}` (`quizBookId`)" + } + ], "foreignKeys": [] }, { @@ -314,7 +324,7 @@ "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '33db5bc2ece01377a4dad8b52e0c573b')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f3458608bcd684e447bc7970cc8cdfdd')" ] } } \ No newline at end of file diff --git a/app/src/androidTest/java/com/android/quizcafe/repository/QuizBookSolvingRepositoryTest.kt b/app/src/androidTest/java/com/android/quizcafe/repository/QuizBookSolvingRepositoryTest.kt index a40a5782..b9536065 100644 --- a/app/src/androidTest/java/com/android/quizcafe/repository/QuizBookSolvingRepositoryTest.kt +++ b/app/src/androidTest/java/com/android/quizcafe/repository/QuizBookSolvingRepositoryTest.kt @@ -84,7 +84,7 @@ class QuizBookSolvingRepositoryTest { setupTestData(quizBookId) val results = mutableListOf>() - repository.createEmptyQuizBookGrade(quizBookId).collect(results::add) + repository.getOrCreateQuizBookGrade(quizBookId).collect(results::add) assertEquals(2, results.size) assertTrue(results[0] is Resource.Loading) @@ -98,7 +98,7 @@ class QuizBookSolvingRepositoryTest { setupTestData(quizBookId) // 먼저 퀴즈북 풀이 생성 - val quizBookGradeLocalId = repository.createEmptyQuizBookGrade(quizBookId).first { it is Resource.Success } + val quizBookGradeLocalId = repository.getOrCreateQuizBookGrade(quizBookId).first { it is Resource.Success } .let { (it as Resource.Success).data } val quizGrade = QuizGrade( @@ -162,7 +162,7 @@ class QuizBookSolvingRepositoryTest { val quizBookId = QuizBookId(1L) setupTestData(quizBookId) - val quizBookGradeLocalId = repository.createEmptyQuizBookGrade(quizBookId) + val quizBookGradeLocalId = repository.getOrCreateQuizBookGrade(quizBookId) .first { it is Resource.Success } .let { (it as Resource.Success).data } diff --git a/app/src/main/java/com/android/quizcafe/core/data/mapper/quizbook/QuizBookMapper.kt b/app/src/main/java/com/android/quizcafe/core/data/mapper/quizbook/QuizBookMapper.kt index 7a17064a..346bed13 100644 --- a/app/src/main/java/com/android/quizcafe/core/data/mapper/quizbook/QuizBookMapper.kt +++ b/app/src/main/java/com/android/quizcafe/core/data/mapper/quizbook/QuizBookMapper.kt @@ -7,9 +7,10 @@ import com.android.quizcafe.core.data.model.quizbook.response.QuizBookWithQuizze import com.android.quizcafe.core.database.model.quizbook.QuizBookEntity import com.android.quizcafe.core.database.model.quizbook.QuizBookWithQuizRelation import com.android.quizcafe.core.domain.model.quizbook.response.QuizBook +import com.android.quizcafe.core.domain.model.value.QuizBookId fun QuizBookEntity.toDomain() = QuizBook( - id = id, + id = QuizBookId(id), version = version, category = category, title = title, @@ -23,7 +24,7 @@ fun QuizBookEntity.toDomain() = QuizBook( ) fun QuizBookResponseDto.toDomain() = QuizBook( - id = quizBookId, + id = QuizBookId(quizBookId), version = version, category = category, description = description, @@ -49,7 +50,7 @@ fun QuizBookWithQuizzesResponseDto.toEntity() = QuizBookEntity( ) fun QuizBookWithQuizzesResponseDto.toDomain() = QuizBook( - id = quizBookId, + id = QuizBookId(quizBookId), version = version, category = category, description = description, @@ -64,7 +65,7 @@ fun QuizBookWithQuizzesResponseDto.toDomain() = QuizBook( ) fun QuizBookWithQuizRelation.toDomain() = QuizBook( - id = quizBookEntity.id, + id = QuizBookId(quizBookEntity.id), version = quizBookEntity.version, category = quizBookEntity.category, title = quizBookEntity.title, diff --git a/app/src/main/java/com/android/quizcafe/core/data/repository/QuizBookSolvingRepositoryImpl.kt b/app/src/main/java/com/android/quizcafe/core/data/repository/QuizBookSolvingRepositoryImpl.kt index d0d3f8ca..1dc21d69 100644 --- a/app/src/main/java/com/android/quizcafe/core/data/repository/QuizBookSolvingRepositoryImpl.kt +++ b/app/src/main/java/com/android/quizcafe/core/data/repository/QuizBookSolvingRepositoryImpl.kt @@ -1,6 +1,5 @@ package com.android.quizcafe.core.data.repository -import android.util.Log import com.android.quizcafe.core.common.network.HttpStatus import com.android.quizcafe.core.data.mapper.solving.toDomain import com.android.quizcafe.core.data.mapper.quiz.toEntity @@ -47,35 +46,29 @@ class QuizBookSolvingRepositoryImpl @Inject constructor( * 퀴즈북 풀기 시작할 때 호출 * localId값 반환 */ - override fun createEmptyQuizBookGrade(quizBookId: QuizBookId): Flow> = flow { + override fun getOrCreateQuizBookGrade(quizBookId: QuizBookId): Flow> = flow { emit(Resource.Loading) - val entity = QuizBookGradeEntity(quizBookId = quizBookId.value) - val generatedId = quizBookGradeDao.upsertQuizBookGrade(entity) + val quizBookGradeRelation = quizBookGradeDao.getQuizBookGrade(quizBookId.value) - if (generatedId <= 0L) { - emit(Resource.Failure(errorMessage = "QuizBookGrade 생성 실패", code = LocalErrorCode.ROOM_ERROR)) - } else { - emit(Resource.Success(QuizBookGradeLocalId(generatedId))) + if(quizBookGradeRelation != null){ + emit(Resource.Success(quizBookGradeRelation.toDomain())) + }else{ + val entity = QuizBookGradeEntity(quizBookId = quizBookId.value) + val quizBookGradeLocalId = quizBookGradeDao.upsertQuizBookGrade(entity) + + if (quizBookGradeLocalId <= 0L) { + emit(Resource.Failure(errorMessage = "QuizBookGrade 생성 실패", code = LocalErrorCode.ROOM_ERROR)) + } else { + val relation = quizBookGradeDao.getQuizBookGradeByLocalId(quizBookGradeLocalId) + relation?.let { + emit(Resource.Success(it.toDomain())) + } ?: emit(Resource.Failure(errorMessage = "QuizBookGrade 생성 후 조회 실패", code = LocalErrorCode.ROOM_ERROR)) + } } }.catch { e -> emit(Resource.Failure(errorMessage = "QuizBookGrade 생성 중 오류: ${e.message}", code = LocalErrorCode.ROOM_ERROR)) } - // 퀴즈북 풀이 기록 가져오기 - override fun getQuizBookGrade(id: QuizBookGradeLocalId): Flow> = flow { - emit(Resource.Loading) - val quizBookGradeRelation = quizBookGradeDao.getQuizBookGrade(id.value) - - if (quizBookGradeRelation == null) { - emit(Resource.Failure(errorMessage = "퀴즈북 풀이 기록을 찾을 수 없습니다", code = LocalErrorCode.ROOM_ERROR)) - } else { - val quizBookGrade = quizBookGradeRelation.toDomain() - emit(Resource.Success(quizBookGrade)) - } - }.catch { e -> - emit(Resource.Failure(errorMessage = "퀴즈북 풀이 기록 조회 중 오류: ${e.message}", code = LocalErrorCode.ROOM_ERROR)) - } - override fun deleteQuizBookGrade(id: QuizBookGradeLocalId): Flow> = flow { emit(Resource.Loading) val deletedCnt = quizBookGradeDao.deleteQuizBookGrade(id.value) @@ -102,10 +95,9 @@ class QuizBookSolvingRepositoryImpl @Inject constructor( } } - override fun getAllQuizBookSolving(): Flow>> = flow { + override fun getAllQuizBookSolving(): Flow>> = apiResponseListToResourceFlow(mapper = QuizBookSolvingResponseDto::toDomain) { quizBookSolvingRemoteDataSource.getAllQuizBookSolvingByUser() - } } // 퀴즈 1개 풀이 기록 저장 및 수정 diff --git a/app/src/main/java/com/android/quizcafe/core/database/dao/quizBook/QuizBookGradeDao.kt b/app/src/main/java/com/android/quizcafe/core/database/dao/quizBook/QuizBookGradeDao.kt index 611d571d..a477f14c 100644 --- a/app/src/main/java/com/android/quizcafe/core/database/dao/quizBook/QuizBookGradeDao.kt +++ b/app/src/main/java/com/android/quizcafe/core/database/dao/quizBook/QuizBookGradeDao.kt @@ -13,10 +13,13 @@ interface QuizBookGradeDao { @Upsert suspend fun upsertQuizBookGrade(entity: QuizBookGradeEntity): Long - // QuizBookGradeEntity LocalId로 QuizQuizBookSolvingResult 리스트 반환 + + @Transaction + @Query("SELECT * FROM QuizBookGradeEntity WHERE localId = :quizBookGradeLocalId") + suspend fun getQuizBookGradeByLocalId(quizBookGradeLocalId: Long): QuizBookGradeWithQuizGradesRelation? @Transaction - @Query("SELECT * FROM QuizBookGradeEntity WHERE localId = :localId") - suspend fun getQuizBookGrade(localId: Long): QuizBookGradeWithQuizGradesRelation? + @Query("SELECT * FROM QuizBookGradeEntity WHERE quizBookId = :quizBookId") + suspend fun getQuizBookGrade(quizBookId: Long): QuizBookGradeWithQuizGradesRelation? @Query("DELETE FROM QuizBookGradeEntity WHERE localId = :localId") suspend fun deleteQuizBookGrade(localId: Long) : Int diff --git a/app/src/main/java/com/android/quizcafe/core/database/model/grading/QuizBookGradeEntity.kt b/app/src/main/java/com/android/quizcafe/core/database/model/grading/QuizBookGradeEntity.kt index bdcc5819..c37bb3b8 100644 --- a/app/src/main/java/com/android/quizcafe/core/database/model/grading/QuizBookGradeEntity.kt +++ b/app/src/main/java/com/android/quizcafe/core/database/model/grading/QuizBookGradeEntity.kt @@ -1,10 +1,18 @@ package com.android.quizcafe.core.database.model.grading import androidx.room.Entity +import androidx.room.Index import androidx.room.PrimaryKey import kotlin.time.Duration -@Entity +@Entity( + indices = [ + Index( + value = ["quizBookId"], + unique = true + ) + ] +) data class QuizBookGradeEntity( @PrimaryKey(autoGenerate = true) val localId: Long = 0, diff --git a/app/src/main/java/com/android/quizcafe/core/domain/model/quizbook/response/QuizBook.kt b/app/src/main/java/com/android/quizcafe/core/domain/model/quizbook/response/QuizBook.kt index 79327dd8..b26f3979 100644 --- a/app/src/main/java/com/android/quizcafe/core/domain/model/quizbook/response/QuizBook.kt +++ b/app/src/main/java/com/android/quizcafe/core/domain/model/quizbook/response/QuizBook.kt @@ -1,9 +1,10 @@ package com.android.quizcafe.core.domain.model.quizbook.response import com.android.quizcafe.core.domain.model.quiz.Quiz +import com.android.quizcafe.core.domain.model.value.QuizBookId data class QuizBook( - val id: Long, + val id: QuizBookId, val version: Long, val category: String, val title: String, diff --git a/app/src/main/java/com/android/quizcafe/core/domain/repository/QuizBookSolvingRepository.kt b/app/src/main/java/com/android/quizcafe/core/domain/repository/QuizBookSolvingRepository.kt index fceb6422..2fcdee6c 100644 --- a/app/src/main/java/com/android/quizcafe/core/domain/repository/QuizBookSolvingRepository.kt +++ b/app/src/main/java/com/android/quizcafe/core/domain/repository/QuizBookSolvingRepository.kt @@ -11,10 +11,7 @@ import kotlinx.coroutines.flow.Flow interface QuizBookSolvingRepository { - fun createEmptyQuizBookGrade(id: QuizBookId): Flow> - - // 퀴즈북 풀이 로컬 기록 가져오기 - fun getQuizBookGrade(id: QuizBookGradeLocalId): Flow> + fun getOrCreateQuizBookGrade(id: QuizBookId): Flow> // 퀴즈북 풀이 로컬 기록 삭제하기 fun deleteQuizBookGrade(id : QuizBookGradeLocalId) : Flow> diff --git a/app/src/main/java/com/android/quizcafe/core/domain/usecase/solving/GetOrCreateQuizBookGradeUseCase.kt b/app/src/main/java/com/android/quizcafe/core/domain/usecase/solving/GetOrCreateQuizBookGradeUseCase.kt new file mode 100644 index 00000000..aad04f28 --- /dev/null +++ b/app/src/main/java/com/android/quizcafe/core/domain/usecase/solving/GetOrCreateQuizBookGradeUseCase.kt @@ -0,0 +1,15 @@ +package com.android.quizcafe.core.domain.usecase.solving + +import com.android.quizcafe.core.domain.model.Resource +import com.android.quizcafe.core.domain.model.solving.QuizBookGrade +import com.android.quizcafe.core.domain.model.value.QuizBookId +import com.android.quizcafe.core.domain.repository.QuizBookSolvingRepository +import kotlinx.coroutines.flow.Flow +import javax.inject.Inject + +class GetOrCreateQuizBookGradeUseCase @Inject constructor( + private val quizBookSolvingRepository: QuizBookSolvingRepository +){ + operator fun invoke(quizBookId: QuizBookId) : Flow> = + quizBookSolvingRepository.getOrCreateQuizBookGrade(quizBookId) +} diff --git a/app/src/main/java/com/android/quizcafe/core/domain/usecase/solving/GetQuizBookGradeUseCase.kt b/app/src/main/java/com/android/quizcafe/core/domain/usecase/solving/GetQuizBookGradeUseCase.kt deleted file mode 100644 index 75416f63..00000000 --- a/app/src/main/java/com/android/quizcafe/core/domain/usecase/solving/GetQuizBookGradeUseCase.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.android.quizcafe.core.domain.usecase.solving - -import com.android.quizcafe.core.domain.model.value.QuizBookGradeLocalId -import com.android.quizcafe.core.domain.repository.QuizBookSolvingRepository -import javax.inject.Inject - -/** - * 현재 QuizBookGrade에 있는 모든 QuizGrade 가져오기 - */ -class GetQuizBookGradeUseCase @Inject constructor( - private val quizBookSolvingRepository: QuizBookSolvingRepository -) { - operator fun invoke(id: QuizBookGradeLocalId) = - quizBookSolvingRepository.getQuizBookGrade(id) -} diff --git a/app/src/main/java/com/android/quizcafe/core/domain/usecase/solving/GetQuizBookLocalIdUseCase.kt b/app/src/main/java/com/android/quizcafe/core/domain/usecase/solving/GetQuizBookLocalIdUseCase.kt deleted file mode 100644 index f27717f9..00000000 --- a/app/src/main/java/com/android/quizcafe/core/domain/usecase/solving/GetQuizBookLocalIdUseCase.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.android.quizcafe.core.domain.usecase.solving - -import com.android.quizcafe.core.domain.model.value.QuizBookId -import com.android.quizcafe.core.domain.repository.QuizBookSolvingRepository -import javax.inject.Inject - -class GetQuizBookLocalIdUseCase @Inject constructor( - private val quizBookSolvingRepository: QuizBookSolvingRepository -) { - operator fun invoke(id: QuizBookId) = - quizBookSolvingRepository.createEmptyQuizBookGrade(id) -} diff --git a/app/src/main/java/com/android/quizcafe/feature/main/MainScreen.kt b/app/src/main/java/com/android/quizcafe/feature/main/MainScreen.kt index 4485ef21..0f33884c 100644 --- a/app/src/main/java/com/android/quizcafe/feature/main/MainScreen.kt +++ b/app/src/main/java/com/android/quizcafe/feature/main/MainScreen.kt @@ -113,7 +113,8 @@ fun MainBottomNavHost( QuizBookListRoute( category = category, - navigateToQuizBookDetail = { quizBookId -> bottomNavController.navigateSingleTopTo("${MainRoute.QuizBookDetail.route}/$quizBookId") }, + navigateToQuizBookDetail = { quizBookId -> + bottomNavController.navigateSingleTopTo("${MainRoute.QuizBookDetail.route}/${quizBookId.value}") }, navigateToCategory = {}, ) } diff --git a/app/src/main/java/com/android/quizcafe/feature/quiz/solve/QuizSolveRoute.kt b/app/src/main/java/com/android/quizcafe/feature/quiz/solve/QuizSolveRoute.kt index f4824ce9..7f1da228 100644 --- a/app/src/main/java/com/android/quizcafe/feature/quiz/solve/QuizSolveRoute.kt +++ b/app/src/main/java/com/android/quizcafe/feature/quiz/solve/QuizSolveRoute.kt @@ -1,5 +1,6 @@ package com.android.quizcafe.feature.quiz.solve +import android.util.Log import android.widget.Toast import androidx.activity.compose.BackHandler import androidx.compose.runtime.Composable @@ -13,7 +14,8 @@ import androidx.compose.ui.platform.LocalContext import androidx.hilt.navigation.compose.hiltViewModel import com.android.quizcafe.R import com.android.quizcafe.core.domain.model.value.QuizBookGradeServerId -import com.android.quizcafe.feature.quiz.solve.component.ExitConfirmDialog +import com.android.quizcafe.feature.quiz.solve.component.ExitSolvingDialog +import com.android.quizcafe.feature.quiz.solve.component.ResumeSolvingDialog import com.android.quizcafe.feature.quiz.solve.viewmodel.QuizSolveEffect import com.android.quizcafe.feature.quiz.solve.viewmodel.QuizSolveIntent import com.android.quizcafe.feature.quiz.solve.viewmodel.QuizSolveViewModel @@ -28,32 +30,18 @@ fun QuizSolveRoute( val context = LocalContext.current val state by viewModel.state.collectAsState() var showExitDialog by remember { mutableStateOf(false)} - - BackHandler(enabled = !showExitDialog) { - viewModel.sendIntent(QuizSolveIntent.OnBackClick) - } - - if (showExitDialog) { - ExitConfirmDialog( - onDismissRequest = { - showExitDialog = false - }, - onExitWithDelete = { - viewModel.sendIntent(QuizSolveIntent.ExitWithDelete) - }, - onExitWithSave = { - viewModel.sendIntent(QuizSolveIntent.ExitWithSave) - }, - ) - } + var showResumeDialog by remember { mutableStateOf(false)} LaunchedEffect(Unit) { - viewModel.sendIntent(QuizSolveIntent.LoadQuizBook(quizBookId)) - viewModel.sendIntent(QuizSolveIntent.GetQuizBookLocalId(quizBookId)) - } - LaunchedEffect(Unit) { + viewModel.sendIntent(QuizSolveIntent.StartSolving(quizBookId)) viewModel.effect.collect { effect -> when (effect) { + QuizSolveEffect.ShowResumeDialog -> { + showResumeDialog = true + } + QuizSolveEffect.CloseResumeDialog -> { + showResumeDialog = false + } QuizSolveEffect.ShowExitDialog -> { showExitDialog = true } @@ -75,6 +63,35 @@ fun QuizSolveRoute( } } + BackHandler(enabled = !showExitDialog) { + viewModel.sendIntent(QuizSolveIntent.OnBackClick) + } + + if (showResumeDialog){ + ResumeSolvingDialog( + onResume = { + viewModel.sendIntent(QuizSolveIntent.ResumeSolving(resumeWithNewSolving = false)) + }, + onStartNew = { + viewModel.sendIntent(QuizSolveIntent.ResumeSolving(resumeWithNewSolving = true)) + } + ) + } + + if (showExitDialog) { + ExitSolvingDialog( + onDismissRequest = { + showExitDialog = false + }, + onExitWithDelete = { + viewModel.sendIntent(QuizSolveIntent.ExitWithDelete) + }, + onExitWithSave = { + viewModel.sendIntent(QuizSolveIntent.ExitWithSave) + }, + ) + } + QuizSolveScreen( uiState = state, onIntent = viewModel::sendIntent diff --git a/app/src/main/java/com/android/quizcafe/feature/quiz/solve/component/ExitConfirmDialog.kt b/app/src/main/java/com/android/quizcafe/feature/quiz/solve/component/ExitSolvingDialog.kt similarity index 96% rename from app/src/main/java/com/android/quizcafe/feature/quiz/solve/component/ExitConfirmDialog.kt rename to app/src/main/java/com/android/quizcafe/feature/quiz/solve/component/ExitSolvingDialog.kt index a715cb87..86df9fcd 100644 --- a/app/src/main/java/com/android/quizcafe/feature/quiz/solve/component/ExitConfirmDialog.kt +++ b/app/src/main/java/com/android/quizcafe/feature/quiz/solve/component/ExitSolvingDialog.kt @@ -18,7 +18,7 @@ import com.android.quizcafe.R import com.android.quizcafe.core.designsystem.theme.QuizCafeTheme @Composable -fun ExitConfirmDialog( +fun ExitSolvingDialog( onDismissRequest: () -> Unit, onExitWithDelete: () -> Unit, onExitWithSave: () -> Unit @@ -101,11 +101,11 @@ fun ExitConfirmDialog( } -@Preview(name = "Exit Confirmation Dialog", showBackground = true) +@Preview(name = "ExitSolvingDialog", showBackground = true) @Composable -fun ExitConfirmationDialogPreview() { +fun ExitSolvingDialogPreview() { QuizCafeTheme { - ExitConfirmDialog ( + ExitSolvingDialog ( onDismissRequest = {}, onExitWithDelete = {}, onExitWithSave = {} diff --git a/app/src/main/java/com/android/quizcafe/feature/quiz/solve/component/ResumeSolvingDialog.kt b/app/src/main/java/com/android/quizcafe/feature/quiz/solve/component/ResumeSolvingDialog.kt new file mode 100644 index 00000000..b9ffb996 --- /dev/null +++ b/app/src/main/java/com/android/quizcafe/feature/quiz/solve/component/ResumeSolvingDialog.kt @@ -0,0 +1,121 @@ +package com.android.quizcafe.feature.quiz.solve.component + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import com.android.quizcafe.R +import com.android.quizcafe.core.designsystem.theme.QuizCafeTheme + + +@Composable +fun ResumeSolvingDialog( + onResume: () -> Unit, + onStartNew: () -> Unit +) { + Dialog( + onDismissRequest = { }, + properties = DialogProperties( + dismissOnBackPress = false, + dismissOnClickOutside = false + ) + ) { + Card( + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surface, + ), + modifier = Modifier.fillMaxWidth() + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp) + .padding(top = 32.dp, bottom = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + text = stringResource(R.string.dialog_resume_title), + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurface + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = stringResource(R.string.dialog_resume_description), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.height(16.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically + ) { + TextButton( + onClick = onStartNew, + colors = ButtonDefaults.textButtonColors( + contentColor = MaterialTheme.colorScheme.onSurface + ) + ) { + Text(text = stringResource(R.string.dialog_start_new_solving)) + } + Spacer(modifier = Modifier.width(8.dp)) + TextButton( + onClick = onResume, + colors = ButtonDefaults.textButtonColors( + contentColor = MaterialTheme.colorScheme.onSurface + ) + ) { + Text(text = stringResource(R.string.dialog_resume_solving)) + } + + + + } + + } + } + } +} + + +@Preview(name = "ResumeSolvingDialog", showBackground = true) +@Composable +fun ResumeSolvingDialogPreview() { + QuizCafeTheme { + ResumeSolvingDialog ( + onResume = {}, + onStartNew = {} + ) + } +} diff --git a/app/src/main/java/com/android/quizcafe/feature/quiz/solve/viewmodel/QuizSolveEffect.kt b/app/src/main/java/com/android/quizcafe/feature/quiz/solve/viewmodel/QuizSolveEffect.kt index c428f8ed..a33affe2 100644 --- a/app/src/main/java/com/android/quizcafe/feature/quiz/solve/viewmodel/QuizSolveEffect.kt +++ b/app/src/main/java/com/android/quizcafe/feature/quiz/solve/viewmodel/QuizSolveEffect.kt @@ -6,6 +6,8 @@ import com.android.quizcafe.core.ui.base.BaseContract sealed class QuizSolveEffect : BaseContract.ViewEffect { data class ShowErrorDialog(val message: String) : QuizSolveEffect() data object ShowExitDialog : QuizSolveEffect() + data object ShowResumeDialog : QuizSolveEffect() + data object CloseResumeDialog : QuizSolveEffect() data object NavigateToBack : QuizSolveEffect() data class NavigateToQuizBookSolvingResult(val quizBookGradeServerId: QuizBookGradeServerId) : QuizSolveEffect() } diff --git a/app/src/main/java/com/android/quizcafe/feature/quiz/solve/viewmodel/QuizSolveIntent.kt b/app/src/main/java/com/android/quizcafe/feature/quiz/solve/viewmodel/QuizSolveIntent.kt index b368459b..9c3a081e 100644 --- a/app/src/main/java/com/android/quizcafe/feature/quiz/solve/viewmodel/QuizSolveIntent.kt +++ b/app/src/main/java/com/android/quizcafe/feature/quiz/solve/viewmodel/QuizSolveIntent.kt @@ -2,17 +2,16 @@ package com.android.quizcafe.feature.quiz.solve.viewmodel import com.android.quizcafe.core.domain.model.quizbook.response.QuizBook import com.android.quizcafe.core.domain.model.solving.QuizBookGrade -import com.android.quizcafe.core.domain.model.value.QuizBookGradeLocalId import com.android.quizcafe.core.domain.model.value.QuizBookGradeServerId import com.android.quizcafe.core.ui.base.BaseContract sealed class QuizSolveIntent : BaseContract.ViewIntent { - data class LoadQuizBook(val quizBookId: Long) : QuizSolveIntent() - data class SuccessGetQuizBook(val data: QuizBook) : QuizSolveIntent() - data class SuccessGetQuizBookGradeResult(val quizBookGrade: QuizBookGrade) : QuizSolveIntent() - data class GetQuizBookLocalId(val quizBookId: Long) : QuizSolveIntent() + data class StartSolving(val quizBookId: Long) : QuizSolveIntent() + data class ResumeSolving(val resumeWithNewSolving : Boolean) : QuizSolveIntent() - data class GetQuizBookGradeResult(val quizBookLocalId: QuizBookGradeLocalId) : QuizSolveIntent() + data class SuccessGetQuizBook(val data: QuizBook) : QuizSolveIntent() + data class SuccessGetQuizBookGrade(val quizBookGrade: QuizBookGrade) : QuizSolveIntent() + data object StartTimer : QuizSolveIntent() data class SelectOption(val option: QuizOption) : QuizSolveIntent() data class UpdatedSubjectiveAnswer(val answer: String) : QuizSolveIntent() diff --git a/app/src/main/java/com/android/quizcafe/feature/quiz/solve/viewmodel/QuizSolveUiState.kt b/app/src/main/java/com/android/quizcafe/feature/quiz/solve/viewmodel/QuizSolveUiState.kt index 3f7d92c0..b1b28e9e 100644 --- a/app/src/main/java/com/android/quizcafe/feature/quiz/solve/viewmodel/QuizSolveUiState.kt +++ b/app/src/main/java/com/android/quizcafe/feature/quiz/solve/viewmodel/QuizSolveUiState.kt @@ -3,6 +3,7 @@ package com.android.quizcafe.feature.quiz.solve.viewmodel import com.android.quizcafe.core.domain.model.quiz.Quiz import com.android.quizcafe.core.domain.model.quiz.QuizGrade import com.android.quizcafe.core.domain.model.quizbook.response.QuizBook +import com.android.quizcafe.core.domain.model.solving.QuizBookGrade import com.android.quizcafe.core.domain.model.value.QuizBookGradeLocalId import com.android.quizcafe.core.ui.base.BaseContract import com.android.quizcafe.feature.quiz.solve.component.AnswerState @@ -52,15 +53,15 @@ data class TimerState( ) data class CommonState( - val isButtonEnabled: Boolean = false + val isButtonEnabled: Boolean = false, + val isTimerActive: Boolean = false ) data class QuizSolveUiState( val isLoading: Boolean = false, val errorMessage: String? = null, - val quizBookLocalId: QuizBookGradeLocalId? = null, val quizBook: QuizBook? = null, - val quizGrades: List = emptyList(), + val quizBookGrade : QuizBookGrade? = null, val currentIndex: Int = 0, val mcq: McqState = McqState( options = listOf( @@ -98,7 +99,7 @@ data class QuizSolveUiState( ) private val currentGrade: QuizGrade? - get() = quizGrades.getOrNull(currentIndex) + get() = quizBookGrade?.quizGrades?.getOrNull(currentIndex) private val currentPhase: AnswerPhase get() = if (currentGrade != null) AnswerPhase.REVIEW else AnswerPhase.ANSWERING diff --git a/app/src/main/java/com/android/quizcafe/feature/quiz/solve/viewmodel/QuizSolveViewModel.kt b/app/src/main/java/com/android/quizcafe/feature/quiz/solve/viewmodel/QuizSolveViewModel.kt index 79559d50..982afc4b 100644 --- a/app/src/main/java/com/android/quizcafe/feature/quiz/solve/viewmodel/QuizSolveViewModel.kt +++ b/app/src/main/java/com/android/quizcafe/feature/quiz/solve/viewmodel/QuizSolveViewModel.kt @@ -3,12 +3,10 @@ package com.android.quizcafe.feature.quiz.solve.viewmodel import android.util.Log import androidx.lifecycle.viewModelScope import com.android.quizcafe.core.domain.model.Resource -import com.android.quizcafe.core.domain.model.value.QuizBookGradeLocalId import com.android.quizcafe.core.domain.model.value.QuizBookId import com.android.quizcafe.core.domain.usecase.quizbook.GetQuizBookUseCase import com.android.quizcafe.core.domain.usecase.solving.DeleteQuizBookGradeUseCase -import com.android.quizcafe.core.domain.usecase.solving.GetQuizBookGradeUseCase -import com.android.quizcafe.core.domain.usecase.solving.GetQuizBookLocalIdUseCase +import com.android.quizcafe.core.domain.usecase.solving.GetOrCreateQuizBookGradeUseCase import com.android.quizcafe.core.domain.usecase.solving.GradeQuizUseCase import com.android.quizcafe.core.domain.usecase.solving.SolveQuizBookUseCase import com.android.quizcafe.core.ui.base.BaseViewModel @@ -20,11 +18,10 @@ import javax.inject.Inject @HiltViewModel class QuizSolveViewModel @Inject constructor( private val getQuizBookUseCase: GetQuizBookUseCase, - private val getQuizBookLocalIdUseCase: GetQuizBookLocalIdUseCase, - private val getQuizBookGradeUseCase: GetQuizBookGradeUseCase, + private val getOrCreateQuizBookGradeUseCase: GetOrCreateQuizBookGradeUseCase, private val gradeQuizUseCase: GradeQuizUseCase, private val solveQuizBookUseCase: SolveQuizBookUseCase, - private val deleteQuizBookGradeUseCase: DeleteQuizBookGradeUseCase + private val deleteQuizBookGradeUseCase: DeleteQuizBookGradeUseCase, ) : BaseViewModel( initialState = QuizSolveUiState() ) { @@ -32,27 +29,35 @@ class QuizSolveViewModel @Inject constructor( viewModelScope.launch { while (true) { delay(1_000L) - sendIntent(QuizSolveIntent.TickTime) + if(state.value.common.isTimerActive){ + sendIntent(QuizSolveIntent.TickTime) + } } } } override suspend fun handleIntent(intent: QuizSolveIntent) { - when (intent) { - QuizSolveIntent.OnBackClick -> { - emitEffect(QuizSolveEffect.ShowExitDialog) - } - is QuizSolveIntent.LoadQuizBook -> { - getQuizBook(intent.quizBookId) + when (intent) { + is QuizSolveIntent.StartSolving -> { + val quizBookId = QuizBookId(intent.quizBookId) + getQuizBook(quizBookId) + getQuizBookGrade(quizBookId) } - - is QuizSolveIntent.GetQuizBookLocalId -> { - getQuizBookLocalId(intent.quizBookId) + is QuizSolveIntent.ResumeSolving -> { + if(intent.resumeWithNewSolving){ + val quizBookId = state.value.quizBook?.id + deleteQuizBookGrade() + quizBookId?.let { + getQuizBookGrade(it) + } + } + emitEffect(QuizSolveEffect.CloseResumeDialog) + sendIntent(QuizSolveIntent.StartTimer) } - is QuizSolveIntent.GetQuizBookGradeResult -> { - getQuizBookGrade(intent.quizBookLocalId) + QuizSolveIntent.OnBackClick -> { + emitEffect(QuizSolveEffect.ShowExitDialog) } is QuizSolveIntent.SubmitNext -> { @@ -64,6 +69,7 @@ class QuizSolveViewModel @Inject constructor( is QuizSolveIntent.ExitWithDelete -> { deleteQuizBookGrade() + emitEffect(QuizSolveEffect.NavigateToBack) } is QuizSolveIntent.ExitWithSave -> { emitEffect(QuizSolveEffect.NavigateToBack) @@ -81,6 +87,11 @@ class QuizSolveViewModel @Inject constructor( override fun reduce(currentState: QuizSolveUiState, intent: QuizSolveIntent): QuizSolveUiState { return when (intent) { + QuizSolveIntent.StartTimer -> { + currentState.copy( + common = currentState.common.copy(isTimerActive = true) + ) + } QuizSolveIntent.TickTime -> { val timer = currentState.timer when (timer.playMode) { @@ -129,7 +140,6 @@ class QuizSolveViewModel @Inject constructor( ) ) - is QuizSolveIntent.LoadQuizBook -> currentState.copy(isLoading = true, errorMessage = null) is QuizSolveIntent.SuccessGetQuizBook -> { currentState.copy( quizBook = intent.data, @@ -137,15 +147,12 @@ class QuizSolveViewModel @Inject constructor( ) } - is QuizSolveIntent.GetQuizBookGradeResult -> - currentState.copy( - quizBookLocalId = intent.quizBookLocalId - ) - - is QuizSolveIntent.SuccessGetQuizBookGradeResult -> + is QuizSolveIntent.SuccessGetQuizBookGrade ->{ currentState.copy( - quizGrades = intent.quizBookGrade.quizGrades + quizBookGrade = intent.quizBookGrade, + currentIndex = intent.quizBookGrade.quizGrades.size ) + } QuizSolveIntent.GradeQuizSuccess -> { if (!currentState.isLastQuestion) { currentState.copy( @@ -159,9 +166,9 @@ class QuizSolveViewModel @Inject constructor( } } - private suspend fun getQuizBook(id: Long) { + private suspend fun getQuizBook(quizBookId: QuizBookId) { getQuizBookUseCase( - QuizBookId(id) + quizBookId ).collect { when (it) { is Resource.Success -> { @@ -179,36 +186,20 @@ class QuizSolveViewModel @Inject constructor( } } } - - private suspend fun getQuizBookLocalId(id: Long) { - getQuizBookLocalIdUseCase( - QuizBookId(id) - ).collect { - when (it) { - is Resource.Success -> { - Log.d("getQuizBookLocalId", "Get QuizBookDetail Success") - // 로컬 id를 uiState에 저장 reduce - sendIntent(QuizSolveIntent.GetQuizBookGradeResult(it.data)) - } - - is Resource.Loading -> { - Log.d("getQuizBookLocalId", "Loading") - } - - is Resource.Failure -> { - Log.d("getQuizBookLocalId", it.errorMessage) - } - } - } - } - private suspend fun getQuizBookGrade(id: QuizBookGradeLocalId) { - getQuizBookGradeUseCase( - id - ).collect { + // 여기서 QuizGrade 정보들이 있냐없냐로 ResumeDialog 띄울지 말지 결정 + private suspend fun getQuizBookGrade( + quizBookId: QuizBookId, + ){ + getOrCreateQuizBookGradeUseCase(quizBookId = quizBookId).collect { when (it) { is Resource.Success -> { Log.d("getQuizBookGradeUseCase", "Get QuizBookDetail Success") - sendIntent(QuizSolveIntent.SuccessGetQuizBookGradeResult(it.data)) + Log.d("getOrCreateQuizBookGradeUseCase", "quizBookGrade : ${it.data}") + val isResume = it.data.quizGrades.isNotEmpty() + if(isResume){ + emitEffect(QuizSolveEffect.ShowResumeDialog) + } + sendIntent(QuizSolveIntent.SuccessGetQuizBookGrade(it.data)) } is Resource.Loading -> { @@ -222,13 +213,12 @@ class QuizSolveViewModel @Inject constructor( } } private suspend fun deleteQuizBookGrade() { - val quizBookLocalId = state.value.quizBookLocalId + val quizBookLocalId = state.value.quizBookGrade?.localId quizBookLocalId?.let { deleteQuizBookGradeUseCase(it).collect { when (it) { is Resource.Success -> { Log.d("deleteQuizBookGradeUseCase", "Get QuizBookDetail Success") - emitEffect(QuizSolveEffect.NavigateToBack) } is Resource.Failure -> { Log.d("deleteQuizBookGradeUseCase", it.errorMessage) @@ -241,7 +231,7 @@ class QuizSolveViewModel @Inject constructor( private suspend fun saveQuizToLocal() { val uiState = state.value val quiz = uiState.currentQuiz - val localId = uiState.quizBookLocalId + val localId = uiState.quizBookGrade?.localId val userAnswer = when (uiState.questionInfo.type) { QuestionType.SUBJECTIVE -> uiState.subjective.answer else -> uiState.mcq.selectedContent @@ -273,11 +263,15 @@ class QuizSolveViewModel @Inject constructor( } } } else { + Log.d( + "saveQuizToLocal", + "quiz: $quiz, localId: $localId, userAnswer: $userAnswer" + ) sendIntent(QuizSolveIntent.GradeQuizError("문제 발생")) } } private suspend fun submitQuizAnswer() { - val localId = state.value.quizBookLocalId + val localId = state.value.quizBookGrade?.localId val elapsedTimeInSeconds: Long = state.value.timer.elapsedSeconds.toLong() if (localId != null) { solveQuizBookUseCase( diff --git a/app/src/main/java/com/android/quizcafe/feature/quizbooklist/QuizBookListContent.kt b/app/src/main/java/com/android/quizcafe/feature/quizbooklist/QuizBookListContent.kt index 09b5c9db..e726908d 100644 --- a/app/src/main/java/com/android/quizcafe/feature/quizbooklist/QuizBookListContent.kt +++ b/app/src/main/java/com/android/quizcafe/feature/quizbooklist/QuizBookListContent.kt @@ -32,11 +32,12 @@ import com.android.quizcafe.core.designsystem.theme.onPrimaryLight import com.android.quizcafe.core.designsystem.theme.outlineLight import com.android.quizcafe.core.designsystem.theme.quizCafeTypography import com.android.quizcafe.core.domain.model.quizbook.response.QuizBook +import com.android.quizcafe.core.domain.model.value.QuizBookId @Composable fun QuizBookCardList( quizBooks: List, - onQuizBookClick: (Long) -> Unit + onQuizBookClick: (QuizBookId) -> Unit ) { LazyColumn { items(quizBooks) { quizBook -> @@ -48,7 +49,7 @@ fun QuizBookCardList( @Composable fun QuizBookCard( quizBook: QuizBook, - onQuizBookClick: (Long) -> Unit + onQuizBookClick: (QuizBookId) -> Unit ) { Spacer(Modifier.height(8.dp)) Card( @@ -181,7 +182,7 @@ fun QuizBookFilterButton( fun QuizBookCardListPreview() { val sampleQuizBooks = listOf( QuizBook( - id = 1L, + id = QuizBookId(1L), ownerName = "시스템 관리자", category = "운영체제", title = "모두의 운영체제", @@ -194,7 +195,7 @@ fun QuizBookCardListPreview() { level = TODO() ), QuizBook( - id = 2L, + id = QuizBookId(2L), ownerName = "싸피_박성준", category = "운영체제", title = "성준이의 운영체제", diff --git a/app/src/main/java/com/android/quizcafe/feature/quizbooklist/QuizBookListContract.kt b/app/src/main/java/com/android/quizcafe/feature/quizbooklist/QuizBookListContract.kt index 85a0c251..75c40827 100644 --- a/app/src/main/java/com/android/quizcafe/feature/quizbooklist/QuizBookListContract.kt +++ b/app/src/main/java/com/android/quizcafe/feature/quizbooklist/QuizBookListContract.kt @@ -1,6 +1,7 @@ package com.android.quizcafe.feature.quizbooklist import com.android.quizcafe.core.domain.model.quizbook.response.QuizBook +import com.android.quizcafe.core.domain.model.value.QuizBookId import com.android.quizcafe.core.ui.base.BaseContract data class QuizBookListUiState( @@ -14,7 +15,7 @@ data class QuizBookListUiState( sealed class QuizBookListIntent : BaseContract.ViewIntent { data object LoadQuizBooks : QuizBookListIntent() - data class ClickQuizBook(val quizBookId: Long) : QuizBookListIntent() + data class ClickQuizBook(val quizBookId: QuizBookId) : QuizBookListIntent() data class UpdateCategory(val category: String) : QuizBookListIntent() data class UpdateFilterOptions(val filterState: FilterState) : QuizBookListIntent() @@ -26,5 +27,5 @@ sealed class QuizBookListIntent : BaseContract.ViewIntent { sealed class QuizBookListEffect : BaseContract.ViewEffect { data class ShowError(val message: String) : QuizBookListEffect() data object NavigateToCategory : QuizBookListEffect() - data class NavigateToQuizBookDetail(val quizBookId: Long) : QuizBookListEffect() + data class NavigateToQuizBookDetail(val quizBookId: QuizBookId) : QuizBookListEffect() } diff --git a/app/src/main/java/com/android/quizcafe/feature/quizbooklist/QuizBookListRoute.kt b/app/src/main/java/com/android/quizcafe/feature/quizbooklist/QuizBookListRoute.kt index df0def5b..a54da96c 100644 --- a/app/src/main/java/com/android/quizcafe/feature/quizbooklist/QuizBookListRoute.kt +++ b/app/src/main/java/com/android/quizcafe/feature/quizbooklist/QuizBookListRoute.kt @@ -8,11 +8,12 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.platform.LocalContext import androidx.hilt.navigation.compose.hiltViewModel import com.android.quizcafe.R +import com.android.quizcafe.core.domain.model.value.QuizBookId @Composable fun QuizBookListRoute( category: String, - navigateToQuizBookDetail: (Long) -> Unit, + navigateToQuizBookDetail: (QuizBookId) -> Unit, navigateToCategory: () -> Unit, viewModel: QuizBookListViewModel = hiltViewModel() ) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c051da8b..800e7283 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -99,7 +99,14 @@ 기록을 저장하지 않고 종료하면 현재까지의 풀이 기록은 삭제됩니다. + 이전에 풀던 기록이 있어요 + + 이어서 푸시겠어요, 아니면 새로 시작하시겠어요? + + 삭제 저장 + 이어서 풀기" + 새로 풀기 \ No newline at end of file From 766f4c8a353086b524588e441970954eea1a8d2b Mon Sep 17 00:00:00 2001 From: Jae Woong Date: Tue, 24 Jun 2025 17:16:45 +0900 Subject: [PATCH 3/7] =?UTF-8?q?chore=20:=20ktlint=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../QuizBookSolvingRepositoryTest.kt | 2 +- .../QuizBookSolvingRemoteDataSource.kt | 2 -- .../QuizBookSolvingRepositoryImpl.kt | 8 ++++---- .../database/dao/quizBook/QuizBookGradeDao.kt | 4 ++-- .../repository/QuizBookSolvingRepository.kt | 2 +- .../GetOrCreateQuizBookGradeUseCase.kt | 6 +++--- .../quizcafe/feature/main/MainScreen.kt | 3 ++- .../feature/quiz/solve/QuizSolveRoute.kt | 9 ++++----- .../feature/quiz/solve/QuizSolveScreen.kt | 4 ---- .../quiz/solve/component/ExitSolvingDialog.kt | 4 +--- .../solve/component/ResumeSolvingDialog.kt | 15 ++------------- .../quiz/solve/viewmodel/QuizSolveIntent.kt | 2 +- .../quiz/solve/viewmodel/QuizSolveUiState.kt | 3 +-- .../solve/viewmodel/QuizSolveViewModel.kt | 19 ++++++++++--------- .../QuizBookSolvingResultRoute.kt | 1 - 15 files changed, 32 insertions(+), 52 deletions(-) diff --git a/app/src/androidTest/java/com/android/quizcafe/repository/QuizBookSolvingRepositoryTest.kt b/app/src/androidTest/java/com/android/quizcafe/repository/QuizBookSolvingRepositoryTest.kt index b9536065..bb7b5114 100644 --- a/app/src/androidTest/java/com/android/quizcafe/repository/QuizBookSolvingRepositoryTest.kt +++ b/app/src/androidTest/java/com/android/quizcafe/repository/QuizBookSolvingRepositoryTest.kt @@ -204,7 +204,7 @@ class QuizBookSolvingRepositoryTest { // When val solveResults = mutableListOf>() - repository.solveQuizBook(quizBookGradeLocalId,1L).collect(solveResults::add) + repository.solveQuizBook(quizBookGradeLocalId, 1L).collect(solveResults::add) // Then assertEquals(2, solveResults.size) diff --git a/app/src/main/java/com/android/quizcafe/core/data/remote/datasource/QuizBookSolvingRemoteDataSource.kt b/app/src/main/java/com/android/quizcafe/core/data/remote/datasource/QuizBookSolvingRemoteDataSource.kt index 7b56d5d7..18f47e0a 100644 --- a/app/src/main/java/com/android/quizcafe/core/data/remote/datasource/QuizBookSolvingRemoteDataSource.kt +++ b/app/src/main/java/com/android/quizcafe/core/data/remote/datasource/QuizBookSolvingRemoteDataSource.kt @@ -1,6 +1,5 @@ package com.android.quizcafe.core.data.remote.datasource -import android.util.Log import com.android.quizcafe.core.data.model.solving.request.QuizBookSolvingRequestDto import com.android.quizcafe.core.data.model.solving.response.QuizBookSolvingResponseDto import com.android.quizcafe.core.data.remote.service.QuizBookSolvingService @@ -20,7 +19,6 @@ class QuizBookSolvingRemoteDataSource @Inject constructor( ): NetworkResult> = quizBookSolvingService.getQuizBookSolving(quizBookSolvingId) - suspend fun solveQuizBook( request: QuizBookSolvingRequestDto ): NetworkResult> = diff --git a/app/src/main/java/com/android/quizcafe/core/data/repository/QuizBookSolvingRepositoryImpl.kt b/app/src/main/java/com/android/quizcafe/core/data/repository/QuizBookSolvingRepositoryImpl.kt index 1dc21d69..e850f3cc 100644 --- a/app/src/main/java/com/android/quizcafe/core/data/repository/QuizBookSolvingRepositoryImpl.kt +++ b/app/src/main/java/com/android/quizcafe/core/data/repository/QuizBookSolvingRepositoryImpl.kt @@ -50,9 +50,9 @@ class QuizBookSolvingRepositoryImpl @Inject constructor( emit(Resource.Loading) val quizBookGradeRelation = quizBookGradeDao.getQuizBookGrade(quizBookId.value) - if(quizBookGradeRelation != null){ + if (quizBookGradeRelation != null) { emit(Resource.Success(quizBookGradeRelation.toDomain())) - }else{ + } else { val entity = QuizBookGradeEntity(quizBookId = quizBookId.value) val quizBookGradeLocalId = quizBookGradeDao.upsertQuizBookGrade(entity) @@ -74,7 +74,7 @@ class QuizBookSolvingRepositoryImpl @Inject constructor( val deletedCnt = quizBookGradeDao.deleteQuizBookGrade(id.value) if (deletedCnt == 1) { emit(Resource.Success(Unit)) - }else{ + } else { emit(Resource.Failure(errorMessage = "퀴즈북 풀이 기록 삭제 중 오류 : deletedCnt = $deletedCnt", code = LocalErrorCode.ROOM_ERROR)) } }.catch { e -> @@ -98,7 +98,7 @@ class QuizBookSolvingRepositoryImpl @Inject constructor( override fun getAllQuizBookSolving(): Flow>> = apiResponseListToResourceFlow(mapper = QuizBookSolvingResponseDto::toDomain) { quizBookSolvingRemoteDataSource.getAllQuizBookSolvingByUser() - } + } // 퀴즈 1개 풀이 기록 저장 및 수정 override fun upsertQuizGrade(quizGrade: QuizGrade): Flow> = flow { diff --git a/app/src/main/java/com/android/quizcafe/core/database/dao/quizBook/QuizBookGradeDao.kt b/app/src/main/java/com/android/quizcafe/core/database/dao/quizBook/QuizBookGradeDao.kt index a477f14c..36481666 100644 --- a/app/src/main/java/com/android/quizcafe/core/database/dao/quizBook/QuizBookGradeDao.kt +++ b/app/src/main/java/com/android/quizcafe/core/database/dao/quizBook/QuizBookGradeDao.kt @@ -13,14 +13,14 @@ interface QuizBookGradeDao { @Upsert suspend fun upsertQuizBookGrade(entity: QuizBookGradeEntity): Long - @Transaction @Query("SELECT * FROM QuizBookGradeEntity WHERE localId = :quizBookGradeLocalId") suspend fun getQuizBookGradeByLocalId(quizBookGradeLocalId: Long): QuizBookGradeWithQuizGradesRelation? + @Transaction @Query("SELECT * FROM QuizBookGradeEntity WHERE quizBookId = :quizBookId") suspend fun getQuizBookGrade(quizBookId: Long): QuizBookGradeWithQuizGradesRelation? @Query("DELETE FROM QuizBookGradeEntity WHERE localId = :localId") - suspend fun deleteQuizBookGrade(localId: Long) : Int + suspend fun deleteQuizBookGrade(localId: Long): Int } diff --git a/app/src/main/java/com/android/quizcafe/core/domain/repository/QuizBookSolvingRepository.kt b/app/src/main/java/com/android/quizcafe/core/domain/repository/QuizBookSolvingRepository.kt index 2fcdee6c..a5ab3eb6 100644 --- a/app/src/main/java/com/android/quizcafe/core/domain/repository/QuizBookSolvingRepository.kt +++ b/app/src/main/java/com/android/quizcafe/core/domain/repository/QuizBookSolvingRepository.kt @@ -14,7 +14,7 @@ interface QuizBookSolvingRepository { fun getOrCreateQuizBookGrade(id: QuizBookId): Flow> // 퀴즈북 풀이 로컬 기록 삭제하기 - fun deleteQuizBookGrade(id : QuizBookGradeLocalId) : Flow> + fun deleteQuizBookGrade(id: QuizBookGradeLocalId): Flow> // 퀴즈북 풀이 완료 기록 가져오기 (서버) fun getQuizBookSolving(id: QuizBookGradeServerId): Flow> diff --git a/app/src/main/java/com/android/quizcafe/core/domain/usecase/solving/GetOrCreateQuizBookGradeUseCase.kt b/app/src/main/java/com/android/quizcafe/core/domain/usecase/solving/GetOrCreateQuizBookGradeUseCase.kt index aad04f28..003cf02a 100644 --- a/app/src/main/java/com/android/quizcafe/core/domain/usecase/solving/GetOrCreateQuizBookGradeUseCase.kt +++ b/app/src/main/java/com/android/quizcafe/core/domain/usecase/solving/GetOrCreateQuizBookGradeUseCase.kt @@ -9,7 +9,7 @@ import javax.inject.Inject class GetOrCreateQuizBookGradeUseCase @Inject constructor( private val quizBookSolvingRepository: QuizBookSolvingRepository -){ - operator fun invoke(quizBookId: QuizBookId) : Flow> = - quizBookSolvingRepository.getOrCreateQuizBookGrade(quizBookId) +) { + operator fun invoke(quizBookId: QuizBookId): Flow> = + quizBookSolvingRepository.getOrCreateQuizBookGrade(quizBookId) } diff --git a/app/src/main/java/com/android/quizcafe/feature/main/MainScreen.kt b/app/src/main/java/com/android/quizcafe/feature/main/MainScreen.kt index 0f33884c..1db39bbe 100644 --- a/app/src/main/java/com/android/quizcafe/feature/main/MainScreen.kt +++ b/app/src/main/java/com/android/quizcafe/feature/main/MainScreen.kt @@ -114,7 +114,8 @@ fun MainBottomNavHost( QuizBookListRoute( category = category, navigateToQuizBookDetail = { quizBookId -> - bottomNavController.navigateSingleTopTo("${MainRoute.QuizBookDetail.route}/${quizBookId.value}") }, + bottomNavController.navigateSingleTopTo("${MainRoute.QuizBookDetail.route}/${quizBookId.value}") + }, navigateToCategory = {}, ) } diff --git a/app/src/main/java/com/android/quizcafe/feature/quiz/solve/QuizSolveRoute.kt b/app/src/main/java/com/android/quizcafe/feature/quiz/solve/QuizSolveRoute.kt index 7f1da228..c8fd161a 100644 --- a/app/src/main/java/com/android/quizcafe/feature/quiz/solve/QuizSolveRoute.kt +++ b/app/src/main/java/com/android/quizcafe/feature/quiz/solve/QuizSolveRoute.kt @@ -1,6 +1,5 @@ package com.android.quizcafe.feature.quiz.solve -import android.util.Log import android.widget.Toast import androidx.activity.compose.BackHandler import androidx.compose.runtime.Composable @@ -29,8 +28,8 @@ fun QuizSolveRoute( ) { val context = LocalContext.current val state by viewModel.state.collectAsState() - var showExitDialog by remember { mutableStateOf(false)} - var showResumeDialog by remember { mutableStateOf(false)} + var showExitDialog by remember { mutableStateOf(false) } + var showResumeDialog by remember { mutableStateOf(false) } LaunchedEffect(Unit) { viewModel.sendIntent(QuizSolveIntent.StartSolving(quizBookId)) @@ -46,7 +45,7 @@ fun QuizSolveRoute( showExitDialog = true } QuizSolveEffect.NavigateToBack -> { - if(showExitDialog) showExitDialog = false + if (showExitDialog) showExitDialog = false navigateToBack() } is QuizSolveEffect.NavigateToQuizBookSolvingResult -> { @@ -67,7 +66,7 @@ fun QuizSolveRoute( viewModel.sendIntent(QuizSolveIntent.OnBackClick) } - if (showResumeDialog){ + if (showResumeDialog) { ResumeSolvingDialog( onResume = { viewModel.sendIntent(QuizSolveIntent.ResumeSolving(resumeWithNewSolving = false)) diff --git a/app/src/main/java/com/android/quizcafe/feature/quiz/solve/QuizSolveScreen.kt b/app/src/main/java/com/android/quizcafe/feature/quiz/solve/QuizSolveScreen.kt index 2406a2f9..7a8a679e 100644 --- a/app/src/main/java/com/android/quizcafe/feature/quiz/solve/QuizSolveScreen.kt +++ b/app/src/main/java/com/android/quizcafe/feature/quiz/solve/QuizSolveScreen.kt @@ -15,8 +15,6 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -119,8 +117,6 @@ fun QuizSolveScreen( } } } - - } } } diff --git a/app/src/main/java/com/android/quizcafe/feature/quiz/solve/component/ExitSolvingDialog.kt b/app/src/main/java/com/android/quizcafe/feature/quiz/solve/component/ExitSolvingDialog.kt index 86df9fcd..7c9db175 100644 --- a/app/src/main/java/com/android/quizcafe/feature/quiz/solve/component/ExitSolvingDialog.kt +++ b/app/src/main/java/com/android/quizcafe/feature/quiz/solve/component/ExitSolvingDialog.kt @@ -93,19 +93,17 @@ fun ExitSolvingDialog( Text(text = stringResource(R.string.dialog_exit_with_save)) } } - } } } } } - @Preview(name = "ExitSolvingDialog", showBackground = true) @Composable fun ExitSolvingDialogPreview() { QuizCafeTheme { - ExitSolvingDialog ( + ExitSolvingDialog( onDismissRequest = {}, onExitWithDelete = {}, onExitWithSave = {} diff --git a/app/src/main/java/com/android/quizcafe/feature/quiz/solve/component/ResumeSolvingDialog.kt b/app/src/main/java/com/android/quizcafe/feature/quiz/solve/component/ResumeSolvingDialog.kt index b9ffb996..4bb1a018 100644 --- a/app/src/main/java/com/android/quizcafe/feature/quiz/solve/component/ResumeSolvingDialog.kt +++ b/app/src/main/java/com/android/quizcafe/feature/quiz/solve/component/ResumeSolvingDialog.kt @@ -1,7 +1,6 @@ package com.android.quizcafe.feature.quiz.solve.component import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -10,13 +9,9 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Close import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton @@ -32,7 +27,6 @@ import androidx.compose.ui.window.DialogProperties import com.android.quizcafe.R import com.android.quizcafe.core.designsystem.theme.QuizCafeTheme - @Composable fun ResumeSolvingDialog( onResume: () -> Unit, @@ -98,22 +92,17 @@ fun ResumeSolvingDialog( ) { Text(text = stringResource(R.string.dialog_resume_solving)) } - - - } - - } + } } } } - @Preview(name = "ResumeSolvingDialog", showBackground = true) @Composable fun ResumeSolvingDialogPreview() { QuizCafeTheme { - ResumeSolvingDialog ( + ResumeSolvingDialog( onResume = {}, onStartNew = {} ) diff --git a/app/src/main/java/com/android/quizcafe/feature/quiz/solve/viewmodel/QuizSolveIntent.kt b/app/src/main/java/com/android/quizcafe/feature/quiz/solve/viewmodel/QuizSolveIntent.kt index 9c3a081e..be7c8731 100644 --- a/app/src/main/java/com/android/quizcafe/feature/quiz/solve/viewmodel/QuizSolveIntent.kt +++ b/app/src/main/java/com/android/quizcafe/feature/quiz/solve/viewmodel/QuizSolveIntent.kt @@ -7,7 +7,7 @@ import com.android.quizcafe.core.ui.base.BaseContract sealed class QuizSolveIntent : BaseContract.ViewIntent { data class StartSolving(val quizBookId: Long) : QuizSolveIntent() - data class ResumeSolving(val resumeWithNewSolving : Boolean) : QuizSolveIntent() + data class ResumeSolving(val resumeWithNewSolving: Boolean) : QuizSolveIntent() data class SuccessGetQuizBook(val data: QuizBook) : QuizSolveIntent() data class SuccessGetQuizBookGrade(val quizBookGrade: QuizBookGrade) : QuizSolveIntent() diff --git a/app/src/main/java/com/android/quizcafe/feature/quiz/solve/viewmodel/QuizSolveUiState.kt b/app/src/main/java/com/android/quizcafe/feature/quiz/solve/viewmodel/QuizSolveUiState.kt index b1b28e9e..4efb03ad 100644 --- a/app/src/main/java/com/android/quizcafe/feature/quiz/solve/viewmodel/QuizSolveUiState.kt +++ b/app/src/main/java/com/android/quizcafe/feature/quiz/solve/viewmodel/QuizSolveUiState.kt @@ -4,7 +4,6 @@ import com.android.quizcafe.core.domain.model.quiz.Quiz import com.android.quizcafe.core.domain.model.quiz.QuizGrade import com.android.quizcafe.core.domain.model.quizbook.response.QuizBook import com.android.quizcafe.core.domain.model.solving.QuizBookGrade -import com.android.quizcafe.core.domain.model.value.QuizBookGradeLocalId import com.android.quizcafe.core.ui.base.BaseContract import com.android.quizcafe.feature.quiz.solve.component.AnswerState import java.util.Locale @@ -61,7 +60,7 @@ data class QuizSolveUiState( val isLoading: Boolean = false, val errorMessage: String? = null, val quizBook: QuizBook? = null, - val quizBookGrade : QuizBookGrade? = null, + val quizBookGrade: QuizBookGrade? = null, val currentIndex: Int = 0, val mcq: McqState = McqState( options = listOf( diff --git a/app/src/main/java/com/android/quizcafe/feature/quiz/solve/viewmodel/QuizSolveViewModel.kt b/app/src/main/java/com/android/quizcafe/feature/quiz/solve/viewmodel/QuizSolveViewModel.kt index 982afc4b..b7031b8d 100644 --- a/app/src/main/java/com/android/quizcafe/feature/quiz/solve/viewmodel/QuizSolveViewModel.kt +++ b/app/src/main/java/com/android/quizcafe/feature/quiz/solve/viewmodel/QuizSolveViewModel.kt @@ -29,7 +29,7 @@ class QuizSolveViewModel @Inject constructor( viewModelScope.launch { while (true) { delay(1_000L) - if(state.value.common.isTimerActive){ + if (state.value.common.isTimerActive) { sendIntent(QuizSolveIntent.TickTime) } } @@ -37,7 +37,6 @@ class QuizSolveViewModel @Inject constructor( } override suspend fun handleIntent(intent: QuizSolveIntent) { - when (intent) { is QuizSolveIntent.StartSolving -> { val quizBookId = QuizBookId(intent.quizBookId) @@ -45,7 +44,7 @@ class QuizSolveViewModel @Inject constructor( getQuizBookGrade(quizBookId) } is QuizSolveIntent.ResumeSolving -> { - if(intent.resumeWithNewSolving){ + if (intent.resumeWithNewSolving) { val quizBookId = state.value.quizBook?.id deleteQuizBookGrade() quizBookId?.let { @@ -147,7 +146,7 @@ class QuizSolveViewModel @Inject constructor( ) } - is QuizSolveIntent.SuccessGetQuizBookGrade ->{ + is QuizSolveIntent.SuccessGetQuizBookGrade -> { currentState.copy( quizBookGrade = intent.quizBookGrade, currentIndex = intent.quizBookGrade.quizGrades.size @@ -159,8 +158,9 @@ class QuizSolveViewModel @Inject constructor( currentIndex = currentState.currentIndex + 1, common = CommonState(false) ) - } else + } else { currentState + } } else -> currentState } @@ -186,17 +186,18 @@ class QuizSolveViewModel @Inject constructor( } } } + // 여기서 QuizGrade 정보들이 있냐없냐로 ResumeDialog 띄울지 말지 결정 private suspend fun getQuizBookGrade( quizBookId: QuizBookId, - ){ + ) { getOrCreateQuizBookGradeUseCase(quizBookId = quizBookId).collect { when (it) { is Resource.Success -> { Log.d("getQuizBookGradeUseCase", "Get QuizBookDetail Success") Log.d("getOrCreateQuizBookGradeUseCase", "quizBookGrade : ${it.data}") val isResume = it.data.quizGrades.isNotEmpty() - if(isResume){ + if (isResume) { emitEffect(QuizSolveEffect.ShowResumeDialog) } sendIntent(QuizSolveIntent.SuccessGetQuizBookGrade(it.data)) @@ -226,7 +227,7 @@ class QuizSolveViewModel @Inject constructor( else -> Unit } } - }?: Log.d("deleteQuizBookGrade", "quizBookLocalId is null") + } ?: Log.d("deleteQuizBookGrade", "quizBookLocalId is null") } private suspend fun saveQuizToLocal() { val uiState = state.value @@ -246,7 +247,7 @@ class QuizSolveViewModel @Inject constructor( when (it) { is Resource.Success -> { sendIntent(QuizSolveIntent.GradeQuizSuccess) - if(uiState.isLastQuestion){ + if (uiState.isLastQuestion) { sendIntent(QuizSolveIntent.SubmitAnswer) } Log.d("getQuizBookGradeUseCase", "Get QuizBookDetail Success") diff --git a/app/src/main/java/com/android/quizcafe/feature/quiz/solvingResult/QuizBookSolvingResultRoute.kt b/app/src/main/java/com/android/quizcafe/feature/quiz/solvingResult/QuizBookSolvingResultRoute.kt index b1582590..22d41de4 100644 --- a/app/src/main/java/com/android/quizcafe/feature/quiz/solvingResult/QuizBookSolvingResultRoute.kt +++ b/app/src/main/java/com/android/quizcafe/feature/quiz/solvingResult/QuizBookSolvingResultRoute.kt @@ -1,6 +1,5 @@ package com.android.quizcafe.feature.quiz.solvingResult -import android.util.Log import android.widget.Toast import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect From 36e08cce5e3443b9b6f910f51962ea3bb90051ff Mon Sep 17 00:00:00 2001 From: Jae Woong Date: Tue, 24 Jun 2025 17:54:39 +0900 Subject: [PATCH 4/7] =?UTF-8?q?update=20:=20=EC=83=88=20=ED=92=80=EC=9D=B4?= =?UTF-8?q?=20=EC=8B=9C=EC=9E=91=ED=95=A0=20=EA=B2=BD=EC=9A=B0\=20Timer=20?= =?UTF-8?q?=EC=8B=9C=EC=9E=91=ED=95=B4=EC=A3=BC=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../feature/quiz/solve/viewmodel/QuizSolveViewModel.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/android/quizcafe/feature/quiz/solve/viewmodel/QuizSolveViewModel.kt b/app/src/main/java/com/android/quizcafe/feature/quiz/solve/viewmodel/QuizSolveViewModel.kt index b7031b8d..ba149a4a 100644 --- a/app/src/main/java/com/android/quizcafe/feature/quiz/solve/viewmodel/QuizSolveViewModel.kt +++ b/app/src/main/java/com/android/quizcafe/feature/quiz/solve/viewmodel/QuizSolveViewModel.kt @@ -156,7 +156,9 @@ class QuizSolveViewModel @Inject constructor( if (!currentState.isLastQuestion) { currentState.copy( currentIndex = currentState.currentIndex + 1, - common = CommonState(false) + common = currentState.common.copy( + isButtonEnabled = false + ) ) } else { currentState @@ -199,6 +201,8 @@ class QuizSolveViewModel @Inject constructor( val isResume = it.data.quizGrades.isNotEmpty() if (isResume) { emitEffect(QuizSolveEffect.ShowResumeDialog) + }else{ + sendIntent(QuizSolveIntent.StartTimer) } sendIntent(QuizSolveIntent.SuccessGetQuizBookGrade(it.data)) } From 969009a3a45f86ea03c03e15928186e51f085145 Mon Sep 17 00:00:00 2001 From: Jae Woong Date: Fri, 27 Jun 2025 13:56:57 +0900 Subject: [PATCH 5/7] =?UTF-8?q?test=20:=20Compose=20UI=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EB=BC=88=EB=8C=80=20=EC=9E=A1=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 뒤로가기 클릭 시 ExitSolvingDialog 열리는지 테스트 --- .../quiz/solve/FakeQuizSolveViewModel.kt | 35 ++++++++++++ .../feature/quiz/solve/QuizSolveScreenTest.kt | 57 +++++++++++++++++++ .../QuizBookSolvingRepositoryTest.kt | 6 +- .../android/quizcafe/core/ui/util/TestTags.kt | 13 +++++ .../feature/quiz/solve/QuizSolveRoute.kt | 3 +- .../quiz/solve/component/ExitSolvingDialog.kt | 26 ++++++--- .../solve/viewmodel/IQuizSolveViewModel.kt | 10 ++++ .../solve/viewmodel/QuizSolveViewModel.kt | 5 +- 8 files changed, 142 insertions(+), 13 deletions(-) create mode 100644 app/src/androidTest/java/com/android/quizcafe/feature/quiz/solve/FakeQuizSolveViewModel.kt create mode 100644 app/src/androidTest/java/com/android/quizcafe/feature/quiz/solve/QuizSolveScreenTest.kt create mode 100644 app/src/main/java/com/android/quizcafe/core/ui/util/TestTags.kt create mode 100644 app/src/main/java/com/android/quizcafe/feature/quiz/solve/viewmodel/IQuizSolveViewModel.kt diff --git a/app/src/androidTest/java/com/android/quizcafe/feature/quiz/solve/FakeQuizSolveViewModel.kt b/app/src/androidTest/java/com/android/quizcafe/feature/quiz/solve/FakeQuizSolveViewModel.kt new file mode 100644 index 00000000..64b7f19d --- /dev/null +++ b/app/src/androidTest/java/com/android/quizcafe/feature/quiz/solve/FakeQuizSolveViewModel.kt @@ -0,0 +1,35 @@ +package com.android.quizcafe.feature.quiz.solve + +import com.android.quizcafe.feature.quiz.solve.viewmodel.IQuizSolveViewModel +import com.android.quizcafe.feature.quiz.solve.viewmodel.QuizSolveEffect +import com.android.quizcafe.feature.quiz.solve.viewmodel.QuizSolveIntent +import com.android.quizcafe.feature.quiz.solve.viewmodel.QuizSolveUiState +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow + +class FakeQuizSolveViewModel : IQuizSolveViewModel { + private val _state = MutableStateFlow(QuizSolveUiState()) + override val state: StateFlow = _state.asStateFlow() + + private val _effect = MutableSharedFlow() + override val effect: SharedFlow = _effect.asSharedFlow() + + val receivedIntents = mutableListOf() + + override fun sendIntent(intent: QuizSolveIntent) { + receivedIntents.add(intent) + } + + // 테스트 편의를 위한 함수들 (인터페이스에는 포함되지 않음) + suspend fun emitEffect(effect: QuizSolveEffect) { + _effect.emit(effect) + } + + fun setState(newState: QuizSolveUiState) { + _state.value = newState + } +} diff --git a/app/src/androidTest/java/com/android/quizcafe/feature/quiz/solve/QuizSolveScreenTest.kt b/app/src/androidTest/java/com/android/quizcafe/feature/quiz/solve/QuizSolveScreenTest.kt new file mode 100644 index 00000000..d5d31f31 --- /dev/null +++ b/app/src/androidTest/java/com/android/quizcafe/feature/quiz/solve/QuizSolveScreenTest.kt @@ -0,0 +1,57 @@ +package com.android.quizcafe.feature.quiz.solve + +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick +import androidx.test.espresso.Espresso +import com.android.quizcafe.core.ui.util.TestTags +import com.android.quizcafe.feature.quiz.solve.viewmodel.QuizSolveEffect +import com.android.quizcafe.feature.quiz.solve.viewmodel.QuizSolveIntent +import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +class QuizSolveScreenTest { + + // 컴포즈 테스트 환경 설정 규칙 + @get:Rule + val composeTestRule = createComposeRule() + private lateinit var fakeViewModel: FakeQuizSolveViewModel + + @Before + fun setUp() { + fakeViewModel = FakeQuizSolveViewModel() + } + + // 실제 앱에서는 ViewModel에서 이 로직을 처리하지만 테스트에서는 onIntent 람다에서 직접 상태를 변경하여 ViewModel의 동작 처리 + @Test + fun whenBackPressed_showsExitDialog_viaViewModelEffect() = runTest { + var wasNavigateBackCalled = false + + composeTestRule.setContent { + QuizSolveRoute( + quizBookId = 1L, + navigateToBack = { wasNavigateBackCalled = true }, + navigateToQuizBookSolvingResult = {}, + viewModel = fakeViewModel + ) + } + + composeTestRule.onNodeWithTag(TestTags.QuizSolve.EXIT_DIALOG).assertDoesNotExist() + + Espresso.pressBack() +// Thread.sleep(2000) // 디버깅용 + assertTrue(fakeViewModel.receivedIntents.any { it is QuizSolveIntent.OnBackClick }) + + // ViewModel 동작 시뮬레이션 및 UI 검증 + fakeViewModel.emitEffect(QuizSolveEffect.ShowExitDialog) + composeTestRule.onNodeWithTag(TestTags.QuizSolve.EXIT_DIALOG).assertIsDisplayed() + + // 다이얼로그 내 버튼 클릭 및 검증 + composeTestRule.onNodeWithTag(TestTags.QuizSolve.EXIT_DIALOG_EXIT_SAVE_BUTTON).performClick() + assertTrue(fakeViewModel.receivedIntents.any { it is QuizSolveIntent.ExitWithSave }) + } +} diff --git a/app/src/androidTest/java/com/android/quizcafe/repository/QuizBookSolvingRepositoryTest.kt b/app/src/androidTest/java/com/android/quizcafe/repository/QuizBookSolvingRepositoryTest.kt index bb7b5114..93c669f8 100644 --- a/app/src/androidTest/java/com/android/quizcafe/repository/QuizBookSolvingRepositoryTest.kt +++ b/app/src/androidTest/java/com/android/quizcafe/repository/QuizBookSolvingRepositoryTest.kt @@ -99,7 +99,7 @@ class QuizBookSolvingRepositoryTest { // 먼저 퀴즈북 풀이 생성 val quizBookGradeLocalId = repository.getOrCreateQuizBookGrade(quizBookId).first { it is Resource.Success } - .let { (it as Resource.Success).data } + .let { (it as Resource.Success).data }.localId val quizGrade = QuizGrade( localId = 1L, @@ -140,7 +140,7 @@ class QuizBookSolvingRepositoryTest { assertEquals(quizGrade.memo, savedQuizGrade.memo) // Repository를 통해 조회한 데이터와도 비교 - val repositoryResult = repository.getQuizBookGrade(quizBookGradeLocalId) + val repositoryResult = repository.getOrCreateQuizBookGrade(quizBookId) .first { it is Resource.Success } .let { (it as Resource.Success).data } @@ -164,7 +164,7 @@ class QuizBookSolvingRepositoryTest { val quizBookGradeLocalId = repository.getOrCreateQuizBookGrade(quizBookId) .first { it is Resource.Success } - .let { (it as Resource.Success).data } + .let { (it as Resource.Success).data }.localId // 3개 문제 중 1번만 틀리고 나머지는 맞음 val quizGrades = listOf( diff --git a/app/src/main/java/com/android/quizcafe/core/ui/util/TestTags.kt b/app/src/main/java/com/android/quizcafe/core/ui/util/TestTags.kt new file mode 100644 index 00000000..69c8e0aa --- /dev/null +++ b/app/src/main/java/com/android/quizcafe/core/ui/util/TestTags.kt @@ -0,0 +1,13 @@ +package com.android.quizcafe.core.ui.util + +object TestTags { + + object QuizSolve { + const val EXIT_DIALOG = "exit_dialog" + const val EXIT_DIALOG_CLOSE_BUTTON = "exit_dialog_close_button" + const val EXIT_DIALOG_TITLE = "exit_dialog_title" + const val EXIT_DIALOG_DESCRIPTION = "exit_dialog_description" + const val EXIT_DIALOG_EXIT_DELETE_BUTTON = "exit_dialog_exit_delete_button" + const val EXIT_DIALOG_EXIT_SAVE_BUTTON = "exit_dialog_exit_save_button" + } +} diff --git a/app/src/main/java/com/android/quizcafe/feature/quiz/solve/QuizSolveRoute.kt b/app/src/main/java/com/android/quizcafe/feature/quiz/solve/QuizSolveRoute.kt index c8fd161a..c8e15ec1 100644 --- a/app/src/main/java/com/android/quizcafe/feature/quiz/solve/QuizSolveRoute.kt +++ b/app/src/main/java/com/android/quizcafe/feature/quiz/solve/QuizSolveRoute.kt @@ -15,6 +15,7 @@ import com.android.quizcafe.R import com.android.quizcafe.core.domain.model.value.QuizBookGradeServerId import com.android.quizcafe.feature.quiz.solve.component.ExitSolvingDialog import com.android.quizcafe.feature.quiz.solve.component.ResumeSolvingDialog +import com.android.quizcafe.feature.quiz.solve.viewmodel.IQuizSolveViewModel import com.android.quizcafe.feature.quiz.solve.viewmodel.QuizSolveEffect import com.android.quizcafe.feature.quiz.solve.viewmodel.QuizSolveIntent import com.android.quizcafe.feature.quiz.solve.viewmodel.QuizSolveViewModel @@ -24,7 +25,7 @@ fun QuizSolveRoute( quizBookId: Long, navigateToBack: () -> Unit, navigateToQuizBookSolvingResult: (QuizBookGradeServerId) -> Unit, - viewModel: QuizSolveViewModel = hiltViewModel() + viewModel: IQuizSolveViewModel = hiltViewModel() ) { val context = LocalContext.current val state by viewModel.state.collectAsState() diff --git a/app/src/main/java/com/android/quizcafe/feature/quiz/solve/component/ExitSolvingDialog.kt b/app/src/main/java/com/android/quizcafe/feature/quiz/solve/component/ExitSolvingDialog.kt index 7c9db175..31f9882a 100644 --- a/app/src/main/java/com/android/quizcafe/feature/quiz/solve/component/ExitSolvingDialog.kt +++ b/app/src/main/java/com/android/quizcafe/feature/quiz/solve/component/ExitSolvingDialog.kt @@ -8,6 +8,7 @@ import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview @@ -16,9 +17,11 @@ import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import com.android.quizcafe.R import com.android.quizcafe.core.designsystem.theme.QuizCafeTheme +import com.android.quizcafe.core.ui.util.TestTags @Composable fun ExitSolvingDialog( + modifier: Modifier = Modifier, onDismissRequest: () -> Unit, onExitWithDelete: () -> Unit, onExitWithSave: () -> Unit @@ -30,14 +33,19 @@ fun ExitSolvingDialog( ) ) { Card( + modifier = modifier + .fillMaxWidth() + .testTag(TestTags.QuizSolve.EXIT_DIALOG), shape = RoundedCornerShape(16.dp), colors = CardDefaults.cardColors( containerColor = MaterialTheme.colorScheme.surface, - ), - modifier = Modifier.fillMaxWidth() + ) ) { Box(contentAlignment = Alignment.TopEnd) { - IconButton(onClick = onDismissRequest) { + IconButton( + modifier = Modifier.testTag(TestTags.QuizSolve.EXIT_DIALOG_CLOSE_BUTTON), + onClick = onDismissRequest + ) { Icon( imageVector = Icons.Default.Close, contentDescription = stringResource(R.string.dialog_close) @@ -55,7 +63,8 @@ fun ExitSolvingDialog( Text( text = stringResource(R.string.dialog_exit_title), style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurface + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.testTag(TestTags.QuizSolve.EXIT_DIALOG_TITLE) ) Spacer(modifier = Modifier.height(8.dp)) @@ -64,7 +73,8 @@ fun ExitSolvingDialog( text = stringResource(R.string.dialog_exit_description), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center + textAlign = TextAlign.Center, + modifier = Modifier.testTag(TestTags.QuizSolve.EXIT_DIALOG_DESCRIPTION) ) Spacer(modifier = Modifier.height(16.dp)) @@ -77,7 +87,8 @@ fun ExitSolvingDialog( onClick = onExitWithDelete, colors = ButtonDefaults.textButtonColors( contentColor = MaterialTheme.colorScheme.error - ) + ), + modifier = Modifier.testTag(TestTags.QuizSolve.EXIT_DIALOG_EXIT_DELETE_BUTTON) ) { Text(text = stringResource(R.string.dialog_exit_with_delete)) } @@ -88,7 +99,8 @@ fun ExitSolvingDialog( onClick = onExitWithSave, colors = ButtonDefaults.textButtonColors( contentColor = MaterialTheme.colorScheme.onSurface - ) + ), + modifier = Modifier.testTag(TestTags.QuizSolve.EXIT_DIALOG_EXIT_SAVE_BUTTON) ) { Text(text = stringResource(R.string.dialog_exit_with_save)) } diff --git a/app/src/main/java/com/android/quizcafe/feature/quiz/solve/viewmodel/IQuizSolveViewModel.kt b/app/src/main/java/com/android/quizcafe/feature/quiz/solve/viewmodel/IQuizSolveViewModel.kt new file mode 100644 index 00000000..6333a3e0 --- /dev/null +++ b/app/src/main/java/com/android/quizcafe/feature/quiz/solve/viewmodel/IQuizSolveViewModel.kt @@ -0,0 +1,10 @@ +package com.android.quizcafe.feature.quiz.solve.viewmodel + +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow + +interface IQuizSolveViewModel { + val state: StateFlow + val effect: SharedFlow + fun sendIntent(intent: QuizSolveIntent) +} diff --git a/app/src/main/java/com/android/quizcafe/feature/quiz/solve/viewmodel/QuizSolveViewModel.kt b/app/src/main/java/com/android/quizcafe/feature/quiz/solve/viewmodel/QuizSolveViewModel.kt index ba149a4a..40c45822 100644 --- a/app/src/main/java/com/android/quizcafe/feature/quiz/solve/viewmodel/QuizSolveViewModel.kt +++ b/app/src/main/java/com/android/quizcafe/feature/quiz/solve/viewmodel/QuizSolveViewModel.kt @@ -24,7 +24,8 @@ class QuizSolveViewModel @Inject constructor( private val deleteQuizBookGradeUseCase: DeleteQuizBookGradeUseCase, ) : BaseViewModel( initialState = QuizSolveUiState() -) { +), + IQuizSolveViewModel { init { viewModelScope.launch { while (true) { @@ -201,7 +202,7 @@ class QuizSolveViewModel @Inject constructor( val isResume = it.data.quizGrades.isNotEmpty() if (isResume) { emitEffect(QuizSolveEffect.ShowResumeDialog) - }else{ + } else { sendIntent(QuizSolveIntent.StartTimer) } sendIntent(QuizSolveIntent.SuccessGetQuizBookGrade(it.data)) From 80de248d74e6a80169d1b48b1e64bc51e82c8ced Mon Sep 17 00:00:00 2001 From: Jae Woong Date: Mon, 30 Jun 2025 17:10:04 +0900 Subject: [PATCH 6/7] =?UTF-8?q?fix=20:=20=EC=82=AC=EC=86=8C=ED=95=9C=20?= =?UTF-8?q?=EB=B2=84=EA=B7=B8=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../1.json | 16 +- .../QuizBookSolvingRepositoryImpl.kt | 32 +-- .../repository/QuizBookSolvingRepository.kt | 1 + .../feature/quiz/solve/QuizSolveRoute.kt | 10 +- .../quiz/solve/viewmodel/QuizSolveIntent.kt | 14 +- .../quiz/solve/viewmodel/QuizSolveUiState.kt | 4 +- .../solve/viewmodel/QuizSolveViewModel.kt | 264 ++++++++---------- 7 files changed, 160 insertions(+), 181 deletions(-) diff --git a/app/schemas/com.android.quizcafe.core.database.QuizCafeDatabase/1.json b/app/schemas/com.android.quizcafe.core.database.QuizCafeDatabase/1.json index b22efd7f..ff6c0e3f 100644 --- a/app/schemas/com.android.quizcafe.core.database.QuizCafeDatabase/1.json +++ b/app/schemas/com.android.quizcafe.core.database.QuizCafeDatabase/1.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 1, - "identityHash": "e8b0c98f11a3e10bbae77fa64b2c1a67", + "identityHash": "cb35ad76919b5cac5c41199d7b88c43a", "entities": [ { "tableName": "quiz", @@ -251,7 +251,17 @@ "localId" ] }, - "indices": [], + "indices": [ + { + "name": "index_QuizBookGradeEntity_quizBookId", + "unique": true, + "columnNames": [ + "quizBookId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_QuizBookGradeEntity_quizBookId` ON `${TABLE_NAME}` (`quizBookId`)" + } + ], "foreignKeys": [] }, { @@ -324,7 +334,7 @@ "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e8b0c98f11a3e10bbae77fa64b2c1a67')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'cb35ad76919b5cac5c41199d7b88c43a')" ] } } \ No newline at end of file diff --git a/app/src/main/java/com/android/quizcafe/core/data/repository/QuizBookSolvingRepositoryImpl.kt b/app/src/main/java/com/android/quizcafe/core/data/repository/QuizBookSolvingRepositoryImpl.kt index 76a3cca0..fe63e9e3 100644 --- a/app/src/main/java/com/android/quizcafe/core/data/repository/QuizBookSolvingRepositoryImpl.kt +++ b/app/src/main/java/com/android/quizcafe/core/data/repository/QuizBookSolvingRepositoryImpl.kt @@ -71,18 +71,6 @@ class QuizBookSolvingRepositoryImpl @Inject constructor( emit(Resource.Failure(errorMessage = "QuizBookGrade 생성 중 오류: ${e.message}", code = LocalErrorCode.ROOM_ERROR)) } - override fun deleteQuizBookGrade(id: QuizBookGradeLocalId): Flow> = flow { - emit(Resource.Loading) - val deletedCnt = quizBookGradeDao.deleteQuizBookGrade(id.value) - if (deletedCnt == 1) { - emit(Resource.Success(Unit)) - } else { - emit(Resource.Failure(errorMessage = "퀴즈북 풀이 기록 삭제 중 오류 : deletedCnt = $deletedCnt", code = LocalErrorCode.ROOM_ERROR)) - } - }.catch { e -> - emit(Resource.Failure(errorMessage = "퀴즈북 풀이 기록 삭제 중 오류: ${e.message}", code = LocalErrorCode.ROOM_ERROR)) - } - override fun getQuizGrade(quizBookGradeLocalId: QuizBookGradeLocalId, quizId: QuizId): Flow> = flow { emit(Resource.Loading) val quizGradeRelation = quizGradeDao.getQuizGradeByQuizId(quizId.value, quizBookGradeLocalId.value) @@ -106,7 +94,6 @@ class QuizBookSolvingRepositoryImpl @Inject constructor( emit(Resource.Failure(errorMessage = "퀴즈북 풀이 기록 조회 중 오류: ${e.message}", code = LocalErrorCode.ROOM_ERROR)) } - override fun getQuizBookSolving(id: QuizBookGradeServerId): Flow> = flow { val quizBookSolvingId = id.value if (quizBookSolvingId == null) { @@ -125,7 +112,6 @@ class QuizBookSolvingRepositoryImpl @Inject constructor( apiResponseListToResourceFlow(mapper = QuizBookSolvingResponseDto::toDomain) { quizBookSolvingRemoteDataSource.getAllQuizBookSolvingByUser() } - } // 퀴즈 1개 풀이 기록 저장 및 수정 override fun upsertQuizGrade(quizGrade: QuizGrade): Flow> = flow { @@ -142,13 +128,25 @@ class QuizBookSolvingRepositoryImpl @Inject constructor( emit(Resource.Failure(errorMessage = "퀴즈 풀이 기록 저장 중 오류: ${e.message}", code = LocalErrorCode.ROOM_ERROR)) } + override fun deleteQuizBookGrade(id: QuizBookGradeLocalId): Flow> = flow { + emit(Resource.Loading) + val deletedCnt = quizBookGradeDao.deleteQuizBookGrade(id.value) + if (deletedCnt == 1) { + emit(Resource.Success(Unit)) + } else { + emit(Resource.Failure(errorMessage = "퀴즈북 풀이 기록 삭제 중 오류 : deletedCnt = $deletedCnt", code = LocalErrorCode.ROOM_ERROR)) + } + }.catch { e -> + emit(Resource.Failure(errorMessage = "퀴즈북 풀이 기록 삭제 중 오류: ${e.message}", code = LocalErrorCode.ROOM_ERROR)) + } + // 로컬에서 퀴즈북 풀이 기록 가져와 requestDto로 변환 후 퀴즈북 풀이 완료 API 요청하기 override fun solveQuizBook( localId: QuizBookGradeLocalId, elapsedTimeInSeconds: Long ): Flow> = flow { emit(Resource.Loading) - val (quizBookGradeEntity, quizGradeEntities) = getQuizBookGradeData(localId) + val (quizBookGradeEntity, quizGradeEntities) = getQuizBookGradeRelation(localId) val quizBookEntity = getQuizBookEntity(quizBookGradeEntity.quizBookId) val requestDto = createQuizBookSolvingRequest( @@ -176,10 +174,10 @@ class QuizBookSolvingRepositoryImpl @Inject constructor( } // 로컬에서 퀴즈북 풀이 기록 가져오기 - null 체크 추가 - private suspend fun getQuizBookGradeData( + private suspend fun getQuizBookGradeRelation( localId: QuizBookGradeLocalId ): Pair> { - val quizBookGradeRelation = quizBookGradeDao.getQuizBookGrade(localId.value) + val quizBookGradeRelation = quizBookGradeDao.getQuizBookGradeByLocalId(localId.value) ?: throw IllegalStateException("QuizBookGrade not found for localId: ${localId.value}") return Pair( diff --git a/app/src/main/java/com/android/quizcafe/core/domain/repository/QuizBookSolvingRepository.kt b/app/src/main/java/com/android/quizcafe/core/domain/repository/QuizBookSolvingRepository.kt index 2383e987..f397ef6e 100644 --- a/app/src/main/java/com/android/quizcafe/core/domain/repository/QuizBookSolvingRepository.kt +++ b/app/src/main/java/com/android/quizcafe/core/domain/repository/QuizBookSolvingRepository.kt @@ -16,6 +16,7 @@ interface QuizBookSolvingRepository { // 퀴즈북 풀이 로컬 기록 삭제하기 fun deleteQuizBookGrade(id: QuizBookGradeLocalId): Flow> + // 퀴즈 풀이 로컬 기록 가져오기 fun getQuizGrade(quizBookGradeLocalId: QuizBookGradeLocalId, quizId: QuizId): Flow> diff --git a/app/src/main/java/com/android/quizcafe/feature/quiz/solve/QuizSolveRoute.kt b/app/src/main/java/com/android/quizcafe/feature/quiz/solve/QuizSolveRoute.kt index b11b23d7..aa217aa5 100644 --- a/app/src/main/java/com/android/quizcafe/feature/quiz/solve/QuizSolveRoute.kt +++ b/app/src/main/java/com/android/quizcafe/feature/quiz/solve/QuizSolveRoute.kt @@ -11,6 +11,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalContext import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.viewmodel.compose.viewModel import com.android.quizcafe.R import com.android.quizcafe.core.domain.model.value.QuizBookGradeServerId import com.android.quizcafe.feature.quiz.solve.component.ExitSolvingDialog @@ -29,12 +30,11 @@ fun QuizSolveRoute( ) { val context = LocalContext.current val state by viewModel.state.collectAsState() + var showExitDialog by remember { mutableStateOf(false) } + var showResumeDialog by remember { mutableStateOf(false) } LaunchedEffect(Unit) { - viewModel.sendIntent(QuizSolveIntent.LoadQuizBook(quizBookId)) - viewModel.sendIntent(QuizSolveIntent.GetQuizBookLocalId(quizBookId)) - } - LaunchedEffect(Unit) { + viewModel.sendIntent(QuizSolveIntent.StartSolving(quizBookId)) viewModel.effect.collect { effect -> when (effect) { QuizSolveEffect.ShowResumeDialog -> { @@ -66,7 +66,7 @@ fun QuizSolveRoute( } BackHandler(enabled = !showExitDialog) { - viewModel.sendIntent(QuizSolveIntent.OnBackClick) + viewModel.sendIntent(QuizSolveIntent.NavigateBack) } if (showResumeDialog) { diff --git a/app/src/main/java/com/android/quizcafe/feature/quiz/solve/viewmodel/QuizSolveIntent.kt b/app/src/main/java/com/android/quizcafe/feature/quiz/solve/viewmodel/QuizSolveIntent.kt index 229a5521..2d3212e7 100644 --- a/app/src/main/java/com/android/quizcafe/feature/quiz/solve/viewmodel/QuizSolveIntent.kt +++ b/app/src/main/java/com/android/quizcafe/feature/quiz/solve/viewmodel/QuizSolveIntent.kt @@ -1,20 +1,19 @@ package com.android.quizcafe.feature.quiz.solve.viewmodel +import com.android.quizcafe.core.domain.model.quiz.QuizGrade import com.android.quizcafe.core.domain.model.quizbook.response.QuizBook import com.android.quizcafe.core.domain.model.solving.QuizBookGrade -import com.android.quizcafe.core.domain.model.value.QuizBookGradeLocalId import com.android.quizcafe.core.domain.model.value.QuizBookGradeServerId import com.android.quizcafe.core.ui.base.BaseContract sealed class QuizSolveIntent : BaseContract.ViewIntent { // 초기화 작업 - data class Initialize(val quizBookId: Long) : QuizSolveIntent() + data class StartSolving(val quizBookId: Long) : QuizSolveIntent() data class LoadQuizBookSuccess(val quizBook: QuizBook) : QuizSolveIntent() data class LoadQuizBookGradeSuccess(val quizBookGrade: QuizBookGrade) : QuizSolveIntent() - data class SetQuizBookLocalId(val quizBookLocalId: QuizBookGradeLocalId) : QuizSolveIntent() - data class StartSolving(val quizBookId: Long) : QuizSolveIntent() - data class ResumeSolving(val resumeWithNewSolving: Boolean) : QuizSolveIntent() + data class ResumeSolving(val resumeWithNewSolving: Boolean) : QuizSolveIntent() + data object StartTimer : QuizSolveIntent() // 문제 선택 data class SelectAnswer(val option: QuizOption) : QuizSolveIntent() @@ -32,6 +31,7 @@ sealed class QuizSolveIntent : BaseContract.ViewIntent { data class SubmitQuizBookSuccess(val quizBookGradeServerId: QuizBookGradeServerId) : QuizSolveIntent() data class HandleError(val message: String?) : QuizSolveIntent() - data object OnBackClick : QuizSolveIntent() - data object TickTime : QuizSolveIntent() + data object UpdateTimer : QuizSolveIntent() + data object ExitWithDelete : QuizSolveIntent() + data object ExitWithSave : QuizSolveIntent() } diff --git a/app/src/main/java/com/android/quizcafe/feature/quiz/solve/viewmodel/QuizSolveUiState.kt b/app/src/main/java/com/android/quizcafe/feature/quiz/solve/viewmodel/QuizSolveUiState.kt index acd10b08..9aff8a0e 100644 --- a/app/src/main/java/com/android/quizcafe/feature/quiz/solve/viewmodel/QuizSolveUiState.kt +++ b/app/src/main/java/com/android/quizcafe/feature/quiz/solve/viewmodel/QuizSolveUiState.kt @@ -47,15 +47,15 @@ data class TimerState( data class CommonState( val playMode: PlayMode = PlayMode.DEFAULT, + val isTimerActive: Boolean = true, val currentIndex: Int = 0 ) data class QuizSolveUiState( val isLoading: Boolean = false, val errorMessage: String? = null, - val quizBookLocalId: QuizBookGradeLocalId? = null, val quizBook: QuizBook? = null, - val quizGrades: List? = null, + val quizBookGrade: QuizBookGrade? = null, val subjective: SubjectiveState = SubjectiveState(), val mcq: McqState = McqState(), val review: ReviewState = ReviewState(), diff --git a/app/src/main/java/com/android/quizcafe/feature/quiz/solve/viewmodel/QuizSolveViewModel.kt b/app/src/main/java/com/android/quizcafe/feature/quiz/solve/viewmodel/QuizSolveViewModel.kt index d0d3050b..81ceca62 100644 --- a/app/src/main/java/com/android/quizcafe/feature/quiz/solve/viewmodel/QuizSolveViewModel.kt +++ b/app/src/main/java/com/android/quizcafe/feature/quiz/solve/viewmodel/QuizSolveViewModel.kt @@ -11,6 +11,7 @@ import com.android.quizcafe.core.domain.usecase.solving.GetOrCreateQuizBookGrade import com.android.quizcafe.core.domain.usecase.solving.GradeQuizUseCase import com.android.quizcafe.core.domain.usecase.solving.SolveQuizBookUseCase import com.android.quizcafe.core.ui.base.BaseViewModel +import com.android.quizcafe.feature.quiz.solve.viewmodel.QuizSolveEffect.* import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -33,10 +34,9 @@ class QuizSolveViewModel @Inject constructor( viewModelScope.launch { while (true) { delay(1_000L) - sendIntent(QuizSolveIntent.UpdateTimer) -// if (state.value.common.isTimerActive) { -// sendIntent(QuizSolveIntent.TickTime) -// } + if (state.value.common.isTimerActive) { + sendIntent(QuizSolveIntent.UpdateTimer) + } } } } @@ -44,70 +44,34 @@ class QuizSolveViewModel @Inject constructor( override suspend fun handleIntent(intent: QuizSolveIntent) { when (intent) { is QuizSolveIntent.StartSolving -> { - val quizBookId = QuizBookId(intent.quizBookId) + val quizBookId = QuizBookId(abs(intent.quizBookId)) getQuizBook(quizBookId) - getQuizBookGrade(quizBookId) + getOrCreateQuizBookGrade(quizBookId) } + is QuizSolveIntent.ResumeSolving -> { if (intent.resumeWithNewSolving) { val quizBookId = state.value.quizBook?.id deleteQuizBookGrade() quizBookId?.let { - getQuizBookGrade(it) + getOrCreateQuizBookGrade(it) } } emitEffect(QuizSolveEffect.CloseResumeDialog) sendIntent(QuizSolveIntent.StartTimer) } - QuizSolveIntent.OnBackClick -> { - emitEffect(QuizSolveEffect.ShowExitDialog) - } - - is QuizSolveIntent.SubmitNext -> { - saveQuizToLocal() - } - - is QuizSolveIntent.SubmitAnswer -> { - submitQuizAnswer() - } - - is QuizSolveIntent.SolveQuizSuccess -> { - emitEffect(QuizSolveEffect.NavigateToQuizBookSolvingResult(intent.quizBookGradeServerId)) - } - - is QuizSolveIntent.GradeQuizError -> { - emitEffect(QuizSolveEffect.ShowErrorDialog(intent.message ?: "")) - } - - QuizSolveIntent.NavigateBack -> { - emitEffect(QuizSolveEffect.NavigatePopBack) - } - - QuizSolveIntent.NavigateToResult -> { - // This will be handled by SubmitQuizBookSuccess - } - - // 초기화 - is QuizSolveIntent.Initialize -> { - loadQuizBook(abs(intent.quizBookId)) - getQuizBookLocalId(intent.quizBookId) - } - - is QuizSolveIntent.SetQuizBookLocalId -> { - getQuizBookGradeResult(intent.quizBookLocalId) - } - is QuizSolveIntent.SubmitQuizBookSuccess -> { - emitEffect(QuizSolveEffect.NavigateToQuizBookSolvingResult(intent.quizBookGradeServerId)) + emitEffect(NavigateToQuizBookSolvingResult(intent.quizBookGradeServerId)) } is QuizSolveIntent.HandleError -> { - emitEffect(QuizSolveEffect.ShowErrorDialog(intent.message ?: "")) + emitEffect(ShowErrorDialog(intent.message ?: "")) } QuizSolveIntent.NavigateToNextQuestion -> { val currentState = state.value + Log.d("test", "currentState : $currentState") when { currentState.common.playMode == PlayMode.REVIEW_MODE && !currentState.review.showExplanation -> { saveQuizToLocal() @@ -115,6 +79,7 @@ class QuizSolveViewModel @Inject constructor( currentState.common.playMode == PlayMode.REVIEW_MODE && currentState.isLastQuestion -> { submitQuizAnswer() } + // 리뷰모드 아닐 때 else -> { saveQuizToLocal() if (currentState.isLastQuestion) { @@ -127,6 +92,12 @@ class QuizSolveViewModel @Inject constructor( QuizSolveIntent.NavigateToPreviousQuestion -> { getQuizAnswer() } + QuizSolveIntent.NavigateToResult -> { + // This will be handled by SubmitQuizBookSuccess + } + QuizSolveIntent.NavigateBack -> { + emitEffect(QuizSolveEffect.ShowExitDialog) + } is QuizSolveIntent.GradeQuizSuccess -> { val currentState = state.value when { @@ -139,23 +110,41 @@ class QuizSolveViewModel @Inject constructor( else -> Unit } } + is QuizSolveIntent.ExitWithDelete -> { + deleteQuizBookGrade() + emitEffect(QuizSolveEffect.NavigateToBack) + } + is QuizSolveIntent.ExitWithSave -> { + emitEffect(QuizSolveEffect.NavigateToBack) + } + is QuizSolveIntent.SelectAnswer, is QuizSolveIntent.UpdateSubjectiveAnswer, is QuizSolveIntent.LoadQuizBookSuccess, is QuizSolveIntent.LoadQuizBookGradeSuccess, - QuizSolveIntent.UpdateTimer -> { - } - - is QuizSolveIntent.GetQuizGradeSuccess -> { - } - - else -> Unit + is QuizSolveIntent.GetQuizGradeSuccess, + is QuizSolveIntent.StartTimer, + is QuizSolveIntent.UpdateTimer -> Unit } } override fun reduce(currentState: QuizSolveUiState, intent: QuizSolveIntent): QuizSolveUiState { return when (intent) { - QuizSolveIntent.TickTime -> { + is QuizSolveIntent.StartSolving -> { + if (intent.quizBookId < 0) { + currentState.copy( + common = currentState.common.copy( + playMode = PlayMode.REVIEW_MODE + ), + isLoading = true, + errorMessage = null + ) + } else { + currentState.copy(isLoading = true, errorMessage = null) + } + } + + QuizSolveIntent.UpdateTimer -> { val timer = currentState.timer currentState.copy( timer = timer.copy( @@ -170,9 +159,6 @@ class QuizSolveViewModel @Inject constructor( selectedId = intent.option.id, selectedContent = intent.option.text ), - common = currentState.common.copy( - isButtonEnabled = true - ) ) is QuizSolveIntent.UpdateSubjectiveAnswer -> @@ -182,20 +168,6 @@ class QuizSolveViewModel @Inject constructor( ) ) - is QuizSolveIntent.Initialize -> { - if (intent.quizBookId < 0) { - currentState.copy( - common = currentState.common.copy( - playMode = PlayMode.REVIEW_MODE - ), - isLoading = true, - errorMessage = null - ) - } else { - currentState.copy(isLoading = true, errorMessage = null) - } - } - is QuizSolveIntent.LoadQuizBookSuccess -> { currentState.copy( quizBook = intent.quizBook, @@ -203,15 +175,17 @@ class QuizSolveViewModel @Inject constructor( ) } - is QuizSolveIntent.SetQuizBookLocalId -> - currentState.copy( - quizBookLocalId = intent.quizBookLocalId - ) - - is QuizSolveIntent.LoadQuizBookGradeSuccess -> + is QuizSolveIntent.LoadQuizBookGradeSuccess -> { + val index = intent.quizBookGrade.quizGrades.size.let { + if (it == currentState.quizBook?.quizList?.size) it - 1 else it + } currentState.copy( - quizGrades = intent.quizBookGrade.quizGrades + quizBookGrade = intent.quizBookGrade, + common = currentState.common.copy( + currentIndex = index + ) ) + } QuizSolveIntent.NavigateToPreviousQuestion -> { if (currentState.common.currentIndex > 0) { @@ -229,75 +203,20 @@ class QuizSolveViewModel @Inject constructor( QuizSolveIntent.GradeQuizSuccess -> { currentState.onLocalSaveSuccess() } + QuizSolveIntent.StartTimer -> { + currentState.copy( + common = currentState.common.copy(isTimerActive = true) + ) + } + is QuizSolveIntent.HandleError, QuizSolveIntent.NavigateBack, QuizSolveIntent.NavigateToNextQuestion, QuizSolveIntent.NavigateToResult, - is QuizSolveIntent.SubmitQuizBookSuccess -> currentState - } - } - - // MARK: - Private Methods - private suspend fun loadQuizBook(id: Long) { - getQuizBookUseCase( - QuizBookId(id) - ).collect { - when (it) { - is Resource.Success -> { - Log.d("getQuizBook", "${it.data.quizList}") - sendIntent(QuizSolveIntent.LoadQuizBookSuccess(it.data)) - } - - is Resource.Loading -> { - Log.d("getQuizBook", "Loading") - } - - is Resource.Failure -> { - Log.d("getQuizBook", it.errorMessage) - } - } - } - } - - private suspend fun getQuizBookLocalId(id: Long) { - getQuizBookLocalIdUseCase( - QuizBookId(abs(id)) - ).collect { - when (it) { - is Resource.Success -> { - Log.d("getQuizBookLocalId", "Get QuizBookDetail Success") - sendIntent(QuizSolveIntent.SetQuizBookLocalId(it.data)) - } - - is Resource.Loading -> { - Log.d("getQuizBookLocalId", "Loading") - } - - is Resource.Failure -> { - Log.d("getQuizBookLocalId", it.errorMessage) - } - } - } - } - - private suspend fun getQuizBookGradeResult(id: QuizBookGradeLocalId) { - getQuizBookGradeUseCase( - id - ).collect { - when (it) { - is Resource.Success -> { - Log.d("getQuizBookGradeUseCase", "Get QuizBookDetail Success") - sendIntent(QuizSolveIntent.LoadQuizBookGradeSuccess(it.data)) - } - - is Resource.Loading -> { - Log.d("getQuizBookGradeUseCase", "Loading") - } - - is Resource.Failure -> { - Log.d("getQuizBookGradeUseCase", it.errorMessage) - } - } + is QuizSolveIntent.SubmitQuizBookSuccess, + is QuizSolveIntent.ExitWithSave, + is QuizSolveIntent.ResumeSolving, + is QuizSolveIntent.ExitWithDelete -> currentState } } @@ -321,7 +240,7 @@ class QuizSolveViewModel @Inject constructor( private suspend fun saveQuizToLocal() { val uiState = state.value val quiz = uiState.currentQuiz - val localId = uiState.quizBookLocalId + val localId = uiState.quizBookGrade?.localId val userAnswer = when (uiState.questionType) { QuestionType.SUBJECTIVE -> uiState.subjective.answer else -> uiState.mcq.selectedContent @@ -350,12 +269,13 @@ class QuizSolveViewModel @Inject constructor( } } } else { + Log.d("saveQuizToLocal", "문제 발생") sendIntent(QuizSolveIntent.HandleError("문제 발생")) } } private suspend fun submitQuizAnswer() { - val localId = state.value.quizBookLocalId + val localId = state.value.quizBookGrade?.localId val elapsedTimeInSeconds: Long = state.value.timer.elapsedSeconds.toLong() if (localId != null) { solveQuizBookUseCase( @@ -385,7 +305,7 @@ class QuizSolveViewModel @Inject constructor( private suspend fun getQuizAnswer() { val currentState = state.value - val localId = currentState.quizBookLocalId + val localId = state.value.quizBookGrade?.localId val quizId = currentState.currentQuiz?.id if (localId != null && quizId != null) { getQuizGradeUseCase( @@ -412,4 +332,54 @@ class QuizSolveViewModel @Inject constructor( sendIntent(QuizSolveIntent.HandleError("문제 발생")) } } + + private suspend fun getQuizBook(quizBookId: QuizBookId) { + getQuizBookUseCase( + quizBookId + ).collect { + when (it) { + is Resource.Success -> { + Log.d("getQuizBook", "${it.data.quizList}") + sendIntent(QuizSolveIntent.LoadQuizBookSuccess(it.data)) + } + + is Resource.Loading -> { + Log.d("getQuizBook", "Loading") + } + + is Resource.Failure -> { + Log.d("getQuizBook", it.errorMessage) + } + } + } + } + + // 여기서 QuizGrade 정보들이 있냐없냐로 ResumeDialog 띄울지 말지 결정 + private suspend fun getOrCreateQuizBookGrade( + quizBookId: QuizBookId, + ) { + getOrCreateQuizBookGradeUseCase(quizBookId = quizBookId).collect { + when (it) { + is Resource.Success -> { + Log.d("getQuizBookGradeUseCase", "Get QuizBookDetail Success") + Log.d("getOrCreateQuizBookGradeUseCase", "quizBookGrade : ${it.data}") + val isResume = it.data.quizGrades.isNotEmpty() + if (isResume) { + emitEffect(QuizSolveEffect.ShowResumeDialog) + } else { + sendIntent(QuizSolveIntent.StartTimer) + } + sendIntent(QuizSolveIntent.LoadQuizBookGradeSuccess(it.data)) + } + + is Resource.Loading -> { + Log.d("getQuizBookGradeUseCase", "Loading") + } + + is Resource.Failure -> { + Log.d("getQuizBookGradeUseCase", it.errorMessage) + } + } + } + } } From 545f7ea7883af0b1d82ff639e79c0cf069372620 Mon Sep 17 00:00:00 2001 From: Jae Woong Date: Tue, 1 Jul 2025 16:37:14 +0900 Subject: [PATCH 7/7] update : QuizGrade? to QuizGrade --- .../core/data/repository/QuizBookSolvingRepositoryImpl.kt | 8 ++++++-- .../feature/quiz/solve/viewmodel/QuizSolveViewModel.kt | 4 +--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/android/quizcafe/core/data/repository/QuizBookSolvingRepositoryImpl.kt b/app/src/main/java/com/android/quizcafe/core/data/repository/QuizBookSolvingRepositoryImpl.kt index fe63e9e3..fd6174d8 100644 --- a/app/src/main/java/com/android/quizcafe/core/data/repository/QuizBookSolvingRepositoryImpl.kt +++ b/app/src/main/java/com/android/quizcafe/core/data/repository/QuizBookSolvingRepositoryImpl.kt @@ -71,11 +71,15 @@ class QuizBookSolvingRepositoryImpl @Inject constructor( emit(Resource.Failure(errorMessage = "QuizBookGrade 생성 중 오류: ${e.message}", code = LocalErrorCode.ROOM_ERROR)) } - override fun getQuizGrade(quizBookGradeLocalId: QuizBookGradeLocalId, quizId: QuizId): Flow> = flow { + override fun getQuizGrade(quizBookGradeLocalId: QuizBookGradeLocalId, quizId: QuizId): Flow> = flow { emit(Resource.Loading) val quizGradeRelation = quizGradeDao.getQuizGradeByQuizId(quizId.value, quizBookGradeLocalId.value) val quizGrade = quizGradeRelation?.toDomain() - emit(Resource.Success(quizGrade)) + if (quizGrade == null) { + emit(Resource.Failure(errorMessage = "getQuizGrade 중 quizGrade is null", code = LocalErrorCode.ROOM_ERROR)) + } else { + emit(Resource.Success(quizGrade)) + } }.catch { e -> emit(Resource.Failure(errorMessage = "퀴즈 한문제씩 가져오다가 오류: ${e.message}", code = LocalErrorCode.ROOM_ERROR)) } diff --git a/app/src/main/java/com/android/quizcafe/feature/quiz/solve/viewmodel/QuizSolveViewModel.kt b/app/src/main/java/com/android/quizcafe/feature/quiz/solve/viewmodel/QuizSolveViewModel.kt index 81ceca62..32c5f715 100644 --- a/app/src/main/java/com/android/quizcafe/feature/quiz/solve/viewmodel/QuizSolveViewModel.kt +++ b/app/src/main/java/com/android/quizcafe/feature/quiz/solve/viewmodel/QuizSolveViewModel.kt @@ -196,9 +196,7 @@ class QuizSolveViewModel @Inject constructor( } is QuizSolveIntent.GetQuizGradeSuccess -> - intent.quizGrade - ?.let { currentState.applyFetchedGrade(it) } - ?: currentState + currentState.applyFetchedGrade(intent.quizGrade) QuizSolveIntent.GradeQuizSuccess -> { currentState.onLocalSaveSuccess()