[AI] 12_Pydantic_스키마 - 100-hours-a-week/9-team-Devths-WIKI GitHub Wiki

Pydantic 스키마 명세

AI Server의 모든 Request/Response Pydantic 스키마 정의


📚 목차


1. 개요

1.1. Pydantic 사용 목적

목적 설명
입력 검증 Request 데이터의 타입, 필수 여부, 제약 조건 검증
출력 직렬화 Response 데이터의 JSON 변환 및 스키마 보장
API 문서화 FastAPI 자동 OpenAPI 문서 생성
타입 안정성 IDE 자동완성 및 타입 힌트 지원

1.2. 스키마 구조

app/schemas/
├── common.py          # 공통 스키마
├── ocr.py             # OCR 관련
├── embed.py           # 임베딩 관련
├── analyze.py         # 분석 관련
├── interview.py       # 면접 관련
├── chat.py            # 채팅 관련
├── calendar.py        # 캘린더 관련
├── masking.py         # 마스킹 관련
└── __init__.py        # 전체 export

2. 공통 스키마

2.1. 기본 응답 스키마

# app/schemas/common.py
from pydantic import BaseModel, Field
from typing import Optional, Any, List
from datetime import datetime
from enum import Enum


class TaskStatus(str, Enum):
    """비동기 작업 상태"""
    PROCESSING = "processing"
    COMPLETED = "completed"
    FAILED = "failed"


class ErrorDetail(BaseModel):
    """에러 상세 정보"""
    code: str = Field(..., description="에러 코드", example="INVALID_REQUEST")
    message: str = Field(..., description="에러 메시지", example="필수 필드가 누락되었습니다")
    details: Optional[dict] = Field(None, description="추가 상세 정보")


class BaseResponse(BaseModel):
    """기본 응답 스키마"""
    success: bool = Field(..., description="요청 성공 여부")


class ErrorResponse(BaseResponse):
    """에러 응답 스키마"""
    success: bool = False
    error: ErrorDetail


class AsyncTaskResponse(BaseModel):
    """비동기 작업 초기 응답"""
    task_id: str = Field(..., description="비동기 작업 ID", example="task_abc123")
    status: TaskStatus = Field(default=TaskStatus.PROCESSING, description="작업 상태")


class AsyncTaskStatusResponse(BaseModel):
    """비동기 작업 상태 조회 응답"""
    task_id: str
    status: TaskStatus
    progress: Optional[int] = Field(None, ge=0, le=100, description="진행률 (%)")
    message: Optional[str] = Field(None, description="상태 메시지")
    result: Optional[Any] = Field(None, description="완료 시 결과 데이터")
    error: Optional[ErrorDetail] = Field(None, description="실패 시 에러 정보")

2.2. 스트리밍 응답 스키마

# app/schemas/common.py (계속)
from typing import Literal


class StreamingChunk(BaseModel):
    """SSE 스트리밍 청크"""
    type: Literal["chunk", "complete", "error"] = Field(..., description="청크 타입")
    content: Optional[str] = Field(None, description="스트리밍 텍스트 (chunk 타입)")
    data: Optional[dict] = Field(None, description="완료 시 전체 데이터 (complete 타입)")
    error: Optional[ErrorDetail] = Field(None, description="에러 정보 (error 타입)")


# SSE 이벤트 포맷 예시
"""
data: {"type": "chunk", "content": "분석 결과..."}

data: {"type": "chunk", "content": "를 확인했습니다."}

data: {"type": "complete", "data": {"score": 85, "grade": "A"}}
"""

2.3. 페이지네이션 스키마

# app/schemas/common.py (계속)
from typing import Generic, TypeVar

T = TypeVar('T')

class PaginatedResponse(BaseModel, Generic[T]):
    """페이지네이션 응답"""
    items: List[T]
    total: int = Field(..., description="전체 항목 수")
    page: int = Field(..., ge=1, description="현재 페이지")
    per_page: int = Field(..., ge=1, le=100, description="페이지당 항목 수")
    has_next: bool = Field(..., description="다음 페이지 존재 여부")

3. OCR 텍스트 추출

3.1. Request

