236 lines
14 KiB
TypeScript
236 lines
14 KiB
TypeScript
import { sanitizeHtml } from '@common/utils/sanitize';
|
|
import type { OilSpillReportData } from './OilSpillReportTemplate';
|
|
|
|
// ─── Report Export Helpers ──────────────────────────────
|
|
export function generateReportHTML(
|
|
templateLabel: string,
|
|
meta: { writeTime: string; author: string; jurisdiction: string },
|
|
sections: { title: string; fields: { key: string; label: string }[] }[],
|
|
getVal: (key: string) => string
|
|
) {
|
|
const rows = sections.map(section => {
|
|
const fieldRows = section.fields.map(f => {
|
|
if (f.label) {
|
|
return `<tr><td style="background:#f0f4f8;padding:8px 12px;border:1px solid #d1d5db;font-weight:600;width:200px;font-size:12px;">${f.label}</td><td style="padding:8px 12px;border:1px solid #d1d5db;font-size:12px;">${getVal(f.key) || '-'}</td></tr>`
|
|
}
|
|
return `<tr><td colspan="2" style="padding:8px 12px;border:1px solid #d1d5db;font-size:12px;white-space:pre-wrap;">${getVal(f.key) || '-'}</td></tr>`
|
|
}).join('')
|
|
return `<h3 style="color:#0891b2;font-size:14px;margin:20px 0 8px;">${section.title}</h3><table style="width:100%;border-collapse:collapse;">${fieldRows}</table>`
|
|
}).join('')
|
|
|
|
return `<!DOCTYPE html><html><head><meta charset="utf-8"><title>${templateLabel}</title>
|
|
<style>@page{size:A4;margin:20mm}body{font-family:'맑은 고딕','Malgun Gothic',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">${templateLabel}</h2>
|
|
<p style="font-size:11px;color:#666">작성일시: ${meta.writeTime} | 작성자: ${meta.author || '-'} | 관할: ${meta.jurisdiction}</p>
|
|
</div>${rows}</body></html>`
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
export function exportAsPDF(html: string, _filename: string) {
|
|
const sanitizedHtml = sanitizeHtml(html)
|
|
const blob = new Blob([sanitizedHtml], { type: 'text/html; charset=utf-8' })
|
|
const url = URL.createObjectURL(blob)
|
|
const win = window.open(url, '_blank')
|
|
if (win) {
|
|
win.addEventListener('afterprint', () => URL.revokeObjectURL(url))
|
|
setTimeout(() => win.print(), 500)
|
|
}
|
|
setTimeout(() => URL.revokeObjectURL(url), 30000)
|
|
}
|
|
|
|
export async function exportAsHWP(
|
|
templateLabel: string,
|
|
meta: { writeTime: string; author: string; jurisdiction: string },
|
|
sections: { title: string; fields: { key: string; label: string }[] }[],
|
|
getVal: (key: string) => string,
|
|
filename: string,
|
|
images?: { step3?: string; step6?: string; sensitiveMap?: string; sensitivityMap?: string },
|
|
) {
|
|
const { exportAsHWPX } = await import('./hwpxExport');
|
|
await exportAsHWPX(templateLabel, meta, sections, getVal, filename, images);
|
|
}
|
|
|
|
export type ViewState =
|
|
| { screen: 'list' }
|
|
| { screen: 'templates' }
|
|
| { screen: 'generate' }
|
|
| { screen: 'view'; data: OilSpillReportData }
|
|
| { screen: 'edit'; data: OilSpillReportData }
|
|
|
|
export const typeColors: Record<string, { bg: string; text: string }> = {
|
|
'초기보고서': { bg: 'rgba(6,182,212,0.15)', text: '#06b6d4' },
|
|
'지휘부 보고': { bg: 'rgba(168,85,247,0.15)', text: '#a855f7' },
|
|
'예측보고서': { bg: 'rgba(59,130,246,0.15)', text: '#3b82f6' },
|
|
'종합보고서': { bg: 'rgba(249,115,22,0.15)', text: '#f97316' },
|
|
'유출유 보고': { bg: 'rgba(234,179,8,0.15)', text: '#eab308' },
|
|
}
|
|
export const statusColors: Record<string, { bg: string; text: string }> = {
|
|
'완료': { bg: 'rgba(34,197,94,0.15)', text: '#22c55e' },
|
|
'수행중': { bg: 'rgba(249,115,22,0.15)', text: '#f97316' },
|
|
'테스트': { bg: 'rgba(138,150,168,0.15)', text: '#8a96a8' },
|
|
}
|
|
export const analysisCatColors: Record<string, { bg: string; text: string; icon: string }> = {
|
|
'유출유 확산예측': { bg: 'rgba(6,182,212,0.12)', text: '#06b6d4', icon: '🛢' },
|
|
'HNS 대기확산': { bg: 'rgba(249,115,22,0.12)', text: '#f97316', icon: '🧪' },
|
|
'긴급구난': { bg: 'rgba(239,68,68,0.12)', text: '#ef4444', icon: '🚨' },
|
|
}
|
|
|
|
export function inferAnalysisCategory(report: OilSpillReportData): string {
|
|
if (report.analysisCategory) return report.analysisCategory
|
|
const t = (report.title || '').toLowerCase()
|
|
const rt = report.reportType || ''
|
|
if (t.includes('hns') || t.includes('대기확산') || t.includes('화학') || t.includes('aloha')) return 'HNS 대기확산'
|
|
if (t.includes('구난') || t.includes('구조') || t.includes('긴급') || t.includes('salvage') || t.includes('rescue')) return '긴급구난'
|
|
if (t.includes('유출유') || t.includes('확산예측') || t.includes('민감자원') || t.includes('유출사고') || t.includes('오염') || t.includes('방제') || rt === '유출유 보고' || rt === '예측보고서') return '유출유 확산예측'
|
|
return ''
|
|
}
|
|
|
|
// ─── PDF/HWP 섹션 포맷 헬퍼 ─────────────────────────────────
|
|
const TH = 'padding:6px 8px;border:1px solid #d1d5db;background:#f0f4f8;font-weight:600;font-size:11px;'
|
|
const TD = 'padding:6px 8px;border:1px solid #d1d5db;font-size:11px;'
|
|
const TABLE = 'width:100%;border-collapse:collapse;'
|
|
|
|
function formatTideTable(tide: OilSpillReportData['tide']): string {
|
|
if (!tide?.length) return ''
|
|
const header = `<tr><th style="${TH}">날짜</th><th style="${TH}">조형</th><th style="${TH}">간조1</th><th style="${TH}">만조1</th><th style="${TH}">간조2</th><th style="${TH}">만조2</th></tr>`
|
|
const rows = tide.map(t =>
|
|
`<tr><td style="${TD}">${t.date}</td><td style="${TD}">${t.tideType}</td><td style="${TD}">${t.lowTide1}</td><td style="${TD}">${t.highTide1}</td><td style="${TD}">${t.lowTide2}</td><td style="${TD}">${t.highTide2}</td></tr>`
|
|
).join('')
|
|
return `<table style="${TABLE}">${header}${rows}</table>`
|
|
}
|
|
|
|
function formatWeatherTable(weather: OilSpillReportData['weather']): string {
|
|
if (!weather?.length) return ''
|
|
const header = `<tr><th style="${TH}">시각</th><th style="${TH}">풍향</th><th style="${TH}">풍속</th><th style="${TH}">유향</th><th style="${TH}">유속</th><th style="${TH}">파고</th></tr>`
|
|
const rows = weather.map(w =>
|
|
`<tr><td style="${TD}">${w.time}</td><td style="${TD}">${w.windDir}</td><td style="${TD}">${w.windSpeed}</td><td style="${TD}">${w.currentDir}</td><td style="${TD}">${w.currentSpeed}</td><td style="${TD}">${w.waveHeight}</td></tr>`
|
|
).join('')
|
|
return `<table style="${TABLE}">${header}${rows}</table>`
|
|
}
|
|
|
|
function formatSpreadTable(spread: OilSpillReportData['spread']): string {
|
|
if (!spread?.length) return ''
|
|
const header = `<tr><th style="${TH}">경과시간</th><th style="${TH}">풍화량</th><th style="${TH}">해상잔유량</th><th style="${TH}">연안부착량</th><th style="${TH}">면적</th></tr>`
|
|
const rows = spread.map(s =>
|
|
`<tr><td style="${TD}">${s.elapsed}</td><td style="${TD}">${s.weathered}</td><td style="${TD}">${s.seaRemain}</td><td style="${TD}">${s.coastAttach}</td><td style="${TD}">${s.area}</td></tr>`
|
|
).join('')
|
|
return `<table style="${TABLE}">${header}${rows}</table>`
|
|
}
|
|
|
|
function formatSensitiveTable(r: OilSpillReportData): string {
|
|
const parts: string[] = []
|
|
|
|
if (r.sensitiveMapImage) {
|
|
parts.push(
|
|
'<p style="font-size:11px;font-weight:600;margin:8px 0 4px;">민감자원 분포 지도</p>' +
|
|
`<img src="${r.sensitiveMapImage}" style="width:100%;max-height:300px;object-fit:contain;border:1px solid #ddd;border-radius:6px;display:block;margin-bottom:8px;" />`
|
|
)
|
|
}
|
|
|
|
if (r.aquaculture?.length) {
|
|
const h = `<tr><th style="${TH}">종류</th><th style="${TH}">면적</th><th style="${TH}">거리</th></tr>`
|
|
const rows = r.aquaculture.map(a => `<tr><td style="${TD}">${a.type}</td><td style="${TD}">${a.area}</td><td style="${TD}">${a.distance}</td></tr>`).join('')
|
|
parts.push(`<p style="font-size:11px;font-weight:600;margin:8px 0 4px;">양식업</p><table style="${TABLE}">${h}${rows}</table>`)
|
|
}
|
|
if (r.beaches?.length) {
|
|
const h = `<tr><th style="${TH}">해수욕장명</th><th style="${TH}">거리</th></tr>`
|
|
const rows = r.beaches.map(b => `<tr><td style="${TD}">${b.name}</td><td style="${TD}">${b.distance}</td></tr>`).join('')
|
|
parts.push(`<p style="font-size:11px;font-weight:600;margin:8px 0 4px;">해수욕장</p><table style="${TABLE}">${h}${rows}</table>`)
|
|
}
|
|
if (r.markets?.length) {
|
|
const h = `<tr><th style="${TH}">수산시장명</th><th style="${TH}">거리</th></tr>`
|
|
const rows = r.markets.map(m => `<tr><td style="${TD}">${m.name}</td><td style="${TD}">${m.distance}</td></tr>`).join('')
|
|
parts.push(`<p style="font-size:11px;font-weight:600;margin:8px 0 4px;">수산시장</p><table style="${TABLE}">${h}${rows}</table>`)
|
|
}
|
|
if (r.esi?.length) {
|
|
const h = `<tr><th style="${TH}">코드</th><th style="${TH}">유형</th><th style="${TH}">길이</th></tr>`
|
|
const rows = r.esi.map(e => `<tr><td style="${TD}">${e.code}</td><td style="${TD}">${e.type}</td><td style="${TD}">${e.length}</td></tr>`).join('')
|
|
parts.push(`<p style="font-size:11px;font-weight:600;margin:8px 0 4px;">ESI 해안선</p><table style="${TABLE}">${h}${rows}</table>`)
|
|
}
|
|
if (r.species?.length) {
|
|
const h = `<tr><th style="${TH}">분류</th><th style="${TH}">종명</th></tr>`
|
|
const rows = r.species.map(s => `<tr><td style="${TD}">${s.category}</td><td style="${TD}">${s.species}</td></tr>`).join('')
|
|
parts.push(`<p style="font-size:11px;font-weight:600;margin:8px 0 4px;">보호생물</p><table style="${TABLE}">${h}${rows}</table>`)
|
|
}
|
|
if (r.habitat?.length) {
|
|
const h = `<tr><th style="${TH}">유형</th><th style="${TH}">면적</th></tr>`
|
|
const rows = r.habitat.map(h2 => `<tr><td style="${TD}">${h2.type}</td><td style="${TD}">${h2.area}</td></tr>`).join('')
|
|
parts.push(`<p style="font-size:11px;font-weight:600;margin:8px 0 4px;">서식지</p><table style="${TABLE}">${h}${rows}</table>`)
|
|
}
|
|
if (r.sensitivityMapImage) {
|
|
parts.push(
|
|
'<p style="font-size:11px;font-weight:600;margin:8px 0 4px;">통합민감도 평가 지도</p>' +
|
|
`<img src="${r.sensitivityMapImage}" style="width:100%;max-height:300px;object-fit:contain;border:1px solid #ddd;border-radius:6px;display:block;margin-bottom:8px;" />`
|
|
)
|
|
}
|
|
|
|
if (r.sensitivity?.length) {
|
|
const h = `<tr><th style="${TH}">민감도</th><th style="${TH}">면적</th></tr>`
|
|
const rows = r.sensitivity.map(s => `<tr><td style="${TD}">${s.level}</td><td style="${TD}">${s.area}</td></tr>`).join('')
|
|
parts.push(`<p style="font-size:11px;font-weight:600;margin:8px 0 4px;">민감도 등급</p><table style="${TABLE}">${h}${rows}</table>`)
|
|
}
|
|
return parts.join('')
|
|
}
|
|
|
|
function formatVesselsTable(vessels: OilSpillReportData['vessels']): string {
|
|
if (!vessels?.length) return ''
|
|
const header = `<tr><th style="${TH}">선명</th><th style="${TH}">기관</th><th style="${TH}">거리</th><th style="${TH}">속력</th><th style="${TH}">톤수</th><th style="${TH}">회수장비</th><th style="${TH}">오일붐</th></tr>`
|
|
const rows = vessels.map(v =>
|
|
`<tr><td style="${TD}">${v.name}</td><td style="${TD}">${v.org}</td><td style="${TD}">${v.dist}</td><td style="${TD}">${v.speed}</td><td style="${TD}">${v.ton}</td><td style="${TD}">${v.collectorType} ${v.collectorCap}</td><td style="${TD}">${v.boomType} ${v.boomLength}</td></tr>`
|
|
).join('')
|
|
return `<table style="${TABLE}">${header}${rows}</table>`
|
|
}
|
|
|
|
function formatRecoveryTable(recovery: OilSpillReportData['recovery']): string {
|
|
if (!recovery?.length) return ''
|
|
const header = `<tr><th style="${TH}">선박명</th><th style="${TH}">회수 기간</th></tr>`
|
|
const rows = recovery.map(r =>
|
|
`<tr><td style="${TD}">${r.shipName}</td><td style="${TD}">${r.period}</td></tr>`
|
|
).join('')
|
|
return `<table style="${TABLE}">${header}${rows}</table>`
|
|
}
|
|
|
|
function formatResultTable(result: OilSpillReportData['result']): string {
|
|
if (!result) return ''
|
|
return `<table style="${TABLE}">
|
|
<tr><td style="${TH}">유출총량</td><td style="${TD}">${result.spillTotal}</td><td style="${TH}">풍화총량</td><td style="${TD}">${result.weatheredTotal}</td></tr>
|
|
<tr><td style="${TH}">회수총량</td><td style="${TD}">${result.recoveredTotal}</td><td style="${TH}">해상잔유량</td><td style="${TD}">${result.seaRemainTotal}</td></tr>
|
|
<tr><td style="${TH}">연안부착량</td><td style="${TD}" colspan="3">${result.coastAttachTotal}</td></tr>
|
|
</table>`
|
|
}
|
|
|
|
export function buildReportGetVal(report: OilSpillReportData) {
|
|
return (key: string): string => {
|
|
if (key === 'author') return report.author ?? ''
|
|
if (key.startsWith('incident.')) {
|
|
const f = key.split('.')[1]
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
return (report.incident as any)[f] || ''
|
|
}
|
|
if (key === '__tide') return formatTideTable(report.tide)
|
|
if (key === '__weather') return formatWeatherTable(report.weather)
|
|
if (key === '__spreadMaps') {
|
|
const img3 = report.step3MapImage
|
|
const img6 = report.step6MapImage
|
|
if (!img3 && !img6) return ''
|
|
const cell = (label: string, src: string) =>
|
|
`<div style="flex:1;min-width:0"><p style="font-size:11px;font-weight:bold;color:#0891b2;margin:0 0 4px;">${label}</p>` +
|
|
`<img src="${src}" style="width:100%;max-height:260px;object-fit:contain;border:1px solid #ddd;border-radius:6px;" /></div>`
|
|
return `<div style="display:flex;gap:12px;margin-bottom:8px;">` +
|
|
(img3 ? cell('3시간 후', img3) : '') +
|
|
(img6 ? cell('6시간 후', img6) : '') +
|
|
`</div>`
|
|
}
|
|
if (key === '__spread') return formatSpreadTable(report.spread)
|
|
if (key === '__sensitive') return formatSensitiveTable(report)
|
|
if (key === '__vessels') return formatVesselsTable(report.vessels)
|
|
if (key === '__recovery') return formatRecoveryTable(report.recovery)
|
|
if (key === '__result') return formatResultTable(report.result)
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
return (report as any)[key] || ''
|
|
}
|
|
}
|