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);
};