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({ screen: 'list' }) const [reports, setReports] = useState([]) const [searchTerm, setSearchTerm] = useState('') const [filterJurisdiction, setFilterJurisdiction] = useState('전체') const [selectedIds, setSelectedIds] = useState>(new Set()) const [previewReport, setPreviewReport] = useState(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 (
{/* Content */}
{/* ──── 보고서 목록 ──── */} {view.screen === 'list' && (
관할
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" />
{filteredReports.length === 0 ? (
📄

저장된 보고서가 없습니다

) : (
{filteredReports.map((report, idx) => ( {(() => { const cat = inferAnalysisCategory(report) const style = cat ? analysisCatColors[cat] : null return ( ) })()} ))}
0} onChange={toggleAll} className="accent-[#06b6d4] w-3.5 h-3.5" /> 번호 보고서명 보고서 유형 분석 종류 생성일시 작성자 관할 상태 수정 다운로드 삭제
toggleSelect(report.id)} className="accent-[#06b6d4] w-3.5 h-3.5" /> {idx + 1} {report.reportType} {style ? (
{style.icon}
{cat}
) : ( - )}
{formatDate(report.createdAt)} {report.author || '-'} {report.jurisdiction} {report.status}
)}
)} {/* ──── 템플릿 (좌: 선택 / 우: 폼) ──── */} {view.screen === 'templates' && ( { setView({ screen: 'list' }); setActiveSubTab('report-list'); refreshList() }} /> )} {/* ──── 보고서 생성 ──── */} {view.screen === 'generate' && ( )} {/* ──── 보기 ──── */} {view.screen === 'view' && (
{ setView({ screen: 'list' }); setActiveSubTab('report-list'); refreshList() }} />
)} {/* ──── 수정 ──── */} {view.screen === 'edit' && (
{ setView({ screen: 'list' }); setActiveSubTab('report-list'); refreshList() }} />
)}
{/* ──── 보고서 미리보기 모달 ──── */} {previewReport && (
{/* ── 왼쪽: 메타 + 다운로드 ── */}
{/* 상단 아이콘·제목 */}
{({ '초기보고서': '📋', '지휘부 보고': '📊', '예측보고서': '🔬', '종합보고서': '📑', '유출유 보고': '🛢️' } as Record)[previewReport.reportType] || '📄'}
{previewReport.title || '제목 없음'}
{previewReport.reportType}
{/* 메타 정보 */}
작성자 {previewReport.author || '—'}
관할 {previewReport.jurisdiction}
생성일시 {formatDate(previewReport.createdAt)}
상태 {previewReport.status}
{/* 수정 버튼 */}
{/* spacer */}
{/* 하단 다운로드 버튼 */}
문서 저장
{/* ── 오른쪽: 본문 뷰어 ── */}
{/* 헤더 */}
보기 보고서 내용
setPreviewReport(null)} className="text-[18px] cursor-pointer text-text-3 leading-none hover:text-text-1 transition-colors" > ✕
{/* 본문 스크롤 영역 */}
{/* 1. 사고개요 */}
1. 사고개요
{[ 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') || '—'}
{/* 2. 유출현황 */}
2. 유출현황
{[ previewReport.incident.pollutant && `유출유종: ${previewReport.incident.pollutant}`, previewReport.incident.spillAmount && `유출량: ${previewReport.incident.spillAmount}`, previewReport.spread?.length > 0 && `확산예측: ${previewReport.spread.length}개 시점 데이터`, ].filter(Boolean).join('\n') || '—'}
{/* 3. 초동조치 / 대응현황 */}
3. 초동조치 / 대응현황
{previewReport.analysis || '—'}
{/* 4. 향후 계획 */}
4. 향후 계획
{previewReport.etcEquipment || '—'}
)}
) }