// 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 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) // 글쓰기 권한 posts Post[] moderators BoardModerator[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([status, sortOrder]) @@index([type, requiresApproval]) @@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") @@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") }