423 lines
16 KiB
TypeScript
423 lines
16 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState, useCallback, useEffect } from "react";
|
|
import { ChartConfig, QueryResult, ChartDataSource } from "./types";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Plus, X } from "lucide-react";
|
|
import { ExternalDbConnectionAPI, ExternalApiConnection } from "@/lib/api/externalDbConnection";
|
|
|
|
interface MapTestConfigPanelProps {
|
|
config?: ChartConfig;
|
|
queryResult?: QueryResult;
|
|
onConfigChange: (config: ChartConfig) => void;
|
|
}
|
|
|
|
/**
|
|
* 지도 테스트 위젯 설정 패널
|
|
* - 타일맵 URL 설정 (VWorld, OpenStreetMap 등)
|
|
* - 위도/경도 컬럼 매핑
|
|
* - 라벨/상태 컬럼 설정
|
|
*/
|
|
export function MapTestConfigPanel({ config, queryResult, onConfigChange }: MapTestConfigPanelProps) {
|
|
const [currentConfig, setCurrentConfig] = useState<ChartConfig>(config || {});
|
|
const [connections, setConnections] = useState<ExternalApiConnection[]>([]);
|
|
const [tileMapSources, setTileMapSources] = useState<Array<{ id: string; url: string }>>([
|
|
{ id: `tilemap_${Date.now()}`, url: "" },
|
|
]);
|
|
|
|
// config prop 변경 시 currentConfig 동기화
|
|
useEffect(() => {
|
|
if (config) {
|
|
setCurrentConfig(config);
|
|
console.log("🔄 config 업데이트:", config);
|
|
}
|
|
}, [config]);
|
|
|
|
// 외부 API 커넥션 목록 불러오기 (REST API만)
|
|
useEffect(() => {
|
|
const loadApiConnections = async () => {
|
|
try {
|
|
const apiConnections = await ExternalDbConnectionAPI.getApiConnections({ is_active: "Y" });
|
|
setConnections(apiConnections);
|
|
console.log("✅ REST API 커넥션 로드 완료:", apiConnections);
|
|
console.log(`📊 총 ${apiConnections.length}개의 REST API 커넥션`);
|
|
} catch (error) {
|
|
console.error("❌ REST API 커넥션 로드 실패:", error);
|
|
}
|
|
};
|
|
|
|
loadApiConnections();
|
|
}, []);
|
|
|
|
// 타일맵 URL을 템플릿 형식으로 변환 (10/856/375.png → {z}/{y}/{x}.png)
|
|
const convertToTileTemplate = (url: string): string => {
|
|
// 이미 템플릿 형식이면 그대로 반환
|
|
if (url.includes("{z}") && url.includes("{y}") && url.includes("{x}")) {
|
|
return url;
|
|
}
|
|
|
|
// 특정 타일 URL 패턴 감지: /숫자/숫자/숫자.png
|
|
const tilePattern = /\/(\d+)\/(\d+)\/(\d+)\.(png|jpg|jpeg)$/i;
|
|
const match = url.match(tilePattern);
|
|
|
|
if (match) {
|
|
// /10/856/375.png → /{z}/{y}/{x}.png
|
|
const convertedUrl = url.replace(tilePattern, "/{z}/{y}/{x}.$4");
|
|
console.log("🔄 타일 URL 자동 변환:", url, "→", convertedUrl);
|
|
return convertedUrl;
|
|
}
|
|
|
|
return url;
|
|
};
|
|
|
|
// 설정 업데이트
|
|
const updateConfig = useCallback(
|
|
(updates: Partial<ChartConfig>) => {
|
|
// tileMapUrl이 업데이트되면 자동으로 템플릿 형식으로 변환
|
|
if (updates.tileMapUrl) {
|
|
updates.tileMapUrl = convertToTileTemplate(updates.tileMapUrl);
|
|
}
|
|
|
|
const newConfig = { ...currentConfig, ...updates };
|
|
setCurrentConfig(newConfig);
|
|
onConfigChange(newConfig);
|
|
},
|
|
[currentConfig, onConfigChange],
|
|
);
|
|
|
|
// 타일맵 소스 추가
|
|
const addTileMapSource = () => {
|
|
setTileMapSources([...tileMapSources, { id: `tilemap_${Date.now()}`, url: "" }]);
|
|
};
|
|
|
|
// 타일맵 소스 제거
|
|
const removeTileMapSource = (id: string) => {
|
|
if (tileMapSources.length === 1) return; // 최소 1개는 유지
|
|
setTileMapSources(tileMapSources.filter((s) => s.id !== id));
|
|
};
|
|
|
|
// 타일맵 소스 업데이트
|
|
const updateTileMapSource = (id: string, url: string) => {
|
|
setTileMapSources(tileMapSources.map((s) => (s.id === id ? { ...s, url } : s)));
|
|
// 첫 번째 타일맵 URL을 config에 저장
|
|
const firstUrl = id === tileMapSources[0].id ? url : tileMapSources[0].url;
|
|
updateConfig({ tileMapUrl: firstUrl });
|
|
};
|
|
|
|
// 외부 커넥션에서 URL 가져오기
|
|
const loadFromConnection = (sourceId: string, connectionId: string) => {
|
|
const connection = connections.find((c) => c.id?.toString() === connectionId);
|
|
if (connection) {
|
|
console.log("🔗 선택된 커넥션:", connection.connection_name, "→", connection.base_url);
|
|
updateTileMapSource(sourceId, connection.base_url);
|
|
}
|
|
};
|
|
|
|
// 사용 가능한 컬럼 목록
|
|
const availableColumns = queryResult?.columns || [];
|
|
const sampleData = queryResult?.rows?.[0] || {};
|
|
|
|
// 기상특보 데이터인지 감지 (reg_ko, wrn 컬럼이 있으면 기상특보)
|
|
const isWeatherAlertData = availableColumns.includes("reg_ko") && availableColumns.includes("wrn");
|
|
|
|
return (
|
|
<div className="space-y-3">
|
|
{/* 타일맵 URL 설정 (외부 커넥션 또는 직접 입력) */}
|
|
<div className="space-y-2">
|
|
<Label className="text-foreground text-xs font-medium">
|
|
타일맵 소스 (지도 배경)
|
|
<span className="text-destructive ml-1">*</span>
|
|
</Label>
|
|
|
|
{/* 외부 커넥션 선택 */}
|
|
<select
|
|
onChange={(e) => {
|
|
const connectionId = e.target.value;
|
|
if (connectionId) {
|
|
const connection = connections.find((c) => c.id?.toString() === connectionId);
|
|
if (connection) {
|
|
console.log("🗺️ 타일맵 커넥션 선택:", connection.connection_name, "→", connection.base_url);
|
|
updateConfig({ tileMapUrl: connection.base_url });
|
|
}
|
|
}
|
|
}}
|
|
className="border-border bg-background h-8 w-full rounded-md border px-2 py-1.5 text-xs"
|
|
>
|
|
<option value="">저장된 커넥션 선택</option>
|
|
{connections.map((conn) => (
|
|
<option key={conn.id} value={conn.id?.toString()}>
|
|
{conn.connection_name}
|
|
{conn.description && ` (${conn.description})`}
|
|
</option>
|
|
))}
|
|
</select>
|
|
|
|
{/* 타일맵 URL 직접 입력 */}
|
|
<Input
|
|
type="text"
|
|
value={currentConfig.tileMapUrl || ""}
|
|
onChange={(e) => updateConfig({ tileMapUrl: e.target.value })}
|
|
placeholder="https://api.vworld.kr/req/wmts/1.0.0/{API_KEY}/Base/{z}/{y}/{x}.png"
|
|
className="h-8 text-xs"
|
|
/>
|
|
<p className="text-muted-foreground text-xs">
|
|
💡 {"{z}/{y}/{x}"}는 그대로 입력하세요 (지도 라이브러리가 자동 치환)
|
|
</p>
|
|
</div>
|
|
|
|
{/* 타일맵 소스 목록 */}
|
|
{/* <div className="space-y-2">
|
|
<div className="flex items-center justify-between">
|
|
<label className="block text-xs font-medium text-foreground">
|
|
타일맵 소스 (REST API)
|
|
<span className="text-destructive ml-1">*</span>
|
|
</label>
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={addTileMapSource}
|
|
className="h-7 gap-1 text-xs"
|
|
>
|
|
<Plus className="h-3 w-3" />
|
|
추가
|
|
</Button>
|
|
</div>
|
|
|
|
{tileMapSources.map((source, index) => (
|
|
<div key={source.id} className="space-y-2 rounded-lg border border-border bg-muted p-3">
|
|
<div className="space-y-1">
|
|
<label className="block text-xs font-medium text-foreground">
|
|
외부 커넥션 선택 (선택사항)
|
|
</label>
|
|
<select
|
|
onChange={(e) => loadFromConnection(source.id, e.target.value)}
|
|
className="w-full px-2 py-1.5 border border-border rounded-md text-xs h-8 bg-background"
|
|
>
|
|
<option value="">직접 입력 또는 커넥션 선택</option>
|
|
{connections.map((conn) => (
|
|
<option key={conn.id} value={conn.id?.toString()}>
|
|
{conn.connection_name}
|
|
{conn.description && ` (${conn.description})`}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
<div className="flex gap-2">
|
|
<Input
|
|
type="text"
|
|
value={source.url}
|
|
onChange={(e) => updateTileMapSource(source.id, e.target.value)}
|
|
placeholder="https://api.vworld.kr/req/wmts/1.0.0/{API_KEY}/Base/{z}/{y}/{x}.png"
|
|
className="h-8 flex-1 text-xs"
|
|
/>
|
|
{tileMapSources.length > 1 && (
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={() => removeTileMapSource(source.id)}
|
|
className="h-8 w-8 text-muted-foreground hover:text-destructive"
|
|
>
|
|
<X className="h-4 w-4" />
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
|
|
<p className="text-xs text-muted-foreground">
|
|
💡 {'{z}/{y}/{x}'}는 그대로 입력하세요 (지도 라이브러리가 자동 치환)
|
|
</p>
|
|
</div> */}
|
|
|
|
{/* 지도 제목 */}
|
|
{/* <div className="space-y-1.5">
|
|
<label className="block text-xs font-medium text-foreground">지도 제목</label>
|
|
<Input
|
|
type="text"
|
|
value={currentConfig.title || ''}
|
|
onChange={(e) => updateConfig({ title: e.target.value })}
|
|
placeholder="위치 지도"
|
|
className="h-10 text-xs"
|
|
/>
|
|
</div> */}
|
|
|
|
{/* 구분선 */}
|
|
{/* <div className="border-t pt-3">
|
|
<h5 className="text-xs font-semibold text-foreground mb-2">📍 마커 데이터 설정 (선택사항)</h5>
|
|
<p className="text-xs text-muted-foreground mb-3">
|
|
데이터 소스 탭에서 API 또는 데이터베이스를 연결하면 마커를 표시할 수 있습니다.
|
|
</p>
|
|
</div> */}
|
|
|
|
{/* 쿼리 결과가 없을 때 */}
|
|
{/* {!queryResult && (
|
|
<div className="p-3 bg-warning/10 border border-warning rounded-lg">
|
|
<div className="text-warning text-xs">
|
|
💡 데이터 소스를 연결하고 쿼리를 실행하면 마커 설정이 가능합니다.
|
|
</div>
|
|
</div>
|
|
)} */}
|
|
|
|
{/* 데이터 필드 매핑 */}
|
|
{queryResult && !isWeatherAlertData && (
|
|
<>
|
|
{/* 위도 컬럼 설정 */}
|
|
<div className="space-y-1.5">
|
|
<label className="text-foreground block text-xs font-medium">위도 컬럼 (Latitude)</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)</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-1.5">
|
|
<label className="text-foreground block text-xs font-medium">상태 컬럼 (마커 색상)</label>
|
|
<select
|
|
value={currentConfig.statusColumn || ""}
|
|
onChange={(e) => updateConfig({ statusColumn: 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>
|
|
</>
|
|
)}
|
|
|
|
{/* 기상특보 데이터 안내 */}
|
|
{queryResult && isWeatherAlertData && (
|
|
<div className="bg-primary/10 border-primary rounded-lg border p-3">
|
|
<div className="text-primary text-xs">
|
|
🚨 기상특보 데이터가 감지되었습니다. 지역명(reg_ko)을 기준으로 자동으로 영역이 표시됩니다.
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{queryResult && (
|
|
<>
|
|
{/* 날씨 정보 표시 옵션 */}
|
|
<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="border-border text-primary focus:ring-primary 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="border-border text-primary focus:ring-primary 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.tileMapUrl ? "✅ 설정됨" : "❌ 미설정"}
|
|
</div>
|
|
<div>
|
|
<strong>위도:</strong> {currentConfig.latitudeColumn || "미설정"}
|
|
</div>
|
|
<div>
|
|
<strong>경도:</strong> {currentConfig.longitudeColumn || "미설정"}
|
|
</div>
|
|
<div>
|
|
<strong>라벨:</strong> {currentConfig.labelColumn || "없음"}
|
|
</div>
|
|
<div>
|
|
<strong>상태:</strong> {currentConfig.statusColumn || "없음"}
|
|
</div>
|
|
<div>
|
|
<strong>날씨 표시:</strong> {currentConfig.showWeather ? "활성화" : "비활성화"}
|
|
</div>
|
|
<div>
|
|
<strong>기상특보 표시:</strong> {currentConfig.showWeatherAlerts ? "활성화" : "비활성화"}
|
|
</div>
|
|
<div>
|
|
<strong>데이터 개수:</strong> {queryResult.rows.length}개
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{/* 필수 필드 확인 */}
|
|
{/* {!currentConfig.tileMapUrl && (
|
|
<div className="p-3 bg-destructive/10 border border-destructive rounded-lg">
|
|
<div className="text-destructive text-xs">
|
|
⚠️ 타일맵 URL을 입력해야 지도가 표시됩니다.
|
|
</div>
|
|
</div>
|
|
)} */}
|
|
</div>
|
|
);
|
|
}
|