# 02. ORM 모델 & Pydantic 스키마

## 개요

이 프로젝트는 두 종류의 "모델"을 사용합니다:

| 구분 | 위치 | 역할 |
|------|------|------|
| **ORM 모델** | `app/models/` | DB 테이블 정의. SQLAlchemy가 SQL로 변환 |
| **Pydantic 스키마** | `app/schemas/` | API 요청/응답의 형태 정의. 자동 검증 |

```
[클라이언트] ←→ [Pydantic 스키마] ←→ [Router] ←→ [Service] ←→ [ORM 모델] ←→ [DB]
```

---

## ORM 모델 (`app/models/`)

### Base 클래스 (`models/base.py`)

```python
class Base(DeclarativeBase):
    pass

class TimestampMixin:
    created_at: Mapped[datetime]    # 자동 서버 시간
    updated_at: Mapped[datetime]    # 수정 시 자동 갱신

class UUIDPrimaryKey:
    id: Mapped[uuid.UUID]           # UUID v4 자동 생성
```

- 모든 모델이 `UUIDPrimaryKey`, `TimestampMixin`, `Base`를 상속합니다
- 상속 순서: `class User(UUIDPrimaryKey, TimestampMixin, Base)` — Python MRO에 따라 왼쪽부터 적용

---

### User 모델 (`models/user.py`)

```python
class User(UUIDPrimaryKey, TimestampMixin, Base):
    __tablename__ = "users"

    name: Mapped[str | None]          # 사용자 이름 (최대 100자)

    face_embeddings = relationship("FaceEmbedding", ...)  # 1:N
    garments = relationship("Garment", ...)               # 1:N
```

**핵심 개념: Unknown User (미분류 사용자)**
- 고정 UUID `00000000-0000-0000-0000-000000000000`인 특수 사용자
- 얼굴 인식이 꺼져 있거나 실패했을 때 모든 의류가 이 사용자에게 할당됩니다
- 고정 UUID로 식별되며, 얼굴 매칭 검색에서 제외됩니다

---

### FaceEmbedding 모델 (`models/face.py`)

```python
class FaceEmbedding(UUIDPrimaryKey, TimestampMixin, Base):
    __tablename__ = "face_embeddings"

    user_id: Mapped[uuid.UUID]        # FK → users.id (CASCADE 삭제)
    embedding = mapped_column(Vector(512))  # 512차원 얼굴 벡터 (pgvector)
    image_path: Mapped[str | None]    # 저장된 얼굴 이미지 경로

    user = relationship("User", ...)  # N:1
```

- **pgvector의 `Vector(512)`**: 512개의 float 값으로 구성된 벡터. insightface가 추출한 얼굴 특징
- 한 사용자가 여러 개의 face_embedding을 가질 수 있음 (다양한 각도/조명의 사진)
- `<=>` 연산자로 코사인 거리 계산 가능 (값이 작을수록 유사)

---

### Garment 모델 (`models/garment.py`)

```python
class Garment(UUIDPrimaryKey, TimestampMixin, Base):
    __tablename__ = "garments"

    user_id: Mapped[uuid.UUID]          # FK → users.id
    category_main: Mapped[str]          # 대분류: top, bottom, outer, dress
    category_sub: Mapped[str | None]    # 소분류: t-shirt, jeans, sneakers, ...
    description: Mapped[str | None]     # 한국어 설명 (LLM 생성)
    tags: Mapped[dict | None]           # JSONB — 색상, 패턴, 소재, 시즌 등
    source_image_path: Mapped[str | None]   # 원본 이미지 경로
    ghost_image_path: Mapped[str | None]    # 고스트 마네킹 이미지 경로
    embedding = mapped_column(Vector(1536))  # 의류 임베딩 (미래 사용, 현재 미사용)
```

**인덱스:**
```python
__table_args__ = (
    Index("ix_garments_user_id", "user_id"),           # 사용자별 조회 빠르게
    Index("ix_garments_category_main", "category_main"), # 카테고리 필터
    Index("ix_garments_tags", "tags", postgresql_using="gin"),  # JSONB 필터
)
```

**tags 필드 예시:**
```json
{
    "color": ["black", "white"],
    "pattern": "striped",
    "material": "cotton",
    "style": ["casual", "sporty"],
    "fit": "regular",
    "occasion": ["daily"],
    "brand": "Nike"
}
```

---

## Pydantic 스키마 (`app/schemas/`)

### 공통 스키마 (`schemas/common.py`)

