스키마 작업완료
This commit is contained in:
@@ -15,8 +15,6 @@ datasource db {
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
// schema.prisma (MySQL)
|
||||
|
||||
// ==== Enums ====
|
||||
enum BoardStatus {
|
||||
active
|
||||
@@ -24,11 +22,16 @@ enum BoardStatus {
|
||||
archived
|
||||
}
|
||||
|
||||
enum BoardType {
|
||||
general
|
||||
special
|
||||
}
|
||||
|
||||
enum AccessLevel {
|
||||
public // 비회원도 접근
|
||||
member // 로그인 필요
|
||||
moderator // 운영진 이상
|
||||
admin // 관리자만
|
||||
public // 비회원도 접근
|
||||
member // 로그인 필요
|
||||
moderator // 운영진 이상
|
||||
admin // 관리자만
|
||||
}
|
||||
|
||||
enum PostStatus {
|
||||
@@ -37,6 +40,11 @@ enum PostStatus {
|
||||
deleted
|
||||
}
|
||||
|
||||
enum AttachmentType {
|
||||
image
|
||||
file
|
||||
}
|
||||
|
||||
enum BoardModRole {
|
||||
MODERATOR
|
||||
MANAGER
|
||||
@@ -54,114 +62,489 @@ enum AuthLevel {
|
||||
ADMIN
|
||||
}
|
||||
|
||||
enum ReactionType {
|
||||
RECOMMEND
|
||||
REPORT
|
||||
}
|
||||
|
||||
enum SanctionType {
|
||||
SUSPEND
|
||||
BAN
|
||||
DOWNGRADE
|
||||
}
|
||||
|
||||
enum TargetType {
|
||||
POST
|
||||
COMMENT
|
||||
USER
|
||||
BOARD
|
||||
SYSTEM
|
||||
}
|
||||
|
||||
// ==== Models ====
|
||||
|
||||
// 게시판 정의(관리자 생성/정렬)
|
||||
model Board {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
slug String @unique // URL용
|
||||
description String?
|
||||
sortOrder Int @default(0) // 원하는 순서
|
||||
status BoardStatus @default(active)
|
||||
isAdultOnly Boolean @default(false) // 성인 인증 필요 여부
|
||||
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) // 글쓰기 권한
|
||||
readLevel AccessLevel @default(public) // 읽기 권한
|
||||
writeLevel AccessLevel @default(member) // 글쓰기 권한
|
||||
|
||||
posts Post[]
|
||||
moderators BoardModerator[]
|
||||
posts Post[]
|
||||
moderators BoardModerator[]
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@map("boards")
|
||||
@@index([status, sortOrder])
|
||||
@@index([type, requiresApproval])
|
||||
@@map("boards")
|
||||
}
|
||||
|
||||
// 게시판 운영진 매핑
|
||||
model BoardModerator {
|
||||
id String @id @default(cuid())
|
||||
boardId String
|
||||
userId String
|
||||
role BoardModRole @default(MODERATOR)
|
||||
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)
|
||||
board Board @relation(fields: [boardId], references: [id], onDelete: Cascade)
|
||||
user User @relation(fields: [userId], references: [userId], onDelete: Cascade)
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@map("board_moderators")
|
||||
@@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)
|
||||
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? // 상단 고정 정렬
|
||||
isPinned Boolean @default(false)
|
||||
pinnedOrder Int?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
lastActivityAt DateTime @default(now())
|
||||
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[]
|
||||
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[]
|
||||
|
||||
@@map("posts")
|
||||
@@index([boardId, status, createdAt])
|
||||
@@index([boardId, isPinned, pinnedOrder])
|
||||
@@map("posts")
|
||||
}
|
||||
|
||||
// ---- 참고: 기존 User 모델 예시 ----
|
||||
// 사용자
|
||||
model User {
|
||||
userId String @id @default(cuid())
|
||||
nickname String @unique
|
||||
passwordHash String?
|
||||
name String
|
||||
birth DateTime
|
||||
phone String @unique
|
||||
rank Int @default(0)
|
||||
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)
|
||||
status UserStatus @default(active)
|
||||
authLevel AuthLevel @default(USER)
|
||||
profileImage String?
|
||||
lastLoginAt DateTime?
|
||||
isAdultVerified Boolean @default(false)
|
||||
loginFailCount Int @default(0)
|
||||
isAdultVerified Boolean @default(false)
|
||||
loginFailCount Int @default(0)
|
||||
agreementTermsAt DateTime
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
posts Post[]
|
||||
boardModerations BoardModerator[]
|
||||
comments Comment[]
|
||||
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")
|
||||
|
||||
@@map("users")
|
||||
@@index([status])
|
||||
@@index([createdAt])
|
||||
@@map("users")
|
||||
}
|
||||
|
||||
// 역할 정의 (RBAC)
|
||||
model Role {
|
||||
roleId String @id @default(cuid())
|
||||
name String @unique
|
||||
description String?
|
||||
|
||||
userRoles UserRole[]
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@map("roles")
|
||||
}
|
||||
|
||||
// 사용자-역할 매핑 (다대다)
|
||||
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
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
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)
|
||||
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
|
||||
author User? @relation(fields: [authorId], references: [userId], onDelete: SetNull)
|
||||
reports Report[]
|
||||
|
||||
@@map("comments")
|
||||
@@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")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user