From b8fd89bcac57a9abf7c2fb8e57533f4893cd0a41 Mon Sep 17 00:00:00 2001 From: Jaeho Date: Sat, 16 May 2026 01:42:18 +0900 Subject: [PATCH 01/10] =?UTF-8?q?feat(pages):=20LoginPage=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/Login/index.ts | 1 + frontend/src/pages/Login/ui/LoginPage.tsx | 121 ++++++++++++++++++++++ 2 files changed, 122 insertions(+) create mode 100644 frontend/src/pages/Login/ui/LoginPage.tsx diff --git a/frontend/src/pages/Login/index.ts b/frontend/src/pages/Login/index.ts index e69de29b..befc7bf0 100644 --- a/frontend/src/pages/Login/index.ts +++ b/frontend/src/pages/Login/index.ts @@ -0,0 +1 @@ +export { default } from './ui/LoginPage' diff --git a/frontend/src/pages/Login/ui/LoginPage.tsx b/frontend/src/pages/Login/ui/LoginPage.tsx new file mode 100644 index 00000000..dd1e43d5 --- /dev/null +++ b/frontend/src/pages/Login/ui/LoginPage.tsx @@ -0,0 +1,121 @@ +import { useEffect, useState } from 'react' +import { Link, Navigate, useLocation } from 'react-router-dom' +import { GithubLoginButton, useAuth } from '@/features/auth' +import { rememberReturnTo } from '@/features/auth/lib/return-to' +import { isApiError } from '@/shared/api' + +type LocationState = { returnTo?: string } | null + +export default function LoginPage() { + const [error, setError] = useState(null) + const location = useLocation() + const { status } = useAuth() + + const returnTo = (location.state as LocationState)?.returnTo ?? null + + useEffect(() => { + rememberReturnTo(returnTo) + }, [returnTo]) + + if (status === 'authenticated') { + return + } + + return ( +
+
+
+ + Stack Up + + + ← 홈으로 + +
+
+ +
+
+
+

+ Sign in +

+

+ 모의 면접을
시작해볼까요? +

+

+ GitHub 계정으로 로그인하면
+ 당신의 레포지토리 기반 맞춤 면접이 준비됩니다. +

+
+ +
+ { + if (isApiError(err)) { + setError(err.message) + } else { + setError('로그인을 시작할 수 없습니다. 잠시 후 다시 시도해주세요.') + } + }} + /> + + {error ? ( +
+ {error} +
+ ) : null} + +

+ 계속 진행하면 StackUp의{' '} + 이용약관과{' '} + 개인정보 처리방침에 + 동의하는 것으로 간주됩니다. +

+
+ +
    + {features.map((f) => ( +
  • +
    + {f.icon} +
    +
    + {f.title} +
    +
  • + ))} +
+
+
+ +
+
+ © 2026 StackUp · CNU 종합설계 + + ← Back to home + +
+
+
+ ) +} + +const features = [ + { icon: '01', title: 'Repo 분석' }, + { icon: '02', title: 'AI 면접' }, + { icon: '03', title: '피드백' }, +] From e0a692de0b205cde48033eef9bcc17dc35d63bc8 Mon Sep 17 00:00:00 2001 From: Jaeho Date: Sat, 16 May 2026 01:42:48 +0900 Subject: [PATCH 02/10] =?UTF-8?q?feat(pages):=20WorkspacePage=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/Workspace/index.ts | 1 + .../src/pages/Workspace/ui/WorkspacePage.tsx | 62 +++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 frontend/src/pages/Workspace/ui/WorkspacePage.tsx diff --git a/frontend/src/pages/Workspace/index.ts b/frontend/src/pages/Workspace/index.ts index e69de29b..2f98a28d 100644 --- a/frontend/src/pages/Workspace/index.ts +++ b/frontend/src/pages/Workspace/index.ts @@ -0,0 +1 @@ +export { default } from './ui/WorkspacePage' diff --git a/frontend/src/pages/Workspace/ui/WorkspacePage.tsx b/frontend/src/pages/Workspace/ui/WorkspacePage.tsx new file mode 100644 index 00000000..f5389094 --- /dev/null +++ b/frontend/src/pages/Workspace/ui/WorkspacePage.tsx @@ -0,0 +1,62 @@ +import { useState } from 'react' +import { useAuth } from '@/features/auth' + +export default function WorkspacePage() { + const { user, logout } = useAuth() + const [loggingOut, setLoggingOut] = useState(false) + + const handleLogout = async () => { + if (loggingOut) return + setLoggingOut(true) + try { + await logout() + } finally { + setLoggingOut(false) + } + } + + return ( +
+

Workspace

+

이력서·레포 관리 화면이 들어갈 자리입니다.

+ + {user ? ( +
+ {user.avatarUrl ? ( + {user.githubUsername} + ) : null} +
+
+ @{user.githubUsername} +
+
+ {user.email ?? 'email 없음'} +
+
+
+ ) : null} + +
+ +
+
+ ) +} From d5ee3f5c20c0c5e3ae7577790ddfc2b995bc946a Mon Sep 17 00:00:00 2001 From: Jaeho Date: Sat, 16 May 2026 01:43:20 +0900 Subject: [PATCH 03/10] =?UTF-8?q?feat(pages):=20AuthCallbackPage=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/AuthCallback/index.ts | 1 + .../AuthCallback/ui/AuthCallbackPage.tsx | 99 +++++++++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 frontend/src/pages/AuthCallback/index.ts create mode 100644 frontend/src/pages/AuthCallback/ui/AuthCallbackPage.tsx diff --git a/frontend/src/pages/AuthCallback/index.ts b/frontend/src/pages/AuthCallback/index.ts new file mode 100644 index 00000000..9a9cdb1d --- /dev/null +++ b/frontend/src/pages/AuthCallback/index.ts @@ -0,0 +1 @@ +export { default } from './ui/AuthCallbackPage' diff --git a/frontend/src/pages/AuthCallback/ui/AuthCallbackPage.tsx b/frontend/src/pages/AuthCallback/ui/AuthCallbackPage.tsx new file mode 100644 index 00000000..ea8acaf0 --- /dev/null +++ b/frontend/src/pages/AuthCallback/ui/AuthCallbackPage.tsx @@ -0,0 +1,99 @@ +import { useEffect, useState } from 'react' +import { Link, useNavigate, useSearchParams } from 'react-router-dom' +import { completeGithubLogin, useAuth } from '@/features/auth' +import { consumeReturnTo } from '@/features/auth/lib/return-to' +import { isApiError } from '@/shared/api' + +const consumedCodes = new Set() + +export default function AuthCallbackPage() { + const [params] = useSearchParams() + const navigate = useNavigate() + const { applyLogin } = useAuth() + + const code = params.get('code') + const stateParam = params.get('state') + const missingParams = !code || !stateParam + + const [error, setError] = useState( + missingParams ? 'GitHub 응답에 code 또는 state가 없습니다.' : null, + ) + + useEffect(() => { + if (missingParams) return + const key = `${code}:${stateParam}` + if (consumedCodes.has(key)) return + consumedCodes.add(key) + + void (async () => { + try { + const response = await completeGithubLogin(code!, stateParam!) + applyLogin(response) + const dest = consumeReturnTo() ?? '/' + navigate(dest, { replace: true }) + } catch (err) { + if (isApiError(err)) { + setError(err.message) + } else { + setError('로그인 처리 중 오류가 발생했습니다.') + } + } + })() + }, [missingParams, code, stateParam, applyLogin, navigate]) + + return ( +
+
+ {error ? ( + <> +

+ Login failed +

+

+ 로그인을 완료하지 못했어요 +

+

{error}

+ + 로그인 페이지로 돌아가기 + + + ) : ( + <> + +

+ Authenticating +

+

+ GitHub 로그인 처리 중… +

+

+ 잠시만 기다려주세요. 곧 이동합니다. +

+ + )} +
+
+ ) +} + +function Spinner() { + return ( + + + + + ) +} From 545414a62b6a6f8796e446cdda83b8a238d35b7f Mon Sep 17 00:00:00 2001 From: Jaeho Date: Sat, 16 May 2026 01:44:16 +0900 Subject: [PATCH 04/10] =?UTF-8?q?feat(providers):=20AppProviders=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/app/providers/AppProviders.tsx | 27 +++++++++++++++++++++ frontend/src/app/providers/index.ts | 1 + 2 files changed, 28 insertions(+) create mode 100644 frontend/src/app/providers/AppProviders.tsx diff --git a/frontend/src/app/providers/AppProviders.tsx b/frontend/src/app/providers/AppProviders.tsx new file mode 100644 index 00000000..6714fbf8 --- /dev/null +++ b/frontend/src/app/providers/AppProviders.tsx @@ -0,0 +1,27 @@ +import { Suspense } from 'react' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { RouterProvider } from 'react-router-dom' +import { AuthProvider } from '@/features/auth' +import { router } from '@/app/router' + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: 1, + refetchOnWindowFocus: false, + staleTime: 30_000, + }, + }, +}) + +export function AppProviders() { + return ( + + + + + + + + ) +} diff --git a/frontend/src/app/providers/index.ts b/frontend/src/app/providers/index.ts index e69de29b..a7f6a1ad 100644 --- a/frontend/src/app/providers/index.ts +++ b/frontend/src/app/providers/index.ts @@ -0,0 +1 @@ +export { AppProviders } from './AppProviders' From deeef849c65df59b91958cfc527319521ad91469 Mon Sep 17 00:00:00 2001 From: Jaeho Date: Sat, 16 May 2026 01:44:42 +0900 Subject: [PATCH 05/10] =?UTF-8?q?feat(router):=20=EB=9D=BC=EC=9A=B0?= =?UTF-8?q?=ED=84=B0=20=EB=AA=A8=EB=93=88=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/app/router/index.tsx | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 frontend/src/app/router/index.tsx diff --git a/frontend/src/app/router/index.tsx b/frontend/src/app/router/index.tsx new file mode 100644 index 00000000..ee278444 --- /dev/null +++ b/frontend/src/app/router/index.tsx @@ -0,0 +1,27 @@ +import { createBrowserRouter } from 'react-router-dom' +import { RequireAuth } from '@/features/auth' +import HomePage from '@/pages/Home' +import LoginPage from '@/pages/Login' +import AuthCallbackPage from '@/pages/AuthCallback' +import WorkspacePage from '@/pages/Workspace' + +export const router = createBrowserRouter([ + { path: '/', element: }, + { path: '/login', element: }, + { path: '/auth/callback', element: }, + { + path: '/workspace', + element: ( + + + + ), + }, + { + path: '/design-system/*', + lazy: async () => { + const mod = await import('@/pages/DesignSystem') + return { Component: mod.default } + }, + }, +]) From a788c5be2a53660f807bbd18377e56922941ae2f Mon Sep 17 00:00:00 2001 From: Jaeho Date: Sat, 16 May 2026 01:45:54 +0900 Subject: [PATCH 06/10] =?UTF-8?q?feat(app):=20app=EC=97=94=ED=8A=B8?= =?UTF-8?q?=EB=A6=AC=20=EC=A0=95=EC=9D=98=20=EB=B0=8F=20=EB=A9=94=ED=83=80?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/index.html | 12 +++++++++--- frontend/src/main.tsx | 4 ++-- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/frontend/index.html b/frontend/index.html index e8231331..aebe5dbf 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1,16 +1,22 @@ - + + - StackUp + + StackUp — IT Interview Solution
diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 037249fa..93987cda 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -1,10 +1,10 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import '@/app/styles' -import App from './App.tsx' +import { AppProviders } from '@/app/providers' createRoot(document.getElementById('root')!).render( - + , ) From eb2aa3e7e955b39b1f8fdd8d515a792e1aedad0d Mon Sep 17 00:00:00 2001 From: Jaeho Date: Sat, 16 May 2026 01:46:39 +0900 Subject: [PATCH 07/10] =?UTF-8?q?refactor(app):=20=EB=A0=88=EA=B1=B0?= =?UTF-8?q?=EC=8B=9C=20app=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/App.tsx | 19 ------------------- frontend/src/app/router/index.ts | 0 2 files changed, 19 deletions(-) delete mode 100644 frontend/src/App.tsx delete mode 100644 frontend/src/app/router/index.ts diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx deleted file mode 100644 index cd9f4def..00000000 --- a/frontend/src/App.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { lazy, Suspense } from 'react' -import HomePage from '@/pages/Home' - -const DesignSystemDemo = lazy(() => import('@/pages/DesignSystem')) - -export default function App() { - const path = - typeof window !== 'undefined' ? window.location.pathname : '/' - - if (path.startsWith('/design-system')) { - return ( - - - - ) - } - - return -} diff --git a/frontend/src/app/router/index.ts b/frontend/src/app/router/index.ts deleted file mode 100644 index e69de29b..00000000 From b553f3e5df559ec6022a4821d3ece38ad6a23235 Mon Sep 17 00:00:00 2001 From: Jaeho Date: Sat, 16 May 2026 01:47:28 +0900 Subject: [PATCH 08/10] =?UTF-8?q?feat(widgets):=20=ED=97=A4=EB=8D=94?= =?UTF-8?q?=EC=97=90=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=83=81=ED=83=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/widgets/site-nav/ui/SiteNav.tsx | 76 ++++++++++++++++---- 1 file changed, 63 insertions(+), 13 deletions(-) diff --git a/frontend/src/widgets/site-nav/ui/SiteNav.tsx b/frontend/src/widgets/site-nav/ui/SiteNav.tsx index fcc1a775..f6f3ae10 100644 --- a/frontend/src/widgets/site-nav/ui/SiteNav.tsx +++ b/frontend/src/widgets/site-nav/ui/SiteNav.tsx @@ -1,4 +1,6 @@ import { useEffect, useState } from 'react' +import { Link } from 'react-router-dom' +import { useAuth } from '@/features/auth' const items = [ { href: '#services', label: 'Services' }, @@ -8,6 +10,8 @@ const items = [ export function SiteNav() { const [scrolled, setScrolled] = useState(false) + const { status, user, logout } = useAuth() + const [loggingOut, setLoggingOut] = useState(false) useEffect(() => { const onScroll = () => setScrolled(window.scrollY > 8) @@ -16,6 +20,16 @@ export function SiteNav() { return () => window.removeEventListener('scroll', onScroll) }, []) + const handleLogout = async () => { + if (loggingOut) return + setLoggingOut(true) + try { + await logout() + } finally { + setLoggingOut(false) + } + } + return (
-
- - Get Started - - → - - +
+ {status === 'authenticated' ? ( + <> + + {user?.avatarUrl ? ( + + ) : null} + {user?.githubUsername ?? 'Workspace'} + + + + ) : ( + <> + + Login + + + Get Started + + → + + + + )}
From 3f060c876213c803e25fedba6037ac9ae21e61c0 Mon Sep 17 00:00:00 2001 From: Jaeho Date: Sat, 16 May 2026 21:04:41 +0900 Subject: [PATCH 09/10] =?UTF-8?q?feat=20:=20useLogout.ts=20=ED=9B=85=20?= =?UTF-8?q?=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/features/auth/model/useLogout.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 frontend/src/features/auth/model/useLogout.ts diff --git a/frontend/src/features/auth/model/useLogout.ts b/frontend/src/features/auth/model/useLogout.ts new file mode 100644 index 00000000..098cae0e --- /dev/null +++ b/frontend/src/features/auth/model/useLogout.ts @@ -0,0 +1,19 @@ +import { useCallback, useState } from 'react' +import { useAuth } from './useAuth' + +export function useLogout() { + const { logout: logoutFromAuth } = useAuth() + const [loggingOut, setLoggingOut] = useState(false) + + const logout = useCallback(async () => { + if (loggingOut) return + setLoggingOut(true) + try { + await logoutFromAuth() + } finally { + setLoggingOut(false) + } + }, [loggingOut, logoutFromAuth]) + + return { logout, loggingOut } +} From 1b0f320787e4a69d1949f82f04153c609ab3e789 Mon Sep 17 00:00:00 2001 From: Jaeho Date: Sat, 16 May 2026 21:05:37 +0900 Subject: [PATCH 10/10] =?UTF-8?q?refactor(auth):=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=95=84=EC=9B=83=20=EB=A1=9C=EC=A7=81=EC=9D=84=20useLogout=20?= =?UTF-8?q?=ED=9B=85=EC=9C=BC=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/features/auth/index.ts | 1 + .../src/pages/Workspace/ui/WorkspacePage.tsx | 19 ++++--------------- frontend/src/widgets/site-nav/ui/SiteNav.tsx | 18 ++++-------------- 3 files changed, 9 insertions(+), 29 deletions(-) diff --git a/frontend/src/features/auth/index.ts b/frontend/src/features/auth/index.ts index 50456a9f..e9302a3f 100644 --- a/frontend/src/features/auth/index.ts +++ b/frontend/src/features/auth/index.ts @@ -1,5 +1,6 @@ export { AuthProvider } from './model/AuthProvider' export { useAuth } from './model/useAuth' +export { useLogout } from './model/useLogout' export type { AuthContextValue, AuthStatus } from './model/AuthContext' export { GithubLoginButton } from './ui/GithubLoginButton' export { RequireAuth } from './ui/RequireAuth' diff --git a/frontend/src/pages/Workspace/ui/WorkspacePage.tsx b/frontend/src/pages/Workspace/ui/WorkspacePage.tsx index f5389094..adb7dd89 100644 --- a/frontend/src/pages/Workspace/ui/WorkspacePage.tsx +++ b/frontend/src/pages/Workspace/ui/WorkspacePage.tsx @@ -1,19 +1,8 @@ -import { useState } from 'react' -import { useAuth } from '@/features/auth' +import { useAuth, useLogout } from '@/features/auth' export default function WorkspacePage() { - const { user, logout } = useAuth() - const [loggingOut, setLoggingOut] = useState(false) - - const handleLogout = async () => { - if (loggingOut) return - setLoggingOut(true) - try { - await logout() - } finally { - setLoggingOut(false) - } - } + const { user } = useAuth() + const { logout, loggingOut } = useLogout() return (
@@ -53,7 +42,7 @@ export default function WorkspacePage() { ) : null}
-
diff --git a/frontend/src/widgets/site-nav/ui/SiteNav.tsx b/frontend/src/widgets/site-nav/ui/SiteNav.tsx index f6f3ae10..4e7a357b 100644 --- a/frontend/src/widgets/site-nav/ui/SiteNav.tsx +++ b/frontend/src/widgets/site-nav/ui/SiteNav.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react' import { Link } from 'react-router-dom' -import { useAuth } from '@/features/auth' +import { useAuth, useLogout } from '@/features/auth' const items = [ { href: '#services', label: 'Services' }, @@ -10,8 +10,8 @@ const items = [ export function SiteNav() { const [scrolled, setScrolled] = useState(false) - const { status, user, logout } = useAuth() - const [loggingOut, setLoggingOut] = useState(false) + const { status, user } = useAuth() + const { logout, loggingOut } = useLogout() useEffect(() => { const onScroll = () => setScrolled(window.scrollY > 8) @@ -20,16 +20,6 @@ export function SiteNav() { return () => window.removeEventListener('scroll', onScroll) }, []) - const handleLogout = async () => { - if (loggingOut) return - setLoggingOut(true) - try { - await logout() - } finally { - setLoggingOut(false) - } - } - return (