Files
frontend/docs/session-extend-confirm-guide.md

13 KiB

세션 연장 Confirm 적용 가이드

1) 문서 목적

JWT 만료 5분 전에 "로그인을 연장하시겠습니까?" confirm 창을 띄우고, 사용자 선택에 따라 세션을 연장하거나 자동 갱신을 중단하는 기능을 서비스 앱에 적용하기 위한 가이드입니다.

이 문서는 서비스별 인증 처리 리팩토링 가이드의 후속 단계로 읽습니다.
@common/auth 기반 authService + token-manager + AuthWrapper 패턴이 먼저 갖춰져 있어야 합니다.

2) 선행 조건

아래 항목이 완료된 서비스만 이 가이드를 적용합니다.

항목 확인
createTokenManager(authConfig) 사용 3-3. 토큰 매니저 공통화
authService.login / silentRefresh에서 getRemainingMs + setupRefreshTimer 3-4. 로그인 성공 처리 표준화
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를 기준으로 두 개의 독립 타이머가 동작합니다.

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)

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)

export interface AuthService {
  login(payload: AuthPayload): Promise<void>;
  silentRefresh(): Promise<void>;
  declineSessionExtend(): void; // confirm 취소 시 refresh 타이머 해제
  logout(): Promise<void>;
}

[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)에서 훅을 실행합니다.

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) 참고:

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) 참고:

<BaseLayout
  queryClient={queryClient}
  authService={authService}
  currentPath={pathname ?? ""}
  handleMenuClick={handleMenuClick}
>

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) 참고:

<ConfirmProvider>
  <AuthWrapper>
    {children}
    <ConfirmDialog />
  </AuthWrapper>
</ConfirmProvider>

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에 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) 관련 문서