import { useState, useEffect, useCallback } from 'react' import { saveReport } from '../services/reportsApi' // ─── Data Types ───────────────────────────────────────────── export type ReportType = '초기보고서' | '지휘부 보고' | '예측보고서' | '종합보고서' | '유출유 보고' export type ReportStatus = '완료' | '수행중' | '테스트' export type Jurisdiction = '남해청' | '서해청' | '중부청' | '동해청' | '제주청' export type AnalysisCategory = '유출유 확산예측' | 'HNS 대기확산' | '긴급구난' | '' export interface OilSpillReportData { id: string title: string createdAt: string updatedAt: string author: string reportType: ReportType analysisCategory: AnalysisCategory jurisdiction: Jurisdiction status: ReportStatus incident: { name: string; writeTime: string; shipName: string; agent: string location: string; lat: string; lon: string; occurTime: string accidentType: string; pollutant: string; spillAmount: string depth: string; seabed: string } tide: { date: string; tideType: string; lowTide1: string; highTide1: string; lowTide2: string; highTide2: string }[] weather: { time: string; sunrise: string; sunset: string; windDir: string; windSpeed: string; currentDir: string; currentSpeed: string; waveHeight: string }[] spread: { elapsed: string; weathered: string; seaRemain: string; coastAttach: string; area: string }[] analysis: string aquaculture: { type: string; area: string; distance: string }[] beaches: { name: string; distance: string }[] markets: { name: string; distance: string }[] esi: { code: string; type: string; length: string }[] species: { category: string; species: string }[] habitat: { type: string; area: string }[] sensitivity: { level: string; area: string; color: string }[] vessels: { name: string; org: string; dist: string; speed: string; ton: string; collectorType: string; collectorCap: string; boomType: string; boomLength: string }[] etcEquipment: string recovery: { shipName: string; period: string }[] result: { spillTotal: string; weatheredTotal: string; recoveredTotal: string; seaRemainTotal: string; coastAttachTotal: string } } // eslint-disable-next-line react-refresh/only-export-components export function createEmptyReport(): OilSpillReportData { const now = new Date() const ts = `${now.getFullYear()}.${String(now.getMonth() + 1).padStart(2, '0')}.${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}` return { id: `RPT-${Date.now()}`, title: '', createdAt: now.toISOString(), updatedAt: now.toISOString(), author: '', reportType: '예측보고서', analysisCategory: '', jurisdiction: '남해청', status: '수행중', incident: { name: '', writeTime: ts, shipName: '', agent: '', location: '', lat: '', lon: '', occurTime: '', accidentType: '', pollutant: '', spillAmount: '', depth: '', seabed: '' }, tide: [{ date: '', tideType: '', lowTide1: '', highTide1: '', lowTide2: '', highTide2: '' }], weather: [{ time: '', sunrise: '', sunset: '', windDir: '', windSpeed: '', currentDir: '', currentSpeed: '', waveHeight: '' }], spread: [{ elapsed: '3시간', weathered: '', seaRemain: '', coastAttach: '', area: '' }, { elapsed: '6시간', weathered: '', seaRemain: '', coastAttach: '', area: '' }], analysis: '', aquaculture: [{ type: '', area: '', distance: '' }], beaches: [{ name: '', distance: '' }], markets: [{ name: '', distance: '' }], esi: [ { code: 'ESI 1', type: '수직암반', length: '' }, { code: 'ESI 2', type: '수평암반', length: '' }, { code: 'ESI 3', type: '세립질 모래', length: '' }, { code: 'ESI 4', type: '조립질 모래', length: '' }, { code: 'ESI 5', type: '모래·자갈', length: '' }, { code: 'ESI 6A', type: '자갈', length: '' }, { code: 'ESI 6B', type: '투과성 사석', length: '' }, { code: 'ESI 7', type: '반폐쇄성 해안', length: '' }, { code: 'ESI 8A', type: '갯벌', length: '' }, { code: 'ESI 8B', type: '염습지', length: '' }, ], species: [{ category: '양서파충류', species: '' }, { category: '조류', species: '' }, { category: '포유류', species: '' }], habitat: [{ type: '갯벌', area: '' }], sensitivity: [ { level: '매우 높음', area: '', color: '#ef4444' }, { level: '높음', area: '', color: '#f97316' }, { level: '보통', area: '', color: '#eab308' }, { level: '낮음', area: '', color: '#22c55e' }, ], vessels: [{ name: '', org: '', dist: '', speed: '', ton: '', collectorType: '', collectorCap: '', boomType: '', boomLength: '' }], etcEquipment: '', recovery: [{ shipName: '', period: '' }], result: { spillTotal: '', weatheredTotal: '', recoveredTotal: '', seaRemainTotal: '', coastAttachTotal: '' }, } } // eslint-disable-next-line react-refresh/only-export-components export function createSampleReport(): OilSpillReportData { return { id: `RPT-${Date.now()}`, title: '여수 수변공원 Diesel 유출사고 대응지원', createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), author: '남해청_방제과', reportType: '초기보고서', analysisCategory: '유출유 확산예측', jurisdiction: '남해청', status: '완료', incident: { name: '여수 수변공원 유출사고', writeTime: '2025.07.16 13:48', shipName: '수변공원', agent: '', location: '여수 돌산 남방', lat: '34° 43′ 37.6″', lon: '127° 43′ 32.6″', occurTime: '2025. 07. 08. 17:55', accidentType: '시설 파손', pollutant: 'Diesel', spillAmount: '1㎘', depth: '12m', seabed: '모래' }, tide: [ { date: '2025-07-08 20:55', tideType: '배꼽사리', lowTide1: '01:53 (146)', highTide1: '13:19 (104)', lowTide2: '07:17 (253)', highTide2: '20:24 (310)' }, { date: '2025-07-08 23:55', tideType: '배꼽사리', lowTide1: '01:53 (146)', highTide1: '13:19 (104)', lowTide2: '07:17 (253)', highTide2: '20:24 (310)' }, ], weather: [ { time: '2025-07-08 20:55', sunrise: '05:22', sunset: '19:46', windDir: '북동', windSpeed: '0.0', currentDir: '북', currentSpeed: '0.0 / 0.0', waveHeight: '0.0' }, { time: '2025-07-08 23:55', sunrise: '05:22', sunset: '19:46', windDir: '북동', windSpeed: '0.0', currentDir: '북', currentSpeed: '0.0 / 0.0', waveHeight: '0.0' }, ], spread: [ { elapsed: '3시간', weathered: '0.48', seaRemain: '0.52', coastAttach: '0.12', area: '1.75' }, { elapsed: '6시간', weathered: '0.64', seaRemain: '0.36', coastAttach: '0.24', area: '3.49' }, ], analysis: '', aquaculture: [ { type: '', area: '3.19', distance: '0.56' }, { type: '', area: '20.15', distance: '0.83' }, { type: '어류', area: '3.84', distance: '1.14' }, { type: '', area: '2.89', distance: '1.40' }, { type: '어류', area: '0.90', distance: '1.42' }, ], beaches: [{ name: '검은모래해수욕장', distance: '5.86' }, { name: '무슬목해수욕장', distance: '6.69' }, { name: '모사금해수욕장', distance: '8.18' }], markets: [{ name: '돌산대교 회타운', distance: '1.09' }], esi: [ { code: 'ESI 1', type: '수직암반', length: '29.05 km' }, { code: 'ESI 2', type: '수평암반', length: '13.76 km' }, { code: 'ESI 3', type: '세립질 모래', length: '1.03 km' }, { code: 'ESI 4', type: '조립질 모래', length: '0.47 km' }, { code: 'ESI 5', type: '모래·자갈', length: '21.10 km' }, { code: 'ESI 6A', type: '자갈', length: '15.29 km' }, { code: 'ESI 6B', type: '투과성 사석', length: '26.60 km' }, { code: 'ESI 7', type: '반폐쇄성 해안', length: '9.39 km' }, { code: 'ESI 8A', type: '갯벌', length: '0.00 km' }, { code: 'ESI 8B', type: '염습지', length: '3.43 km' }, ], species: [{ category: '양서파충류', species: '' }, { category: '조류', species: '뿔논병아리(겨울철새), 꾀꼬리(여름철새)' }, { category: '포유류', species: '수달' }], habitat: [{ type: '갯벌', area: '4.32 km²' }], sensitivity: [ { level: '매우 높음', area: '18.54', color: '#ef4444' }, { level: '높음', area: '31.32', color: '#f97316' }, { level: '보통', area: '59.22', color: '#eab308' }, { level: '낮음', area: '32.13', color: '#22c55e' }, ], vessels: [ { name: '방제15호', org: '서해청', dist: '0.41', speed: '7.0', ton: '450.0', collectorType: '흡착', collectorCap: '140.0', boomType: 'B', boomLength: '60' }, { name: '방제26호', org: '서해청', dist: '0.41', speed: '13.0', ton: '360.0', collectorType: '흡착', collectorCap: '100.0', boomType: 'B', boomLength: '300' }, { name: '방제1001호', org: '서해청', dist: '0.74', speed: '10.0', ton: '564.0', collectorType: '흡착', collectorCap: '30.0', boomType: 'B', boomLength: '20' }, { name: '전남939호', org: '서해청', dist: '0.74', speed: '10.0', ton: '149.0', collectorType: '흡착', collectorCap: '30.0', boomType: 'B', boomLength: '300' }, { name: '동양방제호', org: '서해청', dist: '1.78', speed: '10.0', ton: '179.0', collectorType: '위어,흡착', collectorCap: '66.0', boomType: 'C', boomLength: '300' }, { name: '화학방제2함', org: '서해청', dist: '3.00', speed: '13.0', ton: '501.0', collectorType: '위어,브러쉬', collectorCap: '150.0', boomType: '특수', boomLength: '60' }, { name: '우진방제호', org: '서해청', dist: '13.22', speed: '9.0', ton: '79.0', collectorType: '위어', collectorCap: '20.0', boomType: 'C', boomLength: '300' }, ], etcEquipment: '', recovery: [{ shipName: '방제15호', period: '0' }], result: { spillTotal: '1', weatheredTotal: '0.64', recoveredTotal: '0', seaRemainTotal: '0.36', coastAttachTotal: '0.24' }, } } // ─── localStorage helpers 제거됨 — reportsApi.ts 사용 ──────── // ─── Styles ───────────────────────────────────────────────── const S = { page: { background: 'var(--bg1)', color: 'var(--t1)', padding: '32px 40px', marginBottom: '24px', borderRadius: '6px', border: '1px solid var(--bd)', fontFamily: "'Pretendard', 'Noto Sans KR', sans-serif", fontSize: '12px', lineHeight: '1.6', position: 'relative' as const, width: '100%', boxSizing: 'border-box' as const }, sectionTitle: { background: 'rgba(6,182,212,0.12)', color: 'var(--cyan)', padding: '8px 16px', fontSize: '13px', fontWeight: 700, marginBottom: '12px', borderRadius: '4px', border: '1px solid rgba(6,182,212,0.2)' }, subHeader: { fontSize: '14px', fontWeight: 700, color: 'var(--cyan)', marginBottom: '12px', borderBottom: '2px solid var(--bd)', paddingBottom: '6px' }, table: { width: '100%', tableLayout: 'fixed' as const, borderCollapse: 'collapse' as const, fontSize: '11px', marginBottom: '16px' }, th: { background: 'var(--bg3)', border: '1px solid var(--bd)', padding: '6px 10px', fontWeight: 600, color: 'var(--t2)', textAlign: 'center' as const, fontSize: '10px' }, td: { border: '1px solid var(--bd)', padding: '5px 10px', textAlign: 'center' as const, fontSize: '11px', color: 'var(--t2)' }, tdLeft: { border: '1px solid var(--bd)', padding: '5px 10px', textAlign: 'left' as const, fontSize: '11px', color: 'var(--t2)' }, thLabel: { background: 'var(--bg3)', border: '1px solid var(--bd)', padding: '6px 10px', fontWeight: 600, color: 'var(--t2)', textAlign: 'left' as const, fontSize: '11px', width: '120px' }, mapPlaceholder: { width: '100%', height: '240px', background: 'var(--bg0)', border: '2px dashed var(--bd)', borderRadius: '4px', display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--t3)', fontSize: '13px', fontWeight: 600, marginBottom: '16px' }, } // ─── Editable cell ────────────────────────────────────────── const inputStyle: React.CSSProperties = { width: '100%', background: 'var(--bg0)', border: '1px solid var(--bdL)', borderRadius: '3px', padding: '4px 8px', fontSize: '11px', color: 'var(--t1)', outline: 'none', textAlign: 'center', } function ECell({ value, editing, onChange, align, placeholder }: { value: string; editing: boolean; onChange?: (v: string) => void; align?: 'left' | 'center'; placeholder?: string }) { const style = align === 'left' ? S.tdLeft : S.td if (!editing) { return {value || placeholder || '-'} } return ( onChange?.(e.target.value)} placeholder={placeholder} /> ) } function AddRowBtn({ onClick, label }: { onClick: () => void; label?: string }) { return ( ) } // ─── Page components ──────────────────────────────────────── function Page1({ data, editing, onChange }: { data: OilSpillReportData; editing: boolean; onChange: (d: OilSpillReportData) => void }) { const inc = data.incident const set = (k: keyof typeof inc, v: string) => onChange({ ...data, incident: { ...inc, [k]: v } }) return (
해양오염방제지원시스템
유류오염사고 대응지원 상황도
1. 사고 정보
set('name', v)} placeholder="사고명 입력" /> set('writeTime', v)} /> set('shipName', v)} placeholder="선명" /> set('agent', v)} placeholder="제원" /> set('location', v)} placeholder="위치" /> {editing && set('lat', v)} placeholder="34° 43′ 37.6″" /> set('lon', v)} placeholder="127° 43′ 32.6″" />} set('occurTime', v)} /> set('accidentType', v)} placeholder="사고유형" /> set('pollutant', v)} /> set('spillAmount', v)} /> set('depth', v)} placeholder="수심" /> set('seabed', v)} placeholder="저질" />
사고명작성시간
선명(시설명)제원
사고위치좌표
위도경도
발생시각사고유형
오염물질유출 추정량
수심저질
) } function Page2({ data, editing, onChange }: { data: OilSpillReportData; editing: boolean; onChange: (d: OilSpillReportData) => void }) { const setTide = (i: number, k: string, v: string) => { const t = [...data.tide]; t[i] = { ...t[i], [k]: v }; onChange({ ...data, tide: t }) } const setWeather = (i: number, k: string, v: string) => { const w = [...data.weather]; w[i] = { ...w[i], [k]: v }; onChange({ ...data, weather: w }) } const setSpread = (i: number, k: string, v: string) => { const s = [...data.spread]; s[i] = { ...s[i], [k]: v }; onChange({ ...data, spread: s }) } return (
해양오염방제지원시스템
2. 해양기상정보
조석 정보
{data.tide.map((t, i) => ( setTide(i, 'date', v)} /> setTide(i, 'tideType', v)} /> setTide(i, 'lowTide1', v)} /> setTide(i, 'lowTide2', v)} /> setTide(i, 'highTide1', v)} /> setTide(i, 'highTide2', v)} /> ))}
일자물때저조고조
{editing && onChange({ ...data, tide: [...data.tide, { date: '', tideType: '', lowTide1: '', highTide1: '', lowTide2: '', highTide2: '' }] })} />}
기상 정보
{data.weather.map((w, i) => ( setWeather(i, 'time', v)} /> setWeather(i, 'sunrise', v)} /> setWeather(i, 'sunset', v)} /> setWeather(i, 'windDir', v)} /> setWeather(i, 'windSpeed', v)} /> setWeather(i, 'currentDir', v)} /> setWeather(i, 'currentSpeed', v)} /> setWeather(i, 'waveHeight', v)} /> ))}
기상 예측시간일출일몰풍향풍속(m/s)유향유속(knot/m/s)파고(m)
{editing && onChange({ ...data, weather: [...data.weather, { time: '', sunrise: '', sunset: '', windDir: '', windSpeed: '', currentDir: '', currentSpeed: '', waveHeight: '' }] })} />}
3. 유출유 확산예측
확산예측 3시간 지도
확산예측 6시간 지도
시간별 상세정보
{data.spread.map((s, i) => ( setSpread(i, 'elapsed', v)} /> setSpread(i, 'weathered', v)} /> setSpread(i, 'seaRemain', v)} /> setSpread(i, 'coastAttach', v)} /> setSpread(i, 'area', v)} /> ))}
경과시간풍화량(kl)해상잔존량(kl)연안부착량(kl)오염해역면적(km²)
{editing && onChange({ ...data, spread: [...data.spread, { elapsed: '', weathered: '', seaRemain: '', coastAttach: '', area: '' }] })} />}
) } function Page3({ data, editing, onChange }: { data: OilSpillReportData; editing: boolean; onChange: (d: OilSpillReportData) => void }) { return (
해양오염방제지원시스템
분석
{editing ? (