[add] frontend. 근데 분석결과 들어갈 때, 불러올 수 없다고 뜰때가 종종있음

This commit is contained in:
김희수 2025-07-07 22:18:00 +09:00
parent 46a1c95af1
commit 938c27a6b9
14 changed files with 2256 additions and 9947 deletions

View File

@ -15,45 +15,66 @@ class AnalysisRepository(IAnalysisRepository):
self.session = session
def find_by_member_id(self, member_id: str) -> list[AnalysisVO] | None:
query = select(Analysis).where(Analysis.member_id == member_id).order_by(Analysis.created_at.desc())
analyses = self.session.exec(query).all()
if not analyses:
return None
return [AnalysisVO(**row_to_dict(analysis)) for analysis in analyses]
try:
query = select(Analysis).where(Analysis.member_id == member_id).order_by(Analysis.created_at.desc())
analyses = self.session.exec(query).all()
if not analyses:
return None
return [AnalysisVO(**row_to_dict(analysis)) for analysis in analyses]
except Exception as e:
logger.error(f"❌ 멤버별 분석 조회 실패: {str(e)}")
self.session.rollback()
raise
def find_by_id(self, analysis_id: str) -> AnalysisVO | None:
analysis = self.session.get(Analysis, analysis_id)
if not analysis:
return None
return AnalysisVO(**row_to_dict(analysis))
try:
analysis = self.session.get(Analysis, analysis_id)
if not analysis:
return None
return AnalysisVO(**row_to_dict(analysis))
except Exception as e:
logger.error(f"❌ 분석 ID로 조회 실패: {str(e)}")
self.session.rollback()
raise
def save(self, analysis: AnalysisVO) -> AnalysisVO:
new_analysis = Analysis(
**analysis.model_dump()
)
self.session.add(new_analysis)
self.session.flush()
analysis.id = new_analysis.id
return analysis
try:
new_analysis = Analysis(
**analysis.model_dump()
)
self.session.add(new_analysis)
self.session.flush()
self.session.refresh(new_analysis)
analysis.id = new_analysis.id
return analysis
except Exception as e:
logger.error(f"❌ 분석 저장 실패: {str(e)}")
self.session.rollback()
raise
def update(self, analysis_vo: AnalysisVO) -> AnalysisVO | None:
analysis = self.session.get(Analysis, analysis_vo.id)
logger.info(f"🔄 분석 업데이트 - Analysis ID: {analysis_vo.id}")
if not analysis:
return None
try:
analysis = self.session.get(Analysis, analysis_vo.id)
logger.info(f"🔄 분석 업데이트 - Analysis ID: {analysis_vo.id}")
if not analysis:
return None
# AnalysisVO의 데이터를 SQLModel 객체에 업데이트
analysis_data = analysis_vo.model_dump(exclude_unset=True)
analysis.updated_at = datetime.now()
analysis.sqlmodel_update(analysis_data)
self.session.flush()
# AnalysisVO의 데이터를 SQLModel 객체에 업데이트
analysis_data = analysis_vo.model_dump(exclude_unset=True)
analysis.updated_at = datetime.now()
analysis.sqlmodel_update(analysis_data)
self.session.add(analysis)
self.session.flush()
self.session.refresh(analysis)
return AnalysisVO(**row_to_dict(analysis))
return AnalysisVO(**row_to_dict(analysis))
except Exception as e:
logger.error(f"❌ 분석 업데이트 실패: {str(e)}")
self.session.rollback()
raise

View File

