# JWT + Cookie + Middleware 인증 설계 (최종)

**확정된 API 정보:**
- 인증 방식: Bearer Token (JWT)
- 로그인: `POST /api/v1/login`
- 응답: `{ token: "xxx" }`
- Token 저장: **쿠키** (Middleware 접근 가능)

## ✅ 핵심 발견

**JWT도 쿠키에 저장하면 Middleware에서 처리 가능합니다!**

```typescript
// middleware.ts에서 JWT 토큰 쿠키 접근
const authToken = request.cookies.get('auth_token'); // ✅ 가능!

if (!authToken) {
  redirect('/login');
}
```

따라서 **기존 Middleware 설계를 거의 그대로 사용**할 수 있습니다.

---

## 📋 아키텍처 (기존과 동일)

```
┌─────────────────────────────────────────────────────────────┐
│                      Next.js Frontend                        │
├─────────────────────────────────────────────────────────────┤
│  Middleware (Server)                                         │
│  ├─ Bot Detection (기존)                                     │
│  ├─ Authentication Check (신규)                              │
│  │  ├─ JWT Token 쿠키 확인                                  │
│  │  └─ 없으면 /login 리다이렉트                            │
│  └─ i18n Routing (기존)                                      │
├─────────────────────────────────────────────────────────────┤
│  JWT Client (lib/auth/jwt-client.ts)                        │
│  ├─ Token을 쿠키에 저장                                      │
│  ├─ API 호출 시 Authorization 헤더 추가                     │
│  └─ 401 응답 시 자동 로그아웃                               │
├─────────────────────────────────────────────────────────────┤
│  Auth Context (contexts/AuthContext.tsx)                    │
│  ├─ 사용자 정보 관리                                         │
│  └─ login/logout 함수                                        │
└─────────────────────────────────────────────────────────────┘
                            ↓ HTTP + Cookie + Authorization
┌─────────────────────────────────────────────────────────────┐
│                    Laravel Backend                           │
├─────────────────────────────────────────────────────────────┤
│  JWT Middleware                                              │
│  └─ Bearer Token 검증                                        │
├─────────────────────────────────────────────────────────────┤
│  API Endpoints                                               │
│  ├─ POST /api/v1/login     → { token: "xxx" }               │
│  ├─ POST /api/v1/register                                    │
│  ├─ GET  /api/v1/user                                        │
│  └─ POST /api/v1/logout                                      │
└─────────────────────────────────────────────────────────────┘
```

---

## 🔐 인증 플로우

### 1. 로그인

```
1. POST /api/v1/login
   → { token: "eyJhbGci..." }

2. Token을 쿠키에 저장
   document.cookie = 'auth_token=xxx; Secure; SameSite=Strict'

3. /dashboard 리다이렉트

4. Middleware가 쿠키 확인 ✓

5. 페이지 렌더링
```

### 2. API 호출

```
1. 쿠키에서 Token 읽기
2. Authorization 헤더에 추가
   Authorization: Bearer xxx
3. Laravel이 JWT 검증
4. 데이터 반환
```

### 3. 보호된 페이지 접근

```
사용자 → /dashboard
    ↓
Middleware 실행
    ↓
auth_token 쿠키 확인
    ↓
있음 → 페이지 표시
없음 → /login 리다이렉트
```

---

## 🛠️ 핵심 구현

### 1. Token 저장 (lib/auth/token-storage.ts)

```typescript
export const tokenStorage = {
  /**
   * JWT를 쿠키에 저장
   * - Middleware에서 접근 가능
   * - Secure + SameSite로 보안 강화
   */
  set(token: string): void {
    const maxAge = 86400; // 24시간
    document.cookie = `auth_token=${token}; path=/; max-age=${maxAge}; SameSite=Strict; Secure`;
  },

  /**
   * 쿠키에서 Token 읽기
   * - 클라이언트에서만 사용
   */
  get(): string | null {
    if (typeof window === 'undefined') return null;

    const match = document.cookie.match(/auth_token=([^;]+)/);
    return match ? match[1] : null;
  },

  /**
   * Token 삭제
   */
  remove(): void {
    document.cookie = 'auth_token=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT';
  }
};
```

### 2. JWT Client (lib/auth/jwt-client.ts)

