본문으로 건너뛰기

Relaxation 도메인 모델

데이터 저장 규칙

  • 개인정보 관련 데이터는 private 스키마에 저장되어야 합니다.
  • 비개인정보 데이터는 relaxation 스키마에 저장되어야 합니다.
  • 개인정보 포함 여부에 따라 위 규칙을 우선적으로 적용합니다.
  • Relaxation 도메인에서는 RelaxationPlaybackHistory, RelaxationContentCompletion, RelaxationUserSetting, RelaxationRecommendationStatus, RelaxationDownloadStatus 등 사용자 ID와 연결된 데이터는 모두 개인정보로 취급하여 private 스키마에 저장합니다.
  • 사용자와 연결되지 않은 데이터(RelaxationContent, RelaxationCategory, RelaxationContentDetail, RelaxationUsageGuide 등)만 relaxation 스키마에 저장합니다.

1. 엔티티 관계도 (ERD)

2. 엔티티

2.1 RelaxationContent

interface RelaxationContent {
id: string;
categoryId: string;
durationSeconds: number; // 초 단위
orderIndex: number;
level: RelaxationLevel;
stress: RelaxationStress;
poses: RelaxationPose[];
isActive: boolean;
createdAt: Date;
updatedAt: Date;
recommendationDayIndex?: number; // 1~5일차, null이면 일반 콘텐츠
}

enum RelaxationLevel {
EASY = 'EASY',
MEDIUM = 'MEDIUM',
DIFFICULT = 'DIFFICULT',
}

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

enum RelaxationPose {
LYING_DOWN = 'LYING_DOWN',
SITTING = 'SITTING',
STANDING = 'STANDING',
WALKING = 'WALKING',
}

2.2 RelaxationContentTranslation

interface RelaxationContentTranslation {
id: string;
contentId: string;
thumbnailImageUrl: string;
lottieFileUrl: string;
backgroundLottieUrl?: string;
backgroundImageUrl?: string;
audioFileUrl: string;
language: string;
title: string;
summary: string;
createdAt: Date;
updatedAt: Date;
}

2.3 RelaxationCategory

interface RelaxationCategory {
id: string;
orderIndex: number;
isActive: boolean;
createdAt: Date;
updatedAt: Date;
}

2.4 RelaxationCategoryTranslation

interface RelaxationCategoryTranslation {
id: string;
categoryId: string;
language: string;
title: string;
summary: string;
createdAt: Date;
updatedAt: Date;
}

2.5 RelaxationPlaybackHistory

interface RelaxationPlaybackHistory {
id: string;
userId: string;
userCycleId: string;
contentId: string;
startedAt: Date;
endedAt?: Date;
playbackPosition: number; // 초 단위
isCompleted: boolean;
createdAt: Date;
updatedAt: Date;
}

2.6 RelaxationContentCompletion

interface RelaxationContentCompletion {
id: string;
userId: string;
userCycleId: string;
contentId: string;
completedAt: Date;
createdAt: Date;
}

2.7 RelaxationUserSetting

interface RelaxationUserSetting {
id: string;
userId: string;
userCycleId: string;
repeatMode: boolean;
preferredCategory?: string;
lastPlayedContentId?: string;
createdAt: Date;
updatedAt: Date;
}

2.8 RelaxationRecommendationStatus

interface RelaxationRecommendationStatus {
id: string;
userId: string;
userCycleId: string;
registrationDate: Date;
currentDay: number;
isExpired: boolean;
createdAt: Date;
updatedAt: Date;
}

2.9 RelaxationContentDetail

interface RelaxationContentDetail {
id: string;
contentId: string;
version: string;
createdAt: Date;
updatedAt: Date;
}

2.10 RelaxationContentDetailTranslation

interface RelaxationContentDetailTranslation {
id: string;
detailId: string;
language: string;
purpose: string;
effects: string;
mechanism: string;
precautions?: string;
createdAt: Date;
updatedAt: Date;
}

2.11 RelaxationUsageGuide

interface RelaxationUsageGuide {
id: string;
version: string;
isActive: boolean;
createdAt: Date;
updatedAt: Date;
}

2.12 RelaxationUsageGuideTranslation

