# 인증 및 API 통신 ## 인증 아키텍처 SAM ERP는 **HttpOnly Cookie 기반 인증**을 사용합니다. JavaScript에서 토큰을 직접 접근할 수 없으므로, 모든 인증 API 호출은 **Next.js API Proxy**를 통해 처리됩니다. ``` 클라이언트 (브라우저) │ ├── Server Action 호출 (useEffect에서) │ └── serverFetch() → 서버에서 쿠키 읽기 → 백엔드 API 호출 │ └── 프록시 API 호출 (fetch('/api/proxy/...')) └── API Proxy Route → 서버에서 쿠키 읽기 → 백엔드 API 호출 ``` ## 쿠키 구조 | 쿠키명 | HttpOnly | 용도 | Max-Age | |--------|:--------:|------|---------| | `access_token` | O | API 인증 토큰 | 2시간 | | `refresh_token` | O | 토큰 갱신용 | 7일 | | `is_authenticated` | X | 클라이언트 인증 상태 확인 | 2시간 | - `access_token`, `refresh_token`: HttpOnly → JavaScript 접근 불가 (XSS 방지) - `is_authenticated`: non-HttpOnly → 클라이언트에서 인증 상태 확인 가능 (FCM 등) - 프로덕션: `Secure` 플래그 활성화 (HTTPS만) - `SameSite=Lax`: CSRF 방지 ## 인증 흐름 ### 로그인 ``` 1. 사용자 → /api/auth/login (POST) 2. 백엔드 → access_token + refresh_token 반환 3. API Route → Set-Cookie (HttpOnly) 설정 4. 클라이언트 → /(protected)/dashboard 리다이렉트 ``` ### API 요청 (Server Action) ``` 1. 클라이언트 → Server Action 호출 2. serverFetch() → 쿠키에서 access_token 읽기 3. authenticatedFetch() → Authorization 헤더에 토큰 추가 4. 백엔드 API 호출 → 응답 반환 ``` ### 토큰 만료 시 (401 자동 갱신) ``` 1. API 요청 → 401 응답 수신 2. authenticatedFetch() → refresh_token으로 갱신 요청 3. 새 토큰 수신 → 쿠키 업데이트 4. 원래 요청 재시도 → 성공 5. 갱신 실패 → 쿠키 삭제 → /login 리다이렉트 ``` ### 토큰 갱신 중복 방지 ```typescript // globalThis 레벨 캐싱 (5초) // 여러 요청이 동시에 401을 받아도 refresh는 1회만 실행 // 진행 중인 refresh Promise를 공유하여 대기 ``` ## API 프록시 (`/api/proxy/[...path]`) 클라이언트에서 직접 백엔드 API를 호출해야 하는 경우 프록시 사용: ```typescript // 클라이언트에서 프록시 호출 const response = await fetch('/api/proxy/item-master/init'); const data = await response.json(); ``` 프록시 내부 동작: 1. HttpOnly 쿠키에서 `access_token` 읽기 2. 백엔드 URL 구성 (`/api/proxy/*` → 백엔드 `/*`) 3. `Authorization: Bearer {token}` 헤더 추가 4. 요청 전달 → 응답 반환 5. 401 시 자동 토큰 갱신 후 재시도 6. 새 토큰 → Set-Cookie 헤더로 클라이언트에 전달 ## 인증 보호 ### Protected Layout ```tsx // (protected)/layout.tsx export default function ProtectedLayout({ children }) { // 인증 가드 (뒤로가기 캐시 감지) useAuthGuard(); return ( {/* 401 에러 자동 처리 */} {/* 푸시 알림 */} {/* 권한 기반 접근 제어 */} {children} ); } ``` ### 인증 상태 확인 (클라이언트) ```typescript import { hasAuthToken } from '@/lib/api/auth-headers'; // is_authenticated 쿠키 확인 (non-HttpOnly) if (hasAuthToken()) { // 인증됨 } ``` ## 로그아웃 완전한 로그아웃 절차: 1. Zustand 스토어 초기화 (useAuthStore, useMasterDataStore, useItemMasterStore) 2. sessionStorage 캐시 삭제 (page_config_*, mes-*) 3. localStorage 사용자 데이터 삭제 4. FCM 토큰 해제 (Capacitor 환경) 5. 서버 로그아웃 API 호출 6. /login 리다이렉트 ## 주의사항 - **Server Component에서 쿠키 수정 불가** → Client Component 사용 필수 - **`alert()`, `confirm()`, `prompt()` 사용 금지** → Radix UI Dialog 또는 `toast` 사용 - **API 직접 호출 금지** → 반드시 Server Action 또는 프록시 사용