import { appendClientIdHeader } from '../../app/main/clientIdentity'; import { normalizeAutomationTypeId } from '../../app/main/automationTypeAccess'; import { getRegisteredAccessToken } from '../../app/main/tokenAccess'; import type { BoardAutomationType, BoardDraft, BoardPost } from './types'; class BoardApiError extends Error { status: number; constructor(message: string, status: number) { super(message); this.name = 'BoardApiError'; this.status = status; } } function normalizeBoardAutomationType(value: unknown): BoardAutomationType { return normalizeAutomationTypeId(value); } function resolveBoardApiBaseUrl() { if (import.meta.env.VITE_WORK_SERVER_URL) { return import.meta.env.VITE_WORK_SERVER_URL; } return '/api'; } function resolveBoardFallbackBaseUrl() { 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 BOARD_API_BASE_URL = resolveBoardApiBaseUrl(); const BOARD_API_FALLBACK_BASE_URL = !import.meta.env.VITE_WORK_SERVER_URL && BOARD_API_BASE_URL === '/api' ? resolveBoardFallbackBaseUrl() : null; async function requestOnce(baseUrl: string, path: string, init?: RequestInit) { const headers = appendClientIdHeader(init?.headers); const hasBody = init?.body !== undefined && init.body !== null; const method = init?.method?.toUpperCase() ?? 'GET'; const controller = new AbortController(); const timeoutId = globalThis.setTimeout(() => controller.abort(), 8000); if (hasBody && !headers.has('Content-Type')) { headers.set('Content-Type', 'application/json'); } const token = getRegisteredAccessToken(); if (token && !headers.has('X-Access-Token')) { headers.set('X-Access-Token', token); } let response: Response; try { response = await fetch(`${baseUrl}${path}`, { ...init, headers, signal: controller.signal, cache: init?.cache ?? (method === 'GET' ? 'no-store' : undefined), }); } catch (error) { globalThis.clearTimeout(timeoutId); if (error instanceof DOMException && error.name === 'AbortError') { throw new BoardApiError('게시판 서버 응답이 지연됩니다.', 408); } throw error; } globalThis.clearTimeout(timeoutId); if (!response.ok) { const text = await response.text(); let message = text || '게시판 요청에 실패했습니다.'; try { const payload = JSON.parse(text) as { message?: string }; message = payload.message || message; } catch { // Keep the plain text fallback when the response body is not JSON. } throw new BoardApiError(message, response.status); } return response.json() as Promise; } async function request(path: string, init?: RequestInit): Promise { try { return await requestOnce(BOARD_API_BASE_URL, path, init); } catch (error) { const shouldRetryWithFallback = BOARD_API_FALLBACK_BASE_URL && BOARD_API_FALLBACK_BASE_URL !== BOARD_API_BASE_URL && (error instanceof BoardApiError ? 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(BOARD_API_FALLBACK_BASE_URL, path, init); } } export async function setupBoard() { return request<{ ok: boolean; table: string }>('/board/setup', { method: 'POST', body: JSON.stringify({}), }); } export async function fetchBoardPosts() { const response = await request<{ ok: boolean; items: BoardPost[] }>('/board/posts'); return response.items.map((item) => ({ ...item, automationType: normalizeBoardAutomationType(item.automationType), })); } export async function createBoardPost(draft: BoardDraft) { const response = await request<{ ok: boolean; item: BoardPost }>('/board/posts', { method: 'POST', body: JSON.stringify({ title: draft.title, content: draft.content, automationType: draft.automationType, }), }); return { ...response.item, automationType: normalizeBoardAutomationType(response.item.automationType), }; } export async function updateBoardPost(draft: BoardDraft) { if (!draft.id) { throw new Error('수정할 게시글 ID가 없습니다.'); } const response = await request<{ ok: boolean; item: BoardPost }>(`/board/posts/${draft.id}`, { method: 'PATCH', body: JSON.stringify({ title: draft.title, content: draft.content, automationType: draft.automationType, }), }); return { ...response.item, automationType: normalizeBoardAutomationType(response.item.automationType), }; } export async function receiveBoardPostAutomation(id: number) { const response = await request<{ ok: boolean; item: BoardPost; planItemId: number | null; alreadyReceived: boolean; }>(`/board/posts/${id}/actions/automation-receive`, { method: 'POST', body: JSON.stringify({}), }); return { item: { ...response.item, automationType: normalizeBoardAutomationType(response.item.automationType), }, planItemId: response.planItemId, alreadyReceived: response.alreadyReceived, }; } export async function deleteBoardPost(id: number) { const response = await request<{ ok: boolean; id: number }>(`/board/posts/${id}`, { method: 'DELETE', }); return response.id; }