From 4066a1ca3b7f5f81a8bcd011461ff146a8ad6391 Mon Sep 17 00:00:00 2001 From: Hello-Worldismine Date: Mon, 17 Nov 2025 02:02:46 +0900 Subject: [PATCH 1/7] =?UTF-8?q?=EC=84=B8=EB=B6=80=20=EB=A1=9C=EA=B7=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/Interview/feedback_result.tsx | 115 ++++++++++++++++------- src/pages/Interview/question_loading.tsx | 45 +++++++-- src/services/interviewApi.ts | 67 ++++++++----- 3 files changed, 164 insertions(+), 63 deletions(-) diff --git a/src/pages/Interview/feedback_result.tsx b/src/pages/Interview/feedback_result.tsx index 40499df..af97f27 100644 --- a/src/pages/Interview/feedback_result.tsx +++ b/src/pages/Interview/feedback_result.tsx @@ -3,7 +3,7 @@ import { useEffect, useState } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; import InterviewLayout from '@/layouts/InterviewLayout'; -import type { IFeedbackItem, IFinalFeedbackResponse } from '@/services/interviewApi'; +import type { IFinalFeedbackResponse } from '@/services/interviewApi'; import { getFinalFeedback } from '@/services/interviewApi'; interface IQuestionState { @@ -27,13 +27,39 @@ export default function FeedbackResult() { navigate('/upload'); return; } + const fetchFeedback = async () => { try { setIsLoading(true); + console.log('๐Ÿ“Š ์ตœ์ข… ํ”ผ๋“œ๋ฐฑ ์กฐํšŒ:', sessionId); + const response = await getFinalFeedback(sessionId); - setFeedbackData(response); - const states = response.feedbacks.map((_, idx) => ({ id: idx + 1, showAnswer: false })); - setQuestionStates(states); + + console.log('โœ… ํ”ผ๋“œ๋ฐฑ ์‘๋‹ต:', response); + + // feedbackProgressStatus ํ™•์ธ + if (response.feedbackProgressStatus === 'WORKING') { + // ํ”ผ๋“œ๋ฐฑ ์ƒ์„ฑ ์ค‘ - 5์ดˆ ํ›„ ์žฌ์‹œ๋„ + setTimeout(fetchFeedback, 5000); + return; + } + + if (response.feedbackProgressStatus === 'FAILED') { + setError('ํ”ผ๋“œ๋ฐฑ ์ƒ์„ฑ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.'); + setIsLoading(false); + return; + } + + if (response.interviewSummary) { + setFeedbackData(response); + + // ์งˆ๋ฌธ ์ƒํƒœ ์ดˆ๊ธฐํ™” + const states = response.interviewSummary.questionSummaries.map((_, idx) => ({ + id: idx + 1, + showAnswer: false, + })); + setQuestionStates(states); + } } catch (err) { console.error('โŒ ํ”ผ๋“œ๋ฐฑ ์กฐํšŒ ์‹คํŒจ:', err); setError('ํ”ผ๋“œ๋ฐฑ์„ ๋ถˆ๋Ÿฌ์˜ค๋Š”๋ฐ ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.'); @@ -41,6 +67,7 @@ export default function FeedbackResult() { setIsLoading(false); } }; + void fetchFeedback(); }, [navigate, sessionId]); @@ -64,7 +91,7 @@ export default function FeedbackResult() { ); } - if (error || !feedbackData) { + if (error || !feedbackData || !feedbackData.interviewSummary) { return (
@@ -79,61 +106,69 @@ export default function FeedbackResult() { ); } - const { feedbacks, totalQuestions, timeoutCount } = feedbackData; + const { interviewSummary } = feedbackData; return (

- ์ด {totalQuestions}๋ฌธํ•ญ์— ๋Œ€ํ•œ ์ตœ์ข… ํ”ผ๋“œ๋ฐฑ + {interviewSummary.interviewTitle}์— ๋Œ€ํ•œ ์ตœ์ข… + ํ”ผ๋“œ๋ฐฑ

- {timeoutCount > 0 && ( + {interviewSummary.timeoutQuestionNumber > 0 && (

์‹œ๊ฐ„ ์ดˆ๊ณผ๋กœ ๋‹ต๋ณ€ํ•˜์ง€ ๋ชปํ•œ ์งˆ๋ฌธ{' '} - {timeoutCount}๊ฐœ + {interviewSummary.timeoutQuestionNumber}๊ฐœ

)}
- {feedbacks.map((item: IFeedbackItem, index: number) => { + {interviewSummary.questionSummaries.map((summary, index) => { const isShowingAnswer = questionStates.find((q) => q.id === index + 1)?.showAnswer || false; - const isPositive = item.feedbackType === 'positive'; - const feedbackTypeLabel = isPositive ? 'AI ํ”ผ๋“œ๋ฐฑ(๊ธ์ •)' : 'AI ํ”ผ๋“œ๋ฐฑ(๊ฐœ์„ )'; - const hasAnswer = !!item.answer && item.answer.trim().length > 0; + + // AI ํ”ผ๋“œ๋ฐฑ๊ณผ ์…€ํ”„ ํ”ผ๋“œ๋ฐฑ ์ค‘ ํ‘œ์‹œํ•  ๊ฒƒ ์„ ํƒ + const feedbackText = summary.aiFeedback || summary.selfFeedback; + const feedbackType = summary.aiFeedback ? 'AI ํ”ผ๋“œ๋ฐฑ' : summary.selfFeedback ? '์…€ํ”„ ํ”ผ๋“œ๋ฐฑ' : 'ํ”ผ๋“œ๋ฐฑ ์—†์Œ'; + + // ๋‹ต๋ณ€ ํ…์ŠคํŠธ (Q&A ํ„ด์—์„œ ANSWER๋งŒ ์ถ”์ถœ) + const answerTurns = summary.qnaTurns.filter((turn) => turn.turn === 'ANSWER'); + const hasAnswer = answerTurns.length > 0; return ( -
+
+ {/* ์นด๋“œ ํ—ค๋” */}

- {index + 1}. {item.question} + {summary.questionNumber}. {summary.rootQuestion}

-

{feedbackTypeLabel}

+

{feedbackType}

+ {/* ์นด๋“œ ๋‚ด์šฉ (์Šคํฌ๋กค ๊ฐ€๋Šฅ) */}
{isShowingAnswer ? ( hasAnswer ? (
-
-

๋‹ต๋ณ€:

-

{item.answer}

-
+ {summary.qnaTurns.map((turn, turnIndex) => ( +
+

{turn.turn === 'QUESTION' ? '์งˆ๋ฌธ:' : '๋‹ต๋ณ€:'}

+

{turn.content}

+
+ ))}
) : ( -

{item.timeout ? '์‹œ๊ฐ„ ์ดˆ๊ณผ๋กœ ๋‹ต๋ณ€ํ•˜์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค.' : '๋‹ต๋ณ€์ด ์ œ๊ณต๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.'}

+

์‹œ๊ฐ„ ์ดˆ๊ณผ๋กœ ๋‹ต๋ณ€ํ•˜์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค.

) - ) : item.feedback ? ( -

{item.feedback}

+ ) : feedbackText ? ( +

{feedbackText}

) : (

ํ”ผ๋“œ๋ฐฑ์ด ์ƒ์„ฑ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.

)}
+ {/* ๋ฒ„ํŠผ */}
); diff --git a/src/pages/Interview/question_loading.tsx b/src/pages/Interview/question_loading.tsx index b19f35c..17e5fcd 100644 --- a/src/pages/Interview/question_loading.tsx +++ b/src/pages/Interview/question_loading.tsx @@ -1,17 +1,17 @@ -๏ปฟimport { useEffect } from 'react'; +import { useEffect } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; import InterviewLayout from '@/layouts/InterviewLayout'; import type { ICreateInterviewSessionResponse } from '@/services/interviewApi'; import { createInterviewSession } from '@/services/interviewApi'; -const ANSWER_ROUTE = '/main-answer'; // ํ”„๋กœ์ ํŠธ ๋ผ์šฐํŠธ์— ๋งž๊ฒŒ ์กฐ์ • +const ANSWER_ROUTE = '/main-answer'; type TLocationState = { fileName?: string; jobTitle?: string; interviewType?: 'normal' | 'pressure'; - resumeKey?: string; // S3 key + resumeKey?: string; }; export default function QuestionLoading() { @@ -26,20 +26,29 @@ export default function QuestionLoading() { const interviewType = location.state?.interviewType ?? 'normal'; const resumeKey = location.state?.resumeKey; + console.log('๐ŸŽฌ ๋ฉด์ ‘ ์„ธ์…˜ ์ƒ์„ฑ ์‹œ์ž‘'); + console.log('- fileName:', fileName); + console.log('- jobTitle:', jobTitle); + console.log('- interviewType:', interviewType); + console.log('- resumeKey:', resumeKey); + if (!jobTitle || !resumeKey) { alert('๋ฉด์ ‘ ์ƒ์„ฑ์— ํ•„์š”ํ•œ ์ •๋ณด๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. (jobTitle/resumeKey)'); navigate(-1); return; } - // ์ŠคํŽ™์— ๋งž๊ฒŒ ์š”์ฒญ const resp: ICreateInterviewSessionResponse = await createInterviewSession({ resumeKey, jobTitle, interviewType, }); - // ์„ฑ๊ณต โ†’ ๋‹ต๋ณ€ ํŽ˜์ด์ง€๋กœ ์ด๋™ (ํ•„์š”๊ฐ’ ์ „๋‹ฌ) + console.log('โœ… ๋ฉด์ ‘ ์„ธ์…˜ ์ƒ์„ฑ ์„ฑ๊ณต:', resp); + console.log('- sessionId:', resp.sessionId); + console.log('- firstQuestion:', resp.firstQuestion); + + // ์„ฑ๊ณต โ†’ ๋‹ต๋ณ€ ํŽ˜์ด์ง€๋กœ ์ด๋™ navigate(ANSWER_ROUTE, { replace: true, state: { @@ -52,8 +61,12 @@ export default function QuestionLoading() { fromLoading: true, }, }); - } catch (e) { - console.error('์งˆ๋ฌธ ์ƒ์„ฑ ์‹คํŒจ:', e); + } catch (e: any) { + console.error('โŒ ์งˆ๋ฌธ ์ƒ์„ฑ ์‹คํŒจ:', e); + console.error('- ์—๋Ÿฌ ๋ฉ”์‹œ์ง€:', e.message); + console.error('- ์—๋Ÿฌ ์‘๋‹ต:', e.response?.data); + console.error('- ์—๋Ÿฌ ์ƒํƒœ:', e.response?.status); + alert('๋งž์ถคํ˜• ์งˆ๋ฌธ ์ƒ์„ฑ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.'); navigate(-1); } @@ -82,9 +95,21 @@ export default function QuestionLoading() {
); diff --git a/src/services/interviewApi.ts b/src/services/interviewApi.ts index bcf7cf4..5d57955 100644 --- a/src/services/interviewApi.ts +++ b/src/services/interviewApi.ts @@ -142,29 +142,54 @@ export interface ICreateInterviewSessionResponse { export const createInterviewSession = async ( data: ICreateInterviewSessionRequest, ): Promise => { - const resumeId = extractResumeId(data.resumeKey); - const mode: InterviewMode = data.interviewType === 'pressure' ? 'HARD' : 'NORMAL'; + try { + console.log('๐ŸŽฌ API ํ˜ธ์ถœ: /api/interview-sessions'); + console.log('- resumeKey:', data.resumeKey); + console.log('- jobTitle:', data.jobTitle); + console.log('- interviewType:', data.interviewType); - const payload: ICreateInterviewSessionPayload = { - mode, - jobRole: data.jobTitle, - resumeId, - }; + const resumeId = extractResumeId(data.resumeKey); + const mode: InterviewMode = data.interviewType === 'pressure' ? 'HARD' : 'NORMAL'; - const response = await apiClient.post('/api/interview-sessions', payload); - const apiResult = unwrapResult(response.data); + const payload: ICreateInterviewSessionPayload = { + mode, + jobRole: data.jobTitle, + resumeId, + }; - const firstQuestion: IQuestion = { - questionId: String(apiResult.firstQuestionId), - mainQuestion: apiResult.firstQuestionText, - subQuestion: '', - order: 1, - }; + console.log('๐Ÿ“ค ์š”์ฒญ payload:', payload); - return { - sessionId: String(apiResult.sessionId), - firstQuestion, - }; + const response = await apiClient.post('/api/interview-sessions', payload); + + console.log('๐Ÿ“ฅ ์›๋ณธ ์‘๋‹ต:', response.data); + + const apiResult = unwrapResult(response.data); + + console.log('โœ… unwrap ํ›„ ๊ฒฐ๊ณผ:', apiResult); + + const firstQuestion: IQuestion = { + questionId: String(apiResult.firstQuestionId), + mainQuestion: apiResult.firstQuestionText, + subQuestion: '', + order: 1, + }; + + const result = { + sessionId: String(apiResult.sessionId), + firstQuestion, + }; + + console.log('โœ… ์ตœ์ข… ๋ฐ˜ํ™˜๊ฐ’:', result); + + return result; + } catch (error: any) { + console.error('โŒ createInterviewSession ์—๋Ÿฌ:'); + console.error('- ์—๋Ÿฌ ๊ฐ์ฒด:', error); + console.error('- ์—๋Ÿฌ ๋ฉ”์‹œ์ง€:', error.message); + console.error('- ์‘๋‹ต ๋ฐ์ดํ„ฐ:', error.response?.data); + console.error('- ์‘๋‹ต ์ƒํƒœ:', error.response?.status); + throw error; + } }; // ===================================================== @@ -407,10 +432,10 @@ export interface IFinalFeedbackResponse { interviewSummary: IInterviewSummary | null; // WORKING์ผ ๋•Œ๋Š” null } -/** ์ตœ์ข… ํ”ผ๋“œ๋ฐฑ ์กฐํšŒ (GET /api/interview-sesisons/{sessionId}) */ +/** ์ตœ์ข… ํ”ผ๋“œ๋ฐฑ ์กฐํšŒ (GET /api/interview-sessions/{sessionId}) */ export const getFinalFeedback = async ( sessionId: string | number, ): Promise => { - const response = await apiClient.get(`/api/interview-sesisons/${sessionId}`); + const response = await apiClient.get(`/api/interview-sessions/${sessionId}`); return unwrapResult(response.data); }; From 9f47ad9b6700c68050c234f41ac71b5b851d5b07 Mon Sep 17 00:00:00 2001 From: Hello-Worldismine Date: Mon, 17 Nov 2025 02:06:25 +0900 Subject: [PATCH 2/7] =?UTF-8?q?500=EC=97=90=EB=9F=AC=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/services/api.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/services/api.ts b/src/services/api.ts index 0fae877..0db2d7f 100644 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -39,6 +39,17 @@ apiClient.interceptors.response.use( // ์„œ๋ฒ„๊ฐ€ ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ ๊ฒฝ์šฐ console.error('API Error:', error.response.data); + // ํŠน๋ณ„ ์ฒ˜๋ฆฌ: 500 ์—๋Ÿฌ์—ฌ๋„ result ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์œผ๋ฉด ์„ฑ๊ณต์œผ๋กœ ์ฒ˜๋ฆฌ + // (๋ฐฑ์—”๋“œ ๋ฒ„๊ทธ: 500 ์ฝ”๋“œ๋ฅผ ๋ณด๋‚ด์ง€๋งŒ ์ •์ƒ ๋ฐ์ดํ„ฐ๋„ ํ•จ๊ป˜ ๋ณด๋ƒ„) + if ( + error.response.status === 500 && + error.response.data && + (error.response.data.result || error.response.data.sessionId) + ) { + console.warn('โš ๏ธ 500 ์—๋Ÿฌ์ง€๋งŒ ์ •์ƒ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์–ด ์„ฑ๊ณต์œผ๋กœ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.'); + return error.response; + } + // 401 Unauthorized if (error.response.status === 401) { // ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ ๋“ฑ From 6a7922708cd8adb0fc04c317e37528583eca7d50 Mon Sep 17 00:00:00 2001 From: Hello-Worldismine Date: Mon, 17 Nov 2025 02:12:53 +0900 Subject: [PATCH 3/7] =?UTF-8?q?500=EC=97=90=EB=9F=AC=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/services/api.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/services/api.ts b/src/services/api.ts index 0db2d7f..d78c34c 100644 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -34,22 +34,22 @@ apiClient.interceptors.response.use( return response; }, (error) => { + // ํŠน๋ณ„ ์ฒ˜๋ฆฌ: 500 ์—๋Ÿฌ์—ฌ๋„ result ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์œผ๋ฉด ์„ฑ๊ณต์œผ๋กœ ์ฒ˜๋ฆฌ + // (๋ฐฑ์—”๋“œ ๋ฒ„๊ทธ: 500 ์ฝ”๋“œ๋ฅผ ๋ณด๋‚ด์ง€๋งŒ ์ •์ƒ ๋ฐ์ดํ„ฐ๋„ ํ•จ๊ป˜ ๋ณด๋ƒ„) + if ( + error.response?.status === 500 && + error.response?.data && + (error.response.data.result || error.response.data.sessionId || error.response.data.interviewSummary) + ) { + // ์กฐ์šฉํžˆ ์„ฑ๊ณต์œผ๋กœ ์ฒ˜๋ฆฌ (์—๋Ÿฌ ๋กœ๊ทธ ์—†์ด) + return error.response; + } + // ์—๋Ÿฌ ์ฒ˜๋ฆฌ if (error.response) { // ์„œ๋ฒ„๊ฐ€ ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ ๊ฒฝ์šฐ console.error('API Error:', error.response.data); - // ํŠน๋ณ„ ์ฒ˜๋ฆฌ: 500 ์—๋Ÿฌ์—ฌ๋„ result ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์œผ๋ฉด ์„ฑ๊ณต์œผ๋กœ ์ฒ˜๋ฆฌ - // (๋ฐฑ์—”๋“œ ๋ฒ„๊ทธ: 500 ์ฝ”๋“œ๋ฅผ ๋ณด๋‚ด์ง€๋งŒ ์ •์ƒ ๋ฐ์ดํ„ฐ๋„ ํ•จ๊ป˜ ๋ณด๋ƒ„) - if ( - error.response.status === 500 && - error.response.data && - (error.response.data.result || error.response.data.sessionId) - ) { - console.warn('โš ๏ธ 500 ์—๋Ÿฌ์ง€๋งŒ ์ •์ƒ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์–ด ์„ฑ๊ณต์œผ๋กœ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.'); - return error.response; - } - // 401 Unauthorized if (error.response.status === 401) { // ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ ๋“ฑ From 54d75718628655191bd5ef5a6dfba023d362c432 Mon Sep 17 00:00:00 2001 From: Hello-Worldismine Date: Mon, 17 Nov 2025 02:15:00 +0900 Subject: [PATCH 4/7] =?UTF-8?q?500=EC=97=90=EB=9F=AC=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/Interview/question_loading.tsx | 49 +++++++++--------------- 1 file changed, 19 insertions(+), 30 deletions(-) diff --git a/src/pages/Interview/question_loading.tsx b/src/pages/Interview/question_loading.tsx index 17e5fcd..fbc59f5 100644 --- a/src/pages/Interview/question_loading.tsx +++ b/src/pages/Interview/question_loading.tsx @@ -1,4 +1,5 @@ -import { useEffect } from 'react'; +// src/pages/Interview/question_loading.tsx +import { useEffect, useRef } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; import InterviewLayout from '@/layouts/InterviewLayout'; @@ -11,14 +12,21 @@ type TLocationState = { fileName?: string; jobTitle?: string; interviewType?: 'normal' | 'pressure'; - resumeKey?: string; + resumeKey?: string; // S3 key }; export default function QuestionLoading() { const navigate = useNavigate(); const location = useLocation() as { state?: TLocationState }; + // โœ… ๋ถ€ํŠธ์ŠคํŠธ๋žฉ์ด ์ค‘๋ณต ์‹คํ–‰๋˜๋Š” ๊ฒƒ์„ ๋ง‰๊ธฐ ์œ„ํ•œ ref ๊ฐ€๋“œ + const bootstrappedRef = useRef(false); + useEffect(() => { + // ์ด๋ฏธ ์‹คํ–‰๋œ ์  ์žˆ์œผ๋ฉด ๋‹ค์‹œ ์‹คํ–‰ํ•˜์ง€ ์•Š์Œ + if (bootstrappedRef.current) return; + bootstrappedRef.current = true; + const bootstrap = async () => { try { const fileName = location.state?.fileName ?? '์ž์†Œ์„œ'; @@ -26,29 +34,20 @@ export default function QuestionLoading() { const interviewType = location.state?.interviewType ?? 'normal'; const resumeKey = location.state?.resumeKey; - console.log('๐ŸŽฌ ๋ฉด์ ‘ ์„ธ์…˜ ์ƒ์„ฑ ์‹œ์ž‘'); - console.log('- fileName:', fileName); - console.log('- jobTitle:', jobTitle); - console.log('- interviewType:', interviewType); - console.log('- resumeKey:', resumeKey); - if (!jobTitle || !resumeKey) { alert('๋ฉด์ ‘ ์ƒ์„ฑ์— ํ•„์š”ํ•œ ์ •๋ณด๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. (jobTitle/resumeKey)'); navigate(-1); return; } + // ์ŠคํŽ™์— ๋งž๊ฒŒ ์„ธ์…˜ ์ƒ์„ฑ ์š”์ฒญ const resp: ICreateInterviewSessionResponse = await createInterviewSession({ resumeKey, jobTitle, interviewType, }); - console.log('โœ… ๋ฉด์ ‘ ์„ธ์…˜ ์ƒ์„ฑ ์„ฑ๊ณต:', resp); - console.log('- sessionId:', resp.sessionId); - console.log('- firstQuestion:', resp.firstQuestion); - - // ์„ฑ๊ณต โ†’ ๋‹ต๋ณ€ ํŽ˜์ด์ง€๋กœ ์ด๋™ + // โœ… ์„ฑ๊ณต โ†’ ๋‹ต๋ณ€ ํŽ˜์ด์ง€๋กœ ์ด๋™ navigate(ANSWER_ROUTE, { replace: true, state: { @@ -61,19 +60,16 @@ export default function QuestionLoading() { fromLoading: true, }, }); - } catch (e: any) { - console.error('โŒ ์งˆ๋ฌธ ์ƒ์„ฑ ์‹คํŒจ:', e); - console.error('- ์—๋Ÿฌ ๋ฉ”์‹œ์ง€:', e.message); - console.error('- ์—๋Ÿฌ ์‘๋‹ต:', e.response?.data); - console.error('- ์—๋Ÿฌ ์ƒํƒœ:', e.response?.status); - + } catch (e) { + console.error('์งˆ๋ฌธ ์ƒ์„ฑ ์‹คํŒจ:', e); alert('๋งž์ถคํ˜• ์งˆ๋ฌธ ์ƒ์„ฑ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.'); navigate(-1); } }; void bootstrap(); - }, [location.state, navigate]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [navigate]); // location.state๋Š” ์ดˆ๊ธฐ ํ•œ ๋ฒˆ๋งŒ ์“ฐ๊ณ , ์žฌ์‹คํ–‰์„ ๋ง‰๊ธฐ ์œ„ํ•ด deps์—์„œ ์ œ์™ธ return ( @@ -95,17 +91,10 @@ export default function QuestionLoading() {
); diff --git a/src/hooks/useGetInterviewSummary.ts b/src/hooks/useGetInterviewSummary.ts new file mode 100644 index 0000000..da0bb4d --- /dev/null +++ b/src/hooks/useGetInterviewSummary.ts @@ -0,0 +1,9 @@ +import { getInterviewSummary } from '@/apis/myPage'; +import { useCoreQuery } from '@/hooks/customQuery'; + +export default function useGetInterviewSummary(interviewId: number) { + const { data, isLoading, error } = useCoreQuery(['getInterviewSummary', interviewId], () => getInterviewSummary(interviewId), { + enabled: !!interviewId, + }); + return { data, isLoading, error }; +} diff --git a/src/hooks/useGetQuestionAnswers.ts b/src/hooks/useGetQuestionAnswers.ts new file mode 100644 index 0000000..9c051e0 --- /dev/null +++ b/src/hooks/useGetQuestionAnswers.ts @@ -0,0 +1,9 @@ +import { getQuestionAnswers } from '@/apis/myPage'; +import { useCoreQuery } from '@/hooks/customQuery'; + +export default function useGetQuestionAnswers(questionId: number | null) { + const { data, isLoading, error } = useCoreQuery(['getQuestionAnswers', questionId], () => getQuestionAnswers(questionId!), { + enabled: !!questionId, + }); + return { data, isLoading, error }; +} diff --git a/src/hooks/useGetQuestionFeedback.ts b/src/hooks/useGetQuestionFeedback.ts new file mode 100644 index 0000000..cc7c243 --- /dev/null +++ b/src/hooks/useGetQuestionFeedback.ts @@ -0,0 +1,9 @@ +import { getQuestionFeedback } from '@/apis/myPage'; +import { useCoreQuery } from '@/hooks/customQuery'; + +export default function useGetQuestionFeedback(questionId: number | null) { + const { data, isLoading, error } = useCoreQuery(['getQuestionFeedback', questionId], () => getQuestionFeedback(questionId!), { + enabled: !!questionId, + }); + return { data, isLoading, error }; +} diff --git a/src/hooks/useGetRandomQuestions.ts b/src/hooks/useGetRandomQuestions.ts new file mode 100644 index 0000000..84e90e1 --- /dev/null +++ b/src/hooks/useGetRandomQuestions.ts @@ -0,0 +1,9 @@ +import { getRandomQuestions } from '@/apis/myPage'; +import { useCoreQuery } from '@/hooks/customQuery'; + +export default function useGetRandomQuestions(questionId: number | null) { + const { data, isLoading, error } = useCoreQuery(['getRandomQuestions', questionId], () => getRandomQuestions(questionId!), { + enabled: !!questionId, + }); + return { data, isLoading, error }; +} diff --git a/src/pages/myInterviews.tsx b/src/pages/myInterviews.tsx index 32d2106..bf16c3d 100644 --- a/src/pages/myInterviews.tsx +++ b/src/pages/myInterviews.tsx @@ -1,8 +1,275 @@ -export default function MyInterview() { +import { useEffect, useRef, useState } from 'react'; +import { useParams } from 'react-router-dom'; + +import useGetInterviewSummary from '@/hooks/useGetInterviewSummary'; +import useGetQuestionAnswers from '@/hooks/useGetQuestionAnswers'; +import useGetQuestionFeedback from '@/hooks/useGetQuestionFeedback'; +import useGetRandomQuestions from '@/hooks/useGetRandomQuestions'; + +import ClockFrog from '@/assets/clockFrog.svg?react'; + +type TabType = 'answer' | 'feedback' | 'random'; + +export default function MyInterviews() { + const { id } = useParams<{ id: string }>(); + const interviewId = id ? parseInt(id) : 0; + + const [selectedQuestionId, setSelectedQuestionId] = useState(null); + const [activeTab, setActiveTab] = useState('answer'); + const [playingAudio, setPlayingAudio] = useState(null); + const audioRef = useRef(null); + + const { data: summaryData, isLoading: summaryLoading } = useGetInterviewSummary(interviewId); + const { data: answersData, isLoading: answersLoading } = useGetQuestionAnswers(activeTab === 'answer' ? selectedQuestionId : null); + const { data: feedbackData, isLoading: feedbackLoading } = useGetQuestionFeedback(activeTab === 'feedback' ? selectedQuestionId : null); + const { data: randomData, isLoading: randomLoading } = useGetRandomQuestions(activeTab === 'random' ? selectedQuestionId : null); + + // ์ฒซ ๋ฒˆ์งธ ์งˆ๋ฌธ ์ž๋™ ์„ ํƒ + useEffect(() => { + if (summaryData?.result.questionCards && summaryData.result.questionCards.length > 0 && !selectedQuestionId) { + setSelectedQuestionId(summaryData.result.questionCards[0].questionId); + } + }, [summaryData, selectedQuestionId]); + + const handleAudioPlay = (url: string) => { + if (playingAudio === url) { + audioRef.current?.pause(); + setPlayingAudio(null); + } else { + if (audioRef.current) { + audioRef.current.pause(); + } + audioRef.current = new Audio(url); + audioRef.current.play(); + setPlayingAudio(url); + + audioRef.current.onended = () => { + setPlayingAudio(null); + }; + } + }; + + if (summaryLoading) { + return ( +
+
+ +

๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘...

+
+
+ ); + } + + if (!summaryData?.result) { + return ( +
+

๋ฉด์ ‘ ๋ฐ์ดํ„ฐ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.

+
+ ); + } + + const { title, timedOutCount, questionCards } = summaryData.result; + const isLoading = answersLoading || feedbackLoading || randomLoading; + return ( -
-

interview

-

hi

+
+
+ {/* ํ—ค๋” */} +
+

{title}

+ {timedOutCount > 0 && ( +

+ ์‹œ๊ฐ„ ์ดˆ๊ณผ๋กœ ๋‹ต๋ณ€ํ•˜์ง€ ๋ชปํ•œ ์งˆ๋ฌธ{' '} + {timedOutCount}๊ฐœ +

+ )} +
+ + {/* ์งˆ๋ฌธ ์นด๋“œ ๋ฆฌ์ŠคํŠธ */} +
+

๋ฉด์ ‘ ์งˆ๋ฌธ

+
+ {questionCards.map((card) => ( + + ))} +
+
+ + {/* ํƒญ ๋ฉ”๋‰ด */} +
+
+ + + +
+ + {/* ํƒญ ์ฝ˜ํ…์ธ  */} +
+ {isLoading ? ( +
+ +
+ ) : ( + <> + {/* ๋‹ต๋ณ€ ํ™•์ธ ํƒญ */} + {activeTab === 'answer' && answersData?.result && ( +
+ {answersData.result.map((item) => ( +
+
+ + ์งˆ๋ฌธ {item.order} + +

{item.question}

+
+ {item.answerText ? ( + <> +

{item.answerText}

+ + + ) : ( +

๋‹ต๋ณ€์ด ์—†์Šต๋‹ˆ๋‹ค.

+ )} +
+ ))} +
+ )} + + {/* ํ”ผ๋“œ๋ฐฑ ํ™•์ธ ํƒญ */} + {activeTab === 'feedback' && feedbackData?.result && ( +
+ {/* AI ํ”ผ๋“œ๋ฐฑ */} + {feedbackData.result.aiFeedback && ( +
+

๐Ÿค– AI ํ”ผ๋“œ๋ฐฑ

+

{feedbackData.result.aiFeedback}

+
+ )} + + {/* ์…€ํ”„ ํ”ผ๋“œ๋ฐฑ */} + {feedbackData.result.selfFeedback && ( +
+

โœ๏ธ ์…€ํ”„ ํ”ผ๋“œ๋ฐฑ

+

{feedbackData.result.selfFeedback}

+
+ )} + + {/* ๋™๋ฃŒ ํ”ผ๋“œ๋ฐฑ */} + {feedbackData.result.peerItems && feedbackData.result.peerItems.length > 0 && ( +
+

๐Ÿ‘ฅ ๋™๋ฃŒ ํ”ผ๋“œ๋ฐฑ

+
    + {feedbackData.result.peerItems.map((item, index) => ( +
  • + โ€ข + {item} +
  • + ))} +
+
+ )} + + {!feedbackData.result.aiFeedback && !feedbackData.result.selfFeedback && feedbackData.result.peerItems.length === 0 && ( +

ํ”ผ๋“œ๋ฐฑ์ด ์—†์Šต๋‹ˆ๋‹ค.

+ )} +
+ )} + + {/* ๋žœ๋ค ์งˆ๋ฌธ ํƒญ */} + {activeTab === 'random' && randomData?.result && ( +
+ {randomData.result.length > 0 ? ( + randomData.result.map((item, index) => ( +
+
+ + ๋žœ๋ค ์งˆ๋ฌธ {index + 1} + +

{item.question}

+
+ +
+

๋‹ต๋ณ€:

+

{item.answerText}

+
+ +
+ {item.aiFeedback && ( +
+

AI ํ”ผ๋“œ๋ฐฑ

+

{item.aiFeedback}

+
+ )} + {item.selfFeedback && ( +
+

์…€ํ”„ ํ”ผ๋“œ๋ฐฑ

+

{item.selfFeedback}

+
+ )} +
+ + +
+ )) + ) : ( +

๋žœ๋ค ์งˆ๋ฌธ์ด ์—†์Šต๋‹ˆ๋‹ค.

+ )} +
+ )} + + )} +
+
+
); } diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 43f0d0e..68cbb3a 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -32,7 +32,7 @@ export const router = createBrowserRouter([ { path: 'community/write', element: }, { path: 'community/edit/:id', element: }, { path: 'community/detail/:id', element: }, - { path: 'myInterview/:id', element: }, + { path: 'myInterviews/:id', element: }, { path: 'myEvaluate/:id', element: }, { path: 'upload', element: }, // โ† myInterview์—์„œ upload๋กœ ๋ณ€๊ฒฝ { path: 'upload-done', element: }, diff --git a/src/services/randomQuestionApi.ts b/src/services/randomQuestionApi.ts index 5ddff10..587a0b4 100644 --- a/src/services/randomQuestionApi.ts +++ b/src/services/randomQuestionApi.ts @@ -1,91 +1,164 @@ import apiClient from './api'; +// result๋งŒ ๋ฝ‘์•„์ฃผ๋Š” ํ—ฌํผ +const unwrapResult = (data: any): T => { + if (data && typeof data === 'object' && 'result' in data) { + return data.result as T; + } + return data as T; +}; + // ==================== ํƒ€์ž… ์ •์˜ ==================== +// 1) SSE ์•Œ๋ฆผ payload (ํŒ์—… ์•Œ๋ฆผ์šฉ) โ€” ํŒ์—… ์•Œ๋ฆผ์„ ์œ„ํ•œ SSE ๊ตฌ๋…์šฉ API +export interface IRandomNotificationPayload { + jobName: string; // ์ง๋ฌด ์ด๋ฆ„ + interviewName: string; // ์ธํ„ฐ๋ทฐ ์ œ๋ชฉ + questionNumber: number; // ํ•ด๋‹น ์ธํ„ฐ๋ทฐ์˜ ๋ช‡ ๋ฒˆ์งธ ์งˆ๋ฌธ์ธ์ง€ + peerFeedbackId: number; // = peerAnswerId (๋žœ๋ค ์งˆ๋ฌธ ์กฐํšŒ์— ์‚ฌ์šฉ) +} + +// 2) ๋žœ๋ค ํŒ์—… ์งˆ๋ฌธ ์กฐํšŒ ์‘๋‹ต โ€” ๋žœ๋ค ํŒ์—… ์งˆ๋ฌธ ์กฐํšŒ API +export interface IRandomQuestionContext { + questionId: number; + questionText: string; + presignedRecordingGetUrl: string; + sttText: string; +} + export interface IRandomQuestion { - questionId: string; - question: string; - peerAnswerId: string; + question: { + questionId: number; + questionText: string; + }; + context: IRandomQuestionContext; +} + +// 3) presign ์‘๋‹ต โ€” ๋žœ๋ค ํŒ์—… ์งˆ๋ฌธ - ๋…น์Œ ์—…๋กœ๋“œ์šฉ ํ”„๋ฆฌ์‚ฌ์ธ URL ๋ฐœ๊ธ‰ +export interface IPresignUrlResponse { + uploadUrl: string; // S3 PUT presigned URL + key: string; // ์—…๋กœ๋“œ๋  S3 ์˜ค๋ธŒ์ ํŠธ ๊ฒฝ๋กœ + requiredHeaders: Record; // PUT ์‹œ ํ•จ๊ป˜ ๋ณด๋‚ด์•ผ ํ•  ํ—ค๋”๋“ค } +// recordingKey ์š”์ฒญ์šฉ ํƒ€์ž…์€ ๋” ์ด์ƒ ์‚ฌ์šฉ ์•ˆ ํ•˜์ง€๋งŒ, ๋‚จ๊ฒจ๋‘  (ํ˜ธํ™˜์šฉ) export interface IRandomQuestionRecordingRequest { recordingKey: string; } +// 4) ๋…น์Œ ์ €์žฅ ์‘๋‹ต โ€” ๋žœ๋ค ์งˆ๋ฌธ์— ๋Œ€ํ•œ recording ์ €์žฅ ๋ฐ ํ”ผ๋“œ๋ฐฑ ์ƒ์„ฑ API (๋น„๋™๊ธฐ) export interface IFeedbackRecordingResponse { - recordingId: string; - status: 'processing' | 'completed' | 'failed'; + recordingId: number; + status: 'UPLOADED'; // ๋น„๋™๊ธฐ ์ž‘์—…์ด ํ์— ์˜ฌ๋ผ๊ฐ”๋‹ค๋Š” ๋œป } -export interface IFeedbackResultResponse { - status: 'processing' | 'completed' | 'failed'; - feedback?: string; +// 5) ํ”ผ๋“œ๋ฐฑ ์กฐํšŒ ์‘๋‹ต โ€” ๋žœ๋ค ์งˆ๋ฌธ์— ๋Œ€ํ•œ ํ”ผ๋“œ๋ฐฑ ํ™•์ธ API (polling) +export type FeedbackProgressStatus = 'WORKING' | 'READY' | 'FAILED'; + +export interface IFeedbackResult { + questionId: number; + questionText: string; + aiFeedback: string; + selfFeedback: string; + presignedRecordingGetUrl: string; + sttText: string; } -export interface IPresignUrlResponse { - presignedUrl: string; - fileKey: string; +export interface IFeedbackResultResponse { + progressStatus: FeedbackProgressStatus; + result: IFeedbackResult | null; // WORKING/FAILED์ผ ๋•Œ๋Š” null } // ==================== API ํ•จ์ˆ˜๋“ค ==================== /** * 1. ๋žœ๋ค ํŒ์—… ์งˆ๋ฌธ ์กฐํšŒ + * GET /api/random-questions/peer/{peerAnswerId} + * peerAnswerId = SSE ์•Œ๋ฆผ์˜ peerFeedbackId */ -export const getRandomQuestion = async (peerAnswerId: string): Promise => { +export const getRandomQuestion = async ( + peerAnswerId: number | string, +): Promise => { const response = await apiClient.get(`/api/random-questions/peer/${peerAnswerId}`); - return response.data; + return unwrapResult(response.data); }; /** * 2. ๋žœ๋ค ํŒ์—… ์งˆ๋ฌธ - ๋…น์Œ ์—…๋กœ๋“œ์šฉ ํ”„๋ฆฌ์‚ฌ์ธ URL ๋ฐœ๊ธ‰ + * POST /api/presign/recording/feedback-question + * Body: { questionId: Long, contentType: String } */ -export const getFeedbackRecordingPresignUrl = async (fileName: string): Promise => { - const response = await apiClient.post('/api/presign/recording/feedback-question', { fileName }); - return response.data; +export const getFeedbackRecordingPresignUrl = async ( + questionId: number, + contentType: string, +): Promise => { + const response = await apiClient.post('/api/presign/recording/feedback-question', { + questionId, + contentType, + }); + return unwrapResult(response.data); }; /** - * 3. S3์— ํŒŒ์ผ ์—…๋กœ๋“œ + * 3. S3์— ํŒŒ์ผ ์—…๋กœ๋“œ (ํ”„๋ฆฌ์‚ฌ์ธ URL ์‚ฌ์šฉ) */ -export const uploadToS3 = async (presignedUrl: string, file: Blob): Promise => { +export const uploadToS3 = async ( + presignedUrl: string, + file: Blob, + extraHeaders: Record = {}, +): Promise => { await fetch(presignedUrl, { method: 'PUT', body: file, headers: { 'Content-Type': file.type || 'audio/webm', + ...extraHeaders, }, }); }; /** * 4. ๋žœ๋ค ์งˆ๋ฌธ์— ๋Œ€ํ•œ recording ์ €์žฅ ๋ฐ ํ”ผ๋“œ๋ฐฑ ์ƒ์„ฑ (๋น„๋™๊ธฐ) + * POST /api/random-questions/peer/questions/{questionId} + * Body ์—†์Œ, path param์œผ๋กœ questionId๋งŒ ๋„˜๊น€ */ -export const saveFeedbackRecording = async (questionId: string, data: IRandomQuestionRecordingRequest): Promise => { - const response = await apiClient.get(`/api/random-questions/peer/questions/${questionId}`, { - params: data, - }); - return response.data; +export const saveFeedbackRecording = async ( + questionId: number, +): Promise => { + const response = await apiClient.post( + `/api/random-questions/peer/questions/${questionId}`, + ); + return unwrapResult(response.data); }; /** * 5. ๋žœ๋ค ์งˆ๋ฌธ์— ๋Œ€ํ•œ ํ”ผ๋“œ๋ฐฑ ํ™•์ธ (polling) + * GET /api/random-questions/peer/recordings/{recordingId}/feedbacks */ -export const getFeedbackResult = async (recordingId: string): Promise => { - const response = await apiClient.get(`/api/random-questions/peer/recordings/${recordingId}/feedbacks`); - return response.data; +export const getFeedbackResult = async ( + recordingId: number, +): Promise => { + const response = await apiClient.get( + `/api/random-questions/peer/recordings/${recordingId}/feedbacks`, + ); + return unwrapResult(response.data); }; /** * 6. Polling ํ—ฌํผ ํ•จ์ˆ˜ + * progressStatus ๊ฐ€ READY / FAILED ๊ฐ€ ๋  ๋•Œ๊นŒ์ง€ ์กฐํšŒ */ -export const pollFeedbackResult = async (recordingId: string, maxAttempts: number = 60, interval: number = 5000): Promise => { +export const pollFeedbackResult = async ( + recordingId: number, + maxAttempts: number = 60, + interval: number = 5000, +): Promise => { let attempts = 0; while (attempts < maxAttempts) { const result = await getFeedbackResult(recordingId); - if (result.status === 'completed' || result.status === 'failed') { + if (result.progressStatus === 'READY' || result.progressStatus === 'FAILED') { return result; } @@ -98,9 +171,14 @@ export const pollFeedbackResult = async (recordingId: string, maxAttempts: numbe /** * 7. SSE ๊ตฌ๋… (Server-Sent Events) + * GET /api/subscribe */ -export const subscribeToNotifications = (onMessage: (event: MessageEvent) => void, onError?: (error: Event) => void): EventSource => { - const eventSource = new EventSource(`${apiClient.defaults.baseURL}/api/subscribe`); +export const subscribeToNotifications = ( + onMessage: (event: MessageEvent) => void, + onError?: (error: Event) => void, +): EventSource => { + const baseURL = apiClient.defaults.baseURL ?? ''; + const eventSource = new EventSource(`${baseURL}/api/subscribe`); eventSource.onmessage = onMessage; @@ -115,32 +193,41 @@ export const subscribeToNotifications = (onMessage: (event: MessageEvent) => voi /** * ๋žœ๋ค ์งˆ๋ฌธ ๋…น์Œ ์—…๋กœ๋“œ ๋ฐ ํ”ผ๋“œ๋ฐฑ ๋ฐ›๊ธฐ ์ „์ฒด ํ”Œ๋กœ์šฐ + * + * 1) ํ”„๋ฆฌ์‚ฌ์ธ URL ๋ฐœ๊ธ‰ + * 2) S3 ์—…๋กœ๋“œ + * 3) recording ์ €์žฅ (๋น„๋™๊ธฐ ํ์— ์˜ฌ๋ฆฌ๊ธฐ) + * 4) ํ”ผ๋“œ๋ฐฑ READY ๋  ๋•Œ๊นŒ์ง€ polling + * 5) IFeedbackResult ๋ฆฌํ„ด (aiFeedback, selfFeedback ๋“ฑ ํฌํ•จ) */ -export const uploadFeedbackRecordingAndGetResult = async (questionId: string, audioBlob: Blob): Promise => { +export const uploadFeedbackRecordingAndGetResult = async ( + questionId: number, + audioBlob: Blob, +): Promise => { // 1. ํ”„๋ฆฌ์‚ฌ์ธ URL ๋ฐ›๊ธฐ - const fileName = `feedback-${questionId}-${Date.now()}.webm`; - const { presignedUrl, fileKey } = await getFeedbackRecordingPresignUrl(fileName); + const contentType = audioBlob.type || 'audio/webm'; + const { uploadUrl, requiredHeaders } = await getFeedbackRecordingPresignUrl( + questionId, + contentType, + ); // 2. S3์— ์—…๋กœ๋“œ - await uploadToS3(presignedUrl, audioBlob); + await uploadToS3(uploadUrl, audioBlob, requiredHeaders); - // 3. ํ”ผ๋“œ๋ฐฑ ์ƒ์„ฑ ์‹œ์ž‘ - const { recordingId, status } = await saveFeedbackRecording(questionId, { - recordingKey: fileKey, - }); + // 3. ๋…น์Œ ์ €์žฅ & ๋น„๋™๊ธฐ ํ”ผ๋“œ๋ฐฑ ์ƒ์„ฑ ํŠธ๋ฆฌ๊ฑฐ + const { recordingId, status } = await saveFeedbackRecording(questionId); - // 4. ์ฆ‰์‹œ ์™„๋ฃŒ๋œ ๊ฒฝ์šฐ - if (status === 'completed') { - const result = await getFeedbackResult(recordingId); - return result.feedback || 'ํ”ผ๋“œ๋ฐฑ์„ ๋ฐ›์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค.'; + if (status !== 'UPLOADED') { + throw new Error(`์˜ˆ์ƒ์น˜ ๋ชปํ•œ recording ์ƒํƒœ์ž…๋‹ˆ๋‹ค: ${status}`); } - // 5. ์ฒ˜๋ฆฌ ์ค‘์ด๋ฉด polling + // 4. ํ”ผ๋“œ๋ฐฑ ์ƒ์„ฑ ์ƒํƒœ polling const result = await pollFeedbackResult(recordingId); - if (result.status === 'failed') { + if (result.progressStatus === 'FAILED' || !result.result) { throw new Error('ํ”ผ๋“œ๋ฐฑ ์ƒ์„ฑ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.'); } - return result.feedback || 'ํ”ผ๋“œ๋ฐฑ์„ ๋ฐ›์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค.'; + // 5. ์ตœ์ข… ํ”ผ๋“œ๋ฐฑ ๊ฒฐ๊ณผ ๋ฆฌํ„ด + return result.result; }; diff --git a/src/types/myPage.ts b/src/types/myPage.ts index ab9d01c..56379cc 100644 --- a/src/types/myPage.ts +++ b/src/types/myPage.ts @@ -96,3 +96,73 @@ export const GROWTH_TAG_LABELS: Record = { export const getExperienceHashTags = (tags?: TExperienceTags[]) => (tags ?? []).map((tag) => `# ${EXPERIENCE_TAG_LABELS[tag]}`); export const getGrowthHashTags = (tags?: TGrowthTags[]) => (tags ?? []).map((tag) => `# ${GROWTH_TAG_LABELS[tag]}`); + +// ๋‚˜์˜ ๋ฉด์ ‘ ์ƒ์„ธ - ์ „์ฒด ์š”์•ฝ API +export type TInterviewSummaryRequest = { + interviewId: number; +}; + +export type TQuestionCard = { + order: number; + questionId: number; +}; + +export type TAnswerCheckItem = { + order: number; + questionId: number; + question: string; + answerText: string | null; + recordUrl: string; +}; + +export type TInterviewSummaryResponse = { + errorCode: null | string; + message: string; + result: { + title: string; + timedOutCount: number; + questionCards: TQuestionCard[]; + firstQuestionThread: TAnswerCheckItem[]; + }; +}; + +// ๋‚˜์˜ ๋ฉด์ ‘ ์ƒ์„ธ - ์งˆ๋ฌธ๋ณ„ ๋‹ต๋ณ€ ํ™•์ธ API +export type TQuestionAnswersRequest = { + questionId: number; +}; + +export type TQuestionAnswersResponse = { + errorCode: null | string; + message: string; + result: TAnswerCheckItem[]; +}; + +// ๋‚˜์˜ ๋ฉด์ ‘ ์ƒ์„ธ - ์งˆ๋ฌธ๋ณ„ ํ”ผ๋“œ๋ฐฑ ์กฐํšŒ API +export type TQuestionFeedbackRequest = { + questionId: number; +}; + +export type TQuestionFeedbackResponse = { + errorCode: null | string; + message: string; + result: { + aiFeedback: string; + selfFeedback: string; + peerItems: string[]; + }; +}; + +// ๋‚˜์˜ ๋ฉด์ ‘ ์ƒ์„ธ - ์งˆ๋ฌธ๋ณ„ ๋žœ๋ค ์งˆ๋ฌธ ๋‹ต๋ณ€ ํ™•์ธ API +export type TRandomQuestionItem = { + question: string; + aiFeedback: string; + selfFeedback: string; + answerText: string; + recordingUrl: string; +}; + +export type TRandomQuestionsResponse = { + errorCode: null | string; + message: string; + result: TRandomQuestionItem[]; +}; From 9930eb8e2d665a0c83593845bd8abca348915978 Mon Sep 17 00:00:00 2001 From: Hello-Worldismine Date: Mon, 17 Nov 2025 03:03:00 +0900 Subject: [PATCH 6/7] =?UTF-8?q?api=EC=97=B0=EB=8F=99,=20=EB=A7=88=EC=9D=B4?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=83=81=EC=84=B8=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/myInterviews.tsx | 114 ++++++++++++++++++------------------- 1 file changed, 57 insertions(+), 57 deletions(-) diff --git a/src/pages/myInterviews.tsx b/src/pages/myInterviews.tsx index bf16c3d..37c401a 100644 --- a/src/pages/myInterviews.tsx +++ b/src/pages/myInterviews.tsx @@ -51,10 +51,10 @@ export default function MyInterviews() { if (summaryLoading) { return ( -
-
- -

๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘...

+
+
+ +

๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘...

); @@ -62,8 +62,8 @@ export default function MyInterviews() { if (!summaryData?.result) { return ( -
-

๋ฉด์ ‘ ๋ฐ์ดํ„ฐ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.

+
+

๋ฉด์ ‘ ๋ฐ์ดํ„ฐ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.

); } @@ -72,23 +72,23 @@ export default function MyInterviews() { const isLoading = answersLoading || feedbackLoading || randomLoading; return ( -
-
+
+
{/* ํ—ค๋” */} -
-

{title}

+
+

{title}

{timedOutCount > 0 && ( -

+

์‹œ๊ฐ„ ์ดˆ๊ณผ๋กœ ๋‹ต๋ณ€ํ•˜์ง€ ๋ชปํ•œ ์งˆ๋ฌธ{' '} - {timedOutCount}๊ฐœ + {timedOutCount}๊ฐœ

)}
{/* ์งˆ๋ฌธ ์นด๋“œ ๋ฆฌ์ŠคํŠธ */} -
-

๋ฉด์ ‘ ์งˆ๋ฌธ

-
+
+

๋ฉด์ ‘ ์งˆ๋ฌธ

+
{questionCards.map((card) => ( ))}
{/* ํƒญ ๋ฉ”๋‰ด */} -
-
+
+
{/* ํƒญ ์ฝ˜ํ…์ธ  */} -
+
{isLoading ? ( -
- +
+
) : ( <> {/* ๋‹ต๋ณ€ ํ™•์ธ ํƒญ */} {activeTab === 'answer' && answersData?.result && ( -
+
{answersData.result.map((item) => ( -
-
- +
+
+ ์งˆ๋ฌธ {item.order} -

{item.question}

+

{item.question}

{item.answerText ? ( <> -

{item.answerText}

+

{item.answerText}

) : ( -

๋‹ต๋ณ€์ด ์—†์Šต๋‹ˆ๋‹ค.

+

๋‹ต๋ณ€์ด ์—†์Šต๋‹ˆ๋‹ค.

)}
))} @@ -177,31 +177,31 @@ export default function MyInterviews() { {/* ํ”ผ๋“œ๋ฐฑ ํ™•์ธ ํƒญ */} {activeTab === 'feedback' && feedbackData?.result && ( -
+
{/* AI ํ”ผ๋“œ๋ฐฑ */} {feedbackData.result.aiFeedback && ( -
-

๐Ÿค– AI ํ”ผ๋“œ๋ฐฑ

-

{feedbackData.result.aiFeedback}

+
+

๐Ÿค– AI ํ”ผ๋“œ๋ฐฑ

+

{feedbackData.result.aiFeedback}

)} {/* ์…€ํ”„ ํ”ผ๋“œ๋ฐฑ */} {feedbackData.result.selfFeedback && ( -
-

โœ๏ธ ์…€ํ”„ ํ”ผ๋“œ๋ฐฑ

-

{feedbackData.result.selfFeedback}

+
+

โœ๏ธ ์…€ํ”„ ํ”ผ๋“œ๋ฐฑ

+

{feedbackData.result.selfFeedback}

)} {/* ๋™๋ฃŒ ํ”ผ๋“œ๋ฐฑ */} {feedbackData.result.peerItems && feedbackData.result.peerItems.length > 0 && ( -
-

๐Ÿ‘ฅ ๋™๋ฃŒ ํ”ผ๋“œ๋ฐฑ

-
    +
    +

    ๐Ÿ‘ฅ ๋™๋ฃŒ ํ”ผ๋“œ๋ฐฑ

    +
      {feedbackData.result.peerItems.map((item, index) => ( -
    • - โ€ข +
    • + โ€ข {item}
    • ))} @@ -210,40 +210,40 @@ export default function MyInterviews() { )} {!feedbackData.result.aiFeedback && !feedbackData.result.selfFeedback && feedbackData.result.peerItems.length === 0 && ( -

      ํ”ผ๋“œ๋ฐฑ์ด ์—†์Šต๋‹ˆ๋‹ค.

      +

      ํ”ผ๋“œ๋ฐฑ์ด ์—†์Šต๋‹ˆ๋‹ค.

      )}
    )} {/* ๋žœ๋ค ์งˆ๋ฌธ ํƒญ */} {activeTab === 'random' && randomData?.result && ( -
    +
    {randomData.result.length > 0 ? ( randomData.result.map((item, index) => ( -
    -
    - +
    +
    + ๋žœ๋ค ์งˆ๋ฌธ {index + 1} -

    {item.question}

    +

    {item.question}

    -
    -

    ๋‹ต๋ณ€:

    -

    {item.answerText}

    +
    +

    ๋‹ต๋ณ€:

    +

    {item.answerText}

    -
    +
    {item.aiFeedback && ( -
    -

    AI ํ”ผ๋“œ๋ฐฑ

    -

    {item.aiFeedback}

    +
    +

    AI ํ”ผ๋“œ๋ฐฑ

    +

    {item.aiFeedback}

    )} {item.selfFeedback && ( -
    -

    ์…€ํ”„ ํ”ผ๋“œ๋ฐฑ

    -

    {item.selfFeedback}

    +
    +

    ์…€ํ”„ ํ”ผ๋“œ๋ฐฑ

    +

    {item.selfFeedback}

    )}
    @@ -261,7 +261,7 @@ export default function MyInterviews() {
    )) ) : ( -

    ๋žœ๋ค ์งˆ๋ฌธ์ด ์—†์Šต๋‹ˆ๋‹ค.

    +

    ๋žœ๋ค ์งˆ๋ฌธ์ด ์—†์Šต๋‹ˆ๋‹ค.

    )}
    )} From c380bc1854af8d5b9488aea1b055640a56a67195 Mon Sep 17 00:00:00 2001 From: Hello-Worldismine Date: Mon, 17 Nov 2025 03:04:59 +0900 Subject: [PATCH 7/7] =?UTF-8?q?=EB=A7=88=EC=9D=B4=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EC=B9=B4=EB=93=9C=EA=B0=84=EA=B2=A9=20=EC=A1=B0?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/myPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/myPage.tsx b/src/pages/myPage.tsx index d10611f..95508f1 100644 --- a/src/pages/myPage.tsx +++ b/src/pages/myPage.tsx @@ -83,7 +83,7 @@ export default function MyPage() {

    {title}

    {description}

    - + window.location.replace(title === '๋‚˜์˜ ๋ฉด์ ‘,' ? route.myInterviews : route.evaluate)} />