Layout.tsx 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. 'use client';
  2. import { useState, useCallback, useEffect } from 'react';
  3. import Link from 'next/link';
  4. import { usePathname } from 'next/navigation';
  5. import Styles from '../styles/common.module.scss';
  6. import useAuth from '@/hooks/useAuth';
  7. import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
  8. import { faBars, faXmark, faComments, faHeadset, faUser, faRightToBracket, faSun, faMoon } from '@fortawesome/free-solid-svg-icons';
  9. import { faCommentDots, faClock } from '@fortawesome/free-regular-svg-icons';
  10. import useTheme from '@/hooks/useTheme';
  11. import ChatSidebar from '@/app/component/chat/ChatSidebar';
  12. type Props = {
  13. children: React.ReactNode;
  14. };
  15. export default function Layout({ children }: Props) {
  16. const { isAuthenticated, isLoading, logout } = useAuth();
  17. const { toggleTheme, isDark } = useTheme();
  18. const [sidebarOpen, setSidebarOpen] = useState(false);
  19. const [chatOpen, setChatOpen] = useState(false);
  20. const pathname = usePathname();
  21. const toggleSidebar = useCallback(() => {
  22. setSidebarOpen((prev) => !prev);
  23. }, []);
  24. const toggleChat = useCallback(() => {
  25. setChatOpen((prev) => !prev);
  26. }, []);
  27. const closeOverlay = useCallback(() => {
  28. setSidebarOpen(false);
  29. setChatOpen(false);
  30. }, []);
  31. const [currentTime, setCurrentTime] = useState('');
  32. useEffect(() => {
  33. const updateTime = () => {
  34. const now = new Date();
  35. setCurrentTime(
  36. `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`
  37. );
  38. };
  39. updateTime();
  40. const timer = setInterval(updateTime, 1000);
  41. return () => clearInterval(timer);
  42. }, []);
  43. if (isLoading) {
  44. return <></>;
  45. }
  46. return (
  47. <>
  48. <div id='container' className={`${Styles.container}${sidebarOpen ? ` ${Styles.sidebarOpen}` : ''}${chatOpen ? ` ${Styles.chatOpen}` : ''}`}>
  49. {/* 상단 내용 */}
  50. <header id='header' className={`${Styles.header} flex items-center w-full px-4`}>
  51. <button type='button' className={Styles.hamburger} onClick={toggleSidebar} aria-label='메뉴'>
  52. <FontAwesomeIcon icon={sidebarOpen ? faXmark : faBars} />
  53. </button>
  54. <Link href='/' className={Styles.logo}>
  55. <picture>
  56. <source src="image.webp" type="image/webp" />
  57. <img src="/resources/logo.svg" alt="bitforum logo" />
  58. </picture>
  59. </Link>
  60. <nav className={Styles.pcNav}>
  61. <ul className='flex gap-4'>
  62. <li>
  63. <Link href='/'>
  64. 생방송
  65. </Link>
  66. </li>
  67. <li>
  68. <Link href='/feed'>
  69. 피드
  70. </Link>
  71. </li>
  72. <li>
  73. <Link href='/latest'>
  74. 순위
  75. </Link>
  76. </li>
  77. <li>
  78. <Link href='/market'>
  79. 상점
  80. </Link>
  81. </li>
  82. <li>
  83. <Link href='/market'>
  84. 출석부
  85. </Link>
  86. </li>
  87. <li>
  88. <Link href='/board/notice'>
  89. 고객지원
  90. </Link>
  91. </li>
  92. </ul>
  93. <ul className='flex gap-4 items-center'>
  94. <li>
  95. <button type='button' onClick={toggleTheme} aria-label='다크모드 전환' title={isDark ? '라이트 모드' : '다크 모드'}>
  96. <FontAwesomeIcon icon={isDark ? faSun : faMoon} />
  97. </button>
  98. </li>
  99. <li className={`${Styles.clock} text-sm opacity-70`} style={{ fontVariantNumeric: 'tabular-nums' }}>
  100. <FontAwesomeIcon icon={faClock} className='mr-1' />{currentTime}
  101. </li>
  102. {!isAuthenticated ? (
  103. <>
  104. <li>
  105. <a href='/login'>
  106. 로그인
  107. </a>
  108. </li>
  109. <li>
  110. <a href='/register'>
  111. 회원가입
  112. </a>
  113. </li>
  114. </>
  115. ) : (
  116. <>
  117. <li>
  118. <Link href='/profile'>
  119. 내 정보
  120. </Link>
  121. </li>
  122. <li>
  123. <button type='button' onClick={logout}>
  124. 로그아웃
  125. </button>
  126. </li>
  127. </>
  128. )}
  129. </ul>
  130. </nav>
  131. <button type='button' className={Styles.chatToggle} onClick={toggleChat} aria-label='채팅'>
  132. <FontAwesomeIcon icon={faCommentDots} />
  133. </button>
  134. </header>
  135. {/* 모바일 오버레이 */}
  136. <div className={Styles.overlay} onClick={closeOverlay} />
  137. {/* 메인 내용 */}
  138. <main id='main' className={`${Styles.main} relative`}>
  139. {children}
  140. </main>
  141. {/* 우측 채팅 사이드바 */}
  142. <aside id='chatAside' className={Styles.chatAside}>
  143. <ChatSidebar />
  144. </aside>
  145. {/* 하단 내용 */}
  146. <footer id='footer' className={`${Styles.footer} px-4`}>
  147. <ol>
  148. <li>
  149. <Link href='/docs/teams'>이용약관</Link>
  150. </li>
  151. <li>
  152. <Link href='/docs/privacy'>개인정보처리방침</Link>
  153. </li>
  154. {/* 저작권 표시 */}
  155. <li>© 2025 PLAYR. All rights reserved.</li>
  156. </ol>
  157. </footer>
  158. {/* 모바일 하단 탭바 */}
  159. <nav className={Styles.bottomTab}>
  160. <Link href='/latest' className={pathname.startsWith('/latest') || (pathname.startsWith('/board') && !pathname.startsWith('/board/notice')) || pathname.startsWith('/post') ? Styles.active : undefined}>
  161. <FontAwesomeIcon icon={faComments} />
  162. <span>토론</span>
  163. </Link>
  164. <Link href='/board/notice' className={pathname.startsWith('/board/notice') || pathname.startsWith('/support') ? Styles.active : undefined}>
  165. <FontAwesomeIcon icon={faHeadset} />
  166. <span>고객지원</span>
  167. </Link>
  168. {isAuthenticated ? (
  169. <Link href='/profile' className={pathname.startsWith('/profile') ? Styles.active : undefined}>
  170. <FontAwesomeIcon icon={faUser} />
  171. <span>내 정보</span>
  172. </Link>
  173. ) : (
  174. <a href='/login'>
  175. <FontAwesomeIcon icon={faRightToBracket} />
  176. <span>로그인</span>
  177. </a>
  178. )}
  179. </nav>
  180. </div>
  181. </>
  182. );
  183. }