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
-
- ) : (
-
-
-
- )}
-
-
-
-
-
-
-
-
- : }
- onClick={toggleCollapsed}
- style={{
- fontSize: '16px',
- width: 40,
- height: 40,
- }}
- />
-
-
- {menuItems.find(item => item.key === location.pathname)?.label || 'TradingAgents'}
-
-
-
-
- {/* WebSocket 연결 상태 */}
-
- {connected ? (
- <>
-
- 연결됨
- >
- ) : (
- <>
-
-
- {reconnectAttempts > 0
- ? `재연결 중... (${reconnectAttempts}/${maxReconnectAttempts})`
- : '연결 끊김'
- }
-
- >
- )}
-
-
- {/* 알림 아이콘 */}
-
- }
- style={{ fontSize: '16px' }}
- />
-
-
- {/* 사용자 정보 */}
-
-
- }
- style={{
- backgroundColor: '#1890ff',
- cursor: 'pointer'
- }}
- />
- {!collapsed && (
-
- {user?.username || user?.email}
-
- )}
-
-
-
-
-
-
- {children}
-
-
-
- );
-};
-
-export default MainLayout;
\ No newline at end of file
diff --git a/web/frontend/src/components/Loading/Loading.js b/web/frontend/src/components/Loading/Loading.js
deleted file mode 100644
index 3592bcd4..00000000
--- a/web/frontend/src/components/Loading/Loading.js
+++ /dev/null
@@ -1,56 +0,0 @@
-import React from 'react';
-import { Spin } from 'antd';
-import { LoadingOutlined } from '@ant-design/icons';
-import styled from 'styled-components';
-
-const LoadingContainer = styled.div`
- display: flex;
- justify-content: center;
- align-items: center;
- min-height: ${props => props.fullScreen ? '100vh' : '200px'};
- background-color: ${props => props.fullScreen ? props.theme.colors.background : 'transparent'};
-`;
-
-const LoadingContent = styled.div`
- text-align: center;
-`;
-
-const LoadingText = styled.div`
- margin-top: ${props => props.theme.spacing.md};
- color: ${props => props.theme.colors.textSecondary};
- font-size: ${props => props.theme.typography.fontSize.base};
-`;
-
-const CustomIcon = styled(LoadingOutlined)`
- font-size: ${props => props.size || '24px'};
- color: ${props => props.theme.colors.primary};
-`;
-
-const Loading = ({
- size = 'large',
- text = '로딩 중...',
- fullScreen = false,
- spinning = true
-}) => {
- const iconSize = {
- small: '16px',
- default: '20px',
- large: '24px',
- xlarge: '32px'
- };
-
- return (
-
-
- }
- size={size}
- spinning={spinning}
- />
- {text && {text}}
-
-
- );
-};
-
-export default Loading;
\ No newline at end of file
diff --git a/web/frontend/src/contexts/AuthContext.js b/web/frontend/src/contexts/AuthContext.js
deleted file mode 100644
index d6d01ef0..00000000
--- a/web/frontend/src/contexts/AuthContext.js
+++ /dev/null
@@ -1,282 +0,0 @@
-import React, { createContext, useContext, useReducer, useEffect } from 'react';
-import { message } from 'antd';
-import api from '../services/api';
-
-// Auth Action Types
-const AUTH_ACTIONS = {
- LOGIN_START: 'LOGIN_START',
- LOGIN_SUCCESS: 'LOGIN_SUCCESS',
- LOGIN_FAILURE: 'LOGIN_FAILURE',
- LOGOUT: 'LOGOUT',
- REGISTER_START: 'REGISTER_START',
- REGISTER_SUCCESS: 'REGISTER_SUCCESS',
- REGISTER_FAILURE: 'REGISTER_FAILURE',
- UPDATE_USER: 'UPDATE_USER',
- SET_LOADING: 'SET_LOADING',
-};
-
-// Initial State
-const initialState = {
- user: null,
- isAuthenticated: false,
- loading: true,
- error: null,
-};
-
-// Auth Reducer
-const authReducer = (state, action) => {
- switch (action.type) {
- case AUTH_ACTIONS.LOGIN_START:
- case AUTH_ACTIONS.REGISTER_START:
- return {
- ...state,
- loading: true,
- error: null,
- };
-
- case AUTH_ACTIONS.LOGIN_SUCCESS:
- case AUTH_ACTIONS.REGISTER_SUCCESS:
- return {
- ...state,
- user: action.payload.user,
- isAuthenticated: true,
- loading: false,
- error: null,
- };
-
- case AUTH_ACTIONS.LOGIN_FAILURE:
- case AUTH_ACTIONS.REGISTER_FAILURE:
- return {
- ...state,
- user: null,
- isAuthenticated: false,
- loading: false,
- error: action.payload,
- };
-
- case AUTH_ACTIONS.LOGOUT:
- return {
- ...state,
- user: null,
- isAuthenticated: false,
- loading: false,
- error: null,
- };
-
- case AUTH_ACTIONS.UPDATE_USER:
- return {
- ...state,
- user: { ...state.user, ...action.payload },
- };
-
- case AUTH_ACTIONS.SET_LOADING:
- return {
- ...state,
- loading: action.payload,
- };
-
- default:
- return state;
- }
-};
-
-// Create Context
-const AuthContext = createContext();
-
-// Auth Provider Component
-export const AuthProvider = ({ children }) => {
- const [state, dispatch] = useReducer(authReducer, initialState);
-
- // 로컬 스토리지에서 토큰 확인 및 사용자 정보 로드
- useEffect(() => {
- const initAuth = async () => {
- const token = localStorage.getItem('access_token');
-
- if (token) {
- try {
- // API에 토큰 설정
- api.defaults.headers.common['Authorization'] = `Bearer ${token}`;
-
- // 사용자 정보 가져오기
- const response = await api.get('/api/auth/user/');
-
- dispatch({
- type: AUTH_ACTIONS.LOGIN_SUCCESS,
- payload: { user: response.data },
- });
- } catch (error) {
- // 토큰이 유효하지 않으면 제거
- localStorage.removeItem('access_token');
- localStorage.removeItem('refresh_token');
- delete api.defaults.headers.common['Authorization'];
-
- dispatch({ type: AUTH_ACTIONS.LOGOUT });
- }
- } else {
- dispatch({ type: AUTH_ACTIONS.SET_LOADING, payload: false });
- }
- };
-
- initAuth();
- }, []);
-
- // 로그인 함수
- const login = async (email, password) => {
- dispatch({ type: AUTH_ACTIONS.LOGIN_START });
-
- try {
- const response = await api.post('/api/auth/login/', {
- email,
- password,
- });
-
- const { user, tokens } = response.data;
-
- // 토큰 저장
- localStorage.setItem('access_token', tokens.access);
- localStorage.setItem('refresh_token', tokens.refresh);
-
- // API 헤더에 토큰 설정
- api.defaults.headers.common['Authorization'] = `Bearer ${tokens.access}`;
-
- dispatch({
- type: AUTH_ACTIONS.LOGIN_SUCCESS,
- payload: { user },
- });
-
- message.success('로그인이 완료되었습니다.');
- return { success: true };
-
- } catch (error) {
- const errorMessage = error.response?.data?.message ||
- error.response?.data?.detail ||
- '로그인 중 오류가 발생했습니다.';
-
- dispatch({
- type: AUTH_ACTIONS.LOGIN_FAILURE,
- payload: errorMessage,
- });
-
- message.error(errorMessage);
- return { success: false, error: errorMessage };
- }
- };
-
- // 회원가입 함수
- const register = async (userData) => {
- dispatch({ type: AUTH_ACTIONS.REGISTER_START });
-
- try {
- const response = await api.post('/api/auth/register/', userData);
-
- const { user, tokens } = response.data;
-
- // 토큰 저장
- localStorage.setItem('access_token', tokens.access);
- localStorage.setItem('refresh_token', tokens.refresh);
-
- // API 헤더에 토큰 설정
- api.defaults.headers.common['Authorization'] = `Bearer ${tokens.access}`;
-
- dispatch({
- type: AUTH_ACTIONS.REGISTER_SUCCESS,
- payload: { user },
- });
-
- message.success('회원가입이 완료되었습니다.');
- return { success: true };
-
- } catch (error) {
- const errorMessage = error.response?.data?.message ||
- '회원가입 중 오류가 발생했습니다.';
-
- dispatch({
- type: AUTH_ACTIONS.REGISTER_FAILURE,
- payload: errorMessage,
- });
-
- message.error(errorMessage);
- return { success: false, error: error.response?.data };
- }
- };
-
- // 로그아웃 함수
- const logout = () => {
- // 토큰 제거
- localStorage.removeItem('access_token');
- localStorage.removeItem('refresh_token');
-
- // API 헤더에서 토큰 제거
- delete api.defaults.headers.common['Authorization'];
-
- dispatch({ type: AUTH_ACTIONS.LOGOUT });
-
- message.success('로그아웃되었습니다.');
- };
-
- // 사용자 정보 업데이트
- const updateUser = (userData) => {
- dispatch({
- type: AUTH_ACTIONS.UPDATE_USER,
- payload: userData,
- });
- };
-
- // 프로필 업데이트
- const updateProfile = async (profileData) => {
- try {
- const response = await api.put('/api/auth/profile/', profileData);
-
- // 사용자 정보 새로고침
- const userResponse = await api.get('/api/auth/user/');
- updateUser(userResponse.data);
-
- message.success('프로필이 업데이트되었습니다.');
- return { success: true, data: response.data };
-
- } catch (error) {
- const errorMessage = error.response?.data?.message ||
- '프로필 업데이트 중 오류가 발생했습니다.';
- message.error(errorMessage);
- return { success: false, error: error.response?.data };
- }
- };
-
- // OpenAI API 키 검증
- const checkApiKey = async () => {
- try {
- const response = await api.post('/api/auth/check-api-key/');
- return response.data;
- } catch (error) {
- return { valid: false, message: error.response?.data?.message || '검증 실패' };
- }
- };
-
- // Context Value
- const value = {
- ...state,
- login,
- register,
- logout,
- updateUser,
- updateProfile,
- checkApiKey,
- };
-
- return (
-
- {children}
-
- );
-};
-
-// Custom Hook
-export const useAuth = () => {
- const context = useContext(AuthContext);
-
- if (!context) {
- throw new Error('useAuth must be used within an AuthProvider');
- }
-
- return context;
-};
\ No newline at end of file
diff --git a/web/frontend/src/contexts/WebSocketContext.js b/web/frontend/src/contexts/WebSocketContext.js
deleted file mode 100644
index 9f0e069d..00000000
--- a/web/frontend/src/contexts/WebSocketContext.js
+++ /dev/null
@@ -1,263 +0,0 @@
-import React, { createContext, useContext, useEffect, useRef, useState } from 'react';
-import { message } from 'antd';
-import { useAuth } from './AuthContext';
-
-// WebSocket Context
-const WebSocketContext = createContext();
-
-export const WebSocketProvider = ({ children }) => {
- const { user, isAuthenticated } = useAuth();
- const [connected, setConnected] = useState(false);
- const [messages, setMessages] = useState([]);
- const [analysisProgress, setAnalysisProgress] = useState({});
- const wsRef = useRef(null);
- const reconnectTimeoutRef = useRef(null);
- const [reconnectAttempts, setReconnectAttempts] = useState(0);
- const maxReconnectAttempts = 5;
-
- // WebSocket 연결 함수
- const connect = () => {
- if (!isAuthenticated || !user) {
- return;
- }
-
- const token = localStorage.getItem('access_token');
- if (!token) {
- return;
- }
-
- try {
- const wsUrl = `ws://localhost:8000/ws/trading-analysis/?token=${token}`;
- wsRef.current = new WebSocket(wsUrl);
-
- wsRef.current.onopen = () => {
- console.log('WebSocket 연결됨');
- setConnected(true);
- setReconnectAttempts(0);
-
- // 연결 상태 확인용 ping
- sendMessage({ type: 'ping', timestamp: Date.now() });
- };
-
- wsRef.current.onmessage = (event) => {
- try {
- const data = JSON.parse(event.data);
- handleMessage(data);
- } catch (error) {
- console.error('WebSocket 메시지 파싱 오류:', error);
- }
- };
-
- wsRef.current.onclose = (event) => {
- console.log('WebSocket 연결 해제:', event.code, event.reason);
- setConnected(false);
-
- // 자동 재연결 시도 (정상적인 종료가 아닌 경우)
- if (event.code !== 1000 && reconnectAttempts < maxReconnectAttempts) {
- const delay = Math.pow(2, reconnectAttempts) * 1000; // 지수 백오프
-
- reconnectTimeoutRef.current = setTimeout(() => {
- setReconnectAttempts(prev => prev + 1);
- connect();
- }, delay);
- }
- };
-
- wsRef.current.onerror = (error) => {
- console.error('WebSocket 오류:', error);
- setConnected(false);
- };
-
- } catch (error) {
- console.error('WebSocket 연결 실패:', error);
- setConnected(false);
- }
- };
-
- // WebSocket 연결 해제 함수
- const disconnect = () => {
- if (reconnectTimeoutRef.current) {
- clearTimeout(reconnectTimeoutRef.current);
- }
-
- if (wsRef.current) {
- wsRef.current.close(1000, 'User disconnect');
- wsRef.current = null;
- }
-
- setConnected(false);
- setReconnectAttempts(0);
- };
-
- // 메시지 전송 함수
- const sendMessage = (data) => {
- if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
- wsRef.current.send(JSON.stringify(data));
- } else {
- console.warn('WebSocket이 연결되지 않음');
- }
- };
-
- // 분석 세션 구독
- const subscribeToAnalysis = (sessionId) => {
- sendMessage({
- type: 'subscribe_analysis',
- session_id: sessionId
- });
- };
-
- // 메시지 처리 함수
- const handleMessage = (data) => {
- console.log('WebSocket 메시지 수신:', data);
-
- switch (data.type) {
- case 'connection_established':
- console.log('WebSocket 연결 설정됨:', data.message);
- break;
-
- case 'pong':
- // ping에 대한 응답
- break;
-
- case 'analysis_started':
- setAnalysisProgress(prev => ({
- ...prev,
- [data.session_id]: {
- status: 'running',
- message: data.message,
- progress: 0
- }
- }));
- message.info(data.message);
- break;
-
- case 'analysis_progress':
- setAnalysisProgress(prev => ({
- ...prev,
- [data.session_id]: {
- ...prev[data.session_id],
- status: 'running',
- message: data.content,
- agent: data.agent,
- progress: data.progress,
- }
- }));
-
- // 새로운 메시지 추가
- setMessages(prev => [...prev.slice(-50), {
- id: Date.now(),
- timestamp: new Date(),
- type: data.message_type,
- content: data.content,
- agent: data.agent,
- sessionId: data.session_id
- }]);
- break;
-
- case 'analysis_completed':
- setAnalysisProgress(prev => ({
- ...prev,
- [data.session_id]: {
- status: 'completed',
- message: data.message,
- progress: 100,
- result: data.result,
- }
- }));
- message.success(data.message);
- break;
-
- case 'analysis_failed':
- setAnalysisProgress(prev => ({
- ...prev,
- [data.session_id]: {
- status: 'failed',
- message: data.message,
- progress: 0,
- error: data.message
- }
- }));
- message.error(data.message);
- break;
-
- case 'subscription_confirmed':
- console.log(`분석 세션 ${data.session_id} 구독 완료`);
- break;
-
- case 'subscription_failed':
- message.error(data.message);
- break;
-
- case 'error':
- message.error(data.message);
- break;
-
- default:
- console.log('알 수 없는 메시지 타입:', data.type);
- }
- };
-
- // 분석 진행 상황 초기화
- const clearAnalysisProgress = (sessionId) => {
- setAnalysisProgress(prev => {
- const newProgress = { ...prev };
- delete newProgress[sessionId];
- return newProgress;
- });
- };
-
- // 메시지 목록 초기화
- const clearMessages = () => {
- setMessages([]);
- };
-
- // 사용자 인증 상태 변경 시 WebSocket 연결/해제
- useEffect(() => {
- if (isAuthenticated && user) {
- connect();
- } else {
- disconnect();
- }
-
- return () => {
- disconnect();
- };
- }, [isAuthenticated, user]);
-
- // 컴포넌트 언마운트 시 정리
- useEffect(() => {
- return () => {
- disconnect();
- };
- }, []);
-
- // Context 값
- const value = {
- connected,
- messages,
- analysisProgress,
- sendMessage,
- subscribeToAnalysis,
- clearAnalysisProgress,
- clearMessages,
- reconnectAttempts,
- maxReconnectAttempts
- };
-
- return (
-
- {children}
-
- );
-};
-
-// Custom Hook
-export const useWebSocket = () => {
- const context = useContext(WebSocketContext);
-
- if (!context) {
- throw new Error('useWebSocket must be used within a WebSocketProvider');
- }
-
- return context;
-};
\ No newline at end of file
diff --git a/web/frontend/src/index.css b/web/frontend/src/index.css
deleted file mode 100644
index 67169f29..00000000
--- a/web/frontend/src/index.css
+++ /dev/null
@@ -1,22 +0,0 @@
-/* Reset and base styles */
-body {
- margin: 0;
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
- 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
- sans-serif;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
-}
-
-code {
- font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
- monospace;
-}
-
-* {
- box-sizing: border-box;
-}
-
-html, body, #root {
- height: 100%;
-}
\ No newline at end of file
diff --git a/web/frontend/src/index.js b/web/frontend/src/index.js
deleted file mode 100644
index bcafae7b..00000000
--- a/web/frontend/src/index.js
+++ /dev/null
@@ -1,11 +0,0 @@
-import React from 'react';
-import ReactDOM from 'react-dom/client';
-import App from './App';
-import './index.css';
-
-const root = ReactDOM.createRoot(document.getElementById('root'));
-root.render(
-
-
-
-);
\ No newline at end of file
diff --git a/web/frontend/src/pages/Analysis/Analysis.js b/web/frontend/src/pages/Analysis/Analysis.js
deleted file mode 100644
index 950d0a5f..00000000
--- a/web/frontend/src/pages/Analysis/Analysis.js
+++ /dev/null
@@ -1,129 +0,0 @@
-// web/frontend/src/pages/Analysis/Analysis.js
-
-import React, { useState, useEffect } from 'react';
-import { Card, Divider, Spin, Alert, Typography, Row, Col } 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: 100%;
- margin: 0 auto;
- padding: ${props => props.theme.spacing.lg};
-`;
-
-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/', 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
;
- }
-
- if (currentSessionId && (analysisStatus === 'running' || analysisStatus === 'completed' || analysisStatus === 'failed')) {
- return (
- m.sessionId === currentSessionId)}
- finalReport={finalReport}
- onNewAnalysis={handleNewAnalysis}
- />
- );
- }
-
- return ;
- };
-
- return (
-
-
- AI 기반 주식 분석
-
- 관심 있는 종목에 대한 심층 분석을 시작하세요.
-
-
-
-
- {error && !currentSessionId && ( // Show top-level error only when no session is active
- setError(null)}
- style={{ marginBottom: '24px' }}
- />
- )}
-
- {renderContent()}
-
- );
-};
-
-export default Analysis;
\ No newline at end of file
diff --git a/web/frontend/src/pages/Analysis/components/AnalysisDisplay.js b/web/frontend/src/pages/Analysis/components/AnalysisDisplay.js
deleted file mode 100644
index 95371ad5..00000000
--- a/web/frontend/src/pages/Analysis/components/AnalysisDisplay.js
+++ /dev/null
@@ -1,110 +0,0 @@
-import React, { useRef, useEffect } from 'react';
-import { Card, Progress, Timeline, Button, Result, Typography, Empty, Tag } from 'antd';
-import { FileDoneOutlined, RedoOutlined } from '@ant-design/icons';
-import styled from 'styled-components';
-import ReportDisplay from './ReportDisplay';
-
-const { Title, Paragraph, Text } = Typography;
-
-const DisplayCard = styled(Card)`
- border-radius: ${props => props.theme.borderRadius.lg};
- box-shadow: ${props => props.theme.shadows.lg};
- margin-top: ${props => props.theme.spacing.lg};
- border-radius: ${props => props.theme.borderRadius.md};
- background-color: ${props => props.theme.colors.backgroundSecondary};
-`;
-
-const TimelineContainer = styled.div`
- max-height: 400px;
- overflow-y: auto;
- padding: ${props => props.theme.spacing.md};
- border: 1px solid ${props => props.theme.colors.border};
- border-radius: ${props => props.theme.borderRadius.md};
- background-color: ${props => props.theme.colors.backgroundSecondary};
-`;
-
-const ReportContainer = styled.div`
- margin-top: ${props => props.theme.spacing.lg};
-`;
-
-const agentTagColors = {
- market: 'blue',
- social: 'cyan',
- news: 'green',
- fundamentals: 'purple',
-};
-
-const AnalysisDisplay = ({ sessionId, status, progress, messages, finalReport, onNewAnalysis }) => {
- const timelineEndRef = useRef(null);
-
- useEffect(() => {
- timelineEndRef.current?.scrollIntoView({ behavior: 'smooth' });
- }, [messages]);
-
- const renderRunningState = () => (
- <>
- 분석 진행 중...
- AI 분석가들이 정보를 수집하고 분석하고 있습니다. (세션 ID: {sessionId})
-
- {progress?.message}
-
-
-
- {messages.length > 0 ? messages.map(msg => (
-
-
- {msg.agent}
- {new Date(msg.timestamp).toLocaleTimeString()}
-
- {msg.content}
-
- )) : }
-
-
-
- >
- );
-
- const renderCompletedState = () => (
- }
- subTitle="아래에서 생성된 최종 보고서를 확인하세요."
- extra={[
- } onClick={onNewAnalysis}>
- 새 분석 시작
-
- ]}
- />
- );
-
- 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}
-
- ))}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- }
- size="large"
- block
- >
- {loading ? '분석 시작 중...' : '분석 시작'}
-
-
-
-
- );
-};
-
-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) => (
-
- }
- onClick={() => navigate(`/history`)}
- >
- 보기
-
-
- )
- }
- ];
-
- if (loading) {
- return ;
- }
-
- return (
-
- {/* 환영 메시지 */}
-
-
-
-
- 안녕하세요, {user?.first_name || user?.username}님! 👋
-
-
- AI 기반 거래 분석으로 더 나은 투자 결정을 내려보세요.
-
-
-
-
- }
- onClick={() => navigate('/analysis')}
- style={{
- background: 'rgba(255, 255, 255, 0.2)',
- borderColor: 'rgba(255, 255, 255, 0.4)',
- color: 'white'
- }}
- >
- 새 분석 시작
-
- }
- onClick={() => navigate('/history')}
- style={{
- background: 'rgba(255, 255, 255, 0.1)',
- borderColor: 'rgba(255, 255, 255, 0.3)',
- color: 'white'
- }}
- >
- 분석 기록
-
-
-
-
-
- {/* 통계 카드들 */}
-
-
-
- }
- />
-
-
-
-
- }
- 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