summaryrefslogtreecommitdiff
path: root/lib/bidding/service.ts
diff options
context:
space:
mode:
Diffstat (limited to 'lib/bidding/service.ts')
-rw-r--r--lib/bidding/service.ts424
1 files changed, 401 insertions, 23 deletions
diff --git a/lib/bidding/service.ts b/lib/bidding/service.ts
index 91fea75e..5d384476 100644
--- a/lib/bidding/service.ts
+++ b/lib/bidding/service.ts
@@ -8,7 +8,8 @@ import {
projects,
biddingDocuments,
prItemsForBidding,
- specificationMeetings
+ specificationMeetings,
+ prDocuments
} from '@/db/schema'
import {
eq,
@@ -21,7 +22,7 @@ import {
ilike,
gte,
lte,
- SQL
+ SQL, like
} from 'drizzle-orm'
import { revalidatePath } from 'next/cache'
import { BiddingListItem } from '@/db/schema'
@@ -91,6 +92,9 @@ export async function getBiddings(input: GetBiddingsSchema) {
try {
const offset = (input.page - 1) * input.perPage
+ console.log(input.filters)
+ console.log(input.sort)
+
// ✅ 1) 고급 필터 조건
let advancedWhere: SQL<unknown> | undefined = undefined
if (input.filters && input.filters.length > 0) {
@@ -378,7 +382,7 @@ export interface UpdateBiddingInput extends UpdateBiddingSchema {
}
// 자동 입찰번호 생성
-async function generateBiddingNumber(biddingType: string): Promise<string> {
+async function generateBiddingNumber(biddingType: string, tx?: any, maxRetries: number = 5): Promise<string> {
const year = new Date().getFullYear()
const typePrefix = {
'equipment': 'EQ',
@@ -392,22 +396,44 @@ async function generateBiddingNumber(biddingType: string): Promise<string> {
'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
+ const dbInstance = tx || db
+ const prefix = `${year}${typePrefix}`
+
+ 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}%`))
+
+ let sequence = 1
+ if (result[0]?.maxNumber) {
+ const lastSequence = parseInt(result[0].maxNumber.slice(-4))
+ if (!isNaN(lastSequence)) {
+ sequence = lastSequence + 1
+ }
+ }
+
+ const biddingNumber = `${prefix}${sequence.toString().padStart(4, '0')}`
+
+ // 중복 확인
+ const existing = await dbInstance
+ .select({ id: biddings.id })
+ .from(biddings)
+ .where(eq(biddings.biddingNumber, biddingNumber))
+ .limit(1)
+
+ if (existing.length === 0) {
+ return biddingNumber
+ }
+
+ // 중복이 발견되면 잠시 대기 후 재시도
+ await new Promise(resolve => setTimeout(resolve, 10 + Math.random() * 20))
}
- return `${year}${typePrefix}${sequence.toString().padStart(4, '0')}`
+ throw new Error(`Failed to generate unique bidding number after ${maxRetries} attempts`)
}
// 입찰 생성
@@ -419,7 +445,7 @@ export async function createBidding(input: CreateBiddingInput, userId: string) {
// 프로젝트 정보 조회
let projectName = input.projectName
- if (input.projectId && !projectName) {
+ if (input.projectId) {
const project = await tx
.select({ code: projects.code, name: projects.name })
.from(projects)
@@ -549,8 +575,8 @@ export async function createBidding(input: CreateBiddingInput, userId: string) {
originalFileName: saveResult.originalName!,
fileSize: saveResult.fileSize!,
mimeType: file.type,
- filePath: saveResult.filePath!,
- publicPath: saveResult.publicPath,
+ filePath: saveResult.publicPath!,
+ // publicPath: saveResult.publicPath,
title: `사양설명회 - ${file.name}`,
isPublic: false,
isRequired: false,
@@ -606,13 +632,13 @@ export async function createBidding(input: CreateBiddingInput, userId: string) {
await tx.insert(biddingDocuments).values({
biddingId,
prItemId: newPrItem.id,
- documentType: 'spec',
+ documentType: 'spec_document',
fileName: saveResult.fileName!,
originalFileName: saveResult.originalName!,
fileSize: saveResult.fileSize!,
mimeType: file.type,
- filePath: saveResult.filePath!,
- publicPath: saveResult.publicPath,
+ filePath: saveResult.publicPath!,
+ // publicPath: saveResult.publicPath,
title: `${prItem.itemInfo || prItem.itemCode} 스펙 - ${file.name}`,
description: `PR ${prItem.prNumber}의 스펙 문서`,
isPublic: false,
@@ -813,3 +839,355 @@ export async function getBiddingById(id: number) {
return null
}
}
+
+// 공통 결과 타입
+interface ActionResult<T> {
+ success: boolean
+ data?: T
+ error?: string
+}
+
+// 사양설명회 상세 정보 타입
+export interface SpecificationMeetingDetails {
+ id: number
+ biddingId: number
+ meetingDate: string
+ meetingTime?: string | null
+ location: string
+ address?: string | null
+ contactPerson: string
+ contactPhone?: string | null
+ contactEmail?: string | null
+ agenda?: string | null
+ materials?: string | null
+ notes?: string | null
+ isRequired: boolean
+ createdAt: string
+ updatedAt: string
+ documents: Array<{
+ id: number
+ fileName: string
+ originalFileName: string
+ fileSize: number
+ filePath: string
+ title?: string | null
+ uploadedAt: string
+ uploadedBy?: string | null
+ }>
+}
+
+// PR 상세 정보 타입
+export interface PRDetails {
+ documents: Array<{
+ id: number
+ documentName: string
+ fileName: string
+ originalFileName: string
+ fileSize: number
+ filePath: string
+ registeredAt: string
+ registeredBy: string
+ version?: string | null
+ description?: string | null
+ createdAt: string
+ updatedAt: string
+ }>
+ items: Array<{
+ id: number
+ itemNumber?: string | null
+ itemInfo: string
+ quantity?: number | null
+ quantityUnit?: string | null
+ requestedDeliveryDate?: string | null
+ prNumber?: string | null
+ annualUnitPrice?: number | null
+ currency: string
+ totalWeight?: number | null
+ weightUnit?: string | null
+ materialDescription?: string | null
+ hasSpecDocument: boolean
+ createdAt: string
+ updatedAt: string
+ specDocuments: Array<{
+ id: number
+ fileName: string
+ originalFileName: string
+ fileSize: number
+ filePath: string
+ uploadedAt: string
+ title?: string | null
+ }>
+ }>
+}
+
+/**
+ * 사양설명회 상세 정보 조회 서버 액션
+ */
+export async function getSpecificationMeetingDetailsAction(
+ biddingId: number
+): Promise<ActionResult<SpecificationMeetingDetails>> {
+ try {
+ // 1. 입력 검증
+ if (!biddingId || isNaN(biddingId) || biddingId <= 0) {
+ return {
+ success: false,
+ error: "유효하지 않은 입찰 ID입니다"
+ }
+ }
+
+ // 2. 사양설명회 기본 정보 조회
+ const meeting = await db
+ .select()
+ .from(specificationMeetings)
+ .where(eq(specificationMeetings.biddingId, biddingId))
+ .limit(1)
+
+ if (meeting.length === 0) {
+ return {
+ success: false,
+ error: "사양설명회 정보를 찾을 수 없습니다"
+ }
+ }
+
+ const meetingData = meeting[0]
+
+ // 3. 관련 문서들 조회
+ const documents = await db
+ .select({
+ id: biddingDocuments.id,
+ fileName: biddingDocuments.fileName,
+ originalFileName: biddingDocuments.originalFileName,
+ fileSize: biddingDocuments.fileSize,
+ filePath: biddingDocuments.filePath,
+ title: biddingDocuments.title,
+ uploadedAt: biddingDocuments.uploadedAt,
+ uploadedBy: biddingDocuments.uploadedBy,
+ })
+ .from(biddingDocuments)
+ .where(
+ and(
+ eq(biddingDocuments.biddingId, biddingId),
+ eq(biddingDocuments.documentType, 'specification_meeting'),
+ eq(biddingDocuments.specificationMeetingId, meetingData.id)
+ )
+ )
+
+ // 4. 데이터 직렬화 (Date 객체를 문자열로 변환)
+ const result: SpecificationMeetingDetails = {
+ id: meetingData.id,
+ biddingId: meetingData.biddingId,
+ meetingDate: meetingData.meetingDate?.toISOString() || '',
+ meetingTime: meetingData.meetingTime,
+ location: meetingData.location,
+ address: meetingData.address,
+ contactPerson: meetingData.contactPerson,
+ contactPhone: meetingData.contactPhone,
+ contactEmail: meetingData.contactEmail,
+ agenda: meetingData.agenda,
+ materials: meetingData.materials,
+ notes: meetingData.notes,
+ isRequired: meetingData.isRequired,
+ createdAt: meetingData.createdAt?.toISOString() || '',
+ updatedAt: meetingData.updatedAt?.toISOString() || '',
+ documents: documents.map(doc => ({
+ id: doc.id,
+ fileName: doc.fileName,
+ originalFileName: doc.originalFileName,
+ fileSize: doc.fileSize || 0,
+ filePath: doc.filePath,
+ title: doc.title,
+ uploadedAt: doc.uploadedAt?.toISOString() || '',
+ uploadedBy: doc.uploadedBy,
+ }))
+ }
+
+ return {
+ success: true,
+ data: result
+ }
+
+ } catch (error) {
+ console.error("사양설명회 상세 정보 조회 실패:", error)
+ return {
+ success: false,
+ error: "사양설명회 정보 조회 중 오류가 발생했습니다"
+ }
+ }
+}
+
+/**
+ * PR 상세 정보 조회 서버 액션
+ */
+export async function getPRDetailsAction(
+ biddingId: number
+): Promise<ActionResult<PRDetails>> {
+ try {
+ // 1. 입력 검증
+ if (!biddingId || isNaN(biddingId) || biddingId <= 0) {
+ return {
+ success: false,
+ error: "유효하지 않은 입찰 ID입니다"
+ }
+ }
+
+ // 2. PR 문서들 조회
+ const documents = await db
+ .select({
+ id: prDocuments.id,
+ documentName: prDocuments.documentName,
+ fileName: prDocuments.fileName,
+ originalFileName: prDocuments.originalFileName,
+ fileSize: prDocuments.fileSize,
+ filePath: prDocuments.filePath,
+ registeredAt: prDocuments.registeredAt,
+ registeredBy: prDocuments.registeredBy,
+ version: prDocuments.version,
+ description: prDocuments.description,
+ createdAt: prDocuments.createdAt,
+ updatedAt: prDocuments.updatedAt,
+ })
+ .from(prDocuments)
+ .where(eq(prDocuments.biddingId, biddingId))
+
+ // 3. PR 아이템들 조회
+ const items = await db
+ .select()
+ .from(prItemsForBidding)
+ .where(eq(prItemsForBidding.biddingId, biddingId))
+
+ // 4. 각 아이템별 스펙 문서들 조회
+ const itemsWithDocs = await Promise.all(
+ items.map(async (item) => {
+ const specDocuments = await db
+ .select({
+ id: biddingDocuments.id,
+ fileName: biddingDocuments.fileName,
+ originalFileName: biddingDocuments.originalFileName,
+ fileSize: biddingDocuments.fileSize,
+ filePath: biddingDocuments.filePath,
+ uploadedAt: biddingDocuments.uploadedAt,
+ title: biddingDocuments.title,
+ })
+ .from(biddingDocuments)
+ .where(
+ and(
+ eq(biddingDocuments.biddingId, biddingId),
+ eq(biddingDocuments.documentType, 'spec_document'),
+ eq(biddingDocuments.prItemId, item.id)
+ )
+ )
+
+ // 5. 데이터 직렬화
+ return {
+ id: item.id,
+ itemNumber: item.itemNumber,
+ itemInfo: item.itemInfo,
+ quantity: item.quantity ? Number(item.quantity) : null,
+ quantityUnit: item.quantityUnit,
+ requestedDeliveryDate: item.requestedDeliveryDate?.toISOString().split('T')[0] || null,
+ prNumber: item.prNumber,
+ annualUnitPrice: item.annualUnitPrice ? Number(item.annualUnitPrice) : null,
+ currency: item.currency,
+ totalWeight: item.totalWeight ? Number(item.totalWeight) : null,
+ weightUnit: item.weightUnit,
+ materialDescription: item.materialDescription,
+ hasSpecDocument: item.hasSpecDocument,
+ createdAt: item.createdAt?.toISOString() || '',
+ updatedAt: item.updatedAt?.toISOString() || '',
+ specDocuments: specDocuments.map(doc => ({
+ id: doc.id,
+ fileName: doc.fileName,
+ originalFileName: doc.originalFileName,
+ fileSize: doc.fileSize || 0,
+ filePath: doc.filePath,
+ uploadedAt: doc.uploadedAt?.toISOString() || '',
+ title: doc.title,
+ }))
+ }
+ })
+ )
+
+ const result: PRDetails = {
+ documents: documents.map(doc => ({
+ id: doc.id,
+ documentName: doc.documentName,
+ fileName: doc.fileName,
+ originalFileName: doc.originalFileName,
+ fileSize: doc.fileSize || 0,
+ filePath: doc.filePath,
+ registeredAt: doc.registeredAt?.toISOString() || '',
+ registeredBy: doc.registeredBy,
+ version: doc.version,
+ description: doc.description,
+ createdAt: doc.createdAt?.toISOString() || '',
+ updatedAt: doc.updatedAt?.toISOString() || '',
+ })),
+ items: itemsWithDocs
+ }
+
+ return {
+ success: true,
+ data: result
+ }
+
+ } catch (error) {
+ console.error("PR 상세 정보 조회 실패:", error)
+ return {
+ success: false,
+ error: "PR 정보 조회 중 오류가 발생했습니다"
+ }
+ }
+}
+
+
+
+/**
+ * 입찰 기본 정보 조회 서버 액션 (선택사항)
+ */
+export async function getBiddingBasicInfoAction(
+ biddingId: number
+): Promise<ActionResult<{
+ id: number
+ title: string
+ hasSpecificationMeeting: boolean
+ hasPrDocument: boolean
+}>> {
+ try {
+ if (!biddingId || isNaN(biddingId) || biddingId <= 0) {
+ return {
+ success: false,
+ error: "유효하지 않은 입찰 ID입니다"
+ }
+ }
+
+ // 간단한 입찰 정보만 조회 (성능 최적화)
+ const bidding = await db.query.biddings.findFirst({
+ where: (biddings, { eq }) => eq(biddings.id, biddingId),
+ columns: {
+ id: true,
+ title: true,
+ hasSpecificationMeeting: true,
+ hasPrDocument: true,
+ }
+ })
+
+ if (!bidding) {
+ return {
+ success: false,
+ error: "입찰 정보를 찾을 수 없습니다"
+ }
+ }
+
+ return {
+ success: true,
+ data: bidding
+ }
+
+ } catch (error) {
+ console.error("입찰 기본 정보 조회 실패:", error)
+ return {
+ success: false,
+ error: "입찰 기본 정보 조회 중 오류가 발생했습니다"
+ }
+ }
+} \ No newline at end of file