From 3ed21f79e7e1801ab3f48f8eadf884811bf8d235 Mon Sep 17 00:00:00 2001 From: wonkeun-choi Date: Mon, 1 Dec 2025 01:08:18 +0900 Subject: [PATCH 1/3] Update README.md --- README.md | 5 +++++ 1 file changed, 5 insertions(+) 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 + From 9c386cb7632853e729e0fa71128610a3bc4726a3 Mon Sep 17 00:00:00 2001 From: ChoiWonkeun Date: Wed, 3 Dec 2025 16:24:51 +0900 Subject: [PATCH 2/3] hotfix: api --- .env.production | 1 + src/App.jsx | 1 - src/api/codingService.js | 57 +++++++++------------ src/api/interviewService.js | 43 +++++++++------- src/api/reviewService.js | 20 +++++--- src/features/auth/GithubCallback.jsx | 1 - src/features/auth/GithubLoginButton.jsx | 22 +++++--- src/features/auth/Login.jsx | 56 ++++++++++++++++---- src/features/codingTest/CodingTest.jsx | 65 +++--------------------- src/features/home/Home.jsx | 3 +- src/features/interview/pages/Result.jsx | 2 - src/features/interview/pages/Session.jsx | 2 - src/features/review/CodeReview.jsx | 40 ++------------- src/shared/hooks/useParticlesInit.js | 1 - src/shared/utils/formatters.jsx | 1 - 15 files changed, 140 insertions(+), 175 deletions(-) create mode 100644 .env.production 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/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..171ca1e 100644 --- a/src/api/codingService.js +++ b/src/api/codingService.js @@ -1,68 +1,59 @@ -// 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; /** * ๋‚œ์ด๋„์— ๋”ฐ๋ผ ๋žœ๋ค ์ฝ”๋”ฉ ๋ฌธ์ œ๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค. - * - * @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(); }; /** - * ์ฝ”๋“œ๋ฅผ ๋ฐฑ์—”๋“œ๋กœ ์ œ์ถœํ•˜์—ฌ ์ „์ฒด ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค๋กœ ์ฑ„์ ํ•ฉ๋‹ˆ๋‹ค. - * (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 } + * ์ฝ”๋“œ ์ œ์ถœ & ์ฑ„์  */ export const submitCode = async ({ problemId, code, language, userId }) => { + const accessToken = localStorage.getItem("accessToken"); + const payload = { - problemId, + 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(); }; - -/** - * (์„ ํƒ) ์ฝ”๋“œ ์‹คํ–‰(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..9a72147 100644 --- a/src/api/interviewService.js +++ b/src/api/interviewService.js @@ -1,15 +1,17 @@ -// src/api/interviewService.js - -const BASE_URL = "/api"; +const BASE_URL = import.meta.env.VITE_API_BASE_URL; /** - * ์ƒˆ AI ๋ฉด์ ‘ ์„ธ์…˜ ์‹œ์ž‘ (๋ ˆํฌ URL ๊ธฐ๋ฐ˜ ์งˆ๋ฌธ ์ƒ์„ฑ) - * -> POST /api/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 }), }); @@ -18,18 +20,21 @@ export const startInterview = async (repoUrl) => { throw new Error(err.error || "AI ๋ฉด์ ‘ ์„ธ์…˜ ์‹œ์ž‘์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."); } - return res.json(); // { sessionId, durationSec, questions } + return res.json(); }; /** - * ๋ชจ๋“  ์งˆ๋ฌธ์— ๋Œ€ํ•œ ๋‹ต๋ณ€(STT ์ „ํ™˜๋œ ํ…์ŠคํŠธ ํฌํ•จ)์„ ์ „๋‹ฌํ•˜๊ณ  - * ์ตœ์ข… ๋ฉด์ ‘ ๊ฒฐ๊ณผ ํ”ผ๋“œ๋ฐฑ์„ ๋ฐ›์Œ - * -> POST /api/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 /api/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 3f73cd1..aa1d852 100644 --- a/src/api/reviewService.js +++ b/src/api/reviewService.js @@ -1,24 +1,32 @@ -// src/api/reviewService.js -const BASE_URL = "http://localhost:8080/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/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..1f25c69 100644 --- a/src/features/auth/GithubLoginButton.jsx +++ b/src/features/auth/GithubLoginButton.jsx @@ -1,18 +1,28 @@ -// src/features/auth/GithubLoginButton.jsx -const API_BASE_URL = import.meta.env.VITE_API_BASE_URL; +import { IconBrandGithub } from "@tabler/icons-react"; + +// ํ™˜๊ฒฝ ๋ณ€์ˆ˜์—์„œ ๋ฐฑ์—”๋“œ BASE URL ๊ฐ€์ ธ์˜ค๊ธฐ +const BASE_URL = import.meta.env.VITE_API_BASE_URL; export default function GithubLoginButton() { const handleGithubLogin = () => { - // ๋ฐฑ์—”๋“œ OAuth ์ง„์ž…์  (๋‚˜์ค‘์— ์‹ค์ œ ์—”๋“œํฌ์ธํŠธ๋กœ๋งŒ ๋ฐ”๊ฟ”์ฃผ๋ฉด ๋จ) - 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 2f40349..29320ff 100644 --- a/src/features/auth/Login.jsx +++ b/src/features/auth/Login.jsx @@ -1,18 +1,56 @@ -// src/features/auth/Login.jsx 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 393bf32..d55f52a 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,10 @@ 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(); -}; - -// ----------------------------------------------------------- -// ์ปดํฌ๋„ŒํŠธ ์‹œ์ž‘ -// ----------------------------------------------------------- +import { fetchRandomProblem, submitCode } from "@/api/codingService"; // ์–ธ์–ด ์˜ต์…˜ const LANGUAGE_OPTIONS = [ @@ -212,7 +163,6 @@ export default function CodingTest() { userId: 1, // Long ํƒ€์ž…์ด๋ฏ€๋กœ ์ˆซ์ž 1 ์‚ฌ์šฉ }); setResult(res); - // showInterview๋Š” ๊ธฐ๋ณธ false (์ฝ”๋“œ ๋ฆฌ๋ทฐ ๋จผ์ € ๋ณด์—ฌ์คŒ) } catch (err) { setErrorMsg(err?.message || "์ฑ„์  ์„œ๋ฒ„ ํ†ต์‹  ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."); } finally { @@ -318,10 +268,7 @@ export default function CodingTest() { {/* ์—๋Ÿฌ ์•Œ๋ฆผ */} {errorMsg && (
- +

{errorMsg}

)} @@ -548,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/hooks/useParticlesInit.js b/src/shared/hooks/useParticlesInit.js index d23a5b3..b8acbda 100644 --- a/src/shared/hooks/useParticlesInit.js +++ b/src/shared/hooks/useParticlesInit.js @@ -1,4 +1,3 @@ -// src/hooks/useParticlesInit.js import { useEffect, useState } from "react"; import { initParticlesEngine } from "@tsparticles/react"; import { loadAll } from "@tsparticles/all"; 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'; /** From c3d5cc6516f4cf899573a631fe29352defdeee02 Mon Sep 17 00:00:00 2001 From: ChoiWonkeun Date: Thu, 4 Dec 2025 01:30:13 +0900 Subject: [PATCH 3/3] Fix GitHub OAuth callback routing and update App.jsx --- src/App.jsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) 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
} />