@ -1,19 +1,82 @@
from functools import lru_cache
from pydantic_settings import BaseSettings, SettingsConfigDict
from pydantic import validator, Field
import secrets
import os
class Settings(BaseSettings):
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
case_sensitive=True,
validate_default=True,
)
# MySQL 데이터베이스 설정
DB_HOST: str
DB_PORT: int
DB_USER: str
DB_PASSWORD: str
DB_NAME: str
SECRET_KEY: str
DB_HOST: str = Field(description="Database host")
DB_PORT: int = Field(ge=1, le=65535, description="Database port")
DB_USER: str = Field(min_length=1, description="Database username")
DB_PASSWORD: str = Field(min_length=1, description="Database password")
DB_NAME: str = Field(min_length=1, description="Database name")
# 보안 설정
SECRET_KEY: str = Field(min_length=32, description="JWT secret key")
ACCESS_TOKEN_EXPIRE_MINUTES: int = Field(default=360, ge=1, description="Access token expiration in minutes")
# CORS 설정
ALLOWED_ORIGINS: str = Field(default="http://localhost:3000", description="Allowed CORS origins (comma-separated)")
# 로깅 설정
LOG_LEVEL: str = Field(default="INFO", description="Logging level")
LOG_FILE: str = Field(default="logs/app.log", description="Log file path")
# API 설정
API_V1_STR: str = Field(default="/api/v1", description="API prefix")
PROJECT_NAME: str = Field(default="TradingAgents API", description="Project name")
# Rate Limiting 설정
RATE_LIMIT_REQUESTS: int = Field(default=100, ge=1, description="Rate limit requests per minute")
RATE_LIMIT_PERIOD: int = Field(default=60, ge=1, description="Rate limit period in seconds")
# 환경 설정
ENVIRONMENT: str = Field(default="development", description="Environment (development/staging/production)")
DEBUG: bool = Field(default=True, description="Debug mode")
@validator('ENVIRONMENT')
def validate_environment(cls, v):
allowed_envs = ['development', 'staging', 'production']
if v not in allowed_envs:
raise ValueError(f'Environment must be one of {allowed_envs}')
return v
@validator('LOG_LEVEL')
def validate_log_level(cls, v):
allowed_levels = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']
if v.upper() not in allowed_levels:
raise ValueError(f'Log level must be one of {allowed_levels}')
return v.upper()
@validator('SECRET_KEY')
def validate_secret_key(cls, v):
if len(v) < 32:
raise ValueError('SECRET_KEY must be at least 32 characters long')
return v
@property
def database_url(self) -> str:
return f"mysql+pymysql://{self.DB_USER}:{self.DB_PASSWORD}@{self.DB_HOST}:{self.DB_PORT}/{self.DB_NAME}?charset=utf8mb4"
@property
def allowed_origins_list(self) -> list[str]:
return [origin.strip() for origin in self.ALLOWED_ORIGINS.split(',')]
@property
def is_production(self) -> bool:
return self.ENVIRONMENT == "production"
@property
def is_development(self) -> bool:
return self.ENVIRONMENT == "development"
@lru_cache
def get_settings():

View File

