Plan 도메인 모델
데이터 저장 규칙
- Plan 도메인 관련 모든 데이터는
planPostgreSQL 스키마에 저장되어야 합니다. (business-rules.md 1.1) - 플랜과 연결된 역할(Role) 정보는 IAM 도메인(
iam스키마)을 참조합니다. - 플랜과 연결된 사용자(User) 정보는 User 도메인(
private,operation스키마)을 참조합니다. - 본 문서의 Prisma 스키마 섹션에는 Plan 도메인이 직접 관리하는 엔티티(Plan, PlanRole, UserPlan, 플랜 메타데이터 등)만 정의합니다.
- Role 및 User 엔티티의 전체 Prisma 스키마 정의는 각 도메인의
domain-model.md문서를 참조하세요.
1. 엔티티 관계도 (ERD)
2. 엔티티
2.1 Plan
/**
* 사용자가 이용할 수 있는 서비스의 범위, 기능 세트, 또는 프로그램 수준을 정의하는 엔티티.
* 여러 역할(Role)의 집합으로 구성되며, 사용자에게 직접 할당되어 서비스 수준을 제공합니다.
*/
interface Plan {
id: PlanId; // PK
name: string; // Unique
description?: string;
planType: PlanType; // 플랜 유형 (Therapeutic, Limited Access, Sample, Development, Clinician)
isActive: boolean; // 활성/비활성 상태
createdAt: Date;
updatedAt: Date;
// 관계 필드 (런타임 로드)
roleIds?: RoleId[]; // FK 역할 ID 목록 (효율적 조회를 위해 추가)
userPlans?: UserPlan[]; // 사용자-플랜 할당 정보
metadata?: PlanMetadata[]; // 플랜 메타데이터
versions?: PlanVersion[]; // 플랜 버전 이력
}
2.2 PlanRole (Association Entity)
/**
* Plan과 Role 간의 다대다 관계를 나타내는 연결 엔티티.
*/
interface PlanRole {
planId: PlanId; // Composite PK, FK
roleId: RoleId; // Composite PK, FK (IAM 도메인 참조)
assignedAt: Date;
}
2.3 UserPlan (Association Entity)
/**
* User와 Plan 간의 관계를 나타내는 연결 엔티티.
* 사용자는 한 시점에 하나의 활성 플랜만 가질 수 있습니다.
*/
interface UserPlan {
userId: UserId; // PK, FK (User 도메인)
planId: PlanId; // FK
assignedAt: Date;
expiresAt?: Date; // 플랜 만료일 (선택적)
isActive: boolean; // 현재 활성 플랜 여부
assignedBy?: UserId; // 플랜 할당 담당자
}
2.4 PlanMetadata
/**
* 플랜의 복잡한 비즈니스 메타데이터를 별도로 관리하는 엔티티.
*
* 🎯 설계 목적:
* - 핵심 Plan 엔티티와 복잡한 비즈니스 로직 분리
* - 자주 변경되는 정책/규칙과 안정적인 식별자 분리
* - 성능 최적화: 기본 조회 시 무거운 메타데이터 제외
* - 확장성: 새로운 비즈니스 규칙 추가 시 스키마 변경 없이 JSON으로 유연하게 확장
*/
interface PlanMetadata {
id: string; // PK
planId: PlanId; // FK (unique)
serviceLevel: string; // BASIC, STANDARD, PREMIUM
accessLevel: string; // FULL, LIMITED, SAMPLE
maxDuration?: number; // 최대 이용 기간 (일)
features: FeatureConfig; // 플랜별 기능 설정
businessRules: BusinessRuleConfig; // 비즈니스 규칙
billingInfo?: BillingConfig; // 청구 정보 (외부 시스템 연동용)
createdAt: Date;
updatedAt: Date;
}
/**
* 플랜별 기능 설정 예시
*/
interface FeatureConfig {
sleepTracking: boolean;
questionnaires: string[]; // ["ISI", "DBAS16", "PHQ9"]
learningContent: boolean;
relaxationContent: boolean;
agentCoaching: boolean;
maxSessions?: number;
}
/**
* 비즈니스 규칙 설정 예시
*/
interface BusinessRuleConfig {
autoSuspension: {
inactivityDays: number;
warningDays: number[];
};
contentAccess: {
unlockSchedule: 'immediate' | 'progressive';
prerequisiteCheck: boolean;
};
dataRetention: {
periodDays: number;
anonymizationRules: string[];
};
}
2.5 PlanTemplate
/**
* 플랜 생성을 위한 템플릿 엔티티.
*
* 🎯 설계 목적:
* - 플랜 생성 시 일관성 보장
* - 표준화된 설정으로 운영 실수 방지
* - 새로운 플랜 타입 추가 시 재사용 가능한 기본 설정 제공
* - A/B 테스트나 점진적 기능 출시를 위한 설정 템플릿화
*
* 💡 사용 시나리오:
* 1. 새로운 치료 플랜 생성 시 "THERAPEUTIC" 템플릿 사용
* 2. 지역별 맞춤 플랜 생성 시 기본 템플릿 + 지역 특화 설정
* 3. 프로모션 플랜 생성 시 기존 템플릿 복사 후 할인 정책 적용
*/
interface PlanTemplate {
id: PlanTemplateId; // PK
name: string; // Unique
description?: string;
planCategory: PlanCategory; // THERAPEUTIC, LIMITED_ACCESS, SAMPLE, DEVELOPMENT, CLINICIAN
defaultConfig: DefaultPlanConfig; // 기본 설정 구조
isActive: boolean;
createdAt: Date;
updatedAt: Date;
}
/**
* 템플릿 기본 설정 구조
*/
interface DefaultPlanConfig {
serviceLevel: string;
accessLevel: string;
maxDuration: number;
defaultFeatures: FeatureConfig;
defaultBusinessRules: BusinessRuleConfig;
defaultRoles: string[]; // Role ID 목록
contentSchedule?: {
[week: string]: string[]; // 주차별 추천 콘텐츠
};
}
2.6 PlanVersion
/**
* 플랜의 버전 관리를 위한 엔티티.
*
* 🎯 설계 목적:
* - 플랜 설정 변경 이력 추적
* - 변경 사항 롤백 지원
* - 규정 준수를 위한 감사 추적
* - A/B 테스트를 위한 버전별 설정 관리
* - 점진적 배포(canary deployment) 지원
*
* 💡 사용 시나리오:
* 1. 플랜 설정 변경 시 이전 버전 백업
* 2. 문제 발생 시 이전 버전으로 롤백
* 3. 규제 요구사항에 따른 변경 이력 보존
* 4. 계절별/프로모션별 임시 플랜 설정 버전 관리
*/
interface PlanVersion {
id: PlanVersionId; // PK
templateId: PlanTemplateId; // FK to PlanTemplate
version: string; // e.g., "1.0.0", "2.1.0"
config: PlanVersionConfig; // 버전별 설정
changeLog?: string; // 변경 사항 설명
status: VersionStatus; // DRAFT, ACTIVE, DEPRECATED
effectiveFrom?: Date; // 적용 시작일
effectiveUntil?: Date; // 적용 종료일
createdBy: string; // 생성자 ID
createdAt: Date;
}
/**
* 버전별 플랜 설정
*/
interface PlanVersionConfig {
serviceLevel: string;
accessLevel: string;
maxDuration: number;
features: FeatureConfig;
businessRules: BusinessRuleConfig;
roles: string[]; // Role ID 목록
metadata?: Record<string, any>; // 추가 메타데이터
}
enum VersionStatus {
DRAFT = 'DRAFT', // 초안
ACTIVE = 'ACTIVE', // 활성
DEPRECATED = 'DEPRECATED' // deprecated
}
2.5.1 Plan Version 상세 관리 가이드 📋
Plan Version은 플랜의 생명주기 전체를 관리하는 핵심 메커니즘입니다. 실제 운영에서 어떻게 활용되는지 구체적으로 살펴보겠습니다.
🔄 1. 버전 관리 워크플로우
// 💡 실제 플랜 업데이트 시나리오
async function updatePlanWithVersioning(planId: string, newFeatures: FeatureConfig) {
// 1. 현재 활성 버전 백업
const currentVersion = await planVersionRepo.findActive(planId);
await planVersionService.createSnapshot({
templateId: planId,
version: incrementVersion(currentVersion.version), // "1.2.0" → "1.3.0"
config: currentVersion.config,
status: 'DRAFT',
changeLog: "Pre-update backup before adding new features",
createdBy: currentUserId
});
// 2. 새 버전 생성 (DRAFT 상태)
const newVersion = await planVersionService.create({
templateId: planId,
version: "1.3.0",
config: {
...currentVersion.config,
features: newFeatures // 새 기능 추가
},
status: 'DRAFT',
changeLog: "Added relaxation content and advanced sleep tracking",
createdBy: currentUserId
});
// 3. 테스트 환경에서 검증
await runPlanValidationTests(newVersion);
// 4. 점진적 배포 (10% → 50% → 100%)
await deployVersionGradually(newVersion);
}
function incrementVersion(currentVersion: string): string {
const [major, minor, patch] = currentVersion.split('.').map(Number);
return `${major}.${minor}.${patch + 1}`;
}
🎯 2. 점진적 배포 (Canary Deployment) 전략
// 💡 실제 카나리 배포 시나리오
async function deployVersionGradually(newVersion: PlanVersion) {
// Phase 1: 10% 사용자에게 먼저 배포
await deployToUserSegment(newVersion, {
percentage: 10,
criteria: 'early_adopters',
duration: '3_days'
});
// 3일 후 메트릭 확인
const phase1Metrics = await analyzeVersionMetrics(newVersion, '3_days');
if (phase1Metrics.errorRate < 0.1 && phase1Metrics.userSatisfaction > 4.0) {
// Phase 2: 50% 사용자로 확장
await deployToUserSegment(newVersion, {
percentage: 50,
criteria: 'general_users',
duration: '7_days'
});
// 1주일 후 최종 확인
const phase2Metrics = await analyzeVersionMetrics(newVersion, '7_days');
if (phase2Metrics.stable) {
// Phase 3: 전체 배포 및 활성화
await activateVersionForAllUsers(newVersion);
await deprecateOldVersion(newVersion.templateId, newVersion.version);
}
}
}
interface DeploymentMetrics {
errorRate: number; // 에러 발생률
userSatisfaction: number; // 사용자 만족도 (1-5)
performanceImpact: number; // 성능 영향도
featureAdoption: number; // 새 기능 사용률
stable: boolean; // 전반적 안정성
}
🔄 3. 버전 롤백 메커니즘
// 💡 긴급 롤백 시나리오
async function emergencyRollback(planId: string, reason: string) {
// 1. 현재 활성 버전 식별
const currentVersion = await planVersionRepo.findByStatus(planId, 'ACTIVE');
// 2. 이전 안정 버전 찾기
const stableVersion = await planVersionRepo.findLastStable(planId);
if (!stableVersion) {
throw new Error('No stable version found for rollback');
}
// 3. 즉시 롤백 실행
const rollbackResult = await executeRollback({
fromVersion: currentVersion.version,
toVersion: stableVersion.version,
planId: planId,
reason: reason,
executedBy: currentUserId,
rollbackType: 'EMERGENCY'
});
// 4. 알림 및 로깅
await notifyStakeholders({
type: 'EMERGENCY_ROLLBACK',
planId: planId,
fromVersion: currentVersion.version,
toVersion: stableVersion.version,
reason: reason,
affectedUsers: rollbackResult.affectedUserCount
});
// 5. 문제 버전을 DEPRECATED로 변경
await planVersionService.updateStatus(currentVersion.id, 'DEPRECATED');
return rollbackResult;
}
📊 4. 버전 간 변경사항 추적 및 비교
// 💡 버전 비교 및 영향 분석
async function compareVersions(planId: string, fromVersion: string, toVersion: string) {
const fromConfig = await planVersionRepo.getConfig(planId, fromVersion);
const toConfig = await planVersionRepo.getConfig(planId, toVersion);
const comparison = {
featuresChanged: compareFeatures(fromConfig.features, toConfig.features),
rulesChanged: compareBusinessRules(fromConfig.businessRules, toConfig.businessRules),
rolesChanged: compareRoles(fromConfig.roles, toConfig.roles),
serviceLevel: {
from: fromConfig.serviceLevel,
to: toConfig.serviceLevel,
changed: fromConfig.serviceLevel !== toConfig.serviceLevel
},
impactAssessment: await assessUpgradeImpact(fromConfig, toConfig)
};
return comparison;
}
interface VersionComparison {
featuresChanged: {
added: string[];
removed: string[];
modified: string[];
};
rulesChanged: {
businessLogic: string[];
accessControl: string[];
billing: string[];
};
rolesChanged: {
added: string[];
removed: string[];
};
serviceLevel: {
from: string;
to: string;
changed: boolean;
};
impactAssessment: {
userImpact: 'LOW' | 'MEDIUM' | 'HIGH';
dataImplications: string[];
requiredActions: string[];
estimatedMigrationTime: number; // 분 단위
};
}
🎛️ 5. A/B 테스트를 위한 버전 관리
// 💡 A/B 테스트 시나리오: 새로운 기능 효과 검증
async function setupABTest(planId: string, testConfig: ABTestConfig) {
// 1. 컨트롤 그룹용 현재 버전 유지
const controlVersion = await planVersionRepo.findActive(planId);
// 2. 실험 그룹용 새 버전 생성
const experimentVersion = await planVersionService.create({
templateId: planId,
version: `${controlVersion.version}-exp`,
config: {
...controlVersion.config,
features: {
...controlVersion.config.features,
...testConfig.experimentalFeatures
}
},
status: 'DRAFT',
changeLog: `A/B Test: ${testConfig.testName}`,
effectiveFrom: testConfig.startDate,
effectiveUntil: testConfig.endDate,
createdBy: currentUserId
});
// 3. 사용자 분할 및 버전 할당
const testSetup = await abTestService.setupTest({
controlVersion: controlVersion,
experimentVersion: experimentVersion,
splitRatio: testConfig.splitRatio, // 50:50, 70:30 등
targetCriteria: testConfig.userCriteria,
metrics: testConfig.metricsToTrack
});
return testSetup;
}
interface ABTestConfig {
testName: string;
experimentalFeatures: Partial<FeatureConfig>;
splitRatio: { control: number; experiment: number };
startDate: Date;
endDate: Date;
userCriteria: {
includeNewUsers?: boolean;
regions?: string[];
planTypes?: string[];
minUsageDays?: number;
};
metricsToTrack: string[]; // ['user_engagement', 'feature_adoption', 'retention_rate']
}
📅 6. 계절별/이벤트별 임시 버전 관리
// 💡 계절별 프로모션 플랜 시나리오
async function createSeasonalPromotion(basePlanId: string, promoConfig: PromoConfig) {
const baseVersion = await planVersionRepo.findActive(basePlanId);
// 프로모션 기간 동안만 유효한 특별 버전 생성
const promoVersion = await planVersionService.create({
templateId: basePlanId,
version: `${baseVersion.version}-${promoConfig.promoCode}`,
config: {
...baseVersion.config,
features: {
...baseVersion.config.features,
...promoConfig.bonusFeatures // 추가 혜택
},
businessRules: {
...baseVersion.config.businessRules,
billing: {
...baseVersion.config.businessRules.billing,
discountRate: promoConfig.discountRate,
promotionalPricing: promoConfig.pricing
}
}
},
status: 'ACTIVE',
changeLog: `Seasonal Promotion: ${promoConfig.name}`,
effectiveFrom: promoConfig.startDate,
effectiveUntil: promoConfig.endDate, // 자동 만료
createdBy: currentUserId
});
// 프로모션 종료 후 자동 롤백 스케줄링
await scheduleVersionRollback(promoVersion, baseVersion, promoConfig.endDate);
return promoVersion;
}
🛡️ 7. 버전 관리 베스트 프랙티스
// 💡 버전 관리 가이드라인
const VERSION_MANAGEMENT_RULES = {
// 버전 명명 규칙
naming: {
major: "대규모 기능 변경, 하위 호환성 깨짐", // 1.0.0 → 2.0.0
minor: "새 기능 추가, 하위 호환성 유지", // 1.0.0 → 1.1.0
patch: "버그 수정, 작은 개선", // 1.0.0 → 1.0.1
experimental: "A/B 테스트, 임시 기능" // 1.0.0-exp, 1.0.0-promo
},
// 상태 전환 규칙
statusTransition: {
DRAFT: ['ACTIVE', 'DEPRECATED'], // 초안 → 활성/폐기
ACTIVE: ['DEPRECATED'], // 활성 → 폐기만 가능
DEPRECATED: [] // 폐기 후 변경 불가
},
// 보존 정책
retention: {
activePeriod: '1_year', // 활성 버전 최소 1년 보관
deprecatedPeriod: '3_years', // 폐기 버전 3년 보관 (규정 준수)
auditTrail: 'permanent' // 감사 로그는 영구 보관
},
// 동시 활성 버전 제한
concurrent: {
maxActiveVersions: 1, // 플랜당 최대 1개 활성 버전
maxDraftVersions: 3, // 플랜당 최대 3개 초안 버전
allowExperimental: true // 실험적 버전 허용
}
};
이렇게 Plan Version은 단순한 이력 관리를 넘어서 실시간 서비스 운영, 점진적 기능 출시, 안전한 롤백, A/B 테스트, 계절별 프로모션 등 다양한 비즈니스 요구사항을 지원하는 핵심 인프라입니다! 🚀
2.6 설계 철학: 왜 Metadata와 Template 모델을 분리했는가? 🤔
핵심 설계 원칙: 관심사 분리 (Separation of Concerns)
Plan 도메인에서 핵심 엔티티와 메타데이터, 템플릿을 분리한 이유는 다음과 같습니다:
🔹 1. 변경 빈도의 차이
// ❌ 모든 것을 하나의 테이블에 섞은 경우
model Plan {
id: string // 거의 변경되지 않음
name: string // 가끔 변경됨
description: string // 가끔 변경됨
// 아래 필드들은 자주 변경됨 → 성능 이슈 발생
serviceLevel: string // 정책 변경 시마다 수정
features: Json // 새 기능 출시마다 수정
businessRules: Json // 비즈니스 규칙 변경마다 수정
billingInfo: Json // 가격 정책 변경마다 수정
}
// ✅ 변경 빈도별로 분리한 경우
model Plan {
id: string // 핵심 식별자 (불변)
name: string // 기본 정보 (안정적)
isActive: boolean // 상태 (가끔 변경)
}
model PlanMetadata {
planId: string
serviceLevel: string // 자주 변경되는 정책
features: Json // 자주 변경되는 기능 설정
businessRules: Json // 자주 변경되는 비즈니스 규칙
}
🔹 2. 성능 최적화
-- ❌ 무거운 JSON 필드들이 항상 조회됨
SELECT * FROM plans WHERE isActive = true;
-- → 불필요한 features, businessRules 등도 함께 조회
-- ✅ 필요할 때만 메타데이터 조회
-- 기본 조회: 빠름
SELECT id, name, isActive FROM plans WHERE isActive = true;
-- 상세 조회: 필요시에만
SELECT p.*, pm.* FROM plans p
LEFT JOIN plan_metadata pm ON p.id = pm.plan_id
WHERE p.id = 'plan.therapeutic';
🔹 3. 확장성과 유연성
// ❌ 새로운 비즈니스 규칙 추가 시 스키마 변경 필요
ALTER TABLE plans ADD COLUMN new_business_rule TEXT;
ALTER TABLE plans ADD COLUMN another_feature BOOLEAN;
// ✅ JSON 필드 내에서 유연하게 확장
UPDATE plan_metadata
SET business_rules = jsonb_set(
business_rules,
'{newRule}',
'"new_value"'::jsonb
);
SET features = jsonb_set(
features,
'{newFeature}',
'true'::jsonb
);
🔹 4. 템플릿화를 통한 일관성 보장
// 💡 실제 비즈니스 시나리오
async function createTherapeuticPlan(customConfig?: Partial<PlanConfig>) {
// 1. 표준 템플릿 로드
const template = await planTemplateRepo.findByCategory('THERAPEUTIC');
// 2. 기본 설정 + 커스텀 설정 병합
const config = {
...template.defaultConfig,
...customConfig
};
// 3. 일관된 플랜 생성
return await planService.create({
name: generatePlanName(),
category: 'THERAPEUTIC',
metadata: {
serviceLevel: config.serviceLevel, // 템플릿에서 가져온 표준값
features: config.features, // 표준 + 커스텀
businessRules: config.businessRules // 일관된 규칙 적용
}
});
}
// ✅ 결과: 모든 치료 플랜이 동일한 기준으로 생성됨
🔹 5. 버전 관리를 통한 안정성
// 💡 플랜 설정 변경 시나리오
async function updatePlanFeatures(planId: string, newFeatures: FeatureConfig) {
// 1. 현재 설정을 버전으로 백업
await planVersionService.createSnapshot(planId, {
version: "2.0.0",
changeLog: "Added new relaxation features",
config: currentConfig
});
// 2. 새 설정 적용
await planMetadataService.updateFeatures(planId, newFeatures);
// 3. 문제 발생 시 롤백 가능
if (problemDetected) {
await planVersionService.rollback(planId, "1.9.0");
}
}
📊 분리의 실제 효과
| 측면 | 통합 모델 | 분리 모델 |
|---|---|---|
| 조회 성능 | 느림 (불필요한 JSON 로드) | 빠름 (필요한 것만 로드) |
| 확장성 | 스키마 변경 필요 | JSON으로 유연한 확장 |
| 일관성 | 수동 설정 (실수 위험) | 템플릿으로 자동 보장 |
| 변경 추적 | 어려움 | 버전별 체계적 관리 |
| 테스트 | 복잡함 (모든 필드 목킹) | 단순함 (관심사별 테스트) |
이러한 설계를 통해 복잡한 비즈니스 로직의 체계적 관리, 성능 최적화, 운영 안정성, 개발 생산성을 모두 달성할 수 있습니다! 🎯
2.7 Role (Plan Domain View)
/**
* 역할 엔티티 (Plan 도메인 관점).
* 실제 데이터는 `iam.roles` 테이블에 저장되며, IAM 도메인이 관리합니다.
* Plan 도메인은 역할과 플랜의 연결 관계를 관리합니다.
*/
interface RolePlanView {
id: RoleId;
name: string;
description?: string;
isActive: boolean;
// IAM 도메인에서 관리하는 기타 필드 (permissions 등)
}
2.8 User (Plan Domain View)
/**
* 사용자 엔티티 (Plan 도메인 관점).
* 실제 데이터는 사용자 유형에 따라 다른 스키마에 저장됩니다:
* - 고객(환자) 사용자: `private.users` 테이블
* - 내부 운영자 사용자: `operation.users` 테이블
* Plan 도메인은 사용자-플랜 할당을 관리합니다.
*/
interface UserReference {
id: UserId;
userType: UserType;
// User 도메인에서 관리하는 기타 필드 (email, name 등)
}
3. 값 객체
3.1 PlanId
type PlanId = string; // UUID or other unique identifier
3.2 PlanTemplateId
type PlanTemplateId = string; // UUID or other unique identifier
3.3 PlanVersionId
type PlanVersionId = string; // UUID or other unique identifier
3.4 RoleId
type RoleId = string; // From IAM Domain
3.5 GroupId
type GroupId = string; // From Group Domain
3.6 PlanType
/**
* 플랜 유형을 정의하는 열거형.
*/
enum PlanType {
THERAPEUTIC = 'THERAPEUTIC', // 치료 플랜
LIMITED_ACCESS = 'LIMITED_ACCESS', // 제한 접근 플랜
SAMPLE = 'SAMPLE', // 샘플/체험 플랜
DEVELOPMENT = 'DEVELOPMENT', // 개발/테스트 플랜
CLINICIAN = 'CLINICIAN' // 의료진 플랜
}
3.7 PlanTemplateData
/**
* 플랜 템플릿의 데이터 구조.
*/
interface PlanTemplateData {
defaultRoleIds: RoleId[];
defaultMetadata: Record<string, string>;
planType: PlanType;
isActive: boolean;
}
3.8 PlanVersionData
/**
* 플랜 버전의 데이터 스냅샷.
*/
interface PlanVersionData {
name: string;
description?: string;
planType: PlanType;
roleIds: RoleId[];
metadata: Record<string, string>;
isActive: boolean;
}
4. 집계 (Aggregates)
4.1 Plan Aggregate
- 루트: Plan
- 엔티티: PlanRole (관계), PlanMetadata, PlanVersion, UserPlan (관계)
- 값 객체: PlanId, PlanName (unique), Description, PlanType, IsActive, RoleId[] (참조 ID 목록)
- 참조: Role (IAM 도메인, ID 목록을 통해 간접 참조), User (User 도메인, UserPlan을 통해)
- 불변식:
- Plan 이름은 고유해야 함
- 활성/비활성 상태 관리
- 플랜에는 최소 하나 이상의 역할이 할당되어야 함
- 플랜 유형은 변경 불가 (새 버전 생성으로 처리)
- 사용자는 한 시점에 하나의 활성 플랜만 가질 수 있음
4.2 PlanTemplate Aggregate
- 루트: PlanTemplate
- 값 객체: PlanTemplateId, PlanTemplateData
- 불변식:
- 템플릿 이름은 고유해야 함
- 템플릿 데이터는 유효한 구조여야 함
5. 도메인 서비스
5.1 PlanService
interface PlanService {
createPlan(name: string, planType: PlanType, description?: string, roles?: RoleId[]): Promise<Plan>;
getPlan(id: PlanId): Promise<Plan | null>;
getPlanByName(name: string): Promise<Plan | null>;
getAllPlans(filter?: { planType?: PlanType; isActive?: boolean }): Promise<Plan[]>;
updatePlan(id: PlanId, data: { name?: string; description?: string; isActive?: boolean }): Promise<Plan>;
deletePlan(id: PlanId): Promise<void>; // Soft delete or check dependencies
assignRoleToPlan(planId: PlanId, roleId: RoleId): Promise<void>;
revokeRoleFromPlan(planId: PlanId, roleId: RoleId): Promise<void>;
getPlanRoles(planId: PlanId): Promise<RolePlanView[]>;
validatePlanConfiguration(planId: PlanId): Promise<{ isValid: boolean; errors: string[] }>;
}
5.2 PlanMetadataService
interface PlanMetadataService {
setPlanMetadata(planId: PlanId, key: string, value: string): Promise<void>;
getPlanMetadata(planId: PlanId, key?: string): Promise<PlanMetadata[]>;
deletePlanMetadata(planId: PlanId, key: string): Promise<void>;
updatePlanMetadata(planId: PlanId, key: string, value: string): Promise<void>;
}
5.3 PlanTemplateService
interface PlanTemplateService {
createTemplate(name: string, description: string, templateData: PlanTemplateData): Promise<PlanTemplate>;
getTemplate(id: PlanTemplateId): Promise<PlanTemplate | null>;
getAllTemplates(filter?: { isActive?: boolean }): Promise<PlanTemplate[]>;
updateTemplate(id: PlanTemplateId, data: Partial<PlanTemplate>): Promise<PlanTemplate>;
deleteTemplate(id: PlanTemplateId): Promise<void>;
createPlanFromTemplate(templateId: PlanTemplateId, planName: string): Promise<Plan>;
}
5.4 PlanVersionService
interface PlanVersionService {
createVersion(planId: PlanId, version: string): Promise<PlanVersion>;
getPlanVersions(planId: PlanId): Promise<PlanVersion[]>;
restoreFromVersion(planId: PlanId, versionId: PlanVersionId): Promise<Plan>;
comparePlanVersions(versionId1: PlanVersionId, versionId2: PlanVersionId): Promise<PlanVersionComparison>;
}
5.5 PlanAssignmentService
/**
* Plan 도메인 관점에서 사용자와 플랜의 연결을 관리하는 서비스.
* User 도메인과 협력하여 플랜 할당을 처리합니다.
*/
interface PlanAssignmentService {
assignPlanToUser(userId: UserId, planId: PlanId, expiresAt?: Date): Promise<void>; // User 도메인과 협력
getUserPlan(userId: UserId): Promise<UserPlan | null>; // 현재 활성 플랜 조회
getUserPlanHistory(userId: UserId): Promise<UserPlan[]>; // 플랜 할당 이력 조회
getUsersByPlan(planId: PlanId): Promise<UserReference[]>;
upgradePlan(userId: UserId, newPlanId: PlanId): Promise<void>; // 플랜 업그레이드
downgradePlan(userId: UserId, newPlanId: PlanId): Promise<void>; // 플랜 다운그레이드
validatePlanAssignment(userId: UserId, planId: PlanId): Promise<boolean>;
getPlansAssignmentStatistics(): Promise<PlanAssignmentStats>;
}
6. 도메인 이벤트
6.1 Plan 생명주기 이벤트
- PlanCreated: 새로운 플랜 생성
- PlanUpdated: 플랜 정보 수정
- PlanDeleted: 플랜 삭제
- PlanActivated: 플랜 활성화
- PlanDeactivated: 플랜 비활성화
6.2 Plan-Role 관계 이벤트
- RoleAssignedToPlan: 플랜에 역할 할당
- RoleRevokedFromPlan: 플랜에서 역할 제거
- PlanRoleValidationFailed: 플랜-역할 관계 검증 실패
6.3 Plan 할당 이벤트
- PlanAssignedToGroup: 그룹에 플랜 할당
- PlanChangedForGroup: 그룹의 플랜 변경
- PlanUnassignedFromGroup: 그룹에서 플랜 할당 해제
6.4 Plan 메타데이터 이벤트
- PlanMetadataUpdated: 플랜 메타데이터 변경
- PlanTemplateCreated: 플랜 템플릿 생성
- PlanVersionCreated: 플랜 새 버전 생성
6.5 Plan 검증 이벤트
- PlanValidationFailed: 플랜 구성 검증 실패
- PlanConfigurationChanged: 플랜 구성 변경
7. 도메인 규칙
상세 내용은 business-rules.md 참조
주요 규칙 카테고리:
- 플랜 정의 규칙: 플랜 목적, 구성 요소, 유형별 분류
- 플랜 유형 규칙: 치료, 제한 접근, 샘플, 개발, 클리니션 플랜
- 플랜-역할 관계 규칙: 역할 할당/해제, 검증
- 플랜 할당 규칙: 그룹-플랜 연결, 사용자 영향도 분석
- 플랜 생명주기 규칙: 생성, 수정, 삭제, 상태 관리
- 플랜 버전 관리 규칙: 버전 생성, 롤백, 호환성
- 플랜 템플릿 규칙: 템플릿 생성, 플랜 생성
- 감사 및 보안 규칙: 감사 로그, 접근 제어
8. 데이터베이스 스키마 (Prisma)
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
schemas = ["plan", "iam", "private", "operation"]
}
// --- 외부 도메인 모델 (참조용 stub) ---
// Role 모델 (iam 스키마)
// 실제 정의는 iam/domain-model.md 참조
model Role {
id String @id @map("id")
name String @unique
// ... other fields defined in IAM domain
planRoles PlanRole[]
@@map("roles")
@@schema("iam")
}
// User 모델 (private/operation 스키마)
// 실제 정의는 user/domain-model.md 참조
model User {
id String @id @map("id")
userType String @map("user_type")
// ... other fields defined in User domain
userPlans UserPlan[] @relation
@@map("users")
@@schema("private") // 또는 operation 스키마
}
// --- Plan 도메인 모델 (plan 스키마) ---
// Plan 모델
model Plan {
id String @id @default(uuid()) @map("id")
name String @unique
description String?
planType String @map("plan_type") // PlanType enum
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
// 관계
planRoles PlanRole[]
userPlans UserPlan[] // Plan 에 할당된 사용자들
metadata PlanMetadata[]
versions PlanVersion[]
@@map("plans")
@@schema("plan")
}
// Plan-Role 관계 테이블
model PlanRole {
planId String @map("plan_id")
roleId String @map("role_id") // iam.roles(id) 참조
assignedAt DateTime @default(now()) @map("assigned_at")
// 관계
plan Plan @relation(fields: [planId], references: [id], onDelete: Cascade)
role Role @relation(fields: [roleId], references: [id], onDelete: Cascade)
@@id([planId, roleId])
@@map("plan_roles")
@@schema("plan")
}
// User-Plan 관계 테이블
model UserPlan {
userId String @map("user_id") // private.users(id) | operation.users(id)
planId String @map("plan_id")
assignedAt DateTime @default(now()) @map("assigned_at")
expiresAt DateTime? @map("expires_at") // 플랜 만료일
isActive Boolean @default(true) @map("is_active")
assignedBy String? @map("assigned_by") // 플랜 할당 담당자
// 관계
plan Plan @relation(fields: [planId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@id([userId]) // 사용자는 한 시점에 하나의 활성 플랜만 가짐
@@map("user_plans")
@@schema("plan")
}
// Plan 메타데이터 모델
model PlanMetadata {
planId String @map("plan_id")
metadataKey String @map("metadata_key")
metadataValue String @map("metadata_value")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
// 관계
plan Plan @relation(fields: [planId], references: [id], onDelete: Cascade)
@@id([planId, metadataKey])
@@map("plan_metadata")
@@schema("plan")
}
// Plan 템플릿 모델
model PlanTemplate {
id String @id @default(uuid()) @map("id")
name String @unique
description String?
templateData Json @map("template_data") // PlanTemplateData
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@map("plan_templates")
@@schema("plan")
}
// Plan 버전 모델
model PlanVersion {
id String @id @default(uuid()) @map("id")
planId String @map("plan_id")
version String
versionData Json @map("version_data") // PlanVersionData
createdAt DateTime @default(now()) @map("created_at")
// 관계
plan Plan @relation(fields: [planId], references: [id], onDelete: Cascade)
@@unique([planId, version])
@@map("plan_versions")
@@schema("plan")
}
9. 도메인 간 협력
9.1 IAM 도메인과의 협력
- 역할 정보 조회: Plan 도메인은 IAM 도메인으로부터 역할 정보를 조회
- 권한 계산 지원: 플랜 정보를 IAM 도메인에 제공하여 사용자 권한 계산 지원
9.2 Group 도메인과의 협력
- 플랜 할당: Group 도메인과 협력하여 그룹에 플랜 할당
- 할당 상태 조회: 플랜별 그룹 할당 현황 조회
9.3 User 도메인과의 협력
- 사용자 영향도 분석: 플랜 변경 시 영향받는 사용자 분석
- 서비스 수준 제공: 사용자의 서비스 이용 수준 정의
10. 변경 이력
| 버전 | 날짜 | 작성자 | 변경 내용 |
|---|---|---|---|
| 0.1.0 | 2025-07-15 | bok@weltcorp.com | Plan 도메인 모델 초기 생성 (IAM 도메인에서 분리) |
| 0.2.0 | 2025-07-16 | bok@weltcorp.com | Group-Plan 독립성 확보: Group 참조 제거, UserPlan 추가, 사용자 직접 할당 방식으로 변경 |