706 lines
17 KiB
Plaintext
706 lines
17 KiB
Plaintext
// 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"
|
|
}
|
|
|
|
generator erd {
|
|
provider = "prisma-erd-generator"
|
|
output = "../public/erd.svg"
|
|
}
|
|
|
|
datasource db {
|
|
provider = "sqlite"
|
|
url = env("DATABASE_URL")
|
|
}
|
|
|
|
// ==== Enums ====
|
|
enum BoardStatus {
|
|
active
|
|
hidden
|
|
archived
|
|
}
|
|
|
|
enum BoardType {
|
|
general
|
|
special
|
|
}
|
|
|
|
enum AccessLevel {
|
|
public // 비회원도 접근
|
|
member // 로그인 필요
|
|
moderator // 운영진 이상
|
|
admin // 관리자만
|
|
}
|
|
|
|
enum PostStatus {
|
|
published
|
|
hidden
|
|
deleted
|
|
}
|
|
|
|
enum AttachmentType {
|
|
image
|
|
file
|
|
}
|
|
|
|
enum BoardModRole {
|
|
MODERATOR
|
|
MANAGER
|
|
}
|
|
|
|
enum UserStatus {
|
|
active
|
|
suspended
|
|
withdrawn
|
|
}
|
|
|
|
enum AuthLevel {
|
|
USER
|
|
MOD
|
|
ADMIN
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// ==== 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")
|
|
}
|
|
|
|
// 게시판 정의(관리자 생성/정렬)
|
|
model Board {
|
|
id String @id @default(cuid())
|
|
name String
|
|
slug String @unique // URL용
|
|
description String?
|
|
sortOrder Int @default(0)
|
|
status BoardStatus @default(active)
|
|
type BoardType @default(general) // 일반/특수
|
|
requiresApproval Boolean @default(false) // 게시물 승인 필요 여부
|
|
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)
|
|
|
|
posts Post[]
|
|
moderators BoardModerator[]
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@index([status, sortOrder])
|
|
@@index([type, requiresApproval])
|
|
@@index([categoryId])
|
|
@@map("boards")
|
|
}
|
|
|
|
// 게시판 운영진 매핑
|
|
model BoardModerator {
|
|
id String @id @default(cuid())
|
|
boardId String
|
|
userId String
|
|
role BoardModRole @default(MODERATOR)
|
|
|
|
board Board @relation(fields: [boardId], references: [id], onDelete: Cascade)
|
|
user User @relation(fields: [userId], references: [userId], onDelete: Cascade)
|
|
|
|
createdAt DateTime @default(now())
|
|
|
|
@@unique([boardId, userId]) // 한 게시판당 1회 매핑
|
|
@@index([userId])
|
|
@@map("board_moderators")
|
|
}
|
|
|
|
// 게시글(게시판 내 컨텐츠)
|
|
model Post {
|
|
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[]
|
|
|
|
@@index([boardId, status, createdAt])
|
|
@@index([boardId, isPinned, pinnedOrder])
|
|
@@map("posts")
|
|
}
|
|
|
|
// 사용자
|
|
model User {
|
|
userId String @id @default(cuid())
|
|
nickname String @unique
|
|
passwordHash String?
|
|
name String
|
|
birth DateTime
|
|
phone String @unique
|
|
rank Int @default(0)
|
|
|
|
status UserStatus @default(active)
|
|
authLevel AuthLevel @default(USER)
|
|
profileImage String?
|
|
lastLoginAt DateTime?
|
|
isAdultVerified Boolean @default(false)
|
|
loginFailCount Int @default(0)
|
|
agreementTermsAt DateTime
|
|
|
|
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[]
|
|
|
|
@@index([status])
|
|
@@index([createdAt])
|
|
@@map("users")
|
|
}
|
|
|
|
// 역할 정의 (RBAC)
|
|
model Role {
|
|
roleId String @id @default(cuid())
|
|
name String @unique
|
|
description String?
|
|
|
|
userRoles UserRole[]
|
|
permissions RolePermission[]
|
|
|
|
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")
|
|
}
|
|
|
|
// 사용자-역할 매핑 (다대다)
|
|
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")
|
|
}
|
|
|
|
// 댓글
|
|
model Comment {
|
|
id String @id @default(cuid())
|
|
postId String
|
|
authorId String?
|
|
content String
|
|
isAnonymous Boolean @default(false)
|
|
isSecret Boolean @default(false)
|
|
secretPasswordHash String?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
|
|
author User? @relation(fields: [authorId], references: [userId], onDelete: SetNull)
|
|
reports Report[]
|
|
|
|
@@index([postId, createdAt])
|
|
@@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())
|
|
postId String
|
|
userId String?
|
|
clientHash String?
|
|
type ReactionType @default(RECOMMEND)
|
|
createdAt DateTime @default(now())
|
|
|
|
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)
|
|
|
|
@@index([postId, createdAt])
|
|
@@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")
|
|
}
|
|
|
|
// 쿠폰
|
|
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())
|
|
name String @unique
|
|
category String
|
|
latitude Float
|
|
longitude Float
|
|
address String?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@index([category])
|
|
@@map("partners")
|
|
}
|
|
|
|
// 배너/공지 노출용
|
|
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")
|
|
}
|