{
  "openapi": "3.1.0",
  "info": {
    "title": "다르만 출판사 API",
    "version": "1.0.0",
    "description": "다르만 출판사 소식지 서비스의 외부 연동 API입니다.\n\n## 인증\n`Authorization: Bearer <API_KEY>` 헤더를 보냅니다. (`X-API-Key` 도 가능)\n키는 관리자 화면 > 설정 > API 키에서 발급하며, 권한(scope)을 지정합니다.\n\n## AI 에이전트 작업 흐름\n1. `POST /api/v1/posts` 로 글을 올린다 → 컨펌 전 상태(`pending`)로 저장되고 검토 링크가 함께 반환된다.\n2. `GET /api/v1/review/queue?status=rewrite` 로 재작성 지시를 받은 글과 사유(`review_note`)를 가져온다.\n3. 같은 `slug` 로 `POST /api/v1/posts` 를 다시 호출하면 덮어쓰며 다시 `pending` 이 된다.\n4. 사람이 검토 링크나 메일에서 컨펌하면 게시되고 구독자에게 발송된다. (`POST /api/v1/posts/{id}/confirm` 으로도 가능)\n\n## 검토 웹훅\n글마다 발급되는 토큰으로 인증 없이 호출할 수 있는 주소가 있습니다(메일·슬랙 링크용).\n`POST /api/review/{token}/confirm`, `POST /api/review/{token}/rewrite`"
  },
  "servers": [
    {
      "url": "https://darman-pub.azurewebsites.net"
    }
  ],
  "tags": [
    {
      "name": "posts",
      "description": "글 작성·조회·수정"
    },
    {
      "name": "review",
      "description": "컨펌(승인)·재작성 지시"
    },
    {
      "name": "categories",
      "description": "주제"
    },
    {
      "name": "images",
      "description": "글에 쓸 이미지 업로드"
    },
    {
      "name": "site",
      "description": "화면 문구 · 메뉴 · 고정 페이지 · 배너(광고)"
    },
    {
      "name": "ops",
      "description": "운영 정보·발송·설정"
    },
    {
      "name": "webhook",
      "description": "토큰 기반 검토 웹훅 (API 키 불필요)"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "paths": {
    "/api/v1/categories": {
      "get": {
        "tags": [
          "categories"
        ],
        "summary": "주제 목록",
        "operationId": "listCategories",
        "parameters": [
          {
            "name": "locale",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "언어 (ko·ja·en·de). 생략하면 모든 언어"
          }
        ],
        "responses": {
          "200": {
            "description": "성공",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Category"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "API 키 없음·잘못됨",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "권한(scope) 부족",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "요청이 너무 많음",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/posts": {
      "get": {
        "tags": [
          "posts"
        ],
        "summary": "글 목록",
        "operationId": "listPosts",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "draft",
                "pending",
                "rewrite",
                "scheduled",
                "published",
                "archived"
              ]
            },
            "description": "상태 필터"
          },
          {
            "name": "category",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "주제 슬러그"
          },
          {
            "name": "q",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "제목·태그 검색"
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1
            },
            "description": "페이지"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100
            },
            "description": "페이지 크기(최대 100)"
          },
          {
            "name": "locale",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "언어 (ko·ja·en·de). 생략하면 모든 언어"
          }
        ],
        "responses": {
          "200": {
            "description": "성공",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PostList"
                }
              }
            }
          },
          "401": {
            "description": "API 키 없음·잘못됨",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "권한(scope) 부족",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "요청이 너무 많음",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "posts"
        ],
        "summary": "글 올리기(같은 slug 면 덮어쓰기)",
        "operationId": "createPost",
        "description": "컨펌 게이트가 켜져 있으면 `pending`(검토 대기)으로 저장되고, 응답에 검토 링크·웹훅 주소가 포함됩니다. 재작성 결과를 올릴 때도 같은 `slug` 로 호출하세요.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PostInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "기존 글 덮어씀",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PostSaved"
                }
              }
            }
          },
          "201": {
            "description": "새 글 생성",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PostSaved"
                }
              }
            }
          },
          "400": {
            "description": "입력 오류",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "API 키 없음·잘못됨",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "권한(scope) 부족",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "요청이 너무 많음",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/posts/{id}": {
      "get": {
        "tags": [
          "posts"
        ],
        "summary": "글 상세(본문 포함)",
        "operationId": "getPost",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "글 id"
          }
        ],
        "responses": {
          "200": {
            "description": "성공",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "post": {
                      "$ref": "#/components/schemas/Post"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "API 키 없음·잘못됨",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "권한(scope) 부족",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "없음",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "요청이 너무 많음",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "tags": [
          "posts"
        ],
        "summary": "글 수정",
        "operationId": "updatePost",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "글 id"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PostInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "성공",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PostSaved"
                }
              }
            }
          },
          "401": {
            "description": "API 키 없음·잘못됨",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "권한(scope) 부족",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "없음",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "요청이 너무 많음",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "posts"
        ],
        "summary": "글 삭제",
        "operationId": "deletePost",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "글 id"
          }
        ],
        "responses": {
          "200": {
            "description": "성공",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "deleted": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "API 키 없음·잘못됨",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "권한(scope) 부족",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "요청이 너무 많음",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/site": {
      "get": {
        "tags": [
          "site"
        ],
        "summary": "화면 문구 조회",
        "operationId": "getSite",
        "parameters": [
          {
            "name": "locale",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "언어 (ko·ja·en·de). 생략하면 모든 언어"
          }
        ],
        "description": "각 문구의 현재 값과 기본값·최대 길이·형식을 함께 돌려줍니다. `kind` 가 `rich` 면 `**굵게**` 와 `[링크](/주소)` 를, `lines` 면 줄바꿈을 쓸 수 있습니다.",
        "responses": {
          "200": {
            "description": "성공",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SiteSettings"
                }
              }
            }
          },
          "401": {
            "description": "API 키 없음·잘못됨",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "권한(scope) 부족",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "요청이 너무 많음",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "tags": [
          "site"
        ],
        "summary": "화면 문구 수정",
        "operationId": "updateSite",
        "parameters": [
          {
            "name": "locale",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "언어 (ko·ja·en·de). 생략하면 모든 언어"
          }
        ],
        "description": "보낸 키만 바뀝니다. 빈 문자열로 보내면 기본 문구로 되돌아갑니다. 어떤 값도 HTML 로 해석되지 않습니다.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": {
                  "type": "string"
                },
                "example": {
                  "hero_title": "매일 한 편,\n오늘의 그림과 이야기",
                  "footer_legal": "정보 제공 목적이며 투자 권유가 아닙니다."
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "성공",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SiteSettings"
                }
              }
            }
          },
          "400": {
            "description": "형식 오류(이메일·애드센스 ID 등)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "API 키 없음·잘못됨",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "권한(scope) 부족",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "요청이 너무 많음",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/menu": {
      "get": {
        "tags": [
          "site"
        ],
        "summary": "메뉴 목록",
        "operationId": "listMenu",
        "parameters": [
          {
            "name": "locale",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "언어 (ko·ja·en·de). 생략하면 모든 언어"
          }
        ],
        "responses": {
          "200": {
            "description": "성공",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/MenuItem"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "API 키 없음·잘못됨",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "권한(scope) 부족",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "요청이 너무 많음",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "site"
        ],
        "summary": "메뉴 추가",
        "operationId": "createMenu",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MenuInput"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "성공",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "item": {
                      "$ref": "#/components/schemas/MenuItem"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "입력 오류",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "API 키 없음·잘못됨",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "권한(scope) 부족",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "요청이 너무 많음",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/menu/{id}": {
      "patch": {
        "tags": [
          "site"
        ],
        "summary": "메뉴 수정",
        "operationId": "updateMenu",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "메뉴 id"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MenuInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "성공",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "item": {
                      "$ref": "#/components/schemas/MenuItem"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "API 키 없음·잘못됨",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "권한(scope) 부족",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "없음",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "요청이 너무 많음",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "site"
        ],
        "summary": "메뉴 삭제",
        "operationId": "deleteMenu",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "메뉴 id"
          }
        ],
        "responses": {
          "200": {
            "description": "성공",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "API 키 없음·잘못됨",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "권한(scope) 부족",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "없음",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "요청이 너무 많음",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/pages": {
      "get": {
        "tags": [
          "site"
        ],
        "summary": "고정 페이지 목록",
        "operationId": "listPages",
        "parameters": [
          {
            "name": "locale",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "언어 (ko·ja·en·de). 생략하면 모든 언어"
          }
        ],
        "responses": {
          "200": {
            "description": "성공",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "builtin": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Page"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "API 키 없음·잘못됨",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "권한(scope) 부족",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "요청이 너무 많음",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "site"
        ],
        "summary": "고정 페이지 만들기",
        "operationId": "createPage",
        "description": "`about` · `privacy` · `terms` 로 만들면 기본 화면을 대신합니다. 그 밖의 주소는 `/page/{slug}` 로 열립니다.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PageInput"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "성공",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "page": {
                      "$ref": "#/components/schemas/Page"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "API 키 없음·잘못됨",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "권한(scope) 부족",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "이미 있는 주소",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "요청이 너무 많음",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/pages/{id}": {
      "patch": {
        "tags": [
          "site"
        ],
        "summary": "고정 페이지 수정",
        "operationId": "updatePage",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "페이지 id"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PageInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "성공",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "page": {
                      "$ref": "#/components/schemas/Page"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "API 키 없음·잘못됨",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "권한(scope) 부족",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "없음",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "요청이 너무 많음",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "site"
        ],
        "summary": "고정 페이지 삭제",
        "operationId": "deletePage",
        "description": "기본 화면이 있는 주소라면 지운 뒤 기본 화면이 다시 나갑니다.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "페이지 id"
          }
        ],
        "responses": {
          "200": {
            "description": "성공",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "API 키 없음·잘못됨",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "권한(scope) 부족",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "없음",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "요청이 너무 많음",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/banners": {
      "get": {
        "tags": [
          "site"
        ],
        "summary": "배너 목록(노출·클릭 포함)",
        "operationId": "listBanners",
        "parameters": [
          {
            "name": "locale",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "언어 (ko·ja·en·de). 생략하면 모든 언어"
          }
        ],
        "responses": {
          "200": {
            "description": "성공",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "slots": {
                      "type": "object",
                      "additionalProperties": {
                        "type": "string"
                      }
                    },
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Banner"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "API 키 없음·잘못됨",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "권한(scope) 부족",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "요청이 너무 많음",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "site"
        ],
        "summary": "배너 만들기",
        "operationId": "createBanner",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BannerInput"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "성공",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "banner": {
                      "$ref": "#/components/schemas/Banner"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "입력 오류",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "API 키 없음·잘못됨",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "권한(scope) 부족",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "요청이 너무 많음",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/banners/{id}": {
      "patch": {
        "tags": [
          "site"
        ],
        "summary": "배너 수정",
        "operationId": "updateBanner",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "배너 id"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BannerInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "성공",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "banner": {
                      "$ref": "#/components/schemas/Banner"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "API 키 없음·잘못됨",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "권한(scope) 부족",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "없음",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "요청이 너무 많음",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "site"
        ],
        "summary": "배너 삭제",
        "operationId": "deleteBanner",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "배너 id"
          }
        ],
        "responses": {
          "200": {
            "description": "성공",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "API 키 없음·잘못됨",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "권한(scope) 부족",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "없음",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "요청이 너무 많음",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/images": {
      "get": {
        "tags": [
          "images"
        ],
        "summary": "올린 이미지 목록",
        "operationId": "listImages",
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1
            },
            "description": "페이지"
          }
        ],
        "responses": {
          "200": {
            "description": "성공",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ImageList"
                }
              }
            }
          },
          "401": {
            "description": "API 키 없음·잘못됨",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "권한(scope) 부족",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "요청이 너무 많음",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "images"
        ],
        "summary": "이미지 올리기",
        "operationId": "uploadImage",
        "description": "두 가지 방법 중 편한 쪽을 쓰세요.\n\n1. **바이트 그대로** — `Content-Type: image/png`(또는 image/jpeg·image/gif·image/webp) 로 파일 내용을 본문에 담아 보냅니다. 설명은 `?alt=` 쿼리로 붙입니다.\n2. **JSON** — `Content-Type: application/json` 으로 `{ \"data_base64\": \"...\", \"alt\": \"설명\" }` 을 보냅니다. `data:image/png;base64,` 접두사가 붙어 있어도 됩니다.\n\n형식은 파일 앞부분 바이트로 판별하므로 확장자·Content-Type 을 속일 수 없습니다. SVG 는 받지 않습니다.\n한 장 6MB, 가로·세로 12000px, 4천만 화소까지입니다.\n같은 내용을 다시 올리면 새로 저장하지 않고 기존 이미지를 그대로 돌려줍니다(`deduped: true`).\n\n응답의 `url` 을 본문 마크다운 `![설명](url)` 에 넣거나, 글의 `cover_image` 로 지정하세요.",
        "requestBody": {
          "required": true,
          "content": {
            "image/png": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            },
            "image/jpeg": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            },
            "image/gif": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            },
            "image/webp": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            },
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ImageInput"
              }
            }
          }
        },
        "parameters": [
          {
            "name": "alt",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "대체 텍스트(바이트로 보낼 때)"
          }
        ],
        "responses": {
          "200": {
            "description": "같은 이미지가 이미 있어 그것을 돌려줌",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ImageSaved"
                }
              }
            }
          },
          "201": {
            "description": "새로 저장됨",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ImageSaved"
                }
              }
            }
          },
          "401": {
            "description": "API 키 없음·잘못됨",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "권한(scope) 부족",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "용량·해상도 초과",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "description": "허용하지 않는 형식(SVG 등)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "요청이 너무 많음",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/images/{hex}": {
      "get": {
        "tags": [
          "images"
        ],
        "summary": "이미지 한 장",
        "operationId": "getImage",
        "parameters": [
          {
            "name": "hex",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "이미지 id (32자리 hex)"
          }
        ],
        "responses": {
          "200": {
            "description": "성공",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ImageSaved"
                }
              }
            }
          },
          "401": {
            "description": "API 키 없음·잘못됨",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "권한(scope) 부족",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "없음",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "요청이 너무 많음",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "images"
        ],
        "summary": "이미지 삭제",
        "operationId": "deleteImage",
        "description": "이 이미지를 쓰는 글이 있으면 409 로 막습니다. 그래도 지우려면 `?force=1` 을 붙이세요.",
        "parameters": [
          {
            "name": "hex",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "이미지 id (32자리 hex)"
          },
          {
            "name": "force",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "1"
              ]
            },
            "description": "쓰는 글이 있어도 삭제"
          }
        ],
        "responses": {
          "200": {
            "description": "성공",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "deleted": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "API 키 없음·잘못됨",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "권한(scope) 부족",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "없음",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "사용 중",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "요청이 너무 많음",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/posts/preview": {
      "post": {
        "tags": [
          "posts"
        ],
        "summary": "마크다운 → HTML 미리보기",
        "operationId": "previewMarkdown",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "body_md": {
                    "type": "string"
                  }
                },
                "required": [
                  "body_md"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "성공",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "html": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "API 키 없음·잘못됨",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "권한(scope) 부족",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "요청이 너무 많음",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/review/queue": {
      "get": {
        "tags": [
          "review"
        ],
        "summary": "검토 대기·재작성 목록",
        "operationId": "reviewQueue",
        "description": "`status=rewrite` 로 조회하면 재작성 지시를 받은 글과 사유(`review_note`)를 가져올 수 있습니다.",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "pending",
                "rewrite"
              ]
            },
            "description": "기본 pending"
          },
          {
            "name": "body",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "1"
              ]
            },
            "description": "1 이면 본문 포함"
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            },
            "description": "페이지"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            },
            "description": "페이지 크기"
          }
        ],
        "responses": {
          "200": {
            "description": "성공",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PostList"
                }
              }
            }
          },
          "401": {
            "description": "API 키 없음·잘못됨",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "권한(scope) 부족",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "요청이 너무 많음",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/posts/{id}/confirm": {
      "post": {
        "tags": [
          "review"
        ],
        "summary": "컨펌(승인) → 게시",
        "operationId": "confirmPost",
        "description": "예약 시각이 미래면 `scheduled` 로 두어 그 시각에 발송하고, 아니면 즉시 게시하며 구독자 발송 큐를 적재합니다. 과거에 게시된 적 있는 글은 원래 게시 시각을 유지하고 재발송하지 않습니다.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "글 id"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "notify": {
                    "type": "boolean",
                    "default": true,
                    "description": "false 면 메일 발송 큐를 적재하지 않음"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "성공",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConfirmResult"
                }
              }
            }
          },
          "401": {
            "description": "API 키 없음·잘못됨",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "권한(scope) 부족",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "없음",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "요청이 너무 많음",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/posts/{id}/rewrite": {
      "post": {
        "tags": [
          "review"
        ],
        "summary": "재작성 지시",
        "operationId": "rewritePost",
        "description": "공개를 내리고 사유를 남깁니다. 에이전트는 `GET /api/v1/review/queue?status=rewrite` 로 사유를 읽어 다시 작성합니다.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "글 id"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "note": {
                    "type": "string",
                    "description": "재작성 사유"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "성공",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "post": {
                      "$ref": "#/components/schemas/Post"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "API 키 없음·잘못됨",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "권한(scope) 부족",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "없음",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "요청이 너무 많음",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/posts/{id}/publish": {
      "post": {
        "tags": [
          "review"
        ],
        "summary": "즉시 게시",
        "operationId": "publishPost",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "글 id"
          }
        ],
        "responses": {
          "200": {
            "description": "성공",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PostSaved"
                }
              }
            }
          },
          "401": {
            "description": "API 키 없음·잘못됨",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "권한(scope) 부족",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "요청이 너무 많음",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/posts/{id}/resend": {
      "post": {
        "tags": [
          "ops"
        ],
        "summary": "이미 게시된 글 재발송 큐 적재",
        "operationId": "resendPost",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "글 id"
          }
        ],
        "responses": {
          "200": {
            "description": "성공",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "queued": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "API 키 없음·잘못됨",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "권한(scope) 부족",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "요청이 너무 많음",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/review/notify": {
      "post": {
        "tags": [
          "review"
        ],
        "summary": "검토 요청 메일 발송(관리자)",
        "operationId": "notifyReview",
        "description": "검토 대기 글을 묶어 관리자에게 메일로 보냅니다. 메일 안의 링크에서 바로 컨펌·재작성할 수 있습니다.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "limit": {
                    "type": "integer",
                    "default": 10,
                    "maximum": 50
                  },
                  "to": {
                    "type": "string",
                    "format": "email",
                    "description": "지정하면 이 주소로만 발송"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "성공",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "posts": {
                      "type": "integer"
                    },
                    "sent": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "API 키 없음·잘못됨",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "권한(scope) 부족",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "요청이 너무 많음",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/stats": {
      "get": {
        "tags": [
          "ops"
        ],
        "summary": "운영 통계",
        "operationId": "getStats",
        "responses": {
          "200": {
            "description": "성공",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Stats"
                }
              }
            }
          },
          "401": {
            "description": "API 키 없음·잘못됨",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "권한(scope) 부족",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "요청이 너무 많음",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/deliveries": {
      "get": {
        "tags": [
          "ops"
        ],
        "summary": "발송 내역(수신자 마스킹)",
        "operationId": "listDeliveries",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "상태"
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            },
            "description": "페이지"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            },
            "description": "페이지 크기"
          }
        ],
        "responses": {
          "200": {
            "description": "성공",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "API 키 없음·잘못됨",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "권한(scope) 부족",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "요청이 너무 많음",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/newsletter/tick": {
      "post": {
        "tags": [
          "ops"
        ],
        "summary": "발송 작업 실행(예약 게시·다이제스트·큐 처리)",
        "operationId": "newsletterTick",
        "responses": {
          "200": {
            "description": "성공",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "API 키 없음·잘못됨",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "권한(scope) 부족",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "요청이 너무 많음",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/settings": {
      "get": {
        "tags": [
          "ops"
        ],
        "summary": "설정 조회",
        "operationId": "getSettings",
        "responses": {
          "200": {
            "description": "성공",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "API 키 없음·잘못됨",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "권한(scope) 부족",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "요청이 너무 많음",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "tags": [
          "ops"
        ],
        "summary": "설정 변경",
        "operationId": "updateSettings",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Settings"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "성공",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "API 키 없음·잘못됨",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "권한(scope) 부족",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "요청이 너무 많음",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/review/{token}/confirm": {
      "post": {
        "tags": [
          "webhook"
        ],
        "summary": "검토 토큰으로 컨펌",
        "operationId": "webhookConfirm",
        "security": [],
        "parameters": [
          {
            "name": "token",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "글별 검토 토큰"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "by": {
                    "type": "string",
                    "description": "누가 눌렀는지 기록용"
                  },
                  "notify": {
                    "type": "boolean",
                    "default": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "성공",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConfirmResult"
                }
              }
            }
          },
          "404": {
            "description": "토큰 없음",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/review/{token}/rewrite": {
      "post": {
        "tags": [
          "webhook"
        ],
        "summary": "검토 토큰으로 재작성 지시",
        "operationId": "webhookRewrite",
        "security": [],
        "parameters": [
          {
            "name": "token",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "글별 검토 토큰"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "note": {
                    "type": "string"
                  },
                  "by": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "성공",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "description": "토큰 없음",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/review/{token}": {
      "get": {
        "tags": [
          "webhook"
        ],
        "summary": "검토 대상 상태 조회",
        "operationId": "webhookStatus",
        "security": [],
        "parameters": [
          {
            "name": "token",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "글별 검토 토큰"
          }
        ],
        "responses": {
          "200": {
            "description": "성공",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "description": "토큰 없음",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "발급된 API 키. 권한: content:read(콘텐츠 읽기), content:write(콘텐츠 작성·수정), review:write(컨펌·재작성), admin:read(운영 정보 읽기(통계·발송·회원)), admin:write(운영 설정 변경), affiliate:write(제휴 상품 승인·숨김(쿠팡 링크만))"
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          }
        },
        "required": [
          "error"
        ]
      },
      "Category": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "slug": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "emoji": {
            "type": "string"
          },
          "color": {
            "type": "string"
          },
          "series_label": {
            "type": "string"
          },
          "is_active": {
            "type": "boolean"
          },
          "next_series_no": {
            "type": "integer"
          }
        }
      },
      "Post": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "slug": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "summary": {
            "type": "string"
          },
          "locale": {
            "type": "string",
            "description": "이 글이 속한 언어 사이트 (주제에서 따라온다)"
          },
          "category": {
            "type": "string",
            "description": "주제 슬러그"
          },
          "category_id": {
            "type": "integer"
          },
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "pending",
              "rewrite",
              "scheduled",
              "published",
              "archived"
            ]
          },
          "series_no": {
            "type": [
              "integer",
              "null"
            ]
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "author": {
            "type": "string"
          },
          "source": {
            "type": "string"
          },
          "publish_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "published_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "approved_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "approved_by": {
            "type": [
              "string",
              "null"
            ]
          },
          "review_note": {
            "type": "string",
            "description": "재작성 사유"
          },
          "url": {
            "type": "string"
          },
          "body_md": {
            "type": "string"
          },
          "body_html": {
            "type": "string"
          },
          "cover_image": {
            "type": "string",
            "description": "대표 이미지 경로 (/uploads/…). 카드·메일·공유 미리보기에 쓰인다"
          },
          "cover_image_url": {
            "type": "string"
          },
          "cover_alt": {
            "type": "string"
          },
          "review_url": {
            "type": "string",
            "description": "사람이 검토하는 화면"
          },
          "confirm_webhook": {
            "type": "string"
          },
          "rewrite_webhook": {
            "type": "string"
          }
        }
      },
      "PostList": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean"
          },
          "total": {
            "type": "integer"
          },
          "page": {
            "type": "integer"
          },
          "pages": {
            "type": "integer"
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Post"
            }
          }
        }
      },
      "PostSaved": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean"
          },
          "created": {
            "type": "boolean"
          },
          "queued": {
            "type": "integer",
            "description": "적재된 메일 발송 건수"
          },
          "review_required": {
            "type": "boolean"
          },
          "post": {
            "$ref": "#/components/schemas/Post"
          }
        }
      },
      "PostInput": {
        "type": "object",
        "required": [
          "category",
          "title",
          "body_md"
        ],
        "properties": {
          "category": {
            "type": "string",
            "description": "주제 슬러그 (예: healing). 글의 언어는 이 주제를 따른다"
          },
          "category_id": {
            "type": "integer"
          },
          "title": {
            "type": "string",
            "maxLength": 120
          },
          "summary": {
            "type": "string",
            "maxLength": 300,
            "description": "비우면 본문에서 자동 생성"
          },
          "body_md": {
            "type": "string",
            "description": "마크다운 본문 (최대 60000자)"
          },
          "slug": {
            "type": "string",
            "description": "영문 소문자·숫자·하이픈. 같은 slug 가 있으면 덮어쓴다"
          },
          "series_no": {
            "oneOf": [
              {
                "type": "integer"
              },
              {
                "type": "string",
                "enum": [
                  "auto"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "pending",
              "scheduled",
              "published"
            ],
            "description": "컨펌 게이트가 켜져 있으면 pending 으로 강제됩니다"
          },
          "publish_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "예약 시각(미래면 컨펌 후 그 시각에 발송)"
          },
          "cover_image": {
            "type": "string",
            "description": "대표 이미지. POST /api/v1/images 로 받은 url 을 그대로 넣습니다. 빈 문자열이면 해제"
          },
          "cover_alt": {
            "type": "string",
            "maxLength": 160,
            "description": "대표 이미지 대체 텍스트"
          },
          "author": {
            "type": "string"
          },
          "notify": {
            "type": "boolean",
            "default": true
          },
          "backdate": {
            "type": "boolean",
            "description": "과거 publish_at 을 게시 시각으로 (백필용)"
          }
        }
      },
      "SiteSettings": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean"
          },
          "values": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "현재 문구 (설정하지 않았으면 기본값)"
          },
          "fields": {
            "type": "object",
            "additionalProperties": {
              "type": "object",
              "properties": {
                "default": {
                  "type": "string"
                },
                "maxLength": {
                  "type": "integer"
                },
                "kind": {
                  "type": "string",
                  "enum": [
                    "plain",
                    "rich",
                    "lines",
                    "email",
                    "adsense"
                  ]
                }
              }
            }
          }
        }
      },
      "MenuItem": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "area": {
            "type": "string",
            "enum": [
              "header",
              "footer_browse",
              "footer_subscribe",
              "footer_about"
            ]
          },
          "label": {
            "type": "string"
          },
          "url": {
            "type": "string"
          },
          "sort_order": {
            "type": "integer"
          },
          "is_active": {
            "type": "integer"
          },
          "new_tab": {
            "type": "integer"
          }
        }
      },
      "MenuInput": {
        "type": "object",
        "required": [
          "area",
          "label",
          "url"
        ],
        "properties": {
          "area": {
            "type": "string",
            "enum": [
              "header",
              "footer_browse",
              "footer_subscribe",
              "footer_about"
            ],
            "description": "상단 메뉴 / 푸터의 어느 칸"
          },
          "label": {
            "type": "string",
            "maxLength": 30
          },
          "url": {
            "type": "string",
            "description": "/ 로 시작하는 사이트 내 경로이거나 http(s)·mailto 주소"
          },
          "sort_order": {
            "type": "integer"
          },
          "is_active": {
            "type": "boolean"
          },
          "new_tab": {
            "type": "boolean"
          }
        }
      },
      "Page": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "slug": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "is_active": {
            "type": "integer"
          },
          "in_menu": {
            "type": "integer"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "PageInput": {
        "type": "object",
        "required": [
          "slug",
          "title",
          "body_md"
        ],
        "properties": {
          "slug": {
            "type": "string",
            "description": "영문 소문자·숫자·하이픈. about/privacy/terms 는 기본 화면을 대신한다"
          },
          "title": {
            "type": "string",
            "maxLength": 80
          },
          "body_md": {
            "type": "string",
            "description": "마크다운 (최대 60000자)"
          },
          "description": {
            "type": "string",
            "maxLength": 300,
            "description": "검색결과용. 비우면 본문에서 생성"
          },
          "is_active": {
            "type": "boolean"
          },
          "in_menu": {
            "type": "boolean",
            "description": "푸터에 링크를 넣을지"
          }
        }
      },
      "Banner": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "slot": {
            "type": "string"
          },
          "kind": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "body": {
            "type": "string"
          },
          "cta_label": {
            "type": "string"
          },
          "url": {
            "type": "string"
          },
          "image": {
            "type": "string"
          },
          "image_alt": {
            "type": "string"
          },
          "ad_slot": {
            "type": "string"
          },
          "category": {
            "type": [
              "string",
              "null"
            ]
          },
          "audience": {
            "type": "string"
          },
          "starts_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "ends_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "weight": {
            "type": "integer"
          },
          "is_active": {
            "type": "integer"
          },
          "dismissible": {
            "type": "integer"
          },
          "impressions": {
            "type": "integer"
          },
          "clicks": {
            "type": "integer"
          }
        }
      },
      "BannerInput": {
        "type": "object",
        "required": [
          "slot",
          "kind"
        ],
        "properties": {
          "slot": {
            "type": "string",
            "enum": [
              "topbar",
              "home_hero",
              "home_mid",
              "list_top",
              "post_top",
              "post_bottom",
              "footer_top"
            ],
            "description": "화면의 어느 자리에 띄울지"
          },
          "kind": {
            "type": "string",
            "enum": [
              "text",
              "image",
              "notice",
              "adsense"
            ],
            "description": "notice 는 topbar 전용, adsense 는 게시자 ID 가 설정돼 있어야 한다"
          },
          "name": {
            "type": "string",
            "maxLength": 60,
            "description": "관리용 이름"
          },
          "title": {
            "type": "string",
            "maxLength": 120,
            "description": "**굵게**·[링크](/주소) 가능"
          },
          "body": {
            "type": "string",
            "maxLength": 500,
            "description": "**굵게**·[링크](/주소) 가능"
          },
          "cta_label": {
            "type": "string",
            "maxLength": 30
          },
          "url": {
            "type": "string",
            "description": "/ 로 시작하는 경로이거나 http(s) 주소. 클릭은 /b/{id} 로 계측된다"
          },
          "image": {
            "type": "string",
            "description": "업로드한 이미지의 url (/uploads/…). 외부 이미지는 받지 않는다"
          },
          "image_alt": {
            "type": "string",
            "maxLength": 160
          },
          "ad_slot": {
            "type": "string",
            "description": "애드센스 광고 단위의 data-ad-slot 값"
          },
          "category": {
            "type": "string",
            "description": "이 주제의 화면에서만 노출 (슬러그)"
          },
          "category_id": {
            "type": "integer"
          },
          "audience": {
            "type": "string",
            "enum": [
              "all",
              "guest",
              "member"
            ]
          },
          "starts_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "ends_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "weight": {
            "type": "integer",
            "description": "같은 자리에 여럿이면 큰 쪽이 먼저"
          },
          "is_active": {
            "type": "boolean"
          },
          "dismissible": {
            "type": "boolean",
            "description": "공지 띠에 닫기 버튼"
          }
        }
      },
      "Image": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "32자리 hex"
          },
          "url": {
            "type": "string",
            "description": "사이트 기준 경로 (예: /uploads/ab….jpg). 본문 마크다운·cover_image 에 이 값을 씁니다"
          },
          "absolute_url": {
            "type": "string",
            "description": "도메인까지 붙인 주소"
          },
          "mime": {
            "type": "string",
            "enum": [
              "image/jpeg",
              "image/png",
              "image/gif",
              "image/webp"
            ]
          },
          "bytes": {
            "type": "integer"
          },
          "width": {
            "type": "integer"
          },
          "height": {
            "type": "integer"
          },
          "alt": {
            "type": "string"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ImageInput": {
        "type": "object",
        "required": [
          "data_base64"
        ],
        "properties": {
          "data_base64": {
            "type": "string",
            "description": "이미지 바이트의 base64. data:…;base64, 접두사가 있어도 됩니다"
          },
          "alt": {
            "type": "string",
            "maxLength": 160,
            "description": "대체 텍스트"
          }
        }
      },
      "ImageSaved": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean"
          },
          "image": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Image"
              },
              {
                "type": "object",
                "properties": {
                  "deduped": {
                    "type": "boolean",
                    "description": "같은 이미지가 이미 있어 재사용했는지"
                  }
                }
              }
            ]
          }
        }
      },
      "ImageList": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean"
          },
          "total": {
            "type": "integer"
          },
          "page": {
            "type": "integer"
          },
          "pages": {
            "type": "integer"
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Image"
            }
          }
        }
      },
      "ConfirmResult": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean"
          },
          "result": {
            "type": "string",
            "enum": [
              "published",
              "scheduled",
              "restored",
              "already_published"
            ]
          },
          "queued": {
            "type": "integer"
          },
          "post": {
            "$ref": "#/components/schemas/Post"
          }
        }
      },
      "Stats": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean"
          },
          "members": {
            "type": "integer"
          },
          "subscribers": {
            "type": "integer"
          },
          "posts": {
            "type": "integer"
          },
          "pending": {
            "type": "integer"
          },
          "rewrite": {
            "type": "integer"
          },
          "scheduled": {
            "type": "integer"
          },
          "queued": {
            "type": "integer"
          },
          "sent24h": {
            "type": "integer"
          }
        }
      },
      "Settings": {
        "type": "object",
        "properties": {
          "review_required": {
            "type": "string",
            "enum": [
              "0",
              "1"
            ],
            "description": "새 글을 컨펌 전 상태로 받을지"
          },
          "review_notify": {
            "type": "string",
            "enum": [
              "0",
              "1"
            ],
            "description": "검토 대기 글이 생기면 관리자에게 메일"
          },
          "agent_review": {
            "type": "string",
            "enum": [
              "0",
              "1"
            ]
          },
          "tagline": {
            "type": "string"
          },
          "footer_note": {
            "type": "string"
          }
        }
      }
    }
  }
}