diff options
Diffstat (limited to 'db/seeds_2')
| -rw-r--r-- | db/seeds_2/companySeed.ts | 38 | ||||
| -rw-r--r-- | db/seeds_2/itmeSeed.ts | 108 | ||||
| -rw-r--r-- | db/seeds_2/rfqSeed.ts | 148 | ||||
| -rw-r--r-- | db/seeds_2/seed.ts | 56 | ||||
| -rw-r--r-- | db/seeds_2/taskSeed.ts | 46 | ||||
| -rw-r--r-- | db/seeds_2/userSeed.ts | 39 | ||||
| -rw-r--r-- | db/seeds_2/vendorSeed.ts | 105 |
7 files changed, 540 insertions, 0 deletions
diff --git a/db/seeds_2/companySeed.ts b/db/seeds_2/companySeed.ts new file mode 100644 index 00000000..4293b02b --- /dev/null +++ b/db/seeds_2/companySeed.ts @@ -0,0 +1,38 @@ +import db from "@/db/db" +import { companies } from "@/db/schema/companies" +import { faker } from "@faker-js/faker" +export type NewCompany = typeof companies.$inferInsert + +function generateRandomCompany(): NewCompany { + return { + // Drizzle에서 기본 키(id)는 자동 생성이므로 제외 + name: faker.company.name(), + taxID: faker.number.int({ min: 100000000, max: 999999999 }), + // createdAt은 defaultNow()가 있지만, 예시로 faker를 써서 과거 시점을 넣고 싶다면: + createdAt: faker.date.past() + } +} + +export async function seedCompanies(input: { count: number }) { + try { + const allCompanies: NewCompany[] = [] + + for (let i = 0; i < input.count; i++) { + allCompanies.push(generateRandomCompany()) + } + + console.log("📝 Inserting companies", allCompanies.length) + + // onConflictDoNothing을 사용하여 중복되는 데이터는 건너뛰고 새로운 데이터만 추가 + const result = await db.insert(companies) + .values(allCompanies) + .onConflictDoNothing() + .returning() + + console.log(`✅ Successfully added ${result.length} new companies`) + return result + } catch (err) { + console.error("Failed to seed companies:", err) + throw err + } +} diff --git a/db/seeds_2/itmeSeed.ts b/db/seeds_2/itmeSeed.ts new file mode 100644 index 00000000..6d725c22 --- /dev/null +++ b/db/seeds_2/itmeSeed.ts @@ -0,0 +1,108 @@ +// db/seed/itemSeed.ts +import { faker } from "@faker-js/faker" +import db from "@/db/db" +import { items } from "../schema/items" +import { desc } from "drizzle-orm" + +function generateItemName(category: string): string { + const namePattern = Math.random() + if (namePattern < 0.4) { + // 일반적인 제품명 + return `${category} ${faker.commerce.productName()}` + } else if (namePattern < 0.7) { + // 기술 사양이 포함된 이름 + const spec = faker.number.int({ min: 100, max: 9999 }) + return `${category} ${faker.commerce.productName()} ${spec}` + } else { + // 브랜드가 포함된 이름 + const brand = faker.company.name().split(" ")[0] + return `${brand} ${category} ${faker.commerce.productName()}` + } +} + +async function generateItem(industry: string, category: string, index: number) { + const itemCode = `${industry.substring(0, 3).toUpperCase()}-${category.substring(0, 3).toUpperCase()}-${String(index + 1).padStart(4, "0")}` + + return { + itemCode, + itemName: generateItemName(category), + description: `${industry} - ${category}: ${faker.commerce.productDescription()}`, + } +} + +export async function seedItems(input: { itemsPerCategory: number }) { + try { + // 다양한 산업군별 아이템 카테고리 정의 + const categories = { + Electronics: ["Semiconductors", "PCB Components", "Sensors", "Displays", "Batteries"], + Automotive: ["Engine Parts", "Transmission", "Suspension", "Electrical", "Interior"], + Chemical: ["Industrial Chemicals", "Polymers", "Coatings", "Adhesives", "Solvents"], + Construction: ["Steel", "Concrete", "Lumber", "Insulation", "Hardware"], + Manufacturing: ["Tools", "Machinery", "Spare Parts", "Raw Materials", "Equipment"] + } as const; + + // 현재 가장 큰 itemCode 찾기 + const lastItems = await db.select() + .from(items) + .orderBy(desc(items.itemCode)) + .limit(Object.keys(categories).length * 5); // 각 산업별 서브카테고리 수 + + // 각 카테고리별 마지막 인덱스 추적 + const lastIndices: Record<keyof typeof categories, Record<string, number>> = { + Electronics: {}, + Automotive: {}, + Chemical: {}, + Construction: {}, + Manufacturing: {}, + }; + + // 기존 아이템의 itemCode에서 카테고리와 인덱스 추출 + for (const item of lastItems) { + if (!item.itemCode) continue; + + const [industryCode, categoryCode, indexStr] = item.itemCode.split("-"); + if (!industryCode || !categoryCode || !indexStr) continue; + + // 코드로부터 원래 카테고리 찾기 + const industry = Object.keys(categories).find(ind => ind.substring(0, 3).toUpperCase() === industryCode) as keyof typeof categories; + if (!industry) continue; + + const category = categories[industry].find(cat => cat.substring(0, 3).toUpperCase() === categoryCode); + if (!category) continue; + + if (!lastIndices[industry]) { + lastIndices[industry] = {}; + } + + const itemIndex = parseInt(indexStr); + if (!lastIndices[industry][category] || itemIndex > lastIndices[industry][category]) { + lastIndices[industry][category] = itemIndex; + } + } + + const allItems = []; + + for (const [industry, subcategories] of Object.entries(categories)) { + for (const category of subcategories) { + const startIndex = (lastIndices[industry as keyof typeof categories]?.[category] ?? 0); + + for (let i = 0; i < input.itemsPerCategory; i++) { + allItems.push(await generateItem(industry, category, startIndex + i)); + } + } + } + + console.log("📝 Inserting items", allItems.length); + + const result = await db.insert(items) + .values(allItems) + .onConflictDoNothing() + .returning(); + + console.log(`✅ Successfully added ${result.length} new items across ${Object.keys(categories).length} industries`); + return result; + } catch (err) { + console.error("Failed to seed items:", err); + throw err; + } +}
\ No newline at end of file diff --git a/db/seeds_2/rfqSeed.ts b/db/seeds_2/rfqSeed.ts new file mode 100644 index 00000000..f2cde1b2 --- /dev/null +++ b/db/seeds_2/rfqSeed.ts @@ -0,0 +1,148 @@ +// db/seed/rfqSeed.ts +import { faker } from "@faker-js/faker"; +import db from "@/db/db" +import { eq, inArray, desc } from "drizzle-orm"; + +// 스키마 import +import { + rfqs, rfqItems, rfqVendors, vendorQuotes, rfqAttachments, rfqComments, rfqEvaluations, + Rfq, RfqItem, RfqVendor, VendorQuote, RfqAttach, RfqComment, RfqEvaluation +} from "../schema/rfq"; +import { vendors } from "../schema/vendors"; +import { users } from "../schema/users"; +import { items } from "../schema/items"; + +async function generateRfq(index: number, allUsers: any[], allVendors: any[], allItems: any[]) { + const year = faker.number.int({ min: 2022, max: 2024 }); + const sequence = String(index + 1).padStart(3, "0"); + const rfqCode = `RFQ-${year}-${sequence}`; + + // 상태별 비율 설정 + const statusDistribution = { + DRAFT: 0.3, // 30% + PUBLISHED: 0.3, // 30% + EVALUATION: 0.2, // 20% + AWARDED: 0.2 // 20% + }; + + // 랜덤 상태 결정 + const rand = Math.random(); + let status: "DRAFT" | "PUBLISHED" | "EVALUATION" | "AWARDED"; + if (rand < statusDistribution.DRAFT) status = "DRAFT"; + else if (rand < statusDistribution.DRAFT + statusDistribution.PUBLISHED) status = "PUBLISHED"; + else if (rand < statusDistribution.DRAFT + statusDistribution.PUBLISHED + statusDistribution.EVALUATION) status = "EVALUATION"; + else status = "AWARDED"; + + // RFQ 생성 + const [rfq] = await db.insert(rfqs).values({ + rfqCode, + description: faker.commerce.productDescription(), + projectCode: `PRJ-${faker.string.alphanumeric(6).toUpperCase()}`, + projectName: faker.company.catchPhrase(), + dueDate: faker.date.future(), + status, + createdBy: faker.helpers.arrayElement(allUsers).id, + }).returning(); + + // 3-10개의 items 추가 + const itemCount = faker.number.int({ min: 3, max: 10 }); + const selectedItems = faker.helpers.arrayElements(allItems, itemCount); + for (const item of selectedItems) { + await db.insert(rfqItems).values({ + rfqId: rfq.id, + itemCode: item.itemCode, + description: faker.commerce.productDescription(), + quantity: faker.number.float({ min: 1, max: 1000, fractionDigits: 2 }), + uom: faker.helpers.arrayElement(["EA", "KG", "M", "L", "SET"]), + }); + } + + // 2-5개의 vendors 초대 + const vendorCount = faker.number.int({ min: 2, max: 5 }); + const selectedVendors = faker.helpers.arrayElements(allVendors, vendorCount); + for (const vendor of selectedVendors) { + await db.insert(rfqVendors).values({ + rfqId: rfq.id, + vendorId: vendor.id, + status: faker.helpers.arrayElement(["INVITED", "ACCEPTED", "REJECTED", "QUOTED"]), + }); + + // 50% 확률로 quote 생성 + if (Math.random() > 0.5) { + await db.insert(vendorQuotes).values({ + rfqId: rfq.id, + vendorId: vendor.id, + totalAmount: faker.number.float({ min: 1000, max: 1000000, fractionDigits: 2 }), + currency: faker.helpers.arrayElement(["USD", "EUR", "KRW"]), + leadTime: `${faker.number.int({ min: 1, max: 52 })} weeks`, + notes: faker.lorem.paragraph(), + }); + } + + // 30% 확률로 attachment 추가 + if (Math.random() > 0.7) { + await db.insert(rfqAttachments).values({ + rfqId: rfq.id, + vendorId: vendor.id, + fileName: `${faker.system.fileName()}.pdf`, + filePath: `/uploads/rfq/${rfq.id}/${faker.system.fileName()}.pdf`, + }); + } + + // 40% 확률로 comment 추가 + if (Math.random() > 0.6) { + await db.insert(rfqComments).values({ + rfqId: rfq.id, + vendorId: vendor.id, + commentText: faker.lorem.paragraph(), + commentedBy: faker.helpers.arrayElement(allUsers).id, + }); + } + + // EVALUATION 또는 AWARDED 상태인 경우 평가 데이터 추가 + if (status === "EVALUATION" || status === "AWARDED") { + await db.insert(rfqEvaluations).values({ + rfqId: rfq.id, + vendorId: vendor.id, + evalType: faker.helpers.arrayElement(["TBE", "CBE"]), + result: faker.helpers.arrayElement(["PASS", "FAIL", "ACCEPTABLE"]), + notes: faker.lorem.paragraph(), + }); + } + } + + return rfq; +} + +export async function seedRfqData(input: { count: number }) { + try { + const allUsers = await db.select().from(users); + const allVendors = await db.select().from(vendors); + const allItems = await db.select().from(items); + + // 현재 가장 큰 RFQ 코드 찾기 + const [lastRfq] = await db.select() + .from(rfqs) + .orderBy(desc(rfqs.rfqCode)) + .limit(1); + + // RFQ-YYYY-ddd 형식에서 마지막 번호 추출 + const startIndex = lastRfq?.rfqCode + ? parseInt(lastRfq.rfqCode.split('-')[2]) + : 0; + + console.log("📝 Generating RFQ data...", { lastRfqCode: lastRfq?.rfqCode, startIndex }); + + const generatedRfqs = []; + for (let i = 0; i < input.count; i++) { + const rfq = await generateRfq(startIndex + i, allUsers, allVendors, allItems); + generatedRfqs.push(rfq); + } + + console.log(`✅ Successfully generated ${generatedRfqs.length} new RFQs with related data`); + return generatedRfqs; + } catch (err) { + console.error("Failed to seed RFQ data:", err); + throw err; + } +}
\ No newline at end of file diff --git a/db/seeds_2/seed.ts b/db/seeds_2/seed.ts new file mode 100644 index 00000000..2a21fc6e --- /dev/null +++ b/db/seeds_2/seed.ts @@ -0,0 +1,56 @@ +import { seedCompanies } from "./companySeed"
+import { seedItems } from "./itmeSeed"
+import { seedRfqData } from "./rfqSeed"
+import { seedTasks } from "./taskSeed"
+import { seedUsers } from "./userSeed"
+import { seedVendors } from "./vendorSeed"
+
+// 시드 데이터 수 중앙 관리
+const SEED_COUNTS = {
+ companies: 100,
+ users: 100,
+ items: {
+ itemsPerCategory: 100
+ },
+ vendors: 100,
+ tasks: 100,
+ rfqs: 100
+} as const;
+
+async function runAllSeeds() {
+ console.log("⏳ 전체 시드 실행 시작...")
+ const start = Date.now()
+
+ try {
+ // 1. 먼저 독립적인 기본 데이터 생성
+ await seedCompanies({ count: SEED_COUNTS.companies })
+ console.log("✅ Companies 시드 완료")
+
+ await seedUsers({ count: SEED_COUNTS.users })
+ console.log("✅ Users 시드 완료")
+
+ await seedItems({ itemsPerCategory: SEED_COUNTS.items.itemsPerCategory })
+ console.log("✅ Items 시드 완료")
+
+ // 2. FK 참조가 있는 데이터 생성
+ await seedVendors({ count: SEED_COUNTS.vendors })
+ console.log("✅ Vendors 시드 완료")
+
+ await seedTasks({ count: SEED_COUNTS.tasks })
+ console.log("✅ Tasks 시드 완료")
+
+ // 3. 가장 많은 FK 참조가 있는 RFQ 마지막에 생성
+ await seedRfqData({ count: SEED_COUNTS.rfqs })
+ console.log("✅ RFQ 시드 완료")
+
+ const end = Date.now()
+ console.log(`✅ 모든 시드가 ${end - start}ms 만에 완료되었습니다.`)
+ process.exit(0)
+ } catch (err) {
+ console.error("❌ 시드 실행 중 오류 발생:")
+ console.error(err)
+ process.exit(1)
+ }
+}
+
+runAllSeeds()
\ No newline at end of file diff --git a/db/seeds_2/taskSeed.ts b/db/seeds_2/taskSeed.ts new file mode 100644 index 00000000..24fcc0e8 --- /dev/null +++ b/db/seeds_2/taskSeed.ts @@ -0,0 +1,46 @@ +import db from "@/db/db" +import { tasks } from "@/db/schema/tasks" +import { generateId } from "@/lib/id" +import { faker } from "@faker-js/faker" +export type NewTask = typeof tasks.$inferInsert + + +function generateRandomTask(): NewTask { + return { + id: generateId("task"), + code: undefined, + title: faker.hacker + .phrase() + .replace(/^./, (letter) => letter.toUpperCase()), + status: faker.helpers.shuffle(tasks.status.enumValues)[0] ?? "todo", + label: faker.helpers.shuffle(tasks.label.enumValues)[0] ?? "bug", + priority: faker.helpers.shuffle(tasks.priority.enumValues)[0] ?? "low", + archived: faker.datatype.boolean({ probability: 0.2 }), + createdAt: new Date(), + updatedAt: new Date(), + } + } + + +export async function seedTasks(input: { count: number }) { + try { + const allTasks: NewTask[] = [] + + for (let i = 0; i < input.count; i++) { + allTasks.push(generateRandomTask()) + } + + console.log("📝 Inserting tasks", allTasks.length) + + const result = await db.insert(tasks) + .values(allTasks) + .onConflictDoNothing() + .returning() + + console.log(`✅ Successfully added ${result.length} new tasks`) + return result + } catch (err) { + console.error("Failed to seed tasks:", err) + throw err + } +} diff --git a/db/seeds_2/userSeed.ts b/db/seeds_2/userSeed.ts new file mode 100644 index 00000000..c0258d5e --- /dev/null +++ b/db/seeds_2/userSeed.ts @@ -0,0 +1,39 @@ +import db from "@/db/db" +import { NewUser } from "@/lib/admin-users/repository" +import { faker } from "@faker-js/faker" +import { users } from "../schema/users" + +function generateRandomUser(): NewUser { + return { + name: faker.person.fullName(), // 또는 faker.company.name() + email: faker.internet.email(), // 필수 notNull 필드 + domain: "evcp", // domain을 evcp로 고정 + companyId: null, // companyId는 null로(빈칸) + imageUrl: null, // imageUrl은 null로(빈칸) + createdAt: faker.date.past() // 과거 임의 날짜 + } +} + +export async function seedUsers(input: { count: number }) { + try { + const allUsers: NewUser[] = [] + + for (let i = 0; i < input.count; i++) { + allUsers.push(generateRandomUser()) + } + + console.log("📝 Inserting users", allUsers.length) + + // onConflictDoNothing을 사용하여 중복되는 데이터는 건너뛰고 새로운 데이터만 추가 + const result = await db.insert(users) + .values(allUsers) + .onConflictDoNothing() + .returning() + + console.log(`✅ Successfully added ${result.length} new users`) + return result + } catch (err) { + console.error("Failed to seed users:", err) + throw err + } +} diff --git a/db/seeds_2/vendorSeed.ts b/db/seeds_2/vendorSeed.ts new file mode 100644 index 00000000..98e0a98b --- /dev/null +++ b/db/seeds_2/vendorSeed.ts @@ -0,0 +1,105 @@ +// db/seed/vendorSeed.ts +import { faker } from "@faker-js/faker"; +import db from "@/db/db"; +import { vendors } from "../schema/vendors"; +import { desc } from "drizzle-orm"; + +export async function generateVendor(index: number) { + const vendorTypes = ["MANUFACTURER", "DISTRIBUTOR", "SERVICE_PROVIDER"]; + const countries = ["KR", "US", "CN", "JP", "DE", "FR", "GB", "SG", "VN", "TH"]; + const businessTypes = ["Corporation", "LLC", "Partnership", "Sole Proprietorship"]; + + const country = faker.helpers.arrayElement(countries); + const businessType = faker.helpers.arrayElement(businessTypes); + const vendorType = faker.helpers.arrayElement(vendorTypes); + + // 회사 이름 생성 패턴 다양화 + let vendorName; + const namePattern = Math.random(); + if (namePattern < 0.3) { + // 일반적인 회사명 + vendorName = faker.company.name(); + } else if (namePattern < 0.6) { + // 산업 특화된 이름 + const industry = faker.helpers.arrayElement([ + "Tech", "Manufacturing", "Electronics", "Chemical", + "Automotive", "Energy", "Construction" + ]); + vendorName = `${faker.company.name()} ${industry}`; + } else { + // 지역성이 있는 이름 + const region = faker.location.state(); + vendorName = `${region} ${faker.company.name()}`; + } + + return { + vendorCode: `VEN${String(index + 1).padStart(5, "0")}`, + vendorName, + businessNumber: faker.helpers.replaceSymbols("##-###-#####"), + representativeName: faker.person.fullName(), + businessType, + vendorType, + status: faker.helpers.arrayElement(["ACTIVE", "INACTIVE", "BLACKLISTED"]), + country, + address: faker.location.streetAddress(), + city: faker.location.city(), + state: faker.location.state(), + zipCode: faker.location.zipCode(), + contact1Name: faker.person.fullName(), + contact1Email: faker.internet.email(), + contact1Phone: faker.phone.number(), + contact2Name: Math.random() > 0.5 ? faker.person.fullName() : null, + contact2Email: Math.random() > 0.5 ? faker.internet.email() : null, + contact2Phone: Math.random() > 0.5 ? faker.phone.number() : null, + description: faker.company.catchPhrase(), + website: Math.random() > 0.3 ? faker.internet.url() : null, + foundedYear: faker.number.int({ min: 1950, max: 2023 }), + employeeCount: faker.number.int({ min: 10, max: 10000 }), + annualRevenue: faker.number.float({ + min: 1000000, + max: 1000000000, + fractionDigits: 2 + }), + certifications: Math.random() > 0.5 ? + faker.helpers.arrayElements( + ["ISO 9001", "ISO 14001", "OHSAS 18001", "ISO/TS 16949", "AS9100"], + faker.number.int({ min: 1, max: 3 }) + ) : + null, + }; +} + +export async function seedVendors(input: { count: number }) { + try { + const allVendors = []; + + // 현재 가장 큰 vendorCode 찾기 + const [lastVendor] = await db.select() + .from(vendors) + .orderBy(desc(vendors.vendorCode)) + .limit(1); + + const startIndex = lastVendor?.vendorCode + ? parseInt(lastVendor.vendorCode.replace('VEN', '')) + : 0; + + console.log("📝 Inserting vendors", { lastVendorCode: lastVendor?.vendorCode, startIndex }); + + for (let i = 0; i < input.count; i++) { + allVendors.push(await generateVendor(startIndex + i)); + } + + console.log("📝 Inserting vendors", allVendors.length); + + const result = await db.insert(vendors) + .values(allVendors) + .onConflictDoNothing() + .returning(); + + console.log(`✅ Successfully added ${result.length} new vendors`); + return result; + } catch (err) { + console.error("Failed to seed vendors:", err); + throw err; + } +}
\ No newline at end of file |
