본문으로 건너뛰기

Agent Data 도메인 모델

데이터 저장 규칙

  • Agent Data는 사용자별 개인화 데이터를 저장하는 도메인으로 모든 데이터는 사용자 ID와 연결되어 있습니다.
  • Firestore의 계층적 구조를 활용하여 사용자별 서브컬렉션으로 데이터를 조직화합니다.
  • 실시간 처리 데이터와 집계 데이터를 별도 컬렉션으로 분리하여 쿼리 성능을 최적화합니다.
  • 모든 문서는 스키마 버전을 포함하여 향후 마이그레이션을 지원합니다.
  • GDPR 준수를 위해 사용자 삭제 시 관련된 모든 서브컬렉션이 함께 삭제될 수 있도록 구조화합니다.
  • 문서 크기 제한(1MB)을 고려하여 큰 데이터는 여러 문서로 분할 저장합니다.

1. 엔티티 관계도 (ERD)

2. 엔티티

2.1 TransformedSleepData

interface TransformedSleepData {
id: string;
userId: string;
originalEventId: string;
date: string; // YYYY-MM-DD
sleepData: {
bedTime: string;
wakeTime: string;
totalSleepTime: number; // minutes
sleepEfficiency: number; // percentage
sleepOnsetLatency: number; // minutes
wakeAfterSleepOnset: number; // minutes
numberOfAwakenings: number;
sleepQuality: number; // 1-5 scale
notes?: string;
};
contextInfo: {
treatmentDay: number;
dayOfWeek: string;
isWeekend: boolean;
};
schemaVersion: string;
createdAt: Date;
processedAt: Date;
}

2.2 TransformedQuestionnaireData

interface TransformedQuestionnaireData {
id: string;
userId: string;
originalEventId: string;
questionnaireType: QuestionnaireType;
responses: {
[key: string]: any; // question1Answer, question2Answer, etc.
};
scores: {
totalScore: number;
maxPossible: number;
subscores?: Record<string, number>;
severityLevel: string;
interpretation: string;
};
contextInfo: {
treatmentDay: number;
roundNumber: number;
};
schemaVersion: string;
completedAt: Date;
processedAt: Date;
}

enum QuestionnaireType {
ISI = 'ISI',
PSQI = 'PSQI',
PHQ9 = 'PHQ-9',
GAD7 = 'GAD-7'
}

2.3 TransformedLearningData

interface TransformedLearningData {
id: string;
userId: string;
originalEventId: string;
lessonId: string;
lessonContent: {
title: string;
category: string;
keyConcepts: string[];
learningObjectives: string[];
contentSummary: string;
};
progressData: {
timeSpentMinutes: number;
completionStatus: CompletionStatus;
engagementLevel: EngagementLevel;
};
knowledgeGained: {
newConcepts: string[];
skillsPracticed: string[];
comprehensionLevel: ComprehensionLevel;
};
schemaVersion: string;
completedAt: Date;
processedAt: Date;
}

enum CompletionStatus {
COMPLETED = 'COMPLETED',
IN_PROGRESS = 'IN_PROGRESS',
ABANDONED = 'ABANDONED'
}

enum EngagementLevel {
HIGH = 'HIGH',
MEDIUM = 'MEDIUM',
LOW = 'LOW'
}

enum ComprehensionLevel {
EXCELLENT = 'EXCELLENT',
GOOD = 'GOOD',
NEEDS_REVIEW = 'NEEDS_REVIEW'
}

2.4 TransformedMedicationData

