# Model Layer Guide

이 디렉토리에는 DynamoDB 테이블과 매핑되는 데이터 모델을 정의합니다.

## 모델 작성 규칙

### 기본 구조

```python
import time
import uuid
import api
from pydantic import fields, BaseModel
from typing import Optional, Literal
from sawsi.model.base import DynamoModel, sync


@sync
class MyModel(DynamoModel):
    __dynamo__ = api.dynamo
    _table = 'my_table'
    _indexes = [
        ('status', 'crt'),
    ]
    
    id: str = fields.Field(default_factory=lambda: str(uuid.uuid4()))
    crt: float = fields.Field(default_factory=time.time)
    name: str
    status: Optional[Literal["active", "inactive"]] = None
```

### 필수 요소

- `@sync` 데코레이터: 빌드 시 테이블/인덱스 자동 생성에 등록됨
- `__dynamo__ = api.dynamo`: DynamoDB 연결 객체
- `_table`: DynamoDB 테이블 이름
- `_indexes`: GSI 목록. `(partition_key, sort_key)` 튜플의 리스트

### 선택 요소

- `__create_default_index__ = True`: `u_b`/`crt` 기본 인덱스 생성 (전체 데이터 시간순 조회용)
- `ptn` 필드: 파티션 구분용 문자열 (같은 테이블에 여러 모델 타입 저장 시)
- `id` 필드: UUID4 랜덤 생성 값으로 기본 제공되기 때문에 오버라이딩이 필요할때만 재선언 
- `crt` 필드: 생성일시 (초단위) 값으로 기본 제공되기 때문에 오버라이딩 필요할때만 재선언
### 중첩 모델

DynamoDB에 JSON으로 저장되는 중첩 객체는 `pydantic.BaseModel`을 상속합니다 (`DynamoModel` 아님).

```python
class Address(BaseModel):
    street: str
    city: str
    country: str

@sync
class User(DynamoModel):
    address: Address  # 자동 직렬화/역직렬화
```

### 주요 메서드

| 메서드 | 설명                   |
|--------|----------------------|
| `put()` | 아이템 생성               |
| `update()` | 변경된 필드만 업데이트         |
| `delete()` | 아이템 삭제               |
| `get(id)` | ID로 조회               |
| `batch_get(id_list)` | 여러 아이템 일괄 조회         |
| `query(pk_field, pk_value, ...)` | 인덱스 쿼리 (리스트 반환)      |
| `generate(pk_field, pk_value, ...)` | 인덱스 쿼리 (제너레이터 반환)    |
| `scan()` | 풀 스캔 (사용하는걸 추천하지 않음) |
| `model_dump()` | 딕셔너리 변환              |

### view 메서드 패턴

클라이언트에 반환할 데이터를 필터링하는 `view()` 메서드를 정의하세요.
어드민에서 사용하는 경우는 admin_view() 메서드를 정의해서 따로 사용하는걸 추천합니다.

```python
def view(self) -> dict:
    return {
        'name': self.name,
        'age': self.age
    }
```

---

## 인덱스 쿼리 가이드

### 기본 원칙: pk_field와 sk_field는 항상 필수

`query()`와 `generate()` 모두 `pk_field`와 `sk_field`를 명시적으로 지정해야 합니다.
이 두 필드의 조합이 어떤 GSI를 사용할지 결정합니다.

```python
# _indexes = [('status', 'crt')]  인 모델에서:

# ✅ 올바른 사용
items, end_key = MyModel.query(pk_field='status', pk_value='active', sk_field='crt')

# ❌ sk_field 생략하면 의도하지 않은 인덱스를 탈 수 있음
items, end_key = MyModel.query(pk_field='status', pk_value='active')
```

### query() vs generate() 사용 기준

#### query() → 컨트롤러에서 클라이언트 응답용

`query()`는 `(items, end_key)` 튜플을 반환합니다. 클라이언트에 페이지네이션을 제공해야 하는 컨트롤러에서 사용하세요.

