summaryrefslogtreecommitdiff
path: root/lib/bidding/service.ts
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-09-17 10:40:12 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-09-17 10:40:12 +0000
commit10cb50753ccf318024c4394282f9e8d968dcd1a5 (patch)
treecf4edb96aa172c3b90d88532aff1f536944a2283 /lib/bidding/service.ts
parentf7117370b9cc0c7b96bd1eb23a1b9f5b16cc8ceb (diff)
(최겸) 구매 입찰 오류 수정 및 선적지,하역지 연동,TO Cont, TO PO 개발
Diffstat (limited to 'lib/bidding/service.ts')
-rw-r--r--lib/bidding/service.ts255
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"),
)