openapi: 3.0.3
info:
  title: Lunaple Deterministic API
  version: "2026.10.18"
  description: |
    토정비결·작명 **결정론 계산 API**. AI 해석을 포함하지 않습니다 — 계산 결과(괘·원문·독음, 수리·오격)만 반환하며, 해석은 호출 측 LLM에 맡기도록 설계되었습니다.
    모든 응답에 `source`(계산 근거)와 `version`이 포함됩니다.
servers:
  - url: https://api.lunaple.kr
security:
  - ApiKey: []
components:
  securitySchemes:
    ApiKey: { type: apiKey, in: header, name: X-API-Key }
  schemas:
    Error:
      type: object
      properties: { error: { type: object, properties: { code: { type: string }, message: { type: string } } } }
    TojeongCalc:
      type: object
      properties:
        gwaeId: { type: string, example: "751" }
        upper: { type: integer }
        middle: { type: integer }
        lower: { type: integer }
        koreanAge: { type: integer }
        yearGapja: { type: string, example: 丙午 }
        monthGapja: { type: string }
        dayGapja: { type: string }
        monthDayCount: { type: integer, description: 대상연 음력 생월 일수(29|30) }
        solar: { type: string }
        lunar: { type: object, properties: { year: { type: integer }, month: { type: integer }, day: { type: integer }, leap: { type: boolean } } }
    Gwae:
      type: object
      properties:
        gwaeId: { type: string }
        yearly: { type: array, items: { type: string }, description: 연운 9구절. reading=true 시 "독음(漢字)" 병기 }
        monthly: { type: object, additionalProperties: { type: array, items: { type: string } }, description: "월별 3구절 × 12" }
paths:
  /v1/health:
    get:
      security: []
      summary: 상태 확인
      responses: { "200": { description: ok } }
  /v1/tojeong/gwae:
    post:
      summary: 생년월일 → 토정비결 작괘 + 144괘 원문
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [birth]
              properties:
                birth: { type: string, example: "1988-05-14", description: YYYY-MM-DD }
                calendar: { type: string, enum: [solar, lunar], default: solar }
                leapMonth: { type: boolean, default: false, description: 음력 윤달 출생 (calendar=lunar 일 때) }
                targetYear: { type: integer, example: 2026, description: 보려는 해. 기본 올해 }
                reading: { type: boolean, default: true, description: 한글 독음 병기 여부 }
      responses:
        "200":
          description: 작괘 결과
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { type: object, properties: { calc: { $ref: "#/components/schemas/TojeongCalc" }, gwae: { $ref: "#/components/schemas/Gwae" }, source: { type: object } } }
                  version: { type: string }
        "400": { description: 입력 형식 오류, content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
        "422": { description: 계산 불가(출생 전 연도 등) }
        "429": { description: 쿼터/레이트리밋 초과 }
  /v1/tojeong/gwae/{id}:
    get:
      summary: 괘ID로 원문 직접 조회 (캐시 가능)
      parameters:
        - { name: id, in: path, required: true, schema: { type: string, pattern: "^[1-8][1-6][1-3]$" }, example: "412" }
        - { name: reading, in: query, schema: { type: boolean, default: true } }
      responses:
        "200": { description: 괘 원문 }
        "404": { description: 없음 }
  /v1/name/analyze:
    post:
      summary: 이름풀이 — 81수리·음양·발음오행·5격·점수
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [surname, given]
              properties:
                surname: { oneOf: [{ type: string, example: 金 }, { type: array, items: { type: object, properties: { hanja: { type: string }, hangul: { type: string } } } }] }
                given: { oneOf: [{ type: string, example: 民奎 }, { type: array, items: { type: object, properties: { hanja: { type: string }, hangul: { type: string } } } }], description: 두음·표기음이 사전과 다르면 hangul 지정 }
      responses:
        "200": { description: 분석 결과 (input, result{오격,오격수리,길흉요약,음양,발음오행,점수,등급,출처}, source) }
        "422": { description: 인명용 한자 미등재 (CHAR_NOT_FOUND) }
  /v1/name/char/{hanja}:
    get:
      summary: 인명용 한자 1자 조회 (독음·획수·뜻·부수)
      parameters: [{ name: hanja, in: path, required: true, schema: { type: string } }]
      responses: { "200": { description: 한자 정보 }, "404": { description: 미등재 } }
  /v1/keys/request:
    post:
      security: []
      summary: 샌드박스 API 키 발급 (이메일당 1키, IP당 하루 3회)
      requestBody:
        required: true
        content:
          application/json:
            schema: { type: object, required: [email], properties: { email: { type: string, format: email }, purpose: { type: string } } }
      responses:
        "200": { description: "data.apiKey — 1회만 표시" }
        "400": { description: BAD_EMAIL }
        "429": { description: RATE_LIMIT }
  /v1/billing/plans:
    get: { security: [], summary: 유료 플랜·가격 + 결제 준비 상태, responses: { "200": { description: plans } } }
  /v1/billing/confirm:
    post:
      security: []
      summary: PortOne 결제 확정 → 플랜 적용(+31일). paymentId 는 SDK requestPayment 에 쓴 값
      requestBody: { required: true, content: { application/json: { schema: { type: object, required: [paymentId], properties: { paymentId: { type: string } } } } } }
      responses: { "200": { description: "data.plan, data.planExpiresAt" }, "422": { description: 금액 불일치·미결제 }, "503": { description: 결제 미준비 } }
  /v1/billing/webhook:
    post: { security: [], summary: PortOne 웹훅(Standard Webhooks 서명). 취소·환불 → Sandbox 강등, responses: { "200": { description: ok } } }
