feat(WEB): API 인프라 리팩토링, CEO 대시보드 현황판 개선 및 문서 시스템 강화
- API: fetch-wrapper/proxy/refresh-token 리팩토링, authenticated-fetch 신규 추가 - CEO 대시보드: EnhancedSections 현황판 기능 개선, dashboard transformers/types 확장 - 문서 시스템: ApprovalLine/DocumentHeader/DocumentToolbar/DocumentViewer 개선 - 작업지시서: 검사보고서/작업일지 문서 컴포넌트 개선 (벤딩/스크린/슬랫) - 레이아웃: Sidebar/AuthenticatedLayout 수정 - 작업자화면: WorkerScreen 수정 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
95
src/lib/api/authenticated-fetch.ts
Normal file
95
src/lib/api/authenticated-fetch.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* Authenticated Fetch Gateway
|
||||
*
|
||||
* 인증이 필요한 백엔드 API 호출의 유일한 게이트웨이.
|
||||
* 401 감지 → refresh (globalThis 캐시) → retry 를 한 곳에서 처리.
|
||||
*
|
||||
* 이 모듈이 담당하는 것:
|
||||
* - 요청 실행
|
||||
* - 401 감지 → refreshAccessToken (globalThis 캐시로 프로세스 내 중복 방지)
|
||||
* - 새 토큰으로 재시도
|
||||
*
|
||||
* 이 모듈이 담당하지 않는 것 (호출자가 처리):
|
||||
* - 쿠키 읽기 (PROXY: request.cookies / Server Actions: cookies() API)
|
||||
* - 쿠키 설정 (PROXY: Set-Cookie 헤더 / Server Actions: cookies() API)
|
||||
* - 리다이렉트 (Server Actions: redirect('/login'))
|
||||
* - 헤더 구성 (각 호출자가 자기 방식으로)
|
||||
*/
|
||||
|
||||
import { refreshAccessToken, type RefreshResult } from './refresh-token';
|
||||
|
||||
export type AuthenticatedFetchResult = {
|
||||
/** 백엔드 응답 (성공이든 실패든) */
|
||||
response: Response;
|
||||
/** refresh 성공 시 새 토큰 (호출자가 쿠키에 저장) */
|
||||
newTokens?: RefreshResult;
|
||||
/** true면 인증 실패 (호출자가 쿠키 삭제 + 리다이렉트 처리) */
|
||||
authFailed?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* 인증된 백엔드 요청 실행
|
||||
*
|
||||
* 반환값 상태:
|
||||
* - { response } → 정상 (401 아님)
|
||||
* - { response, newTokens } → 401 → refresh 성공 → 재시도 성공
|
||||
* - { response, authFailed: true } → 인증 실패 (refresh 불가/실패/재시도 실패)
|
||||
*
|
||||
* @param url 백엔드 API URL
|
||||
* @param options fetch 옵션 (호출자가 Authorization 등 헤더 포함)
|
||||
* @param refreshToken refresh_token (없으면 401 시 바로 실패 반환)
|
||||
* @param caller 호출자 이름 (로그용: 'PROXY' | 'serverFetch' | 'ServerApiClient')
|
||||
*/
|
||||
export async function authenticatedFetch(
|
||||
url: string,
|
||||
options: RequestInit,
|
||||
refreshToken: string | undefined,
|
||||
caller: string = 'unknown'
|
||||
): Promise<AuthenticatedFetchResult> {
|
||||
// 1. 요청 실행 (호출자가 이미 모든 헤더 설정)
|
||||
const response = await fetch(url, options);
|
||||
|
||||
// 2. 401이 아니면 그대로 반환
|
||||
if (response.status !== 401) {
|
||||
return { response };
|
||||
}
|
||||
|
||||
// 3. 401이지만 refresh_token 없음 → 인증 실패
|
||||
if (!refreshToken) {
|
||||
console.warn(`🔴 [${caller}] 401 (no refresh token)`);
|
||||
return { response, authFailed: true };
|
||||
}
|
||||
|
||||
// 4. 401 + refresh_token 있음 → 갱신 시도 (globalThis 캐시로 중복 방지)
|
||||
console.log(`🔄 [${caller}] Got 401, attempting token refresh...`);
|
||||
const refreshResult = await refreshAccessToken(refreshToken, caller);
|
||||
|
||||
if (!refreshResult.success || !refreshResult.accessToken) {
|
||||
console.warn(`🔴 [${caller}] Token refresh failed`);
|
||||
return { response, authFailed: true };
|
||||
}
|
||||
|
||||
// 5. 새 토큰으로 재시도
|
||||
console.log(`✅ [${caller}] Token refreshed, retrying...`);
|
||||
const retryHeaders = new Headers(options.headers || {});
|
||||
retryHeaders.set('Authorization', `Bearer ${refreshResult.accessToken}`);
|
||||
|
||||
const retryResponse = await fetch(url, {
|
||||
...options,
|
||||
headers: retryHeaders,
|
||||
});
|
||||
|
||||
console.log(`🔵 [${caller}] Retry status: ${retryResponse.status}`);
|
||||
|
||||
// 6. 재시도도 401 → 인증 실패
|
||||
if (retryResponse.status === 401) {
|
||||
console.warn(`🔴 [${caller}] Retry still 401, auth failed`);
|
||||
return { response: retryResponse, authFailed: true };
|
||||
}
|
||||
|
||||
// 7. 재시도 성공 → 새 토큰과 함께 반환 (호출자가 쿠키 설정)
|
||||
return {
|
||||
response: retryResponse,
|
||||
newTokens: refreshResult,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user