스키마 작업완료
This commit is contained in:
5
package-lock.json
generated
5
package-lock.json
generated
@@ -11,7 +11,8 @@
|
|||||||
"@prisma/client": "^6.17.0",
|
"@prisma/client": "^6.17.0",
|
||||||
"next": "15.5.4",
|
"next": "15.5.4",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0"
|
"react-dom": "19.1.0",
|
||||||
|
"zod": "^3.23.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "2.2.0",
|
"@biomejs/biome": "2.2.0",
|
||||||
@@ -7016,9 +7017,7 @@
|
|||||||
"version": "3.23.8",
|
"version": "3.23.8",
|
||||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz",
|
"resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz",
|
||||||
"integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==",
|
"integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/client": "^6.17.0",
|
"@prisma/client": "^6.17.0",
|
||||||
|
"zod": "^3.23.8",
|
||||||
"next": "15.5.4",
|
"next": "15.5.4",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0"
|
"react-dom": "19.1.0"
|
||||||
|
|||||||
466
prisma/migrations/20251009021103_init_schema/migration.sql
Normal file
466
prisma/migrations/20251009021103_init_schema/migration.sql
Normal file
@@ -0,0 +1,466 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the `Message` table. If the table is not empty, all the data it contains will be lost.
|
||||||
|
- You are about to drop the `User` table. If the table is not empty, all the data it contains will be lost.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- DropTable
|
||||||
|
PRAGMA foreign_keys=off;
|
||||||
|
DROP TABLE "Message";
|
||||||
|
PRAGMA foreign_keys=on;
|
||||||
|
|
||||||
|
-- DropTable
|
||||||
|
PRAGMA foreign_keys=off;
|
||||||
|
DROP TABLE "User";
|
||||||
|
PRAGMA foreign_keys=on;
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "boards" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"slug" TEXT NOT NULL,
|
||||||
|
"description" TEXT,
|
||||||
|
"sortOrder" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"status" TEXT NOT NULL DEFAULT 'active',
|
||||||
|
"type" TEXT NOT NULL DEFAULT 'general',
|
||||||
|
"requiresApproval" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"allowAnonymousPost" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"allowSecretComment" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"isAdultOnly" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"requiredTags" JSONB,
|
||||||
|
"requiredFields" JSONB,
|
||||||
|
"readLevel" TEXT NOT NULL DEFAULT 'public',
|
||||||
|
"writeLevel" TEXT NOT NULL DEFAULT 'member',
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" DATETIME NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "board_moderators" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"boardId" TEXT NOT NULL,
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
"role" TEXT NOT NULL DEFAULT 'MODERATOR',
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
CONSTRAINT "board_moderators_boardId_fkey" FOREIGN KEY ("boardId") REFERENCES "boards" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
CONSTRAINT "board_moderators_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users" ("userId") ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "posts" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"boardId" TEXT NOT NULL,
|
||||||
|
"authorId" TEXT,
|
||||||
|
"title" TEXT NOT NULL,
|
||||||
|
"content" TEXT NOT NULL,
|
||||||
|
"status" TEXT NOT NULL DEFAULT 'published',
|
||||||
|
"isAnonymous" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"isPinned" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"pinnedOrder" INTEGER,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" DATETIME NOT NULL,
|
||||||
|
"lastActivityAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
CONSTRAINT "posts_boardId_fkey" FOREIGN KEY ("boardId") REFERENCES "boards" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
CONSTRAINT "posts_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "users" ("userId") ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "users" (
|
||||||
|
"userId" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"nickname" TEXT NOT NULL,
|
||||||
|
"passwordHash" TEXT,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"birth" DATETIME NOT NULL,
|
||||||
|
"phone" TEXT NOT NULL,
|
||||||
|
"rank" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"status" TEXT NOT NULL DEFAULT 'active',
|
||||||
|
"authLevel" TEXT NOT NULL DEFAULT 'USER',
|
||||||
|
"profileImage" TEXT,
|
||||||
|
"lastLoginAt" DATETIME,
|
||||||
|
"isAdultVerified" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"loginFailCount" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"agreementTermsAt" DATETIME NOT NULL,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" DATETIME NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "roles" (
|
||||||
|
"roleId" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"description" TEXT,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "user_roles" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
"roleId" TEXT NOT NULL,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
CONSTRAINT "user_roles_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users" ("userId") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
CONSTRAINT "user_roles_roleId_fkey" FOREIGN KEY ("roleId") REFERENCES "roles" ("roleId") ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "comments" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"postId" TEXT NOT NULL,
|
||||||
|
"authorId" TEXT,
|
||||||
|
"content" TEXT NOT NULL,
|
||||||
|
"isAnonymous" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"isSecret" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"secretPasswordHash" TEXT,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" DATETIME NOT NULL,
|
||||||
|
CONSTRAINT "comments_postId_fkey" FOREIGN KEY ("postId") REFERENCES "posts" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
CONSTRAINT "comments_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "users" ("userId") ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "tags" (
|
||||||
|
"tagId" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"slug" TEXT NOT NULL,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "post_tags" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"postId" TEXT NOT NULL,
|
||||||
|
"tagId" TEXT NOT NULL,
|
||||||
|
CONSTRAINT "post_tags_postId_fkey" FOREIGN KEY ("postId") REFERENCES "posts" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
CONSTRAINT "post_tags_tagId_fkey" FOREIGN KEY ("tagId") REFERENCES "tags" ("tagId") ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "attachments" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"postId" TEXT NOT NULL,
|
||||||
|
"url" TEXT NOT NULL,
|
||||||
|
"type" TEXT NOT NULL DEFAULT 'image',
|
||||||
|
"size" INTEGER,
|
||||||
|
"width" INTEGER,
|
||||||
|
"height" INTEGER,
|
||||||
|
"sortOrder" INTEGER,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
CONSTRAINT "attachments_postId_fkey" FOREIGN KEY ("postId") REFERENCES "posts" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "reactions" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"postId" TEXT NOT NULL,
|
||||||
|
"userId" TEXT,
|
||||||
|
"clientHash" TEXT,
|
||||||
|
"type" TEXT NOT NULL DEFAULT 'RECOMMEND',
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
CONSTRAINT "reactions_postId_fkey" FOREIGN KEY ("postId") REFERENCES "posts" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
CONSTRAINT "reactions_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users" ("userId") ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "post_view_logs" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"postId" TEXT NOT NULL,
|
||||||
|
"userId" TEXT,
|
||||||
|
"ip" TEXT,
|
||||||
|
"userAgent" TEXT,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
CONSTRAINT "post_view_logs_postId_fkey" FOREIGN KEY ("postId") REFERENCES "posts" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
CONSTRAINT "post_view_logs_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users" ("userId") ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "post_stats" (
|
||||||
|
"postId" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"views" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"recommendCount" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"reportCount" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"commentsCount" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
CONSTRAINT "post_stats_postId_fkey" FOREIGN KEY ("postId") REFERENCES "posts" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "reports" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"reporterId" TEXT,
|
||||||
|
"targetType" TEXT NOT NULL,
|
||||||
|
"postId" TEXT,
|
||||||
|
"commentId" TEXT,
|
||||||
|
"reason" TEXT NOT NULL,
|
||||||
|
"status" TEXT NOT NULL DEFAULT 'open',
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
CONSTRAINT "reports_reporterId_fkey" FOREIGN KEY ("reporterId") REFERENCES "users" ("userId") ON DELETE SET NULL ON UPDATE CASCADE,
|
||||||
|
CONSTRAINT "reports_postId_fkey" FOREIGN KEY ("postId") REFERENCES "posts" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
CONSTRAINT "reports_commentId_fkey" FOREIGN KEY ("commentId") REFERENCES "comments" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "messages" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"senderId" TEXT NOT NULL,
|
||||||
|
"receiverId" TEXT NOT NULL,
|
||||||
|
"body" TEXT NOT NULL,
|
||||||
|
"readAt" DATETIME,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
CONSTRAINT "messages_senderId_fkey" FOREIGN KEY ("senderId") REFERENCES "users" ("userId") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
CONSTRAINT "messages_receiverId_fkey" FOREIGN KEY ("receiverId") REFERENCES "users" ("userId") ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "blocks" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"blockerId" TEXT NOT NULL,
|
||||||
|
"blockedId" TEXT NOT NULL,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
CONSTRAINT "blocks_blockerId_fkey" FOREIGN KEY ("blockerId") REFERENCES "users" ("userId") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
CONSTRAINT "blocks_blockedId_fkey" FOREIGN KEY ("blockedId") REFERENCES "users" ("userId") ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "point_transactions" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
"amount" INTEGER NOT NULL,
|
||||||
|
"reason" TEXT NOT NULL,
|
||||||
|
"referenceType" TEXT,
|
||||||
|
"referenceId" TEXT,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
CONSTRAINT "point_transactions_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users" ("userId") ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "level_thresholds" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"level" INTEGER NOT NULL,
|
||||||
|
"minPoints" INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "banned_keywords" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"pattern" TEXT NOT NULL,
|
||||||
|
"appliesTo" TEXT NOT NULL,
|
||||||
|
"severity" INTEGER NOT NULL DEFAULT 1,
|
||||||
|
"active" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "sanctions" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
"type" TEXT NOT NULL,
|
||||||
|
"reason" TEXT NOT NULL,
|
||||||
|
"startAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"endAt" DATETIME,
|
||||||
|
"createdBy" TEXT,
|
||||||
|
CONSTRAINT "sanctions_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users" ("userId") ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "audit_logs" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"actorId" TEXT,
|
||||||
|
"action" TEXT NOT NULL,
|
||||||
|
"targetType" TEXT,
|
||||||
|
"targetId" TEXT,
|
||||||
|
"meta" JSONB,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
CONSTRAINT "audit_logs_actorId_fkey" FOREIGN KEY ("actorId") REFERENCES "users" ("userId") ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "admin_notifications" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"type" TEXT NOT NULL,
|
||||||
|
"message" TEXT NOT NULL,
|
||||||
|
"targetType" TEXT,
|
||||||
|
"targetId" TEXT,
|
||||||
|
"createdBy" TEXT,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"readAt" DATETIME,
|
||||||
|
CONSTRAINT "admin_notifications_createdBy_fkey" FOREIGN KEY ("createdBy") REFERENCES "users" ("userId") ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "login_sessions" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
"device" TEXT,
|
||||||
|
"ip" TEXT,
|
||||||
|
"userAgent" TEXT,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"lastSeenAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
CONSTRAINT "login_sessions_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users" ("userId") ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "ip_blocks" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"ip" TEXT NOT NULL,
|
||||||
|
"reason" TEXT,
|
||||||
|
"active" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"createdBy" TEXT,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
CONSTRAINT "ip_blocks_createdBy_fkey" FOREIGN KEY ("createdBy") REFERENCES "users" ("userId") ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "nickname_changes" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
"oldNickname" TEXT NOT NULL,
|
||||||
|
"newNickname" TEXT NOT NULL,
|
||||||
|
"changedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
CONSTRAINT "nickname_changes_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users" ("userId") ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "password_reset_tokens" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
"token" TEXT NOT NULL,
|
||||||
|
"expiresAt" DATETIME NOT NULL,
|
||||||
|
"usedAt" DATETIME,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
CONSTRAINT "password_reset_tokens_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users" ("userId") ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "boards_slug_key" ON "boards"("slug");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "boards_status_sortOrder_idx" ON "boards"("status", "sortOrder");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "boards_type_requiresApproval_idx" ON "boards"("type", "requiresApproval");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "board_moderators_userId_idx" ON "board_moderators"("userId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "board_moderators_boardId_userId_key" ON "board_moderators"("boardId", "userId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "posts_boardId_status_createdAt_idx" ON "posts"("boardId", "status", "createdAt");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "posts_boardId_isPinned_pinnedOrder_idx" ON "posts"("boardId", "isPinned", "pinnedOrder");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "users_nickname_key" ON "users"("nickname");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "users_phone_key" ON "users"("phone");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "users_status_idx" ON "users"("status");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "users_createdAt_idx" ON "users"("createdAt");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "roles_name_key" ON "roles"("name");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "user_roles_roleId_idx" ON "user_roles"("roleId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "user_roles_userId_roleId_key" ON "user_roles"("userId", "roleId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "comments_postId_createdAt_idx" ON "comments"("postId", "createdAt");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "tags_name_key" ON "tags"("name");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "tags_slug_key" ON "tags"("slug");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "post_tags_tagId_idx" ON "post_tags"("tagId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "post_tags_postId_tagId_key" ON "post_tags"("postId", "tagId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "attachments_postId_idx" ON "attachments"("postId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "reactions_postId_type_idx" ON "reactions"("postId", "type");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "reactions_userId_idx" ON "reactions"("userId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "reactions_clientHash_idx" ON "reactions"("clientHash");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "reactions_postId_type_userId_clientHash_key" ON "reactions"("postId", "type", "userId", "clientHash");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "post_view_logs_postId_createdAt_idx" ON "post_view_logs"("postId", "createdAt");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "post_view_logs_userId_idx" ON "post_view_logs"("userId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "reports_targetType_idx" ON "reports"("targetType");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "reports_postId_idx" ON "reports"("postId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "reports_commentId_idx" ON "reports"("commentId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "messages_senderId_createdAt_idx" ON "messages"("senderId", "createdAt");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "messages_receiverId_createdAt_idx" ON "messages"("receiverId", "createdAt");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "blocks_blockedId_idx" ON "blocks"("blockedId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "blocks_blockerId_blockedId_key" ON "blocks"("blockerId", "blockedId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "point_transactions_userId_createdAt_idx" ON "point_transactions"("userId", "createdAt");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "level_thresholds_level_key" ON "level_thresholds"("level");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "banned_keywords_pattern_key" ON "banned_keywords"("pattern");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "banned_keywords_appliesTo_idx" ON "banned_keywords"("appliesTo");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "sanctions_userId_type_idx" ON "sanctions"("userId", "type");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "audit_logs_action_createdAt_idx" ON "audit_logs"("action", "createdAt");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "admin_notifications_type_createdAt_idx" ON "admin_notifications"("type", "createdAt");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "login_sessions_userId_lastSeenAt_idx" ON "login_sessions"("userId", "lastSeenAt");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "ip_blocks_ip_active_key" ON "ip_blocks"("ip", "active");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "nickname_changes_userId_changedAt_idx" ON "nickname_changes"("userId", "changedAt");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "password_reset_tokens_token_key" ON "password_reset_tokens"("token");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "password_reset_tokens_userId_expiresAt_idx" ON "password_reset_tokens"("userId", "expiresAt");
|
||||||
@@ -15,8 +15,6 @@ datasource db {
|
|||||||
url = env("DATABASE_URL")
|
url = env("DATABASE_URL")
|
||||||
}
|
}
|
||||||
|
|
||||||
// schema.prisma (MySQL)
|
|
||||||
|
|
||||||
// ==== Enums ====
|
// ==== Enums ====
|
||||||
enum BoardStatus {
|
enum BoardStatus {
|
||||||
active
|
active
|
||||||
@@ -24,11 +22,16 @@ enum BoardStatus {
|
|||||||
archived
|
archived
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum BoardType {
|
||||||
|
general
|
||||||
|
special
|
||||||
|
}
|
||||||
|
|
||||||
enum AccessLevel {
|
enum AccessLevel {
|
||||||
public // 비회원도 접근
|
public // 비회원도 접근
|
||||||
member // 로그인 필요
|
member // 로그인 필요
|
||||||
moderator // 운영진 이상
|
moderator // 운영진 이상
|
||||||
admin // 관리자만
|
admin // 관리자만
|
||||||
}
|
}
|
||||||
|
|
||||||
enum PostStatus {
|
enum PostStatus {
|
||||||
@@ -37,6 +40,11 @@ enum PostStatus {
|
|||||||
deleted
|
deleted
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum AttachmentType {
|
||||||
|
image
|
||||||
|
file
|
||||||
|
}
|
||||||
|
|
||||||
enum BoardModRole {
|
enum BoardModRole {
|
||||||
MODERATOR
|
MODERATOR
|
||||||
MANAGER
|
MANAGER
|
||||||
@@ -54,114 +62,489 @@ enum AuthLevel {
|
|||||||
ADMIN
|
ADMIN
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum ReactionType {
|
||||||
|
RECOMMEND
|
||||||
|
REPORT
|
||||||
|
}
|
||||||
|
|
||||||
|
enum SanctionType {
|
||||||
|
SUSPEND
|
||||||
|
BAN
|
||||||
|
DOWNGRADE
|
||||||
|
}
|
||||||
|
|
||||||
|
enum TargetType {
|
||||||
|
POST
|
||||||
|
COMMENT
|
||||||
|
USER
|
||||||
|
BOARD
|
||||||
|
SYSTEM
|
||||||
|
}
|
||||||
|
|
||||||
// ==== Models ====
|
// ==== Models ====
|
||||||
|
|
||||||
// 게시판 정의(관리자 생성/정렬)
|
// 게시판 정의(관리자 생성/정렬)
|
||||||
model Board {
|
model Board {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
name String
|
name String
|
||||||
slug String @unique // URL용
|
slug String @unique // URL용
|
||||||
description String?
|
description String?
|
||||||
sortOrder Int @default(0) // 원하는 순서
|
sortOrder Int @default(0)
|
||||||
status BoardStatus @default(active)
|
status BoardStatus @default(active)
|
||||||
isAdultOnly Boolean @default(false) // 성인 인증 필요 여부
|
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) // 읽기 권한
|
readLevel AccessLevel @default(public) // 읽기 권한
|
||||||
writeLevel AccessLevel @default(member) // 글쓰기 권한
|
writeLevel AccessLevel @default(member) // 글쓰기 권한
|
||||||
|
|
||||||
posts Post[]
|
posts Post[]
|
||||||
moderators BoardModerator[]
|
moderators BoardModerator[]
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
@@map("boards")
|
|
||||||
@@index([status, sortOrder])
|
@@index([status, sortOrder])
|
||||||
|
@@index([type, requiresApproval])
|
||||||
|
@@map("boards")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 게시판 운영진 매핑
|
// 게시판 운영진 매핑
|
||||||
model BoardModerator {
|
model BoardModerator {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
boardId String
|
boardId String
|
||||||
userId String
|
userId String
|
||||||
role BoardModRole @default(MODERATOR)
|
role BoardModRole @default(MODERATOR)
|
||||||
|
|
||||||
board Board @relation(fields: [boardId], references: [id], onDelete: Cascade)
|
board Board @relation(fields: [boardId], references: [id], onDelete: Cascade)
|
||||||
user User @relation(fields: [userId], references: [userId], onDelete: Cascade)
|
user User @relation(fields: [userId], references: [userId], onDelete: Cascade)
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
@@map("board_moderators")
|
|
||||||
@@unique([boardId, userId]) // 한 게시판당 1회 매핑
|
@@unique([boardId, userId]) // 한 게시판당 1회 매핑
|
||||||
@@index([userId])
|
@@index([userId])
|
||||||
|
@@map("board_moderators")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 게시글(게시판 내 컨텐츠)
|
// 게시글(게시판 내 컨텐츠)
|
||||||
model Post {
|
model Post {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
boardId String
|
boardId String
|
||||||
authorId String?
|
authorId String?
|
||||||
title String
|
title String
|
||||||
content String
|
content String
|
||||||
status PostStatus @default(published)
|
status PostStatus @default(published)
|
||||||
|
isAnonymous Boolean @default(false)
|
||||||
|
|
||||||
isPinned Boolean @default(false)
|
isPinned Boolean @default(false)
|
||||||
pinnedOrder Int? // 상단 고정 정렬
|
pinnedOrder Int?
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
lastActivityAt DateTime @default(now())
|
lastActivityAt DateTime @default(now())
|
||||||
|
|
||||||
board Board @relation(fields: [boardId], references: [id], onDelete: Cascade)
|
board Board @relation(fields: [boardId], references: [id], onDelete: Cascade)
|
||||||
author User? @relation(fields: [authorId], references: [userId], onDelete: SetNull)
|
author User? @relation(fields: [authorId], references: [userId], onDelete: SetNull)
|
||||||
comments Comment[]
|
comments Comment[]
|
||||||
|
attachments Attachment[]
|
||||||
|
postTags PostTag[]
|
||||||
|
reactions Reaction[]
|
||||||
|
reports Report[]
|
||||||
|
stat PostStat?
|
||||||
|
viewLogs PostViewLog[]
|
||||||
|
|
||||||
@@map("posts")
|
|
||||||
@@index([boardId, status, createdAt])
|
@@index([boardId, status, createdAt])
|
||||||
@@index([boardId, isPinned, pinnedOrder])
|
@@index([boardId, isPinned, pinnedOrder])
|
||||||
|
@@map("posts")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- 참고: 기존 User 모델 예시 ----
|
// 사용자
|
||||||
model User {
|
model User {
|
||||||
userId String @id @default(cuid())
|
userId String @id @default(cuid())
|
||||||
nickname String @unique
|
nickname String @unique
|
||||||
passwordHash String?
|
passwordHash String?
|
||||||
name String
|
name String
|
||||||
birth DateTime
|
birth DateTime
|
||||||
phone String @unique
|
phone String @unique
|
||||||
rank Int @default(0)
|
rank Int @default(0)
|
||||||
|
|
||||||
status UserStatus @default(active)
|
status UserStatus @default(active)
|
||||||
authLevel AuthLevel @default(USER)
|
authLevel AuthLevel @default(USER)
|
||||||
profileImage String?
|
profileImage String?
|
||||||
lastLoginAt DateTime?
|
lastLoginAt DateTime?
|
||||||
isAdultVerified Boolean @default(false)
|
isAdultVerified Boolean @default(false)
|
||||||
loginFailCount Int @default(0)
|
loginFailCount Int @default(0)
|
||||||
agreementTermsAt DateTime
|
agreementTermsAt DateTime
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
posts Post[]
|
posts Post[]
|
||||||
boardModerations BoardModerator[]
|
boardModerations BoardModerator[]
|
||||||
comments Comment[]
|
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([status])
|
||||||
@@index([createdAt])
|
@@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 {
|
model Comment {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
postId String
|
postId String
|
||||||
authorId String?
|
authorId String?
|
||||||
content String
|
content String
|
||||||
createdAt DateTime @default(now())
|
isAnonymous Boolean @default(false)
|
||||||
updatedAt DateTime @updatedAt
|
isSecret Boolean @default(false)
|
||||||
|
secretPasswordHash String?
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
|
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
|
||||||
author User? @relation(fields: [authorId], references: [userId], onDelete: SetNull)
|
author User? @relation(fields: [authorId], references: [userId], onDelete: SetNull)
|
||||||
|
reports Report[]
|
||||||
|
|
||||||
@@map("comments")
|
|
||||||
@@index([postId, createdAt])
|
@@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")
|
||||||
}
|
}
|
||||||
|
|||||||
178
prisma/seed.js
178
prisma/seed.js
@@ -2,53 +2,163 @@ const { PrismaClient } = require("@prisma/client");
|
|||||||
|
|
||||||
const prisma = new PrismaClient();
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
async function main() {
|
async function upsertRoles() {
|
||||||
const user = await prisma.user.upsert({
|
const roles = [
|
||||||
where: { nickname: "tester" },
|
{ name: "admin", description: "관리자" },
|
||||||
|
{ name: "editor", description: "운영진" },
|
||||||
|
{ name: "user", description: "일반 사용자" }
|
||||||
|
];
|
||||||
|
for (const r of roles) {
|
||||||
|
await prisma.role.upsert({
|
||||||
|
where: { name: r.name },
|
||||||
|
update: { description: r.description },
|
||||||
|
create: r,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function upsertAdmin() {
|
||||||
|
const admin = await prisma.user.upsert({
|
||||||
|
where: { nickname: "admin" },
|
||||||
update: {},
|
update: {},
|
||||||
create: {
|
create: {
|
||||||
nickname: "tester",
|
nickname: "admin",
|
||||||
name: "Tester",
|
name: "Administrator",
|
||||||
birth: new Date("1990-01-01"),
|
birth: new Date("1990-01-01"),
|
||||||
phone: "010-0000-0000",
|
phone: "010-0000-0001",
|
||||||
agreementTermsAt: new Date()
|
agreementTermsAt: new Date(),
|
||||||
}
|
authLevel: "ADMIN",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const board = await prisma.board.upsert({
|
const adminRole = await prisma.role.findUnique({ where: { name: "admin" } });
|
||||||
where: { slug: "general" },
|
if (adminRole) {
|
||||||
update: {},
|
await prisma.userRole.upsert({
|
||||||
create: {
|
where: {
|
||||||
name: "General",
|
userId_roleId: { userId: admin.userId, roleId: adminRole.roleId },
|
||||||
slug: "general",
|
},
|
||||||
description: "일반 게시판",
|
update: {},
|
||||||
sortOrder: 0
|
create: { userId: admin.userId, roleId: adminRole.roleId },
|
||||||
}
|
});
|
||||||
});
|
}
|
||||||
|
return admin;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function upsertBoards(admin) {
|
||||||
|
const boards = [
|
||||||
|
// 일반
|
||||||
|
{ name: "공지사항", slug: "notice", description: "공지", type: "general", sortOrder: 1 },
|
||||||
|
{ name: "가입인사", slug: "greetings", description: "가입인사", type: "general", sortOrder: 2 },
|
||||||
|
{ name: "버그건의", slug: "bug-report", description: "버그/건의", type: "general", sortOrder: 3 },
|
||||||
|
{ name: "이벤트", slug: "event", description: "이벤트", type: "general", sortOrder: 4 },
|
||||||
|
{ name: "자유게시판", slug: "free", description: "자유", type: "general", sortOrder: 5 },
|
||||||
|
{ name: "무엇이든", slug: "qna", description: "무엇이든 물어보세요", type: "general", sortOrder: 6 },
|
||||||
|
{ name: "마사지꿀팁", slug: "tips", description: "팁", type: "general", sortOrder: 7 },
|
||||||
|
{ name: "익명게시판", slug: "anonymous", description: "익명", type: "general", sortOrder: 8, allowAnonymousPost: true },
|
||||||
|
{ name: "관리사찾아요", slug: "find-therapist", description: "구인/구직", type: "general", sortOrder: 9 },
|
||||||
|
{ name: "청와대", slug: "blue-house", description: "레벨 제한", type: "general", sortOrder: 10, readLevel: "member" },
|
||||||
|
{ name: "방문후기", slug: "reviews", description: "운영자 승인 후 공개", type: "general", sortOrder: 11, requiresApproval: true },
|
||||||
|
// 특수
|
||||||
|
{ name: "출석부", slug: "attendance", description: "데일리 체크인", type: "special", sortOrder: 12 },
|
||||||
|
{ name: "주변 제휴업체", slug: "nearby-partners", description: "위치 기반", type: "special", sortOrder: 13 },
|
||||||
|
{ name: "회원랭킹", slug: "ranking", description: "랭킹", type: "special", sortOrder: 14 },
|
||||||
|
{ name: "무료쿠폰", slug: "free-coupons", description: "쿠폰", type: "special", sortOrder: 15 },
|
||||||
|
{ name: "월간집계", slug: "monthly-stats", description: "월간 통계", type: "special", sortOrder: 16 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const created = [];
|
||||||
|
for (const b of boards) {
|
||||||
|
const board = await prisma.board.upsert({
|
||||||
|
where: { slug: b.slug },
|
||||||
|
update: {
|
||||||
|
description: b.description,
|
||||||
|
sortOrder: b.sortOrder,
|
||||||
|
type: b.type,
|
||||||
|
requiresApproval: !!b.requiresApproval,
|
||||||
|
allowAnonymousPost: !!b.allowAnonymousPost,
|
||||||
|
readLevel: b.readLevel || undefined,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
name: b.name,
|
||||||
|
slug: b.slug,
|
||||||
|
description: b.description,
|
||||||
|
sortOrder: b.sortOrder,
|
||||||
|
type: b.type,
|
||||||
|
requiresApproval: !!b.requiresApproval,
|
||||||
|
allowAnonymousPost: !!b.allowAnonymousPost,
|
||||||
|
readLevel: b.readLevel || undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
created.push(board);
|
||||||
|
|
||||||
|
// 공지/운영 보드는 관리자 모더레이터 지정
|
||||||
|
if (["notice", "bug-report"].includes(board.slug)) {
|
||||||
|
await prisma.boardModerator.upsert({
|
||||||
|
where: {
|
||||||
|
boardId_userId: { boardId: board.id, userId: admin.userId },
|
||||||
|
},
|
||||||
|
update: {},
|
||||||
|
create: { boardId: board.id, userId: admin.userId, role: "MANAGER" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return created;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function seedPolicies() {
|
||||||
|
// 금칙어 예시
|
||||||
|
const banned = [
|
||||||
|
{ pattern: "광고", appliesTo: "POST", severity: 1 },
|
||||||
|
{ pattern: "욕설", appliesTo: "COMMENT", severity: 2 },
|
||||||
|
{ pattern: "스팸", appliesTo: "POST", severity: 2 },
|
||||||
|
];
|
||||||
|
for (const k of banned) {
|
||||||
|
await prisma.bannedKeyword.upsert({
|
||||||
|
where: { pattern: k.pattern },
|
||||||
|
update: { appliesTo: k.appliesTo, severity: k.severity, active: true },
|
||||||
|
create: k,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 레벨 임계 예시
|
||||||
|
const levels = [
|
||||||
|
{ level: 1, minPoints: 0 },
|
||||||
|
{ level: 2, minPoints: 100 },
|
||||||
|
{ level: 3, minPoints: 300 },
|
||||||
|
];
|
||||||
|
for (const l of levels) {
|
||||||
|
await prisma.levelThreshold.upsert({
|
||||||
|
where: { level: l.level },
|
||||||
|
update: { minPoints: l.minPoints },
|
||||||
|
create: l,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
await upsertRoles();
|
||||||
|
const admin = await upsertAdmin();
|
||||||
|
const boards = await upsertBoards(admin);
|
||||||
|
|
||||||
|
// 샘플 글 하나
|
||||||
|
const free = boards.find((b) => b.slug === "free") || boards[0];
|
||||||
const post = await prisma.post.create({
|
const post = await prisma.post.create({
|
||||||
data: {
|
data: {
|
||||||
boardId: board.id,
|
boardId: free.id,
|
||||||
authorId: user.userId,
|
authorId: admin.userId,
|
||||||
title: "첫 글",
|
title: "첫 글",
|
||||||
content: "Hello SQLite + Prisma",
|
content: "메시지 앱 초기 설정 완료",
|
||||||
isPinned: false,
|
status: "published",
|
||||||
status: "published"
|
},
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await prisma.boardModerator.upsert({
|
|
||||||
where: { boardId_userId: { boardId: board.id, userId: user.userId } },
|
|
||||||
update: {},
|
|
||||||
create: { boardId: board.id, userId: user.userId, role: "MODERATOR" }
|
|
||||||
});
|
|
||||||
|
|
||||||
await prisma.comment.createMany({
|
await prisma.comment.createMany({
|
||||||
data: [
|
data: [
|
||||||
{ postId: post.id, authorId: user.userId, content: "첫 댓글" },
|
{ postId: post.id, authorId: admin.userId, content: "환영합니다!" },
|
||||||
{ postId: post.id, authorId: user.userId, content: "두번째 댓글" }
|
{ postId: post.id, authorId: admin.userId, content: "댓글 테스트" },
|
||||||
]
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await seedPolicies();
|
||||||
}
|
}
|
||||||
|
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -285,11 +285,11 @@ Hero/공지 배너 영역
|
|||||||
-- 해야할 일(작업 단위)
|
-- 해야할 일(작업 단위)
|
||||||
|
|
||||||
[백엔드/API]
|
[백엔드/API]
|
||||||
1. Prisma 스키마 최종 확정(모델/관계/index/unique)
|
1. Prisma 스키마 최종 확정(모델/관계/index/unique) o
|
||||||
2. 마이그레이션 생성 및 적용(prisma migrate dev)
|
2. 마이그레이션 생성 및 적용(prisma migrate dev) o
|
||||||
3. Prisma Client 재생성 및 dev DB 초기화
|
3. Prisma Client 재생성 및 dev DB 초기화 o
|
||||||
4. 시드 스크립트 분리(역할/기본 관리자/샘플 게시판)
|
4. 시드 스크립트 분리(역할/기본 관리자/샘플 게시판) o
|
||||||
5. 서비스/리포지토리 계층 뼈대 생성(트랜잭션 유틸 포함)
|
5. 서비스/리포지토리 계층 뼈대 생성(트랜잭션 유틸 포함) o
|
||||||
|
|
||||||
[권한/역할(RBAC)]
|
[권한/역할(RBAC)]
|
||||||
1. 역할·권한 시드(admin/editor/user) 추가
|
1. 역할·권한 시드(admin/editor/user) 추가
|
||||||
|
|||||||
1
public/erd.svg
Normal file
1
public/erd.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 601 KiB |
21
src/app/api/boards/route.ts
Normal file
21
src/app/api/boards/route.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
const boards = await prisma.board.findMany({
|
||||||
|
orderBy: [{ sortOrder: "asc" }, { createdAt: "asc" }],
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
slug: true,
|
||||||
|
description: true,
|
||||||
|
type: true,
|
||||||
|
requiresApproval: true,
|
||||||
|
allowAnonymousPost: true,
|
||||||
|
isAdultOnly: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return NextResponse.json({ boards });
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
32
src/app/api/comments/route.ts
Normal file
32
src/app/api/comments/route.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const createCommentSchema = z.object({
|
||||||
|
postId: z.string().min(1),
|
||||||
|
authorId: z.string().optional(),
|
||||||
|
content: z.string().min(1),
|
||||||
|
isAnonymous: z.boolean().optional(),
|
||||||
|
isSecret: z.boolean().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function POST(req: Request) {
|
||||||
|
const body = await req.json();
|
||||||
|
const parsed = createCommentSchema.safeParse(body);
|
||||||
|
if (!parsed.success) {
|
||||||
|
return NextResponse.json({ error: parsed.error.flatten() }, { status: 400 });
|
||||||
|
}
|
||||||
|
const { postId, authorId, content, isAnonymous, isSecret } = parsed.data;
|
||||||
|
const comment = await prisma.comment.create({
|
||||||
|
data: {
|
||||||
|
postId,
|
||||||
|
authorId: authorId ?? null,
|
||||||
|
content,
|
||||||
|
isAnonymous: !!isAnonymous,
|
||||||
|
isSecret: !!isSecret,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return NextResponse.json({ comment }, { status: 201 });
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
26
src/app/api/posts/route.ts
Normal file
26
src/app/api/posts/route.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const createPostSchema = z.object({
|
||||||
|
boardId: z.string().min(1),
|
||||||
|
authorId: z.string().optional(),
|
||||||
|
title: z.string().min(1),
|
||||||
|
content: z.string().min(1),
|
||||||
|
isAnonymous: z.boolean().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function POST(req: Request) {
|
||||||
|
const body = await req.json();
|
||||||
|
const parsed = createPostSchema.safeParse(body);
|
||||||
|
if (!parsed.success) {
|
||||||
|
return NextResponse.json({ error: parsed.error.flatten() }, { status: 400 });
|
||||||
|
}
|
||||||
|
const { boardId, authorId, title, content, isAnonymous } = parsed.data;
|
||||||
|
const post = await prisma.post.create({
|
||||||
|
data: { boardId, authorId: authorId ?? null, title, content, isAnonymous: !!isAnonymous },
|
||||||
|
});
|
||||||
|
return NextResponse.json({ post }, { status: 201 });
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user