diff --git a/requirements.txt b/requirements.txt index 618ed54d..8c7cda8d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -41,3 +41,4 @@ chainlit rich questionary langgraph==0.4.8 +daphne \ No newline at end of file diff --git a/web/backend/apps/websocket/apps.py b/web/backend/apps/websocket/apps.py index 3be36af2..70c31e36 100644 --- a/web/backend/apps/websocket/apps.py +++ b/web/backend/apps/websocket/apps.py @@ -1,6 +1,6 @@ from django.apps import AppConfig - + class WebsocketConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'apps.websocket' diff --git a/web/backend/apps/websocket/consumers.py b/web/backend/apps/websocket/consumers.py index 38d355d4..d324b436 100644 --- a/web/backend/apps/websocket/consumers.py +++ b/web/backend/apps/websocket/consumers.py @@ -1,15 +1,11 @@ import json from channels.generic.websocket import AsyncWebsocketConsumer from channels.db import database_sync_to_async -from django.contrib.auth.models import AnonymousUser from rest_framework_simplejwt.tokens import UntypedToken from rest_framework_simplejwt.exceptions import InvalidToken, TokenError -from django.contrib.auth import get_user_model from django.conf import settings import jwt -User = get_user_model() - class TradingAnalysisConsumer(AsyncWebsocketConsumer): """거래 분석 실시간 업데이트 WebSocket Consumer""" @@ -121,6 +117,10 @@ class TradingAnalysisConsumer(AsyncWebsocketConsumer): @database_sync_to_async def get_user_from_token(self): """JWT 토큰에서 사용자 정보 추출""" + from django.contrib.auth import get_user_model + from django.contrib.auth.models import AnonymousUser + + User = get_user_model() try: # URL에서 토큰 추출 (query parameter 또는 header) token = None @@ -142,13 +142,16 @@ class TradingAnalysisConsumer(AsyncWebsocketConsumer): # JWT 토큰 검증 try: - UntypedToken(token) # 토큰 유효성 검사 + # simplejwt 설정에서 올바른 서명 키와 알고리즘 가져오기 + from rest_framework_simplejwt.settings import api_settings + + UntypedToken(token) # 토큰 기본 구조 검증 # 토큰에서 사용자 ID 추출 decoded_token = jwt.decode( - token, - settings.SECRET_KEY, - algorithms=['HS256'] + token, + api_settings.SIGNING_KEY, # 올바른 서명 키 사용 + algorithms=[api_settings.ALGORITHM] # 올바른 알고리즘 사용 ) user_id = decoded_token.get('user_id') @@ -171,6 +174,7 @@ class TradingAnalysisConsumer(AsyncWebsocketConsumer): def check_session_ownership(self, session_id): """분석 세션 소유권 확인""" try: + # 지연 import from apps.authentication.models import AnalysisSession session = AnalysisSession.objects.get(id=session_id, user=self.user) return True diff --git a/web/backend/reset_and_run.sh b/web/backend/reset_and_run.sh index 3f14df06..36023437 100644 --- a/web/backend/reset_and_run.sh +++ b/web/backend/reset_and_run.sh @@ -2,6 +2,9 @@ echo "🚀 Django 서버 시작 - 데이터베이스 초기화" +# Django 설정 모듈 환경 변수 설정 +export DJANGO_SETTINGS_MODULE=tradingagents_web.settings + # 1. 데이터베이스 초기화 echo "🔄 데이터베이스 초기화 중..." docker exec -i tradingagents_mysql mysql -u root -ppassword -e " @@ -25,6 +28,6 @@ if not User.objects.filter(email='admin@example.com').exists(): print('✅ 관리자: admin@example.com / admin123!'); " -# 4. 서버 시작 +# 4. 서버 시작 (환경 변수와 함께) echo "🎉 서버 시작!" -python manage.py runserver \ No newline at end of file +daphne -b 0.0.0.0 -p 8000 tradingagents_web.asgi:application \ No newline at end of file diff --git a/web/backend/tradingagents_web/asgi.py b/web/backend/tradingagents_web/asgi.py index 3a78e90a..3844394b 100644 --- a/web/backend/tradingagents_web/asgi.py +++ b/web/backend/tradingagents_web/asgi.py @@ -8,12 +8,15 @@ https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/ """ import os +import django from django.core.asgi import get_asgi_application -from channels.routing import ProtocolTypeRouter, URLRouter -from channels.auth import AuthMiddlewareStack -import apps.websocket.routing os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tradingagents_web.settings') +django.setup() + +from channels.auth import AuthMiddlewareStack +from channels.routing import ProtocolTypeRouter, URLRouter +import apps.websocket.routing application = ProtocolTypeRouter({ "http": get_asgi_application(), diff --git a/web/frontend/src/pages/Analysis/Analysis.js b/web/frontend/src/pages/Analysis/Analysis.js index d60b797f..e49cd6bb 100644 --- a/web/frontend/src/pages/Analysis/Analysis.js +++ b/web/frontend/src/pages/Analysis/Analysis.js @@ -1,33 +1,129 @@ -import React from 'react'; -import { Card, Typography } from 'antd'; -import styled from 'styled-components'; +// web/frontend/src/pages/Analysis/Analysis.js -const { Title, Text } = Typography; +import React, { useState, useEffect } from 'react'; +import { Card, Divider, Spin, Alert, Typography } from 'antd'; +import styled from 'styled-components'; +import api from '../../services/api'; +import { useWebSocket } from '../../contexts/WebSocketContext'; +import AnalysisForm from './components/AnalysisForm'; +import AnalysisDisplay from './components/AnalysisDisplay'; + +const { Title, Paragraph } = Typography; const AnalysisContainer = styled.div` - max-width: 800px; + max-width: 900px; margin: 0 auto; + padding: ${props => props.theme.spacing.lg}; `; -const PlaceholderCard = styled(Card)` - text-align: center; - padding: ${props => props.theme.spacing.xl}; - background: linear-gradient(135deg, #f0f2f5 0%, #e6f7ff 100%); +const CustomPageHeader = styled(Card)` + border: none; + background-color: transparent; + .ant-card-body { + padding: 0; + } `; const Analysis = () => { + const [currentSessionId, setCurrentSessionId] = useState(null); + const [analysisStatus, setAnalysisStatus] = useState('idle'); // idle, running, completed, failed + const [error, setError] = useState(null); + const [finalReport, setFinalReport] = useState(null); + + const { subscribeToAnalysis, analysisProgress, messages, clearMessages, clearAnalysisProgress } = useWebSocket(); + + // WebSocket 메시지로부터 상태 업데이트 + useEffect(() => { + if (currentSessionId && analysisProgress[currentSessionId]) { + const progress = analysisProgress[currentSessionId]; + setAnalysisStatus(progress.status); + if (progress.status === 'completed') { + setFinalReport(progress.result); + } else if (progress.status === 'failed') { + setError(progress.error || '분석 중 알 수 없는 오류가 발생했습니다.'); + } + } + }, [analysisProgress, currentSessionId]); + + // 분석 시작 핸들러 + const handleStartAnalysis = async (values) => { + setAnalysisStatus('starting'); + setError(null); + setFinalReport(null); + clearMessages(); + if(currentSessionId) clearAnalysisProgress(currentSessionId); + + try { + const response = await api.post('/api/trading/start-analysis/', values); + const { session_id } = response.data; + + setCurrentSessionId(session_id); + subscribeToAnalysis(session_id); + setAnalysisStatus('running'); + + } catch (err) { + const errorMessage = err.response?.data?.error || '분석 시작에 실패했습니다.'; + setError(errorMessage); + setAnalysisStatus('failed'); + } + }; + + // 새 분석 시작 핸들러 (Display 컴포넌트에서 호출) + const handleNewAnalysis = () => { + if(currentSessionId) clearAnalysisProgress(currentSessionId); + setCurrentSessionId(null); + setAnalysisStatus('idle'); + setError(null); + setFinalReport(null); + clearMessages(); + }; + + const renderContent = () => { + if (analysisStatus === 'starting') { + return