openapi: 3.0.3
info:
  title: Sendgo API
  description: |
    **Sendgo** 알림 전송 API
    카카오 알림톡(Alimtalk), 브랜드메시지(Brand Message), SMS/LMS/MMS 전송을 지원합니다.

    > **친구톡은 2025-12-31 종료되었습니다.** 2026-01-01 부터 친구톡 발송 요청은
    > 카카오 측에서 브랜드메시지(자유형)로 자동 대체 발송됩니다.
    > `/{version}/friends/send` 는 자유 본문 타입(`FT`/`FI`/`FW`)의 발송 경로로
    > 계속 유지되며, 템플릿 기반 리치 타입·비친구 타게팅·동보 발송은
    > `/{version}/brand-messages/send` 를 사용하세요.

    ## 인증 방법
    1. **토큰 발급**: `accessKey:secretKey`를 Base64 인코딩하여 Basic 인증으로 토큰 요청
    2. **API 호출**: 발급받은 토큰으로 Bearer 인증
       - **v1**: `Authorization: Bearer base64(token)`
       - **v2**: `Authorization: Bearer token`

    토큰은 발급 후 **24시간** 동안 유효합니다. 응답의 `expiresAt`(v2) / `expires_at`(v1) 값을
    확인해 만료 전에 재발급하세요.
  version: "2.0"
  contact:
    name: Sendgo 개발팀
    url: https://sendgo.io
  license:
    name: MIT

servers:
  # 모든 공식 SDK 의 기본값은 `https://sendgo.io` 다. 스펙이 다른 호스트를 광고하면
  # 스펙으로 생성한 클라이언트와 SDK 가 서로 다른 곳을 보게 되므로 순서를 맞춰 둔다.
  # 두 호스트 모두 유효한 인증서로 정상 응답한다.
  - url: https://sendgo.io/api
    description: Sendgo API 서버 (정규 — SDK 기본값)
  - url: https://api.sendgo.io/api
    description: Sendgo API 서버 (별칭)

tags:
  - name: 인증
    description: 토큰 발급
  - name: 알림톡
    description: 카카오 알림톡 (Alimtalk) 전송
  - name: 친구톡
    description: >-
      카카오 친구톡 (Friendtalk) 전송 — **2025-12-31 종료됨(Deprecated)**.
      2026-01-01 부터 카카오 측에서 브랜드메시지(자유형)로 자동 대체 발송됩니다.
      신규 연동은 브랜드메시지를 사용하세요.
  - name: 브랜드메시지
    description: 카카오 브랜드메시지 (Brand Message) 전송 — 친구톡의 후속 채널. v2 전용
  - name: 문자
    description: SMS / LMS / MMS 전송
  - name: 짧은URL
    description: 짧은 URL 생성과 고객 반응(클릭) 분석

