- DB 마이그레이션 007_reports.sql: 7개 테이블 (REPORT_TMPL, REPORT_TMPL_SECT, REPORT_ANALYSIS_CTGR, REPORT_CTGR_SECT, REPORT, REPORT_SECT_DATA 등) + 초기 데이터 (5개 템플릿, 3개 카테고리, 섹션 정의) - 백엔드 reportsService.ts: 템플릿/카테고리 조회, 보고서 CRUD, 섹션 UPSERT - 백엔드 reportsRouter.ts: GET/POST only 패턴 (보안취약점 가이드 준수) - GET /api/reports, GET /api/reports/:sn (조회) - POST /api/reports (생성), POST /:sn/update (수정), POST /:sn/delete (삭제) - POST /:sn/sections/:sectCd (개별 섹션 수정) - 프론트 reportsApi.ts: API 호출 + OilSpillReportData ↔ API 변환 + 캐싱 - 프론트 4개 컴포넌트 localStorage → API 전환: ReportsView, OilSpillReportTemplate, TemplateFormEditor, ReportGenerator Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
538 lines
30 KiB
TypeScript
538 lines
30 KiB
TypeScript
import { useState, useEffect } from 'react';
|
|
import {
|
|
createEmptyReport,
|
|
} from './OilSpillReportTemplate';
|
|
import { consumeReportGenCategory } 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<ReportCategory>(() => {
|
|
const hint = consumeReportGenCategory()
|
|
return (hint === 0 || hint === 1 || hint === 2) ? hint : 0
|
|
})
|
|
const [selectedTemplate, setSelectedTemplate] = useState(0)
|
|
const [sectionsMap, setSectionsMap] = useState<Record<number, ReportSection[]>>(() => ({
|
|
0: CATEGORIES[0].sections.map(s => ({ ...s })),
|
|
1: CATEGORIES[1].sections.map(s => ({ ...s })),
|
|
2: CATEGORIES[2].sections.map(s => ({ ...s })),
|
|
}))
|
|
|
|
// 외부에서 카테고리 힌트가 변경되면 반영
|
|
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)
|
|
}
|
|
}, [])
|
|
|
|
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) {
|
|
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 sectionHTML = activeSections.map(sec => {
|
|
return `<h3 style="color:${cat.color === 'var(--cyan)' ? '#06b6d4' : cat.color === 'var(--orange)' ? '#f97316' : '#ef4444'};font-size:14px;margin:20px 0 8px;">${sec.icon} ${sec.title}</h3><p style="font-size:12px;color:#666;">${sec.desc}</p>`
|
|
}).join('')
|
|
const html = `<!DOCTYPE html><html><head><meta charset="utf-8"><title>${cat.reportName}</title><style>@page{size:A4;margin:20mm}body{font-family:'맑은 고딕',sans-serif;color:#1a1a1a;max-width:800px;margin:0 auto;padding:40px}</style></head><body><div style="text-align:center;margin-bottom:30px"><h1 style="font-size:20px;margin:0">해양환경 위기대응 통합지원시스템</h1><h2 style="font-size:16px;color:#0891b2;margin:8px 0">${cat.reportName}</h2></div>${sectionHTML}</body></html>`
|
|
exportAsPDF(html, cat.reportName)
|
|
}
|
|
|
|
return (
|
|
<div className="flex flex-col h-full w-full">
|
|
{/* Header */}
|
|
<div className="px-6 py-4 border-b border-border bg-bg-1">
|
|
<h2 className="text-[16px] font-bold text-text-1 font-korean">보고서 생성</h2>
|
|
<p className="text-[11px] text-text-3 font-korean mt-1">보고서 유형을 선택하고 포함할 섹션을 구성하여 보고서를 생성합니다.</p>
|
|
|
|
{/* 3 카테고리 카드 */}
|
|
<div style={{ display: 'flex', gap: '14px', marginTop: '16px' }}>
|
|
{CATEGORIES.map((c, i) => {
|
|
const isActive = activeCat === i
|
|
return (
|
|
<button
|
|
key={i}
|
|
onClick={() => { setActiveCat(i as ReportCategory); setSelectedTemplate(0) }}
|
|
style={{
|
|
flex: 1, padding: '14px 16px', borderRadius: '10px', cursor: 'pointer',
|
|
textAlign: 'center', transition: '0.2s',
|
|
border: `1px solid ${isActive ? c.borderColor : 'var(--bd)'}`,
|
|
background: isActive ? c.bgActive : 'var(--bg3)',
|
|
}}
|
|
>
|
|
<div style={{ fontSize: '22px', marginBottom: '4px' }}>{c.icon}</div>
|
|
<div style={{
|
|
fontSize: '12px', fontWeight: 700, fontFamily: 'var(--fK)',
|
|
color: isActive ? c.color : 'var(--t3)',
|
|
}}>
|
|
{c.label}
|
|
</div>
|
|
<div style={{ fontSize: '9px', color: 'var(--t3)', fontFamily: 'var(--fK)', marginTop: '2px' }}>
|
|
{c.desc}
|
|
</div>
|
|
</button>
|
|
)
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex flex-1 overflow-hidden">
|
|
{/* Left Sidebar - Template + Sections */}
|
|
<div className="w-[250px] min-w-[250px] border-r border-border bg-bg-1 flex flex-col overflow-y-auto shrink-0">
|
|
{/* 템플릿 선택 */}
|
|
<div className="px-4 py-3 border-b border-border">
|
|
<h3 className="text-[11px] font-bold text-text-2 font-korean flex items-center gap-2">📄 보고서 템플릿</h3>
|
|
</div>
|
|
<div className="flex flex-col gap-1.5 px-3 py-2 border-b border-border">
|
|
{cat.templates.map((tmpl, i) => (
|
|
<button
|
|
key={i}
|
|
onClick={() => setSelectedTemplate(i)}
|
|
className="flex items-center gap-2 px-3 py-2.5 rounded-lg transition-all text-left"
|
|
style={{
|
|
border: `1px solid ${selectedTemplate === i ? cat.borderColor : 'var(--bd)'}`,
|
|
background: selectedTemplate === i ? cat.bgActive : 'var(--bg2)',
|
|
}}
|
|
>
|
|
<span style={{ fontSize: '14px' }}>{tmpl.icon}</span>
|
|
<span style={{
|
|
fontSize: '11px', fontWeight: 600, fontFamily: 'var(--fK)',
|
|
color: selectedTemplate === i ? cat.color : 'var(--t2)',
|
|
}}>
|
|
{tmpl.label}
|
|
</span>
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{/* 섹션 체크 */}
|
|
<div className="px-4 py-3 border-b border-border">
|
|
<h3 className="text-[11px] font-bold text-text-2 font-korean flex items-center gap-2">📋 포함 섹션</h3>
|
|
</div>
|
|
<div className="flex flex-col gap-1.5 p-3">
|
|
{sections.map(sec => (
|
|
<button
|
|
key={sec.id}
|
|
onClick={() => toggleSection(sec.id)}
|
|
className="flex items-start gap-2.5 p-2.5 rounded-lg border transition-all text-left"
|
|
style={{
|
|
borderColor: sec.checked ? cat.borderColor : 'var(--bd)',
|
|
background: sec.checked ? cat.bgActive : 'var(--bg2)',
|
|
opacity: sec.checked ? 1 : 0.55,
|
|
}}
|
|
>
|
|
<div style={{
|
|
width: '18px', height: '18px', borderRadius: '4px', flexShrink: 0, marginTop: '1px',
|
|
display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '10px',
|
|
background: sec.checked ? cat.color : 'var(--bg3)',
|
|
color: sec.checked ? '#fff' : 'transparent',
|
|
border: sec.checked ? 'none' : '1px solid var(--bd)',
|
|
}}>
|
|
✓
|
|
</div>
|
|
<div style={{ flex: 1, minWidth: 0 }}>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
|
|
<span style={{ fontSize: '12px' }}>{sec.icon}</span>
|
|
<span style={{
|
|
fontSize: '11px', fontWeight: 700, fontFamily: 'var(--fK)',
|
|
color: sec.checked ? 'var(--t1)' : 'var(--t3)',
|
|
}}>
|
|
{sec.title}
|
|
</span>
|
|
</div>
|
|
<p style={{ fontSize: '9px', color: 'var(--t3)', fontFamily: 'var(--fK)', marginTop: '2px' }}>
|
|
{sec.desc}
|
|
</p>
|
|
</div>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Right - Report Preview */}
|
|
<div className="flex-1 flex flex-col overflow-hidden">
|
|
{/* Preview Header */}
|
|
<div className="flex items-center justify-between px-6 py-3 border-b border-border bg-bg-1">
|
|
<h3 className="text-[13px] font-bold text-text-1 font-korean flex items-center gap-2">
|
|
📄 보고서 미리보기
|
|
<span style={{
|
|
fontSize: '10px', fontWeight: 600, padding: '2px 8px', borderRadius: '4px',
|
|
background: cat.bgActive, color: cat.color, fontFamily: 'var(--fK)',
|
|
}}>
|
|
{cat.templates[selectedTemplate].label}
|
|
</span>
|
|
</h3>
|
|
<div className="flex items-center gap-2">
|
|
<button
|
|
onClick={handleDownload}
|
|
className="px-3 py-1.5 text-[11px] font-semibold rounded transition-all font-korean flex items-center gap-1.5"
|
|
style={{ background: cat.bgActive, border: `1px solid ${cat.borderColor}`, color: cat.color }}
|
|
>
|
|
📥 다운로드
|
|
</button>
|
|
<button
|
|
onClick={handleSave}
|
|
className="px-3 py-1.5 text-[11px] font-semibold rounded border border-border bg-bg-2 text-text-1 hover:bg-bg-hover transition-all font-korean flex items-center gap-1.5"
|
|
>
|
|
💾 저장
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Preview Content */}
|
|
<div className="flex-1 overflow-y-auto px-6 py-5">
|
|
{/* Report Header */}
|
|
<div className="rounded-lg border border-border p-8 mb-6" style={{ background: 'var(--bg2)' }}>
|
|
<div className="text-center">
|
|
<p className="text-[10px] text-text-3 font-korean mb-2">해양환경 위기대응 통합지원시스템</p>
|
|
<h2 className="text-[20px] font-bold text-text-1 font-korean mb-2">{cat.reportName}</h2>
|
|
<p className="text-[12px] font-korean" style={{ color: cat.color }}>{cat.templates[selectedTemplate].label}</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Dynamic Sections */}
|
|
{activeSections.map(sec => (
|
|
<div key={sec.id} className="rounded-lg border border-border mb-4 overflow-hidden" style={{ background: 'var(--bg2)' }}>
|
|
<div className="px-5 py-3 border-b border-border">
|
|
<h4 className="text-[13px] font-bold text-text-1 font-korean flex items-center gap-2">
|
|
{sec.icon} {sec.title}
|
|
</h4>
|
|
</div>
|
|
<div className="p-5">
|
|
{/* ── 유출유 확산예측 섹션들 ── */}
|
|
{sec.id === 'oil-spread' && (
|
|
<>
|
|
<div className="w-full h-[140px] bg-bg-3 border border-border rounded-lg flex items-center justify-center text-text-3 text-[12px] font-korean mb-4">
|
|
[확산예측 지도 - 범위 조절 작업]
|
|
</div>
|
|
<div className="grid grid-cols-3 gap-3">
|
|
{[
|
|
{ label: 'KOSPS', value: sampleOilData.spread.kosps, color: '#06b6d4' },
|
|
{ label: 'OpenDrift', value: sampleOilData.spread.openDrift, color: '#ef4444' },
|
|
{ label: 'POSEIDON', value: sampleOilData.spread.poseidon, color: '#f97316' },
|
|
].map((m, i) => (
|
|
<div key={i} className="bg-bg-1 border border-border rounded-lg p-4 text-center">
|
|
<p className="text-[10px] text-text-3 font-korean mb-1">{m.label}</p>
|
|
<p className="text-[20px] font-bold font-mono" style={{ color: m.color }}>{m.value}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</>
|
|
)}
|
|
{sec.id === 'oil-pollution' && (
|
|
<table className="w-full table-fixed" style={{ borderCollapse: 'collapse' }}>
|
|
<colgroup><col style={{ width: '25%' }} /><col style={{ width: '25%' }} /><col style={{ width: '25%' }} /><col style={{ width: '25%' }} /></colgroup>
|
|
<tbody>
|
|
{[
|
|
['유출량', sampleOilData.pollution.spillAmount, '풍화량', sampleOilData.pollution.weathered],
|
|
['해상잔유량', sampleOilData.pollution.seaRemain, '오염해역면적', sampleOilData.pollution.pollutionArea],
|
|
['연안부착량', sampleOilData.pollution.coastAttach, '오염해안길이', sampleOilData.pollution.coastLength],
|
|
].map((row, i) => (
|
|
<tr key={i} className="border-b border-border">
|
|
<td className="px-4 py-3 text-[11px] text-text-3 font-korean bg-[rgba(255,255,255,0.02)]">{row[0]}</td>
|
|
<td className="px-4 py-3 text-[12px] text-text-1 font-mono font-semibold text-right">{row[1]}</td>
|
|
<td className="px-4 py-3 text-[11px] text-text-3 font-korean bg-[rgba(255,255,255,0.02)]">{row[2]}</td>
|
|
<td className="px-4 py-3 text-[12px] text-text-1 font-mono font-semibold text-right">{row[3]}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
)}
|
|
{sec.id === 'oil-sensitive' && (
|
|
<>
|
|
<p className="text-[11px] text-text-3 font-korean mb-3">반경 10 NM 기준</p>
|
|
<div className="flex flex-wrap gap-2">
|
|
{sampleOilData.sensitive.map((item, i) => (
|
|
<span key={i} className="px-3 py-1.5 text-[11px] font-semibold rounded-md bg-bg-3 border border-border text-text-2 font-korean">{item.label}</span>
|
|
))}
|
|
</div>
|
|
</>
|
|
)}
|
|
{sec.id === 'oil-coastal' && (
|
|
<p className="text-[12px] text-text-2 font-korean">
|
|
최초 부착시간: <span className="font-semibold text-text-1">{sampleOilData.coastal.firstTime}</span>
|
|
{' / '}
|
|
부착 해안길이: <span className="font-semibold text-text-1">{sampleOilData.coastal.coastLength}</span>
|
|
</p>
|
|
)}
|
|
{sec.id === 'oil-defense' && (
|
|
<div className="text-[12px] text-text-3 font-korean">
|
|
<p className="mb-2">방제자원 배치 계획에 따른 전략을 수립합니다.</p>
|
|
<div className="w-full h-[100px] bg-bg-3 border border-border rounded-lg flex items-center justify-center text-text-3 text-[12px] font-korean">
|
|
[방제자원 배치 지도]
|
|
</div>
|
|
</div>
|
|
)}
|
|
{sec.id === 'oil-tide' && (
|
|
<p className="text-[12px] text-text-2 font-korean">
|
|
고조: <span className="font-semibold text-text-1">{sampleOilData.tide.highTide1}</span>
|
|
{' / '}저조: <span className="font-semibold text-text-1">{sampleOilData.tide.lowTide}</span>
|
|
{' / '}고조: <span className="font-semibold text-text-1">{sampleOilData.tide.highTide2}</span>
|
|
</p>
|
|
)}
|
|
|
|
{/* ── HNS 대기확산 섹션들 ── */}
|
|
{sec.id === 'hns-atm' && (
|
|
<>
|
|
<div className="w-full h-[140px] bg-bg-3 border border-border rounded-lg flex items-center justify-center text-text-3 text-[12px] font-korean mb-4">
|
|
[대기확산 예측 지도]
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-3">
|
|
{[
|
|
{ label: 'ALOHA', value: sampleHnsData.atm.aloha, color: '#f97316' },
|
|
{ label: 'WRF-Chem', value: sampleHnsData.atm.wrfChem, color: '#22c55e' },
|
|
].map((m, i) => (
|
|
<div key={i} className="bg-bg-1 border border-border rounded-lg p-4 text-center">
|
|
<p className="text-[10px] text-text-3 font-korean mb-1">{m.label} 최대 확산거리</p>
|
|
<p className="text-[20px] font-bold font-mono" style={{ color: m.color }}>{m.value}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</>
|
|
)}
|
|
{sec.id === 'hns-hazard' && (
|
|
<div className="grid grid-cols-3 gap-3">
|
|
{[
|
|
{ label: 'ERPG-2 구역', value: sampleHnsData.hazard.erpg2, color: '#f97316', desc: '건강 영향' },
|
|
{ label: 'ERPG-3 구역', value: sampleHnsData.hazard.erpg3, color: '#ef4444', desc: '생명 위협' },
|
|
{ label: '대피 권고 범위', value: sampleHnsData.hazard.evacuation, color: '#a855f7', desc: '안전거리' },
|
|
].map((h, i) => (
|
|
<div key={i} className="bg-bg-1 border border-border rounded-lg p-4 text-center">
|
|
<p className="text-[9px] font-korean mb-1" style={{ color: h.color, fontWeight: 700 }}>{h.label}</p>
|
|
<p className="text-[18px] font-bold font-mono" style={{ color: h.color }}>{h.value}</p>
|
|
<p className="text-[8px] text-text-3 font-korean mt-1">{h.desc}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
{sec.id === 'hns-substance' && (
|
|
<div className="grid grid-cols-2 gap-2" style={{ fontSize: '11px' }}>
|
|
{[
|
|
{ k: '물질명', v: sampleHnsData.substance.name },
|
|
{ k: 'UN번호', v: sampleHnsData.substance.un },
|
|
{ k: 'CAS번호', v: sampleHnsData.substance.cas },
|
|
{ k: '위험등급', v: sampleHnsData.substance.class },
|
|
].map((r, i) => (
|
|
<div key={i} className="flex justify-between px-3 py-2 bg-bg-1 rounded border border-border">
|
|
<span className="text-text-3 font-korean">{r.k}</span>
|
|
<span className="text-text-1 font-semibold font-mono">{r.v}</span>
|
|
</div>
|
|
))}
|
|
<div className="col-span-2 flex justify-between px-3 py-2 bg-bg-1 rounded border border-border" style={{ borderColor: 'rgba(239,68,68,0.3)' }}>
|
|
<span className="text-text-3 font-korean">독성기준</span>
|
|
<span style={{ color: 'var(--red)', fontWeight: 600, fontFamily: 'var(--fM)', fontSize: '10px' }}>{sampleHnsData.substance.toxicity}</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
{sec.id === 'hns-ppe' && (
|
|
<div className="flex flex-wrap gap-2">
|
|
{sampleHnsData.ppe.map((item, i) => (
|
|
<span key={i} className="px-3 py-1.5 text-[11px] font-semibold rounded-md border text-text-2 font-korean" style={{ background: 'rgba(249,115,22,0.06)', borderColor: 'rgba(249,115,22,0.2)' }}>
|
|
🛡 {item}
|
|
</span>
|
|
))}
|
|
</div>
|
|
)}
|
|
{sec.id === 'hns-facility' && (
|
|
<div className="grid grid-cols-3 gap-3">
|
|
{[
|
|
{ label: '인근 학교', value: `${sampleHnsData.facility.schools}개소`, icon: '🏫' },
|
|
{ label: '의료시설', value: `${sampleHnsData.facility.hospitals}개소`, icon: '🏥' },
|
|
{ label: '주변 인구', value: sampleHnsData.facility.population, icon: '👥' },
|
|
].map((f, i) => (
|
|
<div key={i} className="bg-bg-1 border border-border rounded-lg p-4 text-center">
|
|
<div style={{ fontSize: '18px', marginBottom: '4px' }}>{f.icon}</div>
|
|
<p className="text-[14px] font-bold text-text-1 font-mono">{f.value}</p>
|
|
<p className="text-[9px] text-text-3 font-korean mt-1">{f.label}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
{sec.id === 'hns-3d' && (
|
|
<div className="w-full h-[160px] bg-bg-3 border border-border rounded-lg flex items-center justify-center text-text-3 text-[12px] font-korean">
|
|
[3D 농도 분포 시각화]
|
|
</div>
|
|
)}
|
|
{sec.id === 'hns-weather' && (
|
|
<div className="grid grid-cols-4 gap-3">
|
|
{[
|
|
{ label: '풍향', value: 'NE 42°', icon: '🌬' },
|
|
{ label: '풍속', value: '5.2 m/s', icon: '💨' },
|
|
{ label: '대기안정도', value: 'D (중립)', icon: '🌡' },
|
|
{ label: '기온', value: '8.5°C', icon: '☀️' },
|
|
].map((w, i) => (
|
|
<div key={i} className="bg-bg-1 border border-border rounded-lg p-3 text-center">
|
|
<div style={{ fontSize: '16px', marginBottom: '2px' }}>{w.icon}</div>
|
|
<p className="text-[13px] font-bold text-text-1 font-mono">{w.value}</p>
|
|
<p className="text-[8px] text-text-3 font-korean mt-1">{w.label}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* ── 긴급구난 섹션들 ── */}
|
|
{sec.id === 'rescue-safety' && (
|
|
<div className="grid grid-cols-4 gap-3">
|
|
{[
|
|
{ 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) => (
|
|
<div key={i} className="bg-bg-1 border border-border rounded-lg p-3 text-center">
|
|
<p className="text-[9px] text-text-3 font-korean mb-1">{s.label}</p>
|
|
<p className="text-[16px] font-bold font-mono" style={{ color: s.color }}>{s.value}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
{sec.id === 'rescue-timeline' && (
|
|
<div className="flex flex-col gap-2">
|
|
{[
|
|
{ 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) => (
|
|
<div key={i} className="flex items-center gap-3 px-3 py-2 bg-bg-1 rounded border border-border">
|
|
<span className="font-mono text-[11px] font-bold" style={{ color: e.color, minWidth: '40px' }}>{e.time}</span>
|
|
<span className="text-[11px] text-text-2 font-korean">{e.event}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
{sec.id === 'rescue-casualty' && (
|
|
<div className="grid grid-cols-4 gap-3">
|
|
{[
|
|
{ label: '총원', value: sampleRescueData.casualty.total, color: 'var(--t1)' },
|
|
{ label: '구조완료', value: sampleRescueData.casualty.rescued, color: '#22c55e' },
|
|
{ label: '실종', value: sampleRescueData.casualty.missing, color: '#ef4444' },
|
|
{ label: '부상', value: sampleRescueData.casualty.injured, color: '#f97316' },
|
|
].map((c, i) => (
|
|
<div key={i} className="bg-bg-1 border border-border rounded-lg p-4 text-center">
|
|
<p className="text-[9px] text-text-3 font-korean mb-1">{c.label}</p>
|
|
<p className="text-[24px] font-bold font-mono" style={{ color: c.color }}>{c.value}</p>
|
|
<p className="text-[8px] text-text-3 font-korean mt-0.5">명</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
{sec.id === 'rescue-resource' && (
|
|
<table className="w-full" style={{ borderCollapse: 'collapse', fontSize: '11px' }}>
|
|
<thead>
|
|
<tr className="border-b border-border">
|
|
<th className="px-3 py-2 text-left text-text-3 font-korean">유형</th>
|
|
<th className="px-3 py-2 text-left text-text-3 font-korean">선박/장비명</th>
|
|
<th className="px-3 py-2 text-center text-text-3 font-korean">도착예정</th>
|
|
<th className="px-3 py-2 text-center text-text-3 font-korean">상태</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{sampleRescueData.resources.map((r, i) => (
|
|
<tr key={i} className="border-b border-border">
|
|
<td className="px-3 py-2 text-text-2 font-korean">{r.type}</td>
|
|
<td className="px-3 py-2 text-text-1 font-mono font-semibold">{r.name}</td>
|
|
<td className="px-3 py-2 text-text-2 text-center font-mono">{r.eta}</td>
|
|
<td className="px-3 py-2 text-center">
|
|
<span className="px-2 py-0.5 rounded text-[10px] font-semibold font-korean" style={{
|
|
background: r.status === '투입중' ? 'rgba(34,197,94,0.15)' : r.status === '이동중' ? 'rgba(249,115,22,0.15)' : 'rgba(138,150,168,0.15)',
|
|
color: r.status === '투입중' ? '#22c55e' : r.status === '이동중' ? '#f97316' : '#8a96a8',
|
|
}}>
|
|
{r.status}
|
|
</span>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
)}
|
|
{sec.id === 'rescue-grounding' && (
|
|
<div className="grid grid-cols-3 gap-3">
|
|
{[
|
|
{ label: '좌초 위험도', value: sampleRescueData.grounding.risk, color: '#ef4444' },
|
|
{ label: '최근 천해', value: sampleRescueData.grounding.nearestShallow, color: '#f97316' },
|
|
{ label: '현재 수심', value: sampleRescueData.grounding.depth, color: '#06b6d4' },
|
|
].map((g, i) => (
|
|
<div key={i} className="bg-bg-1 border border-border rounded-lg p-4 text-center">
|
|
<p className="text-[9px] text-text-3 font-korean mb-1">{g.label}</p>
|
|
<p className="text-[14px] font-bold font-mono" style={{ color: g.color }}>{g.value}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
{sec.id === 'rescue-weather' && (
|
|
<div className="grid grid-cols-4 gap-3">
|
|
{[
|
|
{ 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) => (
|
|
<div key={i} className="bg-bg-1 border border-border rounded-lg p-3 text-center">
|
|
<div style={{ fontSize: '16px', marginBottom: '2px' }}>{w.icon}</div>
|
|
<p className="text-[13px] font-bold text-text-1 font-mono">{w.value}</p>
|
|
<p className="text-[8px] text-text-3 font-korean mt-1">{w.label}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
|
|
{activeSections.length === 0 && (
|
|
<div className="flex flex-col items-center justify-center py-20 text-text-3">
|
|
<div className="text-4xl mb-4">📋</div>
|
|
<p className="text-sm font-korean">왼쪽에서 보고서에 포함할 섹션을 선택하세요</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default ReportGenerator;
|