interface RelaxationUsageGuideTranslation {
id: string;
guideId: string;
language: string;
content: string;
createdAt: Date;
updatedAt: Date;
}

2.13 RelaxationDownloadStatus

interface RelaxationDownloadStatus {
id: string;
userId: string;
userCycleId: string;
contentId: string;
downloadProgress: number; // 0-100 백분율
status: DownloadStatusType;
cacheLocation?: string;
createdAt: Date;
updatedAt: Date;
}

enum DownloadStatusType {
PENDING = 'PENDING',
IN_PROGRESS = 'IN_PROGRESS',
PAUSED = 'PAUSED',
COMPLETED = 'COMPLETED',
FAILED = 'FAILED',
}

2.14 RelaxationContentBundle

interface RelaxationContentBundle {
id: string;
bundleName?: string;
version: string;
isActive: boolean;
generatedAt: Date;
createdAt: Date;
updatedAt: Date;
}

2.15 RelaxationBundleContent

interface RelaxationBundleContent {
id: string;
bundleId: string;
contentId: string;
createdAt: Date;
}

3. 값 객체

3.1 CategoryInfo

interface CategoryInfo {
categoryId: string;
name: string;
summary: string;
heroImageUrl: string;
isExpanded: boolean;
}

3.2 PlaybackState

interface PlaybackState {
isPlaying: boolean;
currentPosition: number;
duration: number;
isRepeating: boolean;
}

3.3 DownloadProgress

interface DownloadProgress {
progress: number; // 0-100
bytesDownloaded: number;
totalBytes: number;
}

3.4 RecommendationInfo

interface RecommendationInfo {
day: number;
contentId: string;
contentTitle: string;
}

3.5 ContentMetadata

interface ContentMetadata {
contentId: string;
title: string;
durationSeconds: number;
category: string;
thumbnailImageUrl: string;
backgroundLottieUrl?: string;
backgroundImageUrl?: string;
}

3.6 GuideVersion

interface GuideVersion {
version: string;
updatedAt: Date;
}

3.7 CompletionInfo

interface CompletionInfo {
isCompleted: boolean;
completedAt?: Date;
}

3.8 AudioMetadata

interface AudioMetadata {
format: string;
bitrate: number;
duration: number;
size: number;
}

3.9 ThumbnailImageInfo

interface ThumbnailImageInfo {
url: string;
width: number;
height: number;
format: string;
}

3.10 BackgroundMediaInfo

interface BackgroundMediaInfo {
url: string;
type: 'video' | 'lottie' | 'image';
width?: number;
height?: number;
format?: string;
duration?: number;
}

3.11 CacheInfo

interface CacheInfo {
location: string;
size: number;
lastAccessedAt: Date;
expiresAt?: Date;
}

3.12 RelaxationBundleInfo

interface RelaxationBundleInfo {
currentVersion: string;
clientVersion: string;
needsUpdate: boolean;
bundleName?: string;
generatedAt: Date;
}

3.13 RelaxationContentBundleMetadata

interface RelaxationContentBundleMetadata {
bundleId: string;
version: string;
hash: string;
contentCount: number;
totalSize: number;
}

4. 집계 (Aggregates)

  • 이완요법 콘텐츠(RelaxationContentAggregate)

    • Root: RelaxationContent
    • Entities: RelaxationContentDetail, RelaxationDownloadStatus
    • Value Objects: ContentMetadata, AudioMetadata, ThumbnailImageInfo, BackgroundMediaInfo
  • 카테고리 관리(RelaxationCategoryManagement)

    • Root: RelaxationCategory
    • Entities: RelaxationContent
    • Value Objects: CategoryInfo
  • 재생 관리(RelaxationPlaybackManagement)

    • Root: RelaxationPlaybackHistory
    • Entities: RelaxationContentCompletion
    • Value Objects: PlaybackState, CompletionInfo
  • 추천 관리(RelaxationRecommendationManagement)

    • Root: RelaxationRecommendationStatus
    • Value Objects: RecommendationInfo
  • 사용자 설정(RelaxationUserSettings)

    • Root: RelaxationUserSetting
    • Value Objects: -
  • 사용 가이드(RelaxationUsageGuideAggregate)

    • Root: RelaxationUsageGuide
    • Value Objects: GuideVersion
  • 다운로드 관리(RelaxationDownloadManagement)

    • Root: RelaxationDownloadStatus
    • Value Objects: DownloadProgress, CacheInfo
  • 이완요법 콘텐츠 번들 관리(RelaxationContentBundleManagement)

    • Root: RelaxationContentBundle
    • Entities: RelaxationContent
    • Value Objects: RelaxationBundleInfo, RelaxationContentBundleMetadata

