2025-10-08 23:04:12 +09:00
|
|
|
const { PrismaClient } = require("@prisma/client");
|
2025-11-02 12:07:11 +09:00
|
|
|
const { createHash } = require("crypto");
|
2025-10-08 23:04:12 +09:00
|
|
|
|
|
|
|
|
const prisma = new PrismaClient();
|
|
|
|
|
|
2025-11-02 12:07:11 +09:00
|
|
|
function hashPassword(plain) {
|
|
|
|
|
return createHash("sha256").update(plain, "utf8").digest("hex");
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-02 02:46:20 +09:00
|
|
|
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}`;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-05 23:47:44 +09:00
|
|
|
// 랜덤 제목/문장/이미지 도우미
|
|
|
|
|
const TITLE_FRAGMENTS = [
|
2025-11-05 23:50:01 +09:00
|
|
|
// 아주 짧은 키워드
|
|
|
|
|
"공지", "업뎃", "버그", "요청", "후기", "정보", "TIP", "사진", "잡담", "나눔",
|
|
|
|
|
"질문", "헬프", "리뷰", "이슈", "주의", "긴급", "정리", "모음", "요약", "스샷",
|
|
|
|
|
// 짧은 구문
|
2025-11-05 23:47:44 +09:00
|
|
|
"오늘의 이슈", "핫 토픽", "소소한 일상", "정보 공유", "꿀팁 모음",
|
2025-11-05 23:50:01 +09:00
|
|
|
"개발 노트", "버그 리포트", "아이디어 제안", "함께 보아요",
|
2025-11-05 23:47:44 +09:00
|
|
|
];
|
|
|
|
|
const SENTENCES = [
|
|
|
|
|
"안녕하세요, 간단히 공유 드립니다.",
|
|
|
|
|
"도움이 되셨다면 댓글로 알려주세요.",
|
|
|
|
|
"의견이나 질문은 언제든 환영입니다.",
|
|
|
|
|
"테스트로 작성된 시드 데이터입니다.",
|
|
|
|
|
"참고용 스크린샷을 함께 첨부합니다.",
|
|
|
|
|
"관련 경험 있으시면 팁 부탁드려요.",
|
|
|
|
|
"문서화가 필요해 간단히 정리했습니다.",
|
|
|
|
|
"링크와 자료를 함께 첨부합니다.",
|
|
|
|
|
"개선 제안은 자유롭게 남겨주세요.",
|
|
|
|
|
"읽어주셔서 감사합니다.",
|
|
|
|
|
];
|
2025-11-05 23:50:01 +09:00
|
|
|
const TITLE_SUBS = [
|
|
|
|
|
"지금", "방금", "오늘", "금일", "v2", "2025", "베타", "테스트",
|
|
|
|
|
"임시", "간단히", "빠르게", "짧게", "새로", "업데이트", "정리", "공유",
|
|
|
|
|
];
|
|
|
|
|
const TITLE_EMOJIS = ["🔥", "📌", "✅", "❗", "💡", "🆕", "🔧", "📝", "📷"];
|
|
|
|
|
|
|
|
|
|
function clampTitle(s, max = 60) {
|
|
|
|
|
return s.length <= max ? s : s.slice(0, max).trim();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function pick(arr) { return arr[randomInt(0, arr.length - 1)]; }
|
|
|
|
|
function coin(p = 0.5) { return Math.random() < p; }
|
|
|
|
|
|
2025-11-05 23:47:44 +09:00
|
|
|
function randomTitle(boardName, index) {
|
2025-11-05 23:50:01 +09:00
|
|
|
// 다양한 템플릿으로 제목 생성 (짧은 것도, 긴 것도 포함)
|
|
|
|
|
const a = pick(TITLE_FRAGMENTS);
|
|
|
|
|
const b = pick(TITLE_FRAGMENTS);
|
|
|
|
|
const sub = pick(TITLE_SUBS);
|
|
|
|
|
const emoji = pick(TITLE_EMOJIS);
|
|
|
|
|
const num = (index % 99) + 1;
|
|
|
|
|
|
|
|
|
|
const templates = [
|
|
|
|
|
() => `${a}`,
|
|
|
|
|
() => `${a} ${emoji}`,
|
|
|
|
|
() => `${a} #${num}`,
|
|
|
|
|
() => `${a} ${sub}`,
|
|
|
|
|
() => `${a} · ${b}`,
|
|
|
|
|
() => `[${a}] ${b}`,
|
|
|
|
|
() => `${a}: ${b}`,
|
|
|
|
|
() => `${a} ${b} ${emoji}`,
|
|
|
|
|
// 가끔만 보드명 포함
|
|
|
|
|
() => `${boardName} ${a}`,
|
|
|
|
|
() => `${boardName} ${a} · ${b}`,
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// 짧은 제목 확률을 높이기 위해 템플릿 선택 가중치 없이 랜덤
|
|
|
|
|
const title = pick(templates)();
|
|
|
|
|
return clampTitle(title, 60);
|
2025-11-05 23:47:44 +09:00
|
|
|
}
|
|
|
|
|
function randomSentence() {
|
|
|
|
|
return SENTENCES[randomInt(0, SENTENCES.length - 1)];
|
|
|
|
|
}
|
|
|
|
|
function randomImageUrl(seedKey, w = 800, h = 450) {
|
|
|
|
|
// 외부 랜덤 이미지. 네트워크가 제한되면 /sample.jpg로 대체 가능
|
|
|
|
|
const seed = encodeURIComponent(String(seedKey));
|
|
|
|
|
return `https://picsum.photos/seed/${seed}/${w}/${h}`;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-02 02:46:20 +09:00
|
|
|
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,
|
2025-11-09 19:53:42 +09:00
|
|
|
passwordHash: hashPassword("12341234"),
|
2025-11-02 02:46:20 +09:00
|
|
|
agreementTermsAt: new Date(),
|
|
|
|
|
authLevel: "USER",
|
|
|
|
|
isAdultVerified: Math.random() < 0.6,
|
|
|
|
|
lastLoginAt: Math.random() < 0.8 ? new Date() : null,
|
|
|
|
|
},
|
|
|
|
|
});
|
2025-11-02 12:07:11 +09:00
|
|
|
} else {
|
|
|
|
|
// 기존 사용자도 패스워드를 1234로 업데이트
|
|
|
|
|
await prisma.user.update({
|
|
|
|
|
where: { userId: user.userId },
|
2025-11-09 19:53:42 +09:00
|
|
|
data: { passwordHash: hashPassword("12341234") },
|
2025-11-02 12:07:11 +09:00
|
|
|
});
|
2025-11-02 02:46:20 +09:00
|
|
|
}
|
|
|
|
|
if (roleUser && user) {
|
|
|
|
|
await prisma.userRole.upsert({
|
|
|
|
|
where: { userId_roleId: { userId: user.userId, roleId: roleUser.roleId } },
|
|
|
|
|
update: {},
|
|
|
|
|
create: { userId: user.userId, roleId: roleUser.roleId },
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-13 06:29:33 +09:00
|
|
|
async function upsertCategories() {
|
2025-10-13 07:23:08 +09:00
|
|
|
// 카테고리 트리 (projectmemo 기준 상위 그룹)
|
2025-10-13 06:29:33 +09:00
|
|
|
const categories = [
|
2025-11-02 02:46:20 +09:00
|
|
|
{ name: "메인", slug: "main", sortOrder: 1, status: "active" },
|
2025-10-13 07:23:08 +09:00
|
|
|
{ name: "명예의 전당", slug: "hall-of-fame", sortOrder: 2, status: "active" },
|
2025-11-02 02:46:20 +09:00
|
|
|
{ name: "소통방", slug: "community", sortOrder: 3, status: "active" },
|
2025-11-02 12:07:11 +09:00
|
|
|
{ name: "TEST", slug: "test", sortOrder: 4, status: "active" },
|
2025-10-13 06:29:33 +09:00
|
|
|
];
|
|
|
|
|
const map = {};
|
|
|
|
|
for (const c of categories) {
|
|
|
|
|
const created = await prisma.boardCategory.upsert({
|
|
|
|
|
where: { slug: c.slug },
|
|
|
|
|
update: { name: c.name, sortOrder: c.sortOrder, status: c.status },
|
|
|
|
|
create: c,
|
|
|
|
|
});
|
|
|
|
|
map[c.slug] = created;
|
|
|
|
|
}
|
|
|
|
|
return map; // { slug: category }
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-09 11:23:27 +09:00
|
|
|
async function upsertRoles() {
|
|
|
|
|
const roles = [
|
|
|
|
|
{ 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,
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-10-09 14:37:42 +09:00
|
|
|
// 기본 권한 매핑
|
|
|
|
|
const roleMap = {
|
|
|
|
|
admin: [
|
|
|
|
|
["ADMIN", "ADMINISTER"],
|
|
|
|
|
["BOARD", "MODERATE"],
|
|
|
|
|
["POST", "CREATE"],
|
|
|
|
|
["POST", "UPDATE"],
|
|
|
|
|
["POST", "DELETE"],
|
|
|
|
|
["COMMENT", "DELETE"],
|
|
|
|
|
["USER", "UPDATE"],
|
|
|
|
|
],
|
|
|
|
|
editor: [
|
|
|
|
|
["BOARD", "MODERATE"],
|
|
|
|
|
["POST", "UPDATE"],
|
|
|
|
|
["POST", "DELETE"],
|
|
|
|
|
["COMMENT", "DELETE"],
|
|
|
|
|
],
|
|
|
|
|
user: [
|
|
|
|
|
["POST", "CREATE"],
|
|
|
|
|
["COMMENT", "CREATE"],
|
|
|
|
|
["POST", "READ"],
|
|
|
|
|
["COMMENT", "READ"],
|
|
|
|
|
],
|
|
|
|
|
};
|
|
|
|
|
for (const [roleName, perms] of Object.entries(roleMap)) {
|
|
|
|
|
const role = await prisma.role.findUnique({ where: { name: roleName } });
|
|
|
|
|
if (!role) continue;
|
|
|
|
|
for (const [resource, action] of perms) {
|
|
|
|
|
await prisma.rolePermission.upsert({
|
|
|
|
|
where: {
|
|
|
|
|
roleId_resource_action: {
|
|
|
|
|
roleId: role.roleId,
|
|
|
|
|
resource,
|
|
|
|
|
action,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
update: { allowed: true },
|
|
|
|
|
create: { roleId: role.roleId, resource, action, allowed: true },
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-10-09 11:23:27 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function upsertAdmin() {
|
|
|
|
|
const admin = await prisma.user.upsert({
|
|
|
|
|
where: { nickname: "admin" },
|
2025-11-02 13:32:19 +09:00
|
|
|
update: {
|
2025-11-09 19:53:42 +09:00
|
|
|
passwordHash: hashPassword("12341234"),
|
2025-11-02 13:32:19 +09:00
|
|
|
grade: 7,
|
|
|
|
|
points: 1650000,
|
|
|
|
|
level: 200,
|
|
|
|
|
},
|
2025-10-08 23:04:12 +09:00
|
|
|
create: {
|
2025-10-09 11:23:27 +09:00
|
|
|
nickname: "admin",
|
|
|
|
|
name: "Administrator",
|
2025-10-08 23:44:21 +09:00
|
|
|
birth: new Date("1990-01-01"),
|
2025-10-09 11:23:27 +09:00
|
|
|
phone: "010-0000-0001",
|
2025-11-09 19:53:42 +09:00
|
|
|
passwordHash: hashPassword("12341234"),
|
2025-10-09 11:23:27 +09:00
|
|
|
agreementTermsAt: new Date(),
|
|
|
|
|
authLevel: "ADMIN",
|
2025-11-02 13:32:19 +09:00
|
|
|
grade: 7,
|
|
|
|
|
points: 1650000,
|
|
|
|
|
level: 200,
|
2025-10-09 11:23:27 +09:00
|
|
|
},
|
2025-10-08 23:04:12 +09:00
|
|
|
});
|
2025-10-08 23:44:21 +09:00
|
|
|
|
2025-10-09 11:23:27 +09:00
|
|
|
const adminRole = await prisma.role.findUnique({ where: { name: "admin" } });
|
|
|
|
|
if (adminRole) {
|
|
|
|
|
await prisma.userRole.upsert({
|
|
|
|
|
where: {
|
|
|
|
|
userId_roleId: { userId: admin.userId, roleId: adminRole.roleId },
|
|
|
|
|
},
|
|
|
|
|
update: {},
|
|
|
|
|
create: { userId: admin.userId, roleId: adminRole.roleId },
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
return admin;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-13 06:29:33 +09:00
|
|
|
async function upsertBoards(admin, categoryMap) {
|
2025-10-09 11:23:27 +09:00
|
|
|
const boards = [
|
|
|
|
|
// 일반
|
2025-11-02 03:12:42 +09:00
|
|
|
{ name: "공지사항", slug: "notice", description: "공지", sortOrder: 1, writeLevel: "moderator" },
|
|
|
|
|
{ name: "가입인사", slug: "greetings", description: "가입인사", sortOrder: 2 },
|
|
|
|
|
{ name: "버그건의", slug: "bug-report", description: "버그/건의", sortOrder: 3 },
|
|
|
|
|
{ name: "이벤트", slug: "event", description: "이벤트", sortOrder: 4, requiredTags: { required: ["이벤트"] } },
|
|
|
|
|
{ name: "자유게시판", slug: "free", description: "자유", sortOrder: 5 },
|
|
|
|
|
{ name: "무엇이든", slug: "qna", description: "무엇이든 물어보세요", sortOrder: 6 },
|
|
|
|
|
{ name: "익명게시판", slug: "anonymous", description: "익명", sortOrder: 8, allowAnonymousPost: true, allowSecretComment: true },
|
|
|
|
|
{ name: "청와대", slug: "blue-house", description: "레벨 제한", sortOrder: 10, readLevel: "member" },
|
2025-10-09 11:23:27 +09:00
|
|
|
// 특수
|
2025-11-02 03:12:42 +09:00
|
|
|
{ name: "출석부", slug: "attendance", description: "데일리 체크인", sortOrder: 12 },
|
|
|
|
|
{ name: "회원랭킹", slug: "ranking", description: "랭킹", sortOrder: 14 },
|
|
|
|
|
{ name: "무료쿠폰", slug: "free-coupons", description: "쿠폰", sortOrder: 15 },
|
|
|
|
|
{ name: "월간집계", slug: "monthly-stats", description: "월간 통계", sortOrder: 16 },
|
2025-11-02 12:07:11 +09:00
|
|
|
// TEST
|
|
|
|
|
{ name: "TEST", slug: "test", description: "테스트 게시판", sortOrder: 20 },
|
2025-11-02 02:46:20 +09:00
|
|
|
// 광고/제휴 계열 게시판은 제거(메인/명예의전당/소통방 외)
|
2025-10-09 11:23:27 +09:00
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const created = [];
|
2025-11-02 12:07:11 +09:00
|
|
|
// 특수 랭킹/텍스트/미리보기 뷰 타입 ID 조회 (사전에 upsertViewTypes로 생성됨)
|
2025-11-02 04:39:28 +09:00
|
|
|
const mainSpecial = await prisma.boardViewType.findUnique({ where: { key: "main_special_rank" } });
|
|
|
|
|
const listSpecial = await prisma.boardViewType.findUnique({ where: { key: "list_special_rank" } });
|
2025-11-02 07:01:42 +09:00
|
|
|
const mainText = await prisma.boardViewType.findUnique({ where: { key: "main_text" } });
|
2025-11-02 12:07:11 +09:00
|
|
|
const mainPreview = await prisma.boardViewType.findUnique({ where: { key: "main_preview" } });
|
2025-11-02 04:39:28 +09:00
|
|
|
|
2025-10-09 11:23:27 +09:00
|
|
|
for (const b of boards) {
|
2025-10-13 07:23:08 +09:00
|
|
|
// 카테고리 매핑 규칙 (트리 기준 상위 카테고리)
|
|
|
|
|
const mapBySlug = {
|
|
|
|
|
// 암실소문 (메인)
|
|
|
|
|
notice: "main",
|
|
|
|
|
greetings: "main",
|
|
|
|
|
"bug-report": "main",
|
|
|
|
|
event: "main",
|
|
|
|
|
attendance: "main",
|
|
|
|
|
// 명예의 전당
|
|
|
|
|
ranking: "hall-of-fame",
|
|
|
|
|
"free-coupons": "hall-of-fame",
|
|
|
|
|
"monthly-stats": "hall-of-fame",
|
|
|
|
|
// 소통방(기본값 community로 처리)
|
|
|
|
|
free: "community",
|
|
|
|
|
qna: "community",
|
|
|
|
|
tips: "community",
|
|
|
|
|
anonymous: "community",
|
2025-11-02 12:07:11 +09:00
|
|
|
// TEST
|
|
|
|
|
test: "test",
|
2025-10-13 14:41:51 +09:00
|
|
|
// 광고/제휴
|
2025-10-13 07:23:08 +09:00
|
|
|
};
|
|
|
|
|
const categorySlug = mapBySlug[b.slug] || "community";
|
2025-10-13 06:29:33 +09:00
|
|
|
const category = categoryMap[categorySlug];
|
2025-10-09 11:23:27 +09:00
|
|
|
const board = await prisma.board.upsert({
|
|
|
|
|
where: { slug: b.slug },
|
|
|
|
|
update: {
|
|
|
|
|
description: b.description,
|
|
|
|
|
sortOrder: b.sortOrder,
|
|
|
|
|
allowAnonymousPost: !!b.allowAnonymousPost,
|
|
|
|
|
readLevel: b.readLevel || undefined,
|
2025-10-13 06:29:33 +09:00
|
|
|
categoryId: category ? category.id : undefined,
|
2025-11-02 12:07:11 +09:00
|
|
|
// 메인뷰: 기본이 아니라 텍스트(main_text)로 설정, 랭킹 보드는 특수 랭킹 유지, TEST는 미리보기
|
2025-11-02 07:01:42 +09:00
|
|
|
...(b.slug === "ranking"
|
|
|
|
|
? (mainSpecial ? { mainPageViewTypeId: mainSpecial.id } : {})
|
2025-11-02 12:07:11 +09:00
|
|
|
: b.slug === "test"
|
|
|
|
|
? (mainPreview ? { mainPageViewTypeId: mainPreview.id } : {})
|
2025-11-02 07:01:42 +09:00
|
|
|
: (mainText ? { mainPageViewTypeId: mainText.id } : {})),
|
2025-11-02 04:39:28 +09:00
|
|
|
...(b.slug === "ranking" && listSpecial ? { listViewTypeId: listSpecial.id } : {}),
|
2025-10-09 11:23:27 +09:00
|
|
|
},
|
|
|
|
|
create: {
|
|
|
|
|
name: b.name,
|
|
|
|
|
slug: b.slug,
|
|
|
|
|
description: b.description,
|
|
|
|
|
sortOrder: b.sortOrder,
|
|
|
|
|
allowAnonymousPost: !!b.allowAnonymousPost,
|
|
|
|
|
readLevel: b.readLevel || undefined,
|
2025-10-13 06:29:33 +09:00
|
|
|
categoryId: category ? category.id : undefined,
|
2025-11-02 12:07:11 +09:00
|
|
|
// 메인뷰: 기본이 아니라 텍스트(main_text)로 설정, 랭킹 보드는 특수 랭킹 유지, TEST는 미리보기
|
2025-11-02 07:01:42 +09:00
|
|
|
...(b.slug === "ranking"
|
|
|
|
|
? (mainSpecial ? { mainPageViewTypeId: mainSpecial.id } : {})
|
2025-11-02 12:07:11 +09:00
|
|
|
: b.slug === "test"
|
|
|
|
|
? (mainPreview ? { mainPageViewTypeId: mainPreview.id } : {})
|
2025-11-02 07:01:42 +09:00
|
|
|
: (mainText ? { mainPageViewTypeId: mainText.id } : {})),
|
2025-11-02 04:39:28 +09:00
|
|
|
...(b.slug === "ranking" && listSpecial ? { listViewTypeId: listSpecial.id } : {}),
|
2025-10-09 11:23:27 +09:00
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
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" },
|
|
|
|
|
});
|
2025-10-08 23:44:21 +09:00
|
|
|
}
|
2025-10-09 11:23:27 +09:00
|
|
|
}
|
|
|
|
|
return created;
|
|
|
|
|
}
|
2025-10-08 23:44:21 +09:00
|
|
|
|
2025-10-31 16:27:04 +09:00
|
|
|
async function upsertViewTypes() {
|
|
|
|
|
const viewTypes = [
|
|
|
|
|
// main scope
|
|
|
|
|
{ key: "main_default", name: "기본", scope: "main" },
|
|
|
|
|
{ key: "main_text", name: "텍스트", scope: "main" },
|
|
|
|
|
{ key: "main_preview", name: "미리보기", scope: "main" },
|
|
|
|
|
{ key: "main_special_rank", name: "특수랭킹", scope: "main" },
|
|
|
|
|
// list scope
|
|
|
|
|
{ key: "list_default", name: "기본", scope: "list" },
|
|
|
|
|
{ key: "list_text", name: "텍스트", scope: "list" },
|
|
|
|
|
{ key: "list_preview", name: "미리보기", scope: "list" },
|
|
|
|
|
{ key: "list_special_rank", name: "특수랭킹", scope: "list" },
|
|
|
|
|
];
|
|
|
|
|
for (const vt of viewTypes) {
|
|
|
|
|
await prisma.boardViewType.upsert({
|
|
|
|
|
where: { key: vt.key },
|
|
|
|
|
update: { name: vt.name, scope: vt.scope },
|
|
|
|
|
create: vt,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-02 02:46:20 +09:00
|
|
|
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)];
|
2025-11-05 23:47:44 +09:00
|
|
|
const title = randomTitle(board.name, i);
|
|
|
|
|
const img = randomImageUrl(`${board.slug}-${i}`);
|
|
|
|
|
const p1 = randomSentence();
|
|
|
|
|
const p2 = randomSentence();
|
|
|
|
|
const p3 = randomSentence();
|
2025-11-02 02:46:20 +09:00
|
|
|
data.push({
|
|
|
|
|
boardId: board.id,
|
|
|
|
|
authorId,
|
2025-11-05 23:47:44 +09:00
|
|
|
title,
|
|
|
|
|
content: `<p>${p1}</p>\n<figure><img src="${img}" alt="seed image" /></figure>\n<p>${p2}</p>\n<p>${p3}</p>`,
|
2025-11-02 02:46:20 +09:00
|
|
|
status: "published",
|
|
|
|
|
isAnonymous: board.allowAnonymousPost ? Math.random() < 0.3 : false,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
await prisma.post.createMany({ data });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-09 11:23:27 +09:00
|
|
|
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,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-02 02:46:20 +09:00
|
|
|
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) {
|
2025-11-07 23:41:52 +09:00
|
|
|
await prisma.partnerRequest.upsert({
|
|
|
|
|
where: { id: `${it.region}-${it.name}` },
|
|
|
|
|
update: { address: it.address, imageUrl: it.imageUrl, sortOrder: it.sortOrder, active: true, status: "approved" },
|
|
|
|
|
create: { id: `${it.region}-${it.name}`, region: it.region, name: it.name, address: it.address, imageUrl: it.imageUrl, sortOrder: it.sortOrder, active: true, status: "approved", category: "misc", latitude: 37.5, longitude: 127.0 },
|
2025-11-02 02:46:20 +09:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
// 표시 토글 기본값 보장
|
|
|
|
|
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) },
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-02 04:59:09 +09:00
|
|
|
async function seedBanners() {
|
|
|
|
|
// 기존 배너 정리 후 5개만 채움
|
|
|
|
|
await prisma.banner.deleteMany({});
|
|
|
|
|
const items = Array.from({ length: 5 }).map((_, i) => ({
|
|
|
|
|
title: `메인 배너 ${i + 1}`,
|
|
|
|
|
imageUrl: "/sample.jpg",
|
|
|
|
|
linkUrl: "/",
|
|
|
|
|
sortOrder: i + 1,
|
|
|
|
|
active: true,
|
|
|
|
|
}));
|
|
|
|
|
await prisma.banner.createMany({ data: items });
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-02 07:01:42 +09:00
|
|
|
async function seedMainpageVisibleBoards(boards) {
|
|
|
|
|
const SETTINGS_KEY = "mainpage_settings";
|
|
|
|
|
const setting = await prisma.setting.findUnique({ where: { key: SETTINGS_KEY } });
|
|
|
|
|
const current = setting ? JSON.parse(setting.value) : {};
|
2025-11-02 12:07:11 +09:00
|
|
|
const wantSlugs = new Set(["notice", "free", "ranking", "test"]);
|
2025-11-02 07:01:42 +09:00
|
|
|
const visibleBoardIds = boards.filter((b) => wantSlugs.has(b.slug)).map((b) => b.id);
|
|
|
|
|
const next = { ...current, visibleBoardIds };
|
|
|
|
|
await prisma.setting.upsert({
|
|
|
|
|
where: { key: SETTINGS_KEY },
|
|
|
|
|
update: { value: JSON.stringify(next) },
|
|
|
|
|
create: { key: SETTINGS_KEY, value: JSON.stringify(next) },
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-09 11:23:27 +09:00
|
|
|
async function main() {
|
2025-11-07 21:36:34 +09:00
|
|
|
console.log("DATABASE_URL:", process.env.DATABASE_URL);
|
|
|
|
|
try {
|
|
|
|
|
const tables = await prisma.$queryRaw`SELECT name FROM sqlite_master WHERE type='table'`;
|
|
|
|
|
console.log("SQLite tables:", tables.map((t) => t.name || t.NAME || JSON.stringify(t)));
|
|
|
|
|
} catch {}
|
|
|
|
|
|
|
|
|
|
// SQLite 수동 보정: partner_categories 테이블과 partners.categoryId 컬럼 보장
|
|
|
|
|
try {
|
|
|
|
|
const rows = await prisma.$queryRaw`SELECT name FROM sqlite_master WHERE type='table' AND name='partner_categories'`;
|
|
|
|
|
if (!Array.isArray(rows) || rows.length === 0) {
|
|
|
|
|
console.log("Creating missing table: partner_categories");
|
|
|
|
|
await prisma.$executeRawUnsafe(
|
|
|
|
|
"CREATE TABLE IF NOT EXISTS partner_categories (\n" +
|
|
|
|
|
"id TEXT PRIMARY KEY,\n" +
|
|
|
|
|
"name TEXT NOT NULL UNIQUE,\n" +
|
|
|
|
|
"sortOrder INTEGER NOT NULL DEFAULT 0,\n" +
|
|
|
|
|
"createdAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP\n" +
|
|
|
|
|
")"
|
|
|
|
|
);
|
|
|
|
|
await prisma.$executeRawUnsafe("CREATE INDEX IF NOT EXISTS idx_partner_categories_sortOrder ON partner_categories(sortOrder)");
|
|
|
|
|
}
|
|
|
|
|
const cols = await prisma.$queryRaw`PRAGMA table_info('partners')`;
|
|
|
|
|
const hasCategoryId = Array.isArray(cols) && cols.some((c) => (c.name || c.COLUMN_NAME) === 'categoryId');
|
|
|
|
|
if (!hasCategoryId) {
|
|
|
|
|
console.log("Adding missing column: partners.categoryId");
|
|
|
|
|
await prisma.$executeRawUnsafe("ALTER TABLE partners ADD COLUMN categoryId TEXT");
|
|
|
|
|
// 외래키 제약은 생략 (SQLite에서는 제약 추가가 까다로움)
|
|
|
|
|
}
|
2025-11-07 23:41:52 +09:00
|
|
|
// partner_requests 확장 컬럼 보장 (region, imageUrl, sortOrder, active)
|
|
|
|
|
const prCols = await prisma.$queryRaw`PRAGMA table_info('partner_requests')`;
|
|
|
|
|
const colHas = (name) => Array.isArray(prCols) && prCols.some((c) => (c.name || c.COLUMN_NAME) === name);
|
|
|
|
|
if (!colHas('region')) {
|
|
|
|
|
console.log("Adding missing column: partner_requests.region");
|
|
|
|
|
await prisma.$executeRawUnsafe("ALTER TABLE partner_requests ADD COLUMN region TEXT");
|
|
|
|
|
}
|
|
|
|
|
if (!colHas('imageUrl')) {
|
|
|
|
|
console.log("Adding missing column: partner_requests.imageUrl");
|
|
|
|
|
await prisma.$executeRawUnsafe("ALTER TABLE partner_requests ADD COLUMN imageUrl TEXT");
|
|
|
|
|
}
|
|
|
|
|
if (!colHas('sortOrder')) {
|
|
|
|
|
console.log("Adding missing column: partner_requests.sortOrder");
|
|
|
|
|
await prisma.$executeRawUnsafe("ALTER TABLE partner_requests ADD COLUMN sortOrder INTEGER NOT NULL DEFAULT 0");
|
|
|
|
|
}
|
|
|
|
|
if (!colHas('active')) {
|
|
|
|
|
console.log("Adding missing column: partner_requests.active");
|
|
|
|
|
await prisma.$executeRawUnsafe("ALTER TABLE partner_requests ADD COLUMN active BOOLEAN NOT NULL DEFAULT 1");
|
|
|
|
|
}
|
2025-11-07 21:36:34 +09:00
|
|
|
} catch (e) {
|
|
|
|
|
console.warn("SQLite schema ensure failed:", e);
|
|
|
|
|
}
|
2025-10-09 11:23:27 +09:00
|
|
|
await upsertRoles();
|
|
|
|
|
const admin = await upsertAdmin();
|
2025-10-13 06:29:33 +09:00
|
|
|
const categoryMap = await upsertCategories();
|
2025-10-31 16:27:04 +09:00
|
|
|
await upsertViewTypes();
|
2025-11-02 02:46:20 +09:00
|
|
|
await createRandomUsers(100);
|
|
|
|
|
await removeNonPrimaryBoards();
|
2025-10-13 06:29:33 +09:00
|
|
|
const boards = await upsertBoards(admin, categoryMap);
|
2025-11-02 07:01:42 +09:00
|
|
|
await seedMainpageVisibleBoards(boards);
|
2025-11-02 02:46:20 +09:00
|
|
|
await createPostsForAllBoards(boards, 100, admin);
|
|
|
|
|
await seedPartnerShops();
|
2025-11-02 04:59:09 +09:00
|
|
|
await seedBanners();
|
2025-10-09 11:23:27 +09:00
|
|
|
|
|
|
|
|
await seedPolicies();
|
2025-10-09 17:38:46 +09:00
|
|
|
|
|
|
|
|
// 제휴업체 예시 데이터
|
|
|
|
|
const partners = [
|
2025-11-02 02:46:20 +09:00
|
|
|
{ name: "test", category: "spa", latitude: 37.5700, longitude: 126.9769, address: "서울 종로구" },
|
2025-10-09 17:38:46 +09:00
|
|
|
{ name: "굿짐", category: "gym", latitude: 37.5600, longitude: 126.9820, address: "서울 중구" },
|
|
|
|
|
];
|
2025-11-07 21:36:34 +09:00
|
|
|
// 파트너 카테고리(PartnerCategory) 생성 및 매핑
|
|
|
|
|
const partnerCategoryNames = Array.from(new Set(partners.map((p) => p.category).filter(Boolean)));
|
|
|
|
|
const partnerCategoryMap = {};
|
|
|
|
|
for (let i = 0; i < partnerCategoryNames.length; i++) {
|
|
|
|
|
const name = partnerCategoryNames[i];
|
|
|
|
|
const created = await prisma.partnerCategory.upsert({
|
|
|
|
|
where: { name },
|
|
|
|
|
update: { sortOrder: i + 1 },
|
|
|
|
|
create: { name, sortOrder: i + 1 },
|
|
|
|
|
});
|
|
|
|
|
partnerCategoryMap[name] = created;
|
|
|
|
|
}
|
2025-10-09 17:38:46 +09:00
|
|
|
for (const p of partners) {
|
2025-11-07 21:36:34 +09:00
|
|
|
const categoryRef = p.category ? partnerCategoryMap[p.category] : null;
|
|
|
|
|
const data = { ...p, categoryId: categoryRef ? categoryRef.id : null };
|
|
|
|
|
await prisma.partner.upsert({ where: { name: p.name }, update: data, create: data });
|
2025-10-09 17:38:46 +09:00
|
|
|
}
|
2025-10-08 23:04:12 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
main()
|
|
|
|
|
.catch((e) => {
|
|
|
|
|
console.error(e);
|
|
|
|
|
process.exit(1);
|
|
|
|
|
})
|
|
|
|
|
.finally(async () => {
|
|
|
|
|
await prisma.$disconnect();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|