import { appendClientIdHeader } from './clientIdentity'; import { getRegisteredAccessToken } from './tokenAccess'; export type ErrorLogItem = { id: number; source: 'server' | 'client' | string; sourceLabel: string | null; errorType: string; errorName: string | null; errorMessage: string; detail: string | null; stackTrace: string | null; statusCode: number | null; requestMethod: string | null; requestPath: string | null; relatedPlanId: number | null; relatedWorkId: string | null; context: Record | null; createdAt: string; }; export type ReportClientErrorPayload = { errorType: string; errorName?: string | null; errorMessage: string; detail?: string | null; stackTrace?: string | null; statusCode?: number | null; requestMethod?: string | null; requestPath?: string | null; context?: Record | null; }; class ErrorLogApiError extends Error { status: number; constructor(message: string, status: number) { super(message); this.name = 'ErrorLogApiError'; this.status = status; } } function resolveErrorLogApiBaseUrl() { if (import.meta.env.VITE_WORK_SERVER_URL) { return import.meta.env.VITE_WORK_SERVER_URL; } return '/api'; } function resolveWorkServerFallbackBaseUrl() { if (typeof window === 'undefined') { return null; } const hostname = window.location.hostname; const isLocalWorkServerHost = hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '0.0.0.0'; if (!isLocalWorkServerHost) { return null; } const fallbackUrl = new URL(window.location.origin); fallbackUrl.port = '3100'; fallbackUrl.pathname = '/api'; fallbackUrl.search = ''; fallbackUrl.hash = ''; return fallbackUrl.toString().replace(/\/+$/, ''); } const ERROR_LOG_API_BASE_URL = resolveErrorLogApiBaseUrl(); const ERROR_LOG_API_FALLBACK_BASE_URL = !import.meta.env.VITE_WORK_SERVER_URL && ERROR_LOG_API_BASE_URL === '/api' ? resolveWorkServerFallbackBaseUrl() : null; async function requestOnce(baseUrl: string, path: string, init?: RequestInit): Promise { const headers = appendClientIdHeader(init?.headers); const hasBody = init?.body !== undefined && init.body !== null; const method = init?.method?.toUpperCase() ?? 'GET'; if (hasBody && !headers.has('Content-Type')) { headers.set('Content-Type', 'application/json'); } const response = await fetch(`${baseUrl}${path}`, { ...init, headers, cache: init?.cache ?? (method === 'GET' ? 'no-store' : undefined), }); if (!response.ok) { const text = await response.text(); try { const payload = JSON.parse(text) as { message?: string }; throw new ErrorLogApiError(payload.message || '에러 로그 요청에 실패했습니다.', response.status); } catch { throw new ErrorLogApiError(text || '에러 로그 요청에 실패했습니다.', response.status); } } return response.json() as Promise; } async function request(path: string, init?: RequestInit): Promise { try { return await requestOnce(ERROR_LOG_API_BASE_URL, path, init); } catch (error) { const shouldRetryWithFallback = ERROR_LOG_API_FALLBACK_BASE_URL && ERROR_LOG_API_FALLBACK_BASE_URL !== ERROR_LOG_API_BASE_URL && (error instanceof ErrorLogApiError ? error.status === 404 || error.status === 408 || error.status === 502 : error instanceof Error && /404|Failed to fetch|NetworkError/i.test(error.message)); if (!shouldRetryWithFallback) { throw error; } return requestOnce(ERROR_LOG_API_FALLBACK_BASE_URL, path, init); } } export async function fetchErrorLogs(limit = 50) { const token = getRegisteredAccessToken(); const response = await request<{ ok: boolean; items: ErrorLogItem[] }>(`/error-logs?limit=${limit}`, { headers: { 'X-Access-Token': token, }, }); return response.items; } export async function reportClientError(payload: ReportClientErrorPayload) { try { await request('/error-logs/report', { method: 'POST', body: JSON.stringify({ source: 'client', sourceLabel: '프론트엔드', errorType: payload.errorType, errorName: payload.errorName ?? null, errorMessage: payload.errorMessage, detail: payload.detail ?? null, stackTrace: payload.stackTrace ?? null, statusCode: payload.statusCode ?? null, requestMethod: payload.requestMethod ?? null, requestPath: payload.requestPath ?? null, context: payload.context ?? null, }), }); } catch { // ignore client-side logging failures } }