feat(theme): 다크/라이트 테마 전환 기능 및 시맨틱 컬러 토큰 적용

This commit is contained in:
leedano 2026-03-31 14:57:25 +09:00
부모 5e2076647c
커밋 d8a5acc1e6
21개의 변경된 파일487개의 추가작업 그리고 317개의 파일을 삭제

파일 보기

@ -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]">&#x1F4D6;</span>
</button>

파일 보기

@ -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.152.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)',
},
},
},