From 2325d8ca405103c91d0d51aa4e14ceedb93b5d8e Mon Sep 17 00:00:00 2001 From: ChoiWonkeun Date: Mon, 1 Dec 2025 02:40:33 +0900 Subject: [PATCH] Frontend: prepare production-ready version for deployment --- .dockerignore | 37 ------------- .gitignore | 3 + src/App.jsx | 1 - src/api/codingService.js | 34 ++++-------- src/api/interviewService.js | 16 +++--- src/api/reviewService.js | 7 ++- src/features/auth/GithubCallback.jsx | 1 - src/features/auth/GithubLoginButton.jsx | 7 ++- src/features/auth/Login.jsx | 4 -- src/features/codingTest/CodingTest.jsx | 67 ++++------------------- src/features/interview/pages/Result.jsx | 2 - src/features/interview/pages/Session.jsx | 2 - src/features/review/CodeReview.jsx | 1 - src/shared/hooks/useParticlesInit.js | 1 - tailwind.config.cjs => tailwind.config.js | 13 +++-- vercel.json | 5 ++ vite.config.js | 31 ++++++----- 17 files changed, 71 insertions(+), 161 deletions(-) rename tailwind.config.cjs => tailwind.config.js (58%) create mode 100644 vercel.json diff --git a/.dockerignore b/.dockerignore index ca56b08..3bed94d 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,40 +1,3 @@ -네, default.conf 파일 잘 받았습니다. - -파일을 보니... expires -1; 설정이 있어서, Nginx가 JS/CSS 파일을 캐시하는 문제는 아니었습니다. - -하지만 드디어 진짜 원인을 찾은 것 같습니다. 님이 겪는 문제는 두 가지의 심각한 오류가 동시에 발생하고 있었기 때문입니다. - -화면이 안 바뀌는 문제: web 컨테이너의 .dockerignore 파일에 Vite 캐시(.vite)가 누락되어, 님이 수정한 Review.jsx가 아닌 옛날 파일로 계속 빌드되었습니다. - -"Failed to fetch" 문제: Nginx와 FastAPI의 API 주소 끝에 슬래시(/)가 일치하지 않아 API 요청이 404 오류로 실패하고 있었습니다. - -🛠️ 최종 해결 (1+2번 문제 동시 해결) -아래 4단계를 순서대로 진행하시면, 디자인과 API 오류가 모두 해결됩니다. - -1단계: review-service의 API 경로 수정 -FastAPI(main.py)가 Nginx(default.conf)와 동일하게 슬래시가 붙은 주소를 받도록 수정합니다. - -apps/review-service/main.py 파일을 열어서 @app.post 부분을 수정하세요. - -수정 전: - -Python - -@app.post("/api/review") -async def handle_code_review(code: str = Form(...)): -수정 후: (끝에 / 추가) - -Python - -@app.post("/api/review/") -async def handle_code_review(code: str = Form(...)): -2단계: web의 .dockerignore 파일 수정 -Vite 캐시 폴더(.vite)가 Docker 빌드 시 복사되지 않도록 .dockerignore 파일에 추가합니다. - -apps/web/.dockerignore 파일을 열어서 맨 아래에 .vite를 추가하세요. - -수정 후: - # 기본 무시 항목 node_modules dist diff --git a/.gitignore b/.gitignore index a547bf3..8dea234 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,6 @@ dist-ssr *.njsproj *.sln *.sw? + +# Environment variables (🔒 절대 Git에 올리면 안 되는 민감 정보) +.env diff --git a/src/App.jsx b/src/App.jsx index be249c2..889e59f 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,4 +1,3 @@ -// apps/web/src/App.jsx import { Routes, Route } from "react-router-dom"; // 맨 위 import 부분 diff --git a/src/api/codingService.js b/src/api/codingService.js index 4d3f2a2..48f5732 100644 --- a/src/api/codingService.js +++ b/src/api/codingService.js @@ -1,10 +1,7 @@ -// src/api/codingService.js - -/** - * Vite 프록시 설정을 통해 백엔드(Spring) API 서버와 통신합니다. - * vite.config.js에서 '/api' → http://localhost:8080 같은 식으로 프록시된다고 가정합니다. - */ -const BASE_URL = "/api"; +const BASE_URL = + import.meta.env.VITE_API_BASE_URL + ? import.meta.env.VITE_API_BASE_URL.replace(/\/+$/, "") // 끝에 / 제거 + : "/api"; /** * 난이도에 따라 랜덤 코딩 문제를 가져옵니다. @@ -26,21 +23,20 @@ export const fetchRandomProblem = async (difficulty) => { /** * 코드를 백엔드로 제출하여 전체 테스트 케이스로 채점합니다. - * (Spring의 /api/coding/submissions 엔드포인트 호출) * * @param {object} params - * @param {number} params.problemId - 문제 ID - * @param {string} params.code - 사용자 코드 - * @param {string} params.language - 언어 ("python" | "java" | "cpp" ...) - * @param {string} [params.userId] - 사용자 식별자 (없으면 "guest") - * @returns {Promise} - { submissionId, status, score, passedCount, totalCount, message } + * @param {number} params.problemId + * @param {string} params.code + * @param {string} params.language + * @param {string} [params.userId] + * @returns {Promise} */ export const submitCode = async ({ problemId, code, language, userId }) => { const payload = { - problemId, + problemId, sourceCode: code, language, - userId: userId ?? "guest" + userId: userId ?? "guest", }; const response = await fetch(`${BASE_URL}/coding/submissions`, { @@ -58,11 +54,3 @@ export const submitCode = async ({ problemId, code, language, userId }) => { return await response.json(); }; - -/** - * (선택) 코드 실행(run) 기능을 나중에 붙이고 싶다면 여기에 구현할 수 있습니다. - * 현재 Spring 백엔드에는 /coding/run 같은 엔드포인트가 없어서 기본적으로는 사용하지 않습니다. - */ -// export const runCode = async (code, language, inputData) => { -// // TODO: 나중에 Judge0/Piston 실행용 엔드포인트 만들면 여기에 연결 -// }; diff --git a/src/api/interviewService.js b/src/api/interviewService.js index c8b1c62..3e60163 100644 --- a/src/api/interviewService.js +++ b/src/api/interviewService.js @@ -1,10 +1,11 @@ -// src/api/interviewService.js - -const BASE_URL = "/api"; +const BASE_URL = + import.meta.env.VITE_API_BASE_URL + ? import.meta.env.VITE_API_BASE_URL.replace(/\/+$/, "") + : "/api"; /** * 새 AI 면접 세션 시작 (레포 URL 기반 질문 생성) - * -> POST /api/interview/start + * -> POST /interview/start */ export const startInterview = async (repoUrl) => { const res = await fetch(`${BASE_URL}/interview/start`, { @@ -22,9 +23,8 @@ export const startInterview = async (repoUrl) => { }; /** - * 모든 질문에 대한 답변(STT 전환된 텍스트 포함)을 전달하고 - * 최종 면접 결과 피드백을 받음 - * -> POST /api/interview/feedback + * 모든 질문 답변(STT 포함) 전달하고 최종 피드백 받기 + * -> POST /interview/feedback */ export const requestInterviewFeedback = async (sessionId, answers) => { const res = await fetch(`${BASE_URL}/interview/feedback`, { @@ -43,7 +43,7 @@ export const requestInterviewFeedback = async (sessionId, answers) => { /** * 음성 Blob → 텍스트(STT) - * -> POST /api/interview/stt + * -> POST /interview/stt */ export const transcribeAnswer = async (audioBlob) => { const formData = new FormData(); diff --git a/src/api/reviewService.js b/src/api/reviewService.js index 3f73cd1..afef274 100644 --- a/src/api/reviewService.js +++ b/src/api/reviewService.js @@ -1,5 +1,7 @@ -// src/api/reviewService.js -const BASE_URL = "http://localhost:8080/api"; +const BASE_URL = + import.meta.env.VITE_API_BASE_URL + ? import.meta.env.VITE_API_BASE_URL.replace(/\/+$/, "") + : "/api"; export const fetchCodeReview = async (code, comment, repoUrl) => { const payload = { @@ -37,6 +39,7 @@ export const fetchCodeReview = async (code, comment, repoUrl) => { try { return JSON.parse(raw); } catch { + // 응답이 순수 텍스트일 때 return { review: raw, questions: [] }; } } catch (error) { diff --git a/src/features/auth/GithubCallback.jsx b/src/features/auth/GithubCallback.jsx index 36c5c96..89ce37a 100644 --- a/src/features/auth/GithubCallback.jsx +++ b/src/features/auth/GithubCallback.jsx @@ -1,4 +1,3 @@ -// src/features/auth/GithubCallback.jsx import { useEffect } from "react"; import { useLocation, useNavigate } from "react-router-dom"; diff --git a/src/features/auth/GithubLoginButton.jsx b/src/features/auth/GithubLoginButton.jsx index 7a77fa7..a64d0af 100644 --- a/src/features/auth/GithubLoginButton.jsx +++ b/src/features/auth/GithubLoginButton.jsx @@ -1,9 +1,10 @@ -// src/features/auth/GithubLoginButton.jsx -const API_BASE_URL = import.meta.env.VITE_API_BASE_URL; +const API_BASE_URL = + import.meta.env.VITE_API_BASE_URL + ? import.meta.env.VITE_API_BASE_URL.replace(/\/+$/, "") + : "/api"; export default function GithubLoginButton() { const handleGithubLogin = () => { - // 백엔드 OAuth 진입점 (나중에 실제 엔드포인트로만 바꿔주면 됨) window.location.href = `${API_BASE_URL}/oauth2/authorization/github`; }; diff --git a/src/features/auth/Login.jsx b/src/features/auth/Login.jsx index 2f40349..ac473c8 100644 --- a/src/features/auth/Login.jsx +++ b/src/features/auth/Login.jsx @@ -1,4 +1,3 @@ -// src/features/auth/Login.jsx import GithubLoginButton from "./GithubLoginButton"; export default function Login() { @@ -7,9 +6,6 @@ export default function Login() {
Sign in
- - {/* 나중에 이메일 로그인 넣고 싶으면 여기 */} -
diff --git a/src/features/codingTest/CodingTest.jsx b/src/features/codingTest/CodingTest.jsx index 393bf32..3f605b9 100644 --- a/src/features/codingTest/CodingTest.jsx +++ b/src/features/codingTest/CodingTest.jsx @@ -1,4 +1,3 @@ -// src/features/coding/CodingTest.jsx import { useState } from "react"; import { Link } from "react-router-dom"; import { @@ -13,58 +12,14 @@ import { ChevronRight, Maximize2, Home, - Code2 + Code2, } from "lucide-react"; -// ----------------------------------------------------------- -// [오류 수정] codingService.js 파일을 직접 통합하여 경로 오류 해결 -// ----------------------------------------------------------- - -const BASE_URL = "/api"; - -const fetchRandomProblem = async (difficulty) => { - const query = difficulty ? `?difficulty=${difficulty}` : ""; - // API 경로: /api/coding/problems/random - const response = await fetch(`${BASE_URL}/coding/problems/random${query}`); - - if (!response.ok) { - const text = await response.text().catch(() => ""); - throw new Error( - text || `랜덤 문제를 불러오지 못했습니다. (status: ${response.status})` - ); - } - - return await response.json(); -}; - -const submitCode = async ({ problemId, code, language, userId }) => { - const payload = { - problemId, - sourceCode: code, - language, - userId: userId ?? 1, // userId가 null/undefined일 경우 기본값 1 사용 (Long 타입 일치) - }; - - // API 경로: /api/coding/submissions - const response = await fetch(`${BASE_URL}/coding/submissions`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(payload), - }); - - if (!response.ok) { - const text = await response.text().catch(() => ""); - throw new Error(text || `채점 요청에 실패했습니다. (status: ${response.status})`); - } - - return await response.json(); -}; - -// ----------------------------------------------------------- -// 컴포넌트 시작 -// ----------------------------------------------------------- +// ✅ API 호출은 분리된 서비스에서 import +import { + fetchRandomProblem, + submitCode, +} from "../../api/codingService"; // 언어 옵션 const LANGUAGE_OPTIONS = [ @@ -159,7 +114,7 @@ export default function CodingTest() { if (code !== LANGUAGE_TEMPLATES[language] && code.trim() !== "") { if ( !window.confirm( - "언어를 변경하면 작성 중인 코드가 초기화됩니다. 계속하시겠습니까?" + "언어를 변경하면 작성 중인 코드가 초기화됩니다. 계속하시겠습니까?", ) ) { return; @@ -181,7 +136,7 @@ export default function CodingTest() { setProblem(data); } catch (err) { setErrorMsg( - err?.message || "문제 로딩 중 오류가 발생했습니다. (백엔드 서버 확인 필요)" + err?.message || "문제 로딩 중 오류가 발생했습니다. (백엔드 서버 확인 필요)", ); } finally { setIsLoadingProblem(false); @@ -209,7 +164,7 @@ export default function CodingTest() { problemId: problem.id, code, language, - userId: 1, // Long 타입이므로 숫자 1 사용 + // userId는 codingService에서 guest 처리 (또는 나중에 로그인 정보 연결) }); setResult(res); // showInterview는 기본 false (코드 리뷰 먼저 보여줌) @@ -548,7 +503,7 @@ export default function CodingTest() { - {/* 🔥 오른쪽: 예상 면접 질문 토글 버튼 (결과 요약 버튼 삭제) */} + {/* 🔥 오른쪽: 예상 면접 질문 토글 버튼 */} {Array.isArray(result.interviewQuestions) && result.interviewQuestions.length > 0 && (