213 lines
10 KiB
TypeScript
Executable File
213 lines
10 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'
|
||
|
||
interface TopBarProps {
|
||
activeTab: MainTab
|
||
onTabChange: (tab: MainTab) => void
|
||
}
|
||
|
||
export function TopBar({ activeTab, onTabChange }: TopBarProps) {
|
||
const [showQuickMenu, setShowQuickMenu] = useState(false)
|
||
const quickMenuRef = useRef<HTMLDivElement>(null)
|
||
const { hasPermission, user, logout } = useAuthStore()
|
||
const { menuConfig, isLoaded } = useMenuStore()
|
||
const { mapToggles, toggleMap } = useMapStore()
|
||
|
||
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-1 border-b border-border flex items-center justify-between px-5 relative z-[100]">
|
||
{/* Left Section */}
|
||
<div className="flex items-center gap-4">
|
||
{/* Logo */}
|
||
<div className="flex items-center">
|
||
<img src="/wing_logo_white.svg" alt="WING 해양환경 위기대응" className="h-3.5" />
|
||
</div>
|
||
|
||
{/* 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'
|
||
return (
|
||
<button
|
||
key={tab.id}
|
||
onClick={() => onTabChange(tab.id as MainTab)}
|
||
title={tab.label}
|
||
className={`
|
||
px-2.5 xl:px-4 py-2 rounded-sm text-[13px] transition-all duration-200
|
||
font-korean tracking-[0.2px]
|
||
${isIncident ? 'font-extrabold border-l border-l-[rgba(99,102,241,0.2)] ml-1' : 'font-semibold'}
|
||
${
|
||
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)]'
|
||
: 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)]'
|
||
}
|
||
`}
|
||
>
|
||
<span className="xl:hidden text-[16px] leading-none">{tab.icon}</span>
|
||
<span className="hidden xl:inline">{tab.label}</span>
|
||
</button>
|
||
)
|
||
})}
|
||
|
||
{/* 실시간 상황관리 */}
|
||
<button
|
||
onClick={() => window.open(import.meta.env.VITE_SITUATIONAL_URL ?? 'http://localhost:5174', '_blank')}
|
||
className={`
|
||
px-2.5 xl:px-4 py-2 rounded-sm text-[13px] transition-all duration-200
|
||
font-korean tracking-[0.2px] font-semibold
|
||
border-l border-l-[rgba(239,68,68,0.25)] ml-1
|
||
text-[#f87171] hover:text-[#fca5a5] hover:bg-[rgba(239,68,68,0.1)]
|
||
flex items-center gap-1.5
|
||
`}
|
||
title="실시간 상황관리"
|
||
>
|
||
<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>
|
||
<span className="xl:hidden text-[16px] leading-none">🛰</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-xs font-medium text-status-red animate-pulse">
|
||
<div className="w-1.5 h-1.5 rounded-full bg-status-red animate-pulse" />
|
||
사고 진행중
|
||
</div>
|
||
|
||
{/* Icon Buttons */}
|
||
<button className="w-9 h-9 rounded-sm border border-border bg-bg-3 text-text-2 flex items-center justify-center hover:bg-bg-hover hover:text-text-1 transition-all">
|
||
🔔
|
||
</button>
|
||
<button
|
||
onClick={() => onTabChange('showcase')}
|
||
title="쇼케이스"
|
||
className={`w-9 h-9 rounded-sm border flex items-center justify-center transition-all ${
|
||
activeTab === 'showcase'
|
||
? 'border-primary-cyan bg-[rgba(6,182,212,0.15)] text-primary-cyan'
|
||
: 'border-border bg-bg-3 text-text-2 hover:bg-bg-hover hover:text-text-1'
|
||
}`}
|
||
>
|
||
🎨
|
||
</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-primary-cyan bg-[rgba(6,182,212,0.15)] text-primary-cyan'
|
||
: 'border-border bg-bg-3 text-text-2 hover:bg-bg-hover hover:text-text-1'
|
||
}`}
|
||
>
|
||
⚙️
|
||
</button>
|
||
)}
|
||
{user && (
|
||
<div className="flex items-center gap-2 pl-2 border-l border-border">
|
||
<span className="text-[11px] text-text-2 font-korean">{user.name}</span>
|
||
<button
|
||
onClick={() => logout()}
|
||
className="px-2 py-1 text-[10px] font-semibold text-text-3 border border-border rounded hover:bg-bg-hover hover:text-text-1 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-primary-cyan bg-[rgba(6,182,212,0.15)] text-primary-cyan'
|
||
: 'border-border bg-bg-3 text-text-2 hover:bg-bg-hover hover:text-text-1'
|
||
}`}
|
||
>
|
||
<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-[rgba(18,25,41,0.97)] backdrop-blur-xl border border-border rounded-lg shadow-2xl z-[200] py-2 font-korean">
|
||
{/* 거리·면적 계산 */}
|
||
<div className="px-3 py-1.5 flex items-center gap-2 text-[11px] font-bold text-text-3">
|
||
<span>📐</span> 거리·면적 계산
|
||
</div>
|
||
<button className="w-full px-3 py-2 flex items-center gap-2.5 text-[12px] text-text-2 hover:bg-[rgba(255,255,255,0.06)] hover:text-text-1 transition-all">
|
||
<span className="text-[13px]">↗</span> 거리 재기
|
||
</button>
|
||
<button className="w-full px-3 py-2 flex items-center gap-2.5 text-[12px] text-text-2 hover:bg-[rgba(255,255,255,0.06)] hover:text-text-1 transition-all">
|
||
<span className="text-[13px]">⭕</span> 면적 재기
|
||
</button>
|
||
|
||
<div className="my-1.5 border-t border-border" />
|
||
|
||
{/* 출력 */}
|
||
<div className="px-3 py-1.5 flex items-center gap-2 text-[11px] font-bold text-text-3">
|
||
<span>🖨</span> 출력
|
||
</div>
|
||
<button className="w-full px-3 py-2 flex items-center gap-2.5 text-[12px] text-text-2 hover:bg-[rgba(255,255,255,0.06)] hover:text-text-1 transition-all">
|
||
<span className="text-[13px]">📸</span> 화면 캡쳐 다운로드
|
||
</button>
|
||
<button onClick={() => window.print()} className="w-full px-3 py-2 flex items-center gap-2.5 text-[12px] text-text-2 hover:bg-[rgba(255,255,255,0.06)] hover:text-text-1 transition-all">
|
||
<span className="text-[13px]">🖨</span> 인쇄
|
||
</button>
|
||
|
||
<div className="my-1.5 border-t border-border" />
|
||
|
||
{/* 지도 유형 */}
|
||
<div className="px-3 py-1.5 flex items-center gap-2 text-[11px] font-bold text-text-3">
|
||
<span>🗺</span> 지도 유형
|
||
</div>
|
||
{([
|
||
{ key: 's57' as const, label: 'S-57 전자해도', icon: '🗺' },
|
||
{ key: 's101' as const, label: 'S-101 전자해도', icon: '🗺' },
|
||
{ key: 'threeD' as const, label: '3D 지도', icon: '🗺' },
|
||
{ key: 'satellite' as const, label: '위성 영상', icon: '🛰' },
|
||
]).map(item => (
|
||
<button key={item.key} onClick={() => toggleMap(item.key)} className="w-full px-3 py-2 flex items-center justify-between text-[12px] text-text-2 hover:bg-[rgba(255,255,255,0.06)] transition-all">
|
||
<span className="flex items-center gap-2.5">
|
||
<span className="text-[13px]">{item.icon}</span> {item.label}
|
||
</span>
|
||
<div className={`w-[34px] h-[18px] rounded-full transition-all relative ${mapToggles[item.key] ? 'bg-primary-cyan' : 'bg-bg-3 border border-border'}`}>
|
||
<div className={`absolute top-[2px] w-[14px] h-[14px] rounded-full bg-white shadow transition-all ${mapToggles[item.key] ? 'left-[16px]' : 'left-[2px]'}`} />
|
||
</div>
|
||
</button>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|