wing-ops/frontend/src/common/components/layout/TopBar.tsx

316 lines
13 KiB
TypeScript
Executable File
Raw Blame 히스토리

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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">&#x1F4D6;</span>
</button>
</div>
)}
</div>
</div>
{/* 사용자 매뉴얼 팝업 */}
<UserManualPopup isOpen={showManual} onClose={() => setShowManual(false)} />
</div>
);
}