@ -1,31 +1,129 @@
from fastapi import FastAPI
from fastapi import FastAPI, Request, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.trustedhost import TrustedHostMiddleware
from fastapi.responses import JSONResponse
from utils.database import create_db_and_tables
from utils.containers import Container
from utils.middlewares import RateLimitMiddleware, LoggingMiddleware, SecurityHeadersMiddleware
from utils.exceptions import BaseAPIException
from contextlib import asynccontextmanager
from analysis.interface.controller.analysis_controller import router as analysis_router
from member.interface.controller.member_controller import router as member_router
import logging
from utils.logger import setup_logging
from config.config import get_settings
setup_logging()
settings = get_settings()
app = FastAPI()
app.container = Container()
app.include_router(analysis_router)
app.include_router(member_router)
@app.on_event("startup")
def startup_db_client():
# 라이프사이클 관리
@asynccontextmanager
async def lifespan(app: FastAPI):
logger = logging.getLogger(__name__)
# 시작 시
logger.info("🚀 FastAPI 애플리케이션 시작")
create_db_and_tables()
logger.info("📊 데이터베이스 초기화 완료")
yield
# 종료 시
logger.info("🔄 애플리케이션 종료")
app = FastAPI(
title=settings.PROJECT_NAME,
version="1.0.0",
debug=settings.DEBUG,
lifespan=lifespan
)
# 컨테이너 설정
app.container = Container()
# 미들웨어 설정
app.add_middleware(
CORSMiddleware,
allow_origins=settings.allowed_origins_list,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# 신뢰할 수 있는 호스트 설정
if settings.is_production:
app.add_middleware(TrustedHostMiddleware, allowed_hosts=["tradingagents.com", "*.tradingagents.com"])
# 커스텀 미들웨어 추가
app.add_middleware(SecurityHeadersMiddleware)
app.add_middleware(LoggingMiddleware)
app.add_middleware(
RateLimitMiddleware,
requests_per_minute=settings.RATE_LIMIT_REQUESTS,
period=settings.RATE_LIMIT_PERIOD
)
# 글로벌 예외 처리기
@app.exception_handler(BaseAPIException)
async def api_exception_handler(request: Request, exc: BaseAPIException):
return JSONResponse(
status_code=exc.status_code,
content={
"error": {
"code": exc.error_code,
"message": exc.detail,
"path": str(request.url)
}
}
)
@app.exception_handler(HTTPException)
async def http_exception_handler(request: Request, exc: HTTPException):
return JSONResponse(
status_code=exc.status_code,
content={
"error": {
"code": f"HTTP_{exc.status_code}",
"message": exc.detail,
"path": str(request.url)
}
}
)
@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception):
logger = logging.getLogger(__name__)
logger.error(f"예상치 못한 오류: {str(exc)}", exc_info=True)
return JSONResponse(
status_code=500,
content={
"error": {
"code": "INTERNAL_SERVER_ERROR",
"message": "Internal server error" if settings.is_production else str(exc),
"path": str(request.url)
}
}
)
# 라우터 등록
app.include_router(analysis_router, prefix=settings.API_V1_STR)
app.include_router(member_router, prefix=settings.API_V1_STR)
# 헬스 체크 엔드포인트
@app.get("/health")
async def health_check():
return {
"status": "healthy",
"environment": settings.ENVIRONMENT,
"timestamp": "2024-01-01T00:00:00Z"
}
@app.get("/")
def root():
async def root():
logger = logging.getLogger(__name__)
logger.info("📍 루트 엔드포인트 호출됨")
return {"message": "Trading Agents API"}
return {
"message": "Trading Agents API",
"version": "1.0.0",
"environment": settings.ENVIRONMENT,
"docs_url": "/docs"
}

View File

@ -26,7 +26,7 @@ class CurrentMember(BaseModel):
def __str__(self):
return f"{self.id}({self.role})"
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/members/login")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/members/login")

View File

