File name
Commit message
Commit date
File name
Commit message
Commit date
09-04
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>
);
}