feat(korea): AI 해양분석 챗 (Qwen 2.5) + 이란 발전소 29개 확장 + UI 개선
- AI 해양분석 챗패널 추가 (AiChatPanel, Ollama/Qwen 2.5:7b) - 시스템 프롬프트에 실시간 선박 데이터 자동 주입 - 보라/퍼플 톤 UI 차별화 - Vite 프록시 /ollama 추가 - 이란 발전소 20→29개 확장 (Wikipedia 기반 좌표/용량 보정) - 선박 현황 폰트 사이즈 축소 (11→9px, 13→10px) - OSINT LIVE 3개, 재난뉴스 2개 표시 + 스크롤 - 한국/중국 선박현황, 조업분석 기본 접힘 - AI 해양분석 기본 펼침 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
부모
8448ea7985
커밋
be77d97eb3
@ -6,6 +6,7 @@ import { getDisasterNews, getDisasterCatIcon, getDisasterCatColor } from '../../
|
||||
import { getMarineTrafficCategory } from '../../utils/marineTraffic';
|
||||
import { aggregateFishingStats, GEAR_LABELS } from '../../utils/fishingAnalysis';
|
||||
import type { FishingGearType } from '../../utils/fishingAnalysis';
|
||||
import { AiChatPanel } from '../korea/AiChatPanel';
|
||||
|
||||
type DashboardTab = 'iran' | 'korea';
|
||||
|
||||
@ -349,7 +350,7 @@ function useTimeAgo() {
|
||||
export function EventLog({ events, currentTime, totalShipCount: _totalShipCount, koreanShips, koreanShipsByCategory: _koreanShipsByCategory, chineseShips = [], osintFeed = EMPTY_OSINT, isLive = false, dashboardTab = 'iran', onTabChange: _onTabChange, ships = EMPTY_SHIPS, highlightKoreanShips = false, onToggleHighlightKorean, onShipHover, onShipClick }: Props) {
|
||||
const { t } = useTranslation(['common', 'events', 'ships']);
|
||||
const timeAgo = useTimeAgo();
|
||||
const [collapsed, setCollapsed] = useState<Set<string>>(new Set(['kr-ships', 'cn-ships']));
|
||||
const [collapsed, setCollapsed] = useState<Set<string>>(new Set(['kr-ships', 'cn-ships', 'cn-fishing']));
|
||||
const toggleCollapse = useCallback((key: string) => {
|
||||
setCollapsed(prev => {
|
||||
const next = new Set(prev);
|
||||
@ -633,12 +634,12 @@ export function EventLog({ events, currentTime, totalShipCount: _totalShipCount,
|
||||
borderLeft: `3px solid ${mtColor}`,
|
||||
}}>
|
||||
<span style={{ width: 8, height: 8, borderRadius: '50%', background: mtColor, flexShrink: 0 }} />
|
||||
<span style={{ fontSize: 11, fontWeight: 700, minWidth: 70, color: mtColor }}>{mtLabel}</span>
|
||||
<span style={{ fontSize: 13, fontWeight: 700, color: 'var(--kcg-text)' }}>
|
||||
{list.length}<span style={{ fontSize: 9, fontWeight: 400, color: 'var(--kcg-muted)' }}>{t('common:units.vessels')}</span>
|
||||
<span style={{ fontSize: 9, fontWeight: 700, minWidth: 60, color: mtColor }}>{mtLabel}</span>
|
||||
<span style={{ fontSize: 10, fontWeight: 700, color: 'var(--kcg-text)' }}>
|
||||
{list.length}<span style={{ fontSize: 8, fontWeight: 400, color: 'var(--kcg-muted)' }}>{t('common:units.vessels')}</span>
|
||||
</span>
|
||||
<span style={{ marginLeft: 'auto', fontSize: 9, color: '#22c55e' }}>{t('ships:status.underway')} {moving}</span>
|
||||
<span style={{ fontSize: 9, color: '#ef4444' }}>{t('ships:status.anchored')} {anchored}</span>
|
||||
<span style={{ marginLeft: 'auto', fontSize: 8, color: '#22c55e' }}>{t('ships:status.underway')} {moving}</span>
|
||||
<span style={{ fontSize: 8, color: '#ef4444' }}>{t('ships:status.anchored')} {anchored}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@ -688,12 +689,12 @@ export function EventLog({ events, currentTime, totalShipCount: _totalShipCount,
|
||||
borderLeft: `3px solid ${mtColor}`,
|
||||
}}>
|
||||
<span style={{ width: 8, height: 8, borderRadius: '50%', background: mtColor, flexShrink: 0 }} />
|
||||
<span style={{ fontSize: 11, fontWeight: 700, minWidth: 70, color: mtColor }}>{mtLabel}</span>
|
||||
<span style={{ fontSize: 13, fontWeight: 700, color: 'var(--kcg-text)' }}>
|
||||
{list.length}<span style={{ fontSize: 9, fontWeight: 400, color: 'var(--kcg-muted)' }}>{t('common:units.vessels')}</span>
|
||||
<span style={{ fontSize: 9, fontWeight: 700, minWidth: 60, color: mtColor }}>{mtLabel}</span>
|
||||
<span style={{ fontSize: 10, fontWeight: 700, color: 'var(--kcg-text)' }}>
|
||||
{list.length}<span style={{ fontSize: 8, fontWeight: 400, color: 'var(--kcg-muted)' }}>{t('common:units.vessels')}</span>
|
||||
</span>
|
||||
<span style={{ marginLeft: 'auto', fontSize: 9, color: '#22c55e' }}>{t('ships:status.underway')} {moving}</span>
|
||||
<span style={{ fontSize: 9, color: '#ef4444' }}>{t('ships:status.anchored')} {anchored}</span>
|
||||
<span style={{ marginLeft: 'auto', fontSize: 8, color: '#22c55e' }}>{t('ships:status.underway')} {moving}</span>
|
||||
<span style={{ fontSize: 8, color: '#ef4444' }}>{t('ships:status.anchored')} {anchored}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@ -783,7 +784,7 @@ export function EventLog({ events, currentTime, totalShipCount: _totalShipCount,
|
||||
</a>
|
||||
</div>
|
||||
{!collapsed.has('disaster-news') && (
|
||||
<div className="osint-list" style={{ maxHeight: 310, overflowY: 'auto' }}>
|
||||
<div className="osint-list" style={{ maxHeight: 110, overflowY: 'auto' }}>
|
||||
{disasterItems.map(item => {
|
||||
const icon = getDisasterCatIcon(item.category);
|
||||
const color = getDisasterCatColor(item.category);
|
||||
@ -832,7 +833,7 @@ export function EventLog({ events, currentTime, totalShipCount: _totalShipCount,
|
||||
})()}</span>
|
||||
</div>
|
||||
{!collapsed.has('osint-korea') && (
|
||||
<div className="osint-list">
|
||||
<div className="osint-list" style={{ maxHeight: 165, overflowY: 'auto' }}>
|
||||
{(() => {
|
||||
const filtered = osintFeed.filter(item => item.category !== 'general' && item.category !== 'oil');
|
||||
const seen = new Set<string>();
|
||||
@ -879,6 +880,16 @@ export function EventLog({ events, currentTime, totalShipCount: _totalShipCount,
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* AI 해양분석 챗 */}
|
||||
{isLive && dashboardTab === 'korea' && (
|
||||
<AiChatPanel
|
||||
ships={ships}
|
||||
koreanShipCount={_koreanShipsByCategory ? Object.values(_koreanShipsByCategory).reduce((a, b) => a + b, 0) : 0}
|
||||
chineseShipCount={chineseShips?.length ?? 0}
|
||||
totalShipCount={_totalShipCount}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
264
frontend/src/components/korea/AiChatPanel.tsx
Normal file
264
frontend/src/components/korea/AiChatPanel.tsx
Normal file
@ -0,0 +1,264 @@
|
||||
import { useState, useRef, useEffect, useCallback } from 'react';
|
||||
import type { Ship } from '../../types';
|
||||
|
||||
interface ChatMessage {
|
||||
role: 'user' | 'assistant' | 'system';
|
||||
content: string;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
ships: Ship[];
|
||||
koreanShipCount: number;
|
||||
chineseShipCount: number;
|
||||
totalShipCount: number;
|
||||
}
|
||||
|
||||
const OLLAMA_URL = '/ollama/api/chat';
|
||||
|
||||
function buildSystemPrompt(props: Props): string {
|
||||
const { ships, koreanShipCount, chineseShipCount, totalShipCount } = props;
|
||||
|
||||
// 선박 유형별 통계
|
||||
const byType: Record<string, number> = {};
|
||||
const byFlag: Record<string, number> = {};
|
||||
ships.forEach(s => {
|
||||
byType[s.category || 'unknown'] = (byType[s.category || 'unknown'] || 0) + 1;
|
||||
byFlag[s.flag || 'unknown'] = (byFlag[s.flag || 'unknown'] || 0) + 1;
|
||||
});
|
||||
|
||||
// 중국 어선 통계
|
||||
const cnFishing = ships.filter(s => s.flag === 'CN' && (s.category === 'fishing' || s.typecode === '30'));
|
||||
const cnFishingOperating = cnFishing.filter(s => s.speed >= 2 && s.speed <= 5);
|
||||
|
||||
return `당신은 대한민국 해양경찰청의 해양상황 분석 AI 어시스턴트입니다.
|
||||
현재 실시간 해양 모니터링 데이터를 기반으로 분석을 제공합니다.
|
||||
|
||||
## 현재 해양 상황 요약
|
||||
- 전체 선박: ${totalShipCount}척
|
||||
- 한국 선박: ${koreanShipCount}척
|
||||
- 중국 선박: ${chineseShipCount}척
|
||||
- 중국 어선: ${cnFishing.length}척 (조업 추정: ${cnFishingOperating.length}척)
|
||||
|
||||
## 선박 유형별 현황
|
||||
${Object.entries(byType).map(([k, v]) => `- ${k}: ${v}척`).join('\n')}
|
||||
|
||||
## 국적별 현황 (상위)
|
||||
${Object.entries(byFlag).sort((a, b) => b[1] - a[1]).slice(0, 10).map(([k, v]) => `- ${k}: ${v}척`).join('\n')}
|
||||
|
||||
## 한중어업협정 핵심
|
||||
- 중국 허가어선 906척 (PT 저인망 323쌍, GN 유자망 200척, PS 위망 16척, OT 1척식 13척, FC 운반선 31척)
|
||||
- 특정어업수역 I~IV에서만 조업 허가
|
||||
- 휴어기: PT/OT 4/16~10/15, GN 6/2~8/31
|
||||
- 다크베셀(AIS 차단) 감시 필수
|
||||
|
||||
## 응답 규칙
|
||||
- 한국어로 답변
|
||||
- 간결하고 분석적으로
|
||||
- 데이터 기반 답변 우선
|
||||
- 불법조업 의심 시 근거 제시
|
||||
- 필요시 조치 권고 포함`;
|
||||
}
|
||||
|
||||
export function AiChatPanel({ ships, koreanShipCount, chineseShipCount, totalShipCount }: Props) {
|
||||
const [isOpen, setIsOpen] = useState(true);
|
||||
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
||||
const [input, setInput] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
}, [messages]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) inputRef.current?.focus();
|
||||
}, [isOpen]);
|
||||
|
||||
const sendMessage = useCallback(async () => {
|
||||
if (!input.trim() || isLoading) return;
|
||||
|
||||
const userMsg: ChatMessage = { role: 'user', content: input.trim(), timestamp: Date.now() };
|
||||
setMessages(prev => [...prev, userMsg]);
|
||||
setInput('');
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const systemPrompt = buildSystemPrompt({ ships, koreanShipCount, chineseShipCount, totalShipCount });
|
||||
const apiMessages = [
|
||||
{ role: 'system', content: systemPrompt },
|
||||
...messages.filter(m => m.role !== 'system').map(m => ({ role: m.role, content: m.content })),
|
||||
{ role: 'user', content: userMsg.content },
|
||||
];
|
||||
|
||||
const res = await fetch(OLLAMA_URL, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
model: 'qwen2.5:7b',
|
||||
messages: apiMessages,
|
||||
stream: false,
|
||||
options: { temperature: 0.3, num_predict: 1024 },
|
||||
}),
|
||||
});
|
||||
|
||||
if (!res.ok) throw new Error(`Ollama error: ${res.status}`);
|
||||
const data = await res.json();
|
||||
const assistantMsg: ChatMessage = {
|
||||
role: 'assistant',
|
||||
content: data.message?.content || '응답을 생성할 수 없습니다.',
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
setMessages(prev => [...prev, assistantMsg]);
|
||||
} catch (err) {
|
||||
setMessages(prev => [...prev, {
|
||||
role: 'assistant',
|
||||
content: `오류: ${err instanceof Error ? err.message : 'AI 서버 연결 실패'}. Ollama가 실행 중인지 확인하세요.`,
|
||||
timestamp: Date.now(),
|
||||
}]);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [input, isLoading, messages, ships, koreanShipCount, chineseShipCount, totalShipCount]);
|
||||
|
||||
const quickQuestions = [
|
||||
'현재 해양 상황을 요약해줘',
|
||||
'중국어선 불법조업 의심 분석해줘',
|
||||
'서해 위험도를 평가해줘',
|
||||
'다크베셀 현황 분석해줘',
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
borderTop: '1px solid rgba(168,85,247,0.2)',
|
||||
marginTop: 8,
|
||||
}}>
|
||||
{/* Toggle header */}
|
||||
<div
|
||||
onClick={() => setIsOpen(p => !p)}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: 6,
|
||||
padding: '6px 8px', cursor: 'pointer',
|
||||
background: isOpen ? 'rgba(168,85,247,0.12)' : 'rgba(168,85,247,0.04)',
|
||||
borderRadius: 4,
|
||||
borderLeft: '2px solid rgba(168,85,247,0.5)',
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: 12 }}>🤖</span>
|
||||
<span style={{ fontSize: 11, fontWeight: 700, color: '#c4b5fd' }}>AI 해양분석</span>
|
||||
<span style={{ fontSize: 8, color: '#8b5cf6', marginLeft: 2, background: 'rgba(139,92,246,0.15)', padding: '1px 5px', borderRadius: 3 }}>Qwen 2.5</span>
|
||||
<span style={{ marginLeft: 'auto', fontSize: 8, color: '#8b5cf6' }}>
|
||||
{isOpen ? '▼' : '▶'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Chat body */}
|
||||
{isOpen && (
|
||||
<div style={{
|
||||
display: 'flex', flexDirection: 'column',
|
||||
height: 360, background: 'rgba(88,28,135,0.08)',
|
||||
borderRadius: '0 0 6px 6px', overflow: 'hidden',
|
||||
borderLeft: '2px solid rgba(168,85,247,0.3)',
|
||||
borderBottom: '1px solid rgba(168,85,247,0.15)',
|
||||
}}>
|
||||
{/* Messages */}
|
||||
<div style={{
|
||||
flex: 1, overflowY: 'auto', padding: '6px 8px',
|
||||
display: 'flex', flexDirection: 'column', gap: 6,
|
||||
}}>
|
||||
{messages.length === 0 && (
|
||||
<div style={{ padding: '12px 0', textAlign: 'center' }}>
|
||||
<div style={{ fontSize: 10, color: '#a78bfa', marginBottom: 8 }}>
|
||||
해양 상황에 대해 질문하세요
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||
{quickQuestions.map((q, i) => (
|
||||
<button
|
||||
key={i}
|
||||
onClick={() => { setInput(q); }}
|
||||
style={{
|
||||
background: 'rgba(139,92,246,0.08)',
|
||||
border: '1px solid rgba(139,92,246,0.25)',
|
||||
borderRadius: 4, padding: '4px 8px',
|
||||
fontSize: 9, color: '#a78bfa',
|
||||
cursor: 'pointer', textAlign: 'left',
|
||||
}}
|
||||
>
|
||||
{q}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{messages.map((msg, i) => (
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
alignSelf: msg.role === 'user' ? 'flex-end' : 'flex-start',
|
||||
maxWidth: '85%',
|
||||
background: msg.role === 'user'
|
||||
? 'rgba(139,92,246,0.25)'
|
||||
: 'rgba(168,85,247,0.08)',
|
||||
borderRadius: msg.role === 'user' ? '8px 8px 2px 8px' : '8px 8px 8px 2px',
|
||||
padding: '6px 8px',
|
||||
fontSize: 10,
|
||||
color: '#e2e8f0',
|
||||
lineHeight: 1.5,
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-word',
|
||||
}}
|
||||
>
|
||||
{msg.content}
|
||||
</div>
|
||||
))}
|
||||
{isLoading && (
|
||||
<div style={{
|
||||
alignSelf: 'flex-start', padding: '6px 8px',
|
||||
background: 'rgba(168,85,247,0.08)', borderRadius: 8,
|
||||
fontSize: 10, color: '#a78bfa',
|
||||
}}>
|
||||
분석 중...
|
||||
</div>
|
||||
)}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
|
||||
{/* Input */}
|
||||
<div style={{
|
||||
display: 'flex', gap: 4, padding: '6px 8px',
|
||||
borderTop: '1px solid rgba(255,255,255,0.06)',
|
||||
background: 'rgba(0,0,0,0.15)',
|
||||
}}>
|
||||
<input
|
||||
ref={inputRef}
|
||||
value={input}
|
||||
onChange={e => setInput(e.target.value)}
|
||||
onKeyDown={e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); } }}
|
||||
placeholder="해양 상황 질문..."
|
||||
disabled={isLoading}
|
||||
style={{
|
||||
flex: 1, background: 'rgba(139,92,246,0.06)',
|
||||
border: '1px solid rgba(139,92,246,0.2)',
|
||||
borderRadius: 4, padding: '5px 8px',
|
||||
fontSize: 10, color: '#e2e8f0', outline: 'none',
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
onClick={sendMessage}
|
||||
disabled={isLoading || !input.trim()}
|
||||
style={{
|
||||
background: isLoading || !input.trim() ? '#334155' : '#7c3aed',
|
||||
border: 'none', borderRadius: 4,
|
||||
padding: '4px 10px', fontSize: 10, fontWeight: 700,
|
||||
color: '#fff', cursor: isLoading ? 'not-allowed' : 'pointer',
|
||||
}}
|
||||
>
|
||||
전송
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -72,29 +72,43 @@ export const ME_ENERGY_HAZARD_FACILITIES: EnergyHazardFacility[] = [
|
||||
{ id: 'IL-H04', name: 'Eilat-Ashkelon Pipeline Terminal', nameKo: 'EAPC 에일라트 터미널', lat: 29.5500, lng: 34.9500, country: 'IL', countryKey: 'il', category: 'hazard', subType: 'oil_tank', description: '홍해 원유 수입 파이프라인 터미널' },
|
||||
|
||||
// ════════════════════════════════════════════
|
||||
// 🇮🇷 이란
|
||||
// 🇮🇷 이란 (Wikipedia + OSINT 기반)
|
||||
// 총 설치용량 ~85,000MW, 화력 95%+, 수력 ~12,000MW
|
||||
// ════════════════════════════════════════════
|
||||
// Energy
|
||||
{ id: 'IR-E01', name: 'Bushehr Nuclear Power Plant', nameKo: '부셰르 원자력발전소', lat: 28.8267, lng: 50.8867, country: 'IR', countryKey: 'ir', category: 'energy', subType: 'nuclear', capacityMW: 1000, description: '이란 유일 상업 원전 (VVER-1000)' },
|
||||
{ id: 'IR-E02', name: 'Ramin Thermal Power Plant', nameKo: '라민 화력발전소', lat: 31.3100, lng: 48.7400, country: 'IR', countryKey: 'ir', category: 'energy', subType: 'thermal', capacityMW: 1890, description: '아바즈 인근 증기 화력발전소' },
|
||||
{ id: 'IR-E03', name: 'Neka Thermal Power Plant', nameKo: '네카 화력발전소', lat: 36.6500, lng: 53.3300, country: 'IR', countryKey: 'ir', category: 'energy', subType: 'thermal', capacityMW: 2074, description: '카스피해 연안 최대 화력발전소' },
|
||||
{ id: 'IR-E04', name: 'Shahid Rajaee Power Plant', nameKo: '샤히드 라자이 발전소', lat: 36.3700, lng: 52.9900, country: 'IR', countryKey: 'ir', category: 'energy', subType: 'thermal', capacityMW: 1780, description: '가즈빈 복합화력' },
|
||||
{ id: 'IR-E05', name: 'Isfahan Nuclear Facility (UCF)', nameKo: '이스파한 핵시설 (UCF)', lat: 32.7200, lng: 51.7200, country: 'IR', countryKey: 'ir', category: 'energy', subType: 'nuclear', description: '우라늄전환시설' },
|
||||
{ id: 'IR-E06', name: 'Natanz Enrichment Facility', nameKo: '나탄즈 우라늄농축시설', lat: 33.7250, lng: 51.7267, country: 'IR', countryKey: 'ir', category: 'energy', subType: 'nuclear', description: '주요 원심분리기 농축시설 (지하)' },
|
||||
{ id: 'IR-E07', name: 'Manjil-Rudbar Wind Farm', nameKo: '만질-루드바르 풍력단지', lat: 36.7400, lng: 49.4000, country: 'IR', countryKey: 'ir', category: 'energy', subType: 'wind', capacityMW: 91, description: '이란 최대 풍력발전단지' },
|
||||
{ id: 'IR-E08', name: 'Bandar Abbas Power Plant', nameKo: '반다르아바스 발전소', lat: 27.2000, lng: 56.2500, country: 'IR', countryKey: 'ir', category: 'energy', subType: 'thermal', capacityMW: 1057, description: '호르무즈해협 연안 발전소' },
|
||||
{ id: 'IR-E09', name: 'Shahid Montazeri Power Plant', nameKo: '샤히드 몬타제리 발전소', lat: 32.6500, lng: 51.6800, country: 'IR', countryKey: 'ir', category: 'energy', subType: 'thermal', capacityMW: 1600, description: '이스파한 복합화력 발전소' },
|
||||
{ id: 'IR-E10', name: 'Shahid Salimi (Neka) Power Plant', nameKo: '샤히드 살리미 발전소', lat: 36.6300, lng: 53.3000, country: 'IR', countryKey: 'ir', category: 'energy', subType: 'thermal', capacityMW: 2400, description: '마잔다란주 가스복합 발전소' },
|
||||
{ id: 'IR-E11', name: 'Besat Power Plant', nameKo: '베사트 발전소', lat: 35.8300, lng: 50.9800, country: 'IR', countryKey: 'ir', category: 'energy', subType: 'thermal', capacityMW: 1000, description: '테헤란 남부 가스터빈 발전소' },
|
||||
{ id: 'IR-E12', name: 'Parand Power Plant', nameKo: '파란드 복합화력발전소', lat: 35.4700, lng: 51.0100, country: 'IR', countryKey: 'ir', category: 'energy', subType: 'thermal', capacityMW: 1536, description: '테헤란 서남부 복합화력' },
|
||||
{ id: 'IR-E13', name: 'Shahid Rajaei Dam & Hydro', nameKo: '샤히드 라자이 수력발전소', lat: 36.1500, lng: 53.2800, country: 'IR', countryKey: 'ir', category: 'energy', subType: 'power', capacityMW: 225, description: '사리 인근 수력발전 댐' },
|
||||
{ id: 'IR-E14', name: 'Karun-3 Hydropower Plant', nameKo: '카룬-3 수력발전소', lat: 31.9200, lng: 49.8500, country: 'IR', countryKey: 'ir', category: 'energy', subType: 'power', capacityMW: 2000, description: '이란 최대 수력발전소 (후제스탄주)' },
|
||||
{ id: 'IR-E15', name: 'Dez Dam Hydropower', nameKo: '데즈댐 수력발전소', lat: 32.6100, lng: 48.7700, country: 'IR', countryKey: 'ir', category: 'energy', subType: 'power', capacityMW: 520, description: '후제스탄주 데즈강 수력발전' },
|
||||
{ id: 'IR-E16', name: 'Tabriz Thermal Power Plant', nameKo: '타브리즈 화력발전소', lat: 38.0600, lng: 46.3200, country: 'IR', countryKey: 'ir', category: 'energy', subType: 'thermal', capacityMW: 1386, description: '동아제르바이잔주 복합화력' },
|
||||
{ id: 'IR-E17', name: 'Zarand Solar Power Plant', nameKo: '자란드 태양광발전소', lat: 30.8100, lng: 56.5600, country: 'IR', countryKey: 'ir', category: 'energy', subType: 'power', capacityMW: 10, description: '케르만주 태양광 시범단지' },
|
||||
{ id: 'IR-E18', name: 'Fordow Enrichment Facility', nameKo: '포르도 우라늄농축시설', lat: 34.8800, lng: 51.5800, country: 'IR', countryKey: 'ir', category: 'energy', subType: 'nuclear', description: '지하 우라늄 농축시설 (FFEP, 쿰 인근)' },
|
||||
{ id: 'IR-E19', name: 'Arak Heavy Water Reactor', nameKo: '아라크 중수로', lat: 34.0400, lng: 49.2400, country: 'IR', countryKey: 'ir', category: 'energy', subType: 'nuclear', description: 'IR-40 중수 연구용 원자로 (마르카지주)' },
|
||||
{ id: 'IR-E20', name: 'Binaloud Wind Farm', nameKo: '비날루드 풍력단지', lat: 36.2300, lng: 58.6900, country: 'IR', countryKey: 'ir', category: 'energy', subType: 'wind', capacityMW: 28, description: '라자비호라산주 풍력발전' },
|
||||
// ── 화력발전소 (Thermal) ──
|
||||
{ id: 'IR-E01', name: 'Damavand Power Plant', nameKo: '다마반드 발전소', lat: 35.5200, lng: 51.9800, country: 'IR', countryKey: 'ir', category: 'energy', subType: 'thermal', capacityMW: 2900, description: '이란 최대 화력발전소, 테헤란 남동 50km (가스복합)' },
|
||||
{ id: 'IR-E02', name: 'Shahid Salimi (Neka) Power Plant', nameKo: '샤히드 살리미(네카) 발전소', lat: 36.6500, lng: 53.3300, country: 'IR', countryKey: 'ir', category: 'energy', subType: 'thermal', capacityMW: 2214, description: '마잔다란주, 이란 2위 화력 (카스피해 연안)' },
|
||||
{ id: 'IR-E03', name: 'Shahid Rajaee Combined Cycle', nameKo: '샤히드 라자이 복합화력', lat: 36.3700, lng: 49.9900, country: 'IR', countryKey: 'ir', category: 'energy', subType: 'thermal', capacityMW: 2042, description: '가즈빈주, 이란 3위 복합화력' },
|
||||
{ id: 'IR-E04', name: 'Ramin Steam Power Plant', nameKo: '라민 증기화력발전소', lat: 31.3100, lng: 48.7400, country: 'IR', countryKey: 'ir', category: 'energy', subType: 'thermal', capacityMW: 1890, description: '후제스탄주 아바즈 인근 증기터빈' },
|
||||
{ id: 'IR-E05', name: 'Shahid Montazeri Power Plant', nameKo: '샤히드 몬타제리 발전소', lat: 32.6500, lng: 51.6800, country: 'IR', countryKey: 'ir', category: 'energy', subType: 'thermal', capacityMW: 1600, description: '이스파한, 1984년 가동 개시' },
|
||||
{ id: 'IR-E06', name: 'Parand Combined Cycle', nameKo: '파란드 복합화력', lat: 35.4700, lng: 51.0100, country: 'IR', countryKey: 'ir', category: 'energy', subType: 'thermal', capacityMW: 1536, description: '테헤란 서남부 복합화력' },
|
||||
{ id: 'IR-E07', name: 'Tabriz Thermal Power Plant', nameKo: '타브리즈 화력발전소', lat: 38.0600, lng: 46.3200, country: 'IR', countryKey: 'ir', category: 'energy', subType: 'thermal', capacityMW: 1386, description: '동아제르바이잔주' },
|
||||
{ id: 'IR-E08', name: 'Bandar Abbas Power Plant', nameKo: '반다르아바스 발전소', lat: 27.2000, lng: 56.2500, country: 'IR', countryKey: 'ir', category: 'energy', subType: 'thermal', capacityMW: 1057, description: '호르무즈해협 연안' },
|
||||
{ id: 'IR-E09', name: 'Besat Power Plant', nameKo: '베사트 발전소', lat: 35.8300, lng: 50.9800, country: 'IR', countryKey: 'ir', category: 'energy', subType: 'thermal', capacityMW: 1000, description: '테헤란 남부 가스터빈' },
|
||||
{ id: 'IR-E10', name: 'Tous Power Plant', nameKo: '투스 발전소', lat: 36.3100, lng: 59.5800, country: 'IR', countryKey: 'ir', category: 'energy', subType: 'thermal', capacityMW: 1470, description: '마슈하드 인근 복합화력' },
|
||||
{ id: 'IR-E11', name: 'Fars (Shahid Dastjerdi) Power Plant', nameKo: '파르스 발전소', lat: 29.6000, lng: 52.5000, country: 'IR', countryKey: 'ir', category: 'energy', subType: 'thermal', capacityMW: 1028, description: '시라즈 인근 가스복합' },
|
||||
{ id: 'IR-E12', name: 'Hormozgan Power Plant', nameKo: '호르모즈간 발전소', lat: 27.1800, lng: 56.3000, country: 'IR', countryKey: 'ir', category: 'energy', subType: 'thermal', capacityMW: 906, description: '호르모즈간주 가스복합' },
|
||||
{ id: 'IR-E13', name: 'Shahid Mofateh Power Plant', nameKo: '샤히드 모파테 발전소', lat: 34.7700, lng: 48.5200, country: 'IR', countryKey: 'ir', category: 'energy', subType: 'thermal', capacityMW: 1000, description: '하메단주 복합화력' },
|
||||
{ id: 'IR-E14', name: 'Kerman Combined Cycle', nameKo: '케르만 복합화력', lat: 30.2600, lng: 57.0700, country: 'IR', countryKey: 'ir', category: 'energy', subType: 'thermal', capacityMW: 928, description: '케르만주 복합화력' },
|
||||
{ id: 'IR-E15', name: 'Yazd Combined Cycle', nameKo: '야즈드 복합화력', lat: 31.9000, lng: 54.3700, country: 'IR', countryKey: 'ir', category: 'energy', subType: 'thermal', capacityMW: 948, description: '야즈드주 가스복합' },
|
||||
// ── 수력발전소 (Hydro) ──
|
||||
{ id: 'IR-E16', name: 'Karun-3 (Shahid Rajaee) Dam', nameKo: '카룬-3 수력발전소', lat: 31.8055, lng: 50.0893, country: 'IR', countryKey: 'ir', category: 'energy', subType: 'power', capacityMW: 2280, description: '이란 최대 수력, 후제스탄주 이제 SE 28km, 8기' },
|
||||
{ id: 'IR-E17', name: 'Shahid Abbaspour (Karun-1) Dam', nameKo: '카룬-1 (샤히드 아바스푸르) 수력', lat: 32.0519, lng: 49.6069, country: 'IR', countryKey: 'ir', category: 'energy', subType: 'power', capacityMW: 2000, description: '후제스탄주 마스제드솔레이만 NE 50km' },
|
||||
{ id: 'IR-E18', name: 'Karun-4 Dam', nameKo: '카룬-4 수력발전소', lat: 31.5969, lng: 50.4712, country: 'IR', countryKey: 'ir', category: 'energy', subType: 'power', capacityMW: 1000, description: '카룬강 상류, 2011년 가동' },
|
||||
{ id: 'IR-E19', name: 'Dez Dam Hydropower', nameKo: '데즈댐 수력발전소', lat: 32.6053, lng: 48.4640, country: 'IR', countryKey: 'ir', category: 'energy', subType: 'power', capacityMW: 520, description: '후제스탄주 안디메쉬크 NE 20km, 8기' },
|
||||
{ id: 'IR-E20', name: 'Masjed Soleiman Dam', nameKo: '마스제드솔레이만 수력', lat: 32.0300, lng: 49.2800, country: 'IR', countryKey: 'ir', category: 'energy', subType: 'power', capacityMW: 2000, description: '카룬강 하류, 대형 아치댐' },
|
||||
// ── 원자력/핵시설 (Nuclear) ──
|
||||
{ id: 'IR-E21', name: 'Bushehr Nuclear Power Plant', nameKo: '부셰르 원자력발전소', lat: 28.8267, lng: 50.8867, country: 'IR', countryKey: 'ir', category: 'energy', subType: 'nuclear', capacityMW: 915, description: '이란 유일 상업 원전 (VVER-1000), 1995 러시아 계약' },
|
||||
{ id: 'IR-E22', name: 'Natanz Enrichment Facility', nameKo: '나탄즈 우라늄농축시설', lat: 33.7250, lng: 51.7267, country: 'IR', countryKey: 'ir', category: 'energy', subType: 'nuclear', description: '주요 원심분리기 농축시설 (지하)' },
|
||||
{ id: 'IR-E23', name: 'Fordow Enrichment Facility', nameKo: '포르도 우라늄농축시설', lat: 34.8800, lng: 51.5800, country: 'IR', countryKey: 'ir', category: 'energy', subType: 'nuclear', description: '지하 농축시설 (FFEP, 쿰 인근 산속)' },
|
||||
{ id: 'IR-E24', name: 'Isfahan Nuclear Technology Center', nameKo: '이스파한 핵기술센터 (UCF)', lat: 32.7200, lng: 51.7200, country: 'IR', countryKey: 'ir', category: 'energy', subType: 'nuclear', description: '우라늄전환시설 + 연구용 원자로' },
|
||||
{ id: 'IR-E25', name: 'Arak Heavy Water Reactor (IR-40)', nameKo: '아라크 중수로', lat: 34.0400, lng: 49.2400, country: 'IR', countryKey: 'ir', category: 'energy', subType: 'nuclear', description: 'IR-40 중수 연구용 원자로 (마르카지주)' },
|
||||
{ id: 'IR-E26', name: 'Darkhovin Nuclear Power Plant', nameKo: '다르코빈 원자력발전소', lat: 31.3700, lng: 48.3200, country: 'IR', countryKey: 'ir', category: 'energy', subType: 'nuclear', capacityMW: 360, description: '이란 자체 건설 원전 (2007 착공, 후제스탄주)' },
|
||||
// ── 풍력 (Wind) ──
|
||||
{ id: 'IR-E27', name: 'Manjil-Rudbar Wind Farm', nameKo: '만질-루드바르 풍력단지', lat: 36.7400, lng: 49.4200, country: 'IR', countryKey: 'ir', category: 'energy', subType: 'wind', capacityMW: 101, description: '길란주, 이란 최대 풍력 (2003 가동)' },
|
||||
{ id: 'IR-E28', name: 'Binaloud Wind Farm', nameKo: '비날루드 풍력단지', lat: 36.2200, lng: 58.7500, country: 'IR', countryKey: 'ir', category: 'energy', subType: 'wind', capacityMW: 28, description: '라자비호라산주 니샤푸르 인근, 43기 x 660kW' },
|
||||
// ── 태양광 (Solar) ──
|
||||
{ id: 'IR-E29', name: 'Zarand Solar Power Plant', nameKo: '자란드 태양광발전소', lat: 30.8100, lng: 56.5600, country: 'IR', countryKey: 'ir', category: 'energy', subType: 'power', capacityMW: 10, description: '케르만주 태양광 시범단지' },
|
||||
// Hazard
|
||||
{ id: 'IR-H01', name: 'South Pars Gas Complex (Assaluyeh)', nameKo: '사우스파르스 가스단지 (아살루예)', lat: 27.4800, lng: 52.6100, country: 'IR', countryKey: 'ir', category: 'hazard', subType: 'petrochem', description: '세계 최대 가스전 육상 처리시설 (20+ 페이즈)' },
|
||||
{ id: 'IR-H02', name: 'Kharg Island Oil Terminal', nameKo: '하르그섬 원유터미널', lat: 29.2300, lng: 50.3100, country: 'IR', countryKey: 'ir', category: 'hazard', subType: 'oil_tank', description: '이란 원유 수출의 90% 처리 (저장 2,800만 배럴)' },
|
||||
|
||||
@ -110,6 +110,11 @@ export default defineConfig(({ mode }): UserConfig => ({
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
},
|
||||
'/ollama': {
|
||||
target: 'http://localhost:11434',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/ollama/, ''),
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
불러오는 중...
Reference in New Issue
Block a user