Layout.tsx 6.5 KB

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