본문으로 건너뛰기

기본 약관 관리 기술 명세

1. 개요

이 문서는 Auth 도메인의 기본 약관 관리 시스템에 대한 기술적 구현을 정의합니다. 서비스 이용 약관, 개인정보 처리방침 등 기본적인 약관의 생성, 관리, 동의 처리를 다룹니다.

1.1 주요 기능

  1. 기본 약관 관리
  2. 약관 버전 관리
  3. 필수 동의 처리
  4. 동의 이력 관리
  5. 다국어 지원

1.2 용어집

용어정의관련 컨텍스트동의어
기본 약관(Basic Terms)서비스 이용을 위한 필수적인 기본 약관회원가입, 서비스 이용이용약관
약관 동의(Term Agreement)사용자가 약관 내용을 확인하고 동의하는 행위회원가입, 인증이용 동의
필수 약관(Required Terms)서비스 이용을 위해 반드시 동의가 필요한 약관회원가입, 인증필수 동의
선택 약관(Optional Terms)사용자가 선택적으로 동의할 수 있는 약관마케팅, 알림선택 동의
약관 번역(Term Translation)약관의 다국어 버전국제화, 현지화다국어 약관
동의 이력(Agreement History)사용자의 약관 동의 기록감사, 법률 준수동의 로그

2. 시스템 아키텍처

2.1 컴포넌트 구조

2.2 데이터 모델

interface BasicTerm {
id: string; // 약관 ID
version: string; // 시맨틱 버전
type: BasicTermType; // 약관 유형
title: string; // 약관 제목
content: string; // 약관 내용
isRequired: boolean; // 필수 동의 여부
displayOrder: number; // 표시 순서
status: TermStatus; // 약관 상태
validFrom: Date; // 유효 시작일
validUntil?: Date; // 유효 종료일
createdAt: Date; // 생성일
updatedAt: Date; // 수정일
createdBy: string; // 생성자
updatedBy: string; // 수정자
isActive: boolean; // 활성화 여부
translations: TermTranslation[]; // 다국어 번역
}

interface TermTranslation {
id: string; // 번역 ID
termId: string; // 약관 ID
language: string; // 언어 코드
title: string; // 번역된 제목
content: string; // 번역된 내용
createdAt: Date; // 생성일
updatedAt: Date; // 수정일
}

interface BasicTermAgreement {
id: string; // 동의 ID
userId: string; // 사용자 ID
termId: string; // 약관 ID
version: string; // 동의한 약관 버전
isAgreed: boolean; // 동의 여부
agreedAt: Date; // 동의 일시
deviceInfo: { // 디바이스 정보
type: string;
os: string;
browser: string;
};
ipAddress: string; // IP 주소
userAgent: string; // User Agent
createdAt: Date; // 생성일
}

enum BasicTermType {
SERVICE = 'SERVICE', // 서비스 이용약관
PRIVACY = 'PRIVACY', // 개인정보 처리방침
MARKETING = 'MARKETING', // 마케팅 정보 수신
AGE_VERIFICATION = 'AGE_VERIFICATION' // 연령 확인
}

enum TermStatus {
DRAFT = 'DRAFT', // 초안
ACTIVE = 'ACTIVE', // 활성
INACTIVE = 'INACTIVE', // 비활성
EXPIRED = 'EXPIRED' // 만료
}

2.3 Prisma 스키마

model BasicTerm {
id String @id @default(uuid())
version String // 시맨틱 버전
type BasicTermType
title String
content String @db.Text
isRequired Boolean @default(true)
displayOrder Int
status TermStatus
validFrom DateTime
validUntil DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
createdBy String
updatedBy String
isActive Boolean @default(true)
translations TermTranslation[]
agreements BasicTermAgreement[]

@@index([type, status])
@@index([validFrom, validUntil])
}

model TermTranslation {
id String @id @default(uuid())
termId String
term BasicTerm @relation(fields: [termId], references: [id])
language String
title String
content String @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

@@unique([termId, language])
}

