Files
goldenChart/frontend/src/utils/verificationBoardApi.ts
T

214 lines
6.7 KiB
TypeScript

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<string, string> {
const headers: Record<string, string> = {
'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<string, string> {
const headers: Record<string, string> = {
'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<T>(path: string): Promise<T | null> {
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<T>(path: string, init: RequestInit): Promise<T> {
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<VerificationIssueDto[]> {
const qs = stage ? `?stage=${encodeURIComponent(stage)}` : '';
return (await apiGet<VerificationIssueDto[]>(`/verification-issues${qs}`)) ?? [];
}
export async function loadVerificationIssue(id: number): Promise<VerificationIssueDto | null> {
return apiGet<VerificationIssueDto>(`/verification-issues/${id}`);
}
export async function saveVerificationIssue(dto: VerificationIssueDto): Promise<VerificationIssueDto> {
return mutateOrThrow<VerificationIssueDto>('/verification-issues', {
method: 'POST',
body: JSON.stringify(dto),
});
}
export async function deleteVerificationIssue(id: number): Promise<void> {
await mutateOrThrow<void>(`/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<VerificationIssueCommentDto[]> {
return (await apiGet<VerificationIssueCommentDto[]>(
`/verification-issues/${issueId}/comments`,
)) ?? [];
}
export async function createVerificationIssueComment(
issueId: number,
dto: Pick<VerificationIssueCommentDto, 'content' | 'authorName'>,
): Promise<VerificationIssueCommentDto> {
return mutateOrThrow<VerificationIssueCommentDto>(
`/verification-issues/${issueId}/comments`,
{ method: 'POST', body: JSON.stringify(dto) },
);
}
export async function updateVerificationIssueComment(
issueId: number,
commentId: number,
dto: Pick<VerificationIssueCommentDto, 'content'>,
): Promise<VerificationIssueCommentDto> {
return mutateOrThrow<VerificationIssueCommentDto>(
`/verification-issues/${issueId}/comments/${commentId}`,
{ method: 'PUT', body: JSON.stringify(dto) },
);
}
export async function deleteVerificationIssueComment(
issueId: number,
commentId: number,
): Promise<void> {
await mutateOrThrow<void>(
`/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<VerificationIssueImageDto[]> {
return (await apiGet<VerificationIssueImageDto[]>(
`/verification-issues/${issueId}/images`,
)) ?? [];
}
export async function uploadVerificationIssueImages(
issueId: number,
files: File[],
): Promise<VerificationIssueImageDto[]> {
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<void> {
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})`);
}
}