@ -4,29 +4,81 @@ from sqlmodel import SQLModel, create_engine, Session
from config.config import get_settings
from member.infra.db_models.member import Member
from analysis.infra.db_models.analysis import Analysis
import logging
class DatabaseConnectionError(Exception):
"""데이터베이스 연결 오류"""
pass
logger = logging.getLogger(__name__)
settings = get_settings()
BASE_DIR = Path(__file__).resolve().parent.parent
# MySQL 데이터베이스 URL 구성
DATABASE_URL = f"mysql+pymysql://{settings.DB_USER}:{settings.DB_PASSWORD}@{settings.DB_HOST}:{settings.DB_PORT}/{settings.DB_NAME}?charset=utf8mb4"
# 데이터베이스 연결 설정 개선
engine_config = {
"echo": settings.DEBUG, # 프로덕션에서는 SQL 로그 비활성화
"pool_size": 10, # 연결 풀 크기
"max_overflow": 20, # 최대 초과 연결 수
"pool_pre_ping": True, # 연결 상태 확인
"pool_recycle": 3600, # 1시간마다 연결 재사용
"connect_args": {
"charset": "utf8mb4",
"connect_timeout": 10,
"read_timeout": 30,
"write_timeout": 30,
"init_command": "SET sql_mode='STRICT_TRANS_TABLES'",
}
}
# MySQL 엔진 생성
engine = create_engine(
DATABASE_URL,
echo=True
)
try:
engine = create_engine(settings.database_url, **engine_config)
logger.info("데이터베이스 엔진 생성 완료")
except Exception as e:
logger.error(f"데이터베이스 엔진 생성 실패: {str(e)}")
raise DatabaseConnectionError()
def get_session():
with Session(engine) as session:
"""데이터베이스 세션 생성"""
session = Session(engine)
try:
yield session
session.commit()
except Exception as e:
logger.error(f"데이터베이스 트랜잭션 실패: {str(e)}")
session.rollback()
raise
finally:
session.close()
def create_db_and_tables():
# 테이블 생성
# SQLModel.metadata.drop_all(engine)
SQLModel.metadata.create_all(engine)
"""테이블 생성"""
try:
# 개발 환경에서만 테이블 자동 생성
if not settings.is_production:
SQLModel.metadata.create_all(engine)
logger.info("데이터베이스 테이블 생성 완료")
else:
logger.info("프로덕션 환경 - 테이블 자동 생성 건너뜀")
except Exception as e:
logger.error(f"테이블 생성 실패: {str(e)}")
raise DatabaseConnectionError()
def check_db_connection():
"""데이터베이스 연결 확인"""
try:
with Session(engine) as session:
session.exec("SELECT 1")
logger.info("데이터베이스 연결 확인 완료")
return True
except Exception as e:
logger.error(f"데이터베이스 연결 실패: {str(e)}")
return False
if __name__ == "__main__":
create_db_and_tables()
print(DATABASE_URL)
if check_db_connection():
print("✅ 데이터베이스 연결 성공")
else:
print("❌ 데이터베이스 연결 실패")

201
backend/utils/exceptions.py Normal file
View File

