기본 약관 관리 기술 명세
1. 개요
이 문서는 Auth 도메인의 기본 약관 관리 시스템에 대한 기술적 구현을 정의합니다. 서비스 이용 약관, 개인정보 처리방침 등 기본적인 약관의 생성, 관리, 동의 처리를 다룹니다.
1.1 주요 기능
- 기본 약관 관리
- 약관 버전 관리
- 필수 동의 처리
- 동의 이력 관리
- 다국어 지원
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.0 | 2025-03-26 | bok@weltcorp.com | 최초 작성 |