""" Pydantic 요청/응답 스키마 정의. 모든 API 엔드포인트의 입출력 타입을 명시합니다. """ from __future__ import annotations from typing import Any from pydantic import BaseModel, Field # ── 공통 ────────────────────────────────────────────────────────────────── class ErrorResponse(BaseModel): """표준 에러 응답.""" code: str = Field(..., description="에러 코드 (예: NOT_FOUND)") message: str = Field(..., description="한국어 에러 메시지") detail: str | None = Field(None, description="상세 원인 (선택)") # ── 학교 기본 정보 ────────────────────────────────────────────────────────── class SchoolInfoResponse(BaseModel): """학교 기본 정보 응답.""" sd_schul_code: str school_name: str sido_code: str | None = None sido_name: str | None = None school_kind: str | None = None location_name: str | None = None founding_type: str | None = None road_address: str | None = None coedu_type: str | None = None day_night: str | None = None school_course: str | None = None is_closed: bool | None = None homepage_url: str | None = None established_date: str | None = None # ── 예측 ────────────────────────────────────────────────────────────────── class PredictionRequest(BaseModel): """예측 요청 파라미터.""" horizon_years: int = Field( default=5, ge=1, le=10, description="예측 기간 (년, 1~10)", ) target_col: str = Field( default="student_count", description="예측 대상 컬럼 (예: student_count)", ) class ForecastResponse(BaseModel): """시계열 예측 응답.""" schul_code: str target_col: str forecast_years: list[int] point_forecast: list[float] lower_bound: list[float] upper_bound: list[float] model_version: str context_years: list[int] context_values: list[float] # ── 진단 ────────────────────────────────────────────────────────────────── class ShapValueItem(BaseModel): """SHAP 기여 요인 단일 항목.""" feature: str feature_kr: str shap_value: float feature_value: float class ShapResultResponse(BaseModel): """SHAP 분석 응답.""" schul_code: str status_label: str status_code: int shap_values: dict[str, float] top_factors: list[ShapValueItem] base_value: float model_accuracy: float | None = None class SustainabilityScoreResponse(BaseModel): """지속 가능성 점수 응답.""" schul_code: str total_score: float curriculum_score: float personnel_score: float facility_score: float community_score: float sub_scores: dict[str, float] warnings: list[str] class SimulationResult(BaseModel): """정책 시뮬레이션 단일 결과.""" label: str description: str base_total_score: float sim_total_score: float delta_score: float sim_curriculum_score: float | None = None sim_personnel_score: float | None = None sim_facility_score: float | None = None sim_community_score: float | None = None error: str | None = None class DiagnosticsResponse(BaseModel): """종합 진단 응답.""" schul_code: str school_name: str status_label: str status_code: int sustainability_score: float forecast: ForecastResponse shap_result: ShapResultResponse score_detail: SustainabilityScoreResponse simulations: dict[str, SimulationResult] metadata: dict[str, Any] # ── 시뮬레이션 요청 ──────────────────────────────────────────────────────── class SimulationRequest(BaseModel): """ 사용자 정의 정책 시뮬레이션 요청. school_data_overrides 에 변경할 지표를 지정하면 해당 값을 적용한 지속 가능성 점수를 반환합니다. """ school_data_overrides: dict[str, float] = Field( ..., description="덮어쓸 학교 지표 딕셔너리 (예: {\"teacher_count\": 7})", ) scenario_label: str = Field( default="사용자 정의 시나리오", description="시나리오 이름", ) # ── 지역 위험지도 ────────────────────────────────────────────────────────── class RegionRiskItem(BaseModel): """지역 내 단일 학교 위험 정보.""" sd_schul_code: str school_name: str status_label: str status_code: int sustainability_score: float student_count: float | None = None lat: float | None = None lng: float | None = None class RegionRiskResponse(BaseModel): """지역 위험지도 응답.""" region_code: str region_name: str | None = None schools: list[RegionRiskItem] summary: dict[str, int] = Field( default_factory=dict, description="상태별 학교 수 집계 (정상/주의/위험/고위험)", )