Refine top header menu
All checks were successful
Deploy blog-frontend / build-and-deploy (push) Successful in 2m8s
All checks were successful
Deploy blog-frontend / build-and-deploy (push) Successful in 2m8s
This commit is contained in:
7
LOG.md
7
LOG.md
@@ -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로 바꿨다.
|
||||||
|
|||||||
@@ -1,80 +1,141 @@
|
|||||||
// 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('/');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
const quietActionClass =
|
||||||
<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">
|
'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)]';
|
||||||
<ThemeToggle />
|
|
||||||
|
|
||||||
{_hasHydrated && (
|
return (
|
||||||
isLoggedIn ? (
|
<div
|
||||||
<>
|
ref={menuRef}
|
||||||
{isAdmin && (
|
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 />
|
||||||
|
|
||||||
|
{_hasHydrated && (
|
||||||
|
isLoggedIn ? (
|
||||||
|
<>
|
||||||
|
{isAdmin && (
|
||||||
|
<>
|
||||||
|
<Link
|
||||||
|
href="/admin"
|
||||||
|
onClick={closeMenu}
|
||||||
|
className={quietActionClass}
|
||||||
|
>
|
||||||
|
<Settings size={16} />
|
||||||
|
<span className="hidden sm:inline">관리자</span>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
href="/admin/posts/new"
|
||||||
|
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} />
|
||||||
|
<span>새 글</span>
|
||||||
|
</Link>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleLogout}
|
||||||
|
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} />
|
||||||
|
<span className="hidden sm:inline">로그아웃</span>
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
<>
|
<>
|
||||||
<Link
|
<Link
|
||||||
href="/admin"
|
href="/login"
|
||||||
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="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"
|
||||||
>
|
>
|
||||||
<Settings size={16} />
|
<User size={18} />
|
||||||
<span className="hidden sm:inline">관리자</span>
|
<span className="hidden sm:inline">로그인</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
href="/admin/posts/new"
|
href="/signup"
|
||||||
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 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)]"
|
||||||
>
|
>
|
||||||
<PenLine size={16} />
|
<UserPlus size={16} />
|
||||||
<span>새 글</span>
|
<span className="hidden sm:inline">회원가입</span>
|
||||||
</Link>
|
</Link>
|
||||||
</>
|
</>
|
||||||
)}
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={handleLogout}
|
type="button"
|
||||||
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"
|
aria-label={isMenuOpen ? '상단 메뉴 닫기' : '상단 메뉴 열기'}
|
||||||
>
|
aria-expanded={isMenuOpen}
|
||||||
<LogOut size={16} />
|
onClick={() => setIsMenuOpen((previous) => !previous)}
|
||||||
<span className="hidden sm:inline">로그아웃</span>
|
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)]"
|
||||||
</button>
|
>
|
||||||
</>
|
{isMenuOpen ? <X size={18} /> : <Menu size={18} />}
|
||||||
) : (
|
</button>
|
||||||
<>
|
|
||||||
<Link
|
|
||||||
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"
|
|
||||||
>
|
|
||||||
<User size={18} />
|
|
||||||
<span className="hidden sm:inline">로그인</span>
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<Link
|
|
||||||
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)]"
|
|
||||||
>
|
|
||||||
<UserPlus size={16} />
|
|
||||||
<span className="hidden sm:inline">회원가입</span>
|
|
||||||
</Link>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user