/** * 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 { // 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, }; }