```python
# 컨트롤러 예시
def list_users(self, start_key=None):
    users, end_key = User.query(
        pk_field='status', pk_value='active',
        sk_field='crt',
        start_key=start_key,
        reverse=True,  # 최신순
        limit=20
    )
    return {
        'items': [u.view() for u in users],
        'start_key': end_key  # 클라이언트가 다음 페이지 요청 시 이 값을 보내줌
    }
```

#### generate() → 서비스 내부 로직용

`generate()`는 이터레이터를 반환하며 내부적으로 페이지네이션을 자동 처리합니다. 전체 데이터를 순회해야 하는 서비스 내부 로직에서 사용하세요.

```python
# 서비스 예시: 전체 active 유저에게 알림 발송
def notify_all_active_users(self):
    for user in User.generate(pk_field='status', pk_value='active', sk_field='crt'):
        send_notification(user)
```

### sort key 조건 (sk_condition)

sk_value와 함께 사용하여 범위 쿼리를 수행합니다.

| 조건 | 설명 | 예시 |
|------|------|------|
| `eq` | 같음 (sk_value만 넣으면 자동 적용) | 특정 날짜 |
| `lt` / `lte` | 미만 / 이하 | 특정 시점 이전 |
| `gt` / `gte` | 초과 / 이상 | 특정 시점 이후 |
| `btw` | 범위 (sk_second_value 필요) | 기간 조회 |
| `stw` | 접두사 매칭 | 문자열 prefix |

```python
import time

# 최근 24시간 데이터
items, end_key = MyModel.query(
    pk_field='status', pk_value='active',
    sk_field='crt', sk_value=time.time() - 86400,
    sk_condition='gte'
)

# 특정 기간 범위
items, end_key = MyModel.query(
    pk_field='status', pk_value='active',
    sk_field='crt', sk_value=start_time,
    sk_second_value=end_time,
    sk_condition='btw'
)
```

### start_key / end_key 페이지네이션 패턴

```python
# 컨트롤러에서 페이지네이션
def list_items(self, start_key=None):
    items, end_key = MyModel.query(
        pk_field='category', pk_value='electronics',
        sk_field='crt',
        start_key=start_key,  # 클라이언트가 보내준 값 (첫 요청은 None)
        limit=20,
        reverse=True
    )
    return {
        'items': [item.view() for item in items],
        'start_key': end_key   # None이면 마지막 페이지
    }
```

### 주의사항

1. **인덱스에 정의되지 않은 pk_field/sk_field 조합을 사용하지 마세요.** `_indexes`에 등록된 조합만 GSI로 생성됩니다. 없는 조합으로 쿼리하면 풀 스캔이 발생하거나 에러가 납니다.

2. **filter_expression은 읽기 용량을 절약하지 않습니다.** 필터는 DynamoDB가 인덱스로 데이터를 가져온 후 적용됩니다. 자주 필터링하는 필드가 있다면 인덱스에 추가하는 것을 고려하세요.

3. **컨트롤러에서 generate()를 쓰지 마세요.** generate()는 전체 데이터를 순회하므로 API 응답 시간이 예측 불가능해집니다. 컨트롤러에서는 반드시 query()로 limit과 start_key 기반 페이지네이션을 사용하세요.

4. **reverse=True는 sort key 내림차순입니다.** 반대 정렬이 필요하면 `reverse=True`를 사용하세요. (crt 기준 최신순 = 내림차순)

5. **limit의 의미를 혼동하지 마세요.** limit는 DynamoDB 호출 한 번당 가져오는 최대 아이템 수입니다. filter_expression이 있으면 limit만큼 읽은 후 필터링하므로 반환되는 아이템 수가 limit보다 적을 수 있습니다. 이 경우 `min_returned_item_count` (query 전용)를 활용하세요.

### 새 모델 추가 후 할 일

`build_keeper.py`의 `sync_all_models` 호출에 해당 모델의 모듈 경로가 포함되어 있는지 확인하세요.
