팝업 수정 가능하게 수정

This commit is contained in:
dohyeons 2025-11-12 19:08:41 +09:00
parent 5e8e714e8a
commit 800bd85811
4 changed files with 359 additions and 55 deletions

View File

@ -911,6 +911,128 @@ export default function MultiApiConfig({ dataSource, onChange, onTestResult }: M
</p>
</div>
)}
{/* 지도 팝업 필드 설정 (MapTestWidgetV2 전용) */}
{availableColumns.length > 0 && (
<div className="space-y-2">
<Label htmlFor="popup-fields" className="text-xs">
</Label>
{/* 기존 팝업 필드 목록 */}
{dataSource.popupFields && dataSource.popupFields.length > 0 && (
<div className="space-y-3">
{dataSource.popupFields.map((field, index) => (
<div key={index} className="space-y-2 rounded-lg border bg-muted/30 p-3">
<div className="flex items-center justify-between">
<span className="text-xs font-medium"> {index + 1}</span>
<Button
variant="ghost"
size="sm"
onClick={() => {
const newFields = [...(dataSource.popupFields || [])];
newFields.splice(index, 1);
onChange({ popupFields: newFields });
}}
className="h-6 w-6 p-0"
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
{/* 필드명 선택 */}
<div>
<Label className="text-xs"></Label>
<Select
value={field.fieldName}
onValueChange={(value) => {
const newFields = [...(dataSource.popupFields || [])];
newFields[index].fieldName = value;
onChange({ popupFields: newFields });
}}
>
<SelectTrigger className="h-8 w-full text-xs">
<SelectValue placeholder="필드 선택" />
</SelectTrigger>
<SelectContent>
{availableColumns.map((col) => (
<SelectItem key={col} value={col} className="text-xs">
{col}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* 라벨 입력 */}
<div>
<Label className="text-xs"> </Label>
<Input
value={field.label || ""}
onChange={(e) => {
const newFields = [...(dataSource.popupFields || [])];
newFields[index].label = e.target.value;
onChange({ popupFields: newFields });
}}
placeholder="예: 차량 번호"
className="h-8 w-full text-xs"
dir="ltr"
/>
</div>
{/* 포맷 선택 */}
<div>
<Label className="text-xs"> </Label>
<Select
value={field.format || "text"}
onValueChange={(value: "text" | "date" | "datetime" | "number" | "url") => {
const newFields = [...(dataSource.popupFields || [])];
newFields[index].format = value;
onChange({ popupFields: newFields });
}}
>
<SelectTrigger className="h-8 w-full text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="text" className="text-xs"></SelectItem>
<SelectItem value="number" className="text-xs"></SelectItem>
<SelectItem value="date" className="text-xs"></SelectItem>
<SelectItem value="datetime" className="text-xs"></SelectItem>
<SelectItem value="url" className="text-xs">URL</SelectItem>
</SelectContent>
</Select>
</div>
</div>
))}
</div>
)}
{/* 필드 추가 버튼 */}
<Button
variant="outline"
size="sm"
onClick={() => {
const newFields = [...(dataSource.popupFields || [])];
newFields.push({
fieldName: availableColumns[0] || "",
label: "",
format: "text",
});
onChange({ popupFields: newFields });
}}
className="h-8 w-full gap-2 text-xs"
disabled={availableColumns.length === 0}
>
<Plus className="h-3 w-3" />
</Button>
<p className="text-[10px] text-muted-foreground">
</p>
</div>
)}
</div>
);
}

View File

