콘텐츠로 이동

인증·회원·개인 설정 API

이 그룹은 공개 인증 3종, 로그인 회원의 셀프서비스, 개인 설정과 전역 음색 프리셋 조회를 다룹니다. 보호된 회원 경로는 HMAC 서명 access JWT가 필요하며 X-Workspace-Id는 사용하지 않습니다. refresh JWT는 재발급 본문에서만 사용합니다.

토큰의 claim과 회전 정책은 로그인과 토큰 인증, 공통 envelope는 스키마, 전체 경로 색인은 REST API 참조를 참고하십시오.

인증 API

POST /v1/auth/signup

  • 인증·테넌트: 공개, 워크스페이스 헤더 불필요
  • Content-Type: application/json
  • 성공: 201 Created
  • 부수 효과: NORMAL/EMAIL 회원, 비밀번호 해시와 ROLE_USER 권한 기반 프로필을 생성합니다.

요청 필드:

필드 필수 제약
email 예 공백 불가, 이메일 형식
password 예 공백 불가, 8..100자; 평문은 저장하지 않음
name 예 공백 불가
{
  "email": "user@example.com",
  "password": "<user-password>",
  "name": "홍길동"
}
{
  "status": 200,
  "message": "success",
  "data": {
    "email": "user@example.com",
    "name": "홍길동",
    "language": "ko",
    "timeZone": "Asia/Seoul",
    "level": "NORMAL",
    "type": "EMAIL",
    "roles": ["ROLE_USER"],
    "consent": {
      "isPrivacy": null,
      "isMarketing": null
    },
    "createdAt": "2026-09-23T10:00:00"
  }
}

상태 코드:

  • 201: 생성 성공. 응답 body의 공통 status는 200일 수 있습니다.
  • 400: 이메일 형식, 비밀번호 길이 또는 필수값 오류
  • 409: 활성 회원의 이메일 중복. 사전 조회와 동시 INSERT 경합 모두 충돌로 처리

POST /v1/auth/login

  • 인증·테넌트: 공개
  • 성공: 200 OK
  • 부수 효과: access JWT와 refresh JWT를 발급하고 refresh 원문의 SHA-256 해시를 저장합니다.
필드 필수 제약
username 예 공백 불가; 현재 로그인 식별자를 전달
password 예 공백 불가
{
  "username": "user@example.com",
  "password": "<user-password>"
}
{
  "status": 200,
  "message": "success",
  "data": {
    "tokenType": "Bearer",
    "accessToken": "<access-jwt>",
    "refreshToken": "<refresh-jwt>",
    "expiresIn": 1800
  }
}
  • 200: 토큰 쌍 발급
  • 400: 요청 필드 누락
  • 401: 자격증명 불일치

access JWT는 REST 보호 자원에 사용하고 refresh JWT는 아래 재발급에만 사용합니다. API key나 Ed25519 client-secret과 혼용하지 마십시오.

POST /v1/auth/refresh

  • 인증·테넌트: 공개. Authorization 헤더 대신 본문에 refresh JWT를 전달
  • 성공: 200 OK
  • 부수 효과: 사용한 refresh token을 폐기하고 새 access/refresh 쌍과 새 해시를 저장합니다.
필드 필수 제약
refreshToken 예 공백 불가, 유효한 type=refresh HMAC JWT
{
  "refreshToken": "<refresh-jwt>"
}

응답 형태는 로그인과 같습니다.

  • 200: 회전된 새 토큰 쌍
  • 400: 필드 누락
  • 401: 서명·issuer·type·만료 오류, 이미 폐기됐거나 저장 해시와 맞지 않는 토큰

회원 셀프서비스

아래 경로는 모두 Authorization: Bearer <access-jwt>가 필요합니다. 대상 회원은 JWT sub의 UUID로 고정되며 URL이나 본문으로 다른 회원 ID를 지정할 수 없습니다.

