DB오류 처리중

This commit is contained in:
2026-02-23 16:01:17 +09:00
parent 138bf494db
commit 6d9c9cb010
12 changed files with 368 additions and 109 deletions

View File

@@ -1,4 +1,4 @@
import { useEffect, useState, useRef } from 'react'
import { useEffect, useState, useRef, useMemo } from 'react'
import { useQuery, useQueryClient } from '@tanstack/react-query'
import api from '../lib/api'
import { useWebSocket } from '../hooks/useWebSocket'
@@ -7,6 +7,12 @@ import { TradingChart } from '../components/TradingChart'
import { Activity, Brain, RefreshCw } from 'lucide-react'
import './Dashboard.css'
const formatTime = (isoStr?: string) => {
if (!isoStr) return null;
const d = new Date(isoStr);
return d.toLocaleTimeString('ko-KR', { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false });
};
// 마켓 운영 시간 체크 (한국장 + 미국장)
const checkMarketOpen = () => {
const now = new Date();
@@ -89,7 +95,7 @@ const Dashboard = () => {
return data;
},
refetchInterval: isMarketOpen ? 10000 : false,
refetchInterval: isMarketOpen ? 600000 : false, // 10분마다 배치 실행
// 장외에도 초기 로드는 실행 (DB 조회는 빠름)
})
@@ -195,6 +201,29 @@ const Dashboard = () => {
}
}, [wsMessages, queryClient])
// 보유종목 + 분석종목 전체 코드 수집 (현재가 일괄 조회용)
const allStockCodes = useMemo(() => {
const codes = new Set<string>()
;(portfolio || []).forEach((s: any) => codes.add(s.stock_code))
;(ruleBasedRecommendations?.analyzed_stock_list || []).forEach((s: any) => codes.add(s.stock_code))
return Array.from(codes)
}, [portfolio, ruleBasedRecommendations])
// 현재가 일괄 실시간 조회 (보유종목·분석종목 동기화)
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 ? 10000 : false,
staleTime: 5000,
})
// 보유 종목 코드 Set (추천 목록 필터링용)
const heldStockCodes = new Set((portfolio || []).map((s: any) => s.stock_code))
@@ -230,7 +259,8 @@ const Dashboard = () => {
// }, [isMarketOpen, queryClient])
const { data: aiAnalysisData, refetch: refetchAiAnalysis } = useQuery({
queryKey: ['ai-analysis', selectedStock],
// orderbook?.stock_name을 key에 포함: 호가 데이터 로드 후 종목명이 생기면 재조회
queryKey: ['ai-analysis', selectedStock, orderbook?.stock_name || ''],
queryFn: async () => {
if (!selectedStock) return null;
console.log('🤖 AI 분석 요청:', selectedStock);
@@ -240,8 +270,15 @@ const Dashboard = () => {
const response = await api.get(`/market-data/ai-analysis/${selectedStock}`);
const data = response.data;
// 분석 텍스트 생성
let analysis = `📈 ${data.stock_name} (${data.stock_code})\n\n`;
// 분석 텍스트 생성 (종목명 없으면 여러 소스에서 순차 폴백)
// trim(): KIS API가 공백 문자열을 반환할 경우 대비
const stockNameFallback =
orderbook?.stock_name?.trim() ||
portfolio?.find((s: any) => s.stock_code === selectedStock)?.stock_name?.trim() ||
(ruleBasedRecommendations?.analyzed_stock_list || []).find((s: any) => s.stock_code === selectedStock)?.stock_name?.trim() ||
''
const displayName = data.stock_name?.trim() || stockNameFallback || selectedStock
let analysis = `📈 ${displayName} (${data.stock_code})\n\n`;
// 현재가 & 거래량
analysis += `💰 현재가: ${data.current_price.toLocaleString()}원\n`;
@@ -379,11 +416,13 @@ const Dashboard = () => {
<div className="divide-y divide-gray-700/50 overflow-y-auto custom-scrollbar flex-1">
{portfolio && portfolio.length > 0 ? (
portfolio.map((stock: any) => {
const currentPrice = Number(stock.current_price || 0);
const currentPrice = livePrices?.[stock.stock_code] ?? Number(stock.current_price || 0);
const avgPrice = Number(stock.average_price || 0);
const quantity = Number(stock.quantity || 0);
const totalValue = currentPrice * quantity;
const profitLossRate = Number(stock.profit_loss_rate || 0);
const profitLossRate = avgPrice > 0
? ((currentPrice - avgPrice) / avgPrice) * 100
: Number(stock.profit_loss_rate || 0);
const handleSell = async (e: React.MouseEvent) => {
e.stopPropagation();
@@ -508,10 +547,17 @@ const Dashboard = () => {
<div className="flex-1 grid grid-cols-2 gap-4 min-h-0">
{/* AI 투자 분석 */}
<div className="bg-gray-800/50 backdrop-blur-sm border border-gray-700/50 rounded-xl p-4 shadow-lg flex flex-col">
<h3 className="font-bold flex items-center gap-2 mb-2 text-purple-400 text-sm">
<Brain size={16} />
AI 투자 분석
</h3>
<div className="flex justify-between items-center mb-2">
<h3 className="font-bold flex items-center gap-2 text-purple-400 text-sm">
<Brain size={16} />
AI 투자 분석
</h3>
{aiAnalysisData?.data?.analyzed_at && (
<span className="text-[10px] text-gray-500 font-mono">
{formatTime(aiAnalysisData.data.analyzed_at)}
</span>
)}
</div>
<div className="bg-gray-900/50 rounded-lg p-3 text-gray-300 text-sm leading-relaxed overflow-y-auto custom-scrollbar flex-1 border border-gray-700/30 whitespace-pre-wrap">
{aiAnalysisText ? (
typeof aiAnalysisText === 'string'
@@ -535,7 +581,15 @@ const Dashboard = () => {
{orderbook ? (() => {
// 현재가 주변 ±10단계를 10원 단위로 연속 생성
const currentPrice = orderbook.current_price;
const priceUnit = 10;
// KRX 호가 단위 (코스피/코스닥 공통)
const priceUnit =
currentPrice < 1000 ? 1
: currentPrice < 5000 ? 5
: currentPrice < 10000 ? 10
: currentPrice < 50000 ? 50
: currentPrice < 100000 ? 100
: currentPrice < 500000 ? 500
: 1000;
const steps = 10;
// 매도 호가: 현재가 + 10 ~ 현재가 + 100 (높은 가격부터)
@@ -635,9 +689,16 @@ const Dashboard = () => {
<Brain className="w-4 h-4 text-purple-400" />
분석 종목
</h3>
<span className="text-xs bg-purple-500/20 text-purple-300 px-2 py-0.5 rounded-full">
{ruleBasedRecommendations?.analyzed_stocks || 0}
</span>
<div className="flex items-center gap-2">
{ruleBasedRecommendations?.batch_time && (
<span className="text-[10px] text-gray-500 font-mono">
{formatTime(ruleBasedRecommendations.batch_time)}
</span>
)}
<span className="text-xs bg-purple-500/20 text-purple-300 px-2 py-0.5 rounded-full">
{ruleBasedRecommendations?.analyzed_stocks || 0}
</span>
</div>
</div>
<div className="divide-y divide-gray-700/50 overflow-y-auto custom-scrollbar flex-1">
{ruleBasedRecommendations === undefined ? (
@@ -689,7 +750,7 @@ const Dashboard = () => {
</div>
</div>
<div className="flex justify-between items-center text-[9px] text-gray-500 mt-0.5">
<span>{Math.round(stock.current_price || 0).toLocaleString()}원</span>
<span>{Math.round(livePrices?.[stock.stock_code] ?? stock.current_price ?? 0).toLocaleString()}원</span>
<span className="flex gap-2">
{stock.rsi != null && <span>RSI {stock.rsi.toFixed(0)}</span>}
{stock.stochastic_k != null && <span>K {stock.stochastic_k.toFixed(0)}</span>}