Learning 도메인 모델
데이터 저장 규칙
- 개인정보 관련 데이터는 private 스키마에 저장되어야 합니다.
- 비개인정보 데이터는 learning 스키마에 저장되어야 합니다.
- 개인정보 포함 여부에 따라 위 규칙을 우선적으로 적용합니다.
- Learning 도메인에서는 LearningProgress, LessonCompletion, QuizResponse, TextSetting, StartDate, LessonStatus 등 사용자 ID와 연결된 데이터는 모두 개인정보로 취급하여 private 스키마에 저장합니다.
- 사용자와 연결되지 않은 데이터(Lesson, LearningSession, Quiz 등)만 learning 스키마에 저장합니다.
1. 엔티티 관계도 (ERD)
2. 엔티티
2.1 Lesson
interface Lesson {
id: string;
sessionId: string;
orderIndex: number;
label: number; // 일차별 추천 레슨 라벨 (1일차=1, 2일차=2, ...)
isFinalLesson: boolean; // 최종 레슨 여부 (치료 기간 완료시 해금되는 특별 레슨)
version: string;
isActive: boolean;
createdAt: Date;
updatedAt: Date;
// 관계
session: LearningSession;
translations: LessonTranslation[];
bundleLessons: BundleLesson[]; // 이 레슨이 포함된 번들들
}
2.2 LessonTranslation
interface LessonTranslation {
id: string;
lessonId: string;
language: string;
title: string;
content: string; // 레슨 콘텐츠 JSON 파일 경로
createdAt: Date;
updatedAt: Date;
// 관계
lesson: Lesson;
}
2.3 LearningSession
interface LearningSession {
id: string;
orderIndex: number;
isActive: boolean;
createdAt: Date;
updatedAt: Date;
// 관계
lessons: Lesson[];
translations: LearningSessionTranslation[];
}
2.4 LearningSessionTranslation
interface LearningSessionTranslation {
id: string;
sessionId: string;
language: string;
name: string;
title: string;
description?: string;
createdAt: Date;
updatedAt: Date;
// 관계
session: LearningSession;
}
2.5 LearningProgress
interface LearningProgress {
id: string;
userId: string;
userCycleId: string;
completedLessons: number;
progressPercentage: number;
lastActivityAt: Date;
createdAt: Date;
updatedAt: Date;
}
2.6 LessonCompletion
interface LessonCompletion {
id: string;
userId: string;
userCycleId: string;
label: number; // 일차별 추천 레슨 라벨 (1일차=1, 2일차=2, ...)
completedAt: Date;
createdAt: Date;
}
2.7 LearningStartDate
interface LearningStartDate {
id: string;
userId: string;
userCycleId: string;
startDate: Date;
createdAt: Date;
updatedAt: Date;
}
2.8 Quiz
interface Quiz {
id: string;
version: string;
quizType: QuizType;
relatedLessonId: string;
isActive: boolean;
createdAt: Date;
updatedAt: Date;
// 관계
translations: QuizTranslation[];
options: QuizOption[];
bundleQuizzes: BundleQuiz[]; // 이 퀴즈가 포함된 번들들
responses: QuizResponse[]; // private 스키마 관계
}
enum QuizType {
TRUE_FALSE = 'TRUE_FALSE',
SINGLE_CHOICE = 'SINGLE_CHOICE',
}
2.9 QuizTranslation
interface QuizTranslation {
id: string;
quizId: string;
language: string;
question: string;
explanation: string;
createdAt: Date;
updatedAt: Date;
// 관계
quiz: Quiz;
}
2.10 QuizOption
interface QuizOption {
id: string;
quizId: string;
value: string;
isCorrectValue: boolean;
orderIndex: number;
createdAt: Date;
updatedAt: Date;
// 관계
quiz: Quiz;
translations: QuizOptionTranslation[];
}
2.11 QuizOptionTranslation
interface QuizOptionTranslation {
id: string;
optionId: string;
language: string;
text: string;
createdAt: Date;
updatedAt: Date;
// 관계
option: QuizOption;
}
2.12 QuizResponse
interface QuizResponse {
id: string;
userId: string;
userCycleId: string;
quizId: string;
quizVersion: string;
selectedOptionId: string; // FK to QuizOption.id
selectedValue: string; // Actual option value for preservation
isCorrect: boolean;
answeredAt: Date;
createdAt: Date;
// 관계
quiz: Quiz;
}
2.13 TextSetting
interface TextSetting {
id: string;
userId: string;
userCycleId: string;
fontSize: number;
ttsEnabled: boolean;
ttsLanguage: string;
ttsSpeed: TTSSpeed;
createdAt: Date;
updatedAt: Date;
}
enum TTSSpeed {
SLOW = 'SLOW',
NORMAL = 'NORMAL',
FAST = 'FAST',
}
2.16 Media (사용 중단 - 모바일 앱에서 관리)
// interface Media {
// id: string;
// lessonId: string;
// mediaType: MediaType;
// url: string;
// metadata: MediaMetadata;
// createdAt: Date;
// updatedAt: Date;
// }
// enum MediaType {
// IMAGE = 'IMAGE',
// VIDEO = 'VIDEO',
// AUDIO = 'AUDIO',
// }
2.17 MediaTranslation (사용 중단 - 모바일 앱에서 관리)
// interface MediaTranslation {
// id: string;
// mediaId: string;
// language: string;
// altText?: string;
// caption?: string;
// description?: string;
// createdAt: Date;
// updatedAt: Date;
// }
2.18 LessonStatus
interface LessonStatus {
id: string;
userId: string;
userCycleId: string;
lessonId: string;
label: number; // 일차별 추천 레슨 라벨 (1일차=1, 2일차=2, ...)
status: LessonStatusType;
unlockedAt?: Date;
lastAccessedAt?: Date;
completedAt?: Date;
createdAt: Date;
updatedAt: Date;
}
/**
* 레슨 상태 유형
*
* LOCKED: 아직 해금되지 않은 상태
* UNLOCKED: 해금되어 열람 가능한 상태
* BLOCKED: 해금되었지만 특정 조건으로 열람 불가 상태
* IN_PROGRESS: UNLOCKED 상태에서 사용자가 실제로 열람 중인 상태
* COMPLETED: 열람을 완료한 상태
*
* 참고: IN_PROGRESS는 UNLOCKED의 확장 상태이며, 두 상태 모두 콘텐츠 접근 권한 있음
*/
enum LessonStatusType {
LOCKED = 'LOCKED',
UNLOCKED = 'UNLOCKED',
BLOCKED = 'BLOCKED',
IN_PROGRESS = 'IN_PROGRESS',
COMPLETED = 'COMPLETED',
}
2.15 LearningContentBundle
/**
* 학습 콘텐츠 번들 엔티티 (모바일 캐싱용)
* 활성화된 학습 콘텐츠들의 묶음을 저장합니다.
*/
interface LearningContentBundle {
id: string;
version: string;
generatedAt: Date;
isActive: boolean;
createdAt: Date;
updatedAt: Date;
// 관계
bundleLessons: BundleLesson[];
bundleQuizzes: BundleQuiz[];
}
2.16 BundleLesson
interface BundleLesson {
id: string;
bundleId: string;
lessonId: string;
// 관계
bundle: LearningContentBundle;
lesson: Lesson;
}
2.17 BundleQuiz
interface BundleQuiz {
id: string;
bundleId: string;
quizId: string;
// 관계
bundle: LearningContentBundle;
quiz: Quiz;
}
2.18 LessonLearningHistory
interface LessonLearningHistory {
id: string;
userId: string;
userCycleId: string;
lessonId: string;
label: number; // 일차별 추천 레슨 라벨 (1일차=1, 2일차=2, ...)
startedAt: Date;
endedAt?: Date;
durationSeconds?: number; // seconds
isCompleted: boolean;
deviceInfo?: Record<string, any>;
sessionId: string;
createdAt: Date;
updatedAt: Date;
// 관계
events: LearningEvent[];
}
2.19 LearningEvent
interface LearningEvent {
id: string;
userId: string;
userCycleId: string;
lessonId: string;
historyId?: string;
sessionId: string;
eventType: LearningEventType;
timestamp: Date;
deviceInfo?: Record<string, any>;
metadata?: Record<string, any>; // 추가 메타데이터 (예: 네트워크 상태, 배터리 상태 등)
createdAt: Date;
// 관계
history?: LessonLearningHistory;
}
enum LearningEventType {
START = 'START',
PAUSE = 'PAUSE',
RESUME = 'RESUME',
END = 'END',
}
2.20 LearningStatistics
interface LearningStatistics {
id: string;
userId: string;
userCycleId: string;
lessonId?: string; // null이면 전체 통계
label?: number; // 일차별 추천 레슨 라벨 (1일차=1, 2일차=2, ...)
statType: string; // daily, weekly, monthly, overall
periodStart?: Date;
periodEnd?: Date;
metrics: LearningMetrics; // LearningMetrics 데이터
calculatedAt: Date;
createdAt: Date;
}
3. 값 객체 (Value Objects)
3.1 LessonVersion
interface LessonVersion {
version: string;
createdAt: Date;
isActive: boolean;
}
3.2 LessonAccessInfo
interface LessonAccessInfo {
isAccessible: boolean;
requiredLessons: string[];
unlockDate?: Date;
restrictions?: string[];
}
3.3 UnlockInfo
interface UnlockInfo {
isUnlocked: boolean;
unlockedAt?: Date;
requiredConditions: string[];
}
3.4 SessionInfo
interface SessionInfo {
sessionId: string;
name: string;
description?: string;
totalLessons: number;
completedLessons: number;
progressPercentage: number;
}
3.5 ProgressValue
interface ProgressValue {
current: number;
total: number;
percentage: number;
lastUpdated: Date;
}
3.6 QuizInfo
interface QuizInfo {
quizId: string;
question: string;
options: string[];
correctAnswer: string;
explanation: string;
relatedLessonId?: string;
}
3.7 QuizResult
interface QuizResult {
quizId: string;
selectedAnswer: string;
isCorrect: boolean;
correctAnswer: string;
explanation: string;
timestamp: Date;
}
3.8 TextSize
interface TextSize {
size: number;
unit: 'px' | 'em' | 'rem';
isValid: boolean;
}
3.9 TTSSetting
interface TTSSetting {
enabled: boolean;
language: string;
speed: TTSSpeed;
voice?: string;
}
3.10 LearningContentVersionStatus
interface LearningContentVersionStatus {
version: string;
isActive: boolean;
generatedAt: Date;
contentCount: {
sessions: number;
lessons: number;
quizzes: number;
};
}
3.11 DeviceInfo
interface DeviceInfo {
platform: string;
version: string;
model?: string;
screenSize?: string;
networkType?: string;
batteryLevel?: number;
}
3.12 LearningMetrics
interface LearningMetrics {
averageDuration: number;
minDuration: number;
maxDuration: number;
totalSessions: number;
completionRate: number;
lastLearningDate?: Date;
}
3.13 LearningPattern
interface LearningPattern {
preferredTimeOfDay: string; // e.g., "morning", "afternoon", "evening"
averageSessionDuration: number;
frequencyPerWeek: number;
mostActiveDay: string;
}
3.14 LearningHistoryItem
interface LearningHistoryItem {
lessonId: string;
sessionId: string;
eventType: LearningEventType;
timestamp: Date;
metadata?: Record<string, any>;
}
4. 집계 (Aggregates)
-
수면탐구 레슨(SleepExplorationLesson)
- Root: Lesson
- Entities: LessonStatus
- Value Objects: UnlockInfo, LessonVersion, LessonAccessInfo
-
세션 관리(SessionManagement)
- Root: LearningSession
- Entities: Lesson
- Value Objects: SessionInfo
-
사용자 학습(UserLearning)
- Root: LearningProgress
- Entities: LessonCompletion, LearningStartDate, LessonLearningHistory
- Value Objects: ProgressValue, LearningMetrics, LearningPattern
-
퀴즈 세션(QuizSession)
- Root: Quiz
- Entities: QuizResponse, QuizStatus
- Value Objects: QuizInfo, QuizResult
-
사용자 설정(UserSettings)
- Root: TextSetting
- Value Objects: TextSize, TTSSetting
- 콘텐츠 번들(ContentBundle)
- Root: LearningContentBundle
- Entities: BundleLesson, BundleQuiz
- Value Objects: LearningContentVersionStatus
5. 도메인 서비스
5.1 LessonManagementService
interface LessonManagementService {
getLesson(lessonId: string): Promise<Lesson | null>;
getLessonByOrderIndex(orderIndex: number): Promise<Lesson | null>;
getUnlockedLessons(userId: string): Promise<Lesson[]>;
unlockLessonForUser(userId: string, lessonId: string): Promise<LessonStatus>;
checkLessonAccessible(userId: string, lessonId: string): Promise<boolean>;
getLessonMedia(lessonId: string): Promise<Media[]>;
getLessonVersion(lessonId: string): Promise<number>;
updateLessonVersion(lessonId: string): Promise<Lesson>;
updateLessonStatus(userId: string, lessonId: string, status: LessonStatusType): Promise<LessonStatus>;
trackLessonAccess(userId: string, lessonId: string): Promise<void>;
getLessonStatus(userId: string, lessonId: string): Promise<LessonStatus | null>;
// 보안 강화 메서드
requestSecureLessonContent(userId: string, lessonId: string, appToken: string): Promise<LessonContentResponse>;
validateLessonAccess(userId: string, lessonId: string, appToken: string): Promise<boolean>;
loadLessonContentFromGCS(lessonId: string, language: string): Promise<any>;
}
5.2 SessionManagementService
interface SessionManagementService {
getSession(sessionId: string): Promise<LearningSession | null>;
getLessonsBySession(sessionId: string): Promise<Lesson[]>;
getSessionProgress(userId: string, sessionId: string): Promise<number>;
isSessionCompleted(userId: string, sessionId: string): Promise<boolean>;
getAllSessions(): Promise<LearningSession[]>;
getSessionWithLessons(sessionId: string): Promise<{ session: LearningSession; lessons: Lesson[] }>;
}
5.3 ProgressManagementService
interface ProgressManagementService {
getUserProgress(userId: string): Promise<LearningProgress>;
markLessonAsCompleted(userId: string, lessonId: string): Promise<LessonCompletion>;
getCompletedLessons(userId: string): Promise<LessonCompletion[]>;
calculateProgressPercentage(userId: string): Promise<number>;
updateUserProgress(userId: string): Promise<LearningProgress>;
getUserStartDate(userId: string): Promise<Date | null>;
setUserStartDate(userId: string, startDate: Date): Promise<LearningStartDate>;
getSessionProgress(userId: string, sessionId: string): Promise<number>;
checkAllLessonsCompleted(userId: string): Promise<boolean>;
getTreatmentEndDate(userId: string): Promise<Date | null>;
}
5.4 TextSettingService
interface TextSettingService {
getUserTextSettings(userId: string): Promise<TextSetting>;
updateFontSize(userId: string, size: number): Promise<TextSetting>;
updateTTSSettings(userId: string, ttsSetting: TTSSetting): Promise<TextSetting>;
getDefaultTextSettings(): Promise<TextSetting>;
checkStorageCapacity(): Promise<{ available: boolean; freeSpace: number }>;
}
5.5 DailyContentService
interface DailyContentService {
// 일일 레슨 관련
getDailyLesson(userId: string): Promise<Lesson | null>;
getRecommendedLessonForDay(userId: string, dayIndex: number): Promise<Lesson | null>;
checkLessonEligibilityForDay(userId: string, dayIndex: number): Promise<boolean>;
// 일일 퀴즈 관련
getDailyQuiz(userId: string): Promise<Quiz | null>;
checkQuizEligibilityForDay(userId: string): Promise<boolean>;
// 공통 일일 콘텐츠 관련
getTodayAvailableContent(userId: string): Promise<{
lesson?: Lesson;
quiz?: Quiz;
contentType: 'lesson' | 'quiz' | 'both' | 'none';
}>;
// 추천 로직
calculateRecommendationForSlowProgress(userId: string): Promise<Lesson | null>;
calculateRecommendationForNormalProgress(userId: string): Promise<Lesson | null>;
trackUserPreferences(userId: string, contentId: string, contentType: 'lesson' | 'quiz', interactionType: string): Promise<void>;
}
5.6 QuizService
interface QuizService {
// 퀴즈 응답 및 이력 관리
answerQuiz(userId: string, quizId: string, answer: string): Promise<QuizResult>;
getUserQuizHistory(userId: string): Promise<QuizResponse[]>;
getQuizAccuracyRate(userId: string): Promise<number>;
retryQuiz(quizResponseId: string): Promise<void>;
canAccessQuizzes(userId: string): Promise<boolean>;
getRelatedLesson(quizId: string): Promise<Lesson | null>;
getAvailableQuizzes(userId: string): Promise<Quiz[]>;
// 퀴즈 상태 관리
getQuizStatus(userId: string, quizId: string): Promise<QuizStatus | null>;
updateQuizStatus(userId: string, quizId: string, status: QuizStatusType): Promise<QuizStatus>;
unlockQuizForUser(userId: string, quizId: string): Promise<QuizStatus>;
checkQuizAccessible(userId: string, quizId: string): Promise<boolean>;
getUnlockedQuizzes(userId: string): Promise<Quiz[]>;
trackQuizAccess(userId: string, quizId: string): Promise<void>;
// 퀴즈 버전 관리
createQuizVersion(quizId: string, versionData: QuizVersionData): Promise<Quiz>;
activateQuizVersion(quizId: string, version: number): Promise<Quiz>;
getQuizByVersion(quizId: string, version: number): Promise<Quiz | null>;
getQuizVersionHistory(quizId: string): Promise<Quiz[]>;
// 퀴즈 성능 분석
analyzeQuizPerformance(quizId: string, version?: number): Promise<QuizPerformanceData>;
compareQuizVersions(quizId: string, versionA: number, versionB: number): Promise<QuizVersionComparison>;
}
interface QuizPerformanceData {
quizId: string;
version: number;
correctAnswerRate: number;
averageResponseTime: number;
totalResponses: number;
}
interface QuizVersionComparison {
quizId: string;
versionA: number;
versionB: number;
performanceMetrics: {
versionA: QuizPerformanceData;
versionB: QuizPerformanceData;
};
}
5.7 DataAccessService
interface DataAccessService {
validateUserAccess(userId: string, resourceId: string, resourceType: string): Promise<boolean>;
logDataAccess(userId: string, resourceId: string, resourceType: string): Promise<void>;
getAccessibleResources(userId: string, resourceType: string): Promise<string[]>;
}
5.8 SecureContentService
interface SecureContentService {
// 보안 콘텐츠 접근
requestLessonContent(userId: string, lessonId: string, appToken: string): Promise<LessonContentResponse>;
requestMediaContent(userId: string, mediaId: string, appToken: string): Promise<SecureContentResponse>;
validateAppToken(token: string): Promise<AppTokenValidation>;
// 보안 검증
checkContentPermission(userId: string, contentId: string): Promise<boolean>;
auditContentAccess(userId: string, contentId: string, accessType: string): Promise<void>;
generateMediaSignedUrl(mediaId: string, userId: string): Promise<string>;
}
// 원본 JSON 파일을 직접 응답하므로 래핑 인터페이스 불필요
// GCS 파일 내용을 그대로 any 타입으로 반환
type LessonContentResponse = any; // 원본 lesson JSON 파일 내용
interface SecureContentResponse {
success: boolean;
signedUrl?: string;
expiresAt?: Date;
contentType?: string;
error?: string;
}
interface AppTokenValidation {
isValid: boolean;
deviceId?: string;
userId?: string;
expiresAt?: Date;
error?: string;
}
5.9 LearningContentBundleService
interface LearningContentBundleService {
// 번들 관리
getActiveBundle(): Promise<LearningContentBundle | null>;
createBundle(): Promise<LearningContentBundle>;
updateBundle(id: string, data: { version?: string; isActive?: boolean }): Promise<LearningContentBundle>;
activateBundle(id: string): Promise<LearningContentBundle>;
deactivateBundle(id: string): Promise<LearningContentBundle>;
// 번들 조회
getBundleById(id: string): Promise<LearningContentBundle | null>;
getBundleByVersion(version: string): Promise<LearningContentBundle | null>;
getAllBundles(): Promise<LearningContentBundle[]>;
// 버전 관리
getContentVersionStatus(contentIds: string[]): Promise<LearningContentVersionStatus[]>;
checkContentUpdates(lastSyncTime: number): Promise<{
hasUpdates: boolean;
updatedContent: LearningContentVersionStatus[];
}>;
// 번들 다운로드 및 동기화
generateBundleManifest(bundleId: string): Promise<BundleManifest>;
validateBundleIntegrity(bundleId: string): Promise<boolean>;
// 통계 및 분석
getBundleStatistics(bundleId: string): Promise<BundleStatistics>;
getClientSyncStatus(userId: string): Promise<ClientSyncStatus>;
}
interface BundleManifest {
bundleId: string;
version: string;
secureEndpoints: {
sessions: Map<string, string>; // sessionId -> secure API endpoint
lessons: Map<string, string>; // lessonId -> secure API endpoint
media: Map<string, string>; // mediaId -> secure API endpoint
quizzes: Map<string, string>; // quizId -> secure API endpoint
};
checksums: Map<string, string>; // resourceId -> checksum
totalSize: number;
generatedAt: Date;
requiresAppToken: boolean; // App Token 필요 여부
}
interface BundleStatistics {
bundleId: string;
downloadCount: number;
activeUsers: number;
averageDownloadTime: number;
errorRate: number;
}
interface ClientSyncStatus {
userId: string;
currentBundleVersion: string;
lastSyncTime: Date;
syncedContent: {
sessions: number;
lessons: number;
quizzes: number;
media: number;
};
pendingUpdates: number;
}
5.9 LearningHistoryService
interface LearningHistoryService {
// 학습 이력 기록
startLearning(userId: string, lessonId: string): Promise<LessonLearningHistory>;
endLearning(historyId: string, isCompleted: boolean): Promise<LessonLearningHistory>;
pauseLearning(historyId: string): Promise<void>;
resumeLearning(historyId: string): Promise<void>;
// 배치 처리 (네트워크 트래픽 최적화용)
processBatchHistory(histories: LearningHistoryItem[]): Promise<{ successCount: number; failedCount: number; errors: string[] }>;
// 학습 이력 조회
getLearningHistory(userId: string, lessonId?: string): Promise<LessonLearningHistory[]>;
getActiveLearningSession(userId: string): Promise<LessonLearningHistory | null>;
// 학습 통계
getLessonMetrics(lessonId: string): Promise<LearningMetrics>;
getUserLearningMetrics(userId: string): Promise<LearningMetrics>;
getUserLearningPattern(userId: string): Promise<LearningPattern>;
// 학습 소요시간 분석
getAverageDurationByLesson(lessonId: string): Promise<number>;
getTotalLearningTime(userId: string, startDate?: Date, endDate?: Date): Promise<number>;
getLearningTimeBySession(userId: string, sessionId: string): Promise<number>;
}
6. 도메인 이벤트
6.1 레슨 관련 이벤트
interface LessonUnlockedEvent {
userId: string;
lessonId: string;
unlockedAt: Date;
timestamp: Date;
}
interface LessonCompletedEvent {
userId: string;
lessonId: string;
completedAt: Date;
timestamp: Date;
}
interface AllLessonsCompletedEvent {
userId: string;
totalLessons: number;
completedAt: Date;
timestamp: Date;
}
interface LessonVersionUpdatedEvent {
lessonId: string;
oldVersion: number;
newVersion: number;
timestamp: Date;
}
interface LessonStatusChangedEvent {
userId: string;
lessonId: string;
oldStatus: LessonStatusType;
newStatus: LessonStatusType;
timestamp: Date;
}
interface LessonAccessTrackedEvent {
userId: string;
lessonId: string;
accessedAt: Date;
timestamp: Date;
}
interface FinalLessonUnlockedEvent {
userId: string;
lessonId: string;
treatmentEndDate: Date;
timestamp: Date;
}
6.2 세션 관련 이벤트
interface SessionCompletedEvent {
userId: string;
sessionId: string;
completedAt: Date;
timestamp: Date;
}
interface SessionProgressUpdatedEvent {
userId: string;
sessionId: string;
progressPercentage: number;
timestamp: Date;
}
6.3 설정 관련 이벤트
interface TextSettingsChangedEvent {
userId: string;
fontSize: number;
timestamp: Date;
}
interface TTSActivatedEvent {
userId: string;
language: string;
speed: TTSSpeed;
timestamp: Date;
}
interface TTSDeactivatedEvent {
userId: string;
timestamp: Date;
}
interface StorageCheckFailedEvent {
userId: string;
requiredSpace: number;
availableSpace: number;
timestamp: Date;
}
interface TTSAudioDownloadedEvent {
userId: string;
lessonId: string;
language: string;
fileSize: number;
timestamp: Date;
}
6.4 미디어 관련 이벤트
interface ImageViewerOpenedEvent {
userId: string;
mediaId: string;
lessonId: string;
timestamp: Date;
}
interface VideoPlaybackStartedEvent {
userId: string;
mediaId: string;
lessonId: string;
timestamp: Date;
}
interface AudioClipPlaybackStartedEvent {
userId: string;
mediaId: string;
lessonId: string;
timestamp: Date;
}
interface AudioClipPlaybackPausedEvent {
userId: string;
mediaId: string;
lessonId: string;
timestamp: Date;
}
interface AudioClipPlaybackResumedEvent {
userId: string;
mediaId: string;
lessonId: string;
timestamp: Date;
}
interface AudioClipPlaybackCompletedEvent {
userId: string;
mediaId: string;
lessonId: string;
timestamp: Date;
}
interface AudioClipPlaybackStoppedEvent {
userId: string;
mediaId: string;
lessonId: string;
timestamp: Date;
}
interface MediaLoadFailedEvent {
userId: string;
mediaId: string;
lessonId: string;
reason: string;
timestamp: Date;
}
interface MediaAccessValidatedEvent {
userId: string;
mediaId: string;
lessonId: string;
accessGranted: boolean;
timestamp: Date;
}
6.5 퀴즈 관련 이벤트
interface QuizAnsweredEvent {
userId: string;
quizId: string;
quizVersion: number;
selectedAnswer: string;
isCorrect: boolean;
timestamp: Date;
}
interface QuizResultShownEvent {
userId: string;
quizId: string;
timestamp: Date;
}
interface QuizUnlockedEvent {
userId: string;
quizId: string;
timestamp: Date;
}
interface QuizRetriedEvent {
userId: string;
quizId: string;
previousResponseId: string;
timestamp: Date;
}
interface RelatedLessonAccessedEvent {
userId: string;
quizId: string;
lessonId: string;
timestamp: Date;
}
interface QuizVersionCreatedEvent {
quizId: string;
version: number;
relatedLessonId: string;
timestamp: Date;
}
interface QuizVersionActivatedEvent {
quizId: string;
version: number;
previousVersion?: number;
timestamp: Date;
}
interface QuizPerformanceAnalyzedEvent {
quizId: string;
version: number;
correctAnswerRate: number;
recommendedAction: 'keep' | 'improve' | 'replace';
timestamp: Date;
}
interface QuizStatusChangedEvent {
userId: string;
quizId: string;
oldStatus: QuizStatusType;
newStatus: QuizStatusType;
timestamp: Date;
}
interface QuizAccessTrackedEvent {
userId: string;
quizId: string;
accessedAt: Date;
timestamp: Date;
}
6.6 진행 관련 이벤트
interface ProgressUpdatedEvent {
userId: string;
progressPercentage: number;
completedLessons: number;
timestamp: Date;
}
interface UserActivityRecordedEvent {
userId: string;
lessonId: string;
activityType: string;
timestamp: Date;
}
interface LearningStartDateSetEvent {
userId: string;
startDate: Date;
timestamp: Date;
}
interface LearningStartDateModifiedEvent {
userId: string;
oldStartDate: Date;
newStartDate: Date;
modifiedBy: string;
timestamp: Date;
}
interface TreatmentPeriodEndedEvent {
userId: string;
endDate: Date;
timestamp: Date;
}
6.7 번들 관련 이벤트
interface LearningContentBundleCreatedEvent {
bundleId: string;
version: string;
contentCount: {
sessions: number;
lessons: number;
quizzes: number;
media: number;
};
timestamp: Date;
}
interface LearningContentBundleActivatedEvent {
bundleId: string;
version: string;
previousVersion?: string;
timestamp: Date;
}
interface LearningContentBundleDeactivatedEvent {
bundleId: string;
version: string;
timestamp: Date;
}
interface LearningContentBundleDownloadedEvent {
userId: string;
bundleId: string;
version: string;
downloadTime: number; // milliseconds
timestamp: Date;
}
interface LearningContentBundleSyncStartedEvent {
userId: string;
bundleId: string;
version: string;
timestamp: Date;
}
interface LearningContentBundleSyncCompletedEvent {
userId: string;
bundleId: string;
version: string;
syncedContent: {
sessions: number;
lessons: number;
quizzes: number;
media: number;
};
syncTime: number; // milliseconds
timestamp: Date;
}
interface LearningContentBundleSyncFailedEvent {
userId: string;
bundleId: string;
version: string;
reason: string;
timestamp: Date;
}
interface LearningContentVersionChangedEvent {
contentType: 'session' | 'lesson' | 'quiz';
contentId: string;
oldVersion: number;
newVersion: number;
timestamp: Date;
}
6.8 학습 이력 관련 이벤트
interface LessonLearningStartedEvent {
userId: string;
lessonId: string;
historyId: string;
startedAt: Date;
deviceInfo: DeviceInfo;
timestamp: Date;
}
interface LessonLearningEndedEvent {
userId: string;
lessonId: string;
historyId: string;
startedAt: Date;
endedAt: Date;
duration: number;
isCompleted: boolean;
timestamp: Date;
}
interface LessonLearningPausedEvent {
userId: string;
lessonId: string;
historyId: string;
pausedAt: Date;
timestamp: Date;
}
interface LessonLearningResumedEvent {
userId: string;
lessonId: string;
historyId: string;
resumedAt: Date;
timestamp: Date;
}
interface LearningMetricsCalculatedEvent {
lessonId?: string;
userId?: string;
metrics: LearningMetrics;
timestamp: Date;
}
7. 데이터베이스 스키마 (Prisma)
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
schemas = ["learning", "private"]
}
// Enums
enum QuizType {
TRUE_FALSE
SINGLE_CHOICE
}
// enum MediaType {
// IMAGE
// VIDEO
// AUDIO
// }
enum TTSSpeed {
SLOW
NORMAL
FAST
}
enum LessonStatusType {
LOCKED
UNLOCKED
BLOCKED
IN_PROGRESS
COMPLETED
}
enum QuizStatusType {
PENDING
IN_PROGRESS
COMPLETED
}
enum LearningEventType {
START
PAUSE
RESUME
END
}
// 비개인정보 - learning 스키마
model Lesson {
id String @id @default(uuid())
sessionId String @map("session_id")
orderIndex Int @map("order_index")
label Int @map("label") // 일차별 추천 레슨 라벨 (1일차=1, 2일차=2, ...)
isFinalLesson Boolean @default(false) @map("is_final_lesson") // 최종 레슨 여부 (치료 기간 완료시 해금되는 특별 레슨)
version String @default("1.0.0")
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
// 관계
session LearningSession @relation(fields: [sessionId], references: [id])
translations LessonTranslation[]
bundleLessons BundleLesson[]
@@index([orderIndex])
@@index([sessionId, orderIndex])
@@index([sessionId, isActive, orderIndex])
@@index([isActive, orderIndex])
@@index([label])
@@index([isActive, label])
@@index([isActive, isFinalLesson, orderIndex])
@@map("lessons")
@@schema("learning")
}
model LessonTranslation {
id String @id @default(uuid())
lessonId String @map("lesson_id")
language String
title String
content String @map("content")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
// 관계
lesson Lesson @relation(fields: [lessonId], references: [id], onDelete: Cascade)
@@unique([lessonId, language])
@@index([language])
@@index([language, lessonId, title])
@@map("lesson_translations")
@@schema("learning")
}
model LearningSession {
id String @id @default(uuid())
orderIndex Int @map("order_index")
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
// 관계
lessons Lesson[]
translations LearningSessionTranslation[]
@@index([orderIndex])
@@index([isActive, orderIndex])
@@map("sessions")
@@schema("learning")
}
model LearningSessionTranslation {
id String @id @default(uuid())
sessionId String @map("session_id")
language String
name String
title String
description String?
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
// 관계
session LearningSession @relation(fields: [sessionId], references: [id], onDelete: Cascade)
@@unique([sessionId, language])
@@index([language])
@@index([language, sessionId, name])
@@map("session_translations")
@@schema("learning")
}
/*
// Media 모델들 (사용 중단 - 모바일 앱에서 관리)
model Media {
id String @id @default(uuid())
lessonId String @map("lesson_id")
mediaType MediaType @map("media_type")
url String
metadata Json
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
// 관계
lesson Lesson @relation(fields: [lessonId], references: [id])
translations MediaTranslation[]
@@index([lessonId])
@@map("media")
@@schema("learning")
}
model MediaTranslation {
id String @id @default(uuid())
mediaId String @map("media_id")
language String
altText String? @map("alt_text")
caption String?
description String?
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
// 관계
media Media @relation(fields: [mediaId], references: [id], onDelete: Cascade)
@@unique([mediaId, language])
@@index([language])
@@map("media_translations")
@@schema("learning")
}
*/
model Quiz {
id String @id @default(uuid())
version String @default("1.0.0")
quizType QuizType @map("quiz_type")
relatedLessonId String @map("related_lesson_id")
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
// 관계
translations QuizTranslation[]
options QuizOption[]
bundleQuizzes BundleQuiz[]
// 개인정보와 관련된 관계를 위한 가상 필드 (실제 연결은 private 스키마에서 함)
responses QuizResponse[]
@@unique([id, version])
@@index([relatedLessonId, isActive, quizType])
@@map("quizzes")
@@schema("learning")
}
model QuizTranslation {
id String @id @default(uuid())
quizId String @map("quiz_id")
language String
question String
explanation String
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
// 관계
quiz Quiz @relation(fields: [quizId], references: [id], onDelete: Cascade)
@@unique([quizId, language])
@@index([language])
@@map("quiz_translations")
@@schema("learning")
}
model QuizOption {
id String @id @default(uuid())
quizId String @map("quiz_id")
value String
isCorrectValue Boolean @default(false) @map("is_correct_value")
orderIndex Int @map("order_index")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
// 관계
quiz Quiz @relation(fields: [quizId], references: [id], onDelete: Cascade)
translations QuizOptionTranslation[]
@@index([quizId, orderIndex])
@@map("quiz_options")
@@schema("learning")
}
model QuizOptionTranslation {
id String @id @default(uuid())
optionId String @map("option_id")
language String
text String
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
// 관계
option QuizOption @relation(fields: [optionId], references: [id], onDelete: Cascade)
@@unique([optionId, language])
@@index([language])
@@map("quiz_option_translations")
@@schema("learning")
}
// 학습 콘텐츠 번들 모델 (learning 스키마)
model LearningContentBundle {
id String @id @default(uuid())
version String // 번들 버전 (예: "1.0.0")
generatedAt DateTime @default(now()) @map("generated_at")
isActive Boolean @default(false) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
// 관계
bundleLessons BundleLesson[]
bundleQuizzes BundleQuiz[]
@@unique([version])
@@index([isActive, generatedAt(sort: Desc)])
@@index([generatedAt(sort: Desc)])
@@map("learning_content_bundles")
@@schema("learning")
}
// 번들-레슨 관계 테이블
model BundleLesson {
id String @id @default(uuid())
bundleId String @map("bundle_id")
lessonId String @map("lesson_id")
// 관계
bundle LearningContentBundle @relation(fields: [bundleId], references: [id], onDelete: Cascade)
lesson Lesson @relation(fields: [lessonId], references: [id], onDelete: Cascade)
@@unique([bundleId, lessonId])
@@index([bundleId])
@@index([lessonId])
@@map("bundle_lessons")
@@schema("learning")
}
// 번들-퀴즈 관계 테이블
model BundleQuiz {
id String @id @default(uuid())
bundleId String @map("bundle_id")
quizId String @map("quiz_id")
// 관계
bundle LearningContentBundle @relation(fields: [bundleId], references: [id], onDelete: Cascade)
quiz Quiz @relation(fields: [quizId], references: [id], onDelete: Cascade)
@@unique([bundleId, quizId])
@@index([bundleId])
@@index([quizId])
@@map("bundle_quizzes")
@@schema("learning")
}
// 개인정보 - private 스키마
model LearningStartDate {
id String @id @default(uuid())
userId String @map("user_id")
userCycleId String @map("user_cycle_id")
startDate DateTime @map("start_date")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@unique([userId, userCycleId])
@@index([userId])
@@index([userCycleId])
@@map("learning_start_dates")
@@schema("private")
}
model LearningProgress {
id String @id @default(uuid())
userId String @map("user_id")
userCycleId String @map("user_cycle_id")
completedLessons Int @map("completed_lessons")
progressPercentage Float @map("progress_percentage")
lastActivityAt DateTime @map("last_activity_at")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@unique([userId, userCycleId])
@@index([userId])
@@index([userCycleId])
@@map("learning_progress")
@@schema("private")
}
model LessonCompletion {
id String @id @default(uuid())
userId String @map("user_id")
userCycleId String @map("user_cycle_id")
label Int @map("label")
lessonId String @map("lesson_id")
completedAt DateTime @map("completed_at")
createdAt DateTime @default(now()) @map("created_at")
@@unique([userId, userCycleId, label])
@@index([userId, userCycleId])
@@index([lessonId])
@@index([userId])
@@index([userCycleId])
@@index([userId, userCycleId, completedAt])
@@index([lessonId, completedAt])
@@index([userCycleId, completedAt])
@@map("lesson_completions")
@@schema("private")
}
model QuizResponse {
id String @id @default(uuid())
userId String @map("user_id")
userCycleId String @map("user_cycle_id")
quizId String @map("quiz_id")
quizVersion String @map("quiz_version")
selectedOptionId String @map("selected_option_id") // FK to QuizOption.id
selectedValue String @map("selected_value") // Actual option value for preservation
isCorrect Boolean @map("is_correct")
answeredAt DateTime @map("answered_at")
createdAt DateTime @default(now()) @map("created_at")
// 관계
quiz Quiz @relation(fields: [quizId], references: [id])
@@map("quiz_responses")
@@schema("private")
}
model TextSetting {
id String @id @default(uuid())
userId String @map("user_id")
userCycleId String @map("user_cycle_id")
fontSize Float @map("font_size")
ttsEnabled Boolean @default(false) @map("tts_enabled")
ttsLanguage String @map("tts_language")
ttsSpeed TTSSpeed @default(NORMAL) @map("tts_speed")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@unique([userId, userCycleId])
@@index([userId])
@@index([userCycleId])
@@map("text_settings")
@@schema("private")
}
model LessonStatus {
id String @id @default(uuid())
userId String @map("user_id")
userCycleId String @map("user_cycle_id")
lessonId String @map("lesson_id")
label Int @map("label")
status LessonStatusType @default(LOCKED) @map("status")
unlockedAt DateTime? @map("unlocked_at")
lastAccessedAt DateTime? @map("last_accessed_at")
completedAt DateTime? @map("completed_at")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@unique([userId, userCycleId, label])
@@index([userId, userCycleId])
@@index([lessonId])
@@index([status])
@@index([userId])
@@index([userCycleId])
@@index([userId, userCycleId, status])
@@index([lessonId, status])
@@index([userId, userCycleId, lastAccessedAt])
@@index([userId, userCycleId, unlockedAt])
@@map("lesson_status")
@@schema("private")
}
model LessonLearningHistory {
id String @id @default(uuid())
userId String @map("user_id")
userCycleId String @map("user_cycle_id")
lessonId String @map("lesson_id")
label Int @map("label")
startedAt DateTime @map("started_at")
endedAt DateTime? @map("ended_at")
durationSeconds Int? @map("duration_seconds")
isCompleted Boolean @default(false) @map("is_completed")
deviceInfo Json? @map("device_info")
sessionId String @map("session_id")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
// 관계
events LearningEvent[]
@@index([userId, userCycleId])
@@index([lessonId])
@@index([label])
@@index([startedAt])
@@index([userId, lessonId])
@@index([sessionId])
@@map("lesson_learning_history")
@@schema("private")
}
model LearningEvent {
id String @id @default(uuid())
userId String @map("user_id")
userCycleId String @map("user_cycle_id")
lessonId String @map("lesson_id")
historyId String? @map("history_id")
sessionId String @map("session_id")
eventType LearningEventType @map("event_type")
timestamp DateTime @map("timestamp")
deviceInfo Json? @map("device_info")
metadata Json? // 추가 메타데이터 (예: 네트워크 상태, 배터리 상태 등)
createdAt DateTime @default(now()) @map("created_at")
// 관계
history LessonLearningHistory? @relation(fields: [historyId], references: [id], onDelete: SetNull)
@@index([userId, userCycleId])
@@index([lessonId])
@@index([sessionId])
@@index([timestamp])
@@index([eventType])
@@index([userId, lessonId, sessionId])
@@map("learning_events")
@@schema("private")
}
model LearningStatistics {
id String @id @default(uuid())
userId String @map("user_id")
userCycleId String @map("user_cycle_id")
lessonId String? @map("lesson_id") // null이면 전체 통계
label Int? @map("label")
statType String @map("stat_type") // daily, weekly, monthly, overall
periodStart DateTime? @map("period_start")
periodEnd DateTime? @map("period_end")
metrics Json // LearningMetrics 데이터
calculatedAt DateTime @map("calculated_at")
createdAt DateTime @default(now()) @map("created_at")
@@unique([userId, userCycleId, label, statType, periodStart])
@@index([userId, userCycleId, statType, calculatedAt])
@@map("learning_statistics")
@@schema("private")
}
8. 변경 이력
| 버전 | 날짜 | 작성자 | 변경 내용 |
|---|---|---|---|
| 0.1.0 | 2025-04-03 | bok@weltcorp.com | 최초 작성 |
| 0.2.0 | 2025-05-23 | bok@weltcorp.com | requirements.md 변경사항 반영 - 하이라이트/사용자 입력 기능 제거, 세션 관리 기능 추가, 업데이트된 레슨 상태 관리 반영 |
| 0.2.1 | 2025-05-23 | bok@weltcorp.com | 다국어 지원을 위한 LessonTranslation, LearningSessionTranslation 모델 추가 |
| 0.2.2 | 2025-05-23 | bok@weltcorp.com | 치료주기별 학습 데이터 추적을 위한 userCycleId 필드 추가 |
| 0.2.3 | 2025-06-09 | bok@weltcorp.com | Quiz 버전 관리 기능 추가 - Quiz 엔티티에 version 필드, QuizResponse에 quizVersion 필드 추가, 버전 관리 서비스 및 이벤트 추가 |
| 0.2.4 | 2025-06-09 | bok@weltcorp.com | StartDate 테이블명을 LearningStartDate로 변경 - private 스키마 내 다른 도메인과의 명확한 구분을 위해 도메인 접두사 추가 |
| 0.3.0 | 2025-06-10 | bok@weltcorp.com | 모바일 캐싱을 위한 LearningContentBundle 개념 추가 - 클라이언트 오프라인 학습 지원을 위한 번들 관리 기능 구현 |
| 0.3.1 | 2025-06-10 | bok@weltcorp.com | 오디오 클립 기능 추가 - MediaType에 AUDIO 추가, 오디오 관련 도메인 이벤트, 서비스 메서드, 값 객체 추가 |
| 0.4.0 | 2025-06-10 | bok@weltcorp.com | 레슨 학습 이력 추적 기능 추가 - LessonLearningHistory 엔티티, LearningMetrics/LearningPattern 값 객체, LearningHistoryService 서비스, 관련 도메인 이벤트 추가 |
| 0.4.1 | 2025-06-10 | bok@weltcorp.com | 상세 학습 이벤트 추적 기능 추가 - LearningEvent, LearningStatistics 엔티티 추가, LearningEventType enum 추가, 개별 학습 이벤트 및 통계 관리 기능 구현 |
| 0.4.2 | 2025-06-10 | bok@weltcorp.com | 배치 테이블 모델 단순화 - LearningEventBatch 테이블 제거, 네트워크 트래픽 최적화에만 집중하는 단순한 배치 처리로 변경 |
| 0.4.3 | 2025-06-12 | bok@weltcorp.com | 일일 콘텐츠 도메인 서비스 통합 - RecommendationService를 DailyContentService로 변경, 일일 레슨과 퀴즈 추천을 통합 관리하는 구조로 개선 |
| 0.4.4 | 2025-06-12 | bok@weltcorp.com | 번들 관계 테이블 추가 - LearningContentBundle의 content JSON 필드 제거, BundleLesson/BundleQuiz 관계 테이블 추가로 정규화된 번들 관리 구조로 변경 |
| 0.5.0 | 2025-01-28 | bok@weltcorp.com | 모델 구조 변경 - Media/MediaTranslation 모델 제거 (모바일 앱에서 관리), Lesson에 label/isFinalLesson 필드 추가, LessonTranslation.content Json 타입 변경 |
| 0.5.1 | 2025-01-28 | bok@weltcorp.com | 퀴즈 상태 관리 기능 추가 - QuizStatus 엔티티, QuizStatusType enum, 관련 도메인 서비스 메서드, 도메인 이벤트 추가로 레슨과 일관된 상태 관리 구현 |
| 0.5.2 | 2025-01-20 | elizabeth@weltcorp.com | 현재 Prisma 스키마와 동기화: LessonCompletion에서 lessonId 필드 제거, QuizResponse에서 selectedOptionId/selectedValue 필드 추가, LessonLearningHistory에서 durationSeconds 필드 변경, QuizStatus 모델 제거, 관계 모델 최적화 |