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