펜타 서비스 데이터베이스 스키마 목표 설계서
개요
이 문서는 펜타 서비스의 모든 정책 문서를 기반으로 설계된 완전한 데이터베이스 스키마 목표를 정의합니다. 단순화 버전에서 시작하여 점진적으로 확장 가능한 구조로 설계되었습니다.
1. 사용자 도메인
users (사용자)
| 컬럼명 | 타입 | 설명 | 제약조건 |
|---|---|---|---|
| user_id | BIGINT | 사용자 고유 식별자 | PK, AUTO_INCREMENT |
| VARCHAR(255) | 이메일 주소 | UNIQUE, NOT NULL | |
| nickname | VARCHAR(100) | 사용자 닉네임 | NOT NULL |
| profile_image_url | VARCHAR(500) | 프로필 이미지 URL | NULL 가능 |
| app_language | VARCHAR(10) | 앱 설정 언어 (ko, en, ja, es, zh) | DEFAULT 'ko' |
| reading_language | VARCHAR(10) | 독서 언어 설정 | DEFAULT 'ko' |
| device_os | VARCHAR(50) | 기기 OS 정보 | NULL 가능 |
| device_model | VARCHAR(100) | 기기 모델 정보 | NULL 가능 |
| app_version | VARCHAR(20) | 앱 버전 | NULL 가능 |
| country_code | VARCHAR(2) | 접속 국가 코드 | NULL 가능 |
| created_at | TIMESTAMP | 가입일시 | DEFAULT CURRENT_TIMESTAMP |
| updated_at | TIMESTAMP | 수정일시 | ON UPDATE CURRENT_TIMESTAMP |
| deleted_at | TIMESTAMP | 탈퇴일시 (30일 보관 후 완전 삭제) | NULL 가능 |
| last_login_at | TIMESTAMP | 마지막 로그인 일시 | NULL 가능 |
user_subscriptions (구독 정보)
| 컬럼명 | 타입 | 설명 | 제약조건 |
|---|---|---|---|
| subscription_id | BIGINT | 구독 고유 식별자 | PK, AUTO_INCREMENT |
| user_id | BIGINT | 사용자 ID | FK → users.user_id |
| subscription_type | ENUM | 구독 유형 ('monthly', '6months', 'yearly') | NOT NULL |
| status | ENUM | 구독 상태 ('active', 'cancelled', 'expired', 'pending') | NOT NULL |
| price | DECIMAL(10,2) | 결제 금액 | NOT NULL |
| currency | VARCHAR(3) | 통화 (KRW, JPY, USD, EUR, CNY) | NOT NULL |
| start_date | DATE | 구독 시작일 | NOT NULL |
| end_date | DATE | 구독 종료 예정일 | NOT NULL |
| auto_renewal | BOOLEAN | 자동 갱신 여부 | DEFAULT TRUE |
| payment_method | VARCHAR(50) | 결제 방식 (app_store, google_play, web) | NOT NULL |
| promo_code | VARCHAR(50) | 사용한 프로모션 코드 | NULL 가능 |
| created_at | TIMESTAMP | 생성일시 | DEFAULT CURRENT_TIMESTAMP |
user_settings (사용자 설정)
| 컬럼명 | 타입 | 설명 | 제약조건 |
|---|---|---|---|
| user_id | BIGINT | 사용자 ID | PK, FK → users.user_id |
| auto_scroll_enabled | BOOLEAN | 자동 스크롤 사용 여부 | DEFAULT FALSE |
| auto_scroll_speed | INT | 자동 스크롤 속도 (1-10) | DEFAULT 5 |
| brightness_level | INT | 밝기 레벨 (0-100) | DEFAULT 50 |
| notification_enabled | BOOLEAN | 알림 수신 여부 | DEFAULT TRUE |
| child_age | INT | 자녀 연령 설정 | NULL 가능 |
| viewer_font_size | INT | 뷰어 폰트 크기 | DEFAULT 16 |
| bgm_enabled | BOOLEAN | BGM 재생 여부 | DEFAULT TRUE |
| bgm_volume | INT | BGM 볼륨 (0-100) | DEFAULT 50 |
| updated_at | TIMESTAMP | 수정일시 | ON UPDATE CURRENT_TIMESTAMP |
user_devices (사용자 기기)
| 컬럼명 | 타입 | 설명 | 제약조건 |
|---|---|---|---|
| device_id | BIGINT | 기기 고유 식별자 | PK, AUTO_INCREMENT |
| user_id | BIGINT | 사용자 ID | FK → users.user_id |
| device_uuid | VARCHAR(255) | 기기 UUID | UNIQUE, NOT NULL |
| device_name | VARCHAR(100) | 기기명 | NULL 가능 |
| last_active_at | TIMESTAMP | 마지막 활성 시간 | DEFAULT CURRENT_TIMESTAMP |
| is_active | BOOLEAN | 활성 상태 | DEFAULT TRUE |
2. 콘텐츠 도메인
books (도서)
| 컬럼명 | 타입 | 설명 | 제약조건 |
|---|---|---|---|
| book_id | BIGINT | 도서 고유 식별자 | PK, AUTO_INCREMENT |
| title | VARCHAR(500) | 도서 제목 | NOT NULL |
| author | VARCHAR(200) | 글 작가 | NULL 가능 |
| illustrator | VARCHAR(200) | 그림 작가 | NULL 가능 |
| publisher | VARCHAR(100) | 출판사 (disney, pixar) | NOT NULL |
| synopsis | TEXT | 줄거리 | NULL 가능 |
| age_range_min | INT | 최소 연령 | NOT NULL |
| age_range_max | INT | 최대 연령 | NOT NULL |
| lexile_score_min | INT | 렉사일 지수 최소값 | NULL 가능 |
| lexile_score_max | INT | 렉사일 지수 최대값 | NULL 가능 |
| lexile_type | ENUM | 렉사일 유형 ('estimated', 'official') | NULL 가능 |
| page_count | INT | 총 페이지 수 | NOT NULL |
| has_bgm | BOOLEAN | BGM 포함 여부 | DEFAULT FALSE |
| has_ost | BOOLEAN | OST 포함 여부 | DEFAULT FALSE |
| has_narration | BOOLEAN | 내레이션 포함 여부 | DEFAULT FALSE |
| published_date | DATE | 서비스 공개일 | NOT NULL |
| metadata | JSON | 추가 메타데이터 | NULL 가능 |
| created_at | TIMESTAMP | 생성일시 | DEFAULT CURRENT_TIMESTAMP |
| updated_at | TIMESTAMP | 수정일시 | ON UPDATE CURRENT_TIMESTAMP |
book_languages (도서 지원 언어)
| 컬럼명 | 타입 | 설명 | 제약조건 |
|---|---|---|---|
| book_language_id | BIGINT | 고유 식별자 | PK, AUTO_INCREMENT |
| book_id | BIGINT | 도서 ID | FK → books.book_id |
| language_code | VARCHAR(10) | 언어 코드 (ko, en, ja, es, zh) | NOT NULL |
| title_translated | VARCHAR(500) | 번역된 제목 | NOT NULL |
| cover_image_url | VARCHAR(500) | 언어별 표지 이미지 URL | NOT NULL |
| content_url | VARCHAR(500) | 언어별 콘텐츠 URL | NOT NULL |
| narration_url | VARCHAR(500) | 내레이션 URL | NULL 가능 |
| is_available | BOOLEAN | 서비스 가능 여부 | DEFAULT TRUE |
book_series (시리즈)
| 컬럼명 | 타입 | 설명 | 제약조건 |
|---|---|---|---|
| series_id | BIGINT | 시리즈 고유 식별자 | PK, AUTO_INCREMENT |
| series_name | VARCHAR(200) | 시리즈명 | NOT NULL |
| series_type | ENUM | 시리즈 유형 ('franchise', 'collection') | NOT NULL |
| description | TEXT | 시리즈 설명 | NULL 가능 |
| display_order | INT | 노출 순서 | DEFAULT 0 |
| created_at | TIMESTAMP | 생성일시 | DEFAULT CURRENT_TIMESTAMP |
book_series_mapping (도서-시리즈 매핑)
| 컬럼명 | 타입 | 설명 | 제약조건 |
|---|---|---|---|
| book_id | BIGINT | 도서 ID | PK, FK → books.book_id |
| series_id | BIGINT | 시리즈 ID | PK, FK → book_series.series_id |
| order_in_series | INT | 시리즈 내 순서 | DEFAULT 0 |
episodes (에피소드)
| 컬럼명 | 타입 | 설명 | 제약조건 |
|---|---|---|---|
| episode_id | BIGINT | 에피소드 고유 식별자 | PK, AUTO_INCREMENT |
| book_id | BIGINT | 도서 ID | FK → books.book_id |
| episode_number | INT | 회차 번호 | NOT NULL |
| episode_title | VARCHAR(500) | 에피소드 제목 | NOT NULL |
| thumbnail_url | VARCHAR(500) | 에피소드 썸네일 URL | NULL 가능 |
| created_at | TIMESTAMP | 생성일시 | DEFAULT CURRENT_TIMESTAMP |
book_tags (도서 태그)
| 컬럼명 | 타입 | 설명 | 제약조건 |
|---|---|---|---|
| tag_id | BIGINT | 태그 고유 식별자 | PK, AUTO_INCREMENT |
| tag_name | VARCHAR(100) | 태그명 | NOT NULL, UNIQUE |
| tag_type | ENUM | 태그 유형 ('theme', 'character', 'feature', 'category') | NOT NULL |
| tag_translations | JSON | 다국어 태그명 {"ko": "...", "en": "...", "ja": "...", "es": "...", "zh": "..."} | NULL 가능 |
book_tag_mapping (도서-태그 매핑)
| 컬럼명 | 타입 | 설명 | 제약조건 |
|---|---|---|---|
| book_id | BIGINT | 도서 ID | PK, FK → books.book_id |
| tag_id | BIGINT | 태그 ID | PK, FK → book_tags.tag_id |
characters (캐릭터)
| 컬럼명 | 타입 | 설명 | 제약조건 |
|---|---|---|---|
| character_id | BIGINT | 캐릭터 고유 식별자 | PK, AUTO_INCREMENT |
| character_name | VARCHAR(200) | 캐릭터명 | NOT NULL |
| character_type | ENUM | 캐릭터 유형 ('main', 'supporting', 'minor') | DEFAULT 'main' |
| description | TEXT | 캐릭터 설명 | NULL 가능 |
| image_url | VARCHAR(500) | 캐릭터 이미지 URL | NULL 가능 |
| name_translations | JSON | 다국어 캐릭터명 | NULL 가능 |
| display_order | INT | 노출 순서 | DEFAULT 0 |
| created_at | TIMESTAMP | 생성일시 | DEFAULT CURRENT_TIMESTAMP |
book_characters (도서-캐릭터 매핑)
| 컬럼명 | 타입 | 설명 | 제약조건 |
|---|---|---|---|
| book_id | BIGINT | 도서 ID | PK, FK → books.book_id |
| character_id | BIGINT | 캐릭터 ID | PK, FK → characters.character_id |
| role_in_book | VARCHAR(100) | 작품 내 역할 | NULL 가능 |
categories (카테고리)
| 컬럼명 | 타입 | 설명 | 제약조건 |
|---|---|---|---|
| category_id | BIGINT | 카테고리 고유 식별자 | PK, AUTO_INCREMENT |
| name | VARCHAR(100) | 카테고리명 (기본) | NOT NULL |
| type | VARCHAR(50) | 카테고리 유형 | NOT NULL |
| parent_id | BIGINT | 상위 카테고리 | FK → categories.category_id, NULL 가능 |
| translations | JSON | 다국어 번역 | NULL 가능 |
book_categories (도서-카테고리 매핑)
| 컬럼명 | 타입 | 설명 | 제약조건 |
|---|---|---|---|
| book_id | BIGINT | 도서 ID | PK, FK → books.book_id |
| category_id | BIGINT | 카테고리 ID | PK, FK → categories.category_id |
3. 점수 및 통계 도메인
book_scores (도서 점수)
홈 화면 및 검색의 정렬을 위한 복합 점수 시스템입니다.
| 컬럼명 | 타입 | 설명 | 제약조건 |
|---|---|---|---|
| book_id | BIGINT | 도서 ID | PK, FK → books.book_id |
| pv_score | INT | 페이지뷰 점수 | DEFAULT 0 |
| bookmark_score | INT | 찜하기 점수 (count * 2) | DEFAULT 0 |
| read_50_score | INT | 50% 열람 점수 (count * 2) | DEFAULT 0 |
| read_90_score | INT | 90% 열람 점수 (count * 3) | DEFAULT 0 |
| repeat_view_score | INT | 재열람 점수 | DEFAULT 0 |
| search_click_score | INT | 검색 클릭 점수 (count * 3) | DEFAULT 0 |
| total_score | INT | 총 점수 | GENERATED ALWAYS AS (pv_score + bookmark_score + read_50_score + read_90_score + repeat_view_score + search_click_score) STORED |
| last_calculated | TIMESTAMP | 마지막 계산일시 | DEFAULT CURRENT_TIMESTAMP |
daily_stats (일별 통계)
| 컬럼명 | 타입 | 설명 | 제약조건 |
|---|---|---|---|
| stat_date | DATE | 통계일 | PK |
| book_id | BIGINT | 도서 ID | PK, FK → books.book_id |
| country_code | VARCHAR(2) | 국가 코드 | PK |
| views | INT | 조회수 | DEFAULT 0 |
| bookmarks | INT | 찜하기수 | DEFAULT 0 |
| completions | INT | 완독수 | DEFAULT 0 |
| read_50_percent | INT | 50% 이상 열람수 | DEFAULT 0 |
| read_90_percent | INT | 90% 이상 열람수 | DEFAULT 0 |
| repeat_views | INT | 재열람수 | DEFAULT 0 |
| search_clicks | INT | 검색 클릭수 | DEFAULT 0 |
| stickers_earned | INT | 스티커 획득수 | DEFAULT 0 |
hourly_stats (시간별 통계)
실시간 랭킹을 위한 시간별 집계 테이블입니다.
| 컬럼명 | 타입 | 설명 | 제약조건 |
|---|---|---|---|
| stat_hour | DATETIME | 통계 시간 (YYYY-MM-DD HH:00:00) | PK |
| book_id | BIGINT | 도서 ID | PK, FK → books.book_id |
| country_code | VARCHAR(2) | 국가 코드 | PK |
| hour_views | INT | 시간별 조회수 | DEFAULT 0 |
| hour_bookmarks | INT | 시간별 찜하기수 | DEFAULT 0 |
| hour_completions | INT | 시간별 완독수 | DEFAULT 0 |
| hour_stickers | INT | 시간별 스티커 획득수 | DEFAULT 0 |
realtime_rankings (실시간 랭킹)
| 컬럼명 | 타입 | 설명 | 제약조건 |
|---|---|---|---|
| ranking_id | BIGINT | 랭킹 고유 식별자 | PK, AUTO_INCREMENT |
| ranking_type | ENUM | 랭킹 유형 ('top10', 'popular_sticker') | NOT NULL |
| book_id | BIGINT | 도서 ID | FK → books.book_id |
| rank | INT | 순위 | NOT NULL |
| score | INT | 점수 | NOT NULL |
| country_code | VARCHAR(2) | 국가 코드 | NOT NULL |
| calculated_at | TIMESTAMP | 집계일시 | DEFAULT CURRENT_TIMESTAMP |
| INDEX idx_country_time | (country_code, calculated_at DESC, rank) |
4. 리워드 도메인
stickers (스티커)
| 컬럼명 | 타입 | 설명 | 제약조건 |
|---|---|---|---|
| sticker_id | BIGINT | 스티커 고유 식별자 | PK, AUTO_INCREMENT |
| episode_id | BIGINT | 에피소드 ID | FK → episodes.episode_id, UNIQUE |
| name | VARCHAR(200) | 스티커명 (기본) | NOT NULL |
| name_translations | JSON | 다국어 스티커명 | NULL 가능 |
| image_url | VARCHAR(500) | 이미지 URL | NOT NULL |
| created_at | TIMESTAMP | 생성일시 | DEFAULT CURRENT_TIMESTAMP |
user_stickers (획득 스티커)
| 컬럼명 | 타입 | 설명 | 제약조건 |
|---|---|---|---|
| user_id | BIGINT | 사용자 ID | PK, FK → users.user_id |
| sticker_id | BIGINT | 스티커 ID | PK, FK → stickers.sticker_id |
| earned_at | TIMESTAMP | 획득일시 | DEFAULT CURRENT_TIMESTAMP |
| view_time_seconds | INT | 뷰어 체류 시간 | NOT NULL |
sticker_earning_logs (스티커 획득 로그)
| 컬럼명 | 타입 | 설명 | 제약조건 |
|---|---|---|---|
| log_id | BIGINT | 로그 고유 식별자 | PK, AUTO_INCREMENT |
| user_id | BIGINT | 사용자 ID | FK → users.user_id |
| sticker_id | BIGINT | 스티커 ID | FK → stickers.sticker_id |
| book_id | BIGINT | 도서 ID | FK → books.book_id |
| episode_id | BIGINT | 에피소드 ID | FK → episodes.episode_id |
| earned_at | TIMESTAMP | 획득일시 (UTC+9) | DEFAULT CURRENT_TIMESTAMP |
| scroll_speed | DECIMAL(10,2) | 스크롤 속도 (px/초) | NULL 가능 |
| total_view_time | INT | 총 열람 시간 (초) | NOT NULL |
| is_valid | BOOLEAN | 정상 획득 여부 | DEFAULT TRUE |
| country_code | VARCHAR(2) | 사용자 국가 코드 | NOT NULL |
| INDEX idx_earned_date | (earned_at, sticker_id) | ||
| INDEX idx_user_earned | (user_id, earned_at) |
popular_stickers (인기 스티커)
| 컬럼명 | 타입 | 설명 | 제약조건 |
|---|---|---|---|
| sticker_id | BIGINT | 스티커 ID | PK, FK → stickers.sticker_id |
| calculation_date | DATE | 집계일 | PK |
| hour | INT | 집계 시간 (0-23) | PK |
| earned_count | INT | 해당 시간 획득 수 | DEFAULT 0 |
| daily_total | INT | 24시간 누적 획득 수 | DEFAULT 0 |
| rank | INT | 인기 순위 | NULL 가능 |
| country_code | VARCHAR(2) | 국가 코드 | NOT NULL |
| updated_at | TIMESTAMP | 갱신일시 (UTC+9) | DEFAULT CURRENT_TIMESTAMP |
user_theme_preferences (사용자 테마 선호도)
| 컬럼명 | 타입 | 설명 | 제약조건 |
|---|---|---|---|
| user_id | BIGINT | 사용자 ID | PK, FK → users.user_id |
| theme_tag | VARCHAR(100) | 테마 태그 | PK |
| recent_view_score | INT | 최근 열람 점수 | DEFAULT 0 |
| bookmark_score | INT | 북마크 점수 | DEFAULT 0 |
| completion_score | INT | 완독 점수 | DEFAULT 0 |
| frequency_score | INT | 반복 열람 점수 | DEFAULT 0 |
| total_preference_score | INT | 총 선호도 점수 | DEFAULT 0 |
| last_updated | TIMESTAMP | 마지막 갱신일시 | DEFAULT CURRENT_TIMESTAMP |
user_sticker_recommendations (나만 없는 스티커)
| 컬럼명 | 타입 | 설명 | 제약조건 |
|---|---|---|---|
| recommendation_id | BIGINT | 추천 고유 식별자 | PK, AUTO_INCREMENT |
| user_id | BIGINT | 사용자 ID | FK → users.user_id |
| sticker_id | BIGINT | 추천 스티커 ID | FK → stickers.sticker_id |
| book_id | BIGINT | 도서 ID | FK → books.book_id |
| theme_score | INT | 테마 기반 점수 | DEFAULT 0 |
| popularity_score | INT | 인기도 점수 | DEFAULT 0 |
| total_score | INT | 총 점수 | DEFAULT 0 |
| is_excluded | BOOLEAN | 인기 Top10 제외 여부 | DEFAULT FALSE |
| calculated_at | TIMESTAMP | 계산일시 | DEFAULT CURRENT_TIMESTAMP |
| UNIQUE KEY | (user_id, sticker_id) |
upcoming_stickers (공개 예정 스티커)
| 컬럼명 | 타입 | 설명 | 제약조건 |
|---|---|---|---|
| upcoming_id | BIGINT | 예정 스티커 고유 식별자 | PK, AUTO_INCREMENT |
| book_id | BIGINT | 도서 ID | FK → books.book_id |
| sticker_id | BIGINT | 스티커 ID | FK → stickers.sticker_id, NULL 가능 |
| scheduled_date | DATE | 공개 예정일 | NOT NULL |
| week_start_date | DATE | 주 시작일 (월요일) | NOT NULL |
| country_code | VARCHAR(2) | 국가 코드 | NOT NULL |
| display_order | INT | 노출 순서 | DEFAULT 0 |
| preview_image_url | VARCHAR(500) | 미리보기 이미지 URL | NULL 가능 |
| is_published | BOOLEAN | 공개 완료 여부 | DEFAULT FALSE |
| created_at | TIMESTAMP | 생성일시 | DEFAULT CURRENT_TIMESTAMP |
| INDEX idx_week_country | (week_start_date, country_code) |
5. 사용자 활동 도메인
user_reading_history (독서 기록)
| 컬럼명 | 타입 | 설명 | 제약조건 |
|---|---|---|---|
| history_id | BIGINT | 기록 고유 식별자 | PK, AUTO_INCREMENT |
| user_id | BIGINT | 사용자 ID | FK → users.user_id |
| book_id | BIGINT | 도서 ID | FK → books.book_id |
| episode_id | BIGINT | 에피소드 ID | FK → episodes.episode_id, NULL 가능 |
| language_code | VARCHAR(10) | 열람 언어 | NOT NULL |
| last_position | INT | 마지막 읽은 위치 (픽셀) | DEFAULT 0 |
| scroll_percentage | INT | 스크롤 진행률 (0-100) | DEFAULT 0 |
| is_completed | BOOLEAN | 완독 여부 | DEFAULT FALSE |
| total_view_time | INT | 총 열람 시간 (초) | DEFAULT 0 |
| scroll_speed_avg | DECIMAL(10,2) | 평균 스크롤 속도 | NULL 가능 |
| started_at | TIMESTAMP | 열람 시작일시 | DEFAULT CURRENT_TIMESTAMP |
| last_viewed_at | TIMESTAMP | 마지막 열람일시 | ON UPDATE CURRENT_TIMESTAMP |
user_bookmarks (북마크/찜)
| 컬럼명 | 타입 | 설명 | 제약조건 |
|---|---|---|---|
| user_id | BIGINT | 사용자 ID | PK, FK → users.user_id |
| book_id | BIGINT | 도서 ID | PK, FK → books.book_id |
| bookmarked_at | TIMESTAMP | 북마크일시 | DEFAULT CURRENT_TIMESTAMP |
user_recordings (녹음)
| 컬럼명 | 타입 | 설명 | 제약조건 |
|---|---|---|---|
| recording_id | BIGINT | 녹음 고유 식별자 | PK, AUTO_INCREMENT |
| user_id | BIGINT | 사용자 ID | FK → users.user_id |
| book_id | BIGINT | 도서 ID | FK → books.book_id |
| episode_id | BIGINT | 에피소드 ID | FK → episodes.episode_id, NULL 가능 |
| language_code | VARCHAR(10) | 녹음 언어 | NOT NULL |
| recording_url | VARCHAR(500) | 녹음 파일 URL | NOT NULL |
| duration_seconds | INT | 녹음 길이 (초, 최대 1800) | NOT NULL |
| recorded_at | TIMESTAMP | 녹음일시 | DEFAULT CURRENT_TIMESTAMP |
| UNIQUE KEY | (user_id, book_id, episode_id, language_code) |
user_behavior_logs (사용자 행동 로그)
| 컬럼명 | 타입 | 설명 | 제약조건 |
|---|---|---|---|
| log_id | BIGINT | 로그 고유 식별자 | PK, AUTO_INCREMENT |
| user_id | BIGINT | 사용자 ID | FK → users.user_id |
| action_type | VARCHAR(50) | 행동 유형 | NOT NULL |
| target_type | VARCHAR(50) | 대상 유형 | NOT NULL |
| target_id | BIGINT | 대상 ID | NOT NULL |
| context | JSON | 추가 컨텍스트 정보 | NULL 가능 |
| created_at | TIMESTAMP | 로그 생성일시 | DEFAULT CURRENT_TIMESTAMP |
| INDEX idx_user_action | (user_id, action_type, created_at) |
6. 콘텐츠 노출 도메인
content_lists (콘텐츠 리스트)
| 컬럼명 | 타입 | 설명 | 제약조건 |
|---|---|---|---|
| list_id | BIGINT | 리스트 고유 식별자 | PK, AUTO_INCREMENT |
| list_type | VARCHAR(50) | 리스트 유형 | NOT NULL |
| name | VARCHAR(200) | 리스트명 (기본) | NOT NULL |
| name_translations | JSON | 다국어 리스트명 | NULL 가능 |
| criteria | JSON | 선정 기준 | NULL 가능 |
| display_location | VARCHAR(50) | 노출 위치 | NOT NULL |
| display_order | INT | 노출 순서 | DEFAULT 0 |
| min_items | INT | 최소 아이템 수 | DEFAULT 5 |
| is_active | BOOLEAN | 활성화 여부 | DEFAULT TRUE |
| updated_at | TIMESTAMP | 갱신일시 | DEFAULT CURRENT_TIMESTAMP |
content_list_items (리스트 아이템)
| 컬럼명 | 타입 | 설명 | 제약조건 |
|---|---|---|---|
| list_id | BIGINT | 리스트 ID | PK, FK → content_lists.list_id |
| book_id | BIGINT | 도서 ID | PK, FK → books.book_id |
| position | INT | 순서 | NOT NULL |
| is_manual | BOOLEAN | 수동 정렬 여부 | DEFAULT FALSE |
| added_at | TIMESTAMP | 추가일시 | DEFAULT CURRENT_TIMESTAMP |
banners (배너)
| 컬럼명 | 타입 | 설명 | 제약조건 |
|---|---|---|---|
| banner_id | BIGINT | 배너 고유 식별자 | PK, AUTO_INCREMENT |
| banner_type | VARCHAR(50) | 배너 유형 | NOT NULL |
| image_url | VARCHAR(500) | 이미지 URL | NOT NULL |
| target_type | VARCHAR(50) | 대상 유형 | NULL 가능 |
| target_id | BIGINT | 대상 ID | NULL 가능 |
| target_url | VARCHAR(500) | 연결 URL | NULL 가능 |
| position | INT | 순서 | DEFAULT 0 |
| is_active | BOOLEAN | 활성화 여부 | DEFAULT TRUE |
| start_date | DATETIME | 시작일시 | NOT NULL |
| end_date | DATETIME | 종료일시 | NULL 가능 |
7. 검색 도메인
search_logs (검색 로그)
| 컬럼명 | 타입 | 설명 | 제약조건 |
|---|---|---|---|
| log_id | BIGINT | 로그 고유 식별자 | PK, AUTO_INCREMENT |
| user_id | BIGINT | 사용자 ID | FK → users.user_id |
| search_query | VARCHAR(500) | 검색어 | NOT NULL |
| search_type | VARCHAR(50) | 검색 유형 | NULL 가능 |
| result_count | INT | 검색 결과 수 | DEFAULT 0 |
| clicked_book_id | BIGINT | 클릭한 도서 ID | FK → books.book_id, NULL 가능 |
| click_position | INT | 클릭 위치 | NULL 가능 |
| searched_at | TIMESTAMP | 검색일시 | DEFAULT CURRENT_TIMESTAMP |
popular_searches (인기 검색어)
| 컬럼명 | 타입 | 설명 | 제약조건 |
|---|---|---|---|
| search_term | VARCHAR(500) | 검색어 | PK |
| search_count | INT | 검색 횟수 | DEFAULT 1 |
| click_count | INT | 클릭 횟수 | DEFAULT 0 |
| last_searched_at | TIMESTAMP | 마지막 검색일시 | DEFAULT CURRENT_TIMESTAMP |
search_suggestions (검색어 추천)
| 컬럼명 | 타입 | 설명 | 제약조건 |
|---|---|---|---|
| suggestion_id | BIGINT | 추천 고유 식별자 | PK, AUTO_INCREMENT |
| keyword | VARCHAR(200) | 키워드 | NOT NULL, INDEX |
| suggestion | VARCHAR(500) | 추천 검색어 | NOT NULL |
| suggestion_type | VARCHAR(50) | 추천 유형 | NOT NULL |
| weight | INT | 가중치 | DEFAULT 1 |
| is_active | BOOLEAN | 활성화 여부 | DEFAULT TRUE |
8. 알림 도메인
notifications (알림)
| 컬럼명 | 타입 | 설명 | 제약조건 |
|---|---|---|---|
| notification_id | BIGINT | 알림 고유 식별자 | PK, AUTO_INCREMENT |
| notification_type | ENUM | 알림 유형 | NOT NULL |
| target_book_id | BIGINT | 관련 도서 ID | FK → books.book_id, NULL 가능 |
| target_event_id | BIGINT | 관련 이벤트 ID | FK → events.event_id, NULL 가능 |
| title | JSON | 다국어 제목 | NOT NULL |
| message | JSON | 다국어 내용 | NOT NULL |
| image_url | VARCHAR(500) | 알림 이미지 | NULL 가능 |
| action_url | VARCHAR(500) | 액션 URL | NULL 가능 |
| created_at | TIMESTAMP | 생성일시 | DEFAULT CURRENT_TIMESTAMP |
| expire_at | TIMESTAMP | 만료일시 | NOT NULL |
user_notifications (사용자별 알림)
| 컬럼명 | 타입 | 설명 | 제약조건 |
|---|---|---|---|
| user_id | BIGINT | 사용자 ID | PK, FK → users.user_id |
| notification_id | BIGINT | 알림 ID | PK, FK → notifications.notification_id |
| is_read | BOOLEAN | 읽음 여부 | DEFAULT FALSE |
| is_clicked | BOOLEAN | 클릭 여부 | DEFAULT FALSE |
| received_at | TIMESTAMP | 수신일시 | DEFAULT CURRENT_TIMESTAMP |
| read_at | TIMESTAMP | 읽은 일시 | NULL 가능 |
| clicked_at | TIMESTAMP | 클릭 일시 | NULL 가능 |
push_tokens (푸시 토큰)
| 컬럼명 | 타입 | 설명 | 제약조건 |
|---|---|---|---|
| token_id | BIGINT | 토큰 고유 식별자 | PK, AUTO_INCREMENT |
| user_id | BIGINT | 사용자 ID | FK → users.user_id |
| device_id | BIGINT | 기기 ID | FK → user_devices.device_id |
| push_token | VARCHAR(500) | 푸시 토큰 | UNIQUE, NOT NULL |
| platform | VARCHAR(20) | 플랫폼 (ios, android) | NOT NULL |
| is_active | BOOLEAN | 활성 상태 | DEFAULT TRUE |
| created_at | TIMESTAMP | 생성일시 | DEFAULT CURRENT_TIMESTAMP |
| updated_at | TIMESTAMP | 수정일시 | ON UPDATE CURRENT_TIMESTAMP |
9. 이벤트 도메인
events (이벤트)
| 컬럼명 | 타입 | 설명 | 제약조건 |
|---|---|---|---|
| event_id | BIGINT | 이벤트 고유 식별자 | PK, AUTO_INCREMENT |
| event_type | ENUM | 이벤트 유형 ('event', 'news', 'update') | NOT NULL |
| title | VARCHAR(500) | 이벤트 제목 | NOT NULL |
| content | TEXT | 이벤트 내용 | NOT NULL |
| thumbnail_url | VARCHAR(500) | 대표 이미지 URL | NULL 가능 |
| banner_url | VARCHAR(500) | 배너 이미지 URL | NULL 가능 |
| target_countries | JSON | 대상 국가 목록 | NULL 가능 |
| start_date | DATETIME | 시작일시 | NOT NULL |
| end_date | DATETIME | 종료일시 | NULL 가능 |
| is_active | BOOLEAN | 활성화 여부 | DEFAULT TRUE |
| view_count | INT | 조회수 | DEFAULT 0 |
| created_at | TIMESTAMP | 생성일시 | DEFAULT CURRENT_TIMESTAMP |
event_participants (이벤트 참여자)
| 컬럼명 | 타입 | 설명 | 제약조건 |
|---|---|---|---|
| participant_id | BIGINT | 참여 고유 식별자 | PK, AUTO_INCREMENT |
| event_id | BIGINT | 이벤트 ID | FK → events.event_id |
| user_id | BIGINT | 사용자 ID | FK → users.user_id |
| participated_at | TIMESTAMP | 참여일시 | DEFAULT CURRENT_TIMESTAMP |
| participation_data | JSON | 참여 데이터 | NULL 가능 |
10. 결제 도메인
promo_codes (프로모션 코드)
| 컬럼명 | 타입 | 설명 | 제약조건 |
|---|---|---|---|
| code | VARCHAR(50) | 프로모션 코드 | PK |
| code_type | ENUM | 코드 유형 | NOT NULL |
| discount_type | ENUM | 할인 유형 | NOT NULL |
| discount_value | DECIMAL(10,2) | 할인값 | NOT NULL |
| max_uses | INT | 최대 사용 횟수 | NULL 가능 |
| used_count | INT | 사용된 횟수 | DEFAULT 0 |
| valid_from | DATE | 유효 시작일 | NOT NULL |
| valid_until | DATE | 유효 종료일 | NULL 가능 |
| created_by | BIGINT | 생성자 | FK → users.user_id, NULL 가능 |
payment_logs (결제 로그)
| 컬럼명 | 타입 | 설명 | 제약조건 |
|---|---|---|---|
| payment_id | BIGINT | 결제 고유 식별자 | PK, AUTO_INCREMENT |
| user_id | BIGINT | 사용자 ID | FK → users.user_id |
| subscription_id | BIGINT | 구독 ID | FK → user_subscriptions.subscription_id |
| payment_type | ENUM | 결제 유형 | NOT NULL |
| amount | DECIMAL(10,2) | 금액 | NOT NULL |
| currency | VARCHAR(3) | 통화 | NOT NULL |
| platform_transaction_id | VARCHAR(200) | 플랫폼 거래 ID | UNIQUE |
| platform | VARCHAR(50) | 결제 플랫폼 | NOT NULL |
| status | ENUM | 결제 상태 | NOT NULL |
| refund_reason | TEXT | 환불 사유 | NULL 가능 |
| processed_at | TIMESTAMP | 처리일시 | DEFAULT CURRENT_TIMESTAMP |
referral_rewards (추천 리워드)
| 컬럼명 | 타입 | 설명 | 제약조건 |
|---|---|---|---|
| reward_id | BIGINT | 리워드 고유 식별자 | PK, AUTO_INCREMENT |
| referrer_id | BIGINT | 추천인 ID | FK → users.user_id |
| referee_id | BIGINT | 피추천인 ID | FK → users.user_id |
| reward_type | VARCHAR(50) | 리워드 유형 | NOT NULL |
| reward_value | INT | 리워드 값 | NOT NULL |
| status | VARCHAR(20) | 상태 | NOT NULL |
| created_at | TIMESTAMP | 생성일시 | DEFAULT CURRENT_TIMESTAMP |
11. 운영 관리 도메인
admin_users (관리자)
| 컬럼명 | 타입 | 설명 | 제약조건 |
|---|---|---|---|
| admin_id | BIGINT | 관리자 고유 식별자 | PK, AUTO_INCREMENT |
| VARCHAR(255) | 이메일 | UNIQUE, NOT NULL | |
| name | VARCHAR(100) | 이름 | NOT NULL |
| role | ENUM | 권한 | NOT NULL |
| permissions | JSON | 세부 권한 | NULL 가능 |
| is_active | BOOLEAN | 활성화 여부 | DEFAULT TRUE |
| last_login_at | TIMESTAMP | 마지막 로그인 | NULL 가능 |
| created_at | TIMESTAMP | 생성일시 | DEFAULT CURRENT_TIMESTAMP |
content_schedule (콘텐츠 공개 일정)
| 컬럼명 | 타입 | 설명 | 제약조건 |
|---|---|---|---|
| schedule_id | BIGINT | 일정 고유 식별자 | PK, AUTO_INCREMENT |
| book_id | BIGINT | 도서 ID | FK → books.book_id |
| scheduled_date | DATE | 공개 예정일 | NOT NULL |
| country_code | VARCHAR(2) | 국가 코드 | NOT NULL |
| is_published | BOOLEAN | 공개 완료 여부 | DEFAULT FALSE |
| published_at | TIMESTAMP | 실제 공개일시 | NULL 가능 |
| created_at | TIMESTAMP | 생성일시 | DEFAULT CURRENT_TIMESTAMP |
| UNIQUE KEY | (scheduled_date, country_code) |
operation_logs (운영 로그)
| 컬럼명 | 타입 | 설명 | 제약조건 |
|---|---|---|---|
| log_id | BIGINT | 로그 고유 식별자 | PK, AUTO_INCREMENT |
| admin_id | BIGINT | 관리자 ID | FK → admin_users.admin_id |
| action_type | VARCHAR(100) | 액션 유형 | NOT NULL |
| target_type | VARCHAR(50) | 대상 유형 | NOT NULL |
| target_id | BIGINT | 대상 ID | NOT NULL |
| before_data | JSON | 변경 전 데이터 | NULL 가능 |
| after_data | JSON | 변경 후 데이터 | NULL 가능 |
| ip_address | VARCHAR(45) | IP 주소 | NULL 가능 |
| created_at | TIMESTAMP | 생성일시 | DEFAULT CURRENT_TIMESTAMP |
주요 인덱스 전략
-- 사용자 도메인
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_country ON users(country_code);
CREATE INDEX idx_user_subscriptions_status ON user_subscriptions(user_id, status);
-- 콘텐츠 도메인
CREATE INDEX idx_books_published ON books(published_date DESC);
CREATE INDEX idx_book_languages ON book_languages(language_code);
CREATE INDEX idx_book_series_type ON book_series(series_type, display_order);
CREATE INDEX idx_book_tags_type ON book_tags(tag_type, tag_name);
CREATE INDEX idx_characters_order ON characters(display_order, character_name);
-- 점수 및 통계
CREATE INDEX idx_book_scores_total ON book_scores(total_score DESC);
CREATE INDEX idx_daily_stats_date ON daily_stats(stat_date, country_code);
CREATE INDEX idx_hourly_stats ON hourly_stats(stat_hour, country_code);
CREATE INDEX idx_realtime_rankings ON realtime_rankings(country_code, calculated_at DESC, rank);
-- 사용자 활동
CREATE INDEX idx_reading_history_user ON user_reading_history(user_id, last_viewed_at DESC);
CREATE INDEX idx_reading_history_book ON user_reading_history(book_id, is_completed);
CREATE INDEX idx_bookmarks_user ON user_bookmarks(user_id, bookmarked_at DESC);
-- 리워드
CREATE INDEX idx_user_stickers ON user_stickers(user_id, earned_at DESC);
CREATE INDEX idx_sticker_logs_date ON sticker_earning_logs(earned_at, sticker_id);
CREATE INDEX idx_popular_stickers ON popular_stickers(calculation_date, rank);
CREATE INDEX idx_theme_preferences ON user_theme_preferences(user_id, total_preference_score DESC);
-- 검색
CREATE INDEX idx_search_logs_user ON search_logs(user_id, searched_at DESC);
CREATE INDEX idx_search_logs_query ON search_logs(search_query);
CREATE INDEX idx_popular_searches ON popular_searches(search_count DESC);
-- 콘텐츠 노출
CREATE INDEX idx_content_lists_active ON content_lists(is_active, display_location, display_order);
CREATE INDEX idx_content_list_items ON content_list_items(list_id, position);
CREATE INDEX idx_banners_active ON banners(is_active, start_date, end_date);
데이터 보관 및 삭제 정책
-
사용자 데이터
- 탈퇴 후 30일간 보관
- 30일 후 개인정보 완전 삭제
- 통계 데이터는 익명화하여 보관
-
알림 데이터
- 생성 후 14일 자동 삭제
- 읽음 상태와 관계없이 삭제
-
로그 데이터
- 검색 로그: 90일 보관
- 행동 로그: 180일 보관
- 결제 로그: 영구 보관
- 운영 로그: 영구 보관
-
통계 데이터
- 시간별 집계: 30일 보관
- 일별 집계: 1년 보관
- 월별 집계: 영구 보관
성능 최적화 고려사항
-
파티셔닝 전략
- daily_stats: 날짜별 파티셔닝
- hourly_stats: 월별 파티셔닝
- user_behavior_logs: 월별 파티셔닝
-
캐싱 전략
- Redis를 활용한 실시간 랭킹 캐싱
- 인기 검색어 캐싱
- 사용자 선호도 캐싱
-
비동기 처리
- 점수 계산 배치 작업
- 통계 집계 배치 작업
- 추천 시스템 갱신
확장 계획
Phase 1 (MVP - 단순화 스키마)
- 핵심 기능만 구현
- 기본 통계만 수집
- 단순 정렬 기준
Phase 2 (점수 시스템 도입)
- 복합 점수 시스템 구현
- 실시간 랭킹 고도화
- 개인화 추천 시작
Phase 3 (고급 기능)
- 상세 행동 추적
- A/B 테스트 시스템
- 머신러닝 기반 추천
Phase 4 (글로벌 확장)
- 다중 리전 지원
- 실시간 동기화
- 글로벌 통계 시스템
이 목표 스키마는 단순화 버전에서 시작하여 서비스 성장에 따라 점진적으로 확장 가능한 구조로 설계되었습니다.