import type { VerificationIssueStage } from './verificationBoardStages'; const API_BASE = import.meta.env.VITE_API_BASE_URL ?? 'http://localhost:8080/api'; export interface VerificationIssueDto { id?: number; title: string; content: string; reproductionPath?: string; stage: VerificationIssueStage; createdAt?: string; updatedAt?: string; } function getDeviceId(): string { let id = localStorage.getItem('gc_device_id'); if (!id) { id = crypto.randomUUID(); localStorage.setItem('gc_device_id', id); } return id; } function authHeaders(): Record { const headers: Record = { 'Content-Type': 'application/json', 'x-device-id': getDeviceId(), }; const userId = localStorage.getItem('gc_user_id'); if (userId) headers['x-user-id'] = userId; return headers; } function authHeadersMultipart(): Record { const headers: Record = { 'x-device-id': getDeviceId(), }; const userId = localStorage.getItem('gc_user_id'); if (userId) headers['x-user-id'] = userId; return headers; } export function verificationIssueImageUrl(issueId: number, imageId: number): string { return `${API_BASE}/verification-issues/${issueId}/images/${imageId}/file`; } async function apiGet(path: string): Promise { try { const res = await fetch(`${API_BASE}${path}`, { headers: authHeaders() }); if (!res.ok) return null; return (await res.json()) as T; } catch { return null; } } async function mutateOrThrow(path: string, init: RequestInit): Promise { const res = await fetch(`${API_BASE}${path}`, { ...init, headers: { ...authHeaders(), ...(init.headers ?? {}) }, }); const text = await res.text().catch(() => ''); if (!res.ok) { throw new Error(text ? text.slice(0, 200) : `요청 실패 (${res.status})`); } if (res.status === 204 || !text) return undefined as T; return JSON.parse(text) as T; } export async function loadVerificationIssues( stage?: VerificationIssueStage | '', ): Promise { const qs = stage ? `?stage=${encodeURIComponent(stage)}` : ''; return (await apiGet(`/verification-issues${qs}`)) ?? []; } export async function loadVerificationIssue(id: number): Promise { return apiGet(`/verification-issues/${id}`); } export async function saveVerificationIssue(dto: VerificationIssueDto): Promise { return mutateOrThrow('/verification-issues', { method: 'POST', body: JSON.stringify(dto), }); } export async function deleteVerificationIssue(id: number): Promise { await mutateOrThrow(`/verification-issues/${id}`, { method: 'DELETE' }); } // ── 댓글 ───────────────────────────────────────────────────────────────────── export interface VerificationIssueCommentDto { id?: number; issueId?: number; content: string; authorName?: string; createdAt?: string; updatedAt?: string; } export async function loadVerificationIssueComments( issueId: number, ): Promise { return (await apiGet( `/verification-issues/${issueId}/comments`, )) ?? []; } export async function createVerificationIssueComment( issueId: number, dto: Pick, ): Promise { return mutateOrThrow( `/verification-issues/${issueId}/comments`, { method: 'POST', body: JSON.stringify(dto) }, ); } export async function updateVerificationIssueComment( issueId: number, commentId: number, dto: Pick, ): Promise { return mutateOrThrow( `/verification-issues/${issueId}/comments/${commentId}`, { method: 'PUT', body: JSON.stringify(dto) }, ); } export async function deleteVerificationIssueComment( issueId: number, commentId: number, ): Promise { await mutateOrThrow( `/verification-issues/${issueId}/comments/${commentId}`, { method: 'DELETE' }, ); } // ── 첨부 이미지 ───────────────────────────────────────────────────────────── export interface VerificationIssueImageDto { id?: number; issueId?: number; fileName?: string; contentType?: string; fileSize?: number; sortOrder?: number; url?: string; createdAt?: string; } export async function loadVerificationIssueImages( issueId: number, ): Promise { return (await apiGet( `/verification-issues/${issueId}/images`, )) ?? []; } export async function uploadVerificationIssueImages( issueId: number, files: File[], ): Promise { const form = new FormData(); files.forEach(f => form.append('files', f, f.name)); const res = await fetch(`${API_BASE}/verification-issues/${issueId}/images`, { method: 'POST', headers: authHeadersMultipart(), body: form, }); const text = await res.text().catch(() => ''); if (!res.ok) { throw new Error(parseVerificationUploadError(res.status, text)); } return JSON.parse(text) as VerificationIssueImageDto[]; } function parseVerificationUploadError(status: number, text: string): string { if (text) { try { const json = JSON.parse(text) as { message?: string }; if (json.message) return json.message; } catch { if (text.includes('413') || text.toLowerCase().includes('too large')) { return '업로드 용량이 너무 큽니다. 이미지를 줄이거나 5MB 이하 파일을 사용하세요.'; } if (!text.trimStart().startsWith('<')) { return text.slice(0, 200); } } } if (status === 413) { return '업로드 용량이 너무 큽니다. 이미지를 줄이거나 5MB 이하 파일을 사용하세요.'; } if (status === 400) return '파일 형식 또는 크기를 확인하세요. (jpeg, png, gif, webp, bmp, zip, hwp, hwpx · 파일당 5MB)'; return `이미지 업로드에 실패했습니다. (${status})`; } export async function deleteVerificationIssueImage( issueId: number, imageId: number, ): Promise { const res = await fetch( `${API_BASE}/verification-issues/${issueId}/images/${imageId}`, { method: 'DELETE', headers: authHeadersMultipart() }, ); if (!res.ok && res.status !== 204) { throw new Error(`삭제 실패 (${res.status})`); } }