diff --git a/.env.production b/.env.production new file mode 100644 index 0000000..bc1a18e --- /dev/null +++ b/.env.production @@ -0,0 +1 @@ +VITE_API_BASE_URL=https://api.skill-boost.store \ No newline at end of file diff --git a/README.md b/README.md index e61c9d1..f11d3d6 100644 --- a/README.md +++ b/README.md @@ -27,3 +27,8 @@ http://localhost:8080 ํ”„๋ก ํŠธ: http://localhost:3000 + +๐Ÿ”ต 4. ์Œ์„ฑ ์ธ์‹ ๋ชจ๋ธ(Vosk) ์„ค์น˜ - vosk-model-small-ko-0.22 + +https://alphacephei.com/vosk/models + diff --git a/src/App.jsx b/src/App.jsx index 889e59f..87a0487 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,6 +1,6 @@ import { Routes, Route } from "react-router-dom"; -// ๋งจ ์œ„ import ๋ถ€๋ถ„ +// Auth import Login from "@/features/auth/Login"; import GithubCallback from "@/features/auth/GithubCallback"; @@ -17,7 +17,7 @@ import Result from "./features/interview/pages/Result"; export default function App() { return ( - {/* ๊ธฐ๋ณธ */} + {/* ๊ธฐ๋ณธ ํ™ˆ */} } /> {/* ์ฝ”๋”ฉํ…Œ์ŠคํŠธ */} @@ -30,8 +30,12 @@ export default function App() { } /> } /> } /> + + {/* ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€ */} } /> - } /> + + {/* ๐Ÿ”ฅ GitHub OAuth ์ฝœ๋ฐฑ (๋ฐฑ์—”๋“œ์—์„œ http://localhost:3000/oauth2/redirect ๋กœ ๋ณด๋ƒ„) */} + } /> {/* 404 */} Not Found} /> diff --git a/src/api/codingService.js b/src/api/codingService.js index 48f5732..171ca1e 100644 --- a/src/api/codingService.js +++ b/src/api/codingService.js @@ -1,55 +1,58 @@ -const BASE_URL = - import.meta.env.VITE_API_BASE_URL - ? import.meta.env.VITE_API_BASE_URL.replace(/\/+$/, "") // ๋์— / ์ œ๊ฑฐ - : "/api"; +const BASE_URL = import.meta.env.VITE_API_BASE_URL; /** * ๋‚œ์ด๋„์— ๋”ฐ๋ผ ๋žœ๋ค ์ฝ”๋”ฉ ๋ฌธ์ œ๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค. - * - * @param {"EASY"|"MEDIUM"|"HARD"|undefined} difficulty - * @returns {Promise} - { id, title, description, difficulty, tags, samples: [...] } */ export const fetchRandomProblem = async (difficulty) => { + const accessToken = localStorage.getItem("accessToken"); + const query = difficulty ? `?difficulty=${difficulty}` : ""; - const response = await fetch(`${BASE_URL}/coding/problems/random${query}`); + const response = await fetch( + `${BASE_URL}/api/coding/problems/random${query}`, + { + headers: { + ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}), + }, + } + ); if (!response.ok) { const text = await response.text().catch(() => ""); - throw new Error(text || `๋žœ๋ค ๋ฌธ์ œ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค. (status: ${response.status})`); + throw new Error( + text || `๋žœ๋ค ๋ฌธ์ œ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค. (status: ${response.status})` + ); } return await response.json(); }; /** - * ์ฝ”๋“œ๋ฅผ ๋ฐฑ์—”๋“œ๋กœ ์ œ์ถœํ•˜์—ฌ ์ „์ฒด ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค๋กœ ์ฑ„์ ํ•ฉ๋‹ˆ๋‹ค. - * - * @param {object} params - * @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 accessToken = localStorage.getItem("accessToken"); + const payload = { problemId, sourceCode: code, language, - userId: userId ?? "guest", + userId: userId ?? 1, }; - const response = await fetch(`${BASE_URL}/coding/submissions`, { + const response = await fetch(`${BASE_URL}/api/coding/submissions`, { method: "POST", headers: { "Content-Type": "application/json", + ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}), }, body: JSON.stringify(payload), }); if (!response.ok) { const text = await response.text().catch(() => ""); - throw new Error(text || `์ฑ„์  ์š”์ฒญ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. (status: ${response.status})`); + throw new Error( + text || `์ฑ„์  ์š”์ฒญ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. (status: ${response.status})` + ); } return await response.json(); diff --git a/src/api/interviewService.js b/src/api/interviewService.js index 3e60163..9a72147 100644 --- a/src/api/interviewService.js +++ b/src/api/interviewService.js @@ -1,16 +1,17 @@ -const BASE_URL = - import.meta.env.VITE_API_BASE_URL - ? import.meta.env.VITE_API_BASE_URL.replace(/\/+$/, "") - : "/api"; +const BASE_URL = import.meta.env.VITE_API_BASE_URL; /** - * ์ƒˆ AI ๋ฉด์ ‘ ์„ธ์…˜ ์‹œ์ž‘ (๋ ˆํฌ URL ๊ธฐ๋ฐ˜ ์งˆ๋ฌธ ์ƒ์„ฑ) - * -> POST /interview/start + * ์ƒˆ AI ๋ฉด์ ‘ ์„ธ์…˜ ์‹œ์ž‘ */ export const startInterview = async (repoUrl) => { - const res = await fetch(`${BASE_URL}/interview/start`, { + const accessToken = localStorage.getItem("accessToken"); + + const res = await fetch(`${BASE_URL}/api/interview/start`, { method: "POST", - headers: { "Content-Type": "application/json" }, + headers: { + "Content-Type": "application/json", + ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}), + }, body: JSON.stringify({ repoUrl }), }); @@ -19,17 +20,21 @@ export const startInterview = async (repoUrl) => { throw new Error(err.error || "AI ๋ฉด์ ‘ ์„ธ์…˜ ์‹œ์ž‘์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."); } - return res.json(); // { sessionId, durationSec, questions } + return res.json(); }; /** - * ๋ชจ๋“  ์งˆ๋ฌธ ๋‹ต๋ณ€(STT ํฌํ•จ) ์ „๋‹ฌํ•˜๊ณ  ์ตœ์ข… ํ”ผ๋“œ๋ฐฑ ๋ฐ›๊ธฐ - * -> POST /interview/feedback + * ์ตœ์ข… ํ”ผ๋“œ๋ฐฑ ์š”์ฒญ */ export const requestInterviewFeedback = async (sessionId, answers) => { - const res = await fetch(`${BASE_URL}/interview/feedback`, { + const accessToken = localStorage.getItem("accessToken"); + + const res = await fetch(`${BASE_URL}/api/interview/feedback`, { method: "POST", - headers: { "Content-Type": "application/json" }, + headers: { + "Content-Type": "application/json", + ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}), + }, body: JSON.stringify({ sessionId, answers }), }); @@ -42,15 +47,19 @@ export const requestInterviewFeedback = async (sessionId, answers) => { }; /** - * ์Œ์„ฑ Blob โ†’ ํ…์ŠคํŠธ(STT) - * -> POST /interview/stt + * ์Œ์„ฑ STT */ export const transcribeAnswer = async (audioBlob) => { + const accessToken = localStorage.getItem("accessToken"); + const formData = new FormData(); formData.append("audio", audioBlob); - const res = await fetch(`${BASE_URL}/interview/stt`, { + const res = await fetch(`${BASE_URL}/api/interview/stt`, { method: "POST", + headers: { + ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}), + }, body: formData, }); @@ -59,6 +68,6 @@ export const transcribeAnswer = async (audioBlob) => { throw new Error(err.error || "์Œ์„ฑ ์ธ์‹์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."); } - const data = await res.json(); // { text: "..." } + const data = await res.json(); return data.text || ""; }; diff --git a/src/api/reviewService.js b/src/api/reviewService.js index afef274..62bcb75 100644 --- a/src/api/reviewService.js +++ b/src/api/reviewService.js @@ -1,26 +1,32 @@ -const BASE_URL = - import.meta.env.VITE_API_BASE_URL - ? import.meta.env.VITE_API_BASE_URL.replace(/\/+$/, "") - : "/api"; +const BASE_URL = import.meta.env.VITE_API_BASE_URL; export const fetchCodeReview = async (code, comment, repoUrl) => { + const accessToken = localStorage.getItem("accessToken"); + const payload = { - code: code, - comment: comment && comment.trim() ? comment.trim() : null, - repoUrl: repoUrl && repoUrl.trim() ? repoUrl.trim() : null, + code, + comment: comment?.trim() || null, + repoUrl: repoUrl?.trim() || null, }; try { - const res = await fetch(`${BASE_URL}/review`, { + const res = await fetch(`${BASE_URL}/api/review`, { method: "POST", headers: { "Content-Type": "application/json", + ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}), }, body: JSON.stringify(payload), }); const raw = await res.text(); + if (res.status === 401 || res.status === 403) { + throw new Error( + "๋กœ๊ทธ์ธ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. GitHub ๋กœ๊ทธ์ธ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด ์ฃผ์„ธ์š”." + ); + } + if (!res.ok) { try { const errJson = JSON.parse(raw); diff --git a/src/features/auth/GithubLoginButton.jsx b/src/features/auth/GithubLoginButton.jsx index a64d0af..1f25c69 100644 --- a/src/features/auth/GithubLoginButton.jsx +++ b/src/features/auth/GithubLoginButton.jsx @@ -1,19 +1,28 @@ -const API_BASE_URL = - import.meta.env.VITE_API_BASE_URL - ? import.meta.env.VITE_API_BASE_URL.replace(/\/+$/, "") - : "/api"; +import { IconBrandGithub } from "@tabler/icons-react"; + +// ํ™˜๊ฒฝ ๋ณ€์ˆ˜์—์„œ ๋ฐฑ์—”๋“œ BASE URL ๊ฐ€์ ธ์˜ค๊ธฐ +const BASE_URL = import.meta.env.VITE_API_BASE_URL; export default function GithubLoginButton() { const handleGithubLogin = () => { - window.location.href = `${API_BASE_URL}/oauth2/authorization/github`; + // OAuth2 ๋กœ๊ทธ์ธ ์—”๋“œํฌ์ธํŠธ + window.location.href = `${BASE_URL}/oauth2/authorization/github`; }; return ( ); } diff --git a/src/features/auth/Login.jsx b/src/features/auth/Login.jsx index ac473c8..29320ff 100644 --- a/src/features/auth/Login.jsx +++ b/src/features/auth/Login.jsx @@ -2,13 +2,55 @@ import GithubLoginButton from "./GithubLoginButton"; export default function Login() { return ( -
-
-
-
Sign in
- +
+
+ +
+
+
+ + +
+

