{
  "openapi": "3.1.0",
  "info": {
    "title": "shopforagent search API",
    "version": "1.0.0",
    "description": "한국 커머스 상품 검색 + 제휴 링크 반환 API. 결과 항목은 소스별 미지원 필드를 null 또는 생략으로 구분한다. 이 파일이 HTTP 계약의 단일 출처다(JSON 표현 — 검증 테스트가 YAML 의존성 없이 읽는다)."
  },
  "servers": [
    { "url": "https://shopforagent.shop" }
  ],
  "paths": {
    "/api/pool/search": {
      "get": {
        "summary": "큐레이션 풀 검색 (미스 시 쿠팡 폴백)",
        "parameters": [
          { "name": "keywords", "in": "query", "required": true, "schema": { "type": "string" }, "description": "쉼표 구분 질의. 서버가 정규화·오타 교정·동의어 확장을 적용한다." },
          { "name": "platform", "in": "query", "required": false, "schema": { "type": "string" }, "description": "플랫폼 필터. 지정하면 다른 플랫폼 폴백을 하지 않는다." },
          { "name": "limit", "in": "query", "required": false, "schema": { "type": "integer", "minimum": 1, "maximum": 50, "default": 5 }, "description": "상한 50. 초과 요청은 50으로 절단되고 meta.limit_applied에 반영된다." }
        ],
        "responses": {
          "200": { "description": "검색 결과(0건 포함)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SearchResponse" } } } },
          "400": { "description": "keywords 누락·limit 형식 오류", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } },
          "401": { "description": "토큰 모드에서 토큰 불일치", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } },
          "429": { "description": "IP 시간당 한도 초과", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } },
          "500": { "description": "풀 파일을 읽을 수 없음", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } },
          "502": { "description": "폴백 업스트림 실패", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }
        }
      }
    },
    "/api/coupang/search": {
      "get": {
        "summary": "쿠팡 파트너스 실검색 + 딥링크",
        "parameters": [
          { "name": "keywords", "in": "query", "required": true, "schema": { "type": "string" } },
          { "name": "limit", "in": "query", "required": false, "schema": { "type": "integer", "minimum": 1, "maximum": 50, "default": 5 } }
        ],
        "responses": {
          "200": { "description": "검색 결과(0건 포함)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SearchResponse" } } } },
          "400": { "description": "keywords 누락·limit 형식 오류", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } },
          "429": { "description": "IP 시간당 또는 소스 일일 예산 초과", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } },
          "502": { "description": "업스트림 실패·타임아웃", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } },
          "503": { "description": "쿠팡 키 미등록(coupang_not_configured)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }
        }
      }
    },
    "/healthz": {
      "get": {
        "summary": "생존 확인",
        "responses": { "200": { "description": "ok (text/plain)" } }
      }
    },
    "/statsz": {
      "get": {
        "summary": "프로세스·소스 카운터 + 풀 상태",
        "responses": { "200": { "description": "statistics JSON", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Statsz" } } } } }
      }
    }
  },
  "components": {
    "schemas": {
      "SearchResponse": {
        "type": "object",
        "required": ["results", "meta", "disclosure"],
        "additionalProperties": false,
        "properties": {
          "results": { "type": "array", "items": { "$ref": "#/components/schemas/Item" } },
          "meta": { "$ref": "#/components/schemas/Meta" },
          "disclosure": { "type": "string", "description": "포함된 소스의 공시 문구. 결과가 없어도 사이트 수준 공시를 싣는다." }
        }
      },
      "Item": {
        "type": "object",
        "required": ["source", "platform", "product_name", "affiliate_url", "price", "image_url", "fetched_at", "disclosure"],
        "additionalProperties": false,
        "properties": {
          "source": { "type": "string", "enum": ["pool", "coupang"] },
          "product_id": { "type": "string", "description": "소스 내 식별자. 풀은 미지원이라 생략된다." },
          "platform": { "type": "string" },
          "product_name": { "type": "string" },
          "affiliate_url": { "type": "string", "description": "업스트림 원문 그대로. 링크 생성에 실패한 상품은 빈 문자열이다." },
          "price": {
            "oneOf": [
              { "$ref": "#/components/schemas/Price" },
              { "type": "null" }
            ],
            "description": "가격을 제공하지 않는 소스는 null(0원과 구분)."
          },
          "image_url": { "type": ["string", "null"], "description": "이미지를 제공하지 않는 소스는 null." },
          "added_at": { "type": "string", "description": "풀 등록 시각(RFC3339). 쿠팡은 생략." },
          "fetched_at": { "type": "string", "description": "조회 시각(RFC3339, UTC). 항상 있다." },
          "disclosure": { "type": "string", "description": "소스별 공시 문구. 모든 결과에 동반된다." }
        }
      },
      "Price": {
        "type": "object",
        "required": ["amount", "currency"],
        "additionalProperties": false,
        "properties": {
          "amount": { "type": "integer" },
          "currency": { "type": "string" }
        }
      },
      "Meta": {
        "type": "object",
        "required": ["applied_query", "normalized_terms", "sources_tried", "source_counts", "limit_applied", "retryable", "fetched_at"],
        "additionalProperties": false,
        "properties": {
          "applied_query": { "type": "string" },
          "normalized_terms": { "type": "array", "items": { "type": "string" } },
          "corrections": { "type": "array", "items": { "$ref": "#/components/schemas/Correction" } },
          "expansions": { "type": "array", "items": { "$ref": "#/components/schemas/Expansion" } },
          "sources_tried": { "type": "array", "items": { "type": "string", "enum": ["pool", "coupang"] } },
          "source_counts": { "type": "object", "additionalProperties": { "type": "integer" } },
          "empty_reason": { "type": "string", "enum": ["no_match", "not_seeded", "upstream_empty"] },
          "limit_applied": { "type": "integer" },
          "limit_requested": { "type": "integer" },
          "retryable": { "type": "boolean" },
          "retry_after_seconds": { "type": "integer" },
          "fetched_at": { "type": "string" }
        }
      },
      "Correction": {
        "type": "object",
        "required": ["from", "to"],
        "additionalProperties": false,
        "properties": { "from": { "type": "string" }, "to": { "type": "string" } }
      },
      "Expansion": {
        "type": "object",
        "required": ["from", "to"],
        "additionalProperties": false,
        "properties": { "from": { "type": "string" }, "to": { "type": "array", "items": { "type": "string" } } }
      },
      "ErrorResponse": {
        "type": "object",
        "required": ["error"],
        "additionalProperties": false,
        "properties": {
          "error": {
            "type": "object",
            "required": ["code", "message", "retryable"],
            "additionalProperties": false,
            "properties": {
              "code": {
                "type": "string",
                "enum": ["bad_request", "method_not_allowed", "unauthorized", "rate_limited", "quota_exceeded", "pool_unreadable", "coupang_not_configured", "upstream_timeout", "upstream_error"]
              },
              "message": { "type": "string" },
              "retryable": { "type": "boolean" },
              "retry_after_seconds": { "type": "integer" }
            }
          }
        }
      },
      "Statsz": {
        "type": "object",
        "required": ["requests_total", "responses_total", "errors_total", "by_endpoint", "pool", "sources"],
        "properties": {
          "requests_total": { "type": "integer" },
          "responses_total": { "type": "integer" },
          "errors_total": { "type": "integer" },
          "by_endpoint": { "type": "object" },
          "pool": { "type": "object" },
          "sources": {
            "type": "object",
            "additionalProperties": {
              "type": "object",
              "required": ["requests", "results", "empty", "errors"],
              "properties": {
                "requests": { "type": "integer" },
                "results": { "type": "integer" },
                "empty": { "type": "integer" },
                "errors": { "type": "integer" }
              }
            }
          }
        }
      }
    }
  }
}
