File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
'use client';
import Image from 'next/image';
import { useEffect, useRef, useState } from 'react';
import { logout } from '@/app/(protected)/_actions';
import { countUnread, 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과 겹쳤다.
*
* 남은 알림 버튼도 연결할 기능이 아직 없어 `onAction` 없이 둔다 — 시안의 모양을 유지하려고
* 비활성으로 만들지는 않았다.
*/
export function AdminHeader({ adminName, notifications }: AdminHeaderProps) {
const [isNotificationOpen, setIsNotificationOpen] = useState(false);
const notificationRef = useRef<HTMLDivElement>(null);
const unreadCount = countUnread(notifications);
const [isMenuOpen, setIsMenuOpen] = useState(false);
const profileRef = useRef<HTMLDivElement>(null);
// 바깥을 누르거나 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 (
<header className={styles.header}>
<div className={styles.logo}>
{/* 옆 글자가 같은 내용을 이미 말하므로 장식으로 둔다 — alt를 채우면 두 번 읽힌다. */}
<Image
src="/images/logo-foxpay-symbol.svg"
alt=""
width={28}
height={28}
className={styles.logoMark}
priority
/>
<span className={styles.title}>
<span className={styles.titleStrong}>FOX PAY</span>{' '}
<span className={styles.titleSub}>ADMIN</span>
</span>
</div>
<div className={styles.fnc}>
<div className={styles.btnGroup} ref={notificationRef}>
<HeaderIconButton
icon={<i className="fox-ico fox-ico-BellSimple" aria-hidden="true" />}
label={unreadCount > 0 ? `알림 ${unreadCount}건` : '알림'}
hasBadge={unreadCount > 0}
isExpanded={isNotificationOpen}
onAction={() => {
setIsNotificationOpen((open) => !open);
setIsMenuOpen(false);
}}
/>
{isNotificationOpen && (
<div className={styles.notifications} role="menu">
{notifications.length === 0 ? (
<p className={styles.notificationEmpty}>새 알림이 없습니다.</p>
) : (
<ul className={styles.notificationList}>
{notifications.map((item) => (
<li key={item.id}>
<div
className={`${styles.notification} ${
item.isRead ? '' : styles.notificationUnread
}`}
>
<span className={styles.notificationTitle}>
{item.title}
</span>
<span className={styles.notificationContent}>
{item.content}
</span>
</div>
</li>
))}
</ul>
)}
</div>
)}
</div>
<div className={styles.profile} ref={profileRef}>
<button
type="button"
className={styles.profileButton}
aria-haspopup="menu"
aria-expanded={isMenuOpen}
onClick={() => setIsMenuOpen((open) => !open)}
>
{/* 시안은 프로필 사진 자리지만 관리자 사진을 받는 경로가 없어 사람 아이콘으로 채운다. */}
<span className={styles.avatar}>
<i className="fox-ico fox-ico--fill fox-ico-User" aria-hidden="true" />
</span>
<span className={styles.profileName}>{adminName}</span>
<span className={styles.caret}>
<i className="fox-ico fox-ico-CaretDown" aria-hidden="true" />
</span>
</button>
{isMenuOpen && (
<div className={styles.menu} role="menu">
<form action={logout}>
<button type="submit" role="menuitem" className={styles.menuItem}>
로그아웃
</button>
</form>
</div>
)}
</div>
</div>
</header>
);
}