- 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>
143 lines
4.2 KiB
TypeScript
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,
|
|
};
|
|
} |