wing-ops/frontend/src/tabs/reports/components/TemplateFormEditor.tsx

402 lines
17 KiB
TypeScript

import { useState } from 'react';
import { createEmptyReport, type ReportType, type Jurisdiction } from './OilSpillReportTemplate';
import { useAuthStore } from '@common/store/authStore';
import { templateTypes } from './reportTypes';
import { generateReportHTML, exportAsPDF, exportAsHWP } from './reportUtils';
import { saveReport } from '../services/reportsApi';
interface TemplateFormEditorProps {
onSave: () => void;
onBack: () => void;
}
function TemplateFormEditor({ onSave, onBack }: TemplateFormEditorProps) {
const user = useAuthStore((s) => s.user);
const [selectedType, setSelectedType] = useState<ReportType>('초기보고서');
const [formData, setFormData] = useState<Record<string, string>>({});
const [reportMeta, setReportMeta] = useState(() => {
const now = new Date();
return {
title: '',
author: '',
jurisdiction: (user?.org?.abbr || '') as Jurisdiction,
writeTime: `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`,
};
});
const [autoSave, setAutoSave] = useState(false);
const [showPreview, setShowPreview] = useState(false);
const template = templateTypes.find((t) => t.id === selectedType)!;
const getVal = (key: string) => {
if (key === 'author') return reportMeta.author;
if (key === 'incident.writeTime') return reportMeta.writeTime;
return formData[key] || '';
};
const setVal = (key: string, val: string) => {
if (key === 'author') {
setReportMeta((p) => ({ ...p, author: val }));
return;
}
if (key === 'incident.writeTime') {
setReportMeta((p) => ({ ...p, writeTime: val }));
return;
}
setFormData((p) => ({ ...p, [key]: val }));
};
const handleSave = async () => {
const report = createEmptyReport(reportMeta.jurisdiction);
report.reportType = selectedType;
report.author = reportMeta.author;
report.title = formData['incident.name'] || `${selectedType} ${reportMeta.writeTime}`;
report.status = '완료';
report.incident.writeTime = reportMeta.writeTime;
// Map all incident fields
const incFields = [
'name',
'occurTime',
'location',
'shipName',
'accidentType',
'pollutant',
'spillAmount',
'lat',
'lon',
'depth',
'seabed',
] as const;
incFields.forEach((f) => {
const val = formData[`incident.${f}`];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (val) (report.incident as any)[f] = val;
});
report.analysis =
formData['spreadAnalysis'] ||
formData['initialResponse'] ||
formData['responseStatus'] ||
formData['responseDetail'] ||
'';
try {
await saveReport(report);
onSave();
} catch (err) {
console.error('[reports] 저장 오류:', err);
alert('보고서 저장 중 오류가 발생했습니다.');
}
};
const doExport = (format: 'pdf' | 'hwp') => {
const meta = {
writeTime: reportMeta.writeTime,
author: reportMeta.author,
jurisdiction: reportMeta.jurisdiction,
};
const filename =
formData['incident.name'] ||
`${template.label}_${reportMeta.writeTime.replace(/[\s:]/g, '_')}`;
if (format === 'pdf') {
const html = generateReportHTML(template.label, meta, template.sections, getVal);
exportAsPDF(html, filename);
} else {
exportAsHWP(template.label, meta, template.sections, getVal, filename);
}
};
return (
<div className="flex h-full">
{/* Left Sidebar - Template Selection */}
<div className="w-60 min-w-[240px] border-r border-stroke bg-bg-surface flex flex-col py-4 px-3 gap-2 overflow-y-auto shrink-0">
<div className="px-1 mb-2">
<h3 className="text-title-4 font-bold text-fg font-korean"> 릿 </h3>
<p className="text-caption text-fg-disabled font-korean mt-1">
릿 .
</p>
</div>
{templateTypes.map((t) => (
<button
key={t.id}
onClick={() => setSelectedType(t.id)}
className={`flex flex-col items-start p-3 rounded-lg border transition-all text-left ${
selectedType === t.id
? 'border-color-accent bg-[rgba(6,182,212,0.08)]'
: 'border-stroke bg-bg-elevated hover:border-stroke-light'
}`}
>
<span className="text-lg mb-1">{t.icon}</span>
<span
className={`text-label-1 font-bold font-korean ${selectedType === t.id ? 'text-color-accent' : 'text-fg'}`}
>
{t.label}
</span>
<span className="text-caption text-fg-disabled font-korean mt-0.5">{t.desc}</span>
</button>
))}
</div>
{/* Right - Form */}
<div className="flex-1 flex flex-col overflow-hidden">
{/* Form Header */}
<div className="flex items-center justify-between px-6 py-3 border-b border-stroke bg-bg-surface">
<div className="flex items-center gap-3">
<span className="text-lg">{template.icon}</span>
<span className="text-subtitle font-bold text-fg font-korean">{template.label}</span>
<span
className="px-2 py-0.5 text-caption font-semibold rounded font-korean"
style={{
background: 'color-mix(in srgb, var(--color-accent) 15%, transparent)',
color: 'var(--color-accent)',
}}
>
</span>
</div>
<div className="flex items-center gap-3">
<span className="text-label-2 text-fg-disabled font-korean">:</span>
<button
onClick={() => setAutoSave(!autoSave)}
className={`relative w-9 h-[18px] rounded-full transition-all ${autoSave ? 'bg-color-accent' : 'bg-bg-card border border-stroke'}`}
>
<span
className={`absolute top-[2px] w-3.5 h-3.5 rounded-full bg-white shadow transition-all ${autoSave ? 'left-[18px]' : 'left-[2px]'}`}
/>
</button>
<span
className="text-label-2 font-semibold font-korean"
style={{ color: autoSave ? 'var(--color-accent)' : 'var(--fg-disabled)' }}
>
{autoSave ? 'ON' : 'OFF'}
</span>
</div>
</div>
{/* Form Content */}
<div className="flex-1 overflow-y-auto px-6 py-5">
{template.sections.map((section, sIdx) => (
<div key={sIdx} className="mb-6 w-full">
<h4 className="text-title-4 font-bold font-korean mb-3 text-color-accent">
{section.title}
</h4>
<table className="w-full table-fixed border-collapse">
<colgroup>
<col style={{ width: '180px' }} />
<col />
</colgroup>
<tbody>
{section.fields.map((field, fIdx) => (
<tr key={fIdx} className="border-b border-stroke">
{field.label ? (
<>
<td className="px-4 py-3 text-label-2 font-semibold text-fg-disabled font-korean bg-[rgba(255,255,255,0.03)] align-middle">
{field.label}
</td>
<td className="px-4 py-2 align-middle">
{field.type === 'text' && (
<input
value={getVal(field.key)}
onChange={(e) => setVal(field.key, e.target.value)}
placeholder={`${field.label} 입력`}
className="w-full bg-transparent text-label-1 text-fg font-korean outline-none placeholder-fg-disabled"
/>
)}
{field.type === 'checkbox-group' && field.options && (
<div className="flex items-center gap-4">
{field.options.map((opt) => (
<label
key={opt}
className="flex items-center gap-1.5 text-label-2 text-fg-sub font-korean cursor-pointer"
>
<input
type="checkbox"
className="accent-[var(--color-accent)] w-3.5 h-3.5"
/>
{opt}
</label>
))}
</div>
)}
</td>
</>
) : (
<td colSpan={2} className="px-4 py-3">
<textarea
value={getVal(field.key)}
onChange={(e) => setVal(field.key, e.target.value)}
placeholder="내용을 입력하세요..."
className="w-full min-h-[120px] bg-bg-elevated border border-stroke rounded-md px-3 py-2 text-label-1 text-fg font-korean outline-none placeholder-fg-disabled resize-y focus:border-color-accent"
/>
</td>
)}
</tr>
))}
</tbody>
</table>
</div>
))}
</div>
{/* Bottom Action Bar */}
<div className="flex items-center justify-between px-6 py-3 border-t border-stroke bg-bg-surface">
<div className="flex items-center gap-2">
<button
onClick={() => doExport('pdf')}
className="px-3 py-2 text-label-2 font-semibold rounded bg-color-danger text-white hover:opacity-90 transition-all"
>
PDF
</button>
<button
onClick={() => doExport('hwp')}
className="px-3 py-2 text-label-2 font-semibold rounded bg-color-info text-white hover:opacity-90 transition-all"
>
HWPX
</button>
</div>
<div className="flex items-center gap-3">
<button
onClick={onBack}
className="px-4 py-2 text-label-2 font-semibold rounded text-fg-disabled hover:text-fg transition-all font-korean"
>
</button>
<button
onClick={() => setShowPreview(true)}
className="px-4 py-2 text-label-2 font-semibold rounded border border-color-accent text-color-accent hover:bg-[rgba(6,182,212,0.1)] transition-all font-korean"
>
</button>
<button
onClick={handleSave}
className="px-5 py-2 text-label-2 font-semibold rounded bg-color-accent text-bg-0 hover:shadow-[0_0_12px_rgba(6,182,212,0.3)] transition-all font-korean flex items-center gap-1"
>
</button>
</div>
</div>
</div>
{/* Preview Modal */}
{showPreview && (
<div
className="fixed inset-0 z-50 flex items-center justify-center"
style={{ background: 'rgba(0,0,0,0.6)' }}
>
<div
className="bg-bg-base border border-stroke rounded-xl shadow-2xl flex flex-col"
style={{ width: 'calc(100vw - 48px)', height: 'calc(100vh - 48px)' }}
>
{/* Modal Header */}
<div className="flex items-center justify-between px-6 py-4 border-b border-stroke">
<div className="flex items-center gap-3">
<span className="text-lg">{template.icon}</span>
<span className="text-subtitle font-bold text-fg font-korean">
{template.label}
</span>
<span
className="px-2 py-0.5 text-caption font-semibold rounded font-korean"
style={{
background: 'color-mix(in srgb, var(--color-accent) 15%, transparent)',
color: 'var(--color-accent)',
}}
>
</span>
</div>
<button
onClick={() => setShowPreview(false)}
className="w-8 h-8 rounded-lg flex items-center justify-center text-fg-disabled hover:text-fg hover:bg-bg-elevated transition-all text-lg"
>
</button>
</div>
{/* Modal Body - Report Preview */}
<div className="flex-1 overflow-y-auto px-6 py-5">
<div className="w-full">
{/* Report Title */}
<div className="text-center mb-8">
<h2 className="text-title-1 font-bold text-fg font-korean mb-1">
</h2>
<h3 className="text-subtitle font-semibold font-korean text-color-accent">
{formData['incident.name'] || template.label}
</h3>
<p className="text-label-2 text-fg-disabled font-korean mt-2">
: {reportMeta.writeTime} | : {reportMeta.author || '-'} | :{' '}
{reportMeta.jurisdiction}
</p>
</div>
{/* Sections */}
{template.sections.map((section, sIdx) => (
<div key={sIdx} className="mb-5">
<h4
className="text-title-4 font-bold font-korean mb-2 px-2 py-1.5 rounded"
style={{
color: 'var(--color-accent)',
background: 'color-mix(in srgb, var(--color-accent) 6%, transparent)',
}}
>
{section.title}
</h4>
<table className="w-full table-fixed border-collapse border border-stroke">
<colgroup>
<col style={{ width: '200px' }} />
<col />
</colgroup>
<tbody>
{section.fields.map((field, fIdx) => {
const val = getVal(field.key);
return field.label ? (
<tr key={fIdx} className="border-b border-stroke">
<td className="px-4 py-2.5 text-label-2 font-semibold text-fg-disabled font-korean bg-[rgba(255,255,255,0.03)] border-r border-stroke align-middle">
{field.label}
</td>
<td className="px-4 py-2.5 text-label-1 text-fg font-korean align-middle">
{val || <span className="text-fg-disabled">-</span>}
</td>
</tr>
) : (
<tr key={fIdx} className="border-b border-stroke">
<td
colSpan={2}
className="px-4 py-3 text-label-1 text-fg font-korean whitespace-pre-wrap"
>
{val || <span className="text-fg-disabled"> </span>}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
))}
</div>
</div>
{/* Modal Footer */}
<div className="flex items-center justify-end gap-3 px-6 py-3 border-t border-stroke">
<button
onClick={() => setShowPreview(false)}
className="px-4 py-2 text-label-2 font-semibold rounded text-fg-disabled hover:text-fg transition-all font-korean"
>
</button>
<button
onClick={() => {
setShowPreview(false);
handleSave();
}}
className="px-5 py-2 text-label-2 font-semibold rounded bg-color-accent text-bg-0 hover:shadow-[0_0_12px_rgba(6,182,212,0.3)] transition-all font-korean"
>
</button>
</div>
</div>
</div>
)}
</div>
);
}
export default TemplateFormEditor;