interface TransformedMedicationData {
id: string;
userId: string;
originalEventId: string;
medicationId: string;
medicationProfile: {
medicationName: string;
dosagePerIntakeMg: number;
medicationForm: string;
intakeTimesOfDay: string[];
isPrescription: boolean;
prescriptionType?: 'Chronic' | 'Acute' | 'AsNeeded';
startDate: string;
endDate?: string;
indicationCategory?: string;
};
knowledgeGraphSummary: {
activeIngredients: string[];
therapeuticClass: string;
indicationTags: string[];
interactionRiskLevel?: 'Low' | 'Moderate' | 'High' | 'Severe';
pregnancyCategory?: string;
sideEffects?: string[];
};
dataCollectionMetadata: {
dataCollectionMethod: 'manual' | 'ocr' | 'external_import';
dataConfidenceLevel: 'high' | 'medium' | 'low';
ocrModelVersion?: string;
imageHash?: string;
needsVerification: boolean;
};
contextInfo: {
treatmentDay: number;
dayIndex: number;
};
schemaVersion: string;
capturedAt: Date;
processedAt: Date;
}

2.5 MedicationInteractionAlert

interface MedicationInteractionAlert {
id: string;
userId: string;
alertId: string;
interactions: {
medication1: string;
medication2: string;
interactionType: string;
severity: 'Low' | 'Moderate' | 'High' | 'Severe';
description: string;
clinicalRecommendation: string;
}[];
recommendations: {
actionRequired: boolean;
suggestedAction?: string;
consultPhysician: boolean;
monitoringRequired?: boolean;
};
contextInfo: {
dayIndex: number;
triggeredBy: string; // Which event triggered this alert
};
alertedAt: Date;
expiresAt?: Date;
}

2.6 MedicationRecommendation

interface MedicationRecommendation {
id: string;
userId: string;
dayIndex: number;
recommendations: {
type: 'taper' | 'discontinue' | 'timing_adjustment' | 'compliance_reminder';
medicationId: string;
medicationName: string;
suggestion: string;
rationale: string;
priority: 'high' | 'medium' | 'low';
}[];
taperPlan?: {
medicationId: string;
currentDosageMg: number;
targetDosageMg: number;
durationWeeks: number;
schedule: {
weekNumber: number;
dosageMg: number;
notes?: string;
}[];
};
contextInfo: {
treatmentDay: number;
basedOnData: string[]; // Which data sources informed this recommendation
};
generatedAt: Date;
validUntil?: Date;
}

2.7 MedicationDailySummary

interface MedicationDailySummary {
id: string;
userId: string;
dayIndex: number;
medicationsActive: {
totalCount: number;
prescriptionCount: number;
overTheCounterCount: number;
byTherapeuticClass: Record<string, number>;
};
complianceStatus: {
expectedIntakes: number;
recordedIntakes?: number;
complianceRate?: number; // percentage
missedDoses?: string[];
};
alertsSummary: {
interactionAlertsCount: number;
highSeverityAlertsCount: number;
recommendations: string[];
};
contextInfo: {
date: string;
treatmentDay: number;
};
createdAt: Date;
updatedAt?: Date;
}

2.8 UserInsights

interface UserInsights {
id: string;
userId: string;
insightType: InsightType;
insightDate: string; // YYYY-MM-DD
integratedAnalysis: {
overallStatus: string;
keyPatterns: string[];
correlations: CorrelationInsight[];
anomalies: string[];
};
recommendations: {
immediate: string[];
shortTerm: string[];
longTerm: string[];
};
keyHighlights: string[];
confidenceScore: number;
generatedAt: Date;
expiresAt: Date;
}

enum InsightType {
DAILY = 'DAILY',
WEEKLY = 'WEEKLY',
MONTHLY = 'MONTHLY',
INTEGRATED = 'INTEGRATED'
}

interface CorrelationInsight {
factor1: string;
factor2: string;
correlation: number;
significance: string;
interpretation: string;
}

2.5 AggregatedData

interface AggregatedData {
id: string;
userId: string;
aggregationType: AggregationType;
period: string; // e.g., "2024-02-14", "2024-W07", "2024-02"
domainData: {
sleep?: any;
questionnaires?: any;
learning?: any;
};
crossDomainAnalysis: {
patterns: string[];
trends: TrendAnalysis[];
insights: string[];
};
metadata: {
dataPoints: number;
completeness: number; // percentage
lastUpdated: Date;
};
createdAt: Date;
}