model BasicTermAgreement {
id String @id @default(uuid())
userId String
termId String
term BasicTerm @relation(fields: [termId], references: [id])
version String
isAgreed Boolean
agreedAt DateTime
deviceInfo Json // DeviceInfo
ipAddress String
userAgent String
createdAt DateTime @default(now())

@@index([userId, termId])
@@index([agreedAt])
}

enum BasicTermType {
SERVICE
PRIVACY
MARKETING
AGE_VERIFICATION
}

enum TermStatus {
DRAFT
ACTIVE
DEPRECATED
INACTIVE
}

2.4 저장소 구현

@Injectable()
export class BasicTermRepository {
constructor(private readonly prisma: PrismaService) {}

async createTerm(data: CreateBasicTermDto): Promise<BasicTerm> {
return this.prisma.basicTerm.create({
data: {
...data,
translations: {
create: data.translations,
},
},
include: {
translations: true,
},
});
}

async findActiveTerms(type?: BasicTermType): Promise<BasicTerm[]> {
return this.prisma.basicTerm.findMany({
where: {
...(type && { type }),
status: TermStatus.ACTIVE,
isActive: true,
validFrom: { lte: new Date() },
OR: [
{ validUntil: null },
{ validUntil: { gt: new Date() } },
],
},
include: {
translations: true,
},
orderBy: [
{ displayOrder: 'asc' },
{ version: 'desc' },
],
});
}

async createAgreement(data: CreateTermAgreementDto): Promise<BasicTermAgreement> {
return this.prisma.basicTermAgreement.create({
data: {
...data,
deviceInfo: data.deviceInfo as unknown as Prisma.JsonValue,
},
include: {
term: {
include: {
translations: true,
},
},
},
});
}

async findUserAgreements(userId: string): Promise<BasicTermAgreement[]> {
return this.prisma.basicTermAgreement.findMany({
where: {
userId,
isAgreed: true,
},
include: {
term: {
include: {
translations: true,
},
},
},
orderBy: { agreedAt: 'desc' },
});
}

async checkRequiredTermsAgreement(userId: string): Promise<boolean> {
const requiredTerms = await this.prisma.basicTerm.findMany({
where: {
isRequired: true,
status: TermStatus.ACTIVE,
isActive: true,
},
select: { id: true },
});

const agreedTerms = await this.prisma.basicTermAgreement.findMany({
where: {
userId,
termId: { in: requiredTerms.map(t => t.id) },
isAgreed: true,
},
select: { termId: true },
});

return requiredTerms.length === agreedTerms.length;
}
}

3. 약관 동의 프로세스

3.1 약관 생성 API

요청 (Request)
{
"title": "서비스 이용약관",
"type": "service",
"required": true,
"defaultLanguage": "ko",
"content": "본 약관은 서비스 이용에 관한 사항을 규정합니다...",
"validFrom": 1709251200000,
"validUntil": 1735689599000
}
응답 (Response)
  • 성공 응답 (201 Created)
{
"id": 1,
"version": "1.0.0",
"title": "서비스 이용약관",
"type": "service",
"required": true,
"defaultLanguage": "ko",
"status": "DRAFT",
"validFrom": 1709251200000,
"validUntil": 1735689599000,
"createdAt": 1711011600000,
"updatedAt": 1711011600000
}

3.2 회원가입 시 약관 동의

interface SignupTermService {
// 회원가입에 필요한 필수 약관 목록 조회
getRequiredTerms(): Promise<BasicTerm[]>;

// 약관 동의 처리
createSignupAgreements(params: {
userId: string;
agreements: {
termId: string;
isAgreed: boolean;
}[];
metadata: AgreementMetadata;
}): Promise<BasicTermAgreement[]>;

// 필수 약관 동의 검증
validateRequiredAgreements(userId: string): Promise<ValidationResult>;
}

3.3 약관 동의 이벤트

