- 백엔드 vessels 라우터/서비스/스케줄러 추가 (1분 주기 한국 해역 폴링) - 공통 useVesselSignals 훅 + vesselApi/vesselSignalClient 서비스 추가 - MapView에 VesselLayer/VesselInteraction/MapBoundsTracker 통합 (호버·팝업·상세 모달) - OilSpill/HNS/Rescue/Incidents 뷰에 선박 신호 연동 - vesselMockData 정리, aerial IMAGE_API_URL 기본값 변경
648 lines
19 KiB
TypeScript
648 lines
19 KiB
TypeScript
import { useState } from 'react';
|
|
import type { VesselPosition } from '@common/types/vessel';
|
|
import { getShipKindLabel } from './VesselLayer';
|
|
|
|
export interface VesselHoverInfo {
|
|
x: number;
|
|
y: number;
|
|
vessel: VesselPosition;
|
|
}
|
|
|
|
function formatDateTime(iso: string): string {
|
|
const d = new Date(iso);
|
|
if (Number.isNaN(d.getTime())) return '-';
|
|
const pad = (n: number) => String(n).padStart(2, '0');
|
|
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
}
|
|
|
|
function displayVal(v: unknown): string {
|
|
if (v === undefined || v === null || v === '') return '-';
|
|
return String(v);
|
|
}
|
|
|
|
export function VesselHoverTooltip({ hover }: { hover: VesselHoverInfo }) {
|
|
const v = hover.vessel;
|
|
const speed = v.sog !== undefined ? `${v.sog.toFixed(1)} kn` : '- kn';
|
|
const heading = v.heading ?? v.cog;
|
|
const headingText = heading !== undefined ? `HDG ${Math.round(heading)}°` : 'HDG -';
|
|
const typeText = [getShipKindLabel(v.shipKindCode) ?? v.shipTy ?? '-', v.nationalCode]
|
|
.filter(Boolean)
|
|
.join(' · ');
|
|
return (
|
|
<div
|
|
className="absolute z-[1000] pointer-events-none rounded-md"
|
|
style={{
|
|
left: hover.x + 12,
|
|
top: hover.y - 12,
|
|
background: 'var(--bg-elevated)',
|
|
border: '1px solid var(--stroke-default)',
|
|
padding: '8px 12px',
|
|
boxShadow: '0 8px 24px rgba(0,0,0,0.5)',
|
|
minWidth: 150,
|
|
}}
|
|
>
|
|
<div className="text-label-2 font-bold text-fg" style={{ marginBottom: 3 }}>
|
|
{v.shipNm ?? '(이름 없음)'}
|
|
</div>
|
|
<div className="text-caption text-fg-disabled" style={{ marginBottom: 4 }}>
|
|
{typeText}
|
|
</div>
|
|
<div className="flex justify-between text-caption">
|
|
<span className="text-color-accent font-semibold">{speed}</span>
|
|
<span className="text-fg-disabled">{headingText}</span>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function VesselPopupPanel({
|
|
vessel: v,
|
|
onClose,
|
|
onDetail,
|
|
}: {
|
|
vessel: VesselPosition;
|
|
onClose: () => void;
|
|
onDetail: () => void;
|
|
}) {
|
|
const statusText = v.status ?? '-';
|
|
const isAccident = (v.status ?? '').includes('사고');
|
|
const statusColor = isAccident ? 'var(--color-danger)' : 'var(--color-success)';
|
|
const statusBg = isAccident
|
|
? 'color-mix(in srgb, var(--color-danger) 15%, transparent)'
|
|
: 'color-mix(in srgb, var(--color-success) 10%, transparent)';
|
|
const speed = v.sog !== undefined ? `${v.sog.toFixed(1)} kn` : '-';
|
|
const heading = v.heading ?? v.cog;
|
|
const headingText = heading !== undefined ? `${Math.round(heading)}°` : '-';
|
|
const receivedAt = v.lastUpdate ? formatDateTime(v.lastUpdate) : '-';
|
|
|
|
return (
|
|
<div
|
|
style={{
|
|
position: 'absolute',
|
|
top: '50%',
|
|
left: '50%',
|
|
transform: 'translate(-50%,-50%)',
|
|
zIndex: 9995,
|
|
width: 300,
|
|
background: 'rgba(13,17,23,0.97)',
|
|
border: '1px solid rgba(48,54,61,0.8)',
|
|
borderRadius: 12,
|
|
boxShadow: '0 16px 48px rgba(0,0,0,0.7)',
|
|
overflow: 'hidden',
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: 8,
|
|
padding: '10px 14px',
|
|
background: 'rgba(22,27,34,0.97)',
|
|
borderBottom: '1px solid rgba(48,54,61,0.8)',
|
|
}}
|
|
>
|
|
<div
|
|
className="flex items-center justify-center text-title-2"
|
|
style={{ width: 28, height: 20 }}
|
|
>
|
|
{v.nationalCode ?? '🚢'}
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<div
|
|
className="text-label-1 font-[800] whitespace-nowrap overflow-hidden text-ellipsis"
|
|
style={{ color: '#e6edf3' }}
|
|
>
|
|
{v.shipNm ?? '(이름 없음)'}
|
|
</div>
|
|
<div className="text-caption font-mono" style={{ color: '#8b949e' }}>
|
|
MMSI: {v.mmsi}
|
|
</div>
|
|
</div>
|
|
<span
|
|
onClick={onClose}
|
|
className="text-title-3 cursor-pointer p-[2px]"
|
|
style={{ color: '#8b949e' }}
|
|
>
|
|
✕
|
|
</span>
|
|
</div>
|
|
|
|
<div
|
|
className="w-full flex items-center justify-center text-[40px]"
|
|
style={{
|
|
height: 120,
|
|
background: 'rgba(22,27,34,0.97)',
|
|
borderBottom: '1px solid rgba(48,54,61,0.6)',
|
|
color: '#484f58',
|
|
}}
|
|
>
|
|
🚢
|
|
</div>
|
|
|
|
<div
|
|
className="flex gap-2"
|
|
style={{ padding: '6px 14px', borderBottom: '1px solid rgba(48,54,61,0.6)' }}
|
|
>
|
|
<span
|
|
className="text-caption font-bold rounded text-color-info"
|
|
style={{
|
|
padding: '2px 8px',
|
|
background: 'color-mix(in srgb, var(--color-info) 12%, transparent)',
|
|
border: '1px solid color-mix(in srgb, var(--color-info) 25%, transparent)',
|
|
}}
|
|
>
|
|
{getShipKindLabel(v.shipKindCode) ?? v.shipTy ?? '-'}
|
|
</span>
|
|
<span
|
|
className="text-caption font-bold rounded"
|
|
style={{
|
|
padding: '2px 8px',
|
|
background: statusBg,
|
|
border: `1px solid color-mix(in srgb, ${statusColor} 40%, transparent)`,
|
|
color: statusColor,
|
|
}}
|
|
>
|
|
{statusText}
|
|
</span>
|
|
</div>
|
|
|
|
<div style={{ padding: '4px 0' }}>
|
|
<PopupRow label="속도/항로" value={`${speed} / ${headingText}`} accent />
|
|
<PopupRow label="흘수" value={v.draught !== undefined ? `${v.draught.toFixed(2)} m` : '-'} />
|
|
<div
|
|
className="flex flex-col gap-1"
|
|
style={{
|
|
padding: '6px 14px',
|
|
borderBottom: '1px solid rgba(48,54,61,0.4)',
|
|
}}
|
|
>
|
|
<div className="flex justify-between">
|
|
<span className="text-caption" style={{ color: '#8b949e' }}>
|
|
출항지
|
|
</span>
|
|
<span className="text-caption font-semibold font-mono" style={{ color: '#c9d1d9' }}>
|
|
-
|
|
</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-caption" style={{ color: '#8b949e' }}>
|
|
입항지
|
|
</span>
|
|
<span className="text-caption font-semibold font-mono" style={{ color: '#c9d1d9' }}>
|
|
{v.destination ?? '-'}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<PopupRow label="데이터 수신" value={receivedAt} muted />
|
|
</div>
|
|
|
|
<div className="flex gap-1.5" style={{ padding: '10px 14px' }}>
|
|
<button
|
|
onClick={onDetail}
|
|
className="flex-1 text-caption font-bold cursor-pointer text-center rounded-sm"
|
|
style={{
|
|
padding: 6,
|
|
color: '#58a6ff',
|
|
background: 'rgba(88,166,255,0.12)',
|
|
border: '1px solid rgba(88,166,255,0.3)',
|
|
}}
|
|
>
|
|
📋 상세정보
|
|
</button>
|
|
<button
|
|
className="flex-1 text-caption font-bold cursor-pointer text-center rounded-sm"
|
|
style={{
|
|
padding: 6,
|
|
color: '#a5d6ff',
|
|
background: 'rgba(165,214,255,0.1)',
|
|
border: '1px solid rgba(165,214,255,0.25)',
|
|
}}
|
|
>
|
|
🔍 항적조회
|
|
</button>
|
|
<button
|
|
className="flex-1 text-caption font-bold cursor-pointer text-center rounded-sm"
|
|
style={{
|
|
padding: 6,
|
|
color: '#22d3ee',
|
|
background: 'rgba(34,211,238,0.1)',
|
|
border: '1px solid rgba(34,211,238,0.25)',
|
|
}}
|
|
>
|
|
📐 항로예측
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function PopupRow({
|
|
label,
|
|
value,
|
|
accent,
|
|
muted,
|
|
}: {
|
|
label: string;
|
|
value: string;
|
|
accent?: boolean;
|
|
muted?: boolean;
|
|
}) {
|
|
return (
|
|
<div
|
|
className="flex justify-between text-caption"
|
|
style={{
|
|
padding: '6px 14px',
|
|
borderBottom: '1px solid rgba(48,54,61,0.4)',
|
|
}}
|
|
>
|
|
<span style={{ color: '#8b949e' }}>{label}</span>
|
|
<span
|
|
className="font-semibold font-mono"
|
|
style={{
|
|
color: muted ? '#8b949e' : accent ? '#22d3ee' : '#c9d1d9',
|
|
fontSize: muted ? 9 : 10,
|
|
}}
|
|
>
|
|
{value}
|
|
</span>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
type DetTab = 'info' | 'nav' | 'spec' | 'ins' | 'dg';
|
|
const TAB_LABELS: { key: DetTab; label: string }[] = [
|
|
{ key: 'info', label: '상세정보' },
|
|
{ key: 'nav', label: '항해정보' },
|
|
{ key: 'spec', label: '선박제원' },
|
|
{ key: 'ins', label: '보험정보' },
|
|
{ key: 'dg', label: '위험물정보' },
|
|
];
|
|
|
|
export function VesselDetailModal({
|
|
vessel: v,
|
|
onClose,
|
|
}: {
|
|
vessel: VesselPosition;
|
|
onClose: () => void;
|
|
}) {
|
|
const [tab, setTab] = useState<DetTab>('info');
|
|
|
|
return (
|
|
<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.65)',
|
|
backdropFilter: 'blur(6px)',
|
|
}}
|
|
>
|
|
<div
|
|
className="flex flex-col overflow-hidden bg-bg-surface border border-stroke"
|
|
style={{
|
|
width: 560,
|
|
height: '85vh',
|
|
borderRadius: 14,
|
|
boxShadow: '0 24px 64px rgba(0,0,0,0.7)',
|
|
}}
|
|
>
|
|
<div
|
|
className="shrink-0 flex items-center justify-between bg-bg-surface border-b border-stroke"
|
|
style={{ padding: '14px 18px' }}
|
|
>
|
|
<div className="flex items-center gap-[10px]">
|
|
<span className="text-lg">{v.nationalCode ?? '🚢'}</span>
|
|
<div>
|
|
<div className="text-title-3 font-[800] text-fg">{v.shipNm ?? '(이름 없음)'}</div>
|
|
<div className="text-caption text-fg-disabled font-mono">
|
|
MMSI: {v.mmsi} · IMO: {displayVal(v.imo)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<span onClick={onClose} className="text-title-2 cursor-pointer text-fg-disabled">
|
|
✕
|
|
</span>
|
|
</div>
|
|
|
|
<div
|
|
className="shrink-0 flex gap-0.5 overflow-x-auto bg-bg-base border-b border-stroke"
|
|
style={{ padding: '0 18px' }}
|
|
>
|
|
{TAB_LABELS.map((t) => (
|
|
<button
|
|
key={t.key}
|
|
onClick={() => setTab(t.key)}
|
|
className="text-label-2 cursor-pointer whitespace-nowrap"
|
|
style={{
|
|
padding: '8px 11px',
|
|
fontWeight: tab === t.key ? 600 : 400,
|
|
color: tab === t.key ? 'var(--color-accent)' : 'var(--fg-disabled)',
|
|
borderBottom:
|
|
tab === t.key ? '2px solid var(--color-accent)' : '2px solid transparent',
|
|
background: 'none',
|
|
border: 'none',
|
|
transition: '0.15s',
|
|
}}
|
|
>
|
|
{t.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
<div
|
|
className="flex-1 overflow-y-auto flex flex-col"
|
|
style={{
|
|
padding: '16px 18px',
|
|
gap: 14,
|
|
scrollbarWidth: 'thin',
|
|
scrollbarColor: 'var(--stroke-default) transparent',
|
|
}}
|
|
>
|
|
{tab === 'info' && <TabInfo v={v} />}
|
|
{tab === 'nav' && <TabNav />}
|
|
{tab === 'spec' && <TabSpec v={v} />}
|
|
{tab === 'ins' && <TabInsurance />}
|
|
{tab === 'dg' && <TabDangerous />}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function Sec({
|
|
title,
|
|
borderColor,
|
|
bgColor,
|
|
badge,
|
|
children,
|
|
}: {
|
|
title: string;
|
|
borderColor?: string;
|
|
bgColor?: string;
|
|
badge?: React.ReactNode;
|
|
children: React.ReactNode;
|
|
}) {
|
|
return (
|
|
<div
|
|
style={{
|
|
border: `1px solid ${borderColor || 'var(--stroke-default)'}`,
|
|
borderRadius: 8,
|
|
overflow: 'hidden',
|
|
}}
|
|
>
|
|
<div
|
|
className="text-label-2 font-bold text-fg-sub flex items-center justify-between"
|
|
style={{
|
|
padding: '8px 12px',
|
|
background: bgColor || 'var(--bg-base)',
|
|
borderBottom: `1px solid ${borderColor || 'var(--stroke-default)'}`,
|
|
}}
|
|
>
|
|
<span>{title}</span>
|
|
{badge}
|
|
</div>
|
|
{children}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function Grid({ children }: { children: React.ReactNode }) {
|
|
return <div className="grid grid-cols-2">{children}</div>;
|
|
}
|
|
|
|
function Cell({
|
|
label,
|
|
value,
|
|
span,
|
|
color,
|
|
}: {
|
|
label: string;
|
|
value: string;
|
|
span?: boolean;
|
|
color?: string;
|
|
}) {
|
|
return (
|
|
<div
|
|
style={{
|
|
padding: '8px 12px',
|
|
borderBottom: '1px solid color-mix(in srgb, var(--stroke-default) 60%, transparent)',
|
|
borderRight: span
|
|
? 'none'
|
|
: '1px solid color-mix(in srgb, var(--stroke-default) 60%, transparent)',
|
|
gridColumn: span ? '1 / -1' : undefined,
|
|
}}
|
|
>
|
|
<div className="text-caption text-fg-disabled" style={{ marginBottom: 2 }}>
|
|
{label}
|
|
</div>
|
|
<div
|
|
className="text-label-2 font-semibold font-mono"
|
|
style={{ color: color || 'var(--fg)' }}
|
|
>
|
|
{value}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function StatusBadge({ label, color }: { label: string; color: string }) {
|
|
return (
|
|
<span
|
|
className="text-caption font-bold"
|
|
style={{
|
|
padding: '2px 6px',
|
|
borderRadius: 8,
|
|
marginLeft: 'auto',
|
|
background: `color-mix(in srgb, ${color} 25%, transparent)`,
|
|
color,
|
|
}}
|
|
>
|
|
{label}
|
|
</span>
|
|
);
|
|
}
|
|
|
|
function TabInfo({ v }: { v: VesselPosition }) {
|
|
const speed = v.sog !== undefined ? `${v.sog.toFixed(1)} kn` : '-';
|
|
const heading = v.heading ?? v.cog;
|
|
const headingText = heading !== undefined ? `${Math.round(heading)}°` : '-';
|
|
return (
|
|
<>
|
|
<div
|
|
className="w-full rounded-lg overflow-hidden flex items-center justify-center text-[60px] text-fg-disabled bg-bg-base"
|
|
style={{ height: 160 }}
|
|
>
|
|
🚢
|
|
</div>
|
|
|
|
<Sec title="📡 실시간 현황">
|
|
<Grid>
|
|
<Cell label="선박상태" value={displayVal(v.status)} />
|
|
<Cell
|
|
label="속도 / 항로"
|
|
value={`${speed} / ${headingText}`}
|
|
color="var(--color-accent)"
|
|
/>
|
|
<Cell label="위도" value={`${v.lat.toFixed(4)}°N`} />
|
|
<Cell label="경도" value={`${v.lon.toFixed(4)}°E`} />
|
|
<Cell label="흘수" value={v.draught !== undefined ? `${v.draught} m` : '-'} />
|
|
<Cell label="수신시간" value={v.lastUpdate ? formatDateTime(v.lastUpdate) : '-'} />
|
|
</Grid>
|
|
</Sec>
|
|
|
|
<Sec title="🚢 항해 일정">
|
|
<Grid>
|
|
<Cell label="출항지" value="-" />
|
|
<Cell label="입항지" value={displayVal(v.destination)} />
|
|
<Cell label="출항일시" value="-" />
|
|
<Cell label="입항일시(ETA)" value="-" />
|
|
</Grid>
|
|
</Sec>
|
|
</>
|
|
);
|
|
}
|
|
|
|
function TabNav() {
|
|
const hours = ['08', '09', '10', '11', '12', '13', '14'];
|
|
const heights = [45, 60, 78, 82, 70, 85, 75];
|
|
const colors = [
|
|
'color-mix(in srgb, var(--color-success) 30%, transparent)',
|
|
'color-mix(in srgb, var(--color-success) 40%, transparent)',
|
|
'color-mix(in srgb, var(--color-info) 40%, transparent)',
|
|
'color-mix(in srgb, var(--color-info) 50%, transparent)',
|
|
'color-mix(in srgb, var(--color-info) 50%, transparent)',
|
|
'color-mix(in srgb, var(--color-info) 60%, transparent)',
|
|
'color-mix(in srgb, var(--color-accent) 50%, transparent)',
|
|
];
|
|
|
|
return (
|
|
<>
|
|
<Sec title="🗺 최근 항적 (24h)">
|
|
<div
|
|
className="flex items-center justify-center relative overflow-hidden bg-bg-base"
|
|
style={{ height: 180 }}
|
|
>
|
|
<svg
|
|
width="100%"
|
|
height="100%"
|
|
viewBox="0 0 400 180"
|
|
style={{ position: 'absolute', inset: 0 }}
|
|
>
|
|
<path
|
|
d="M50,150 C80,140 120,100 160,95 S240,70 280,50 S340,30 370,20"
|
|
fill="none"
|
|
stroke="var(--color-accent)"
|
|
strokeWidth="2"
|
|
strokeDasharray="6,3"
|
|
opacity=".6"
|
|
/>
|
|
<circle cx="50" cy="150" r="4" fill="var(--fg-disabled)" />
|
|
<circle cx="160" cy="95" r="3" fill="var(--color-accent)" opacity=".5" />
|
|
<circle cx="280" cy="50" r="3" fill="var(--color-accent)" opacity=".5" />
|
|
<circle cx="370" cy="20" r="5" fill="var(--color-accent)" />
|
|
</svg>
|
|
</div>
|
|
</Sec>
|
|
|
|
<Sec title="📊 속도 이력">
|
|
<div className="p-3 bg-bg-base">
|
|
<div className="flex items-end gap-1.5" style={{ height: 80 }}>
|
|
{hours.map((h, i) => (
|
|
<div key={h} className="flex-1 flex flex-col items-center gap-0.5">
|
|
<div
|
|
className="w-full"
|
|
style={{
|
|
background: colors[i],
|
|
borderRadius: '2px 2px 0 0',
|
|
height: `${heights[i]}%`,
|
|
}}
|
|
/>
|
|
<span className="text-caption text-fg-disabled">{h}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
<div className="text-center text-caption text-fg-disabled" style={{ marginTop: 6 }}>
|
|
평균: <b className="text-color-info">8.4 kn</b> · 최대:{' '}
|
|
<b className="text-color-accent">11.2 kn</b>
|
|
</div>
|
|
</div>
|
|
</Sec>
|
|
</>
|
|
);
|
|
}
|
|
|
|
function TabSpec({ v }: { v: VesselPosition }) {
|
|
const loa = v.length !== undefined ? `${v.length} m` : '-';
|
|
const beam = v.width !== undefined ? `${v.width} m` : '-';
|
|
return (
|
|
<>
|
|
<Sec title="📐 선체 제원">
|
|
<Grid>
|
|
<Cell label="선종" value={displayVal(getShipKindLabel(v.shipKindCode) ?? v.shipTy)} />
|
|
<Cell label="선적국" value={displayVal(v.nationalCode)} />
|
|
<Cell label="총톤수 (GT)" value="-" />
|
|
<Cell label="재화중량 (DWT)" value="-" />
|
|
<Cell label="전장 (LOA)" value={loa} />
|
|
<Cell label="선폭" value={beam} />
|
|
<Cell label="건조년도" value="-" />
|
|
<Cell label="건조 조선소" value="-" />
|
|
</Grid>
|
|
</Sec>
|
|
|
|
<Sec title="📡 통신 / 식별">
|
|
<Grid>
|
|
<Cell label="MMSI" value={String(v.mmsi)} />
|
|
<Cell label="IMO" value={displayVal(v.imo)} />
|
|
<Cell label="호출부호" value="-" />
|
|
<Cell label="선급" value="-" />
|
|
</Grid>
|
|
</Sec>
|
|
</>
|
|
);
|
|
}
|
|
|
|
function TabInsurance() {
|
|
return (
|
|
<>
|
|
<Sec title="🏢 선주 / 운항사">
|
|
<Grid>
|
|
<Cell label="선주" value="-" />
|
|
<Cell label="운항사" value="-" />
|
|
<Cell label="P&I Club" value="-" span />
|
|
</Grid>
|
|
</Sec>
|
|
|
|
<Sec
|
|
title="🚢 선체보험 (H&M)"
|
|
borderColor="color-mix(in srgb, var(--color-accent) 20%, transparent)"
|
|
bgColor="color-mix(in srgb, var(--color-accent) 6%, transparent)"
|
|
badge={<StatusBadge label="-" color="var(--color-success)" />}
|
|
>
|
|
<Grid>
|
|
<Cell label="보험사" value="-" />
|
|
<Cell label="보험가액" value="-" color="var(--color-accent)" />
|
|
<Cell label="보험기간" value="-" color="var(--color-success)" />
|
|
<Cell label="면책금" value="-" />
|
|
</Grid>
|
|
</Sec>
|
|
</>
|
|
);
|
|
}
|
|
|
|
function TabDangerous() {
|
|
return (
|
|
<Sec
|
|
title="⚠ 위험물 화물 신고정보"
|
|
bgColor="color-mix(in srgb, var(--color-warning) 6%, transparent)"
|
|
>
|
|
<Grid>
|
|
<Cell label="화물명" value="-" color="var(--color-warning)" />
|
|
<Cell label="컨테이너갯수/총량" value="-" />
|
|
<Cell label="하역업체코드" value="-" />
|
|
<Cell label="하역기간" value="-" />
|
|
</Grid>
|
|
</Sec>
|
|
);
|
|
}
|