"use client"; import { useCallback, useEffect, useState } from "react"; import { RefreshCw, TrendingDown, TrendingUp } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Skeleton } from "@/components/ui/skeleton"; import { fetchDashboardBalance } from "@/features/dashboard/apis/dashboard.api"; import type { DashboardBalanceSummary, DashboardHoldingItem, } from "@/features/dashboard/types/dashboard.types"; import type { KisRuntimeCredentials } from "@/features/settings/store/use-kis-runtime-store"; import { cn } from "@/lib/utils"; interface HoldingsPanelProps { credentials: KisRuntimeCredentials; } /** 천단위 포맷 */ function fmt(v: number) { return Number.isFinite(v) ? v.toLocaleString("ko-KR") : "0"; } /** 수익률 색상 */ function profitClass(v: number) { if (v > 0) return "text-red-500"; if (v < 0) return "text-blue-600 dark:text-blue-400"; return "text-muted-foreground"; } /** * @description 매매창 하단에 보유 종목 및 평가손익 현황을 표시합니다. * @see features/trade/components/layout/TradeDashboardContent.tsx - holdingsPanel prop으로 DashboardLayout에 전달 * @see features/dashboard/apis/dashboard.api.ts - fetchDashboardBalance API 호출 */ export function HoldingsPanel({ credentials }: HoldingsPanelProps) { // [State] 잔고/보유종목 데이터 const [summary, setSummary] = useState(null); const [holdings, setHoldings] = useState([]); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [isExpanded, setIsExpanded] = useState(true); /** * UI 흐름: HoldingsPanel 마운트 or 새로고침 버튼 -> loadBalance -> fetchDashboardBalance API -> * 응답 -> summary/holdings 상태 업데이트 -> 테이블 렌더링 */ const loadBalance = useCallback(async () => { setIsLoading(true); setError(null); try { const data = await fetchDashboardBalance(credentials); setSummary(data.summary); setHoldings(data.holdings); } catch (err) { setError( err instanceof Error ? err.message : "잔고 조회 중 오류가 발생했습니다.", ); } finally { setIsLoading(false); } }, [credentials]); // [Effect] 컴포넌트 마운트 시 잔고 조회 useEffect(() => { loadBalance(); }, [loadBalance]); return (
{/* ========== HOLDINGS HEADER ========== */}
{/* 요약 배지: 수익/손실 */} {summary && !isLoading && (
= 0 ? "bg-red-50 text-red-600 dark:bg-red-900/25 dark:text-red-400" : "bg-blue-50 text-blue-600 dark:bg-blue-900/25 dark:text-blue-400", )} > {summary.totalProfitLoss >= 0 ? ( ) : ( )} {summary.totalProfitLoss >= 0 ? "+" : ""} {fmt(summary.totalProfitLoss)}원 ( {summary.totalProfitRate >= 0 ? "+" : ""} {summary.totalProfitRate.toFixed(2)}%)
)}
{/* 새로고침 버튼 */}
{/* ========== HOLDINGS CONTENT ========== */} {isExpanded && (
{/* 요약 바 */} {summary && !isLoading && (
= 0 ? "+" : ""}${fmt(summary.totalProfitLoss)}원`} tone={ summary.totalProfitLoss > 0 ? "profit" : summary.totalProfitLoss < 0 ? "loss" : "neutral" } /> = 0 ? "+" : ""}${summary.totalProfitRate.toFixed(2)}%`} tone={ summary.totalProfitRate > 0 ? "profit" : summary.totalProfitRate < 0 ? "loss" : "neutral" } />
)} {/* 로딩 상태 */} {isLoading && } {/* 에러 상태 */} {!isLoading && error && (
{error}
)} {/* 보유 종목 없음 */} {!isLoading && !error && holdings.length === 0 && (
보유 중인 종목이 없습니다.
)} {/* 보유 종목 테이블 */} {!isLoading && !error && holdings.length > 0 && (
{/* 테이블 헤더 */}
종목명
보유수량
평균단가
현재가
평가손익
수익률
{/* 종목 행 */} {holdings.map((holding) => ( ))}
)}
)}
); } /** 요약 항목 */ function SummaryItem({ label, value, tone, }: { label: string; value: string; tone?: "profit" | "loss" | "neutral"; }) { return (

{label}

{value}

); } /** 보유 종목 행 */ function HoldingRow({ holding }: { holding: DashboardHoldingItem }) { return (
{/* 종목명 */}

{holding.name}

{holding.symbol} · {holding.market}

{/* 보유수량 */}
{fmt(holding.quantity)}주
{/* 평균단가 */}
{fmt(holding.averagePrice)}
{/* 현재가 */}
{fmt(holding.currentPrice)}
{/* 평가손익 */}
{holding.profitLoss >= 0 ? "+" : ""} {fmt(holding.profitLoss)}
{/* 수익률 */}
{holding.profitRate >= 0 ? "+" : ""} {holding.profitRate.toFixed(2)}%
); } /** 로딩 스켈레톤 */ function HoldingsSkeleton() { return (
{[1, 2, 3].map((i) => (
))}
); }