Refine top header menu
All checks were successful
Deploy blog-frontend / build-and-deploy (push) Successful in 2m8s

This commit is contained in:
wypark
2026-05-28 23:04:43 +09:00
parent 701bbdea4a
commit caa7196608
2 changed files with 113 additions and 45 deletions

7
LOG.md
View File

@@ -2,6 +2,13 @@
## 2026-05-28 ## 2026-05-28
- 상단 우측의 테마 토글, 로그인/회원가입, 관리자/새 글/로그아웃 액션을 작은 메뉴 버튼 뒤로 숨기고, 클릭 시 왼쪽으로 펼쳐지는 패널 애니메이션으로 노출되도록 수정했다.
- 메뉴 바깥 클릭과 Escape 키로 닫히게 하고, 링크 이동/로그아웃 시 메뉴가 닫히도록 정리했다.
- 검증: `npm run lint` 통과, `npm run build` 통과. build 중 sitemap fetch는 로컬 네트워크 제한으로 기존처럼 경고를 출력했지만 실패하지 않았다. Browser로 데스크톱/모바일 홈에서 닫힘 상태와 펼침 상태를 확인했다.
- 다음 추천 작업: 실제 로그인/관리자 상태에서 펼침 패널의 버튼 수가 늘어났을 때 모바일 폭에서 자연스럽게 보이는지 production Origin에서 확인한다.
## 2026-05-28
- Pretendard Variable WOFF2를 `src/app/fonts`에 self-host로 추가하고 `next/font/local`로 전역 적용했다. - Pretendard Variable WOFF2를 `src/app/fonts`에 self-host로 추가하고 `next/font/local`로 전역 적용했다.
- 홈을 Apple editorial 톤으로 정리해 히어로, 공지, 대표 글, 최신 글 중심으로 줄이고 최근 업데이트/카테고리 선반/인기 글/하단 아카이브 CTA를 제거했다. - 홈을 Apple editorial 톤으로 정리해 히어로, 공지, 대표 글, 최신 글 중심으로 줄이고 최근 업데이트/카테고리 선반/인기 글/하단 아카이브 CTA를 제거했다.
- TopHeader와 ThemeToggle을 아이콘 중심의 조용한 컨트롤로 낮추고, Sidebar는 모바일 기본 닫힘 + 오버레이 drawer로 바꿨다. - TopHeader와 ThemeToggle을 아이콘 중심의 조용한 컨트롤로 낮추고, Sidebar는 모바일 기본 닫힘 + 오버레이 drawer로 바꿨다.

View File

