const BASE = import.meta.env.VITE_API_BASE_URL || ''; // 동시 401 발생 시 refresh를 한 번만 실행하기 위한 Promise 공유 let refreshingPromise = null; async function tryRefresh() { if (refreshingPromise) return refreshingPromise; refreshingPromise = (async () => { const rt = localStorage.getItem('refreshToken'); if (!rt) throw new Error('no_refresh_token'); const res = await fetch(BASE + '/api/auth/refresh', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ refreshToken: rt }), }); if (!res.ok) throw new Error('refresh_failed'); const data = await res.json(); localStorage.setItem('token', data.token); localStorage.setItem('refreshToken', data.refreshToken); return data.token; })().finally(() => { refreshingPromise = null; }); return refreshingPromise; } async function doFetch(path, options, token) { const headers = { 'Content-Type': 'application/json', ...options.headers }; if (token) headers['Authorization'] = `Bearer ${token}`; return fetch(BASE + path, { ...options, headers }); } async function parseError(res) { let message = res.statusText; try { const body = await res.json(); if (body.error) message = body.error; } catch {} const err = new Error(message); err.status = res.status; return err; } // 204 No Content는 null 반환, 나머지는 JSON 파싱 async function parseResponse(res) { if (res.status === 204) return null; return res.json(); } export async function apiFetch(path, options = {}) { const token = localStorage.getItem('token'); const res = await doFetch(path, options, token); if (res.status === 401) { try { const newToken = await tryRefresh(); // 새 토큰으로 원래 요청 재시도 const retryRes = await doFetch(path, options, newToken); if (retryRes.status === 401) { window.dispatchEvent(new Event('auth:unauthorized')); throw await parseError(retryRes); } if (!retryRes.ok) throw await parseError(retryRes); return parseResponse(retryRes); } catch (e) { // refresh 자체 실패 → 로그아웃 if (e.message === 'no_refresh_token' || e.message === 'refresh_failed') { window.dispatchEvent(new Event('auth:unauthorized')); const err = new Error('인증이 필요합니다'); err.status = 401; throw err; } throw e; } } if (!res.ok) throw await parseError(res); return parseResponse(res); }