ERP-node/frontend/components/admin/dashboard/VehicleMapConfigPanel.tsx

442 lines
19 KiB
TypeScript
Raw Normal View History

2025-10-28 13:38:22 +09:00
"use client";
2025-10-28 13:38:22 +09:00
import React, { useState, useCallback } from "react";
import { ChartConfig, QueryResult, MarkerColorRule } from "./types";
import { Plus, Trash2 } from "lucide-react";
import { v4 as uuidv4 } from "uuid";
interface VehicleMapConfigPanelProps {
config?: ChartConfig;
queryResult?: QueryResult;
onConfigChange: (config: ChartConfig) => void;
}
/**
*
* - /
* - /
*/
export function VehicleMapConfigPanel({ config, queryResult, onConfigChange }: VehicleMapConfigPanelProps) {
const [currentConfig, setCurrentConfig] = useState<ChartConfig>(config || {});
// 설정 업데이트
2025-10-28 13:38:22 +09:00
const updateConfig = useCallback(
(updates: Partial<ChartConfig>) => {
const newConfig = { ...currentConfig, ...updates };
setCurrentConfig(newConfig);
onConfigChange(newConfig);
},
[currentConfig, onConfigChange],
);
// 사용 가능한 컬럼 목록
const availableColumns = queryResult?.columns || [];
const sampleData = queryResult?.rows?.[0] || {};
2025-10-28 13:38:22 +09:00
// 마커 색상 모드 변경
const handleMarkerColorModeChange = useCallback(
(mode: "single" | "conditional") => {
if (mode === "single") {
updateConfig({
markerColorMode: "single",
markerColorColumn: undefined,
markerColorRules: undefined,
markerDefaultColor: "#3b82f6", // 파란색
});
} else {
updateConfig({
markerColorMode: "conditional",
markerColorRules: [],
markerDefaultColor: "#6b7280", // 회색
});
}
},
[updateConfig],
);
// 마커 색상 규칙 추가
const addColorRule = useCallback(() => {
const newRule: MarkerColorRule = {
id: uuidv4(),
value: "",
color: "#3b82f6",
label: "",
};
const currentRules = currentConfig.markerColorRules || [];
updateConfig({ markerColorRules: [...currentRules, newRule] });
}, [currentConfig.markerColorRules, updateConfig]);
// 마커 색상 규칙 삭제
const deleteColorRule = useCallback(
(id: string) => {
const currentRules = currentConfig.markerColorRules || [];
updateConfig({ markerColorRules: currentRules.filter((rule) => rule.id !== id) });
},
[currentConfig.markerColorRules, updateConfig],
);
// 마커 색상 규칙 업데이트
const updateColorRule = useCallback(
(id: string, updates: Partial<MarkerColorRule>) => {
const currentRules = currentConfig.markerColorRules || [];
updateConfig({
markerColorRules: currentRules.map((rule) => (rule.id === id ? { ...rule, ...updates } : rule)),
});
},
[currentConfig.markerColorRules, updateConfig],
);
return (
2025-10-22 13:40:15 +09:00
<div className="space-y-3">
2025-10-29 17:53:03 +09:00
<h4 className="text-xs font-semibold text-foreground">🗺 </h4>
{/* 쿼리 결과가 없을 때 */}
{!queryResult && (
2025-10-29 17:53:03 +09:00
<div className="rounded-lg border border-warning bg-warning/10 p-3">
<div className="text-xs text-warning">
💡 SQL .
</div>
</div>
)}
{/* 데이터 필드 매핑 */}
{queryResult && (
<>
{/* 지도 제목 */}
2025-10-22 13:40:15 +09:00
<div className="space-y-1.5">
2025-10-29 17:53:03 +09:00
<label className="block text-xs font-medium text-foreground"> </label>
<input
type="text"
2025-10-28 13:38:22 +09:00
value={currentConfig.title || ""}
onChange={(e) => updateConfig({ title: e.target.value })}
placeholder="차량 위치 지도"
2025-10-29 17:53:03 +09:00
className="w-full rounded-lg border border-border px-2 py-1.5 text-xs"
/>
</div>
{/* 위도 컬럼 설정 */}
2025-10-22 13:40:15 +09:00
<div className="space-y-1.5">
2025-10-29 17:53:03 +09:00
<label className="block text-xs font-medium text-foreground">
(Latitude)
2025-10-29 17:53:03 +09:00
<span className="ml-1 text-destructive">*</span>
</label>
<select
2025-10-28 13:38:22 +09:00
value={currentConfig.latitudeColumn || ""}
onChange={(e) => updateConfig({ latitudeColumn: e.target.value })}
2025-10-29 17:53:03 +09:00
className="w-full rounded-lg border border-border px-2 py-1.5 text-xs"
>
<option value=""></option>
{availableColumns.map((col) => (
<option key={col} value={col}>
{col} {sampleData[col] && `(예: ${sampleData[col]})`}
</option>
))}
</select>
</div>
{/* 경도 컬럼 설정 */}
2025-10-22 13:40:15 +09:00
<div className="space-y-1.5">
2025-10-29 17:53:03 +09:00
<label className="block text-xs font-medium text-foreground">
(Longitude)
2025-10-29 17:53:03 +09:00
<span className="ml-1 text-destructive">*</span>
</label>
<select
2025-10-28 13:38:22 +09:00
value={currentConfig.longitudeColumn || ""}
onChange={(e) => updateConfig({ longitudeColumn: e.target.value })}
2025-10-29 17:53:03 +09:00
className="w-full rounded-lg border border-border px-2 py-1.5 text-xs"
>
<option value=""></option>
{availableColumns.map((col) => (
<option key={col} value={col}>
{col} {sampleData[col] && `(예: ${sampleData[col]})`}
</option>
))}
</select>
</div>
{/* 라벨 컬럼 (선택사항) */}
2025-10-22 13:40:15 +09:00
<div className="space-y-1.5">
2025-10-29 17:53:03 +09:00
<label className="block text-xs font-medium text-foreground"> ( )</label>
<select
2025-10-28 13:38:22 +09:00
value={currentConfig.labelColumn || ""}
onChange={(e) => updateConfig({ labelColumn: e.target.value })}
2025-10-29 17:53:03 +09:00
className="w-full rounded-lg border border-border px-2 py-1.5 text-xs"
>
<option value=""> ()</option>
{availableColumns.map((col) => (
<option key={col} value={col}>
{col}
</option>
))}
</select>
</div>
2025-10-28 13:38:22 +09:00
{/* 마커 색상 설정 */}
<div className="space-y-2 border-t pt-3">
2025-10-29 17:53:03 +09:00
<h5 className="text-xs font-semibold text-foreground">🎨 </h5>
2025-10-28 13:38:22 +09:00
{/* 색상 모드 선택 */}
<div className="space-y-1.5">
2025-10-29 17:53:03 +09:00
<label className="block text-xs font-medium text-foreground"> </label>
2025-10-28 13:38:22 +09:00
<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"
2025-10-29 17:53:03 +09:00
? "border-primary bg-primary/10 font-medium text-primary"
: "border-border bg-background text-foreground hover:bg-muted"
2025-10-28 13:38:22 +09:00
}`}
>
</button>
<button
type="button"
onClick={() => handleMarkerColorModeChange("conditional")}
className={`flex-1 rounded-lg border px-3 py-2 text-xs transition-colors ${
currentConfig.markerColorMode === "conditional"
2025-10-29 17:53:03 +09:00
? "border-primary bg-primary/10 font-medium text-primary"
: "border-border bg-background text-foreground hover:bg-muted"
2025-10-28 13:38:22 +09:00
}`}
>
</button>
</div>
</div>
{/* 단일 색상 모드 */}
{(currentConfig.markerColorMode || "single") === "single" && (
2025-10-29 17:53:03 +09:00
<div className="space-y-1.5 rounded-lg bg-muted p-3">
<label className="block text-xs font-medium text-foreground"> </label>
2025-10-28 13:38:22 +09:00
<div className="flex items-center gap-2">
<input
type="color"
value={currentConfig.markerDefaultColor || "#3b82f6"}
onChange={(e) => updateConfig({ markerDefaultColor: e.target.value })}
2025-10-29 17:53:03 +09:00
className="h-8 w-12 cursor-pointer rounded border border-border"
2025-10-28 13:38:22 +09:00
/>
<input
type="text"
value={currentConfig.markerDefaultColor || "#3b82f6"}
onChange={(e) => updateConfig({ markerDefaultColor: e.target.value })}
placeholder="#3b82f6"
2025-10-29 17:53:03 +09:00
className="flex-1 rounded-lg border border-border px-2 py-1.5 text-xs"
2025-10-28 13:38:22 +09:00
/>
</div>
2025-10-29 17:53:03 +09:00
<p className="text-xs text-muted-foreground"> </p>
2025-10-28 13:38:22 +09:00
</div>
)}
{/* 조건부 색상 모드 */}
{currentConfig.markerColorMode === "conditional" && (
2025-10-29 17:53:03 +09:00
<div className="space-y-2 rounded-lg bg-muted p-3">
2025-10-28 13:38:22 +09:00
{/* 색상 조건 컬럼 선택 */}
<div className="space-y-1.5">
2025-10-29 17:53:03 +09:00
<label className="block text-xs font-medium text-foreground">
2025-10-28 13:38:22 +09:00
2025-10-29 17:53:03 +09:00
<span className="ml-1 text-destructive">*</span>
2025-10-28 13:38:22 +09:00
</label>
<select
value={currentConfig.markerColorColumn || ""}
onChange={(e) => updateConfig({ markerColorColumn: e.target.value })}
2025-10-29 17:53:03 +09:00
className="w-full rounded-lg border border-border px-2 py-1.5 text-xs"
2025-10-28 13:38:22 +09:00
>
<option value=""></option>
{availableColumns.map((col) => (
<option key={col} value={col}>
{col}
</option>
))}
</select>
2025-10-29 17:53:03 +09:00
<p className="text-xs text-muted-foreground"> </p>
2025-10-28 13:38:22 +09:00
</div>
{/* 기본 색상 */}
<div className="space-y-1.5">
2025-10-29 17:53:03 +09:00
<label className="block text-xs font-medium text-foreground"> </label>
2025-10-28 13:38:22 +09:00
<div className="flex items-center gap-2">
<input
type="color"
value={currentConfig.markerDefaultColor || "#6b7280"}
onChange={(e) => updateConfig({ markerDefaultColor: e.target.value })}
2025-10-29 17:53:03 +09:00
className="h-8 w-12 cursor-pointer rounded border border-border"
2025-10-28 13:38:22 +09:00
/>
<input
type="text"
value={currentConfig.markerDefaultColor || "#6b7280"}
onChange={(e) => updateConfig({ markerDefaultColor: e.target.value })}
placeholder="#6b7280"
2025-10-29 17:53:03 +09:00
className="flex-1 rounded-lg border border-border px-2 py-1.5 text-xs"
2025-10-28 13:38:22 +09:00
/>
</div>
2025-10-29 17:53:03 +09:00
<p className="text-xs text-muted-foreground"> </p>
2025-10-28 13:38:22 +09:00
</div>
{/* 색상 규칙 목록 */}
<div className="space-y-2">
<div className="flex items-center justify-between">
2025-10-29 17:53:03 +09:00
<label className="block text-xs font-medium text-foreground"> </label>
2025-10-28 13:38:22 +09:00
<button
type="button"
onClick={addColorRule}
2025-10-29 17:53:03 +09:00
className="flex items-center gap-1 rounded-lg bg-primary px-2 py-1 text-xs text-white transition-colors hover:bg-primary/90"
2025-10-28 13:38:22 +09:00
>
<Plus className="h-3 w-3" />
</button>
</div>
{/* 규칙 리스트 */}
{(currentConfig.markerColorRules || []).length === 0 ? (
2025-10-29 17:53:03 +09:00
<div className="rounded-lg border border-border bg-background p-3 text-center">
<p className="text-xs text-muted-foreground"> </p>
2025-10-28 13:38:22 +09:00
</div>
) : (
<div className="space-y-2">
{(currentConfig.markerColorRules || []).map((rule) => (
2025-10-29 17:53:03 +09:00
<div key={rule.id} className="space-y-2 rounded-lg border border-border bg-background p-2">
2025-10-28 13:38:22 +09:00
{/* 규칙 헤더 */}
<div className="flex items-center justify-between">
2025-10-29 17:53:03 +09:00
<span className="text-xs font-medium text-foreground"></span>
2025-10-28 13:38:22 +09:00
<button
type="button"
onClick={() => deleteColorRule(rule.id)}
2025-10-29 17:53:03 +09:00
className="text-destructive transition-colors hover:text-destructive"
2025-10-28 13:38:22 +09:00
>
<Trash2 className="h-3 w-3" />
</button>
</div>
{/* 조건 값 */}
<div className="space-y-1">
2025-10-29 17:53:03 +09:00
<label className="block text-xs font-medium text-foreground"> ()</label>
2025-10-28 13:38:22 +09:00
<input
type="text"
value={rule.value}
onChange={(e) => updateColorRule(rule.id, { value: e.target.value })}
placeholder="예: active, inactive"
2025-10-29 17:53:03 +09:00
className="w-full rounded border border-border px-2 py-1 text-xs"
2025-10-28 13:38:22 +09:00
/>
</div>
{/* 색상 */}
<div className="space-y-1">
2025-10-29 17:53:03 +09:00
<label className="block text-xs font-medium text-foreground"></label>
2025-10-28 13:38:22 +09:00
<div className="flex items-center gap-2">
<input
type="color"
value={rule.color}
onChange={(e) => updateColorRule(rule.id, { color: e.target.value })}
2025-10-29 17:53:03 +09:00
className="h-8 w-12 cursor-pointer rounded border border-border"
2025-10-28 13:38:22 +09:00
/>
<input
type="text"
value={rule.color}
onChange={(e) => updateColorRule(rule.id, { color: e.target.value })}
placeholder="#3b82f6"
2025-10-29 17:53:03 +09:00
className="flex-1 rounded border border-border px-2 py-1 text-xs"
2025-10-28 13:38:22 +09:00
/>
</div>
</div>
{/* 라벨 (선택사항) */}
<div className="space-y-1">
2025-10-29 17:53:03 +09:00
<label className="block text-xs font-medium text-foreground"> ()</label>
2025-10-28 13:38:22 +09:00
<input
type="text"
value={rule.label || ""}
onChange={(e) => updateColorRule(rule.id, { label: e.target.value })}
placeholder="예: 활성, 비활성"
2025-10-29 17:53:03 +09:00
className="w-full rounded border border-border px-2 py-1 text-xs"
2025-10-28 13:38:22 +09:00
/>
</div>
</div>
))}
</div>
)}
</div>
</div>
)}
</div>
2025-10-23 13:17:21 +09:00
{/* 날씨 정보 표시 옵션 */}
2025-10-28 13:38:22 +09:00
<div className="space-y-1.5">
2025-10-29 17:53:03 +09:00
<label className="flex cursor-pointer items-center gap-2 text-xs font-medium text-foreground">
2025-10-28 13:38:22 +09:00
<input
type="checkbox"
checked={currentConfig.showWeather || false}
onChange={(e) => updateConfig({ showWeather: e.target.checked })}
2025-10-29 17:53:03 +09:00
className="text-primary focus:ring-primary h-4 w-4 rounded border-border focus:ring-2"
2025-10-28 13:38:22 +09:00
/>
<span> </span>
</label>
2025-10-29 17:53:03 +09:00
<p className="ml-6 text-xs text-muted-foreground"> </p>
2025-10-28 13:38:22 +09:00
</div>
2025-10-23 13:17:21 +09:00
2025-10-28 13:38:22 +09:00
<div className="space-y-1.5">
2025-10-29 17:53:03 +09:00
<label className="flex cursor-pointer items-center gap-2 text-xs font-medium text-foreground">
2025-10-28 13:38:22 +09:00
<input
type="checkbox"
checked={currentConfig.showWeatherAlerts || false}
onChange={(e) => updateConfig({ showWeatherAlerts: e.target.checked })}
2025-10-29 17:53:03 +09:00
className="text-primary focus:ring-primary h-4 w-4 rounded border-border focus:ring-2"
2025-10-28 13:38:22 +09:00
/>
<span> </span>
</label>
2025-10-29 17:53:03 +09:00
<p className="ml-6 text-xs text-muted-foreground">
2025-10-28 13:38:22 +09:00
(/)
</p>
</div>
2025-10-23 13:17:21 +09:00
{/* 설정 미리보기 */}
2025-10-29 17:53:03 +09:00
<div className="rounded-lg bg-muted p-3">
<div className="mb-2 text-xs font-medium text-foreground">📋 </div>
2025-10-28 13:38:22 +09:00
<div className="text-muted-foreground space-y-1 text-xs">
<div>
<strong>:</strong> {currentConfig.latitudeColumn || "미설정"}
</div>
<div>
<strong>:</strong> {currentConfig.longitudeColumn || "미설정"}
</div>
<div>
<strong>:</strong> {currentConfig.labelColumn || "없음"}
</div>
<div>
<strong> :</strong> {currentConfig.markerColorMode === "conditional" ? "조건부" : "단일"}
</div>
{currentConfig.markerColorMode === "conditional" && (
<>
<div>
<strong> :</strong> {currentConfig.markerColorColumn || "미설정"}
</div>
<div>
<strong> :</strong> {(currentConfig.markerColorRules || []).length}
</div>
</>
)}
<div>
<strong> :</strong> {currentConfig.showWeather ? "활성화" : "비활성화"}
</div>
<div>
<strong> :</strong> {currentConfig.showWeatherAlerts ? "활성화" : "비활성화"}
</div>
<div>
<strong> :</strong> {queryResult.rows.length}
</div>
</div>
</div>
{/* 필수 필드 확인 */}
{(!currentConfig.latitudeColumn || !currentConfig.longitudeColumn) && (
2025-10-29 17:53:03 +09:00
<div className="rounded-lg border border-destructive bg-destructive/10 p-3">
<div className="text-xs text-destructive">
.
</div>
</div>
)}
</>
)}
</div>
);
}