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
1 change: 1 addition & 0 deletions .env.production
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
VITE_API_BASE_URL=https://api.skill-boost.store
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,8 @@ http://localhost:8080
프론트:

http://localhost:3000

🔵 4. 음성 인식 모델(Vosk) 설치 - vosk-model-small-ko-0.22

https://alphacephei.com/vosk/models

10 changes: 7 additions & 3 deletions src/App.jsx
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -17,7 +17,7 @@ import Result from "./features/interview/pages/Result";
export default function App() {
return (
<Routes>
{/* 기본 */}
{/* 기본 */}
<Route path="/" element={<Home />} />

{/* 코딩테스트 */}
Expand All @@ -30,8 +30,12 @@ export default function App() {
<Route path="/interview" element={<Intro />} />
<Route path="/interview/session" element={<Session />} />
<Route path="/interview/result" element={<Result />} />

{/* 로그인 페이지 */}
<Route path="/login" element={<Login />} />
<Route path="/oauth/github/callback" element={<GithubCallback />} />

{/* 🔥 GitHub OAuth 콜백 (백엔드에서 http://localhost:3000/oauth2/redirect 로 보냄) */}
<Route path="/oauth2/redirect" element={<GithubCallback />} />

{/* 404 */}
<Route path="*" element={<div style={{ padding: 24 }}>Not Found</div>} />
Expand Down
43 changes: 23 additions & 20 deletions src/api/codingService.js
Original file line number Diff line number Diff line change
@@ -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<object>} - { 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<object>}
* 코드 제출 & 채점
*/
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();
Expand Down
43 changes: 26 additions & 17 deletions src/api/interviewService.js
Original file line number Diff line number Diff line change
@@ -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 }),
});

Expand All @@ -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 }),
});

Expand All @@ -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,
});

Expand All @@ -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 || "";
};
22 changes: 14 additions & 8 deletions src/api/reviewService.js
Original file line number Diff line number Diff line change
@@ -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);
Expand Down
23 changes: 16 additions & 7 deletions src/features/auth/GithubLoginButton.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<button
onClick={handleGithubLogin}
className="btn-neon w-full py-2 mt-3 text-sm"
className="
w-full py-3 mt-4 flex items-center justify-center gap-2
rounded-xl font-semibold text-[15px]
bg-[#0d1117]/70 hover:bg-[#0d1117]/90
border border-white/10 backdrop-blur-lg
hover:shadow-[0_0_15px_rgba(255,255,255,0.25)]
transition-all duration-300
"
>
Login with GitHub
<IconBrandGithub size={20} className="text-white" />
<span className="text-white">Login with GitHub</span>
</button>
);
}
54 changes: 48 additions & 6 deletions src/features/auth/Login.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,55 @@ import GithubLoginButton from "./GithubLoginButton";

export default function Login() {
return (
<div className="bg-app min-h-screen flex items-center justify-center p-4">
<div className="gcard max-w-md w-full">
<div className="ginner glass-sheen p-6 space-y-4">
<div className="gheader text-base">Sign in</div>
<GithubLoginButton />
<div className="min-h-screen flex flex-col items-center justify-center bg-[#0D1117] text-white p-4 relative overflow-hidden">
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-full max-w-3xl h-80 bg-blue-500/10 blur-[100px] rounded-full pointer-events-none" />

<div className="w-full max-w-md z-10">
<div className="bg-[#161B22] border border-[#30363D] rounded-2xl shadow-2xl p-8 md:p-10">
<div className="flex flex-col items-center mb-8 text-center space-y-4">
<svg
height="48"
viewBox="0 0 16 16"
version="1.1"
width="48"
aria-hidden="true"
className="fill-white/90"
>
<path d="M8 0c4.42 0 8 3.58 8 8a8.013 8.013 0 0 1-5.45 7.59c-.4.08-.55-.17-.55-.38 0-.27.01-1.13.01-2.2 0-.75-.25-1.23-.54-1.48 1.78-.2 3.65-.88 3.65-3.95 0-.88-.31-1.59-.82-2.15.08-.2.36-1.02-.08-2.12 0 0-.67-.22-2.2.82-.64-.18-1.32-.27-2-.27-.68 0-1.36.09-2 .27-1.53-1.03-2.2-.82-2.2-.82-.44 1.1-.16 1.92-.08 2.12-.51.56-.82 1.28-.82 2.15 0 3.06 1.86 3.75 3.64 3.95-.23.2-.44.55-.51 1.07-.46.21-1.61.55-2.33-.66-.15-.24-.6-.83-1.23-.82-.67.01-.27.38.01.53.34.19.73.9.82 1.13.16.45.68 1.31 2.69.94 0 .67.01 1.3.01 1.49 0 .21-.15.45-.55.38A7.995 7.995 0 0 1 0 8c0-4.42 3.58-8 8-8Z"></path>
</svg>

<div>
<h1 className="text-2xl font-bold tracking-tight text-white">
Master Your Code
</h1>
<p className="mt-2 text-sm text-[#8B949E]">
AI-powered code reviews, mock interviews,<br />and technical testing.
</p>
</div>
</div>

<div className="w-full">
<GithubLoginButton />
</div>

<div className="relative mt-8">
<div className="absolute inset-0 flex items-center" aria-hidden="true">
<div className="w-full border-t border-[#30363D]"></div>
</div>
<div className="relative flex justify-center text-sm">
<span className="px-2 bg-[#161B22] text-[#8B949E] text-xs uppercase tracking-wide">
Developer Focused
</span>
</div>
</div>
</div>

{/* 링크 제거 및 단순 카피라이트 텍스트 */}
<p className="mt-8 text-center text-xs text-[#484F58]">
© 2025 SkillBoost. All rights reserved.
</p>

</div>
</div>
);
}
}
Loading