Files
todos2/frontend/src/lib/api.ts
jungwoo choi 074b5133bf feat: 풀스택 할일관리 앱 구현 (통합 모달 + 간트차트)
- Backend: FastAPI + MongoDB + Redis (카테고리, 할일 CRUD, 파일 첨부, 검색, 대시보드)
- Frontend: Next.js 15 + Tailwind + React Query + Zustand
- 통합 TodoModal: 생성/수정 모달 통합, 탭 구조 (기본/태그와 첨부)
- 간트차트: 카테고리별 할일 타임라인 시각화
- TodoCard: 제목/카테고리/우선순위/태그/첨부 한줄 표시
- Docker Compose 배포 (Frontend:3010, Backend:8010, MongoDB:27021, Redis:6391)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 15:45:03 +09:00

122 lines
3.0 KiB
TypeScript

import { ApiError } from "@/types";
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000";
class ApiClient {
private baseUrl: string;
constructor(baseUrl: string) {
this.baseUrl = baseUrl;
}
private async request<T>(
method: string,
path: string,
options?: {
body?: unknown;
params?: Record<string, string | number | boolean | undefined | null>;
}
): Promise<T> {
const url = new URL(`${this.baseUrl}${path}`);
if (options?.params) {
Object.entries(options.params).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
url.searchParams.set(key, String(value));
}
});
}
const res = await fetch(url.toString(), {
method,
headers: {
"Content-Type": "application/json",
},
body: options?.body ? JSON.stringify(options.body) : undefined,
});
if (res.status === 204) {
return undefined as T;
}
if (!res.ok) {
const error = await res
.json()
.catch(() => ({ detail: "알 수 없는 오류가 발생했습니다" }));
throw {
detail: error.detail || `HTTP ${res.status} Error`,
status: res.status,
} as ApiError;
}
return res.json();
}
get<T>(
path: string,
params?: Record<string, string | number | boolean | undefined | null>
) {
return this.request<T>("GET", path, { params });
}
post<T>(path: string, body?: unknown) {
return this.request<T>("POST", path, { body });
}
put<T>(path: string, body?: unknown) {
return this.request<T>("PUT", path, { body });
}
patch<T>(path: string, body?: unknown) {
return this.request<T>("PATCH", path, { body });
}
delete<T>(path: string) {
return this.request<T>("DELETE", path);
}
async uploadFiles<T>(path: string, files: File[]): Promise<T> {
const url = new URL(`${this.baseUrl}${path}`);
const formData = new FormData();
files.forEach((file) => formData.append("files", file));
const res = await fetch(url.toString(), {
method: "POST",
body: formData,
});
if (!res.ok) {
const error = await res
.json()
.catch(() => ({ detail: "파일 업로드에 실패했습니다" }));
throw {
detail: error.detail || `HTTP ${res.status} Error`,
status: res.status,
} as ApiError;
}
return res.json();
}
async downloadFile(path: string, filename: string): Promise<void> {
const url = new URL(`${this.baseUrl}${path}`);
const res = await fetch(url.toString());
if (!res.ok) {
throw new Error("다운로드에 실패했습니다");
}
const blob = await res.blob();
const downloadUrl = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = downloadUrl;
link.download = filename;
document.body.appendChild(link);
link.click();
link.remove();
window.URL.revokeObjectURL(downloadUrl);
}
}
export const apiClient = new ApiClient(API_BASE_URL);