```typescript
import { tokenStorage } from './token-storage';

class JwtClient {
  private baseURL = 'https://api.5130.co.kr';

  /**
   * 로그인
   */
  async login(email: string, password: string): Promise<User> {
    const response = await fetch(`${this.baseURL}/api/v1/login`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email, password }),
    });

    if (!response.ok) {
      throw new Error('Login failed');
    }

    const { token } = await response.json();

    // ✅ Token을 쿠키에 저장
    tokenStorage.set(token);

    // 사용자 정보 조회
    return await this.getCurrentUser();
  }

  /**
   * 현재 사용자 정보
   */
  async getCurrentUser(): Promise<User> {
    const token = tokenStorage.get();

    if (!token) {
      throw new Error('No token');
    }

    const response = await fetch(`${this.baseURL}/api/v1/user`, {
      headers: {
        'Authorization': `Bearer ${token}`, // ✅ Authorization 헤더
      },
    });

    if (response.status === 401) {
      tokenStorage.remove();
      throw new Error('Unauthorized');
    }

    return await response.json();
  }

  /**
   * 로그아웃
   */
  async logout(): Promise<void> {
    const token = tokenStorage.get();

    if (token) {
      await fetch(`${this.baseURL}/api/v1/logout`, {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${token}`,
        },
      });
    }

    // ✅ 쿠키 삭제
    tokenStorage.remove();
  }
}

export const jwtClient = new JwtClient();
```

### 3. Middleware (middleware.ts) - 기존과 거의 동일!

```typescript
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import createIntlMiddleware from 'next-intl/middleware';
import { locales, defaultLocale } from '@/i18n/config';

const intlMiddleware = createIntlMiddleware({
  locales,
  defaultLocale,
  localePrefix: 'as-needed',
});

// 보호된 라우트
const PROTECTED_ROUTES = [
  '/dashboard',
  '/profile',
  '/settings',
  '/admin',
  '/tenant',
  '/users',
  '/reports',
];

// 공개 라우트
const PUBLIC_ROUTES = [
  '/',
  '/login',
  '/register',
  '/about',
  '/contact',
];

function isProtectedRoute(pathname: string): boolean {
  return PROTECTED_ROUTES.some(route => pathname.startsWith(route));
}

function isPublicRoute(pathname: string): boolean {
  return PUBLIC_ROUTES.some(route => pathname === route || pathname.startsWith(route));
}

