@@ -75,6 +75,7 @@ const TodayQuizSection: React.FC = () => {
7575 const [ isCorrectFromAI , setIsCorrectFromAI ] = useState < boolean | null > ( null ) ;
7676 const [ resultChars , setResultChars ] = useState < string [ ] > ( [ ] ) ;
7777 const [ feedbackChars , setFeedbackChars ] = useState < string [ ] > ( [ ] ) ;
78+ const [ isDuplicateAnswer , setIsDuplicateAnswer ] = useState < boolean > ( false ) ;
7879 const { openModal } = useModal ( ) ;
7980 const sseConnectionRef = useRef < { close : ( ) => void } | null > ( null ) ;
8081
@@ -137,11 +138,18 @@ const TodayQuizSection: React.FC = () => {
137138 const timer = setTimeout ( ( ) => {
138139 const newPercentages : { [ key : number ] : number } = { } ;
139140 [ 1 , 2 , 3 , 4 ] . forEach ( choice => {
140- // 사용자의 선택을 포함한 새로운 비율 계산
141141 const originalRate = selectionRates . selectionRates [ choice . toString ( ) ] || 0 ;
142142 const originalCount = Math . round ( originalRate * selectionRates . totalCount ) ;
143- const newCount = selectedAnswer === choice ? originalCount + 1 : originalCount ;
144- const newTotalCount = selectionRates . totalCount + 1 ;
143+
144+ // 중복 답변이 아닌 경우에만 새로운 선택을 카운트에 추가
145+ let newCount = originalCount ;
146+ let newTotalCount = selectionRates . totalCount ;
147+
148+ if ( ! isDuplicateAnswer ) {
149+ newCount = selectedAnswer === choice ? originalCount + 1 : originalCount ;
150+ newTotalCount = selectionRates . totalCount + 1 ;
151+ }
152+
145153 const newRate = newCount / newTotalCount ;
146154 newPercentages [ choice ] = Math . round ( newRate * 100 ) ;
147155 } ) ;
@@ -150,7 +158,7 @@ const TodayQuizSection: React.FC = () => {
150158
151159 return ( ) => clearTimeout ( timer ) ;
152160 }
153- } , [ isSubmitted , selectionRates , selectedAnswer ] ) ;
161+ } , [ isSubmitted , selectionRates , selectedAnswer , isDuplicateAnswer ] ) ;
154162
155163 const { data : question , isLoading } = useQuery ( {
156164 queryKey : [ 'todayQuiz' , subscriptionId , quizId ] ,
@@ -252,51 +260,114 @@ const TodayQuizSection: React.FC = () => {
252260 }
253261 const submitResponse = await quizAPI . submitTodayQuizAnswer ( quizId , submitAnswer , subscriptionId ) ;
254262
255- // submitResponse에서 userQuizAnswerId 추출 (모든 타입에서 공통)
256- let userQuizAnswerId : string ;
257-
263+ // 응답에서 UserQuizAnswerResponseDto 추출
264+ let responseData ;
258265 if ( submitResponse && typeof submitResponse === 'object' ) {
259- if ( 'data' in submitResponse && submitResponse . data ) {
260- userQuizAnswerId = ( submitResponse . data as any ) . toString ( ) ;
261- } else if ( 'userQuizAnswerId' in submitResponse ) {
262- userQuizAnswerId = ( submitResponse as any ) . userQuizAnswerId . toString ( ) ;
263- } else {
264- userQuizAnswerId = ( submitResponse as any ) . toString ( ) ;
265- }
266+ responseData = ( 'data' in submitResponse ) ? submitResponse . data : submitResponse ;
266267 } else {
267- userQuizAnswerId = ( submitResponse as any ) . toString ( ) ;
268+ responseData = submitResponse ;
268269 }
269270
270- // 모든 타입에서 평가 API 호출
271- try {
272- await quizAPI . evaluateQuizAnswer ( userQuizAnswerId ) ;
273- } catch ( evaluateError ) {
274- console . error ( '답안 평가 요청 실패:' , evaluateError ) ;
275- // 평가 API 실패해도 계속 진행
276- }
277-
278- if ( displayQuiz . quizType === 'MULTIPLE_CHOICE' ) {
279- // 객관식: 클라이언트에서 정답 검증
280- const correctAnswerNumber = parseInt ( displayQuiz . answerNumber || '1' ) ;
281- const isCorrect = selectedAnswer === correctAnswerNumber ;
282- const choiceText = displayQuiz [ `choice${ correctAnswerNumber } ` as keyof QuizData ] as string ;
283- const cleanAnswerText = choiceText ? choiceText . replace ( / ^ \d + \. \s * / , '' ) : '' ;
284- const answerText = `${ correctAnswerNumber } 번. ${ cleanAnswerText } ` ;
271+ // 중복 답변 체크
272+ if ( ( responseData as any ) ?. duplicated ) {
273+ // 중복 답변 상태 설정
274+ setIsDuplicateAnswer ( true ) ;
285275
286- const result : AnswerResult = {
287- isCorrect,
288- answer : answerText ,
289- commentary : displayQuiz . commentary
276+ // 중복 답변인 경우 모달 표시하고 기존 답안 결과 보여주기
277+ openModal ( {
278+ title : '이미 답변한 문제입니다' ,
279+ content : (
280+ < div className = "text-center py-4" >
281+ < div className = "w-16 h-16 mx-auto mb-4 bg-blue-100 rounded-full flex items-center justify-center" >
282+ < svg className = "w-8 h-8 text-blue-500" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >
283+ < path strokeLinecap = "round" strokeLinejoin = "round" strokeWidth = { 2 } d = "M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
284+ </ svg >
285+ </ div >
286+ < p className = "text-gray-700 text-lg mb-4" > 이전에 답변한 내용을 확인해보세요!</ p >
287+ </ div >
288+ ) ,
289+ size : 'sm'
290+ } ) ;
291+
292+ // 중복 답변인 경우 기존 답안 결과 표시
293+ const duplicateResult : AnswerResult = {
294+ isCorrect : ( responseData as any ) ?. correct || false ,
295+ answer : ( responseData as any ) ?. answer || displayQuiz . answer || '' ,
296+ commentary : ( responseData as any ) ?. commentary || displayQuiz . commentary
290297 } ;
291298
292- setAnswerResult ( result ) ;
293- setIsSubmitted ( true ) ;
294- } else if ( displayQuiz . quizType === 'SHORT_ANSWER' ) {
295- // 주관식: 평가 API 호출하여 정답/오답 확인
299+ setAnswerResult ( duplicateResult ) ;
296300 setIsSubmitted ( true ) ;
297301
302+ // 중복 답변인 경우 이전 선택한 답안 복원 (객관식만)
303+ if ( displayQuiz . quizType === 'MULTIPLE_CHOICE' ) {
304+ const userAnswer = ( responseData as any ) ?. userAnswer ;
305+ if ( userAnswer ) {
306+ // userAnswer에서 선택 번호 추출 (예: "1. text" -> 1)
307+ const answerMatch = userAnswer . match ( / ^ ( \d + ) / ) ;
308+ if ( answerMatch ) {
309+ setSelectedAnswer ( parseInt ( answerMatch [ 1 ] ) ) ;
310+ }
311+ }
312+
313+ // 선택 비율 가져오기
314+ if ( quizId ) {
315+ try {
316+ const ratesResponse = await quizAPI . getQuizSelectionRates ( quizId ) ;
317+ if ( ratesResponse && typeof ratesResponse === 'object' ) {
318+ const ratesData = ( 'data' in ratesResponse ) ? ratesResponse . data : ratesResponse ;
319+ setSelectionRates ( ratesData as SelectionRatesData ) ;
320+ }
321+ } catch ( error ) {
322+ console . error ( '중복 답변 - 선택 비율 데이터 가져오기 실패:' , error ) ;
323+ }
324+ }
325+ }
326+
327+ return ; // 여기서 중단
328+ }
329+
330+ // responseData에서 userQuizAnswerId 추출 (모든 타입에서 공통)
331+ const userQuizAnswerId = ( responseData as any ) ?. userQuizAnswerId ?. toString ( ) || '' ;
332+
333+
334+ if ( displayQuiz . quizType === 'MULTIPLE_CHOICE' ) {
335+ // 객관식: evaluate API로 정답/오답 확인
336+ try {
337+ const evaluateResponse = await quizAPI . evaluateQuizAnswer ( userQuizAnswerId ) ;
338+
339+ // 평가 응답에서 결과 추출
340+ let evaluateData ;
341+ if ( evaluateResponse && typeof evaluateResponse === 'object' ) {
342+ evaluateData = ( 'data' in evaluateResponse ) ? evaluateResponse . data : evaluateResponse ;
343+ } else {
344+ evaluateData = evaluateResponse ;
345+ }
346+
347+ const result : AnswerResult = {
348+ isCorrect : ( evaluateData as any ) ?. correct || false ,
349+ answer : ( evaluateData as any ) ?. answer || displayQuiz . answer || '' ,
350+ commentary : ( evaluateData as any ) ?. commentary || displayQuiz . commentary
351+ } ;
352+
353+ setAnswerResult ( result ) ;
354+ setIsSubmitted ( true ) ;
355+ } catch ( evaluateError ) {
356+ console . error ( '객관식 답안 평가 실패:' , evaluateError ) ;
357+
358+ // 평가 API 실패 시 응답 데이터 사용
359+ const fallbackResult : AnswerResult = {
360+ isCorrect : ( responseData as any ) ?. correct || false ,
361+ answer : ( responseData as any ) ?. answer || displayQuiz . answer || '' ,
362+ commentary : ( responseData as any ) ?. commentary || displayQuiz . commentary
363+ } ;
364+
365+ setAnswerResult ( fallbackResult ) ;
366+ setIsSubmitted ( true ) ;
367+ }
368+ } else if ( displayQuiz . quizType === 'SHORT_ANSWER' ) {
369+ // 주관식: evaluate API로 정답/오답 확인
298370 try {
299- // 평가 API 호출하여 정답/오답 결과 받기
300371 const evaluateResponse = await quizAPI . evaluateQuizAnswer ( userQuizAnswerId ) ;
301372
302373 // 평가 응답에서 결과 추출
@@ -307,33 +378,33 @@ const TodayQuizSection: React.FC = () => {
307378 evaluateData = evaluateResponse ;
308379 }
309380
310- // 평가 결과로 결과 설정
311381 const result : AnswerResult = {
312- isCorrect : ( evaluateData as any ) ?. isCorrect || false ,
382+ isCorrect : ( evaluateData as any ) ?. correct || false ,
313383 answer : ( evaluateData as any ) ?. answer || displayQuiz . answer || '' ,
314384 commentary : ( evaluateData as any ) ?. commentary || displayQuiz . commentary
315385 } ;
316386
317387 setAnswerResult ( result ) ;
388+ setIsSubmitted ( true ) ;
318389 } catch ( evaluateError ) {
319390 console . error ( '주관식 답안 평가 실패:' , evaluateError ) ;
320391
321- // 평가 API 실패 시 기본값으로 처리
392+ // 평가 API 실패 시 응답 데이터 사용
322393 const fallbackResult : AnswerResult = {
323- isCorrect : false ,
324- answer : displayQuiz . answer || '' ,
325- commentary : displayQuiz . commentary
394+ isCorrect : ( responseData as any ) ?. correct || false ,
395+ answer : ( responseData as any ) ?. answer || displayQuiz . answer || '' ,
396+ commentary : ( responseData as any ) ?. commentary || displayQuiz . commentary
326397 } ;
327398
328399 setAnswerResult ( fallbackResult ) ;
400+ setIsSubmitted ( true ) ;
329401 }
330402 } else if ( displayQuiz . quizType === 'SUBJECTIVE' ) {
331- // 서술형: AI 피드백 있음
332- // 먼저 기본 결과 표시 (AI 피드백 없이)
403+ // 서술형: 응답 데이터로 초기 결과 설정 후 AI 피드백
333404 const initialResult : AnswerResult = {
334- isCorrect : false , // 일단 false로 설정, AI 피드백에서 업데이트
335- answer : displayQuiz . answer || '' ,
336- commentary : displayQuiz . commentary
405+ isCorrect : ( responseData as any ) ?. correct || false ,
406+ answer : ( responseData as any ) ?. answer || displayQuiz . answer || '' ,
407+ commentary : ( responseData as any ) ?. commentary || displayQuiz . commentary
337408 } ;
338409
339410 setAnswerResult ( initialResult ) ;
@@ -572,7 +643,7 @@ const TodayQuizSection: React.FC = () => {
572643 < path strokeLinecap = "round" strokeLinejoin = "round" strokeWidth = { 2 } d = "M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
573644 </ svg >
574645 < span className = "text-sm font-medium text-blue-700" >
575- 총 { isSubmitted ? selectionRates . totalCount + 1 : selectionRates . totalCount } 명이 이 문제를 풀었습니다
646+ 총 { isSubmitted && ! isDuplicateAnswer ? selectionRates . totalCount + 1 : selectionRates . totalCount } 명이 이 문제를 풀었습니다
576647 </ span >
577648 </ div >
578649 ) }
@@ -725,8 +796,8 @@ const TodayQuizSection: React.FC = () => {
725796 { /* Result Section - 제출 후에만 표시 */ }
726797 { isSubmitted && answerResult && (
727798 < div className = "max-w-4xl mx-auto mb-8" >
728- { /* 정답/오답 메시지 - 객관식, 주관식에 표시 FIXME: 주관식은 일단 제외) */ }
729- { ( displayQuiz ?. quizType === 'MULTIPLE_CHOICE' ) && (
799+ { /* 정답/오답 메시지 - 모든 타입에 표시 */ }
800+ { ( displayQuiz ?. quizType === 'MULTIPLE_CHOICE' || displayQuiz ?. quizType === 'SHORT_ANSWER' || displayQuiz ?. quizType === 'SUBJECTIVE' ) && (
730801 < div className = { `inline-flex items-center rounded-full px-6 py-3 mb-6 ${
731802 answerResult . isCorrect ? 'bg-green-100' : 'bg-red-100'
732803 } `} >
@@ -885,9 +956,15 @@ const TodayQuizSection: React.FC = () => {
885956 const originalRate = selectionRates . selectionRates [ choice . toString ( ) ] || 0 ;
886957 const originalCount = Math . round ( originalRate * selectionRates . totalCount ) ;
887958
888- // 사용자의 선택을 포함해서 새로운 비율 계산
889- const newCount = selectedAnswer === choice ? originalCount + 1 : originalCount ;
890- const newTotalCount = selectionRates . totalCount + 1 ;
959+ // 중복 답변이 아닌 경우에만 사용자의 선택을 포함해서 새로운 비율 계산
960+ let newCount = originalCount ;
961+ let newTotalCount = selectionRates . totalCount ;
962+
963+ if ( ! isDuplicateAnswer ) {
964+ newCount = selectedAnswer === choice ? originalCount + 1 : originalCount ;
965+ newTotalCount = selectionRates . totalCount + 1 ;
966+ }
967+
891968 const newRate = newCount / newTotalCount ;
892969 const actualPercentage = Math . round ( newRate * 100 ) ;
893970
0 commit comments