wing-ops/frontend/src/components/views/AssetsView.tsx
htlee a0f64e4b11 style: 기존 코드 ESLint/TypeScript 에러 수정
- frontend: ESLint 에러 86건 수정 (unused-vars, set-state-in-effect, static-components 등)
- backend: simulation.ts req.params 타입 단언 추가

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 15:47:29 +09:00

2048 lines
131 KiB
TypeScript
Executable File
Raw Blame 히스토리

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useEffect, useRef } from 'react'
import L from 'leaflet'
import 'leaflet/dist/leaflet.css'
type AssetsTab = 'management' | 'upload' | 'theory' | 'insurance'
// ── Mock Data ──
interface AssetOrg {
id: number
type: string
jurisdiction: string
area: string
name: string
address: string
vessel: number
skimmer: number
pump: number
vehicle: number
sprayer: number
totalAssets: number
phone: string
lat: number
lng: number
pinSize: 'hq' | 'lg' | 'md'
equipment: { category: string; icon: string; count: number }[]
contacts: { role: string; name: string; phone: string }[]
}
const organizations: AssetOrg[] = [
// ── 중부지방해양경찰청 ──
{
id: 1, type: '해경관할', jurisdiction: '중부지방해양경찰청', area: '인천', name: '인천해양경찰서',
address: '인천광역시 중구 북성동1가 80-8', vessel: 19, skimmer: 30, pump: 18, vehicle: 2, sprayer: 15, totalAssets: 234, phone: '010-4779-4191',
lat: 37.4563, lng: 126.5922, pinSize: 'hq',
equipment: [
{ category: '방제선', icon: '🚢', count: 19 }, { category: '유회수기', icon: '⚙', count: 30 }, { category: '비치크리너', icon: '🏖', count: 2 },
{ category: '이송펌프', icon: '🔧', count: 18 }, { category: '방제차량', icon: '🚛', count: 2 }, { category: '해안운반차', icon: '🚜', count: 1 },
{ category: '고압세척기', icon: '💧', count: 26 }, { category: '저압세척기', icon: '🚿', count: 3 }, { category: '동력분무기', icon: '💨', count: 14 },
{ category: '유량계측기', icon: '📏', count: 1 }, { category: '방제창고', icon: '🏭', count: 19 }, { category: '발전기', icon: '⚡', count: 9 },
{ category: '현장지휘소', icon: '🏕', count: 2 }, { category: '지원장비', icon: '🔩', count: 9 }, { category: '장비부품', icon: '🔗', count: 46 },
{ category: '경비함정방제', icon: '⚓', count: 18 }, { category: '살포장치', icon: '🌊', count: 15 },
],
contacts: [{ role: '방제과장', name: '김○○', phone: '032-835-0001' }, { role: '방제담당', name: '이○○', phone: '032-835-0002' }],
},
{
id: 2, type: '해경경찰서', jurisdiction: '중부지방해양경찰청', area: '평택', name: '평택해양경찰서',
address: '평택시 만호리 706번지', vessel: 14, skimmer: 27, pump: 33, vehicle: 3, sprayer: 22, totalAssets: 193, phone: '010-9812-8102',
lat: 36.9694, lng: 126.8300, pinSize: 'lg',
equipment: [
{ category: '방제선', icon: '🚢', count: 14 }, { category: '유회수기', icon: '⚙', count: 27 }, { category: '비치크리너', icon: '🏖', count: 1 },
{ category: '이송펌프', icon: '🔧', count: 33 }, { category: '방제차량', icon: '🚛', count: 3 }, { category: '해안운반차', icon: '🚜', count: 1 },
{ category: '고압세척기', icon: '💧', count: 12 }, { category: '저압세척기', icon: '🚿', count: 5 }, { category: '동력분무기', icon: '💨', count: 2 },
{ category: '유량계측기', icon: '📏', count: 1 }, { category: '방제창고', icon: '🏭', count: 35 }, { category: '발전기', icon: '⚡', count: 9 },
{ category: '지원장비', icon: '🔩', count: 10 }, { category: '장비부품', icon: '🔗', count: 4 },
{ category: '경비함정방제', icon: '⚓', count: 14 }, { category: '살포장치', icon: '🌊', count: 22 },
],
contacts: [{ role: '방제담당', name: '박○○', phone: '031-682-0001' }],
},
{
id: 3, type: '해경경찰서', jurisdiction: '중부지방해양경찰청', area: '태안', name: '태안해양경찰서',
address: '충남 태안군 근흥면 신진부두길2', vessel: 10, skimmer: 27, pump: 21, vehicle: 8, sprayer: 15, totalAssets: 185, phone: '010-2965-4423',
lat: 36.7456, lng: 126.2978, pinSize: 'lg',
equipment: [
{ category: '방제선', icon: '🚢', count: 10 }, { category: '유회수기', icon: '⚙', count: 27 }, { category: '비치크리너', icon: '🏖', count: 4 },
{ category: '이송펌프', icon: '🔧', count: 21 }, { category: '방제차량', icon: '🚛', count: 8 }, { category: '해안운반차', icon: '🚜', count: 8 },
{ category: '고압세척기', icon: '💧', count: 14 }, { category: '저압세척기', icon: '🚿', count: 8 }, { category: '동력분무기', icon: '💨', count: 6 },
{ category: '유량계측기', icon: '📏', count: 1 }, { category: '방제창고', icon: '🏭', count: 28 }, { category: '발전기', icon: '⚡', count: 11 },
{ category: '지원장비', icon: '🔩', count: 16 }, { category: '경비함정방제', icon: '⚓', count: 8 }, { category: '살포장치', icon: '🌊', count: 15 },
],
contacts: [{ role: '방제담당', name: '최○○', phone: '041-674-0001' }],
},
{
id: 4, type: '파출소', jurisdiction: '중부지방해양경찰청', area: '보령', name: '보령해양경찰서',
address: '보령시 해안로 740', vessel: 3, skimmer: 8, pump: 5, vehicle: 3, sprayer: 11, totalAssets: 80, phone: '010-2940-6343',
lat: 36.3335, lng: 126.5874, pinSize: 'md',
equipment: [
{ category: '방제선', icon: '🚢', count: 3 }, { category: '유회수기', icon: '⚙', count: 8 }, { category: '이송펌프', icon: '🔧', count: 5 },
{ category: '방제차량', icon: '🚛', count: 3 }, { category: '해안운반차', icon: '🚜', count: 1 }, { category: '고압세척기', icon: '💧', count: 5 },
{ category: '저압세척기', icon: '🚿', count: 2 }, { category: '동력분무기', icon: '💨', count: 1 }, { category: '유량계측기', icon: '📏', count: 1 },
{ category: '방제창고', icon: '🏭', count: 22 }, { category: '발전기', icon: '⚡', count: 2 }, { category: '지원장비', icon: '🔩', count: 6 },
{ category: '장비부품', icon: '🔗', count: 4 }, { category: '경비함정방제', icon: '⚓', count: 6 }, { category: '살포장치', icon: '🌊', count: 11 },
],
contacts: [{ role: '방제담당', name: '정○○', phone: '041-931-0001' }],
},
// ── 서해지방해양경찰청 ──
{
id: 5, type: '해경관할', jurisdiction: '서해지방해양경찰청', area: '여수', name: '여수해양경찰서',
address: '광양시 항만9로 89', vessel: 55, skimmer: 92, pump: 63, vehicle: 12, sprayer: 47, totalAssets: 464, phone: '010-2785-2493',
lat: 34.7407, lng: 127.7385, pinSize: 'hq',
equipment: [
{ category: '방제선', icon: '🚢', count: 55 }, { category: '유회수기', icon: '⚙', count: 92 }, { category: '비치크리너', icon: '🏖', count: 5 },
{ category: '이송펌프', icon: '🔧', count: 63 }, { category: '방제차량', icon: '🚛', count: 12 }, { category: '해안운반차', icon: '🚜', count: 4 },
{ category: '고압세척기', icon: '💧', count: 48 }, { category: '저압세척기', icon: '🚿', count: 7 }, { category: '동력분무기', icon: '💨', count: 25 },
{ category: '유량계측기', icon: '📏', count: 1 }, { category: '방제창고', icon: '🏭', count: 37 }, { category: '발전기', icon: '⚡', count: 16 },
{ category: '현장지휘소', icon: '🏕', count: 2 }, { category: '지원장비', icon: '🔩', count: 14 }, { category: '장비부품', icon: '🔗', count: 14 },
{ category: '경비함정방제', icon: '⚓', count: 22 }, { category: '살포장치', icon: '🌊', count: 47 },
],
contacts: [{ role: '방제과장', name: '윤○○', phone: '061-660-0001' }, { role: '방제담당', name: '장○○', phone: '061-660-0002' }],
},
{
id: 6, type: '해경경찰서', jurisdiction: '서해지방해양경찰청', area: '목포', name: '목포해양경찰서',
address: '목포시 고하대로 597번길 99-64', vessel: 10, skimmer: 19, pump: 18, vehicle: 3, sprayer: 16, totalAssets: 169, phone: '010-9812-8439',
lat: 34.7936, lng: 126.3839, pinSize: 'lg',
equipment: [
{ category: '방제선', icon: '🚢', count: 10 }, { category: '유회수기', icon: '⚙', count: 19 }, { category: '이송펌프', icon: '🔧', count: 18 },
{ category: '방제차량', icon: '🚛', count: 3 }, { category: '해안운반차', icon: '🚜', count: 1 }, { category: '고압세척기', icon: '💧', count: 7 },
{ category: '저압세척기', icon: '🚿', count: 4 }, { category: '동력분무기', icon: '💨', count: 2 }, { category: '유량계측기', icon: '📏', count: 1 },
{ category: '방제창고', icon: '🏭', count: 21 }, { category: '발전기', icon: '⚡', count: 4 }, { category: '지원장비', icon: '🔩', count: 31 },
{ category: '장비부품', icon: '🔗', count: 17 }, { category: '경비함정방제', icon: '⚓', count: 15 }, { category: '살포장치', icon: '🌊', count: 16 },
],
contacts: [{ role: '방제담당', name: '조○○', phone: '061-244-0001' }],
},
{
id: 7, type: '해경경찰서', jurisdiction: '서해지방해양경찰청', area: '군산', name: '군산해양경찰서',
address: '전북 군산시 오식도동 506', vessel: 6, skimmer: 22, pump: 12, vehicle: 3, sprayer: 17, totalAssets: 155, phone: '010-2618-3406',
lat: 35.9900, lng: 126.7133, pinSize: 'lg',
equipment: [
{ category: '방제선', icon: '🚢', count: 6 }, { category: '유회수기', icon: '⚙', count: 22 }, { category: '비치크리너', icon: '🏖', count: 2 },
{ category: '이송펌프', icon: '🔧', count: 12 }, { category: '방제차량', icon: '🚛', count: 3 }, { category: '해안운반차', icon: '🚜', count: 1 },
{ category: '고압세척기', icon: '💧', count: 5 }, { category: '저압세척기', icon: '🚿', count: 4 }, { category: '동력분무기', icon: '💨', count: 2 },
{ category: '유량계측기', icon: '📏', count: 1 }, { category: '방제창고', icon: '🏭', count: 6 }, { category: '발전기', icon: '⚡', count: 5 },
{ category: '현장지휘소', icon: '🏕', count: 3 }, { category: '지원장비', icon: '🔩', count: 11 }, { category: '장비부품', icon: '🔗', count: 50 },
{ category: '경비함정방제', icon: '⚓', count: 5 }, { category: '살포장치', icon: '🌊', count: 17 },
],
contacts: [{ role: '방제담당', name: '한○○', phone: '063-462-0001' }],
},
{
id: 8, type: '해경경찰서', jurisdiction: '서해지방해양경찰청', area: '완도', name: '완도해양경찰서',
address: '완도군 완도읍 장보고대로 383', vessel: 3, skimmer: 9, pump: 7, vehicle: 3, sprayer: 11, totalAssets: 75, phone: '061-550-2183',
lat: 34.3110, lng: 126.7550, pinSize: 'lg',
equipment: [
{ category: '방제선', icon: '🚢', count: 3 }, { category: '유회수기', icon: '⚙', count: 9 }, { category: '이송펌프', icon: '🔧', count: 7 },
{ category: '방제차량', icon: '🚛', count: 3 }, { category: '해안운반차', icon: '🚜', count: 1 }, { category: '고압세척기', icon: '💧', count: 3 },
{ category: '저압세척기', icon: '🚿', count: 2 }, { category: '동력분무기', icon: '💨', count: 1 }, { category: '유량계측기', icon: '📏', count: 1 },
{ category: '방제창고', icon: '🏭', count: 24 }, { category: '발전기', icon: '⚡', count: 2 },
{ category: '경비함정방제', icon: '⚓', count: 8 }, { category: '살포장치', icon: '🌊', count: 11 },
],
contacts: [{ role: '방제담당', name: '이○○', phone: '061-550-0001' }],
},
{
id: 9, type: '파출소', jurisdiction: '서해지방해양경찰청', area: '부안', name: '부안해양경찰서',
address: '전북 군산시 오식도동 506', vessel: 2, skimmer: 8, pump: 7, vehicle: 2, sprayer: 7, totalAssets: 66, phone: '063-928-xxxx',
lat: 35.7316, lng: 126.7328, pinSize: 'md',
equipment: [
{ category: '방제선', icon: '🚢', count: 2 }, { category: '유회수기', icon: '⚙', count: 8 }, { category: '이송펌프', icon: '🔧', count: 7 },
{ category: '방제차량', icon: '🚛', count: 2 }, { category: '해안운반차', icon: '🚜', count: 1 }, { category: '고압세척기', icon: '💧', count: 2 },
{ category: '저압세척기', icon: '🚿', count: 2 }, { category: '동력분무기', icon: '💨', count: 1 }, { category: '유량계측기', icon: '📏', count: 1 },
{ category: '방제창고', icon: '🏭', count: 15 }, { category: '발전기', icon: '⚡', count: 3 }, { category: '현장지휘소', icon: '🏕', count: 2 },
{ category: '지원장비', icon: '🔩', count: 6 }, { category: '경비함정방제', icon: '⚓', count: 7 }, { category: '살포장치', icon: '🌊', count: 7 },
],
contacts: [{ role: '방제담당', name: '김○○', phone: '063-928-0001' }],
},
// ── 남해지방해양경찰청 ──
{
id: 10, type: '해경관할', jurisdiction: '남해지방해양경찰청', area: '부산', name: '부산해양경찰서',
address: '부산시 영도구 해양로 293', vessel: 108, skimmer: 22, pump: 25, vehicle: 10, sprayer: 24, totalAssets: 313, phone: '010-2609-1456',
lat: 35.0746, lng: 129.0686, pinSize: 'hq',
equipment: [
{ category: '방제선', icon: '🚢', count: 108 }, { category: '유회수기', icon: '⚙', count: 22 }, { category: '이송펌프', icon: '🔧', count: 25 },
{ category: '방제차량', icon: '🚛', count: 10 }, { category: '해안운반차', icon: '🚜', count: 1 }, { category: '고압세척기', icon: '💧', count: 38 },
{ category: '저압세척기', icon: '🚿', count: 8 }, { category: '동력분무기', icon: '💨', count: 6 }, { category: '유량계측기', icon: '📏', count: 1 },
{ category: '방제창고', icon: '🏭', count: 21 }, { category: '발전기', icon: '⚡', count: 11 }, { category: '현장지휘소', icon: '🏕', count: 2 },
{ category: '지원장비', icon: '🔩', count: 20 }, { category: '경비함정방제', icon: '⚓', count: 16 }, { category: '살포장치', icon: '🌊', count: 24 },
],
contacts: [{ role: '방제과장', name: '임○○', phone: '051-400-0001' }],
},
{
id: 11, type: '해경관할', jurisdiction: '남해지방해양경찰청', area: '울산', name: '울산해양경찰서',
address: '울산광역시 남구 장생포 고래로 166', vessel: 46, skimmer: 69, pump: 26, vehicle: 11, sprayer: 20, totalAssets: 311, phone: '010-9812-8210',
lat: 35.5008, lng: 129.3824, pinSize: 'hq',
equipment: [
{ category: '방제선', icon: '🚢', count: 46 }, { category: '유회수기', icon: '⚙', count: 69 }, { category: '비치크리너', icon: '🏖', count: 4 },
{ category: '이송펌프', icon: '🔧', count: 26 }, { category: '방제차량', icon: '🚛', count: 11 }, { category: '해안운반차', icon: '🚜', count: 5 },
{ category: '고압세척기', icon: '💧', count: 23 }, { category: '저압세척기', icon: '🚿', count: 6 }, { category: '동력분무기', icon: '💨', count: 6 },
{ category: '유량계측기', icon: '📏', count: 1 }, { category: '방제창고', icon: '🏭', count: 32 }, { category: '발전기', icon: '⚡', count: 7 },
{ category: '현장지휘소', icon: '🏕', count: 1 }, { category: '지원장비', icon: '🔩', count: 40 },
{ category: '경비함정방제', icon: '⚓', count: 14 }, { category: '살포장치', icon: '🌊', count: 20 },
],
contacts: [{ role: '방제과장', name: '강○○', phone: '052-228-0001' }],
},
{
id: 12, type: '해경경찰서', jurisdiction: '남해지방해양경찰청', area: '창원', name: '창원해양경찰서',
address: '창원시 마산합포구 신포동 1가', vessel: 12, skimmer: 25, pump: 14, vehicle: 10, sprayer: 10, totalAssets: 139, phone: '010-4634-7364',
lat: 35.1796, lng: 128.5681, pinSize: 'lg',
equipment: [
{ category: '방제선', icon: '🚢', count: 12 }, { category: '유회수기', icon: '⚙', count: 25 }, { category: '비치크리너', icon: '🏖', count: 2 },
{ category: '이송펌프', icon: '🔧', count: 14 }, { category: '방제차량', icon: '🚛', count: 10 }, { category: '해안운반차', icon: '🚜', count: 1 },
{ category: '고압세척기', icon: '💧', count: 7 }, { category: '저압세척기', icon: '🚿', count: 2 }, { category: '동력분무기', icon: '💨', count: 1 },
{ category: '유량계측기', icon: '📏', count: 1 }, { category: '방제창고', icon: '🏭', count: 21 }, { category: '발전기', icon: '⚡', count: 4 },
{ category: '현장지휘소', icon: '🏕', count: 2 }, { category: '지원장비', icon: '🔩', count: 20 },
{ category: '경비함정방제', icon: '⚓', count: 7 }, { category: '살포장치', icon: '🌊', count: 10 },
],
contacts: [{ role: '방제담당', name: '송○○', phone: '055-220-0001' }],
},
{
id: 13, type: '해경경찰서', jurisdiction: '남해지방해양경찰청', area: '통영', name: '통영해양경찰서',
address: '통영시 광도면 죽림리 1564-4', vessel: 6, skimmer: 15, pump: 9, vehicle: 5, sprayer: 13, totalAssets: 104, phone: '010-9812-8495',
lat: 34.8544, lng: 128.4331, pinSize: 'lg',
equipment: [
{ category: '방제선', icon: '🚢', count: 6 }, { category: '유회수기', icon: '⚙', count: 15 }, { category: '비치크리너', icon: '🏖', count: 2 },
{ category: '이송펌프', icon: '🔧', count: 9 }, { category: '방제차량', icon: '🚛', count: 5 }, { category: '해안운반차', icon: '🚜', count: 1 },
{ category: '고압세척기', icon: '💧', count: 3 }, { category: '저압세척기', icon: '🚿', count: 2 }, { category: '동력분무기', icon: '💨', count: 1 },
{ category: '유량계측기', icon: '📏', count: 1 }, { category: '방제창고', icon: '🏭', count: 18 }, { category: '발전기', icon: '⚡', count: 4 },
{ category: '현장지휘소', icon: '🏕', count: 1 }, { category: '지원장비', icon: '🔩', count: 11 },
{ category: '경비함정방제', icon: '⚓', count: 12 }, { category: '살포장치', icon: '🌊', count: 13 },
],
contacts: [{ role: '방제담당', name: '서○○', phone: '055-640-0001' }],
},
{
id: 14, type: '해경경찰서', jurisdiction: '남해지방해양경찰청', area: '사천', name: '사천해양경찰서',
address: '사천시 신항만길 1길 17', vessel: 2, skimmer: 9, pump: 6, vehicle: 2, sprayer: 7, totalAssets: 80, phone: '010-9812-8352',
lat: 34.9310, lng: 128.0660, pinSize: 'lg',
equipment: [
{ category: '방제선', icon: '🚢', count: 2 }, { category: '유회수기', icon: '⚙', count: 9 }, { category: '이송펌프', icon: '🔧', count: 6 },
{ category: '방제차량', icon: '🚛', count: 2 }, { category: '해안운반차', icon: '🚜', count: 1 }, { category: '고압세척기', icon: '💧', count: 2 },
{ category: '저압세척기', icon: '🚿', count: 2 }, { category: '동력분무기', icon: '💨', count: 4 }, { category: '유량계측기', icon: '📏', count: 1 },
{ category: '방제창고', icon: '🏭', count: 31 }, { category: '발전기', icon: '⚡', count: 2 }, { category: '현장지휘소', icon: '🏕', count: 2 },
{ category: '지원장비', icon: '🔩', count: 1 }, { category: '경비함정방제', icon: '⚓', count: 8 }, { category: '살포장치', icon: '🌊', count: 7 },
],
contacts: [{ role: '방제담당', name: '박○○', phone: '055-830-0001' }],
},
// ── 동해지방해양경찰청 ──
{
id: 15, type: '해경경찰서', jurisdiction: '동해지방해양경찰청', area: '동해', name: '동해해양경찰서',
address: '동해시 임항로 130', vessel: 6, skimmer: 23, pump: 11, vehicle: 6, sprayer: 14, totalAssets: 156, phone: '010-9812-8073',
lat: 37.5247, lng: 129.1143, pinSize: 'lg',
equipment: [
{ category: '방제선', icon: '🚢', count: 6 }, { category: '유회수기', icon: '⚙', count: 23 }, { category: '비치크리너', icon: '🏖', count: 1 },
{ category: '이송펌프', icon: '🔧', count: 11 }, { category: '방제차량', icon: '🚛', count: 6 }, { category: '해안운반차', icon: '🚜', count: 3 },
{ category: '고압세척기', icon: '💧', count: 5 }, { category: '저압세척기', icon: '🚿', count: 2 }, { category: '동력분무기', icon: '💨', count: 5 },
{ category: '유량계측기', icon: '📏', count: 1 }, { category: '방제창고', icon: '🏭', count: 38 }, { category: '발전기', icon: '⚡', count: 2 },
{ category: '현장지휘소', icon: '🏕', count: 1 }, { category: '지원장비', icon: '🔩', count: 20 }, { category: '장비부품', icon: '🔗', count: 10 },
{ category: '경비함정방제', icon: '⚓', count: 8 }, { category: '살포장치', icon: '🌊', count: 14 },
],
contacts: [{ role: '방제담당', name: '남○○', phone: '033-530-0001' }],
},
{
id: 16, type: '해경경찰서', jurisdiction: '동해지방해양경찰청', area: '포항', name: '포항해양경찰서',
address: '포항시 남구 희망대로 1341', vessel: 10, skimmer: 13, pump: 21, vehicle: 4, sprayer: 21, totalAssets: 135, phone: '010-3108-2183',
lat: 36.0190, lng: 129.3651, pinSize: 'lg',
equipment: [
{ category: '방제선', icon: '🚢', count: 10 }, { category: '유회수기', icon: '⚙', count: 13 }, { category: '비치크리너', icon: '🏖', count: 1 },
{ category: '이송펌프', icon: '🔧', count: 21 }, { category: '방제차량', icon: '🚛', count: 4 }, { category: '해안운반차', icon: '🚜', count: 1 },
{ category: '고압세척기', icon: '💧', count: 7 }, { category: '저압세척기', icon: '🚿', count: 2 }, { category: '동력분무기', icon: '💨', count: 3 },
{ category: '유량계측기', icon: '📏', count: 1 }, { category: '방제창고', icon: '🏭', count: 15 }, { category: '발전기', icon: '⚡', count: 5 },
{ category: '현장지휘소', icon: '🏕', count: 1 }, { category: '지원장비', icon: '🔩', count: 20 },
{ category: '경비함정방제', icon: '⚓', count: 10 }, { category: '살포장치', icon: '🌊', count: 21 },
],
contacts: [{ role: '방제담당', name: '오○○', phone: '054-244-0001' }],
},
{
id: 17, type: '파출소', jurisdiction: '동해지방해양경찰청', area: '속초', name: '속초해양경찰서',
address: '속초시 설악금강대교로 206', vessel: 2, skimmer: 6, pump: 4, vehicle: 1, sprayer: 17, totalAssets: 85, phone: '033-634-2186',
lat: 38.2070, lng: 128.5918, pinSize: 'md',
equipment: [
{ category: '방제선', icon: '🚢', count: 2 }, { category: '유회수기', icon: '⚙', count: 6 }, { category: '이송펌프', icon: '🔧', count: 4 },
{ category: '방제차량', icon: '🚛', count: 1 }, { category: '해안운반차', icon: '🚜', count: 1 }, { category: '고압세척기', icon: '💧', count: 2 },
{ category: '저압세척기', icon: '🚿', count: 2 }, { category: '동력분무기', icon: '💨', count: 1 }, { category: '유량계측기', icon: '📏', count: 1 },
{ category: '방제창고', icon: '🏭', count: 16 }, { category: '발전기', icon: '⚡', count: 2 }, { category: '지원장비', icon: '🔩', count: 11 },
{ category: '장비부품', icon: '🔗', count: 11 }, { category: '경비함정방제', icon: '⚓', count: 8 }, { category: '살포장치', icon: '🌊', count: 17 },
],
contacts: [{ role: '방제담당', name: '양○○', phone: '033-633-0001' }],
},
{
id: 18, type: '파출소', jurisdiction: '동해지방해양경찰청', area: '울진', name: '울진해양경찰서',
address: '울진군 후포면 후포리 623-148', vessel: 2, skimmer: 6, pump: 4, vehicle: 1, sprayer: 8, totalAssets: 66, phone: '010-9812-8076',
lat: 36.9932, lng: 129.4003, pinSize: 'md',
equipment: [
{ category: '방제선', icon: '🚢', count: 2 }, { category: '유회수기', icon: '⚙', count: 6 }, { category: '이송펌프', icon: '🔧', count: 4 },
{ category: '방제차량', icon: '🚛', count: 1 }, { category: '해안운반차', icon: '🚜', count: 1 }, { category: '고압세척기', icon: '💧', count: 3 },
{ category: '저압세척기', icon: '🚿', count: 2 }, { category: '동력분무기', icon: '💨', count: 1 }, { category: '유량계측기', icon: '📏', count: 1 },
{ category: '방제창고', icon: '🏭', count: 13 }, { category: '발전기', icon: '⚡', count: 4 }, { category: '현장지휘소', icon: '🏕', count: 2 },
{ category: '지원장비', icon: '🔩', count: 4 }, { category: '장비부품', icon: '🔗', count: 4 },
{ category: '경비함정방제', icon: '⚓', count: 10 }, { category: '살포장치', icon: '🌊', count: 8 },
],
contacts: [{ role: '방제담당', name: '배○○', phone: '054-782-0001' }],
},
// ── 제주지방해양경찰청 ──
{
id: 19, type: '해경경찰서', jurisdiction: '제주지방해양경찰청', area: '제주', name: '제주해양경찰서',
address: '제주시 임항로 85', vessel: 4, skimmer: 21, pump: 17, vehicle: 3, sprayer: 16, totalAssets: 113, phone: '064-766-2691',
lat: 33.5154, lng: 126.5268, pinSize: 'lg',
equipment: [
{ category: '방제선', icon: '🚢', count: 4 }, { category: '유회수기', icon: '⚙', count: 21 }, { category: '비치크리너', icon: '🏖', count: 2 },
{ category: '이송펌프', icon: '🔧', count: 17 }, { category: '방제차량', icon: '🚛', count: 3 }, { category: '해안운반차', icon: '🚜', count: 1 },
{ category: '고압세척기', icon: '💧', count: 5 }, { category: '저압세척기', icon: '🚿', count: 3 }, { category: '동력분무기', icon: '💨', count: 4 },
{ category: '유량계측기', icon: '📏', count: 1 }, { category: '방제창고', icon: '🏭', count: 24 }, { category: '발전기', icon: '⚡', count: 6 },
{ category: '현장지휘소', icon: '🏕', count: 2 }, { category: '경비함정방제', icon: '⚓', count: 4 }, { category: '살포장치', icon: '🌊', count: 16 },
],
contacts: [{ role: '방제담당', name: '문○○', phone: '064-750-0001' }],
},
{
id: 20, type: '해경경찰서', jurisdiction: '제주지방해양경찰청', area: '서귀포', name: '서귀포해양경찰서',
address: '서귀포시 안덕면 화순해안로69', vessel: 3, skimmer: 9, pump: 15, vehicle: 3, sprayer: 14, totalAssets: 67, phone: '064-793-2186',
lat: 33.2469, lng: 126.5600, pinSize: 'lg',
equipment: [
{ category: '방제선', icon: '🚢', count: 3 }, { category: '유회수기', icon: '⚙', count: 9 }, { category: '비치크리너', icon: '🏖', count: 1 },
{ category: '이송펌프', icon: '🔧', count: 15 }, { category: '방제차량', icon: '🚛', count: 3 }, { category: '해안운반차', icon: '🚜', count: 1 },
{ category: '고압세척기', icon: '💧', count: 2 }, { category: '동력분무기', icon: '💨', count: 1 }, { category: '유량계측기', icon: '📏', count: 1 },
{ category: '방제창고', icon: '🏭', count: 10 }, { category: '발전기', icon: '⚡', count: 3 },
{ category: '경비함정방제', icon: '⚓', count: 4 }, { category: '살포장치', icon: '🌊', count: 14 },
],
contacts: [{ role: '방제담당', name: '고○○', phone: '064-730-0001' }],
},
// ── 중앙특수구조단 ──
{
id: 21, type: '관련기관', jurisdiction: '해양경찰청(중앙)', area: '중앙', name: '중앙특수구조단',
address: '부산광역시 영도구 해양로 301', vessel: 1, skimmer: 0, pump: 5, vehicle: 2, sprayer: 0, totalAssets: 39, phone: '051-580-2044',
lat: 35.0580, lng: 129.0590, pinSize: 'md',
equipment: [
{ category: '방제선', icon: '🚢', count: 1 }, { category: '이송펌프', icon: '🔧', count: 5 }, { category: '방제차량', icon: '🚛', count: 2 },
{ category: '유량계측기', icon: '📏', count: 1 }, { category: '발전기', icon: '⚡', count: 2 }, { category: '지원장비', icon: '🔩', count: 27 },
{ category: '경비함정방제', icon: '⚓', count: 1 },
],
contacts: [{ role: '구조단장', name: '김○○', phone: '051-580-2044' }],
},
// ── 기름저장시설 ──
{
id: 22, type: '기름저장시설', jurisdiction: '서해지방해양경찰청', area: '여수', name: '오일허브코리아여수㈜ 외 4개',
address: '전남 여수시 신덕동 325', vessel: 0, skimmer: 0, pump: 0, vehicle: 0, sprayer: 0, totalAssets: 1, phone: '061-686-3611',
lat: 34.745, lng: 127.745, pinSize: 'md',
equipment: [{ category: '고압세척기', icon: '💧', count: 1 }],
contacts: [{ role: '담당', name: '오일허브코리아여수㈜', phone: '061-686-3611' }],
},
{
id: 23, type: '기름저장시설', jurisdiction: '남해지방해양경찰청', area: '부산', name: 'SK에너지 외 2개',
address: '부산시 영도구 해양로 1', vessel: 0, skimmer: 0, pump: 0, vehicle: 0, sprayer: 0, totalAssets: 3, phone: '051-643-3331',
lat: 35.175, lng: 129.075, pinSize: 'md',
equipment: [{ category: '고압세척기', icon: '💧', count: 1 }, { category: '동력분무기', icon: '💨', count: 1 }, { category: '방제창고', icon: '🏭', count: 1 }],
contacts: [{ role: '담당', name: 'HD현대오일뱅크㈜', phone: '051-643-3331' }],
},
{
id: 24, type: '기름저장시설', jurisdiction: '남해지방해양경찰청', area: '울산', name: 'SK지오센트릭 외 5개',
address: '울산광역시 남구 신여천로 2', vessel: 0, skimmer: 0, pump: 0, vehicle: 0, sprayer: 0, totalAssets: 9, phone: '052-208-2851',
lat: 35.535, lng: 129.305, pinSize: 'md',
equipment: [{ category: '동력분무기', icon: '💨', count: 4 }, { category: '방제창고', icon: '🏭', count: 5 }],
contacts: [{ role: '담당', name: 'SK엔텀㈜', phone: '052-208-2851' }],
},
{
id: 25, type: '기름저장시설', jurisdiction: '남해지방해양경찰청', area: '통영', name: '한국가스공사 통영기지본부',
address: '통영시 광도면 안정로 770', vessel: 0, skimmer: 0, pump: 0, vehicle: 0, sprayer: 0, totalAssets: 1, phone: '055-640-6014',
lat: 35.05, lng: 128.41, pinSize: 'md',
equipment: [{ category: '방제창고', icon: '🏭', count: 1 }],
contacts: [{ role: '담당', name: '한국가스공사', phone: '055-640-6014' }],
},
{
id: 26, type: '기름저장시설', jurisdiction: '동해지방해양경찰청', area: '동해', name: 'HD현대오일뱅크㈜ 외 4개',
address: '강릉시 옥계면 동해대로 206', vessel: 0, skimmer: 0, pump: 0, vehicle: 0, sprayer: 0, totalAssets: 9, phone: '033-534-2093',
lat: 37.52, lng: 129.11, pinSize: 'md',
equipment: [{ category: '동력분무기', icon: '💨', count: 2 }, { category: '방제창고', icon: '🏭', count: 6 }, { category: '발전기', icon: '⚡', count: 1 }],
contacts: [{ role: '담당', name: 'HD현대오일뱅크㈜', phone: '033-534-2093' }],
},
{
id: 27, type: '기름저장시설', jurisdiction: '동해지방해양경찰청', area: '포항', name: '포스코케미칼 외 1개',
address: '포항시 남구 동해안로 6262', vessel: 0, skimmer: 0, pump: 0, vehicle: 0, sprayer: 1, totalAssets: 2, phone: '054-290-8222',
lat: 37.73, lng: 129.01, pinSize: 'md',
equipment: [{ category: '동력분무기', icon: '💨', count: 1 }, { category: '살포장치', icon: '🌊', count: 1 }],
contacts: [{ role: '담당', name: 'OCI(주)', phone: '054-290-8222' }],
},
{
id: 28, type: '기름저장시설', jurisdiction: '서해지방해양경찰청', area: '목포', name: '흑산도내연발전소 외 2개',
address: '전남 신안군 흑산일주로70', vessel: 0, skimmer: 0, pump: 0, vehicle: 0, sprayer: 0, totalAssets: 4, phone: '061-351-2342',
lat: 35.04, lng: 126.58, pinSize: 'md',
equipment: [{ category: '고압세척기', icon: '💧', count: 3 }, { category: '발전기', icon: '⚡', count: 1 }],
contacts: [{ role: '담당', name: '안마도내연발전소', phone: '061-351-2342' }],
},
{
id: 29, type: '기름저장시설', jurisdiction: '서해지방해양경찰청', area: '여수', name: '오일허브코리아여수㈜ 외 4개',
address: '전남 여수시 신덕동 325', vessel: 0, skimmer: 0, pump: 0, vehicle: 0, sprayer: 0, totalAssets: 4, phone: '061-686-3611',
lat: 34.75, lng: 127.735, pinSize: 'md',
equipment: [{ category: '동력분무기', icon: '💨', count: 1 }, { category: '방제창고', icon: '🏭', count: 3 }],
contacts: [{ role: '담당', name: '오일허브코리아여수㈜', phone: '061-686-3611' }],
},
{
id: 30, type: '기름저장시설', jurisdiction: '중부지방해양경찰청', area: '인천', name: 'GS칼텍스㈜ 외 10개',
address: '인천광역시 중구 월미로 182', vessel: 0, skimmer: 0, pump: 0, vehicle: 0, sprayer: 0, totalAssets: 13, phone: '010-8777-6922',
lat: 37.45, lng: 126.505, pinSize: 'md',
equipment: [{ category: '고압세척기', icon: '💧', count: 7 }, { category: '동력분무기', icon: '💨', count: 6 }],
contacts: [{ role: '담당', name: 'GS칼텍스㈜', phone: '010-8777-6922' }],
},
{
id: 31, type: '기름저장시설', jurisdiction: '중부지방해양경찰청', area: '태안', name: 'HD현대케미칼 외 4개',
address: '충남 서산시 대산읍 평신2로 26', vessel: 0, skimmer: 0, pump: 0, vehicle: 0, sprayer: 0, totalAssets: 4, phone: '041-924-1068',
lat: 36.91, lng: 126.415, pinSize: 'md',
equipment: [{ category: '해안운반차', icon: '🚜', count: 2 }, { category: '동력분무기', icon: '💨', count: 2 }],
contacts: [{ role: '담당', name: 'HD현대케미칼', phone: '041-924-1068' }],
},
{
id: 32, type: '기름저장시설', jurisdiction: '중부지방해양경찰청', area: '평택', name: '현대오일터미널(주) 외 4개',
address: '평택시 포승읍 포승공단순환로 11', vessel: 0, skimmer: 0, pump: 0, vehicle: 0, sprayer: 1, totalAssets: 4, phone: '031-683-5101',
lat: 36.985, lng: 126.835, pinSize: 'md',
equipment: [{ category: '고압세척기', icon: '💧', count: 2 }, { category: '발전기', icon: '⚡', count: 1 }, { category: '살포장치', icon: '🌊', count: 1 }],
contacts: [{ role: '담당', name: '(주)경동탱크터미널', phone: '031-683-5101' }],
},
// ── 기타 ──
{
id: 33, type: '기타', jurisdiction: '남해지방해양경찰청', area: '사천', name: '한국남동발전(주) 외 2개',
address: '고성군 하이면 하이로1', vessel: 0, skimmer: 0, pump: 0, vehicle: 0, sprayer: 0, totalAssets: 8, phone: '070-4486-7474',
lat: 34.965, lng: 128.56, pinSize: 'md',
equipment: [{ category: '동력분무기', icon: '💨', count: 3 }, { category: '방제창고', icon: '🏭', count: 5 }],
contacts: [{ role: '담당', name: '(주)고성그린파워', phone: '070-4486-7474' }],
},
{
id: 34, type: '기타', jurisdiction: '남해지방해양경찰청', area: '울산', name: 'HD현대미포',
address: '울산광역시 동구 방어진순환도로100', vessel: 0, skimmer: 0, pump: 0, vehicle: 0, sprayer: 0, totalAssets: 1, phone: '052-250-3551',
lat: 35.53, lng: 129.315, pinSize: 'md',
equipment: [{ category: '방제창고', icon: '🏭', count: 1 }],
contacts: [{ role: '담당', name: 'HD현대미포', phone: '052-250-3551' }],
},
{
id: 35, type: '기타', jurisdiction: '남해지방해양경찰청', area: '통영', name: '삼성중공업 외 1개',
address: '거제시 장평3로 80', vessel: 0, skimmer: 0, pump: 0, vehicle: 0, sprayer: 0, totalAssets: 2, phone: '055-630-5373',
lat: 35.05, lng: 128.41, pinSize: 'md',
equipment: [{ category: '동력분무기', icon: '💨', count: 2 }],
contacts: [{ role: '담당', name: '삼성중공업', phone: '055-630-5373' }],
},
{
id: 36, type: '기타', jurisdiction: '동해지방해양경찰청', area: '동해', name: '한국남부발전㈜',
address: '삼척시 원덕읍 삼척로 734', vessel: 0, skimmer: 0, pump: 0, vehicle: 0, sprayer: 0, totalAssets: 2, phone: '070-7713-5153',
lat: 37.45, lng: 129.17, pinSize: 'md',
equipment: [{ category: '방제창고', icon: '🏭', count: 2 }],
contacts: [{ role: '담당', name: '한국남부발전㈜', phone: '070-7713-5153' }],
},
{
id: 37, type: '기타', jurisdiction: '동해지방해양경찰청', area: '울진', name: '한울원전',
address: '울진군 북면 울진북로 2040', vessel: 0, skimmer: 0, pump: 0, vehicle: 0, sprayer: 0, totalAssets: 4, phone: '054-785-4833',
lat: 37.065, lng: 129.39, pinSize: 'md',
equipment: [{ category: '방제창고', icon: '🏭', count: 2 }, { category: '지원장비', icon: '🔩', count: 2 }],
contacts: [{ role: '담당', name: '한울원전', phone: '054-785-4833' }],
},
{
id: 38, type: '기타', jurisdiction: '서해지방해양경찰청', area: '여수', name: '㈜HR-PORT 외 5개',
address: '여수시 제철로', vessel: 0, skimmer: 0, pump: 0, vehicle: 0, sprayer: 0, totalAssets: 16, phone: '061-791-0358',
lat: 34.755, lng: 127.73, pinSize: 'md',
equipment: [{ category: '고압세척기', icon: '💧', count: 1 }, { category: '저압세척기', icon: '🚿', count: 1 }, { category: '동력분무기', icon: '💨', count: 7 }, { category: '방제창고', icon: '🏭', count: 3 }, { category: '발전기', icon: '⚡', count: 1 }, { category: '지원장비', icon: '🔩', count: 3 }],
contacts: [{ role: '담당', name: '㈜ 한진', phone: '061-791-0358' }],
},
{
id: 39, type: '기타', jurisdiction: '중부지방해양경찰청', area: '인천', name: '삼광조선공업㈜ 외 1개',
address: '인천 동구 보세로42번길41', vessel: 0, skimmer: 0, pump: 0, vehicle: 0, sprayer: 0, totalAssets: 5, phone: '010-3321-2959',
lat: 37.45, lng: 126.505, pinSize: 'md',
equipment: [{ category: '고압세척기', icon: '💧', count: 5 }],
contacts: [{ role: '담당', name: '삼광조선공업㈜', phone: '010-3321-2959' }],
},
// ── 방제유창청소업체 ──
{
id: 40, type: '업체', jurisdiction: '중부지방해양경찰청', area: '인천', name: '방제유창청소업체(㈜클린포트)',
address: '㈜클린포트', vessel: 0, skimmer: 0, pump: 0, vehicle: 0, sprayer: 0, totalAssets: 3, phone: '032-882-8279',
lat: 37.45, lng: 126.505, pinSize: 'md',
equipment: [{ category: '고압세척기', icon: '💧', count: 2 }, { category: '발전기', icon: '⚡', count: 1 }],
contacts: [{ role: '담당', name: '㈜클린포트', phone: '032-882-8279' }],
},
{
id: 41, type: '업체', jurisdiction: '남해지방해양경찰청', area: '부산', name: '방제유창청소업체(대용환경㈜ 외 38개)',
address: '㈜태평양해양산업', vessel: 0, skimmer: 0, pump: 0, vehicle: 0, sprayer: 0, totalAssets: 51, phone: '051-242-0622',
lat: 35.18, lng: 129.085, pinSize: 'md',
equipment: [{ category: '고압세척기', icon: '💧', count: 31 }, { category: '저압세척기', icon: '🚿', count: 5 }, { category: '동력분무기', icon: '💨', count: 3 }, { category: '방제창고', icon: '🏭', count: 5 }, { category: '발전기', icon: '⚡', count: 6 }, { category: '현장지휘소', icon: '🏕', count: 1 }],
contacts: [{ role: '담당', name: '(주)경원마린서비스', phone: '051-242-0622' }],
},
{
id: 42, type: '업체', jurisdiction: '남해지방해양경찰청', area: '울산', name: '방제유창청소업체((주)한유마린서비스 외 8개)',
address: '대상해운(주)', vessel: 0, skimmer: 0, pump: 0, vehicle: 0, sprayer: 0, totalAssets: 12, phone: '010-5499-7401',
lat: 35.54, lng: 129.295, pinSize: 'md',
equipment: [{ category: '고압세척기', icon: '💧', count: 11 }, { category: '방제창고', icon: '🏭', count: 1 }],
contacts: [{ role: '담당', name: '(주)골든씨', phone: '010-5499-7401' }],
},
{
id: 43, type: '업체', jurisdiction: '동해지방해양경찰청', area: '포항', name: '방제유창청소업체(블루씨 외 1개)',
address: '(주)블루씨', vessel: 0, skimmer: 0, pump: 0, vehicle: 0, sprayer: 1, totalAssets: 3, phone: '054-278-8200',
lat: 36.015, lng: 129.365, pinSize: 'md',
equipment: [{ category: '고압세척기', icon: '💧', count: 2 }, { category: '살포장치', icon: '🌊', count: 1 }],
contacts: [{ role: '담당', name: '(주)블루씨', phone: '054-278-8200' }],
},
{
id: 44, type: '업체', jurisdiction: '서해지방해양경찰청', area: '목포', name: '방제유창청소업체(㈜한국해운 외 1개)',
address: '㈜한국해운 목포지사', vessel: 0, skimmer: 0, pump: 0, vehicle: 0, sprayer: 0, totalAssets: 1, phone: '010-8615-4326',
lat: 35.04, lng: 126.58, pinSize: 'md',
equipment: [{ category: '고압세척기', icon: '💧', count: 1 }],
contacts: [{ role: '담당', name: '㈜아라', phone: '010-8615-4326' }],
},
{
id: 45, type: '업체', jurisdiction: '서해지방해양경찰청', area: '여수', name: '방제유창청소업체(마로해운 외 11개)',
address: '㈜우진실업', vessel: 0, skimmer: 0, pump: 0, vehicle: 0, sprayer: 2, totalAssets: 54, phone: '061-654-9603',
lat: 34.74, lng: 127.75, pinSize: 'md',
equipment: [{ category: '고압세척기', icon: '💧', count: 28 }, { category: '동력분무기', icon: '💨', count: 15 }, { category: '방제창고', icon: '🏭', count: 5 }, { category: '발전기', icon: '⚡', count: 4 }, { category: '살포장치', icon: '🌊', count: 2 }],
contacts: [{ role: '담당', name: '(유)피케이엘', phone: '061-654-9603' }],
},
{
id: 46, type: '업체', jurisdiction: '중부지방해양경찰청', area: '태안', name: '방제유창청소업체(우진해운㈜)',
address: '우진해운㈜', vessel: 0, skimmer: 0, pump: 0, vehicle: 0, sprayer: 0, totalAssets: 6, phone: '010-4384-6817',
lat: 36.905, lng: 126.42, pinSize: 'md',
equipment: [{ category: '고압세척기', icon: '💧', count: 3 }, { category: '저압세척기', icon: '🚿', count: 1 }, { category: '발전기', icon: '⚡', count: 2 }],
contacts: [{ role: '담당', name: '우진해운㈜', phone: '010-4384-6817' }],
},
{
id: 47, type: '업체', jurisdiction: '중부지방해양경찰청', area: '평택', name: '방제유창청소업체((주)씨앤 외 3개)',
address: '㈜씨앤', vessel: 0, skimmer: 0, pump: 0, vehicle: 0, sprayer: 0, totalAssets: 6, phone: '031-683-2389',
lat: 36.99, lng: 126.825, pinSize: 'md',
equipment: [{ category: '고압세척기', icon: '💧', count: 3 }, { category: '저압세척기', icon: '🚿', count: 1 }, { category: '발전기', icon: '⚡', count: 2 }],
contacts: [{ role: '담당', name: '(주)소스코리아', phone: '031-683-2389' }],
},
{
id: 48, type: '업체', jurisdiction: '남해지방해양경찰청', area: '부산', name: '방제유창청소업체(㈜지앤비마린서비스)',
address: '㈜지앤비마린서비스', vessel: 0, skimmer: 0, pump: 0, vehicle: 0, sprayer: 0, totalAssets: 1, phone: '051-242-0622',
lat: 35.185, lng: 129.07, pinSize: 'md',
equipment: [{ category: '방제창고', icon: '🏭', count: 1 }],
contacts: [{ role: '담당', name: '(주)경원마린서비스', phone: '051-242-0622' }],
},
// ── 정유사 ──
{
id: 49, type: '정유사', jurisdiction: '남해지방해양경찰청', area: '울산', name: 'SK엔텀(주) 외 4개',
address: '울산광역시 남구 고사동 110-64', vessel: 0, skimmer: 0, pump: 0, vehicle: 0, sprayer: 0, totalAssets: 5, phone: '052-231-2318',
lat: 35.545, lng: 129.31, pinSize: 'md',
equipment: [{ category: '방제창고', icon: '🏭', count: 5 }],
contacts: [{ role: '담당', name: 'S-OIL㈜', phone: '052-231-2318' }],
},
{
id: 50, type: '정유사', jurisdiction: '서해지방해양경찰청', area: '여수', name: 'GS칼텍스㈜',
address: '여수시 낙포단지길 251', vessel: 0, skimmer: 0, pump: 0, vehicle: 0, sprayer: 20, totalAssets: 27, phone: '061-680-2121',
lat: 34.735, lng: 127.755, pinSize: 'md',
equipment: [{ category: '고압세척기', icon: '💧', count: 3 }, { category: '방제창고', icon: '🏭', count: 4 }, { category: '살포장치', icon: '🌊', count: 20 }],
contacts: [{ role: '담당', name: 'GS칼텍스㈜', phone: '061-680-2121' }],
},
{
id: 51, type: '정유사', jurisdiction: '중부지방해양경찰청', area: '태안', name: 'HD현대오일뱅크㈜',
address: '서산시 대산읍 평신2로 182', vessel: 0, skimmer: 0, pump: 0, vehicle: 0, sprayer: 0, totalAssets: 2, phone: '010-2050-5291',
lat: 36.915, lng: 126.41, pinSize: 'md',
equipment: [{ category: '동력분무기', icon: '💨', count: 2 }],
contacts: [{ role: '담당', name: 'HD현대오일뱅크㈜', phone: '010-2050-5291' }],
},
// ── 지자체 ──
{
id: 52, type: '지자체', jurisdiction: '남해지방해양경찰청', area: '부산', name: '부산광역시 외 8개',
address: '부산광역시 동구 좌천동', vessel: 0, skimmer: 0, pump: 0, vehicle: 0, sprayer: 0, totalAssets: 12, phone: '051-607-4484',
lat: 35.17, lng: 129.08, pinSize: 'md',
equipment: [{ category: '고압세척기', icon: '💧', count: 1 }, { category: '방제창고', icon: '🏭', count: 11 }],
contacts: [{ role: '담당', name: '남구청', phone: '051-607-4484' }],
},
{
id: 53, type: '지자체', jurisdiction: '남해지방해양경찰청', area: '사천', name: '사천시 외 3개',
address: '사천시 신항로 3', vessel: 0, skimmer: 0, pump: 0, vehicle: 0, sprayer: 0, totalAssets: 6, phone: '055-670-2484',
lat: 34.935, lng: 128.075, pinSize: 'md',
equipment: [{ category: '방제창고', icon: '🏭', count: 6 }],
contacts: [{ role: '담당', name: '고성군', phone: '055-670-2484' }],
},
{
id: 54, type: '지자체', jurisdiction: '남해지방해양경찰청', area: '울산', name: '울산북구청 외 2개',
address: '울산광역시 북구 구유동 654-2', vessel: 0, skimmer: 0, pump: 0, vehicle: 0, sprayer: 0, totalAssets: 4, phone: '051-709-4611',
lat: 35.55, lng: 129.32, pinSize: 'md',
equipment: [{ category: '방제창고', icon: '🏭', count: 4 }],
contacts: [{ role: '담당', name: '부산기장군청', phone: '051-709-4611' }],
},
{
id: 55, type: '지자체', jurisdiction: '남해지방해양경찰청', area: '창원', name: '창원 진해구 외 1개',
address: '창원시 진해구 천자로 105', vessel: 0, skimmer: 0, pump: 0, vehicle: 0, sprayer: 0, totalAssets: 2, phone: '051-970-4482',
lat: 35.055, lng: 128.645, pinSize: 'md',
equipment: [{ category: '고압세척기', icon: '💧', count: 1 }, { category: '방제창고', icon: '🏭', count: 1 }],
contacts: [{ role: '담당', name: '부산 강서구', phone: '051-970-4482' }],
},
{
id: 56, type: '지자체', jurisdiction: '동해지방해양경찰청', area: '동해', name: '삼척시 외 1개',
address: '삼척시 근덕면 덕산리 107-74', vessel: 0, skimmer: 0, pump: 0, vehicle: 0, sprayer: 0, totalAssets: 4, phone: '033-640-5284',
lat: 37.45, lng: 129.16, pinSize: 'md',
equipment: [{ category: '고압세척기', icon: '💧', count: 1 }, { category: '방제창고', icon: '🏭', count: 3 }],
contacts: [{ role: '담당', name: '강릉시', phone: '033-640-5284' }],
},
{
id: 57, type: '지자체', jurisdiction: '동해지방해양경찰청', area: '울진', name: '영덕군',
address: '남정면 장사리 74-1', vessel: 0, skimmer: 0, pump: 0, vehicle: 0, sprayer: 0, totalAssets: 3, phone: '054-730-6562',
lat: 36.35, lng: 129.4, pinSize: 'md',
equipment: [{ category: '방제창고', icon: '🏭', count: 3 }],
contacts: [{ role: '담당', name: '영덕군', phone: '054-730-6562' }],
},
{
id: 58, type: '지자체', jurisdiction: '서해지방해양경찰청', area: '목포', name: '영광군 외 1개',
address: '영광군 염산면 향화로', vessel: 0, skimmer: 0, pump: 0, vehicle: 0, sprayer: 0, totalAssets: 5, phone: '061-270-3419',
lat: 35.04, lng: 126.58, pinSize: 'md',
equipment: [{ category: '방제창고', icon: '🏭', count: 5 }],
contacts: [{ role: '담당', name: '목포시', phone: '061-270-3419' }],
},
{
id: 59, type: '지자체', jurisdiction: '서해지방해양경찰청', area: '여수', name: '광양시 외 1개',
address: '순천시 진상면 성지로 8', vessel: 0, skimmer: 0, pump: 0, vehicle: 0, sprayer: 0, totalAssets: 3, phone: '061-797-2791',
lat: 34.76, lng: 127.725, pinSize: 'md',
equipment: [{ category: '방제창고', icon: '🏭', count: 3 }],
contacts: [{ role: '담당', name: '광양시', phone: '061-797-2791' }],
},
{
id: 60, type: '지자체', jurisdiction: '중부지방해양경찰청', area: '인천', name: '옹진군청 외 4개',
address: '인천광역시 옹진군 덕적면 진리 387', vessel: 0, skimmer: 0, pump: 0, vehicle: 0, sprayer: 0, totalAssets: 10, phone: '010-2740-9388',
lat: 37.45, lng: 126.505, pinSize: 'md',
equipment: [{ category: '고압세척기', icon: '💧', count: 5 }, { category: '동력분무기', icon: '💨', count: 4 }, { category: '발전기', icon: '⚡', count: 1 }],
contacts: [{ role: '담당', name: '김포시청', phone: '010-2740-9388' }],
},
{
id: 61, type: '지자체', jurisdiction: '중부지방해양경찰청', area: '태안', name: '태안군청',
address: '충남 태안군 근흥면 신진도리 75-36', vessel: 0, skimmer: 0, pump: 0, vehicle: 0, sprayer: 0, totalAssets: 1, phone: '041-670-2877',
lat: 36.745, lng: 126.305, pinSize: 'md',
equipment: [{ category: '방제창고', icon: '🏭', count: 1 }],
contacts: [{ role: '담당', name: '태안군청', phone: '041-670-2877' }],
},
{
id: 62, type: '지자체', jurisdiction: '중부지방해양경찰청', area: '평택', name: '안산시청 외 2개',
address: '경기도 안산시 단원구 진두길 97', vessel: 0, skimmer: 0, pump: 0, vehicle: 0, sprayer: 0, totalAssets: 6, phone: '041-350-4292',
lat: 37.32, lng: 126.83, pinSize: 'md',
equipment: [{ category: '동력분무기', icon: '💨', count: 1 }, { category: '방제창고', icon: '🏭', count: 5 }],
contacts: [{ role: '담당', name: '당진시청', phone: '041-350-4292' }],
},
// ── 하역시설 ──
{
id: 63, type: '기타', jurisdiction: '서해지방해양경찰청', area: '여수', name: '㈜HR-PORT 외 5개',
address: '여수시 제철로', vessel: 0, skimmer: 0, pump: 0, vehicle: 0, sprayer: 0, totalAssets: 1, phone: '061-791-0358',
lat: 34.748, lng: 127.74, pinSize: 'md',
equipment: [{ category: '방제창고', icon: '🏭', count: 1 }],
contacts: [{ role: '담당', name: '㈜ 한진', phone: '061-791-0358' }],
},
// ── 해군 ──
{
id: 64, type: '해군', jurisdiction: '동해지방해양경찰청', area: '동해', name: '해군1함대사령부 외 1개',
address: '동해시 대동로 430', vessel: 0, skimmer: 0, pump: 0, vehicle: 0, sprayer: 0, totalAssets: 1, phone: '033-539-7323',
lat: 37.525, lng: 129.115, pinSize: 'md',
equipment: [{ category: '방제창고', icon: '🏭', count: 1 }],
contacts: [{ role: '담당', name: '1함대 사령부', phone: '033-539-7323' }],
},
{
id: 65, type: '해군', jurisdiction: '중부지방해양경찰청', area: '인천', name: '해병대 제9518부대',
address: '인천광역시 옹진군', vessel: 0, skimmer: 0, pump: 0, vehicle: 0, sprayer: 0, totalAssets: 2, phone: '010-4801-3473',
lat: 37.45, lng: 126.505, pinSize: 'md',
equipment: [{ category: '발전기', icon: '⚡', count: 2 }],
contacts: [{ role: '담당', name: '해병대 제9518부대', phone: '010-4801-3473' }],
},
// ── 해양환경공단 ──
{
id: 66, type: '해양환경공단', jurisdiction: '남해지방해양경찰청', area: '부산', name: '부산지사',
address: '창원시 진해구 안골동', vessel: 0, skimmer: 0, pump: 0, vehicle: 0, sprayer: 6, totalAssets: 14, phone: '051-466-3944',
lat: 35.105, lng: 128.715, pinSize: 'md',
equipment: [{ category: '고압세척기', icon: '💧', count: 3 }, { category: '저압세척기', icon: '🚿', count: 1 }, { category: '방제창고', icon: '🏭', count: 1 }, { category: '발전기', icon: '⚡', count: 2 }, { category: '현장지휘소', icon: '🏕', count: 1 }, { category: '살포장치', icon: '🌊', count: 6 }],
contacts: [{ role: '담당', name: '부산지사', phone: '051-466-3944' }],
},
{
id: 67, type: '해양환경공단', jurisdiction: '남해지방해양경찰청', area: '사천', name: '마산지사',
address: '사천시 신항만1길 23', vessel: 0, skimmer: 0, pump: 0, vehicle: 0, sprayer: 0, totalAssets: 9, phone: '010-3598-4202',
lat: 34.925, lng: 128.065, pinSize: 'md',
equipment: [{ category: '방제창고', icon: '🏭', count: 8 }, { category: '발전기', icon: '⚡', count: 1 }],
contacts: [{ role: '담당', name: '마산지사', phone: '010-3598-4202' }],
},
{
id: 68, type: '해양환경공단', jurisdiction: '남해지방해양경찰청', area: '울산', name: '울산지사',
address: '울산광역시 남구 장생포고래로 276번길 27', vessel: 0, skimmer: 0, pump: 0, vehicle: 0, sprayer: 1, totalAssets: 16, phone: '052-238-7718',
lat: 35.538, lng: 129.3, pinSize: 'md',
equipment: [{ category: '고압세척기', icon: '💧', count: 6 }, { category: '저압세척기', icon: '🚿', count: 1 }, { category: '방제창고', icon: '🏭', count: 4 }, { category: '발전기', icon: '⚡', count: 4 }, { category: '살포장치', icon: '🌊', count: 1 }],
contacts: [{ role: '담당', name: '울산지사', phone: '052-238-7718' }],
},
{
id: 69, type: '해양환경공단', jurisdiction: '남해지방해양경찰청', area: '창원', name: '마산지사',
address: '창원시 마산합포구 드림베이대로59', vessel: 0, skimmer: 0, pump: 0, vehicle: 0, sprayer: 1, totalAssets: 7, phone: '010-2265-3928',
lat: 35.055, lng: 128.645, pinSize: 'md',
equipment: [{ category: '고압세척기', icon: '💧', count: 4 }, { category: '발전기', icon: '⚡', count: 2 }, { category: '살포장치', icon: '🌊', count: 1 }],
contacts: [{ role: '담당', name: '마산지사', phone: '010-2265-3928' }],
},
{
id: 70, type: '해양환경공단', jurisdiction: '남해지방해양경찰청', area: '통영', name: '마산지사',
address: '거제시 장승로 112', vessel: 0, skimmer: 0, pump: 0, vehicle: 0, sprayer: 0, totalAssets: 8, phone: '010-2636-5313',
lat: 35.05, lng: 128.41, pinSize: 'md',
equipment: [{ category: '방제창고', icon: '🏭', count: 8 }],
contacts: [{ role: '담당', name: '마산지사', phone: '010-2636-5313' }],
},
{
id: 71, type: '해양환경공단', jurisdiction: '동해지방해양경찰청', area: '동해', name: '동해지사',
address: '동해시 대동로 210', vessel: 0, skimmer: 0, pump: 0, vehicle: 0, sprayer: 2, totalAssets: 17, phone: '010-7499-0257',
lat: 37.515, lng: 129.105, pinSize: 'md',
equipment: [{ category: '해안운반차', icon: '🚜', count: 2 }, { category: '고압세척기', icon: '💧', count: 2 }, { category: '동력분무기', icon: '💨', count: 2 }, { category: '방제창고', icon: '🏭', count: 8 }, { category: '발전기', icon: '⚡', count: 1 }, { category: '살포장치', icon: '🌊', count: 2 }],
contacts: [{ role: '담당', name: '동해지사', phone: '010-7499-0257' }],
},
{
id: 72, type: '해양환경공단', jurisdiction: '동해지방해양경찰청', area: '울진', name: '포항지사',
address: '울진군 죽변면 죽변리 36-88', vessel: 0, skimmer: 0, pump: 0, vehicle: 0, sprayer: 0, totalAssets: 2, phone: '054-273-5595',
lat: 37.06, lng: 129.42, pinSize: 'md',
equipment: [{ category: '방제창고', icon: '🏭', count: 2 }],
contacts: [{ role: '담당', name: '포항지사', phone: '054-273-5595' }],
},
{
id: 73, type: '해양환경공단', jurisdiction: '동해지방해양경찰청', area: '포항', name: '포항지사',
address: '포항시 북구 해안로 44-10', vessel: 0, skimmer: 0, pump: 0, vehicle: 0, sprayer: 2, totalAssets: 8, phone: '054-273-5595',
lat: 36.025, lng: 129.375, pinSize: 'md',
equipment: [{ category: '고압세척기', icon: '💧', count: 2 }, { category: '동력분무기', icon: '💨', count: 1 }, { category: '발전기', icon: '⚡', count: 2 }, { category: '현장지휘소', icon: '🏕', count: 1 }, { category: '살포장치', icon: '🌊', count: 2 }],
contacts: [{ role: '담당', name: '포항지사', phone: '054-273-5595' }],
},
{
id: 74, type: '해양환경공단', jurisdiction: '서해지방해양경찰청', area: '군산', name: '군산지사',
address: '군산시 임해로 452', vessel: 0, skimmer: 0, pump: 0, vehicle: 0, sprayer: 4, totalAssets: 12, phone: '063-443-4813',
lat: 35.975, lng: 126.715, pinSize: 'md',
equipment: [{ category: '고압세척기', icon: '💧', count: 2 }, { category: '저압세척기', icon: '🚿', count: 2 }, { category: '동력분무기', icon: '💨', count: 1 }, { category: '발전기', icon: '⚡', count: 3 }, { category: '살포장치', icon: '🌊', count: 4 }],
contacts: [{ role: '담당', name: '군산지사', phone: '063-443-4813' }],
},
{
id: 75, type: '해양환경공단', jurisdiction: '서해지방해양경찰청', area: '목포', name: '목포지사',
address: '전남 목포시 죽교동 683', vessel: 0, skimmer: 0, pump: 0, vehicle: 0, sprayer: 0, totalAssets: 10, phone: '061-242-9663',
lat: 35.04, lng: 126.58, pinSize: 'md',
equipment: [{ category: '고압세척기', icon: '💧', count: 1 }, { category: '저압세척기', icon: '🚿', count: 2 }, { category: '방제창고', icon: '🏭', count: 6 }, { category: '발전기', icon: '⚡', count: 1 }],
contacts: [{ role: '담당', name: '목포지사', phone: '061-242-9663' }],
},
{
id: 76, type: '해양환경공단', jurisdiction: '서해지방해양경찰청', area: '여수', name: '여수지사',
address: '여수시 덕충동', vessel: 0, skimmer: 0, pump: 0, vehicle: 0, sprayer: 3, totalAssets: 12, phone: '061-654-6431',
lat: 34.742, lng: 127.748, pinSize: 'md',
equipment: [{ category: '고압세척기', icon: '💧', count: 5 }, { category: '저압세척기', icon: '🚿', count: 1 }, { category: '발전기', icon: '⚡', count: 3 }, { category: '살포장치', icon: '🌊', count: 3 }],
contacts: [{ role: '담당', name: '여수지사', phone: '061-654-6431' }],
},
{
id: 77, type: '해양환경공단', jurisdiction: '서해지방해양경찰청', area: '완도', name: '목포지사 완도사업소',
address: '완도군 완도읍 해변공원로 20-1', vessel: 0, skimmer: 0, pump: 0, vehicle: 0, sprayer: 0, totalAssets: 3, phone: '061-242-9663',
lat: 34.315, lng: 126.755, pinSize: 'md',
equipment: [{ category: '고압세척기', icon: '💧', count: 1 }, { category: '방제창고', icon: '🏭', count: 2 }],
contacts: [{ role: '담당', name: '목포지사', phone: '061-242-9663' }],
},
{
id: 78, type: '해양환경공단', jurisdiction: '제주지방해양경찰청', area: '서귀포', name: '제주지사(서귀포)',
address: '서귀포시 칠십리로72번길 14', vessel: 0, skimmer: 0, pump: 0, vehicle: 0, sprayer: 1, totalAssets: 1, phone: '064-753-4356',
lat: 33.245, lng: 126.565, pinSize: 'md',
equipment: [{ category: '살포장치', icon: '🌊', count: 1 }],
contacts: [{ role: '담당', name: '제주지사', phone: '064-753-4356' }],
},
{
id: 79, type: '해양환경공단', jurisdiction: '제주지방해양경찰청', area: '제주', name: '제주지사(제주)',
address: '제주시 임항로97', vessel: 0, skimmer: 0, pump: 0, vehicle: 0, sprayer: 3, totalAssets: 20, phone: '064-753-4356',
lat: 33.517, lng: 126.528, pinSize: 'md',
equipment: [{ category: '고압세척기', icon: '💧', count: 3 }, { category: '저압세척기', icon: '🚿', count: 1 }, { category: '동력분무기', icon: '💨', count: 2 }, { category: '방제창고', icon: '🏭', count: 10 }, { category: '발전기', icon: '⚡', count: 1 }, { category: '살포장치', icon: '🌊', count: 3 }],
contacts: [{ role: '담당', name: '제주지사', phone: '064-753-4356' }],
},
{
id: 80, type: '해양환경공단', jurisdiction: '중부지방해양경찰청', area: '보령', name: '대산지사(보령)',
address: '보령시 해안로 740', vessel: 0, skimmer: 0, pump: 0, vehicle: 0, sprayer: 0, totalAssets: 5, phone: '041-664-9101',
lat: 36.333, lng: 126.612, pinSize: 'md',
equipment: [{ category: '고압세척기', icon: '💧', count: 1 }, { category: '방제창고', icon: '🏭', count: 4 }],
contacts: [{ role: '담당', name: '대산지사', phone: '041-664-9101' }],
},
{
id: 81, type: '해양환경공단', jurisdiction: '중부지방해양경찰청', area: '인천', name: '인천지사',
address: '인천광역시 중구 연안부두로 128번길 35', vessel: 0, skimmer: 0, pump: 0, vehicle: 0, sprayer: 0, totalAssets: 11, phone: '010-7133-2167',
lat: 37.45, lng: 126.505, pinSize: 'md',
equipment: [{ category: '고압세척기', icon: '💧', count: 5 }, { category: '저압세척기', icon: '🚿', count: 1 }, { category: '동력분무기', icon: '💨', count: 3 }, { category: '발전기', icon: '⚡', count: 2 }],
contacts: [{ role: '담당', name: '인천지사', phone: '010-7133-2167' }],
},
{
id: 82, type: '해양환경공단', jurisdiction: '중부지방해양경찰청', area: '태안', name: '대산지사(태안)',
address: '서산시 대산읍 대죽1로 325', vessel: 0, skimmer: 0, pump: 0, vehicle: 0, sprayer: 1, totalAssets: 17, phone: '041-664-9101',
lat: 36.908, lng: 126.413, pinSize: 'md',
equipment: [{ category: '해안운반차', icon: '🚜', count: 1 }, { category: '고압세척기', icon: '💧', count: 5 }, { category: '저압세척기', icon: '🚿', count: 1 }, { category: '동력분무기', icon: '💨', count: 1 }, { category: '방제창고', icon: '🏭', count: 5 }, { category: '발전기', icon: '⚡', count: 3 }, { category: '살포장치', icon: '🌊', count: 1 }],
contacts: [{ role: '담당', name: '대산지사', phone: '041-664-9101' }],
},
{
id: 83, type: '해양환경공단', jurisdiction: '중부지방해양경찰청', area: '평택', name: '평택지사',
address: '당진시 송악읍 고대공단2길', vessel: 0, skimmer: 0, pump: 0, vehicle: 0, sprayer: 1, totalAssets: 13, phone: '031-683-7973',
lat: 36.905, lng: 126.635, pinSize: 'md',
equipment: [{ category: '고압세척기', icon: '💧', count: 3 }, { category: '저압세척기', icon: '🚿', count: 2 }, { category: '방제창고', icon: '🏭', count: 3 }, { category: '발전기', icon: '⚡', count: 4 }, { category: '살포장치', icon: '🌊', count: 1 }],
contacts: [{ role: '담당', name: '평택지사', phone: '031-683-7973' }],
},
// ── 수협 ──
{
id: 84, type: '기타', jurisdiction: '남해지방해양경찰청', area: '통영', name: '삼성중공업 외 1개',
address: '거제시 장평3로 80', vessel: 0, skimmer: 0, pump: 0, vehicle: 0, sprayer: 0, totalAssets: 1, phone: '055-630-5373',
lat: 35.05, lng: 128.41, pinSize: 'md',
equipment: [{ category: '고압세척기', icon: '💧', count: 1 }],
contacts: [{ role: '담당', name: '삼성중공업', phone: '055-630-5373' }],
},
]
interface InsuranceRow {
shipName: string
mmsi: string
imo: string
insType: string
insurer: string
policyNo: string
start: string
expiry: string
limit: string
}
const insuranceDemoData: InsuranceRow[] = [
{ shipName: '유조선 한라호', mmsi: '440123456', imo: '9876001', insType: 'P&I 보험', insurer: '한국P&I클럽', policyNo: 'PI-2025-1234', start: '2025-07-01', expiry: '2026-06-30', limit: '50억' },
{ shipName: '화학물질운반선 제주호', mmsi: '440345678', imo: '9876002', insType: '선주책임보험', insurer: '삼성화재', policyNo: 'SF-2025-9012', start: '2025-09-16', expiry: '2026-09-15', limit: '80억' },
{ shipName: '방제선 OCEAN STAR', mmsi: '440123789', imo: '9876003', insType: 'P&I 보험', insurer: '한국P&I클럽', policyNo: 'PI-2025-3456', start: '2025-11-21', expiry: '2026-11-20', limit: '120억' },
{ shipName: 'LNG운반선 부산호', mmsi: '440567890', imo: '9876004', insType: '해상보험', insurer: 'DB손해보험', policyNo: 'DB-2025-7890', start: '2025-08-02', expiry: '2026-08-01', limit: '200억' },
{ shipName: '유조선 백두호', mmsi: '440789012', imo: '9876005', insType: 'P&I 보험', insurer: 'SK해운보험', policyNo: 'MH-2025-5678', start: '2025-01-01', expiry: '2025-12-31', limit: '30억' },
]
const uploadHistory = [
{ filename: '여수서_장비자재_2601.xlsx', date: '2026-01-25 14:30', uploader: '남해청_방제과', count: 45 },
{ filename: '인천서_오일펜스현황.xlsx', date: '2026-01-22 10:15', uploader: '중부청_방제과', count: 12 },
{ filename: '전체_방제정_현황.xlsx', date: '2026-01-20 09:00', uploader: '본청_방제과', count: 18 },
]
const typeTagCls = (type: string) => {
if (type === '해경관할') return 'bg-[rgba(239,68,68,0.1)] text-status-red'
if (type === '해경경찰서') return 'bg-[rgba(59,130,246,0.1)] text-primary-blue'
if (type === '파출소') return 'bg-[rgba(34,197,94,0.1)] text-status-green'
if (type === '관련기관') return 'bg-[rgba(168,85,247,0.1)] text-primary-purple'
if (type === '해양환경공단') return 'bg-[rgba(6,182,212,0.1)] text-primary-cyan'
if (type === '업체') return 'bg-[rgba(245,158,11,0.1)] text-status-orange'
if (type === '지자체') return 'bg-[rgba(236,72,153,0.1)] text-[#ec4899]'
if (type === '기름저장시설') return 'bg-[rgba(139,92,246,0.1)] text-[#8b5cf6]'
if (type === '정유사') return 'bg-[rgba(20,184,166,0.1)] text-[#14b8a6]'
if (type === '해군') return 'bg-[rgba(100,116,139,0.1)] text-[#64748b]'
if (type === '기타') return 'bg-[rgba(107,114,128,0.1)] text-[#6b7280]'
return 'bg-[rgba(156,163,175,0.1)] text-[#9ca3af]'
}
const typeColor = (type: string) => {
switch (type) {
case '해경관할': return { bg: 'rgba(6,182,212,0.3)', border: '#06b6d4', selected: '#22d3ee' }
case '해경경찰서': return { bg: 'rgba(59,130,246,0.3)', border: '#3b82f6', selected: '#60a5fa' }
case '파출소': return { bg: 'rgba(34,197,94,0.3)', border: '#22c55e', selected: '#4ade80' }
case '관련기관': return { bg: 'rgba(168,85,247,0.3)', border: '#a855f7', selected: '#c084fc' }
case '해양환경공단': return { bg: 'rgba(20,184,166,0.3)', border: '#14b8a6', selected: '#2dd4bf' }
case '업체': return { bg: 'rgba(245,158,11,0.3)', border: '#f59e0b', selected: '#fbbf24' }
case '지자체': return { bg: 'rgba(236,72,153,0.3)', border: '#ec4899', selected: '#f472b6' }
case '기름저장시설': return { bg: 'rgba(139,92,246,0.3)', border: '#8b5cf6', selected: '#a78bfa' }
case '정유사': return { bg: 'rgba(13,148,136,0.3)', border: '#0d9488', selected: '#2dd4bf' }
case '해군': return { bg: 'rgba(100,116,139,0.3)', border: '#64748b', selected: '#94a3b8' }
default: return { bg: 'rgba(107,114,128,0.3)', border: '#6b7280', selected: '#9ca3af' }
}
}
// ── AssetMap (Leaflet) ──
function AssetMap({
organizations: orgs,
selectedOrg,
onSelectOrg,
regionFilter,
onRegionFilterChange,
}: {
organizations: AssetOrg[]
selectedOrg: AssetOrg
onSelectOrg: (o: AssetOrg) => void
regionFilter: string
onRegionFilterChange: (v: string) => void
}) {
const mapContainerRef = useRef<HTMLDivElement>(null)
const mapRef = useRef<L.Map | null>(null)
const markersRef = useRef<L.LayerGroup | null>(null)
// Initialize map once
useEffect(() => {
if (!mapContainerRef.current || mapRef.current) return
const map = L.map(mapContainerRef.current, {
center: [35.9, 127.8],
zoom: 7,
zoomControl: false,
attributionControl: false,
})
// Dark-themed OpenStreetMap tiles
L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
maxZoom: 19,
}).addTo(map)
L.control.zoom({ position: 'topright' }).addTo(map)
L.control.attribution({ position: 'bottomright' }).addAttribution(
'&copy; <a href="https://www.openstreetmap.org/copyright">OSM</a> &copy; <a href="https://carto.com/">CARTO</a>'
).addTo(map)
mapRef.current = map
markersRef.current = L.layerGroup().addTo(map)
return () => {
map.remove()
mapRef.current = null
markersRef.current = null
}
}, [])
// Update markers when orgs or selectedOrg changes
useEffect(() => {
if (!mapRef.current || !markersRef.current) return
markersRef.current.clearLayers()
orgs.forEach(org => {
const isSelected = selectedOrg.id === org.id
const tc = typeColor(org.type)
const radius = org.pinSize === 'hq' ? 14 : org.pinSize === 'lg' ? 10 : 7
const cm = L.circleMarker([org.lat, org.lng], {
radius: isSelected ? radius + 4 : radius,
fillColor: isSelected ? tc.selected : tc.bg,
color: isSelected ? tc.selected : tc.border,
weight: isSelected ? 3 : 2,
fillOpacity: isSelected ? 0.9 : 0.7,
})
cm.bindTooltip(
`<div style="text-align:center;font-family:'Noto Sans KR',sans-serif;">
<div style="font-weight:700;font-size:11px;">${org.name}</div>
<div style="font-size:10px;opacity:0.7;">${org.type} · 자산 ${org.totalAssets}건</div>
</div>`,
{ permanent: org.pinSize === 'hq' || isSelected, direction: 'top', offset: [0, -radius - 2], className: 'asset-map-tooltip' }
)
cm.on('click', () => onSelectOrg(org))
markersRef.current!.addLayer(cm)
})
}, [orgs, selectedOrg, onSelectOrg])
// Pan to selected org
useEffect(() => {
if (!mapRef.current) return
mapRef.current.flyTo([selectedOrg.lat, selectedOrg.lng], 10, { duration: 0.8 })
}, [selectedOrg])
return (
<div className="w-full h-full relative">
<style>{`
.asset-map-tooltip {
background: rgba(15,21,36,0.92) !important;
border: 1px solid rgba(30,42,66,0.8) !important;
color: #e4e8f1 !important;
border-radius: 6px !important;
padding: 4px 8px !important;
box-shadow: 0 4px 12px rgba(0,0,0,0.4) !important;
}
.asset-map-tooltip::before {
border-top-color: rgba(15,21,36,0.92) !important;
}
`}</style>
<div ref={mapContainerRef} className="w-full h-full" />
{/* Region filter overlay */}
<div className="absolute top-3 left-3 z-[1000] flex gap-1">
{[
{ value: 'all', label: '전체' },
{ value: '남해', label: '남해청' },
{ value: '서해', label: '서해청' },
{ value: '중부', label: '중부청' },
{ value: '동해', label: '동해청' },
{ value: '제주', label: '제주청' },
].map(r => (
<button
key={r.value}
onClick={() => onRegionFilterChange(r.value)}
className={`px-2.5 py-1.5 text-[10px] font-bold rounded font-korean transition-colors ${
regionFilter === r.value
? 'bg-primary-cyan/20 text-primary-cyan border border-primary-cyan/40'
: 'bg-bg-0/80 text-text-2 border border-border hover:bg-bg-hover/80'
}`}
>
{r.label}
</button>
))}
</div>
{/* Legend overlay */}
<div className="absolute bottom-3 left-3 z-[1000] bg-bg-0/90 border border-border rounded-sm p-2.5 backdrop-blur-sm">
<div className="text-[9px] text-text-3 font-bold mb-1.5 font-korean"></div>
{[
{ color: '#06b6d4', label: '해경관할' },
{ color: '#3b82f6', label: '해경경찰서' },
{ color: '#22c55e', label: '파출소' },
{ color: '#a855f7', label: '관련기관' },
{ color: '#14b8a6', label: '해양환경공단' },
{ color: '#f59e0b', label: '업체' },
{ color: '#ec4899', label: '지자체' },
{ color: '#8b5cf6', label: '기름저장시설' },
{ color: '#0d9488', label: '정유사' },
{ color: '#64748b', label: '해군' },
{ color: '#6b7280', label: '기타' },
].map((item, i) => (
<div key={i} className="flex items-center gap-1.5 mb-0.5 last:mb-0">
<span className="w-2.5 h-2.5 rounded-full inline-block flex-shrink-0" style={{ background: item.color }} />
<span className="text-[10px] text-text-2 font-korean">{item.label}</span>
</div>
))}
</div>
</div>
)
}
// ── Tab 0: 자산 관리 ──
function AssetManagementTab() {
const [viewMode, setViewMode] = useState<'list' | 'map'>('list')
const [selectedOrg, setSelectedOrg] = useState<AssetOrg>(organizations[0])
const [detailTab, setDetailTab] = useState<'equip' | 'material' | 'contact'>('equip')
const [regionFilter, setRegionFilter] = useState('all')
const [searchTerm, setSearchTerm] = useState('')
const [typeFilterVal, setTypeFilterVal] = useState('all')
const [currentPage, setCurrentPage] = useState(1)
const pageSize = 15
const filtered = organizations.filter(o => {
if (regionFilter !== 'all' && !o.jurisdiction.includes(regionFilter)) return false
if (typeFilterVal !== 'all' && o.type !== typeFilterVal) return false
if (searchTerm && !o.name.includes(searchTerm) && !o.address.includes(searchTerm)) return false
return true
})
const totalPages = Math.max(1, Math.ceil(filtered.length / pageSize))
const safePage = Math.min(currentPage, totalPages)
const paged = filtered.slice((safePage - 1) * pageSize, safePage * pageSize)
// 필터 변경 시 첫 페이지로
// eslint-disable-next-line react-hooks/set-state-in-effect
useEffect(() => { setCurrentPage(1) }, [regionFilter, typeFilterVal, searchTerm])
const regionShort = (j: string) => {
if (j.includes('중부')) return '중부청'
if (j.includes('서해')) return '서해청'
if (j.includes('남해')) return '남해청'
if (j.includes('동해')) return '동해청'
if (j.includes('중앙')) return '중특단'
return '제주청'
}
return (
<div className="flex flex-col h-full">
{/* View Switcher & Filters */}
<div className="flex items-center justify-between mb-3 pb-3 border-b border-border">
<div className="flex gap-1">
<button
onClick={() => setViewMode('list')}
className={`px-3 py-1.5 text-[11px] font-semibold rounded-sm font-korean transition-colors ${
viewMode === 'list'
? 'bg-[rgba(6,182,212,0.15)] text-primary-cyan border border-primary-cyan/30'
: 'bg-bg-3 border border-border text-text-2 hover:bg-bg-hover'
}`}
>
📋
</button>
<button
onClick={() => setViewMode('map')}
className={`px-3 py-1.5 text-[11px] font-semibold rounded-sm font-korean transition-colors ${
viewMode === 'map'
? 'bg-[rgba(6,182,212,0.15)] text-primary-cyan border border-primary-cyan/30'
: 'bg-bg-3 border border-border text-text-2 hover:bg-bg-hover'
}`}
>
🗺
</button>
</div>
<div className="flex gap-1.5 items-center">
<input
type="text"
placeholder="기관명 검색..."
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
className="prd-i w-40 py-1.5 px-2.5"
/>
<select value={regionFilter} onChange={e => setRegionFilter(e.target.value)} className="prd-i w-[100px] py-1.5 px-2">
<option value="all"> </option>
<option value="남해"></option>
<option value="서해"></option>
<option value="중부"></option>
<option value="동해"></option>
<option value="제주"></option>
</select>
<select value={typeFilterVal} onChange={e => setTypeFilterVal(e.target.value)} className="prd-i w-[100px] py-1.5 px-2">
<option value="all"> </option>
<option value="해경관할"></option>
<option value="해경경찰서"></option>
<option value="파출소"></option>
<option value="관련기관"></option>
<option value="해양환경공단"></option>
<option value="업체"></option>
<option value="지자체"></option>
<option value="기름저장시설"></option>
<option value="정유사"></option>
<option value="해군"></option>
<option value="기타"></option>
</select>
</div>
</div>
{viewMode === 'list' ? (
/* ── LIST VIEW ── */
<div className="flex-1 bg-bg-3 border border-border rounded-md overflow-hidden flex flex-col">
<div className="flex-1">
<table className="w-full text-left" style={{ tableLayout: 'fixed' }}>
<colgroup>
<col style={{ width: '3.5%' }} />
<col style={{ width: '7%' }} />
<col style={{ width: '7%' }} />
<col style={{ width: '12%' }} />
<col />
<col style={{ width: '8%' }} />
<col style={{ width: '7%' }} />
<col style={{ width: '7%' }} />
<col style={{ width: '5%' }} />
<col style={{ width: '5%' }} />
<col style={{ width: '5%' }} />
</colgroup>
<thead>
<tr className="border-b border-border bg-bg-0">
{['번호', '유형', '관할청', '기관명', '주소', '방제선', '유회수기', '이송펌프', '방제차량', '살포장치', '총자산'].map((h, i) => (
<th key={i} className={`px-2.5 py-2.5 text-[10px] font-bold text-text-2 font-korean border-b border-border ${[0,5,6,7,8,9,10].includes(i) ? 'text-center' : ''}`}>
{h}
</th>
))}
</tr>
</thead>
<tbody>
{paged.map((org, idx) => (
<tr
key={org.id}
className={`border-b border-border/50 hover:bg-[rgba(255,255,255,0.02)] cursor-pointer transition-colors ${
selectedOrg.id === org.id ? 'bg-[rgba(6,182,212,0.03)]' : ''
}`}
onClick={() => { setSelectedOrg(org); setViewMode('map') }}
>
<td className="px-2.5 py-2 text-center font-mono text-[10px]">{(safePage - 1) * pageSize + idx + 1}</td>
<td className="px-2.5 py-2">
<span className={`text-[9px] px-1.5 py-0.5 rounded font-bold font-korean ${typeTagCls(org.type)}`}>{org.type}</span>
</td>
<td className="px-2.5 py-2 text-[10px] font-semibold font-korean">{regionShort(org.jurisdiction)}</td>
<td className="px-2.5 py-2 text-[10px] font-semibold text-primary-cyan font-korean cursor-pointer truncate">{org.name}</td>
<td className="px-2.5 py-2 text-[10px] text-text-3 font-korean truncate">{org.address}</td>
<td className="px-2.5 py-2 text-center font-mono text-[10px] font-semibold">{org.vessel}</td>
<td className="px-2.5 py-2 text-center font-mono text-[10px]">{org.skimmer}</td>
<td className="px-2.5 py-2 text-center font-mono text-[10px]">{org.pump}</td>
<td className="px-2.5 py-2 text-center font-mono text-[10px]">{org.vehicle}</td>
<td className="px-2.5 py-2 text-center font-mono text-[10px]">{org.sprayer}</td>
<td className="px-2.5 py-2 text-center font-bold text-primary-cyan font-mono text-[10px]">{org.totalAssets}</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Pagination */}
<div className="flex items-center justify-center gap-4 px-4 py-2.5 border-t border-border bg-bg-0">
<span className="text-[10px] text-text-3 font-korean">
<span className="font-semibold text-text-2">{filtered.length}</span> {' '}
<span className="font-semibold text-text-2">{(safePage - 1) * pageSize + 1}-{Math.min(safePage * pageSize, filtered.length)}</span>
</span>
<div className="flex items-center gap-1">
<button
onClick={() => setCurrentPage(1)}
disabled={safePage <= 1}
className="px-1.5 py-1 text-[10px] rounded border border-border bg-bg-3 text-text-2 disabled:opacity-30 hover:bg-bg-hover transition-colors cursor-pointer disabled:cursor-default"
>«</button>
<button
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
disabled={safePage <= 1}
className="px-1.5 py-1 text-[10px] rounded border border-border bg-bg-3 text-text-2 disabled:opacity-30 hover:bg-bg-hover transition-colors cursor-pointer disabled:cursor-default"
></button>
{Array.from({ length: totalPages }, (_, i) => i + 1).map(p => (
<button
key={p}
onClick={() => setCurrentPage(p)}
className={`w-6 h-6 text-[10px] font-bold rounded transition-colors cursor-pointer ${
p === safePage
? 'bg-primary-cyan/20 text-primary-cyan border border-primary-cyan/40'
: 'border border-border bg-bg-3 text-text-3 hover:bg-bg-hover'
}`}
>{p}</button>
))}
<button
onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
disabled={safePage >= totalPages}
className="px-1.5 py-1 text-[10px] rounded border border-border bg-bg-3 text-text-2 disabled:opacity-30 hover:bg-bg-hover transition-colors cursor-pointer disabled:cursor-default"
></button>
<button
onClick={() => setCurrentPage(totalPages)}
disabled={safePage >= totalPages}
className="px-1.5 py-1 text-[10px] rounded border border-border bg-bg-3 text-text-2 disabled:opacity-30 hover:bg-bg-hover transition-colors cursor-pointer disabled:cursor-default"
>»</button>
</div>
</div>
</div>
) : (
/* ── MAP VIEW ── */
<div className="flex-1 flex overflow-hidden rounded-md border border-border">
{/* Map */}
<div className="flex-1 relative overflow-hidden">
<AssetMap
organizations={filtered}
selectedOrg={selectedOrg}
onSelectOrg={setSelectedOrg}
regionFilter={regionFilter}
onRegionFilterChange={setRegionFilter}
/>
</div>
{/* Right Detail Panel */}
<aside className="w-[340px] min-w-[340px] bg-bg-1 border-l border-border flex flex-col">
{/* Header */}
<div className="p-4 border-b border-border">
<div className="text-sm font-bold mb-1 font-korean">{selectedOrg.name}</div>
<div className="text-[11px] text-text-2 font-semibold font-korean mb-1">{selectedOrg.type} · {regionShort(selectedOrg.jurisdiction)} · {selectedOrg.area}</div>
<div className="text-[11px] text-text-3 font-korean">{selectedOrg.address}</div>
</div>
{/* Sub-tabs */}
<div className="flex border-b border-border">
{(['equip', 'material', 'contact'] as const).map(t => (
<button
key={t}
onClick={() => setDetailTab(t)}
className={`flex-1 py-2.5 text-center text-[11px] font-semibold font-korean border-b-2 transition-colors cursor-pointer ${
detailTab === t
? 'text-primary-cyan border-primary-cyan'
: 'text-text-3 border-transparent hover:text-text-2'
}`}
>
{t === 'equip' ? '장비' : t === 'material' ? '자재' : '연락처'}
</button>
))}
</div>
{/* Content */}
<div className="flex-1 overflow-y-auto p-3.5 scrollbar-thin">
{/* Summary */}
<div className="grid grid-cols-3 gap-1.5 mb-3">
{[
{ value: `${selectedOrg.vessel}`, label: '방제선' },
{ value: `${selectedOrg.skimmer}`, label: '유회수기' },
{ value: String(selectedOrg.totalAssets), label: '총 자산' },
].map((s, i) => (
<div key={i} className="bg-bg-3 border border-border rounded-sm p-2.5 text-center">
<div className="text-lg font-bold text-primary-cyan font-mono">{s.value}</div>
<div className="text-[9px] text-text-3 mt-0.5 font-korean">{s.label}</div>
</div>
))}
</div>
{detailTab === 'equip' && (
<div className="flex flex-col gap-1">
{selectedOrg.equipment.length > 0 ? selectedOrg.equipment.map((cat, ci) => {
const unitMap: Record<string, string> = {
'방제선': '척', '유회수기': '대', '비치크리너': '대', '이송펌프': '대', '방제차량': '대',
'해안운반차': '대', '고압세척기': '대', '저압세척기': '대', '동력분무기': '대', '유량계측기': '대',
'방제창고': '개소', '발전기': '대', '현장지휘소': '개', '지원장비': '대', '장비부품': '개',
'경비함정방제': '대', '살포장치': '대',
}
const unit = unitMap[cat.category] || '개'
return (
<div key={ci} className="flex items-center justify-between px-2.5 py-2 bg-bg-3 border border-border rounded-sm hover:bg-bg-hover transition-colors">
<span className="text-[11px] font-semibold flex items-center gap-1.5 font-korean">
{cat.icon} {cat.category}
</span>
<span className="text-[11px] font-bold font-mono"><span className="text-primary-cyan">{cat.count}</span><span className="text-text-3 font-normal ml-0.5">{unit}</span></span>
</div>
)
}) : (
<div className="text-center text-text-3 text-xs py-8 font-korean"> .</div>
)}
</div>
)}
{detailTab === 'material' && (
<div className="flex flex-col gap-1.5">
{[
['방제선', `${selectedOrg.vessel}`],
['유회수기', `${selectedOrg.skimmer}`],
['이송펌프', `${selectedOrg.pump}`],
['방제차량', `${selectedOrg.vehicle}`],
['살포장치', `${selectedOrg.sprayer}`],
['총 자산', `${selectedOrg.totalAssets}`],
].map(([k, v], i) => (
<div key={i} className="flex justify-between px-2.5 py-2 bg-bg-0 rounded text-[11px]">
<span className="text-text-3 font-korean">{k}</span>
<span className="font-mono font-semibold text-text-1">{v}</span>
</div>
))}
</div>
)}
{detailTab === 'contact' && (
<div className="bg-bg-3 border border-border rounded-sm p-3">
{selectedOrg.contacts.length > 0 ? selectedOrg.contacts.map((c, i) => (
<div key={i} className="flex flex-col gap-1 mb-3 last:mb-0">
{[
['기관/업체', c.name],
['연락처', c.phone],
].map(([k, v], j) => (
<div key={j} className="flex justify-between py-1 text-[11px]">
<span className="text-text-3 font-korean">{k}</span>
<span className="font-mono text-text-1">{v}</span>
</div>
))}
{i < selectedOrg.contacts.length - 1 && <div className="border-t border-border my-1" />}
</div>
)) : (
<div className="text-center text-text-3 text-xs py-4 font-korean"> .</div>
)}
</div>
)}
</div>
{/* Bottom Actions */}
<div className="p-3.5 border-t border-border flex gap-2">
<button className="flex-1 py-2.5 rounded-sm text-xs font-semibold font-korean text-white border-none cursor-pointer" style={{ background: 'linear-gradient(135deg, var(--cyan), var(--blue))' }}>
📥
</button>
<button className="flex-1 py-2.5 rounded-sm text-xs font-semibold font-korean bg-bg-3 border border-border text-text-2 cursor-pointer hover:bg-bg-hover transition-colors">
</button>
</div>
</aside>
</div>
)}
</div>
)
}
// ── Tab: 방제자원 이론 ──
interface TheoryItem {
title: string
source: string
desc: string
tags?: { label: string; color: string }[]
highlight?: boolean
}
interface TheorySection {
icon: string
title: string
color: string
bgTint: string
items: TheoryItem[]
dividerAfter?: number
dividerLabel?: string
}
const THEORY_SECTIONS: TheorySection[] = [
{
icon: '🚢', title: '방제선 성능 기준', color: 'var(--blue)', bgTint: 'rgba(59,130,246,.08)',
items: [
{
title: '해양경찰청 방제선 성능기준 고시',
source: '해양경찰청 고시 제2022-11호 | 방제선·방제정 등급별 회수용량·속력·펌프사양 기준 정의',
desc: '1~5등급 방제선 기준 · 회수능력(㎥/h) · 오일펜스 전장 탑재량 · WING 자산 등급 필터링 근거',
},
{
title: 'IMO OPRC 1990 — 방제자원 비축 기준',
source: 'International Convention on Oil Pollution Preparedness, Response and Co-operation | IMO, 1990',
desc: '국가 방제역량 비축 최저 기준 · 항만별 Tier 1/2/3 대응자원 분류 · 국내 방제자원 DB 설계 기초',
},
{
title: '해양오염방제업 등록기준 (해양환경관리법 시행규칙)',
source: '해양수산부령 | 별표 9 — 방제업 종류별 방제선·기자재 보유기준',
desc: '제1종·제2종 방제업 자산 보유기준 · 오일펜스 전장·회수기 용량 법적 최저기준 · WING 자산현황 적법성 검증 기준',
},
],
},
{
icon: '🪢', title: '오일펜스·흡착재 규격', color: 'var(--boom, #f59e0b)', bgTint: 'rgba(245,158,11,.08)',
items: [
{
title: 'ASTM F625 — Standard Guide for Selecting Mechanical Oil Spill Equipment',
source: 'ASTM International | 오일펜스·회수기·흡착재 성능시험·선정 기준 가이드',
desc: '오일펜스 인장강도·부력기준 · 흡착포 흡수율(g/g) 측정법 · WING 자산 성능등급 분류 참조 기준',
},
{
title: '기름오염방제시 오일펜스 사용지침 (ITOPF TIP 03 한국어판)',
source: 'ITOPF | 해양경찰청·해양환경관리공단 번역, 2011',
desc: '커튼형·펜스형·해안용 규격분류 · 유속별 운용한계(0.7~3.0 kt) · 힘 계산식 F=100·A·V² · 앵커 파지력 기준표',
},
],
},
{
icon: '⚙️', title: '방제자원 배치·동원 이론', color: 'var(--purple)', bgTint: 'rgba(168,85,247,.08)',
dividerAfter: 2, dividerLabel: '📐 최적화 수리모델 참고문헌',
items: [
{
title: 'An Emergency Scheduling Model for Oil Containment Boom in Dynamically Changing Marine Oil Spills',
source: 'Xu, Y. et al. | Ningbo Univ. | Systems 2025, 13, 716 · DOI: 10.3390/systems13080716',
desc: 'IMOGWO 다목적 최적화 · 스케줄링 시간+경제·생태손실 동시 최소화 · 동적 오일필름 기반 방제정 라우팅',
highlight: true,
},
{
title: 'Dynamic Resource Allocation to Support Oil Spill Response Planning',
source: 'Garrett, R.A. et al. | Eur. J. Oper. Res. 257:272286, 2017',
desc: '불확실성 하 방제자원 동적 배분 최적화 · 시나리오별 비축량 산정 · WING 자산 우선순위 배치 알고리즘 이론 기반',
},
{
title: '해양오염방제 국가긴급방제계획 (NOSCP)',
source: '해양경찰청 | 국가긴급방제계획, 2023년판',
desc: 'Tier 3급 대형사고 자원 동원체계 · 기관별 역할분담·지휘계통 · WING 방제자산 연계 법적 근거',
},
{
title: 'A Mixed Integer Programming Approach to Improve Oil Spill Response Resource Allocation in the Canadian Arctic',
source: 'Das, T., Goerlandt, F. & Pelot, R. | Multimodal Transportation Vol.3 No.1, 100110, 2023',
desc: '혼합정수계획법으로 응급 방제자원 거점 위치 선택 + 자원 할당 동시 최적화. 비용·응답시간 트레이드오프 파레토 분석.',
highlight: true,
tags: [
{ label: 'MIP 수리모델', color: 'var(--purple)' },
{ label: '자원 위치 선택', color: 'var(--blue)' },
{ label: '북극해 적용', color: 'var(--cyan)' },
],
},
{
title: '유전알고리즘을 이용하여 최적화된 방제자원 배치안의 분포도 분석',
source: '김혜진, 김용혁 | 한국융합학회논문지 Vol.11 No.4, pp.1116, 2020',
desc: 'GA(유전알고리즘)로 방제자원 배치 최적화 및 시뮬레이션 분포도 분석. 국내 해역 실정에 맞는 자원 배치 패턴 도출.',
highlight: true,
tags: [
{ label: 'GA 메타휴리스틱', color: 'var(--purple)' },
{ label: '국내 연구', color: 'var(--green, #22c55e)' },
{ label: '배치 분포도 분석', color: 'var(--boom, #f59e0b)' },
],
},
{
title: 'A Two-Stage Stochastic Optimization Framework for Environmentally Sensitive Oil Spill Response Resource Allocation',
source: 'Rahman, M.A., Kuhel, M.T. & Novoa, C. | arXiv preprint arXiv:2511.22218, 2025',
desc: '확률적 MILP 2단계 프레임워크로 불확실성 포함 최적 자원 배치. 환경민감구역 가중치 반영.',
highlight: true,
tags: [
{ label: '확률적 MILP', color: 'var(--purple)' },
{ label: '2단계 최적화', color: 'var(--blue)' },
{ label: '환경민감구역', color: 'var(--green, #22c55e)' },
],
},
{
title: 'Mixed-Integer Dynamic Optimization for Oil-Spill Response Planning with Integration of Dynamic Oil Weathering Model',
source: 'You, F. & Leyffer, S. | Argonne National Laboratory Technical Note, 2008',
desc: '동적 최적화(MINLP/MILP) 프레임워크로 오일스필 대응 스케줄링 + 오일 풍화·거동 물리모델 통합.',
highlight: true,
tags: [
{ label: 'MINLP 동적 최적화', color: 'var(--purple)' },
{ label: '오일 풍화 모델 통합', color: 'var(--boom, #f59e0b)' },
],
},
],
},
{
icon: '🗄', title: '자산 현행화·데이터 관리', color: 'var(--green, #22c55e)', bgTint: 'rgba(34,197,94,.08)',
items: [
{
title: '해양오염방제자원 현황관리 지침',
source: '해양경찰청 예규 | 방제자원 등록·현행화·이력관리 절차 규정',
desc: '분기별 자산 실사 기준 · 자산분류코드 체계 · WING 업로드 양식(xlsx) 필드 정의 근거',
},
{
title: 'ISO 55000 — Asset Management: Overview, Principles and Terminology',
source: 'International Organization for Standardization | ISO 55000:2014',
desc: '자산 생애주기 관리 원칙 · 자산가치·상태 평가 프레임워크 · WING 자산 노후도·교체주기 산정 이론 기준',
},
],
},
]
const TAG_COLORS: Record<string, { bg: string; bd: string; fg: string }> = {
'var(--purple)': { bg: 'rgba(168,85,247,0.08)', bd: 'rgba(168,85,247,0.2)', fg: '#a855f7' },
'var(--blue)': { bg: 'rgba(59,130,246,0.08)', bd: 'rgba(59,130,246,0.2)', fg: '#3b82f6' },
'var(--cyan)': { bg: 'rgba(6,182,212,0.08)', bd: 'rgba(6,182,212,0.2)', fg: '#06b6d4' },
'var(--green, #22c55e)': { bg: 'rgba(34,197,94,0.08)', bd: 'rgba(34,197,94,0.2)', fg: '#22c55e' },
'var(--boom, #f59e0b)': { bg: 'rgba(245,158,11,0.08)', bd: 'rgba(245,158,11,0.2)', fg: '#f59e0b' },
}
function AssetTheoryTab() {
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '0' }}>
<div style={{ fontSize: '18px', fontWeight: 700, fontFamily: 'var(--fK)', marginBottom: '4px' }}>
📚
</div>
<div style={{ fontSize: '12px', color: 'var(--t3)', fontFamily: 'var(--fK)', marginBottom: '24px' }}>
· ·
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '18px', alignItems: 'start' }}>
{/* Left column */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '14px' }}>
{THEORY_SECTIONS.slice(0, 2).map((sec) => (
<TheoryCard key={sec.title} section={sec} />
))}
</div>
{/* Right column */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '14px' }}>
{THEORY_SECTIONS.slice(2).map((sec) => (
<TheoryCard key={sec.title} section={sec} />
))}
</div>
</div>
</div>
)
}
function TheoryCard({ section }: { section: TheorySection }) {
const badgeBg = section.bgTint.replace(/[\d.]+\)$/, '0.15)')
return (
<div style={{
background: 'var(--bg3)', border: '1px solid var(--bd)',
borderRadius: 'var(--rM, 10px)', overflow: 'hidden',
}}>
{/* Section Header */}
<div style={{
padding: '12px 16px', background: section.bgTint,
borderBottom: '1px solid var(--bd)',
display: 'flex', alignItems: 'center', gap: '8px',
}}>
<span style={{ fontSize: '14px' }}>{section.icon}</span>
<span style={{ fontSize: '12px', fontWeight: 700, color: section.color, fontFamily: 'var(--fK)' }}>
{section.title}
</span>
</div>
{/* Items */}
<div style={{ padding: '14px 16px', display: 'flex', flexDirection: 'column', gap: '8px', fontSize: '9px', fontFamily: 'var(--fK)' }}>
{section.items.map((item, i) => (
<div key={i}>
{/* Divider */}
{section.dividerAfter !== undefined && i === section.dividerAfter + 1 && (
<div style={{ borderTop: '1px dashed var(--bd)', margin: '4px 0 12px', paddingTop: '8px' }}>
<div style={{ fontSize: '8px', fontWeight: 700, color: section.color, marginBottom: '6px', opacity: 0.7 }}>
{section.dividerLabel}
</div>
</div>
)}
<div style={{
display: 'grid', gridTemplateColumns: '24px 1fr', gap: '8px',
padding: '8px 10px', background: 'var(--bg0)', borderRadius: '6px',
borderLeft: item.highlight ? `2px solid ${section.color}` : undefined,
}}>
{/* Number badge */}
<div style={{
width: '20px', height: '20px', borderRadius: '4px',
background: badgeBg,
display: 'flex', alignItems: 'center', justifyContent: 'center',
fontSize: '9px', flexShrink: 0,
fontWeight: item.highlight ? 700 : 400,
color: item.highlight ? section.color : undefined,
}}>
{['①','②','③','④','⑤','⑥','⑦','⑧','⑨','⑩'][i]}
</div>
<div>
<div style={{ color: 'var(--t1)', fontWeight: 700, marginBottom: '2px' }}>
{item.title}
</div>
<div style={{ color: 'var(--t3)', lineHeight: '1.6' }}>
{item.source}
</div>
{/* Tags */}
{item.tags && (
<div style={{ marginTop: '3px', display: 'flex', flexWrap: 'wrap', gap: '3px' }}>
{item.tags.map((tag, ti) => {
const tc = TAG_COLORS[tag.color] || { bg: 'rgba(107,114,128,0.08)', bd: 'rgba(107,114,128,0.2)', fg: '#6b7280' }
return (
<span key={ti} style={{
padding: '1px 5px', borderRadius: '3px', fontSize: '8px',
color: tc.fg, background: tc.bg, border: `1px solid ${tc.bd}`,
}}>
{tag.label}
</span>
)
})}
</div>
)}
<div style={{ marginTop: '2px', color: 'var(--t2)' }}>
{item.desc}
</div>
</div>
</div>
</div>
))}
</div>
</div>
)
}
// ── Tab 1: 자산 현행화 (업로드) ──
function AssetUploadTab() {
const [uploadMode, setUploadMode] = useState<'add' | 'replace'>('add')
const [uploaded, setUploaded] = useState(false)
const handleUpload = () => {
setUploaded(true)
setTimeout(() => setUploaded(false), 3000)
}
return (
<div className="flex gap-8 h-full overflow-auto">
{/* Left - Upload */}
<div className="flex-1 max-w-[580px]">
<div className="text-[13px] font-bold mb-3.5 font-korean">📤 </div>
{/* Drop Zone */}
<div className="border-2 border-dashed border-border-light rounded-md py-10 px-5 text-center mb-5 cursor-pointer hover:border-primary-cyan/40 transition-colors">
<div className="text-4xl mb-2.5 opacity-50">📁</div>
<div className="text-sm font-semibold mb-1.5 font-korean"> </div>
<div className="text-[11px] text-text-3 mb-4 font-korean">(.xlsx), CSV · 10MB</div>
<button className="px-7 py-2.5 text-[13px] font-semibold rounded-sm text-white border-none cursor-pointer font-korean" style={{ background: 'linear-gradient(135deg, var(--blue), #2563eb)' }}>
</button>
</div>
{/* Asset Classification */}
<div className="mb-4">
<label className="block text-xs font-semibold mb-1.5 text-text-2 font-korean"> </label>
<select className="prd-i w-full">
<option></option>
<option></option>
<option></option>
<option></option>
<option>·</option>
<option>MPRS·</option>
</select>
</div>
{/* Jurisdiction */}
<div className="mb-4">
<label className="block text-xs font-semibold mb-1.5 text-text-2 font-korean"> </label>
<select className="prd-i w-full">
<option> - </option>
<option> - </option>
<option> - </option>
<option> - </option>
<option> - </option>
<option> - </option>
<option> - </option>
</select>
</div>
{/* Upload Mode */}
<div className="mb-5">
<label className="block text-xs font-semibold mb-1.5 text-text-2 font-korean"> </label>
<div className="flex gap-4 text-xs text-text-2 font-korean">
<label className="flex items-center gap-1.5 cursor-pointer">
<input type="radio" checked={uploadMode === 'add'} onChange={() => setUploadMode('add')} className="accent-primary-blue" />
( + )
</label>
<label className="flex items-center gap-1.5 cursor-pointer">
<input type="radio" checked={uploadMode === 'replace'} onChange={() => setUploadMode('replace')} className="accent-primary-blue" />
</label>
</div>
</div>
{/* Upload Button */}
<button
onClick={handleUpload}
className={`w-full py-3.5 rounded-sm text-sm font-bold font-korean border-none cursor-pointer transition-all ${
uploaded
? 'bg-[rgba(34,197,94,0.2)] text-status-green border border-status-green'
: 'text-white'
}`}
style={!uploaded ? { background: 'linear-gradient(135deg, var(--blue), #2563eb)' } : undefined}
>
{uploaded ? '✅ 업로드 완료!' : '📤 업로드 실행'}
</button>
</div>
{/* Right - Permission & History */}
<div className="flex-1 max-w-[480px]">
{/* Permission System */}
<div className="text-[13px] font-bold mb-3.5 font-korean">🔐 </div>
<div className="flex flex-col gap-2 mb-7">
{[
{ icon: '👑', role: '본청 관리자', desc: '전체 자산 조회·수정·삭제·업로드', color: 'text-status-red', bg: 'rgba(239,68,68,0.15)' },
{ icon: '🏛', role: '지방청 담당자', desc: '소속 지방청 및 하위 해경서 자산 수정·업로드', color: 'text-status-orange', bg: 'rgba(249,115,22,0.15)' },
{ icon: '⚓', role: '해경서 담당자', desc: '소속 해경서 자산 수정·업로드', color: 'text-primary-blue', bg: 'rgba(59,130,246,0.15)' },
{ icon: '👤', role: '일반 사용자', desc: '조회·다운로드만 가능', color: 'text-text-2', bg: 'rgba(100,116,139,0.15)' },
].map((p, i) => (
<div key={i} className="flex items-center gap-3 p-3.5 px-4 bg-bg-3 border border-border rounded-sm">
<div className="w-9 h-9 rounded-full flex items-center justify-center text-base" style={{ background: p.bg }}>{p.icon}</div>
<div>
<div className={`text-xs font-bold font-korean ${p.color}`}>{p.role}</div>
<div className="text-[10px] text-text-3 font-korean">{p.desc}</div>
</div>
</div>
))}
</div>
{/* Upload History */}
<div className="text-[13px] font-bold mb-3.5 font-korean">📋 </div>
<div className="flex flex-col gap-2">
{uploadHistory.map((h, i) => (
<div key={i} className="flex justify-between items-center p-3.5 px-4 bg-bg-3 border border-border rounded-sm">
<div>
<div className="text-xs font-semibold font-korean">{h.filename}</div>
<div className="text-[10px] text-text-3 mt-0.5 font-korean">{h.date} · {h.uploader} · {h.count}</div>
</div>
<span className="px-2 py-0.5 rounded-full text-[10px] font-semibold bg-[rgba(34,197,94,0.15)] text-status-green"></span>
</div>
))}
</div>
</div>
</div>
)
}
// ── Tab 2: 선박 보험정보 — 한국해운조합 API 연동 ──
function ShipInsuranceTab() {
const [apiConnected, setApiConnected] = useState(false)
const [showConfig, setShowConfig] = useState(false)
const [configEndpoint, setConfigEndpoint] = useState('https://api.haewoon.or.kr/v1/insurance')
const [configApiKey, setConfigApiKey] = useState('')
const [configKeyType, setConfigKeyType] = useState('mmsi')
const [configRespType, setConfigRespType] = useState('json')
const [searchType, setSearchType] = useState('mmsi')
const [searchVal, setSearchVal] = useState('')
const [insTypeFilter, setInsTypeFilter] = useState('전체')
const [viewState, setViewState] = useState<'empty' | 'loading' | 'result'>('empty')
const [resultData, setResultData] = useState<InsuranceRow[]>([])
const [lastSync, setLastSync] = useState('—')
const placeholderMap: Record<string, string> = {
mmsi: 'MMSI 번호 입력 (예: 440123456)',
imo: 'IMO 번호 입력 (예: 9876543)',
shipname: '선박명 입력 (예: 한라호)',
callsign: '호출부호 입력 (예: HLXX1)',
}
const getStatus = (expiry: string) => {
const now = new Date()
const exp = new Date(expiry)
const daysLeft = Math.ceil((exp.getTime() - now.getTime()) / (1000 * 60 * 60 * 24))
if (exp < now) return 'expired' as const
if (daysLeft <= 30) return 'soon' as const
return 'valid' as const
}
const handleSaveConfig = () => {
if (!configApiKey) { alert('API Key를 입력하세요.'); return }
setShowConfig(false)
alert('API 설정이 저장되었습니다.')
}
const handleTestConnect = async () => {
await new Promise(r => setTimeout(r, 1200))
alert('⚠ API Key가 설정되지 않았습니다.\n[API 설정] 버튼에서 한국해운조합 API Key를 먼저 등록하세요.')
}
const loadDemoData = () => {
setResultData(insuranceDemoData)
setViewState('result')
setApiConnected(false)
setLastSync(new Date().toLocaleString('ko-KR'))
}
const handleQuery = async () => {
if (!searchVal.trim()) { alert('조회값을 입력하세요.'); return }
setViewState('loading')
await new Promise(r => setTimeout(r, 900))
loadDemoData()
}
const handleBatchQuery = async () => {
setViewState('loading')
await new Promise(r => setTimeout(r, 1400))
loadDemoData()
}
const handleFullSync = async () => {
setLastSync('동기화 중...')
await new Promise(r => setTimeout(r, 1000))
setLastSync(new Date().toLocaleString('ko-KR'))
alert('전체 동기화는 API 연동 후 활성화됩니다.')
}
// summary computation
const validCount = resultData.filter(r => getStatus(r.expiry) !== 'expired').length
const soonList = resultData.filter(r => getStatus(r.expiry) === 'soon')
const expiredList = resultData.filter(r => getStatus(r.expiry) === 'expired')
return (
<div style={{ display: 'flex', flexDirection: 'column', flex: 1, overflow: 'auto' }}>
{/* ── 헤더 ── */}
<div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', marginBottom: 20 }}>
<div>
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 4 }}>
<div style={{ fontSize: 18, fontWeight: 700, fontFamily: 'var(--fK)' }}>🛡 </div>
<div style={{
display: 'flex', alignItems: 'center', gap: 5, padding: '3px 10px', borderRadius: 10,
fontSize: 10, fontWeight: 700, fontFamily: 'var(--fK)',
background: apiConnected ? 'rgba(34,197,94,.12)' : 'rgba(239,68,68,.12)',
color: apiConnected ? 'var(--green)' : 'var(--red)',
border: `1px solid ${apiConnected ? 'rgba(34,197,94,.25)' : 'rgba(239,68,68,.25)'}`,
}}>
<span style={{ width: 6, height: 6, borderRadius: '50%', background: apiConnected ? 'var(--green)' : 'var(--red)', display: 'inline-block' }} />
{apiConnected ? 'API 연결됨' : 'API 미연결'}
</div>
</div>
<div style={{ fontSize: 12, color: 'var(--t3)', fontFamily: 'var(--fK)' }}>(KSA) Open API · P&I </div>
</div>
<div style={{ display: 'flex', gap: 8 }}>
<button onClick={handleTestConnect} style={{ padding: '8px 16px', background: 'rgba(6,182,212,.12)', color: 'var(--cyan)', border: '1px solid rgba(6,182,212,.3)', borderRadius: 'var(--rS)', fontSize: 12, fontWeight: 600, cursor: 'pointer', fontFamily: 'var(--fK)' }}>🔌 </button>
<button onClick={() => setShowConfig(v => !v)} style={{ padding: '8px 16px', background: 'var(--bg3)', color: 'var(--t2)', border: '1px solid var(--bd)', borderRadius: 'var(--rS)', fontSize: 12, fontWeight: 600, cursor: 'pointer', fontFamily: 'var(--fK)' }}> API </button>
</div>
</div>
{/* ── API 설정 패널 ── */}
{showConfig && (
<div style={{ background: 'var(--bg3)', border: '1px solid var(--bd)', borderRadius: 'var(--rM)', padding: '20px 24px', marginBottom: 20 }}>
<div style={{ fontSize: 13, fontWeight: 700, fontFamily: 'var(--fK)', marginBottom: 14, color: 'var(--cyan)' }}> API </div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12, marginBottom: 16 }}>
<div>
<label style={{ display: 'block', fontSize: 11, fontWeight: 600, color: 'var(--t2)', fontFamily: 'var(--fK)', marginBottom: 5 }}>API Endpoint URL</label>
<input type="text" value={configEndpoint} onChange={e => setConfigEndpoint(e.target.value)} placeholder="https://api.haewoon.or.kr/v1/..."
style={{ width: '100%', padding: '9px 12px', background: 'var(--bg0)', border: '1px solid var(--bd)', borderRadius: 'var(--rS)', color: 'var(--t1)', fontFamily: 'var(--fM)', fontSize: 12, outline: 'none', boxSizing: 'border-box' }} />
</div>
<div>
<label style={{ display: 'block', fontSize: 11, fontWeight: 600, color: 'var(--t2)', fontFamily: 'var(--fK)', marginBottom: 5 }}>API Key</label>
<input type="password" value={configApiKey} onChange={e => setConfigApiKey(e.target.value)} placeholder="발급받은 API Key 입력"
style={{ width: '100%', padding: '9px 12px', background: 'var(--bg0)', border: '1px solid var(--bd)', borderRadius: 'var(--rS)', color: 'var(--t1)', fontFamily: 'var(--fM)', fontSize: 12, outline: 'none', boxSizing: 'border-box' }} />
</div>
<div>
<label style={{ display: 'block', fontSize: 11, fontWeight: 600, color: 'var(--t2)', fontFamily: 'var(--fK)', marginBottom: 5 }}> </label>
<select value={configKeyType} onChange={e => setConfigKeyType(e.target.value)} className="prd-i" style={{ borderColor: 'var(--bd)', width: '100%' }}>
<option value="mmsi">MMSI</option>
<option value="imo">IMO </option>
<option value="shipname"></option>
<option value="callsign"></option>
</select>
</div>
<div>
<label style={{ display: 'block', fontSize: 11, fontWeight: 600, color: 'var(--t2)', fontFamily: 'var(--fK)', marginBottom: 5 }}> </label>
<select value={configRespType} onChange={e => setConfigRespType(e.target.value)} className="prd-i" style={{ borderColor: 'var(--bd)', width: '100%' }}>
<option value="json">JSON</option>
<option value="xml">XML</option>
</select>
</div>
</div>
<div style={{ display: 'flex', gap: 8 }}>
<button onClick={handleSaveConfig} style={{ padding: '9px 20px', background: 'linear-gradient(135deg, var(--cyan), var(--blue))', color: '#fff', border: 'none', borderRadius: 'var(--rS)', fontSize: 12, fontWeight: 700, cursor: 'pointer', fontFamily: 'var(--fK)' }}>💾 </button>
<button onClick={() => setShowConfig(false)} style={{ padding: '9px 16px', background: 'var(--bg0)', color: 'var(--t2)', border: '1px solid var(--bd)', borderRadius: 'var(--rS)', fontSize: 12, cursor: 'pointer', fontFamily: 'var(--fK)' }}></button>
</div>
{/* API 연동 안내 */}
<div style={{ marginTop: 16, padding: '12px 16px', background: 'rgba(6,182,212,.05)', border: '1px solid rgba(6,182,212,.15)', borderRadius: 'var(--rS)', fontSize: 10, color: 'var(--t3)', fontFamily: 'var(--fK)', lineHeight: 1.8 }}>
<span style={{ color: 'var(--cyan)', fontWeight: 700 }}>📋 API </span><br />
IT지원팀에 API <br />
<br />
데이터: P&I , , , , ,
</div>
</div>
)}
{/* ── 검색 영역 ── */}
<div style={{ background: 'var(--bg3)', border: '1px solid var(--bd)', borderRadius: 'var(--rM)', padding: '18px 20px', marginBottom: 16 }}>
<div style={{ fontSize: 12, fontWeight: 700, fontFamily: 'var(--fK)', marginBottom: 12, color: 'var(--t2)' }}>🔍 </div>
<div style={{ display: 'flex', gap: 8, alignItems: 'flex-end', flexWrap: 'wrap' }}>
<div>
<label style={{ display: 'block', fontSize: 10, fontWeight: 600, color: 'var(--t3)', fontFamily: 'var(--fK)', marginBottom: 4 }}> </label>
<select value={searchType} onChange={e => setSearchType(e.target.value)} className="prd-i" style={{ borderColor: 'var(--bd)', minWidth: 120 }}>
<option value="mmsi">MMSI</option>
<option value="imo">IMO </option>
<option value="shipname"></option>
<option value="callsign"></option>
</select>
</div>
<div style={{ flex: 1, minWidth: 220 }}>
<label style={{ display: 'block', fontSize: 10, fontWeight: 600, color: 'var(--t3)', fontFamily: 'var(--fK)', marginBottom: 4 }}></label>
<input type="text" value={searchVal} onChange={e => setSearchVal(e.target.value)} placeholder={placeholderMap[searchType]}
style={{ width: '100%', padding: '9px 14px', background: 'var(--bg0)', border: '1px solid var(--bd)', borderRadius: 'var(--rS)', color: 'var(--t1)', fontFamily: 'var(--fM)', fontSize: 13, outline: 'none', boxSizing: 'border-box' }} />
</div>
<div>
<label style={{ display: 'block', fontSize: 10, fontWeight: 600, color: 'var(--t3)', fontFamily: 'var(--fK)', marginBottom: 4 }}> </label>
<select value={insTypeFilter} onChange={e => setInsTypeFilter(e.target.value)} className="prd-i" style={{ borderColor: 'var(--bd)', minWidth: 140 }}>
<option></option>
<option>P&I </option>
<option></option>
<option>()</option>
<option></option>
</select>
</div>
<button onClick={handleQuery} style={{ padding: '9px 24px', background: 'linear-gradient(135deg, var(--cyan), var(--blue))', color: '#fff', border: 'none', borderRadius: 'var(--rS)', fontSize: 13, fontWeight: 700, cursor: 'pointer', fontFamily: 'var(--fK)', flexShrink: 0 }}>🔍 </button>
<button onClick={handleBatchQuery} style={{ padding: '9px 18px', background: 'rgba(168,85,247,.12)', color: 'var(--purple)', border: '1px solid rgba(168,85,247,.3)', borderRadius: 'var(--rS)', fontSize: 12, fontWeight: 600, cursor: 'pointer', fontFamily: 'var(--fK)', flexShrink: 0 }}>📋 </button>
</div>
</div>
{/* ── 결과 영역 ── */}
{/* 초기 안내 상태 */}
{viewState === 'empty' && (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: '60px 20px', background: 'var(--bg3)', border: '1px solid var(--bd)', borderRadius: 'var(--rM)' }}>
<div style={{ fontSize: 48, marginBottom: 16, opacity: 0.3 }}>🛡</div>
<div style={{ fontSize: 14, fontWeight: 700, color: 'var(--t2)', fontFamily: 'var(--fK)', marginBottom: 8 }}> API </div>
<div style={{ fontSize: 12, color: 'var(--t3)', fontFamily: 'var(--fK)', textAlign: 'center', lineHeight: 1.8 }}>
API API Key를 <br />
MMSI·IMO· .<br />
<span style={{ color: 'var(--cyan)' }}> </span> .
</div>
<div style={{ marginTop: 20, display: 'flex', gap: 10 }}>
<button onClick={() => setShowConfig(true)} style={{ padding: '10px 20px', background: 'rgba(6,182,212,.12)', color: 'var(--cyan)', border: '1px solid rgba(6,182,212,.3)', borderRadius: 'var(--rS)', fontSize: 12, fontWeight: 600, cursor: 'pointer', fontFamily: 'var(--fK)' }}> API </button>
<button onClick={loadDemoData} style={{ padding: '10px 20px', background: 'var(--bg0)', color: 'var(--t2)', border: '1px solid var(--bd)', borderRadius: 'var(--rS)', fontSize: 12, fontWeight: 600, cursor: 'pointer', fontFamily: 'var(--fK)' }}>📊 </button>
</div>
</div>
)}
{/* 로딩 */}
{viewState === 'loading' && (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: 60, background: 'var(--bg3)', border: '1px solid var(--bd)', borderRadius: 'var(--rM)' }}>
<div style={{ width: 36, height: 36, border: '3px solid var(--bd)', borderTopColor: 'var(--cyan)', borderRadius: '50%', animation: 'spin 0.8s linear infinite', marginBottom: 14 }} />
<div style={{ fontSize: 13, color: 'var(--t2)', fontFamily: 'var(--fK)' }}> API ...</div>
</div>
)}
{/* 결과 테이블 */}
{viewState === 'result' && (
<>
{/* 요약 카드 */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 10, marginBottom: 14 }}>
{[
{ label: '전체', val: resultData.length, color: 'var(--cyan)', bg: 'rgba(6,182,212,.08)' },
{ label: '유효', val: validCount, color: 'var(--green)', bg: 'rgba(34,197,94,.08)' },
{ label: '만료임박(30일)', val: soonList.length, color: 'var(--yellow)', bg: 'rgba(234,179,8,.08)' },
{ label: '만료/미가입', val: resultData.length - validCount, color: 'var(--red)', bg: 'rgba(239,68,68,.08)' },
].map((c, i) => (
<div key={i} style={{ padding: '14px 16px', background: c.bg, border: `1px solid ${c.color}33`, borderRadius: 'var(--rS)', textAlign: 'center' }}>
<div style={{ fontSize: 22, fontWeight: 800, color: c.color, fontFamily: 'var(--fM)' }}>{c.val}</div>
<div style={{ fontSize: 10, color: 'var(--t3)', fontFamily: 'var(--fK)', marginTop: 2 }}>{c.label}</div>
</div>
))}
</div>
{/* 테이블 */}
<div style={{ background: 'var(--bg3)', border: '1px solid var(--bd)', borderRadius: 'var(--rM)', overflow: 'hidden', marginBottom: 12 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '12px 16px', borderBottom: '1px solid var(--bd)' }}>
<div style={{ fontSize: 12, fontWeight: 700, fontFamily: 'var(--fK)', color: 'var(--t1)' }}> <span style={{ color: 'var(--cyan)' }}>{resultData.length}</span></div>
<div style={{ display: 'flex', gap: 6 }}>
<button onClick={() => alert('엑셀 내보내기 기능은 실제 API 연동 후 활성화됩니다.')} style={{ padding: '5px 12px', background: 'rgba(34,197,94,.1)', color: 'var(--green)', border: '1px solid rgba(34,197,94,.25)', borderRadius: 'var(--rS)', fontSize: 11, fontWeight: 600, cursor: 'pointer', fontFamily: 'var(--fK)' }}>📥 </button>
<button onClick={handleQuery} style={{ padding: '5px 12px', background: 'var(--bg0)', color: 'var(--t2)', border: '1px solid var(--bd)', borderRadius: 'var(--rS)', fontSize: 11, cursor: 'pointer', fontFamily: 'var(--fK)' }}>🔄 </button>
</div>
</div>
<div style={{ overflowX: 'auto' }}>
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 11, fontFamily: 'var(--fK)' }}>
<thead>
<tr style={{ background: 'var(--bg0)' }}>
{[
{ label: '선박명', align: 'left' },
{ label: 'MMSI', align: 'center' },
{ label: 'IMO', align: 'center' },
{ label: '보험종류', align: 'center' },
{ label: '보험사', align: 'center' },
{ label: '증권번호', align: 'center' },
{ label: '보험기간', align: 'center' },
{ label: '보상한도', align: 'right' },
{ label: '상태', align: 'center' },
].map((h, i) => (
<th key={i} style={{ padding: '10px 14px', textAlign: h.align as 'left' | 'center' | 'right', fontWeight: 700, color: 'var(--t2)', borderBottom: '1px solid var(--bd)', whiteSpace: 'nowrap' }}>{h.label}</th>
))}
</tr>
</thead>
<tbody>
{resultData.map((r, i) => {
const st = getStatus(r.expiry)
const isExp = st === 'expired'
const isSoon = st === 'soon'
return (
<tr key={i} style={{ borderBottom: '1px solid var(--bd)', background: isExp ? 'rgba(239,68,68,.03)' : undefined }}>
<td style={{ padding: '10px 14px', fontWeight: 600 }}>{r.shipName}</td>
<td style={{ padding: '10px 14px', textAlign: 'center', fontFamily: 'var(--fM)', fontSize: 11 }}>{r.mmsi || '—'}</td>
<td style={{ padding: '10px 14px', textAlign: 'center', fontFamily: 'var(--fM)', fontSize: 11 }}>{r.imo || '—'}</td>
<td style={{ padding: '10px 14px', textAlign: 'center' }}>{r.insType}</td>
<td style={{ padding: '10px 14px', textAlign: 'center' }}>{r.insurer}</td>
<td style={{ padding: '10px 14px', textAlign: 'center', fontFamily: 'var(--fM)', fontSize: 10, color: 'var(--t3)' }}>{r.policyNo}</td>
<td style={{ padding: '10px 14px', textAlign: 'center', fontFamily: 'var(--fM)', fontSize: 11, color: isExp ? 'var(--red)' : isSoon ? 'var(--yellow)' : undefined, fontWeight: isExp || isSoon ? 700 : undefined }}>{r.start} ~ {r.expiry}</td>
<td style={{ padding: '10px 14px', textAlign: 'right', fontWeight: 700, fontFamily: 'var(--fM)' }}>{r.limit}</td>
<td style={{ padding: '10px 14px', textAlign: 'center' }}>
<span style={{
padding: '3px 10px', borderRadius: 10, fontSize: 10, fontWeight: 600,
background: isExp ? 'rgba(239,68,68,.15)' : isSoon ? 'rgba(234,179,8,.15)' : 'rgba(34,197,94,.15)',
color: isExp ? 'var(--red)' : isSoon ? 'var(--yellow)' : 'var(--green)',
}}>
{isExp ? '만료' : isSoon ? '만료임박' : '유효'}
</span>
</td>
</tr>
)
})}
</tbody>
</table>
</div>
</div>
{/* 경고 */}
{(expiredList.length > 0 || soonList.length > 0) && (
<div style={{ padding: '12px 16px', background: 'rgba(234,179,8,.06)', border: '1px solid rgba(234,179,8,.25)', borderRadius: 'var(--rS)', fontSize: 12, color: 'var(--t2)', fontFamily: 'var(--fK)', marginBottom: 12 }}>
{expiredList.length > 0 && (
<><span style={{ color: 'var(--red)', fontWeight: 700 }}> {expiredList.length}:</span> {expiredList.map(r => r.shipName).join(', ')}<br /></>
)}
{soonList.length > 0 && (
<><span style={{ color: 'var(--yellow)', fontWeight: 700 }}> (30) {soonList.length}:</span> {soonList.map(r => r.shipName).join(', ')}</>
)}
</div>
)}
</>
)}
{/* ── API 연동 정보 푸터 ── */}
<div style={{ marginTop: 16, padding: '12px 16px', background: 'var(--bg3)', border: '1px solid var(--bd)', borderRadius: 'var(--rS)', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<div style={{ fontSize: 10, color: 'var(--t3)', fontFamily: 'var(--fK)', lineHeight: 1.7 }}>
<span style={{ color: 'var(--t2)', fontWeight: 700 }}> :</span> (KSA) · haewoon.or.kr<br />
<span style={{ color: 'var(--t2)', fontWeight: 700 }}> :</span> REST API (JSON) · · TTL 1
</div>
<div style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
<span style={{ fontSize: 10, color: 'var(--t3)', fontFamily: 'var(--fK)' }}> :</span>
<span style={{ fontSize: 10, color: 'var(--t2)', fontFamily: 'var(--fM)' }}>{lastSync}</span>
<button onClick={handleFullSync} style={{ padding: '4px 10px', background: 'var(--bg0)', color: 'var(--t2)', border: '1px solid var(--bd)', borderRadius: 4, fontSize: 10, cursor: 'pointer', fontFamily: 'var(--fK)' }}> </button>
</div>
</div>
</div>
)
}
// ── Main AssetsView ──
export function AssetsView() {
const [activeTab, setActiveTab] = useState<AssetsTab>('management')
return (
<div className="flex flex-col h-full w-full bg-bg-0">
{/* Tab Navigation */}
<div className="flex items-center justify-between border-b border-border bg-bg-1" style={{ flexShrink: 0 }}>
<div className="flex">
{([
{ id: 'management' as const, icon: '🗂', label: '자산 관리' },
{ id: 'upload' as const, icon: '📤', label: '자산 현행화 (업로드)' },
{ id: 'theory' as const, icon: '📚', label: '방제자원 이론' },
{ id: 'insurance' as const, icon: '🛡', label: '선박 보험정보' },
]).map(tab => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`px-5 py-3.5 text-xs font-semibold transition-all font-korean border-b-2 ${
activeTab === tab.id
? 'text-primary-cyan border-primary-cyan'
: 'text-text-3 border-transparent hover:text-text-2'
}`}
>
{tab.icon} {tab.label}
</button>
))}
</div>
<div className="flex items-center gap-1.5 px-3.5 py-1.5 border rounded-full text-[11px] text-primary-blue font-korean mr-4" style={{ borderColor: 'rgba(59,130,246,0.3)' }}>
👤 _방제과 ( )
</div>
</div>
{/* Content */}
<div className="flex-1 overflow-auto px-6 py-5">
<div className="w-full h-full">
{activeTab === 'management' && <AssetManagementTab />}
{activeTab === 'upload' && <AssetUploadTab />}
{activeTab === 'theory' && <AssetTheoryTab />}
{activeTab === 'insurance' && <ShipInsuranceTab />}
</div>
</div>
</div>
)
}