From e7fa6acc23e80239f596562d4567f8070eb38efc Mon Sep 17 00:00:00 2001 From: woo-dev-log Date: Thu, 31 Jul 2025 17:20:41 +0900 Subject: [PATCH 1/8] =?UTF-8?q?chore:=20React=20Query=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - @tanstack/react-query 설치 - GitHub OAuth API 상태 관리 개선 준비 - 향후 API 호출 최적화 및 캐싱 기능 활용 예정 --- package-lock.json | 27 +++++++++++++++++++++++++++ package.json | 3 ++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 125a2a5..24f062a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-tabs": "^1.1.0", "@radix-ui/react-toggle": "^1.1.0", + "@tanstack/react-query": "^5.83.0", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "cmdk": "^1.0.0", @@ -1503,6 +1504,32 @@ "vite": "^5.2.0 || ^6 || ^7" } }, + "node_modules/@tanstack/query-core": { + "version": "5.83.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.83.0.tgz", + "integrity": "sha512-0M8dA+amXUkyz5cVUm/B+zSk3xkQAcuXuz5/Q/LveT4ots2rBpPTZOzd7yJa2Utsf8D2Upl5KyjhHRY+9lB/XA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.83.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.83.0.tgz", + "integrity": "sha512-/XGYhZ3foc5H0VM2jLSD/NyBRIOK4q9kfeml4+0x2DlL6xVuAcVEW+hTlTapAmejObg0i3eNqhkr2dT+eciwoQ==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.83.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, "node_modules/@types/cookie": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", diff --git a/package.json b/package.json index 5bdbeed..c9ab803 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-tabs": "^1.1.0", "@radix-ui/react-toggle": "^1.1.0", + "@tanstack/react-query": "^5.83.0", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "cmdk": "^1.0.0", @@ -52,4 +53,4 @@ "public" ] } -} \ No newline at end of file +} From 7b2fec584ebc9d4ae3b45b1be700494e99b63420 Mon Sep 17 00:00:00 2001 From: woo-dev-log Date: Fri, 1 Aug 2025 10:00:03 +0900 Subject: [PATCH 2/8] =?UTF-8?q?feat:=20GitHub=20OAuth=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EC=8B=9C=EC=8A=A4=ED=85=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - LoginPage: GitHub OAuth URL 생성 및 리다이렉트 기능 - AuthCallbackPage: OAuth 콜백 처리 및 상태 분기 로직 - React Query useMutation 적용으로 자동 상태 관리 - API 함수 분리 (lib/api.ts) - main.tsx에 QueryClientProvider 설정 OAuth 2.0 Authorization Code Flow 구현 - CSRF 보호를 위한 state 파라미터 검증 - 기존유저(200) vs 신규유저(210) 자동 분기 React Query 최적화 - OAuth API만 retry: false 설정으로 재시도 방지 - 일반 mutation은 retry: 2 유지 - 전역 QueryClient 설정 최적화 --- .env | 1 + src/lib/api.ts | 16 ++++++ src/main.tsx | 23 +++++++- src/pages/auth/AuthCallbackPage.tsx | 89 +++++++++++++++++++++++++++++ src/pages/auth/LoginPage.tsx | 24 +++++++- 5 files changed, 150 insertions(+), 3 deletions(-) create mode 100644 .env create mode 100644 src/pages/auth/AuthCallbackPage.tsx diff --git a/.env b/.env new file mode 100644 index 0000000..81c42a3 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +VITE_GITHUB_CLIENT_ID=Ov23lidN0L52PDUnLEDp \ No newline at end of file diff --git a/src/lib/api.ts b/src/lib/api.ts index 026a90c..347a371 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -37,3 +37,19 @@ export const fetchStacks = async (): Promise => { } return response.json(); }; + +export const fetchLogin = async (code: string) => { + const response = await fetch(`/api/auth/sign-in?code=${encodeURIComponent(code)}`, { + method: 'GET', + headers: { 'Content-Type': 'application/json' }, + }); + + if (!response.ok) { + throw new Error(`GitHub 로그인 실패: ${response.status}`); + } + + return { + status: response.status, + data: await response.json() + }; +}; \ No newline at end of file diff --git a/src/main.tsx b/src/main.tsx index a778dd7..df20604 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -6,6 +6,21 @@ import App from "./App.tsx"; import { LoginPage } from "./pages/auth/LoginPage.tsx"; import { BoardPage } from "./pages/home/index.tsx"; import { ExplorePage } from "./pages/explore/index.tsx"; +import AuthCallbackPage from "./pages/auth/AuthCallbackPage.tsx"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 1000 * 60 * 5, + retry: 3, + refetchOnWindowFocus: false, + }, + mutations: { + retry: 2, + }, + }, +}); async function enableMocking() { if (import.meta.env.DEV) { @@ -37,12 +52,18 @@ const router = createBrowserRouter([ path: "/login", element: , }, + { + path: "/callback", + element: , + } ]); enableMocking().then(() => { createRoot(document.getElementById("root")!).render( - + + + ); }); diff --git a/src/pages/auth/AuthCallbackPage.tsx b/src/pages/auth/AuthCallbackPage.tsx new file mode 100644 index 0000000..52921bc --- /dev/null +++ b/src/pages/auth/AuthCallbackPage.tsx @@ -0,0 +1,89 @@ +import { useEffect } from 'react'; +import { useNavigate, useSearchParams } from 'react-router-dom'; +import GlobalLayout from '@/components/layout/GlobalLayout'; +import { Button } from '@/components/ui/button'; +import { useMutation } from '@tanstack/react-query'; +import { fetchLogin } from '@/lib/api'; + +export default function AuthCallbackPage() { + const [searchParams] = useSearchParams(); + const navigate = useNavigate(); + + const signInMutation = useMutation({ + mutationFn: fetchLogin, + retry: false, + onSuccess: (result) => { + if (result.status === 200) { + navigate('/', { replace: true }); + } else if (result.status === 210) { + navigate('/onboarding', { replace: true }); + } + }, + onError: (error) => { + console.error('OAuth callback error:', error); + } + }); + + useEffect(() => { + const handleCallback = () => { + const code = searchParams.get('code'); + const error = searchParams.get('error'); + const state = searchParams.get('state'); + + if (error) { + navigate('/login', { replace: true }); + console.error('GitHub OAuth 에러:', error); + return; + } + + if (!code) { + navigate('/login', { replace: true }); + console.log('GitHub에서 인증 코드를 받지 못했습니다'); + return; + } + + const storedState = sessionStorage.getItem('github_oauth_state'); + if (state !== storedState) { + navigate('/login', { replace: true }); + console.log('보안 검증 실패'); + return; + } + + signInMutation.mutate(code); + sessionStorage.removeItem('github_oauth_state'); + } + + handleCallback(); + }, [searchParams, navigate, signInMutation]); + + return ( + +
+ {signInMutation.isPending && ( +
+
+

GitHub 로그인 처리 중...

+

사용자 정보를 확인하고 있습니다

+
+ )} + + {signInMutation.isError && ( +
+

로그인 실패

+
+

+ {signInMutation.error?.message || '로그인 처리 중 오류가 발생했습니다'} +

+
+ +
+ )} +
+ + ); +} \ No newline at end of file diff --git a/src/pages/auth/LoginPage.tsx b/src/pages/auth/LoginPage.tsx index 14e50cd..cfc20ed 100644 --- a/src/pages/auth/LoginPage.tsx +++ b/src/pages/auth/LoginPage.tsx @@ -2,11 +2,30 @@ import GlobalLayout from "@/components/layout/GlobalLayout"; import { Button } from "@/components/ui/button"; export function LoginPage() { + const handleGitHubLogin = async () => { + try { + const clientId = import.meta.env.VITE_GITHUB_CLIENT_ID;; + const redirectUri = `${window.location.origin}/callback`; + const scope = 'user:email'; + const state = Math.random().toString(36).substring(7); + + const githubAuthUrl = `https://github.com/login/oauth/authorize?` + + `client_id=${clientId}&` + + `redirect_uri=${encodeURIComponent(redirectUri)}&` + + `scope=${scope}&` + + `state=${state}`; + + sessionStorage.setItem('github_oauth_state', state); + window.location.href = githubAuthUrl; + } catch (error) { + console.error('GitHub 로그인 오류:', error); + } + }; + return (
@@ -20,6 +39,7 @@ export function LoginPage() {