본문으로 건너뛰기

Plan 도메인 모델

데이터 저장 규칙

  • Plan 도메인 관련 모든 데이터는 plan PostgreSQL 스키마에 저장되어야 합니다. (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 참조

주요 규칙 카테고리:

  1. 플랜 정의 규칙: 플랜 목적, 구성 요소, 유형별 분류
  2. 플랜 유형 규칙: 치료, 제한 접근, 샘플, 개발, 클리니션 플랜
  3. 플랜-역할 관계 규칙: 역할 할당/해제, 검증
  4. 플랜 할당 규칙: 그룹-플랜 연결, 사용자 영향도 분석
  5. 플랜 생명주기 규칙: 생성, 수정, 삭제, 상태 관리
  6. 플랜 버전 관리 규칙: 버전 생성, 롤백, 호환성
  7. 플랜 템플릿 규칙: 템플릿 생성, 플랜 생성
  8. 감사 및 보안 규칙: 감사 로그, 접근 제어

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.02025-07-15bok@weltcorp.comPlan 도메인 모델 초기 생성 (IAM 도메인에서 분리)
0.2.02025-07-16bok@weltcorp.comGroup-Plan 독립성 확보: Group 참조 제거, UserPlan 추가, 사용자 직접 할당 방식으로 변경