diff --git a/.gitignore b/.gitignore index 1d1d0c8b..2581d38a 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,3 @@ eval_data/ *.egg-info/ results/ .env -wallet/ \ No newline at end of file diff --git a/backend/requirements.txt b/backend/requirements.txt index 61b8a863..875b85dd 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -26,13 +26,15 @@ httpcore==1.0.9 httptools==0.6.4 httpx==0.28.1 idna==3.10 -iniconfig==2.1.1 +iniconfig==2.1.0 Jinja2==3.1.6 kombu==5.5.4 markdown-it-py==3.0.0 MarkupSafe==3.0.2 mdurl==0.1.2 mocker==1.1.1 +mysqlclient==2.2.7 +oracledb==3.2.0 packaging==25.0 passlib==1.7.4 pluggy==1.6.0 diff --git a/web/frontend/.gitignore b/frontend/.gitignore similarity index 100% rename from web/frontend/.gitignore rename to frontend/.gitignore diff --git a/web/frontend/package-lock.json b/frontend/package-lock.json similarity index 100% rename from web/frontend/package-lock.json rename to frontend/package-lock.json diff --git a/web/frontend/package.json b/frontend/package.json similarity index 100% rename from web/frontend/package.json rename to frontend/package.json diff --git a/web/frontend/public/index.html b/frontend/public/index.html similarity index 100% rename from web/frontend/public/index.html rename to frontend/public/index.html diff --git a/web/backend/config/config.py b/web/backend/config/config.py deleted file mode 100644 index 8b9aaffd..00000000 --- a/web/backend/config/config.py +++ /dev/null @@ -1,18 +0,0 @@ -from functools import lru_cache -from pydantic_settings import BaseSettings, SettingsConfigDict - -class Settings(BaseSettings): - model_config = SettingsConfigDict( - env_file=".env", - env_file_encoding="utf-8", - ) - - DB_USER: str - DB_PASSWORD: str - WALLET_PASSWORD: str - DB_DSN: str - SECRET_KEY: str - -@lru_cache -def get_settings(): - return Settings() \ No newline at end of file diff --git a/web/backend/main.py b/web/backend/main.py deleted file mode 100644 index e69de29b..00000000 diff --git a/web/backend/session/__init__.py b/web/backend/session/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/web/backend/user/__init__.py b/web/backend/user/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/web/backend/utils/containers.py b/web/backend/utils/containers.py deleted file mode 100644 index e69de29b..00000000 diff --git a/web/backend/utils/database.py b/web/backend/utils/database.py deleted file mode 100644 index e69de29b..00000000 diff --git a/web/backend/utils/middlewares.py b/web/backend/utils/middlewares.py deleted file mode 100644 index e69de29b..00000000 diff --git a/web/frontend/src/App.js b/web/frontend/src/App.js deleted file mode 100644 index 2c896455..00000000 --- a/web/frontend/src/App.js +++ /dev/null @@ -1,96 +0,0 @@ -import React from 'react'; -import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'; -import { ConfigProvider } from 'antd'; -import koKR from 'antd/locale/ko_KR'; -import { AuthProvider, useAuth } from './contexts/AuthContext'; -import { WebSocketProvider } from './contexts/WebSocketContext'; -import { ThemeProvider } from 'styled-components'; -import GlobalStyle from './styles/GlobalStyle'; -import theme from './styles/theme'; - -// Components -import Layout from './components/Layout/Layout'; -import Login from './pages/Login/Login'; -import Register from './pages/Register/Register'; -import Dashboard from './pages/Dashboard/Dashboard'; -import Analysis from './pages/Analysis/Analysis'; -import History from './pages/History/History'; -import Profile from './pages/Profile/Profile'; -import Loading from './components/Loading/Loading'; - -// Protected Route Component -const ProtectedRoute = ({ children }) => { - const { user, loading } = useAuth(); - - if (loading) { - return ; - } - - return user ? children : ; -}; - -// Public Route Component (redirect to dashboard if already logged in) -const PublicRoute = ({ children }) => { - const { user, loading } = useAuth(); - - if (loading) { - return ; - } - - return !user ? children : ; -}; - -function App() { - return ( - - - - - - - {/* Public Routes */} - - - - } - /> - - - - } - /> - - {/* Protected Routes */} - - - - - } /> - } /> - } /> - } /> - } /> - - - - - } - /> - - - - - - ); -} - -export default App; \ No newline at end of file diff --git a/web/frontend/src/components/Layout/Layout.js b/web/frontend/src/components/Layout/Layout.js deleted file mode 100644 index c687ec37..00000000 --- a/web/frontend/src/components/Layout/Layout.js +++ /dev/null @@ -1,262 +0,0 @@ -import React, { useState } from 'react'; -import { Layout as AntLayout, Menu, Avatar, Dropdown, Button, Badge, Typography } from 'antd'; -import { - DashboardOutlined, - LineChartOutlined, - HistoryOutlined, - UserOutlined, - LogoutOutlined, - MenuFoldOutlined, - MenuUnfoldOutlined, - BellOutlined, - WifiOutlined, - DisconnectOutlined -} from '@ant-design/icons'; -import { useLocation, useNavigate } from 'react-router-dom'; -import { useAuth } from '../../contexts/AuthContext'; -import { useWebSocket } from '../../contexts/WebSocketContext'; -import styled from 'styled-components'; - -const { Header, Sider, Content } = AntLayout; -const { Title } = Typography; - -const StyledLayout = styled(AntLayout)` - min-height: 100vh; -`; - -const StyledHeader = styled(Header)` - background: ${props => props.theme.colors.background}; - border-bottom: 1px solid ${props => props.theme.colors.borderLight}; - padding: 0 ${props => props.theme.spacing.lg}; - display: flex; - align-items: center; - justify-content: space-between; - position: sticky; - top: 0; - z-index: ${props => props.theme.zIndex.sticky}; -`; - -const StyledSider = styled(Sider)` - background: ${props => props.theme.colors.background}; - border-right: 1px solid ${props => props.theme.colors.borderLight}; - - .ant-layout-sider-trigger { - background: ${props => props.theme.colors.backgroundSecondary}; - border-top: 1px solid ${props => props.theme.colors.borderLight}; - color: ${props => props.theme.colors.text}; - } -`; - -const StyledContent = styled(Content)` - background: ${props => props.theme.colors.backgroundSecondary}; - padding: ${props => props.theme.spacing.lg}; - overflow-y: auto; -`; - -const HeaderLeft = styled.div` - display: flex; - align-items: center; - gap: ${props => props.theme.spacing.md}; -`; - -const HeaderRight = styled.div` - display: flex; - align-items: center; - gap: ${props => props.theme.spacing.md}; -`; - -const Logo = styled.div` - display: flex; - align-items: center; - gap: ${props => props.theme.spacing.sm}; - font-weight: ${props => props.theme.typography.fontWeight.bold}; - font-size: ${props => props.theme.typography.fontSize.lg}; - color: ${props => props.theme.colors.primary}; -`; - -const ConnectionStatus = styled.div` - display: flex; - align-items: center; - gap: ${props => props.theme.spacing.xs}; - font-size: ${props => props.theme.typography.fontSize.sm}; - color: ${props => props.connected ? props.theme.colors.success : props.theme.colors.error}; -`; - -const UserInfo = styled.div` - display: flex; - align-items: center; - gap: ${props => props.theme.spacing.sm}; -`; - -const MainLayout = ({ children }) => { - const [collapsed, setCollapsed] = useState(false); - const { user, logout } = useAuth(); - const { connected, reconnectAttempts, maxReconnectAttempts } = useWebSocket(); - const location = useLocation(); - const navigate = useNavigate(); - - // 메뉴 아이템 정의 - const menuItems = [ - { - key: '/dashboard', - icon: , - label: '대시보드', - }, - { - key: '/analysis', - icon: , - label: '분석 시작', - }, - { - key: '/history', - icon: , - label: '분석 기록', - }, - { - key: '/profile', - icon: , - label: '프로필', - }, - ]; - - // 사용자 드롭다운 메뉴 - const userMenuItems = [ - { - key: 'profile', - icon: , - label: '프로필', - onClick: () => navigate('/profile'), - }, - { - type: 'divider', - }, - { - key: 'logout', - icon: , - label: '로그아웃', - onClick: logout, - }, - ]; - - const handleMenuClick = ({ key }) => { - navigate(key); - }; - - const toggleCollapsed = () => { - setCollapsed(!collapsed); - }; - - return ( - - -
- {!collapsed ? ( - - - TradingAgents - - ) : ( -
- -
- )} -
- - - - - - - - - ]} - /> - ); - - const renderFailedState = () => ( - } onClick={onNewAnalysis}> - 다시 시도 - - ]} - /> - ); - - return ( - - {status === 'running' && renderRunningState()} - {status === 'completed' && renderCompletedState()} - {status === 'failed' && renderFailedState()} - - {status === 'completed' && finalReport && ( - - - - )} - - ); -}; - -export default AnalysisDisplay; \ No newline at end of file diff --git a/web/frontend/src/pages/Analysis/components/AnalysisForm.js b/web/frontend/src/pages/Analysis/components/AnalysisForm.js deleted file mode 100644 index 92f3475d..00000000 --- a/web/frontend/src/pages/Analysis/components/AnalysisForm.js +++ /dev/null @@ -1,144 +0,0 @@ -// web/frontend/src/pages/Analysis/components/AnalysisForm.js - -import React from 'react'; -import { Form, Input, Button, Card, Select, Slider, Checkbox, Row, Col, Typography } from 'antd'; -import { FundOutlined, SendOutlined } from '@ant-design/icons'; -import styled from 'styled-components'; - -const { Title } = Typography; -const { Option } = Select; - -const FormCard = styled(Card)` - border-radius: ${props => props.theme.borderRadius.lg}; - box-shadow: ${props => props.theme.shadows.lg}; -`; - -const analystsOptions = [ - { label: '시장 분석가 (Market)', value: 'market' }, - { label: '소셜 분석가 (Social)', value: 'social' }, - { label: '뉴스 분석가 (News)', value: 'news' }, - { label: '재무 분석가 (Fundamentals)', value: 'fundamentals' }, -]; - -const shallowThinkerOptions = [ - { value: 'gpt-4o-mini', label: 'GPT-4o-mini - 빠르고 효율적인 모델' }, - { value: 'gpt-4.1-nano', label: 'GPT-4.1-nano - 초경량 모델' }, - { value: 'gpt-4.1-mini', label: 'GPT-4.1-mini - 준수한 성능의 컴팩트 모델' }, - { value: 'gpt-4o', label: 'GPT-4o - 표준 모델' }, -]; - -const deepThinkerOptions = [ - { value: 'gpt-4.1-nano', label: 'GPT-4.1-nano - 초경량 모델' }, - { value: 'gpt-4.1-mini', label: 'GPT-4.1-mini - 준수한 성능의 컴팩트 모델' }, - { value: 'gpt-4o', label: 'GPT-4o - 표준 모델' }, - { value: 'o4-mini', label: 'o4-mini - 특화된 소형 추론 모델' }, - { value: 'o3-mini', label: 'o3-mini - 경량 고급 추론 모델' }, - { value: 'o3', label: 'o3 - 전체 고급 추론 모델' }, - { value: 'o1', label: 'o1 - 최상위 추론 및 문제 해결 모델' }, -]; - - -const AnalysisForm = ({ onStartAnalysis, loading }) => { - const [form] = Form.useForm(); - - const handleSubmit = (values) => { - onStartAnalysis(values); - }; - - return ( - - 새 분석 시작 -
- - } - placeholder="예: AAPL, GOOGL, MSFT" - size="large" - /> - - - - - - {analystsOptions.map(option => ( - - {option.label} - - ))} - - - - - - - - - - - - - - - - - - - - - - - - -
-
- ); -}; - -export default AnalysisForm; \ No newline at end of file diff --git a/web/frontend/src/pages/Analysis/components/ReportDisplay.js b/web/frontend/src/pages/Analysis/components/ReportDisplay.js deleted file mode 100644 index 4860fdd2..00000000 --- a/web/frontend/src/pages/Analysis/components/ReportDisplay.js +++ /dev/null @@ -1,46 +0,0 @@ -import React from 'react'; -import { Card, Typography } from 'antd'; -import styled from 'styled-components'; - -const { Title } = Typography; - -const ReportWrapper = styled.div` - padding: ${props => props.theme.spacing.md}; - background-color: ${props => props.theme.colors.background}; -`; - -const SectionCard = styled(Card)` - margin-bottom: ${props => props.theme.spacing.lg}; - border-radius: ${props => props.theme.borderRadius.lg}; - box-shadow: ${props => props.theme.shadows.md}; - & .ant-card-head { - background-color: ${props => props.theme.colors.backgroundSecondary}; - } - & .ant-card-body { - padding-top: 16px; - padding-bottom: 16px; - } -`; - -const ReportDisplay = ({ reportData }) => { - if (!reportData) { - return null; - } - - return ( - - -
-                    {reportData}
-                
-
-
- ); -}; - -export default ReportDisplay; \ No newline at end of file diff --git a/web/frontend/src/pages/Dashboard/Dashboard.js b/web/frontend/src/pages/Dashboard/Dashboard.js deleted file mode 100644 index 9b34368d..00000000 --- a/web/frontend/src/pages/Dashboard/Dashboard.js +++ /dev/null @@ -1,354 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { Card, Row, Col, Statistic, Typography, Button, Table, Tag, Space } from 'antd'; -import { - LineChartOutlined, - TrophyOutlined, - ClockCircleOutlined, - RocketOutlined, - PlayCircleOutlined, - EyeOutlined -} from '@ant-design/icons'; -import { useNavigate } from 'react-router-dom'; -import { useAuth } from '../../contexts/AuthContext'; -import { useWebSocket } from '../../contexts/WebSocketContext'; -import { tradingAPI } from '../../services/api'; -import styled from 'styled-components'; -import Loading from '../../components/Loading/Loading'; - -const { Title, Text } = Typography; - -const DashboardContainer = styled.div` - max-width: 1200px; - margin: 0 auto; -`; - -const WelcomeCard = styled(Card)` - background: linear-gradient(135deg, #1890ff 0%, #722ed1 100%); - color: white; - margin-bottom: ${props => props.theme.spacing.lg}; - border: none; - - .ant-card-body { - padding: ${props => props.theme.spacing.xl}; - } -`; - -const WelcomeContent = styled.div` - display: flex; - justify-content: space-between; - align-items: center; - - @media (max-width: ${props => props.theme.breakpoints.md}) { - flex-direction: column; - text-align: center; - gap: ${props => props.theme.spacing.lg}; - } -`; - -const WelcomeText = styled.div` - h2 { - color: white !important; - margin-bottom: ${props => props.theme.spacing.sm}; - } - - p { - color: rgba(255, 255, 255, 0.85); - font-size: ${props => props.theme.typography.fontSize.lg}; - margin: 0; - } -`; - -const QuickActions = styled.div` - display: flex; - gap: ${props => props.theme.spacing.md}; - - @media (max-width: ${props => props.theme.breakpoints.sm}) { - flex-direction: column; - width: 100%; - } -`; - -const StatsCard = styled(Card)` - height: 100%; - - .ant-statistic-title { - color: ${props => props.theme.colors.textSecondary}; - font-weight: ${props => props.theme.typography.fontWeight.medium}; - } - - .ant-statistic-content { - color: ${props => props.theme.colors.text}; - } -`; - -const RecentAnalysisCard = styled(Card)` - .ant-card-head-title { - font-weight: ${props => props.theme.typography.fontWeight.semibold}; - } -`; - -const Dashboard = () => { - const [loading, setLoading] = useState(true); - const [stats, setStats] = useState({ - totalAnalyses: 0, - runningAnalyses: 0, - completedAnalyses: 0, - thisMonth: 0 - }); - const [recentAnalyses, setRecentAnalyses] = useState([]); - const { user } = useAuth(); - const { connected, analysisProgress } = useWebSocket(); - const navigate = useNavigate(); - - useEffect(() => { - loadDashboardData(); - }, []); - - const loadDashboardData = async () => { - try { - setLoading(true); - - // 분석 기록 가져오기 - const historyResponse = await tradingAPI.getAnalysisHistory(); - const analyses = historyResponse.data.results || []; - - // 실행 중인 분석 가져오기 - const runningResponse = await tradingAPI.getRunningAnalyses(); - const runningAnalyses = runningResponse.data.results || []; - - // 통계 계산 - const totalCount = analyses.length; - const runningCount = runningAnalyses.length; - const completedCount = analyses.filter(a => a.status === 'completed').length; - - // 이번 달 분석 수 - const currentMonth = new Date().getMonth(); - const currentYear = new Date().getFullYear(); - const thisMonthCount = analyses.filter(analysis => { - const analysisDate = new Date(analysis.created_at); - return analysisDate.getMonth() === currentMonth && - analysisDate.getFullYear() === currentYear; - }).length; - - setStats({ - totalAnalyses: totalCount, - runningAnalyses: runningCount, - completedAnalyses: completedCount, - thisMonth: thisMonthCount - }); - - // 최근 분석 5개만 표시 - setRecentAnalyses(analyses.slice(0, 5)); - - } catch (error) { - console.error('대시보드 데이터 로드 실패:', error); - } finally { - setLoading(false); - } - }; - - const getStatusColor = (status) => { - const colors = { - pending: 'orange', - running: 'blue', - completed: 'green', - failed: 'red', - cancelled: 'default' - }; - return colors[status] || 'default'; - }; - - const getStatusText = (status) => { - const texts = { - pending: '대기 중', - running: '실행 중', - completed: '완료', - failed: '실패', - cancelled: '취소됨' - }; - return texts[status] || status; - }; - - const columns = [ - { - title: '종목', - dataIndex: 'ticker', - key: 'ticker', - render: (ticker) => {ticker} - }, - { - title: '분석 날짜', - dataIndex: 'analysis_date', - key: 'analysis_date', - }, - { - title: '상태', - dataIndex: 'status', - key: 'status', - render: (status) => ( - - {getStatusText(status)} - - ) - }, - { - title: '생성일', - dataIndex: 'created_at', - key: 'created_at', - render: (date) => new Date(date).toLocaleDateString('ko-KR') - }, - { - title: '작업', - key: 'actions', - render: (_, record) => ( - - - - ) - } - ]; - - if (loading) { - return ; - } - - return ( - - {/* 환영 메시지 */} - - - - - 안녕하세요, {user?.first_name || user?.username}님! 👋 - - - AI 기반 거래 분석으로 더 나은 투자 결정을 내려보세요. - - - - - - - - - - - {/* 통계 카드들 */} - - - - } - /> - - - - - } - valueStyle={{ color: '#1890ff' }} - /> - - - - - } - valueStyle={{ color: '#52c41a' }} - /> - - - - - } - /> - - - - - {/* 최근 분석 */} - navigate('/history')}> - 모두 보기 - - } - > - {recentAnalyses.length > 0 ? ( - - ) : ( -
- 아직 분석 기록이 없습니다. -
- -
- )} - - - {/* WebSocket 연결 상태 정보 */} - {!connected && ( - - - 실시간 업데이트 연결이 끊어졌습니다. 일부 기능이 제한될 수 있습니다. - - - )} - - ); -}; - -export default Dashboard; \ No newline at end of file diff --git a/web/frontend/src/pages/History/History.js b/web/frontend/src/pages/History/History.js deleted file mode 100644 index a0c6fde8..00000000 --- a/web/frontend/src/pages/History/History.js +++ /dev/null @@ -1,33 +0,0 @@ -import React from 'react'; -import { Card, Typography } from 'antd'; -import styled from 'styled-components'; - -const { Title, Text } = Typography; - -const HistoryContainer = styled.div` - max-width: 1200px; - margin: 0 auto; -`; - -const PlaceholderCard = styled(Card)` - text-align: center; - padding: ${props => props.theme.spacing.xl}; - background: linear-gradient(135deg, #f0f2f5 0%, #e6f7ff 100%); -`; - -const History = () => { - return ( - - - 분석 기록 페이지 - - 여기에 사용자의 모든 분석 기록이 표시됩니다. -
- 테이블 형태로 분석 결과를 확인하고 필터링할 수 있습니다. -
-
-
- ); -}; - -export default History; \ No newline at end of file diff --git a/web/frontend/src/pages/Login/Login.js b/web/frontend/src/pages/Login/Login.js deleted file mode 100644 index 8f6d5f7c..00000000 --- a/web/frontend/src/pages/Login/Login.js +++ /dev/null @@ -1,197 +0,0 @@ -import React, { useState } from 'react'; -import { Form, Input, Button, Card, Typography, Alert, Divider } from 'antd'; -import { UserOutlined, LockOutlined, LineChartOutlined } from '@ant-design/icons'; -import { Link, useNavigate } from 'react-router-dom'; -import { useAuth } from '../../contexts/AuthContext'; -import styled from 'styled-components'; - -const { Title, Text } = Typography; - -const LoginContainer = styled.div` - min-height: 100vh; - display: flex; - align-items: center; - justify-content: center; - background: linear-gradient(135deg, #1890ff 0%, #722ed1 100%); - padding: ${props => props.theme.spacing.lg}; -`; - -const LoginCard = styled(Card)` - width: 100%; - max-width: 400px; - box-shadow: ${props => props.theme.shadows.xl}; - border: none; - border-radius: ${props => props.theme.borderRadius.lg}; -`; - -const LogoSection = styled.div` - text-align: center; - margin-bottom: ${props => props.theme.spacing.xl}; -`; - -const Logo = styled.div` - display: flex; - align-items: center; - justify-content: center; - gap: ${props => props.theme.spacing.sm}; - margin-bottom: ${props => props.theme.spacing.md}; -`; - -const LogoIcon = styled(LineChartOutlined)` - font-size: 32px; - color: ${props => props.theme.colors.primary}; -`; - -const LogoText = styled(Title)` - margin: 0; - color: ${props => props.theme.colors.primary}; - font-weight: ${props => props.theme.typography.fontWeight.bold}; -`; - -const SubTitle = styled(Text)` - color: ${props => props.theme.colors.textSecondary}; - font-size: ${props => props.theme.typography.fontSize.base}; -`; - -const StyledForm = styled(Form)` - .ant-form-item { - margin-bottom: ${props => props.theme.spacing.lg}; - } -`; - -const LoginButton = styled(Button)` - width: 100%; - height: 44px; - font-size: ${props => props.theme.typography.fontSize.base}; - font-weight: ${props => props.theme.typography.fontWeight.medium}; -`; - -const RegisterLink = styled.div` - text-align: center; - margin-top: ${props => props.theme.spacing.lg}; - color: ${props => props.theme.colors.textSecondary}; -`; - -const Login = () => { - const [form] = Form.useForm(); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(''); - const { login } = useAuth(); - const navigate = useNavigate(); - - const handleSubmit = async (values) => { - setLoading(true); - setError(''); - - try { - const result = await login(values.email, values.password); - - if (result.success) { - navigate('/dashboard'); - } else { - setError(result.error || '로그인에 실패했습니다.'); - } - } catch (error) { - setError('로그인 중 오류가 발생했습니다.'); - } finally { - setLoading(false); - } - }; - - const handleFormChange = () => { - if (error) { - setError(''); - } - }; - - return ( - - - - - - TradingAgents - - AI 거래 분석 플랫폼에 로그인하세요 - - - {error && ( - - )} - - - - } - placeholder="이메일을 입력하세요" - autoComplete="email" - /> - - - - } - placeholder="비밀번호를 입력하세요" - autoComplete="current-password" - /> - - - - - 로그인 - - - - - 또는 - - - 계정이 없으신가요?{' '} - - 회원가입 - - - - - ); -}; - -export default Login; \ No newline at end of file diff --git a/web/frontend/src/pages/Profile/Profile.js b/web/frontend/src/pages/Profile/Profile.js deleted file mode 100644 index 12349a5f..00000000 --- a/web/frontend/src/pages/Profile/Profile.js +++ /dev/null @@ -1,33 +0,0 @@ -import React from 'react'; -import { Card, Typography } from 'antd'; -import styled from 'styled-components'; - -const { Title, Text } = Typography; - -const ProfileContainer = styled.div` - max-width: 800px; - margin: 0 auto; -`; - -const PlaceholderCard = styled(Card)` - text-align: center; - padding: ${props => props.theme.spacing.xl}; - background: linear-gradient(135deg, #f0f2f5 0%, #e6f7ff 100%); -`; - -const Profile = () => { - return ( - - - 프로필 설정 페이지 - - 여기에 사용자 프로필 설정 기능이 들어갑니다. -
- 개인정보 수정, OpenAI API 키 설정, 기본 분석 옵션 설정 등이 포함됩니다. -
-
-
- ); -}; - -export default Profile; \ No newline at end of file diff --git a/web/frontend/src/pages/Register/Register.js b/web/frontend/src/pages/Register/Register.js deleted file mode 100644 index 4311a8f8..00000000 --- a/web/frontend/src/pages/Register/Register.js +++ /dev/null @@ -1,298 +0,0 @@ -import React, { useState } from 'react'; -import { Form, Input, Button, Card, Typography, Alert, Divider } from 'antd'; -import { UserOutlined, LockOutlined, MailOutlined, LineChartOutlined } from '@ant-design/icons'; -import { Link, useNavigate } from 'react-router-dom'; -import { useAuth } from '../../contexts/AuthContext'; -import styled from 'styled-components'; - -const { Title, Text } = Typography; - -const RegisterContainer = styled.div` - min-height: 100vh; - display: flex; - align-items: center; - justify-content: center; - background: linear-gradient(135deg, #1890ff 0%, #722ed1 100%); - padding: ${props => props.theme.spacing.lg}; -`; - -const RegisterCard = styled(Card)` - width: 100%; - max-width: 450px; - box-shadow: ${props => props.theme.shadows.xl}; - border: none; - border-radius: ${props => props.theme.borderRadius.lg}; -`; - -const LogoSection = styled.div` - text-align: center; - margin-bottom: ${props => props.theme.spacing.xl}; -`; - -const Logo = styled.div` - display: flex; - align-items: center; - justify-content: center; - gap: ${props => props.theme.spacing.sm}; - margin-bottom: ${props => props.theme.spacing.md}; -`; - -const LogoIcon = styled(LineChartOutlined)` - font-size: 32px; - color: ${props => props.theme.colors.primary}; -`; - -const LogoText = styled(Title)` - margin: 0; - color: ${props => props.theme.colors.primary}; - font-weight: ${props => props.theme.typography.fontWeight.bold}; -`; - -const SubTitle = styled(Text)` - color: ${props => props.theme.colors.textSecondary}; - font-size: ${props => props.theme.typography.fontSize.base}; -`; - -const StyledForm = styled(Form)` - .ant-form-item { - margin-bottom: ${props => props.theme.spacing.md}; - } -`; - -const RegisterButton = styled(Button)` - width: 100%; - height: 44px; - font-size: ${props => props.theme.typography.fontSize.base}; - font-weight: ${props => props.theme.typography.fontWeight.medium}; -`; - -const LoginLink = styled.div` - text-align: center; - margin-top: ${props => props.theme.spacing.lg}; - color: ${props => props.theme.colors.textSecondary}; -`; - -const Register = () => { - const [form] = Form.useForm(); - const [loading, setLoading] = useState(false); - const [errors, setErrors] = useState({}); - const { register } = useAuth(); - const navigate = useNavigate(); - - const handleSubmit = async (values) => { - setLoading(true); - setErrors({}); - - try { - const result = await register({ - email: values.email, - username: values.username, - first_name: values.firstName, - last_name: values.lastName, - password: values.password, - password_confirm: values.confirmPassword, - }); - - if (result.success) { - navigate('/dashboard'); - } else { - if (result.error && typeof result.error === 'object') { - setErrors(result.error); - } else { - setErrors({ general: result.error || '회원가입에 실패했습니다.' }); - } - } - } catch (error) { - setErrors({ general: '회원가입 중 오류가 발생했습니다.' }); - } finally { - setLoading(false); - } - }; - - const handleFormChange = () => { - if (Object.keys(errors).length > 0) { - setErrors({}); - } - }; - - // 에러 메시지 포맷팅 - const getErrorMessage = (fieldName) => { - const error = errors[fieldName]; - if (Array.isArray(error)) { - return error[0]; - } - return error; - }; - - return ( - - - - - - TradingAgents - - AI 거래 분석 플랫폼에 가입하세요 - - - {errors.general && ( - - )} - - - - } - placeholder="이메일을 입력하세요" - autoComplete="email" - /> - - - - } - placeholder="사용자명을 입력하세요" - autoComplete="username" - /> - - -
- - - - - - - -
- - - } - placeholder="비밀번호를 입력하세요" - autoComplete="new-password" - /> - - - ({ - validator(_, value) { - if (!value || getFieldValue('password') === value) { - return Promise.resolve(); - } - return Promise.reject(new Error('비밀번호가 일치하지 않습니다.')); - }, - }), - ]} - > - } - placeholder="비밀번호를 다시 입력하세요" - autoComplete="new-password" - /> - - - - - 회원가입 - - -
- - 또는 - - - 이미 계정이 있으신가요?{' '} - - 로그인 - - -
-
- ); -}; - -export default Register; \ No newline at end of file diff --git a/web/frontend/src/services/api.js b/web/frontend/src/services/api.js deleted file mode 100644 index 07bd8311..00000000 --- a/web/frontend/src/services/api.js +++ /dev/null @@ -1,174 +0,0 @@ -import axios from 'axios'; -import { message } from 'antd'; - -// API 베이스 URL -const BASE_URL = process.env.REACT_APP_API_URL || 'http://localhost:8000'; - -// Axios 인스턴스 생성 -const api = axios.create({ - baseURL: BASE_URL, - timeout: 30000, - headers: { - 'Content-Type': 'application/json', - }, -}); - -// 요청 인터셉터 -api.interceptors.request.use( - (config) => { - // 토큰이 있으면 헤더에 추가 - const token = localStorage.getItem('access_token'); - if (token) { - config.headers.Authorization = `Bearer ${token}`; - } - - console.log(`API 요청: ${config.method?.toUpperCase()} ${config.url}`); - return config; - }, - (error) => { - console.error('API 요청 오류:', error); - return Promise.reject(error); - } -); - -// 응답 인터셉터 -api.interceptors.response.use( - (response) => { - console.log(`API 응답: ${response.config.method?.toUpperCase()} ${response.config.url} - ${response.status}`); - return response; - }, - async (error) => { - const originalRequest = error.config; - - console.error('API 응답 오류:', error.response?.status, error.response?.data); - - // 401 오류 (인증 실패) 처리 - if (error.response?.status === 401 && !originalRequest._retry) { - originalRequest._retry = true; - - const refreshToken = localStorage.getItem('refresh_token'); - - if (refreshToken) { - try { - // 토큰 갱신 시도 - const response = await axios.post( - `${BASE_URL}/api/auth/token/refresh/`, - { refresh: refreshToken } - ); - - const newToken = response.data.access; - localStorage.setItem('access_token', newToken); - - // 원래 요청에 새 토큰 적용 - originalRequest.headers.Authorization = `Bearer ${newToken}`; - api.defaults.headers.common['Authorization'] = `Bearer ${newToken}`; - - return api(originalRequest); - - } catch (refreshError) { - console.error('토큰 갱신 실패:', refreshError); - - // 리프레시 토큰도 만료된 경우 로그아웃 처리 - localStorage.removeItem('access_token'); - localStorage.removeItem('refresh_token'); - delete api.defaults.headers.common['Authorization']; - - // 로그인 페이지로 리디렉션 - window.location.href = '/login'; - - message.error('세션이 만료되었습니다. 다시 로그인해주세요.'); - } - } else { - // 리프레시 토큰이 없는 경우 로그아웃 처리 - localStorage.removeItem('access_token'); - localStorage.removeItem('refresh_token'); - delete api.defaults.headers.common['Authorization']; - - window.location.href = '/login'; - message.error('인증이 필요합니다. 로그인해주세요.'); - } - } - - // 다른 오류들 처리 - if (error.response?.status >= 500) { - message.error('서버 오류가 발생했습니다. 잠시 후 다시 시도해주세요.'); - } else if (error.response?.status === 403) { - message.error('접근 권한이 없습니다.'); - } else if (error.response?.status === 404) { - message.error('요청한 리소스를 찾을 수 없습니다.'); - } - - return Promise.reject(error); - } -); - -// API 함수들 -export const authAPI = { - // 로그인 - login: (email, password) => - api.post('/api/auth/login/', { email, password }), - - // 회원가입 - register: (userData) => - api.post('/api/auth/register/', userData), - - // 사용자 정보 조회 - getUser: () => - api.get('/api/auth/user/'), - - // 프로필 조회 - getProfile: () => - api.get('/api/auth/profile/'), - - // 프로필 업데이트 - updateProfile: (profileData) => - api.put('/api/auth/profile/', profileData), - - // OpenAI API 키 검증 - checkApiKey: () => - api.post('/api/auth/check-api-key/'), - - // OpenAI API 키 제거 - removeApiKey: () => - api.delete('/api/auth/remove-api-key/'), - - // 분석 세션 목록 - getAnalysisSessions: () => - api.get('/api/auth/sessions/'), -}; - -export const tradingAPI = { - // 분석 설정 정보 조회 - getAnalysisConfig: () => - api.get('/api/trading/config/'), - - // 분석 옵션 조회 - getAnalysisOptions: () => - api.get('/api/trading/options/'), - - // 분석 시작 - startAnalysis: (analysisData) => - api.post('/api/trading/start/', analysisData), - - // 분석 상태 조회 - getAnalysisStatus: (sessionId) => - api.get(`/api/trading/status/${sessionId}/`), - - // 분석 취소 - cancelAnalysis: (sessionId) => - api.post(`/api/trading/cancel/${sessionId}/`), - - // 분석 기록 조회 - getAnalysisHistory: () => - api.get('/api/trading/history/'), - - // 분석 보고서 조회 - getAnalysisReport: (sessionId) => - api.get(`/api/trading/report/${sessionId}/`), - - // 실행 중인 분석 조회 - getRunningAnalyses: () => - api.get('/api/trading/running/'), -}; - -export default api; \ No newline at end of file diff --git a/web/frontend/src/styles/GlobalStyle.js b/web/frontend/src/styles/GlobalStyle.js deleted file mode 100644 index 56be736b..00000000 --- a/web/frontend/src/styles/GlobalStyle.js +++ /dev/null @@ -1,338 +0,0 @@ -import { createGlobalStyle } from 'styled-components'; - -const GlobalStyle = createGlobalStyle` - /* Reset and Base Styles */ - * { - margin: 0; - padding: 0; - box-sizing: border-box; - } - - html { - font-size: 16px; - line-height: 1.5; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - } - - body { - font-family: ${props => props.theme.typography.fontFamily}; - color: ${props => props.theme.colors.text}; - background-color: ${props => props.theme.colors.background}; - font-size: ${props => props.theme.typography.fontSize.base}; - line-height: ${props => props.theme.typography.lineHeight.normal}; - } - - /* Link Styles */ - a { - color: ${props => props.theme.colors.primary}; - text-decoration: none; - transition: color ${props => props.theme.transitions.fast}; - - &:hover { - color: ${props => props.theme.colors.primaryHover}; - } - - &:active { - color: ${props => props.theme.colors.primaryActive}; - } - } - - /* Button Reset */ - button { - border: none; - background: none; - cursor: pointer; - font-family: inherit; - } - - /* Input Reset */ - input, textarea, select { - font-family: inherit; - font-size: inherit; - line-height: inherit; - } - - /* Remove outline on focus for non-keyboard users */ - :focus:not(:focus-visible) { - outline: none; - } - - /* Scrollbar Styles */ - ::-webkit-scrollbar { - width: 8px; - height: 8px; - } - - ::-webkit-scrollbar-track { - background: ${props => props.theme.colors.backgroundTertiary}; - border-radius: ${props => props.theme.borderRadius.sm}; - } - - ::-webkit-scrollbar-thumb { - background: ${props => props.theme.colors.border}; - border-radius: ${props => props.theme.borderRadius.sm}; - - &:hover { - background: ${props => props.theme.colors.textSecondary}; - } - } - - /* Ant Design Customizations */ - .ant-layout { - min-height: 100vh; - } - - .ant-layout-header { - background: ${props => props.theme.colors.background}; - border-bottom: 1px solid ${props => props.theme.colors.borderLight}; - padding: 0 ${props => props.theme.spacing.lg}; - display: flex; - align-items: center; - justify-content: space-between; - } - - .ant-layout-sider { - background: ${props => props.theme.colors.background}; - border-right: 1px solid ${props => props.theme.colors.borderLight}; - } - - .ant-layout-content { - background: ${props => props.theme.colors.backgroundSecondary}; - padding: ${props => props.theme.spacing.lg}; - min-height: calc(100vh - 64px); - } - - .ant-menu { - background: transparent; - border-right: none; - } - - .ant-menu-item { - margin: 0; - border-radius: ${props => props.theme.borderRadius.base}; - margin-bottom: ${props => props.theme.spacing.xs}; - - &:hover { - background-color: ${props => props.theme.colors.primaryLight}; - } - } - - .ant-menu-item-selected { - background-color: ${props => props.theme.colors.primaryLight} !important; - - &::after { - display: none; - } - } - - .ant-card { - border-radius: ${props => props.theme.borderRadius.md}; - box-shadow: ${props => props.theme.shadows.sm}; - border: 1px solid ${props => props.theme.colors.borderLight}; - } - - .ant-card-head { - border-bottom: 1px solid ${props => props.theme.colors.borderLight}; - } - - .ant-btn { - border-radius: ${props => props.theme.borderRadius.base}; - font-weight: ${props => props.theme.typography.fontWeight.medium}; - transition: all ${props => props.theme.transitions.fast}; - } - - .ant-btn-primary { - background-color: ${props => props.theme.colors.primary}; - border-color: ${props => props.theme.colors.primary}; - - &:hover, &:focus { - background-color: ${props => props.theme.colors.primaryHover}; - border-color: ${props => props.theme.colors.primaryHover}; - } - - &:active { - background-color: ${props => props.theme.colors.primaryActive}; - border-color: ${props => props.theme.colors.primaryActive}; - } - } - - .ant-input, .ant-input-password, .ant-select-selector { - border-radius: ${props => props.theme.borderRadius.base}; - transition: all ${props => props.theme.transitions.fast}; - - &:hover { - border-color: ${props => props.theme.colors.primaryHover}; - } - - &:focus, &.ant-input-focused, &.ant-select-focused .ant-select-selector { - border-color: ${props => props.theme.colors.primary}; - box-shadow: 0 0 0 2px ${props => props.theme.colors.primaryLight}; - } - } - - .ant-form-item-label > label { - font-weight: ${props => props.theme.typography.fontWeight.medium}; - color: ${props => props.theme.colors.text}; - } - - .ant-table { - border-radius: ${props => props.theme.borderRadius.md}; - } - - .ant-table-thead > tr > th { - background-color: ${props => props.theme.colors.backgroundTertiary}; - border-bottom: 1px solid ${props => props.theme.colors.borderLight}; - font-weight: ${props => props.theme.typography.fontWeight.semibold}; - } - - .ant-table-tbody > tr:hover > td { - background-color: ${props => props.theme.colors.backgroundSecondary}; - } - - .ant-progress-line { - .ant-progress-bg { - transition: all ${props => props.theme.transitions.base}; - } - } - - .ant-tag { - border-radius: ${props => props.theme.borderRadius.base}; - font-weight: ${props => props.theme.typography.fontWeight.medium}; - } - - .ant-notification { - .ant-notification-notice { - border-radius: ${props => props.theme.borderRadius.md}; - box-shadow: ${props => props.theme.shadows.lg}; - } - } - - .ant-message { - .ant-message-notice-content { - border-radius: ${props => props.theme.borderRadius.md}; - box-shadow: ${props => props.theme.shadows.md}; - } - } - - /* Custom Status Colors */ - .status-pending { - color: ${props => props.theme.colors.pending}; - } - - .status-running { - color: ${props => props.theme.colors.running}; - } - - .status-completed { - color: ${props => props.theme.colors.completed}; - } - - .status-failed { - color: ${props => props.theme.colors.failed}; - } - - .status-cancelled { - color: ${props => props.theme.colors.cancelled}; - } - - /* Trading Colors */ - .bullish { - color: ${props => props.theme.colors.bullish}; - } - - .bearish { - color: ${props => props.theme.colors.bearish}; - } - - .neutral { - color: ${props => props.theme.colors.neutral}; - } - - /* Utility Classes */ - .text-center { - text-align: center; - } - - .text-right { - text-align: right; - } - - .text-left { - text-align: left; - } - - .mb-0 { margin-bottom: 0 !important; } - .mb-1 { margin-bottom: ${props => props.theme.spacing.xs} !important; } - .mb-2 { margin-bottom: ${props => props.theme.spacing.sm} !important; } - .mb-3 { margin-bottom: ${props => props.theme.spacing.md} !important; } - .mb-4 { margin-bottom: ${props => props.theme.spacing.lg} !important; } - .mb-5 { margin-bottom: ${props => props.theme.spacing.xl} !important; } - - .mt-0 { margin-top: 0 !important; } - .mt-1 { margin-top: ${props => props.theme.spacing.xs} !important; } - .mt-2 { margin-top: ${props => props.theme.spacing.sm} !important; } - .mt-3 { margin-top: ${props => props.theme.spacing.md} !important; } - .mt-4 { margin-top: ${props => props.theme.spacing.lg} !important; } - .mt-5 { margin-top: ${props => props.theme.spacing.xl} !important; } - - .ml-0 { margin-left: 0 !important; } - .ml-1 { margin-left: ${props => props.theme.spacing.xs} !important; } - .ml-2 { margin-left: ${props => props.theme.spacing.sm} !important; } - .ml-3 { margin-left: ${props => props.theme.spacing.md} !important; } - .ml-4 { margin-left: ${props => props.theme.spacing.lg} !important; } - - .mr-0 { margin-right: 0 !important; } - .mr-1 { margin-right: ${props => props.theme.spacing.xs} !important; } - .mr-2 { margin-right: ${props => props.theme.spacing.sm} !important; } - .mr-3 { margin-right: ${props => props.theme.spacing.md} !important; } - .mr-4 { margin-right: ${props => props.theme.spacing.lg} !important; } - - .p-0 { padding: 0 !important; } - .p-1 { padding: ${props => props.theme.spacing.xs} !important; } - .p-2 { padding: ${props => props.theme.spacing.sm} !important; } - .p-3 { padding: ${props => props.theme.spacing.md} !important; } - .p-4 { padding: ${props => props.theme.spacing.lg} !important; } - - /* Loading Animation */ - @keyframes spin { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg); } - } - - .animate-spin { - animation: spin 1s linear infinite; - } - - /* Fade Animations */ - @keyframes fadeIn { - from { opacity: 0; } - to { opacity: 1; } - } - - @keyframes fadeOut { - from { opacity: 1; } - to { opacity: 0; } - } - - .animate-fade-in { - animation: fadeIn ${props => props.theme.transitions.base}; - } - - .animate-fade-out { - animation: fadeOut ${props => props.theme.transitions.base}; - } - - /* Responsive Utilities */ - @media (max-width: ${props => props.theme.breakpoints.sm}) { - .ant-layout-content { - padding: ${props => props.theme.spacing.md}; - } - - .ant-card { - margin-bottom: ${props => props.theme.spacing.md}; - } - } -`; - -export default GlobalStyle; \ No newline at end of file diff --git a/web/frontend/src/styles/theme.js b/web/frontend/src/styles/theme.js deleted file mode 100644 index 1010b3d2..00000000 --- a/web/frontend/src/styles/theme.js +++ /dev/null @@ -1,211 +0,0 @@ -// 테마 설정 -const theme = { - colors: { - // Primary Colors - primary: '#1890ff', - primaryHover: '#40a9ff', - primaryActive: '#096dd9', - primaryLight: '#e6f7ff', - - // Secondary Colors - secondary: '#722ed1', - secondaryHover: '#9254de', - secondaryActive: '#531dab', - - // Success Colors - success: '#52c41a', - successHover: '#73d13d', - successActive: '#389e0d', - successLight: '#f6ffed', - - // Warning Colors - warning: '#fa8c16', - warningHover: '#ffa940', - warningActive: '#d46b08', - warningLight: '#fff7e6', - - // Error Colors - error: '#ff4d4f', - errorHover: '#ff7875', - errorActive: '#d9363e', - errorLight: '#fff2f0', - - // Info Colors - info: '#1890ff', - infoHover: '#40a9ff', - infoActive: '#096dd9', - infoLight: '#e6f7ff', - - // Neutral Colors - text: '#262626', - textSecondary: '#8c8c8c', - textLight: '#bfbfbf', - textDisabled: '#d9d9d9', - - // Background Colors - background: '#ffffff', - backgroundSecondary: '#fafafa', - backgroundTertiary: '#f5f5f5', - - // Border Colors - border: '#d9d9d9', - borderLight: '#f0f0f0', - borderSecondary: '#e6f7ff', - - // Card & Surface Colors - cardBg: '#ffffff', - surfaceBg: '#fafafa', - - // Trading Specific Colors - bullish: '#52c41a', - bearish: '#ff4d4f', - neutral: '#fa8c16', - - // Analysis Status Colors - pending: '#faad14', - running: '#1890ff', - completed: '#52c41a', - failed: '#ff4d4f', - cancelled: '#8c8c8c', - }, - - typography: { - fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif', - - // Font Sizes - fontSize: { - xs: '12px', - sm: '14px', - base: '16px', - lg: '18px', - xl: '20px', - '2xl': '24px', - '3xl': '28px', - '4xl': '32px', - '5xl': '36px', - }, - - // Font Weights - fontWeight: { - light: 300, - normal: 400, - medium: 500, - semibold: 600, - bold: 700, - }, - - // Line Heights - lineHeight: { - tight: 1.2, - normal: 1.5, - relaxed: 1.75, - }, - }, - - spacing: { - xs: '4px', - sm: '8px', - md: '16px', - lg: '24px', - xl: '32px', - '2xl': '48px', - '3xl': '64px', - '4xl': '96px', - }, - - borderRadius: { - none: '0', - sm: '2px', - base: '6px', - md: '8px', - lg: '12px', - xl: '16px', - '2xl': '24px', - full: '50%', - }, - - shadows: { - none: 'none', - sm: '0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)', - base: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)', - md: '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)', - lg: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)', - xl: '0 25px 50px -12px rgba(0, 0, 0, 0.25)', - }, - - breakpoints: { - xs: '480px', - sm: '576px', - md: '768px', - lg: '992px', - xl: '1200px', - xxl: '1600px', - }, - - zIndex: { - dropdown: 1000, - sticky: 1020, - fixed: 1030, - modalBackdrop: 1040, - modal: 1050, - popover: 1060, - tooltip: 1070, - notification: 1080, - }, - - transitions: { - fast: '150ms ease-in-out', - base: '250ms ease-in-out', - slow: '350ms ease-in-out', - }, - - // Component Specific Themes - components: { - button: { - height: { - sm: '24px', - base: '32px', - lg: '40px', - }, - padding: { - sm: '4px 15px', - base: '4px 15px', - lg: '6px 15px', - }, - }, - - input: { - height: { - sm: '24px', - base: '32px', - lg: '40px', - }, - }, - - card: { - padding: '24px', - borderRadius: '8px', - boxShadow: '0 1px 2px 0 rgba(0, 0, 0, 0.03), 0 1px 6px -1px rgba(0, 0, 0, 0.02), 0 2px 4px 0 rgba(0, 0, 0, 0.02)', - }, - - layout: { - header: { - height: '64px', - background: '#ffffff', - borderBottom: '1px solid #f0f0f0', - }, - sidebar: { - width: '200px', - collapsedWidth: '80px', - background: '#001529', - }, - content: { - padding: '24px', - background: '#fafafa', - minHeight: 'calc(100vh - 64px)', - }, - }, - }, -}; - -export default theme; \ No newline at end of file