diff options
Diffstat (limited to 'db/seeds')
| -rw-r--r-- | db/seeds/contract.ts | 2 | ||||
| -rw-r--r-- | db/seeds/create-contract-cli.ts | 260 | ||||
| -rw-r--r-- | db/seeds/rfqSeed.ts | 6 | ||||
| -rw-r--r-- | db/seeds/vendorSeed.ts | 4 |
4 files changed, 266 insertions, 6 deletions
diff --git a/db/seeds/contract.ts b/db/seeds/contract.ts index 1c266c72..cd2edca4 100644 --- a/db/seeds/contract.ts +++ b/db/seeds/contract.ts @@ -57,7 +57,7 @@ async function main() { const NUM_CONTRACTS = 30 for (let i = 0; i < NUM_CONTRACTS; i++) { - // 무작위 벤더, 프로젝트 선택 + // 무작위 협력업체, 프로젝트 선택 const randomVendor = faker.helpers.arrayElement(allVendors) const randomProject = faker.helpers.arrayElement(allProjects) diff --git a/db/seeds/create-contract-cli.ts b/db/seeds/create-contract-cli.ts new file mode 100644 index 00000000..3b0a8963 --- /dev/null +++ b/db/seeds/create-contract-cli.ts @@ -0,0 +1,260 @@ +import db from "@/db/db" +import { contracts, contractItems } from "@/db/schema/contract" +import { vendors } from "@/db/schema/vendors" +import { items } from "@/db/schema/items" +import { projects } from "@/db/schema/projects" +import { faker } from "@faker-js/faker" +import { eq } from "drizzle-orm" + +// 명령행 인자 파싱 +const args = process.argv.slice(2); +let vendorId: number | undefined, + projectId: number | undefined, + itemIds: number[] | undefined, + numContracts = 1, + shouldDeleteExisting = false; + +// 명령행 인자 파서 +function parseArgs() { + for (const arg of args) { + if (arg.startsWith('--vendor=')) { + vendorId = parseInt(arg.split('=')[1], 10); + } else if (arg.startsWith('--project=')) { + projectId = parseInt(arg.split('=')[1], 10); + } else if (arg.startsWith('--items=')) { + itemIds = arg.split('=')[1].split(',').map(id => parseInt(id.trim(), 10)); + } else if (arg.startsWith('--count=')) { + numContracts = parseInt(arg.split('=')[1], 10); + } else if (arg === '--delete-existing') { + shouldDeleteExisting = true; + } else if (arg === '--help' || arg === '-h') { + showHelp(); + process.exit(0); + } + } + + if (!vendorId || !projectId || !itemIds || itemIds.length === 0) { + console.log("필수 인자가 누락되었습니다: vendorId, projectId, 또는 itemIds"); + showHelp(); + process.exit(1); + } +} + +function showHelp() { + console.log(` +사용법: npx tsx create-contract-cli.ts [옵션] + +필수 옵션: + --vendor=<vendorId> 협력업체 ID + --project=<projectId> 프로젝트 ID + --items=<itemId1,itemId2> 품목 ID (쉼표로 구분) + +선택 옵션: + --count=<num> 생성할 계약 수량 (기본값: 1) + --delete-existing 기존 계약 데이터 모두 삭제 + --help, -h 도움말 표시 + +예시: + npx tsx create-contract-cli.ts --vendor=1 --project=2 --items=3,4,5 --count=3 + `); +} + +async function main() { + console.log("계약 시딩 시작..."); + parseArgs(); + + console.log(`설정: vendorId=${vendorId}, projectId=${projectId}, itemIds=[${itemIds!.join(', ')}], 계약 수량=${numContracts}`); + + // DB에서 선택된 vendor, project, items 확인 + const vendor = await db.select().from(vendors).where(eq(vendors.id, vendorId!)).limit(1); + if (vendor.length === 0) { + console.log(`ID가 ${vendorId}인 협력업체를 찾을 수 없습니다.`); + process.exit(1); + } + + const project = await db.select().from(projects).where(eq(projects.id, projectId!)).limit(1); + if (project.length === 0) { + console.log(`ID가 ${projectId}인 프로젝트를 찾을 수 없습니다.`); + process.exit(1); + } + + // 여러 아이템 ID 확인 + const selectedItems = await db.select().from(items).where( + itemIds!.map(id => eq(items.id, id)).reduce((a, b) => db.or(a, b)) + ); + + if (selectedItems.length !== itemIds!.length) { + const foundIds = selectedItems.map(item => item.id); + const missingIds = itemIds!.filter(id => !foundIds.includes(id)); + console.log(`일부 아이템 ID를 찾을 수 없습니다: ${missingIds.join(', ')}`); + process.exit(1); + } + + console.log(`협력업체: ${vendor[0].name}`); + console.log(`프로젝트: ${project[0].name}`); + console.log(`선택된 품목: ${selectedItems.map(i => i.name || i.itemCode).join(', ')}`); + + // 기존 데이터 삭제 (요청된 경우) + if (shouldDeleteExisting) { + await db.delete(contractItems); + await db.delete(contracts); + console.log("기존 계약 데이터가 모두 삭제되었습니다."); + } + + // -- 조선업 맥락에 맞는 예시 문구들 -- + const shipyardProjectNamesKR = [ + "벌크선 선체 건조", + "컨테이너선 엔진 오버홀", + "해양 플랜트 모듈 제작", + "LNG 운반선 화물창 시공", + "FPSO 개조 작업", + "밸러스트 수 처리장치 설치", + "VLCC 프로펠러 교체", + "드릴십 탑사이드 모듈 제작", + ] + const paymentTermsExamples = [ + "선금 30%, 중도금 20%, 잔금 50% (인도 후 60일)", + "LC 결제 (신용장 발행 후 30일)", + "전액 선결제", + "계약금 10%, 선박 명명식 시 40%, 인도 시 50%", + ] + const deliveryTermsExamples = [ + "FOB 부산항", + "CIF 상하이항", + "DAP 울산조선소", + "DDP 거제 옥포조선소", + ] + const deliveryLocations = [ + "부산 영도조선소", + "울산 본사 도크 #3", + "거제 옥포조선소 해양공장", + "목포신항 부두", + ] + + // 계약 생성 루프 + for (let i = 0; i < numContracts; i++) { + // 예: C-1234 + const contractNo = `C-${faker.number.int({ min: 1000, max: 9999 })}` + + // 예: "조선 프로젝트: 벌크선 선체 건조 - (프로젝트명)" + const randomShipyardName = faker.helpers.arrayElement(shipyardProjectNamesKR) + const contractName = `조선 프로젝트: ${randomShipyardName} - ${project[0].name}` + + // 상태 + const randomStatus = faker.helpers.arrayElement([ + "ACTIVE", + "FINISHED", + "CANCELED", + "ON_HOLD", + ]) + + // 날짜 + const startDate = faker.helpers.maybe(() => faker.date.past({ years: 1 }), { + probability: 0.7, + }) + const endDate = faker.helpers.maybe(() => faker.date.future({ years: 0.5 }), { + probability: 0.5, + }) + + // PO에 추가된 필드들에 대해 랜덤 할당 + const paymentTerms = faker.helpers.arrayElement(paymentTermsExamples) + const deliveryTerms = faker.helpers.arrayElement(deliveryTermsExamples) + const deliveryDate = faker.date.future({ years: 1 }) + const deliveryLocation = faker.helpers.arrayElement(deliveryLocations) + + // 가격 관련 (단순 랜덤) + const totalAmount = faker.number.float({ min: 5000000, max: 500000000 }) + const discount = faker.number.float({ min: 0, max: 500000}) + const tax = faker.number.float({ min: 0, max: 1000000 }) + const shippingFee = faker.number.float({ min: 0, max: 300000}) + const netTotal = totalAmount - discount + tax + shippingFee + + // 부분 납품/결제 가능 여부 + const partialShippingAllowed = faker.datatype.boolean() + const partialPaymentAllowed = faker.datatype.boolean() + + // remarks, version + const remarks = faker.helpers.maybe(() => faker.lorem.sentence(), { + probability: 0.4, + }) + const version = 1 + + // DB에 계약 insert + const [insertedContract] = await db + .insert(contracts) + .values({ + projectId: projectId!, + vendorId: vendorId!, + contractNo, + contractName, + status: randomStatus, + startDate: startDate ?? null, + endDate: endDate ?? null, + + paymentTerms, + deliveryTerms, + deliveryDate, + deliveryLocation, + + currency: faker.helpers.arrayElement(["KRW", "USD"]), + totalAmount, + discount, + tax, + shippingFee, + netTotal, + + partialShippingAllowed, + partialPaymentAllowed, + + remarks: remarks ?? null, + version, + }) + .returning({ id: contracts.id }) + + const contractId = insertedContract.id + console.log(`계약 생성 완료 #${contractId}: ${contractNo}`) + + // 선택한 아이템들에 대해 contract_items 생성 + for (const itemId of itemIds!) { + const item = selectedItems.find(i => i.id === itemId); + + const quantity = faker.number.int({ min: 1, max: 100 }) + const unitPrice = faker.number.float({ min: 1000, max: 500000 }) + + // 세율, 세금, 합계 계산 + const taxRate = faker.helpers.arrayElement([0, 0.05, 0.1]) + const taxAmount = parseFloat((unitPrice * quantity * taxRate).toFixed(2)) + const totalLineAmount = parseFloat((unitPrice * quantity + taxAmount).toFixed(2)) + + const description = item?.name || faker.commerce.productName() + const remark = faker.helpers.maybe(() => faker.lorem.sentence(), { probability: 0.3 }) + + const [insertedContractItem] = await db + .insert(contractItems) + .values({ + contractId, + itemId, + description, + quantity, + unitPrice, + taxRate, + taxAmount, + totalLineAmount, + remark: remark ?? null, + }) + .returning({ id: contractItems.id }) + + console.log( + ` -> 계약 품목 생성 완료 #${insertedContractItem.id} (itemId=${itemId})` + ) + } + } + + console.log("시딩 작업이 완료되었습니다.") + process.exit(0) +} + +main().catch((err) => { + console.error(err) + process.exit(1) +})
\ No newline at end of file diff --git a/db/seeds/rfqSeed.ts b/db/seeds/rfqSeed.ts index 2d37393d..a0cebad2 100644 --- a/db/seeds/rfqSeed.ts +++ b/db/seeds/rfqSeed.ts @@ -20,7 +20,7 @@ export async function seedRfqData() { await db.delete(rfqItems); await db.delete(rfqs); - // (벤더 목록이 이미 존재한다고 가정) + // (협력업체 목록이 이미 존재한다고 가정) // 혹은 필요한 경우 vendors도 시드할 수 있음 const existingVendors = await db.select().from(vendors); if (existingVendors.length === 0) { @@ -68,7 +68,7 @@ export async function seedRfqData() { } // 3-2) RFQ Vendors - // - 랜덤으로 2~3개 벤더를 pick + // - 랜덤으로 2~3개 협력업체를 pick const pickedVendors = faker.helpers.arrayElements(existingVendors, faker.number.int({ min: 2, max: 3 })); for (const ven of pickedVendors) { const newRfqVendor = { @@ -78,7 +78,7 @@ export async function seedRfqData() { }; const [insertedRfqVendor] = await db.insert(rfqVendors).values(newRfqVendor).returning(); - // 3-3) 임의로 벤더 Quotes 생성 (50% 확률) + // 3-3) 임의로 협력업체 Quotes 생성 (50% 확률) if (faker.datatype.boolean()) { const newQuote = { rfqId, diff --git a/db/seeds/vendorSeed.ts b/db/seeds/vendorSeed.ts index bb659562..4fca98d8 100644 --- a/db/seeds/vendorSeed.ts +++ b/db/seeds/vendorSeed.ts @@ -38,7 +38,7 @@ async function seedVendors() { const [insertedVendor] = await db.insert(vendors).values(newVendor).returning(); const vendorId = insertedVendor.id; - // 4) Contacts 생성 (각 벤더당 1~3명) + // 4) Contacts 생성 (각 협력업체당 1~3명) const contactCount = faker.number.int({ min: 1, max: 3 }); for (let j = 0; j < contactCount; j++) { const newContact = { @@ -52,7 +52,7 @@ async function seedVendors() { await db.insert(vendorContacts).values(newContact); } - // 5) Possible Items 생성 (각 벤더당 2~5개) + // 5) Possible Items 생성 (각 협력업체당 2~5개) // 여기서 "items" 테이블에서 랜덤 pick → vendorPossibleItems에 참조 const itemCount = faker.number.int({ min: 2, max: 5 }); |
