import ky, { HTTPError } from 'ky'; import { API_BASE_URL } from '@/shared/config/env'; import { toCamelCase, toSnakeCase } from './util'; export class ApiError extends Error { readonly status: number; readonly statusText: string; readonly body?: unknown; constructor(status: number, statusText: string, body?: unknown) { super(`[${status}] ${statusText}`); this.name = 'ApiError'; this.status = status; this.statusText = statusText; this.body = body; } } export const apiClient = ky.create({ baseUrl: API_BASE_URL, headers: { 'Accept-Language': 'ko-KR' }, retry: { limit: 0 }, stringifyJson: (data) => JSON.stringify(toSnakeCase(data)), parseJson: (text) => toCamelCase(JSON.parse(text)), hooks: { beforeRequest: [ // TODO: 인증 헤더 자리 // (request) => request.headers.set('Authorization', `Bearer ${token}`), ], afterResponse: [], }, }); export interface OrvalRequestConfig { url: string; method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'; params?: Record; headers?: Record; data?: unknown; signal?: AbortSignal; } export const orvalInstance = async ({ url, method, params, headers, data, signal, }: OrvalRequestConfig): Promise => { const rawUrl = url.startsWith('/') ? url.slice(1) : url; try { const response = await apiClient(rawUrl, { method, headers, signal, ...(data !== undefined && { json: data }), ...(params && { searchParams: toSnakeCase(params) as Record, }), }); if ( response.status === 204 || response.headers.get('content-length') === '0' ) { return undefined as T; } return await response.json(); } catch (error) { if (error instanceof HTTPError) { throw new ApiError( error.response.status, error.response.statusText, error.data ); } throw error; } };