GET /v1/members/me

  • 요청 필드·본문: 없음
  • 성공: 200 OK
  • 부수 효과: 없음
{
  "status": 200,
  "message": "success",
  "data": {
    "email": "user@example.com",
    "name": "홍길동",
    "phone": "010-0000-0000",
    "imagePath": null,
    "language": "ko",
    "timeZone": "Asia/Seoul",
    "level": "NORMAL",
    "type": "EMAIL",
    "roles": ["ROLE_USER"],
    "company": {
      "companyName": "예시 기관",
      "industryCode": "H1",
      "industryOther": null,
      "companySizeCode": "M1",
      "companySizeOther": null,
      "jobPositionCode": null,
      "jobPositionOther": null,
      "jobDutiesCode": null,
      "jobDutiesOther": null
    },
    "consent": {
      "isPrivacy": true,
      "isMarketing": false
    },
    "createdAt": "2026-09-23T10:00:00"
  }
}
  • 200: 프로필 반환
  • 401: access JWT 누락·오류
  • 404: JWT subject에 해당하는 활성 회원 없음

company는 미등록이면 응답에서 생략될 수 있고, 동의 이력이 없으면 각 동의 값은 null입니다.

PUT /v1/members/me

  • 성공: 200 OK
  • 부수 효과: 회원 프로필을 변경하고, company가 있으면 회사 정보를 upsert하며, consent의 값이 기존 최신 값과 달라진 항목만 동의 이력을 append합니다.

최상위 필드:

필드 필수 제약·의미
name 예 공백 불가
phone 아니요 null이면 변경 없음
imagePath 아니요 null이면 변경 없음
language 예 공백 불가
timeZone 예 공백 불가
company 아니요 null이면 회사 섹션 변경 없음
consent 아니요 null이면 동의 변경 없음

company가 존재할 때 companyName, industryCode, companySizeCode는 필수입니다. 회사명은 최대 128자, industryCode와 companySizeCode는 2자리 영숫자입니다. jobPositionCode, jobDutiesCode는 선택이지만 값이 있으면 2자리 영숫자여야 합니다. 각 *Other는 최대 64자입니다. 이 커밋은 코드의 실제 공통코드 존재 여부까지 확인하지 않고 형식만 검증합니다.

consent.isPrivacy, consent.isMarketing은 각각 선택 Boolean이며 null은 해당 항목을 변경하지 않는다는 뜻입니다.

{
  "name": "홍길동",
  "phone": "010-0000-0000",
  "imagePath": null,
  "language": "ko",
  "timeZone": "Asia/Seoul",
  "company": {
    "companyName": "예시 기관",
    "industryCode": "H1",
    "industryOther": null,
    "companySizeCode": "M1",
    "companySizeOther": null,
    "jobPositionCode": null,
    "jobPositionOther": null,
    "jobDutiesCode": null,
    "jobDutiesOther": null
  },
  "consent": {
    "isPrivacy": true,
    "isMarketing": false
  }
}

응답은 GET /v1/members/me와 같은 최신 프로필입니다.

  • 200: 수정 성공
  • 400: 필수값, 회사 코드 형식 또는 길이 오류
  • 401: 인증 오류
  • 404: 회원 없음
  • 409: 연락처가 다른 활성 회원과 중복

PATCH /v1/members/me/password

  • 성공: 200 OK
  • 부수 효과: 현재 비밀번호를 확인한 뒤 새 비밀번호를 Argon2id로 해시해 교체
필드 필수 제약
currentPassword 예 공백 불가
newPassword 예 공백 불가, 8..100자
{
  "currentPassword": "<current-password>",
  "newPassword": "<new-password>"
}
{
  "status": 200,
  "message": "success"
}
  • 200: 변경 성공
  • 400: 요청 검증 실패 또는 현재 비밀번호 불일치
  • 401: 인증 오류
  • 404: 회원 또는 비밀번호 정보 없음

DELETE /v1/members/me

  • 본문: 선택. 본문 전체를 생략할 수 있음
  • 성공: 200 OK
  • 부수 효과: 회원을 soft delete하고 파기 예정 시각을 설정하며 탈퇴 이유를 기록합니다. 활성 refresh token은 모두 폐기됩니다.