5. 도메인 서비스

5.1 ContentManagementService

interface ContentManagementService {
getContentList(): Promise<RelaxationContent[]>;
getContentByCategory(categoryId: string): Promise<RelaxationContent[]>;
getContentMetadata(contentId: string): Promise<ContentMetadata>;
getContentDetail(contentId: string): Promise<RelaxationContentDetail>;
updateContentVersion(contentId: string): Promise<RelaxationContentDetail>;
validateContentAccess(userId: string, contentId: string): Promise<boolean>;
getCategoryList(): Promise<RelaxationCategory[]>;
}

5.2 PlaybackControlService

interface PlaybackControlService {
startPlayback(userId: string, contentId: string): Promise<RelaxationPlaybackHistory>;
pausePlayback(userId: string, contentId: string): Promise<RelaxationPlaybackHistory>;
resumePlayback(userId: string, contentId: string): Promise<RelaxationPlaybackHistory>;
stopPlayback(userId: string, contentId: string): Promise<RelaxationPlaybackHistory>;
toggleRepeatMode(userId: string): Promise<RelaxationUserSetting>;
markContentAsCompleted(userId: string, contentId: string): Promise<RelaxationContentCompletion>;
getPlaybackState(userId: string, contentId: string): Promise<PlaybackState>;
savePlaybackPosition(userId: string, contentId: string, position: number): Promise<void>;
}

5.3 RecommendationService

interface RecommendationService {
getDailyRecommendation(userId: string): Promise<RelaxationContent | null>;
initializeRecommendationStatus(userId: string): Promise<RelaxationRecommendationStatus>;
updateRecommendationDay(userId: string): Promise<RelaxationRecommendationStatus>;
checkRecommendationPeriod(userId: string): Promise<boolean>;
getRecommendationHistory(userId: string): Promise<RecommendationInfo[]>;
}

5.4 DownloadService

interface DownloadService {
startContentDownload(userId: string, contentId: string): Promise<RelaxationDownloadStatus>;
updateDownloadProgress(userId: string, contentId: string, progress: number): Promise<RelaxationDownloadStatus>;
pauseDownload(userId: string, contentId: string): Promise<RelaxationDownloadStatus>;
resumeDownload(userId: string, contentId: string): Promise<RelaxationDownloadStatus>;
completeDownload(userId: string, contentId: string, cacheLocation: string): Promise<RelaxationDownloadStatus>;
getCachedContent(userId: string, contentId: string): Promise<CacheInfo | null>;
clearCache(userId: string): Promise<void>;
}

5.5 GuideService

interface GuideService {
getActiveGuide(): Promise<RelaxationUsageGuide>;
updateGuideVersion(guideId: string, content: string): Promise<RelaxationUsageGuide>;
getGuideHistory(): Promise<RelaxationUsageGuide[]>;
markGuideAsViewed(userId: string, guideId: string): Promise<void>;
}

5.6 SecurityService

interface SecurityService {
validateUserAccess(userId: string, resourceId: string, resourceType: string): Promise<boolean>;
generateSecureDownloadUrl(contentId: string): Promise<string>;
logAccessAttempt(userId: string, resourceId: string, success: boolean): Promise<void>;
preventUnauthorizedCopy(contentId: string): Promise<void>;
}

5.7 CategoryManagementService

interface CategoryManagementService {
toggleCategoryExpansion(categoryId: string, isExpanded: boolean): Promise<void>;
getCategoryWithContents(categoryId: string): Promise<{ category: RelaxationCategory; contents: RelaxationContent[] }>;
getCategorySummary(categoryId: string): Promise<string>;
}

5.8 UserSettingService

