From bdc78d24f5e7579d5c7f124411f664b5ca739882 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=9A=A9=EA=B7=9C?= Date: Tue, 20 May 2025 17:38:21 +0900 Subject: [PATCH] =?UTF-8?q?feat=20/=20=EB=B0=B0=ED=8F=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/package-lock.json | 5 +- backend/package.json | 6 + backend/server.js | 130 +++++++++------------- frontend/.gitignore | 1 + frontend/src/apis/Challenge.js | 2 +- frontend/src/apis/RepoRank.js | 4 +- frontend/src/apis/github.js | 3 +- frontend/src/components/Badges.module.css | 62 +++++------ frontend/src/pages/LoginPage.jsx | 6 +- frontend/src/pages/ProfilePage.jsx | 2 +- 10 files changed, 102 insertions(+), 119 deletions(-) diff --git a/backend/package-lock.json b/backend/package-lock.json index 1e2dfaaf..cdeef695 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -1,9 +1,12 @@ { - "name": "backend", + "name": "github-oauth-backend", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { + "name": "github-oauth-backend", + "version": "1.0.0", "dependencies": { "axios": "^1.9.0", "cors": "^2.8.5", diff --git a/backend/package.json b/backend/package.json index f76cd672..0d6b5ccb 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,4 +1,10 @@ { + "name": "github-oauth-backend", + "version": "1.0.0", + "main": "server.js", + "scripts": { + "start": "node server.js" + }, "dependencies": { "axios": "^1.9.0", "cors": "^2.8.5", diff --git a/backend/server.js b/backend/server.js index 065acea5..5ff6bf21 100644 --- a/backend/server.js +++ b/backend/server.js @@ -4,31 +4,45 @@ const cors = require("cors"); const jwt = require("jsonwebtoken"); require("dotenv").config(); +const connectDB = require("./config/db"); +const challengeRoutes = require("./routes/challengeRoutes"); + const app = express(); +const CLIENT_ID = process.env.GITHUB_CLIENT_ID; +const CLIENT_SECRET = process.env.GITHUB_CLIENT_SECRET; +const JWT_SECRET = process.env.JWT_SECRET || "my_jwt_secret_key"; +const FRONT_URL = process.env.FRONT_URL || "http://localhost:5173"; +const SERVER_URL = + process.env.SERVER_URL || `http://localhost:${process.env.PORT || 4000}`; +const PORT = process.env.PORT || 4000; + +const allowedOrigins = [FRONT_URL, "http://localhost:5173"]; + +const userAccessTokens = {}; + app.use( cors({ - origin: "http://localhost:5173", + origin: (origin, callback) => { + if (!origin || allowedOrigins.includes(origin)) { + callback(null, true); + } else { + callback(new Error("Not allowed by CORS")); + } + }, credentials: true, }) ); app.use(express.json()); -const CLIENT_ID = process.env.GITHUB_CLIENT_ID; -const CLIENT_SECRET = process.env.GITHUB_CLIENT_SECRET; -const JWT_SECRET = process.env.JWT_SECRET || "my_jwt_secret_key"; - -const userAccessTokens = {}; - -// OAuth URL 발급 +// OAuth 경로 app.get("/oauth/github", (req, res) => { - const redirectUri = "http://localhost:4000/oauth/github/callback"; + const redirectUri = `${SERVER_URL}/oauth/github/callback`; const url = `https://github.com/login/oauth/authorize?client_id=${CLIENT_ID}&redirect_uri=${redirectUri}&scope=repo`; res.json({ url }); }); -// OAuth 콜백 app.get("/oauth/github/callback", async (req, res) => { const code = req.query.code; @@ -46,7 +60,6 @@ app.get("/oauth/github/callback", async (req, res) => { ); const accessToken = tokenRes.data.access_token; - const userRes = await axios.get("https://api.github.com/user", { headers: { Authorization: `Bearer ${accessToken}` }, }); @@ -77,7 +90,7 @@ app.get("/oauth/github/callback", async (req, res) => { } }); -// 인증 프록시(미들웨어) +// 인증 미들웨어 function authenticate(req, res, next) { const authHeader = req.headers.authorization; if (!authHeader) return res.status(401).json({ message: "인증 토큰 없음" }); @@ -92,21 +105,17 @@ function authenticate(req, res, next) { } } -// GitHub Proxy (README 자동 디코딩 포함) +// GitHub proxy app.get("/github/proxy", authenticate, async (req, res) => { const { path, ...params } = req.query; - //클라이언트가 보낸 인코딩된 path를 한 번 디코딩 let dp = path; try { dp = decodeURIComponent(dp); } catch (e) {} - //혹시 두 번 인코딩된 경우를 대비해 다시 디코딩 try { dp = decodeURIComponent(dp); } catch (e) {} - - //fullPath 앞에 슬래시가 없으면 추가 const fullPath = dp.startsWith("/") ? dp : `/${dp}`; const token = userAccessTokens[req.user.login]; @@ -114,6 +123,7 @@ app.get("/github/proxy", authenticate, async (req, res) => { try { const isReadme = fullPath.includes("/readme"); + const githubRes = await axios.get(`https://api.github.com${fullPath}`, { params, headers: { @@ -124,50 +134,33 @@ app.get("/github/proxy", authenticate, async (req, res) => { }, responseType: isReadme ? "text" : "json", }); + res.send(githubRes.data); } catch (error) { - if (error.response?.status === 409) { - console.warn(`Empty repo for path: ${fullPath}`); - return res.json([]); + const status = error.response?.status || 500; + const message = error.response?.data?.message || "GitHub 호출 실패"; + + // 빈 레포 에러는 따로 처리해서 빈 배열을 반환 + if (message === "Git Repository is empty.") { + console.warn(`⚠️ Empty repository for ${fullPath}`); + return res.status(200).json([]); // 프론트가 parse할 수 있게 정상 응답 } - console.error("Proxy 실패:", error.response?.data || error.message); - res.status(500).json({ message: "GitHub 호출 실패" }); + + console.error( + "GitHub API 호출 실패:", + error.response?.data || error.message + ); + return res.status(status).json({ + message: "GitHub 호출 실패", + githubMessage: message, + }); } }); -app.post( - "/github/proxy/repos/:owner/:repo/pulls/:number/comments", - authenticate, - async (req, res) => { - const { owner, repo, number } = req.params; - const { body, commit_id, path, position } = req.body; - const token = userAccessTokens[req.user.login]; - if (!token) return res.status(404).json({ message: "AccessToken 없음" }); +// Challenge API 등록 +connectDB(); +app.use("/api/challenge", challengeRoutes); - try { - const response = await axios.post( - `https://api.github.com/repos/${owner}/${repo}/pulls/${number}/comments`, - { - body, - commit_id, - path, - position, - }, - { - headers: { - Authorization: `Bearer ${token}`, - Accept: "application/vnd.github+json", - }, - } - ); - res.json(response.data); - } catch (error) { - console.error("리뷰 코멘트 실패:", error.response?.data || error.message); - res.status(500).json({ message: "GitHub POST 호출 실패" }); - } - } -); -// 일반 PR 코멘트 달기용 (본문 전체에 댓글) app.post( "/github/proxy/repos/:owner/:repo/issues/:number/comments", authenticate, @@ -200,30 +193,7 @@ app.post( } ); -function authenticate(req, res, next) { - const authHeader = req.headers.authorization; - if (!authHeader) return res.status(401).json({ message: "인증 토큰 없음" }); - - const token = authHeader.split(" ")[1]; - try { - const decoded = jwt.verify(token, JWT_SECRET); - req.user = decoded; - next(); - } catch (e) { - return res.status(401).json({ message: "토큰 검증 실패" }); - } -} - -app.listen(4000, () => { - console.log("✅ 백엔드 서버 실행 중 http://localhost:4000"); -}); - -app.get("/", (req, res) => { - res.send("✅ GitHub OAuth 서버 작동 중"); +// ✅ 서버 실행 +app.listen(PORT, () => { + console.log(`✅ 백엔드 서버 실행 중: http://localhost:${PORT}`); }); - -const connectDB = require("./config/db.js"); -connectDB(); // db연결 - -const challengeRoutes = require("./routes/challengeRoutes"); -app.use("/api/challenge", challengeRoutes); diff --git a/frontend/.gitignore b/frontend/.gitignore index a547bf36..d7307976 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -22,3 +22,4 @@ dist-ssr *.njsproj *.sln *.sw? +*.env diff --git a/frontend/src/apis/Challenge.js b/frontend/src/apis/Challenge.js index b188b374..51df13c2 100644 --- a/frontend/src/apis/Challenge.js +++ b/frontend/src/apis/Challenge.js @@ -1,6 +1,6 @@ import axios from "axios"; -const API_BASE = "http://localhost:4000/api/challenge"; +const API_BASE = `${import.meta.env.VITE_API_URL}/api/challenge`; // 챌린지 참여 요청 export const joinChallenge = async ({ githubId, type }) => { diff --git a/frontend/src/apis/RepoRank.js b/frontend/src/apis/RepoRank.js index c42ef078..4d31b431 100644 --- a/frontend/src/apis/RepoRank.js +++ b/frontend/src/apis/RepoRank.js @@ -1,8 +1,10 @@ import axios from "axios"; +const API_BASE = `${import.meta.env.VITE_API_URL}/github/proxy`; + // ✅ 백엔드 프록시 경로 설정 const github = axios.create({ - baseURL: "http://localhost:4000/github/proxy", + baseURL: API_BASE, }); /** diff --git a/frontend/src/apis/github.js b/frontend/src/apis/github.js index 5af0976a..1b03bf51 100644 --- a/frontend/src/apis/github.js +++ b/frontend/src/apis/github.js @@ -1,6 +1,6 @@ import axios from "axios"; -const API_BASE = "http://localhost:4000"; +const API_BASE = import.meta.env.VITE_API_URL; export const githubAxios = axios.create({ baseURL: "https://api.github.com", @@ -428,7 +428,6 @@ export const getKRMonthRange = (year, month) => { }; }; - export const getAllUserCommitRepos = async (username, since, until) => { try { const combinedCommits = []; diff --git a/frontend/src/components/Badges.module.css b/frontend/src/components/Badges.module.css index 2689bae7..f3d068d4 100644 --- a/frontend/src/components/Badges.module.css +++ b/frontend/src/components/Badges.module.css @@ -1,41 +1,41 @@ .carouselWrapper { - height: 100% !important; - display: flex; - align-items: center; - justify-content: center; - box-shadow: 0 0.25rem 1rem rgba(0, 0, 0, 0.05); - border-radius: 0.5rem; - background-color: #fff; - transition : 0.2s ease; - } + height: 100% !important; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 0.25rem 1rem rgba(0, 0, 0, 0.05); + border-radius: 0.5rem; + background-color: #fff; + transition : 0.2s ease; +} .carouselWrapper:hover{ - transform : translateY(-3px); +transform : translateY(-3px); } - + .carouselCard { - display: flex; - justify-content: space-around; - align-items: flex-start; - padding: 1rem; - background-color: transparent; - border-radius: .5rem; - box-shadow: 0 2px 8px rgba(0,0,0,0.1); - height: 100%; - } + display: flex; + justify-content: space-around; + align-items: flex-start; + padding: 1rem; + background-color: transparent; + border-radius: .5rem; + box-shadow: 0 2px 8px rgba(0,0,0,0.1); + height: 100%; +} .badgeImage { - max-width: 30%; - height: auto; - } - - /* 화살표 아이콘 */ + max-width: 30%; + height: auto; +} + +/* 화살표 아이콘 */ .arrow { - font-size: 2rem; - color: #C7BCE4; /* 기본 색 */ - transition: color 0.3s; - } + font-size: 2rem; + color: #C7BCE4; /* 기본 색 */ + transition: color 0.3s; +} .carousel-control-prev:hover .arrow, .carousel-control-next:hover .arrow { - color: #5F41B2; /* hover 시 색 */ - } \ No newline at end of file + color: #5F41B2; /* hover 시 색 */ +} \ No newline at end of file diff --git a/frontend/src/pages/LoginPage.jsx b/frontend/src/pages/LoginPage.jsx index 4526fe25..81058655 100644 --- a/frontend/src/pages/LoginPage.jsx +++ b/frontend/src/pages/LoginPage.jsx @@ -5,6 +5,8 @@ import styles from "./LoginPage.module.css"; import LogoutBtn from "../components/LogoutBtn"; import { useNavigate } from "react-router-dom"; +const API_BASE = import.meta.env.VITE_API_URL; + const LoginPage = () => { const [socialUser, setSocialUser] = useState(null); const navigate = useNavigate(); @@ -29,7 +31,7 @@ const LoginPage = () => { setSocialUser(payload); navigate("/profile"); } catch (err) { - console.error("🚨 JWT 디코딩 실패:", err); + console.error("JWT 디코딩 실패:", err); } } }; @@ -39,7 +41,7 @@ const LoginPage = () => { }, []); const onClickSocialLogin = async () => { - const res = await fetch("http://localhost:4000/oauth/github"); + const res = await fetch(`${API_BASE}/oauth/github`); const { url } = await res.json(); window.open(url, "_blank", "width=400,height=300"); }; diff --git a/frontend/src/pages/ProfilePage.jsx b/frontend/src/pages/ProfilePage.jsx index 256a1e8d..54cc9fc1 100644 --- a/frontend/src/pages/ProfilePage.jsx +++ b/frontend/src/pages/ProfilePage.jsx @@ -1,7 +1,7 @@ import React, { useEffect, useState } from "react"; import css from "./ProfilePage.module.css"; import UserStatCard from "../components/UserStatCard"; -import { getGitHubUserInfo, getRateLimit, getUserRepos } from "../apis/github"; +import { getGitHubUserInfo, getUserRepos } from "../apis/github"; import RepoTable from "../components/RepoTable"; import Header from "../components/Header"; import CommitTimeChart from "../components/CommitTimeChart";