# app/schemas/ocr.py
from pydantic import BaseModel, Field, HttpUrl, validator
from typing import Literal


class OCRExtractRequest(BaseModel):
    """OCR 텍스트 추출 요청"""
    file_url: HttpUrl = Field(
        ..., 
        description="S3 파일 URL",
        example="https://s3.amazonaws.com/bucket/resume.pdf"
    )
    file_type: Literal["pdf", "image"] = Field(
        ..., 
        description="파일 타입"
    )
    type: Literal["resume", "portfolio", "job_posting"] = Field(
        ..., 
        description="문서 타입"
    )
    user_id: str = Field(
        ..., 
        min_length=1,
        description="사용자 ID",
        example="user_456"
    )
    document_id: str = Field(
        ..., 
        min_length=1,
        description="문서 ID",
        example="resume_123"
    )

    @validator('file_url')
    def validate_file_url(cls, v):
        url_str = str(v)
        if not any(ext in url_str.lower() for ext in ['.pdf', '.png', '.jpg', '.jpeg', '.webp']):
            raise ValueError('지원하지 않는 파일 형식입니다')
        return v

    class Config:
        json_schema_extra = {
            "example": {
                "file_url": "https://s3.amazonaws.com/bucket/resume.pdf",
                "file_type": "pdf",
                "type": "resume",
                "user_id": "user_456",
                "document_id": "resume_123"
            }
        }

3.2. Response

# app/schemas/ocr.py (계속)
from typing import List, Optional
from pydantic import BaseModel, Field


class PageText(BaseModel):
    """페이지별 텍스트"""
    page: int = Field(..., ge=1, description="페이지 번호")
    text: str = Field(..., description="페이지 텍스트")


class OCRResult(BaseModel):
    """OCR 처리 결과"""
    success: bool
    extracted_text: str = Field(..., description="추출된 전체 텍스트")
    pages: List[PageText] = Field(default=[], description="페이지별 텍스트")
    vector_id: str = Field(..., description="VectorDB 저장 ID")
    collection: str = Field(..., description="VectorDB 컬렉션명")


class OCRExtractResponse(AsyncTaskResponse):
    """OCR 추출 초기 응답 (비동기)"""
    pass  # task_id, status 반환


class OCRStatusResponse(AsyncTaskStatusResponse):
    """OCR 상태 조회 응답"""
    result: Optional[OCRResult] = None

4. 임베딩 저장

4.1. Request

# app/schemas/embed.py
from pydantic import BaseModel, Field, validator
from typing import Literal


class EmbedRequest(BaseModel):
    """임베딩 저장 요청"""
    type: Literal["resume", "portfolio", "job_posting"] = Field(
        ..., 
        description="문서 타입"
    )
    id: str = Field(
        ..., 
        min_length=1,
        description="문서 ID",
        example="resume_123"
    )
    user_id: str = Field(
        ..., 
        min_length=1,
        description="사용자 ID",
        example="user_456"
    )
    text: str = Field(
        ..., 
        min_length=10,
        description="임베딩할 텍스트"
    )

    @validator('text')
    def validate_text_length(cls, v):
        if len(v) > 100000:  # 약 25,000 토큰
            raise ValueError('텍스트가 너무 깁니다 (최대 100,000자)')
        return v

    class Config:
        json_schema_extra = {
            "example": {
                "type": "resume",
                "id": "resume_123",
                "user_id": "user_456",
                "text": "3년차 백엔드 개발자입니다. Python, FastAPI..."
            }
        }

4.2. Response

# app/schemas/embed.py (계속)

class EmbedResponse(BaseModel):
    """임베딩 저장 응답"""
    success: bool = True
    type: str = Field(..., description="문서 타입")
    id: str = Field(..., description="문서 ID")
    vector_id: str = Field(..., description="VectorDB 저장 ID")
    collection: str = Field(..., description="VectorDB 컬렉션명")

    class Config:
        json_schema_extra = {
            "example": {
                "success": True,
                "type": "resume",
                "id": "resume_123",
                "vector_id": "vec_abc123",
                "collection": "resumes"
            }
        }

5. 분석 및 매칭도

5.1. Request

# app/schemas/analyze.py
from pydantic import BaseModel, Field, validator
from typing import Optional


