Layout.tsx 2.8 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283
  1. 'use client';
  2. import { useState, useCallback } from 'react';
  3. import { usePathname } from 'next/navigation';
  4. import Styles from '../styles/layout.module.scss';
  5. import Header from './Header';
  6. import Footer from './Footer'
  7. import ChannelSidebar from '@/app/component/channel/ChannelSidebar';
  8. import MarketSidebar from '@/app/component/MarketSidebar';
  9. import ChatDock from '@/app/component/ChatDock';
  10. import { ChannelListItem } from '@/types/channel';
  11. import { FEATURE_CHANNEL } from '@/constants/features';
  12. type Props = {
  13. children: React.ReactNode;
  14. initialChannels?: ChannelListItem[];
  15. // 우측 유틸 슬롯 (D6 3열 그리드) — 제공 페이지만 desktop 이상에서 3열 렌더
  16. utility?: React.ReactNode;
  17. };
  18. export default function Layout({ children, initialChannels, utility }: Props) {
  19. const [sidebarOpen, setSidebarOpen] = useState(false);
  20. const pathname = usePathname();
  21. const toggleSidebar = useCallback(() => {
  22. setSidebarOpen((prev) => !prev);
  23. }, []);
  24. const closeOverlay = useCallback(() => {
  25. setSidebarOpen(false);
  26. }, []);
  27. // 좌측 사이드바 노출 규칙:
  28. // - 채널 기능 ON → 항상(채널 목록은 전역 내비)
  29. // - 채널 기능 OFF → 종목(/stock) 계열에서만(관심종목). 그 외 페이지는 no-aside 로 본문 확장
  30. const isStockRoute = pathname === '/stock' || pathname.startsWith('/stock/');
  31. const showAside = FEATURE_CHANNEL || isStockRoute;
  32. return (
  33. <>
  34. {/* 접근성: 스킵 링크 — Tab 첫 포커스 시 노출 */}
  35. <a href='#main' className={Styles.skipLink}>본문 바로가기</a>
  36. <div id='container' className={`${Styles.container}${sidebarOpen ? ` ${Styles.sidebarOpen}` : ''}${!showAside ? ` ${Styles.noAside}` : ''}${utility ? ` ${Styles.hasUtility}` : ''}`}>
  37. <Header sidebarOpen={sidebarOpen} onToggle={toggleSidebar} showAside={showAside} />
  38. {/* 모바일 오버레이 */}
  39. <div className={Styles.overlay} onClick={closeOverlay} />
  40. {/* 좌측 사이드바 — 채널 기능 ON: 채널 목록 / OFF: 관심종목(종목 메뉴에서만) */}
  41. {showAside && (
  42. <aside id='aside' className={Styles.aside} aria-label='사이드바'>
  43. {FEATURE_CHANNEL ? (
  44. <ChannelSidebar initialChannels={initialChannels} />
  45. ) : (
  46. <MarketSidebar />
  47. )}
  48. </aside>
  49. )}
  50. {/* 메인 내용 */}
  51. <main id='main' className={`${Styles.main} relative`}>
  52. {children}
  53. </main>
  54. {/* 우측 유틸 슬롯 (desktop 이상 전용) */}
  55. {utility && (
  56. <aside id='utility' className={Styles.utility} aria-label='보조 콘텐츠'>
  57. {utility}
  58. </aside>
  59. )}
  60. {/* 우측 고정 채팅 도크 — 페이지 전환에도 유지(상시 마운트). desktop=컬럼(접기), mobile=드로어 */}
  61. <ChatDock />
  62. {/* 하단 */}
  63. <Footer />
  64. </div>
  65. </>
  66. );
  67. }