enum AggregationType {
DAILY = 'DAILY',
WEEKLY = 'WEEKLY',
MONTHLY = 'MONTHLY'
}

interface TrendAnalysis {
metric: string;
direction: 'IMPROVING' | 'STABLE' | 'DECLINING';
changePercent: number;
significance: string;
}

2.6 EventProcessingLog

interface EventProcessingLog {
id: string;
userId: string;
eventId: string;
sourceDomain: SourceDomain;
eventType: string;
processingStatus: ProcessingStatus;
processingDuration: number; // milliseconds
errorDetails?: {
errorCode: string;
errorMessage: string;
stackTrace?: string;
};
retryCount: number;
receivedAt: Date;
processedAt?: Date;
}

enum SourceDomain {
SLEEP = 'SLEEP',
QUESTIONNAIRE = 'QUESTIONNAIRE',
LEARNING = 'LEARNING',
MEDICATION = 'MEDICATION'
}

enum ProcessingStatus {
RECEIVED = 'RECEIVED',
VALIDATING = 'VALIDATING',
TRANSFORMING = 'TRANSFORMING',
STORING = 'STORING',
COMPLETED = 'COMPLETED',
FAILED = 'FAILED',
RETRYING = 'RETRYING'
}

2.7 ProcessingMetrics

interface ProcessingMetrics {
id: string;
metricDate: string; // YYYY-MM-DD
performanceMetrics: {
totalEventsProcessed: number;
averageProcessingTime: number;
p95ProcessingTime: number;
failureRate: number;
retryRate: number;
};
dataQualityMetrics: {
completenessScore: number;
accuracyScore: number;
consistencyScore: number;
dataAnomalies: number;
};
systemHealthMetrics: {
uptime: number;
apiAvailability: number;
firestoreLatency: number;
errorRate: number;
};
createdAt: Date;
}

3. 값 객체

3.1 SleepMetrics

interface SleepMetrics {
efficiency: number;
duration: number;
quality: number;
continuity: number;
}

3.2 TreatmentContext

interface TreatmentContext {
treatmentDay: number;
treatmentPhase: TreatmentPhase;
weekInTreatment: number;
}

enum TreatmentPhase {
EARLY = 'EARLY', // Days 1-14
ACTIVE = 'ACTIVE', // Days 15-60
MAINTENANCE = 'MAINTENANCE', // Days 61-90
COMPLETION = 'COMPLETION' // Days 91+
}

3.3 DataQualityScore

interface DataQualityScore {
completeness: number; // 0-100
accuracy: number; // 0-100
consistency: number; // 0-100
overall: number; // 0-100
}

3.4 InsightConfidence

interface InsightConfidence {
score: number; // 0-100
factors: {
dataCompleteness: number;
patternStrength: number;
sampleSize: number;
};
}

3.5 TimeRange

interface TimeRange {
start: Date;
end: Date;
duration: number; // days
}

3.6 FieldMapping

interface FieldMapping {
sourceField: string;
targetField: string;
transformation?: TransformationType;
}

enum TransformationType {
DIRECT = 'DIRECT',
RENAME = 'RENAME',
CALCULATE = 'CALCULATE',
AGGREGATE = 'AGGREGATE'
}

3.7 AnalysisResult

interface AnalysisResult {
metric: string;
value: number;
unit: string;
interpretation: string;
confidence: number;
}

3.8 PatternDetection

interface PatternDetection {
patternType: string;
strength: number; // 0-1
occurrences: number;
description: string;
relevance: 'HIGH' | 'MEDIUM' | 'LOW';
}