interface UserSettingService {
getUserSettings(userId: string): Promise<RelaxationUserSetting>;
updateRepeatMode(userId: string, enabled: boolean): Promise<RelaxationUserSetting>;
updatePreferredCategory(userId: string, categoryId: string): Promise<RelaxationUserSetting>;
updateLastPlayedContent(userId: string, contentId: string): Promise<RelaxationUserSetting>;
getDefaultSettings(): Promise<RelaxationUserSetting>;
}

5.9 RelaxationContentBundleService

interface RelaxationContentBundleService {
getActiveBundle(): Promise<RelaxationContentBundle>;
checkBundleUpdate(clientVersion: string): Promise<RelaxationBundleInfo>;
createNewBundle(contents: RelaxationContent[], bundleName?: string): Promise<RelaxationContentBundle>;
activateBundle(bundleId: string): Promise<RelaxationContentBundle>;
getBundleMetadata(bundleId: string): Promise<RelaxationContentBundleMetadata>;
validateBundleIntegrity(bundleId: string): Promise<boolean>;
getBundleHistory(): Promise<RelaxationContentBundle[]>;
}

5.10 RelaxationContentCacheService

interface RelaxationContentCacheService {
cacheContentBundle(bundle: RelaxationContentBundle, contents: RelaxationContent[]): Promise<void>;
getCachedBundle(): Promise<{ bundle: RelaxationContentBundle; contents: RelaxationContent[] } | null>;
updateCachedBundle(newBundle: RelaxationContentBundle, contents: RelaxationContent[]): Promise<void>;
clearCache(): Promise<void>;
getCachedBundleVersion(): Promise<string | null>;
validateCacheIntegrity(): Promise<boolean>;
getCacheSize(): Promise<number>;
optimizeCache(): Promise<void>;
}

6. 도메인 이벤트

6.1 콘텐츠 관련 이벤트

interface RelaxationContentListRequestedEvent {
userId: string;
timestamp: Date;
}

interface ContentMetadataRetrievedEvent {
contentId: string;
userId: string;
timestamp: Date;
}

interface ContentDownloadStartedEvent {
userId: string;
contentId: string;
timestamp: Date;
}

interface ContentDownloadProgressUpdatedEvent {
userId: string;
contentId: string;
progress: number;
timestamp: Date;
}

interface ContentDownloadCompletedEvent {
userId: string;
contentId: string;
cacheLocation: string;
timestamp: Date;
}

interface ContentDownloadResumedEvent {
userId: string;
contentId: string;
timestamp: Date;
}

interface ContentPlaybackRecordedEvent {
userId: string;
contentId: string;
startedAt: Date;
timestamp: Date;
}

interface ContentCompletedEvent {
userId: string;
contentId: string;
completedAt: Date;
timestamp: Date;
}

interface ContentPlaybackStateSavedEvent {
userId: string;
contentId: string;
position: number;
timestamp: Date;
}

interface ContentAccessValidatedEvent {
userId: string;
contentId: string;
accessGranted: boolean;
timestamp: Date;
}

interface ContentDetailInfoProvidedEvent {
userId: string;
contentId: string;
timestamp: Date;
}

interface ContentBundleVersionCheckedEvent {
clientVersion: string;
serverVersion: string;
needsUpdate: boolean;
bundleName?: string;
timestamp: Date;
}

interface ContentBundleCachedEvent {
bundleId: string;
version: string;
itemCount: number;
cacheSize: number;
timestamp: Date;
}

interface ContentBundleUpdatedEvent {
oldBundleId?: string;
newBundleId: string;
oldVersion?: string;
newVersion: string;
contentCount: number;
timestamp: Date;
}

interface BundleVersionMismatchDetectedEvent {
clientVersion: string;
serverVersion: string;
timestamp: Date;
}

interface BundleCacheValidationFailedEvent {
bundleId: string;
version: string;
reason: string;
timestamp: Date;
}

interface ContentBundleActivatedEvent {
bundleId: string;
version: string;
activatedAt: Date;
timestamp: Date;
}

6.2 추천 관련 이벤트

interface DailyRecommendationGeneratedEvent {
userId: string;
contentId: string;
day: number;
timestamp: Date;
}

