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

213 lines
10 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'
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>
)
}