class AnalyzeRequest(BaseModel):
    """분석 및 매칭도 요청"""
    resume_id: str = Field(..., description="이력서 ID")
    posting_id: str = Field(..., description="채용공고 ID")
    resume_text: str = Field(
        ..., 
        min_length=50,
        description="이력서 텍스트"
    )
    posting_text: str = Field(
        ..., 
        min_length=50,
        description="채용공고 텍스트"
    )

    @validator('resume_text')
    def validate_resume_text(cls, v):
        if len(v) < 100:
            raise ValueError('이력서 텍스트가 너무 짧습니다 (최소 100자)')
        return v

    @validator('posting_text')
    def validate_posting_text(cls, v):
        if len(v) < 50:
            raise ValueError('채용공고 텍스트가 너무 짧습니다 (최소 50자)')
        return v

5.2. Response

# app/schemas/analyze.py (계속)
from typing import List
from enum import Enum


class MatchGrade(str, Enum):
    """매칭 등급"""
    S = "S"
    A = "A"
    B = "B"
    C = "C"
    D = "D"
    F = "F"


class ResumeAnalysis(BaseModel):
    """이력서 분석 결과"""
    strengths: List[str] = Field(..., description="강점 목록")
    weaknesses: List[str] = Field(..., description="약점 목록")
    suggestions: List[str] = Field(..., description="개선 제안")


class PostingAnalysis(BaseModel):
    """채용공고 분석 결과"""
    company: str = Field(..., description="회사명")
    position: str = Field(..., description="포지션")
    required_skills: List[str] = Field(..., description="필수 스킬")
    preferred_skills: List[str] = Field(default=[], description="우대 스킬")


class MatchingResult(BaseModel):
    """매칭도 분석 결과"""
    score: int = Field(..., ge=0, le=100, description="매칭 점수")
    grade: MatchGrade = Field(..., description="등급")
    matched_skills: List[str] = Field(..., description="매칭된 스킬")
    missing_skills: List[str] = Field(..., description="부족한 스킬")


class AnalyzeResponse(BaseModel):
    """분석 및 매칭도 응답"""
    success: bool = True
    resume_analysis: ResumeAnalysis
    posting_analysis: PostingAnalysis
    matching: MatchingResult

    class Config:
        json_schema_extra = {
            "example": {
                "success": True,
                "resume_analysis": {
                    "strengths": ["Python 3년 경험", "FastAPI 숙련"],
                    "weaknesses": ["클라우드 경험 부족"],
                    "suggestions": ["AWS 자격증 취득 추천"]
                },
                "posting_analysis": {
                    "company": "카카오",
                    "position": "백엔드 개발자",
                    "required_skills": ["Python", "FastAPI"],
                    "preferred_skills": ["AWS", "Kubernetes"]
                },
                "matching": {
                    "score": 85,
                    "grade": "A",
                    "matched_skills": ["Python", "FastAPI"],
                    "missing_skills": ["AWS"]
                }
            }
        }

6. 모의 면접 - 질문 생성

6.1. Request

# app/schemas/interview.py
from pydantic import BaseModel, Field
from typing import Literal, Optional


class InterviewType(str, Enum):
    """면접 유형"""
    TECHNICAL = "technical"
    PERSONALITY = "personality"


class InterviewQuestionRequest(BaseModel):
    """면접 질문 생성 요청"""
    room_id: str = Field(..., description="채팅방 ID")
    session_id: str = Field(..., description="면접 세션 ID")
    interview_type: Literal["technical", "personality"] = Field(
        ..., 
        description="면접 유형"
    )
    resume_text: str = Field(..., min_length=50, description="이력서 텍스트")
    posting_text: Optional[str] = Field(None, description="채용공고 텍스트 (선택)")

    class Config:
        json_schema_extra = {
            "example": {
                "room_id": "room_001",
                "session_id": "session_abc123",
                "interview_type": "technical",
                "resume_text": "3년차 백엔드 개발자...",
                "posting_text": "백엔드 개발자 채용..."
            }
        }

6.2. Response

# app/schemas/interview.py (계속)

