""" API 응답 모델 정의 타입 안정성 및 API 문서화를 위한 Pydantic 모델들 """ from pydantic import BaseModel, Field, ConfigDict from typing import List, Optional, Dict, Any, Union from datetime import datetime from enum import Enum # ================================ # 기본 응답 모델 # ================================ class BaseResponse(BaseModel): """기본 응답 모델""" success: bool = Field(description="요청 성공 여부") message: Optional[str] = Field(None, description="응답 메시지") timestamp: datetime = Field(default_factory=datetime.now, description="응답 시간") class ErrorResponse(BaseResponse): """에러 응답 모델""" success: bool = Field(False, description="요청 성공 여부") error: Dict[str, Any] = Field(description="에러 정보") model_config = ConfigDict( json_schema_extra={ "example": { "success": False, "message": "요청 처리 중 오류가 발생했습니다", "error": { "code": "VALIDATION_ERROR", "details": "입력 데이터가 올바르지 않습니다" }, "timestamp": "2025-01-01T12:00:00" } } ) class SuccessResponse(BaseResponse): """성공 응답 모델""" success: bool = Field(True, description="요청 성공 여부") data: Optional[Any] = Field(None, description="응답 데이터") # ================================ # 열거형 정의 # ================================ class FileStatus(str, Enum): """파일 상태""" ACTIVE = "active" INACTIVE = "inactive" PROCESSING = "processing" ERROR = "error" class MaterialCategory(str, Enum): """자재 카테고리""" PIPE = "PIPE" FITTING = "FITTING" VALVE = "VALVE" FLANGE = "FLANGE" BOLT = "BOLT" GASKET = "GASKET" INSTRUMENT = "INSTRUMENT" EXCLUDE = "EXCLUDE" class JobStatus(str, Enum): """작업 상태""" ACTIVE = "active" COMPLETED = "completed" ON_HOLD = "on_hold" CANCELLED = "cancelled" # ================================ # 파일 관련 모델 # ================================ class FileInfo(BaseModel): """파일 정보 모델""" id: int = Field(description="파일 ID") filename: str = Field(description="파일명") original_filename: str = Field(description="원본 파일명") job_no: Optional[str] = Field(None, description="작업 번호") bom_name: Optional[str] = Field(None, description="BOM 이름") revision: str = Field(default="Rev.0", description="리비전") parsed_count: int = Field(default=0, description="파싱된 자재 수") bom_type: str = Field(default="unknown", description="BOM 타입") status: FileStatus = Field(description="파일 상태") file_size: Optional[int] = Field(None, description="파일 크기 (bytes)") upload_date: datetime = Field(description="업로드 일시") description: Optional[str] = Field(None, description="파일 설명") model_config = ConfigDict( json_schema_extra={ "example": { "id": 1, "filename": "BOM_Rev1.xlsx", "original_filename": "BOM_Rev1.xlsx", "job_no": "TK-2025-001", "bom_name": "메인 BOM", "revision": "Rev.1", "parsed_count": 150, "bom_type": "excel", "status": "active", "file_size": 2048576, "upload_date": "2025-01-01T12:00:00", "description": "파일: BOM_Rev1.xlsx" } } ) class FileListResponse(BaseResponse): """파일 목록 응답 모델""" success: bool = Field(True, description="요청 성공 여부") data: List[FileInfo] = Field(description="파일 목록") total_count: int = Field(description="전체 파일 수") cache_hit: bool = Field(default=False, description="캐시 히트 여부") class FileDeleteResponse(BaseResponse): """파일 삭제 응답 모델""" success: bool = Field(True, description="삭제 성공 여부") message: str = Field(description="삭제 결과 메시지") deleted_file_id: int = Field(description="삭제된 파일 ID") # ================================ # 자재 관련 모델 # ================================ class MaterialInfo(BaseModel): """자재 정보 모델""" id: int = Field(description="자재 ID") file_id: int = Field(description="파일 ID") line_number: Optional[int] = Field(None, description="엑셀 행 번호") original_description: str = Field(description="원본 품명") classified_category: Optional[MaterialCategory] = Field(None, description="분류된 카테고리") classified_subcategory: Optional[str] = Field(None, description="세부 분류") material_grade: Optional[str] = Field(None, description="재질 등급") schedule: Optional[str] = Field(None, description="스케줄") size_spec: Optional[str] = Field(None, description="사이즈 규격") quantity: float = Field(description="수량") unit: str = Field(description="단위") classification_confidence: Optional[float] = Field(None, description="분류 신뢰도") is_verified: bool = Field(default=False, description="검증 여부") model_config = ConfigDict( json_schema_extra={ "example": { "id": 1, "file_id": 1, "line_number": 5, "original_description": "PIPE, SEAMLESS, A333-6, 6\", SCH40", "classified_category": "PIPE", "classified_subcategory": "SEAMLESS", "material_grade": "A333-6", "schedule": "SCH40", "size_spec": "6\"", "quantity": 12.5, "unit": "EA", "classification_confidence": 0.95, "is_verified": False } } ) class MaterialListResponse(BaseResponse): """자재 목록 응답 모델""" success: bool = Field(True, description="요청 성공 여부") data: List[MaterialInfo] = Field(description="자재 목록") total_count: int = Field(description="전체 자재 수") file_info: Optional[FileInfo] = Field(None, description="파일 정보") cache_hit: bool = Field(default=False, description="캐시 히트 여부") # ================================ # 작업 관련 모델 # ================================ class JobInfo(BaseModel): """작업 정보 모델""" job_no: str = Field(description="작업 번호") job_name: str = Field(description="작업명") client_name: Optional[str] = Field(None, description="고객사명") end_user: Optional[str] = Field(None, description="최종 사용자") epc_company: Optional[str] = Field(None, description="EPC 회사") status: JobStatus = Field(description="작업 상태") created_at: datetime = Field(description="생성 일시") file_count: int = Field(default=0, description="파일 수") material_count: int = Field(default=0, description="자재 수") model_config = ConfigDict( json_schema_extra={ "example": { "job_no": "TK-2025-001", "job_name": "석유화학 플랜트 배관 프로젝트", "client_name": "한국석유화학", "end_user": "울산공장", "epc_company": "현대엔지니어링", "status": "active", "created_at": "2025-01-01T09:00:00", "file_count": 3, "material_count": 450 } } ) class JobListResponse(BaseResponse): """작업 목록 응답 모델""" success: bool = Field(True, description="요청 성공 여부") data: List[JobInfo] = Field(description="작업 목록") total_count: int = Field(description="전체 작업 수") cache_hit: bool = Field(default=False, description="캐시 히트 여부") # ================================ # 분류 관련 모델 # ================================ class ClassificationResult(BaseModel): """분류 결과 모델""" category: MaterialCategory = Field(description="분류된 카테고리") subcategory: Optional[str] = Field(None, description="세부 분류") confidence: float = Field(description="분류 신뢰도 (0.0-1.0)") material_grade: Optional[str] = Field(None, description="재질 등급") size_spec: Optional[str] = Field(None, description="사이즈 규격") schedule: Optional[str] = Field(None, description="스케줄") details: Optional[Dict[str, Any]] = Field(None, description="분류 상세 정보") model_config = ConfigDict( json_schema_extra={ "example": { "category": "PIPE", "subcategory": "SEAMLESS", "confidence": 0.95, "material_grade": "A333-6", "size_spec": "6\"", "schedule": "SCH40", "details": { "matched_keywords": ["PIPE", "SEAMLESS", "A333-6"], "size_detected": True, "material_detected": True } } } ) class ClassificationResponse(BaseResponse): """분류 응답 모델""" success: bool = Field(True, description="분류 성공 여부") data: ClassificationResult = Field(description="분류 결과") processing_time: float = Field(description="처리 시간 (초)") cache_hit: bool = Field(default=False, description="캐시 히트 여부") # ================================ # 통계 관련 모델 # ================================ class MaterialStatistics(BaseModel): """자재 통계 모델""" category: MaterialCategory = Field(description="자재 카테고리") count: int = Field(description="개수") percentage: float = Field(description="비율 (%)") total_quantity: float = Field(description="총 수량") unique_items: int = Field(description="고유 항목 수") class ProjectStatistics(BaseModel): """프로젝트 통계 모델""" job_no: str = Field(description="작업 번호") total_materials: int = Field(description="총 자재 수") total_files: int = Field(description="총 파일 수") category_breakdown: List[MaterialStatistics] = Field(description="카테고리별 분석") classification_accuracy: float = Field(description="분류 정확도") verified_percentage: float = Field(description="검증 완료율") model_config = ConfigDict( json_schema_extra={ "example": { "job_no": "TK-2025-001", "total_materials": 450, "total_files": 3, "category_breakdown": [ { "category": "PIPE", "count": 180, "percentage": 40.0, "total_quantity": 1250.5, "unique_items": 45 } ], "classification_accuracy": 0.92, "verified_percentage": 0.75 } } ) class StatisticsResponse(BaseResponse): """통계 응답 모델""" success: bool = Field(True, description="요청 성공 여부") data: ProjectStatistics = Field(description="통계 데이터") cache_hit: bool = Field(default=False, description="캐시 히트 여부") # ================================ # 시스템 관련 모델 # ================================ class CacheInfo(BaseModel): """캐시 정보 모델""" status: str = Field(description="캐시 상태") used_memory: str = Field(description="사용 메모리") connected_clients: int = Field(description="연결된 클라이언트 수") hit_rate: float = Field(description="캐시 히트율 (%)") total_commands: int = Field(description="총 명령 수") class SystemHealthResponse(BaseResponse): """시스템 상태 응답 모델""" success: bool = Field(True, description="요청 성공 여부") data: Dict[str, Any] = Field(description="시스템 상태 정보") cache_info: Optional[CacheInfo] = Field(None, description="캐시 정보") database_status: str = Field(description="데이터베이스 상태") api_version: str = Field(description="API 버전") # ================================ # 유니온 타입 (여러 응답 타입) # ================================ # API 응답으로 사용할 수 있는 모든 타입 APIResponse = Union[ SuccessResponse, ErrorResponse, FileListResponse, FileDeleteResponse, MaterialListResponse, JobListResponse, ClassificationResponse, StatisticsResponse, SystemHealthResponse ]