이희진 진행사항 중간세이브
This commit is contained in:
@@ -3,7 +3,8 @@
|
||||
import React, { useEffect, useState, useCallback, useMemo } from "react";
|
||||
import dynamic from "next/dynamic";
|
||||
import { DashboardElement, ChartDataSource } from "@/components/admin/dashboard/types";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Loader2, RefreshCw } from "lucide-react";
|
||||
import "leaflet/dist/leaflet.css";
|
||||
|
||||
// Leaflet 아이콘 경로 설정 (엑박 방지)
|
||||
@@ -60,6 +61,7 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [geoJsonData, setGeoJsonData] = useState<any>(null);
|
||||
const [lastRefreshTime, setLastRefreshTime] = useState<Date | null>(null);
|
||||
|
||||
console.log("🧪 MapTestWidgetV2 렌더링!", element);
|
||||
console.log("📍 마커:", markers.length, "🔷 폴리곤:", polygons.length);
|
||||
@@ -136,6 +138,7 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
||||
|
||||
setMarkers(allMarkers);
|
||||
setPolygons(allPolygons);
|
||||
setLastRefreshTime(new Date());
|
||||
} catch (err: any) {
|
||||
console.error("❌ 데이터 로딩 중 오류:", err);
|
||||
setError(err.message);
|
||||
@@ -144,6 +147,12 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
||||
}
|
||||
}, [dataSources]);
|
||||
|
||||
// 수동 새로고침 핸들러
|
||||
const handleManualRefresh = useCallback(() => {
|
||||
console.log("🔄 수동 새로고침 버튼 클릭");
|
||||
loadMultipleDataSources();
|
||||
}, [loadMultipleDataSources]);
|
||||
|
||||
// REST API 데이터 로딩
|
||||
const loadRestApiData = async (source: ChartDataSource): Promise<{ markers: MarkerData[]; polygons: PolygonData[] }> => {
|
||||
console.log(`🌐 REST API 데이터 로딩 시작:`, source.name, `mapDisplayType:`, source.mapDisplayType);
|
||||
@@ -263,11 +272,47 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
||||
return convertToMapData(rows, source.name || source.id || "Database", source.mapDisplayType);
|
||||
};
|
||||
|
||||
// XML 데이터 파싱 (UTIC API 등)
|
||||
const parseXmlData = (xmlText: string): any[] => {
|
||||
try {
|
||||
console.log(" 📄 XML 파싱 시작");
|
||||
const parser = new DOMParser();
|
||||
const xmlDoc = parser.parseFromString(xmlText, "text/xml");
|
||||
|
||||
const records = xmlDoc.getElementsByTagName("record");
|
||||
const results: any[] = [];
|
||||
|
||||
for (let i = 0; i < records.length; i++) {
|
||||
const record = records[i];
|
||||
const obj: any = {};
|
||||
|
||||
for (let j = 0; j < record.children.length; j++) {
|
||||
const child = record.children[j];
|
||||
obj[child.tagName] = child.textContent || "";
|
||||
}
|
||||
|
||||
results.push(obj);
|
||||
}
|
||||
|
||||
console.log(` ✅ XML 파싱 완료: ${results.length}개 레코드`);
|
||||
return results;
|
||||
} catch (error) {
|
||||
console.error(" ❌ XML 파싱 실패:", error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
// 텍스트 데이터 파싱 (CSV, 기상청 형식 등)
|
||||
const parseTextData = (text: string): any[] => {
|
||||
try {
|
||||
console.log(" 🔍 원본 텍스트 (처음 500자):", text.substring(0, 500));
|
||||
|
||||
// XML 형식 감지
|
||||
if (text.trim().startsWith("<?xml") || text.trim().startsWith("<result>")) {
|
||||
console.log(" 📄 XML 형식 데이터 감지");
|
||||
return parseXmlData(text);
|
||||
}
|
||||
|
||||
const lines = text.split('\n').filter(line => {
|
||||
const trimmed = line.trim();
|
||||
return trimmed &&
|
||||
@@ -382,8 +427,8 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
||||
}
|
||||
|
||||
// 마커 데이터 처리 (위도/경도가 있는 경우)
|
||||
let lat = row.lat || row.latitude || row.y;
|
||||
let lng = row.lng || row.longitude || row.x;
|
||||
let lat = row.lat || row.latitude || row.y || row.locationDataY;
|
||||
let lng = row.lng || row.longitude || row.x || row.locationDataX;
|
||||
|
||||
// 위도/경도가 없으면 지역 코드/지역명으로 변환 시도
|
||||
if ((lat === undefined || lng === undefined) && (row.code || row.areaCode || row.regionCode || row.tmFc || row.stnId)) {
|
||||
@@ -715,6 +760,31 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
||||
}
|
||||
}, [dataSources, loadMultipleDataSources]);
|
||||
|
||||
// 자동 새로고침
|
||||
useEffect(() => {
|
||||
if (!dataSources || dataSources.length === 0) return;
|
||||
|
||||
// 모든 데이터 소스 중 가장 짧은 refreshInterval 찾기
|
||||
const intervals = dataSources
|
||||
.map((ds) => ds.refreshInterval)
|
||||
.filter((interval): interval is number => typeof interval === "number" && interval > 0);
|
||||
|
||||
if (intervals.length === 0) return;
|
||||
|
||||
const minInterval = Math.min(...intervals);
|
||||
console.log(`⏱️ 자동 새로고침 설정: ${minInterval}초마다`);
|
||||
|
||||
const intervalId = setInterval(() => {
|
||||
console.log("🔄 자동 새로고침 실행");
|
||||
loadMultipleDataSources();
|
||||
}, minInterval * 1000);
|
||||
|
||||
return () => {
|
||||
console.log("⏹️ 자동 새로고침 정리");
|
||||
clearInterval(intervalId);
|
||||
};
|
||||
}, [dataSources, loadMultipleDataSources]);
|
||||
|
||||
// 타일맵 URL (chartConfig에서 가져오기)
|
||||
const tileMapUrl = element?.chartConfig?.tileMapUrl ||
|
||||
`https://api.vworld.kr/req/wmts/1.0.0/${VWORLD_API_KEY}/Base/{z}/{y}/{x}.png`;
|
||||
@@ -737,9 +807,26 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
||||
</h3>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{element?.dataSources?.length || 0}개 데이터 소스 연결됨
|
||||
{lastRefreshTime && (
|
||||
<span className="ml-2">
|
||||
• 마지막 업데이트: {lastRefreshTime.toLocaleTimeString("ko-KR")}
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
{loading && <Loader2 className="h-4 w-4 animate-spin" />}
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleManualRefresh}
|
||||
disabled={loading}
|
||||
className="h-8 gap-2 text-xs"
|
||||
>
|
||||
<RefreshCw className={`h-3 w-3 ${loading ? "animate-spin" : ""}`} />
|
||||
새로고침
|
||||
</Button>
|
||||
{loading && <Loader2 className="h-4 w-4 animate-spin" />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 지도 */}
|
||||
@@ -769,19 +856,22 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
||||
|
||||
{/* 폴리곤 렌더링 */}
|
||||
{/* GeoJSON 렌더링 (육지 지역 경계선) */}
|
||||
{geoJsonData && polygons.length > 0 && (
|
||||
{(() => {
|
||||
console.log(`🗺️ GeoJSON 렌더링 조건 체크:`, {
|
||||
geoJsonData: !!geoJsonData,
|
||||
polygonsLength: polygons.length,
|
||||
polygonNames: polygons.map(p => p.name),
|
||||
});
|
||||
return null;
|
||||
})()}
|
||||
{geoJsonData && polygons.length > 0 ? (
|
||||
<GeoJSON
|
||||
key={JSON.stringify(polygons.map(p => p.id))} // 폴리곤 변경 시 재렌더링
|
||||
data={geoJsonData}
|
||||
style={(feature: any) => {
|
||||
const ctpName = feature?.properties?.CTP_KOR_NM; // 시/도명 (예: 경상북도)
|
||||
const sigName = feature?.properties?.SIG_KOR_NM; // 시/군/구명 (예: 군위군)
|
||||
|
||||
// 🔍 디버그: GeoJSON 속성 확인
|
||||
if (ctpName === "경상북도" || sigName?.includes("군위") || sigName?.includes("영천")) {
|
||||
console.log(`🔍 GeoJSON 속성:`, { ctpName, sigName, properties: feature?.properties });
|
||||
console.log(`🔍 매칭 시도할 폴리곤:`, polygons.map(p => p.name));
|
||||
}
|
||||
|
||||
// 폴리곤 매칭 (시/군/구명 우선, 없으면 시/도명)
|
||||
const matchingPolygon = polygons.find(p => {
|
||||
if (!p.name) return false;
|
||||
@@ -859,6 +949,8 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<>{console.log(`⚠️ GeoJSON 렌더링 안 됨: geoJsonData=${!!geoJsonData}, polygons=${polygons.length}`)}</>
|
||||
)}
|
||||
|
||||
{/* 폴리곤 렌더링 (해상 구역만) */}
|
||||
@@ -902,21 +994,79 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
||||
key={marker.id}
|
||||
position={[marker.lat, marker.lng]}
|
||||
>
|
||||
<Popup>
|
||||
<div className="min-w-[200px]">
|
||||
<div className="mb-2 font-semibold">{marker.name}</div>
|
||||
{marker.source && (
|
||||
<div className="mb-1 text-xs text-muted-foreground">
|
||||
출처: {marker.source}
|
||||
<Popup maxWidth={350}>
|
||||
<div className="min-w-[250px] max-w-[350px]">
|
||||
{/* 제목 */}
|
||||
<div className="mb-2 border-b pb-2">
|
||||
<div className="text-base font-bold">{marker.name}</div>
|
||||
{marker.source && (
|
||||
<div className="mt-1 text-xs text-muted-foreground">
|
||||
📡 {marker.source}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 상세 정보 */}
|
||||
<div className="space-y-2">
|
||||
{marker.description && (
|
||||
<div className="rounded bg-muted p-2">
|
||||
<div className="mb-1 text-xs font-semibold text-foreground">상세 정보</div>
|
||||
<div className="text-xs text-muted-foreground whitespace-pre-wrap">
|
||||
{(() => {
|
||||
try {
|
||||
const parsed = JSON.parse(marker.description);
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
{parsed.incidenteTypeCd === "1" && (
|
||||
<div className="font-semibold text-destructive">🚨 교통사고</div>
|
||||
)}
|
||||
{parsed.incidenteTypeCd === "2" && (
|
||||
<div className="font-semibold text-warning">🚧 도로공사</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.status && (
|
||||
<div className="text-xs">
|
||||
<span className="font-semibold">상태:</span> {marker.status}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 좌표 */}
|
||||
<div className="border-t pt-2 text-[10px] text-muted-foreground">
|
||||
📍 {marker.lat.toFixed(6)}, {marker.lng.toFixed(6)}
|
||||
</div>
|
||||
)}
|
||||
{marker.status && (
|
||||
<div className="mb-1 text-xs">
|
||||
상태: {marker.status}
|
||||
</div>
|
||||
)}
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{marker.lat.toFixed(6)}, {marker.lng.toFixed(6)}
|
||||
</div>
|
||||
</div>
|
||||
</Popup>
|
||||
|
||||
Reference in New Issue
Block a user