This commit is contained in:
koreacomp5
2025-11-02 02:46:20 +09:00
parent 27cf98eef2
commit 0bf18968ad
50 changed files with 892 additions and 121 deletions

View 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");

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "partners" ADD COLUMN "imageUrl" TEXT;

View 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;

View File

@@ -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")
}

View File

@@ -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) {