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

442 lines
19 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
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 || {});
// 설정 업데이트
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] || {};
// 마커 색상 모드 변경
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 (
<div className="space-y-3">
<h4 className="text-foreground text-xs font-semibold">🗺 </h4>
{/* 쿼리 결과가 없을 때 */}
{!queryResult && (
<div className="border-warning bg-warning/10 rounded-lg border p-3">
<div className="text-warning text-xs">
💡 SQL .
</div>
</div>
)}
{/* 데이터 필드 매핑 */}
{queryResult && (
<>
{/* 지도 제목 */}
<div className="space-y-1.5">
<label className="text-foreground block text-xs font-medium"> </label>
<input
type="text"
value={currentConfig.title || ""}
onChange={(e) => updateConfig({ title: e.target.value })}
placeholder="차량 위치 지도"
className="border-border w-full rounded-lg border px-2 py-1.5 text-xs"
/>
</div>
{/* 위도 컬럼 설정 */}
<div className="space-y-1.5">
<label className="text-foreground block text-xs font-medium">
(Latitude)
<span className="text-destructive ml-1">*</span>
</label>
<select
value={currentConfig.latitudeColumn || ""}
onChange={(e) => updateConfig({ latitudeColumn: e.target.value })}
className="border-border w-full rounded-lg 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>
{/* 경도 컬럼 설정 */}
<div className="space-y-1.5">
<label className="text-foreground block text-xs font-medium">
(Longitude)
<span className="text-destructive ml-1">*</span>
</label>
<select
value={currentConfig.longitudeColumn || ""}
onChange={(e) => updateConfig({ longitudeColumn: e.target.value })}
className="border-border w-full rounded-lg 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>
{/* 라벨 컬럼 (선택사항) */}
<div className="space-y-1.5">
<label className="text-foreground block text-xs font-medium"> ( )</label>
<select
value={currentConfig.labelColumn || ""}
onChange={(e) => updateConfig({ labelColumn: e.target.value })}
className="border-border w-full rounded-lg border px-2 py-1.5 text-xs"
>
<option value=""> ()</option>
{availableColumns.map((col) => (
<option key={col} value={col}>
{col}
</option>
))}
</select>
</div>
{/* 마커 색상 설정 */}
<div className="space-y-2 border-t pt-3">
<h5 className="text-foreground text-xs font-semibold">🎨 </h5>
{/* 색상 모드 선택 */}
<div className="space-y-1.5">
<label className="text-foreground block text-xs font-medium"> </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-primary bg-primary/10 text-primary font-medium"
: "border-border bg-background text-foreground hover:bg-muted"
}`}
>
</button>
<button
type="button"
onClick={() => handleMarkerColorModeChange("conditional")}
className={`flex-1 rounded-lg border px-3 py-2 text-xs transition-colors ${
currentConfig.markerColorMode === "conditional"
? "border-primary bg-primary/10 text-primary font-medium"
: "border-border bg-background text-foreground hover:bg-muted"
}`}
>
</button>
</div>
</div>
{/* 단일 색상 모드 */}
{(currentConfig.markerColorMode || "single") === "single" && (
<div className="bg-muted space-y-1.5 rounded-lg p-3">
<label className="text-foreground block text-xs font-medium"> </label>
<div className="flex items-center gap-2">
<input
type="color"
value={currentConfig.markerDefaultColor || "#3b82f6"}
onChange={(e) => updateConfig({ markerDefaultColor: e.target.value })}
className="border-border h-8 w-12 cursor-pointer rounded border"
/>
<input
type="text"
value={currentConfig.markerDefaultColor || "#3b82f6"}
onChange={(e) => updateConfig({ markerDefaultColor: e.target.value })}
placeholder="#3b82f6"
className="border-border flex-1 rounded-lg border px-2 py-1.5 text-xs"
/>
</div>
<p className="text-muted-foreground text-xs"> </p>
</div>
)}
{/* 조건부 색상 모드 */}
{currentConfig.markerColorMode === "conditional" && (
<div className="bg-muted space-y-2 rounded-lg p-3">
{/* 색상 조건 컬럼 선택 */}
<div className="space-y-1.5">
<label className="text-foreground block text-xs font-medium">
<span className="text-destructive ml-1">*</span>
</label>
<select
value={currentConfig.markerColorColumn || ""}
onChange={(e) => updateConfig({ markerColorColumn: e.target.value })}
className="border-border w-full rounded-lg border px-2 py-1.5 text-xs"
>
<option value=""></option>
{availableColumns.map((col) => (
<option key={col} value={col}>
{col}
</option>
))}
</select>
<p className="text-muted-foreground text-xs"> </p>
</div>
{/* 기본 색상 */}
<div className="space-y-1.5">
<label className="text-foreground block text-xs font-medium"> </label>
<div className="flex items-center gap-2">
<input
type="color"
value={currentConfig.markerDefaultColor || "#6b7280"}
onChange={(e) => updateConfig({ markerDefaultColor: e.target.value })}
className="border-border h-8 w-12 cursor-pointer rounded border"
/>
<input
type="text"
value={currentConfig.markerDefaultColor || "#6b7280"}
onChange={(e) => updateConfig({ markerDefaultColor: e.target.value })}
placeholder="#6b7280"
className="border-border flex-1 rounded-lg border px-2 py-1.5 text-xs"
/>
</div>
<p className="text-muted-foreground text-xs"> </p>
</div>
{/* 색상 규칙 목록 */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<label className="text-foreground block text-xs font-medium"> </label>
<button
type="button"
onClick={addColorRule}
className="bg-primary hover:bg-primary/90 flex items-center gap-1 rounded-lg px-2 py-1 text-xs text-white transition-colors"
>
<Plus className="h-3 w-3" />
</button>
</div>
{/* 규칙 리스트 */}
{(currentConfig.markerColorRules || []).length === 0 ? (
<div className="border-border bg-background rounded-lg border p-3 text-center">
<p className="text-muted-foreground text-xs"> </p>
</div>
) : (
<div className="space-y-2">
{(currentConfig.markerColorRules || []).map((rule) => (
<div key={rule.id} className="border-border bg-background space-y-2 rounded-lg border p-2">
{/* 규칙 헤더 */}
<div className="flex items-center justify-between">
<span className="text-foreground text-xs font-medium"></span>
<button
type="button"
onClick={() => deleteColorRule(rule.id)}
className="text-destructive hover:text-destructive transition-colors"
>
<Trash2 className="h-3 w-3" />
</button>
</div>
{/* 조건 값 */}
<div className="space-y-1">
<label className="text-foreground block text-xs font-medium"> ()</label>
<input
type="text"
value={rule.value}
onChange={(e) => updateColorRule(rule.id, { value: e.target.value })}
placeholder="예: active, inactive"
className="border-border w-full rounded border px-2 py-1 text-xs"
/>
</div>
{/* 색상 */}
<div className="space-y-1">
<label className="text-foreground block text-xs font-medium"></label>
<div className="flex items-center gap-2">
<input
type="color"
value={rule.color}
onChange={(e) => updateColorRule(rule.id, { color: e.target.value })}
className="border-border h-8 w-12 cursor-pointer rounded border"
/>
<input
type="text"
value={rule.color}
onChange={(e) => updateColorRule(rule.id, { color: e.target.value })}
placeholder="#3b82f6"
className="border-border flex-1 rounded border px-2 py-1 text-xs"
/>
</div>
</div>
{/* 라벨 (선택사항) */}
<div className="space-y-1">
<label className="text-foreground block text-xs font-medium"> ()</label>
<input
type="text"
value={rule.label || ""}
onChange={(e) => updateColorRule(rule.id, { label: e.target.value })}
placeholder="예: 활성, 비활성"
className="border-border w-full rounded border px-2 py-1 text-xs"
/>
</div>
</div>
))}
</div>
)}
</div>
</div>
)}
</div>
{/* 날씨 정보 표시 옵션 */}
<div className="space-y-1.5">
<label className="text-foreground flex cursor-pointer items-center gap-2 text-xs font-medium">
<input
type="checkbox"
checked={currentConfig.showWeather || false}
onChange={(e) => updateConfig({ showWeather: e.target.checked })}
className="text-primary focus:ring-primary border-border h-4 w-4 rounded focus:ring-2"
/>
<span> </span>
</label>
<p className="text-muted-foreground ml-6 text-xs"> </p>
</div>
<div className="space-y-1.5">
<label className="text-foreground flex cursor-pointer items-center gap-2 text-xs font-medium">
<input
type="checkbox"
checked={currentConfig.showWeatherAlerts || false}
onChange={(e) => updateConfig({ showWeatherAlerts: e.target.checked })}
className="text-primary focus:ring-primary border-border h-4 w-4 rounded focus:ring-2"
/>
<span> </span>
</label>
<p className="text-muted-foreground ml-6 text-xs">
(/)
</p>
</div>
{/* 설정 미리보기 */}
<div className="bg-muted rounded-lg p-3">
<div className="text-foreground mb-2 text-xs font-medium">📋 </div>
<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) && (
<div className="border-destructive bg-destructive/10 rounded-lg border p-3">
<div className="text-destructive text-xs">
.
</div>
</div>
)}
</>
)}
</div>
);
}