ERP-node/frontend/components/admin/dashboard/MapTestConfigPanel.tsx

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-gray-700">
( )
<span className="text-red-500 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-gray-300 rounded-md text-xs h-8 bg-white"
>
<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-gray-700">
타일맵 소스 (REST API)
<span className="text-red-500 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-gray-200 bg-gray-50 p-3">
<div className="space-y-1">
<label className="block text-xs font-medium text-gray-600">
외부 커넥션 선택 (선택사항)
</label>
<select
onChange={(e) => loadFromConnection(source.id, e.target.value)}
className="w-full px-2 py-1.5 border border-gray-300 rounded-md text-xs h-8 bg-white"
>
<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-gray-500 hover:text-red-600"
>
<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-gray-700">지도 제목</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-gray-700 mb-2">📍 마커 데이터 설정 (선택사항)</h5>
<p className="text-xs text-muted-foreground mb-3">
데이터 소스 탭에서 API 또는 데이터베이스를 연결하면 마커를 표시할 수 있습니다.
</p>
</div> */}
{/* 쿼리 결과가 없을 때 */}
{/* {!queryResult && (
<div className="p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
<div className="text-yellow-800 text-xs">
💡 데이터 소스를 연결하고 쿼리를 실행하면 마커 설정이 가능합니다.
</div>
</div>
)} */}
{/* 데이터 필드 매핑 */}
{queryResult && !isWeatherAlertData && (
<>
{/* 위도 컬럼 설정 */}
<div className="space-y-1.5">
<label className="block text-xs font-medium text-gray-700">
(Latitude)
</label>
<select
value={currentConfig.latitudeColumn || ''}
onChange={(e) => updateConfig({ latitudeColumn: e.target.value })}
className="w-full px-2 py-1.5 border border-gray-300 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-gray-700">
(Longitude)
</label>
<select
value={currentConfig.longitudeColumn || ''}
onChange={(e) => updateConfig({ longitudeColumn: e.target.value })}
className="w-full px-2 py-1.5 border border-gray-300 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-gray-700">
( )
</label>
<select
value={currentConfig.labelColumn || ''}
onChange={(e) => updateConfig({ labelColumn: e.target.value })}
className="w-full px-2 py-1.5 border border-gray-300 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-gray-700">
( )
</label>
<select
value={currentConfig.statusColumn || ''}
onChange={(e) => updateConfig({ statusColumn: e.target.value })}
className="w-full px-2 py-1.5 border border-gray-300 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-blue-50 border border-blue-200 rounded-lg">
<div className="text-blue-800 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-gray-700 cursor-pointer">
<input
type="checkbox"
checked={currentConfig.showWeather || false}
onChange={(e) => updateConfig({ showWeather: e.target.checked })}
className="h-4 w-4 rounded border-gray-300 text-primary focus:ring-2 focus:ring-primary"
/>
<span> </span>
</label>
<p className="text-xs text-gray-500 ml-6">
</p>
</div>
<div className="space-y-1.5">
<label className="flex items-center gap-2 text-xs font-medium text-gray-700 cursor-pointer">
<input
type="checkbox"
checked={currentConfig.showWeatherAlerts || false}
onChange={(e) => updateConfig({ showWeatherAlerts: e.target.checked })}
className="h-4 w-4 rounded border-gray-300 text-primary focus:ring-2 focus:ring-primary"
/>
<span> </span>
</label>
<p className="text-xs text-gray-500 ml-6">
(/)
</p>
</div>
{/* 설정 미리보기 */}
<div className="p-3 bg-gray-50 rounded-lg">
<div className="text-xs font-medium text-gray-700 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-red-50 border border-red-200 rounded-lg">
<div className="text-red-800 text-xs">
⚠️ 타일맵 URL을 입력해야 지도가 표시됩니다.
</div>
</div>
)} */}
</div>
);
}