인증·회원·개인 설정 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 |
예 | 공백 불가 |
{
"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 |
예 | 공백 불가 |
{
"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 |
응답 형태는 로그인과 같습니다.
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자 |
200: 변경 성공400: 요청 검증 실패 또는 현재 비밀번호 불일치401: 인증 오류404: 회원 또는 비밀번호 정보 없음
DELETE /v1/members/me¶
- 본문: 선택. 본문 전체를 생략할 수 있음
- 성공:
200 OK - 부수 효과: 회원을 soft delete하고 파기 예정 시각을 설정하며 탈퇴 이유를 기록합니다. 활성 refresh token은 모두 폐기됩니다.
| 필드 | 필수 | 제약 |
|---|---|---|
leaveReason |
아니요 | null 가능, 최대 255자 |
응답은 {"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: 미지원configCode404: 설정 없음,data.code=MEMBER_CONFIG_NOT_FOUND
PUT /v1/members/me/configs/{configCode}¶
- path
configCode: 필수, 현재TTS_VOICE - 본문
configValue: 필수, 공백 불가, 최대 32자 - 성공: 신규
201 Created, 기존 값 변경200 OK - 부수 효과: 회원+항목 1건을 멱등 upsert
응답은 단건 설정 조회와 같습니다.
400: 코드, 본문 또는 길이 오류404: 회원 없음 또는 프리셋 부재. 프리셋 부재는data.code=VOICE_PRESET_NOT_FOUND409: 비활성 프리셋(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 - 부수 효과: 설정 행을 물리 삭제하여 시스템 기본 동작으로 복귀. 설정이 없어도 성공하는 멱등 연산
400: 미지원configCode401: 인증 오류
음색 프리셋¶
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: 이 경로에 허용되지 않은 권한