Skip to content
Open
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
5 changes: 4 additions & 1 deletion backend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
130 changes: 50 additions & 80 deletions backend/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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}` },
});
Expand Down Expand Up @@ -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: "인증 토큰 없음" });
Expand All @@ -92,28 +105,25 @@ 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];
if (!token) return res.status(404).json({ message: "AccessToken 없음" });

try {
const isReadme = fullPath.includes("/readme");

const githubRes = await axios.get(`https://api.github.com${fullPath}`, {
params,
headers: {
Expand All @@ -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,
Expand Down Expand Up @@ -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);
1 change: 1 addition & 0 deletions frontend/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ dist-ssr
*.njsproj
*.sln
*.sw?
*.env
2 changes: 1 addition & 1 deletion frontend/src/apis/Challenge.js
Original file line number Diff line number Diff line change
@@ -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 }) => {
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/apis/RepoRank.js
Original file line number Diff line number Diff line change
@@ -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,
});

/**
Expand Down
3 changes: 1 addition & 2 deletions frontend/src/apis/github.js
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -428,7 +428,6 @@ export const getKRMonthRange = (year, month) => {
};
};


export const getAllUserCommitRepos = async (username, since, until) => {
try {
const combinedCommits = [];
Expand Down
62 changes: 31 additions & 31 deletions frontend/src/components/Badges.module.css
Original file line number Diff line number Diff line change
@@ -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 시 색 */
}
color: #5F41B2; /* hover 시 색 */
}
6 changes: 4 additions & 2 deletions frontend/src/pages/LoginPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -29,7 +31,7 @@ const LoginPage = () => {
setSocialUser(payload);
navigate("/profile");
} catch (err) {
console.error("🚨 JWT 디코딩 실패:", err);
console.error("JWT 디코딩 실패:", err);
}
}
};
Expand All @@ -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");
};
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/pages/ProfilePage.jsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down