Compare commits

..

No commits in common. "7e38f82d0cd3eac07ec1e470bc46fcef4d52b00b" and "04fea9a526a45281f3946488f8cfa974bdc7a21f" have entirely different histories.

15 changed files with 176 additions and 231 deletions

View File

@ -220,7 +220,13 @@ export function DashboardSidebar() {
subtype="booking-alert" subtype="booking-alert"
onDragStart={handleDragStart} onDragStart={handleDragStart}
/> />
{/* 정비 일정 관리 위젯 제거 - 커스텀 목록 카드로 대체 가능 */} <DraggableItem
icon="🔧"
title="정비 일정 관리"
type="widget"
subtype="maintenance"
onDragStart={handleDragStart}
/>
<DraggableItem <DraggableItem
icon="📂" icon="📂"
title="문서 다운로드" title="문서 다운로드"

View File

@ -31,7 +31,6 @@ export function ElementConfigModal({ element, isOpen, onClose, onSave }: Element
const [chartConfig, setChartConfig] = useState<ChartConfig>(element.chartConfig || {}); const [chartConfig, setChartConfig] = useState<ChartConfig>(element.chartConfig || {});
const [queryResult, setQueryResult] = useState<QueryResult | null>(null); const [queryResult, setQueryResult] = useState<QueryResult | null>(null);
const [currentStep, setCurrentStep] = useState<1 | 2>(1); const [currentStep, setCurrentStep] = useState<1 | 2>(1);
const [customTitle, setCustomTitle] = useState<string>(element.customTitle || "");
// 차트 설정이 필요 없는 위젯 (쿼리/API만 필요) // 차트 설정이 필요 없는 위젯 (쿼리/API만 필요)
const isSimpleWidget = const isSimpleWidget =
@ -57,7 +56,6 @@ export function ElementConfigModal({ element, isOpen, onClose, onSave }: Element
setChartConfig(element.chartConfig || {}); setChartConfig(element.chartConfig || {});
setQueryResult(null); setQueryResult(null);
setCurrentStep(1); setCurrentStep(1);
setCustomTitle(element.customTitle || "");
} }
}, [isOpen, element]); }, [isOpen, element]);
@ -121,14 +119,13 @@ export function ElementConfigModal({ element, isOpen, onClose, onSave }: Element
...element, ...element,
dataSource, dataSource,
chartConfig, chartConfig,
customTitle: customTitle.trim() || undefined, // 빈 문자열이면 undefined
}; };
console.log(" 저장할 element:", updatedElement); console.log(" 저장할 element:", updatedElement);
onSave(updatedElement); onSave(updatedElement);
onClose(); onClose();
}, [element, dataSource, chartConfig, customTitle, onSave, onClose]); }, [element, dataSource, chartConfig, onSave, onClose]);
// 모달이 열려있지 않으면 렌더링하지 않음 // 모달이 열려있지 않으면 렌더링하지 않음
if (!isOpen) return null; if (!isOpen) return null;
@ -150,32 +147,28 @@ export function ElementConfigModal({ element, isOpen, onClose, onSave }: Element
chartConfig.yAxis && chartConfig.yAxis &&
(typeof chartConfig.yAxis === "string" || (Array.isArray(chartConfig.yAxis) && chartConfig.yAxis.length > 0)); (typeof chartConfig.yAxis === "string" || (Array.isArray(chartConfig.yAxis) && chartConfig.yAxis.length > 0));
// customTitle이 변경되었는지 확인 const canSave = isSimpleWidget
const isTitleChanged = customTitle.trim() !== (element.customTitle || ""); ? // 간단한 위젯: 2단계에서 쿼리 테스트 후 저장 가능
currentStep === 2 && queryResult && queryResult.rows.length > 0
const canSave = isTitleChanged || // 제목만 변경해도 저장 가능 : isMapWidget
(isSimpleWidget ? // 지도 위젯: 위도/경도 매핑 필요
? // 간단한 위젯: 2단계에서 쿼리 테스트 후 저장 가능 currentStep === 2 &&
currentStep === 2 && queryResult && queryResult.rows.length > 0 queryResult &&
: isMapWidget queryResult.rows.length > 0 &&
? // 지도 위젯: 위도/경도 매핑 필요 chartConfig.latitudeColumn &&
currentStep === 2 && chartConfig.longitudeColumn
queryResult && : // 차트: 기존 로직 (2단계에서 차트 설정 필요)
queryResult.rows.length > 0 && currentStep === 2 &&
chartConfig.latitudeColumn && queryResult &&
chartConfig.longitudeColumn queryResult.rows.length > 0 &&
: // 차트: 기존 로직 (2단계에서 차트 설정 필요) chartConfig.xAxis &&
currentStep === 2 && (isPieChart || isApiSource
queryResult && ? // 파이/도넛 차트 또는 REST API
queryResult.rows.length > 0 && chartConfig.aggregation === "count"
chartConfig.xAxis && ? true // count는 Y축 없어도 됨
(isPieChart || isApiSource : hasYAxis // 다른 집계(sum, avg, max, min) 또는 집계 없음 → Y축 필수
? // 파이/도넛 차트 또는 REST API : // 일반 차트 (DB): Y축 필수
chartConfig.aggregation === "count" hasYAxis);
? true // count는 Y축 없어도 됨
: hasYAxis // 다른 집계(sum, avg, max, min) 또는 집계 없음 → Y축 필수
: // 일반 차트 (DB): Y축 필수
hasYAxis));
return ( return (
<div className="fixed inset-0 z-[9999] flex items-center justify-center bg-black/50 backdrop-blur-sm"> <div className="fixed inset-0 z-[9999] flex items-center justify-center bg-black/50 backdrop-blur-sm">
@ -185,39 +178,20 @@ export function ElementConfigModal({ element, isOpen, onClose, onSave }: Element
}`} }`}
> >
{/* 모달 헤더 */} {/* 모달 헤더 */}
<div className="border-b p-6"> <div className="flex items-center justify-between border-b p-6">
<div className="flex items-center justify-between"> <div>
<div className="flex-1"> <h2 className="text-xl font-semibold text-gray-900">{element.title} </h2>
<h2 className="text-xl font-semibold text-gray-900">{element.title} </h2> <p className="mt-1 text-sm text-gray-500">
<p className="mt-1 text-sm text-gray-500"> {isSimpleWidget
{isSimpleWidget ? "데이터 소스를 설정하세요"
? "데이터 소스를 설정하세요" : currentStep === 1
: currentStep === 1 ? "데이터 소스를 선택하세요"
? "데이터 소스를 선택하세요" : "쿼리를 실행하고 차트를 설정하세요"}
: "쿼리를 실행하고 차트를 설정하세요"}
</p>
</div>
<Button variant="ghost" size="icon" onClick={onClose} className="h-8 w-8">
<X className="h-5 w-5" />
</Button>
</div>
{/* 커스텀 제목 입력 */}
<div className="mt-4">
<label className="block text-sm font-medium text-gray-700 mb-1">
()
</label>
<input
type="text"
value={customTitle}
onChange={(e) => setCustomTitle(e.target.value)}
placeholder={`예: 정비 일정 목록, 창고 위치 현황 등 (비워두면 자동 생성)`}
className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary"
/>
<p className="mt-1 text-xs text-gray-500">
💡 (: "maintenance_schedules 목록")
</p> </p>
</div> </div>
<Button variant="ghost" size="icon" onClick={onClose} className="h-8 w-8">
<X className="h-5 w-5" />
</Button>
</div> </div>
{/* 진행 상황 표시 - 간단한 위젯은 표시 안 함 */} {/* 진행 상황 표시 - 간단한 위젯은 표시 안 함 */}

View File

@ -54,7 +54,6 @@ export interface DashboardElement {
position: Position; position: Position;
size: Size; size: Size;
title: string; title: string;
customTitle?: string; // 사용자 정의 제목 (옵션)
content: string; content: string;
dataSource?: ChartDataSource; // 데이터 소스 설정 dataSource?: ChartDataSource; // 데이터 소스 설정
chartConfig?: ChartConfig; // 차트 설정 chartConfig?: ChartConfig; // 차트 설정

View File

@ -25,7 +25,6 @@ const DocumentWidget = dynamic(() => import("./widgets/DocumentWidget"), { ssr:
const BookingAlertWidget = dynamic(() => import("./widgets/BookingAlertWidget"), { ssr: false }); const BookingAlertWidget = dynamic(() => import("./widgets/BookingAlertWidget"), { ssr: false });
const MaintenanceWidget = dynamic(() => import("./widgets/MaintenanceWidget"), { ssr: false }); const MaintenanceWidget = dynamic(() => import("./widgets/MaintenanceWidget"), { ssr: false });
const CalculatorWidget = dynamic(() => import("./widgets/CalculatorWidget"), { ssr: false }); const CalculatorWidget = dynamic(() => import("./widgets/CalculatorWidget"), { ssr: false });
const CalendarWidget = dynamic(() => import("@/components/admin/dashboard/widgets/CalendarWidget").then(mod => ({ default: mod.CalendarWidget })), { ssr: false });
/** /**
* - DashboardSidebar의 subtype * - DashboardSidebar의 subtype
@ -37,11 +36,11 @@ function renderWidget(element: DashboardElement) {
// === 위젯 종류 === // === 위젯 종류 ===
case "exchange": case "exchange":
return <ExchangeWidget element={element} />; return <ExchangeWidget />;
case "weather": case "weather":
return <WeatherWidget element={element} />; return <WeatherWidget />;
case "calculator": case "calculator":
return <CalculatorWidget element={element} />; return <CalculatorWidget />;
case "clock": case "clock":
return ( return (
<div className="flex h-full w-full items-center justify-center bg-gradient-to-br from-blue-400 to-purple-600 p-4 text-white"> <div className="flex h-full w-full items-center justify-center bg-gradient-to-br from-blue-400 to-purple-600 p-4 text-white">
@ -56,21 +55,28 @@ function renderWidget(element: DashboardElement) {
case "list-summary": case "list-summary":
return <ListSummaryWidget element={element} />; return <ListSummaryWidget element={element} />;
case "risk-alert": case "risk-alert":
return <RiskAlertWidget element={element} />; return <RiskAlertWidget />;
case "calendar": case "calendar":
return <CalendarWidget element={element} />; return (
<div className="flex h-full w-full items-center justify-center bg-gradient-to-br from-green-400 to-teal-600 p-4 text-white">
<div className="text-center">
<div className="mb-2 text-3xl">📅</div>
<div className="text-sm"> ( )</div>
</div>
</div>
);
case "status-summary": case "status-summary":
return <StatusSummaryWidget element={element} />; return <StatusSummaryWidget element={element} />;
// === 운영/작업 지원 === // === 운영/작업 지원 ===
case "todo": case "todo":
return <TodoWidget element={element} />; return <TodoWidget />;
case "booking-alert": case "booking-alert":
return <BookingAlertWidget element={element} />; return <BookingAlertWidget />;
case "maintenance": case "maintenance":
return <MaintenanceWidget element={element} />; return <MaintenanceWidget />;
case "document": case "document":
return <DocumentWidget element={element} />; return <DocumentWidget />;
case "list": case "list":
return <ListSummaryWidget element={element} />; return <ListSummaryWidget element={element} />;

View File

@ -2,7 +2,6 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { Check, X, Phone, MapPin, Package, Clock, AlertCircle } from "lucide-react"; import { Check, X, Phone, MapPin, Package, Clock, AlertCircle } from "lucide-react";
import { DashboardElement } from "@/components/admin/dashboard/types";
interface BookingRequest { interface BookingRequest {
id: string; id: string;
@ -20,11 +19,7 @@ interface BookingRequest {
estimatedCost?: number; estimatedCost?: number;
} }
interface BookingAlertWidgetProps { export default function BookingAlertWidget() {
element?: DashboardElement;
}
export default function BookingAlertWidget({ element }: BookingAlertWidgetProps) {
const [bookings, setBookings] = useState<BookingRequest[]>([]); const [bookings, setBookings] = useState<BookingRequest[]>([]);
const [newCount, setNewCount] = useState(0); const [newCount, setNewCount] = useState(0);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@ -161,7 +156,7 @@ export default function BookingAlertWidget({ element }: BookingAlertWidgetProps)
<div className="border-b border-gray-200 bg-white px-4 py-3"> <div className="border-b border-gray-200 bg-white px-4 py-3">
<div className="mb-3 flex items-center justify-between"> <div className="mb-3 flex items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<h3 className="text-lg font-bold text-gray-800">🔔 {element?.customTitle || "예약 요청 알림"}</h3> <h3 className="text-lg font-bold text-gray-800">🔔 </h3>
{newCount > 0 && ( {newCount > 0 && (
<span className="flex h-6 w-6 items-center justify-center rounded-full bg-red-500 text-xs font-bold text-white"> <span className="flex h-6 w-6 items-center justify-center rounded-full bg-red-500 text-xs font-bold text-white">
{newCount} {newCount}

View File

@ -9,14 +9,12 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { DashboardElement } from '@/components/admin/dashboard/types';
interface CalculatorWidgetProps { interface CalculatorWidgetProps {
element?: DashboardElement;
className?: string; className?: string;
} }
export default function CalculatorWidget({ element, className = '' }: CalculatorWidgetProps) { export default function CalculatorWidget({ className = '' }: CalculatorWidgetProps) {
const [display, setDisplay] = useState<string>('0'); const [display, setDisplay] = useState<string>('0');
const [previousValue, setPreviousValue] = useState<number | null>(null); const [previousValue, setPreviousValue] = useState<number | null>(null);
const [operation, setOperation] = useState<string | null>(null); const [operation, setOperation] = useState<string | null>(null);
@ -119,10 +117,7 @@ export default function CalculatorWidget({ element, className = '' }: Calculator
return ( return (
<div className={`h-full w-full p-3 bg-gradient-to-br from-slate-50 to-gray-100 ${className}`}> <div className={`h-full w-full p-3 bg-gradient-to-br from-slate-50 to-gray-100 ${className}`}>
<div className="h-full flex flex-col gap-2"> <div className="h-full flex flex-col justify-center gap-2">
{/* 제목 */}
<h3 className="text-base font-semibold text-gray-900 text-center">🧮 {element?.customTitle || "계산기"}</h3>
{/* 디스플레이 */} {/* 디스플레이 */}
<div className="bg-white border-2 border-gray-200 rounded-lg p-4 shadow-inner min-h-[80px]"> <div className="bg-white border-2 border-gray-200 rounded-lg p-4 shadow-inner min-h-[80px]">
<div className="text-right h-full flex flex-col justify-center"> <div className="text-right h-full flex flex-col justify-center">

View File

@ -2,7 +2,6 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { FileText, Download, Calendar, Folder, Search } from "lucide-react"; import { FileText, Download, Calendar, Folder, Search } from "lucide-react";
import { DashboardElement } from "@/components/admin/dashboard/types";
interface Document { interface Document {
id: string; id: string;
@ -14,69 +13,64 @@ interface Document {
description?: string; description?: string;
} }
// 목 데이터 (하드코딩 - 주석처리됨) // 목 데이터
// const mockDocuments: Document[] = [ const mockDocuments: Document[] = [
// { {
// id: "1", id: "1",
// name: "2025년 1월 세금계산서.pdf", name: "2025년 1월 세금계산서.pdf",
// category: "세금계산서", category: "세금계산서",
// size: "1.2 MB", size: "1.2 MB",
// uploadDate: "2025-01-05", uploadDate: "2025-01-05",
// url: "/documents/tax-invoice-202501.pdf", url: "/documents/tax-invoice-202501.pdf",
// description: "1월 매출 세금계산서", description: "1월 매출 세금계산서",
// }, },
// { {
// id: "2", id: "2",
// name: "차량보험증권_서울12가3456.pdf", name: "차량보험증권_서울12가3456.pdf",
// category: "보험", category: "보험",
// size: "856 KB", size: "856 KB",
// uploadDate: "2024-12-20", uploadDate: "2024-12-20",
// url: "/documents/insurance-vehicle-1.pdf", url: "/documents/insurance-vehicle-1.pdf",
// description: "1톤 트럭 종합보험", description: "1톤 트럭 종합보험",
// }, },
// { {
// id: "3", id: "3",
// name: "운송계약서_ABC물류.pdf", name: "운송계약서_ABC물류.pdf",
// category: "계약서", category: "계약서",
// size: "2.4 MB", size: "2.4 MB",
// uploadDate: "2024-12-15", uploadDate: "2024-12-15",
// url: "/documents/contract-abc-logistics.pdf", url: "/documents/contract-abc-logistics.pdf",
// description: "ABC물류 연간 운송 계약", description: "ABC물류 연간 운송 계약",
// }, },
// { {
// id: "4", id: "4",
// name: "2024년 12월 세금계산서.pdf", name: "2024년 12월 세금계산서.pdf",
// category: "세금계산서", category: "세금계산서",
// size: "1.1 MB", size: "1.1 MB",
// uploadDate: "2024-12-05", uploadDate: "2024-12-05",
// url: "/documents/tax-invoice-202412.pdf", url: "/documents/tax-invoice-202412.pdf",
// }, },
// { {
// id: "5", id: "5",
// name: "화물배상책임보험증권.pdf", name: "화물배상책임보험증권.pdf",
// category: "보험", category: "보험",
// size: "720 KB", size: "720 KB",
// uploadDate: "2024-11-30", uploadDate: "2024-11-30",
// url: "/documents/cargo-insurance.pdf", url: "/documents/cargo-insurance.pdf",
// description: "화물 배상책임보험", description: "화물 배상책임보험",
// }, },
// { {
// id: "6", id: "6",
// name: "차고지 임대계약서.pdf", name: "차고지 임대계약서.pdf",
// category: "계약서", category: "계약서",
// size: "1.8 MB", size: "1.8 MB",
// uploadDate: "2024-11-15", uploadDate: "2024-11-15",
// url: "/documents/garage-lease-contract.pdf", url: "/documents/garage-lease-contract.pdf",
// }, },
// ]; ];
interface DocumentWidgetProps { export default function DocumentWidget() {
element?: DashboardElement; const [documents] = useState<Document[]>(mockDocuments);
}
export default function DocumentWidget({ element }: DocumentWidgetProps) {
// TODO: 실제 API 연동 필요
const [documents] = useState<Document[]>([]);
const [filter, setFilter] = useState<"all" | Document["category"]>("all"); const [filter, setFilter] = useState<"all" | Document["category"]>("all");
const [searchTerm, setSearchTerm] = useState(""); const [searchTerm, setSearchTerm] = useState("");
@ -132,7 +126,7 @@ export default function DocumentWidget({ element }: DocumentWidgetProps) {
{/* 헤더 */} {/* 헤더 */}
<div className="border-b border-gray-200 bg-white px-4 py-3"> <div className="border-b border-gray-200 bg-white px-4 py-3">
<div className="mb-3 flex items-center justify-between"> <div className="mb-3 flex items-center justify-between">
<h3 className="text-lg font-bold text-gray-800">📂 {element?.customTitle || "문서 관리"}</h3> <h3 className="text-lg font-bold text-gray-800">📂 </h3>
<button className="rounded-lg bg-primary px-3 py-1.5 text-sm text-white transition-colors hover:bg-primary/90"> <button className="rounded-lg bg-primary px-3 py-1.5 text-sm text-white transition-colors hover:bg-primary/90">
+ +
</button> </button>

View File

@ -12,17 +12,14 @@ import { TrendingUp, TrendingDown, RefreshCw, ArrowRightLeft } from 'lucide-reac
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { DashboardElement } from '@/components/admin/dashboard/types';
interface ExchangeWidgetProps { interface ExchangeWidgetProps {
element?: DashboardElement;
baseCurrency?: string; baseCurrency?: string;
targetCurrency?: string; targetCurrency?: string;
refreshInterval?: number; // 새로고침 간격 (ms), 기본값: 600000 (10분) refreshInterval?: number; // 새로고침 간격 (ms), 기본값: 600000 (10분)
} }
export default function ExchangeWidget({ export default function ExchangeWidget({
element,
baseCurrency = 'KRW', baseCurrency = 'KRW',
targetCurrency = 'USD', targetCurrency = 'USD',
refreshInterval = 600000, refreshInterval = 600000,
@ -139,7 +136,7 @@ export default function ExchangeWidget({
{/* 헤더 */} {/* 헤더 */}
<div className="flex items-center justify-between mb-3"> <div className="flex items-center justify-between mb-3">
<div className="flex-1"> <div className="flex-1">
<h3 className="text-base font-semibold text-gray-900 mb-1">💱 {element?.customTitle || "환율"}</h3> <h3 className="text-base font-semibold text-gray-900 mb-1">💱 </h3>
<p className="text-xs text-gray-500"> <p className="text-xs text-gray-500">
{lastUpdated {lastUpdated
? `업데이트: ${lastUpdated.toLocaleTimeString('ko-KR', { ? `업데이트: ${lastUpdated.toLocaleTimeString('ko-KR', {

View File

@ -14,52 +14,51 @@ interface MaintenanceSchedule {
estimatedCost?: number; estimatedCost?: number;
} }
// 목 데이터 (하드코딩 - 주석처리됨) // 목 데이터
// const mockSchedules: MaintenanceSchedule[] = [ const mockSchedules: MaintenanceSchedule[] = [
// { {
// id: "1", id: "1",
// vehicleNumber: "서울12가3456", vehicleNumber: "서울12가3456",
// vehicleType: "1톤 트럭", vehicleType: "1톤 트럭",
// maintenanceType: "정기점검", maintenanceType: "정기점검",
// scheduledDate: new Date(Date.now() + 2 * 24 * 60 * 60 * 1000).toISOString(), scheduledDate: new Date(Date.now() + 2 * 24 * 60 * 60 * 1000).toISOString(),
// status: "scheduled", status: "scheduled",
// notes: "6개월 정기점검", notes: "6개월 정기점검",
// estimatedCost: 300000, estimatedCost: 300000,
// }, },
// { {
// id: "2", id: "2",
// vehicleNumber: "경기34나5678", vehicleNumber: "경기34나5678",
// vehicleType: "2.5톤 트럭", vehicleType: "2.5톤 트럭",
// maintenanceType: "오일교환", maintenanceType: "오일교환",
// scheduledDate: new Date(Date.now() + 1 * 24 * 60 * 60 * 1000).toISOString(), scheduledDate: new Date(Date.now() + 1 * 24 * 60 * 60 * 1000).toISOString(),
// status: "scheduled", status: "scheduled",
// estimatedCost: 150000, estimatedCost: 150000,
// }, },
// { {
// id: "3", id: "3",
// vehicleNumber: "인천56다7890", vehicleNumber: "인천56다7890",
// vehicleType: "라보", vehicleType: "라보",
// maintenanceType: "타이어교체", maintenanceType: "타이어교체",
// scheduledDate: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000).toISOString(), scheduledDate: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000).toISOString(),
// status: "overdue", status: "overdue",
// notes: "긴급", notes: "긴급",
// estimatedCost: 400000, estimatedCost: 400000,
// }, },
// { {
// id: "4", id: "4",
// vehicleNumber: "부산78라1234", vehicleNumber: "부산78라1234",
// vehicleType: "1톤 트럭", vehicleType: "1톤 트럭",
// maintenanceType: "수리", maintenanceType: "수리",
// scheduledDate: new Date().toISOString(), scheduledDate: new Date().toISOString(),
// status: "in_progress", status: "in_progress",
// notes: "엔진 점검 중", notes: "엔진 점검 중",
// estimatedCost: 800000, estimatedCost: 800000,
// }, },
// ]; ];
export default function MaintenanceWidget() { export default function MaintenanceWidget() {
// TODO: 실제 API 연동 필요 const [schedules] = useState<MaintenanceSchedule[]>(mockSchedules);
const [schedules] = useState<MaintenanceSchedule[]>([]);
const [filter, setFilter] = useState<"all" | MaintenanceSchedule["status"]>("all"); const [filter, setFilter] = useState<"all" | MaintenanceSchedule["status"]>("all");
const [selectedDate, setSelectedDate] = useState<Date>(new Date()); const [selectedDate, setSelectedDate] = useState<Date>(new Date());

View File

@ -150,8 +150,7 @@ export default function MapSummaryWidget({ element }: MapSummaryWidgetProps) {
} }
}; };
// customTitle이 있으면 사용, 없으면 테이블명으로 자동 생성 const displayTitle = tableName ? `${translateTableName(tableName)} 위치` : "위치 지도";
const displayTitle = element.customTitle || (tableName ? `${translateTableName(tableName)} 위치` : "위치 지도");
return ( return (
<div className="flex h-full w-full flex-col overflow-hidden bg-gradient-to-br from-slate-50 to-blue-50 p-2"> <div className="flex h-full w-full flex-col overflow-hidden bg-gradient-to-br from-slate-50 to-blue-50 p-2">
@ -182,15 +181,13 @@ export default function MapSummaryWidget({ element }: MapSummaryWidgetProps) {
)} )}
{/* 지도 (항상 표시) */} {/* 지도 (항상 표시) */}
<div className="relative flex-1 rounded border border-gray-300 bg-white overflow-hidden z-0"> <div className="flex-1 rounded border border-gray-300 bg-white overflow-hidden">
<MapContainer <MapContainer
key={`map-${element.id}`}
center={[36.5, 127.5]} center={[36.5, 127.5]}
zoom={7} zoom={7}
style={{ height: "100%", width: "100%", zIndex: 0 }} style={{ height: "100%", width: "100%" }}
zoomControl={true} zoomControl={true}
preferCanvas={true} preferCanvas={true}
className="z-0"
> >
{/* 브이월드 타일맵 */} {/* 브이월드 타일맵 */}
<TileLayer <TileLayer

View File

@ -6,7 +6,6 @@ import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { RefreshCw, AlertTriangle, Cloud, Construction } from "lucide-react"; import { RefreshCw, AlertTriangle, Cloud, Construction } from "lucide-react";
import { apiClient } from "@/lib/api/client"; import { apiClient } from "@/lib/api/client";
import { DashboardElement } from "@/components/admin/dashboard/types";
// 알림 타입 // 알림 타입
type AlertType = "accident" | "weather" | "construction"; type AlertType = "accident" | "weather" | "construction";
@ -22,11 +21,7 @@ interface Alert {
timestamp: string; timestamp: string;
} }
interface RiskAlertWidgetProps { export default function RiskAlertWidget() {
element?: DashboardElement;
}
export default function RiskAlertWidget({ element }: RiskAlertWidgetProps) {
const [alerts, setAlerts] = useState<Alert[]>([]); const [alerts, setAlerts] = useState<Alert[]>([]);
const [isRefreshing, setIsRefreshing] = useState(false); const [isRefreshing, setIsRefreshing] = useState(false);
const [filter, setFilter] = useState<AlertType | "all">("all"); const [filter, setFilter] = useState<AlertType | "all">("all");
@ -168,7 +163,7 @@ export default function RiskAlertWidget({ element }: RiskAlertWidgetProps) {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<AlertTriangle className="h-5 w-5 text-red-600" /> <AlertTriangle className="h-5 w-5 text-red-600" />
<h3 className="text-base font-semibold text-gray-900">{element?.customTitle || "리스크 / 알림"}</h3> <h3 className="text-base font-semibold text-gray-900"> / </h3>
{stats.high > 0 && ( {stats.high > 0 && (
<Badge className="bg-red-100 text-red-700 hover:bg-red-100"> {stats.high}</Badge> <Badge className="bg-red-100 text-red-700 hover:bg-red-100"> {stats.high}</Badge>
)} )}

View File

@ -349,8 +349,7 @@ export default function StatusSummaryWidget({
return name; return name;
}; };
// customTitle이 있으면 사용, 없으면 테이블명으로 자동 생성 const displayTitle = tableName ? `${translateTableName(tableName)} 현황` : title;
const displayTitle = element.customTitle || (tableName ? `${translateTableName(tableName)} 현황` : title);
return ( return (
<div className={`flex h-full w-full flex-col overflow-hidden bg-gradient-to-br ${bgGradient} p-2`}> <div className={`flex h-full w-full flex-col overflow-hidden bg-gradient-to-br ${bgGradient} p-2`}>

View File

@ -2,7 +2,6 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { Plus, Check, X, Clock, AlertCircle, GripVertical, ChevronDown } from "lucide-react"; import { Plus, Check, X, Clock, AlertCircle, GripVertical, ChevronDown } from "lucide-react";
import { DashboardElement } from "@/components/admin/dashboard/types";
interface TodoItem { interface TodoItem {
id: string; id: string;
@ -28,11 +27,7 @@ interface TodoStats {
overdue: number; overdue: number;
} }
interface TodoWidgetProps { export default function TodoWidget() {
element?: DashboardElement;
}
export default function TodoWidget({ element }: TodoWidgetProps) {
const [todos, setTodos] = useState<TodoItem[]>([]); const [todos, setTodos] = useState<TodoItem[]>([]);
const [stats, setStats] = useState<TodoStats | null>(null); const [stats, setStats] = useState<TodoStats | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@ -198,7 +193,7 @@ export default function TodoWidget({ element }: TodoWidgetProps) {
{/* 헤더 */} {/* 헤더 */}
<div className="border-b border-gray-200 bg-white px-4 py-3"> <div className="border-b border-gray-200 bg-white px-4 py-3">
<div className="mb-3 flex items-center justify-between"> <div className="mb-3 flex items-center justify-between">
<h3 className="text-lg font-bold text-gray-800"> {element?.customTitle || "To-Do / 긴급 지시"}</h3> <h3 className="text-lg font-bold text-gray-800"> To-Do / </h3>
<button <button
onClick={() => setShowAddForm(!showAddForm)} 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" 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"

View File

@ -172,15 +172,13 @@ export default function VehicleMapOnlyWidget({ element, refreshInterval = 30000
{/* 지도 영역 - 브이월드 타일맵 */} {/* 지도 영역 - 브이월드 타일맵 */}
<div className="h-[calc(100%-60px)]"> <div className="h-[calc(100%-60px)]">
<div className="relative h-full overflow-hidden rounded-lg border-2 border-gray-300 bg-white z-0"> <div className="relative h-full overflow-hidden rounded-lg border-2 border-gray-300 bg-white">
<MapContainer <MapContainer
key={`vehicle-map-${element.id}`}
center={[36.5, 127.5]} center={[36.5, 127.5]}
zoom={7} zoom={7}
style={{ height: "100%", width: "100%", zIndex: 0 }} style={{ height: "100%", width: "100%" }}
zoomControl={true} zoomControl={true}
preferCanvas={true} preferCanvas={true}
className="z-0"
> >
{/* 브이월드 타일맵 (HTTPS, 캐싱 적용) */} {/* 브이월드 타일맵 (HTTPS, 캐싱 적용) */}
<TileLayer <TileLayer

View File

@ -24,16 +24,13 @@ import { Button } from '@/components/ui/button';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '@/components/ui/command'; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '@/components/ui/command';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { DashboardElement } from '@/components/admin/dashboard/types';
interface WeatherWidgetProps { interface WeatherWidgetProps {
element?: DashboardElement;
city?: string; city?: string;
refreshInterval?: number; // 새로고침 간격 (ms), 기본값: 600000 (10분) refreshInterval?: number; // 새로고침 간격 (ms), 기본값: 600000 (10분)
} }
export default function WeatherWidget({ export default function WeatherWidget({
element,
city = '서울', city = '서울',
refreshInterval = 600000, refreshInterval = 600000,
}: WeatherWidgetProps) { }: WeatherWidgetProps) {
@ -312,7 +309,6 @@ export default function WeatherWidget({
{/* 헤더 */} {/* 헤더 */}
<div className="flex items-center justify-between mb-3"> <div className="flex items-center justify-between mb-3">
<div className="flex-1"> <div className="flex-1">
<h3 className="text-lg font-semibold text-gray-900 mb-1">🌤 {element?.customTitle || "날씨"}</h3>
<div className="flex items-center gap-2 mb-1"> <div className="flex items-center gap-2 mb-1">
<Popover open={open} onOpenChange={setOpen}> <Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild> <PopoverTrigger asChild>
@ -320,10 +316,10 @@ export default function WeatherWidget({
variant="ghost" variant="ghost"
role="combobox" role="combobox"
aria-expanded={open} aria-expanded={open}
className="justify-between text-sm text-gray-600 hover:bg-white/50 h-auto py-0.5 px-2" className="justify-between text-lg font-semibold text-gray-900 hover:bg-white/50 h-auto py-1 px-2"
> >
{cities.find((city) => city.value === selectedCity)?.label || '도시 선택'} {cities.find((city) => city.value === selectedCity)?.label || '도시 선택'}
<ChevronsUpDown className="ml-2 h-3.5 w-3.5 shrink-0 opacity-50" /> <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="w-[200px] p-0" align="start"> <PopoverContent className="w-[200px] p-0" align="start">