develop #58
@ -55,7 +55,7 @@ export function LoginPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-screen h-screen flex overflow-hidden relative" style={{ background: '#001028' }}>
|
||||
<div className="w-screen h-screen flex overflow-hidden relative bg-[#001028]">
|
||||
{/* Background image */}
|
||||
<div style={{
|
||||
position: 'absolute', inset: 0,
|
||||
@ -75,14 +75,14 @@ export function LoginPage() {
|
||||
}} />
|
||||
|
||||
{/* Center: Login Form */}
|
||||
<div className="w-full flex flex-col items-start justify-center relative z-[1]" style={{ padding: '40px 50px 40px 120px' }}>
|
||||
<div className="w-full flex flex-col items-start justify-center relative z-[1] px-[120px] py-[40px]" style={{ paddingLeft: 120, paddingRight: 50 }}>
|
||||
<div style={{ width: '100%', maxWidth: 360 }}>
|
||||
{/* Logo */}
|
||||
<div className="text-center" style={{ marginBottom: 36 }}>
|
||||
<div className="text-center mb-9">
|
||||
<img
|
||||
src="/wing_logo_text_white.svg"
|
||||
alt="WING 해양환경 위기대응 통합시스템"
|
||||
style={{ height: 28, margin: '0 auto', display: 'block' }}
|
||||
className="h-7 mx-auto block"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -97,12 +97,12 @@ export function LoginPage() {
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
{/* User ID */}
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<label className="block text-[10px] font-semibold text-text-3" style={{ marginBottom: 6, letterSpacing: '0.3px' }}>
|
||||
<div className="mb-4">
|
||||
<label className="block text-[10px] font-semibold text-text-3 mb-1.5" style={{ letterSpacing: '0.3px' }}>
|
||||
아이디
|
||||
</label>
|
||||
<div className="relative">
|
||||
<span className="absolute text-sm text-text-3" style={{ left: 12, top: '50%', transform: 'translateY(-50%)', pointerEvents: 'none' }}>
|
||||
<span className="absolute text-sm text-text-3 pointer-events-none" style={{ left: 12, top: '50%', transform: 'translateY(-50%)' }}>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
|
||||
</span>
|
||||
<input
|
||||
@ -130,12 +130,12 @@ export function LoginPage() {
|
||||
</div>
|
||||
|
||||
{/* Password */}
|
||||
<div style={{ marginBottom: 20 }}>
|
||||
<label className="block text-[10px] font-semibold text-text-3" style={{ marginBottom: 6, letterSpacing: '0.3px' }}>
|
||||
<div className="mb-5">
|
||||
<label className="block text-[10px] font-semibold text-text-3 mb-1.5" style={{ letterSpacing: '0.3px' }}>
|
||||
비밀번호
|
||||
</label>
|
||||
<div className="relative">
|
||||
<span className="absolute text-sm text-text-3" style={{ left: 12, top: '50%', transform: 'translateY(-50%)', pointerEvents: 'none' }}>
|
||||
<span className="absolute text-sm text-text-3 pointer-events-none" style={{ left: 12, top: '50%', transform: 'translateY(-50%)' }}>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
|
||||
</span>
|
||||
<input
|
||||
@ -162,18 +162,17 @@ export function LoginPage() {
|
||||
</div>
|
||||
|
||||
{/* Remember + Forgot */}
|
||||
<div className="flex items-center justify-between" style={{ marginBottom: 20 }}>
|
||||
<div className="flex items-center justify-between mb-5">
|
||||
<label className="flex items-center gap-1.5 text-[11px] text-text-3 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={remember}
|
||||
onChange={(e) => setRemember(e.target.checked)}
|
||||
style={{ accentColor: 'var(--cyan)' }}
|
||||
className="accent-[var(--cyan)]"
|
||||
/>
|
||||
아이디 저장
|
||||
</label>
|
||||
<button type="button" className="text-[11px] text-primary-cyan cursor-pointer"
|
||||
style={{ background: 'none', border: 'none', textDecoration: 'none' }}
|
||||
<button type="button" className="text-[11px] text-primary-cyan cursor-pointer bg-transparent border-none"
|
||||
onMouseEnter={(e) => e.currentTarget.style.textDecoration = 'underline'}
|
||||
onMouseLeave={(e) => e.currentTarget.style.textDecoration = 'none'}
|
||||
>
|
||||
@ -183,12 +182,12 @@ export function LoginPage() {
|
||||
|
||||
{/* Pending approval */}
|
||||
{pendingMessage && (
|
||||
<div className="flex items-start gap-2 text-[11px] rounded-sm" style={{
|
||||
padding: '10px 12px', marginBottom: 16,
|
||||
<div className="flex items-start gap-2 text-[11px] rounded-sm mb-4" style={{
|
||||
padding: '10px 12px',
|
||||
background: 'rgba(6,182,212,0.08)', border: '1px solid rgba(6,182,212,0.2)',
|
||||
color: '#67e8f9',
|
||||
}}>
|
||||
<span className="text-sm shrink-0" style={{ marginTop: 1 }}>
|
||||
<span className="text-sm shrink-0 mt-px">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
|
||||
</span>
|
||||
<span>{pendingMessage}</span>
|
||||
@ -197,8 +196,8 @@ export function LoginPage() {
|
||||
|
||||
{/* Error */}
|
||||
{error && (
|
||||
<div className="flex items-center gap-1.5 text-[11px] rounded-sm" style={{
|
||||
padding: '8px 12px', marginBottom: 16,
|
||||
<div className="flex items-center gap-1.5 text-[11px] rounded-sm mb-4" style={{
|
||||
padding: '8px 12px',
|
||||
background: 'rgba(239,68,68,0.08)', border: '1px solid rgba(239,68,68,0.2)',
|
||||
color: '#f87171',
|
||||
}}>
|
||||
@ -248,10 +247,10 @@ export function LoginPage() {
|
||||
</form>
|
||||
|
||||
{/* Divider */}
|
||||
<div className="flex items-center gap-3" style={{ margin: '24px 0' }}>
|
||||
<div className="flex-1 bg-border" style={{ height: 1 }} />
|
||||
<div className="flex items-center gap-3 my-6">
|
||||
<div className="flex-1 bg-border h-px" />
|
||||
<span className="text-[9px] text-text-3">또는</span>
|
||||
<div className="flex-1 bg-border" style={{ height: 1 }} />
|
||||
<div className="flex-1 bg-border h-px" />
|
||||
</div>
|
||||
|
||||
{/* Google / Certificate */}
|
||||
@ -268,11 +267,8 @@ export function LoginPage() {
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<button type="button" className="w-full rounded-md bg-bg-3 border border-border text-text-2 text-[11px] font-semibold cursor-pointer flex items-center justify-center gap-1.5"
|
||||
style={{
|
||||
padding: '10px',
|
||||
transition: 'background 0.15s',
|
||||
}}
|
||||
<button type="button" className="w-full rounded-md bg-bg-3 border border-border text-text-2 text-[11px] font-semibold cursor-pointer flex items-center justify-center gap-1.5 px-[10px] py-[10px]"
|
||||
style={{ transition: 'background 0.15s' }}
|
||||
onMouseEnter={(e) => e.currentTarget.style.background = 'var(--bgH)'}
|
||||
onMouseLeave={(e) => e.currentTarget.style.background = 'var(--bg3)'}
|
||||
>
|
||||
@ -283,20 +279,20 @@ export function LoginPage() {
|
||||
|
||||
{/* Demo accounts info (DEV only) */}
|
||||
{import.meta.env.DEV && (
|
||||
<div className="rounded-md" style={{
|
||||
marginTop: 24, padding: '10px 12px',
|
||||
<div className="rounded-md mt-6" style={{
|
||||
padding: '10px 12px',
|
||||
background: 'rgba(6,182,212,0.04)', border: '1px solid rgba(6,182,212,0.08)',
|
||||
}}>
|
||||
<div className="text-[9px] font-bold text-primary-cyan" style={{ marginBottom: 6 }}>
|
||||
<div className="text-[9px] font-bold text-primary-cyan mb-1.5">
|
||||
데모 계정
|
||||
</div>
|
||||
<div className="flex flex-col" style={{ gap: 3 }}>
|
||||
<div className="flex flex-col gap-[3px]">
|
||||
{DEMO_ACCOUNTS.map((acc) => (
|
||||
<div key={acc.id}
|
||||
onClick={() => { setUserId(acc.id); setPassword(acc.password); clearError() }}
|
||||
className="flex justify-between items-center cursor-pointer"
|
||||
className="flex justify-between items-center cursor-pointer rounded"
|
||||
style={{
|
||||
padding: '4px 6px', borderRadius: 4,
|
||||
padding: '4px 6px',
|
||||
transition: 'background 0.15s',
|
||||
}}
|
||||
onMouseEnter={(e) => e.currentTarget.style.background = 'rgba(6,182,212,0.06)'}
|
||||
@ -316,9 +312,9 @@ export function LoginPage() {
|
||||
</div>{/* end form card */}
|
||||
|
||||
{/* Footer */}
|
||||
<div className="text-center text-[9px] text-text-3" style={{ marginTop: 24, lineHeight: 1.6 }}>
|
||||
<div className="text-center text-[9px] text-text-3 mt-6" className="leading-[1.6]">
|
||||
<div>WING V2.0 | 해양경찰청 기동방제과 위기대응 통합시스템</div>
|
||||
<div style={{ marginTop: 2, color: 'rgba(134,144,166,0.6)' }}>
|
||||
<div className="mt-0.5" style={{ color: 'rgba(134,144,166,0.6)' }}>
|
||||
© 2026 Korea Coast Guard. All rights reserved.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -443,7 +443,7 @@ export function MapView({
|
||||
<span>{SENSITIVE_ICONS[d.type]}</span>
|
||||
<strong style={{ color: SENSITIVE_COLORS[d.type] }}>{d.name}</strong>
|
||||
</div>
|
||||
<div className="text-[10px]" style={{ color: '#666' }}>
|
||||
<div className="text-[10px] text-[#666]">
|
||||
반경: {d.radiusM}m<br />
|
||||
도달 예상: <strong style={{ color: d.arrivalTimeH <= 6 ? '#ef4444' : '#f97316' }}>{d.arrivalTimeH}h</strong>
|
||||
</div>
|
||||
@ -592,16 +592,16 @@ export function MapView({
|
||||
{/* 사고 위치 팝업 (클릭 시) */}
|
||||
{incidentCoord && !isNaN(incidentCoord.lat) && !isNaN(incidentCoord.lon) && !popupInfo && (
|
||||
<Popup longitude={incidentCoord.lon} latitude={incidentCoord.lat} anchor="bottom" offset={30} closeButton={false} closeOnClick={false}>
|
||||
<div className="text-sm" style={{ color: '#333' }}>
|
||||
<div className="text-sm text-[#333]">
|
||||
<strong>사고 지점</strong>
|
||||
<br />
|
||||
<span className="text-xs" style={{ color: '#666' }}>
|
||||
<span className="text-xs text-[#666]">
|
||||
{decimalToDMS(incidentCoord.lat, true)}
|
||||
<br />
|
||||
{decimalToDMS(incidentCoord.lon, false)}
|
||||
</span>
|
||||
<br />
|
||||
<span className="text-xs font-mono" style={{ color: '#888' }}>
|
||||
<span className="text-xs font-mono text-[#888]">
|
||||
({incidentCoord.lat.toFixed(4)}°, {incidentCoord.lon.toFixed(4)}°)
|
||||
</span>
|
||||
</div>
|
||||
@ -616,7 +616,7 @@ export function MapView({
|
||||
anchor="bottom"
|
||||
onClose={() => setPopupInfo(null)}
|
||||
>
|
||||
<div style={{ color: '#333' }}>{popupInfo.content}</div>
|
||||
<div className="text-[#333]">{popupInfo.content}</div>
|
||||
</Popup>
|
||||
)}
|
||||
|
||||
@ -672,7 +672,7 @@ function MapControls({ center, zoom }: { center: [number, number]; zoom: number
|
||||
const { current: map } = useMap()
|
||||
|
||||
return (
|
||||
<div className="absolute top-4 left-4" style={{ zIndex: 10 }}>
|
||||
<div className="absolute top-4 left-4 z-10">
|
||||
<div className="flex flex-col gap-2">
|
||||
<button
|
||||
onClick={() => map?.zoomIn()}
|
||||
@ -709,7 +709,7 @@ interface MapLegendProps {
|
||||
function MapLegend({ dispersionResult, incidentCoord, oilTrajectory = [], boomLines = [], selectedModels = new Set(['OpenDrift'] as PredictionModel[]) }: MapLegendProps) {
|
||||
if (dispersionResult && incidentCoord) {
|
||||
return (
|
||||
<div className="absolute top-4 right-4 bg-[rgba(18,25,41,0.95)] backdrop-blur-xl border border-border rounded-lg p-3.5 min-w-[200px]" style={{ zIndex: 20 }}>
|
||||
<div className="absolute top-4 right-4 bg-[rgba(18,25,41,0.95)] backdrop-blur-xl border border-border rounded-lg p-3.5 min-w-[200px] z-[20]">
|
||||
<div className="flex items-center gap-1.5 mb-2.5">
|
||||
<div className="text-base">📍</div>
|
||||
<div>
|
||||
@ -719,7 +719,7 @@ function MapLegend({ dispersionResult, incidentCoord, oilTrajectory = [], boomLi
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-[9px] text-text-2 mb-2" style={{ background: 'rgba(249,115,22,0.08)', padding: '8px', borderRadius: '6px' }}>
|
||||
<div className="text-[9px] text-text-2 mb-2 rounded" style={{ background: 'rgba(249,115,22,0.08)', padding: '8px' }}>
|
||||
<div className="flex justify-between mb-[3px]">
|
||||
<span className="text-text-3">물질</span>
|
||||
<span className="font-semibold text-status-orange">{dispersionResult.substance}</span>
|
||||
@ -750,7 +750,7 @@ function MapLegend({ dispersionResult, incidentCoord, oilTrajectory = [], boomLi
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 mt-2" style={{ padding: '6px', background: 'rgba(168,85,247,0.08)', borderRadius: '4px' }}>
|
||||
<div className="flex items-center gap-1.5 mt-2 rounded" style={{ padding: '6px', background: 'rgba(168,85,247,0.08)' }}>
|
||||
<div className="text-xs">🧭</div>
|
||||
<span className="text-[9px] text-text-3">풍향 (방사형)</span>
|
||||
</div>
|
||||
@ -760,7 +760,7 @@ function MapLegend({ dispersionResult, incidentCoord, oilTrajectory = [], boomLi
|
||||
|
||||
if (oilTrajectory.length > 0) {
|
||||
return (
|
||||
<div className="absolute top-4 right-4 bg-[rgba(18,25,41,0.9)] backdrop-blur-xl border border-border rounded-md p-3.5 min-w-[180px]" style={{ zIndex: 20 }}>
|
||||
<div className="absolute top-4 right-4 bg-[rgba(18,25,41,0.9)] backdrop-blur-xl border border-border rounded-md p-3.5 min-w-[180px] z-[20]">
|
||||
<h4 className="text-[11px] font-bold uppercase tracking-wider text-text-3 mb-2.5">범례</h4>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
{Array.from(selectedModels).map(model => (
|
||||
@ -770,7 +770,7 @@ function MapLegend({ dispersionResult, incidentCoord, oilTrajectory = [], boomLi
|
||||
</div>
|
||||
))}
|
||||
{selectedModels.size === 3 && (
|
||||
<div className="flex items-center gap-2 text-xs text-text-3" style={{ fontSize: '9px' }}>
|
||||
<div className="flex items-center gap-2 text-[9px] text-text-3">
|
||||
<span className="font-korean">(앙상블 모드)</span>
|
||||
</div>
|
||||
)}
|
||||
@ -783,15 +783,15 @@ function MapLegend({ dispersionResult, incidentCoord, oilTrajectory = [], boomLi
|
||||
<>
|
||||
<div className="h-px bg-border my-1" />
|
||||
<div className="flex items-center gap-2 text-xs text-text-2">
|
||||
<div style={{ width: '14px', height: '3px', background: '#ef4444', borderRadius: '1px' }} />
|
||||
<div className="w-[14px] h-[3px] bg-[#ef4444] rounded-[1px]" />
|
||||
<span className="font-korean">긴급 오일펜스</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-xs text-text-2">
|
||||
<div style={{ width: '14px', height: '3px', background: '#f97316', borderRadius: '1px' }} />
|
||||
<div className="w-[14px] h-[3px] bg-[#f97316] rounded-[1px]" />
|
||||
<span className="font-korean">중요 오일펜스</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-xs text-text-2">
|
||||
<div style={{ width: '14px', height: '3px', background: '#eab308', borderRadius: '1px' }} />
|
||||
<div className="w-[14px] h-[3px] bg-[#eab308] rounded-[1px]" />
|
||||
<span className="font-korean">보통 오일펜스</span>
|
||||
</div>
|
||||
</>
|
||||
@ -868,7 +868,7 @@ function TimelineControl({
|
||||
</div>
|
||||
<div className="tb" onClick={handleForward}>▶▶</div>
|
||||
<div className="tb" onClick={handleEnd}>⏭</div>
|
||||
<div style={{ width: '8px' }} />
|
||||
<div className="w-2" />
|
||||
<div className="tb" onClick={toggleSpeed}>{playbackSpeed}×</div>
|
||||
</div>
|
||||
<div className="tlt">
|
||||
@ -963,10 +963,10 @@ function BacktrackReplayBar({ replayFrame, totalFrames, ships }: { replayFrame:
|
||||
<div className="text-sm text-primary-purple font-mono font-bold">
|
||||
{progress.toFixed(0)}%
|
||||
</div>
|
||||
<div className="flex-1 h-1 bg-border relative" style={{ borderRadius: '2px' }}>
|
||||
<div className="flex-1 h-1 bg-border relative rounded-[2px]">
|
||||
<div
|
||||
className="h-full"
|
||||
style={{ width: `${progress}%`, background: 'linear-gradient(90deg, var(--purple), var(--cyan))', borderRadius: '2px', transition: 'width 0.05s' }}
|
||||
className="h-full rounded-[2px]"
|
||||
style={{ width: `${progress}%`, background: 'linear-gradient(90deg, var(--purple), var(--cyan))', transition: 'width 0.05s' }}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-1.5">
|
||||
|
||||
@ -60,7 +60,7 @@ export function CctvView() {
|
||||
return (
|
||||
<div className="flex h-full overflow-hidden" style={{ margin: '-20px -24px', height: 'calc(100% + 40px)' }}>
|
||||
{/* 왼쪽: 목록 패널 */}
|
||||
<div className="flex flex-col overflow-hidden bg-bg-1 border-r border-border" style={{ width: 290, minWidth: 290 }}>
|
||||
<div className="flex flex-col overflow-hidden bg-bg-1 border-r border-border w-[290px] min-w-[290px]">
|
||||
{/* 헤더 */}
|
||||
<div className="p-3 pb-2.5 border-b border-border shrink-0 bg-bg-2">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
@ -142,7 +142,7 @@ export function CctvView() {
|
||||
</div>
|
||||
|
||||
{/* 가운데: 영상 뷰어 */}
|
||||
<div className="flex-1 flex flex-col overflow-hidden min-w-0" style={{ background: '#04070f' }}>
|
||||
<div className="flex-1 flex flex-col overflow-hidden min-w-0 bg-[#04070f]">
|
||||
{/* 뷰어 툴바 */}
|
||||
<div className="flex items-center justify-between px-4 py-2 border-b border-border bg-bg-2 shrink-0 gap-2.5">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
@ -193,26 +193,25 @@ export function CctvView() {
|
||||
</div>
|
||||
|
||||
{/* 영상 그리드 */}
|
||||
<div className="flex-1 gap-0.5 p-0.5 overflow-hidden relative" style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: `repeat(${gridCols}, 1fr)`,
|
||||
gridTemplateRows: `repeat(${gridCols}, 1fr)`,
|
||||
background: '#000',
|
||||
}}>
|
||||
<div className="flex-1 gap-0.5 p-0.5 overflow-hidden relative grid bg-black"
|
||||
style={{
|
||||
gridTemplateColumns: `repeat(${gridCols}, 1fr)`,
|
||||
gridTemplateRows: `repeat(${gridCols}, 1fr)`,
|
||||
}}>
|
||||
{Array.from({ length: totalCells }).map((_, i) => {
|
||||
const cam = activeCells[i]
|
||||
return (
|
||||
<div key={i} className="relative flex items-center justify-center overflow-hidden" style={{ background: '#0a0e18', border: '1px solid rgba(255,255,255,.06)' }}>
|
||||
<div key={i} className="relative flex items-center justify-center overflow-hidden bg-[#0a0e18]" style={{ border: '1px solid rgba(255,255,255,.06)' }}>
|
||||
{cam ? (
|
||||
<>
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="text-4xl opacity-20">📹</div>
|
||||
</div>
|
||||
<div className="absolute top-2 left-2 flex items-center gap-1.5">
|
||||
<span className="text-[9px] font-bold px-1.5 py-0.5 rounded" style={{ background: 'rgba(0,0,0,.7)' }}>{cam.cameraNm}</span>
|
||||
<span className="text-[8px] font-bold px-1 py-0.5 rounded" style={{ background: 'rgba(239,68,68,.3)', color: '#f87171' }}>● REC</span>
|
||||
<span className="text-[9px] font-bold px-1.5 py-0.5 rounded bg-black/70">{cam.cameraNm}</span>
|
||||
<span className="text-[8px] font-bold px-1 py-0.5 rounded text-[#f87171]" style={{ background: 'rgba(239,68,68,.3)' }}>● REC</span>
|
||||
</div>
|
||||
<div className="absolute bottom-2 left-2 text-[9px] font-mono px-1.5 py-0.5 rounded" style={{ background: 'rgba(0,0,0,.7)', color: 'var(--t3)' }}>
|
||||
<div className="absolute bottom-2 left-2 text-[9px] font-mono px-1.5 py-0.5 rounded text-text-3 bg-black/70">
|
||||
{cam.coordDc ?? ''} · {cam.sourceNm ?? ''}
|
||||
</div>
|
||||
<div className="absolute inset-0 flex items-center justify-center text-[11px] font-korean text-text-3 opacity-60">
|
||||
@ -237,14 +236,14 @@ export function CctvView() {
|
||||
</div>
|
||||
|
||||
{/* 오른쪽: 미니맵 + 정보 */}
|
||||
<div className="flex flex-col overflow-hidden bg-bg-1 border-l border-border" style={{ width: 232, minWidth: 232 }}>
|
||||
<div className="flex flex-col overflow-hidden bg-bg-1 border-l border-border w-[232px] min-w-[232px]">
|
||||
{/* 지도 헤더 */}
|
||||
<div className="px-3 py-2 border-b border-border text-[11px] font-bold text-text-1 font-korean bg-bg-2 shrink-0 flex items-center justify-between">
|
||||
<span>🗺 위치 지도</span>
|
||||
<span className="text-[9px] text-text-3 font-normal">클릭하여 선택</span>
|
||||
</div>
|
||||
{/* 미니맵 (placeholder) */}
|
||||
<div className="w-full bg-bg-3 flex items-center justify-center shrink-0 relative" style={{ height: 210 }}>
|
||||
<div className="w-full bg-bg-3 flex items-center justify-center shrink-0 relative h-[210px]">
|
||||
<div className="text-[10px] text-text-3 font-korean opacity-50">지도 영역</div>
|
||||
{/* 간략 지도 표현 */}
|
||||
<div className="absolute inset-2 rounded-md border border-border/30 overflow-hidden" style={{ background: 'linear-gradient(180deg, rgba(6,182,212,.03), rgba(59,130,246,.05))' }}>
|
||||
|
||||
@ -466,7 +466,7 @@ export function RealtimeDrone() {
|
||||
zoom: 10,
|
||||
}}
|
||||
mapStyle={BASE_STYLE}
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
className="w-full h-full"
|
||||
attributionControl={false}
|
||||
>
|
||||
<DeckGLOverlay layers={deckLayers} />
|
||||
|
||||
@ -128,8 +128,8 @@ export function SatelliteRequest() {
|
||||
|
||||
// ── 섹션 헤더 헬퍼 (BlackSky 폼) ──
|
||||
const sectionHeader = (num: number, label: string) => (
|
||||
<div className="text-[11px] font-bold font-korean mb-2.5 flex items-center gap-1.5" style={{ color: '#818cf8' }}>
|
||||
<div className="w-[18px] h-[18px] rounded-[5px] flex items-center justify-center text-[9px] font-bold" style={{ background: 'rgba(99,102,241,.12)', color: '#818cf8' }}>{num}</div>
|
||||
<div className="text-[11px] font-bold font-korean mb-2.5 flex items-center gap-1.5 text-[#818cf8]">
|
||||
<div className="w-[18px] h-[18px] rounded-[5px] flex items-center justify-center text-[9px] font-bold text-[#818cf8]" style={{ background: 'rgba(99,102,241,.12)' }}>{num}</div>
|
||||
{label}
|
||||
</div>
|
||||
)
|
||||
@ -321,8 +321,8 @@ export function SatelliteRequest() {
|
||||
>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="w-[42px] h-[42px] rounded-[10px] flex items-center justify-center border" style={{ background: 'linear-gradient(135deg,#1a1a2e,#16213e)', borderColor: 'rgba(99,102,241,.3)' }}>
|
||||
<span className="text-[11px] font-extrabold font-mono" style={{ color: '#818cf8', letterSpacing: '-.5px' }}>B<span style={{ color: '#a78bfa' }}>Sky</span></span>
|
||||
<div className="w-[42px] h-[42px] rounded-[10px] flex items-center justify-center border border-[rgba(99,102,241,.3)]" style={{ background: 'linear-gradient(135deg,#1a1a2e,#16213e)' }}>
|
||||
<span className="text-[11px] font-extrabold font-mono text-[#818cf8] tracking-[-0.5px]">B<span className="text-[#a78bfa]">Sky</span></span>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm font-bold text-text-1 font-korean">BlackSky</div>
|
||||
@ -330,7 +330,7 @@ export function SatelliteRequest() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="px-2 py-0.5 rounded-[10px] text-[8px] font-semibold" style={{ background: 'rgba(34,197,94,.1)', border: '1px solid rgba(34,197,94,.2)', color: '#22c55e' }}>API 연결됨</span>
|
||||
<span className="px-2 py-0.5 rounded-[10px] text-[8px] font-semibold text-green-500" style={{ background: 'rgba(34,197,94,.1)', border: '1px solid rgba(34,197,94,.2)' }}>API 연결됨</span>
|
||||
<span className="text-base text-text-3">→</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -348,7 +348,7 @@ export function SatelliteRequest() {
|
||||
))}
|
||||
</div>
|
||||
<div className="text-[9px] text-text-3 font-korean leading-relaxed">고빈도 소형위성 군집 기반 긴급 촬영. 해양 사고 현장 신속 모니터링에 최적화. Dawn-to-Dusk 촬영 가능.</div>
|
||||
<div className="mt-2 text-[8px] text-text-3 font-mono">API: <span style={{ color: '#818cf8' }}>eapi.maxar.com/e1so/rapidoc</span></div>
|
||||
<div className="mt-2 text-[8px] text-text-3 font-mono">API: <span className="text-[#818cf8]">eapi.maxar.com/e1so/rapidoc</span></div>
|
||||
</div>
|
||||
|
||||
{/* UP42 (EO + SAR) */}
|
||||
@ -358,8 +358,8 @@ export function SatelliteRequest() {
|
||||
>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="w-[42px] h-[42px] rounded-[10px] flex items-center justify-center border" style={{ background: 'linear-gradient(135deg,#0a1628,#162a50)', borderColor: 'rgba(59,130,246,.3)' }}>
|
||||
<span className="text-[13px] font-extrabold font-mono" style={{ color: '#60a5fa', letterSpacing: '-.5px' }}>up<sup className="text-[7px] align-super">42</sup></span>
|
||||
<div className="w-[42px] h-[42px] rounded-[10px] flex items-center justify-center border border-[rgba(59,130,246,.3)]" style={{ background: 'linear-gradient(135deg,#0a1628,#162a50)' }}>
|
||||
<span className="text-[13px] font-extrabold font-mono text-[#60a5fa] tracking-[-0.5px]">up<sup className="text-[7px] align-super">42</sup></span>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm font-bold text-text-1 font-korean">UP42 — EO + SAR</div>
|
||||
@ -367,7 +367,7 @@ export function SatelliteRequest() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="px-2 py-0.5 rounded-[10px] text-[8px] font-semibold" style={{ background: 'rgba(34,197,94,.1)', border: '1px solid rgba(34,197,94,.2)', color: '#22c55e' }}>API 연결됨</span>
|
||||
<span className="px-2 py-0.5 rounded-[10px] text-[8px] font-semibold text-green-500" style={{ background: 'rgba(34,197,94,.1)', border: '1px solid rgba(34,197,94,.2)' }}>API 연결됨</span>
|
||||
<span className="text-base text-text-3">→</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -386,7 +386,7 @@ export function SatelliteRequest() {
|
||||
</div>
|
||||
<div className="flex gap-1.5 mb-2.5 flex-wrap">
|
||||
{['Pléiades Neo', 'SPOT 6/7'].map((t, i) => (
|
||||
<span key={i} className="px-1.5 py-px rounded text-[8px] font-korean" style={{ background: 'rgba(59,130,246,.08)', border: '1px solid rgba(59,130,246,.15)', color: '#60a5fa' }}>{t}</span>
|
||||
<span key={i} className="px-1.5 py-px rounded text-[8px] font-korean text-[#60a5fa]" style={{ background: 'rgba(59,130,246,.08)', border: '1px solid rgba(59,130,246,.15)' }}>{t}</span>
|
||||
))}
|
||||
{['TerraSAR-X', 'Capella SAR', 'ICEYE'].map((t, i) => (
|
||||
<span key={i} className="px-1.5 py-px rounded text-[8px] font-korean" style={{ background: 'rgba(6,182,212,.08)', border: '1px solid rgba(6,182,212,.15)', color: 'var(--cyan)' }}>{t}</span>
|
||||
@ -394,7 +394,7 @@ export function SatelliteRequest() {
|
||||
<span className="px-1.5 py-px rounded text-[8px] font-korean" style={{ background: 'rgba(139,148,158,.08)', border: '1px solid rgba(139,148,158,.15)', color: 'var(--t3)' }}>+11 more</span>
|
||||
</div>
|
||||
<div className="text-[9px] text-text-3 font-korean leading-relaxed">광학(EO) + 합성개구레이더(SAR) 통합 마켓. 야간·악천후 시 SAR 활용. 다중 위성 소스 자동 최적 선택.</div>
|
||||
<div className="mt-2 text-[8px] text-text-3 font-mono">API: <span style={{ color: '#60a5fa' }}>up42.com</span></div>
|
||||
<div className="mt-2 text-[8px] text-text-3 font-mono">API: <span className="text-[#60a5fa]">up42.com</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -408,22 +408,22 @@ export function SatelliteRequest() {
|
||||
|
||||
{/* ── BlackSky 긴급 촬영 요청 ── */}
|
||||
{modalPhase === 'blacksky' && (
|
||||
<div className="border rounded-[14px] w-[860px] max-h-[90vh] flex flex-col overflow-hidden" style={{ background: '#0d1117', borderColor: 'rgba(99,102,241,.3)', boxShadow: '0 24px 80px rgba(0,0,0,.7)' }}>
|
||||
<div className="border rounded-[14px] w-[860px] max-h-[90vh] flex flex-col overflow-hidden border-[rgba(99,102,241,.3)]" style={{ background: '#0d1117', boxShadow: '0 24px 80px rgba(0,0,0,.7)' }}>
|
||||
{/* 헤더 */}
|
||||
<div className="px-6 py-4 border-b flex items-center justify-between shrink-0 relative" style={{ borderColor: '#21262d' }}>
|
||||
<div className="px-6 py-4 border-b border-[#21262d] flex items-center justify-between shrink-0 relative">
|
||||
<div className="absolute top-0 left-0 right-0 h-0.5" style={{ background: 'linear-gradient(90deg,#6366f1,#818cf8,#a78bfa)' }} />
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-9 h-9 rounded-lg flex items-center justify-center border" style={{ background: 'linear-gradient(135deg,#1a1a2e,#16213e)', borderColor: 'rgba(99,102,241,.3)' }}>
|
||||
<span className="text-[10px] font-extrabold font-mono" style={{ color: '#818cf8' }}>B<span style={{ color: '#a78bfa' }}>Sky</span></span>
|
||||
<div className="w-9 h-9 rounded-lg flex items-center justify-center border border-[rgba(99,102,241,.3)]" style={{ background: 'linear-gradient(135deg,#1a1a2e,#16213e)' }}>
|
||||
<span className="text-[10px] font-extrabold font-mono text-[#818cf8]">B<span className="text-[#a78bfa]">Sky</span></span>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-[15px] font-bold font-korean" style={{ color: '#e2e8f0' }}>BlackSky — 긴급 위성 촬영 요청</div>
|
||||
<div className="text-[9px] font-korean mt-0.5" style={{ color: '#64748b' }}>Maxar E1SO RapiDoc API · 고빈도 긴급 태스킹</div>
|
||||
<div className="text-[15px] font-bold font-korean text-[#e2e8f0]">BlackSky — 긴급 위성 촬영 요청</div>
|
||||
<div className="text-[9px] font-korean mt-0.5 text-[#64748b]">Maxar E1SO RapiDoc API · 고빈도 긴급 태스킹</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="px-3 py-1 rounded-md text-[9px] font-semibold font-korean" style={{ background: 'rgba(99,102,241,.1)', border: '1px solid rgba(99,102,241,.25)', color: '#818cf8' }}>API Docs ↗</span>
|
||||
<button onClick={() => setModalPhase('none')} className="text-lg cursor-pointer p-1 bg-transparent border-none" style={{ color: '#64748b' }}>✕</button>
|
||||
<span className="px-3 py-1 rounded-md text-[9px] font-semibold font-korean text-[#818cf8]" style={{ background: 'rgba(99,102,241,.1)', border: '1px solid rgba(99,102,241,.25)' }}>API Docs ↗</span>
|
||||
<button onClick={() => setModalPhase('none')} className="text-lg cursor-pointer p-1 bg-transparent border-none text-[#64748b]">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -432,9 +432,9 @@ export function SatelliteRequest() {
|
||||
{/* API 상태 */}
|
||||
<div className="flex items-center gap-2.5 px-3.5 py-2.5 rounded-lg" style={{ background: 'rgba(34,197,94,.06)', border: '1px solid rgba(34,197,94,.15)' }}>
|
||||
<div className="w-2 h-2 rounded-full" style={{ background: '#22c55e', boxShadow: '0 0 6px rgba(34,197,94,.5)' }} />
|
||||
<span className="text-[10px] font-semibold font-korean" style={{ color: '#22c55e' }}>API Connected</span>
|
||||
<span className="text-[9px] font-mono" style={{ color: '#64748b' }}>eapi.maxar.com/e1so/rapidoc · Latency: 142ms</span>
|
||||
<span className="ml-auto text-[8px] font-mono" style={{ color: '#64748b' }}>Quota: 47/50 요청 잔여</span>
|
||||
<span className="text-[10px] font-semibold font-korean text-green-500">API Connected</span>
|
||||
<span className="text-[9px] font-mono text-[#64748b]">eapi.maxar.com/e1so/rapidoc · Latency: 142ms</span>
|
||||
<span className="ml-auto text-[8px] font-mono text-[#64748b]">Quota: 47/50 요청 잔여</span>
|
||||
</div>
|
||||
|
||||
{/* ① 태스킹 유형 */}
|
||||
@ -442,7 +442,7 @@ export function SatelliteRequest() {
|
||||
{sectionHeader(1, '태스킹 유형 · 우선순위')}
|
||||
<div className="grid grid-cols-3 gap-2.5">
|
||||
<div>
|
||||
<label className="block text-[9px] font-korean mb-1" style={{ color: '#64748b' }}>촬영 유형 <span style={{ color: '#f87171' }}>*</span></label>
|
||||
<label className="block text-[9px] font-korean mb-1 text-[#64748b]">촬영 유형 <span className="text-red-400">*</span></label>
|
||||
<select className={bsInput} style={bsInputStyle}>
|
||||
<option>긴급 태스킹 (Emergency)</option>
|
||||
<option>표준 태스킹 (Standard)</option>
|
||||
@ -450,7 +450,7 @@ export function SatelliteRequest() {
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-[9px] font-korean mb-1" style={{ color: '#64748b' }}>우선순위 <span style={{ color: '#f87171' }}>*</span></label>
|
||||
<label className="block text-[9px] font-korean mb-1 text-[#64748b]">우선순위 <span className="text-red-400">*</span></label>
|
||||
<select className={bsInput} style={bsInputStyle}>
|
||||
<option>P1 — 긴급 (90분 내)</option>
|
||||
<option>P2 — 높음 (6시간 내)</option>
|
||||
@ -458,7 +458,7 @@ export function SatelliteRequest() {
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-[9px] font-korean mb-1" style={{ color: '#64748b' }}>촬영 모드</label>
|
||||
<label className="block text-[9px] font-korean mb-1 text-[#64748b]">촬영 모드</label>
|
||||
<select className={bsInput} style={bsInputStyle}>
|
||||
<option>Single Collect</option>
|
||||
<option>Multi-pass Monitoring</option>
|
||||
@ -473,26 +473,26 @@ export function SatelliteRequest() {
|
||||
{sectionHeader(2, '관심 영역 (AOI)')}
|
||||
<div className="grid grid-cols-3 gap-2.5 items-end">
|
||||
<div>
|
||||
<label className="block text-[9px] font-korean mb-1" style={{ color: '#64748b' }}>중심 위도 <span style={{ color: '#f87171' }}>*</span></label>
|
||||
<label className="block text-[9px] font-korean mb-1 text-[#64748b]">중심 위도 <span className="text-red-400">*</span></label>
|
||||
<input type="text" defaultValue="34.5832" className={bsInput} style={bsInputStyle} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-[9px] font-korean mb-1" style={{ color: '#64748b' }}>중심 경도 <span style={{ color: '#f87171' }}>*</span></label>
|
||||
<label className="block text-[9px] font-korean mb-1 text-[#64748b]">중심 경도 <span className="text-red-400">*</span></label>
|
||||
<input type="text" defaultValue="128.4217" className={bsInput} style={bsInputStyle} />
|
||||
</div>
|
||||
<button className="px-3.5 py-2 rounded-md text-[10px] font-semibold cursor-pointer font-korean whitespace-nowrap" style={{ border: '1px solid rgba(99,102,241,.3)', background: 'rgba(99,102,241,.08)', color: '#818cf8' }}>📍 지도에서 AOI 그리기</button>
|
||||
<button className="px-3.5 py-2 rounded-md text-[10px] font-semibold cursor-pointer font-korean whitespace-nowrap text-[#818cf8]" style={{ border: '1px solid rgba(99,102,241,.3)', background: 'rgba(99,102,241,.08)' }}>📍 지도에서 AOI 그리기</button>
|
||||
</div>
|
||||
<div className="mt-2 grid grid-cols-3 gap-2.5">
|
||||
<div>
|
||||
<label className="block text-[9px] font-korean mb-1" style={{ color: '#64748b' }}>AOI 반경 (km)</label>
|
||||
<label className="block text-[9px] font-korean mb-1 text-[#64748b]">AOI 반경 (km)</label>
|
||||
<input type="number" defaultValue={10} step={1} min={1} className={bsInput} style={bsInputStyle} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-[9px] font-korean mb-1" style={{ color: '#64748b' }}>최대 구름량 (%)</label>
|
||||
<label className="block text-[9px] font-korean mb-1 text-[#64748b]">최대 구름량 (%)</label>
|
||||
<input type="number" defaultValue={20} step={5} min={0} max={100} className={bsInput} style={bsInputStyle} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-[9px] font-korean mb-1" style={{ color: '#64748b' }}>최대 Off-nadir (°)</label>
|
||||
<label className="block text-[9px] font-korean mb-1 text-[#64748b]">최대 Off-nadir (°)</label>
|
||||
<input type="number" defaultValue={25} step={5} min={0} max={45} className={bsInput} style={bsInputStyle} />
|
||||
</div>
|
||||
</div>
|
||||
@ -503,15 +503,15 @@ export function SatelliteRequest() {
|
||||
{sectionHeader(3, '촬영 기간 · 반복')}
|
||||
<div className="grid grid-cols-3 gap-2.5">
|
||||
<div>
|
||||
<label className="block text-[9px] font-korean mb-1" style={{ color: '#64748b' }}>촬영 시작 <span style={{ color: '#f87171' }}>*</span></label>
|
||||
<label className="block text-[9px] font-korean mb-1 text-[#64748b]">촬영 시작 <span className="text-red-400">*</span></label>
|
||||
<input type="datetime-local" defaultValue="2026-02-26T08:00" className={bsInput} style={bsInputStyle} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-[9px] font-korean mb-1" style={{ color: '#64748b' }}>촬영 종료 <span style={{ color: '#f87171' }}>*</span></label>
|
||||
<label className="block text-[9px] font-korean mb-1 text-[#64748b]">촬영 종료 <span className="text-red-400">*</span></label>
|
||||
<input type="datetime-local" defaultValue="2026-02-27T20:00" className={bsInput} style={bsInputStyle} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-[9px] font-korean mb-1" style={{ color: '#64748b' }}>반복 촬영</label>
|
||||
<label className="block text-[9px] font-korean mb-1 text-[#64748b]">반복 촬영</label>
|
||||
<select className={bsInput} style={bsInputStyle}>
|
||||
<option>1회 (단일)</option>
|
||||
<option>매 패스 (가용 시)</option>
|
||||
@ -528,7 +528,7 @@ export function SatelliteRequest() {
|
||||
{sectionHeader(4, '산출물 설정')}
|
||||
<div className="grid grid-cols-2 gap-2.5">
|
||||
<div>
|
||||
<label className="block text-[9px] font-korean mb-1" style={{ color: '#64748b' }}>산출물 형식 <span style={{ color: '#f87171' }}>*</span></label>
|
||||
<label className="block text-[9px] font-korean mb-1 text-[#64748b]">산출물 형식 <span className="text-red-400">*</span></label>
|
||||
<select className={bsInput} style={bsInputStyle}>
|
||||
<option>Ortho-Rectified (정사보정)</option>
|
||||
<option>Pan-sharpened (팬샤프닝)</option>
|
||||
@ -536,7 +536,7 @@ export function SatelliteRequest() {
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-[9px] font-korean mb-1" style={{ color: '#64748b' }}>파일 포맷</label>
|
||||
<label className="block text-[9px] font-korean mb-1 text-[#64748b]">파일 포맷</label>
|
||||
<select className={bsInput} style={bsInputStyle}>
|
||||
<option>GeoTIFF</option>
|
||||
<option>NITF</option>
|
||||
@ -551,7 +551,7 @@ export function SatelliteRequest() {
|
||||
{ label: '변화탐지 (Change Detection)', checked: false },
|
||||
{ label: '웹훅 알림', checked: false },
|
||||
].map((opt, i) => (
|
||||
<label key={i} className="flex items-center gap-1 text-[9px] cursor-pointer font-korean" style={{ color: '#94a3b8' }}>
|
||||
<label key={i} className="flex items-center gap-1 text-[9px] cursor-pointer font-korean text-[#94a3b8]">
|
||||
<input type="checkbox" defaultChecked={opt.checked} style={{ accentColor: '#818cf8', transform: 'scale(.85)' }} /> {opt.label}
|
||||
</label>
|
||||
))}
|
||||
@ -563,7 +563,7 @@ export function SatelliteRequest() {
|
||||
{sectionHeader(5, '연계 사고 · 비고')}
|
||||
<div className="grid grid-cols-2 gap-2.5 mb-2">
|
||||
<div>
|
||||
<label className="block text-[9px] font-korean mb-1" style={{ color: '#64748b' }}>연계 사고번호</label>
|
||||
<label className="block text-[9px] font-korean mb-1 text-[#64748b]">연계 사고번호</label>
|
||||
<select className={bsInput} style={bsInputStyle}>
|
||||
<option>OIL-2024-0892 · M/V STELLAR DAISY</option>
|
||||
<option>HNS-2024-041 · 울산 온산항 톨루엔 유출</option>
|
||||
@ -572,24 +572,23 @@ export function SatelliteRequest() {
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-[9px] font-korean mb-1" style={{ color: '#64748b' }}>요청자</label>
|
||||
<label className="block text-[9px] font-korean mb-1 text-[#64748b]">요청자</label>
|
||||
<input type="text" placeholder="소속 / 이름" className={bsInput} style={bsInputStyle} />
|
||||
</div>
|
||||
</div>
|
||||
<textarea
|
||||
placeholder="촬영 요청 목적, 특이사항, 관심 대상 등을 기록합니다..."
|
||||
className="w-full h-[50px] px-3 py-2.5 rounded-md text-[10px] font-korean outline-none resize-y leading-relaxed box-border"
|
||||
style={{ border: '1px solid #21262d', background: '#161b22', color: '#e2e8f0' }}
|
||||
className="w-full h-[50px] px-3 py-2.5 rounded-md text-[10px] font-korean outline-none resize-y leading-relaxed box-border border border-[#21262d] text-[#e2e8f0] bg-[#161b22]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 하단 버튼 */}
|
||||
<div className="px-6 py-3.5 border-t flex items-center gap-2 shrink-0" style={{ borderColor: '#21262d' }}>
|
||||
<div className="flex-1 text-[9px] font-korean leading-relaxed" style={{ color: '#64748b' }}>
|
||||
<span style={{ color: '#f87171' }}>*</span> 필수 항목 · 긴급 태스킹은 P1 우선순위로 90분 내 최초 영상 수신
|
||||
<div className="px-6 py-3.5 border-t border-[#21262d] flex items-center gap-2 shrink-0">
|
||||
<div className="flex-1 text-[9px] font-korean leading-relaxed text-[#64748b]">
|
||||
<span className="text-red-400">*</span> 필수 항목 · 긴급 태스킹은 P1 우선순위로 90분 내 최초 영상 수신
|
||||
</div>
|
||||
<button onClick={() => setModalPhase('provider')} className="px-5 py-2.5 rounded-lg border text-xs font-semibold cursor-pointer font-korean" style={{ borderColor: '#21262d', background: '#161b22', color: '#94a3b8' }}>← 뒤로</button>
|
||||
<button onClick={() => setModalPhase('provider')} className="px-5 py-2.5 rounded-lg border border-[#21262d] text-xs font-semibold cursor-pointer font-korean text-[#94a3b8] bg-[#161b22]">← 뒤로</button>
|
||||
<button onClick={() => setModalPhase('none')} className="px-7 py-2.5 rounded-lg border-none text-xs font-bold cursor-pointer font-korean text-white" style={{ background: 'linear-gradient(135deg,#6366f1,#818cf8)', boxShadow: '0 4px 16px rgba(99,102,241,.35)' }}>🛰 BlackSky 촬영 요청 제출</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -597,31 +596,31 @@ export function SatelliteRequest() {
|
||||
|
||||
{/* ── UP42 카탈로그 주문 ── */}
|
||||
{modalPhase === 'up42' && (
|
||||
<div className="border rounded-[14px] w-[920px] max-h-[90vh] flex flex-col overflow-hidden" style={{ background: '#0d1117', borderColor: 'rgba(59,130,246,.3)', boxShadow: '0 24px 80px rgba(0,0,0,.7)' }}>
|
||||
<div className="border rounded-[14px] w-[920px] max-h-[90vh] flex flex-col overflow-hidden border-[rgba(59,130,246,.3)]" style={{ background: '#0d1117', boxShadow: '0 24px 80px rgba(0,0,0,.7)' }}>
|
||||
{/* 헤더 */}
|
||||
<div className="px-6 py-4 border-b flex items-center justify-between shrink-0 relative" style={{ borderColor: '#21262d' }}>
|
||||
<div className="px-6 py-4 border-b border-[#21262d] flex items-center justify-between shrink-0 relative">
|
||||
<div className="absolute top-0 left-0 right-0 h-0.5" style={{ background: 'linear-gradient(90deg,#3b82f6,#06b6d4,#22c55e)' }} />
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-9 h-9 rounded-lg flex items-center justify-center border" style={{ background: 'linear-gradient(135deg,#0a1628,#162a50)', borderColor: 'rgba(59,130,246,.3)' }}>
|
||||
<span className="text-[13px] font-extrabold font-mono" style={{ color: '#60a5fa', letterSpacing: '-.5px' }}>up<sup className="text-[7px] align-super">42</sup></span>
|
||||
<div className="w-9 h-9 rounded-lg flex items-center justify-center border border-[rgba(59,130,246,.3)]" style={{ background: 'linear-gradient(135deg,#0a1628,#162a50)' }}>
|
||||
<span className="text-[13px] font-extrabold font-mono text-[#60a5fa] tracking-[-0.5px]">up<sup className="text-[7px] align-super">42</sup></span>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-[15px] font-bold font-korean" style={{ color: '#e2e8f0' }}>위성 촬영 요청 — 새 태스킹 주문</div>
|
||||
<div className="text-[9px] font-korean mt-0.5" style={{ color: '#64748b' }}>관심 지역(AOI)을 그리고 위성 패스를 선택하세요</div>
|
||||
<div className="text-[15px] font-bold font-korean text-[#e2e8f0]">위성 촬영 요청 — 새 태스킹 주문</div>
|
||||
<div className="text-[9px] font-korean mt-0.5 text-[#64748b]">관심 지역(AOI)을 그리고 위성 패스를 선택하세요</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="px-3 py-1 rounded-full text-[10px] font-semibold font-korean" style={{ background: 'rgba(234,179,8,.1)', border: '1px solid rgba(234,179,8,.25)', color: '#eab308' }}>⚠ Beijing-3N 납기 지연 2.15–2.23</span>
|
||||
<button onClick={() => setModalPhase('none')} className="text-lg cursor-pointer p-1 bg-transparent border-none" style={{ color: '#64748b' }}>✕</button>
|
||||
<button onClick={() => setModalPhase('none')} className="text-lg cursor-pointer p-1 bg-transparent border-none text-[#64748b]">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 본문 (좌: 사이드바, 우: 지도+AOI) */}
|
||||
<div className="flex-1 flex overflow-hidden">
|
||||
{/* 왼쪽: 위성 카탈로그 */}
|
||||
<div className="flex flex-col overflow-hidden border-r" style={{ width: 320, minWidth: 320, borderColor: '#21262d', background: '#0d1117' }}>
|
||||
<div className="flex flex-col overflow-hidden border-r border-[#21262d]" style={{ width: 320, minWidth: 320, background: '#0d1117' }}>
|
||||
{/* Optical / SAR / Elevation 탭 */}
|
||||
<div className="flex border-b shrink-0" style={{ borderColor: '#21262d' }}>
|
||||
<div className="flex border-b border-[#21262d] shrink-0">
|
||||
{(['optical', 'sar', 'elevation'] as const).map(t => (
|
||||
<button
|
||||
key={t}
|
||||
@ -636,15 +635,15 @@ export function SatelliteRequest() {
|
||||
</div>
|
||||
|
||||
{/* 필터 바 */}
|
||||
<div className="flex items-center gap-1.5 px-3 py-2 border-b shrink-0" style={{ borderColor: '#21262d' }}>
|
||||
<span className="px-2 py-0.5 rounded text-[9px] font-semibold" style={{ background: 'rgba(59,130,246,.1)', color: '#60a5fa', border: '1px solid rgba(59,130,246,.2)' }}>Filters ✎</span>
|
||||
<span className="px-2 py-0.5 rounded text-[9px] font-semibold" style={{ background: 'rgba(99,102,241,.1)', color: '#818cf8', border: '1px solid rgba(99,102,241,.2)' }}>☁ 구름 ≤ 20% ✕</span>
|
||||
<span className="ml-auto text-[9px] font-mono" style={{ color: '#64748b' }}>↕ 해상도 우선</span>
|
||||
<div className="flex items-center gap-1.5 px-3 py-2 border-b border-[#21262d] shrink-0">
|
||||
<span className="px-2 py-0.5 rounded text-[9px] font-semibold text-[#60a5fa]" style={{ background: 'rgba(59,130,246,.1)', border: '1px solid rgba(59,130,246,.2)' }}>Filters ✎</span>
|
||||
<span className="px-2 py-0.5 rounded text-[9px] font-semibold text-[#818cf8]" style={{ background: 'rgba(99,102,241,.1)', border: '1px solid rgba(99,102,241,.2)' }}>☁ 구름 ≤ 20% ✕</span>
|
||||
<span className="ml-auto text-[9px] font-mono text-[#64748b]">↕ 해상도 우선</span>
|
||||
</div>
|
||||
|
||||
{/* 컬렉션 수 */}
|
||||
<div className="px-3 py-1.5 border-b text-[9px] font-korean shrink-0" style={{ borderColor: '#21262d', color: '#64748b' }}>
|
||||
이 지역에서 <b style={{ color: '#e2e8f0' }}>{up42Filtered.length}</b>개 컬렉션 사용 가능
|
||||
<div className="px-3 py-1.5 border-b border-[#21262d] text-[9px] font-korean shrink-0 text-[#64748b]">
|
||||
이 지역에서 <b className="text-[#e2e8f0]">{up42Filtered.length}</b>개 컬렉션 사용 가능
|
||||
</div>
|
||||
|
||||
{/* 위성 목록 */}
|
||||
@ -653,22 +652,21 @@ export function SatelliteRequest() {
|
||||
<div
|
||||
key={sat.id}
|
||||
onClick={() => setUp42SelSat(up42SelSat === sat.id ? null : sat.id)}
|
||||
className="flex items-center gap-2.5 px-3 py-2.5 border-b cursor-pointer transition-colors"
|
||||
className="flex items-center gap-2.5 px-3 py-2.5 border-b border-[#161b22] cursor-pointer transition-colors"
|
||||
style={{
|
||||
borderColor: '#161b22',
|
||||
background: up42SelSat === sat.id ? 'rgba(59,130,246,.08)' : 'transparent',
|
||||
}}
|
||||
>
|
||||
<div className="w-1 h-8 rounded-full shrink-0" style={{ background: sat.color }} />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-[11px] font-semibold truncate font-korean" style={{ color: '#e2e8f0' }}>{sat.name}</div>
|
||||
<div className="text-[11px] font-semibold truncate font-korean text-[#e2e8f0]">{sat.name}</div>
|
||||
<div className="flex items-center gap-2 mt-0.5">
|
||||
<span className="text-[9px] font-bold font-mono" style={{ color: sat.color }}>{sat.res}</span>
|
||||
{sat.cloud > 0 && <span className="text-[8px] font-mono" style={{ color: '#64748b' }}>☁ ≤{sat.cloud}%</span>}
|
||||
{sat.cloud > 0 && <span className="text-[8px] font-mono text-[#64748b]">☁ ≤{sat.cloud}%</span>}
|
||||
{'delay' in sat && sat.delay && <span className="text-[8px] font-bold" style={{ color: '#eab308' }}>⚠ 지연</span>}
|
||||
</div>
|
||||
</div>
|
||||
{up42SelSat === sat.id && <span className="text-[12px]" style={{ color: '#3b82f6' }}>✓</span>}
|
||||
{up42SelSat === sat.id && <span className="text-[12px] text-blue-500">✓</span>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@ -679,51 +677,51 @@ export function SatelliteRequest() {
|
||||
{/* 지도 영역 (placeholder) */}
|
||||
<div className="flex-1 relative" style={{ background: '#0a0e18' }}>
|
||||
{/* 검색바 */}
|
||||
<div className="absolute top-3 left-3 right-3 flex items-center gap-2 px-3 py-2 rounded-lg z-10" style={{ background: 'rgba(13,17,23,.9)', border: '1px solid #21262d', backdropFilter: 'blur(8px)' }}>
|
||||
<span style={{ color: '#8690a6', fontSize: 13 }}>🔍</span>
|
||||
<input type="text" placeholder="위치 또는 좌표 입력..." className="flex-1 bg-transparent border-none outline-none text-[11px] font-korean" style={{ color: '#e2e8f0' }} />
|
||||
<div className="absolute top-3 left-3 right-3 flex items-center gap-2 px-3 py-2 rounded-lg z-10 border border-[#21262d]" style={{ background: 'rgba(13,17,23,.9)', backdropFilter: 'blur(8px)' }}>
|
||||
<span className="text-[#8690a6] text-[13px]">🔍</span>
|
||||
<input type="text" placeholder="위치 또는 좌표 입력..." className="flex-1 bg-transparent border-none outline-none text-[11px] font-korean text-[#e2e8f0]" />
|
||||
</div>
|
||||
|
||||
{/* 지도 placeholder */}
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="text-3xl mb-2 opacity-20">🗺</div>
|
||||
<div className="text-[11px] font-korean opacity-40" style={{ color: '#64748b' }}>지도 영역 — AOI를 그려 위성 패스를 확인하세요</div>
|
||||
<div className="text-[11px] font-korean opacity-40 text-[#64748b]">지도 영역 — AOI를 그려 위성 패스를 확인하세요</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* AOI 도구 버튼 (오른쪽 사이드) */}
|
||||
<div className="absolute top-14 right-3 flex flex-col gap-1 p-1.5 rounded-lg z-10" style={{ background: 'rgba(13,17,23,.9)', border: '1px solid #21262d' }}>
|
||||
<div className="text-[7px] font-bold text-center mb-0.5" style={{ color: '#64748b' }}>ADD</div>
|
||||
<div className="absolute top-14 right-3 flex flex-col gap-1 p-1.5 rounded-lg z-10 border border-[#21262d]" style={{ background: 'rgba(13,17,23,.9)' }}>
|
||||
<div className="text-[7px] font-bold text-center mb-0.5 text-[#64748b]">ADD</div>
|
||||
{[
|
||||
{ icon: '⬜', title: '사각형 AOI' },
|
||||
{ icon: '🔷', title: '다각형 AOI' },
|
||||
{ icon: '⭕', title: '원형 AOI' },
|
||||
{ icon: '📁', title: '파일 업로드' },
|
||||
].map((t, i) => (
|
||||
<button key={i} className="w-7 h-7 flex items-center justify-center rounded text-sm cursor-pointer border-none" style={{ background: '#161b22', color: '#8690a6' }} title={t.title}>{t.icon}</button>
|
||||
<button key={i} className="w-7 h-7 flex items-center justify-center rounded text-sm cursor-pointer border-none bg-[#161b22] text-[#8690a6]" title={t.title}>{t.icon}</button>
|
||||
))}
|
||||
<div className="h-px my-0.5" style={{ background: '#21262d' }} />
|
||||
<div className="text-[7px] font-bold text-center mb-0.5" style={{ color: '#64748b' }}>AOI</div>
|
||||
<button className="w-7 h-7 flex items-center justify-center rounded text-sm cursor-pointer border-none" style={{ background: '#161b22', color: '#8690a6' }} title="저장된 AOI">💾</button>
|
||||
<button className="w-7 h-7 flex items-center justify-center rounded text-sm cursor-pointer border-none" style={{ background: '#161b22', color: '#ef4444' }} title="AOI 삭제">🗑</button>
|
||||
<div className="h-px my-0.5 bg-[#21262d]" />
|
||||
<div className="text-[7px] font-bold text-center mb-0.5 text-[#64748b]">AOI</div>
|
||||
<button className="w-7 h-7 flex items-center justify-center rounded text-sm cursor-pointer border-none bg-[#161b22] text-[#8690a6]" title="저장된 AOI">💾</button>
|
||||
<button className="w-7 h-7 flex items-center justify-center rounded text-sm cursor-pointer border-none bg-[#161b22] text-[#ef4444]" title="AOI 삭제">🗑</button>
|
||||
</div>
|
||||
|
||||
{/* 줌 컨트롤 */}
|
||||
<div className="absolute bottom-3 right-3 flex flex-col rounded-md overflow-hidden z-10" style={{ border: '1px solid #21262d' }}>
|
||||
<button className="w-7 h-7 flex items-center justify-center text-sm cursor-pointer border-none" style={{ background: '#161b22', color: '#8690a6' }}>+</button>
|
||||
<button className="w-7 h-7 flex items-center justify-center text-sm cursor-pointer border-none border-t" style={{ background: '#161b22', color: '#8690a6', borderTopColor: '#21262d' }}>−</button>
|
||||
<div className="absolute bottom-3 right-3 flex flex-col rounded-md overflow-hidden z-10 border border-[#21262d]">
|
||||
<button className="w-7 h-7 flex items-center justify-center text-sm cursor-pointer border-none bg-[#161b22] text-[#8690a6]">+</button>
|
||||
<button className="w-7 h-7 flex items-center justify-center text-sm cursor-pointer border-none border-t border-t-[#21262d] bg-[#161b22] text-[#8690a6]">−</button>
|
||||
</div>
|
||||
|
||||
{/* 이 지역 검색 버튼 */}
|
||||
<div className="absolute bottom-3 left-1/2 -translate-x-1/2 z-10">
|
||||
<button className="px-4 py-2 rounded-full text-[10px] font-semibold cursor-pointer font-korean" style={{ background: 'rgba(59,130,246,.9)', color: '#fff', border: 'none', boxShadow: '0 2px 12px rgba(59,130,246,.3)' }}>🔍 이 지역 검색</button>
|
||||
<button className="px-4 py-2 rounded-full text-[10px] font-semibold cursor-pointer font-korean text-white border-none" style={{ background: 'rgba(59,130,246,.9)', boxShadow: '0 2px 12px rgba(59,130,246,.3)' }}>🔍 이 지역 검색</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 위성 패스 타임라인 */}
|
||||
<div className="border-t px-4 py-3 shrink-0" style={{ borderColor: '#21262d', background: 'rgba(13,17,23,.95)' }}>
|
||||
<div className="text-[10px] font-bold font-korean mb-2" style={{ color: '#e2e8f0' }}>🛰 오늘 가용 위성 패스 — 선택된 AOI 통과 예정</div>
|
||||
<div className="border-t border-[#21262d] px-4 py-3 shrink-0" style={{ background: 'rgba(13,17,23,.95)' }}>
|
||||
<div className="text-[10px] font-bold font-korean mb-2 text-[#e2e8f0]">🛰 오늘 가용 위성 패스 — 선택된 AOI 통과 예정</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
{up42Passes.map((p, i) => (
|
||||
<div
|
||||
@ -737,10 +735,10 @@ export function SatelliteRequest() {
|
||||
>
|
||||
<div className="w-1.5 h-5 rounded-full shrink-0" style={{ background: p.color }} />
|
||||
<div className="flex-1 flex items-center gap-3 min-w-0">
|
||||
<span className="text-[10px] font-bold font-korean min-w-[100px]" style={{ color: '#e2e8f0' }}>{p.sat}</span>
|
||||
<span className="text-[9px] font-bold font-mono min-w-[110px]" style={{ color: '#60a5fa' }}>{p.time}</span>
|
||||
<span className="text-[9px] font-mono" style={{ color: '#06b6d4' }}>{p.res}</span>
|
||||
<span className="text-[8px] font-mono" style={{ color: '#64748b' }}>{p.cloud}</span>
|
||||
<span className="text-[10px] font-bold font-korean min-w-[100px] text-[#e2e8f0]">{p.sat}</span>
|
||||
<span className="text-[9px] font-bold font-mono min-w-[110px] text-[#60a5fa]">{p.time}</span>
|
||||
<span className="text-[9px] font-mono" className="text-cyan-500">{p.res}</span>
|
||||
<span className="text-[8px] font-mono text-[#64748b]">{p.cloud}</span>
|
||||
</div>
|
||||
{p.note && (
|
||||
<span className="px-1.5 py-px rounded text-[8px] font-bold shrink-0" style={{
|
||||
@ -748,7 +746,7 @@ export function SatelliteRequest() {
|
||||
color: p.note === '최우선 추천' ? '#22c55e' : p.note === '초고해상도' ? '#06b6d4' : p.note === 'SAR' ? '#f59e0b' : '#818cf8',
|
||||
}}>{p.note}</span>
|
||||
)}
|
||||
{up42SelPass === i && <span className="text-xs" style={{ color: '#3b82f6' }}>✓</span>}
|
||||
{up42SelPass === i && <span className="text-xs text-blue-500">✓</span>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@ -757,13 +755,13 @@ export function SatelliteRequest() {
|
||||
</div>
|
||||
|
||||
{/* 푸터 */}
|
||||
<div className="px-6 py-3 border-t flex items-center justify-between shrink-0" style={{ borderColor: '#21262d' }}>
|
||||
<div className="text-[9px] font-korean" style={{ color: '#64748b' }}>원하는 위성을 찾지 못했나요? <span style={{ color: '#60a5fa', cursor: 'pointer' }}>태스킹 주문 생성</span> 또는 <span style={{ color: '#60a5fa', cursor: 'pointer' }}>자세히 보기 ↗</span></div>
|
||||
<div className="px-6 py-3 border-t border-[#21262d] flex items-center justify-between shrink-0">
|
||||
<div className="text-[9px] font-korean text-[#64748b]">원하는 위성을 찾지 못했나요? <span className="text-[#60a5fa] cursor-pointer">태스킹 주문 생성</span> 또는 <span className="text-[#60a5fa] cursor-pointer">자세히 보기 ↗</span></div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[11px] font-korean mr-1.5" style={{ color: '#8690a6' }}>
|
||||
<span className="text-[11px] font-korean mr-1.5 text-[#8690a6]">
|
||||
선택: {up42SelSat ? up42Satellites.find(s => s.id === up42SelSat)?.name : '없음'}
|
||||
</span>
|
||||
<button onClick={() => setModalPhase('provider')} className="px-4 py-2 rounded-lg border text-[11px] font-semibold cursor-pointer font-korean" style={{ borderColor: '#21262d', background: '#161b22', color: '#94a3b8' }}>← 뒤로</button>
|
||||
<button onClick={() => setModalPhase('provider')} className="px-4 py-2 rounded-lg border border-[#21262d] text-[11px] font-semibold cursor-pointer font-korean text-[#94a3b8] bg-[#161b22]">← 뒤로</button>
|
||||
<button
|
||||
onClick={() => setModalPhase('none')}
|
||||
className="px-6 py-2 rounded-lg border-none text-[11px] font-bold cursor-pointer font-korean text-white transition-opacity"
|
||||
|
||||
@ -686,8 +686,8 @@ export function SensorAnalysis() {
|
||||
{/* Axis indicator */}
|
||||
<div className="absolute bottom-16 left-4" style={{ fontSize: '9px', fontFamily: 'var(--fM)' }}>
|
||||
<div style={{ color: '#ef4444' }}>X →</div>
|
||||
<div style={{ color: '#22c55e' }}>Y ↑</div>
|
||||
<div style={{ color: '#3b82f6' }}>Z ⊙</div>
|
||||
<div className="text-green-500">Y ↑</div>
|
||||
<div className="text-blue-500">Z ⊙</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -128,7 +128,7 @@ function ShipInsurance() {
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-[11px] font-semibold text-text-2 mb-1.5">조회 기본값 — 조회 키 타입</label>
|
||||
<select value={configKeyType} onChange={e => setConfigKeyType(e.target.value)} className="prd-i w-full" style={{ borderColor: 'var(--bd)' }}>
|
||||
<select value={configKeyType} onChange={e => setConfigKeyType(e.target.value)} className="prd-i w-full" className="border-border">
|
||||
<option value="mmsi">MMSI</option>
|
||||
<option value="imo">IMO 번호</option>
|
||||
<option value="shipname">선박명</option>
|
||||
@ -137,7 +137,7 @@ function ShipInsurance() {
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-[11px] font-semibold text-text-2 mb-1.5">응답 형식</label>
|
||||
<select value={configRespType} onChange={e => setConfigRespType(e.target.value)} className="prd-i w-full" style={{ borderColor: 'var(--bd)' }}>
|
||||
<select value={configRespType} onChange={e => setConfigRespType(e.target.value)} className="prd-i w-full" className="border-border">
|
||||
<option value="json">JSON</option>
|
||||
<option value="xml">XML</option>
|
||||
</select>
|
||||
@ -163,7 +163,7 @@ function ShipInsurance() {
|
||||
<div className="flex gap-2 items-end flex-wrap">
|
||||
<div>
|
||||
<label className="block text-[10px] font-semibold text-text-3 mb-1">조회 키 타입</label>
|
||||
<select value={searchType} onChange={e => setSearchType(e.target.value)} className="prd-i min-w-[120px]" style={{ borderColor: 'var(--bd)' }}>
|
||||
<select value={searchType} onChange={e => setSearchType(e.target.value)} className="prd-i min-w-[120px]" className="border-border">
|
||||
<option value="mmsi">MMSI</option>
|
||||
<option value="imo">IMO 번호</option>
|
||||
<option value="shipname">선박명</option>
|
||||
@ -177,7 +177,7 @@ function ShipInsurance() {
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-[10px] font-semibold text-text-3 mb-1">보험 종류</label>
|
||||
<select value={insTypeFilter} onChange={e => setInsTypeFilter(e.target.value)} className="prd-i min-w-[140px]" style={{ borderColor: 'var(--bd)' }}>
|
||||
<select value={insTypeFilter} onChange={e => setInsTypeFilter(e.target.value)} className="prd-i min-w-[140px]" className="border-border">
|
||||
<option>전체</option>
|
||||
<option>P&I 보험</option>
|
||||
<option>선주책임보험</option>
|
||||
|
||||
@ -63,7 +63,7 @@ export function HNSAnalysisListTable({ onTabChange }: HNSAnalysisListTableProps)
|
||||
<span className="text-[18px]">📋</span>
|
||||
HNS 대기확산 분석 목록
|
||||
</div>
|
||||
<span className="text-[10px] text-text-3 bg-bg-3 font-mono" style={{ padding: '4px 10px', borderRadius: '12px' }}>
|
||||
<span className="text-[10px] text-text-3 bg-bg-3 font-mono px-[10px] py-1 rounded-xl">
|
||||
총 {analyses.length}건
|
||||
</span>
|
||||
</div>
|
||||
@ -71,15 +71,12 @@ export function HNSAnalysisListTable({ onTabChange }: HNSAnalysisListTableProps)
|
||||
<input
|
||||
type="text"
|
||||
placeholder="검색..."
|
||||
className="bg-bg-3 border border-border rounded-sm text-[11px]"
|
||||
style={{ padding: '8px 12px', width: '200px' }}
|
||||
className="bg-bg-3 border border-border rounded-sm text-[11px] px-3 py-2 w-[200px]"
|
||||
/>
|
||||
<button
|
||||
onClick={() => onTabChange('analysis')}
|
||||
className="text-status-orange text-[11px] font-semibold cursor-pointer flex items-center gap-1"
|
||||
className="text-status-orange text-[11px] font-semibold cursor-pointer flex items-center gap-1 px-4 py-2 rounded-sm"
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
borderRadius: '6px',
|
||||
border: '1px solid rgba(249,115,22,0.3)',
|
||||
background: 'linear-gradient(135deg, rgba(249,115,22,0.15), rgba(239,68,68,0.1))',
|
||||
}}
|
||||
@ -92,41 +89,41 @@ export function HNSAnalysisListTable({ onTabChange }: HNSAnalysisListTableProps)
|
||||
{/* Table */}
|
||||
<div className="flex-1 overflow-auto">
|
||||
{loading ? (
|
||||
<div className="text-center text-text-3 text-[12px]" style={{ padding: '80px 0' }}>로딩 중...</div>
|
||||
<div className="text-center text-text-3 text-[12px] py-20">로딩 중...</div>
|
||||
) : (
|
||||
<table className="w-full text-[11px]" style={{ borderCollapse: 'collapse' }}>
|
||||
<table className="w-full text-[11px] border-collapse">
|
||||
<thead className="sticky top-0 z-10">
|
||||
<tr className="bg-bg-2 border-b border-border">
|
||||
<th className="text-center text-[10px] font-semibold text-text-2" style={{ padding: '12px 16px', width: '50px' }}>번호</th>
|
||||
<th className="text-left text-[10px] font-semibold text-text-2" style={{ padding: '12px 16px', minWidth: '180px' }}>분석명</th>
|
||||
<th className="text-center text-[10px] font-semibold text-text-2" style={{ padding: '12px 16px', minWidth: '100px' }}>물질</th>
|
||||
<th className="text-center text-[10px] font-semibold text-text-2" style={{ padding: '12px 16px', minWidth: '130px' }}>사고일시</th>
|
||||
<th className="text-center text-[10px] font-semibold text-text-2" style={{ padding: '12px 16px', minWidth: '100px' }}>분석날짜</th>
|
||||
<th className="text-center text-[10px] font-semibold text-text-2" style={{ padding: '12px 16px', minWidth: '120px' }}>사고지점</th>
|
||||
<th className="text-center text-[10px] font-semibold text-text-2" style={{ padding: '12px 16px', minWidth: '90px' }}>유출량</th>
|
||||
<th className="text-center text-[10px] font-semibold text-text-2" style={{ padding: '12px 16px', minWidth: '80px' }}>알고리즘</th>
|
||||
<th className="text-center text-[10px] font-semibold text-text-2" style={{ padding: '12px 16px', width: '70px' }}>예측시간</th>
|
||||
<th className="text-center text-[10px] font-semibold text-text-2" style={{ padding: '12px 16px', width: '70px' }}>
|
||||
<th className="text-center text-[10px] font-semibold text-text-2 px-4 py-3 w-[50px]">번호</th>
|
||||
<th className="text-left text-[10px] font-semibold text-text-2 px-4 py-3 min-w-[180px]">분석명</th>
|
||||
<th className="text-center text-[10px] font-semibold text-text-2 px-4 py-3 min-w-[100px]">물질</th>
|
||||
<th className="text-center text-[10px] font-semibold text-text-2 px-4 py-3 min-w-[130px]">사고일시</th>
|
||||
<th className="text-center text-[10px] font-semibold text-text-2 px-4 py-3 min-w-[100px]">분석날짜</th>
|
||||
<th className="text-center text-[10px] font-semibold text-text-2 px-4 py-3 min-w-[120px]">사고지점</th>
|
||||
<th className="text-center text-[10px] font-semibold text-text-2 px-4 py-3 min-w-[90px]">유출량</th>
|
||||
<th className="text-center text-[10px] font-semibold text-text-2 px-4 py-3 min-w-[80px]">알고리즘</th>
|
||||
<th className="text-center text-[10px] font-semibold text-text-2 px-4 py-3 w-[70px]">예측시간</th>
|
||||
<th className="text-center text-[10px] font-semibold text-text-2 px-4 py-3 w-[70px]">
|
||||
<div className="flex flex-col items-center gap-0.5">
|
||||
<span>AEGL-3</span>
|
||||
<span className="text-[8px] text-text-3">생명위협</span>
|
||||
</div>
|
||||
</th>
|
||||
<th className="text-center text-[10px] font-semibold text-text-2" style={{ padding: '12px 16px', width: '70px' }}>
|
||||
<th className="text-center text-[10px] font-semibold text-text-2 px-4 py-3 w-[70px]">
|
||||
<div className="flex flex-col items-center gap-0.5">
|
||||
<span>AEGL-2</span>
|
||||
<span className="text-[8px] text-text-3">건강피해</span>
|
||||
</div>
|
||||
</th>
|
||||
<th className="text-center text-[10px] font-semibold text-text-2" style={{ padding: '12px 16px', width: '70px' }}>
|
||||
<th className="text-center text-[10px] font-semibold text-text-2 px-4 py-3 w-[70px]">
|
||||
<div className="flex flex-col items-center gap-0.5">
|
||||
<span>AEGL-1</span>
|
||||
<span className="text-[8px] text-text-3">불쾌감</span>
|
||||
</div>
|
||||
</th>
|
||||
<th className="text-center text-[10px] font-semibold text-text-2" style={{ padding: '12px 16px', minWidth: '80px' }}>위험등급</th>
|
||||
<th className="text-center text-[10px] font-semibold text-text-2" style={{ padding: '12px 16px', minWidth: '90px' }}>피해반경</th>
|
||||
<th className="text-center text-[10px] font-semibold text-text-2" style={{ padding: '12px 16px', minWidth: '120px' }}>분석자</th>
|
||||
<th className="text-center text-[10px] font-semibold text-text-2 px-4 py-3 min-w-[80px]">위험등급</th>
|
||||
<th className="text-center text-[10px] font-semibold text-text-2 px-4 py-3 min-w-[90px]">피해반경</th>
|
||||
<th className="text-center text-[10px] font-semibold text-text-2 px-4 py-3 min-w-[120px]">분석자</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@ -151,30 +148,30 @@ export function HNSAnalysisListTable({ onTabChange }: HNSAnalysisListTableProps)
|
||||
onMouseEnter={(e) => e.currentTarget.style.background = 'var(--bg2)'}
|
||||
onMouseLeave={(e) => e.currentTarget.style.background = index % 2 === 0 ? 'transparent' : 'rgba(255,255,255,0.01)'}
|
||||
>
|
||||
<td className="text-center text-text-3 font-mono" style={{ padding: '12px 16px' }}>{item.hnsAnlysSn}</td>
|
||||
<td className="font-medium" style={{ padding: '12px 16px' }}>{item.anlysNm}</td>
|
||||
<td className="text-center" style={{ padding: '12px 16px' }}>
|
||||
<td className="text-center text-text-3 font-mono px-4 py-3">{item.hnsAnlysSn}</td>
|
||||
<td className="font-medium px-4 py-3">{item.anlysNm}</td>
|
||||
<td className="text-center px-4 py-3">
|
||||
<span
|
||||
className="text-[9px] font-semibold text-status-orange"
|
||||
style={{ padding: '4px 8px', borderRadius: '4px', background: 'rgba(249,115,22,0.12)' }}
|
||||
className="text-[9px] font-semibold text-status-orange px-2 py-1 rounded"
|
||||
style={{ background: 'rgba(249,115,22,0.12)' }}
|
||||
>
|
||||
{substanceTag(item.sbstNm)}
|
||||
</span>
|
||||
</td>
|
||||
<td className="text-center text-text-2 font-mono text-[10px]" style={{ padding: '12px 16px' }}>{formatDate(item.acdntDtm, 'full')}</td>
|
||||
<td className="text-center text-text-3 font-mono text-[10px]" style={{ padding: '12px 16px' }}>{formatDate(item.regDtm, 'date')}</td>
|
||||
<td className="text-center text-text-2" style={{ padding: '12px 16px' }}>{item.locNm || '—'}</td>
|
||||
<td className="text-center text-text-2 font-mono" style={{ padding: '12px 16px' }}>{amount}</td>
|
||||
<td className="text-center" style={{ padding: '12px 16px' }}>
|
||||
<td className="text-center text-text-2 font-mono text-[10px] px-4 py-3">{formatDate(item.acdntDtm, 'full')}</td>
|
||||
<td className="text-center text-text-3 font-mono text-[10px] px-4 py-3">{formatDate(item.regDtm, 'date')}</td>
|
||||
<td className="text-center text-text-2 px-4 py-3">{item.locNm || '—'}</td>
|
||||
<td className="text-center text-text-2 font-mono px-4 py-3">{amount}</td>
|
||||
<td className="text-center px-4 py-3">
|
||||
<span
|
||||
className="text-[9px] font-semibold text-primary-cyan"
|
||||
style={{ padding: '4px 8px', borderRadius: '4px', background: 'rgba(6,182,212,0.12)' }}
|
||||
className="text-[9px] font-semibold text-primary-cyan px-2 py-1 rounded"
|
||||
style={{ background: 'rgba(6,182,212,0.12)' }}
|
||||
>
|
||||
{item.algoCd || '—'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="text-center text-text-2 font-mono" style={{ padding: '12px 16px' }}>{item.fcstHr ? `${item.fcstHr}H` : '—'}</td>
|
||||
<td className="text-center" style={{ padding: '12px 16px' }}>
|
||||
<td className="text-center text-text-2 font-mono px-4 py-3">{item.fcstHr ? `${item.fcstHr}H` : '—'}</td>
|
||||
<td className="text-center px-4 py-3">
|
||||
<div
|
||||
className="w-6 h-6 rounded-full mx-auto"
|
||||
style={{
|
||||
@ -183,7 +180,7 @@ export function HNSAnalysisListTable({ onTabChange }: HNSAnalysisListTableProps)
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
<td className="text-center" style={{ padding: '12px 16px' }}>
|
||||
<td className="text-center px-4 py-3">
|
||||
<div
|
||||
className="w-6 h-6 rounded-full mx-auto"
|
||||
style={{
|
||||
@ -192,7 +189,7 @@ export function HNSAnalysisListTable({ onTabChange }: HNSAnalysisListTableProps)
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
<td className="text-center" style={{ padding: '12px 16px' }}>
|
||||
<td className="text-center px-4 py-3">
|
||||
<div
|
||||
className="w-6 h-6 rounded-full mx-auto"
|
||||
style={{
|
||||
@ -201,16 +198,16 @@ export function HNSAnalysisListTable({ onTabChange }: HNSAnalysisListTableProps)
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
<td className="text-center" style={{ padding: '12px 16px' }}>
|
||||
<td className="text-center px-4 py-3">
|
||||
<span
|
||||
className="text-[9px] font-semibold"
|
||||
style={{ padding: '4px 10px', borderRadius: '4px', background: riskStyle.bg, color: riskStyle.color }}
|
||||
className="text-[9px] font-semibold px-[10px] py-1 rounded"
|
||||
style={{ background: riskStyle.bg, color: riskStyle.color }}
|
||||
>
|
||||
{riskLabel}
|
||||
</span>
|
||||
</td>
|
||||
<td className="text-center text-text-2 font-mono" style={{ padding: '12px 16px' }}>{damageRadius}</td>
|
||||
<td className="text-center text-text-3 text-[10px]" style={{ padding: '12px 16px' }}>{item.analystNm || '—'}</td>
|
||||
<td className="text-center text-text-2 font-mono px-4 py-3">{damageRadius}</td>
|
||||
<td className="text-center text-text-3 text-[10px] px-4 py-3">{item.analystNm || '—'}</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
@ -219,7 +216,7 @@ export function HNSAnalysisListTable({ onTabChange }: HNSAnalysisListTableProps)
|
||||
)}
|
||||
|
||||
{!loading && analyses.length === 0 && (
|
||||
<div className="text-center text-text-3 text-[12px]" style={{ padding: '80px 0' }}>분석 데이터가 없습니다.</div>
|
||||
<div className="text-center text-text-3 text-[12px] py-20">분석 데이터가 없습니다.</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -88,21 +88,19 @@ export function HNSLeftPanel({
|
||||
<div>
|
||||
<label className="hns-lbl text-[8px] text-text-3 block mb-1">사고명</label>
|
||||
<input
|
||||
className="hns-inp w-full"
|
||||
style={{ padding: '8px 10px', background: 'var(--bg0)', border: '1px solid var(--bd)', borderRadius: 'var(--rS)', fontSize: '11px' }}
|
||||
className="hns-inp w-full px-[10px] py-2 bg-bg-0 border border-border rounded text-[11px]"
|
||||
value={accidentName}
|
||||
onChange={(e) => setAccidentName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 사고일시 + 예측시간 */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '6px' }}>
|
||||
<div className="grid grid-cols-2 gap-1.5">
|
||||
<div>
|
||||
<label className="hns-lbl text-[8px] text-text-3 block mb-1">사고일시</label>
|
||||
<input
|
||||
className="hns-inp w-full"
|
||||
className="hns-inp w-full px-[10px] py-2 bg-bg-0 border border-border rounded text-[11px]"
|
||||
type="datetime-local"
|
||||
style={{ padding: '8px 10px', background: 'var(--bg0)', border: '1px solid var(--bd)', borderRadius: 'var(--rS)', fontSize: '11px' }}
|
||||
defaultValue="2025-02-11T05:02"
|
||||
/>
|
||||
</div>
|
||||
@ -125,17 +123,15 @@ export function HNSLeftPanel({
|
||||
|
||||
{/* 사고지점 */}
|
||||
<div
|
||||
className="p-[10px] rounded-md"
|
||||
style={{ background: 'var(--bg0)', border: '1px solid rgba(6,182,212,0.2)' }}
|
||||
className="p-[10px] rounded-md bg-bg-0"
|
||||
style={{ border: '1px solid rgba(6,182,212,0.2)' }}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<label className="hns-lbl text-[8px] text-text-3">📍 사고지점</label>
|
||||
<button
|
||||
onClick={onMapSelectClick}
|
||||
className="text-primary-cyan text-[8px] font-bold cursor-pointer"
|
||||
className="text-primary-cyan text-[8px] font-bold cursor-pointer px-[10px] py-1 rounded"
|
||||
style={{
|
||||
padding: '4px 10px',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid rgba(6,182,212,0.3)',
|
||||
background: 'rgba(6,182,212,0.08)',
|
||||
transition: '0.15s'
|
||||
@ -144,12 +140,11 @@ export function HNSLeftPanel({
|
||||
🗺 지도에서 클릭
|
||||
</button>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '6px', marginBottom: '6px' }}>
|
||||
<div className="grid grid-cols-2 gap-1.5 mb-1.5">
|
||||
<div>
|
||||
<label className="hns-lbl text-[8px] text-text-3 block mb-1">위도</label>
|
||||
<input
|
||||
className="hns-inp w-full"
|
||||
style={{ padding: '8px 10px', background: 'var(--bg0)', border: '1px solid var(--bd)', borderRadius: 'var(--rS)', fontSize: '11px' }}
|
||||
className="hns-inp w-full px-[10px] py-2 bg-bg-0 border border-border rounded text-[11px]"
|
||||
value={incidentCoord.lat.toFixed(4)}
|
||||
onChange={(e) => onCoordChange({ ...incidentCoord, lat: parseFloat(e.target.value) || 0 })}
|
||||
/>
|
||||
@ -157,8 +152,7 @@ export function HNSLeftPanel({
|
||||
<div>
|
||||
<label className="hns-lbl text-[8px] text-text-3 block mb-1">경도</label>
|
||||
<input
|
||||
className="hns-inp w-full"
|
||||
style={{ padding: '8px 10px', background: 'var(--bg0)', border: '1px solid var(--bd)', borderRadius: 'var(--rS)', fontSize: '11px' }}
|
||||
className="hns-inp w-full px-[10px] py-2 bg-bg-0 border border-border rounded text-[11px]"
|
||||
value={incidentCoord.lon.toFixed(4)}
|
||||
onChange={(e) => onCoordChange({ ...incidentCoord, lon: parseFloat(e.target.value) || 0 })}
|
||||
/>
|
||||
@ -167,8 +161,7 @@ export function HNSLeftPanel({
|
||||
<div>
|
||||
<label className="hns-lbl text-[8px] text-text-3 block mb-1">상세 위치</label>
|
||||
<input
|
||||
className="hns-inp w-full"
|
||||
style={{ padding: '8px 10px', background: 'var(--bg0)', border: '1px solid var(--bd)', borderRadius: 'var(--rS)', fontSize: '11px' }}
|
||||
className="hns-inp w-full px-[10px] py-2 bg-bg-0 border border-border rounded text-[11px]"
|
||||
value={locationName}
|
||||
onChange={(e) => setLocationName(e.target.value)}
|
||||
/>
|
||||
@ -187,30 +180,30 @@ export function HNSLeftPanel({
|
||||
</div>
|
||||
<span className="text-[7px] text-text-3 font-mono">KMA API · 울산 AWS</span>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr 1fr', gap: '6px', marginBottom: '6px' }}>
|
||||
<div className="text-center bg-bg-0 rounded border border-border" style={{ padding: '6px 2px' }}>
|
||||
<div className="grid grid-cols-4 gap-1.5 mb-1.5">
|
||||
<div className="text-center bg-bg-0 rounded border border-border py-1.5 px-0.5">
|
||||
<div className="text-[12px] font-extrabold font-mono text-primary-cyan">5.2</div>
|
||||
<div className="text-[7px] text-text-3">풍속(m/s)</div>
|
||||
</div>
|
||||
<div className="text-center bg-bg-0 rounded border border-border" style={{ padding: '6px 2px' }}>
|
||||
<div className="text-center bg-bg-0 rounded border border-border py-1.5 px-0.5">
|
||||
<div className="text-[12px] font-extrabold font-mono text-primary-cyan">SW 225°</div>
|
||||
<div className="text-[7px] text-text-3">풍향</div>
|
||||
</div>
|
||||
<div className="text-center bg-bg-0 rounded border border-border" style={{ padding: '6px 2px' }}>
|
||||
<div className="text-center bg-bg-0 rounded border border-border py-1.5 px-0.5">
|
||||
<div className="text-[12px] font-extrabold font-mono text-status-orange">8.5°C</div>
|
||||
<div className="text-[7px] text-text-3">기온</div>
|
||||
</div>
|
||||
<div className="text-center bg-bg-0 rounded border border-border" style={{ padding: '6px 2px' }}>
|
||||
<div className="text-center bg-bg-0 rounded border border-border py-1.5 px-0.5">
|
||||
<div className="text-[12px] font-extrabold font-mono text-primary-blue">62%</div>
|
||||
<div className="text-[7px] text-text-3">습도</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '6px' }}>
|
||||
<div className="flex justify-between bg-bg-0 text-[8px]" style={{ padding: '3px 6px', borderRadius: '3px' }}>
|
||||
<div className="grid grid-cols-2 gap-1.5">
|
||||
<div className="flex justify-between bg-bg-0 text-[8px] px-1.5 py-[3px] rounded-[3px]">
|
||||
<span className="text-text-3">대기안정도</span>
|
||||
<span className="font-semibold">D (중립)</span>
|
||||
</div>
|
||||
<div className="flex justify-between bg-bg-0 text-[8px]" style={{ padding: '3px 6px', borderRadius: '3px' }}>
|
||||
<div className="flex justify-between bg-bg-0 text-[8px] px-1.5 py-[3px] rounded-[3px]">
|
||||
<span className="text-text-3">지표 조도</span>
|
||||
<span className="font-semibold">해안</span>
|
||||
</div>
|
||||
@ -218,7 +211,7 @@ export function HNSLeftPanel({
|
||||
</div>
|
||||
|
||||
{/* 알고리즘 선택 */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '6px' }}>
|
||||
<div className="grid grid-cols-2 gap-1.5">
|
||||
<div>
|
||||
<label className="hns-lbl text-[8px] text-text-3 block mb-1">예측 알고리즘</label>
|
||||
<ComboBox
|
||||
@ -296,16 +289,14 @@ export function HNSLeftPanel({
|
||||
{/* UN번호 / CAS번호 */}
|
||||
<div>
|
||||
<label className="hns-lbl text-[8px] text-text-3 block mb-1">UN번호 / CAS번호</label>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '6px' }}>
|
||||
<div className="grid grid-cols-2 gap-1.5">
|
||||
<input
|
||||
className="hns-inp w-full"
|
||||
style={{ padding: '8px 10px', background: 'var(--bg0)', border: '1px solid var(--bd)', borderRadius: 'var(--rS)', fontSize: '11px' }}
|
||||
className="hns-inp w-full px-[10px] py-2 bg-bg-0 border border-border rounded text-[11px]"
|
||||
value={unNumber}
|
||||
readOnly
|
||||
/>
|
||||
<input
|
||||
className="hns-inp w-full"
|
||||
style={{ padding: '8px 10px', background: 'var(--bg0)', border: '1px solid var(--bd)', borderRadius: 'var(--rS)', fontSize: '11px' }}
|
||||
className="hns-inp w-full px-[10px] py-2 bg-bg-0 border border-border rounded text-[11px]"
|
||||
value={casNumber}
|
||||
readOnly
|
||||
/>
|
||||
@ -313,13 +304,12 @@ export function HNSLeftPanel({
|
||||
</div>
|
||||
|
||||
{/* 유출량 + 단위 */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '6px' }}>
|
||||
<div className="grid grid-cols-2 gap-1.5">
|
||||
<div>
|
||||
<label className="hns-lbl text-[8px] text-text-3 block mb-1">유출량</label>
|
||||
<input
|
||||
className="hns-inp w-full"
|
||||
className="hns-inp w-full px-[10px] py-2 bg-bg-0 border border-border rounded text-[11px]"
|
||||
type="number"
|
||||
style={{ padding: '8px 10px', background: 'var(--bg0)', border: '1px solid var(--bd)', borderRadius: 'var(--rS)', fontSize: '11px' }}
|
||||
value={amount}
|
||||
onChange={(e) => setAmount(e.target.value)}
|
||||
step="0.1"
|
||||
@ -364,7 +354,7 @@ export function HNSLeftPanel({
|
||||
<div className="text-[8px] font-bold text-status-orange mb-1">
|
||||
⚠ 물질 위험 특성
|
||||
</div>
|
||||
<div className="text-[8px]" style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '3px' }}>
|
||||
<div className="grid grid-cols-2 gap-[3px] text-[8px]">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-text-3">인화점</span>
|
||||
<span className="text-status-red font-semibold font-mono">4°C</span>
|
||||
@ -402,15 +392,15 @@ export function HNSLeftPanel({
|
||||
</div>
|
||||
<div className="flex flex-col gap-0.5 text-[8px]">
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="w-2 h-2" style={{ borderRadius: '2px', background: 'rgba(239,68,68,0.7)' }}></div>
|
||||
<div className="w-2 h-2 rounded-[2px]" style={{ background: 'rgba(239,68,68,0.7)' }}></div>
|
||||
<span className="text-text-3">AEGL-3 (생명위협) — 500 ppm</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="w-2 h-2" style={{ borderRadius: '2px', background: 'rgba(249,115,22,0.7)' }}></div>
|
||||
<div className="w-2 h-2 rounded-[2px]" style={{ background: 'rgba(249,115,22,0.7)' }}></div>
|
||||
<span className="text-text-3">AEGL-2 (건강피해) — 150 ppm</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="w-2 h-2" style={{ borderRadius: '2px', background: 'rgba(234,179,8,0.7)' }}></div>
|
||||
<div className="w-2 h-2 rounded-[2px]" style={{ background: 'rgba(234,179,8,0.7)' }}></div>
|
||||
<span className="text-text-3">AEGL-1 (불쾌감) — 37 ppm</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -424,9 +414,8 @@ export function HNSLeftPanel({
|
||||
<button
|
||||
onClick={onRunPrediction}
|
||||
disabled={isRunningPrediction}
|
||||
className="text-white text-[13px] font-bold rounded-md"
|
||||
className="text-white text-[13px] font-bold rounded-md px-[40px] py-3"
|
||||
style={{
|
||||
padding: '12px 40px',
|
||||
background: isRunningPrediction
|
||||
? 'var(--t4)'
|
||||
: 'linear-gradient(135deg, var(--orange), var(--red))',
|
||||
@ -440,8 +429,7 @@ export function HNSLeftPanel({
|
||||
</button>
|
||||
<button
|
||||
onClick={handleReset}
|
||||
className="bg-bg-3 border border-border rounded-md text-text-2 text-[12px] font-semibold cursor-pointer"
|
||||
style={{ padding: '12px 24px' }}
|
||||
className="bg-bg-3 border border-border rounded-md text-text-2 text-[12px] font-semibold cursor-pointer px-6 py-3"
|
||||
>
|
||||
🔄 초기화
|
||||
</button>
|
||||
@ -530,15 +518,15 @@ export function HNSLeftPanel({
|
||||
📊 통계
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex justify-between items-center p-2 bg-bg-0" style={{ borderRadius: 'var(--rS)' }}>
|
||||
<div className="flex justify-between items-center p-2 bg-bg-0 rounded">
|
||||
<span className="text-[10px] text-text-3">전체 분석</span>
|
||||
<span className="text-sm font-bold text-primary-cyan font-mono">8건</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center p-2 bg-bg-0" style={{ borderRadius: 'var(--rS)' }}>
|
||||
<div className="flex justify-between items-center p-2 bg-bg-0 rounded">
|
||||
<span className="text-[10px] text-text-3">고위험 (AEGL-3)</span>
|
||||
<span className="text-sm font-bold text-status-red font-mono">3건</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center p-2 bg-bg-0" style={{ borderRadius: 'var(--rS)' }}>
|
||||
<div className="flex justify-between items-center p-2 bg-bg-0 rounded">
|
||||
<span className="text-[10px] text-text-3">중위험 (AEGL-2)</span>
|
||||
<span className="text-sm font-bold text-status-orange font-mono">5건</span>
|
||||
</div>
|
||||
|
||||
@ -132,7 +132,7 @@ export function HNSScenarioView() {
|
||||
return (
|
||||
<div className="flex flex-col flex-1 w-full h-full overflow-hidden bg-bg-0">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between shrink-0 border-b border-border" style={{ padding: '14px 20px', background: 'var(--bg1)' }}>
|
||||
<div className="flex items-center justify-between shrink-0 border-b border-border px-5 py-[14px] bg-bg-1">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<span className="text-base">📊</span>
|
||||
<div>
|
||||
@ -148,8 +148,7 @@ export function HNSScenarioView() {
|
||||
<select
|
||||
value={selectedIncident}
|
||||
onChange={(e) => setSelectedIncident(Number(e.target.value))}
|
||||
className="prd-i"
|
||||
style={{ width: '280px', fontSize: '11px' }}
|
||||
className="prd-i w-[280px] text-[11px]"
|
||||
>
|
||||
{incidents.length === 0
|
||||
? <option value={0}>분석 데이터 없음</option>
|
||||
@ -162,11 +161,10 @@ export function HNSScenarioView() {
|
||||
</select>
|
||||
<button
|
||||
onClick={() => setModalOpen(true)}
|
||||
className="cursor-pointer whitespace-nowrap font-bold text-status-orange"
|
||||
className="cursor-pointer whitespace-nowrap font-bold text-status-orange text-[11px] px-[14px] py-1.5 rounded-sm"
|
||||
style={{
|
||||
padding: '6px 14px', background: 'rgba(249,115,22,0.12)',
|
||||
border: '1px solid rgba(249,115,22,0.3)', borderRadius: '6px',
|
||||
fontSize: '11px',
|
||||
background: 'rgba(249,115,22,0.12)',
|
||||
border: '1px solid rgba(249,115,22,0.3)',
|
||||
}}
|
||||
>
|
||||
+ 신규 시나리오
|
||||
@ -177,17 +175,15 @@ export function HNSScenarioView() {
|
||||
{/* Body: Left list + Right detail */}
|
||||
<div className="flex flex-1 overflow-hidden">
|
||||
{/* ── Left: Scenario List ── */}
|
||||
<div className="flex flex-col overflow-hidden shrink-0 border-r border-border" style={{ width: '370px', minWidth: '370px', background: 'var(--bg1)' }}>
|
||||
<div className="flex items-center justify-between border-b border-border" style={{ padding: '10px 14px' }}>
|
||||
<div className="flex flex-col overflow-hidden shrink-0 border-r border-border bg-bg-1" style={{ width: '370px', minWidth: '370px' }}>
|
||||
<div className="flex items-center justify-between border-b border-border px-[14px] py-2.5">
|
||||
<span className="text-[11px] font-bold text-text-3">
|
||||
시나리오 목록 — 톨루엔 대기확산
|
||||
</span>
|
||||
<div className="flex gap-1">
|
||||
{['시간순', '위험도순'].map((label, i) => (
|
||||
<button key={i} className="cursor-pointer" style={{
|
||||
padding: '3px 8px', fontSize: '9px', fontWeight: 600,
|
||||
borderRadius: '4px',
|
||||
border: '1px solid var(--bd)', background: i === 0 ? 'rgba(249,115,22,0.08)' : 'var(--bg3)',
|
||||
<button key={i} className="cursor-pointer px-2 py-[3px] text-[9px] font-semibold rounded-sm border border-border" style={{
|
||||
background: i === 0 ? 'rgba(249,115,22,0.08)' : 'var(--bg3)',
|
||||
color: i === 0 ? 'var(--orange)' : 'var(--t3)',
|
||||
}}>
|
||||
{label}
|
||||
@ -197,7 +193,7 @@ export function HNSScenarioView() {
|
||||
</div>
|
||||
|
||||
{/* Scrollable list */}
|
||||
<div className="flex-1 overflow-y-auto flex flex-col gap-1.5" style={{ padding: '8px', scrollbarWidth: 'thin', scrollbarColor: 'var(--bdL) transparent' }}>
|
||||
<div className="flex-1 overflow-y-auto flex flex-col gap-1.5 p-2" style={{ scrollbarWidth: 'thin', scrollbarColor: 'var(--bdL) transparent' }}>
|
||||
{scenarios.map((scn, idx) => {
|
||||
const sev = SEVERITY_STYLE[scn.severity]
|
||||
const isSel = selectedIdx === idx
|
||||
@ -221,37 +217,37 @@ export function HNSScenarioView() {
|
||||
{scn.id} {scn.name}
|
||||
</span>
|
||||
</div>
|
||||
<span className="font-bold" style={{ padding: '2px 8px', background: sev.bg, borderRadius: '8px', fontSize: '8px', color: sev.color }}>
|
||||
<span className="font-bold px-2 py-[2px] rounded-lg text-[8px]" style={{ background: sev.bg, color: sev.color }}>
|
||||
{scn.severity}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Time row */}
|
||||
<div className="flex items-center gap-1.5 mb-1.5">
|
||||
<span className="font-bold font-mono text-status-orange" style={{ padding: '2px 6px', background: 'rgba(249,115,22,0.1)', borderRadius: '3px', fontSize: '9px' }}>
|
||||
<span className="font-bold font-mono text-status-orange text-[9px] px-1.5 py-[2px] rounded-[3px]" style={{ background: 'rgba(249,115,22,0.1)' }}>
|
||||
{scn.timeStep}
|
||||
</span>
|
||||
<span className="text-[9px] text-text-3 font-mono">{scn.datetime}</span>
|
||||
<span className="ml-auto text-text-3" style={{ fontSize: '8px' }}>{scn.wind}</span>
|
||||
<span className="ml-auto text-text-3 text-[8px]">{scn.wind}</span>
|
||||
</div>
|
||||
|
||||
{/* Metrics grid */}
|
||||
<div className="grid font-mono" style={{ gridTemplateColumns: '1fr 1fr 1fr 1fr', gap: '4px', fontSize: '8px' }}>
|
||||
<div className="grid grid-cols-4 gap-1 font-mono text-[8px]">
|
||||
{[
|
||||
{ label: '최대농도', value: scn.maxConc, color: '#f87171' },
|
||||
{ label: 'IDLH반경', value: scn.idlhRadius, color: '#f87171' },
|
||||
{ label: 'ERPG-2', value: scn.erpg2, color: '#f97316' },
|
||||
{ label: '영향인구', value: scn.population, color: '#f87171' },
|
||||
].map((m, i) => (
|
||||
<div key={i} className="text-center" style={{ padding: '3px', background: 'var(--bg0)', borderRadius: '3px' }}>
|
||||
<div className="text-text-3" style={{ fontSize: '7px' }}>{m.label}</div>
|
||||
<div key={i} className="text-center p-[3px] bg-bg-0 rounded-[3px]">
|
||||
<div className="text-text-3 text-[7px]">{m.label}</div>
|
||||
<div className="font-bold" style={{ color: m.color }}>{m.value}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div className="text-text-2 mt-1.5" style={{ fontSize: '8px', lineHeight: 1.4 }}>
|
||||
<div className="text-text-2 mt-1.5 text-[8px] leading-[1.4]">
|
||||
{scn.description}
|
||||
</div>
|
||||
</div>
|
||||
@ -260,23 +256,18 @@ export function HNSScenarioView() {
|
||||
</div>
|
||||
|
||||
{/* Bottom buttons */}
|
||||
<div className="flex gap-2 border-t border-border" style={{ padding: '10px 14px' }}>
|
||||
<div className="flex gap-2 border-t border-border px-[14px] py-2.5">
|
||||
<button
|
||||
onClick={() => setActiveView(1)}
|
||||
className="flex-1 cursor-pointer font-bold text-status-orange"
|
||||
className="flex-1 cursor-pointer font-bold text-status-orange text-[11px] p-2 rounded-sm"
|
||||
style={{
|
||||
padding: '8px', borderRadius: '6px',
|
||||
background: 'rgba(249,115,22,0.1)', border: '1px solid rgba(249,115,22,0.3)',
|
||||
fontSize: '11px',
|
||||
background: 'rgba(249,115,22,0.1)',
|
||||
border: '1px solid rgba(249,115,22,0.3)',
|
||||
}}
|
||||
>
|
||||
📊 선택 시나리오 비교
|
||||
</button>
|
||||
<button className="cursor-pointer font-semibold text-text-2" style={{
|
||||
padding: '8px 14px', borderRadius: '6px',
|
||||
background: 'var(--bg3)', border: '1px solid var(--bd)',
|
||||
fontSize: '11px',
|
||||
}}>
|
||||
<button className="cursor-pointer font-semibold text-text-2 text-[11px] px-[14px] py-2 rounded-sm bg-bg-3 border border-border">
|
||||
📄 보고서
|
||||
</button>
|
||||
</div>
|
||||
@ -331,20 +322,18 @@ export function HNSScenarioView() {
|
||||
function ScenarioDetail({ scenario }: { scenario: HnsScenario }) {
|
||||
const d = scenario.detail
|
||||
return (
|
||||
<div className="flex-1 overflow-y-auto flex flex-col gap-3.5" style={{ padding: '16px', scrollbarWidth: 'thin' }}>
|
||||
<div className="flex-1 overflow-y-auto flex flex-col gap-3.5 p-4" style={{ scrollbarWidth: 'thin' }}>
|
||||
{/* Hero card */}
|
||||
<div className="relative overflow-hidden rounded-md" style={{
|
||||
<div className="relative overflow-hidden rounded-md p-4" style={{
|
||||
background: 'linear-gradient(135deg, rgba(249,115,22,0.06), rgba(239,68,68,0.04))',
|
||||
border: '1px solid rgba(249,115,22,0.2)',
|
||||
padding: '16px',
|
||||
}}>
|
||||
<div className="absolute top-0 left-0 right-0 h-0.5" style={{ background: 'linear-gradient(90deg, #f97316, #ef4444, #a855f7)' }} />
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<span className="text-sm font-bold">
|
||||
{scenario.id} {scenario.name}
|
||||
</span>
|
||||
<span className="font-bold" style={{
|
||||
padding: '2px 8px', borderRadius: '8px', fontSize: '9px',
|
||||
<span className="font-bold px-2 py-[2px] rounded-lg text-[9px]" style={{
|
||||
background: SEVERITY_STYLE[scenario.severity].bg, color: SEVERITY_STYLE[scenario.severity].color,
|
||||
}}>
|
||||
{scenario.severity}
|
||||
@ -353,7 +342,7 @@ function ScenarioDetail({ scenario }: { scenario: HnsScenario }) {
|
||||
{scenario.datetime}
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(6, 1fr)', gap: '8px' }}>
|
||||
<div className="grid gap-2" style={{ gridTemplateColumns: 'repeat(6, 1fr)' }}>
|
||||
{[
|
||||
{ label: '최대농도', value: d.maxConc, color: '#f87171' },
|
||||
{ label: 'IDLH 반경', value: d.idlhRadius, color: '#f87171' },
|
||||
@ -362,18 +351,18 @@ function ScenarioDetail({ scenario }: { scenario: HnsScenario }) {
|
||||
{ label: '영향인구', value: d.population, color: '#f87171' },
|
||||
{ label: '유출량', value: d.spillAmount, color: 'var(--orange)' },
|
||||
].map((m, i) => (
|
||||
<div key={i} className="text-center rounded-sm" style={{ background: 'rgba(0,0,0,0.15)', padding: '8px' }}>
|
||||
<div className="text-text-3" style={{ fontSize: '8px' }}>{m.label}</div>
|
||||
<div className="text-base font-bold font-mono whitespace-pre-line" style={{ color: m.color, lineHeight: 1.2, marginTop: '2px' }}>{m.value}</div>
|
||||
<div key={i} className="text-center rounded-sm p-2" style={{ background: 'rgba(0,0,0,0.15)' }}>
|
||||
<div className="text-text-3 text-[8px]">{m.label}</div>
|
||||
<div className="text-base font-bold font-mono whitespace-pre-line mt-[2px]" style={{ color: m.color, lineHeight: 1.2 }}>{m.value}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Two-column section */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px' }}>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{/* Threat Zones */}
|
||||
<div className="rounded-md border border-border" style={{ background: 'var(--bg2)', padding: '14px' }}>
|
||||
<div className="rounded-md border border-border bg-bg-2 p-[14px]">
|
||||
<h4 className="text-[12px] font-bold mb-2.5">
|
||||
⚠️ 위험 구역
|
||||
</h4>
|
||||
@ -384,7 +373,7 @@ function ScenarioDetail({ scenario }: { scenario: HnsScenario }) {
|
||||
{ label: 'ERPG-1 (주의권고)', value: scenario.zones.erpg1, color: '#fbbf24' },
|
||||
{ label: 'TWA (작업허용)', value: scenario.zones.twa, color: '#22c55e' },
|
||||
].map((z, i) => (
|
||||
<div key={i} className="flex justify-between items-center" style={{ padding: '6px 8px', background: 'var(--bg0)', borderRadius: '4px', borderLeft: `3px solid ${z.color}` }}>
|
||||
<div key={i} className="flex justify-between items-center bg-bg-0 rounded-sm" style={{ padding: '6px 8px', borderLeft: `3px solid ${z.color}` }}>
|
||||
<span className="text-[10px] text-text-2">{z.label}</span>
|
||||
<span className="text-[11px] font-bold font-mono" style={{ color: z.color }}>{z.value}</span>
|
||||
</div>
|
||||
@ -393,16 +382,13 @@ function ScenarioDetail({ scenario }: { scenario: HnsScenario }) {
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="rounded-md border border-border" style={{ background: 'var(--bg2)', padding: '14px' }}>
|
||||
<div className="rounded-md border border-border bg-bg-2 p-[14px]">
|
||||
<h4 className="text-[12px] font-bold mb-2.5">
|
||||
🛡 대응 권고 사항
|
||||
</h4>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
{scenario.actions.map((action, i) => (
|
||||
<div key={i} className="flex items-start gap-1.5 text-[10px] text-text-2" style={{
|
||||
padding: '5px 8px', background: 'var(--bg0)', borderRadius: '4px',
|
||||
lineHeight: 1.4,
|
||||
}}>
|
||||
<div key={i} className="flex items-start gap-1.5 text-[10px] text-text-2 bg-bg-0 rounded-sm leading-[1.4]" className="py-[5px] px-2">
|
||||
<span className="text-status-orange font-bold shrink-0">•</span>
|
||||
{action}
|
||||
</div>
|
||||
@ -412,11 +398,11 @@ function ScenarioDetail({ scenario }: { scenario: HnsScenario }) {
|
||||
</div>
|
||||
|
||||
{/* Weather */}
|
||||
<div className="rounded-md border border-border" style={{ background: 'var(--bg2)', padding: '14px' }}>
|
||||
<div className="rounded-md border border-border bg-bg-2 p-[14px]">
|
||||
<h4 className="text-[12px] font-bold mb-2.5">
|
||||
🌊 기상 조건
|
||||
</h4>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(6, 1fr)', gap: '8px' }}>
|
||||
<div className="grid gap-2" style={{ gridTemplateColumns: 'repeat(6, 1fr)' }}>
|
||||
{[
|
||||
{ label: '풍향', value: scenario.weather.dir, icon: '🌬' },
|
||||
{ label: '풍속', value: scenario.weather.speed, icon: '💨' },
|
||||
@ -428,7 +414,7 @@ function ScenarioDetail({ scenario }: { scenario: HnsScenario }) {
|
||||
<div key={i} className="text-center p-2 rounded-sm bg-bg-0">
|
||||
<div className="text-sm mb-0.5">{w.icon}</div>
|
||||
<div className="text-[12px] font-bold font-mono">{w.value}</div>
|
||||
<div className="text-text-3 mt-0.5" style={{ fontSize: '8px' }}>{w.label}</div>
|
||||
<div className="text-text-3 mt-0.5 text-[8px]">{w.label}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@ -470,18 +456,18 @@ function ScenarioComparison() {
|
||||
const barX = [30, 70, 110, 150, 190]
|
||||
|
||||
return (
|
||||
<div className="flex-1 overflow-y-auto flex flex-col gap-3.5" style={{ padding: '16px 20px', scrollbarWidth: 'thin', scrollbarColor: 'var(--bdL) transparent' }}>
|
||||
<div className="flex-1 overflow-y-auto flex flex-col gap-3.5 px-5 py-4" style={{ scrollbarWidth: 'thin', scrollbarColor: 'var(--bdL) transparent' }}>
|
||||
{/* Title */}
|
||||
<div className="text-[13px] font-bold mb-0.5">
|
||||
📊 시나리오 비교 — 시간대별 대기확산 지표 추이
|
||||
</div>
|
||||
|
||||
{/* ── Chart 1: 최대 지표면 농도 추이 (Line + Area) ── */}
|
||||
<div className="rounded-md border border-border" style={{ background: 'var(--bg3)', padding: '14px' }}>
|
||||
<div className="rounded-md border border-border bg-bg-3 p-[14px]">
|
||||
<div className="text-[11px] font-bold mb-2.5">
|
||||
최대 지표면 농도 (ppm) 변화 추이
|
||||
</div>
|
||||
<svg viewBox="0 0 500 140" style={{ width: '100%', height: '130px' }}>
|
||||
<svg viewBox="0 0 500 140" className="w-full" style={{ height: '130px' }}>
|
||||
<defs>
|
||||
<linearGradient id="hnsGrad" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stopColor="#f97316" stopOpacity={0.12} />
|
||||
@ -512,13 +498,13 @@ function ScenarioComparison() {
|
||||
</div>
|
||||
|
||||
{/* ── Charts 2 & 3: 2-column grid ── */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '14px' }}>
|
||||
<div className="grid grid-cols-2 gap-[14px]">
|
||||
{/* Chart 2: 위험 반경 변화 (Multi-line) */}
|
||||
<div className="rounded-md border border-border" style={{ background: 'var(--bg3)', padding: '14px' }}>
|
||||
<div className="rounded-md border border-border bg-bg-3 p-[14px]">
|
||||
<div className="text-[11px] font-bold mb-2.5">
|
||||
위험 반경 (km) 변화
|
||||
</div>
|
||||
<svg viewBox="0 0 260 100" style={{ width: '100%', height: '85px' }}>
|
||||
<svg viewBox="0 0 260 100" className="w-full" style={{ height: '85px' }}>
|
||||
<line x1="30" y1="10" x2="30" y2="85" stroke="#21262d" strokeWidth={0.5} />
|
||||
<line x1="30" y1="85" x2="230" y2="85" stroke="#21262d" strokeWidth={0.5} />
|
||||
{/* IDLH line (red solid) */}
|
||||
@ -544,11 +530,11 @@ function ScenarioComparison() {
|
||||
</div>
|
||||
|
||||
{/* Chart 3: 영향 인구 변화 (Bar) */}
|
||||
<div className="rounded-md border border-border" style={{ background: 'var(--bg3)', padding: '14px' }}>
|
||||
<div className="rounded-md border border-border bg-bg-3 p-[14px]">
|
||||
<div className="text-[11px] font-bold mb-2.5">
|
||||
영향 인구 (명) 변화
|
||||
</div>
|
||||
<svg viewBox="0 0 240 100" style={{ width: '100%', height: '85px' }}>
|
||||
<svg viewBox="0 0 240 100" className="w-full" style={{ height: '85px' }}>
|
||||
<line x1="30" y1="10" x2="30" y2="85" stroke="#21262d" strokeWidth={0.5} />
|
||||
<line x1="30" y1="85" x2="230" y2="85" stroke="#21262d" strokeWidth={0.5} />
|
||||
{D.map((d, i) => {
|
||||
@ -571,18 +557,15 @@ function ScenarioComparison() {
|
||||
</div>
|
||||
|
||||
{/* ── Chart 4: 시나리오 비교표 ── */}
|
||||
<div className="rounded-md border border-border overflow-x-auto" style={{ background: 'var(--bg3)', padding: '14px' }}>
|
||||
<div className="rounded-md border border-border overflow-x-auto bg-bg-3 p-[14px]">
|
||||
<div className="text-[11px] font-bold mb-2.5">
|
||||
📋 시나리오 비교표
|
||||
</div>
|
||||
<table className="w-full text-[10px]" style={{ borderCollapse: 'collapse' }}>
|
||||
<table className="w-full text-[10px] border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-bg-0">
|
||||
{['지표', ...D.map(d => `${d.id} (${d.label})`)].map((h, i) => (
|
||||
<th key={i} className="text-text-3 border-b border-border" style={{
|
||||
padding: '8px 10px', textAlign: i === 0 ? 'left' : 'center',
|
||||
fontSize: '9px',
|
||||
}}>
|
||||
<th key={i} className="text-text-3 border-b border-border text-[9px] px-[10px] py-2" style={{ textAlign: i === 0 ? 'left' : 'center' }}>
|
||||
{h}
|
||||
</th>
|
||||
))}
|
||||
@ -591,44 +574,44 @@ function ScenarioComparison() {
|
||||
<tbody>
|
||||
{/* 최대농도 */}
|
||||
<tr className="border-b border-border">
|
||||
<td className="text-text-2" style={{ padding: '6px 10px' }}>최대농도 (ppm)</td>
|
||||
<td className="text-text-2 px-[10px] py-1.5">최대농도 (ppm)</td>
|
||||
{D.map(d => (
|
||||
<td key={d.id} className="text-center font-mono font-semibold" style={{ padding: '6px', color: SEV_COLOR[d.severity] }}>{d.conc}</td>
|
||||
<td key={d.id} className="text-center font-mono font-semibold p-1.5" style={{ color: SEV_COLOR[d.severity] }}>{d.conc}</td>
|
||||
))}
|
||||
</tr>
|
||||
{/* IDLH 반경 */}
|
||||
<tr className="border-b border-border">
|
||||
<td className="text-text-2" style={{ padding: '6px 10px' }}>IDLH 반경 (km)</td>
|
||||
<td className="text-text-2 px-[10px] py-1.5">IDLH 반경 (km)</td>
|
||||
{D.map(d => (
|
||||
<td key={d.id} className="text-center font-mono font-semibold" style={{ padding: '6px', color: d.idlh > 0 ? '#f87171' : '#22c55e' }}>{d.idlh || 0}</td>
|
||||
<td key={d.id} className="text-center font-mono font-semibold p-1.5" style={{ color: d.idlh > 0 ? '#f87171' : '#22c55e' }}>{d.idlh || 0}</td>
|
||||
))}
|
||||
</tr>
|
||||
{/* ERPG-2 반경 */}
|
||||
<tr className="border-b border-border">
|
||||
<td className="text-text-2" style={{ padding: '6px 10px' }}>ERPG-2 반경 (km)</td>
|
||||
<td className="text-text-2 px-[10px] py-1.5">ERPG-2 반경 (km)</td>
|
||||
{D.map(d => (
|
||||
<td key={d.id} className="text-center font-mono font-semibold" style={{ padding: '6px', color: d.erpg2 > 0 ? '#f97316' : '#22c55e' }}>{d.erpg2 || 0}</td>
|
||||
<td key={d.id} className="text-center font-mono font-semibold p-1.5" style={{ color: d.erpg2 > 0 ? '#f97316' : '#22c55e' }}>{d.erpg2 || 0}</td>
|
||||
))}
|
||||
</tr>
|
||||
{/* 영향인구 */}
|
||||
<tr className="border-b border-border">
|
||||
<td className="text-text-2" style={{ padding: '6px 10px' }}>영향인구 (명)</td>
|
||||
<td className="text-text-2 px-[10px] py-1.5">영향인구 (명)</td>
|
||||
{D.map(d => (
|
||||
<td key={d.id} className="text-center font-mono font-semibold" style={{ padding: '6px', color: SEV_COLOR[d.severity] }}>{d.pop.toLocaleString()}</td>
|
||||
<td key={d.id} className="text-center font-mono font-semibold p-1.5" style={{ color: SEV_COLOR[d.severity] }}>{d.pop.toLocaleString()}</td>
|
||||
))}
|
||||
</tr>
|
||||
{/* 풍향/풍속 */}
|
||||
<tr className="border-b border-border">
|
||||
<td className="text-text-2" style={{ padding: '6px 10px' }}>풍향 / 풍속</td>
|
||||
<td className="text-text-2 px-[10px] py-1.5">풍향 / 풍속</td>
|
||||
{D.map(d => (
|
||||
<td key={d.id} className="text-center font-mono text-primary-cyan" style={{ padding: '6px' }}>{d.wind}</td>
|
||||
<td key={d.id} className="text-center font-mono text-primary-cyan p-1.5">{d.wind}</td>
|
||||
))}
|
||||
</tr>
|
||||
{/* 위험 등급 */}
|
||||
<tr>
|
||||
<td className="text-text-2" style={{ padding: '6px 10px' }}>위험 등급</td>
|
||||
<td className="text-text-2 px-[10px] py-1.5">위험 등급</td>
|
||||
{D.map(d => (
|
||||
<td key={d.id} className="text-center font-bold" style={{ padding: '6px', color: SEV_COLOR[d.severity] }}>{d.severity}</td>
|
||||
<td key={d.id} className="text-center font-bold p-1.5" style={{ color: SEV_COLOR[d.severity] }}>{d.severity}</td>
|
||||
))}
|
||||
</tr>
|
||||
</tbody>
|
||||
@ -642,9 +625,8 @@ function ScenarioComparison() {
|
||||
function ScenarioMapOverlay() {
|
||||
return (
|
||||
<div className="flex-1 flex items-center justify-center flex-col gap-4">
|
||||
<div className="flex items-center justify-center rounded-md border border-border text-text-3 text-[13px]" style={{
|
||||
<div className="flex items-center justify-center rounded-md border border-border text-text-3 text-[13px] bg-bg-2" style={{
|
||||
width: '80%', maxWidth: '600px', height: '300px',
|
||||
background: 'var(--bg2)',
|
||||
}}>
|
||||
[시나리오별 확산범위 오버레이 지도]
|
||||
</div>
|
||||
@ -656,7 +638,7 @@ function ScenarioMapOverlay() {
|
||||
{ label: 'T+6h 차단 후', color: '#22c55e' },
|
||||
].map((item, i) => (
|
||||
<div key={i} className="flex items-center gap-1.5">
|
||||
<div className="rounded-full opacity-50" style={{ width: '12px', height: '12px', background: item.color }} />
|
||||
<div className="w-3 h-3 rounded-full opacity-50" style={{ background: item.color }} />
|
||||
<span className="text-[10px] text-text-2">{item.label}</span>
|
||||
</div>
|
||||
))}
|
||||
@ -703,16 +685,10 @@ function NewScenarioModal({ isOpen, onClose, onSubmit }: {
|
||||
|
||||
return (
|
||||
<div ref={backdropRef} className="fixed inset-0 z-[9999] flex items-center justify-center" style={{ background: 'rgba(0,0,0,0.55)', backdropFilter: 'blur(4px)' }}>
|
||||
<div className="flex flex-col overflow-hidden" style={{
|
||||
width: '520px', maxHeight: 'calc(100vh - 80px)',
|
||||
background: 'var(--bg1)', border: '1px solid var(--bd)',
|
||||
borderRadius: '14px',
|
||||
boxShadow: '0 20px 60px rgba(0,0,0,0.5)',
|
||||
}}>
|
||||
<div className="flex flex-col overflow-hidden rounded-[14px] bg-bg-1 border border-border w-[520px] max-h-[calc(100vh-80px)]" style={{ boxShadow: '0 20px 60px rgba(0,0,0,0.5)' }}>
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-3 border-b border-border" style={{ padding: '16px 20px' }}>
|
||||
<div className="flex items-center justify-center text-base" style={{
|
||||
width: '36px', height: '36px', borderRadius: '10px',
|
||||
<div className="flex items-center gap-3 border-b border-border px-5 py-4">
|
||||
<div className="flex items-center justify-center text-base w-9 h-9 rounded-[10px]" style={{
|
||||
background: 'linear-gradient(135deg, rgba(249,115,22,0.2), rgba(239,68,68,0.15))',
|
||||
border: '1px solid rgba(249,115,22,0.3)',
|
||||
}}>🧪</div>
|
||||
@ -724,17 +700,17 @@ function NewScenarioModal({ isOpen, onClose, onSubmit }: {
|
||||
물질·기상·유출조건을 설정하여 새 시나리오를 생성합니다
|
||||
</div>
|
||||
</div>
|
||||
<button onClick={onClose} className="flex items-center justify-center cursor-pointer text-text-3 text-[12px] rounded-sm border border-border" style={{ width: '28px', height: '28px', background: 'var(--bg3)' }}>✕</button>
|
||||
<button onClick={onClose} className="flex items-center justify-center w-7 h-7 cursor-pointer text-text-3 text-[12px] rounded-sm border border-border bg-bg-3">✕</button>
|
||||
</div>
|
||||
|
||||
{/* Scrollable content */}
|
||||
<div className="flex-1 overflow-y-auto flex flex-col gap-3.5" style={{ padding: '16px 20px' }}>
|
||||
<div className="flex-1 overflow-y-auto flex flex-col gap-3.5 px-5 py-4">
|
||||
{/* 기본 정보 */}
|
||||
<ModalSection title="기본 정보">
|
||||
<ModalField label="시나리오명">
|
||||
<input className="prd-i" value={name} onChange={e => setName(e.target.value)} placeholder="예: 풍향 변화 시나리오" />
|
||||
</ModalField>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '8px' }}>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<ModalField label="시간 단계">
|
||||
<select className="prd-i" value={timeStep} onChange={e => setTimeStep(e.target.value)}>
|
||||
{['T+0h', 'T+1h', 'T+3h', 'T+6h', 'T+12h', 'T+24h'].map(t => <option key={t} value={t}>{t}</option>)}
|
||||
@ -754,10 +730,10 @@ function NewScenarioModal({ isOpen, onClose, onSubmit }: {
|
||||
</select>
|
||||
</ModalField>
|
||||
{/* Material properties card */}
|
||||
<div style={{
|
||||
display: 'grid', gridTemplateColumns: 'repeat(5, 1fr)', gap: '4px',
|
||||
padding: '8px', background: 'rgba(249,115,22,0.04)',
|
||||
border: '1px solid rgba(249,115,22,0.15)', borderRadius: '6px',
|
||||
<div className="grid p-2 rounded-sm" style={{
|
||||
gridTemplateColumns: 'repeat(5, 1fr)', gap: '4px',
|
||||
background: 'rgba(249,115,22,0.04)',
|
||||
border: '1px solid rgba(249,115,22,0.15)',
|
||||
}}>
|
||||
{[
|
||||
{ label: 'MW', value: mat.mw },
|
||||
@ -767,12 +743,12 @@ function NewScenarioModal({ isOpen, onClose, onSubmit }: {
|
||||
{ label: 'ERPG-2', value: mat.erpg2 },
|
||||
].map((p, i) => (
|
||||
<div key={i} className="text-center">
|
||||
<div className="text-text-3" style={{ fontSize: '8px' }}>{p.label}</div>
|
||||
<div className="text-text-3 text-[8px]">{p.label}</div>
|
||||
<div className="text-[10px] font-bold text-status-orange font-mono">{p.value}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '8px' }}>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<ModalField label="유출 유형">
|
||||
<select className="prd-i" value={releaseType} onChange={e => setReleaseType(e.target.value)}>
|
||||
<option value="instant">순간 유출</option>
|
||||
@ -783,7 +759,7 @@ function NewScenarioModal({ isOpen, onClose, onSubmit }: {
|
||||
<ModalField label="유출량">
|
||||
<div className="flex gap-1">
|
||||
<input className="prd-i flex-1" type="number" value={amount} onChange={e => setAmount(e.target.value)} />
|
||||
<select className="prd-i" value={unit} onChange={e => setUnit(e.target.value)} style={{ width: '60px' }}>
|
||||
<select className="prd-i w-[60px]" value={unit} onChange={e => setUnit(e.target.value)}>
|
||||
{['t', 'kg', 'm³', 'L'].map(u => <option key={u} value={u}>{u}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
@ -793,7 +769,7 @@ function NewScenarioModal({ isOpen, onClose, onSubmit }: {
|
||||
|
||||
{/* 기상 조건 */}
|
||||
<ModalSection title="기상 조건">
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '8px' }}>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<ModalField label="풍향">
|
||||
<select className="prd-i" value={windDir} onChange={e => setWindDir(e.target.value)}>
|
||||
{['N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW'].map(d => <option key={d} value={d}>{d}</option>)}
|
||||
@ -806,7 +782,7 @@ function NewScenarioModal({ isOpen, onClose, onSubmit }: {
|
||||
<input className="prd-i" type="number" value={temp} onChange={e => setTemp(e.target.value)} step={0.1} />
|
||||
</ModalField>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '8px' }}>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<ModalField label="대기안정도 (Pasquill)">
|
||||
<select className="prd-i" value={stability} onChange={e => setStability(e.target.value)}>
|
||||
{['A (매우 불안정)', 'B (불안정)', 'C (약간 불안정)', 'D (중립)', 'E (약간 안정)', 'F (안정)'].map(s => <option key={s[0]} value={s[0]}>{s}</option>)}
|
||||
@ -827,10 +803,10 @@ function NewScenarioModal({ isOpen, onClose, onSubmit }: {
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex gap-2 border-t border-border" style={{ padding: '14px 20px' }}>
|
||||
<button onClick={onClose} className="flex-1 text-[12px] font-semibold cursor-pointer rounded-md text-text-2" style={{ padding: '10px', background: 'var(--bg3)', border: '1px solid var(--bd)' }}>취소</button>
|
||||
<button onClick={handleSubmit} className="cursor-pointer rounded-md text-[12px] font-bold text-white" style={{
|
||||
flex: 2, padding: '10px',
|
||||
<div className="flex gap-2 border-t border-border px-5 py-[14px]">
|
||||
<button onClick={onClose} className="flex-1 text-[12px] font-semibold cursor-pointer rounded-md text-text-2 p-[10px] bg-bg-3 border border-border">취소</button>
|
||||
<button onClick={handleSubmit} className="cursor-pointer rounded-md text-[12px] font-bold text-white p-[10px]" style={{
|
||||
flex: 2,
|
||||
background: 'linear-gradient(135deg, var(--orange), #ef4444)',
|
||||
border: 'none',
|
||||
opacity: name.trim() ? 1 : 0.5,
|
||||
|
||||
@ -197,8 +197,7 @@ ${styles}
|
||||
placeholder="물질명 또는 CAS 번호 검색..."
|
||||
value={searchQuery}
|
||||
onChange={e => setSearchQuery(e.target.value)}
|
||||
className="rounded-sm border border-border text-[10px] outline-none bg-bg-3 px-3 py-1.5"
|
||||
style={{ width: 200 }}
|
||||
className="rounded-sm border border-border text-[10px] outline-none bg-bg-3 px-3 py-1.5 w-[200px]"
|
||||
/>
|
||||
<button
|
||||
onClick={handleExportPDF}
|
||||
@ -209,7 +208,7 @@ ${styles}
|
||||
</div>
|
||||
|
||||
{/* 서브탭 */}
|
||||
<div className="flex rounded-md border border-border mb-5 bg-bg-3 p-1" style={{ gap: 3 }} data-html2pdf-ignore>
|
||||
<div className="flex rounded-md border border-border mb-5 bg-bg-3 p-1 gap-[3px]" data-html2pdf-ignore>
|
||||
{tabLabels.map((tab, idx) => (
|
||||
<button
|
||||
key={idx}
|
||||
@ -227,12 +226,12 @@ ${styles}
|
||||
<div className="absolute top-0 left-0 right-0" style={{ height: 3, background: 'linear-gradient(90deg,var(--orange),var(--cyan),var(--green),var(--purple))' }} />
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<span className="text-[13px] font-bold text-status-orange">SEBC 해양 거동 분류 체계</span>
|
||||
<span className="text-[8px] font-semibold text-status-orange" style={{ padding: '2px 8px', borderRadius: 10, background: 'rgba(249,115,22,.1)' }}>Standard European Behaviour Classification</span>
|
||||
<span className="text-[8px] font-semibold text-status-orange py-[2px] px-2 rounded-md" style={{ background: 'rgba(249,115,22,.1)' }}>Standard European Behaviour Classification</span>
|
||||
</div>
|
||||
<div className="text-[10px] text-text-2 leading-[1.7] mb-[14px]">
|
||||
HNS 물질이 해양에 유출되었을 때의 <b className="text-status-orange">물리·화학적 거동</b>에 따라 분류하는 국제 표준 체계입니다. 물질의 밀도, 증기압, 용해도에 따라 5개 주요 거동 유형과 혼합 유형으로 구분되며, 각 유형별로 대응 전략이 달라집니다.
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(5,1fr)', gap: 8, marginBottom: 14 }}>
|
||||
<div className="grid mb-[14px]" style={{ gridTemplateColumns: 'repeat(5,1fr)', gap: 8 }}>
|
||||
{/* G: Gas */}
|
||||
<div className="p-3 rounded-md text-center" style={{ background: 'rgba(168,85,247,.06)', border: '1px solid rgba(168,85,247,.2)' }}>
|
||||
<div className="flex items-center justify-center text-[18px] mx-auto mb-2" style={{ width: 36, height: 36, borderRadius: '50%', background: 'rgba(168,85,247,.15)' }}>💨</div>
|
||||
@ -277,7 +276,7 @@ ${styles}
|
||||
{/* 복합 거동 */}
|
||||
<div className="rounded-md p-3 border border-border bg-bg-3">
|
||||
<div className="text-[10px] font-bold mb-2">🔀 복합 거동 유형</div>
|
||||
<div className="text-center text-[8px]" style={{ display: 'grid', gridTemplateColumns: 'repeat(5,1fr)', gap: 6 }}>
|
||||
<div className="grid text-center text-[8px]" style={{ gridTemplateColumns: 'repeat(5,1fr)', gap: 6 }}>
|
||||
<div className="rounded p-1.5 bg-bg-0"><span className="font-bold text-primary-purple">GD</span><br/><span className="text-text-3">기체+용해</span></div>
|
||||
<div className="rounded p-1.5 bg-bg-0"><span className="font-bold text-status-red">ED</span><br/><span className="text-text-3">증발+용해</span></div>
|
||||
<div className="rounded p-1.5 bg-bg-0"><span className="font-bold text-status-yellow">FE</span><br/><span className="text-text-3">부유+증발</span></div>
|
||||
@ -308,7 +307,7 @@ ${styles}
|
||||
</div>
|
||||
|
||||
{/* 주요 HNS 물질 카드 */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 14, marginBottom: 16 }}>
|
||||
<div className="grid grid-cols-2 gap-[14px] mb-4">
|
||||
|
||||
{/* 암모니아 */}
|
||||
{filtered.find(s => s.casNumber === '7664-41-7') && (
|
||||
@ -320,7 +319,7 @@ ${styles}
|
||||
<span className="text-[8px] font-semibold text-status-red px-1.5 py-0.5" style={{ borderRadius: 3, background: 'rgba(239,68,68,.1)' }}>독성</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-[8px] mb-2" style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 4 }}>
|
||||
<div className="grid grid-cols-2 gap-1 text-[8px] mb-2">
|
||||
<div className="rounded bg-bg-0 px-1.5 py-1"><span className="text-text-3">CAS:</span> <span className="font-mono">7664-41-7</span></div>
|
||||
<div className="rounded bg-bg-0 px-1.5 py-1"><span className="text-text-3">분자량:</span> <span className="font-mono">17.03</span></div>
|
||||
<div className="rounded bg-bg-0 px-1.5 py-1"><span className="text-text-3">끓는점:</span> <span className="font-mono text-primary-cyan">-33.4°C</span></div>
|
||||
@ -328,7 +327,7 @@ ${styles}
|
||||
<div className="rounded bg-bg-0 px-1.5 py-1"><span className="text-text-3">인화점:</span> <span className="font-mono">N/A (불연)</span></div>
|
||||
<div className="rounded bg-bg-0 px-1.5 py-1"><span className="text-text-3">용해도:</span> <span className="font-mono text-primary-cyan">매우 높음</span></div>
|
||||
</div>
|
||||
<div className="text-center text-[7px]" style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 4 }}>
|
||||
<div className="grid text-center text-[7px]" style={{ gridTemplateColumns: '1fr 1fr 1fr', gap: 4 }}>
|
||||
<div style={{ padding: 4, background: 'rgba(34,197,94,.06)', border: '1px solid rgba(34,197,94,.12)', borderRadius: 3 }}><span className="text-status-green">AEGL-2</span><br/><b>160 ppm</b></div>
|
||||
<div style={{ padding: 4, background: 'rgba(249,115,22,.06)', border: '1px solid rgba(249,115,22,.12)', borderRadius: 3 }}><span className="text-status-orange">ERPG-2</span><br/><b>150 ppm</b></div>
|
||||
<div style={{ padding: 4, background: 'rgba(239,68,68,.06)', border: '1px solid rgba(239,68,68,.12)', borderRadius: 3 }}><span className="text-status-red">IDLH</span><br/><b>300 ppm</b></div>
|
||||
@ -347,7 +346,7 @@ ${styles}
|
||||
<span className="text-[8px] font-semibold text-status-orange px-1.5 py-0.5" style={{ borderRadius: 3, background: 'rgba(249,115,22,.1)' }}>인화성</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-[8px] mb-2" style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 4 }}>
|
||||
<div className="grid grid-cols-2 gap-1 text-[8px] mb-2">
|
||||
<div className="rounded bg-bg-0 px-1.5 py-1"><span className="text-text-3">CAS:</span> <span className="font-mono">67-56-1</span></div>
|
||||
<div className="rounded bg-bg-0 px-1.5 py-1"><span className="text-text-3">분자량:</span> <span className="font-mono">32.04</span></div>
|
||||
<div className="rounded bg-bg-0 px-1.5 py-1"><span className="text-text-3">끓는점:</span> <span className="font-mono text-primary-cyan">64.7°C</span></div>
|
||||
@ -355,7 +354,7 @@ ${styles}
|
||||
<div className="rounded bg-bg-0 px-1.5 py-1"><span className="text-text-3">인화점:</span> <span className="font-mono text-status-orange">11°C</span></div>
|
||||
<div className="rounded bg-bg-0 px-1.5 py-1"><span className="text-text-3">용해도:</span> <span className="font-mono text-primary-cyan">완전 혼화</span></div>
|
||||
</div>
|
||||
<div className="text-center text-[7px]" style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 4 }}>
|
||||
<div className="grid text-center text-[7px]" style={{ gridTemplateColumns: '1fr 1fr 1fr', gap: 4 }}>
|
||||
<div style={{ padding: 4, background: 'rgba(34,197,94,.06)', border: '1px solid rgba(34,197,94,.12)', borderRadius: 3 }}><span className="text-status-green">AEGL-2</span><br/><b>2,100 ppm</b></div>
|
||||
<div style={{ padding: 4, background: 'rgba(249,115,22,.06)', border: '1px solid rgba(249,115,22,.12)', borderRadius: 3 }}><span className="text-status-orange">ERPG-2</span><br/><b>1,000 ppm</b></div>
|
||||
<div style={{ padding: 4, background: 'rgba(239,68,68,.06)', border: '1px solid rgba(239,68,68,.12)', borderRadius: 3 }}><span className="text-status-red">IDLH</span><br/><b>6,000 ppm</b></div>
|
||||
@ -374,7 +373,7 @@ ${styles}
|
||||
<span className="text-[8px] font-semibold text-status-red px-1.5 py-0.5" style={{ borderRadius: 3, background: 'rgba(239,68,68,.1)' }}>폭발</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-[8px] mb-2" style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 4 }}>
|
||||
<div className="grid grid-cols-2 gap-1 text-[8px] mb-2">
|
||||
<div className="rounded bg-bg-0 px-1.5 py-1"><span className="text-text-3">CAS:</span> <span className="font-mono">1333-74-0</span></div>
|
||||
<div className="rounded bg-bg-0 px-1.5 py-1"><span className="text-text-3">분자량:</span> <span className="font-mono">2.016</span></div>
|
||||
<div className="rounded bg-bg-0 px-1.5 py-1"><span className="text-text-3">끓는점:</span> <span className="font-mono text-primary-cyan">-252.9°C</span></div>
|
||||
@ -396,7 +395,7 @@ ${styles}
|
||||
<span className="text-[8px] font-semibold text-status-orange px-1.5 py-0.5" style={{ borderRadius: 3, background: 'rgba(249,115,22,.1)' }}>인화/폭발</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-[8px] mb-2" style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 4 }}>
|
||||
<div className="grid grid-cols-2 gap-1 text-[8px] mb-2">
|
||||
<div className="rounded bg-bg-0 px-1.5 py-1"><span className="text-text-3">CAS:</span> <span className="font-mono">74-82-8</span></div>
|
||||
<div className="rounded bg-bg-0 px-1.5 py-1"><span className="text-text-3">분자량:</span> <span className="font-mono">16.04</span></div>
|
||||
<div className="rounded bg-bg-0 px-1.5 py-1"><span className="text-text-3">끓는점:</span> <span className="font-mono text-primary-cyan">-161.5°C</span></div>
|
||||
@ -418,7 +417,7 @@ ${styles}
|
||||
<span className="text-[8px] font-semibold text-status-red px-1.5 py-0.5" style={{ borderRadius: 3, background: 'rgba(239,68,68,.1)' }}>독성</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-[8px] mb-2" style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 4 }}>
|
||||
<div className="grid grid-cols-2 gap-1 text-[8px] mb-2">
|
||||
<div className="rounded bg-bg-0 px-1.5 py-1"><span className="text-text-3">CAS:</span> <span className="font-mono">108-95-2</span></div>
|
||||
<div className="rounded bg-bg-0 px-1.5 py-1"><span className="text-text-3">분자량:</span> <span className="font-mono">94.11</span></div>
|
||||
<div className="rounded bg-bg-0 px-1.5 py-1"><span className="text-text-3">끓는점:</span> <span className="font-mono">181.7°C</span></div>
|
||||
@ -440,7 +439,7 @@ ${styles}
|
||||
<span className="text-[8px] font-semibold text-status-orange px-1.5 py-0.5" style={{ borderRadius: 3, background: 'rgba(249,115,22,.1)' }}>인화성</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-[8px] mb-2" style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 4 }}>
|
||||
<div className="grid grid-cols-2 gap-1 text-[8px] mb-2">
|
||||
<div className="rounded bg-bg-0 px-1.5 py-1"><span className="text-text-3">CAS:</span> <span className="font-mono">108-88-3</span></div>
|
||||
<div className="rounded bg-bg-0 px-1.5 py-1"><span className="text-text-3">분자량:</span> <span className="font-mono">92.14</span></div>
|
||||
<div className="rounded bg-bg-0 px-1.5 py-1"><span className="text-text-3">끓는점:</span> <span className="font-mono">110.6°C</span></div>
|
||||
@ -458,7 +457,7 @@ ${styles}
|
||||
{/* ═══ MAT PANEL 2: 위험도 기준 ═══ */}
|
||||
{activeTab === 2 && (
|
||||
<div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16, marginBottom: 16 }}>
|
||||
<div className="grid grid-cols-2 gap-4 mb-4">
|
||||
{/* AEGL 기준 */}
|
||||
<div className="rounded-[10px] p-4 border border-border bg-bg-3" style={{ borderTop: '3px solid var(--green)' }}>
|
||||
<div className="text-[13px] font-bold text-status-green mb-[10px]">🟢 AEGL (Acute Exposure Guideline Level)</div>
|
||||
@ -504,22 +503,22 @@ ${styles}
|
||||
{/* 물질별 위험도 비교표 */}
|
||||
<div className="rounded-[10px] p-4 border border-border bg-bg-3">
|
||||
<div className="text-xs font-bold mb-3">📊 주요 HNS 물질별 위험도 기준 비교 (ppm)</div>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 9 }}>
|
||||
<table className="w-full border-collapse text-[9px]">
|
||||
<thead>
|
||||
<tr style={{ background: 'rgba(168,85,247,.06)' }}>
|
||||
<th style={{ padding: '7px 8px', textAlign: 'left', borderBottom: '2px solid var(--bdL)' }} className="text-primary-purple">물질</th>
|
||||
<th style={{ padding: '7px 8px', textAlign: 'center', borderBottom: '2px solid var(--bdL)' }} className="text-status-green">AEGL-1</th>
|
||||
<th style={{ padding: '7px 8px', textAlign: 'center', borderBottom: '2px solid var(--bdL)' }} className="text-status-orange">AEGL-2</th>
|
||||
<th style={{ padding: '7px 8px', textAlign: 'center', borderBottom: '2px solid var(--bdL)' }} className="text-status-red">AEGL-3</th>
|
||||
<th style={{ padding: '7px 8px', textAlign: 'center', borderBottom: '2px solid var(--bdL)' }} className="text-status-orange">ERPG-2</th>
|
||||
<th style={{ padding: '7px 8px', textAlign: 'center', borderBottom: '2px solid var(--bdL)' }} className="text-status-red">IDLH</th>
|
||||
<th style={{ padding: '7px 8px', textAlign: 'center', borderBottom: '2px solid var(--bdL)' }} className="text-status-orange">LFL(%)</th>
|
||||
<th style={{ padding: '7px 8px', textAlign: 'center', borderBottom: '2px solid var(--bdL)' }} className="text-text-2">SEBC</th>
|
||||
<th className="text-primary-purple text-left" style={{ padding: '7px 8px', borderBottom: '2px solid var(--bdL)' }}>물질</th>
|
||||
<th className="text-status-green text-center" style={{ padding: '7px 8px', borderBottom: '2px solid var(--bdL)' }}>AEGL-1</th>
|
||||
<th className="text-status-orange text-center" style={{ padding: '7px 8px', borderBottom: '2px solid var(--bdL)' }}>AEGL-2</th>
|
||||
<th className="text-status-red text-center" style={{ padding: '7px 8px', borderBottom: '2px solid var(--bdL)' }}>AEGL-3</th>
|
||||
<th className="text-status-orange text-center" style={{ padding: '7px 8px', borderBottom: '2px solid var(--bdL)' }}>ERPG-2</th>
|
||||
<th className="text-status-red text-center" style={{ padding: '7px 8px', borderBottom: '2px solid var(--bdL)' }}>IDLH</th>
|
||||
<th className="text-status-orange text-center" style={{ padding: '7px 8px', borderBottom: '2px solid var(--bdL)' }}>LFL(%)</th>
|
||||
<th className="text-text-2 text-center" style={{ padding: '7px 8px', borderBottom: '2px solid var(--bdL)' }}>SEBC</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr style={{ borderBottom: '1px solid rgba(255,255,255,.04)' }}>
|
||||
<td style={{ padding: '6px 8px' }} className="font-semibold text-primary-purple">NH₃ 암모니아</td>
|
||||
<td className="font-semibold text-primary-purple" className="py-1.5 px-2">NH₃ 암모니아</td>
|
||||
<td className="text-center font-mono text-status-green">30</td>
|
||||
<td className="text-center font-mono text-status-orange">160</td>
|
||||
<td className="text-center font-mono text-status-red">1,100</td>
|
||||
@ -529,7 +528,7 @@ ${styles}
|
||||
<td className="text-center font-semibold text-primary-purple">G/GD</td>
|
||||
</tr>
|
||||
<tr style={{ borderBottom: '1px solid rgba(255,255,255,.04)' }}>
|
||||
<td style={{ padding: '6px 8px' }} className="font-semibold text-primary-cyan">CH₃OH 메탄올</td>
|
||||
<td className="font-semibold text-primary-cyan" className="py-1.5 px-2">CH₃OH 메탄올</td>
|
||||
<td className="text-center font-mono text-status-green">530</td>
|
||||
<td className="text-center font-mono text-status-orange">2,100</td>
|
||||
<td className="text-center font-mono text-status-red">14,000</td>
|
||||
@ -539,7 +538,7 @@ ${styles}
|
||||
<td className="text-center font-semibold text-primary-cyan">ED</td>
|
||||
</tr>
|
||||
<tr style={{ borderBottom: '1px solid rgba(255,255,255,.04)' }}>
|
||||
<td style={{ padding: '6px 8px' }} className="font-semibold text-status-red">H₂ 수소</td>
|
||||
<td className="font-semibold text-status-red" className="py-1.5 px-2">H₂ 수소</td>
|
||||
<td className="text-center text-text-3">-</td>
|
||||
<td className="text-center text-text-3">-</td>
|
||||
<td className="text-center text-text-3">-</td>
|
||||
@ -549,7 +548,7 @@ ${styles}
|
||||
<td className="text-center font-semibold text-primary-purple">G</td>
|
||||
</tr>
|
||||
<tr style={{ borderBottom: '1px solid rgba(255,255,255,.04)' }}>
|
||||
<td style={{ padding: '6px 8px' }} className="font-semibold text-status-orange">CH₄ LNG</td>
|
||||
<td className="font-semibold text-status-orange" className="py-1.5 px-2">CH₄ LNG</td>
|
||||
<td className="text-center text-text-3">-</td>
|
||||
<td className="text-center text-text-3">-</td>
|
||||
<td className="text-center text-text-3">-</td>
|
||||
@ -559,7 +558,7 @@ ${styles}
|
||||
<td className="text-center font-semibold text-primary-purple">G</td>
|
||||
</tr>
|
||||
<tr style={{ borderBottom: '1px solid rgba(255,255,255,.04)' }}>
|
||||
<td style={{ padding: '6px 8px' }} className="font-semibold text-status-green">C₆H₅OH 페놀</td>
|
||||
<td className="font-semibold text-status-green" className="py-1.5 px-2">C₆H₅OH 페놀</td>
|
||||
<td className="text-center font-mono text-status-green">19</td>
|
||||
<td className="text-center font-mono text-status-orange">29</td>
|
||||
<td className="text-center font-mono text-status-red">57</td>
|
||||
@ -569,7 +568,7 @@ ${styles}
|
||||
<td className="text-center font-semibold text-status-green">S/SD</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style={{ padding: '6px 8px' }} className="font-semibold text-status-yellow">C₇H₈ 톨루엔</td>
|
||||
<td className="font-semibold text-status-yellow" className="py-1.5 px-2">C₇H₈ 톨루엔</td>
|
||||
<td className="text-center font-mono text-status-green">67</td>
|
||||
<td className="text-center font-mono text-status-orange">560</td>
|
||||
<td className="text-center font-mono text-status-red">3,700</td>
|
||||
@ -625,22 +624,22 @@ ${styles}
|
||||
</select>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 9 }}>
|
||||
<table className="w-full border-collapse text-[9px]">
|
||||
<thead>
|
||||
<tr style={{ background: 'rgba(249,115,22,.06)' }}>
|
||||
<th style={{ padding: '7px 6px', textAlign: 'left', borderBottom: '2px solid var(--bdL)', width: 28 }} className="text-status-orange">No.</th>
|
||||
<th style={{ padding: '7px 6px', textAlign: 'left', borderBottom: '2px solid var(--bdL)' }} className="text-status-orange">약자/제품명</th>
|
||||
<th style={{ padding: '7px 6px', textAlign: 'left', borderBottom: '2px solid var(--bdL)' }} className="text-text-2">영문명</th>
|
||||
<th style={{ padding: '7px 6px', textAlign: 'left', borderBottom: '2px solid var(--bdL)' }} className="text-text-2">영문명 동의어</th>
|
||||
<th style={{ padding: '7px 6px', textAlign: 'left', borderBottom: '2px solid var(--bdL)' }} className="text-primary-cyan">국문명</th>
|
||||
<th style={{ padding: '7px 6px', textAlign: 'left', borderBottom: '2px solid var(--bdL)', fontSize: 8 }} className="text-text-2">국문 동의어 / 주요 사용처</th>
|
||||
<th style={{ padding: '7px 6px', textAlign: 'center', borderBottom: '2px solid var(--bdL)', width: 60 }} className="text-text-2">UN번호</th>
|
||||
<th style={{ padding: '7px 6px', textAlign: 'center', borderBottom: '2px solid var(--bdL)', width: 72 }} className="text-text-2">CAS번호</th>
|
||||
<th className="text-status-orange text-left" style={{ padding: '7px 6px', borderBottom: '2px solid var(--bdL)', width: 28 }}>No.</th>
|
||||
<th className="text-status-orange text-left" style={{ padding: '7px 6px', borderBottom: '2px solid var(--bdL)' }}>약자/제품명</th>
|
||||
<th className="text-text-2 text-left" style={{ padding: '7px 6px', borderBottom: '2px solid var(--bdL)' }}>영문명</th>
|
||||
<th className="text-text-2 text-left" style={{ padding: '7px 6px', borderBottom: '2px solid var(--bdL)' }}>영문명 동의어</th>
|
||||
<th className="text-primary-cyan text-left" style={{ padding: '7px 6px', borderBottom: '2px solid var(--bdL)' }}>국문명</th>
|
||||
<th className="text-text-2 text-left text-[8px]" style={{ padding: '7px 6px', borderBottom: '2px solid var(--bdL)' }}>국문 동의어 / 주요 사용처</th>
|
||||
<th className="text-text-2 text-center" style={{ padding: '7px 6px', borderBottom: '2px solid var(--bdL)', width: 60 }}>UN번호</th>
|
||||
<th className="text-text-2 text-center" style={{ padding: '7px 6px', borderBottom: '2px solid var(--bdL)', width: 72 }}>CAS번호</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{hmsLoading ? (
|
||||
<tr><td colSpan={8} style={{ padding: '20px 8px' }} className="text-center text-text-3">검색 중...</td></tr>
|
||||
<tr><td colSpan={8} className="text-center text-text-3 py-5 px-2">검색 중...</td></tr>
|
||||
) : hmsPageData.length > 0 ? hmsPageData.map((s: HNSSearchSubstance, idx: number) => {
|
||||
const isSel = hmsSelectedId === s.id
|
||||
return (
|
||||
@ -649,18 +648,18 @@ ${styles}
|
||||
onMouseOver={e => { if (!isSel) e.currentTarget.style.background = 'rgba(249,115,22,.03)' }}
|
||||
onMouseOut={e => { if (!isSel) e.currentTarget.style.background = '' }}
|
||||
>
|
||||
<td style={{ padding: 6 }} className="font-mono text-text-3">{(hmsPage - 1) * HMS_PER_PAGE + idx + 1}</td>
|
||||
<td style={{ padding: 6 }} className="font-semibold font-mono text-status-orange">{s.abbreviation}</td>
|
||||
<td style={{ padding: 6 }}>{s.nameEn}</td>
|
||||
<td style={{ padding: 6, fontSize: 8 }} className="text-text-3">{s.synonymsEn}</td>
|
||||
<td style={{ padding: 6 }} className="font-semibold"><span className="text-primary-cyan underline cursor-pointer" onClick={e => { e.stopPropagation(); setHmsSelectedId(s.id); setHmsDetailTab(0) }}>{s.nameKr}</span></td>
|
||||
<td style={{ padding: 6, fontSize: 8 }} className="text-text-3">{s.synonymsKr}</td>
|
||||
<td className="font-mono text-text-3 p-1.5">{(hmsPage - 1) * HMS_PER_PAGE + idx + 1}</td>
|
||||
<td className="font-semibold font-mono text-status-orange p-1.5">{s.abbreviation}</td>
|
||||
<td className="p-1.5">{s.nameEn}</td>
|
||||
<td className="text-text-3 text-[8px] p-1.5">{s.synonymsEn}</td>
|
||||
<td className="font-semibold p-1.5"><span className="text-primary-cyan underline cursor-pointer" onClick={e => { e.stopPropagation(); setHmsSelectedId(s.id); setHmsDetailTab(0) }}>{s.nameKr}</span></td>
|
||||
<td className="text-text-3 text-[8px] p-1.5">{s.synonymsKr}</td>
|
||||
<td className="text-center font-mono">{s.unNumber}</td>
|
||||
<td className="text-center font-mono">{s.casNumber}</td>
|
||||
</tr>
|
||||
)
|
||||
}) : (
|
||||
<tr><td colSpan={8} style={{ padding: '20px 8px' }} className="text-center text-text-3">검색 결과가 없습니다.</td></tr>
|
||||
<tr><td colSpan={8} className="text-center text-text-3 py-5 px-2">검색 결과가 없습니다.</td></tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
@ -695,7 +694,7 @@ function HmsDetailPanel({ substance: s, activeTab, onTabChange }: { substance: H
|
||||
const sebcColor = s.sebc.startsWith('G') ? 'var(--purple)' : s.sebc.startsWith('E') ? 'var(--red)' : s.sebc.startsWith('F') ? 'var(--yellow)' : s.sebc.startsWith('D') ? 'var(--cyan)' : s.sebc.startsWith('S') ? 'var(--green)' : 'var(--t2)'
|
||||
|
||||
return (
|
||||
<div className="rounded-[10px] overflow-hidden" style={{ background: 'var(--bg3)', border: '1px solid rgba(6,182,212,.25)', boxShadow: '0 4px 20px rgba(6,182,212,.08)' }}>
|
||||
<div className="rounded-[10px] overflow-hidden bg-bg-3" style={{ border: '1px solid rgba(6,182,212,.25)', boxShadow: '0 4px 20px rgba(6,182,212,.08)' }}>
|
||||
{/* Tab Navigation */}
|
||||
<div className="flex border-b border-border" style={{ background: 'linear-gradient(90deg,rgba(6,182,212,.06),rgba(249,115,22,.04))' }}>
|
||||
{tabLabels.map((label, i) => (
|
||||
@ -712,20 +711,20 @@ function HmsDetailPanel({ substance: s, activeTab, onTabChange }: { substance: H
|
||||
<div className="flex-1">
|
||||
<div style={{ fontSize: 18, fontWeight: 800 }}>{s.nameKr} <span className="text-xs font-normal text-text-3">({s.nameEn})</span></div>
|
||||
<div className="flex flex-wrap gap-1.5 mt-1.5">
|
||||
<span className="text-[9px] font-semibold font-mono text-primary-cyan rounded" style={{ padding: '2px 8px', background: 'rgba(6,182,212,.1)', border: '1px solid rgba(6,182,212,.2)' }}>CAS: {s.casNumber}</span>
|
||||
<span className="text-[9px] font-semibold font-mono text-status-orange rounded" style={{ padding: '2px 8px', background: 'rgba(249,115,22,.1)', border: '1px solid rgba(249,115,22,.2)' }}>UN: {s.unNumber}</span>
|
||||
<span className="text-[9px] font-semibold text-primary-purple rounded" style={{ padding: '2px 8px', background: 'rgba(168,85,247,.1)', border: '1px solid rgba(168,85,247,.2)' }}>운송방법: {s.transportMethod}</span>
|
||||
<span className="text-[9px] font-semibold text-status-green rounded" style={{ padding: '2px 8px', background: 'rgba(34,197,94,.1)', border: '1px solid rgba(34,197,94,.2)' }}>SEBC: {s.sebc}</span>
|
||||
<span className="text-[9px] font-semibold font-mono text-primary-cyan rounded py-[2px] px-2" style={{ background: 'rgba(6,182,212,.1)', border: '1px solid rgba(6,182,212,.2)' }}>CAS: {s.casNumber}</span>
|
||||
<span className="text-[9px] font-semibold font-mono text-status-orange rounded py-[2px] px-2" style={{ background: 'rgba(249,115,22,.1)', border: '1px solid rgba(249,115,22,.2)' }}>UN: {s.unNumber}</span>
|
||||
<span className="text-[9px] font-semibold text-primary-purple rounded py-[2px] px-2" style={{ background: 'rgba(168,85,247,.1)', border: '1px solid rgba(168,85,247,.2)' }}>운송방법: {s.transportMethod}</span>
|
||||
<span className="text-[9px] font-semibold text-status-green rounded py-[2px] px-2" style={{ background: 'rgba(34,197,94,.1)', border: '1px solid rgba(34,197,94,.2)' }}>SEBC: {s.sebc}</span>
|
||||
</div>
|
||||
<div className="text-[9px] text-text-3 mt-1"><b>유사명:</b> {s.synonymsKr} | <b>특성:</b> {s.hazardClass}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 14 }}>
|
||||
<div className="grid grid-cols-2 gap-[14px]">
|
||||
{/* Left: 물리·화학적 특성 */}
|
||||
<div>
|
||||
<div className="text-[11px] font-bold text-status-orange mb-2">⚗️ 물리·화학적 특성</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 4, fontSize: 9 }}>
|
||||
<div className="grid grid-cols-2 gap-1 text-[9px]">
|
||||
{([
|
||||
['용도', s.usage, 'var(--cyan)'],
|
||||
['상태', s.state, 'var(--cyan)'],
|
||||
@ -787,7 +786,7 @@ function HmsDetailPanel({ substance: s, activeTab, onTabChange }: { substance: H
|
||||
{/* TAB 1: 방제거리·PPE·MSDS */}
|
||||
{activeTab === 1 && (
|
||||
<div className="p-4">
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 14 }}>
|
||||
<div className="grid grid-cols-2 gap-[14px]">
|
||||
{/* 방제거리 */}
|
||||
<div className="rounded-md overflow-hidden" style={{ border: '1px solid rgba(239,68,68,.2)' }}>
|
||||
<div style={{ padding: '10px 12px', background: 'linear-gradient(135deg,rgba(239,68,68,.08),transparent)', borderBottom: '1px solid rgba(239,68,68,.12)' }}>
|
||||
@ -796,15 +795,15 @@ function HmsDetailPanel({ substance: s, activeTab, onTabChange }: { substance: H
|
||||
<div className="p-3 flex flex-col gap-2">
|
||||
<div className="rounded-sm" style={{ padding: 10, background: 'rgba(249,115,22,.04)', border: '1px solid rgba(249,115,22,.12)' }}>
|
||||
<div className="text-[10px] font-bold text-status-orange mb-1">🔥 화재 시</div>
|
||||
<div className="text-[9px] text-text-2" style={{ lineHeight: 1.6 }}>격리거리: <b className="text-status-red">{s.responseDistanceFire}</b> 이상</div>
|
||||
<div className="text-[9px] text-text-2" className="leading-[1.6]">격리거리: <b className="text-status-red">{s.responseDistanceFire}</b> 이상</div>
|
||||
</div>
|
||||
<div className="rounded-sm" style={{ padding: 10, background: 'rgba(168,85,247,.04)', border: '1px solid rgba(168,85,247,.12)' }}>
|
||||
<div className="text-[10px] font-bold text-primary-purple mb-1">💨 유출 시 (비화재)</div>
|
||||
<div className="text-[9px] text-text-2" style={{ lineHeight: 1.6 }}>주간 방호활동거리: <b className="text-primary-purple">{s.responseDistanceSpillDay}</b><br />야간 방호활동거리: <b className="text-status-red">{s.responseDistanceSpillNight}</b></div>
|
||||
<div className="text-[9px] text-text-2" className="leading-[1.6]">주간 방호활동거리: <b className="text-primary-purple">{s.responseDistanceSpillDay}</b><br />야간 방호활동거리: <b className="text-status-red">{s.responseDistanceSpillNight}</b></div>
|
||||
</div>
|
||||
<div className="rounded-sm" style={{ padding: 10, background: 'rgba(6,182,212,.04)', border: '1px solid rgba(6,182,212,.12)' }}>
|
||||
<div className="text-[10px] font-bold text-primary-cyan mb-1">🌊 해상 유출 시</div>
|
||||
<div className="text-[9px] text-text-2" style={{ lineHeight: 1.6 }}>{s.marineResponse}</div>
|
||||
<div className="text-[9px] text-text-2" className="leading-[1.6]">{s.marineResponse}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -815,7 +814,7 @@ function HmsDetailPanel({ substance: s, activeTab, onTabChange }: { substance: H
|
||||
<div style={{ padding: '10px 12px', background: 'linear-gradient(135deg,rgba(34,197,94,.08),transparent)', borderBottom: '1px solid rgba(34,197,94,.12)' }}>
|
||||
<div className="text-xs font-bold text-status-green">🛡 개인보호장구 (PPE) 추천</div>
|
||||
</div>
|
||||
<div style={{ padding: 12, display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 6, fontSize: 9 }}>
|
||||
<div className="grid grid-cols-2 gap-1.5 text-[9px] p-3">
|
||||
<div className="text-center rounded" style={{ padding: 8, background: 'rgba(239,68,68,.04)', border: '1px solid rgba(239,68,68,.1)' }}>
|
||||
<div className="text-base mb-[3px]">🧑🚒</div>
|
||||
<div className="font-bold text-status-red">근거리</div>
|
||||
@ -834,7 +833,7 @@ function HmsDetailPanel({ substance: s, activeTab, onTabChange }: { substance: H
|
||||
<div className="text-xs font-bold" style={{ color: '#60a5fa' }}>📄 MSDS 주요 정보</div>
|
||||
<button className="text-[8px] font-semibold cursor-pointer rounded" style={{ padding: '3px 10px', background: 'rgba(59,130,246,.1)', border: '1px solid rgba(59,130,246,.2)', color: '#60a5fa' }}>📥 전문 다운로드</button>
|
||||
</div>
|
||||
<div className="text-[8px] text-text-2 leading-[1.7]" style={{ padding: 10 }}>
|
||||
<div className="text-[8px] text-text-2 leading-[1.7] p-2.5">
|
||||
<b>§2 유해성·위험성:</b> {s.msds.hazard}<br />
|
||||
<b>§4 응급조치:</b> {s.msds.firstAid}<br />
|
||||
<b>§5 소화방법:</b> {s.msds.fireFighting}<br />
|
||||
@ -851,7 +850,7 @@ function HmsDetailPanel({ substance: s, activeTab, onTabChange }: { substance: H
|
||||
{/* TAB 2: IBC CODE·EmS 대응 */}
|
||||
{activeTab === 2 && (
|
||||
<div className="p-4">
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 14 }}>
|
||||
<div className="grid grid-cols-2 gap-[14px]">
|
||||
{/* IBC CODE */}
|
||||
<div className="rounded-md overflow-hidden" style={{ border: '1px solid rgba(249,115,22,.2)' }}>
|
||||
<div style={{ padding: '10px 12px', background: 'linear-gradient(135deg,rgba(249,115,22,.08),transparent)', borderBottom: '1px solid rgba(249,115,22,.12)' }}>
|
||||
@ -899,15 +898,15 @@ function HmsDetailPanel({ substance: s, activeTab, onTabChange }: { substance: H
|
||||
<div className="p-3 flex flex-col gap-2 text-[9px]">
|
||||
<div className="rounded-sm" style={{ padding: 8, background: 'rgba(239,68,68,.04)', border: '1px solid rgba(239,68,68,.1)' }}>
|
||||
<div className="font-bold text-status-red mb-[3px]">🔥 화재 대응</div>
|
||||
<div className="text-text-2" style={{ lineHeight: 1.6 }}>{s.emsFire}</div>
|
||||
<div className="text-text-2" className="leading-[1.6]">{s.emsFire}</div>
|
||||
</div>
|
||||
<div className="rounded-sm" style={{ padding: 8, background: 'rgba(168,85,247,.04)', border: '1px solid rgba(168,85,247,.1)' }}>
|
||||
<div className="font-bold text-primary-purple mb-[3px]">💧 유출 대응</div>
|
||||
<div className="text-text-2" style={{ lineHeight: 1.6 }}>{s.emsSpill}</div>
|
||||
<div className="text-text-2" className="leading-[1.6]">{s.emsSpill}</div>
|
||||
</div>
|
||||
<div className="rounded-sm" style={{ padding: 8, background: 'rgba(6,182,212,.04)', border: '1px solid rgba(6,182,212,.1)' }}>
|
||||
<div className="font-bold text-primary-cyan mb-[3px]">🏥 응급조치</div>
|
||||
<div className="text-text-2" style={{ lineHeight: 1.6 }}>{s.emsFirstAid}</div>
|
||||
<div className="text-text-2" className="leading-[1.6]">{s.emsFirstAid}</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<button className="text-[10px] font-semibold cursor-pointer rounded-sm text-status-red" style={{ padding: '6px 16px', background: 'rgba(239,68,68,.1)', border: '1px solid rgba(239,68,68,.2)' }}>📋 EmS 전문 보기</button>
|
||||
@ -920,21 +919,21 @@ function HmsDetailPanel({ substance: s, activeTab, onTabChange }: { substance: H
|
||||
|
||||
{/* TAB 3: 화물적부도·항구별 코드 */}
|
||||
{activeTab === 3 && (
|
||||
<div style={{ padding: 16 }}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 14 }}>
|
||||
<div className="p-4">
|
||||
<div className="grid grid-cols-2 gap-[14px]">
|
||||
{/* 화물적부도 */}
|
||||
<div style={{ border: '1px solid rgba(168,85,247,.2)', borderRadius: 8, overflow: 'hidden' }}>
|
||||
<div className="rounded-lg overflow-hidden" style={{ border: '1px solid rgba(168,85,247,.2)' }}>
|
||||
<div style={{ padding: '10px 12px', background: 'linear-gradient(135deg,rgba(168,85,247,.08),transparent)', borderBottom: '1px solid rgba(168,85,247,.12)' }}>
|
||||
<div style={{ fontSize: 12, fontWeight: 700, color: 'var(--purple)' }}>📋 화물적부도 화물코드</div>
|
||||
<div style={{ fontSize: 8, color: 'var(--t3)' }}>클릭 시 물질검색창으로 이동</div>
|
||||
<div className="text-[12px] font-bold text-primary-purple">📋 화물적부도 화물코드</div>
|
||||
<div className="text-[8px] text-text-3">클릭 시 물질검색창으로 이동</div>
|
||||
</div>
|
||||
<div style={{ padding: 12 }}>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 9 }}>
|
||||
<div className="p-3">
|
||||
<table className="w-full border-collapse text-[9px]">
|
||||
<thead><tr style={{ background: 'rgba(168,85,247,.05)' }}>
|
||||
<th style={{ padding: '6px 8px', textAlign: 'left', borderBottom: '1px solid var(--bdL)', color: 'var(--purple)' }}>화물코드</th>
|
||||
<th style={{ padding: '6px 8px', textAlign: 'left', borderBottom: '1px solid var(--bdL)', color: 'var(--t2)' }}>약자/제품명</th>
|
||||
<th style={{ padding: '6px 8px', textAlign: 'left', borderBottom: '1px solid var(--bdL)', color: 'var(--t2)' }}>국적/회사</th>
|
||||
<th style={{ padding: '6px 8px', textAlign: 'center', borderBottom: '1px solid var(--bdL)', color: 'var(--t2)' }}>출처</th>
|
||||
<th className="text-primary-purple text-left" style={{ padding: '6px 8px', borderBottom: '1px solid var(--bdL)' }}>화물코드</th>
|
||||
<th className="text-text-2 text-left" style={{ padding: '6px 8px', borderBottom: '1px solid var(--bdL)' }}>약자/제품명</th>
|
||||
<th className="text-text-2 text-left" style={{ padding: '6px 8px', borderBottom: '1px solid var(--bdL)' }}>국적/회사</th>
|
||||
<th className="text-text-2 text-center" style={{ padding: '6px 8px', borderBottom: '1px solid var(--bdL)' }}>출처</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
{s.cargoCodes.map((c, i) => {
|
||||
@ -942,10 +941,10 @@ function HmsDetailPanel({ substance: s, activeTab, onTabChange }: { substance: H
|
||||
const srcBg = c.source === '적부도' ? 'rgba(249,115,22,.1)' : c.source === '용선자' ? 'rgba(6,182,212,.1)' : 'rgba(34,197,94,.1)'
|
||||
return (
|
||||
<tr key={i} style={{ borderBottom: i < s.cargoCodes.length - 1 ? '1px solid rgba(255,255,255,.04)' : undefined }}>
|
||||
<td style={{ padding: '5px 8px', fontFamily: 'var(--fM)', color: 'var(--purple)', fontWeight: 600, cursor: 'pointer' }}>{c.code}</td>
|
||||
<td style={{ padding: '5px 8px' }}>{c.name}</td>
|
||||
<td style={{ padding: '5px 8px', color: 'var(--t3)' }}>{c.company}</td>
|
||||
<td style={{ textAlign: 'center' }}><span style={{ padding: '1px 6px', borderRadius: 3, background: srcBg, color: srcColor, fontSize: 7 }}>{c.source}</span></td>
|
||||
<td className="font-mono text-primary-purple font-semibold cursor-pointer" className="py-[5px] px-2">{c.code}</td>
|
||||
<td className="py-[5px] px-2">{c.name}</td>
|
||||
<td className="text-text-3" className="py-[5px] px-2">{c.company}</td>
|
||||
<td className="text-center"><span style={{ padding: '1px 6px', borderRadius: 3, background: srcBg, color: srcColor, fontSize: 7 }}>{c.source}</span></td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
@ -955,18 +954,18 @@ function HmsDetailPanel({ substance: s, activeTab, onTabChange }: { substance: H
|
||||
</div>
|
||||
|
||||
{/* 항구별 코드 */}
|
||||
<div style={{ border: '1px solid rgba(6,182,212,.2)', borderRadius: 8, overflow: 'hidden' }}>
|
||||
<div className="rounded-lg overflow-hidden" style={{ border: '1px solid rgba(6,182,212,.2)' }}>
|
||||
<div style={{ padding: '10px 12px', background: 'linear-gradient(135deg,rgba(6,182,212,.08),transparent)', borderBottom: '1px solid rgba(6,182,212,.12)' }}>
|
||||
<div style={{ fontSize: 12, fontWeight: 700, color: 'var(--cyan)' }}>🏗 항구별 코드</div>
|
||||
<div style={{ fontSize: 8, color: 'var(--t3)' }}>Port-MIS 위험물반입신고현황 연동</div>
|
||||
<div className="text-[12px] font-bold text-primary-cyan">🏗 항구별 코드</div>
|
||||
<div className="text-[8px] text-text-3">Port-MIS 위험물반입신고현황 연동</div>
|
||||
</div>
|
||||
<div style={{ padding: 12 }}>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 9 }}>
|
||||
<div className="p-3">
|
||||
<table className="w-full border-collapse text-[9px]">
|
||||
<thead><tr style={{ background: 'rgba(6,182,212,.05)' }}>
|
||||
<th style={{ padding: '6px 8px', textAlign: 'left', borderBottom: '1px solid var(--bdL)', color: 'var(--cyan)' }}>항구</th>
|
||||
<th style={{ padding: '6px 8px', textAlign: 'left', borderBottom: '1px solid var(--bdL)', color: 'var(--t2)' }}>청코드</th>
|
||||
<th style={{ padding: '6px 8px', textAlign: 'center', borderBottom: '1px solid var(--bdL)', color: 'var(--t2)' }}>최근 반입</th>
|
||||
<th style={{ padding: '6px 8px', textAlign: 'center', borderBottom: '1px solid var(--bdL)', color: 'var(--t2)' }}>빈도</th>
|
||||
<th className="text-primary-cyan text-left" style={{ padding: '6px 8px', borderBottom: '1px solid var(--bdL)' }}>항구</th>
|
||||
<th className="text-text-2 text-left" style={{ padding: '6px 8px', borderBottom: '1px solid var(--bdL)' }}>청코드</th>
|
||||
<th className="text-text-2 text-center" style={{ padding: '6px 8px', borderBottom: '1px solid var(--bdL)' }}>최근 반입</th>
|
||||
<th className="text-text-2 text-center" style={{ padding: '6px 8px', borderBottom: '1px solid var(--bdL)' }}>빈도</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
{s.portFrequency.map((p, i) => {
|
||||
@ -974,17 +973,17 @@ function HmsDetailPanel({ substance: s, activeTab, onTabChange }: { substance: H
|
||||
const freqBg = p.frequency === '높음' ? 'rgba(239,68,68,.1)' : p.frequency === '중간' ? 'rgba(249,115,22,.1)' : 'rgba(34,197,94,.1)'
|
||||
return (
|
||||
<tr key={i} style={{ borderBottom: i < s.portFrequency.length - 1 ? '1px solid rgba(255,255,255,.04)' : undefined }}>
|
||||
<td style={{ padding: '5px 8px', fontWeight: 600 }}>{p.port}</td>
|
||||
<td style={{ padding: '5px 8px', fontFamily: 'var(--fM)', color: 'var(--cyan)' }}>{p.portCode}</td>
|
||||
<td style={{ textAlign: 'center', fontFamily: 'var(--fM)' }}>{p.lastImport}</td>
|
||||
<td style={{ textAlign: 'center' }}><span style={{ padding: '1px 6px', borderRadius: 3, background: freqBg, color: freqColor, fontWeight: 600, fontSize: 8 }}>{p.frequency}</span></td>
|
||||
<td className="font-semibold" className="py-[5px] px-2">{p.port}</td>
|
||||
<td className="font-mono text-primary-cyan" className="py-[5px] px-2">{p.portCode}</td>
|
||||
<td className="text-center font-mono">{p.lastImport}</td>
|
||||
<td className="text-center"><span style={{ padding: '1px 6px', borderRadius: 3, background: freqBg, color: freqColor, fontWeight: 600, fontSize: 8 }}>{p.frequency}</span></td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
<div style={{ marginTop: 10, textAlign: 'center' }}>
|
||||
<button style={{ padding: '6px 16px', background: 'rgba(6,182,212,.1)', border: '1px solid rgba(6,182,212,.2)', borderRadius: 6, color: 'var(--cyan)', fontSize: 10, fontWeight: 600, cursor: 'pointer' }}>🔗 Port-MIS 화물적부도 조회</button>
|
||||
<div className="mt-2.5 text-center">
|
||||
<button className="text-primary-cyan text-[10px] font-semibold cursor-pointer rounded-sm" style={{ padding: '6px 16px', background: 'rgba(6,182,212,.1)', border: '1px solid rgba(6,182,212,.2)' }}>🔗 Port-MIS 화물적부도 조회</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -997,9 +996,9 @@ function HmsDetailPanel({ substance: s, activeTab, onTabChange }: { substance: H
|
||||
|
||||
function InfoBoxRow({ label, value, bg, border, labelColor, valueColor }: { label: string; value: string; bg: string; border: string; labelColor: string; valueColor: string }) {
|
||||
return (
|
||||
<div style={{ padding: '6px 8px', background: bg, border: `1px solid ${border}`, borderRadius: 4, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div className="flex items-center justify-between rounded" style={{ padding: '6px 8px', background: bg, border: `1px solid ${border}` }}>
|
||||
<span><b style={{ color: labelColor }}>{label}</b></span>
|
||||
<span style={{ fontFamily: 'var(--fM)', color: valueColor, fontWeight: 700 }}>{value}</span>
|
||||
<span className="font-mono font-bold" style={{ color: valueColor }}>{value}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -15,8 +15,8 @@ function HNSManualViewer() {
|
||||
const card = 'rounded-md p-4 mb-3'
|
||||
|
||||
return (
|
||||
<div className="flex-1 overflow-y-auto" style={{ background: 'var(--bg0)', scrollbarWidth: 'thin', scrollbarColor: 'var(--bdL) transparent' }}>
|
||||
<div style={{ padding: '16px 20px', maxWidth: '1200px', margin: '0 auto' }}>
|
||||
<div className="flex-1 overflow-y-auto bg-bg-0" style={{ scrollbarWidth: 'thin', scrollbarColor: 'var(--bdL) transparent' }}>
|
||||
<div className="px-5 py-4 max-w-[1200px] mx-auto">
|
||||
|
||||
{/* 헤더 */}
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
@ -27,7 +27,7 @@ function HNSManualViewer() {
|
||||
</div>
|
||||
|
||||
{/* 목차 카드 그리드 */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4,1fr)', gap: '10px', marginBottom: '20px' }}>
|
||||
<div className="grid mb-5" style={{ gridTemplateColumns: 'repeat(4,1fr)', gap: '10px' }}>
|
||||
{[
|
||||
{ icon: '📘', title: '1. 서론', desc: 'HNS 정의 · OPRC-HNS 의정서 · HNS 협약 범위 및 목적', color: 'var(--cyan)' },
|
||||
{ icon: '⚖️', title: '2. IMO 협약·의정서·규칙', desc: 'SOLAS · MARPOL · IBC Code · IMDG Code · IGC Code', color: 'var(--cyan)' },
|
||||
@ -39,18 +39,18 @@ function HNSManualViewer() {
|
||||
{ icon: '📊', title: '8. 자료표', desc: '물질별 데이터시트 · AEGL · 노출 한계값', color: 'var(--cyan)' },
|
||||
].map(ch => (
|
||||
<div key={ch.title} className={card} style={{ background: 'var(--bg3)', border: '1px solid var(--bd)', cursor: 'pointer', transition: '.2s' }}>
|
||||
<div style={{ fontSize: '20px', marginBottom: '6px' }}>{ch.icon}</div>
|
||||
<div className="text-[20px] mb-1.5">{ch.icon}</div>
|
||||
<div className="text-[11px] font-bold">{ch.title}</div>
|
||||
<div className="text-[9px] text-text-3 mt-1" style={{ lineHeight: '1.4' }}>{ch.desc}</div>
|
||||
<div className="text-[9px] text-text-3 mt-1 leading-[1.4]">{ch.desc}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* SEBC 거동 분류 */}
|
||||
<div className={card} style={{ background: 'var(--bg3)', border: '1px solid var(--bd)' }}>
|
||||
<div className={card} className="bg-bg-3 border border-border">
|
||||
<div className="text-[13px] font-bold mb-2.5">SEBC 거동 분류 (Standard European Behaviour Classification)</div>
|
||||
<div className="text-[9px] text-text-3 mb-2.5" className="leading-normal">물질의 물리적·화학적 특성(용해도, 밀도, 증기압, 점도)에 따라 이론적 거동을 5가지 주요 범주 + 7가지 하위 범주로 분류</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(5,1fr)', gap: '8px' }}>
|
||||
<div className="text-[9px] text-text-3 mb-2.5 leading-normal">물질의 물리적·화학적 특성(용해도, 밀도, 증기압, 점도)에 따라 이론적 거동을 5가지 주요 범주 + 7가지 하위 범주로 분류</div>
|
||||
<div className="grid gap-2" style={{ gridTemplateColumns: 'repeat(5,1fr)' }}>
|
||||
{[
|
||||
{ icon: '💨', label: 'G — 가스', desc: '대기 중 확산\n증기압 > 101.3kPa\n예: 암모니아, 염소', color: 'rgba(139,92,246' },
|
||||
{ icon: '🌫️', label: 'E — 증발', desc: '수면→대기 증발\n증기압 > 3kPa\n예: 벤젠, 톨루엔', color: 'rgba(249,115,22' },
|
||||
@ -58,19 +58,19 @@ function HNSManualViewer() {
|
||||
{ icon: '💧', label: 'D — 용해', desc: '해수에 용해\n용해도 > 5%\n예: 메탄올, 염산', color: 'rgba(6,182,212' },
|
||||
{ icon: '⬇️', label: 'S — 침강', desc: '해저로 침강\n밀도 > 1.025\n예: EDC, 사염화탄소', color: 'rgba(139,148,158' },
|
||||
].map(s => (
|
||||
<div key={s.label} className="text-center" style={{ padding: '10px 6px', background: `${s.color},.08)`, border: `1px solid ${s.color},.2)`, borderRadius: '6px' }}>
|
||||
<div style={{ fontSize: '22px', marginBottom: '4px' }}>{s.icon}</div>
|
||||
<div key={s.label} className="text-center px-[6px] py-[10px] rounded-sm" style={{ background: `${s.color},.08)`, border: `1px solid ${s.color},.2)` }}>
|
||||
<div className="text-[22px] mb-1">{s.icon}</div>
|
||||
<div className="text-[11px] font-bold" style={{ color: `${s.color},1)` }}>{s.label}</div>
|
||||
<div className="text-text-3 whitespace-pre-line" style={{ fontSize: '8px', marginTop: '3px', lineHeight: '1.3' }}>{s.desc}</div>
|
||||
<div className="text-text-3 whitespace-pre-line text-[8px] mt-[3px] leading-[1.3]">{s.desc}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* IMDG Code 위험물 등급 */}
|
||||
<div className={card} style={{ background: 'var(--bg3)', border: '1px solid var(--bd)' }}>
|
||||
<div className={card} className="bg-bg-3 border border-border">
|
||||
<div className="text-[13px] font-bold mb-2.5">IMDG Code 위험물 등급 (Hazard Classification)</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3,1fr)', gap: '6px' }}>
|
||||
<div className="grid gap-1.5" style={{ gridTemplateColumns: 'repeat(3,1fr)' }}>
|
||||
{[
|
||||
{ icon: '💥', label: 'Class 1 — 폭발물', sub: 'Explosives', bg: 'rgba(249,115,22,.15)' },
|
||||
{ icon: '🫧', label: 'Class 2 — 가스', sub: '인화성/비인화성/독성', bg: 'rgba(34,197,94,.12)' },
|
||||
@ -82,11 +82,11 @@ function HNSManualViewer() {
|
||||
{ icon: '🧪', label: 'Class 8 — 부식성', sub: 'Corrosive Substances', bg: 'rgba(139,148,158,.12)' },
|
||||
{ icon: '⚠️', label: 'Class 9 — 기타', sub: '환경유해물질 포함', bg: 'rgba(139,148,158,.12)' },
|
||||
].map(c => (
|
||||
<div key={c.label} className="flex items-center gap-2 p-2" style={{ background: 'var(--bg0)', borderRadius: '5px' }}>
|
||||
<div className="flex items-center justify-center shrink-0 text-sm" style={{ width: '28px', height: '28px', background: c.bg, borderRadius: '4px' }}>{c.icon}</div>
|
||||
<div key={c.label} className="flex items-center gap-2 p-2 bg-bg-0 rounded-[5px]">
|
||||
<div className="flex items-center justify-center shrink-0 text-sm w-7 h-7 rounded" style={{ background: c.bg }}>{c.icon}</div>
|
||||
<div>
|
||||
<div className="text-[10px] font-bold">{c.label}</div>
|
||||
<div className="text-text-3" style={{ fontSize: '8px' }}>{c.sub}</div>
|
||||
<div className="text-text-3 text-[8px]">{c.sub}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@ -94,7 +94,7 @@ function HNSManualViewer() {
|
||||
</div>
|
||||
|
||||
{/* HNS 사고 대응 프로세스 */}
|
||||
<div className={card} style={{ background: 'var(--bg3)', border: '1px solid var(--bd)' }}>
|
||||
<div className={card} className="bg-bg-3 border border-border">
|
||||
<div className="text-[13px] font-bold mb-2.5">HNS 사고 대응 프로세스</div>
|
||||
<div className="flex items-stretch gap-1 text-[9px]">
|
||||
{[
|
||||
@ -105,21 +105,21 @@ function HNSManualViewer() {
|
||||
{ icon: '🔄', step: '5단계: 유출후 관리', desc: '환경 회복/복원\n비용 문서화\n사고 검토/교훈', color: 'rgba(34,197,94', textColor: '#22c55e' },
|
||||
].map((s, i) => (
|
||||
<div key={s.step} className="flex items-stretch flex-1">
|
||||
<div className="flex-1 text-center" style={{ padding: '10px 8px', background: `${s.color},.08)`, border: `1px solid ${s.color},.2)`, borderRadius: '6px' }}>
|
||||
<div className="flex-1 text-center px-2 py-[10px] rounded-sm" style={{ background: `${s.color},.08)`, border: `1px solid ${s.color},.2)` }}>
|
||||
<div className="text-base mb-1">{s.icon}</div>
|
||||
<div className="font-bold mb-0.5" style={{ color: s.textColor }}>{s.step}</div>
|
||||
<div className="text-text-3 whitespace-pre-line" style={{ fontSize: '8px', lineHeight: '1.3' }}>{s.desc}</div>
|
||||
<div className="text-text-3 whitespace-pre-line text-[8px] leading-[1.3]">{s.desc}</div>
|
||||
</div>
|
||||
{i < 4 && <div className="flex items-center text-sm" style={{ color: 'var(--bd)', padding: '0 2px' }}>→</div>}
|
||||
{i < 4 && <div className="flex items-center text-sm px-[2px]" style={{ color: 'var(--bd)' }}>→</div>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 대응 기술 매트릭스 */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '14px', marginBottom: '14px' }}>
|
||||
<div className="grid gap-[14px] mb-[14px]" style={{ gridTemplateColumns: '1fr 1fr' }}>
|
||||
{/* 선박 중심 조치 */}
|
||||
<div className={card} style={{ background: 'var(--bg3)', border: '1px solid var(--bd)' }}>
|
||||
<div className={card} className="bg-bg-3 border border-border">
|
||||
<div className="text-[12px] font-bold mb-2">🚢 선박 중심 조치</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
{[
|
||||
@ -129,8 +129,8 @@ function HNSManualViewer() {
|
||||
{ icon: '🔄', title: '화물 이송 (Cargo Transfer)', desc: '위험 화물을 다른 선박/탱크로 이적' },
|
||||
{ icon: '🔧', title: '밀봉/마개 (Sealing & Plugging)', desc: '유출 지점 임시 차단 및 봉쇄' },
|
||||
].map(item => (
|
||||
<div key={item.title} className="flex items-center gap-2" style={{ padding: '6px 8px', background: 'var(--bg0)', borderRadius: '4px' }}>
|
||||
<span className="text-[12px] text-center" style={{ width: '20px' }}>{item.icon}</span>
|
||||
<div key={item.title} className="flex items-center gap-2 px-2 py-1.5 bg-bg-0 rounded">
|
||||
<span className="text-[12px] text-center w-5">{item.icon}</span>
|
||||
<div className="text-[9px]">
|
||||
<b>{item.title}</b><br />
|
||||
<span className="text-text-3">{item.desc}</span>
|
||||
@ -140,7 +140,7 @@ function HNSManualViewer() {
|
||||
</div>
|
||||
</div>
|
||||
{/* 오염물질 중심 조치 */}
|
||||
<div className={card} style={{ background: 'var(--bg3)', border: '1px solid var(--bd)' }}>
|
||||
<div className={card} className="bg-bg-3 border border-border">
|
||||
<div className="text-[12px] font-bold mb-2">🧪 오염물질 중심 조치</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
{[
|
||||
@ -150,8 +150,8 @@ function HNSManualViewer() {
|
||||
{ icon: '⬇️', title: '해저 회수 (Subsea Recovery)', desc: '침강 물질 ROV/잠수 회수' },
|
||||
{ icon: '🔥', title: '제어 연소 (Controlled Burning)', desc: '인화성 물질 현장 소각 처리' },
|
||||
].map(item => (
|
||||
<div key={item.title} className="flex items-center gap-2" style={{ padding: '6px 8px', background: 'var(--bg0)', borderRadius: '4px' }}>
|
||||
<span className="text-[12px] text-center" style={{ width: '20px' }}>{item.icon}</span>
|
||||
<div key={item.title} className="flex items-center gap-2 px-2 py-1.5 bg-bg-0 rounded">
|
||||
<span className="text-[12px] text-center w-5">{item.icon}</span>
|
||||
<div className="text-[9px]">
|
||||
<b>{item.title}</b><br />
|
||||
<span className="text-text-3">{item.desc}</span>
|
||||
@ -163,9 +163,9 @@ function HNSManualViewer() {
|
||||
</div>
|
||||
|
||||
{/* PPE / 안전구역 / 노출한계 */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '14px', marginBottom: '14px' }}>
|
||||
<div className="grid gap-[14px] mb-[14px]" style={{ gridTemplateColumns: '1fr 1fr 1fr' }}>
|
||||
{/* PPE */}
|
||||
<div className={card} style={{ background: 'var(--bg3)', border: '1px solid var(--bd)' }}>
|
||||
<div className={card} className="bg-bg-3 border border-border">
|
||||
<div className="text-[12px] font-bold mb-2">🦺 개인보호장비 (PPE)</div>
|
||||
<div className="flex flex-col gap-1 text-[9px]">
|
||||
{[
|
||||
@ -182,28 +182,28 @@ function HNSManualViewer() {
|
||||
</div>
|
||||
</div>
|
||||
{/* 안전구역 */}
|
||||
<div className={card} style={{ background: 'var(--bg3)', border: '1px solid var(--bd)' }}>
|
||||
<div className={card} className="bg-bg-3 border border-border">
|
||||
<div className="text-[12px] font-bold mb-2">🔴 안전 구역 설정</div>
|
||||
<div className="flex flex-col gap-1.5 text-[9px]">
|
||||
<div className="text-center flex flex-col items-center justify-center" style={{ padding: '10px', background: 'rgba(239,68,68,.08)', border: '2px solid rgba(239,68,68,.3)', borderRadius: '50%', aspectRatio: '1' }}>
|
||||
<div className="text-center flex flex-col items-center justify-center p-[10px]" style={{ background: 'rgba(239,68,68,.08)', border: '2px solid rgba(239,68,68,.3)', borderRadius: '50%', aspectRatio: '1' }}>
|
||||
<b style={{ color: '#f87171', fontSize: '11px' }}>HOT ZONE</b>
|
||||
<span className="text-text-3" style={{ fontSize: '8px' }}>직접 위험구역<br />Level A/B PPE 필수</span>
|
||||
<span className="text-text-3 text-[8px]">직접 위험구역<br />Level A/B PPE 필수</span>
|
||||
</div>
|
||||
<div className="text-center" style={{ padding: '8px', background: 'rgba(251,191,36,.06)', border: '1.5px solid rgba(251,191,36,.25)', borderRadius: '6px' }}>
|
||||
<div className="text-center p-2 rounded-sm" style={{ background: 'rgba(251,191,36,.06)', border: '1.5px solid rgba(251,191,36,.25)' }}>
|
||||
<b style={{ color: '#fbbf24', fontSize: '10px' }}>WARM ZONE</b>
|
||||
<span className="text-text-3" style={{ fontSize: '8px' }}> — 오염제거/전환 구역</span>
|
||||
<span className="text-text-3 text-[8px]"> — 오염제거/전환 구역</span>
|
||||
</div>
|
||||
<div className="text-center" style={{ padding: '8px', background: 'rgba(34,197,94,.06)', border: '1.5px solid rgba(34,197,94,.25)', borderRadius: '6px' }}>
|
||||
<div className="text-center p-2 rounded-sm" style={{ background: 'rgba(34,197,94,.06)', border: '1.5px solid rgba(34,197,94,.25)' }}>
|
||||
<b style={{ color: '#22c55e', fontSize: '10px' }}>COLD ZONE</b>
|
||||
<span className="text-text-3" style={{ fontSize: '8px' }}> — 지휘/지원 구역</span>
|
||||
<span className="text-text-3 text-[8px]"> — 지휘/지원 구역</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* 노출한계 (AEGL) */}
|
||||
<div className={card} style={{ background: 'var(--bg3)', border: '1px solid var(--bd)' }}>
|
||||
<div className={card} className="bg-bg-3 border border-border">
|
||||
<div className="text-[12px] font-bold mb-2">📊 노출한계 (AEGL 기준)</div>
|
||||
<div className="text-[9px] text-text-3 mb-1.5" style={{ lineHeight: '1.4' }}>Acute Exposure Guideline Levels (EPA)<br />암모니아(NH₃) 예시 — ppm 기준</div>
|
||||
<table className="w-full font-mono" style={{ borderCollapse: 'collapse', fontSize: '8px' }}>
|
||||
<div className="text-[9px] text-text-3 mb-1.5 leading-[1.4]">Acute Exposure Guideline Levels (EPA)<br />암모니아(NH₃) 예시 — ppm 기준</div>
|
||||
<table className="w-full font-mono border-collapse text-[8px]">
|
||||
<thead>
|
||||
<tr className="border-b border-border">
|
||||
<th className="p-1 text-left text-text-3">구분</th>
|
||||
@ -228,7 +228,7 @@ function HNSManualViewer() {
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="text-text-3 mt-1.5" style={{ fontSize: '7px', lineHeight: '1.4' }}>
|
||||
<div className="text-text-3 mt-1.5 text-[7px] leading-[1.4]">
|
||||
AEGL-1: 불쾌감 (비장애성)<br />
|
||||
AEGL-2: 심각한 건강 영향 (비가역적)<br />
|
||||
AEGL-3: 생명 위협 또는 사망
|
||||
@ -237,7 +237,7 @@ function HNSManualViewer() {
|
||||
</div>
|
||||
|
||||
{/* 출처 */}
|
||||
<div className="text-text-3 rounded-sm" style={{ padding: '10px', background: 'var(--bg3)', fontSize: '8px', lineHeight: '1.5' }}>
|
||||
<div className="text-text-3 rounded-sm bg-bg-3 p-[10px] text-[8px] leading-[1.5]">
|
||||
<b>출처:</b> Marine HNS Response Manual — Bonn Agreement / HELCOM / REMPEC (WestMOPoCo Project, 2024 한국어판)<br />
|
||||
번역: 원해민, 이시연, 양보경, 강성길, 이성엽 — KRISO 선박해양플랜트연구소 / NOWPAP MERRAC<br />
|
||||
원본: Alcaro L., Brandt J., Giraud W., Mannozzi M., Nicolas-Kopec A. (2021) ISBN: 978-2-87893-147-1
|
||||
|
||||
@ -169,7 +169,7 @@ export function IncidentsLeftPanel({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col bg-bg-1 border-r border-border overflow-hidden shrink-0" style={{ width: '360px' }}>
|
||||
<div className="flex flex-col bg-bg-1 border-r border-border overflow-hidden shrink-0 w-[360px]">
|
||||
{/* Search */}
|
||||
<div className="px-4 py-3 border-b border-border shrink-0">
|
||||
<div className="relative">
|
||||
@ -346,7 +346,7 @@ export function IncidentsLeftPanel({
|
||||
padding: '3px 7px', borderRadius: '4px', lineHeight: 1,
|
||||
border: '1px solid rgba(59,130,246,0.25)', background: 'rgba(59,130,246,0.08)', color: '#60a5fa',
|
||||
transition: '0.15s',
|
||||
}}>📹 <span style={{ fontSize: '8px' }}>{inc.mediaCount}</span></button>
|
||||
}}>📹 <span className="text-[8px]">{inc.mediaCount}</span></button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@ -464,7 +464,7 @@ const WeatherPopup = forwardRef<HTMLDivElement, {
|
||||
<span className="text-xs">⬆</span>
|
||||
<div>
|
||||
<div className="text-text-3 text-[7px]">고조 (만조)</div>
|
||||
<div className="font-bold font-mono text-[10px]" style={{ color: '#60a5fa' }}>{data.highTide}</div>
|
||||
<div className="font-bold font-mono text-[10px] text-[#60a5fa]">{data.highTide}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 flex items-center gap-1.5 px-2 py-1.5 rounded-md"
|
||||
@ -492,12 +492,12 @@ const WeatherPopup = forwardRef<HTMLDivElement, {
|
||||
</div>
|
||||
|
||||
{/* Impact */}
|
||||
<div style={{
|
||||
marginTop: 8, padding: '6px 10px',
|
||||
background: 'rgba(249,115,22,0.05)', border: '1px solid rgba(249,115,22,0.12)', borderRadius: 6,
|
||||
<div className="mt-2 rounded" style={{
|
||||
padding: '6px 10px',
|
||||
background: 'rgba(249,115,22,0.05)', border: '1px solid rgba(249,115,22,0.12)',
|
||||
}}>
|
||||
<div className="font-bold text-status-orange" style={{ fontSize: 8, marginBottom: 3 }}>⚠ 방제 작업 영향</div>
|
||||
<div className="text-text-2" style={{ fontSize: 8, lineHeight: 1.5 }}>{data.impactDc}</div>
|
||||
<div className="font-bold text-status-orange text-[8px] mb-[3px]">⚠ 방제 작업 영향</div>
|
||||
<div className="text-text-2 text-[8px] leading-[1.5]">{data.impactDc}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -507,10 +507,10 @@ WeatherPopup.displayName = 'WeatherPopup'
|
||||
|
||||
function WxCell({ icon, label, value }: { icon: string; label: string; value: string }) {
|
||||
return (
|
||||
<div className="flex items-center bg-bg-0" style={{ padding: '6px 8px', borderRadius: 6, gap: 6 }}>
|
||||
<span style={{ fontSize: 12 }}>{icon}</span>
|
||||
<div className="flex items-center bg-bg-0 rounded gap-[6px]" className="py-1.5 px-2">
|
||||
<span className="text-[12px]">{icon}</span>
|
||||
<div>
|
||||
<div className="text-text-3" style={{ fontSize: 7 }}>{label}</div>
|
||||
<div className="text-text-3 text-[7px]">{label}</div>
|
||||
<div className="font-semibold font-mono">{value}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -124,9 +124,9 @@ export function IncidentsRightPanel({
|
||||
|
||||
if (!incident) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center bg-bg-1 border-l border-border" style={{ width: '280px', minWidth: '280px' }}>
|
||||
<div className="flex flex-col items-center justify-center bg-bg-1 border-l border-border w-[280px] min-w-[280px]">
|
||||
<div className="text-center text-text-3 text-[11px]">
|
||||
<div className="text-[32px] mb-2" style={{ opacity: 0.3 }}>📊</div>
|
||||
<div className="text-[32px] mb-2 opacity-30">📊</div>
|
||||
좌측에서 사고를 선택하면<br />통합분석 조회가 표시됩니다
|
||||
</div>
|
||||
</div>
|
||||
@ -134,7 +134,7 @@ export function IncidentsRightPanel({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col bg-bg-1 border-l border-border overflow-hidden h-full" style={{ width: '280px', minWidth: '280px' }}>
|
||||
<div className="flex flex-col bg-bg-1 border-l border-border overflow-hidden h-full w-[280px] min-w-[280px]">
|
||||
{/* Header */}
|
||||
<div className="px-[14px] py-2.5 border-b border-border shrink-0">
|
||||
<div className="text-xs font-bold mb-0.5">
|
||||
@ -189,10 +189,10 @@ export function IncidentsRightPanel({
|
||||
style={{ accentColor: sec.color }}
|
||||
/>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-[10px] font-semibold whitespace-nowrap overflow-hidden" style={{ textOverflow: 'ellipsis' }}>
|
||||
<div className="text-[10px] font-semibold whitespace-nowrap overflow-hidden text-ellipsis">
|
||||
{item.name}
|
||||
</div>
|
||||
<div className="text-text-3 font-mono" style={{ fontSize: '8px' }}>
|
||||
<div className="text-text-3 font-mono text-[8px]">
|
||||
{item.sub}
|
||||
</div>
|
||||
</div>
|
||||
@ -219,17 +219,15 @@ export function IncidentsRightPanel({
|
||||
<div className="bg-bg-2 border border-border rounded-md p-2.5">
|
||||
<div className="flex items-center gap-1.5 mb-2">
|
||||
<span className="text-sm">🐟</span>
|
||||
<span className="text-xs font-bold" style={{ color: '#22c55e' }}>
|
||||
<span className="text-xs font-bold text-[#22c55e]">
|
||||
민감자원
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col" style={{ gap: '3px' }}>
|
||||
<div className="flex flex-col gap-[3px]">
|
||||
{sensitive.map(res => (
|
||||
<label key={res.id} className="flex items-center cursor-pointer text-[9px]"
|
||||
<label key={res.id} className="flex items-center cursor-pointer text-[9px] gap-[5px] rounded-[3px]"
|
||||
style={{
|
||||
gap: '5px',
|
||||
padding: '4px 6px', background: 'rgba(34,197,94,0.06)',
|
||||
borderRadius: '3px',
|
||||
}}>
|
||||
<input
|
||||
type="checkbox"
|
||||
@ -250,14 +248,14 @@ export function IncidentsRightPanel({
|
||||
<div className="bg-bg-2 border border-border rounded-md p-2.5">
|
||||
<div className="flex items-center gap-1.5 mb-2">
|
||||
<span className="text-sm">🛡</span>
|
||||
<span className="text-xs font-bold" style={{ color: '#f59e0b' }}>
|
||||
<span className="text-xs font-bold text-[#f59e0b]">
|
||||
근처 방제자원
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Empty state */}
|
||||
<div className="py-2.5 text-center text-text-3 text-[10px]" style={{ lineHeight: 1.7 }}>
|
||||
<div className="text-xl mb-1" style={{ opacity: 0.4 }}>🚢</div>
|
||||
<div className="py-2.5 text-center text-text-3 text-[10px] leading-[1.7]">
|
||||
<div className="text-xl mb-1 opacity-40">🚢</div>
|
||||
지도에서 선박을 클릭하면<br />부근 방제자원이 표시됩니다
|
||||
</div>
|
||||
|
||||
@ -265,7 +263,7 @@ export function IncidentsRightPanel({
|
||||
<div className="mt-2 pt-2" style={{ borderTop: '1px solid rgba(245,158,11,0.1)' }}>
|
||||
<div className="flex items-center justify-between mb-[5px]">
|
||||
<span className="text-[9px] text-text-3">탐색 반경</span>
|
||||
<span className="text-[10px] font-bold font-mono" style={{ color: '#f59e0b' }}>
|
||||
<span className="text-[10px] font-bold font-mono text-[#f59e0b]">
|
||||
{nearbyRadius} nm
|
||||
</span>
|
||||
</div>
|
||||
|
||||
파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
Load Diff
@ -52,15 +52,21 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
|
||||
|
||||
if (!media) {
|
||||
return (
|
||||
<div onClick={(e) => { if (e.target === e.currentTarget) onClose() }} style={{
|
||||
position: 'fixed', inset: 0, zIndex: 10000,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
background: 'rgba(0,0,0,0.7)', backdropFilter: 'blur(6px)',
|
||||
}}>
|
||||
<div style={{
|
||||
width: 300, padding: 40, background: '#0d1117', border: '1px solid #30363d',
|
||||
borderRadius: 14, textAlign: 'center', color: '#8b949e', fontSize: 12,
|
||||
}}>
|
||||
<div
|
||||
onClick={(e) => { if (e.target === e.currentTarget) onClose() }}
|
||||
className="fixed inset-0 z-[10000] flex items-center justify-center"
|
||||
style={{ background: 'rgba(0,0,0,0.7)', backdropFilter: 'blur(6px)' }}
|
||||
>
|
||||
<div
|
||||
className="text-center text-[12px] text-[#8b949e]"
|
||||
style={{
|
||||
width: 300,
|
||||
padding: 40,
|
||||
background: '#0d1117',
|
||||
border: '1px solid #30363d',
|
||||
borderRadius: 14,
|
||||
}}
|
||||
>
|
||||
현장정보를 불러오는 중...
|
||||
</div>
|
||||
</div>
|
||||
@ -75,31 +81,39 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
|
||||
const showCctv = activeTab === 'all' || activeTab === 'cctv'
|
||||
|
||||
return (
|
||||
<div onClick={(e) => { if (e.target === e.currentTarget) onClose() }} style={{
|
||||
position: 'fixed', inset: 0, zIndex: 10000,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
background: 'rgba(0,0,0,0.7)', backdropFilter: 'blur(6px)',
|
||||
}}>
|
||||
<div style={{
|
||||
width: '95vw', height: '92vh', maxWidth: 1600,
|
||||
background: '#0d1117', border: '1px solid #30363d', borderRadius: 14,
|
||||
overflow: 'hidden', display: 'flex', flexDirection: 'column',
|
||||
boxShadow: '0 24px 64px rgba(0,0,0,0.7)',
|
||||
}}>
|
||||
<div
|
||||
onClick={(e) => { if (e.target === e.currentTarget) onClose() }}
|
||||
className="fixed inset-0 z-[10000] flex items-center justify-center"
|
||||
style={{ background: 'rgba(0,0,0,0.7)', backdropFilter: 'blur(6px)' }}
|
||||
>
|
||||
<div
|
||||
className="flex flex-col overflow-hidden"
|
||||
style={{
|
||||
width: '95vw',
|
||||
height: '92vh',
|
||||
maxWidth: 1600,
|
||||
background: '#0d1117',
|
||||
border: '1px solid #30363d',
|
||||
borderRadius: 14,
|
||||
boxShadow: '0 24px 64px rgba(0,0,0,0.7)',
|
||||
}}
|
||||
>
|
||||
{/* ── Header ─────────────────────────────────── */}
|
||||
<div style={{
|
||||
flexShrink: 0, padding: '12px 20px',
|
||||
background: 'linear-gradient(135deg,#161b22,#0d1117)',
|
||||
borderBottom: '1px solid #30363d',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
||||
}}>
|
||||
<div
|
||||
className="shrink-0 flex items-center justify-between"
|
||||
style={{
|
||||
padding: '12px 20px',
|
||||
background: 'linear-gradient(135deg,#161b22,#0d1117)',
|
||||
borderBottom: '1px solid #30363d',
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-[10px]">
|
||||
<span className="text-lg">📋</span>
|
||||
<div>
|
||||
<div style={{ fontSize: 14, fontWeight: 800, color: '#f0f6fc' }}>
|
||||
<div className="text-[14px] font-[800] text-[#f0f6fc]">
|
||||
현장정보 — {incident.name}
|
||||
</div>
|
||||
<div style={{ fontSize: 10, color: '#8b949e', fontFamily: 'var(--fM)' }}>
|
||||
<div className="text-[10px] text-[#8b949e] font-mono">
|
||||
{incident.name} · {incident.date} · 사진 {media.photoCnt} / 영상 {media.videoCnt} / 위성 {media.satCnt} / CCTV {media.cctvCnt}
|
||||
</div>
|
||||
</div>
|
||||
@ -126,20 +140,21 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
|
||||
color: '#c084fc',
|
||||
}}>📤 업로드</button>
|
||||
{/* Close */}
|
||||
<span onClick={onClose} style={{
|
||||
fontSize: 18, cursor: 'pointer', color: '#8b949e', padding: '2px 6px',
|
||||
borderRadius: 4,
|
||||
}}>✕</span>
|
||||
<span
|
||||
onClick={onClose}
|
||||
className="text-[18px] cursor-pointer text-[#8b949e] rounded"
|
||||
style={{ padding: '2px 6px' }}
|
||||
>✕</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Timeline ────────────────────────────────── */}
|
||||
<div style={{
|
||||
flexShrink: 0, padding: '6px 20px', borderBottom: '1px solid #21262d',
|
||||
display: 'flex', alignItems: 'center', gap: 10,
|
||||
}}>
|
||||
<span style={{ fontSize: 9, color: '#8b949e', whiteSpace: 'nowrap' }}>TIMELINE</span>
|
||||
<div style={{ flex: 1, position: 'relative', height: 16 }}>
|
||||
<div
|
||||
className="shrink-0 flex items-center gap-[10px]"
|
||||
style={{ padding: '6px 20px', borderBottom: '1px solid #21262d' }}
|
||||
>
|
||||
<span className="text-[9px] text-[#8b949e] whitespace-nowrap">TIMELINE</span>
|
||||
<div className="flex-1 relative" style={{ height: 16 }}>
|
||||
<div style={{ position: 'absolute', top: 7, left: 0, right: 0, height: 2, background: '#21262d', borderRadius: 1 }} />
|
||||
{timelineDots.map((d, i) => (
|
||||
<div key={i} style={{
|
||||
@ -149,31 +164,34 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
|
||||
}} />
|
||||
))}
|
||||
</div>
|
||||
<div style={{ fontSize: 8, color: '#8b949e', fontFamily: 'var(--fM)', display: 'flex', gap: 8, whiteSpace: 'nowrap' }}>
|
||||
<div className="flex gap-2 text-[8px] font-mono text-[#8b949e] whitespace-nowrap">
|
||||
<span style={{ color: '#ef4444' }}>● 초기</span>
|
||||
<span style={{ color: '#f59e0b' }}>● 대응</span>
|
||||
<span style={{ color: '#8b949e' }}>● 종료</span>
|
||||
<span className="text-[#8b949e]">● 종료</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── 2x2 Grid Content ────────────────────────── */}
|
||||
<div style={{
|
||||
flex: 1, display: 'grid', overflow: 'hidden',
|
||||
gridTemplateColumns: (showPhoto || showSat) && (showVideo || showCctv) ? '1fr 1fr' : '1fr',
|
||||
gridTemplateRows: (showPhoto || showVideo) && (showSat || showCctv) ? '1fr 1fr' : '1fr',
|
||||
gap: 1, background: '#21262d',
|
||||
}}>
|
||||
<div
|
||||
className="flex-1 overflow-hidden grid"
|
||||
style={{
|
||||
gridTemplateColumns: (showPhoto || showSat) && (showVideo || showCctv) ? '1fr 1fr' : '1fr',
|
||||
gridTemplateRows: (showPhoto || showVideo) && (showSat || showCctv) ? '1fr 1fr' : '1fr',
|
||||
gap: 1,
|
||||
background: '#21262d',
|
||||
}}
|
||||
>
|
||||
{/* ── Q1: 현장사진 ──────────────────────────── */}
|
||||
{showPhoto && (
|
||||
<div style={{ background: '#0d1117', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
|
||||
<div className="flex flex-col overflow-hidden bg-[#0d1117]">
|
||||
{/* Section header */}
|
||||
<div style={{
|
||||
flexShrink: 0, padding: '8px 16px', display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
||||
borderBottom: '1px solid #21262d',
|
||||
}}>
|
||||
<div
|
||||
className="shrink-0 flex items-center justify-between"
|
||||
style={{ padding: '8px 16px', borderBottom: '1px solid #21262d' }}
|
||||
>
|
||||
<div className="flex items-center gap-[6px]">
|
||||
<span style={{ fontSize: 12 }}>📷</span>
|
||||
<span style={{ fontSize: 12, fontWeight: 700, color: '#f0f6fc' }}>
|
||||
<span className="text-[12px]">📷</span>
|
||||
<span className="text-[12px] font-bold text-[#f0f6fc]">
|
||||
현장사진 — {str(media.photoMeta, 'title', '현장 사진')}
|
||||
</span>
|
||||
</div>
|
||||
@ -182,33 +200,31 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
|
||||
</div>
|
||||
</div>
|
||||
{/* Photo content */}
|
||||
<div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', flexDirection: 'column', gap: 8 }}>
|
||||
<div style={{ fontSize: 48, color: '#30363d' }}>📷</div>
|
||||
<div style={{ fontSize: 12, color: '#c9d1d9', fontWeight: 600 }}>
|
||||
<div className="flex-1 flex items-center justify-center flex-col gap-2">
|
||||
<div className="text-[48px] text-[#30363d]">📷</div>
|
||||
<div className="text-[12px] text-[#c9d1d9] font-semibold">
|
||||
{incident.name.replace('유류유출', '유출 현장').replace('오염', '현장')} 해상 사진
|
||||
</div>
|
||||
<div style={{ fontSize: 9, color: '#8b949e', fontFamily: 'var(--fM)' }}>
|
||||
<div className="text-[9px] text-[#8b949e] font-mono">
|
||||
{str(media.photoMeta, 'date')} · {str(media.photoMeta, 'by')}
|
||||
</div>
|
||||
</div>
|
||||
{/* Thumbnails */}
|
||||
<div style={{ flexShrink: 0, padding: '8px 12px', borderTop: '1px solid #21262d' }}>
|
||||
<div style={{ display: 'flex', gap: 6, marginBottom: 6 }}>
|
||||
<div className="shrink-0" style={{ padding: '8px 12px', borderTop: '1px solid #21262d' }}>
|
||||
<div className="flex gap-1.5" style={{ marginBottom: 6 }}>
|
||||
{Array.from({ length: Math.min(num(media.photoMeta, 'thumbCount'), 7) }).map((_, i) => (
|
||||
<div key={i} style={{
|
||||
<div key={i} className="flex items-center justify-center text-[14px] text-[#30363d] cursor-pointer" style={{
|
||||
width: 40, height: 36, borderRadius: 4,
|
||||
background: i === 0 ? 'rgba(168,85,247,0.15)' : '#161b22',
|
||||
border: i === 0 ? '2px solid rgba(168,85,247,0.5)' : '1px solid #30363d',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
fontSize: 14, color: '#30363d', cursor: 'pointer',
|
||||
}}>📷</div>
|
||||
))}
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<span style={{ fontSize: 8, color: '#8b949e' }}>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-[8px] text-[#8b949e]">
|
||||
📷 사진 {num(media.photoMeta, 'thumbCount')}장 · {str(media.photoMeta, 'stage')}
|
||||
</span>
|
||||
<span style={{ fontSize: 8, color: '#a78bfa', cursor: 'pointer' }}>🔗 R&D 연계</span>
|
||||
<span className="text-[8px] text-[#a78bfa] cursor-pointer">🔗 R&D 연계</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -216,51 +232,59 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
|
||||
|
||||
{/* ── Q2: 드론 영상 ─────────────────────────── */}
|
||||
{showVideo && (
|
||||
<div style={{ background: '#0d1117', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
|
||||
<div style={{
|
||||
flexShrink: 0, padding: '8px 16px', display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
||||
borderBottom: '1px solid #21262d',
|
||||
}}>
|
||||
<div className="flex flex-col overflow-hidden bg-[#0d1117]">
|
||||
<div
|
||||
className="shrink-0 flex items-center justify-between"
|
||||
style={{ padding: '8px 16px', borderBottom: '1px solid #21262d' }}
|
||||
>
|
||||
<div className="flex items-center gap-[6px]">
|
||||
<span style={{ fontSize: 12 }}>🎬</span>
|
||||
<span style={{ fontSize: 12, fontWeight: 700, color: '#f0f6fc' }}>
|
||||
<span className="text-[12px]">🎬</span>
|
||||
<span className="text-[12px] font-bold text-[#f0f6fc]">
|
||||
드론 영상 — {str(media.droneMeta, 'title', '드론 영상')}
|
||||
</span>
|
||||
</div>
|
||||
<span style={{
|
||||
padding: '2px 8px', borderRadius: 4, fontSize: 9, fontWeight: 700,
|
||||
background: 'rgba(239,68,68,0.15)', color: '#ef4444',
|
||||
}}>● REC</span>
|
||||
<span
|
||||
className="text-[9px] font-bold text-[#ef4444] rounded"
|
||||
style={{
|
||||
padding: '2px 8px',
|
||||
background: 'rgba(239,68,68,0.15)',
|
||||
}}
|
||||
>● REC</span>
|
||||
</div>
|
||||
<div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', flexDirection: 'column', gap: 8 }}>
|
||||
<div style={{ fontSize: 48, color: '#30363d' }}>🎬</div>
|
||||
<div style={{ fontSize: 12, color: '#c9d1d9', fontWeight: 600 }}>
|
||||
<div className="flex-1 flex items-center justify-center flex-col gap-2">
|
||||
<div className="text-[48px] text-[#30363d]">🎬</div>
|
||||
<div className="text-[12px] text-[#c9d1d9] font-semibold">
|
||||
드론 항공 촬영 영상
|
||||
</div>
|
||||
<div style={{ fontSize: 9, color: '#8b949e', fontFamily: 'var(--fM)' }}>
|
||||
<div className="text-[9px] text-[#8b949e] font-mono">
|
||||
{str(media.droneMeta, 'device')} · {str(media.droneMeta, 'alt')} 고도
|
||||
</div>
|
||||
</div>
|
||||
{/* Video controls */}
|
||||
<div style={{ flexShrink: 0, padding: '10px 16px', borderTop: '1px solid #21262d', display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 12 }}>
|
||||
<span style={{ fontSize: 12, color: '#8b949e', cursor: 'pointer' }}>⏮</span>
|
||||
<div style={{
|
||||
width: 28, height: 28, borderRadius: '50%', background: 'rgba(168,85,247,0.15)',
|
||||
border: '1px solid rgba(168,85,247,0.3)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
fontSize: 12, color: '#c084fc', cursor: 'pointer',
|
||||
}}>▶</div>
|
||||
<span style={{ fontSize: 12, color: '#8b949e', cursor: 'pointer' }}>⏭</span>
|
||||
<span style={{ fontSize: 10, color: '#8b949e', fontFamily: 'var(--fM)' }}>02:34 / {str(media.droneMeta, 'duration')}</span>
|
||||
<div
|
||||
className="shrink-0 flex flex-col gap-2"
|
||||
style={{ padding: '10px 16px', borderTop: '1px solid #21262d' }}
|
||||
>
|
||||
<div className="flex items-center justify-center gap-3">
|
||||
<span className="text-[12px] text-[#8b949e] cursor-pointer">⏮</span>
|
||||
<div
|
||||
className="flex items-center justify-center text-[12px] text-[#c084fc] cursor-pointer"
|
||||
style={{
|
||||
width: 28, height: 28, borderRadius: '50%',
|
||||
background: 'rgba(168,85,247,0.15)',
|
||||
border: '1px solid rgba(168,85,247,0.3)',
|
||||
}}
|
||||
>▶</div>
|
||||
<span className="text-[12px] text-[#8b949e] cursor-pointer">⏭</span>
|
||||
<span className="text-[10px] text-[#8b949e] font-mono">02:34 / {str(media.droneMeta, 'duration')}</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<span style={{ fontSize: 8, color: '#8b949e' }}>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-[8px] text-[#8b949e]">
|
||||
🎬 영상 {num(media.droneMeta, 'videoCount')}건 · {str(media.droneMeta, 'stage')}
|
||||
</span>
|
||||
<div className="flex gap-[8px]">
|
||||
<span style={{ fontSize: 8, color: '#58a6ff', cursor: 'pointer' }}>📂 전체보기</span>
|
||||
<span style={{ fontSize: 8, color: '#a78bfa', cursor: 'pointer' }}>🔗 R&D 연계</span>
|
||||
<span className="text-[8px] text-[#58a6ff] cursor-pointer">📂 전체보기</span>
|
||||
<span className="text-[8px] text-[#a78bfa] cursor-pointer">🔗 R&D 연계</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -269,14 +293,14 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
|
||||
|
||||
{/* ── Q3: 위성영상 ──────────────────────────── */}
|
||||
{showSat && (
|
||||
<div style={{ background: '#0d1117', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
|
||||
<div style={{
|
||||
flexShrink: 0, padding: '8px 16px', display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
||||
borderBottom: '1px solid #21262d',
|
||||
}}>
|
||||
<div className="flex flex-col overflow-hidden bg-[#0d1117]">
|
||||
<div
|
||||
className="shrink-0 flex items-center justify-between"
|
||||
style={{ padding: '8px 16px', borderBottom: '1px solid #21262d' }}
|
||||
>
|
||||
<div className="flex items-center gap-[6px]">
|
||||
<span style={{ fontSize: 12 }}>🛰</span>
|
||||
<span style={{ fontSize: 12, fontWeight: 700, color: '#f0f6fc' }}>
|
||||
<span className="text-[12px]">🛰</span>
|
||||
<span className="text-[12px] font-bold text-[#f0f6fc]">
|
||||
위성영상 — {str(media.satMeta, 'title', '위성영상')}
|
||||
</span>
|
||||
</div>
|
||||
@ -284,51 +308,52 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
|
||||
<NavBtn label="◀" /> <NavBtn label="▶" /> <NavBtn label="↗" />
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', position: 'relative' }}>
|
||||
<div className="flex-1 flex items-center justify-center relative">
|
||||
{str(media.satMeta, 'detection') !== '—' && (
|
||||
<div style={{
|
||||
position: 'absolute', top: '15%', left: '10%', width: '55%', height: '60%',
|
||||
border: '2px dashed #ef4444', borderRadius: 4,
|
||||
display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 6,
|
||||
}}>
|
||||
<div style={{ position: 'absolute', top: -10, left: 8, fontSize: 9, fontWeight: 700, color: '#ef4444', fontFamily: 'var(--fM)', background: '#0d1117', padding: '0 4px' }}>
|
||||
<div
|
||||
className="absolute text-[9px] font-bold text-[#ef4444] font-mono bg-[#0d1117]"
|
||||
style={{ top: -10, left: 8, padding: '0 4px' }}
|
||||
>
|
||||
{str(media.satMeta, 'detection')}
|
||||
</div>
|
||||
<div style={{ fontSize: 40, color: '#30363d' }}>🛰</div>
|
||||
<div style={{ fontSize: 11, color: '#c9d1d9', fontWeight: 600 }}>
|
||||
<div className="text-[40px] text-[#30363d]">🛰</div>
|
||||
<div className="text-[11px] text-[#c9d1d9] font-semibold">
|
||||
{str(media.satMeta, 'title', '위성영상')} 위성영상
|
||||
</div>
|
||||
<div style={{ fontSize: 8, color: '#8b949e', fontFamily: 'var(--fM)' }}>
|
||||
<div className="text-[8px] text-[#8b949e] font-mono">
|
||||
{str(media.satMeta, 'date')} · 해상도 {str(media.satMeta, 'resolution')}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{str(media.satMeta, 'detection') === '—' && (
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<div style={{ fontSize: 40, color: '#30363d' }}>🛰</div>
|
||||
<div style={{ fontSize: 11, color: '#8b949e', marginTop: 8 }}>위성영상 없음</div>
|
||||
<div className="text-center">
|
||||
<div className="text-[40px] text-[#30363d]">🛰</div>
|
||||
<div className="text-[11px] text-[#8b949e]" style={{ marginTop: 8 }}>위성영상 없음</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ flexShrink: 0, padding: '8px 12px', borderTop: '1px solid #21262d' }}>
|
||||
<div className="shrink-0" style={{ padding: '8px 12px', borderTop: '1px solid #21262d' }}>
|
||||
{num(media.satMeta, 'thumbCount') > 0 && (
|
||||
<div style={{ display: 'flex', gap: 6, marginBottom: 6 }}>
|
||||
<div className="flex gap-1.5" style={{ marginBottom: 6 }}>
|
||||
{Array.from({ length: num(media.satMeta, 'thumbCount') }).map((_, i) => (
|
||||
<div key={i} style={{
|
||||
<div key={i} className="flex items-center justify-center text-[14px] text-[#30363d] cursor-pointer" style={{
|
||||
width: 40, height: 36, borderRadius: 4,
|
||||
background: i === 0 ? 'rgba(168,85,247,0.15)' : '#161b22',
|
||||
border: i === 0 ? '2px solid rgba(168,85,247,0.5)' : '1px solid #30363d',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
fontSize: 14, color: '#30363d', cursor: 'pointer',
|
||||
}}>🛰</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<span style={{ fontSize: 8, color: '#8b949e' }}>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-[8px] text-[#8b949e]">
|
||||
🛰 위성 {num(media.satMeta, 'thumbCount')}장 · {str(media.satMeta, 'sensor')}
|
||||
</span>
|
||||
<span style={{ fontSize: 8, color: '#58a6ff', cursor: 'pointer' }}>🔍 편집/측 비교</span>
|
||||
<span className="text-[8px] text-[#58a6ff] cursor-pointer">🔍 편집/측 비교</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -336,43 +361,49 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
|
||||
|
||||
{/* ── Q4: CCTV ──────────────────────────────── */}
|
||||
{showCctv && (
|
||||
<div style={{ background: '#0d1117', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
|
||||
<div style={{
|
||||
flexShrink: 0, padding: '8px 16px', display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
||||
borderBottom: '1px solid #21262d',
|
||||
}}>
|
||||
<div className="flex flex-col overflow-hidden bg-[#0d1117]">
|
||||
<div
|
||||
className="shrink-0 flex items-center justify-between"
|
||||
style={{ padding: '8px 16px', borderBottom: '1px solid #21262d' }}
|
||||
>
|
||||
<div className="flex items-center gap-[6px]">
|
||||
<span style={{ fontSize: 12 }}>📹</span>
|
||||
<span style={{ fontSize: 12, fontWeight: 700, color: '#f0f6fc' }}>
|
||||
<span className="text-[12px]">📹</span>
|
||||
<span className="text-[12px] font-bold text-[#f0f6fc]">
|
||||
CCTV — {str(media.cctvMeta, 'title', 'CCTV')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-[6px]">
|
||||
{bool(media.cctvMeta, 'live') && (
|
||||
<span style={{
|
||||
padding: '2px 8px', borderRadius: 4, fontSize: 9, fontWeight: 700,
|
||||
background: 'rgba(34,197,94,0.15)', color: '#22c55e',
|
||||
}}>● LIVE</span>
|
||||
<span
|
||||
className="text-[9px] font-bold text-[#22c55e] rounded"
|
||||
style={{
|
||||
padding: '2px 8px',
|
||||
background: 'rgba(34,197,94,0.15)',
|
||||
}}
|
||||
>● LIVE</span>
|
||||
)}
|
||||
<NavBtn label="↗" />
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', flexDirection: 'column', gap: 8, position: 'relative' }}>
|
||||
<div className="flex-1 flex items-center justify-center flex-col gap-2 relative">
|
||||
{bool(media.cctvMeta, 'live') && (
|
||||
<div style={{ position: 'absolute', top: 10, left: 16, fontSize: 9, fontWeight: 700, color: '#ef4444', fontFamily: 'var(--fM)' }}>
|
||||
<div className="absolute text-[9px] font-bold text-[#ef4444] font-mono" style={{ top: 10, left: 16 }}>
|
||||
● LIVE {new Date().toLocaleTimeString('ko-KR', { hour12: false })}
|
||||
</div>
|
||||
)}
|
||||
<div style={{ fontSize: 48, color: '#30363d' }}>📹</div>
|
||||
<div style={{ fontSize: 12, color: '#c9d1d9', fontWeight: 600 }}>
|
||||
<div className="text-[48px] text-[#30363d]">📹</div>
|
||||
<div className="text-[12px] text-[#c9d1d9] font-semibold">
|
||||
{str(media.cctvMeta, 'title', 'CCTV').replace('#', 'CCTV #')}
|
||||
</div>
|
||||
<div style={{ fontSize: 9, color: '#8b949e', fontFamily: 'var(--fM)' }}>
|
||||
<div className="text-[9px] text-[#8b949e] font-mono">
|
||||
{str(media.cctvMeta, 'ptz')} · {str(media.cctvMeta, 'angle')} · {bool(media.cctvMeta, 'live') ? '실시간 스트리밍' : '녹화 영상'}
|
||||
</div>
|
||||
</div>
|
||||
{/* CAM buttons */}
|
||||
<div style={{ flexShrink: 0, padding: '10px 16px', borderTop: '1px solid #21262d', display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
<div
|
||||
className="shrink-0 flex flex-col gap-2"
|
||||
style={{ padding: '10px 16px', borderTop: '1px solid #21262d' }}
|
||||
>
|
||||
<div className="flex gap-[6px]">
|
||||
{Array.from({ length: num(media.cctvMeta, 'camCount') }).map((_, i) => (
|
||||
<button key={i} onClick={() => setSelectedCam(i)} style={{
|
||||
@ -384,13 +415,13 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
|
||||
}}>CAM{i + 1}</button>
|
||||
))}
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<span style={{ fontSize: 8, color: '#8b949e' }}>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-[8px] text-[#8b949e]">
|
||||
📹 CCTV {num(media.cctvMeta, 'camCount')}채널 · {str(media.cctvMeta, 'location')}
|
||||
</span>
|
||||
<div className="flex gap-[8px]">
|
||||
<span style={{ fontSize: 8, color: '#ef4444', cursor: 'pointer' }}>🔴 녹화영상</span>
|
||||
<span style={{ fontSize: 8, color: '#58a6ff', cursor: 'pointer' }}>🎥 PTZ</span>
|
||||
<span className="text-[8px] text-[#ef4444] cursor-pointer">🔴 녹화영상</span>
|
||||
<span className="text-[8px] text-[#58a6ff] cursor-pointer">🎥 PTZ</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -399,17 +430,20 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
|
||||
</div>
|
||||
|
||||
{/* ── Bottom Bar ──────────────────────────────── */}
|
||||
<div style={{
|
||||
flexShrink: 0, padding: '8px 20px',
|
||||
background: '#161b22', borderTop: '1px solid #30363d',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
||||
}}>
|
||||
<div style={{ display: 'flex', gap: 16, fontSize: 10, fontFamily: 'var(--fM)', color: '#8b949e' }}>
|
||||
<span>📷 사진 <b style={{ color: '#f0f6fc' }}>{media.photoCnt}</b></span>
|
||||
<span>🎬 영상 <b style={{ color: '#f0f6fc' }}>{media.videoCnt}</b></span>
|
||||
<span>🛰 위성 <b style={{ color: '#f0f6fc' }}>{media.satCnt}</b></span>
|
||||
<span>📹 CCTV <b style={{ color: '#f0f6fc' }}>{media.cctvCnt}</b></span>
|
||||
<span>📎 총 <b style={{ color: '#c084fc' }}>{total}건</b></span>
|
||||
<div
|
||||
className="shrink-0 flex items-center justify-between"
|
||||
style={{
|
||||
padding: '8px 20px',
|
||||
background: '#161b22',
|
||||
borderTop: '1px solid #30363d',
|
||||
}}
|
||||
>
|
||||
<div className="flex gap-4 text-[10px] font-mono text-[#8b949e]">
|
||||
<span>📷 사진 <b className="text-[#f0f6fc]">{media.photoCnt}</b></span>
|
||||
<span>🎬 영상 <b className="text-[#f0f6fc]">{media.videoCnt}</b></span>
|
||||
<span>🛰 위성 <b className="text-[#f0f6fc]">{media.satCnt}</b></span>
|
||||
<span>📹 CCTV <b className="text-[#f0f6fc]">{media.cctvCnt}</b></span>
|
||||
<span>📎 총 <b className="text-[#c084fc]">{total}건</b></span>
|
||||
</div>
|
||||
<div className="flex gap-[8px]">
|
||||
<BottomBtn icon="📥" label="다운로드" bg="rgba(100,116,139,0.1)" bd="rgba(100,116,139,0.2)" fg="#8b949e" />
|
||||
@ -424,21 +458,26 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
|
||||
|
||||
function NavBtn({ label }: { label: string }) {
|
||||
return (
|
||||
<button style={{
|
||||
width: 22, height: 22, borderRadius: 4, fontSize: 10,
|
||||
background: '#161b22', border: '1px solid #30363d', color: '#8b949e',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer',
|
||||
}}>{label}</button>
|
||||
<button
|
||||
className="flex items-center justify-center text-[10px] text-[#8b949e] cursor-pointer rounded bg-[#161b22]"
|
||||
style={{
|
||||
width: 22, height: 22,
|
||||
border: '1px solid #30363d',
|
||||
}}
|
||||
>{label}</button>
|
||||
)
|
||||
}
|
||||
|
||||
function BottomBtn({ icon, label, bg, bd, fg }: { icon: string; label: string; bg: string; bd: string; fg: string }) {
|
||||
return (
|
||||
<button style={{
|
||||
padding: '6px 14px', borderRadius: 6, fontSize: 10, fontWeight: 700,
|
||||
cursor: 'pointer',
|
||||
background: bg, border: `1px solid ${bd}`, color: fg,
|
||||
display: 'flex', alignItems: 'center', gap: 4,
|
||||
}}>{icon} {label}</button>
|
||||
<button
|
||||
className="flex items-center gap-1 text-[10px] font-bold cursor-pointer rounded-sm"
|
||||
style={{
|
||||
padding: '6px 14px',
|
||||
background: bg,
|
||||
border: `1px solid ${bd}`,
|
||||
color: fg,
|
||||
}}
|
||||
>{icon} {label}</button>
|
||||
)
|
||||
}
|
||||
|
||||
@ -97,7 +97,7 @@ function OverviewPanel() {
|
||||
{ num: '④', color: 'var(--purple)', bg: 'rgba(168,85,247,.05)', bd: 'rgba(168,85,247,.12)', text: '실패 안전성 확보 — 조류 초과 시 오일펜스 이탈 방지 방향각 자동 보정' },
|
||||
].map((item, i) => (
|
||||
<div key={i} className="px-2.5 py-1.5 rounded-md" style={{ background: item.bg, border: `1px solid ${item.bd}` }}>
|
||||
<span style={{ color: item.color, fontWeight: 700 }}>{item.num}</span> <b>{item.text}</b>
|
||||
<span className="font-bold" style={{ color: item.color }}>{item.num}</span> <b>{item.text}</b>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@ -123,7 +123,7 @@ function OverviewPanel() {
|
||||
border: `${step.bold ? '2px' : '1px'} solid ${step.bd}`,
|
||||
}}>
|
||||
<div className="text-[15px] mb-1">{step.icon}</div>
|
||||
<div style={{ fontWeight: 700, color: step.color }}>{step.label}</div>
|
||||
<div className="font-bold" style={{ color: step.color }}>{step.label}</div>
|
||||
<div className="text-text-3" style={{ whiteSpace: 'pre-line' }}>{step.sub}</div>
|
||||
</div>
|
||||
{i < 5 && <div className="px-1.5 text-text-3 text-[14px]">▶</div>}
|
||||
@ -217,7 +217,7 @@ function DeploymentTheoryPanel() {
|
||||
<div className="rounded-lg p-3.5 bg-bg-0" style={{ border: '1px solid rgba(6,182,212,.2)', borderTop: '3px solid var(--cyan)' }}>
|
||||
<div className="text-[11px] font-bold mb-2 text-primary-cyan">V형 (Chevron)</div>
|
||||
<div className="flex items-center justify-center p-4 rounded-md mb-2" style={{ background: 'rgba(6,182,212,.04)' }}>
|
||||
<svg width="180" height="120" viewBox="0 0 100 65" style={{ overflow: 'visible' }}>
|
||||
<svg width="180" height="120" viewBox="0 0 100 65" className="overflow-visible">
|
||||
<defs><marker id="arr1" markerWidth="6" markerHeight="6" refX="3" refY="3" orient="auto"><path d="M0,0 L0,6 L6,3 z" fill="rgba(6,182,212,.7)" /></marker></defs>
|
||||
<line x1="10" y1="15" x2="50" y2="55" stroke="rgba(249,115,22,.8)" strokeWidth="2.5" />
|
||||
<line x1="90" y1="15" x2="50" y2="55" stroke="rgba(249,115,22,.8)" strokeWidth="2.5" />
|
||||
@ -239,7 +239,7 @@ function DeploymentTheoryPanel() {
|
||||
<div className="rounded-lg p-3.5 bg-bg-0" style={{ border: '1px solid rgba(34,197,94,.2)', borderTop: '3px solid var(--green)' }}>
|
||||
<div className="text-[11px] font-bold mb-2 text-status-green">U형 (Horseshoe)</div>
|
||||
<div className="flex items-center justify-center p-4 rounded-md mb-2" style={{ background: 'rgba(34,197,94,.04)' }}>
|
||||
<svg width="180" height="120" viewBox="0 0 100 65" style={{ overflow: 'visible' }}>
|
||||
<svg width="180" height="120" viewBox="0 0 100 65" className="overflow-visible">
|
||||
<defs><marker id="arr2" markerWidth="6" markerHeight="6" refX="3" refY="3" orient="auto"><path d="M0,0 L0,6 L6,3 z" fill="rgba(6,182,212,.7)" /></marker></defs>
|
||||
<path d="M15,10 L15,45 Q50,65 85,45 L85,10" fill="none" stroke="rgba(34,197,94,.8)" strokeWidth="2.5" />
|
||||
<circle cx="50" cy="52" r="4" fill="rgba(34,197,94,.5)" />
|
||||
@ -260,7 +260,7 @@ function DeploymentTheoryPanel() {
|
||||
<div className="rounded-lg p-3.5 bg-bg-0" style={{ border: '1px solid rgba(168,85,247,.2)', borderTop: '3px solid var(--purple)' }}>
|
||||
<div className="text-[11px] font-bold mb-2 text-primary-purple">J형 (Skimming)</div>
|
||||
<div className="flex items-center justify-center p-4 rounded-md mb-2" style={{ background: 'rgba(168,85,247,.04)' }}>
|
||||
<svg width="180" height="120" viewBox="0 0 100 65" style={{ overflow: 'visible' }}>
|
||||
<svg width="180" height="120" viewBox="0 0 100 65" className="overflow-visible">
|
||||
<defs><marker id="arr3" markerWidth="6" markerHeight="6" refX="3" refY="3" orient="auto"><path d="M0,0 L0,6 L6,3 z" fill="rgba(6,182,212,.7)" /></marker></defs>
|
||||
<line x1="80" y1="8" x2="80" y2="48" stroke="rgba(168,85,247,.8)" strokeWidth="2.5" />
|
||||
<path d="M80,48 Q55,60 30,35" fill="none" stroke="rgba(168,85,247,.8)" strokeWidth="2.5" />
|
||||
@ -362,7 +362,7 @@ function OptimizationPanel() {
|
||||
{ grade: 'ESI 9~10', desc: '맹그로브·습지', w: 'w = 1.0', color: 'var(--red)', bg: 'rgba(239,68,68,.08)', bd: 'rgba(239,68,68,.2)' },
|
||||
].map((esi, i) => (
|
||||
<div key={i} className="p-1.5 rounded text-center" style={{ background: esi.bg, border: esi.bd ? `1px solid ${esi.bd}` : undefined }}>
|
||||
<div style={{ fontWeight: 700, color: esi.color }}>{esi.grade}</div>
|
||||
<div className="font-bold" style={{ color: esi.color }}>{esi.grade}</div>
|
||||
<div className="text-text-3">{esi.desc}</div>
|
||||
<div className="font-bold">{esi.w}</div>
|
||||
</div>
|
||||
@ -417,7 +417,7 @@ function OptimizationPanel() {
|
||||
<div className="rounded-md p-3.5 bg-bg-3 border border-border">
|
||||
<div className="text-[11px] font-bold mb-2.5">🔬 보조 최적화 알고리즘 비교 적용</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-[10px]" style={{ borderCollapse: 'collapse' }}>
|
||||
<table className="w-full text-[10px] border-collapse">
|
||||
<thead>
|
||||
<tr style={{ background: 'rgba(255,255,255,.03)', borderBottom: '1px solid var(--bdL)' }}>
|
||||
{['알고리즘', '유형', '장점', '단점', 'WING 활용'].map(h => (
|
||||
@ -435,8 +435,8 @@ function OptimizationPanel() {
|
||||
<tr key={i} style={{ borderBottom: '1px solid rgba(255,255,255,.04)', background: i % 2 === 1 ? 'rgba(255,255,255,.01)' : undefined }}>
|
||||
<td className="py-[7px] px-2.5 font-bold" style={{ color: row.color }}>{row.name}</td>
|
||||
<td className="py-[7px] px-2.5 text-center text-text-2">{row.type}</td>
|
||||
<td className="py-[7px] px-2.5 text-center text-text-2" style={{ whiteSpace: 'pre-line' }}>{row.pros}</td>
|
||||
<td className="py-[7px] px-2.5 text-center text-text-2" style={{ whiteSpace: 'pre-line' }}>{row.cons}</td>
|
||||
<td className="py-[7px] px-2.5 text-center text-text-2 whitespace-pre-line">{row.pros}</td>
|
||||
<td className="py-[7px] px-2.5 text-center text-text-2 whitespace-pre-line">{row.cons}</td>
|
||||
<td className="py-[7px] px-2.5 text-center" style={{ color: row.wingColor }}>{row.wing}</td>
|
||||
</tr>
|
||||
))}
|
||||
|
||||
@ -75,18 +75,18 @@ ${styles}
|
||||
<div>
|
||||
<div className="text-base font-bold">유출유 확산 모델 이론 및 검증</div>
|
||||
<div className="flex items-center gap-2.5 mt-1 flex-wrap">
|
||||
<span className="text-[11px] font-semibold">🔷 <span style={{ color: '#06b6d4' }}>KOSPS</span></span>
|
||||
<span className="text-[11px] font-semibold">🔴 <span style={{ color: '#ef4444' }}>POSEIDON</span></span>
|
||||
<span className="text-[11px] font-semibold">🔵 <span style={{ color: '#3b82f6' }}>OpenDrift</span></span>
|
||||
<span className="text-[11px] font-semibold">⚡ <span style={{ color: '#a855f7' }}>앙상블</span></span>
|
||||
<span className="text-[11px] font-semibold">🔷 <span className="text-cyan-500">KOSPS</span></span>
|
||||
<span className="text-[11px] font-semibold">🔴 <span className="text-red-500">POSEIDON</span></span>
|
||||
<span className="text-[11px] font-semibold">🔵 <span className="text-blue-500">OpenDrift</span></span>
|
||||
<span className="text-[11px] font-semibold">⚡ <span className="text-[#a855f7]">앙상블</span></span>
|
||||
<span className="text-[10px] text-text-3">라그랑지안 입자추적 이론 기반</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button onClick={handleExportPDF}
|
||||
data-html2pdf-ignore
|
||||
className="px-3.5 py-1.5 rounded-md text-[11px] font-semibold cursor-pointer"
|
||||
style={{ border: '1px solid rgba(59,130,246,.3)', background: 'rgba(59,130,246,.08)', color: 'var(--blue)' }}>
|
||||
className="px-3.5 py-1.5 rounded-md text-[11px] font-semibold cursor-pointer text-primary-blue"
|
||||
style={{ border: '1px solid rgba(59,130,246,.3)', background: 'rgba(59,130,246,.08)' }}>
|
||||
📤 PDF 내보내기
|
||||
</button>
|
||||
</div>
|
||||
@ -97,10 +97,9 @@ ${styles}
|
||||
<button key={tab.id} onClick={() => setActivePanel(tab.id)}
|
||||
className="flex-1 py-2 px-1 text-[12px] font-semibold rounded-md transition-all duration-150 cursor-pointer"
|
||||
style={{
|
||||
border: activePanel === tab.id ? '1px solid var(--cyan)' : '1px solid var(--bd)',
|
||||
border: activePanel === tab.id ? '1px solid var(--cyan)' : '1px solid var(--bd)',
|
||||
color: activePanel === tab.id ? 'var(--cyan)' : 'var(--t3)',
|
||||
background: activePanel === tab.id ? 'rgba(6,182,212,.08)' : 'var(--bg3)',
|
||||
fontWeight: 600,
|
||||
}}>
|
||||
{tab.icon}{' '}{tab.nameColor
|
||||
? <span style={{ color: tab.nameColor }}>{tab.name}</span>
|
||||
@ -132,7 +131,7 @@ const bodyText = "text-xs text-text-2 leading-[1.8]"
|
||||
const codeBox = "bg-bg-0 rounded-sm p-[10px] font-mono text-xs leading-loose"
|
||||
const tag = (color: string) => ({
|
||||
padding: '3px 8px', borderRadius: '4px', fontSize: '11px', color,
|
||||
background: `${color}14`, border: `1px solid ${color}30`
|
||||
background: `${color}14`, border: `1px solid ${color}30`,
|
||||
} as const)
|
||||
|
||||
/* ═══ 패널 0: 시스템 개요 ═══ */
|
||||
@ -149,7 +148,7 @@ function SystemOverviewPanel() {
|
||||
<div className="w-[30px] h-[30px] rounded-lg flex items-center justify-center text-base" style={{ background: 'rgba(6,182,212,.15)' }}>📋</div>
|
||||
<span className="text-sm font-bold">유출유 확산 모델이란?</span>
|
||||
</div>
|
||||
<div style={{ ...bodyText, fontSize: '12px', lineHeight: '1.8' }}>
|
||||
<div className="text-xs text-text-2 leading-[1.8] text-[12px]">
|
||||
해상에서 유출된 <b className="text-status-orange">원유 및 유류</b>가 해수면 위에서 이동·확산·풍화되는 과정을 물리·화학적 이론에 기반해 전산으로 모의하는 프로그램입니다. <b className="text-primary-cyan">라그랑지안 입자추적법</b>을 기반으로 해류·바람·조류의 복합 영향을 시뮬레이션합니다.
|
||||
</div>
|
||||
</div>
|
||||
@ -158,15 +157,15 @@ function SystemOverviewPanel() {
|
||||
<div className="w-[30px] h-[30px] rounded-lg flex items-center justify-center text-base" style={{ background: 'rgba(59,130,246,.15)' }}>🎯</div>
|
||||
<span className="text-sm font-bold">활용 목적</span>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5" style={{ fontSize: '12px', color: 'var(--t2)' }}>
|
||||
<div className="flex flex-col gap-1.5 text-[12px] text-text-2">
|
||||
<div className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-md" style={{ background: 'rgba(239,68,68,.04)', border: '1px solid rgba(239,68,68,.12)' }}>
|
||||
<span style={{ color: 'var(--red)', fontWeight: 700 }}>①</span> 유출 사고 시 <b>오염 확산 범위 신속 예측</b>
|
||||
<span className="text-status-red font-bold">①</span> 유출 사고 시 <b>오염 확산 범위 신속 예측</b>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-md" style={{ background: 'rgba(249,115,22,.04)', border: '1px solid rgba(249,115,22,.12)' }}>
|
||||
<span className="text-status-orange font-bold">②</span> <b>방제 자원 배치</b> 및 오일펜스 전략 수립
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-md" style={{ background: 'rgba(34,197,94,.04)', border: '1px solid rgba(34,197,94,.12)' }}>
|
||||
<span style={{ color: 'var(--green)', fontWeight: 700 }}>③</span> 해안선 도달 예측 및 <b>피해 최소화</b> 대응
|
||||
<span className="text-status-green font-bold">③</span> 해안선 도달 예측 및 <b>피해 최소화</b> 대응
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -177,8 +176,8 @@ function SystemOverviewPanel() {
|
||||
<div className="grid grid-cols-3 gap-3 mb-4">
|
||||
{/* Step 1 */}
|
||||
<div className={`${card} ${cardBg}`} style={{ borderTop: '3px solid var(--orange)', position: 'relative' }}>
|
||||
<div className="absolute top-2.5 right-3 w-7 h-7 rounded-full flex items-center justify-center text-xs font-extrabold"
|
||||
style={{ background: 'rgba(249,115,22,.15)', color: 'var(--orange)', fontFamily: 'var(--fM)' }}>1</div>
|
||||
<div className="absolute top-2.5 right-3 w-7 h-7 rounded-full flex items-center justify-center text-xs font-extrabold font-mono"
|
||||
style={{ background: 'rgba(249,115,22,.15)', color: 'var(--orange)' }}>1</div>
|
||||
<div style={labelStyle('var(--orange)')}>🛢️ 유류 물성 반영</div>
|
||||
<div className="text-[11px] mb-2.5 text-text-2 leading-[1.7]">유류 종류별 <b>물리·화학적 특성</b>을 모델에 반영하여 실제 거동을 재현합니다.</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
@ -191,8 +190,8 @@ function SystemOverviewPanel() {
|
||||
</div>
|
||||
{/* Step 2 */}
|
||||
<div className={`${card} ${cardBg}`} style={{ borderTop: '3px solid var(--cyan)', position: 'relative' }}>
|
||||
<div className="absolute top-2.5 right-3 w-7 h-7 rounded-full flex items-center justify-center text-xs font-extrabold"
|
||||
style={{ background: 'rgba(6,182,212,.15)', color: 'var(--cyan)', fontFamily: 'var(--fM)' }}>2</div>
|
||||
<div className="absolute top-2.5 right-3 w-7 h-7 rounded-full flex items-center justify-center text-xs font-extrabold font-mono"
|
||||
style={{ background: 'rgba(6,182,212,.15)', color: 'var(--cyan)' }}>2</div>
|
||||
<div style={labelStyle('var(--cyan)')}>🌊 해양환경 데이터 수집</div>
|
||||
<div className="text-[11px] mb-2.5 text-text-2 leading-[1.7]"><b>실시간 해양·기상 관측 데이터</b>를 수집하여 모델 강제력으로 입력합니다.</div>
|
||||
<div className="flex flex-col gap-1 text-[10px] text-text-2">
|
||||
@ -203,8 +202,8 @@ function SystemOverviewPanel() {
|
||||
</div>
|
||||
{/* Step 3 */}
|
||||
<div className={`${card} ${cardBg}`} style={{ borderTop: '3px solid var(--green)', position: 'relative' }}>
|
||||
<div className="absolute top-2.5 right-3 w-7 h-7 rounded-full flex items-center justify-center text-xs font-extrabold"
|
||||
style={{ background: 'rgba(34,197,94,.15)', color: 'var(--green)', fontFamily: 'var(--fM)' }}>3</div>
|
||||
<div className="absolute top-2.5 right-3 w-7 h-7 rounded-full flex items-center justify-center text-xs font-extrabold font-mono"
|
||||
style={{ background: 'rgba(34,197,94,.15)', color: 'var(--green)' }}>3</div>
|
||||
<div style={labelStyle('var(--green)')}>🧮 수치 시뮬레이션</div>
|
||||
<div className="text-[11px] mb-2.5 text-text-2 leading-[1.7]"><b>라그랑지안 입자 시뮬레이션</b>으로 유출유 이동·확산·풍화를 동시 계산합니다.</div>
|
||||
<div className="flex flex-col gap-1 text-[10px] text-text-2">
|
||||
@ -251,10 +250,10 @@ function SystemOverviewPanel() {
|
||||
<table className="w-full border-collapse" style={{ fontSize: '11px' }}>
|
||||
<thead>
|
||||
<tr style={{ background: 'rgba(255,255,255,.03)', borderBottom: '1px solid var(--bdL)' }}>
|
||||
<th style={{ padding: '8px 12px', textAlign: 'left', color: 'var(--t3)', fontWeight: 600, width: '15%' }}>구분</th>
|
||||
<th style={{ padding: '8px 12px', textAlign: 'center', color: 'var(--cyan)', fontWeight: 700, width: '28%' }}>🔷 KOSPS</th>
|
||||
<th style={{ padding: '8px 12px', textAlign: 'center', color: 'var(--blue)', fontWeight: 700, width: '28%' }}>🔵 POSEIDON</th>
|
||||
<th style={{ padding: '8px 12px', textAlign: 'center', color: 'var(--blue)', fontWeight: 700, width: '29%' }}>🔵 OpenDrift</th>
|
||||
<th className="py-2 px-3 text-left text-text-3 font-semibold" style={{ width: '15%' }}>구분</th>
|
||||
<th className="py-2 px-3 text-center text-primary-cyan font-bold" style={{ width: '28%' }}>🔷 KOSPS</th>
|
||||
<th className="py-2 px-3 text-center text-primary-blue font-bold" style={{ width: '28%' }}>🔵 POSEIDON</th>
|
||||
<th className="py-2 px-3 text-center text-primary-blue font-bold" style={{ width: '29%' }}>🔵 OpenDrift</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@ -277,7 +276,7 @@ function SystemOverviewPanel() {
|
||||
{ label: 'WING 활용\n역할', k: '<span style="color:var(--cyan);font-weight:600">한국 서해·남해<br>조류 정밀 예측</span><br><span style="color:var(--t3)">ESI 방제지도 연동</span>', p: '<span style="color:var(--blue);font-weight:600">매개변수 최적화<br>정확도 향상</span><br><span style="color:var(--t3)">뜰개 관측 동화</span>', o: '<span style="color:var(--green);font-weight:600">광역·3D 확산<br>다중 시나리오</span><br><span style="color:var(--t3)">오픈소스 모듈 확장</span>' },
|
||||
].map((row, i) => (
|
||||
<tr key={row.label} style={{ borderBottom: '1px solid rgba(255,255,255,.04)', background: i % 2 ? 'rgba(255,255,255,.01)' : 'transparent' }}>
|
||||
<td style={{ padding: '7px 12px', color: 'var(--t3)' }} dangerouslySetInnerHTML={{ __html: sanitizeHtml(row.label.replace(/\n/g, '<br>')) }} />
|
||||
<td className="py-[7px] px-3 text-text-3" dangerouslySetInnerHTML={{ __html: sanitizeHtml(row.label.replace(/\n/g, '<br>')) }} />
|
||||
<td style={{ padding: '7px 12px', textAlign: 'center', fontSize: '10px' }} dangerouslySetInnerHTML={{ __html: sanitizeHtml(row.k) }} />
|
||||
<td style={{ padding: '7px 12px', textAlign: 'center', fontSize: '10px' }} dangerouslySetInnerHTML={{ __html: sanitizeHtml(row.p) }} />
|
||||
<td style={{ padding: '7px 12px', textAlign: 'center', fontSize: '10px' }} dangerouslySetInnerHTML={{ __html: sanitizeHtml(row.o) }} />
|
||||
@ -1012,7 +1011,7 @@ function OpenDriftPanel() {
|
||||
<div key={w.title} className="p-2.5 rounded-lg text-center" style={{ background: `${w.color}0A`, border: `1px solid ${w.color}25` }}>
|
||||
<div className="text-base mb-1">{w.icon}</div>
|
||||
<div className="text-[11px] font-semibold" style={{ color: w.color }}>{w.title}</div>
|
||||
<div className="text-[10px] mt-1" style={{ color: 'var(--t3)', lineHeight: '1.5' }}>{w.desc}</div>
|
||||
<div className="text-[10px] mt-1 text-text-3 leading-normal">{w.desc}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@ -1084,13 +1083,13 @@ function OpenDriftPanel() {
|
||||
<div className="mb-2">
|
||||
<div className="text-[11px] mb-1 text-text-2"><b>적용 강제력 모델 6종 조합</b></div>
|
||||
<div className="grid grid-cols-3 gap-1 text-[10px] text-text-2">
|
||||
<div className="px-2 py-1 rounded" style={{ background: 'rgba(59,130,246,.05)', border: '1px solid rgba(59,130,246,.12)', color: 'var(--t2)' }}><b style={{ color: '#3b82f6' }}>해류</b> HYCOM / CMEMS</div>
|
||||
<div className="px-2 py-1 rounded" style={{ background: 'rgba(59,130,246,.05)', border: '1px solid rgba(59,130,246,.12)', color: 'var(--t2)' }}><b className="text-blue-500">해류</b> HYCOM / CMEMS</div>
|
||||
<div className="px-2 py-1 rounded" style={{ background: 'rgba(34,197,94,.05)', border: '1px solid rgba(34,197,94,.12)', color: 'var(--t2)' }}><b className="text-status-green">파랑</b> CMEMS / ECMWF-ERA5</div>
|
||||
<div className="px-2 py-1 rounded" style={{ background: 'rgba(249,115,22,.05)', border: '1px solid rgba(249,115,22,.12)', color: 'var(--t2)' }}><b className="text-status-orange">기상</b> NCEP-GFS / ECMWF</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-2 rounded-md text-[10px]" style={{ background: 'rgba(59,130,246,.04)', border: '1px solid rgba(59,130,246,.1)', color: 'var(--t2)', lineHeight: '1.7' }}>
|
||||
<b style={{ color: '#3b82f6' }}>WING 적용 의의</b> : OpenOil이 한국 해역에서 실효적으로 작동함을 검증한 국내 최신 연구. 6종 Met-Ocean 조합 비교는 WING의 OpenDrift 강제력 선택 기준(CMEMS+ECMWF)의 직접적 근거.
|
||||
<b className="text-blue-500">WING 적용 의의</b> : OpenOil이 한국 해역에서 실효적으로 작동함을 검증한 국내 최신 연구. 6종 Met-Ocean 조합 비교는 WING의 OpenDrift 강제력 선택 기준(CMEMS+ECMWF)의 직접적 근거.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1117,12 +1116,12 @@ function OpenDriftPanel() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-1 text-[10px] mb-2 text-text-2">
|
||||
<div className="px-2 py-1 rounded" style={{ background: 'rgba(6,182,212,.05)', border: '1px solid rgba(6,182,212,.12)', color: 'var(--t2)' }}><b style={{ color: '#06b6d4' }}>해수유동</b> Navier-Stokes 2D 연속·운동방정식</div>
|
||||
<div className="px-2 py-1 rounded" style={{ background: 'rgba(6,182,212,.05)', border: '1px solid rgba(6,182,212,.12)', color: 'var(--t2)' }}><b className="text-cyan-500">해수유동</b> Navier-Stokes 2D 연속·운동방정식</div>
|
||||
<div className="px-2 py-1 rounded" style={{ background: 'rgba(168,85,247,.05)', border: '1px solid rgba(168,85,247,.12)', color: 'var(--t2)' }}><b className="text-primary-purple">조류</b> 조화분석 실시간 조류 DB 구축</div>
|
||||
<div className="px-2 py-1 rounded" style={{ background: 'rgba(249,115,22,.05)', border: '1px solid rgba(249,115,22,.12)', color: 'var(--t2)' }}><b className="text-status-orange">풍화</b> 퍼짐·증발·유상화·생분해 5단계</div>
|
||||
</div>
|
||||
<div className="p-2 rounded-md text-[10px]" style={{ background: 'rgba(6,182,212,.04)', border: '1px solid rgba(6,182,212,.1)', color: 'var(--t2)', lineHeight: '1.7' }}>
|
||||
<b style={{ color: '#06b6d4' }}>WING 적용 의의</b> : OpenDrift·KOSPS의 라그랑지안 입자추적 이론적 원전. 조류 영향 미약 해역에서 취송류·해류 성분이 확산에 결정적 역할 — OpenDrift 한국 해역 적용 정당성 근거.
|
||||
<b className="text-cyan-500">WING 적용 의의</b> : OpenDrift·KOSPS의 라그랑지안 입자추적 이론적 원전. 조류 영향 미약 해역에서 취송류·해류 성분이 확산에 결정적 역할 — OpenDrift 한국 해역 적용 정당성 근거.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1160,7 +1159,7 @@ function OpenDriftPanel() {
|
||||
<div className="px-2 py-1 rounded text-center" style={{ background: 'rgba(239,68,68,.06)', border: '1px solid rgba(239,68,68,.12)' }}><div className="font-bold text-status-red">α = 3%</div><div className="text-text-3">과대 확산</div></div>
|
||||
<div className="px-2 py-1 rounded text-center" style={{ background: 'rgba(234,179,8,.06)', border: '1px solid rgba(234,179,8,.12)' }}><div className="font-bold text-status-yellow">α = 2.5%</div><div className="text-text-3">다소 빠름</div></div>
|
||||
<div className="px-2 py-1 rounded text-center" style={{ background: 'rgba(34,197,94,.08)', border: '1px solid rgba(34,197,94,.2)' }}><div className="font-bold text-status-green">α = 2% ✓</div><div className="text-text-3">최적 일치</div></div>
|
||||
<div className="px-2 py-1 rounded text-center" style={{ background: 'rgba(6,182,212,.06)', border: '1px solid rgba(6,182,212,.12)' }}><div className="font-bold" style={{ color: '#06b6d4' }}>θ = 20° ✓</div><div className="text-text-3">최적 편향각</div></div>
|
||||
<div className="px-2 py-1 rounded text-center" style={{ background: 'rgba(6,182,212,.06)', border: '1px solid rgba(6,182,212,.12)' }}><div className="font-bold" className="text-cyan-500">θ = 20° ✓</div><div className="text-text-3">최적 편향각</div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-2 rounded-md text-[10px]" style={{ background: 'rgba(249,115,22,.04)', border: '1px solid rgba(249,115,22,.1)', color: 'var(--t2)', lineHeight: '1.7' }}>
|
||||
@ -1290,7 +1289,7 @@ function WeatheringPanel() {
|
||||
<div className="flex-1 p-2.5 text-center" style={{ background: `${s.color}0F`, border: `1px solid ${s.color}33`, borderRadius: i === 0 ? '8px 0 0 8px' : i === 3 ? '0 8px 8px 0' : '0' }}>
|
||||
<div className="text-[10px] font-bold mb-1" style={{ color: s.color }}>{s.time}</div>
|
||||
<div className="text-[11px] font-semibold mb-1">{s.title}</div>
|
||||
<div className="text-[10px] whitespace-pre-line" style={{ color: 'var(--t3)', lineHeight: '1.5' }}>{s.desc}</div>
|
||||
<div className="text-[10px] whitespace-pre-line text-text-3 leading-normal">{s.desc}</div>
|
||||
</div>
|
||||
{s.nextColor && <div style={{ width: '2px', background: `linear-gradient(180deg,${s.color},${s.nextColor})` }} />}
|
||||
</div>
|
||||
@ -1503,7 +1502,7 @@ function RoadmapPanel() {
|
||||
<div className="flex-1 p-2.5 text-center" style={{ background: `${s.color}0F`, border: `1px solid ${s.color}33`, borderRadius: i === 0 ? '8px 0 0 8px' : i === 3 ? '0 8px 8px 0' : '0' }}>
|
||||
<div className="text-[10px] font-bold mb-1" style={{ color: s.color }}>{s.phase}</div>
|
||||
<div className="text-[11px] font-semibold mb-1">{s.title}</div>
|
||||
<div className="text-[10px] whitespace-pre-line" style={{ color: 'var(--t3)', lineHeight: '1.5' }}>{s.desc}</div>
|
||||
<div className="text-[10px] whitespace-pre-line text-text-3 leading-normal">{s.desc}</div>
|
||||
</div>
|
||||
{s.nextColor && <div style={{ width: '2px', background: `linear-gradient(180deg,${s.color},${s.nextColor})` }} />}
|
||||
</div>
|
||||
|
||||
@ -79,26 +79,28 @@ const PredictionInputSection = ({
|
||||
</div>
|
||||
|
||||
{expanded && (
|
||||
<div className="px-4 pb-4" style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
|
||||
<div className="px-4 pb-4 flex flex-col gap-[6px]">
|
||||
{/* Input Mode Selection */}
|
||||
<div style={{ display: 'flex', gap: '10px', alignItems: 'center', fontSize: '11px' }}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: '3px', cursor: 'pointer' }}>
|
||||
<div className="flex items-center gap-[10px] text-[11px]">
|
||||
<label className="flex items-center gap-[3px] cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
name="prdType"
|
||||
checked={inputMode === 'direct'}
|
||||
onChange={() => setInputMode('direct')}
|
||||
style={{ accentColor: 'var(--cyan)', margin: 0, width: '11px', height: '11px' }}
|
||||
className="m-0 w-[11px] h-[11px]"
|
||||
className="accent-[var(--cyan)]"
|
||||
/>
|
||||
직접 입력
|
||||
</label>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: '3px', cursor: 'pointer' }}>
|
||||
<label className="flex items-center gap-[3px] cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
name="prdType"
|
||||
checked={inputMode === 'upload'}
|
||||
onChange={() => setInputMode('upload')}
|
||||
style={{ accentColor: 'var(--cyan)', margin: 0, width: '11px', height: '11px' }}
|
||||
className="m-0 w-[11px] h-[11px]"
|
||||
className="accent-[var(--cyan)]"
|
||||
/>
|
||||
이미지 업로드
|
||||
</label>
|
||||
@ -129,78 +131,55 @@ const PredictionInputSection = ({
|
||||
|
||||
{/* Upload Success Message */}
|
||||
{uploadedImage && (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '6px',
|
||||
padding: '6px 8px',
|
||||
background: 'rgba(34,197,94,0.1)',
|
||||
border: '1px solid rgba(34,197,94,0.3)',
|
||||
borderRadius: 'var(--rS)',
|
||||
fontSize: '10px',
|
||||
color: '#22c55e',
|
||||
fontWeight: 600
|
||||
}}>
|
||||
<span style={{ fontSize: '12px' }}>✓</span>
|
||||
<div className="flex items-center gap-[6px] text-[10px] font-semibold text-[#22c55e] rounded"
|
||||
style={{
|
||||
padding: '6px 8px',
|
||||
background: 'rgba(34,197,94,0.1)',
|
||||
border: '1px solid rgba(34,197,94,0.3)',
|
||||
borderRadius: 'var(--rS)',
|
||||
}}>
|
||||
<span className="text-[12px]">✓</span>
|
||||
내 이미지가 업로드됨
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* File Upload Area */}
|
||||
{!uploadedImage ? (
|
||||
<label style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: '20px',
|
||||
background: 'var(--bg0)',
|
||||
border: '2px dashed var(--bd)',
|
||||
borderRadius: 'var(--rS)',
|
||||
cursor: 'pointer',
|
||||
transition: '0.15s',
|
||||
fontSize: '11px',
|
||||
color: 'var(--t3)',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.borderColor = 'var(--cyan)'
|
||||
e.currentTarget.style.background = 'rgba(6,182,212,0.05)'
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.borderColor = 'var(--bd)'
|
||||
e.currentTarget.style.background = 'var(--bg0)'
|
||||
}}>
|
||||
<label className="flex items-center justify-center text-[11px] text-text-3 cursor-pointer"
|
||||
style={{
|
||||
padding: '20px',
|
||||
background: 'var(--bg0)',
|
||||
border: '2px dashed var(--bd)',
|
||||
borderRadius: 'var(--rS)',
|
||||
transition: '0.15s',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.borderColor = 'var(--cyan)'
|
||||
e.currentTarget.style.background = 'rgba(6,182,212,0.05)'
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.borderColor = 'var(--bd)'
|
||||
e.currentTarget.style.background = 'var(--bg0)'
|
||||
}}>
|
||||
📁 이미지 파일을 선택하세요
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handleImageUpload}
|
||||
style={{ display: 'none' }}
|
||||
className="hidden"
|
||||
/>
|
||||
</label>
|
||||
) : (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: '8px 10px',
|
||||
background: 'var(--bg0)',
|
||||
border: '1px solid var(--bd)',
|
||||
borderRadius: 'var(--rS)',
|
||||
fontSize: '10px',
|
||||
fontFamily: 'var(--fM)'
|
||||
}}>
|
||||
<div className="flex items-center justify-between font-mono text-[10px] bg-bg-0 border border-border"
|
||||
style={{
|
||||
padding: '8px 10px',
|
||||
borderRadius: 'var(--rS)',
|
||||
}}>
|
||||
<span className="text-text-2">📄 {uploadedFileName || 'example_plot_0.gif'}</span>
|
||||
<button
|
||||
onClick={removeUploadedImage}
|
||||
style={{
|
||||
padding: '2px 6px',
|
||||
fontSize: '10px',
|
||||
color: 'var(--t3)',
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
transition: '0.15s'
|
||||
}}
|
||||
className="text-[10px] text-text-3 bg-transparent border-none cursor-pointer"
|
||||
style={{ padding: '2px 6px', transition: '0.15s' }}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.color = 'var(--red)'
|
||||
}}
|
||||
@ -214,7 +193,7 @@ const PredictionInputSection = ({
|
||||
)}
|
||||
|
||||
{/* Dropdowns */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '4px' }}>
|
||||
<div className="grid grid-cols-2 gap-1">
|
||||
<ComboBox
|
||||
className="prd-i"
|
||||
value=""
|
||||
@ -242,8 +221,8 @@ const PredictionInputSection = ({
|
||||
)}
|
||||
|
||||
{/* Coordinates + Map Button */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr auto', gap: '4px', alignItems: 'center' }}>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="grid items-center gap-1" style={{ gridTemplateColumns: '1fr 1fr auto' }}>
|
||||
<input
|
||||
className="prd-i"
|
||||
type="number"
|
||||
@ -270,22 +249,18 @@ const PredictionInputSection = ({
|
||||
</div>
|
||||
{/* 도분초 표시 */}
|
||||
{incidentCoord && !isNaN(incidentCoord.lat) && !isNaN(incidentCoord.lon) && (
|
||||
<div style={{
|
||||
fontSize: '9px',
|
||||
color: 'var(--t3)',
|
||||
fontFamily: 'var(--fM)',
|
||||
padding: '4px 8px',
|
||||
background: 'var(--bg0)',
|
||||
borderRadius: 'var(--rS)',
|
||||
border: '1px solid var(--bd)'
|
||||
}}>
|
||||
<div className="text-[9px] text-text-3 font-mono border border-border bg-bg-0"
|
||||
style={{
|
||||
padding: '4px 8px',
|
||||
borderRadius: 'var(--rS)',
|
||||
}}>
|
||||
{decimalToDMS(incidentCoord.lat, true)} / {decimalToDMS(incidentCoord.lon, false)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Oil Type + Oil Kind */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '4px' }}>
|
||||
<div className="grid grid-cols-2 gap-1">
|
||||
<ComboBox
|
||||
className="prd-i"
|
||||
value={spillType}
|
||||
@ -312,7 +287,7 @@ const PredictionInputSection = ({
|
||||
</div>
|
||||
|
||||
{/* Volume + Unit + Duration */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 65px 1fr', gap: '4px', alignItems: 'center' }}>
|
||||
<div className="grid items-center gap-1" style={{ gridTemplateColumns: '1fr 65px 1fr' }}>
|
||||
<input
|
||||
className="prd-i"
|
||||
placeholder="유출량"
|
||||
@ -348,24 +323,22 @@ const PredictionInputSection = ({
|
||||
|
||||
{/* Image Analysis Note (Upload Mode Only) */}
|
||||
{inputMode === 'upload' && uploadedImage && (
|
||||
<div style={{
|
||||
padding: '8px',
|
||||
background: 'rgba(59,130,246,0.08)',
|
||||
border: '1px solid rgba(59,130,246,0.2)',
|
||||
borderRadius: 'var(--rS)',
|
||||
fontSize: '9px',
|
||||
color: 'var(--t3)',
|
||||
lineHeight: '1.4'
|
||||
}}>
|
||||
<div className="text-[9px] text-text-3 leading-[1.4]"
|
||||
style={{
|
||||
padding: '8px',
|
||||
background: 'rgba(59,130,246,0.08)',
|
||||
border: '1px solid rgba(59,130,246,0.2)',
|
||||
borderRadius: 'var(--rS)',
|
||||
}}>
|
||||
📊 이미지 내 확산경로를 분석하였습니다. 각 방제요소 가이드 참고하세요.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Divider */}
|
||||
<div style={{ height: '1px', background: 'var(--bd)', margin: '2px 0' }} />
|
||||
<div className="h-px bg-border my-0.5" />
|
||||
|
||||
{/* Model Selection (다중 선택) */}
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '3px' }}>
|
||||
<div className="flex flex-wrap gap-[3px]">
|
||||
{([
|
||||
{ id: 'KOSPS' as PredictionModel, color: 'var(--cyan)' },
|
||||
{ id: 'POSEIDON' as PredictionModel, color: 'var(--red)' },
|
||||
@ -373,7 +346,7 @@ const PredictionInputSection = ({
|
||||
] as const).map(m => (
|
||||
<div
|
||||
key={m.id}
|
||||
className={`prd-mc ${selectedModels.has(m.id) ? 'on' : ''}`}
|
||||
className={`prd-mc ${selectedModels.has(m.id) ? 'on' : ''} cursor-pointer`}
|
||||
onClick={() => {
|
||||
const next = new Set(selectedModels)
|
||||
if (next.has(m.id)) {
|
||||
@ -383,14 +356,13 @@ const PredictionInputSection = ({
|
||||
}
|
||||
onModelsChange(next)
|
||||
}}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
<span className="prd-md" style={{ background: m.color }} />
|
||||
{m.id}
|
||||
</div>
|
||||
))}
|
||||
<div
|
||||
className={`prd-mc ${selectedModels.size === ALL_MODELS.length ? 'on' : ''}`}
|
||||
className={`prd-mc ${selectedModels.size === ALL_MODELS.length ? 'on' : ''} cursor-pointer`}
|
||||
onClick={() => {
|
||||
if (selectedModels.size === ALL_MODELS.length) {
|
||||
onModelsChange(new Set(['KOSPS']))
|
||||
@ -398,7 +370,6 @@ const PredictionInputSection = ({
|
||||
onModelsChange(new Set(ALL_MODELS))
|
||||
}
|
||||
}}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
<span className="prd-md" style={{ background: 'var(--purple)' }} />
|
||||
앙상블
|
||||
@ -407,8 +378,8 @@ const PredictionInputSection = ({
|
||||
|
||||
{/* Run Button */}
|
||||
<button
|
||||
className="prd-btn pri"
|
||||
style={{ padding: '7px', fontSize: '11px', marginTop: '2px' }}
|
||||
className="prd-btn pri mt-0.5"
|
||||
style={{ padding: '7px', fontSize: '11px' }}
|
||||
onClick={onRunSimulation}
|
||||
disabled={isRunningSimulation}
|
||||
>
|
||||
|
||||
@ -227,7 +227,7 @@ function CheckboxLabel({ checked, children }: { checked?: boolean; children: str
|
||||
type="checkbox"
|
||||
defaultChecked={checked}
|
||||
className="w-[13px] h-[13px]"
|
||||
style={{ accentColor: 'var(--cyan)' }}
|
||||
className="accent-[var(--cyan)]"
|
||||
/>
|
||||
{children}
|
||||
</label>
|
||||
|
||||
@ -249,7 +249,7 @@ function ReportGenerator({ onSave }: ReportGeneratorProps) {
|
||||
</>
|
||||
)}
|
||||
{sec.id === 'oil-pollution' && (
|
||||
<table className="w-full table-fixed" style={{ borderCollapse: 'collapse' }}>
|
||||
<table className="w-full table-fixed" className="border-collapse">
|
||||
<colgroup><col style={{ width: '25%' }} /><col style={{ width: '25%' }} /><col style={{ width: '25%' }} /><col style={{ width: '25%' }} /></colgroup>
|
||||
<tbody>
|
||||
{[
|
||||
@ -327,7 +327,7 @@ function ReportGenerator({ onSave }: ReportGeneratorProps) {
|
||||
{ label: '대피 권고 범위', value: sampleHnsData.hazard.evacuation, color: '#a855f7', desc: '안전거리' },
|
||||
].map((h, i) => (
|
||||
<div key={i} className="bg-bg-1 border border-border rounded-lg p-4 text-center">
|
||||
<p className="text-[9px] font-korean mb-1" style={{ color: h.color, fontWeight: 700 }}>{h.label}</p>
|
||||
<p className="text-[9px] font-bold font-korean mb-1" style={{ color: h.color }}>{h.label}</p>
|
||||
<p className="text-[18px] font-bold font-mono" style={{ color: h.color }}>{h.value}</p>
|
||||
<p className="text-[8px] text-text-3 font-korean mt-1">{h.desc}</p>
|
||||
</div>
|
||||
@ -335,7 +335,7 @@ function ReportGenerator({ onSave }: ReportGeneratorProps) {
|
||||
</div>
|
||||
)}
|
||||
{sec.id === 'hns-substance' && (
|
||||
<div className="grid grid-cols-2 gap-2" style={{ fontSize: '11px' }}>
|
||||
<div className="grid grid-cols-2 gap-2 text-[11px]">
|
||||
{[
|
||||
{ k: '물질명', v: sampleHnsData.substance.name },
|
||||
{ k: 'UN번호', v: sampleHnsData.substance.un },
|
||||
@ -347,9 +347,9 @@ function ReportGenerator({ onSave }: ReportGeneratorProps) {
|
||||
<span className="text-text-1 font-semibold font-mono">{r.v}</span>
|
||||
</div>
|
||||
))}
|
||||
<div className="col-span-2 flex justify-between px-3 py-2 bg-bg-1 rounded border border-border" style={{ borderColor: 'rgba(239,68,68,0.3)' }}>
|
||||
<div className="col-span-2 flex justify-between px-3 py-2 bg-bg-1 rounded border border-[rgba(239,68,68,0.3)]">
|
||||
<span className="text-text-3 font-korean">독성기준</span>
|
||||
<span style={{ color: 'var(--red)', fontWeight: 600, fontFamily: 'var(--fM)', fontSize: '10px' }}>{sampleHnsData.substance.toxicity}</span>
|
||||
<span className="text-[var(--red)] font-semibold font-mono text-[10px]">{sampleHnsData.substance.toxicity}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@ -370,7 +370,7 @@ function ReportGenerator({ onSave }: ReportGeneratorProps) {
|
||||
{ label: '주변 인구', value: sampleHnsData.facility.population, icon: '👥' },
|
||||
].map((f, i) => (
|
||||
<div key={i} className="bg-bg-1 border border-border rounded-lg p-4 text-center">
|
||||
<div style={{ fontSize: '18px', marginBottom: '4px' }}>{f.icon}</div>
|
||||
<div className="text-[18px] mb-1">{f.icon}</div>
|
||||
<p className="text-[14px] font-bold text-text-1 font-mono">{f.value}</p>
|
||||
<p className="text-[9px] text-text-3 font-korean mt-1">{f.label}</p>
|
||||
</div>
|
||||
@ -391,7 +391,7 @@ function ReportGenerator({ onSave }: ReportGeneratorProps) {
|
||||
{ label: '기온', value: '8.5°C', icon: '☀️' },
|
||||
].map((w, i) => (
|
||||
<div key={i} className="bg-bg-1 border border-border rounded-lg p-3 text-center">
|
||||
<div style={{ fontSize: '16px', marginBottom: '2px' }}>{w.icon}</div>
|
||||
<div className="text-[16px] mb-0.5">{w.icon}</div>
|
||||
<p className="text-[13px] font-bold text-text-1 font-mono">{w.value}</p>
|
||||
<p className="text-[8px] text-text-3 font-korean mt-1">{w.label}</p>
|
||||
</div>
|
||||
@ -424,7 +424,7 @@ function ReportGenerator({ onSave }: ReportGeneratorProps) {
|
||||
{ time: '07:15', event: '해경 3009함 현장 도착, 방제 개시', color: '#06b6d4' },
|
||||
].map((e, i) => (
|
||||
<div key={i} className="flex items-center gap-3 px-3 py-2 bg-bg-1 rounded border border-border">
|
||||
<span className="font-mono text-[11px] font-bold" style={{ color: e.color, minWidth: '40px' }}>{e.time}</span>
|
||||
<span className="font-mono text-[11px] font-bold min-w-[40px]" style={{ color: e.color }}>{e.time}</span>
|
||||
<span className="text-[11px] text-text-2 font-korean">{e.event}</span>
|
||||
</div>
|
||||
))}
|
||||
@ -447,7 +447,7 @@ function ReportGenerator({ onSave }: ReportGeneratorProps) {
|
||||
</div>
|
||||
)}
|
||||
{sec.id === 'rescue-resource' && (
|
||||
<table className="w-full" style={{ borderCollapse: 'collapse', fontSize: '11px' }}>
|
||||
<table className="w-full text-[11px]" className="border-collapse">
|
||||
<thead>
|
||||
<tr className="border-b border-border">
|
||||
<th className="px-3 py-2 text-left text-text-3 font-korean">유형</th>
|
||||
@ -498,7 +498,7 @@ function ReportGenerator({ onSave }: ReportGeneratorProps) {
|
||||
{ label: '시정', value: '8 km', icon: '👁' },
|
||||
].map((w, i) => (
|
||||
<div key={i} className="bg-bg-1 border border-border rounded-lg p-3 text-center">
|
||||
<div style={{ fontSize: '16px', marginBottom: '2px' }}>{w.icon}</div>
|
||||
<div className="text-[16px] mb-0.5">{w.icon}</div>
|
||||
<p className="text-[13px] font-bold text-text-1 font-mono">{w.value}</p>
|
||||
<p className="text-[8px] text-text-3 font-korean mt-1">{w.label}</p>
|
||||
</div>
|
||||
|
||||
@ -117,7 +117,7 @@ export function ReportsView() {
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex-1 overflow-auto">
|
||||
<table className="w-full table-fixed" style={{ borderCollapse: 'collapse' }}>
|
||||
<table className="w-full table-fixed" className="border-collapse">
|
||||
<colgroup>
|
||||
<col style={{ width: '3%' }} />
|
||||
<col style={{ width: '4%' }} />
|
||||
|
||||
@ -128,7 +128,7 @@ function TemplateFormEditor({ onSave, onBack }: TemplateFormEditorProps) {
|
||||
<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-[13px] font-bold font-korean mb-3" style={{ color: '#06b6d4' }}>{section.title}</h4>
|
||||
<h4 className="text-[13px] font-bold font-korean mb-3" className="text-cyan-500">{section.title}</h4>
|
||||
<table className="w-full table-fixed border-collapse">
|
||||
<colgroup>
|
||||
<col style={{ width: '180px' }} />
|
||||
@ -235,7 +235,7 @@ function TemplateFormEditor({ onSave, onBack }: TemplateFormEditorProps) {
|
||||
{/* Report Title */}
|
||||
<div className="text-center mb-8">
|
||||
<h2 className="text-[18px] font-bold text-text-1 font-korean mb-1">해양오염방제지원시스템</h2>
|
||||
<h3 className="text-[15px] font-semibold font-korean" style={{ color: '#06b6d4' }}>
|
||||
<h3 className="text-[15px] font-semibold font-korean" className="text-cyan-500">
|
||||
{formData['incident.name'] || template.label}
|
||||
</h3>
|
||||
<p className="text-[11px] text-text-3 font-korean mt-2">
|
||||
|
||||
@ -215,7 +215,7 @@ export function RescueScenarioView() {
|
||||
style={{ border: `1px solid ${isSel ? 'rgba(6,182,212,.35)' : 'var(--bd)'}`, background: isSel ? 'rgba(6,182,212,.04)' : 'var(--bg3)' }}>
|
||||
{/* Top: checkbox + ID + severity */}
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<input type="checkbox" checked={checked.has(sc.id)} onChange={e => { e.stopPropagation(); toggleCheck(sc.id) }} style={{ accentColor: 'var(--cyan)' }} />
|
||||
<input type="checkbox" checked={checked.has(sc.id)} onChange={e => { e.stopPropagation(); toggleCheck(sc.id) }} className="accent-[var(--cyan)]" />
|
||||
<span className="text-xs font-extrabold font-mono" style={{ color: isSel ? 'var(--cyan)' : 'var(--t1)' }}>{sc.id}</span>
|
||||
<span className="px-1.5 py-px rounded text-[8px] font-bold font-mono" style={{ background: sev.bg, color: sev.color }}>{sev.label}</span>
|
||||
<span className="ml-auto text-[9px] text-text-3 font-mono">{sc.timeStep}</span>
|
||||
|
||||
@ -173,15 +173,14 @@ function CenterMap({ activeType }: { activeType: AccidentType }) {
|
||||
</div>
|
||||
|
||||
{/* 선박 모형 */}
|
||||
<div className="absolute z-[15]" style={{ top: '42%', left: '46%', transform: 'rotate(-15deg)' }}>
|
||||
<div className="relative" style={{
|
||||
width: '72px', height: '20px',
|
||||
<div className="absolute z-[15] top-[42%] left-[46%] -rotate-[15deg]">
|
||||
<div className="relative w-[72px] h-5" style={{
|
||||
background: 'linear-gradient(90deg, #4a3728, #6b4c33)',
|
||||
borderRadius: '3px 10px 10px 3px',
|
||||
border: '1px solid rgba(255,150,50,.4)',
|
||||
boxShadow: '0 0 18px rgba(255,100,0,.2)'
|
||||
}}>
|
||||
<div className="absolute" style={{ top: '-3px', left: '60%', width: '2px', height: '7px', background: '#888', borderRadius: '1px' }} />
|
||||
<div className="absolute w-0.5 h-[7px] bg-[#888] rounded-[1px]" style={{ top: '-3px', left: '60%' }} />
|
||||
</div>
|
||||
<div className="text-[7px] text-center mt-0.5 font-mono text-[rgba(255,200,150,0.5)]">M/V SEA GUARDIAN</div>
|
||||
</div>
|
||||
@ -193,23 +192,19 @@ function CenterMap({ activeType }: { activeType: AccidentType }) {
|
||||
border: '1.5px dashed rgba(6,182,212,.2)'
|
||||
}} />
|
||||
{/* 구역 라벨 */}
|
||||
<div className="absolute z-[6] text-[8px] font-korean font-semibold tracking-wider uppercase whitespace-pre-line text-[rgba(6,182,212,0.45)]" style={{
|
||||
top: '50%', left: '36%',
|
||||
}}>
|
||||
<div className="absolute z-[6] text-[8px] font-korean font-semibold tracking-wider uppercase whitespace-pre-line text-[rgba(6,182,212,0.45)] top-1/2 left-[36%]">
|
||||
{d.zone.replace('\\n', '\n')}
|
||||
</div>
|
||||
|
||||
{/* SAR 자산 */}
|
||||
<div className="absolute z-10 text-[7px] font-mono text-[rgba(200,220,255,0.35)]" style={{ top: '10%', left: '42%' }}>ETA 5 MIN ─</div>
|
||||
<div className="absolute z-10 text-[7px] font-mono text-[rgba(200,220,255,0.35)]" style={{ top: '14%', left: '56%' }}>ETA 15 MIN ─</div>
|
||||
<div className="absolute z-[12] text-sm opacity-60" style={{ top: '7%', left: '52%', transform: 'rotate(-30deg)' }}>🚁</div>
|
||||
<div className="absolute z-[12] text-[8px] font-mono text-[rgba(200,220,255,0.45)]" style={{ top: '20%', left: '60%' }}>6M</div>
|
||||
<div className="absolute z-[12] text-[11px] opacity-45" style={{ top: '28%', left: '54%' }}>🚢</div>
|
||||
<div className="absolute z-10 text-[7px] font-mono text-[rgba(200,220,255,0.35)] top-[10%] left-[42%]">ETA 5 MIN ─</div>
|
||||
<div className="absolute z-10 text-[7px] font-mono text-[rgba(200,220,255,0.35)] top-[14%] left-[56%]">ETA 15 MIN ─</div>
|
||||
<div className="absolute z-[12] text-sm opacity-60 top-[7%] left-[52%] -rotate-[30deg]">🚁</div>
|
||||
<div className="absolute z-[12] text-[8px] font-mono text-[rgba(200,220,255,0.45)] top-[20%] left-[60%]">6M</div>
|
||||
<div className="absolute z-[12] text-[11px] opacity-45 top-[28%] left-[54%]">🚢</div>
|
||||
|
||||
{/* 환경 민감 구역 */}
|
||||
<div className="absolute z-10 px-3.5 py-2 rounded bg-[rgba(34,197,94,0.06)] border border-[rgba(34,197,94,0.2)]" style={{
|
||||
bottom: '6%', right: '6%',
|
||||
}}>
|
||||
<div className="absolute z-10 px-3.5 py-2 rounded bg-[rgba(34,197,94,0.06)] border border-[rgba(34,197,94,0.2)] bottom-[6%] right-[6%]">
|
||||
<div className="text-[9px] font-bold font-serif uppercase tracking-wider leading-snug text-[rgba(34,197,94,0.55)]">
|
||||
ENVIRONMENTALLY SENSITIVE<br />AREA: AQUACULTURE FARM
|
||||
</div>
|
||||
|
||||
@ -238,7 +238,7 @@ function ScatMap({ segments, selectedSeg, onSelectSeg, onOpenPopup }: ScatMapPro
|
||||
<Map
|
||||
initialViewState={{ longitude: 126.55, latitude: 33.38, zoom: 10 }}
|
||||
mapStyle={BASE_STYLE}
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
className="w-full h-full"
|
||||
attributionControl={false}
|
||||
onZoom={e => setZoom(e.viewState.zoom)}
|
||||
>
|
||||
|
||||
@ -121,7 +121,7 @@ function PopupMap({
|
||||
key={`${lat}-${lng}`}
|
||||
initialViewState={{ longitude: lng, latitude: lat, zoom: 15 }}
|
||||
mapStyle={BASE_STYLE}
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
className="w-full h-full"
|
||||
attributionControl={false}
|
||||
>
|
||||
<DeckGLOverlay layers={deckLayers} />
|
||||
|
||||
@ -360,7 +360,7 @@ export function WeatherView() {
|
||||
zoom: WEATHER_MAP_ZOOM,
|
||||
}}
|
||||
mapStyle={WEATHER_MAP_STYLE}
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
className="w-full h-full"
|
||||
onClick={handleMapClick}
|
||||
attributionControl={false}
|
||||
>
|
||||
|
||||
불러오는 중...
Reference in New Issue
Block a user