import { useState, useEffect } from 'react'; import { createEmptyReport, } from './OilSpillReportTemplate'; import { consumeReportGenCategory, consumeHnsReportPayload, type HnsReportPayload, consumeOilReportPayload, type OilReportPayload } from '@common/hooks/useSubMenu'; import { saveReport } from '../services/reportsApi'; import { CATEGORIES, sampleOilData, sampleHnsData, sampleRescueData, type ReportCategory, type ReportSection, } from './reportTypes'; import { exportAsPDF } from './reportUtils'; interface ReportGeneratorProps { onSave: () => void; } function ReportGenerator({ onSave }: ReportGeneratorProps) { const [activeCat, setActiveCat] = useState(() => { const hint = consumeReportGenCategory() return (hint === 0 || hint === 1 || hint === 2) ? hint : 0 }) const [selectedTemplate, setSelectedTemplate] = useState(0) const [sectionsMap, setSectionsMap] = useState>(() => ({ 0: CATEGORIES[0].sections.map(s => ({ ...s })), 1: CATEGORIES[1].sections.map(s => ({ ...s })), 2: CATEGORIES[2].sections.map(s => ({ ...s })), })) // HNS 실 데이터 (없으면 sampleHnsData fallback) const [hnsPayload, setHnsPayload] = useState(null) // OIL 실 데이터 (없으면 sampleOilData fallback) const [oilPayload, setOilPayload] = useState(null) // 외부에서 카테고리 힌트가 변경되면 반영 useEffect(() => { const hint = consumeReportGenCategory() if (hint === 0 || hint === 1 || hint === 2) { // eslint-disable-next-line react-hooks/set-state-in-effect setActiveCat(hint) setSelectedTemplate(0) } // HNS 데이터 소비 const payload = consumeHnsReportPayload() if (payload) setHnsPayload(payload) // OIL 예측 데이터 소비 const oilData = consumeOilReportPayload() if (oilData) setOilPayload(oilData) }, []) const cat = CATEGORIES[activeCat] const sections = sectionsMap[activeCat] const activeSections = sections.filter(s => s.checked) const toggleSection = (id: string) => { setSectionsMap(prev => ({ ...prev, [activeCat]: prev[activeCat].map(s => s.id === id ? { ...s, checked: !s.checked } : s), })) } const handleSave = async () => { const report = createEmptyReport() report.reportType = activeCat === 0 ? '예측보고서' : activeCat === 1 ? '종합보고서' : '초기보고서' report.analysisCategory = activeCat === 0 ? '유출유 확산예측' : activeCat === 1 ? 'HNS 대기확산' : '긴급구난' report.title = cat.reportName report.status = '완료' report.author = '시스템 자동생성' if (activeCat === 0) { if (oilPayload) { report.incident.name = oilPayload.incident.name; report.incident.occurTime = oilPayload.incident.occurTime; report.incident.location = oilPayload.incident.location; report.incident.lat = String(oilPayload.incident.lat ?? ''); report.incident.lon = String(oilPayload.incident.lon ?? ''); report.incident.shipName = oilPayload.incident.shipName; report.incident.pollutant = oilPayload.pollution.oilType; report.incident.spillAmount = oilPayload.pollution.spillAmount; } else { report.incident.pollutant = sampleOilData.pollution.oilType; report.incident.spillAmount = sampleOilData.pollution.spillAmount; } } try { await saveReport(report) onSave() } catch (err) { console.error('[reports] 저장 오류:', err) alert('보고서 저장 중 오류가 발생했습니다.') } } const handleDownload = () => { const secColor = cat.color === 'var(--cyan)' ? '#06b6d4' : cat.color === 'var(--orange)' ? '#f97316' : '#ef4444'; const sectionHTML = activeSections.map(sec => { let content = `

${sec.desc}

`; // OIL 섹션에 실 데이터 삽입 if (activeCat === 0 && oilPayload) { if (sec.id === 'oil-pollution') { const rows = [ ['유출량', oilPayload.pollution.spillAmount, '풍화량', oilPayload.pollution.weathered], ['해상잔유량', oilPayload.pollution.seaRemain, '오염해역면적', oilPayload.pollution.pollutionArea], ['연안부착량', oilPayload.pollution.coastAttach, '오염해안길이', oilPayload.pollution.coastLength], ]; const simBanner = !oilPayload.hasSimulation ? '

시뮬레이션이 실행되지 않아 오염량은 입력값 기준으로 표시됩니다.

' : ''; const trs = rows.map(r => `${r[0]}${r[1]}${r[2]}${r[3]}` ).join(''); content = `${simBanner}${trs}
`; } } // HNS 섹션에 실 데이터 삽입 if (activeCat === 1 && hnsPayload) { if (sec.id === 'hns-atm') { const mapImg = hnsPayload.mapImageDataUrl ? `` : '
[대기확산 예측 지도]
'; content = `${mapImg}
${hnsPayload.atm.model}
${hnsPayload.atm.maxDistance}
최대 농도
${hnsPayload.maxConcentration}
AEGL-1 면적
${hnsPayload.aeglAreas.aegl1}
`; } else if (sec.id === 'hns-hazard') { content = `
AEGL-3
${hnsPayload.hazard.aegl3} (${hnsPayload.aeglAreas.aegl3})
AEGL-2
${hnsPayload.hazard.aegl2} (${hnsPayload.aeglAreas.aegl2})
AEGL-1
${hnsPayload.hazard.aegl1} (${hnsPayload.aeglAreas.aegl1})
`; } else if (sec.id === 'hns-substance') { content = `
물질명${hnsPayload.substance.name}
독성기준${hnsPayload.substance.toxicity}
`; } else if (sec.id === 'hns-weather') { content = `
풍향
${hnsPayload.weather.windDir}
풍속
${hnsPayload.weather.windSpeed}
안정도
${hnsPayload.weather.stability}
기온
${hnsPayload.weather.temperature}
`; } } return `

${sec.icon} ${sec.title}

${content}`; }).join('') const html = `${cat.reportName}

해양환경 위기대응 통합지원시스템

${cat.reportName}

${sectionHTML}` exportAsPDF(html, cat.reportName) } return (
{/* Header */}

보고서 생성

보고서 유형을 선택하고 포함할 섹션을 구성하여 보고서를 생성합니다.

{/* 3 카테고리 카드 */}
{CATEGORIES.map((c, i) => { const isActive = activeCat === i return ( ) })}
{/* Left Sidebar - Template + Sections */}
{/* 템플릿 선택 */}

📄 보고서 템플릿

{cat.templates.map((tmpl, i) => ( ))}
{/* 섹션 체크 */}

📋 포함 섹션

{sections.map(sec => ( ))}
{/* Right - Report Preview */}
{/* Preview Header */}

📄 보고서 미리보기 {cat.templates[selectedTemplate].label}

{/* Preview Content */}
{/* Report Header */}

해양환경 위기대응 통합지원시스템

{cat.reportName}

{cat.templates[selectedTemplate].label}

{/* Dynamic Sections */} {activeSections.map(sec => (

{sec.icon} {sec.title}

{/* ── 유출유 확산예측 섹션들 ── */} {sec.id === 'oil-spread' && ( <>
[확산예측 지도 - 범위 조절 작업]
{[ { label: 'KOSPS', value: oilPayload?.spread.kosps || sampleOilData.spread.kosps, color: '#06b6d4' }, { label: 'OpenDrift', value: oilPayload?.spread.openDrift || sampleOilData.spread.openDrift, color: '#ef4444' }, { label: 'POSEIDON', value: oilPayload?.spread.poseidon || sampleOilData.spread.poseidon, color: '#f97316' }, ].map((m, i) => (

{m.label}

{m.value}

))}
)} {sec.id === 'oil-pollution' && ( <> {oilPayload && !oilPayload.hasSimulation && (
시뮬레이션이 실행되지 않아 오염량은 입력값 기준으로 표시됩니다.
)} {[ ['유출량', oilPayload?.pollution.spillAmount || sampleOilData.pollution.spillAmount, '풍화량', oilPayload?.pollution.weathered || sampleOilData.pollution.weathered], ['해상잔유량', oilPayload?.pollution.seaRemain || sampleOilData.pollution.seaRemain, '오염해역면적', oilPayload?.pollution.pollutionArea || sampleOilData.pollution.pollutionArea], ['연안부착량', oilPayload?.pollution.coastAttach || sampleOilData.pollution.coastAttach, '오염해안길이', oilPayload?.pollution.coastLength || sampleOilData.pollution.coastLength], ].map((row, i) => ( ))}
{row[0]} {row[1]} {row[2]} {row[3]}
)} {sec.id === 'oil-sensitive' && ( <>

반경 10 NM 기준

{sampleOilData.sensitive.map((item, i) => ( {item.label} ))}
)} {sec.id === 'oil-coastal' && (

최초 부착시간: {oilPayload?.coastal?.firstTime ?? sampleOilData.coastal.firstTime} {' / '} 부착 해안길이: {oilPayload?.pollution.coastLength || sampleOilData.coastal.coastLength}

)} {sec.id === 'oil-defense' && (

방제자원 배치 계획에 따른 전략을 수립합니다.

[방제자원 배치 지도]
)} {sec.id === 'oil-tide' && ( <>

고조: {sampleOilData.tide.highTide1} {' / '}저조: {sampleOilData.tide.lowTide} {' / '}고조: {sampleOilData.tide.highTide2}

{oilPayload?.weather && (

기상: 풍향/풍속 {oilPayload.weather.windDir} {' / '}파고 {oilPayload.weather.waveHeight} {' / '}기온 {oilPayload.weather.temp}

)} )} {/* ── HNS 대기확산 섹션들 ── */} {sec.id === 'hns-atm' && ( <> {hnsPayload?.mapImageDataUrl ? ( 대기확산 예측 지도 ) : (
[대기확산 예측 지도]
)}
{[ { label: hnsPayload?.atm.model || 'ALOHA', value: hnsPayload?.atm.maxDistance || sampleHnsData.atm.aloha, color: '#f97316', desc: '최대 확산거리' }, { label: '최대 농도', value: hnsPayload?.maxConcentration || '—', color: '#ef4444', desc: '지상 1.5m 기준' }, { label: 'AEGL-1 면적', value: hnsPayload?.aeglAreas.aegl1 || '—', color: '#06b6d4', desc: '확산 영향 면적' }, ].map((m, i) => (

{m.label}

{m.value}

{m.desc}

))}
)} {sec.id === 'hns-hazard' && (
{[ { label: 'AEGL-3 구역', value: hnsPayload?.hazard.aegl3 || sampleHnsData.hazard.erpg3, area: hnsPayload?.aeglAreas.aegl3, color: '#ef4444', desc: '생명 위협' }, { label: 'AEGL-2 구역', value: hnsPayload?.hazard.aegl2 || sampleHnsData.hazard.erpg2, area: hnsPayload?.aeglAreas.aegl2, color: '#f97316', desc: '건강 피해' }, { label: 'AEGL-1 구역', value: hnsPayload?.hazard.aegl1 || sampleHnsData.hazard.evacuation, area: hnsPayload?.aeglAreas.aegl1, color: '#eab308', desc: '불쾌감' }, ].map((h, i) => (

{h.label}

{h.value}

{h.area &&

{h.area}

}

{h.desc}

))}
)} {sec.id === 'hns-substance' && (
{[ { k: '물질명', v: hnsPayload?.substance.name || sampleHnsData.substance.name }, { k: 'UN번호', v: hnsPayload?.substance.un || sampleHnsData.substance.un }, { k: 'CAS번호', v: hnsPayload?.substance.cas || sampleHnsData.substance.cas }, { k: '위험등급', v: hnsPayload?.substance.class || sampleHnsData.substance.class }, ].map((r, i) => (
{r.k} {r.v}
))}
독성기준 {hnsPayload?.substance.toxicity || sampleHnsData.substance.toxicity}
)} {sec.id === 'hns-ppe' && (
{sampleHnsData.ppe.map((item, i) => ( 🛡 {item} ))}
)} {sec.id === 'hns-facility' && (
{[ { label: '인근 학교', value: `${sampleHnsData.facility.schools}개소`, icon: '🏫' }, { label: '의료시설', value: `${sampleHnsData.facility.hospitals}개소`, icon: '🏥' }, { label: '주변 인구', value: sampleHnsData.facility.population, icon: '👥' }, ].map((f, i) => (
{f.icon}

{f.value}

{f.label}

))}
)} {sec.id === 'hns-3d' && (
[3D 농도 분포 시각화]
)} {sec.id === 'hns-weather' && (
{[ { label: '풍향', value: hnsPayload?.weather.windDir || 'NE 42°', icon: '🌬' }, { label: '풍속', value: hnsPayload?.weather.windSpeed || '5.2 m/s', icon: '💨' }, { label: '대기안정도', value: hnsPayload?.weather.stability || 'D (중립)', icon: '🌡' }, { label: '기온', value: hnsPayload?.weather.temperature || '8.5°C', icon: '☀️' }, ].map((w, i) => (
{w.icon}

{w.value}

{w.label}

))}
)} {/* ── 긴급구난 섹션들 ── */} {sec.id === 'rescue-safety' && (
{[ { label: 'GM (복원력)', value: sampleRescueData.safety.gm, color: '#f97316' }, { label: '경사각 (Heel)', value: sampleRescueData.safety.heel, color: '#ef4444' }, { label: '트림 (Trim)', value: sampleRescueData.safety.trim, color: '#06b6d4' }, { label: '안전 상태', value: sampleRescueData.safety.status, color: '#f97316' }, ].map((s, i) => (

{s.label}

{s.value}

))}
)} {sec.id === 'rescue-timeline' && (
{[ { time: '06:28', event: '충돌 발생 — ORIENTAL GLORY ↔ HAI FENG 168', color: '#ef4444' }, { time: '06:30', event: 'No.1P 탱크 파공, 벙커C유 유출 개시', color: '#f97316' }, { time: '06:35', event: 'VHF Ch.16 조난통신, 해경 출동 요청', color: '#eab308' }, { time: '07:15', event: '해경 3009함 현장 도착, 방제 개시', color: '#06b6d4' }, ].map((e, i) => (
{e.time} {e.event}
))}
)} {sec.id === 'rescue-casualty' && (
{[ { label: '총원', value: sampleRescueData.casualty.total }, { label: '구조완료', value: sampleRescueData.casualty.rescued, color: '#22c55e' }, { label: '실종', value: sampleRescueData.casualty.missing, color: '#ef4444' }, { label: '부상', value: sampleRescueData.casualty.injured, color: '#f97316' }, ].map((c, i) => (

{c.label}

{c.value}

))}
)} {sec.id === 'rescue-resource' && ( {sampleRescueData.resources.map((r, i) => ( ))}
유형 선박/장비명 도착예정 상태
{r.type} {r.name} {r.eta} {r.status}
)} {sec.id === 'rescue-grounding' && (
{[ { label: '좌초 위험도', value: sampleRescueData.grounding.risk, color: '#ef4444' }, { label: '최근 천해', value: sampleRescueData.grounding.nearestShallow, color: '#f97316' }, { label: '현재 수심', value: sampleRescueData.grounding.depth, color: '#06b6d4' }, ].map((g, i) => (

{g.label}

{g.value}

))}
)} {sec.id === 'rescue-weather' && (
{[ { label: '파고', value: '1.5 m', icon: '🌊' }, { label: '풍속', value: '5.2 m/s', icon: '🌬' }, { label: '조류', value: '1.2 kts NE', icon: '🌀' }, { label: '시정', value: '8 km', icon: '👁' }, ].map((w, i) => (
{w.icon}

{w.value}

{w.label}

))}
)}
))} {activeSections.length === 0 && (
📋

왼쪽에서 보고서에 포함할 섹션을 선택하세요

)}
) } export default ReportGenerator;