+ Master Your Code +

+

+ AI-powered code reviews, mock interviews,
and technical testing. +

+
+
+ +
+ +
+ +
+ +
+ + Developer Focused + +
+
+ + {/* ๋งํฌ ์ œ๊ฑฐ ๋ฐ ๋‹จ์ˆœ ์นดํ”ผ๋ผ์ดํŠธ ํ…์ŠคํŠธ */} +

+ ยฉ 2025 SkillBoost. All rights reserved. +

+
); -} +} \ No newline at end of file diff --git a/src/features/codingTest/CodingTest.jsx b/src/features/codingTest/CodingTest.jsx index 3f605b9..3a9f999 100644 --- a/src/features/codingTest/CodingTest.jsx +++ b/src/features/codingTest/CodingTest.jsx @@ -15,11 +15,7 @@ import { Code2, } from "lucide-react"; -// โœ… API ํ˜ธ์ถœ์€ ๋ถ„๋ฆฌ๋œ ์„œ๋น„์Šค์—์„œ import -import { - fetchRandomProblem, - submitCode, -} from "../../api/codingService"; +import { fetchRandomProblem, submitCode } from "@/api/codingService"; // ์–ธ์–ด ์˜ต์…˜ const LANGUAGE_OPTIONS = [ @@ -167,7 +163,6 @@ export default function CodingTest() { // userId๋Š” codingService์—์„œ guest ์ฒ˜๋ฆฌ (๋˜๋Š” ๋‚˜์ค‘์— ๋กœ๊ทธ์ธ ์ •๋ณด ์—ฐ๊ฒฐ) }); setResult(res); - // showInterview๋Š” ๊ธฐ๋ณธ false (์ฝ”๋“œ ๋ฆฌ๋ทฐ ๋จผ์ € ๋ณด์—ฌ์คŒ) } catch (err) { setErrorMsg(err?.message || "์ฑ„์  ์„œ๋ฒ„ ํ†ต์‹  ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."); } finally { @@ -273,10 +268,7 @@ export default function CodingTest() { {/* ์—๋Ÿฌ ์•Œ๋ฆผ */} {errorMsg && (
- +

{errorMsg}

)} @@ -503,7 +495,7 @@ export default function CodingTest() {
- {/* ๐Ÿ”ฅ ์˜ค๋ฅธ์ชฝ: ์˜ˆ์ƒ ๋ฉด์ ‘ ์งˆ๋ฌธ ํ† ๊ธ€ ๋ฒ„ํŠผ */} + {/* ์˜ˆ์ƒ ๋ฉด์ ‘ ์งˆ๋ฌธ ํ† ๊ธ€ ๋ฒ„ํŠผ */} {Array.isArray(result.interviewQuestions) && result.interviewQuestions.length > 0 && (
); -} +} \ No newline at end of file diff --git a/src/shared/utils/formatters.jsx b/src/shared/utils/formatters.jsx index 6541412..dc74e8a 100644 --- a/src/shared/utils/formatters.jsx +++ b/src/shared/utils/formatters.jsx @@ -1,4 +1,3 @@ -// src/utils/formatters.js import React from 'react'; /**