@ -0,0 +1,201 @@
from fastapi import HTTPException, status
from typing import Any, Dict, Optional
import logging
logger = logging.getLogger(__name__)
class BaseAPIException(HTTPException):
"""기본 API 예외 클래스"""
def __init__(
self,
status_code: int,
detail: str,
error_code: str = None,
headers: Optional[Dict[str, Any]] = None
):
super().__init__(status_code=status_code, detail=detail, headers=headers)
self.error_code = error_code or self.__class__.__name__
logger.error(f"API Exception: {self.error_code} - {detail}")
# 인증/권한 관련 예외
class AuthenticationError(BaseAPIException):
"""인증 실패 예외"""
def __init__(self, detail: str = "Authentication failed"):
super().__init__(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=detail,
error_code="AUTH_001"
)
class AuthorizationError(BaseAPIException):
"""권한 부족 예외"""
def __init__(self, detail: str = "Insufficient permissions"):
super().__init__(
status_code=status.HTTP_403_FORBIDDEN,
detail=detail,
error_code="AUTH_002"
)
class InvalidTokenError(BaseAPIException):
"""토큰 오류 예외"""
def __init__(self, detail: str = "Invalid or expired token"):
super().__init__(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=detail,
error_code="AUTH_003"
)
# 비즈니스 로직 관련 예외
class ResourceNotFoundError(BaseAPIException):
"""리소스 찾을 수 없음 예외"""
def __init__(self, resource_type: str, resource_id: str = None):
detail = f"{resource_type} not found"
if resource_id:
detail += f" (ID: {resource_id})"
super().__init__(
status_code=status.HTTP_404_NOT_FOUND,
detail=detail,
error_code="BIZ_001"
)
class DuplicateResourceError(BaseAPIException):
"""중복 리소스 예외"""
def __init__(self, resource_type: str, field: str = None):
detail = f"{resource_type} already exists"
if field:
detail += f" (field: {field})"
super().__init__(
status_code=status.HTTP_409_CONFLICT,
detail=detail,
error_code="BIZ_002"
)
class ValidationError(BaseAPIException):
"""입력 검증 실패 예외"""
def __init__(self, detail: str = "Validation failed", field: str = None):
if field:
detail = f"Validation failed for field: {field} - {detail}"
super().__init__(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=detail,
error_code="VAL_001"
)
class BusinessLogicError(BaseAPIException):
"""비즈니스 로직 오류 예외"""
def __init__(self, detail: str):
super().__init__(
status_code=status.HTTP_400_BAD_REQUEST,
detail=detail,
error_code="BIZ_003"
)
# 분석 관련 예외
class AnalysisError(BaseAPIException):
"""분석 실행 오류 예외"""
def __init__(self, detail: str = "Analysis execution failed"):
super().__init__(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=detail,
error_code="ANALYSIS_001"
)
class AnalysisNotFoundError(ResourceNotFoundError):
"""분석 세션 찾을 수 없음 예외"""
def __init__(self, analysis_id: str = None):
super().__init__("Analysis", analysis_id)
self.error_code = "ANALYSIS_002"
class AnalysisAccessDeniedError(AuthorizationError):
"""분석 접근 권한 없음 예외"""
def __init__(self, analysis_id: str = None):
detail = "Access denied to analysis"
if analysis_id:
detail += f" (ID: {analysis_id})"
super().__init__(detail)
self.error_code = "ANALYSIS_003"
# 멤버 관련 예외
class MemberNotFoundError(ResourceNotFoundError):
"""멤버 찾을 수 없음 예외"""
def __init__(self, member_id: str = None):
super().__init__("Member", member_id)
self.error_code = "MEMBER_001"
class MemberAlreadyExistsError(DuplicateResourceError):
"""멤버 이미 존재 예외"""
def __init__(self, field: str = "email"):
super().__init__("Member", field)
self.error_code = "MEMBER_002"
class InvalidCredentialsError(BaseAPIException):
"""잘못된 로그인 정보 예외"""
def __init__(self):
super().__init__(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid email or password",
error_code="MEMBER_003"
)
# 데이터베이스 관련 예외
class DatabaseError(BaseAPIException):
"""데이터베이스 오류 예외"""
def __init__(self, detail: str = "Database operation failed"):
super().__init__(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=detail,
error_code="DB_001"
)
class DatabaseConnectionError(DatabaseError):
"""데이터베이스 연결 오류 예외"""
def __init__(self):
super().__init__("Database connection failed")
self.error_code = "DB_002"
# 외부 서비스 관련 예외
class ExternalServiceError(BaseAPIException):
"""외부 서비스 오류 예외"""
def __init__(self, service_name: str, detail: str = "External service error"):
super().__init__(
status_code=status.HTTP_502_BAD_GATEWAY,
detail=f"{service_name}: {detail}",
error_code="EXT_001"
)
class TradingAgentsServiceError(ExternalServiceError):
"""TradingAgents 서비스 오류 예외"""
def __init__(self, detail: str = "TradingAgents service error"):
super().__init__("TradingAgents", detail)
self.error_code = "EXT_002"
# 에러 핸들러 유틸리티
def handle_database_error(e: Exception, operation: str = "database operation") -> DatabaseError:
"""데이터베이스 예외를 처리하고 적절한 예외로 변환"""
logger.error(f"Database error during {operation}: {str(e)}", exc_info=True)
# 특정 데이터베이스 오류를 더 구체적인 예외로 변환
error_message = str(e).lower()
if "connection" in error_message:
return DatabaseConnectionError()
elif "duplicate" in error_message or "unique constraint" in error_message:
return DuplicateResourceError("Resource", "unique field")
elif "foreign key" in error_message:
return ValidationError("Referenced resource does not exist")
else:
return DatabaseError(f"Database operation failed: {operation}")
def handle_validation_error(e: Exception, field: str = None) -> ValidationError:
"""입력 검증 예외를 처리"""
logger.warning(f"Validation error: {str(e)}")
return ValidationError(str(e), field)
def handle_business_logic_error(e: Exception, context: str = None) -> BusinessLogicError:
"""비즈니스 로직 예외를 처리"""
detail = str(e)
if context:
detail = f"{context}: {detail}"
logger.error(f"Business logic error: {detail}")
return BusinessLogicError(detail)

