기존 HTML Blob → .doc 저장 방식을 OWPML 표준 HWPX(ZIP+XML) 포맷으로 교체. JSZip으로 HWPX 파일을 순수 브라우저에서 생성하여 한글에서 직접 열 수 있도록 구현. - hwpxExport.ts 신규: HWPX ZIP 패키징 (mimetype, header.xml, section0.xml 등) - reportUtils.ts: exportAsHWP → dynamic import로 HWPX 위임 - ReportsView.tsx, TemplateFormEditor.tsx: 구조화 데이터 직접 전달 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
403 lines
24 KiB
TypeScript
Executable File
403 lines
24 KiB
TypeScript
Executable File
import { useState, useEffect, useCallback } from 'react'
|
|
import {
|
|
OilSpillReportTemplate,
|
|
type OilSpillReportData,
|
|
type Jurisdiction,
|
|
} from './OilSpillReportTemplate'
|
|
import { loadReportsFromApi, loadReportDetail, deleteReportApi } from '../services/reportsApi'
|
|
import { useSubMenu } from '@common/hooks/useSubMenu'
|
|
import { templateTypes } from './reportTypes'
|
|
import {
|
|
generateReportHTML,
|
|
exportAsPDF,
|
|
exportAsHWP,
|
|
typeColors,
|
|
statusColors,
|
|
analysisCatColors,
|
|
inferAnalysisCategory,
|
|
type ViewState,
|
|
} from './reportUtils';
|
|
import type { TemplateType } from './reportTypes';
|
|
import TemplateFormEditor from './TemplateFormEditor'
|
|
import ReportGenerator from './ReportGenerator'
|
|
|
|
// ─── Main ReportsView ────────────────────────────────────
|
|
export function ReportsView() {
|
|
const { activeSubTab, setActiveSubTab } = useSubMenu('reports')
|
|
const [view, setView] = useState<ViewState>({ screen: 'list' })
|
|
const [reports, setReports] = useState<OilSpillReportData[]>([])
|
|
const [searchTerm, setSearchTerm] = useState('')
|
|
const [filterJurisdiction, setFilterJurisdiction] = useState<string>('전체')
|
|
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set())
|
|
const [previewReport, setPreviewReport] = useState<OilSpillReportData | null>(null)
|
|
|
|
const refreshList = useCallback(async () => {
|
|
try {
|
|
const list = await loadReportsFromApi()
|
|
setReports(list)
|
|
} catch (err) {
|
|
console.error('[reports] 목록 조회 오류:', err)
|
|
}
|
|
}, [])
|
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
useEffect(() => { refreshList() }, [refreshList])
|
|
|
|
// SubMenuBar 탭과 내부 view 동기화
|
|
useEffect(() => {
|
|
if (activeSubTab === 'report-list') {
|
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
setView({ screen: 'list' })
|
|
refreshList()
|
|
} else if (activeSubTab === 'template') {
|
|
setView({ screen: 'templates' })
|
|
} else if (activeSubTab === 'generate') {
|
|
setView({ screen: 'generate' })
|
|
}
|
|
}, [activeSubTab, refreshList])
|
|
|
|
const handleSave = () => { refreshList(); setView({ screen: 'list' }); setActiveSubTab('report-list') }
|
|
|
|
const handleDelete = async (id: string) => {
|
|
if (!confirm('이 보고서를 삭제하시겠습니까?')) return
|
|
try {
|
|
await deleteReportApi(parseInt(id, 10))
|
|
refreshList()
|
|
setSelectedIds(prev => { const n = new Set(prev); n.delete(id); return n })
|
|
} catch (err) {
|
|
console.error('[reports] 삭제 오류:', err)
|
|
alert('보고서 삭제 중 오류가 발생했습니다.')
|
|
}
|
|
}
|
|
|
|
const formatDate = (iso: string) => {
|
|
try {
|
|
const d = new Date(iso)
|
|
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`
|
|
} catch { return iso }
|
|
}
|
|
|
|
const filteredReports = reports.filter(r => {
|
|
if (filterJurisdiction !== '전체' && r.jurisdiction !== filterJurisdiction) return false
|
|
if (searchTerm && !r.title.toLowerCase().includes(searchTerm.toLowerCase())) return false
|
|
return true
|
|
})
|
|
|
|
const toggleSelect = (id: string) => setSelectedIds(prev => { const n = new Set(prev); if (n.has(id)) { n.delete(id) } else { n.add(id) } return n })
|
|
const toggleAll = () => selectedIds.size === filteredReports.length ? setSelectedIds(new Set()) : setSelectedIds(new Set(filteredReports.map(r => r.id)))
|
|
|
|
const jurisdictions: Jurisdiction[] = ['남해청', '서해청', '중부청', '동해청', '제주청']
|
|
|
|
return (
|
|
<div className="flex flex-col h-full w-full bg-bg-0">
|
|
{/* Content */}
|
|
<div className="flex-1 overflow-hidden flex flex-col">
|
|
|
|
{/* ──── 보고서 목록 ──── */}
|
|
{view.screen === 'list' && (
|
|
<div className="flex flex-col h-full">
|
|
<div className="flex items-center justify-between px-5 py-3 border-b border-border bg-bg-1">
|
|
<div className="flex items-center gap-3">
|
|
<span className="text-[11px] text-text-3 font-korean">관할</span>
|
|
<select value={filterJurisdiction} onChange={e => setFilterJurisdiction(e.target.value)} className="px-3 py-1.5 text-[11px] bg-bg-2 border border-border rounded text-text-1 font-korean outline-none focus:border-primary-cyan">
|
|
<option value="전체">전체</option>
|
|
{jurisdictions.map(j => <option key={j} value={j}>{j}</option>)}
|
|
</select>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<input type="text" placeholder="보고서명 검색..." value={searchTerm} onChange={e => setSearchTerm(e.target.value)} className="w-52 px-3 py-1.5 text-[11px] bg-bg-2 border border-border rounded text-text-1 placeholder-text-3 font-korean outline-none focus:border-primary-cyan" />
|
|
<button className="px-3 py-1.5 text-[11px] font-semibold rounded bg-bg-3 border border-border text-text-2 hover:bg-bg-hover hover:text-text-1 transition-all font-korean">다운로드</button>
|
|
<button onClick={() => { setView({ screen: 'templates' }); setActiveSubTab('template') }} className="px-3 py-1.5 text-[11px] font-semibold rounded bg-primary-cyan text-bg-0 hover:shadow-[0_0_8px_rgba(6,182,212,0.3)] transition-all font-korean">+ 수정</button>
|
|
</div>
|
|
</div>
|
|
|
|
{filteredReports.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 mb-2">저장된 보고서가 없습니다</p>
|
|
<button onClick={() => { setView({ screen: 'templates' }); setActiveSubTab('template') }} className="px-4 py-2 text-xs font-semibold rounded-md border border-primary-cyan text-primary-cyan hover:bg-[rgba(6,182,212,0.1)] transition-all font-korean mt-4">템플릿에서 작성</button>
|
|
</div>
|
|
) : (
|
|
<div className="flex-1 overflow-auto">
|
|
<table className="w-full table-fixed border-collapse">
|
|
<colgroup>
|
|
<col style={{ width: '3%' }} />
|
|
<col style={{ width: '4%' }} />
|
|
<col />
|
|
<col style={{ width: '9%' }} />
|
|
<col style={{ width: '11%' }} />
|
|
<col style={{ width: '12%' }} />
|
|
<col style={{ width: '8%' }} />
|
|
<col style={{ width: '7%' }} />
|
|
<col style={{ width: '6%' }} />
|
|
<col style={{ width: '5%' }} />
|
|
<col style={{ width: '6%' }} />
|
|
<col style={{ width: '4%' }} />
|
|
</colgroup>
|
|
<thead>
|
|
<tr className="border-b border-border bg-bg-1 sticky top-0 z-10">
|
|
<th className="px-3 py-3 text-center"><input type="checkbox" checked={selectedIds.size === filteredReports.length && filteredReports.length > 0} onChange={toggleAll} className="accent-[#06b6d4] w-3.5 h-3.5" /></th>
|
|
<th className="px-3 py-3 text-[11px] font-semibold text-text-3 text-center font-korean">번호</th>
|
|
<th className="px-4 py-3 text-[11px] font-semibold text-text-3 text-left font-korean">보고서명</th>
|
|
<th className="px-3 py-3 text-[11px] font-semibold text-text-3 text-center font-korean">보고서 유형</th>
|
|
<th className="px-3 py-3 text-[11px] font-semibold text-text-3 text-center font-korean">분석 종류</th>
|
|
<th className="px-3 py-3 text-[11px] font-semibold text-text-3 text-center font-korean">생성일시</th>
|
|
<th className="px-3 py-3 text-[11px] font-semibold text-text-3 text-center font-korean">작성자</th>
|
|
<th className="px-3 py-3 text-[11px] font-semibold text-text-3 text-center font-korean">관할</th>
|
|
<th className="px-3 py-3 text-[11px] font-semibold text-text-3 text-center font-korean">상태</th>
|
|
<th className="px-3 py-3 text-[11px] font-semibold text-text-3 text-center font-korean">수정</th>
|
|
<th className="px-3 py-3 text-[11px] font-semibold text-text-3 text-center font-korean">다운로드</th>
|
|
<th className="px-3 py-3 text-[11px] font-semibold text-text-3 text-center font-korean">삭제</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{filteredReports.map((report, idx) => (
|
|
<tr key={report.id} className="border-b border-border hover:bg-[rgba(255,255,255,0.02)] transition-colors">
|
|
<td className="px-3 py-3 text-center"><input type="checkbox" checked={selectedIds.has(report.id)} onChange={() => toggleSelect(report.id)} className="accent-[#06b6d4] w-3.5 h-3.5" /></td>
|
|
<td className="px-3 py-3 text-[12px] text-text-3 text-center font-mono">{idx + 1}</td>
|
|
<td className="px-4 py-3 truncate"><button onClick={async () => { try { const detail = await loadReportDetail(parseInt(report.id, 10)); setPreviewReport(detail) } catch { setPreviewReport(report) } }} className="text-[12px] font-semibold text-primary-cyan hover:underline text-left font-korean truncate max-w-full block">{report.title || '제목 없음'}</button></td>
|
|
<td className="px-3 py-3 text-center"><span className="inline-block px-2.5 py-1 text-[10px] font-semibold rounded font-korean" style={{ background: typeColors[report.reportType]?.bg || 'rgba(138,150,168,0.15)', color: typeColors[report.reportType]?.text || '#8a96a8' }}>{report.reportType}</span></td>
|
|
{(() => {
|
|
const cat = inferAnalysisCategory(report)
|
|
const style = cat ? analysisCatColors[cat] : null
|
|
return (
|
|
<td className="px-3 py-3 text-center">
|
|
{style ? (
|
|
<div className="inline-flex items-center gap-1.5">
|
|
<div className="flex items-center justify-center w-5 h-5 rounded-full text-[11px]" style={{ background: style.bg, boxShadow: `0 0 6px ${style.text}25` }}>{style.icon}</div>
|
|
<span className="text-[9px] font-semibold font-korean" style={{ color: style.text }}>{cat}</span>
|
|
</div>
|
|
) : (
|
|
<span className="text-[10px] text-text-3 font-korean">-</span>
|
|
)}
|
|
</td>
|
|
)
|
|
})()}
|
|
<td className="px-3 py-3 text-[11px] text-text-2 text-center font-mono">{formatDate(report.createdAt)}</td>
|
|
<td className="px-3 py-3 text-[11px] text-text-2 text-center font-korean">{report.author || '-'}</td>
|
|
<td className="px-3 py-3 text-[11px] text-text-2 text-center font-korean">{report.jurisdiction}</td>
|
|
<td className="px-3 py-3 text-center"><span className="inline-block px-2.5 py-1 text-[10px] font-semibold rounded font-korean" style={{ background: statusColors[report.status]?.bg, color: statusColors[report.status]?.text }}>{report.status}</span></td>
|
|
<td className="px-3 py-3 text-center"><button onClick={async () => { try { const detail = await loadReportDetail(parseInt(report.id, 10)); setView({ screen: 'edit', data: detail }) } catch { setView({ screen: 'edit', data: { ...report } }) } }} className="text-[11px] text-primary-cyan hover:underline font-korean">수정</button></td>
|
|
<td className="px-3 py-3 text-center"><button className="inline-flex items-center gap-1 px-2 py-1 text-[10px] font-semibold rounded bg-[rgba(239,68,68,0.12)] text-[#ef4444] border border-[rgba(239,68,68,0.25)] hover:bg-[rgba(239,68,68,0.2)] transition-all">PDF</button></td>
|
|
<td className="px-3 py-3 text-center"><button onClick={() => handleDelete(report.id)} className="w-7 h-7 rounded flex items-center justify-center text-status-red hover:bg-[rgba(239,68,68,0.1)] transition-all text-sm">🗑</button></td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* ──── 템플릿 (좌: 선택 / 우: 폼) ──── */}
|
|
{view.screen === 'templates' && (
|
|
<TemplateFormEditor onSave={handleSave} onBack={() => { setView({ screen: 'list' }); setActiveSubTab('report-list'); refreshList() }} />
|
|
)}
|
|
|
|
{/* ──── 보고서 생성 ──── */}
|
|
{view.screen === 'generate' && (
|
|
<ReportGenerator onSave={handleSave} />
|
|
)}
|
|
|
|
{/* ──── 보기 ──── */}
|
|
{view.screen === 'view' && (
|
|
<div className="flex-1 overflow-auto px-4 py-4">
|
|
<OilSpillReportTemplate mode="view" initialData={view.data} onBack={() => { setView({ screen: 'list' }); setActiveSubTab('report-list'); refreshList() }} />
|
|
</div>
|
|
)}
|
|
|
|
{/* ──── 수정 ──── */}
|
|
{view.screen === 'edit' && (
|
|
<div className="flex-1 overflow-auto px-4 py-4">
|
|
<OilSpillReportTemplate mode="edit" initialData={view.data} onSave={handleSave} onBack={() => { setView({ screen: 'list' }); setActiveSubTab('report-list'); refreshList() }} />
|
|
</div>
|
|
)}
|
|
|
|
</div>
|
|
|
|
{/* ──── 보고서 미리보기 모달 ──── */}
|
|
{previewReport && (
|
|
<div className="fixed inset-0 z-[9999] flex items-center justify-center" style={{ background: 'rgba(0,0,0,0.65)', backdropFilter: 'blur(6px)' }}>
|
|
<div className="flex overflow-hidden bg-bg-2 border border-border" style={{ borderRadius: '14px', width: 'min(96vw, 1320px)', height: 'min(94vh, 860px)', boxShadow: '0 28px 72px rgba(0,0,0,0.6)' }}>
|
|
|
|
{/* ── 왼쪽: 메타 + 다운로드 ── */}
|
|
<div className="flex flex-col shrink-0 w-60 border-r border-border bg-bg-1">
|
|
{/* 상단 아이콘·제목 */}
|
|
<div className="px-[18px] pt-5 pb-4 border-b border-border">
|
|
<div className="text-center mb-2.5 text-[28px]">
|
|
{({ '초기보고서': '📋', '지휘부 보고': '📊', '예측보고서': '🔬', '종합보고서': '📑', '유출유 보고': '🛢️' } as Record<string, string>)[previewReport.reportType] || '📄'}
|
|
</div>
|
|
<div className="text-center font-korean text-[13px] font-bold leading-snug" style={{ wordBreak: 'keep-all' }}>
|
|
{previewReport.title || '제목 없음'}
|
|
</div>
|
|
<div className="text-center mt-2">
|
|
<span className="inline-block px-2.5 py-1 text-[10px] font-semibold rounded font-korean" style={{ background: typeColors[previewReport.reportType]?.bg || 'rgba(138,150,168,0.15)', color: typeColors[previewReport.reportType]?.text || '#8a96a8' }}>
|
|
{previewReport.reportType}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 메타 정보 */}
|
|
<div className="flex flex-col gap-2.5 font-korean text-[11px] px-[18px] py-3.5 border-b border-border">
|
|
<div className="flex flex-col gap-0.5">
|
|
<span className="text-text-3 text-[9px] uppercase tracking-wide">작성자</span>
|
|
<span className="font-semibold">{previewReport.author || '—'}</span>
|
|
</div>
|
|
<div className="flex flex-col gap-0.5">
|
|
<span className="text-text-3 text-[9px] uppercase tracking-wide">관할</span>
|
|
<span className="font-semibold">{previewReport.jurisdiction}</span>
|
|
</div>
|
|
<div className="flex flex-col gap-0.5">
|
|
<span className="text-text-3 text-[9px] uppercase tracking-wide">생성일시</span>
|
|
<span className="font-mono font-semibold">{formatDate(previewReport.createdAt)}</span>
|
|
</div>
|
|
<div className="flex flex-col gap-0.5">
|
|
<span className="text-text-3 text-[9px] uppercase tracking-wide">상태</span>
|
|
<b style={{ color: statusColors[previewReport.status]?.text || 'var(--t1)' }}>{previewReport.status}</b>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 수정 버튼 */}
|
|
<div className="px-[18px] py-3 border-b border-border">
|
|
<button
|
|
onClick={() => { setPreviewReport(null); setView({ screen: 'edit', data: { ...previewReport } }) }}
|
|
className="w-full font-korean text-[11px] font-semibold cursor-pointer rounded-md border border-[rgba(6,182,212,0.3)] bg-[rgba(6,182,212,0.08)] text-primary-cyan py-2"
|
|
>
|
|
✏ 수정 모드
|
|
</button>
|
|
</div>
|
|
|
|
{/* spacer */}
|
|
<div className="flex-1" />
|
|
|
|
{/* 하단 다운로드 버튼 */}
|
|
<div className="flex flex-col gap-2 px-4 py-3.5 border-t border-border">
|
|
<div className="text-center font-korean mb-0.5 text-[9px] text-text-3">문서 저장</div>
|
|
<button
|
|
onClick={() => {
|
|
const tpl = templateTypes.find(t => t.id === previewReport.reportType)
|
|
if (tpl) {
|
|
const getVal = (key: string) => {
|
|
if (key === 'author') return previewReport.author
|
|
if (key.startsWith('incident.')) {
|
|
const f = key.split('.')[1]
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
return (previewReport.incident as any)[f] || ''
|
|
}
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
return (previewReport as any)[key] || ''
|
|
}
|
|
const html = generateReportHTML(tpl.label, { writeTime: previewReport.incident.writeTime, author: previewReport.author, jurisdiction: previewReport.jurisdiction }, tpl.sections, getVal)
|
|
exportAsPDF(html, previewReport.title || tpl.label)
|
|
}
|
|
}}
|
|
className="w-full flex items-center justify-center gap-1.5 font-korean text-[12px] font-bold cursor-pointer rounded-md py-[11px]"
|
|
style={{ border: '1px solid rgba(239,68,68,0.4)', background: 'rgba(239,68,68,0.1)', color: 'var(--red)' }}
|
|
>
|
|
<span>📄</span> PDF 저장
|
|
</button>
|
|
<button
|
|
onClick={() => {
|
|
const tpl = templateTypes.find(t => t.id === previewReport.reportType) as TemplateType | undefined
|
|
if (tpl) {
|
|
const getVal = (key: string) => {
|
|
if (key === 'author') return previewReport.author
|
|
if (key.startsWith('incident.')) {
|
|
const f = key.split('.')[1]
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
return (previewReport.incident as any)[f] || ''
|
|
}
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
return (previewReport as any)[key] || ''
|
|
}
|
|
const meta = { writeTime: previewReport.incident.writeTime, author: previewReport.author, jurisdiction: previewReport.jurisdiction }
|
|
const filename = previewReport.title || tpl.label
|
|
exportAsHWP(tpl.label, meta, tpl.sections, getVal, filename)
|
|
}
|
|
}}
|
|
className="w-full flex items-center justify-center gap-1.5 font-korean text-[12px] font-bold cursor-pointer rounded-md py-[11px]"
|
|
style={{ border: '1px solid rgba(59,130,246,0.4)', background: 'rgba(59,130,246,0.1)', color: 'var(--blue)' }}
|
|
>
|
|
<span>📝</span> HWPX 저장
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* ── 오른쪽: 본문 뷰어 ── */}
|
|
<div className="flex-1 flex flex-col overflow-hidden">
|
|
{/* 헤더 */}
|
|
<div className="flex items-center justify-between shrink-0 px-5 py-3.5 border-b border-border">
|
|
<div className="flex items-center gap-2">
|
|
<span className="font-korean text-[9px] px-2 py-[3px] rounded bg-[rgba(6,182,212,0.1)] text-primary-cyan font-semibold">보기</span>
|
|
<span className="font-korean text-[12px] text-text-3">보고서 내용</span>
|
|
</div>
|
|
<span
|
|
onClick={() => setPreviewReport(null)}
|
|
className="text-[18px] cursor-pointer text-text-3 leading-none hover:text-text-1 transition-colors"
|
|
>
|
|
✕
|
|
</span>
|
|
</div>
|
|
|
|
{/* 본문 스크롤 영역 */}
|
|
<div className="flex-1 overflow-y-auto p-6" style={{ scrollbarWidth: 'thin' }}>
|
|
<div className="flex flex-col gap-4">
|
|
{/* 1. 사고개요 */}
|
|
<div>
|
|
<div className="font-korean text-[12px] font-bold text-primary-cyan border-b pb-1 mb-2" style={{ borderColor: 'rgba(6,182,212,0.15)' }}>
|
|
1. 사고개요
|
|
</div>
|
|
<div className="font-korean text-[12px] leading-[1.7] whitespace-pre-wrap mt-2">
|
|
{[
|
|
previewReport.incident.name && `사고명: ${previewReport.incident.name}`,
|
|
previewReport.incident.occurTime && `발생일시: ${previewReport.incident.occurTime}`,
|
|
previewReport.incident.location && `발생위치: ${previewReport.incident.location}`,
|
|
previewReport.incident.shipName && `사고선박: ${previewReport.incident.shipName}`,
|
|
previewReport.incident.accidentType && `사고유형: ${previewReport.incident.accidentType}`,
|
|
].filter(Boolean).join('\n') || '—'}
|
|
</div>
|
|
</div>
|
|
|
|
{/* 2. 유출현황 */}
|
|
<div>
|
|
<div className="font-korean text-[12px] font-bold text-primary-cyan border-b pb-1 mb-2" style={{ borderColor: 'rgba(6,182,212,0.15)' }}>
|
|
2. 유출현황
|
|
</div>
|
|
<div className="font-korean text-[12px] leading-[1.7] whitespace-pre-wrap mt-2">
|
|
{[
|
|
previewReport.incident.pollutant && `유출유종: ${previewReport.incident.pollutant}`,
|
|
previewReport.incident.spillAmount && `유출량: ${previewReport.incident.spillAmount}`,
|
|
previewReport.spread?.length > 0 && `확산예측: ${previewReport.spread.length}개 시점 데이터`,
|
|
].filter(Boolean).join('\n') || '—'}
|
|
</div>
|
|
</div>
|
|
|
|
{/* 3. 초동조치 / 대응현황 */}
|
|
<div>
|
|
<div className="font-korean text-[12px] font-bold text-primary-cyan border-b pb-1 mb-2" style={{ borderColor: 'rgba(6,182,212,0.15)' }}>
|
|
3. 초동조치 / 대응현황
|
|
</div>
|
|
<div className="font-korean text-[12px] leading-[1.7] whitespace-pre-wrap mt-2">
|
|
{previewReport.analysis || '—'}
|
|
</div>
|
|
</div>
|
|
|
|
{/* 4. 향후 계획 */}
|
|
<div>
|
|
<div className="font-korean text-[12px] font-bold text-primary-cyan border-b pb-1 mb-2" style={{ borderColor: 'rgba(6,182,212,0.15)' }}>
|
|
4. 향후 계획
|
|
</div>
|
|
<div className="font-korean text-[12px] leading-[1.7] whitespace-pre-wrap mt-2">
|
|
{previewReport.etcEquipment || '—'}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|