Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
afb60a1
refactor: Domain 모델 패키지 구조 변경 및 import 경로 업데이트
chanho0908 Jan 19, 2026
8509da4
feat: Exam domain 모델 추가
chanho0908 Jan 19, 2026
6ce274b
feat: Exam data 모델 추가
chanho0908 Jan 19, 2026
ff49d02
feat: ExamRepository 인터페이스 추가
chanho0908 Jan 19, 2026
dc61833
feat: ExamDataSource 구현 추가
chanho0908 Jan 19, 2026
72585dd
feat: DefaultExamRepository 구현 추가
chanho0908 Jan 19, 2026
703e5a0
feat: QuestionRepository에 시험 문제 조회 메서드 추가
chanho0908 Jan 19, 2026
0565c15
feat: RemoteQuestionDataSource에 시험 문제 조회 추가
chanho0908 Jan 19, 2026
0dcd11e
feat: DefaultQuestionRepository에 시험 문제 조회 구현
chanho0908 Jan 19, 2026
57218b8
feat: DI 모듈에 Exam 의존성 추가
chanho0908 Jan 19, 2026
4354fde
test: 패키지 구조 변경에 따른 테스트 코드 업데이트
chanho0908 Jan 19, 2026
95541de
feat: DesignSystem에 ExamQuestionCard 및 테마 업데이트
chanho0908 Jan 19, 2026
712206c
feat: Exam 기능 문자열 리소스 추가
chanho0908 Jan 19, 2026
b9033a8
feat: Exam 화면 Navigation Routes 추가
chanho0908 Jan 19, 2026
5f5992a
feat: ExamMainScreen 구현
chanho0908 Jan 19, 2026
4fd5570
feat: ExamProgressScreen 구현
chanho0908 Jan 19, 2026
d6749ce
feat: ExamDetailScreen 구현
chanho0908 Jan 19, 2026
eb4da2e
feat: ExamCompleteScreen 구현
chanho0908 Jan 19, 2026
248bc2d
feat: MainScreen에 Exam 탭 추가
chanho0908 Jan 19, 2026
2556778
feat: DI 모듈에 Exam ViewModel 및 Navigation 추가
chanho0908 Jan 19, 2026
5f230ef
chore: 빌드 설정 업데이트
chanho0908 Jan 19, 2026
d0ac88b
chore: 사용하지 않는 화면 파일 제거
chanho0908 Jan 19, 2026
702ab2c
chore: 프로필 화면 제거
chanho0908 Jan 19, 2026
66ae335
test: FakeRemoteQuestionDataSource 테스트 추가
chanho0908 Jan 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion composeApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ kotlin {
implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlinx.serialization.json)
implementation(libs.kotlinx.collections.immutable)
implementation(libs.napier)
implementation(libs.kotlinx.datetime)
implementation(libs.napier)
}
commonTest.dependencies {
implementation(libs.kotlin.test)
Expand Down
44 changes: 40 additions & 4 deletions composeApp/src/commonMain/composeResources/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
<string name="bottom_nav_question">문제</string>
<string name="bottom_nav_test">시험</string>
<string name="bottom_nav_history">기록</string>
<string name="bottom_nav_profile">프로필</string>

<!-- GoogleSignInButton -->
<string name="google_sign_in_button_description">Google 로그인 버튼</string>
Expand Down Expand Up @@ -32,8 +31,6 @@
<!-- My Answer Section -->
<string name="my_answer">내 답변</string>
<string name="edit">수정</string>
<string name="delete">삭제</string>
<string name="cancel">취소</string>
<string name="save">저장</string>
<string name="no_answer_yet">아직 작성한 답변이 없습니다</string>
<string name="answer_placeholder">답변을 입력하세요</string>
Expand All @@ -45,7 +42,7 @@
<string name="delete_history_confirm">이 답변을 삭제하시겠습니까?</string>

<!-- Answer History Section -->
<string name="answer_history_count">답변 히스토리 (%d)</string>
<string name="answer_history_count">답변 히스토리</string>

<!-- Bottom Action -->
<string name="add_answer">답변 추가하기</string>
Expand All @@ -56,4 +53,43 @@

<!-- Date Formatting -->
<string name="last_modified_prefix">마지막 수정: </string>

<!-- Exam Screen -->
<string name="exam_tab_create">시험 생성</string>
<string name="exam_tab_history">시험 기록</string>

<!-- Exam Create Tab -->
<string name="exam_question_count">문제 개수</string>
<string name="exam_category_select">카테고리 선택</string>
<string name="exam_available_questions">사용 가능한 문제</string>
<string name="exam_category_description">선택한 카테고리에서 문제가 출제됩니다</string>
<string name="exam_start">시험 시작하기</string>
<string name="exam_question_unit">문제 시험</string>
<string name="exam_count_unit">개</string>

<!-- Exam History Tab -->
<string name="exam_history_empty_title">아직 시험 기록이 없습니다</string>
<string name="exam_history_empty_description">시험을 시작해서 기록을 남겨보세요</string>
<string name="exam_correct_answer">정답</string>
<string name="exam_delete">삭제</string>
<string name="exam_delete_confirm_title">시험 삭제</string>
<string name="exam_delete_confirm_message">이 시험 기록을 삭제하시겠습니까?</string>
<string name="exam_delete_success">시험 기록을 삭제했습니다.</string>

<!-- Exam Progress Screen -->
<string name="exam_answer_placeholder">여기에 답변을 작성하세요...</string>
<string name="exam_answer_prompt">답변을 작성해주세요</string>
<string name="exam_button_previous">이전</string>
<string name="exam_button_next">다음</string>
<string name="exam_button_submit">제출하기</string>

<!-- Exam Result Screen -->
<string name="exam_result_title">시험 상세</string>
<string name="exam_result_complete_title">시험 완료!</string>
<string name="exam_result_no_answer">답변을 작성하지 않았습니다</string>
<string name="exam_result_my_answer">내 답변</string>
<string name="exam_result_back_to_questions">문제 목록으로</string>
<string name="exam_question_count_format">문제</string>
<string name="exam_card_question_label">질문</string>
<string name="exam_card_answer_label">내 답변</string>
</resources>
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.peto.droidmorning.di

import com.peto.droidmorning.exam.complete.navigation.ExamCompleteNavGraphContributor
import com.peto.droidmorning.exam.progress.navigation.ExamProgressNavGraphContributor
import com.peto.droidmorning.login.navigation.LoginNavGraphContributor
import com.peto.droidmorning.main.navigation.MainNavGraphContributor
import com.peto.droidmorning.navigation.NavGraphContributor
Expand All @@ -12,4 +14,6 @@ val navigationModule =
single<NavGraphContributor>(named("login")) { LoginNavGraphContributor() }
single<NavGraphContributor>(named("main")) { MainNavGraphContributor() }
single<NavGraphContributor>(named("QuestionDetail")) { QuestionDetailNavGraph() }
single<NavGraphContributor>(named("ExamProgress")) { ExamProgressNavGraphContributor() }
single<NavGraphContributor>(named("ExamComplete")) { ExamCompleteNavGraphContributor() }
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
package com.peto.droidmorning.di

import com.peto.droidmorning.exam.complete.vm.ExamCompleteViewModel
import com.peto.droidmorning.exam.detail.vm.ExamDetailViewModel
import com.peto.droidmorning.exam.main.vm.ExamViewModel
import com.peto.droidmorning.exam.progress.vm.ExamProgressViewModel
import com.peto.droidmorning.login.vm.LoginViewModel
import com.peto.droidmorning.main.vm.MainViewModel
import com.peto.droidmorning.questions.detail.vm.QuestionDetailViewModel
import com.peto.droidmorning.questions.list.vm.QuestionViewModel
import org.koin.core.module.dsl.viewModelOf
Expand All @@ -9,6 +14,11 @@ import org.koin.dsl.module
val viewModelModule =
module {
viewModelOf(::LoginViewModel)
viewModelOf(::MainViewModel)
viewModelOf(::QuestionViewModel)
viewModelOf(::QuestionDetailViewModel)
viewModelOf(::ExamViewModel)
viewModelOf(::ExamProgressViewModel)
viewModelOf(::ExamDetailViewModel)
viewModelOf(::ExamCompleteViewModel)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
package com.peto.droidmorning.exam.complete

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.peto.droidmorning.designsystem.component.ExamQuestionCard
import com.peto.droidmorning.designsystem.theme.AppTheme
import com.peto.droidmorning.designsystem.theme.Dimen
import com.peto.droidmorning.exam.complete.model.ExamCompleteUiState
import com.peto.droidmorning.exam.complete.preview.ExamCompleteUiStatePreviewParameterProvider
import com.peto.droidmorning.exam.complete.vm.ExamCompleteViewModel
import droidmorning.composeapp.generated.resources.Res
import droidmorning.composeapp.generated.resources.exam_result_back_to_questions
import droidmorning.composeapp.generated.resources.exam_result_complete_title
import org.jetbrains.compose.resources.stringResource
import org.koin.compose.viewmodel.koinViewModel

@Composable
fun ExamCompleteScreen(
examId: Long,
onNavigateToQuestions: () -> Unit,
viewModel: ExamCompleteViewModel = koinViewModel(),
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()

LaunchedEffect(examId) {
viewModel.loadExamDetail(examId)
}

ExamCompleteScreenContent(
uiState = uiState,
onNavigateToQuestions = onNavigateToQuestions,
)
}

@Composable
private fun ExamCompleteScreenContent(
uiState: ExamCompleteUiState,
onNavigateToQuestions: () -> Unit,
modifier: Modifier = Modifier,
) {
Scaffold(
modifier =
modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.background),
containerColor = MaterialTheme.colorScheme.background,
bottomBar = {
Box(
modifier =
Modifier
.fillMaxWidth()
.background(Color.Transparent),
) {
Button(
onClick = onNavigateToQuestions,
modifier =
Modifier
.fillMaxWidth()
.padding(Dimen.spacingBase),
colors =
ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary,
),
) {
Text(
text = stringResource(Res.string.exam_result_back_to_questions),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
)
}
}
},
) { paddingValues ->
LazyColumn(
modifier =
Modifier
.fillMaxSize()
.padding(paddingValues),
contentPadding = PaddingValues(Dimen.spacingBase),
verticalArrangement = Arrangement.spacedBy(Dimen.spacingBase),
) {
item { ExamCompleteHeader() }

itemsIndexed(uiState.examDetails) { index, examDetail ->
ExamQuestionCard(
questionNumber = index + 1,
examDetail = examDetail,
)
}
}
}
}

