Header.tsx 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. 'use client';
  2. import Styles from '../styles/layout.module.scss';
  3. import { useCallback } from 'react';
  4. import Link from 'next/link';
  5. import { usePathname } from 'next/navigation';
  6. import useAuth from '@/hooks/useAuth';
  7. import useDragScroll from '@/hooks/useDragScroll';
  8. import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
  9. import { faBars, faXmark, faCartShopping, faChevronLeft, faChevronRight } from '@fortawesome/free-solid-svg-icons';
  10. import NotificationBell from '@/app/component/NotificationBell';
  11. import Profile from '@/app/component/Profile';
  12. import Logo from '@/app/component/Logo';
  13. import PointChargeIcon from '@/public/icons/layout/point.svg';
  14. import useCart from '@/hooks/useCart';
  15. const navItems = [
  16. { href: '/live', label: '생방송' },
  17. { href: '/creators', label: '크리에이터' },
  18. { href: '/games', label: '게임' },
  19. { href: '/feed/all', label: '피드' },
  20. { href: '/ranking/hall-of-fame', label: '순위' },
  21. { href: '/store', label: '상점' },
  22. { href: '/attendance', label: '출석부' },
  23. { href: '/board/notice', label: '고객지원' }
  24. ];
  25. const SCROLL_AMOUNT = 120;
  26. type Props = {
  27. sidebarOpen: boolean;
  28. onToggle: () => void;
  29. };
  30. export default function Header({ sidebarOpen, onToggle }: Props)
  31. {
  32. const { isAuthenticated } = useAuth();
  33. const { totalCount: cartCount } = useCart();
  34. const pathname = usePathname();
  35. const dragScroll = useDragScroll<HTMLDivElement>();
  36. const scrollTabs = useCallback((dir: 'left' | 'right') => {
  37. const el = dragScroll.ref.current;
  38. if (!el) {
  39. return;
  40. }
  41. el.scrollBy({ left: dir === 'left' ? -SCROLL_AMOUNT : SCROLL_AMOUNT, behavior: 'smooth' });
  42. }, [dragScroll.ref]);
  43. const handlePopupCharge = useCallback(() => {
  44. const w = 450, h = 635;
  45. const left = window.screenX + (window.outerWidth - w) / 2;
  46. const top = window.screenY + (window.outerHeight - h) / 2;
  47. window.open('/charge', 'charge', `width=${w},height=${h},left=${left},top=${top},scrollbars=no,resizable=no`);
  48. }, []);
  49. return (
  50. <header id='header' className={Styles.header}>
  51. {/* 1줄: 로고 + 우측 아이콘 (PC에서는 전체 내비) */}
  52. <div className={Styles.headerRow1}>
  53. <button type='button' className={Styles.hamburger} onClick={onToggle} aria-label='메뉴'>
  54. <FontAwesomeIcon icon={sidebarOpen ? faXmark : faBars} />
  55. </button>
  56. <Link href='/' className={Styles.logo} aria-label="DPOT 홈">
  57. <Logo size="sm" />
  58. </Link>
  59. {/* PC 내비게이션 */}
  60. <nav className={Styles.pcNav}>
  61. <ul className='flex gap-4'>
  62. {navItems.map(item => (
  63. <li key={item.label}>
  64. <Link href={item.href}>{item.label}</Link>
  65. </li>
  66. ))}
  67. </ul>
  68. </nav>
  69. {/* 우측 아이콘 (항상 표시) */}
  70. <div className={Styles.headerActions}>
  71. <Link href="/cart" className={Styles.cartBtn} title="장바구니" aria-label="장바구니">
  72. <FontAwesomeIcon icon={faCartShopping} />
  73. {cartCount > 0 && (
  74. <span className={Styles.cartBadge}>{cartCount > 99 ? '99+' : cartCount}</span>
  75. )}
  76. </Link>
  77. {isAuthenticated && (
  78. <>
  79. <button type="button" className={Styles.chargeBtn} onClick={handlePopupCharge} title="포인트 충전">
  80. <PointChargeIcon width={24} height={24} />
  81. </button>
  82. <NotificationBell />
  83. </>
  84. )}
  85. <Profile />
  86. </div>
  87. </div>
  88. {/* 2줄: 모바일 전용 가로 스크롤 탭 */}
  89. <div className={Styles.headerRow2}>
  90. <button type='button' className={Styles.scrollArrow} onClick={() => scrollTabs('left')} aria-label='왼쪽 스크롤'>
  91. <FontAwesomeIcon icon={faChevronLeft} />
  92. </button>
  93. <div
  94. className={Styles.mobileTabScroll}
  95. ref={dragScroll.ref}
  96. onMouseDown={dragScroll.onMouseDown}
  97. onMouseMove={dragScroll.onMouseMove}
  98. onMouseUp={dragScroll.onMouseUp}
  99. onMouseLeave={dragScroll.onMouseLeave}
  100. >
  101. {navItems.map(item => (
  102. <Link
  103. key={item.label}
  104. href={item.href}
  105. className={`${Styles.mobileTab}${pathname === item.href || (item.href !== '/' && pathname.startsWith(item.href)) ? ` ${Styles.mobileTabActive}` : ''}`}
  106. >
  107. {item.label}
  108. </Link>
  109. ))}
  110. </div>
  111. <button type='button' className={Styles.scrollArrow} onClick={() => scrollTabs('right')} aria-label='오른쪽 스크롤'>
  112. <FontAwesomeIcon icon={faChevronRight} />
  113. </button>
  114. </div>
  115. </header>
  116. );
  117. }