View File

@ -0,0 +1,188 @@
from fastapi import Request, Response, HTTPException, status
from starlette.middleware.base import BaseHTTPMiddleware
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from starlette.types import ASGIApp
import time
import logging
from typing import Callable, Dict
from datetime import datetime, timedelta
logger = logging.getLogger(__name__)
class SecurityHeadersMiddleware(BaseHTTPMiddleware):
"""보안 헤더 추가 미들웨어"""
def __init__(self, app: ASGIApp):
super().__init__(app)
async def dispatch(self, request: Request, call_next: Callable) -> Response:
response = await call_next(request)
# 보안 헤더 추가
response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["X-Frame-Options"] = "DENY"
response.headers["X-XSS-Protection"] = "1; mode=block"
response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains"
response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
response.headers["Content-Security-Policy"] = "default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; img-src 'self' data: https://fastapi.tiangolo.com"
return response
class LoggingMiddleware(BaseHTTPMiddleware):
"""요청/응답 로깅 미들웨어"""
def __init__(self, app: ASGIApp):
super().__init__(app)
async def dispatch(self, request: Request, call_next: Callable) -> Response:
start_time = time.time()
# 요청 로깅
logger.info(
f"Request started: {request.method} {request.url.path} "
f"from {request.client.host if request.client else 'unknown'}"
)
try:
response = await call_next(request)
# 응답 로깅
process_time = time.time() - start_time
logger.info(
f"Request completed: {request.method} {request.url.path} "
f"- Status: {response.status_code} - Duration: {process_time:.4f}s"
)
response.headers["X-Process-Time"] = str(process_time)
return response
except Exception as e:
process_time = time.time() - start_time
logger.error(
f"Request failed: {request.method} {request.url.path} "
f"- Error: {str(e)} - Duration: {process_time:.4f}s"
)
raise
class RateLimitMiddleware(BaseHTTPMiddleware):
"""간단한 Rate Limiting 미들웨어"""
def __init__(self, app: ASGIApp, requests_per_minute: int = 100, period: int = 60):
super().__init__(app)
self.requests_per_minute = requests_per_minute
self.period = period
self.requests: Dict[str, list] = {}
def _get_client_ip(self, request: Request) -> str:
"""클라이언트 IP 주소 획득"""
forwarded = request.headers.get("X-Forwarded-For")
if forwarded:
return forwarded.split(",")[0].strip()
return request.client.host if request.client else "unknown"
def _clean_old_requests(self, client_ip: str) -> None:
"""오래된 요청 기록 정리"""
now = datetime.now()
cutoff = now - timedelta(seconds=self.period)
if client_ip in self.requests:
self.requests[client_ip] = [
req_time for req_time in self.requests[client_ip]
if req_time > cutoff
]
async def dispatch(self, request: Request, call_next: Callable) -> Response:
client_ip = self._get_client_ip(request)
now = datetime.now()
# 오래된 요청 정리
self._clean_old_requests(client_ip)
# 해당 클라이언트의 요청 목록 초기화
if client_ip not in self.requests:
self.requests[client_ip] = []
current_requests = len(self.requests[client_ip])
if current_requests >= self.requests_per_minute:
logger.warning(f"Rate limit exceeded for client: {client_ip}")
return JSONResponse(
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
content={
"error": "Rate limit exceeded",
"message": f"Too many requests. Max {self.requests_per_minute} requests per {self.period} seconds.",
"retry_after": self.period
}
)
# 현재 요청 기록
self.requests[client_ip].append(now)
response = await call_next(request)
# Rate limit 정보 헤더 추가
response.headers["X-RateLimit-Limit"] = str(self.requests_per_minute)
response.headers["X-RateLimit-Remaining"] = str(self.requests_per_minute - current_requests - 1)
response.headers["X-RateLimit-Reset"] = str(int((now + timedelta(seconds=self.period)).timestamp()))
return response
class ErrorHandlingMiddleware(BaseHTTPMiddleware):
"""전역 에러 핸들링 미들웨어"""
def __init__(self, app: ASGIApp):
super().__init__(app)
async def dispatch(self, request: Request, call_next: Callable) -> Response:
try:
response = await call_next(request)
return response
except HTTPException as e:
# HTTPException은 FastAPI에서 자동으로 처리되므로 다시 raise
raise e
except Exception as e:
logger.error(f"Unhandled error in request {request.method} {request.url.path}: {str(e)}", exc_info=True)
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content={
"error": "Internal Server Error",
"message": "An unexpected error occurred. Please try again later.",
"request_id": getattr(request.state, 'request_id', 'unknown')
}
)
def setup_cors_middleware(app, allowed_origins: list[str]):
"""CORS 미들웨어 설정"""
app.add_middleware(
CORSMiddleware,
allow_origins=allowed_origins,
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"],
allow_headers=["*"],
expose_headers=["X-Process-Time", "X-RateLimit-Limit", "X-RateLimit-Remaining", "X-RateLimit-Reset"]
)
def setup_all_middlewares(app, settings):
"""모든 미들웨어 설정"""
# 주의: 미들웨어는 나중에 등록된 것부터 먼저 실행됨
# 1. CORS (가장 먼저 실행되어야 함)
setup_cors_middleware(app, settings.allowed_origins_list)
# 2. 보안 헤더
app.add_middleware(SecurityHeadersMiddleware)
# 3. Rate Limiting
app.add_middleware(
RateLimitMiddleware,
requests_per_minute=settings.RATE_LIMIT_REQUESTS,
period=settings.RATE_LIMIT_PERIOD
)
# 4. 로깅 (가장 안쪽에서 로깅)
app.add_middleware(LoggingMiddleware)
# 5. 에러 핸들링 (가장 바깥쪽에서 에러 캐치)
app.add_middleware(ErrorHandlingMiddleware)

