)}
{q.answer.technicalScore && (
-
+
기술적 이해
{q.answer.technicalScore}/10
)}
{q.answer.structureScore && (
-
+
답변 구조
{q.answer.structureScore}/10
@@ -557,13 +707,19 @@ export default function Interview() {
))}
-
+
+
)}
diff --git a/FE/src/pages/Questions.jsx b/FE/src/pages/Questions.jsx
index 9561efb..adf8781 100644
--- a/FE/src/pages/Questions.jsx
+++ b/FE/src/pages/Questions.jsx
@@ -15,11 +15,11 @@ const CATEGORY_OPTS = [
];
const DIFF_OPTS = [
{ id: 'all', label: '난이도 전체' },
- { id: 1, label: '1' },
- { id: 2, label: '2' },
- { id: 3, label: '3' },
- { id: 4, label: '4' },
- { id: 5, label: '5' },
+ { id: 1, label: '1단계' },
+ { id: 2, label: '2단계' },
+ { id: 3, label: '3단계' },
+ { id: 4, label: '4단계' },
+ { id: 5, label: '5단계' },
];
const TYPE_OPTS = [
{ id: 'all', label: '타입 전체' },
@@ -34,55 +34,128 @@ function Questions() {
const nav = useNavigate();
/* ==== 필터 상태 ==== */
- const [keyword, setKeyword] = useState('');
- const [category, setCategory] = useState('all');
+ const [keyword, setKeyword] = useState('');
+ const [category, setCategory] = useState('all');
const [difficulty, setDifficulty] = useState('all');
- const [qType, setQType] = useState('all');
- const [onlyFav, setOnlyFav] = useState(false);
+ const [qType, setQType] = useState('all');
+ const [onlyFav, setOnlyFav] = useState(false);
/* ==== 데이터 ==== */
- const [questions, setQuestions] = useState([]);
- const [loading, setLoading] = useState(true);
+ const [questions, setQuestions] = useState([]);
+ const [loading, setLoading] = useState(true);
const [expandedQuestions, setExpandedQuestions] = useState(new Set());
+ const [isAuthenticated, setIsAuthenticated] = useState(false);
+ const [userInfo, setUserInfo] = useState(null);
/* ==== 모달 상태 ==== */
- const [showModal, setShowModal] = useState(false);
- const [companies, setCompanies] = useState([]);
- const [positions, setPositions] = useState([]);
- const [companyId, setCompanyId] = useState('');
- const [positionId, setPositionId] = useState('');
- const [questionCount, setQuestionCount] = useState(5);
- const [modalBusy, setModalBusy] = useState(false);
+ const [showModal, setShowModal] = useState(false);
+ const [companies, setCompanies] = useState([]);
+ const [positions, setPositions] = useState([]);
+ const [companyId, setCompanyId] = useState('');
+ const [positionId, setPositionId] = useState('');
+ const [questionCount, setQuestionCount] = useState(5);
+ const [selectedDifficulty, setSelectedDifficulty] = useState(3); // 새로 추가
+ const [modalBusy, setModalBusy] = useState(false);
+
+ /* ── 간단한 인증 확인 (localStorage 기반) ── */
+ useEffect(() => {
+ const checkAuth = () => {
+ const token = localStorage.getItem('accessToken') || localStorage.getItem('token');
+ const userId = localStorage.getItem('userId');
+ const userEmail = localStorage.getItem('userEmail');
+
+ if (token && userId) {
+ setIsAuthenticated(true);
+ setUserInfo({
+ id: userId,
+ email: userEmail,
+ token: token
+ });
+ console.log('인증 확인됨 - 사용자 ID:', userId);
+ } else {
+ console.log('인증 정보 없음 - 로그인 페이지로 이동');
+ nav('/signin');
+ }
+ };
+
+ checkAuth();
+ }, [nav]);
/* ── 질문 검색 ── */
const fetchQuestions = async () => {
+ if (!isAuthenticated) return;
+
setLoading(true);
try {
- const params = {
- keyword: keyword || undefined,
- category: category !== 'all' ? category : undefined,
- difficultyLevel: difficulty !== 'all' ? difficulty : undefined,
- type: qType !== 'all' ? qType : undefined,
- size: 100,
- };
- const { data } = await api.get('/api/interviews/questions/search', { params });
- setQuestions(data.data?.content || []);
+ let questionData = [];
+
+ if (onlyFav) {
+ // 즐겨찾기 질문만 가져오기
+ try {
+ const { data } = await api.get('/api/interviews/questions/favorites');
+ questionData = data.data || [];
+ console.log('즐겨찾기 질문:', questionData);
+ } catch (err) {
+ if (err.response?.status === 401) {
+ console.log('인증 만료 - 로그인 페이지로 이동');
+ localStorage.clear();
+ nav('/signin');
+ return;
+ }
+ throw err;
+ }
+ } else {
+ // 일반 질문 검색
+ const params = {
+ keyword: keyword || undefined,
+ category: category !== 'all' ? category : undefined,
+ difficultyLevel: difficulty !== 'all' ? difficulty : undefined,
+ type: qType !== 'all' ? qType : undefined,
+ size: 100,
+ };
+
+ try {
+ const { data } = await api.get('/api/interviews/questions/search', { params });
+ questionData = data.data?.content || [];
+ console.log('검색된 질문:', questionData);
+ } catch (err) {
+ if (err.response?.status === 401) {
+ console.log('인증 만료 - 로그인 페이지로 이동');
+ localStorage.clear();
+ nav('/signin');
+ return;
+ }
+ throw err;
+ }
+ }
+
+ setQuestions(questionData);
} catch (e) {
- console.error(e);
+ console.error('질문 검색 오류:', e);
alert('질문을 불러오지 못했습니다.');
} finally {
setLoading(false);
}
};
- useEffect(() => { fetchQuestions(); }, [keyword, category, difficulty, qType]); // eslint-disable-line
- /* ── 즐겨찾기 ── */
+ useEffect(() => {
+ if (isAuthenticated) {
+ fetchQuestions();
+ }
+ }, [keyword, category, difficulty, qType, onlyFav, isAuthenticated]);
+
+ /* ── 즐겨찾기 토글 ── */
const toggleFav = async (id) => {
try {
await api.post(`/api/interviews/questions/${id}/favorite`);
setQuestions(prev => prev.map(q => (q.id === id ? { ...q, isFavorite: !q.isFavorite } : q)));
} catch (e) {
- console.error(e);
+ console.error('즐겨찾기 변경 오류:', e);
+ if (e.response?.status === 401) {
+ localStorage.clear();
+ nav('/signin');
+ return;
+ }
alert('즐겨찾기 변경 실패');
}
};
@@ -108,7 +181,12 @@ function Questions() {
const { data } = await api.get('/api/companies');
setCompanies(data.data || []);
} catch (e) {
- console.error(e);
+ console.error('회사 목록 로드 오류:', e);
+ if (e.response?.status === 401) {
+ localStorage.clear();
+ nav('/signin');
+ return;
+ }
alert('회사 목록을 가져올 수 없습니다.');
}
};
@@ -122,12 +200,17 @@ function Questions() {
const { data } = await api.get(`/api/companies/${id}/positions`);
setPositions(data.data || []);
} catch (e) {
- console.error(e);
+ console.error('포지션 목록 로드 오류:', e);
+ if (e.response?.status === 401) {
+ localStorage.clear();
+ nav('/signin');
+ return;
+ }
alert('포지션 목록을 가져올 수 없습니다.');
}
};
- /* ── AI 질문 생성 ── */
+ /* ── AI 질문 생성 (난이도 선택 기능 추가) ── */
const generateQuestions = async () => {
if (!positionId) return alert('포지션을 선택하세요.');
@@ -138,11 +221,16 @@ function Questions() {
try {
const resumeRes = await api.get('/api/resume');
myResumeId = resumeRes.data.data.id;
+ console.log('내 이력서 ID:', myResumeId);
} catch (e) {
if (e.response?.status === 404) {
alert('이력서를 먼저 작성해주세요.');
setModalBusy(false);
return;
+ } else if (e.response?.status === 401) {
+ localStorage.clear();
+ nav('/signin');
+ return;
}
throw e;
}
@@ -151,37 +239,38 @@ function Questions() {
const pos = positions.find(p => p.id === Number(positionId));
const positionName = pos?.title || pos?.name || '';
- /* 3) AI를 사용한 면접 세션 생성 (질문 자동 생성) */
+ /* 3) 난이도가 포함된 AI 면접 세션 생성 */
const createInterviewRes = await api.post('/api/interviews', {
resumeId: myResumeId,
positionId: Number(positionId),
- title: `AI 생성 질문 - ${positionName} - ${new Date().toLocaleDateString()}`,
- description: `${positionName} 포지션을 위한 맞춤형 면접 질문`,
+ title: `AI 생성 질문 - ${positionName} (난이도 ${selectedDifficulty}) - ${new Date().toLocaleDateString()}`,
+ description: `${positionName} 포지션을 위한 맞춤형 면접 질문 (난이도 ${selectedDifficulty}단계)`,
type: "TEXT",
mode: "PRACTICE",
- useAI: true, // AI 사용하여 질문 자동 생성
+ useAI: true,
questionCount: questionCount,
- difficultyLevel: 3,
- categoryFilter: "기술면접"
+ difficultyLevel: selectedDifficulty, // 사용자가 선택한 난이도 사용
+ categoryFilter: category !== 'all' ? category : "기술면접",
+ expectedDurationMinutes: questionCount * 5, // 질문당 5분 예상
+ public: false
});
- const interviewId = createInterviewRes.data.data.id;
+ const interviewId = createInterviewRes.data.data?.id || createInterviewRes.data.id;
+ console.log('생성된 면접 ID:', interviewId);
/* 4) 생성된 면접의 질문들 확인 */
const { data: questionsData } = await api.get(`/api/interviews/${interviewId}/questions`);
if (questionsData.data && questionsData.data.length > 0) {
- alert(`${questionsData.data.length}개의 면접 질문이 생성되었습니다!`);
- // 질문 목록 새로고침
+ alert(`${questionsData.data.length}개의 맞춤형 면접 질문이 생성되었습니다! (난이도 ${selectedDifficulty}단계)`);
await fetchQuestions();
} else {
- // 질문이 자동 생성되지 않았다면, 기존 질문 풀에서 가져오기
alert('AI 질문 생성 중... 잠시만 기다려주세요.');
- // 카테고리와 난이도에 맞는 질문 검색
+ // 선택된 난이도로 기본 질문 검색
const searchParams = {
- category: '기술면접',
- difficultyLevel: 3,
+ category: category !== 'all' ? category : '기술면접',
+ difficultyLevel: selectedDifficulty,
size: questionCount
};
@@ -190,10 +279,10 @@ function Questions() {
});
if (searchData.data?.content && searchData.data.content.length > 0) {
- alert(`${searchData.data.content.length}개의 관련 질문을 찾았습니다!`);
+ alert(`${searchData.data.content.length}개의 관련 질문을 찾았습니다! (난이도 ${selectedDifficulty}단계)`);
await fetchQuestions();
} else {
- alert('적합한 질문을 찾을 수 없습니다. 다른 조건으로 시도해주세요.');
+ alert('적합한 질문을 찾을 수 없습니다.');
}
}
@@ -201,35 +290,39 @@ function Questions() {
setCompanyId('');
setPositionId('');
setQuestionCount(5);
+ setSelectedDifficulty(3);
} catch (e) {
console.error('질문 생성 오류:', e);
- if (e.response?.status === 404) {
+ if (e.response?.status === 401) {
+ localStorage.clear();
+ nav('/signin');
+ return;
+ } else if (e.response?.status === 404) {
alert('이력서를 먼저 작성해주세요.');
} else {
- // 대체 방안: 기존 질문 풀에서 랜덤하게 가져오기
- try {
- const { data: searchData } = await api.get('/api/interviews/questions/search', {
- params: { size: questionCount }
- });
-
- if (searchData.data?.content && searchData.data.content.length > 0) {
- alert(`기본 질문 ${searchData.data.content.length}개를 준비했습니다!`);
- await fetchQuestions();
- setShowModal(false);
- } else {
- alert('질문 생성에 실패했습니다. 잠시 후 다시 시도해주세요.');
- }
- } catch (searchErr) {
- alert('질문 생성에 실패했습니다.');
- }
+ alert('질문 생성에 실패했습니다. 잠시 후 다시 시도해주세요.');
}
} finally {
setModalBusy(false);
}
};
- /* ── 화면에 보여줄 질문 after fav 필터 ── */
- const list = questions.filter(q => (onlyFav ? q.isFavorite : true));
+ /* ── 화면에 보여줄 질문 ── */
+ const list = questions;
+
+ if (!isAuthenticated) {
+ return (
+
+ );
+ }
/* ─────── JSX ─────── */
return (
@@ -238,7 +331,12 @@ function Questions() {
- 예상 질문 리스트
+
+
예상 질문 리스트
+
+ 사용자: {userInfo?.email} (ID: {userInfo?.id})
+
+
{/* 필터 & 새 질문 생성 버튼 */}
@@ -296,6 +394,7 @@ function Questions() {
{q.category}
난이도 {q.difficultyLevel}
+
{q.type}
{q.answer && (
diff --git a/FE/src/utils/api.jsx b/FE/src/utils/api.jsx
index a5f3622..e8a56e9 100644
--- a/FE/src/utils/api.jsx
+++ b/FE/src/utils/api.jsx
@@ -10,87 +10,208 @@ const api = axios.create({
});
/* ------------------------------------------------------------------ */
-/* 공통 인터셉터 – Access-Token 자동 부착 & 401 → refresh 로직 */
+/* 향상된 인증 인터셉터 */
/* ------------------------------------------------------------------ */
api.interceptors.request.use((cfg) => {
- const token = localStorage.getItem('accessToken');
- if (token) cfg.headers.Authorization = `Bearer ${token}`;
+ // 토큰 확인 및 자동 부착
+ const token = localStorage.getItem('accessToken') || localStorage.getItem('token');
+ if (token) {
+ cfg.headers.Authorization = `Bearer ${token}`;
+ console.log('API 요청에 토큰 부착:', cfg.url);
+ } else {
+ console.warn('토큰이 없습니다:', cfg.url);
+ }
return cfg;
});
api.interceptors.response.use(
(res) => res,
async (err) => {
- if (
- err.response?.status === 401 &&
- !err.config._retry &&
- localStorage.getItem('refreshToken')
- ) {
- err.config._retry = true;
- try {
- const { data } = await axios.post(`${API_BASE_URL}/api/auth/refresh`, {
- refreshToken: localStorage.getItem('refreshToken'),
- });
- const accessToken = data.data.accessToken;
- localStorage.setItem('accessToken', accessToken);
- err.config.headers.Authorization = `Bearer ${accessToken}`;
- return api(err.config);
- } catch (_) {
- localStorage.removeItem('accessToken');
- localStorage.removeItem('refreshToken');
- window.location.href = '/signin';
+ const originalRequest = err.config;
+
+ // 401 오류 처리
+ if (err.response?.status === 401 && !originalRequest._retry) {
+ originalRequest._retry = true;
+
+ const refreshToken = localStorage.getItem('refreshToken');
+
+ if (refreshToken) {
+ try {
+ console.log('토큰 갱신 시도...');
+ const { data } = await axios.post(`${API_BASE_URL}/api/auth/refresh`, {
+ refreshToken: refreshToken,
+ });
+
+ const newAccessToken = data.data.accessToken;
+ localStorage.setItem('accessToken', newAccessToken);
+ localStorage.setItem('token', newAccessToken); // 호환성
+
+ originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;
+ console.log('토큰 갱신 성공');
+ return api(originalRequest);
+ } catch (refreshError) {
+ console.error('토큰 갱신 실패:', refreshError);
+ }
}
+
+ // 토큰 갱신 실패 또는 리프레시 토큰 없음
+ console.log('인증 실패 - 로그인 필요');
+ localStorage.clear();
+ window.location.href = '/signin';
}
+
return Promise.reject(err);
},
);
/* ------------------------------------------------------------------ */
-/* Interview */
+/* 인증 상태 확인 유틸리티 */
/* ------------------------------------------------------------------ */
-export const createInterview = (payload) =>
- api.post('/api/interviews', payload); // 인증 토큰으로 사용자 식별
+export const checkAuthStatus = () => {
+ const token = localStorage.getItem('accessToken') || localStorage.getItem('token');
+ const userId = localStorage.getItem('userId');
+
+ return {
+ isAuthenticated: !!(token && userId),
+ token,
+ userId,
+ userEmail: localStorage.getItem('userEmail')
+ };
+};
+
+export const clearAuthData = () => {
+ localStorage.removeItem('accessToken');
+ localStorage.removeItem('token');
+ localStorage.removeItem('refreshToken');
+ localStorage.removeItem('userId');
+ localStorage.removeItem('userEmail');
+};
-export const startInterview = (interviewId) =>
- api.post(`/api/interviews/${interviewId}/start`);
-
-export const getInterviewQuestions = (interviewId) =>
- api.get(`/api/interviews/${interviewId}/questions`);
-
-export const getNextQuestion = (interviewId) =>
- api.get(`/api/interviews/${interviewId}/next-question`);
-
-export const submitAnswer = (questionId, body) =>
- api.post(`/api/interviews/questions/${questionId}/answer`, body);
-
-export const uploadAudioAnswer = (questionId, formData) =>
- api.post(`/api/interviews/questions/${questionId}/answer/audio`, formData, {
+/* ------------------------------------------------------------------ */
+/* Interview API */
+/* ------------------------------------------------------------------ */
+export const createInterview = (payload) => {
+ console.log('Creating interview with payload:', payload);
+ return api.post('/api/interviews', payload);
+};
+
+export const startInterview = (interviewId) => {
+ console.log('Starting interview:', interviewId);
+ return api.post(`/api/interviews/${interviewId}/start`);
+};
+
+export const getInterviewQuestions = (interviewId) => {
+ console.log('Getting interview questions for:', interviewId);
+ return api.get(`/api/interviews/${interviewId}/questions`);
+};
+
+export const getNextQuestion = (interviewId) => {
+ console.log('Getting next question for interview:', interviewId);
+ return api.get(`/api/interviews/${interviewId}/next-question`);
+};
+
+export const submitAnswer = (questionId, body) => {
+ console.log('Submitting answer for question:', questionId, body);
+ return api.post(`/api/interviews/questions/${questionId}/answer`, body);
+};
+
+export const uploadAudioAnswer = (questionId, formData) => {
+ console.log('Uploading audio answer for question:', questionId);
+ return api.post(`/api/interviews/questions/${questionId}/answer/audio`, formData, {
headers: { 'Content-Type': 'multipart/form-data' },
});
+};
-export const completeInterview = (interviewId) =>
- api.post(`/api/interviews/${interviewId}/complete`);
+export const completeInterview = (interviewId) => {
+ console.log('Completing interview:', interviewId);
+ return api.post(`/api/interviews/${interviewId}/complete`);
+};
-export const updateInterviewTime = (interviewId, timeData) =>
- api.post(`/api/interviews/${interviewId}/time`, timeData);
+export const updateInterviewTime = (interviewId, timeData) => {
+ console.log('Updating interview time:', interviewId, timeData);
+ return api.post(`/api/interviews/${interviewId}/time`, timeData);
+};
/* ------------------------------------------------------------------ */
-/* Question 검색 & 즐겨찾기 */
+/* Question 검색 & 즐겨찾기 (인증 필수) */
/* ------------------------------------------------------------------ */
-export const searchQuestions = (params) =>
- api.get('/api/interviews/questions/search', { params });
+export const searchQuestions = (params) => {
+ console.log('Searching questions with params:', params);
+ const authStatus = checkAuthStatus();
+ if (!authStatus.isAuthenticated) {
+ throw new Error('Authentication required');
+ }
+ return api.get('/api/interviews/questions/search', { params });
+};
+
+export const toggleFavoriteQuestion = (questionId) => {
+ console.log('Toggling favorite for question:', questionId);
+ const authStatus = checkAuthStatus();
+ if (!authStatus.isAuthenticated) {
+ throw new Error('Authentication required');
+ }
+ return api.post(`/api/interviews/questions/${questionId}/favorite`);
+};
+
+export const getFavoriteQuestions = () => {
+ console.log('Getting favorite questions');
+ const authStatus = checkAuthStatus();
+ if (!authStatus.isAuthenticated) {
+ throw new Error('Authentication required');
+ }
+ return api.get('/api/interviews/questions/favorites');
+};
-export const toggleFavoriteQuestion = (questionId) =>
- api.post(`/api/interviews/questions/${questionId}/favorite`);
+/* ------------------------------------------------------------------ */
+/* Resume & Position */
+/* ------------------------------------------------------------------ */
+export const getMyResume = () => {
+ console.log('Getting my resume');
+ return api.get('/api/resume');
+};
+
+export const resumeExists = () => {
+ console.log('Checking if resume exists');
+ return api.get('/api/resume/exists');
+};
+
+export const listCompanies = () => {
+ console.log('Getting companies list');
+ return api.get('/api/companies');
+};
+
+export const listPositions = (companyId) => {
+ console.log('Getting positions for company:', companyId);
+ return api.get(`/api/companies/${companyId}/positions`);
+};
/* ------------------------------------------------------------------ */
-/* Resume & Position (면접 생성 시 필수 ID) */
+/* Auth 관련 (대체 방법) */
/* ------------------------------------------------------------------ */
-export const getMyResume = () => api.get('/api/resume'); // 존재 시 200
-export const resumeExists = () => api.get('/api/resume/exists'); // true / false
+export const getCurrentUser = () => {
+ console.log('Getting current user via localStorage');
+ const authStatus = checkAuthStatus();
+
+ if (!authStatus.isAuthenticated) {
+ throw new Error('User not authenticated');
+ }
+
+ // localStorage 기반으로 사용자 정보 반환
+ return Promise.resolve({
+ data: {
+ data: {
+ id: authStatus.userId,
+ email: authStatus.userEmail,
+ isAuthenticated: true
+ }
+ }
+ });
+};
-export const listCompanies = () => api.get('/api/companies'); // 선택지 표시용
-export const listPositions = (companyId) =>
- api.get(`/api/companies/${companyId}/positions`);
+// 실제 서버 API 호출 (작동하는 경우)
+export const getCurrentUserFromAPI = () => {
+ console.log('Getting current user from API');
+ return api.get('/api/auth/me');
+};
export default api;
\ No newline at end of file