{
  "openapi": "3.1.0",
  "info": {
    "title": "클래드 — 건설자재 API — 에이전트 API",
    "version": "1.0.0-draft",
    "summary": "건설자재의 조건 검색, 제품 상세, 견적 산출.",
    "description": "조건으로 걸러 견적까지 한 번에 받습니다. 공개되는 것은 가격표가 아니라 견적을 계산해 주는 함수입니다. 면적 단가와 물량 구간, 곡률·마감 할증 같은 산식이 등록되어 있고 총액과 산출 근거만 응답합니다. 단가는 응답에 포함되지 않으며, 크롤링으로는 가져갈 수 없습니다. 조회는 무료이고 지갑이 필요 없습니다. 제품과 공급사를 찾는 일은 제조사가 원하는 노출이기 때문입니다. 값을 매기는 것은 견적뿐입니다. 견적 창구는 지갑을 제시한 요청에만 열립니다. 지갑 없이 견적을 요청하면 402 로 가격표와 지갑 개설 경로를 함께 돌려줍니다. 어느 법인의 에이전트가 무엇을 물었는지는 로그로 남습니다. 지갑·한도·차감은 동작합니다. 다만 충전(입금)과 제조사 정산(출금)은 없습니다 —         그 둘이 전자금융거래법 검토가 필요한 지점이라 법률 검토를 마친 뒤에 엽니다.         지금 잔액은 운영자가 부여합니다.",
    "contact": {
      "name": "클래드 — 건설자재 API",
      "email": "ljk@ljkstudio.com",
      "url": "https://clad.kr"
    }
  },
  "x-pricing-status": "provisional",
  "x-pricing-note": "유료 상태의 금액은 아직 정해지지 않았습니다(null). 파일럿으로 확인한 뒤에 싣습니다. infeasible 의 0 은 가정이 아니라 원칙이므로 지금부터 싣습니다 — 못 만든다는 답에는 값을 받지 않습니다.",
  "servers": [
    {
      "url": "https://clad.kr/api/v1",
      "description": "운영 중. 지갑과 견적이 여기서 돕니다."
    },
    {
      "url": "https://api.clad.kr/v1",
      "description": "전용 호스트 (개통 준비 중)"
    }
  ],
  "x-brand": {
    "name": "클래드 — 건설자재 API",
    "alternateName": [
      "클래드",
      "CLAD"
    ]
  },
  "paths": {
    "/products": {
      "get": {
        "operationId": "searchProducts",
        "summary": "조건으로 제품을 거른다",
        "description": "무료 경로입니다. 지갑도 키도 필요 없습니다. **값이 등록되지 않은 조건은 통과시키지 않습니다.** 두께를 모르는 품목에 \"3mm 되나요\"의 답은 \"모른다\"이지 \"된다\"가 아닙니다. 모르면서 통과시키면 못 만드는 것을 만들 수 있다고 답하게 됩니다. 그래서 응답에 misses 를 함께 싣습니다 — 조건을 낮춰야 하는 것(out_of_range)과 아무도 그 값을 안 적어 둔 것(unknown)은 다른 문제입니다.",
        "x-status": "live",
        "x-wallet-required": false,
        "parameters": [
          {
            "name": "material",
            "in": "query",
            "description": "소재. 한글 값도 받지만 URL 인코딩을 피하려면 영문 enum 을 권합니다. aluminum=알루미늄, stainless=스테인리스, zinc=아연, copper=동, steel=강판",
            "schema": {
              "type": "string",
              "enum": [
                "aluminum",
                "stainless",
                "zinc",
                "copper",
                "steel"
              ]
            }
          },
          {
            "name": "thickness_mm",
            "in": "query",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "description": "두께(mm)"
          },
          {
            "name": "max_radius_mm",
            "in": "query",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "description": "최소 곡률반경(mm) 상한"
          },
          {
            "name": "max_width_mm",
            "in": "query",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "description": "최대 폭(mm)"
          },
          {
            "name": "finish",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "마감(예: PVDF)"
          },
          {
            "name": "fire_rating",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "방화 등급"
          },
          {
            "name": "supplier_brn",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "공급사 사업자등록번호"
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 20,
              "maximum": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "조건을 만족하는 제품 목록",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProductList"
                }
              }
            }
          }
        }
      }
    },
    "/products/{sku}": {
      "get": {
        "operationId": "getProduct",
        "summary": "제품 상세 제원",
        "description": "무료 경로입니다. 지갑도 서명도 필요 없습니다.",
        "x-status": "live",
        "x-wallet-required": false,
        "parameters": [
          {
            "name": "sku",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "제품 상세",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Product"
                }
              }
            }
          },
          "404": {
            "description": "해당 SKU 없음"
          }
        }
      }
    },
    "/suppliers/{brn}": {
      "get": {
        "operationId": "getSupplier",
        "summary": "공급사 프로파일",
        "description": "사업자등록번호로 고정합니다. 같은 상호의 법인이 여럿이면 상호로는 특정되지 않습니다. 무료 경로입니다. 지갑도 서명도 필요 없습니다.",
        "x-status": "planned",
        "x-wallet-required": false,
        "parameters": [
          {
            "name": "brn",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "사업자등록번호(숫자 10자리)"
          }
        ],
        "responses": {
          "200": {
            "description": "공급사 프로파일"
          },
          "404": {
            "description": "해당 사업자등록번호 없음"
          }
        }
      }
    },
    "/quotes": {
      "post": {
        "operationId": "createQuote",
        "summary": "견적을 계산한다",
        "description": "총액과 산출 근거만 응답합니다. 단가는 나가지 않습니다. 조회와 달리 견적은 값을 매기는 경로이므로 지갑을 제시한 요청에만 열립니다. 지갑 헤더 없이 부르면 402 로 가격표를 돌려주니, 먼저 부르고 402 를 읽어도 됩니다.",
        "x-status": "live",
        "x-wallet-required": true,
        "x-pricing-status": "provisional",
        "x-quote-state-pricing": {
          "infeasible": 0,
          "needs_review": null,
          "indicative": null
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/CladWallet"
          },
          {
            "$ref": "#/components/parameters/CladKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/QuoteRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "견적 결과",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Quote"
                }
              }
            }
          },
          "402": {
            "description": "지갑이 없거나 한도가 모자랍니다. 본문에 가격표와 지갑 개설 경로가 들어 있어 에이전트가 사람에게 묻지 않고 다음 판단을 할 수 있습니다.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentRequired"
                },
                "example": {
                  "error": "payment_required",
                  "price_krw": null,
                  "quote_state_pricing": {
                    "infeasible": 0,
                    "needs_review": null,
                    "indicative": null
                  },
                  "accepts": [
                    "clad-credit"
                  ],
                  "wallet_required": true,
                  "docs": "https://clad.kr/wallet"
                }
              }
            }
          }
        }
      }
    },
    "/quotes/{id}/reserve": {
      "post": {
        "operationId": "reserveQuote",
        "summary": "서명된 견적의 자리를 확보한다",
        "description": "견적 금액을 예치해 자재와 납기를 잡아 둡니다. 발주 단계이므로 에스크로 사업자와의 제휴가 전제입니다. 2단계 기능이고 아직 열려 있지 않습니다.",
        "x-status": "planned",
        "x-phase": 2,
        "x-wallet-required": true,
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "견적 식별자"
          },
          {
            "$ref": "#/components/parameters/CladWallet"
          },
          {
            "$ref": "#/components/parameters/CladKey"
          }
        ],
        "responses": {
          "200": {
            "description": "예치 결과",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Reservation"
                }
              }
            }
          },
          "402": {
            "description": "예치금이 모자랍니다",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentRequired"
                }
              }
            }
          },
          "409": {
            "description": "이미 예치되었거나 견적이 만료되었습니다"
          }
        }
      }
    },
    "/wallets": {
      "post": {
        "operationId": "createWallet",
        "summary": "법인 지갑을 연다",
        "description": "사업자등록번호를 국세청 상태 조회로 확인하고, 등기부와 사업자등록증을 대조한 뒤 발급합니다. 새 신원 체계를 만드는 것이 아니라 이미 쓰고 있는 사업자등록번호에 기계가 읽을 주소를 붙이는 일입니다. 지갑을 얻는 경로이므로 지갑 헤더 없이 부릅니다.",
        "x-status": "live",
        "x-wallet-required": false,
        "responses": {
          "201": {
            "description": "개설된 법인 지갑",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Wallet"
                }
              }
            }
          },
          "409": {
            "description": "해당 사업자등록번호로 이미 지갑이 있습니다"
          },
          "422": {
            "description": "사업자등록번호가 유효하지 않거나 휴폐업 상태입니다"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WalletCreateRequest"
              }
            }
          }
        }
      }
    },
    "/wallets/{handle}": {
      "get": {
        "operationId": "getWallet",
        "summary": "잔액과 한도와 소진율",
        "description": "에이전트가 자기 예산을 알아야 다음 판단을 합니다.",
        "x-status": "live",
        "x-wallet-required": true,
        "parameters": [
          {
            "$ref": "#/components/parameters/WalletHandlePath"
          },
          {
            "$ref": "#/components/parameters/CladKey"
          }
        ],
        "responses": {
          "200": {
            "description": "지갑 상태",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Wallet"
                }
              }
            }
          },
          "404": {
            "description": "해당 핸들 없음"
          }
        }
      }
    },
    "/wallets/{handle}/children": {
      "post": {
        "operationId": "createChildWallet",
        "summary": "현장 지갑과 에이전트 지갑을 발급한다",
        "description": "법인 지갑만 하위 지갑을 발급합니다. 한도와 허용 목록과 유효기간을 여기서 정합니다. 제약처럼 보이지만 한도가 명시되어 있으면 사람이 매번 확인할 이유가 없어져 에이전트가 오히려 풀려납니다.",
        "x-status": "live",
        "x-wallet-required": true,
        "parameters": [
          {
            "$ref": "#/components/parameters/WalletHandlePath"
          },
          {
            "$ref": "#/components/parameters/CladKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ChildWalletRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "발급된 현장 지갑",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Wallet"
                }
              }
            }
          },
          "403": {
            "description": "하위 지갑은 하위 지갑을 발급하지 못합니다"
          }
        }
      }
    },
    "/suppliers/{brn}/inquiries": {
      "get": {
        "operationId": "listSupplierInquiries",
        "summary": "누가 무엇을 물었는지",
        "description": "제조사가 자기 제품에 들어온 견적 요청을 봅니다. 자기 사업자등록번호에 대해서만 열립니다. 지금 이 정보는 아무 데도 남지 않습니다.",
        "x-status": "live",
        "x-wallet-required": true,
        "parameters": [
          {
            "name": "brn",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "공급사 사업자등록번호(숫자 10자리)"
          },
          {
            "name": "since",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "이 날짜 이후"
          },
          {
            "name": "state",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "infeasible",
                "needs_review",
                "indicative"
              ]
            },
            "description": "견적 상태로 거른다"
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 50,
              "maximum": 200
            }
          },
          {
            "$ref": "#/components/parameters/CladKey"
          }
        ],
        "responses": {
          "200": {
            "description": "문의 목록",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InquiryList"
                }
              }
            }
          },
          "403": {
            "description": "다른 법인의 문의는 보지 못합니다"
          }
        }
      }
    }
  },
  "components": {
    "parameters": {
      "CladWallet": {
        "name": "X-Clad-Wallet",
        "in": "header",
        "required": false,
        "description": "지갑 핸들. 법인 핸들 뒤에 하위 지갑 이름을 붙입니다. 핸들은 사업자등록번호에 묶입니다. 새 신원 체계가 아니라 이미 회사를 고정하는 데 쓰는 번호에 기계가 읽을 주소를 붙인 것입니다. 없으면 402 가 나갑니다.",
        "schema": {
          "type": "string",
          "pattern": "^@[a-z0-9-]+\\.clad\\.kr(/.+)?$"
        },
        "example": "@hyundai-eng.clad.kr/A현장-외장"
      },
      "CladKey": {
        "name": "X-Clad-Key",
        "in": "header",
        "required": false,
        "description": "지갑 발급 때 한 번 나가는 API 키. 저장하지 않으므로 잃어버리면 다시 발급합니다. 어느 법인의 에이전트가 무엇을 물었는지가 이 키로 확정됩니다.",
        "schema": {
          "type": "string",
          "pattern": "^clad_[0-9a-f]{48}$"
        }
      },
      "CladSignature": {
        "name": "X-Clad-Signature",
        "in": "header",
        "required": false,
        "x-status": "planned",
        "description": "요청 본문과 타임스탬프를 Ed25519 로 서명한 값. 아직 열지 않았습니다 — 지금은 X-Clad-Key 로 인증합니다. 키만으로는 요청 내용의 위변조를 막지 못하므로 다음 단계입니다.",
        "schema": {
          "type": "string"
        }
      },
      "WalletHandlePath": {
        "name": "handle",
        "in": "path",
        "required": true,
        "description": "지갑 핸들. 경로에 넣을 때는 URL 인코딩합니다.",
        "schema": {
          "type": "string"
        },
        "example": "@hyundai-eng.clad.kr"
      }
    },
    "schemas": {
      "ProductList": {
        "type": "object",
        "required": [
          "count",
          "items"
        ],
        "properties": {
          "count": {
            "type": "integer"
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Product"
            }
          },
          "misses": {
            "type": "array",
            "description": "떨어진 품목과 그 이유. unknown 은 값이 등록되지 않아 걸러진 것이고, out_of_range 는 한계를 넘어선 것입니다. 둘은 다른 문제라 구분해서 냅니다.",
            "items": {
              "type": "object",
              "properties": {
                "sku": {
                  "type": "string"
                },
                "field": {
                  "type": "string"
                },
                "reason": {
                  "type": "string",
                  "enum": [
                    "unknown",
                    "out_of_range"
                  ]
                }
              }
            }
          }
        }
      },
      "Product": {
        "type": "object",
        "description": "확인되지 않은 값은 null 입니다. 추정치를 넣지 않습니다.",
        "required": [
          "sku",
          "name",
          "supplier_brn"
        ],
        "properties": {
          "sku": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "supplier_brn": {
            "type": "string",
            "description": "공급사 사업자등록번호"
          },
          "material": {
            "type": "string",
            "enum": [
              "aluminum",
              "stainless",
              "zinc",
              "copper",
              "steel"
            ]
          },
          "thickness_min_mm": {
            "type": [
              "number",
              "null"
            ],
            "description": "성형 가능한 가장 얇은 판"
          },
          "thickness_max_mm": {
            "type": [
              "number",
              "null"
            ],
            "description": "성형 가능한 가장 두꺼운 판"
          },
          "tolerance_mm": {
            "type": [
              "number",
              "null"
            ],
            "description": "형상 공차. ± 로 읽습니다"
          },
          "curve_class": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "type": "string",
              "enum": [
                "평면",
                "단곡",
                "복곡",
                "자유곡면"
              ]
            },
            "description": "성형 가능한 곡면 등급"
          },
          "confirmed_fields": {
            "type": "integer",
            "description": "제조사가 확인해 준 필드 수. 낮을수록 조건 검색에서 많이 빠집니다."
          },
          "total_fields": {
            "type": "integer"
          },
          "max_width_mm": {
            "type": [
              "number",
              "null"
            ]
          },
          "max_length_mm": {
            "type": [
              "number",
              "null"
            ]
          },
          "min_radius_mm": {
            "type": [
              "number",
              "null"
            ],
            "description": "성형 가능한 최소 곡률반경"
          },
          "weight_kg_m2": {
            "type": [
              "number",
              "null"
            ]
          },
          "finish": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "type": "string"
            },
            "description": "대응하는 마감"
          },
          "fire_rating": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "type": "string"
            },
            "description": "만족하는 방화 등급"
          },
          "certifications": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Certification"
            }
          },
          "moq": {
            "type": [
              "number",
              "null"
            ],
            "description": "최소 주문 수량"
          },
          "lead_time_days": {
            "type": [
              "integer",
              "null"
            ]
          },
          "etim_class": {
            "type": [
              "string",
              "null"
            ]
          },
          "unspsc": {
            "type": [
              "string",
              "null"
            ]
          },
          "updated": {
            "type": "string",
            "format": "date"
          }
        }
      },
      "Certification": {
        "type": "object",
        "required": [
          "name"
        ],
        "properties": {
          "name": {
            "type": "string"
          },
          "issuer": {
            "type": [
              "string",
              "null"
            ]
          },
          "number": {
            "type": [
              "string",
              "null"
            ]
          },
          "valid_until": {
            "type": [
              "string",
              "null"
            ],
            "format": "date",
            "description": "만료를 감시합니다"
          }
        }
      },
      "QuoteRequest": {
        "type": "object",
        "required": [
          "sku",
          "quantity"
        ],
        "properties": {
          "sku": {
            "type": "string"
          },
          "quantity": {
            "type": "number"
          },
          "unit": {
            "type": "string",
            "enum": [
              "m2",
              "ea",
              "m"
            ],
            "default": "m2"
          },
          "radius_mm": {
            "type": [
              "number",
              "null"
            ]
          },
          "finish": {
            "type": [
              "string",
              "null"
            ]
          },
          "due_date": {
            "type": [
              "string",
              "null"
            ],
            "format": "date"
          }
        }
      },
      "Quote": {
        "type": "object",
        "required": [
          "state"
        ],
        "properties": {
          "state": {
            "type": "string",
            "enum": [
              "infeasible",
              "needs_review",
              "indicative"
            ],
            "description": "infeasible: 성형 한계나 치수를 넘어서 계산 전에 거절 / needs_review: 조건이 부족해 담당자에게 이관 / indicative: 참고 견적 즉시 산출"
          },
          "total_krw": {
            "type": [
              "number",
              "null"
            ],
            "description": "총액. 단가는 포함하지 않습니다."
          },
          "basis": {
            "type": "array",
            "description": "산출 근거. 어떤 산식이 어떻게 걸렸는지.",
            "items": {
              "type": "object",
              "properties": {
                "factor": {
                  "type": "string"
                },
                "applied": {
                  "type": "string"
                }
              }
            }
          },
          "valid_until": {
            "type": [
              "string",
              "null"
            ],
            "format": "date"
          },
          "index_linked": {
            "type": [
              "string",
              "null"
            ],
            "description": "연동된 원자재 지수"
          },
          "reason": {
            "type": [
              "string",
              "null"
            ],
            "description": "infeasible 일 때 거절 사유"
          },
          "charged_krw": {
            "type": [
              "number",
              "null"
            ],
            "description": "이 견적에 실제로 청구된 금액. 상태에 따라 다릅니다. infeasible 은 0 입니다. 못 만든다는 답에는 값을 받지 않습니다.",
            "examples": [
              null
            ]
          },
          "wallet": {
            "type": [
              "object",
              "null"
            ],
            "description": "청구 뒤의 지갑 상태. 에이전트가 자기 예산을 알아야 다음 판단을 합니다.",
            "properties": {
              "remaining_krw": {
                "type": "number",
                "description": "이번 기간에 남은 금액"
              },
              "period_end": {
                "type": "string",
                "format": "date",
                "description": "한도가 다시 차는 날"
              }
            }
          }
        }
      },
      "PaymentRequired": {
        "type": "object",
        "description": "402 본문. 사람이 읽는 안내가 아니라 에이전트가 읽고 다음 행동을 정하는 데이터입니다. 가격표를 통째로 실어 보내므로 요청 한 번으로 상태별 값을 전부 알 수 있습니다.",
        "required": [
          "error",
          "accepts",
          "wallet_required"
        ],
        "properties": {
          "error": {
            "type": "string",
            "enum": [
              "payment_required"
            ]
          },
          "price_krw": {
            "type": [
              "number",
              "null"
            ],
            "description": "이 요청이 성립했을 때 예상되는 청구액. 아직 정해지지 않아 null 입니다."
          },
          "quote_state_pricing": {
            "type": "object",
            "description": "견적 상태별 가격. infeasible: 0원 / needs_review: 미정 / indicative: 미정. infeasible 이 0 인 것은 실수가 아닙니다. 못 만든다는 답에는 값을 받지 않습니다. 그래야 제조사가 성형 한계를 정직하게 등록할 이유가 생깁니다. 데이터 품질을 계약서가 아니라 가격표로 담보하는 장치입니다. 유료 두 상태가 null 인 것은 값이 없다는 뜻이 아니라 아직 확정하지 않았다는 뜻입니다.",
            "additionalProperties": {
              "type": [
                "number",
                "null"
              ]
            },
            "examples": [
              {
                "infeasible": 0,
                "needs_review": null,
                "indicative": null
              }
            ]
          },
          "accepts": {
            "type": "array",
            "description": "받는 결제 수단. 지금은 원화 선불 크레딧 하나뿐인데도 배열로 둡니다. 한 가지만 받는 필드를 문자열로 잡아 두면 나중에 수단이 늘 때 스펙이 깨지기 때문입니다. 배열이면 항목 하나를 더하는 일로 끝납니다.",
            "items": {
              "type": "string",
              "enum": [
                "clad-credit"
              ]
            },
            "examples": [
              [
                "clad-credit"
              ]
            ]
          },
          "wallet_required": {
            "type": "boolean",
            "description": "지갑 없이는 이 경로가 열리지 않습니다"
          },
          "docs": {
            "type": "string",
            "format": "uri",
            "description": "지갑 개설 안내"
          },
          "shortfall_krw": {
            "type": [
              "number",
              "null"
            ],
            "description": "지갑은 있는데 한도가 모자랄 때 얼마가 모자란지"
          }
        }
      },
      "Wallet": {
        "type": "object",
        "description": "지갑은 두 종류입니다. 법인 지갑은 사업자등록번호가 소유하고 충전과 하위 발급을 합니다. 현장 지갑은 법인 지갑이 발급하고 충전하지 못하며 한도 안에서만 씁니다. 건설 조달은 원래 그렇게 일합니다 — 현장별 예산, 공종별 한도, 금액별 결재선.",
        "required": [
          "handle",
          "kind"
        ],
        "properties": {
          "handle": {
            "type": "string",
            "description": "핸들 형식은 @{slug}.clad.kr 이고 사업자등록번호에 묶입니다.",
            "examples": [
              "@hyundai-eng.clad.kr",
              "@hyundai-eng.clad.kr/A현장-외장"
            ]
          },
          "kind": {
            "type": "string",
            "enum": [
              "corporate",
              "site"
            ],
            "description": "corporate: 법인 지갑 / site: 현장과 에이전트 지갑"
          },
          "brn": {
            "type": "string",
            "description": "사업자등록번호. 법인 지갑의 소유자이고 현장 지갑도 이 번호를 상속합니다."
          },
          "parent_handle": {
            "type": [
              "string",
              "null"
            ],
            "description": "현장 지갑을 발급한 법인 지갑"
          },
          "label": {
            "type": [
              "string",
              "null"
            ],
            "description": "현장명이나 에이전트 이름"
          },
          "public_key": {
            "type": [
              "string",
              "null"
            ],
            "description": "요청 서명 검증에 쓰는 Ed25519 공개키"
          },
          "topup_allowed": {
            "type": "boolean",
            "description": "충전은 법인 지갑만 됩니다"
          },
          "balance_krw": {
            "type": [
              "number",
              "null"
            ],
            "description": "법인 지갑의 남은 크레딧"
          },
          "limits": {
            "$ref": "#/components/schemas/WalletLimits"
          },
          "usage": {
            "type": [
              "object",
              "null"
            ],
            "description": "이번 기간의 소진율",
            "properties": {
              "period_start": {
                "type": "string",
                "format": "date"
              },
              "period_end": {
                "type": "string",
                "format": "date"
              },
              "spent_krw": {
                "type": "number"
              },
              "remaining_krw": {
                "type": "number"
              },
              "quote_count": {
                "type": "integer"
              }
            }
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "suspended",
              "expired",
              "revoked"
            ]
          },
          "created": {
            "type": "string",
            "format": "date"
          }
        }
      },
      "WalletLimits": {
        "type": [
          "object",
          "null"
        ],
        "description": "현장 지갑의 울타리. 법인 지갑에는 없습니다. 한도가 명시되어 있으면 사람이 매번 확인할 이유가 없어집니다.",
        "properties": {
          "monthly_limit_krw": {
            "type": [
              "number",
              "null"
            ],
            "description": "월 한도"
          },
          "per_request_limit_krw": {
            "type": [
              "number",
              "null"
            ],
            "description": "건당 상한"
          },
          "allowed_etim_classes": {
            "type": "array",
            "description": "허용 카테고리. 비어 있으면 제한 없음.",
            "items": {
              "type": "string"
            }
          },
          "allowed_supplier_brns": {
            "type": "array",
            "description": "허용 공급사 사업자등록번호. 비어 있으면 제한 없음.",
            "items": {
              "type": "string"
            }
          },
          "expires_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date",
            "description": "유효기간. 보통 현장 준공 예정일을 넣습니다."
          },
          "approval_threshold_krw": {
            "type": [
              "number",
              "null"
            ],
            "description": "이 금액을 넘으면 자동으로 사람에게 승인 요청이 갑니다."
          }
        }
      },
      "WalletCreateRequest": {
        "type": "object",
        "required": [
          "brn",
          "slug",
          "public_key"
        ],
        "properties": {
          "brn": {
            "type": "string",
            "description": "사업자등록번호(숫자 10자리). 국세청 상태 조회로 확인합니다."
          },
          "slug": {
            "type": "string",
            "description": "핸들에 쓸 이름. @{slug}.clad.kr 이 됩니다.",
            "pattern": "^[a-z0-9-]+$"
          },
          "public_key": {
            "type": "string",
            "description": "Ed25519 공개키"
          },
          "contact_email": {
            "type": [
              "string",
              "null"
            ],
            "format": "email"
          }
        }
      },
      "ChildWalletRequest": {
        "type": "object",
        "required": [
          "label"
        ],
        "properties": {
          "label": {
            "type": "string",
            "description": "현장명이나 에이전트 이름",
            "examples": [
              "A현장-외장"
            ]
          },
          "public_key": {
            "type": [
              "string",
              "null"
            ],
            "description": "에이전트가 따로 키를 쓸 때"
          },
          "limits": {
            "$ref": "#/components/schemas/WalletLimits"
          }
        }
      },
      "Reservation": {
        "type": "object",
        "description": "2단계 기능입니다. 예치와 에스크로는 법률 검토와 제휴가 끝난 뒤에 엽니다.",
        "required": [
          "quote_id",
          "state"
        ],
        "properties": {
          "quote_id": {
            "type": "string"
          },
          "state": {
            "type": "string",
            "enum": [
              "reserved",
              "expired",
              "released"
            ]
          },
          "reserved_krw": {
            "type": "number",
            "description": "예치된 금액"
          },
          "hold_until": {
            "type": [
              "string",
              "null"
            ],
            "format": "date"
          },
          "supplier_brn": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "InquiryList": {
        "type": "object",
        "required": [
          "count",
          "items"
        ],
        "properties": {
          "count": {
            "type": "integer"
          },
          "items": {
            "type": "array",
            "items": {
              "type": "object",
              "description": "견적을 물어본 법인과 조건. 제조사에게는 이것 자체가 리드입니다.",
              "properties": {
                "asked_at": {
                  "type": "string",
                  "format": "date"
                },
                "buyer_brn": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "description": "핸들을 제시하지 않은 요청은 null 이고 익명 등급으로 처리됩니다."
                },
                "buyer_handle": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "sku": {
                  "type": "string"
                },
                "quantity": {
                  "type": [
                    "number",
                    "null"
                  ]
                },
                "unit": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "state": {
                  "type": "string",
                  "enum": [
                    "infeasible",
                    "needs_review",
                    "indicative"
                  ]
                },
                "reason": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "description": "infeasible 일 때 거절 사유. 어떤 한계에서 걸렸는지가 남습니다."
                }
              }
            }
          }
        }
      }
    }
  },
  "x-billing": {
    "status": "metering-live",
    "pricing_status": "provisional",
    "free_paths": [
      "/products",
      "/products/{sku}",
      "/suppliers/{brn}"
    ],
    "charged_paths": [
      "/quotes"
    ],
    "quote_state_pricing": {
      "infeasible": 0,
      "needs_review": null,
      "indicative": null
    },
    "accepts": [
      "clad-credit"
    ],
    "topup": {
      "status": "planned",
      "note": "충전 경로가 없습니다. 잔액은 운영자가 부여합니다."
    },
    "settlement": {
      "status": "planned",
      "note": "제조사 정산 경로가 없습니다."
    },
    "note": "조회는 무료이고 값을 매기는 것은 견적뿐입니다. 지갑·한도·차감·기록은 실제로 동작합니다. 충전과 정산만 없습니다 — 제3자에게 대금이 흘러가는 구조가 전자금융거래법 검토가 필요한 지점이라, 그 두 개만 떼어 두었습니다. 유료 두 상태의 금액이 정해지기 전에는 차감액이 0원입니다."
  }
}