# Conflicts: # docs/RELEASE-NOTES.md # frontend/src/common/components/map/MapView.tsx # frontend/src/tabs/incidents/components/DischargeZonePanel.tsx # frontend/src/tabs/incidents/components/IncidentsView.tsx
177 lines
5.8 KiB
TypeScript
177 lines
5.8 KiB
TypeScript
import { useRef, useState } from 'react';
|
|
import { MapView } from '@common/components/map/MapView';
|
|
import type { OilReportPayload } from '@common/hooks/useSubMenu';
|
|
|
|
interface OilSpreadMapPanelProps {
|
|
mapData: OilReportPayload['mapData'];
|
|
capturedStep3: string | null;
|
|
capturedStep6: string | null;
|
|
onCaptureStep3: (dataUrl: string) => void;
|
|
onCaptureStep6: (dataUrl: string) => void;
|
|
onResetStep3: () => void;
|
|
onResetStep6: () => void;
|
|
}
|
|
|
|
interface MapSlotProps {
|
|
label: string;
|
|
step: number;
|
|
mapData: NonNullable<OilReportPayload['mapData']>;
|
|
captured: string | null;
|
|
onCapture: (dataUrl: string) => void;
|
|
onReset: () => void;
|
|
}
|
|
|
|
const MapSlot = ({ label, step, mapData, captured, onCapture, onReset }: MapSlotProps) => {
|
|
const captureRef = useRef<(() => Promise<string | null>) | null>(null);
|
|
const [isCapturing, setIsCapturing] = useState(false);
|
|
|
|
const handleCapture = async () => {
|
|
if (!captureRef.current) return;
|
|
setIsCapturing(true);
|
|
const dataUrl = await captureRef.current();
|
|
setIsCapturing(false);
|
|
if (dataUrl) onCapture(dataUrl);
|
|
};
|
|
|
|
return (
|
|
<div className="flex flex-col">
|
|
{/* 라벨 */}
|
|
<div className="flex items-center gap-1.5 mb-1.5">
|
|
<span
|
|
className="text-label-2 font-bold font-korean px-2 py-0.5 rounded"
|
|
style={{
|
|
background: 'color-mix(in srgb, var(--color-accent) 12%, transparent)',
|
|
color: 'var(--color-accent)',
|
|
border: '1px solid color-mix(in srgb, var(--color-accent) 25%, transparent)',
|
|
}}
|
|
>
|
|
{label}
|
|
</span>
|
|
</div>
|
|
|
|
{/* 지도 + 캡처 오버레이 */}
|
|
<div
|
|
className="relative rounded-lg border border-stroke overflow-hidden"
|
|
style={{ aspectRatio: '16/9' }}
|
|
>
|
|
<MapView
|
|
center={mapData.center}
|
|
zoom={mapData.zoom}
|
|
incidentCoord={{ lat: mapData.center[0], lon: mapData.center[1] }}
|
|
oilTrajectory={mapData.trajectory}
|
|
externalCurrentTime={step}
|
|
centerPoints={mapData.centerPoints}
|
|
showBeached={true}
|
|
showTimeLabel={true}
|
|
simulationStartTime={mapData.simulationStartTime || undefined}
|
|
mapCaptureRef={captureRef}
|
|
showOverlays={false}
|
|
/>
|
|
|
|
{captured && (
|
|
<div className="absolute top-2 right-2 z-10" style={{ width: '180px' }}>
|
|
<div
|
|
className="rounded-lg overflow-hidden"
|
|
style={{
|
|
border: '1px solid color-mix(in srgb, var(--color-accent) 50%, transparent)',
|
|
boxShadow: '0 4px 16px rgba(0,0,0,0.5)',
|
|
}}
|
|
>
|
|
<img src={captured} alt={`${label} 캡처`} className="w-full block" />
|
|
<div
|
|
className="flex items-center justify-between px-2 py-1"
|
|
style={{
|
|
background: 'rgba(15,23,42,0.85)',
|
|
borderTop: '1px solid color-mix(in srgb, var(--color-accent) 30%, transparent)',
|
|
}}
|
|
>
|
|
<span
|
|
className="text-caption font-korean font-semibold"
|
|
style={{ color: 'var(--color-accent)' }}
|
|
>
|
|
📷 캡처 완료
|
|
</span>
|
|
<button
|
|
onClick={onReset}
|
|
className="text-caption font-korean hover:text-fg transition-colors"
|
|
style={{ color: 'rgba(148,163,184,0.8)' }}
|
|
>
|
|
다시
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* 캡처 버튼 */}
|
|
<div className="flex items-center justify-between mt-1.5">
|
|
<p className="text-caption text-fg-disabled font-korean">
|
|
{captured ? 'PDF 출력 시 포함됩니다.' : '원하는 범위를 선택 후 캡처하세요.'}
|
|
</p>
|
|
<button
|
|
onClick={handleCapture}
|
|
disabled={isCapturing || !!captured}
|
|
className="px-2.5 py-1 text-label-2 font-semibold rounded transition-all font-korean flex items-center gap-1"
|
|
style={{
|
|
background: captured
|
|
? 'color-mix(in srgb, var(--color-accent) 6%, transparent)'
|
|
: 'color-mix(in srgb, var(--color-accent) 12%, transparent)',
|
|
border: '1px solid color-mix(in srgb, var(--color-accent) 40%, transparent)',
|
|
color: captured
|
|
? 'color-mix(in srgb, var(--color-accent) 50%, transparent)'
|
|
: 'var(--color-accent)',
|
|
opacity: isCapturing ? 0.6 : 1,
|
|
cursor: captured ? 'default' : 'pointer',
|
|
}}
|
|
>
|
|
{captured ? '✓ 캡처됨' : '📷 캡처'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const OilSpreadMapPanel = ({
|
|
mapData,
|
|
capturedStep3,
|
|
capturedStep6,
|
|
onCaptureStep3,
|
|
onCaptureStep6,
|
|
onResetStep3,
|
|
onResetStep6,
|
|
}: OilSpreadMapPanelProps) => {
|
|
if (!mapData) {
|
|
return (
|
|
<div className="w-full h-[200px] bg-bg-card border border-stroke rounded-lg flex items-center justify-center text-fg-disabled text-label-1 font-korean mb-4">
|
|
확산 예측 데이터가 없습니다. 예측 탭에서 시뮬레이션을 실행 후 보고서를 생성하세요.
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="mb-4">
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<MapSlot
|
|
label="3시간 후"
|
|
step={3}
|
|
mapData={mapData}
|
|
captured={capturedStep3}
|
|
onCapture={onCaptureStep3}
|
|
onReset={onResetStep3}
|
|
/>
|
|
<MapSlot
|
|
label="6시간 후"
|
|
step={6}
|
|
mapData={mapData}
|
|
captured={capturedStep6}
|
|
onCapture={onCaptureStep6}
|
|
onReset={onResetStep6}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default OilSpreadMapPanel;
|