class InterviewQuestionResponse(BaseModel):
    """면접 질문 생성 응답"""
    success: bool = True
    question_id: str = Field(..., description="질문 ID")
    question: str = Field(..., description="생성된 질문")
    is_followup: bool = Field(..., description="꼬리질문 여부")
    question_number: int = Field(..., ge=1, le=5, description="질문 번호")

    class Config:
        json_schema_extra = {
            "example": {
                "success": True,
                "question_id": "q_001",
                "question": "Python의 GIL에 대해 설명해주세요.",
                "is_followup": False,
                "question_number": 1
            }
        }

7. 모의 면접 - Q&A 저장

7.1. Request

# app/schemas/interview.py (계속)

class InterviewSaveRequest(BaseModel):
    """면접 Q&A 저장 요청"""
    room_id: str = Field(..., description="채팅방 ID")
    session_id: str = Field(..., description="면접 세션 ID")
    question_id: str = Field(..., description="질문 ID")
    question: str = Field(..., description="질문 내용")
    answer: str = Field(..., min_length=1, description="사용자 답변")
    is_followup: bool = Field(..., description="꼬리질문 여부")
    question_number: int = Field(..., ge=1, le=5, description="질문 번호")

    class Config:
        json_schema_extra = {
            "example": {
                "room_id": "room_001",
                "session_id": "session_abc123",
                "question_id": "q_001",
                "question": "Python의 GIL에 대해 설명해주세요.",
                "answer": "GIL은 Global Interpreter Lock으로...",
                "is_followup": False,
                "question_number": 1
            }
        }

7.2. Response

# app/schemas/interview.py (계속)

class InterviewSaveResponse(BaseModel):
    """면접 Q&A 저장 응답"""
    success: bool = True
    qa_id: str = Field(..., description="저장된 Q&A ID")
    session_id: str = Field(..., description="면접 세션 ID")
    saved_count: int = Field(..., ge=1, description="현재까지 저장된 Q&A 수")
    max_questions: int = Field(default=5, description="최대 질문 수")

    class Config:
        json_schema_extra = {
            "example": {
                "success": True,
                "qa_id": "qa_001",
                "session_id": "session_abc123",
                "saved_count": 1,
                "max_questions": 5
            }
        }

8. 모의 면접 - 리포트

8.1. Request

# app/schemas/interview.py (계속)

class InterviewReportRequest(BaseModel):
    """면접 리포트 생성 요청"""
    room_id: str = Field(..., description="채팅방 ID")
    session_id: str = Field(..., description="면접 세션 ID")
    interview_type: Literal["technical", "personality"] = Field(
        ..., 
        description="면접 유형"
    )
    resume_text: str = Field(..., description="이력서 텍스트")
    posting_text: Optional[str] = Field(None, description="채용공고 텍스트")
    ended_by: Literal["auto", "manual"] = Field(
        ..., 
        description="종료 방식 (auto: 5개 완료, manual: 직접 종료)"
    )

    class Config:
        json_schema_extra = {
            "example": {
                "room_id": "room_001",
                "session_id": "session_abc123",
                "interview_type": "technical",
                "resume_text": "이력서 텍스트...",
                "posting_text": "채용공고 텍스트...",
                "ended_by": "auto"
            }
        }

8.2. Response

# app/schemas/interview.py (계속)
from typing import List


class QAEvaluation(BaseModel):
    """개별 Q&A 평가"""
    qa_id: str = Field(..., description="Q&A ID")
    question: str = Field(..., description="질문")
    answer: str = Field(..., description="답변")
    score: int = Field(..., ge=0, le=100, description="점수")
    good_points: List[str] = Field(..., description="잘한 점")
    improvements: List[str] = Field(..., description="개선점")


class InterviewReport(BaseModel):
    """면접 종합 리포트"""
    total_score: int = Field(..., ge=0, le=100, description="총점")
    grade: str = Field(..., description="등급 (S/A/B/C/D/F)")
    strength_patterns: List[str] = Field(..., description="강점 패턴")
    weakness_patterns: List[str] = Field(..., description="약점 패턴")
    learning_guide: List[str] = Field(..., description="학습 가이드")