필드 필수 제약
leaveReason 아니요 null 가능, 최대 255자
{
  "leaveReason": "서비스 이용 종료"
}

응답은 {"status":200,"message":"success"}입니다.

  • 200: 탈퇴 처리 성공
  • 400: 탈퇴 이유 길이 초과
  • 401: 인증 오류
  • 404: 활성 회원 없음

이미 발급된 access JWT를 즉시 폐기하는 deny-list는 없습니다. 탈퇴 후 refresh는 차단되지만 기존 access JWT는 만료 전까지 암호학적으로 유효할 수 있습니다. 운영 영향은 보안과 현재 격차를 참고하십시오.

회원 개인 설정

모든 설정 경로는 access JWT 전용이고 워크스페이스 헤더가 필요 없습니다. 현재 지원하는 configCode는 TTS_VOICE 하나입니다.

GET /v1/members/me/configs

  • 요청: 없음
  • 성공: 200 OK; 설정이 없으면 count: 0, 빈 list
  • 정렬: configCode 오름차순
  • 부수 효과: 없음
{
  "status": 200,
  "message": "success",
  "count": 1,
  "list": [
    {
      "configCode": "TTS_VOICE",
      "configValue": "female_warm",
      "createdAt": "2026-09-23T10:00:00",
      "updatedAt": "2026-09-23T10:00:00"
    }
  ]
}

GET /v1/members/me/configs/{configCode}

  • path configCode: 필수, 화이트리스트 값 TTS_VOICE
  • 성공: 200 OK
  • 부수 효과: 없음
{
  "status": 200,
  "message": "success",
  "data": {
    "configCode": "TTS_VOICE",
    "configValue": "female_warm",
    "createdAt": "2026-09-23T10:00:00",
    "updatedAt": "2026-09-23T10:00:00"
  }
}
  • 400: 미지원 configCode
  • 404: 설정 없음, data.code=MEMBER_CONFIG_NOT_FOUND

PUT /v1/members/me/configs/{configCode}

  • path configCode: 필수, 현재 TTS_VOICE
  • 본문 configValue: 필수, 공백 불가, 최대 32자
  • 성공: 신규 201 Created, 기존 값 변경 200 OK
  • 부수 효과: 회원+항목 1건을 멱등 upsert
{
  "configValue": "female_warm"
}

응답은 단건 설정 조회와 같습니다.

  • 400: 코드, 본문 또는 길이 오류
  • 404: 회원 없음 또는 프리셋 부재. 프리셋 부재는 data.code=VOICE_PRESET_NOT_FOUND
  • 409: 비활성 프리셋(VOICE_PRESET_INACTIVE) 또는 동시 INSERT 경합 (MEMBER_CONFIG_CONFLICT)

configValue는 물리 FK가 아닌 논리 참조이므로 이 API가 voice_preset.preset_code의 존재와 활성 상태를 검사합니다.

DELETE /v1/members/me/configs/{configCode}

  • path configCode: 필수, 현재 TTS_VOICE
  • 성공: 항상 200 OK
  • 부수 효과: 설정 행을 물리 삭제하여 시스템 기본 동작으로 복귀. 설정이 없어도 성공하는 멱등 연산
{
  "status": 200,
  "message": "success"
}
  • 400: 미지원 configCode
  • 401: 인증 오류

음색 프리셋

GET /v1/voice-presets

  • 인증: access JWT 또는 워크스페이스 API key
  • 테넌트: 전역 마스터이므로 X-Workspace-Id 불필요
  • 요청 query·본문: 없음
  • 성공: 200 OK
  • 부수 효과: 없음
{
  "status": 200,
  "message": "success",
  "count": 1,
  "list": [
    {
      "presetCode": "female_warm",
      "presetName": "메인 여성",
      "gender": "F",
      "ageGroup": "adult"
    }
  ]
}

활성 프리셋만 sort_order, presetCode 순으로 반환합니다. 엔진 내부 필드인 ttsProvider, ttsModel, voiceParams는 응답하지 않습니다.

  • 200: 목록 또는 빈 목록
  • 401: credential 없음·오류
  • 403: 이 경로에 허용되지 않은 권한