- 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>
2048 lines
131 KiB
TypeScript
Executable File
2048 lines
131 KiB
TypeScript
Executable File
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(
|
||
'© <a href="https://www.openstreetmap.org/copyright">OSM</a> © <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:272–286, 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.11–16, 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>
|
||
)
|
||
}
|