test
Some checks failed
deploy-on-main / deploy (push) Failing after 22s

This commit is contained in:
koreacomp5
2025-11-09 22:05:22 +09:00
parent 34e831f738
commit a007ac11ce
21 changed files with 845 additions and 112 deletions

View File

@@ -229,8 +229,8 @@ model User {
nickname String @unique
passwordHash String?
name String
birth DateTime
phone String @unique
birth DateTime?
phone String? @unique
rank Int @default(0)
// 누적 포인트, 레벨, 등급(0~10)
points Int @default(0)
@@ -259,6 +259,7 @@ model User {
blocksInitiated Block[] @relation("Blocker")
blocksReceived Block[] @relation("Blocked")
pointTxns PointTransaction[]
attendances Attendance[]
sanctions Sanction[]
nicknameChanges NicknameChange[]
passwordResetTokens PasswordResetToken[] @relation("PasswordResetUser")
@@ -506,6 +507,20 @@ model PointTransaction {
@@map("point_transactions")
}
// 출석부 기록 (사용자별 일자 단위 출석)
model Attendance {
id String @id @default(cuid())
userId String
date DateTime // 자정 기준 날짜만 사용
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [userId], onDelete: Cascade)
@@unique([userId, date])
@@index([userId, date])
@@map("attendance")
}
// 레벨 임계치(선택)
model LevelThreshold {
id String @id @default(cuid())

View File

@@ -270,6 +270,25 @@ async function upsertAdmin() {
return admin;
}
async function seedAdminAttendance(admin) {
try {
const now = new Date();
const year = now.getUTCFullYear();
const days = [1, 2, 5, 6]; // 11월 1,2,5,6일
for (const d of days) {
const date = new Date(Date.UTC(year, 10, d, 0, 0, 0, 0)); // 10 = November (0-based)
// @@unique([userId, date]) 기준으로 업서트
await prisma.attendance.upsert({
where: { userId_date: { userId: admin.userId, date } },
update: {},
create: { userId: admin.userId, date },
});
}
} catch (e) {
console.warn("seedAdminAttendance failed:", e);
}
}
async function upsertBoards(admin, categoryMap) {
const boards = [
// 일반
@@ -292,11 +311,12 @@ async function upsertBoards(admin, categoryMap) {
];
const created = [];
// 특수 랭킹/텍스트/미리보기 뷰 타입 ID 조회 (사전에 upsertViewTypes로 생성됨)
// 텍스트/특수랭킹/특수출석 뷰 타입 ID 조회 (사전에 upsertViewTypes로 생성됨)
const mainText = await prisma.boardViewType.findUnique({ where: { key: "main_text" } });
const listText = await prisma.boardViewType.findUnique({ where: { key: "list_text" } });
const mainSpecial = await prisma.boardViewType.findUnique({ where: { key: "main_special_rank" } });
const listSpecial = await prisma.boardViewType.findUnique({ where: { key: "list_special_rank" } });
const mainText = await prisma.boardViewType.findUnique({ where: { key: "main_text" } });
const mainPreview = await prisma.boardViewType.findUnique({ where: { key: "main_preview" } });
const listSpecialAttendance = await prisma.boardViewType.findUnique({ where: { key: "list_special_attendance" } });
for (const b of boards) {
// 카테고리 매핑 규칙 (트리 기준 상위 카테고리)
@@ -330,13 +350,12 @@ async function upsertBoards(admin, categoryMap) {
allowAnonymousPost: !!b.allowAnonymousPost,
readLevel: b.readLevel || undefined,
categoryId: category ? category.id : undefined,
// 메인뷰: 기본이 아니라 텍스트(main_text)로 설정, 랭킹 보드는 특수 랭킹 유지, TEST는 미리보기
...(b.slug === "ranking"
? (mainSpecial ? { mainPageViewTypeId: mainSpecial.id } : {})
: b.slug === "test"
? (mainPreview ? { mainPageViewTypeId: mainPreview.id } : {})
: (mainText ? { mainPageViewTypeId: mainText.id } : {})),
// 기본은 텍스트, 'ranking'은 특수랭킹, 'attendance'는 특수출석으로 오버라이드
...(mainText ? { mainPageViewTypeId: mainText.id } : {}),
...(listText ? { listViewTypeId: listText.id } : {}),
...(b.slug === "ranking" && mainSpecial ? { mainPageViewTypeId: mainSpecial.id } : {}),
...(b.slug === "ranking" && listSpecial ? { listViewTypeId: listSpecial.id } : {}),
...(b.slug === "attendance" && listSpecialAttendance ? { listViewTypeId: listSpecialAttendance.id } : {}),
},
create: {
name: b.name,
@@ -346,13 +365,12 @@ async function upsertBoards(admin, categoryMap) {
allowAnonymousPost: !!b.allowAnonymousPost,
readLevel: b.readLevel || undefined,
categoryId: category ? category.id : undefined,
// 메인뷰: 기본이 아니라 텍스트(main_text)로 설정, 랭킹 보드는 특수 랭킹 유지, TEST는 미리보기
...(b.slug === "ranking"
? (mainSpecial ? { mainPageViewTypeId: mainSpecial.id } : {})
: b.slug === "test"
? (mainPreview ? { mainPageViewTypeId: mainPreview.id } : {})
: (mainText ? { mainPageViewTypeId: mainText.id } : {})),
// 기본은 텍스트, 'ranking'은 특수랭킹, 'attendance'는 특수출석으로 오버라이드
...(mainText ? { mainPageViewTypeId: mainText.id } : {}),
...(listText ? { listViewTypeId: listText.id } : {}),
...(b.slug === "ranking" && mainSpecial ? { mainPageViewTypeId: mainSpecial.id } : {}),
...(b.slug === "ranking" && listSpecial ? { listViewTypeId: listSpecial.id } : {}),
...(b.slug === "attendance" && listSpecialAttendance ? { listViewTypeId: listSpecialAttendance.id } : {}),
},
});
created.push(board);
@@ -373,16 +391,15 @@ async function upsertBoards(admin, categoryMap) {
async function upsertViewTypes() {
const viewTypes = [
// main scope
{ key: "main_default", name: "기본", scope: "main" },
// main scope (기본/없음 제거, 텍스트 중심)
{ 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" },
// list scope (기본/없음 제거, 텍스트 중심)
{ key: "list_text", name: "텍스트", scope: "list" },
{ key: "list_preview", name: "미리보기", scope: "list" },
{ key: "list_special_rank", name: "특수랭킹", scope: "list" },
{ key: "list_special_attendance", name: "특수출석", scope: "list" },
];
for (const vt of viewTypes) {
await prisma.boardViewType.upsert({
@@ -404,6 +421,8 @@ 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) {
// 회원랭킹 보드는 특수랭킹용이라 게시글을 시드하지 않습니다.
if (board.slug === "ranking") continue;
const data = [];
for (let i = 0; i < countPerBoard; i++) {
const authorId = ["notice", "bug-report"].includes(board.slug)
@@ -530,6 +549,21 @@ async function main() {
);
await prisma.$executeRawUnsafe("CREATE INDEX IF NOT EXISTS idx_partner_categories_sortOrder ON partner_categories(sortOrder)");
}
// Attendance 테이블 보장 (마이그레이션 미실행 환경 대응)
const att = await prisma.$queryRaw`SELECT name FROM sqlite_master WHERE type='table' AND name='attendance'`;
if (!Array.isArray(att) || att.length === 0) {
console.log("Creating missing table: attendance");
await prisma.$executeRawUnsafe(
"CREATE TABLE IF NOT EXISTS attendance (\n" +
"id TEXT PRIMARY KEY,\n" +
"userId TEXT NOT NULL,\n" +
"date DATETIME NOT NULL,\n" +
"createdAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP\n" +
")"
);
await prisma.$executeRawUnsafe("CREATE UNIQUE INDEX IF NOT EXISTS idx_attendance_user_date ON attendance(userId, date)");
await prisma.$executeRawUnsafe("CREATE INDEX IF NOT EXISTS idx_attendance_date ON attendance(date)");
}
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) {
@@ -566,6 +600,7 @@ async function main() {
await createRandomUsers(100);
await removeNonPrimaryBoards();
const boards = await upsertBoards(admin, categoryMap);
await seedAdminAttendance(admin);
await seedMainpageVisibleBoards(boards);
await createPostsForAllBoards(boards, 100, admin);
await seedPartnerShops();