ㄱㄱ
This commit is contained in:
18
prisma/migrations/20251101152100_/migration.sql
Normal file
18
prisma/migrations/20251101152100_/migration.sql
Normal file
@@ -0,0 +1,18 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "partner_shops" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"region" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"address" TEXT NOT NULL,
|
||||
"imageUrl" TEXT NOT NULL,
|
||||
"sortOrder" INTEGER NOT NULL DEFAULT 0,
|
||||
"active" BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "partner_shops_active_sortOrder_idx" ON "partner_shops"("active", "sortOrder");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "partner_shops_name_region_key" ON "partner_shops"("name", "region");
|
||||
2
prisma/migrations/20251101161632_/migration.sql
Normal file
2
prisma/migrations/20251101161632_/migration.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "partners" ADD COLUMN "imageUrl" TEXT;
|
||||
23
prisma/migrations/20251101162445_/migration.sql
Normal file
23
prisma/migrations/20251101162445_/migration.sql
Normal file
@@ -0,0 +1,23 @@
|
||||
-- RedefineTables
|
||||
PRAGMA defer_foreign_keys=ON;
|
||||
PRAGMA foreign_keys=OFF;
|
||||
CREATE TABLE "new_partners" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"name" TEXT NOT NULL,
|
||||
"category" TEXT NOT NULL,
|
||||
"latitude" REAL NOT NULL,
|
||||
"longitude" REAL NOT NULL,
|
||||
"address" TEXT,
|
||||
"imageUrl" TEXT,
|
||||
"sortOrder" INTEGER NOT NULL DEFAULT 0,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL
|
||||
);
|
||||
INSERT INTO "new_partners" ("address", "category", "createdAt", "id", "imageUrl", "latitude", "longitude", "name", "updatedAt") SELECT "address", "category", "createdAt", "id", "imageUrl", "latitude", "longitude", "name", "updatedAt" FROM "partners";
|
||||
DROP TABLE "partners";
|
||||
ALTER TABLE "new_partners" RENAME TO "partners";
|
||||
CREATE UNIQUE INDEX "partners_name_key" ON "partners"("name");
|
||||
CREATE INDEX "partners_category_idx" ON "partners"("category");
|
||||
CREATE INDEX "partners_sortOrder_idx" ON "partners"("sortOrder");
|
||||
PRAGMA foreign_keys=ON;
|
||||
PRAGMA defer_foreign_keys=OFF;
|
||||
@@ -675,10 +675,13 @@ model Partner {
|
||||
latitude Float
|
||||
longitude Float
|
||||
address String?
|
||||
imageUrl String?
|
||||
sortOrder Int @default(0)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([category])
|
||||
@@index([sortOrder])
|
||||
@@map("partners")
|
||||
}
|
||||
|
||||
@@ -742,3 +745,20 @@ model Setting {
|
||||
|
||||
@@map("settings")
|
||||
}
|
||||
|
||||
// 메인 노출용 제휴 샵 가로 스크롤 데이터
|
||||
model PartnerShop {
|
||||
id String @id @default(cuid())
|
||||
region String
|
||||
name String
|
||||
address String
|
||||
imageUrl String
|
||||
sortOrder Int @default(0)
|
||||
active Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@unique([name, region])
|
||||
@@index([active, sortOrder])
|
||||
@@map("partner_shops")
|
||||
}
|
||||
|
||||
180
prisma/seed.js
180
prisma/seed.js
@@ -2,16 +2,87 @@ const { PrismaClient } = require("@prisma/client");
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
function randomInt(min, max) {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
|
||||
function randomDate(startYear, endYear) {
|
||||
const start = new Date(`${startYear}-01-01`).getTime();
|
||||
const end = new Date(`${endYear}-12-31`).getTime();
|
||||
return new Date(randomInt(start, end));
|
||||
}
|
||||
|
||||
function generateRandomKoreanName() {
|
||||
const lastNames = ["김", "이", "박", "최", "정", "강", "조", "윤", "장", "임"];
|
||||
const firstParts = ["민", "서", "도", "현", "지", "아", "윤", "준", "하", "유", "채", "은", "수", "태", "나"];
|
||||
const secondParts = ["우", "영", "민", "서", "진", "현", "빈", "율", "솔", "연", "환", "호", "린", "훈", "경"];
|
||||
const last = lastNames[randomInt(0, lastNames.length - 1)];
|
||||
const first = firstParts[randomInt(0, firstParts.length - 1)] + secondParts[randomInt(0, secondParts.length - 1)];
|
||||
return last + first;
|
||||
}
|
||||
|
||||
function generateUniquePhone(i) {
|
||||
const mid = String(2000 + (i % 8000)).padStart(4, "0");
|
||||
const last = String(3000 + i).padStart(4, "0");
|
||||
return `010-${mid}-${last}`;
|
||||
}
|
||||
|
||||
function generateNickname(i) {
|
||||
const suffix = Math.random().toString(36).slice(2, 4);
|
||||
return `user${String(i + 1).padStart(3, "0")}${suffix}`;
|
||||
}
|
||||
|
||||
async function createRandomUsers(count = 100) {
|
||||
const roleUser = await prisma.role.findUnique({ where: { name: "user" } });
|
||||
// 사용되지 않은 전화번호를 찾는 보조 함수
|
||||
async function findAvailablePhone(startIndex) {
|
||||
let offset = 0;
|
||||
while (true) {
|
||||
const candidate = generateUniquePhone(startIndex + offset);
|
||||
const exists = await prisma.user.findUnique({ where: { phone: candidate } });
|
||||
if (!exists) return candidate;
|
||||
offset += 1;
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
// 닉네임을 결정론적으로 생성해 재실행 시 중복 생성 방지
|
||||
const nickname = `user${String(i + 1).padStart(3, "0")}`;
|
||||
const existing = await prisma.user.findUnique({ where: { nickname } });
|
||||
let user = existing;
|
||||
if (!existing) {
|
||||
const name = generateRandomKoreanName();
|
||||
const birth = randomDate(1975, 2005);
|
||||
const phone = await findAvailablePhone(i + 2); // admin이 0001 사용하므로 겹치지 않도록 오프셋
|
||||
user = await prisma.user.create({
|
||||
data: {
|
||||
nickname,
|
||||
name,
|
||||
birth,
|
||||
phone,
|
||||
agreementTermsAt: new Date(),
|
||||
authLevel: "USER",
|
||||
isAdultVerified: Math.random() < 0.6,
|
||||
lastLoginAt: Math.random() < 0.8 ? new Date() : null,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (roleUser && user) {
|
||||
await prisma.userRole.upsert({
|
||||
where: { userId_roleId: { userId: user.userId, roleId: roleUser.roleId } },
|
||||
update: {},
|
||||
create: { userId: user.userId, roleId: roleUser.roleId },
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function upsertCategories() {
|
||||
// 카테고리 트리 (projectmemo 기준 상위 그룹)
|
||||
const categories = [
|
||||
{ name: "암실소문", slug: "main", sortOrder: 1, status: "active" },
|
||||
{ name: "메인", slug: "main", sortOrder: 1, status: "active" },
|
||||
{ name: "명예의 전당", slug: "hall-of-fame", sortOrder: 2, status: "active" },
|
||||
{ name: "주변 제휴업체", slug: "nearby-partners", sortOrder: 3, status: "active" },
|
||||
{ name: "제휴업소 정보", slug: "partner-info", sortOrder: 4, status: "active" },
|
||||
{ name: "방문후기", slug: "reviews", sortOrder: 5, status: "active" },
|
||||
{ name: "소통방", slug: "community", sortOrder: 6, status: "active" },
|
||||
{ name: "광고/제휴", slug: "ads-affiliates", sortOrder: 7, status: "active" },
|
||||
{ name: "소통방", slug: "community", sortOrder: 3, status: "active" },
|
||||
];
|
||||
const map = {};
|
||||
for (const c of categories) {
|
||||
@@ -119,20 +190,13 @@ async function upsertBoards(admin, categoryMap) {
|
||||
{ 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, allowSecretComment: 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, requiredTags: { anyOf: ["업체명", "지역"] } },
|
||||
// 특수
|
||||
{ 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 },
|
||||
// 제휴업소 일반
|
||||
{ name: "제휴업소", slug: "partners-photos", description: "사진 전용 게시판", type: "general", sortOrder: 17, requiredFields: { imageOnly: true, minImages: 1, maxImages: 10 } },
|
||||
// 광고/제휴
|
||||
{ name: "제휴문의", slug: "partner-contact", description: "제휴문의", type: "general", sortOrder: 18, requiredFields: { imageOnly: true, minImages: 1, maxImages: 10 } },
|
||||
{ name: "제휴업소 요청", slug: "partner-req", description: "제휴업소 요청", type: "general", sortOrder: 19, requiredFields: { imageOnly: true, minImages: 1, maxImages: 10 } },
|
||||
// 광고/제휴 계열 게시판은 제거(메인/명예의전당/소통방 외)
|
||||
];
|
||||
|
||||
const created = [];
|
||||
@@ -149,22 +213,12 @@ async function upsertBoards(admin, categoryMap) {
|
||||
ranking: "hall-of-fame",
|
||||
"free-coupons": "hall-of-fame",
|
||||
"monthly-stats": "hall-of-fame",
|
||||
// 주변 제휴업체
|
||||
"nearby-partners": "nearby-partners",
|
||||
// 제휴업소 정보
|
||||
"partners-photos": "partner-info",
|
||||
// 방문후기
|
||||
reviews: "reviews",
|
||||
// 소통방(기본값 community로 처리)
|
||||
free: "community",
|
||||
qna: "community",
|
||||
tips: "community",
|
||||
anonymous: "community",
|
||||
"find-therapist": "community",
|
||||
"blue-house": "community",
|
||||
// 광고/제휴
|
||||
"partner-contact": "ads-affiliates",
|
||||
"partner-req": "ads-affiliates",
|
||||
};
|
||||
const categorySlug = mapBySlug[b.slug] || "community";
|
||||
const category = categoryMap[categorySlug];
|
||||
@@ -229,6 +283,35 @@ async function upsertViewTypes() {
|
||||
}
|
||||
}
|
||||
|
||||
async function removeNonPrimaryBoards() {
|
||||
// 메인/명예의전당/소통방에 해당하지 않는 게시판(광고/제휴 계열) 정리
|
||||
await prisma.board.deleteMany({
|
||||
where: { slug: { in: ["partners-photos", "partner-contact", "partner-req"] } },
|
||||
});
|
||||
}
|
||||
|
||||
async function createPostsForAllBoards(boards, countPerBoard = 100, admin) {
|
||||
const users = await prisma.user.findMany({ select: { userId: true } });
|
||||
const userIds = users.map((u) => u.userId);
|
||||
for (const board of boards) {
|
||||
const data = [];
|
||||
for (let i = 0; i < countPerBoard; i++) {
|
||||
const authorId = ["notice", "bug-report"].includes(board.slug)
|
||||
? admin.userId
|
||||
: userIds[randomInt(0, userIds.length - 1)];
|
||||
data.push({
|
||||
boardId: board.id,
|
||||
authorId,
|
||||
title: `${board.name} 샘플 글 ${i + 1}`,
|
||||
content: `이 게시판(${board.slug})의 자동 시드 게시물 #${i + 1} 입니다.\n\n테스트용 내용입니다.`,
|
||||
status: "published",
|
||||
isAnonymous: board.allowAnonymousPost ? Math.random() < 0.3 : false,
|
||||
});
|
||||
}
|
||||
await prisma.post.createMany({ data });
|
||||
}
|
||||
}
|
||||
|
||||
async function seedPolicies() {
|
||||
// 금칙어 예시
|
||||
const banned = [
|
||||
@@ -259,37 +342,46 @@ async function seedPolicies() {
|
||||
}
|
||||
}
|
||||
|
||||
async function seedPartnerShops() {
|
||||
const items = [
|
||||
{ region: "경기도", name: "test1", address: "수원시 팔달구 매산로 45", imageUrl: "/sample.jpg", sortOrder: 1 },
|
||||
{ region: "강원도", name: "test2", address: "춘천시 중앙로 112", imageUrl: "/sample.jpg", sortOrder: 2 },
|
||||
];
|
||||
for (const it of items) {
|
||||
await prisma.partnerShop.upsert({
|
||||
where: { name_region: { name: it.name, region: it.region } },
|
||||
update: { address: it.address, imageUrl: it.imageUrl, sortOrder: it.sortOrder, active: true },
|
||||
create: it,
|
||||
});
|
||||
}
|
||||
// 표시 토글 기본값 보장
|
||||
const SETTINGS_KEY = "mainpage_settings";
|
||||
const setting = await prisma.setting.findUnique({ where: { key: SETTINGS_KEY } });
|
||||
const current = setting ? JSON.parse(setting.value) : {};
|
||||
const next = { showPartnerShops: true, ...current };
|
||||
await prisma.setting.upsert({
|
||||
where: { key: SETTINGS_KEY },
|
||||
update: { value: JSON.stringify(next) },
|
||||
create: { key: SETTINGS_KEY, value: JSON.stringify(next) },
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
await upsertRoles();
|
||||
const admin = await upsertAdmin();
|
||||
const categoryMap = await upsertCategories();
|
||||
await upsertViewTypes();
|
||||
await createRandomUsers(100);
|
||||
await removeNonPrimaryBoards();
|
||||
const boards = await upsertBoards(admin, categoryMap);
|
||||
|
||||
// 샘플 글 하나
|
||||
const free = boards.find((b) => b.slug === "free") || boards[0];
|
||||
const post = await prisma.post.create({
|
||||
data: {
|
||||
boardId: free.id,
|
||||
authorId: admin.userId,
|
||||
title: "첫 글",
|
||||
content: "메시지 앱 초기 설정 완료",
|
||||
status: "published",
|
||||
},
|
||||
});
|
||||
await prisma.comment.createMany({
|
||||
data: [
|
||||
{ postId: post.id, authorId: admin.userId, content: "환영합니다!" },
|
||||
{ postId: post.id, authorId: admin.userId, content: "댓글 테스트" },
|
||||
],
|
||||
});
|
||||
await createPostsForAllBoards(boards, 100, admin);
|
||||
await seedPartnerShops();
|
||||
|
||||
await seedPolicies();
|
||||
|
||||
// 제휴업체 예시 데이터
|
||||
const partners = [
|
||||
{ name: "힐링마사지", category: "spa", latitude: 37.5665, longitude: 126.9780, address: "서울 중구" },
|
||||
{ name: "웰빙테라피", category: "spa", latitude: 37.5700, longitude: 126.9769, address: "서울 종로구" },
|
||||
{ name: "test", category: "spa", latitude: 37.5700, longitude: 126.9769, address: "서울 종로구" },
|
||||
{ name: "굿짐", category: "gym", latitude: 37.5600, longitude: 126.9820, address: "서울 중구" },
|
||||
];
|
||||
for (const p of partners) {
|
||||
|
||||
Reference in New Issue
Block a user