4. 집계 (Aggregates)

  • 사용자 데이터 변환 (UserDataTransformation)

    • Root: TransformedSleepData, TransformedQuestionnaireData, TransformedLearningData, TransformedMedicationData
    • Value Objects: SleepMetrics, TreatmentContext, FieldMapping
    • 책임: 원본 도메인 데이터를 LLM 친화적 형태로 변환
  • 데이터 집계 (DataAggregation)

    • Root: AggregatedData
    • Entities: DailySleepSummary, WeeklySleepSummary, MonthlySleepSummary, MedicationDailySummary
    • Value Objects: TimeRange, AnalysisResult
    • 책임: 시계열 데이터의 집계 및 트렌드 분석
  • 인사이트 생성 (InsightGeneration)

    • Root: UserInsights
    • Entities: QuestionnaireTrend, LearningProgress, KnowledgeBase, MedicationInteractionAlert, MedicationRecommendation
    • Value Objects: InsightConfidence, PatternDetection
    • 책임: 통합 인사이트 및 개인화된 권장사항 생성
  • 이벤트 처리 (EventProcessing)

    • Root: EventProcessingLog
    • Entities: ProcessingMetrics
    • Value Objects: DataQualityScore
    • 책임: 이벤트 수신, 검증, 변환, 저장 프로세스 관리

5. 도메인 서비스

5.1 EventSubscriptionService

interface EventSubscriptionService {
subscribeToSleepEvents(): Promise<void>;
subscribeToQuestionnaireEvents(): Promise<void>;
subscribeToLearningEvents(): Promise<void>;
subscribeToMedicationEvents(): Promise<void>;
handleIncomingEvent(event: DomainEvent): Promise<void>;
validateEventSchema(event: DomainEvent): Promise<boolean>;
checkEventDuplication(eventId: string): Promise<boolean>;
routeEventToProcessor(event: DomainEvent): Promise<void>;
}

5.2 DataTransformationService

interface DataTransformationService {
transformSleepData(rawData: any): Promise<TransformedSleepData>;
transformQuestionnaireData(rawData: any): Promise<TransformedQuestionnaireData>;
transformLearningData(rawData: any): Promise<TransformedLearningData>;
transformMedicationData(rawData: any): Promise<TransformedMedicationData>;
applyFieldMappings(data: any, mappings: FieldMapping[]): any;
enrichWithContext(data: any, userId: string): Promise<any>;
validateTransformedData(data: any, schema: string): boolean;
joinKnowledgeGraphData(medicationId: string): Promise<any>;
extractRelevantKnowledgeGraphFields(knowledgeGraph: any): any;
}

5.3 DataAggregationService

interface DataAggregationService {
aggregateDailySleepData(userId: string, date: string): Promise<DailySleepSummary>;
aggregateWeeklySleepData(userId: string, weekStart: string): Promise<WeeklySleepSummary>;
aggregateMonthlySleepData(userId: string, yearMonth: string): Promise<MonthlySleepSummary>;
aggregateDailyMedicationData(userId: string, dayIndex: number): Promise<MedicationDailySummary>;
calculateTrends(userId: string, timeRange: TimeRange): Promise<TrendAnalysis[]>;
generateCrossDomainAnalysis(userId: string, period: string): Promise<any>;
updateAggregatedData(userId: string, aggregationType: AggregationType): Promise<void>;
}

5.4 InsightGenerationService

interface InsightGenerationService {
generateDailyInsights(userId: string, date: string): Promise<UserInsights>;
generateWeeklyInsights(userId: string, weekStart: string): Promise<UserInsights>;
generateIntegratedInsights(userId: string): Promise<UserInsights>;
detectPatterns(userId: string, domain: string): Promise<PatternDetection[]>;
analyzeCorrelations(userId: string): Promise<CorrelationInsight[]>;
generateRecommendations(userId: string, analysis: any): Promise<string[]>;
calculateInsightConfidence(data: any): Promise<InsightConfidence>;
}

5.5 FirestoreStorageService

interface FirestoreStorageService {
storeTransformedData(collection: string, data: any): Promise<string>;
queryUserData(userId: string, collection: string, filters?: any): Promise<any[]>;
batchWriteDocuments(operations: BatchOperation[]): Promise<void>;
createUserSubcollection(userId: string, subcollection: string): Promise<void>;
archiveUserData(userId: string): Promise<void>;
restoreUserData(userId: string): Promise<void>;
deleteUserData(userId: string): Promise<void>;
handleDocumentSizeLimit(data: any): Promise<string[]>; // Returns array of document IDs
}