interface RecommendationPeriodExpiredEvent {
userId: string;
expirationDate: Date;
timestamp: Date;
}

6.3 카테고리 관련 이벤트

interface CategoryExpandedEvent {
userId: string;
categoryId: string;
timestamp: Date;
}

interface CategoryCollapsedEvent {
userId: string;
categoryId: string;
timestamp: Date;
}

interface ContentCategoryRetrievedEvent {
categoryId: string;
timestamp: Date;
}

6.4 가이드 관련 이벤트

interface GuideAccessedEvent {
userId: string;
guideId: string;
timestamp: Date;
}

interface GuideVersionUpdatedEvent {
guideId: string;
oldVersion: string;
newVersion: string;
timestamp: Date;
}

6.5 재생 관련 이벤트

interface PlaybackHistoryRecordedEvent {
userId: string;
contentId: string;
duration: number;
timestamp: Date;
}

interface RepeatModeChangedEvent {
userId: string;
enabled: boolean;
timestamp: Date;
}

interface PlaybackPausedEvent {
userId: string;
contentId: string;
position: number;
timestamp: Date;
}

interface PlaybackResumedEvent {
userId: string;
contentId: string;
position: number;
timestamp: Date;
}

6.6 UI 관련 이벤트

interface BottomSheetDisplayedEvent {
userId: string;
contentId: string;
timestamp: Date;
}

interface BottomSheetConfirmedEvent {
userId: string;
contentId: string;
timestamp: Date;
}

interface BottomSheetCancelledEvent {
userId: string;
contentId: string;
timestamp: Date;
}

interface ExitConfirmationPopupDisplayedEvent {
userId: string;
contentId: string;
timestamp: Date;
}

interface ContentExitConfirmedEvent {
userId: string;
contentId: string;
timestamp: Date;
}

interface ContentExitCancelledEvent {
userId: string;
contentId: string;
timestamp: Date;
}

7. 데이터베이스 스키마 (Prisma)

generator client {
provider = "prisma-client-js"
}

datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
schemas = ["relaxation", "private", "public"]
}

// Enums (public 스키마에 정의됨 - 여러 도메인에서 공통 사용)
enum DownloadStatusType {
PENDING
IN_PROGRESS
PAUSED
COMPLETED
FAILED

@@schema("public")
}

// Relaxation 도메인 전용 enum들
enum RelaxationLevel {
EASY
MEDIUM
DIFFICULT

@@schema("relaxation")
}

enum RelaxationStress {
LOW
MEDIUM
HIGH

@@schema("relaxation")
}

enum RelaxationPose {
LYING_DOWN
SITTING
STANDING
WALKING

@@schema("relaxation")
}

// 비개인정보 - relaxation 스키마
model RelaxationContent {
id String @id @default(uuid())
categoryId String @map("category_id")
durationSeconds Int @map("duration_seconds")
orderIndex Int @map("order_index")
level RelaxationLevel
stress RelaxationStress
poses RelaxationPose[]
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
recommendationDayIndex Int? @map("recommendation_day_index") // 1~5일차, null이면 일반 콘텐츠

// 관계
category RelaxationCategory @relation(fields: [categoryId], references: [id])
translations RelaxationContentTranslation[]
details RelaxationContentDetail[]
bundleContents RelaxationBundleContent[]

// 개인정보와 관련된 관계를 위한 가상 필드 (실제 연결은 private 스키마에서 함)
playbackHistories RelaxationPlaybackHistory[]
completions RelaxationContentCompletion[]
downloadStatuses RelaxationDownloadStatus[]

@@index([categoryId, orderIndex])
@@index([categoryId])
@@index([recommendationDayIndex]) // 추천 콘텐츠 조회 최적화
@@map("relaxation_contents")
@@schema("relaxation")
}

model RelaxationContentTranslation {
id String @id @default(uuid())
contentId String @map("content_id")
thumbnailImageUrl String @map("thumbnail_image_url")
lottieFileUrl String @map("lottie_file_url")
backgroundLottieUrl String? @map("background_lottie_url")
backgroundImageUrl String? @map("background_image_url")
audioFileUrl String @map("audio_file_url")
language String
title String
summary String
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")

// 관계
content RelaxationContent @relation(fields: [contentId], references: [id], onDelete: Cascade)

@@unique([contentId, language])
@@index([language])
@@map("relaxation_content_translations")
@@schema("relaxation")
}

