const API_BASE_URL = import.meta.env.VITE_API_BASE_URL ?? ''; const ADMIN_LOGIN_URL = '/uat/uia/actionSecurityLogin.do'; const ADMIN_ID = 'admin'; const ADMIN_PASSWORD = '1'; const MAX_RETRY_COUNT = 3; type ApiResponse = { success: boolean; message: string; data: T; } class ApiClient { private loginPromise: Promise | null = null; private toApiUrl(path: string) { if (/^https?:\/\//i.test(path)) { return path; } return `${API_BASE_URL}${path}`; } private createHeaders(headers?: HeadersInit) { const result = new Headers(headers); result.set('X-Requested-With', 'XMLHttpRequest'); return result; } private async login() { if (!this.loginPromise) { this.loginPromise = fetch(this.toApiUrl(ADMIN_LOGIN_URL), { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-Requested-With': 'XMLHttpRequest', }, body: new URLSearchParams({ id: ADMIN_ID, password: ADMIN_PASSWORD, }), }).then(async (response) => { if (!response.ok) { throw new Error(`로그인 실패 : ${response.status}`); } const contentType = response.headers.get('content-type') ?? ''; if (contentType === 'application/json') { const result = (await response.json()) as ApiResponse; if (!result.success) { throw new Error(result.message); } } }).finally(() => { this.loginPromise = null; }); } return this.loginPromise; } private async needsLogin(response: Response) { if(response.status === 401 || response.status === 403) { return true; } const contentType= response.headers.get("content-type") ?? ''; if (contentType.includes('text/html')) { const html = await response.clone().text(); return ( html.includes('/uat/uia/actionMain.do') || html.includes('actionSecurityLogin.do') ); } if (contentType.includes('application/json')) { try { const result = (await response.clone().json()) as ApiResponse; return (!result.success && /login|로그인/i.test(result.message ?? '')); } catch { return false; } } return false; } private async request( path: string, init: RequestInit = {}, retryCount = 0 ): Promise { const response = await fetch(this.toApiUrl(path), { ...init, credentials: 'include', headers: this.createHeaders(init.headers), redirect: 'manual', }); if (await this.needsLogin(response)) { if (retryCount >= MAX_RETRY_COUNT) { throw new Error('인증 재시도 횟수 초과'); } await this.login(); return this.request(path, init, retryCount + 1); } return response; } async get(path: string): Promise { const response = await this.request(path, { method: 'GET', }); return this.parseJson(response); } async post( path: string, body?: unknown ): Promise { const response = await this.request(path, { method: 'POST', body: body ? JSON.stringify(body) : undefined, headers: { 'Content-Type': 'application/json', }, }); return this.parseJson(response); } private async parseJson(response: Response): Promise { if (!response.ok) { throw new Error(`API request failed: ${response.status}`); } const result = (await response.json()) as ApiResponse; if (!result.success) { throw new Error(result.message ?? 'API request failed'); } return result.data as T; } } export const apiClient = new ApiClient();