Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import importConfig from './eslint/import.mjs';
import reactConfig from './eslint/react.mjs';
import tsConfig from './eslint/typescript.mjs';

import reactHooks from 'eslint-plugin-react-hooks';

export default [
{
ignores: ['src/vite-env.d.ts'],
Expand Down Expand Up @@ -41,5 +43,14 @@ export default [
defaultConfig,
importConfig,
reactConfig,

{
plugins: { 'react-hooks': reactHooks },
rules: {
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn',
},
},

prettierPluginRecommended,
];
3 changes: 1 addition & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
"@types/react-dom": "^19.0.4",
"@vitejs/plugin-react": "^4.3.4",
"eslint": "^9.35.0",
"eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.19",
"globals": "^15.15.0",
"typescript": "~5.7.2",
Expand Down
2 changes: 1 addition & 1 deletion src/components/RandomQuestion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export default function RandomQuestion() {
); // 3~8초

return () => clearTimeout(timeout);
}, []);
}, [questions.length]);

// ===== 유틸 =====
const formatTime = (s: number) => {
Expand Down
2 changes: 1 addition & 1 deletion src/hooks/useGetCommunityDetail.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
}
190 changes: 109 additions & 81 deletions src/pages/Interview/feedback_result.tsx
Original file line number Diff line number Diff line change
@@ -1,98 +1,142 @@
import { useState } from 'react';
// src/pages/Interview/feedback_result.tsx
import { useEffect, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';

import InterviewLayout from '@/layouts/InterviewLayout';
import type { IFinalFeedbackResponse } from '@/services/interviewApi';
import { getFinalFeedback } from '@/services/interviewApi';

interface IQuestionState {
id: number;
showAnswer: boolean;
}

export default function FeedbackResult() {
const [questionStates, setQuestionStates] = useState<IQuestionState[]>([
{ id: 1, showAnswer: false },
{ id: 2, showAnswer: false },
{ id: 3, showAnswer: false },
{ id: 4, showAnswer: false },
]);
const navigate = useNavigate();
const location = useLocation() as { state?: { sessionId?: string } };
const { sessionId } = location.state || {};

const feedbacks = [
{
id: 1,
title: '간단히 자기소개를 해주세요',
type: '긍정적 피드백',
feedback:
'예상 질문은 잘 알겠지만 꼬리 질문에서 말이 막혀 아쉽습니다. 실제 면접이라고 생각하고 진행한 탓에 말이 빨라졌고, 직무 관련 경험을 구체적으로 설명했을 때는 면접관이 긍정적인 반응을 보였습니다. 다만 마지막 자기소개를 준비하지 못해 아쉬움이 남...예상 질문은 잘 알겠지만 꼬리 질문에서 말이 막혀 아쉽습니다. 실제 면접이라고 생각하고 진행한 탓에 말이 빨라졌고, 직무 관련 경험을 구체적으로 설명했을 때는 면접관이 긍정적인 반응을 보였습니다. 다만 마지막 자기소개를 준비하지 못해 아쉬움이 남...',
answer:
'내 답변 내용 내 답변 내용 내 답변 내용 내 답변 내용 내 답변 내용 내 답변 내용 내 답변 내용 내 답변 내용 내 답변 내용 내 답변 내용 내 답변 내용 내 답변 내용 내 답변 내용 내 답변 내용 내 답변 내용 내 답변 내용 내 답변 내용 내 답변 내용 내 답변 내용 내 답변 내용 내 답변 내용 내 답변 내용 내 답변 내용 내 답변 내용 내 답변 내용 내 답변 내용 내 답변 내용 내 답변 내용 내 답변 내용 내 답변 내용 내 답변 내용 내 답변 내용 내 답변 내용 내 답변 내용 내 답변 내용 내 답변 내용 내 답변 내용 내 답변 내용 내 답변 내용 내 답변 내용 내 답변 내용 내 답변 내용',
},
{
id: 2,
title: '간단히 자기소개를 해주세요',
type: '긍정적 피드백',
feedback:
'예상 질문은 잘 알겠지만 꼬리 질문에서 말이 막혀 아쉽습니다. 실제 면접이라고 생각하고 진행한 탓에 말이 빨라졌고, 직무 관련 경험을 구체적으로 설명했을 때는 면접관이 긍정적인 반응을 보였습니다. 다만 마지막 자기소개를 준비하지 못해 아쉬움이 남...예상 질문은 잘 알겠지만 꼬리 질문에서 말이 막혀 아쉽습니다. 실제 면접이라고 생각하고 진행한 탓에 말이 빨라졌고, 직무 관련 경험을 구체적으로 설명했을 때는 면접관이 긍정적인 반응을 보였습니다. 다만 마지막 자기소개를 준비하지 못해 아쉬움이 남...',
answer: '내 답변 내용입니다.',
},
{
id: 3,
title: '간단히 자기소개를 해주세요',
type: '부정적 피드백',
feedback:
'예상 질문은 잘 알겠지만 꼬리 질문에서 말이 막혀 아쉽습니다. 실제 면접이라고 생각하고 진행한 탓에 말이 빨라졌고, 직무 관련 경험을 구체적으로 설명했을 때는 면접관이 긍정적인 반응을 보였습니다. 다만 마지막 자기소개를 준비하지 못해 아쉬움이 남...예상 질문은 잘 알겠지만 꼬리 질문에서 말이 막혀 아쉽습니다. 실제 면접이라고 생각하고 진행한 탓에 말이 빨라졌고, 직무 관련 경험을 구체적으로 설명했을 때는 면접관이 긍정적인 반응을 보였습니다. 다만 마지막 자기소개를 준비하지 못해 아쉬움이 남...',
answer: '내 답변 내용입니다.',
},
{
id: 4,
title: '간단히 자기소개를 해주세요',
type: '부정적 피드백',
feedback:
'예상 질문은 잘 알겠지만 꼬리 질문에서 말이 막혀 아쉽습니다. 실제 면접이라고 생각하고 진행한 탓에 말이 빨라졌고, 직무 관련 경험을 구체적으로 설명했을 때는 면접관이 긍정적인 반응을 보였습니다. 다만 마지막 자기소개를 준비하지 못해 아쉬움이 남...',
answer: '내 답변 내용입니다.',
},
];
const [feedbackData, setFeedbackData] = useState<IFinalFeedbackResponse | null>(null);
const [questionStates, setQuestionStates] = useState<IQuestionState[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);

useEffect(() => {
if (!sessionId) {
alert('세션 정보가 없습니다.');
navigate('/upload');
return;
}
const fetchFeedback = async () => {
try {
setIsLoading(true);
const response = await getFinalFeedback(sessionId);
setFeedbackData(response);
const states = response.feedbacks.map((_, idx: number) => ({ id: idx + 1, showAnswer: false }));
setQuestionStates(states);
} catch (err) {
console.error('❌ 피드백 조회 실패:', err);
setError('피드백을 불러오는데 실패했습니다.');
} finally {
setIsLoading(false);
}
};
void fetchFeedback();
}, [navigate, sessionId]);

const toggleAnswer = (id: number) => {
setQuestionStates((prev) => prev.map((q) => (q.id === id ? { ...q, showAnswer: !q.showAnswer } : q)));
};

if (isLoading) {
return (
<InterviewLayout activeMenu="feedback">
<div className="flex-1 flex items-center justify-center">
<div className="text-center">
<div className="mb-4">
<img src="src/assets/clockFrog.svg" alt="로딩" className="w-32 h-auto mx-auto animate-pulse" />
</div>
<p className="text-gray-600 text-lg">피드백을 생성하고 있습니다...</p>
<p className="text-gray-500 text-sm mt-2">최대 5분 정도 소요될 수 있습니다.</p>
</div>
</div>
</InterviewLayout>
);
}

if (error || !feedbackData) {
return (
<InterviewLayout activeMenu="feedback">
<div className="flex-1 flex items-center justify-center">
<div className="text-center">
<p className="text-red-500 text-lg mb-4">{error || '피드백 데이터를 불러올 수 없습니다.'}</p>
<button onClick={() => navigate('/upload')} className="bg-gray-900 hover:bg-gray-800 text-white px-6 py-3 rounded-lg font-medium transition-colors">
처음으로 돌아가기
</button>
</div>
</div>
</InterviewLayout>
);
}

const { feedbacks, totalQuestions, timeoutCount } = feedbackData;

return (
<InterviewLayout activeMenu="feedback">
{/* 중앙 컨텐츠 영역 */}
<div className="flex-1 px-8 pt-4">
{/* 상단 정보 */}
<div className="mb-6">
<h2 className="text-2xl font-semibold text-gray-900 mb-3">
<span className="inline-block bg-gray-400 text-white px-4 py-1 rounded-full text-sm mr-2">2025년_3월_자소서</span>에 대한 최종 피드백
<span className="inline-block bg-gray-400 text-white px-4 py-1 rounded-full text-sm mr-2">총 {totalQuestions}문항</span>에 대한 최종 피드백
</h2>
<p className="text-gray-600">
시간 초과로 답변하지 못한 질문 <span className="inline-block bg-coral-500 text-white px-3 py-1 rounded-md text-sm font-medium">n개</span>
</p>
{timeoutCount > 0 && (
<p className="text-gray-600">
시간 초과로 답변하지 못한 질문{' '}
<span className="inline-block bg-coral-500 text-white px-3 py-1 rounded-md text-sm font-medium">{timeoutCount}개</span>
</p>
)}
</div>

{/* 질문 카드 그리드 */}
<div className="grid grid-cols-2 gap-6 pb-8">
{feedbacks.map((item) => {
const isShowingAnswer = questionStates.find((q) => q.id === item.id)?.showAnswer;
{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 피드백(개선)';
const hasAnswer = !!item.answer && item.answer.trim().length > 0;

return (
<div key={item.id} className={`rounded-2xl p-6 shadow-sm transition-colors ${isShowingAnswer ? 'bg-gray-200' : 'bg-white'}`}>
{/* 카드 헤더 */}
<div
key={`${item.questionId}-${index}`}
className={`rounded-2xl p-6 shadow-sm transition-colors ${isShowingAnswer ? 'bg-gray-200' : 'bg-white'}`}
>
<div className="mb-4">
<h3 className="font-semibold text-gray-900 mb-2">
{item.id}. {item.title}
{index + 1}. {item.question}
</h3>
<p className="text-sm text-gray-500">AI 피드백 | {item.type}</p>
<p className="text-sm text-gray-500">{feedbackTypeLabel}</p>
</div>

{/* 카드 내용 (스크롤 가능) */}
<div className="mb-4 max-h-40 overflow-y-auto scrollbar-thin scrollbar-thumb-gray-300 scrollbar-track-gray-100">
<p className="text-sm text-gray-700 leading-relaxed">{isShowingAnswer ? item.answer : item.feedback}</p>
{isShowingAnswer ? (
hasAnswer ? (
<div className="space-y-3">
<div>
<p className="text-xs font-semibold text-gray-600 mb-1">답변:</p>
<p className="text-sm text-gray-700 leading-relaxed whitespace-pre-wrap">{item.answer}</p>
</div>
</div>
) : (
<p className="text-sm text-gray-500 italic">{item.timeout ? '시간 초과로 답변하지 못했습니다.' : '답변이 제공되지 않았습니다.'}</p>
)
) : item.feedback ? (
<p className="text-sm text-gray-700 leading-relaxed whitespace-pre-wrap">{item.feedback}</p>
) : (
<p className="text-sm text-gray-500 italic">피드백이 생성되지 않았습니다.</p>
)}
</div>

{/* 버튼 */}
<div className="flex justify-end">
<button
onClick={() => toggleAnswer(item.id)}
onClick={() => toggleAnswer(index + 1)}
className={`px-6 py-2 rounded-lg text-sm font-medium transition-colors ${
isShowingAnswer ? 'bg-coral-500 text-white hover:bg-coral-600' : 'bg-gray-500 text-white hover:bg-gray-600'
}`}
Expand All @@ -107,28 +151,12 @@ export default function FeedbackResult() {
</div>

<style>{`
.bg-coral-500 {
background-color: #ff7f66;
}
.bg-coral-600 {
background-color: #ff6b52;
}
.hover\\:bg-coral-600:hover {
background-color: #ff6b52;
}

/* 스크롤바 스타일링 */
.scrollbar-thin::-webkit-scrollbar {
width: 6px;
}
.scrollbar-thumb-gray-300::-webkit-scrollbar-thumb {
background-color: #d1d5db;
border-radius: 3px;
}
.scrollbar-track-gray-100::-webkit-scrollbar-track {
background-color: #f3f4f6;
border-radius: 3px;
}
.bg-coral-500 { background-color: #ff7f66; }
.bg-coral-600 { background-color: #ff6b52; }
.hover\\:bg-coral-600:hover { background-color: #ff6b52; }
.scrollbar-thin::-webkit-scrollbar { width: 6px; }
.scrollbar-thumb-gray-300::-webkit-scrollbar-thumb { background-color: #d1d5db; border-radius: 3px; }
.scrollbar-track-gray-100::-webkit-scrollbar-track { background-color: #f3f4f6; border-radius: 3px; }
`}</style>
</InterviewLayout>
);
Expand Down
Loading