paths:
  /{version}/token:
    post:
      tags: [인증]
      summary: 액세스 토큰 발급
      description: |
        `accessKey:secretKey`를 Base64로 인코딩하여 Basic 인증 헤더로 전달하면 Bearer 토큰을 발급합니다.
        토큰 유효시간은 **24시간**입니다.
      operationId: issueToken
      parameters:
        - $ref: '#/components/parameters/ApiVersion'
      security:
        - BasicAuth: []
      responses:
        '200':
          description: 토큰 발급 성공
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TokenResponse'
              example:
                data:
                  token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /{version}/notices/send:
    post:
      tags: [알림톡]
      summary: 카카오 알림톡 전송
      description: |
        사전에 Sendgo 콘솔에서 승인받은 템플릿 코드를 사용하여 알림톡을 전송합니다.
        `replaceSms: "Y"` 설정 시, 알림톡 전송 실패 시 자동으로 SMS 대체 발송합니다.
      operationId: sendAlimtalk
      parameters:
        - $ref: '#/components/parameters/ApiVersion'
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AlimtalkRequest'
            examples:
              basicSend:
                summary: 기본 즉시 발송
                value:
                  at: null
                  scheduleType: "DIRECTLY"
                  templateCode: "ORDER_CONFIRM_001"
                  replaceSms: "N"
                  smsSubject: null
                  smsContent: null
                  contacts:
                    - contact: "01012345678"
                      name: "홍길동"
                      var1: "ORD-20260101-001"
                      var2: "스프링 입문서"
                      var3: "29,000원"
                  kakaoSenderKey: "your_kakao_sender_key"
                  senderKey: "your_sms_sender_key"
              withFallback:
                summary: SMS 대체 발송 활성화
                value:
                  at: null
                  scheduleType: "DIRECTLY"
                  templateCode: "DELIVERY_001"
                  replaceSms: "Y"
                  smsSubject: "[배송 시작 안내]"
                  smsContent: "주문하신 상품이 출고되었습니다.\n송장번호: #{var2}"
                  contacts:
                    - contact: "01012345678"
                      name: "홍길동"
                      var1: "ORD-001"
                      var2: "1234567890"
                  kakaoSenderKey: "your_kakao_sender_key"
                  senderKey: "your_sms_sender_key"
              scheduledSend:
                summary: 예약 발송
                value:
                  at: "2026-04-01 09:00:00"
                  scheduleType: "SCHEDULED"
                  templateCode: "PROMO_001"
                  replaceSms: "N"
                  contacts:
                    - contact: "01012345678"
                      var1: "봄맞이 30% 할인"
                  kakaoSenderKey: "your_kakao_sender_key"
                  senderKey: "your_sms_sender_key"
      responses:
        '200':
          $ref: '#/components/responses/SuccessResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/Forbidden'

  /{version}/friends/send:
    post:
      tags: [친구톡]
      summary: 카카오 친구톡 전송 (Deprecated)
      deprecated: true
      description: |
        카카오 채널 친구에게 친구톡을 전송합니다.
        이미지, 버튼, 광고성 메시지 등 다양한 형태를 지원합니다.

        > **Deprecated — 친구톡은 카카오 정책에 따라 2025-12-31 종료되었습니다.**
        > 2026-01-01 부터 이 엔드포인트로 들어온 발송 요청은 카카오 측에서
        > **브랜드메시지(자유형)** 로 자동 대체 발송됩니다. 호출은 계속 성공하지만
        > 실제로 나가는 것은 브랜드메시지입니다.
        >
        > **엔드포인트는 제거되지 않습니다.** 자유 본문 타입(`FT`/`FI`/`FW`)을 개별
        > 수신자에게 보내는 경로는 현재 이 엔드포인트뿐입니다 —
        > `/{version}/brand-messages/send` 는 같은 조합에 대해 `NOT_A_BRAND_MESSAGE`
        > 를 반환합니다.
        >
        > 다음의 경우에는 `/{version}/brand-messages/send` 를 사용하세요.
        > - 템플릿 기반 리치 타입 (`FL`/`FC`/`FM`/`FP`/`FA`)
        > - 채널 친구가 **아닌** 수신자 (`targeting` = `N` / `I`)
        > - 수신 동의한 전체 채널 친구 동보 (`targeting` = `F`)
        >
        > 대체 발송 자체를 원하지 않으면 친구톡 발송을 시도하지 말고
        > 문자(`/{version}/messages/send`)로 요청하세요.
      operationId: sendFriendtalk
      parameters:
        - $ref: '#/components/parameters/ApiVersion'
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/FriendtalkRequest'
            examples:
              basicText:
                summary: 기본 텍스트 친구톡
                value:
                  at: null
                  scheduleType: "DIRECTLY"
                  messageType: "FT"
                  content: "안녕하세요! 봄맞이 30% 할인 이벤트가 시작되었습니다."
                  buttons: []
                  image: null
                  imageUrl: null
                  imageLink: null
                  adFlag: "Y"
                  wide: "N"
                  adult: "N"
                  header: null
                  replaceSms: "N"
                  contacts:
                    - contact: "01012345678"
                  kakaoSenderKey: "your_kakao_sender_key"
                  senderKey: "your_sms_sender_key"
              imageType:
                summary: 이미지 포함 친구톡
                value:
                  messageType: "FI"
                  content: "이번 주 특가 상품을 확인하세요!"
                  imageUrl: "https://example.com/event-banner.jpg"
                  imageLink: "https://example.com/event"
                  adFlag: "Y"
                  wide: "N"
                  adult: "N"
                  replaceSms: "N"
                  scheduleType: "DIRECTLY"
                  contacts:
                    - contact: "01012345678"
                  kakaoSenderKey: "your_kakao_sender_key"
                  senderKey: "your_sms_sender_key"
      responses:
        '200':
          $ref: '#/components/responses/SuccessResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/Forbidden'

  /{version}/brand-messages/send:
    post:
      tags: [브랜드메시지]
      summary: 카카오 브랜드메시지 전송
      description: |
        카카오 브랜드메시지를 전송합니다. 친구톡의 후속 채널로, 메시지 타입이
        친구톡과 1:1 대응됩니다 (`FT`→`BT`, `FI`→`BI`, `FW`→`BW`, `FL`→`BL`,
        `FC`→`BC`, `FM`→`BM`, `FP`→`BP`, `FA`→`BA`). 요청에는 친구톡 코드를
        그대로 넘기고, 변환은 서버가 처리합니다.

        `targeting` 에 따라 두 경로로 나뉩니다.
        - `M`/`N`/`I` → 단건 발송(`BRAND_BASIC`). `contacts` 필수, 응답에 발송 건수 포함.
        - `F` → 동보 발송(`BRAND_GROUP`). `contacts` 불필요, 접수 여부만 응답.

        `FT`/`FI`/`FW` 를 `M`/`N`/`I` 로 보내는 경우에는 `/{version}/friends/send`
        를 사용하세요. 이 엔드포인트는 `NOT_A_BRAND_MESSAGE` 를 반환합니다.
      operationId: sendBrandMessage
      parameters:
        - $ref: '#/components/parameters/ApiVersion'
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BrandMessageRequest'
            examples:
              basicToFriends:
                summary: 단건 발송 — 채널 친구 대상 (targeting=M)
                value:
                  kakaoSenderKey: "your_kakao_sender_key"
                  targeting: "M"
                  messageType: "FL"
                  friendTemplateUuid: "9cd5460b-6458-4edc-9b11-c26d3013c340"
                  adFlag: "Y"
                  replaceSms: "Y"
                  smsSubject: "[여름 특가]"
                  smsContent: "여름 한정 특가를 확인하세요."
                  senderKey: "your_sms_sender_key"
                  scheduleType: "DIRECTLY"
                  contacts:
                    - contact: "01012345678"
                      name: "홍길동"
                      var1: "29,000원"
              basicToNonFriends:
                summary: 단건 발송 — 채널 친구가 아닌 수신자 (targeting=N)
                value:
                  kakaoSenderKey: "your_kakao_sender_key"
                  targeting: "N"
                  messageType: "FM"
                  friendTemplateUuid: "9cd5460b-6458-4edc-9b11-c26d3013c340"
                  adFlag: "Y"
                  scheduleType: "DIRECTLY"
                  contacts:
                    - contact: "01012345678"
              groupBroadcast:
                summary: 동보 발송 — 수신 동의한 전체 채널 친구 (targeting=F)
                value:
                  kakaoSenderKey: "your_kakao_sender_key"
                  targeting: "F"
                  messageType: "FW"
                  friendTemplateUuid: "9cd5460b-6458-4edc-9b11-c26d3013c340"
                  adFlag: "Y"
                  scheduleType: "DIRECTLY"
      responses:
        '200':
          $ref: '#/components/responses/SuccessResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/Forbidden'

  /{version}/brand-messages:
    get:
      tags: [브랜드메시지]
      summary: 브랜드메시지 캠페인 목록 조회
      description: |
        브랜드메시지 캠페인(`BRAND_GROUP`, `BRAND_BASIC`)만 반환합니다.
        친구톡 캠페인은 `/{version}/friends` 를 사용하세요.
      operationId: listBrandMessages
      parameters:
        - $ref: '#/components/parameters/ApiVersion'
        - name: from
          in: query
          required: false
          description: 조회 시작일 (기본값 90일 전)
          schema: { type: string, format: date }
        - name: to
          in: query
          required: false
          description: 조회 종료일 (기본값 현재)
          schema: { type: string, format: date }
        - name: count
          in: query
          required: false
          description: 페이지당 개수
          schema: { type: integer, default: 30 }
      security:
        - BearerAuth: []
      responses:
        '200':
          $ref: '#/components/responses/SuccessResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /{version}/brand-messages/{campaign_id}:
    get:
      tags: [브랜드메시지]
      summary: 브랜드메시지 캠페인 상세 조회
      operationId: showBrandMessage
      parameters:
        - $ref: '#/components/parameters/ApiVersion'
        - name: campaign_id
          in: path
          required: true
          description: 발송 응답의 `campaignId` (캠페인 UUID)
          schema: { type: string, format: uuid }
      security:
        - BearerAuth: []
      responses:
        '200':
          $ref: '#/components/responses/SuccessResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

  /{version}/messages/send:
    post:
      tags: [문자]
      summary: SMS / LMS / MMS 전송
      description: |
        SMS(단문, 90자), LMS(장문, 2,000자), MMS(멀티미디어) 문자를 전송합니다.
        `messageType` 필드로 전송 유형을 구분합니다.
      operationId: sendMessage
      parameters:
        - $ref: '#/components/parameters/ApiVersion'
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SmsRequest'
            examples:
              sms:
                summary: SMS (단문)
                value:
                  campaignType: "MESSAGE"
                  messageType: "SMS"
                  scheduleType: "DIRECTLY"
                  at: null
                  subject: null
                  content: "인증번호: 123456"
                  files: []
                  contacts:
                    - contact: "01012345678"
                  senderKey: "your_sms_sender_key"
              lms:
                summary: LMS (장문)
                value:
                  campaignType: "MESSAGE"
                  messageType: "LMS"
                  scheduleType: "DIRECTLY"
                  at: null
                  subject: "[공지] 서비스 점검 안내"
                  content: "안녕하세요.\n\n서비스 점검이 예정되어 있습니다.\n일시: 2026-04-01 02:00 ~ 06:00"
                  files: []
                  contacts:
                    - contact: "01012345678"
                  senderKey: "your_sms_sender_key"
      responses:
        '200':
          $ref: '#/components/responses/SuccessResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/Forbidden'

  /{version}/short-urls:
    get:
      tags: [짧은URL]
      summary: 짧은 URL 목록 조회
      operationId: listShortUrls
      parameters:
        - $ref: '#/components/parameters/ApiVersion'
        - name: from
          in: query
          required: false
          description: 조회 시작일 (기본값 90일 전)
          schema: { type: string, format: date }
        - name: to
          in: query
          required: false
          description: 조회 종료일 (기본값 현재)
          schema: { type: string, format: date }
        - name: count
          in: query
          required: false
          description: 페이지당 개수
          schema: { type: integer, default: 30 }
      security:
        - BearerAuth: []
      responses:
        '200':
          $ref: '#/components/responses/SuccessResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
    post:
      tags: [짧은URL]
      summary: 짧은 URL 생성
      description: |
        같은 소유자가 같은 `targetUrl` 을 다시 줄이면 기존 링크를 그대로 반환합니다.
        캠페인별로 반응을 따로 집계하려면 `forceNew: true` 를 사용하세요.

        `http`/`https` 스킴만 허용됩니다.
      operationId: createShortUrl
      parameters:
        - $ref: '#/components/parameters/ApiVersion'
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ShortUrlRequest'
      responses:
        '201':
          $ref: '#/components/responses/SuccessResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '422':
          $ref: '#/components/responses/ValidationFailed'

  /{version}/short-urls/{code}:
    get:
      tags: [짧은URL]
      summary: 짧은 URL 상세 조회
      operationId: showShortUrl
      parameters:
        - $ref: '#/components/parameters/ApiVersion'
        - $ref: '#/components/parameters/ShortUrlCode'
      security:
        - BearerAuth: []
      responses:
        '200':
          $ref: '#/components/responses/SuccessResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
    delete:
      tags: [짧은URL]
      summary: 짧은 URL 리다이렉트 중지
      description: |
        링크를 삭제하지 않고 비활성화합니다. 이미 발송된 메시지의 링크는
        즉시 `410 Gone` 을 반환하게 되며, 누적된 통계는 그대로 남습니다.
      operationId: deactivateShortUrl
      parameters:
        - $ref: '#/components/parameters/ApiVersion'
        - $ref: '#/components/parameters/ShortUrlCode'
      security:
        - BearerAuth: []
      responses:
        '200':
          $ref: '#/components/responses/SuccessResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

  /{version}/short-urls/{code}/stats:
    get:
      tags: [짧은URL]
      summary: 짧은 URL 반응 통계 조회
      description: |
        일별 추이와 디바이스/유입경로/국가별 분해를 반환합니다.
        일별 추이는 사전 집계표에서 읽으므로 클릭 수가 많아도 응답 시간이 일정합니다.
      operationId: shortUrlStats
      parameters:
        - $ref: '#/components/parameters/ApiVersion'
        - $ref: '#/components/parameters/ShortUrlCode'
        - name: from
          in: query
          required: false
          description: 조회 시작일 (기본값 29일 전)
          schema: { type: string, format: date }
        - name: to
          in: query
          required: false
          description: 조회 종료일 (기본값 현재)
          schema: { type: string, format: date }
      security:
        - BearerAuth: []
      responses:
        '200':
          $ref: '#/components/responses/SuccessResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