@ -8,7 +8,7 @@ import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Loader2, CheckCircle, XCircle } from "lucide-react";
import { Loader2, CheckCircle, XCircle, Plus, Trash2 } from "lucide-react";
interface MultiDatabaseConfigProps {
dataSource: ChartDataSource;
@ -673,6 +673,128 @@ ORDER BY 하위부서수 DESC`,
</p>
</div>
)}
{/* 지도 팝업 필드 설정 (MapTestWidgetV2 전용) */}
{availableColumns.length > 0 && (
<div className="space-y-2">
<Label htmlFor="popup-fields" className="text-xs">
</Label>
{/* 기존 팝업 필드 목록 */}
{dataSource.popupFields && dataSource.popupFields.length > 0 && (
<div className="space-y-3">
{dataSource.popupFields.map((field, index) => (
<div key={index} className="space-y-2 rounded-lg border bg-muted/30 p-3">
<div className="flex items-center justify-between">
<span className="text-xs font-medium"> {index + 1}</span>
<Button
variant="ghost"
size="sm"
onClick={() => {
const newFields = [...(dataSource.popupFields || [])];
newFields.splice(index, 1);
onChange({ popupFields: newFields });
}}
className="h-6 w-6 p-0"
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
{/* 필드명 선택 */}
<div>
<Label className="text-xs"></Label>
<Select
value={field.fieldName}
onValueChange={(value) => {
const newFields = [...(dataSource.popupFields || [])];
newFields[index].fieldName = value;
onChange({ popupFields: newFields });
}}
>
<SelectTrigger className="h-8 w-full text-xs">
<SelectValue placeholder="필드 선택" />
</SelectTrigger>
<SelectContent>
{availableColumns.map((col) => (
<SelectItem key={col} value={col} className="text-xs">
{col}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* 라벨 입력 */}
<div>
<Label className="text-xs"> </Label>
<Input
value={field.label || ""}
onChange={(e) => {
const newFields = [...(dataSource.popupFields || [])];
newFields[index].label = e.target.value;
onChange({ popupFields: newFields });
}}
placeholder="예: 차량 번호"
className="h-8 w-full text-xs"
dir="ltr"
/>
</div>
{/* 포맷 선택 */}
<div>
<Label className="text-xs"> </Label>
<Select
value={field.format || "text"}
onValueChange={(value: "text" | "date" | "datetime" | "number" | "url") => {
const newFields = [...(dataSource.popupFields || [])];
newFields[index].format = value;
onChange({ popupFields: newFields });
}}
>
<SelectTrigger className="h-8 w-full text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="text" className="text-xs"></SelectItem>
<SelectItem value="number" className="text-xs"></SelectItem>
<SelectItem value="date" className="text-xs"></SelectItem>
<SelectItem value="datetime" className="text-xs"></SelectItem>
<SelectItem value="url" className="text-xs">URL</SelectItem>
</SelectContent>
</Select>
</div>
</div>
))}
</div>
)}
{/* 필드 추가 버튼 */}
<Button
variant="outline"
size="sm"
onClick={() => {
const newFields = [...(dataSource.popupFields || [])];
newFields.push({
fieldName: availableColumns[0] || "",
label: "",
format: "text",
});
onChange({ popupFields: newFields });
}}
className="h-8 w-full gap-2 text-xs"
disabled={availableColumns.length === 0}
>
<Plus className="h-3 w-3" />
</Button>
<p className="text-[10px] text-muted-foreground">
</p>
</div>
)}
</div>
);
}

View File

@ -171,6 +171,13 @@ export interface ChartDataSource {
// 메트릭 설정 (CustomMetricTestWidget용)
selectedColumns?: string[]; // 표시할 컬럼 선택 (빈 배열이면 전체 표시)
// 지도 팝업 설정 (MapTestWidgetV2용)
popupFields?: {
fieldName: string; // DB 컬럼명 (예: vehicle_number)
label: string; // 표시할 한글명 (예: 차량 번호)
format?: "text" | "date" | "datetime" | "number" | "url"; // 표시 포맷
}[];
}
export interface ChartConfig {

View File

@ -919,7 +919,7 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
// 첫 번째 데이터 소스의 새로고침 간격 사용 (초)
const firstDataSource = dataSources[0];
const refreshInterval = firstDataSource?.refreshInterval ?? 5;
// 0이면 자동 새로고침 비활성화
if (refreshInterval === 0) {
return;
@ -1123,12 +1123,12 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
// 첫 번째 데이터 소스의 마커 종류 가져오기
const firstDataSource = dataSources?.[0];
const markerType = firstDataSource?.markerType || "circle";
let markerIcon: any;
if (typeof window !== "undefined") {
const L = require("leaflet");
const heading = marker.heading || 0;
if (markerType === "arrow") {
// 화살표 마커
markerIcon = L.divIcon({
@ -1216,63 +1216,117 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
return (
<Marker key={marker.id} position={[marker.lat, marker.lng]} icon={markerIcon}>
<Popup maxWidth={350}>
<div className="max-w-[350px] min-w-[250px]">
{/* 제목 */}
<div className="mb-2 border-b pb-2">
<div className="text-base font-bold">{marker.name}</div>
{marker.source && <div className="text-muted-foreground mt-1 text-xs">📡 {marker.source}</div>}
</div>
<div className="max-w-[350px] min-w-[250px]" dir="ltr">
{/* 데이터 소스명만 표시 */}
{marker.source && (
<div className="mb-2 border-b pb-2">
<div className="text-muted-foreground text-xs">📡 {marker.source}</div>
</div>
)}
{/* 상세 정보 */}
<div className="space-y-2">
{marker.description && (
<div className="bg-muted rounded p-2">
<div className="text-foreground mb-1 text-xs font-semibold"> </div>
<div className="text-muted-foreground text-xs whitespace-pre-wrap">
{(() => {
try {
const parsed = JSON.parse(marker.description);
return (
<div className="space-y-1">
{parsed.incidenteTypeCd === "1" && (
<div className="text-destructive font-semibold">🚨 </div>
)}
{parsed.incidenteTypeCd === "2" && (
<div className="text-warning font-semibold">🚧 </div>
)}
{parsed.addressJibun && <div>📍 {parsed.addressJibun}</div>}
{parsed.addressNew && parsed.addressNew !== parsed.addressJibun && (
<div>📍 {parsed.addressNew}</div>
)}
{parsed.roadName && <div>🛣 {parsed.roadName}</div>}
{parsed.linkName && <div>🔗 {parsed.linkName}</div>}
{parsed.incidentMsg && (
<div className="mt-2 border-t pt-2">💬 {parsed.incidentMsg}</div>
)}
{parsed.eventContent && (
<div className="mt-2 border-t pt-2">📝 {parsed.eventContent}</div>
)}
{parsed.startDate && <div className="text-[10px]">🕐 {parsed.startDate}</div>}
{parsed.endDate && <div className="text-[10px]">🕐 : {parsed.endDate}</div>}
</div>
);
} catch {
return marker.description;
}
})()}
</div>
</div>
)}
{marker.description &&
(() => {
const firstDataSource = dataSources?.[0];
const popupFields = firstDataSource?.popupFields;
{marker.status && (
<div className="text-xs">
<span className="font-semibold">:</span> {marker.status}
</div>
)}
// popupFields가 설정되어 있으면 설정된 필드만 표시
if (popupFields && popupFields.length > 0) {
try {
const parsed = JSON.parse(marker.description);
return (
<div className="bg-muted rounded p-2">
<div className="text-foreground mb-1 text-xs font-semibold"> </div>
<div className="space-y-2">
{popupFields.map((field, idx) => {
const value = parsed[field.fieldName];
if (value === undefined || value === null) return null;
// 포맷팅 적용
let formattedValue = value;
if (field.format === "date" && value) {
formattedValue = new Date(value).toLocaleDateString("ko-KR");
} else if (field.format === "datetime" && value) {
formattedValue = new Date(value).toLocaleString("ko-KR");
} else if (field.format === "number" && typeof value === "number") {
formattedValue = value.toLocaleString();
} else if (
field.format === "url" &&
typeof value === "string" &&
value.startsWith("http")
) {
return (
<div key={idx} className="text-xs">
<span className="text-muted-foreground font-medium">{field.label}:</span>{" "}
<a
href={value}
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
</a>
</div>
);
}
return (
<div key={idx} className="text-xs">
<span className="text-muted-foreground font-medium">{field.label}:</span>{" "}
<span className="text-foreground">{String(formattedValue)}</span>
</div>
);
})}
</div>
</div>
);
} catch (error) {
return (
<div className="bg-muted rounded p-2">
<div className="text-foreground mb-1 text-xs font-semibold"> </div>
<div className="text-muted-foreground text-xs">{marker.description}</div>
</div>
);
}
}
// popupFields가 없으면 전체 데이터 표시 (기본 동작)
try {
const parsed = JSON.parse(marker.description);
return (
<div className="bg-muted rounded p-2">
<div className="text-foreground mb-1 text-xs font-semibold"> </div>
<div className="space-y-2">
{Object.entries(parsed).map(([key, value], idx) => {
if (value === undefined || value === null) return null;
// 좌표 필드는 제외 (하단에 별도 표시)
if (["lat", "lng", "latitude", "longitude", "x", "y"].includes(key)) return null;
return (
<div key={idx} className="text-xs">
<span className="text-muted-foreground font-medium">{key}:</span>{" "}
<span className="text-foreground">{String(value)}</span>
</div>
);
})}
</div>
</div>
);
} catch (error) {
return (
<div className="bg-muted rounded p-2">
<div className="text-foreground mb-1 text-xs font-semibold"> </div>
<div className="text-muted-foreground text-xs">{marker.description}</div>
</div>
);
}
})()}
{/* 좌표 */}
<div className="text-muted-foreground border-t pt-2 text-[10px]">
📍 {marker.lat.toFixed(6)}, {marker.lng.toFixed(6)}
{marker.lat.toFixed(6)}, {marker.lng.toFixed(6)}
</div>
</div>
</div>
@ -1280,7 +1334,6 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
</Marker>
);
})}
</MapContainer>
)}
</div>