ID: key_26_22_05_25_P1_1_5 Created date: 5월 25 2026 월요일
연관 문서
개발 일정 > Phase 1 UI 개발 | UI (web-plate)
개요
next-auth v5를 활용한 로그인·회원가입 페이지를 구현한다.
서버 Auth API(1-2_서버-Auth-API)와 연동하여 세션을 관리한다.
- 예상 소요: 2~3일
- 선행 조건: 1-2_서버-Auth-API, P6_CLAUDE-md-UI 완료
- 완료 기준: 브라우저에서 로그인 성공 후
/home리다이렉트 동작 확인
현재 web-plate 구현 현황
기존 코드 충돌 분석
아래 충돌 항목을 반드시 확인하고 작업 지시서대로 수정할 것
| 항목 | 작업 지시서 | 현재 web-plate 상태 | 충돌 여부 |
|---|---|---|---|
| next-auth 버전 | v5 | v5 beta.30 설치됨 | ✅ 일치 |
| auth 설정 파일 | src/auth.ts | src/auth.ts 존재 | ✅ 일치 (수정 필요) |
[...nextauth]/route.ts | handlers 재export | 이미 존재 | ✅ 유지 |
| Credentials authorize | CIRA 백엔드 연동 | hardcoded admin/admin | ⚠️ 수정 필요 |
| 로그인 필드 | email | username 사용 중 | ⚠️ 수정 필요 |
| 회원가입 개념 | 이메일/비번 신규 가입 | Google OAuth 후 프로필 등록 | ⚠️ 목적 충돌 → 해결 방안 참조 |
| 세션 토큰 | accessToken (CIRA JWT) | idToken (Google OAuth) | ⚠️ 확장 필요 |
| API 클라이언트 | axios 인터셉터 | fetch + /api/proxy/ 패턴 사용 중 | ⚠️ proxy 방식 유지 + 토큰 주입 추가 |
| 리다이렉트 경로 | /projects | /todo → /home | ⚠️ /home 으로 통일 |
| 미들웨어 | 신규 구현 | 이미 구현 완료 | ✅ 경로 추가만 필요 |
| 폼 유효성 | react-hook-form + zod | 미설치 | ⚠️ Server Action 방식으로 대체 |
| Google OAuth | 미포함 | 이미 구성됨 | ✅ 유지 |
구현 화면 목록
| 경로 | 설명 | 현황 |
|---|---|---|
/login | 로그인 페이지 | 기존 파일 수정 |
/cira/register | CIRA 이메일 회원가입 | 신규 생성 (기존 /register와 분리) |
회원가입 경로 분리 이유
기존/register는 Google OAuth 후 프로필 등록 용도로 다른 서비스에서 사용 중.
CIRA 전용 이메일 회원가입은/cira/register로 분리하여 충돌 방지.
Claude Code 작업 수행서
목표
기존 src/auth.ts의 Credentials provider를 CIRA 백엔드 API와 연동하고,
로그인 폼 및 CIRA 전용 회원가입 페이지를 구현한다.
기존 Google OAuth 흐름과 proxy API 패턴은 그대로 유지한다.
작업 지시
web-plate 프로젝트의 인증 기능을 CIRA 백엔드와 연동하도록 수정해줘.
기존 Google OAuth 설정 및 /api/proxy/ 패턴은 변경하지 않는다.
[프로젝트 경로]
- C:\workspace\tsh\boilerplate\fe\web-plate
[현재 프로젝트 스택 확인]
- Next.js 16.1.3, React 19
- next-auth v5 (beta.30) — src/auth.ts 중앙 설정
- API 패턴: fetch + /api/proxy/[...path]/route.ts
- 스타일: Tailwind CSS
- 상태관리: zustand (설치됨, 미사용)
- react-hook-form, zod: 미설치 → Server Action 방식으로 대체
---
[수행 작업 1] src/auth.ts — Credentials authorize 수정
기존 hardcoded 로직을 CIRA 백엔드 호출로 교체.
Google 설정은 변경하지 않는다.
변경 전:
async authorize(credentials) {
if (credentials?.username === "admin" && credentials?.password === "admin") {
return { id: "1", name: "Admin", email: "admin@tsh.com" };
}
return null;
}
변경 후:
credentials: {
email: { label: "Email" },
password: { label: "Password", type: "password" },
},
async authorize(credentials) {
try {
const res = await fetch(`${process.env.BACKEND_URL}/api/v1/auth/login`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
email: credentials?.email,
password: credentials?.password,
}),
});
if (!res.ok) return null;
const data = await res.json();
// 백엔드 응답: { success: true, data: { accessToken, user: { id, email, name } } }
return {
id: data.data.user.id,
email: data.data.user.email,
name: data.data.user.name,
accessToken: data.data.accessToken,
};
} catch {
return null;
}
}
callbacks 수정 (idToken 유지 + accessToken 추가):
async jwt({ token, account, user }) {
if (account) {
token.idToken = account.id_token; // Google OAuth — 기존 유지
}
if (user) {
token.accessToken = (user as any).accessToken; // CIRA JWT
}
return token;
},
async session({ session, token }) {
session.idToken = token.idToken as string; // 기존 유지
session.accessToken = token.accessToken as string; // CIRA JWT 추가
return session;
},
---
[수행 작업 2] src/next-auth.d.ts — 타입 확장
기존 idToken에 accessToken 추가:
declare module "next-auth" {
interface Session {
idToken?: string;
accessToken?: string; // CIRA JWT 추가
user: {
id?: string;
} & DefaultSession["user"];
}
}
declare module "next-auth/jwt" {
interface JWT {
idToken?: string;
accessToken?: string; // CIRA JWT 추가
}
}
---
[수행 작업 3] src/app/(auth)/login/page.tsx — 기존 파일 수정
변경 사항:
1. Server Action의 signIn 필드: username → email
2. 로그인 성공 redirectTo: "/todo" → "/home"
3. 입력 필드 placeholder 및 name 속성 수정
4. 이메일 입력 type="text" → type="email"
5. "CIRA 회원가입" 링크 추가 → /cira/register
최종 폼 구조:
<form action={async (formData) => {
"use server";
await signIn("credentials", {
email: formData.get("email"),
password: formData.get("password"),
redirectTo: "/home",
});
}}>
<input type="email" name="email" placeholder="이메일" ... />
<input type="password" name="password" placeholder="비밀번호" ... />
<button type="submit">로그인</button>
</form>
<a href="/cira/register">CIRA 계정 회원가입</a>
---
[수행 작업 4] src/app/(auth)/cira/register/page.tsx — 신규 생성
기존 /register는 Google OAuth 후 프로필 등록 용도이므로 수정하지 않는다.
CIRA 전용 이메일/비밀번호 가입 페이지를 /cira/register 경로로 신규 생성.
"use server" Server Action으로 구현:
async function registerAction(formData: FormData) {
"use server";
const name = formData.get("name") as string;
const email = formData.get("email") as string;
const password = formData.get("password") as string;
const confirm = formData.get("confirm") as string;
// 서버 사이드 기본 검증
if (password !== confirm) {
// redirect with error param
redirect("/cira/register?error=password_mismatch");
}
const res = await fetch(`${process.env.BACKEND_URL}/api/v1/auth/register`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name, email, password }),
});
if (!res.ok) {
redirect("/cira/register?error=register_failed");
}
redirect("/login?registered=true");
}
폼 필드: 이름, 이메일(type=email), 비밀번호(type=password), 비밀번호 확인
에러 표시: URL searchParams의 error 값을 읽어 인라인 메시지 표시
---
[수행 작업 5] src/middleware.ts — CIRA 경로 추가
기존 미들웨어는 수정하지 않고 matcher에 CIRA 경로만 추가:
export const config = {
matcher: [
"/((?!api/auth|_next/static|_next/image|favicon.ico).*)",
],
};
// 기존 auth 미들웨어 로직 그대로 유지
// /cira/register는 인증 없이 접근 가능하도록 isRegisterPage 조건에 추가:
const isRegisterPage = pathname === "/register" || pathname === "/cira/register";
---
[수행 작업 6] src/app/api/proxy/[...path]/route.ts — Authorization 헤더 주입
기존 proxy 패턴을 유지하되 CIRA accessToken을 서버에서 주입.
auth() 함수로 서버 사이드 세션 조회 후 헤더 추가:
import { auth } from "@/auth";
// proxyHandler 내부 api.fetcher 호출 전:
const session = await auth();
const authHeader = session?.accessToken
? { Authorization: `Bearer ${session.accessToken}` }
: {};
const result = await api.fetcher(targetPath, {
method,
headers: authHeader, // Authorization 헤더 추가
body: body ? JSON.stringify(body) : undefined,
});
---
[수행 작업 7] .env.local 확인
BACKEND_URL 이 CIRA 서버를 가리키도록 확인/수정:
BACKEND_URL=http://localhost:8080 (로컬 CIRA 서버)
---
[검증]
1. 이메일/비밀번호로 로그인 → /home 리다이렉트 확인
2. session.accessToken 값이 존재하는지 확인 (서버 컴포넌트에서 auth() 호출)
3. /api/proxy/ 경로 호출 시 Authorization 헤더가 전달되는지 서버 로그 확인
4. 미인증 상태로 /home 접근 시 /login 리다이렉트 확인
5. /cira/register 에서 회원가입 후 /login?registered=true 이동 확인
[변경하지 않는 것]
- Google OAuth 설정 (src/auth.ts의 Google provider)
- 기존 /register 페이지 (Google OAuth 후 프로필 등록 흐름)
- /api/proxy/ 기본 구조
- middleware.ts 핵심 로직
- 기존 설치된 패키지 목록 (react-hook-form, zod 미설치 유지)
완료 기준
| 항목 | 기준 |
|---|---|
| 이메일 로그인 | CIRA 백엔드 인증 성공 → /home 리다이렉트 |
| Google 로그인 | 기존 동작 유지 (회귀 없음) |
| CIRA 회원가입 | /cira/register → 가입 → /login 이동 |
| accessToken | session.accessToken 값 존재 확인 |
| proxy 인증 헤더 | /api/proxy/ 호출 시 Bearer 헤더 포함 |
| 미인증 접근 | /home, /cira/* 접근 시 /login 리다이렉트 |