# 06. 옷장 조회 및 필터링

## 개요

의류 목록 조회 API는 사용자의 의류를 조회하고, 의류의 소유자를 변경할 수 있는 엔드포인트입니다.
의류 데이터는 의류 분석 파이프라인(05-garment-pipeline.md)에서 생성됩니다.

`user_id`를 생략하면 **전체 사용자의 의류를 교차 조회**(cross-user query)할 수 있습니다.

## 관련 파일

| 파일 | 역할 |
|------|------|
| `app/routers/garment.py` | API 엔드포인트 (목록 조회 포함) |
| `app/services/wardrobe_service.py` | 쿼리 빌더 + 필터링 로직 |
| `app/schemas/garment.py` | GarmentListResponse 응답 스키마 |
| `app/schemas/common.py` | GarmentBrief, PageMeta |

---

## 엔드포인트

### `GET /api/v1/garments`

의류 목록을 조회합니다. `user_id`를 생략하면 전체 사용자의 의류를 조회합니다.

### 파라미터

| 파라미터 | 위치 | 타입 | 설명 |
|----------|------|------|------|
| `user_id` | query | UUID | 사용자 ID (선택, 생략 시 전체 사용자) |
| `category_main` | query | string | 대분류 필터 (top, bottom, outer, dress) |
| `category_sub` | query | string | 소분류 필터 (t-shirt, jeans, ...) |
| `style` | query | string | 스타일 필터 (casual, formal, sporty, streetwear) |
| `color` | query | string | 색상 필터 (black, white, ...) |
| `page` | query | int | 페이지 번호 (1부터 시작, 기본값: 1) |
| `size` | query | int | 페이지 크기 (1~100, 기본값: 20) |

### 응답 예시

```json
{
    "items": [
        {
            "id": "abc-...",
            "user_id": "a1b2c3d4-...",
            "user_name": "홍길동",
            "category_main": "top",
            "category_sub": "t-shirt",
            "description": "블랙 코튼 반팔 티셔츠",
            "tags": {
                "color": ["black"],
                "pattern": "solid",
                "material": "cotton",
                "style": ["casual"],
                "fit": "regular",
                "occasion": ["daily"],
                "brand": "unknown"
            },
            "source_image_path": "/storage/originals/user_id/abc.jpg",
            "ghost_image_path": "/storage/ghost_mannequin/user_id/def.png",
            "created_at": "2026-04-07T07:00:00"
        }
    ],
    "meta": {
        "page": 1,
        "size": 20,
        "total": 42
    }
}
```

---

## 필터링 로직 (`services/wardrobe_service.py`)

### 쿼리 빌더 패턴

```python
async def list_garments(self, db, *, user_id=None, category_main=None, ...):
    # 기본 쿼리 (user relation을 joinedload로 로딩)
    base = select(Garment).options(joinedload(Garment.user))

    # user_id가 있으면 해당 사용자만, 없으면 전체 조회
    if user_id:
        base = base.where(Garment.user_id == user_id)

    # 조건부 필터 추가
    if category_main:
        base = base.where(Garment.category_main == category_main)
    if category_sub:
        base = base.where(Garment.category_sub == category_sub)
```

### 카테고리 필터 (컬럼 직접 비교)

```sql
-- user_id + category_main 필터
WHERE garments.user_id = :user_id
  AND garments.category_main = 'top'

-- user_id 생략 시 (전체 사용자)
WHERE garments.category_main = 'top'

-- category_sub 필터
  AND garments.category_sub = 't-shirt'
```

- 일반 문자열 컬럼이므로 `==` 비교
- `ix_garments_category_main` 인덱스가 걸려 있어 빠름

### 태그 필터 (JSONB 검색)

```python
if style:
    base = base.where(Garment.tags["style"].astext.contains(style))
if color:
    base = base.where(Garment.tags["color"].astext.contains(color))
```

**동작 방식:**
1. `Garment.tags["style"]` → JSONB의 `style` 키 접근
2. `.astext` → JSON 값을 텍스트로 변환
3. `.contains(style)` → SQL `LIKE '%casual%'` 와 유사

- GIN 인덱스(`ix_garments_tags`)가 JSONB 필터링 성능을 지원합니다

---

## 페이지네이션

```python
# 전체 카운트
count_q = select(func.count()).select_from(base.subquery())
total = (await db.execute(count_q)).scalar_one()

# 페이지네이션 적용
query = base.order_by(Garment.created_at.desc())  # 최신순 정렬
    .offset((page - 1) * size)                      # 건너뛰기
    .limit(size)                                     # 가져올 개수
```

**예시: page=2, size=5**
```
전체 42개 아이템
offset = (2-1) * 5 = 5   → 5개 건너뜀
limit = 5                 → 5개 가져옴
→ 6번째~10번째 아이템 반환
```

### 검증

```python
page: int = Query(1, ge=1)           # 1 이상
size: int = Query(20, ge=1, le=100)  # 1~100
```

- `page=0` → 422 Validation Error
- `size=200` → 422 Validation Error

---

## 라우터의 URL 변환

```python
items = [
    GarmentBrief(
        ...
        user_id=g.user_id,
        user_name=g.user.name,            # joinedload로 로딩된 User 관계
        source_image_path=f"/storage/{g.source_image_path}" if g.source_image_path else None,
        ghost_image_path=f"/storage/{g.ghost_image_path}" if g.ghost_image_path else None,
        ...
    )
    for g in garments
]
```

DB에 저장된 상대경로(`originals/user_id/file.jpg`)를 API 응답에서 URL 경로(`/storage/originals/user_id/file.jpg`)로 변환합니다.

---

## 의류 소유자 변경 (Reassignment)

### `PATCH /api/v1/garments/{id}`

개별 의류의 소유자를 변경합니다.

```json
// 요청
{ "user_id": "new-user-uuid-..." }

// 응답
{ "id": "garment-uuid", "user_id": "new-user-uuid-...", "user_name": "홍길동" }
```

### `PATCH /api/v1/garments/bulk`

여러 의류의 소유자를 일괄 변경합니다.

```json
// 요청
{ "garment_ids": ["uuid-1", "uuid-2", ...], "user_id": "new-user-uuid-..." }

// 응답
{ "updated_count": 2, "user_id": "new-user-uuid-...", "user_name": "홍길동" }
```

---

## 사용 시나리오

```bash
# 1. 전체 사용자의 의류 조회 (cross-user)
GET /api/v1/garments

# 2. 특정 사용자의 옷장
GET /api/v1/garments?user_id=00000000-0000-0000-0000-000000000000

# 3. 상의만 조회
GET /api/v1/garments?user_id={user_id}&category_main=top

# 4. 캐주얼 옷만
GET /api/v1/garments?style=casual

# 5. 검은색 옷만, 5개씩 페이지네이션
GET /api/v1/garments?color=black&page=1&size=5
```
