Files
sam-react-prod/src/lib/api/refresh-token.ts
byeongcheolryu 29e7b41615 chore(WEB): 다수 컴포넌트 개선 및 CEO 대시보드 추가
- CEO 대시보드 컴포넌트 추가
- AuthenticatedLayout 개선
- 각 모듈 actions.ts 에러 핸들링 개선
- API fetch-wrapper, refresh-token 로직 개선
- ReceivablesStatus 컴포넌트 업데이트
- globals.css 스타일 업데이트
- 기타 다수 컴포넌트 수정

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-08 17:15:42 +09:00

143 lines
4.2 KiB
TypeScript

/**
* 🔄 Refresh Token 공통 모듈
*
* 프록시(/api/proxy)와 serverFetch 양쪽에서 사용하는 공통 토큰 갱신 로직
*
* 문제: useEffect에서 여러 API 동시 호출 시 refresh_token 충돌
* - 첫 번째 요청이 refresh_token 사용 → 성공 (토큰 폐기됨)
* - 두 번째 요청이 같은 refresh_token 사용 → 실패 (이미 폐기됨)
*
* 해결: 5초간 refresh 결과 캐싱
* - 동시 요청들이 같은 새 토큰을 공유
* - 진행 중인 refresh Promise도 공유하여 중복 요청 방지
* - 프록시와 serverFetch가 같은 캐시를 공유하여 더 효율적
*/
export type RefreshResult = {
success: boolean;
accessToken?: string;
refreshToken?: string;
expiresIn?: number;
};
// 캐시 상태 (모듈 레벨에서 공유)
let refreshCache: {
promise: Promise<RefreshResult> | null;
timestamp: number;
result: RefreshResult | null;
} = {
promise: null,
timestamp: 0,
result: null,
};
const REFRESH_CACHE_TTL = 5000; // 5초
/**
* 실제 토큰 갱신 수행 (내부 함수)
*/
async function doRefreshToken(refreshToken: string): Promise<RefreshResult> {
try {
const refreshUrl = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/refresh`;
console.log('🔄 [RefreshToken] Refresh request:', {
url: refreshUrl,
hasApiKey: !!process.env.API_KEY,
refreshTokenLength: refreshToken?.length,
});
const response = await fetch(refreshUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-API-KEY': process.env.API_KEY || '',
},
body: JSON.stringify({
refresh_token: refreshToken,
}),
});
if (!response.ok) {
const errorBody = await response.text();
console.warn('🔴 [RefreshToken] Token refresh failed:', {
status: response.status,
statusText: response.statusText,
body: errorBody,
});
return { success: false };
}
const data = await response.json();
console.log('✅ [RefreshToken] Token refreshed successfully');
return {
success: true,
accessToken: data.access_token,
refreshToken: data.refresh_token,
expiresIn: data.expires_in,
};
} catch (error) {
console.error('🔴 [RefreshToken] Token refresh error:', error);
return { success: false };
}
}
/**
* 토큰 갱신 함수 (5초 캐싱 적용)
*
* 동시 요청 시:
* 1. 캐시된 결과가 있으면 즉시 반환
* 2. 진행 중인 refresh가 있으면 그 Promise를 기다림
* 3. 둘 다 없으면 새 refresh 시작
*
* @param refreshToken - 현재 refresh_token
* @param caller - 호출자 식별 (로그용: 'PROXY' | 'serverFetch')
*/
export async function refreshAccessToken(
refreshToken: string,
caller: string = 'unknown'
): Promise<RefreshResult> {
const now = Date.now();
// 1. 캐시된 성공 결과가 유효하면 즉시 반환
if (refreshCache.result && refreshCache.result.success && now - refreshCache.timestamp < REFRESH_CACHE_TTL) {
console.log(`🔵 [${caller}] Using cached refresh result (age: ${now - refreshCache.timestamp}ms)`);
return refreshCache.result;
}
// 2. 진행 중인 refresh가 있고, 아직 결과가 없으면 기다림 (실패한 결과는 캐시 안 함)
if (refreshCache.promise && !refreshCache.result && now - refreshCache.timestamp < REFRESH_CACHE_TTL) {
console.log(`🔵 [${caller}] Waiting for ongoing refresh...`);
return refreshCache.promise;
}
// 2-1. 이전 refresh가 실패했으면 캐시 초기화
if (refreshCache.result && !refreshCache.result.success) {
console.log(`🔄 [${caller}] Previous refresh failed, clearing cache...`);
refreshCache.promise = null;
refreshCache.result = null;
}
// 3. 새 refresh 시작
console.log(`🔄 [${caller}] Starting new refresh request...`);
refreshCache.timestamp = now;
refreshCache.result = null;
refreshCache.promise = doRefreshToken(refreshToken).then(result => {
refreshCache.result = result;
return result;
});
return refreshCache.promise;
}
/**
* 캐시 초기화 (테스트용)
*/
export function clearRefreshCache(): void {
refreshCache = {
promise: null,
timestamp: 0,
result: null,
};
}