refactor: api/ 주석 정리 및 소소한 개선
- client.js JSDoc 추가 (apiFetch, tryRefresh, apiUpload, localizeError) - auth.js 단순 함수 JSDoc 제거, createLaunchTicket why 주석 유지 - chain.js BASE 중복 선언 이유 주석 추가, 단순 함수 JSDoc 제거 - announcements.js 후행 빈 줄 제거 - users.js getUsers 쿼리스트링 → URLSearchParams 변경 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -32,9 +32,12 @@ export async function ssafyCallback(code, state) {
|
|||||||
// 토큰을 리프레시하고 새 access token을 반환 (동시 호출 방지 포함)
|
// 토큰을 리프레시하고 새 access token을 반환 (동시 호출 방지 포함)
|
||||||
export { tryRefresh as refreshToken } from './client';
|
export { tryRefresh as refreshToken } from './client';
|
||||||
|
|
||||||
// 게임 런처용 일회용 티켓 발급 (JWT를 URL에 노출하지 않기 위해 사용)
|
/**
|
||||||
|
* 게임 런처용 일회용 티켓 발급
|
||||||
|
* JWT를 URL에 직접 노출하지 않기 위해 단기 티켓으로 대체
|
||||||
|
* @returns {Promise<string>} 티켓 문자열
|
||||||
|
*/
|
||||||
export async function createLaunchTicket() {
|
export async function createLaunchTicket() {
|
||||||
const data = await apiFetch('/api/auth/launch-ticket', { method: 'POST' });
|
const data = await apiFetch('/api/auth/launch-ticket', { method: 'POST' });
|
||||||
return data.ticket;
|
return data.ticket;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import { apiFetch } from './client';
|
import { apiFetch } from './client';
|
||||||
|
|
||||||
|
// exportWalletKey는 비밀번호 오류 시 서버가 401을 반환하므로
|
||||||
|
// apiFetch의 401 자동 refresh/로그아웃을 우회하기 위해 BASE를 직접 참조
|
||||||
const BASE = import.meta.env.VITE_API_BASE_URL || '';
|
const BASE = import.meta.env.VITE_API_BASE_URL || '';
|
||||||
|
|
||||||
// --- 지갑 ---
|
// --- 지갑 ---
|
||||||
|
|
||||||
export async function getBalance() {
|
export async function getBalance() {
|
||||||
return apiFetch('/api/chain/balance');
|
return apiFetch('/api/chain/balance');
|
||||||
}
|
}
|
||||||
@@ -11,8 +14,13 @@ export async function getWallet() {
|
|||||||
return apiFetch('/api/chain/wallet');
|
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) {
|
export async function exportWalletKey(password) {
|
||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem('token');
|
||||||
const headers = { 'Content-Type': 'application/json' };
|
const headers = { 'Content-Type': 'application/json' };
|
||||||
@@ -33,6 +41,7 @@ export async function exportWalletKey(password) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- 자산 ---
|
// --- 자산 ---
|
||||||
|
|
||||||
export async function getAssets() {
|
export async function getAssets() {
|
||||||
return apiFetch('/api/chain/assets');
|
return apiFetch('/api/chain/assets');
|
||||||
}
|
}
|
||||||
@@ -42,11 +51,13 @@ export async function getAsset(id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- 인벤토리 ---
|
// --- 인벤토리 ---
|
||||||
|
|
||||||
export async function getInventory() {
|
export async function getInventory() {
|
||||||
return apiFetch('/api/chain/inventory');
|
return apiFetch('/api/chain/inventory');
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 마켓 ---
|
// --- 마켓 ---
|
||||||
|
|
||||||
export async function getMarketListings() {
|
export async function getMarketListings() {
|
||||||
return apiFetch('/api/chain/market');
|
return apiFetch('/api/chain/market');
|
||||||
}
|
}
|
||||||
@@ -55,6 +66,12 @@ export async function getListing(id) {
|
|||||||
return apiFetch(`/api/chain/market/${id}`);
|
return apiFetch(`/api/chain/market/${id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Idempotency-Key 헤더를 붙인 POST 요청
|
||||||
|
* 중복 제출(네트워크 재시도 등)로 인한 이중 처리를 서버에서 방지
|
||||||
|
* @param {string} path
|
||||||
|
* @param {object} body
|
||||||
|
*/
|
||||||
function idempotentPost(path, body) {
|
function idempotentPost(path, body) {
|
||||||
return apiFetch(path, {
|
return apiFetch(path, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
const BASE = import.meta.env.VITE_API_BASE_URL || '';
|
const BASE = import.meta.env.VITE_API_BASE_URL || '';
|
||||||
|
|
||||||
/** 네트워크 에러 메시지를 한국어로 변환 */
|
/**
|
||||||
|
* 네트워크 에러 메시지를 한국어로 변환
|
||||||
|
* @param {string} message
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
function localizeError(message) {
|
function localizeError(message) {
|
||||||
if (typeof message !== 'string') return message;
|
if (typeof message !== 'string') return message;
|
||||||
if (message.includes('Failed to fetch')) return '서버에 연결할 수 없습니다';
|
if (message.includes('Failed to fetch')) return '서버에 연결할 수 없습니다';
|
||||||
@@ -9,9 +13,14 @@ function localizeError(message) {
|
|||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 동시 401 발생 시 refresh를 한 번만 실행하기 위한 Promise 공유
|
|
||||||
let refreshingPromise = null;
|
let refreshingPromise = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 리프레시 토큰으로 액세스 토큰 갱신
|
||||||
|
* 동시 401 발생 시 refresh를 한 번만 실행하기 위해 Promise를 공유
|
||||||
|
* @returns {Promise<string>} 새 액세스 토큰
|
||||||
|
* @throws {Error} refresh_failed — 리프레시 토큰 만료 또는 서버 오류
|
||||||
|
*/
|
||||||
export async function tryRefresh() {
|
export async function tryRefresh() {
|
||||||
if (refreshingPromise) return refreshingPromise;
|
if (refreshingPromise) return refreshingPromise;
|
||||||
|
|
||||||
@@ -40,7 +49,7 @@ async function doFetch(path, options, token) {
|
|||||||
return fetch(BASE + path, { ...options, headers, credentials: 'include' });
|
return fetch(BASE + path, { ...options, headers, credentials: 'include' });
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 에러 코드별 기본 한국어 메시지 */
|
/** @type {Record<string, string>} 에러 코드별 기본 한국어 메시지 */
|
||||||
const ERROR_MESSAGES = {
|
const ERROR_MESSAGES = {
|
||||||
bad_request: '잘못된 요청입니다',
|
bad_request: '잘못된 요청입니다',
|
||||||
unauthorized: '로그인이 필요합니다',
|
unauthorized: '로그인이 필요합니다',
|
||||||
@@ -66,12 +75,20 @@ async function parseError(res) {
|
|||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 204 No Content는 null 반환, 나머지는 JSON 파싱
|
|
||||||
async function parseResponse(res) {
|
async function parseResponse(res) {
|
||||||
if (res.status === 204) return null;
|
if (res.status === 204) return null;
|
||||||
return res.json();
|
return res.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 인증 포함 API 요청 래퍼
|
||||||
|
* - 401 응답 시 토큰 자동 갱신 후 재시도
|
||||||
|
* - 네트워크 에러 / 5xx 응답 시 GET·HEAD 요청만 최대 2회 재시도 (exponential backoff)
|
||||||
|
* @param {string} path - API 경로 (예: '/api/chain/balance')
|
||||||
|
* @param {RequestInit} [options={}] - fetch 옵션
|
||||||
|
* @returns {Promise<any>} 응답 JSON (204는 null)
|
||||||
|
* @throws {Error} HTTP 에러 또는 네트워크 에러
|
||||||
|
*/
|
||||||
export async function apiFetch(path, options = {}, _retryCount = 0) {
|
export async function apiFetch(path, options = {}, _retryCount = 0) {
|
||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem('token');
|
||||||
let res;
|
let res;
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { apiFetch } from './client';
|
import { apiFetch } from './client';
|
||||||
|
|
||||||
export function getUsers(offset = 0, limit = 20) {
|
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) {
|
export function updateUserRole(id, role) {
|
||||||
|
|||||||
Reference in New Issue
Block a user