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>
This commit is contained in:
jungwoo choi
2026-02-12 15:45:03 +09:00
parent b54811ad8d
commit 074b5133bf
81 changed files with 17027 additions and 19 deletions

View File

@ -0,0 +1,95 @@
"use client";
import {
PieChart,
Pie,
Cell,
ResponsiveContainer,
Tooltip,
Legend,
} from "recharts";
import { CategoryStat } from "@/types";
interface CategoryChartProps {
data: CategoryStat[] | undefined;
isLoading: boolean;
}
export default function CategoryChart({ data, isLoading }: CategoryChartProps) {
if (isLoading) {
return (
<div className="bg-white rounded-xl border border-gray-200 p-5">
<h3 className="text-sm font-semibold text-gray-900 mb-4">
</h3>
<div className="h-[250px] flex items-center justify-center">
<div className="w-40 h-40 rounded-full border-8 border-gray-200 animate-pulse" />
</div>
</div>
);
}
if (!data || data.length === 0) {
return (
<div className="bg-white rounded-xl border border-gray-200 p-5">
<h3 className="text-sm font-semibold text-gray-900 mb-4">
</h3>
<div className="h-[250px] flex items-center justify-center text-gray-400 text-sm">
</div>
</div>
);
}
const chartData = data.map((item) => ({
name: item.name,
value: item.count,
color: item.color,
}));
return (
<div className="bg-white rounded-xl border border-gray-200 p-5">
<h3 className="text-sm font-semibold text-gray-900 mb-4">
</h3>
<div className="h-[250px]">
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={chartData}
cx="50%"
cy="50%"
innerRadius={60}
outerRadius={90}
paddingAngle={2}
dataKey="value"
>
{chartData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
))}
</Pie>
<Tooltip
formatter={(value: number, name: string) => [
`${value}`,
name,
]}
contentStyle={{
borderRadius: "8px",
border: "1px solid #e5e7eb",
fontSize: "12px",
}}
/>
<Legend
verticalAlign="bottom"
height={36}
formatter={(value) => (
<span className="text-xs text-gray-600">{value}</span>
)}
/>
</PieChart>
</ResponsiveContainer>
</div>
</div>
);
}