model RelaxationCategory {
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")

// 관계
contents RelaxationContent[]
translations RelaxationCategoryTranslation[]

@@index([orderIndex])
@@map("categories")
@@schema("relaxation")
}

model RelaxationCategoryTranslation {
id String @id @default(uuid())
categoryId String @map("category_id")
language String
title String
summary String
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")

// 관계
category RelaxationCategory @relation(fields: [categoryId], references: [id], onDelete: Cascade)

@@unique([categoryId, language])
@@index([language])
@@map("category_translations")
@@schema("relaxation")
}

model RelaxationContentDetail {
id String @id @default(uuid())
contentId String @map("content_id")
version String @default("1.0.0")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")

// 관계
content RelaxationContent @relation(fields: [contentId], references: [id], onDelete: Cascade)
translations RelaxationContentDetailTranslation[]

@@unique([contentId, version])
@@index([contentId])
@@map("relaxation_content_details")
@@schema("relaxation")
}

model RelaxationContentDetailTranslation {
id String @id @default(uuid())
detailId String @map("detail_id")
language String
purpose String @db.Text
effects String @db.Text
mechanism String @db.Text
precautions String? @db.Text
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")

// 관계
detail RelaxationContentDetail @relation(fields: [detailId], references: [id], onDelete: Cascade)

@@unique([detailId, language])
@@index([language])
@@map("relaxation_content_detail_translations")
@@schema("relaxation")
}

model RelaxationUsageGuide {
id String @id @default(uuid())
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")

// 관계
translations RelaxationUsageGuideTranslation[]

@@index([isActive])
@@map("usage_guides")
@@schema("relaxation")
}

model RelaxationUsageGuideTranslation {
id String @id @default(uuid())
guideId String @map("guide_id")
language String
content String @db.Text
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")

// 관계
guide RelaxationUsageGuide @relation(fields: [guideId], references: [id], onDelete: Cascade)

@@unique([guideId, language])
@@index([language])
@@map("usage_guide_translations")
@@schema("relaxation")
}

model RelaxationContentBundle {
id String @id @default(uuid())
bundleName String? @map("bundle_name")
version String @unique
isActive Boolean @default(false) @map("is_active")
generatedAt DateTime @default(now()) @map("generated_at")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")

// 관계
bundleContents RelaxationBundleContent[]

@@index([isActive])
@@index([generatedAt])
@@index([version])
@@map("relaxation_content_bundles")
@@schema("relaxation")
}

// 번들-콘텐츠 관계 테이블 (relaxation 스키마)
model RelaxationBundleContent {
id String @id @default(uuid())
bundleId String @map("bundle_id")
contentId String @map("content_id")
createdAt DateTime @default(now()) @map("created_at")

// 관계
bundle RelaxationContentBundle @relation(fields: [bundleId], references: [id], onDelete: Cascade)
content RelaxationContent @relation(fields: [contentId], references: [id], onDelete: Cascade)

@@unique([bundleId, contentId])
@@index([bundleId])
@@index([contentId])
@@map("bundle_contents")
@@schema("relaxation")
}

// 개인정보 - private 스키마
model RelaxationPlaybackHistory {
id String @id @default(uuid())
userId String @map("user_id")
userCycleId String @map("user_cycle_id")
contentId String @map("content_id")
startedAt DateTime @map("started_at")
endedAt DateTime? @map("ended_at")
playbackPosition Int @map("playback_position") // 초 단위
isCompleted Boolean @default(false) @map("is_completed")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")

// 관계
content RelaxationContent @relation(fields: [contentId], references: [id], onDelete: Cascade)

@@index([userId, userCycleId])
@@index([contentId])
@@index([userId])
@@index([userCycleId])
@@map("relaxation_playback_histories")
@@schema("private")
}

