diff --git a/docs/auth-service-refactoring-guide.md b/docs/auth-service-refactoring-guide.md index bc092147d..c3db77ca2 100644 --- a/docs/auth-service-refactoring-guide.md +++ b/docs/auth-service-refactoring-guide.md @@ -1,32 +1,97 @@ # 서비스별 인증 처리 리팩토링 가이드 -## 1) 문서 목적 +## 1) 목적과 원칙 -여러 서비스 앱에서 중복 구현된 인증 로직을 공통 모듈(`@common/auth`) 기반으로 통일하기 위한 표준 가이드 +여러 서비스 앱에 흩어진 인증 로직을 `@common/auth` 기반으로 통일하기 위한 표준 가이드입니다. +이 문서를 **위에서 아래로** 적용하면 `authService` + `AuthWrapper` 패턴이 갖춰집니다. 핵심 원칙: -- 인증 정책은 공통 모듈에 모은다. -- 서비스별 차이는 설정(`authConfig`)으로만 표현한다. -- 토큰 갱신 타이머는 JWT `exp` 기준으로 계산한다. +- 인증 **정책·유틸**은 `@common/auth`에 모은다. +- 서비스별 차이는 `authConfig` 설정으로만 표현한다. +- 앱은 API 오케스트레이션(`authService`)과 진입점(`AuthWrapper`)만 구현한다. +- `login` / `silentRefresh` 성공 시 토큰을 저장한다. +- 세션 연장은 confirm **확인** 시 `silentRefresh`로 수행한다. + → [세션 연장 Confirm 적용 가이드](./session-extend-confirm-guide.md) -## 2) 공통 아키텍처 +참고 구현: `apps/as/src/entities/auth/api/auth-service.ts` -인증 관련 책임을 아래처럼 분리한다. +--- -- `shared/config/env.ts`: 환경 변수 노출 -- `shared/config/auth.ts`: 서비스별 인증 키/도메인 설정 -- `entities/auth/lib/token-manager.ts`: 공통 토큰 매니저 생성/re-export -- `entities/auth/api/*`: 로그인/토큰 갱신 API 오케스트레이션 -- 레이아웃: BaseLayout 컴포넌트에 `tokenManager` 주입 +## 2) 목표 아키텍처 -## 3) 단계별 리팩토링 방법 +### 2-1. 레이어 책임 -### 3-1. 환경 변수 정의 통일 (`shared/config/env.ts`) +| 레이어 | 경로 | 역할 | +| --- | --- | --- | +| 환경 변수 | `shared/config/env.ts` | `CLAIM_NAME` 등 키/도메인 값 노출 | +| 인증 설정 | `shared/config/auth.ts` | env → `AuthConfig` 매핑 | +| 토큰 매니저 | `entities/auth/lib/token-manager.ts` | `createTokenManager(authConfig)` + re-export | +| 인증 서비스 | `entities/auth/api/auth-service.ts` | login / silentRefresh / declineSessionExtend / logout | +| 인증 래퍼 | `entities/auth/lib/auth-wrapper.tsx` | auth 체크, `auth:logout` 수신 | +| 배럴 | `entities/auth/index.ts` | 외부 import 경로 통일 | +| 레이아웃 | `BaseLayout` 등 | `authService`를 Header까지 전달 | -서비스마다 이름이 다른 인증 환경 변수를 한 곳에서 노출한다. +### 2-2. 런타임 흐름 -예시 코드: +```mermaid +flowchart TD + A[login / silentRefresh 성공] --> B[setToken] + C[세션 연장 confirm 확인] --> D[silentRefresh] + D --> B + E[세션 연장 confirm 취소] --> F[declineSessionExtend] + F --> G[clearRefreshTimer] + H[갱신 실패 / logout] --> I[clearAuthToken + auth:logout] +``` + +세션 연장 confirm UX는 [세션 연장 가이드](./session-extend-confirm-guide.md)에서 적용한다. + +--- + +## 3) 공통 계약 + +### 3-1. AuthConfig + +```ts +interface AuthConfig { + authUserId: string; + claimName: string; + cookieDomain?: string; + accessToken?: string; + refreshToken?: string; +} +``` + +### 3-2. AuthService + +```ts +interface AuthService { + login(payload: AuthPayload): Promise; + silentRefresh(): Promise; + declineSessionExtend(): void; + logout(): Promise; +} +``` + +### 3-3. TokenManager API + +앱에서 사용하는 메서드: + +| 메서드 | 용도 | +| --- | --- | +| `setToken` / `getToken` | 쿠키 저장·조회 | +| `clearRefreshTimer` | `declineSessionExtend`에 연결 | +| `clearAuthToken` | 인증 쿠키 일괄 삭제 | + +--- + +## 4) 적용 순서 + +의존 관계 순으로 진행한다. + +### 4-1. 환경 변수 통일 + +파일: `shared/config/env.ts` ```ts export const CLAIM_NAME = process.env.NEXT_PUBLIC_CLAIM_NAME || "Authorization"; @@ -38,11 +103,9 @@ export const REFRESH_TOKEN = export const COOKIE_DOMAIN = process.env.NEXT_PUBLIC_COOKIE_DOMAIN; ``` -### 3-2. 서비스별 인증 설정 분리 (`shared/config/auth.ts`) +### 4-2. 인증 설정 -하드코딩된 키명을 제거하고 `AuthConfig`로 매핑한다. - -예시 코드: +파일: `shared/config/auth.ts` ```ts import type { AuthConfig } from "@common/auth"; @@ -63,11 +126,9 @@ export const authConfig: AuthConfig = { }; ``` -### 3-3. 토큰 매니저 공통화 (`entities/auth/lib/token-manager.ts`) +### 4-3. 토큰 매니저 -서비스 고유 `TokenManager` 구현을 제거하고 `createTokenManager`를 사용한다. - -예시 코드: +파일: `entities/auth/lib/token-manager.ts` ```ts import { authConfig } from "@/shared/config/auth"; @@ -79,109 +140,148 @@ export const { getToken, setToken, removeToken, - setupRefreshTimer, clearRefreshTimer, - clearAuthToken, getAccessToken, getRefreshToken, getUserId, } = tokenManager; ``` -### 3-4. 로그인 성공 처리 표준화 (`entities/auth/api/login-service.ts`) +### 4-4. 인증 서비스 -로그인 성공 시 토큰 저장과 자동 갱신 타이머 설정을 한 흐름으로 고정한다. +파일: `entities/auth/api/auth-service.ts` +참고: `apps/as/src/entities/auth/api/auth-service.ts` -예시 코드: +#### login ```ts -import { getRemainingMs } from "@common/auth"; -import { setToken, setupRefreshTimer } from "../lib/token-manager"; - -async function onLoginSuccess(params: { - accessToken: string; - tokenId: string; - refresh: () => Promise; -}) { - setToken(authConfig.claimName, params.accessToken); - setToken(authConfig.authUserId, params.tokenId); - - const remainingMs = getRemainingMs(params.accessToken); - if (remainingMs <= 0) return; - - setupRefreshTimer(params.refresh, remainingMs); -} +setToken(CLAIM_NAME, accessToken); +setToken(AUTH_USER_ID, tokenId); ``` -### 3-5. 토큰 갱신 실패 처리 표준화 - -토큰 갱신 실패 시 인증 토큰을 일괄 정리한다. - -예시 코드: +#### silentRefresh ```ts -import { clearAuthToken } from "../lib/token-manager"; - async function silentRefresh() { try { - ... + // SILENT_REFRESH API → setToken(...) } catch (error) { - clearAuthToken(); - throw error; + // clearAuthToken은 내부에서 this를 쓰므로 tokenManager 객체로 호출 + tokenManager.clearAuthToken(); + if (typeof window !== "undefined") { + window.dispatchEvent(new CustomEvent("auth:logout")); + } } } ``` -### 3-6. 공통 레이아웃/쉘에 토큰 매니저 주입 +#### declineSessionExtend -공통 컴포넌트가 인증 기능을 직접 사용할 수 있도록 주입한다. +```ts +declineSessionExtend: clearRefreshTimer, +``` -예시 코드: +| 사용자 선택 | 호출 | +| --- | --- | +| confirm **확인** | `silentRefresh()` | +| confirm **취소** | `declineSessionExtend()` | -```tsx -import { tokenManager } from "@/entities/auth"; +#### logout -export function AppLayout({ children }: { children: React.ReactNode }) { - return ( - - {children} - - ); +```ts +async function logout() { + tokenManager.clearAuthToken(); + clearDeviceId(); + + if (typeof window !== "undefined") { + window.dispatchEvent(new CustomEvent("auth:logout")); + } } ``` -### 3-7. 배럴 파일 re-export 정리 (`entities/auth/index.ts`) +httpOnly refresh 쿠키 삭제가 필요하면 `fetch("/api/auth/logout")`를 먼저 호출한다 (`apps/st` 참고). -외부에서 인증 엔티티를 일관된 경로로 사용하도록 re-export한다. - -예시 코드: +#### export ```ts -export { LoginApi } from "./api/login-service"; +export const authService: AuthService = { + login, + silentRefresh, + declineSessionExtend: clearRefreshTimer, + logout, +}; +``` + +### 4-5. AuthWrapper + +파일: `entities/auth/lib/auth-wrapper.tsx` +인증 체크와 `auth:logout` 수신을 구현한다. + +```tsx +useEffect(() => { + // authCheck, 로그인 리다이렉트 +}, []); + +useEffect(() => { + function handleAuthLogout() { + setUserInfo(null); + if (!isAtLoginRedirectUrl(AUTH_LOGIN_REDIRECT_URL)) { + router.replace(AUTH_LOGIN_REDIRECT_URL); + } + } + + window.addEventListener("auth:logout", handleAuthLogout); + return () => window.removeEventListener("auth:logout", handleAuthLogout); +}, []); +``` + +### 4-6. 배럴 + +파일: `entities/auth/index.ts` + +```ts +export { authService } from "./api/auth-service"; export { AuthWrapper } from "./lib/auth-wrapper"; export { tokenManager } from "./lib/token-manager"; ``` -## 4) 서비스별 적용 체크리스트 +### 4-7. 레이아웃·Provider 연결 -각 서비스 담당자는 아래 항목을 모두 충족해야 한다. +```tsx +import { authService } from "@/entities/auth"; -- `authConfig`에 서비스별 키명이 정확히 매핑되어 있다. -- 서비스 내부 커스텀 TokenManager 클래스가 제거되었다. -- 로그인/갱신 로직이 `getRemainingMs` 기반 타이머를 사용한다. -- BaseLayout 컴포넌트에 `tokenManager`가 주입되어 있다. -- 외부 참조 경로가 `entities/auth` 배럴 경로로 통일되어 있다. + + {children} + +``` -## 5) 세션 연장 Confirm 적용 (후속 단계) +`AppProvider`에 `AuthWrapper`를 마운트한다. -리팩토링이 완료된 서비스(`authService` + `AuthWrapper` + `ConfirmProvider`)는 [세션 연장 Confirm 적용 가이드](./session-extend-confirm-guide.md)를 따라 JWT 만료 5분 전 confirm UX를 추가한다. +--- -추가 체크 항목: +## 5) 적용 체크리스트 -- `token-manager`에서 `clearRefreshTimer`를 re-export한다. -- `AuthWrapper`에 `useSessionExtendConfirm`을 연결한다. -- `clearRefreshTimer`는 앱 `token-manager` 싱글톤을 통해 전달한다 (`authService`의 `setupRefreshTimer`와 동일 인스턴스). -- `ConfirmProvider` 하위에 `AuthWrapper`와 `ConfirmDialog`가 마운트되어 있다. +- [ ] `env.ts`에 인증 관련 환경 변수가 정의되어 있다 +- [ ] `authConfig`가 `AuthConfig`로 매핑되어 있다 +- [ ] `createTokenManager(authConfig)`로 tokenManager를 생성한다 +- [ ] `authService`가 `AuthService` 네 메서드를 구현한다 +- [ ] `entities/auth` 배럴로 외부 참조가 통일되어 있다 +- [ ] `BaseLayout`에 `authService`가 주입된다 +- [ ] `AppProvider`에 `AuthWrapper`가 마운트되어 있다 + +--- + +## 6) 후속 단계 + +위 체크리스트 통과 후 [세션 연장 Confirm 적용 가이드](./session-extend-confirm-guide.md)를 진행한다. + +--- + +## 7) 참고 구현 + +| 앱 | auth-service | +| --- | --- | +| as (기준) | `apps/as/src/entities/auth/api/auth-service.ts` | +| cs | `apps/cs/src/entities/auth/api/auth-service.ts` | +| st | `apps/st/src/entities/auth/api/auth-service.ts` | +| cm | `apps/cm/src/entities/auth/api/auth-service.ts` | diff --git a/docs/session-extend-confirm-guide.md b/docs/session-extend-confirm-guide.md index 23a1ce099..2315d1666 100644 --- a/docs/session-extend-confirm-guide.md +++ b/docs/session-extend-confirm-guide.md @@ -1,107 +1,97 @@ # 세션 연장 Confirm 적용 가이드 -## 1) 문서 목적 +## 1) 목적과 원칙 -JWT 만료 **5분 전**에 `"로그인을 연장하시겠습니까?"` confirm 창을 띄우고, 사용자 선택에 따라 세션을 연장하거나 자동 갱신을 중단하는 기능을 서비스 앱에 적용하기 위한 가이드입니다. +JWT 만료 **5분 전**에 `"로그인을 연장하시겠습니까?"` confirm을 띄우고, 선택에 따라 세션을 연장하는 UX를 서비스 앱에 적용하기 위한 가이드입니다. -이 문서는 [서비스별 인증 처리 리팩토링 가이드](./auth-service-refactoring-guide.md)의 **후속 단계**로 읽습니다. -`@common/auth` 기반 `authService` + `token-manager` + `AuthWrapper` 패턴이 먼저 갖춰져 있어야 합니다. +이 문서는 [서비스별 인증 처리 리팩토링 가이드](./auth-service-refactoring-guide.md)의 **후속 단계**입니다. + +핵심 원칙: + +- confirm UX는 `@common/components` **Header**의 `useSessionExtendConfirm`이 담당한다. +- confirm **확인** → `authService.silentRefresh()` +- confirm **취소** → `authService.declineSessionExtend()` +- 앱은 `ConfirmProvider` 계층과 `authService` 주입을 맞춘다. + +**적용 완료**: `cs`, `as`, `st` + +--- ## 2) 선행 조건 -아래 항목이 완료된 서비스만 이 가이드를 적용합니다. +[리팩토링 가이드 체크리스트](./auth-service-refactoring-guide.md#5-적용-체크리스트)를 통과한 서비스만 진행한다. -| 항목 | 확인 | -| -------------------------------------------------------------------------------- | ------------------------------------------------------------------- | -| `createTokenManager(authConfig)` 사용 | [3-3. 토큰 매니저 공통화](./auth-service-refactoring-guide.md) | -| `authService.login` / `silentRefresh`에서 `getRemainingMs` + `setupRefreshTimer` | [3-4. 로그인 성공 처리 표준화](./auth-service-refactoring-guide.md) | -| `authService.declineSessionExtend` 구현 | confirm 취소 시 refresh 타이머 해제 | -| `AuthWrapper`에서 새로고침 시 refresh 타이머 복구 | cs / as / st 참고 | -| `ConfirmProvider` + `ConfirmDialog` 마운트 | AppProvider | -| `ConfirmProvider` **하위**에 `BaseLayout` → `Header` 렌더링 | `useConfirm` 사용 조건 | -| `BaseLayout`에 `authService` prop 전달 | Header까지 전달됨 | +| 항목 | 리팩토링 가이드 | +| --- | --- | +| `authService` (`silentRefresh`, `declineSessionExtend`) | [4-4](./auth-service-refactoring-guide.md#44-인증-서비스) | +| `BaseLayout`에 `authService` 주입 | [4-7](./auth-service-refactoring-guide.md#47-레이아웃provider-연결) | -**적용 완료 앱**: `cs`, `as`, `st` -**미적용**: `wr`, `sn`, `fo` 등 legacy `LoginApi` 패턴 앱 +--- -## 3) 전체 흐름 +## 3) 목표 아키텍처 -로그인 이후 JWT `exp`를 기준으로 **두 개의 독립 타이머**가 동작합니다. +### 3-1. 레이어 책임 + +| 레이어 | 위치 | 역할 | +| --- | --- | --- | +| JWT 시간 계산 | `@common/auth` `jwt-utils.ts` | `getRemainingMs`, confirm 버퍼 상수 | +| 토큰·정리 | `@common/auth` `token-manager.ts` | `setToken`, `clearRefreshTimer`, `clearAuthToken` | +| API 오케스트레이션 | `entities/auth/api/auth-service.ts` | `silentRefresh`, `declineSessionExtend` | +| confirm UX | `@common/components` Header | 카운트다운 + 5분 전 confirm | +| 앱 진입점 | `entities/auth/lib/auth-wrapper.tsx` | auth 체크, `auth:logout` | +| 레이아웃 | layout / BaseLayout wrapper | `authService` → Header | +| UI 쉘 | `app/providers/app-provider.tsx` | `ConfirmProvider`, `ConfirmDialog` | + +### 3-2. 런타임 흐름 ```mermaid sequenceDiagram participant Login as authService.login - participant RefreshTimer as refreshTimer participant Header as Header participant User as 사용자 participant API as authService.silentRefresh - Login->>RefreshTimer: setupRefreshTimer(exp - 1분) + Login->>Login: setToken Header->>Header: confirm 예약(exp - 5분) Note over Header: 만료 5분 전 Header->>User: 로그인을 연장하시겠습니까? alt 확인 User->>API: silentRefresh() - API->>RefreshTimer: 새 토큰 기준 타이머 재설정 + API->>API: setToken Header->>Header: confirm 타이머 재설정 else 취소 - Header->>API: declineSessionExtend() - API->>RefreshTimer: clearRefreshTimer() - Note over RefreshTimer: 취소시 만료까지 자동 갱신 없음 + Header->>Header: declineSessionExtend() end ``` -| 시점 | 동작 | 담당 | -| --------------------------- | ------------------------------------- | ------------------------------------------- | -| 로그인 / silentRefresh 성공 | 만료 1분 전 자동 `silentRefresh` 예약 | `authService` → `setupRefreshTimer` | -| 만료 5분 전 | confirm 표시 | `Header` → `useSessionExtendConfirm` | -| confirm **확인** | 즉시 `silentRefresh` + 타이머 재설정 | Header → `authService.silentRefresh` | -| confirm **취소** | 자동 `silentRefresh` 타이머 해제 | Header → `authService.declineSessionExtend` | +| 시점 | 동작 | 담당 | +| --- | --- | --- | +| 로그인 / silentRefresh 성공 | 토큰 저장 | `authService` | +| 만료 5분 전 | confirm 표시 | Header → `useSessionExtendConfirm` | +| confirm **확인** | `silentRefresh` + confirm 타이머 재설정 | Header → `authService.silentRefresh` | +| confirm **취소** | `declineSessionExtend` | Header → `authService.declineSessionExtend` | +| 토큰 완전 만료 | logout | Header | -### 3-1. 시간 계산 (`getRemainingMs`) +--- -`[packages/common/auth/jwt-utils.ts](../packages/common/auth/jwt-utils.ts)` +## 4) 공통 계약 -```typescript -export const REFRESH_BUFFER_MS = 60_000; // silent refresh: 만료 1분 전 +### 4-1. 시간 버퍼 (`getRemainingMs`) + +파일: `packages/common/auth/jwt-utils.ts` + +```ts export const SESSION_EXTEND_CONFIRM_BUFFER_MS = 5 * 60_000; // confirm: 만료 5분 전 -getRemainingMs(token); // → refresh 타이머 delay getRemainingMs(token, SESSION_EXTEND_CONFIRM_BUFFER_MS); // → confirm 타이머 delay getRemainingMs(token, 0) > 0; // → 토큰 미만료 여부 ``` -`bufferMs` 인자 하나로 용도를 구분합니다. 기존 `getRemainingMs(token)` 호출부는 변경하지 않아도 됩니다. +### 4-2. Header ↔ AuthService -## 4) 공통 모듈 변경 사항 +공통 Header가 아래처럼 연결한다. -### 4-1. `@common/auth` — `AuthService` / `clearRefreshTimer` - -`[packages/common/auth/types.ts](../packages/common/auth/types.ts)` - -```typescript -export interface AuthService { - login(payload: AuthPayload): Promise; - silentRefresh(): Promise; - declineSessionExtend(): void; // confirm 취소 시 refresh 타이머 해제 - logout(): Promise; -} -``` - -`[packages/common/auth/token-manager.ts](../packages/common/auth/token-manager.ts)` - -- `setupRefreshTimer`: 만료 1분 전 `silentRefresh` 예약 (기존) -- `clearRefreshTimer`: 예약된 refresh 타이머 해제 (신규) -- `clearAuthToken`: 내부에서 `clearRefreshTimer` 호출 - -`refreshTimer`는 `createTokenManager()` 클로저 안의 **앱당 싱글톤** 변수입니다. -`authService.declineSessionExtend()`가 내부에서 `clearRefreshTimer()`를 호출하므로, Header는 token-manager를 직접 import하지 않습니다. - -### 4-2. `@common/components` — Header + `useSessionExtendConfirm` - -`[packages/common/components/src/ui/header/index.tsx](../packages/common/components/src/ui/header/index.tsx)`에서 훅을 실행합니다. - -```typescript +```ts useSessionExtendConfirm({ enabled: mounted && @@ -110,68 +100,27 @@ useSessionExtendConfirm({ !!authService && !isAtLoginRedirectUrl(getAuthLoginRedirectUrl()), getAccessToken: () => getCookie(appConfig.CLAIM_NAME ?? ""), - silentRefresh: authService.silentRefresh, - clearRefreshTimer: authService.declineSessionExtend, + silentRefresh: authService?.silentRefresh ?? (async () => {}), + declineSessionExtend: authService?.declineSessionExtend ?? (() => {}), }); ``` -| enabled 조건 | 이유 | -| ---------------------------- | ------------------------------- | -| `mounted` | hydration 후 타이머 시작 | -| `isLoggedIn` | 비로그인 제외 | -| `!viewOnly` | `VBaseLayout` 등 조회 전용 제외 | -| `!!authService` | legacy 앱 자동 비활성 | -| `!isAtLoginRedirectUrl(...)` | 로그인 페이지 제외 | +### 4-3. Provider 계층 -세션 만료 **카운트다운 UI**와 confirm 로직이 Header에 응집됩니다. - -## 5) 서비스별 적용 단계 - -### 5-1. `auth-service`에 `declineSessionExtend` 추가 - -`[apps/cs/src/entities/auth/api/auth-service.ts](../apps/cs/src/entities/auth/api/auth-service.ts)` 참고: - -```typescript -import { clearRefreshTimer } from "../lib/token-manager"; - -function declineSessionExtend(): void { - clearRefreshTimer(); -} - -export const authService: AuthService = { - login, - silentRefresh, - declineSessionExtend, - logout, -}; +``` +ConfirmProvider + └─ AuthWrapper + └─ … → BaseLayout → Header + └─ ConfirmDialog ``` -### 5-2. `BaseLayout`에 `authService` 전달 +--- -`[apps/cs/app/(main)/(with-base-layout)/layout.tsx](../apps/cs/app/(main)`/(with-base-layout)/layout.tsx) 참고: +## 5) 적용 순서 -```tsx - -``` +### 5-1. AppProvider — ConfirmProvider 마운트 -`BaseLayout`은 `authService`를 `Header`에 그대로 전달합니다. **AuthWrapper에서 훅을 연결하지 않습니다.** - -### 5-3. `AuthWrapper` — refresh 타이머 복구만 유지 - -`[apps/cs/src/entities/auth/lib/auth-wrapper.tsx](../apps/cs/src/entities/auth/lib/auth-wrapper.tsx)`: - -- `useSessionExtendConfirm` **사용하지 않음** -- 페이지 새로고침 시 `setupRefreshTimer(authService.silentRefresh, remainingMs)` 복구 유지 -- 인증 체크, `auth:logout` 수신 유지 - -### 5-4. `AppProvider` 구조 확인 - -`[apps/cs/src/app/providers/app-provider.tsx](../apps/cs/src/app/providers/app-provider.tsx)` 참고: +파일: `app/providers/app-provider.tsx` ```tsx @@ -182,64 +131,84 @@ export const authService: AuthService = { ``` -`Header`는 `ConfirmProvider` 하위에서 렌더링되므로 `useConfirm()`이 동작합니다. +참고: `apps/cs/src/app/providers/app-provider.tsx` -## 6) 책임 분리 요약 +### 5-2. BaseLayout — authService 전달 -| 레이어 | 파일 | 역할 | -| ------------------ | ------------------------------------ | -------------------------------------------------- | -| JWT 시간 계산 | `@common/auth` `jwt-utils.ts` | `getRemainingMs`, 버퍼 상수 | -| refresh 타이머 | `@common/auth` `token-manager.ts` | `setupRefreshTimer`, `clearRefreshTimer` | -| API 오케스트레이션 | `entities/auth/api/auth-service.ts` | login, silentRefresh, declineSessionExtend, logout | -| confirm UX | `@common/components` `Header` | 카운트다운 UI + 5분 전 confirm | -| 앱 진입점 | `entities/auth/lib/auth-wrapper.tsx` | 타이머 복구, auth 체크 | -| 레이아웃 | `app/.../layout.tsx` | `authService` → `BaseLayout` → `Header` | -| UI 쉘 | `app/providers/app-provider.tsx` | ConfirmProvider, ConfirmDialog | +```tsx +import { authService } from "@/entities/auth"; -## 7) 엣지 케이스 + + {children} + +``` -| 상황 | 동작 | -| -------------------- | --------------------------------------------------------------- | -| 토큰 유효 시간 < 5분 | `confirmMs === 0`이면 페이지 진입 즉시 confirm | -| confirm **취소** | `declineSessionExtend()` + 동일 JWT에 재표시 안 함 | -| confirm **확인** | `silentRefresh()` → 새 토큰 기준 confirm/refresh 타이머 재설정 | -| `silentRefresh` 실패 | `authService`가 `auth:logout` 발행 → AuthWrapper 리다이렉트 | -| 로그아웃 | `clearAuthToken`이 refresh 타이머 정리, 훅 cleanup | -| `viewOnly` Header | confirm 비활성 | -| `authService` 미전달 | confirm 비활성 (wr/fo 등) | -| 페이지 새로고침 | AuthWrapper가 refresh 타이머 복구, Header가 confirm 타이머 복구 | +### 5-3. authService — declineSessionExtend -## 8) 적용 체크리스트 +리팩토링 [4-4](./auth-service-refactoring-guide.md#44-인증-서비스)에서 이미 구현했다면 확인만 한다. -[리팩토링 가이드 체크리스트](./auth-service-refactoring-guide.md#4-서비스별-적용-체크리스트)에 더해 아래를 확인합니다. +```ts +export const authService: AuthService = { + login, + silentRefresh, + declineSessionExtend: clearRefreshTimer, + logout, +}; +``` -- [ ] `auth-service`에 `declineSessionExtend` 구현 -- [ ] `token-manager`에서 `clearRefreshTimer` re-export -- [ ] `BaseLayout`에 `authService` prop 전달 (→ Header까지) -- [ ] `ConfirmProvider` > `AuthWrapper` > `BaseLayout` > `Header` 계층 확인 -- [ ] `AuthWrapper`에 `useSessionExtendConfirm` **없음** 확인 -- [ ] 만료 5분 전 confirm 노출 확인 -- [ ] 확인 클릭 시 `SILENT_REFRESH` API 호출 확인 -- [ ] 취소 클릭 시 만료 1분 전 자동 refresh 미발생 확인 +--- -## 9) 검증 방법 +## 6) 엣지 케이스 + +| 상황 | 동작 | +| --- | --- | +| 토큰 유효 시간 < 5분 | 진입 즉시 confirm | +| confirm **취소** | `declineSessionExtend()` + 동일 JWT에 재표시 안 함 | +| confirm **확인** | `silentRefresh()` → 새 토큰 기준 confirm 타이머 재설정 | +| `silentRefresh` 실패 | `auth:logout` → AuthWrapper 리다이렉트 | +| 로그아웃 | `clearAuthToken`, 훅 cleanup | +| 페이지 새로고침 | Header가 confirm 타이머 재예약 | + +--- + +## 7) 적용 체크리스트 + +- [ ] `ConfirmProvider` > `AuthWrapper` > `BaseLayout` > `Header` 계층 +- [ ] `ConfirmDialog`가 `ConfirmProvider` 하위에 마운트됨 +- [ ] `BaseLayout`에 `authService` 전달 +- [ ] `authService.declineSessionExtend` 구현 +- [ ] 만료 5분 전 confirm 노출 +- [ ] 확인 클릭 시 `SILENT_REFRESH` 호출 +- [ ] 취소 후 세션이 연장되지 않음 + +--- + +## 8) 검증 방법 1. 로그인 후 JWT `exp`가 5분 이내가 되도록 대기하거나 테스트 토큰 사용 2. confirm 문구 `"로그인을 연장하시겠습니까?"` 표시 확인 -3. **확인** → Network 탭에서 `SILENT_REFRESH` PUT 호출, 쿠키 갱신, confirm 재예약 -4. **취소** → 만료 1분 전까지 `SILENT_REFRESH` 자동 호출 없음 -5. 취소 후 세션 만료 → 401 / 로그인 리다이렉트 -6. 새로고침 후 refresh·confirm 타이머 복구 +3. **확인** → Network에서 `SILENT_REFRESH` PUT, 쿠키 갱신, confirm 재예약 +4. **취소** → 만료 시 로그아웃/리다이렉트 +5. 새로고침 후 confirm 타이머 복구 -## 10) 참고 구현 +--- -| 앱 | Header (공통) | auth-service | BaseLayout layout | -| --- | ------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | -| cs | `[packages/common/components/src/ui/header/index.tsx](../packages/common/components/src/ui/header/index.tsx)` | `[apps/cs/src/entities/auth/api/auth-service.ts](../apps/cs/src/entities/auth/api/auth-service.ts)` | `[apps/cs/app/(main)/(with-base-layout)/layout.tsx](../apps/cs/app/(main)`/(with-base-layout)/layout.tsx) | -| as | (동일) | `[apps/as/src/entities/auth/api/auth-service.ts](../apps/as/src/entities/auth/api/auth-service.ts)` | `[apps/as/src/app/layouts/base-layout.tsx](../apps/as/src/app/layouts/base-layout.tsx)` | -| st | (동일) | `[apps/st/src/entities/auth/api/auth-service.ts](../apps/st/src/entities/auth/api/auth-service.ts)` | `[apps/st/app/(main)/(with-base-layout)/layout.tsx](../apps/st/app/(main)`/(with-base-layout)/layout.tsx) | +## 9) 참고 구현 -## 11) 관련 문서 +| 앱 | auth-service | BaseLayout / layout | AppProvider | +| --- | --- | --- | --- | +| as (기준) | `apps/as/src/entities/auth/api/auth-service.ts` | `apps/as/src/app/layouts/base-layout.tsx` | (동등 Provider) | +| cs | `apps/cs/src/entities/auth/api/auth-service.ts` | `apps/cs/app/(main)/(with-base-layout)/layout.tsx` | `apps/cs/src/app/providers/app-provider.tsx` | +| st | `apps/st/src/entities/auth/api/auth-service.ts` | `apps/st/app/(main)/(with-base-layout)/layout.tsx` | (동등 Provider) | -- [서비스별 인증 처리 리팩토링 가이드](./auth-service-refactoring-guide.md) — 선행 리팩토링 -- `[packages/common/auth/README.md](../packages/common/auth/README.md)` — `getRemainingMs`, token-manager API +공통: + +- `packages/common/components/src/ui/header/index.tsx` +- `packages/common/components/src/lib/use-session-extend-confirm.ts` + +--- + +## 10) 관련 문서 + +- [서비스별 인증 처리 리팩토링 가이드](./auth-service-refactoring-guide.md) +- `packages/common/auth/README.md`