```python
class PageParams(BaseModel):
    page: int = 1
    size: int = 20

class PageMeta(BaseModel):
    page: int       # 현재 페이지
    size: int       # 페이지 크기
    total: int      # 전체 아이템 수

class GarmentBrief(BaseModel):
    """옷장 목록에서 사용하는 간략한 의류 정보"""
    id: uuid.UUID
    user_id: uuid.UUID              # 소유 사용자 ID
    user_name: str | None           # 소유 사용자 이름
    category_main: str
    category_sub: str | None
    description: str | None
    tags: dict | None
    source_image_path: str | None
    ghost_image_path: str | None
    created_at: datetime
```

---

### 얼굴 스키마 (`schemas/face.py`)

```python
class FaceRegisterResponse(BaseModel):
    user_id: uuid.UUID
    user_name: str | None
    is_new_user: bool       # True: 새 사용자 생성됨, False: 기존 사용자 매칭됨
    confidence: float       # 매칭 신뢰도 (0~1). 새 사용자면 1.0

class FaceIdentifyResponse(BaseModel):
    user_id: uuid.UUID
    user_name: str | None
    confidence: float
```

---

### 의류 스키마 (`schemas/garment.py`)

```python
# 얼굴 상태 enum
class FaceStatus(str, Enum):
    known = "known"                 # 등록된 사용자와 매칭됨
    unknown = "unknown"             # 얼굴 감지됐으나 미등록 사용자
    not_detected = "not_detected"   # 얼굴이 감지되지 않음
    not_used = "not_used"           # use_face_id=False로 건너뜀

# 분석 결과 (한 벌의 옷)
class GarmentAnalysisItem(BaseModel):
    id: uuid.UUID
    category_main: str
    category_sub: str | None
    description: str | None
    tags: dict | None
    ghost_image_url: str | None    # "/storage/ghost_mannequin/..." URL

# 통합 분석 API 응답 (POST /api/v1/garments/analyze)
class GarmentAnalyzeResponse(BaseModel):
    face_status: FaceStatus         # 얼굴 인식 결과 상태
    user_id: uuid.UUID              # 할당된 사용자 ID
    user_name: str | None           # 할당된 사용자 이름
    garments: list[GarmentAnalysisItem]

# 고스트 마네킹 생성 응답 (POST /api/v1/garments/{id}/ghost)
class GhostGenerateResponse(BaseModel):
    id: uuid.UUID
    ghost_image_url: str | None

# 의류 재할당 요청 (PATCH /api/v1/garments/{id})
class GarmentUpdateRequest(BaseModel):
    user_id: uuid.UUID

# 의류 재할당 응답
class GarmentUpdateResponse(BaseModel):
    id: uuid.UUID
    user_id: uuid.UUID
    user_name: str | None

# 벌크 재할당 요청 (PATCH /api/v1/garments/bulk)
class GarmentBulkUpdateRequest(BaseModel):
    garment_ids: list[uuid.UUID]
    user_id: uuid.UUID

# 벌크 재할당 응답
class GarmentBulkUpdateResponse(BaseModel):
    updated_count: int
    user_id: uuid.UUID
    user_name: str | None

# 단일 의류 상세
class GarmentDetail(BaseModel):
    id: uuid.UUID
    user_id: uuid.UUID
    category_main: str
    # ... (모든 필드)
    created_at: datetime
    updated_at: datetime

# 삭제 응답
class GarmentDeleteResponse(BaseModel):
    deleted: bool = True
    id: uuid.UUID
```

---

### 옷장 스키마 (`schemas/wardrobe.py`)

```python
class GarmentListResponse(BaseModel):
    items: list[GarmentBrief]    # 의류 목록 (user_id, user_name 포함)
    meta: PageMeta               # 페이지 정보 (page, size, total)
```

---

### 사용자 스키마 (`schemas/user.py`)

```python
class UserSummary(BaseModel):
    id: uuid.UUID
    name: str | None
    garment_count: int             # 해당 사용자의 의류 수

class UserListResponse(BaseModel):
    users: list[UserSummary]
```

---

## 모델 vs 스키마 관계

```
[GarmentAnalyzeResponse]  ←  Router가 ORM → 스키마로 변환
     │
     ├── garments[0]: GarmentAnalysisItem
     │     ↑
     │   Garment(ORM) → _garment_to_analysis_item() → GarmentAnalysisItem
     │
     └── garments[1]: ...
```

- 라우터에서 ORM 객체를 받아서 Pydantic 스키마로 변환합니다
- 이 변환은 라우터 레벨에서 이루어지며, 서비스는 ORM 객체를 반환합니다
- `ghost_image_url`은 DB에 저장된 상대경로에 `/storage/` 접두사를 붙여 생성합니다
