diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-09-17 10:40:12 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-09-17 10:40:12 +0000 |
| commit | 10cb50753ccf318024c4394282f9e8d968dcd1a5 (patch) | |
| tree | cf4edb96aa172c3b90d88532aff1f536944a2283 /lib/bidding/service.ts | |
| parent | f7117370b9cc0c7b96bd1eb23a1b9f5b16cc8ceb (diff) | |
(최겸) 구매 입찰 오류 수정 및 선적지,하역지 연동,TO Cont, TO PO 개발
Diffstat (limited to 'lib/bidding/service.ts')
| -rw-r--r-- | lib/bidding/service.ts | 255 |
1 files changed, 104 insertions, 151 deletions
diff --git a/lib/bidding/service.ts b/lib/bidding/service.ts index 55146c4b..89e4f80f 100644 --- a/lib/bidding/service.ts +++ b/lib/bidding/service.ts @@ -13,9 +13,6 @@ import { biddingConditions, users, basicContractTemplates, - paymentTerms, - incoterms, - vendors, vendorsWithTypesView, biddingCompanies } from '@/db/schema' @@ -30,10 +27,11 @@ import { ilike, gte, lte, - SQL, like + SQL, + like, + notInArray } from 'drizzle-orm' import { revalidatePath } from 'next/cache' -import { BiddingListItem } from '@/db/schema' import { filterColumns } from '@/lib/filter-columns' import { CreateBiddingSchema, GetBiddingsSchema, UpdateBiddingSchema } from './validation' import { saveFile } from '../file-stroage' @@ -167,17 +165,17 @@ export async function getBiddings(input: GetBiddingsSchema) { } if (input.submissionDateFrom) { - basicConditions.push(gte(biddingListView.submissionStartDate, input.submissionDateFrom)) + basicConditions.push(gte(biddingListView.submissionStartDate, new Date(input.submissionDateFrom))) } if (input.submissionDateTo) { - basicConditions.push(lte(biddingListView.submissionEndDate, input.submissionDateTo)) + basicConditions.push(lte(biddingListView.submissionEndDate, new Date(input.submissionDateTo))) } if (input.createdAtFrom) { - basicConditions.push(gte(biddingListView.createdAt, input.createdAtFrom)) + basicConditions.push(gte(biddingListView.createdAt, new Date(input.createdAtFrom))) } if (input.createdAtTo) { - basicConditions.push(lte(biddingListView.createdAt, input.createdAtTo)) + basicConditions.push(lte(biddingListView.createdAt, new Date(input.createdAtTo))) } // 가격 범위 필터 @@ -367,118 +365,129 @@ export async function getBiddingMonthlyStats(year: number = new Date().getFullYe } export interface CreateBiddingInput extends CreateBiddingSchema { - // 사양설명회 정보 (선택사항) - specificationMeeting?: { - meetingDate: string - meetingTime: string - location: string - address: string - contactPerson: string - contactPhone: string - contactEmail: string - agenda: string - materials: string - notes: string - isRequired: boolean - meetingFiles: File[] - } | null - - // PR 아이템들 (선택사항) - prItems?: Array<{ - id: string - prNumber: string - itemCode: string - itemInfo: string - quantity: string - quantityUnit: string - totalWeight: string - weightUnit: string - materialDescription: string - hasSpecDocument: boolean - requestedDeliveryDate: string - specFiles: File[] - isRepresentative: boolean - }> - - // 입찰 조건 (선택사항) - biddingConditions?: { - paymentTerms: string - taxConditions: string - incoterms: string - contractDeliveryDate: string - shippingPort: string - destinationPort: string - isPriceAdjustmentApplicable: boolean - sparePartOptions: string - } + // 사양설명회 정보 (선택사항) + specificationMeeting?: { + meetingDate: string + meetingTime: string + location: string + address: string + contactPerson: string + contactPhone: string + contactEmail: string + agenda: string + materials: string + notes: string + isRequired: boolean + meetingFiles: File[] + } | null + + // PR 아이템들 (선택사항) + prItems?: Array<{ + id: string + prNumber: string + itemCode: string + itemInfo: string + quantity: string + quantityUnit: string + totalWeight: string + weightUnit: string + materialDescription: string + hasSpecDocument: boolean + requestedDeliveryDate: string + specFiles: File[] + isRepresentative: boolean + }> + + // 입찰 조건 (선택사항) + biddingConditions?: { + paymentTerms: string + taxConditions: string + incoterms: string + contractDeliveryDate: string + shippingPort: string + destinationPort: string + isPriceAdjustmentApplicable: boolean + sparePartOptions: string } + // 계약 기간 정보 + contractStartDate?: string + contractEndDate?: string +} + export interface UpdateBiddingInput extends UpdateBiddingSchema { id: number } // 자동 입찰번호 생성 -async function generateBiddingNumber(biddingType: string, tx?: any, maxRetries: number = 5): Promise<string> { - const year = new Date().getFullYear() - const typePrefix = { - 'equipment': 'EQ', - 'construction': 'CT', - 'service': 'SV', - 'lease': 'LS', - 'steel_stock': 'SS', - 'piping': 'PP', - 'transport': 'TP', - 'waste': 'WS', - 'sale': 'SL' - }[biddingType] || 'GN' - - const dbInstance = tx || db - const prefix = `${year}${typePrefix}` +async function generateBiddingNumber( + userId?: string, + tx?: any, + maxRetries: number = 5 +): Promise<string> { + // user 테이블의 user.userCode가 있으면 발주담당자 코드로 사용 + // userId가 주어졌을 때 user.userCode를 조회, 없으면 '000' 사용 + let purchaseManagerCode = '000'; + if (userId) { + const user = await db + .select({ userCode: users.userCode }) + .from(users) + .where(eq(users.id, parseInt(userId))) + .limit(1); + if (user[0]?.userCode && user[0].userCode.length >= 3) { + purchaseManagerCode = user[0].userCode.substring(0, 3).toUpperCase(); + } + } + const managerCode = (purchaseManagerCode && purchaseManagerCode.length >= 3) + ? purchaseManagerCode.substring(0, 3).toUpperCase() + : '000'; + + const dbInstance = tx || db; + const prefix = `B${managerCode}`; for (let attempt = 0; attempt < maxRetries; attempt++) { - // 현재 최대 시퀀스 번호 조회 + // 현재 최대 일련번호 조회 const result = await dbInstance .select({ maxNumber: sql<string>`MAX(${biddings.biddingNumber})` }) .from(biddings) - .where(like(biddings.biddingNumber, `${prefix}%`)) + .where(like(biddings.biddingNumber, `${prefix}%`)); - let sequence = 1 + let sequence = 1; if (result[0]?.maxNumber) { - const lastSequence = parseInt(result[0].maxNumber.slice(-4)) + const lastSequence = parseInt(result[0].maxNumber.slice(-5)); if (!isNaN(lastSequence)) { - sequence = lastSequence + 1 + sequence = lastSequence + 1; } } - const biddingNumber = `${prefix}${sequence.toString().padStart(4, '0')}` + const biddingNumber = `${prefix}${sequence.toString().padStart(5, '0')}`; // 중복 확인 const existing = await dbInstance .select({ id: biddings.id }) .from(biddings) .where(eq(biddings.biddingNumber, biddingNumber)) - .limit(1) + .limit(1); if (existing.length === 0) { - return biddingNumber + return biddingNumber; } - // 중복이 발견되면 잠시 대기 후 재시도 - await new Promise(resolve => setTimeout(resolve, 10 + Math.random() * 20)) + // 중복이 발견되면 잠시 대기 후 재시도 (동시성 문제 방지) + await new Promise(resolve => setTimeout(resolve, 10 + Math.random() * 20)); } - throw new Error(`Failed to generate unique bidding number after ${maxRetries} attempts`) + throw new Error(`Failed to generate unique bidding number after ${maxRetries} attempts`); } - // 입찰 생성 export async function createBidding(input: CreateBiddingInput, userId: string) { try { const userName = await getUserNameById(userId) return await db.transaction(async (tx) => { // 자동 입찰번호 생성 - const biddingNumber = await generateBiddingNumber(input.biddingType) + const biddingNumber = await generateBiddingNumber(userId) // 프로젝트 정보 조회 let projectName = input.projectName @@ -541,7 +550,8 @@ export async function createBidding(input: CreateBiddingInput, userId: string) { contractType: input.contractType, biddingType: input.biddingType, awardCount: input.awardCount, - contractPeriod: input.contractPeriod, + contractStartDate: input.contractStartDate ? parseDate(input.contractStartDate) : null, + contractEndDate: input.contractEndDate ? parseDate(input.contractEndDate) : null, // 자동 등록일 설정 biddingRegistrationDate: new Date(), @@ -640,7 +650,7 @@ export async function createBidding(input: CreateBiddingInput, userId: string) { paymentTerms: input.biddingConditions.paymentTerms, taxConditions: input.biddingConditions.taxConditions, incoterms: input.biddingConditions.incoterms, - contractDeliveryDate: input.biddingConditions.contractDeliveryDate ? new Date(input.biddingConditions.contractDeliveryDate) : null, + contractDeliveryDate: input.biddingConditions.contractDeliveryDate || null, shippingPort: input.biddingConditions.shippingPort, destinationPort: input.biddingConditions.destinationPort, isPriceAdjustmentApplicable: input.biddingConditions.isPriceAdjustmentApplicable, @@ -703,7 +713,6 @@ export async function createBidding(input: CreateBiddingInput, userId: string) { isPublic: false, isRequired: false, uploadedBy: userName, - displayOrder: fileIndex + 1, }) } else { console.error(`Failed to save spec file: ${file.name}`, saveResult.error) @@ -796,10 +805,9 @@ export async function updateBidding(input: UpdateBiddingInput, userId: string) { if (input.contractType !== undefined) updateData.contractType = input.contractType if (input.biddingType !== undefined) updateData.biddingType = input.biddingType if (input.awardCount !== undefined) updateData.awardCount = input.awardCount - if (input.contractPeriod !== undefined) updateData.contractPeriod = input.contractPeriod + if (input.contractStartDate !== undefined) updateData.contractStartDate = parseDate(input.contractStartDate) + if (input.contractEndDate !== undefined) updateData.contractEndDate = parseDate(input.contractEndDate) - if (input.preQuoteDate !== undefined) updateData.preQuoteDate = parseDate(input.preQuoteDate) - if (input.biddingRegistrationDate !== undefined) updateData.biddingRegistrationDate = parseDate(input.biddingRegistrationDate) if (input.submissionStartDate !== undefined) updateData.submissionStartDate = parseDate(input.submissionStartDate) if (input.submissionEndDate !== undefined) updateData.submissionEndDate = parseDate(input.submissionEndDate) if (input.evaluationDate !== undefined) updateData.evaluationDate = parseDate(input.evaluationDate) @@ -1039,15 +1047,15 @@ export async function getSpecificationMeetingDetailsAction( biddingId: meetingData.biddingId, meetingDate: meetingData.meetingDate?.toISOString() || '', meetingTime: meetingData.meetingTime, - location: meetingData.location, + location: meetingData.location || '', address: meetingData.address, - contactPerson: meetingData.contactPerson, + contactPerson: meetingData.contactPerson || '', contactPhone: meetingData.contactPhone, contactEmail: meetingData.contactEmail, agenda: meetingData.agenda, materials: meetingData.materials, notes: meetingData.notes, - isRequired: meetingData.isRequired, + isRequired: meetingData.isRequired || false, createdAt: meetingData.createdAt?.toISOString() || '', updatedAt: meetingData.updatedAt?.toISOString() || '', documents: documents.map(doc => ({ @@ -1183,7 +1191,7 @@ export async function getPRDetailsAction( createdAt: doc.createdAt?.toISOString() || '', updatedAt: doc.updatedAt?.toISOString() || '', })), - items: itemsWithDocs + items: itemsWithDocs as any } return { @@ -1200,8 +1208,6 @@ export async function getPRDetailsAction( } } - - /** * 입찰 기본 정보 조회 서버 액션 (선택사항) */ @@ -1241,7 +1247,7 @@ export async function getBiddingBasicInfoAction( return { success: true, - data: bidding + data: bidding as any } } catch (error) { @@ -1306,7 +1312,7 @@ export async function updateBiddingConditions( paymentTerms: updates.paymentTerms, taxConditions: updates.taxConditions, incoterms: updates.incoterms, - contractDeliveryDate: updates.contractDeliveryDate ? new Date(updates.contractDeliveryDate) : null, + contractDeliveryDate: updates.contractDeliveryDate || null, shippingPort: updates.shippingPort, destinationPort: updates.destinationPort, isPriceAdjustmentApplicable: updates.isPriceAdjustmentApplicable, @@ -1324,7 +1330,7 @@ export async function updateBiddingConditions( // 새로 생성 await tx.insert(biddingConditions).values({ biddingId, - ...updateData, + ...updateData as any, }) } @@ -1346,59 +1352,6 @@ export async function updateBiddingConditions( } // 활성 템플릿 조회 서버 액션 -// 입찰 조건 옵션 관련 서버 액션들 -export async function getActivePaymentTerms() { - try { - const result = await db - .select({ - code: paymentTerms.code, - description: paymentTerms.description, - isActive: paymentTerms.isActive, - createdAt: paymentTerms.createdAt, - }) - .from(paymentTerms) - .where(eq(paymentTerms.isActive, true)) - .orderBy(paymentTerms.createdAt) - - return { - success: true, - data: result - } - } catch (error) { - console.error('Error fetching active payment terms:', error) - return { - success: false, - error: '지급조건 조회 중 오류가 발생했습니다.' - } - } -} - -export async function getActiveIncoterms() { - try { - const result = await db - .select({ - code: incoterms.code, - description: incoterms.description, - isActive: incoterms.isActive, - createdAt: incoterms.createdAt, - }) - .from(incoterms) - .where(eq(incoterms.isActive, true)) - .orderBy(incoterms.createdAt) - - return { - success: true, - data: result - } - } catch (error) { - console.error('Error fetching active incoterms:', error) - return { - success: false, - error: '운송조건 조회 중 오류가 발생했습니다.' - } - } -} - export async function getActiveContractTemplates() { try { // 활성 상태의 템플릿들 조회 @@ -1461,7 +1414,7 @@ export async function searchVendorsForBidding(searchTerm: string = "", biddingId and( whereCondition, // 이미 참여중인 벤더 제외 - excludedIds.length > 0 ? sql`${vendorsWithTypesView.id} NOT IN (${excludedIds})` : undefined, + excludedIds.length > 0 ? notInArray(vendorsWithTypesView.id, excludedIds) : undefined, // ACTIVE 상태인 벤더만 검색 // eq(vendorsWithTypesView.status, "ACTIVE"), ) |