interface BatchOperation {
operation: 'CREATE' | 'UPDATE' | 'DELETE';
collection: string;
documentId?: string;
data?: any;
}

5.6 APIProviderService

interface APIProviderService {
getUserProfile(userId: string): Promise<any>;
getUserContext(userId: string): Promise<any>;
getSleepPatterns(userId: string, period: string): Promise<any>;
getSleepTrends(userId: string, compareWith: string): Promise<any>;
getQuestionnaireHistory(userId: string, type?: string): Promise<any>;
getQuestionnaireTrends(userId: string, type: string): Promise<any>;
getLearningProgress(userId: string): Promise<any>;
getLearningKnowledge(userId: string, category?: string): Promise<any>;
getIntegratedInsights(userId: string): Promise<any>;
validateAPIAccess(userId: string, requesterId: string): Promise<boolean>;
}

5.7 DataQualityService

interface DataQualityService {
assessDataQuality(data: any): Promise<DataQualityScore>;
detectAnomalies(userId: string, domain: string): Promise<string[]>;
validateDataConsistency(userId: string): Promise<boolean>;
generateQualityReport(userId: string): Promise<any>;
flagDataIssues(userId: string, issues: string[]): Promise<void>;
autoCorrectData(data: any, rules: any[]): Promise<any>;
}

5.8 MonitoringService

interface MonitoringService {
recordProcessingMetrics(event: any, duration: number): Promise<void>;
trackAPIUsage(endpoint: string, userId: string): Promise<void>;
monitorSystemHealth(): Promise<any>;
generatePerformanceReport(date: string): Promise<ProcessingMetrics>;
alertOnThresholdBreach(metric: string, value: number): Promise<void>;
trackDataVolume(userId: string): Promise<number>;
}

6. 도메인 이벤트

6.1 이벤트 수신 관련

interface ExternalEventReceived {
eventId: string;
sourceDomain: string;
eventType: string;
userId: string;
payload: any;
receivedAt: Date;
timestamp: Date;
}

interface EventValidationCompleted {
eventId: string;
isValid: boolean;
validationErrors?: string[];
timestamp: Date;
}

interface DuplicateEventDetected {
eventId: string;
originalProcessedAt: Date;
timestamp: Date;
}

6.2 데이터 변환 관련

interface DataTransformationStarted {
eventId: string;
userId: string;
transformationType: string;
timestamp: Date;
}

interface DataTransformationCompleted {
eventId: string;
userId: string;
documentId: string;
transformedFields: string[];
timestamp: Date;
}

interface DataTransformationFailed {
eventId: string;
userId: string;
errorCode: string;
errorMessage: string;
timestamp: Date;
}

6.3 저장 관련

interface FirestoreWriteCompleted {
documentId: string;
collection: string;
userId: string;
documentSize: number;
timestamp: Date;
}

interface FirestoreWriteFailed {
collection: string;
userId: string;
errorCode: string;
errorMessage: string;
timestamp: Date;
}

interface DocumentSplitRequired {
originalDocumentId: string;
splitInto: number;
reason: string;
timestamp: Date;
}

6.4 집계 관련

interface AggregationTriggered {
userId: string;
aggregationType: string;
period: string;
timestamp: Date;
}

interface DailyAggregationCompleted {
userId: string;
date: string;
metricsProcessed: number;
timestamp: Date;
}

interface TrendAnalysisCompleted {
userId: string;
analysisType: string;
trendsIdentified: string[];
timestamp: Date;
}

6.5 인사이트 관련

interface UserInsightsGenerated {
userId: string;
insightId: string;
insightType: string;
keyInsights: string[];
confidenceScore: number;
timestamp: Date;
}