@Composable
private fun ExamCompleteHeader(modifier: Modifier = Modifier) {
Column(
modifier = modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(Dimen.spacingSm),
) {
Box(
modifier =
Modifier
.size(80.dp)
.background(
color = Color(0xFFB8E6D5),
shape = CircleShape,
),
contentAlignment = Alignment.Center,
) {
Icon(
imageVector = Icons.Default.Check,
contentDescription = null,
modifier = Modifier.size(32.dp),
tint = Color(0xFF34A853),
)
}

Text(
text = stringResource(Res.string.exam_result_complete_title),
style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center,
)
}
}

@Preview
@Composable
private fun ExamCompleteScreenPreview(
@PreviewParameter(ExamCompleteUiStatePreviewParameterProvider::class)
uiState: ExamCompleteUiState,
) {
AppTheme {
ExamCompleteScreenContent(
uiState = uiState,
onNavigateToQuestions = {},
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.peto.droidmorning.exam.complete.model

import androidx.compose.runtime.Immutable
import com.peto.droidmorning.domain.model.exam.ExamDetail
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList

@Immutable
data class ExamCompleteUiState(
val examDetails: ImmutableList<ExamDetail> = persistentListOf(),
) {
fun updateExamDetails(examDetails: List<ExamDetail>): ExamCompleteUiState = copy(examDetails = examDetails.toImmutableList())
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.peto.droidmorning.exam.complete.navigation

import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.NavType
import androidx.navigation.compose.composable
import androidx.navigation.navArgument
import androidx.navigation.navigation
import androidx.navigation.toRoute
import com.peto.droidmorning.exam.complete.ExamCompleteScreen
import com.peto.droidmorning.navigation.NavGraphContributor
import com.peto.droidmorning.navigation.NavRoutes

class ExamCompleteNavGraphContributor : NavGraphContributor {
override val graphRoute: NavRoutes
get() = NavRoutes.ExamCompleteGraph

override val startDestination: String
get() = NavRoutes.ExamComplete.ROUTE

override val priority: Int = 4

override fun NavGraphBuilder.registerGraph(navController: NavHostController) {
navigation(
route = graphRoute.route,
startDestination = startDestination,
) {
composable(
route = NavRoutes.ExamComplete.ROUTE,
arguments =
listOf(
navArgument("examId") {
type = NavType.LongType
},
),
) { backStackEntry ->
val args = backStackEntry.toRoute<NavRoutes.ExamComplete>()

ExamCompleteScreen(
examId = args.examId,
onNavigateToQuestions = {
navController.popBackStack(NavRoutes.Main.route, inclusive = false)
},
)
}
}
}
}
Loading