Files
msgapp/prisma/schema.prisma

784 lines
20 KiB
Plaintext
Raw Normal View History

2025-10-08 23:04:12 +09:00
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
2025-11-01 11:38:25 +09:00
/*
2025-10-09 05:40:03 +09:00
generator erd {
provider = "prisma-erd-generator"
output = "../public/erd.svg"
}
2025-11-01 11:38:25 +09:00
*/
2025-10-09 05:40:03 +09:00
2025-10-08 23:04:12 +09:00
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
2025-10-08 23:44:21 +09:00
// ==== Enums ====
enum BoardStatus {
active
hidden
archived
}
2025-10-09 11:23:27 +09:00
2025-10-08 23:44:21 +09:00
enum AccessLevel {
2025-10-09 11:23:27 +09:00
public // 비회원도 접근
member // 로그인 필요
moderator // 운영진 이상
admin // 관리자만
2025-10-08 23:44:21 +09:00
}
enum PostStatus {
published
hidden
deleted
}
2025-10-09 11:23:27 +09:00
enum AttachmentType {
image
file
}
2025-10-08 23:44:21 +09:00
enum BoardModRole {
MODERATOR
MANAGER
}
enum UserStatus {
active
suspended
withdrawn
}
enum AuthLevel {
USER
MOD
ADMIN
}
2025-10-09 11:23:27 +09:00
enum ReactionType {
RECOMMEND
REPORT
}
enum SanctionType {
SUSPEND
BAN
DOWNGRADE
}
enum TargetType {
POST
COMMENT
USER
BOARD
SYSTEM
}
// RBAC 리소스/액션 정의
enum Resource {
BOARD
POST
COMMENT
USER
ADMIN
}
enum Action {
READ
CREATE
UPDATE
DELETE
MODERATE
ADMINISTER
}
2025-10-08 23:44:21 +09:00
// ==== Models ====
// 게시판 대분류
model BoardCategory {
id String @id @default(cuid())
name String @unique
slug String @unique
sortOrder Int @default(0)
status String @default("active")
boards Board[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([status, sortOrder])
@@map("board_categories")
}
2025-10-08 23:44:21 +09:00
// 게시판 정의(관리자 생성/정렬)
model Board {
2025-10-09 11:23:27 +09:00
id String @id @default(cuid())
name String
slug String @unique // URL용
description String?
sortOrder Int @default(0)
status BoardStatus @default(active)
allowAnonymousPost Boolean @default(false) // 익명 글 허용
allowSecretComment Boolean @default(false) // 비밀댓글 허용
isAdultOnly Boolean @default(false) // 성인 인증 필요 여부
requiredTags Json?
requiredFields Json?
readLevel AccessLevel @default(public) // 읽기 권한
writeLevel AccessLevel @default(member) // 글쓰기 권한
// 대분류
categoryId String?
category BoardCategory? @relation(fields: [categoryId], references: [id], onDelete: SetNull)
2025-10-31 16:27:04 +09:00
// 뷰 타입 설정 (동적 테이블 참조)
mainPageViewTypeId String?
mainPageViewType BoardViewType? @relation("MainPageViewType", fields: [mainPageViewTypeId], references: [id], onDelete: SetNull)
listViewTypeId String?
listViewType BoardViewType? @relation("ListViewType", fields: [listViewTypeId], references: [id], onDelete: SetNull)
2025-10-09 11:23:27 +09:00
posts Post[]
moderators BoardModerator[]
2025-10-08 23:44:21 +09:00
2025-10-09 11:23:27 +09:00
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
2025-10-08 23:44:21 +09:00
@@index([status, sortOrder])
@@index([categoryId])
2025-10-31 16:27:04 +09:00
@@index([mainPageViewTypeId])
@@index([listViewTypeId])
2025-10-09 11:23:27 +09:00
@@map("boards")
2025-10-08 23:44:21 +09:00
}
2025-10-31 16:27:04 +09:00
// 게시판 뷰 타입 정의 (enum 대신 데이터 테이블)
model BoardViewType {
id String @id @default(cuid())
key String @unique // 예: preview, text, special_rank
name String // 표시용 이름
scope String // 용도 구분: 'main' | 'list' 등 자유 텍스트
// 역참조
mainBoards Board[] @relation("MainPageViewType")
listBoards Board[] @relation("ListViewType")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([scope])
@@map("board_view_types")
}
2025-10-08 23:44:21 +09:00
// 게시판 운영진 매핑
model BoardModerator {
2025-10-09 11:23:27 +09:00
id String @id @default(cuid())
boardId String
userId String
role BoardModRole @default(MODERATOR)
2025-10-08 23:44:21 +09:00
2025-10-09 11:23:27 +09:00
board Board @relation(fields: [boardId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [userId], onDelete: Cascade)
2025-10-08 23:44:21 +09:00
createdAt DateTime @default(now())
@@unique([boardId, userId]) // 한 게시판당 1회 매핑
@@index([userId])
2025-10-09 11:23:27 +09:00
@@map("board_moderators")
2025-10-08 23:44:21 +09:00
}
// 게시글(게시판 내 컨텐츠)
model Post {
2025-10-09 11:23:27 +09:00
id String @id @default(cuid())
boardId String
authorId String?
title String
content String
status PostStatus @default(published)
isAnonymous Boolean @default(false)
isPinned Boolean @default(false)
pinnedOrder Int?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
lastActivityAt DateTime @default(now())
board Board @relation(fields: [boardId], references: [id], onDelete: Cascade)
author User? @relation(fields: [authorId], references: [userId], onDelete: SetNull)
comments Comment[]
attachments Attachment[]
postTags PostTag[]
reactions Reaction[]
reports Report[]
stat PostStat?
viewLogs PostViewLog[]
2025-10-08 23:44:21 +09:00
@@index([boardId, status, createdAt])
@@index([boardId, isPinned, pinnedOrder])
2025-10-09 11:23:27 +09:00
@@map("posts")
2025-10-08 23:44:21 +09:00
}
2025-10-09 11:23:27 +09:00
// 사용자
2025-10-08 23:04:12 +09:00
model User {
2025-10-09 11:23:27 +09:00
userId String @id @default(cuid())
nickname String @unique
passwordHash String?
name String
birth DateTime
phone String @unique
rank Int @default(0)
2025-11-02 04:39:23 +09:00
// 누적 포인트, 레벨, 등급(0~10)
points Int @default(0)
level Int @default(1)
grade Int @default(0)
2025-10-09 11:23:27 +09:00
status UserStatus @default(active)
authLevel AuthLevel @default(USER)
2025-10-08 23:44:21 +09:00
profileImage String?
lastLoginAt DateTime?
2025-10-09 11:23:27 +09:00
isAdultVerified Boolean @default(false)
loginFailCount Int @default(0)
2025-10-08 23:44:21 +09:00
agreementTermsAt DateTime
2025-10-09 11:23:27 +09:00
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
posts Post[]
boardModerations BoardModerator[]
comments Comment[]
userRoles UserRole[]
reportsFiled Report[] @relation("ReportReporter")
reactions Reaction[] @relation("UserReactions")
messagesSent Message[] @relation("MessageSender")
messagesReceived Message[] @relation("MessageReceiver")
blocksInitiated Block[] @relation("Blocker")
blocksReceived Block[] @relation("Blocked")
pointTxns PointTransaction[]
sanctions Sanction[]
nicknameChanges NicknameChange[]
passwordResetTokens PasswordResetToken[] @relation("PasswordResetUser")
auditLogs AuditLog[] @relation("AuditActor")
loginSessions LoginSession[]
adminNotifications AdminNotification[] @relation("AdminNotificationCreator")
ipBlocksCreated IPBlock[] @relation("IPBlockCreator")
postViewLogs PostViewLog[] @relation("PostViewLogUser")
couponRedemptions CouponRedemption[]
2025-10-08 23:44:21 +09:00
@@index([status])
@@index([createdAt])
2025-10-09 11:23:27 +09:00
@@map("users")
2025-10-08 23:04:12 +09:00
}
2025-10-09 11:23:27 +09:00
// 역할 정의 (RBAC)
model Role {
roleId String @id @default(cuid())
name String @unique
description String?
userRoles UserRole[]
permissions RolePermission[]
2025-10-09 11:23:27 +09:00
createdAt DateTime @default(now())
@@map("roles")
}
// 역할별 권한 매핑 (리소스×액션)
model RolePermission {
id String @id @default(cuid())
roleId String
resource Resource
action Action
allowed Boolean @default(true)
createdAt DateTime @default(now())
role Role @relation(fields: [roleId], references: [roleId], onDelete: Cascade)
@@unique([roleId, resource, action])
@@index([resource, action])
@@map("role_permissions")
}
2025-10-09 11:23:27 +09:00
// 사용자-역할 매핑 (다대다)
model UserRole {
id String @id @default(cuid())
userId String
roleId String
user User @relation(fields: [userId], references: [userId], onDelete: Cascade)
role Role @relation(fields: [roleId], references: [roleId], onDelete: Cascade)
createdAt DateTime @default(now())
@@unique([userId, roleId])
@@index([roleId])
@@map("user_roles")
}
// 댓글
2025-10-08 23:44:21 +09:00
model Comment {
2025-10-09 11:23:27 +09:00
id String @id @default(cuid())
postId String
2025-11-02 15:13:03 +09:00
parentId String? // 부모 댓글 ID (null이면 최상위 댓글)
depth Int @default(0) // 댓글 깊이 (0=최상위, 1=1단계 대댓글, 2=2단계 대댓글)
2025-10-09 11:23:27 +09:00
authorId String?
content String
isAnonymous Boolean @default(false)
isSecret Boolean @default(false)
secretPasswordHash String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
2025-11-02 15:13:03 +09:00
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
parent Comment? @relation("CommentReplies", fields: [parentId], references: [id], onDelete: Cascade)
replies Comment[] @relation("CommentReplies")
author User? @relation(fields: [authorId], references: [userId], onDelete: SetNull)
reports Report[]
2025-10-09 11:23:27 +09:00
@@index([postId, createdAt])
2025-11-02 15:13:03 +09:00
@@index([parentId])
2025-10-09 11:23:27 +09:00
@@map("comments")
}
// 태그 및 매핑
model Tag {
tagId String @id @default(cuid())
name String @unique
slug String @unique
postTags PostTag[]
createdAt DateTime @default(now())
@@map("tags")
}
model PostTag {
id String @id @default(cuid())
postId String
tagId String
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
tag Tag @relation(fields: [tagId], references: [tagId], onDelete: Cascade)
@@unique([postId, tagId])
@@index([tagId])
@@map("post_tags")
}
// 첨부파일(이미지/파일)
model Attachment {
id String @id @default(cuid())
postId String
url String
type AttachmentType @default(image)
size Int?
width Int?
height Int?
sortOrder Int?
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
@@index([postId])
@@map("attachments")
}
// 반응(추천/신고)
model Reaction {
id String @id @default(cuid())
2025-10-08 23:44:21 +09:00
postId String
2025-10-09 11:23:27 +09:00
userId String?
clientHash String?
type ReactionType @default(RECOMMEND)
createdAt DateTime @default(now())
2025-10-08 23:44:21 +09:00
2025-10-09 11:23:27 +09:00
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
user User? @relation("UserReactions", fields: [userId], references: [userId], onDelete: SetNull)
@@unique([postId, type, userId, clientHash])
@@index([postId, type])
@@index([userId])
@@index([clientHash])
@@map("reactions")
}
// 게시글 열람 로그(게시판별 열람 로그 요구)
model PostViewLog {
id String @id @default(cuid())
postId String
userId String?
ip String?
userAgent String?
createdAt DateTime @default(now())
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
user User? @relation("PostViewLogUser", fields: [userId], references: [userId], onDelete: SetNull)
2025-10-08 23:44:21 +09:00
@@index([postId, createdAt])
2025-10-09 11:23:27 +09:00
@@index([userId])
@@map("post_view_logs")
}
// 게시글 통계 카운터
model PostStat {
postId String @id
views Int @default(0)
recommendCount Int @default(0)
reportCount Int @default(0)
commentsCount Int @default(0)
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
@@map("post_stats")
}
// 신고(게시글/댓글 대상)
model Report {
id String @id @default(cuid())
reporterId String?
targetType TargetType
postId String?
commentId String?
reason String
status String @default("open")
createdAt DateTime @default(now())
reporter User? @relation("ReportReporter", fields: [reporterId], references: [userId], onDelete: SetNull)
post Post? @relation(fields: [postId], references: [id], onDelete: Cascade)
comment Comment? @relation(fields: [commentId], references: [id], onDelete: Cascade)
@@index([targetType])
@@index([postId])
@@index([commentId])
@@map("reports")
}
// 쪽지(DM)
model Message {
id String @id @default(cuid())
senderId String
receiverId String
body String
readAt DateTime?
createdAt DateTime @default(now())
sender User @relation("MessageSender", fields: [senderId], references: [userId], onDelete: Cascade)
receiver User @relation("MessageReceiver", fields: [receiverId], references: [userId], onDelete: Cascade)
@@index([senderId, createdAt])
@@index([receiverId, createdAt])
@@map("messages")
}
// 차단
model Block {
id String @id @default(cuid())
blockerId String
blockedId String
createdAt DateTime @default(now())
blocker User @relation("Blocker", fields: [blockerId], references: [userId], onDelete: Cascade)
blocked User @relation("Blocked", fields: [blockedId], references: [userId], onDelete: Cascade)
@@unique([blockerId, blockedId])
@@index([blockedId])
@@map("blocks")
}
// 포인트 트랜잭션
model PointTransaction {
id String @id @default(cuid())
userId String
amount Int // + 적립, - 차감
reason String
referenceType TargetType?
referenceId String?
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [userId], onDelete: Cascade)
@@index([userId, createdAt])
@@map("point_transactions")
}
// 레벨 임계치(선택)
model LevelThreshold {
id String @id @default(cuid())
level Int @unique
minPoints Int
@@map("level_thresholds")
}
// 금칙어 정책
model BannedKeyword {
id String @id @default(cuid())
pattern String @unique
appliesTo TargetType // POST/COMMENT/MESSAGE 등
severity Int @default(1)
active Boolean @default(true)
createdAt DateTime @default(now())
@@index([appliesTo])
@@map("banned_keywords")
}
// 제재 이력
model Sanction {
id String @id @default(cuid())
userId String
type SanctionType
reason String
startAt DateTime @default(now())
endAt DateTime?
createdBy String?
user User @relation(fields: [userId], references: [userId], onDelete: Cascade)
@@index([userId, type])
@@map("sanctions")
}
// 감사 로그
model AuditLog {
id String @id @default(cuid())
actorId String?
action String
targetType TargetType?
targetId String?
meta Json?
createdAt DateTime @default(now())
actor User? @relation("AuditActor", fields: [actorId], references: [userId], onDelete: SetNull)
@@index([action, createdAt])
@@map("audit_logs")
}
// 관리자 알림함(신고/포인트 이상/후기 등 이벤트)
model AdminNotification {
id String @id @default(cuid())
type String
message String
targetType TargetType?
targetId String?
createdBy String?
createdAt DateTime @default(now())
readAt DateTime?
createdByUser User? @relation("AdminNotificationCreator", fields: [createdBy], references: [userId], onDelete: SetNull)
@@index([type, createdAt])
@@map("admin_notifications")
}
// 로그인 기기/최근 접속 IP 기록
model LoginSession {
id String @id @default(cuid())
userId String
device String?
ip String?
userAgent String?
createdAt DateTime @default(now())
lastSeenAt DateTime @default(now())
user User @relation(fields: [userId], references: [userId], onDelete: Cascade)
@@index([userId, lastSeenAt])
@@map("login_sessions")
}
// IP 차단 목록
model IPBlock {
id String @id @default(cuid())
ip String
reason String?
active Boolean @default(true)
createdBy String?
createdAt DateTime @default(now())
createdByUser User? @relation("IPBlockCreator", fields: [createdBy], references: [userId], onDelete: SetNull)
@@unique([ip, active])
@@map("ip_blocks")
}
// 닉네임 변경 이력
model NicknameChange {
id String @id @default(cuid())
userId String
oldNickname String
newNickname String
changedAt DateTime @default(now())
user User @relation(fields: [userId], references: [userId], onDelete: Cascade)
@@index([userId, changedAt])
@@map("nickname_changes")
}
// 인증 보조 - 비밀번호 재설정 토큰
model PasswordResetToken {
id String @id @default(cuid())
userId String
token String @unique
expiresAt DateTime
usedAt DateTime?
createdAt DateTime @default(now())
user User @relation("PasswordResetUser", fields: [userId], references: [userId], onDelete: Cascade)
@@index([userId, expiresAt])
@@map("password_reset_tokens")
2025-10-08 23:04:12 +09:00
}
// 쿠폰
model Coupon {
id String @id @default(cuid())
code String @unique
title String
description String?
stock Int @default(0) // 총 재고
maxPerUser Int @default(1) // 1인당 제한
expiresAt DateTime?
active Boolean @default(true)
createdAt DateTime @default(now())
redemptions CouponRedemption[]
@@index([active, expiresAt])
@@map("coupons")
}
model CouponRedemption {
id String @id @default(cuid())
couponId String
userId String
createdAt DateTime @default(now())
coupon Coupon @relation(fields: [couponId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [userId], onDelete: Cascade)
@@unique([couponId, userId])
@@index([userId, createdAt])
@@map("coupon_redemptions")
}
// 제휴업체(위치 기반)
model Partner {
id String @id @default(cuid())
2025-10-10 11:22:43 +09:00
name String @unique
category String
2025-11-07 21:36:34 +09:00
categoryId String?
latitude Float
longitude Float
address String?
2025-11-02 02:46:20 +09:00
imageUrl String?
sortOrder Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
2025-11-07 21:36:34 +09:00
categoryRef PartnerCategory? @relation(fields: [categoryId], references: [id])
@@index([category])
2025-11-07 21:36:34 +09:00
@@index([categoryId])
2025-11-02 02:46:20 +09:00
@@index([sortOrder])
@@map("partners")
}
2025-11-07 21:36:34 +09:00
// 제휴업체 카테고리(관리자 생성/삭제)
model PartnerCategory {
id String @id @default(cuid())
name String @unique
sortOrder Int @default(0)
createdAt DateTime @default(now())
partners Partner[]
@@index([sortOrder])
@@map("partner_categories")
}
// 배너/공지 노출용
model Banner {
id String @id @default(cuid())
title String
imageUrl String
linkUrl String?
active Boolean @default(true)
sortOrder Int @default(0)
startAt DateTime?
endAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([active, sortOrder])
@@index([startAt, endAt])
@@map("banners")
}
// 제휴 문의
model PartnerInquiry {
id String @id @default(cuid())
name String
contact String
category String?
message String
status String @default("pending") // pending/approved/rejected
createdAt DateTime @default(now())
approvedAt DateTime?
@@index([status, createdAt])
@@map("partner_inquiries")
}
// 제휴업소 등록 요청
model PartnerRequest {
id String @id @default(cuid())
name String
category String
latitude Float
longitude Float
address String?
contact String?
status String @default("pending") // pending/approved/rejected
createdAt DateTime @default(now())
approvedAt DateTime?
@@index([status, createdAt])
@@map("partner_requests")
}
2025-10-31 16:27:04 +09:00
// 시스템 설정 (key-value 형태)
model Setting {
id String @id @default(cuid())
key String @unique
value String // JSON 문자열로 저장
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("settings")
}
2025-11-02 02:46:20 +09:00
// 메인 노출용 제휴 샵 가로 스크롤 데이터
model PartnerShop {
id String @id @default(cuid())
region String
name String
address String
imageUrl String
sortOrder Int @default(0)
active Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([name, region])
@@index([active, sortOrder])
@@map("partner_shops")
}