# 13 · 문서 스키마 설계

**언제 읽나**: 저장 포맷(JSON)을 정하거나 바꿀 때.

---

## 권장 구조 (그대로 써도 된다)

```json
{
  "id": "doc_xxx",
  "title": "문서 제목",
  "preset": "insta-square",
  "theme": "toss",
  "pages": [
    {
      "id": "pg_xxx",
      "elements": [
        {
          "id": "el_xxx",
          "type": "text",
          "frame": { "x": 120, "y": 80, "w": 840, "h": 220 },
          "rotation": 0,
          "style": {
            "size": 48,
            "color": "@fg",
            "font": "pretendard",
            "fit": "grow"
          },
          "content": "텍스트",
          "assetId": null
        }
      ]
    }
  ],
  "updatedAt": "2026-10-17T13:15:50.914Z"
}
```

규칙:
- `frame`의 값은 **절대 디자인 px**. 비율이나 % 가 아니다.
- `style.size`도 **절대 px**.
- 색은 `"@fg"` 같은 **토큰 이름**으로 저장한다. hex를 직접 넣지 않는다.
- `fit`은 `"grow"` 또는 `"shrink"` 둘뿐. ❌ `"clip"` 을 만들지 않는다.
- 이미지는 `assetId` 문자열만. ❌ base64를 여기 넣지 않는다.

## 에셋은 별도 저장

```json
{
  "as_xxx": {
    "id": "as_xxx",
    "type": "image/jpeg",
    "w": 2048, "h": 1536,
    "bytes": 147320,
    "dataURL": "data:image/jpeg;base64,...",
    "createdAt": "..."
  }
}
```

**이유**: Undo가 스냅샷 방식이면, 문서에 base64가 있을 때 되돌리기마다 수 MB가 복제된다.
실측: 분리 후 4000×3000 사진에도 문서 1KB, 히스토리 6단계 3KB.

---

## 호환 처리는 한 곳에서만

저장된 문서를 읽을 때 **반드시 통과시키는 정규화 함수**를 둔다.
필드 추가·이름 변경을 전부 여기서 흡수한다. ❌ 컴포넌트마다 분기를 흩뿌리지 않는다.

```js
export function normalizeDoc(raw) {
  if (!raw) return createEmptyDoc();
  return {
    ...createEmptyDoc(),
    ...raw,
    pages: (raw.pages ?? []).map((p) => ({
      ...p,
      elements: (p.elements ?? []).map((el) => ({
        ...defaultElement(el.type),
        ...el,
        // 중첩 객체는 반드시 분리 병합한다 (통째 교체하면 값이 사라진다)
        frame: { ...defaultFrame(), ...el.frame },
        style: { ...defaultStyle(el.type), ...el.style },
      })),
    })),
  };
}
```

---

## 스키마를 바꿀 때 체크리스트

```
□ 옛날 문서를 실제로 열어서 테스트했다
□ 기본값을 추가했다면 기존 문서에도 들어간다
□ 필드를 제거했다면 남아 있어도 깨지지 않는다
□ 템플릿(같은 스키마를 쓴다면)도 함께 갱신했다
□ 검증 스크립트가 새 필드를 확인한다
```
