From 910f39e2b61d365385d60a89f42af1d95b5220e1 Mon Sep 17 00:00:00 2001 From: LMS10 Date: Mon, 4 May 2026 19:30:02 +0900 Subject: [PATCH 01/10] =?UTF-8?q?=E2=9C=A8=20Feat:=20next-auth=20=EC=84=A4?= =?UTF-8?q?=EC=B9=98=20=EB=B0=8F=20=EC=9D=B8=EC=A6=9D=20=EA=B4=80=EB=A0=A8?= =?UTF-8?q?=20=ED=83=80=EC=9E=85=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/types/auth.ts | 28 ++++++++++++++++++++++++++++ src/types/next-auth.d.ts | 27 +++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 src/types/auth.ts create mode 100644 src/types/next-auth.d.ts diff --git a/src/types/auth.ts b/src/types/auth.ts new file mode 100644 index 0000000..ec15f7e --- /dev/null +++ b/src/types/auth.ts @@ -0,0 +1,28 @@ +export interface SignupRequest { + email: string; + name: string; + password: string; +} + +export interface SigninRequest { + email: string; + password: string; +} + +export interface AuthUser { + accessToken: string; + email: string; + name: string; + workspaceCount: number; + lastWorkspaceId: number | null; + userId: number; +} + +export interface AuthResponse { + success: boolean; + data: AuthUser; + meta: { + timestamp: string; + traceId: string; + }; +} diff --git a/src/types/next-auth.d.ts b/src/types/next-auth.d.ts new file mode 100644 index 0000000..74e0ebc --- /dev/null +++ b/src/types/next-auth.d.ts @@ -0,0 +1,27 @@ +import 'next-auth'; +import 'next-auth/jwt'; + +declare module 'next-auth' { + interface Session { + accessToken: string; + user: { + id: string; + email: string; + name: string; + }; + } + + interface User { + id: string; + email: string; + name: string; + accessToken: string; + } +} + +declare module 'next-auth/jwt' { + interface JWT { + accessToken: string; + userId: string; + } +} From 2497b395c9336f3e8bb2ac84333982214a34ef2a Mon Sep 17 00:00:00 2001 From: LMS10 Date: Mon, 4 May 2026 19:31:28 +0900 Subject: [PATCH 02/10] =?UTF-8?q?=E2=9C=A8=20Feat:=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8/=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20API=20?= =?UTF-8?q?=ED=98=B8=EC=B6=9C=20=ED=95=A8=EC=88=98=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/auth-api.ts | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 src/lib/auth-api.ts diff --git a/src/lib/auth-api.ts b/src/lib/auth-api.ts new file mode 100644 index 0000000..ae1771a --- /dev/null +++ b/src/lib/auth-api.ts @@ -0,0 +1,44 @@ +import { AuthResponse, SigninRequest, SignupRequest } from '@/types/auth'; + +const API_URL = process.env.NEXT_PUBLIC_API_URL; + +export class ApiError extends Error { + constructor( + public status: number, + message: string, + ) { + super(message); + } +} + +export async function signupApi(body: SignupRequest): Promise { + const res = await fetch(`${API_URL}/api/v1/auth/signup`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }); + + const data: AuthResponse = await res.json(); + + if (!res.ok || !data.success) { + throw new ApiError(res.status, '회원가입에 실패했습니다.'); + } + + return data; +} + +export async function signinApi(body: SigninRequest): Promise { + const res = await fetch(`${API_URL}/api/v1/auth/signin`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }); + + const data: AuthResponse = await res.json(); + + if (!res.ok || !data.success) { + throw new ApiError(res.status, '이메일 혹은 비밀번호를 확인해주세요.'); + } + + return data; +} From f577ed66b271b6cf111d998873c6027e57bf4616 Mon Sep 17 00:00:00 2001 From: LMS10 Date: Mon, 4 May 2026 19:50:53 +0900 Subject: [PATCH 03/10] =?UTF-8?q?=E2=9C=A8=20Feat:=20NextAuth=20Credential?= =?UTF-8?q?s=20=EC=84=A4=EC=A0=95=20=EB=B0=8F=20=EB=9D=BC=EC=9A=B0?= =?UTF-8?q?=ED=8A=B8=20=ED=95=B8=EB=93=A4=EB=9F=AC=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/api/auth/[...nextauth]/route.ts | 62 +++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 src/app/api/auth/[...nextauth]/route.ts diff --git a/src/app/api/auth/[...nextauth]/route.ts b/src/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 0000000..731e18f --- /dev/null +++ b/src/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1,62 @@ +import NextAuth, { NextAuthOptions } from 'next-auth'; +import CredentialsProvider from 'next-auth/providers/credentials'; +import { signinApi } from '@/lib/auth-api'; + +export const authOptions: NextAuthOptions = { + providers: [ + CredentialsProvider({ + name: 'Credentials', + credentials: { + email: { label: 'Email', type: 'email' }, + password: { label: 'Password', type: 'password' }, + }, + async authorize(credentials) { + if (!credentials?.email || !credentials?.password) return null; + + try { + const res = await signinApi({ + email: credentials.email, + password: credentials.password, + }); + + const user = res.data; + + return { + id: String(user.userId), + email: user.email, + name: user.name, + accessToken: user.accessToken, + }; + } catch { + return null; + } + }, + }), + ], + + callbacks: { + async jwt({ token, user }) { + if (user) { + token.accessToken = user.accessToken; + token.userId = user.id; + } + return token; + }, + async session({ session, token }) { + session.accessToken = token.accessToken; + session.user.id = token.userId; + return session; + }, + }, + + pages: { + signIn: '/login', + }, + + session: { + strategy: 'jwt', + }, +}; + +const handler = NextAuth(authOptions); +export { handler as GET, handler as POST }; From 0e48385adb34f872b14a85474fb87296ae2b8a46 Mon Sep 17 00:00:00 2001 From: LMS10 Date: Mon, 4 May 2026 19:51:23 +0900 Subject: [PATCH 04/10] =?UTF-8?q?=E2=9C=A8=20Feat:=20=EC=9D=B8=EC=A6=9D=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=20=EA=B8=B0=EB=B0=98=20=EB=9D=BC=EC=9A=B0?= =?UTF-8?q?=ED=8A=B8=20=EB=B3=B4=ED=98=B8=20=EB=AF=B8=EB=93=A4=EC=9B=A8?= =?UTF-8?q?=EC=96=B4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/middleware.ts | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 src/middleware.ts diff --git a/src/middleware.ts b/src/middleware.ts new file mode 100644 index 0000000..bc768ea --- /dev/null +++ b/src/middleware.ts @@ -0,0 +1,26 @@ +import { getToken } from 'next-auth/jwt'; +import { NextResponse, NextRequest } from 'next/server'; + +export async function middleware(req: NextRequest) { + const token = await getToken({ req, secret: process.env.NEXTAUTH_SECRET }); + const isLoggedIn = !!token; + const { pathname } = req.nextUrl; + + const isAuthPage = pathname.startsWith('/login') || pathname.startsWith('/signup'); + + const isProtectedPage = pathname.startsWith('/workspace') || pathname.startsWith('/mypage'); + + if (isProtectedPage && !isLoggedIn) { + return NextResponse.redirect(new URL('/login', req.url)); + } + + if (isAuthPage && isLoggedIn) { + return NextResponse.redirect(new URL('/workspace', req.url)); + } + + return NextResponse.next(); +} + +export const config = { + matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'], +}; From 3da492209c5940213cf747ed4977cf98df99fe7c Mon Sep 17 00:00:00 2001 From: LMS10 Date: Mon, 4 May 2026 19:52:25 +0900 Subject: [PATCH 05/10] =?UTF-8?q?=E2=9C=A8=20Feat:=20SessionProvider=20?= =?UTF-8?q?=EB=9E=98=ED=8D=BC=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20=EB=B0=8F=20=EB=A3=A8=ED=8A=B8=20=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EC=95=84=EC=9B=83=EC=97=90=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/layout.tsx | 3 ++- src/components/AuthProvider.tsx | 7 +++++++ 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 src/components/AuthProvider.tsx diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 7b4da5d..2b03a3c 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,5 +1,6 @@ import localFont from 'next/font/local'; import './globals.css'; +import AuthProvider from '@/components/AuthProvider'; import ModalRoot from '@/components/modal/ModalRoot'; const pretendard = localFont({ @@ -16,7 +17,7 @@ export default function RootLayout({ return ( - {children} + {children}