v1.3.0 릴리즈 노트
릴리즈 날짜: 2026년 4월 타입: Minor Release (Breaking Changes 포함) 호환성: ⚠️ Breaking — 약관 조회 응답 스키마 변경, 클라이언트 마이그레이션 필요
📋 요약
약관(agreements) 조회 및 동의/철회 엔드포인트 전체를 단일 UserAgreementDto 구조로 통일하고, 레거시 중복 엔드포인트 4개를 deprecated 처리했습니다.
핵심 목표:
- 클라이언트가 약관 본문을 얻기 위해 여러 엔드포인트를 호출하던 구조 제거
- 엔드포인트별로 다른 DTO 타입을 쓰던 비일관성 제거 (조회와 쓰기 API 모두)
- 중복 책임(약관 조회/동의) 엔드포인트 정리
- 동의/철회 API의 URL 대칭 (
POST/DELETE /v1/users/agreements/:agreementId/consent)
🗂 변경 요약 매트릭스
조회 API (Read)
| 엔드포인트 | 변경 | 브레이킹 |
|---|---|---|
GET /v1/auth/agreements | Swagger deprecated 태그 (기능 유지) | ❌ |
POST /v1/auth/login | 응답 agreements 필드 → 항상 [] | ✅ |
GET /v1/mobile/app-settings | agreements 필드 → UserAgreementDto[] (본문 포함) | ✅ |
GET /v1/users/agreements | 응답 → UserAgreementsResponseDto | ✅ |
GET /v1/users/agreements/history | 응답 → UserAgreementHistoryResponseDto | ✅ |
쓰기 API (Write)
| 엔드포인트 | 변경 | 브레이킹 |
|---|---|---|
POST /v1/users/agreements/:agreementId/consent | 신규 (동의 기록) | - |
DELETE /v1/users/agreements/:agreementId/consent | 응답 → UserAgreementDto | ✅ |
POST /v1/agreements/versions/:versionId/agree | Swagger deprecated 태그 | ❌ |
GET /v1/agreements/versions/:versionId/has-agreed | Swagger deprecated 태그 | ❌ |
🆕 신규 DTO: UserAgreementDto
모든 agreement 조회 응답의 아이템에 사용되는 통일된 타입.
interface UserAgreementDto {
agreementId: string; // 약관 ID
versionId: string; // 약관 버전 ID
type: AgreementsType; // TERMS | CONSENT | PRIVACY_POLICY | CONSENT_*
title: string; // 약관 제목
text: string; // 약관 요약 텍스트
detailsUrl: string; // 상세 URL
orderIndex: number; // UI 표시 순서 (오름차순)
isRequired: boolean; // 필수 동의 여부
// 로그인 컨텍스트에서만 제공 (비로그인 엔드포인트에선 undefined)
agreed?: boolean; // 현재 동의 여부
agreedAt?: number; // 동의 시각 (Unix ms)
revokedAt?: number; // 철회 시각 (Unix ms)
}
타입별 동작 (클라이언트 가이드)
type | 동의 가능 여부 | UI 동작 |
|---|---|---|
TERMS, CONSENT, CONSENT_* | 동의 가능 | 체크박스/토글 제공 |
PRIVACY_POLICY | 공지성 (동의 아님) | 정보 표시만, 동의 액션 없음 |
참고: 이전 버전의
isAgreeable필드는 제거되었습니다.type으로 분기하세요.
🔄 엔드포인트별 상세 변경
1. GET /v1/mobile/app-settings — agreements 필드 확장
Before (v1.2.0):
{
"agreements": [
{
"versionsId": "bab42d93-...",
"type": "TERMS",
"validFrom": 1746316800000,
"isRequired": true,
"orderIndex": 1
}
]
}
After (v1.3.0):
{
"agreements": [
{
"agreementId": "9f8a7b6c-...",
"versionId": "bab42d93-...",
"type": "TERMS",
"title": "서비스 이용약관",
"text": "서비스 이용약관 내용...",
"detailsUrl": "https://www.weltcorp.com/terms",
"orderIndex": 1,
"isRequired": true
}
]
}
변경점:
versionsId→versionId(오타 수정)validFrom제거agreementId/title/text/detailsUrl추가 (본문 포함)agreed/agreedAt/revokedAt는 없음 (AppToken 컨텍스트)
이 엔드포인트가 새로운 통합 진입점입니다. 회원가입 전/로그인 전 약관 조회는 여기서 한 번에 받을 수 있습니다.
2. GET /v1/users/agreements — 로그인 사용자 현재 상태
Before (v1.2.0):
{
"items": [
{
"agreementId": "agreement-uuid",
"versionId": "b8a961f2-...",
"type": "CONSENT",
"isRequired": true,
"title": "약관 제목",
"text": "본문",
"detailsUrl": "https://...",
"agreed": true,
"isAgreeable": true
}
]
}
After (v1.3.0):
{
"items": [
{
"agreementId": "agreement-uuid",
"versionId": "b8a961f2-...",
"type": "CONSENT",
"title": "약관 제목",
"text": "본문",
"detailsUrl": "https://...",
"orderIndex": 0,
"isRequired": true,
"agreed": true,
"agreedAt": 1767836575312
}
]
}
변경점:
isAgreeable제거 →type으로 분기orderIndex추가agreedAt(Unix ms) 추가- 철회된 약관의 경우
agreed: false+revokedAt(Unix ms)
3. GET /v1/users/agreements/history — 동의 이력
Before (v1.2.0):
{
"items": [
{
"agreementId": "agreement-uuid",
"versionId": "version-uuid",
"type": "CONSENT_PERSONAL_DATA",
"isRequired": true,
"title": "Datenschutzeinwilligung",
"detailsUrl": "https://...",
"agreed": true,
"version": "7",
"timestamp": "2026-02-05T17:20:00.000Z"
}
]
}
After (v1.3.0):
{
"items": [
{
"agreementId": "agreement-uuid",
"versionId": "version-uuid",
"type": "CONSENT_PERSONAL_DATA",
"title": "Datenschutzeinwilligung",
"text": "본문",
"detailsUrl": "https://...",
"orderIndex": 2,
"isRequired": true,
"agreed": true,
"agreedAt": 1738776000000
}
]
}
변경점:
version(string) 제거 —versionId로 식별timestamp(ISO string) 제거 →agreedAt/revokedAt(Unix ms)agreed: true이력 →agreedAt사용agreed: false이력 (철회) →agreedAt+revokedAt모두 존재
text/orderIndex추가
4. POST /v1/auth/login — agreements 필드 빈 배열
변경점:
- 응답의
agreements필드가 **항상 빈 배열[]**로 반환됩니다. - 마이그레이션 경로: 로그인 후 약관 상태가 필요하면
GET /v1/users/agreements를 호출하세요. - 향후 스웨거 문서에도
deprecated태그가 표시될 예정입니다 (별도 PR).
5. GET /v1/auth/agreements — Deprecated
변경점:
- Swagger에
deprecated: true태그 표시 - 기능은 그대로 동작 (점진적 이관 가능)
- 마이그레이션 경로:
- 비로그인 컨텍스트:
GET /v1/mobile/app-settings사용 - 로그인 컨텍스트:
GET /v1/users/agreements사용
- 비로그인 컨텍스트:
📦 PRIVACY_POLICY 처리 변경
v1.2.0에서는 GET /v1/auth/agreements 응답에 privacyPolicy가 별도 필드로 분리되어 있었습니다.
v1.3.0부터는 items[] 배열 내부에 type='PRIVACY_POLICY' 아이템으로 포함됩니다.
클라이언트 구현 예시
Before (v1.2.0):
const { items, privacyPolicy } = await fetchAuthAgreements();
// items는 동의 대상, privacyPolicy는 공지성 별도 렌더링
After (v1.3.0):
const { items } = await fetchUserAgreements();
// 하나의 배열 안에 모든 타입 혼재
const consentItems = items.filter(item =>
item.type !== 'PRIVACY_POLICY'
);
const privacyPolicyItem = items.find(item =>
item.type === 'PRIVACY_POLICY'
);
🆕 신규: POST /v1/users/agreements/:agreementId/consent
Request:
POST /v1/users/agreements/9f8a7b6c.../consent
Authorization: Bearer <JWT>
Content-Type: application/json
{ "versionId": "b8a961f2-6e29-44f7-a7cd-67005d177362" }
Response (200 OK): UserAgreementDto
{
"agreementId": "9f8a7b6c-...",
"versionId": "b8a961f2-...",
"type": "TERMS",
"title": "서비스 이용약관",
"text": "본문...",
"detailsUrl": "https://...",
"orderIndex": 1,
"isRequired": true,
"agreed": true,
"agreedAt": 1767836575312
}
특징:
- 멱등성: 이미 같은 versionId로 활성 동의된 상태면 기존 상태 반환 (중복 INSERT 없음)
- 검증:
versionId가 URL의agreementId에 속하지 않으면 400VERSION_AGREEMENT_MISMATCH - 메타데이터: 서버가
ipAddress/userAgent자동 기록 (클라이언트 전송 불필요)
에러 코드:
| HTTP | 코드 | 조건 |
|---|---|---|
| 400 | VERSION_AGREEMENT_MISMATCH (4002) | versionId가 agreementId 소속 아님 |
| 400 | (validation) | versionId UUID 아님/누락 |
| 401 | - | JWT 없음/만료 |
| 404 | AGREEMENT_VERSION_NOT_FOUND (4001) | versionId 미존재 |
🔄 변경: DELETE /v1/users/agreements/:agreementId/consent
Before:
{
"success": true,
"agreementId": "9f8a7b6c-...",
"agreed": false,
"revokedAt": "2026-03-11T10:15:30.000Z"
}
After (v1.3.0):
{
"agreementId": "9f8a7b6c-...",
"versionId": "b8a961f2-...",
"type": "CONSENT",
"title": "Marketing Consent",
"text": "본문...",
"detailsUrl": "https://...",
"orderIndex": 8,
"isRequired": false,
"agreed": false,
"agreedAt": 1700000000000,
"revokedAt": 1767836575312
}
변경점:
success필드 제거 (HTTP 200 자체가 성공)revokedAt포맷 ISO string → Unix ms (number)- 약관 본문(
title,text,detailsUrl,orderIndex,type,isRequired) 포함 agreedAt필드 추가 (최초 동의 시각)
⚠️ Deprecated 쓰기 엔드포인트
POST /v1/agreements/versions/:versionId/agree
→ POST /v1/users/agreements/:agreementId/consent 사용 권장
GET /v1/agreements/versions/:versionId/has-agreed
→ GET /v1/users/agreements 응답의 items[].agreed 필드 사용 권장
두 엔드포인트 모두 기능은 유지됩니다. Swagger에 deprecated 태그만 추가되었습니다.
✅ 클라이언트 마이그레이션 체크리스트
필드 리네임/제거
-
agreements[].versionsId→versionId(mobile/app-settings) -
agreements[].validFrom제거 (mobile/app-settings) — 필요 없으면 사용 코드 삭제 -
items[].isAgreeable제거 (users/agreements) →type기반 분기 코드로 변경 -
items[].version,items[].timestamp제거 (users/agreements/history) →versionId,agreedAt/revokedAt로 대체
신규 필드 활용
-
agreements[].title/text/detailsUrl(mobile/app-settings) — 본문을 별도 호출 없이 표시 가능 -
items[].agreedAt(Unix ms, users/agreements) — 동의 시각 UI 표시 -
items[].revokedAt(Unix ms, users/agreements, history) — 철회 시각 UI 표시 -
items[].text,orderIndex(history) — UI 일관된 정렬 및 본문
엔드포인트 이관
-
POST /v1/auth/login응답의agreements필드 사용 코드 제거 →GET /v1/users/agreements호출로 교체 -
GET /v1/auth/agreements사용 시점 정리:- 회원가입 전 →
GET /v1/mobile/app-settings - 로그인 후 →
GET /v1/users/agreements
- 회원가입 전 →
- POST 동의 호출:
POST /v1/agreements/versions/{versionId}/agree→POST /v1/users/agreements/{agreementId}/consent+ body{ versionId }로 변경 - has-agreed 엔드포인트 사용 제거 →
GET /v1/users/agreements응답의items[].agreed참조
DELETE 응답 파싱 변경
-
response.success제거 (HTTP 200 자체가 성공) -
response.revokedAt을 **Unix ms(number)**로 처리 (기존 ISO string) - DELETE 응답의
title/text/detailsUrl신규 필드로 UI 즉시 갱신 가능 (조회 재요청 불필요)
PRIVACY_POLICY 처리
-
response.privacyPolicy참조 제거 →items.find(i => i.type === 'PRIVACY_POLICY')로 변경 - PRIVACY_POLICY 아이템은 동의 액션 UI 노출 안 함 (공지성)
🚀 배포 순서 권장 (백엔드 관점)
본 변경은 단일 PR/단일 배포이지만, 실제 프로덕션 롤아웃은 아래 순서 권장:
- 스테이징 배포 → 클라이언트 팀이 스키마 변경 확인
- 클라이언트 측 코드 대응 완료 후 프로덕션 배포
- 프로덕션 배포 후
/v1/auth/agreements,/v1/auth/login.agreements사용처 추적 - 충분한 이관 기간 후 완전 제거 (별도 PR)
🔗 참고 자료
설계/계획
docs/superpowers/specs/2026-04-21-agreements-api-consolidation-design.md— 조회 API 통일docs/superpowers/specs/2026-04-21-agreements-consent-api-design.md— 동의/철회 API 정리docs/superpowers/plans/2026-04-21-agreements-api-consolidation.mddocs/superpowers/plans/2026-04-21-agreements-consent-api.md
구현
- Swagger: 로컬 —
http://localhost:3000/v1/docs - 공유 DTO:
libs/shared/contracts-agreements/src/lib/dtos/user-agreement.dto.ts - 신규 도메인 에러:
libs/feature/agreementsVersionAgreementMismatchError(코드 4002)
📝 Out-of-Scope (별도 PR 예정)
- 타 앱(
dha-sleep-api,dha-metabolic-api,dta-wikr-api,dta-wed-api) 마이그레이션 libs/feature/auth/core의LoginResponseDto.agreementsSwaggerdeprecated태그 추가 (타 앱 영향 검토 필요)libs/feature/auth/core의 레거시 command 핸들러 실제 제거