interface PatternDetected {
userId: string;
patternType: string;
domain: string;
strength: number;
description: string;
timestamp: Date;
}

interface CorrelationDiscovered {
userId: string;
factor1: string;
factor2: string;
correlationValue: number;
significance: string;
timestamp: Date;
}

6.6 API 관련

interface APIRequestReceived {
requestId: string;
userId: string;
endpoint: string;
requesterId: string;
timestamp: Date;
}

interface APIResponseSent {
requestId: string;
userId: string;
responseTime: number;
statusCode: number;
timestamp: Date;
}

interface RateLimitExceeded {
requesterId: string;
endpoint: string;
limit: number;
windowSeconds: number;
timestamp: Date;
}

6.7 데이터 보관 관련

interface UserDataArchived {
userId: string;
archiveLocation: string;
dataSize: number;
reason: string;
timestamp: Date;
}

interface UserDataRestored {
userId: string;
restoreFrom: string;
dataSize: number;
duration: number;
timestamp: Date;
}

interface DataRetentionPolicyExecuted {
affectedUsers: number;
dataDeleted: number;
dataArchived: number;
timestamp: Date;
}

6.8 모니터링 관련

interface PerformanceThresholdBreached {
metric: string;
threshold: number;
actualValue: number;
severity: 'WARNING' | 'CRITICAL';
timestamp: Date;
}

interface DataQualityIssueDetected {
userId: string;
issueType: string;
affectedData: string;
severity: string;
timestamp: Date;
}

interface SystemHealthCheckCompleted {
status: 'HEALTHY' | 'DEGRADED' | 'UNHEALTHY';
metrics: any;
timestamp: Date;
}

7. Firestore 컬렉션 구조

// Root Collections
interface Collections {
users: {
// Document ID: userId
[userId: string]: {
treatmentStartDate: string;
treatmentPhase: string;
isActive: boolean;
lastActivityAt: Timestamp;
createdAt: Timestamp;
updatedAt: Timestamp;

// Subcollections
sleepData: {
// Document ID: YYYY-MM-DD
[date: string]: TransformedSleepData;
};

questionnaireData: {
// Document ID: auto-generated
[docId: string]: TransformedQuestionnaireData;
};

learningData: {
// Document ID: auto-generated
[docId: string]: TransformedLearningData;
};

medicationData: {
profiles: {
// Document ID: medicationId
[medicationId: string]: TransformedMedicationData;
};
interactions: {
// Document ID: alertId
[alertId: string]: MedicationInteractionAlert;
};
recommendations: {
// Document ID: dayIndex
[dayIndex: string]: MedicationRecommendation;
};
};

insights: {
daily: {
// Document ID: YYYY-MM-DD
[date: string]: UserInsights;
};
weekly: {
// Document ID: YYYY-Www (e.g., 2024-W07)
[week: string]: UserInsights;
};
integrated: {
// Document ID: auto-generated with timestamp
[docId: string]: UserInsights;
};
};

aggregations: {
sleep: {
daily: {
// Document ID: YYYY-MM-DD
[date: string]: DailySleepSummary;
};
weekly: {
// Document ID: YYYY-Www
[week: string]: WeeklySleepSummary;
};
monthly: {
// Document ID: YYYY-MM
[month: string]: MonthlySleepSummary;
};
};
questionnaires: {
trends: {
// Document ID: questionnaireType (ISI, PSQI, etc.)
[type: string]: QuestionnaireTrend;
};
};
learning: {
progress: {
// Document ID: 'current'
current: LearningProgress;
};
knowledge: {
// Document ID: 'base'
base: KnowledgeBase;
};
};
medication: {
daily: {
// Document ID: dayIndex
[dayIndex: string]: MedicationDailySummary;
};
};
};
};
};

eventLogs: {
// Document ID: auto-generated
[logId: string]: EventProcessingLog;
};

systemMetrics: {
daily: {
// Document ID: YYYY-MM-DD
[date: string]: ProcessingMetrics;
};
};
}