@@ -1,27 +1,71 @@
// src/components/layout/TopHeader.tsx // src/components/layout/TopHeader.tsx
'use client'; 'use client';
import { useEffect, useRef, useState } from 'react';
import Link from 'next/link'; import Link from 'next/link';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useAuthStore } from '@/store/authStore'; import { useAuthStore } from '@/store/authStore';
import { LogOut, PenLine, Settings, User, UserPlus } from 'lucide-react'; import { LogOut, Menu, PenLine, Settings, User, UserPlus, X } from 'lucide-react';
import ThemeToggle from '@/components/theme/ThemeToggle'; import ThemeToggle from '@/components/theme/ThemeToggle';
export default function TopHeader() { export default function TopHeader() {
const router = useRouter(); const router = useRouter();
const { isLoggedIn, role, logout, _hasHydrated } = useAuthStore(); const { isLoggedIn, role, logout, _hasHydrated } = useAuthStore();
const [isMenuOpen, setIsMenuOpen] = useState(false);
const menuRef = useRef<HTMLDivElement>(null);
const isAdmin = _hasHydrated && role?.includes('ADMIN'); const isAdmin = _hasHydrated && role?.includes('ADMIN');
useEffect(() => {
const handlePointerDown = (event: PointerEvent) => {
if (!menuRef.current?.contains(event.target as Node)) {
setIsMenuOpen(false);
}
};
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
setIsMenuOpen(false);
}
};
document.addEventListener('pointerdown', handlePointerDown);
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('pointerdown', handlePointerDown);
document.removeEventListener('keydown', handleKeyDown);
};
}, []);
const closeMenu = () => setIsMenuOpen(false);
const handleLogout = () => { const handleLogout = () => {
if (confirm('로그아웃 하시겠습니까?')) { if (confirm('로그아웃 하시겠습니까?')) {
closeMenu();
logout(); logout();
alert('로그아웃 되었습니다.'); alert('로그아웃 되었습니다.');
router.push('/'); router.push('/');
} }
}; };
const quietActionClass =
'flex h-9 shrink-0 items-center gap-2 rounded-full border border-[var(--color-line)] bg-[var(--color-control)] px-3 text-sm font-semibold text-[var(--color-text-muted)] shadow-[var(--shadow-control)] backdrop-blur-2xl transition-colors hover:bg-[var(--color-surface-strong)] hover:text-[var(--color-text)]';
return ( return (
<div className="absolute right-4 top-4 z-30 flex max-w-[calc(100vw-5.5rem)] flex-wrap items-center justify-end gap-2 md:right-6 md:top-6 md:max-w-none"> <div
ref={menuRef}
className="absolute right-4 top-4 z-30 flex max-w-[calc(100vw-2rem)] items-center justify-end gap-2 md:right-6 md:top-6"
>
<div
className={[
'flex min-w-0 items-center gap-2 overflow-hidden transition-all duration-300 ease-out',
isMenuOpen
? 'max-w-[calc(100vw-5rem)] translate-x-0 opacity-100'
: 'pointer-events-none max-w-0 translate-x-3 opacity-0',
].join(' ')}
aria-hidden={!isMenuOpen}
>
<div className="flex min-w-max items-center gap-2 rounded-full border border-[var(--color-line)] bg-[var(--color-surface)]/85 p-1.5 shadow-[var(--shadow-control)] backdrop-blur-2xl">
<ThemeToggle /> <ThemeToggle />
{_hasHydrated && ( {_hasHydrated && (
@@ -31,7 +75,8 @@ export default function TopHeader() {
<> <>
<Link <Link
href="/admin" href="/admin"
className="flex h-9 items-center gap-2 rounded-full border border-[var(--color-line)] bg-[var(--color-control)] px-3 text-sm font-semibold text-[var(--color-text-muted)] shadow-[var(--shadow-control)] backdrop-blur-2xl transition-colors hover:bg-[var(--color-surface-strong)] hover:text-[var(--color-accent)]" onClick={closeMenu}
className={quietActionClass}
> >
<Settings size={16} /> <Settings size={16} />
<span className="hidden sm:inline"></span> <span className="hidden sm:inline"></span>
@@ -39,7 +84,8 @@ export default function TopHeader() {
<Link <Link
href="/admin/posts/new" href="/admin/posts/new"
className="flex h-9 items-center gap-2 rounded-full bg-[var(--color-text)] px-3 text-sm font-semibold text-[var(--color-page)] shadow-[var(--shadow-control)] transition-all hover:opacity-90 dark:bg-white dark:text-black" onClick={closeMenu}
className="flex h-9 shrink-0 items-center gap-2 rounded-full bg-[var(--color-text)] px-3 text-sm font-semibold text-[var(--color-page)] shadow-[var(--shadow-control)] transition-all hover:opacity-90 dark:bg-white dark:text-black"
> >
<PenLine size={16} /> <PenLine size={16} />
<span> </span> <span> </span>
@@ -48,8 +94,9 @@ export default function TopHeader() {
)} )}
<button <button
type="button"
onClick={handleLogout} onClick={handleLogout}
className="flex h-9 items-center gap-2 rounded-full border border-[var(--color-line)] bg-[var(--color-control)] px-3 text-sm font-medium text-[var(--color-text-muted)] shadow-[var(--shadow-control)] backdrop-blur-2xl transition-colors hover:bg-[var(--color-surface-strong)] hover:text-red-500" className="flex h-9 shrink-0 items-center gap-2 rounded-full border border-[var(--color-line)] bg-[var(--color-control)] px-3 text-sm font-medium text-[var(--color-text-muted)] shadow-[var(--shadow-control)] backdrop-blur-2xl transition-colors hover:bg-[var(--color-surface-strong)] hover:text-red-500"
> >
<LogOut size={16} /> <LogOut size={16} />
<span className="hidden sm:inline"></span> <span className="hidden sm:inline"></span>
@@ -59,7 +106,8 @@ export default function TopHeader() {
<> <>
<Link <Link
href="/login" href="/login"
className="flex h-9 items-center gap-2 rounded-full border border-transparent px-2.5 text-sm font-semibold text-[var(--color-text-muted)] transition-colors hover:text-[var(--color-text)] sm:px-3" onClick={closeMenu}
className="flex h-9 shrink-0 items-center gap-2 rounded-full border border-transparent px-2.5 text-sm font-semibold text-[var(--color-text-muted)] transition-colors hover:text-[var(--color-text)] sm:px-3"
> >
<User size={18} /> <User size={18} />
<span className="hidden sm:inline"></span> <span className="hidden sm:inline"></span>
@@ -67,7 +115,8 @@ export default function TopHeader() {
<Link <Link
href="/signup" href="/signup"
className="flex h-9 items-center gap-2 rounded-full border border-[var(--color-line)] bg-[var(--color-control)] px-3 text-sm font-semibold text-[var(--color-text)] shadow-[var(--shadow-control)] backdrop-blur-2xl transition-all hover:bg-[var(--color-surface-strong)]" onClick={closeMenu}
className="flex h-9 shrink-0 items-center gap-2 rounded-full border border-[var(--color-line)] bg-[var(--color-control)] px-3 text-sm font-semibold text-[var(--color-text)] shadow-[var(--shadow-control)] backdrop-blur-2xl transition-all hover:bg-[var(--color-surface-strong)]"
> >
<UserPlus size={16} /> <UserPlus size={16} />
<span className="hidden sm:inline"></span> <span className="hidden sm:inline"></span>
@@ -76,5 +125,17 @@ export default function TopHeader() {
) )
)} )}
</div> </div>
</div>
<button
type="button"
aria-label={isMenuOpen ? '상단 메뉴 닫기' : '상단 메뉴 열기'}
aria-expanded={isMenuOpen}
onClick={() => setIsMenuOpen((previous) => !previous)}
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full border border-[var(--color-line)] bg-[var(--color-control)] text-[var(--color-text)] shadow-[var(--shadow-control)] backdrop-blur-2xl transition-colors hover:bg-[var(--color-surface-strong)]"
>
{isMenuOpen ? <X size={18} /> : <Menu size={18} />}
</button>
</div>
); );
} }