/** * @file HoldingsList.tsx * @description 대시보드 좌측 영역의 보유 종목 리스트 컴포넌트 * @remarks * - [레이어] Components / UI * - [사용자 행동] 종목 리스트 스크롤 -> 특정 종목 클릭(선택) -> 우측 상세 프레뷰 갱신 * - [데이터 흐름] DashboardContainer(mergedHoldings) -> HoldingsList -> HoldingItemRow -> onSelect(Callback) * - [연관 파일] DashboardContainer.tsx, dashboard.types.ts, use-price-flash.ts * @author jihoon87.lee */ import { AlertCircle, Wallet2 } from "lucide-react"; import { RefreshCcw } from "lucide-react"; import { useRouter } from "next/navigation"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; import type { DashboardHoldingItem } from "@/features/dashboard/types/dashboard.types"; import { formatCurrency, formatPercent, getChangeToneClass, } from "@/features/dashboard/utils/dashboard-format"; import { useTradeNavigationStore } from "@/features/trade/store/use-trade-navigation-store"; import { cn } from "@/lib/utils"; import { usePriceFlash } from "@/features/dashboard/hooks/use-price-flash"; interface HoldingsListProps { /** 보유 종목 데이터 리스트 (실시간 시세 병합됨) */ holdings: DashboardHoldingItem[]; /** 현재 선택된 종목의 심볼 (없으면 null) */ selectedSymbol: string | null; /** 데이터 로딩 상태 */ isLoading: boolean; /** 에러 메시지 (없으면 null) */ error: string | null; /** 섹션 재조회 핸들러 */ onRetry?: () => void; /** 종목 선택 시 호출되는 핸들러 */ onSelect: (symbol: string) => void; } /** * [컴포넌트] 보유 종목 리스트 * 사용자의 잔고 정보를 바탕으로 실시간 시세가 반영된 종목 카드 목록을 렌더링합니다. * * @param props HoldingsListProps * @see DashboardContainer.tsx - 좌측 메인 영역에서 실시간 병합 데이터를 전달받아 호출 * @see DashboardContainer.tsx - setSelectedSymbol 핸들러를 onSelect로 전달 */ export function HoldingsList({ holdings, selectedSymbol, isLoading, error, onRetry, onSelect, }: HoldingsListProps) { const router = useRouter(); const setPendingTarget = useTradeNavigationStore( (state) => state.setPendingTarget, ); const handleNavigateToTrade = (holding: DashboardHoldingItem) => { setPendingTarget({ symbol: holding.symbol, name: holding.name, market: holding.market, }); router.push("/trade"); }; return ( {/* ========== 카드 헤더: 타이틀 및 설명 ========== */} 보유 종목 현재 보유 중인 종목을 선택하면 우측 상세가 갱신됩니다. {/* ========== 카드 본문: 상태별 메시지 및 리스트 ========== */} {/* 로딩 중 상태 (데이터가 아직 없는 경우) */} {isLoading && holdings.length === 0 && (

보유 종목을 불러오는 중입니다.

)} {/* 에러 발생 상태 */} {error && (

{error}

한국투자증권 API가 일시적으로 불안정할 수 있습니다. 잠시 후 다시 시도해 주세요.

{onRetry ? ( ) : null}
)} {/* 데이터 없음 상태 */} {!isLoading && holdings.length === 0 && !error && (

보유 종목이 없습니다.

)} {/* 종목 리스트 렌더링 영역 */} {holdings.length > 0 && (
{holdings.map((holding) => ( ))}
)}
); } interface HoldingItemRowProps { /** 개별 종목 정보 */ holding: DashboardHoldingItem; /** 선택 여부 */ isSelected: boolean; /** 클릭 핸들러 */ onSelect: (symbol: string) => void; /** 거래 페이지 이동 핸들러 */ onNavigateToTrade: (holding: DashboardHoldingItem) => void; } /** * [컴포넌트] 보유 종목 개별 행 (아이템) * 종목의 기본 정보와 실시간 시세, 현재 손익 상태를 표시합니다. * * @param props HoldingItemRowProps * @see HoldingsList.tsx - holdings.map 내에서 호출 * @see use-price-flash.ts - 현재가 변경 감지 및 애니메이션 효과 트리거 */ function HoldingItemRow({ holding, isSelected, onSelect, onNavigateToTrade, }: HoldingItemRowProps) { // [State/Hook] 현재가 기반 가격 반짝임 효과 상태 관리 // @see use-price-flash.ts - 현재가 변경 감지 시 symbol을 기준으로 이펙트 발생 여부 결정 const flash = usePriceFlash(holding.currentPrice, holding.symbol); // [UI] 손익 상태에 따른 텍스트 색상 클래스 결정 (상승: red, 하락: blue) const toneClass = getChangeToneClass(holding.profitLoss); return ( ); }