투두리스트랑 커스텀통계카드

This commit is contained in:
leeheejin
2025-10-20 15:52:22 +09:00
parent 652aa1e9b0
commit 7ceecd15af
11 changed files with 618 additions and 279 deletions

View File

@@ -38,6 +38,7 @@ export default function TodoWidget({ element }: TodoWidgetProps) {
const { selectedDate } = useDashboard();
const [todos, setTodos] = useState<TodoItem[]>([]);
const [internalTodos, setInternalTodos] = useState<TodoItem[]>([]); // 내장 API 투두
const [stats, setStats] = useState<TodoStats | null>(null);
const [loading, setLoading] = useState(true);
const [filter, setFilter] = useState<"all" | "pending" | "in_progress" | "completed">("all");
@@ -62,6 +63,21 @@ export default function TodoWidget({ element }: TodoWidgetProps) {
const token = localStorage.getItem("authToken");
const userLang = localStorage.getItem("userLang") || "KR";
// 내장 API 투두 항상 조회 (외부 DB 모드에서도)
const filterParam = filter !== "all" ? `?status=${filter}` : "";
const internalResponse = await fetch(`http://localhost:9771/api/todos${filterParam}`, {
headers: {
Authorization: `Bearer ${token}`,
},
});
let internalData: TodoItem[] = [];
if (internalResponse.ok) {
const result = await internalResponse.json();
internalData = result.data || [];
setInternalTodos(internalData);
}
// 외부 DB 조회 (dataSource가 설정된 경우)
if (element?.dataSource?.query) {
// console.log("🔍 TodoWidget - 외부 DB 조회 시작");
@@ -111,8 +127,10 @@ export default function TodoWidget({ element }: TodoWidgetProps) {
// console.log("📋 변환된 Todos:", externalTodos);
// console.log("📋 변환된 Todos 개수:", externalTodos.length);
setTodos(externalTodos);
setStats(calculateStatsFromTodos(externalTodos));
// 외부 DB 데이터 + 내장 데이터 합치기
const mergedTodos = [...externalTodos, ...internalData];
setTodos(mergedTodos);
setStats(calculateStatsFromTodos(mergedTodos));
// console.log("✅ setTodos, setStats 호출 완료!");
} else {
@@ -120,20 +138,10 @@ export default function TodoWidget({ element }: TodoWidgetProps) {
// console.error("❌ API 오류:", errorText);
}
}
// 내장 API 조회 (기본)
// 내장 API 조회 (기본)
else {
const filterParam = filter !== "all" ? `?status=${filter}` : "";
const response = await fetch(`http://localhost:9771/api/todos${filterParam}`, {
headers: {
Authorization: `Bearer ${token}`,
},
});
if (response.ok) {
const result = await response.json();
setTodos(result.data || []);
setStats(result.stats);
}
setTodos(internalData);
setStats(calculateStatsFromTodos(internalData));
}
} catch (error) {
// console.error("To-Do 로딩 오류:", error);
@@ -180,10 +188,6 @@ export default function TodoWidget({ element }: TodoWidgetProps) {
const handleAddTodo = async () => {
if (!newTodo.title.trim()) return;
if (isExternalData) {
alert("외부 데이터베이스 조회 모드에서는 추가할 수 없습니다.");
return;
}
try {
const token = localStorage.getItem("authToken");
@@ -325,28 +329,31 @@ export default function TodoWidget({ element }: TodoWidgetProps) {
<div className="flex h-full flex-col bg-gradient-to-br from-slate-50 to-blue-50">
{/* 제목 - 항상 표시 */}
<div className="border-b border-gray-200 bg-white px-4 py-2">
<h3 className="text-lg font-bold text-gray-800">{element?.customTitle || "To-Do / 긴급 지시"}</h3>
{selectedDate && (
<div className="mt-1 flex items-center gap-1 text-xs text-green-600">
<CalendarIcon className="h-3 w-3" />
<span className="font-semibold">{formatSelectedDate()} </span>
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-bold text-gray-800">{element?.customTitle || "To-Do / 긴급 지시"}</h3>
{selectedDate && (
<div className="mt-1 flex items-center gap-1 text-xs text-green-600">
<CalendarIcon className="h-3 w-3" />
<span className="font-semibold">{formatSelectedDate()} </span>
</div>
)}
</div>
)}
{/* 추가 버튼 - 항상 표시 */}
<button
onClick={() => setShowAddForm(!showAddForm)}
className="flex items-center gap-1 rounded-lg bg-primary px-3 py-1.5 text-sm text-white transition-colors hover:bg-primary/90"
title="할 일 추가"
>
<Plus className="h-4 w-4" />
</button>
</div>
</div>
{/* 헤더 (추가 버튼, 통계, 필터) - showHeader가 false일 때만 숨김 */}
{/* 헤더 (통계, 필터) - showHeader가 false일 때만 숨김 */}
{element?.showHeader !== false && (
<div className="border-b border-gray-200 bg-white px-4 py-3">
<div className="mb-3 flex items-center justify-end">
<button
onClick={() => setShowAddForm(!showAddForm)}
className="flex items-center gap-1 rounded-lg bg-primary px-3 py-1.5 text-sm text-white transition-colors hover:bg-primary/90"
>
<Plus className="h-4 w-4" />
</button>
</div>
{/* 통계 */}
{stats && (
<div className="grid grid-cols-4 gap-2 text-xs mb-3">
@@ -390,19 +397,21 @@ export default function TodoWidget({ element }: TodoWidgetProps) {
{/* 추가 폼 */}
{showAddForm && (
<div className="border-b border-gray-200 bg-white p-4">
<div className="max-h-[400px] overflow-y-auto border-b border-gray-200 bg-white p-4">
<div className="space-y-2">
<input
type="text"
placeholder="할 일 제목*"
value={newTodo.title}
onChange={(e) => setNewTodo({ ...newTodo, title: e.target.value })}
onKeyDown={(e) => e.stopPropagation()}
className="w-full rounded border border-gray-300 px-3 py-2 text-sm focus:border-primary focus:outline-none"
/>
<textarea
placeholder="상세 설명 (선택)"
value={newTodo.description}
onChange={(e) => setNewTodo({ ...newTodo, description: e.target.value })}
onKeyDown={(e) => e.stopPropagation()}
className="w-full rounded border border-gray-300 px-3 py-2 text-sm focus:border-primary focus:outline-none"
rows={2}
/>
@@ -454,7 +463,7 @@ export default function TodoWidget({ element }: TodoWidgetProps) {
)}
{/* To-Do 리스트 */}
<div className="flex-1 overflow-y-auto p-4">
<div className="flex-1 overflow-y-auto p-4 min-h-0">
{filteredTodos.length === 0 ? (
<div className="flex h-full items-center justify-center text-gray-400">
<div className="text-center">