2
frontend/.env.example Normal file
View File

@ -0,0 +1,2 @@
REACT_APP_API_URL=http://localhost:8000
REACT_APP_WS_URL=ws://localhost:8000

21
frontend/.gitignore vendored
View File

@ -1 +1,20 @@
node_modules
# Created by https://www.toptal.com/developers/gitignore/api/react
# Edit at https://www.toptal.com/developers/gitignore?templates=react
### react ###
.DS_*
*.log
logs
**/*.backup.*
**/*.back.*
node_modules
bower_components
*.sublime*
psd
thumb
sketch
# End of https://www.toptal.com/developers/gitignore/api/react

89
frontend/README.md Normal file
View File

@ -0,0 +1,89 @@
# Trading Agents Frontend
React + TypeScript로 구현된 Trading Agents 프론트엔드입니다.
## 주요 기능
- **사용자 인증**: 회원가입, 로그인, JWT 토큰 관리
- **분석 관리**: 주식 분석 요청, 진행 상황 추적, 결과 조회
- **실시간 업데이트**: WebSocket을 통한 실시간 분석 진행 상황 업데이트
- **반응형 디자인**: 모바일 및 데스크톱 지원
## 기술 스택
- **React 18**: 메인 프론트엔드 프레임워크
- **TypeScript**: 타입 안정성
- **React Router**: 라우팅
- **Styled Components**: CSS-in-JS 스타일링
- **React Hook Form**: 폼 관리
- **Yup**: 폼 유효성 검사
- **React Query**: 서버 상태 관리
- **Axios**: HTTP 클라이언트
- **React Hot Toast**: 토스트 알림
## 프로젝트 구조
```
src/
├── components/ # 재사용 가능한 컴포넌트
│ ├── auth/ # 인증 관련 컴포넌트
│ ├── analysis/ # 분석 관련 컴포넌트
│ └── common/ # 공통 컴포넌트
├── contexts/ # React Context
├── hooks/ # 커스텀 훅
├── pages/ # 페이지 컴포넌트
├── services/ # API 서비스
├── types/ # TypeScript 타입 정의
└── utils/ # 유틸리티 함수
```
## 설치 및 실행
1. 의존성 설치:
```bash
npm install
```
2. 환경 변수 설정:
```bash
cp .env.example .env
```
3. 개발 서버 실행:
```bash
npm start
```
4. 프로덕션 빌드:
```bash
npm run build
```
## 환경 변수
- `REACT_APP_API_URL`: 백엔드 API 서버 URL
- `REACT_APP_WS_URL`: WebSocket 서버 URL
## 주요 컴포넌트
### 인증 관련
- `LoginForm`: 로그인 폼
- `RegisterForm`: 회원가입 폼
- `AuthContext`: 인증 상태 관리
### 분석 관련
- `AnalysisForm`: 새 분석 요청 폼
- `AnalysisList`: 분석 세션 목록
- `AnalysisResult`: 분석 결과 표시
### 공통
- `Layout`: 공통 레이아웃
- `ProtectedRoute`: 인증된 사용자만 접근 가능한 라우트
## API 연동
백엔드와의 통신을 위해 다음 서비스들을 사용합니다:
- `AuthService`: 인증 관련 API
- `AnalysisService`: 분석 관련 API
- WebSocket 연결을 통한 실시간 업데이트

