summaryrefslogtreecommitdiff
path: root/lib/bidding/service.ts
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-08-11 09:02:00 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-08-11 09:02:00 +0000
commitcbb4c7fe0b94459162ad5e998bc05cd293e0ff96 (patch)
tree0a26712f7685e4f6511e637b9a81269d90a47c8f /lib/bidding/service.ts
parenteb654f88214095f71be142b989e620fd28db3f69 (diff)
(대표님) 입찰, EDP 변경사항 대응, spreadJS 오류 수정, 벤더실사 수정
Diffstat (limited to 'lib/bidding/service.ts')
-rw-r--r--lib/bidding/service.ts815
1 files changed, 815 insertions, 0 deletions
diff --git a/lib/bidding/service.ts b/lib/bidding/service.ts
new file mode 100644
index 00000000..91fea75e
--- /dev/null
+++ b/lib/bidding/service.ts
@@ -0,0 +1,815 @@
+'use server'
+
+import db from '@/db/db'
+import {
+ biddings,
+ biddingListView,
+ biddingNoticeTemplate,
+ projects,
+ biddingDocuments,
+ prItemsForBidding,
+ specificationMeetings
+} from '@/db/schema'
+import {
+ eq,
+ desc,
+ asc,
+ and,
+ or,
+ count,
+ sql,
+ ilike,
+ gte,
+ lte,
+ SQL
+} 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'
+
+
+export async function getBiddingNoticeTemplate() {
+ try {
+ const result = await db
+ .select()
+ .from(biddingNoticeTemplate)
+ .where(eq(biddingNoticeTemplate.type, 'standard'))
+ .limit(1)
+
+ return result[0] || null
+ } catch (error) {
+ console.error('Failed to get bidding notice template:', error)
+ throw new Error('입찰공고문 템플릿을 불러오는데 실패했습니다.')
+ }
+}
+
+export async function saveBiddingNoticeTemplate(formData: {
+ title: string
+ content: string
+}) {
+ try {
+ const { title, content } = formData
+
+ // 기존 템플릿 확인
+ const existing = await db
+ .select()
+ .from(biddingNoticeTemplate)
+ .where(eq(biddingNoticeTemplate.type, 'standard'))
+ .limit(1)
+
+ if (existing.length > 0) {
+ // 업데이트
+ await db
+ .update(biddingNoticeTemplate)
+ .set({
+ title,
+ content,
+ updatedAt: new Date(),
+ })
+ .where(eq(biddingNoticeTemplate.type, 'standard'))
+ } else {
+ // 새로 생성
+ await db.insert(biddingNoticeTemplate).values({
+ type: 'standard',
+ title,
+ content,
+ })
+ }
+
+ revalidatePath('/admin/bidding-notice')
+ return { success: true, message: '입찰공고문 템플릿이 저장되었습니다.' }
+ } catch (error) {
+ console.error('Failed to save bidding notice template:', error)
+ throw new Error('입찰공고문 템플릿 저장에 실패했습니다.')
+ }
+}
+
+
+export async function getBiddings(input: GetBiddingsSchema) {
+ try {
+ const offset = (input.page - 1) * input.perPage
+
+ // ✅ 1) 고급 필터 조건
+ let advancedWhere: SQL<unknown> | undefined = undefined
+ if (input.filters && input.filters.length > 0) {
+ advancedWhere = filterColumns({
+ table: biddingListView,
+ filters: input.filters,
+ joinOperator: input.joinOperator || 'and',
+ })
+ }
+
+ // ✅ 2) 기본 필터 조건들
+ const basicConditions: SQL<unknown>[] = []
+
+ if (input.biddingNumber) {
+ basicConditions.push(ilike(biddingListView.biddingNumber, `%${input.biddingNumber}%`))
+ }
+
+ if (input.status && input.status.length > 0) {
+ basicConditions.push(
+ or(...input.status.map(status => eq(biddingListView.status, status)))!
+ )
+ }
+
+ if (input.biddingType && input.biddingType.length > 0) {
+ basicConditions.push(
+ or(...input.biddingType.map(type => eq(biddingListView.biddingType, type)))!
+ )
+ }
+
+ if (input.contractType && input.contractType.length > 0) {
+ basicConditions.push(
+ or(...input.contractType.map(type => eq(biddingListView.contractType, type)))!
+ )
+ }
+
+ if (input.managerName) {
+ basicConditions.push(ilike(biddingListView.managerName, `%${input.managerName}%`))
+ }
+
+ // 날짜 필터들
+ if (input.preQuoteDateFrom) {
+ basicConditions.push(gte(biddingListView.preQuoteDate, input.preQuoteDateFrom))
+ }
+ if (input.preQuoteDateTo) {
+ basicConditions.push(lte(biddingListView.preQuoteDate, input.preQuoteDateTo))
+ }
+
+ if (input.submissionDateFrom) {
+ basicConditions.push(gte(biddingListView.submissionStartDate, input.submissionDateFrom))
+ }
+ if (input.submissionDateTo) {
+ basicConditions.push(lte(biddingListView.submissionEndDate, input.submissionDateTo))
+ }
+
+ if (input.createdAtFrom) {
+ basicConditions.push(gte(biddingListView.createdAt, input.createdAtFrom))
+ }
+ if (input.createdAtTo) {
+ basicConditions.push(lte(biddingListView.createdAt, input.createdAtTo))
+ }
+
+ // 가격 범위 필터
+ if (input.budgetMin) {
+ basicConditions.push(gte(biddingListView.budget, input.budgetMin))
+ }
+ if (input.budgetMax) {
+ basicConditions.push(lte(biddingListView.budget, input.budgetMax))
+ }
+
+ // Boolean 필터
+ if (input.hasSpecificationMeeting === "true") {
+ basicConditions.push(eq(biddingListView.hasSpecificationMeeting, true))
+ } else if (input.hasSpecificationMeeting === "false") {
+ basicConditions.push(eq(biddingListView.hasSpecificationMeeting, false))
+ }
+
+ if (input.hasPrDocument === "true") {
+ basicConditions.push(eq(biddingListView.hasPrDocument, true))
+ } else if (input.hasPrDocument === "false") {
+ basicConditions.push(eq(biddingListView.hasPrDocument, false))
+ }
+
+ const basicWhere = basicConditions.length > 0 ? and(...basicConditions) : undefined
+
+ // ✅ 3) 글로벌 검색 조건
+ let globalWhere: SQL<unknown> | undefined = undefined
+ if (input.search) {
+ const s = `%${input.search}%`
+ const searchConditions = [
+ ilike(biddingListView.biddingNumber, s),
+ ilike(biddingListView.title, s),
+ ilike(biddingListView.projectName, s),
+ ilike(biddingListView.itemName, s),
+ ilike(biddingListView.managerName, s),
+ ilike(biddingListView.prNumber, s),
+ ilike(biddingListView.remarks, s),
+ ]
+ globalWhere = or(...searchConditions)
+ }
+
+ // ✅ 4) 최종 WHERE 조건
+ const whereConditions: SQL<unknown>[] = []
+ if (advancedWhere) whereConditions.push(advancedWhere)
+ if (basicWhere) whereConditions.push(basicWhere)
+ if (globalWhere) whereConditions.push(globalWhere)
+
+ const finalWhere = whereConditions.length > 0 ? and(...whereConditions) : undefined
+
+ // ✅ 5) 전체 개수 조회
+ const totalResult = await db
+ .select({ count: count() })
+ .from(biddingListView)
+ .where(finalWhere)
+
+ const total = totalResult[0]?.count || 0
+
+ if (total === 0) {
+ return { data: [], pageCount: 0, total: 0 }
+ }
+
+ console.log("Total biddings:", total)
+
+ // ✅ 6) 정렬 및 페이징
+ const orderByColumns = input.sort.map((sort) => {
+ const column = sort.id as keyof typeof biddingListView.$inferSelect
+ return sort.desc ? desc(biddingListView[column]) : asc(biddingListView[column])
+ })
+
+ if (orderByColumns.length === 0) {
+ orderByColumns.push(desc(biddingListView.createdAt))
+ }
+
+ // ✅ 7) 메인 쿼리 - 매우 간단해짐!
+ const data = await db
+ .select()
+ .from(biddingListView)
+ .where(finalWhere)
+ .orderBy(...orderByColumns)
+ .limit(input.perPage)
+ .offset(offset)
+
+ const pageCount = Math.ceil(total / input.perPage)
+
+ // ✅ 8) 포맷팅 불필요 - 뷰에서 이미 완성된 데이터!
+ return { data, pageCount, total }
+
+ } catch (err) {
+ console.error("Error in getBiddings:", err)
+ return { data: [], pageCount: 0, total: 0 }
+ }
+}
+// 상태별 개수 집계
+export async function getBiddingStatusCounts() {
+ try {
+ const counts = await db
+ .select({
+ status: biddings.status,
+ count: count(),
+ })
+ .from(biddings)
+ .groupBy(biddings.status)
+
+ return counts.reduce((acc, { status, count }) => {
+ acc[status] = count
+ return acc
+ }, {} as Record<string, number>)
+ } catch (error) {
+ console.error('Failed to get bidding status counts:', error)
+ return {}
+ }
+}
+
+// 입찰유형별 개수 집계
+export async function getBiddingTypeCounts() {
+ try {
+ const counts = await db
+ .select({
+ biddingType: biddings.biddingType,
+ count: count(),
+ })
+ .from(biddings)
+ .groupBy(biddings.biddingType)
+
+ return counts.reduce((acc, { biddingType, count }) => {
+ acc[biddingType] = count
+ return acc
+ }, {} as Record<string, number>)
+ } catch (error) {
+ console.error('Failed to get bidding type counts:', error)
+ return {}
+ }
+}
+
+// 담당자별 개수 집계
+export async function getBiddingManagerCounts() {
+ try {
+ const counts = await db
+ .select({
+ managerName: biddings.managerName,
+ count: count(),
+ })
+ .from(biddings)
+ .where(sql`${biddings.managerName} IS NOT NULL AND ${biddings.managerName} != ''`)
+ .groupBy(biddings.managerName)
+
+ return counts.reduce((acc, { managerName, count }) => {
+ if (managerName) {
+ acc[managerName] = count
+ }
+ return acc
+ }, {} as Record<string, number>)
+ } catch (error) {
+ console.error('Failed to get bidding manager counts:', error)
+ return {}
+ }
+}
+
+// 월별 입찰 생성 통계
+export async function getBiddingMonthlyStats(year: number = new Date().getFullYear()) {
+ try {
+ const stats = await db
+ .select({
+ month: sql<number>`EXTRACT(MONTH FROM ${biddings.createdAt})`.as('month'),
+ count: count(),
+ })
+ .from(biddings)
+ .where(sql`EXTRACT(YEAR FROM ${biddings.createdAt}) = ${year}`)
+ .groupBy(sql`EXTRACT(MONTH FROM ${biddings.createdAt})`)
+ .orderBy(sql`EXTRACT(MONTH FROM ${biddings.createdAt})`)
+
+ // 1-12월 전체 배열 생성 (없는 월은 0으로)
+ const monthlyData = Array.from({ length: 12 }, (_, i) => {
+ const month = i + 1
+ const found = stats.find(stat => stat.month === month)
+ return {
+ month,
+ count: found?.count || 0,
+ }
+ })
+
+ return monthlyData
+ } catch (error) {
+ console.error('Failed to get bidding monthly stats:', error)
+ return []
+ }
+}
+
+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
+ } | null
+
+ // PR 아이템들 (선택사항)
+ prItemsForBidding?: Array<{
+ itemNumber: string
+ projectInfo: string
+ itemInfo: string
+ shi: string
+ requestedDeliveryDate: string
+ annualUnitPrice: string
+ currency: string
+ quantity: string
+ quantityUnit: string
+ totalWeight: string
+ weightUnit: string
+ materialDescription: string
+ prNumber: string
+ specFiles: File[]
+ }>
+ }
+
+export interface UpdateBiddingInput extends UpdateBiddingSchema {
+ id: number
+}
+
+// 자동 입찰번호 생성
+async function generateBiddingNumber(biddingType: string): 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 lastBidding = await db
+ .select({ biddingNumber: biddings.biddingNumber })
+ .from(biddings)
+ .where(eq(biddings.biddingNumber, `${year}${typePrefix}%`))
+ .orderBy(biddings.biddingNumber)
+ .limit(1)
+
+ let sequence = 1
+ if (lastBidding.length > 0) {
+ const lastNumber = lastBidding[0].biddingNumber
+ const lastSequence = parseInt(lastNumber.slice(-4))
+ sequence = lastSequence + 1
+ }
+
+ return `${year}${typePrefix}${sequence.toString().padStart(4, '0')}`
+}
+
+// 입찰 생성
+export async function createBidding(input: CreateBiddingInput, userId: string) {
+ try {
+ return await db.transaction(async (tx) => {
+ // 자동 입찰번호 생성
+ const biddingNumber = await generateBiddingNumber(input.biddingType)
+
+ // 프로젝트 정보 조회
+ let projectName = input.projectName
+ if (input.projectId && !projectName) {
+ const project = await tx
+ .select({ code: projects.code, name: projects.name })
+ .from(projects)
+ .where(eq(projects.id, input.projectId))
+ .limit(1)
+
+ if (project.length > 0) {
+ projectName = `${project[0].code} (${project[0].name})`
+ }
+ }
+
+ // 표준 공고문 템플릿 가져오기
+ let standardContent = ''
+ if (!input.content) {
+ try {
+ const template = await tx
+ .select({ content: biddingNoticeTemplate.content })
+ .from(biddingNoticeTemplate)
+ .where(eq(biddingNoticeTemplate.type, 'standard'))
+ .limit(1)
+
+ if (template.length > 0) {
+ standardContent = template[0].content
+ }
+ } catch (error) {
+ console.warn('Failed to load standard template:', error)
+ }
+ }
+
+ // 날짜 변환 함수
+ const parseDate = (dateStr?: string) => {
+ if (!dateStr) return null
+ try {
+ return new Date(dateStr)
+ } catch {
+ return null
+ }
+ }
+
+ // 1. 입찰 생성
+ const [newBidding] = await tx
+ .insert(biddings)
+ .values({
+ biddingNumber,
+ revision: input.revision || 0,
+
+ // 프로젝트 정보
+ projectId: input.projectId,
+ projectName,
+
+ itemName: input.itemName,
+ title: input.title,
+ description: input.description,
+ content: input.content || standardContent,
+
+ contractType: input.contractType,
+ biddingType: input.biddingType,
+ awardCount: input.awardCount,
+ contractPeriod: input.contractPeriod,
+
+ // 자동 등록일 설정
+ biddingRegistrationDate: new Date(),
+ submissionStartDate: parseDate(input.submissionStartDate),
+ submissionEndDate: parseDate(input.submissionEndDate),
+ evaluationDate: parseDate(input.evaluationDate),
+
+ hasSpecificationMeeting: input.hasSpecificationMeeting || false,
+ hasPrDocument: input.hasPrDocument || false,
+ prNumber: input.prNumber,
+
+ currency: input.currency,
+ budget: input.budget ? parseFloat(input.budget) : null,
+ targetPrice: input.targetPrice ? parseFloat(input.targetPrice) : null,
+ finalBidPrice: input.finalBidPrice ? parseFloat(input.finalBidPrice) : null,
+
+ status: input.status || 'bidding_generated',
+ isPublic: input.isPublic || false,
+ managerName: input.managerName,
+ managerEmail: input.managerEmail,
+ managerPhone: input.managerPhone,
+
+ remarks: input.remarks,
+ createdBy: userId,
+ updatedBy: userId,
+ })
+ .returning({ id: biddings.id })
+
+ const biddingId = newBidding.id
+
+ // 2. 사양설명회 정보 저장 (있는 경우)
+ if (input.specificationMeeting) {
+ const [newSpecMeeting] = await tx
+ .insert(specificationMeetings)
+ .values({
+ biddingId,
+ meetingDate: new Date(input.specificationMeeting.meetingDate),
+ meetingTime: input.specificationMeeting.meetingTime,
+ location: input.specificationMeeting.location,
+ address: input.specificationMeeting.address,
+ contactPerson: input.specificationMeeting.contactPerson,
+ contactPhone: input.specificationMeeting.contactPhone,
+ contactEmail: input.specificationMeeting.contactEmail,
+ agenda: input.specificationMeeting.agenda,
+ materials: input.specificationMeeting.materials,
+ notes: input.specificationMeeting.notes,
+ isRequired: input.specificationMeeting.isRequired,
+ })
+ .returning({ id: specificationMeetings.id })
+
+ // 2-1. 사양설명회 첨부파일 저장
+ if (input.specificationMeeting.meetingFiles && input.specificationMeeting.meetingFiles.length > 0) {
+ for (const file of input.specificationMeeting.meetingFiles) {
+ try {
+ const saveResult = await saveFile({
+ file,
+ directory: `biddings/${biddingId}/specification-meeting`,
+ originalName: file.name,
+ userId
+ })
+
+ if (saveResult.success) {
+ await tx.insert(biddingDocuments).values({
+ biddingId,
+ specificationMeetingId: newSpecMeeting.id,
+ documentType: 'specification_meeting',
+ fileName: saveResult.fileName!,
+ originalFileName: saveResult.originalName!,
+ fileSize: saveResult.fileSize!,
+ mimeType: file.type,
+ filePath: saveResult.filePath!,
+ publicPath: saveResult.publicPath,
+ title: `사양설명회 - ${file.name}`,
+ isPublic: false,
+ isRequired: false,
+ uploadedBy: userId,
+ })
+ } else {
+ console.error(`Failed to save specification meeting file: ${file.name}`, saveResult.error)
+ // 파일 저장 실패해도 전체 트랜잭션은 계속 진행
+ }
+ } catch (error) {
+ console.error(`Error saving specification meeting file: ${file.name}`, error)
+ }
+ }
+ }
+ }
+
+ // 3. PR 아이템들 저장 (있는 경우)
+ if (input.prItems && input.prItems.length > 0) {
+ for (const prItem of input.prItems) {
+ // PR 아이템 저장
+ const [newPrItem] = await tx.insert(prItemsForBidding).values({
+ biddingId,
+ itemNumber: prItem.itemCode, // itemCode를 itemNumber로 매핑
+ projectInfo: '', // 필요시 추가
+ itemInfo: prItem.itemInfo,
+ shi: '', // 필요시 추가
+ requestedDeliveryDate: prItem.requestedDeliveryDate ? new Date(prItem.requestedDeliveryDate) : null,
+ annualUnitPrice: null, // 필요시 추가
+ currency: 'KRW', // 기본값 또는 입력받은 값
+ quantity: prItem.quantity ? parseFloat(prItem.quantity) : null,
+ quantityUnit: prItem.quantityUnit as any, // enum 타입에 맞게
+ totalWeight: null, // 필요시 추가
+ weightUnit: null, // 필요시 추가
+ materialDescription: '', // 필요시 추가
+ prNumber: prItem.prNumber,
+ hasSpecDocument: prItem.specFiles.length > 0,
+ isRepresentative: prItem.isRepresentative,
+ }).returning({ id: prItemsForBidding.id })
+
+ // 3-1. 스펙 파일들 저장 (있는 경우)
+ if (prItem.specFiles.length > 0) {
+ for (let fileIndex = 0; fileIndex < prItem.specFiles.length; fileIndex++) {
+ const file = prItem.specFiles[fileIndex]
+ try {
+ const saveResult = await saveFile({
+ file,
+ directory: `biddings/${biddingId}/pr-items/${newPrItem.id}/specs`,
+ originalName: file.name,
+ userId
+ })
+
+ if (saveResult.success) {
+ await tx.insert(biddingDocuments).values({
+ biddingId,
+ prItemId: newPrItem.id,
+ documentType: 'spec',
+ fileName: saveResult.fileName!,
+ originalFileName: saveResult.originalName!,
+ fileSize: saveResult.fileSize!,
+ mimeType: file.type,
+ filePath: saveResult.filePath!,
+ publicPath: saveResult.publicPath,
+ title: `${prItem.itemInfo || prItem.itemCode} 스펙 - ${file.name}`,
+ description: `PR ${prItem.prNumber}의 스펙 문서`,
+ isPublic: false,
+ isRequired: false,
+ uploadedBy: userId,
+ displayOrder: fileIndex + 1,
+ })
+ } else {
+ console.error(`Failed to save spec file: ${file.name}`, saveResult.error)
+ // 파일 저장 실패해도 전체 트랜잭션은 계속 진행
+ }
+ } catch (error) {
+ console.error(`Error saving spec file: ${file.name}`, error)
+ }
+ }
+ }
+ }
+ }
+
+ // 캐시 무효화
+ revalidatePath('/evcp/bid')
+
+ return {
+ success: true,
+ message: '입찰이 성공적으로 생성되었습니다.',
+ data: { id: biddingId, biddingNumber }
+ }
+ })
+ } catch (error) {
+ console.error('Error creating bidding:', error)
+ return {
+ success: false,
+ error: error instanceof Error ? error.message : '입찰 생성 중 오류가 발생했습니다.'
+ }
+ }
+ }
+// 입찰 수정
+export async function updateBidding(input: UpdateBiddingInput, userId: string) {
+ try {
+ // 존재 여부 확인
+ const existing = await db
+ .select({ id: biddings.id })
+ .from(biddings)
+ .where(eq(biddings.id, input.id))
+ .limit(1)
+
+ if (existing.length === 0) {
+ return {
+ success: false,
+ error: '존재하지 않는 입찰입니다.'
+ }
+ }
+
+ // 입찰번호 중복 체크 (다른 레코드에서)
+ if (input.biddingNumber) {
+ const duplicate = await db
+ .select({ id: biddings.id })
+ .from(biddings)
+ .where(eq(biddings.biddingNumber, input.biddingNumber))
+ .limit(1)
+
+ if (duplicate.length > 0 && duplicate[0].id !== input.id) {
+ return {
+ success: false,
+ error: '이미 존재하는 입찰번호입니다.'
+ }
+ }
+ }
+
+ // 날짜 문자열을 Date 객체로 변환
+ const parseDate = (dateStr?: string) => {
+ if (!dateStr) return undefined
+ try {
+ return new Date(dateStr)
+ } catch {
+ return undefined
+ }
+ }
+
+ // 업데이트할 데이터 준비
+ const updateData: any = {
+ updatedAt: new Date(),
+ updatedBy: userId,
+ }
+
+ // 정의된 필드들만 업데이트
+ if (input.biddingNumber !== undefined) updateData.biddingNumber = input.biddingNumber
+ if (input.revision !== undefined) updateData.revision = input.revision
+ if (input.projectName !== undefined) updateData.projectName = input.projectName
+ if (input.itemName !== undefined) updateData.itemName = input.itemName
+ if (input.title !== undefined) updateData.title = input.title
+ if (input.description !== undefined) updateData.description = input.description
+ if (input.content !== undefined) updateData.content = input.content
+
+ 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.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)
+
+ if (input.hasSpecificationMeeting !== undefined) updateData.hasSpecificationMeeting = input.hasSpecificationMeeting
+ if (input.hasPrDocument !== undefined) updateData.hasPrDocument = input.hasPrDocument
+ if (input.prNumber !== undefined) updateData.prNumber = input.prNumber
+
+ if (input.currency !== undefined) updateData.currency = input.currency
+ if (input.budget !== undefined) updateData.budget = input.budget ? parseFloat(input.budget) : null
+ if (input.targetPrice !== undefined) updateData.targetPrice = input.targetPrice ? parseFloat(input.targetPrice) : null
+ if (input.finalBidPrice !== undefined) updateData.finalBidPrice = input.finalBidPrice ? parseFloat(input.finalBidPrice) : null
+
+ if (input.status !== undefined) updateData.status = input.status
+ if (input.isPublic !== undefined) updateData.isPublic = input.isPublic
+ if (input.managerName !== undefined) updateData.managerName = input.managerName
+ if (input.managerEmail !== undefined) updateData.managerEmail = input.managerEmail
+ if (input.managerPhone !== undefined) updateData.managerPhone = input.managerPhone
+
+ if (input.remarks !== undefined) updateData.remarks = input.remarks
+
+ // 입찰 수정
+ await db
+ .update(biddings)
+ .set(updateData)
+ .where(eq(biddings.id, input.id))
+
+ revalidatePath('/admin/biddings')
+ revalidatePath(`/admin/biddings/${input.id}`)
+
+ return {
+ success: true,
+ message: '입찰이 성공적으로 수정되었습니다.'
+ }
+
+ } catch (error) {
+ console.error('Error updating bidding:', error)
+ return {
+ success: false,
+ error: '입찰 수정 중 오류가 발생했습니다.'
+ }
+ }
+}
+
+// 입찰 삭제
+export async function deleteBidding(id: number) {
+ try {
+ const existing = await db
+ .select({ id: biddings.id })
+ .from(biddings)
+ .where(eq(biddings.id, id))
+ .limit(1)
+
+ if (existing.length === 0) {
+ return {
+ success: false,
+ error: '존재하지 않는 입찰입니다.'
+ }
+ }
+
+ await db
+ .delete(biddings)
+ .where(eq(biddings.id, id))
+
+ revalidatePath('/admin/biddings')
+
+ return {
+ success: true,
+ message: '입찰이 성공적으로 삭제되었습니다.'
+ }
+
+ } catch (error) {
+ console.error('Error deleting bidding:', error)
+ return {
+ success: false,
+ error: '입찰 삭제 중 오류가 발생했습니다.'
+ }
+ }
+}
+
+// 단일 입찰 조회
+export async function getBiddingById(id: number) {
+ try {
+ const bidding = await db
+ .select()
+ .from(biddings)
+ .where(eq(biddings.id, id))
+ .limit(1)
+
+ if (bidding.length === 0) {
+ return null
+ }
+
+ return bidding[0]
+ } catch (error) {
+ console.error('Error getting bidding:', error)
+ return null
+ }
+}