'use client'; import Image from 'next/image'; import Link from 'next/link'; import { useRouter } from 'next/navigation'; import { startTransition, useEffect, useOptimistic, useRef, useState } from 'react'; import { logout } from '@/app/(protected)/_actions'; import { markNotificationReadAction } from '@/app/(protected)/(basic)/_actions'; import { countUnread, resolveNotificationHref, type AdminNotification, } from '@/lib/domain/notification'; import styles from './admin-header.module.scss'; import { HeaderIconButton } from './header-icon-button'; interface AdminHeaderProps { /** 프로필에 표시할 이름. 세션의 관리자 이름을 상위에서 받아온다. */ adminName: string; /** 종 버튼이 펼치는 알림. 상위 레이아웃이 조회해 넘긴다. */ notifications: AdminNotification[]; } /** * Basic 레이아웃 헤더 — 시안 header(7028:7614)를 그대로 옮겼다. 좌측 로고, 우측에 아이콘 * 버튼 3개와 프로필 pill이 온다. * * 시안과 다른 점 넷(전부 사용자 확정): * - 로고: 시안(관리자페이지 QsFVFFUKGH68xAPVkF3SJ9 logo 5199:9639)의 정사각 심볼과 * "FOX PAY ADMIN" 2색 글자를 그대로 쓴다. * - 사이드바 토글 버튼을 뺐다 — 시안에 없다. * - 프로필 pill을 눌러 로그아웃한다. 시안에 로그아웃 자리가 없는데 다른 경로도 없어서, * CaretDown이 이미 "열린다"고 알리고 있는 이 자리에 메뉴를 붙였다. * * - 검색·사용자 아이콘 버튼을 뺐다 — 연결할 기능이 없어 눌러도 아무 일이 없었고, 사용자 * 자리는 옆 프로필 pill과 겹쳤다. * * 이슈: 알림 읽음은 낙관적으로 표시한 뒤 `router.refresh()`로 서버 값에 되맞춘다 — 백엔드 * PUT이 아직 행을 갱신하지 못해(notification-repository 참조) 새로 고치면 안 읽음으로 돌아온다. */ export function AdminHeader({ adminName, notifications }: AdminHeaderProps) { const router = useRouter(); const [isNotificationOpen, setIsNotificationOpen] = useState(false); const notificationRef = useRef(null); const [optimisticNotifications, markReadOptimistically] = useOptimistic( notifications, (current, id: string) => current.map((item) => (item.id === id ? { ...item, isRead: true } : item)) ); const unreadCount = countUnread(optimisticNotifications); const [isMenuOpen, setIsMenuOpen] = useState(false); const profileRef = useRef(null); function openNotification(item: AdminNotification) { setIsNotificationOpen(false); if (item.isRead) { return; } startTransition(async () => { markReadOptimistically(item.id); await markNotificationReadAction(item.id); router.refresh(); }); } // 바깥을 누르거나 Esc를 누르면 닫는다 — 열린 동안에만 듣는다. 프로필·알림이 같은 규칙이라 // 한 곳에서 처리하고, 하나를 열면 다른 하나는 닫는다. useEffect(() => { if (!isMenuOpen && !isNotificationOpen) { return; } function handlePointerDown(event: MouseEvent) { const target = event.target as Node; if (!profileRef.current?.contains(target)) { setIsMenuOpen(false); } if (!notificationRef.current?.contains(target)) { setIsNotificationOpen(false); } } function handleKeyDown(event: KeyboardEvent) { if (event.key === 'Escape') { setIsMenuOpen(false); setIsNotificationOpen(false); } } document.addEventListener('mousedown', handlePointerDown); document.addEventListener('keydown', handleKeyDown); return () => { document.removeEventListener('mousedown', handlePointerDown); document.removeEventListener('keydown', handleKeyDown); }; }, [isMenuOpen, isNotificationOpen]); return (
{/* 옆 글자가 같은 내용을 이미 말하므로 장식으로 둔다 — alt를 채우면 두 번 읽힌다. */} FOX PAY{' '} ADMIN