/** * ๐Ÿ”„ 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 | null; timestamp: number; result: RefreshResult | null; } = { promise: null, timestamp: 0, result: null, }; const REFRESH_CACHE_TTL = 5000; // 5์ดˆ /** * ์‹ค์ œ ํ† ํฐ ๊ฐฑ์‹  ์ˆ˜ํ–‰ (๋‚ด๋ถ€ ํ•จ์ˆ˜) */ async function doRefreshToken(refreshToken: string): Promise { 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 { 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, }; }