펜타 서비스 데이터베이스 스키마
개요
펜타 서비스의 데이터베이스 스키마 문서입니다.
- 개발 환경: SQLite3
- 운영 환경: PostgreSQL 13+
- ORM: Django ORM + django-parler (다국어 지원)
테이블 구조
users (사용자)
| 컬럼명 | 타입 | 제약조건 | 설명 |
|---|---|---|---|
| id | INTEGER | PK | |
| password | VARCHAR(128) | NOT NULL | |
| is_superuser | BOOLEAN | NOT NULL | |
| VARCHAR(255) | NOT NULL, UNIQUE | ||
| username | VARCHAR(150) | ||
| full_name | VARCHAR(255) | NOT NULL | |
| social_provider | VARCHAR(50) | 소셜 로그인 제공자 | |
| social_id | VARCHAR(255) | 소셜 로그인 ID | |
| language | VARCHAR(10) | NOT NULL | 기본값: ko |
| country | VARCHAR(2) | ||
| age | INTEGER | ||
| is_subscribed | BOOLEAN | NOT NULL | |
| subscription_type | VARCHAR(20) | ||
| subscription_end_date | DATETIME | ||
| is_active | BOOLEAN | NOT NULL | |
| is_staff | BOOLEAN | NOT NULL | |
| date_joined | DATETIME | NOT NULL | |
| last_login | DATETIME | ||
| marketing_consent | BOOLEAN | NOT NULL | |
| deleted_at | DATETIME | 소프트 삭제 |
user_devices (사용자 기기)
| 컬럼명 | 타입 | 제약조건 | 설명 |
|---|---|---|---|
| id | INTEGER | PK | |
| user_id | BIGINT | NOT NULL, FK(users) | |
| device_id | VARCHAR(255) | NOT NULL, UNIQUE | |
| device_type | VARCHAR(50) | NOT NULL | ios, android, web |
| device_model | VARCHAR(100) | NOT NULL | |
| app_version | VARCHAR(20) | NOT NULL | |
| last_active | DATETIME | NOT NULL | |
| created_at | DATETIME | NOT NULL |
books (도서)
| 컬럼명 | 타입 | 제약조건 | 설명 |
|---|---|---|---|
| id | INTEGER | PK | |
| age_range | VARCHAR(20) | 3-5, 6-8 등 | |
| genre | VARCHAR(20) | NOT NULL | |
| status | VARCHAR(20) | NOT NULL | draft, published, hidden |
| thumbnail_url | VARCHAR(500) | ||
| views | INTEGER | NOT NULL | 기본값: 0 |
| is_free | BOOLEAN | NOT NULL | |
| is_new | BOOLEAN | NOT NULL | |
| is_exclusive | BOOLEAN | NOT NULL | |
| is_featured | BOOLEAN | NOT NULL | |
| featured_at | DATETIME | ||
| is_active | BOOLEAN | NOT NULL | |
| average_rating | DECIMAL(3,2) | NOT NULL | 기본값: 0 |
| total_ratings | INTEGER | NOT NULL | 기본값: 0 |
| pv_score | INTEGER | NOT NULL | 기본값: 0 |
| metadata | TEXT | JSON 형태 | |
| created_at | DATETIME | NOT NULL | |
| published_date | DATE | NOT NULL |
books_translation (도서 번역)
| 컬럼명 | 타입 | 제약조건 | 설명 |
|---|---|---|---|
| id | INTEGER | PK | |
| master_id | BIGINT | FK(books) | |
| language_code | VARCHAR(15) | NOT NULL | ko, en, ja, es, zh |
| title | VARCHAR(500) | NOT NULL | |
| author | VARCHAR(200) | ||
| publisher | VARCHAR(100) | NOT NULL | |
| synopsis | TEXT | ||
| description | TEXT | ||
| cover_url | VARCHAR(500) | ||
| content_url | VARCHAR(500) |
episodes (에피소드)
| 컬럼명 | 타입 | 제약조건 | 설명 |
|---|---|---|---|
| id | INTEGER | PK | |
| book_id | BIGINT | NOT NULL, FK(books) | |
| episode_number | INTEGER | NOT NULL | |
| is_free | BOOLEAN | NOT NULL | |
| published_date | DATE |
episodes_translation (에피소드 번역)
| 컬럼명 | 타입 | 제약조건 | 설명 |
|---|---|---|---|
| id | INTEGER | PK | |
| master_id | BIGINT | FK(episodes) | |
| language_code | VARCHAR(15) | NOT NULL | |
| title | VARCHAR(500) | NOT NULL |
episode_pages (에피소드 페이지)
| 컬럼명 | 타입 | 제약조건 | 설명 |
|---|---|---|---|
| id | INTEGER | PK | |
| episode_id | BIGINT | NOT NULL, FK(episodes) | |
| page_number | INTEGER | NOT NULL | 페이지 번호 |
| image_url | VARCHAR(500) | NOT NULL | 페이지 이미지 URL |
| width | INTEGER | 이미지 너비 (픽셀) | |
| height | INTEGER | 이미지 높이 (픽셀) | |
| file_size | INTEGER | 파일 크기 (바이트) | |
| created_at | DATETIME | NOT NULL |
stickers (스티커)
| 컬럼명 | 타입 | 제약조건 | 설명 |
|---|---|---|---|
| id | INTEGER | PK | |
| episode_id | BIGINT | NOT NULL, FK(episodes), UNIQUE | |
| image_url | VARCHAR(500) | NOT NULL | |
| created_at | DATETIME | NOT NULL |
stickers_translation (스티커 번역)
| 컬럼명 | 타입 | 제약조건 | 설명 |
|---|---|---|---|
| id | INTEGER | PK | |
| master_id | BIGINT | FK(stickers) | |
| language_code | VARCHAR(15) | NOT NULL | |
| name | VARCHAR(200) | NOT NULL | |
| description | TEXT |
user_stickers (사용자 스티커)
| 컬럼명 | 타입 | 제약조건 | 설명 |
|---|---|---|---|
| id | INTEGER | PK | |
| user_id | BIGINT | NOT NULL, FK(users) | |
| sticker_id | BIGINT | NOT NULL, FK(stickers) | |
| earned_at | DATETIME | NOT NULL |
bookmarks (북마크)
| 컬럼명 | 타입 | 제약조건 | 설명 |
|---|---|---|---|
| id | INTEGER | PK | |
| user_id | BIGINT | NOT NULL, FK(users) | |
| book_id | BIGINT | NOT NULL, FK(books) | |
| created_at | DATETIME | NOT NULL |
reading_history (독서 기록)
| 컬럼명 | 타입 | 제약조건 | 설명 |
|---|---|---|---|
| id | INTEGER | PK | |
| user_id | BIGINT | NOT NULL, FK(users) | |
| book_id | BIGINT | NOT NULL, FK(books) | |
| episode_id | BIGINT | FK(episodes) | |
| last_position | INTEGER | NOT NULL | 기본값: 0 |
| is_completed | BOOLEAN | NOT NULL | |
| last_read_at | DATETIME | NOT NULL |
subscriptions (구독)
| 컬럼명 | 타입 | 제약조건 | 설명 |
|---|---|---|---|
| id | INTEGER | PK | |
| user_id | BIGINT | NOT NULL, FK(users) | |
| type | VARCHAR(20) | NOT NULL | basic, premium |
| status | VARCHAR(20) | NOT NULL | active, cancelled, expired |
| start_date | DATETIME | NOT NULL | |
| end_date | DATETIME | NOT NULL | |
| cancelled_at | DATETIME | ||
| base_price | DECIMAL(10,2) | NOT NULL | |
| discount_rate | DECIMAL(5,2) | NOT NULL | 기본값: 0 |
| final_price | DECIMAL(10,2) | NOT NULL | |
| currency | VARCHAR(3) | NOT NULL | 기본값: KRW |
| auto_renew | BOOLEAN | NOT NULL | 기본값: TRUE |
| next_billing_date | DATETIME | ||
| promo_code | VARCHAR(50) | ||
| created_at | DATETIME | NOT NULL | |
| updated_at | DATETIME | NOT NULL |
characters (캐릭터)
| 컬럼명 | 타입 | 제약조건 | 설명 |
|---|---|---|---|
| id | INTEGER | PK | |
| character_type | VARCHAR(20) | NOT NULL | main, supporting |
| image_url | VARCHAR(500) | NOT NULL | |
| display_order | INTEGER | NOT NULL | 기본값: 0 |
| created_at | DATETIME | NOT NULL |
characters_translation (캐릭터 번역)
| 컬럼명 | 타입 | 제약조건 | 설명 |
|---|---|---|---|
| id | INTEGER | PK | |
| master_id | BIGINT | FK(characters) | |
| language_code | VARCHAR(15) | NOT NULL | |
| name | VARCHAR(200) | NOT NULL | |
| description | TEXT |
categories (카테고리)
| 컬럼명 | 타입 | 제약조건 | 설명 |
|---|---|---|---|
| id | INTEGER | PK | |
| type | VARCHAR(50) | NOT NULL | age, genre, theme |
| parent_id | BIGINT | FK(categories) | 자기참조 |
categories_translation (카테고리 번역)
| 컬럼명 | 타입 | 제약조건 | 설명 |
|---|---|---|---|
| id | INTEGER | PK | |
| master_id | BIGINT | FK(categories) | |
| language_code | VARCHAR(15) | NOT NULL | |
| name | VARCHAR(100) | NOT NULL |
book_series (도서 시리즈)
| 컬럼명 | 타입 | 제약조건 | 설명 |
|---|---|---|---|
| id | INTEGER | PK | |
| series_name | VARCHAR(200) | NOT NULL | |
| series_type | VARCHAR(20) | NOT NULL | franchise, collection |
| display_order | INTEGER | NOT NULL | 기본값: 0 |
| created_at | DATETIME | NOT NULL |
book_series_mapping (도서-시리즈 매핑)
| 컬럼명 | 타입 | 제약조건 | 설명 |
|---|---|---|---|
| id | INTEGER | PK | |
| book_id | BIGINT | NOT NULL, FK(books) | |
| series_id | BIGINT | NOT NULL, FK(book_series) | |
| order_in_series | INTEGER | NOT NULL | 기본값: 0 |
book_tags (도서 태그)
| 컬럼명 | 타입 | 제약조건 | 설명 |
|---|---|---|---|
| id | INTEGER | PK | |
| tag_type | VARCHAR(50) | NOT NULL | |
| created_at | DATETIME | NOT NULL |
book_tags_translation (도서 태그 번역)
| 컬럼명 | 타입 | 제약조건 | 설명 |
|---|---|---|---|
| id | INTEGER | PK | |
| master_id | BIGINT | FK(book_tags) | |
| language_code | VARCHAR(15) | NOT NULL | |
| name | VARCHAR(100) | NOT NULL |
book_tag_mapping (도서-태그 매핑)
| 컬럼명 | 타입 | 제약조건 | 설명 |
|---|---|---|---|
| id | INTEGER | PK | |
| book_id | BIGINT | NOT NULL, FK(books) | |
| tag_id | BIGINT | NOT NULL, FK(book_tags) |
book_categories (도서-카테고리 매핑)
| 컬럼명 | 타입 | 제약조건 | 설명 |
|---|---|---|---|
| id | INTEGER | PK | |
| book_id | BIGINT | NOT NULL, FK(books) | |
| category_id | BIGINT | NOT NULL, FK(categories) |
book_characters (도서-캐릭터 매핑)
| 컬럼명 | 타입 | 제약조건 | 설명 |
|---|---|---|---|
| id | INTEGER | PK | |
| book_id | BIGINT | NOT NULL, FK(books) | |
| character_id | BIGINT | NOT NULL, FK(characters) | |
| is_main | BOOLEAN | NOT NULL | 기본값: FALSE |
book_scores (도서 점수)
| 컬럼명 | 타입 | 제약조건 | 설명 |
|---|---|---|---|
| id | INTEGER | PK | |
| book_id | BIGINT | NOT NULL, FK(books), UNIQUE | |
| pv_score | INTEGER | NOT NULL | 기본값: 0 |
| bookmark_score | INTEGER | NOT NULL | 기본값: 0 |
| read_50_score | INTEGER | NOT NULL | 기본값: 0 |
| read_90_score | INTEGER | NOT NULL | 기본값: 0 |
| search_click_score | INTEGER | NOT NULL | 기본값: 0 |
| total_score | INTEGER | GENERATED | 계산 컬럼 |
| updated_at | DATETIME | NOT NULL |
daily_stats (일별 통계)
| 컬럼명 | 타입 | 제약조건 | 설명 |
|---|---|---|---|
| id | INTEGER | PK | |
| stat_date | DATE | NOT NULL | |
| book_id | BIGINT | FK(books) | |
| country_code | VARCHAR(2) | ||
| views | INTEGER | NOT NULL | 기본값: 0 |
| bookmarks | INTEGER | NOT NULL | 기본값: 0 |
| completions | INTEGER | NOT NULL | 기본값: 0 |
| new_users | INTEGER | NOT NULL | 기본값: 0 |
| active_users | INTEGER | NOT NULL | 기본값: 0 |
banners (배너)
| 컬럼명 | 타입 | 제약조건 | 설명 |
|---|---|---|---|
| id | INTEGER | PK | |
| banner_type | VARCHAR(50) | NOT NULL | main, event, notice |
| target_type | VARCHAR(50) | book, url, event | |
| target_id | VARCHAR(200) | ||
| display_order | INTEGER | NOT NULL | 기본값: 0 |
| is_active | BOOLEAN | NOT NULL | |
| start_date | DATETIME | NOT NULL | |
| end_date | DATETIME | NOT NULL | |
| created_at | DATETIME | NOT NULL |
banners_translation (배너 번역)
| 컬럼명 | 타입 | 제약조건 | 설명 |
|---|---|---|---|
| id | INTEGER | PK | |
| master_id | BIGINT | FK(banners) | |
| language_code | VARCHAR(15) | NOT NULL | |
| image_url | VARCHAR(500) | NOT NULL | |
| alt_text | VARCHAR(200) |
notifications (알림)
| 컬럼명 | 타입 | 제약조건 | 설명 |
|---|---|---|---|
| id | INTEGER | PK | |
| type | VARCHAR(50) | NOT NULL | |
| target_type | VARCHAR(50) | all, user, group | |
| target_id | VARCHAR(200) | ||
| is_active | BOOLEAN | NOT NULL | |
| send_date | DATETIME | NOT NULL | |
| expire_date | DATETIME | ||
| created_at | DATETIME | NOT NULL |
notifications_translation (알림 번역)
| 컬럼명 | 타입 | 제약조건 | 설명 |
|---|---|---|---|
| id | INTEGER | PK | |
| master_id | BIGINT | FK(notifications) | |
| language_code | VARCHAR(15) | NOT NULL | |
| title | VARCHAR(200) | NOT NULL | |
| message | TEXT | NOT NULL |
user_notifications (사용자 알림)
| 컬럼명 | 타입 | 제약조건 | 설명 |
|---|---|---|---|
| id | INTEGER | PK | |
| user_id | BIGINT | NOT NULL, FK(users) | |
| notification_id | BIGINT | NOT NULL, FK(notifications) | |
| is_read | BOOLEAN | NOT NULL | 기본값: FALSE |
| read_at | DATETIME | ||
| created_at | DATETIME | NOT NULL |
events_news (이벤트/뉴스)
| 컬럼명 | 타입 | 제약조건 | 설명 |
|---|---|---|---|
| id | INTEGER | PK | |
| type | VARCHAR(50) | NOT NULL | event, news, notice |
| status | VARCHAR(20) | NOT NULL | 기본값: draft |
| start_date | DATETIME | NOT NULL | |
| end_date | DATETIME | NOT NULL | |
| is_active | BOOLEAN | NOT NULL | |
| created_at | DATETIME | NOT NULL |
events_news_translation (이벤트/뉴스 번역)
| 컬럼명 | 타입 | 제약조건 | 설명 |
|---|---|---|---|
| id | INTEGER | PK | |
| master_id | BIGINT | FK(events_news) | |
| language_code | VARCHAR(15) | NOT NULL | |
| title | VARCHAR(200) | NOT NULL | |
| content | TEXT | NOT NULL | |
| image_url | VARCHAR(500) |
home_homesection (홈 섹션)
| 컬럼명 | 타입 | 제약조건 | 설명 |
|---|---|---|---|
| id | INTEGER | PK | |
| section_type | VARCHAR(50) | NOT NULL | |
| display_order | INTEGER | NOT NULL | 기본값: 0 |
| is_active | BOOLEAN | NOT NULL | |
| created_at | DATETIME | NOT NULL |
home_homesection_translation (홈 섹션 번역)
| 컬럼명 | 타입 | 제약조건 | 설명 |
|---|---|---|---|
| id | INTEGER | PK | |
| master_id | BIGINT | FK(home_homesection) | |
| language_code | VARCHAR(15) | NOT NULL | |
| title | VARCHAR(200) | NOT NULL | |
| subtitle | VARCHAR(200) |
home_homesectioncontent (홈 섹션 콘텐츠)
| 컬럼명 | 타입 | 제약조건 | 설명 |
|---|---|---|---|
| id | INTEGER | PK | |
| section_id | BIGINT | NOT NULL, FK(home_homesection) | |
| book_id | BIGINT | NOT NULL, FK(books) | |
| display_order | INTEGER | NOT NULL | 기본값: 0 |
home_realtimeranking (실시간 랭킹)
| 컬럼명 | 타입 | 제약조건 | 설명 |
|---|---|---|---|
| id | INTEGER | PK | |
| book_id | BIGINT | NOT NULL, FK(books) | |
| rank | INTEGER | NOT NULL | |
| score | INTEGER | NOT NULL | |
| country_code | VARCHAR(2) | ||
| calculated_at | DATETIME | NOT NULL |
promo_codes (프로모션 코드)
| 컬럼명 | 타입 | 제약조건 | 설명 |
|---|---|---|---|
| id | INTEGER | PK | |
| code | VARCHAR(50) | NOT NULL, UNIQUE | |
| discount_type | VARCHAR(20) | NOT NULL | percentage, fixed |
| discount_value | DECIMAL(10,2) | NOT NULL | |
| valid_from | DATETIME | NOT NULL | |
| valid_to | DATETIME | NOT NULL | |
| max_uses | INTEGER | ||
| used_count | INTEGER | NOT NULL | 기본값: 0 |
| is_active | BOOLEAN | NOT NULL | |
| created_at | DATETIME | NOT NULL |
promo_code_usages (프로모션 코드 사용)
| 컬럼명 | 타입 | 제약조건 | 설명 |
|---|---|---|---|
| id | INTEGER | PK | |
| user_id | BIGINT | NOT NULL, FK(users) | |
| promo_code_id | BIGINT | NOT NULL, FK(promo_codes) | |
| used_at | DATETIME | NOT NULL |
payment_transactions (결제 거래)
| 컬럼명 | 타입 | 제약조건 | 설명 |
|---|---|---|---|
| id | INTEGER | PK | |
| user_id | BIGINT | NOT NULL, FK(users) | |
| subscription_id | BIGINT | FK(subscriptions) | |
| transaction_id | VARCHAR(100) | NOT NULL, UNIQUE | 외부 거래 ID |
| amount | DECIMAL(10,2) | NOT NULL | |
| currency | VARCHAR(3) | NOT NULL | 기본값: KRW |
| payment_method | VARCHAR(50) | NOT NULL | |
| status | VARCHAR(20) | NOT NULL | |
| created_at | DATETIME | NOT NULL |
search_history (검색 기록)
| 컬럼명 | 타입 | 제약조건 | 설명 |
|---|---|---|---|
| id | INTEGER | PK | |
| user_id | BIGINT | FK(users) | |
| query | VARCHAR(200) | NOT NULL | |
| result_count | INTEGER | NOT NULL | 기본값: 0 |
| clicked_book_id | BIGINT | FK(books) | |
| searched_at | DATETIME | NOT NULL |
recordings (녹음)
| 컬럼명 | 타입 | 제약조건 | 설명 |
|---|---|---|---|
| id | INTEGER | PK | |
| user_id | BIGINT | NOT NULL, FK(users) | |
| book_id | BIGINT | NOT NULL, FK(books) | |
| episode_id | BIGINT | FK(episodes) | |
| file_url | VARCHAR(500) | NOT NULL | |
| duration | INTEGER | NOT NULL | 초 단위 |
| created_at | DATETIME | NOT NULL |
stickers_popular (인기 스티커)
| 컬럼명 | 타입 | 제약조건 | 설명 |
|---|---|---|---|
| id | INTEGER | PK | |
| sticker_id | BIGINT | NOT NULL, FK(stickers) | |
| collected_count | INTEGER | NOT NULL | 기본값: 0 |
| rank | INTEGER | NOT NULL | |
| calculated_at | DATETIME | NOT NULL |
sticker_stats (스티커 통계)
| 컬럼명 | 타입 | 제약조건 | 설명 |
|---|---|---|---|
| id | INTEGER | PK | |
| sticker_id | BIGINT | NOT NULL, FK(stickers) | |
| date | DATE | NOT NULL | |
| collected_count | INTEGER | NOT NULL | 기본값: 0 |
주요 인덱스
사용자 관련
- idx_users_email: users(email)
- idx_users_social: users(social_provider, social_id)
- idx_devices_user: user_devices(user_id)
- idx_devices_device_id: user_devices(device_id)
콘텐츠 관련
- idx_books_status: books(status, is_active)
- idx_books_featured: books(is_featured, featured_at DESC)
- idx_books_pv_score: books(pv_score DESC)
- idx_episodes_book: episodes(book_id)
- idx_episode_pages_episode: episode_pages(episode_id)
- idx_episode_pages_unique: episode_pages(episode_id, page_number) UNIQUE
활동 추적
- idx_bookmarks_user: bookmarks(user_id)
- idx_history_user: reading_history(user_id)
- idx_history_last_read: reading_history(last_read_at DESC)
번역 테이블
- 모든 translation 테이블: (master_id, language_code) UNIQUE
- 모든 translation 테이블: idx_trans_master(master_id)
- 모든 translation 테이블: idx_trans_lang(language_code)
관계 다이어그램
users
├─ user_devices (1:N)
├─ bookmarks (1:N)
├─ reading_history (1:N)
├─ user_stickers (1:N)
├─ subscriptions (1:N)
└─ recordings (1:N)
books
├─ books_translation (1:N)
├─ episodes (1:N)
│ ├─ episodes_translation (1:N)
│ ├─ episode_pages (1:N)
│ └─ stickers (1:1)
│ └─ stickers_translation (1:N)
├─ book_series_mapping (N:M with book_series)
├─ book_tag_mapping (N:M with book_tags)
├─ book_categories (N:M with categories)
└─ book_characters (N:M with characters)
모든 마스터 테이블
└─ translation 테이블 (1:N, 언어별)