openapi: 3.1.0
info:
  title: 수열에너지원 자원지도 Open API
  version: 1.0.0
  description: 전국 500m 격자 수열자원, 시설, 관측자료, 잠재량, 경제성 및 비동기 분석·내보내기 API
servers:
  - url: /api/v1
tags:
  - name: Public
  - name: Resources
  - name: Analysis
  - name: Exports
  - name: Administration
security:
  - ApiKeyAuth: []
paths:
  /health:
    get:
      tags: [Public]
      security: []
      summary: 데이터베이스 및 PostGIS 상태 조회
      responses:
        '200': { $ref: '#/components/responses/Success' }
        '503': { $ref: '#/components/responses/Error' }
  /openapi:
    get:
      tags: [Public]
      security: []
      summary: OpenAPI 3.1 명세
      responses:
        '200':
          description: OpenAPI YAML
          content:
            application/yaml:
              schema: { type: string }
  /codes:
    get:
      tags: [Public]
      security: []
      summary: 공통코드 조회
      parameters:
        - { name: group, in: query, schema: { type: string } }
      responses:
        '200': { $ref: '#/components/responses/Success' }
  /datasets:
    get:
      tags: [Public]
      security: []
      summary: 공개 데이터셋 카탈로그
      description: 각 항목은 자료 성격(dataNature)과 합성자료 여부(isSynthetic)를 포함한다.
      responses:
        '200': { $ref: '#/components/responses/DatasetCatalogSuccess' }
  /metadata:
    get:
      tags: [Public]
      security: []
      summary: 데이터셋 메타데이터와 버전 조회
      description: 데이터셋과 각 버전에 dataNature/isSynthetic 합성자료 표지를 제공한다.
      parameters:
        - { name: datasetCode, in: query, schema: { type: string } }
      responses:
        '200': { $ref: '#/components/responses/MetadataSuccess' }
  /summary:
    get:
      tags: [Public]
      security: []
      summary: 행정구역별 잠재량 집계
      parameters:
        - { name: administrativeCode, in: query, schema: { type: string } }
        - { $ref: '#/components/parameters/WaterSourceType' }
      responses:
        '200': { $ref: '#/components/responses/Success' }
  /map/summary:
    get:
      tags: [Public]
      security: []
      summary: 저해상도 지도용 행정구역 집계
      parameters:
        - { name: administrativeCode, in: query, schema: { type: string } }
        - { $ref: '#/components/parameters/WaterSourceType' }
      responses:
        '200': { $ref: '#/components/responses/Success' }
  /map/layers/administrative-areas:
    get:
      tags: [Resources]
      summary: 현행 시도·시군구 행정경계 GeoJSON 조회
      parameters: &mapLayerParameters
        - { $ref: '#/components/parameters/Bbox' }
        - { name: toleranceM, in: query, schema: { type: number, minimum: 0, maximum: 5000, default: 0 } }
        - { $ref: '#/components/parameters/Cursor' }
        - { $ref: '#/components/parameters/Limit' }
      responses:
        '200': { $ref: '#/components/responses/FeatureCollection' }
        '400': { $ref: '#/components/responses/Error' }
  /map/layers/basins:
    get:
      tags: [Resources]
      summary: 유역경계 GeoJSON 조회
      parameters: *mapLayerParameters
      responses:
        '200': { $ref: '#/components/responses/FeatureCollection' }
  /map/layers/water-bodies:
    get:
      tags: [Resources]
      summary: 하천·저수지·호소 등 수체 GeoJSON 조회
      parameters: *mapLayerParameters
      responses:
        '200': { $ref: '#/components/responses/FeatureCollection' }
  /map/layers/stations:
    get:
      tags: [Resources]
      summary: 관측소 GeoJSON 조회
      parameters: *mapLayerParameters
      responses:
        '200': { $ref: '#/components/responses/FeatureCollection' }
  /map/layers/demand-sites:
    get:
      tags: [Resources]
      summary: 열수요처 GeoJSON 조회
      parameters: *mapLayerParameters
      responses:
        '200': { $ref: '#/components/responses/FeatureCollection' }
  /map/layers/restrictions:
    get:
      tags: [Resources]
      summary: 규제 참조구역 GeoJSON 조회
      description: 규제 적합계수 승인 전 참조 Geometry이며 approvalStatus가 mapping_required로 표시될 수 있다.
      parameters: *mapLayerParameters
      responses:
        '200': { $ref: '#/components/responses/FeatureCollection' }
  /search:
    get:
      tags: [Resources]
      summary: 시설·관측소·행정구역·유역·수체 통합검색
      parameters:
        - { name: q, in: query, required: true, schema: { type: string, minLength: 2, maxLength: 100 } }
        - { $ref: '#/components/parameters/Limit' }
      responses:
        '200': { $ref: '#/components/responses/Success' }
        '400': { $ref: '#/components/responses/Error' }
  /grids:
    get:
      tags: [Resources]
      summary: EPSG:4326 GeoJSON 500m 격자 조회
      parameters:
        - { $ref: '#/components/parameters/Bbox' }
        - { $ref: '#/components/parameters/AdministrativeCode' }
        - { $ref: '#/components/parameters/DatasetVersion' }
        - { $ref: '#/components/parameters/IsSynthetic' }
        - { $ref: '#/components/parameters/QualityStatus' }
        - { $ref: '#/components/parameters/Cursor' }
        - { $ref: '#/components/parameters/Limit' }
      responses:
        '200': { $ref: '#/components/responses/FeatureCollection' }
        '401': { $ref: '#/components/responses/Error' }
        '429': { $ref: '#/components/responses/Error' }
  /grids/{gridId}:
    get:
      tags: [Resources]
      summary: 500m 격자 공간관계·시설·최근 관측·분석결과 상세 조회
      parameters:
        - name: gridId
          in: path
          required: true
          schema: { type: string }
      responses:
        '200': { $ref: '#/components/responses/Success' }
        '401': { $ref: '#/components/responses/Error' }
        '404': { $ref: '#/components/responses/Error' }
  /resources:
    get:
      tags: [Resources]
      summary: 수열원 유형별 운영시설·용량 집계
      parameters:
        - { $ref: '#/components/parameters/WaterSourceType' }
        - { $ref: '#/components/parameters/GridId' }
        - { $ref: '#/components/parameters/AdministrativeCode' }
        - { $ref: '#/components/parameters/DatasetVersion' }
        - { $ref: '#/components/parameters/IsSynthetic' }
        - { $ref: '#/components/parameters/QualityStatus' }
        - { $ref: '#/components/parameters/From' }
        - { $ref: '#/components/parameters/To' }
        - { $ref: '#/components/parameters/Bbox' }
      responses:
        '200': { $ref: '#/components/responses/Success' }
  /facilities:
    get:
      tags: [Resources]
      summary: 수열원 시설 GeoJSON 조회
      parameters:
        - { $ref: '#/components/parameters/WaterSourceType' }
        - { $ref: '#/components/parameters/GridId' }
        - { $ref: '#/components/parameters/AdministrativeCode' }
        - { $ref: '#/components/parameters/DatasetVersion' }
        - { $ref: '#/components/parameters/IsSynthetic' }
        - { $ref: '#/components/parameters/QualityStatus' }
        - { $ref: '#/components/parameters/From' }
        - { $ref: '#/components/parameters/To' }
        - { $ref: '#/components/parameters/Bbox' }
        - { $ref: '#/components/parameters/Cursor' }
        - { $ref: '#/components/parameters/Limit' }
      responses:
        '200': { $ref: '#/components/responses/FeatureCollection' }
  /observations:
    get:
      tags: [Resources]
      summary: 수온·유량 등 관측 시계열 조회
      parameters:
        - { name: stationId, in: query, schema: { type: string, format: uuid } }
        - { name: parameterCode, in: query, schema: { type: string } }
        - { $ref: '#/components/parameters/WaterSourceType' }
        - { $ref: '#/components/parameters/GridId' }
        - { $ref: '#/components/parameters/AdministrativeCode' }
        - { $ref: '#/components/parameters/DatasetVersion' }
        - { $ref: '#/components/parameters/IsSynthetic' }
        - { $ref: '#/components/parameters/QualityStatus' }
        - { $ref: '#/components/parameters/Bbox' }
        - { name: from, in: query, schema: { type: string, format: date-time } }
        - { name: to, in: query, schema: { type: string, format: date-time } }
        - { $ref: '#/components/parameters/Cursor' }
        - { $ref: '#/components/parameters/Limit' }
      responses:
        '200': { $ref: '#/components/responses/Success' }
  /potentials:
    get:
      tags: [Resources]
      summary: 최신 이론적·기술적·시장 잠재량 GeoJSON 조회
      parameters:
        - { $ref: '#/components/parameters/WaterSourceType' }
        - { $ref: '#/components/parameters/PotentialType' }
        - { $ref: '#/components/parameters/GridId' }
        - { $ref: '#/components/parameters/AdministrativeCode' }
        - { $ref: '#/components/parameters/DatasetVersion' }
        - { $ref: '#/components/parameters/IsSynthetic' }
        - { $ref: '#/components/parameters/QualityStatus' }
        - { $ref: '#/components/parameters/From' }
        - { $ref: '#/components/parameters/To' }
        - { $ref: '#/components/parameters/Bbox' }
        - { $ref: '#/components/parameters/Cursor' }
        - { $ref: '#/components/parameters/Limit' }
      responses:
        '200': { $ref: '#/components/responses/FeatureCollection' }
  /economics:
    get:
      tags: [Resources]
      summary: LCOH·LACH 경제성 결과 조회
      parameters:
        - { $ref: '#/components/parameters/GridId' }
        - { $ref: '#/components/parameters/WaterSourceType' }
        - { $ref: '#/components/parameters/AdministrativeCode' }
        - { $ref: '#/components/parameters/DatasetVersion' }
        - { $ref: '#/components/parameters/IsSynthetic' }
        - { $ref: '#/components/parameters/QualityStatus' }
        - { $ref: '#/components/parameters/From' }
        - { $ref: '#/components/parameters/To' }
        - { $ref: '#/components/parameters/Bbox' }
        - { $ref: '#/components/parameters/Cursor' }
        - { $ref: '#/components/parameters/Limit' }
      responses:
        '200': { $ref: '#/components/responses/Success' }
  /katb:
    get:
      tags: [Resources]
      summary: 승인 또는 검토 중인 K-ATB 파라미터 조회
      parameters:
        - { name: version, in: query, schema: { type: string } }
        - { name: technologyCode, in: query, schema: { type: string } }
      responses:
        '200': { $ref: '#/components/responses/Success' }
  /scenarios:
    get:
      tags: [Resources]
      summary: 공개 분석 시나리오 조회
      responses:
        '200': { $ref: '#/components/responses/Success' }
  /analysis/potential:
    post:
      tags: [Analysis]
      summary: 잠재량 계산 작업 등록
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/PotentialInput' }
      responses:
        '202': { $ref: '#/components/responses/Accepted' }
        '400': { $ref: '#/components/responses/Error' }
  /analysis/economics:
    post:
      tags: [Analysis]
      summary: 경제성 계산 작업 등록
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/EconomicInput' }
      responses:
        '202': { $ref: '#/components/responses/Accepted' }
        '400': { $ref: '#/components/responses/Error' }
  /analysis/jobs/{id}:
    get:
      tags: [Analysis]
      summary: 분석 작업 상태와 결과 조회
      parameters:
        - { $ref: '#/components/parameters/UuidPath' }
      responses:
        '200': { $ref: '#/components/responses/Success' }
        '404': { $ref: '#/components/responses/Error' }
  /analysis/jobs/{id}/cancel:
    post:
      tags: [Analysis]
      summary: 요청자 소유의 대기 중 분석 작업 취소
      parameters:
        - { $ref: '#/components/parameters/UuidPath' }
      responses:
        '200': { $ref: '#/components/responses/Success' }
        '409': { $ref: '#/components/responses/Error' }
  /downloads:
    get:
      tags: [Exports]
      summary: 요청자가 생성한 내보내기 작업 목록
      parameters:
        - { $ref: '#/components/parameters/Limit' }
      responses:
        '200': { $ref: '#/components/responses/Success' }
    post:
      tags: [Exports]
      summary: CSV, Excel 또는 GeoJSON 내보내기 작업 등록
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ExportInput' }
      responses:
        '202': { $ref: '#/components/responses/Accepted' }
  /downloads/{id}:
    get:
      tags: [Exports]
      summary: 내보내기 작업 상태와 산출 위치 조회
      parameters:
        - { $ref: '#/components/parameters/UuidPath' }
      responses:
        '200': { $ref: '#/components/responses/Success' }
        '404': { $ref: '#/components/responses/Error' }
  /downloads/{id}/file:
    get:
      tags: [Exports]
      summary: 완료된 내보내기 파일 다운로드
      parameters:
        - { $ref: '#/components/parameters/UuidPath' }
      responses:
        '200':
          description: CSV, Excel 또는 GeoJSON 파일
          content:
            text/csv: { schema: { type: string, format: binary } }
            application/geo+json: { schema: { type: object } }
            application/vnd.openxmlformats-officedocument.spreadsheetml.sheet:
              { schema: { type: string, format: binary } }
        '404': { $ref: '#/components/responses/Error' }
  /admin/imports:
    get:
      tags: [Administration]
      summary: 최근 적재 작업 목록 조회
      security:
        - ApiKeyAuth: []
          AdminSecret: []
      responses:
        '200': { $ref: '#/components/responses/Success' }
    post:
      tags: [Administration]
      summary: 서버 입력경로 기반 적재 작업 등록
      security:
        - ApiKeyAuth: []
          AdminSecret: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [datasetCode, version, inputPath, manifestPath]
              properties:
                datasetCode: { type: string }
                version: { type: string }
                inputPath: { type: string, description: DATA_INCOMING_ROOT 내부 상대경로 }
                manifestPath: { type: string, description: DATA_INCOMING_ROOT 내부 상대경로 }
      responses:
        '202': { $ref: '#/components/responses/Accepted' }
        '403': { $ref: '#/components/responses/Error' }
  /admin/imports/{id}:
    get:
      tags: [Administration]
      summary: 적재 작업·오류 조회
      security:
        - ApiKeyAuth: []
          AdminSecret: []
      parameters:
        - { $ref: '#/components/parameters/UuidPath' }
      responses:
        '200': { $ref: '#/components/responses/Success' }
  /admin/imports/{id}/approve:
    post:
      tags: [Administration]
      summary: QA 완료 적재 작업 승격 승인
      security:
        - ApiKeyAuth: []
          AdminSecret: []
      parameters:
        - { $ref: '#/components/parameters/UuidPath' }
      responses:
        '200': { $ref: '#/components/responses/Success' }
        '409': { $ref: '#/components/responses/Error' }
  /admin/aggregations:
    post:
      tags: [Administration]
      summary: WebGIS 서비스 물리화 뷰 갱신 작업 등록
      security:
        - ApiKeyAuth: []
          AdminSecret: []
      responses:
        '202': { $ref: '#/components/responses/Accepted' }
  /admin/aggregations/{id}:
    get:
      tags: [Administration]
      summary: 공간집계 작업 상태 조회
      security:
        - ApiKeyAuth: []
          AdminSecret: []
      parameters:
        - { $ref: '#/components/parameters/UuidPath' }
      responses:
        '200': { $ref: '#/components/responses/Success' }
        '404': { $ref: '#/components/responses/Error' }
  /admin/api-clients:
    get:
      tags: [Administration]
      summary: API 클라이언트와 키 상태 조회
      security:
        - ApiKeyAuth: []
          AdminSecret: []
      responses:
        '200': { $ref: '#/components/responses/Success' }
    post:
      tags: [Administration]
      summary: API 클라이언트 등록 및 키 최초 발급
      security:
        - ApiKeyAuth: []
          AdminSecret: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [clientCode, clientName, label]
              properties:
                clientCode: { type: string, pattern: '^[a-z0-9][a-z0-9_-]{2,63}$' }
                clientName: { type: string }
                label: { type: string }
                contactEmail: { type: string, format: email }
                roles: { type: array, items: { type: string, enum: [reader, analyst, importer, admin] } }
                minuteQuota: { type: integer, minimum: 1 }
                dailyQuota: { type: integer, minimum: 1 }
                expiresAt: { type: string, format: date-time }
      responses:
        '201': { $ref: '#/components/responses/Success' }
  /admin/api-keys/{id}/revoke:
    post:
      tags: [Administration]
      summary: API 키 폐기
      security:
        - ApiKeyAuth: []
          AdminSecret: []
      parameters:
        - { $ref: '#/components/parameters/UuidPath' }
      responses:
        '200': { $ref: '#/components/responses/Success' }
  /admin/api-clients/{id}/quota:
    post:
      tags: [Administration]
      summary: API 클라이언트 분당·일일 할당량 변경
      security:
        - ApiKeyAuth: []
          AdminSecret: []
      parameters:
        - { $ref: '#/components/parameters/UuidPath' }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [minuteQuota, dailyQuota]
              properties:
                minuteQuota: { type: integer, minimum: 1, maximum: 100000 }
                dailyQuota: { type: integer, minimum: 1, maximum: 10000000 }
      responses:
        '200': { $ref: '#/components/responses/Success' }
  /admin/analysis-jobs:
    get:
      tags: [Administration]
      summary: 분석 작업 목록 조회
      security:
        - ApiKeyAuth: []
          AdminSecret: []
      responses:
        '200': { $ref: '#/components/responses/Success' }
  /admin/analysis-jobs/{id}/cancel:
    post:
      tags: [Administration]
      summary: 대기 중 분석 작업 취소
      security:
        - ApiKeyAuth: []
          AdminSecret: []
      parameters:
        - { $ref: '#/components/parameters/UuidPath' }
      responses:
        '200': { $ref: '#/components/responses/Success' }
        '409': { $ref: '#/components/responses/Error' }
  /admin/audit:
    get:
      tags: [Administration]
      summary: 최근 API 감사기록 조회
      security:
        - ApiKeyAuth: []
          AdminSecret: []
      responses:
        '200': { $ref: '#/components/responses/Success' }
  /admin/analysis-contexts:
    get:
      tags: [Administration]
      summary: K-ATB 및 시나리오 승인상태 조회
      security:
        - ApiKeyAuth: []
          AdminSecret: []
      responses:
        '200': { $ref: '#/components/responses/Success' }
  /admin/katb/{id}/approve:
    post:
      tags: [Administration]
      summary: 출처와 파라미터가 준비된 검토 중 K-ATB 승인
      security:
        - ApiKeyAuth: []
          AdminSecret: []
      parameters:
        - { $ref: '#/components/parameters/UuidPath' }
      responses:
        '200': { $ref: '#/components/responses/Success' }
        '409': { $ref: '#/components/responses/Error' }
  /admin/scenarios/{id}/publish:
    post:
      tags: [Administration]
      summary: 파라미터가 준비된 검토 중 시나리오 게시
      security:
        - ApiKeyAuth: []
          AdminSecret: []
      parameters:
        - { $ref: '#/components/parameters/UuidPath' }
      responses:
        '200': { $ref: '#/components/responses/Success' }
        '409': { $ref: '#/components/responses/Error' }
  /admin/mapping-candidates:
    get:
      tags: [Administration]
      summary: 공인 매핑 입력이 필요한 시설·관측소 후보 조회
      security:
        - ApiKeyAuth: []
          AdminSecret: []
      responses:
        '200': { $ref: '#/components/responses/Success' }
  /admin/mapping-candidates/{candidateType}/{id}/decision:
    post:
      tags: [Administration]
      summary: 공식 근거를 첨부한 시설·관측소 매핑 승인 또는 반려
      security:
        - ApiKeyAuth: []
          AdminSecret: []
      parameters:
        - name: candidateType
          in: path
          required: true
          schema: { type: string, enum: [facility_reference, unlocated_station] }
        - { $ref: '#/components/parameters/UuidPath' }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [decision, evidenceUri]
              properties:
                decision: { type: string, enum: [approved, rejected] }
                evidenceUri: { type: string, format: uri }
                officialKey: { type: string }
                longitude: { type: number, minimum: 124, maximum: 132 }
                latitude: { type: number, minimum: 32, maximum: 40 }
                notes: { type: string }
      responses:
        '200': { $ref: '#/components/responses/Success' }
        '409': { $ref: '#/components/responses/Error' }
  /admin/usage:
    get:
      tags: [Administration]
      summary: 최근 API 사용량·오류율·처리시간 집계
      security:
        - ApiKeyAuth: []
          AdminSecret: []
      responses:
        '200': { $ref: '#/components/responses/Success' }
  /admin/datasets:
    get:
      tags: [Administration]
      summary: 데이터셋과 버전 관리 목록
      security:
        - ApiKeyAuth: []
          AdminSecret: []
      responses:
        '200': { $ref: '#/components/responses/Success' }
    post:
      tags: [Administration]
      summary: 데이터셋 등록
      security:
        - ApiKeyAuth: []
          AdminSecret: []
      responses:
        '201': { $ref: '#/components/responses/Success' }
  /admin/datasets/{id}/versions:
    post:
      tags: [Administration]
      summary: draft 데이터셋 버전 등록
      security:
        - ApiKeyAuth: []
          AdminSecret: []
      parameters:
        - { $ref: '#/components/parameters/UuidPath' }
      responses:
        '201': { $ref: '#/components/responses/Success' }
  /admin/dataset-versions/{id}/status:
    post:
      tags: [Administration]
      summary: 데이터셋 버전 품질검증·게시·반려·대체 상태 변경
      security:
        - ApiKeyAuth: []
          AdminSecret: []
      parameters:
        - { $ref: '#/components/parameters/UuidPath' }
      responses:
        '200': { $ref: '#/components/responses/Success' }
        '409': { $ref: '#/components/responses/Error' }