class InterviewReportResponse(BaseModel):
    """면접 리포트 응답"""
    success: bool = True
    room_id: str
    session_id: str
    evaluations: List[QAEvaluation] = Field(..., description="개별 Q&A 평가")
    report: InterviewReport = Field(..., description="종합 리포트")

    class Config:
        json_schema_extra = {
            "example": {
                "success": True,
                "room_id": "room_001",
                "session_id": "session_abc123",
                "evaluations": [
                    {
                        "qa_id": "qa_001",
                        "question": "Python의 GIL이 뭔가요?",
                        "answer": "GIL은...",
                        "score": 80,
                        "good_points": ["기본 개념 이해"],
                        "improvements": ["심화 설명 추가"]
                    }
                ],
                "report": {
                    "total_score": 78,
                    "grade": "B+",
                    "strength_patterns": ["기술 개념 이해도 높음"],
                    "weakness_patterns": ["심화 개념 설명 부족"],
                    "learning_guide": ["Python 심화 학습 권장"]
                }
            }
        }

9. 대화 처리

9.1. Request

# app/schemas/chat.py
from pydantic import BaseModel, Field
from typing import Optional, List, Literal, Any


class ChatMessage(BaseModel):
    """채팅 메시지"""
    role: Literal["user", "assistant"] = Field(..., description="메시지 발신자")
    content: str = Field(..., description="메시지 내용")


class ToolResult(BaseModel):
    """Tool 실행 결과"""
    tool: str = Field(..., description="실행된 Tool 이름")
    success: bool = Field(..., description="실행 성공 여부")
    data: Any = Field(..., description="Tool 실행 결과 데이터")


class ChatRequest(BaseModel):
    """채팅 요청"""
    room_id: str = Field(..., description="채팅방 ID")
    user_id: str = Field(..., description="사용자 ID")
    message: Optional[str] = Field(None, description="사용자 메시지")
    history: List[ChatMessage] = Field(
        default=[], 
        max_length=20,
        description="대화 히스토리 (최근 20개)"
    )
    tool_result: Optional[ToolResult] = Field(
        None, 
        description="Tool 실행 결과 (Backend가 전달)"
    )

    class Config:
        json_schema_extra = {
            "example": {
                "room_id": "room_001",
                "user_id": "user_456",
                "message": "이번 주 일정 알려줘",
                "history": [
                    {"role": "user", "content": "안녕"},
                    {"role": "assistant", "content": "안녕하세요!"}
                ]
            }
        }

9.2. Response

# app/schemas/chat.py (계속)

class ToolCallParams(BaseModel):
    """Tool 호출 파라미터"""
    # Tool에 따라 동적으로 변할 수 있음
    class Config:
        extra = 'allow'


class ToolCall(BaseModel):
    """Tool 호출 정보"""
    tool: Literal[
        "get_schedule", 
        "add_schedule", 
        "update_schedule", 
        "delete_schedule"
    ] = Field(..., description="호출할 Tool 이름")
    params: dict = Field(..., description="Tool 파라미터")


class ChatResponse(BaseModel):
    """채팅 응답"""
    success: bool = True
    response: Optional[str] = Field(
        None, 
        description="AI 응답 (tool_used가 있으면 null)"
    )
    tool_used: Optional[ToolCall] = Field(
        None, 
        description="실행할 Tool 정보 (response가 있으면 null)"
    )

    class Config:
        json_schema_extra = {
            "examples": [
                {
                    "name": "일반 응답",
                    "value": {
                        "success": True,
                        "response": "이력서 작성 팁을 알려드릴게요...",
                        "tool_used": None
                    }
                },
                {
                    "name": "Tool 호출",
                    "value": {
                        "success": True,
                        "response": None,
                        "tool_used": {
                            "tool": "get_schedule",
                            "params": {
                                "start_date": "2026-01-06",
                                "end_date": "2026-01-12"
                            }
                        }
                    }
                }
            ]
        }

10. 캘린더 파싱

10.1. Request

# app/schemas/calendar.py
from pydantic import BaseModel, Field, HttpUrl, validator
from typing import Optional


