diff --git a/frontend/index.html b/frontend/index.html index 92f98e0..b97769e 100755 --- a/frontend/index.html +++ b/frontend/index.html @@ -8,6 +8,12 @@ frontend +
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index b883083..2a1a3ba 100755 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -61,9 +61,9 @@ function App() {
- WING + WING
+
{/* Background image */}
diff --git a/frontend/src/common/components/layout/TopBar.tsx b/frontend/src/common/components/layout/TopBar.tsx index 2d99d0e..07e556f 100755 --- a/frontend/src/common/components/layout/TopBar.tsx +++ b/frontend/src/common/components/layout/TopBar.tsx @@ -3,6 +3,7 @@ import type { MainTab } from '../../types/navigation' import { useAuthStore } from '../../store/authStore' import { useMenuStore } from '../../store/menuStore' import { useMapStore } from '../../store/mapStore' +import { useThemeStore } from '../../store/themeStore' import UserManualPopup from '../ui/UserManualPopup' interface TopBarProps { @@ -17,6 +18,7 @@ export function TopBar({ activeTab, onTabChange }: TopBarProps) { const { hasPermission, user, logout } = useAuthStore() const { menuConfig, isLoaded } = useMenuStore() const { mapToggles, toggleMap, mapTypes, measureMode, setMeasureMode } = useMapStore() + const { theme, toggleTheme } = useThemeStore() const MAP_TABS = new Set(['prediction', 'hns', 'scat', 'incidents']) const isMapTab = MAP_TABS.has(activeTab) @@ -53,7 +55,7 @@ export function TopBar({ activeTab, onTabChange }: TopBarProps) { className="flex items-center hover:opacity-80 transition-opacity cursor-pointer" title="홈으로 이동" > - WING 해양환경 위기대응 + WING 해양환경 위기대응 {/* Divider */} @@ -83,21 +85,21 @@ export function TopBar({ activeTab, onTabChange }: TopBarProps) { ${isMonitor ? 'border-l border-l-[rgba(239,68,68,0.25)] ml-1 flex items-center gap-1.5' : ''} ${ isMonitor - ? 'text-[#f87171] hover:text-[#fca5a5] hover:bg-[rgba(239,68,68,0.1)]' + ? 'text-color-danger hover:text-[#fca5a5] hover:bg-[rgba(239,68,68,0.1)]' : activeTab === tab.id ? isIncident ? 'text-[#a5b4fc] bg-[rgba(99,102,241,0.18)] shadow-[0_0_8px_rgba(99,102,241,0.3)]' - : 'text-[#22d3ee] bg-[rgba(6,182,212,0.15)] shadow-[0_0_8px_rgba(6,182,212,0.3)]' + : 'text-color-accent bg-[rgba(6,182,212,0.15)] shadow-[0_0_8px_rgba(6,182,212,0.3)]' : isIncident ? 'text-[#818cf8] hover:text-[#a5b4fc] hover:bg-[rgba(99,102,241,0.1)]' - : 'text-[#c8d6e5] hover:text-white hover:bg-[rgba(255,255,255,0.08)]' + : 'text-fg-sub hover:text-fg hover:bg-[var(--hover-overlay)]' } `} > {isMonitor ? ( <> - + {tab.label} {tab.icon} @@ -165,7 +167,7 @@ export function TopBar({ activeTab, onTabChange }: TopBarProps) { {showQuickMenu && ( -
+
{/* 거리·면적 계산 */} {/*
📐 거리·면적 계산 @@ -178,7 +180,7 @@ export function TopBar({ activeTab, onTabChange }: TopBarProps) { ? 'text-fg-disabled opacity-40 cursor-not-allowed' : measureMode === 'distance' ? 'text-color-accent bg-[rgba(6,182,212,0.1)]' - : 'text-fg-sub hover:bg-[rgba(255,255,255,0.06)] hover:text-fg' + : 'text-fg hover:bg-[var(--hover-overlay)]' }`} > 거리 재기 @@ -192,7 +194,7 @@ export function TopBar({ activeTab, onTabChange }: TopBarProps) { ? 'text-fg-disabled opacity-40 cursor-not-allowed' : measureMode === 'area' ? 'text-color-accent bg-[rgba(6,182,212,0.1)]' - : 'text-fg-sub hover:bg-[rgba(255,255,255,0.06)] hover:text-fg' + : 'text-fg hover:bg-[var(--hover-overlay)]' }`} > 면적 재기 @@ -205,10 +207,10 @@ export function TopBar({ activeTab, onTabChange }: TopBarProps) {
🖨 출력
- - @@ -219,7 +221,7 @@ export function TopBar({ activeTab, onTabChange }: TopBarProps) { 🗺 지도 유형
{mapTypes.map(item => ( - + +
+ {/* 매뉴얼 */} diff --git a/frontend/src/common/store/themeStore.ts b/frontend/src/common/store/themeStore.ts new file mode 100644 index 0000000..66a63b1 --- /dev/null +++ b/frontend/src/common/store/themeStore.ts @@ -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((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 }); + }, +})); diff --git a/frontend/src/common/styles/base.css b/frontend/src/common/styles/base.css index aeca664..a57da1f 100644 --- a/frontend/src/common/styles/base.css +++ b/frontend/src/common/styles/base.css @@ -138,6 +138,25 @@ --purple-800: #6b21a8; --purple-900: #581c87; --purple-1000: #3b0764; + /* hover overlay */ + --hover-overlay: rgba(255, 255, 255, 0.06); + --dropdown-bg: rgba(18, 25, 41, 0.97); + } + + /* ── Light theme overrides ── */ + [data-theme="light"] { + --bg-base: #f8fafc; + --bg-surface: #ffffff; + --bg-elevated: #f1f5f9; + --bg-card: #ffffff; + --bg-surface-hover: #e2e8f0; + --stroke-default: #cbd5e1; + --stroke-light: #e2e8f0; + --fg-default: #0f172a; + --fg-sub: #475569; + --fg-disabled: #94a3b8; + --hover-overlay: rgba(0, 0, 0, 0.04); + --dropdown-bg: rgba(255, 255, 255, 0.97); } * { @@ -169,4 +188,14 @@ input[type="date"]::-webkit-calendar-picker-indicator:hover { opacity: 1; } + + /* Light theme: calendar icon reset */ + [data-theme="light"] input[type="date"]::-webkit-calendar-picker-indicator { + filter: none; + } + + /* Light theme: invert white logos */ + [data-theme="light"] .wing-logo { + filter: brightness(0) saturate(100%); + } } diff --git a/frontend/src/common/styles/components.css b/frontend/src/common/styles/components.css index bc03a02..a4cbf1a 100644 --- a/frontend/src/common/styles/components.css +++ b/frontend/src/common/styles/components.css @@ -830,10 +830,10 @@ .boom-setting-input { width: 56px; padding: 3px 6px; - background: var(--bg-elevated); + background: var(--bg-base); border: 1px solid var(--stroke-default); border-radius: 4px; - color: var(--color-warning); + color: var(--color-accent); font-family: var(--font-mono); font-size: 11px; font-weight: 600; @@ -1333,4 +1333,91 @@ opacity: 0.4; cursor: not-allowed; } + + /* ═══ Light Theme Overrides ═══ */ + + /* CCTV popup */ + [data-theme="light"] .cctv-dark-popup .maplibregl-popup-content { + background: #ffffff; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); + border: 1px solid var(--stroke-default); + } + [data-theme="light"] .cctv-dark-popup .maplibregl-popup-tip { + border-top-color: #ffffff; + border-bottom-color: #ffffff; + } + [data-theme="light"] .cctv-dark-popup .maplibregl-popup-close-button { + color: var(--fg-disabled); + } + [data-theme="light"] .cctv-dark-popup .maplibregl-popup-close-button:hover { + color: var(--fg-default); + } + + /* Date/Time picker color-scheme */ + [data-theme="light"] .prd-date-input, + [data-theme="light"] .prd-time-input { + color-scheme: light; + } + [data-theme="light"] select.prd-i.prd-time-select { + color-scheme: light; + } + + /* Select option */ + [data-theme="light"] select.prd-i option { + background: #ffffff; + } + [data-theme="light"] select.prd-i option:checked { + background: linear-gradient(0deg, rgba(6, 182, 212, 0.15) 0%, rgba(6, 182, 212, 0.08) 100%); + } + + /* Select hover border */ + [data-theme="light"] select.prd-i:hover { + border-color: var(--stroke-light); + } + + /* Model chip text */ + [data-theme="light"] .prd-mc { + color: var(--fg-disabled); + } + [data-theme="light"] .prd-mc.on { + color: var(--fg-default); + } + + /* Coordinate display */ + [data-theme="light"] .cod { + background: rgba(255, 255, 255, 0.85); + color: var(--fg-sub); + } + [data-theme="light"] .cov { + color: var(--fg-default); + } + + /* Weather info panel */ + [data-theme="light"] .wip { + background: rgba(255, 255, 255, 0.85); + } + [data-theme="light"] .wii-value { + color: var(--fg-default); + } + [data-theme="light"] .wii-label { + color: var(--fg-sub); + } + + /* Timeline control panel */ + [data-theme="light"] .tlb { + background: rgba(255, 255, 255, 0.95); + } + + /* Timeline boom tooltip */ + [data-theme="light"] .tlbm .tlbt { + background: rgba(255, 255, 255, 0.95); + } + + /* Combo item border */ + [data-theme="light"] .combo-item { + border-bottom: 1px solid var(--stroke-light); + } + [data-theme="light"] .combo-list { + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1); + } } diff --git a/frontend/src/tabs/aerial/components/CCTVPlayer.tsx b/frontend/src/tabs/aerial/components/CCTVPlayer.tsx index fb6292e..899587f 100644 --- a/frontend/src/tabs/aerial/components/CCTVPlayer.tsx +++ b/frontend/src/tabs/aerial/components/CCTVPlayer.tsx @@ -226,7 +226,7 @@ export const CCTVPlayer = forwardRef(({ // 오프라인 if (playerState === 'offline') { return ( -
+
📹
{sttsCd === 'MAINT' ? '점검중' : '오프라인'} @@ -239,7 +239,7 @@ export const CCTVPlayer = forwardRef(({ // URL 미설정 if (playerState === 'no-url') { return ( -
+
📹
스트림 URL 미설정
{cameraNm}
@@ -250,7 +250,7 @@ export const CCTVPlayer = forwardRef(({ // 에러 if (playerState === 'error') { return ( -
+
⚠️
연결 실패
{cameraNm}
@@ -268,7 +268,7 @@ export const CCTVPlayer = forwardRef(({
{/* 로딩 오버레이 */} {playerState === 'loading' && ( -
+
📹
연결 중...
@@ -338,7 +338,7 @@ export const CCTVPlayer = forwardRef(({ {sttsCd === 'LIVE' && ( ● REC diff --git a/frontend/src/tabs/aerial/components/CctvView.tsx b/frontend/src/tabs/aerial/components/CctvView.tsx index 017d4b3..4f0f583 100644 --- a/frontend/src/tabs/aerial/components/CctvView.tsx +++ b/frontend/src/tabs/aerial/components/CctvView.tsx @@ -265,7 +265,7 @@ export function CctvView() {
{/* 가운데: 영상 뷰어 */} -
+
{/* 뷰어 툴바 */}
@@ -484,9 +484,9 @@ export function CctvView() { offset={14} className="cctv-dark-popup" > -
-
{mapPopup.cameraNm}
-
{mapPopup.locDc ?? ''}
+
+
{mapPopup.cameraNm}
+
{mapPopup.locDc ?? ''}
{mapPopup.sttsCd === 'LIVE' ? '● LIVE' : '● OFF'} - {mapPopup.sourceNm} + {mapPopup.sourceNm}
+ API Docs ↗ +
{/* 본문 */} -
+
{/* API 상태 */}
API Connected - eapi.maxar.com/e1so/rapidoc · Latency: 142ms - Quota: 47/50 요청 잔여 + eapi.maxar.com/e1so/rapidoc · Latency: 142ms + Quota: 47/50 요청 잔여
{/* ① 태스킹 유형 */} @@ -727,7 +727,7 @@ export function SatelliteRequest() { {sectionHeader(1, '태스킹 유형 · 우선순위')}
- +
- +
- +
- +
- +
- +
- +
- +
@@ -788,15 +788,15 @@ export function SatelliteRequest() { {sectionHeader(3, '촬영 기간 · 반복')}
- +
- +
- + @@ -821,7 +821,7 @@ export function SatelliteRequest() {
- + {opt.label} ))} @@ -848,7 +848,7 @@ export function SatelliteRequest() { {sectionHeader(5, '연계 사고 · 비고')}
- +
- +