316 lines
13 KiB
TypeScript
Executable File
316 lines
13 KiB
TypeScript
Executable File
import { useState, useRef, useEffect, useMemo } from 'react';
|
||
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 {
|
||
activeTab: MainTab;
|
||
onTabChange: (tab: MainTab) => void;
|
||
}
|
||
|
||
export function TopBar({ activeTab, onTabChange }: TopBarProps) {
|
||
const [showQuickMenu, setShowQuickMenu] = useState(false);
|
||
const [showManual, setShowManual] = useState(false);
|
||
const quickMenuRef = useRef<HTMLDivElement>(null);
|
||
const { hasPermission, user, logout } = useAuthStore();
|
||
const { menuConfig, isLoaded } = useMenuStore();
|
||
const { mapToggles, toggleMap, mapTypes, measureMode, setMeasureMode } = useMapStore();
|
||
const { theme, toggleTheme } = useThemeStore();
|
||
|
||
const MAP_TABS = new Set<string>(['prediction', 'hns', 'scat', 'incidents']);
|
||
const isMapTab = MAP_TABS.has(activeTab);
|
||
|
||
const handleToggleMeasure = (mode: 'distance' | 'area') => {
|
||
if (!isMapTab) return;
|
||
setMeasureMode(measureMode === mode ? null : mode);
|
||
setShowQuickMenu(false);
|
||
};
|
||
|
||
const tabs = useMemo(() => {
|
||
if (!isLoaded || menuConfig.length === 0) return [];
|
||
|
||
return menuConfig
|
||
.filter((m) => m.enabled && hasPermission(m.id))
|
||
.sort((a, b) => a.order - b.order);
|
||
}, [hasPermission, user?.permissions, menuConfig, isLoaded]);
|
||
|
||
useEffect(() => {
|
||
const handler = (e: MouseEvent) => {
|
||
if (quickMenuRef.current && !quickMenuRef.current.contains(e.target as Node))
|
||
setShowQuickMenu(false);
|
||
};
|
||
if (showQuickMenu) document.addEventListener('mousedown', handler);
|
||
return () => document.removeEventListener('mousedown', handler);
|
||
}, [showQuickMenu]);
|
||
|
||
return (
|
||
<div className="h-[52px] bg-bg-surface border-b border-stroke flex items-center justify-between px-5 relative z-[100]">
|
||
{/* Left Section */}
|
||
<div className="flex items-center gap-4">
|
||
{/* Logo */}
|
||
<button
|
||
onClick={() => tabs[0] && onTabChange(tabs[0].id as MainTab)}
|
||
className="flex items-center hover:opacity-80 transition-opacity cursor-pointer"
|
||
title="홈으로 이동"
|
||
>
|
||
<img src="/wing_logo_white.svg" alt="WING 해양환경 위기대응" className="h-5 wing-logo" />
|
||
</button>
|
||
|
||
{/* Divider */}
|
||
<div className="w-px h-6 bg-border-light" />
|
||
|
||
{/* Tabs */}
|
||
<div className="flex gap-0.5">
|
||
{tabs.map((tab) => {
|
||
const isIncident = tab.id === 'incidents';
|
||
const isMonitor = tab.id === 'monitor';
|
||
const handleClick = () => {
|
||
if (isMonitor) {
|
||
window.open(
|
||
import.meta.env.VITE_SITUATIONAL_URL ?? 'https://kcg.gc-si.dev',
|
||
'_blank',
|
||
);
|
||
} else {
|
||
onTabChange(tab.id as MainTab);
|
||
}
|
||
};
|
||
return (
|
||
<button
|
||
key={tab.id}
|
||
onClick={handleClick}
|
||
title={tab.label}
|
||
className={`
|
||
px-2.5 xl:px-4 py-2 text-title-2 font-bold transition-all duration-200
|
||
font-korean tracking-navigation border-b-2 border-transparent
|
||
${isIncident ? 'ml-1' : ''}
|
||
${isMonitor ? 'ml-1 flex items-center gap-1.5' : ''}
|
||
${
|
||
isMonitor
|
||
? 'text-color-danger'
|
||
: activeTab === tab.id
|
||
? isIncident
|
||
? 'text-[#a5b4fc] border-b-[#a5b4fc]'
|
||
: 'text-color-accent border-b-color-accent'
|
||
: isIncident
|
||
? 'text-[#818cf8] hover:text-[#a5b4fc]'
|
||
: 'text-fg-sub hover:text-fg'
|
||
}
|
||
`}
|
||
>
|
||
{isMonitor ? (
|
||
<>
|
||
<span className="hidden xl:flex items-center gap-1.5">
|
||
<span className="w-1.5 h-1.5 rounded-full bg-color-danger animate-pulse inline-block" />
|
||
{tab.label}
|
||
</span>
|
||
<span className="xl:hidden text-[1rem] leading-none">{tab.icon}</span>
|
||
</>
|
||
) : (
|
||
<>
|
||
<span className="xl:hidden text-[1rem] leading-none">{tab.icon}</span>
|
||
<span className="hidden xl:inline">{tab.label}</span>
|
||
</>
|
||
)}
|
||
</button>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Right Section */}
|
||
<div className="flex items-center gap-3">
|
||
{/* Status Badge */}
|
||
{/* <div className="flex items-center gap-2 px-3 py-1.5 bg-[rgba(239,68,68,0.1)] border border-[rgba(239,68,68,0.2)] rounded-sm text-caption font-medium text-color-danger animate-pulse">
|
||
<div className="w-1.5 h-1.5 rounded-full bg-color-danger animate-pulse" />
|
||
사고 진행중
|
||
</div> */}
|
||
|
||
{/* Icon Buttons */}
|
||
{/* <button className="w-9 h-9 rounded-sm border border-stroke bg-bg-card text-fg-sub flex items-center justify-center hover:bg-bg-surface-hover hover:text-fg transition-all">
|
||
🔔
|
||
</button> */}
|
||
{hasPermission('admin') && (
|
||
<button
|
||
onClick={() => onTabChange('admin')}
|
||
className={`w-9 h-9 rounded-sm border flex items-center justify-center transition-all ${
|
||
activeTab === 'admin'
|
||
? 'border-color-accent bg-[rgba(6,182,212,0.15)] text-color-accent'
|
||
: 'border-stroke bg-bg-card text-fg-sub hover:bg-bg-surface-hover hover:text-fg'
|
||
}`}
|
||
>
|
||
⚙️
|
||
</button>
|
||
)}
|
||
{user && (
|
||
<div className="flex items-center gap-2 pl-2 border-l border-stroke">
|
||
<span className="text-label-2 text-fg-sub font-korean">{user.name}</span>
|
||
<button
|
||
onClick={() => logout()}
|
||
className="px-2 py-1 text-label-2 font-medium text-fg-disabled border border-stroke rounded hover:bg-bg-surface-hover hover:text-fg transition-all font-korean"
|
||
title="로그아웃"
|
||
>
|
||
로그아웃
|
||
</button>
|
||
</div>
|
||
)}
|
||
|
||
{/* Quick Menu */}
|
||
<div ref={quickMenuRef} className="relative">
|
||
<button
|
||
onClick={() => setShowQuickMenu(!showQuickMenu)}
|
||
className={`w-9 h-9 rounded-sm border flex items-center justify-center transition-all ${
|
||
showQuickMenu
|
||
? 'border-color-accent bg-[rgba(6,182,212,0.15)] text-color-accent'
|
||
: 'border-stroke bg-bg-card text-fg-sub hover:bg-bg-surface-hover hover:text-fg'
|
||
}`}
|
||
>
|
||
<svg
|
||
width="16"
|
||
height="16"
|
||
viewBox="0 0 16 16"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
strokeWidth="1.5"
|
||
strokeLinecap="round"
|
||
>
|
||
<line x1="2" y1="4" x2="14" y2="4" />
|
||
<line x1="2" y1="8" x2="14" y2="8" />
|
||
<line x1="2" y1="12" x2="14" y2="12" />
|
||
</svg>
|
||
</button>
|
||
|
||
{showQuickMenu && (
|
||
<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-title-6 font-bold text-fg-disabled">
|
||
<span>📐</span> 거리·면적 계산
|
||
</div> */}
|
||
<button
|
||
onClick={() => handleToggleMeasure('distance')}
|
||
disabled={!isMapTab}
|
||
className={`w-full px-3 py-2 flex items-center gap-2.5 text-title-5 transition-all ${
|
||
!isMapTab
|
||
? 'text-fg-disabled opacity-40 cursor-not-allowed'
|
||
: measureMode === 'distance'
|
||
? 'text-color-accent bg-[rgba(6,182,212,0.1)]'
|
||
: 'text-fg hover:bg-[var(--hover-overlay)]'
|
||
}`}
|
||
>
|
||
<span className="text-title-4">↗</span> 거리 재기
|
||
{measureMode === 'distance' && (
|
||
<span className="ml-auto text-title-6 text-color-accent">활성</span>
|
||
)}
|
||
</button>
|
||
<button
|
||
onClick={() => handleToggleMeasure('area')}
|
||
disabled={!isMapTab}
|
||
className={`w-full px-3 py-2 flex items-center gap-2.5 text-title-5 transition-all ${
|
||
!isMapTab
|
||
? 'text-fg-disabled opacity-40 cursor-not-allowed'
|
||
: measureMode === 'area'
|
||
? 'text-color-accent bg-[rgba(6,182,212,0.1)]'
|
||
: 'text-fg hover:bg-[var(--hover-overlay)]'
|
||
}`}
|
||
>
|
||
<span className="text-title-4">⭕</span> 면적 재기
|
||
{measureMode === 'area' && (
|
||
<span className="ml-auto text-title-6 text-color-accent">활성</span>
|
||
)}
|
||
</button>
|
||
|
||
<div className="my-1.5 border-t border-stroke" />
|
||
|
||
{/* 출력 */}
|
||
<div className="px-3 py-1.5 flex items-center gap-2 text-title-6 font-bold text-fg-disabled">
|
||
<span>🖨</span> 출력
|
||
</div>
|
||
<button className="w-full px-3 py-2 flex items-center gap-2.5 text-title-5 text-fg hover:bg-[var(--hover-overlay)] transition-all">
|
||
<span className="text-title-4">📸</span> 화면 캡쳐 다운로드
|
||
</button>
|
||
<button
|
||
onClick={() => window.print()}
|
||
className="w-full px-3 py-2 flex items-center gap-2.5 text-title-5 text-fg hover:bg-[var(--hover-overlay)] transition-all"
|
||
>
|
||
<span className="text-title-4">🖨</span> 인쇄
|
||
</button>
|
||
|
||
<div className="my-1.5 border-t border-stroke" />
|
||
|
||
{/* 지도 유형 */}
|
||
<div className="px-3 py-1.5 flex items-center gap-2 text-title-6 font-bold text-fg-disabled">
|
||
<span>🗺</span> 지도 유형
|
||
</div>
|
||
{mapTypes.map((item) => (
|
||
<button
|
||
key={item.mapKey}
|
||
onClick={() => toggleMap(item.mapKey)}
|
||
className="w-full px-3 py-2 flex items-center justify-between text-title-5 text-fg-sub hover:bg-[var(--hover-overlay)] transition-all"
|
||
>
|
||
<span className="flex items-center gap-2.5">
|
||
<span className="text-title-4">🗺</span> {item.mapNm}
|
||
</span>
|
||
<div
|
||
className={`w-[34px] h-[18px] rounded-full transition-all relative ${mapToggles[item.mapKey] ? '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 ${mapToggles[item.mapKey] ? 'left-[16px]' : 'left-[2px]'}`}
|
||
/>
|
||
</div>
|
||
</button>
|
||
))}
|
||
|
||
<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-title-5 text-fg hover:bg-[var(--hover-overlay)] transition-all"
|
||
>
|
||
<span className="flex items-center gap-2.5">
|
||
<span className="text-title-4">
|
||
{theme === 'dark' ? '\u2600\uFE0F' : '\uD83C\uDF19'}
|
||
</span>
|
||
{theme === 'dark' ? '라이트 모드' : '다크 모드'}
|
||
</span>
|
||
<div
|
||
className={`w-[34px] h-[18px] rounded-full transition-all relative ${
|
||
theme === 'light' ? 'bg-color-accent' : 'bg-bg-card border border-stroke'
|
||
}`}
|
||
>
|
||
<div
|
||
className={`absolute top-[2px] w-[14px] h-[14px] rounded-full bg-white shadow transition-all ${
|
||
theme === 'light' ? 'left-[16px]' : 'left-[2px]'
|
||
}`}
|
||
/>
|
||
</div>
|
||
</button>
|
||
|
||
<div className="my-1.5 border-t border-stroke" />
|
||
|
||
{/* 매뉴얼 */}
|
||
<button
|
||
onClick={() => {
|
||
setShowManual(true);
|
||
setShowQuickMenu(false);
|
||
}}
|
||
className="w-full px-3 py-2 flex items-center gap-2.5 text-title-5 text-fg hover:bg-[var(--hover-overlay)] transition-all"
|
||
>
|
||
<span className="text-title-4">📖</span> 사용자 매뉴얼
|
||
</button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* 사용자 매뉴얼 팝업 */}
|
||
<UserManualPopup isOpen={showManual} onClose={() => setShowManual(false)} />
|
||
</div>
|
||
);
|
||
}
|