components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
    AdminSecret:
      type: apiKey
      in: header
      name: X-Admin-Secret
  parameters:
    UuidPath:
      name: id
      in: path
      required: true
      schema: { type: string, format: uuid }
    Cursor:
      name: cursor
      in: query
      schema: { type: string }
    Limit:
      name: limit
      in: query
      schema: { type: integer, minimum: 1, maximum: 1000, default: 100 }
    Bbox:
      name: bbox
      in: query
      description: EPSG:4326 minLon,minLat,maxLon,maxLat. 상세 격자·분석 결과는 최대 1 제곱도, 지도 레이어는 최대 4 제곱도이며 초과 시 BBOX_TOO_LARGE(413)를 반환한다.
      schema: { type: string, pattern: '^-?\d+(\.\d+)?,-?\d+(\.\d+)?,-?\d+(\.\d+)?,-?\d+(\.\d+)?$' }
    WaterSourceType:
      name: waterSourceType
      in: query
      schema: { $ref: '#/components/schemas/WaterSourceType' }
    PotentialType:
      name: potentialType
      in: query
      schema: { type: string, enum: [theoretical, technical, market] }
    GridId:
      name: gridId
      in: query
      schema: { type: string }
    AdministrativeCode:
      name: administrativeCode
      in: query
      schema: { type: string }
    DatasetVersion:
      name: datasetVersion
      in: query
      description: 데이터셋 버전 문자열로 결과를 제한한다.
      schema: { type: string }
    IsSynthetic:
      name: isSynthetic
      in: query
      description: 합성자료 여부로 결과를 제한한다.
      schema: { type: boolean }
    QualityStatus:
      name: qualityStatus
      in: query
      description: 데이터 버전 품질상태로 결과를 제한한다.
      schema: { type: string, enum: [complete, published_with_rejections] }
    From:
      name: from
      in: query
      schema: { type: string, format: date-time }
    To:
      name: to
      in: query
      schema: { type: string, format: date-time }
  schemas:
    WaterSourceType:
      type: string
      enum: [river, water_supply, wastewater, groundwater]
    DataNature:
      type: string
      enum: [official, synthetic, derived]
    DatasetCatalogItem:
      type: object
      required: [dataset_code, dataNature, isSynthetic]
      properties:
        dataset_code: { type: string }
        dataNature: { $ref: '#/components/schemas/DataNature' }
        isSynthetic: { type: boolean }
        latestVersionIsSynthetic: { type: [boolean, 'null'] }
        source_row_count: { type: [integer, 'null'], minimum: 0 }
        rejected_row_count: { type: [integer, 'null'], minimum: 0 }
        rejection_ratio: { type: [number, 'null'], minimum: 0, maximum: 1 }
        quality_status: { type: [string, 'null'], enum: [complete, published_with_rejections, null] }
      additionalProperties: true
    DatasetMetadataItem:
      type: object
      required: [dataset_code, dataNature, isSynthetic, versions]
      properties:
        dataset_code: { type: string }
        dataNature: { $ref: '#/components/schemas/DataNature' }
        isSynthetic: { type: boolean }
        versions:
          type: array
          items:
            type: object
            required: [version, status, isSynthetic]
            properties:
              version: { type: string }
              status: { type: string }
              isSynthetic: { type: boolean }
              sourceRowCount: { type: [integer, 'null'], minimum: 0 }
              rejectedRowCount: { type: integer, minimum: 0 }
              rejectionRatio: { type: number, minimum: 0, maximum: 1 }
              qualityStatus: { type: string, enum: [complete, published_with_rejections] }
            additionalProperties: true
      additionalProperties: true
    SuccessEnvelope:
      type: object
      required: [data, meta, links]
      properties:
        data: {}
        meta:
          type: object
          required: [requestId]
          properties:
            requestId: { type: string, format: uuid }
            cursor: { type: [string, 'null'] }
            limit: { type: integer }
            count: { type: integer }
        links:
          type: object
          required: [self, next]
          properties:
            self: { type: string }
            next: { type: [string, 'null'] }
          additionalProperties: { type: [string, 'null'] }
    ErrorEnvelope:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message, details, requestId]
          properties:
            code: { type: string }
            message: { type: string }
            details: {}
            requestId: { type: string, format: uuid }
    PotentialInput:
      type: object
      additionalProperties: false
      description: 계산 재현성을 위해 승인 문맥 ID가 필수다. K-ATB·시나리오 관리 파라미터는 요청값을 신뢰하지 않고 선택한 승인 버전의 값으로 서버에서 대체한다.
      required:
        [
          scenarioId,
          katbVersionId,
          target,
          waterSourceType,
          referenceStart,
          referenceEnd,
          flowM3S,
          deltaTemperatureK,
          waterDensityKgM3,
          specificHeatKjKgK,
          availableHours,
          intakeRatio,
          systemAvailability,
          technicalEfficiency,
          demandMatchRatio,
          adoptionRate,
          regulatoryFactor,
        ]
      properties:
        scenarioId: { type: string, format: uuid, description: 사용할 공개 시나리오의 명시적 ID }
        katbVersionId: { type: string, format: uuid, description: 사용할 승인 K-ATB 버전의 명시적 ID }
        inputDatasetVersionIds:
          type: array
          maxItems: 100
          items: { type: string, format: uuid }
          default: []
        gridId:
          { type: string, deprecated: true, description: 하위 호환용 단일 격자 ID. 신규 요청은 target을 사용한다. }
        target: { $ref: '#/components/schemas/AnalysisTarget' }
        flowAllocation:
          {
            type: string,
            enum: [total, per_grid],
            default: total,
            description: 다중 격자에서 총유량을 나눌지 격자마다 적용할지 지정,
          }
        waterSourceType: { $ref: '#/components/schemas/WaterSourceType' }
        referenceStart: { type: string, format: date }
        referenceEnd: { type: string, format: date }
        flowM3S: { type: number, minimum: 0 }
        temperatureC: { type: number, minimum: -10, maximum: 60 }
        deltaTemperatureK: { type: number, minimum: 0 }
        waterDensityKgM3: { type: number, exclusiveMinimum: 0 }
        specificHeatKjKgK: { type: number, exclusiveMinimum: 0 }
        availableHours: { type: number, minimum: 0, maximum: 8784 }
        intakeRatio: { type: number, minimum: 0, maximum: 1 }
        systemAvailability: { type: number, minimum: 0, maximum: 1 }
        technicalEfficiency: { type: number, minimum: 0, maximum: 1 }
        cop: { type: number, exclusiveMinimum: 1, maximum: 20 }
        demandMatchRatio: { type: number, minimum: 0, maximum: 1 }
        adoptionRate: { type: number, minimum: 0, maximum: 1 }
        regulatoryFactor: { type: number, minimum: 0, maximum: 1 }
        referenceEmissionKgCo2eKwh: { type: number, minimum: 0, default: 0 }
        projectEmissionKgCo2eKwh: { type: number, minimum: 0, default: 0 }
    EconomicInput:
      type: object
      additionalProperties: false
      description: 시나리오의 기준연도·할인율·탄소비용 포함 여부는 요청값 대신 선택한 공개 시나리오의 승인값으로 서버에서 대체한다.
      required: [scenarioId, katbVersionId, target, baseYear, discountRate, capex, annualValues]
      properties:
        scenarioId: { type: string, format: uuid, description: 사용할 공개 시나리오의 명시적 ID }
        katbVersionId: { type: string, format: uuid, description: 사용할 승인 K-ATB 버전의 명시적 ID }
        inputDatasetVersionIds:
          type: array
          maxItems: 100
          items: { type: string, format: uuid }
          default: []
        potentialResultId: { type: string, format: uuid }
        gridId: { type: string, deprecated: true, description: 하위 호환용 단일 격자 ID }
        target: { $ref: '#/components/schemas/AnalysisTarget' }
        baseYear: { type: integer, minimum: 2000, maximum: 2200 }
        currencyCode: { type: string, minLength: 3, maxLength: 3, default: KRW }
        discountRate: { type: number, exclusiveMinimum: -1, maximum: 1 }
        capex: { type: number, minimum: 0 }
        includeCarbonCost: { type: boolean, default: false }
        annualValues:
          type: array
          minItems: 1
          items:
            type: object
            additionalProperties: false
            required: [year, operatingCost, usefulHeatKwh, avoidedCost]
            properties:
              year: { type: integer, minimum: 0 }
              operatingCost: { type: number, minimum: 0 }
              replacementCost: { type: number, minimum: 0, default: 0 }
              residualValue: { type: number, minimum: 0, default: 0 }
              usefulHeatKwh: { type: number, minimum: 0 }
              avoidedCost: { type: number, minimum: 0 }
              carbonBenefit: { type: number, minimum: 0, default: 0 }
    AnalysisTarget:
      oneOf:
        - type: object
          additionalProperties: false
          required: [type, gridIds]
          properties:
            type: { type: string, const: grids }
            gridIds:
              type: array
              minItems: 1
              maxItems: 5000
              uniqueItems: true
              items: { type: string }
        - type: object
          additionalProperties: false
          required: [type, bbox]
          properties:
            type: { type: string, const: bbox }
            bbox:
              type: array
              minItems: 4
              maxItems: 4
              items: { type: number }
        - type: object
          additionalProperties: false
          required: [type, geometry]
          properties:
            type: { type: string, const: geometry }
            geometry:
              type: object
              required: [type, coordinates]
              properties:
                type: { type: string, enum: [Polygon, MultiPolygon] }
                coordinates: {}
        - type: object
          additionalProperties: false
          required: [type, administrativeCode]
          properties:
            type: { type: string, const: administrative }
            administrativeCode: { type: string }
        - type: object
          additionalProperties: false
          required: [type, facilityId]
          properties:
            type: { type: string, const: facility }
            facilityId: { type: string, format: uuid }
    ExportInput:
      type: object
      additionalProperties: false
      required: [exportType, outputFormat]
      properties:
        exportType: { type: string, enum: [facilities, observations, potentials, economics] }
        outputFormat: { type: string, enum: [csv, geojson, xlsx] }
        filters:
          type: object
          properties:
            waterSourceType: { $ref: '#/components/schemas/WaterSourceType' }
            potentialType: { type: string, enum: [theoretical, technical, market] }
            gridId: { type: string }
            stationId: { type: string, format: uuid }
            parameterCode: { type: string }
            datasetCode: { type: string }
            datasetVersion: { type: string }
            isSynthetic: { type: boolean }
            qualityStatus: { type: string, enum: [complete, published_with_rejections] }
            maxRows: { type: integer, minimum: 1, maximum: 1000000, default: 200000 }
            from: { type: string, format: date-time }
            to: { type: string, format: date-time }
            bbox:
              type: array
              minItems: 4
              maxItems: 4
              items: { type: number }
  responses:
    DatasetCatalogSuccess:
      description: 데이터셋 카탈로그 조회 성공
      content:
        application/json:
          schema:
            allOf:
              - $ref: '#/components/schemas/SuccessEnvelope'
              - type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/DatasetCatalogItem' }
    MetadataSuccess:
      description: 데이터셋 메타데이터 조회 성공
      content:
        application/json:
          schema:
            allOf:
              - $ref: '#/components/schemas/SuccessEnvelope'
              - type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/DatasetMetadataItem' }
    Success:
      description: 성공
      content:
        application/json:
          schema: { $ref: '#/components/schemas/SuccessEnvelope' }
    Accepted:
      description: 비동기 작업 등록됨
      headers:
        Location: { schema: { type: string } }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/SuccessEnvelope' }
    FeatureCollection:
      description: RFC 7946 GeoJSON 결과를 data에 포함한 성공 응답
      content:
        application/json:
          schema: { $ref: '#/components/schemas/SuccessEnvelope' }
    Error:
      description: 표준 오류
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorEnvelope' }
