# 세션 연장 Confirm 적용 가이드 ## 1) 문서 목적 JWT 만료 **5분 전**에 `"로그인을 연장하시겠습니까?"` confirm 창을 띄우고, 사용자 선택에 따라 세션을 연장하거나 자동 갱신을 중단하는 기능을 서비스 앱에 적용하기 위한 가이드입니다. 이 문서는 [서비스별 인증 처리 리팩토링 가이드](./auth-service-refactoring-guide.md)의 **후속 단계**로 읽습니다. `@common/auth` 기반 `authService` + `token-manager` + `AuthWrapper` 패턴이 먼저 갖춰져 있어야 합니다. ## 2) 선행 조건 아래 항목이 완료된 서비스만 이 가이드를 적용합니다. | 항목 | 확인 | | -------------------------------------------------------------------------------- | ------------------------------------------------------------------- | | `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까지 전달됨 | **적용 완료 앱**: `cs`, `as`, `st` **미적용**: `wr`, `sn`, `fo` 등 legacy `LoginApi` 패턴 앱 ## 3) 전체 흐름 로그인 이후 JWT `exp`를 기준으로 **두 개의 독립 타이머**가 동작합니다. ```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분) Header->>Header: confirm 예약(exp - 5분) Note over Header: 만료 5분 전 Header->>User: 로그인을 연장하시겠습니까? alt 확인 User->>API: silentRefresh() API->>RefreshTimer: 새 토큰 기준 타이머 재설정 Header->>Header: confirm 타이머 재설정 else 취소 Header->>API: declineSessionExtend() API->>RefreshTimer: clearRefreshTimer() Note over RefreshTimer: 취소시 만료까지 자동 갱신 없음 end ``` | 시점 | 동작 | 담당 | | --------------------------- | ------------------------------------- | ------------------------------------------- | | 로그인 / silentRefresh 성공 | 만료 1분 전 자동 `silentRefresh` 예약 | `authService` → `setupRefreshTimer` | | 만료 5분 전 | confirm 표시 | `Header` → `useSessionExtendConfirm` | | confirm **확인** | 즉시 `silentRefresh` + 타이머 재설정 | Header → `authService.silentRefresh` | | confirm **취소** | 자동 `silentRefresh` 타이머 해제 | Header → `authService.declineSessionExtend` | ### 3-1. 시간 계산 (`getRemainingMs`) `[packages/common/auth/jwt-utils.ts](../packages/common/auth/jwt-utils.ts)` ```typescript export const REFRESH_BUFFER_MS = 60_000; // silent refresh: 만료 1분 전 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) 공통 모듈 변경 사항 ### 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 useSessionExtendConfirm({ enabled: mounted && isLoggedIn && !viewOnly && !!authService && !isAtLoginRedirectUrl(getAuthLoginRedirectUrl()), getAccessToken: () => getCookie(appConfig.CLAIM_NAME ?? ""), silentRefresh: authService.silentRefresh, clearRefreshTimer: authService.declineSessionExtend, }); ``` | enabled 조건 | 이유 | | ---------------------------- | ------------------------------- | | `mounted` | hydration 후 타이머 시작 | | `isLoggedIn` | 비로그인 제외 | | `!viewOnly` | `VBaseLayout` 등 조회 전용 제외 | | `!!authService` | legacy 앱 자동 비활성 | | `!isAtLoginRedirectUrl(...)` | 로그인 페이지 제외 | 세션 만료 **카운트다운 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, }; ``` ### 5-2. `BaseLayout`에 `authService` 전달 `[apps/cs/app/(main)/(with-base-layout)/layout.tsx](../apps/cs/app/(main)`/(with-base-layout)/layout.tsx) 참고: ```tsx ``` `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)` 참고: ```tsx {children} ``` `Header`는 `ConfirmProvider` 하위에서 렌더링되므로 `useConfirm()`이 동작합니다. ## 6) 책임 분리 요약 | 레이어 | 파일 | 역할 | | ------------------ | ------------------------------------ | -------------------------------------------------- | | 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 | ## 7) 엣지 케이스 | 상황 | 동작 | | -------------------- | --------------------------------------------------------------- | | 토큰 유효 시간 < 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 타이머 복구 | ## 8) 적용 체크리스트 [리팩토링 가이드 체크리스트](./auth-service-refactoring-guide.md#4-서비스별-적용-체크리스트)에 더해 아래를 확인합니다. - [ ] `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) 검증 방법 1. 로그인 후 JWT `exp`가 5분 이내가 되도록 대기하거나 테스트 토큰 사용 2. confirm 문구 `"로그인을 연장하시겠습니까?"` 표시 확인 3. **확인** → Network 탭에서 `SILENT_REFRESH` PUT 호출, 쿠키 갱신, confirm 재예약 4. **취소** → 만료 1분 전까지 `SILENT_REFRESH` 자동 호출 없음 5. 취소 후 세션 만료 → 401 / 로그인 리다이렉트 6. 새로고침 후 refresh·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) | ## 11) 관련 문서 - [서비스별 인증 처리 리팩토링 가이드](./auth-service-refactoring-guide.md) — 선행 리팩토링 - `[packages/common/auth/README.md](../packages/common/auth/README.md)` — `getRemainingMs`, token-manager API