/** * PortfolioPanel.tsx — 보유 종목 패널 컴포넌트 * * 역할: * - 보유 종목 목록을 데이트레이딩/스윙 그룹별로 구분 표시 * - 각 종목의 수익률, 현재가, 평균단가, 전량 청산 버튼 제공 * - 실시간 가격(livePrices)으로 수익률 실시간 갱신 * * 최적화: * - PortfolioItem, PortfolioPanel 모두 React.memo 적용 * - 가격 변동 시에도 선택된 종목만 시각적 변화 (border-l-2 하이라이트) * * 한국/미국 시장 구분: * - 종목 코드가 6자리 숫자면 KR, 그 외는 US */ import React from 'react' import { Activity, TrendingUp, Clock } from 'lucide-react' import { Position } from '../types/trading' import { isUsStock } from '../utils/stockUtils' import AccountSetupNotice from './AccountSetupNotice' import DayTradingStatusBar from './DayTradingStatusBar' /** * 가격 포맷 — 시장에 따라 원화 또는 달러 표시 * @param price - 가격 * @param isUs - 미국 주식 여부 */ const formatPrice = (price: number, isUs: boolean): string => { if (isUs) { return `$${price.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}` } return `${price.toLocaleString()}원` } /** PortfolioItem Props 정의 */ interface PortfolioItemProps { /** 보유 종목 데이터 */ stock: Position /** 실시간 가격 맵 (종목코드 → 현재가) */ livePrices: Record | undefined /** 현재 선택된 종목 코드 (하이라이트용) */ selectedStock: string /** 종목 선택 콜백 */ onSelectStock: (code: string) => void /** AI 분석 새로고침 콜백 (이미 선택된 종목 클릭 시) */ onRefetchAiAnalysis: () => void /** 전량 청산(시장가 매도) 콜백 */ onSell: (stockCode: string, stockName: string, quantity: number) => Promise /** true면 청산 버튼 비활성화 (예: 사용자 모드 확정 전) */ sellDisabled?: boolean } /** * PortfolioItem — 개별 보유 종목 카드 * * 표시 정보: 종목명, KR/US 마켓 배지, 데이트레이딩 배지, 수익률, * 종목코드, 수량, 평균단가, 현재가, 전량 청산 버튼 * * 동작: * - 클릭: 해당 종목 선택 (차트/호가/AI분석 전환) * - 이미 선택된 종목 클릭: AI 분석 새로고침 * - 전량 청산 버튼: 시장가 매도 요청 */ const PortfolioItem = React.memo(({ stock, livePrices, selectedStock, onSelectStock, onRefetchAiAnalysis, onSell, sellDisabled, }: PortfolioItemProps) => { // 현재가: 실시간 가격 우선, 없으면 포트폴리오 API 가격 사용 const currentPrice = livePrices?.[stock.stock_code] ?? Number(stock.current_price || 0) const avgPrice = Number(stock.average_price || 0) const quantity = Number(stock.quantity || 0) // 수익률: 평균단가 기반 실시간 계산 (평균단가 없으면 API 값 폴백) const profitLossRate = avgPrice > 0 ? ((currentPrice - avgPrice) / avgPrice) * 100 : Number(stock.profit_loss_rate || 0) const isUs = isUsStock(stock.stock_code) const isSelected = selectedStock === stock.stock_code const isUp = profitLossRate >= 0 const handleSellClick = async (e: React.MouseEvent) => { e.stopPropagation() if (sellDisabled) return try { await onSell(stock.stock_code, stock.stock_name, quantity) } catch (error: unknown) { const err = error as any // 계좌 미등록/미검증(400/423)은 Dashboard 상단 인라인 안내로 처리 — alert 생략 if (err?.isAccountSetupRequired) return alert(`매도 실패: ${err.response?.data?.detail || err.message}`) } } return (
{ if (isSelected) onRefetchAiAnalysis() else onSelectStock(stock.stock_code) }} className="px-3 pt-2 pb-1 cursor-pointer" > {/* Row 1: Name + P&L% */}
{stock.stock_name} {isUs ? 'US' : 'KR'} {stock.strategy_type === 'daytrading' && ( 데이 )}
{profitLossRate > 0 ? '+' : ''}{profitLossRate.toFixed(2)}%
{/* Row 2: Code + qty */}
{stock.stock_code} {quantity}{isUs ? 'shares' : '주'}
{/* Row 3: Avg → Current */}
{formatPrice(avgPrice, isUs)} {formatPrice(currentPrice, isUs)}
{/* Sell button */}
) }) PortfolioItem.displayName = 'PortfolioItem' /** PortfolioPanel Props 정의 */ interface PortfolioPanelProps { /** 보유 종목 배열 (마켓 필터 적용 후) */ portfolio: Position[] | undefined /** 실시간 가격 맵 */ livePrices: Record | undefined /** 현재 선택된 종목 코드 */ selectedStock: string /** 종목 선택 콜백 */ onSelectStock: (code: string) => void /** AI 분석 새로고침 콜백 */ onRefetchAiAnalysis: () => void /** 전량 청산 콜백 */ onSell: (stockCode: string, stockName: string, quantity: number) => Promise /** true면 청산 버튼 비활성화 (예: 사용자 모드 확정 전) */ sellDisabled?: boolean /** true면 KIS 계좌 미등록/미검증 — 빈 목록 대신 계좌 설정 안내 표시 */ accountSetupRequired?: boolean /** 계좌 미등록/미검증 안내 문구 (accountSetupRequired=true일 때만 사용) */ accountSetupMessage?: string } /** * PortfolioPanel — 보유 종목 목록 패널 * * 그룹 구분: * - 데이트레이딩: strategy_type === 'daytrading' (주황색 배지) * - 스윙: 그 외 (보라색 배지) */ const PortfolioPanel = React.memo(({ portfolio, livePrices, selectedStock, onSelectStock, onRefetchAiAnalysis, onSell, sellDisabled, accountSetupRequired, accountSetupMessage, }: PortfolioPanelProps) => { const dayStocks = portfolio?.filter(s => s.strategy_type === 'daytrading') ?? [] const swingStocks = portfolio?.filter(s => s.strategy_type !== 'daytrading') ?? [] const total = portfolio?.length ?? 0 const renderGroup = (stocks: Position[], label: string, icon: React.ReactNode, color: string) => { if (stocks.length === 0) return null return ( <>
{icon} {label} {stocks.length}
{stocks.map((stock: Position) => ( ))} ) } return (
{/* Header */}

보유 종목

{dayStocks.length > 0 && ( 데이 {dayStocks.length} )} {swingStocks.length > 0 && ( 스윙 {swingStocks.length} )} {total}
{/* List */}
{accountSetupRequired ? (
) : total > 0 ? ( <> {renderGroup(dayStocks, '데이트레이딩', , 'text-[#f97316]')} {renderGroup(swingStocks, '스윙', , 'text-[#a78bfa]')} ) : (

보유 중인 종목이 없습니다.

)}
{/* 데이트레이딩 엔진 상태 (감시/ORB/포지션/당일손익) */} {!accountSetupRequired && }
) }) PortfolioPanel.displayName = 'PortfolioPanel' export default PortfolioPanel