feat(design): 디자인 시스템 시맨틱 토큰 전환 및 다크/라이트 테마 전환 기능 #142
@ -8,6 +8,12 @@
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;600;700;900&family=JetBrains+Mono:wght@400;500;600&family=Outfit:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
|
||||
<title>frontend</title>
|
||||
<script>
|
||||
document.documentElement.setAttribute(
|
||||
'data-theme',
|
||||
localStorage.getItem('wing-theme') || 'dark'
|
||||
);
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@ -61,9 +61,9 @@ function App() {
|
||||
<div style={{
|
||||
width: '100vw', height: '100vh', display: 'flex',
|
||||
flexDirection: 'column', alignItems: 'center', justifyContent: 'center',
|
||||
background: '#001028', gap: 16,
|
||||
background: 'var(--bg-base)', gap: 16,
|
||||
}}>
|
||||
<img src="/wing_logo_text_white.svg" alt="WING" style={{ height: 28, opacity: 0.8 }} />
|
||||
<img src="/wing_logo_text_white.svg" alt="WING" className="wing-logo" style={{ height: 28, opacity: 0.8 }} />
|
||||
<div style={{
|
||||
width: 32, height: 32, border: '3px solid rgba(6,182,212,0.2)',
|
||||
borderTop: '3px solid rgba(6,182,212,0.8)', borderRadius: '50%',
|
||||
|
||||
@ -55,7 +55,7 @@ export function LoginPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-screen h-screen flex overflow-hidden relative bg-[#001028]">
|
||||
<div className="w-screen h-screen flex overflow-hidden relative bg-bg-base">
|
||||
{/* Background image */}
|
||||
<div style={{
|
||||
position: 'absolute', inset: 0,
|
||||
@ -82,7 +82,7 @@ export function LoginPage() {
|
||||
<img
|
||||
src="/wing_logo_text_white.svg"
|
||||
alt="WING 해양환경 위기대응 통합시스템"
|
||||
className="h-7 mx-auto block"
|
||||
className="h-7 mx-auto block wing-logo"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@ -3,6 +3,7 @@ import type { MainTab } from '../../types/navigation'
|
||||
import { useAuthStore } from '../../store/authStore'
|
||||
import { useMenuStore } from '../../store/menuStore'
|
||||
import { useMapStore } from '../../store/mapStore'
|
||||
import { useThemeStore } from '../../store/themeStore'
|
||||
import UserManualPopup from '../ui/UserManualPopup'
|
||||
|
||||
interface TopBarProps {
|
||||
@ -17,6 +18,7 @@ export function TopBar({ activeTab, onTabChange }: TopBarProps) {
|
||||
const { hasPermission, user, logout } = useAuthStore()
|
||||
const { menuConfig, isLoaded } = useMenuStore()
|
||||
const { mapToggles, toggleMap, mapTypes, measureMode, setMeasureMode } = useMapStore()
|
||||
const { theme, toggleTheme } = useThemeStore()
|
||||
|
||||
const MAP_TABS = new Set<string>(['prediction', 'hns', 'scat', 'incidents'])
|
||||
const isMapTab = MAP_TABS.has(activeTab)
|
||||
@ -53,7 +55,7 @@ export function TopBar({ activeTab, onTabChange }: TopBarProps) {
|
||||
className="flex items-center hover:opacity-80 transition-opacity cursor-pointer"
|
||||
title="홈으로 이동"
|
||||
>
|
||||
<img src="/wing_logo_white.svg" alt="WING 해양환경 위기대응" className="h-3.5" />
|
||||
<img src="/wing_logo_white.svg" alt="WING 해양환경 위기대응" className="h-3.5 wing-logo" />
|
||||
</button>
|
||||
|
||||
{/* Divider */}
|
||||
@ -83,21 +85,21 @@ export function TopBar({ activeTab, onTabChange }: TopBarProps) {
|
||||
${isMonitor ? 'border-l border-l-[rgba(239,68,68,0.25)] ml-1 flex items-center gap-1.5' : ''}
|
||||
${
|
||||
isMonitor
|
||||
? 'text-[#f87171] hover:text-[#fca5a5] hover:bg-[rgba(239,68,68,0.1)]'
|
||||
? 'text-color-danger hover:text-[#fca5a5] hover:bg-[rgba(239,68,68,0.1)]'
|
||||
: activeTab === tab.id
|
||||
? isIncident
|
||||
? 'text-[#a5b4fc] bg-[rgba(99,102,241,0.18)] shadow-[0_0_8px_rgba(99,102,241,0.3)]'
|
||||
: 'text-[#22d3ee] bg-[rgba(6,182,212,0.15)] shadow-[0_0_8px_rgba(6,182,212,0.3)]'
|
||||
: 'text-color-accent bg-[rgba(6,182,212,0.15)] shadow-[0_0_8px_rgba(6,182,212,0.3)]'
|
||||
: isIncident
|
||||
? 'text-[#818cf8] hover:text-[#a5b4fc] hover:bg-[rgba(99,102,241,0.1)]'
|
||||
: 'text-[#c8d6e5] hover:text-white hover:bg-[rgba(255,255,255,0.08)]'
|
||||
: 'text-fg-sub hover:text-fg hover:bg-[var(--hover-overlay)]'
|
||||
}
|
||||
`}
|
||||
>
|
||||
{isMonitor ? (
|
||||
<>
|
||||
<span className="hidden xl:flex items-center gap-1.5">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-[#f87171] animate-pulse inline-block" />
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-color-danger animate-pulse inline-block" />
|
||||
{tab.label}
|
||||
</span>
|
||||
<span className="xl:hidden text-[1rem] leading-none">{tab.icon}</span>
|
||||
@ -165,7 +167,7 @@ export function TopBar({ activeTab, onTabChange }: TopBarProps) {
|
||||
</button>
|
||||
|
||||
{showQuickMenu && (
|
||||
<div className="absolute top-[44px] right-0 w-[220px] bg-[rgba(18,25,41,0.97)] backdrop-blur-xl border border-stroke rounded-lg shadow-2xl z-[200] py-2 font-korean">
|
||||
<div className="absolute top-[44px] right-0 w-[220px] bg-[var(--dropdown-bg)] backdrop-blur-xl border border-stroke rounded-lg shadow-2xl z-[200] py-2 font-korean">
|
||||
{/* 거리·면적 계산 */}
|
||||
{/* <div className="px-3 py-1.5 flex items-center gap-2 text-[0.6875rem] font-bold text-fg-disabled">
|
||||
<span>📐</span> 거리·면적 계산
|
||||
@ -178,7 +180,7 @@ export function TopBar({ activeTab, onTabChange }: TopBarProps) {
|
||||
? 'text-fg-disabled opacity-40 cursor-not-allowed'
|
||||
: measureMode === 'distance'
|
||||
? 'text-color-accent bg-[rgba(6,182,212,0.1)]'
|
||||
: 'text-fg-sub hover:bg-[rgba(255,255,255,0.06)] hover:text-fg'
|
||||
: 'text-fg hover:bg-[var(--hover-overlay)]'
|
||||
}`}
|
||||
>
|
||||
<span className="text-[0.8125rem]">↗</span> 거리 재기
|
||||
@ -192,7 +194,7 @@ export function TopBar({ activeTab, onTabChange }: TopBarProps) {
|
||||
? 'text-fg-disabled opacity-40 cursor-not-allowed'
|
||||
: measureMode === 'area'
|
||||
? 'text-color-accent bg-[rgba(6,182,212,0.1)]'
|
||||
: 'text-fg-sub hover:bg-[rgba(255,255,255,0.06)] hover:text-fg'
|
||||
: 'text-fg hover:bg-[var(--hover-overlay)]'
|
||||
}`}
|
||||
>
|
||||
<span className="text-[0.8125rem]">⭕</span> 면적 재기
|
||||
@ -205,10 +207,10 @@ export function TopBar({ activeTab, onTabChange }: TopBarProps) {
|
||||
<div className="px-3 py-1.5 flex items-center gap-2 text-[0.6875rem] font-bold text-fg-disabled">
|
||||
<span>🖨</span> 출력
|
||||
</div>
|
||||
<button className="w-full px-3 py-2 flex items-center gap-2.5 text-[0.75rem] text-fg-sub hover:bg-[rgba(255,255,255,0.06)] hover:text-fg transition-all">
|
||||
<button className="w-full px-3 py-2 flex items-center gap-2.5 text-[0.75rem] text-fg hover:bg-[var(--hover-overlay)] transition-all">
|
||||
<span className="text-[0.8125rem]">📸</span> 화면 캡쳐 다운로드
|
||||
</button>
|
||||
<button onClick={() => window.print()} className="w-full px-3 py-2 flex items-center gap-2.5 text-[0.75rem] text-fg-sub hover:bg-[rgba(255,255,255,0.06)] hover:text-fg transition-all">
|
||||
<button onClick={() => window.print()} className="w-full px-3 py-2 flex items-center gap-2.5 text-[0.75rem] text-fg hover:bg-[var(--hover-overlay)] transition-all">
|
||||
<span className="text-[0.8125rem]">🖨</span> 인쇄
|
||||
</button>
|
||||
|
||||
@ -219,7 +221,7 @@ export function TopBar({ activeTab, onTabChange }: TopBarProps) {
|
||||
<span>🗺</span> 지도 유형
|
||||
</div>
|
||||
{mapTypes.map(item => (
|
||||
<button key={item.mapKey} onClick={() => toggleMap(item.mapKey)} className="w-full px-3 py-2 flex items-center justify-between text-[0.75rem] text-fg-sub hover:bg-[rgba(255,255,255,0.06)] transition-all">
|
||||
<button key={item.mapKey} onClick={() => toggleMap(item.mapKey)} className="w-full px-3 py-2 flex items-center justify-between text-[0.75rem] text-fg-sub hover:bg-[var(--hover-overlay)] transition-all">
|
||||
<span className="flex items-center gap-2.5">
|
||||
<span className="text-[0.8125rem]">🗺</span> {item.mapNm}
|
||||
</span>
|
||||
@ -231,13 +233,33 @@ export function TopBar({ activeTab, onTabChange }: TopBarProps) {
|
||||
|
||||
<div className="my-1.5 border-t border-stroke" />
|
||||
|
||||
{/* 테마 전환 */}
|
||||
<button
|
||||
onClick={() => { toggleTheme(); setShowQuickMenu(false); }}
|
||||
className="w-full px-3 py-2 flex items-center justify-between text-[0.75rem] text-fg hover:bg-[var(--hover-overlay)] transition-all"
|
||||
>
|
||||
<span className="flex items-center gap-2.5">
|
||||
<span className="text-[0.8125rem]">{theme === 'dark' ? '\u2600\uFE0F' : '\uD83C\uDF19'}</span>
|
||||
{theme === 'dark' ? '라이트 모드' : '다크 모드'}
|
||||
</span>
|
||||
<div className={`w-[34px] h-[18px] rounded-full transition-all relative ${
|
||||
theme === 'light' ? 'bg-color-accent' : 'bg-bg-card border border-stroke'
|
||||
}`}>
|
||||
<div className={`absolute top-[2px] w-[14px] h-[14px] rounded-full bg-white shadow transition-all ${
|
||||
theme === 'light' ? 'left-[16px]' : 'left-[2px]'
|
||||
}`} />
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div className="my-1.5 border-t border-stroke" />
|
||||
|
||||
{/* 매뉴얼 */}
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowManual(true)
|
||||
setShowQuickMenu(false)
|
||||
}}
|
||||
className="w-full px-3 py-2 flex items-center gap-2.5 text-[0.75rem] text-fg-sub hover:bg-[rgba(255,255,255,0.06)] hover:text-fg transition-all"
|
||||
className="w-full px-3 py-2 flex items-center gap-2.5 text-[0.75rem] text-fg hover:bg-[var(--hover-overlay)] transition-all"
|
||||
>
|
||||
<span className="text-[0.8125rem]">📖</span> 사용자 매뉴얼
|
||||
</button>
|
||||
|
||||
26
frontend/src/common/store/themeStore.ts
Normal file
26
frontend/src/common/store/themeStore.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
type ThemeMode = 'dark' | 'light';
|
||||
|
||||
interface ThemeState {
|
||||
theme: ThemeMode;
|
||||
toggleTheme: () => void;
|
||||
setTheme: (mode: ThemeMode) => void;
|
||||
}
|
||||
|
||||
export const useThemeStore = create<ThemeState>((set, get) => ({
|
||||
theme: (localStorage.getItem('wing-theme') as ThemeMode) || 'dark',
|
||||
|
||||
toggleTheme: () => {
|
||||
const next = get().theme === 'dark' ? 'light' : 'dark';
|
||||
localStorage.setItem('wing-theme', next);
|
||||
document.documentElement.setAttribute('data-theme', next);
|
||||
set({ theme: next });
|
||||
},
|
||||
|
||||
setTheme: (mode) => {
|
||||
localStorage.setItem('wing-theme', mode);
|
||||
document.documentElement.setAttribute('data-theme', mode);
|
||||
set({ theme: mode });
|
||||
},
|
||||
}));
|
||||
@ -138,6 +138,25 @@
|
||||
--purple-800: #6b21a8;
|
||||
--purple-900: #581c87;
|
||||
--purple-1000: #3b0764;
|
||||
/* hover overlay */
|
||||
--hover-overlay: rgba(255, 255, 255, 0.06);
|
||||
--dropdown-bg: rgba(18, 25, 41, 0.97);
|
||||
}
|
||||
|
||||
/* ── Light theme overrides ── */
|
||||
[data-theme="light"] {
|
||||
--bg-base: #f8fafc;
|
||||
--bg-surface: #ffffff;
|
||||
--bg-elevated: #f1f5f9;
|
||||
--bg-card: #ffffff;
|
||||
--bg-surface-hover: #e2e8f0;
|
||||
--stroke-default: #cbd5e1;
|
||||
--stroke-light: #e2e8f0;
|
||||
--fg-default: #0f172a;
|
||||
--fg-sub: #475569;
|
||||
--fg-disabled: #94a3b8;
|
||||
--hover-overlay: rgba(0, 0, 0, 0.04);
|
||||
--dropdown-bg: rgba(255, 255, 255, 0.97);
|
||||
}
|
||||
|
||||
* {
|
||||
@ -169,4 +188,14 @@
|
||||
input[type="date"]::-webkit-calendar-picker-indicator:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Light theme: calendar icon reset */
|
||||
[data-theme="light"] input[type="date"]::-webkit-calendar-picker-indicator {
|
||||
filter: none;
|
||||
}
|
||||
|
||||
/* Light theme: invert white logos */
|
||||
[data-theme="light"] .wing-logo {
|
||||
filter: brightness(0) saturate(100%);
|
||||
}
|
||||
}
|
||||
|
||||
@ -830,10 +830,10 @@
|
||||
.boom-setting-input {
|
||||
width: 56px;
|
||||
padding: 3px 6px;
|
||||
background: var(--bg-elevated);
|
||||
background: var(--bg-base);
|
||||
border: 1px solid var(--stroke-default);
|
||||
border-radius: 4px;
|
||||
color: var(--color-warning);
|
||||
color: var(--color-accent);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
@ -1333,4 +1333,91 @@
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* ═══ Light Theme Overrides ═══ */
|
||||
|
||||
/* CCTV popup */
|
||||
[data-theme="light"] .cctv-dark-popup .maplibregl-popup-content {
|
||||
background: #ffffff;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid var(--stroke-default);
|
||||
}
|
||||
[data-theme="light"] .cctv-dark-popup .maplibregl-popup-tip {
|
||||
border-top-color: #ffffff;
|
||||
border-bottom-color: #ffffff;
|
||||
}
|
||||
[data-theme="light"] .cctv-dark-popup .maplibregl-popup-close-button {
|
||||
color: var(--fg-disabled);
|
||||
}
|
||||
[data-theme="light"] .cctv-dark-popup .maplibregl-popup-close-button:hover {
|
||||
color: var(--fg-default);
|
||||
}
|
||||
|
||||
/* Date/Time picker color-scheme */
|
||||
[data-theme="light"] .prd-date-input,
|
||||
[data-theme="light"] .prd-time-input {
|
||||
color-scheme: light;
|
||||
}
|
||||
[data-theme="light"] select.prd-i.prd-time-select {
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
/* Select option */
|
||||
[data-theme="light"] select.prd-i option {
|
||||
background: #ffffff;
|
||||
}
|
||||
[data-theme="light"] select.prd-i option:checked {
|
||||
background: linear-gradient(0deg, rgba(6, 182, 212, 0.15) 0%, rgba(6, 182, 212, 0.08) 100%);
|
||||
}
|
||||
|
||||
/* Select hover border */
|
||||
[data-theme="light"] select.prd-i:hover {
|
||||
border-color: var(--stroke-light);
|
||||
}
|
||||
|
||||
/* Model chip text */
|
||||
[data-theme="light"] .prd-mc {
|
||||
color: var(--fg-disabled);
|
||||
}
|
||||
[data-theme="light"] .prd-mc.on {
|
||||
color: var(--fg-default);
|
||||
}
|
||||
|
||||
/* Coordinate display */
|
||||
[data-theme="light"] .cod {
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
color: var(--fg-sub);
|
||||
}
|
||||
[data-theme="light"] .cov {
|
||||
color: var(--fg-default);
|
||||
}
|
||||
|
||||
/* Weather info panel */
|
||||
[data-theme="light"] .wip {
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
}
|
||||
[data-theme="light"] .wii-value {
|
||||
color: var(--fg-default);
|
||||
}
|
||||
[data-theme="light"] .wii-label {
|
||||
color: var(--fg-sub);
|
||||
}
|
||||
|
||||
/* Timeline control panel */
|
||||
[data-theme="light"] .tlb {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
}
|
||||
|
||||
/* Timeline boom tooltip */
|
||||
[data-theme="light"] .tlbm .tlbt {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
}
|
||||
|
||||
/* Combo item border */
|
||||
[data-theme="light"] .combo-item {
|
||||
border-bottom: 1px solid var(--stroke-light);
|
||||
}
|
||||
[data-theme="light"] .combo-list {
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
@ -226,7 +226,7 @@ export const CCTVPlayer = forwardRef<CCTVPlayerHandle, CCTVPlayerProps>(({
|
||||
// 오프라인
|
||||
if (playerState === 'offline') {
|
||||
return (
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center bg-[#0a0e18]">
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center bg-bg-base">
|
||||
<div className="text-2xl opacity-30 mb-2">📹</div>
|
||||
<div className="text-[11px] font-korean text-fg-disabled opacity-70">
|
||||
{sttsCd === 'MAINT' ? '점검중' : '오프라인'}
|
||||
@ -239,7 +239,7 @@ export const CCTVPlayer = forwardRef<CCTVPlayerHandle, CCTVPlayerProps>(({
|
||||
// URL 미설정
|
||||
if (playerState === 'no-url') {
|
||||
return (
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center bg-[#0a0e18]">
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center bg-bg-base">
|
||||
<div className="text-2xl opacity-20 mb-2">📹</div>
|
||||
<div className="text-[10px] font-korean text-fg-disabled opacity-50">스트림 URL 미설정</div>
|
||||
<div className="text-[9px] font-korean text-fg-disabled opacity-30 mt-1">{cameraNm}</div>
|
||||
@ -250,7 +250,7 @@ export const CCTVPlayer = forwardRef<CCTVPlayerHandle, CCTVPlayerProps>(({
|
||||
// 에러
|
||||
if (playerState === 'error') {
|
||||
return (
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center bg-[#0a0e18]">
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center bg-bg-base">
|
||||
<div className="text-2xl opacity-30 mb-2">⚠️</div>
|
||||
<div className="text-[10px] font-korean text-color-danger opacity-70">연결 실패</div>
|
||||
<div className="text-[9px] font-korean text-fg-disabled opacity-40 mt-1">{cameraNm}</div>
|
||||
@ -268,7 +268,7 @@ export const CCTVPlayer = forwardRef<CCTVPlayerHandle, CCTVPlayerProps>(({
|
||||
<div ref={containerRef} className="absolute inset-0">
|
||||
{/* 로딩 오버레이 */}
|
||||
{playerState === 'loading' && (
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center bg-[#0a0e18] z-10">
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center bg-bg-base z-10">
|
||||
<div className="text-lg opacity-40 animate-pulse mb-2">📹</div>
|
||||
<div className="text-[10px] font-korean text-fg-disabled opacity-50">연결 중...</div>
|
||||
</div>
|
||||
@ -338,7 +338,7 @@ export const CCTVPlayer = forwardRef<CCTVPlayerHandle, CCTVPlayerProps>(({
|
||||
</span>
|
||||
{sttsCd === 'LIVE' && (
|
||||
<span
|
||||
className="text-[8px] font-bold px-1 py-0.5 rounded text-[#f87171]"
|
||||
className="text-[8px] font-bold px-1 py-0.5 rounded text-color-danger"
|
||||
style={{ background: 'rgba(239,68,68,.3)' }}
|
||||
>
|
||||
● REC
|
||||
|
||||
@ -265,7 +265,7 @@ export function CctvView() {
|
||||
</div>
|
||||
|
||||
{/* 가운데: 영상 뷰어 */}
|
||||
<div className="flex-1 flex flex-col overflow-hidden min-w-0 bg-[#04070f]">
|
||||
<div className="flex-1 flex flex-col overflow-hidden min-w-0 bg-bg-base">
|
||||
{/* 뷰어 툴바 */}
|
||||
<div className="flex items-center justify-between px-4 py-2 border-b border-stroke bg-bg-elevated shrink-0 gap-2.5">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
@ -484,9 +484,9 @@ export function CctvView() {
|
||||
offset={14}
|
||||
className="cctv-dark-popup"
|
||||
>
|
||||
<div className="p-2" style={{ minWidth: 150, background: '#1a1f2e', borderRadius: 6 }}>
|
||||
<div className="text-[11px] font-bold text-white mb-1">{mapPopup.cameraNm}</div>
|
||||
<div className="text-[9px] text-gray-400 mb-1.5">{mapPopup.locDc ?? ''}</div>
|
||||
<div className="p-2" style={{ minWidth: 150, background: 'var(--bg-card)', borderRadius: 6 }}>
|
||||
<div className="text-[11px] font-bold text-fg mb-1">{mapPopup.cameraNm}</div>
|
||||
<div className="text-[9px] text-fg-disabled mb-1.5">{mapPopup.locDc ?? ''}</div>
|
||||
<div className="flex items-center gap-1.5 mb-2">
|
||||
<span className="text-[8px] font-bold px-1.5 py-px rounded-full"
|
||||
style={mapPopup.sttsCd === 'LIVE'
|
||||
@ -494,7 +494,7 @@ export function CctvView() {
|
||||
: { background: 'rgba(148,163,184,.15)', color: '#94a3b8' }
|
||||
}
|
||||
>{mapPopup.sttsCd === 'LIVE' ? '● LIVE' : '● OFF'}</span>
|
||||
<span className="text-[8px] text-gray-500">{mapPopup.sourceNm}</span>
|
||||
<span className="text-[8px] text-fg-disabled">{mapPopup.sourceNm}</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => { handleSelectCamera(mapPopup); setMapPopup(null) }}
|
||||
@ -520,7 +520,7 @@ export function CctvView() {
|
||||
{Array.from({ length: totalCells }).map((_, i) => {
|
||||
const cam = activeCells[i]
|
||||
return (
|
||||
<div key={i} className="relative flex items-center justify-center overflow-hidden bg-[#0a0e18]" style={{ border: '1px solid rgba(255,255,255,.06)' }}>
|
||||
<div key={i} className="relative flex items-center justify-center overflow-hidden bg-bg-base" style={{ border: '1px solid var(--stroke-light)' }}>
|
||||
{cam ? (
|
||||
<CCTVPlayer
|
||||
ref={el => { playerRefs.current[i] = el }}
|
||||
|
||||
@ -212,7 +212,7 @@ export function RealtimeDrone() {
|
||||
</div>
|
||||
|
||||
{/* 중앙: 영상 뷰어 */}
|
||||
<div className="flex-1 flex flex-col overflow-hidden min-w-0 bg-[#04070f]">
|
||||
<div className="flex-1 flex flex-col overflow-hidden min-w-0 bg-bg-base">
|
||||
{/* 툴바 */}
|
||||
<div className="flex items-center justify-between px-4 py-2 border-b border-stroke bg-bg-elevated shrink-0 gap-2.5">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
@ -338,13 +338,13 @@ export function RealtimeDrone() {
|
||||
offset={36}
|
||||
className="cctv-dark-popup"
|
||||
>
|
||||
<div className="p-2.5" style={{ minWidth: 170, background: '#1a1f2e', borderRadius: 6 }}>
|
||||
<div className="p-2.5" style={{ minWidth: 170, background: 'var(--bg-card)', borderRadius: 6 }}>
|
||||
<div className="flex items-center gap-1.5 mb-1">
|
||||
<span className="text-sm">🚁</span>
|
||||
<div className="text-[11px] font-bold text-white">{mapPopup.shipName}</div>
|
||||
<div className="text-[11px] font-bold text-fg">{mapPopup.shipName}</div>
|
||||
</div>
|
||||
<div className="text-[9px] text-gray-400 mb-0.5">{mapPopup.droneModel}</div>
|
||||
<div className="text-[8px] text-gray-500 font-mono mb-2">{mapPopup.ip} · {mapPopup.region}</div>
|
||||
<div className="text-[9px] text-fg-disabled mb-0.5">{mapPopup.droneModel}</div>
|
||||
<div className="text-[8px] text-fg-disabled font-mono mb-2">{mapPopup.ip} · {mapPopup.region}</div>
|
||||
<div className="flex items-center gap-1.5 mb-2">
|
||||
<span className="text-[8px] font-bold px-1.5 py-px rounded-full"
|
||||
style={{ background: statusInfo(mapPopup.status).bg, color: statusInfo(mapPopup.status).color }}
|
||||
@ -384,7 +384,7 @@ export function RealtimeDrone() {
|
||||
{Array.from({ length: totalCells }).map((_, i) => {
|
||||
const stream = activeCells[i]
|
||||
return (
|
||||
<div key={i} className="relative flex items-center justify-center overflow-hidden bg-[#0a0e18]" style={{ border: '1px solid rgba(255,255,255,.06)' }}>
|
||||
<div key={i} className="relative flex items-center justify-center overflow-hidden bg-bg-base" style={{ border: '1px solid var(--stroke-light)' }}>
|
||||
{stream && stream.status === 'streaming' && stream.hlsUrl ? (
|
||||
<CCTVPlayer
|
||||
ref={el => { playerRefs.current[i] = el }}
|
||||
|
||||
@ -189,14 +189,14 @@ 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 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>
|
||||
<div className="text-[11px] font-bold font-korean mb-2.5 flex items-center gap-1.5 text-color-tertiary">
|
||||
<div className="w-[18px] h-[18px] rounded-[5px] flex items-center justify-center text-[9px] font-bold text-color-tertiary" style={{ background: 'rgba(99,102,241,.12)' }}>{num}</div>
|
||||
{label}
|
||||
</div>
|
||||
)
|
||||
|
||||
const bsInput = "w-full px-3 py-2 rounded-md text-[11px] font-korean outline-none box-border"
|
||||
const bsInputStyle = { border: '1px solid #21262d', background: '#161b22', color: '#e2e8f0' }
|
||||
const bsInputStyle = { border: '1px solid var(--stroke-default)', background: 'var(--bg-surface)', color: 'var(--fg-default)' }
|
||||
|
||||
return (
|
||||
<div className="overflow-y-auto px-6 pt-1 pb-2" style={{ height: mainTab === 'map' ? '100%' : undefined }}>
|
||||
@ -607,7 +607,7 @@ 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 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>
|
||||
<span className="text-[11px] font-extrabold font-mono text-color-tertiary tracking-[-0.5px]">B<span className="text-color-tertiary">Sky</span></span>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm font-bold text-fg font-korean">BlackSky</div>
|
||||
@ -633,7 +633,7 @@ export function SatelliteRequest() {
|
||||
))}
|
||||
</div>
|
||||
<div className="text-[9px] text-fg-disabled font-korean leading-relaxed">고빈도 소형위성 군집 기반 긴급 촬영. 해양 사고 현장 신속 모니터링에 최적화. Dawn-to-Dusk 촬영 가능.</div>
|
||||
<div className="mt-2 text-[8px] text-fg-disabled font-mono">API: <span className="text-[#818cf8]">eapi.maxar.com/e1so/rapidoc</span></div>
|
||||
<div className="mt-2 text-[8px] text-fg-disabled font-mono">API: <span className="text-color-tertiary">eapi.maxar.com/e1so/rapidoc</span></div>
|
||||
</div>
|
||||
|
||||
{/* UP42 (EO + SAR) */}
|
||||
@ -644,7 +644,7 @@ 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 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>
|
||||
<span className="text-[13px] font-extrabold font-mono text-color-info tracking-[-0.5px]">up<sup className="text-[7px] align-super">42</sup></span>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm font-bold text-fg font-korean">UP42 — EO + SAR</div>
|
||||
@ -671,7 +671,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 text-[#60a5fa]" style={{ background: 'rgba(59,130,246,.08)', border: '1px solid rgba(59,130,246,.15)' }}>{t}</span>
|
||||
<span key={i} className="px-1.5 py-px rounded text-[8px] font-korean text-color-info" 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(--color-accent)' }}>{t}</span>
|
||||
@ -679,7 +679,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(--fg-disabled)' }}>+11 more</span>
|
||||
</div>
|
||||
<div className="text-[9px] text-fg-disabled font-korean leading-relaxed">광학(EO) + 합성개구레이더(SAR) 통합 마켓. 야간·악천후 시 SAR 활용. 다중 위성 소스 자동 최적 선택.</div>
|
||||
<div className="mt-2 text-[8px] text-fg-disabled font-mono">API: <span className="text-[#60a5fa]">up42.com</span></div>
|
||||
<div className="mt-2 text-[8px] text-fg-disabled font-mono">API: <span className="text-color-info">up42.com</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -693,33 +693,33 @@ export function SatelliteRequest() {
|
||||
|
||||
{/* ── BlackSky 긴급 촬영 요청 ── */}
|
||||
{modalPhase === 'blacksky' && (
|
||||
<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="border rounded-[14px] w-[860px] max-h-[90vh] flex flex-col overflow-hidden border-[rgba(99,102,241,.3)]" style={{ background: 'var(--bg-base)', boxShadow: '0 24px 80px rgba(0,0,0,.7)' }}>
|
||||
{/* 헤더 */}
|
||||
<div className="px-6 py-4 border-b border-[#21262d] flex items-center justify-between shrink-0 relative">
|
||||
<div className="px-6 py-4 border-b border-stroke 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 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>
|
||||
<span className="text-[10px] font-extrabold font-mono text-color-tertiary">B<span className="text-color-tertiary">Sky</span></span>
|
||||
</div>
|
||||
<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 className="text-[15px] font-bold font-korean text-fg">BlackSky — 긴급 위성 촬영 요청</div>
|
||||
<div className="text-[9px] font-korean mt-0.5 text-fg-disabled">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 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>
|
||||
<span className="px-3 py-1 rounded-md text-[9px] font-semibold font-korean text-color-tertiary" 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-fg-disabled">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 본문 */}
|
||||
<div className="flex-1 overflow-y-auto px-6 py-5 flex flex-col gap-4" style={{ scrollbarWidth: 'thin', scrollbarColor: '#21262d transparent' }}>
|
||||
<div className="flex-1 overflow-y-auto px-6 py-5 flex flex-col gap-4" style={{ scrollbarWidth: 'thin', scrollbarColor: 'var(--stroke-default) transparent' }}>
|
||||
{/* 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 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>
|
||||
<span className="text-[9px] font-mono text-fg-disabled">eapi.maxar.com/e1so/rapidoc · Latency: 142ms</span>
|
||||
<span className="ml-auto text-[8px] font-mono text-fg-disabled">Quota: 47/50 요청 잔여</span>
|
||||
</div>
|
||||
|
||||
{/* ① 태스킹 유형 */}
|
||||
@ -727,7 +727,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 text-[#64748b]">촬영 유형 <span className="text-red-400">*</span></label>
|
||||
<label className="block text-[9px] font-korean mb-1 text-fg-disabled">촬영 유형 <span className="text-red-400">*</span></label>
|
||||
<select className={bsInput} style={bsInputStyle}>
|
||||
<option>긴급 태스킹 (Emergency)</option>
|
||||
<option>표준 태스킹 (Standard)</option>
|
||||
@ -735,7 +735,7 @@ export function SatelliteRequest() {
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-[9px] font-korean mb-1 text-[#64748b]">우선순위 <span className="text-red-400">*</span></label>
|
||||
<label className="block text-[9px] font-korean mb-1 text-fg-disabled">우선순위 <span className="text-red-400">*</span></label>
|
||||
<select className={bsInput} style={bsInputStyle}>
|
||||
<option>P1 — 긴급 (90분 내)</option>
|
||||
<option>P2 — 높음 (6시간 내)</option>
|
||||
@ -743,7 +743,7 @@ export function SatelliteRequest() {
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-[9px] font-korean mb-1 text-[#64748b]">촬영 모드</label>
|
||||
<label className="block text-[9px] font-korean mb-1 text-fg-disabled">촬영 모드</label>
|
||||
<select className={bsInput} style={bsInputStyle}>
|
||||
<option>Single Collect</option>
|
||||
<option>Multi-pass Monitoring</option>
|
||||
@ -758,26 +758,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 text-[#64748b]">중심 위도 <span className="text-red-400">*</span></label>
|
||||
<label className="block text-[9px] font-korean mb-1 text-fg-disabled">중심 위도 <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 text-[#64748b]">중심 경도 <span className="text-red-400">*</span></label>
|
||||
<label className="block text-[9px] font-korean mb-1 text-fg-disabled">중심 경도 <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 text-[#818cf8]" style={{ border: '1px solid rgba(99,102,241,.3)', background: 'rgba(99,102,241,.08)' }}>📍 지도에서 AOI 그리기</button>
|
||||
<button className="px-3.5 py-2 rounded-md text-[10px] font-semibold cursor-pointer font-korean whitespace-nowrap text-color-tertiary" 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 text-[#64748b]">AOI 반경 (km)</label>
|
||||
<label className="block text-[9px] font-korean mb-1 text-fg-disabled">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 text-[#64748b]">최대 구름량 (%)</label>
|
||||
<label className="block text-[9px] font-korean mb-1 text-fg-disabled">최대 구름량 (%)</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 text-[#64748b]">최대 Off-nadir (°)</label>
|
||||
<label className="block text-[9px] font-korean mb-1 text-fg-disabled">최대 Off-nadir (°)</label>
|
||||
<input type="number" defaultValue={25} step={5} min={0} max={45} className={bsInput} style={bsInputStyle} />
|
||||
</div>
|
||||
</div>
|
||||
@ -788,15 +788,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 text-[#64748b]">촬영 시작 <span className="text-red-400">*</span></label>
|
||||
<label className="block text-[9px] font-korean mb-1 text-fg-disabled">촬영 시작 <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 text-[#64748b]">촬영 종료 <span className="text-red-400">*</span></label>
|
||||
<label className="block text-[9px] font-korean mb-1 text-fg-disabled">촬영 종료 <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 text-[#64748b]">반복 촬영</label>
|
||||
<label className="block text-[9px] font-korean mb-1 text-fg-disabled">반복 촬영</label>
|
||||
<select className={bsInput} style={bsInputStyle}>
|
||||
<option>1회 (단일)</option>
|
||||
<option>매 패스 (가용 시)</option>
|
||||
@ -813,7 +813,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 text-[#64748b]">산출물 형식 <span className="text-red-400">*</span></label>
|
||||
<label className="block text-[9px] font-korean mb-1 text-fg-disabled">산출물 형식 <span className="text-red-400">*</span></label>
|
||||
<select className={bsInput} style={bsInputStyle}>
|
||||
<option>Ortho-Rectified (정사보정)</option>
|
||||
<option>Pan-sharpened (팬샤프닝)</option>
|
||||
@ -821,7 +821,7 @@ export function SatelliteRequest() {
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-[9px] font-korean mb-1 text-[#64748b]">파일 포맷</label>
|
||||
<label className="block text-[9px] font-korean mb-1 text-fg-disabled">파일 포맷</label>
|
||||
<select className={bsInput} style={bsInputStyle}>
|
||||
<option>GeoTIFF</option>
|
||||
<option>NITF</option>
|
||||
@ -836,7 +836,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 text-[#94a3b8]">
|
||||
<label key={i} className="flex items-center gap-1 text-[9px] cursor-pointer font-korean text-fg-disabled">
|
||||
<input type="checkbox" defaultChecked={opt.checked} style={{ accentColor: '#818cf8', transform: 'scale(.85)' }} /> {opt.label}
|
||||
</label>
|
||||
))}
|
||||
@ -848,7 +848,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 text-[#64748b]">연계 사고번호</label>
|
||||
<label className="block text-[9px] font-korean mb-1 text-fg-disabled">연계 사고번호</label>
|
||||
<select className={bsInput} style={bsInputStyle}>
|
||||
<option>OIL-2024-0892 · M/V STELLAR DAISY</option>
|
||||
<option>HNS-2024-041 · 울산 온산항 톨루엔 유출</option>
|
||||
@ -857,23 +857,23 @@ export function SatelliteRequest() {
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-[9px] font-korean mb-1 text-[#64748b]">요청자</label>
|
||||
<label className="block text-[9px] font-korean mb-1 text-fg-disabled">요청자</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 border border-[#21262d] text-[#e2e8f0] bg-[#161b22]"
|
||||
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-stroke text-fg bg-bg-elevated"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 하단 버튼 */}
|
||||
<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]">
|
||||
<div className="px-6 py-3.5 border-t border-stroke flex items-center gap-2 shrink-0">
|
||||
<div className="flex-1 text-[9px] font-korean leading-relaxed text-fg-disabled">
|
||||
<span className="text-red-400">*</span> 필수 항목 · 긴급 태스킹은 P1 우선순위로 90분 내 최초 영상 수신
|
||||
</div>
|
||||
<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('provider')} className="px-5 py-2.5 rounded-lg border border-stroke text-xs font-semibold cursor-pointer font-korean text-fg-sub bg-bg-elevated">← 뒤로</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>
|
||||
@ -881,31 +881,31 @@ export function SatelliteRequest() {
|
||||
|
||||
{/* ── UP42 카탈로그 주문 ── */}
|
||||
{modalPhase === 'up42' && (
|
||||
<div className="border rounded-[14px] w-[920px] flex flex-col overflow-hidden border-[rgba(59,130,246,.3)]" style={{ background: '#0d1117', boxShadow: '0 24px 80px rgba(0,0,0,.7)', height: '85vh' }}>
|
||||
<div className="border rounded-[14px] w-[920px] flex flex-col overflow-hidden border-[rgba(59,130,246,.3)]" style={{ background: 'var(--bg-base)', boxShadow: '0 24px 80px rgba(0,0,0,.7)', height: '85vh' }}>
|
||||
{/* 헤더 */}
|
||||
<div className="px-6 py-4 border-b border-[#21262d] flex items-center justify-between shrink-0 relative">
|
||||
<div className="px-6 py-4 border-b border-stroke 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 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>
|
||||
<span className="text-[13px] font-extrabold font-mono text-color-info tracking-[-0.5px]">up<sup className="text-[7px] align-super">42</sup></span>
|
||||
</div>
|
||||
<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 className="text-[15px] font-bold font-korean text-fg">위성 촬영 요청 — 새 태스킹 주문</div>
|
||||
<div className="text-[9px] font-korean mt-0.5 text-fg-disabled">관심 지역(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 text-[#64748b]">✕</button>
|
||||
<button onClick={() => setModalPhase('none')} className="text-lg cursor-pointer p-1 bg-transparent border-none text-fg-disabled">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 본문 (좌: 사이드바, 우: 지도+AOI) */}
|
||||
<div className="flex-1 flex overflow-hidden">
|
||||
{/* 왼쪽: 위성 카탈로그 */}
|
||||
<div className="flex flex-col overflow-hidden border-r border-[#21262d]" style={{ width: 320, minWidth: 320, background: '#0d1117' }}>
|
||||
<div className="flex flex-col overflow-hidden border-r border-stroke" style={{ width: 320, minWidth: 320, background: 'var(--bg-base)' }}>
|
||||
{/* Optical / SAR / Elevation 탭 */}
|
||||
<div className="flex border-b border-[#21262d] shrink-0">
|
||||
<div className="flex border-b border-stroke shrink-0">
|
||||
{(['optical', 'sar', 'elevation'] as const).map(t => (
|
||||
<button
|
||||
key={t}
|
||||
@ -920,34 +920,34 @@ export function SatelliteRequest() {
|
||||
</div>
|
||||
|
||||
{/* 필터 바 */}
|
||||
<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 className="flex items-center gap-1.5 px-3 py-2 border-b border-stroke shrink-0">
|
||||
<span className="px-2 py-0.5 rounded text-[9px] font-semibold text-color-info" 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-color-tertiary" 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-fg-disabled">↕ 해상도 우선</span>
|
||||
</div>
|
||||
|
||||
{/* 컬렉션 수 */}
|
||||
<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 className="px-3 py-1.5 border-b border-stroke text-[9px] font-korean shrink-0 text-fg-disabled">
|
||||
이 지역에서 <b className="text-fg">{up42Filtered.length}</b>개 컬렉션 사용 가능
|
||||
</div>
|
||||
|
||||
{/* 위성 목록 */}
|
||||
<div className="flex-1 overflow-y-auto" style={{ scrollbarWidth: 'thin', scrollbarColor: '#21262d transparent' }}>
|
||||
<div className="flex-1 overflow-y-auto" style={{ scrollbarWidth: 'thin', scrollbarColor: 'var(--stroke-default) transparent' }}>
|
||||
{up42Filtered.map(sat => (
|
||||
<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 border-[#161b22] cursor-pointer transition-colors"
|
||||
className="flex items-center gap-2.5 px-3 py-2.5 border-b border-stroke cursor-pointer transition-colors"
|
||||
style={{
|
||||
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 text-[#e2e8f0]">{sat.name}</div>
|
||||
<div className="text-[11px] font-semibold truncate font-korean text-fg">{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 text-[#64748b]">☁ ≤{sat.cloud}%</span>}
|
||||
{sat.cloud > 0 && <span className="text-[8px] font-mono text-fg-disabled">☁ ≤{sat.cloud}%</span>}
|
||||
{'delay' in sat && sat.delay && <span className="text-[8px] font-bold" style={{ color: '#eab308' }}>⚠ 지연</span>}
|
||||
</div>
|
||||
</div>
|
||||
@ -1008,33 +1008,33 @@ export function SatelliteRequest() {
|
||||
</Map>
|
||||
|
||||
{/* 범례 오버레이 */}
|
||||
<div className="absolute top-3 left-3 px-3 py-2 rounded-lg z-10 border border-[#21262d]" style={{ background: 'rgba(13,17,23,.9)', backdropFilter: 'blur(8px)' }}>
|
||||
<div className="text-[9px] font-bold text-[#64748b] mb-1.5">🛰 위성 궤도</div>
|
||||
<div className="absolute top-3 left-3 px-3 py-2 rounded-lg z-10 border border-stroke" style={{ background: 'rgba(13,17,23,.9)', backdropFilter: 'blur(8px)' }}>
|
||||
<div className="text-[9px] font-bold text-fg-disabled mb-1.5">🛰 위성 궤도</div>
|
||||
{satPasses.slice(0, 4).map(p => (
|
||||
<div key={p.id} className="flex items-center gap-1.5 mb-1">
|
||||
<div className="w-3 h-[2px] rounded-sm" style={{ background: p.color }} />
|
||||
<span className="text-[8px] text-[#94a3b8]">{p.satellite}</span>
|
||||
<span className="text-[8px] text-fg-disabled">{p.satellite}</span>
|
||||
</div>
|
||||
))}
|
||||
<div className="flex items-center gap-1.5 mt-1.5 pt-1.5 border-t border-[#21262d]">
|
||||
<div className="flex items-center gap-1.5 mt-1.5 pt-1.5 border-t border-stroke">
|
||||
<div className="w-3 h-3 rounded border border-[#3b82f6]" style={{ background: 'rgba(59,130,246,.1)' }} />
|
||||
<span className="text-[8px] text-[#64748b]">한국 영역 AOI</span>
|
||||
<span className="text-[8px] text-fg-disabled">한국 영역 AOI</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 로딩 */}
|
||||
{satPassesLoading && (
|
||||
<div className="absolute inset-0 flex items-center justify-center z-10" style={{ background: 'rgba(0,0,0,.5)' }}>
|
||||
<div className="text-[11px] text-[#60a5fa] font-korean animate-pulse">🛰 위성 패스 조회 중...</div>
|
||||
<div className="text-[11px] text-color-info font-korean animate-pulse">🛰 위성 패스 조회 중...</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 위성 패스 타임라인 */}
|
||||
<div className="border-t border-[#21262d] px-4 py-3 shrink-0 max-h-[200px] overflow-y-auto" style={{ background: 'rgba(13,17,23,.95)', scrollbarWidth: 'thin', scrollbarColor: '#21262d transparent' }}>
|
||||
<div className="text-[10px] font-bold font-korean mb-2 text-[#e2e8f0]">
|
||||
<div className="border-t border-stroke px-4 py-3 shrink-0 max-h-[200px] overflow-y-auto" style={{ background: 'rgba(13,17,23,.95)', scrollbarWidth: 'thin', scrollbarColor: 'var(--stroke-default) transparent' }}>
|
||||
<div className="text-[10px] font-bold font-korean mb-2 text-fg">
|
||||
🛰 한국 주변 실시간 위성 패스 ({satPasses.length}개)
|
||||
<span className="text-[8px] text-[#64748b] font-normal ml-2">클릭하여 궤도 확인</span>
|
||||
<span className="text-[8px] text-fg-disabled font-normal ml-2">클릭하여 궤도 확인</span>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
{satPasses.map(pass => {
|
||||
@ -1048,20 +1048,20 @@ export function SatelliteRequest() {
|
||||
onClick={() => setUp42SelPass(up42SelPass === pass.id ? null : pass.id)}
|
||||
className="flex items-center gap-3 px-3 py-2 rounded-md cursor-pointer transition-colors"
|
||||
style={{
|
||||
background: up42SelPass === pass.id ? 'rgba(59,130,246,.1)' : '#161b22',
|
||||
border: up42SelPass === pass.id ? `1px solid ${pass.color}40` : '1px solid #21262d',
|
||||
background: up42SelPass === pass.id ? 'rgba(59,130,246,.1)' : 'var(--bg-elevated)',
|
||||
border: up42SelPass === pass.id ? `1px solid ${pass.color}40` : '1px solid var(--stroke-light)',
|
||||
}}
|
||||
>
|
||||
<div className="w-1.5 h-6 rounded-full shrink-0" style={{ background: pass.color }} />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[10px] font-bold font-korean text-[#e2e8f0]">{pass.satellite}</span>
|
||||
<span className="text-[8px] text-[#64748b]">{pass.provider}</span>
|
||||
<span className="text-[10px] font-bold font-korean text-fg">{pass.satellite}</span>
|
||||
<span className="text-[8px] text-fg-disabled">{pass.provider}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-0.5">
|
||||
<span className="text-[9px] font-bold font-mono text-[#60a5fa]">{timeStr}</span>
|
||||
<span className="text-[9px] font-bold font-mono text-color-info">{timeStr}</span>
|
||||
<span className="text-[9px] font-mono" style={{ color: pass.color }}>{pass.resolution}</span>
|
||||
<span className="text-[8px] font-mono text-[#64748b]">EL {pass.maxElevation}° · {pass.direction === 'ascending' ? '↗ 상승' : '↘ 하강'}</span>
|
||||
<span className="text-[8px] font-mono text-fg-disabled">EL {pass.maxElevation}° · {pass.direction === 'ascending' ? '↗ 상승' : '↘ 하강'}</span>
|
||||
</div>
|
||||
</div>
|
||||
<span className="px-1.5 py-px rounded text-[8px] font-bold shrink-0" style={{
|
||||
@ -1078,18 +1078,18 @@ export function SatelliteRequest() {
|
||||
</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="px-6 py-3 border-t border-stroke flex items-center justify-between shrink-0">
|
||||
<div className="text-[9px] font-korean text-fg-disabled">원하는 위성을 찾지 못했나요? <span className="text-color-info cursor-pointer">태스킹 주문 생성</span> 또는 <span className="text-color-info cursor-pointer">자세히 보기 ↗</span></div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[11px] font-korean mr-1.5 text-[#8690a6]">
|
||||
<span className="text-[11px] font-korean mr-1.5 text-fg-disabled">
|
||||
선택: {up42SelPass ? satPasses.find(p => p.id === up42SelPass)?.satellite : up42SelSat ? up42Satellites.find(s => s.id === up42SelSat)?.name : '없음'}
|
||||
</span>
|
||||
<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('provider')} className="px-4 py-2 rounded-lg border border-stroke text-[11px] font-semibold cursor-pointer font-korean text-fg-disabled bg-bg-elevated">← 뒤로</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"
|
||||
style={{
|
||||
background: up42SelSat ? 'linear-gradient(135deg,#3b82f6,#06b6d4)' : '#21262d',
|
||||
background: up42SelSat ? 'linear-gradient(135deg,#3b82f6,#06b6d4)' : 'var(--stroke-light)',
|
||||
opacity: up42SelSat ? 1 : 0.5,
|
||||
color: up42SelSat ? '#fff' : '#64748b',
|
||||
boxShadow: up42SelSat ? '0 4px 16px rgba(59,130,246,.35)' : 'none',
|
||||
|
||||
@ -51,9 +51,9 @@ function getZoneIndex(distanceNm: number): number {
|
||||
}
|
||||
|
||||
function StatusBadge({ status }: { status: Status }) {
|
||||
if (status === 'forbidden') return <span className="text-[8px] font-bold px-1.5 py-0.5 rounded" style={{ background: 'rgba(239,68,68,0.15)', color: '#ef4444' }}>배출불가</span>
|
||||
if (status === 'allowed') return <span className="text-[8px] font-bold px-1.5 py-0.5 rounded" style={{ background: 'rgba(34,197,94,0.15)', color: '#22c55e' }}>배출가능</span>
|
||||
return <span className="text-[8px] font-bold px-1.5 py-0.5 rounded" style={{ background: 'rgba(234,179,8,0.15)', color: '#eab308' }}>조건부</span>
|
||||
if (status === 'forbidden') return <span className="text-[8px] font-bold px-1.5 py-0.5 rounded" style={{ background: 'rgba(239,68,68,0.15)', color: 'var(--color-danger)' }}>배출불가</span>
|
||||
if (status === 'allowed') return <span className="text-[8px] font-bold px-1.5 py-0.5 rounded" style={{ background: 'rgba(34,197,94,0.15)', color: 'var(--color-success)' }}>배출가능</span>
|
||||
return <span className="text-[8px] font-bold px-1.5 py-0.5 rounded" style={{ background: 'rgba(234,179,8,0.15)', color: 'var(--color-caution)' }}>조건부</span>
|
||||
}
|
||||
|
||||
interface DischargeZonePanelProps {
|
||||
@ -75,8 +75,8 @@ export function DischargeZonePanel({ lat, lon, distanceNm, onClose }: DischargeZ
|
||||
style={{
|
||||
width: 320,
|
||||
maxHeight: 'calc(100% - 32px)',
|
||||
background: 'rgba(13,17,23,0.95)',
|
||||
border: '1px solid #30363d',
|
||||
background: 'var(--bg-base)',
|
||||
border: '1px solid var(--stroke-default)',
|
||||
boxShadow: '0 16px 48px rgba(0,0,0,0.5)',
|
||||
backdropFilter: 'blur(12px)',
|
||||
}}
|
||||
@ -86,25 +86,25 @@ export function DischargeZonePanel({ lat, lon, distanceNm, onClose }: DischargeZ
|
||||
className="shrink-0 flex items-center justify-between"
|
||||
style={{
|
||||
padding: '10px 14px',
|
||||
borderBottom: '1px solid #30363d',
|
||||
background: 'linear-gradient(135deg, #1c2333, #161b22)',
|
||||
borderBottom: '1px solid var(--stroke-default)',
|
||||
background: 'var(--bg-elevated)',
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<div className="text-[11px] font-bold text-[#f0f6fc] font-korean">🚢 오염물 배출 규정</div>
|
||||
<div className="text-[8px] text-[#8b949e] font-korean">해양환경관리법 제22조</div>
|
||||
<div className="text-[11px] font-bold text-fg font-korean">🚢 오염물 배출 규정</div>
|
||||
<div className="text-[8px] text-fg-sub font-korean">해양환경관리법 제22조</div>
|
||||
</div>
|
||||
<span onClick={onClose} className="text-[14px] cursor-pointer text-[#8b949e] hover:text-[#f0f6fc]">✕</span>
|
||||
<span onClick={onClose} className="text-[14px] cursor-pointer text-fg-sub hover:text-fg">✕</span>
|
||||
</div>
|
||||
|
||||
{/* Location Info */}
|
||||
<div className="shrink-0" style={{ padding: '8px 14px', borderBottom: '1px solid #21262d' }}>
|
||||
<div className="shrink-0" style={{ padding: '8px 14px', borderBottom: '1px solid var(--stroke-light)' }}>
|
||||
<div className="flex items-center justify-between mb-1.5">
|
||||
<span className="text-[9px] text-[#8b949e] font-korean">선택 위치</span>
|
||||
<span className="text-[9px] text-[#c9d1d9] font-mono">{lat.toFixed(4)}°N, {lon.toFixed(4)}°E</span>
|
||||
<span className="text-[9px] text-fg-sub font-korean">선택 위치</span>
|
||||
<span className="text-[9px] text-fg font-mono">{lat.toFixed(4)}°N, {lon.toFixed(4)}°E</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-[9px] text-[#8b949e] font-korean">영해기선 거리 (추정)</span>
|
||||
<span className="text-[9px] text-fg-sub font-korean">영해기선 거리 (추정)</span>
|
||||
<span className="text-[11px] font-bold font-mono" style={{ color: ZONE_COLORS[zoneIdx] }}>
|
||||
{distanceNm.toFixed(1)} NM
|
||||
</span>
|
||||
@ -119,9 +119,9 @@ export function DischargeZonePanel({ lat, lon, distanceNm, onClose }: DischargeZ
|
||||
padding: '3px 0',
|
||||
fontSize: 8,
|
||||
fontWeight: i === zoneIdx ? 700 : 400,
|
||||
color: i === zoneIdx ? '#fff' : '#8b949e',
|
||||
background: i === zoneIdx ? ZONE_COLORS[i] : 'rgba(255,255,255,0.04)',
|
||||
border: i === zoneIdx ? 'none' : '1px solid #21262d',
|
||||
color: i === zoneIdx ? 'var(--fg-default)' : 'var(--fg-sub)',
|
||||
background: i === zoneIdx ? ZONE_COLORS[i] : 'var(--hover-overlay)',
|
||||
border: i === zoneIdx ? 'none' : '1px solid var(--stroke-light)',
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
@ -131,7 +131,7 @@ export function DischargeZonePanel({ lat, lon, distanceNm, onClose }: DischargeZ
|
||||
</div>
|
||||
|
||||
{/* Rules */}
|
||||
<div className="flex-1 overflow-y-auto" style={{ scrollbarWidth: 'thin', scrollbarColor: '#30363d transparent' }}>
|
||||
<div className="flex-1 overflow-y-auto" style={{ scrollbarWidth: 'thin', scrollbarColor: 'var(--stroke-default) transparent' }}>
|
||||
{categories.map(cat => {
|
||||
const catRules = RULES.filter(r => r.category === cat)
|
||||
const isExpanded = expandedCat === cat
|
||||
@ -140,7 +140,7 @@ export function DischargeZonePanel({ lat, lon, distanceNm, onClose }: DischargeZ
|
||||
const summaryColor = allForbidden ? '#ef4444' : allAllowed ? '#22c55e' : '#eab308'
|
||||
|
||||
return (
|
||||
<div key={cat} style={{ borderBottom: '1px solid #21262d' }}>
|
||||
<div key={cat} style={{ borderBottom: '1px solid var(--stroke-light)' }}>
|
||||
<div
|
||||
className="flex items-center justify-between cursor-pointer"
|
||||
onClick={() => setExpandedCat(isExpanded ? null : cat)}
|
||||
@ -148,13 +148,13 @@ export function DischargeZonePanel({ lat, lon, distanceNm, onClose }: DischargeZ
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div style={{ width: 6, height: 6, borderRadius: '50%', background: summaryColor }} />
|
||||
<span className="text-[10px] font-bold text-[#c9d1d9] font-korean">{cat}</span>
|
||||
<span className="text-[10px] font-bold text-fg font-korean">{cat}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[8px] font-semibold" style={{ color: summaryColor }}>
|
||||
{allForbidden ? '전체 불가' : allAllowed ? '전체 가능' : '항목별 상이'}
|
||||
</span>
|
||||
<span className="text-[9px] text-[#8b949e]">{isExpanded ? '▾' : '▸'}</span>
|
||||
<span className="text-[9px] text-fg-sub">{isExpanded ? '▾' : '▸'}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -167,18 +167,18 @@ export function DischargeZonePanel({ lat, lon, distanceNm, onClose }: DischargeZ
|
||||
style={{
|
||||
padding: '5px 8px',
|
||||
marginBottom: 2,
|
||||
background: 'rgba(255,255,255,0.02)',
|
||||
background: 'var(--hover-overlay)',
|
||||
borderRadius: 4,
|
||||
}}
|
||||
>
|
||||
<span className="text-[9px] text-[#c9d1d9] font-korean">{rule.item}</span>
|
||||
<span className="text-[9px] text-fg font-korean">{rule.item}</span>
|
||||
<StatusBadge status={rule.zones[zoneIdx]} />
|
||||
</div>
|
||||
))}
|
||||
{catRules.some(r => r.condition && r.zones[zoneIdx] !== 'forbidden') && (
|
||||
<div className="mt-1" style={{ padding: '4px 8px' }}>
|
||||
{catRules.filter(r => r.condition && r.zones[zoneIdx] !== 'forbidden').map((r, i) => (
|
||||
<div key={i} className="text-[7px] text-[#8b949e] font-korean leading-relaxed">
|
||||
<div key={i} className="text-[7px] text-fg-sub font-korean leading-relaxed">
|
||||
💡 {r.item}: {r.condition}
|
||||
</div>
|
||||
))}
|
||||
@ -192,9 +192,9 @@ export function DischargeZonePanel({ lat, lon, distanceNm, onClose }: DischargeZ
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="shrink-0" style={{ padding: '6px 14px', borderTop: '1px solid #21262d' }}>
|
||||
<div className="text-[7px] text-[#8b949e] font-korean leading-relaxed">
|
||||
※ 거리는 최근접 해안선 기준 추정치입니다. 실제 영해기선과 차이가 있을 수 있습니다.
|
||||
<div className="shrink-0" style={{ padding: '6px 14px', borderTop: '1px solid var(--stroke-light)' }}>
|
||||
<div className="text-[7px] text-fg-sub font-korean leading-relaxed">
|
||||
※ 거리는 최근접 해안선 기준 추정치입니다. 실제 영해기선과 차이가 있습니다.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -466,7 +466,7 @@ const WeatherPopup = forwardRef<HTMLDivElement, {
|
||||
<span className="text-xs">⬆</span>
|
||||
<div>
|
||||
<div className="text-fg-disabled text-[7px]">고조 (만조)</div>
|
||||
<div className="font-bold font-mono text-[10px] text-[#60a5fa]">{data?.highTide || '-'}</div>
|
||||
<div className="font-bold font-mono text-[10px] text-color-info">{data?.highTide || '-'}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 flex items-center gap-1.5 px-2 py-1.5 rounded-md"
|
||||
|
||||
@ -335,7 +335,7 @@ export function IncidentsRightPanel({
|
||||
<div className="bg-bg-elevated border border-stroke 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 text-[#22c55e]">민감자원</span>
|
||||
<span className="text-xs font-bold text-color-success">민감자원</span>
|
||||
</div>
|
||||
<div className="flex flex-col gap-[3px]">
|
||||
{sensCategories.length === 0 ? (
|
||||
@ -372,11 +372,11 @@ export function IncidentsRightPanel({
|
||||
<div className="bg-bg-elevated border border-stroke 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 text-[#f59e0b]">
|
||||
<span className="text-xs font-bold text-color-boom">
|
||||
근처 방제자원
|
||||
</span>
|
||||
{nearbyOrgs.length > 0 && (
|
||||
<span className="ml-auto text-[9px] font-mono text-[#f59e0b]">{nearbyOrgs.length}개</span>
|
||||
<span className="ml-auto text-[9px] font-mono text-color-boom">{nearbyOrgs.length}개</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -406,7 +406,7 @@ export function IncidentsRightPanel({
|
||||
{org.areaNm}{org.totalAssets > 0 ? ` · 장비 ${org.totalAssets}개` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-[9px] font-mono text-[#f59e0b] shrink-0">
|
||||
<span className="text-[9px] font-mono text-color-boom shrink-0">
|
||||
{org.distanceNm.toFixed(1)} nm
|
||||
</span>
|
||||
</div>
|
||||
@ -418,7 +418,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-fg-disabled">탐색 반경</span>
|
||||
<span className="text-[10px] font-bold font-mono text-[#f59e0b]">
|
||||
<span className="text-[10px] font-bold font-mono text-color-boom">
|
||||
{nearbyRadius} nm
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@ -600,7 +600,7 @@ export function IncidentsView() {
|
||||
padding: '3px 8px',
|
||||
background: 'rgba(239,68,68,0.06)',
|
||||
border: '1px solid rgba(239,68,68,0.2)',
|
||||
color: '#f87171',
|
||||
color: 'var(--color-danger)',
|
||||
}}
|
||||
>
|
||||
✕ 닫기
|
||||
@ -648,10 +648,10 @@ export function IncidentsView() {
|
||||
closeOnClick={false}
|
||||
>
|
||||
<div className="text-center min-w-[180px] text-xs">
|
||||
<div className="font-semibold text-[#1a1a2e]" style={{ marginBottom: 6 }}>
|
||||
<div className="font-semibold text-fg" style={{ marginBottom: 6 }}>
|
||||
{incidentPopup.incident.name}
|
||||
</div>
|
||||
<div className="text-[11px] text-[#555] leading-[1.6]">
|
||||
<div className="text-[11px] text-fg-disabled leading-[1.6]">
|
||||
<div>상태: {getStatusLabel(incidentPopup.incident.status)}</div>
|
||||
<div>
|
||||
일시: {incidentPopup.incident.date} {incidentPopup.incident.time}
|
||||
@ -661,7 +661,7 @@ export function IncidentsView() {
|
||||
<div>원인: {incidentPopup.incident.causeType}</div>
|
||||
)}
|
||||
{incidentPopup.incident.prediction && (
|
||||
<div className="text-[#0891b2]">{incidentPopup.incident.prediction}</div>
|
||||
<div className="text-color-accent">{incidentPopup.incident.prediction}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@ -676,8 +676,8 @@ export function IncidentsView() {
|
||||
style={{
|
||||
left: hoverInfo.x + 12,
|
||||
top: hoverInfo.y - 12,
|
||||
background: '#161b22',
|
||||
border: '1px solid #30363d',
|
||||
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,
|
||||
@ -752,8 +752,8 @@ export function IncidentsView() {
|
||||
right: dischargeMode ? 340 : 180,
|
||||
padding: '6px 10px',
|
||||
background: dischargeMode ? 'rgba(6,182,212,0.15)' : 'rgba(13,17,23,0.88)',
|
||||
border: dischargeMode ? '1px solid rgba(6,182,212,0.4)' : '1px solid #30363d',
|
||||
color: dischargeMode ? '#22d3ee' : '#8b949e',
|
||||
border: dischargeMode ? '1px solid rgba(6,182,212,0.4)' : '1px solid var(--stroke-default)',
|
||||
color: dischargeMode ? '#22d3ee' : 'var(--fg-disabled)',
|
||||
backdropFilter: 'blur(8px)',
|
||||
transition: 'all 0.2s',
|
||||
}}
|
||||
@ -796,7 +796,7 @@ export function IncidentsView() {
|
||||
className="absolute top-[10px] right-[10px] z-[500] rounded-md"
|
||||
style={{
|
||||
background: 'rgba(13,17,23,0.88)',
|
||||
border: '1px solid #30363d',
|
||||
border: '1px solid var(--stroke-default)',
|
||||
padding: '8px 12px',
|
||||
backdropFilter: 'blur(8px)',
|
||||
}}
|
||||
@ -807,7 +807,7 @@ export function IncidentsView() {
|
||||
width: 6,
|
||||
height: 6,
|
||||
borderRadius: '50%',
|
||||
background: '#22c55e',
|
||||
background: 'var(--color-success)',
|
||||
animation: 'pd 1.5s infinite',
|
||||
}}
|
||||
/>
|
||||
@ -834,7 +834,7 @@ export function IncidentsView() {
|
||||
className="absolute bottom-[10px] left-[10px] z-[500] rounded-md flex flex-col gap-1.5"
|
||||
style={{
|
||||
background: 'rgba(13,17,23,0.88)',
|
||||
border: '1px solid #30363d',
|
||||
border: '1px solid var(--stroke-default)',
|
||||
padding: '8px 12px',
|
||||
backdropFilter: 'blur(8px)',
|
||||
}}
|
||||
@ -1013,7 +1013,7 @@ export function IncidentsView() {
|
||||
padding: '4px 12px',
|
||||
background: 'rgba(59,130,246,0.1)',
|
||||
border: '1px solid rgba(59,130,246,0.2)',
|
||||
color: '#58a6ff',
|
||||
color: 'var(--color-info)',
|
||||
}}
|
||||
>
|
||||
📋 보고서 생성
|
||||
@ -1213,8 +1213,8 @@ function VesselPopupPanel({
|
||||
transform: 'translate(-50%,-50%)',
|
||||
zIndex: 9995,
|
||||
width: 300,
|
||||
background: '#161b22',
|
||||
border: '1px solid #30363d',
|
||||
background: 'var(--bg-elevated)',
|
||||
border: '1px solid var(--stroke-default)',
|
||||
borderRadius: 12,
|
||||
boxShadow: '0 16px 48px rgba(0,0,0,0.6)',
|
||||
overflow: 'hidden',
|
||||
@ -1227,8 +1227,8 @@ function VesselPopupPanel({
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
padding: '10px 14px',
|
||||
background: 'linear-gradient(135deg,#1c2333,#161b22)',
|
||||
borderBottom: '1px solid #30363d',
|
||||
background: 'var(--bg-elevated)',
|
||||
borderBottom: '1px solid var(--stroke-default)',
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-center text-[16px]" style={{ width: 28, height: 20 }}>
|
||||
@ -1236,20 +1236,20 @@ function VesselPopupPanel({
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div
|
||||
className="text-[12px] font-[800] text-[#f0f6fc] whitespace-nowrap overflow-hidden text-ellipsis"
|
||||
className="text-[12px] font-[800] text-fg whitespace-nowrap overflow-hidden text-ellipsis"
|
||||
>
|
||||
{v.name}
|
||||
</div>
|
||||
<div className="text-[9px] text-[#8b949e] font-mono">MMSI: {v.mmsi}</div>
|
||||
<div className="text-[9px] text-fg-disabled font-mono">MMSI: {v.mmsi}</div>
|
||||
</div>
|
||||
<span onClick={onClose} className="text-[14px] cursor-pointer text-[#8b949e] p-[2px]">
|
||||
<span onClick={onClose} className="text-[14px] cursor-pointer text-fg-disabled p-[2px]">
|
||||
✕
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Ship Image */}
|
||||
<div
|
||||
className="w-full flex items-center justify-center text-[40px] text-[#30363d]"
|
||||
className="w-full flex items-center justify-center text-[40px] text-fg-disabled"
|
||||
style={{
|
||||
height: 120,
|
||||
background: '#0d1117',
|
||||
@ -1262,7 +1262,7 @@ function VesselPopupPanel({
|
||||
{/* Tags */}
|
||||
<div className="flex gap-2" style={{ padding: '6px 14px', borderBottom: '1px solid #21262d' }}>
|
||||
<span
|
||||
className="text-[8px] font-bold rounded text-[#58a6ff]"
|
||||
className="text-[8px] font-bold rounded text-color-info"
|
||||
style={{
|
||||
padding: '2px 8px',
|
||||
background: 'rgba(59,130,246,0.12)',
|
||||
@ -1296,14 +1296,14 @@ function VesselPopupPanel({
|
||||
}}
|
||||
>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-[10px] text-[#8b949e]">출항지</span>
|
||||
<span className="text-[10px] text-[#c9d1d9] font-semibold font-mono">
|
||||
<span className="text-[10px] text-fg-disabled">출항지</span>
|
||||
<span className="text-[10px] text-fg-sub font-semibold font-mono">
|
||||
{v.depart}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-[10px] text-[#8b949e]">입항지</span>
|
||||
<span className="text-[10px] text-[#c9d1d9] font-semibold font-mono">
|
||||
<span className="text-[10px] text-fg-disabled">입항지</span>
|
||||
<span className="text-[10px] text-fg-sub font-semibold font-mono">
|
||||
{v.arrive}
|
||||
</span>
|
||||
</div>
|
||||
@ -1315,7 +1315,7 @@ function VesselPopupPanel({
|
||||
<div className="flex gap-1.5" style={{ padding: '10px 14px' }}>
|
||||
<button
|
||||
onClick={onDetail}
|
||||
className="flex-1 text-[10px] font-bold cursor-pointer text-center rounded-sm text-[#58a6ff]"
|
||||
className="flex-1 text-[10px] font-bold cursor-pointer text-center rounded-sm text-color-info"
|
||||
style={{
|
||||
padding: 6,
|
||||
background: 'rgba(59,130,246,0.12)',
|
||||
@ -1325,7 +1325,7 @@ function VesselPopupPanel({
|
||||
📋 상세정보
|
||||
</button>
|
||||
<button
|
||||
className="flex-1 text-[10px] font-bold cursor-pointer text-center rounded-sm text-[#a78bfa]"
|
||||
className="flex-1 text-[10px] font-bold cursor-pointer text-center rounded-sm text-color-tertiary"
|
||||
style={{
|
||||
padding: 6,
|
||||
background: 'rgba(168,85,247,0.1)',
|
||||
@ -1335,7 +1335,7 @@ function VesselPopupPanel({
|
||||
🔍 항적조회
|
||||
</button>
|
||||
<button
|
||||
className="flex-1 text-[10px] font-bold cursor-pointer text-center rounded-sm text-[#22d3ee]"
|
||||
className="flex-1 text-[10px] font-bold cursor-pointer text-center rounded-sm text-color-accent"
|
||||
style={{
|
||||
padding: 6,
|
||||
background: 'rgba(6,182,212,0.1)',
|
||||
@ -1368,7 +1368,7 @@ function PopupRow({
|
||||
borderBottom: '1px solid rgba(48,54,61,0.4)',
|
||||
}}
|
||||
>
|
||||
<span className="text-[#8b949e]">{label}</span>
|
||||
<span className="text-fg-disabled">{label}</span>
|
||||
<span
|
||||
className="font-semibold font-mono"
|
||||
style={{
|
||||
@ -1431,13 +1431,13 @@ function VesselDetailModal({ vessel: v, onClose }: { vessel: Vessel; onClose: ()
|
||||
<div className="flex items-center gap-[10px]">
|
||||
<span className="text-lg">{v.flag}</span>
|
||||
<div>
|
||||
<div className="text-[14px] font-[800] text-[#f0f6fc]">{v.name}</div>
|
||||
<div className="text-[10px] text-[#8b949e] font-mono">
|
||||
<div className="text-[14px] font-[800] text-fg">{v.name}</div>
|
||||
<div className="text-[10px] text-fg-disabled font-mono">
|
||||
MMSI: {v.mmsi} · IMO: {v.imo}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span onClick={onClose} className="text-[16px] cursor-pointer text-[#8b949e]">
|
||||
<span onClick={onClose} className="text-[16px] cursor-pointer text-fg-disabled">
|
||||
✕
|
||||
</span>
|
||||
</div>
|
||||
@ -1509,7 +1509,7 @@ function Sec({
|
||||
return (
|
||||
<div style={{ border: `1px solid ${borderColor || '#21262d'}`, borderRadius: 8, overflow: 'hidden' }}>
|
||||
<div
|
||||
className="text-[11px] font-bold text-[#c9d1d9] flex items-center justify-between"
|
||||
className="text-[11px] font-bold text-fg-sub flex items-center justify-between"
|
||||
style={{
|
||||
padding: '8px 12px',
|
||||
background: bgColor || '#0d1117',
|
||||
@ -1548,7 +1548,7 @@ function Cell({
|
||||
gridColumn: span ? '1 / -1' : undefined,
|
||||
}}
|
||||
>
|
||||
<div className="text-[9px] text-[#8b949e]" style={{ marginBottom: 2 }}>{label}</div>
|
||||
<div className="text-[9px] text-fg-disabled" style={{ marginBottom: 2 }}>{label}</div>
|
||||
<div className="text-[11px] font-semibold font-mono" style={{ color: color || '#f0f6fc' }}>
|
||||
{value}
|
||||
</div>
|
||||
@ -1578,7 +1578,7 @@ function TabInfo({ v }: { v: Vessel }) {
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="w-full rounded-lg overflow-hidden flex items-center justify-center text-[60px] text-[#30363d]"
|
||||
className="w-full rounded-lg overflow-hidden flex items-center justify-center text-[60px] text-fg-disabled"
|
||||
style={{
|
||||
height: 160,
|
||||
background: '#0d1117',
|
||||
@ -1657,7 +1657,7 @@ function TabNav(_props: { v: Vessel }) {
|
||||
</Sec>
|
||||
|
||||
<Sec title="📊 속도 이력">
|
||||
<div className="p-3 bg-[#0d1117]">
|
||||
<div className="p-3 bg-bg-base">
|
||||
<div className="flex items-end gap-1.5" style={{ height: 80 }}>
|
||||
{hours.map((h, i) => (
|
||||
<div
|
||||
@ -1672,13 +1672,13 @@ function TabNav(_props: { v: Vessel }) {
|
||||
height: `${heights[i]}%`,
|
||||
}}
|
||||
/>
|
||||
<span className="text-[7px] text-[#8b949e]">{h}</span>
|
||||
<span className="text-[7px] text-fg-disabled">{h}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="text-center text-[8px] text-[#8b949e]" style={{ marginTop: 6 }}>
|
||||
평균: <b className="text-[#58a6ff]">8.4 kn</b> · 최대:{' '}
|
||||
<b className="text-[#22d3ee]">11.2 kn</b>
|
||||
<div className="text-center text-[8px] 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>
|
||||
@ -1718,7 +1718,7 @@ function TabSpec({ v }: { v: Vessel }) {
|
||||
</Sec>
|
||||
|
||||
<Sec title="⚠ 위험물 적재 정보">
|
||||
<div className="p-[10px_12px] bg-[#0d1117]">
|
||||
<div className="p-[10px_12px] bg-bg-base">
|
||||
<div
|
||||
className="flex items-center gap-2 rounded"
|
||||
style={{
|
||||
@ -1729,14 +1729,14 @@ function TabSpec({ v }: { v: Vessel }) {
|
||||
>
|
||||
<span className="text-[12px]">🛢</span>
|
||||
<div className="flex-1">
|
||||
<div className="text-[10px] font-semibold text-[#f0f6fc]">
|
||||
<div className="text-[10px] font-semibold text-fg">
|
||||
{v.cargo.split('·')[0].trim()}
|
||||
</div>
|
||||
<div className="text-[8px] text-[#8b949e]">{v.cargo}</div>
|
||||
<div className="text-[8px] text-fg-disabled">{v.cargo}</div>
|
||||
</div>
|
||||
{v.cargo.includes('IMO') && (
|
||||
<span
|
||||
className="text-[8px] font-bold text-[#f87171]"
|
||||
className="text-[8px] font-bold text-color-danger"
|
||||
style={{
|
||||
padding: '2px 6px',
|
||||
background: 'rgba(239,68,68,0.15)',
|
||||
@ -1811,7 +1811,7 @@ function TabInsurance(_props: { v: Vessel }) {
|
||||
</Sec>
|
||||
|
||||
<div
|
||||
className="rounded-sm text-[9px] text-[#8b949e]"
|
||||
className="rounded-sm text-[9px] text-fg-disabled"
|
||||
style={{
|
||||
padding: '8px 10px',
|
||||
background: 'rgba(59,130,246,0.04)',
|
||||
@ -1834,7 +1834,7 @@ function TabDangerous({ v }: { v: Vessel }) {
|
||||
bgColor="rgba(249,115,22,0.06)"
|
||||
badge={
|
||||
<span
|
||||
className="text-[8px] font-bold text-[#ef4444]"
|
||||
className="text-[8px] font-bold text-color-danger"
|
||||
style={{
|
||||
padding: '2px 6px',
|
||||
background: 'rgba(239,68,68,0.15)',
|
||||
@ -1861,16 +1861,16 @@ function TabDangerous({ v }: { v: Vessel }) {
|
||||
|
||||
<Sec title="📋 화물창 및 첨부">
|
||||
<div
|
||||
className="flex items-center justify-between gap-2 bg-[#0d1117]"
|
||||
className="flex items-center justify-between gap-2 bg-bg-base"
|
||||
style={{ padding: '12px' }}
|
||||
>
|
||||
<div
|
||||
className="flex items-center gap-2 text-[11px] whitespace-nowrap"
|
||||
>
|
||||
<span className="text-[#8b949e]">화물창 2개이상 여부</span>
|
||||
<span className="text-fg-disabled">화물창 2개이상 여부</span>
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<span
|
||||
className="flex items-center justify-center text-[8px] text-[#22d3ee]"
|
||||
className="flex items-center justify-center text-[8px] text-color-accent"
|
||||
style={{
|
||||
width: 14,
|
||||
height: 14,
|
||||
@ -1880,11 +1880,11 @@ function TabDangerous({ v }: { v: Vessel }) {
|
||||
>
|
||||
✓
|
||||
</span>
|
||||
<span className="font-semibold text-[10px] text-[#22d3ee]">예</span>
|
||||
<span className="font-semibold text-[10px] text-color-accent">예</span>
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
className="text-[10px] font-semibold text-[#58a6ff] cursor-pointer whitespace-nowrap shrink-0 rounded"
|
||||
className="text-[10px] font-semibold text-color-info cursor-pointer whitespace-nowrap shrink-0 rounded"
|
||||
style={{
|
||||
padding: '6px 14px',
|
||||
background: 'rgba(59,130,246,0.1)',
|
||||
@ -1909,7 +1909,7 @@ function TabDangerous({ v }: { v: Vessel }) {
|
||||
|
||||
<Sec title="🚨 비상 대응 요약 (EmS)" bgColor="rgba(234,179,8,0.06)">
|
||||
<div
|
||||
className="flex flex-col gap-1.5 bg-[#0d1117]"
|
||||
className="flex flex-col gap-1.5 bg-bg-base"
|
||||
style={{ padding: '10px 12px' }}
|
||||
>
|
||||
<EmsRow
|
||||
@ -1937,7 +1937,7 @@ function TabDangerous({ v }: { v: Vessel }) {
|
||||
</Sec>
|
||||
|
||||
<div
|
||||
className="rounded-sm text-[9px] text-[#8b949e]"
|
||||
className="rounded-sm text-[9px] text-fg-disabled"
|
||||
style={{
|
||||
padding: '8px 10px',
|
||||
background: 'rgba(249,115,22,0.04)',
|
||||
@ -1976,8 +1976,8 @@ function EmsRow({
|
||||
>
|
||||
<span className="text-[13px]">{icon}</span>
|
||||
<div>
|
||||
<div className="text-[9px] text-[#8b949e]">{label}</div>
|
||||
<div className="text-[10px] font-semibold text-[#f0f6fc]">{value}</div>
|
||||
<div className="text-[9px] text-fg-disabled">{label}</div>
|
||||
<div className="text-[10px] font-semibold text-fg">{value}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@ -2017,13 +2017,13 @@ function ActionBtn({
|
||||
function VesselTooltipContent({ vessel: v }: { vessel: Vessel }) {
|
||||
return (
|
||||
<>
|
||||
<div className="text-[11px] font-bold text-[#f0f6fc]" style={{ marginBottom: 3 }}>{v.name}</div>
|
||||
<div className="text-[9px] text-[#8b949e]" style={{ marginBottom: 4 }}>
|
||||
<div className="text-[11px] font-bold text-fg" style={{ marginBottom: 3 }}>{v.name}</div>
|
||||
<div className="text-[9px] text-fg-disabled" style={{ marginBottom: 4 }}>
|
||||
{v.typS} · {v.flag}
|
||||
</div>
|
||||
<div className="flex justify-between text-[9px]">
|
||||
<span className="text-[#22d3ee] font-semibold">{v.speed} kn</span>
|
||||
<span className="text-[#8b949e]">HDG {v.heading}°</span>
|
||||
<span className="text-color-accent font-semibold">{v.speed} kn</span>
|
||||
<span className="text-fg-disabled">HDG {v.heading}°</span>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
@ -2035,8 +2035,8 @@ function IncidentTooltipContent({ incident: i }: { incident: IncidentCompat }) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="text-[11px] font-bold text-[#f0f6fc]" style={{ marginBottom: 3 }}>{i.name}</div>
|
||||
<div className="text-[9px] text-[#8b949e]" style={{ marginBottom: 4 }}>
|
||||
<div className="text-[11px] font-bold text-fg" style={{ marginBottom: 3 }}>{i.name}</div>
|
||||
<div className="text-[9px] text-fg-disabled" style={{ marginBottom: 4 }}>
|
||||
{i.date} {i.time}
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
@ -2046,7 +2046,7 @@ function IncidentTooltipContent({ incident: i }: { incident: IncidentCompat }) {
|
||||
>
|
||||
{getStatusLabel(i.status)}
|
||||
</span>
|
||||
<span className="text-[9px] text-[#58a6ff] font-mono">
|
||||
<span className="text-[9px] text-color-info font-mono">
|
||||
{i.location.lat.toFixed(3)}°N, {i.location.lon.toFixed(3)}°E
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@ -58,12 +58,12 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
|
||||
style={{ background: 'rgba(0,0,0,0.7)', backdropFilter: 'blur(6px)' }}
|
||||
>
|
||||
<div
|
||||
className="text-center text-[12px] text-[#8b949e]"
|
||||
className="text-center text-[12px] text-fg-disabled"
|
||||
style={{
|
||||
width: 300,
|
||||
padding: 40,
|
||||
background: '#0d1117',
|
||||
border: '1px solid #30363d',
|
||||
background: 'var(--bg-base)',
|
||||
border: '1px solid var(--stroke-default)',
|
||||
borderRadius: 14,
|
||||
}}
|
||||
>
|
||||
@ -92,8 +92,8 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
|
||||
width: '95vw',
|
||||
height: '92vh',
|
||||
maxWidth: 1600,
|
||||
background: '#0d1117',
|
||||
border: '1px solid #30363d',
|
||||
background: 'var(--bg-base)',
|
||||
border: '1px solid var(--stroke-default)',
|
||||
borderRadius: 14,
|
||||
boxShadow: '0 24px 64px rgba(0,0,0,0.7)',
|
||||
}}
|
||||
@ -103,17 +103,17 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
|
||||
className="shrink-0 flex items-center justify-between"
|
||||
style={{
|
||||
padding: '12px 20px',
|
||||
background: 'linear-gradient(135deg,#161b22,#0d1117)',
|
||||
borderBottom: '1px solid #30363d',
|
||||
background: 'linear-gradient(135deg,var(--bg-elevated),var(--bg-base))',
|
||||
borderBottom: '1px solid var(--stroke-default)',
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-[10px]">
|
||||
<span className="text-lg">📋</span>
|
||||
<div>
|
||||
<div className="text-[14px] font-[800] text-[#f0f6fc]">
|
||||
<div className="text-[14px] font-[800] text-fg">
|
||||
현장정보 — {incident.name}
|
||||
</div>
|
||||
<div className="text-[10px] text-[#8b949e] font-mono">
|
||||
<div className="text-[10px] text-fg-disabled font-mono">
|
||||
{incident.name} · {incident.date} · 사진 {media.photoCnt} / 영상 {media.videoCnt} / 위성 {media.satCnt} / CCTV {media.cctvCnt}
|
||||
</div>
|
||||
</div>
|
||||
@ -126,7 +126,7 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
|
||||
padding: '5px 12px', borderRadius: 6, fontSize: 11, fontWeight: activeTab === t.id ? 700 : 400,
|
||||
cursor: 'pointer', border: 'none',
|
||||
background: activeTab === t.id ? 'rgba(168,85,247,0.15)' : 'transparent',
|
||||
color: activeTab === t.id ? '#c084fc' : '#8b949e',
|
||||
color: activeTab === t.id ? '#c084fc' : 'var(--fg-disabled)',
|
||||
}}>
|
||||
{t.icon ? `${t.icon} ${t.label}` : t.label}
|
||||
</button>
|
||||
@ -142,7 +142,7 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
|
||||
{/* Close */}
|
||||
<span
|
||||
onClick={onClose}
|
||||
className="text-[18px] cursor-pointer text-[#8b949e] rounded"
|
||||
className="text-[18px] cursor-pointer text-fg-disabled rounded"
|
||||
style={{ padding: '2px 6px' }}
|
||||
>✕</span>
|
||||
</div>
|
||||
@ -151,11 +151,11 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
|
||||
{/* ── Timeline ────────────────────────────────── */}
|
||||
<div
|
||||
className="shrink-0 flex items-center gap-[10px]"
|
||||
style={{ padding: '6px 20px', borderBottom: '1px solid #21262d' }}
|
||||
style={{ padding: '6px 20px', borderBottom: '1px solid var(--stroke-light)' }}
|
||||
>
|
||||
<span className="text-[9px] text-[#8b949e] whitespace-nowrap">TIMELINE</span>
|
||||
<span className="text-[9px] text-fg-disabled 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 }} />
|
||||
<div style={{ position: 'absolute', top: 7, left: 0, right: 0, height: 2, background: 'var(--stroke-light)', borderRadius: 1 }} />
|
||||
{timelineDots.map((d, i) => (
|
||||
<div key={i} style={{
|
||||
position: 'absolute', left: `${d.pct}%`, top: 3,
|
||||
@ -164,10 +164,10 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
|
||||
}} />
|
||||
))}
|
||||
</div>
|
||||
<div className="flex gap-2 text-[8px] font-mono text-[#8b949e] whitespace-nowrap">
|
||||
<div className="flex gap-2 text-[8px] font-mono text-fg-disabled whitespace-nowrap">
|
||||
<span style={{ color: '#ef4444' }}>● 초기</span>
|
||||
<span style={{ color: '#f59e0b' }}>● 대응</span>
|
||||
<span className="text-[#8b949e]">● 종료</span>
|
||||
<span className="text-fg-disabled">● 종료</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -178,20 +178,20 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
|
||||
gridTemplateColumns: (showPhoto || showSat) && (showVideo || showCctv) ? '1fr 1fr' : '1fr',
|
||||
gridTemplateRows: (showPhoto || showVideo) && (showSat || showCctv) ? '1fr 1fr' : '1fr',
|
||||
gap: 1,
|
||||
background: '#21262d',
|
||||
background: 'var(--stroke-light)',
|
||||
}}
|
||||
>
|
||||
{/* ── Q1: 현장사진 ──────────────────────────── */}
|
||||
{showPhoto && (
|
||||
<div className="flex flex-col overflow-hidden bg-[#0d1117]">
|
||||
<div className="flex flex-col overflow-hidden bg-bg-base">
|
||||
{/* Section header */}
|
||||
<div
|
||||
className="shrink-0 flex items-center justify-between"
|
||||
style={{ padding: '8px 16px', borderBottom: '1px solid #21262d' }}
|
||||
style={{ padding: '8px 16px', borderBottom: '1px solid var(--stroke-light)' }}
|
||||
>
|
||||
<div className="flex items-center gap-[6px]">
|
||||
<span className="text-[12px]">📷</span>
|
||||
<span className="text-[12px] font-bold text-[#f0f6fc]">
|
||||
<span className="text-[12px] font-bold text-fg">
|
||||
현장사진 — {str(media.photoMeta, 'title', '현장 사진')}
|
||||
</span>
|
||||
</div>
|
||||
@ -201,30 +201,30 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
|
||||
</div>
|
||||
{/* Photo content */}
|
||||
<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 className="text-[48px]" style={{ color: 'var(--stroke-default)' }}>📷</div>
|
||||
<div className="text-[12px] text-fg-sub font-semibold">
|
||||
{incident.name.replace('유류유출', '유출 현장').replace('오염', '현장')} 해상 사진
|
||||
</div>
|
||||
<div className="text-[9px] text-[#8b949e] font-mono">
|
||||
<div className="text-[9px] text-fg-disabled font-mono">
|
||||
{str(media.photoMeta, 'date')} · {str(media.photoMeta, 'by')}
|
||||
</div>
|
||||
</div>
|
||||
{/* Thumbnails */}
|
||||
<div className="shrink-0" style={{ padding: '8px 12px', borderTop: '1px solid #21262d' }}>
|
||||
<div className="shrink-0" style={{ padding: '8px 12px', borderTop: '1px solid var(--stroke-light)' }}>
|
||||
<div className="flex gap-1.5" style={{ marginBottom: 6 }}>
|
||||
{Array.from({ length: Math.min(num(media.photoMeta, 'thumbCount'), 7) }).map((_, i) => (
|
||||
<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',
|
||||
<div key={i} className="flex items-center justify-center text-[14px] cursor-pointer" style={{
|
||||
width: 40, height: 36, borderRadius: 4, color: 'var(--stroke-default)',
|
||||
background: i === 0 ? 'rgba(168,85,247,0.15)' : 'var(--bg-elevated)',
|
||||
border: i === 0 ? '2px solid rgba(168,85,247,0.5)' : '1px solid var(--stroke-default)',
|
||||
}}>📷</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-[8px] text-[#8b949e]">
|
||||
<span className="text-[8px] text-fg-disabled">
|
||||
📷 사진 {num(media.photoMeta, 'thumbCount')}장 · {str(media.photoMeta, 'stage')}
|
||||
</span>
|
||||
<span className="text-[8px] text-[#a78bfa] cursor-pointer">🔗 R&D 연계</span>
|
||||
<span className="text-[8px] text-color-tertiary cursor-pointer">🔗 R&D 연계</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -232,19 +232,19 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
|
||||
|
||||
{/* ── Q2: 드론 영상 ─────────────────────────── */}
|
||||
{showVideo && (
|
||||
<div className="flex flex-col overflow-hidden bg-[#0d1117]">
|
||||
<div className="flex flex-col overflow-hidden bg-bg-base">
|
||||
<div
|
||||
className="shrink-0 flex items-center justify-between"
|
||||
style={{ padding: '8px 16px', borderBottom: '1px solid #21262d' }}
|
||||
style={{ padding: '8px 16px', borderBottom: '1px solid var(--stroke-light)' }}
|
||||
>
|
||||
<div className="flex items-center gap-[6px]">
|
||||
<span className="text-[12px]">🎬</span>
|
||||
<span className="text-[12px] font-bold text-[#f0f6fc]">
|
||||
<span className="text-[12px] font-bold text-fg">
|
||||
드론 영상 — {str(media.droneMeta, 'title', '드론 영상')}
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
className="text-[9px] font-bold text-[#ef4444] rounded"
|
||||
className="text-[9px] font-bold text-color-danger rounded"
|
||||
style={{
|
||||
padding: '2px 8px',
|
||||
background: 'rgba(239,68,68,0.15)',
|
||||
@ -252,39 +252,39 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
|
||||
>● REC</span>
|
||||
</div>
|
||||
<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 className="text-[48px]" style={{ color: 'var(--stroke-default)' }}>🎬</div>
|
||||
<div className="text-[12px] text-fg-sub font-semibold">
|
||||
드론 항공 촬영 영상
|
||||
</div>
|
||||
<div className="text-[9px] text-[#8b949e] font-mono">
|
||||
<div className="text-[9px] text-fg-disabled font-mono">
|
||||
{str(media.droneMeta, 'device')} · {str(media.droneMeta, 'alt')} 고도
|
||||
</div>
|
||||
</div>
|
||||
{/* Video controls */}
|
||||
<div
|
||||
className="shrink-0 flex flex-col gap-2"
|
||||
style={{ padding: '10px 16px', borderTop: '1px solid #21262d' }}
|
||||
style={{ padding: '10px 16px', borderTop: '1px solid var(--stroke-light)' }}
|
||||
>
|
||||
<div className="flex items-center justify-center gap-3">
|
||||
<span className="text-[12px] text-[#8b949e] cursor-pointer">⏮</span>
|
||||
<span className="text-[12px] text-fg-disabled cursor-pointer">⏮</span>
|
||||
<div
|
||||
className="flex items-center justify-center text-[12px] text-[#c084fc] cursor-pointer"
|
||||
className="flex items-center justify-center text-[12px] text-color-tertiary 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>
|
||||
<span className="text-[12px] text-fg-disabled cursor-pointer">⏭</span>
|
||||
<span className="text-[10px] text-fg-disabled font-mono">02:34 / {str(media.droneMeta, 'duration')}</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-[8px] text-[#8b949e]">
|
||||
<span className="text-[8px] text-fg-disabled">
|
||||
🎬 영상 {num(media.droneMeta, 'videoCount')}건 · {str(media.droneMeta, 'stage')}
|
||||
</span>
|
||||
<div className="flex gap-[8px]">
|
||||
<span className="text-[8px] text-[#58a6ff] cursor-pointer">📂 전체보기</span>
|
||||
<span className="text-[8px] text-[#a78bfa] cursor-pointer">🔗 R&D 연계</span>
|
||||
<span className="text-[8px] text-color-info cursor-pointer">📂 전체보기</span>
|
||||
<span className="text-[8px] text-color-tertiary cursor-pointer">🔗 R&D 연계</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -293,14 +293,14 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
|
||||
|
||||
{/* ── Q3: 위성영상 ──────────────────────────── */}
|
||||
{showSat && (
|
||||
<div className="flex flex-col overflow-hidden bg-[#0d1117]">
|
||||
<div className="flex flex-col overflow-hidden bg-bg-base">
|
||||
<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 className="text-[12px]">🛰</span>
|
||||
<span className="text-[12px] font-bold text-[#f0f6fc]">
|
||||
<span className="text-[12px] font-bold text-fg">
|
||||
위성영상 — {str(media.satMeta, 'title', '위성영상')}
|
||||
</span>
|
||||
</div>
|
||||
@ -316,24 +316,24 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
|
||||
display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 6,
|
||||
}}>
|
||||
<div
|
||||
className="absolute text-[9px] font-bold text-[#ef4444] font-mono bg-[#0d1117]"
|
||||
className="absolute text-[9px] font-bold text-color-danger font-mono bg-bg-base"
|
||||
style={{ top: -10, left: 8, padding: '0 4px' }}
|
||||
>
|
||||
{str(media.satMeta, 'detection')}
|
||||
</div>
|
||||
<div className="text-[40px] text-[#30363d]">🛰</div>
|
||||
<div className="text-[11px] text-[#c9d1d9] font-semibold">
|
||||
<div className="text-[40px] text-fg-disabled">🛰</div>
|
||||
<div className="text-[11px] text-fg-sub font-semibold">
|
||||
{str(media.satMeta, 'title', '위성영상')} 위성영상
|
||||
</div>
|
||||
<div className="text-[8px] text-[#8b949e] font-mono">
|
||||
<div className="text-[8px] text-fg-disabled font-mono">
|
||||
{str(media.satMeta, 'date')} · 해상도 {str(media.satMeta, 'resolution')}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{str(media.satMeta, 'detection') === '—' && (
|
||||
<div className="text-center">
|
||||
<div className="text-[40px] text-[#30363d]">🛰</div>
|
||||
<div className="text-[11px] text-[#8b949e]" style={{ marginTop: 8 }}>위성영상 없음</div>
|
||||
<div className="text-[40px] text-fg-disabled">🛰</div>
|
||||
<div className="text-[11px] text-fg-disabled" style={{ marginTop: 8 }}>위성영상 없음</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@ -341,7 +341,7 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
|
||||
{num(media.satMeta, 'thumbCount') > 0 && (
|
||||
<div className="flex gap-1.5" style={{ marginBottom: 6 }}>
|
||||
{Array.from({ length: num(media.satMeta, 'thumbCount') }).map((_, i) => (
|
||||
<div key={i} className="flex items-center justify-center text-[14px] text-[#30363d] cursor-pointer" style={{
|
||||
<div key={i} className="flex items-center justify-center text-[14px] text-fg-disabled 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',
|
||||
@ -350,10 +350,10 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-[8px] text-[#8b949e]">
|
||||
<span className="text-[8px] text-fg-disabled">
|
||||
🛰 위성 {num(media.satMeta, 'thumbCount')}장 · {str(media.satMeta, 'sensor')}
|
||||
</span>
|
||||
<span className="text-[8px] text-[#58a6ff] cursor-pointer">🔍 편집/측 비교</span>
|
||||
<span className="text-[8px] text-color-info cursor-pointer">🔍 편집/측 비교</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -361,21 +361,21 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
|
||||
|
||||
{/* ── Q4: CCTV ──────────────────────────────── */}
|
||||
{showCctv && (
|
||||
<div className="flex flex-col overflow-hidden bg-[#0d1117]">
|
||||
<div className="flex flex-col overflow-hidden bg-bg-base">
|
||||
<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 className="text-[12px]">📹</span>
|
||||
<span className="text-[12px] font-bold text-[#f0f6fc]">
|
||||
<span className="text-[12px] font-bold text-fg">
|
||||
CCTV — {str(media.cctvMeta, 'title', 'CCTV')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-[6px]">
|
||||
{bool(media.cctvMeta, 'live') && (
|
||||
<span
|
||||
className="text-[9px] font-bold text-[#22c55e] rounded"
|
||||
className="text-[9px] font-bold text-color-success rounded"
|
||||
style={{
|
||||
padding: '2px 8px',
|
||||
background: 'rgba(34,197,94,0.15)',
|
||||
@ -387,15 +387,15 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
|
||||
</div>
|
||||
<div className="flex-1 flex items-center justify-center flex-col gap-2 relative">
|
||||
{bool(media.cctvMeta, 'live') && (
|
||||
<div className="absolute text-[9px] font-bold text-[#ef4444] font-mono" style={{ top: 10, left: 16 }}>
|
||||
<div className="absolute text-[9px] font-bold text-color-danger font-mono" style={{ top: 10, left: 16 }}>
|
||||
● LIVE {new Date().toLocaleTimeString('ko-KR', { hour12: false })}
|
||||
</div>
|
||||
)}
|
||||
<div className="text-[48px] text-[#30363d]">📹</div>
|
||||
<div className="text-[12px] text-[#c9d1d9] font-semibold">
|
||||
<div className="text-[48px] text-fg-disabled">📹</div>
|
||||
<div className="text-[12px] text-fg-sub font-semibold">
|
||||
{str(media.cctvMeta, 'title', 'CCTV').replace('#', 'CCTV #')}
|
||||
</div>
|
||||
<div className="text-[9px] text-[#8b949e] font-mono">
|
||||
<div className="text-[9px] text-fg-disabled font-mono">
|
||||
{str(media.cctvMeta, 'ptz')} · {str(media.cctvMeta, 'angle')} · {bool(media.cctvMeta, 'live') ? '실시간 스트리밍' : '녹화 영상'}
|
||||
</div>
|
||||
</div>
|
||||
@ -416,12 +416,12 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
|
||||
))}
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-[8px] text-[#8b949e]">
|
||||
<span className="text-[8px] text-fg-disabled">
|
||||
📹 CCTV {num(media.cctvMeta, 'camCount')}채널 · {str(media.cctvMeta, 'location')}
|
||||
</span>
|
||||
<div className="flex gap-[8px]">
|
||||
<span className="text-[8px] text-[#ef4444] cursor-pointer">🔴 녹화영상</span>
|
||||
<span className="text-[8px] text-[#58a6ff] cursor-pointer">🎥 PTZ</span>
|
||||
<span className="text-[8px] text-color-danger cursor-pointer">🔴 녹화영상</span>
|
||||
<span className="text-[8px] text-color-info cursor-pointer">🎥 PTZ</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -438,12 +438,12 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
|
||||
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 className="flex gap-4 text-[10px] font-mono text-fg-disabled">
|
||||
<span>📷 사진 <b className="text-fg">{media.photoCnt}</b></span>
|
||||
<span>🎬 영상 <b className="text-fg">{media.videoCnt}</b></span>
|
||||
<span>🛰 위성 <b className="text-fg">{media.satCnt}</b></span>
|
||||
<span>📹 CCTV <b className="text-fg">{media.cctvCnt}</b></span>
|
||||
<span>📎 총 <b className="text-color-tertiary">{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" />
|
||||
@ -459,7 +459,7 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
|
||||
function NavBtn({ label }: { label: string }) {
|
||||
return (
|
||||
<button
|
||||
className="flex items-center justify-center text-[10px] text-[#8b949e] cursor-pointer rounded bg-[#161b22]"
|
||||
className="flex items-center justify-center text-[10px] text-fg-disabled cursor-pointer rounded bg-bg-elevated"
|
||||
style={{
|
||||
width: 22, height: 22,
|
||||
border: '1px solid #30363d',
|
||||
|
||||
@ -243,7 +243,7 @@ export function LeftPanel({
|
||||
</div>
|
||||
) : (
|
||||
<div className="px-4 pb-4">
|
||||
<p className="text-[11px] text-fg-disabled font-korean text-center py-2">선택된 사고정보가 없습니다.</p>
|
||||
<p className="text-[12px] text-fg-disabled font-korean text-center py-2">선택된 사고정보가 없습니다.</p>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
@ -266,7 +266,7 @@ export function LeftPanel({
|
||||
{expandedSections.impactResources && (
|
||||
<div className="px-4 pb-4">
|
||||
{sensitiveResources.length === 0 ? (
|
||||
<p className="text-[13px] text-fg-disabled text-center font-korean">영향받는 민감자원 목록</p>
|
||||
<p className="text-[12px] text-fg-disabled text-center font-korean">영향받는 민감자원 목록</p>
|
||||
) : (
|
||||
<div className="space-y-1.5">
|
||||
{sensitiveResources.map(({ category, count, totalArea }) => {
|
||||
|
||||
@ -232,9 +232,9 @@ const OilBoomSection = ({
|
||||
<div key={setting.key} style={{
|
||||
background: 'var(--bg-base)',
|
||||
borderRadius: 'var(--radius-sm)',
|
||||
}} className="flex items-center justify-between p-[6px_8px] border border-stroke">
|
||||
<span className="text-[9px] text-fg-disabled">● {setting.label}</span>
|
||||
<div className="flex items-center gap-[2px]">
|
||||
}} className="flex items-center justify-between px-2.5 py-1.5 border border-stroke">
|
||||
<span className="flex-1 text-[9px] text-fg-disabled truncate">● {setting.label}</span>
|
||||
<div className="flex items-center gap-1 shrink-0 w-[80px] justify-end">
|
||||
<input
|
||||
type="number"
|
||||
value={setting.value}
|
||||
@ -245,7 +245,7 @@ const OilBoomSection = ({
|
||||
className="boom-setting-input"
|
||||
step={setting.key === 'waveHeightCorrectionFactor' ? 0.1 : 1}
|
||||
/>
|
||||
<span className="text-[9px] text-fg-disabled">{setting.unit}</span>
|
||||
<span className="text-[9px] text-fg-disabled w-[14px]">{setting.unit}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@ -423,18 +423,18 @@ function NewScenarioModal({ ops, onClose }: { ops: RescueOpsItem[]; onClose: ()
|
||||
<div className={sectionTitleCls}>{sectionIcon(1)} 기본 정보</div>
|
||||
<div className="grid grid-cols-2 gap-2.5">
|
||||
<div>
|
||||
<label className={labelCls}>시나리오명 <span className="text-[#f87171]">*</span></label>
|
||||
<label className={labelCls}>시나리오명 <span className="text-color-danger">*</span></label>
|
||||
<input type="text" placeholder="예: T+3h 기관실 침수 확대" className={inputCls} />
|
||||
</div>
|
||||
<div>
|
||||
<label className={labelCls}>연계 사고 <span className="text-[#f87171]">*</span></label>
|
||||
<label className={labelCls}>연계 사고 <span className="text-color-danger">*</span></label>
|
||||
<select defaultValue="0" className={inputCls}>
|
||||
{ops.map((op, i) => <option key={op.rescueOpsSn} value={i}>{op.opsCd} · {op.vesselNm}</option>)}
|
||||
<option value="new">+ 신규 사고 등록...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className={labelCls}>시간 단계 (Time Step) <span className="text-[#f87171]">*</span></label>
|
||||
<label className={labelCls}>시간 단계 (Time Step) <span className="text-color-danger">*</span></label>
|
||||
<select defaultValue="T+3h" className={inputCls}>
|
||||
{['T+0h (사고 발생 직후)', 'T+1h', 'T+2h', 'T+3h', 'T+6h', 'T+12h', 'T+24h', 'T+48h'].map(t => <option key={t} value={t}>{t}</option>)}
|
||||
</select>
|
||||
@ -451,11 +451,11 @@ function NewScenarioModal({ ops, onClose }: { ops: RescueOpsItem[]; onClose: ()
|
||||
<div className={sectionTitleCls}>{sectionIcon(2)} 선박 정보</div>
|
||||
<div className="grid grid-cols-3 gap-2.5">
|
||||
<div>
|
||||
<label className={labelCls}>선박명 <span className="text-[#f87171]">*</span></label>
|
||||
<label className={labelCls}>선박명 <span className="text-color-danger">*</span></label>
|
||||
<input type="text" defaultValue="M/V SEA GUARDIAN" className={inputCls} />
|
||||
</div>
|
||||
<div>
|
||||
<label className={labelCls}>선종 <span className="text-[#f87171]">*</span></label>
|
||||
<label className={labelCls}>선종 <span className="text-color-danger">*</span></label>
|
||||
<select defaultValue="화물선 (Cargo)" className={inputCls}>
|
||||
{['유조선 (Tanker)', '화물선 (Cargo)', '컨테이너선 (Container)', '여객선 (Passenger)', '어선 (Fishing)', 'LNG선', '케미컬선 (Chemical)', '기타'].map(v => <option key={v}>{v}</option>)}
|
||||
</select>
|
||||
@ -494,7 +494,7 @@ function NewScenarioModal({ ops, onClose }: { ops: RescueOpsItem[]; onClose: ()
|
||||
<div className={sectionTitleCls}>{sectionIcon(3)} 사고 조건 · 선체 상태</div>
|
||||
<div className="grid grid-cols-3 gap-2.5">
|
||||
<div>
|
||||
<label className={labelCls}>사고 유형 <span className="text-[#f87171]">*</span></label>
|
||||
<label className={labelCls}>사고 유형 <span className="text-color-danger">*</span></label>
|
||||
<select defaultValue="충돌 (Collision)" className={inputCls}>
|
||||
{['충돌 (Collision)', '좌초 (Grounding)', '침수 (Flooding)', '기관고장 (Engine Failure)', '화재 (Fire)', '전복 (Capsizing)', '구조손상 (Structural)'].map(v => <option key={v}>{v}</option>)}
|
||||
</select>
|
||||
@ -705,7 +705,7 @@ function ScenarioComparison({ chartData }: { chartData: ChartDataItem[] }) {
|
||||
{/* Grid */}
|
||||
{[0, 0.5, 1.0, 1.5, 2.0].map(v => {
|
||||
const y = PY + ph - (v / 2.0) * ph
|
||||
return <g key={v}><line x1={PX} x2={W - PX} y1={y} y2={y} stroke="rgba(255,255,255,.06)" /><text x={PX - 6} y={y + 3} textAnchor="end" fill="var(--fg-disabled)" fontSize={8} fontFamily="var(--font-mono)">{v}</text></g>
|
||||
return <g key={v}><line x1={PX} x2={W - PX} y1={y} y2={y} stroke="var(--stroke-light)" /><text x={PX - 6} y={y + 3} textAnchor="end" fill="var(--fg-disabled)" fontSize={8} fontFamily="var(--font-mono)">{v}</text></g>
|
||||
})}
|
||||
{/* GM=1.0 threshold */}
|
||||
<line x1={PX} x2={W - PX} y1={PY + ph - (1.0 / 2.0) * ph} y2={PY + ph - (1.0 / 2.0) * ph} stroke="rgba(239,68,68,.4)" strokeDasharray="4" />
|
||||
@ -732,7 +732,7 @@ function ScenarioComparison({ chartData }: { chartData: ChartDataItem[] }) {
|
||||
<svg viewBox={`0 0 ${W} ${H}`} width="100%" style={{ maxHeight: 160 }}>
|
||||
{[0, 5, 10, 15, 20, 25].map(v => {
|
||||
const y = PY + ph - (v / 25) * ph
|
||||
return <g key={v}><line x1={PX} x2={W - PX} y1={y} y2={y} stroke="rgba(255,255,255,.06)" /><text x={PX - 6} y={y + 3} textAnchor="end" fill="var(--fg-disabled)" fontSize={7} fontFamily="var(--font-mono)">{v}</text></g>
|
||||
return <g key={v}><line x1={PX} x2={W - PX} y1={y} y2={y} stroke="var(--stroke-light)" /><text x={PX - 6} y={y + 3} textAnchor="end" fill="var(--fg-disabled)" fontSize={7} fontFamily="var(--font-mono)">{v}</text></g>
|
||||
})}
|
||||
<line x1={PX} x2={W - PX} y1={PY + ph - (15 / 25) * ph} y2={PY + ph - (15 / 25) * ph} stroke="rgba(239,68,68,.3)" strokeDasharray="4" />
|
||||
<polyline points={chartData.map((d, i) => `${PX + i * xStep},${PY + ph - (d.list / 25) * ph}`).join(' ')} fill="none" stroke="var(--color-warning)" strokeWidth={2} />
|
||||
@ -751,7 +751,7 @@ function ScenarioComparison({ chartData }: { chartData: ChartDataItem[] }) {
|
||||
<svg viewBox={`0 0 ${W} ${H}`} width="100%" style={{ maxHeight: 160 }}>
|
||||
{[0, 50, 100, 150, 200].map(v => {
|
||||
const y = PY + ph - (v / 200) * ph
|
||||
return <g key={v}><line x1={PX} x2={W - PX} y1={y} y2={y} stroke="rgba(255,255,255,.06)" /><text x={PX - 6} y={y + 3} textAnchor="end" fill="var(--fg-disabled)" fontSize={7} fontFamily="var(--font-mono)">{v}</text></g>
|
||||
return <g key={v}><line x1={PX} x2={W - PX} y1={y} y2={y} stroke="var(--stroke-light)" /><text x={PX - 6} y={y + 3} textAnchor="end" fill="var(--fg-disabled)" fontSize={7} fontFamily="var(--font-mono)">{v}</text></g>
|
||||
})}
|
||||
{chartData.map((d, i) => {
|
||||
const barW = xStep * 0.5
|
||||
|
||||
@ -131,7 +131,7 @@ function LeftPanel({
|
||||
</div>
|
||||
|
||||
{/* CCTV 피드 */}
|
||||
<div className="w-full aspect-[4/3] bg-[#1a0000] border border-stroke rounded-md flex items-center justify-center relative overflow-hidden mt-1.5 flex-shrink-0">
|
||||
<div className="w-full aspect-[4/3] bg-bg-base border border-stroke rounded-md flex items-center justify-center relative overflow-hidden mt-1.5 flex-shrink-0">
|
||||
<div className="absolute inset-0" style={{ background: 'repeating-linear-gradient(0deg, rgba(255,0,0,.03), transparent 2px)' }} />
|
||||
<div className="text-[9px] text-[rgba(255,60,60,0.35)] font-mono">CCTV FEED #1</div>
|
||||
<div className="absolute top-1 left-1.5 text-[7px] text-[rgba(255,60,60,0.5)] font-mono">● REC</div>
|
||||
@ -146,7 +146,7 @@ function CenterMap({ activeType }: { activeType: AccidentType }) {
|
||||
const at = accidentTypes.find(t => t.id === activeType)!
|
||||
|
||||
return (
|
||||
<div className="flex-1 relative overflow-hidden bg-[#0a1628]">
|
||||
<div className="flex-1 relative overflow-hidden bg-bg-base">
|
||||
{/* 해양 배경 그라데이션 */}
|
||||
<div className="absolute inset-0" style={{
|
||||
background: 'radial-gradient(ellipse at 30% 40%, rgba(6,90,130,.25) 0%, transparent 60%), radial-gradient(ellipse at 70% 60%, rgba(8,60,100,.2) 0%, transparent 50%), linear-gradient(180deg, #0a1628, #0d1f35 50%, #091520)'
|
||||
@ -163,7 +163,7 @@ function CenterMap({ activeType }: { activeType: AccidentType }) {
|
||||
}} />
|
||||
|
||||
{/* 사고 해역 정보 */}
|
||||
<div className="absolute top-2.5 left-2.5 z-20 bg-[rgba(13,17,23,0.92)] border border-stroke rounded-md px-3 py-2 font-mono text-[9px] text-fg-disabled">
|
||||
<div className="absolute top-2.5 left-2.5 z-20 bg-[var(--dropdown-bg)] border border-stroke rounded-md px-3 py-2 backdrop-blur-sm font-mono text-[9px] text-fg-disabled">
|
||||
<div className="text-[10px] font-bold text-fg font-korean mb-1">사고 해역 정보</div>
|
||||
<div className="grid gap-x-1.5 gap-y-px" style={{ gridTemplateColumns: '32px 1fr' }}>
|
||||
<span>위치</span><b className="text-fg">37°28'N, 126°15'E</b>
|
||||
|
||||
@ -8,32 +8,32 @@ export default {
|
||||
extend: {
|
||||
colors: {
|
||||
bg: {
|
||||
base: '#0a0e1a',
|
||||
surface: '#0f1524',
|
||||
elevated: '#121929',
|
||||
card: '#1a2236',
|
||||
'surface-hover': '#1e2844',
|
||||
base: 'var(--bg-base)',
|
||||
surface: 'var(--bg-surface)',
|
||||
elevated: 'var(--bg-elevated)',
|
||||
card: 'var(--bg-card)',
|
||||
'surface-hover': 'var(--bg-surface-hover)',
|
||||
},
|
||||
stroke: {
|
||||
DEFAULT: '#1e2a42',
|
||||
light: '#2a3a5c',
|
||||
DEFAULT: 'var(--stroke-default)',
|
||||
light: 'var(--stroke-light)',
|
||||
},
|
||||
fg: {
|
||||
DEFAULT: '#edf0f7',
|
||||
sub: '#c0c8dc',
|
||||
disabled: '#9ba3b8',
|
||||
DEFAULT: 'var(--fg-default)',
|
||||
sub: 'var(--fg-sub)',
|
||||
disabled: 'var(--fg-disabled)',
|
||||
},
|
||||
color: {
|
||||
accent: '#06b6d4',
|
||||
info: '#3b82f6',
|
||||
tertiary: '#a855f7',
|
||||
danger: '#ef4444',
|
||||
warning: '#f97316',
|
||||
caution: '#eab308',
|
||||
success: '#22c55e',
|
||||
accent: 'var(--color-accent)',
|
||||
info: 'var(--color-info)',
|
||||
tertiary: 'var(--color-tertiary)',
|
||||
danger: 'var(--color-danger)',
|
||||
warning: 'var(--color-warning)',
|
||||
caution: 'var(--color-caution)',
|
||||
success: 'var(--color-success)',
|
||||
boom: {
|
||||
DEFAULT: '#f59e0b',
|
||||
hover: '#fbbf24',
|
||||
DEFAULT: 'var(--color-boom)',
|
||||
hover: 'var(--color-boom-hover)',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
불러오는 중...
Reference in New Issue
Block a user