합의(Agreements) 도메인 모델
데이터 저장 규칙
- 합의 도메인 관련 데이터는
agreementPostgreSQL 스키마에 저장되어야 합니다. - 약관(Term)과 동의(Consent) 정보는 하나의 통합된 테이블 구조로 관리됩니다.
- 사용자의 약관 동의 및 동의 상태는
private스키마의user_agreementments테이블에 저장됩니다. - 본 문서의 Prisma 스키마 섹션에는 합의 도메인이 직접 관리하는 엔티티(Agreement, AgreementVersion, AgreementTranslation)만 정의합니다.
- User 엔티티의 전체 Prisma 스키마 정의는 User 도메인의
domain-model.md문서를 참조하세요.
1. 엔티티 관계도 (ERD)
참고: User 엔티티는 외부 도메인(private 스키마)에 속하며, Agreements 도메인은 이와 관계를 맺습니다. 위 ERD는 Agreements 도메인 관점에서의 주요 엔티티와 관계를 보여줍니다.
2. 엔티티
2.1 Agreement
/**
* 약관 또는 동의 항목을 나타내는 엔티티.
* 타입 필드로 TERMS(약관) 또는 CONSENT(동의)를 구분합니다.
*/
interface Agreement {
id: AgreementId; // PK
name: string;
version: string; // 주 버전 (최신 버전은 AgreementVersion에서 관리)
status: AgreementStatus;
type: AgreementType; // 약관(TERMS) 또는 동의(CONSENT) 구분
isRequired: boolean; // 필수 동의 여부
orderIndex: number | null; // UI 표시 순서 (0부터 시작, null이면 타입별 기본 순서 적용)
validFrom: Date | null; // 유효 시작일
validUntil: Date | null; // 유효 종료일
createdBy: string; // 생성자 ID
createdAt: Date; // 생성일
updatedAt: Date; // 수정일
// 관계 필드 (런타임 로드)
versions?: AgreementVersion[]; // 버전 목록
userAgreements?: UserAgreement[]; // 사용자 동의 목록
}
2.2 AgreementVersion
/**
* 약관/동의 항목의 특정 버전을 나타내는 엔티티.
*/
interface AgreementVersion {
id: AgreementVersionId; // PK
agreementsId: AgreementId; // FK
versionNumber: string; // 버전 번호 (major.minor.patch)
changelog: string | null; // 변경 이력
status: AgreementStatus; // 버전 상태
activatedAt: Date | null; // 활성화 일시
createdBy: string | null; // 생성자 ID
createdAt: Date; // 생성일
// 관계 필드 (런타임 로드)
agreement?: Agreement; // 소속 약관/동의
translations?: AgreementTranslation[]; // 번역 목록
userAgreements?: UserAgreement[]; // 사용자 동의 목록
}
2.3 AgreementTranslation
/**
* 약관/동의 버전의 특정 언어 번역을 나타내는 엔티티.
*/
interface AgreementTranslation {
id: AgreementTranslationId; // PK
agreementsVersionId: AgreementVersionId; // FK
language: string; // 언어 코드 (e.g., 'ko', 'en', 'de')
content: string | null; // 번역된 콘텐츠 (텍스트)
detailsUrl: string | null; // 외부 콘텐츠 URL
createdAt: Date; // 생성일
updatedAt: Date; // 수정일
// 관계 필드 (런타임 로드)
agreementVersion?: AgreementVersion; // 소속 버전
}
2.4 UserAgreement
/**
* 사용자의 특정 약관/동의에 대한 동의 기록을 나타내는 엔티티.
*/
interface UserAgreement {
id: UserAgreementId; // PK
userId: string; // FK, User 엔티티 참조
agreementsId: AgreementId; // FK
agreementsVersionId: AgreementVersionId; // FK
agreedAt: Date; // 동의 일시
ipAddress: string; // 동의 시 IP 주소
createdAt: Date; // 생성일
// 관계 필드 (런타임 로드)
user?: User; // 동의한 사용자
agreement?: Agreement; // 동의한 약관/동의
agreementVersion?: AgreementVersion; // 동의한 버전
}
2.5 User (Agreements Domain View)
/**
* 사용자 엔티티 (Agreements 도메인 관점).
* 실제 데이터는 `private.users` 테이블에 저장되며, User 도메인이 관리합니다.
* Agreements는 사용자 ID를 참조하여 약관/동의 상태를 관리합니다.
*/
interface UserAgreementsView {
id: string;
// User 도메인에서 관리하는 기타 필드 (email, status 등)
// 관계 필드 (런타임 로드)
userAgreements?: UserAgreement[]; // 사용자의 동의 목록
}
3. 값 객체
3.1 AgreementId
type AgreementId = string; // UUID or other unique identifier
3.2 AgreementVersionId
type AgreementVersionId = string; // UUID or other unique identifier
3.3 AgreementTranslationId
type AgreementTranslationId = string; // UUID or other unique identifier
3.4 UserAgreementId
type UserAgreementId = string; // UUID or other unique identifier
3.5 AgreementStatus
/**
* 약관/동의 상태를 나타내는 열거형.
*/
enum AgreementStatus {
DRAFT = 'DRAFT', // 초안
ACTIVE = 'ACTIVE', // 활성
INACTIVE = 'INACTIVE', // 비활성
EXPIRED = 'EXPIRED', // 만료
}
3.6 AgreementType
/**
* 약관/동의 타입을 나타내는 열거형.
* 개인정보보호법 및 정보통신망법에 따라 동의 유형을 세분화하여 관리합니다.
*/
enum AgreementType {
// 기본 타입
TERMS = 'TERMS', // 서비스 이용 약관
CONSENT = 'CONSENT', // 일반 동의 (레거시 호환)
PRIVACY_POLICY = 'PRIVACY_POLICY', // 개인정보 처리방침
// 세분화된 동의 타입
CONSENT_AGE_VERIFICATION = 'CONSENT_AGE_VERIFICATION', // 만 14세 이상 확인 동의
CONSENT_PERSONAL_DATA = 'CONSENT_PERSONAL_DATA', // 개인정보 수집·이용 동의
CONSENT_SENSITIVE_DATA = 'CONSENT_SENSITIVE_DATA', // 민감정보 수집·이용 동의
CONSENT_SENSITIVE_DATA_HEALTH = 'CONSENT_SENSITIVE_DATA_HEALTH', // 건강정보(민감정보) 수집·이용 동의
}
AgreementType 상세 설명
| 타입 | 설명 | 필수 여부 | 비고 |
|---|---|---|---|
TERMS | 서비스 이용 약관 | 필수 | - |
CONSENT | 일반 동의 | 설정에 따름 | 레거시 호환성 유지 |
PRIVACY_POLICY | 개인정보 처리방침 | 필수 | - |
CONSENT_AGE_VERIFICATION | 만 14세 이상 확인 동의 | 필수 | 개인정보보호법 제22조, 정보통신망법 제31조 |
CONSENT_PERSONAL_DATA | 개인정보 수집·이용 동의 | 필수 | 개인정보보호법 제15조 |
CONSENT_SENSITIVE_DATA | 민감정보 수집·이용 동의 | 선택 | 개인정보보호법 제23조 |
CONSENT_SENSITIVE_DATA_HEALTH | 건강정보(민감정보) 수집·이용 동의 | 선택 | 개인정보보호법 제23조, HealthKit/Google Fit 연동 시 필요, 동의 시 health:read 권한 부여 |
4. 집계 (Aggregates)
4.1 Agreement Aggregate
- 루트: Agreement
- 엔티티: AgreementVersion, AgreementTranslation
- 값 객체: AgreementId, AgreementType, AgreementStatus
- 불변식:
- 동일한 타입의 약관은 한 시점에 하나만 활성화 가능
- 약관 메타데이터 변경 이력은 불변함
- 활성화된 약관은 삭제 불가
- 버전별 유효 기간이 설정됨
4.2 UserAgreement Aggregate
- 루트: UserAgreement
- 값 객체: UserAgreementId, AgreeAt, IpAddress
- 참조: User (외부 참조), Agreement, AgreementVersion
- 불변식:
- 사용자 약관 동의 기록은 불변(추가만 가능)
- 동의 기록은 사용자 계정 삭제 후에도 보관
- 필수 약관에 대한 동의 없이는 서비스 이용 불가
- 동의 철회 기록 유지
5. 도메인 서비스
5.1 AgreementService
interface AgreementService {
createAgreement(
data: {
name: string;
type: AgreementType;
isRequired: boolean;
defaultLanguage: string;
contentUrl?: string;
validFrom?: Date;
validUntil?: Date;
},
createdBy: string
): Promise<Agreement>;
getAgreement(id: AgreementId): Promise<Agreement | null>;
getAgreementByTypeAndStatus(type: AgreementType, status: AgreementStatus): Promise<Agreement[]>;
getAllAgreements(filter?: { status?: AgreementStatus; type?: AgreementType; page?: number; pageSize?: number }): Promise<{ items: Agreement[]; metadata: PaginationMeta }>;
updateAgreement(
id: AgreementId,
data: {
name?: string;
isRequired?: boolean;
validFrom?: Date;
validUntil?: Date;
}
): Promise<Agreement>;
changeAgreementStatus(id: AgreementId, status: AgreementStatus): Promise<Agreement>;
// 버전 관리
addAgreementVersion(agreementId: AgreementId, versionNumber: string, changelog?: string, createdBy?: string): Promise<AgreementVersion>;
activateAgreementVersion(versionId: AgreementVersionId): Promise<AgreementVersion>;
// 번역 관리
addAgreementTranslation(versionId: AgreementVersionId, language: string, content?: string, detailsUrl?: string): Promise<AgreementTranslation>;
getAgreementHistory(type: AgreementType): Promise<{
type: AgreementType;
currentVersion: string;
versions: AgreementVersion[];
}>;
}
5.2 UserAgreementService
interface UserAgreementService {
recordUserAgreement(userId: string, agreementId: AgreementId, versionId: AgreementVersionId, ipAddress: string): Promise<UserAgreement>;
getUserAgreements(userId: string): Promise<UserAgreement[]>;
getUserAgreementHistory(userId: string): Promise<{
userId: string;
agreements: Array<{
agreementId: AgreementId;
version: string;
title: string;
type: AgreementType;
isAgreed: boolean;
agreedAt: Date;
}>;
}>;
validateUserAgreements(
userId: string,
requiredAgreementTypes: AgreementType[]
): Promise<{
validated: boolean;
missingAgreements: Array<{
type: AgreementType;
title: string;
required: boolean;
}>;
}>;
recordBatchAgreements(
userId: string,
agreements: Array<{
agreementId: AgreementId;
isAgreed: boolean;
}>,
ipAddress: string
): Promise<{
userId: string;
timestamp: Date;
agreements: UserAgreement[];
requiredAgreementsComplete: boolean;
}>;
}
5.3 ConsentService
interface ConsentService {
defineConsent(data: { key: string; title: string; description: string; purpose: string; scope: string; required: boolean }): Promise<Agreement>;
updateConsentDefinition(
id: AgreementId,
data: {
title?: string;
description?: string;
purpose?: string;
scope?: string;
}
): Promise<Agreement>;
recordUserConsent(userId: string, consentKey: string, isAgreed: boolean, ipAddress: string): Promise<UserAgreement>;
updateUserConsent(userId: string, consentKey: string, isAgreed: boolean, ipAddress: string): Promise<UserAgreement>;
getConsentDefinitions(filter?: { required?: boolean; page?: number; pageSize?: number }): Promise<{ items: Agreement[]; metadata: PaginationMeta }>;
getUserConsents(userId: string): Promise<{
userId: string;
consents: Array<{
key: string;
title: string;
isAgreed: boolean;
required: boolean;
agreedAt: Date | null;
updatedAt: Date;
}>;
}>;
validateUserConsents(
userId: string,
requiredConsentKeys: string[]
): Promise<{
validated: boolean;
missingConsents: Array<{
key: string;
title: string;
required: boolean;
}>;
}>;
}
5.4 AgreementValidationService
interface AgreementValidationService {
validateAgreement(
userId: string,
requiredTermsTypes: string[],
requiredConsentKeys: string[]
): Promise<{
userId: string;
validated: boolean;
missingTerms: Array<{
type: string;
title: string;
required: boolean;
}>;
missingConsents: Array<{
key: string;
title: string;
required: boolean;
}>;
}>;
getAllActiveAgreements(language?: string): Promise<{
terms: Agreement[];
consents: Agreement[];
}>;
}
6. 도메인 이벤트
상세 내용은 event-storming.md 참조
- 약관 관련 이벤트: TermsCreated, TermsUpdated, TermsActivated, TermsDeactivated, TermsExpired, TermsTranslationAdded, UserTermsAgreementRecorded
- 동의 관련 이벤트: ConsentDefinitionCreated, ConsentDefinitionUpdated, UserConsentRecorded, UserConsentUpdated, AgreementValidationRequested, AgreementValidated
- 프론트엔드 약관 이벤트: TermsDisplayed, TermsConsentRequested, TermsAgreementSubmitted, TermsUpdatedNotificationDisplayed, LanguageVersionSelected
- 프론트엔드 동의 이벤트: ConsentOptionsDisplayed, ConsentStatusSubmitted, ConsentHistoryDisplayed
- 통합 UI 이벤트: IntegratedAgreementsDisplayed, AllAgreementsAccepted, AgreementBatchSubmitted
7. 도메인 규칙
상세 내용은 business-rules.md 참조
주요 규칙 카테고리는 다음과 같습니다:
- 약관 생성 규칙: 고유 식별자, 버전 형식, 생성자 정보, 최소 한 개 이상 언어, 상태 관리
- 약관 버전 관리 규칙: 버전 규칙, 활성화, 비활성화
- 약관 번역 관리 규칙: 지원 언어, 언어당 하나의 번역, 번역 검수
- 약관 표시 규칙: UI 요구사항, 접근성, 이력 표시
- 동의 데이터 관리 규칙: 사용자 식별, 동의 범위, 동의 목적, 유효 기간, 메타데이터
- 동의 검증 규칙: 형식 검증, 상태 검증, 범위 검증
- 동의 취소 규칙: 취소 조건, 연관 처리
- 이력 관리 규칙: 이력 기록, 감사 요구사항
- 통합 사용자 동의 관리 규칙: 동의 기록, 동의 요구사항, 동의 철회, 통합 UI 표시
- 데이터 모델 규칙: 약관과 동의의 통합 저장소 관리
7.1 약관 및 동의 통합 관리 규칙
- 약관(Term)과 동의(Consent)는 단일 테이블 구조(Agreement)에 저장합니다.
- 각 레코드는 type 필드(TERMS, CONSENT, PRIVACY_POLICY)로 구분합니다.
- 공통 속성은 기본 필드로, 타입별 특수 속성은 필요에 따라 확장합니다.
- 약관과 동의 레코드를 동일한 ID 체계로 관리하여 참조 일관성을 유지합니다.
- 사용자 동의는 불변 이벤트로 기록하며, 취소/변경은 신규 기록으로 관리합니다.
8. 데이터베이스 스키마 (Prisma)
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
schemas = ["agreement", "private"]
}
// --- 외부 도메인 모델 (참조용 stub) ---
// User 모델 (private 스키마)
// 실제 정의는 user/domain-model.md 참조
model User {
id String @id @default(uuid())
// ... other fields defined in User domain
userAgreementments UserAgreement[]
@@map("users")
@@schema("private")
}
// --- Agreements 도메인 모델 ---
// 약관 상태 열거형
enum AgreementStatus {
DRAFT
ACTIVE
INACTIVE
EXPIRED
@@schema("agreement")
}
// 약관 타입 열거형
enum AgreementType {
TERMS
CONSENT
PRIVACY_POLICY
@@schema("agreement")
}
// 약관/동의 모델
model Agreements {
id String @id @default(uuid())
name String
version String
status AgreementStatus
type AgreementType @map("type")
isRequired Boolean @default(false) @map("is_required")
orderIndex Int? @map("order_index") // UI 표시 순서 (0부터 시작)
validFrom DateTime? @map("valid_from")
validUntil DateTime? @map("valid_until")
createdBy String @map("created_by")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
// 관계
versions AgreementsVersion[]
userAgreementments UserAgreement[]
@@index([status])
@@index([type])
@@index([validFrom, validUntil])
@@map("agreements")
@@schema("agreement")
}
// 약관/동의 버전 모델
model AgreementsVersion {
id String @id @default(uuid())
agreementsId String @map("agreements_id")
versionNumber String @map("version_number")
changelog String?
status AgreementStatus @default(DRAFT)
activatedAt DateTime? @map("activated_at")
createdBy String? @map("created_by")
createdAt DateTime @default(now()) @map("created_at")
// 관계
agreements Agreements @relation(fields: [agreementsId], references: [id])
translations AgreementsTranslation[]
userAgreementments UserAgreement[]
@@unique([agreementsId, versionNumber])
@@map("agreements_versions")
@@schema("agreement")
}
// 약관/동의 번역 모델
model AgreementsTranslation {
id String @id @default(uuid())
agreementsVersionId String @map("agreements_version_id")
language String
content String? @db.Text
detailsUrl String? @map("details_url")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
// 관계
agreementsVersion AgreementsVersion @relation(fields: [agreementsVersionId], references: [id])
@@unique([agreementsVersionId, language])
@@map("agreements_translations")
@@schema("agreement")
}
// 사용자 동의 모델
model UserAgreement {
id String @id @default(uuid())
userId String @map("user_id")
agreementsId String @map("agreements_id")
agreementsVersionId String @map("agreements_version_id")
agreedAt DateTime @map("agreementd_at")
ipAddress String @map("ip_address")
createdAt DateTime @default(now()) @map("created_at")
// 관계
user User @relation(fields: [userId], references: [id])
agreements Agreements @relation(fields: [agreementsId], references: [id])
agreementsVersion AgreementsVersion @relation(fields: [agreementsVersionId], references: [id])
@@index([userId])
@@index([agreementsId])
@@map("user_agreementments")
@@schema("private")
}
참고: Prisma 스키마에서는 agreement 스키마에 약관/동의 관련 엔티티를 정의하고, private 스키마에 사용자-약관 관계를 정의했습니다. 실제 구현 시 약관과 동의를 타입 필드로 구분하여 단일 테이블에서 관리합니다.
9. 변경 이력
| 버전 | 날짜 | 작성자 | 변경 내용 |
|---|---|---|---|
| 0.1.0 | 2025-05-08 | bok@weltcorp.com | 최초 문서 생성 (약관 및 동의 도메인 모델 통합) |