openapi: 3.0.3
info:
  title: User Service API
  description: |
    KT AI 기반 소상공인 이벤트 자동 생성 서비스 - User Service API

    사용자 인증 및 매장정보 관리를 담당하는 마이크로서비스

    **주요 기능:**
    - 회원가입
    - 로그인/로그아웃
    - 프로필 조회 및 수정
    - 비밀번호 변경

    **보안:**
    - JWT Bearer 토큰 기반 인증
    - bcrypt 비밀번호 해싱
  version: 1.0.0
  contact:
    name: Digital Garage Team
    email: support@kt-event-marketing.com

servers:
  - url: http://localhost:8081
    description: Local Development Server
  - url: https://dev-api.kt-event-marketing.com/user/v1
    description: Development Server
  - url: https://api.kt-event-marketing.com/user/v1
    description: Production Server

tags:
  - name: Authentication
    description: 인증 관련 API (로그인, 로그아웃, 회원가입)
  - name: Profile
    description: 프로필 관련 API (조회, 수정, 비밀번호 변경)

paths:
  /users/register:
    post:
      tags:
        - Authentication
      summary: 회원가입
      description: |
        소상공인 회원가입 API

        **유저스토리:** UFR-USER-010

        **주요 기능:**
        - 기본 정보 및 매장 정보 등록
        - 비밀번호 bcrypt 해싱
        - JWT 토큰 자동 발급

        **처리 흐름:**
        1. 중복 사용자 확인 (이메일/전화번호 기반)
        2. 비밀번호 해싱 (bcrypt)
        3. User/Store 데이터베이스 트랜잭션 처리
        4. JWT 토큰 생성 및 세션 저장 (Redis)
      operationId: registerUser
      x-user-story: UFR-USER-010
      x-controller: UserController
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RegisterRequest'
            examples:
              restaurant:
                summary: 음식점 회원가입 예시
                value:
                  name: 홍길동
                  phoneNumber: "01012345678"
                  email: hong@example.com
                  password: "Password123!"
                  storeName: 맛있는집
                  industry: 음식점
                  address: 서울시 강남구 테헤란로 123
                  businessHours: "월-금 11:00-22:00, 토-일 12:00-21:00"
              cafe:
                summary: 카페 회원가입 예시
                value:
                  name: 김철수
                  phoneNumber: "01087654321"
                  email: kim@example.com
                  password: "SecurePass456!"
                  storeName: 아메리카노 카페
                  industry: 카페
                  address: 서울시 서초구 서초대로 456
                  businessHours: "매일 09:00-20:00"
      responses:
        '201':
          description: 회원가입 성공
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RegisterResponse'
              examples:
                success:
                  summary: 회원가입 성공 응답
                  value:
                    token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEyMywicm9sZSI6Ik9XTkVSIiwiaWF0IjoxNjE2MjM5MDIyLCJleHAiOjE2MTY4NDM4MjJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
                    userId: 123
                    userName: 홍길동
                    storeId: 456
                    storeName: 맛있는집
        '400':
          description: 잘못된 요청
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                duplicateUser:
                  summary: 중복 사용자
                  value:
                    code: USER_001
                    message: 이미 가입된 이메일입니다
                    timestamp: 2025-10-22T10:30:00Z
                validationError:
                  summary: 입력 검증 오류
                  value:
                    code: VALIDATION_ERROR
                    message: 비밀번호는 8자 이상이어야 합니다
                    timestamp: 2025-10-22T10:30:00Z
        '500':
          description: 서버 오류
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /users/login:
    post:
      tags:
        - Authentication
      summary: 로그인
      description: |
        소상공인 로그인 API

        **유저스토리:** UFR-USER-020

        **주요 기능:**
        - 이메일/비밀번호 인증
        - JWT 토큰 발급
        - Redis 세션 저장
        - 최종 로그인 시각 업데이트 (비동기)

        **보안:**
        - Timing Attack 방어 (에러 메시지 통일)
        - bcrypt 비밀번호 검증
        - JWT 토큰 7일 만료
      operationId: loginUser
      x-user-story: UFR-USER-020
      x-controller: UserController
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LoginRequest'
            examples:
              default:
                summary: 로그인 요청 예시
                value:
                  email: hong@example.com
                  password: "Password123!"
      responses:
        '200':
          description: 로그인 성공
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LoginResponse'
              examples:
                success:
                  summary: 로그인 성공 응답
                  value:
                    token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEyMywicm9sZSI6Ik9XTkVSIiwiaWF0IjoxNjE2MjM5MDIyLCJleHAiOjE2MTY4NDM4MjJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
                    userId: 123
                    userName: 홍길동
                    role: OWNER
                    email: hong@example.com
        '401':
          description: 인증 실패
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                authFailed:
                  summary: 인증 실패
                  value:
                    code: AUTH_001
                    message: 이메일 또는 비밀번호를 확인해주세요
                    timestamp: 2025-10-22T10:30:00Z

  /users/logout:
    post:
      tags:
        - Authentication
      summary: 로그아웃
      description: |
        로그아웃 API

        **유저스토리:** UFR-USER-040

        **주요 기능:**
        - Redis 세션 삭제
        - JWT 토큰 Blacklist 추가
        - 멱등성 보장

        **처리 흐름:**
        1. JWT 토큰 검증
        2. Redis 세션 삭제
        3. JWT Blacklist 추가 (남은 만료 시간만큼 TTL 설정)
        4. 로그아웃 이벤트 발행
      operationId: logoutUser
      x-user-story: UFR-USER-040
      x-controller: UserController
      security:
        - BearerAuth: []
      responses:
        '200':
          description: 로그아웃 성공
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LogoutResponse'
              examples:
                success:
                  summary: 로그아웃 성공 응답
                  value:
                    success: true
                    message: 안전하게 로그아웃되었습니다
        '401':
          description: 인증 실패 (유효하지 않은 토큰)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                invalidToken:
                  summary: 유효하지 않은 토큰
                  value:
                    code: AUTH_002
                    message: 유효하지 않은 토큰입니다
                    timestamp: 2025-10-22T10:30:00Z

  /users/profile:
    get:
      tags:
        - Profile
      summary: 프로필 조회
      description: |
        사용자 프로필 조회 API

        **유저스토리:** UFR-USER-030

        **조회 정보:**
        - 기본 정보 (이름, 전화번호, 이메일)
        - 매장 정보 (매장명, 업종, 주소, 영업시간)
      operationId: getProfile
      x-user-story: UFR-USER-030
      x-controller: UserController
      security:
        - BearerAuth: []
      responses:
        '200':
          description: 프로필 조회 성공
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProfileResponse'
              examples:
                success:
                  summary: 프로필 조회 성공 응답
                  value:
                    userId: 123
                    userName: 홍길동
                    phoneNumber: "01012345678"
                    email: hong@example.com
                    role: OWNER
                    storeId: 456
                    storeName: 맛있는집
                    industry: 음식점
                    address: 서울시 강남구 테헤란로 123
                    businessHours: "월-금 11:00-22:00, 토-일 12:00-21:00"
                    createdAt: 2025-09-01T10:00:00Z
                    lastLoginAt: 2025-10-22T09:00:00Z
        '401':
          description: 인증 실패
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: 사용자를 찾을 수 없음
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                notFound:
                  summary: 사용자 없음
                  value:
                    code: USER_003
                    message: 사용자를 찾을 수 없습니다
                    timestamp: 2025-10-22T10:30:00Z

    put:
      tags:
        - Profile
      summary: 프로필 수정
      description: |
        사용자 프로필 수정 API

        **유저스토리:** UFR-USER-030

        **수정 가능 항목:**
        - 기본 정보: 이름, 전화번호, 이메일
        - 매장 정보: 매장명, 업종, 주소, 영업시간

        **주의사항:**
        - 비밀번호 변경은 별도 API 사용 (/users/password)
        - 전화번호 변경 시 향후 재인증 필요 (현재는 직접 변경 가능)
        - Optimistic Locking으로 동시성 제어
      operationId: updateProfile
      x-user-story: UFR-USER-030
      x-controller: UserController
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateProfileRequest'
            examples:
              fullUpdate:
                summary: 전체 정보 수정
                value:
                  name: 홍길동
                  phoneNumber: "01012345678"
                  email: hong.new@example.com
                  storeName: 맛있는집 (리뉴얼)
                  industry: 퓨전음식점
                  address: 서울시 강남구 테헤란로 456
                  businessHours: "매일 11:00-23:00"
              partialUpdate:
                summary: 일부 정보 수정 (이메일, 영업시간)
                value:
                  email: hong.updated@example.com
                  businessHours: "월-금 10:00-22:00, 토-일 휴무"
      responses:
        '200':
          description: 프로필 수정 성공
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UpdateProfileResponse'
              examples:
                success:
                  summary: 프로필 수정 성공 응답
                  value:
                    userId: 123
                    userName: 홍길동
                    email: hong.new@example.com
                    storeId: 456
                    storeName: 맛있는집 (리뉴얼)
        '400':
          description: 잘못된 요청
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: 인증 실패
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: 사용자를 찾을 수 없음
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: 동시성 충돌 (다른 세션에서 수정)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                conflict:
                  summary: 동시성 충돌
                  value:
                    code: USER_005
                    message: 다른 세션에서 프로필을 수정했습니다. 새로고침 후 다시 시도하세요
                    timestamp: 2025-10-22T10:30:00Z

  /users/password:
    put:
      tags:
        - Profile
      summary: 비밀번호 변경
      description: |
        비밀번호 변경 API

        **유저스토리:** UFR-USER-030

        **주요 기능:**
        - 현재 비밀번호 확인 필수
        - 새 비밀번호 규칙 검증 (8자 이상, 영문/숫자/특수문자 포함)
        - bcrypt 해싱

        **보안:**
        - 현재 비밀번호 검증 실패 시 400 Bad Request
        - 비밀번호 변경 후 기존 세션 유지 (로그아웃 불필요)
      operationId: changePassword
      x-user-story: UFR-USER-030
      x-controller: UserController
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ChangePasswordRequest'
            examples:
              default:
                summary: 비밀번호 변경 요청
                value:
                  currentPassword: "Password123!"
                  newPassword: "NewSecurePass456!"
      responses:
        '200':
          description: 비밀번호 변경 성공
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChangePasswordResponse'
              examples:
                success:
                  summary: 비밀번호 변경 성공 응답
                  value:
                    success: true
                    message: 비밀번호가 성공적으로 변경되었습니다
        '400':
          description: 현재 비밀번호 불일치 또는 새 비밀번호 규칙 위반
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                invalidCurrentPassword:
                  summary: 현재 비밀번호 불일치
                  value:
                    code: USER_004
                    message: 현재 비밀번호가 일치하지 않습니다
                    timestamp: 2025-10-22T10:30:00Z
                invalidNewPassword:
                  summary: 새 비밀번호 규칙 위반
                  value:
                    code: VALIDATION_ERROR
                    message: 비밀번호는 8자 이상이어야 하며 영문/숫자/특수문자를 포함해야 합니다
                    timestamp: 2025-10-22T10:30:00Z
        '401':
          description: 인증 실패
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /users/{userId}/store:
    get:
      tags:
        - Profile
      summary: 매장정보 조회 (서비스 연동용)
      description: |
        특정 사용자의 매장정보를 조회하는 API (내부 서비스 연동용)

        **사용 목적:**
        - Event Service에서 이벤트 생성 시 매장정보 조회
        - Content Service에서 매장정보 기반 콘텐츠 생성
        - Service-to-Service 통신용 내부 API

        **주의사항:**
        - Internal API로 외부 노출 금지
        - API Gateway에서 인증된 서비스만 접근 허용
        - 매장정보는 Redis 캐시 우선 조회 (TTL 30분)
      operationId: getStoreByUserId
      x-user-story: Service Integration
      x-controller: UserController
      security:
        - BearerAuth: []
      parameters:
        - name: userId
          in: path
          required: true
          description: 사용자 ID
          schema:
            type: integer
            format: int64
          example: 123
      responses:
        '200':
          description: 매장정보 조회 성공
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StoreDetailResponse'
              examples:
                success:
                  summary: 매장정보 조회 성공 응답
                  value:
                    userId: 123
                    storeId: 456
                    storeName: 맛있는집
                    industry: 음식점
                    address: 서울시 강남구 테헤란로 123
                    businessHours: "월-금 11:00-22:00, 토-일 12:00-21:00"
        '401':
          description: 인증 실패
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                unauthorized:
                  summary: 인증 실패
                  value:
                    code: AUTH_002
                    message: 유효하지 않은 토큰입니다
                    timestamp: 2025-10-22T10:30:00Z
        '403':
          description: 권한 없음 (내부 서비스만 접근 가능)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                forbidden:
                  summary: 권한 없음
                  value:
                    code: AUTH_003
                    message: 이 API는 내부 서비스만 접근 가능합니다
                    timestamp: 2025-10-22T10:30:00Z
        '404':
          description: 사용자 또는 매장을 찾을 수 없음
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                notFound:
                  summary: 사용자 또는 매장 없음
                  value:
                    code: USER_003
                    message: 사용자 또는 매장을 찾을 수 없습니다
                    timestamp: 2025-10-22T10:30:00Z
        '500':
          description: 서버 오류
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: |
        JWT Bearer 토큰 인증

        **형식:** Authorization: Bearer {JWT_TOKEN}

        **토큰 만료:** 7일

        **Claims:**
        - userId: 사용자 ID
        - role: 사용자 역할 (OWNER)
        - iat: 발급 시각
        - exp: 만료 시각

  schemas:
    RegisterRequest:
      type: object
      required:
        - name
        - phoneNumber
        - email
        - password
        - storeName
        - industry
        - address
      properties:
        name:
          type: string
          minLength: 2
          maxLength: 50
          description: 사용자 이름 (2자 이상, 한글/영문)
          example: 홍길동
        phoneNumber:
          type: string
          pattern: '^010\d{8}$'
          description: 휴대폰 번호 (010XXXXXXXX)
          example: "01012345678"
        email:
          type: string
          format: email
          maxLength: 100
          description: 이메일 주소
          example: hong@example.com
        password:
          type: string
          minLength: 8
          maxLength: 100
          description: 비밀번호 (8자 이상, 영문/숫자/특수문자 포함)
          example: "Password123!"
        storeName:
          type: string
          minLength: 2
          maxLength: 100
          description: 매장명
          example: 맛있는집
        industry:
          type: string
          maxLength: 50
          description: 업종 (예 음식점, 카페, 소매점 등)
          example: 음식점
        address:
          type: string
          minLength: 5
          maxLength: 200
          description: 매장 주소
          example: 서울시 강남구 테헤란로 123
        businessHours:
          type: string
          maxLength: 200
          description: 영업시간 (선택 사항)
          example: "월-금 11:00-22:00, 토-일 12:00-21:00"

    RegisterResponse:
      type: object
      required:
        - token
        - userId
        - userName
        - storeId
        - storeName
      properties:
        token:
          type: string
          description: JWT 토큰 (7일 만료)
          example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEyMywicm9sZSI6Ik9XTkVSIiwiaWF0IjoxNjE2MjM5MDIyLCJleHAiOjE2MTY4NDM4MjJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
        userId:
          type: integer
          format: int64
          description: 사용자 ID
          example: 123
        userName:
          type: string
          description: 사용자 이름
          example: 홍길동
        storeId:
          type: integer
          format: int64
          description: 매장 ID
          example: 456
        storeName:
          type: string
          description: 매장명
          example: 맛있는집

    LoginRequest:
      type: object
      required:
        - email
        - password
      properties:
        email:
          type: string
          format: email
          maxLength: 100
          description: 이메일 주소
          example: hong@example.com
        password:
          type: string
          minLength: 8
          description: 비밀번호
          example: "Password123!"

    LoginResponse:
      type: object
      required:
        - token
        - userId
        - userName
        - role
        - email
      properties:
        token:
          type: string
          description: JWT 토큰 (7일 만료)
          example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEyMywicm9sZSI6Ik9XTkVSIiwiaWF0IjoxNjE2MjM5MDIyLCJleHAiOjE2MTY4NDM4MjJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
        userId:
          type: integer
          format: int64
          description: 사용자 ID
          example: 123
        userName:
          type: string
          description: 사용자 이름
          example: 홍길동
        role:
          type: string
          enum: [OWNER, ADMIN]
          description: 사용자 역할
          example: OWNER
        email:
          type: string
          format: email
          description: 이메일 주소
          example: hong@example.com

    LogoutResponse:
      type: object
      required:
        - success
        - message
      properties:
        success:
          type: boolean
          description: 로그아웃 성공 여부
          example: true
        message:
          type: string
          description: 응답 메시지
          example: 안전하게 로그아웃되었습니다

    ProfileResponse:
      type: object
      required:
        - userId
        - userName
        - phoneNumber
        - email
        - role
        - storeId
        - storeName
        - industry
        - address
      properties:
        userId:
          type: integer
          format: int64
          description: 사용자 ID
          example: 123
        userName:
          type: string
          description: 사용자 이름
          example: 홍길동
        phoneNumber:
          type: string
          description: 휴대폰 번호
          example: "01012345678"
        email:
          type: string
          format: email
          description: 이메일 주소
          example: hong@example.com
        role:
          type: string
          enum: [OWNER, ADMIN]
          description: 사용자 역할
          example: OWNER
        storeId:
          type: integer
          format: int64
          description: 매장 ID
          example: 456
        storeName:
          type: string
          description: 매장명
          example: 맛있는집
        industry:
          type: string
          description: 업종
          example: 음식점
        address:
          type: string
          description: 매장 주소
          example: 서울시 강남구 테헤란로 123
        businessHours:
          type: string
          description: 영업시간
          example: "월-금 11:00-22:00, 토-일 12:00-21:00"
        createdAt:
          type: string
          format: date-time
          description: 가입 일시
          example: 2025-09-01T10:00:00Z
        lastLoginAt:
          type: string
          format: date-time
          description: 최종 로그인 일시
          example: 2025-10-22T09:00:00Z

    UpdateProfileRequest:
      type: object
      properties:
        name:
          type: string
          minLength: 2
          maxLength: 50
          description: 사용자 이름 (선택 사항)
          example: 홍길동
        phoneNumber:
          type: string
          pattern: '^010\d{8}$'
          description: 휴대폰 번호 (선택 사항, 향후 재인증 필요)
          example: "01012345678"
        email:
          type: string
          format: email
          maxLength: 100
          description: 이메일 주소 (선택 사항)
          example: hong.new@example.com
        storeName:
          type: string
          minLength: 2
          maxLength: 100
          description: 매장명 (선택 사항)
          example: 맛있는집 (리뉴얼)
        industry:
          type: string
          maxLength: 50
          description: 업종 (선택 사항)
          example: 퓨전음식점
        address:
          type: string
          minLength: 5
          maxLength: 200
          description: 매장 주소 (선택 사항)
          example: 서울시 강남구 테헤란로 456
        businessHours:
          type: string
          maxLength: 200
          description: 영업시간 (선택 사항)
          example: "매일 11:00-23:00"

    UpdateProfileResponse:
      type: object
      required:
        - userId
        - userName
        - email
        - storeId
        - storeName
      properties:
        userId:
          type: integer
          format: int64
          description: 사용자 ID
          example: 123
        userName:
          type: string
          description: 사용자 이름
          example: 홍길동
        email:
          type: string
          format: email
          description: 이메일 주소
          example: hong.new@example.com
        storeId:
          type: integer
          format: int64
          description: 매장 ID
          example: 456
        storeName:
          type: string
          description: 매장명
          example: 맛있는집 (리뉴얼)

    ChangePasswordRequest:
      type: object
      required:
        - currentPassword
        - newPassword
      properties:
        currentPassword:
          type: string
          minLength: 8
          description: 현재 비밀번호
          example: "Password123!"
        newPassword:
          type: string
          minLength: 8
          maxLength: 100
          description: 새 비밀번호 (8자 이상, 영문/숫자/특수문자 포함)
          example: "NewSecurePass456!"

    ChangePasswordResponse:
      type: object
      required:
        - success
        - message
      properties:
        success:
          type: boolean
          description: 비밀번호 변경 성공 여부
          example: true
        message:
          type: string
          description: 응답 메시지
          example: 비밀번호가 성공적으로 변경되었습니다

    StoreDetailResponse:
      type: object
      required:
        - userId
        - storeId
        - storeName
        - industry
        - address
      properties:
        userId:
          type: integer
          format: int64
          description: 사용자 ID
          example: 123
        storeId:
          type: integer
          format: int64
          description: 매장 ID
          example: 456
        storeName:
          type: string
          description: 매장명
          example: 맛있는집
        industry:
          type: string
          description: 업종
          example: 음식점
        address:
          type: string
          description: 매장 주소
          example: 서울시 강남구 테헤란로 123
        businessHours:
          type: string
          description: 영업시간
          example: "월-금 11:00-22:00, 토-일 12:00-21:00"

    ErrorResponse:
      type: object
      required:
        - code
        - message
        - timestamp
      properties:
        code:
          type: string
          description: 에러 코드
          example: USER_001
          enum:
            - USER_001  # 중복 사용자
            - USER_003  # 사용자 없음
            - USER_004  # 현재 비밀번호 불일치
            - USER_005  # 동시성 충돌
            - AUTH_001  # 인증 실패
            - AUTH_002  # 유효하지 않은 토큰
            - AUTH_003  # 권한 없음 (내부 서비스만 접근)
            - VALIDATION_ERROR  # 입력 검증 오류
        message:
          type: string
          description: 에러 메시지
          example: 이미 가입된 이메일입니다
        timestamp:
          type: string
          format: date-time
          description: 에러 발생 시각
          example: 2025-10-22T10:30:00Z
        details:
          type: array
          description: 상세 에러 정보 (선택 사항)
          items:
            type: string
          example: ["필드명: 필수 항목입니다"]
