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 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"> <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> <title>frontend</title>
<script>
document.documentElement.setAttribute(
'data-theme',
localStorage.getItem('wing-theme') || 'dark'
);
</script>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

파일 보기

@ -61,9 +61,9 @@ function App() {
<div style={{ <div style={{
width: '100vw', height: '100vh', display: 'flex', width: '100vw', height: '100vh', display: 'flex',
flexDirection: 'column', alignItems: 'center', justifyContent: 'center', 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={{ <div style={{
width: 32, height: 32, border: '3px solid rgba(6,182,212,0.2)', width: 32, height: 32, border: '3px solid rgba(6,182,212,0.2)',
borderTop: '3px solid rgba(6,182,212,0.8)', borderRadius: '50%', borderTop: '3px solid rgba(6,182,212,0.8)', borderRadius: '50%',

파일 보기

@ -55,7 +55,7 @@ export function LoginPage() {
} }
return ( 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 */} {/* Background image */}
<div style={{ <div style={{
position: 'absolute', inset: 0, position: 'absolute', inset: 0,
@ -82,7 +82,7 @@ export function LoginPage() {
<img <img
src="/wing_logo_text_white.svg" src="/wing_logo_text_white.svg"
alt="WING 해양환경 위기대응 통합시스템" alt="WING 해양환경 위기대응 통합시스템"
className="h-7 mx-auto block" className="h-7 mx-auto block wing-logo"
/> />
</div> </div>

파일 보기

@ -3,6 +3,7 @@ import type { MainTab } from '../../types/navigation'
import { useAuthStore } from '../../store/authStore' import { useAuthStore } from '../../store/authStore'
import { useMenuStore } from '../../store/menuStore' import { useMenuStore } from '../../store/menuStore'
import { useMapStore } from '../../store/mapStore' import { useMapStore } from '../../store/mapStore'
import { useThemeStore } from '../../store/themeStore'
import UserManualPopup from '../ui/UserManualPopup' import UserManualPopup from '../ui/UserManualPopup'
interface TopBarProps { interface TopBarProps {
@ -17,6 +18,7 @@ export function TopBar({ activeTab, onTabChange }: TopBarProps) {
const { hasPermission, user, logout } = useAuthStore() const { hasPermission, user, logout } = useAuthStore()
const { menuConfig, isLoaded } = useMenuStore() const { menuConfig, isLoaded } = useMenuStore()
const { mapToggles, toggleMap, mapTypes, measureMode, setMeasureMode } = useMapStore() const { mapToggles, toggleMap, mapTypes, measureMode, setMeasureMode } = useMapStore()
const { theme, toggleTheme } = useThemeStore()
const MAP_TABS = new Set<string>(['prediction', 'hns', 'scat', 'incidents']) const MAP_TABS = new Set<string>(['prediction', 'hns', 'scat', 'incidents'])
const isMapTab = MAP_TABS.has(activeTab) 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" className="flex items-center hover:opacity-80 transition-opacity cursor-pointer"
title="홈으로 이동" 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> </button>
{/* Divider */} {/* 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 ? 'border-l border-l-[rgba(239,68,68,0.25)] ml-1 flex items-center gap-1.5' : ''}
${ ${
isMonitor 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 : activeTab === tab.id
? isIncident ? isIncident
? 'text-[#a5b4fc] bg-[rgba(99,102,241,0.18)] shadow-[0_0_8px_rgba(99,102,241,0.3)]' ? '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 : isIncident
? 'text-[#818cf8] hover:text-[#a5b4fc] hover:bg-[rgba(99,102,241,0.1)]' ? '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 ? ( {isMonitor ? (
<> <>
<span className="hidden xl:flex items-center gap-1.5"> <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} {tab.label}
</span> </span>
<span className="xl:hidden text-[1rem] leading-none">{tab.icon}</span> <span className="xl:hidden text-[1rem] leading-none">{tab.icon}</span>
@ -165,7 +167,7 @@ export function TopBar({ activeTab, onTabChange }: TopBarProps) {
</button> </button>
{showQuickMenu && ( {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"> {/* <div className="px-3 py-1.5 flex items-center gap-2 text-[0.6875rem] font-bold text-fg-disabled">
<span>📐</span> · <span>📐</span> ·
@ -178,7 +180,7 @@ export function TopBar({ activeTab, onTabChange }: TopBarProps) {
? 'text-fg-disabled opacity-40 cursor-not-allowed' ? 'text-fg-disabled opacity-40 cursor-not-allowed'
: measureMode === 'distance' : measureMode === 'distance'
? 'text-color-accent bg-[rgba(6,182,212,0.1)]' ? '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> <span className="text-[0.8125rem]"></span>
@ -192,7 +194,7 @@ export function TopBar({ activeTab, onTabChange }: TopBarProps) {
? 'text-fg-disabled opacity-40 cursor-not-allowed' ? 'text-fg-disabled opacity-40 cursor-not-allowed'
: measureMode === 'area' : measureMode === 'area'
? 'text-color-accent bg-[rgba(6,182,212,0.1)]' ? '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> <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"> <div className="px-3 py-1.5 flex items-center gap-2 text-[0.6875rem] font-bold text-fg-disabled">
<span>🖨</span> <span>🖨</span>
</div> </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> <span className="text-[0.8125rem]">📸</span>
</button> </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> <span className="text-[0.8125rem]">🖨</span>
</button> </button>
@ -219,7 +221,7 @@ export function TopBar({ activeTab, onTabChange }: TopBarProps) {
<span>🗺</span> <span>🗺</span>
</div> </div>
{mapTypes.map(item => ( {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="flex items-center gap-2.5">
<span className="text-[0.8125rem]">🗺</span> {item.mapNm} <span className="text-[0.8125rem]">🗺</span> {item.mapNm}
</span> </span>
@ -231,13 +233,33 @@ export function TopBar({ activeTab, onTabChange }: TopBarProps) {
<div className="my-1.5 border-t border-stroke" /> <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 <button
onClick={() => { onClick={() => {
setShowManual(true) setShowManual(true)
setShowQuickMenu(false) 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> <span className="text-[0.8125rem]">&#x1F4D6;</span>
</button> </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-800: #6b21a8;
--purple-900: #581c87; --purple-900: #581c87;
--purple-1000: #3b0764; --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 { input[type="date"]::-webkit-calendar-picker-indicator:hover {
opacity: 1; 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 { .boom-setting-input {
width: 56px; width: 56px;
padding: 3px 6px; padding: 3px 6px;
background: var(--bg-elevated); background: var(--bg-base);
border: 1px solid var(--stroke-default); border: 1px solid var(--stroke-default);
border-radius: 4px; border-radius: 4px;
color: var(--color-warning); color: var(--color-accent);
font-family: var(--font-mono); font-family: var(--font-mono);
font-size: 11px; font-size: 11px;
font-weight: 600; font-weight: 600;
@ -1333,4 +1333,91 @@
opacity: 0.4; opacity: 0.4;
cursor: not-allowed; 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') { if (playerState === 'offline') {
return ( 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-2xl opacity-30 mb-2">📹</div>
<div className="text-[11px] font-korean text-fg-disabled opacity-70"> <div className="text-[11px] font-korean text-fg-disabled opacity-70">
{sttsCd === 'MAINT' ? '점검중' : '오프라인'} {sttsCd === 'MAINT' ? '점검중' : '오프라인'}
@ -239,7 +239,7 @@ export const CCTVPlayer = forwardRef<CCTVPlayerHandle, CCTVPlayerProps>(({
// URL 미설정 // URL 미설정
if (playerState === 'no-url') { if (playerState === 'no-url') {
return ( 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-2xl opacity-20 mb-2">📹</div>
<div className="text-[10px] font-korean text-fg-disabled opacity-50"> URL </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> <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') { if (playerState === 'error') {
return ( 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-2xl opacity-30 mb-2"></div>
<div className="text-[10px] font-korean text-color-danger opacity-70"> </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> <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"> <div ref={containerRef} className="absolute inset-0">
{/* 로딩 오버레이 */} {/* 로딩 오버레이 */}
{playerState === 'loading' && ( {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-lg opacity-40 animate-pulse mb-2">📹</div>
<div className="text-[10px] font-korean text-fg-disabled opacity-50"> ...</div> <div className="text-[10px] font-korean text-fg-disabled opacity-50"> ...</div>
</div> </div>
@ -338,7 +338,7 @@ export const CCTVPlayer = forwardRef<CCTVPlayerHandle, CCTVPlayerProps>(({
</span> </span>
{sttsCd === 'LIVE' && ( {sttsCd === 'LIVE' && (
<span <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)' }} style={{ background: 'rgba(239,68,68,.3)' }}
> >
REC REC

파일 보기

@ -265,7 +265,7 @@ export function CctvView() {
</div> </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 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"> <div className="flex items-center gap-2 min-w-0">
@ -484,9 +484,9 @@ export function CctvView() {
offset={14} offset={14}
className="cctv-dark-popup" className="cctv-dark-popup"
> >
<div className="p-2" style={{ minWidth: 150, background: '#1a1f2e', borderRadius: 6 }}> <div className="p-2" style={{ minWidth: 150, background: 'var(--bg-card)', borderRadius: 6 }}>
<div className="text-[11px] font-bold text-white mb-1">{mapPopup.cameraNm}</div> <div className="text-[11px] font-bold text-fg mb-1">{mapPopup.cameraNm}</div>
<div className="text-[9px] text-gray-400 mb-1.5">{mapPopup.locDc ?? ''}</div> <div className="text-[9px] text-fg-disabled mb-1.5">{mapPopup.locDc ?? ''}</div>
<div className="flex items-center gap-1.5 mb-2"> <div className="flex items-center gap-1.5 mb-2">
<span className="text-[8px] font-bold px-1.5 py-px rounded-full" <span className="text-[8px] font-bold px-1.5 py-px rounded-full"
style={mapPopup.sttsCd === 'LIVE' style={mapPopup.sttsCd === 'LIVE'
@ -494,7 +494,7 @@ export function CctvView() {
: { background: 'rgba(148,163,184,.15)', color: '#94a3b8' } : { background: 'rgba(148,163,184,.15)', color: '#94a3b8' }
} }
>{mapPopup.sttsCd === 'LIVE' ? '● LIVE' : '● OFF'}</span> >{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> </div>
<button <button
onClick={() => { handleSelectCamera(mapPopup); setMapPopup(null) }} onClick={() => { handleSelectCamera(mapPopup); setMapPopup(null) }}
@ -520,7 +520,7 @@ export function CctvView() {
{Array.from({ length: totalCells }).map((_, i) => { {Array.from({ length: totalCells }).map((_, i) => {
const cam = activeCells[i] const cam = activeCells[i]
return ( 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 ? ( {cam ? (
<CCTVPlayer <CCTVPlayer
ref={el => { playerRefs.current[i] = el }} ref={el => { playerRefs.current[i] = el }}

파일 보기

@ -212,7 +212,7 @@ export function RealtimeDrone() {
</div> </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 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"> <div className="flex items-center gap-2 min-w-0">
@ -338,13 +338,13 @@ export function RealtimeDrone() {
offset={36} offset={36}
className="cctv-dark-popup" 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"> <div className="flex items-center gap-1.5 mb-1">
<span className="text-sm">🚁</span> <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>
<div className="text-[9px] text-gray-400 mb-0.5">{mapPopup.droneModel}</div> <div className="text-[9px] text-fg-disabled 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-[8px] text-fg-disabled font-mono mb-2">{mapPopup.ip} · {mapPopup.region}</div>
<div className="flex items-center gap-1.5 mb-2"> <div className="flex items-center gap-1.5 mb-2">
<span className="text-[8px] font-bold px-1.5 py-px rounded-full" <span className="text-[8px] font-bold px-1.5 py-px rounded-full"
style={{ background: statusInfo(mapPopup.status).bg, color: statusInfo(mapPopup.status).color }} style={{ background: statusInfo(mapPopup.status).bg, color: statusInfo(mapPopup.status).color }}
@ -384,7 +384,7 @@ export function RealtimeDrone() {
{Array.from({ length: totalCells }).map((_, i) => { {Array.from({ length: totalCells }).map((_, i) => {
const stream = activeCells[i] const stream = activeCells[i]
return ( 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 ? ( {stream && stream.status === 'streaming' && stream.hlsUrl ? (
<CCTVPlayer <CCTVPlayer
ref={el => { playerRefs.current[i] = el }} ref={el => { playerRefs.current[i] = el }}

파일 보기

@ -189,14 +189,14 @@ export function SatelliteRequest() {
// ── 섹션 헤더 헬퍼 (BlackSky 폼) ── // ── 섹션 헤더 헬퍼 (BlackSky 폼) ──
const sectionHeader = (num: number, label: string) => ( 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="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-[#818cf8]" style={{ background: 'rgba(99,102,241,.12)' }}>{num}</div> <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} {label}
</div> </div>
) )
const bsInput = "w-full px-3 py-2 rounded-md text-[11px] font-korean outline-none box-border" 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 ( return (
<div className="overflow-y-auto px-6 pt-1 pb-2" style={{ height: mainTab === 'map' ? '100%' : undefined }}> <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 justify-between mb-3">
<div className="flex items-center gap-2.5"> <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)' }}> <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> <div>
<div className="text-sm font-bold text-fg font-korean">BlackSky</div> <div className="text-sm font-bold text-fg font-korean">BlackSky</div>
@ -633,7 +633,7 @@ export function SatelliteRequest() {
))} ))}
</div> </div>
<div className="text-[9px] text-fg-disabled font-korean leading-relaxed"> . . Dawn-to-Dusk .</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> </div>
{/* UP42 (EO + SAR) */} {/* UP42 (EO + SAR) */}
@ -644,7 +644,7 @@ export function SatelliteRequest() {
<div className="flex items-center justify-between mb-3"> <div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2.5"> <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)' }}> <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> <div>
<div className="text-sm font-bold text-fg font-korean">UP42 EO + SAR</div> <div className="text-sm font-bold text-fg font-korean">UP42 EO + SAR</div>
@ -671,7 +671,7 @@ export function SatelliteRequest() {
</div> </div>
<div className="flex gap-1.5 mb-2.5 flex-wrap"> <div className="flex gap-1.5 mb-2.5 flex-wrap">
{['Pléiades Neo', 'SPOT 6/7'].map((t, i) => ( {['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) => ( {['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> <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> <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>
<div className="text-[9px] text-fg-disabled font-korean leading-relaxed">(EO) + (SAR) . · SAR . .</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>
</div> </div>
@ -693,33 +693,33 @@ export function SatelliteRequest() {
{/* ── BlackSky 긴급 촬영 요청 ── */} {/* ── BlackSky 긴급 촬영 요청 ── */}
{modalPhase === '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="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="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)' }}> <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> <div>
<div className="text-[15px] font-bold font-korean text-[#e2e8f0]">BlackSky </div> <div className="text-[15px] font-bold font-korean text-fg">BlackSky </div>
<div className="text-[9px] font-korean mt-0.5 text-[#64748b]">Maxar E1SO RapiDoc API · </div> <div className="text-[9px] font-korean mt-0.5 text-fg-disabled">Maxar E1SO RapiDoc API · </div>
</div> </div>
</div> </div>
<div className="flex items-center gap-2"> <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> <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-[#64748b]"></button> <button onClick={() => setModalPhase('none')} className="text-lg cursor-pointer p-1 bg-transparent border-none text-fg-disabled"></button>
</div> </div>
</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 상태 */} {/* 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="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)' }} /> <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-[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="text-[9px] font-mono text-fg-disabled">eapi.maxar.com/e1so/rapidoc · Latency: 142ms</span>
<span className="ml-auto text-[8px] font-mono text-[#64748b]">Quota: 47/50 </span> <span className="ml-auto text-[8px] font-mono text-fg-disabled">Quota: 47/50 </span>
</div> </div>
{/* ① 태스킹 유형 */} {/* ① 태스킹 유형 */}
@ -727,7 +727,7 @@ export function SatelliteRequest() {
{sectionHeader(1, '태스킹 유형 · 우선순위')} {sectionHeader(1, '태스킹 유형 · 우선순위')}
<div className="grid grid-cols-3 gap-2.5"> <div className="grid grid-cols-3 gap-2.5">
<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}> <select className={bsInput} style={bsInputStyle}>
<option> (Emergency)</option> <option> (Emergency)</option>
<option> (Standard)</option> <option> (Standard)</option>
@ -735,7 +735,7 @@ export function SatelliteRequest() {
</select> </select>
</div> </div>
<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}> <select className={bsInput} style={bsInputStyle}>
<option>P1 (90 )</option> <option>P1 (90 )</option>
<option>P2 (6 )</option> <option>P2 (6 )</option>
@ -743,7 +743,7 @@ export function SatelliteRequest() {
</select> </select>
</div> </div>
<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}> <select className={bsInput} style={bsInputStyle}>
<option>Single Collect</option> <option>Single Collect</option>
<option>Multi-pass Monitoring</option> <option>Multi-pass Monitoring</option>
@ -758,26 +758,26 @@ export function SatelliteRequest() {
{sectionHeader(2, '관심 영역 (AOI)')} {sectionHeader(2, '관심 영역 (AOI)')}
<div className="grid grid-cols-3 gap-2.5 items-end"> <div className="grid grid-cols-3 gap-2.5 items-end">
<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="34.5832" className={bsInput} style={bsInputStyle} /> <input type="text" defaultValue="34.5832" className={bsInput} style={bsInputStyle} />
</div> </div>
<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} /> <input type="text" defaultValue="128.4217" className={bsInput} style={bsInputStyle} />
</div> </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>
<div className="mt-2 grid grid-cols-3 gap-2.5"> <div className="mt-2 grid grid-cols-3 gap-2.5">
<div> <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} /> <input type="number" defaultValue={10} step={1} min={1} className={bsInput} style={bsInputStyle} />
</div> </div>
<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} /> <input type="number" defaultValue={20} step={5} min={0} max={100} className={bsInput} style={bsInputStyle} />
</div> </div>
<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} /> <input type="number" defaultValue={25} step={5} min={0} max={45} className={bsInput} style={bsInputStyle} />
</div> </div>
</div> </div>
@ -788,15 +788,15 @@ export function SatelliteRequest() {
{sectionHeader(3, '촬영 기간 · 반복')} {sectionHeader(3, '촬영 기간 · 반복')}
<div className="grid grid-cols-3 gap-2.5"> <div className="grid grid-cols-3 gap-2.5">
<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-26T08:00" className={bsInput} style={bsInputStyle} /> <input type="datetime-local" defaultValue="2026-02-26T08:00" className={bsInput} style={bsInputStyle} />
</div> </div>
<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} /> <input type="datetime-local" defaultValue="2026-02-27T20:00" className={bsInput} style={bsInputStyle} />
</div> </div>
<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}> <select className={bsInput} style={bsInputStyle}>
<option>1 ()</option> <option>1 ()</option>
<option> ( )</option> <option> ( )</option>
@ -813,7 +813,7 @@ export function SatelliteRequest() {
{sectionHeader(4, '산출물 설정')} {sectionHeader(4, '산출물 설정')}
<div className="grid grid-cols-2 gap-2.5"> <div className="grid grid-cols-2 gap-2.5">
<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}> <select className={bsInput} style={bsInputStyle}>
<option>Ortho-Rectified ()</option> <option>Ortho-Rectified ()</option>
<option>Pan-sharpened ()</option> <option>Pan-sharpened ()</option>
@ -821,7 +821,7 @@ export function SatelliteRequest() {
</select> </select>
</div> </div>
<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}> <select className={bsInput} style={bsInputStyle}>
<option>GeoTIFF</option> <option>GeoTIFF</option>
<option>NITF</option> <option>NITF</option>
@ -836,7 +836,7 @@ export function SatelliteRequest() {
{ label: '변화탐지 (Change Detection)', checked: false }, { label: '변화탐지 (Change Detection)', checked: false },
{ label: '웹훅 알림', checked: false }, { label: '웹훅 알림', checked: false },
].map((opt, i) => ( ].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} <input type="checkbox" defaultChecked={opt.checked} style={{ accentColor: '#818cf8', transform: 'scale(.85)' }} /> {opt.label}
</label> </label>
))} ))}
@ -848,7 +848,7 @@ export function SatelliteRequest() {
{sectionHeader(5, '연계 사고 · 비고')} {sectionHeader(5, '연계 사고 · 비고')}
<div className="grid grid-cols-2 gap-2.5 mb-2"> <div className="grid grid-cols-2 gap-2.5 mb-2">
<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}> <select className={bsInput} style={bsInputStyle}>
<option>OIL-2024-0892 · M/V STELLAR DAISY</option> <option>OIL-2024-0892 · M/V STELLAR DAISY</option>
<option>HNS-2024-041 · </option> <option>HNS-2024-041 · </option>
@ -857,23 +857,23 @@ export function SatelliteRequest() {
</select> </select>
</div> </div>
<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} /> <input type="text" placeholder="소속 / 이름" className={bsInput} style={bsInputStyle} />
</div> </div>
</div> </div>
<textarea <textarea
placeholder="촬영 요청 목적, 특이사항, 관심 대상 등을 기록합니다..." 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> </div>
{/* 하단 버튼 */} {/* 하단 버튼 */}
<div className="px-6 py-3.5 border-t border-[#21262d] flex items-center gap-2 shrink-0"> <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-[#64748b]"> <div className="flex-1 text-[9px] font-korean leading-relaxed text-fg-disabled">
<span className="text-red-400">*</span> · P1 90 <span className="text-red-400">*</span> · P1 90
</div> </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> <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>
</div> </div>
@ -881,31 +881,31 @@ export function SatelliteRequest() {
{/* ── UP42 카탈로그 주문 ── */} {/* ── UP42 카탈로그 주문 ── */}
{modalPhase === '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="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="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)' }}> <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> <div>
<div className="text-[15px] font-bold font-korean text-[#e2e8f0]"> </div> <div className="text-[15px] font-bold font-korean text-fg"> </div>
<div className="text-[9px] font-korean mt-0.5 text-[#64748b]"> (AOI) </div> <div className="text-[9px] font-korean mt-0.5 text-fg-disabled"> (AOI) </div>
</div> </div>
</div> </div>
<div className="flex items-center gap-2"> <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> <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>
</div> </div>
{/* 본문 (좌: 사이드바, 우: 지도+AOI) */} {/* 본문 (좌: 사이드바, 우: 지도+AOI) */}
<div className="flex-1 flex overflow-hidden"> <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 탭 */} {/* 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 => ( {(['optical', 'sar', 'elevation'] as const).map(t => (
<button <button
key={t} key={t}
@ -920,34 +920,34 @@ export function SatelliteRequest() {
</div> </div>
{/* 필터 바 */} {/* 필터 바 */}
<div className="flex items-center gap-1.5 px-3 py-2 border-b border-[#21262d] shrink-0"> <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-[#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-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-[#818cf8]" style={{ background: 'rgba(99,102,241,.1)', border: '1px solid rgba(99,102,241,.2)' }}> 20% </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-[#64748b]"> </span> <span className="ml-auto text-[9px] font-mono text-fg-disabled"> </span>
</div> </div>
{/* 컬렉션 수 */} {/* 컬렉션 수 */}
<div className="px-3 py-1.5 border-b border-[#21262d] text-[9px] font-korean shrink-0 text-[#64748b]"> <div className="px-3 py-1.5 border-b border-stroke text-[9px] font-korean shrink-0 text-fg-disabled">
<b className="text-[#e2e8f0]">{up42Filtered.length}</b> <b className="text-fg">{up42Filtered.length}</b>
</div> </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 => ( {up42Filtered.map(sat => (
<div <div
key={sat.id} key={sat.id}
onClick={() => setUp42SelSat(up42SelSat === sat.id ? null : 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={{ style={{
background: up42SelSat === sat.id ? 'rgba(59,130,246,.08)' : 'transparent', 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="w-1 h-8 rounded-full shrink-0" style={{ background: sat.color }} />
<div className="flex-1 min-w-0"> <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"> <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> <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>} {'delay' in sat && sat.delay && <span className="text-[8px] font-bold" style={{ color: '#eab308' }}> </span>}
</div> </div>
</div> </div>
@ -1008,33 +1008,33 @@ export function SatelliteRequest() {
</Map> </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="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-[#64748b] mb-1.5">🛰 </div> <div className="text-[9px] font-bold text-fg-disabled mb-1.5">🛰 </div>
{satPasses.slice(0, 4).map(p => ( {satPasses.slice(0, 4).map(p => (
<div key={p.id} className="flex items-center gap-1.5 mb-1"> <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 }} /> <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>
))} ))}
<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)' }} /> <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>
</div> </div>
{/* 로딩 */} {/* 로딩 */}
{satPassesLoading && ( {satPassesLoading && (
<div className="absolute inset-0 flex items-center justify-center z-10" style={{ background: 'rgba(0,0,0,.5)' }}> <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> </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="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-[#e2e8f0]"> <div className="text-[10px] font-bold font-korean mb-2 text-fg">
🛰 ({satPasses.length}) 🛰 ({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>
<div className="flex flex-col gap-1.5"> <div className="flex flex-col gap-1.5">
{satPasses.map(pass => { {satPasses.map(pass => {
@ -1048,20 +1048,20 @@ export function SatelliteRequest() {
onClick={() => setUp42SelPass(up42SelPass === pass.id ? null : pass.id)} onClick={() => setUp42SelPass(up42SelPass === pass.id ? null : pass.id)}
className="flex items-center gap-3 px-3 py-2 rounded-md cursor-pointer transition-colors" className="flex items-center gap-3 px-3 py-2 rounded-md cursor-pointer transition-colors"
style={{ style={{
background: up42SelPass === pass.id ? 'rgba(59,130,246,.1)' : '#161b22', background: up42SelPass === pass.id ? 'rgba(59,130,246,.1)' : 'var(--bg-elevated)',
border: up42SelPass === pass.id ? `1px solid ${pass.color}40` : '1px solid #21262d', 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="w-1.5 h-6 rounded-full shrink-0" style={{ background: pass.color }} />
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-[10px] font-bold font-korean text-[#e2e8f0]">{pass.satellite}</span> <span className="text-[10px] font-bold font-korean text-fg">{pass.satellite}</span>
<span className="text-[8px] text-[#64748b]">{pass.provider}</span> <span className="text-[8px] text-fg-disabled">{pass.provider}</span>
</div> </div>
<div className="flex items-center gap-2 mt-0.5"> <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-[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>
</div> </div>
<span className="px-1.5 py-px rounded text-[8px] font-bold shrink-0" style={{ <span className="px-1.5 py-px rounded text-[8px] font-bold shrink-0" style={{
@ -1078,18 +1078,18 @@ export function SatelliteRequest() {
</div> </div>
{/* 푸터 */} {/* 푸터 */}
<div className="px-6 py-3 border-t border-[#21262d] flex items-center justify-between shrink-0"> <div className="px-6 py-3 border-t border-stroke 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="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"> <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 : '없음'} : {up42SelPass ? satPasses.find(p => p.id === up42SelPass)?.satellite : up42SelSat ? up42Satellites.find(s => s.id === up42SelSat)?.name : '없음'}
</span> </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 <button
onClick={() => setModalPhase('none')} onClick={() => setModalPhase('none')}
className="px-6 py-2 rounded-lg border-none text-[11px] font-bold cursor-pointer font-korean text-white transition-opacity" className="px-6 py-2 rounded-lg border-none text-[11px] font-bold cursor-pointer font-korean text-white transition-opacity"
style={{ style={{
background: up42SelSat ? 'linear-gradient(135deg,#3b82f6,#06b6d4)' : '#21262d', background: up42SelSat ? 'linear-gradient(135deg,#3b82f6,#06b6d4)' : 'var(--stroke-light)',
opacity: up42SelSat ? 1 : 0.5, opacity: up42SelSat ? 1 : 0.5,
color: up42SelSat ? '#fff' : '#64748b', color: up42SelSat ? '#fff' : '#64748b',
boxShadow: up42SelSat ? '0 4px 16px rgba(59,130,246,.35)' : 'none', 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 }) { 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 === '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: '#22c55e' }}></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: '#eab308' }}></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 { interface DischargeZonePanelProps {
@ -75,8 +75,8 @@ export function DischargeZonePanel({ lat, lon, distanceNm, onClose }: DischargeZ
style={{ style={{
width: 320, width: 320,
maxHeight: 'calc(100% - 32px)', maxHeight: 'calc(100% - 32px)',
background: 'rgba(13,17,23,0.95)', background: 'var(--bg-base)',
border: '1px solid #30363d', border: '1px solid var(--stroke-default)',
boxShadow: '0 16px 48px rgba(0,0,0,0.5)', boxShadow: '0 16px 48px rgba(0,0,0,0.5)',
backdropFilter: 'blur(12px)', backdropFilter: 'blur(12px)',
}} }}
@ -86,25 +86,25 @@ export function DischargeZonePanel({ lat, lon, distanceNm, onClose }: DischargeZ
className="shrink-0 flex items-center justify-between" className="shrink-0 flex items-center justify-between"
style={{ style={{
padding: '10px 14px', padding: '10px 14px',
borderBottom: '1px solid #30363d', borderBottom: '1px solid var(--stroke-default)',
background: 'linear-gradient(135deg, #1c2333, #161b22)', background: 'var(--bg-elevated)',
}} }}
> >
<div> <div>
<div className="text-[11px] font-bold text-[#f0f6fc] font-korean">🚢 </div> <div className="text-[11px] font-bold text-fg font-korean">🚢 </div>
<div className="text-[8px] text-[#8b949e] font-korean"> 22</div> <div className="text-[8px] text-fg-sub font-korean"> 22</div>
</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> </div>
{/* Location Info */} {/* 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"> <div className="flex items-center justify-between mb-1.5">
<span className="text-[9px] text-[#8b949e] font-korean"> </span> <span className="text-[9px] text-fg-sub 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 font-mono">{lat.toFixed(4)}°N, {lon.toFixed(4)}°E</span>
</div> </div>
<div className="flex items-center justify-between mb-2"> <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] }}> <span className="text-[11px] font-bold font-mono" style={{ color: ZONE_COLORS[zoneIdx] }}>
{distanceNm.toFixed(1)} NM {distanceNm.toFixed(1)} NM
</span> </span>
@ -119,9 +119,9 @@ export function DischargeZonePanel({ lat, lon, distanceNm, onClose }: DischargeZ
padding: '3px 0', padding: '3px 0',
fontSize: 8, fontSize: 8,
fontWeight: i === zoneIdx ? 700 : 400, fontWeight: i === zoneIdx ? 700 : 400,
color: i === zoneIdx ? '#fff' : '#8b949e', color: i === zoneIdx ? 'var(--fg-default)' : 'var(--fg-sub)',
background: i === zoneIdx ? ZONE_COLORS[i] : 'rgba(255,255,255,0.04)', background: i === zoneIdx ? ZONE_COLORS[i] : 'var(--hover-overlay)',
border: i === zoneIdx ? 'none' : '1px solid #21262d', border: i === zoneIdx ? 'none' : '1px solid var(--stroke-light)',
}} }}
> >
{label} {label}
@ -131,7 +131,7 @@ export function DischargeZonePanel({ lat, lon, distanceNm, onClose }: DischargeZ
</div> </div>
{/* Rules */} {/* 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 => { {categories.map(cat => {
const catRules = RULES.filter(r => r.category === cat) const catRules = RULES.filter(r => r.category === cat)
const isExpanded = expandedCat === cat const isExpanded = expandedCat === cat
@ -140,7 +140,7 @@ export function DischargeZonePanel({ lat, lon, distanceNm, onClose }: DischargeZ
const summaryColor = allForbidden ? '#ef4444' : allAllowed ? '#22c55e' : '#eab308' const summaryColor = allForbidden ? '#ef4444' : allAllowed ? '#22c55e' : '#eab308'
return ( return (
<div key={cat} style={{ borderBottom: '1px solid #21262d' }}> <div key={cat} style={{ borderBottom: '1px solid var(--stroke-light)' }}>
<div <div
className="flex items-center justify-between cursor-pointer" className="flex items-center justify-between cursor-pointer"
onClick={() => setExpandedCat(isExpanded ? null : cat)} 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 className="flex items-center gap-2">
<div style={{ width: 6, height: 6, borderRadius: '50%', background: summaryColor }} /> <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>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-[8px] font-semibold" style={{ color: summaryColor }}> <span className="text-[8px] font-semibold" style={{ color: summaryColor }}>
{allForbidden ? '전체 불가' : allAllowed ? '전체 가능' : '항목별 상이'} {allForbidden ? '전체 불가' : allAllowed ? '전체 가능' : '항목별 상이'}
</span> </span>
<span className="text-[9px] text-[#8b949e]">{isExpanded ? '▾' : '▸'}</span> <span className="text-[9px] text-fg-sub">{isExpanded ? '▾' : '▸'}</span>
</div> </div>
</div> </div>
@ -167,18 +167,18 @@ export function DischargeZonePanel({ lat, lon, distanceNm, onClose }: DischargeZ
style={{ style={{
padding: '5px 8px', padding: '5px 8px',
marginBottom: 2, marginBottom: 2,
background: 'rgba(255,255,255,0.02)', background: 'var(--hover-overlay)',
borderRadius: 4, 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]} /> <StatusBadge status={rule.zones[zoneIdx]} />
</div> </div>
))} ))}
{catRules.some(r => r.condition && r.zones[zoneIdx] !== 'forbidden') && ( {catRules.some(r => r.condition && r.zones[zoneIdx] !== 'forbidden') && (
<div className="mt-1" style={{ padding: '4px 8px' }}> <div className="mt-1" style={{ padding: '4px 8px' }}>
{catRules.filter(r => r.condition && r.zones[zoneIdx] !== 'forbidden').map((r, i) => ( {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} 💡 {r.item}: {r.condition}
</div> </div>
))} ))}
@ -192,9 +192,9 @@ export function DischargeZonePanel({ lat, lon, distanceNm, onClose }: DischargeZ
</div> </div>
{/* Footer */} {/* Footer */}
<div className="shrink-0" style={{ padding: '6px 14px', borderTop: '1px solid #21262d' }}> <div className="shrink-0" style={{ padding: '6px 14px', borderTop: '1px solid var(--stroke-light)' }}>
<div className="text-[7px] text-[#8b949e] font-korean leading-relaxed"> <div className="text-[7px] text-fg-sub font-korean leading-relaxed">
. . . .
</div> </div>
</div> </div>
</div> </div>

파일 보기

@ -466,7 +466,7 @@ const WeatherPopup = forwardRef<HTMLDivElement, {
<span className="text-xs"></span> <span className="text-xs"></span>
<div> <div>
<div className="text-fg-disabled text-[7px]"> ()</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> </div>
<div className="flex-1 flex items-center gap-1.5 px-2 py-1.5 rounded-md" <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="bg-bg-elevated border border-stroke rounded-md p-2.5">
<div className="flex items-center gap-1.5 mb-2"> <div className="flex items-center gap-1.5 mb-2">
<span className="text-sm">🐟</span> <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>
<div className="flex flex-col gap-[3px]"> <div className="flex flex-col gap-[3px]">
{sensCategories.length === 0 ? ( {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="bg-bg-elevated border border-stroke rounded-md p-2.5">
<div className="flex items-center gap-1.5 mb-2"> <div className="flex items-center gap-1.5 mb-2">
<span className="text-sm">🛡</span> <span className="text-sm">🛡</span>
<span className="text-xs font-bold text-[#f59e0b]"> <span className="text-xs font-bold text-color-boom">
</span> </span>
{nearbyOrgs.length > 0 && ( {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> </div>
@ -406,7 +406,7 @@ export function IncidentsRightPanel({
{org.areaNm}{org.totalAssets > 0 ? ` · 장비 ${org.totalAssets}` : ''} {org.areaNm}{org.totalAssets > 0 ? ` · 장비 ${org.totalAssets}` : ''}
</div> </div>
</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 {org.distanceNm.toFixed(1)} nm
</span> </span>
</div> </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="mt-2 pt-2" style={{ borderTop: '1px solid rgba(245,158,11,0.1)' }}>
<div className="flex items-center justify-between mb-[5px]"> <div className="flex items-center justify-between mb-[5px]">
<span className="text-[9px] text-fg-disabled"> </span> <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 {nearbyRadius} nm
</span> </span>
</div> </div>

파일 보기

@ -600,7 +600,7 @@ export function IncidentsView() {
padding: '3px 8px', padding: '3px 8px',
background: 'rgba(239,68,68,0.06)', background: 'rgba(239,68,68,0.06)',
border: '1px solid rgba(239,68,68,0.2)', border: '1px solid rgba(239,68,68,0.2)',
color: '#f87171', color: 'var(--color-danger)',
}} }}
> >
@ -648,10 +648,10 @@ export function IncidentsView() {
closeOnClick={false} closeOnClick={false}
> >
<div className="text-center min-w-[180px] text-xs"> <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} {incidentPopup.incident.name}
</div> </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>: {getStatusLabel(incidentPopup.incident.status)}</div>
<div> <div>
: {incidentPopup.incident.date} {incidentPopup.incident.time} : {incidentPopup.incident.date} {incidentPopup.incident.time}
@ -661,7 +661,7 @@ export function IncidentsView() {
<div>: {incidentPopup.incident.causeType}</div> <div>: {incidentPopup.incident.causeType}</div>
)} )}
{incidentPopup.incident.prediction && ( {incidentPopup.incident.prediction && (
<div className="text-[#0891b2]">{incidentPopup.incident.prediction}</div> <div className="text-color-accent">{incidentPopup.incident.prediction}</div>
)} )}
</div> </div>
</div> </div>
@ -676,8 +676,8 @@ export function IncidentsView() {
style={{ style={{
left: hoverInfo.x + 12, left: hoverInfo.x + 12,
top: hoverInfo.y - 12, top: hoverInfo.y - 12,
background: '#161b22', background: 'var(--bg-elevated)',
border: '1px solid #30363d', border: '1px solid var(--stroke-default)',
padding: '8px 12px', padding: '8px 12px',
boxShadow: '0 8px 24px rgba(0,0,0,0.5)', boxShadow: '0 8px 24px rgba(0,0,0,0.5)',
minWidth: 150, minWidth: 150,
@ -752,8 +752,8 @@ export function IncidentsView() {
right: dischargeMode ? 340 : 180, right: dischargeMode ? 340 : 180,
padding: '6px 10px', padding: '6px 10px',
background: dischargeMode ? 'rgba(6,182,212,0.15)' : 'rgba(13,17,23,0.88)', 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', border: dischargeMode ? '1px solid rgba(6,182,212,0.4)' : '1px solid var(--stroke-default)',
color: dischargeMode ? '#22d3ee' : '#8b949e', color: dischargeMode ? '#22d3ee' : 'var(--fg-disabled)',
backdropFilter: 'blur(8px)', backdropFilter: 'blur(8px)',
transition: 'all 0.2s', transition: 'all 0.2s',
}} }}
@ -796,7 +796,7 @@ export function IncidentsView() {
className="absolute top-[10px] right-[10px] z-[500] rounded-md" className="absolute top-[10px] right-[10px] z-[500] rounded-md"
style={{ style={{
background: 'rgba(13,17,23,0.88)', background: 'rgba(13,17,23,0.88)',
border: '1px solid #30363d', border: '1px solid var(--stroke-default)',
padding: '8px 12px', padding: '8px 12px',
backdropFilter: 'blur(8px)', backdropFilter: 'blur(8px)',
}} }}
@ -807,7 +807,7 @@ export function IncidentsView() {
width: 6, width: 6,
height: 6, height: 6,
borderRadius: '50%', borderRadius: '50%',
background: '#22c55e', background: 'var(--color-success)',
animation: 'pd 1.5s infinite', 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" className="absolute bottom-[10px] left-[10px] z-[500] rounded-md flex flex-col gap-1.5"
style={{ style={{
background: 'rgba(13,17,23,0.88)', background: 'rgba(13,17,23,0.88)',
border: '1px solid #30363d', border: '1px solid var(--stroke-default)',
padding: '8px 12px', padding: '8px 12px',
backdropFilter: 'blur(8px)', backdropFilter: 'blur(8px)',
}} }}
@ -1013,7 +1013,7 @@ export function IncidentsView() {
padding: '4px 12px', padding: '4px 12px',
background: 'rgba(59,130,246,0.1)', background: 'rgba(59,130,246,0.1)',
border: '1px solid rgba(59,130,246,0.2)', 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%)', transform: 'translate(-50%,-50%)',
zIndex: 9995, zIndex: 9995,
width: 300, width: 300,
background: '#161b22', background: 'var(--bg-elevated)',
border: '1px solid #30363d', border: '1px solid var(--stroke-default)',
borderRadius: 12, borderRadius: 12,
boxShadow: '0 16px 48px rgba(0,0,0,0.6)', boxShadow: '0 16px 48px rgba(0,0,0,0.6)',
overflow: 'hidden', overflow: 'hidden',
@ -1227,8 +1227,8 @@ function VesselPopupPanel({
alignItems: 'center', alignItems: 'center',
gap: 8, gap: 8,
padding: '10px 14px', padding: '10px 14px',
background: 'linear-gradient(135deg,#1c2333,#161b22)', background: 'var(--bg-elevated)',
borderBottom: '1px solid #30363d', borderBottom: '1px solid var(--stroke-default)',
}} }}
> >
<div className="flex items-center justify-center text-[16px]" style={{ width: 28, height: 20 }}> <div className="flex items-center justify-center text-[16px]" style={{ width: 28, height: 20 }}>
@ -1236,20 +1236,20 @@ function VesselPopupPanel({
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div <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} {v.name}
</div> </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> </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> </span>
</div> </div>
{/* Ship Image */} {/* Ship Image */}
<div <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={{ style={{
height: 120, height: 120,
background: '#0d1117', background: '#0d1117',
@ -1262,7 +1262,7 @@ function VesselPopupPanel({
{/* Tags */} {/* Tags */}
<div className="flex gap-2" style={{ padding: '6px 14px', borderBottom: '1px solid #21262d' }}> <div className="flex gap-2" style={{ padding: '6px 14px', borderBottom: '1px solid #21262d' }}>
<span <span
className="text-[8px] font-bold rounded text-[#58a6ff]" className="text-[8px] font-bold rounded text-color-info"
style={{ style={{
padding: '2px 8px', padding: '2px 8px',
background: 'rgba(59,130,246,0.12)', background: 'rgba(59,130,246,0.12)',
@ -1296,14 +1296,14 @@ function VesselPopupPanel({
}} }}
> >
<div className="flex justify-between"> <div className="flex justify-between">
<span className="text-[10px] text-[#8b949e]"></span> <span className="text-[10px] text-fg-disabled"></span>
<span className="text-[10px] text-[#c9d1d9] font-semibold font-mono"> <span className="text-[10px] text-fg-sub font-semibold font-mono">
{v.depart} {v.depart}
</span> </span>
</div> </div>
<div className="flex justify-between"> <div className="flex justify-between">
<span className="text-[10px] text-[#8b949e]"></span> <span className="text-[10px] text-fg-disabled"></span>
<span className="text-[10px] text-[#c9d1d9] font-semibold font-mono"> <span className="text-[10px] text-fg-sub font-semibold font-mono">
{v.arrive} {v.arrive}
</span> </span>
</div> </div>
@ -1315,7 +1315,7 @@ function VesselPopupPanel({
<div className="flex gap-1.5" style={{ padding: '10px 14px' }}> <div className="flex gap-1.5" style={{ padding: '10px 14px' }}>
<button <button
onClick={onDetail} 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={{ style={{
padding: 6, padding: 6,
background: 'rgba(59,130,246,0.12)', background: 'rgba(59,130,246,0.12)',
@ -1325,7 +1325,7 @@ function VesselPopupPanel({
📋 📋
</button> </button>
<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={{ style={{
padding: 6, padding: 6,
background: 'rgba(168,85,247,0.1)', background: 'rgba(168,85,247,0.1)',
@ -1335,7 +1335,7 @@ function VesselPopupPanel({
🔍 🔍
</button> </button>
<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={{ style={{
padding: 6, padding: 6,
background: 'rgba(6,182,212,0.1)', background: 'rgba(6,182,212,0.1)',
@ -1368,7 +1368,7 @@ function PopupRow({
borderBottom: '1px solid rgba(48,54,61,0.4)', borderBottom: '1px solid rgba(48,54,61,0.4)',
}} }}
> >
<span className="text-[#8b949e]">{label}</span> <span className="text-fg-disabled">{label}</span>
<span <span
className="font-semibold font-mono" className="font-semibold font-mono"
style={{ style={{
@ -1431,13 +1431,13 @@ function VesselDetailModal({ vessel: v, onClose }: { vessel: Vessel; onClose: ()
<div className="flex items-center gap-[10px]"> <div className="flex items-center gap-[10px]">
<span className="text-lg">{v.flag}</span> <span className="text-lg">{v.flag}</span>
<div> <div>
<div className="text-[14px] font-[800] text-[#f0f6fc]">{v.name}</div> <div className="text-[14px] font-[800] text-fg">{v.name}</div>
<div className="text-[10px] text-[#8b949e] font-mono"> <div className="text-[10px] text-fg-disabled font-mono">
MMSI: {v.mmsi} · IMO: {v.imo} MMSI: {v.mmsi} · IMO: {v.imo}
</div> </div>
</div> </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> </span>
</div> </div>
@ -1509,7 +1509,7 @@ function Sec({
return ( return (
<div style={{ border: `1px solid ${borderColor || '#21262d'}`, borderRadius: 8, overflow: 'hidden' }}> <div style={{ border: `1px solid ${borderColor || '#21262d'}`, borderRadius: 8, overflow: 'hidden' }}>
<div <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={{ style={{
padding: '8px 12px', padding: '8px 12px',
background: bgColor || '#0d1117', background: bgColor || '#0d1117',
@ -1548,7 +1548,7 @@ function Cell({
gridColumn: span ? '1 / -1' : undefined, 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' }}> <div className="text-[11px] font-semibold font-mono" style={{ color: color || '#f0f6fc' }}>
{value} {value}
</div> </div>
@ -1578,7 +1578,7 @@ function TabInfo({ v }: { v: Vessel }) {
return ( return (
<> <>
<div <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={{ style={{
height: 160, height: 160,
background: '#0d1117', background: '#0d1117',
@ -1657,7 +1657,7 @@ function TabNav(_props: { v: Vessel }) {
</Sec> </Sec>
<Sec title="📊 속도 이력"> <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 }}> <div className="flex items-end gap-1.5" style={{ height: 80 }}>
{hours.map((h, i) => ( {hours.map((h, i) => (
<div <div
@ -1672,13 +1672,13 @@ function TabNav(_props: { v: Vessel }) {
height: `${heights[i]}%`, height: `${heights[i]}%`,
}} }}
/> />
<span className="text-[7px] text-[#8b949e]">{h}</span> <span className="text-[7px] text-fg-disabled">{h}</span>
</div> </div>
))} ))}
</div> </div>
<div className="text-center text-[8px] text-[#8b949e]" style={{ marginTop: 6 }}> <div className="text-center text-[8px] text-fg-disabled" style={{ marginTop: 6 }}>
: <b className="text-[#58a6ff]">8.4 kn</b> · :{' '} : <b className="text-color-info">8.4 kn</b> · :{' '}
<b className="text-[#22d3ee]">11.2 kn</b> <b className="text-color-accent">11.2 kn</b>
</div> </div>
</div> </div>
</Sec> </Sec>
@ -1718,7 +1718,7 @@ function TabSpec({ v }: { v: Vessel }) {
</Sec> </Sec>
<Sec title="⚠ 위험물 적재 정보"> <Sec title="⚠ 위험물 적재 정보">
<div className="p-[10px_12px] bg-[#0d1117]"> <div className="p-[10px_12px] bg-bg-base">
<div <div
className="flex items-center gap-2 rounded" className="flex items-center gap-2 rounded"
style={{ style={{
@ -1729,14 +1729,14 @@ function TabSpec({ v }: { v: Vessel }) {
> >
<span className="text-[12px]">🛢</span> <span className="text-[12px]">🛢</span>
<div className="flex-1"> <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()} {v.cargo.split('·')[0].trim()}
</div> </div>
<div className="text-[8px] text-[#8b949e]">{v.cargo}</div> <div className="text-[8px] text-fg-disabled">{v.cargo}</div>
</div> </div>
{v.cargo.includes('IMO') && ( {v.cargo.includes('IMO') && (
<span <span
className="text-[8px] font-bold text-[#f87171]" className="text-[8px] font-bold text-color-danger"
style={{ style={{
padding: '2px 6px', padding: '2px 6px',
background: 'rgba(239,68,68,0.15)', background: 'rgba(239,68,68,0.15)',
@ -1811,7 +1811,7 @@ function TabInsurance(_props: { v: Vessel }) {
</Sec> </Sec>
<div <div
className="rounded-sm text-[9px] text-[#8b949e]" className="rounded-sm text-[9px] text-fg-disabled"
style={{ style={{
padding: '8px 10px', padding: '8px 10px',
background: 'rgba(59,130,246,0.04)', background: 'rgba(59,130,246,0.04)',
@ -1834,7 +1834,7 @@ function TabDangerous({ v }: { v: Vessel }) {
bgColor="rgba(249,115,22,0.06)" bgColor="rgba(249,115,22,0.06)"
badge={ badge={
<span <span
className="text-[8px] font-bold text-[#ef4444]" className="text-[8px] font-bold text-color-danger"
style={{ style={{
padding: '2px 6px', padding: '2px 6px',
background: 'rgba(239,68,68,0.15)', background: 'rgba(239,68,68,0.15)',
@ -1861,16 +1861,16 @@ function TabDangerous({ v }: { v: Vessel }) {
<Sec title="📋 화물창 및 첨부"> <Sec title="📋 화물창 및 첨부">
<div <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' }} style={{ padding: '12px' }}
> >
<div <div
className="flex items-center gap-2 text-[11px] whitespace-nowrap" 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="inline-flex items-center gap-1">
<span <span
className="flex items-center justify-center text-[8px] text-[#22d3ee]" className="flex items-center justify-center text-[8px] text-color-accent"
style={{ style={{
width: 14, width: 14,
height: 14, height: 14,
@ -1880,11 +1880,11 @@ function TabDangerous({ v }: { v: Vessel }) {
> >
</span> </span>
<span className="font-semibold text-[10px] text-[#22d3ee]"></span> <span className="font-semibold text-[10px] text-color-accent"></span>
</span> </span>
</div> </div>
<button <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={{ style={{
padding: '6px 14px', padding: '6px 14px',
background: 'rgba(59,130,246,0.1)', 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)"> <Sec title="🚨 비상 대응 요약 (EmS)" bgColor="rgba(234,179,8,0.06)">
<div <div
className="flex flex-col gap-1.5 bg-[#0d1117]" className="flex flex-col gap-1.5 bg-bg-base"
style={{ padding: '10px 12px' }} style={{ padding: '10px 12px' }}
> >
<EmsRow <EmsRow
@ -1937,7 +1937,7 @@ function TabDangerous({ v }: { v: Vessel }) {
</Sec> </Sec>
<div <div
className="rounded-sm text-[9px] text-[#8b949e]" className="rounded-sm text-[9px] text-fg-disabled"
style={{ style={{
padding: '8px 10px', padding: '8px 10px',
background: 'rgba(249,115,22,0.04)', background: 'rgba(249,115,22,0.04)',
@ -1976,8 +1976,8 @@ function EmsRow({
> >
<span className="text-[13px]">{icon}</span> <span className="text-[13px]">{icon}</span>
<div> <div>
<div className="text-[9px] text-[#8b949e]">{label}</div> <div className="text-[9px] text-fg-disabled">{label}</div>
<div className="text-[10px] font-semibold text-[#f0f6fc]">{value}</div> <div className="text-[10px] font-semibold text-fg">{value}</div>
</div> </div>
</div> </div>
) )
@ -2017,13 +2017,13 @@ function ActionBtn({
function VesselTooltipContent({ vessel: v }: { vessel: Vessel }) { function VesselTooltipContent({ vessel: v }: { vessel: Vessel }) {
return ( return (
<> <>
<div className="text-[11px] font-bold text-[#f0f6fc]" style={{ marginBottom: 3 }}>{v.name}</div> <div className="text-[11px] font-bold text-fg" style={{ marginBottom: 3 }}>{v.name}</div>
<div className="text-[9px] text-[#8b949e]" style={{ marginBottom: 4 }}> <div className="text-[9px] text-fg-disabled" style={{ marginBottom: 4 }}>
{v.typS} · {v.flag} {v.typS} · {v.flag}
</div> </div>
<div className="flex justify-between text-[9px]"> <div className="flex justify-between text-[9px]">
<span className="text-[#22d3ee] font-semibold">{v.speed} kn</span> <span className="text-color-accent font-semibold">{v.speed} kn</span>
<span className="text-[#8b949e]">HDG {v.heading}°</span> <span className="text-fg-disabled">HDG {v.heading}°</span>
</div> </div>
</> </>
) )
@ -2035,8 +2035,8 @@ function IncidentTooltipContent({ incident: i }: { incident: IncidentCompat }) {
return ( return (
<> <>
<div className="text-[11px] font-bold text-[#f0f6fc]" style={{ marginBottom: 3 }}>{i.name}</div> <div className="text-[11px] font-bold text-fg" style={{ marginBottom: 3 }}>{i.name}</div>
<div className="text-[9px] text-[#8b949e]" style={{ marginBottom: 4 }}> <div className="text-[9px] text-fg-disabled" style={{ marginBottom: 4 }}>
{i.date} {i.time} {i.date} {i.time}
</div> </div>
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
@ -2046,7 +2046,7 @@ function IncidentTooltipContent({ incident: i }: { incident: IncidentCompat }) {
> >
{getStatusLabel(i.status)} {getStatusLabel(i.status)}
</span> </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 {i.location.lat.toFixed(3)}°N, {i.location.lon.toFixed(3)}°E
</span> </span>
</div> </div>

파일 보기

@ -58,12 +58,12 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
style={{ background: 'rgba(0,0,0,0.7)', backdropFilter: 'blur(6px)' }} style={{ background: 'rgba(0,0,0,0.7)', backdropFilter: 'blur(6px)' }}
> >
<div <div
className="text-center text-[12px] text-[#8b949e]" className="text-center text-[12px] text-fg-disabled"
style={{ style={{
width: 300, width: 300,
padding: 40, padding: 40,
background: '#0d1117', background: 'var(--bg-base)',
border: '1px solid #30363d', border: '1px solid var(--stroke-default)',
borderRadius: 14, borderRadius: 14,
}} }}
> >
@ -92,8 +92,8 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
width: '95vw', width: '95vw',
height: '92vh', height: '92vh',
maxWidth: 1600, maxWidth: 1600,
background: '#0d1117', background: 'var(--bg-base)',
border: '1px solid #30363d', border: '1px solid var(--stroke-default)',
borderRadius: 14, borderRadius: 14,
boxShadow: '0 24px 64px rgba(0,0,0,0.7)', 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" className="shrink-0 flex items-center justify-between"
style={{ style={{
padding: '12px 20px', padding: '12px 20px',
background: 'linear-gradient(135deg,#161b22,#0d1117)', background: 'linear-gradient(135deg,var(--bg-elevated),var(--bg-base))',
borderBottom: '1px solid #30363d', borderBottom: '1px solid var(--stroke-default)',
}} }}
> >
<div className="flex items-center gap-[10px]"> <div className="flex items-center gap-[10px]">
<span className="text-lg">📋</span> <span className="text-lg">📋</span>
<div> <div>
<div className="text-[14px] font-[800] text-[#f0f6fc]"> <div className="text-[14px] font-[800] text-fg">
{incident.name} {incident.name}
</div> </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} {incident.name} · {incident.date} · {media.photoCnt} / {media.videoCnt} / {media.satCnt} / CCTV {media.cctvCnt}
</div> </div>
</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, padding: '5px 12px', borderRadius: 6, fontSize: 11, fontWeight: activeTab === t.id ? 700 : 400,
cursor: 'pointer', border: 'none', cursor: 'pointer', border: 'none',
background: activeTab === t.id ? 'rgba(168,85,247,0.15)' : 'transparent', 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} {t.icon ? `${t.icon} ${t.label}` : t.label}
</button> </button>
@ -142,7 +142,7 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
{/* Close */} {/* Close */}
<span <span
onClick={onClose} onClick={onClose}
className="text-[18px] cursor-pointer text-[#8b949e] rounded" className="text-[18px] cursor-pointer text-fg-disabled rounded"
style={{ padding: '2px 6px' }} style={{ padding: '2px 6px' }}
></span> ></span>
</div> </div>
@ -151,11 +151,11 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
{/* ── Timeline ────────────────────────────────── */} {/* ── Timeline ────────────────────────────────── */}
<div <div
className="shrink-0 flex items-center gap-[10px]" 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 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) => ( {timelineDots.map((d, i) => (
<div key={i} style={{ <div key={i} style={{
position: 'absolute', left: `${d.pct}%`, top: 3, position: 'absolute', left: `${d.pct}%`, top: 3,
@ -164,10 +164,10 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
}} /> }} />
))} ))}
</div> </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: '#ef4444' }}> </span>
<span style={{ color: '#f59e0b' }}> </span> <span style={{ color: '#f59e0b' }}> </span>
<span className="text-[#8b949e]"> </span> <span className="text-fg-disabled"> </span>
</div> </div>
</div> </div>
@ -178,20 +178,20 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
gridTemplateColumns: (showPhoto || showSat) && (showVideo || showCctv) ? '1fr 1fr' : '1fr', gridTemplateColumns: (showPhoto || showSat) && (showVideo || showCctv) ? '1fr 1fr' : '1fr',
gridTemplateRows: (showPhoto || showVideo) && (showSat || showCctv) ? '1fr 1fr' : '1fr', gridTemplateRows: (showPhoto || showVideo) && (showSat || showCctv) ? '1fr 1fr' : '1fr',
gap: 1, gap: 1,
background: '#21262d', background: 'var(--stroke-light)',
}} }}
> >
{/* ── Q1: 현장사진 ──────────────────────────── */} {/* ── Q1: 현장사진 ──────────────────────────── */}
{showPhoto && ( {showPhoto && (
<div className="flex flex-col overflow-hidden bg-[#0d1117]"> <div className="flex flex-col overflow-hidden bg-bg-base">
{/* Section header */} {/* Section header */}
<div <div
className="shrink-0 flex items-center justify-between" 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]"> <div className="flex items-center gap-[6px]">
<span className="text-[12px]">📷</span> <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', '현장 사진')} {str(media.photoMeta, 'title', '현장 사진')}
</span> </span>
</div> </div>
@ -201,30 +201,30 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
</div> </div>
{/* Photo content */} {/* Photo content */}
<div className="flex-1 flex items-center justify-center flex-col gap-2"> <div className="flex-1 flex items-center justify-center flex-col gap-2">
<div className="text-[48px] text-[#30363d]">📷</div> <div className="text-[48px]" style={{ color: 'var(--stroke-default)' }}>📷</div>
<div className="text-[12px] text-[#c9d1d9] font-semibold"> <div className="text-[12px] text-fg-sub font-semibold">
{incident.name.replace('유류유출', '유출 현장').replace('오염', '현장')} {incident.name.replace('유류유출', '유출 현장').replace('오염', '현장')}
</div> </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')} {str(media.photoMeta, 'date')} · {str(media.photoMeta, 'by')}
</div> </div>
</div> </div>
{/* Thumbnails */} {/* 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 }}> <div className="flex gap-1.5" style={{ marginBottom: 6 }}>
{Array.from({ length: Math.min(num(media.photoMeta, 'thumbCount'), 7) }).map((_, i) => ( {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={{ <div key={i} className="flex items-center justify-center text-[14px] cursor-pointer" style={{
width: 40, height: 36, borderRadius: 4, width: 40, height: 36, borderRadius: 4, color: 'var(--stroke-default)',
background: i === 0 ? 'rgba(168,85,247,0.15)' : '#161b22', 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 #30363d', border: i === 0 ? '2px solid rgba(168,85,247,0.5)' : '1px solid var(--stroke-default)',
}}>📷</div> }}>📷</div>
))} ))}
</div> </div>
<div className="flex justify-between items-center"> <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')} 📷 {num(media.photoMeta, 'thumbCount')} · {str(media.photoMeta, 'stage')}
</span> </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> </div>
</div> </div>
@ -232,19 +232,19 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
{/* ── Q2: 드론 영상 ─────────────────────────── */} {/* ── Q2: 드론 영상 ─────────────────────────── */}
{showVideo && ( {showVideo && (
<div className="flex flex-col overflow-hidden bg-[#0d1117]"> <div className="flex flex-col overflow-hidden bg-bg-base">
<div <div
className="shrink-0 flex items-center justify-between" 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]"> <div className="flex items-center gap-[6px]">
<span className="text-[12px]">🎬</span> <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', '드론 영상')} {str(media.droneMeta, 'title', '드론 영상')}
</span> </span>
</div> </div>
<span <span
className="text-[9px] font-bold text-[#ef4444] rounded" className="text-[9px] font-bold text-color-danger rounded"
style={{ style={{
padding: '2px 8px', padding: '2px 8px',
background: 'rgba(239,68,68,0.15)', background: 'rgba(239,68,68,0.15)',
@ -252,39 +252,39 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
> REC</span> > REC</span>
</div> </div>
<div className="flex-1 flex items-center justify-center flex-col gap-2"> <div className="flex-1 flex items-center justify-center flex-col gap-2">
<div className="text-[48px] text-[#30363d]">🎬</div> <div className="text-[48px]" style={{ color: 'var(--stroke-default)' }}>🎬</div>
<div className="text-[12px] text-[#c9d1d9] font-semibold"> <div className="text-[12px] text-fg-sub font-semibold">
</div> </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')} {str(media.droneMeta, 'device')} · {str(media.droneMeta, 'alt')}
</div> </div>
</div> </div>
{/* Video controls */} {/* Video controls */}
<div <div
className="shrink-0 flex flex-col gap-2" 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"> <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 <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={{ style={{
width: 28, height: 28, borderRadius: '50%', width: 28, height: 28, borderRadius: '50%',
background: 'rgba(168,85,247,0.15)', background: 'rgba(168,85,247,0.15)',
border: '1px solid rgba(168,85,247,0.3)', border: '1px solid rgba(168,85,247,0.3)',
}} }}
></div> ></div>
<span className="text-[12px] text-[#8b949e] cursor-pointer"></span> <span className="text-[12px] text-fg-disabled cursor-pointer"></span>
<span className="text-[10px] text-[#8b949e] font-mono">02:34 / {str(media.droneMeta, 'duration')}</span> <span className="text-[10px] text-fg-disabled font-mono">02:34 / {str(media.droneMeta, 'duration')}</span>
</div> </div>
<div className="flex justify-between items-center"> <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')} 🎬 {num(media.droneMeta, 'videoCount')} · {str(media.droneMeta, 'stage')}
</span> </span>
<div className="flex gap-[8px]"> <div className="flex gap-[8px]">
<span className="text-[8px] text-[#58a6ff] cursor-pointer">📂 </span> <span className="text-[8px] text-color-info cursor-pointer">📂 </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> </div>
</div> </div>
@ -293,14 +293,14 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
{/* ── Q3: 위성영상 ──────────────────────────── */} {/* ── Q3: 위성영상 ──────────────────────────── */}
{showSat && ( {showSat && (
<div className="flex flex-col overflow-hidden bg-[#0d1117]"> <div className="flex flex-col overflow-hidden bg-bg-base">
<div <div
className="shrink-0 flex items-center justify-between" className="shrink-0 flex items-center justify-between"
style={{ padding: '8px 16px', borderBottom: '1px solid #21262d' }} style={{ padding: '8px 16px', borderBottom: '1px solid #21262d' }}
> >
<div className="flex items-center gap-[6px]"> <div className="flex items-center gap-[6px]">
<span className="text-[12px]">🛰</span> <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', '위성영상')} {str(media.satMeta, 'title', '위성영상')}
</span> </span>
</div> </div>
@ -316,24 +316,24 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 6, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 6,
}}> }}>
<div <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' }} style={{ top: -10, left: 8, padding: '0 4px' }}
> >
{str(media.satMeta, 'detection')} {str(media.satMeta, 'detection')}
</div> </div>
<div className="text-[40px] text-[#30363d]">🛰</div> <div className="text-[40px] text-fg-disabled">🛰</div>
<div className="text-[11px] text-[#c9d1d9] font-semibold"> <div className="text-[11px] text-fg-sub font-semibold">
{str(media.satMeta, 'title', '위성영상')} {str(media.satMeta, 'title', '위성영상')}
</div> </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')} {str(media.satMeta, 'date')} · {str(media.satMeta, 'resolution')}
</div> </div>
</div> </div>
)} )}
{str(media.satMeta, 'detection') === '—' && ( {str(media.satMeta, 'detection') === '—' && (
<div className="text-center"> <div className="text-center">
<div className="text-[40px] text-[#30363d]">🛰</div> <div className="text-[40px] text-fg-disabled">🛰</div>
<div className="text-[11px] text-[#8b949e]" style={{ marginTop: 8 }}> </div> <div className="text-[11px] text-fg-disabled" style={{ marginTop: 8 }}> </div>
</div> </div>
)} )}
</div> </div>
@ -341,7 +341,7 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
{num(media.satMeta, 'thumbCount') > 0 && ( {num(media.satMeta, 'thumbCount') > 0 && (
<div className="flex gap-1.5" style={{ marginBottom: 6 }}> <div className="flex gap-1.5" style={{ marginBottom: 6 }}>
{Array.from({ length: num(media.satMeta, 'thumbCount') }).map((_, i) => ( {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, width: 40, height: 36, borderRadius: 4,
background: i === 0 ? 'rgba(168,85,247,0.15)' : '#161b22', background: i === 0 ? 'rgba(168,85,247,0.15)' : '#161b22',
border: i === 0 ? '2px solid rgba(168,85,247,0.5)' : '1px solid #30363d', 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>
)} )}
<div className="flex justify-between items-center"> <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')} 🛰 {num(media.satMeta, 'thumbCount')} · {str(media.satMeta, 'sensor')}
</span> </span>
<span className="text-[8px] text-[#58a6ff] cursor-pointer">🔍 / </span> <span className="text-[8px] text-color-info cursor-pointer">🔍 / </span>
</div> </div>
</div> </div>
</div> </div>
@ -361,21 +361,21 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
{/* ── Q4: CCTV ──────────────────────────────── */} {/* ── Q4: CCTV ──────────────────────────────── */}
{showCctv && ( {showCctv && (
<div className="flex flex-col overflow-hidden bg-[#0d1117]"> <div className="flex flex-col overflow-hidden bg-bg-base">
<div <div
className="shrink-0 flex items-center justify-between" className="shrink-0 flex items-center justify-between"
style={{ padding: '8px 16px', borderBottom: '1px solid #21262d' }} style={{ padding: '8px 16px', borderBottom: '1px solid #21262d' }}
> >
<div className="flex items-center gap-[6px]"> <div className="flex items-center gap-[6px]">
<span className="text-[12px]">📹</span> <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')} CCTV {str(media.cctvMeta, 'title', 'CCTV')}
</span> </span>
</div> </div>
<div className="flex items-center gap-[6px]"> <div className="flex items-center gap-[6px]">
{bool(media.cctvMeta, 'live') && ( {bool(media.cctvMeta, 'live') && (
<span <span
className="text-[9px] font-bold text-[#22c55e] rounded" className="text-[9px] font-bold text-color-success rounded"
style={{ style={{
padding: '2px 8px', padding: '2px 8px',
background: 'rgba(34,197,94,0.15)', background: 'rgba(34,197,94,0.15)',
@ -387,15 +387,15 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
</div> </div>
<div className="flex-1 flex items-center justify-center flex-col gap-2 relative"> <div className="flex-1 flex items-center justify-center flex-col gap-2 relative">
{bool(media.cctvMeta, 'live') && ( {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 })} LIVE {new Date().toLocaleTimeString('ko-KR', { hour12: false })}
</div> </div>
)} )}
<div className="text-[48px] text-[#30363d]">📹</div> <div className="text-[48px] text-fg-disabled">📹</div>
<div className="text-[12px] text-[#c9d1d9] font-semibold"> <div className="text-[12px] text-fg-sub font-semibold">
{str(media.cctvMeta, 'title', 'CCTV').replace('#', 'CCTV #')} {str(media.cctvMeta, 'title', 'CCTV').replace('#', 'CCTV #')}
</div> </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') ? '실시간 스트리밍' : '녹화 영상'} {str(media.cctvMeta, 'ptz')} · {str(media.cctvMeta, 'angle')} · {bool(media.cctvMeta, 'live') ? '실시간 스트리밍' : '녹화 영상'}
</div> </div>
</div> </div>
@ -416,12 +416,12 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
))} ))}
</div> </div>
<div className="flex justify-between items-center"> <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')} 📹 CCTV {num(media.cctvMeta, 'camCount')} · {str(media.cctvMeta, 'location')}
</span> </span>
<div className="flex gap-[8px]"> <div className="flex gap-[8px]">
<span className="text-[8px] text-[#ef4444] cursor-pointer">🔴 </span> <span className="text-[8px] text-color-danger cursor-pointer">🔴 </span>
<span className="text-[8px] text-[#58a6ff] cursor-pointer">🎥 PTZ</span> <span className="text-[8px] text-color-info cursor-pointer">🎥 PTZ</span>
</div> </div>
</div> </div>
</div> </div>
@ -438,12 +438,12 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
borderTop: '1px solid #30363d', borderTop: '1px solid #30363d',
}} }}
> >
<div className="flex gap-4 text-[10px] font-mono text-[#8b949e]"> <div className="flex gap-4 text-[10px] font-mono text-fg-disabled">
<span>📷 <b className="text-[#f0f6fc]">{media.photoCnt}</b></span> <span>📷 <b className="text-fg">{media.photoCnt}</b></span>
<span>🎬 <b className="text-[#f0f6fc]">{media.videoCnt}</b></span> <span>🎬 <b className="text-fg">{media.videoCnt}</b></span>
<span>🛰 <b className="text-[#f0f6fc]">{media.satCnt}</b></span> <span>🛰 <b className="text-fg">{media.satCnt}</b></span>
<span>📹 CCTV <b className="text-[#f0f6fc]">{media.cctvCnt}</b></span> <span>📹 CCTV <b className="text-fg">{media.cctvCnt}</b></span>
<span>📎 <b className="text-[#c084fc]">{total}</b></span> <span>📎 <b className="text-color-tertiary">{total}</b></span>
</div> </div>
<div className="flex gap-[8px]"> <div className="flex gap-[8px]">
<BottomBtn icon="📥" label="다운로드" bg="rgba(100,116,139,0.1)" bd="rgba(100,116,139,0.2)" fg="#8b949e" /> <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 }) { function NavBtn({ label }: { label: string }) {
return ( return (
<button <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={{ style={{
width: 22, height: 22, width: 22, height: 22,
border: '1px solid #30363d', border: '1px solid #30363d',

파일 보기

@ -243,7 +243,7 @@ export function LeftPanel({
</div> </div>
) : ( ) : (
<div className="px-4 pb-4"> <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> </div>
) )
)} )}
@ -266,7 +266,7 @@ export function LeftPanel({
{expandedSections.impactResources && ( {expandedSections.impactResources && (
<div className="px-4 pb-4"> <div className="px-4 pb-4">
{sensitiveResources.length === 0 ? ( {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"> <div className="space-y-1.5">
{sensitiveResources.map(({ category, count, totalArea }) => { {sensitiveResources.map(({ category, count, totalArea }) => {

파일 보기

@ -232,9 +232,9 @@ const OilBoomSection = ({
<div key={setting.key} style={{ <div key={setting.key} style={{
background: 'var(--bg-base)', background: 'var(--bg-base)',
borderRadius: 'var(--radius-sm)', borderRadius: 'var(--radius-sm)',
}} className="flex items-center justify-between p-[6px_8px] border border-stroke"> }} className="flex items-center justify-between px-2.5 py-1.5 border border-stroke">
<span className="text-[9px] text-fg-disabled"> {setting.label}</span> <span className="flex-1 text-[9px] text-fg-disabled truncate"> {setting.label}</span>
<div className="flex items-center gap-[2px]"> <div className="flex items-center gap-1 shrink-0 w-[80px] justify-end">
<input <input
type="number" type="number"
value={setting.value} value={setting.value}
@ -245,7 +245,7 @@ const OilBoomSection = ({
className="boom-setting-input" className="boom-setting-input"
step={setting.key === 'waveHeightCorrectionFactor' ? 0.1 : 1} 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>
</div> </div>
))} ))}

파일 보기

@ -423,18 +423,18 @@ function NewScenarioModal({ ops, onClose }: { ops: RescueOpsItem[]; onClose: ()
<div className={sectionTitleCls}>{sectionIcon(1)} </div> <div className={sectionTitleCls}>{sectionIcon(1)} </div>
<div className="grid grid-cols-2 gap-2.5"> <div className="grid grid-cols-2 gap-2.5">
<div> <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} /> <input type="text" placeholder="예: T+3h 기관실 침수 확대" className={inputCls} />
</div> </div>
<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}> <select defaultValue="0" className={inputCls}>
{ops.map((op, i) => <option key={op.rescueOpsSn} value={i}>{op.opsCd} · {op.vesselNm}</option>)} {ops.map((op, i) => <option key={op.rescueOpsSn} value={i}>{op.opsCd} · {op.vesselNm}</option>)}
<option value="new">+ ...</option> <option value="new">+ ...</option>
</select> </select>
</div> </div>
<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}> <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>)} {['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> </select>
@ -451,11 +451,11 @@ function NewScenarioModal({ ops, onClose }: { ops: RescueOpsItem[]; onClose: ()
<div className={sectionTitleCls}>{sectionIcon(2)} </div> <div className={sectionTitleCls}>{sectionIcon(2)} </div>
<div className="grid grid-cols-3 gap-2.5"> <div className="grid grid-cols-3 gap-2.5">
<div> <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} /> <input type="text" defaultValue="M/V SEA GUARDIAN" className={inputCls} />
</div> </div>
<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}> <select defaultValue="화물선 (Cargo)" className={inputCls}>
{['유조선 (Tanker)', '화물선 (Cargo)', '컨테이너선 (Container)', '여객선 (Passenger)', '어선 (Fishing)', 'LNG선', '케미컬선 (Chemical)', '기타'].map(v => <option key={v}>{v}</option>)} {['유조선 (Tanker)', '화물선 (Cargo)', '컨테이너선 (Container)', '여객선 (Passenger)', '어선 (Fishing)', 'LNG선', '케미컬선 (Chemical)', '기타'].map(v => <option key={v}>{v}</option>)}
</select> </select>
@ -494,7 +494,7 @@ function NewScenarioModal({ ops, onClose }: { ops: RescueOpsItem[]; onClose: ()
<div className={sectionTitleCls}>{sectionIcon(3)} · </div> <div className={sectionTitleCls}>{sectionIcon(3)} · </div>
<div className="grid grid-cols-3 gap-2.5"> <div className="grid grid-cols-3 gap-2.5">
<div> <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}> <select defaultValue="충돌 (Collision)" className={inputCls}>
{['충돌 (Collision)', '좌초 (Grounding)', '침수 (Flooding)', '기관고장 (Engine Failure)', '화재 (Fire)', '전복 (Capsizing)', '구조손상 (Structural)'].map(v => <option key={v}>{v}</option>)} {['충돌 (Collision)', '좌초 (Grounding)', '침수 (Flooding)', '기관고장 (Engine Failure)', '화재 (Fire)', '전복 (Capsizing)', '구조손상 (Structural)'].map(v => <option key={v}>{v}</option>)}
</select> </select>
@ -705,7 +705,7 @@ function ScenarioComparison({ chartData }: { chartData: ChartDataItem[] }) {
{/* Grid */} {/* Grid */}
{[0, 0.5, 1.0, 1.5, 2.0].map(v => { {[0, 0.5, 1.0, 1.5, 2.0].map(v => {
const y = PY + ph - (v / 2.0) * ph 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 */} {/* 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" /> <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 }}> <svg viewBox={`0 0 ${W} ${H}`} width="100%" style={{ maxHeight: 160 }}>
{[0, 5, 10, 15, 20, 25].map(v => { {[0, 5, 10, 15, 20, 25].map(v => {
const y = PY + ph - (v / 25) * ph 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" /> <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} /> <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 }}> <svg viewBox={`0 0 ${W} ${H}`} width="100%" style={{ maxHeight: 160 }}>
{[0, 50, 100, 150, 200].map(v => { {[0, 50, 100, 150, 200].map(v => {
const y = PY + ph - (v / 200) * ph 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) => { {chartData.map((d, i) => {
const barW = xStep * 0.5 const barW = xStep * 0.5

파일 보기

@ -131,7 +131,7 @@ function LeftPanel({
</div> </div>
{/* CCTV 피드 */} {/* 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="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="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> <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)! const at = accidentTypes.find(t => t.id === activeType)!
return ( 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={{ <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)' 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="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' }}> <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> <span></span><b className="text-fg">37°28'N, 126°15'E</b>

파일 보기

@ -8,32 +8,32 @@ export default {
extend: { extend: {
colors: { colors: {
bg: { bg: {
base: '#0a0e1a', base: 'var(--bg-base)',
surface: '#0f1524', surface: 'var(--bg-surface)',
elevated: '#121929', elevated: 'var(--bg-elevated)',
card: '#1a2236', card: 'var(--bg-card)',
'surface-hover': '#1e2844', 'surface-hover': 'var(--bg-surface-hover)',
}, },
stroke: { stroke: {
DEFAULT: '#1e2a42', DEFAULT: 'var(--stroke-default)',
light: '#2a3a5c', light: 'var(--stroke-light)',
}, },
fg: { fg: {
DEFAULT: '#edf0f7', DEFAULT: 'var(--fg-default)',
sub: '#c0c8dc', sub: 'var(--fg-sub)',
disabled: '#9ba3b8', disabled: 'var(--fg-disabled)',
}, },
color: { color: {
accent: '#06b6d4', accent: 'var(--color-accent)',
info: '#3b82f6', info: 'var(--color-info)',
tertiary: '#a855f7', tertiary: 'var(--color-tertiary)',
danger: '#ef4444', danger: 'var(--color-danger)',
warning: '#f97316', warning: 'var(--color-warning)',
caution: '#eab308', caution: 'var(--color-caution)',
success: '#22c55e', success: 'var(--color-success)',
boom: { boom: {
DEFAULT: '#f59e0b', DEFAULT: 'var(--color-boom)',
hover: '#fbbf24', hover: 'var(--color-boom-hover)',
}, },
}, },
}, },