class CalendarParseRequest(BaseModel):
    """캘린더 일정 파싱 요청"""
    file_url: Optional[HttpUrl] = Field(
        None, 
        description="파일 URL (text와 둘 중 하나 필수)"
    )
    text: Optional[str] = Field(
        None, 
        description="텍스트 (file_url과 둘 중 하나 필수)"
    )

    @validator('text', always=True)
    def validate_input(cls, v, values):
        if not v and not values.get('file_url'):
            raise ValueError('file_url 또는 text 중 하나는 필수입니다')
        return v

    class Config:
        json_schema_extra = {
            "example": {
                "file_url": None,
                "text": "카카오 백엔드 개발자 채용\n서류 마감: 2026-01-15\n코딩테스트: 2026-01-20"
            }
        }

10.2. Response

# app/schemas/calendar.py (계속)
from typing import List, Optional


class ScheduleItem(BaseModel):
    """일정 항목"""
    stage: str = Field(..., description="전형 단계")
    date: str = Field(..., description="날짜 (YYYY-MM-DD)")
    time: Optional[str] = Field(None, description="시간 (HH:MM)")


class CalendarParseResponse(BaseModel):
    """캘린더 파싱 응답"""
    success: bool = True
    company: str = Field(..., description="회사명")
    position: str = Field(..., description="포지션")
    schedules: List[ScheduleItem] = Field(..., description="일정 목록")
    hashtags: List[str] = Field(default=[], description="자동 생성 해시태그")

    class Config:
        json_schema_extra = {
            "example": {
                "success": True,
                "company": "카카오",
                "position": "백엔드 개발자",
                "schedules": [
                    {"stage": "서류 마감", "date": "2026-01-15", "time": None},
                    {"stage": "코딩테스트", "date": "2026-01-20", "time": "14:00"},
                    {"stage": "1차 면접", "date": "2026-01-25", "time": None}
                ],
                "hashtags": ["#카카오", "#백엔드", "#신입"]
            }
        }

11. 마스킹

11.1. Request

# app/schemas/masking.py
from pydantic import BaseModel, Field, HttpUrl
from typing import Literal


class MaskingDraftRequest(BaseModel):
    """마스킹 요청"""
    file_url: HttpUrl = Field(..., description="S3 파일 URL")
    file_type: Literal["image", "pdf"] = Field(..., description="파일 타입")

    class Config:
        json_schema_extra = {
            "example": {
                "file_url": "https://s3.amazonaws.com/bucket/document.png",
                "file_type": "image"
            }
        }

11.2. Response

# app/schemas/masking.py (계속)
from typing import List, Optional, Tuple
from enum import Enum


class PIIType(str, Enum):
    """개인정보 유형"""
    NAME = "name"
    PHONE = "phone"
    EMAIL = "email"
    ADDRESS = "address"
    ID_NUMBER = "id_number"
    FACE = "face"


class DetectedPII(BaseModel):
    """감지된 개인정보"""
    type: PIIType = Field(..., description="개인정보 유형")
    coordinates: List[int] = Field(
        ..., 
        min_length=4, 
        max_length=4,
        description="좌표 [x1, y1, x2, y2]"
    )
    confidence: float = Field(..., ge=0, le=1, description="신뢰도")


class MaskingResult(BaseModel):
    """마스킹 처리 결과"""
    success: bool = True
    original_url: str = Field(..., description="원본 파일 URL")
    masked_url: str = Field(..., description="마스킹된 파일 URL")
    thumbnail_url: str = Field(..., description="썸네일 URL")
    detected_pii: List[DetectedPII] = Field(..., description="감지된 개인정보 목록")


class MaskingDraftResponse(AsyncTaskResponse):
    """마스킹 초기 응답 (비동기)"""
    pass  # task_id, status 반환


class MaskingStatusResponse(AsyncTaskStatusResponse):
    """마스킹 상태 조회 응답"""
    result: Optional[MaskingResult] = None

    class Config:
        json_schema_extra = {
            "example": {
                "task_id": "task_masking_001",
                "status": "completed",
                "result": {
                    "success": True,
                    "original_url": "https://s3.../document.png",
                    "masked_url": "https://s3.../document_masked.png",
                    "thumbnail_url": "https://s3.../document_masked_thumb.png",
                    "detected_pii": [
                        {"type": "name", "coordinates": [100, 50, 200, 80], "confidence": 0.95},
                        {"type": "phone", "coordinates": [100, 100, 250, 130], "confidence": 0.92}
                    ]
                }
            }
        }

