Merge pull request '샤드시옌으로 쫙 수정' (#164) from lhj into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/164
This commit is contained in:
commit
1c1a8633ae
|
|
@ -28,6 +28,8 @@
|
|||
--color-input: var(--input);
|
||||
--color-border: var(--border);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-success: var(--success);
|
||||
--color-warning: var(--warning);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
|
|
@ -63,6 +65,8 @@
|
|||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--success: oklch(0.647 0.176 142.5);
|
||||
--warning: oklch(0.808 0.171 85.6);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
|
|
@ -106,6 +110,8 @@
|
|||
--accent: oklch(0.269 0 0);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--success: oklch(0.697 0.17 142.5);
|
||||
--warning: oklch(0.808 0.171 85.6);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.556 0 0);
|
||||
|
|
|
|||
|
|
@ -26,124 +26,124 @@ import {
|
|||
// 위젯 동적 임포트
|
||||
const WeatherWidget = dynamic(() => import("@/components/dashboard/widgets/WeatherWidget"), {
|
||||
ssr: false,
|
||||
loading: () => <div className="flex h-full items-center justify-center text-sm text-gray-500">로딩 중...</div>,
|
||||
loading: () => <div className="flex h-full items-center justify-center text-sm text-muted-foreground">로딩 중...</div>,
|
||||
});
|
||||
|
||||
const ExchangeWidget = dynamic(() => import("@/components/dashboard/widgets/ExchangeWidget"), {
|
||||
ssr: false,
|
||||
loading: () => <div className="flex h-full items-center justify-center text-sm text-gray-500">로딩 중...</div>,
|
||||
loading: () => <div className="flex h-full items-center justify-center text-sm text-muted-foreground">로딩 중...</div>,
|
||||
});
|
||||
|
||||
const CalculatorWidget = dynamic(() => import("@/components/dashboard/widgets/CalculatorWidget"), {
|
||||
ssr: false,
|
||||
loading: () => <div className="flex h-full items-center justify-center text-sm text-gray-500">로딩 중...</div>,
|
||||
loading: () => <div className="flex h-full items-center justify-center text-sm text-muted-foreground">로딩 중...</div>,
|
||||
});
|
||||
|
||||
const VehicleStatusWidget = dynamic(() => import("@/components/dashboard/widgets/VehicleStatusWidget"), {
|
||||
ssr: false,
|
||||
loading: () => <div className="flex h-full items-center justify-center text-sm text-gray-500">로딩 중...</div>,
|
||||
loading: () => <div className="flex h-full items-center justify-center text-sm text-muted-foreground">로딩 중...</div>,
|
||||
});
|
||||
|
||||
const VehicleListWidget = dynamic(() => import("@/components/dashboard/widgets/VehicleListWidget"), {
|
||||
ssr: false,
|
||||
loading: () => <div className="flex h-full items-center justify-center text-sm text-gray-500">로딩 중...</div>,
|
||||
loading: () => <div className="flex h-full items-center justify-center text-sm text-muted-foreground">로딩 중...</div>,
|
||||
});
|
||||
|
||||
const VehicleMapOnlyWidget = dynamic(() => import("@/components/dashboard/widgets/VehicleMapOnlyWidget"), {
|
||||
ssr: false,
|
||||
loading: () => <div className="flex h-full items-center justify-center text-sm text-gray-500">로딩 중...</div>,
|
||||
loading: () => <div className="flex h-full items-center justify-center text-sm text-muted-foreground">로딩 중...</div>,
|
||||
});
|
||||
|
||||
// 범용 지도 위젯 (차량, 창고, 고객 등 모든 위치 위젯 통합)
|
||||
const MapSummaryWidget = dynamic(() => import("@/components/dashboard/widgets/MapSummaryWidget"), {
|
||||
ssr: false,
|
||||
loading: () => <div className="flex h-full items-center justify-center text-sm text-gray-500">로딩 중...</div>,
|
||||
loading: () => <div className="flex h-full items-center justify-center text-sm text-muted-foreground">로딩 중...</div>,
|
||||
});
|
||||
|
||||
// 🧪 테스트용 지도 위젯 (REST API 지원)
|
||||
const MapTestWidget = dynamic(() => import("@/components/dashboard/widgets/MapTestWidget"), {
|
||||
ssr: false,
|
||||
loading: () => <div className="flex h-full items-center justify-center text-sm text-gray-500">로딩 중...</div>,
|
||||
loading: () => <div className="flex h-full items-center justify-center text-sm text-muted-foreground">로딩 중...</div>,
|
||||
});
|
||||
|
||||
// 🧪 테스트용 지도 위젯 V2 (다중 데이터 소스)
|
||||
const MapTestWidgetV2 = dynamic(() => import("@/components/dashboard/widgets/MapTestWidgetV2"), {
|
||||
ssr: false,
|
||||
loading: () => <div className="flex h-full items-center justify-center text-sm text-gray-500">로딩 중...</div>,
|
||||
loading: () => <div className="flex h-full items-center justify-center text-sm text-muted-foreground">로딩 중...</div>,
|
||||
});
|
||||
|
||||
// 🧪 테스트용 차트 위젯 (다중 데이터 소스)
|
||||
const ChartTestWidget = dynamic(() => import("@/components/dashboard/widgets/ChartTestWidget"), {
|
||||
ssr: false,
|
||||
loading: () => <div className="flex h-full items-center justify-center text-sm text-gray-500">로딩 중...</div>,
|
||||
loading: () => <div className="flex h-full items-center justify-center text-sm text-muted-foreground">로딩 중...</div>,
|
||||
});
|
||||
|
||||
const ListTestWidget = dynamic(
|
||||
() => import("@/components/dashboard/widgets/ListTestWidget").then((mod) => ({ default: mod.ListTestWidget })),
|
||||
{
|
||||
ssr: false,
|
||||
loading: () => <div className="flex h-full items-center justify-center text-sm text-gray-500">로딩 중...</div>,
|
||||
loading: () => <div className="flex h-full items-center justify-center text-sm text-muted-foreground">로딩 중...</div>,
|
||||
},
|
||||
);
|
||||
|
||||
const CustomMetricTestWidget = dynamic(() => import("@/components/dashboard/widgets/CustomMetricTestWidget"), {
|
||||
ssr: false,
|
||||
loading: () => <div className="flex h-full items-center justify-center text-sm text-gray-500">로딩 중...</div>,
|
||||
loading: () => <div className="flex h-full items-center justify-center text-sm text-muted-foreground">로딩 중...</div>,
|
||||
});
|
||||
|
||||
const RiskAlertTestWidget = dynamic(() => import("@/components/dashboard/widgets/RiskAlertTestWidget"), {
|
||||
ssr: false,
|
||||
loading: () => <div className="flex h-full items-center justify-center text-sm text-gray-500">로딩 중...</div>,
|
||||
loading: () => <div className="flex h-full items-center justify-center text-sm text-muted-foreground">로딩 중...</div>,
|
||||
});
|
||||
|
||||
// 범용 상태 요약 위젯 (차량, 배송 등 모든 상태 위젯 통합)
|
||||
const StatusSummaryWidget = dynamic(() => import("@/components/dashboard/widgets/StatusSummaryWidget"), {
|
||||
ssr: false,
|
||||
loading: () => <div className="flex h-full items-center justify-center text-sm text-gray-500">로딩 중...</div>,
|
||||
loading: () => <div className="flex h-full items-center justify-center text-sm text-muted-foreground">로딩 중...</div>,
|
||||
});
|
||||
|
||||
// 범용 목록 위젯 (차량, 기사, 제품 등 모든 목록 위젯 통합) - 다른 분 작업 중, 임시 주석
|
||||
/* const ListSummaryWidget = dynamic(() => import("@/components/dashboard/widgets/ListSummaryWidget"), {
|
||||
ssr: false,
|
||||
loading: () => <div className="flex h-full items-center justify-center text-sm text-gray-500">로딩 중...</div>,
|
||||
loading: () => <div className="flex h-full items-center justify-center text-sm text-muted-foreground">로딩 중...</div>,
|
||||
}); */
|
||||
|
||||
// 개별 위젯들 (주석 처리 - StatusSummaryWidget으로 통합됨)
|
||||
// const DeliveryStatusSummaryWidget = dynamic(() => import("@/components/dashboard/widgets/DeliveryStatusSummaryWidget"), {
|
||||
// ssr: false,
|
||||
// loading: () => <div className="flex h-full items-center justify-center text-sm text-gray-500">로딩 중...</div>,
|
||||
// loading: () => <div className="flex h-full items-center justify-center text-sm text-muted-foreground">로딩 중...</div>,
|
||||
// });
|
||||
// const DeliveryTodayStatsWidget = dynamic(() => import("@/components/dashboard/widgets/DeliveryTodayStatsWidget"), {
|
||||
// ssr: false,
|
||||
// loading: () => <div className="flex h-full items-center justify-center text-sm text-gray-500">로딩 중...</div>,
|
||||
// loading: () => <div className="flex h-full items-center justify-center text-sm text-muted-foreground">로딩 중...</div>,
|
||||
// });
|
||||
// const CargoListWidget = dynamic(() => import("@/components/dashboard/widgets/CargoListWidget"), {
|
||||
// ssr: false,
|
||||
// loading: () => <div className="flex h-full items-center justify-center text-sm text-gray-500">로딩 중...</div>,
|
||||
// loading: () => <div className="flex h-full items-center justify-center text-sm text-muted-foreground">로딩 중...</div>,
|
||||
// });
|
||||
// const CustomerIssuesWidget = dynamic(() => import("@/components/dashboard/widgets/CustomerIssuesWidget"), {
|
||||
// ssr: false,
|
||||
// loading: () => <div className="flex h-full items-center justify-center text-sm text-gray-500">로딩 중...</div>,
|
||||
// loading: () => <div className="flex h-full items-center justify-center text-sm text-muted-foreground">로딩 중...</div>,
|
||||
// });
|
||||
|
||||
const RiskAlertWidget = dynamic(() => import("@/components/dashboard/widgets/RiskAlertWidget"), {
|
||||
ssr: false,
|
||||
loading: () => <div className="flex h-full items-center justify-center text-sm text-gray-500">로딩 중...</div>,
|
||||
loading: () => <div className="flex h-full items-center justify-center text-sm text-muted-foreground">로딩 중...</div>,
|
||||
});
|
||||
|
||||
const TaskWidget = dynamic(() => import("@/components/dashboard/widgets/TaskWidget"), {
|
||||
ssr: false,
|
||||
loading: () => <div className="flex h-full items-center justify-center text-sm text-gray-500">로딩 중...</div>,
|
||||
loading: () => <div className="flex h-full items-center justify-center text-sm text-muted-foreground">로딩 중...</div>,
|
||||
});
|
||||
|
||||
const BookingAlertWidget = dynamic(() => import("@/components/dashboard/widgets/BookingAlertWidget"), {
|
||||
ssr: false,
|
||||
loading: () => <div className="flex h-full items-center justify-center text-sm text-gray-500">로딩 중...</div>,
|
||||
loading: () => <div className="flex h-full items-center justify-center text-sm text-muted-foreground">로딩 중...</div>,
|
||||
});
|
||||
|
||||
const DocumentWidget = dynamic(() => import("@/components/dashboard/widgets/DocumentWidget"), {
|
||||
ssr: false,
|
||||
loading: () => <div className="flex h-full items-center justify-center text-sm text-gray-500">로딩 중...</div>,
|
||||
loading: () => <div className="flex h-full items-center justify-center text-sm text-muted-foreground">로딩 중...</div>,
|
||||
});
|
||||
|
||||
// 시계 위젯 임포트
|
||||
|
|
@ -160,25 +160,25 @@ import { Button } from "@/components/ui/button";
|
|||
// 야드 관리 3D 위젯
|
||||
const YardManagement3DWidget = dynamic(() => import("./widgets/YardManagement3DWidget"), {
|
||||
ssr: false,
|
||||
loading: () => <div className="flex h-full items-center justify-center text-sm text-gray-500">로딩 중...</div>,
|
||||
loading: () => <div className="flex h-full items-center justify-center text-sm text-muted-foreground">로딩 중...</div>,
|
||||
});
|
||||
|
||||
// 작업 이력 위젯
|
||||
const WorkHistoryWidget = dynamic(() => import("@/components/dashboard/widgets/WorkHistoryWidget"), {
|
||||
ssr: false,
|
||||
loading: () => <div className="flex h-full items-center justify-center text-sm text-gray-500">로딩 중...</div>,
|
||||
loading: () => <div className="flex h-full items-center justify-center text-sm text-muted-foreground">로딩 중...</div>,
|
||||
});
|
||||
|
||||
// 커스텀 통계 카드 위젯
|
||||
const CustomStatsWidget = dynamic(() => import("@/components/dashboard/widgets/CustomStatsWidget"), {
|
||||
ssr: false,
|
||||
loading: () => <div className="flex h-full items-center justify-center text-sm text-gray-500">로딩 중...</div>,
|
||||
loading: () => <div className="flex h-full items-center justify-center text-sm text-muted-foreground">로딩 중...</div>,
|
||||
});
|
||||
|
||||
// 사용자 커스텀 카드 위젯
|
||||
const CustomMetricWidget = dynamic(() => import("@/components/dashboard/widgets/CustomMetricWidget"), {
|
||||
ssr: false,
|
||||
loading: () => <div className="flex h-full items-center justify-center text-sm text-gray-500">로딩 중...</div>,
|
||||
loading: () => <div className="flex h-full items-center justify-center text-sm text-muted-foreground">로딩 중...</div>,
|
||||
});
|
||||
|
||||
interface CanvasElementProps {
|
||||
|
|
@ -712,33 +712,33 @@ export function CanvasElement({
|
|||
if (element.type === "chart") {
|
||||
switch (element.subtype) {
|
||||
case "bar":
|
||||
return "bg-gradient-to-br from-indigo-400 to-purple-600";
|
||||
return "bg-gradient-to-br from-primary to-purple-500";
|
||||
case "pie":
|
||||
return "bg-gradient-to-br from-pink-400 to-red-500";
|
||||
return "bg-gradient-to-br from-destructive to-destructive/80";
|
||||
case "line":
|
||||
return "bg-gradient-to-br from-blue-400 to-cyan-400";
|
||||
return "bg-gradient-to-br from-primary to-primary/80";
|
||||
default:
|
||||
return "bg-gray-200";
|
||||
return "bg-muted";
|
||||
}
|
||||
} else if (element.type === "widget") {
|
||||
switch (element.subtype) {
|
||||
case "exchange":
|
||||
return "bg-gradient-to-br from-pink-400 to-yellow-400";
|
||||
return "bg-gradient-to-br from-warning to-warning/80";
|
||||
case "weather":
|
||||
return "bg-gradient-to-br from-cyan-400 to-indigo-800";
|
||||
return "bg-gradient-to-br from-primary to-primary/80";
|
||||
case "clock":
|
||||
return "bg-gradient-to-br from-teal-400 to-cyan-600";
|
||||
return "bg-gradient-to-br from-primary to-primary/80";
|
||||
case "calendar":
|
||||
return "bg-gradient-to-br from-indigo-400 to-purple-600";
|
||||
return "bg-gradient-to-br from-primary to-purple-500";
|
||||
case "driver-management":
|
||||
return "bg-gradient-to-br from-blue-400 to-indigo-600";
|
||||
return "bg-gradient-to-br from-primary to-primary";
|
||||
case "list":
|
||||
return "bg-gradient-to-br from-cyan-400 to-blue-600";
|
||||
return "bg-gradient-to-br from-primary to-primary/80";
|
||||
default:
|
||||
return "bg-gray-200";
|
||||
return "bg-muted";
|
||||
}
|
||||
}
|
||||
return "bg-gray-200";
|
||||
return "bg-muted";
|
||||
};
|
||||
|
||||
// 드래그/리사이즈 중일 때는 임시 위치/크기 사용, 아니면 실제 값 사용
|
||||
|
|
@ -758,7 +758,7 @@ export function CanvasElement({
|
|||
<div
|
||||
ref={elementRef}
|
||||
data-element-id={element.id}
|
||||
className={`absolute min-h-[120px] min-w-[120px] cursor-move overflow-hidden rounded-lg border-2 bg-white shadow-lg ${isSelected ? "border-blue-500 shadow-blue-200" : "border-gray-400"} ${isDragging || isResizing ? "transition-none" : "transition-all duration-150"} `}
|
||||
className={`absolute min-h-[120px] min-w-[120px] cursor-move overflow-hidden rounded-lg border-2 bg-background shadow-lg ${isSelected ? "border-primary ring-2 ring-primary/20" : "border-border"} ${isDragging || isResizing ? "transition-none" : "transition-all duration-150"} `}
|
||||
style={{
|
||||
left: displayPosition.x,
|
||||
top: displayPosition.y,
|
||||
|
|
@ -809,7 +809,7 @@ export function CanvasElement({
|
|||
)}
|
||||
{/* 제목 */}
|
||||
{!element.type || element.type !== "chart" ? (
|
||||
<span className="text-xs font-bold text-gray-800">{element.customTitle || element.title}</span>
|
||||
<span className="text-xs font-bold text-foreground">{element.customTitle || element.title}</span>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
|
|
@ -817,7 +817,7 @@ export function CanvasElement({
|
|||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="element-close hover:bg-destructive h-5 w-5 text-gray-400 hover:text-white"
|
||||
className="element-close hover:bg-destructive h-5 w-5 text-muted-foreground hover:text-white"
|
||||
onClick={handleRemove}
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
title="삭제"
|
||||
|
|
@ -831,9 +831,9 @@ export function CanvasElement({
|
|||
<div className="relative h-[calc(100%-32px)] px-2 pb-2">
|
||||
{element.type === "chart" ? (
|
||||
// 차트 렌더링
|
||||
<div className="h-full w-full bg-white">
|
||||
<div className="h-full w-full bg-background">
|
||||
{isLoadingData ? (
|
||||
<div className="flex h-full w-full items-center justify-center text-gray-500">
|
||||
<div className="flex h-full w-full items-center justify-center text-muted-foreground">
|
||||
<div className="text-center">
|
||||
<div className="border-primary mx-auto mb-2 h-6 w-6 animate-spin rounded-full border-2 border-t-transparent" />
|
||||
<div className="text-sm">데이터 로딩 중...</div>
|
||||
|
|
@ -926,7 +926,7 @@ export function CanvasElement({
|
|||
) : element.type === "widget" && element.subtype === "status-summary" ? (
|
||||
// 커스텀 상태 카드 - 범용 위젯
|
||||
<div className="widget-interactive-area h-full w-full">
|
||||
<StatusSummaryWidget element={element} title="상태 요약" icon="📊" bgGradient="from-slate-50 to-blue-50" />
|
||||
<StatusSummaryWidget element={element} title="상태 요약" icon="📊" bgGradient="from-background to-primary/10" />
|
||||
</div>
|
||||
) : /* element.type === "widget" && element.subtype === "list-summary" ? (
|
||||
// 커스텀 목록 카드 - 범용 위젯 (다른 분 작업 중 - 임시 주석)
|
||||
|
|
@ -940,7 +940,7 @@ export function CanvasElement({
|
|||
element={element}
|
||||
title="배송/화물 현황"
|
||||
icon="📦"
|
||||
bgGradient="from-slate-50 to-blue-50"
|
||||
bgGradient="from-background to-primary/10"
|
||||
/>
|
||||
</div>
|
||||
) : element.type === "widget" && element.subtype === "delivery-status-summary" ? (
|
||||
|
|
@ -950,7 +950,7 @@ export function CanvasElement({
|
|||
element={element}
|
||||
title="배송 상태 요약"
|
||||
icon="📊"
|
||||
bgGradient="from-slate-50 to-blue-50"
|
||||
bgGradient="from-background to-primary/10"
|
||||
statusConfig={{
|
||||
배송중: { label: "배송중", color: "blue" },
|
||||
완료: { label: "완료", color: "green" },
|
||||
|
|
@ -966,7 +966,7 @@ export function CanvasElement({
|
|||
element={element}
|
||||
title="오늘 처리 현황"
|
||||
icon="📈"
|
||||
bgGradient="from-slate-50 to-green-50"
|
||||
bgGradient="from-background to-success/10"
|
||||
/>
|
||||
</div>
|
||||
) : element.type === "widget" && element.subtype === "cargo-list" ? (
|
||||
|
|
@ -976,7 +976,7 @@ export function CanvasElement({
|
|||
element={element}
|
||||
title="화물 목록"
|
||||
icon="📦"
|
||||
bgGradient="from-slate-50 to-orange-50"
|
||||
bgGradient="from-background to-warning/10"
|
||||
/>
|
||||
</div>
|
||||
) : element.type === "widget" && element.subtype === "customer-issues" ? (
|
||||
|
|
@ -986,7 +986,7 @@ export function CanvasElement({
|
|||
element={element}
|
||||
title="고객 클레임/이슈"
|
||||
icon="⚠️"
|
||||
bgGradient="from-slate-50 to-red-50"
|
||||
bgGradient="from-background to-destructive/10"
|
||||
/>
|
||||
</div>
|
||||
) : element.type === "widget" && element.subtype === "risk-alert" ? (
|
||||
|
|
@ -1111,7 +1111,7 @@ function ResizeHandle({ position, onMouseDown }: ResizeHandleProps) {
|
|||
|
||||
return (
|
||||
<div
|
||||
className={`resize-handle absolute h-3 w-3 border border-white bg-green-500 ${getPositionClass()} `}
|
||||
className={`resize-handle absolute h-3 w-3 border border-white bg-success ${getPositionClass()} `}
|
||||
onMouseDown={(e) => onMouseDown(e, position)}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -117,17 +117,17 @@ export function ChartConfigPanel({
|
|||
다음 컬럼은 객체 또는 배열 타입이라서 차트 축으로 선택할 수 없습니다:
|
||||
<div className="mt-1 flex flex-wrap gap-1">
|
||||
{complexColumns.map((col) => (
|
||||
<Badge key={col} variant="outline" className="bg-red-50">
|
||||
<Badge key={col} variant="outline" className="bg-destructive/10">
|
||||
{col} ({columnTypes[col]})
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 text-xs text-gray-600">
|
||||
<div className="mt-2 text-xs text-foreground">
|
||||
<strong>해결 방법:</strong> JSON Path를 사용하여 중첩된 객체 내부의 값을 직접 추출하세요.
|
||||
<br />
|
||||
예: <code className="rounded bg-gray-100 px-1">main</code> 또는{" "}
|
||||
<code className="rounded bg-gray-100 px-1">data.items</code>
|
||||
예: <code className="rounded bg-muted px-1">main</code> 또는{" "}
|
||||
<code className="rounded bg-muted px-1">data.items</code>
|
||||
</div>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
|
@ -135,7 +135,7 @@ export function ChartConfigPanel({
|
|||
|
||||
{/* 차트 제목 */}
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs font-medium text-gray-700">차트 제목</Label>
|
||||
<Label className="text-xs font-medium text-foreground">차트 제목</Label>
|
||||
<Input
|
||||
type="text"
|
||||
value={currentConfig.title || ""}
|
||||
|
|
@ -149,9 +149,9 @@ export function ChartConfigPanel({
|
|||
|
||||
{/* X축 설정 */}
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs font-medium text-gray-700">
|
||||
<Label className="text-xs font-medium text-foreground">
|
||||
X축 (카테고리)
|
||||
<span className="ml-1 text-red-500">*</span>
|
||||
<span className="ml-1 text-destructive">*</span>
|
||||
</Label>
|
||||
<Select value={currentConfig.xAxis || undefined} onValueChange={(value) => updateConfig({ xAxis: value })}>
|
||||
<SelectTrigger className="h-8 text-xs">
|
||||
|
|
@ -170,39 +170,39 @@ export function ChartConfigPanel({
|
|||
return (
|
||||
<SelectItem key={col} value={col} className="text-xs">
|
||||
{col}
|
||||
{previewText && <span className="ml-1.5 text-[10px] text-gray-500">(예: {previewText})</span>}
|
||||
{previewText && <span className="ml-1.5 text-[10px] text-muted-foreground">(예: {previewText})</span>}
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{simpleColumns.length === 0 && (
|
||||
<p className="text-[11px] text-red-500">사용 가능한 컬럼이 없습니다. JSON Path를 확인하세요.</p>
|
||||
<p className="text-[11px] text-destructive">사용 가능한 컬럼이 없습니다. JSON Path를 확인하세요.</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Y축 설정 (다중 선택 가능) */}
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs font-medium text-gray-700">
|
||||
<Label className="text-xs font-medium text-foreground">
|
||||
Y축 (값) - 여러 개 선택 가능
|
||||
{!isPieChart && !isApiSource && <span className="ml-1 text-red-500">*</span>}
|
||||
{!isPieChart && !isApiSource && <span className="ml-1 text-destructive">*</span>}
|
||||
{(isPieChart || isApiSource) && (
|
||||
<span className="ml-1.5 text-[11px] text-gray-500">(선택사항 - 그룹핑+집계 사용 가능)</span>
|
||||
<span className="ml-1.5 text-[11px] text-muted-foreground">(선택사항 - 그룹핑+집계 사용 가능)</span>
|
||||
)}
|
||||
</Label>
|
||||
<div className="max-h-48 overflow-y-auto rounded border border-gray-200 bg-gray-50 p-2">
|
||||
<div className="max-h-48 overflow-y-auto rounded border border-border bg-muted p-2">
|
||||
<div className="space-y-1.5">
|
||||
{/* 숫자 타입 우선 표시 */}
|
||||
{numericColumns.length > 0 && (
|
||||
<>
|
||||
<div className="mb-1.5 text-[11px] font-medium text-green-700">숫자 타입 (권장)</div>
|
||||
<div className="mb-1.5 text-[11px] font-medium text-success">숫자 타입 (권장)</div>
|
||||
{numericColumns.map((col) => {
|
||||
const isSelected = Array.isArray(currentConfig.yAxis)
|
||||
? currentConfig.yAxis.includes(col)
|
||||
: currentConfig.yAxis === col;
|
||||
|
||||
return (
|
||||
<div key={col} className="flex items-center gap-1.5 rounded border-green-500 bg-green-50 p-1.5">
|
||||
<div key={col} className="flex items-center gap-1.5 rounded border-success bg-success/10 p-1.5">
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
onCheckedChange={(checked) => {
|
||||
|
|
@ -229,7 +229,7 @@ export function ChartConfigPanel({
|
|||
<Label className="flex-1 cursor-pointer text-xs font-normal">
|
||||
<span className="font-medium">{col}</span>
|
||||
{sampleData[col] !== undefined && (
|
||||
<span className="ml-1.5 text-[10px] text-gray-600">(예: {sampleData[col]})</span>
|
||||
<span className="ml-1.5 text-[10px] text-foreground">(예: {sampleData[col]})</span>
|
||||
)}
|
||||
</Label>
|
||||
</div>
|
||||
|
|
@ -242,7 +242,7 @@ export function ChartConfigPanel({
|
|||
{simpleColumns.filter((col) => !numericColumns.includes(col)).length > 0 && (
|
||||
<>
|
||||
{numericColumns.length > 0 && <div className="my-1.5 border-t"></div>}
|
||||
<div className="mb-1.5 text-[11px] font-medium text-gray-600">기타 타입</div>
|
||||
<div className="mb-1.5 text-[11px] font-medium text-foreground">기타 타입</div>
|
||||
{simpleColumns
|
||||
.filter((col) => !numericColumns.includes(col))
|
||||
.map((col) => {
|
||||
|
|
@ -251,7 +251,7 @@ export function ChartConfigPanel({
|
|||
: currentConfig.yAxis === col;
|
||||
|
||||
return (
|
||||
<div key={col} className="flex items-center gap-1.5 rounded p-1.5 hover:bg-gray-50">
|
||||
<div key={col} className="flex items-center gap-1.5 rounded p-1.5 hover:bg-muted">
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
onCheckedChange={(checked) => {
|
||||
|
|
@ -278,7 +278,7 @@ export function ChartConfigPanel({
|
|||
<Label className="flex-1 cursor-pointer text-xs font-normal">
|
||||
{col}
|
||||
{sampleData[col] !== undefined && (
|
||||
<span className="ml-1.5 text-[10px] text-gray-500">
|
||||
<span className="ml-1.5 text-[10px] text-muted-foreground">
|
||||
(예: {String(sampleData[col]).substring(0, 30)})
|
||||
</span>
|
||||
)}
|
||||
|
|
@ -291,9 +291,9 @@ export function ChartConfigPanel({
|
|||
</div>
|
||||
</div>
|
||||
{simpleColumns.length === 0 && (
|
||||
<p className="text-[11px] text-red-500">사용 가능한 컬럼이 없습니다. JSON Path를 확인하세요.</p>
|
||||
<p className="text-[11px] text-destructive">사용 가능한 컬럼이 없습니다. JSON Path를 확인하세요.</p>
|
||||
)}
|
||||
<p className="text-[11px] text-gray-500">
|
||||
<p className="text-[11px] text-muted-foreground">
|
||||
팁: 여러 항목을 선택하면 비교 차트가 생성됩니다 (예: 갤럭시 vs 아이폰)
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -302,9 +302,9 @@ export function ChartConfigPanel({
|
|||
|
||||
{/* 집계 함수 */}
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs font-medium text-gray-700">
|
||||
<Label className="text-xs font-medium text-foreground">
|
||||
집계 함수
|
||||
<span className="ml-1.5 text-[11px] text-gray-500">(데이터 처리 방식)</span>
|
||||
<span className="ml-1.5 text-[11px] text-muted-foreground">(데이터 처리 방식)</span>
|
||||
</Label>
|
||||
<Select
|
||||
value={currentConfig.aggregation || "none"}
|
||||
|
|
@ -338,16 +338,16 @@ export function ChartConfigPanel({
|
|||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-[11px] text-gray-500">
|
||||
<p className="text-[11px] text-muted-foreground">
|
||||
그룹핑 필드와 함께 사용하면 자동으로 데이터를 집계합니다. (예: 부서별 개수, 월별 합계)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 그룹핑 필드 (선택사항) */}
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs font-medium text-gray-700">
|
||||
<Label className="text-xs font-medium text-foreground">
|
||||
그룹핑 필드 (선택사항)
|
||||
<span className="ml-1.5 text-[11px] text-gray-500">(같은 값끼리 묶어서 집계)</span>
|
||||
<span className="ml-1.5 text-[11px] text-muted-foreground">(같은 값끼리 묶어서 집계)</span>
|
||||
</Label>
|
||||
<Select
|
||||
value={currentConfig.groupBy || undefined}
|
||||
|
|
@ -373,7 +373,7 @@ export function ChartConfigPanel({
|
|||
|
||||
{/* 차트 색상 */}
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs font-medium text-gray-700">차트 색상</Label>
|
||||
<Label className="text-xs font-medium text-foreground">차트 색상</Label>
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
{[
|
||||
["#3B82F6", "#EF4444", "#10B981", "#F59E0B"], // 기본
|
||||
|
|
@ -387,8 +387,8 @@ export function ChartConfigPanel({
|
|||
onClick={() => updateConfig({ colors: colorSet })}
|
||||
className={`flex h-8 rounded border-2 transition-colors ${
|
||||
JSON.stringify(currentConfig.colors) === JSON.stringify(colorSet)
|
||||
? "border-gray-800"
|
||||
: "border-gray-300 hover:border-gray-400"
|
||||
? "border-foreground"
|
||||
: "border-border hover:border-border/80"
|
||||
}`}
|
||||
>
|
||||
{colorSet.map((color, idx) => (
|
||||
|
|
|
|||
|
|
@ -466,7 +466,7 @@ export const DashboardCanvas = forwardRef<HTMLDivElement, DashboardCanvasProps>(
|
|||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={`dashboard-canvas relative w-full ${isDragOver ? "bg-blue-50/50" : ""} `}
|
||||
className={`dashboard-canvas relative w-full ${isDragOver ? "bg-primary/5" : ""} `}
|
||||
style={{
|
||||
backgroundColor,
|
||||
height: `${canvasHeight}px`,
|
||||
|
|
@ -512,7 +512,7 @@ export const DashboardCanvas = forwardRef<HTMLDivElement, DashboardCanvasProps>(
|
|||
)}
|
||||
{/* 배치된 요소들 렌더링 */}
|
||||
{elements.length === 0 && (
|
||||
<div className="pointer-events-none absolute inset-0 flex items-center justify-center text-gray-400">
|
||||
<div className="pointer-events-none absolute inset-0 flex items-center justify-center text-muted-foreground">
|
||||
<div className="text-center">
|
||||
<div className="text-sm">상단 메뉴에서 차트나 위젯을 선택하세요</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -582,11 +582,11 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D
|
|||
// 로딩 중이면 로딩 화면 표시
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center bg-gray-50">
|
||||
<div className="flex h-full items-center justify-center bg-muted">
|
||||
<div className="text-center">
|
||||
<div className="border-primary mx-auto mb-4 h-12 w-12 animate-spin rounded-full border-4 border-t-transparent" />
|
||||
<div className="text-lg font-medium text-gray-700">대시보드 로딩 중...</div>
|
||||
<div className="mt-1 text-sm text-gray-500">잠시만 기다려주세요</div>
|
||||
<div className="text-lg font-medium text-foreground">대시보드 로딩 중...</div>
|
||||
<div className="mt-1 text-sm text-muted-foreground">잠시만 기다려주세요</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -594,7 +594,7 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D
|
|||
|
||||
return (
|
||||
<DashboardProvider>
|
||||
<div className="flex h-full flex-col bg-gray-50">
|
||||
<div className="flex h-full flex-col bg-muted">
|
||||
{/* 상단 메뉴바 */}
|
||||
<DashboardTopMenu
|
||||
onSaveLayout={saveLayout}
|
||||
|
|
@ -610,7 +610,7 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D
|
|||
|
||||
{/* 캔버스 영역 - 해상도에 따른 크기, 중앙 정렬 */}
|
||||
{/* overflow-auto 제거 - 외부 페이지 스크롤 사용 */}
|
||||
<div className="dashboard-canvas-container flex flex-1 items-start justify-center bg-gray-100 p-8">
|
||||
<div className="dashboard-canvas-container flex flex-1 items-start justify-center bg-muted p-8">
|
||||
<div
|
||||
className="relative"
|
||||
style={{
|
||||
|
|
@ -679,8 +679,8 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D
|
|||
>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-green-100">
|
||||
<CheckCircle2 className="h-6 w-6 text-green-600" />
|
||||
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-success/10">
|
||||
<CheckCircle2 className="h-6 w-6 text-success" />
|
||||
</div>
|
||||
<DialogTitle className="text-center">저장 완료</DialogTitle>
|
||||
<DialogDescription className="text-center">대시보드가 성공적으로 저장되었습니다.</DialogDescription>
|
||||
|
|
@ -711,7 +711,7 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D
|
|||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>취소</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleClearConfirm} className="bg-red-600 hover:bg-red-700">
|
||||
<AlertDialogAction onClick={handleClearConfirm} className="bg-destructive hover:bg-destructive/90">
|
||||
초기화
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
|
|
|
|||
|
|
@ -177,7 +177,7 @@ export function DashboardSaveModal({
|
|||
{/* 대시보드 이름 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="title">
|
||||
대시보드 이름 <span className="text-red-500">*</span>
|
||||
대시보드 이름 <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="title"
|
||||
|
|
@ -235,7 +235,7 @@ export function DashboardSaveModal({
|
|||
|
||||
{/* 메뉴 할당 옵션 */}
|
||||
{assignToMenu && (
|
||||
<div className="ml-6 space-y-4 border-l-2 border-gray-200 pl-4">
|
||||
<div className="ml-6 space-y-4 border-l-2 border-border pl-4">
|
||||
{/* 메뉴 타입 선택 */}
|
||||
<div className="space-y-2">
|
||||
<Label>메뉴 타입</Label>
|
||||
|
|
@ -260,8 +260,8 @@ export function DashboardSaveModal({
|
|||
<Label>메뉴 선택</Label>
|
||||
{loadingMenus ? (
|
||||
<div className="flex items-center justify-center py-4">
|
||||
<Loader2 className="h-5 w-5 animate-spin text-gray-400" />
|
||||
<span className="ml-2 text-sm text-gray-500">메뉴 목록 로딩 중...</span>
|
||||
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
|
||||
<span className="ml-2 text-sm text-muted-foreground">메뉴 목록 로딩 중...</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
|
|
@ -273,7 +273,7 @@ export function DashboardSaveModal({
|
|||
<SelectGroup>
|
||||
<SelectLabel>{menuType === "admin" ? "관리자 메뉴" : "사용자 메뉴"}</SelectLabel>
|
||||
{flatMenus.length === 0 ? (
|
||||
<div className="px-2 py-3 text-sm text-gray-500">사용 가능한 메뉴가 없습니다.</div>
|
||||
<div className="px-2 py-3 text-sm text-muted-foreground">사용 가능한 메뉴가 없습니다.</div>
|
||||
) : (
|
||||
flatMenus.map((menu) => (
|
||||
<SelectItem key={menu.uniqueKey} value={menu.id}>
|
||||
|
|
@ -285,7 +285,7 @@ export function DashboardSaveModal({
|
|||
</SelectContent>
|
||||
</Select>
|
||||
{selectedMenuId && (
|
||||
<div className="rounded-md bg-gray-50 p-2 text-sm text-gray-700">
|
||||
<div className="rounded-md bg-muted p-2 text-sm text-foreground">
|
||||
선택된 메뉴:{" "}
|
||||
<span className="font-medium">{flatMenus.find((m) => m.id === selectedMenuId)?.label}</span>
|
||||
</div>
|
||||
|
|
@ -293,7 +293,7 @@ export function DashboardSaveModal({
|
|||
</div>
|
||||
)}
|
||||
{assignToMenu && selectedMenuId && (
|
||||
<p className="mt-1 text-xs text-gray-500">
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
선택한 메뉴의 URL이 이 대시보드로 자동 설정됩니다.
|
||||
{menuType === "admin" && " (관리자 모드 파라미터 포함)"}
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -16,13 +16,13 @@ interface DashboardToolbarProps {
|
|||
export function DashboardToolbar({ onClearCanvas, onSaveLayout, canvasBackgroundColor, onCanvasBackgroundColorChange }: DashboardToolbarProps) {
|
||||
const [showColorPicker, setShowColorPicker] = useState(false);
|
||||
return (
|
||||
<div className="absolute top-5 left-5 bg-white p-3 rounded-lg shadow-lg z-50 flex gap-3">
|
||||
<div className="absolute top-5 left-5 bg-background p-3 rounded-lg shadow-lg z-50 flex gap-3">
|
||||
<button
|
||||
onClick={onClearCanvas}
|
||||
className="
|
||||
px-4 py-2 border border-gray-300 bg-white rounded-md
|
||||
text-sm font-medium text-gray-700
|
||||
hover:bg-gray-50 hover:border-gray-400
|
||||
px-4 py-2 border border-border bg-background rounded-md
|
||||
text-sm font-medium text-foreground
|
||||
hover:bg-muted hover:border-border/80
|
||||
transition-colors duration-200
|
||||
"
|
||||
>
|
||||
|
|
@ -32,9 +32,9 @@ export function DashboardToolbar({ onClearCanvas, onSaveLayout, canvasBackground
|
|||
<button
|
||||
onClick={onSaveLayout}
|
||||
className="
|
||||
px-4 py-2 border border-gray-300 bg-white rounded-md
|
||||
text-sm font-medium text-gray-700
|
||||
hover:bg-gray-50 hover:border-gray-400
|
||||
px-4 py-2 border border-border bg-background rounded-md
|
||||
text-sm font-medium text-foreground
|
||||
hover:bg-muted hover:border-border/80
|
||||
transition-colors duration-200
|
||||
"
|
||||
>
|
||||
|
|
@ -46,36 +46,36 @@ export function DashboardToolbar({ onClearCanvas, onSaveLayout, canvasBackground
|
|||
<button
|
||||
onClick={() => setShowColorPicker(!showColorPicker)}
|
||||
className="
|
||||
px-4 py-2 border border-gray-300 bg-white rounded-md
|
||||
text-sm font-medium text-gray-700
|
||||
hover:bg-gray-50 hover:border-gray-400
|
||||
px-4 py-2 border border-border bg-background rounded-md
|
||||
text-sm font-medium text-foreground
|
||||
hover:bg-muted hover:border-border/80
|
||||
transition-colors duration-200
|
||||
flex items-center gap-2
|
||||
"
|
||||
>
|
||||
🎨 캔버스 색상
|
||||
<div
|
||||
className="w-4 h-4 rounded border border-gray-300"
|
||||
className="w-4 h-4 rounded border border-border"
|
||||
style={{ backgroundColor: canvasBackgroundColor }}
|
||||
/>
|
||||
</button>
|
||||
|
||||
{/* 색상 선택 패널 */}
|
||||
{showColorPicker && (
|
||||
<div className="absolute top-full left-0 mt-2 bg-white p-4 rounded-lg shadow-xl z-50 border border-gray-200 w-[280px]">
|
||||
<div className="absolute top-full left-0 mt-2 bg-background p-4 rounded-lg shadow-xl z-50 border border-border w-[280px]">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<input
|
||||
type="color"
|
||||
value={canvasBackgroundColor}
|
||||
onChange={(e) => onCanvasBackgroundColorChange(e.target.value)}
|
||||
className="h-10 w-16 border border-gray-300 rounded cursor-pointer"
|
||||
className="h-10 w-16 border border-border rounded cursor-pointer"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={canvasBackgroundColor}
|
||||
onChange={(e) => onCanvasBackgroundColorChange(e.target.value)}
|
||||
placeholder="#ffffff"
|
||||
className="flex-1 px-2 py-1 text-sm border border-gray-300 rounded"
|
||||
className="flex-1 px-2 py-1 text-sm border border-border rounded"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -89,7 +89,7 @@ export function DashboardToolbar({ onClearCanvas, onSaveLayout, canvasBackground
|
|||
<button
|
||||
key={color}
|
||||
onClick={() => onCanvasBackgroundColorChange(color)}
|
||||
className={`h-8 rounded border-2 ${canvasBackgroundColor === color ? 'border-blue-500 ring-2 ring-blue-200' : 'border-gray-300'}`}
|
||||
className={`h-8 rounded border-2 ${canvasBackgroundColor === color ? 'border-primary ring-2 ring-primary/20' : 'border-border'}`}
|
||||
style={{ backgroundColor: color }}
|
||||
title={color}
|
||||
/>
|
||||
|
|
@ -98,7 +98,7 @@ export function DashboardToolbar({ onClearCanvas, onSaveLayout, canvasBackground
|
|||
|
||||
<button
|
||||
onClick={() => setShowColorPicker(false)}
|
||||
className="w-full px-3 py-1.5 text-sm text-gray-600 border border-gray-300 rounded hover:bg-gray-50"
|
||||
className="w-full px-3 py-1.5 text-sm text-foreground border border-border rounded hover:bg-muted"
|
||||
>
|
||||
닫기
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -265,13 +265,13 @@ export function DashboardTopMenu({
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-16 items-center justify-between border-b bg-white px-6 shadow-sm">
|
||||
<div className="flex h-16 items-center justify-between border-b bg-background px-6 shadow-sm">
|
||||
{/* 좌측: 대시보드 제목 */}
|
||||
<div className="flex items-center gap-4">
|
||||
{dashboardTitle && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-lg font-semibold text-gray-900">{dashboardTitle}</span>
|
||||
<span className="rounded bg-blue-100 px-2 py-0.5 text-xs font-medium text-blue-700">편집 중</span>
|
||||
<span className="text-lg font-semibold text-foreground">{dashboardTitle}</span>
|
||||
<span className="rounded bg-primary/10 px-2 py-0.5 text-xs font-medium text-primary">편집 중</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -287,7 +287,7 @@ export function DashboardTopMenu({
|
|||
/>
|
||||
)}
|
||||
|
||||
<div className="h-6 w-px bg-gray-300" />
|
||||
<div className="h-6 w-px bg-border" />
|
||||
|
||||
{/* 배경색 선택 */}
|
||||
{onBackgroundColorChange && (
|
||||
|
|
@ -295,7 +295,7 @@ export function DashboardTopMenu({
|
|||
<PopoverTrigger asChild>
|
||||
<Button variant="outline" size="sm" className="gap-2">
|
||||
<Palette className="h-4 w-4" />
|
||||
<div className="h-4 w-4 rounded border border-gray-300" style={{ backgroundColor }} />
|
||||
<div className="h-4 w-4 rounded border border-border" style={{ backgroundColor }} />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="z-[99999] w-64">
|
||||
|
|
@ -349,7 +349,7 @@ export function DashboardTopMenu({
|
|||
</Popover>
|
||||
)}
|
||||
|
||||
<div className="h-6 w-px bg-gray-300" />
|
||||
<div className="h-6 w-px bg-border" />
|
||||
|
||||
{/* 차트 선택 */}
|
||||
<Select value={chartValue} onValueChange={handleChartSelect}>
|
||||
|
|
@ -417,7 +417,7 @@ export function DashboardTopMenu({
|
|||
|
||||
{/* 우측: 액션 버튼 */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" size="sm" onClick={onClearCanvas} className="gap-2 text-red-600 hover:text-red-700">
|
||||
<Button variant="outline" size="sm" onClick={onClearCanvas} className="gap-2 text-destructive hover:text-destructive">
|
||||
<Trash2 className="h-4 w-4" />
|
||||
초기화
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -49,9 +49,9 @@ export function DateFilterPanel({ config, dateColumns, onChange }: DateFilterPan
|
|||
<Card className="p-4">
|
||||
<div className="flex cursor-pointer items-center justify-between" onClick={() => setIsExpanded(!isExpanded)}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Calendar className="h-4 w-4 text-blue-600" />
|
||||
<Label className="cursor-pointer text-sm font-medium text-gray-700">데이터 필터 (선택)</Label>
|
||||
{dateFilter.enabled && <span className="rounded bg-blue-100 px-2 py-0.5 text-xs text-blue-700">활성</span>}
|
||||
<Calendar className="h-4 w-4 text-primary" />
|
||||
<Label className="cursor-pointer text-sm font-medium text-foreground">데이터 필터 (선택)</Label>
|
||||
{dateFilter.enabled && <span className="rounded bg-primary/10 px-2 py-0.5 text-xs text-primary">활성</span>}
|
||||
</div>
|
||||
{isExpanded ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
|
||||
</div>
|
||||
|
|
@ -81,7 +81,7 @@ export function DateFilterPanel({ config, dateColumns, onChange }: DateFilterPan
|
|||
<>
|
||||
{/* 날짜 컬럼 선택 */}
|
||||
<div>
|
||||
<Label className="mb-2 text-sm font-medium text-gray-700">날짜 컬럼</Label>
|
||||
<Label className="mb-2 text-sm font-medium text-foreground">날짜 컬럼</Label>
|
||||
<Select
|
||||
value={dateFilter.dateColumn || ""}
|
||||
onValueChange={(value) =>
|
||||
|
|
@ -104,12 +104,12 @@ export function DateFilterPanel({ config, dateColumns, onChange }: DateFilterPan
|
|||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="mt-1 text-xs text-gray-500">감지된 날짜 컬럼: {dateColumns.join(", ")}</p>
|
||||
<p className="mt-1 text-xs text-muted-foreground">감지된 날짜 컬럼: {dateColumns.join(", ")}</p>
|
||||
</div>
|
||||
|
||||
{/* 빠른 선택 */}
|
||||
<div>
|
||||
<Label className="mb-2 text-sm font-medium text-gray-700">빠른 선택</Label>
|
||||
<Label className="mb-2 text-sm font-medium text-foreground">빠른 선택</Label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
|
|
@ -149,7 +149,7 @@ export function DateFilterPanel({ config, dateColumns, onChange }: DateFilterPan
|
|||
{/* 직접 입력 */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label className="mb-2 text-sm font-medium text-gray-700">시작일</Label>
|
||||
<Label className="mb-2 text-sm font-medium text-foreground">시작일</Label>
|
||||
<Input
|
||||
type="date"
|
||||
value={dateFilter.startDate || ""}
|
||||
|
|
@ -165,7 +165,7 @@ export function DateFilterPanel({ config, dateColumns, onChange }: DateFilterPan
|
|||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="mb-2 text-sm font-medium text-gray-700">종료일</Label>
|
||||
<Label className="mb-2 text-sm font-medium text-foreground">종료일</Label>
|
||||
<Input
|
||||
type="date"
|
||||
value={dateFilter.endDate || ""}
|
||||
|
|
@ -184,7 +184,7 @@ export function DateFilterPanel({ config, dateColumns, onChange }: DateFilterPan
|
|||
|
||||
{/* 필터 정보 */}
|
||||
{dateFilter.startDate && dateFilter.endDate && (
|
||||
<div className="rounded-md bg-blue-50 p-3 text-sm text-blue-800">
|
||||
<div className="rounded-md bg-primary/10 p-3 text-sm text-primary">
|
||||
<strong>필터 적용:</strong> {dateFilter.dateColumn} 컬럼에서 {dateFilter.startDate}부터{" "}
|
||||
{dateFilter.endDate}까지 데이터를 가져옵니다.
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -247,7 +247,7 @@ export function ElementConfigModal({ element, isOpen, onClose, onSave, onPreview
|
|||
return (
|
||||
<div className="fixed inset-0 z-[9999] flex items-center justify-center bg-black/50 backdrop-blur-sm">
|
||||
<div
|
||||
className={`flex flex-col rounded-xl border bg-white shadow-2xl ${
|
||||
className={`flex flex-col rounded-xl border bg-background shadow-2xl ${
|
||||
currentStep === 1 && !isSimpleWidget ? "h-auto max-h-[70vh] w-full max-w-3xl" : "h-[85vh] w-full max-w-5xl"
|
||||
}`}
|
||||
>
|
||||
|
|
@ -255,7 +255,7 @@ export function ElementConfigModal({ element, isOpen, onClose, onSave, onPreview
|
|||
<div className="border-b p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex-1">
|
||||
<h2 className="text-xl font-semibold text-gray-900">{element.title} 설정</h2>
|
||||
<h2 className="text-xl font-semibold text-foreground">{element.title} 설정</h2>
|
||||
</div>
|
||||
<Button variant="ghost" size="icon" onClick={onClose} className="h-8 w-8">
|
||||
<X className="h-5 w-5" />
|
||||
|
|
@ -264,7 +264,7 @@ export function ElementConfigModal({ element, isOpen, onClose, onSave, onPreview
|
|||
|
||||
{/* 커스텀 제목 입력 */}
|
||||
<div className="mt-4">
|
||||
<label className="mb-1 block text-sm font-medium text-gray-700">위젯 제목 (선택사항)</label>
|
||||
<label className="mb-1 block text-sm font-medium text-foreground">위젯 제목 (선택사항)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={customTitle}
|
||||
|
|
@ -274,9 +274,9 @@ export function ElementConfigModal({ element, isOpen, onClose, onSave, onPreview
|
|||
e.stopPropagation();
|
||||
}}
|
||||
placeholder="예: 정비 일정 목록, 창고 위치 현황 등 (비워두면 자동 생성)"
|
||||
className="focus:border-primary focus:ring-primary w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:ring-1 focus:outline-none"
|
||||
className="focus:border-primary focus:ring-primary w-full rounded-md border border-border px-3 py-2 text-sm focus:ring-1 focus:outline-none"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-gray-500">
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
비워두면 테이블명으로 자동 생성됩니다 (예: "maintenance_schedules 목록")
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -288,9 +288,9 @@ export function ElementConfigModal({ element, isOpen, onClose, onSave, onPreview
|
|||
id="showHeader"
|
||||
checked={showHeader}
|
||||
onChange={(e) => setShowHeader(e.target.checked)}
|
||||
className="text-primary focus:ring-primary h-4 w-4 rounded border-gray-300"
|
||||
className="text-primary focus:ring-primary h-4 w-4 rounded border-border"
|
||||
/>
|
||||
<label htmlFor="showHeader" className="text-sm font-medium text-gray-700">
|
||||
<label htmlFor="showHeader" className="text-sm font-medium text-foreground">
|
||||
위젯 헤더 표시 (제목 + 새로고침 버튼)
|
||||
</label>
|
||||
</div>
|
||||
|
|
@ -298,9 +298,9 @@ export function ElementConfigModal({ element, isOpen, onClose, onSave, onPreview
|
|||
|
||||
{/* 진행 상황 표시 - 간단한 위젯과 헤더 전용 위젯은 표시 안 함 */}
|
||||
{!isSimpleWidget && !isHeaderOnlyWidget && (
|
||||
<div className="border-b bg-gray-50 px-6 py-4">
|
||||
<div className="border-b bg-muted px-6 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-sm font-medium text-gray-700">
|
||||
<div className="text-sm font-medium text-foreground">
|
||||
단계 {currentStep} / 2: {currentStep === 1 ? "데이터 소스 선택" : "데이터 설정 및 차트 설정"}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -356,9 +356,9 @@ export function ElementConfigModal({ element, isOpen, onClose, onSave, onPreview
|
|||
onConfigChange={handleChartConfigChange}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-full items-center justify-center rounded-lg border-2 border-dashed border-gray-300 bg-gray-50 p-8 text-center">
|
||||
<div className="flex h-full items-center justify-center rounded-lg border-2 border-dashed border-border bg-muted p-8 text-center">
|
||||
<div>
|
||||
<div className="mt-1 text-xs text-gray-500">데이터를 가져온 후 지도 설정이 표시됩니다</div>
|
||||
<div className="mt-1 text-xs text-muted-foreground">데이터를 가져온 후 지도 설정이 표시됩니다</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
@ -373,9 +373,9 @@ export function ElementConfigModal({ element, isOpen, onClose, onSave, onPreview
|
|||
query={dataSource.query}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-full items-center justify-center rounded-lg border-2 border-dashed border-gray-300 bg-gray-50 p-8 text-center">
|
||||
<div className="flex h-full items-center justify-center rounded-lg border-2 border-dashed border-border bg-muted p-8 text-center">
|
||||
<div>
|
||||
<div className="mt-1 text-xs text-gray-500">데이터를 가져온 후 차트 설정이 표시됩니다</div>
|
||||
<div className="mt-1 text-xs text-muted-foreground">데이터를 가져온 후 차트 설정이 표시됩니다</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -387,7 +387,7 @@ export function ElementConfigModal({ element, isOpen, onClose, onSave, onPreview
|
|||
)}
|
||||
|
||||
{/* 모달 푸터 */}
|
||||
<div className="flex items-center justify-between border-t bg-gray-50 p-6">
|
||||
<div className="flex items-center justify-between border-t bg-muted p-6">
|
||||
<div>{queryResult && <Badge variant="default">{queryResult.rows.length}개 데이터 로드됨</Badge>}</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
|
|
|
|||
|
|
@ -291,31 +291,31 @@ export function ElementConfigSidebar({ element, isOpen, onClose, onApply }: Elem
|
|||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"fixed top-14 left-0 z-[100] flex h-[calc(100vh-3.5rem)] w-72 flex-col bg-gray-50 transition-transform duration-300 ease-in-out",
|
||||
"fixed top-14 left-0 z-[100] flex h-[calc(100vh-3.5rem)] w-72 flex-col bg-muted transition-transform duration-300 ease-in-out",
|
||||
isOpen ? "translate-x-0" : "translate-x-[-100%]",
|
||||
)}
|
||||
>
|
||||
{/* 헤더 */}
|
||||
<div className="flex items-center justify-between bg-white px-3 py-2 shadow-sm">
|
||||
<div className="flex items-center justify-between bg-background px-3 py-2 shadow-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="bg-primary/10 flex h-6 w-6 items-center justify-center rounded">
|
||||
<span className="text-primary text-xs font-bold">⚙</span>
|
||||
</div>
|
||||
<span className="text-xs font-semibold text-gray-900">{element.title}</span>
|
||||
<span className="text-xs font-semibold text-foreground">{element.title}</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="flex h-6 w-6 items-center justify-center rounded transition-colors hover:bg-gray-100"
|
||||
className="flex h-6 w-6 items-center justify-center rounded transition-colors hover:bg-muted"
|
||||
>
|
||||
<X className="h-3.5 w-3.5 text-gray-500" />
|
||||
<X className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 본문: 스크롤 가능 영역 */}
|
||||
<div className="flex-1 overflow-y-auto p-3">
|
||||
{/* 기본 설정 카드 */}
|
||||
<div className="mb-3 rounded-lg bg-white p-3 shadow-sm">
|
||||
<div className="mb-2 text-[10px] font-semibold tracking-wide text-gray-500 uppercase">기본 설정</div>
|
||||
<div className="mb-3 rounded-lg bg-background p-3 shadow-sm">
|
||||
<div className="mb-2 text-[10px] font-semibold tracking-wide text-muted-foreground uppercase">기본 설정</div>
|
||||
<div className="space-y-2">
|
||||
{/* 커스텀 제목 입력 */}
|
||||
<div>
|
||||
|
|
@ -325,20 +325,20 @@ export function ElementConfigSidebar({ element, isOpen, onClose, onApply }: Elem
|
|||
onChange={(e) => setCustomTitle(e.target.value)}
|
||||
onKeyDown={(e) => e.stopPropagation()}
|
||||
placeholder="위젯 제목"
|
||||
className="focus:border-primary focus:ring-primary/20 h-8 w-full rounded border border-gray-200 bg-gray-50 px-2 text-xs placeholder:text-gray-400 focus:bg-white focus:ring-1 focus:outline-none"
|
||||
className="focus:border-primary focus:ring-primary/20 h-8 w-full rounded border border-border bg-muted px-2 text-xs placeholder:text-muted-foreground focus:bg-background focus:ring-1 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 헤더 표시 옵션 */}
|
||||
<label className="flex cursor-pointer items-center gap-2 rounded border border-gray-200 bg-gray-50 px-2 py-1.5 transition-colors hover:border-gray-300">
|
||||
<label className="flex cursor-pointer items-center gap-2 rounded border border-border bg-muted px-2 py-1.5 transition-colors hover:border-border">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="showHeader"
|
||||
checked={showHeader}
|
||||
onChange={(e) => setShowHeader(e.target.checked)}
|
||||
className="text-primary focus:ring-primary h-3 w-3 rounded border-gray-300"
|
||||
className="text-primary focus:ring-primary h-3 w-3 rounded border-border"
|
||||
/>
|
||||
<span className="text-xs text-gray-700">헤더 표시</span>
|
||||
<span className="text-xs text-foreground">헤더 표시</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -346,7 +346,7 @@ export function ElementConfigSidebar({ element, isOpen, onClose, onApply }: Elem
|
|||
{/* 다중 데이터 소스 위젯 */}
|
||||
{isMultiDataSourceWidget && (
|
||||
<>
|
||||
<div className="rounded-lg bg-white p-3 shadow-sm">
|
||||
<div className="rounded-lg bg-background p-3 shadow-sm">
|
||||
<MultiDataSourceConfig
|
||||
dataSources={dataSources}
|
||||
onChange={setDataSources}
|
||||
|
|
@ -372,11 +372,11 @@ export function ElementConfigSidebar({ element, isOpen, onClose, onApply }: Elem
|
|||
|
||||
{/* 지도 위젯: 타일맵 URL 설정 */}
|
||||
{element.subtype === "map-summary-v2" && (
|
||||
<div className="rounded-lg bg-white shadow-sm">
|
||||
<div className="rounded-lg bg-background shadow-sm">
|
||||
<details className="group">
|
||||
<summary className="flex cursor-pointer items-center justify-between p-3 hover:bg-gray-50">
|
||||
<summary className="flex cursor-pointer items-center justify-between p-3 hover:bg-muted">
|
||||
<div>
|
||||
<div className="text-xs font-semibold tracking-wide text-gray-500 uppercase">
|
||||
<div className="text-xs font-semibold tracking-wide text-muted-foreground uppercase">
|
||||
타일맵 설정 (선택사항)
|
||||
</div>
|
||||
<div className="text-muted-foreground mt-0.5 text-[10px]">기본 VWorld 타일맵 사용 중</div>
|
||||
|
|
@ -403,11 +403,11 @@ export function ElementConfigSidebar({ element, isOpen, onClose, onApply }: Elem
|
|||
|
||||
{/* 차트 위젯: 차트 설정 */}
|
||||
{element.subtype === "chart" && (
|
||||
<div className="rounded-lg bg-white shadow-sm">
|
||||
<div className="rounded-lg bg-background shadow-sm">
|
||||
<details className="group" open>
|
||||
<summary className="flex cursor-pointer items-center justify-between p-3 hover:bg-gray-50">
|
||||
<summary className="flex cursor-pointer items-center justify-between p-3 hover:bg-muted">
|
||||
<div>
|
||||
<div className="text-xs font-semibold tracking-wide text-gray-500 uppercase">차트 설정</div>
|
||||
<div className="text-xs font-semibold tracking-wide text-muted-foreground uppercase">차트 설정</div>
|
||||
<div className="text-muted-foreground mt-0.5 text-[10px]">
|
||||
{testResults.size > 0
|
||||
? `${testResults.size}개 데이터 소스 • X축, Y축, 차트 타입 설정`
|
||||
|
|
@ -439,24 +439,24 @@ export function ElementConfigSidebar({ element, isOpen, onClose, onApply }: Elem
|
|||
|
||||
{/* 헤더 전용 위젯이 아닐 때만 데이터 소스 표시 */}
|
||||
{!isHeaderOnlyWidget && !isMultiDataSourceWidget && (
|
||||
<div className="rounded-lg bg-white p-3 shadow-sm">
|
||||
<div className="mb-2 text-[10px] font-semibold tracking-wide text-gray-500 uppercase">데이터 소스</div>
|
||||
<div className="rounded-lg bg-background p-3 shadow-sm">
|
||||
<div className="mb-2 text-[10px] font-semibold tracking-wide text-muted-foreground uppercase">데이터 소스</div>
|
||||
|
||||
<Tabs
|
||||
defaultValue={dataSource.type}
|
||||
onValueChange={(value) => handleDataSourceTypeChange(value as "database" | "api")}
|
||||
className="w-full"
|
||||
>
|
||||
<TabsList className="grid h-7 w-full grid-cols-2 bg-gray-100 p-0.5">
|
||||
<TabsList className="grid h-7 w-full grid-cols-2 bg-muted p-0.5">
|
||||
<TabsTrigger
|
||||
value="database"
|
||||
className="h-6 rounded text-[11px] data-[state=active]:bg-white data-[state=active]:shadow-sm"
|
||||
className="h-6 rounded text-[11px] data-[state=active]:bg-background data-[state=active]:shadow-sm"
|
||||
>
|
||||
데이터베이스
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="api"
|
||||
className="h-6 rounded text-[11px] data-[state=active]:bg-white data-[state=active]:shadow-sm"
|
||||
className="h-6 rounded text-[11px] data-[state=active]:bg-background data-[state=active]:shadow-sm"
|
||||
>
|
||||
REST API
|
||||
</TabsTrigger>
|
||||
|
|
@ -552,9 +552,9 @@ export function ElementConfigSidebar({ element, isOpen, onClose, onApply }: Elem
|
|||
|
||||
{/* 데이터 로드 상태 */}
|
||||
{queryResult && (
|
||||
<div className="mt-2 flex items-center gap-1.5 rounded bg-green-50 px-2 py-1">
|
||||
<div className="h-1.5 w-1.5 rounded-full bg-green-500" />
|
||||
<span className="text-[10px] font-medium text-green-700">
|
||||
<div className="mt-2 flex items-center gap-1.5 rounded bg-success/10 px-2 py-1">
|
||||
<div className="h-1.5 w-1.5 rounded-full bg-success" />
|
||||
<span className="text-[10px] font-medium text-success">
|
||||
{queryResult.rows.length}개 데이터 로드됨
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -564,10 +564,10 @@ export function ElementConfigSidebar({ element, isOpen, onClose, onApply }: Elem
|
|||
</div>
|
||||
|
||||
{/* 푸터: 적용 버튼 */}
|
||||
<div className="flex gap-2 bg-white p-2 shadow-[0_-2px_8px_rgba(0,0,0,0.05)]">
|
||||
<div className="flex gap-2 bg-background p-2 shadow-[0_-2px_8px_rgba(0,0,0,0.05)]">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="flex-1 rounded bg-gray-100 py-2 text-xs font-medium text-gray-700 transition-colors hover:bg-gray-200"
|
||||
className="flex-1 rounded bg-muted py-2 text-xs font-medium text-foreground transition-colors hover:bg-muted"
|
||||
>
|
||||
취소
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -123,9 +123,9 @@ export function MapTestConfigPanel({ config, queryResult, onConfigChange }: MapT
|
|||
<div className="space-y-3">
|
||||
{/* 타일맵 URL 설정 (외부 커넥션 또는 직접 입력) */}
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-medium text-gray-700">
|
||||
<Label className="text-xs font-medium text-foreground">
|
||||
타일맵 소스 (지도 배경)
|
||||
<span className="text-red-500 ml-1">*</span>
|
||||
<span className="text-destructive ml-1">*</span>
|
||||
</Label>
|
||||
|
||||
{/* 외부 커넥션 선택 */}
|
||||
|
|
@ -140,7 +140,7 @@ export function MapTestConfigPanel({ config, queryResult, onConfigChange }: MapT
|
|||
}
|
||||
}
|
||||
}}
|
||||
className="w-full px-2 py-1.5 border border-gray-300 rounded-md text-xs h-8 bg-white"
|
||||
className="w-full px-2 py-1.5 border border-border rounded-md text-xs h-8 bg-background"
|
||||
>
|
||||
<option value="">저장된 커넥션 선택</option>
|
||||
{connections.map((conn) => (
|
||||
|
|
@ -167,9 +167,9 @@ export function MapTestConfigPanel({ config, queryResult, onConfigChange }: MapT
|
|||
{/* 타일맵 소스 목록 */}
|
||||
{/* <div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="block text-xs font-medium text-gray-700">
|
||||
<label className="block text-xs font-medium text-foreground">
|
||||
타일맵 소스 (REST API)
|
||||
<span className="text-red-500 ml-1">*</span>
|
||||
<span className="text-destructive ml-1">*</span>
|
||||
</label>
|
||||
<Button
|
||||
type="button"
|
||||
|
|
@ -184,14 +184,14 @@ export function MapTestConfigPanel({ config, queryResult, onConfigChange }: MapT
|
|||
</div>
|
||||
|
||||
{tileMapSources.map((source, index) => (
|
||||
<div key={source.id} className="space-y-2 rounded-lg border border-gray-200 bg-gray-50 p-3">
|
||||
<div key={source.id} className="space-y-2 rounded-lg border border-border bg-muted p-3">
|
||||
<div className="space-y-1">
|
||||
<label className="block text-xs font-medium text-gray-600">
|
||||
<label className="block text-xs font-medium text-foreground">
|
||||
외부 커넥션 선택 (선택사항)
|
||||
</label>
|
||||
<select
|
||||
onChange={(e) => loadFromConnection(source.id, e.target.value)}
|
||||
className="w-full px-2 py-1.5 border border-gray-300 rounded-md text-xs h-8 bg-white"
|
||||
className="w-full px-2 py-1.5 border border-border rounded-md text-xs h-8 bg-background"
|
||||
>
|
||||
<option value="">직접 입력 또는 커넥션 선택</option>
|
||||
{connections.map((conn) => (
|
||||
|
|
@ -217,7 +217,7 @@ export function MapTestConfigPanel({ config, queryResult, onConfigChange }: MapT
|
|||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => removeTileMapSource(source.id)}
|
||||
className="h-8 w-8 text-gray-500 hover:text-red-600"
|
||||
className="h-8 w-8 text-muted-foreground hover:text-destructive"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
|
|
@ -233,7 +233,7 @@ export function MapTestConfigPanel({ config, queryResult, onConfigChange }: MapT
|
|||
|
||||
{/* 지도 제목 */}
|
||||
{/* <div className="space-y-1.5">
|
||||
<label className="block text-xs font-medium text-gray-700">지도 제목</label>
|
||||
<label className="block text-xs font-medium text-foreground">지도 제목</label>
|
||||
<Input
|
||||
type="text"
|
||||
value={currentConfig.title || ''}
|
||||
|
|
@ -245,7 +245,7 @@ export function MapTestConfigPanel({ config, queryResult, onConfigChange }: MapT
|
|||
|
||||
{/* 구분선 */}
|
||||
{/* <div className="border-t pt-3">
|
||||
<h5 className="text-xs font-semibold text-gray-700 mb-2">📍 마커 데이터 설정 (선택사항)</h5>
|
||||
<h5 className="text-xs font-semibold text-foreground mb-2">📍 마커 데이터 설정 (선택사항)</h5>
|
||||
<p className="text-xs text-muted-foreground mb-3">
|
||||
데이터 소스 탭에서 API 또는 데이터베이스를 연결하면 마커를 표시할 수 있습니다.
|
||||
</p>
|
||||
|
|
@ -253,8 +253,8 @@ export function MapTestConfigPanel({ config, queryResult, onConfigChange }: MapT
|
|||
|
||||
{/* 쿼리 결과가 없을 때 */}
|
||||
{/* {!queryResult && (
|
||||
<div className="p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
|
||||
<div className="text-yellow-800 text-xs">
|
||||
<div className="p-3 bg-warning/10 border border-warning rounded-lg">
|
||||
<div className="text-warning text-xs">
|
||||
💡 데이터 소스를 연결하고 쿼리를 실행하면 마커 설정이 가능합니다.
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -265,13 +265,13 @@ export function MapTestConfigPanel({ config, queryResult, onConfigChange }: MapT
|
|||
<>
|
||||
{/* 위도 컬럼 설정 */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="block text-xs font-medium text-gray-700">
|
||||
<label className="block text-xs font-medium text-foreground">
|
||||
위도 컬럼 (Latitude)
|
||||
</label>
|
||||
<select
|
||||
value={currentConfig.latitudeColumn || ''}
|
||||
onChange={(e) => updateConfig({ latitudeColumn: e.target.value })}
|
||||
className="w-full px-2 py-1.5 border border-gray-300 rounded-lg text-xs"
|
||||
className="w-full px-2 py-1.5 border border-border rounded-lg text-xs"
|
||||
>
|
||||
<option value="">선택하세요</option>
|
||||
{availableColumns.map((col) => (
|
||||
|
|
@ -284,13 +284,13 @@ export function MapTestConfigPanel({ config, queryResult, onConfigChange }: MapT
|
|||
|
||||
{/* 경도 컬럼 설정 */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="block text-xs font-medium text-gray-700">
|
||||
<label className="block text-xs font-medium text-foreground">
|
||||
경도 컬럼 (Longitude)
|
||||
</label>
|
||||
<select
|
||||
value={currentConfig.longitudeColumn || ''}
|
||||
onChange={(e) => updateConfig({ longitudeColumn: e.target.value })}
|
||||
className="w-full px-2 py-1.5 border border-gray-300 rounded-lg text-xs"
|
||||
className="w-full px-2 py-1.5 border border-border rounded-lg text-xs"
|
||||
>
|
||||
<option value="">선택하세요</option>
|
||||
{availableColumns.map((col) => (
|
||||
|
|
@ -303,13 +303,13 @@ export function MapTestConfigPanel({ config, queryResult, onConfigChange }: MapT
|
|||
|
||||
{/* 라벨 컬럼 (선택사항) */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="block text-xs font-medium text-gray-700">
|
||||
<label className="block text-xs font-medium text-foreground">
|
||||
라벨 컬럼 (마커 표시명)
|
||||
</label>
|
||||
<select
|
||||
value={currentConfig.labelColumn || ''}
|
||||
onChange={(e) => updateConfig({ labelColumn: e.target.value })}
|
||||
className="w-full px-2 py-1.5 border border-gray-300 rounded-lg text-xs"
|
||||
className="w-full px-2 py-1.5 border border-border rounded-lg text-xs"
|
||||
>
|
||||
<option value="">선택하세요 (선택사항)</option>
|
||||
{availableColumns.map((col) => (
|
||||
|
|
@ -322,13 +322,13 @@ export function MapTestConfigPanel({ config, queryResult, onConfigChange }: MapT
|
|||
|
||||
{/* 상태 컬럼 (선택사항) */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="block text-xs font-medium text-gray-700">
|
||||
<label className="block text-xs font-medium text-foreground">
|
||||
상태 컬럼 (마커 색상)
|
||||
</label>
|
||||
<select
|
||||
value={currentConfig.statusColumn || ''}
|
||||
onChange={(e) => updateConfig({ statusColumn: e.target.value })}
|
||||
className="w-full px-2 py-1.5 border border-gray-300 rounded-lg text-xs"
|
||||
className="w-full px-2 py-1.5 border border-border rounded-lg text-xs"
|
||||
>
|
||||
<option value="">선택하세요 (선택사항)</option>
|
||||
{availableColumns.map((col) => (
|
||||
|
|
@ -343,8 +343,8 @@ export function MapTestConfigPanel({ config, queryResult, onConfigChange }: MapT
|
|||
|
||||
{/* 기상특보 데이터 안내 */}
|
||||
{queryResult && isWeatherAlertData && (
|
||||
<div className="p-3 bg-blue-50 border border-blue-200 rounded-lg">
|
||||
<div className="text-blue-800 text-xs">
|
||||
<div className="p-3 bg-primary/10 border border-primary rounded-lg">
|
||||
<div className="text-primary text-xs">
|
||||
🚨 기상특보 데이터가 감지되었습니다. 지역명(reg_ko)을 기준으로 자동으로 영역이 표시됩니다.
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -355,38 +355,38 @@ export function MapTestConfigPanel({ config, queryResult, onConfigChange }: MapT
|
|||
|
||||
{/* 날씨 정보 표시 옵션 */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="flex items-center gap-2 text-xs font-medium text-gray-700 cursor-pointer">
|
||||
<label className="flex items-center gap-2 text-xs font-medium text-foreground cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={currentConfig.showWeather || false}
|
||||
onChange={(e) => updateConfig({ showWeather: e.target.checked })}
|
||||
className="h-4 w-4 rounded border-gray-300 text-primary focus:ring-2 focus:ring-primary"
|
||||
className="h-4 w-4 rounded border-border text-primary focus:ring-2 focus:ring-primary"
|
||||
/>
|
||||
<span>날씨 정보 표시</span>
|
||||
</label>
|
||||
<p className="text-xs text-gray-500 ml-6">
|
||||
<p className="text-xs text-muted-foreground ml-6">
|
||||
마커 팝업에 해당 위치의 날씨 정보를 함께 표시합니다
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<label className="flex items-center gap-2 text-xs font-medium text-gray-700 cursor-pointer">
|
||||
<label className="flex items-center gap-2 text-xs font-medium text-foreground cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={currentConfig.showWeatherAlerts || false}
|
||||
onChange={(e) => updateConfig({ showWeatherAlerts: e.target.checked })}
|
||||
className="h-4 w-4 rounded border-gray-300 text-primary focus:ring-2 focus:ring-primary"
|
||||
className="h-4 w-4 rounded border-border text-primary focus:ring-2 focus:ring-primary"
|
||||
/>
|
||||
<span>기상특보 영역 표시</span>
|
||||
</label>
|
||||
<p className="text-xs text-gray-500 ml-6">
|
||||
<p className="text-xs text-muted-foreground ml-6">
|
||||
현재 발효 중인 기상특보(주의보/경보)를 지도에 색상 영역으로 표시합니다
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 설정 미리보기 */}
|
||||
<div className="p-3 bg-gray-50 rounded-lg">
|
||||
<div className="text-xs font-medium text-gray-700 mb-2">📋 설정 미리보기</div>
|
||||
<div className="p-3 bg-muted rounded-lg">
|
||||
<div className="text-xs font-medium text-foreground mb-2">📋 설정 미리보기</div>
|
||||
<div className="text-xs text-muted-foreground space-y-1">
|
||||
<div><strong>타일맵:</strong> {currentConfig.tileMapUrl ? '✅ 설정됨' : '❌ 미설정'}</div>
|
||||
<div><strong>위도:</strong> {currentConfig.latitudeColumn || '미설정'}</div>
|
||||
|
|
@ -403,8 +403,8 @@ export function MapTestConfigPanel({ config, queryResult, onConfigChange }: MapT
|
|||
|
||||
{/* 필수 필드 확인 */}
|
||||
{/* {!currentConfig.tileMapUrl && (
|
||||
<div className="p-3 bg-red-50 border border-red-200 rounded-lg">
|
||||
<div className="text-red-800 text-xs">
|
||||
<div className="p-3 bg-destructive/10 border border-destructive rounded-lg">
|
||||
<div className="text-destructive text-xs">
|
||||
⚠️ 타일맵 URL을 입력해야 지도가 표시됩니다.
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -177,7 +177,7 @@ export const MenuAssignmentModal: React.FC<MenuAssignmentModalProps> = ({
|
|||
<Label>할당할 메뉴 선택</Label>
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-4">
|
||||
<Loader2 className="h-6 w-6 animate-spin text-gray-400" />
|
||||
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
) : (
|
||||
<Select value={selectedMenuId} onValueChange={setSelectedMenuId}>
|
||||
|
|
|
|||
|
|
@ -302,8 +302,8 @@ export function MultiChartConfigPanel({
|
|||
|
||||
{/* 안내 메시지 */}
|
||||
{dataSourceConfigs.length > 0 && (
|
||||
<div className="rounded-lg bg-blue-50 p-3">
|
||||
<p className="text-xs text-blue-900">
|
||||
<div className="rounded-lg bg-primary/10 p-3">
|
||||
<p className="text-xs text-primary">
|
||||
{mergeMode ? (
|
||||
<>
|
||||
🔗 {dataSourceConfigs.length}개의 데이터 소스가 하나의 라인/바로 병합되어 표시됩니다.
|
||||
|
|
|
|||
|
|
@ -168,8 +168,8 @@ ORDER BY 하위부서수 DESC`,
|
|||
{/* 쿼리 에디터 헤더 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Database className="h-3.5 w-3.5 text-blue-600" />
|
||||
<h4 className="text-xs font-semibold text-gray-800">SQL 쿼리 에디터</h4>
|
||||
<Database className="h-3.5 w-3.5 text-primary" />
|
||||
<h4 className="text-xs font-semibold text-foreground">SQL 쿼리 에디터</h4>
|
||||
</div>
|
||||
<Button onClick={executeQuery} disabled={isExecuting || !query.trim()} size="sm" className="h-7 text-xs">
|
||||
{isExecuting ? (
|
||||
|
|
@ -188,7 +188,7 @@ ORDER BY 하위부서수 DESC`,
|
|||
|
||||
{/* 샘플 쿼리 아코디언 */}
|
||||
<Collapsible open={sampleQueryOpen} onOpenChange={setSampleQueryOpen}>
|
||||
<CollapsibleTrigger className="flex w-full items-center gap-1.5 rounded border border-gray-200 bg-gray-50 px-2 py-1.5 text-xs font-medium text-gray-700 transition-colors hover:bg-gray-100">
|
||||
<CollapsibleTrigger className="flex w-full items-center gap-1.5 rounded border border-border bg-muted px-2 py-1.5 text-xs font-medium text-foreground transition-colors hover:bg-muted">
|
||||
{sampleQueryOpen ? <ChevronDown className="h-3 w-3" /> : <ChevronRight className="h-3 w-3" />}
|
||||
샘플 쿼리
|
||||
</CollapsibleTrigger>
|
||||
|
|
@ -196,33 +196,33 @@ ORDER BY 하위부서수 DESC`,
|
|||
<div className="flex flex-wrap gap-1.5">
|
||||
<button
|
||||
onClick={() => insertSampleQuery("users")}
|
||||
className="flex items-center gap-1 rounded border border-gray-200 bg-white px-2 py-1 text-[11px] transition-colors hover:bg-gray-50"
|
||||
className="flex items-center gap-1 rounded border border-border bg-background px-2 py-1 text-[11px] transition-colors hover:bg-muted"
|
||||
>
|
||||
<Code className="h-3 w-3" />
|
||||
부서별 사용자
|
||||
</button>
|
||||
<button
|
||||
onClick={() => insertSampleQuery("dept")}
|
||||
className="flex items-center gap-1 rounded border border-gray-200 bg-white px-2 py-1 text-[11px] transition-colors hover:bg-gray-50"
|
||||
className="flex items-center gap-1 rounded border border-border bg-background px-2 py-1 text-[11px] transition-colors hover:bg-muted"
|
||||
>
|
||||
<Code className="h-3 w-3" />
|
||||
부서 정보
|
||||
</button>
|
||||
<button
|
||||
onClick={() => insertSampleQuery("usersByDate")}
|
||||
className="rounded border border-gray-200 bg-white px-2 py-1 text-[11px] transition-colors hover:bg-gray-50"
|
||||
className="rounded border border-border bg-background px-2 py-1 text-[11px] transition-colors hover:bg-muted"
|
||||
>
|
||||
월별 가입 추이
|
||||
</button>
|
||||
<button
|
||||
onClick={() => insertSampleQuery("usersByPosition")}
|
||||
className="rounded border border-gray-200 bg-white px-2 py-1 text-[11px] transition-colors hover:bg-gray-50"
|
||||
className="rounded border border-border bg-background px-2 py-1 text-[11px] transition-colors hover:bg-muted"
|
||||
>
|
||||
직급별 분포
|
||||
</button>
|
||||
<button
|
||||
onClick={() => insertSampleQuery("deptHierarchy")}
|
||||
className="rounded border border-gray-200 bg-white px-2 py-1 text-[11px] transition-colors hover:bg-gray-50"
|
||||
className="rounded border border-border bg-background px-2 py-1 text-[11px] transition-colors hover:bg-muted"
|
||||
>
|
||||
부서 계층
|
||||
</button>
|
||||
|
|
@ -300,15 +300,15 @@ ORDER BY 하위부서수 DESC`,
|
|||
{/* 쿼리 결과 미리보기 */}
|
||||
{queryResult && (
|
||||
<Card>
|
||||
<div className="border-b border-gray-200 bg-gray-50 px-2 py-1.5">
|
||||
<div className="border-b border-border bg-muted px-2 py-1.5">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-xs font-medium text-gray-700">쿼리 결과</span>
|
||||
<span className="text-xs font-medium text-foreground">쿼리 결과</span>
|
||||
<Badge variant="secondary" className="h-4 text-[10px]">
|
||||
{queryResult.rows.length}행
|
||||
</Badge>
|
||||
</div>
|
||||
<span className="text-[10px] text-gray-500">실행 시간: {queryResult.executionTime}ms</span>
|
||||
<span className="text-[10px] text-muted-foreground">실행 시간: {queryResult.executionTime}ms</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -339,13 +339,13 @@ ORDER BY 하위부서수 DESC`,
|
|||
</Table>
|
||||
|
||||
{queryResult.rows.length > 10 && (
|
||||
<div className="mt-2 text-center text-[10px] text-gray-500">
|
||||
<div className="mt-2 text-center text-[10px] text-muted-foreground">
|
||||
... 및 {queryResult.rows.length - 10}개 더 (미리보기는 10행까지만 표시)
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="py-6 text-center text-xs text-gray-500">결과가 없습니다.</div>
|
||||
<div className="py-6 text-center text-xs text-muted-foreground">결과가 없습니다.</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
|
|
|
|||
|
|
@ -122,9 +122,9 @@ export function ResolutionSelector({ value, onChange, currentScreenResolution }:
|
|||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<Monitor className="h-4 w-4 text-gray-500" />
|
||||
<Monitor className="h-4 w-4 text-muted-foreground" />
|
||||
<Select value={value} onValueChange={(v) => onChange(v as Resolution)}>
|
||||
<SelectTrigger className={`w-[180px] ${isTooLarge ? "border-orange-500" : ""}`}>
|
||||
<SelectTrigger className={`w-[180px] ${isTooLarge ? "border-warning" : ""}`}>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="z-[99999]">
|
||||
|
|
@ -133,31 +133,31 @@ export function ResolutionSelector({ value, onChange, currentScreenResolution }:
|
|||
<SelectItem value="hd">
|
||||
<div className="flex items-center gap-2">
|
||||
<span>HD</span>
|
||||
<span className="text-xs text-gray-500">1280x720</span>
|
||||
<span className="text-xs text-muted-foreground">1280x720</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="fhd">
|
||||
<div className="flex items-center gap-2">
|
||||
<span>Full HD</span>
|
||||
<span className="text-xs text-gray-500">1920x1080</span>
|
||||
<span className="text-xs text-muted-foreground">1920x1080</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="qhd">
|
||||
<div className="flex items-center gap-2">
|
||||
<span>QHD</span>
|
||||
<span className="text-xs text-gray-500">2560x1440</span>
|
||||
<span className="text-xs text-muted-foreground">2560x1440</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="uhd">
|
||||
<div className="flex items-center gap-2">
|
||||
<span>4K UHD</span>
|
||||
<span className="text-xs text-gray-500">3840x2160</span>
|
||||
<span className="text-xs text-muted-foreground">3840x2160</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{isTooLarge && <span className="text-xs text-orange-600">⚠️ 현재 화면보다 큽니다</span>}
|
||||
{isTooLarge && <span className="text-xs text-warning">⚠️ 현재 화면보다 큽니다</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -88,12 +88,12 @@ export function VehicleMapConfigPanel({ config, queryResult, onConfigChange }: V
|
|||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<h4 className="text-xs font-semibold text-gray-800">🗺️ 지도 설정</h4>
|
||||
<h4 className="text-xs font-semibold text-foreground">🗺️ 지도 설정</h4>
|
||||
|
||||
{/* 쿼리 결과가 없을 때 */}
|
||||
{!queryResult && (
|
||||
<div className="rounded-lg border border-yellow-200 bg-yellow-50 p-3">
|
||||
<div className="text-xs text-yellow-800">
|
||||
<div className="rounded-lg border border-warning bg-warning/10 p-3">
|
||||
<div className="text-xs text-warning">
|
||||
💡 먼저 SQL 쿼리를 실행하여 데이터를 가져온 후 지도를 설정할 수 있습니다.
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -104,26 +104,26 @@ export function VehicleMapConfigPanel({ config, queryResult, onConfigChange }: V
|
|||
<>
|
||||
{/* 지도 제목 */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="block text-xs font-medium text-gray-700">지도 제목</label>
|
||||
<label className="block text-xs font-medium text-foreground">지도 제목</label>
|
||||
<input
|
||||
type="text"
|
||||
value={currentConfig.title || ""}
|
||||
onChange={(e) => updateConfig({ title: e.target.value })}
|
||||
placeholder="차량 위치 지도"
|
||||
className="w-full rounded-lg border border-gray-300 px-2 py-1.5 text-xs"
|
||||
className="w-full rounded-lg border border-border px-2 py-1.5 text-xs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 위도 컬럼 설정 */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="block text-xs font-medium text-gray-700">
|
||||
<label className="block text-xs font-medium text-foreground">
|
||||
위도 컬럼 (Latitude)
|
||||
<span className="ml-1 text-red-500">*</span>
|
||||
<span className="ml-1 text-destructive">*</span>
|
||||
</label>
|
||||
<select
|
||||
value={currentConfig.latitudeColumn || ""}
|
||||
onChange={(e) => updateConfig({ latitudeColumn: e.target.value })}
|
||||
className="w-full rounded-lg border border-gray-300 px-2 py-1.5 text-xs"
|
||||
className="w-full rounded-lg border border-border px-2 py-1.5 text-xs"
|
||||
>
|
||||
<option value="">선택하세요</option>
|
||||
{availableColumns.map((col) => (
|
||||
|
|
@ -136,14 +136,14 @@ export function VehicleMapConfigPanel({ config, queryResult, onConfigChange }: V
|
|||
|
||||
{/* 경도 컬럼 설정 */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="block text-xs font-medium text-gray-700">
|
||||
<label className="block text-xs font-medium text-foreground">
|
||||
경도 컬럼 (Longitude)
|
||||
<span className="ml-1 text-red-500">*</span>
|
||||
<span className="ml-1 text-destructive">*</span>
|
||||
</label>
|
||||
<select
|
||||
value={currentConfig.longitudeColumn || ""}
|
||||
onChange={(e) => updateConfig({ longitudeColumn: e.target.value })}
|
||||
className="w-full rounded-lg border border-gray-300 px-2 py-1.5 text-xs"
|
||||
className="w-full rounded-lg border border-border px-2 py-1.5 text-xs"
|
||||
>
|
||||
<option value="">선택하세요</option>
|
||||
{availableColumns.map((col) => (
|
||||
|
|
@ -156,11 +156,11 @@ export function VehicleMapConfigPanel({ config, queryResult, onConfigChange }: V
|
|||
|
||||
{/* 라벨 컬럼 (선택사항) */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="block text-xs font-medium text-gray-700">라벨 컬럼 (마커 표시명)</label>
|
||||
<label className="block text-xs font-medium text-foreground">라벨 컬럼 (마커 표시명)</label>
|
||||
<select
|
||||
value={currentConfig.labelColumn || ""}
|
||||
onChange={(e) => updateConfig({ labelColumn: e.target.value })}
|
||||
className="w-full rounded-lg border border-gray-300 px-2 py-1.5 text-xs"
|
||||
className="w-full rounded-lg border border-border px-2 py-1.5 text-xs"
|
||||
>
|
||||
<option value="">선택하세요 (선택사항)</option>
|
||||
{availableColumns.map((col) => (
|
||||
|
|
@ -173,19 +173,19 @@ export function VehicleMapConfigPanel({ config, queryResult, onConfigChange }: V
|
|||
|
||||
{/* 마커 색상 설정 */}
|
||||
<div className="space-y-2 border-t pt-3">
|
||||
<h5 className="text-xs font-semibold text-gray-800">🎨 마커 색상 설정</h5>
|
||||
<h5 className="text-xs font-semibold text-foreground">🎨 마커 색상 설정</h5>
|
||||
|
||||
{/* 색상 모드 선택 */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="block text-xs font-medium text-gray-700">색상 모드</label>
|
||||
<label className="block text-xs font-medium text-foreground">색상 모드</label>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleMarkerColorModeChange("single")}
|
||||
className={`flex-1 rounded-lg border px-3 py-2 text-xs transition-colors ${
|
||||
(currentConfig.markerColorMode || "single") === "single"
|
||||
? "border-blue-300 bg-blue-50 font-medium text-blue-700"
|
||||
: "border-gray-300 bg-white text-gray-700 hover:bg-gray-50"
|
||||
? "border-primary bg-primary/10 font-medium text-primary"
|
||||
: "border-border bg-background text-foreground hover:bg-muted"
|
||||
}`}
|
||||
>
|
||||
단일 색상
|
||||
|
|
@ -195,8 +195,8 @@ export function VehicleMapConfigPanel({ config, queryResult, onConfigChange }: V
|
|||
onClick={() => handleMarkerColorModeChange("conditional")}
|
||||
className={`flex-1 rounded-lg border px-3 py-2 text-xs transition-colors ${
|
||||
currentConfig.markerColorMode === "conditional"
|
||||
? "border-blue-300 bg-blue-50 font-medium text-blue-700"
|
||||
: "border-gray-300 bg-white text-gray-700 hover:bg-gray-50"
|
||||
? "border-primary bg-primary/10 font-medium text-primary"
|
||||
: "border-border bg-background text-foreground hover:bg-muted"
|
||||
}`}
|
||||
>
|
||||
조건부 색상
|
||||
|
|
@ -206,40 +206,40 @@ export function VehicleMapConfigPanel({ config, queryResult, onConfigChange }: V
|
|||
|
||||
{/* 단일 색상 모드 */}
|
||||
{(currentConfig.markerColorMode || "single") === "single" && (
|
||||
<div className="space-y-1.5 rounded-lg bg-gray-50 p-3">
|
||||
<label className="block text-xs font-medium text-gray-700">마커 색상</label>
|
||||
<div className="space-y-1.5 rounded-lg bg-muted p-3">
|
||||
<label className="block text-xs font-medium text-foreground">마커 색상</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="color"
|
||||
value={currentConfig.markerDefaultColor || "#3b82f6"}
|
||||
onChange={(e) => updateConfig({ markerDefaultColor: e.target.value })}
|
||||
className="h-8 w-12 cursor-pointer rounded border border-gray-300"
|
||||
className="h-8 w-12 cursor-pointer rounded border border-border"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={currentConfig.markerDefaultColor || "#3b82f6"}
|
||||
onChange={(e) => updateConfig({ markerDefaultColor: e.target.value })}
|
||||
placeholder="#3b82f6"
|
||||
className="flex-1 rounded-lg border border-gray-300 px-2 py-1.5 text-xs"
|
||||
className="flex-1 rounded-lg border border-border px-2 py-1.5 text-xs"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500">모든 마커가 동일한 색상으로 표시됩니다</p>
|
||||
<p className="text-xs text-muted-foreground">모든 마커가 동일한 색상으로 표시됩니다</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 조건부 색상 모드 */}
|
||||
{currentConfig.markerColorMode === "conditional" && (
|
||||
<div className="space-y-2 rounded-lg bg-gray-50 p-3">
|
||||
<div className="space-y-2 rounded-lg bg-muted p-3">
|
||||
{/* 색상 조건 컬럼 선택 */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="block text-xs font-medium text-gray-700">
|
||||
<label className="block text-xs font-medium text-foreground">
|
||||
색상 조건 컬럼
|
||||
<span className="ml-1 text-red-500">*</span>
|
||||
<span className="ml-1 text-destructive">*</span>
|
||||
</label>
|
||||
<select
|
||||
value={currentConfig.markerColorColumn || ""}
|
||||
onChange={(e) => updateConfig({ markerColorColumn: e.target.value })}
|
||||
className="w-full rounded-lg border border-gray-300 px-2 py-1.5 text-xs"
|
||||
className="w-full rounded-lg border border-border px-2 py-1.5 text-xs"
|
||||
>
|
||||
<option value="">선택하세요</option>
|
||||
{availableColumns.map((col) => (
|
||||
|
|
@ -248,38 +248,38 @@ export function VehicleMapConfigPanel({ config, queryResult, onConfigChange }: V
|
|||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="text-xs text-gray-500">이 컬럼의 값에 따라 마커 색상이 결정됩니다</p>
|
||||
<p className="text-xs text-muted-foreground">이 컬럼의 값에 따라 마커 색상이 결정됩니다</p>
|
||||
</div>
|
||||
|
||||
{/* 기본 색상 */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="block text-xs font-medium text-gray-700">기본 색상</label>
|
||||
<label className="block text-xs font-medium text-foreground">기본 색상</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="color"
|
||||
value={currentConfig.markerDefaultColor || "#6b7280"}
|
||||
onChange={(e) => updateConfig({ markerDefaultColor: e.target.value })}
|
||||
className="h-8 w-12 cursor-pointer rounded border border-gray-300"
|
||||
className="h-8 w-12 cursor-pointer rounded border border-border"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={currentConfig.markerDefaultColor || "#6b7280"}
|
||||
onChange={(e) => updateConfig({ markerDefaultColor: e.target.value })}
|
||||
placeholder="#6b7280"
|
||||
className="flex-1 rounded-lg border border-gray-300 px-2 py-1.5 text-xs"
|
||||
className="flex-1 rounded-lg border border-border px-2 py-1.5 text-xs"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500">규칙에 매칭되지 않는 경우 사용할 색상</p>
|
||||
<p className="text-xs text-muted-foreground">규칙에 매칭되지 않는 경우 사용할 색상</p>
|
||||
</div>
|
||||
|
||||
{/* 색상 규칙 목록 */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="block text-xs font-medium text-gray-700">색상 규칙</label>
|
||||
<label className="block text-xs font-medium text-foreground">색상 규칙</label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={addColorRule}
|
||||
className="flex items-center gap-1 rounded-lg bg-blue-500 px-2 py-1 text-xs text-white transition-colors hover:bg-blue-600"
|
||||
className="flex items-center gap-1 rounded-lg bg-primary px-2 py-1 text-xs text-white transition-colors hover:bg-primary/90"
|
||||
>
|
||||
<Plus className="h-3 w-3" />
|
||||
추가
|
||||
|
|
@ -288,20 +288,20 @@ export function VehicleMapConfigPanel({ config, queryResult, onConfigChange }: V
|
|||
|
||||
{/* 규칙 리스트 */}
|
||||
{(currentConfig.markerColorRules || []).length === 0 ? (
|
||||
<div className="rounded-lg border border-gray-200 bg-white p-3 text-center">
|
||||
<p className="text-xs text-gray-500">추가 버튼을 눌러 색상 규칙을 만드세요</p>
|
||||
<div className="rounded-lg border border-border bg-background p-3 text-center">
|
||||
<p className="text-xs text-muted-foreground">추가 버튼을 눌러 색상 규칙을 만드세요</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{(currentConfig.markerColorRules || []).map((rule) => (
|
||||
<div key={rule.id} className="space-y-2 rounded-lg border border-gray-200 bg-white p-2">
|
||||
<div key={rule.id} className="space-y-2 rounded-lg border border-border bg-background p-2">
|
||||
{/* 규칙 헤더 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-medium text-gray-700">규칙</span>
|
||||
<span className="text-xs font-medium text-foreground">규칙</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => deleteColorRule(rule.id)}
|
||||
className="text-red-500 transition-colors hover:text-red-700"
|
||||
className="text-destructive transition-colors hover:text-destructive"
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</button>
|
||||
|
|
@ -309,45 +309,45 @@ export function VehicleMapConfigPanel({ config, queryResult, onConfigChange }: V
|
|||
|
||||
{/* 조건 값 */}
|
||||
<div className="space-y-1">
|
||||
<label className="block text-xs font-medium text-gray-600">값 (조건)</label>
|
||||
<label className="block text-xs font-medium text-foreground">값 (조건)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={rule.value}
|
||||
onChange={(e) => updateColorRule(rule.id, { value: e.target.value })}
|
||||
placeholder="예: active, inactive"
|
||||
className="w-full rounded border border-gray-300 px-2 py-1 text-xs"
|
||||
className="w-full rounded border border-border px-2 py-1 text-xs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 색상 */}
|
||||
<div className="space-y-1">
|
||||
<label className="block text-xs font-medium text-gray-600">색상</label>
|
||||
<label className="block text-xs font-medium text-foreground">색상</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="color"
|
||||
value={rule.color}
|
||||
onChange={(e) => updateColorRule(rule.id, { color: e.target.value })}
|
||||
className="h-8 w-12 cursor-pointer rounded border border-gray-300"
|
||||
className="h-8 w-12 cursor-pointer rounded border border-border"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={rule.color}
|
||||
onChange={(e) => updateColorRule(rule.id, { color: e.target.value })}
|
||||
placeholder="#3b82f6"
|
||||
className="flex-1 rounded border border-gray-300 px-2 py-1 text-xs"
|
||||
className="flex-1 rounded border border-border px-2 py-1 text-xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 라벨 (선택사항) */}
|
||||
<div className="space-y-1">
|
||||
<label className="block text-xs font-medium text-gray-600">라벨 (선택)</label>
|
||||
<label className="block text-xs font-medium text-foreground">라벨 (선택)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={rule.label || ""}
|
||||
onChange={(e) => updateColorRule(rule.id, { label: e.target.value })}
|
||||
placeholder="예: 활성, 비활성"
|
||||
className="w-full rounded border border-gray-300 px-2 py-1 text-xs"
|
||||
className="w-full rounded border border-border px-2 py-1 text-xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -361,36 +361,36 @@ export function VehicleMapConfigPanel({ config, queryResult, onConfigChange }: V
|
|||
|
||||
{/* 날씨 정보 표시 옵션 */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="flex cursor-pointer items-center gap-2 text-xs font-medium text-gray-700">
|
||||
<label className="flex cursor-pointer items-center gap-2 text-xs font-medium text-foreground">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={currentConfig.showWeather || false}
|
||||
onChange={(e) => updateConfig({ showWeather: e.target.checked })}
|
||||
className="text-primary focus:ring-primary h-4 w-4 rounded border-gray-300 focus:ring-2"
|
||||
className="text-primary focus:ring-primary h-4 w-4 rounded border-border focus:ring-2"
|
||||
/>
|
||||
<span>날씨 정보 표시</span>
|
||||
</label>
|
||||
<p className="ml-6 text-xs text-gray-500">마커 팝업에 해당 위치의 날씨 정보를 함께 표시합니다</p>
|
||||
<p className="ml-6 text-xs text-muted-foreground">마커 팝업에 해당 위치의 날씨 정보를 함께 표시합니다</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<label className="flex cursor-pointer items-center gap-2 text-xs font-medium text-gray-700">
|
||||
<label className="flex cursor-pointer items-center gap-2 text-xs font-medium text-foreground">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={currentConfig.showWeatherAlerts || false}
|
||||
onChange={(e) => updateConfig({ showWeatherAlerts: e.target.checked })}
|
||||
className="text-primary focus:ring-primary h-4 w-4 rounded border-gray-300 focus:ring-2"
|
||||
className="text-primary focus:ring-primary h-4 w-4 rounded border-border focus:ring-2"
|
||||
/>
|
||||
<span>기상특보 영역 표시</span>
|
||||
</label>
|
||||
<p className="ml-6 text-xs text-gray-500">
|
||||
<p className="ml-6 text-xs text-muted-foreground">
|
||||
현재 발효 중인 기상특보(주의보/경보)를 지도에 색상 영역으로 표시합니다
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 설정 미리보기 */}
|
||||
<div className="rounded-lg bg-gray-50 p-3">
|
||||
<div className="mb-2 text-xs font-medium text-gray-700">📋 설정 미리보기</div>
|
||||
<div className="rounded-lg bg-muted p-3">
|
||||
<div className="mb-2 text-xs font-medium text-foreground">📋 설정 미리보기</div>
|
||||
<div className="text-muted-foreground space-y-1 text-xs">
|
||||
<div>
|
||||
<strong>위도:</strong> {currentConfig.latitudeColumn || "미설정"}
|
||||
|
|
@ -428,8 +428,8 @@ export function VehicleMapConfigPanel({ config, queryResult, onConfigChange }: V
|
|||
|
||||
{/* 필수 필드 확인 */}
|
||||
{(!currentConfig.latitudeColumn || !currentConfig.longitudeColumn) && (
|
||||
<div className="rounded-lg border border-red-200 bg-red-50 p-3">
|
||||
<div className="text-xs text-red-800">
|
||||
<div className="rounded-lg border border-destructive bg-destructive/10 p-3">
|
||||
<div className="text-xs text-destructive">
|
||||
⚠️ 위도와 경도 컬럼을 반드시 선택해야 지도에 표시할 수 있습니다.
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -27,13 +27,13 @@ export function Chart({ chartType, data, config, width, height }: ChartProps) {
|
|||
if (!data || !data.labels.length || !data.datasets.length) {
|
||||
return (
|
||||
<div
|
||||
className="flex items-center justify-center rounded-lg border-2 border-dashed border-gray-300 bg-gray-50"
|
||||
className="flex items-center justify-center rounded-lg border-2 border-dashed border-border bg-muted"
|
||||
style={{ width, height }}
|
||||
>
|
||||
<div className="text-center">
|
||||
<div className="mb-2 text-4xl">📊</div>
|
||||
<div className="text-sm font-medium text-gray-600">데이터를 설정하세요</div>
|
||||
<div className="mt-1 text-xs text-gray-500">차트 설정에서 데이터 소스와 축을 설정하세요</div>
|
||||
<div className="text-sm font-medium text-foreground">데이터를 설정하세요</div>
|
||||
<div className="mt-1 text-xs text-muted-foreground">차트 설정에서 데이터 소스와 축을 설정하세요</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -68,13 +68,13 @@ export function Chart({ chartType, data, config, width, height }: ChartProps) {
|
|||
default:
|
||||
return (
|
||||
<div
|
||||
className="flex items-center justify-center rounded-lg border-2 border-dashed border-gray-300 bg-gray-50"
|
||||
className="flex items-center justify-center rounded-lg border-2 border-dashed border-border bg-muted"
|
||||
style={{ width, height }}
|
||||
>
|
||||
<div className="text-center">
|
||||
<div className="mb-2 text-4xl">❓</div>
|
||||
<div className="text-sm font-medium text-gray-600">지원하지 않는 차트 타입</div>
|
||||
<div className="mt-1 text-xs text-gray-500">{chartType}</div>
|
||||
<div className="text-sm font-medium text-foreground">지원하지 않는 차트 타입</div>
|
||||
<div className="mt-1 text-xs text-muted-foreground">{chartType}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -203,9 +203,9 @@ export function ChartRenderer({ element, data, width, height = 200 }: ChartRende
|
|||
// 로딩 중
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center text-gray-500">
|
||||
<div className="flex h-full w-full items-center justify-center text-muted-foreground">
|
||||
<div className="text-center">
|
||||
<div className="mx-auto mb-2 h-6 w-6 animate-spin rounded-full border-2 border-blue-600 border-t-transparent" />
|
||||
<div className="mx-auto mb-2 h-6 w-6 animate-spin rounded-full border-2 border-primary border-t-transparent" />
|
||||
<div className="text-sm">데이터 로딩 중...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -215,7 +215,7 @@ export function ChartRenderer({ element, data, width, height = 200 }: ChartRende
|
|||
// 에러
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center text-red-500">
|
||||
<div className="flex h-full w-full items-center justify-center text-destructive">
|
||||
<div className="text-center">
|
||||
<div className="mb-2 text-2xl">⚠️</div>
|
||||
<div className="text-sm font-medium">오류 발생</div>
|
||||
|
|
@ -232,7 +232,7 @@ export function ChartRenderer({ element, data, width, height = 200 }: ChartRende
|
|||
|
||||
if (!chartData || !element.chartConfig?.xAxis || (needsYAxis && !element.chartConfig?.yAxis)) {
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center text-gray-500">
|
||||
<div className="flex h-full w-full items-center justify-center text-muted-foreground">
|
||||
<div className="text-center">
|
||||
<div className="text-sm">데이터를 설정해주세요</div>
|
||||
</div>
|
||||
|
|
@ -264,7 +264,7 @@ export function ChartRenderer({ element, data, width, height = 200 }: ChartRende
|
|||
});
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="flex h-full w-full items-center justify-center bg-white p-0.5">
|
||||
<div ref={containerRef} className="flex h-full w-full items-center justify-center bg-background p-0.5">
|
||||
<div className="flex items-center justify-center">
|
||||
<Chart
|
||||
chartType={element.subtype}
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ export function ComboChartComponent({ data, config, width = 250, height = 200 }:
|
|||
return (
|
||||
<div className="w-full h-full p-2">
|
||||
{title && (
|
||||
<div className="text-center text-sm font-semibold text-gray-700 mb-2">
|
||||
<div className="text-center text-sm font-semibold text-foreground mb-2">
|
||||
{title}
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ export function StackedBarChartComponent({ data, config, width = 250, height = 2
|
|||
return (
|
||||
<div className="w-full h-full p-2">
|
||||
{title && (
|
||||
<div className="text-center text-sm font-semibold text-gray-700 mb-2">
|
||||
<div className="text-center text-sm font-semibold text-foreground mb-2">
|
||||
{title}
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -374,7 +374,7 @@ export function ApiConfig({ dataSource, onChange, onTestResult }: ApiConfigProps
|
|||
<div className="space-y-4">
|
||||
{/* 외부 커넥션 선택 - 항상 표시 */}
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-medium text-gray-700">외부 커넥션 (선택)</Label>
|
||||
<Label className="text-xs font-medium text-foreground">외부 커넥션 (선택)</Label>
|
||||
<Select value={selectedConnectionId} onValueChange={handleConnectionSelect}>
|
||||
<SelectTrigger className="h-8 text-xs">
|
||||
<SelectValue placeholder="저장된 커넥션 선택" />
|
||||
|
|
@ -387,22 +387,22 @@ export function ApiConfig({ dataSource, onChange, onTestResult }: ApiConfigProps
|
|||
apiConnections.map((conn) => (
|
||||
<SelectItem key={conn.id} value={String(conn.id)} className="text-xs">
|
||||
{conn.connection_name}
|
||||
{conn.description && <span className="ml-1.5 text-[10px] text-gray-500">({conn.description})</span>}
|
||||
{conn.description && <span className="ml-1.5 text-[10px] text-muted-foreground">({conn.description})</span>}
|
||||
</SelectItem>
|
||||
))
|
||||
) : (
|
||||
<SelectItem value="no-connections" disabled className="text-xs text-gray-500">
|
||||
<SelectItem value="no-connections" disabled className="text-xs text-muted-foreground">
|
||||
등록된 커넥션이 없습니다
|
||||
</SelectItem>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-[11px] text-gray-500">저장한 REST API 설정을 불러올 수 있습니다</p>
|
||||
<p className="text-[11px] text-muted-foreground">저장한 REST API 설정을 불러올 수 있습니다</p>
|
||||
</div>
|
||||
|
||||
{/* API URL */}
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs font-medium text-gray-700">API URL *</Label>
|
||||
<Label className="text-xs font-medium text-foreground">API URL *</Label>
|
||||
<Input
|
||||
type="url"
|
||||
placeholder="https://api.example.com/data 또는 /api/typ01/url/wrn_now_data.php"
|
||||
|
|
@ -410,7 +410,7 @@ export function ApiConfig({ dataSource, onChange, onTestResult }: ApiConfigProps
|
|||
onChange={(e) => onChange({ endpoint: e.target.value })}
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
<p className="text-[11px] text-gray-500">
|
||||
<p className="text-[11px] text-muted-foreground">
|
||||
전체 URL 또는 base_url 이후 경로를 입력하세요 (외부 커넥션 선택 시 base_url 자동 입력)
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -418,7 +418,7 @@ export function ApiConfig({ dataSource, onChange, onTestResult }: ApiConfigProps
|
|||
{/* 쿼리 파라미터 */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-xs font-medium text-gray-700">URL 쿼리 파라미터</Label>
|
||||
<Label className="text-xs font-medium text-foreground">URL 쿼리 파라미터</Label>
|
||||
<Button variant="outline" size="sm" onClick={addQueryParam} className="h-6 text-[11px]">
|
||||
<Plus className="mr-1 h-3 w-3" />
|
||||
추가
|
||||
|
|
@ -445,7 +445,7 @@ export function ApiConfig({ dataSource, onChange, onTestResult }: ApiConfigProps
|
|||
/>
|
||||
<button
|
||||
onClick={() => removeQueryParam(param.id)}
|
||||
className="flex h-7 w-7 items-center justify-center rounded hover:bg-gray-100"
|
||||
className="flex h-7 w-7 items-center justify-center rounded hover:bg-muted"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
|
|
@ -453,17 +453,17 @@ export function ApiConfig({ dataSource, onChange, onTestResult }: ApiConfigProps
|
|||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="py-2 text-center text-[11px] text-gray-500">추가된 파라미터가 없습니다</p>
|
||||
<p className="py-2 text-center text-[11px] text-muted-foreground">추가된 파라미터가 없습니다</p>
|
||||
);
|
||||
})()}
|
||||
|
||||
<p className="text-[11px] text-gray-500">예: category=electronics, limit=10</p>
|
||||
<p className="text-[11px] text-muted-foreground">예: category=electronics, limit=10</p>
|
||||
</div>
|
||||
|
||||
{/* 헤더 */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-xs font-medium text-gray-700">요청 헤더</Label>
|
||||
<Label className="text-xs font-medium text-foreground">요청 헤더</Label>
|
||||
<Button variant="outline" size="sm" onClick={addHeader}>
|
||||
<Plus className="mr-1 h-3 w-3" />
|
||||
추가
|
||||
|
|
@ -524,20 +524,20 @@ export function ApiConfig({ dataSource, onChange, onTestResult }: ApiConfigProps
|
|||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="py-2 text-center text-sm text-gray-500">추가된 헤더가 없습니다</p>
|
||||
<p className="py-2 text-center text-sm text-muted-foreground">추가된 헤더가 없습니다</p>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
|
||||
{/* JSON Path */}
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-medium text-gray-700">JSON Path (선택)</Label>
|
||||
<Label className="text-xs font-medium text-foreground">JSON Path (선택)</Label>
|
||||
<Input
|
||||
placeholder="data.results"
|
||||
value={dataSource.jsonPath || ""}
|
||||
onChange={(e) => onChange({ jsonPath: e.target.value })}
|
||||
/>
|
||||
<p className="text-[11px] text-gray-500">
|
||||
<p className="text-[11px] text-muted-foreground">
|
||||
JSON 응답에서 데이터 배열의 경로 (예: data.results, items, response.data)
|
||||
<br />
|
||||
비워두면 전체 응답을 사용합니다
|
||||
|
|
@ -563,12 +563,12 @@ export function ApiConfig({ dataSource, onChange, onTestResult }: ApiConfigProps
|
|||
|
||||
{/* 테스트 오류 */}
|
||||
{testError && (
|
||||
<div className="rounded bg-red-50 px-2 py-2">
|
||||
<div className="rounded bg-destructive/10 px-2 py-2">
|
||||
<div className="flex items-start gap-2">
|
||||
<AlertCircle className="mt-0.5 h-5 w-5 flex-shrink-0 text-red-600" />
|
||||
<AlertCircle className="mt-0.5 h-5 w-5 flex-shrink-0 text-destructive" />
|
||||
<div>
|
||||
<div className="text-sm font-medium text-red-800">API 호출 실패</div>
|
||||
<div className="mt-1 text-sm text-red-700">{testError}</div>
|
||||
<div className="text-sm font-medium text-destructive">API 호출 실패</div>
|
||||
<div className="mt-1 text-sm text-destructive">{testError}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -576,9 +576,9 @@ export function ApiConfig({ dataSource, onChange, onTestResult }: ApiConfigProps
|
|||
|
||||
{/* 테스트 결과 */}
|
||||
{testResult && (
|
||||
<div className="rounded bg-green-50 px-2 py-2">
|
||||
<div className="mb-2 text-sm font-medium text-green-800">API 호출 성공</div>
|
||||
<div className="space-y-1 text-xs text-green-700">
|
||||
<div className="rounded bg-success/10 px-2 py-2">
|
||||
<div className="mb-2 text-sm font-medium text-success">API 호출 성공</div>
|
||||
<div className="space-y-1 text-xs text-success">
|
||||
<div>총 {testResult.rows.length}개의 데이터를 불러왔습니다</div>
|
||||
<div>컬럼: {testResult.columns.join(", ")}</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -19,8 +19,8 @@ export function DataSourceSelector({ dataSource, onTypeChange }: DataSourceSelec
|
|||
return (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-800">1단계: 데이터 소스 선택</h3>
|
||||
<p className="mt-1 text-sm text-gray-600">차트에 표시할 데이터를 어디서 가져올지 선택하세요</p>
|
||||
<h3 className="text-lg font-semibold text-foreground">1단계: 데이터 소스 선택</h3>
|
||||
<p className="mt-1 text-sm text-foreground">차트에 표시할 데이터를 어디서 가져올지 선택하세요</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
|
|
@ -28,20 +28,20 @@ export function DataSourceSelector({ dataSource, onTypeChange }: DataSourceSelec
|
|||
<Card
|
||||
className={`cursor-pointer p-6 transition-all ${
|
||||
dataSource.type === "database"
|
||||
? "border-2 border-blue-500 bg-blue-50"
|
||||
: "border-2 border-gray-200 hover:border-gray-300"
|
||||
? "border-2 border-primary bg-primary/10"
|
||||
: "border-2 border-border hover:border-border"
|
||||
}`}
|
||||
onClick={() => onTypeChange("database")}
|
||||
>
|
||||
<div className="flex flex-col items-center space-y-3 text-center">
|
||||
<div className={`rounded-full p-4 ${dataSource.type === "database" ? "bg-blue-100" : "bg-gray-100"}`}>
|
||||
<Database className={`h-8 w-8 ${dataSource.type === "database" ? "text-blue-600" : "text-gray-600"}`} />
|
||||
<div className={`rounded-full p-4 ${dataSource.type === "database" ? "bg-primary/10" : "bg-muted"}`}>
|
||||
<Database className={`h-8 w-8 ${dataSource.type === "database" ? "text-primary" : "text-foreground"}`} />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-semibold text-gray-900">데이터베이스</h4>
|
||||
<p className="mt-1 text-sm text-gray-600">SQL 쿼리로 데이터 조회</p>
|
||||
<h4 className="font-semibold text-foreground">데이터베이스</h4>
|
||||
<p className="mt-1 text-sm text-foreground">SQL 쿼리로 데이터 조회</p>
|
||||
</div>
|
||||
<div className="space-y-1 text-xs text-gray-500">
|
||||
<div className="space-y-1 text-xs text-muted-foreground">
|
||||
<div>✓ 현재 DB 또는 외부 DB</div>
|
||||
<div>✓ SELECT 쿼리 지원</div>
|
||||
<div>✓ 실시간 데이터 조회</div>
|
||||
|
|
@ -53,20 +53,20 @@ export function DataSourceSelector({ dataSource, onTypeChange }: DataSourceSelec
|
|||
<Card
|
||||
className={`cursor-pointer p-6 transition-all ${
|
||||
dataSource.type === "api"
|
||||
? "border-2 border-green-500 bg-green-50"
|
||||
: "border-2 border-gray-200 hover:border-gray-300"
|
||||
? "border-2 border-success bg-success/10"
|
||||
: "border-2 border-border hover:border-border"
|
||||
}`}
|
||||
onClick={() => onTypeChange("api")}
|
||||
>
|
||||
<div className="flex flex-col items-center space-y-3 text-center">
|
||||
<div className={`rounded-full p-4 ${dataSource.type === "api" ? "bg-green-100" : "bg-gray-100"}`}>
|
||||
<Globe className={`h-8 w-8 ${dataSource.type === "api" ? "text-green-600" : "text-gray-600"}`} />
|
||||
<div className={`rounded-full p-4 ${dataSource.type === "api" ? "bg-success/10" : "bg-muted"}`}>
|
||||
<Globe className={`h-8 w-8 ${dataSource.type === "api" ? "text-success" : "text-foreground"}`} />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-semibold text-gray-900">REST API</h4>
|
||||
<p className="mt-1 text-sm text-gray-600">외부 API에서 데이터 가져오기</p>
|
||||
<h4 className="font-semibold text-foreground">REST API</h4>
|
||||
<p className="mt-1 text-sm text-foreground">외부 API에서 데이터 가져오기</p>
|
||||
</div>
|
||||
<div className="space-y-1 text-xs text-gray-500">
|
||||
<div className="space-y-1 text-xs text-muted-foreground">
|
||||
<div>✓ GET 요청 지원</div>
|
||||
<div>✓ JSON 응답 파싱</div>
|
||||
<div>✓ 커스텀 헤더 설정</div>
|
||||
|
|
@ -77,10 +77,10 @@ export function DataSourceSelector({ dataSource, onTypeChange }: DataSourceSelec
|
|||
|
||||
{/* 선택된 타입 표시 */}
|
||||
{dataSource.type && (
|
||||
<div className="rounded-lg border border-gray-200 bg-gray-50 p-3">
|
||||
<div className="rounded-lg border border-border bg-muted p-3">
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<span className="font-medium text-gray-700">선택됨:</span>
|
||||
<span className="text-gray-900">{dataSource.type === "database" ? "🗄️ 데이터베이스" : "🌐 REST API"}</span>
|
||||
<span className="font-medium text-foreground">선택됨:</span>
|
||||
<span className="text-foreground">{dataSource.type === "database" ? "🗄️ 데이터베이스" : "🌐 REST API"}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ export function DatabaseConfig({ dataSource, onChange }: DatabaseConfigProps) {
|
|||
<div className="space-y-3">
|
||||
{/* 현재 DB vs 외부 DB 선택 */}
|
||||
<div>
|
||||
<Label className="mb-2 block text-xs font-medium text-gray-700">데이터베이스 선택</Label>
|
||||
<Label className="mb-2 block text-xs font-medium text-foreground">데이터베이스 선택</Label>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
|
|
@ -61,7 +61,7 @@ export function DatabaseConfig({ dataSource, onChange }: DatabaseConfigProps) {
|
|||
className={`flex flex-1 items-center gap-1.5 rounded border px-2 py-1.5 text-xs transition-colors ${
|
||||
dataSource.connectionType === "current"
|
||||
? "bg-primary border-primary text-white"
|
||||
: "border-gray-200 bg-white hover:bg-gray-50"
|
||||
: "border-border bg-background hover:bg-muted"
|
||||
}`}
|
||||
>
|
||||
<Database className="h-3 w-3" />
|
||||
|
|
@ -75,7 +75,7 @@ export function DatabaseConfig({ dataSource, onChange }: DatabaseConfigProps) {
|
|||
className={`flex flex-1 items-center gap-1.5 rounded border px-2 py-1.5 text-xs transition-colors ${
|
||||
dataSource.connectionType === "external"
|
||||
? "bg-primary border-primary text-white"
|
||||
: "border-gray-200 bg-white hover:bg-gray-50"
|
||||
: "border-border bg-background hover:bg-muted"
|
||||
}`}
|
||||
>
|
||||
<Server className="h-3 w-3" />
|
||||
|
|
@ -88,12 +88,12 @@ export function DatabaseConfig({ dataSource, onChange }: DatabaseConfigProps) {
|
|||
{dataSource.connectionType === "external" && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-xs font-medium text-gray-700">외부 커넥션</Label>
|
||||
<Label className="text-xs font-medium text-foreground">외부 커넥션</Label>
|
||||
<button
|
||||
onClick={() => {
|
||||
router.push("/admin/external-connections");
|
||||
}}
|
||||
className="flex items-center gap-1 text-[11px] text-blue-600 transition-colors hover:text-blue-700"
|
||||
className="flex items-center gap-1 text-[11px] text-primary transition-colors hover:text-primary"
|
||||
>
|
||||
<ExternalLink className="h-3 w-3" />
|
||||
커넥션 관리
|
||||
|
|
@ -102,17 +102,17 @@ export function DatabaseConfig({ dataSource, onChange }: DatabaseConfigProps) {
|
|||
|
||||
{loading && (
|
||||
<div className="flex items-center justify-center py-3">
|
||||
<div className="h-4 w-4 animate-spin rounded-full border-2 border-gray-300 border-t-blue-600" />
|
||||
<span className="ml-2 text-xs text-gray-600">로딩 중...</span>
|
||||
<div className="h-4 w-4 animate-spin rounded-full border-2 border-border border-t-blue-600" />
|
||||
<span className="ml-2 text-xs text-foreground">로딩 중...</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="rounded bg-red-50 px-2 py-1.5">
|
||||
<div className="text-xs text-red-800">{error}</div>
|
||||
<div className="rounded bg-destructive/10 px-2 py-1.5">
|
||||
<div className="text-xs text-destructive">{error}</div>
|
||||
<button
|
||||
onClick={loadExternalConnections}
|
||||
className="mt-1 text-[11px] text-red-600 underline hover:no-underline"
|
||||
className="mt-1 text-[11px] text-destructive underline hover:no-underline"
|
||||
>
|
||||
다시 시도
|
||||
</button>
|
||||
|
|
@ -120,13 +120,13 @@ export function DatabaseConfig({ dataSource, onChange }: DatabaseConfigProps) {
|
|||
)}
|
||||
|
||||
{!loading && !error && connections.length === 0 && (
|
||||
<div className="rounded bg-yellow-50 px-2 py-2 text-center">
|
||||
<div className="mb-1 text-xs text-yellow-800">등록된 커넥션이 없습니다</div>
|
||||
<div className="rounded bg-warning/10 px-2 py-2 text-center">
|
||||
<div className="mb-1 text-xs text-warning">등록된 커넥션이 없습니다</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
router.push("/admin/external-connections");
|
||||
}}
|
||||
className="text-[11px] text-yellow-700 underline hover:no-underline"
|
||||
className="text-[11px] text-warning underline hover:no-underline"
|
||||
>
|
||||
커넥션 등록하기
|
||||
</button>
|
||||
|
|
@ -149,7 +149,7 @@ export function DatabaseConfig({ dataSource, onChange }: DatabaseConfigProps) {
|
|||
<SelectItem key={conn.id} value={String(conn.id)} className="text-xs">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="font-medium">{conn.connection_name}</span>
|
||||
<span className="text-[10px] text-gray-500">({conn.db_type.toUpperCase()})</span>
|
||||
<span className="text-[10px] text-muted-foreground">({conn.db_type.toUpperCase()})</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
|
|
@ -157,7 +157,7 @@ export function DatabaseConfig({ dataSource, onChange }: DatabaseConfigProps) {
|
|||
</Select>
|
||||
|
||||
{selectedConnection && (
|
||||
<div className="space-y-0.5 rounded bg-gray-50 px-2 py-1.5 text-[11px] text-gray-600">
|
||||
<div className="space-y-0.5 rounded bg-muted px-2 py-1.5 text-[11px] text-foreground">
|
||||
<div>
|
||||
<span className="font-medium">커넥션:</span> {selectedConnection.connection_name}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -630,8 +630,8 @@ export default function MultiApiConfig({ dataSource, onChange, onTestResult }: M
|
|||
<div
|
||||
className={`flex items-center gap-2 rounded-md p-2 text-xs ${
|
||||
testResult.success
|
||||
? "bg-green-50 text-green-700"
|
||||
: "bg-red-50 text-red-700"
|
||||
? "bg-success/10 text-success"
|
||||
: "bg-destructive/10 text-destructive"
|
||||
}`}
|
||||
>
|
||||
{testResult.success ? (
|
||||
|
|
@ -710,12 +710,12 @@ export default function MultiApiConfig({ dataSource, onChange, onTestResult }: M
|
|||
}[type];
|
||||
|
||||
const typeColor = {
|
||||
number: "text-blue-600 bg-blue-50",
|
||||
string: "text-gray-600 bg-gray-50",
|
||||
date: "text-purple-600 bg-purple-50",
|
||||
boolean: "text-green-600 bg-green-50",
|
||||
object: "text-orange-600 bg-orange-50",
|
||||
unknown: "text-gray-400 bg-gray-50"
|
||||
number: "text-primary bg-primary/10",
|
||||
string: "text-muted-foreground bg-muted",
|
||||
date: "text-purple-500 bg-purple-500/10",
|
||||
boolean: "text-success bg-success/10",
|
||||
object: "text-warning bg-warning/10",
|
||||
unknown: "text-muted-foreground/50 bg-muted"
|
||||
}[type];
|
||||
|
||||
return (
|
||||
|
|
@ -746,7 +746,7 @@ export default function MultiApiConfig({ dataSource, onChange, onTestResult }: M
|
|||
h-4 w-4 rounded border-2 flex items-center justify-center transition-colors
|
||||
${isSelected
|
||||
? "border-primary bg-primary"
|
||||
: "border-gray-300 bg-background"
|
||||
: "border-border bg-background"
|
||||
}
|
||||
`}>
|
||||
{isSelected && (
|
||||
|
|
|
|||
|
|
@ -324,10 +324,10 @@ export default function MultiDataSourceConfig({
|
|||
{(item.status || item.level) && (
|
||||
<div className={`rounded px-2 py-0.5 text-[10px] font-medium ${
|
||||
(item.status || item.level)?.includes('경보') || (item.status || item.level)?.includes('위험')
|
||||
? 'bg-red-100 text-red-700'
|
||||
? 'bg-destructive/10 text-destructive'
|
||||
: (item.status || item.level)?.includes('주의')
|
||||
? 'bg-orange-100 text-orange-700'
|
||||
: 'bg-blue-100 text-blue-700'
|
||||
? 'bg-warning/10 text-warning'
|
||||
: 'bg-primary/10 text-primary'
|
||||
}`}>
|
||||
{item.status || item.level}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -406,8 +406,8 @@ ORDER BY 하위부서수 DESC`,
|
|||
<div
|
||||
className={`flex items-center gap-2 rounded-md p-2 text-xs ${
|
||||
testResult.success
|
||||
? "bg-green-50 text-green-700"
|
||||
: "bg-red-50 text-red-700"
|
||||
? "bg-success/10 text-success"
|
||||
: "bg-destructive/10 text-destructive"
|
||||
}`}
|
||||
>
|
||||
{testResult.success ? (
|
||||
|
|
@ -491,12 +491,12 @@ ORDER BY 하위부서수 DESC`,
|
|||
}[type];
|
||||
|
||||
const typeColor = {
|
||||
number: "text-blue-600 bg-blue-50",
|
||||
string: "text-gray-600 bg-gray-50",
|
||||
date: "text-purple-600 bg-purple-50",
|
||||
boolean: "text-green-600 bg-green-50",
|
||||
object: "text-orange-600 bg-orange-50",
|
||||
unknown: "text-gray-400 bg-gray-50"
|
||||
number: "text-primary bg-primary/10",
|
||||
string: "text-foreground bg-muted",
|
||||
date: "text-purple-500 bg-purple-500/10",
|
||||
boolean: "text-success bg-success/10",
|
||||
object: "text-warning bg-warning/10",
|
||||
unknown: "text-muted-foreground bg-muted"
|
||||
}[type];
|
||||
|
||||
return (
|
||||
|
|
@ -527,7 +527,7 @@ ORDER BY 하위부서수 DESC`,
|
|||
h-4 w-4 rounded border-2 flex items-center justify-center transition-colors
|
||||
${isSelected
|
||||
? "border-primary bg-primary"
|
||||
: "border-gray-300 bg-background"
|
||||
: "border-border bg-background"
|
||||
}
|
||||
`}>
|
||||
{isSelected && (
|
||||
|
|
|
|||
|
|
@ -91,19 +91,19 @@ export function CalendarSettings({ config, onSave, onClose }: CalendarSettingsPr
|
|||
{
|
||||
value: "light",
|
||||
label: "Light",
|
||||
gradient: "bg-gradient-to-br from-white to-gray-100",
|
||||
text: "text-gray-900",
|
||||
gradient: "bg-gradient-to-br from-background to-muted",
|
||||
text: "text-foreground",
|
||||
},
|
||||
{
|
||||
value: "dark",
|
||||
label: "Dark",
|
||||
gradient: "bg-gradient-to-br from-gray-800 to-gray-900",
|
||||
gradient: "bg-gradient-to-br from-foreground to-foreground",
|
||||
text: "text-white",
|
||||
},
|
||||
{
|
||||
value: "custom",
|
||||
label: "사용자",
|
||||
gradient: "bg-gradient-to-br from-blue-400 to-purple-600",
|
||||
gradient: "bg-gradient-to-br from-primary to-purple-500",
|
||||
text: "text-white",
|
||||
},
|
||||
].map((theme) => (
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ export function CalendarWidget({ element, onConfigUpdate }: CalendarWidgetProps)
|
|||
return (
|
||||
<div className="relative flex h-full w-full flex-col">
|
||||
{/* 헤더 - 네비게이션 */}
|
||||
<div className="flex items-center justify-between border-b border-gray-200 p-2">
|
||||
<div className="flex items-center justify-between border-b border-border p-2">
|
||||
{/* 이전 월 버튼 */}
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={handlePrevMonth}>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
|
|
@ -123,7 +123,7 @@ export function CalendarWidget({ element, onConfigUpdate }: CalendarWidgetProps)
|
|||
<div className="absolute bottom-2 right-2">
|
||||
<Popover open={settingsOpen} onOpenChange={setSettingsOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 bg-white/80 hover:bg-white">
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 bg-background/80 hover:bg-background">
|
||||
<Settings className="h-4 w-4" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
|
|
|
|||
|
|
@ -97,19 +97,19 @@ export function ClockSettings({ config, onSave, onClose }: ClockSettingsProps) {
|
|||
{
|
||||
value: "light",
|
||||
label: "Light",
|
||||
gradient: "bg-gradient-to-br from-white to-gray-100",
|
||||
text: "text-gray-900",
|
||||
gradient: "bg-gradient-to-br from-background to-muted",
|
||||
text: "text-foreground",
|
||||
},
|
||||
{
|
||||
value: "dark",
|
||||
label: "Dark",
|
||||
gradient: "bg-gradient-to-br from-gray-800 to-gray-900",
|
||||
gradient: "bg-gradient-to-br from-foreground to-foreground",
|
||||
text: "text-white",
|
||||
},
|
||||
{
|
||||
value: "custom",
|
||||
label: "사용자",
|
||||
gradient: "bg-gradient-to-br from-blue-400 to-purple-600",
|
||||
gradient: "bg-gradient-to-br from-primary to-purple-500",
|
||||
text: "text-white",
|
||||
},
|
||||
].map((theme) => (
|
||||
|
|
|
|||
|
|
@ -116,7 +116,7 @@ export function ClockWidget({ element, onConfigUpdate }: ClockWidgetProps) {
|
|||
<div className="absolute top-2 right-2">
|
||||
<Popover open={settingsOpen} onOpenChange={setSettingsOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 bg-white/80 hover:bg-white">
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 bg-background/80 hover:bg-background">
|
||||
<Settings className="h-4 w-4" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
|
|
|
|||
|
|
@ -112,22 +112,22 @@ function getThemeClasses(theme: string, customColor?: string) {
|
|||
|
||||
const themes = {
|
||||
light: {
|
||||
container: "bg-white text-gray-900",
|
||||
date: "text-gray-600",
|
||||
time: "text-gray-900",
|
||||
timezone: "text-gray-500",
|
||||
container: "bg-background text-foreground",
|
||||
date: "text-foreground",
|
||||
time: "text-foreground",
|
||||
timezone: "text-muted-foreground",
|
||||
},
|
||||
dark: {
|
||||
container: "bg-gray-900 text-white",
|
||||
date: "text-gray-300",
|
||||
date: "text-muted-foreground",
|
||||
time: "text-white",
|
||||
timezone: "text-gray-400",
|
||||
timezone: "text-muted-foreground",
|
||||
},
|
||||
custom: {
|
||||
container: "bg-gradient-to-br from-blue-400 to-purple-600 text-white",
|
||||
date: "text-blue-100",
|
||||
container: "bg-gradient-to-br from-primary to-purple-500 text-white",
|
||||
date: "text-primary/70",
|
||||
time: "text-white",
|
||||
timezone: "text-blue-200",
|
||||
timezone: "text-primary/80",
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -25,25 +25,25 @@ export function DriverListView({ drivers, config, isCompact = false }: DriverLis
|
|||
return (
|
||||
<div className="flex h-full flex-col items-center justify-center space-y-3 p-4">
|
||||
<div className="text-center">
|
||||
<div className="text-3xl font-bold text-gray-900">{drivers.length}</div>
|
||||
<div className="text-sm text-gray-600">전체 기사</div>
|
||||
<div className="text-3xl font-bold text-foreground">{drivers.length}</div>
|
||||
<div className="text-sm text-foreground">전체 기사</div>
|
||||
</div>
|
||||
<div className="grid w-full grid-cols-2 gap-2 text-center text-xs">
|
||||
<div className="rounded-lg bg-green-100 p-2">
|
||||
<div className="font-semibold text-green-800">{stats.driving}</div>
|
||||
<div className="text-green-600">운행중</div>
|
||||
<div className="rounded-lg bg-success/10 p-2">
|
||||
<div className="font-semibold text-success">{stats.driving}</div>
|
||||
<div className="text-success">운행중</div>
|
||||
</div>
|
||||
<div className="rounded-lg bg-gray-100 p-2">
|
||||
<div className="font-semibold text-gray-800">{stats.standby}</div>
|
||||
<div className="text-gray-600">대기중</div>
|
||||
<div className="rounded-lg bg-muted p-2">
|
||||
<div className="font-semibold text-foreground">{stats.standby}</div>
|
||||
<div className="text-foreground">대기중</div>
|
||||
</div>
|
||||
<div className="rounded-lg bg-orange-100 p-2">
|
||||
<div className="font-semibold text-orange-800">{stats.resting}</div>
|
||||
<div className="text-orange-600">휴식중</div>
|
||||
<div className="rounded-lg bg-warning/10 p-2">
|
||||
<div className="font-semibold text-warning">{stats.resting}</div>
|
||||
<div className="text-warning">휴식중</div>
|
||||
</div>
|
||||
<div className="rounded-lg bg-red-100 p-2">
|
||||
<div className="font-semibold text-red-800">{stats.maintenance}</div>
|
||||
<div className="text-red-600">점검중</div>
|
||||
<div className="rounded-lg bg-destructive/10 p-2">
|
||||
<div className="font-semibold text-destructive">{stats.maintenance}</div>
|
||||
<div className="text-destructive">점검중</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -53,54 +53,54 @@ export function DriverListView({ drivers, config, isCompact = false }: DriverLis
|
|||
// 빈 데이터 처리
|
||||
if (drivers.length === 0) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center text-sm text-gray-500">조회된 기사 정보가 없습니다</div>
|
||||
<div className="flex h-full items-center justify-center text-sm text-muted-foreground">조회된 기사 정보가 없습니다</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full w-full overflow-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="sticky top-0 z-10 bg-gray-50">
|
||||
<thead className="sticky top-0 z-10 bg-muted">
|
||||
<tr>
|
||||
{visibleColumns.includes("status") && (
|
||||
<th className="px-3 py-2 text-left text-xs font-semibold text-gray-700">{COLUMN_LABELS.status}</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-semibold text-foreground">{COLUMN_LABELS.status}</th>
|
||||
)}
|
||||
{visibleColumns.includes("name") && (
|
||||
<th className="px-3 py-2 text-left text-xs font-semibold text-gray-700">{COLUMN_LABELS.name}</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-semibold text-foreground">{COLUMN_LABELS.name}</th>
|
||||
)}
|
||||
{visibleColumns.includes("vehicleNumber") && (
|
||||
<th className="px-3 py-2 text-left text-xs font-semibold text-gray-700">{COLUMN_LABELS.vehicleNumber}</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-semibold text-foreground">{COLUMN_LABELS.vehicleNumber}</th>
|
||||
)}
|
||||
{visibleColumns.includes("vehicleType") && (
|
||||
<th className="px-3 py-2 text-left text-xs font-semibold text-gray-700">{COLUMN_LABELS.vehicleType}</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-semibold text-foreground">{COLUMN_LABELS.vehicleType}</th>
|
||||
)}
|
||||
{visibleColumns.includes("departure") && (
|
||||
<th className="px-3 py-2 text-left text-xs font-semibold text-gray-700">{COLUMN_LABELS.departure}</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-semibold text-foreground">{COLUMN_LABELS.departure}</th>
|
||||
)}
|
||||
{visibleColumns.includes("destination") && (
|
||||
<th className="px-3 py-2 text-left text-xs font-semibold text-gray-700">{COLUMN_LABELS.destination}</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-semibold text-foreground">{COLUMN_LABELS.destination}</th>
|
||||
)}
|
||||
{visibleColumns.includes("departureTime") && (
|
||||
<th className="px-3 py-2 text-left text-xs font-semibold text-gray-700">{COLUMN_LABELS.departureTime}</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-semibold text-foreground">{COLUMN_LABELS.departureTime}</th>
|
||||
)}
|
||||
{visibleColumns.includes("estimatedArrival") && (
|
||||
<th className="px-3 py-2 text-left text-xs font-semibold text-gray-700">
|
||||
<th className="px-3 py-2 text-left text-xs font-semibold text-foreground">
|
||||
{COLUMN_LABELS.estimatedArrival}
|
||||
</th>
|
||||
)}
|
||||
{visibleColumns.includes("phone") && (
|
||||
<th className="px-3 py-2 text-left text-xs font-semibold text-gray-700">{COLUMN_LABELS.phone}</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-semibold text-foreground">{COLUMN_LABELS.phone}</th>
|
||||
)}
|
||||
{visibleColumns.includes("progress") && (
|
||||
<th className="px-3 py-2 text-left text-xs font-semibold text-gray-700">{COLUMN_LABELS.progress}</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-semibold text-foreground">{COLUMN_LABELS.progress}</th>
|
||||
)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200 bg-white">
|
||||
<tbody className="divide-y divide-gray-200 bg-background">
|
||||
{drivers.map((driver) => {
|
||||
const statusColors = getStatusColor(driver.status);
|
||||
return (
|
||||
<tr key={driver.id} className="transition-colors hover:bg-gray-50">
|
||||
<tr key={driver.id} className="transition-colors hover:bg-muted">
|
||||
{visibleColumns.includes("status") && (
|
||||
<td className="px-3 py-2">
|
||||
<span
|
||||
|
|
@ -111,42 +111,42 @@ export function DriverListView({ drivers, config, isCompact = false }: DriverLis
|
|||
</td>
|
||||
)}
|
||||
{visibleColumns.includes("name") && (
|
||||
<td className="px-3 py-2 text-sm font-medium text-gray-900">{driver.name}</td>
|
||||
<td className="px-3 py-2 text-sm font-medium text-foreground">{driver.name}</td>
|
||||
)}
|
||||
{visibleColumns.includes("vehicleNumber") && (
|
||||
<td className="px-3 py-2 text-sm text-gray-700">{driver.vehicleNumber}</td>
|
||||
<td className="px-3 py-2 text-sm text-foreground">{driver.vehicleNumber}</td>
|
||||
)}
|
||||
{visibleColumns.includes("vehicleType") && (
|
||||
<td className="px-3 py-2 text-sm text-gray-600">{driver.vehicleType}</td>
|
||||
<td className="px-3 py-2 text-sm text-foreground">{driver.vehicleType}</td>
|
||||
)}
|
||||
{visibleColumns.includes("departure") && (
|
||||
<td className="px-3 py-2 text-sm text-gray-700">
|
||||
{driver.departure || <span className="text-gray-400">-</span>}
|
||||
<td className="px-3 py-2 text-sm text-foreground">
|
||||
{driver.departure || <span className="text-muted-foreground">-</span>}
|
||||
</td>
|
||||
)}
|
||||
{visibleColumns.includes("destination") && (
|
||||
<td className="px-3 py-2 text-sm text-gray-700">
|
||||
{driver.destination || <span className="text-gray-400">-</span>}
|
||||
<td className="px-3 py-2 text-sm text-foreground">
|
||||
{driver.destination || <span className="text-muted-foreground">-</span>}
|
||||
</td>
|
||||
)}
|
||||
{visibleColumns.includes("departureTime") && (
|
||||
<td className="px-3 py-2 text-sm text-gray-600">{formatTime(driver.departureTime)}</td>
|
||||
<td className="px-3 py-2 text-sm text-foreground">{formatTime(driver.departureTime)}</td>
|
||||
)}
|
||||
{visibleColumns.includes("estimatedArrival") && (
|
||||
<td className="px-3 py-2 text-sm text-gray-600">{formatTime(driver.estimatedArrival)}</td>
|
||||
<td className="px-3 py-2 text-sm text-foreground">{formatTime(driver.estimatedArrival)}</td>
|
||||
)}
|
||||
{visibleColumns.includes("phone") && (
|
||||
<td className="px-3 py-2 text-sm text-gray-600">{driver.phone}</td>
|
||||
<td className="px-3 py-2 text-sm text-foreground">{driver.phone}</td>
|
||||
)}
|
||||
{visibleColumns.includes("progress") && (
|
||||
<td className="px-3 py-2">
|
||||
{driver.progress !== undefined ? (
|
||||
<div className="flex items-center space-x-2">
|
||||
<Progress value={driver.progress} className="h-2 w-16" />
|
||||
<span className="text-xs text-gray-600">{driver.progress}%</span>
|
||||
<span className="text-xs text-foreground">{driver.progress}%</span>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-gray-400">-</span>
|
||||
<span className="text-muted-foreground">-</span>
|
||||
)}
|
||||
</td>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ export function DriverManagementSettings({ config, onSave, onClose }: DriverMana
|
|||
<Card
|
||||
key={key}
|
||||
className={`cursor-pointer border p-3 transition-colors ${
|
||||
localConfig.visibleColumns.includes(key) ? "border-primary bg-primary/5" : "hover:bg-gray-50"
|
||||
localConfig.visibleColumns.includes(key) ? "border-primary bg-primary/5" : "hover:bg-muted"
|
||||
}`}
|
||||
onClick={() => toggleColumn(key)}
|
||||
>
|
||||
|
|
@ -128,7 +128,7 @@ export function DriverManagementSettings({ config, onSave, onClose }: DriverMana
|
|||
</div>
|
||||
|
||||
{/* 푸터 - 고정 */}
|
||||
<div className="flex flex-shrink-0 justify-end gap-3 border-t border-gray-200 bg-gray-50 p-4">
|
||||
<div className="flex flex-shrink-0 justify-end gap-3 border-t border-border bg-muted p-4">
|
||||
<Button variant="outline" onClick={onClose}>
|
||||
취소
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -70,14 +70,14 @@ export function DriverManagementWidget({ element, onConfigUpdate }: DriverManage
|
|||
const isCompact = element.size.width < 400 || element.size.height < 300;
|
||||
|
||||
return (
|
||||
<div className="relative flex h-full w-full flex-col bg-white">
|
||||
<div className="relative flex h-full w-full flex-col bg-background">
|
||||
{/* 헤더 - 컴팩트 모드가 아닐 때만 표시 */}
|
||||
{!isCompact && (
|
||||
<div className="flex-shrink-0 border-b border-gray-200 bg-gray-50 px-3 py-2">
|
||||
<div className="flex-shrink-0 border-b border-border bg-muted px-3 py-2">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
{/* 검색 */}
|
||||
<div className="relative max-w-xs flex-1">
|
||||
<Search className="absolute left-2 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-gray-400" />
|
||||
<Search className="absolute left-2 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" />
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="기사명, 차량번호 검색"
|
||||
|
|
@ -132,20 +132,20 @@ export function DriverManagementWidget({ element, onConfigUpdate }: DriverManage
|
|||
</div>
|
||||
|
||||
{/* 통계 정보 */}
|
||||
<div className="mt-2 flex items-center gap-3 text-xs text-gray-600">
|
||||
<div className="mt-2 flex items-center gap-3 text-xs text-foreground">
|
||||
<span>
|
||||
전체 <span className="font-semibold text-gray-900">{filteredDrivers.length}</span>명
|
||||
전체 <span className="font-semibold text-foreground">{filteredDrivers.length}</span>명
|
||||
</span>
|
||||
<span className="text-gray-400">|</span>
|
||||
<span className="text-muted-foreground">|</span>
|
||||
<span>
|
||||
운행중{" "}
|
||||
<span className="font-semibold text-green-600">
|
||||
<span className="font-semibold text-success">
|
||||
{filteredDrivers.filter((d) => d.status === "driving").length}
|
||||
</span>
|
||||
명
|
||||
</span>
|
||||
<span className="text-gray-400">|</span>
|
||||
<span className="text-xs text-gray-500">최근 업데이트: {lastRefresh.toLocaleTimeString("ko-KR")}</span>
|
||||
<span className="text-muted-foreground">|</span>
|
||||
<span className="text-xs text-muted-foreground">최근 업데이트: {lastRefresh.toLocaleTimeString("ko-KR")}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -168,8 +168,8 @@ export function ListWidget({ element, onConfigUpdate }: ListWidgetProps) {
|
|||
return (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="mx-auto mb-2 h-6 w-6 animate-spin rounded-full border-2 border-blue-600 border-t-transparent" />
|
||||
<div className="text-sm text-gray-600">데이터 로딩 중...</div>
|
||||
<div className="mx-auto mb-2 h-6 w-6 animate-spin rounded-full border-2 border-primary border-t-transparent" />
|
||||
<div className="text-sm text-foreground">데이터 로딩 중...</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -181,8 +181,8 @@ export function ListWidget({ element, onConfigUpdate }: ListWidgetProps) {
|
|||
<div className="flex h-full w-full items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="mb-2 text-2xl">⚠️</div>
|
||||
<div className="text-sm font-medium text-red-600">오류 발생</div>
|
||||
<div className="mt-1 text-xs text-gray-500">{error}</div>
|
||||
<div className="text-sm font-medium text-destructive">오류 발생</div>
|
||||
<div className="mt-1 text-xs text-muted-foreground">{error}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -194,8 +194,8 @@ export function ListWidget({ element, onConfigUpdate }: ListWidgetProps) {
|
|||
<div className="flex h-full w-full flex-col items-center justify-center gap-4 p-4">
|
||||
<div className="text-center">
|
||||
<div className="mb-2 text-4xl">📋</div>
|
||||
<div className="text-sm font-medium text-gray-700">리스트를 설정하세요</div>
|
||||
<div className="mt-1 text-xs text-gray-500">⚙️ 버튼을 클릭하여 데이터 소스와 컬럼을 설정해주세요</div>
|
||||
<div className="text-sm font-medium text-foreground">리스트를 설정하세요</div>
|
||||
<div className="mt-1 text-xs text-muted-foreground">⚙️ 버튼을 클릭하여 데이터 소스와 컬럼을 설정해주세요</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -222,7 +222,7 @@ export function ListWidget({ element, onConfigUpdate }: ListWidgetProps) {
|
|||
<div className="flex h-full w-full flex-col p-4">
|
||||
{/* 제목 - 항상 표시 */}
|
||||
<div className="mb-4">
|
||||
<h3 className="text-sm font-semibold text-gray-700">{element.customTitle || element.title}</h3>
|
||||
<h3 className="text-sm font-semibold text-foreground">{element.customTitle || element.title}</h3>
|
||||
</div>
|
||||
|
||||
{/* 테이블 뷰 */}
|
||||
|
|
@ -251,7 +251,7 @@ export function ListWidget({ element, onConfigUpdate }: ListWidgetProps) {
|
|||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={displayColumns.filter((col) => col.visible).length}
|
||||
className="text-center text-gray-500"
|
||||
className="text-center text-muted-foreground"
|
||||
>
|
||||
데이터가 없습니다
|
||||
</TableCell>
|
||||
|
|
@ -281,7 +281,7 @@ export function ListWidget({ element, onConfigUpdate }: ListWidgetProps) {
|
|||
{config.viewMode === "card" && (
|
||||
<div className="flex-1 overflow-auto">
|
||||
{paginatedRows.length === 0 ? (
|
||||
<div className="flex h-full items-center justify-center text-gray-500">데이터가 없습니다</div>
|
||||
<div className="flex h-full items-center justify-center text-muted-foreground">데이터가 없습니다</div>
|
||||
) : (
|
||||
<div
|
||||
className={`grid gap-4 ${config.compactMode ? "text-xs" : "text-sm"}`}
|
||||
|
|
@ -296,9 +296,9 @@ export function ListWidget({ element, onConfigUpdate }: ListWidgetProps) {
|
|||
.filter((col) => col.visible)
|
||||
.map((col) => (
|
||||
<div key={col.id}>
|
||||
<div className="text-xs font-medium text-gray-500">{col.label || col.name}</div>
|
||||
<div className="text-xs font-medium text-muted-foreground">{col.label || col.name}</div>
|
||||
<div
|
||||
className={`font-medium text-gray-900 ${col.align === "center" ? "text-center" : col.align === "right" ? "text-right" : ""}`}
|
||||
className={`font-medium text-foreground ${col.align === "center" ? "text-center" : col.align === "right" ? "text-right" : ""}`}
|
||||
>
|
||||
{String(row[col.dataKey || col.field] ?? "")}
|
||||
</div>
|
||||
|
|
@ -315,7 +315,7 @@ export function ListWidget({ element, onConfigUpdate }: ListWidgetProps) {
|
|||
{/* 페이지네이션 */}
|
||||
{config.enablePagination && totalPages > 1 && (
|
||||
<div className="mt-4 flex items-center justify-between text-sm">
|
||||
<div className="text-gray-600">
|
||||
<div className="text-foreground">
|
||||
{startIdx + 1}-{Math.min(endIdx, data.rows.length)} / {data.rows.length}개
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
|
|
@ -328,9 +328,9 @@ export function ListWidget({ element, onConfigUpdate }: ListWidgetProps) {
|
|||
이전
|
||||
</Button>
|
||||
<div className="flex items-center gap-1 px-2">
|
||||
<span className="text-gray-700">{currentPage}</span>
|
||||
<span className="text-gray-400">/</span>
|
||||
<span className="text-gray-500">{totalPages}</span>
|
||||
<span className="text-foreground">{currentPage}</span>
|
||||
<span className="text-muted-foreground">/</span>
|
||||
<span className="text-muted-foreground">{totalPages}</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
|
|
|
|||
|
|
@ -142,15 +142,15 @@ export function ListWidgetConfigModal({ isOpen, element, onClose, onSave }: List
|
|||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
|
||||
<div className="flex max-h-[90vh] w-[90vw] max-w-6xl flex-col rounded-xl border bg-white shadow-2xl">
|
||||
<div className="flex max-h-[90vh] w-[90vw] max-w-6xl flex-col rounded-xl border bg-background shadow-2xl">
|
||||
{/* 헤더 */}
|
||||
<div className="space-y-4 border-b px-6 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-xl font-bold">📋 리스트 위젯 설정</h2>
|
||||
<p className="mt-1 text-sm text-gray-600">데이터 소스와 컬럼을 설정하세요</p>
|
||||
<p className="mt-1 text-sm text-foreground">데이터 소스와 컬럼을 설정하세요</p>
|
||||
</div>
|
||||
<button onClick={onClose} className="rounded-lg p-2 transition-colors hover:bg-gray-100">
|
||||
<button onClick={onClose} className="rounded-lg p-2 transition-colors hover:bg-muted">
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -173,34 +173,34 @@ export function ListWidgetConfigModal({ isOpen, element, onClose, onSave }: List
|
|||
</div>
|
||||
|
||||
{/* 참고: 리스트 위젯은 제목이 항상 표시됩니다 */}
|
||||
<div className="rounded bg-blue-50 p-2 text-xs text-blue-700">💡 리스트 위젯은 제목이 항상 표시됩니다</div>
|
||||
<div className="rounded bg-primary/10 p-2 text-xs text-primary">💡 리스트 위젯은 제목이 항상 표시됩니다</div>
|
||||
</div>
|
||||
|
||||
{/* 진행 상태 표시 */}
|
||||
<div className="border-b bg-gray-50 px-6 py-4">
|
||||
<div className="border-b bg-muted px-6 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className={`flex items-center gap-2 ${currentStep >= 1 ? "text-blue-600" : "text-gray-400"}`}>
|
||||
<div className={`flex items-center gap-2 ${currentStep >= 1 ? "text-primary" : "text-muted-foreground"}`}>
|
||||
<div
|
||||
className={`flex h-8 w-8 items-center justify-center rounded-full ${currentStep >= 1 ? "bg-blue-600 text-white" : "bg-gray-300"}`}
|
||||
className={`flex h-8 w-8 items-center justify-center rounded-full ${currentStep >= 1 ? "bg-primary text-white" : "bg-muted"}`}
|
||||
>
|
||||
1
|
||||
</div>
|
||||
<span className="text-sm font-medium">데이터 소스</span>
|
||||
</div>
|
||||
<div className="h-0.5 w-12 bg-gray-300" />
|
||||
<div className={`flex items-center gap-2 ${currentStep >= 2 ? "text-blue-600" : "text-gray-400"}`}>
|
||||
<div className="h-0.5 w-12 bg-muted" />
|
||||
<div className={`flex items-center gap-2 ${currentStep >= 2 ? "text-primary" : "text-muted-foreground"}`}>
|
||||
<div
|
||||
className={`flex h-8 w-8 items-center justify-center rounded-full ${currentStep >= 2 ? "bg-blue-600 text-white" : "bg-gray-300"}`}
|
||||
className={`flex h-8 w-8 items-center justify-center rounded-full ${currentStep >= 2 ? "bg-primary text-white" : "bg-muted"}`}
|
||||
>
|
||||
2
|
||||
</div>
|
||||
<span className="text-sm font-medium">데이터 가져오기</span>
|
||||
</div>
|
||||
<div className="h-0.5 w-12 bg-gray-300" />
|
||||
<div className={`flex items-center gap-2 ${currentStep >= 3 ? "text-blue-600" : "text-gray-400"}`}>
|
||||
<div className="h-0.5 w-12 bg-muted" />
|
||||
<div className={`flex items-center gap-2 ${currentStep >= 3 ? "text-primary" : "text-muted-foreground"}`}>
|
||||
<div
|
||||
className={`flex h-8 w-8 items-center justify-center rounded-full ${currentStep >= 3 ? "bg-blue-600 text-white" : "bg-gray-300"}`}
|
||||
className={`flex h-8 w-8 items-center justify-center rounded-full ${currentStep >= 3 ? "bg-primary text-white" : "bg-muted"}`}
|
||||
>
|
||||
3
|
||||
</div>
|
||||
|
|
@ -240,21 +240,21 @@ export function ListWidgetConfigModal({ isOpen, element, onClose, onSave }: List
|
|||
{/* 오른쪽: 데이터 미리보기 */}
|
||||
<div>
|
||||
{queryResult && queryResult.rows.length > 0 ? (
|
||||
<div className="rounded-lg border bg-gray-50 p-4">
|
||||
<h3 className="mb-3 font-semibold text-gray-800">📋 데이터 미리보기</h3>
|
||||
<div className="overflow-x-auto rounded bg-white p-3">
|
||||
<div className="rounded-lg border bg-muted p-4">
|
||||
<h3 className="mb-3 font-semibold text-foreground">📋 데이터 미리보기</h3>
|
||||
<div className="overflow-x-auto rounded bg-background p-3">
|
||||
<Badge variant="secondary" className="mb-2">
|
||||
{queryResult.totalRows}개 데이터
|
||||
</Badge>
|
||||
<pre className="text-xs text-gray-700">
|
||||
<pre className="text-xs text-foreground">
|
||||
{JSON.stringify(queryResult.rows.slice(0, 3), null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-full items-center justify-center rounded-lg border-2 border-dashed border-gray-300 bg-gray-50 p-8 text-center">
|
||||
<div className="flex h-full items-center justify-center rounded-lg border-2 border-dashed border-border bg-muted p-8 text-center">
|
||||
<div>
|
||||
<div className="mt-1 text-xs text-gray-500">데이터를 가져온 후 미리보기가 표시됩니다</div>
|
||||
<div className="mt-1 text-xs text-muted-foreground">데이터를 가져온 후 미리보기가 표시됩니다</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -288,10 +288,10 @@ export function ListWidgetConfigModal({ isOpen, element, onClose, onSave }: List
|
|||
</div>
|
||||
|
||||
{/* 푸터 */}
|
||||
<div className="flex items-center justify-between border-t bg-gray-50 p-6">
|
||||
<div className="flex items-center justify-between border-t bg-muted p-6">
|
||||
<div>
|
||||
{queryResult && (
|
||||
<Badge variant="default" className="bg-green-600">
|
||||
<Badge variant="default" className="bg-success">
|
||||
📊 {queryResult.rows.length}개 데이터 로드됨
|
||||
</Badge>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -132,31 +132,31 @@ export function ListWidgetConfigSidebar({ element, isOpen, onClose, onApply }: L
|
|||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"fixed top-14 left-0 z-[100] flex h-[calc(100vh-3.5rem)] w-80 flex-col bg-gray-50 transition-transform duration-300 ease-in-out",
|
||||
"fixed top-14 left-0 z-[100] flex h-[calc(100vh-3.5rem)] w-80 flex-col bg-muted transition-transform duration-300 ease-in-out",
|
||||
isOpen ? "translate-x-0" : "translate-x-[-100%]",
|
||||
)}
|
||||
>
|
||||
{/* 헤더 */}
|
||||
<div className="flex items-center justify-between bg-white px-3 py-2 shadow-sm">
|
||||
<div className="flex items-center justify-between bg-background px-3 py-2 shadow-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="bg-primary/10 flex h-6 w-6 items-center justify-center rounded">
|
||||
<span className="text-primary text-xs font-bold">📋</span>
|
||||
</div>
|
||||
<span className="text-xs font-semibold text-gray-900">리스트 위젯 설정</span>
|
||||
<span className="text-xs font-semibold text-foreground">리스트 위젯 설정</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="flex h-6 w-6 items-center justify-center rounded transition-colors hover:bg-gray-100"
|
||||
className="flex h-6 w-6 items-center justify-center rounded transition-colors hover:bg-muted"
|
||||
>
|
||||
<X className="h-3.5 w-3.5 text-gray-500" />
|
||||
<X className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 본문: 스크롤 가능 영역 */}
|
||||
<div className="flex-1 overflow-y-auto p-3">
|
||||
{/* 기본 설정 */}
|
||||
<div className="mb-3 rounded-lg bg-white p-3 shadow-sm">
|
||||
<div className="mb-2 text-[10px] font-semibold tracking-wide text-gray-500 uppercase">기본 설정</div>
|
||||
<div className="mb-3 rounded-lg bg-background p-3 shadow-sm">
|
||||
<div className="mb-2 text-[10px] font-semibold tracking-wide text-muted-foreground uppercase">기본 설정</div>
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<input
|
||||
|
|
@ -165,31 +165,31 @@ export function ListWidgetConfigSidebar({ element, isOpen, onClose, onApply }: L
|
|||
onChange={(e) => setTitle(e.target.value)}
|
||||
onKeyDown={(e) => e.stopPropagation()}
|
||||
placeholder="리스트 이름"
|
||||
className="focus:border-primary focus:ring-primary/20 h-8 w-full rounded border border-gray-200 bg-gray-50 px-2 text-xs placeholder:text-gray-400 focus:bg-white focus:ring-1 focus:outline-none"
|
||||
className="focus:border-primary focus:ring-primary/20 h-8 w-full rounded border border-border bg-muted px-2 text-xs placeholder:text-muted-foreground focus:bg-background focus:ring-1 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 데이터 소스 */}
|
||||
<div className="rounded-lg bg-white p-3 shadow-sm">
|
||||
<div className="mb-2 text-[10px] font-semibold tracking-wide text-gray-500 uppercase">데이터 소스</div>
|
||||
<div className="rounded-lg bg-background p-3 shadow-sm">
|
||||
<div className="mb-2 text-[10px] font-semibold tracking-wide text-muted-foreground uppercase">데이터 소스</div>
|
||||
|
||||
<Tabs
|
||||
defaultValue={dataSource.type}
|
||||
onValueChange={(value) => handleDataSourceTypeChange(value as "database" | "api")}
|
||||
className="w-full"
|
||||
>
|
||||
<TabsList className="grid h-7 w-full grid-cols-2 bg-gray-100 p-0.5">
|
||||
<TabsList className="grid h-7 w-full grid-cols-2 bg-muted p-0.5">
|
||||
<TabsTrigger
|
||||
value="database"
|
||||
className="h-6 rounded text-[11px] data-[state=active]:bg-white data-[state=active]:shadow-sm"
|
||||
className="h-6 rounded text-[11px] data-[state=active]:bg-background data-[state=active]:shadow-sm"
|
||||
>
|
||||
데이터베이스
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="api"
|
||||
className="h-6 rounded text-[11px] data-[state=active]:bg-white data-[state=active]:shadow-sm"
|
||||
className="h-6 rounded text-[11px] data-[state=active]:bg-background data-[state=active]:shadow-sm"
|
||||
>
|
||||
REST API
|
||||
</TabsTrigger>
|
||||
|
|
@ -211,17 +211,17 @@ export function ListWidgetConfigSidebar({ element, isOpen, onClose, onApply }: L
|
|||
|
||||
{/* 데이터 로드 상태 */}
|
||||
{queryResult && (
|
||||
<div className="mt-2 flex items-center gap-1.5 rounded bg-green-50 px-2 py-1">
|
||||
<div className="h-1.5 w-1.5 rounded-full bg-green-500" />
|
||||
<span className="text-[10px] font-medium text-green-700">{queryResult.rows.length}개 데이터 로드됨</span>
|
||||
<div className="mt-2 flex items-center gap-1.5 rounded bg-success/10 px-2 py-1">
|
||||
<div className="h-1.5 w-1.5 rounded-full bg-success" />
|
||||
<span className="text-[10px] font-medium text-success">{queryResult.rows.length}개 데이터 로드됨</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 컬럼 설정 - 쿼리 실행 후에만 표시 */}
|
||||
{queryResult && (
|
||||
<div className="mt-3 rounded-lg bg-white p-3 shadow-sm">
|
||||
<div className="mb-2 text-[10px] font-semibold tracking-wide text-gray-500 uppercase">컬럼 설정</div>
|
||||
<div className="mt-3 rounded-lg bg-background p-3 shadow-sm">
|
||||
<div className="mb-2 text-[10px] font-semibold tracking-wide text-muted-foreground uppercase">컬럼 설정</div>
|
||||
<UnifiedColumnEditor
|
||||
queryResult={queryResult}
|
||||
config={listConfig}
|
||||
|
|
@ -232,18 +232,18 @@ export function ListWidgetConfigSidebar({ element, isOpen, onClose, onApply }: L
|
|||
|
||||
{/* 테이블 옵션 - 컬럼이 있을 때만 표시 */}
|
||||
{listConfig.columns.length > 0 && (
|
||||
<div className="mt-3 rounded-lg bg-white p-3 shadow-sm">
|
||||
<div className="mb-2 text-[10px] font-semibold tracking-wide text-gray-500 uppercase">테이블 옵션</div>
|
||||
<div className="mt-3 rounded-lg bg-background p-3 shadow-sm">
|
||||
<div className="mb-2 text-[10px] font-semibold tracking-wide text-muted-foreground uppercase">테이블 옵션</div>
|
||||
<ListTableOptions config={listConfig} onConfigChange={handleListConfigChange} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 푸터: 적용 버튼 */}
|
||||
<div className="flex gap-2 bg-white p-2 shadow-[0_-2px_8px_rgba(0,0,0,0.05)]">
|
||||
<div className="flex gap-2 bg-background p-2 shadow-[0_-2px_8px_rgba(0,0,0,0.05)]">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="flex-1 rounded bg-gray-100 py-2 text-xs font-medium text-gray-700 transition-colors hover:bg-gray-200"
|
||||
className="flex-1 rounded bg-muted py-2 text-xs font-medium text-foreground transition-colors hover:bg-muted"
|
||||
>
|
||||
취소
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -67,11 +67,11 @@ export function MonthView({ days, config, isCompact = false, selectedDate, onDat
|
|||
const sizeClass = isCompact ? "text-xs" : "text-sm";
|
||||
const cursorClass = day.isCurrentMonth ? "cursor-pointer" : "cursor-default";
|
||||
|
||||
let colorClass = "text-gray-700";
|
||||
let colorClass = "text-foreground";
|
||||
|
||||
// 현재 월이 아닌 날짜
|
||||
if (!day.isCurrentMonth) {
|
||||
colorClass = "text-gray-300";
|
||||
colorClass = "text-muted-foreground";
|
||||
}
|
||||
// 선택된 날짜
|
||||
else if (isSelected(day)) {
|
||||
|
|
@ -87,7 +87,7 @@ export function MonthView({ days, config, isCompact = false, selectedDate, onDat
|
|||
}
|
||||
// 주말
|
||||
else if (config.highlightWeekends && day.isWeekend) {
|
||||
colorClass = "text-red-600";
|
||||
colorClass = "text-destructive";
|
||||
}
|
||||
|
||||
let bgClass = "";
|
||||
|
|
@ -96,7 +96,7 @@ export function MonthView({ days, config, isCompact = false, selectedDate, onDat
|
|||
} else if (config.highlightToday && day.isToday) {
|
||||
bgClass = "";
|
||||
} else {
|
||||
bgClass = "hover:bg-gray-100";
|
||||
bgClass = "hover:bg-muted";
|
||||
}
|
||||
|
||||
return `${baseClass} ${sizeClass} ${colorClass} ${bgClass} ${cursorClass}`;
|
||||
|
|
@ -112,7 +112,7 @@ export function MonthView({ days, config, isCompact = false, selectedDate, onDat
|
|||
return (
|
||||
<div
|
||||
key={name}
|
||||
className={`text-center text-xs font-semibold ${isWeekend && config.highlightWeekends ? "text-red-600" : "text-gray-600"}`}
|
||||
className={`text-center text-xs font-semibold ${isWeekend && config.highlightWeekends ? "text-destructive" : "text-foreground"}`}
|
||||
>
|
||||
{name}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -233,41 +233,41 @@ export function TodoWidgetConfigModal({ isOpen, element, onClose, onSave }: Todo
|
|||
|
||||
return (
|
||||
<div className="fixed inset-0 z-[100] flex items-center justify-center bg-black/50">
|
||||
<div className="relative flex h-[90vh] w-[90vw] max-w-6xl flex-col rounded-lg bg-white shadow-xl">
|
||||
<div className="relative flex h-[90vh] w-[90vw] max-w-6xl flex-col rounded-lg bg-background shadow-xl">
|
||||
{/* 헤더 */}
|
||||
<div className="flex items-center justify-between border-b border-gray-200 px-6 py-4">
|
||||
<div className="flex items-center justify-between border-b border-border px-6 py-4">
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-gray-800">일정관리 위젯 설정</h2>
|
||||
<p className="mt-1 text-sm text-gray-500">
|
||||
<h2 className="text-xl font-bold text-foreground">일정관리 위젯 설정</h2>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
데이터 소스와 쿼리를 설정하면 자동으로 일정 목록이 표시됩니다
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="rounded-lg p-2 text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-700"
|
||||
className="rounded-lg p-2 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 진행 상태 */}
|
||||
<div className="border-b border-gray-200 bg-gray-50 px-6 py-3">
|
||||
<div className="border-b border-border bg-muted px-6 py-3">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className={`flex items-center gap-2 ${currentStep === 1 ? "text-primary" : "text-gray-400"}`}>
|
||||
<div className={`flex items-center gap-2 ${currentStep === 1 ? "text-primary" : "text-muted-foreground"}`}>
|
||||
<div
|
||||
className={`flex h-8 w-8 items-center justify-center rounded-full font-semibold ${
|
||||
currentStep === 1 ? "bg-primary text-white" : "bg-gray-200"
|
||||
currentStep === 1 ? "bg-primary text-white" : "bg-muted"
|
||||
}`}
|
||||
>
|
||||
1
|
||||
</div>
|
||||
<span className="font-medium">데이터 소스 선택</span>
|
||||
</div>
|
||||
<ChevronRight className="h-4 w-4 text-gray-400" />
|
||||
<div className={`flex items-center gap-2 ${currentStep === 2 ? "text-primary" : "text-gray-400"}`}>
|
||||
<ChevronRight className="h-4 w-4 text-muted-foreground" />
|
||||
<div className={`flex items-center gap-2 ${currentStep === 2 ? "text-primary" : "text-muted-foreground"}`}>
|
||||
<div
|
||||
className={`flex h-8 w-8 items-center justify-center rounded-full font-semibold ${
|
||||
currentStep === 2 ? "bg-primary text-white" : "bg-gray-200"
|
||||
currentStep === 2 ? "bg-primary text-white" : "bg-muted"
|
||||
}`}
|
||||
>
|
||||
2
|
||||
|
|
@ -312,47 +312,47 @@ export function TodoWidgetConfigModal({ isOpen, element, onClose, onSave }: Todo
|
|||
{currentStep === 2 && (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<div className="mb-4 rounded-lg bg-blue-50 p-4">
|
||||
<h3 className="mb-2 font-semibold text-blue-900">💡 컬럼명 가이드</h3>
|
||||
<p className="mb-2 text-sm text-blue-700">
|
||||
<div className="mb-4 rounded-lg bg-primary/10 p-4">
|
||||
<h3 className="mb-2 font-semibold text-primary">💡 컬럼명 가이드</h3>
|
||||
<p className="mb-2 text-sm text-primary">
|
||||
쿼리 결과에 다음 컬럼명이 있으면 자동으로 일정 항목으로 변환됩니다:
|
||||
</p>
|
||||
<ul className="space-y-1 text-sm text-blue-600">
|
||||
<ul className="space-y-1 text-sm text-primary">
|
||||
<li>
|
||||
<code className="rounded bg-blue-100 px-1 py-0.5">id</code> - 고유 ID (없으면 자동 생성)
|
||||
<code className="rounded bg-primary/10 px-1 py-0.5">id</code> - 고유 ID (없으면 자동 생성)
|
||||
</li>
|
||||
<li>
|
||||
<code className="rounded bg-blue-100 px-1 py-0.5">title</code>,{" "}
|
||||
<code className="rounded bg-blue-100 px-1 py-0.5">task</code>,{" "}
|
||||
<code className="rounded bg-blue-100 px-1 py-0.5">name</code> - 제목 (필수)
|
||||
<code className="rounded bg-primary/10 px-1 py-0.5">title</code>,{" "}
|
||||
<code className="rounded bg-primary/10 px-1 py-0.5">task</code>,{" "}
|
||||
<code className="rounded bg-primary/10 px-1 py-0.5">name</code> - 제목 (필수)
|
||||
</li>
|
||||
<li>
|
||||
<code className="rounded bg-blue-100 px-1 py-0.5">description</code>,{" "}
|
||||
<code className="rounded bg-blue-100 px-1 py-0.5">desc</code>,{" "}
|
||||
<code className="rounded bg-blue-100 px-1 py-0.5">content</code> - 상세 설명
|
||||
<code className="rounded bg-primary/10 px-1 py-0.5">description</code>,{" "}
|
||||
<code className="rounded bg-primary/10 px-1 py-0.5">desc</code>,{" "}
|
||||
<code className="rounded bg-primary/10 px-1 py-0.5">content</code> - 상세 설명
|
||||
</li>
|
||||
<li>
|
||||
<code className="rounded bg-blue-100 px-1 py-0.5">priority</code> - 우선순위 (urgent, high,
|
||||
<code className="rounded bg-primary/10 px-1 py-0.5">priority</code> - 우선순위 (urgent, high,
|
||||
normal, low)
|
||||
</li>
|
||||
<li>
|
||||
<code className="rounded bg-blue-100 px-1 py-0.5">status</code> - 상태 (pending, in_progress,
|
||||
<code className="rounded bg-primary/10 px-1 py-0.5">status</code> - 상태 (pending, in_progress,
|
||||
completed)
|
||||
</li>
|
||||
<li>
|
||||
<code className="rounded bg-blue-100 px-1 py-0.5">assigned_to</code>,{" "}
|
||||
<code className="rounded bg-blue-100 px-1 py-0.5">assignedTo</code>,{" "}
|
||||
<code className="rounded bg-blue-100 px-1 py-0.5">user</code> - 담당자
|
||||
<code className="rounded bg-primary/10 px-1 py-0.5">assigned_to</code>,{" "}
|
||||
<code className="rounded bg-primary/10 px-1 py-0.5">assignedTo</code>,{" "}
|
||||
<code className="rounded bg-primary/10 px-1 py-0.5">user</code> - 담당자
|
||||
</li>
|
||||
<li>
|
||||
<code className="rounded bg-blue-100 px-1 py-0.5">due_date</code>,{" "}
|
||||
<code className="rounded bg-blue-100 px-1 py-0.5">dueDate</code>,{" "}
|
||||
<code className="rounded bg-blue-100 px-1 py-0.5">deadline</code> - 마감일
|
||||
<code className="rounded bg-primary/10 px-1 py-0.5">due_date</code>,{" "}
|
||||
<code className="rounded bg-primary/10 px-1 py-0.5">dueDate</code>,{" "}
|
||||
<code className="rounded bg-primary/10 px-1 py-0.5">deadline</code> - 마감일
|
||||
</li>
|
||||
<li>
|
||||
<code className="rounded bg-blue-100 px-1 py-0.5">is_urgent</code>,{" "}
|
||||
<code className="rounded bg-blue-100 px-1 py-0.5">isUrgent</code>,{" "}
|
||||
<code className="rounded bg-blue-100 px-1 py-0.5">urgent</code> - 긴급 여부
|
||||
<code className="rounded bg-primary/10 px-1 py-0.5">is_urgent</code>,{" "}
|
||||
<code className="rounded bg-primary/10 px-1 py-0.5">isUrgent</code>,{" "}
|
||||
<code className="rounded bg-primary/10 px-1 py-0.5">urgent</code> - 긴급 여부
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
@ -365,26 +365,26 @@ export function TodoWidgetConfigModal({ isOpen, element, onClose, onSave }: Todo
|
|||
</div>
|
||||
|
||||
{/* 디버그: 항상 표시되는 테스트 메시지 */}
|
||||
<div className="mt-4 rounded-lg bg-yellow-50 border-2 border-yellow-500 p-4">
|
||||
<p className="text-sm font-bold text-yellow-900">
|
||||
<div className="mt-4 rounded-lg bg-warning/10 border-2 border-warning p-4">
|
||||
<p className="text-sm font-bold text-warning">
|
||||
🔍 디버그: queryResult 상태 = {queryResult ? "있음" : "없음"}
|
||||
</p>
|
||||
{queryResult && (
|
||||
<p className="text-xs text-yellow-700 mt-1">
|
||||
<p className="text-xs text-warning mt-1">
|
||||
rows: {queryResult.rows?.length}개, error: {queryResult.error || "없음"}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{queryResult && !queryResult.error && queryResult.rows && queryResult.rows.length > 0 && (
|
||||
<div className="mt-4 rounded-lg bg-green-50 border-2 border-green-500 p-4">
|
||||
<h3 className="mb-2 font-semibold text-green-900">✅ 쿼리 테스트 성공!</h3>
|
||||
<p className="text-sm text-green-700">
|
||||
<div className="mt-4 rounded-lg bg-success/10 border-2 border-success p-4">
|
||||
<h3 className="mb-2 font-semibold text-success">✅ 쿼리 테스트 성공!</h3>
|
||||
<p className="text-sm text-success">
|
||||
총 <strong>{queryResult.rows.length}개</strong>의 일정 항목을 찾았습니다.
|
||||
</p>
|
||||
<div className="mt-3 rounded bg-white p-3">
|
||||
<p className="mb-2 text-xs font-semibold text-gray-600">첫 번째 데이터 미리보기:</p>
|
||||
<pre className="overflow-x-auto text-xs text-gray-700">
|
||||
<div className="mt-3 rounded bg-background p-3">
|
||||
<p className="mb-2 text-xs font-semibold text-foreground">첫 번째 데이터 미리보기:</p>
|
||||
<pre className="overflow-x-auto text-xs text-foreground">
|
||||
{JSON.stringify(queryResult.rows[0], null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
|
|
@ -392,10 +392,10 @@ export function TodoWidgetConfigModal({ isOpen, element, onClose, onSave }: Todo
|
|||
)}
|
||||
|
||||
{/* 데이터베이스 연동 쿼리 (선택사항) */}
|
||||
<div className="mt-6 space-y-4 rounded-lg border-2 border-purple-200 bg-purple-50 p-4">
|
||||
<div className="mt-6 space-y-4 rounded-lg border-2 border-purple-500 bg-purple-500/10 p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="font-semibold text-purple-900">🔗 데이터베이스 연동 (선택사항)</h3>
|
||||
<h3 className="font-semibold text-purple-700">🔗 데이터베이스 연동 (선택사항)</h3>
|
||||
<p className="text-sm text-purple-700">
|
||||
위젯에서 추가/수정/삭제 시 데이터베이스에 직접 반영
|
||||
</p>
|
||||
|
|
@ -405,9 +405,9 @@ export function TodoWidgetConfigModal({ isOpen, element, onClose, onSave }: Todo
|
|||
type="checkbox"
|
||||
checked={enableDbSync}
|
||||
onChange={(e) => setEnableDbSync(e.target.checked)}
|
||||
className="h-4 w-4 rounded border-purple-300"
|
||||
className="h-4 w-4 rounded border-purple-500/50"
|
||||
/>
|
||||
<span className="text-sm font-medium text-purple-900">활성화</span>
|
||||
<span className="text-sm font-medium text-purple-700">활성화</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
|
|
@ -419,8 +419,8 @@ export function TodoWidgetConfigModal({ isOpen, element, onClose, onSave }: Todo
|
|||
onClick={() => setDbSyncMode("simple")}
|
||||
className={`flex-1 rounded px-4 py-2 text-sm font-medium transition-colors ${
|
||||
dbSyncMode === "simple"
|
||||
? "bg-purple-600 text-white"
|
||||
: "bg-white text-purple-600 hover:bg-purple-100"
|
||||
? "bg-purple-500 text-white"
|
||||
: "bg-background text-purple-500 hover:bg-purple-500/10"
|
||||
}`}
|
||||
>
|
||||
간편 모드
|
||||
|
|
@ -429,8 +429,8 @@ export function TodoWidgetConfigModal({ isOpen, element, onClose, onSave }: Todo
|
|||
onClick={() => setDbSyncMode("advanced")}
|
||||
className={`flex-1 rounded px-4 py-2 text-sm font-medium transition-colors ${
|
||||
dbSyncMode === "advanced"
|
||||
? "bg-purple-600 text-white"
|
||||
: "bg-white text-purple-600 hover:bg-purple-100"
|
||||
? "bg-purple-500 text-white"
|
||||
: "bg-background text-purple-500 hover:bg-purple-500/10"
|
||||
}`}
|
||||
>
|
||||
고급 모드
|
||||
|
|
@ -439,14 +439,14 @@ export function TodoWidgetConfigModal({ isOpen, element, onClose, onSave }: Todo
|
|||
|
||||
{/* 간편 모드 */}
|
||||
{dbSyncMode === "simple" && (
|
||||
<div className="space-y-4 rounded-lg border border-purple-300 bg-white p-4">
|
||||
<div className="space-y-4 rounded-lg border border-purple-500/50 bg-background p-4">
|
||||
<p className="text-sm text-purple-700">
|
||||
테이블명과 컬럼 매핑만 입력하면 자동으로 INSERT/UPDATE/DELETE 쿼리가 생성됩니다.
|
||||
</p>
|
||||
|
||||
{/* 테이블명 */}
|
||||
<div>
|
||||
<Label className="text-sm font-semibold text-purple-900">테이블명 *</Label>
|
||||
<Label className="text-sm font-semibold text-purple-700">테이블명 *</Label>
|
||||
<Input
|
||||
value={tableName}
|
||||
onChange={(e) => setTableName(e.target.value)}
|
||||
|
|
@ -457,10 +457,10 @@ export function TodoWidgetConfigModal({ isOpen, element, onClose, onSave }: Todo
|
|||
|
||||
{/* 컬럼 매핑 */}
|
||||
<div>
|
||||
<Label className="text-sm font-semibold text-purple-900">컬럼 매핑</Label>
|
||||
<Label className="text-sm font-semibold text-purple-700">컬럼 매핑</Label>
|
||||
<div className="mt-2 grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="text-xs text-gray-600">ID 컬럼</label>
|
||||
<label className="text-xs text-foreground">ID 컬럼</label>
|
||||
<Input
|
||||
value={columnMapping.id}
|
||||
onChange={(e) => setColumnMapping({ ...columnMapping, id: e.target.value })}
|
||||
|
|
@ -469,7 +469,7 @@ export function TodoWidgetConfigModal({ isOpen, element, onClose, onSave }: Todo
|
|||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-gray-600">제목 컬럼</label>
|
||||
<label className="text-xs text-foreground">제목 컬럼</label>
|
||||
<Input
|
||||
value={columnMapping.title}
|
||||
onChange={(e) => setColumnMapping({ ...columnMapping, title: e.target.value })}
|
||||
|
|
@ -478,7 +478,7 @@ export function TodoWidgetConfigModal({ isOpen, element, onClose, onSave }: Todo
|
|||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-gray-600">설명 컬럼</label>
|
||||
<label className="text-xs text-foreground">설명 컬럼</label>
|
||||
<Input
|
||||
value={columnMapping.description}
|
||||
onChange={(e) => setColumnMapping({ ...columnMapping, description: e.target.value })}
|
||||
|
|
@ -487,7 +487,7 @@ export function TodoWidgetConfigModal({ isOpen, element, onClose, onSave }: Todo
|
|||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-gray-600">우선순위 컬럼</label>
|
||||
<label className="text-xs text-foreground">우선순위 컬럼</label>
|
||||
<Input
|
||||
value={columnMapping.priority}
|
||||
onChange={(e) => setColumnMapping({ ...columnMapping, priority: e.target.value })}
|
||||
|
|
@ -496,7 +496,7 @@ export function TodoWidgetConfigModal({ isOpen, element, onClose, onSave }: Todo
|
|||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-gray-600">상태 컬럼</label>
|
||||
<label className="text-xs text-foreground">상태 컬럼</label>
|
||||
<Input
|
||||
value={columnMapping.status}
|
||||
onChange={(e) => setColumnMapping({ ...columnMapping, status: e.target.value })}
|
||||
|
|
@ -505,7 +505,7 @@ export function TodoWidgetConfigModal({ isOpen, element, onClose, onSave }: Todo
|
|||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-gray-600">담당자 컬럼</label>
|
||||
<label className="text-xs text-foreground">담당자 컬럼</label>
|
||||
<Input
|
||||
value={columnMapping.assignedTo}
|
||||
onChange={(e) => setColumnMapping({ ...columnMapping, assignedTo: e.target.value })}
|
||||
|
|
@ -514,7 +514,7 @@ export function TodoWidgetConfigModal({ isOpen, element, onClose, onSave }: Todo
|
|||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-gray-600">마감일 컬럼</label>
|
||||
<label className="text-xs text-foreground">마감일 컬럼</label>
|
||||
<Input
|
||||
value={columnMapping.dueDate}
|
||||
onChange={(e) => setColumnMapping({ ...columnMapping, dueDate: e.target.value })}
|
||||
|
|
@ -523,7 +523,7 @@ export function TodoWidgetConfigModal({ isOpen, element, onClose, onSave }: Todo
|
|||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-gray-600">긴급 여부 컬럼</label>
|
||||
<label className="text-xs text-foreground">긴급 여부 컬럼</label>
|
||||
<Input
|
||||
value={columnMapping.isUrgent}
|
||||
onChange={(e) => setColumnMapping({ ...columnMapping, isUrgent: e.target.value })}
|
||||
|
|
@ -545,8 +545,8 @@ export function TodoWidgetConfigModal({ isOpen, element, onClose, onSave }: Todo
|
|||
|
||||
{/* INSERT 쿼리 */}
|
||||
<div>
|
||||
<Label className="text-sm font-semibold text-purple-900">INSERT 쿼리 (추가)</Label>
|
||||
<p className="mb-2 text-xs text-purple-600">
|
||||
<Label className="text-sm font-semibold text-purple-700">INSERT 쿼리 (추가)</Label>
|
||||
<p className="mb-2 text-xs text-purple-500">
|
||||
사용 가능한 변수: ${"{title}"}, ${"{description}"}, ${"{priority}"}, ${"{status}"}, ${"{assignedTo}"}, ${"{dueDate}"}, ${"{isUrgent}"}
|
||||
</p>
|
||||
<textarea
|
||||
|
|
@ -562,14 +562,14 @@ export function TodoWidgetConfigModal({ isOpen, element, onClose, onSave }: Todo
|
|||
Object.assign(element, updates);
|
||||
}}
|
||||
placeholder="예: INSERT INTO tasks (title, description, status) VALUES ('${title}', '${description}', '${status}')"
|
||||
className="h-20 w-full rounded border border-purple-300 bg-white px-3 py-2 text-sm font-mono focus:border-purple-500 focus:outline-none"
|
||||
className="h-20 w-full rounded border border-purple-500/50 bg-background px-3 py-2 text-sm font-mono focus:border-purple-500 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* UPDATE 쿼리 */}
|
||||
<div>
|
||||
<Label className="text-sm font-semibold text-purple-900">UPDATE 쿼리 (상태 변경)</Label>
|
||||
<p className="mb-2 text-xs text-purple-600">
|
||||
<Label className="text-sm font-semibold text-purple-700">UPDATE 쿼리 (상태 변경)</Label>
|
||||
<p className="mb-2 text-xs text-purple-500">
|
||||
사용 가능한 변수: ${"{id}"}, ${"{status}"}
|
||||
</p>
|
||||
<textarea
|
||||
|
|
@ -585,14 +585,14 @@ export function TodoWidgetConfigModal({ isOpen, element, onClose, onSave }: Todo
|
|||
Object.assign(element, updates);
|
||||
}}
|
||||
placeholder="예: UPDATE tasks SET status = '${status}' WHERE id = ${id}"
|
||||
className="h-20 w-full rounded border border-purple-300 bg-white px-3 py-2 text-sm font-mono focus:border-purple-500 focus:outline-none"
|
||||
className="h-20 w-full rounded border border-purple-500/50 bg-background px-3 py-2 text-sm font-mono focus:border-purple-500 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* DELETE 쿼리 */}
|
||||
<div>
|
||||
<Label className="text-sm font-semibold text-purple-900">DELETE 쿼리 (삭제)</Label>
|
||||
<p className="mb-2 text-xs text-purple-600">
|
||||
<Label className="text-sm font-semibold text-purple-700">DELETE 쿼리 (삭제)</Label>
|
||||
<p className="mb-2 text-xs text-purple-500">
|
||||
사용 가능한 변수: ${"{id}"}
|
||||
</p>
|
||||
<textarea
|
||||
|
|
@ -608,7 +608,7 @@ export function TodoWidgetConfigModal({ isOpen, element, onClose, onSave }: Todo
|
|||
Object.assign(element, updates);
|
||||
}}
|
||||
placeholder="예: DELETE FROM tasks WHERE id = ${id}"
|
||||
className="h-20 w-full rounded border border-purple-300 bg-white px-3 py-2 text-sm font-mono focus:border-purple-500 focus:outline-none"
|
||||
className="h-20 w-full rounded border border-purple-500/50 bg-background px-3 py-2 text-sm font-mono focus:border-purple-500 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -621,7 +621,7 @@ export function TodoWidgetConfigModal({ isOpen, element, onClose, onSave }: Todo
|
|||
</div>
|
||||
|
||||
{/* 하단 버튼 */}
|
||||
<div className="flex items-center justify-between border-t border-gray-200 px-6 py-4">
|
||||
<div className="flex items-center justify-between border-t border-border px-6 py-4">
|
||||
<div>
|
||||
{currentStep > 1 && (
|
||||
<Button onClick={handlePrev} variant="outline">
|
||||
|
|
|
|||
|
|
@ -137,11 +137,11 @@ export default function YardManagement3DWidget({
|
|||
// 편집 모드: 레이아웃 선택 UI
|
||||
if (isEditMode) {
|
||||
return (
|
||||
<div className="widget-interactive-area flex h-full w-full flex-col bg-white">
|
||||
<div className="widget-interactive-area flex h-full w-full flex-col bg-background">
|
||||
<div className="flex items-center justify-between border-b p-4">
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-gray-700">야드 레이아웃 선택</h3>
|
||||
<p className="mt-1 text-xs text-gray-500">
|
||||
<h3 className="text-sm font-semibold text-foreground">야드 레이아웃 선택</h3>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
{config?.layoutName ? `선택됨: ${config.layoutName}` : "표시할 야드 레이아웃을 선택하세요"}
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -153,14 +153,14 @@ export default function YardManagement3DWidget({
|
|||
<div className="flex-1 overflow-auto p-4">
|
||||
{isLoading ? (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<div className="text-sm text-gray-500">로딩 중...</div>
|
||||
<div className="text-sm text-muted-foreground">로딩 중...</div>
|
||||
</div>
|
||||
) : layouts.length === 0 ? (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="mb-2 text-4xl">🏗️</div>
|
||||
<div className="text-sm text-gray-600">생성된 야드 레이아웃이 없습니다</div>
|
||||
<div className="mt-1 text-xs text-gray-400">먼저 야드 레이아웃을 생성하세요</div>
|
||||
<div className="text-sm text-foreground">생성된 야드 레이아웃이 없습니다</div>
|
||||
<div className="mt-1 text-xs text-muted-foreground">먼저 야드 레이아웃을 생성하세요</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
|
|
@ -169,17 +169,17 @@ export default function YardManagement3DWidget({
|
|||
<div
|
||||
key={layout.id}
|
||||
className={`rounded-lg border p-3 transition-all ${
|
||||
config?.layoutId === layout.id ? "border-blue-500 bg-blue-50" : "border-gray-200 bg-white"
|
||||
config?.layoutId === layout.id ? "border-primary bg-primary/10" : "border-border bg-background"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<button onClick={() => handleSelectLayout(layout)} className="flex-1 text-left hover:opacity-80">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-gray-900">{layout.name}</span>
|
||||
{config?.layoutId === layout.id && <Check className="h-4 w-4 text-blue-600" />}
|
||||
<span className="font-medium text-foreground">{layout.name}</span>
|
||||
{config?.layoutId === layout.id && <Check className="h-4 w-4 text-primary" />}
|
||||
</div>
|
||||
{layout.description && <p className="mt-1 text-xs text-gray-500">{layout.description}</p>}
|
||||
<div className="mt-2 text-xs text-gray-400">배치된 자재: {layout.placement_count}개</div>
|
||||
{layout.description && <p className="mt-1 text-xs text-muted-foreground">{layout.description}</p>}
|
||||
<div className="mt-2 text-xs text-muted-foreground">배치된 자재: {layout.placement_count}개</div>
|
||||
</button>
|
||||
<div className="flex gap-1">
|
||||
<Button
|
||||
|
|
@ -195,7 +195,7 @@ export default function YardManagement3DWidget({
|
|||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-red-600 hover:bg-red-50"
|
||||
className="text-destructive hover:bg-destructive/10"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setDeleteLayoutId(layout.id);
|
||||
|
|
@ -230,18 +230,18 @@ export default function YardManagement3DWidget({
|
|||
<DialogTitle>야드 레이아웃 삭제</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm text-gray-600">
|
||||
<p className="text-sm text-foreground">
|
||||
이 야드 레이아웃을 삭제하시겠습니까?
|
||||
<br />
|
||||
레이아웃 내의 모든 배치 정보도 함께 삭제됩니다.
|
||||
<br />
|
||||
<span className="font-semibold text-red-600">이 작업은 되돌릴 수 없습니다.</span>
|
||||
<span className="font-semibold text-destructive">이 작업은 되돌릴 수 없습니다.</span>
|
||||
</p>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button variant="outline" onClick={() => setDeleteLayoutId(null)}>
|
||||
취소
|
||||
</Button>
|
||||
<Button onClick={handleDeleteLayout} className="bg-red-600 hover:bg-red-700">
|
||||
<Button onClick={handleDeleteLayout} className="bg-destructive hover:bg-destructive/90">
|
||||
삭제
|
||||
</Button>
|
||||
</div>
|
||||
|
|
@ -256,12 +256,12 @@ export default function YardManagement3DWidget({
|
|||
if (!config?.layoutId) {
|
||||
console.warn("⚠️ 야드관리 위젯: layoutId가 설정되지 않음", { config, isEditMode });
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center bg-gray-50">
|
||||
<div className="flex h-full w-full items-center justify-center bg-muted">
|
||||
<div className="text-center">
|
||||
<div className="mb-2 text-4xl">🏗️</div>
|
||||
<div className="text-sm font-medium text-gray-600">야드 레이아웃이 설정되지 않았습니다</div>
|
||||
<div className="mt-1 text-xs text-gray-400">대시보드 편집에서 레이아웃을 선택하세요</div>
|
||||
<div className="mt-2 text-xs text-red-500">
|
||||
<div className="text-sm font-medium text-foreground">야드 레이아웃이 설정되지 않았습니다</div>
|
||||
<div className="mt-1 text-xs text-muted-foreground">대시보드 편집에서 레이아웃을 선택하세요</div>
|
||||
<div className="mt-2 text-xs text-destructive">
|
||||
디버그: config={JSON.stringify(config)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ export function YardWidgetConfigModal({ element, isOpen, onClose, onSave }: Yard
|
|||
onChange={(e) => setCustomTitle(e.target.value)}
|
||||
placeholder="제목을 입력하세요 (비워두면 기본 제목 사용)"
|
||||
/>
|
||||
<p className="text-xs text-gray-500">기본 제목: 야드 관리 3D</p>
|
||||
<p className="text-xs text-muted-foreground">기본 제목: 야드 관리 3D</p>
|
||||
</div>
|
||||
|
||||
{/* 헤더 표시 여부 */}
|
||||
|
|
|
|||
|
|
@ -38,23 +38,23 @@ export function YardWidgetConfigSidebar({ element, isOpen, onClose, onApply }: Y
|
|||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"fixed top-14 left-0 z-[100] flex h-[calc(100vh-3.5rem)] w-80 flex-col bg-gray-50 transition-transform duration-300 ease-in-out",
|
||||
"fixed top-14 left-0 z-[100] flex h-[calc(100vh-3.5rem)] w-80 flex-col bg-muted transition-transform duration-300 ease-in-out",
|
||||
isOpen ? "translate-x-0" : "translate-x-[-100%]",
|
||||
)}
|
||||
>
|
||||
{/* 헤더 */}
|
||||
<div className="flex items-center justify-between bg-white px-3 py-2 shadow-sm">
|
||||
<div className="flex items-center justify-between bg-background px-3 py-2 shadow-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="bg-primary/10 flex h-6 w-6 items-center justify-center rounded">
|
||||
<span className="text-primary text-xs font-bold">🏗️</span>
|
||||
</div>
|
||||
<span className="text-xs font-semibold text-gray-900">야드 관리 위젯 설정</span>
|
||||
<span className="text-xs font-semibold text-foreground">야드 관리 위젯 설정</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="flex h-6 w-6 items-center justify-center rounded transition-colors hover:bg-gray-100"
|
||||
className="flex h-6 w-6 items-center justify-center rounded transition-colors hover:bg-muted"
|
||||
>
|
||||
<X className="h-3.5 w-3.5 text-gray-500" />
|
||||
<X className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
|
@ -62,8 +62,8 @@ export function YardWidgetConfigSidebar({ element, isOpen, onClose, onApply }: Y
|
|||
<div className="flex-1 overflow-y-auto p-3">
|
||||
<div className="space-y-3">
|
||||
{/* 위젯 제목 */}
|
||||
<div className="rounded-lg bg-white p-3 shadow-sm">
|
||||
<div className="mb-2 text-[10px] font-semibold tracking-wide text-gray-500 uppercase">위젯 제목</div>
|
||||
<div className="rounded-lg bg-background p-3 shadow-sm">
|
||||
<div className="mb-2 text-[10px] font-semibold tracking-wide text-muted-foreground uppercase">위젯 제목</div>
|
||||
<Input
|
||||
value={customTitle}
|
||||
onChange={(e) => setCustomTitle(e.target.value)}
|
||||
|
|
@ -71,12 +71,12 @@ export function YardWidgetConfigSidebar({ element, isOpen, onClose, onApply }: Y
|
|||
className="h-8 text-xs"
|
||||
style={{ fontSize: "12px" }}
|
||||
/>
|
||||
<p className="mt-1 text-[10px] text-gray-500">기본 제목: 야드 관리 3D</p>
|
||||
<p className="mt-1 text-[10px] text-muted-foreground">기본 제목: 야드 관리 3D</p>
|
||||
</div>
|
||||
|
||||
{/* 헤더 표시 */}
|
||||
<div className="rounded-lg bg-white p-3 shadow-sm">
|
||||
<div className="mb-2 text-[10px] font-semibold tracking-wide text-gray-500 uppercase">헤더 표시</div>
|
||||
<div className="rounded-lg bg-background p-3 shadow-sm">
|
||||
<div className="mb-2 text-[10px] font-semibold tracking-wide text-muted-foreground uppercase">헤더 표시</div>
|
||||
<RadioGroup
|
||||
value={showHeader ? "show" : "hide"}
|
||||
onValueChange={(value) => setShowHeader(value === "show")}
|
||||
|
|
@ -100,10 +100,10 @@ export function YardWidgetConfigSidebar({ element, isOpen, onClose, onApply }: Y
|
|||
</div>
|
||||
|
||||
{/* 푸터 */}
|
||||
<div className="flex gap-2 bg-white p-2 shadow-[0_-2px_8px_rgba(0,0,0,0.05)]">
|
||||
<div className="flex gap-2 bg-background p-2 shadow-[0_-2px_8px_rgba(0,0,0,0.05)]">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="flex-1 rounded bg-gray-100 py-2 text-xs font-medium text-gray-700 transition-colors hover:bg-gray-200"
|
||||
className="flex-1 rounded bg-muted py-2 text-xs font-medium text-foreground transition-colors hover:bg-muted"
|
||||
>
|
||||
취소
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -175,23 +175,23 @@ export default function CustomMetricConfigSidebar({
|
|||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"fixed top-14 left-0 z-[100] flex h-[calc(100vh-3.5rem)] w-80 flex-col bg-gray-50 transition-transform duration-300 ease-in-out",
|
||||
"fixed top-14 left-0 z-[100] flex h-[calc(100vh-3.5rem)] w-80 flex-col bg-muted transition-transform duration-300 ease-in-out",
|
||||
isOpen ? "translate-x-0" : "translate-x-[-100%]",
|
||||
)}
|
||||
>
|
||||
{/* 헤더 */}
|
||||
<div className="flex items-center justify-between bg-white px-3 py-2 shadow-sm">
|
||||
<div className="flex items-center justify-between bg-background px-3 py-2 shadow-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="bg-primary/10 flex h-6 w-6 items-center justify-center rounded">
|
||||
<span className="text-primary text-xs font-bold">📊</span>
|
||||
</div>
|
||||
<span className="text-xs font-semibold text-gray-900">커스텀 카드 설정</span>
|
||||
<span className="text-xs font-semibold text-foreground">커스텀 카드 설정</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="flex h-6 w-6 items-center justify-center rounded transition-colors hover:bg-gray-100"
|
||||
className="flex h-6 w-6 items-center justify-center rounded transition-colors hover:bg-muted"
|
||||
>
|
||||
<X className="h-3.5 w-3.5 text-gray-500" />
|
||||
<X className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
|
@ -199,12 +199,12 @@ export default function CustomMetricConfigSidebar({
|
|||
<div className="flex-1 overflow-y-auto p-3">
|
||||
<div className="space-y-3">
|
||||
{/* 헤더 설정 */}
|
||||
<div className="rounded-lg bg-white p-3 shadow-sm">
|
||||
<div className="mb-2 text-[10px] font-semibold tracking-wide text-gray-500 uppercase">헤더 설정</div>
|
||||
<div className="rounded-lg bg-background p-3 shadow-sm">
|
||||
<div className="mb-2 text-[10px] font-semibold tracking-wide text-muted-foreground uppercase">헤더 설정</div>
|
||||
<div className="space-y-2">
|
||||
{/* 제목 입력 */}
|
||||
<div>
|
||||
<label className="mb-0.5 block text-[9px] font-medium text-gray-500">제목</label>
|
||||
<label className="mb-0.5 block text-[9px] font-medium text-muted-foreground">제목</label>
|
||||
<Input
|
||||
value={customTitle}
|
||||
onChange={(e) => setCustomTitle(e.target.value)}
|
||||
|
|
@ -216,15 +216,15 @@ export default function CustomMetricConfigSidebar({
|
|||
|
||||
{/* 헤더 표시 여부 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-[9px] font-medium text-gray-500">헤더 표시</label>
|
||||
<label className="text-[9px] font-medium text-muted-foreground">헤더 표시</label>
|
||||
<button
|
||||
onClick={() => setShowHeader(!showHeader)}
|
||||
className={`relative inline-flex h-5 w-9 items-center rounded-full transition-colors ${
|
||||
showHeader ? "bg-primary" : "bg-gray-300"
|
||||
showHeader ? "bg-primary" : "bg-muted"
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
||||
className={`inline-block h-4 w-4 transform rounded-full bg-background transition-transform ${
|
||||
showHeader ? "translate-x-5" : "translate-x-0.5"
|
||||
}`}
|
||||
/>
|
||||
|
|
@ -234,15 +234,15 @@ export default function CustomMetricConfigSidebar({
|
|||
</div>
|
||||
|
||||
{/* 데이터 소스 타입 선택 */}
|
||||
<div className="rounded-lg bg-white p-3 shadow-sm">
|
||||
<div className="mb-2 text-[10px] font-semibold tracking-wide text-gray-500 uppercase">데이터 소스 타입</div>
|
||||
<div className="rounded-lg bg-background p-3 shadow-sm">
|
||||
<div className="mb-2 text-[10px] font-semibold tracking-wide text-muted-foreground uppercase">데이터 소스 타입</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<button
|
||||
onClick={() => handleDataSourceTypeChange("database")}
|
||||
className={`flex h-16 items-center justify-center rounded border transition-all ${
|
||||
dataSourceType === "database"
|
||||
? "border-primary bg-primary/5 text-primary"
|
||||
: "border-gray-200 bg-gray-50 text-gray-600 hover:border-gray-300"
|
||||
: "border-border bg-muted text-foreground hover:border-border"
|
||||
}`}
|
||||
>
|
||||
<span className="text-sm font-medium">데이터베이스</span>
|
||||
|
|
@ -252,7 +252,7 @@ export default function CustomMetricConfigSidebar({
|
|||
className={`flex h-16 items-center justify-center rounded border transition-all ${
|
||||
dataSourceType === "api"
|
||||
? "border-primary bg-primary/5 text-primary"
|
||||
: "border-gray-200 bg-gray-50 text-gray-600 hover:border-gray-300"
|
||||
: "border-border bg-muted text-foreground hover:border-border"
|
||||
}`}
|
||||
>
|
||||
<span className="text-sm font-medium">REST API</span>
|
||||
|
|
@ -275,9 +275,9 @@ export default function CustomMetricConfigSidebar({
|
|||
)}
|
||||
|
||||
{/* 일반 지표 설정 (항상 표시) */}
|
||||
<div className="rounded-lg bg-white p-3 shadow-sm">
|
||||
<div className="rounded-lg bg-background p-3 shadow-sm">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<div className="text-[10px] font-semibold tracking-wide text-gray-500 uppercase">일반 지표</div>
|
||||
<div className="text-[10px] font-semibold tracking-wide text-muted-foreground uppercase">일반 지표</div>
|
||||
{queryColumns.length > 0 && (
|
||||
<Button size="sm" variant="outline" className="h-7 gap-1 text-xs" onClick={addMetric}>
|
||||
<Plus className="h-3 w-3" />
|
||||
|
|
@ -287,11 +287,11 @@ export default function CustomMetricConfigSidebar({
|
|||
</div>
|
||||
|
||||
{queryColumns.length === 0 ? (
|
||||
<p className="text-xs text-gray-500">먼저 쿼리를 실행하세요</p>
|
||||
<p className="text-xs text-muted-foreground">먼저 쿼리를 실행하세요</p>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{metrics.length === 0 ? (
|
||||
<p className="text-xs text-gray-500">추가된 지표가 없습니다</p>
|
||||
<p className="text-xs text-muted-foreground">추가된 지표가 없습니다</p>
|
||||
) : (
|
||||
metrics.map((metric, index) => (
|
||||
<div
|
||||
|
|
@ -299,7 +299,7 @@ export default function CustomMetricConfigSidebar({
|
|||
onDragOver={(e) => handleDragOver(e, index)}
|
||||
onDrop={(e) => handleDrop(e, index)}
|
||||
className={cn(
|
||||
"rounded-md border bg-white p-2 transition-all",
|
||||
"rounded-md border bg-background p-2 transition-all",
|
||||
draggedIndex === index && "opacity-50",
|
||||
dragOverIndex === index && draggedIndex !== index && "border-primary border-2",
|
||||
)}
|
||||
|
|
@ -312,21 +312,21 @@ export default function CustomMetricConfigSidebar({
|
|||
onDragEnd={handleDragEnd}
|
||||
className="cursor-grab active:cursor-grabbing"
|
||||
>
|
||||
<GripVertical className="h-4 w-4 shrink-0 text-gray-400" />
|
||||
<GripVertical className="h-4 w-4 shrink-0 text-muted-foreground" />
|
||||
</div>
|
||||
<div className="grid min-w-0 flex-1 grid-cols-[1fr,auto,auto] items-center gap-2">
|
||||
<span className="truncate text-xs font-medium text-gray-900">
|
||||
<span className="truncate text-xs font-medium text-foreground">
|
||||
{metric.label || "새 지표"}
|
||||
</span>
|
||||
<span className="text-[10px] text-gray-500">{metric.aggregation.toUpperCase()}</span>
|
||||
<span className="text-[10px] text-muted-foreground">{metric.aggregation.toUpperCase()}</span>
|
||||
<button
|
||||
onClick={() => setExpandedMetric(expandedMetric === metric.id ? null : metric.id)}
|
||||
className="flex items-center justify-center rounded p-0.5 hover:bg-gray-100"
|
||||
className="flex items-center justify-center rounded p-0.5 hover:bg-muted"
|
||||
>
|
||||
{expandedMetric === metric.id ? (
|
||||
<ChevronUp className="h-3.5 w-3.5 text-gray-500" />
|
||||
<ChevronUp className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
) : (
|
||||
<ChevronDown className="h-3.5 w-3.5 text-gray-500" />
|
||||
<ChevronDown className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -334,12 +334,12 @@ export default function CustomMetricConfigSidebar({
|
|||
|
||||
{/* 설정 영역 */}
|
||||
{expandedMetric === metric.id && (
|
||||
<div className="mt-2 space-y-1.5 border-t border-gray-200 pt-2">
|
||||
<div className="mt-2 space-y-1.5 border-t border-border pt-2">
|
||||
{/* 2열 그리드 레이아웃 */}
|
||||
<div className="grid grid-cols-2 gap-1.5">
|
||||
{/* 컬럼 */}
|
||||
<div>
|
||||
<label className="mb-0.5 block text-[9px] font-medium text-gray-500">컬럼</label>
|
||||
<label className="mb-0.5 block text-[9px] font-medium text-muted-foreground">컬럼</label>
|
||||
<Select
|
||||
value={metric.field}
|
||||
onValueChange={(value) => updateMetric(metric.id, "field", value)}
|
||||
|
|
@ -359,7 +359,7 @@ export default function CustomMetricConfigSidebar({
|
|||
|
||||
{/* 집계 함수 */}
|
||||
<div>
|
||||
<label className="mb-0.5 block text-[9px] font-medium text-gray-500">집계</label>
|
||||
<label className="mb-0.5 block text-[9px] font-medium text-muted-foreground">집계</label>
|
||||
<Select
|
||||
value={metric.aggregation}
|
||||
onValueChange={(value: any) => updateMetric(metric.id, "aggregation", value)}
|
||||
|
|
@ -379,7 +379,7 @@ export default function CustomMetricConfigSidebar({
|
|||
|
||||
{/* 단위 */}
|
||||
<div>
|
||||
<label className="mb-0.5 block text-[9px] font-medium text-gray-500">단위</label>
|
||||
<label className="mb-0.5 block text-[9px] font-medium text-muted-foreground">단위</label>
|
||||
<Input
|
||||
value={metric.unit}
|
||||
onChange={(e) => updateMetric(metric.id, "unit", e.target.value)}
|
||||
|
|
@ -390,7 +390,7 @@ export default function CustomMetricConfigSidebar({
|
|||
|
||||
{/* 소수점 */}
|
||||
<div>
|
||||
<label className="mb-0.5 block text-[9px] font-medium text-gray-500">소수점</label>
|
||||
<label className="mb-0.5 block text-[9px] font-medium text-muted-foreground">소수점</label>
|
||||
<Select
|
||||
value={String(metric.decimals)}
|
||||
onValueChange={(value) => updateMetric(metric.id, "decimals", parseInt(value))}
|
||||
|
|
@ -411,7 +411,7 @@ export default function CustomMetricConfigSidebar({
|
|||
|
||||
{/* 표시 이름 (전체 너비) */}
|
||||
<div>
|
||||
<label className="mb-0.5 block text-[9px] font-medium text-gray-500">표시 이름</label>
|
||||
<label className="mb-0.5 block text-[9px] font-medium text-muted-foreground">표시 이름</label>
|
||||
<Input
|
||||
value={metric.label}
|
||||
onChange={(e) => updateMetric(metric.id, "label", e.target.value)}
|
||||
|
|
@ -421,7 +421,7 @@ export default function CustomMetricConfigSidebar({
|
|||
</div>
|
||||
|
||||
{/* 삭제 버튼 */}
|
||||
<div className="border-t border-gray-200 pt-1.5">
|
||||
<div className="border-t border-border pt-1.5">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
|
|
@ -442,13 +442,13 @@ export default function CustomMetricConfigSidebar({
|
|||
</div>
|
||||
|
||||
{/* 그룹별 카드 생성 모드 (항상 표시) */}
|
||||
<div className="rounded-lg bg-white p-3 shadow-sm">
|
||||
<div className="mb-2 text-[10px] font-semibold tracking-wide text-gray-500 uppercase">표시 모드</div>
|
||||
<div className="rounded-lg bg-background p-3 shadow-sm">
|
||||
<div className="mb-2 text-[10px] font-semibold tracking-wide text-muted-foreground uppercase">표시 모드</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<label className="text-xs font-medium text-gray-900">그룹별 카드 생성</label>
|
||||
<p className="mt-0.5 text-[9px] text-gray-500">
|
||||
<label className="text-xs font-medium text-foreground">그룹별 카드 생성</label>
|
||||
<p className="mt-0.5 text-[9px] text-muted-foreground">
|
||||
쿼리 결과의 각 행을 개별 카드로 표시
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -461,18 +461,18 @@ export default function CustomMetricConfigSidebar({
|
|||
}
|
||||
}}
|
||||
className={`relative inline-flex h-5 w-9 items-center rounded-full transition-colors ${
|
||||
groupByMode ? "bg-primary" : "bg-gray-300"
|
||||
groupByMode ? "bg-primary" : "bg-muted"
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
||||
className={`inline-block h-4 w-4 transform rounded-full bg-background transition-transform ${
|
||||
groupByMode ? "translate-x-5" : "translate-x-0.5"
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
{groupByMode && (
|
||||
<div className="rounded-md bg-blue-50 p-2 text-[9px] text-blue-700">
|
||||
<div className="rounded-md bg-primary/10 p-2 text-[9px] text-primary">
|
||||
<p className="font-medium">💡 사용 방법</p>
|
||||
<ul className="mt-1 space-y-0.5 pl-3 text-[8px]">
|
||||
<li>• 첫 번째 컬럼: 카드 제목</li>
|
||||
|
|
@ -487,8 +487,8 @@ export default function CustomMetricConfigSidebar({
|
|||
|
||||
{/* 그룹별 카드 전용 쿼리 (활성화 시에만 표시) */}
|
||||
{groupByMode && groupByDataSource && (
|
||||
<div className="rounded-lg bg-white p-3 shadow-sm">
|
||||
<div className="mb-2 text-[10px] font-semibold tracking-wide text-gray-500 uppercase">
|
||||
<div className="rounded-lg bg-background p-3 shadow-sm">
|
||||
<div className="mb-2 text-[10px] font-semibold tracking-wide text-muted-foreground uppercase">
|
||||
그룹별 카드 쿼리
|
||||
</div>
|
||||
<DatabaseConfig dataSource={groupByDataSource} onChange={handleGroupByDataSourceUpdate} />
|
||||
|
|
@ -503,7 +503,7 @@ export default function CustomMetricConfigSidebar({
|
|||
</div>
|
||||
|
||||
{/* 푸터 */}
|
||||
<div className="flex gap-2 border-t bg-white p-3 shadow-sm">
|
||||
<div className="flex gap-2 border-t bg-background p-3 shadow-sm">
|
||||
<Button variant="outline" className="h-8 flex-1 text-xs" onClick={onClose}>
|
||||
취소
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -7,38 +7,38 @@ export function getStatusColor(status: DriverInfo["status"]) {
|
|||
switch (status) {
|
||||
case "driving":
|
||||
return {
|
||||
bg: "bg-green-100",
|
||||
text: "text-green-800",
|
||||
border: "border-green-300",
|
||||
badge: "bg-green-500",
|
||||
bg: "bg-success/10",
|
||||
text: "text-success",
|
||||
border: "border-success",
|
||||
badge: "bg-success",
|
||||
};
|
||||
case "standby":
|
||||
return {
|
||||
bg: "bg-gray-100",
|
||||
text: "text-gray-800",
|
||||
border: "border-gray-300",
|
||||
badge: "bg-gray-500",
|
||||
bg: "bg-muted",
|
||||
text: "text-foreground",
|
||||
border: "border-border",
|
||||
badge: "bg-muted0",
|
||||
};
|
||||
case "resting":
|
||||
return {
|
||||
bg: "bg-orange-100",
|
||||
text: "text-orange-800",
|
||||
border: "border-orange-300",
|
||||
badge: "bg-orange-500",
|
||||
bg: "bg-warning/10",
|
||||
text: "text-warning",
|
||||
border: "border-warning",
|
||||
badge: "bg-warning",
|
||||
};
|
||||
case "maintenance":
|
||||
return {
|
||||
bg: "bg-red-100",
|
||||
text: "text-red-800",
|
||||
border: "border-red-300",
|
||||
badge: "bg-red-500",
|
||||
bg: "bg-destructive/10",
|
||||
text: "text-destructive",
|
||||
border: "border-destructive",
|
||||
badge: "bg-destructive",
|
||||
};
|
||||
default:
|
||||
return {
|
||||
bg: "bg-gray-100",
|
||||
text: "text-gray-800",
|
||||
border: "border-gray-300",
|
||||
badge: "bg-gray-500",
|
||||
bg: "bg-muted",
|
||||
text: "text-foreground",
|
||||
border: "border-border",
|
||||
badge: "bg-muted0",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -123,7 +123,7 @@ export function ColumnSelector({ queryResult, config, onConfigChange }: ColumnSe
|
|||
className={`group relative rounded-md border transition-all ${
|
||||
isSelected
|
||||
? "border-primary/40 bg-primary/5 shadow-sm"
|
||||
: "border-gray-200 bg-white hover:border-gray-300 hover:shadow-sm"
|
||||
: "border-border bg-background hover:border-border hover:shadow-sm"
|
||||
} ${isDraggable ? "cursor-grab active:cursor-grabbing" : ""} ${
|
||||
draggedIndex === columnIndex ? "scale-95 opacity-50" : ""
|
||||
}`}
|
||||
|
|
@ -137,20 +137,20 @@ export function ColumnSelector({ queryResult, config, onConfigChange }: ColumnSe
|
|||
/>
|
||||
<GripVertical
|
||||
className={`h-3.5 w-3.5 shrink-0 transition-colors ${
|
||||
isDraggable ? "group-hover:text-primary text-gray-400" : "text-gray-300"
|
||||
isDraggable ? "group-hover:text-primary text-muted-foreground" : "text-muted-foreground"
|
||||
}`}
|
||||
/>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-baseline gap-1.5">
|
||||
<span className="truncate text-[11px] font-medium text-gray-900">{field}</span>
|
||||
{previewText && <span className="shrink-0 text-[9px] text-gray-400">예: {previewText}</span>}
|
||||
<span className="truncate text-[11px] font-medium text-foreground">{field}</span>
|
||||
{previewText && <span className="shrink-0 text-[9px] text-muted-foreground">예: {previewText}</span>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 설정 영역 */}
|
||||
{isSelected && selectedCol && (
|
||||
<div className="border-t border-gray-100 bg-gray-50/50 px-2.5 py-1.5">
|
||||
<div className="border-t border-border bg-muted/50 px-2.5 py-1.5">
|
||||
<div className="grid grid-cols-2 gap-1.5">
|
||||
{/* 표시 이름 */}
|
||||
<div className="min-w-0">
|
||||
|
|
@ -158,7 +158,7 @@ export function ColumnSelector({ queryResult, config, onConfigChange }: ColumnSe
|
|||
value={selectedCol.label}
|
||||
onChange={(e) => handleLabelChange(field, e.target.value)}
|
||||
placeholder="표시 이름"
|
||||
className="focus:border-primary focus:ring-primary/20 h-6 w-full border-gray-200 bg-white px-1.5 text-[10px] placeholder:text-gray-400 focus:ring-1"
|
||||
className="focus:border-primary focus:ring-primary/20 h-6 w-full border-border bg-background px-1.5 text-[10px] placeholder:text-muted-foreground focus:ring-1"
|
||||
style={{ fontSize: "10px" }}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -170,7 +170,7 @@ export function ColumnSelector({ queryResult, config, onConfigChange }: ColumnSe
|
|||
onValueChange={(value: "left" | "center" | "right") => handleAlignChange(field, value)}
|
||||
>
|
||||
<SelectTrigger
|
||||
className="focus:border-primary focus:ring-primary/20 h-6 w-full border-gray-200 bg-white px-1.5 text-[10px] focus:ring-1 [&>span]:leading-none [&>svg]:h-3 [&>svg]:w-3"
|
||||
className="focus:border-primary focus:ring-primary/20 h-6 w-full border-border bg-background px-1.5 text-[10px] focus:ring-1 [&>span]:leading-none [&>svg]:h-3 [&>svg]:w-3"
|
||||
style={{ fontSize: "10px", height: "24px", minHeight: "24px" }}
|
||||
>
|
||||
<SelectValue />
|
||||
|
|
@ -210,7 +210,7 @@ export function ColumnSelector({ queryResult, config, onConfigChange }: ColumnSe
|
|||
return (
|
||||
<div
|
||||
key={field}
|
||||
className="group rounded-md border border-gray-200 bg-white transition-all hover:border-gray-300 hover:shadow-sm"
|
||||
className="group rounded-md border border-border bg-background transition-all hover:border-border hover:shadow-sm"
|
||||
>
|
||||
<div className="flex items-center gap-2 px-2.5 py-2">
|
||||
<Checkbox
|
||||
|
|
@ -218,11 +218,11 @@ export function ColumnSelector({ queryResult, config, onConfigChange }: ColumnSe
|
|||
onCheckedChange={() => handleToggle(field)}
|
||||
className="h-3.5 w-3.5 shrink-0"
|
||||
/>
|
||||
<GripVertical className="h-3.5 w-3.5 shrink-0 text-gray-300" />
|
||||
<GripVertical className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-baseline gap-1.5">
|
||||
<span className="truncate text-[11px] font-medium text-gray-600">{field}</span>
|
||||
{previewText && <span className="shrink-0 text-[9px] text-gray-400">예: {previewText}</span>}
|
||||
<span className="truncate text-[11px] font-medium text-foreground">{field}</span>
|
||||
{previewText && <span className="shrink-0 text-[9px] text-muted-foreground">예: {previewText}</span>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -232,9 +232,9 @@ export function ColumnSelector({ queryResult, config, onConfigChange }: ColumnSe
|
|||
</div>
|
||||
|
||||
{selectedColumns.length === 0 && (
|
||||
<div className="mt-3 flex items-center gap-2 rounded-md border border-amber-200 bg-amber-50/50 px-3 py-2">
|
||||
<span className="text-amber-600">⚠️</span>
|
||||
<span className="text-[10px] text-amber-700">최소 1개 이상의 컬럼을 선택해주세요</span>
|
||||
<div className="mt-3 flex items-center gap-2 rounded-md border border-warning bg-warning/10 px-3 py-2">
|
||||
<span className="text-warning">⚠️</span>
|
||||
<span className="text-[10px] text-warning">최소 1개 이상의 컬럼을 선택해주세요</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export function ListTableOptions({ config, onConfigChange }: ListTableOptionsPro
|
|||
<div className="space-y-3">
|
||||
{/* 뷰 모드 */}
|
||||
<div>
|
||||
<Label className="mb-1 block text-[9px] font-medium text-gray-600">뷰 모드</Label>
|
||||
<Label className="mb-1 block text-[9px] font-medium text-foreground">뷰 모드</Label>
|
||||
<RadioGroup
|
||||
value={config.viewMode}
|
||||
onValueChange={(value: "table" | "card") => onConfigChange({ viewMode: value })}
|
||||
|
|
@ -46,7 +46,7 @@ export function ListTableOptions({ config, onConfigChange }: ListTableOptionsPro
|
|||
{/* 카드 뷰 컬럼 수 */}
|
||||
{config.viewMode === "card" && (
|
||||
<div>
|
||||
<Label className="mb-1 block text-[9px] font-medium text-gray-600">카드 컬럼 수</Label>
|
||||
<Label className="mb-1 block text-[9px] font-medium text-foreground">카드 컬럼 수</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min="1"
|
||||
|
|
@ -55,13 +55,13 @@ export function ListTableOptions({ config, onConfigChange }: ListTableOptionsPro
|
|||
onChange={(e) => onConfigChange({ cardColumns: parseInt(e.target.value) || 3 })}
|
||||
className="h-6 w-full px-1.5 text-[11px]"
|
||||
/>
|
||||
<p className="mt-0.5 text-[9px] text-gray-500">한 줄에 표시할 카드 개수 (1-6)</p>
|
||||
<p className="mt-0.5 text-[9px] text-muted-foreground">한 줄에 표시할 카드 개수 (1-6)</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 페이지 크기 */}
|
||||
<div>
|
||||
<Label className="mb-1 block text-[9px] font-medium text-gray-600">페이지당 행 수</Label>
|
||||
<Label className="mb-1 block text-[9px] font-medium text-foreground">페이지당 행 수</Label>
|
||||
<Select
|
||||
value={String(config.pageSize)}
|
||||
onValueChange={(value) => onConfigChange({ pageSize: parseInt(value) })}
|
||||
|
|
@ -91,7 +91,7 @@ export function ListTableOptions({ config, onConfigChange }: ListTableOptionsPro
|
|||
|
||||
{/* 기능 활성화 */}
|
||||
<div>
|
||||
<Label className="mb-1 block text-[9px] font-medium text-gray-600">페이지네이션</Label>
|
||||
<Label className="mb-1 block text-[9px] font-medium text-foreground">페이지네이션</Label>
|
||||
<RadioGroup
|
||||
value={config.enablePagination ? "enabled" : "disabled"}
|
||||
onValueChange={(value) => onConfigChange({ enablePagination: value === "enabled" })}
|
||||
|
|
@ -115,7 +115,7 @@ export function ListTableOptions({ config, onConfigChange }: ListTableOptionsPro
|
|||
{/* 헤더 표시 */}
|
||||
{config.viewMode === "table" && (
|
||||
<div>
|
||||
<Label className="mb-1 block text-[9px] font-medium text-gray-600">헤더 표시</Label>
|
||||
<Label className="mb-1 block text-[9px] font-medium text-foreground">헤더 표시</Label>
|
||||
<RadioGroup
|
||||
value={config.showHeader ? "show" : "hide"}
|
||||
onValueChange={(value) => onConfigChange({ showHeader: value === "show" })}
|
||||
|
|
@ -140,7 +140,7 @@ export function ListTableOptions({ config, onConfigChange }: ListTableOptionsPro
|
|||
{/* 줄무늬 행 */}
|
||||
{config.viewMode === "table" && (
|
||||
<div>
|
||||
<Label className="mb-1 block text-[9px] font-medium text-gray-600">줄무늬 행</Label>
|
||||
<Label className="mb-1 block text-[9px] font-medium text-foreground">줄무늬 행</Label>
|
||||
<RadioGroup
|
||||
value={config.stripedRows ? "enabled" : "disabled"}
|
||||
onValueChange={(value) => onConfigChange({ stripedRows: value === "enabled" })}
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ export function ManualColumnEditor({ config, onConfigChange }: ManualColumnEdito
|
|||
<div>
|
||||
{/* 헤더 */}
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<p className="text-[10px] text-gray-500">직접 컬럼을 추가하고 데이터 필드를 매핑하세요</p>
|
||||
<p className="text-[10px] text-muted-foreground">직접 컬럼을 추가하고 데이터 필드를 매핑하세요</p>
|
||||
<button
|
||||
onClick={handleAddColumn}
|
||||
className="bg-primary hover:bg-primary/90 flex items-center gap-1 rounded-md px-2 py-1 text-[10px] font-medium text-white transition-colors"
|
||||
|
|
@ -102,24 +102,24 @@ export function ManualColumnEditor({ config, onConfigChange }: ManualColumnEdito
|
|||
handleDragEnd();
|
||||
e.currentTarget.style.cursor = "grab";
|
||||
}}
|
||||
className={`group relative rounded-md border border-gray-200 bg-white shadow-sm transition-all hover:border-gray-300 hover:shadow-sm ${
|
||||
className={`group relative rounded-md border border-border bg-background shadow-sm transition-all hover:border-border hover:shadow-sm ${
|
||||
draggedIndex === index ? "scale-95 opacity-50" : ""
|
||||
} cursor-grab active:cursor-grabbing`}
|
||||
>
|
||||
{/* 헤더 */}
|
||||
<div className="flex items-center gap-2 px-2.5 py-2">
|
||||
<GripVertical className="group-hover:text-primary h-3.5 w-3.5 shrink-0 text-gray-400 transition-colors" />
|
||||
<span className="text-[11px] font-medium text-gray-900">컬럼 {index + 1}</span>
|
||||
<GripVertical className="group-hover:text-primary h-3.5 w-3.5 shrink-0 text-muted-foreground transition-colors" />
|
||||
<span className="text-[11px] font-medium text-foreground">컬럼 {index + 1}</span>
|
||||
<button
|
||||
onClick={() => handleRemove(col.id)}
|
||||
className="ml-auto flex h-5 w-5 items-center justify-center rounded text-gray-400 transition-colors hover:bg-red-50 hover:text-red-600"
|
||||
className="ml-auto flex h-5 w-5 items-center justify-center rounded text-muted-foreground transition-colors hover:bg-destructive/10 hover:text-destructive"
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 설정 영역 */}
|
||||
<div className="border-t border-gray-100 bg-gray-50/50 px-2.5 py-1.5">
|
||||
<div className="border-t border-border bg-muted/50 px-2.5 py-1.5">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
{/* 표시 이름 */}
|
||||
<div className="min-w-0">
|
||||
|
|
@ -127,7 +127,7 @@ export function ManualColumnEditor({ config, onConfigChange }: ManualColumnEdito
|
|||
value={col.label}
|
||||
onChange={(e) => handleUpdate(col.id, { label: e.target.value })}
|
||||
placeholder="표시 이름"
|
||||
className="focus:border-primary focus:ring-primary/20 h-6 w-full border-gray-200 bg-white px-1.5 text-[10px] placeholder:text-gray-400 focus:ring-1"
|
||||
className="focus:border-primary focus:ring-primary/20 h-6 w-full border-border bg-background px-1.5 text-[10px] placeholder:text-muted-foreground focus:ring-1"
|
||||
style={{ fontSize: "10px" }}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -138,7 +138,7 @@ export function ManualColumnEditor({ config, onConfigChange }: ManualColumnEdito
|
|||
value={col.field}
|
||||
onChange={(e) => handleUpdate(col.id, { field: e.target.value })}
|
||||
placeholder="데이터 필드"
|
||||
className="focus:border-primary focus:ring-primary/20 h-6 w-full border-gray-200 bg-white px-1.5 text-[10px] placeholder:text-gray-400 focus:ring-1"
|
||||
className="focus:border-primary focus:ring-primary/20 h-6 w-full border-border bg-background px-1.5 text-[10px] placeholder:text-muted-foreground focus:ring-1"
|
||||
style={{ fontSize: "10px" }}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -150,7 +150,7 @@ export function ManualColumnEditor({ config, onConfigChange }: ManualColumnEdito
|
|||
onValueChange={(value: "left" | "center" | "right") => handleUpdate(col.id, { align: value })}
|
||||
>
|
||||
<SelectTrigger
|
||||
className="focus:border-primary focus:ring-primary/20 h-6 w-full border-gray-200 bg-white px-1.5 text-[10px] focus:ring-1 [&>span]:leading-none [&>svg]:h-3 [&>svg]:w-3"
|
||||
className="focus:border-primary focus:ring-primary/20 h-6 w-full border-border bg-background px-1.5 text-[10px] focus:ring-1 [&>span]:leading-none [&>svg]:h-3 [&>svg]:w-3"
|
||||
style={{ fontSize: "10px", height: "24px", minHeight: "24px" }}
|
||||
>
|
||||
<SelectValue />
|
||||
|
|
@ -175,9 +175,9 @@ export function ManualColumnEditor({ config, onConfigChange }: ManualColumnEdito
|
|||
</div>
|
||||
|
||||
{columns.length === 0 && (
|
||||
<div className="mt-3 flex items-center gap-2 rounded-md border border-amber-200 bg-amber-50/50 px-3 py-2">
|
||||
<span className="text-amber-600">⚠️</span>
|
||||
<span className="text-[10px] text-amber-700">컬럼을 추가하여 시작하세요</span>
|
||||
<div className="mt-3 flex items-center gap-2 rounded-md border border-warning bg-warning/10 px-3 py-2">
|
||||
<span className="text-warning">⚠️</span>
|
||||
<span className="text-[10px] text-warning">컬럼을 추가하여 시작하세요</span>
|
||||
<button
|
||||
onClick={handleAddColumn}
|
||||
className="bg-primary hover:bg-primary/90 ml-auto flex items-center gap-1 rounded-md px-2 py-1 text-[10px] font-medium text-white transition-colors"
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ export function UnifiedColumnEditor({ queryResult, config, onConfigChange }: Uni
|
|||
<div>
|
||||
{/* 헤더 */}
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<p className="text-[10px] text-gray-500">컬럼을 선택하고 편집하세요</p>
|
||||
<p className="text-[10px] text-muted-foreground">컬럼을 선택하고 편집하세요</p>
|
||||
<button
|
||||
onClick={handleAddColumn}
|
||||
className="bg-primary hover:bg-primary/90 flex items-center gap-1 rounded-md px-2 py-1 text-[10px] font-medium text-white transition-colors"
|
||||
|
|
@ -124,7 +124,7 @@ export function UnifiedColumnEditor({ queryResult, config, onConfigChange }: Uni
|
|||
className={`group relative rounded-md border transition-all ${
|
||||
col.visible
|
||||
? "border-primary/40 bg-primary/5 shadow-sm"
|
||||
: "border-gray-200 bg-white hover:border-gray-300 hover:shadow-sm"
|
||||
: "border-border bg-background hover:border-border hover:shadow-sm"
|
||||
} ${draggedIndex === index ? "scale-95 opacity-50" : ""}`}
|
||||
>
|
||||
{/* 헤더 */}
|
||||
|
|
@ -146,19 +146,19 @@ export function UnifiedColumnEditor({ queryResult, config, onConfigChange }: Uni
|
|||
}}
|
||||
className="cursor-grab active:cursor-grabbing"
|
||||
>
|
||||
<GripVertical className="group-hover:text-primary h-3.5 w-3.5 shrink-0 text-gray-400 transition-colors" />
|
||||
<GripVertical className="group-hover:text-primary h-3.5 w-3.5 shrink-0 text-muted-foreground transition-colors" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="truncate text-[11px] font-medium text-gray-900">
|
||||
<span className="truncate text-[11px] font-medium text-foreground">
|
||||
{col.field || "(필드명 없음)"}
|
||||
</span>
|
||||
{previewText && <span className="shrink-0 text-[9px] text-gray-400">예: {previewText}</span>}
|
||||
{previewText && <span className="shrink-0 text-[9px] text-muted-foreground">예: {previewText}</span>}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleRemove(col.id)}
|
||||
className="ml-auto flex h-5 w-5 items-center justify-center rounded text-gray-400 transition-colors hover:bg-red-50 hover:text-red-600"
|
||||
className="ml-auto flex h-5 w-5 items-center justify-center rounded text-muted-foreground transition-colors hover:bg-destructive/10 hover:text-destructive"
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</button>
|
||||
|
|
@ -166,7 +166,7 @@ export function UnifiedColumnEditor({ queryResult, config, onConfigChange }: Uni
|
|||
|
||||
{/* 설정 영역 */}
|
||||
{col.visible && (
|
||||
<div className="border-t border-gray-100 bg-gray-50/50 px-2.5 py-1.5">
|
||||
<div className="border-t border-border bg-muted/50 px-2.5 py-1.5">
|
||||
<div className="grid grid-cols-2 gap-1.5">
|
||||
{/* 표시 이름 */}
|
||||
<div className="min-w-0">
|
||||
|
|
@ -174,7 +174,7 @@ export function UnifiedColumnEditor({ queryResult, config, onConfigChange }: Uni
|
|||
value={col.label}
|
||||
onChange={(e) => handleUpdate(col.id, { label: e.target.value })}
|
||||
placeholder="표시 이름"
|
||||
className="focus:border-primary focus:ring-primary/20 h-6 w-full border-gray-200 bg-white px-1.5 text-[10px] placeholder:text-gray-400 focus:ring-1"
|
||||
className="focus:border-primary focus:ring-primary/20 h-6 w-full border-border bg-background px-1.5 text-[10px] placeholder:text-muted-foreground focus:ring-1"
|
||||
style={{ fontSize: "10px" }}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -186,7 +186,7 @@ export function UnifiedColumnEditor({ queryResult, config, onConfigChange }: Uni
|
|||
onValueChange={(value: "left" | "center" | "right") => handleUpdate(col.id, { align: value })}
|
||||
>
|
||||
<SelectTrigger
|
||||
className="focus:border-primary focus:ring-primary/20 h-6 w-full border-gray-200 bg-white px-1.5 text-[10px] focus:ring-1 [&>span]:leading-none [&>svg]:h-3 [&>svg]:w-3"
|
||||
className="focus:border-primary focus:ring-primary/20 h-6 w-full border-border bg-background px-1.5 text-[10px] focus:ring-1 [&>span]:leading-none [&>svg]:h-3 [&>svg]:w-3"
|
||||
style={{ fontSize: "10px", height: "24px", minHeight: "24px" }}
|
||||
>
|
||||
<SelectValue />
|
||||
|
|
@ -213,9 +213,9 @@ export function UnifiedColumnEditor({ queryResult, config, onConfigChange }: Uni
|
|||
</div>
|
||||
|
||||
{columns.length === 0 && (
|
||||
<div className="mt-3 flex items-center gap-2 rounded-md border border-amber-200 bg-amber-50/50 px-3 py-2">
|
||||
<span className="text-amber-600">⚠️</span>
|
||||
<span className="text-[10px] text-amber-700">쿼리를 실행하거나 컬럼을 추가하세요</span>
|
||||
<div className="mt-3 flex items-center gap-2 rounded-md border border-warning bg-warning/10 px-3 py-2">
|
||||
<span className="text-warning">⚠️</span>
|
||||
<span className="text-[10px] text-warning">쿼리를 실행하거나 컬럼을 추가하세요</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -94,13 +94,13 @@ export default function MaterialAddModal({ isOpen, material, onClose, onAdd }: M
|
|||
|
||||
<div className="space-y-4">
|
||||
{/* 자재 정보 */}
|
||||
<div className="rounded-lg bg-gray-50 p-4">
|
||||
<div className="mb-2 text-sm font-medium text-gray-600">선택한 자재</div>
|
||||
<div className="rounded-lg bg-muted p-4">
|
||||
<div className="mb-2 text-sm font-medium text-foreground">선택한 자재</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="h-10 w-10 rounded border" style={{ backgroundColor: material.default_color }} />
|
||||
<div>
|
||||
<div className="font-medium">{material.material_name}</div>
|
||||
<div className="text-sm text-gray-600">{material.material_code}</div>
|
||||
<div className="text-sm text-foreground">{material.material_code}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -117,7 +117,7 @@ export default function MaterialAddModal({ isOpen, material, onClose, onAdd }: M
|
|||
min="1"
|
||||
className="flex-1"
|
||||
/>
|
||||
<span className="text-sm text-gray-600">{material.unit}</span>
|
||||
<span className="text-sm text-foreground">{material.unit}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -126,7 +126,7 @@ export default function MaterialAddModal({ isOpen, material, onClose, onAdd }: M
|
|||
<Label>3D 위치</Label>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<div>
|
||||
<Label htmlFor="posX" className="text-xs text-gray-600">
|
||||
<Label htmlFor="posX" className="text-xs text-foreground">
|
||||
X (좌우)
|
||||
</Label>
|
||||
<Input
|
||||
|
|
@ -138,7 +138,7 @@ export default function MaterialAddModal({ isOpen, material, onClose, onAdd }: M
|
|||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="posY" className="text-xs text-gray-600">
|
||||
<Label htmlFor="posY" className="text-xs text-foreground">
|
||||
Y (높이)
|
||||
</Label>
|
||||
<Input
|
||||
|
|
@ -150,7 +150,7 @@ export default function MaterialAddModal({ isOpen, material, onClose, onAdd }: M
|
|||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="posZ" className="text-xs text-gray-600">
|
||||
<Label htmlFor="posZ" className="text-xs text-foreground">
|
||||
Z (앞뒤)
|
||||
</Label>
|
||||
<Input
|
||||
|
|
@ -169,7 +169,7 @@ export default function MaterialAddModal({ isOpen, material, onClose, onAdd }: M
|
|||
<Label>3D 크기</Label>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<div>
|
||||
<Label htmlFor="sizeX" className="text-xs text-gray-600">
|
||||
<Label htmlFor="sizeX" className="text-xs text-foreground">
|
||||
너비
|
||||
</Label>
|
||||
<Input
|
||||
|
|
@ -182,7 +182,7 @@ export default function MaterialAddModal({ isOpen, material, onClose, onAdd }: M
|
|||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="sizeY" className="text-xs text-gray-600">
|
||||
<Label htmlFor="sizeY" className="text-xs text-foreground">
|
||||
높이
|
||||
</Label>
|
||||
<Input
|
||||
|
|
@ -195,7 +195,7 @@ export default function MaterialAddModal({ isOpen, material, onClose, onAdd }: M
|
|||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="sizeZ" className="text-xs text-gray-600">
|
||||
<Label htmlFor="sizeZ" className="text-xs text-foreground">
|
||||
깊이
|
||||
</Label>
|
||||
<Input
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ export default function MaterialEditPanel({ placement, onClose, onUpdate, onRemo
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="w-80 border-l bg-white p-4">
|
||||
<div className="w-80 border-l bg-background p-4">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<h3 className="text-lg font-semibold">자재 정보</h3>
|
||||
<Button variant="ghost" size="sm" onClick={onClose}>
|
||||
|
|
@ -82,18 +82,18 @@ export default function MaterialEditPanel({ placement, onClose, onUpdate, onRemo
|
|||
|
||||
<div className="space-y-4">
|
||||
{/* 읽기 전용 정보 */}
|
||||
<div className="space-y-3 rounded-lg bg-gray-50 p-3">
|
||||
<div className="text-xs font-medium text-gray-500">자재 정보 (읽기 전용)</div>
|
||||
<div className="space-y-3 rounded-lg bg-muted p-3">
|
||||
<div className="text-xs font-medium text-muted-foreground">자재 정보 (읽기 전용)</div>
|
||||
<div>
|
||||
<div className="text-xs text-gray-600">자재 코드</div>
|
||||
<div className="text-xs text-foreground">자재 코드</div>
|
||||
<div className="mt-1 text-sm font-medium">{placement.material_code}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-gray-600">자재 이름</div>
|
||||
<div className="text-xs text-foreground">자재 이름</div>
|
||||
<div className="mt-1 text-sm font-medium">{placement.material_name}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-gray-600">수량</div>
|
||||
<div className="text-xs text-foreground">수량</div>
|
||||
<div className="mt-1 text-sm font-medium">
|
||||
{placement.quantity} {placement.unit}
|
||||
</div>
|
||||
|
|
@ -102,14 +102,14 @@ export default function MaterialEditPanel({ placement, onClose, onUpdate, onRemo
|
|||
|
||||
{/* 배치 정보 (편집 가능) */}
|
||||
<div className="space-y-3">
|
||||
<div className="text-xs font-medium text-gray-500">배치 정보 (편집 가능)</div>
|
||||
<div className="text-xs font-medium text-muted-foreground">배치 정보 (편집 가능)</div>
|
||||
|
||||
{/* 3D 크기 */}
|
||||
<div>
|
||||
<Label className="text-xs">크기</Label>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<div>
|
||||
<Label htmlFor="edit-sizeX" className="text-xs text-gray-600">
|
||||
<Label htmlFor="edit-sizeX" className="text-xs text-foreground">
|
||||
너비
|
||||
</Label>
|
||||
<Input
|
||||
|
|
@ -123,7 +123,7 @@ export default function MaterialEditPanel({ placement, onClose, onUpdate, onRemo
|
|||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="edit-sizeY" className="text-xs text-gray-600">
|
||||
<Label htmlFor="edit-sizeY" className="text-xs text-foreground">
|
||||
높이
|
||||
</Label>
|
||||
<Input
|
||||
|
|
@ -137,7 +137,7 @@ export default function MaterialEditPanel({ placement, onClose, onUpdate, onRemo
|
|||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="edit-sizeZ" className="text-xs text-gray-600">
|
||||
<Label htmlFor="edit-sizeZ" className="text-xs text-foreground">
|
||||
깊이
|
||||
</Label>
|
||||
<Input
|
||||
|
|
@ -217,7 +217,7 @@ export default function MaterialEditPanel({ placement, onClose, onUpdate, onRemo
|
|||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>취소</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleRemove} className="bg-red-600 hover:bg-red-700">
|
||||
<AlertDialogAction onClick={handleRemove} className="bg-destructive hover:bg-destructive/90">
|
||||
해제
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ export default function MaterialLibrary({ isOpen, onClose, onSelect }: MaterialL
|
|||
{/* 검색 및 필터 */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-gray-400" />
|
||||
<Search className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="자재 코드 또는 이름 검색..."
|
||||
value={searchText}
|
||||
|
|
@ -105,7 +105,7 @@ export default function MaterialLibrary({ isOpen, onClose, onSelect }: MaterialL
|
|||
<select
|
||||
value={selectedCategory}
|
||||
onChange={(e) => setSelectedCategory(e.target.value)}
|
||||
className="rounded-md border border-gray-300 px-3 py-2 text-sm"
|
||||
className="rounded-md border border-border px-3 py-2 text-sm"
|
||||
>
|
||||
<option value="">전체 카테고리</option>
|
||||
{categories.map((category) => (
|
||||
|
|
@ -119,10 +119,10 @@ export default function MaterialLibrary({ isOpen, onClose, onSelect }: MaterialL
|
|||
{/* 자재 목록 */}
|
||||
{isLoading ? (
|
||||
<div className="flex h-64 items-center justify-center">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-gray-400" />
|
||||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
) : materials.length === 0 ? (
|
||||
<div className="flex h-64 items-center justify-center text-gray-500">
|
||||
<div className="flex h-64 items-center justify-center text-muted-foreground">
|
||||
{searchText || selectedCategory ? "검색 결과가 없습니다" : "등록된 자재가 없습니다"}
|
||||
</div>
|
||||
) : (
|
||||
|
|
@ -142,7 +142,7 @@ export default function MaterialLibrary({ isOpen, onClose, onSelect }: MaterialL
|
|||
<TableRow
|
||||
key={material.id}
|
||||
className={`cursor-pointer ${
|
||||
selectedMaterial?.id === material.id ? "bg-blue-50" : "hover:bg-gray-50"
|
||||
selectedMaterial?.id === material.id ? "bg-primary/10" : "hover:bg-muted"
|
||||
}`}
|
||||
onClick={() => setSelectedMaterial(material)}
|
||||
>
|
||||
|
|
@ -162,17 +162,17 @@ export default function MaterialLibrary({ isOpen, onClose, onSelect }: MaterialL
|
|||
|
||||
{/* 선택된 자재 정보 */}
|
||||
{selectedMaterial && (
|
||||
<div className="rounded-lg bg-blue-50 p-4">
|
||||
<div className="mb-2 text-sm font-medium text-blue-900">선택된 자재</div>
|
||||
<div className="rounded-lg bg-primary/10 p-4">
|
||||
<div className="mb-2 text-sm font-medium text-primary">선택된 자재</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="h-10 w-10 rounded border" style={{ backgroundColor: selectedMaterial.default_color }} />
|
||||
<div className="flex-1">
|
||||
<div className="font-medium">{selectedMaterial.material_name}</div>
|
||||
<div className="text-sm text-gray-600">{selectedMaterial.material_code}</div>
|
||||
<div className="text-sm text-foreground">{selectedMaterial.material_code}</div>
|
||||
</div>
|
||||
</div>
|
||||
{selectedMaterial.description && (
|
||||
<div className="mt-2 text-sm text-gray-600">{selectedMaterial.description}</div>
|
||||
<div className="mt-2 text-sm text-foreground">{selectedMaterial.description}</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -90,10 +90,10 @@ export default function Yard3DViewer({ layoutId }: Yard3DViewerProps) {
|
|||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center bg-gray-50">
|
||||
<div className="flex h-full w-full items-center justify-center bg-muted">
|
||||
<div className="text-center">
|
||||
<Loader2 className="mx-auto h-8 w-8 animate-spin text-blue-600" />
|
||||
<div className="mt-2 text-sm text-gray-600">3D 장면 로딩 중...</div>
|
||||
<Loader2 className="mx-auto h-8 w-8 animate-spin text-primary" />
|
||||
<div className="mt-2 text-sm text-foreground">3D 장면 로딩 중...</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -101,10 +101,10 @@ export default function Yard3DViewer({ layoutId }: Yard3DViewerProps) {
|
|||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center bg-gray-50">
|
||||
<div className="flex h-full w-full items-center justify-center bg-muted">
|
||||
<div className="text-center">
|
||||
<div className="mb-2 text-4xl">⚠️</div>
|
||||
<div className="text-sm font-medium text-gray-600">{error}</div>
|
||||
<div className="text-sm font-medium text-foreground">{error}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -112,10 +112,10 @@ export default function Yard3DViewer({ layoutId }: Yard3DViewerProps) {
|
|||
|
||||
if (placements.length === 0) {
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center bg-gray-50">
|
||||
<div className="flex h-full w-full items-center justify-center bg-muted">
|
||||
<div className="text-center">
|
||||
<div className="mb-2 text-4xl">📦</div>
|
||||
<div className="text-sm font-medium text-gray-600">배치된 자재가 없습니다</div>
|
||||
<div className="text-sm font-medium text-foreground">배치된 자재가 없습니다</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -132,23 +132,23 @@ export default function Yard3DViewer({ layoutId }: Yard3DViewerProps) {
|
|||
|
||||
{/* 야드 이름 (좌측 상단) */}
|
||||
{layoutName && (
|
||||
<div className="absolute top-4 left-4 z-49 rounded-lg border border-gray-300 bg-white px-4 py-2 shadow-lg">
|
||||
<h2 className="text-base font-bold text-gray-900">{layoutName}</h2>
|
||||
<div className="absolute top-4 left-4 z-49 rounded-lg border border-border bg-background px-4 py-2 shadow-lg">
|
||||
<h2 className="text-base font-bold text-foreground">{layoutName}</h2>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 선택된 자재 정보 패널 (우측 상단) */}
|
||||
{selectedPlacement && (
|
||||
<div className="absolute top-4 right-4 z-50 w-64 rounded-lg border border-gray-300 bg-white p-4 shadow-xl">
|
||||
<div className="absolute top-4 right-4 z-50 w-64 rounded-lg border border-border bg-background p-4 shadow-xl">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<h3 className="text-sm font-semibold text-gray-800">
|
||||
<h3 className="text-sm font-semibold text-foreground">
|
||||
{selectedPlacement.material_name ? "자재 정보" : "미설정 요소"}
|
||||
</h3>
|
||||
<button
|
||||
onClick={() => {
|
||||
setSelectedPlacement(null);
|
||||
}}
|
||||
className="rounded-full p-1 text-gray-400 hover:bg-gray-100 hover:text-gray-600"
|
||||
className="rounded-full p-1 text-muted-foreground hover:bg-muted hover:text-foreground"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
|
|
@ -157,23 +157,23 @@ export default function Yard3DViewer({ layoutId }: Yard3DViewerProps) {
|
|||
{selectedPlacement.material_name && selectedPlacement.quantity && selectedPlacement.unit ? (
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<label className="text-xs font-medium text-gray-500">자재명</label>
|
||||
<div className="mt-1 text-sm font-semibold text-gray-900">{selectedPlacement.material_name}</div>
|
||||
<label className="text-xs font-medium text-muted-foreground">자재명</label>
|
||||
<div className="mt-1 text-sm font-semibold text-foreground">{selectedPlacement.material_name}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-xs font-medium text-gray-500">수량</label>
|
||||
<div className="mt-1 text-sm font-semibold text-gray-900">
|
||||
<label className="text-xs font-medium text-muted-foreground">수량</label>
|
||||
<div className="mt-1 text-sm font-semibold text-foreground">
|
||||
{selectedPlacement.quantity} {selectedPlacement.unit}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-lg bg-orange-50 p-3 text-center">
|
||||
<div className="rounded-lg bg-warning/10 p-3 text-center">
|
||||
<div className="mb-2 text-2xl">⚠️</div>
|
||||
<div className="text-sm font-medium text-orange-700">데이터 바인딩이</div>
|
||||
<div className="text-sm font-medium text-orange-700">설정되지 않았습니다</div>
|
||||
<div className="mt-2 text-xs text-orange-600">편집 모드에서 설정해주세요</div>
|
||||
<div className="text-sm font-medium text-warning">데이터 바인딩이</div>
|
||||
<div className="text-sm font-medium text-warning">설정되지 않았습니다</div>
|
||||
<div className="mt-2 text-xs text-warning">편집 모드에서 설정해주세요</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ const Yard3DCanvas = dynamic(() => import("./Yard3DCanvas"), {
|
|||
ssr: false,
|
||||
loading: () => (
|
||||
<div className="flex h-full items-center justify-center bg-gray-900">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-gray-400" />
|
||||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
),
|
||||
});
|
||||
|
|
@ -465,7 +465,7 @@ export default function YardEditor({ layout, onBack }: YardEditorProps) {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col bg-white">
|
||||
<div className="flex h-full flex-col bg-background">
|
||||
{/* 상단 툴바 */}
|
||||
<div className="flex items-center justify-between border-b p-4">
|
||||
<div className="flex items-center gap-4">
|
||||
|
|
@ -476,16 +476,16 @@ export default function YardEditor({ layout, onBack }: YardEditorProps) {
|
|||
<div className="flex items-center gap-2">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold">{layout.name}</h2>
|
||||
{layout.description && <p className="text-sm text-gray-500">{layout.description}</p>}
|
||||
{layout.description && <p className="text-sm text-muted-foreground">{layout.description}</p>}
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" onClick={handleEditLayout} className="h-8 w-8 p-0">
|
||||
<Edit2 className="h-4 w-4 text-gray-500" />
|
||||
<Edit2 className="h-4 w-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{hasUnsavedChanges && <span className="text-sm font-medium text-orange-600">미저장 변경사항 있음</span>}
|
||||
{hasUnsavedChanges && <span className="text-sm font-medium text-warning">미저장 변경사항 있음</span>}
|
||||
<Button size="sm" onClick={handleSave} disabled={isSaving || !hasUnsavedChanges}>
|
||||
{isSaving ? (
|
||||
<>
|
||||
|
|
@ -515,8 +515,8 @@ export default function YardEditor({ layout, onBack }: YardEditorProps) {
|
|||
{/* 좌측: 3D 캔버스 */}
|
||||
<div className="flex-1">
|
||||
{isLoading ? (
|
||||
<div className="flex h-full items-center justify-center bg-gray-50">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-gray-400" />
|
||||
<div className="flex h-full items-center justify-center bg-muted">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
) : (
|
||||
<Yard3DCanvas
|
||||
|
|
@ -537,7 +537,7 @@ export default function YardEditor({ layout, onBack }: YardEditorProps) {
|
|||
</div>
|
||||
|
||||
{/* 우측: 요소 목록 또는 설정 패널 */}
|
||||
<div className="w-80 border-l bg-white">
|
||||
<div className="w-80 border-l bg-background">
|
||||
{showConfigPanel && selectedPlacement ? (
|
||||
// 설정 패널
|
||||
<YardElementConfigPanel
|
||||
|
|
@ -556,12 +556,12 @@ export default function YardEditor({ layout, onBack }: YardEditorProps) {
|
|||
요소 추가
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500">총 {placements.length}개</p>
|
||||
<p className="text-xs text-muted-foreground">총 {placements.length}개</p>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-auto p-2">
|
||||
{placements.length === 0 ? (
|
||||
<div className="flex h-full items-center justify-center p-4 text-center text-sm text-gray-500">
|
||||
<div className="flex h-full items-center justify-center p-4 text-center text-sm text-muted-foreground">
|
||||
요소가 없습니다.
|
||||
<br />
|
||||
{'위의 "요소 추가" 버튼을 클릭하세요.'}
|
||||
|
|
@ -577,10 +577,10 @@ export default function YardEditor({ layout, onBack }: YardEditorProps) {
|
|||
key={placement.id}
|
||||
className={`rounded-lg border p-3 transition-all ${
|
||||
isSelected
|
||||
? "border-blue-500 bg-blue-50"
|
||||
? "border-primary bg-primary/10"
|
||||
: configured
|
||||
? "border-gray-200 bg-white hover:border-gray-300"
|
||||
: "border-orange-200 bg-orange-50"
|
||||
? "border-border bg-background hover:border-border"
|
||||
: "border-warning bg-warning/10"
|
||||
}`}
|
||||
onClick={() => handleSelectPlacement(placement)}
|
||||
>
|
||||
|
|
@ -588,15 +588,15 @@ export default function YardEditor({ layout, onBack }: YardEditorProps) {
|
|||
<div className="flex-1">
|
||||
{configured ? (
|
||||
<>
|
||||
<div className="text-sm font-medium text-gray-900">{placement.material_name}</div>
|
||||
<div className="mt-1 text-xs text-gray-500">
|
||||
<div className="text-sm font-medium text-foreground">{placement.material_name}</div>
|
||||
<div className="mt-1 text-xs text-muted-foreground">
|
||||
수량: {placement.quantity} {placement.unit}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="text-sm font-medium text-orange-700">요소 #{placement.id}</div>
|
||||
<div className="mt-1 text-xs text-orange-600">데이터 바인딩 설정 필요</div>
|
||||
<div className="text-sm font-medium text-warning">요소 #{placement.id}</div>
|
||||
<div className="mt-1 text-xs text-warning">데이터 바인딩 설정 필요</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -618,7 +618,7 @@ export default function YardEditor({ layout, onBack }: YardEditorProps) {
|
|||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-red-600 hover:bg-red-50"
|
||||
className="text-destructive hover:bg-destructive/10"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDeletePlacement(placement.id);
|
||||
|
|
@ -645,12 +645,12 @@ export default function YardEditor({ layout, onBack }: YardEditorProps) {
|
|||
<DialogTitle className="flex items-center gap-2">
|
||||
{saveResultDialog.success ? (
|
||||
<>
|
||||
<CheckCircle className="h-5 w-5 text-green-600" />
|
||||
<CheckCircle className="h-5 w-5 text-success" />
|
||||
저장 완료
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<AlertCircle className="h-5 w-5 text-red-600" />
|
||||
<AlertCircle className="h-5 w-5 text-destructive" />
|
||||
저장 실패
|
||||
</>
|
||||
)}
|
||||
|
|
@ -671,20 +671,20 @@ export default function YardEditor({ layout, onBack }: YardEditorProps) {
|
|||
<DialogContent onPointerDown={(e) => e.stopPropagation()}>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<AlertCircle className="h-5 w-5 text-orange-600" />
|
||||
<AlertCircle className="h-5 w-5 text-warning" />
|
||||
요소 삭제 확인
|
||||
</DialogTitle>
|
||||
<DialogDescription className="pt-2">
|
||||
이 요소를 삭제하시겠습니까?
|
||||
<br />
|
||||
<span className="font-semibold text-orange-600">저장 버튼을 눌러야 최종적으로 삭제됩니다.</span>
|
||||
<span className="font-semibold text-warning">저장 버튼을 눌러야 최종적으로 삭제됩니다.</span>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button variant="outline" onClick={() => setDeleteConfirmDialog({ open: false, placementId: null })}>
|
||||
취소
|
||||
</Button>
|
||||
<Button onClick={confirmDeletePlacement} className="bg-red-600 hover:bg-red-700">
|
||||
<Button onClick={confirmDeletePlacement} className="bg-destructive hover:bg-destructive/90">
|
||||
삭제
|
||||
</Button>
|
||||
</div>
|
||||
|
|
@ -699,7 +699,7 @@ export default function YardEditor({ layout, onBack }: YardEditorProps) {
|
|||
<DialogContent onPointerDown={(e) => e.stopPropagation()}>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Edit2 className="h-5 w-5 text-blue-600" />
|
||||
<Edit2 className="h-5 w-5 text-primary" />
|
||||
야드 레이아웃 정보 수정
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
|
|
|||
|
|
@ -298,7 +298,7 @@ export default function YardElementConfigPanel({ placement, onSave, onCancel }:
|
|||
<SelectItem key={conn.id} value={String(conn.id)}>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium">{conn.connection_name}</span>
|
||||
<span className="text-xs text-gray-500">({conn.db_type.toUpperCase()})</span>
|
||||
<span className="text-xs text-muted-foreground">({conn.db_type.toUpperCase()})</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
|
|
@ -367,7 +367,7 @@ export default function YardElementConfigPanel({ placement, onSave, onCancel }:
|
|||
placeholder="data.items"
|
||||
className="mt-1"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-gray-500">예: data.items (응답에서 배열이 있는 경로)</p>
|
||||
<p className="mt-1 text-xs text-muted-foreground">예: data.items (응답에서 배열이 있는 경로)</p>
|
||||
</div>
|
||||
|
||||
<Button onClick={executeRestApi} disabled={isExecuting} size="sm" className="w-full">
|
||||
|
|
@ -409,7 +409,7 @@ export default function YardElementConfigPanel({ placement, onSave, onCancel }:
|
|||
{queryResult.rows.slice(0, 10).map((row, idx) => (
|
||||
<TableRow
|
||||
key={idx}
|
||||
className={selectedRowIndex === idx ? "bg-blue-50" : ""}
|
||||
className={selectedRowIndex === idx ? "bg-primary/10" : ""}
|
||||
onClick={() => setSelectedRowIndex(idx)}
|
||||
>
|
||||
<TableCell>
|
||||
|
|
@ -428,7 +428,7 @@ export default function YardElementConfigPanel({ placement, onSave, onCancel }:
|
|||
</Table>
|
||||
</div>
|
||||
{queryResult.rows.length > 10 && (
|
||||
<p className="mt-2 text-xs text-gray-500">... 및 {queryResult.rows.length - 10}개 더</p>
|
||||
<p className="mt-2 text-xs text-muted-foreground">... 및 {queryResult.rows.length - 10}개 더</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
|
@ -468,7 +468,7 @@ export default function YardElementConfigPanel({ placement, onSave, onCancel }:
|
|||
<div>
|
||||
<Label className="text-xs">단위 입력</Label>
|
||||
<Input value={unit} onChange={(e) => setUnit(e.target.value)} placeholder="EA" className="mt-1" />
|
||||
<p className="mt-1 text-xs text-gray-500">예: EA, BOX, KG, M, L 등</p>
|
||||
<p className="mt-1 text-xs text-muted-foreground">예: EA, BOX, KG, M, L 등</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ export default function YardLayoutCreateModal({ isOpen, onClose, onCreate }: Yar
|
|||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="yard-name">
|
||||
야드 이름 <span className="text-red-500">*</span>
|
||||
야드 이름 <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="yard-name"
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ export default function YardLayoutList({ layouts, isLoading, onSelect, onDelete,
|
|||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-gray-400" />
|
||||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -136,7 +136,7 @@ export default function YardLayoutList({ layouts, isLoading, onSelect, onDelete,
|
|||
{/* 검색 및 정렬 */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-gray-400" />
|
||||
<Search className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="야드 이름 또는 설명 검색..."
|
||||
value={searchText}
|
||||
|
|
@ -147,7 +147,7 @@ export default function YardLayoutList({ layouts, isLoading, onSelect, onDelete,
|
|||
<select
|
||||
value={sortOrder}
|
||||
onChange={(e) => setSortOrder(e.target.value as "recent" | "name")}
|
||||
className="rounded-md border border-gray-300 px-3 py-2 text-sm"
|
||||
className="rounded-md border border-border px-3 py-2 text-sm"
|
||||
>
|
||||
<option value="recent">최근 수정순</option>
|
||||
<option value="name">이름순</option>
|
||||
|
|
@ -157,7 +157,7 @@ export default function YardLayoutList({ layouts, isLoading, onSelect, onDelete,
|
|||
{/* 테이블 */}
|
||||
{sortedLayouts.length === 0 ? (
|
||||
<div className="flex flex-1 items-center justify-center">
|
||||
<div className="text-center text-gray-500">
|
||||
<div className="text-center text-muted-foreground">
|
||||
{searchText ? "검색 결과가 없습니다" : "등록된 야드가 없습니다"}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -175,11 +175,11 @@ export default function YardLayoutList({ layouts, isLoading, onSelect, onDelete,
|
|||
</TableHeader>
|
||||
<TableBody>
|
||||
{sortedLayouts.map((layout) => (
|
||||
<TableRow key={layout.id} className="cursor-pointer hover:bg-gray-50" onClick={() => onSelect(layout)}>
|
||||
<TableRow key={layout.id} className="cursor-pointer hover:bg-muted" onClick={() => onSelect(layout)}>
|
||||
<TableCell className="font-medium">{layout.name}</TableCell>
|
||||
<TableCell className="text-gray-600">{layout.description || "-"}</TableCell>
|
||||
<TableCell className="text-foreground">{layout.description || "-"}</TableCell>
|
||||
<TableCell className="text-center">{layout.placement_count}개</TableCell>
|
||||
<TableCell className="text-sm text-gray-500">{formatDate(layout.updated_at)}</TableCell>
|
||||
<TableCell className="text-sm text-muted-foreground">{formatDate(layout.updated_at)}</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild onClick={(e) => e.stopPropagation()}>
|
||||
|
|
@ -190,7 +190,7 @@ export default function YardLayoutList({ layouts, isLoading, onSelect, onDelete,
|
|||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => onSelect(layout)}>편집</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleDuplicateClick(layout)}>복제</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setDeleteTarget(layout)} className="text-red-600">
|
||||
<DropdownMenuItem onClick={() => setDeleteTarget(layout)} className="text-destructive">
|
||||
삭제
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
|
|
@ -204,7 +204,7 @@ export default function YardLayoutList({ layouts, isLoading, onSelect, onDelete,
|
|||
)}
|
||||
|
||||
{/* 총 개수 */}
|
||||
<div className="text-sm text-gray-500">총 {sortedLayouts.length}개</div>
|
||||
<div className="text-sm text-muted-foreground">총 {sortedLayouts.length}개</div>
|
||||
|
||||
{/* 삭제 확인 모달 */}
|
||||
<AlertDialog open={!!deleteTarget} onOpenChange={() => setDeleteTarget(null)}>
|
||||
|
|
@ -222,7 +222,7 @@ export default function YardLayoutList({ layouts, isLoading, onSelect, onDelete,
|
|||
<AlertDialogAction
|
||||
onClick={handleDeleteConfirm}
|
||||
disabled={isDeleting}
|
||||
className="bg-red-600 hover:bg-red-700"
|
||||
className="bg-destructive hover:bg-destructive/90"
|
||||
>
|
||||
{isDeleting ? (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -171,7 +171,7 @@ function renderWidget(element: DashboardElement) {
|
|||
// === 기본 fallback ===
|
||||
default:
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center bg-gradient-to-br from-gray-400 to-gray-600 p-4 text-white">
|
||||
<div className="flex h-full w-full items-center justify-center bg-gradient-to-br from-muted to-muted-foreground p-4 text-white">
|
||||
<div className="text-center">
|
||||
<div className="mb-2 text-3xl">❓</div>
|
||||
<div className="text-sm">알 수 없는 위젯 타입: {element.subtype}</div>
|
||||
|
|
@ -528,11 +528,11 @@ export function DashboardViewer({
|
|||
// 요소가 없는 경우
|
||||
if (elements.length === 0) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center bg-gray-50">
|
||||
<div className="flex h-full items-center justify-center bg-muted">
|
||||
<div className="text-center">
|
||||
<div className="mb-4 text-6xl">📊</div>
|
||||
<div className="mb-2 text-xl font-medium text-gray-700">표시할 요소가 없습니다</div>
|
||||
<div className="text-sm text-gray-500">대시보드 편집기에서 차트나 위젯을 추가해보세요</div>
|
||||
<div className="mb-2 text-xl font-medium text-foreground">표시할 요소가 없습니다</div>
|
||||
<div className="text-sm text-muted-foreground">대시보드 편집기에서 차트나 위젯을 추가해보세요</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -541,7 +541,7 @@ export function DashboardViewer({
|
|||
return (
|
||||
<DashboardProvider>
|
||||
{/* 데스크톱: 디자이너에서 설정한 위치 그대로 렌더링 (화면에 맞춰 비율 유지) */}
|
||||
<div className="hidden min-h-screen bg-gray-100 py-8 lg:block" style={{ backgroundColor }}>
|
||||
<div className="hidden min-h-screen bg-muted py-8 lg:block" style={{ backgroundColor }}>
|
||||
<div className="mx-auto px-4" style={{ maxWidth: `${canvasConfig.width}px` }}>
|
||||
{/* 다운로드 버튼 */}
|
||||
<div className="mb-4 flex justify-end">
|
||||
|
|
@ -584,7 +584,7 @@ export function DashboardViewer({
|
|||
</div>
|
||||
|
||||
{/* 태블릿 이하: 반응형 세로 정렬 */}
|
||||
<div className="block min-h-screen bg-gray-100 p-4 lg:hidden" style={{ backgroundColor }}>
|
||||
<div className="block min-h-screen bg-muted p-4 lg:hidden" style={{ backgroundColor }}>
|
||||
<div className="mx-auto max-w-3xl space-y-4">
|
||||
{/* 다운로드 버튼 */}
|
||||
<div className="flex justify-end">
|
||||
|
|
@ -646,16 +646,16 @@ function ViewerElement({ element, data, isLoading, onRefresh, isMobile, canvasWi
|
|||
// 태블릿 이하: 세로 스택 카드 스타일
|
||||
return (
|
||||
<div
|
||||
className="relative overflow-hidden rounded-lg border border-gray-200 bg-white shadow-sm"
|
||||
className="relative overflow-hidden rounded-lg border border-border bg-background shadow-sm"
|
||||
style={{ minHeight: "300px" }}
|
||||
>
|
||||
{element.showHeader !== false && (
|
||||
<div className="flex items-center justify-between px-2 py-1">
|
||||
<h3 className="text-xs font-semibold text-gray-800">{element.customTitle || element.title}</h3>
|
||||
<h3 className="text-xs font-semibold text-foreground">{element.customTitle || element.title}</h3>
|
||||
<button
|
||||
onClick={onRefresh}
|
||||
disabled={isLoading}
|
||||
className="text-gray-400 transition-colors hover:text-gray-600 disabled:opacity-50"
|
||||
className="text-muted-foreground transition-colors hover:text-foreground disabled:opacity-50"
|
||||
title="새로고침"
|
||||
>
|
||||
<svg
|
||||
|
|
@ -677,7 +677,7 @@ function ViewerElement({ element, data, isLoading, onRefresh, isMobile, canvasWi
|
|||
<div className={element.showHeader !== false ? "p-2" : "p-2"} style={{ minHeight: "250px" }}>
|
||||
{!isMounted ? (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<div className="h-6 w-6 animate-spin rounded-full border-2 border-blue-500 border-t-transparent" />
|
||||
<div className="h-6 w-6 animate-spin rounded-full border-2 border-primary border-t-transparent" />
|
||||
</div>
|
||||
) : element.type === "chart" ? (
|
||||
<ChartRenderer element={element} data={data} width={undefined} height={250} />
|
||||
|
|
@ -686,10 +686,10 @@ function ViewerElement({ element, data, isLoading, onRefresh, isMobile, canvasWi
|
|||
)}
|
||||
</div>
|
||||
{isLoading && (
|
||||
<div className="bg-opacity-75 absolute inset-0 flex items-center justify-center bg-white">
|
||||
<div className="bg-opacity-75 absolute inset-0 flex items-center justify-center bg-background">
|
||||
<div className="text-center">
|
||||
<div className="mx-auto mb-2 h-6 w-6 animate-spin rounded-full border-2 border-blue-500 border-t-transparent" />
|
||||
<div className="text-sm text-gray-600">업데이트 중...</div>
|
||||
<div className="mx-auto mb-2 h-6 w-6 animate-spin rounded-full border-2 border-primary border-t-transparent" />
|
||||
<div className="text-sm text-foreground">업데이트 중...</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -703,7 +703,7 @@ function ViewerElement({ element, data, isLoading, onRefresh, isMobile, canvasWi
|
|||
|
||||
return (
|
||||
<div
|
||||
className="absolute overflow-hidden rounded-lg border border-gray-200 bg-white shadow-sm"
|
||||
className="absolute overflow-hidden rounded-lg border border-border bg-background shadow-sm"
|
||||
style={{
|
||||
left: `${(element.position.x / canvasWidth) * 100}%`,
|
||||
top: element.position.y,
|
||||
|
|
@ -713,11 +713,11 @@ function ViewerElement({ element, data, isLoading, onRefresh, isMobile, canvasWi
|
|||
>
|
||||
{element.showHeader !== false && (
|
||||
<div className="flex items-center justify-between px-2 py-1">
|
||||
<h3 className="text-xs font-semibold text-gray-800">{element.customTitle || element.title}</h3>
|
||||
<h3 className="text-xs font-semibold text-foreground">{element.customTitle || element.title}</h3>
|
||||
<button
|
||||
onClick={onRefresh}
|
||||
disabled={isLoading}
|
||||
className="text-gray-400 transition-colors hover:text-gray-600 disabled:opacity-50"
|
||||
className="text-muted-foreground transition-colors hover:text-foreground disabled:opacity-50"
|
||||
title="새로고침"
|
||||
>
|
||||
<svg
|
||||
|
|
@ -739,7 +739,7 @@ function ViewerElement({ element, data, isLoading, onRefresh, isMobile, canvasWi
|
|||
<div className={element.showHeader !== false ? "h-[calc(100%-32px)] w-full" : "h-full w-full"}>
|
||||
{!isMounted ? (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<div className="h-6 w-6 animate-spin rounded-full border-2 border-blue-500 border-t-transparent" />
|
||||
<div className="h-6 w-6 animate-spin rounded-full border-2 border-primary border-t-transparent" />
|
||||
</div>
|
||||
) : element.type === "chart" ? (
|
||||
<ChartRenderer
|
||||
|
|
@ -753,10 +753,10 @@ function ViewerElement({ element, data, isLoading, onRefresh, isMobile, canvasWi
|
|||
)}
|
||||
</div>
|
||||
{isLoading && (
|
||||
<div className="bg-opacity-75 absolute inset-0 flex items-center justify-center bg-white">
|
||||
<div className="bg-opacity-75 absolute inset-0 flex items-center justify-center bg-background">
|
||||
<div className="text-center">
|
||||
<div className="mx-auto mb-2 h-6 w-6 animate-spin rounded-full border-2 border-blue-500 border-t-transparent" />
|
||||
<div className="text-sm text-gray-600">업데이트 중...</div>
|
||||
<div className="mx-auto mb-2 h-6 w-6 animate-spin rounded-full border-2 border-primary border-t-transparent" />
|
||||
<div className="text-sm text-foreground">업데이트 중...</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -129,10 +129,10 @@ export default function BookingAlertWidget({ element }: BookingAlertWidgetProps)
|
|||
const diff = scheduled.getTime() - now.getTime();
|
||||
const hours = Math.floor(diff / (1000 * 60 * 60));
|
||||
|
||||
if (hours < 0) return { text: "⏰ 시간 초과", color: "text-red-600" };
|
||||
if (hours < 2) return { text: `⏱️ ${hours}시간 후`, color: "text-red-600" };
|
||||
if (hours < 4) return { text: `⏱️ ${hours}시간 후`, color: "text-orange-600" };
|
||||
return { text: `📅 ${hours}시간 후`, color: "text-gray-600" };
|
||||
if (hours < 0) return { text: "⏰ 시간 초과", color: "text-destructive" };
|
||||
if (hours < 2) return { text: `⏱️ ${hours}시간 후`, color: "text-destructive" };
|
||||
if (hours < 4) return { text: `⏱️ ${hours}시간 후`, color: "text-warning" };
|
||||
return { text: `📅 ${hours}시간 후`, color: "text-foreground" };
|
||||
};
|
||||
|
||||
const isNew = (createdAt: string) => {
|
||||
|
|
@ -143,27 +143,27 @@ export default function BookingAlertWidget({ element }: BookingAlertWidgetProps)
|
|||
if (loading) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<div className="text-gray-500">로딩 중...</div>
|
||||
<div className="text-muted-foreground">로딩 중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col bg-gradient-to-br from-slate-50 to-rose-50">
|
||||
<div className="flex h-full flex-col bg-gradient-to-br from-background to-destructive/10">
|
||||
{/* 신규 알림 배너 */}
|
||||
{showNotification && newCount > 0 && (
|
||||
<div className="animate-pulse border-b border-rose-300 bg-rose-100 px-4 py-2 text-center">
|
||||
<span className="font-bold text-rose-700">🔔 새로운 예약 {newCount}건이 도착했습니다!</span>
|
||||
<div className="animate-pulse border-b border-destructive bg-destructive/10 px-4 py-2 text-center">
|
||||
<span className="font-bold text-destructive">🔔 새로운 예약 {newCount}건이 도착했습니다!</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 헤더 */}
|
||||
<div className="border-b border-gray-200 bg-white px-4 py-3">
|
||||
<div className="border-b border-border bg-background px-4 py-3">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<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-foreground">{element?.customTitle || "예약 요청 알림"}</h3>
|
||||
{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-destructive text-xs font-bold text-white">
|
||||
{newCount}
|
||||
</span>
|
||||
)}
|
||||
|
|
@ -183,7 +183,7 @@ export default function BookingAlertWidget({ element }: BookingAlertWidgetProps)
|
|||
key={f}
|
||||
onClick={() => setFilter(f)}
|
||||
className={`rounded px-3 py-1 text-xs font-medium transition-colors ${
|
||||
filter === f ? "bg-primary text-white" : "bg-gray-100 text-gray-600 hover:bg-gray-200"
|
||||
filter === f ? "bg-primary text-white" : "bg-muted text-foreground hover:bg-muted"
|
||||
}`}
|
||||
>
|
||||
{f === "pending" ? "대기중" : f === "accepted" ? "수락됨" : "전체"}
|
||||
|
|
@ -195,7 +195,7 @@ export default function BookingAlertWidget({ element }: BookingAlertWidgetProps)
|
|||
{/* 예약 리스트 */}
|
||||
<div className="flex-1 overflow-y-auto p-4">
|
||||
{bookings.length === 0 ? (
|
||||
<div className="flex h-full items-center justify-center text-gray-400">
|
||||
<div className="flex h-full items-center justify-center text-muted-foreground">
|
||||
<div className="text-center">
|
||||
<div className="mb-2 text-4xl">📭</div>
|
||||
<div>예약 요청이 없습니다</div>
|
||||
|
|
@ -206,14 +206,14 @@ export default function BookingAlertWidget({ element }: BookingAlertWidgetProps)
|
|||
{bookings.map((booking) => (
|
||||
<div
|
||||
key={booking.id}
|
||||
className={`group relative rounded-lg border-2 bg-white p-4 shadow-sm transition-all hover:shadow-md ${
|
||||
booking.priority === "urgent" ? "border-red-400" : "border-gray-200"
|
||||
className={`group relative rounded-lg border-2 bg-background p-4 shadow-sm transition-all hover:shadow-md ${
|
||||
booking.priority === "urgent" ? "border-destructive" : "border-border"
|
||||
} ${booking.status !== "pending" ? "opacity-60" : ""}`}
|
||||
>
|
||||
{/* NEW 뱃지 */}
|
||||
{isNew(booking.createdAt) && booking.status === "pending" && (
|
||||
<div className="absolute -right-2 -top-2 animate-bounce">
|
||||
<span className="flex h-12 w-12 items-center justify-center rounded-full bg-red-500 text-xs font-bold text-white shadow-lg">
|
||||
<span className="flex h-12 w-12 items-center justify-center rounded-full bg-destructive text-xs font-bold text-white shadow-lg">
|
||||
🆕
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -221,7 +221,7 @@ export default function BookingAlertWidget({ element }: BookingAlertWidgetProps)
|
|||
|
||||
{/* 우선순위 표시 */}
|
||||
{booking.priority === "urgent" && (
|
||||
<div className="mb-2 flex items-center gap-1 text-sm font-bold text-red-600">
|
||||
<div className="mb-2 flex items-center gap-1 text-sm font-bold text-destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
긴급 예약
|
||||
</div>
|
||||
|
|
@ -233,8 +233,8 @@ export default function BookingAlertWidget({ element }: BookingAlertWidgetProps)
|
|||
<div className="mb-1 flex items-center gap-2">
|
||||
<span className="text-2xl">{getVehicleIcon(booking.vehicleType)}</span>
|
||||
<div>
|
||||
<div className="font-bold text-gray-800">{booking.customerName}</div>
|
||||
<div className="flex items-center gap-1 text-xs text-gray-600">
|
||||
<div className="font-bold text-foreground">{booking.customerName}</div>
|
||||
<div className="flex items-center gap-1 text-xs text-foreground">
|
||||
<Phone className="h-3 w-3" />
|
||||
{booking.customerPhone}
|
||||
</div>
|
||||
|
|
@ -245,14 +245,14 @@ export default function BookingAlertWidget({ element }: BookingAlertWidgetProps)
|
|||
<div className="flex gap-1">
|
||||
<button
|
||||
onClick={() => handleAccept(booking.id)}
|
||||
className="flex items-center gap-1 rounded bg-green-500 px-3 py-1.5 text-sm font-medium text-white transition-colors hover:bg-green-600"
|
||||
className="flex items-center gap-1 rounded bg-success px-3 py-1.5 text-sm font-medium text-white transition-colors hover:bg-success/90"
|
||||
>
|
||||
<Check className="h-4 w-4" />
|
||||
수락
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleReject(booking.id)}
|
||||
className="flex items-center gap-1 rounded bg-red-500 px-3 py-1.5 text-sm font-medium text-white transition-colors hover:bg-red-600"
|
||||
className="flex items-center gap-1 rounded bg-destructive px-3 py-1.5 text-sm font-medium text-white transition-colors hover:bg-destructive/90"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
거절
|
||||
|
|
@ -260,26 +260,26 @@ export default function BookingAlertWidget({ element }: BookingAlertWidgetProps)
|
|||
</div>
|
||||
)}
|
||||
{booking.status === "accepted" && (
|
||||
<span className="rounded bg-green-100 px-2 py-1 text-xs font-medium text-green-700">
|
||||
<span className="rounded bg-success/10 px-2 py-1 text-xs font-medium text-success">
|
||||
✓ 수락됨
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 경로 정보 */}
|
||||
<div className="mb-3 space-y-2 border-t border-gray-100 pt-3">
|
||||
<div className="mb-3 space-y-2 border-t border-border pt-3">
|
||||
<div className="flex items-start gap-2 text-sm">
|
||||
<MapPin className="mt-0.5 h-4 w-4 flex-shrink-0 text-blue-600" />
|
||||
<MapPin className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-gray-700">출발지</div>
|
||||
<div className="text-gray-600">{booking.pickupLocation}</div>
|
||||
<div className="font-medium text-foreground">출발지</div>
|
||||
<div className="text-foreground">{booking.pickupLocation}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-2 text-sm">
|
||||
<MapPin className="mt-0.5 h-4 w-4 flex-shrink-0 text-rose-600" />
|
||||
<MapPin className="mt-0.5 h-4 w-4 flex-shrink-0 text-destructive" />
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-gray-700">도착지</div>
|
||||
<div className="text-gray-600">{booking.dropoffLocation}</div>
|
||||
<div className="font-medium text-foreground">도착지</div>
|
||||
<div className="text-foreground">{booking.dropoffLocation}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -287,8 +287,8 @@ export default function BookingAlertWidget({ element }: BookingAlertWidgetProps)
|
|||
{/* 상세 정보 */}
|
||||
<div className="grid grid-cols-2 gap-2 text-xs">
|
||||
<div className="flex items-center gap-1">
|
||||
<Package className="h-3 w-3 text-gray-500" />
|
||||
<span className="text-gray-600">
|
||||
<Package className="h-3 w-3 text-muted-foreground" />
|
||||
<span className="text-foreground">
|
||||
{booking.cargoType} ({booking.weight}kg)
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -169,55 +169,55 @@ export default function CalculatorWidget({ element, className = '' }: Calculator
|
|||
}, [display, previousValue, operation, waitingForOperand]);
|
||||
|
||||
return (
|
||||
<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 w-full p-2 sm:p-3 bg-background ${className}`}>
|
||||
<div className="h-full flex flex-col gap-1.5 sm:gap-2">
|
||||
{/* 제목 */}
|
||||
<h3 className="text-base font-semibold text-gray-900 text-center">{element?.customTitle || "계산기"}</h3>
|
||||
<h3 className="text-sm sm:text-base font-semibold text-foreground 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-background border-2 border-border rounded-lg p-2 sm:p-4 shadow-inner min-h-[60px] sm:min-h-[80px]">
|
||||
<div className="text-right h-full flex flex-col justify-center">
|
||||
<div className="h-4 mb-1">
|
||||
<div className="h-3 sm:h-4 mb-0.5 sm:mb-1">
|
||||
{operation && previousValue !== null && (
|
||||
<div className="text-xs text-gray-400">
|
||||
<div className="text-[10px] sm:text-xs text-muted-foreground">
|
||||
{previousValue} {operation}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-gray-900 truncate">
|
||||
<div className="text-lg sm:text-2xl font-bold text-foreground truncate">
|
||||
{display}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 버튼 그리드 */}
|
||||
<div className="flex-1 grid grid-cols-4 gap-2">
|
||||
<div className="flex-1 grid grid-cols-4 gap-1 sm:gap-2">
|
||||
{/* 첫 번째 줄 */}
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleClear}
|
||||
className="h-full text-red-600 hover:bg-red-50 hover:text-red-700 font-semibold select-none"
|
||||
className="h-full text-xs sm:text-base text-destructive hover:bg-destructive/10 hover:text-destructive font-semibold select-none"
|
||||
>
|
||||
AC
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleSign}
|
||||
className="h-full text-gray-600 hover:bg-gray-100 font-semibold select-none"
|
||||
className="h-full text-xs sm:text-base text-foreground hover:bg-muted font-semibold select-none"
|
||||
>
|
||||
+/-
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handlePercent}
|
||||
className="h-full text-gray-600 hover:bg-gray-100 font-semibold select-none"
|
||||
className="h-full text-xs sm:text-base text-foreground hover:bg-muted font-semibold select-none"
|
||||
>
|
||||
%
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={() => handleOperation('÷')}
|
||||
className="h-full bg-blue-500 hover:bg-blue-600 text-white font-semibold select-none"
|
||||
className="h-full text-xs sm:text-base bg-primary hover:bg-primary/90 text-white font-semibold select-none"
|
||||
>
|
||||
÷
|
||||
</Button>
|
||||
|
|
@ -226,28 +226,28 @@ export default function CalculatorWidget({ element, className = '' }: Calculator
|
|||
<Button
|
||||
variant="outline"
|
||||
onClick={() => handleNumber('7')}
|
||||
className="h-full hover:bg-gray-100 font-semibold text-lg select-none"
|
||||
className="h-full text-sm sm:text-lg hover:bg-muted font-semibold select-none"
|
||||
>
|
||||
7
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => handleNumber('8')}
|
||||
className="h-full hover:bg-gray-100 font-semibold text-lg select-none"
|
||||
className="h-full text-sm sm:text-lg hover:bg-muted font-semibold select-none"
|
||||
>
|
||||
8
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => handleNumber('9')}
|
||||
className="h-full hover:bg-gray-100 font-semibold text-lg select-none"
|
||||
className="h-full text-sm sm:text-lg hover:bg-muted font-semibold select-none"
|
||||
>
|
||||
9
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={() => handleOperation('×')}
|
||||
className="h-full bg-blue-500 hover:bg-blue-600 text-white font-semibold select-none"
|
||||
className="h-full text-xs sm:text-base bg-primary hover:bg-primary/90 text-white font-semibold select-none"
|
||||
>
|
||||
×
|
||||
</Button>
|
||||
|
|
@ -256,28 +256,28 @@ export default function CalculatorWidget({ element, className = '' }: Calculator
|
|||
<Button
|
||||
variant="outline"
|
||||
onClick={() => handleNumber('4')}
|
||||
className="h-full hover:bg-gray-100 font-semibold text-lg select-none"
|
||||
className="h-full text-sm sm:text-lg hover:bg-muted font-semibold select-none"
|
||||
>
|
||||
4
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => handleNumber('5')}
|
||||
className="h-full hover:bg-gray-100 font-semibold text-lg select-none"
|
||||
className="h-full text-sm sm:text-lg hover:bg-muted font-semibold select-none"
|
||||
>
|
||||
5
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => handleNumber('6')}
|
||||
className="h-full hover:bg-gray-100 font-semibold text-lg select-none"
|
||||
className="h-full text-sm sm:text-lg hover:bg-muted font-semibold select-none"
|
||||
>
|
||||
6
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={() => handleOperation('-')}
|
||||
className="h-full bg-blue-500 hover:bg-blue-600 text-white font-semibold select-none"
|
||||
className="h-full text-xs sm:text-base bg-primary hover:bg-primary/90 text-white font-semibold select-none"
|
||||
>
|
||||
-
|
||||
</Button>
|
||||
|
|
@ -286,28 +286,28 @@ export default function CalculatorWidget({ element, className = '' }: Calculator
|
|||
<Button
|
||||
variant="outline"
|
||||
onClick={() => handleNumber('1')}
|
||||
className="h-full hover:bg-gray-100 font-semibold text-lg select-none"
|
||||
className="h-full text-sm sm:text-lg hover:bg-muted font-semibold select-none"
|
||||
>
|
||||
1
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => handleNumber('2')}
|
||||
className="h-full hover:bg-gray-100 font-semibold text-lg select-none"
|
||||
className="h-full text-sm sm:text-lg hover:bg-muted font-semibold select-none"
|
||||
>
|
||||
2
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => handleNumber('3')}
|
||||
className="h-full hover:bg-gray-100 font-semibold text-lg select-none"
|
||||
className="h-full text-sm sm:text-lg hover:bg-muted font-semibold select-none"
|
||||
>
|
||||
3
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={() => handleOperation('+')}
|
||||
className="h-full bg-blue-500 hover:bg-blue-600 text-white font-semibold select-none"
|
||||
className="h-full text-xs sm:text-base bg-primary hover:bg-primary/90 text-white font-semibold select-none"
|
||||
>
|
||||
+
|
||||
</Button>
|
||||
|
|
@ -316,21 +316,21 @@ export default function CalculatorWidget({ element, className = '' }: Calculator
|
|||
<Button
|
||||
variant="outline"
|
||||
onClick={() => handleNumber('0')}
|
||||
className="h-full col-span-2 hover:bg-gray-100 font-semibold text-lg select-none"
|
||||
className="h-full col-span-2 text-sm sm:text-lg hover:bg-muted font-semibold select-none"
|
||||
>
|
||||
0
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleDecimal}
|
||||
className="h-full hover:bg-gray-100 font-semibold text-lg select-none"
|
||||
className="h-full text-sm sm:text-lg hover:bg-muted font-semibold select-none"
|
||||
>
|
||||
.
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={handleEquals}
|
||||
className="h-full bg-green-500 hover:bg-green-600 text-white font-semibold select-none"
|
||||
className="h-full text-xs sm:text-base bg-success hover:bg-success/90 text-white font-semibold select-none"
|
||||
>
|
||||
=
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -83,11 +83,11 @@ export default function CargoListWidget({ element }: CargoListWidgetProps) {
|
|||
if (statusLower.includes("배송중") || statusLower.includes("delivering")) {
|
||||
return "bg-primary text-primary-foreground";
|
||||
} else if (statusLower.includes("완료") || statusLower.includes("delivered")) {
|
||||
return "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-100";
|
||||
return "bg-success/10 text-success dark:bg-success/20 dark:text-success";
|
||||
} else if (statusLower.includes("지연") || statusLower.includes("delayed")) {
|
||||
return "bg-destructive text-destructive-foreground";
|
||||
} else if (statusLower.includes("픽업") || statusLower.includes("pending")) {
|
||||
return "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-100";
|
||||
return "bg-warning/10 text-warning dark:bg-warning/20 dark:text-warning";
|
||||
}
|
||||
return "bg-muted text-muted-foreground";
|
||||
};
|
||||
|
|
|
|||
|
|
@ -330,7 +330,7 @@ export default function ChartTestWidget({ element }: ChartTestWidgetProps) {
|
|||
}, [data, dataSourceConfigs, mergeMode, dataSources]);
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col bg-white">
|
||||
<div className="flex h-full w-full flex-col bg-background">
|
||||
{/* 차트 영역 - 전체 공간 사용 */}
|
||||
<div ref={containerRef} className="flex-1 overflow-hidden p-2">
|
||||
{error ? (
|
||||
|
|
|
|||
|
|
@ -38,14 +38,14 @@ const calculateMetric = (rows: any[], field: string, aggregation: string): numbe
|
|||
}
|
||||
};
|
||||
|
||||
// 색상 스타일 매핑
|
||||
// 색상 스타일 매핑 (차분한 색상)
|
||||
const colorMap = {
|
||||
indigo: { bg: "bg-indigo-50", text: "text-indigo-600", border: "border-indigo-200" },
|
||||
green: { bg: "bg-green-50", text: "text-green-600", border: "border-green-200" },
|
||||
blue: { bg: "bg-blue-50", text: "text-blue-600", border: "border-blue-200" },
|
||||
purple: { bg: "bg-purple-50", text: "text-purple-600", border: "border-purple-200" },
|
||||
orange: { bg: "bg-orange-50", text: "text-orange-600", border: "border-orange-200" },
|
||||
gray: { bg: "bg-gray-50", text: "text-gray-600", border: "border-gray-200" },
|
||||
indigo: { bg: "bg-muted", text: "text-foreground", border: "border-border" },
|
||||
green: { bg: "bg-muted", text: "text-foreground", border: "border-border" },
|
||||
blue: { bg: "bg-muted", text: "text-foreground", border: "border-border" },
|
||||
purple: { bg: "bg-muted", text: "text-foreground", border: "border-border" },
|
||||
orange: { bg: "bg-muted", text: "text-foreground", border: "border-border" },
|
||||
gray: { bg: "bg-muted", text: "text-foreground", border: "border-border" },
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -704,10 +704,10 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg
|
|||
// 로딩 상태 (원본 스타일)
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center overflow-hidden bg-white p-2">
|
||||
<div className="flex h-full w-full items-center justify-center overflow-hidden bg-background p-2">
|
||||
<div className="text-center">
|
||||
<div className="border-primary mx-auto h-8 w-8 animate-spin rounded-full border-2 border-t-transparent" />
|
||||
<p className="mt-2 text-sm text-gray-500">데이터 로딩 중...</p>
|
||||
<p className="mt-2 text-sm text-muted-foreground">데이터 로딩 중...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -716,12 +716,12 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg
|
|||
// 에러 상태 (원본 스타일)
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center overflow-hidden bg-white p-2">
|
||||
<div className="flex h-full w-full items-center justify-center overflow-hidden bg-background p-2">
|
||||
<div className="text-center">
|
||||
<p className="text-sm text-red-600">⚠️ {error}</p>
|
||||
<p className="text-sm text-destructive">⚠️ {error}</p>
|
||||
<button
|
||||
onClick={handleManualRefresh}
|
||||
className="mt-2 rounded bg-red-100 px-3 py-1 text-xs text-red-700 hover:bg-red-200"
|
||||
className="mt-2 rounded bg-destructive/10 px-3 py-1 text-xs text-destructive hover:bg-destructive/20"
|
||||
>
|
||||
다시 시도
|
||||
</button>
|
||||
|
|
@ -733,8 +733,8 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg
|
|||
// 데이터 소스 없음 (원본 스타일)
|
||||
if (!(element?.dataSources || element?.chartConfig?.dataSources) && !isGroupByMode) {
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center overflow-hidden bg-white p-2">
|
||||
<p className="text-sm text-gray-500">데이터 소스를 연결해주세요</p>
|
||||
<div className="flex h-full w-full items-center justify-center overflow-hidden bg-background p-2">
|
||||
<p className="text-sm text-muted-foreground">데이터 소스를 연결해주세요</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -742,15 +742,15 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg
|
|||
// 메트릭 설정 없음 (원본 스타일)
|
||||
if (metricConfig.length === 0 && !isGroupByMode) {
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center overflow-hidden bg-white p-2">
|
||||
<p className="text-sm text-gray-500">메트릭을 설정해주세요</p>
|
||||
<div className="flex h-full w-full items-center justify-center overflow-hidden bg-background p-2">
|
||||
<p className="text-sm text-muted-foreground">메트릭을 설정해주세요</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 메인 렌더링 (원본 스타일 - 심플하게)
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col bg-white p-2">
|
||||
<div className="flex h-full w-full flex-col bg-background p-2">
|
||||
{/* 콘텐츠 영역 - 스크롤 가능하도록 개선 */}
|
||||
<div
|
||||
className="grid w-full gap-2 overflow-y-auto"
|
||||
|
|
@ -769,7 +769,7 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg
|
|||
key={`group-${index}`}
|
||||
className={`flex flex-col items-center justify-center rounded-lg border ${colors.bg} ${colors.border} p-2`}
|
||||
>
|
||||
<div className="text-[10px] text-gray-600">{card.label}</div>
|
||||
<div className="text-[10px] text-foreground">{card.label}</div>
|
||||
<div className={`mt-0.5 text-xl font-bold ${colors.text}`}>{card.value.toLocaleString()}</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -792,7 +792,7 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg
|
|||
}}
|
||||
className={`flex cursor-pointer flex-col items-center justify-center rounded-lg border ${colors.bg} ${colors.border} p-2 transition-all hover:shadow-md`}
|
||||
>
|
||||
<div className="text-[10px] text-gray-600">{metric.label}</div>
|
||||
<div className="text-[10px] text-foreground">{metric.label}</div>
|
||||
<div className={`mt-0.5 text-xl font-bold ${colors.text}`}>
|
||||
{formattedValue}
|
||||
{metric.unit && <span className="ml-0.5 text-sm">{metric.unit}</span>}
|
||||
|
|
|
|||
|
|
@ -35,12 +35,12 @@ const calculateMetric = (rows: any[], field: string, aggregation: string): numbe
|
|||
|
||||
// 색상 스타일 매핑
|
||||
const colorMap = {
|
||||
indigo: { bg: "bg-indigo-50", text: "text-indigo-600", border: "border-indigo-200" },
|
||||
green: { bg: "bg-green-50", text: "text-green-600", border: "border-green-200" },
|
||||
blue: { bg: "bg-blue-50", text: "text-blue-600", border: "border-blue-200" },
|
||||
purple: { bg: "bg-purple-50", text: "text-purple-600", border: "border-purple-200" },
|
||||
orange: { bg: "bg-orange-50", text: "text-orange-600", border: "border-orange-200" },
|
||||
gray: { bg: "bg-gray-50", text: "text-gray-600", border: "border-gray-200" },
|
||||
indigo: { bg: "bg-muted", text: "text-foreground", border: "border-border" },
|
||||
green: { bg: "bg-muted", text: "text-foreground", border: "border-border" },
|
||||
blue: { bg: "bg-muted", text: "text-foreground", border: "border-border" },
|
||||
purple: { bg: "bg-muted", text: "text-foreground", border: "border-border" },
|
||||
orange: { bg: "bg-muted", text: "text-foreground", border: "border-border" },
|
||||
gray: { bg: "bg-muted", text: "text-foreground", border: "border-border" },
|
||||
};
|
||||
|
||||
export default function CustomMetricWidget({ element }: CustomMetricWidgetProps) {
|
||||
|
|
@ -298,10 +298,10 @@ export default function CustomMetricWidget({ element }: CustomMetricWidgetProps)
|
|||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center bg-white">
|
||||
<div className="flex h-full items-center justify-center bg-background">
|
||||
<div className="text-center">
|
||||
<div className="border-primary mx-auto h-8 w-8 animate-spin rounded-full border-2 border-t-transparent" />
|
||||
<p className="mt-2 text-sm text-gray-500">데이터 로딩 중...</p>
|
||||
<p className="mt-2 text-sm text-muted-foreground">데이터 로딩 중...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -309,12 +309,12 @@ export default function CustomMetricWidget({ element }: CustomMetricWidgetProps)
|
|||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center bg-white p-4">
|
||||
<div className="flex h-full items-center justify-center bg-background p-4">
|
||||
<div className="text-center">
|
||||
<p className="text-sm text-red-600">⚠️ {error}</p>
|
||||
<p className="text-sm text-destructive">⚠️ {error}</p>
|
||||
<button
|
||||
onClick={loadData}
|
||||
className="mt-2 rounded bg-red-100 px-3 py-1 text-xs text-red-700 hover:bg-red-200"
|
||||
className="mt-2 rounded bg-destructive/10 px-3 py-1 text-xs text-destructive hover:bg-destructive/20"
|
||||
>
|
||||
다시 시도
|
||||
</button>
|
||||
|
|
@ -344,10 +344,10 @@ export default function CustomMetricWidget({ element }: CustomMetricWidgetProps)
|
|||
|
||||
if (shouldShowEmpty) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center bg-white p-4">
|
||||
<div className="flex h-full items-center justify-center bg-background p-4">
|
||||
<div className="max-w-xs space-y-2 text-center">
|
||||
<h3 className="text-sm font-bold text-gray-900">사용자 커스텀 카드</h3>
|
||||
<div className="space-y-1.5 text-xs text-gray-600">
|
||||
<h3 className="text-sm font-bold text-foreground">사용자 커스텀 카드</h3>
|
||||
<div className="space-y-1.5 text-xs text-foreground">
|
||||
<p className="font-medium">📊 맞춤형 지표 위젯</p>
|
||||
<ul className="space-y-0.5 text-left">
|
||||
<li>• SQL 쿼리로 데이터를 불러옵니다</li>
|
||||
|
|
@ -359,7 +359,7 @@ export default function CustomMetricWidget({ element }: CustomMetricWidgetProps)
|
|||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="mt-2 rounded-lg bg-blue-50 p-2 text-[10px] text-blue-700">
|
||||
<div className="mt-2 rounded-lg bg-primary/10 p-2 text-[10px] text-primary">
|
||||
<p className="font-medium">⚙️ 설정 방법</p>
|
||||
<p className="mb-1">
|
||||
{isGroupByMode
|
||||
|
|
@ -379,7 +379,7 @@ export default function CustomMetricWidget({ element }: CustomMetricWidgetProps)
|
|||
|
||||
return (
|
||||
<div
|
||||
className={`flex h-full w-full overflow-hidden bg-white p-0.5 ${
|
||||
className={`flex h-full w-full overflow-hidden bg-background p-0.5 ${
|
||||
isHorizontalLayout ? "flex-row gap-0.5" : "flex-col gap-0.5"
|
||||
}`}
|
||||
>
|
||||
|
|
@ -396,7 +396,7 @@ export default function CustomMetricWidget({ element }: CustomMetricWidgetProps)
|
|||
key={`group-${index}`}
|
||||
className={`flex flex-1 flex-col items-center justify-center rounded border ${colors.bg} ${colors.border} p-0.5`}
|
||||
>
|
||||
<div className="text-[8px] leading-tight text-gray-600">{card.label}</div>
|
||||
<div className="text-[8px] leading-tight text-foreground">{card.label}</div>
|
||||
<div className={`mt-0 text-xs leading-none font-bold ${colors.text}`}>{card.value.toLocaleString()}</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -412,7 +412,7 @@ export default function CustomMetricWidget({ element }: CustomMetricWidgetProps)
|
|||
key={metric.id}
|
||||
className={`flex flex-1 flex-col items-center justify-center rounded border ${colors.bg} ${colors.border} p-0.5`}
|
||||
>
|
||||
<div className="text-[8px] leading-tight text-gray-600">{metric.label}</div>
|
||||
<div className="text-[8px] leading-tight text-foreground">{metric.label}</div>
|
||||
<div className={`mt-0 text-xs leading-none font-bold ${colors.text}`}>
|
||||
{formattedValue}
|
||||
<span className="ml-0 text-[8px]">{metric.unit}</span>
|
||||
|
|
|
|||
|
|
@ -600,26 +600,26 @@ export default function CustomStatsWidget({ element, refreshInterval = 60000 }:
|
|||
// 색상 매핑
|
||||
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" },
|
||||
indigo: { bg: "bg-primary/10", text: "text-primary" },
|
||||
green: { bg: "bg-success/10", text: "text-success" },
|
||||
blue: { bg: "bg-primary/10", text: "text-primary" },
|
||||
purple: { bg: "bg-purple-500/10", text: "text-purple-500" },
|
||||
orange: { bg: "bg-warning/10", text: "text-warning" },
|
||||
yellow: { bg: "bg-warning/10", text: "text-warning" },
|
||||
cyan: { bg: "bg-primary/10", text: "text-primary" },
|
||||
pink: { bg: "bg-muted", text: "text-foreground" },
|
||||
teal: { bg: "bg-primary/10", text: "text-primary" },
|
||||
gray: { bg: "bg-muted", text: "text-foreground" },
|
||||
};
|
||||
return colorMap[color] || colorMap.gray;
|
||||
};
|
||||
|
||||
if (isLoading && stats.length === 0) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center bg-gray-50">
|
||||
<div className="flex h-full items-center justify-center bg-muted">
|
||||
<div className="text-center">
|
||||
<div className="mx-auto h-8 w-8 animate-spin rounded-full border-4 border-blue-500 border-t-transparent" />
|
||||
<div className="mt-2 text-sm text-gray-600">로딩 중...</div>
|
||||
<div className="mx-auto h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent" />
|
||||
<div className="mt-2 text-sm text-foreground">로딩 중...</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -627,14 +627,14 @@ export default function CustomStatsWidget({ element, refreshInterval = 60000 }:
|
|||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center bg-gray-50 p-4">
|
||||
<div className="flex h-full items-center justify-center bg-muted p-4">
|
||||
<div className="text-center">
|
||||
<div className="mb-2 text-4xl">⚠️</div>
|
||||
<div className="text-sm font-medium text-gray-600">{error}</div>
|
||||
{!element?.dataSource?.query && <div className="mt-2 text-xs text-gray-500">쿼리를 설정하세요</div>}
|
||||
<div className="text-sm font-medium text-foreground">{error}</div>
|
||||
{!element?.dataSource?.query && <div className="mt-2 text-xs text-muted-foreground">쿼리를 설정하세요</div>}
|
||||
<button
|
||||
onClick={loadData}
|
||||
className="mt-3 rounded-lg bg-blue-500 px-4 py-2 text-sm text-white hover:bg-blue-600"
|
||||
className="mt-3 rounded-lg bg-primary px-4 py-2 text-sm text-white hover:bg-primary/90"
|
||||
>
|
||||
다시 시도
|
||||
</button>
|
||||
|
|
@ -645,11 +645,11 @@ export default function CustomStatsWidget({ element, refreshInterval = 60000 }:
|
|||
|
||||
if (stats.length === 0) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center bg-gray-50 p-4">
|
||||
<div className="flex h-full items-center justify-center bg-muted p-4">
|
||||
<div className="text-center">
|
||||
<div className="mb-2 text-4xl">📊</div>
|
||||
<div className="text-sm font-medium text-gray-600">데이터 없음</div>
|
||||
<div className="mt-2 text-xs text-gray-500">쿼리를 실행하여 통계를 확인하세요</div>
|
||||
<div className="text-sm font-medium text-foreground">데이터 없음</div>
|
||||
<div className="mt-2 text-xs text-muted-foreground">쿼리를 실행하여 통계를 확인하세요</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -685,13 +685,13 @@ export default function CustomStatsWidget({ element, refreshInterval = 60000 }:
|
|||
// console.log("🎨 렌더링 - allStats:", allStats.map(s => s.label));
|
||||
|
||||
return (
|
||||
<div className="relative flex h-full flex-col bg-white">
|
||||
<div className="relative flex h-full flex-col bg-background">
|
||||
{/* 헤더 영역 */}
|
||||
<div className="flex items-center justify-between border-b bg-gray-50 px-4 py-2">
|
||||
<div className="flex items-center justify-between border-b bg-muted px-4 py-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-lg">📊</span>
|
||||
<span className="text-sm font-medium text-gray-700">커스텀 통계</span>
|
||||
<span className="text-xs text-gray-500">({stats.length}개 표시 중)</span>
|
||||
<span className="text-sm font-medium text-foreground">커스텀 통계</span>
|
||||
<span className="text-xs text-muted-foreground">({stats.length}개 표시 중)</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
|
|
@ -701,7 +701,7 @@ export default function CustomStatsWidget({ element, refreshInterval = 60000 }:
|
|||
setSelectedStats(currentLabels);
|
||||
setShowSettings(true);
|
||||
}}
|
||||
className="flex items-center gap-1 rounded-lg px-3 py-1.5 text-sm text-gray-600 hover:bg-gray-100"
|
||||
className="flex items-center gap-1 rounded-lg px-3 py-1.5 text-sm text-foreground hover:bg-muted"
|
||||
title="표시할 통계 선택"
|
||||
>
|
||||
<span>⚙️</span>
|
||||
|
|
@ -716,7 +716,7 @@ export default function CustomStatsWidget({ element, refreshInterval = 60000 }:
|
|||
const colors = getColorClasses(stat.color);
|
||||
return (
|
||||
<div key={index} className={`rounded-lg border ${colors.bg} p-4 text-center`}>
|
||||
<div className="text-sm text-gray-600">{stat.label}</div>
|
||||
<div className="text-sm text-foreground">{stat.label}</div>
|
||||
<div className={`mt-2 text-3xl font-bold ${colors.text}`}>
|
||||
{stat.value.toFixed(stat.unit === "%" || stat.unit === "분" ? 1 : 0).toLocaleString()}
|
||||
<span className="ml-1 text-lg">{stat.unit}</span>
|
||||
|
|
@ -730,15 +730,15 @@ export default function CustomStatsWidget({ element, refreshInterval = 60000 }:
|
|||
{/* 설정 모달 */}
|
||||
{showSettings && (
|
||||
<div className="absolute inset-0 z-20 flex items-center justify-center bg-black/50">
|
||||
<div className="max-h-[80%] w-[90%] max-w-md overflow-auto rounded-lg bg-white p-6 shadow-xl">
|
||||
<div className="max-h-[80%] w-[90%] max-w-md overflow-auto rounded-lg bg-background p-6 shadow-xl">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<h3 className="text-lg font-bold">표시할 통계 선택</h3>
|
||||
<button onClick={() => setShowSettings(false)} className="text-2xl text-gray-500 hover:text-gray-700">
|
||||
<button onClick={() => setShowSettings(false)} className="text-2xl text-muted-foreground hover:text-foreground">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 text-sm text-gray-600">표시하고 싶은 통계를 선택하세요 (최대 제한 없음)</div>
|
||||
<div className="mb-4 text-sm text-foreground">표시하고 싶은 통계를 선택하세요 (최대 제한 없음)</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
{allStats.map((stat, index) => {
|
||||
|
|
@ -747,7 +747,7 @@ export default function CustomStatsWidget({ element, refreshInterval = 60000 }:
|
|||
<label
|
||||
key={index}
|
||||
className={`flex cursor-pointer items-center gap-3 rounded-lg border p-3 transition-colors ${
|
||||
isChecked ? "border-blue-500 bg-blue-50" : "hover:bg-gray-50"
|
||||
isChecked ? "border-primary bg-primary/10" : "hover:bg-muted"
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
|
|
@ -761,9 +761,9 @@ export default function CustomStatsWidget({ element, refreshInterval = 60000 }:
|
|||
/>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium">{stat.label}</div>
|
||||
<div className="text-sm text-gray-500">단위: {stat.unit}</div>
|
||||
<div className="text-sm text-muted-foreground">단위: {stat.unit}</div>
|
||||
</div>
|
||||
{isChecked && <span className="text-blue-600">✓</span>}
|
||||
{isChecked && <span className="text-primary">✓</span>}
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
|
|
@ -772,7 +772,7 @@ export default function CustomStatsWidget({ element, refreshInterval = 60000 }:
|
|||
<div className="mt-6 flex gap-2">
|
||||
<button
|
||||
onClick={handleApplySettings}
|
||||
className="flex-1 rounded-lg bg-blue-500 py-2 text-white hover:bg-blue-600"
|
||||
className="flex-1 rounded-lg bg-primary py-2 text-white hover:bg-primary/90"
|
||||
>
|
||||
적용 ({selectedStats.length}개 선택)
|
||||
</button>
|
||||
|
|
@ -781,7 +781,7 @@ export default function CustomStatsWidget({ element, refreshInterval = 60000 }:
|
|||
// console.log("❌ 설정 취소");
|
||||
setShowSettings(false);
|
||||
}}
|
||||
className="rounded-lg border px-4 py-2 hover:bg-gray-50"
|
||||
className="rounded-lg border px-4 py-2 hover:bg-muted"
|
||||
>
|
||||
취소
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -85,9 +85,9 @@ export default function CustomerIssuesWidget({ element }: CustomerIssuesWidgetPr
|
|||
if (priorityLower.includes("긴급") || priorityLower.includes("high") || priorityLower.includes("urgent")) {
|
||||
return "bg-destructive text-destructive-foreground";
|
||||
} else if (priorityLower.includes("보통") || priorityLower.includes("medium") || priorityLower.includes("normal")) {
|
||||
return "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-100";
|
||||
return "bg-warning/10 text-warning dark:bg-warning/20 dark:text-warning";
|
||||
} else if (priorityLower.includes("낮음") || priorityLower.includes("low")) {
|
||||
return "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-100";
|
||||
return "bg-success/10 text-success dark:bg-success/20 dark:text-success";
|
||||
}
|
||||
return "bg-muted text-muted-foreground";
|
||||
};
|
||||
|
|
@ -98,7 +98,7 @@ export default function CustomerIssuesWidget({ element }: CustomerIssuesWidgetPr
|
|||
if (statusLower.includes("처리중") || statusLower.includes("processing") || statusLower.includes("pending")) {
|
||||
return "bg-primary text-primary-foreground";
|
||||
} else if (statusLower.includes("완료") || statusLower.includes("resolved") || statusLower.includes("closed")) {
|
||||
return "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-100";
|
||||
return "bg-success/10 text-success dark:bg-success/20 dark:text-success";
|
||||
}
|
||||
return "bg-muted text-muted-foreground";
|
||||
};
|
||||
|
|
@ -188,7 +188,7 @@ export default function CustomerIssuesWidget({ element }: CustomerIssuesWidgetPr
|
|||
onClick={() => setFilterPriority("보통")}
|
||||
className={`rounded-md px-3 py-1 text-xs transition-colors ${
|
||||
filterPriority === "보통"
|
||||
? "bg-yellow-100 text-yellow-800"
|
||||
? "bg-warning/10 text-warning"
|
||||
: "bg-muted text-muted-foreground hover:bg-muted/80"
|
||||
}`}
|
||||
>
|
||||
|
|
@ -198,7 +198,7 @@ export default function CustomerIssuesWidget({ element }: CustomerIssuesWidgetPr
|
|||
onClick={() => setFilterPriority("낮음")}
|
||||
className={`rounded-md px-3 py-1 text-xs transition-colors ${
|
||||
filterPriority === "낮음"
|
||||
? "bg-green-100 text-green-800"
|
||||
? "bg-success/10 text-success"
|
||||
: "bg-muted text-muted-foreground hover:bg-muted/80"
|
||||
}`}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -89,45 +89,45 @@ export default function DeliveryStatusSummaryWidget({ element }: DeliveryStatusS
|
|||
const getBorderColor = (status: string) => {
|
||||
switch (status) {
|
||||
case "배송중":
|
||||
return "border-blue-500";
|
||||
return "border-primary";
|
||||
case "완료":
|
||||
return "border-green-500";
|
||||
return "border-success";
|
||||
case "지연":
|
||||
return "border-red-500";
|
||||
return "border-destructive";
|
||||
case "픽업 대기":
|
||||
return "border-yellow-500";
|
||||
return "border-warning";
|
||||
default:
|
||||
return "border-gray-500";
|
||||
return "border-border";
|
||||
}
|
||||
};
|
||||
|
||||
const getDotColor = (status: string) => {
|
||||
switch (status) {
|
||||
case "배송중":
|
||||
return "bg-blue-500";
|
||||
return "bg-primary";
|
||||
case "완료":
|
||||
return "bg-green-500";
|
||||
return "bg-success";
|
||||
case "지연":
|
||||
return "bg-red-500";
|
||||
return "bg-destructive";
|
||||
case "픽업 대기":
|
||||
return "bg-yellow-500";
|
||||
return "bg-warning/100";
|
||||
default:
|
||||
return "bg-gray-500";
|
||||
return "bg-muted0";
|
||||
}
|
||||
};
|
||||
|
||||
const getTextColor = (status: string) => {
|
||||
switch (status) {
|
||||
case "배송중":
|
||||
return "text-blue-600";
|
||||
return "text-primary";
|
||||
case "완료":
|
||||
return "text-green-600";
|
||||
return "text-success";
|
||||
case "지연":
|
||||
return "text-red-600";
|
||||
return "text-destructive";
|
||||
case "픽업 대기":
|
||||
return "text-yellow-600";
|
||||
return "text-warning";
|
||||
default:
|
||||
return "text-gray-600";
|
||||
return "text-foreground";
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -136,7 +136,7 @@ export default function DeliveryStatusSummaryWidget({ element }: DeliveryStatusS
|
|||
<div className="flex h-full items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="border-primary mx-auto h-8 w-8 animate-spin rounded-full border-2 border-t-transparent" />
|
||||
<p className="mt-2 text-sm text-gray-500">데이터 로딩 중...</p>
|
||||
<p className="mt-2 text-sm text-muted-foreground">데이터 로딩 중...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -145,11 +145,11 @@ export default function DeliveryStatusSummaryWidget({ element }: DeliveryStatusS
|
|||
if (error) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<div className="text-center text-red-500">
|
||||
<div className="text-center text-destructive">
|
||||
<p className="text-sm">⚠️ {error}</p>
|
||||
<button
|
||||
onClick={loadData}
|
||||
className="mt-2 rounded bg-red-100 px-3 py-1 text-xs text-red-700 hover:bg-red-200"
|
||||
className="mt-2 rounded bg-destructive/10 px-3 py-1 text-xs text-destructive hover:bg-destructive/20"
|
||||
>
|
||||
다시 시도
|
||||
</button>
|
||||
|
|
@ -161,7 +161,7 @@ export default function DeliveryStatusSummaryWidget({ element }: DeliveryStatusS
|
|||
if (!element?.dataSource?.query) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<div className="text-center text-gray-500">
|
||||
<div className="text-center text-muted-foreground">
|
||||
<p className="text-sm">데이터를 연결하세요</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -171,20 +171,20 @@ export default function DeliveryStatusSummaryWidget({ element }: DeliveryStatusS
|
|||
const totalCount = statusData.reduce((sum, item) => sum + item.count, 0);
|
||||
|
||||
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-background to-primary/10 p-2">
|
||||
{/* 헤더 */}
|
||||
<div className="mb-2 flex flex-shrink-0 items-center justify-between">
|
||||
<div className="flex-1">
|
||||
<h3 className="text-sm font-bold text-gray-900">📊 배송 상태 요약</h3>
|
||||
<h3 className="text-sm font-bold text-foreground">📊 배송 상태 요약</h3>
|
||||
{totalCount > 0 ? (
|
||||
<p className="text-xs text-gray-500">총 {totalCount.toLocaleString()}건</p>
|
||||
<p className="text-xs text-muted-foreground">총 {totalCount.toLocaleString()}건</p>
|
||||
) : (
|
||||
<p className="text-xs text-orange-500">⚙️ 데이터 연결 필요</p>
|
||||
<p className="text-xs text-warning">⚙️ 데이터 연결 필요</p>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={loadData}
|
||||
className="border-border hover:bg-accent flex h-7 w-7 items-center justify-center rounded border bg-white p-0 text-xs disabled:opacity-50"
|
||||
className="border-border hover:bg-accent flex h-7 w-7 items-center justify-center rounded border bg-background p-0 text-xs disabled:opacity-50"
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? "⏳" : "🔄"}
|
||||
|
|
@ -198,11 +198,11 @@ export default function DeliveryStatusSummaryWidget({ element }: DeliveryStatusS
|
|||
{statusData.map((item) => (
|
||||
<div
|
||||
key={item.status}
|
||||
className={`rounded border-l-2 bg-white p-1.5 shadow-sm ${getBorderColor(item.status)}`}
|
||||
className={`rounded border-l-2 bg-background p-1.5 shadow-sm ${getBorderColor(item.status)}`}
|
||||
>
|
||||
<div className="mb-0.5 flex items-center gap-1">
|
||||
<div className={`h-1.5 w-1.5 rounded-full ${getDotColor(item.status)}`}></div>
|
||||
<div className="text-xs font-medium text-gray-600">{item.status}</div>
|
||||
<div className="text-xs font-medium text-foreground">{item.status}</div>
|
||||
</div>
|
||||
<div className={`text-lg font-bold ${getTextColor(item.status)}`}>{item.count.toLocaleString()}</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -176,15 +176,15 @@ export default function DeliveryStatusWidget({ element, refreshInterval = 60000
|
|||
const getStatusColor = (status: DeliveryItem["status"]) => {
|
||||
switch (status) {
|
||||
case "in_transit":
|
||||
return "bg-blue-100 text-blue-700 border-blue-300";
|
||||
return "bg-primary/10 text-primary border-primary";
|
||||
case "delivered":
|
||||
return "bg-green-100 text-green-700 border-green-300";
|
||||
return "bg-success/10 text-success border-success";
|
||||
case "delayed":
|
||||
return "bg-red-100 text-red-700 border-red-300";
|
||||
return "bg-destructive/10 text-destructive border-destructive";
|
||||
case "pickup_waiting":
|
||||
return "bg-yellow-100 text-yellow-700 border-yellow-300";
|
||||
return "bg-warning/10 text-warning border-warning";
|
||||
default:
|
||||
return "bg-gray-100 text-gray-700 border-gray-300";
|
||||
return "bg-muted text-foreground border-border";
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -236,13 +236,13 @@ export default function DeliveryStatusWidget({ element, refreshInterval = 60000
|
|||
const getIssueStatusColor = (status: CustomerIssue["status"]) => {
|
||||
switch (status) {
|
||||
case "open":
|
||||
return "bg-red-100 text-red-700 border-red-300";
|
||||
return "bg-destructive/10 text-destructive border-destructive";
|
||||
case "in_progress":
|
||||
return "bg-yellow-100 text-yellow-700 border-yellow-300";
|
||||
return "bg-warning/10 text-warning border-warning";
|
||||
case "resolved":
|
||||
return "bg-green-100 text-green-700 border-green-300";
|
||||
return "bg-success/10 text-success border-success";
|
||||
default:
|
||||
return "bg-gray-100 text-gray-700 border-gray-300";
|
||||
return "bg-muted text-foreground border-border";
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -293,12 +293,12 @@ export default function DeliveryStatusWidget({ element, refreshInterval = 60000
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="h-full w-full overflow-auto bg-gradient-to-br from-slate-50 to-blue-50 p-4">
|
||||
<div className="h-full w-full overflow-auto bg-gradient-to-br from-background to-primary/10 p-4">
|
||||
{/* 헤더 */}
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-gray-900">📦 배송 / 화물 처리 현황</h3>
|
||||
<p className="text-xs text-gray-500">마지막 업데이트: {lastUpdate.toLocaleTimeString("ko-KR")}</p>
|
||||
<h3 className="text-lg font-bold text-foreground">📦 배송 / 화물 처리 현황</h3>
|
||||
<p className="text-xs text-muted-foreground">마지막 업데이트: {lastUpdate.toLocaleTimeString("ko-KR")}</p>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" onClick={loadData} disabled={isLoading} className="h-8 w-8 p-0">
|
||||
<RefreshCw className={`h-4 w-4 ${isLoading ? "animate-spin" : ""}`} />
|
||||
|
|
@ -307,96 +307,96 @@ export default function DeliveryStatusWidget({ element, refreshInterval = 60000
|
|||
|
||||
{/* 배송 상태 요약 */}
|
||||
<div className="mb-3">
|
||||
<h4 className="mb-2 text-sm font-semibold text-gray-700">배송 상태 요약 (클릭하여 필터링)</h4>
|
||||
<h4 className="mb-2 text-sm font-semibold text-foreground">배송 상태 요약 (클릭하여 필터링)</h4>
|
||||
<div className="grid grid-cols-2 gap-2 md:grid-cols-5">
|
||||
<button
|
||||
onClick={() => setSelectedStatus("all")}
|
||||
className={`rounded-lg border-l-4 p-1.5 shadow-sm transition-all ${
|
||||
selectedStatus === "all"
|
||||
? "border-gray-900 bg-gray-100 ring-2 ring-gray-900"
|
||||
: "border-gray-500 bg-white hover:bg-gray-50"
|
||||
? "border-foreground bg-muted ring-2 ring-foreground"
|
||||
: "border-border bg-background hover:bg-muted"
|
||||
}`}
|
||||
>
|
||||
<div className="mb-0.5 text-xs text-gray-600">전체</div>
|
||||
<div className="text-lg font-bold text-gray-900">{deliveries.length}</div>
|
||||
<div className="mb-0.5 text-xs text-foreground">전체</div>
|
||||
<div className="text-lg font-bold text-foreground">{deliveries.length}</div>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setSelectedStatus("in_transit")}
|
||||
className={`rounded-lg border-l-4 p-1.5 shadow-sm transition-all ${
|
||||
selectedStatus === "in_transit"
|
||||
? "border-blue-900 bg-blue-100 ring-2 ring-blue-900"
|
||||
: "border-blue-500 bg-white hover:bg-blue-50"
|
||||
? "border-primary bg-primary/10 ring-2 ring-primary"
|
||||
: "border-primary bg-background hover:bg-primary/10"
|
||||
}`}
|
||||
>
|
||||
<div className="mb-0.5 text-xs text-gray-600">배송중</div>
|
||||
<div className="text-lg font-bold text-blue-600">{statusStats.in_transit}</div>
|
||||
<div className="mb-0.5 text-xs text-foreground">배송중</div>
|
||||
<div className="text-lg font-bold text-primary">{statusStats.in_transit}</div>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setSelectedStatus("delivered")}
|
||||
className={`rounded-lg border-l-4 p-1.5 shadow-sm transition-all ${
|
||||
selectedStatus === "delivered"
|
||||
? "border-green-900 bg-green-100 ring-2 ring-green-900"
|
||||
: "border-green-500 bg-white hover:bg-green-50"
|
||||
? "border-success bg-success/10 ring-2 ring-success"
|
||||
: "border-success bg-background hover:bg-success/10"
|
||||
}`}
|
||||
>
|
||||
<div className="mb-0.5 text-xs text-gray-600">완료</div>
|
||||
<div className="text-lg font-bold text-green-600">{statusStats.delivered}</div>
|
||||
<div className="mb-0.5 text-xs text-foreground">완료</div>
|
||||
<div className="text-lg font-bold text-success">{statusStats.delivered}</div>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setSelectedStatus("delayed")}
|
||||
className={`rounded-lg border-l-4 p-1.5 shadow-sm transition-all ${
|
||||
selectedStatus === "delayed"
|
||||
? "border-red-900 bg-red-100 ring-2 ring-red-900"
|
||||
: "border-red-500 bg-white hover:bg-red-50"
|
||||
? "border-destructive bg-destructive/10 ring-2 ring-destructive"
|
||||
: "border-destructive bg-background hover:bg-destructive/10"
|
||||
}`}
|
||||
>
|
||||
<div className="mb-0.5 text-xs text-gray-600">지연</div>
|
||||
<div className="text-lg font-bold text-red-600">{statusStats.delayed}</div>
|
||||
<div className="mb-0.5 text-xs text-foreground">지연</div>
|
||||
<div className="text-lg font-bold text-destructive">{statusStats.delayed}</div>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setSelectedStatus("pickup_waiting")}
|
||||
className={`rounded-lg border-l-4 p-1.5 shadow-sm transition-all ${
|
||||
selectedStatus === "pickup_waiting"
|
||||
? "border-yellow-900 bg-yellow-100 ring-2 ring-yellow-900"
|
||||
: "border-yellow-500 bg-white hover:bg-yellow-50"
|
||||
? "border-warning bg-warning/10 ring-2 ring-warning"
|
||||
: "border-warning bg-background hover:bg-warning/10"
|
||||
}`}
|
||||
>
|
||||
<div className="mb-0.5 text-xs text-gray-600">픽업 대기</div>
|
||||
<div className="text-lg font-bold text-yellow-600">{statusStats.pickup_waiting}</div>
|
||||
<div className="mb-0.5 text-xs text-foreground">픽업 대기</div>
|
||||
<div className="text-lg font-bold text-warning">{statusStats.pickup_waiting}</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 오늘 발송/도착 건수 */}
|
||||
<div className="mb-3">
|
||||
<h4 className="mb-2 text-sm font-semibold text-gray-700">오늘 처리 현황</h4>
|
||||
<h4 className="mb-2 text-sm font-semibold text-foreground">오늘 처리 현황</h4>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="rounded-lg border-l-4 border-gray-500 bg-white p-1.5 shadow-sm">
|
||||
<div className="mb-0.5 text-xs text-gray-600">발송 건수</div>
|
||||
<div className="text-lg font-bold text-gray-900">{todayStats.shipped}</div>
|
||||
<div className="text-xs text-gray-500">건</div>
|
||||
<div className="rounded-lg border-l-4 border-border bg-background p-1.5 shadow-sm">
|
||||
<div className="mb-0.5 text-xs text-foreground">발송 건수</div>
|
||||
<div className="text-lg font-bold text-foreground">{todayStats.shipped}</div>
|
||||
<div className="text-xs text-muted-foreground">건</div>
|
||||
</div>
|
||||
<div className="rounded-lg border-l-4 border-gray-500 bg-white p-1.5 shadow-sm">
|
||||
<div className="mb-0.5 text-xs text-gray-600">도착 건수</div>
|
||||
<div className="text-lg font-bold text-gray-900">{todayStats.delivered}</div>
|
||||
<div className="text-xs text-gray-500">건</div>
|
||||
<div className="rounded-lg border-l-4 border-border bg-background p-1.5 shadow-sm">
|
||||
<div className="mb-0.5 text-xs text-foreground">도착 건수</div>
|
||||
<div className="text-lg font-bold text-foreground">{todayStats.delivered}</div>
|
||||
<div className="text-xs text-muted-foreground">건</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 필터링된 화물 리스트 */}
|
||||
<div className="mb-3">
|
||||
<h4 className="mb-2 flex items-center gap-2 text-sm font-semibold text-gray-700">
|
||||
<Package className="h-4 w-4 text-gray-600" />
|
||||
<h4 className="mb-2 flex items-center gap-2 text-sm font-semibold text-foreground">
|
||||
<Package className="h-4 w-4 text-foreground" />
|
||||
{selectedStatus === "all" && `전체 화물 (${filteredDeliveries.length})`}
|
||||
{selectedStatus === "in_transit" && `배송 중인 화물 (${filteredDeliveries.length})`}
|
||||
{selectedStatus === "delivered" && `배송 완료 (${filteredDeliveries.length})`}
|
||||
{selectedStatus === "delayed" && `지연 중인 화물 (${filteredDeliveries.length})`}
|
||||
{selectedStatus === "pickup_waiting" && `픽업 대기 (${filteredDeliveries.length})`}
|
||||
</h4>
|
||||
<div className="overflow-hidden rounded-lg border border-gray-200 bg-white shadow-sm">
|
||||
<div className="overflow-hidden rounded-lg border border-border bg-background shadow-sm">
|
||||
{filteredDeliveries.length === 0 ? (
|
||||
<div className="p-6 text-center text-sm text-gray-500">
|
||||
<div className="p-6 text-center text-sm text-muted-foreground">
|
||||
{selectedStatus === "all" ? "화물이 없습니다" : "해당 상태의 화물이 없습니다"}
|
||||
</div>
|
||||
) : (
|
||||
|
|
@ -404,12 +404,12 @@ export default function DeliveryStatusWidget({ element, refreshInterval = 60000
|
|||
{filteredDeliveries.map((delivery) => (
|
||||
<div
|
||||
key={delivery.id}
|
||||
className="border-b border-gray-200 p-3 transition-colors last:border-b-0 hover:bg-gray-50"
|
||||
className="border-b border-border p-3 transition-colors last:border-b-0 hover:bg-muted"
|
||||
>
|
||||
<div className="mb-2 flex items-start justify-between">
|
||||
<div>
|
||||
<div className="text-sm font-semibold text-gray-900">{delivery.customer}</div>
|
||||
<div className="text-xs text-gray-600">{delivery.trackingNumber}</div>
|
||||
<div className="text-sm font-semibold text-foreground">{delivery.customer}</div>
|
||||
<div className="text-xs text-foreground">{delivery.trackingNumber}</div>
|
||||
</div>
|
||||
<span
|
||||
className={`rounded-md border px-2 py-1 text-xs font-semibold ${getStatusColor(delivery.status)}`}
|
||||
|
|
@ -417,7 +417,7 @@ export default function DeliveryStatusWidget({ element, refreshInterval = 60000
|
|||
{getStatusText(delivery.status)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-1 text-xs text-gray-600">
|
||||
<div className="space-y-1 text-xs text-foreground">
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="font-medium">경로:</span>
|
||||
<span>
|
||||
|
|
@ -429,7 +429,7 @@ export default function DeliveryStatusWidget({ element, refreshInterval = 60000
|
|||
<span>{delivery.estimatedDelivery}</span>
|
||||
</div>
|
||||
{delivery.delayReason && (
|
||||
<div className="flex items-center gap-1 text-red-600">
|
||||
<div className="flex items-center gap-1 text-destructive">
|
||||
<AlertTriangle className="h-3 w-3" />
|
||||
<span className="font-medium">사유:</span>
|
||||
<span>{delivery.delayReason}</span>
|
||||
|
|
@ -445,27 +445,27 @@ export default function DeliveryStatusWidget({ element, refreshInterval = 60000
|
|||
|
||||
{/* 고객 클레임/이슈 리포트 */}
|
||||
<div>
|
||||
<h4 className="mb-2 flex items-center gap-2 text-sm font-semibold text-gray-700">
|
||||
<XCircle className="h-4 w-4 text-orange-600" />
|
||||
<h4 className="mb-2 flex items-center gap-2 text-sm font-semibold text-foreground">
|
||||
<XCircle className="h-4 w-4 text-warning" />
|
||||
고객 클레임/이슈 ({issues.filter((i) => i.status !== "resolved").length})
|
||||
</h4>
|
||||
<div className="overflow-hidden rounded-lg border border-gray-200 bg-white shadow-sm">
|
||||
<div className="overflow-hidden rounded-lg border border-border bg-background shadow-sm">
|
||||
{issues.length === 0 ? (
|
||||
<div className="p-6 text-center text-sm text-gray-500">이슈가 없습니다</div>
|
||||
<div className="p-6 text-center text-sm text-muted-foreground">이슈가 없습니다</div>
|
||||
) : (
|
||||
<div className="scrollbar-thin scrollbar-thumb-gray-300 scrollbar-track-gray-100 max-h-[200px] overflow-y-auto">
|
||||
{issues.map((issue) => (
|
||||
<div
|
||||
key={issue.id}
|
||||
className="border-b border-gray-200 p-3 transition-colors last:border-b-0 hover:bg-gray-50"
|
||||
className="border-b border-border p-3 transition-colors last:border-b-0 hover:bg-muted"
|
||||
>
|
||||
<div className="mb-2 flex items-start justify-between">
|
||||
<div>
|
||||
<div className="text-sm font-semibold text-gray-900">{issue.customer}</div>
|
||||
<div className="text-xs text-gray-600">{issue.trackingNumber}</div>
|
||||
<div className="text-sm font-semibold text-foreground">{issue.customer}</div>
|
||||
<div className="text-xs text-foreground">{issue.trackingNumber}</div>
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
<span className="rounded-md border border-gray-300 bg-gray-100 px-2 py-1 text-xs font-semibold text-gray-700">
|
||||
<span className="rounded-md border border-border bg-muted px-2 py-1 text-xs font-semibold text-foreground">
|
||||
{getIssueTypeText(issue.issueType)}
|
||||
</span>
|
||||
<span
|
||||
|
|
@ -475,9 +475,9 @@ export default function DeliveryStatusWidget({ element, refreshInterval = 60000
|
|||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1 text-xs text-gray-600">
|
||||
<div className="space-y-1 text-xs text-foreground">
|
||||
<div>{issue.description}</div>
|
||||
<div className="text-gray-500">접수: {issue.reportedAt}</div>
|
||||
<div className="text-muted-foreground">접수: {issue.reportedAt}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ export default function DeliveryTodayStatsWidget({ element }: DeliveryTodayStats
|
|||
<div className="flex h-full items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="border-primary mx-auto h-8 w-8 animate-spin rounded-full border-2 border-t-transparent" />
|
||||
<p className="mt-2 text-sm text-gray-500">데이터 로딩 중...</p>
|
||||
<p className="mt-2 text-sm text-muted-foreground">데이터 로딩 중...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -104,11 +104,11 @@ export default function DeliveryTodayStatsWidget({ element }: DeliveryTodayStats
|
|||
if (error) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<div className="text-center text-red-500">
|
||||
<div className="text-center text-destructive">
|
||||
<p className="text-sm">⚠️ {error}</p>
|
||||
<button
|
||||
onClick={loadData}
|
||||
className="mt-2 rounded bg-red-100 px-3 py-1 text-xs text-red-700 hover:bg-red-200"
|
||||
className="mt-2 rounded bg-destructive/10 px-3 py-1 text-xs text-destructive hover:bg-destructive/20"
|
||||
>
|
||||
다시 시도
|
||||
</button>
|
||||
|
|
@ -120,7 +120,7 @@ export default function DeliveryTodayStatsWidget({ element }: DeliveryTodayStats
|
|||
if (!element?.dataSource?.query) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<div className="text-center text-gray-500">
|
||||
<div className="text-center text-muted-foreground">
|
||||
<p className="text-sm">데이터를 연결하세요</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -128,11 +128,11 @@ export default function DeliveryTodayStatsWidget({ element }: DeliveryTodayStats
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col overflow-hidden bg-white p-4">
|
||||
<div className="flex h-full flex-col overflow-hidden bg-background p-4">
|
||||
{/* 헤더 */}
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<h3 className="text-lg font-semibold text-gray-800">오늘 처리 현황</h3>
|
||||
<button onClick={loadData} className="rounded-full p-1 text-gray-500 hover:bg-gray-100" title="새로고침">
|
||||
<h3 className="text-lg font-semibold text-foreground">오늘 처리 현황</h3>
|
||||
<button onClick={loadData} className="rounded-full p-1 text-muted-foreground hover:bg-muted" title="새로고침">
|
||||
🔄
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -140,19 +140,19 @@ export default function DeliveryTodayStatsWidget({ element }: DeliveryTodayStats
|
|||
{/* 통계 카드 */}
|
||||
<div className="flex flex-1 flex-col gap-4">
|
||||
{/* 오늘 발송 */}
|
||||
<div className="flex flex-1 flex-col items-center justify-center rounded-lg bg-gradient-to-br from-blue-50 to-blue-100 p-6">
|
||||
<div className="flex flex-1 flex-col items-center justify-center rounded-lg bg-gradient-to-br from-primary/10 to-primary/20 p-6">
|
||||
<div className="mb-2 text-4xl">📤</div>
|
||||
<p className="text-sm font-medium text-blue-700">오늘 발송</p>
|
||||
<p className="mt-2 text-4xl font-bold text-blue-800">{todayStats.shipped.toLocaleString()}</p>
|
||||
<p className="mt-1 text-xs text-blue-600">건</p>
|
||||
<p className="text-sm font-medium text-primary">오늘 발송</p>
|
||||
<p className="mt-2 text-4xl font-bold text-primary">{todayStats.shipped.toLocaleString()}</p>
|
||||
<p className="mt-1 text-xs text-primary">건</p>
|
||||
</div>
|
||||
|
||||
{/* 오늘 도착 */}
|
||||
<div className="flex flex-1 flex-col items-center justify-center rounded-lg bg-gradient-to-br from-green-50 to-green-100 p-6">
|
||||
<div className="flex flex-1 flex-col items-center justify-center rounded-lg bg-gradient-to-br from-success/10 to-success/20 p-6">
|
||||
<div className="mb-2 text-4xl">📥</div>
|
||||
<p className="text-sm font-medium text-green-700">오늘 도착</p>
|
||||
<p className="mt-2 text-4xl font-bold text-green-800">{todayStats.delivered.toLocaleString()}</p>
|
||||
<p className="mt-1 text-xs text-green-600">건</p>
|
||||
<p className="text-sm font-medium text-success">오늘 도착</p>
|
||||
<p className="mt-2 text-4xl font-bold text-success">{todayStats.delivered.toLocaleString()}</p>
|
||||
<p className="mt-1 text-xs text-success">건</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -105,13 +105,13 @@ export default function DocumentWidget({ element }: DocumentWidgetProps) {
|
|||
const getCategoryColor = (category: Document["category"]) => {
|
||||
switch (category) {
|
||||
case "계약서":
|
||||
return "bg-blue-100 text-blue-700";
|
||||
return "bg-primary/10 text-primary";
|
||||
case "보험":
|
||||
return "bg-green-100 text-green-700";
|
||||
return "bg-success/10 text-success";
|
||||
case "세금계산서":
|
||||
return "bg-amber-100 text-amber-700";
|
||||
return "bg-warning/10 text-warning";
|
||||
case "기타":
|
||||
return "bg-gray-100 text-gray-700";
|
||||
return "bg-muted text-foreground";
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -128,11 +128,11 @@ export default function DocumentWidget({ element }: DocumentWidgetProps) {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col bg-gradient-to-br from-slate-50 to-blue-50">
|
||||
<div className="flex h-full flex-col bg-background">
|
||||
{/* 헤더 */}
|
||||
<div className="border-b border-gray-200 bg-white px-4 py-3">
|
||||
<div className="border-b border-border bg-background px-4 py-3">
|
||||
<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-foreground">{element?.customTitle || "문서 관리"}</h3>
|
||||
<button className="rounded-lg bg-primary px-3 py-1.5 text-sm text-white transition-colors hover:bg-primary/90">
|
||||
+ 업로드
|
||||
</button>
|
||||
|
|
@ -140,33 +140,33 @@ export default function DocumentWidget({ element }: DocumentWidgetProps) {
|
|||
|
||||
{/* 통계 */}
|
||||
<div className="mb-3 grid grid-cols-4 gap-2 text-xs">
|
||||
<div className="rounded bg-gray-50 px-2 py-1.5 text-center">
|
||||
<div className="font-bold text-gray-700">{stats.total}</div>
|
||||
<div className="text-gray-600">전체</div>
|
||||
<div className="rounded bg-muted px-2 py-1.5 text-center">
|
||||
<div className="font-bold text-foreground">{stats.total}</div>
|
||||
<div className="text-foreground">전체</div>
|
||||
</div>
|
||||
<div className="rounded bg-blue-50 px-2 py-1.5 text-center">
|
||||
<div className="font-bold text-blue-700">{stats.contract}</div>
|
||||
<div className="text-blue-600">계약서</div>
|
||||
<div className="rounded bg-primary/10 px-2 py-1.5 text-center">
|
||||
<div className="font-bold text-primary">{stats.contract}</div>
|
||||
<div className="text-primary">계약서</div>
|
||||
</div>
|
||||
<div className="rounded bg-green-50 px-2 py-1.5 text-center">
|
||||
<div className="font-bold text-green-700">{stats.insurance}</div>
|
||||
<div className="text-green-600">보험</div>
|
||||
<div className="rounded bg-success/10 px-2 py-1.5 text-center">
|
||||
<div className="font-bold text-success">{stats.insurance}</div>
|
||||
<div className="text-success">보험</div>
|
||||
</div>
|
||||
<div className="rounded bg-amber-50 px-2 py-1.5 text-center">
|
||||
<div className="font-bold text-amber-700">{stats.tax}</div>
|
||||
<div className="text-amber-600">계산서</div>
|
||||
<div className="rounded bg-warning/10 px-2 py-1.5 text-center">
|
||||
<div className="font-bold text-warning">{stats.tax}</div>
|
||||
<div className="text-warning">계산서</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 검색 */}
|
||||
<div className="mb-3 relative">
|
||||
<Search className="absolute left-3 top-2.5 h-4 w-4 text-gray-400" />
|
||||
<Search className="absolute left-3 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="문서명 검색..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="w-full rounded border border-gray-300 py-2 pl-10 pr-3 text-sm focus:border-primary focus:outline-none"
|
||||
className="w-full rounded border border-border py-2 pl-10 pr-3 text-sm focus:border-primary focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -177,7 +177,7 @@ export default function DocumentWidget({ element }: DocumentWidgetProps) {
|
|||
key={f}
|
||||
onClick={() => setFilter(f)}
|
||||
className={`rounded px-3 py-1 text-xs font-medium transition-colors ${
|
||||
filter === f ? "bg-primary text-white" : "bg-gray-100 text-gray-600 hover:bg-gray-200"
|
||||
filter === f ? "bg-primary text-white" : "bg-muted text-foreground hover:bg-muted"
|
||||
}`}
|
||||
>
|
||||
{f === "all" ? "전체" : f}
|
||||
|
|
@ -189,7 +189,7 @@ export default function DocumentWidget({ element }: DocumentWidgetProps) {
|
|||
{/* 문서 리스트 */}
|
||||
<div className="flex-1 overflow-y-auto p-4">
|
||||
{filteredDocuments.length === 0 ? (
|
||||
<div className="flex h-full items-center justify-center text-gray-400">
|
||||
<div className="flex h-full items-center justify-center text-muted-foreground">
|
||||
<div className="text-center">
|
||||
<div className="mb-2 text-4xl">📭</div>
|
||||
<div>문서가 없습니다</div>
|
||||
|
|
@ -200,10 +200,10 @@ export default function DocumentWidget({ element }: DocumentWidgetProps) {
|
|||
{filteredDocuments.map((doc) => (
|
||||
<div
|
||||
key={doc.id}
|
||||
className="group flex items-center gap-3 rounded-lg border border-gray-200 bg-white p-3 shadow-sm transition-all hover:border-primary hover:shadow-md"
|
||||
className="group flex items-center gap-3 rounded-lg border border-border bg-background p-3 shadow-sm transition-all hover:border-primary hover:shadow-md"
|
||||
>
|
||||
{/* 아이콘 */}
|
||||
<div className="flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-lg bg-gray-50 text-2xl">
|
||||
<div className="flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-lg bg-muted text-2xl">
|
||||
{getCategoryIcon(doc.category)}
|
||||
</div>
|
||||
|
||||
|
|
@ -211,11 +211,11 @@ export default function DocumentWidget({ element }: DocumentWidgetProps) {
|
|||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="truncate font-medium text-gray-800">{doc.name}</div>
|
||||
<div className="truncate font-medium text-foreground">{doc.name}</div>
|
||||
{doc.description && (
|
||||
<div className="mt-0.5 truncate text-xs text-gray-600">{doc.description}</div>
|
||||
<div className="mt-0.5 truncate text-xs text-foreground">{doc.description}</div>
|
||||
)}
|
||||
<div className="mt-1 flex items-center gap-3 text-xs text-gray-500">
|
||||
<div className="mt-1 flex items-center gap-3 text-xs text-muted-foreground">
|
||||
<span className={`rounded px-2 py-0.5 ${getCategoryColor(doc.category)}`}>
|
||||
{doc.category}
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -122,10 +122,10 @@ export default function ExchangeWidget({
|
|||
// 로딩 상태
|
||||
if (loading && !exchangeRate) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center bg-gradient-to-br from-green-50 to-emerald-50 rounded-lg border p-6">
|
||||
<div className="flex h-full items-center justify-center bg-background rounded-lg border p-6">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<RefreshCw className="h-8 w-8 animate-spin text-green-500" />
|
||||
<p className="text-sm text-gray-600">환율 정보 불러오는 중...</p>
|
||||
<RefreshCw className="h-8 w-8 animate-spin text-success" />
|
||||
<p className="text-sm text-foreground">환율 정보 불러오는 중...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -135,12 +135,12 @@ export default function ExchangeWidget({
|
|||
const hasError = error || !exchangeRate;
|
||||
|
||||
return (
|
||||
<div className="h-full bg-gradient-to-br from-green-50 to-emerald-50 rounded-lg border p-4 @container">
|
||||
<div className="h-full bg-background rounded-lg border p-4 @container">
|
||||
{/* 헤더 */}
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex-1">
|
||||
<h3 className="text-base font-semibold text-gray-900 mb-1">{element?.customTitle || "환율"}</h3>
|
||||
<p className="text-xs text-gray-500">
|
||||
<h3 className="text-base font-semibold text-foreground mb-1">{element?.customTitle || "환율"}</h3>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{lastUpdated
|
||||
? `업데이트: ${lastUpdated.toLocaleTimeString('ko-KR', {
|
||||
hour: '2-digit',
|
||||
|
|
@ -163,7 +163,7 @@ export default function ExchangeWidget({
|
|||
{/* 통화 선택 - 반응형 (좁을 때 세로 배치) */}
|
||||
<div className="flex @[300px]:flex-row flex-col items-center gap-2 mb-3">
|
||||
<Select value={base} onValueChange={setBase}>
|
||||
<SelectTrigger className="w-full @[300px]:flex-1 bg-white h-8 text-xs">
|
||||
<SelectTrigger className="w-full @[300px]:flex-1 bg-background h-8 text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
|
@ -179,13 +179,13 @@ export default function ExchangeWidget({
|
|||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleSwap}
|
||||
className="h-8 w-8 p-0 rounded-full hover:bg-white @[300px]:rotate-0 rotate-90"
|
||||
className="h-8 w-8 p-0 rounded-full hover:bg-background @[300px]:rotate-0 rotate-90"
|
||||
>
|
||||
<ArrowRightLeft className="h-3 w-3" />
|
||||
</Button>
|
||||
|
||||
<Select value={target} onValueChange={setTarget}>
|
||||
<SelectTrigger className="w-full @[300px]:flex-1 bg-white h-8 text-xs">
|
||||
<SelectTrigger className="w-full @[300px]:flex-1 bg-background h-8 text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
|
@ -200,11 +200,11 @@ export default function ExchangeWidget({
|
|||
|
||||
{/* 에러 메시지 */}
|
||||
{hasError && (
|
||||
<div className="mb-3 p-3 bg-red-50 border border-red-200 rounded-lg">
|
||||
<p className="text-xs text-red-600 text-center">{error || '환율 정보를 불러올 수 없습니다.'}</p>
|
||||
<div className="mb-3 p-3 bg-destructive/10 border border-destructive rounded-lg">
|
||||
<p className="text-xs text-destructive text-center">{error || '환율 정보를 불러올 수 없습니다.'}</p>
|
||||
<button
|
||||
onClick={fetchExchangeRate}
|
||||
className="mt-2 w-full text-xs text-red-600 hover:text-red-700 underline"
|
||||
className="mt-2 w-full text-xs text-destructive hover:text-destructive underline"
|
||||
>
|
||||
다시 시도
|
||||
</button>
|
||||
|
|
@ -213,12 +213,12 @@ export default function ExchangeWidget({
|
|||
|
||||
{/* 환율 표시 */}
|
||||
{!hasError && (
|
||||
<div className="mb-2 bg-white rounded-lg border p-2">
|
||||
<div className="mb-2 bg-background rounded-lg border p-2">
|
||||
<div className="text-center">
|
||||
<div className="text-xs text-gray-400 mb-0.5">
|
||||
<div className="text-xs text-muted-foreground mb-0.5">
|
||||
{exchangeRate.base === 'KRW' ? '1,000' : '1'} {getCurrencySymbol(exchangeRate.base)} =
|
||||
</div>
|
||||
<div className="text-lg font-bold text-gray-900">
|
||||
<div className="text-lg font-bold text-foreground">
|
||||
{exchangeRate.base === 'KRW'
|
||||
? (exchangeRate.rate * 1000).toLocaleString('ko-KR', {
|
||||
minimumFractionDigits: 2,
|
||||
|
|
@ -229,13 +229,13 @@ export default function ExchangeWidget({
|
|||
maximumFractionDigits: 4,
|
||||
})}
|
||||
</div>
|
||||
<div className="text-xs text-gray-400 mt-0.5">{getCurrencySymbol(exchangeRate.target)}</div>
|
||||
<div className="text-xs text-muted-foreground mt-0.5">{getCurrencySymbol(exchangeRate.target)}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 계산기 입력 */}
|
||||
<div className="bg-white rounded-lg border p-2">
|
||||
<div className="bg-background rounded-lg border p-2">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
|
|
@ -247,30 +247,30 @@ export default function ExchangeWidget({
|
|||
autoComplete="off"
|
||||
className="flex-1 text-center text-sm font-semibold"
|
||||
/>
|
||||
<span className="text-xs text-gray-400 w-12">{base}</span>
|
||||
<span className="text-xs text-muted-foreground w-12">{base}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<div className="h-px flex-1 bg-gradient-to-r from-transparent via-gray-300 to-transparent" />
|
||||
<span className="text-xs text-gray-400">▼</span>
|
||||
<div className="h-px flex-1 bg-gradient-to-r from-transparent via-gray-300 to-transparent" />
|
||||
<div className="h-px flex-1 bg-gradient-to-r from-transparent via-border to-transparent" />
|
||||
<span className="text-xs text-muted-foreground">▼</span>
|
||||
<div className="h-px flex-1 bg-gradient-to-r from-transparent via-border to-transparent" />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex-1 text-center text-lg font-bold text-green-600 bg-green-50 border border-green-200 rounded px-2 py-1.5">
|
||||
<div className="flex-1 text-center text-lg font-bold text-success bg-success/10 border border-success rounded px-2 py-1.5">
|
||||
{calculateResult().toLocaleString('ko-KR', {
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 2,
|
||||
})}
|
||||
</div>
|
||||
<span className="text-xs text-gray-400 w-12">{target}</span>
|
||||
<span className="text-xs text-muted-foreground w-12">{target}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 데이터 출처 */}
|
||||
<div className="mt-3 pt-2 border-t text-center">
|
||||
<p className="text-xs text-gray-400">출처: {exchangeRate.source}</p>
|
||||
<p className="text-xs text-muted-foreground">출처: {exchangeRate.source}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -203,7 +203,7 @@ export default function ListSummaryWidget({ element }: ListSummaryWidgetProps) {
|
|||
<div className="flex h-full items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="border-primary mx-auto h-8 w-8 animate-spin rounded-full border-2 border-t-transparent" />
|
||||
<p className="mt-2 text-sm text-gray-500">데이터 로딩 중...</p>
|
||||
<p className="mt-2 text-sm text-muted-foreground">데이터 로딩 중...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -212,11 +212,11 @@ export default function ListSummaryWidget({ element }: ListSummaryWidgetProps) {
|
|||
if (error) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<div className="text-center text-red-500">
|
||||
<div className="text-center text-destructive">
|
||||
<p className="text-sm">⚠️ {error}</p>
|
||||
<button
|
||||
onClick={loadData}
|
||||
className="mt-2 rounded bg-red-100 px-3 py-1 text-xs text-red-700 hover:bg-red-200"
|
||||
className="mt-2 rounded bg-destructive/10 px-3 py-1 text-xs text-destructive hover:bg-destructive/20"
|
||||
>
|
||||
다시 시도
|
||||
</button>
|
||||
|
|
@ -230,8 +230,8 @@ export default function ListSummaryWidget({ element }: ListSummaryWidgetProps) {
|
|||
<div className="flex h-full items-center justify-center p-3">
|
||||
<div className="max-w-xs space-y-2 text-center">
|
||||
<div className="text-3xl">📋</div>
|
||||
<h3 className="text-sm font-bold text-gray-900">데이터 목록</h3>
|
||||
<div className="space-y-1.5 text-xs text-gray-600">
|
||||
<h3 className="text-sm font-bold text-foreground">데이터 목록</h3>
|
||||
<div className="space-y-1.5 text-xs text-foreground">
|
||||
<p className="font-medium">📋 테이블 형식 데이터 표시 위젯</p>
|
||||
<ul className="space-y-0.5 text-left">
|
||||
<li>• SQL 쿼리로 데이터를 불러옵니다</li>
|
||||
|
|
@ -240,7 +240,7 @@ export default function ListSummaryWidget({ element }: ListSummaryWidgetProps) {
|
|||
<li>• 실시간 데이터 모니터링 가능</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="mt-2 rounded-lg bg-blue-50 p-2 text-[10px] text-blue-700">
|
||||
<div className="mt-2 rounded-lg bg-primary/10 p-2 text-[10px] text-primary">
|
||||
<p>SQL 쿼리를 입력하고 저장하세요</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -249,16 +249,16 @@ export default function ListSummaryWidget({ element }: ListSummaryWidgetProps) {
|
|||
}
|
||||
|
||||
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-background to-primary/10 p-2">
|
||||
{/* 헤더 */}
|
||||
<div className="mb-2 flex flex-shrink-0 items-center justify-between">
|
||||
<div className="flex-1">
|
||||
<h3 className="text-sm font-bold text-gray-900">📋 {displayTitle}</h3>
|
||||
<p className="text-xs text-gray-500">총 {filteredData.length.toLocaleString()}건</p>
|
||||
<h3 className="text-sm font-bold text-foreground">📋 {displayTitle}</h3>
|
||||
<p className="text-xs text-muted-foreground">총 {filteredData.length.toLocaleString()}건</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={loadData}
|
||||
className="border-border hover:bg-accent flex h-7 w-7 items-center justify-center rounded border bg-white p-0 text-xs disabled:opacity-50"
|
||||
className="border-border hover:bg-accent flex h-7 w-7 items-center justify-center rounded border bg-background p-0 text-xs disabled:opacity-50"
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? "⏳" : "🔄"}
|
||||
|
|
@ -273,7 +273,7 @@ export default function ListSummaryWidget({ element }: ListSummaryWidgetProps) {
|
|||
placeholder="검색..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="focus:border-primary focus:ring-primary w-full rounded border border-gray-300 px-2 py-1 text-xs focus:ring-1 focus:outline-none"
|
||||
className="focus:border-primary focus:ring-primary w-full rounded border border-border px-2 py-1 text-xs focus:ring-1 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -282,20 +282,20 @@ export default function ListSummaryWidget({ element }: ListSummaryWidgetProps) {
|
|||
<div className="flex-1 overflow-auto">
|
||||
{filteredData.length > 0 ? (
|
||||
<table className="w-full border-collapse text-xs">
|
||||
<thead className="sticky top-0 bg-gray-100">
|
||||
<thead className="sticky top-0 bg-muted">
|
||||
<tr>
|
||||
{columns.map((col) => (
|
||||
<th key={col.key} className="border border-gray-300 px-2 py-1 text-left font-semibold text-gray-700">
|
||||
<th key={col.key} className="border border-border px-2 py-1 text-left font-semibold text-foreground">
|
||||
{col.label}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white">
|
||||
<tbody className="bg-background">
|
||||
{filteredData.map((row, idx) => (
|
||||
<tr key={idx} className="hover:bg-gray-50">
|
||||
<tr key={idx} className="hover:bg-muted">
|
||||
{columns.map((col) => (
|
||||
<td key={col.key} className="border border-gray-300 px-2 py-1 text-gray-800">
|
||||
<td key={col.key} className="border border-border px-2 py-1 text-foreground">
|
||||
{String(row[col.key] || "")}
|
||||
</td>
|
||||
))}
|
||||
|
|
@ -304,7 +304,7 @@ export default function ListSummaryWidget({ element }: ListSummaryWidgetProps) {
|
|||
</tbody>
|
||||
</table>
|
||||
) : (
|
||||
<div className="flex h-full items-center justify-center text-gray-500">
|
||||
<div className="flex h-full items-center justify-center text-muted-foreground">
|
||||
<p className="text-sm">검색 결과가 없습니다</p>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -70,13 +70,13 @@ export default function MaintenanceWidget() {
|
|||
const getStatusBadge = (status: MaintenanceSchedule["status"]) => {
|
||||
switch (status) {
|
||||
case "scheduled":
|
||||
return <span className="rounded bg-blue-100 px-2 py-1 text-xs font-medium text-blue-700">예정</span>;
|
||||
return <span className="rounded bg-primary/10 px-2 py-1 text-xs font-medium text-primary">예정</span>;
|
||||
case "in_progress":
|
||||
return <span className="rounded bg-amber-100 px-2 py-1 text-xs font-medium text-amber-700">진행중</span>;
|
||||
return <span className="rounded bg-warning/10 px-2 py-1 text-xs font-medium text-warning">진행중</span>;
|
||||
case "completed":
|
||||
return <span className="rounded bg-green-100 px-2 py-1 text-xs font-medium text-green-700">완료</span>;
|
||||
return <span className="rounded bg-success/10 px-2 py-1 text-xs font-medium text-success">완료</span>;
|
||||
case "overdue":
|
||||
return <span className="rounded bg-red-100 px-2 py-1 text-xs font-medium text-red-700">지연</span>;
|
||||
return <span className="rounded bg-destructive/10 px-2 py-1 text-xs font-medium text-destructive">지연</span>;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -115,11 +115,11 @@ export default function MaintenanceWidget() {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col bg-gradient-to-br from-slate-50 to-teal-50">
|
||||
<div className="flex h-full flex-col bg-gradient-to-br from-background to-primary/10">
|
||||
{/* 헤더 */}
|
||||
<div className="border-b border-gray-200 bg-white px-4 py-3">
|
||||
<div className="border-b border-border bg-background px-4 py-3">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<h3 className="text-lg font-bold text-gray-800">🔧 정비 일정 관리</h3>
|
||||
<h3 className="text-lg font-bold text-foreground">🔧 정비 일정 관리</h3>
|
||||
<button className="rounded-lg bg-primary px-3 py-1.5 text-sm text-white transition-colors hover:bg-primary/90">
|
||||
+ 일정 추가
|
||||
</button>
|
||||
|
|
@ -127,21 +127,21 @@ export default function MaintenanceWidget() {
|
|||
|
||||
{/* 통계 */}
|
||||
<div className="mb-3 grid grid-cols-4 gap-2 text-xs">
|
||||
<div className="rounded bg-blue-50 px-2 py-1.5 text-center">
|
||||
<div className="font-bold text-blue-700">{stats.scheduled}</div>
|
||||
<div className="text-blue-600">예정</div>
|
||||
<div className="rounded bg-primary/10 px-2 py-1.5 text-center">
|
||||
<div className="font-bold text-primary">{stats.scheduled}</div>
|
||||
<div className="text-primary">예정</div>
|
||||
</div>
|
||||
<div className="rounded bg-amber-50 px-2 py-1.5 text-center">
|
||||
<div className="font-bold text-amber-700">{stats.inProgress}</div>
|
||||
<div className="text-amber-600">진행중</div>
|
||||
<div className="rounded bg-warning/10 px-2 py-1.5 text-center">
|
||||
<div className="font-bold text-warning">{stats.inProgress}</div>
|
||||
<div className="text-warning">진행중</div>
|
||||
</div>
|
||||
<div className="rounded bg-red-50 px-2 py-1.5 text-center">
|
||||
<div className="font-bold text-red-700">{stats.overdue}</div>
|
||||
<div className="text-red-600">지연</div>
|
||||
<div className="rounded bg-destructive/10 px-2 py-1.5 text-center">
|
||||
<div className="font-bold text-destructive">{stats.overdue}</div>
|
||||
<div className="text-destructive">지연</div>
|
||||
</div>
|
||||
<div className="rounded bg-gray-50 px-2 py-1.5 text-center">
|
||||
<div className="font-bold text-gray-700">{stats.total}</div>
|
||||
<div className="text-gray-600">전체</div>
|
||||
<div className="rounded bg-muted px-2 py-1.5 text-center">
|
||||
<div className="font-bold text-foreground">{stats.total}</div>
|
||||
<div className="text-foreground">전체</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -152,7 +152,7 @@ export default function MaintenanceWidget() {
|
|||
key={f}
|
||||
onClick={() => setFilter(f)}
|
||||
className={`rounded px-3 py-1 text-xs font-medium transition-colors ${
|
||||
filter === f ? "bg-primary text-white" : "bg-gray-100 text-gray-600 hover:bg-gray-200"
|
||||
filter === f ? "bg-primary text-white" : "bg-muted text-foreground hover:bg-muted"
|
||||
}`}
|
||||
>
|
||||
{f === "all" ? "전체" : f === "scheduled" ? "예정" : f === "in_progress" ? "진행중" : "지연"}
|
||||
|
|
@ -164,7 +164,7 @@ export default function MaintenanceWidget() {
|
|||
{/* 일정 리스트 */}
|
||||
<div className="flex-1 overflow-y-auto p-4">
|
||||
{filteredSchedules.length === 0 ? (
|
||||
<div className="flex h-full items-center justify-center text-gray-400">
|
||||
<div className="flex h-full items-center justify-center text-muted-foreground">
|
||||
<div className="text-center">
|
||||
<div className="mb-2 text-4xl">📅</div>
|
||||
<div>정비 일정이 없습니다</div>
|
||||
|
|
@ -175,34 +175,34 @@ export default function MaintenanceWidget() {
|
|||
{filteredSchedules.map((schedule) => (
|
||||
<div
|
||||
key={schedule.id}
|
||||
className={`group rounded-lg border-2 bg-white p-4 shadow-sm transition-all hover:shadow-md ${
|
||||
schedule.status === "overdue" ? "border-red-300" : "border-gray-200"
|
||||
className={`group rounded-lg border-2 bg-background p-4 shadow-sm transition-all hover:shadow-md ${
|
||||
schedule.status === "overdue" ? "border-destructive" : "border-border"
|
||||
}`}
|
||||
>
|
||||
<div className="mb-2 flex items-start justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-2xl">{getMaintenanceIcon(schedule.maintenanceType)}</span>
|
||||
<div>
|
||||
<div className="font-bold text-gray-800">{schedule.vehicleNumber}</div>
|
||||
<div className="text-xs text-gray-600">{schedule.vehicleType}</div>
|
||||
<div className="font-bold text-foreground">{schedule.vehicleNumber}</div>
|
||||
<div className="text-xs text-foreground">{schedule.vehicleType}</div>
|
||||
</div>
|
||||
</div>
|
||||
{getStatusBadge(schedule.status)}
|
||||
</div>
|
||||
|
||||
<div className="mb-3 rounded bg-gray-50 p-2">
|
||||
<div className="text-sm font-medium text-gray-700">{schedule.maintenanceType}</div>
|
||||
{schedule.notes && <div className="mt-1 text-xs text-gray-600">{schedule.notes}</div>}
|
||||
<div className="mb-3 rounded bg-muted p-2">
|
||||
<div className="text-sm font-medium text-foreground">{schedule.maintenanceType}</div>
|
||||
{schedule.notes && <div className="mt-1 text-xs text-foreground">{schedule.notes}</div>}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2 text-xs">
|
||||
<div className="flex items-center gap-1 text-gray-600">
|
||||
<div className="flex items-center gap-1 text-foreground">
|
||||
<Calendar className="h-3 w-3" />
|
||||
{new Date(schedule.scheduledDate).toLocaleDateString()}
|
||||
</div>
|
||||
<div
|
||||
className={`flex items-center gap-1 font-medium ${
|
||||
schedule.status === "overdue" ? "text-red-600" : "text-blue-600"
|
||||
schedule.status === "overdue" ? "text-destructive" : "text-primary"
|
||||
}`}
|
||||
>
|
||||
<Clock className="h-3 w-3" />
|
||||
|
|
@ -218,17 +218,17 @@ export default function MaintenanceWidget() {
|
|||
{/* 액션 버튼 */}
|
||||
{schedule.status === "scheduled" && (
|
||||
<div className="mt-3 flex gap-2">
|
||||
<button className="flex-1 rounded bg-blue-500 px-3 py-1.5 text-xs font-medium text-white hover:bg-blue-600">
|
||||
<button className="flex-1 rounded bg-primary px-3 py-1.5 text-xs font-medium text-white hover:bg-primary/90">
|
||||
시작
|
||||
</button>
|
||||
<button className="flex-1 rounded bg-gray-200 px-3 py-1.5 text-xs font-medium text-gray-700 hover:bg-gray-300">
|
||||
<button className="flex-1 rounded bg-muted px-3 py-1.5 text-xs font-medium text-foreground hover:bg-muted/90">
|
||||
일정 변경
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{schedule.status === "in_progress" && (
|
||||
<div className="mt-3">
|
||||
<button className="w-full rounded bg-green-500 px-3 py-1.5 text-xs font-medium text-white hover:bg-green-600">
|
||||
<button className="w-full rounded bg-success px-3 py-1.5 text-xs font-medium text-white hover:bg-success/90">
|
||||
<Check className="mr-1 inline h-3 w-3" />
|
||||
완료 처리
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -155,20 +155,20 @@ export default function MapSummaryWidget({ element }: MapSummaryWidgetProps) {
|
|||
const displayTitle = element.customTitle || (tableName ? `${translateTableName(tableName)} 위치` : "위치 지도");
|
||||
|
||||
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-background to-primary/10 p-2">
|
||||
{/* 헤더 */}
|
||||
<div className="mb-2 flex flex-shrink-0 items-center justify-between">
|
||||
<div className="flex-1">
|
||||
<h3 className="text-sm font-bold text-gray-900">{displayTitle}</h3>
|
||||
<h3 className="text-sm font-bold text-foreground">{displayTitle}</h3>
|
||||
{element?.dataSource?.query ? (
|
||||
<p className="text-xs text-gray-500">총 {markers.length.toLocaleString()}개 마커</p>
|
||||
<p className="text-xs text-muted-foreground">총 {markers.length.toLocaleString()}개 마커</p>
|
||||
) : (
|
||||
<p className="text-xs text-orange-500">⚙️ 톱니바퀴 버튼을 눌러 데이터를 연결하세요</p>
|
||||
<p className="text-xs text-warning">⚙️ 톱니바퀴 버튼을 눌러 데이터를 연결하세요</p>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={loadMapData}
|
||||
className="flex h-7 w-7 items-center justify-center rounded border border-border bg-white p-0 text-xs hover:bg-accent disabled:opacity-50"
|
||||
className="flex h-7 w-7 items-center justify-center rounded border border-border bg-background p-0 text-xs hover:bg-accent disabled:opacity-50"
|
||||
disabled={loading || !element?.dataSource?.query}
|
||||
>
|
||||
{loading ? "⏳" : "🔄"}
|
||||
|
|
@ -177,13 +177,13 @@ export default function MapSummaryWidget({ element }: MapSummaryWidgetProps) {
|
|||
|
||||
{/* 에러 메시지 (지도 위에 오버레이) */}
|
||||
{error && (
|
||||
<div className="mb-2 rounded border border-red-300 bg-red-50 p-2 text-center text-xs text-red-600">
|
||||
<div className="mb-2 rounded border border-destructive bg-destructive/10 p-2 text-center text-xs text-destructive">
|
||||
⚠️ {error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 지도 (항상 표시) */}
|
||||
<div className="relative flex-1 rounded border border-gray-300 bg-white overflow-hidden z-0">
|
||||
<div className="relative flex-1 rounded border border-border bg-background overflow-hidden z-0">
|
||||
<MapContainer
|
||||
key={`map-${element.id}`}
|
||||
center={[36.5, 127.5]}
|
||||
|
|
|
|||
|
|
@ -247,15 +247,15 @@ const findNearestCity = (lat: number, lng: number): string => {
|
|||
const getWeatherIcon = (weatherMain: string) => {
|
||||
switch (weatherMain.toLowerCase()) {
|
||||
case "clear":
|
||||
return <Sun className="h-4 w-4 text-yellow-500" />;
|
||||
return <Sun className="h-4 w-4 text-warning" />;
|
||||
case "rain":
|
||||
return <CloudRain className="h-4 w-4 text-blue-500" />;
|
||||
return <CloudRain className="h-4 w-4 text-primary" />;
|
||||
case "snow":
|
||||
return <CloudSnow className="h-4 w-4 text-blue-300" />;
|
||||
return <CloudSnow className="h-4 w-4 text-primary/70" />;
|
||||
case "clouds":
|
||||
return <Cloud className="h-4 w-4 text-gray-400" />;
|
||||
return <Cloud className="h-4 w-4 text-muted-foreground" />;
|
||||
default:
|
||||
return <Wind className="h-4 w-4 text-gray-500" />;
|
||||
return <Wind className="h-4 w-4 text-muted-foreground" />;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -867,22 +867,22 @@ function MapTestWidget({ element }: MapTestWidgetProps) {
|
|||
const dataSource = element?.dataSource;
|
||||
|
||||
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-background to-primary/10 p-2">
|
||||
{/* 헤더 */}
|
||||
<div className="mb-2 flex flex-shrink-0 items-center justify-between">
|
||||
<div className="flex-1">
|
||||
<h3 className="text-sm font-bold text-gray-900">{displayTitle}</h3>
|
||||
<h3 className="text-sm font-bold text-foreground">{displayTitle}</h3>
|
||||
{dataSource ? (
|
||||
<p className="text-xs text-gray-500">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{dataSource.type === "api" ? "🌐 REST API" : "💾 Database"} · 총 {markers.length.toLocaleString()}개 마커
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-xs text-orange-500">데이터를 연결하세요</p>
|
||||
<p className="text-xs text-warning">데이터를 연결하세요</p>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={loadMapData}
|
||||
className="border-border hover:bg-accent flex h-7 w-7 items-center justify-center rounded border bg-white p-0 text-xs disabled:opacity-50"
|
||||
className="border-border hover:bg-accent flex h-7 w-7 items-center justify-center rounded border bg-background p-0 text-xs disabled:opacity-50"
|
||||
disabled={loading || !element?.dataSource}
|
||||
>
|
||||
{loading ? "⏳" : "🔄"}
|
||||
|
|
@ -891,19 +891,19 @@ function MapTestWidget({ element }: MapTestWidgetProps) {
|
|||
|
||||
{/* 에러 메시지 (지도 위에 오버레이) */}
|
||||
{error && (
|
||||
<div className="mb-2 rounded border border-red-300 bg-red-50 p-2 text-center text-xs text-red-600">
|
||||
<div className="mb-2 rounded border border-destructive bg-destructive/10 p-2 text-center text-xs text-destructive">
|
||||
⚠️ {error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 지도 또는 빈 상태 */}
|
||||
<div className="relative z-0 flex-1 overflow-hidden rounded border border-gray-300 bg-white">
|
||||
<div className="relative z-0 flex-1 overflow-hidden rounded border border-border bg-background">
|
||||
{!element?.chartConfig?.tileMapUrl && !element?.dataSource ? (
|
||||
// 타일맵 URL도 없고 데이터 소스도 없을 때: 빈 상태 표시
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<div className="text-center">
|
||||
<p className="text-sm font-medium text-gray-600">🗺️ 지도를 설정하세요</p>
|
||||
<p className="mt-2 text-xs text-gray-500">
|
||||
<p className="text-sm font-medium text-foreground">🗺️ 지도를 설정하세요</p>
|
||||
<p className="mt-2 text-xs text-muted-foreground">
|
||||
차트 설정에서 타일맵 URL을 입력하거나
|
||||
<br />
|
||||
데이터 소스에서 마커 데이터를 연결하세요
|
||||
|
|
@ -979,10 +979,10 @@ function MapTestWidget({ element }: MapTestWidgetProps) {
|
|||
<div style="font-weight: 600; font-size: 12px; color: ${getAlertColor(alert.severity)};">
|
||||
${alert.title}
|
||||
</div>
|
||||
<div style="font-size: 11px; color: #6b7280; margin-top: 4px;">
|
||||
<div style="font-size: 11px; color: hsl(var(--muted-foreground)); margin-top: 4px;">
|
||||
${alert.description}
|
||||
</div>
|
||||
<div style="font-size: 10px; color: #9ca3af; margin-top: 4px;">
|
||||
<div style="font-size: 10px; color: hsl(var(--muted-foreground) / 0.7); margin-top: 4px;">
|
||||
${new Date(alert.timestamp).toLocaleString("ko-KR")}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1064,10 +1064,10 @@ function MapTestWidget({ element }: MapTestWidgetProps) {
|
|||
}}
|
||||
>
|
||||
<div style={{ fontWeight: "600", fontSize: "11px", color: alertColor }}>{alert.title}</div>
|
||||
<div style={{ fontSize: "10px", color: "#6b7280", marginTop: "3px" }}>
|
||||
<div className="text-[10px] text-muted-foreground mt-[3px]">
|
||||
{alert.description}
|
||||
</div>
|
||||
<div style={{ fontSize: "9px", color: "#9ca3af", marginTop: "3px" }}>
|
||||
<div className="text-[9px] text-muted-foreground/70 mt-[3px]">
|
||||
{new Date(alert.timestamp).toLocaleString("ko-KR")}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1113,7 +1113,7 @@ function MapTestWidget({ element }: MapTestWidgetProps) {
|
|||
<div className="mb-2 border-b pb-2">
|
||||
<div className="mb-1 text-sm font-bold">{marker.name}</div>
|
||||
{marker.description && (
|
||||
<div className="text-xs text-gray-600 whitespace-pre-line">{marker.description}</div>
|
||||
<div className="text-xs text-foreground whitespace-pre-line">{marker.description}</div>
|
||||
)}
|
||||
{marker.info && Object.entries(marker.info)
|
||||
.filter(([key]) => !["latitude", "longitude", "lat", "lng"].includes(key.toLowerCase()))
|
||||
|
|
@ -1131,22 +1131,22 @@ function MapTestWidget({ element }: MapTestWidgetProps) {
|
|||
{getWeatherIcon(marker.weather.weatherMain)}
|
||||
<span className="text-xs font-semibold">현재 날씨</span>
|
||||
</div>
|
||||
<div className="text-xs text-gray-600">{marker.weather.weatherDescription}</div>
|
||||
<div className="text-xs text-foreground">{marker.weather.weatherDescription}</div>
|
||||
<div className="mt-2 space-y-1 text-xs">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-500">온도</span>
|
||||
<span className="text-muted-foreground">온도</span>
|
||||
<span className="font-medium">{marker.weather.temperature}°C</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-500">체감온도</span>
|
||||
<span className="text-muted-foreground">체감온도</span>
|
||||
<span className="font-medium">{marker.weather.feelsLike}°C</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-500">습도</span>
|
||||
<span className="text-muted-foreground">습도</span>
|
||||
<span className="font-medium">{marker.weather.humidity}%</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-500">풍속</span>
|
||||
<span className="text-muted-foreground">풍속</span>
|
||||
<span className="font-medium">{marker.weather.windSpeed} m/s</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1162,7 +1162,7 @@ function MapTestWidget({ element }: MapTestWidgetProps) {
|
|||
|
||||
{/* 범례 (특보가 있을 때만 표시) */}
|
||||
{weatherAlerts && weatherAlerts.length > 0 && (
|
||||
<div className="absolute right-4 bottom-4 z-10 rounded-lg border bg-white p-3 shadow-lg">
|
||||
<div className="absolute right-4 bottom-4 z-10 rounded-lg border bg-background p-3 shadow-lg">
|
||||
<div className="mb-2 flex items-center gap-1 text-xs font-semibold">
|
||||
<AlertTriangle className="h-3 w-3" />
|
||||
기상특보
|
||||
|
|
@ -1181,7 +1181,7 @@ function MapTestWidget({ element }: MapTestWidgetProps) {
|
|||
<span>약한 주의보</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 border-t pt-2 text-[10px] text-gray-500">총 {weatherAlerts.length}건 발효 중</div>
|
||||
<div className="mt-2 border-t pt-2 text-[10px] text-muted-foreground">총 {weatherAlerts.length}건 발효 중</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -490,9 +490,9 @@ export default function RiskAlertTestWidget({ element }: RiskAlertTestWidgetProp
|
|||
|
||||
const getSeverityColor = (severity: "high" | "medium" | "low") => {
|
||||
switch (severity) {
|
||||
case "high": return "bg-red-500";
|
||||
case "medium": return "bg-yellow-500";
|
||||
case "low": return "bg-blue-500";
|
||||
case "high": return "bg-destructive";
|
||||
case "medium": return "bg-warning/100";
|
||||
case "low": return "bg-primary";
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -503,7 +503,7 @@ export default function RiskAlertTestWidget({ element }: RiskAlertTestWidgetProp
|
|||
<div className="flex h-full items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="border-primary mx-auto h-8 w-8 animate-spin rounded-full border-2 border-t-transparent" />
|
||||
<p className="mt-2 text-sm text-gray-500">데이터 로딩 중...</p>
|
||||
<p className="mt-2 text-sm text-muted-foreground">데이터 로딩 중...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -512,11 +512,11 @@ export default function RiskAlertTestWidget({ element }: RiskAlertTestWidgetProp
|
|||
if (error) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<div className="text-center text-red-500">
|
||||
<div className="text-center text-destructive">
|
||||
<p className="text-sm">⚠️ {error}</p>
|
||||
<button
|
||||
onClick={loadMultipleDataSources}
|
||||
className="mt-2 rounded bg-red-100 px-3 py-1 text-xs text-red-700 hover:bg-red-200"
|
||||
className="mt-2 rounded bg-destructive/10 px-3 py-1 text-xs text-destructive hover:bg-destructive/20"
|
||||
>
|
||||
다시 시도
|
||||
</button>
|
||||
|
|
@ -530,8 +530,8 @@ export default function RiskAlertTestWidget({ element }: RiskAlertTestWidgetProp
|
|||
<div className="flex h-full items-center justify-center p-3">
|
||||
<div className="max-w-xs space-y-2 text-center">
|
||||
<div className="text-3xl">🚨</div>
|
||||
<h3 className="text-sm font-bold text-gray-900">리스크/알림</h3>
|
||||
<div className="space-y-1.5 text-xs text-gray-600">
|
||||
<h3 className="text-sm font-bold text-foreground">리스크/알림</h3>
|
||||
<div className="space-y-1.5 text-xs text-foreground">
|
||||
<p className="font-medium">다중 데이터 소스 지원</p>
|
||||
<ul className="space-y-0.5 text-left">
|
||||
<li>• 여러 REST API 동시 연결</li>
|
||||
|
|
@ -540,7 +540,7 @@ export default function RiskAlertTestWidget({ element }: RiskAlertTestWidgetProp
|
|||
<li>• 알림 타입별 필터링</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="mt-2 rounded-lg bg-blue-50 p-2 text-[10px] text-blue-700">
|
||||
<div className="mt-2 rounded-lg bg-primary/10 p-2 text-[10px] text-primary">
|
||||
<p className="font-medium">⚙️ 설정 방법</p>
|
||||
<p>데이터 소스를 추가하고 저장하세요</p>
|
||||
</div>
|
||||
|
|
@ -550,9 +550,9 @@ export default function RiskAlertTestWidget({ element }: RiskAlertTestWidgetProp
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col overflow-hidden bg-gradient-to-br from-red-50 to-orange-50">
|
||||
<div className="flex h-full w-full flex-col overflow-hidden bg-background">
|
||||
{/* 헤더 */}
|
||||
<div className="flex items-center justify-between border-b bg-white/80 p-3">
|
||||
<div className="flex items-center justify-between border-b bg-background/80 p-3">
|
||||
<div>
|
||||
<h3 className="text-base font-semibold">
|
||||
{element?.customTitle || "리스크/알림"}
|
||||
|
|
@ -610,14 +610,14 @@ export default function RiskAlertTestWidget({ element }: RiskAlertTestWidgetProp
|
|||
|
||||
<div className="flex-1 space-y-1.5 overflow-y-auto pr-1">
|
||||
{filteredAlerts.length === 0 ? (
|
||||
<div className="flex h-full items-center justify-center text-gray-500">
|
||||
<div className="flex h-full items-center justify-center text-muted-foreground">
|
||||
<p className="text-sm">알림이 없습니다</p>
|
||||
</div>
|
||||
) : (
|
||||
filteredAlerts.map((alert) => (
|
||||
<Card key={alert.id} className="border-l-4 p-2" style={{ borderLeftColor: alert.severity === "high" ? "#ef4444" : alert.severity === "medium" ? "#f59e0b" : "#3b82f6" }}>
|
||||
<Card key={alert.id} className="p-2">
|
||||
<div className="flex items-start gap-2">
|
||||
<div className={`mt-0.5 rounded-full p-1 ${alert.severity === "high" ? "bg-red-100 text-red-600" : alert.severity === "medium" ? "bg-yellow-100 text-yellow-600" : "bg-blue-100 text-blue-600"}`}>
|
||||
<div className={`mt-0.5 rounded-full p-1 ${alert.severity === "high" ? "bg-destructive/10 text-destructive" : alert.severity === "medium" ? "bg-warning/10 text-warning" : "bg-primary/10 text-primary"}`}>
|
||||
{getTypeIcon(alert.type)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
|
|
@ -630,10 +630,10 @@ export default function RiskAlertTestWidget({ element }: RiskAlertTestWidgetProp
|
|||
</Badge>
|
||||
</div>
|
||||
{alert.location && (
|
||||
<p className="text-[10px] text-gray-500 mt-0.5">📍 {alert.location}</p>
|
||||
<p className="text-[10px] text-muted-foreground mt-0.5">📍 {alert.location}</p>
|
||||
)}
|
||||
<p className="text-[10px] text-gray-600 mt-0.5 line-clamp-2">{alert.description}</p>
|
||||
<div className="mt-1 flex items-center gap-2 text-[9px] text-gray-400">
|
||||
<p className="text-[10px] text-foreground mt-0.5 line-clamp-2">{alert.description}</p>
|
||||
<div className="mt-1 flex items-center gap-2 text-[9px] text-muted-foreground">
|
||||
<span>{new Date(alert.timestamp).toLocaleString("ko-KR")}</span>
|
||||
{alert.source && <span>· {alert.source}</span>}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -135,11 +135,11 @@ export default function RiskAlertWidget({ element }: RiskAlertWidgetProps) {
|
|||
const getAlertIcon = (type: AlertType) => {
|
||||
switch (type) {
|
||||
case "accident":
|
||||
return <AlertTriangle className="h-5 w-5 text-red-600" />;
|
||||
return <AlertTriangle className="h-5 w-5 text-destructive" />;
|
||||
case "weather":
|
||||
return <Cloud className="h-5 w-5 text-blue-600" />;
|
||||
return <Cloud className="h-5 w-5 text-primary" />;
|
||||
case "construction":
|
||||
return <Construction className="h-5 w-5 text-yellow-600" />;
|
||||
return <Construction className="h-5 w-5 text-warning" />;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -208,30 +208,30 @@ export default function RiskAlertWidget({ element }: RiskAlertWidgetProps) {
|
|||
<div className="grid grid-cols-3 gap-3">
|
||||
<Card
|
||||
className={`cursor-pointer p-3 transition-all hover:shadow-md ${
|
||||
filter === "accident" ? "bg-red-50" : ""
|
||||
filter === "accident" ? "bg-destructive/10" : ""
|
||||
}`}
|
||||
onClick={() => setFilter(filter === "accident" ? "all" : "accident")}
|
||||
>
|
||||
<div className="text-xs text-muted-foreground">교통사고</div>
|
||||
<div className="text-2xl font-bold text-red-600">{stats.accident}건</div>
|
||||
<div className="text-2xl font-bold text-destructive">{stats.accident}건</div>
|
||||
</Card>
|
||||
<Card
|
||||
className={`cursor-pointer p-3 transition-all hover:shadow-md ${
|
||||
filter === "weather" ? "bg-blue-50" : ""
|
||||
filter === "weather" ? "bg-primary/10" : ""
|
||||
}`}
|
||||
onClick={() => setFilter(filter === "weather" ? "all" : "weather")}
|
||||
>
|
||||
<div className="text-xs text-muted-foreground">날씨특보</div>
|
||||
<div className="text-2xl font-bold text-blue-600">{stats.weather}건</div>
|
||||
<div className="text-2xl font-bold text-primary">{stats.weather}건</div>
|
||||
</Card>
|
||||
<Card
|
||||
className={`cursor-pointer p-3 transition-all hover:shadow-md ${
|
||||
filter === "construction" ? "bg-yellow-50" : ""
|
||||
filter === "construction" ? "bg-warning/10" : ""
|
||||
}`}
|
||||
onClick={() => setFilter(filter === "construction" ? "all" : "construction")}
|
||||
>
|
||||
<div className="text-xs text-muted-foreground">도로공사</div>
|
||||
<div className="text-2xl font-bold text-yellow-600">{stats.construction}건</div>
|
||||
<div className="text-2xl font-bold text-warning">{stats.construction}건</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -141,7 +141,7 @@ export default function StatusSummaryWidget({
|
|||
element,
|
||||
title = "상태 요약",
|
||||
icon = "📊",
|
||||
bgGradient = "from-slate-50 to-blue-50",
|
||||
bgGradient = "from-background to-primary/10",
|
||||
statusConfig,
|
||||
}: StatusSummaryWidgetProps) {
|
||||
const [statusData, setStatusData] = useState<StatusData[]>([]);
|
||||
|
|
@ -265,13 +265,13 @@ export default function StatusSummaryWidget({
|
|||
}
|
||||
|
||||
const colorMap = {
|
||||
blue: { border: "border-blue-500", dot: "bg-blue-500", text: "text-blue-600" },
|
||||
green: { border: "border-green-500", dot: "bg-green-500", text: "text-green-600" },
|
||||
red: { border: "border-red-500", dot: "bg-red-500", text: "text-red-600" },
|
||||
yellow: { border: "border-yellow-500", dot: "bg-yellow-500", text: "text-yellow-600" },
|
||||
orange: { border: "border-orange-500", dot: "bg-orange-500", text: "text-orange-600" },
|
||||
purple: { border: "border-purple-500", dot: "bg-purple-500", text: "text-purple-600" },
|
||||
gray: { border: "border-gray-500", dot: "bg-gray-500", text: "text-gray-600" },
|
||||
blue: { border: "border-primary", dot: "bg-primary", text: "text-primary" },
|
||||
green: { border: "border-success", dot: "bg-success", text: "text-success" },
|
||||
red: { border: "border-destructive", dot: "bg-destructive", text: "text-destructive" },
|
||||
yellow: { border: "border-warning", dot: "bg-warning", text: "text-warning" },
|
||||
orange: { border: "border-warning", dot: "bg-warning", text: "text-warning" },
|
||||
purple: { border: "border-purple-500", dot: "bg-purple-500/100", text: "text-purple-500" },
|
||||
gray: { border: "border-border", dot: "bg-muted-foreground", text: "text-muted-foreground" },
|
||||
};
|
||||
|
||||
return colorMap[color as keyof typeof colorMap] || colorMap.gray;
|
||||
|
|
@ -282,7 +282,7 @@ export default function StatusSummaryWidget({
|
|||
<div className="flex h-full items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="border-primary mx-auto h-8 w-8 animate-spin rounded-full border-2 border-t-transparent" />
|
||||
<p className="mt-2 text-sm text-gray-500">데이터 로딩 중...</p>
|
||||
<p className="mt-2 text-sm text-muted-foreground">데이터 로딩 중...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -291,11 +291,11 @@ export default function StatusSummaryWidget({
|
|||
if (error) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<div className="text-center text-red-500">
|
||||
<div className="text-center text-destructive">
|
||||
<p className="text-sm">⚠️ {error}</p>
|
||||
<button
|
||||
onClick={loadData}
|
||||
className="mt-2 rounded bg-red-100 px-3 py-1 text-xs text-red-700 hover:bg-red-200"
|
||||
className="mt-2 rounded bg-destructive/10 px-3 py-1 text-xs text-destructive hover:bg-destructive/20"
|
||||
>
|
||||
다시 시도
|
||||
</button>
|
||||
|
|
@ -309,8 +309,8 @@ export default function StatusSummaryWidget({
|
|||
<div className="flex h-full items-center justify-center p-3">
|
||||
<div className="max-w-xs space-y-2 text-center">
|
||||
<div className="text-3xl">{icon}</div>
|
||||
<h3 className="text-sm font-bold text-gray-900">{title}</h3>
|
||||
<div className="space-y-1.5 text-xs text-gray-600">
|
||||
<h3 className="text-sm font-bold text-foreground">{title}</h3>
|
||||
<div className="space-y-1.5 text-xs text-foreground">
|
||||
<p className="font-medium">📊 상태별 데이터 집계 위젯</p>
|
||||
<ul className="space-y-0.5 text-left">
|
||||
<li>• SQL 쿼리로 데이터를 불러옵니다</li>
|
||||
|
|
@ -319,7 +319,7 @@ export default function StatusSummaryWidget({
|
|||
<li>• 색상과 라벨 커스터마이징 지원</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="mt-2 rounded-lg bg-blue-50 p-2 text-[10px] text-blue-700">
|
||||
<div className="mt-2 rounded-lg bg-primary/10 p-2 text-[10px] text-primary">
|
||||
<p className="font-medium">⚙️ 설정 방법</p>
|
||||
<p>SQL 쿼리를 입력하고 저장하세요</p>
|
||||
</div>
|
||||
|
|
@ -357,18 +357,18 @@ export default function StatusSummaryWidget({
|
|||
{/* 헤더 */}
|
||||
<div className="mb-2 flex flex-shrink-0 items-center justify-between">
|
||||
<div className="flex-1">
|
||||
<h3 className="text-sm font-bold text-gray-900">
|
||||
<h3 className="text-sm font-bold text-foreground">
|
||||
{icon} {displayTitle}
|
||||
</h3>
|
||||
{totalCount > 0 ? (
|
||||
<p className="text-xs text-gray-500">총 {totalCount.toLocaleString()}건</p>
|
||||
<p className="text-xs text-muted-foreground">총 {totalCount.toLocaleString()}건</p>
|
||||
) : (
|
||||
<p className="text-xs text-orange-500">⚙️ 데이터 연결 필요</p>
|
||||
<p className="text-xs text-warning">⚙️ 데이터 연결 필요</p>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={loadData}
|
||||
className="border-border hover:bg-accent flex h-7 w-7 items-center justify-center rounded border bg-white p-0 text-xs disabled:opacity-50"
|
||||
className="border-border hover:bg-accent flex h-7 w-7 items-center justify-center rounded border bg-background p-0 text-xs disabled:opacity-50"
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? "⏳" : "🔄"}
|
||||
|
|
@ -382,10 +382,10 @@ export default function StatusSummaryWidget({
|
|||
{statusData.map((item) => {
|
||||
const colors = getColorClasses(item.status);
|
||||
return (
|
||||
<div key={item.status} className="rounded border border-gray-200 bg-white p-1.5 shadow-sm">
|
||||
<div key={item.status} className="rounded border border-border bg-background p-1.5 shadow-sm">
|
||||
<div className="mb-0.5 flex items-center gap-1">
|
||||
<div className={`h-1.5 w-1.5 rounded-full ${colors.dot}`}></div>
|
||||
<div className="text-xs font-medium text-gray-600">{item.status}</div>
|
||||
<div className="text-xs font-medium text-foreground">{item.status}</div>
|
||||
</div>
|
||||
<div className={`text-lg font-bold ${colors.text}`}>{item.count.toLocaleString()}</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -481,10 +481,10 @@ export default function TaskWidget({ element }: TaskWidgetProps) {
|
|||
|
||||
const getPriorityColor = (priority: TaskItem["priority"]) => {
|
||||
switch (priority) {
|
||||
case "urgent": return "bg-red-100 text-red-700 border-red-300";
|
||||
case "high": return "bg-orange-100 text-orange-700 border-orange-300";
|
||||
case "normal": return "bg-blue-100 text-blue-700 border-blue-300";
|
||||
case "low": return "bg-gray-100 text-gray-700 border-gray-300";
|
||||
case "urgent": return "bg-destructive/10 text-destructive border-destructive";
|
||||
case "high": return "bg-warning/10 text-warning border-warning";
|
||||
case "normal": return "bg-primary/10 text-primary border-primary";
|
||||
case "low": return "bg-muted text-foreground border-border";
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -549,22 +549,22 @@ export default function TaskWidget({ element }: TaskWidgetProps) {
|
|||
if (loading) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<div className="text-gray-500">로딩 중...</div>
|
||||
<div className="text-muted-foreground">로딩 중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col bg-gradient-to-br from-slate-50 to-blue-50">
|
||||
<div className="flex h-full flex-col bg-background">
|
||||
{/* 제목 */}
|
||||
<div className="border-b border-gray-200 bg-white px-4 py-2">
|
||||
<div className="border-b border-border bg-background px-4 py-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-gray-800">
|
||||
<h3 className="text-lg font-bold text-foreground">
|
||||
{element?.customTitle || "일정관리 위젯"}
|
||||
</h3>
|
||||
{selectedDate && (
|
||||
<div className="mt-1 flex items-center gap-1 text-xs text-green-600">
|
||||
<div className="mt-1 flex items-center gap-1 text-xs text-success">
|
||||
<CalendarIcon className="h-3 w-3" />
|
||||
<span className="font-semibold">{formatSelectedDate()} 일정</span>
|
||||
</div>
|
||||
|
|
@ -582,24 +582,24 @@ export default function TaskWidget({ element }: TaskWidgetProps) {
|
|||
|
||||
{/* 헤더 (통계, 필터) */}
|
||||
{element?.showHeader !== false && (
|
||||
<div className="border-b border-gray-200 bg-white px-4 py-3">
|
||||
<div className="border-b border-border bg-background px-4 py-3">
|
||||
{stats && (
|
||||
<div className="grid grid-cols-4 gap-2 text-xs mb-3">
|
||||
<div className="rounded bg-blue-50 px-2 py-1.5 text-center">
|
||||
<div className="font-bold text-blue-700">{stats.pending}</div>
|
||||
<div className="text-blue-600">대기</div>
|
||||
<div className="rounded bg-primary/10 px-2 py-1.5 text-center">
|
||||
<div className="font-bold text-primary">{stats.pending}</div>
|
||||
<div className="text-primary">대기</div>
|
||||
</div>
|
||||
<div className="rounded bg-amber-50 px-2 py-1.5 text-center">
|
||||
<div className="font-bold text-amber-700">{stats.inProgress}</div>
|
||||
<div className="text-amber-600">진행중</div>
|
||||
<div className="rounded bg-warning/10 px-2 py-1.5 text-center">
|
||||
<div className="font-bold text-warning">{stats.inProgress}</div>
|
||||
<div className="text-warning">진행중</div>
|
||||
</div>
|
||||
<div className="rounded bg-red-50 px-2 py-1.5 text-center">
|
||||
<div className="font-bold text-red-700">{stats.urgent}</div>
|
||||
<div className="text-red-600">긴급</div>
|
||||
<div className="rounded bg-destructive/10 px-2 py-1.5 text-center">
|
||||
<div className="font-bold text-destructive">{stats.urgent}</div>
|
||||
<div className="text-destructive">긴급</div>
|
||||
</div>
|
||||
<div className="rounded bg-rose-50 px-2 py-1.5 text-center">
|
||||
<div className="font-bold text-rose-700">{stats.overdue}</div>
|
||||
<div className="text-rose-600">지연</div>
|
||||
<div className="rounded bg-destructive/10 px-2 py-1.5 text-center">
|
||||
<div className="font-bold text-destructive">{stats.overdue}</div>
|
||||
<div className="text-destructive">지연</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -610,7 +610,7 @@ export default function TaskWidget({ element }: TaskWidgetProps) {
|
|||
key={f}
|
||||
onClick={() => setFilter(f)}
|
||||
className={`rounded px-3 py-1 text-xs font-medium transition-colors ${
|
||||
filter === f ? "bg-primary text-white" : "bg-gray-100 text-gray-600 hover:bg-gray-200"
|
||||
filter === f ? "bg-primary text-white" : "bg-muted text-foreground hover:bg-muted"
|
||||
}`}
|
||||
>
|
||||
{f === "all" ? "전체" : f === "pending" ? "대기" : f === "in_progress" ? "진행중" : "완료"}
|
||||
|
|
@ -622,7 +622,7 @@ export default function TaskWidget({ element }: TaskWidgetProps) {
|
|||
|
||||
{/* 추가 폼 */}
|
||||
{showAddForm && (
|
||||
<div className="max-h-[400px] overflow-y-auto border-b border-gray-200 bg-white p-4">
|
||||
<div className="max-h-[400px] overflow-y-auto border-b border-border bg-background p-4">
|
||||
<div className="space-y-2">
|
||||
<input
|
||||
type="text"
|
||||
|
|
@ -630,21 +630,21 @@ export default function TaskWidget({ element }: TaskWidgetProps) {
|
|||
value={newTask.title}
|
||||
onChange={(e) => setNewTask({ ...newTask, 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"
|
||||
className="w-full rounded border border-border px-3 py-2 text-sm focus:border-primary focus:outline-none"
|
||||
/>
|
||||
<textarea
|
||||
placeholder="상세 설명 (선택)"
|
||||
value={newTask.description}
|
||||
onChange={(e) => setNewTask({ ...newTask, 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"
|
||||
className="w-full rounded border border-border px-3 py-2 text-sm focus:border-primary focus:outline-none"
|
||||
rows={2}
|
||||
/>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<select
|
||||
value={newTask.priority}
|
||||
onChange={(e) => setNewTask({ ...newTask, priority: e.target.value as TaskItem["priority"] })}
|
||||
className="rounded border border-gray-300 px-3 py-2 text-sm focus:border-primary focus:outline-none"
|
||||
className="rounded border border-border px-3 py-2 text-sm focus:border-primary focus:outline-none"
|
||||
>
|
||||
<option value="low">🟢 낮음</option>
|
||||
<option value="normal">🟡 보통</option>
|
||||
|
|
@ -655,7 +655,7 @@ export default function TaskWidget({ element }: TaskWidgetProps) {
|
|||
type="datetime-local"
|
||||
value={newTask.dueDate}
|
||||
onChange={(e) => setNewTask({ ...newTask, dueDate: e.target.value })}
|
||||
className="rounded border border-gray-300 px-3 py-2 text-sm focus:border-primary focus:outline-none"
|
||||
className="rounded border border-border px-3 py-2 text-sm focus:border-primary focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
|
|
@ -664,9 +664,9 @@ export default function TaskWidget({ element }: TaskWidgetProps) {
|
|||
type="checkbox"
|
||||
checked={newTask.isUrgent}
|
||||
onChange={(e) => setNewTask({ ...newTask, isUrgent: e.target.checked })}
|
||||
className="h-4 w-4 rounded border-gray-300"
|
||||
className="h-4 w-4 rounded border-border"
|
||||
/>
|
||||
<span className="text-red-600 font-medium">긴급</span>
|
||||
<span className="text-destructive font-medium">긴급</span>
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
|
|
@ -678,7 +678,7 @@ export default function TaskWidget({ element }: TaskWidgetProps) {
|
|||
</button>
|
||||
<button
|
||||
onClick={() => setShowAddForm(false)}
|
||||
className="rounded bg-gray-200 px-4 py-2 text-sm text-gray-700 hover:bg-gray-300"
|
||||
className="rounded bg-muted px-4 py-2 text-sm text-foreground hover:bg-muted/90"
|
||||
>
|
||||
취소
|
||||
</button>
|
||||
|
|
@ -690,7 +690,7 @@ export default function TaskWidget({ element }: TaskWidgetProps) {
|
|||
{/* Task 리스트 */}
|
||||
<div className="flex-1 overflow-y-auto p-4 min-h-0">
|
||||
{filteredTasks.length === 0 ? (
|
||||
<div className="flex h-full items-center justify-center text-gray-400">
|
||||
<div className="flex h-full items-center justify-center text-muted-foreground">
|
||||
<div className="text-center">
|
||||
<div className="mb-2 text-4xl">📝</div>
|
||||
<div>{selectedDate ? `${formatSelectedDate()} 일정이 없습니다` : `일정이 없습니다`}</div>
|
||||
|
|
@ -701,8 +701,8 @@ export default function TaskWidget({ element }: TaskWidgetProps) {
|
|||
{filteredTasks.map((task) => (
|
||||
<div
|
||||
key={task.id}
|
||||
className={`group relative rounded-lg border-2 bg-white p-3 shadow-sm transition-all hover:shadow-md ${
|
||||
task.isUrgent || task.status === "overdue" ? "border-red-400" : "border-gray-200"
|
||||
className={`group relative rounded-lg border-2 bg-background p-3 shadow-sm transition-all hover:shadow-md ${
|
||||
task.isUrgent || task.status === "overdue" ? "border-destructive" : "border-border"
|
||||
} ${task.status === "completed" ? "opacity-60" : ""}`}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
|
|
@ -716,26 +716,26 @@ export default function TaskWidget({ element }: TaskWidgetProps) {
|
|||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex-1">
|
||||
<div className={`font-medium ${task.status === "completed" ? "line-through" : ""}`}>
|
||||
{task.isUrgent && <span className="mr-1 text-red-600">⚡</span>}
|
||||
{task.isUrgent && <span className="mr-1 text-destructive">⚡</span>}
|
||||
{task.vehicleNumber ? (
|
||||
<>
|
||||
<span className="font-bold">{task.vehicleNumber}</span>
|
||||
{task.vehicleType && <span className="ml-2 text-xs text-gray-600">({task.vehicleType})</span>}
|
||||
{task.vehicleType && <span className="ml-2 text-xs text-foreground">({task.vehicleType})</span>}
|
||||
</>
|
||||
) : (
|
||||
task.title
|
||||
)}
|
||||
</div>
|
||||
{task.maintenanceType && (
|
||||
<div className="mt-1 rounded bg-gray-50 px-2 py-1 text-xs font-medium text-gray-700">
|
||||
<div className="mt-1 rounded bg-muted px-2 py-1 text-xs font-medium text-foreground">
|
||||
{task.maintenanceType}
|
||||
</div>
|
||||
)}
|
||||
{task.description && (
|
||||
<div className="mt-1 text-xs text-gray-600">{task.description}</div>
|
||||
<div className="mt-1 text-xs text-foreground">{task.description}</div>
|
||||
)}
|
||||
{task.dueDate && (
|
||||
<div className="mt-1 text-xs text-gray-500">{getTimeRemaining(task.dueDate)}</div>
|
||||
<div className="mt-1 text-xs text-muted-foreground">{getTimeRemaining(task.dueDate)}</div>
|
||||
)}
|
||||
{task.estimatedCost && (
|
||||
<div className="mt-1 text-xs font-bold text-primary">
|
||||
|
|
@ -749,7 +749,7 @@ export default function TaskWidget({ element }: TaskWidgetProps) {
|
|||
{task.status !== "completed" && (
|
||||
<button
|
||||
onClick={() => handleUpdateStatus(task.id, "completed")}
|
||||
className="rounded p-1 text-green-600 hover:bg-green-50"
|
||||
className="rounded p-1 text-success hover:bg-success/10"
|
||||
title="완료"
|
||||
>
|
||||
<Check className="h-4 w-4" />
|
||||
|
|
@ -757,7 +757,7 @@ export default function TaskWidget({ element }: TaskWidgetProps) {
|
|||
)}
|
||||
<button
|
||||
onClick={() => handleDelete(task.id)}
|
||||
className="rounded p-1 text-red-600 hover:bg-red-50"
|
||||
className="rounded p-1 text-destructive hover:bg-destructive/10"
|
||||
title="삭제"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
|
|
@ -772,8 +772,8 @@ export default function TaskWidget({ element }: TaskWidgetProps) {
|
|||
onClick={() => handleUpdateStatus(task.id, "pending")}
|
||||
className={`rounded px-2 py-1 text-xs ${
|
||||
task.status === "pending"
|
||||
? "bg-blue-100 text-blue-700"
|
||||
: "bg-gray-100 text-gray-600 hover:bg-gray-200"
|
||||
? "bg-primary/10 text-primary"
|
||||
: "bg-muted text-foreground hover:bg-muted"
|
||||
}`}
|
||||
>
|
||||
대기
|
||||
|
|
@ -782,8 +782,8 @@ export default function TaskWidget({ element }: TaskWidgetProps) {
|
|||
onClick={() => handleUpdateStatus(task.id, "in_progress")}
|
||||
className={`rounded px-2 py-1 text-xs ${
|
||||
task.status === "in_progress"
|
||||
? "bg-amber-100 text-amber-700"
|
||||
: "bg-gray-100 text-gray-600 hover:bg-gray-200"
|
||||
? "bg-warning/10 text-warning"
|
||||
: "bg-muted text-foreground hover:bg-muted"
|
||||
}`}
|
||||
>
|
||||
진행중
|
||||
|
|
|
|||
|
|
@ -267,13 +267,13 @@ export default function TodoWidget({ element }: TodoWidgetProps) {
|
|||
const getPriorityColor = (priority: TodoItem["priority"]) => {
|
||||
switch (priority) {
|
||||
case "urgent":
|
||||
return "bg-red-100 text-red-700 border-red-300";
|
||||
return "bg-destructive/10 text-destructive border-destructive";
|
||||
case "high":
|
||||
return "bg-orange-100 text-orange-700 border-orange-300";
|
||||
return "bg-warning/10 text-warning border-warning";
|
||||
case "normal":
|
||||
return "bg-blue-100 text-blue-700 border-blue-300";
|
||||
return "bg-primary/10 text-primary border-primary";
|
||||
case "low":
|
||||
return "bg-gray-100 text-gray-700 border-gray-300";
|
||||
return "bg-muted text-foreground border-border";
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -327,20 +327,20 @@ export default function TodoWidget({ element }: TodoWidgetProps) {
|
|||
if (loading) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<div className="text-gray-500">로딩 중...</div>
|
||||
<div className="text-muted-foreground">로딩 중...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col bg-gradient-to-br from-slate-50 to-blue-50">
|
||||
<div className="flex h-full flex-col bg-background">
|
||||
{/* 제목 - 항상 표시 */}
|
||||
<div className="border-b border-gray-200 bg-white px-4 py-2">
|
||||
<div className="border-b border-border bg-background px-4 py-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-gray-800">{element?.customTitle || "To-Do / 긴급 지시"}</h3>
|
||||
<h3 className="text-lg font-bold text-foreground">{element?.customTitle || "To-Do / 긴급 지시"}</h3>
|
||||
{selectedDate && (
|
||||
<div className="mt-1 flex items-center gap-1 text-xs text-green-600">
|
||||
<div className="mt-1 flex items-center gap-1 text-xs text-success">
|
||||
<CalendarIcon className="h-3 w-3" />
|
||||
<span className="font-semibold">{formatSelectedDate()} 할일</span>
|
||||
</div>
|
||||
|
|
@ -360,25 +360,25 @@ export default function TodoWidget({ element }: TodoWidgetProps) {
|
|||
|
||||
{/* 헤더 (통계, 필터) - showHeader가 false일 때만 숨김 */}
|
||||
{element?.showHeader !== false && (
|
||||
<div className="border-b border-gray-200 bg-white px-4 py-3">
|
||||
<div className="border-b border-border bg-background px-4 py-3">
|
||||
{/* 통계 */}
|
||||
{stats && (
|
||||
<div className="grid grid-cols-4 gap-2 text-xs mb-3">
|
||||
<div className="rounded bg-blue-50 px-2 py-1.5 text-center">
|
||||
<div className="font-bold text-blue-700">{stats.pending}</div>
|
||||
<div className="text-blue-600">대기</div>
|
||||
<div className="rounded bg-primary/10 px-2 py-1.5 text-center">
|
||||
<div className="font-bold text-primary">{stats.pending}</div>
|
||||
<div className="text-primary">대기</div>
|
||||
</div>
|
||||
<div className="rounded bg-amber-50 px-2 py-1.5 text-center">
|
||||
<div className="font-bold text-amber-700">{stats.inProgress}</div>
|
||||
<div className="text-amber-600">진행중</div>
|
||||
<div className="rounded bg-warning/10 px-2 py-1.5 text-center">
|
||||
<div className="font-bold text-warning">{stats.inProgress}</div>
|
||||
<div className="text-warning">진행중</div>
|
||||
</div>
|
||||
<div className="rounded bg-red-50 px-2 py-1.5 text-center">
|
||||
<div className="font-bold text-red-700">{stats.urgent}</div>
|
||||
<div className="text-red-600">긴급</div>
|
||||
<div className="rounded bg-destructive/10 px-2 py-1.5 text-center">
|
||||
<div className="font-bold text-destructive">{stats.urgent}</div>
|
||||
<div className="text-destructive">긴급</div>
|
||||
</div>
|
||||
<div className="rounded bg-rose-50 px-2 py-1.5 text-center">
|
||||
<div className="font-bold text-rose-700">{stats.overdue}</div>
|
||||
<div className="text-rose-600">지연</div>
|
||||
<div className="rounded bg-destructive/10 px-2 py-1.5 text-center">
|
||||
<div className="font-bold text-destructive">{stats.overdue}</div>
|
||||
<div className="text-destructive">지연</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -392,7 +392,7 @@ export default function TodoWidget({ element }: TodoWidgetProps) {
|
|||
className={`rounded px-3 py-1 text-xs font-medium transition-colors ${
|
||||
filter === f
|
||||
? "bg-primary text-white"
|
||||
: "bg-gray-100 text-gray-600 hover:bg-gray-200"
|
||||
: "bg-muted text-foreground hover:bg-muted"
|
||||
}`}
|
||||
>
|
||||
{f === "all" ? "전체" : f === "pending" ? "대기" : f === "in_progress" ? "진행중" : "완료"}
|
||||
|
|
@ -404,7 +404,7 @@ export default function TodoWidget({ element }: TodoWidgetProps) {
|
|||
|
||||
{/* 추가 폼 */}
|
||||
{showAddForm && (
|
||||
<div className="max-h-[400px] overflow-y-auto border-b border-gray-200 bg-white p-4">
|
||||
<div className="max-h-[400px] overflow-y-auto border-b border-border bg-background p-4">
|
||||
<div className="space-y-2">
|
||||
<input
|
||||
type="text"
|
||||
|
|
@ -412,21 +412,21 @@ export default function TodoWidget({ element }: TodoWidgetProps) {
|
|||
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"
|
||||
className="w-full rounded border border-border 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"
|
||||
className="w-full rounded border border-border px-3 py-2 text-sm focus:border-primary focus:outline-none"
|
||||
rows={2}
|
||||
/>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<select
|
||||
value={newTodo.priority}
|
||||
onChange={(e) => setNewTodo({ ...newTodo, priority: e.target.value as TodoItem["priority"] })}
|
||||
className="rounded border border-gray-300 px-3 py-2 text-sm focus:border-primary focus:outline-none"
|
||||
className="rounded border border-border px-3 py-2 text-sm focus:border-primary focus:outline-none"
|
||||
>
|
||||
<option value="low">🟢 낮음</option>
|
||||
<option value="normal">🟡 보통</option>
|
||||
|
|
@ -437,7 +437,7 @@ export default function TodoWidget({ element }: TodoWidgetProps) {
|
|||
type="datetime-local"
|
||||
value={newTodo.dueDate}
|
||||
onChange={(e) => setNewTodo({ ...newTodo, dueDate: e.target.value })}
|
||||
className="rounded border border-gray-300 px-3 py-2 text-sm focus:border-primary focus:outline-none"
|
||||
className="rounded border border-border px-3 py-2 text-sm focus:border-primary focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
|
|
@ -446,9 +446,9 @@ export default function TodoWidget({ element }: TodoWidgetProps) {
|
|||
type="checkbox"
|
||||
checked={newTodo.isUrgent}
|
||||
onChange={(e) => setNewTodo({ ...newTodo, isUrgent: e.target.checked })}
|
||||
className="h-4 w-4 rounded border-gray-300"
|
||||
className="h-4 w-4 rounded border-border"
|
||||
/>
|
||||
<span className="text-red-600 font-medium">긴급 지시</span>
|
||||
<span className="text-destructive font-medium">긴급 지시</span>
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
|
|
@ -460,7 +460,7 @@ export default function TodoWidget({ element }: TodoWidgetProps) {
|
|||
</button>
|
||||
<button
|
||||
onClick={() => setShowAddForm(false)}
|
||||
className="rounded bg-gray-200 px-4 py-2 text-sm text-gray-700 hover:bg-gray-300"
|
||||
className="rounded bg-muted px-4 py-2 text-sm text-foreground hover:bg-muted/90"
|
||||
>
|
||||
취소
|
||||
</button>
|
||||
|
|
@ -472,7 +472,7 @@ export default function TodoWidget({ element }: TodoWidgetProps) {
|
|||
{/* To-Do 리스트 */}
|
||||
<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="flex h-full items-center justify-center text-muted-foreground">
|
||||
<div className="text-center">
|
||||
<div className="mb-2 text-4xl">📝</div>
|
||||
<div>{selectedDate ? `${formatSelectedDate()} 할 일이 없습니다` : "할 일이 없습니다"}</div>
|
||||
|
|
@ -483,8 +483,8 @@ export default function TodoWidget({ element }: TodoWidgetProps) {
|
|||
{filteredTodos.map((todo) => (
|
||||
<div
|
||||
key={todo.id}
|
||||
className={`group relative rounded-lg border-2 bg-white p-3 shadow-sm transition-all hover:shadow-md ${
|
||||
todo.isUrgent ? "border-red-400" : "border-gray-200"
|
||||
className={`group relative rounded-lg border-2 bg-background p-3 shadow-sm transition-all hover:shadow-md ${
|
||||
todo.isUrgent ? "border-destructive" : "border-border"
|
||||
} ${todo.status === "completed" ? "opacity-60" : ""}`}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
|
|
@ -496,14 +496,14 @@ export default function TodoWidget({ element }: TodoWidgetProps) {
|
|||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex-1">
|
||||
<div className={`font-medium ${todo.status === "completed" ? "line-through" : ""}`}>
|
||||
{todo.isUrgent && <span className="mr-1 text-red-600">⚡</span>}
|
||||
{todo.isUrgent && <span className="mr-1 text-destructive">⚡</span>}
|
||||
{todo.title}
|
||||
</div>
|
||||
{todo.description && (
|
||||
<div className="mt-1 text-xs text-gray-600">{todo.description}</div>
|
||||
<div className="mt-1 text-xs text-foreground">{todo.description}</div>
|
||||
)}
|
||||
{todo.dueDate && (
|
||||
<div className="mt-1 text-xs text-gray-500">{getTimeRemaining(todo.dueDate)}</div>
|
||||
<div className="mt-1 text-xs text-muted-foreground">{getTimeRemaining(todo.dueDate)}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
|
@ -512,7 +512,7 @@ export default function TodoWidget({ element }: TodoWidgetProps) {
|
|||
{todo.status !== "completed" && (
|
||||
<button
|
||||
onClick={() => handleUpdateStatus(todo.id, "completed")}
|
||||
className="rounded p-1 text-green-600 hover:bg-green-50"
|
||||
className="rounded p-1 text-success hover:bg-success/10"
|
||||
title="완료"
|
||||
>
|
||||
<Check className="h-4 w-4" />
|
||||
|
|
@ -520,7 +520,7 @@ export default function TodoWidget({ element }: TodoWidgetProps) {
|
|||
)}
|
||||
<button
|
||||
onClick={() => handleDelete(todo.id)}
|
||||
className="rounded p-1 text-red-600 hover:bg-red-50"
|
||||
className="rounded p-1 text-destructive hover:bg-destructive/10"
|
||||
title="삭제"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
|
|
@ -535,8 +535,8 @@ export default function TodoWidget({ element }: TodoWidgetProps) {
|
|||
onClick={() => handleUpdateStatus(todo.id, "pending")}
|
||||
className={`rounded px-2 py-1 text-xs ${
|
||||
todo.status === "pending"
|
||||
? "bg-blue-100 text-blue-700"
|
||||
: "bg-gray-100 text-gray-600 hover:bg-gray-200"
|
||||
? "bg-primary/10 text-primary"
|
||||
: "bg-muted text-foreground hover:bg-muted"
|
||||
}`}
|
||||
>
|
||||
대기
|
||||
|
|
@ -545,8 +545,8 @@ export default function TodoWidget({ element }: TodoWidgetProps) {
|
|||
onClick={() => handleUpdateStatus(todo.id, "in_progress")}
|
||||
className={`rounded px-2 py-1 text-xs ${
|
||||
todo.status === "in_progress"
|
||||
? "bg-amber-100 text-amber-700"
|
||||
: "bg-gray-100 text-gray-600 hover:bg-gray-200"
|
||||
? "bg-warning/10 text-warning"
|
||||
: "bg-muted text-foreground hover:bg-muted"
|
||||
}`}
|
||||
>
|
||||
진행중
|
||||
|
|
|
|||
|
|
@ -247,10 +247,10 @@ export default function TransportStatsWidget({ element, refreshInterval = 60000
|
|||
|
||||
if (isLoading && !stats) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center bg-gray-50">
|
||||
<div className="flex h-full items-center justify-center bg-muted">
|
||||
<div className="text-center">
|
||||
<div className="mx-auto h-8 w-8 animate-spin rounded-full border-4 border-blue-500 border-t-transparent" />
|
||||
<div className="mt-2 text-sm text-gray-600">로딩 중...</div>
|
||||
<div className="mx-auto h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent" />
|
||||
<div className="mt-2 text-sm text-foreground">로딩 중...</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -258,14 +258,14 @@ export default function TransportStatsWidget({ element, refreshInterval = 60000
|
|||
|
||||
if (error || !stats) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center bg-gray-50 p-4">
|
||||
<div className="flex h-full items-center justify-center bg-muted p-4">
|
||||
<div className="text-center">
|
||||
<div className="mb-2 text-4xl">⚠️</div>
|
||||
<div className="text-sm font-medium text-gray-600">{error || "데이터 없음"}</div>
|
||||
{!element?.dataSource?.query && <div className="mt-2 text-xs text-gray-500">쿼리를 설정하세요</div>}
|
||||
<div className="text-sm font-medium text-foreground">{error || "데이터 없음"}</div>
|
||||
{!element?.dataSource?.query && <div className="mt-2 text-xs text-muted-foreground">쿼리를 설정하세요</div>}
|
||||
<button
|
||||
onClick={loadData}
|
||||
className="mt-3 rounded-lg bg-blue-500 px-4 py-2 text-sm text-white hover:bg-blue-600"
|
||||
className="mt-3 rounded-lg bg-primary px-4 py-2 text-sm text-white hover:bg-primary/90"
|
||||
>
|
||||
다시 시도
|
||||
</button>
|
||||
|
|
@ -275,48 +275,48 @@ export default function TransportStatsWidget({ element, refreshInterval = 60000
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center bg-white p-6">
|
||||
<div className="flex h-full items-center justify-center bg-background p-6">
|
||||
<div className="grid w-full gap-4" style={{ gridTemplateColumns: "repeat(auto-fit, minmax(180px, 1fr))" }}>
|
||||
{/* 총 건수 */}
|
||||
<div className="rounded-lg border bg-indigo-50 p-4 text-center">
|
||||
<div className="text-sm text-gray-600">총 건수</div>
|
||||
<div className="mt-2 text-3xl font-bold text-indigo-600">
|
||||
<div className="rounded-lg border bg-primary/10 p-4 text-center">
|
||||
<div className="text-sm text-foreground">총 건수</div>
|
||||
<div className="mt-2 text-3xl font-bold text-primary">
|
||||
{stats.total_count.toLocaleString()}
|
||||
<span className="ml-1 text-lg">건</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 총 운송량 */}
|
||||
<div className="rounded-lg border bg-green-50 p-4 text-center">
|
||||
<div className="text-sm text-gray-600">총 운송량</div>
|
||||
<div className="mt-2 text-3xl font-bold text-green-600">
|
||||
<div className="rounded-lg border bg-success/10 p-4 text-center">
|
||||
<div className="text-sm text-foreground">총 운송량</div>
|
||||
<div className="mt-2 text-3xl font-bold text-success">
|
||||
{stats.total_weight.toFixed(1)}
|
||||
<span className="ml-1 text-lg">톤</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 누적 거리 */}
|
||||
<div className="rounded-lg border bg-blue-50 p-4 text-center">
|
||||
<div className="text-sm text-gray-600">누적 거리</div>
|
||||
<div className="mt-2 text-3xl font-bold text-blue-600">
|
||||
<div className="rounded-lg border bg-primary/10 p-4 text-center">
|
||||
<div className="text-sm text-foreground">누적 거리</div>
|
||||
<div className="mt-2 text-3xl font-bold text-primary">
|
||||
{stats.total_distance.toFixed(1)}
|
||||
<span className="ml-1 text-lg">km</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 정시 도착률 */}
|
||||
<div className="rounded-lg border bg-purple-50 p-4 text-center">
|
||||
<div className="text-sm text-gray-600">정시 도착률</div>
|
||||
<div className="mt-2 text-3xl font-bold text-purple-600">
|
||||
<div className="rounded-lg border bg-purple-500/10 p-4 text-center">
|
||||
<div className="text-sm text-foreground">정시 도착률</div>
|
||||
<div className="mt-2 text-3xl font-bold text-purple-500">
|
||||
{stats.on_time_rate.toFixed(1)}
|
||||
<span className="ml-1 text-lg">%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 평균 배송시간 */}
|
||||
<div className="rounded-lg border bg-orange-50 p-4 text-center">
|
||||
<div className="text-sm text-gray-600">평균 배송시간</div>
|
||||
<div className="mt-2 text-3xl font-bold text-orange-600">
|
||||
<div className="rounded-lg border bg-warning/10 p-4 text-center">
|
||||
<div className="text-sm text-foreground">평균 배송시간</div>
|
||||
<div className="mt-2 text-3xl font-bold text-warning">
|
||||
{stats.avg_delivery_time.toFixed(1)}
|
||||
<span className="ml-1 text-lg">분</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -74,11 +74,11 @@ export default function VehicleListWidget({ element, refreshInterval = 30000 }:
|
|||
|
||||
const getStatusColor = (status: string) => {
|
||||
const s = status?.toLowerCase() || "";
|
||||
if (s === "active" || s === "running") return "bg-green-500";
|
||||
if (s === "inactive" || s === "idle") return "bg-yellow-500";
|
||||
if (s === "maintenance") return "bg-orange-500";
|
||||
if (s === "warning" || s === "breakdown") return "bg-red-500";
|
||||
return "bg-gray-500";
|
||||
if (s === "active" || s === "running") return "bg-success";
|
||||
if (s === "inactive" || s === "idle") return "bg-warning/100";
|
||||
if (s === "maintenance") return "bg-warning";
|
||||
if (s === "warning" || s === "breakdown") return "bg-destructive";
|
||||
return "bg-muted0";
|
||||
};
|
||||
|
||||
const getStatusText = (status: string) => {
|
||||
|
|
@ -94,12 +94,12 @@ export default function VehicleListWidget({ element, refreshInterval = 30000 }:
|
|||
selectedStatus === "all" ? vehicles : vehicles.filter((v) => v.status?.toLowerCase() === selectedStatus);
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col bg-gradient-to-br from-slate-50 to-blue-50 p-4">
|
||||
<div className="flex h-full w-full flex-col bg-gradient-to-br from-background to-primary/10 p-4">
|
||||
{/* 헤더 */}
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-gray-900">차량 목록</h3>
|
||||
<p className="text-xs text-gray-500">마지막 업데이트: {lastUpdate.toLocaleTimeString("ko-KR")}</p>
|
||||
<h3 className="text-lg font-bold text-foreground">차량 목록</h3>
|
||||
<p className="text-xs text-muted-foreground">마지막 업데이트: {lastUpdate.toLocaleTimeString("ko-KR")}</p>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" onClick={loadVehicles} disabled={isLoading} className="h-8 w-8 p-0">
|
||||
<RefreshCw className={`h-4 w-4 ${isLoading ? "animate-spin" : ""}`} />
|
||||
|
|
@ -111,7 +111,7 @@ export default function VehicleListWidget({ element, refreshInterval = 30000 }:
|
|||
<button
|
||||
onClick={() => setSelectedStatus("all")}
|
||||
className={`rounded-md px-3 py-1 text-xs font-medium whitespace-nowrap transition-colors ${
|
||||
selectedStatus === "all" ? "bg-gray-900 text-white" : "bg-white text-gray-700 hover:bg-gray-100"
|
||||
selectedStatus === "all" ? "bg-gray-900 text-white" : "bg-background text-foreground hover:bg-muted"
|
||||
}`}
|
||||
>
|
||||
전체 ({vehicles.length})
|
||||
|
|
@ -119,7 +119,7 @@ export default function VehicleListWidget({ element, refreshInterval = 30000 }:
|
|||
<button
|
||||
onClick={() => setSelectedStatus("active")}
|
||||
className={`rounded-md px-3 py-1 text-xs font-medium whitespace-nowrap transition-colors ${
|
||||
selectedStatus === "active" ? "bg-green-500 text-white" : "bg-white text-gray-700 hover:bg-gray-100"
|
||||
selectedStatus === "active" ? "bg-success text-white" : "bg-background text-foreground hover:bg-muted"
|
||||
}`}
|
||||
>
|
||||
운행 중 ({vehicles.filter((v) => v.status?.toLowerCase() === "active").length})
|
||||
|
|
@ -127,7 +127,7 @@ export default function VehicleListWidget({ element, refreshInterval = 30000 }:
|
|||
<button
|
||||
onClick={() => setSelectedStatus("inactive")}
|
||||
className={`rounded-md px-3 py-1 text-xs font-medium whitespace-nowrap transition-colors ${
|
||||
selectedStatus === "inactive" ? "bg-yellow-500 text-white" : "bg-white text-gray-700 hover:bg-gray-100"
|
||||
selectedStatus === "inactive" ? "bg-warning/100 text-white" : "bg-background text-foreground hover:bg-muted"
|
||||
}`}
|
||||
>
|
||||
대기 ({vehicles.filter((v) => v.status?.toLowerCase() === "inactive").length})
|
||||
|
|
@ -135,7 +135,7 @@ export default function VehicleListWidget({ element, refreshInterval = 30000 }:
|
|||
<button
|
||||
onClick={() => setSelectedStatus("maintenance")}
|
||||
className={`rounded-md px-3 py-1 text-xs font-medium whitespace-nowrap transition-colors ${
|
||||
selectedStatus === "maintenance" ? "bg-orange-500 text-white" : "bg-white text-gray-700 hover:bg-gray-100"
|
||||
selectedStatus === "maintenance" ? "bg-warning text-white" : "bg-background text-foreground hover:bg-muted"
|
||||
}`}
|
||||
>
|
||||
정비 ({vehicles.filter((v) => v.status?.toLowerCase() === "maintenance").length})
|
||||
|
|
@ -143,7 +143,7 @@ export default function VehicleListWidget({ element, refreshInterval = 30000 }:
|
|||
<button
|
||||
onClick={() => setSelectedStatus("warning")}
|
||||
className={`rounded-md px-3 py-1 text-xs font-medium whitespace-nowrap transition-colors ${
|
||||
selectedStatus === "warning" ? "bg-red-500 text-white" : "bg-white text-gray-700 hover:bg-gray-100"
|
||||
selectedStatus === "warning" ? "bg-destructive text-white" : "bg-background text-foreground hover:bg-muted"
|
||||
}`}
|
||||
>
|
||||
고장 ({vehicles.filter((v) => v.status?.toLowerCase() === "warning").length})
|
||||
|
|
@ -153,10 +153,10 @@ export default function VehicleListWidget({ element, refreshInterval = 30000 }:
|
|||
{/* 차량 목록 */}
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{filteredVehicles.length === 0 ? (
|
||||
<div className="flex h-full items-center justify-center rounded-lg border-2 border-dashed border-gray-300 bg-white">
|
||||
<div className="flex h-full items-center justify-center rounded-lg border-2 border-dashed border-border bg-background">
|
||||
<div className="text-center">
|
||||
<Truck className="mx-auto h-12 w-12 text-gray-300" />
|
||||
<p className="mt-2 text-sm text-gray-500">차량이 없습니다</p>
|
||||
<Truck className="mx-auto h-12 w-12 text-muted-foreground" />
|
||||
<p className="mt-2 text-sm text-muted-foreground">차량이 없습니다</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
|
|
@ -164,12 +164,12 @@ export default function VehicleListWidget({ element, refreshInterval = 30000 }:
|
|||
{filteredVehicles.map((vehicle) => (
|
||||
<div
|
||||
key={vehicle.id}
|
||||
className="rounded-lg border border-gray-200 bg-white p-3 shadow-sm transition-all hover:shadow-md"
|
||||
className="rounded-lg border border-border bg-background p-3 shadow-sm transition-all hover:shadow-md"
|
||||
>
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Truck className="h-4 w-4 text-gray-600" />
|
||||
<span className="font-semibold text-gray-900">{vehicle.vehicle_name}</span>
|
||||
<Truck className="h-4 w-4 text-foreground" />
|
||||
<span className="font-semibold text-foreground">{vehicle.vehicle_name}</span>
|
||||
</div>
|
||||
<span
|
||||
className={`rounded-full px-2 py-0.5 text-xs font-semibold text-white ${getStatusColor(vehicle.status)}`}
|
||||
|
|
@ -178,22 +178,22 @@ export default function VehicleListWidget({ element, refreshInterval = 30000 }:
|
|||
</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1 text-xs text-gray-600">
|
||||
<div className="space-y-1 text-xs text-foreground">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-500">차량번호</span>
|
||||
<span className="text-muted-foreground">차량번호</span>
|
||||
<span className="font-mono font-medium">{vehicle.vehicle_number}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-500">기사</span>
|
||||
<span className="text-muted-foreground">기사</span>
|
||||
<span className="font-medium">{vehicle.driver_name || "미배정"}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Navigation className="h-3 w-3 text-gray-400" />
|
||||
<span className="flex-1 truncate text-gray-700">{vehicle.destination || "대기 중"}</span>
|
||||
<Navigation className="h-3 w-3 text-muted-foreground" />
|
||||
<span className="flex-1 truncate text-foreground">{vehicle.destination || "대기 중"}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Gauge className="h-3 w-3 text-gray-400" />
|
||||
<span className="text-gray-700">{vehicle.speed || 0} km/h</span>
|
||||
<Gauge className="h-3 w-3 text-muted-foreground" />
|
||||
<span className="text-foreground">{vehicle.speed || 0} km/h</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -164,12 +164,12 @@ export default function VehicleMapOnlyWidget({ element, refreshInterval = 30000
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="h-full w-full bg-gradient-to-br from-slate-50 to-blue-50 p-4">
|
||||
<div className="h-full w-full bg-gradient-to-br from-background to-primary/10 p-4">
|
||||
{/* 헤더 */}
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-gray-900">🗺️ 차량 위치 지도</h3>
|
||||
<p className="text-xs text-gray-500">마지막 업데이트: {lastUpdate.toLocaleTimeString("ko-KR")}</p>
|
||||
<h3 className="text-lg font-bold text-foreground">🗺️ 차량 위치 지도</h3>
|
||||
<p className="text-xs text-muted-foreground">마지막 업데이트: {lastUpdate.toLocaleTimeString("ko-KR")}</p>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" onClick={loadVehicles} disabled={isLoading} className="h-8 w-8 p-0">
|
||||
<RefreshCw className={`h-4 w-4 ${isLoading ? "animate-spin" : ""}`} />
|
||||
|
|
@ -178,7 +178,7 @@ export default function VehicleMapOnlyWidget({ element, refreshInterval = 30000
|
|||
|
||||
{/* 지도 영역 - 브이월드 타일맵 */}
|
||||
<div className="h-[calc(100%-60px)]">
|
||||
<div className="relative z-0 h-full overflow-hidden rounded-lg border-2 border-gray-300 bg-white">
|
||||
<div className="relative z-0 h-full overflow-hidden rounded-lg border-2 border-border bg-background">
|
||||
<MapContainer
|
||||
key={`vehicle-map-${element.id}`}
|
||||
center={[36.5, 127.5]}
|
||||
|
|
@ -235,19 +235,19 @@ export default function VehicleMapOnlyWidget({ element, refreshInterval = 30000
|
|||
</MapContainer>
|
||||
|
||||
{/* 지도 정보 */}
|
||||
<div className="absolute top-2 right-2 z-[1000] rounded-lg bg-white/90 p-2 shadow-lg backdrop-blur-sm">
|
||||
<div className="text-xs text-gray-600">
|
||||
<div className="absolute top-2 right-2 z-[1000] rounded-lg bg-background/90 p-2 shadow-lg backdrop-blur-sm">
|
||||
<div className="text-xs text-foreground">
|
||||
<div className="mb-1 font-semibold">🗺️ 브이월드 (VWorld)</div>
|
||||
<div className="text-xs">국토교통부 공식 지도</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 차량 수 표시 또는 설정 안내 */}
|
||||
<div className="absolute bottom-2 left-2 z-[1000] rounded-lg bg-white/90 p-2 shadow-lg backdrop-blur-sm">
|
||||
<div className="absolute bottom-2 left-2 z-[1000] rounded-lg bg-background/90 p-2 shadow-lg backdrop-blur-sm">
|
||||
{vehicles.length > 0 ? (
|
||||
<div className="text-xs font-semibold text-gray-900">총 {vehicles.length}대 모니터링 중</div>
|
||||
<div className="text-xs font-semibold text-foreground">총 {vehicles.length}대 모니터링 중</div>
|
||||
) : (
|
||||
<div className="text-xs text-gray-600">데이터를 연결하세요</div>
|
||||
<div className="text-xs text-foreground">데이터를 연결하세요</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -124,15 +124,15 @@ export default function VehicleStatusWidget({ element, refreshInterval = 30000 }
|
|||
const activeRate = statusData.total > 0 ? ((statusData.active / statusData.total) * 100).toFixed(1) : "0";
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col overflow-hidden bg-gradient-to-br from-slate-50 to-green-50 p-2">
|
||||
<div className="flex h-full w-full flex-col overflow-hidden bg-gradient-to-br from-background to-success/10 p-2">
|
||||
{/* 헤더 */}
|
||||
<div className="mb-2 flex flex-shrink-0 items-center justify-between">
|
||||
<div className="flex-1">
|
||||
<h3 className="text-sm font-bold text-gray-900">📊 차량 상태 현황</h3>
|
||||
<h3 className="text-sm font-bold text-foreground">📊 차량 상태 현황</h3>
|
||||
{statusData.total > 0 ? (
|
||||
<p className="text-xs text-gray-500">{lastUpdate.toLocaleTimeString("ko-KR")}</p>
|
||||
<p className="text-xs text-muted-foreground">{lastUpdate.toLocaleTimeString("ko-KR")}</p>
|
||||
) : (
|
||||
<p className="text-xs text-orange-500">⚙️ 데이터 연결 필요</p>
|
||||
<p className="text-xs text-warning">⚙️ 데이터 연결 필요</p>
|
||||
)}
|
||||
</div>
|
||||
<Button variant="outline" size="sm" onClick={loadStatusData} disabled={isLoading} className="h-7 w-7 p-0">
|
||||
|
|
@ -143,15 +143,15 @@ export default function VehicleStatusWidget({ element, refreshInterval = 30000 }
|
|||
{/* 스크롤 가능한 콘텐츠 영역 */}
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{/* 총 차량 수 */}
|
||||
<div className="mb-1 rounded border border-gray-200 bg-white p-1.5 shadow-sm">
|
||||
<div className="mb-1 rounded border border-border bg-background p-1.5 shadow-sm">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-xs text-gray-600">총 차량</div>
|
||||
<div className="text-base font-bold text-gray-900">{statusData.total}대</div>
|
||||
<div className="text-xs text-foreground">총 차량</div>
|
||||
<div className="text-base font-bold text-foreground">{statusData.total}대</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-xs text-gray-600">가동률</div>
|
||||
<div className="flex items-center gap-0.5 text-sm font-bold text-green-600">{activeRate}%</div>
|
||||
<div className="text-xs text-foreground">가동률</div>
|
||||
<div className="flex items-center gap-0.5 text-sm font-bold text-success">{activeRate}%</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -159,39 +159,39 @@ export default function VehicleStatusWidget({ element, refreshInterval = 30000 }
|
|||
{/* 상태별 카드 */}
|
||||
<div className="grid grid-cols-2 gap-1.5">
|
||||
{/* 운행 중 */}
|
||||
<div className="rounded border-l-2 border-green-500 bg-white p-1.5 shadow-sm">
|
||||
<div className="rounded border-l-2 border-success bg-background p-1.5 shadow-sm">
|
||||
<div className="mb-0.5 flex items-center gap-1">
|
||||
<div className="h-1.5 w-1.5 rounded-full bg-green-500"></div>
|
||||
<div className="text-xs font-medium text-gray-600">운행</div>
|
||||
<div className="h-1.5 w-1.5 rounded-full bg-success"></div>
|
||||
<div className="text-xs font-medium text-foreground">운행</div>
|
||||
</div>
|
||||
<div className="text-lg font-bold text-green-600">{statusData.active}</div>
|
||||
<div className="text-lg font-bold text-success">{statusData.active}</div>
|
||||
</div>
|
||||
|
||||
{/* 대기 */}
|
||||
<div className="rounded border-l-2 border-yellow-500 bg-white p-1.5 shadow-sm">
|
||||
<div className="rounded border-l-2 border-warning bg-background p-1.5 shadow-sm">
|
||||
<div className="mb-0.5 flex items-center gap-1">
|
||||
<div className="h-1.5 w-1.5 rounded-full bg-yellow-500"></div>
|
||||
<div className="text-xs font-medium text-gray-600">대기</div>
|
||||
<div className="h-1.5 w-1.5 rounded-full bg-warning/100"></div>
|
||||
<div className="text-xs font-medium text-foreground">대기</div>
|
||||
</div>
|
||||
<div className="text-lg font-bold text-yellow-600">{statusData.inactive}</div>
|
||||
<div className="text-lg font-bold text-warning">{statusData.inactive}</div>
|
||||
</div>
|
||||
|
||||
{/* 정비 */}
|
||||
<div className="rounded border-l-2 border-orange-500 bg-white p-1.5 shadow-sm">
|
||||
<div className="rounded border-l-2 border-warning bg-background p-1.5 shadow-sm">
|
||||
<div className="mb-0.5 flex items-center gap-1">
|
||||
<div className="h-1.5 w-1.5 rounded-full bg-orange-500"></div>
|
||||
<div className="text-xs font-medium text-gray-600">정비</div>
|
||||
<div className="h-1.5 w-1.5 rounded-full bg-warning"></div>
|
||||
<div className="text-xs font-medium text-foreground">정비</div>
|
||||
</div>
|
||||
<div className="text-lg font-bold text-orange-600">{statusData.maintenance}</div>
|
||||
<div className="text-lg font-bold text-warning">{statusData.maintenance}</div>
|
||||
</div>
|
||||
|
||||
{/* 고장 */}
|
||||
<div className="rounded border-l-2 border-red-500 bg-white p-1.5 shadow-sm">
|
||||
<div className="rounded border-l-2 border-destructive bg-background p-1.5 shadow-sm">
|
||||
<div className="mb-0.5 flex items-center gap-1">
|
||||
<div className="h-1.5 w-1.5 rounded-full bg-red-500"></div>
|
||||
<div className="text-xs font-medium text-gray-600">고장</div>
|
||||
<div className="h-1.5 w-1.5 rounded-full bg-destructive"></div>
|
||||
<div className="text-xs font-medium text-foreground">고장</div>
|
||||
</div>
|
||||
<div className="text-lg font-bold text-red-600">{statusData.warning}</div>
|
||||
<div className="text-lg font-bold text-destructive">{statusData.warning}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -38,15 +38,15 @@ interface WeatherMapWidgetProps {
|
|||
const getWeatherIcon = (weatherMain: string) => {
|
||||
switch (weatherMain.toLowerCase()) {
|
||||
case "clear":
|
||||
return <Sun className="h-6 w-6 text-yellow-500" />;
|
||||
return <Sun className="h-6 w-6 text-warning" />;
|
||||
case "rain":
|
||||
return <CloudRain className="h-6 w-6 text-blue-500" />;
|
||||
return <CloudRain className="h-6 w-6 text-primary" />;
|
||||
case "snow":
|
||||
return <CloudSnow className="h-6 w-6 text-blue-300" />;
|
||||
return <CloudSnow className="h-6 w-6 text-primary/70" />;
|
||||
case "clouds":
|
||||
return <Cloud className="h-6 w-6 text-gray-400" />;
|
||||
return <Cloud className="h-6 w-6 text-muted-foreground" />;
|
||||
default:
|
||||
return <Wind className="h-6 w-6 text-gray-500" />;
|
||||
return <Wind className="h-6 w-6 text-muted-foreground" />;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -263,28 +263,28 @@ export default function WeatherWidget({
|
|||
const getWeatherIcon = (weatherMain: string) => {
|
||||
switch (weatherMain.toLowerCase()) {
|
||||
case 'clear':
|
||||
return <Sun className="h-12 w-12 text-yellow-500" />;
|
||||
return <Sun className="h-12 w-12 text-warning" />;
|
||||
case 'clouds':
|
||||
return <Cloud className="h-12 w-12 text-gray-400" />;
|
||||
return <Cloud className="h-12 w-12 text-muted-foreground" />;
|
||||
case 'rain':
|
||||
case 'drizzle':
|
||||
return <CloudRain className="h-12 w-12 text-blue-500" />;
|
||||
return <CloudRain className="h-12 w-12 text-primary" />;
|
||||
case 'snow':
|
||||
return <CloudSnow className="h-12 w-12 text-blue-300" />;
|
||||
return <CloudSnow className="h-12 w-12 text-primary/70" />;
|
||||
default:
|
||||
return <Cloud className="h-12 w-12 text-gray-400" />;
|
||||
return <Cloud className="h-12 w-12 text-muted-foreground" />;
|
||||
}
|
||||
};
|
||||
|
||||
// 로딩 상태
|
||||
if (loading && !weather) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center bg-gradient-to-br from-blue-50 to-indigo-50 rounded-lg border p-6">
|
||||
<div className="flex h-full items-center justify-center bg-background rounded-lg border p-6">
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<RefreshCw className="h-8 w-8 animate-spin text-blue-500" />
|
||||
<RefreshCw className="h-8 w-8 animate-spin text-primary" />
|
||||
<div className="text-center">
|
||||
<p className="text-sm font-semibold text-gray-800 mb-1">실제 기상청 API 연결 중...</p>
|
||||
<p className="text-xs text-gray-500">실시간 관측 데이터를 가져오고 있습니다</p>
|
||||
<p className="text-sm font-semibold text-foreground mb-1">실제 기상청 API 연결 중...</p>
|
||||
<p className="text-xs text-muted-foreground">실시간 관측 데이터를 가져오고 있습니다</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -295,21 +295,17 @@ export default function WeatherWidget({
|
|||
if (error || !weather) {
|
||||
const isTestMode = error?.includes('API 키가 설정되지 않았습니다');
|
||||
return (
|
||||
<div className={`flex h-full flex-col items-center justify-center rounded-lg border p-6 ${
|
||||
isTestMode
|
||||
? 'bg-gradient-to-br from-yellow-50 to-orange-50'
|
||||
: 'bg-gradient-to-br from-red-50 to-orange-50'
|
||||
}`}>
|
||||
<Cloud className="h-12 w-12 text-gray-400 mb-2" />
|
||||
<div className="flex h-full flex-col items-center justify-center rounded-lg border p-6 bg-background">
|
||||
<Cloud className="h-12 w-12 text-muted-foreground mb-2" />
|
||||
<div className="text-center mb-3">
|
||||
<p className="text-sm font-semibold text-gray-800 mb-1">
|
||||
<p className="text-sm font-semibold text-foreground mb-1">
|
||||
{isTestMode ? '⚠️ 테스트 모드' : '❌ 연결 실패'}
|
||||
</p>
|
||||
<p className="text-xs text-gray-600">
|
||||
<p className="text-xs text-foreground">
|
||||
{error || '날씨 정보를 불러올 수 없습니다.'}
|
||||
</p>
|
||||
{isTestMode && (
|
||||
<p className="text-xs text-yellow-700 mt-2">
|
||||
<p className="text-xs text-warning mt-2">
|
||||
임시 데이터가 표시됩니다
|
||||
</p>
|
||||
)}
|
||||
|
|
@ -328,11 +324,11 @@ export default function WeatherWidget({
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="h-full bg-gradient-to-br from-blue-50 to-indigo-50 rounded-lg border p-4">
|
||||
<div className="h-full bg-background rounded-lg border p-4">
|
||||
{/* 헤더 */}
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex-1">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-1">🌤️ {element?.customTitle || "날씨"}</h3>
|
||||
<h3 className="text-lg font-semibold text-foreground mb-1">🌤️ {element?.customTitle || "날씨"}</h3>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
|
|
@ -340,7 +336,7 @@ export default function WeatherWidget({
|
|||
variant="ghost"
|
||||
role="combobox"
|
||||
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-sm text-foreground hover:bg-muted/80 h-auto py-0.5 px-2"
|
||||
>
|
||||
{cities.find((city) => city.value === selectedCity)?.label || '도시 선택'}
|
||||
<ChevronsUpDown className="ml-2 h-3.5 w-3.5 shrink-0 opacity-50" />
|
||||
|
|
@ -376,7 +372,7 @@ export default function WeatherWidget({
|
|||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 pl-2">
|
||||
<p className="text-xs text-muted-foreground pl-2">
|
||||
{lastUpdated
|
||||
? `업데이트: ${lastUpdated.toLocaleTimeString('ko-KR', {
|
||||
hour: '2-digit',
|
||||
|
|
@ -397,7 +393,7 @@ export default function WeatherWidget({
|
|||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[200px] p-3" align="end">
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-sm font-semibold text-gray-900 mb-3">표시 항목</h4>
|
||||
<h4 className="text-sm font-semibold text-foreground mb-3">표시 항목</h4>
|
||||
{weatherItems.map((item) => {
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
|
|
@ -407,8 +403,8 @@ export default function WeatherWidget({
|
|||
className={cn(
|
||||
'w-full flex items-center gap-2 px-2 py-1.5 rounded text-xs transition-colors',
|
||||
selectedItems.includes(item.id)
|
||||
? 'bg-blue-50 text-blue-700'
|
||||
: 'text-gray-600 hover:bg-gray-50'
|
||||
? 'bg-primary/10 text-primary'
|
||||
: 'text-foreground hover:bg-muted'
|
||||
)}
|
||||
>
|
||||
<Check
|
||||
|
|
@ -439,31 +435,31 @@ export default function WeatherWidget({
|
|||
{/* 반응형 그리드 레이아웃 - 자동 조정 */}
|
||||
<div className="grid gap-2" style={{ gridTemplateColumns: 'repeat(auto-fit, minmax(150px, 1fr))' }}>
|
||||
{/* 날씨 아이콘 및 온도 */}
|
||||
<div className="bg-white/50 rounded-lg p-3">
|
||||
<div className="bg-muted/80 rounded-lg p-3">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="flex-shrink-0">
|
||||
{(() => {
|
||||
const iconClass = "h-5 w-5";
|
||||
switch (weather.weatherMain.toLowerCase()) {
|
||||
case 'clear':
|
||||
return <Sun className={`${iconClass} text-yellow-500`} />;
|
||||
return <Sun className={`${iconClass} text-warning`} />;
|
||||
case 'clouds':
|
||||
return <Cloud className={`${iconClass} text-gray-400`} />;
|
||||
return <Cloud className={`${iconClass} text-muted-foreground`} />;
|
||||
case 'rain':
|
||||
case 'drizzle':
|
||||
return <CloudRain className={`${iconClass} text-blue-500`} />;
|
||||
return <CloudRain className={`${iconClass} text-primary`} />;
|
||||
case 'snow':
|
||||
return <CloudSnow className={`${iconClass} text-blue-300`} />;
|
||||
return <CloudSnow className={`${iconClass} text-primary/70`} />;
|
||||
default:
|
||||
return <Cloud className={`${iconClass} text-gray-400`} />;
|
||||
return <Cloud className={`${iconClass} text-muted-foreground`} />;
|
||||
}
|
||||
})()}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-sm font-bold text-gray-900 leading-tight truncate">
|
||||
<div className="text-sm font-bold text-foreground leading-tight truncate">
|
||||
{weather.temperature}°C
|
||||
</div>
|
||||
<p className="text-xs text-gray-400 capitalize leading-tight truncate">
|
||||
<p className="text-xs text-muted-foreground capitalize leading-tight truncate">
|
||||
{weather.weatherDescription}
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -472,11 +468,11 @@ export default function WeatherWidget({
|
|||
|
||||
{/* 기온 - 선택 가능 */}
|
||||
{selectedItems.includes('temperature') && (
|
||||
<div className="flex items-center gap-1.5 bg-white/50 rounded-lg p-3">
|
||||
<Sun className="h-3.5 w-3.5 text-orange-500 flex-shrink-0" />
|
||||
<div className="flex items-center gap-1.5 bg-muted/80 rounded-lg p-3">
|
||||
<Sun className="h-3.5 w-3.5 text-warning flex-shrink-0" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-xs text-gray-400 leading-tight truncate">기온</p>
|
||||
<p className="text-sm font-semibold text-gray-900 leading-tight truncate">
|
||||
<p className="text-xs text-muted-foreground leading-tight truncate">기온</p>
|
||||
<p className="text-sm font-semibold text-foreground leading-tight truncate">
|
||||
{weather.temperature}°C
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -485,11 +481,11 @@ export default function WeatherWidget({
|
|||
|
||||
{/* 체감 온도 */}
|
||||
{selectedItems.includes('feelsLike') && (
|
||||
<div className="flex items-center gap-1.5 bg-white/50 rounded-lg p-3">
|
||||
<Wind className="h-3.5 w-3.5 text-blue-500 flex-shrink-0" />
|
||||
<div className="flex items-center gap-1.5 bg-muted/80 rounded-lg p-3">
|
||||
<Wind className="h-3.5 w-3.5 text-primary flex-shrink-0" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-xs text-gray-400 leading-tight truncate">체감온도</p>
|
||||
<p className="text-sm font-semibold text-gray-900 leading-tight truncate">
|
||||
<p className="text-xs text-muted-foreground leading-tight truncate">체감온도</p>
|
||||
<p className="text-sm font-semibold text-foreground leading-tight truncate">
|
||||
{weather.feelsLike}°C
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -498,11 +494,11 @@ export default function WeatherWidget({
|
|||
|
||||
{/* 습도 */}
|
||||
{selectedItems.includes('humidity') && (
|
||||
<div className="flex items-center gap-1.5 bg-white/50 rounded-lg p-3">
|
||||
<Droplets className="h-3.5 w-3.5 text-blue-500 flex-shrink-0" />
|
||||
<div className="flex items-center gap-1.5 bg-muted/80 rounded-lg p-3">
|
||||
<Droplets className="h-3.5 w-3.5 text-primary flex-shrink-0" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-xs text-gray-400 leading-tight truncate">습도</p>
|
||||
<p className="text-sm font-semibold text-gray-900 leading-tight truncate">
|
||||
<p className="text-xs text-muted-foreground leading-tight truncate">습도</p>
|
||||
<p className="text-sm font-semibold text-foreground leading-tight truncate">
|
||||
{weather.humidity}%
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -511,11 +507,11 @@ export default function WeatherWidget({
|
|||
|
||||
{/* 풍속 */}
|
||||
{selectedItems.includes('windSpeed') && (
|
||||
<div className="flex items-center gap-1.5 bg-white/50 rounded-lg p-3">
|
||||
<Wind className="h-3.5 w-3.5 text-green-500 flex-shrink-0" />
|
||||
<div className="flex items-center gap-1.5 bg-muted/80 rounded-lg p-3">
|
||||
<Wind className="h-3.5 w-3.5 text-success flex-shrink-0" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-xs text-gray-400 leading-tight truncate">풍속</p>
|
||||
<p className="text-sm font-semibold text-gray-900 leading-tight truncate">
|
||||
<p className="text-xs text-muted-foreground leading-tight truncate">풍속</p>
|
||||
<p className="text-sm font-semibold text-foreground leading-tight truncate">
|
||||
{weather.windSpeed} m/s
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -524,11 +520,11 @@ export default function WeatherWidget({
|
|||
|
||||
{/* 기압 */}
|
||||
{selectedItems.includes('pressure') && (
|
||||
<div className="flex items-center gap-1.5 bg-white/50 rounded-lg p-3">
|
||||
<div className="flex items-center gap-1.5 bg-muted/80 rounded-lg p-3">
|
||||
<Gauge className="h-3.5 w-3.5 text-purple-500 flex-shrink-0" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-xs text-gray-400 leading-tight truncate">기압</p>
|
||||
<p className="text-sm font-semibold text-gray-900 leading-tight truncate">
|
||||
<p className="text-xs text-muted-foreground leading-tight truncate">기압</p>
|
||||
<p className="text-sm font-semibold text-foreground leading-tight truncate">
|
||||
{weather.pressure} hPa
|
||||
</p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -77,10 +77,10 @@ export default function WorkHistoryWidget({ element, refreshInterval = 60000 }:
|
|||
|
||||
if (isLoading && data.length === 0) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center bg-gray-50">
|
||||
<div className="flex h-full items-center justify-center bg-muted">
|
||||
<div className="text-center">
|
||||
<div className="mx-auto h-8 w-8 animate-spin rounded-full border-4 border-blue-500 border-t-transparent" />
|
||||
<div className="mt-2 text-sm text-gray-600">로딩 중...</div>
|
||||
<div className="mx-auto h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent" />
|
||||
<div className="mt-2 text-sm text-foreground">로딩 중...</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -88,14 +88,14 @@ export default function WorkHistoryWidget({ element, refreshInterval = 60000 }:
|
|||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center bg-gray-50 p-4">
|
||||
<div className="flex h-full items-center justify-center bg-muted p-4">
|
||||
<div className="text-center">
|
||||
<div className="mb-2 text-4xl">⚠️</div>
|
||||
<div className="text-sm font-medium text-gray-600">{error}</div>
|
||||
{!element.dataSource?.query && <div className="mt-2 text-xs text-gray-500">쿼리를 설정하세요</div>}
|
||||
<div className="text-sm font-medium text-foreground">{error}</div>
|
||||
{!element.dataSource?.query && <div className="mt-2 text-xs text-muted-foreground">쿼리를 설정하세요</div>}
|
||||
<button
|
||||
onClick={loadData}
|
||||
className="mt-3 rounded-lg bg-blue-500 px-4 py-2 text-sm text-white hover:bg-blue-600"
|
||||
className="mt-3 rounded-lg bg-primary px-4 py-2 text-sm text-white hover:bg-primary/90"
|
||||
>
|
||||
다시 시도
|
||||
</button>
|
||||
|
|
@ -105,7 +105,7 @@ export default function WorkHistoryWidget({ element, refreshInterval = 60000 }:
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col bg-white">
|
||||
<div className="flex h-full flex-col bg-background">
|
||||
{/* 필터 */}
|
||||
<div className="flex gap-2 border-b p-3">
|
||||
<select
|
||||
|
|
@ -134,7 +134,7 @@ export default function WorkHistoryWidget({ element, refreshInterval = 60000 }:
|
|||
|
||||
<button
|
||||
onClick={loadData}
|
||||
className="ml-auto rounded bg-blue-500 px-3 py-1 text-sm text-white hover:bg-blue-600"
|
||||
className="ml-auto rounded bg-primary px-3 py-1 text-sm text-white hover:bg-primary/90"
|
||||
>
|
||||
🔄 새로고침
|
||||
</button>
|
||||
|
|
@ -143,7 +143,7 @@ export default function WorkHistoryWidget({ element, refreshInterval = 60000 }:
|
|||
{/* 테이블 */}
|
||||
<div className="flex-1 overflow-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="sticky top-0 bg-gray-50 text-left">
|
||||
<thead className="sticky top-0 bg-muted text-left">
|
||||
<tr>
|
||||
<th className="border-b px-3 py-2 font-medium">작업번호</th>
|
||||
<th className="border-b px-3 py-2 font-medium">일시</th>
|
||||
|
|
@ -158,7 +158,7 @@ export default function WorkHistoryWidget({ element, refreshInterval = 60000 }:
|
|||
<tbody>
|
||||
{data.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={8} className="py-8 text-center text-gray-500">
|
||||
<td colSpan={8} className="py-8 text-center text-muted-foreground">
|
||||
작업 이력이 없습니다
|
||||
</td>
|
||||
</tr>
|
||||
|
|
@ -167,7 +167,7 @@ export default function WorkHistoryWidget({ element, refreshInterval = 60000 }:
|
|||
.filter((item) => selectedType === "all" || item.work_type === selectedType)
|
||||
.filter((item) => selectedStatus === "all" || item.status === selectedStatus)
|
||||
.map((item, index) => (
|
||||
<tr key={item.id || index} className="border-b hover:bg-gray-50">
|
||||
<tr key={item.id || index} className="border-b hover:bg-muted">
|
||||
<td className="px-3 py-2 font-mono text-xs">{item.work_number}</td>
|
||||
<td className="px-3 py-2">
|
||||
{item.work_date
|
||||
|
|
@ -180,7 +180,7 @@ export default function WorkHistoryWidget({ element, refreshInterval = 60000 }:
|
|||
: "-"}
|
||||
</td>
|
||||
<td className="px-3 py-2">
|
||||
<span className="rounded bg-blue-100 px-2 py-1 text-xs font-medium text-blue-800">
|
||||
<span className="rounded bg-primary/10 px-2 py-1 text-xs font-medium text-primary">
|
||||
{WORK_TYPE_LABELS[item.work_type as WorkType] || item.work_type}
|
||||
</span>
|
||||
</td>
|
||||
|
|
@ -194,7 +194,7 @@ export default function WorkHistoryWidget({ element, refreshInterval = 60000 }:
|
|||
</td>
|
||||
<td className="px-3 py-2">
|
||||
<span
|
||||
className={`rounded px-2 py-1 text-xs font-medium ${WORK_STATUS_COLORS[item.status as WorkStatus] || "bg-gray-100 text-gray-800"}`}
|
||||
className={`rounded px-2 py-1 text-xs font-medium ${WORK_STATUS_COLORS[item.status as WorkStatus] || "bg-muted text-foreground"}`}
|
||||
>
|
||||
{WORK_STATUS_LABELS[item.status as WorkStatus] || item.status}
|
||||
</span>
|
||||
|
|
@ -207,7 +207,7 @@ export default function WorkHistoryWidget({ element, refreshInterval = 60000 }:
|
|||
</div>
|
||||
|
||||
{/* 푸터 */}
|
||||
<div className="border-t bg-gray-50 px-3 py-2 text-xs text-gray-600">전체 {data.length}건</div>
|
||||
<div className="border-t bg-muted px-3 py-2 text-xs text-foreground">전체 {data.length}건</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue