팝업 수정 가능하게 수정
This commit is contained in:
parent
5e8e714e8a
commit
800bd85811
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue