openapi: 3.0.3
info:
  title: MEKONG Exchange API
  description: |
    MEKONG 현물 암호화폐 거래소 공개 REST API.
    모든 금액은 **최소단위 정수의 문자열**(예: 1 USDT = "1000000", 6 decimals). float 금지.
    인증 2가지: (1) Bearer JWT (POST /v1/auth/login/register로 발급),
    (2) **API 키 HMAC**(봇용) — 헤더 `X-API-Key`, `X-API-Timestamp`(unix ms),
    `X-API-Signature`=HMAC-SHA256(secret, timestamp+method+path+sha256(body)hex). 타임스탬프 ±30s.
  version: "1.0.0"
servers:
  - url: http://localhost:15000
    description: dev
security:
  - bearerAuth: []
tags:
  - name: auth
  - name: account
  - name: orders
  - name: market
  - name: wallet
paths:
  /v1/auth/register:
    post:
      tags: [auth]
      summary: 회원가입 (JWT 발급)
      security: []
      requestBody: { $ref: '#/components/requestBodies/Credentials' }
      responses:
        '201': { description: 생성됨, content: { application/json: { schema: { $ref: '#/components/schemas/AuthResponse' } } } }
        '400': { $ref: '#/components/responses/Error' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /v1/auth/login:
    post:
      tags: [auth]
      summary: 로그인 (JWT 발급)
      security: []
      requestBody: { $ref: '#/components/requestBodies/Credentials' }
      responses:
        '200': { description: 성공, content: { application/json: { schema: { $ref: '#/components/schemas/AuthResponse' } } } }
        '401': { $ref: '#/components/responses/Error' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /v1/me:
    get:
      tags: [account]
      summary: 내 프로필 (KYC 레벨·VIP 수수료 티어 포함)
      responses:
        '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/User' } } } }
  /v1/balances:
    get:
      tags: [account]
      summary: 자산별 잔고 (available/hold)
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  balances:
                    type: array
                    items: { $ref: '#/components/schemas/Balance' }
  /v1/orders:
    post:
      tags: [orders]
      summary: 주문 접수 (지정가/시장가/IOC/FOK/Post-Only/스톱)
      parameters:
        - name: Idempotency-Key
          in: header
          required: false
          schema: { type: string }
          description: 멱등 키 — 재시도 시 같은 주문 반환(중복 체결 방지)
      requestBody:
        required: true
        content: { application/json: { schema: { $ref: '#/components/schemas/PlaceOrder' } } }
      responses:
        '201': { description: 접수됨, content: { application/json: { schema: { $ref: '#/components/schemas/Order' } } } }
        '400': { $ref: '#/components/responses/Error' }
        '409': { description: 거절 (FOK 미충족·Post-Only 테이커·중복 등) }
        '429': { $ref: '#/components/responses/RateLimited' }
    get:
      tags: [orders]
      summary: 미체결 주문 (대기 스톱 포함)
      responses:
        '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/OrderList' } } } }
    delete:
      tags: [orders]
      summary: 전체 취소 (미체결+대기 스톱)
      parameters:
        - name: market
          in: query
          required: false
          schema: { type: string }
          description: 지정 시 해당 마켓만
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object, properties: { canceled: { type: integer } } } } } }
  /v1/orders/history:
    get:
      tags: [orders]
      summary: 주문 이력
      parameters:
        - { name: limit, in: query, schema: { type: integer, default: 100 } }
      responses:
        '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/OrderList' } } } }
  /v1/mytrades:
    get:
      tags: [orders]
      summary: 내 체결 이력 (개별 체결, 수수료 포함)
      parameters:
        - { name: market, in: query, schema: { type: string } }
        - { name: limit, in: query, schema: { type: integer, default: 100 } }
      responses:
        '200': { description: OK }
  /v1/orders/{id}:
    put:
      tags: [orders]
      summary: 주문 수정 (취소 → 새 가격/수량으로 재접수, 새 주문 id 반환·시간우선순위 상실)
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                price: { type: string, description: 새 가격 (quote 최소단위, 생략=유지) }
                qty: { type: string, description: 새 총 수량 (base 최소단위, 생략=미체결 잔량) }
      responses:
        '200': { description: 새 주문 }
        '422': { description: 수정 불가 상태(체결/취소됨, 지정가 아님) }
        '504': { description: 취소는 됐으나 잔고 반영 지연 — 재접수 필요 }
    get:
      tags: [orders]
      summary: 주문 조회
      parameters: [{ name: id, in: path, required: true, schema: { type: string } }]
      responses:
        '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/Order' } } } }
        '404': { $ref: '#/components/responses/Error' }
    delete:
      tags: [orders]
      summary: 주문 취소
      parameters: [{ name: id, in: path, required: true, schema: { type: string } }]
      responses:
        '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/Order' } } } }
  /v1/markets:
    get:
      tags: [market]
      summary: 마켓 목록
      security: []
      responses:
        '200': { description: OK }
  /v1/markets/{symbol}/ticker:
    get:
      tags: [market]
      summary: 24시간 티커 (시/고/저/종·거래량·체결수)
      security: []
      parameters: [{ name: symbol, in: path, required: true, schema: { type: string } }]
      responses:
        '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/Ticker' } } } }
  /v1/tickers:
    get:
      tags: [market]
      summary: 전 마켓 24시간 티커 배치 (24h 체결 있는 마켓만)
      security: []
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object, properties: { tickers: { type: array, items: { $ref: '#/components/schemas/Ticker' } } } } } } }
  /v1/markets/{symbol}/orderbook:
    get:
      tags: [market]
      summary: 호가창
      security: []
      parameters: [{ name: symbol, in: path, required: true, schema: { type: string } }]
      responses: { '200': { description: OK } }
  /v1/markets/{symbol}/candles:
    get:
      tags: [market]
      summary: 캔들 (OHLCV)
      security: []
      parameters:
        - { name: symbol, in: path, required: true, schema: { type: string } }
        - { name: interval, in: query, schema: { type: string, default: 1m } }
      responses: { '200': { description: OK } }
  /v1/markets/{symbol}/trades:
    get:
      tags: [market]
      summary: 최근 체결
      security: []
      parameters: [{ name: symbol, in: path, required: true, schema: { type: string } }]
      responses: { '200': { description: OK } }
  /v1/api-keys:
    post:
      tags: [account]
      summary: API 키 발급 (프로그래밍 트레이딩 — secret 1회 노출)
      requestBody:
        required: false
        content: { application/json: { schema: { type: object, properties: { label: { type: string } } } } }
      responses:
        '201':
          description: 발급됨
          content:
            application/json:
              schema:
                type: object
                properties: { key_id: { type: string }, secret: { type: string }, label: { type: string } }
    get:
      tags: [account]
      summary: 내 API 키 목록 (secret 미포함)
      responses: { '200': { description: OK } }
  /v1/api-keys/{keyId}:
    delete:
      tags: [account]
      summary: API 키 폐기
      parameters: [{ name: keyId, in: path, required: true, schema: { type: string } }]
      responses: { '200': { description: OK } }
  /v1/deposits/{asset}/address:
    get:
      tags: [wallet]
      summary: 입금 주소 발급 (실체인)
      parameters: [{ name: asset, in: path, required: true, schema: { type: string } }]
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object, properties: { address: { type: string }, memo: { type: string } } } } } }
  /v1/withdrawals:
    post:
      tags: [wallet]
      summary: 출금 요청 (KYC·일일한도·4-eyes 승인 대상)
      parameters:
        - { name: Idempotency-Key, in: header, required: false, schema: { type: string }, description: 이중 출금 방지 멱등 키 }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [asset, to_address, amount]
              properties:
                asset: { type: string }
                to_address: { type: string }
                amount: { type: string, description: 최소단위 정수 문자열 }
                memo: { type: string }
      responses:
        '201': { description: 접수됨 }
        '403': { description: KYC/한도 미달 }
    get:
      tags: [wallet]
      summary: 출금 내역
      responses: { '200': { description: OK } }
  /v1/orders/oco:
    post:
      tags: [orders]
      summary: OCO 주문 (지정가 익절 + 스톱 손절, 한쪽 체결 시 다른 쪽 자동 취소)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [market, side, qty, limit_price, stop_trigger, stop_price]
              properties:
                market: { type: string, example: BTC-USDT }
                side: { type: string, enum: [buy, sell] }
                qty: { type: string, description: base 최소단위 }
                limit_price: { type: string, description: 익절 지정가 (quote 최소단위) }
                stop_trigger: { type: string, description: 스톱 발동 트리거 }
                stop_price: { type: string, description: 발동 후 지정가 }
      responses:
        '201': { description: 접수됨 (두 주문, 홀드는 1회) }
  /v1/convert:
    post:
      tags: [orders]
      summary: 컨버트 (인스턴트 스왑 — 내부적으로 시장가 주문)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [from, to, amount]
              properties:
                from: { type: string, example: BTC }
                to: { type: string, example: USDT }
                amount: { type: string, description: from 자산 최소단위 }
      responses:
        '200': { description: 스왑 완료 }
        '400': { description: NO_ROUTE / NO_LIQUIDITY / NO_PRICE }
  /v1/price-alerts:
    get:
      tags: [account]
      summary: 내 가격 알림 목록
      responses: { '200': { description: OK } }
    post:
      tags: [account]
      summary: 가격 알림 생성 (도달 시 인앱 알림)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [market, direction, target_price]
              properties:
                market: { type: string }
                direction: { type: string, enum: [above, below] }
                target_price: { type: string, description: quote 최소단위 }
      responses: { '201': { description: 생성됨 } }
  /v1/notifications:
    get:
      tags: [account]
      summary: 인앱 알림 목록 (로그인·입출금·KYC·가격 알림)
      responses: { '200': { description: OK } }
  /v1/referral:
    get:
      tags: [account]
      summary: 내 추천 코드·피추천인 수·누적 커미션
      responses: { '200': { description: OK } }
  /v1/referral/commissions:
    get:
      tags: [account]
      summary: 추천 커미션 내역 (피추천인 이메일 마스킹)
      responses: { '200': { description: OK } }
  /v1/proof-of-reserves:
    get:
      tags: [market]
      summary: 지급준비금 증명 (자산별 부채 vs 핫월렛 준비금)
      security: []
      responses: { '200': { description: OK } }
  /v1/status:
    get:
      tags: [market]
      summary: 시스템 상태 (백엔드 실시간 헬스 프로브)
      security: []
      responses: { '200': { description: OK } }