interface BasicTermAgreementEvent {
type: 'TERM_AGREEMENT';
userId: string;
termId: string;
agreementId: string;
isAgreed: boolean;
metadata: {
termType: BasicTermType;
termVersion: string;
isRequired: boolean;
};
timestamp: Date;
}

4. 약관 버전 관리

4.1 버전 관리 전략

  • 시맨틱 버전닝 사용 (MAJOR.MINOR.PATCH)
  • MAJOR: 필수 동의가 필요한 주요 변경
  • MINOR: 선택 동의가 필요한 일반 변경
  • PATCH: 단순 오타 수정 등 재동의 불필요 변경

4.2 버전 전환 프로세스

interface TermVersionService {
// 새 버전 생성
createNewVersion(params: {
termId: string;
version: string;
content: string;
translations: Record<string, TermTranslation>;
changeType: 'MAJOR' | 'MINOR' | 'PATCH';
}): Promise<BasicTerm>;

// 버전 활성화
activateVersion(termId: string, version: string): Promise<void>;

// 이전 버전 비활성화
deactivateVersion(termId: string, version: string): Promise<void>;
}

5. 캐싱 전략

5.1 캐시 구조

interface TermCache {
key: string; // 캐시 키
term: BasicTerm; // 약관 데이터
validUntil: Date; // 캐시 만료일
}

// Redis 캐시 키 패턴
const CACHE_KEYS = {
activeTerm: (type: BasicTermType) => `term:active:${type}`,
termVersion: (id: string, version: string) => `term:${id}:${version}`,
userAgreements: (userId: string) => `agreements:${userId}`
};

5.2 캐시 설정

  • Redis 사용
  • 활성 약관 캐시 TTL: 1시간
  • 사용자 동의 정보 캐시 TTL: 30분
  • 캐시 무효화: LRU 정책

6. 감사 및 모니터링

6.1 감사 로깅

interface TermAuditLog {
eventId: string;
eventType: TermEventType;
termId: string;
userId?: string;
action: string;
metadata: {
termType: BasicTermType;
version: string;
deviceInfo?: DeviceInfo;
ipAddress?: string;
};
timestamp: Date;
}

enum TermEventType {
TERM_CREATED = 'TERM_CREATED',
TERM_UPDATED = 'TERM_UPDATED',
TERM_ACTIVATED = 'TERM_ACTIVATED',
AGREEMENT_CREATED = 'AGREEMENT_CREATED',
AGREEMENT_REVOKED = 'AGREEMENT_REVOKED'
}

6.2 모니터링 지표

@Injectable()
export class TermMetricsService {
constructor(private readonly prisma: PrismaService) {}

async getTermMetrics(): Promise<TermMetrics[]> {
return this.prisma.$queryRaw`
SELECT
bt.type as term_type,
bt.title,
COUNT(DISTINCT bta.user_id) as agreed_users,
COUNT(DISTINCT CASE WHEN bta.is_agreed THEN bta.id END) as active_agreements,
AVG(EXTRACT(EPOCH FROM (NOW() - bta.agreed_at)) / 86400) as avg_agreement_age_days
FROM "BasicTerm" bt
LEFT JOIN "BasicTermAgreement" bta ON bt.id = bta.term_id
WHERE bt.status = 'ACTIVE'
GROUP BY bt.type, bt.title
ORDER BY agreed_users DESC
`;
}
}

7. 보안 정책

7.1 접근 제어

  • 약관 관리자 역할 정의
  • 약관 조회는 인증 없이 가능
  • 약관 수정은 관리자 권한 필요
  • 동의 이력 조회는 본인 또는 관리자만 가능

7.2 데이터 보안

  • 약관 내용 변경 이력 보관
  • 동의 기록 무결성 보장
  • 민감한 메타데이터 암호화

8. 변경 이력

버전날짜작성자변경 내용
0.1.02025-03-26bok@weltcorp.com최초 작성