자산API DB캐쉬 사용

This commit is contained in:
2026-02-24 17:36:24 +09:00
parent d2b4b398e5
commit 8c9b4abc72
6 changed files with 116 additions and 45 deletions

View File

@@ -56,7 +56,13 @@ const PortfolioPanel = React.memo(({
className={`w-full p-3 hover:bg-gray-700/30 transition-all duration-200 group ${selectedStock === stock.stock_code ? 'bg-blue-500/10 border-l-2 border-blue-500' : 'border-l-2 border-transparent'}`}
>
<div
onClick={() => { onSelectStock(stock.stock_code); onRefetchAiAnalysis() }}
onClick={() => {
if (selectedStock === stock.stock_code) {
onRefetchAiAnalysis() // 같은 종목 재클릭 → 강제 새로고침
} else {
onSelectStock(stock.stock_code) // 새 종목 → queryKey 변경으로 자동 fetch
}
}}
className="cursor-pointer"
>
<div className="flex justify-between items-start mb-1">

View File

@@ -77,7 +77,13 @@ const RecommendationPanel = React.memo(({
return (
<button
key={`${stock.stock_code}-${idx}`}
onClick={() => { onSelectStock(stock.stock_code); onRefetchAiAnalysis() }}
onClick={() => {
if (selectedStock === stock.stock_code) {
onRefetchAiAnalysis() // 같은 종목 재클릭 → 강제 새로고침
} else {
onSelectStock(stock.stock_code) // 새 종목 → queryKey 변경으로 자동 fetch
}
}}
className={`w-full p-3 text-left hover:bg-gray-700/30 transition-all duration-200 ${selectedStock === stock.stock_code ? 'bg-purple-500/10 border-l-2 border-purple-500' : 'border-l-2 border-transparent'}`}
>
<div className="flex justify-between items-start mb-1">

View File

@@ -55,22 +55,20 @@ const Dashboard = () => {
// 실시간성이 필요한 것만 refetchInterval 유지
// ─────────────────────────────────────────────────────────────
// ✅ 보유종목 요약 자산 - 매매 시에만 갱신 (polling 없음)
const { data: portfolioSummary, error: portfolioSummaryError } = useQuery({
queryKey: ['portfolio-summary'],
queryFn: async () => (await api.get('/portfolio/summary')).data,
staleTime: Infinity,
})
// ✅ 보유종목 목록 - 매매 시에만 갱신 (polling 없음)
const { data: portfolio } = useQuery({
queryKey: ['portfolio'],
// ✅ 보유종목 목록 + 요약 자산 - 단일 API 호출 (lock 경쟁 제거)
const { data: portfolioCombined, error: portfolioSummaryError } = useQuery({
queryKey: ['portfolio-combined'],
queryFn: async () => {
const data = (await api.get('/portfolio')).data;
return data.filter((item: any) => item.quantity > 0);
const data = (await api.get('/portfolio/combined')).data;
return {
holdings: (data.holdings as any[]).filter((item) => item.quantity > 0),
summary: data.summary,
};
},
staleTime: Infinity,
})
const portfolio = portfolioCombined?.holdings
const portfolioSummary = portfolioCombined?.summary
// MCP 상태 - 30초 (UI 정보용, 느려도 무방)
const { data: mcpStatus } = useQuery({
@@ -98,8 +96,7 @@ const Dashboard = () => {
if (data.trades_executed && data.trades_executed.length > 0) {
console.log('✅ 자동매매 체결:', data.trades_executed.length, '건');
queryClient.invalidateQueries({ queryKey: ['trade-history'] });
queryClient.invalidateQueries({ queryKey: ['portfolio'] });
queryClient.invalidateQueries({ queryKey: ['portfolio-summary'] });
queryClient.invalidateQueries({ queryKey: ['portfolio-combined'] });
}
return data;
@@ -189,9 +186,7 @@ const Dashboard = () => {
if (prevLatestTradeIdRef.current !== null && latestId > prevLatestTradeIdRef.current) {
const t = tradeHistory[0];
console.log('✅ 새 매매 감지:', t.stock_name, t.trade_type, '→ 포트폴리오 갱신');
// setTimeout 중복 제거 → 1회만 호출
queryClient.invalidateQueries({ queryKey: ['portfolio'] });
queryClient.invalidateQueries({ queryKey: ['portfolio-summary'] });
queryClient.invalidateQueries({ queryKey: ['portfolio-combined'] });
}
prevLatestTradeIdRef.current = latestId;
}, [tradeHistory, queryClient])
@@ -207,8 +202,7 @@ const Dashboard = () => {
if (latest.type === 'trade_executed') {
console.log('🔔 WebSocket trade_executed → 포트폴리오 즉시 갱신');
queryClient.invalidateQueries({ queryKey: ['portfolio'] });
queryClient.invalidateQueries({ queryKey: ['portfolio-summary'] });
queryClient.invalidateQueries({ queryKey: ['portfolio-combined'] });
queryClient.invalidateQueries({ queryKey: ['trade-history'] });
}
@@ -217,34 +211,22 @@ const Dashboard = () => {
}
}, [wsMessages, queryClient])
// 보유종목 코드 목록 (useMemo로 안정화)
const allStockCodes = useMemo(() => {
return (portfolio || []).map((s: any) => s.stock_code)
// ✅ 현재가 - /combined에서 이미 받은 current_price 활용 (별도 API 호출 불필요)
const livePrices = useMemo<Record<string, number>>(() => {
const prices: Record<string, number> = {}
;(portfolio || []).forEach((s: any) => {
if (s.current_price) prices[s.stock_code] = Number(s.current_price)
})
return prices
}, [portfolio])
// ✅ 현재가 - 보유종목이 있을 때만, 30초 간격
// (portfolio가 변경될 때만 queryKey도 바뀌어 자동 재조회)
const { data: livePrices } = useQuery({
queryKey: ['live-prices', allStockCodes.join(',')],
queryFn: async () => {
if (allStockCodes.length === 0) return {}
const response = await api.get('/market-data/prices', {
params: { codes: allStockCodes.join(',') }
})
return (response.data.prices || {}) as Record<string, number>
},
enabled: allStockCodes.length > 0,
refetchInterval: isMarketOpen ? 30000 : false,
staleTime: 20000,
})
// useMemo로 Set 안정화 → React.memo 패널에 전달 시 레퍼런스 유지
const heldStockCodes = useMemo(
const heldStockCodes = useMemo<Set<string>>(
() => new Set((portfolio || []).map((s: any) => s.stock_code as string)),
[portfolio]
)
const pendingBuyStockCodes = useMemo(
const pendingBuyStockCodes = useMemo<Set<string>>(
() => new Set(
(tradeHistory || [])
.filter((t: any) => t.status === 'PENDING' && t.trade_type === 'BUY')
@@ -267,8 +249,7 @@ const Dashboard = () => {
const handleSell = useCallback(async (stockCode: string, stockName: string, quantity: number) => {
await api.post('/portfolio/sell', { stock_code: stockCode, quantity, price: null })
// 매도 즉시 포트폴리오 갱신
queryClient.invalidateQueries({ queryKey: ['portfolio'] })
queryClient.invalidateQueries({ queryKey: ['portfolio-summary'] })
queryClient.invalidateQueries({ queryKey: ['portfolio-combined'] })
queryClient.invalidateQueries({ queryKey: ['trade-history'] })
console.log(`✅ ${stockName} 매도 주문 완료`)
}, [queryClient])