11255
frontend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,24 +1,32 @@
{
"name": "tradingagents-web-frontend",
"name": "trading-agents-frontend",
"version": "0.1.0",
"private": true,
"dependencies": {
"@ant-design/icons": "^5.2.6",
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.3.0",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"antd": "^5.10.0",
"axios": "^1.5.0",
"dayjs": "^1.11.9",
"@types/jest": "^27.5.2",
"@types/node": "^16.18.68",
"@types/react": "^18.2.43",
"@types/react-dom": "^18.2.17",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-markdown": "^8.0.7",
"react-router-dom": "^6.4.0",
"react-scripts": "5.0.1",
"recharts": "^2.8.0",
"remark-gfm": "^4.0.1",
"styled-components": "^6.0.8",
"websocket": "^1.0.34"
"typescript": "^4.9.5",
"web-vitals": "^2.1.4",
"react-router-dom": "^6.20.1",
"axios": "^1.6.2",
"react-query": "^3.39.3",
"@types/react-router-dom": "^5.3.3",
"react-hook-form": "^7.48.2",
"@hookform/resolvers": "^3.3.2",
"yup": "^1.3.3",
"styled-components": "^6.1.1",
"@types/styled-components": "^5.1.34",
"react-hot-toast": "^2.4.1",
"react-markdown": "^9.0.1",
"remark-gfm": "^4.0.0"
},
"scripts": {
"start": "react-scripts start",
@ -44,5 +52,7 @@
"last 1 safari version"
]
},
"proxy": "http://localhost:8000"
}
"devDependencies": {
"@types/react-query": "^1.2.9"
}
}

View File

@ -7,14 +7,14 @@
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="TradingAgents - Multi-Agents LLM Financial Trading Framework"
content="Trading Agents - AI 기반 주식 분석 플랫폼"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>TradingAgents - AI 거래 분석 플랫폼</title>
<title>Trading Agents</title>
</head>
<body>
<noscript>JavaScript를 활성화해야 이 앱을 실행할 수 있습니다.</noscript>
<div id="root"></div>
</body>
</html>
</html>

37
frontend/tsconfig.json Normal file
View File

@ -0,0 +1,37 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"es6"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"baseUrl": "src",
"paths": {
"@/*": ["*"],
"@/components/*": ["components/*"],
"@/pages/*": ["pages/*"],
"@/hooks/*": ["hooks/*"],
"@/services/*": ["services/*"],
"@/types/*": ["types/*"],
"@/utils/*": ["utils/*"],
"@/contexts/*": ["contexts/*"]
}
},
"include": [
"src"
]
}