components:
  securitySchemes:
    BasicAuth:
      type: http
      scheme: basic
      description: "`accessKey:secretKey`를 Base64로 인코딩 (토큰 발급 시 사용)"
    BearerAuth:
      type: http
      scheme: bearer
      description: |
        발급받은 토큰 사용
        - v1: `Bearer base64(token)`
        - v2: `Bearer token`

  parameters:
    ShortUrlCode:
      name: code
      in: path
      required: true
      description: 짧은 URL 코드
      schema: { type: string, maxLength: 32 }
      example: k7Rm2xQ
    ApiVersion:
      name: version
      in: path
      required: true
      description: API 버전
      schema:
        type: string
        enum: [v1, v2]
        default: v1
      example: v2

  schemas:
    ShortUrlRequest:
      type: object
      required: [targetUrl]
      properties:
        targetUrl:
          type: string
          format: uri
          maxLength: 2048
          description: 줄일 원본 URL. `http`/`https` 만 허용됩니다.
          example: https://example.com/promotions/summer-sale
        title:
          type: string
          maxLength: 255
          nullable: true
          description: 관리 화면에서 구분하기 위한 이름
          example: 여름 세일 랜딩
        expiresAt:
          type: string
          format: date-time
          nullable: true
          description: 이 시각 이후에는 리다이렉트하지 않고 `410 Gone` 을 반환합니다.
          example: '2026-09-30 23:59:59'
        forceNew:
          type: boolean
          default: false
          description: |
            true 면 같은 URL 이라도 새 코드를 만듭니다.
            캠페인별로 반응을 분리해 집계할 때 사용합니다.
    Contact:
      type: object
      required: [contact]
      description: 수신자 정보
      properties:
        contact:
          type: string
          description: 수신자 전화번호 (필수)
          example: "01012345678"
        name:
          type: string
          description: 수신자 이름 (선택)
          example: "홍길동"
        var1:
          type: string
          description: 템플릿 변수 #{var1}
          example: "ORD-20260101-001"
        var2:
          type: string
          description: 템플릿 변수 #{var2}
        var3:
          type: string
          description: 템플릿 변수 #{var3}
        var4:
          type: string
          description: 템플릿 변수 #{var4}
        var5:
          type: string
          description: 템플릿 변수 #{var5}
        var6:
          type: string
          description: 템플릿 변수 #{var6}
        var7:
          type: string
          description: 템플릿 변수 #{var7}
        var8:
          type: string
          description: 템플릿 변수 #{var8}

    AlimtalkRequest:
      type: object
      required: [templateCode, contacts, kakaoSenderKey, senderKey]
      description: 카카오 알림톡 전송 요청
      properties:
        at:
          type: string
          nullable: true
          description: "발송 예약 시각 (null = 즉시). 형식: Y-m-d H:i:s"
          example: "2026-04-01 09:00:00"
        scheduleType:
          type: string
          enum: [DIRECTLY, SCHEDULED]
          default: DIRECTLY
          description: "발송 유형 (DIRECTLY: 즉시, SCHEDULED: 예약)"
        templateCode:
          type: string
          description: Sendgo 콘솔에서 승인받은 알림톡 템플릿 코드 (필수)
          example: "ORDER_CONFIRM_001"
        replaceSms:
          type: string
          enum: ["Y", "N"]
          default: "N"
          description: "알림톡 실패 시 SMS 대체 발송 여부 (Y: 활성화)"
        smsSubject:
          type: string
          nullable: true
          description: 대체 SMS 제목 (replaceSms=Y 일 때 필수)
          example: "[배송 안내]"
        smsContent:
          type: string
          nullable: true
          description: 대체 SMS 내용 (replaceSms=Y 일 때 필수)
        contacts:
          type: array
          description: 수신자 목록 (최소 1명)
          items:
            $ref: '#/components/schemas/Contact'
        kakaoSenderKey:
          type: string
          description: 카카오 발신프로필 키
          example: "your_kakao_sender_key"
        senderKey:
          type: string
          description: SMS 발신자 키 (대체 발송 시 사용)
          example: "your_sms_sender_key"

    BrandMessageRequest:
      type: object
      required: [kakaoSenderKey, targeting, messageType, friendTemplateUuid]
      description: |
        카카오 브랜드메시지 전송 요청.

        친구톡 요청 필드를 모두 그대로 사용하며, 브랜드메시지 전용으로
        `targeting` 과 `friendTemplateUuid` 가 추가됩니다.
        `contacts` 는 `targeting` 이 `M`/`N`/`I` 일 때만 필요합니다.
      allOf:
        - $ref: '#/components/schemas/FriendtalkRequest'
        - type: object
          properties:
            targeting:
              type: string
              enum: [M, N, I, F]
              description: |
                발송 대상
                - `M`: 채널 친구
                - `N`: 채널 친구가 아닌 수신자
                - `I`: 친구 + 비친구 전체
                - `F`: 수신 동의한 전체 채널 친구 (동보, `contacts` 불필요)
              example: "M"
            friendTemplateUuid:
              type: string
              format: uuid
              description: 브랜드 템플릿 UUID (`templateUuid` 로도 허용)
              example: "9cd5460b-6458-4edc-9b11-c26d3013c340"
            friendGroupKey:
              type: string
              nullable: true
              description: 동보(`F`) 발송 대상 그룹 키
            pushAlarm:
              type: string
              enum: [Y, N]
              default: "Y"
              description: 푸시 알림 여부

    FriendtalkRequest:
      type: object
      required: [content, contacts, kakaoSenderKey, senderKey]
      description: >-
        카카오 친구톡 전송 요청. 친구톡은 2025-12-31 종료되어 카카오 측에서
        브랜드메시지(자유형)로 자동 대체 발송됩니다. 이 스키마 자체는
        `BrandMessageRequest` 의 베이스로도 계속 사용되므로 유지됩니다.
      properties:
        at:
          type: string
          nullable: true
          description: "발송 예약 시각 (null = 즉시)"
        scheduleType:
          type: string
          enum: [DIRECTLY, SCHEDULED]
          default: DIRECTLY
          description: 발송 유형
        messageType:
          type: string
          enum: [FT, FI, FW, FL, FM, FC, FA, FP]
          default: FT
          description: |
            메시지 유형
            - FT: 기본 텍스트
            - FI: 이미지형
            - FW: 와이드 이미지
            - FL: 리스트형
            - FM: 이미지+텍스트
            - FC: 캐러셀
            - FA: 아이템 리스트
            - FP: 프리미엄 동영상
        content:
          type: string
          description: 메시지 본문 (필수)
          example: "안녕하세요! 봄맞이 30% 할인 이벤트가 시작되었습니다."
        buttons:
          type: array
          description: 버튼 목록
          items:
            type: object
        image:
          nullable: true
          description: 이미지 파일 (imageUrl과 둘 중 하나)
        imageUrl:
          type: string
          nullable: true
          description: 이미지 URL (image와 둘 중 하나)
          example: "https://example.com/banner.jpg"
        imageLink:
          type: string
          nullable: true
          description: 이미지 클릭 시 이동할 URL
        adFlag:
          type: string
          enum: ["Y", "N"]
          default: "Y"
          description: "광고성 메시지 여부"
        wide:
          type: string
          enum: ["Y", "N"]
          default: "N"
          description: "와이드 이미지 사용 여부"
        adult:
          type: string
          enum: ["Y", "N"]
          default: "N"
          description: "성인 콘텐츠 여부"
        header:
          type: string
          nullable: true
          description: 헤더 텍스트
        replaceSms:
          type: string
          enum: ["Y", "N"]
          default: "N"
          description: SMS 대체 발송 여부
        smsSubject:
          type: string
          nullable: true
          description: 대체 SMS 제목
        smsContent:
          type: string
          nullable: true
          description: 대체 SMS 내용
        contacts:
          type: array
          description: 수신자 목록
          items:
            $ref: '#/components/schemas/Contact'
        kakaoSenderKey:
          type: string
          description: 카카오 발신프로필 키
        senderKey:
          type: string
          description: SMS 발신자 키

    SmsRequest:
      type: object
      required: [content, contacts, senderKey]
      description: SMS / LMS / MMS 전송 요청
      properties:
        campaignType:
          type: string
          enum: [MESSAGE, ADVERTISE, ELECTION]
          default: MESSAGE
          description: |
            캠페인 유형
            - MESSAGE: 일반 메시지
            - ADVERTISE: 광고성 메시지
            - ELECTION: 선거 관련
        messageType:
          type: string
          enum: [SMS, LMS, MMS]
          default: SMS
          description: |
            메시지 유형
            - SMS: 단문 (90자 이하)
            - LMS: 장문 (2,000자 이하)
            - MMS: 멀티미디어
        scheduleType:
          type: string
          enum: [DIRECTLY, SCHEDULED]
          default: DIRECTLY
          description: 발송 유형
        at:
          type: string
          nullable: true
          description: 발송 예약 시각 (null = 즉시)
        subject:
          type: string
          nullable: true
          description: 메시지 제목 (LMS/MMS에서 사용)
          example: "[공지사항]"
        content:
          type: string
          description: 메시지 본문 (필수)
          example: "인증번호: 123456"
        files:
          type: array
          description: 첨부 파일 목록 (MMS)
          items:
            type: object
        contacts:
          type: array
          description: 수신자 목록
          items:
            $ref: '#/components/schemas/Contact'
        senderKey:
          type: string
          description: SMS 발신자 키
          example: "your_sms_sender_key"

    TokenResponse:
      type: object
      properties:
        data:
          type: object
          properties:
            token:
              type: string
              description: API 액세스 토큰 (유효시간 24시간)
              example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."

    SuccessResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        data:
          type: object
          description: 전송 결과 데이터

    ErrorResponse:
      type: object
      description: API 오류 응답 (v2에서 에러 코드 세분화)
      properties:
        code:
          type: string
          description: 에러 코드
          example: "INVALID_TEMPLATE_CODE"
        message:
          type: string
          description: 에러 메시지
          example: "존재하지 않는 템플릿 코드입니다."

  responses:
    SuccessResponse:
      description: 전송 성공
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/SuccessResponse'

    ValidationFailed:
      description: 입력값 검증 실패
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            code: "VALIDATION_FAILED"
            message: "The given data was invalid."
            errors:
              targetUrl:
                - "The target url field must be a valid URL."

    BadRequest:
      description: 잘못된 요청 (파라미터 누락/형식 오류)
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          examples:
            emptyContacts:
              value:
                code: "EMPTY_CONTACTS"
                message: "수신자 정보가 비어있습니다."
            invalidTemplate:
              value:
                code: "INVALID_TEMPLATE_CODE"
                message: "존재하지 않는 템플릿 코드입니다."

    Unauthorized:
      description: 인증 실패
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          examples:
            invalidKey:
              value:
                code: "INVALID_ACCESS_KEY"
                message: "유효하지 않은 액세스 키입니다."

    Forbidden:
      description: 권한 없음
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          examples:
            notApproved:
              value:
                code: "ACCESS_KEY_NOT_APPROVED"
                message: "승인되지 않은 앱입니다."
            ipNotAllowed:
              value:
                code: "IP_NOT_ALLOWED"
                message: "허용되지 않은 IP입니다."

    PaymentRequired:
      description: 크레딧 부족
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            code: "PAYMENT_REQUIRED"
            message: "크레딧이 부족합니다."

    NotFound:
      description: 대상을 찾을 수 없음
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          examples:
            campaignNotFound:
              value:
                code: "NOT_FOUND"
                message: "캠페인을 찾을 수 없습니다."
            kakaoSenderNotFound:
              value:
                code: "INVALID_KAKAO_SENDER_KEY"
                message: "Incorrect kakaoSenderKey"