model RelaxationContentCompletion {
id String @id @default(uuid())
userId String @map("user_id")
userCycleId String @map("user_cycle_id")
contentId String @map("content_id")
completedAt DateTime @map("completed_at")
createdAt DateTime @default(now()) @map("created_at")

// 관계
content RelaxationContent @relation(fields: [contentId], references: [id], onDelete: Cascade)

@@unique([userId, userCycleId, contentId])
@@index([userId, userCycleId])
@@index([contentId])
@@index([userId])
@@index([userCycleId])
@@map("relaxation_content_completions")
@@schema("private")
}

model RelaxationUserSetting {
id String @id @default(uuid())
userId String @map("user_id")
userCycleId String @map("user_cycle_id")
repeatMode Boolean @default(false) @map("repeat_mode")
preferredCategory String? @map("preferred_category")
lastPlayedContentId String? @map("last_played_content_id")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")

@@unique([userId, userCycleId])
@@index([userId])
@@index([userCycleId])
@@map("relaxation_user_settings")
@@schema("private")
}

model RelaxationRecommendationStatus {
id String @id @default(uuid())
userId String @map("user_id")
userCycleId String @map("user_cycle_id")
registrationDate DateTime @map("registration_date")
currentDay Int @default(1) @map("current_day")
isExpired Boolean @default(false) @map("is_expired")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")

@@unique([userId, userCycleId])
@@index([userId])
@@index([userCycleId])
@@map("relaxation_recommendation_statuses")
@@schema("private")
}

model RelaxationDownloadStatus {
id String @id @default(uuid())
userId String @map("user_id")
userCycleId String @map("user_cycle_id")
contentId String @map("content_id")
downloadProgress Float @default(0) @map("download_progress") // 0-100
status DownloadStatusType @default(PENDING)
cacheLocation String? @map("cache_location")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")

// 관계
content RelaxationContent @relation(fields: [contentId], references: [id], onDelete: Cascade)

@@unique([userId, userCycleId, contentId])
@@index([userId, userCycleId])
@@index([contentId])
@@index([status])
@@index([userId])
@@index([userCycleId])
@@map("relaxation_download_statuses")
@@schema("private")
}

8. 변경 이력

버전날짜작성자변경 내용
0.1.02025-06-10bok@weltcorp.com최초 작성
0.1.12025-06-10bok@weltcorp.com콘텐츠 목록 클라이언트 캐싱을 위한 버전 관리 엔티티, 서비스, 이벤트 추가
0.1.22025-06-10bok@weltcorp.com콘텐츠 버전 관리를 ContentBundle 방식으로 변경하여 QuestionnaireBundle과 일관성 확보
0.1.32025-06-10bok@weltcorp.comContentBundle을 RelaxationContentBundle로 명확화
0.1.42025-06-10bok@weltcorp.comRelaxationContent에 thumbnailImageUrl 추가, backgroundImageUrl을 backgroundVideoUrl로 변경
0.1.52025-06-10bok@weltcorp.comCategory 엔티티에 heroImageUrl 필드 추가
0.1.62025-06-12bok@weltcorp.comRelaxationContent에 lottieFileUrl 필드 추가, Category에서 heroImageUrl 필드 제거, DownloadStatusType을 public 스키마로 이동
0.1.72025-06-12bok@weltcorp.comCategory를 RelaxationCategory로, CategoryTranslation을 RelaxationCategoryTranslation으로 명확화하여 모델명 충돌 방지
0.1.82025-06-12bok@weltcorp.com모든 일반적인 모델명을 Relaxation 접두사로 명확화 (UsageGuide→RelaxationUsageGuide, PlaybackHistory→RelaxationPlaybackHistory 등)
0.1.92025-06-12bok@weltcorp.comContentDetail→RelaxationContentDetail로 변경하고 private 스키마 테이블명에 relaxation_ 접두사 추가하여 다른 도메인과 구분
0.1.102025-06-12bok@weltcorp.comRelaxationContent에 level, stress, poses 필드 추가 및 관련 enum 정의 (RelaxationLevel, RelaxationStress, RelaxationPose)
0.1.112025-01-20elizabeth@weltcorp.com현재 Prisma 스키마와 동기화: 미디어 URL 필드를 Translation 테이블로 이동, duration→durationSeconds 변경, enum 값 수정, recommendationDayIndex 추가, hash 필드 제거, version 타입 변경