Files
msgapp/prisma/schema.prisma

586 lines
14 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-10-09 05:40:03 +09:00
generator erd {
provider = "prisma-erd-generator"
output = "../public/erd.svg"
}
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
enum BoardType {
general
special
}
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 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)
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) // 글쓰기 권한
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])
2025-10-09 11:23:27 +09:00
@@index([type, requiresApproval])
@@map("boards")
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)
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")
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
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())
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
}