diff --git a/apps/web/package.json b/apps/web/package.json index 7e8bbe44..0ae2f811 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -43,6 +43,8 @@ "@tailwindcss/postcss": "^4", "@testing-library/dom": "^10.4.1", "@testing-library/react": "^16.3.2", + "@types/google-one-tap": "^1.2.7", + "@types/google.accounts": "^0.0.18", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", diff --git a/apps/web/src/_pages/auth/use-cases/social-login-url.ts b/apps/web/src/_pages/auth/use-cases/social-login-url.ts deleted file mode 100644 index 5f36b714..00000000 --- a/apps/web/src/_pages/auth/use-cases/social-login-url.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { API_BASE_URL, TEAM_ID } from "@/shared/config/env"; - -/** - * 구글 소셜 로그인 시작 URL 반환 - * 브라우저가 이 URL로 이동하면 백엔드가 구글 로그인 페이지로 302 리다이렉트 - * 구글 인증 완료 후 /oauth/callback?accessToken=...&refreshToken=... 으로 복귀 - */ -export function getGoogleLoginUrl(): string { - return `${API_BASE_URL}/${TEAM_ID}/auth/google`; -} - -/** - * 카카오 소셜 로그인 시작 URL 반환 - * 브라우저가 이 URL로 이동하면 백엔드가 카카오 로그인 페이지로 302 리다이렉트 - * 카카오 인증 완료 후 /oauth/callback?accessToken=...&refreshToken=... 으로 복귀 - */ -export function getKakaoLoginUrl(): string { - return `${API_BASE_URL}/${TEAM_ID}/auth/kakao`; -} diff --git a/apps/web/src/_pages/auth/use-cases/social-login.ts b/apps/web/src/_pages/auth/use-cases/social-login.ts new file mode 100644 index 00000000..8135a873 --- /dev/null +++ b/apps/web/src/_pages/auth/use-cases/social-login.ts @@ -0,0 +1,33 @@ +import { API_BASE_URL, TEAM_ID } from "@/shared/config/env"; + +/** + * 구글 소셜 로그인 + * Google Identity Services SDK로 받은 access_token을 백엔드로 전달 + * 백엔드가 토큰 검증 후 자체 JWT(accessToken, refreshToken) 발급 + */ +export async function loginWithGoogle(accessToken: string) { + const res = await fetch(`${API_BASE_URL}/${TEAM_ID}/oauth/google`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ token: accessToken }), + }); + + if (!res.ok) throw new Error("구글 로그인 실패"); + return res.json() as Promise<{ accessToken: string; refreshToken: string }>; +} + +/** + * 카카오 소셜 로그인 + * Next.js Route Handler에서 카카오 access_token으로 교환 후 백엔드로 전달 + * 백엔드가 토큰 검증 후 자체 JWT(accessToken, refreshToken) 발급 + */ +export async function loginWithKakao(accessToken: string) { + const res = await fetch(`${API_BASE_URL}/${TEAM_ID}/oauth/kakao`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ token: accessToken }), + }); + + if (!res.ok) throw new Error("카카오 로그인 실패"); + return res.json() as Promise<{ accessToken: string; refreshToken: string }>; +} diff --git a/apps/web/src/app/api/oauth/kakao/route.ts b/apps/web/src/app/api/oauth/kakao/route.ts new file mode 100644 index 00000000..f90f05c1 --- /dev/null +++ b/apps/web/src/app/api/oauth/kakao/route.ts @@ -0,0 +1,36 @@ +import { type NextRequest, NextResponse } from "next/server"; + +// 카카오 인가 코드 -> access_token 교환 +// client_secret이 필요하기 때문에 브라우저가 아닌 서버(Route Handler)에서 처리 +export async function GET(request: NextRequest) { + const { searchParams } = new URL(request.url); + const code = searchParams.get("code"); + + if (!code) { + return NextResponse.json({ message: "인가 코드가 없습니다." }, { status: 400 }); + } + + const tokenRes = await fetch("https://kauth.kakao.com/oauth/token", { + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: new URLSearchParams({ + grant_type: "authorization_code", + client_id: process.env.NEXT_PUBLIC_KAKAO_CLIENT_ID!, + client_secret: process.env.KAKAO_CLIENT_SECRET!, + code, + redirect_uri: process.env.NEXT_PUBLIC_KAKAO_REDIRECT_URI!, + }), + }); + + if (!tokenRes.ok) { + return NextResponse.json({ message: "카카오 토큰 교환 실패" }, { status: 400 }); + } + + const { access_token } = await tokenRes.json(); + + if (!access_token) { + return NextResponse.json({ message: "카카오 토큰 응답 오류" }, { status: 400 }); + } + + return NextResponse.json({ accessToken: access_token }); +} diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index 58757a69..53a3d36c 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -1,5 +1,6 @@ import type { Metadata } from "next"; import "./globals.css"; +import Script from "next/script"; import { AmplitudeInit } from "@/amplitude"; export const metadata: Metadata = { @@ -23,6 +24,7 @@ export default function RootLayout({ /> +