function stripLocale(pathname: string): string {
  for (const locale of locales) {
    if (pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`) {
      return pathname.slice(`/${locale}`.length) || '/';
    }
  }
  return pathname;
}

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;

  // 1. Bot Detection (기존 로직)
  // ... bot check code ...

  // 2. 정적 파일 제외
  if (
    pathname.includes('/_next/') ||
    pathname.includes('/api/') ||
    pathname.match(/\.(ico|png|jpg|jpeg|svg|gif|webp)$/)
  ) {
    return intlMiddleware(request);
  }

  // 3. 로케일 제거
  const pathnameWithoutLocale = stripLocale(pathname);

  // 4. ✅ JWT Token 쿠키 확인
  const authToken = request.cookies.get('auth_token');
  const isAuthenticated = !!authToken;

  // 5. 보호된 라우트 체크
  if (isProtectedRoute(pathnameWithoutLocale) && !isAuthenticated) {
    const url = new URL('/login', request.url);
    url.searchParams.set('redirect', pathname);
    return NextResponse.redirect(url);
  }

  // 6. 게스트 전용 라우트 (이미 로그인한 경우)
  if (
    (pathnameWithoutLocale === '/login' || pathnameWithoutLocale === '/register') &&
    isAuthenticated
  ) {
    return NextResponse.redirect(new URL('/dashboard', request.url));
  }

  // 7. i18n 미들웨어
  return intlMiddleware(request);
}

export const config = {
  matcher: [
    '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp|ico)$).*)',
  ],
};
```

**변경 사항:**
```diff
- const sessionCookie = request.cookies.get('laravel_session');
+ const authToken = request.cookies.get('auth_token');
```

거의 동일합니다!

### 4. Auth Context (contexts/AuthContext.tsx)

```typescript
'use client';

import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import { jwtClient } from '@/lib/auth/jwt-client';
import { useRouter } from 'next/navigation';

interface User {
  id: number;
  name: string;
  email: string;
}

interface AuthContextType {
  user: User | null;
  loading: boolean;
  login: (email: string, password: string) => Promise<void>;
  logout: () => Promise<void>;
}

const AuthContext = createContext<AuthContextType | undefined>(undefined);

export function AuthProvider({ children }: { children: ReactNode }) {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);
  const router = useRouter();

  // 초기 로드 시 사용자 정보 가져오기
  useEffect(() => {
    jwtClient.getCurrentUser()
      .then(setUser)
      .catch(() => setUser(null))
      .finally(() => setLoading(false));
  }, []);

  const login = async (email: string, password: string) => {
    const user = await jwtClient.login(email, password);
    setUser(user);
    router.push('/dashboard');
  };

  const logout = async () => {
    await jwtClient.logout();
    setUser(null);
    router.push('/login');
  };

  return (
    <AuthContext.Provider value={{ user, loading, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
}

export function useAuth() {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error('useAuth must be used within AuthProvider');
  }
  return context;
}
```

---

## 📊 세션 쿠키 vs JWT 쿠키 비교

| 항목 | 세션 쿠키 (Sanctum) | JWT 쿠키 (현재) |
|------|---------------------|------------------|
| **쿠키 이름** | `laravel_session` | `auth_token` |
| **Middleware 접근** | ✅ 가능 | ✅ 가능 |
| **인증 체크** | 쿠키 존재 확인 | 쿠키 존재 확인 |
| **API 호출** | 쿠키 자동 포함 | Authorization 헤더 |
| **CSRF 토큰** | ✅ 필요 | ❌ 불필요 |
| **서버 상태** | Stateful (세션 저장) | Stateless |
| **보안** | HTTP-only 가능 | Secure + SameSite |
| **구현 복잡도** | 동일 | 동일 |

**결론:** Middleware 관점에서는 거의 동일합니다!

---

## 🎯 구현 순서

### Phase 1: 기본 인프라 (30분)
- [x] auth-config.ts
- [ ] token-storage.ts
- [ ] jwt-client.ts
- [ ] types/auth.ts

### Phase 2: Middleware 통합 (20분)
- [ ] middleware.ts 업데이트
  - JWT 토큰 쿠키 체크
  - Protected routes 가드

### Phase 3: Auth Context (20분)
- [ ] AuthContext.tsx
- [ ] layout.tsx에 AuthProvider 추가

### Phase 4: 로그인 페이지 (40분)
- [ ] /login/page.tsx
- [ ] LoginForm 컴포넌트
- [ ] Form validation (react-hook-form + zod)

### Phase 5: 테스트 (30분)
- [ ] 로그인 → 대시보드
- [ ] 비로그인 → 대시보드 → /login 튕김
- [ ] 로그아웃 → 다시 튕김

**총 소요시간: 약 2시간 20분**

---

## ✅ 최종 정리

### 핵심 포인트

1. **JWT를 쿠키에 저장** → Middleware 접근 가능
2. **기존 Middleware 설계 유지** → 가드 컴포넌트 불필요
3. **차이점은 미미함:**
   - 쿠키 이름: `laravel_session` → `auth_token`
   - CSRF 토큰 불필요
   - API 호출 시 Authorization 헤더 추가

### 장점

- ✅ Middleware에서 서버사이드 인증 체크
- ✅ 클라이언트 가드 컴포넌트 불필요
- ✅ 중복 코드 제거
- ✅ 기존 설계(authentication-design.md) 거의 그대로 사용

### 변경 사항

**최소한의 변경만 필요:**
```typescript
// 1. Token 저장: 쿠키 사용
tokenStorage.set(token);

// 2. Middleware: 쿠키 이름만 변경
const authToken = request.cookies.get('auth_token');

// 3. API 호출: Authorization 헤더 추가
headers: { 'Authorization': `Bearer ${token}` }

// 4. CSRF 토큰: 제거
// getCsrfToken() 불필요
```

---

## 🚀 다음 단계

1. ✅ 설계 확정 완료
2. ⏳ 디자인 컴포넌트 대기
3. ⏳ 백엔드 API 엔드포인트 확인
   - POST /api/v1/register
   - GET /api/v1/user
   - POST /api/v1/logout
4. 🚀 구현 시작 (2-3시간)

**준비되면 바로 시작합니다!** 🎯

---

## 관련 파일

### 프론트엔드
- `src/middleware.ts` - 인증 미들웨어
- `src/lib/api/auth/auth-config.ts` - 인증 설정 (라우트, URL)
- `src/lib/api/auth/token-storage.ts` - Token 저장 관리
- `src/lib/api/auth/jwt-client.ts` - JWT 클라이언트
- `src/contexts/AuthContext.tsx` - 클라이언트 인증 상태 관리
- `src/app/[locale]/(auth)/login/page.tsx` - 로그인 페이지
- `src/app/[locale]/(protected)/dashboard/page.tsx` - 보호된 페이지

### 설정 파일
- `.env.local` - 환경 변수 (API URL, API Key)
- `next.config.ts` - Next.js 설정