From 392ac642b01b3036a3ce6cd6655caa55ea3a266c Mon Sep 17 00:00:00 2001 From: jieun Date: Tue, 20 Oct 2026 21:20:28 +0900 Subject: [PATCH] Implement Vue quiz screen and fix route authority --- src/main/webapp/app/quiz/quiz.component.ts | 100 +++++++++++++++++++++ src/main/webapp/app/quiz/quiz.service.ts | 64 +++++++++++++ src/main/webapp/app/quiz/quiz.vue | 78 ++++++++++++++++ src/main/webapp/app/router/index.ts | 8 ++ 4 files changed, 250 insertions(+) create mode 100644 src/main/webapp/app/quiz/quiz.component.ts create mode 100644 src/main/webapp/app/quiz/quiz.service.ts create mode 100644 src/main/webapp/app/quiz/quiz.vue diff --git a/src/main/webapp/app/quiz/quiz.component.ts b/src/main/webapp/app/quiz/quiz.component.ts new file mode 100644 index 0000000..ef7d3b2 --- /dev/null +++ b/src/main/webapp/app/quiz/quiz.component.ts @@ -0,0 +1,100 @@ +import { defineComponent, onMounted, ref } from 'vue'; +import { + type ExamQuestionView, + type ExamResult, + type OptionKey, + type SubjectItem, + fetchActiveSubjects, + loadQuestions, + startExam, + submitExam, +} from './quiz.service'; + +export default defineComponent({ + name: 'QuizView', + setup() { + const subjects = ref([]); + const selectedSubjectId = ref(null); + const attemptId = ref(null); + const questions = ref([]); + const selectedAnswers = ref>({}); + const result = ref(null); + const errorMessage = ref(null); + const loading = ref(false); + + onMounted(async () => { + try { + subjects.value = await fetchActiveSubjects(); + } catch { + errorMessage.value = '과목 목록을 불러오지 못했습니다.'; + } + }); + + const handleStartExam = async () => { + if (!selectedSubjectId.value) return; + + errorMessage.value = null; + loading.value = true; + + try { + const started = await startExam(selectedSubjectId.value); + attemptId.value = started.attemptId; + + const loadedQuestions = await loadQuestions(started.attemptId); + questions.value = loadedQuestions; + selectedAnswers.value = {}; + + loadedQuestions.forEach(q => { + selectedAnswers.value[q.questionId] = null; + }); + } catch (err: any) { + errorMessage.value = err?.response?.data?.detail || '응시를 시작할 수 없습니다.'; + attemptId.value = null; + } finally { + loading.value = false; + } + }; + + const handleSubmitExam = async () => { + if (!attemptId.value) return; + + errorMessage.value = null; + loading.value = true; + + try { + const payload = questions.value.map(q => ({ + questionId: q.questionId, + selectedOption: selectedAnswers.value[q.questionId] ?? null, + })); + + result.value = await submitExam(attemptId.value, payload); + } catch (err: any) { + errorMessage.value = err?.response?.data?.detail || '답안 제출 중 오류가 발생했습니다.'; + } finally { + loading.value = false; + } + }; + + const resetQuiz = () => { + attemptId.value = null; + questions.value = []; + selectedAnswers.value = {}; + result.value = null; + errorMessage.value = null; + }; + + return { + subjects, + selectedSubjectId, + attemptId, + questions, + selectedAnswers, + result, + errorMessage, + loading, + handleStartExam, + handleSubmitExam, + resetQuiz, + }; + }, +}); diff --git a/src/main/webapp/app/quiz/quiz.service.ts b/src/main/webapp/app/quiz/quiz.service.ts new file mode 100644 index 0000000..d50e02c --- /dev/null +++ b/src/main/webapp/app/quiz/quiz.service.ts @@ -0,0 +1,64 @@ +import axios from 'axios'; + +export type OptionKey = 'A' | 'B' | 'C' | 'D'; + +export interface SubjectItem { + id: number; + name: string; + description?: string; + active: boolean; +} + +export interface ChoiceView { + optionKey: OptionKey; + content: string; + displayOrder: number; +} + +export interface ExamQuestionView { + questionId: number; + prompt: string; + choices: ChoiceView[]; +} + +export interface SubmitAnswer { + questionId: number; + selectedOption: OptionKey | null; +} + +export interface AnswerResult { + questionId: number; + questionText: string; + selectedOption: OptionKey | null; + selectedContent: string | null; + correct: boolean; + awardedPoints: number; + explanation?: string; +} + +export interface ExamResult { + attemptId: number; + totalScore: number; + submittedAt: string; + answers: AnswerResult[]; +} + +export async function fetchActiveSubjects(): Promise { + const response = await axios.get('/api/subjects?page=0&size=100'); + return response.data.filter(s => s.active); +} + +export async function startExam(subjectId: number): Promise<{ attemptId: number; startedAt: string }> { + const response = await axios.post('/api/exam-attempts/start', { subjectId }); + return response.data; +} + +export async function loadQuestions(attemptId: number): Promise { + const response = await axios.get(`/api/exam-attempts/${attemptId}/questions`); + return response.data; +} + +export async function submitExam(attemptId: number, answers: SubmitAnswer[]): Promise { + const response = await axios.post(`/api/exam-attempts/${attemptId}/submit`, { answers }); + return response.data; +} diff --git a/src/main/webapp/app/quiz/quiz.vue b/src/main/webapp/app/quiz/quiz.vue new file mode 100644 index 0000000..abd9f66 --- /dev/null +++ b/src/main/webapp/app/quiz/quiz.vue @@ -0,0 +1,78 @@ + + + diff --git a/src/main/webapp/app/router/index.ts b/src/main/webapp/app/router/index.ts index 9979b1a..cf300f6 100644 --- a/src/main/webapp/app/router/index.ts +++ b/src/main/webapp/app/router/index.ts @@ -7,6 +7,8 @@ import admin from '@/router/admin'; import entities from '@/router/entities'; import pages from '@/router/pages'; +const QuizView = () => import('@/quiz/quiz.vue'); + export const createRouter = () => createVueRouter({ history: createWebHistory(), @@ -28,6 +30,12 @@ export const createRouter = () => component: Error, meta: { error404: true }, }, + { + path: '/quiz', + name: 'QuizView', + component: QuizView, + meta: { authorities: ['ROLE_USER'] }, + }, ...account, ...admin, entities,