416 lines
16 KiB
TypeScript
416 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-xs font-medium text-foreground">
|
|
타일맵 소스 (지도 배경)
|
|
<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="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>
|
|
|
|
{/* 타일맵 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-xs text-muted-foreground">
|
|
💡 {'{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="block text-xs font-medium text-foreground">
|
|
위도 컬럼 (Latitude)
|
|
</label>
|
|
<select
|
|
value={currentConfig.latitudeColumn || ''}
|
|
onChange={(e) => updateConfig({ latitudeColumn: e.target.value })}
|
|
className="w-full px-2 py-1.5 border border-border rounded-lg 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-foreground">
|
|
경도 컬럼 (Longitude)
|
|
</label>
|
|
<select
|
|
value={currentConfig.longitudeColumn || ''}
|
|
onChange={(e) => updateConfig({ longitudeColumn: e.target.value })}
|
|
className="w-full px-2 py-1.5 border border-border rounded-lg 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-foreground">
|
|
라벨 컬럼 (마커 표시명)
|
|
</label>
|
|
<select
|
|
value={currentConfig.labelColumn || ''}
|
|
onChange={(e) => updateConfig({ labelColumn: e.target.value })}
|
|
className="w-full px-2 py-1.5 border border-border rounded-lg 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="block text-xs font-medium text-foreground">
|
|
상태 컬럼 (마커 색상)
|
|
</label>
|
|
<select
|
|
value={currentConfig.statusColumn || ''}
|
|
onChange={(e) => updateConfig({ statusColumn: e.target.value })}
|
|
className="w-full px-2 py-1.5 border border-border rounded-lg text-xs"
|
|
>
|
|
<option value="">선택하세요 (선택사항)</option>
|
|
{availableColumns.map((col) => (
|
|
<option key={col} value={col}>
|
|
{col}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{/* 기상특보 데이터 안내 */}
|
|
{queryResult && isWeatherAlertData && (
|
|
<div className="p-3 bg-primary/10 border border-primary rounded-lg">
|
|
<div className="text-primary text-xs">
|
|
🚨 기상특보 데이터가 감지되었습니다. 지역명(reg_ko)을 기준으로 자동으로 영역이 표시됩니다.
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{queryResult && (
|
|
<>
|
|
|
|
{/* 날씨 정보 표시 옵션 */}
|
|
<div className="space-y-1.5">
|
|
<label className="flex items-center gap-2 text-xs font-medium text-foreground cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
checked={currentConfig.showWeather || false}
|
|
onChange={(e) => updateConfig({ showWeather: e.target.checked })}
|
|
className="h-4 w-4 rounded border-border text-primary focus:ring-2 focus:ring-primary"
|
|
/>
|
|
<span>날씨 정보 표시</span>
|
|
</label>
|
|
<p className="text-xs text-muted-foreground ml-6">
|
|
마커 팝업에 해당 위치의 날씨 정보를 함께 표시합니다
|
|
</p>
|
|
</div>
|
|
|
|
<div className="space-y-1.5">
|
|
<label className="flex items-center gap-2 text-xs font-medium text-foreground cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
checked={currentConfig.showWeatherAlerts || false}
|
|
onChange={(e) => updateConfig({ showWeatherAlerts: e.target.checked })}
|
|
className="h-4 w-4 rounded border-border text-primary focus:ring-2 focus:ring-primary"
|
|
/>
|
|
<span>기상특보 영역 표시</span>
|
|
</label>
|
|
<p className="text-xs text-muted-foreground ml-6">
|
|
현재 발효 중인 기상특보(주의보/경보)를 지도에 색상 영역으로 표시합니다
|
|
</p>
|
|
</div>
|
|
|
|
{/* 설정 미리보기 */}
|
|
<div className="p-3 bg-muted rounded-lg">
|
|
<div className="text-xs font-medium text-foreground mb-2">📋 설정 미리보기</div>
|
|
<div className="text-xs text-muted-foreground space-y-1">
|
|
<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>
|
|
);
|
|
}
|
|
|