검증게시판 기능 추가
This commit is contained in:
@@ -0,0 +1,213 @@
|
||||
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));
|
||||
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 · 파일당 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})`);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user