diff --git a/backend-node/data/todos/todos.json b/backend-node/data/todos/todos.json
index 0637a088..653d5636 100644
--- a/backend-node/data/todos/todos.json
+++ b/backend-node/data/todos/todos.json
@@ -1 +1,54 @@
-[]
\ No newline at end of file
+[
+ {
+ "id": "e5bb334c-d58a-4068-ad77-2607a41f4675",
+ "title": "ㅁㄴㅇㄹ",
+ "description": "ㅁㄴㅇㄹ",
+ "priority": "normal",
+ "status": "pending",
+ "assignedTo": "",
+ "dueDate": "2025-10-20T18:17",
+ "createdAt": "2025-10-20T06:15:49.610Z",
+ "updatedAt": "2025-10-20T06:15:49.610Z",
+ "isUrgent": false,
+ "order": 0
+ },
+ {
+ "id": "334be17c-7776-47e8-89ec-4b57c4a34bcd",
+ "title": "연동되어주겠니?",
+ "description": "",
+ "priority": "normal",
+ "status": "pending",
+ "assignedTo": "",
+ "dueDate": "",
+ "createdAt": "2025-10-20T06:20:06.343Z",
+ "updatedAt": "2025-10-20T06:20:06.343Z",
+ "isUrgent": false,
+ "order": 1
+ },
+ {
+ "id": "f85b81de-fcbd-4858-8973-247d9d6e70ed",
+ "title": "연동되어주겠니?11",
+ "description": "ㄴㅇㄹ",
+ "priority": "normal",
+ "status": "pending",
+ "assignedTo": "",
+ "dueDate": "2025-10-20T17:22",
+ "createdAt": "2025-10-20T06:20:53.818Z",
+ "updatedAt": "2025-10-20T06:20:53.818Z",
+ "isUrgent": false,
+ "order": 2
+ },
+ {
+ "id": "58d2b26f-5197-4df1-b5d4-724a72ee1d05",
+ "title": "연동되어주려무니",
+ "description": "ㅁㄴㅇㄹ",
+ "priority": "normal",
+ "status": "pending",
+ "assignedTo": "",
+ "dueDate": "2025-10-21T15:21",
+ "createdAt": "2025-10-20T06:21:19.817Z",
+ "updatedAt": "2025-10-20T06:21:19.817Z",
+ "isUrgent": false,
+ "order": 3
+ }
+]
\ No newline at end of file
diff --git a/frontend/components/admin/dashboard/CanvasElement.tsx b/frontend/components/admin/dashboard/CanvasElement.tsx
index 66b9d65f..db58207e 100644
--- a/frontend/components/admin/dashboard/CanvasElement.tsx
+++ b/frontend/components/admin/dashboard/CanvasElement.tsx
@@ -118,8 +118,8 @@ const WorkHistoryWidget = dynamic(() => import("@/components/dashboard/widgets/W
loading: () =>
로딩 중...
,
});
-// 운송 통계 위젯
-const TransportStatsWidget = dynamic(() => import("@/components/dashboard/widgets/TransportStatsWidget"), {
+// 커스텀 통계 카드 위젯
+const CustomStatsWidget = dynamic(() => import("@/components/dashboard/widgets/CustomStatsWidget"), {
ssr: false,
loading: () => 로딩 중...
,
});
@@ -750,9 +750,9 @@ export function CanvasElement({
) : element.type === "widget" && element.subtype === "transport-stats" ? (
- // 운송 통계 위젯 렌더링
+ // 커스텀 통계 카드 위젯 렌더링
-
+
) : element.type === "widget" && element.subtype === "todo" ? (
// To-Do 위젯 렌더링
diff --git a/frontend/components/admin/dashboard/DashboardDesigner.tsx b/frontend/components/admin/dashboard/DashboardDesigner.tsx
index f943375f..3e01cff1 100644
--- a/frontend/components/admin/dashboard/DashboardDesigner.tsx
+++ b/frontend/components/admin/dashboard/DashboardDesigner.tsx
@@ -647,7 +647,7 @@ function getElementTitle(type: ElementType, subtype: ElementSubtype): string {
case "work-history":
return "작업 이력";
case "transport-stats":
- return "운송 통계";
+ return "커스텀 통계 카드";
default:
return "위젯";
}
@@ -693,7 +693,7 @@ function getElementContent(type: ElementType, subtype: ElementSubtype): string {
case "work-history":
return "work-history";
case "transport-stats":
- return "transport-stats";
+ return "커스텀 통계 카드";
default:
return "위젯 내용이 여기에 표시됩니다";
}
diff --git a/frontend/components/admin/dashboard/DashboardSidebar.tsx b/frontend/components/admin/dashboard/DashboardSidebar.tsx
index d11decc1..4a2c239f 100644
--- a/frontend/components/admin/dashboard/DashboardSidebar.tsx
+++ b/frontend/components/admin/dashboard/DashboardSidebar.tsx
@@ -226,7 +226,7 @@ export function DashboardSidebar() {
onDragStart={handleDragStart}
/>
데이터 위젯
리스트 위젯
야드 관리 3D
- 운송 통계
+ 커스텀 통계 카드
{/* 지도 */}
커스텀 지도 카드
{/* 커스텀 목록 카드 */}
diff --git a/frontend/components/admin/dashboard/ElementConfigModal.tsx b/frontend/components/admin/dashboard/ElementConfigModal.tsx
index 93796257..6aba88db 100644
--- a/frontend/components/admin/dashboard/ElementConfigModal.tsx
+++ b/frontend/components/admin/dashboard/ElementConfigModal.tsx
@@ -52,7 +52,7 @@ export function ElementConfigModal({ element, isOpen, onClose, onSave }: Element
element.subtype === "customer-issues" ||
element.subtype === "driver-management" ||
element.subtype === "work-history" || // 작업 이력 위젯 (쿼리 필요)
- element.subtype === "transport-stats"; // 운송 통계 위젯 (쿼리 필요)
+ element.subtype === "transport-stats"; // 커스텀 통계 카드 위젯 (쿼리 필요)
// 자체 기능 위젯 (DB 연결 불필요, 헤더 설정만 가능)
const isSelfContainedWidget =
diff --git a/frontend/components/admin/dashboard/types.ts b/frontend/components/admin/dashboard/types.ts
index c663e187..139f42c0 100644
--- a/frontend/components/admin/dashboard/types.ts
+++ b/frontend/components/admin/dashboard/types.ts
@@ -38,7 +38,7 @@ export type ElementSubtype =
| "list"
| "yard-management-3d" // 야드 관리 3D 위젯
| "work-history" // 작업 이력 위젯
- | "transport-stats"; // 운송 통계 위젯
+ | "transport-stats"; // 커스텀 통계 카드 위젯
export interface Position {
x: number;
diff --git a/frontend/components/dashboard/DashboardViewer.tsx b/frontend/components/dashboard/DashboardViewer.tsx
index b2588584..c00b2807 100644
--- a/frontend/components/dashboard/DashboardViewer.tsx
+++ b/frontend/components/dashboard/DashboardViewer.tsx
@@ -47,7 +47,7 @@ const WorkHistoryWidget = dynamic(() => import("./widgets/WorkHistoryWidget"), {
ssr: false,
});
-const TransportStatsWidget = dynamic(() => import("./widgets/TransportStatsWidget"), {
+const CustomStatsWidget = dynamic(() => import("./widgets/CustomStatsWidget"), {
ssr: false,
});
@@ -104,7 +104,7 @@ function renderWidget(element: DashboardElement) {
return ;
case "transport-stats":
- return ;
+ return ;
// === 차량 관련 (추가 위젯) ===
case "vehicle-status":
diff --git a/frontend/components/dashboard/widgets/CustomStatsWidget.tsx b/frontend/components/dashboard/widgets/CustomStatsWidget.tsx
new file mode 100644
index 00000000..8a25bc31
--- /dev/null
+++ b/frontend/components/dashboard/widgets/CustomStatsWidget.tsx
@@ -0,0 +1,505 @@
+/**
+ * 커스텀 통계 카드 위젯
+ * - 쿼리 결과에서 숫자 데이터를 자동으로 감지하여 통계 표시
+ * - 합계, 평균, 비율 등 자동 계산
+ * - 처리량, 효율, 가동설비, 재고점유율 등 다양한 통계 지원
+ */
+
+"use client";
+
+import React, { useState, useEffect } from "react";
+import { DashboardElement } from "@/components/admin/dashboard/types";
+
+interface CustomStatsWidgetProps {
+ element?: DashboardElement;
+ refreshInterval?: number;
+}
+
+interface StatItem {
+ label: string;
+ value: number;
+ unit: string;
+ color: string;
+ icon: string;
+}
+
+export default function CustomStatsWidget({ element, refreshInterval = 60000 }: CustomStatsWidgetProps) {
+ const [allStats, setAllStats] = useState([]); // 모든 통계
+ const [stats, setStats] = useState([]); // 표시할 통계
+ const [isLoading, setIsLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [showSettings, setShowSettings] = useState(false);
+ const [selectedStats, setSelectedStats] = useState([]); // 선택된 통계 라벨
+
+ // localStorage 키 생성 (위젯별로 고유하게)
+ const storageKey = `custom-stats-widget-${element?.id || "default"}`;
+
+ // 초기 로드 시 저장된 설정 불러오기
+ React.useEffect(() => {
+ const saved = localStorage.getItem(storageKey);
+ if (saved) {
+ try {
+ const parsed = JSON.parse(saved);
+ setSelectedStats(parsed);
+ } catch (e) {
+ console.error("설정 로드 실패:", e);
+ }
+ }
+ }, [storageKey]);
+
+ // 데이터 로드
+ const loadData = async () => {
+ try {
+ setIsLoading(true);
+ setError(null);
+
+ // 쿼리가 설정되어 있지 않으면 안내 메시지만 표시
+ if (!element?.dataSource?.query) {
+ setError("쿼리를 설정해주세요");
+ setIsLoading(false);
+ return;
+ }
+
+ // 쿼리 실행하여 통계 계산
+ const token = localStorage.getItem("authToken");
+ const response = await fetch("/api/dashboards/execute-query", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${token}`,
+ },
+ body: JSON.stringify({
+ query: element.dataSource.query,
+ connectionType: element.dataSource.connectionType || "current",
+ externalConnectionId: element.dataSource.externalConnectionId,
+ }),
+ });
+
+ if (!response.ok) throw new Error("데이터 로딩 실패");
+
+ const result = await response.json();
+
+ if (!result.success || !result.data?.rows) {
+ throw new Error(result.message || "데이터 로드 실패");
+ }
+
+ const data = result.data.rows || [];
+
+ if (data.length === 0) {
+ setStats([]);
+ return;
+ }
+
+ const firstRow = data[0];
+ const statsItems: StatItem[] = [];
+
+ // 1. 총 건수 (항상 표시)
+ statsItems.push({
+ label: "총 건수",
+ value: data.length,
+ unit: "건",
+ color: "indigo",
+ icon: "📊",
+ });
+
+ // 2. 모든 숫자 컬럼 자동 감지
+ const numericColumns: { [key: string]: { sum: number; avg: number; count: number } } = {};
+
+ Object.keys(firstRow).forEach((key) => {
+ const value = firstRow[key];
+ // 숫자로 변환 가능한 컬럼만 선택 (id, order 같은 식별자 제외)
+ if (
+ value !== null &&
+ !isNaN(parseFloat(value)) &&
+ !key.toLowerCase().includes("id") &&
+ !key.toLowerCase().includes("order") &&
+ key.toLowerCase() !== "id"
+ ) {
+ const validValues = data
+ .map((item: any) => parseFloat(item[key]))
+ .filter((v: number) => !isNaN(v) && v !== 0);
+
+ if (validValues.length > 0) {
+ const sum = validValues.reduce((acc: number, v: number) => acc + v, 0);
+ numericColumns[key] = {
+ sum,
+ avg: sum / validValues.length,
+ count: validValues.length,
+ };
+ }
+ }
+ });
+
+ // 3. 키워드 기반 자동 라벨링 및 단위 설정
+ const columnConfig: {
+ [key: string]: {
+ keywords: string[];
+ unit: string;
+ color: string;
+ icon: string;
+ useAvg?: boolean;
+ koreanLabel?: string; // 한글 라벨
+ };
+ } = {
+ // 무게/중량
+ weight: {
+ keywords: ["weight", "cargo_weight", "total_weight", "tonnage"],
+ unit: "톤",
+ color: "green",
+ icon: "⚖️",
+ koreanLabel: "총 운송량"
+ },
+ // 거리
+ distance: {
+ keywords: ["distance", "total_distance"],
+ unit: "km",
+ color: "blue",
+ icon: "🛣️",
+ koreanLabel: "누적 거리"
+ },
+ // 시간/기간
+ time: {
+ keywords: ["time", "duration", "delivery_time", "delivery_duration"],
+ unit: "분",
+ color: "orange",
+ icon: "⏱️",
+ useAvg: true,
+ koreanLabel: "평균 배송시간"
+ },
+ // 수량/개수
+ quantity: {
+ keywords: ["quantity", "qty", "amount"],
+ unit: "개",
+ color: "purple",
+ icon: "📦",
+ koreanLabel: "총 수량"
+ },
+ // 금액/가격
+ price: {
+ keywords: ["price", "cost", "fee"],
+ unit: "원",
+ color: "yellow",
+ icon: "💰",
+ koreanLabel: "총 금액"
+ },
+ // 비율/퍼센트
+ rate: {
+ keywords: ["rate", "ratio", "percent", "efficiency"],
+ unit: "%",
+ color: "cyan",
+ icon: "📈",
+ useAvg: true,
+ koreanLabel: "평균 비율"
+ },
+ // 처리량
+ throughput: {
+ keywords: ["throughput", "output", "production"],
+ unit: "개",
+ color: "pink",
+ icon: "⚡",
+ koreanLabel: "총 처리량"
+ },
+ // 재고
+ stock: {
+ keywords: ["stock", "inventory"],
+ unit: "개",
+ color: "teal",
+ icon: "📦",
+ koreanLabel: "재고 수량"
+ },
+ // 설비/장비
+ equipment: {
+ keywords: ["equipment", "facility", "machine"],
+ unit: "대",
+ color: "gray",
+ icon: "🏭",
+ koreanLabel: "가동 설비"
+ },
+ };
+
+ // 4. 각 숫자 컬럼을 통계 카드로 변환
+ Object.entries(numericColumns).forEach(([key, stats]) => {
+ let label = key;
+ let unit = "";
+ let color = "gray";
+ let icon = "📊";
+ let useAvg = false;
+ let matchedConfig = null;
+
+ // 키워드 매칭으로 라벨, 단위, 색상 자동 설정
+ for (const [configKey, config] of Object.entries(columnConfig)) {
+ if (config.keywords.some((keyword) => key.toLowerCase().includes(keyword.toLowerCase()))) {
+ unit = config.unit;
+ color = config.color;
+ icon = config.icon;
+ useAvg = config.useAvg || false;
+ matchedConfig = config;
+
+ // 한글 라벨 사용 또는 자동 변환
+ label = config.koreanLabel || key
+ .replace(/_/g, " ")
+ .replace(/([A-Z])/g, " $1")
+ .trim();
+ break;
+ }
+ }
+
+ // 매칭되지 않은 경우 기본 라벨 생성
+ if (!matchedConfig) {
+ label = key
+ .replace(/_/g, " ")
+ .replace(/([A-Z])/g, " $1")
+ .trim()
+ .split(" ")
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
+ .join(" ");
+ }
+
+ // 합계 또는 평균 선택
+ const value = useAvg ? stats.avg : stats.sum;
+
+ statsItems.push({
+ label,
+ value,
+ unit,
+ color,
+ icon,
+ });
+ });
+
+ // 5. Boolean 컬럼 비율 계산 (정시도착률, 성공률 등)
+ const booleanMapping: { [key: string]: string } = {
+ is_on_time: "정시 도착률",
+ on_time: "정시 도착률",
+ success: "성공률",
+ completed: "완료율",
+ delivered: "배송 완료율",
+ approved: "승인률",
+ };
+
+ Object.keys(firstRow).forEach((key) => {
+ const lowerKey = key.toLowerCase();
+ const matchedKey = Object.keys(booleanMapping).find((k) => lowerKey.includes(k));
+
+ if (matchedKey) {
+ const validItems = data.filter((item: any) => item[key] !== null && item[key] !== undefined);
+
+ if (validItems.length > 0) {
+ const trueCount = validItems.filter((item: any) => {
+ const val = item[key];
+ return val === true || val === "true" || val === 1 || val === "1" || val === "Y";
+ }).length;
+
+ const rate = (trueCount / validItems.length) * 100;
+
+ statsItems.push({
+ label: booleanMapping[matchedKey],
+ value: rate,
+ unit: "%",
+ color: "purple",
+ icon: "✅",
+ });
+ }
+ }
+ });
+
+ setAllStats(statsItems);
+
+ // 초기에는 모든 통계 표시 (최대 6개)
+ if (selectedStats.length === 0) {
+ setStats(statsItems.slice(0, 6));
+ setSelectedStats(statsItems.slice(0, 6).map((s) => s.label));
+ } else {
+ // 선택된 통계만 표시
+ const filtered = statsItems.filter((s) => selectedStats.includes(s.label));
+ setStats(filtered);
+ }
+ } catch (err) {
+ console.error("통계 로드 실패:", err);
+ setError(err instanceof Error ? err.message : "데이터를 불러올 수 없습니다");
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ loadData();
+ const interval = setInterval(loadData, refreshInterval);
+ return () => clearInterval(interval);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [refreshInterval, element?.dataSource]);
+
+ // 색상 매핑
+ const getColorClasses = (color: string) => {
+ const colorMap: { [key: string]: { bg: string; text: string } } = {
+ indigo: { bg: "bg-indigo-50", text: "text-indigo-600" },
+ green: { bg: "bg-green-50", text: "text-green-600" },
+ blue: { bg: "bg-blue-50", text: "text-blue-600" },
+ purple: { bg: "bg-purple-50", text: "text-purple-600" },
+ orange: { bg: "bg-orange-50", text: "text-orange-600" },
+ yellow: { bg: "bg-yellow-50", text: "text-yellow-600" },
+ cyan: { bg: "bg-cyan-50", text: "text-cyan-600" },
+ pink: { bg: "bg-pink-50", text: "text-pink-600" },
+ teal: { bg: "bg-teal-50", text: "text-teal-600" },
+ gray: { bg: "bg-gray-50", text: "text-gray-600" },
+ };
+ return colorMap[color] || colorMap.gray;
+ };
+
+ if (isLoading && stats.length === 0) {
+ return (
+
+ );
+ }
+
+ if (error) {
+ return (
+
+
+
⚠️
+
{error}
+ {!element?.dataSource?.query && (
+
톱니바퀴 아이콘을 클릭하여 쿼리를 설정하세요
+ )}
+
+
+
+ );
+ }
+
+ if (stats.length === 0) {
+ return (
+
+
+
📊
+
데이터 없음
+
쿼리를 실행하여 통계를 확인하세요
+
+
+ );
+ }
+
+ const handleToggleStat = (label: string) => {
+ setSelectedStats((prev) => {
+ if (prev.includes(label)) {
+ return prev.filter((l) => l !== label);
+ } else {
+ return [...prev, label];
+ }
+ });
+ };
+
+ const handleApplySettings = () => {
+ const filtered = allStats.filter((s) => selectedStats.includes(s.label));
+ setStats(filtered);
+ setShowSettings(false);
+
+ // localStorage에 설정 저장
+ localStorage.setItem(storageKey, JSON.stringify(selectedStats));
+ };
+
+ return (
+
+ {/* 헤더 영역 */}
+
+
+ 📊
+ 커스텀 통계
+ ({stats.length}개 표시 중)
+
+
+
+
+ {/* 통계 카드 */}
+
+
+ {stats.map((stat, index) => {
+ const colors = getColorClasses(stat.color);
+ return (
+
+
+ {stat.icon} {stat.label}
+
+
+ {stat.value.toFixed(stat.unit === "%" || stat.unit === "분" ? 1 : 0).toLocaleString()}
+ {stat.unit}
+
+
+ );
+ })}
+
+
+
+ {/* 설정 모달 */}
+ {showSettings && (
+
+
+
+
표시할 통계 선택
+
+
+
+
+ 표시하고 싶은 통계를 선택하세요 (최대 제한 없음)
+
+
+
+ {allStats.map((stat, index) => (
+
+ ))}
+
+
+
+
+
+
+
+
+ )}
+
+ );
+}
diff --git a/frontend/components/dashboard/widgets/TodoWidget.tsx b/frontend/components/dashboard/widgets/TodoWidget.tsx
index dd4652d5..6ab85294 100644
--- a/frontend/components/dashboard/widgets/TodoWidget.tsx
+++ b/frontend/components/dashboard/widgets/TodoWidget.tsx
@@ -38,6 +38,7 @@ export default function TodoWidget({ element }: TodoWidgetProps) {
const { selectedDate } = useDashboard();
const [todos, setTodos] = useState([]);
+ const [internalTodos, setInternalTodos] = useState([]); // 내장 API 투두
const [stats, setStats] = useState(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) {
{/* 제목 - 항상 표시 */}
-
{element?.customTitle || "To-Do / 긴급 지시"}
- {selectedDate && (
-
-
-
{formatSelectedDate()} 할일
+
+
+
{element?.customTitle || "To-Do / 긴급 지시"}
+ {selectedDate && (
+
+
+ {formatSelectedDate()} 할일
+
+ )}
- )}
+ {/* 추가 버튼 - 항상 표시 */}
+
+
- {/* 헤더 (추가 버튼, 통계, 필터) - showHeader가 false일 때만 숨김 */}
+ {/* 헤더 (통계, 필터) - showHeader가 false일 때만 숨김 */}
{element?.showHeader !== false && (
-
-
-
-
{/* 통계 */}
{stats && (
@@ -390,19 +397,21 @@ export default function TodoWidget({ element }: TodoWidgetProps) {
{/* 추가 폼 */}
{showAddForm && (
-