components:
  securitySchemes:
    bearerAuth: { type: http, scheme: bearer, bearerFormat: JWT }
  requestBodies:
    Credentials:
      required: true
      content:
        application/json:
          schema:
            type: object
            required: [email, password]
            properties:
              email: { type: string, format: email }
              password: { type: string, minLength: 8 }
  responses:
    Error:
      description: 오류
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    RateLimited:
      description: 레이트 리밋 초과 (Retry-After 헤더 참조)
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
  schemas:
    Error:
      type: object
      properties:
        error:
          type: object
          properties: { code: { type: string }, message: { type: string } }
    AuthResponse:
      type: object
      properties:
        access_token: { type: string }
        user: { $ref: '#/components/schemas/User' }
    User:
      type: object
      properties:
        id: { type: string }
        email: { type: string }
        status: { type: string, enum: [active, frozen, closed] }
        kyc_level: { type: integer }
        fee_tier: { type: integer, description: VIP 수수료 티어 (0=기본) }
        created_at_ms: { type: integer, format: int64 }
    Balance:
      type: object
      properties:
        asset: { type: string }
        available: { type: string }
        hold: { type: string }
    PlaceOrder:
      type: object
      required: [market, side, type, qty]
      properties:
        market: { type: string, example: BTC-USDT }
        side: { type: string, enum: [buy, sell] }
        type: { type: string, enum: [limit, market, ioc, fok, post_only, stop_limit] }
        price: { type: string, description: 최소단위 정수. market은 생략, stop_limit은 발동 후 지정가 }
        qty: { type: string }
        trigger_price: { type: string, description: stop_limit 전용 발동 가격 }
        client_order_id: { type: string, description: 멱등 키(헤더 Idempotency-Key와 동일 역할) }
    Order:
      type: object
      properties:
        id: { type: string }
        market: { type: string }
        side: { type: string }
        type: { type: string }
        price: { type: string }
        qty: { type: string }
        filled_qty: { type: string }
        status: { type: string, enum: [new, open, partially_filled, pending_trigger, filled, canceled, rejected] }
        client_order_id: { type: string }
        created_at_ms: { type: integer, format: int64 }
    OrderList:
      type: object
      properties:
        orders:
          type: array
          items: { $ref: '#/components/schemas/Order' }
    Ticker:
      type: object
      properties:
        market: { type: string }
        last_price: { type: string }
        open_24h: { type: string }
        high_24h: { type: string }
        low_24h: { type: string }
        volume_24h: { type: string }
        trades_24h: { type: integer }
