diff --git a/src/hooks/useGetCommunityDetail.ts b/src/hooks/useGetCommunityDetail.ts index 9dc82ea..e0256f1 100644 --- a/src/hooks/useGetCommunityDetail.ts +++ b/src/hooks/useGetCommunityDetail.ts @@ -4,7 +4,7 @@ import { getCommunityDetail } from '@/apis/community'; import { useCoreQuery } from '@/hooks/customQuery'; -export default function usegetCommunityDetail(param: TCommumityDetailRequest) { +export default function useGetCommunityDetail(param: TCommumityDetailRequest) { const { data } = useCoreQuery(['getCommunityDetail', param.pageId], () => getCommunityDetail(param)); return { data }; } diff --git a/src/pages/Interview/feedback_result.tsx b/src/pages/Interview/feedback_result.tsx index 40499df..6a8bb9f 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 { @@ -32,7 +32,7 @@ export default function FeedbackResult() { setIsLoading(true); const response = await getFinalFeedback(sessionId); setFeedbackData(response); - const states = response.feedbacks.map((_, idx) => ({ id: idx + 1, showAnswer: false })); + const states = response.feedbacks.map((_, idx: number) => ({ id: idx + 1, showAnswer: false })); setQuestionStates(states); } catch (err) { console.error('❌ 피드백 조회 실패:', err); @@ -97,7 +97,7 @@ export default function FeedbackResult() {
- {feedbacks.map((item: IFeedbackItem, index: number) => { + {feedbacks.map((item, index: number) => { const isShowingAnswer = questionStates.find((q) => q.id === index + 1)?.showAnswer || false; const isPositive = item.feedbackType === 'positive'; const feedbackTypeLabel = isPositive ? 'AI 피드백(긍정)' : 'AI 피드백(개선)'; diff --git a/src/pages/communityDetail.tsx b/src/pages/communityDetail.tsx index 3fb875c..d9860d0 100644 --- a/src/pages/communityDetail.tsx +++ b/src/pages/communityDetail.tsx @@ -2,7 +2,7 @@ import { useNavigate, useParams } from 'react-router-dom'; import { formatUpdatedAt } from '@/utils/time'; -import usegetCommunityDetail from '@/hooks/useGetCommunityDetail'; +import useGetCommunityDetail from '@/hooks/useGetCommunityDetail'; import type { TValues } from '@/components/community/writeForm'; import WriteForm from '@/components/community/writeForm'; @@ -12,7 +12,7 @@ import { route } from '@/routes/route'; export default function CommunityDetail() { const navigate = useNavigate(); const { id } = useParams<{ id: string }>(); - const { data } = usegetCommunityDetail({ pageId: Number(id) }); + const { data } = useGetCommunityDetail({ pageId: Number(id) }); const values: Partial = data ?? {}; return ( diff --git a/src/services/interviewApi.ts b/src/services/interviewApi.ts index bcf7cf4..b256161 100644 --- a/src/services/interviewApi.ts +++ b/src/services/interviewApi.ts @@ -22,26 +22,16 @@ export interface IResumePresignResponse { } /** 자소서 업로드용 프리사인 URL 발급 */ -export const getResumePresignUrl = async ( - fileName: string, -): Promise => { - const response = await apiClient.post( - '/api/presign/resume', - null, - { - params: { fileName }, - }, - ); +export const getResumePresignUrl = async (fileName: string): Promise => { + const response = await apiClient.post('/api/presign/resume', null, { + params: { fileName }, + }); return unwrapResult(response.data); }; /** S3 공통 업로드 헬퍼 (resume / recording 둘 다 사용) */ -export const uploadToS3 = async ( - presignedUrl: string, - file: File | Blob, - extraHeaders: Record = {}, -): Promise => { +export const uploadToS3 = async (presignedUrl: string, file: File | Blob, extraHeaders: Record = {}): Promise => { const baseHeaders: Record = {}; // Blob에 type이 있으면 기본 Content-Type으로 사용 @@ -95,10 +85,10 @@ export const uploadResume = async (file: File): Promise => { // ===================================================== /** 프론트에서 쓰는 인터뷰 타입 */ -export type InterviewType = 'normal' | 'pressure'; +export type TInterviewType = 'normal' | 'pressure'; /** BE에서 사용하는 면접 모드 */ -export type InterviewMode = 'NORMAL' | 'HARD'; +export type TInterviewMode = 'NORMAL' | 'HARD'; /** 프론트에서 쓰는 요청 타입 */ export interface ICreateInterviewSessionRequest { @@ -107,12 +97,12 @@ export interface ICreateInterviewSessionRequest { /** 직무 이름 */ jobTitle: string; /** 'normal' | 'pressure' → NORMAL | HARD 로 매핑 */ - interviewType: InterviewType; + interviewType: TInterviewType; } /** BE에 실제로 보내는 payload (mode / jobRole / resumeId) */ interface ICreateInterviewSessionPayload { - mode: InterviewMode; + mode: TInterviewMode; jobRole: string; resumeId: string; } @@ -139,11 +129,9 @@ export interface ICreateInterviewSessionResponse { } /** 자소서 기반 질문 생성 및 첫번째 질문 조회 */ -export const createInterviewSession = async ( - data: ICreateInterviewSessionRequest, -): Promise => { +export const createInterviewSession = async (data: ICreateInterviewSessionRequest): Promise => { const resumeId = extractResumeId(data.resumeKey); - const mode: InterviewMode = data.interviewType === 'pressure' ? 'HARD' : 'NORMAL'; + const mode: TInterviewMode = data.interviewType === 'pressure' ? 'HARD' : 'NORMAL'; const payload: ICreateInterviewSessionPayload = { mode, @@ -185,10 +173,7 @@ export interface IRecordingPresignResponse { } /** 녹음 업로드용 프리사인 URL 발급 */ -export const getRecordingPresignUrl = async ( - questionId: number, - contentType: string, -): Promise => { +export const getRecordingPresignUrl = async (questionId: number, contentType: string): Promise => { const payload: IRecordingPresignRequest = { questionId, contentType }; const response = await apiClient.post('/api/presign/recording', payload); @@ -200,31 +185,29 @@ export const getRecordingPresignUrl = async ( // ===================================================== /** 녹음 제출 API 응답 status (현재 스펙상 UPLOADED 고정) */ -export type RecordingEnqueueStatus = 'UPLOADED'; +export type TRecordingEnqueueStatus = 'UPLOADED'; /** 녹음 저장 응답 (POST /api/questions/{questionId}/recordings) */ export interface ISaveRecordingResponse { recordingId: number; - status: RecordingEnqueueStatus; + status: TRecordingEnqueueStatus; } /** 녹음 저장 및 꼬리질문 생성 API (비동기, 바로 응답) */ -export const saveRecording = async ( - questionId: number, -): Promise => { +export const saveRecording = async (questionId: number): Promise => { const response = await apiClient.post(`/api/questions/${questionId}/recordings`); return unwrapResult(response.data); }; /** Polling API status */ -export type RecordingResultStatus = 'WORKING' | 'READY' | 'FAILED'; +export type TRecordingResultStatus = 'WORKING' | 'READY' | 'FAILED'; /** next.type */ -export type NextQuestionType = 'FOLLOW_UP' | 'ROOT' | 'NONE'; +export type TNextQuestionType = 'FOLLOW_UP' | 'ROOT' | 'NONE'; /** Polling/Timeout 공통 next 객체 타입 */ export interface IRecordingResultNext { - type: NextQuestionType; + type: TNextQuestionType; nextQuestionId: number | null; nextQuestionText: string | null; rootId: number | null; @@ -235,14 +218,12 @@ export interface IRecordingResultNext { /** Polling API 응답 타입 (GET /api/recordings/{recordingId}/results) */ export interface IRecordingResultResponse { sessionId: number; - status: RecordingResultStatus; + status: TRecordingResultStatus; next: IRecordingResultNext | null; } /** Polling API - 한 번 조회 */ -export const getRecordingResult = async ( - recordingId: number, -): Promise => { +export const getRecordingResult = async (recordingId: number): Promise => { const response = await apiClient.get(`/api/recordings/${recordingId}/results`); return unwrapResult(response.data); }; @@ -258,21 +239,17 @@ const mapNextToQuestion = (next: IRecordingResultNext | null): IQuestion | null return { questionId: String(next.nextQuestionId), mainQuestion: isFollowUp - ? next.rootText ?? '' // 꼬리질문이면 rootText를 메인 질문으로 - : next.nextQuestionText ?? '', // 루트 질문이면 그대로 + ? (next.rootText ?? '') // 꼬리질문이면 rootText를 메인 질문으로 + : (next.nextQuestionText ?? ''), // 루트 질문이면 그대로 subQuestion: isFollowUp - ? next.nextQuestionText ?? '' // 꼬리질문 텍스트 + ? (next.nextQuestionText ?? '') // 꼬리질문 텍스트 : '', order: next.rootIndex ?? 0, }; }; /** Polling 헬퍼: READY/FAILED 될 때까지 반복 조회 */ -export const pollRecordingResult = async ( - recordingId: number, - maxAttempts: number = 60, - intervalMs: number = 3000, -): Promise => { +export const pollRecordingResult = async (recordingId: number, maxAttempts: number = 60, intervalMs: number = 3000): Promise => { let attempts = 0; while (attempts < maxAttempts) { @@ -297,12 +274,8 @@ export const pollRecordingResult = async ( * 녹음 파일 업로드 + recording 저장 + polling 후 * 다음 질문(IQuestion) 혹은 null(세션 종료) 반환 */ -export const uploadRecordingAndGetNext = async ( - questionId: string | number, - audioBlob: Blob, -): Promise => { - const numericQuestionId = - typeof questionId === 'string' ? Number(questionId) : questionId; +export const uploadRecordingAndGetNext = async (questionId: string | number, audioBlob: Blob): Promise => { + const numericQuestionId = typeof questionId === 'string' ? Number(questionId) : questionId; if (Number.isNaN(numericQuestionId)) { throw new Error('유효하지 않은 questionId 입니다.'); @@ -311,10 +284,7 @@ export const uploadRecordingAndGetNext = async ( const contentType = (audioBlob as any).type || 'audio/webm'; // 1) Presign 발급 - const { uploadUrl, requiredHeaders } = await getRecordingPresignUrl( - numericQuestionId, - contentType, - ); + const { uploadUrl, requiredHeaders } = await getRecordingPresignUrl(numericQuestionId, contentType); // 2) S3 업로드 await uploadToS3(uploadUrl, audioBlob, requiredHeaders); @@ -345,9 +315,7 @@ export interface ITimeoutResponse { } /** 사용자가 시간초과로 답변하지 못한 경우 - Timeout API 호출 */ -export const sendTimeout = async ( - questionId: string | number, -): Promise => { +export const sendTimeout = async (questionId: string | number): Promise => { const response = await apiClient.post(`/api/questions/${questionId}/timeout`); return unwrapResult(response.data); }; @@ -357,9 +325,7 @@ export const sendTimeout = async ( * - next.type === ROOT → 다음 루트 질문 * - next.type === NONE → 더 이상 질문 없음 (최종 피드백 조회) */ -export const timeoutAndGetNextQuestion = async ( - questionId: string | number, -): Promise => { +export const timeoutAndGetNextQuestion = async (questionId: string | number): Promise => { const result = await sendTimeout(questionId); if (!result.next || result.next.type === 'NONE') { @@ -374,14 +340,14 @@ export const timeoutAndGetNextQuestion = async ( // ===================================================== /** 피드백 생성 진행 상태 */ -export type FeedbackProgressStatus = 'WORKING' | 'READY' | 'FAILED'; +export type TFeedbackProgressStatus = 'WORKING' | 'READY' | 'FAILED'; /** QnA 턴 타입 */ -export type FeedbackTurnType = 'QUESTION' | 'ANSWER'; +export type TFeedbackTurnType = 'QUESTION' | 'ANSWER'; /** 한 턴 (질문 / 답변) */ export interface IQnaTurn { - turn: FeedbackTurnType; + turn: TFeedbackTurnType; content: string; } @@ -403,14 +369,22 @@ export interface IInterviewSummary { /** 최종 피드백 조회 응답 */ export interface IFinalFeedbackResponse { - feedbackProgressStatus: FeedbackProgressStatus; + feedbackProgressStatus: TFeedbackProgressStatus; interviewSummary: IInterviewSummary | null; // WORKING일 때는 null + feedbacks: { + feedbackType: 'positive' | 'improvement'; + answer: string; + timeout: boolean; + feedback: string; + questionId: number; + question: string; + }[]; + totalQuestions: number; + timeoutCount: number; } /** 최종 피드백 조회 (GET /api/interview-sesisons/{sessionId}) */ -export const getFinalFeedback = async ( - sessionId: string | number, -): Promise => { +export const getFinalFeedback = async (sessionId: string | number): Promise => { const response = await apiClient.get(`/api/interview-sesisons/${sessionId}`); return unwrapResult(response.data); };