{
  "openapi": "3.1.0",
  "info": {
    "title": "SAZU 사주 API · 만세력 API",
    "version": "2026-08-29",
    "summary": "생년월일로 사주팔자·만세력·대운·오행·신살·합형충파해·격국·용신을 계산하는 한국어 REST API",
    "description": "생년월일·출생시간·성별을 보내면 15개 분석 모듈을 JSON 으로 돌려줍니다.\n\n이 스펙은 공개 문서(https://www.sazu.app/manse-api/docs)에 실린 엔드포인트만 담습니다.\n사람이 읽는 사본은 https://www.sazu.app/manse-api/docs.md 에 있습니다.\n\nFree 플랜은 샌드박스로 동작해 문서에 공개된 샘플 프로필 5종의 입력과 정확히 일치할 때만 응답합니다.\n그 밖의 생년월일은 400 SAMPLE_PROFILE_REQUIRED 로 응답하며, 이는 오류가 아니라 Free 플랜의 정상 동작입니다.\n\n요청 한도 — Free: 월 500회 · 분당 10회,\nPro: 월 10,000회 · 분당 30회.",
    "contact": {
      "name": "SAZU",
      "email": "contact@sazu.app",
      "url": "https://www.sazu.app/manse-api"
    },
    "termsOfService": "https://www.sazu.app/manse-api/legal/terms"
  },
  "externalDocs": {
    "description": "API 문서",
    "url": "https://www.sazu.app/manse-api/docs"
  },
  "servers": [
    {
      "url": "https://api.sazu.app",
      "description": "production"
    }
  ],
  "security": [
    {
      "apiKeyHeader": []
    },
    {
      "bearerAuth": []
    }
  ],
  "tags": [
    {
      "name": "sazu",
      "description": "사주 분석"
    },
    {
      "name": "calendar",
      "description": "음양력 변환"
    },
    {
      "name": "account",
      "description": "키 상태·오류 이력"
    }
  ],
  "paths": {
    "/v1/sazu/calculate": {
      "post": {
        "summary": "사주 분석 (핵심)",
        "operationId": "sazuCalculate",
        "tags": [
          "sazu"
        ],
        "responses": {
          "200": {
            "description": "성공",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "분당 허용 요청 수",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "현재 창에서 남은 요청 수",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "한도가 초기화되는 Unix 초",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SazuCalculateResponse"
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR: 요청 바디의 파라미터가 잘못되었습니다. error.details 에서 누락·타입 오류 필드를 확인하세요.\nSAMPLE_PROFILE_REQUIRED: Free(샌드박스) 키로 샘플 프로필과 다른 생년월일을 요청했습니다. 문서의 샘플 프로필 5종의 입력값과 정확히 일치해야 응답합니다. 응답의 error.details.sampleProfiles 에 전체 목록이 함께 반환되므로 코드에서 바로 참조하실 수 있습니다. 실제 생년월일 계산은 Pro 에서 제공됩니다.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "success": false,
                  "error": {
                    "code": "VALIDATION_ERROR",
                    "message": "요청 바디의 파라미터가 잘못되었습니다. error.details 에서 누락·타입 오류 필드를 확인하세요."
                  }
                }
              }
            }
          },
          "401": {
            "description": "MISSING_API_KEY: 인증 헤더가 누락되었습니다. x-api-key 또는 Authorization: Bearer <키> 헤더 중 하나를 포함해야 합니다. 헤더 이름의 대소문자는 무관하지만 철자는 정확히 일치해야 합니다.\nINVALID_API_KEY: API 키가 존재하지 않습니다. 헤더 값(x-api-key 또는 Authorization: Bearer)이 대시보드의 키와 일치하는지 확인하세요.\nKEY_REVOKED: 키가 폐기(revoke)되었습니다. 대시보드에서 새 키를 발급한 뒤 코드에 반영하세요.\nKEY_EXPIRED: 키의 만료일이 지났습니다. 대시보드에서 만료일을 갱신하거나 새 키를 발급하세요.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "success": false,
                  "error": {
                    "code": "MISSING_API_KEY",
                    "message": "인증 헤더가 누락되었습니다. x-api-key 또는 Authorization: Bearer <키> 헤더 중 하나를 포함해야 합니다. 헤더 이름의 대소문자는 무관하지만 철자는 정확히 일치해야 합니다."
                  }
                }
              }
            }
          },
          "403": {
            "description": "ORIGIN_NOT_ALLOWED: 요청 Origin 헤더가 키의 허용 도메인에 등록되지 않았습니다. Pro 키의 경우 대시보드 → API 키 → \"도메인\" 버튼에서 호출할 도메인을 등록하세요. 서버 사이드(Origin 헤더 없음) 호출은 등록 여부와 무관하게 통과합니다.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "success": false,
                  "error": {
                    "code": "ORIGIN_NOT_ALLOWED",
                    "message": "요청 Origin 헤더가 키의 허용 도메인에 등록되지 않았습니다. Pro 키의 경우 대시보드 → API 키 → \"도메인\" 버튼에서 호출할 도메인을 등록하세요. 서버 사이드(Origin 헤더 없음) 호출은 등록 여부와 무관하게 통과합니다."
                  }
                }
              }
            }
          },
          "429": {
            "description": "RATE_LIMIT_EXCEEDED: 분당 요청 한도를 초과했습니다. X-RateLimit-Reset 헤더의 Unix 타임스탬프 이후 재시도하세요.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "success": false,
                  "error": {
                    "code": "RATE_LIMIT_EXCEEDED",
                    "message": "분당 요청 한도를 초과했습니다. X-RateLimit-Reset 헤더의 Unix 타임스탬프 이후 재시도하세요."
                  }
                }
              }
            }
          },
          "500": {
            "description": "INTERNAL_ERROR: 서버 내부 오류입니다. 동일한 요청이 반복 실패하면 문의해 주세요.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "success": false,
                  "error": {
                    "code": "INTERNAL_ERROR",
                    "message": "서버 내부 오류입니다. 동일한 요청이 반복 실패하면 문의해 주세요."
                  }
                }
              }
            }
          },
          "503": {
            "description": "AUTH_UNAVAILABLE: 인증 서비스에 일시적 장애가 발생했습니다. 잠시 후 재시도하세요. 반복될 경우 문의해 주세요.\nQUOTA_UNAVAILABLE: 사용량 집계 서비스에 일시적 장애가 발생했습니다. 잠시 후 재시도하세요.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "success": false,
                  "error": {
                    "code": "AUTH_UNAVAILABLE",
                    "message": "인증 서비스에 일시적 장애가 발생했습니다. 잠시 후 재시도하세요. 반복될 경우 문의해 주세요."
                  }
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "birthYear": {
                    "type": "integer",
                    "description": "출생년도 (1900~2100). 정수 문자열(\"1990\") 도 허용."
                  },
                  "birthMonth": {
                    "type": "integer",
                    "description": "출생월 (1~12). 정수 문자열(\"3\") 도 허용. JavaScript Date.getMonth() 의 0~11 그대로 전송하면 거부됩니다."
                  },
                  "birthDay": {
                    "type": "integer",
                    "description": "출생일 (1~31). 정수 문자열도 허용."
                  },
                  "birthHour": {
                    "type": [
                      "integer",
                      "null"
                    ],
                    "description": "출생시 (0~23), 모르면 null. 정수 문자열도 허용."
                  },
                  "birthMinute": {
                    "type": "integer",
                    "description": "출생분 (0~59), 기본값 0. 정수 문자열도 허용."
                  },
                  "isFemale": {
                    "type": "boolean",
                    "description": "여성 여부. 누락 시 기본값 false(남성)로 처리되며 응답 헤더 X-Input-Coerced 와 meta.warnings 로 통지합니다. 성별은 사주 결과(대운 진행 방향·해석)에 큰 영향을 주므로 명시 권장."
                  },
                  "isLunar": {
                    "type": "boolean",
                    "description": "음력 여부, 기본값 false. 음력으로 보내실 때는 윤달 여부(isLeapMonth)를 함께 확인하세요 — 아래 항목 참조."
                  },
                  "isLeapMonth": {
                    "type": "boolean",
                    "description": "음력 입력이 윤달인지 여부, 기본값 false. isLunar: true 일 때만 사용합니다. 윤달이 드는 해에는 같은 달이 두 번 오므로 연·월·일만으로는 날짜가 하나로 정해지지 않습니다. 예를 들어 1998년에는 5월이 두 번(평5월·윤5월) 있어 음력 1998-05-15 는 양력 6월 9일과 7월 8일 두 가지가 되고, 29일 차이라 월주·일주·시주가 모두 달라집니다. 윤달 출생이면 true 를 보내주세요. [2026-08-22 반영]"
                  },
                  "birthCity": {
                    "type": "string",
                    "description": "출생 도시, 기본값 \"서울\""
                  },
                  "locale": {
                    "type": "string",
                    "enum": [
                      "ko",
                      "han"
                    ],
                    "description": "한글 또는 한자, 기본값 \"ko\""
                  },
                  "modules": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "enum": [
                        "fourPillars",
                        "decadeFortune",
                        "elements",
                        "summary",
                        "sinStrength",
                        "sinsal",
                        "relationships",
                        "ghostElements",
                        "gyeokguk",
                        "yongsin",
                        "weolun",
                        "seun",
                        "wongukInteraction",
                        "dailyInteraction",
                        "evaluation"
                      ]
                    }
                  },
                  "decadeCount": {
                    "type": "integer",
                    "description": "대운 개수 (11~20, 기본 13)"
                  },
                  "trueSolarTime": {
                    "type": "boolean",
                    "description": "진태양시(경도차 + 균시차) 적용 여부, 기본 false. false = 한국 관습(자시 23:30, 경도차만). true = 한국천문연구원 방식 진태양시(자시 23:00, 균시차 포함). [API v1.1.0(2026-07-01)+] 자세한 차이는 §birthCity 참조."
                  },
                  "detail": {
                    "type": "string",
                    "enum": [
                      "minimal",
                      "standard",
                      "full"
                    ],
                    "description": "응답 상세 수준. minimal(값만), standard(핵심해석, 기본), full(전체)"
                  }
                },
                "required": [
                  "birthYear",
                  "birthMonth",
                  "birthDay"
                ]
              },
              "examples": {
                "sandboxProfile": {
                  "summary": "Free 샌드박스 샘플 프로필 (strong-male)",
                  "description": "Free 키는 문서에 공개된 샘플 프로필 5종의 입력과 정확히 일치할 때만 응답합니다.",
                  "value": {
                    "birthYear": 1998,
                    "birthMonth": 5,
                    "birthDay": 19,
                    "birthHour": 10,
                    "birthMinute": 0,
                    "isFemale": false,
                    "isLunar": false,
                    "birthCity": "서울"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/sazu/modules": {
      "get": {
        "summary": "사용 가능한 모듈 목록",
        "operationId": "sazuModules",
        "tags": [
          "sazu"
        ],
        "responses": {
          "200": {
            "description": "성공",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "분당 허용 요청 수",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "현재 창에서 남은 요청 수",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "한도가 초기화되는 Unix 초",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessResponse"
                }
              }
            }
          },
          "401": {
            "description": "MISSING_API_KEY: 인증 헤더가 누락되었습니다. x-api-key 또는 Authorization: Bearer <키> 헤더 중 하나를 포함해야 합니다. 헤더 이름의 대소문자는 무관하지만 철자는 정확히 일치해야 합니다.\nINVALID_API_KEY: API 키가 존재하지 않습니다. 헤더 값(x-api-key 또는 Authorization: Bearer)이 대시보드의 키와 일치하는지 확인하세요.\nKEY_REVOKED: 키가 폐기(revoke)되었습니다. 대시보드에서 새 키를 발급한 뒤 코드에 반영하세요.\nKEY_EXPIRED: 키의 만료일이 지났습니다. 대시보드에서 만료일을 갱신하거나 새 키를 발급하세요.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "success": false,
                  "error": {
                    "code": "MISSING_API_KEY",
                    "message": "인증 헤더가 누락되었습니다. x-api-key 또는 Authorization: Bearer <키> 헤더 중 하나를 포함해야 합니다. 헤더 이름의 대소문자는 무관하지만 철자는 정확히 일치해야 합니다."
                  }
                }
              }
            }
          },
          "403": {
            "description": "ORIGIN_NOT_ALLOWED: 요청 Origin 헤더가 키의 허용 도메인에 등록되지 않았습니다. Pro 키의 경우 대시보드 → API 키 → \"도메인\" 버튼에서 호출할 도메인을 등록하세요. 서버 사이드(Origin 헤더 없음) 호출은 등록 여부와 무관하게 통과합니다.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "success": false,
                  "error": {
                    "code": "ORIGIN_NOT_ALLOWED",
                    "message": "요청 Origin 헤더가 키의 허용 도메인에 등록되지 않았습니다. Pro 키의 경우 대시보드 → API 키 → \"도메인\" 버튼에서 호출할 도메인을 등록하세요. 서버 사이드(Origin 헤더 없음) 호출은 등록 여부와 무관하게 통과합니다."
                  }
                }
              }
            }
          },
          "429": {
            "description": "RATE_LIMIT_EXCEEDED: 분당 요청 한도를 초과했습니다. X-RateLimit-Reset 헤더의 Unix 타임스탬프 이후 재시도하세요.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "success": false,
                  "error": {
                    "code": "RATE_LIMIT_EXCEEDED",
                    "message": "분당 요청 한도를 초과했습니다. X-RateLimit-Reset 헤더의 Unix 타임스탬프 이후 재시도하세요."
                  }
                }
              }
            }
          },
          "500": {
            "description": "INTERNAL_ERROR: 서버 내부 오류입니다. 동일한 요청이 반복 실패하면 문의해 주세요.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "success": false,
                  "error": {
                    "code": "INTERNAL_ERROR",
                    "message": "서버 내부 오류입니다. 동일한 요청이 반복 실패하면 문의해 주세요."
                  }
                }
              }
            }
          },
          "503": {
            "description": "AUTH_UNAVAILABLE: 인증 서비스에 일시적 장애가 발생했습니다. 잠시 후 재시도하세요. 반복될 경우 문의해 주세요.\nQUOTA_UNAVAILABLE: 사용량 집계 서비스에 일시적 장애가 발생했습니다. 잠시 후 재시도하세요.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "success": false,
                  "error": {
                    "code": "AUTH_UNAVAILABLE",
                    "message": "인증 서비스에 일시적 장애가 발생했습니다. 잠시 후 재시도하세요. 반복될 경우 문의해 주세요."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/me": {
      "get": {
        "summary": "현재 키의 tier · 분당/월간 한도 · 사용량 · 재설정 시각 · ban 상태. 응답: data: { tier, rateLimitPerMinute, monthlyQuota, used, remaining, quotaCycleStart, quotaResetAt, banned, bannedUntil }",
        "operationId": "me",
        "tags": [
          "account"
        ],
        "responses": {
          "200": {
            "description": "성공",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "분당 허용 요청 수",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "현재 창에서 남은 요청 수",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "한도가 초기화되는 Unix 초",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyStatusResponse"
                }
              }
            }
          },
          "401": {
            "description": "MISSING_API_KEY: 인증 헤더가 누락되었습니다. x-api-key 또는 Authorization: Bearer <키> 헤더 중 하나를 포함해야 합니다. 헤더 이름의 대소문자는 무관하지만 철자는 정확히 일치해야 합니다.\nINVALID_API_KEY: API 키가 존재하지 않습니다. 헤더 값(x-api-key 또는 Authorization: Bearer)이 대시보드의 키와 일치하는지 확인하세요.\nKEY_REVOKED: 키가 폐기(revoke)되었습니다. 대시보드에서 새 키를 발급한 뒤 코드에 반영하세요.\nKEY_EXPIRED: 키의 만료일이 지났습니다. 대시보드에서 만료일을 갱신하거나 새 키를 발급하세요.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "success": false,
                  "error": {
                    "code": "MISSING_API_KEY",
                    "message": "인증 헤더가 누락되었습니다. x-api-key 또는 Authorization: Bearer <키> 헤더 중 하나를 포함해야 합니다. 헤더 이름의 대소문자는 무관하지만 철자는 정확히 일치해야 합니다."
                  }
                }
              }
            }
          },
          "403": {
            "description": "ORIGIN_NOT_ALLOWED: 요청 Origin 헤더가 키의 허용 도메인에 등록되지 않았습니다. Pro 키의 경우 대시보드 → API 키 → \"도메인\" 버튼에서 호출할 도메인을 등록하세요. 서버 사이드(Origin 헤더 없음) 호출은 등록 여부와 무관하게 통과합니다.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "success": false,
                  "error": {
                    "code": "ORIGIN_NOT_ALLOWED",
                    "message": "요청 Origin 헤더가 키의 허용 도메인에 등록되지 않았습니다. Pro 키의 경우 대시보드 → API 키 → \"도메인\" 버튼에서 호출할 도메인을 등록하세요. 서버 사이드(Origin 헤더 없음) 호출은 등록 여부와 무관하게 통과합니다."
                  }
                }
              }
            }
          },
          "429": {
            "description": "RATE_LIMIT_EXCEEDED: 분당 요청 한도를 초과했습니다. X-RateLimit-Reset 헤더의 Unix 타임스탬프 이후 재시도하세요.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "success": false,
                  "error": {
                    "code": "RATE_LIMIT_EXCEEDED",
                    "message": "분당 요청 한도를 초과했습니다. X-RateLimit-Reset 헤더의 Unix 타임스탬프 이후 재시도하세요."
                  }
                }
              }
            }
          },
          "500": {
            "description": "INTERNAL_ERROR: 서버 내부 오류입니다. 동일한 요청이 반복 실패하면 문의해 주세요.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "success": false,
                  "error": {
                    "code": "INTERNAL_ERROR",
                    "message": "서버 내부 오류입니다. 동일한 요청이 반복 실패하면 문의해 주세요."
                  }
                }
              }
            }
          },
          "503": {
            "description": "AUTH_UNAVAILABLE: 인증 서비스에 일시적 장애가 발생했습니다. 잠시 후 재시도하세요. 반복될 경우 문의해 주세요.\nQUOTA_UNAVAILABLE: 사용량 집계 서비스에 일시적 장애가 발생했습니다. 잠시 후 재시도하세요.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "success": false,
                  "error": {
                    "code": "AUTH_UNAVAILABLE",
                    "message": "인증 서비스에 일시적 장애가 발생했습니다. 잠시 후 재시도하세요. 반복될 경우 문의해 주세요."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/me/errors": {
      "get": {
        "summary": "본인 키로 발생한 최근 4xx/5xx 오류 이력 (15일 retention)",
        "operationId": "meErrors",
        "tags": [
          "account"
        ],
        "responses": {
          "200": {
            "description": "성공",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "분당 허용 요청 수",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "현재 창에서 남은 요청 수",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "한도가 초기화되는 Unix 초",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessResponse"
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR: 요청 바디의 파라미터가 잘못되었습니다. error.details 에서 누락·타입 오류 필드를 확인하세요.\nSAMPLE_PROFILE_REQUIRED: Free(샌드박스) 키로 샘플 프로필과 다른 생년월일을 요청했습니다. 문서의 샘플 프로필 5종의 입력값과 정확히 일치해야 응답합니다. 응답의 error.details.sampleProfiles 에 전체 목록이 함께 반환되므로 코드에서 바로 참조하실 수 있습니다. 실제 생년월일 계산은 Pro 에서 제공됩니다.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "success": false,
                  "error": {
                    "code": "VALIDATION_ERROR",
                    "message": "요청 바디의 파라미터가 잘못되었습니다. error.details 에서 누락·타입 오류 필드를 확인하세요."
                  }
                }
              }
            }
          },
          "401": {
            "description": "MISSING_API_KEY: 인증 헤더가 누락되었습니다. x-api-key 또는 Authorization: Bearer <키> 헤더 중 하나를 포함해야 합니다. 헤더 이름의 대소문자는 무관하지만 철자는 정확히 일치해야 합니다.\nINVALID_API_KEY: API 키가 존재하지 않습니다. 헤더 값(x-api-key 또는 Authorization: Bearer)이 대시보드의 키와 일치하는지 확인하세요.\nKEY_REVOKED: 키가 폐기(revoke)되었습니다. 대시보드에서 새 키를 발급한 뒤 코드에 반영하세요.\nKEY_EXPIRED: 키의 만료일이 지났습니다. 대시보드에서 만료일을 갱신하거나 새 키를 발급하세요.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "success": false,
                  "error": {
                    "code": "MISSING_API_KEY",
                    "message": "인증 헤더가 누락되었습니다. x-api-key 또는 Authorization: Bearer <키> 헤더 중 하나를 포함해야 합니다. 헤더 이름의 대소문자는 무관하지만 철자는 정확히 일치해야 합니다."
                  }
                }
              }
            }
          },
          "403": {
            "description": "ORIGIN_NOT_ALLOWED: 요청 Origin 헤더가 키의 허용 도메인에 등록되지 않았습니다. Pro 키의 경우 대시보드 → API 키 → \"도메인\" 버튼에서 호출할 도메인을 등록하세요. 서버 사이드(Origin 헤더 없음) 호출은 등록 여부와 무관하게 통과합니다.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "success": false,
                  "error": {
                    "code": "ORIGIN_NOT_ALLOWED",
                    "message": "요청 Origin 헤더가 키의 허용 도메인에 등록되지 않았습니다. Pro 키의 경우 대시보드 → API 키 → \"도메인\" 버튼에서 호출할 도메인을 등록하세요. 서버 사이드(Origin 헤더 없음) 호출은 등록 여부와 무관하게 통과합니다."
                  }
                }
              }
            }
          },
          "429": {
            "description": "RATE_LIMIT_EXCEEDED: 분당 요청 한도를 초과했습니다. X-RateLimit-Reset 헤더의 Unix 타임스탬프 이후 재시도하세요.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "success": false,
                  "error": {
                    "code": "RATE_LIMIT_EXCEEDED",
                    "message": "분당 요청 한도를 초과했습니다. X-RateLimit-Reset 헤더의 Unix 타임스탬프 이후 재시도하세요."
                  }
                }
              }
            }
          },
          "500": {
            "description": "INTERNAL_ERROR: 서버 내부 오류입니다. 동일한 요청이 반복 실패하면 문의해 주세요.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "success": false,
                  "error": {
                    "code": "INTERNAL_ERROR",
                    "message": "서버 내부 오류입니다. 동일한 요청이 반복 실패하면 문의해 주세요."
                  }
                }
              }
            }
          },
          "503": {
            "description": "AUTH_UNAVAILABLE: 인증 서비스에 일시적 장애가 발생했습니다. 잠시 후 재시도하세요. 반복될 경우 문의해 주세요.\nQUOTA_UNAVAILABLE: 사용량 집계 서비스에 일시적 장애가 발생했습니다. 잠시 후 재시도하세요.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "success": false,
                  "error": {
                    "code": "AUTH_UNAVAILABLE",
                    "message": "인증 서비스에 일시적 장애가 발생했습니다. 잠시 후 재시도하세요. 반복될 경우 문의해 주세요."
                  }
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "반환 개수 (1~100, 기본 20)",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "페이징 (기본 0)",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "status 필터 (기본 all)",
            "schema": {
              "type": "string",
              "enum": [
                "all",
                "4xx",
                "5xx",
                "400",
                "500"
              ]
            }
          },
          {
            "name": "endpoint",
            "in": "query",
            "required": false,
            "description": "엔드포인트 정확 일치 필터 (예: /v1/sazu/calculate)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "since",
            "in": "query",
            "required": false,
            "description": "조회 시작 시각, 기본 24시간 전, 최대 15일 (DB retention)",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          }
        ]
      }
    },
    "/v1/calendar/convert": {
      "post": {
        "summary": "음양력 변환",
        "operationId": "calendarConvert",
        "tags": [
          "calendar"
        ],
        "responses": {
          "200": {
            "description": "성공",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "분당 허용 요청 수",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "현재 창에서 남은 요청 수",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "한도가 초기화되는 Unix 초",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessResponse"
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR: 요청 바디의 파라미터가 잘못되었습니다. error.details 에서 누락·타입 오류 필드를 확인하세요.\nSAMPLE_PROFILE_REQUIRED: Free(샌드박스) 키로 샘플 프로필과 다른 생년월일을 요청했습니다. 문서의 샘플 프로필 5종의 입력값과 정확히 일치해야 응답합니다. 응답의 error.details.sampleProfiles 에 전체 목록이 함께 반환되므로 코드에서 바로 참조하실 수 있습니다. 실제 생년월일 계산은 Pro 에서 제공됩니다.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "success": false,
                  "error": {
                    "code": "VALIDATION_ERROR",
                    "message": "요청 바디의 파라미터가 잘못되었습니다. error.details 에서 누락·타입 오류 필드를 확인하세요."
                  }
                }
              }
            }
          },
          "401": {
            "description": "MISSING_API_KEY: 인증 헤더가 누락되었습니다. x-api-key 또는 Authorization: Bearer <키> 헤더 중 하나를 포함해야 합니다. 헤더 이름의 대소문자는 무관하지만 철자는 정확히 일치해야 합니다.\nINVALID_API_KEY: API 키가 존재하지 않습니다. 헤더 값(x-api-key 또는 Authorization: Bearer)이 대시보드의 키와 일치하는지 확인하세요.\nKEY_REVOKED: 키가 폐기(revoke)되었습니다. 대시보드에서 새 키를 발급한 뒤 코드에 반영하세요.\nKEY_EXPIRED: 키의 만료일이 지났습니다. 대시보드에서 만료일을 갱신하거나 새 키를 발급하세요.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "success": false,
                  "error": {
                    "code": "MISSING_API_KEY",
                    "message": "인증 헤더가 누락되었습니다. x-api-key 또는 Authorization: Bearer <키> 헤더 중 하나를 포함해야 합니다. 헤더 이름의 대소문자는 무관하지만 철자는 정확히 일치해야 합니다."
                  }
                }
              }
            }
          },
          "403": {
            "description": "ORIGIN_NOT_ALLOWED: 요청 Origin 헤더가 키의 허용 도메인에 등록되지 않았습니다. Pro 키의 경우 대시보드 → API 키 → \"도메인\" 버튼에서 호출할 도메인을 등록하세요. 서버 사이드(Origin 헤더 없음) 호출은 등록 여부와 무관하게 통과합니다.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "success": false,
                  "error": {
                    "code": "ORIGIN_NOT_ALLOWED",
                    "message": "요청 Origin 헤더가 키의 허용 도메인에 등록되지 않았습니다. Pro 키의 경우 대시보드 → API 키 → \"도메인\" 버튼에서 호출할 도메인을 등록하세요. 서버 사이드(Origin 헤더 없음) 호출은 등록 여부와 무관하게 통과합니다."
                  }
                }
              }
            }
          },
          "429": {
            "description": "RATE_LIMIT_EXCEEDED: 분당 요청 한도를 초과했습니다. X-RateLimit-Reset 헤더의 Unix 타임스탬프 이후 재시도하세요.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "success": false,
                  "error": {
                    "code": "RATE_LIMIT_EXCEEDED",
                    "message": "분당 요청 한도를 초과했습니다. X-RateLimit-Reset 헤더의 Unix 타임스탬프 이후 재시도하세요."
                  }
                }
              }
            }
          },
          "500": {
            "description": "INTERNAL_ERROR: 서버 내부 오류입니다. 동일한 요청이 반복 실패하면 문의해 주세요.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "success": false,
                  "error": {
                    "code": "INTERNAL_ERROR",
                    "message": "서버 내부 오류입니다. 동일한 요청이 반복 실패하면 문의해 주세요."
                  }
                }
              }
            }
          },
          "503": {
            "description": "AUTH_UNAVAILABLE: 인증 서비스에 일시적 장애가 발생했습니다. 잠시 후 재시도하세요. 반복될 경우 문의해 주세요.\nQUOTA_UNAVAILABLE: 사용량 집계 서비스에 일시적 장애가 발생했습니다. 잠시 후 재시도하세요.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "success": false,
                  "error": {
                    "code": "AUTH_UNAVAILABLE",
                    "message": "인증 서비스에 일시적 장애가 발생했습니다. 잠시 후 재시도하세요. 반복될 경우 문의해 주세요."
                  }
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "year": {
                    "type": "integer",
                    "description": "년도. (사주 분석 /v1/sazu/calculate 의 birthYear 와 다른 필드명이니 주의)"
                  },
                  "month": {
                    "type": "integer",
                    "description": "월"
                  },
                  "day": {
                    "type": "integer",
                    "description": "일"
                  },
                  "direction": {
                    "type": "string",
                    "enum": [
                      "toSolar",
                      "toLunar"
                    ],
                    "description": "변환 방향. toSolar = 음력→양력, toLunar = 양력→음력"
                  },
                  "isLeapMonth": {
                    "type": "boolean",
                    "description": "윤달 여부, 기본값 false. direction: \"toSolar\"(음력→양력) 일 때만 사용합니다. 윤달이 드는 해에는 같은 달이 두 번 오므로, 음력 1998-05-15 는 isLeapMonth 값에 따라 양력 6월 9일(평5월) 또는 7월 8일(윤5월)이 됩니다. 반대 방향(toLunar)은 입력 날짜가 하나로 정해지므로 보내실 필요가 없고, 결과의 isLeap 로 윤달 여부를 알려드립니다. [2026-08-22 반영]"
                  }
                },
                "required": [
                  "year",
                  "month",
                  "day",
                  "direction"
                ]
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "apiKeyHeader": {
        "type": "apiKey",
        "in": "header",
        "name": "x-api-key",
        "description": "대시보드(https://www.sazu.app/manse-api/dashboard/keys)에서 발급합니다. 서버 환경변수에만 두고 브라우저로 내려보내지 마세요."
      },
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "`Authorization: Bearer <API 키>` 형태로도 보낼 수 있습니다. x-api-key 와 둘 중 하나만 쓰면 됩니다."
      }
    },
    "schemas": {
      "SuccessResponse": {
        "type": "object",
        "properties": {
          "success": {
            "const": true
          },
          "data": {
            "type": "object",
            "additionalProperties": true
          },
          "meta": {
            "type": "object",
            "additionalProperties": true
          }
        },
        "required": [
          "success",
          "data"
        ]
      },
      "SazuCalculateResponse": {
        "type": "object",
        "properties": {
          "success": {
            "const": true
          },
          "data": {
            "type": "object",
            "properties": {
              "modules": {
                "type": "object",
                "description": "요청한 분석 모듈의 결과. 키는 모듈 id 입니다.",
                "additionalProperties": true
              }
            },
            "additionalProperties": true
          },
          "meta": {
            "type": "object",
            "properties": {
              "responseMs": {
                "type": "number",
                "description": "서버 처리 시간(ms)"
              },
              "modules": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "실제로 반환된 모듈 id"
              },
              "cached": {
                "type": "boolean"
              },
              "tier": {
                "type": "string"
              },
              "sample": {
                "type": "boolean",
                "description": "true 면 Free 샌드박스의 고정 샘플 응답입니다."
              },
              "sampleProfile": {
                "type": "string",
                "description": "일치한 샘플 프로필 id"
              }
            },
            "additionalProperties": true
          }
        },
        "required": [
          "success",
          "data"
        ]
      },
      "KeyStatusResponse": {
        "type": "object",
        "properties": {
          "success": {
            "const": true
          },
          "data": {
            "type": "object",
            "properties": {
              "tier": {
                "type": "string"
              },
              "rateLimitPerMinute": {
                "type": "integer"
              },
              "monthlyQuota": {
                "type": "integer"
              },
              "used": {
                "type": "integer"
              },
              "remaining": {
                "type": "integer"
              },
              "quotaCycleStart": {
                "type": "string",
                "format": "date-time"
              },
              "quotaResetAt": {
                "type": "string",
                "format": "date-time"
              },
              "banned": {
                "type": "boolean"
              },
              "bannedUntil": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              }
            },
            "additionalProperties": true
          }
        },
        "required": [
          "success",
          "data"
        ]
      },
      "ErrorResponse": {
        "type": "object",
        "properties": {
          "success": {
            "const": false
          },
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "VALIDATION_ERROR",
                  "SAMPLE_PROFILE_REQUIRED",
                  "MISSING_API_KEY",
                  "INVALID_API_KEY",
                  "KEY_REVOKED",
                  "KEY_EXPIRED",
                  "ORIGIN_NOT_ALLOWED",
                  "RATE_LIMIT_EXCEEDED",
                  "AUTH_UNAVAILABLE",
                  "QUOTA_UNAVAILABLE",
                  "INTERNAL_ERROR"
                ]
              },
              "message": {
                "type": "string"
              },
              "details": {
                "type": "object",
                "additionalProperties": true
              }
            },
            "required": [
              "code",
              "message"
            ]
          }
        },
        "required": [
          "success",
          "error"
        ]
      }
    }
  }
}