import axios, { type AxiosInstance, type AxiosResponse, type InternalAxiosRequestConfig, } from 'axios' // 표준 응답 포맷 타입 (Global Response Wrapper) — 백엔드 TransformInterceptor와 동일 구조 export interface StandardResponse { success: boolean data?: T error?: { code: string message: string } } // 에러 코드 사전 — CLAUDE.md / 백엔드 HttpExceptionFilter 와 1:1 매핑 const ErrorCodeLexicon: Record = { SYS_001: '일시적인 시스템 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.', AUTH_001: '로그인이 필요한 서비스입니다. 로그인 후 이용해 주세요.', AUTH_002: '안전을 위해 로그아웃 되었습니다. 다시 로그인해 주세요.', AUTH_003: '해당 메뉴나 기능에 접근할 수 있는 권한이 없습니다.', VAL_001: '입력하신 정보를 다시 확인해 주세요. (필수값 누락 또는 형식 오류)', RES_001: '요청하신 정보나 페이지를 찾을 수 없습니다.', BIZ_001: '요청하신 작업을 완료하지 못했습니다. 다시 시도해 주세요.', } // 사용자 노출용 알림 — Toast 라이브러리 도입 시 이 함수만 교체하면 됨 // SSG 프리렌더(서버) 단계에서는 window 가 없으므로 클라이언트에서만 동작 const showErrorUI = (message: string) => { if (import.meta.client) { window.alert(message) } } // 싱글턴 인스턴스 — 최초 호출 시 1회만 생성 let apiInstance: AxiosInstance | null = null const createApi = (): AxiosInstance => { // 런타임 환경변수에서 API 베이스 URL 주입 (NUXT_PUBLIC_API_BASE_URL) const config = useRuntimeConfig() const api = axios.create({ baseURL: (config.public.apiBaseUrl as string) || '/api', timeout: 10000, // HttpOnly/Secure 쿠키 인증 시 자격 증명을 함께 전송 withCredentials: true, }) // 요청 인터셉터 — CSRF, 추가 헤더 등 필요 시 확장 api.interceptors.request.use( (cfg: InternalAxiosRequestConfig) => cfg, (error: unknown) => Promise.reject(error), ) // 응답 인터셉터 — Global Response Wrapper 언래핑 + 에러코드 → 메시지 매핑 api.interceptors.response.use( (response: AxiosResponse) => { const payload = response.data if (payload && typeof payload.success === 'boolean') { if (payload.success) { // 성공 시 data 만 언래핑하여 반환 return payload.data as never } // HTTP 200 이지만 비즈니스 로직상 실패인 경우 const errCode = payload.error?.code || 'BIZ_001' showErrorUI(ErrorCodeLexicon[errCode] || ErrorCodeLexicon.SYS_001) return Promise.reject(payload.error) } return payload as never }, (error) => { let errCode = 'SYS_001' if (error?.response) { const status: number = error.response.status const payload = error.response.data as StandardResponse | undefined if (payload?.error?.code) { errCode = payload.error.code } else if (status === 401) errCode = 'AUTH_001' else if (status === 403) errCode = 'AUTH_003' else if (status === 400 || status === 422) errCode = 'VAL_001' else if (status === 404) errCode = 'RES_001' } showErrorUI(ErrorCodeLexicon[errCode] || ErrorCodeLexicon.SYS_001) return Promise.reject(error) }, ) return api } /** * useApi — 컴포넌트/스토어에서 axios 인스턴스를 가져올 때 사용하는 composable * 직접 import 대신 useApi() 사용을 권장 (테스트 시 모킹 용이) */ export function useApi(): AxiosInstance { if (!apiInstance) { apiInstance = createApi() } return apiInstance }