DB 스키마
Penta 백엔드의 전체 데이터베이스 스키마 문서이다. Django ORM 기반이며, 다국어 지원은 django-parler를 사용한다. 소프트 삭제는 django-safedelete로 처리한다.
- 개발 환경: SQLite3
- 운영 환경: PostgreSQL
- ORM: Django ORM + django-parler (다국어) + django-safedelete (소프트 삭제)
1. 사용자 도메인
User (users_user)
사용자 모델. AbstractBaseUser + PermissionsMixin + SafeDeleteModel을 상속한다. USERNAME_FIELD는 id이다.
| 필드명 | 타입 | 설명 |
|---|---|---|
| id | BigAutoField | PK |
| EmailField(255) | 이메일 (NULL 허용, 소셜 로그인 시 placeholder 생성) | |
| username | CharField(150) | 사용자명 (NULL 허용) |
| full_name | CharField(255) | 이름 |
| nickname | CharField(100) | 닉네임 (UNIQUE, 인덱스) |
| social_provider | CharField(50) | 소셜 로그인 제공자 (google, apple, kakao, line, facebook) |
| social_id | CharField(255) | 소셜 로그인 고유 ID |
| language | CharField(10) | 언어 (기본: en) |
| country | CharField(2) | 국가 코드 |
| birth_year | IntegerField | 출생 연도 |
| has_subscribed_before | BooleanField | 과거 구독 이력 여부 (기본: False) |
| is_active | BooleanField | 활성 상태 (기본: True) |
| is_staff | BooleanField | 관리자 여부 (기본: False) |
| date_joined | DateTimeField | 가입일 |
| last_login | DateTimeField | 마지막 로그인 |
| app_language | CharField(10) | 앱 표시 언어 (기본: en) |
| viewer_languages | JSONField | 콘텐츠 언어 설정 (리스트) |
| push_notification_enabled | BooleanField | 푸시 알림 마스터 토글 (기본: True) |
| notification_marketing | BooleanField | 마케팅 알림 (기본: False) |
| notification_bookmark | BooleanField | 북마크 알림 (기본: True) |
| marketing_consent | BooleanField | 마케팅 동의 (기본: False) |
| deletion_reason | CharField(50) | 탈퇴 사유 (not_used_often, expensive, lack_content, app_error, other) |
| deleted | DateTimeField | 소프트 삭제 시각 (SafeDelete 자동 관리) |
인덱스: email, (social_provider, social_id)
유니크 제약조건: (social_provider, social_id) - deleted가 NULL인 경우에만 (조건부 유니크)
주요 속성/메서드:
is_in_trial: 가입 후 3일 무료 체험 중인지 (has_subscribed_before=False일 때)is_subscription_active: 구독 활성 여부 (구독 만료일 또는 무료 체험 확인)subscription_type: 현재 구독 타입subscription_end_date: 구독 종료일
UserDevice (users_device)
사용자 기기 정보. FCM 토큰을 저장하여 푸시 알림에 사용한다.
| 필드명 | 타입 | 설명 |
|---|---|---|
| id | BigAutoField | PK |
| user_id | FK(User) | 사용자 (CASCADE) |
| device_id | CharField(255) | 기기 고유 ID (UNIQUE) |
| device_type | CharField(50) | 기기 타입 (ios, android) |
| device_model | CharField(100) | 기기 모델명 |
| app_version | CharField(20) | 앱 버전 |
| timezone | CharField(50) | 타임존 |
| fcm_token | CharField(500) | FCM 푸시 토큰 (인덱스) |
| is_active | BooleanField | 활성 상태 (기본: True) |
| push_enabled | BooleanField | 푸시 허용 (기본: True) |
| last_active | DateTimeField | 마지막 활동 (auto_now) |
| created_at | DateTimeField | 생성일 (auto_now_add) |
인덱스: (user, last_active)
LoginHistory (users_loginhistory)
로그인 이력 기록.
| 필드명 | 타입 | 설명 |
|---|---|---|
| id | BigAutoField | PK |
| user_id | FK(User) | 사용자 (CASCADE) |
| login_type | CharField(50) | 로그인 방식 (email, google, apple, kakao, line, facebook) |
| ip_address | GenericIPAddressField | IP 주소 |
| user_agent | TextField | User-Agent |
| device_id | FK(UserDevice) | 기기 (SET_NULL, NULL 허용) |
| created_at | DateTimeField | 로그인 시각 (auto_now_add) |
인덱스: (user, -created_at)
ReviewRequestLog (users_reviewrequestlog)
앱 리뷰 요청 노출 기록.
| 필드명 | 타입 | 설명 |
|---|---|---|
| id | BigAutoField | PK |
| user_id | FK(User) | 사용자 (CASCADE) |
| device_id | CharField(255) | 기기 ID |
| created_at | DateTimeField | 생성일 (auto_now_add) |
인덱스: (user, -created_at)
2. 콘텐츠 도메인
Book (books_book) + BookTranslation
도서 모델. TranslatableModel을 상속하며, 번역 필드와 비번역 필드가 분리된다.
비번역 필드 (books_book):
| 필드명 | 타입 | 설명 |
|---|---|---|
| id | BigAutoField | PK |
| book_code | CharField(10) | 도서 코드 (UNIQUE, 예: A001) |
| lexile_label | CharField(50) | 렉사일 수준 레이블 (예: 400L - 600L) |
| age_label | CharField(50) | 연령대 레이블 (예: 2-5, 3-7) |
| brand | CharField(20) | 브랜드 (disney, pixar) |
| status | CharField(20) | 상태 (ongoing, completed, hiatus) |
| views | IntegerField | 조회수 (기본: 0) |
| is_active | BooleanField | 활성 상태 (기본: True) |
| average_rating | DecimalField(3,2) | 평균 평점 (기본: 0.00) |
| pv_score | IntegerField | PV 점수 (기본: 0) |
| series_id | FK(BookSeries) | 시리즈 (SET_NULL) |
| lexile_filter_id | FK(HomeFilter) | 렉사일 필터 (SET_NULL, 1:1) |
| created_at | DateTimeField | 생성일 |
번역 필드 (books_book_translation):
| 필드명 | 타입 | 설명 |
|---|---|---|
| title | CharField(500) | 제목 |
| author | CharField(200) | 저자 |
| illustrator | CharField(200) | 일러스트레이터 |
| publisher | CharField(100) | 출판사 |
| synopsis | TextField | 시놉시스 |
| cover_url | URLField(500) | 표지 URL |
| content_url | URLField(500) | 콘텐츠 URL |
| published_date | DateField | 출판일 |
| is_new | BooleanField | NEW 뱃지 표시 여부 |
관계:
series: FK -> BookSeries (SET_NULL)characters: M2M -> Characterillustrators: M2M -> Illustratorage_filters: M2M -> HomeFilter (AGE_ 코드 필터)
인덱스: status, -views
Episode (books_episode) + EpisodeTranslation
에피소드 모델.
비번역 필드 (books_episode):
| 필드명 | 타입 | 설명 |
|---|---|---|
| id | BigAutoField | PK |
| book_id | FK(Book) | 도서 (CASCADE) |
| episode_number | IntegerField | 에피소드 번호 |
| published_date | DateField | 출판일 |
| views | IntegerField | 조회수 (기본: 0) |
번역 필드 (books_episode_translation):
| 필드명 | 타입 | 설명 |
|---|---|---|
| title | CharField(500) | 제목 |
| pages | JSONField | 페이지 URL/메타데이터 리스트 |
유니크 제약조건: (book, episode_number)
인덱스: -views
BookSeries (books_series) + Translation
| 필드명 | 타입 | 설명 |
|---|---|---|
| id | BigAutoField | PK |
| display_order | IntegerField | 표시 순서 (기본: 0) |
| created_at | DateTimeField | 생성일 |
번역 필드: name (CharField(200))
인덱스: display_order
Character (books_character) + Translation
| 필드명 | 타입 | 설명 |
|---|---|---|
| id | BigAutoField | PK |
| character_key | CharField(100) | 고유 식별자 (UNIQUE, 예: disney_jasmine) |
| brand | CharField(20) | 브랜드 (disney, pixar) |
| image_url | URLField(500) | 이미지 URL |
| display_order | IntegerField | 표시 순서 (기본: 0) |
| created_at | DateTimeField | 생성일 |
번역 필드: character_name (CharField(200))
인덱스: display_order, character_key
Illustrator (books_illustrator) + Translation
| 필드명 | 타입 | 설명 |
|---|---|---|
| id | BigAutoField | PK |
| profile_image_url | URLField(500) | 프로필 이미지 |
| display_order | IntegerField | 표시 순서 (기본: 0) |
| created_at | DateTimeField | 생성일 |
| updated_at | DateTimeField | 수정일 (auto_now) |
번역 필드: name (CharField(200)), bio (TextField)
인덱스: display_order
ReadingHistory (books_readinghistory)
읽기 이력. 에피소드 단위로 진행률을 추적한다.
| 필드명 | 타입 | 설명 |
|---|---|---|
| id | BigAutoField | PK |
| user_id | FK(User) | 사용자 (CASCADE) |
| book_id | FK(Book) | 도서 (CASCADE) |
| episode_id | FK(Episode) | 에피소드 (CASCADE) |
| last_page | IntegerField | 현재 페이지 번호 (기본: 0) |
| total_pages | IntegerField | 총 페이지 수 (기본: 0) |
| progress_percentage | FloatField | 진행률 0.0~100.0 (기본: 0.0) |
| reading_time | IntegerField | 읽기 시간 (초, 기본: 0) |
| is_completed | BooleanField | 완독 여부 (기본: False) |
| started_at | DateTimeField | 시작일 |
| last_read_at | DateTimeField | 마지막 읽은 시각 |
| completed_at | DateTimeField | 완독 시각 |
유니크 제약조건: (user, book, episode)
인덱스: (user, -last_read_at), -last_read_at, (user, book, is_completed), (user, is_completed)
Bookmark (books_bookmark)
| 필드명 | 타입 | 설명 |
|---|---|---|
| id | BigAutoField | PK |
| user_id | FK(User) | 사용자 (CASCADE) |
| book_id | FK(Book) | 도서 (CASCADE) |
| created_at | DateTimeField | 생성일 |
유니크 제약조건: (user, book)
Recording (books_recording)
사용자 녹음 파일.
| 필드명 | 타입 | 설명 |
|---|---|---|
| id | BigAutoField | PK |
| user_id | FK(User) | 사용자 (CASCADE) |
| book_id | FK(Book) | 도서 (CASCADE) |
| episode_id | FK(Episode) | 에피소드 (CASCADE, NULL 허용) |
| language_code | CharField(10) | 녹음 언어 |
| file_url | URLField(500) | 파일 URL |
| duration | IntegerField | 녹음 길이 (초) |
| created_at | DateTimeField | 생성일 |
유니크 제약조건: (user, book, episode, language_code)
인덱스: (user, book), (user, episode)
DailyStat (books_dailystat)
일별 도서 통계.
| 필드명 | 타입 | 설명 |
|---|---|---|
| id | BigAutoField | PK |
| stat_date | DateField | 통계 날짜 |
| book_id | FK(Book) | 도서 (CASCADE) |
| views | IntegerField | 조회수 (기본: 0) |
| completions | IntegerField | 완독 수 (기본: 0) |
| bookmarks | IntegerField | 북마크 수 (기본: 0) |
| stickers_earned | IntegerField | 스티커 획득 수 (기본: 0) |
유니크 제약조건: (stat_date, book)
인덱스: (stat_date, -views)
3. 홈/배너/큐레이션
HomeFilter (home_homefilter) + Translation
홈 화면 필터 옵션 (탭, 시리즈, 읽기 수준).
| 필드명 | 타입 | 설명 |
|---|---|---|
| id | BigAutoField | PK |
| filter_type | CharField(20) | 필터 타입 (tab, series, reading_level) |
| value | CharField(100) | 필터 값 (API 쿼리용) |
| code | CharField(50) | 고유 코드 (UNIQUE, 예: AGE_0_3, LEX_400_600, TAB_DISNEY) |
| display_order | IntegerField | 표시 순서 (기본: 0) |
| is_active | BooleanField | 활성 상태 (기본: True) |
| created_at | DateTimeField | 생성일 (auto_now_add) |
| updated_at | DateTimeField | 수정일 (auto_now) |
번역 필드: name (CharField(100))
정렬: filter_type, display_order, id
RealtimeRanking (home_realtimeranking)
실시간 랭킹 (국가별, 시간별).
| 필드명 | 타입 | 설명 |
|---|---|---|
| id | BigAutoField | PK |
| book_id | FK(Book) | 도서 (CASCADE) |
| country | CharField(2) | 국가 코드 (KR, US, JP 등) |
| rank | IntegerField | 순위 (1~10) |
| previous_rank | IntegerField | 이전 순위 (NULL 허용) |
| score | FloatField | 점수 (기본: 0) |
| ranking_date | DateField | 랭킹 날짜 |
| ranking_hour | IntegerField | 랭킹 시간 (0~23) |
| created_at | DateTimeField | 생성일 (auto_now_add) |
| updated_at | DateTimeField | 수정일 (auto_now) |
유니크 제약조건: (book, country, ranking_date, ranking_hour)
인덱스: (country, ranking_date, ranking_hour, rank)
주요 속성: rank_change (new/up/down/same), rank_change_value (순위 변동 수치)
Banner (books_banner) + Translation
배너 모델.
비번역 필드:
| 필드명 | 타입 | 설명 |
|---|---|---|
| id | BigAutoField | PK |
| link_type | CharField(20) | 링크 타입 (book, external, none, collection) |
| content_type | CharField(20) | 콘텐츠 타입 (book, event, collection) |
| sequence | IntegerField | 표시 순서 (기본: 0) |
| is_active | BooleanField | 활성 상태 (기본: True) |
| start_date | DateTimeField | 시작일 |
| end_date | DateTimeField | 종료일 (NULL 허용) |
번역 필드:
| 필드명 | 타입 | 설명 |
|---|---|---|
| title | CharField(200) | 제목 |
| image_url | URLField(500) | 배너 이미지 URL |
| link_url | URLField(500) | 링크 URL |
| target_type | CharField(20) | 언어별 콘텐츠 타입 |
| content_id | BigIntegerField | 언어별 Book ID |
| curation_id | BigIntegerField | 언어별 Curation ID |
인덱스: (is_active, start_date, end_date)
Curation (home_curation) + Translation
큐레이션/모아보기 모델.
| 필드명 | 타입 | 설명 |
|---|---|---|
| id | BigAutoField | PK |
| curation_type | CharField(20) | 큐레이션 타입 (theme, collection) |
| display_order | IntegerField | 표시 순서 (기본: 0) |
| is_active | BooleanField | 활성 상태 (기본: True) |
| created_at | DateTimeField | 생성일 (auto_now_add) |
| updated_at | DateTimeField | 수정일 (auto_now) |
번역 필드: title (CharField(200)), description (TextField)
관계: books M2M -> Book (through CurationItem)
CurationItem (home_curationitem)
큐레이션-도서 매핑 (through 테이블).
| 필드명 | 타입 | 설명 |
|---|---|---|
| id | BigAutoField | PK |
| curation_id | FK(Curation) | 큐레이션 (CASCADE) |
| book_id | FK(Book) | 도서 (CASCADE) |
| display_order | IntegerField | 표시 순서 (기본: 999) |
| created_at | DateTimeField | 생성일 (auto_now_add) |
유니크 제약조건: (curation, book)
4. 스티커
Sticker (stickers_sticker) + Translation
에피소드 완독 시 획득하는 스티커. 에피소드와 1:1 관계이다.
| 필드명 | 타입 | 설명 |
|---|---|---|
| id | BigAutoField | PK |
| episode_id | OneToOne(Episode) | 에피소드 (CASCADE, UNIQUE) |
| image_url | URLField(500) | 스티커 이미지 URL |
| base_score | FloatField | 사전 계산된 기본 점수 (기본: 0.0, 인덱스) |
| score_updated_at | DateTimeField | 점수 갱신 시각 |
| created_at | DateTimeField | 생성일 |
번역 필드: name (CharField(200))
인덱스: (-base_score, -created_at)
점수 계산: 신규(+3) + 캠페인(+2) + 어제수집(+1) + 인기도(+0.1*수집자수)
UserSticker (stickers_usersticker)
사용자가 수집한 스티커.
| 필드명 | 타입 | 설명 |
|---|---|---|
| id | BigAutoField | PK |
| user_id | FK(User) | 사용자 (CASCADE) |
| sticker_id | FK(Sticker) | 스티커 (CASCADE) |
| earned_at | DateTimeField | 획득 시각 |
유니크 제약조건: (user, sticker)
인덱스: (user, -earned_at), earned_at
StickerStats (stickers_stat)
스티커 수집 통계. 스티커와 1:1 관계이다.
| 필드명 | 타입 | 설명 |
|---|---|---|
| id | BigAutoField | PK |
| sticker_id | OneToOne(Sticker) | 스티커 (CASCADE) |
| total_collectors | IntegerField | 총 수집자 수 (기본: 0) |
| daily_collectors | IntegerField | 일간 수집자 수 (기본: 0) |
| weekly_collectors | IntegerField | 주간 수집자 수 (기본: 0) |
| monthly_collectors | IntegerField | 월간 수집자 수 (기본: 0) |
| last_updated | DateTimeField | 마지막 갱신 시각 |
인덱스: -total_collectors, -daily_collectors, -weekly_collectors, -monthly_collectors
StickersPopular (stickers_popular)
인기 스티커 목록 (기간별, 국가별).
| 필드명 | 타입 | 설명 |
|---|---|---|
| id | BigAutoField | PK |
| sticker_id | FK(Sticker) | 스티커 (CASCADE) |
| position | IntegerField | 위치 (기본: 0) |
| popularity_score | FloatField | 인기 점수 (기본: 0.0) |
| period | CharField(20) | 기간 (daily, weekly, monthly, all-time) |
| country | CharField(2) | 국가 코드 (NULL이면 글로벌) |
| created_at | DateTimeField | 생성일 |
| updated_at | DateTimeField | 수정일 (auto_now) |
유니크 제약조건: (sticker, period, country)
인덱스: (period, country, position), updated_at, -popularity_score
StickersMissing (stickers_missing)
사용자가 보유하지 않은 스티커 목록.
| 필드명 | 타입 | 설명 |
|---|---|---|
| id | BigAutoField | PK |
| user_id | FK(User) | 사용자 (CASCADE) |
| sticker_id | FK(Sticker) | 스티커 (CASCADE) |
| position | IntegerField | 위치 (기본: 0) |
| created_at | DateTimeField | 생성일 |
| updated_at | DateTimeField | 수정일 (auto_now) |
유니크 제약조건: (user, sticker)
인덱스: (user, position), updated_at
StickersUpcoming (stickers_upcoming)
공개 예정 스티커 목록.
| 필드명 | 타입 | 설명 |
|---|---|---|
| id | BigAutoField | PK |
| sticker_id | FK(Sticker) | 스티커 (CASCADE) |
| position | IntegerField | 위치 (기본: 0) |
| release_date | DateTimeField | 공개 예정일 |
| is_featured | BooleanField | 추천 여부 (기본: False) |
| created_at | DateTimeField | 생성일 |
| updated_at | DateTimeField | 수정일 (auto_now) |
인덱스: (release_date, position), (is_featured, release_date), updated_at
StickerWishlist (stickers_wishlist)
사용자의 관심 스티커(찜) 목록.
| 필드명 | 타입 | 설명 |
|---|---|---|
| id | BigAutoField | PK |
| user_id | FK(User) | 사용자 (CASCADE) |
| sticker_id | FK(Sticker) | 스티커 (CASCADE) |
| created_at | DateTimeField | 생성일 |
유니크 제약조건: (user, sticker)
인덱스: (user, created_at), (sticker, created_at)
StickerExposure (stickers_exposure)
유저에게 Popular/Missing 영역에서 노출된 스티커 기록. 최근 7일 내 노출 스티커에 -2점 적용.
| 필드명 | 타입 | 설명 |
|---|---|---|
| id | BigAutoField | PK |
| user_id | FK(User) | 사용자 (CASCADE) |
| sticker_id | FK(Sticker) | 스티커 (CASCADE) |
| exposure_type | CharField(20) | 노출 유형 (popular, missing) |
| exposed_at | DateTimeField | 노출 시각 |
유니크 제약조건: (user, sticker, exposure_type) - 같은 조합은 하나만 유지, exposed_at만 갱신
인덱스: (user, exposed_at), exposed_at
StickerCampaign (stickers_campaign)
이벤트/캠페인과 스티커 연결. 캠페인 대상 스티커에 기본 +2점 부스트 적용.
| 필드명 | 타입 | 설명 |
|---|---|---|
| id | BigAutoField | PK |
| sticker_id | FK(Sticker) | 스티커 (CASCADE) |
| event_id | FK(Event) | 이벤트 (CASCADE, NULL 허용) |
| campaign_name | CharField(200) | 캠페인 이름 |
| is_active | BooleanField | 활성 상태 (기본: True) |
| priority_boost | IntegerField | 우선순위 부스트 점수 (기본: 2) |
| start_date | DateTimeField | 시작일 |
| end_date | DateTimeField | 종료일 (NULL 허용) |
| created_at | DateTimeField | 생성일 |
| updated_at | DateTimeField | 수정일 (auto_now) |
인덱스: (sticker, is_active), (start_date, end_date), (event, is_active)
주요 속성: is_ongoing - 현재 진행중인 캠페인인지 확인
5. 결제
Subscription (payments_subscription)
구독 모델. User와 1:1 관계이다.
| 필드명 | 타입 | 설명 |
|---|---|---|
| id | BigAutoField | PK |
| user_id | OneToOne(User) | 사용자 (CASCADE) |
| type | CharField(20) | 구독 타입 (1_month, 6_month, 12_month) |
| start_date | DateTimeField | 시작일 |
| end_date | DateTimeField | 종료일 (구독 유효성의 주요 기준) |
| cancelled_at | DateTimeField | 취소일 (NULL 허용) |
| is_cancelled | BooleanField | 취소 여부 (기본: False, 자동갱신만 중지) |
| pause_until | DateTimeField | 일시정지 종료일 (NULL 허용) |
| grace_until | DateTimeField | 유예 기간 종료일 (NULL 허용) |
| auto_renew | BooleanField | 자동갱신 (기본: False) |
| next_billing_date | DateTimeField | 다음 결제일 |
| promo_code | CharField(50) | 적용된 프로모 코드 |
| cancellation_reason | CharField(50) | 취소 사유 |
| has_referral_bonus | BooleanField | 래퍼럴 혜택 여부 |
| referral_bonus_weeks | IntegerField | 래퍼럴 보너스 주수 (기본: 0) |
| referral_minimum_period_days | IntegerField | 래퍼럴 최소 유지 기간 일수 (기본: 14) |
| referral_benefits_revoked | BooleanField | 래퍼럴 혜택 회수 여부 (기본: False) |
| created_at | DateTimeField | 생성일 (auto_now_add) |
| updated_at | DateTimeField | 수정일 (auto_now) |
인덱스: (user, is_cancelled, end_date), end_date
주요 속성/메서드:
effective_end: grace_until 고려한 실제 종료일is_active: 현재 활성 여부 (일시정지, 만료 확인)status: active / cancelled / paused / expiredcancel(reason): 취소 처리 (래퍼럴 혜택 회수 포함)reduce_by_weeks(weeks): 구독 기간 단축
PaymentTransaction (payments_transaction)
결제 거래 기록.
| 필드명 | 타입 | 설명 |
|---|---|---|
| id | CharField(32) | PK (수동 설정) |
| user_id | FK(User) | 사용자 (CASCADE) |
| type | CharField(20) | 거래 타입 (subscription, renewal, refund, cancellation) |
| status | CharField(20) | 상태 (pending, completed, failed, refunded) |
| amount | DecimalField(10,2) | 금액 |
| currency | CharField(3) | 통화 (기본: USD) |
| payment_method | CharField(50) | 결제 수단 (기본: google_play) |
| gateway_transaction_id | CharField(200) | 게이트웨이 거래 ID |
| purchase_token | CharField(500) | 구매 토큰 |
| metadata | JSONField | 추가 데이터 (기본: ) |
| external_id | CharField(200) | 외부 ID |
| gateway | CharField(50) | 게이트웨이 |
| created_at | DateTimeField | 생성일 (auto_now_add) |
| processed_at | DateTimeField | 처리일 (auto_now) |
인덱스: (user, status), gateway_transaction_id, purchase_token
AppStoreTransaction (payments_app_store_transaction)
Apple App Store 거래 캐시. 멱등적 검증을 위한 모델.
| 필드명 | 타입 | 설명 |
|---|---|---|
| id | BigAutoField | PK |
| user_id | FK(User) | 사용자 (CASCADE) |
| transaction_id | CharField(200) | 거래 ID (UNIQUE) |
| original_transaction_id | CharField(200) | 원본 거래 ID |
| product_id | CharField(200) | 상품 ID |
| subscription_type | CharField(50) | 구독 타입 |
| environment | CharField(20) | 환경 (Sandbox/Production) |
| app_account_token | CharField(200) | 앱 계정 토큰 |
| signed_at | DateTimeField | 서명 시각 |
| signed_transaction_jws | TextField | JWS 서명 거래 데이터 |
| purchase_date | DateTimeField | 구매일 |
| expires_date | DateTimeField | 만료일 |
| price_amount | DecimalField(10,2) | 가격 (기본: 0.00) |
| price_currency | CharField(3) | 통화 (기본: USD) |
| auto_renew_status | BooleanField | 자동갱신 상태 (기본: True) |
| is_trial_period | BooleanField | 체험판 여부 (기본: False) |
| is_in_intro_offer_period | BooleanField | 소개 오퍼 여부 (기본: False) |
| ownership_type | CharField(20) | 소유 유형 (PURCHASED, FAMILY_SHARED) |
| raw_transaction | JSONField | 원본 거래 JSON |
| raw_renewal | JSONField | 원본 갱신 JSON |
| last_verified_at | DateTimeField | 마지막 검증 시각 |
| created_at | DateTimeField | 생성일 (auto_now_add) |
| updated_at | DateTimeField | 수정일 (auto_now) |
인덱스: transaction_id, original_transaction_id, (user, transaction_id), app_account_token
GooglePlayReceipt (payments_google_play_receipt)
Google Play v2 API 구독 영수증.
| 필드명 | 타입 | 설명 |
|---|---|---|
| id | BigAutoField | PK |
| latest_order_id | CharField(200) | 최신 주문 ID (UNIQUE) |
| purchase_token | CharField(500) | 구매 토큰 |
| linked_purchase_token | CharField(500) | 연결된 구매 토큰 (토큰 교체 추적) |
| subscription_state | CharField(50) | 구독 상태 (ACTIVE, CANCELED, EXPIRED 등) |
| acknowledgement_state | CharField(50) | 확인 상태 (PENDING, ACKNOWLEDGED 등) |
| start_time | DateTimeField | 구독 시작일 |
| expiry_time | DateTimeField | 만료/갱신 시점 |
| price_amount_micros | BigIntegerField | 가격 (micros, 1,000,000 = 1.00) |
| currency_code | CharField(3) | 통화 코드 |
| region_code | CharField(2) | 지역 코드 |
| product_id | CharField(200) | 상품 ID |
| base_plan_id | CharField(200) | 기본 플랜 ID |
| offer_id | CharField(200) | 오퍼 ID |
| offer_tags | JSONField | 오퍼 태그 (리스트) |
| auto_renew_enabled | BooleanField | 자동갱신 설정 (기본: True) |
| canceled_state_context | JSONField | 취소 사유/시점 (기본: ) |
| paused_state_context | JSONField | 일시중지 사유/시점 (기본: ) |
| raw_json | JSONField | 원본 JSON 스냅샷 |
| user_id | FK(User) | 사용자 (CASCADE, NULL 허용) |
| transaction_id | OneToOne(PaymentTransaction) | 거래 (CASCADE, NULL 허용) |
| created_at | DateTimeField | 생성일 (auto_now_add) |
| updated_at | DateTimeField | 수정일 (auto_now) |
인덱스: latest_order_id, purchase_token, subscription_state, acknowledgement_state, start_time, expiry_time, product_id
RefundRequest (payments_refundrequest)
환불 요청 및 추적.
| 필드명 | 타입 | 설명 |
|---|---|---|
| id | BigAutoField | PK |
| transaction_id | FK(PaymentTransaction) | 거래 (CASCADE) |
| user_id | FK(User) | 사용자 (CASCADE) |
| status | CharField(20) | 상태 (pending, approved, rejected, completed, failed) |
| refund_type | CharField(30) | 환불 유형 (full, partial, subscription_cancel) |
| reason | CharField(30) | 사유 (user_request, technical_issue, billing_error 등) |
| reason_description | TextField | 상세 사유 |
| google_refund_id | CharField(200) | Google 환불 ID (UNIQUE) |
| google_order_id | CharField(200) | Google 주문 ID |
| purchase_token | CharField(500) | 구매 토큰 |
| revoke_entitlement | BooleanField | 즉시 접근 취소 여부 (기본: True) |
| initiated_by_id | FK(User) | 환불 처리 관리자 (SET_NULL) |
| metadata | JSONField | 추가 데이터 (기본: ) |
| error_message | TextField | 에러 메시지 |
| created_at | DateTimeField | 생성일 (auto_now_add) |
| processed_at | DateTimeField | 처리 시작일 |
| completed_at | DateTimeField | 완료일 |
인덱스: status, google_refund_id, google_order_id, purchase_token, created_at
6. 프로모코드/레퍼럴
PromoCode (promocodes_promocode) + Translation
프로모션 코드 모델.
| 필드명 | 타입 | 설명 |
|---|---|---|
| id | BigAutoField | PK |
| code | CharField(50) | 프로모 코드 (UNIQUE) |
| type | CharField(20) | 타입 (corporate, influencer, referral) |
| name | CharField(100) | 프로모션 이름 |
| description | TextField | 프로모션 설명 |
| bonus_weeks | IntegerField | 보너스 주수 (기본: 0) |
| max_uses | IntegerField | 최대 사용 횟수 (NULL이면 무제한) |
| current_uses | IntegerField | 현재 사용 횟수 (기본: 0) |
| one_time_only | BooleanField | 1회 사용 제한 (기본: False) |
| new_users_only | BooleanField | 신규 사용자 전용 (기본: False) |
| valid_from | DateTimeField | 유효 시작일 |
| valid_until | DateTimeField | 유효 종료일 (NULL 허용) |
| is_active | BooleanField | 활성 여부 (기본: True) |
| subscription_types | JSONField | 적용 가능 구독 타입 (리스트) |
| is_referral_stackable | BooleanField | 래퍼럴 중복 가능 (기본: False) |
| referrer_bonus_weeks | IntegerField | 추천인 보너스 주수 |
| offer_mapping | JSONField | 플랜별 offer ID 매핑 (기본: ) |
| discount_mapping | JSONField | 플랜별 할인율(%) 매핑 (기본: ) |
| ios_offer_code | CharField(100) | iOS App Store offer 코드 |
| qr_code_url | URLField | QR 코드 URL |
| partner_name | CharField(100) | 협력사 이름 |
| created_at | DateTimeField | 생성일 (auto_now_add) |
| updated_at | DateTimeField | 수정일 (auto_now) |
번역 필드: banner_text (CharField(200)) - 프로모 배너 텍스트
인덱스: (code, is_active), type, (valid_from, valid_until)
주요 속성/메서드: remaining_uses, is_valid, can_be_used_by_user(user), apply_to_subscription(type, price)
PromoCodeUsage (promocodes_usage)
프로모 코드 사용 내역.
| 필드명 | 타입 | 설명 |
|---|---|---|
| id | BigAutoField | PK |
| user_id | FK(User) | 사용자 (CASCADE) |
| promo_code_id | FK(PromoCode) | 프로모 코드 (CASCADE) |
| subscription_id | FK(Subscription) | 구독 (CASCADE, NULL 허용) |
| used_at | DateTimeField | 사용 시각 (auto_now_add) |
| ip_address | GenericIPAddressField | IP 주소 |
| user_agent | TextField | User-Agent |
| applied_bonus_weeks | IntegerField | 적용된 보너스 주수 (기본: 0) |
유니크 제약조건: (user, promo_code) - 사용자당 프로모 코드 1회 사용
인덱스: (promo_code, -used_at), (user, -used_at)
ReferralCode (promocodes_referral)
래퍼럴 코드 (추천인 시스템). PromoCode와 1:1 관계이다.
| 필드명 | 타입 | 설명 |
|---|---|---|
| id | BigAutoField | PK |
| referrer_id | FK(User) | 추천인 (CASCADE) |
| promo_code_id | OneToOne(PromoCode) | 연결된 프로모 코드 (CASCADE) |
| total_referrals | IntegerField | 총 추천 수 (기본: 0) |
| successful_referrals | IntegerField | 성공한 추천 수 (기본: 0) |
| created_at | DateTimeField | 생성일 (auto_now_add) |
AppliedPromoCode (promocodes_applied)
임시 적용된 프로모 코드 (IAP 결제 전 임시 저장). User와 1:1 관계이다.
| 필드명 | 타입 | 설명 |
|---|---|---|
| id | BigAutoField | PK |
| user_id | OneToOne(User) | 사용자 (CASCADE) |
| promo_code_id | FK(PromoCode) | 프로모 코드 (CASCADE) |
| created_at | DateTimeField | 생성일 (auto_now_add) |
| expires_at | DateTimeField | 만료 시각 |
ReferralReward (promocodes_referral_reward)
래퍼럴 리워드 내역.
| 필드명 | 타입 | 설명 |
|---|---|---|
| id | BigAutoField | PK |
| referral_code_id | FK(ReferralCode) | 래퍼럴 코드 (CASCADE) |
| user_id | FK(User) | 수혜자 (CASCADE) |
| reward_type | CharField(20) | 리워드 유형 (referrer: 추천인, referee: 피추천인) |
| bonus_weeks | IntegerField | 추가 구독 주수 |
| subscription_id | FK(Subscription) | 구독 (CASCADE) |
| status | CharField(20) | 상태 (pending, applied, revoked) |
| awarded_at | DateTimeField | 지급 시각 (auto_now_add) |
| applied_at | DateTimeField | 실제 적용 시각 |
| revoked_at | DateTimeField | 회수 시각 |
| revoke_reason | TextField | 회수 사유 |
주요 메서드: revoke(reason) - 리워드 회수, 구독 기간 차감 포함
PartnerPromotion (promocodes_partner_promotion)
파트너 프로모션 링크 관리.
| 필드명 | 타입 | 설명 |
|---|---|---|
| id | BigAutoField | PK |
| code | CharField(20) | 짧은 URL 코드 (UNIQUE) |
| partner_name | CharField(100) | 파트너 이름 |
| promo_code_id | FK(PromoCode) | 프로모 코드 (CASCADE, NULL 허용) |
| landing_page | URLField | 파트너 전용 랜딩 페이지 |
| custom_message | TextField | 파트너 전용 메시지 |
| custom_image_url | URLField | 파트너 이미지 URL |
| is_active | BooleanField | 활성 상태 (기본: True) |
| total_clicks | IntegerField | 총 클릭 수 (기본: 0) |
| total_conversions | IntegerField | 총 전환 수 (기본: 0) |
| total_views | IntegerField | 총 조회 수 (기본: 0) |
| created_at | DateTimeField | 생성일 (auto_now_add) |
| updated_at | DateTimeField | 수정일 (auto_now) |
인덱스: code, partner_name
주요 속성: conversion_rate - 전환율 (%)
PromoCodeClick (promocodes_click_tracking)
프로모션 클릭 추적.
| 필드명 | 타입 | 설명 |
|---|---|---|
| id | BigAutoField | PK |
| promo_code_id | FK(PromoCode) | 프로모 코드 (CASCADE, NULL 허용) |
| partner_promotion_id | FK(PartnerPromotion) | 파트너 프로모션 (SET_NULL, NULL 허용) |
| ip_address | GenericIPAddressField | IP 주소 |
| user_agent | TextField | User-Agent |
| referer | URLField | 리퍼러 URL |
| utm_source | CharField(100) | UTM 소스 |
| utm_medium | CharField(100) | UTM 매체 |
| utm_campaign | CharField(100) | UTM 캠페인 |
| utm_term | CharField(100) | UTM 키워드 |
| utm_content | CharField(100) | UTM 콘텐츠 |
| device_type | CharField(20) | 기기 유형 (mobile, tablet, desktop) |
| os | CharField(50) | 운영체제 |
| browser | CharField(50) | 브라우저 |
| clicked_at | DateTimeField | 클릭 시각 (auto_now_add) |
| converted | BooleanField | 전환 여부 (기본: False) |
| converted_at | DateTimeField | 전환 시각 |
인덱스: (promo_code, -clicked_at), (partner_promotion, -clicked_at), (converted, -clicked_at)
7. 이벤트
Event (events_news) + Translation
이벤트/뉴스 모델.
비번역 필드:
| 필드명 | 타입 | 설명 |
|---|---|---|
| id | BigAutoField | PK |
| type | CharField(20) | 유형 (event, news) |
| image_url | URLField | 대표 이미지 URL |
| start_date | DateTimeField | 시작일 |
| end_date | DateTimeField | 종료일 (NULL 허용) |
| button_action | CharField(20) | 버튼 동작 (subscribe, external_link, deeplink, none) |
| button_action_value | CharField(500) | 버튼 동작 값 |
| button_color | CharField(7) | 버튼 색상 (기본: #660FD1) |
| target_audience | CharField(20) | 대상 (all, guests, new_users, non_subscribers, subscribers) |
| sequence | IntegerField | 표시 순서 (기본: 0) |
| is_active | BooleanField | 활성 상태 (기본: True) |
| created_at | DateTimeField | 생성일 (auto_now_add) |
| updated_at | DateTimeField | 수정일 (auto_now) |
번역 필드:
| 필드명 | 타입 | 설명 |
|---|---|---|
| title | CharField(200) | 제목 |
| content | TextField | 내용 |
| subtitle | CharField(200) | 부제목 |
| body | TextField | 본문 |
| notes | JSONField | 참고사항 (리스트) |
| hero_image_url | URLField(500) | 히어로 이미지 URL |
| button_text | CharField(100) | 버튼 텍스트 |
정렬: -start_date
주요 속성: is_ongoing - 현재 진행중인지 확인
EventParticipation (events_news_participations)
이벤트 참여 기록.
| 필드명 | 타입 | 설명 |
|---|---|---|
| id | BigAutoField | PK |
| user_id | FK(User) | 사용자 (CASCADE) |
| event_id | FK(Event) | 이벤트 (CASCADE) |
| participated_at | DateTimeField | 참여 시각 (auto_now_add) |
| completed | BooleanField | 완료 여부 (기본: False) |
| completed_at | DateTimeField | 완료 시각 |
유니크 제약조건: (user, event)
8. 알림
Notification (notifications_notification) + Translation
알림 모델.
| 필드명 | 타입 | 설명 |
|---|---|---|
| id | BigAutoField | PK |
| type | CharField(50) | 유형 (user_content: 유저소식, penta_news: 팬타소식) |
| subtype | CharField(50) | 서브타입 (sticker_release, inquiry_response, feature_update, brand_news, event_promotion) |
| action_type | CharField(50) | 동작 유형 (book_detail, event_news, event_event, inquiry_detail) |
| target_id | BigIntegerField | 대상 ID (book_id 또는 event_id) |
| created_at | DateTimeField | 생성일 |
번역 필드: title (CharField(500)), message (TextField)
인덱스: -created_at, (type, subtype)
UserNotification (notifications_usernotification)
사용자별 알림 수신 상태.
| 필드명 | 타입 | 설명 |
|---|---|---|
| id | BigAutoField | PK |
| user_id | FK(User) | 사용자 (CASCADE) |
| notification_id | FK(Notification) | 알림 (CASCADE) |
| is_read | BooleanField | 읽음 여부 (기본: False) |
| received_at | DateTimeField | 수신 시각 |
유니크 제약조건: (user, notification)
인덱스: (user, is_read, -received_at)
PushMessageTemplate (notifications_push_message_template) + Translation
푸시 메시지 템플릿.
| 필드명 | 타입 | 설명 |
|---|---|---|
| id | BigAutoField | PK |
| push_type | CharField(30) | 푸시 유형 (trial_reminder, sticker_release, weekly_update, inactive_routine, brand_news, event_promotion, feature_update) |
| is_active | BooleanField | 활성 상태 (기본: True) |
| created_at | DateTimeField | 생성일 (auto_now_add) |
번역 필드: title (CharField(200)), body (TextField)
인덱스: (push_type, is_active)
PushLog (notifications_push_log)
푸시 발송 기록.
| 필드명 | 타입 | 설명 |
|---|---|---|
| id | BigAutoField | PK |
| user_id | FK(User) | 사용자 (CASCADE) |
| push_type | CharField(30) | 푸시 유형 |
| template_id | FK(PushMessageTemplate) | 템플릿 (SET_NULL, NULL 허용) |
| sent_at | DateTimeField | 발송 시각 (auto_now_add) |
| status | CharField(10) | 상태 (success, failed) |
인덱스: (user, push_type, -sent_at), (user, -sent_at)
9. 지원
FAQ (support_faq) + Translation
자주 묻는 질문.
| 필드명 | 타입 | 설명 |
|---|---|---|
| id | BigAutoField | PK |
| category | CharField(20) | 카테고리 (account, payment, content, technical, other) |
| order | IntegerField | 표시 순서 (기본: 0) |
| is_active | BooleanField | 활성 상태 (기본: True) |
| created_at | DateTimeField | 생성일 (auto_now_add) |
| updated_at | DateTimeField | 수정일 (auto_now) |
번역 필드: question (CharField(500)), answer (TextField)
정렬: category, order, -created_at
Inquiry (support_inquiry)
1:1 문의.
| 필드명 | 타입 | 설명 |
|---|---|---|
| id | BigAutoField | PK |
| user_id | FK(User) | 사용자 (CASCADE) |
| type | CharField(20) | 유형 (bug, payment, content, account, suggestion, other) |
| subject | CharField(200) | 제목 |
| message | TextField | 내용 |
| status | CharField(20) | 상태 (pending, in_progress, resolved, closed) |
| admin_response | TextField | 관리자 응답 |
| responded_at | DateTimeField | 응답 시각 |
| created_at | DateTimeField | 생성일 (auto_now_add) |
| updated_at | DateTimeField | 수정일 (auto_now) |
정렬: -created_at
Announcement (support_announcement) + Translation
공지사항.
| 필드명 | 타입 | 설명 |
|---|---|---|
| id | BigAutoField | PK |
| is_active | BooleanField | 활성 상태 (기본: True) |
| published_at | DateTimeField | 게시일 |
| created_at | DateTimeField | 생성일 (auto_now_add) |
| updated_at | DateTimeField | 수정일 (auto_now) |
번역 필드: title (CharField(200)), content (TextField)
정렬: -published_at
10. 블로그
Post (blog_post) + Translation
블로그 포스트.
비번역 필드:
| 필드명 | 타입 | 설명 |
|---|---|---|
| id | BigAutoField | PK |
| slug | SlugField(200) | URL 슬러그 (UNIQUE) |
| author_id | FK(User) | 작성자 (SET_NULL) |
| category_id | FK(Category) | 카테고리 (SET_NULL) |
| status | CharField(20) | 상태 (draft, published, archived) |
| is_featured | BooleanField | 메인 노출 여부 (기본: False) |
| thumbnail | ImageField | 썸네일 이미지 |
| views | PositiveIntegerField | 조회수 (기본: 0) |
| published_at | DateTimeField | 게시일 |
| created_at | DateTimeField | 생성일 (auto_now_add) |
| updated_at | DateTimeField | 수정일 (auto_now) |
번역 필드:
| 필드명 | 타입 | 설명 |
|---|---|---|
| title | CharField(200) | 제목 |
| excerpt | TextField | 요약 (목록 표시용) |
| content | TextField | 본문 (HTML 또는 Markdown) |
| meta_title | CharField(70) | SEO 제목 |
| meta_description | CharField(160) | SEO 설명 |
관계: tags M2M -> Tag
인덱스: (status, -published_at), (is_featured, status), -views
Category (blog_category) + Translation
블로그 카테고리.
| 필드명 | 타입 | 설명 |
|---|---|---|
| id | BigAutoField | PK |
| slug | SlugField(100) | URL 슬러그 (UNIQUE) |
| display_order | PositiveIntegerField | 표시 순서 (기본: 0) |
| is_active | BooleanField | 활성 상태 (기본: True) |
| created_at | DateTimeField | 생성일 (auto_now_add) |
번역 필드: name (CharField(100)), description (TextField)
Tag (blog_tag) + Translation
블로그 태그 (해시태그).
| 필드명 | 타입 | 설명 |
|---|---|---|
| id | BigAutoField | PK |
| slug | SlugField(50) | URL 슬러그 (UNIQUE, 자동 생성) |
| post_count | PositiveIntegerField | 사용된 포스트 수 (캐시, 기본: 0) |
| created_at | DateTimeField | 생성일 (auto_now_add) |
번역 필드: name (CharField(50))
정렬: -post_count, slug
BlogImage (blog_image)
블로그 콘텐츠 내 이미지.
| 필드명 | 타입 | 설명 |
|---|---|---|
| id | BigAutoField | PK |
| post_id | FK(Post) | 포스트 (CASCADE, NULL 허용) |
| image | ImageField | 이미지 파일 (WebP 변환 저장) |
| original_filename | CharField(255) | 원본 파일명 |
| file_size | PositiveIntegerField | 파일 크기 (bytes, 기본: 0) |
| created_at | DateTimeField | 생성일 (auto_now_add) |
PostView (blog_post_view)
포스트 조회 기록 (어뷰징 방지). 동일 IP에서 1시간 내 중복 조회 시 views 카운트 미증가.
| 필드명 | 타입 | 설명 |
|---|---|---|
| id | BigAutoField | PK |
| post_id | FK(Post) | 포스트 (CASCADE) |
| ip_address | GenericIPAddressField | IP 주소 |
| session_key | CharField(40) | 세션 키 |
| user_id | FK(User) | 사용자 (SET_NULL, NULL 허용) |
| user_agent | CharField(500) | User-Agent |
| viewed_at | DateTimeField | 조회 시각 (auto_now_add) |
인덱스: (post, ip_address, viewed_at), (post, session_key, viewed_at), -viewed_at
주요 클래스 메서드: can_count_view(), record_view(), cleanup_old_records(days=7)
기타
AppConfig (app_config)
앱 동적 설정 (key-value).
| 필드명 | 타입 | 설명 |
|---|---|---|
| id | BigAutoField | PK |
| key | CharField(50) | 설정 키 (UNIQUE, 인덱스, 예: min_required_version) |
| value | CharField(100) | 설정 값 (NULL이면 비활성) |
| description | TextField | 설명 |
| updated_at | DateTimeField | 수정일 (auto_now) |
| created_at | DateTimeField | 생성일 (auto_now_add) |
캐싱: 5분 캐시 (get_value(key) 메서드)