'use client'; import '@/app/styles/market-sidebar.scss'; import Link from 'next/link'; import { useCallback, useEffect, useRef, useState } from 'react'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faAnglesLeft, faAnglesRight, faStar, faSun, faMoon } from '@fortawesome/free-solid-svg-icons'; import useTheme from '@/hooks/useTheme'; import useAuth from '@/hooks/useAuth'; import { fetchApi } from '@/lib/utils/client'; import { changeDirection, changeSymbol, formatChangeRate, formatNumber } from '@/lib/utils/stock'; import { StockWatchListResponse, StockWatchRow } from '@/types/stock'; const STORAGE_KEY = 'az-sidebar-collapsed'; const WIDTH_EXPANDED = '220px'; const WIDTH_COLLAPSED = '64px'; // 좌측 관심종목 사이드바 — 로그인 회원의 관심종목(종목명·현재가·등락)을 표시. 종목 상세의 ☆ 토글과 'watchlist:changed' 이벤트로 연동. // 접기 메커니즘은 ChannelSidebar 와 동일: html 루트의 --antooza-sidebar-width 로 layout grid 열 폭 반응. export default function MarketSidebar() { const [collapsed, setCollapsed] = useState(true); const { toggleTheme, isDark } = useTheme(); const { isAuthenticated } = useAuth(); const [items, setItems] = useState([]); const [loading, setLoading] = useState(false); const reqRef = useRef(0); // 최초 mount 시 localStorage에서 collapsed 상태 복원 useEffect(() => { try { const saved = window.localStorage.getItem(STORAGE_KEY); if (saved === 'false') { setCollapsed(false); } } catch {} }, []); // collapsed 변경 시 html 루트 CSS 변수 동기화 useEffect(() => { const root = document.documentElement; root.style.setProperty('--antooza-sidebar-width', collapsed ? WIDTH_COLLAPSED : WIDTH_EXPANDED); try { window.localStorage.setItem(STORAGE_KEY, String(collapsed)); } catch {} return () => { // unmount 시 원복 (breakpoint 별 기본값으로 복귀) root.style.removeProperty('--antooza-sidebar-width'); }; }, [collapsed]); const loadItems = useCallback(() => { if (!isAuthenticated) { setItems([]); return; } const reqId = ++reqRef.current; setLoading(true); fetchApi('/api/stocks/watchlist', { method: 'GET', silent: true }) .then(res => { // 늦게 도착한 이전 요청이 최신 목록을 덮어쓰지 않도록 순번 가드 (auth effect + watchlist:changed 중첩 방지) if (reqId !== reqRef.current) { return; } if (res.success && res.data) { setItems(res.data.list ?? []); } }) .catch(() => {}) .finally(() => { if (reqId === reqRef.current) { setLoading(false); } }); }, [isAuthenticated]); // 로그인 상태 변화 시 목록 로드 useEffect(() => { loadItems(); }, [loadItems]); // 상세 페이지 등에서 관심종목 변경 시 갱신 useEffect(() => { const handler = () => loadItems(); window.addEventListener('watchlist:changed', handler); return () => window.removeEventListener('watchlist:changed', handler); }, [loadItems]); const handleToggleCollapse = useCallback(() => { setCollapsed(prev => !prev); }, []); return (
{/* 헤더: 타이틀 + 접기 토글 */}
{!collapsed && ( 관심종목 )}
{/* 본문: 관심종목 목록 */}
{collapsed ? ( ) : !isAuthenticated ? (

로그인 후 관심종목을
확인할 수 있습니다.

) : loading ? (

불러오는 중…

) : items.length === 0 ? (

관심종목이 없습니다.
종목 상세에서 ☆ 로 추가하세요.

) : (
    {items.map(item => { const dir = changeDirection(item.changeRate); return (
  • {item.name} {formatNumber(item.closePrice)} {changeSymbol(dir)} {formatChangeRate(item.changeRate)}
  • ); })}
)}
{/* 하단: 테마 전환 (feature-off 로 ChannelSidebar 토글 미노출 시 여기서 제공) */}
); }