diff --git a/src/api/announcements.js b/src/api/announcements.js index 52fd871..363aeb1 100644 --- a/src/api/announcements.js +++ b/src/api/announcements.js @@ -20,4 +20,4 @@ export async function updateAnnouncement(id, title, content) { export async function deleteAnnouncement(id) { return apiFetch(`/api/announcements/${id}`, { method: 'DELETE' }); -} +} \ No newline at end of file diff --git a/src/api/auth.js b/src/api/auth.js index 558beff..817a3ec 100644 --- a/src/api/auth.js +++ b/src/api/auth.js @@ -32,9 +32,12 @@ export async function ssafyCallback(code, state) { // 토큰을 리프레시하고 새 access token을 반환 (동시 호출 방지 포함) export { tryRefresh as refreshToken } from './client'; -// 게임 런처용 일회용 티켓 발급 (JWT를 URL에 노출하지 않기 위해 사용) +/** + * 게임 런처용 일회용 티켓 발급 + * JWT를 URL에 직접 노출하지 않기 위해 단기 티켓으로 대체 + * @returns {Promise} 티켓 문자열 + */ export async function createLaunchTicket() { const data = await apiFetch('/api/auth/launch-ticket', { method: 'POST' }); return data.ticket; } - diff --git a/src/api/chain.js b/src/api/chain.js index 8b8c879..689759f 100644 --- a/src/api/chain.js +++ b/src/api/chain.js @@ -1,8 +1,11 @@ import { apiFetch } from './client'; +// exportWalletKey는 비밀번호 오류 시 서버가 401을 반환하므로 +// apiFetch의 401 자동 refresh/로그아웃을 우회하기 위해 BASE를 직접 참조 const BASE = import.meta.env.VITE_API_BASE_URL || ''; // --- 지갑 --- + export async function getBalance() { return apiFetch('/api/chain/balance'); } @@ -11,8 +14,13 @@ export async function getWallet() { return apiFetch('/api/chain/wallet'); } -// 키 내보내기는 비밀번호 오류 시 서버가 401을 반환하므로, -// apiFetch의 401 자동 refresh/로그아웃을 우회하기 위해 직접 fetch한다. +/** + * 개인키 내보내기 + * apiFetch를 우회해 직접 fetch 사용 — 비밀번호 오류(401)를 로그아웃 없이 처리하기 위함 + * @param {string} password + * @returns {Promise<{privateKey: string}>} + * @throws {Error} 비밀번호 오류 시 status 401 + */ export async function exportWalletKey(password) { const token = localStorage.getItem('token'); const headers = { 'Content-Type': 'application/json' }; @@ -33,6 +41,7 @@ export async function exportWalletKey(password) { } // --- 자산 --- + export async function getAssets() { return apiFetch('/api/chain/assets'); } @@ -42,11 +51,13 @@ export async function getAsset(id) { } // --- 인벤토리 --- + export async function getInventory() { return apiFetch('/api/chain/inventory'); } // --- 마켓 --- + export async function getMarketListings() { return apiFetch('/api/chain/market'); } @@ -55,6 +66,12 @@ export async function getListing(id) { return apiFetch(`/api/chain/market/${id}`); } +/** + * Idempotency-Key 헤더를 붙인 POST 요청 + * 중복 제출(네트워크 재시도 등)로 인한 이중 처리를 서버에서 방지 + * @param {string} path + * @param {object} body + */ function idempotentPost(path, body) { return apiFetch(path, { method: 'POST', diff --git a/src/api/client.js b/src/api/client.js index 40dacd8..67675b0 100644 --- a/src/api/client.js +++ b/src/api/client.js @@ -1,6 +1,10 @@ const BASE = import.meta.env.VITE_API_BASE_URL || ''; -/** 네트워크 에러 메시지를 한국어로 변환 */ +/** + * 네트워크 에러 메시지를 한국어로 변환 + * @param {string} message + * @returns {string} + */ function localizeError(message) { if (typeof message !== 'string') return message; if (message.includes('Failed to fetch')) return '서버에 연결할 수 없습니다'; @@ -9,9 +13,14 @@ function localizeError(message) { return message; } -// 동시 401 발생 시 refresh를 한 번만 실행하기 위한 Promise 공유 let refreshingPromise = null; +/** + * 리프레시 토큰으로 액세스 토큰 갱신 + * 동시 401 발생 시 refresh를 한 번만 실행하기 위해 Promise를 공유 + * @returns {Promise} 새 액세스 토큰 + * @throws {Error} refresh_failed — 리프레시 토큰 만료 또는 서버 오류 + */ export async function tryRefresh() { if (refreshingPromise) return refreshingPromise; @@ -40,7 +49,7 @@ async function doFetch(path, options, token) { return fetch(BASE + path, { ...options, headers, credentials: 'include' }); } -/** 에러 코드별 기본 한국어 메시지 */ +/** @type {Record} 에러 코드별 기본 한국어 메시지 */ const ERROR_MESSAGES = { bad_request: '잘못된 요청입니다', unauthorized: '로그인이 필요합니다', @@ -66,12 +75,20 @@ async function parseError(res) { return err; } -// 204 No Content는 null 반환, 나머지는 JSON 파싱 async function parseResponse(res) { if (res.status === 204) return null; return res.json(); } +/** + * 인증 포함 API 요청 래퍼 + * - 401 응답 시 토큰 자동 갱신 후 재시도 + * - 네트워크 에러 / 5xx 응답 시 GET·HEAD 요청만 최대 2회 재시도 (exponential backoff) + * @param {string} path - API 경로 (예: '/api/chain/balance') + * @param {RequestInit} [options={}] - fetch 옵션 + * @returns {Promise} 응답 JSON (204는 null) + * @throws {Error} HTTP 에러 또는 네트워크 에러 + */ export async function apiFetch(path, options = {}, _retryCount = 0) { const token = localStorage.getItem('token'); let res; diff --git a/src/api/users.js b/src/api/users.js index 34b57c1..79190ba 100644 --- a/src/api/users.js +++ b/src/api/users.js @@ -1,7 +1,8 @@ import { apiFetch } from './client'; export function getUsers(offset = 0, limit = 20) { - return apiFetch(`/api/users?offset=${offset}&limit=${limit}`); + const params = new URLSearchParams({ offset, limit }); + return apiFetch(`/api/users?${params}`); } export function updateUserRole(id, role) {