// Firestore Security Rules
const securityRules = `
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Users can only access their own data
match /users/{userId}/{document=**} {
allow read: if request.auth != null &&
(request.auth.uid == userId ||
request.auth.token.role == 'ai-agent' ||
request.auth.token.role == 'admin');
allow write: if request.auth != null &&
request.auth.token.role in ['ai-agent', 'admin'];
}

// Event logs - admin and system only
match /eventLogs/{document=**} {
allow read, write: if request.auth != null &&
request.auth.token.role in ['admin', 'system'];
}

// System metrics - read for admin, write for system
match /systemMetrics/{document=**} {
allow read: if request.auth != null &&
request.auth.token.role == 'admin';
allow write: if request.auth != null &&
request.auth.token.role == 'system';
}
}
}
`;

// Firestore Indexes
const firestoreIndexes = [
{
collectionGroup: "sleepData",
queryScope: "COLLECTION_GROUP",
fields: [
{ fieldPath: "userId", order: "ASCENDING" },
{ fieldPath: "date", order: "DESCENDING" }
]
},
{
collectionGroup: "questionnaireData",
queryScope: "COLLECTION_GROUP",
fields: [
{ fieldPath: "userId", order: "ASCENDING" },
{ fieldPath: "questionnaireType", order: "ASCENDING" },
{ fieldPath: "completedAt", order: "DESCENDING" }
]
},
{
collectionGroup: "learningData",
queryScope: "COLLECTION_GROUP",
fields: [
{ fieldPath: "userId", order: "ASCENDING" },
{ fieldPath: "completedAt", order: "DESCENDING" }
]
},
{
collectionGroup: "medicationData",
queryScope: "COLLECTION_GROUP",
fields: [
{ fieldPath: "userId", order: "ASCENDING" },
{ fieldPath: "medicationId", order: "ASCENDING" },
{ fieldPath: "capturedAt", order: "DESCENDING" }
]
},
{
collectionGroup: "interactions",
queryScope: "COLLECTION_GROUP",
fields: [
{ fieldPath: "userId", order: "ASCENDING" },
{ fieldPath: "severity", order: "ASCENDING" },
{ fieldPath: "alertedAt", order: "DESCENDING" }
]
},
{
collectionId: "eventLogs",
fields: [
{ fieldPath: "userId", order: "ASCENDING" },
{ fieldPath: "processingStatus", order: "ASCENDING" },
{ fieldPath: "receivedAt", order: "DESCENDING" }
]
}
];

8. PostgreSQL JSONB 마이그레이션

Status: Implementation Complete (Phase 1-4) 상세 문서: migration-plan.md

8.1 마이그레이션 배경

현재 Firestore 기반 저장소의 성능 및 비용 문제를 해결하기 위해 PostgreSQL JSONB로 마이그레이션합니다.

8.2 PostgreSQL 스키마

enum AgentDataDomain {
QUESTIONNAIRE
SLEEP
JOURNAL
CONDITION
MEDICATION
RELAXATION
LEARNING
USER_CONTEXT
EXTERNAL_HEALTH

@@schema("agent_data")
}

model AgentData {
id String @id @default(uuid())
userId String @map("user_id")
domain AgentDataDomain
dataType String @map("data_type") @db.VarChar(50)
referenceKey String @default("") @map("reference_key") @db.VarChar(255)
data Json
metadata Json @default("{}")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")

@@unique([userId, domain, dataType, referenceKey], name: "unique_user_domain_type_key")
@@index([userId, domain])
@@index([userId, domain, dataType])
@@index([createdAt(sort: Desc)])
@@map("agent_data")
@@schema("agent_data")
}

8.3 Firestore → PostgreSQL 매핑

