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

223 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'
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 } = 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 */}
<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-3.5" />
</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 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'}
${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)]'
: 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)]'
}
`}
>
{isMonitor ? (
<>
<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" />
{tab.label}
</span>
<span className="xl:hidden text-[16px] leading-none">{tab.icon}</span>
</>
) : (
<>
<span className="xl:hidden text-[16px] 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-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>
{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>
{mapTypes.map(item => (
<button key={item.mapKey} onClick={() => toggleMap(item.mapKey)} 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]">🗺</span> {item.mapNm}
</span>
<div className={`w-[34px] h-[18px] rounded-full transition-all relative ${mapToggles[item.mapKey] ? '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.mapKey] ? 'left-[16px]' : 'left-[2px]'}`} />
</div>
</button>
))}
<div className="my-1.5 border-t border-border" />
{/* 매뉴얼 */}
<button
onClick={() => {
setShowManual(true)
setShowQuickMenu(false)
}}
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]">&#x1F4D6;</span>
</button>
</div>
)}
</div>
</div>
{/* 사용자 매뉴얼 팝업 */}
<UserManualPopup isOpen={showManual} onClose={() => setShowManual(false)} />
</div>
)
}