From 482ecc90294fe5d0492dd0945ae4728064094b75 Mon Sep 17 00:00:00 2001 From: tlrdmsEjrqhRdl Date: Thu, 2 Jul 2026 13:44:54 +0900 Subject: [PATCH 1/2] =?UTF-8?q?fix(app):=20DataGSM=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EB=A6=AC=EB=8B=A4=EC=9D=B4=EB=A0=89=ED=8A=B8=20?= =?UTF-8?q?=EA=B2=BD=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/auth/dg/login/route.ts | 66 ++++++++++++++++++++++++++++++ src/shared/ui/dg-signin-button.tsx | 4 +- 2 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 app/auth/dg/login/route.ts diff --git a/app/auth/dg/login/route.ts b/app/auth/dg/login/route.ts new file mode 100644 index 0000000..fce22ef --- /dev/null +++ b/app/auth/dg/login/route.ts @@ -0,0 +1,66 @@ +import { createHash, randomBytes } from "node:crypto"; +import { NextResponse } from "next/server"; + +const DATAGSM_AUTHORIZE_URL = "https://oauth.authorization.datagsm.kr/v1/oauth/authorize"; +const OAUTH_CALLBACK_PATH = "/oauth/callback"; +const OAUTH_COOKIE_MAX_AGE_SECONDS = 60 * 5; + +function createPkceCodeVerifier() { + return randomBytes(64).toString("base64url"); +} + +function createPkceCodeChallenge(codeVerifier: string) { + return createHash("sha256").update(codeVerifier).digest("base64url"); +} + +function createState() { + return randomBytes(32).toString("base64url"); +} + +export async function GET(request: Request) { + const clientId = process.env.NEXT_PUBLIC_DATAGSM_CLIENT_ID; + + if (!clientId) { + return NextResponse.json( + { message: "NEXT_PUBLIC_DATAGSM_CLIENT_ID is not configured." }, + { status: 500 }, + ); + } + + const requestUrl = new URL(request.url); + const redirectUri = new URL(OAUTH_CALLBACK_PATH, requestUrl.origin).toString(); + const state = createState(); + const codeVerifier = createPkceCodeVerifier(); + const codeChallenge = createPkceCodeChallenge(codeVerifier); + + const authorizeUrl = new URL(DATAGSM_AUTHORIZE_URL); + authorizeUrl.searchParams.set("client_id", clientId); + authorizeUrl.searchParams.set("redirect_uri", redirectUri); + authorizeUrl.searchParams.set("response_type", "code"); + authorizeUrl.searchParams.set("state", state); + authorizeUrl.searchParams.set("code_challenge", codeChallenge); + authorizeUrl.searchParams.set("code_challenge_method", "S256"); + + const response = NextResponse.redirect(authorizeUrl); + + response.cookies.set("dg_oauth_state", state, { + httpOnly: true, + maxAge: OAUTH_COOKIE_MAX_AGE_SECONDS, + path: "/auth/dg", + sameSite: "lax", + }); + response.cookies.set("dg_oauth_code_verifier", codeVerifier, { + httpOnly: true, + maxAge: OAUTH_COOKIE_MAX_AGE_SECONDS, + path: "/auth/dg", + sameSite: "lax", + }); + response.cookies.set("dg_oauth_redirect_uri", redirectUri, { + httpOnly: true, + maxAge: OAUTH_COOKIE_MAX_AGE_SECONDS, + path: "/auth/dg", + sameSite: "lax", + }); + + return response; +} diff --git a/src/shared/ui/dg-signin-button.tsx b/src/shared/ui/dg-signin-button.tsx index 32c3441..36cabdf 100644 --- a/src/shared/ui/dg-signin-button.tsx +++ b/src/shared/ui/dg-signin-button.tsx @@ -4,8 +4,8 @@ import { DGIcon } from "@/shared/assets/DGIcon"; export function DgSigninButton() { const handleLogin = () => { - // 백엔드(/auth/dg/login)가 PKCE 쿠키를 심고 DataGSM 으로 리다이렉트해야 하므로 - // SPA 라우팅이 아니라 same-origin 프록시를 타는 전체 페이지 이동이어야 한다. + // same-origin Route Handler 가 현재 origin 기준 PKCE 쿠키를 심고 + // DataGSM 인증 페이지로 넘겨야 하므로 전체 페이지 이동으로 시작한다. window.location.href = "/auth/dg/login"; }; From f521fe1ca216f16270dc6fed0b83bb8119d3abd5 Mon Sep 17 00:00:00 2001 From: tlrdmsEjrqhRdl Date: Thu, 2 Jul 2026 13:45:02 +0900 Subject: [PATCH 2/2] =?UTF-8?q?chore(global):=20=ED=8F=B0=ED=8A=B8=20?= =?UTF-8?q?=EB=B0=8F=20=EB=AF=B8=EB=93=A4=EC=9B=A8=EC=96=B4=20=EA=B2=BD?= =?UTF-8?q?=EA=B3=A0=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/globals.css | 2 +- app/layout.tsx | 15 ++++++++------- middleware.ts | 9 +-------- 3 files changed, 10 insertions(+), 16 deletions(-) diff --git a/app/globals.css b/app/globals.css index bb23a45..49da3d9 100644 --- a/app/globals.css +++ b/app/globals.css @@ -86,7 +86,7 @@ --font-family-base: "Pretendard Variable", Pretendard, -apple-system, BlinkMacSystemFont, system-ui, sans-serif; - --font-logo: "Black Han Sans", sans-serif; + --font-logo: var(--font-black-han-sans), sans-serif; --radius-sm: 4px; --radius-md: 8px; diff --git a/app/layout.tsx b/app/layout.tsx index 3bdd169..77a9760 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,7 +1,14 @@ import type { Metadata } from "next"; +import { Black_Han_Sans } from "next/font/google"; import { QueryProvider } from "@/shared/providers/query-provider"; import "./globals.css"; +const blackHanSans = Black_Han_Sans({ + subsets: ["latin"], + weight: "400", + variable: "--font-black-han-sans", +}); + export const metadata: Metadata = { title: "취준", description: "한국 취준생을 위한 코딩 테스트 풀이 플랫폼", @@ -9,18 +16,12 @@ export const metadata: Metadata = { export default function RootLayout({ children }: { children: React.ReactNode }) { return ( - + - - - {children} diff --git a/middleware.ts b/middleware.ts index 6d7dc9d..f0a649b 100644 --- a/middleware.ts +++ b/middleware.ts @@ -1,5 +1,5 @@ import { NextResponse, type NextRequest } from "next/server"; -import { PUBLIC_ROUTES, REFRESH_TOKEN_COOKIE } from "@/shared/config/routes"; +import { REFRESH_TOKEN_COOKIE } from "@/shared/config/routes"; export const config = { matcher: ["/((?!api|auth|_next/static|_next/image|favicon.ico|.*\\..*).*)"], @@ -13,12 +13,5 @@ export function middleware(request: NextRequest) { if (pathname === "/signin" && refreshToken) { return NextResponse.redirect(new URL("/", request.url)); } - - // 비공개 경로 + 미인증 → /signin 으로. - const isPublic = PUBLIC_ROUTES.some((route) => route === pathname); - // if (!isPublic && !refreshToken) { - // return NextResponse.redirect(new URL("/signin", request.url)); - // } - return NextResponse.next(); }