DB오류 처리중
This commit is contained in:
@@ -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>}
|
||||
|
||||
Reference in New Issue
Block a user