12. 파일 구조

12.1. 전체 스키마 파일 구조

app/schemas/
├── __init__.py
├── common.py          # 42번째 줄까지
├── ocr.py             # 50줄
├── embed.py           # 40줄
├── analyze.py         # 80줄
├── interview.py       # 150줄
├── chat.py            # 80줄
├── calendar.py        # 50줄
└── masking.py         # 60줄

12.2. __init__.py 예시

# app/schemas/__init__.py
"""Pydantic 스키마 모듈"""

# Common
from .common import (
    BaseResponse,
    ErrorResponse,
    ErrorDetail,
    AsyncTaskResponse,
    AsyncTaskStatusResponse,
    TaskStatus,
    StreamingChunk,
    PaginatedResponse,
)

# OCR
from .ocr import (
    OCRExtractRequest,
    OCRExtractResponse,
    OCRStatusResponse,
    OCRResult,
)

# Embed
from .embed import (
    EmbedRequest,
    EmbedResponse,
)

# Analyze
from .analyze import (
    AnalyzeRequest,
    AnalyzeResponse,
    ResumeAnalysis,
    PostingAnalysis,
    MatchingResult,
    MatchGrade,
)

# Interview
from .interview import (
    InterviewQuestionRequest,
    InterviewQuestionResponse,
    InterviewSaveRequest,
    InterviewSaveResponse,
    InterviewReportRequest,
    InterviewReportResponse,
    QAEvaluation,
    InterviewReport,
)

# Chat
from .chat import (
    ChatRequest,
    ChatResponse,
    ChatMessage,
    ToolCall,
    ToolResult,
)

# Calendar
from .calendar import (
    CalendarParseRequest,
    CalendarParseResponse,
    ScheduleItem,
)

# Masking
from .masking import (
    MaskingDraftRequest,
    MaskingDraftResponse,
    MaskingStatusResponse,
    MaskingResult,
    DetectedPII,
    PIIType,
)

__all__ = [
    # Common
    "BaseResponse",
    "ErrorResponse",
    "ErrorDetail",
    "AsyncTaskResponse",
    "AsyncTaskStatusResponse",
    "TaskStatus",
    "StreamingChunk",
    "PaginatedResponse",
    # OCR
    "OCRExtractRequest",
    "OCRExtractResponse",
    "OCRStatusResponse",
    "OCRResult",
    # Embed
    "EmbedRequest",
    "EmbedResponse",
    # Analyze
    "AnalyzeRequest",
    "AnalyzeResponse",
    "ResumeAnalysis",
    "PostingAnalysis",
    "MatchingResult",
    "MatchGrade",
    # Interview
    "InterviewQuestionRequest",
    "InterviewQuestionResponse",
    "InterviewSaveRequest",
    "InterviewSaveResponse",
    "InterviewReportRequest",
    "InterviewReportResponse",
    "QAEvaluation",
    "InterviewReport",
    # Chat
    "ChatRequest",
    "ChatResponse",
    "ChatMessage",
    "ToolCall",
    "ToolResult",
    # Calendar
    "CalendarParseRequest",
    "CalendarParseResponse",
    "ScheduleItem",
    # Masking
    "MaskingDraftRequest",
    "MaskingDraftResponse",
    "MaskingStatusResponse",
    "MaskingResult",
    "DetectedPII",
    "PIIType",
]

12.3. 라우터에서 사용 예시

# app/modules/analyze/router.py
from fastapi import APIRouter
from fastapi.responses import StreamingResponse
from app.schemas import AnalyzeRequest, AnalyzeResponse

router = APIRouter(prefix="/ai", tags=["Analyze"])

@router.post(
    "/analyze",
    response_model=AnalyzeResponse,
    summary="이력서 분석 및 매칭도 계산",
    description="이력서와 채용공고를 분석하고 매칭도를 계산합니다. (SSE 스트리밍)"
)
async def analyze(request: AnalyzeRequest):
    """분석 API"""
    ...