@@ -4,71 +4,167 @@ import { useQuery } from '@tanstack/react-query';
44import { quizAPI } from '../utils/api' ;
55import Container from './common/Container' ;
66import Section from './common/Section' ;
7+ import { useModal } from '../hooks/useModal' ;
78
89interface QuizData {
9- quiz : string ;
10+ question : string ;
1011 choice1 : string ;
1112 choice2 : string ;
1213 choice3 : string ;
1314 choice4 : string ;
15+ answerNumber : string ;
16+ commentary : string ;
1417}
1518
1619interface AnswerResult {
1720 isCorrect : boolean ;
1821 answer : string ;
22+ commentary : string ;
1923}
2024
2125// 임시 데이터
2226const fakeTodayQuiz : QuizData = {
23- quiz : "다음 중 JavaScript에서 변수를 선언하는 올바른 방법은?" ,
27+ question : "다음 중 JavaScript에서 변수를 선언하는 올바른 방법은?" ,
2428 choice1 : "1. variable myVar = 10;" ,
2529 choice2 : "2. let myVar = 10;" ,
2630 choice3 : "3. declare myVar = 10;" ,
27- choice4 : "4. set myVar = 10;"
31+ choice4 : "4. set myVar = 10;" ,
32+ answerNumber : "" ,
33+ commentary : ""
2834} ;
2935
3036const fakeAnswer : AnswerResult = {
3137 isCorrect : true ,
32- answer : "2. let myVar = 10;"
38+ answer : "2번. let myVar = 10;" ,
39+ commentary : "let은 ES6에서 도입된 블록 스코프 변수 선언 키워드입니다."
3340} ;
3441
3542const TodayQuizPage : React . FC = ( ) => {
3643 const [ searchParams ] = useSearchParams ( ) ;
3744 const [ selectedAnswer , setSelectedAnswer ] = useState < number | null > ( null ) ;
3845 const [ isSubmitted , setIsSubmitted ] = useState ( false ) ;
3946 const [ answerResult , setAnswerResult ] = useState < AnswerResult | null > ( null ) ;
47+ const { openModal } = useModal ( ) ;
4048
4149 const subscriptionId = searchParams . get ( 'subscriptionId' ) ;
4250 const quizId = searchParams . get ( 'quizId' ) ;
4351
44- const { data : quiz , isLoading, error } = useQuery ( {
52+ const { data : question , isLoading, error } = useQuery ( {
4553 queryKey : [ 'todayQuiz' , subscriptionId , quizId ] ,
4654 queryFn : async ( ) => {
4755 const response = await quizAPI . getTodayQuiz ( subscriptionId || undefined , quizId || undefined ) ;
48- console . log ( 'Quiz response:' , response ) ;
4956
50- return response as QuizData ;
57+ // 다양한 응답 구조 처리
58+ let quizData ;
59+
60+ if ( response && typeof response === 'object' ) {
61+ // Case 1: { data: { question, choice1, choice2, choice3, choice4 } }
62+ if ( 'data' in response && response . data ) {
63+ quizData = response . data ;
64+ }
65+ // Case 2: { question, choice1, choice2, choice3, choice4 } 직접
66+ else if ( 'question' in response ) {
67+ quizData = response ;
68+ }
69+ // Case 3: 기타 구조
70+ else {
71+ quizData = response ;
72+ }
73+ } else {
74+ quizData = null ;
75+ }
76+
77+ return quizData as QuizData ;
5178 } ,
5279 enabled : ! ! ( subscriptionId && quizId ) ,
5380 } ) ;
81+
5482
5583 const handleSubmit = async ( e : React . FormEvent ) => {
5684 e . preventDefault ( ) ;
5785
5886 if ( selectedAnswer === null ) {
59- alert ( '선택지를 먼저 클릭해주세요!' ) ;
87+ openModal ( {
88+ title : '선택 필요' ,
89+ content : (
90+ < div className = "text-center py-4" >
91+ < div className = "w-16 h-16 mx-auto mb-4 bg-yellow-100 rounded-full flex items-center justify-center" >
92+ < svg className = "w-8 h-8 text-yellow-500" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >
93+ < path strokeLinecap = "round" strokeLinejoin = "round" strokeWidth = { 2 } d = "M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z" />
94+ </ svg >
95+ </ div >
96+ < p className = "text-gray-700 text-lg" > 선택지를 먼저 클릭해주세요!</ p >
97+ </ div >
98+ ) ,
99+ size : 'sm'
100+ } ) ;
60101 return ;
61102 }
62103
63- if ( quizId && subscriptionId ) {
104+ if ( quizId && subscriptionId && displayQuiz ) {
64105 try {
65- const result = await quizAPI . submitTodayQuizAnswer ( quizId , selectedAnswer , subscriptionId ) ;
66- setAnswerResult ( result as AnswerResult ) ;
106+ // API로 답안 제출 (기록용)
107+ await quizAPI . submitTodayQuizAnswer ( quizId , selectedAnswer , subscriptionId ) ;
108+
109+ // 클라이언트에서 정답 검증
110+ const correctAnswerNumber = parseInt ( displayQuiz . answerNumber ) ;
111+ const isCorrect = selectedAnswer === correctAnswerNumber ;
112+
113+ // 정답 텍스트 찾기
114+ const answerText = displayQuiz [ `choice${ correctAnswerNumber } ` as keyof QuizData ] as string ;
115+ const cleanAnswerText = answerText ? answerText . replace ( / ^ \d + \. \s * / , '' ) : '' ;
116+
117+ const result : AnswerResult = {
118+ isCorrect,
119+ answer : `${ correctAnswerNumber } 번. ${ cleanAnswerText } ` ,
120+ commentary : displayQuiz . commentary
121+ } ;
122+
123+ setAnswerResult ( result ) ;
67124 setIsSubmitted ( true ) ;
68- } catch ( error ) {
69- console . error ( 'Failed to submit answer:' , error ) ;
70- // API 실패 시 fake 데이터 사용
71- setAnswerResult ( fakeAnswer ) ;
125+ } catch ( error : any ) {
126+
127+ // 400 에러 체크 및 메시지 처리
128+ if ( error ?. status === 400 ) {
129+ const errorMessage = error ?. message || '잘못된 요청입니다. 답안 제출에 실패했습니다.' ;
130+
131+ openModal ( {
132+ title : '알림' ,
133+ content : (
134+ < div className = "text-center py-4" >
135+ < div className = "w-16 h-16 mx-auto mb-4 bg-red-100 rounded-full flex items-center justify-center" >
136+ < svg className = "w-8 h-8 text-red-500" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >
137+ < path strokeLinecap = "round" strokeLinejoin = "round" strokeWidth = { 2 } d = "M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
138+ </ svg >
139+ </ div >
140+ < p className = "text-gray-700 text-lg" > { errorMessage } </ p >
141+ </ div >
142+ ) ,
143+ size : 'sm'
144+ } ) ;
145+ return ;
146+ }
147+
148+ // 추가적인 400 에러 체크 (다른 형태일 수 있음)
149+ // if (error?.message?.includes('400') || error?.toString()?.includes('400')) {
150+ // console.log('Found 400 in message, showing alert');
151+ // alert('이미 제출한 문제입니다.');
152+ // return;
153+ // }
154+
155+ // 기타 API 실패해도 클라이언트에서 정답 검증
156+ const correctAnswerNumber = parseInt ( displayQuiz . answerNumber || '2' ) ;
157+ const isCorrect = selectedAnswer === correctAnswerNumber ;
158+ const answerText = displayQuiz [ `choice${ correctAnswerNumber } ` as keyof QuizData ] as string ;
159+ const cleanAnswerText = answerText ? answerText . replace ( / ^ \d + \. \s * / , '' ) : '' ;
160+
161+ const result : AnswerResult = {
162+ isCorrect,
163+ answer : `${ correctAnswerNumber } 번. ${ cleanAnswerText } ` ,
164+ commentary : displayQuiz . commentary || '해설 정보가 없습니다.'
165+ } ;
166+
167+ setAnswerResult ( result ) ;
72168 setIsSubmitted ( true ) ;
73169 }
74170 }
@@ -94,7 +190,7 @@ const TodayQuizPage: React.FC = () => {
94190 }
95191
96192 // API 실패 시 fake 데이터 사용
97- const displayQuiz = quiz || fakeTodayQuiz ;
193+ const displayQuiz = question || fakeTodayQuiz ;
98194
99195 if ( isSubmitted && answerResult ) {
100196 const isCorrect = answerResult . isCorrect ;
@@ -115,11 +211,19 @@ const TodayQuizPage: React.FC = () => {
115211 < div className = "bg-white rounded-2xl p-8 shadow-sm border border-gray-100 mb-8" >
116212 < h3 className = "text-xl font-bold text-gray-900 mb-4" > 결과</ h3 >
117213 < p className = "text-gray-700 leading-relaxed mb-4" > 답안이 성공적으로 제출되었습니다.</ p >
118- < div className = "p-4 bg-brand-50 rounded-xl" >
214+
215+ < div className = "p-4 bg-brand-50 rounded-xl mb-6" >
119216 < p className = "text-brand-800 font-medium" >
120217 정답: { answerResult . answer }
121218 </ p >
122219 </ div >
220+
221+ < div className = "p-4 bg-gray-50 rounded-xl" >
222+ < h4 className = "text-lg font-bold text-gray-900 mb-2" > 해설</ h4 >
223+ < p className = "text-gray-700 leading-relaxed" >
224+ { answerResult . commentary }
225+ </ p >
226+ </ div >
123227 </ div >
124228 </ div >
125229 </ Container >
@@ -148,26 +252,72 @@ const TodayQuizPage: React.FC = () => {
148252 < div className = "font-pretendard text-center py-8" >
149253 { /* Question Box */ }
150254 < div className = "bg-brand-50 border border-brand-200 rounded-xl p-6 text-lg font-medium text-gray-800 mb-8 max-w-4xl mx-auto text-left" >
151- < strong > Q. { displayQuiz . quiz } </ strong >
255+ < strong > Q. { displayQuiz ?. question } </ strong >
152256 </ div >
153257
154258 { /* Quiz Form */ }
155259 < form onSubmit = { handleSubmit } >
156260 { /* Options List - 세로 배치로 변경 */ }
157- < div className = "space-y-3 max-w-4xl mx-auto mb-8" >
158- { [ displayQuiz . choice1 , displayQuiz . choice2 , displayQuiz . choice3 , displayQuiz . choice4 ] . map ( ( choice , index ) => (
159- < div
160- key = { index }
161- onClick = { ( ) => handleOptionClick ( index + 1 ) }
162- className = { `border-2 p-4 rounded-xl cursor-pointer transition-all duration-200 hover:shadow-md text-left ${
163- selectedAnswer === index + 1
164- ? 'border-brand-500 bg-brand-100 text-brand-800'
165- : 'border-gray-300 bg-white text-gray-700 hover:border-brand-300'
166- } `}
167- >
168- < span className = "font-medium leading-relaxed break-words" > { choice } </ span >
169- </ div >
170- ) ) }
261+ < div className = "space-y-4 max-w-4xl mx-auto mb-8" >
262+ { [ displayQuiz ?. choice1 , displayQuiz ?. choice2 , displayQuiz ?. choice3 , displayQuiz ?. choice4 ] . map ( ( choice , index ) => {
263+ const isSelected = selectedAnswer === index + 1 ;
264+
265+ // 앞의 "1. 2. 3. 4." 숫자 부분만 제거
266+ const displayChoice = choice ? choice . replace ( / ^ \d + \. \s * / , '' ) : '' ;
267+ const optionNumber = index + 1 ;
268+
269+ return (
270+ < div
271+ key = { index }
272+ onClick = { ( ) => handleOptionClick ( index + 1 ) }
273+ className = { `group relative p-5 rounded-2xl cursor-pointer transition-all duration-300 transform hover:scale-[1.02] hover:shadow-lg text-left border-2 ${
274+ isSelected
275+ ? 'border-brand-500 bg-gradient-to-r from-brand-50 to-brand-100 shadow-md'
276+ : 'border-gray-200 bg-white hover:border-brand-300 hover:bg-gray-50'
277+ } `}
278+ >
279+ < div className = "flex items-center space-x-4" >
280+ { /* Option Number with modern design */ }
281+ < div className = { `flex-shrink-0 w-10 h-10 rounded-xl flex items-center justify-center font-bold text-lg transition-all duration-300 ${
282+ isSelected
283+ ? 'bg-gradient-to-br from-brand-500 to-brand-600 text-white shadow-md'
284+ : 'bg-gradient-to-br from-gray-100 to-gray-200 text-gray-600 group-hover:from-brand-100 group-hover:to-brand-200 group-hover:text-brand-700'
285+ } `} >
286+ < span className = "font-extrabold" > { optionNumber } </ span >
287+ </ div >
288+
289+ { /* Option Text */ }
290+ < div className = "flex-1 min-w-0" >
291+ < span className = { `font-medium leading-relaxed break-words transition-colors duration-300 ${
292+ isSelected
293+ ? 'text-brand-800'
294+ : 'text-gray-700 group-hover:text-gray-900'
295+ } `} >
296+ { displayChoice }
297+ </ span >
298+ </ div >
299+
300+ { /* Selected Indicator */ }
301+ { isSelected && (
302+ < div className = "flex-shrink-0" >
303+ < div className = "w-7 h-7 rounded-xl bg-gradient-to-br from-green-500 to-green-600 flex items-center justify-center shadow-md" >
304+ < svg className = "w-4 h-4 text-white" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >
305+ < path strokeLinecap = "round" strokeLinejoin = "round" strokeWidth = { 3 } d = "M5 13l4 4L19 7" />
306+ </ svg >
307+ </ div >
308+ </ div >
309+ ) }
310+ </ div >
311+
312+ { /* Hover Effect Border */ }
313+ < div className = { `absolute inset-0 rounded-2xl transition-opacity duration-300 ${
314+ isSelected
315+ ? 'opacity-0'
316+ : 'opacity-0 group-hover:opacity-100 bg-gradient-to-r from-brand-500/5 to-navy-500/5'
317+ } `} />
318+ </ div >
319+ ) ;
320+ } ) }
171321 </ div >
172322
173323 { /* Submit Button */ }
0 commit comments