Layout.tsx 6.7 KB

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