From 86135dcf1095f1ed72228d64c935e52cd6c5733f Mon Sep 17 00:00:00 2001 From: leeheejin Date: Mon, 20 Oct 2025 17:42:35 +0900 Subject: [PATCH] =?UTF-8?q?=EB=8C=80=EC=8B=9C=EB=B3=B4=EB=93=9C=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=EB=94=94=EB=B2=A8=EB=A1=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend-node/data/todos/todos.json | 7 +- .../admin/dashboard/CanvasElement.tsx | 2 +- .../admin/dashboard/DashboardDesigner.tsx | 14 +- .../admin/dashboard/DashboardSidebar.tsx | 5 +- .../admin/dashboard/DashboardTopMenu.tsx | 2 +- .../widgets/YardManagement3DWidget.tsx | 2 +- .../components/dashboard/DashboardViewer.tsx | 22 +- .../dashboard/widgets/CustomStatsWidget.tsx | 466 ++++++++++++++---- .../dashboard/widgets/RiskAlertWidget.tsx | 98 ++-- 9 files changed, 447 insertions(+), 171 deletions(-) diff --git a/backend-node/data/todos/todos.json b/backend-node/data/todos/todos.json index 653d5636..e10d42af 100644 --- a/backend-node/data/todos/todos.json +++ b/backend-node/data/todos/todos.json @@ -4,13 +4,14 @@ "title": "ㅁㄴㅇㄹ", "description": "ㅁㄴㅇㄹ", "priority": "normal", - "status": "pending", + "status": "completed", "assignedTo": "", "dueDate": "2025-10-20T18:17", "createdAt": "2025-10-20T06:15:49.610Z", - "updatedAt": "2025-10-20T06:15:49.610Z", + "updatedAt": "2025-10-20T07:36:06.370Z", "isUrgent": false, - "order": 0 + "order": 0, + "completedAt": "2025-10-20T07:36:06.370Z" }, { "id": "334be17c-7776-47e8-89ec-4b57c4a34bcd", diff --git a/frontend/components/admin/dashboard/CanvasElement.tsx b/frontend/components/admin/dashboard/CanvasElement.tsx index db58207e..7b5453f9 100644 --- a/frontend/components/admin/dashboard/CanvasElement.tsx +++ b/frontend/components/admin/dashboard/CanvasElement.tsx @@ -739,7 +739,7 @@ export function CanvasElement({ isEditMode={true} config={element.yardConfig} onConfigChange={(newConfig) => { - console.log("🏗️ 야드 설정 업데이트:", { elementId: element.id, newConfig }); + // console.log("🏗️ 야드 설정 업데이트:", { elementId: element.id, newConfig }); onUpdate(element.id, { yardConfig: newConfig }); }} /> diff --git a/frontend/components/admin/dashboard/DashboardDesigner.tsx b/frontend/components/admin/dashboard/DashboardDesigner.tsx index 3e01cff1..7391f042 100644 --- a/frontend/components/admin/dashboard/DashboardDesigner.tsx +++ b/frontend/components/admin/dashboard/DashboardDesigner.tsx @@ -335,13 +335,13 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D const elementsData = elements.map((el) => { // 야드 위젯인 경우 설정 로그 출력 - if (el.subtype === "yard-management-3d") { - console.log("💾 야드 위젯 저장:", { - id: el.id, - yardConfig: el.yardConfig, - hasLayoutId: !!el.yardConfig?.layoutId, - }); - } + // if (el.subtype === "yard-management-3d") { + // console.log("💾 야드 위젯 저장:", { + // id: el.id, + // yardConfig: el.yardConfig, + // hasLayoutId: !!el.yardConfig?.layoutId, + // }); + // } return { id: el.id, type: el.type, diff --git a/frontend/components/admin/dashboard/DashboardSidebar.tsx b/frontend/components/admin/dashboard/DashboardSidebar.tsx index 4a2c239f..62c50fdc 100644 --- a/frontend/components/admin/dashboard/DashboardSidebar.tsx +++ b/frontend/components/admin/dashboard/DashboardSidebar.tsx @@ -200,12 +200,13 @@ export function DashboardSidebar() { subtype="todo" onDragStart={handleDragStart} /> - + /> */} {/* 정비 일정 관리 위젯 제거 - 커스텀 목록 카드로 대체 가능 */} 달력 시계 할 일 - 예약 알림 + {/* 예약 알림 */} 정비 일정 문서 리스크 알림 diff --git a/frontend/components/admin/dashboard/widgets/YardManagement3DWidget.tsx b/frontend/components/admin/dashboard/widgets/YardManagement3DWidget.tsx index f3e5a1be..80839961 100644 --- a/frontend/components/admin/dashboard/widgets/YardManagement3DWidget.tsx +++ b/frontend/components/admin/dashboard/widgets/YardManagement3DWidget.tsx @@ -60,7 +60,7 @@ export default function YardManagement3DWidget({ // 레이아웃 목록이 로드되었고, 설정이 없으면 첫 번째 레이아웃 자동 선택 useEffect(() => { if (isEditMode && layouts.length > 0 && !config?.layoutId && onConfigChange) { - console.log("🔧 첫 번째 야드 레이아웃 자동 선택:", layouts[0]); + // console.log("🔧 첫 번째 야드 레이아웃 자동 선택:", layouts[0]); onConfigChange({ layoutId: layouts[0].id, layoutName: layouts[0].name, diff --git a/frontend/components/dashboard/DashboardViewer.tsx b/frontend/components/dashboard/DashboardViewer.tsx index 287286cf..7406a8e0 100644 --- a/frontend/components/dashboard/DashboardViewer.tsx +++ b/frontend/components/dashboard/DashboardViewer.tsx @@ -90,20 +90,26 @@ function renderWidget(element: DashboardElement) { return ; case "yard-management-3d": - console.log("🏗️ 야드관리 위젯 렌더링:", { - elementId: element.id, - yardConfig: element.yardConfig, - yardConfigType: typeof element.yardConfig, - hasLayoutId: !!element.yardConfig?.layoutId, - layoutId: element.yardConfig?.layoutId, - layoutName: element.yardConfig?.layoutName, - }); + // console.log("🏗️ 야드관리 위젯 렌더링:", { + // elementId: element.id, + // yardConfig: element.yardConfig, + // yardConfigType: typeof element.yardConfig, + // hasLayoutId: !!element.yardConfig?.layoutId, + // layoutId: element.yardConfig?.layoutId, + // layoutName: element.yardConfig?.layoutName, + // }); return ; case "work-history": return ; case "transport-stats": + // console.log("📊 [DashboardViewer] CustomStatsWidget 렌더링:", { + // elementId: element.id, + // hasDataSource: !!element.dataSource, + // query: element.dataSource?.query?.substring(0, 50) + "...", + // dataSourceType: element.dataSource?.type, + // }); return ; // === 차량 관련 (추가 위젯) === diff --git a/frontend/components/dashboard/widgets/CustomStatsWidget.tsx b/frontend/components/dashboard/widgets/CustomStatsWidget.tsx index 8a25bc31..85f3cde0 100644 --- a/frontend/components/dashboard/widgets/CustomStatsWidget.tsx +++ b/frontend/components/dashboard/widgets/CustomStatsWidget.tsx @@ -24,31 +24,46 @@ interface StatItem { } export default function CustomStatsWidget({ element, refreshInterval = 60000 }: CustomStatsWidgetProps) { + // console.log("🚀 CustomStatsWidget 마운트:", { + // elementId: element?.id, + // query: element?.dataSource?.query?.substring(0, 50) + "...", + // hasDataSource: !!element?.dataSource, + // }); + 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([]); // 선택된 통계 라벨 + const selectedStatsRef = React.useRef([]); // 현재 선택된 통계를 추적하는 ref + const isInitializedRef = React.useRef(false); // 초기화 여부 추적 + const lastQueryRef = React.useRef(""); // 마지막 쿼리 추적 - // localStorage 키 생성 (위젯별로 고유하게) - const storageKey = `custom-stats-widget-${element?.id || "default"}`; + // localStorage 키 생성 (쿼리 기반으로 고유하게 - 편집/보기 모드 공유) + const queryHash = element?.dataSource?.query + ? btoa(element.dataSource.query) // 전체 쿼리를 base64로 인코딩 + : "default"; + const storageKey = `custom-stats-widget-${queryHash}`; + + // console.log("🔑 storageKey:", storageKey, "(쿼리:", element?.dataSource?.query?.substring(0, 30) + "...)"); - // 초기 로드 시 저장된 설정 불러오기 + // 쿼리가 변경되면 초기화 상태 리셋 React.useEffect(() => { - const saved = localStorage.getItem(storageKey); - if (saved) { - try { - const parsed = JSON.parse(saved); - setSelectedStats(parsed); - } catch (e) { - console.error("설정 로드 실패:", e); - } + const currentQuery = element?.dataSource?.query || ""; + if (currentQuery !== lastQueryRef.current) { + isInitializedRef.current = false; + lastQueryRef.current = currentQuery; } - }, [storageKey]); + }, [element?.dataSource?.query]); + + // selectedStats 변경 시 ref 업데이트 + React.useEffect(() => { + selectedStatsRef.current = selectedStats; + }, [selectedStats]); // 데이터 로드 - const loadData = async () => { + const loadData = React.useCallback(async () => { try { setIsLoading(true); setError(null); @@ -130,90 +145,204 @@ export default function CustomStatsWidget({ element, refreshInterval = 60000 }: } }); - // 3. 키워드 기반 자동 라벨링 및 단위 설정 + // 3. 컬럼명 한글 번역 매핑 + const columnNameTranslation: { [key: string]: string } = { + // 일반 + "id": "ID", + "name": "이름", + "title": "제목", + "description": "설명", + "status": "상태", + "type": "유형", + "category": "카테고리", + "date": "날짜", + "time": "시간", + "created_at": "생성일", + "updated_at": "수정일", + "deleted_at": "삭제일", + + // 물류/운송 + "tracking_number": "운송장 번호", + "customer": "고객", + "origin": "출발지", + "destination": "목적지", + "estimated_delivery": "예상 도착", + "actual_delivery": "실제 도착", + "delay_reason": "지연 사유", + "priority": "우선순위", + "cargo_weight": "화물 중량", + "total_weight": "총 중량", + "weight": "중량", + "distance": "거리", + "total_distance": "총 거리", + "delivery_time": "배송 시간", + "delivery_duration": "배송 소요시간", + "is_on_time": "정시 도착 여부", + "on_time": "정시", + + // 수량/금액 + "quantity": "수량", + "qty": "수량", + "amount": "금액", + "price": "가격", + "cost": "비용", + "fee": "수수료", + "total": "합계", + "sum": "총합", + + // 비율/효율 + "rate": "비율", + "ratio": "비율", + "percent": "퍼센트", + "percentage": "백분율", + "efficiency": "효율", + + // 생산/처리 + "throughput": "처리량", + "output": "산출량", + "production": "생산량", + "volume": "용량", + + // 재고/설비 + "stock": "재고", + "inventory": "재고", + "equipment": "설비", + "facility": "시설", + "machine": "기계", + + // 평가 + "score": "점수", + "rating": "평점", + "point": "점수", + "grade": "등급", + + // 기타 + "temperature": "온도", + "temp": "온도", + "speed": "속도", + "velocity": "속도", + "count": "개수", + "number": "번호", + }; + + // 4. 키워드 기반 자동 라벨링 및 단위 설정 const columnConfig: { [key: string]: { keywords: string[]; unit: string; color: string; icon: string; - useAvg?: boolean; + aggregation: "sum" | "avg" | "max" | "min"; // 집계 방식 koreanLabel?: string; // 한글 라벨 }; } = { - // 무게/중량 + // 무게/중량 - 합계 weight: { - keywords: ["weight", "cargo_weight", "total_weight", "tonnage"], + keywords: ["weight", "cargo_weight", "total_weight", "tonnage", "ton"], unit: "톤", color: "green", icon: "⚖️", + aggregation: "sum", koreanLabel: "총 운송량" }, - // 거리 + // 거리 - 합계 distance: { - keywords: ["distance", "total_distance"], + keywords: ["distance", "total_distance", "km", "kilometer"], unit: "km", color: "blue", icon: "🛣️", + aggregation: "sum", koreanLabel: "누적 거리" }, - // 시간/기간 + // 시간/기간 - 평균 time: { - keywords: ["time", "duration", "delivery_time", "delivery_duration"], + keywords: ["time", "duration", "delivery_time", "delivery_duration", "hour", "minute"], unit: "분", color: "orange", icon: "⏱️", - useAvg: true, + aggregation: "avg", koreanLabel: "평균 배송시간" }, - // 수량/개수 + // 수량/개수 - 합계 quantity: { - keywords: ["quantity", "qty", "amount"], + keywords: ["quantity", "qty", "count", "number"], unit: "개", color: "purple", icon: "📦", + aggregation: "sum", koreanLabel: "총 수량" }, - // 금액/가격 - price: { - keywords: ["price", "cost", "fee"], + // 금액/가격 - 합계 + amount: { + keywords: ["amount", "price", "cost", "fee", "total", "sum"], unit: "원", color: "yellow", icon: "💰", + aggregation: "sum", koreanLabel: "총 금액" }, - // 비율/퍼센트 + // 비율/퍼센트 - 평균 rate: { - keywords: ["rate", "ratio", "percent", "efficiency"], + keywords: ["rate", "ratio", "percent", "efficiency", "%"], unit: "%", color: "cyan", icon: "📈", - useAvg: true, + aggregation: "avg", koreanLabel: "평균 비율" }, - // 처리량 + // 처리량 - 합계 throughput: { - keywords: ["throughput", "output", "production"], + keywords: ["throughput", "output", "production", "volume"], unit: "개", color: "pink", icon: "⚡", + aggregation: "sum", koreanLabel: "총 처리량" }, - // 재고 + // 재고 - 평균 (현재 재고는 평균이 의미있음) stock: { keywords: ["stock", "inventory"], unit: "개", color: "teal", icon: "📦", - koreanLabel: "재고 수량" + aggregation: "avg", + koreanLabel: "평균 재고" }, - // 설비/장비 + // 설비/장비 - 평균 equipment: { keywords: ["equipment", "facility", "machine"], unit: "대", color: "gray", icon: "🏭", - koreanLabel: "가동 설비" + aggregation: "avg", + koreanLabel: "평균 가동 설비" + }, + // 점수/평점 - 평균 + score: { + keywords: ["score", "rating", "point", "grade"], + unit: "점", + color: "indigo", + icon: "⭐", + aggregation: "avg", + koreanLabel: "평균 점수" + }, + // 온도 - 평균 + temperature: { + keywords: ["temp", "temperature", "degree"], + unit: "°C", + color: "red", + icon: "🌡️", + aggregation: "avg", + koreanLabel: "평균 온도" + }, + // 속도 - 평균 + speed: { + keywords: ["speed", "velocity"], + unit: "km/h", + color: "blue", + icon: "🚀", + aggregation: "avg", + koreanLabel: "평균 속도" }, }; @@ -223,40 +352,102 @@ export default function CustomStatsWidget({ element, refreshInterval = 60000 }: let unit = ""; let color = "gray"; let icon = "📊"; - let useAvg = false; + let aggregation: "sum" | "avg" | "max" | "min" = "sum"; // 기본값은 합계 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; + aggregation = config.aggregation; matchedConfig = config; // 한글 라벨 사용 또는 자동 변환 - label = config.koreanLabel || key - .replace(/_/g, " ") - .replace(/([A-Z])/g, " $1") - .trim(); + if (config.koreanLabel) { + label = config.koreanLabel; + } else { + // 집계 방식에 따라 접두어 추가 + const prefix = aggregation === "avg" ? "평균 " : aggregation === "sum" ? "총 " : ""; + label = prefix + 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 translatedName = columnNameTranslation[key.toLowerCase()]; + + if (translatedName) { + // 번역된 이름이 있으면 사용 + label = translatedName; + } else { + // 컬럼명에 avg, average, mean이 포함되면 평균으로 간주 + if (key.toLowerCase().includes("avg") || + key.toLowerCase().includes("average") || + key.toLowerCase().includes("mean")) { + aggregation = "avg"; + + // 언더스코어로 분리된 각 단어 번역 시도 + const cleanKey = key.replace(/avg|average|mean/gi, "").replace(/_/g, " ").trim(); + const words = cleanKey.split(/[_\s]+/); + const translatedWords = words.map(word => + columnNameTranslation[word.toLowerCase()] || word + ); + label = "평균 " + translatedWords.join(" "); + } + // total, sum이 포함되면 합계로 간주 + else if (key.toLowerCase().includes("total") || key.toLowerCase().includes("sum")) { + aggregation = "sum"; + + // 언더스코어로 분리된 각 단어 번역 시도 + const cleanKey = key.replace(/total|sum/gi, "").replace(/_/g, " ").trim(); + const words = cleanKey.split(/[_\s]+/); + const translatedWords = words.map(word => + columnNameTranslation[word.toLowerCase()] || word + ); + label = "총 " + translatedWords.join(" "); + } + // 기본값 - 각 단어별로 번역 시도 + else { + const words = key.split(/[_\s]+/); + const translatedWords = words.map(word => { + const translated = columnNameTranslation[word.toLowerCase()]; + if (translated) { + return translated; + } + // 번역이 없으면 첫 글자 대문자로 + return word.charAt(0).toUpperCase() + word.slice(1); + }); + label = translatedWords.join(" "); + } + } } - // 합계 또는 평균 선택 - const value = useAvg ? stats.avg : stats.sum; + // 집계 방식에 따라 값 선택 + let value: number; + switch (aggregation) { + case "avg": + value = stats.avg; + break; + case "sum": + value = stats.sum; + break; + case "max": + value = Math.max(...data.map((item: any) => parseFloat(item[key]) || 0)); + break; + case "min": + value = Math.min(...data.map((item: any) => parseFloat(item[key]) || 0)); + break; + default: + value = stats.sum; + } statsItems.push({ label, @@ -277,11 +468,20 @@ export default function CustomStatsWidget({ element, refreshInterval = 60000 }: approved: "승인률", }; + const addedBooleanLabels = new Set(); // 중복 방지 + Object.keys(firstRow).forEach((key) => { const lowerKey = key.toLowerCase(); const matchedKey = Object.keys(booleanMapping).find((k) => lowerKey.includes(k)); if (matchedKey) { + const label = booleanMapping[matchedKey]; + + // 이미 추가된 라벨이면 스킵 + if (addedBooleanLabels.has(label)) { + return; + } + const validItems = data.filter((item: any) => item[key] !== null && item[key] !== undefined); if (validItems.length > 0) { @@ -293,34 +493,95 @@ export default function CustomStatsWidget({ element, refreshInterval = 60000 }: const rate = (trueCount / validItems.length) * 100; statsItems.push({ - label: booleanMapping[matchedKey], + label, value: rate, unit: "%", color: "purple", icon: "✅", }); + + addedBooleanLabels.add(label); } } }); + // console.log("📊 생성된 통계 항목:", statsItems.map(s => s.label)); setAllStats(statsItems); - // 초기에는 모든 통계 표시 (최대 6개) - if (selectedStats.length === 0) { - setStats(statsItems.slice(0, 6)); - setSelectedStats(statsItems.slice(0, 6).map((s) => s.label)); + // 초기화가 아직 안됐으면 localStorage에서 설정 불러오기 + if (!isInitializedRef.current) { + const saved = localStorage.getItem(storageKey); + // console.log("💾 저장된 설정:", saved); + + if (saved) { + try { + const savedLabels = JSON.parse(saved); + // console.log("✅ 저장된 라벨:", savedLabels); + + const filtered = statsItems.filter((s) => savedLabels.includes(s.label)); + // console.log("🔍 필터링된 통계:", filtered.map(s => s.label)); + // console.log(`📊 일치율: ${filtered.length}/${savedLabels.length} (${Math.round(filtered.length / savedLabels.length * 100)}%)`); + + // 50% 이상 일치하면 저장된 설정 사용 + const matchRate = filtered.length / savedLabels.length; + if (matchRate >= 0.5 && filtered.length > 0) { + setStats(filtered); + // 실제 표시되는 라벨로 업데이트 + const actualLabels = filtered.map(s => s.label); + setSelectedStats(actualLabels); + selectedStatsRef.current = actualLabels; + // localStorage도 업데이트하여 다음에는 정확히 일치하도록 + localStorage.setItem(storageKey, JSON.stringify(actualLabels)); + // console.log(`✅ ${filtered.length}개 통계 표시 (저장된 설정 기반)`); + } else { + // 일치율이 낮으면 처음 6개 표시하고 localStorage 업데이트 + // console.warn(`⚠️ 일치율 ${Math.round(matchRate * 100)}% - 기본값 사용`); + const defaultLabels = statsItems.slice(0, 6).map((s) => s.label); + setStats(statsItems.slice(0, 6)); + setSelectedStats(defaultLabels); + selectedStatsRef.current = defaultLabels; + localStorage.setItem(storageKey, JSON.stringify(defaultLabels)); + } + } catch (e) { + // console.error("❌ 설정 파싱 실패:", e); + const defaultLabels = statsItems.slice(0, 6).map((s) => s.label); + setStats(statsItems.slice(0, 6)); + setSelectedStats(defaultLabels); + selectedStatsRef.current = defaultLabels; + } + } else { + // 저장된 설정이 없으면 처음 6개 표시 + // console.log("🆕 저장된 설정 없음. 기본값 사용"); + const defaultLabels = statsItems.slice(0, 6).map((s) => s.label); + setStats(statsItems.slice(0, 6)); + setSelectedStats(defaultLabels); + selectedStatsRef.current = defaultLabels; + } + isInitializedRef.current = true; } else { - // 선택된 통계만 표시 - const filtered = statsItems.filter((s) => selectedStats.includes(s.label)); - setStats(filtered); + // 이미 초기화됐으면 현재 선택된 통계 유지 + const currentSelected = selectedStatsRef.current; + // console.log("🔄 현재 선택된 통계:", currentSelected); + + if (currentSelected.length > 0) { + const filtered = statsItems.filter((s) => currentSelected.includes(s.label)); + // console.log("🔍 필터링 결과:", filtered.map(s => s.label)); + + if (filtered.length > 0) { + setStats(filtered); + } else { + // console.warn("⚠️ 선택된 항목과 일치하는 통계가 없음"); + setStats(statsItems.slice(0, 6)); + } + } } } catch (err) { - console.error("통계 로드 실패:", err); + // console.error("통계 로드 실패:", err); setError(err instanceof Error ? err.message : "데이터를 불러올 수 없습니다"); } finally { setIsLoading(false); } - }; + }, [element?.dataSource, storageKey]); useEffect(() => { loadData(); @@ -391,23 +652,35 @@ export default function CustomStatsWidget({ element, refreshInterval = 60000 }: const handleToggleStat = (label: string) => { setSelectedStats((prev) => { - if (prev.includes(label)) { - return prev.filter((l) => l !== label); - } else { - return [...prev, label]; - } + const newStats = prev.includes(label) + ? prev.filter((l) => l !== label) + : [...prev, label]; + // console.log("🔘 토글:", label, "→", newStats.length + "개 선택"); + return newStats; }); }; const handleApplySettings = () => { + // console.log("💾 설정 적용:", selectedStats); + // console.log("📊 전체 통계:", allStats.map(s => s.label)); + const filtered = allStats.filter((s) => selectedStats.includes(s.label)); + // console.log("✅ 필터링 결과:", filtered.map(s => s.label)); + setStats(filtered); + selectedStatsRef.current = selectedStats; // ref도 업데이트 setShowSettings(false); // localStorage에 설정 저장 localStorage.setItem(storageKey, JSON.stringify(selectedStats)); + // console.log("💾 localStorage 저장 완료:", selectedStats.length + "개"); }; + // 렌더링 시 상태 로그 + // console.log("🎨 렌더링 - stats:", stats.map(s => s.label)); + // console.log("🎨 렌더링 - selectedStats:", selectedStats); + // console.log("🎨 렌더링 - allStats:", allStats.map(s => s.label)); + return (
{/* 헤더 영역 */} @@ -418,7 +691,13 @@ export default function CustomStatsWidget({ element, refreshInterval = 60000 }: ({stats.length}개 표시 중)
{/* 통계 카드 */} -
+
setFilter(filter === "accident" ? "all" : "accident")} > -
교통사고
-
{stats.accident}건
+
교통사고
+
{stats.accident}건
setFilter(filter === "weather" ? "all" : "weather")} > -
날씨특보
-
{stats.weather}건
+
날씨특보
+
{stats.weather}건
setFilter(filter === "construction" ? "all" : "construction")} > -
도로공사
-
{stats.construction}건
+
도로공사
+
{stats.construction}건
{/* 필터 상태 표시 */} {filter !== "all" && (
- + {getAlertTypeName(filter)} 필터 적용 중 @@ -236,44 +214,44 @@ export default function RiskAlertWidget({ element }: RiskAlertWidgetProps) {
{filteredAlerts.length === 0 ? ( -
알림이 없습니다
+
알림이 없습니다
) : ( filteredAlerts.map((alert) => (
{getAlertIcon(alert.type)}
-
-

{alert.title}

+
+

{alert.title}

{newAlertIds.has(alert.id) && ( - + NEW )} - + {alert.severity === "high" ? "긴급" : alert.severity === "medium" ? "주의" : "정보"}
-

{alert.location}

-

{alert.description}

+

{alert.location}

+

{alert.description}

-
{formatTime(alert.timestamp)}
+
{formatTime(alert.timestamp)}
)) )}
{/* 안내 메시지 */} -
+
💡 1분마다 자동으로 업데이트됩니다