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-xs font-semibold text-gray-800">🗺 </h4>
{/* 쿼리 결과가 없을 때 */}
{!queryResult && (
<div className="rounded-lg border border-yellow-200 bg-yellow-50 p-3">
<div className="text-xs text-yellow-800">
💡 SQL .
</div>
</div>
)}
{/* 데이터 필드 매핑 */}
{queryResult && (
<>
{/* 지도 제목 */}
<div className="space-y-1.5">
<label className="block text-xs font-medium text-gray-700"> </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"
/>
</div>
{/* 위도 컬럼 설정 */}
<div className="space-y-1.5">
<label className="block text-xs font-medium text-gray-700">
(Latitude)
<span className="ml-1 text-red-500">*</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"
>
<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="block text-xs font-medium text-gray-700">
(Longitude)
<span className="ml-1 text-red-500">*</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"
>
<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="block text-xs font-medium text-gray-700"> ( )</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"
>
<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-xs font-semibold text-gray-800">🎨 </h5>
{/* 색상 모드 선택 */}
<div className="space-y-1.5">
<label className="block text-xs font-medium text-gray-700"> </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"
}`}
>
</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-blue-300 bg-blue-50 font-medium text-blue-700"
: "border-gray-300 bg-white text-gray-700 hover:bg-gray-50"
}`}
>
</button>
</div>
</div>
{/* 단일 색상 모드 */}
{(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="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"
/>
<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"
/>
</div>
<p className="text-xs text-gray-500"> </p>
</div>
)}
{/* 조건부 색상 모드 */}
{currentConfig.markerColorMode === "conditional" && (
<div className="space-y-2 rounded-lg bg-gray-50 p-3">
{/* 색상 조건 컬럼 선택 */}
<div className="space-y-1.5">
<label className="block text-xs font-medium text-gray-700">
<span className="ml-1 text-red-500">*</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"
>
<option value=""></option>
{availableColumns.map((col) => (
<option key={col} value={col}>
{col}
</option>
))}
</select>
<p className="text-xs text-gray-500"> </p>
</div>
{/* 기본 색상 */}
<div className="space-y-1.5">
<label className="block text-xs font-medium text-gray-700"> </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"
/>
<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"
/>
</div>
<p className="text-xs text-gray-500"> </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>
<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"
>
<Plus className="h-3 w-3" />
</button>
</div>
{/* 규칙 리스트 */}
{(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>
) : (
<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 className="flex items-center justify-between">
<span className="text-xs font-medium text-gray-700"></span>
<button
type="button"
onClick={() => deleteColorRule(rule.id)}
className="text-red-500 transition-colors hover:text-red-700"
>
<Trash2 className="h-3 w-3" />
</button>
</div>
{/* 조건 값 */}
<div className="space-y-1">
<label className="block text-xs font-medium text-gray-600"> ()</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"
/>
</div>
{/* 색상 */}
<div className="space-y-1">
<label className="block text-xs font-medium text-gray-600"></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"
/>
<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"
/>
</div>
</div>
{/* 라벨 (선택사항) */}
<div className="space-y-1">
<label className="block text-xs font-medium text-gray-600"> ()</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"
/>
</div>
</div>
))}
</div>
)}
</div>
</div>
)}
</div>
{/* 날씨 정보 표시 옵션 */}
<div className="space-y-1.5">
<label className="flex cursor-pointer items-center gap-2 text-xs font-medium text-gray-700">
<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"
/>
<span> </span>
</label>
<p className="ml-6 text-xs text-gray-500"> </p>
</div>
<div className="space-y-1.5">
<label className="flex cursor-pointer items-center gap-2 text-xs font-medium text-gray-700">
<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"
/>
<span> </span>
</label>
<p className="ml-6 text-xs text-gray-500">
(/)
</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="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="rounded-lg border border-red-200 bg-red-50 p-3">
<div className="text-xs text-red-800">
.
</div>
</div>
)}
</>
)}
</div>
);
}