Firestore 경로PostgreSQL 컬럼
questionnaire/{userId}/round_{N}/{type}domain='QUESTIONNAIRE', reference_key='{N}_{type}'
sleep/sleepLog/{userId}/{dayIndex}domain='SLEEP', data_type='sleep_log', reference_key='{dayIndex}'
journal/{userId}/entries/{id}domain='JOURNAL', data_type='entry', reference_key='{id}'
condition/{userId}/reports/{date}domain='CONDITION', data_type='report', reference_key='{date}'
medication/{userId}domain='MEDICATION', data_type='profile', reference_key=''
relaxation/{userId}/sessions/{id}domain='RELAXATION', data_type='session', reference_key='{id}'
learning/{userId}/lessons/{id}domain='LEARNING', data_type='progress', reference_key='{id}'
userContext/{userId}domain='USER_CONTEXT', data_type='context', reference_key=''
externalHealth/{userId}domain='EXTERNAL_HEALTH', data_type='summary', reference_key=''

8.4 Repository 구조

libs/feature/agent-data/src/lib/infrastructure/repositories/
├── base-agent-data.repository.ts # PostgreSQL 공통 기능
├── prisma-transformed-sleep-data.repository.ts # Sleep 도메인 Prisma 구현
├── prisma-transformed-questionnaire-data.repository.ts # Questionnaire Prisma 구현
├── cached-prisma-sleep-data.repository.ts # PostgreSQL + Redis 캐시 (Phase 4)
├── unified-transformed-sleep-data.repository.ts # Dual-Write (Phase 2-3)
├── unified-transformed-questionnaire-data.repository.ts
├── firestore-*.repository.ts # Firestore 구현 (Legacy)
└── cached-transformed-sleep-data.repository.ts # Firestore + Redis (Phase 1)

8.5 Phase별 Repository 선택

Phase설정Sleep RepositoryQuestionnaire Repository
Phase 1enablePostgres: falseCachedTransformedSleepDataRepositoryFirestoreTransformedQuestionnaireDataRepository
Phase 2enablePostgres: true, primarySource: firestoreUnifiedTransformedSleepDataRepositoryUnifiedTransformedQuestionnaireDataRepository
Phase 3enablePostgres: true, primarySource: postgresUnifiedTransformedSleepDataRepositoryUnifiedTransformedQuestionnaireDataRepository
Phase 4enableFirestore: falseCachedPrismaTransformedSleepDataRepositoryPrismaTransformedQuestionnaireDataRepository

8.6 환경 설정

# Phase 1: Firestore만 사용 (기본값)
agentData:
dualWrite:
enableFirestore: true
enablePostgres: false
primarySource: firestore

# Phase 2: Dual-Write 시작
agentData:
dualWrite:
enableFirestore: true
enablePostgres: true
primarySource: firestore

# Phase 3: PostgreSQL Primary
agentData:
dualWrite:
enableFirestore: true
enablePostgres: true
primarySource: postgres

# Phase 4: PostgreSQL Only
agentData:
dualWrite:
enableFirestore: false
enablePostgres: true
primarySource: postgres

8.7 마이그레이션 서비스

// 마이그레이션 실행
const result = await migrationService.runMigration({
domains: ['sleep', 'questionnaire'],
batchSize: 100,
dryRun: false,
});

// 데이터 검증
const validation = await migrationService.validateMigration(['all'], 100);

8.8 기대 효과

메트릭Before (Firestore)After (PostgreSQL)
user_profile_summary 응답1-5초100-300ms
네트워크 왕복9-15회1-3회
월간 비용$70-250~$5
데이터 정합성Eventually ConsistentStrong Consistent

9. 변경 이력

버전날짜작성자변경 내용
0.1.02025-06-01bok@weltcorp.com최초 작성
0.2.02025-11-10bok@weltcorp.comMedication 도메인 데이터 수집 및 변환 추가 (TransformedMedicationData, MedicationInteractionAlert, MedicationRecommendation, MedicationDailySummary 엔티티 추가)
0.3.02026-01-12claudePostgreSQL JSONB 마이그레이션 계획 추가 (Section 8)
0.4.02026-01-12claudePostgreSQL 마이그레이션 구현 완료 (Phase 1-4), Repository 구조 및 환경 설정 문서화