From cd0ce0cbe8af8719a6f542098ec78f2a5c1222ce Mon Sep 17 00:00:00 2001 From: dujinkim Date: Mon, 1 Dec 2025 10:28:05 +0000 Subject: (최겸) 구매 입찰 사전견적 개발(rfq-last) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../evcp/(evcp)/(procurement)/rfq-last/page.tsx | 51 +- components/ProjectSelector.tsx | 2 +- components/bidding/ProjectSelectorBid.tsx | 2 +- .../bidding/manage/create-pre-quote-rfq-dialog.tsx | 421 ++++--- db/schema/bidding.ts | 1 + db/schema/rfqLast.ts | 20 + i18n/locales/en/menu.json | 2 +- i18n/locales/ko/menu.json | 2 +- lib/bidding/actions.ts | 26 +- lib/bidding/detail/service.ts | 14 - lib/bidding/list/biddings-table-columns.tsx | 22 +- lib/bidding/pre-quote/service.ts | 1143 ++++++-------------- lib/bidding/service.ts | 306 ++---- .../detail/general-contract-info-header.tsx | 21 +- .../main/create-general-contract-dialog.tsx | 20 +- .../main/general-contracts-table-columns.tsx | 37 +- .../main/general-contracts-table.tsx | 21 +- lib/general-contracts/types.ts | 26 +- lib/rfq-last/quotation-compare-view.tsx | 22 +- lib/rfq-last/service.ts | 37 + lib/rfq-last/shared/rfq-items-dialog.tsx | 2 +- lib/rfq-last/table/rfq-table-columns.tsx | 338 +++++- lib/rfq-last/table/rfq-table.tsx | 9 +- lib/rfq-last/validations.ts | 7 +- .../editor/quotation-items-table.tsx | 2 +- 25 files changed, 1134 insertions(+), 1420 deletions(-) diff --git a/app/[lng]/evcp/(evcp)/(procurement)/rfq-last/page.tsx b/app/[lng]/evcp/(evcp)/(procurement)/rfq-last/page.tsx index ab63c14f..6830dbe9 100644 --- a/app/[lng]/evcp/(evcp)/(procurement)/rfq-last/page.tsx +++ b/app/[lng]/evcp/(evcp)/(procurement)/rfq-last/page.tsx @@ -37,14 +37,16 @@ interface RfqPageProps { // 탭별 데이터 카운트를 가져오는 함수 async function getTabCounts() { try { - const [generalData, itbData, rfqData] = await Promise.all([ + const [generalData, preBiddingData, itbData, rfqData] = await Promise.all([ getRfqs({ page: 1, perPage: 1, sort: [], filters: [], joinOperator: "and", search: "", rfqCategory: "general" }), + getRfqs({ page: 1, perPage: 1, sort: [], filters: [], joinOperator: "and", search: "", rfqCategory: "pre_bidding" }), getRfqs({ page: 1, perPage: 1, sort: [], filters: [], joinOperator: "and", search: "", rfqCategory: "itb" }), getRfqs({ page: 1, perPage: 1, sort: [], filters: [], joinOperator: "and", search: "", rfqCategory: "rfq" }), ]); return { general: generalData.total || 0, + pre_bidding: preBiddingData.total || 0, itb: itbData.total || 0, rfq: rfqData?.total || 0, }; @@ -52,6 +54,7 @@ async function getTabCounts() { console.error("Error fetching tab counts:", error); return { general: 0, + pre_bidding: 0, itb: 0, rfq: 0, }; @@ -76,6 +79,7 @@ export default async function RfqPage(props: RfqPageProps) { // 각 탭별로 데이터 프리패칭 // const allData = await getRfqs({ ...search, rfqCategory: "all" }); const generalData = await getRfqs({ ...search, rfqCategory: "general" }); + const preBiddingData = await getRfqs({ ...search, rfqCategory: "pre_bidding" }); const itbData = await getRfqs({ ...search, rfqCategory: "itb" }); const rfqData = await getRfqs({ ...search, rfqCategory: "rfq" }); @@ -94,7 +98,7 @@ export default async function RfqPage(props: RfqPageProps) { {/* 탭 컨테이너 */} - + @@ -123,6 +127,15 @@ export default async function RfqPage(props: RfqPageProps) { )} + + + 사전견적(입찰) + {tabCounts.pre_bidding > 0 && ( + + {tabCounts.pre_bidding} + + )} + @@ -160,6 +173,40 @@ export default async function RfqPage(props: RfqPageProps) { + {/* 사전견적(입찰) 탭 */} + + + } + > + + + + {/* ITB 탭 */} (undefined) + const [selectedBidPic, setSelectedBidPic] = React.useState(undefined) const { data: session } = useSession() const userId = React.useMemo(() => { @@ -165,24 +170,31 @@ export function CreatePreQuoteRfqDialog({ }, }) + /* const { fields, append, remove } = useFieldArray({ control: form.control, name: "items", }) + */ - // 입찰담당자 정보 로드 + // 견적담당자 정보 로드 React.useEffect(() => { const loadBiddingInfo = async () => { if (!biddingId || !open) return try { const bidding = await getBiddingById(biddingId) - if (bidding && bidding.bidPicId) { - // 입찰담당자 정보를 로드하는 로직 추가 필요 - // 현재는 임시로 bidPicId를 사용 + if (bidding) { setSelectedBidPic({ - USER_ID: bidding.bidPicId, - DISPLAY_NAME: bidding.bidPicName || '입찰담당자' + DISPLAY_NAME: bidding.bidPicName || '', + PURCHASE_GROUP_CODE: bidding.bidPicCode || '', + EMPLOYEE_NUMBER: '', + user: bidding.bidPicId ? { + id: bidding.bidPicId, + name: bidding.bidPicName || '', + email: '', + employeeNumber: null + } : undefined }) } } catch (error) { @@ -192,17 +204,58 @@ export function CreatePreQuoteRfqDialog({ loadBiddingInfo() }, [biddingId, open]) + + // 프로젝트 정보 상태 추가 + const [projectInfo, setProjectInfo] = React.useState("") // 다이얼로그가 열릴 때 폼 초기화 React.useEffect(() => { if (open) { + // 입찰 정보를 기반으로 기본값 설정 + let rfqTitle = ""; + let projectId: number | undefined = undefined; + let contractStartDate: Date | undefined = undefined; + let contractEndDate: Date | undefined = undefined; + let biddingNumber = ""; + + const loadDetailedBiddingInfo = async () => { + if (biddingId) { + try { + const bidding = await getBiddingById(biddingId) + if (bidding) { + rfqTitle = bidding.title; + biddingNumber = bidding.biddingNumber; + + // 프로젝트 정보 설정 + const pCode = bidding.projectCode || ""; + const pName = bidding.projectName || ""; + setProjectInfo(pCode && pName ? `${pCode} - ${pName}` : pCode || pName || ""); + + // 폼 값 설정 + form.setValue("rfqTitle", rfqTitle); + form.setValue("rfqType", "pre_bidding"); // 기본값 설정 + if (biddingNumber) form.setValue("biddingNumber", biddingNumber); + + if (bidding.contractStartDate) form.setValue("contractStartDate", new Date(bidding.contractStartDate)); + if (bidding.contractEndDate) form.setValue("contractEndDate", new Date(bidding.contractEndDate)); + } + } catch (e) { + console.error(e); + } + } + }; + loadDetailedBiddingInfo(); + form.reset({ - rfqType: "", + rfqType: "pre_bidding", // 기본값 rfqTitle: "", - dueDate: undefined, - picUserId: selectedBidPic?.USER_ID, + dueDate: undefined, // 필수값 해제되었으므로 undefined 가능 + picUserId: selectedBidPic?.user?.id, projectId: undefined, remark: "", + biddingNumber: "", + contractStartDate: undefined, + contractEndDate: undefined, items: initialItems.length > 0 ? initialItems : [ { itemCode: "", @@ -217,11 +270,11 @@ export function CreatePreQuoteRfqDialog({ }) setPreviewCode("") } - }, [open, initialItems, form, selectedBidPic]) + }, [open, initialItems, form, selectedBidPic, biddingId]) // 견적담당자 선택 시 RFQ 코드 미리보기 생성 React.useEffect(() => { - if (!selectedBidPic?.USER_ID) { + if (!selectedBidPic?.user?.id) { setPreviewCode("") return } @@ -230,7 +283,7 @@ export function CreatePreQuoteRfqDialog({ (async () => { setIsLoadingPreview(true) try { - const code = await previewGeneralRfqCode(selectedBidPic.USER_ID) + const code = await previewGeneralRfqCode(selectedBidPic.user!.id) setPreviewCode(code) } catch (error) { console.error("코드 미리보기 오류:", error) @@ -277,25 +330,26 @@ export function CreatePreQuoteRfqDialog({ return } - if (!selectedBidPic?.USER_ID) { - toast.error("입찰담당자를 선택해주세요") - return - } - const picUserId = selectedBidPic.USER_ID + + const picUserId = selectedBidPic?.user?.id || session?.user?.id setIsLoading(true) try { // 서버 액션 호출 (입찰 조건 포함) const result = await createPreQuoteRfqAction({ + // biddingId, // createPreQuoteRfqAction 인터페이스 변경됨 biddingId, - rfqType: data.rfqType, + rfqType: data.rfqType || "pre_bidding", rfqTitle: data.rfqTitle, - dueDate: data.dueDate, + dueDate: data.dueDate ? new Date(data.dueDate) : undefined, // optional이지만 submit시에는 값이 있을 수 있음 (없으면 서비스에서 처리) picUserId, projectId: data.projectId, remark: data.remark || "", + biddingNumber: data.biddingNumber, // 추가 + contractStartDate: data.contractStartDate, // 추가 + contractEndDate: data.contractEndDate, // 추가 items: data.items as Array<{ itemCode: string; itemName: string; @@ -336,7 +390,8 @@ export function CreatePreQuoteRfqDialog({ } } - // 아이템 추가 + // 아이템 추가 (사용안함) + /* const handleAddItem = () => { append({ itemCode: "", @@ -348,6 +403,7 @@ export function CreatePreQuoteRfqDialog({ remark: "", }) } + */ return ( @@ -371,7 +427,7 @@ export function CreatePreQuoteRfqDialog({
{/* 견적 종류 */} -
+ {/*
견적 종류 * - + + + )} /> -
+
*/} {/* 제출마감일 */} ( - 제출마감일 * + 제출마감일 @@ -420,7 +467,7 @@ export function CreatePreQuoteRfqDialog({ {field.value ? ( format(field.value, "yyyy-MM-dd") ) : ( - 제출마감일을 선택하세요 + 제출마감일을 선택하세요 (선택) )} @@ -468,23 +515,24 @@ export function CreatePreQuoteRfqDialog({ /> {/* 프로젝트 선택 */} - ( +
프로젝트 - {/* ProjectSelector는 별도 컴포넌트 필요 */} field.onChange(e.target.value ? Number(e.target.value) : undefined)} + value={projectInfo} + readOnly + className="bg-muted" + placeholder="프로젝트 정보 없음" /> - +
+ ( + )} /> @@ -502,7 +550,7 @@ export function CreatePreQuoteRfqDialog({ selectedCode={selectedBidPic} onCodeSelect={(code) => { setSelectedBidPic(code) - field.onChange(code.USER_ID) + field.onChange(code.user?.id) }} placeholder="입찰담당자 선택" /> @@ -526,6 +574,87 @@ export function CreatePreQuoteRfqDialog({
)} + {/* 계약기간 */} +
+ ( + + 계약기간 시작 + + + + + + + + + + + + + )} + /> + + ( + + 계약기간 종료 + + + + + + + + + + + + + )} + /> +
+ {/* 비고 */} - {/* 아이템 정보 섹션 */} -
-
-

자재 정보

- -
- -
- {fields.map((field, index) => ( -
-
- - 자재 #{index + 1} - - {fields.length > 1 && ( - - )} -
- - {/* 자재그룹 선택 */} -
- - 자재그룹(자재그룹명) * - -
- { - const itemCode = form.watch(`items.${index}.itemCode`); - const itemName = form.watch(`items.${index}.itemName`); - if (itemCode && itemName) { - return { - materialGroupCode: itemCode, - materialGroupDescription: itemName, - displayText: `${itemCode} - ${itemName}` - } as MaterialSearchItem; - } - return null; - })()} - onMaterialSelect={(material) => { - form.setValue(`items.${index}.itemCode`, material?.materialGroupCode || ''); - form.setValue(`items.${index}.itemName`, material?.materialGroupDescription || ''); - }} - placeholder="자재그룹을 검색하세요..." - title="자재그룹 선택" - description="원하는 자재그룹을 검색하고 선택해주세요." - triggerVariant="outline" - /> -
-
- - {/* 자재코드 선택 */} -
- - 자재코드(자재명) - -
- { - const materialCode = form.watch(`items.${index}.materialCode`); - const materialName = form.watch(`items.${index}.materialName`); - if (materialCode && materialName) { - return { - materialCode: materialCode, - materialName: materialName, - displayText: `${materialCode} - ${materialName}` - } as SAPMaterialSearchItem; - } - return null; - })()} - onMaterialSelect={(material) => { - form.setValue(`items.${index}.materialCode`, material?.materialCode || ''); - form.setValue(`items.${index}.materialName`, material?.materialName || ''); - }} - placeholder="자재코드를 검색하세요..." - title="자재코드 선택" - description="원하는 자재코드를 검색하고 선택해주세요." - triggerVariant="outline" - /> -
-
- -
- {/* 수량 */} - ( - - - 수량 * - - - field.onChange(Number(e.target.value))} - /> - - - - )} - /> - - {/* 단위 */} - ( - - - 단위 * - - - - - )} - /> -
- - {/* 비고 */} -
- ( - - 비고 - - - - - - )} - /> -
-
- ))} -
-
+ {/* 아이템 정보 섹션 (자동 매핑되므로 UI 제거) */} + {/*
+ ... +
*/} diff --git a/db/schema/bidding.ts b/db/schema/bidding.ts index cc79f482..c08ea921 100644 --- a/db/schema/bidding.ts +++ b/db/schema/bidding.ts @@ -156,6 +156,7 @@ export const biddings = pgTable('biddings', { biddingSourceType: varchar('bidding_source_type', { length: 20 }).notNull().default('manual'), // 기본 정보 projectName: varchar('project_name', { length: 300 }), // 프로젝트명 + projectCode: varchar('project_code', { length: 100 }), // 프로젝트 코드 (새로 추가) itemName: varchar('item_name', { length: 300 }), // 품목명 title: varchar('title', { length: 300 }).notNull(), // 입찰명 description: text('description'), diff --git a/db/schema/rfqLast.ts b/db/schema/rfqLast.ts index 2f2a6710..325942f4 100644 --- a/db/schema/rfqLast.ts +++ b/db/schema/rfqLast.ts @@ -83,6 +83,13 @@ export const rfqsLast = pgTable( rfqType: varchar("rfq_type", { length: 255 }), rfqTitle: varchar("rfq_title", { length: 255 }), + // 입찰 사전견적 추가 필드 + biddingNumber: varchar("bidding_number", { length: 50 }), + contractStartDate: date("contract_start_date", { mode: "date" }) + .$type(), + contractEndDate: date("contract_end_date", { mode: "date" }) + .$type(), + //ITB 추가 필드 projectCompany: varchar("project_company", { length: 255 }), projectFlag: varchar("project_flag", { length: 255 }), @@ -309,6 +316,11 @@ export const rfqsLastView = pgView("rfqs_last_view").as((qb) => { rfqType: sql`${rfqsLast.rfqType}`.as("rfq_type"), rfqTitle: sql`${rfqsLast.rfqTitle}`.as("rfq_title"), + // 입찰 사전견적 추가 필드 + biddingNumber: sql`${rfqsLast.biddingNumber}`.as("bidding_number"), + contractStartDate: sql`${rfqsLast.contractStartDate}`.as("contract_start_date"), + contractEndDate: sql`${rfqsLast.contractEndDate}`.as("contract_end_date"), + // ITB 관련 필드 projectCompany: sql`${rfqsLast.projectCompany}`.as("project_company"), projectFlag: sql`${rfqsLast.projectFlag}`.as("project_flag"), @@ -480,6 +492,11 @@ export const rfqLastDetailsView = pgView("rfq_last_details_view").as((qb) => { rfqType: sql`${rfqsTable.rfqType}`.as("rfq_type"), rfqTitle: sql`${rfqsTable.rfqTitle}`.as("rfq_title"), + // 입찰 사전견적 추가 필드 + biddingNumber: sql`${rfqsTable.biddingNumber}`.as("bidding_number"), + contractStartDate: sql`${rfqsTable.contractStartDate}`.as("contract_start_date"), + contractEndDate: sql`${rfqsTable.contractEndDate}`.as("contract_end_date"), + // ITB 관련 정보 projectCompany: sql`${rfqsTable.projectCompany}`.as("project_company"), projectFlag: sql`${rfqsTable.projectFlag}`.as("project_flag"), @@ -677,6 +694,9 @@ export const prItemsLastView = pgView("pr_items_last_view").as((qb) => { rfqCode: rfqsLast.rfqCode, rfqType: rfqsLast.rfqType, rfqTitle: rfqsLast.rfqTitle, + biddingNumber: rfqsLast.biddingNumber, + contractStartDate: rfqsLast.contractStartDate, + contractEndDate: rfqsLast.contractEndDate, itemCode: rfqsLast.itemCode, itemName: rfqsLast.itemName, prNumber: rfqsLast.prNumber, diff --git a/i18n/locales/en/menu.json b/i18n/locales/en/menu.json index 368dbd92..bb99f0ef 100644 --- a/i18n/locales/en/menu.json +++ b/i18n/locales/en/menu.json @@ -239,7 +239,7 @@ "ship_tbe_desc": "Create response to order TBE request", "rfb_response": "Order RFB Response", "rfb_response_desc": "Create response to bid request", - "po": "PO", + "po": "PO/Contract Management", "po_desc": "Order list confirmation and electronic signature", "po_amendment": "PO Amendment", "po_amendment_desc": "Order list confirmation and electronic signature", diff --git a/i18n/locales/ko/menu.json b/i18n/locales/ko/menu.json index 249118b1..d5c159e2 100644 --- a/i18n/locales/ko/menu.json +++ b/i18n/locales/ko/menu.json @@ -242,7 +242,7 @@ "ship_tbe_desc": "발주용 TBE 요청에 대한 응답 작성", "rfb_response": "발주 RFB 응답", "rfb_response_desc": "입찰 요청에 대한 응답 작성", - "po": "PO", + "po": "PO/계약 관리", "po_desc": "발주 리스트 확인 및 전자서명", "po_amendment": "PO Amendment", "po_amendment_desc": "발주 리스트 확인 및 전자서명", diff --git a/lib/bidding/actions.ts b/lib/bidding/actions.ts index 02501b27..4e7da36c 100644 --- a/lib/bidding/actions.ts +++ b/lib/bidding/actions.ts @@ -96,9 +96,11 @@ export async function transmitToContract(biddingId: number, userId: number) { bidAmount: companyPrItemBids.bidAmount, currency: companyPrItemBids.currency, // PR 아이템 정보도 함께 조회 - itemNumber: prItemsForBidding.itemNumber, - itemInfo: prItemsForBidding.itemInfo, - materialDescription: prItemsForBidding.materialDescription, + projectId: prItemsForBidding.projectId, + materialGroupNumber: prItemsForBidding.materialGroupNumber, + materialGroupInfo: prItemsForBidding.materialGroupInfo, + materialInfo: prItemsForBidding.materialInfo, + specification: prItemsForBidding.specification, quantity: prItemsForBidding.quantity, quantityUnit: prItemsForBidding.quantityUnit, }) @@ -119,7 +121,8 @@ export async function transmitToContract(biddingId: number, userId: number) { } // 계약 번호 자동 생성 (실제 규칙에 맞게) - const contractNumber = await generateContractNumber(userId.toString(), biddingData.contractType) + const safeUserId = userId ? String(userId) : '0'; + const contractNumber = await generateContractNumber(safeUserId, biddingData.contractType) console.log('Generated contractNumber:', contractNumber) // general-contract 생성 (발주비율 계산된 최종 금액 사용) @@ -132,7 +135,7 @@ export async function transmitToContract(biddingId: number, userId: number) { name: biddingData.title, vendorId: winnerCompany.companyId, linkedBidNumber: biddingData.biddingNumber, - contractAmount: totalContractAmount ? totalContractAmount.toString() as any : null, // 발주비율 계산된 최종 금액 사용 + contractAmount: !isNaN(totalContractAmount) ? String(totalContractAmount) : null, // 발주비율 계산된 최종 금액 사용 startDate: biddingData.contractStartDate || null, endDate: biddingData.contractEndDate || null, currency: biddingData.currency || 'KRW', @@ -161,16 +164,17 @@ export async function transmitToContract(biddingId: number, userId: number) { await db.insert(generalContractItems).values({ contractId: contractId, - itemCode: bid.itemNumber || '', - itemInfo: bid.itemInfo || '', - specification: bid.materialDescription || '', - quantity: finalQuantity || null, + projectId: bid.projectId, + itemCode: bid.materialGroupNumber || '', + itemInfo: bid.materialGroupInfo || '', + specification: bid.specification || '', + quantity: !isNaN(finalQuantity) ? String(finalQuantity) : null, quantityUnit: bid.quantityUnit || '', totalWeight: null, // 중량 정보 제외 weightUnit: '', // 중량 단위 제외 contractDeliveryDate: bid.proposedDeliveryDate || null, - contractUnitPrice: bid.bidUnitPrice ? String(bid.bidUnitPrice) : null, - contractAmount: finalAmount ? String(finalAmount) : null, + contractUnitPrice: !isNaN(bidUnitPrice) ? String(bidUnitPrice) : null, + contractAmount: !isNaN(finalAmount) ? String(finalAmount) : null, contractCurrency: bid.currency || biddingData.currency || 'KRW', }) } diff --git a/lib/bidding/detail/service.ts b/lib/bidding/detail/service.ts index e425959c..f52ecb1e 100644 --- a/lib/bidding/detail/service.ts +++ b/lib/bidding/detail/service.ts @@ -2086,20 +2086,6 @@ export async function submitPartnerResponse( const biddingId = biddingCompanyInfo[0]?.biddingId - // 최종제출인 경우, 입찰 상태를 평가중으로 변경 (bidding_opened 상태에서만) - if (biddingId && response.finalQuoteAmount !== undefined && response.isFinalSubmission) { - await tx - .update(biddings) - .set({ - status: 'evaluation_of_bidding', - updatedAt: new Date() - }) - .where(and( - eq(biddings.id, biddingId), - eq(biddings.status, 'bidding_opened') - )) - } - return biddingId }) diff --git a/lib/bidding/list/biddings-table-columns.tsx b/lib/bidding/list/biddings-table-columns.tsx index 9b8c19c5..62d4dbe7 100644 --- a/lib/bidding/list/biddings-table-columns.tsx +++ b/lib/bidding/list/biddings-table-columns.tsx @@ -122,14 +122,20 @@ export function getBiddingsColumns({ setRowAction }: GetColumnsProps): ColumnDef // ░░░ 프로젝트명 ░░░ { accessorKey: "projectName", - header: ({ column }) => , - cell: ({ row }) => ( -
- {row.original.projectName || '-'} -
- ), + header: ({ column }) => , + cell: ({ row }) => { + const code = row.original.projectCode; + const name = row.original.projectName; + const displayText = code && name ? `${code} (${name})` : (code || name || '-'); + + return ( +
+ {displayText} +
+ ) + }, size: 150, - meta: { excelHeader: "프로젝트 No." }, + meta: { excelHeader: "프로젝트" }, }, // ░░░ 입찰명 ░░░ { @@ -241,7 +247,7 @@ export function getBiddingsColumns({ setRowAction }: GetColumnsProps): ColumnDef accessorKey: "biddingRegistrationDate", header: ({ column }) => , cell: ({ row }) => ( - {formatDate(row.original.biddingRegistrationDate , "KR")} + {row.original.biddingRegistrationDate ? formatDate(row.original.biddingRegistrationDate, "KR") : '-'} ), size: 100, meta: { excelHeader: "입찰등록일" }, diff --git a/lib/bidding/pre-quote/service.ts b/lib/bidding/pre-quote/service.ts index 0f938b24..08cb0e2c 100644 --- a/lib/bidding/pre-quote/service.ts +++ b/lib/bidding/pre-quote/service.ts @@ -2,16 +2,16 @@ import db from '@/db/db' import { biddingCompanies, biddingCompaniesContacts, companyConditionResponses, biddings, prItemsForBidding, biddingDocuments, companyPrItemBids, priceAdjustmentForms } from '@/db/schema/bidding' -import { basicContractTemplates } from '@/db/schema' +import { basicContractTemplates, rfqLastDetails, rfqLastVendorResponses, rfqLastVendorResponseHistory, rfqsLast, rfqPrItems, users } from '@/db/schema' import { vendors } from '@/db/schema/vendors' -import { users } from '@/db/schema' import { sendEmail } from '@/lib/mail/sendEmail' -import { eq, inArray, and, ilike, sql } from 'drizzle-orm' +import { eq, inArray, and, ilike, sql, desc, like } from 'drizzle-orm' import { mkdir, writeFile } from 'fs/promises' import path from 'path' import { revalidateTag, revalidatePath } from 'next/cache' import { basicContract } from '@/db/schema/basicContractDocumnet' import { saveFile } from '@/lib/file-stroage' +import { getDefaultDueDate } from '@/lib/rfq-last/service' // userId를 user.name으로 변환하는 유틸리티 함수 async function getUserNameById(userId: string): Promise { @@ -151,50 +151,6 @@ export async function updateBiddingCompany(id: number, input: UpdateBiddingCompa } } -// 본입찰 등록 상태 업데이트 (복수 업체 선택 가능) -export async function updatePreQuoteSelection(companyIds: number[], isSelected: boolean) { - try { - // 업체들의 입찰 ID 조회 (캐시 무효화를 위해) - const companies = await db - .select({ biddingId: biddingCompanies.biddingId }) - .from(biddingCompanies) - .where(inArray(biddingCompanies.id, companyIds)) - .limit(1) - - await db.update(biddingCompanies) - .set({ - isPreQuoteSelected: isSelected, - invitationStatus: 'pending', // 초기 상태: 초대 대기 - updatedAt: new Date() - }) - .where(inArray(biddingCompanies.id, companyIds)) - - // 캐시 무효화 - if (companies.length > 0) { - const biddingId = companies[0].biddingId - revalidateTag(`bidding-${biddingId}`) - revalidateTag('bidding-detail') - revalidateTag('quotation-vendors') - revalidateTag('quotation-details') - revalidatePath(`/evcp/bid/${biddingId}`) - } - - const message = isSelected - ? `${companyIds.length}개 업체가 본입찰 대상으로 선정되었습니다.` - : `${companyIds.length}개 업체의 본입찰 선정이 취소되었습니다.` - - return { - success: true, - message - } - } catch (error) { - console.error('Failed to update pre-quote selection:', error) - return { - success: false, - error: error instanceof Error ? error.message : '본입찰 선정 상태 업데이트에 실패했습니다.' - } - } -} // 사전견적용 업체 삭제 export async function deleteBiddingCompany(id: number) { @@ -244,84 +200,6 @@ export async function deleteBiddingCompany(id: number) { } } -// 특정 입찰의 참여 업체 목록 조회 (company_condition_responses와 vendors 조인) -export async function getBiddingCompanies(biddingId: number) { - try { - const companies = await db - .select({ - // bidding_companies 필드들 - id: biddingCompanies.id, - biddingId: biddingCompanies.biddingId, - companyId: biddingCompanies.companyId, - invitationStatus: biddingCompanies.invitationStatus, - invitedAt: biddingCompanies.invitedAt, - respondedAt: biddingCompanies.respondedAt, - preQuoteAmount: biddingCompanies.preQuoteAmount, - preQuoteSubmittedAt: biddingCompanies.preQuoteSubmittedAt, - preQuoteDeadline: biddingCompanies.preQuoteDeadline, - isPreQuoteSelected: biddingCompanies.isPreQuoteSelected, - isPreQuoteParticipated: biddingCompanies.isPreQuoteParticipated, - isAttendingMeeting: biddingCompanies.isAttendingMeeting, - notes: biddingCompanies.notes, - contactPerson: biddingCompanies.contactPerson, - contactEmail: biddingCompanies.contactEmail, - contactPhone: biddingCompanies.contactPhone, - createdAt: biddingCompanies.createdAt, - updatedAt: biddingCompanies.updatedAt, - - // vendors 테이블에서 업체 정보 - companyName: vendors.vendorName, - companyCode: vendors.vendorCode, - companyEmail: vendors.email, // 벤더의 기본 이메일 - - // company_condition_responses 필드들 - paymentTermsResponse: companyConditionResponses.paymentTermsResponse, - taxConditionsResponse: companyConditionResponses.taxConditionsResponse, - proposedContractDeliveryDate: companyConditionResponses.proposedContractDeliveryDate, - priceAdjustmentResponse: companyConditionResponses.priceAdjustmentResponse, - isInitialResponse: companyConditionResponses.isInitialResponse, - incotermsResponse: companyConditionResponses.incotermsResponse, - proposedShippingPort: companyConditionResponses.proposedShippingPort, - proposedDestinationPort: companyConditionResponses.proposedDestinationPort, - sparePartResponse: companyConditionResponses.sparePartResponse, - additionalProposals: companyConditionResponses.additionalProposals, - }) - .from(biddingCompanies) - .leftJoin( - vendors, - eq(biddingCompanies.companyId, vendors.id) - ) - .leftJoin( - companyConditionResponses, - eq(biddingCompanies.id, companyConditionResponses.biddingCompanyId) - ) - .where(eq(biddingCompanies.biddingId, biddingId)) - - // 디버깅: 서버에서 가져온 데이터 확인 - console.log('=== getBiddingCompanies Server Log ===') - console.log('Total companies:', companies.length) - if (companies.length > 0) { - console.log('First company:', { - companyName: companies[0].companyName, - companyEmail: companies[0].companyEmail, - companyCode: companies[0].companyCode, - companyId: companies[0].companyId - }) - } - console.log('======================================') - - return { - success: true, - data: companies - } - } catch (error) { - console.error('Failed to get bidding companies:', error) - return { - success: false, - error: error instanceof Error ? error.message : '업체 목록 조회에 실패했습니다.' - } - } -} // 선택된 업체들에게 사전견적 초대 발송 interface CompanyWithContacts { @@ -332,512 +210,6 @@ interface CompanyWithContacts { additionalEmails: string[] } -export async function sendPreQuoteInvitations(companiesData: CompanyWithContacts[], preQuoteDeadline?: Date | string) { - try { - console.log('=== sendPreQuoteInvitations called ==='); - console.log('companiesData:', JSON.stringify(companiesData, null, 2)); - - if (companiesData.length === 0) { - return { - success: false, - error: '선택된 업체가 없습니다.' - } - } - - const companyIds = companiesData.map(c => c.id); - console.log('companyIds:', companyIds); - - // 선택된 업체들의 정보와 입찰 정보 조회 - const companiesInfo = await db - .select({ - biddingCompanyId: biddingCompanies.id, - companyId: biddingCompanies.companyId, - biddingId: biddingCompanies.biddingId, - companyName: vendors.vendorName, - companyEmail: vendors.email, - // 입찰 정보 - biddingNumber: biddings.biddingNumber, - revision: biddings.revision, - projectName: biddings.projectName, - biddingTitle: biddings.title, - itemName: biddings.itemName, - preQuoteDate: biddings.preQuoteDate, - budget: biddings.budget, - currency: biddings.currency, - bidPicName: biddings.bidPicName, - supplyPicName: biddings.supplyPicName, - }) - .from(biddingCompanies) - .leftJoin(vendors, eq(biddingCompanies.companyId, vendors.id)) - .leftJoin(biddings, eq(biddingCompanies.biddingId, biddings.id)) - .where(inArray(biddingCompanies.id, companyIds)) - - console.log('companiesInfo fetched:', JSON.stringify(companiesInfo, null, 2)); - - if (companiesInfo.length === 0) { - return { - success: false, - error: '업체 정보를 찾을 수 없습니다.' - } - } - - // 모든 필드가 null이 아닌지 확인하고 안전하게 변환 - const safeCompaniesInfo = companiesInfo.map(company => ({ - ...company, - companyName: company.companyName ?? '', - companyEmail: company.companyEmail ?? '', - biddingNumber: company.biddingNumber ?? '', - revision: company.revision ?? '', - projectName: company.projectName ?? '', - biddingTitle: company.biddingTitle ?? '', - itemName: company.itemName ?? '', - preQuoteDate: company.preQuoteDate ?? null, - budget: company.budget ?? null, - currency: company.currency ?? '', - bidPicName: company.bidPicName ?? '', - supplyPicName: company.supplyPicName ?? '', - })); - - console.log('safeCompaniesInfo prepared:', JSON.stringify(safeCompaniesInfo, null, 2)); - - await db.transaction(async (tx) => { - // 선택된 업체들의 상태를 '사전견적 초대 발송'으로 변경 - for (const id of companyIds) { - await tx.update(biddingCompanies) - .set({ - invitationStatus: 'pre_quote_sent', // 사전견적 초대 발송 상태 - invitedAt: new Date(), - preQuoteDeadline: preQuoteDeadline ? new Date(preQuoteDeadline) : null, - updatedAt: new Date() - }) - .where(eq(biddingCompanies.id, id)) - } - }) - - // 각 업체별로 이메일 발송 (담당자 정보 포함) - console.log('=== Starting email sending ==='); - for (const company of safeCompaniesInfo) { - console.log(`Processing company: ${company.companyName} (biddingCompanyId: ${company.biddingCompanyId})`); - - const companyData = companiesData.find(c => c.id === company.biddingCompanyId); - if (!companyData) { - console.log(`No companyData found for biddingCompanyId: ${company.biddingCompanyId}`); - continue; - } - - console.log('companyData found:', JSON.stringify(companyData, null, 2)); - - const mainEmail = companyData.selectedMainEmail || ''; - const ccEmails = Array.isArray(companyData.additionalEmails) ? companyData.additionalEmails : []; - - console.log(`mainEmail: ${mainEmail}, ccEmails: ${JSON.stringify(ccEmails)}`); - - if (mainEmail) { - try { - console.log('Preparing to send email...'); - - const emailContext = { - companyName: company.companyName, - biddingNumber: company.biddingNumber, - revision: company.revision, - projectName: company.projectName, - biddingTitle: company.biddingTitle, - itemName: company.itemName, - preQuoteDate: company.preQuoteDate ? new Date(company.preQuoteDate).toLocaleDateString() : '', - budget: company.budget ? String(company.budget) : '', - currency: company.currency, - bidPicName: company.bidPicName, - supplyPicName: company.supplyPicName, - loginUrl: `${process.env.NEXT_PUBLIC_APP_URL}/partners/bid/${company.biddingId}/pre-quote`, - currentYear: new Date().getFullYear(), - language: 'ko' - }; - - console.log('Email context prepared:', JSON.stringify(emailContext, null, 2)); - - await sendEmail({ - to: mainEmail, - cc: ccEmails.length > 0 ? ccEmails : undefined, - template: 'pre-quote-invitation', - context: emailContext - }) - - console.log(`Email sent successfully to ${mainEmail}`); - } catch (emailError) { - console.error(`Failed to send email to ${mainEmail}:`, emailError) - // 이메일 발송 실패해도 전체 프로세스는 계속 진행 - } - } - } - // 3. 입찰 상태를 사전견적 요청으로 변경 (bidding_generated 상태에서만) - for (const company of companiesInfo) { - await db.transaction(async (tx) => { - await tx - .update(biddings) - .set({ - status: 'request_for_quotation', - updatedAt: new Date() - }) - .where(and( - eq(biddings.id, company.biddingId), - eq(biddings.status, 'bidding_generated') - )) - }) - } - return { - success: true, - message: `${companyIds.length}개 업체에 사전견적 초대를 발송했습니다.` - } - } catch (error) { - console.error('Failed to send pre-quote invitations:', error) - return { - success: false, - error: error instanceof Error ? error.message : '초대 발송에 실패했습니다.' - } - } -} - -// Partners에서 특정 업체의 입찰 정보 조회 (사전견적 단계) -export async function getBiddingCompaniesForPartners(biddingId: number, companyId: number) { - try { - // 1. 먼저 입찰 기본 정보를 가져옴 - const biddingResult = await db - .select({ - id: biddings.id, - biddingNumber: biddings.biddingNumber, - revision: biddings.revision, - projectName: biddings.projectName, - itemName: biddings.itemName, - title: biddings.title, - description: biddings.description, - contractType: biddings.contractType, - biddingType: biddings.biddingType, - awardCount: biddings.awardCount, - contractStartDate: biddings.contractStartDate, - contractEndDate: biddings.contractEndDate, - preQuoteDate: biddings.preQuoteDate, - biddingRegistrationDate: biddings.biddingRegistrationDate, - submissionStartDate: biddings.submissionStartDate, - submissionEndDate: biddings.submissionEndDate, - evaluationDate: biddings.evaluationDate, - currency: biddings.currency, - budget: biddings.budget, - targetPrice: biddings.targetPrice, - status: biddings.status, - bidPicName: biddings.bidPicName, - supplyPicName: biddings.supplyPicName, - }) - .from(biddings) - .where(eq(biddings.id, biddingId)) - .limit(1) - - if (biddingResult.length === 0) { - return null - } - - const biddingData = biddingResult[0] - - // 2. 해당 업체의 biddingCompanies 정보 조회 - const companyResult = await db - .select({ - biddingCompanyId: biddingCompanies.id, - biddingId: biddingCompanies.biddingId, - invitationStatus: biddingCompanies.invitationStatus, - preQuoteAmount: biddingCompanies.preQuoteAmount, - preQuoteSubmittedAt: biddingCompanies.preQuoteSubmittedAt, - preQuoteDeadline: biddingCompanies.preQuoteDeadline, - isPreQuoteSelected: biddingCompanies.isPreQuoteSelected, - isPreQuoteParticipated: biddingCompanies.isPreQuoteParticipated, - isAttendingMeeting: biddingCompanies.isAttendingMeeting, - // company_condition_responses 정보 - paymentTermsResponse: companyConditionResponses.paymentTermsResponse, - taxConditionsResponse: companyConditionResponses.taxConditionsResponse, - incotermsResponse: companyConditionResponses.incotermsResponse, - proposedContractDeliveryDate: companyConditionResponses.proposedContractDeliveryDate, - proposedShippingPort: companyConditionResponses.proposedShippingPort, - proposedDestinationPort: companyConditionResponses.proposedDestinationPort, - priceAdjustmentResponse: companyConditionResponses.priceAdjustmentResponse, - sparePartResponse: companyConditionResponses.sparePartResponse, - isInitialResponse: companyConditionResponses.isInitialResponse, - additionalProposals: companyConditionResponses.additionalProposals, - }) - .from(biddingCompanies) - .leftJoin( - companyConditionResponses, - eq(biddingCompanies.id, companyConditionResponses.biddingCompanyId) - ) - .where( - and( - eq(biddingCompanies.biddingId, biddingId), - eq(biddingCompanies.companyId, companyId) - ) - ) - .limit(1) - - // 3. 결과 조합 - if (companyResult.length === 0) { - // 아직 초대되지 않은 상태 - return { - ...biddingData, - biddingCompanyId: null, - biddingId: biddingData.id, - invitationStatus: null, - preQuoteAmount: null, - preQuoteSubmittedAt: null, - preQuoteDeadline: null, - isPreQuoteSelected: false, - isPreQuoteParticipated: null, - isAttendingMeeting: null, - paymentTermsResponse: null, - taxConditionsResponse: null, - incotermsResponse: null, - proposedContractDeliveryDate: null, - proposedShippingPort: null, - proposedDestinationPort: null, - priceAdjustmentResponse: null, - sparePartResponse: null, - isInitialResponse: null, - additionalProposals: null, - } - } - - const companyData = companyResult[0] - - return { - ...biddingData, - ...companyData, - biddingId: biddingData.id, // bidding ID 보장 - } - } catch (error) { - console.error('Failed to get bidding companies for partners:', error) - throw error - } -} - -// Partners에서 사전견적 응답 제출 -export async function submitPreQuoteResponse( - biddingCompanyId: number, - responseData: { - preQuoteAmount?: number // 품목별 계산에서 자동으로 계산되므로 optional - prItemQuotations?: PrItemQuotation[] // 품목별 견적 정보 추가 - paymentTermsResponse?: string - taxConditionsResponse?: string - incotermsResponse?: string - proposedContractDeliveryDate?: string - proposedShippingPort?: string - proposedDestinationPort?: string - priceAdjustmentResponse?: boolean - isInitialResponse?: boolean - sparePartResponse?: string - additionalProposals?: string - priceAdjustmentForm?: any - }, - userId: string -) { - try { - let finalAmount = responseData.preQuoteAmount || 0 - - await db.transaction(async (tx) => { - // 1. 품목별 견적 정보 최종 저장 (사전견적 제출) - if (responseData.prItemQuotations && responseData.prItemQuotations.length > 0) { - // 기존 사전견적 품목 삭제 후 새로 생성 - await tx.delete(companyPrItemBids) - .where( - and( - eq(companyPrItemBids.biddingCompanyId, biddingCompanyId), - eq(companyPrItemBids.isPreQuote, true) - ) - ) - - // 품목별 견적 최종 저장 - for (const item of responseData.prItemQuotations) { - await tx.insert(companyPrItemBids) - .values({ - biddingCompanyId, - prItemId: item.prItemId, - bidUnitPrice: item.bidUnitPrice.toString(), - bidAmount: item.bidAmount.toString(), - proposedDeliveryDate: item.proposedDeliveryDate || null, - technicalSpecification: item.technicalSpecification || null, - currency: 'KRW', - isPreQuote: true, - submittedAt: new Date(), - createdAt: new Date(), - updatedAt: new Date() - }) - } - - // 총 금액 다시 계산 - finalAmount = responseData.prItemQuotations.reduce((sum, item) => sum + item.bidAmount, 0) - } - - // 2. biddingCompanies 업데이트 (사전견적 금액, 제출 시간, 상태 변경) - await tx.update(biddingCompanies) - .set({ - preQuoteAmount: finalAmount.toString(), - preQuoteSubmittedAt: new Date(), - invitationStatus: 'pre_quote_submitted', // 사전견적제출완료 상태로 변경 - updatedAt: new Date() - }) - .where(eq(biddingCompanies.id, biddingCompanyId)) - - // 3. company_condition_responses 업데이트 - const finalConditionResult = await tx.update(companyConditionResponses) - .set({ - paymentTermsResponse: responseData.paymentTermsResponse, - taxConditionsResponse: responseData.taxConditionsResponse, - incotermsResponse: responseData.incotermsResponse, - proposedContractDeliveryDate: responseData.proposedContractDeliveryDate, - proposedShippingPort: responseData.proposedShippingPort, - proposedDestinationPort: responseData.proposedDestinationPort, - priceAdjustmentResponse: responseData.priceAdjustmentResponse, - isInitialResponse: responseData.isInitialResponse, - sparePartResponse: responseData.sparePartResponse, - additionalProposals: responseData.additionalProposals, - updatedAt: new Date() - }) - .where(eq(companyConditionResponses.biddingCompanyId, biddingCompanyId)) - .returning() - - // 4. 연동제 정보 저장 (연동제 적용이 true이고 연동제 정보가 있는 경우) - if (responseData.priceAdjustmentResponse && responseData.priceAdjustmentForm && finalConditionResult.length > 0) { - const companyConditionResponseId = finalConditionResult[0].id - - const priceAdjustmentData = { - companyConditionResponsesId: companyConditionResponseId, - itemName: responseData.priceAdjustmentForm.itemName, - adjustmentReflectionPoint: responseData.priceAdjustmentForm.adjustmentReflectionPoint, - majorApplicableRawMaterial: responseData.priceAdjustmentForm.majorApplicableRawMaterial, - adjustmentFormula: responseData.priceAdjustmentForm.adjustmentFormula, - rawMaterialPriceIndex: responseData.priceAdjustmentForm.rawMaterialPriceIndex, - referenceDate: responseData.priceAdjustmentForm.referenceDate as string || null, - comparisonDate: responseData.priceAdjustmentForm.comparisonDate as string || null, - adjustmentRatio: responseData.priceAdjustmentForm.adjustmentRatio || null, - notes: responseData.priceAdjustmentForm.notes, - adjustmentConditions: responseData.priceAdjustmentForm.adjustmentConditions, - majorNonApplicableRawMaterial: responseData.priceAdjustmentForm.majorNonApplicableRawMaterial, - adjustmentPeriod: responseData.priceAdjustmentForm.adjustmentPeriod, - contractorWriter: responseData.priceAdjustmentForm.contractorWriter, - adjustmentDate: responseData.priceAdjustmentForm.adjustmentDate as string || null, - nonApplicableReason: responseData.priceAdjustmentForm.nonApplicableReason, - } as any - - // 기존 연동제 정보가 있는지 확인 - const existingPriceAdjustment = await tx - .select() - .from(priceAdjustmentForms) - .where(eq(priceAdjustmentForms.companyConditionResponsesId, companyConditionResponseId)) - .limit(1) - - if (existingPriceAdjustment.length > 0) { - // 업데이트 - await tx - .update(priceAdjustmentForms) - .set(priceAdjustmentData) - .where(eq(priceAdjustmentForms.companyConditionResponsesId, companyConditionResponseId)) - } else { - // 새로 생성 - await tx.insert(priceAdjustmentForms).values(priceAdjustmentData) - } - } - - // 5. 입찰 상태를 사전견적 접수로 변경 (request_for_quotation 상태에서만) - // 또한 사전견적 접수일 업데이트 - const biddingCompany = await tx - .select({ biddingId: biddingCompanies.biddingId }) - .from(biddingCompanies) - .where(eq(biddingCompanies.id, biddingCompanyId)) - .limit(1) - - if (biddingCompany.length > 0) { - await tx - .update(biddings) - .set({ - status: 'received_quotation', - preQuoteDate: new Date().toISOString().split('T')[0], // 사전견적 접수일 업데이트 - updatedAt: new Date() - }) - .where(and( - eq(biddings.id, biddingCompany[0].biddingId), - eq(biddings.status, 'request_for_quotation') - )) - } - }) - - return { - success: true, - message: '사전견적이 성공적으로 제출되었습니다.' - } - } catch (error) { - console.error('Failed to submit pre-quote response:', error) - return { - success: false, - error: error instanceof Error ? error.message : '사전견적 제출에 실패했습니다.' - } - } -} - -// Partners에서 사전견적 참여 의사 결정 (참여/미참여) -export async function respondToPreQuoteInvitation( - biddingCompanyId: number, - response: 'pre_quote_accepted' | 'pre_quote_declined' -) { - try { - await db.update(biddingCompanies) - .set({ - invitationStatus: response, // pre_quote_accepted 또는 pre_quote_declined - respondedAt: new Date(), - updatedAt: new Date() - }) - .where(eq(biddingCompanies.id, biddingCompanyId)) - - const message = response === 'pre_quote_accepted' ? - '사전견적 참여를 수락했습니다.' : - '사전견적 참여를 거절했습니다.' - - return { - success: true, - message - } - } catch (error) { - console.error('Failed to respond to pre-quote invitation:', error) - return { - success: false, - error: error instanceof Error ? error.message : '응답 처리에 실패했습니다.' - } - } -} - -// 벤더에서 사전견적 참여 여부 결정 (isPreQuoteParticipated 사용) -export async function setPreQuoteParticipation( - biddingCompanyId: number, - isParticipating: boolean -) { - try { - await db.update(biddingCompanies) - .set({ - isPreQuoteParticipated: isParticipating, - respondedAt: new Date(), - updatedAt: new Date() - }) - .where(eq(biddingCompanies.id, biddingCompanyId)) - - const message = isParticipating ? - '사전견적 참여를 확정했습니다. 이제 견적서를 작성하실 수 있습니다.' : - '사전견적 참여를 거절했습니다.' - - return { - success: true, - message - } - } catch (error) { - console.error('Failed to set pre-quote participation:', error) - return { - success: false, - error: error instanceof Error ? error.message : '참여 의사 처리에 실패했습니다.' - } - } -} // PR 아이템 조회 (입찰에 포함된 품목들) export async function getPrItemsForBidding(biddingId: number, companyId?: number) { @@ -937,146 +309,6 @@ export async function getSpecDocumentsForPrItem(prItemId: number) { } } -// 사전견적 임시저장 -export async function savePreQuoteDraft( - biddingCompanyId: number, - responseData: { - prItemQuotations?: PrItemQuotation[] - paymentTermsResponse?: string - taxConditionsResponse?: string - incotermsResponse?: string - proposedContractDeliveryDate?: string - proposedShippingPort?: string - proposedDestinationPort?: string - priceAdjustmentResponse?: boolean - isInitialResponse?: boolean - sparePartResponse?: string - additionalProposals?: string - priceAdjustmentForm?: any - }, - userId: string -) { - try { - let totalAmount = 0 - console.log('responseData', responseData) - - await db.transaction(async (tx) => { - // 품목별 견적 정보 저장 - if (responseData.prItemQuotations && responseData.prItemQuotations.length > 0) { - // 기존 사전견적 품목 삭제 (임시저장 시 덮어쓰기) - await tx.delete(companyPrItemBids) - .where( - and( - eq(companyPrItemBids.biddingCompanyId, biddingCompanyId), - eq(companyPrItemBids.isPreQuote, true) - ) - ) - - // 새로운 품목별 견적 저장 - for (const item of responseData.prItemQuotations) { - await tx.insert(companyPrItemBids) - .values({ - biddingCompanyId, - prItemId: item.prItemId, - bidUnitPrice: item.bidUnitPrice.toString(), - bidAmount: item.bidAmount.toString(), - proposedDeliveryDate: item.proposedDeliveryDate || null, - technicalSpecification: item.technicalSpecification || null, - currency: 'KRW', - isPreQuote: true, // 사전견적 표시 - submittedAt: new Date(), - createdAt: new Date(), - updatedAt: new Date() - }) - } - - // 총 금액 계산 - totalAmount = responseData.prItemQuotations.reduce((sum, item) => sum + item.bidAmount, 0) - - // biddingCompanies에 총 금액 임시 저장 (status는 변경하지 않음) - await tx.update(biddingCompanies) - .set({ - preQuoteAmount: totalAmount.toString(), - updatedAt: new Date() - }) - .where(eq(biddingCompanies.id, biddingCompanyId)) - } - - // company_condition_responses 업데이트 (임시저장) - const conditionResult = await tx.update(companyConditionResponses) - .set({ - paymentTermsResponse: responseData.paymentTermsResponse || null, - taxConditionsResponse: responseData.taxConditionsResponse || null, - incotermsResponse: responseData.incotermsResponse || null, - proposedContractDeliveryDate: responseData.proposedContractDeliveryDate || null, - proposedShippingPort: responseData.proposedShippingPort || null, - proposedDestinationPort: responseData.proposedDestinationPort || null, - priceAdjustmentResponse: responseData.priceAdjustmentResponse || null, - isInitialResponse: responseData.isInitialResponse || null, - sparePartResponse: responseData.sparePartResponse || null, - additionalProposals: responseData.additionalProposals || null, - updatedAt: new Date() - }) - .where(eq(companyConditionResponses.biddingCompanyId, biddingCompanyId)) - .returning() - - // 연동제 정보 저장 (연동제 적용이 true이고 연동제 정보가 있는 경우) - if (responseData.priceAdjustmentResponse && responseData.priceAdjustmentForm && conditionResult.length > 0) { - const companyConditionResponseId = conditionResult[0].id - - const priceAdjustmentData = { - companyConditionResponsesId: companyConditionResponseId, - itemName: responseData.priceAdjustmentForm.itemName, - adjustmentReflectionPoint: responseData.priceAdjustmentForm.adjustmentReflectionPoint, - majorApplicableRawMaterial: responseData.priceAdjustmentForm.majorApplicableRawMaterial, - adjustmentFormula: responseData.priceAdjustmentForm.adjustmentFormula, - rawMaterialPriceIndex: responseData.priceAdjustmentForm.rawMaterialPriceIndex, - referenceDate: responseData.priceAdjustmentForm.referenceDate as string || null, - comparisonDate: responseData.priceAdjustmentForm.comparisonDate as string || null, - adjustmentRatio: responseData.priceAdjustmentForm.adjustmentRatio || null, - notes: responseData.priceAdjustmentForm.notes, - adjustmentConditions: responseData.priceAdjustmentForm.adjustmentConditions, - majorNonApplicableRawMaterial: responseData.priceAdjustmentForm.majorNonApplicableRawMaterial, - adjustmentPeriod: responseData.priceAdjustmentForm.adjustmentPeriod, - contractorWriter: responseData.priceAdjustmentForm.contractorWriter, - adjustmentDate: responseData.priceAdjustmentForm.adjustmentDate as string || null, - nonApplicableReason: responseData.priceAdjustmentForm.nonApplicableReason, - } as any - - // 기존 연동제 정보가 있는지 확인 - const existingPriceAdjustment = await tx - .select() - .from(priceAdjustmentForms) - .where(eq(priceAdjustmentForms.companyConditionResponsesId, companyConditionResponseId)) - .limit(1) - - if (existingPriceAdjustment.length > 0) { - // 업데이트 - await tx - .update(priceAdjustmentForms) - .set(priceAdjustmentData) - .where(eq(priceAdjustmentForms.companyConditionResponsesId, companyConditionResponseId)) - } else { - // 새로 생성 - await tx.insert(priceAdjustmentForms).values(priceAdjustmentData) - } - } - }) - - return { - success: true, - message: '임시저장이 완료되었습니다.', - totalAmount - } - } catch (error) { - console.error('Failed to save pre-quote draft:', error) - return { - success: false, - error: error instanceof Error ? error.message : '임시저장에 실패했습니다.' - } - } -} - // 견적 문서 업로드 export async function uploadPreQuoteDocument( biddingId: number, @@ -1165,40 +397,6 @@ export async function getPreQuoteDocuments(biddingId: number, companyId: number) } } -// 저장된 품목별 견적 조회 (임시저장/기존 데이터 불러오기용) -export async function getSavedPrItemQuotations(biddingCompanyId: number) { - try { - const savedQuotations = await db - .select({ - prItemId: companyPrItemBids.prItemId, - bidUnitPrice: companyPrItemBids.bidUnitPrice, - bidAmount: companyPrItemBids.bidAmount, - proposedDeliveryDate: companyPrItemBids.proposedDeliveryDate, - technicalSpecification: companyPrItemBids.technicalSpecification, - currency: companyPrItemBids.currency - }) - .from(companyPrItemBids) - .where( - and( - eq(companyPrItemBids.biddingCompanyId, biddingCompanyId), - eq(companyPrItemBids.isPreQuote, true) - ) - ) - - // Decimal 타입을 number로 변환 - return savedQuotations.map(item => ({ - prItemId: item.prItemId, - bidUnitPrice: parseFloat(item.bidUnitPrice || '0'), - bidAmount: parseFloat(item.bidAmount || '0'), - proposedDeliveryDate: item.proposedDeliveryDate, - technicalSpecification: item.technicalSpecification, - currency: item.currency - })) - } catch (error) { - console.error('Failed to get saved PR item quotations:', error) - return [] - } - } // 견적 문서 정보 조회 (다운로드용) export async function getPreQuoteDocumentForDownload( @@ -1673,4 +871,337 @@ export async function getSelectedVendorsForBidding(biddingId: number) { vendors: [] } } -} \ No newline at end of file +} + +//입찰 사전견적 생성 서버액션 +interface CreatePreQuoteRfqInput { + rfqType: string; + rfqTitle: string; + dueDate: Date; + picUserId: number; + projectId?: number; + remark?: string; + biddingNumber?: string; + biddingId?: number; // 추가 + contractStartDate?: Date; + contractEndDate?: Date; + items: Array<{ + itemCode: string; + itemName: string; + quantity: number; + uom: string; + remark?: string; + materialCode?: string; + materialName?: string; + }>; + biddingConditions?: { + paymentTerms?: string | null + taxConditions?: string | null + incoterms?: string | null + incotermsOption?: string | null + contractDeliveryDate?: string | null + shippingPort?: string | null + destinationPort?: string | null + isPriceAdjustmentApplicable?: boolean | null + sparePartOptions?: string | null + }; + createdBy: number; + updatedBy: number; +} + +export async function createPreQuoteRfqAction(input: CreatePreQuoteRfqInput) { + try { + // 트랜잭션으로 처리 + const result = await db.transaction(async (tx) => { + // 1. 구매 담당자 정보 조회 + const picUser = await tx + .select({ + name: users.name, + email: users.email, + userCode: users.userCode + }) + .from(users) + .where(eq(users.id, input.picUserId)) + .limit(1); + + if (!picUser || picUser.length === 0) { + throw new Error("구매 담당자를 찾을 수 없습니다"); + } + + // 2. userCode 확인 (3자리) + const userCode = picUser[0].userCode; + if (!userCode || userCode.length !== 3) { + throw new Error("구매 담당자의 userCode가 올바르지 않습니다 (3자리 필요)"); + } + + // 3. RFQ 코드 생성 (B + userCode + 00001) + const rfqCode = await generatePreQuoteRfqCode(userCode); + + // 4. 대표 아이템 정보 추출 (첫 번째 아이템) + const representativeItem = input.items[0]; + + // 5. 마감일 기본값 설정 (입력값 없으면 생성일 + 7일) + const dueDate = input.dueDate || await getDefaultDueDate(); + + // 6. rfqsLast 테이블에 기본 정보 삽입 + const [newRfq] = await tx + .insert(rfqsLast) + .values({ + rfqCode, + rfqType: 'pre_bidding', + rfqTitle: input.rfqTitle, + status: "RFQ 생성", + dueDate: dueDate, // 마감일 기본값 설정 + biddingNumber: input.biddingNumber || null, + contractStartDate: input.contractStartDate || null, + contractEndDate: input.contractEndDate || null, + + // 프로젝트 정보 (선택사항) + projectId: input.projectId || null, + + // 대표 아이템 정보 + itemCode: representativeItem.materialCode || representativeItem.itemCode, + itemName: representativeItem.materialName || representativeItem.itemName, + + // 담당자 정보 + pic: input.picUserId, + picCode: userCode, // userCode를 picCode로 사용 + picName: picUser[0].name || '', + + // 기타 정보 + remark: input.remark || null, + createdBy: input.createdBy, + updatedBy: input.updatedBy, + createdAt: new Date(), + updatedAt: new Date(), + }) + .returning(); + + // 7. rfqPrItems 테이블에 아이템들 삽입 + const prItemsData = input.items.map((item, index) => ({ + rfqsLastId: newRfq.id, + rfqItem: `${index + 1}`.padStart(3, '0'), // 001, 002, ... + prItem: null, // 일반견적에서는 PR 아이템 번호를 null로 설정 + prNo: null, // 일반견적에서는 PR 번호를 null로 설정 + + // 자재그룹 정보 + materialCategory: item.itemCode, // 자재그룹코드 + materialDescription: item.itemName, // 자재그룹명 + + // 자재 정보 + materialCode: item.materialCode, // SAP 자재코드 + acc: item.materialName || null, // 자재명 (ACC 컬럼에 저장) + quantity: item.quantity, // 수량 + uom: item.uom, // 단위 + + majorYn: index === 0, // 첫 번째 아이템을 주요 아이템으로 설정 + remark: item.remark || null, // 비고 + })); + + await tx.insert(rfqPrItems).values(prItemsData); + + // 8. 벤더 및 조건 생성 (biddingId가 있는 경우) + if (input.biddingId) { + // 입찰 조건 매핑 + const rfqConditions = mapBiddingConditionsToRfqConditions(input.biddingConditions); + + // 입찰 업체 조회 + const biddingVendors = await tx + .select({ + companyId: biddingCompanies.companyId, + }) + .from(biddingCompanies) + .where(eq(biddingCompanies.biddingId, input.biddingId)); + + if (biddingVendors.length > 0) { + for (const vendor of biddingVendors) { + if (!vendor.companyId) continue; + + // rfqLastDetails 생성 + const [rfqDetail] = await tx + .insert(rfqLastDetails) + .values({ + rfqsLastId: newRfq.id, + vendorsId: vendor.companyId, + currency: rfqConditions.currency, + paymentTermsCode: rfqConditions.paymentTermsCode || null, + incotermsCode: rfqConditions.incotermsCode || null, + incotermsDetail: rfqConditions.incotermsDetail || null, + deliveryDate: rfqConditions.deliveryDate || null, + taxCode: rfqConditions.taxCode || null, + placeOfShipping: rfqConditions.placeOfShipping || null, + placeOfDestination: rfqConditions.placeOfDestination || null, + materialPriceRelatedYn: rfqConditions.materialPriceRelatedYn, + sparepartYn: rfqConditions.sparepartYn, + sparepartDescription: rfqConditions.sparepartDescription || null, + updatedBy: input.updatedBy, + createdBy: input.createdBy, + isLatest: true, + }) + .returning(); + + // rfqLastVendorResponses 생성 + const [vendorResponse] = await tx + .insert(rfqLastVendorResponses) + .values({ + rfqsLastId: newRfq.id, + rfqLastDetailsId: rfqDetail.id, + vendorId: vendor.companyId, + status: '대기중', + responseVersion: 1, + isLatest: true, + participationStatus: '미응답', + currency: rfqConditions.currency, + // 구매자 제시 조건을 벤더 제안 조건의 초기값으로 복사 + vendorCurrency: rfqConditions.currency, + vendorPaymentTermsCode: rfqConditions.paymentTermsCode || null, + vendorIncotermsCode: rfqConditions.incotermsCode || null, + vendorIncotermsDetail: rfqConditions.incotermsDetail || null, + vendorDeliveryDate: rfqConditions.vendorDeliveryDate || null, + vendorTaxCode: rfqConditions.taxCode || null, + vendorPlaceOfShipping: rfqConditions.placeOfShipping || null, + vendorPlaceOfDestination: rfqConditions.placeOfDestination || null, + vendorMaterialPriceRelatedYn: rfqConditions.materialPriceRelatedYn, + vendorSparepartYn: rfqConditions.sparepartYn, + vendorSparepartDescription: rfqConditions.sparepartDescription || null, + createdBy: input.createdBy, + updatedBy: input.updatedBy, + }) + .returning(); + + // 이력 기록 + await tx + .insert(rfqLastVendorResponseHistory) + .values({ + vendorResponseId: vendorResponse.id, + action: '생성', + newStatus: '대기중', + changeDetails: { + action: '사전견적용 일반견적 생성', + biddingId: input.biddingId, + conditions: rfqConditions, + }, + performedBy: input.createdBy, + }); + } + } + } + + return newRfq; + }); + + return { + success: true, + message: "입찰 사전견적이 성공적으로 생성되었습니다", + data: { + id: result.id, + rfqCode: result.rfqCode, + }, + }; + + } catch (error) { + console.error("입찰 사전견적 생성 오류:", error); + + if (error instanceof Error) { + return { + success: false, + error: error.message, + }; + } + + return { + success: false, + error: "입찰 사전견적 생성 중 오류가 발생했습니다", + }; + } +} + +// 사전견적(입찰) RFQ 코드 생성 (B+userCode(3자리)+일련번호5자리 형식) +async function generatePreQuoteRfqCode(userCode: string): Promise { + // circular dependency check: use dynamic import for schema if needed, but generatePreQuoteRfqCode is used inside the action + // rfqsLast is already imported at top. + + try { + // 동일한 userCode를 가진 마지막 사전견적 번호 조회 + const lastRfq = await db + .select({ rfqCode: rfqsLast.rfqCode }) + .from(rfqsLast) + .where( + and( + like(rfqsLast.rfqCode, `B${userCode}%`) // 같은 userCode로 시작하는 RFQ만 조회 + ) + ) + .orderBy(desc(rfqsLast.createdAt)) + .limit(1); + + let nextNumber = 1; + + if (lastRfq.length > 0 && lastRfq[0].rfqCode) { + // B+userCode(3자리)+일련번호(5자리) 형식에서 마지막 5자리 숫자 추출 + const rfqCode = lastRfq[0].rfqCode; + const serialNumber = rfqCode.slice(-5); // 마지막 5자리 추출 + + // 숫자인지 확인하고 다음 번호 생성 + if (/^\d{5}$/.test(serialNumber)) { + nextNumber = parseInt(serialNumber) + 1; + } + } + + // 5자리 숫자로 패딩 + const paddedNumber = String(nextNumber).padStart(5, '0'); + return `B${userCode}${paddedNumber}`; + } catch (error) { + console.error("Error generating Pre-Quote RFQ code:", error); + // 에러 발생 시 타임스탬프 기반 코드 생성 + const timestamp = Date.now().toString().slice(-5); + return `B${userCode}${timestamp}`; + } +} + +// Helper function to map bidding conditions +function mapBiddingConditionsToRfqConditions(conditions?: CreatePreQuoteRfqInput['biddingConditions']) { + if (!conditions) { + return { + currency: 'KRW', + paymentTermsCode: undefined, + incotermsCode: undefined, + incotermsDetail: undefined, + deliveryDate: undefined, + taxCode: undefined, + placeOfShipping: undefined, + placeOfDestination: undefined, + materialPriceRelatedYn: false, + sparepartYn: false, + sparepartDescription: undefined, + vendorDeliveryDate: undefined + } + } + + // contractDeliveryDate 문자열을 Date로 변환 (timestamp 타입용) + let deliveryDate: Date | undefined = undefined + if (conditions.contractDeliveryDate) { + try { + const date = new Date(conditions.contractDeliveryDate) + if (!isNaN(date.getTime())) { + deliveryDate = date + } + } catch (error) { + console.warn('Failed to parse contractDeliveryDate:', error) + } + } + + return { + currency: 'KRW', // 기본값 + paymentTermsCode: conditions.paymentTerms || undefined, + incotermsCode: conditions.incoterms || undefined, + incotermsDetail: conditions.incotermsOption || undefined, + deliveryDate: deliveryDate, // timestamp 타입 (rfqLastDetails용) + vendorDeliveryDate: deliveryDate, // date 타입 (rfqLastVendorResponses용) + taxCode: conditions.taxConditions || undefined, + placeOfShipping: conditions.shippingPort || undefined, + placeOfDestination: conditions.destinationPort || undefined, + materialPriceRelatedYn: conditions.isPriceAdjustmentApplicable ?? false, + sparepartYn: !!conditions.sparePartOptions, // sparePartOptions가 있으면 true + sparepartDescription: conditions.sparePartOptions || undefined, + } +} diff --git a/lib/bidding/service.ts b/lib/bidding/service.ts index 0064b66f..a658ee6a 100644 --- a/lib/bidding/service.ts +++ b/lib/bidding/service.ts @@ -391,6 +391,7 @@ export async function getBiddings(input: GetBiddingsSchema) { id: biddings.id, biddingNumber: biddings.biddingNumber, originalBiddingNumber: biddings.originalBiddingNumber, + projectCode: biddings.projectCode, projectName: biddings.projectName, title: biddings.title, @@ -2150,6 +2151,7 @@ export async function updateBiddingProjectInfo(biddingId: number) { try { const firstItem = await db .select({ + projectId: prItemsForBidding.projectId, projectInfo: prItemsForBidding.projectInfo }) .from(prItemsForBidding) @@ -2157,16 +2159,36 @@ export async function updateBiddingProjectInfo(biddingId: number) { .orderBy(prItemsForBidding.id) .limit(1) - if (firstItem.length > 0 && firstItem[0].projectInfo) { + if (firstItem.length > 0) { + let projectName = firstItem[0].projectInfo + let projectCode = null + + if (firstItem[0].projectId) { + const project = await db + .select({ + name: projects.name, + code: projects.code + }) + .from(projects) + .where(eq(projects.id, firstItem[0].projectId)) + .limit(1) + + if (project.length > 0) { + projectName = project[0].name + projectCode = project[0].code + } + } + await db .update(biddings) .set({ - projectName: firstItem[0].projectInfo, + projectName: projectName, + projectCode: projectCode, updatedAt: new Date() }) .where(eq(biddings.id, biddingId)) - console.log(`Bidding ${biddingId} project info updated to: ${firstItem[0].projectInfo}`) + console.log(`Bidding ${biddingId} project info updated to: ${projectName} (${projectCode})`) } } catch (error) { console.error('Failed to update bidding project info:', error) @@ -2556,227 +2578,6 @@ export async function updateBiddingConditions( } } -// 사전견적용 일반견적 생성 액션 -export async function createPreQuoteRfqAction(input: { - biddingId: number - rfqType: string - rfqTitle: string - dueDate: Date - picUserId: number - projectId?: number - remark?: string - items: Array<{ - itemCode: string - itemName: string - materialCode?: string - materialName?: string - quantity: number - uom: string - remark?: string - }> - biddingConditions?: { - paymentTerms?: string | null - taxConditions?: string | null - incoterms?: string | null - incotermsOption?: string | null - contractDeliveryDate?: string | null - shippingPort?: string | null - destinationPort?: string | null - isPriceAdjustmentApplicable?: boolean | null - sparePartOptions?: string | null - } - createdBy: number - updatedBy: number -}) { - try { - // 일반견적 생성 서버 액션 및 필요한 스키마 import - const { createGeneralRfqAction } = await import('@/lib/rfq-last/service') - const { rfqLastDetails, rfqLastVendorResponses, rfqLastVendorResponseHistory } = await import('@/db/schema') - - // 일반견적 생성 - const result = await createGeneralRfqAction({ - rfqType: input.rfqType, - rfqTitle: input.rfqTitle, - dueDate: input.dueDate, - picUserId: input.picUserId, - projectId: input.projectId, - remark: input.remark || '', - items: input.items.map(item => ({ - itemCode: item.itemCode, - itemName: item.itemName, - quantity: item.quantity, - uom: item.uom, - remark: item.remark, - materialCode: item.materialCode, - materialName: item.materialName, - })), - createdBy: input.createdBy, - updatedBy: input.updatedBy, - }) - - if (!result.success || !result.data) { - return { - success: false, - error: result.error || '사전견적용 일반견적 생성에 실패했습니다', - } - } - - const rfqId = result.data.id - const conditions = input.biddingConditions - - // 입찰 조건을 RFQ 조건으로 매핑 - const mapBiddingConditionsToRfqConditions = () => { - if (!conditions) { - return { - currency: 'KRW', - paymentTermsCode: undefined, - incotermsCode: undefined, - incotermsDetail: undefined, - deliveryDate: undefined, - taxCode: undefined, - placeOfShipping: undefined, - placeOfDestination: undefined, - materialPriceRelatedYn: false, - sparepartYn: false, - sparepartDescription: undefined, - } - } - - // contractDeliveryDate 문자열을 Date로 변환 (timestamp 타입용) - let deliveryDate: Date | undefined = undefined - if (conditions.contractDeliveryDate) { - try { - const date = new Date(conditions.contractDeliveryDate) - if (!isNaN(date.getTime())) { - deliveryDate = date - } - } catch (error) { - console.warn('Failed to parse contractDeliveryDate:', error) - } - } - - return { - currency: 'KRW', // 기본값 - paymentTermsCode: conditions.paymentTerms || undefined, - incotermsCode: conditions.incoterms || undefined, - incotermsDetail: conditions.incotermsOption || undefined, - deliveryDate: deliveryDate, // timestamp 타입 (rfqLastDetails용) - vendorDeliveryDate: deliveryDate, // date 타입 (rfqLastVendorResponses용) - taxCode: conditions.taxConditions || undefined, - placeOfShipping: conditions.shippingPort || undefined, - placeOfDestination: conditions.destinationPort || undefined, - materialPriceRelatedYn: conditions.isPriceAdjustmentApplicable ?? false, - sparepartYn: !!conditions.sparePartOptions, // sparePartOptions가 있으면 true - sparepartDescription: conditions.sparePartOptions || undefined, - } - } - - const rfqConditions = mapBiddingConditionsToRfqConditions() - - // 입찰에 참여한 업체 목록 조회 - const vendorsResult = await getBiddingVendors(input.biddingId) - if (!vendorsResult.success || !vendorsResult.data || vendorsResult.data.length === 0) { - return { - success: true, - message: '사전견적용 일반견적이 생성되었습니다. (참여 업체 없음)', - data: { - rfqCode: result.data.rfqCode, - rfqId: result.data.id, - }, - } - } - - // 각 업체에 대해 rfqLastDetails와 rfqLastVendorResponses 생성 - await db.transaction(async (tx) => { - for (const vendor of vendorsResult.data) { - if (!vendor.companyId) continue - - // 1. rfqLastDetails 생성 (구매자 제시 조건) - const [rfqDetail] = await tx - .insert(rfqLastDetails) - .values({ - rfqsLastId: rfqId, - vendorsId: vendor.companyId, - currency: rfqConditions.currency, - paymentTermsCode: rfqConditions.paymentTermsCode || null, - incotermsCode: rfqConditions.incotermsCode || null, - incotermsDetail: rfqConditions.incotermsDetail || null, - deliveryDate: rfqConditions.deliveryDate || null, - taxCode: rfqConditions.taxCode || null, - placeOfShipping: rfqConditions.placeOfShipping || null, - placeOfDestination: rfqConditions.placeOfDestination || null, - materialPriceRelatedYn: rfqConditions.materialPriceRelatedYn, - sparepartYn: rfqConditions.sparepartYn, - sparepartDescription: rfqConditions.sparepartDescription || null, - updatedBy: input.updatedBy, - createdBy: input.createdBy, - isLatest: true, - }) - .returning() - - // 2. rfqLastVendorResponses 생성 (초기 응답 레코드) - const [vendorResponse] = await tx - .insert(rfqLastVendorResponses) - .values({ - rfqsLastId: rfqId, - rfqLastDetailsId: rfqDetail.id, - vendorId: vendor.companyId, - status: '대기중', - responseVersion: 1, - isLatest: true, - participationStatus: '미응답', - currency: rfqConditions.currency, - // 구매자 제시 조건을 벤더 제안 조건의 초기값으로 복사 - vendorCurrency: rfqConditions.currency, - vendorPaymentTermsCode: rfqConditions.paymentTermsCode || null, - vendorIncotermsCode: rfqConditions.incotermsCode || null, - vendorIncotermsDetail: rfqConditions.incotermsDetail || null, - vendorDeliveryDate: rfqConditions.vendorDeliveryDate || null, - vendorTaxCode: rfqConditions.taxCode || null, - vendorPlaceOfShipping: rfqConditions.placeOfShipping || null, - vendorPlaceOfDestination: rfqConditions.placeOfDestination || null, - vendorMaterialPriceRelatedYn: rfqConditions.materialPriceRelatedYn, - vendorSparepartYn: rfqConditions.sparepartYn, - vendorSparepartDescription: rfqConditions.sparepartDescription || null, - createdBy: input.createdBy, - updatedBy: input.updatedBy, - }) - .returning() - - // 3. 이력 기록 - await tx - .insert(rfqLastVendorResponseHistory) - .values({ - vendorResponseId: vendorResponse.id, - action: '생성', - newStatus: '대기중', - changeDetails: { - action: '사전견적용 일반견적 생성', - biddingId: input.biddingId, - conditions: rfqConditions, - }, - performedBy: input.createdBy, - }) - } - }) - - return { - success: true, - message: `사전견적용 일반견적이 성공적으로 생성되었습니다. (${vendorsResult.data.length}개 업체 추가)`, - data: { - rfqCode: result.data.rfqCode, - rfqId: result.data.id, - }, - } - } catch (error) { - console.error('Failed to create pre-quote RFQ:', error) - return { - success: false, - error: error instanceof Error ? error.message : '사전견적용 일반견적 생성에 실패했습니다', - } - } -} - // 일반견적 RFQ 코드 미리보기 (rfq-last/service에서 재사용) export async function previewGeneralRfqCode(picUserId: number): Promise { try { @@ -3404,8 +3205,65 @@ export async function getVendorContactsByVendorId(vendorId: number) { // bid-receive 페이지용 함수들 // ═══════════════════════════════════════════════════════════════ +// 입찰서 접수 기간 만료 체크 및 상태 업데이트 +export async function checkAndCloseExpiredBiddings() { + try { + const now = new Date() + + // 1. 기간이 만료되었는데 아직 진행중인 입찰 조회 + const expiredBiddings = await db + .select({ id: biddings.id }) + .from(biddings) + .where( + and( + or( + eq(biddings.status, 'bidding_opened') + ), + lte(biddings.submissionEndDate, now) + ) + ) + + if (expiredBiddings.length === 0) { + return + } + + const expiredBiddingIds = expiredBiddings.map(b => b.id) + + // 2. 입찰 상태를 '입찰마감(bidding_closed)'으로 변경 + await db + .update(biddings) + .set({ status: 'bidding_closed' }) + .where(inArray(biddings.id, expiredBiddingIds)) + + // 3. 최종 제출 버튼을 누르지 않은 벤더들의 가장 마지막 견적을 최종 제출로 처리 + // biddingCompanies 테이블에 이미 마지막 견적 정보가 저장되어 있다고 가정 + await db + .update(biddingCompanies) + .set({ + isFinalSubmission: true, + invitationStatus: 'bidding_submitted' // 상태도 최종 응찰로 변경 + }) + .where( + and( + inArray(biddingCompanies.biddingId, expiredBiddingIds), + eq(biddingCompanies.isFinalSubmission, false), + isNotNull(biddingCompanies.finalQuoteAmount) // 견적 금액이 있는 경우만 (참여한 경우) + ) + ) + + // 데이터 갱신을 위해 경로 재검증 + revalidatePath('/evcp/bid-receive') + + } catch (error) { + console.error('Error in checkAndCloseExpiredBiddings:', error) + } +} + // bid-receive: 입찰서접수및마감 페이지용 입찰 목록 조회 export async function getBiddingsForReceive(input: GetBiddingsSchema) { + // 조회 전 만료된 입찰 상태 업데이트 + await checkAndCloseExpiredBiddings() + try { const offset = (input.page - 1) * input.perPage diff --git a/lib/general-contracts/detail/general-contract-info-header.tsx b/lib/general-contracts/detail/general-contract-info-header.tsx index c0a79d09..c966685e 100644 --- a/lib/general-contracts/detail/general-contract-info-header.tsx +++ b/lib/general-contracts/detail/general-contract-info-header.tsx @@ -1,5 +1,6 @@ import { Building2, Package, DollarSign, Calendar, FileText } from 'lucide-react' import { formatDate } from '@/lib/utils' +import { GENERAL_CONTRACT_TYPE_LABELS, GeneralContractType } from '@/lib/general-contracts/types' interface GeneralContractInfoHeaderProps { contract: { @@ -45,24 +46,6 @@ const categoryLabels = { '매각계약': '매각계약' } -const typeLabels = { - 'UP': '자재단가계약', - 'LE': '임대차계약', - 'IL': '개별운송계약', - 'AL': '연간운송계약', - 'OS': '외주용역계약', - 'OW': '도급계약', - 'LO': 'LOI', - 'FA': 'FA', - 'SC': '납품합의계약', - 'OF': '클레임상계계약', - 'AW': '사전작업합의', - 'AD': '사전납품합의', - 'SG': '임치(물품보관)계약', - 'SR': '폐기물매각계약', - 'SP': 'S-PEpC' -} - export function GeneralContractInfoHeader({ contract }: GeneralContractInfoHeaderProps) { return (
@@ -137,7 +120,7 @@ export function GeneralContractInfoHeader({ contract }: GeneralContractInfoHeade
계약종류 - {typeLabels[contract.type as keyof typeof typeLabels] || contract.type} + {GENERAL_CONTRACT_TYPE_LABELS[contract.type as GeneralContractType] || contract.type}
diff --git a/lib/general-contracts/main/create-general-contract-dialog.tsx b/lib/general-contracts/main/create-general-contract-dialog.tsx index 8a506e4f..720192d8 100644 --- a/lib/general-contracts/main/create-general-contract-dialog.tsx +++ b/lib/general-contracts/main/create-general-contract-dialog.tsx @@ -22,6 +22,7 @@ import { createContract } from "@/lib/general-contracts/service" import { GENERAL_CONTRACT_CATEGORIES, GENERAL_CONTRACT_TYPES, + GENERAL_CONTRACT_TYPE_LABELS, GENERAL_EXECUTION_METHODS } from "@/lib/general-contracts/types" import { useSession } from "next-auth/react" @@ -209,26 +210,9 @@ export function CreateGeneralContractDialog() { {GENERAL_CONTRACT_TYPES.map((type) => { - const typeLabels = { - 'UP': '자재단가계약', - 'LE': '임대차계약', - 'IL': '개별운송계약', - 'AL': '연간운송계약', - 'OS': '외주용역계약', - 'OW': '도급계약', - 'LO': 'LOI', - 'FA': 'FA', - 'SC': '납품합의계약', - 'OF': '클레임상계계약', - 'AW': '사전작업합의', - 'AD': '사전납품합의', - 'SG': '임치(물품보관)계약', - 'SR': '폐기물매각계약', - 'SP': 'S-PEpC' - } return ( - {type} - {typeLabels[type as keyof typeof typeLabels]} + {type} - {GENERAL_CONTRACT_TYPE_LABELS[type]} ) })} diff --git a/lib/general-contracts/main/general-contracts-table-columns.tsx b/lib/general-contracts/main/general-contracts-table-columns.tsx index c43bb383..ce51b791 100644 --- a/lib/general-contracts/main/general-contracts-table-columns.tsx +++ b/lib/general-contracts/main/general-contracts-table-columns.tsx @@ -17,6 +17,7 @@ import { import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header" import { DataTableRowAction } from "@/types/table" import { formatDate } from "@/lib/utils" +import { GENERAL_CONTRACT_TYPE_LABELS, GeneralContractType, isGeneralContractType } from "@/lib/general-contracts/types" // 일반계약 리스트 아이템 타입 정의 export interface GeneralContractListItem { @@ -115,40 +116,10 @@ const getCategoryText = (category: string) => { // 계약종류 텍스트 변환 const getTypeText = (type: string) => { - switch (type) { - case 'UP': - return '자재단가계약' - case 'LE': - return '임대차계약' - case 'IL': - return '개별운송계약' - case 'AL': - return '연간운송계약' - case 'OS': - return '외주용역계약' - case 'OW': - return '도급계약' - case 'LO': - return 'LOI' - case 'FA': - return 'FA' - case 'SC': - return '납품합의계약' - case 'OF': - return '클레임상계계약' - case 'AW': - return '사전작업합의' - case 'AD': - return '사전납품합의' - case 'SG': - return '임치(물품보관)계약' - case 'SR': - return '폐기물매각계약' - case 'SP': - return 'S-PEpC' - default: - return type + if (isGeneralContractType(type)) { + return GENERAL_CONTRACT_TYPE_LABELS[type]; } + return type; } // 체결방식 텍스트 변환 diff --git a/lib/general-contracts/main/general-contracts-table.tsx b/lib/general-contracts/main/general-contracts-table.tsx index 5428435e..95bfe602 100644 --- a/lib/general-contracts/main/general-contracts-table.tsx +++ b/lib/general-contracts/main/general-contracts-table.tsx @@ -16,7 +16,8 @@ import { getGeneralContracts, getGeneralContractStatusCounts } from "@/lib/gener import { GeneralContractsTableToolbarActions } from "./general-contracts-table-toolbar-actions" import { GeneralContractUpdateSheet } from "./general-contract-update-sheet" import { - GENERAL_EXECUTION_METHODS + GENERAL_EXECUTION_METHODS, + GENERAL_CONTRACT_TYPE_LABELS } from "@/lib/general-contracts/types" // 상태 라벨 매핑 @@ -42,23 +43,7 @@ const contractCategoryLabels = { } // 계약종류 라벨 매핑 -const contractTypeLabels = { - 'UP': '자재단가계약', - 'LE': '임대차계약', - 'IL': '개별운송계약', - 'AL': '연간운송계약', - 'OS': '외주용역계약', - 'OW': '도급계약', - 'LO': 'LOI', - 'FA': 'FA', - 'SC': '납품합의계약', - 'OF': '클레임상계계약', - 'AW': '사전작업합의', - 'AD': '사전납품합의', - 'SG': '임치(물품보관)계약', - 'SR': '폐기물매각계약', - 'SP': 'S-PEpC' -} +const contractTypeLabels = GENERAL_CONTRACT_TYPE_LABELS; interface GeneralContractsTableProps { promises: Promise< diff --git a/lib/general-contracts/types.ts b/lib/general-contracts/types.ts index 6793d76c..9761b414 100644 --- a/lib/general-contracts/types.ts +++ b/lib/general-contracts/types.ts @@ -25,11 +25,35 @@ export const GENERAL_CONTRACT_TYPES = [ 'AD', // 사전납품합의 (Advanced Delivery) 'SG', // 임치(물품보관)계약 'SR', // 폐기물매각계약 (Scrap) - 'SP' // S-PEpC + 'SP', // S-PEpC + 'CC', // 소모품 단가계약 + 'DC', // 수출입가공 납품확인서 + 'SA' // 정산합의 ] as const; export type GeneralContractType = typeof GENERAL_CONTRACT_TYPES[number]; +export const GENERAL_CONTRACT_TYPE_LABELS: Record = { + 'UP': '자재단가계약', + 'LE': '임대차계약', + 'IL': '개별운송계약', + 'AL': '연간운송계약', + 'OS': '외주용역계약', + 'OW': '도급계약', + 'LO': 'LOI', + 'FA': 'FA', + 'SC': '납품합의계약', + 'OF': '클레임상계계약', + 'AW': '사전작업합의', + 'AD': '사전납품합의', + 'SG': '임치(물품보관)계약', + 'SR': '폐기물매각계약', + 'SP': 'S-PEpC', + 'CC': '소모품 단가계약', + 'DC': '수출입가공 납품확인서', + 'SA': '정산합의' +}; + // 3. 계약상태 export const GENERAL_CONTRACT_STATUSES = [ 'Draft', // 임시 저장 diff --git a/lib/rfq-last/quotation-compare-view.tsx b/lib/rfq-last/quotation-compare-view.tsx index be19f738..86ef4444 100644 --- a/lib/rfq-last/quotation-compare-view.tsx +++ b/lib/rfq-last/quotation-compare-view.tsx @@ -55,6 +55,7 @@ import { ComparisonData, selectVendor, cancelVendorSelection, VendorResponseVers import { createPO, createGeneralContract, createBidding } from "./contract-actions"; import { toast } from "sonner"; import { useRouter } from "next/navigation" +import { GENERAL_CONTRACT_TYPES, GENERAL_CONTRACT_TYPE_LABELS } from "@/lib/general-contracts/types"; interface QuotationCompareViewProps { data: ComparisonData; @@ -82,23 +83,10 @@ export function QuotationCompareView({ data }: QuotationCompareViewProps) { const [contractEndDate, setContractEndDate] = React.useState(""); // 계약종류 옵션 - const contractTypes = [ - { value: 'UP', label: 'UP - 자재단가계약' }, - { value: 'LE', label: 'LE - 임대차계약' }, - { value: 'IL', label: 'IL - 개별운송계약' }, - { value: 'AL', label: 'AL - 연간운송계약' }, - { value: 'OS', label: 'OS - 외주용역계약' }, - { value: 'OW', label: 'OW - 도급계약' }, - { value: 'IS', label: 'IS - 검사계약' }, - { value: 'LO', label: 'LO - LOI (의향서)' }, - { value: 'FA', label: 'FA - Frame Agreement' }, - { value: 'SC', label: 'SC - 납품합의계약' }, - { value: 'OF', label: 'OF - 클레임상계계약' }, - { value: 'AW', label: 'AW - 사전작업합의' }, - { value: 'AD', label: 'AD - 사전납품합의' }, - { value: 'AM', label: 'AM - 설계계약' }, - { value: 'SC_SELL', label: 'SC - 폐기물매각계약' }, - ]; + const contractTypes = GENERAL_CONTRACT_TYPES.map(type => ({ + value: type, + label: `${type} - ${GENERAL_CONTRACT_TYPE_LABELS[type]}` + })); // 입찰 관련 state 추가 const [biddingContractType, setBiddingContractType] = React.useState<"unit_price" | "general" | "sale">("unit_price"); diff --git a/lib/rfq-last/service.ts b/lib/rfq-last/service.ts index 7ebec795..68cfdac7 100644 --- a/lib/rfq-last/service.ts +++ b/lib/rfq-last/service.ts @@ -27,6 +27,37 @@ export async function getDefaultDueDate(): Promise { return defaultDueDate; } +export type Project = { + id: number; + projectCode: string; + projectName: string; + type: string; +} + +export async function getProjects(): Promise { + try { + // 트랜잭션을 사용하여 프로젝트 데이터 조회 + const projectList = await db.transaction(async (tx) => { + // 모든 프로젝트 조회 + const results = await tx + .select({ + id: projects.id, + projectCode: projects.code, // 테이블의 실제 컬럼명에 맞게 조정 + projectName: projects.name, // 테이블의 실제 컬럼명에 맞게 조정 + type: projects.type, // 테이블의 실제 컬럼명에 맞게 조정 + }) + .from(projects) + .orderBy(projects.code); + + return results; + }); + + return projectList; + } catch (error) { + console.error("프로젝트 목록 가져오기 실패:", error); + return []; // 오류 발생 시 빈 배열 반환 + } +} export async function getRfqs(input: GetRfqsSchema) { unstable_noStore(); @@ -47,6 +78,11 @@ export async function getRfqs(input: GetRfqsSchema) { typeFilter = like(rfqsLastView.rfqCode,'F%'); break; + case "pre_bidding": + // 사전견적(입찰): rfqCode가 B로 시작하는 경우 + typeFilter = + like(rfqsLastView.rfqCode,'B%'); + break; case "itb": // ITB: projectCompany가 있는 경우 typeFilter = @@ -106,6 +142,7 @@ export async function getRfqs(input: GetRfqsSchema) { ilike(rfqsLastView.projectName, s), ilike(rfqsLastView.rfqTitle, s), ilike(rfqsLastView.prNumber, s), + ilike(rfqsLastView.biddingNumber, s), ].filter(Boolean); if (searchConditions.length > 0) { diff --git a/lib/rfq-last/shared/rfq-items-dialog.tsx b/lib/rfq-last/shared/rfq-items-dialog.tsx index eed3d154..afed9576 100644 --- a/lib/rfq-last/shared/rfq-items-dialog.tsx +++ b/lib/rfq-last/shared/rfq-items-dialog.tsx @@ -345,7 +345,7 @@ export function RfqItemsDialog({ {item.materialCode || "-"} {item.acc && ( - ACC: {item.acc} + {item.acc} )}
diff --git a/lib/rfq-last/table/rfq-table-columns.tsx b/lib/rfq-last/table/rfq-table-columns.tsx index 62f14579..58c45aa0 100644 --- a/lib/rfq-last/table/rfq-table-columns.tsx +++ b/lib/rfq-last/table/rfq-table-columns.tsx @@ -24,7 +24,7 @@ type NextRouter = ReturnType; interface GetColumnsProps { setRowAction: React.Dispatch | null>>; - rfqCategory?: "general" | "itb" | "rfq"; + rfqCategory?: "general" | "itb" | "rfq" | "pre_bidding"; router: NextRouter; } @@ -755,6 +755,342 @@ export function getRfqColumns({ ]; } + // ═══════════════════════════════════════════════════════════════ + // 사전견적(입찰) 컬럼 정의 + // ═══════════════════════════════════════════════════════════════ + if (rfqCategory === "pre_bidding") { + return [ + // Checkbox + { + id: "select", + header: ({ table }) => ( + table.toggleAllPageRowsSelected(!!v)} + aria-label="select all" + className="translate-y-0.5" + /> + ), + cell: ({ row }) => ( + row.toggleSelected(!!v)} + aria-label="select row" + className="translate-y-0.5" + /> + ), + size: 40, + enableSorting: false, + enableHiding: false, + }, + + // 견적 No. + { + accessorKey: "rfqCode", + header: ({ column }) => , + cell: ({ row }) => ( + {row.original.rfqCode} + ), + size: 120, + }, + + // 입찰 No. (추가) + { + accessorKey: "biddingNumber", + header: ({ column }) => , + cell: ({ row }) => ( + {row.original.biddingNumber || "-"} + ), + size: 120, + }, + + // 상세 - 수정됨 + { + id: "detail", + header: "상세", + cell: ({ row }) => ( + + ), + size: 60, + }, + + // 견적상태 + { + accessorKey: "status", + header: ({ column }) => , + cell: ({ row }) => ( + + {row.original.status} + + ), + size: 120, + }, + + // 프로젝트 (프로젝트명) + { + accessorKey: "projectName", + header: ({ column }) => , + cell: ({ row }) => ( +
+ + {row.original.projectCode} + + + {row.original.projectName || "-"} + +
+ ), + size: 220, + }, + + // // 시리즈 + // { + // accessorKey: "series", + // header: ({ column }) => , + // cell: ({ row }) => { + // const series = row.original.series; + // if (!series) return "-"; + // const label = series === "SS" ? "시리즈 통합" : series === "II" ? "품목 통합" : series; + // return {label}; + // }, + // size: 100, + // }, + + // // 선급 + // { + // accessorKey: "classNo", + // header: ({ column }) => , + // cell: ({ row }) => row.original.classNo || "-", + // size: 80, + // }, + + // 견적명 + { + accessorKey: "rfqTitle", + header: ({ column }) => , + cell: ({ row }) => ( +
+ {row.original.rfqTitle || "-"} +
+ ), + size: 200, + }, + + // 자재그룹 (자재그룹명) + { + accessorKey: "majorItemMaterialDescription", + header: ({ column }) => , + cell: ({ row }) => ( +
+ + {row.original.majorItemMaterialCategory} + + + {row.original.majorItemMaterialDescription || "-"} + +
+ ), + size: 180, + }, + + // 자재코드 + { + accessorKey: "itemCode", + header: ({ column }) => , + cell: ({ row }) => ( + {row.original.itemCode || "-"} + ), + size: 100, + }, + // 자재명 + { + accessorKey: "itemName", + header: ({ column }) => , + cell: ({ row }) => ( + {row.original.itemName || "-"} + ), + size: 100, + }, + + // 견적 자료 + { + id: "rfqDocument", + header: ({ column }) => , + cell: ({ row }) => ( + + ), + size: 80, + }, + + // 견적품목수 + { + accessorKey: "prItemsCount", + header: ({ column }) => , + cell: ({ row }) => ( + + ), + size: 90, + }, + + // 견적생성일 + { + accessorKey: "createdAt", + header: ({ column }) => , + cell: ({ row }) => { + const date = row.original.createdAt; + return date ? format(new Date(date), "yyyy-MM-dd") : "-"; + }, + size: 100, + }, + + // 견적발송일 + { + accessorKey: "rfqSendDate", + header: ({ column }) => , + cell: ({ row }) => { + const date = row.original.rfqSendDate; + return date ? format(new Date(date), "yyyy-MM-dd") : "-"; + }, + size: 100, + }, + + // 견적마감일 + { + accessorKey: "dueDate", + header: ({ column }) => , + cell: ({ row }) => { + const date = row.original.dueDate; + if (!date) return "-"; + + const now = new Date(); + const dueDate = new Date(date); + const daysLeft = differenceInDays(dueDate, now); + + // 상태별 스타일과 아이콘 설정 + let statusIcon; + let statusText; + let statusClass; + + if (daysLeft < 0) { + const daysOverdue = Math.abs(daysLeft); + statusIcon = ; + statusText = `${daysOverdue}일 지남`; + statusClass = "text-red-600"; + } else if (daysLeft === 0) { + statusIcon = ; + statusText = "오늘 마감"; + statusClass = "text-orange-600"; + } else if (daysLeft <= 3) { + statusIcon = ; + statusText = `${daysLeft}일 남음`; + statusClass = "text-amber-600"; + } else if (daysLeft <= 7) { + statusIcon = ; + statusText = `${daysLeft}일 남음`; + statusClass = "text-blue-600"; + } else { + statusIcon = ; + statusText = `${daysLeft}일 남음`; + statusClass = "text-green-600"; + } + + return ( +
+ + {format(dueDate, "yyyy-MM-dd")} + +
+ {statusIcon} + {statusText} +
+
+ ); + }, + size: 120, + }, + + // 계약기간 (추가) + { + id: "contractPeriod", + header: ({ column }) => , + cell: ({ row }) => { + const start = row.original.contractStartDate; + const end = row.original.contractEndDate; + + if (!start && !end) return "-"; + + return ( +
+ {start ? format(new Date(start), "yyyy-MM-dd") : "미정"} + ~ + {end ? format(new Date(end), "yyyy-MM-dd") : "미정"} +
+ ); + }, + size: 120, + }, + + // 구매담당자 + { + accessorKey: "picUserName", + header: ({ column }) => , + cell: ({ row }) => { + const name = row.original.picUserName || row.original.picName || "-"; + const picCode = row.original.picCode || ""; + return name === "-" ? "-" : `${name}(${picCode})`; + }, + size: 100, + }, + + // 최종수정일 + { + accessorKey: "updatedAt", + header: ({ column }) => , + cell: ({ row }) => { + const date = row.original.updatedAt; + return date ? format(new Date(date), "yyyy-MM-dd HH:mm") : "-"; + }, + size: 120, + }, + + // 최종수정자 + { + accessorKey: "updatedByUserName", + header: ({ column }) => , + cell: ({ row }) => row.original.updatedByUserName || "-", + size: 100, + }, + + // 비고 + { + accessorKey: "remark", + header: ({ column }) => , + cell: ({ row }) => row.original.remark || "-", + size: 150, + }, + ]; + } + // ═══════════════════════════════════════════════════════════════ // 일반견적 컬럼 정의 // ═══════════════════════════════════════════════════════════════ diff --git a/lib/rfq-last/table/rfq-table.tsx b/lib/rfq-last/table/rfq-table.tsx index 80f1422e..e8dd299d 100644 --- a/lib/rfq-last/table/rfq-table.tsx +++ b/lib/rfq-last/table/rfq-table.tsx @@ -29,7 +29,7 @@ import { RfqItemsDialog } from "../shared/rfq-items-dialog"; interface RfqTableProps { data: Awaited>; - rfqCategory?: "general" | "itb" | "rfq"; + rfqCategory?: "general" | "itb" | "rfq" | "pre_bidding"; className?: string; } @@ -274,7 +274,7 @@ export function RfqTable({ { id: "vendorCount", label: "업체수", type: "number" }, { id: "dueDate", label: "마감일", type: "date" }, { id: "rfqSendDate", label: "발송일", type: "date" }, - ...(rfqCategory === "general" ? [ + ...(rfqCategory === "general" || rfqCategory === "pre_bidding" ? [ { id: "rfqType", label: "견적 유형", @@ -283,10 +283,14 @@ export function RfqTable({ { label: "단가계약", value: "단가계약" }, { label: "매각계약", value: "매각계약" }, { label: "일반계약", value: "일반계약" }, + ...(rfqCategory === "pre_bidding" ? [{ label: "사전견적(입찰)", value: "사전견적(입찰)" }] : []) ] }, { id: "rfqTitle", label: "견적 제목", type: "text" }, ] as DataTableAdvancedFilterField[] : []), + ...(rfqCategory === "pre_bidding" ? [ + { id: "biddingNumber", label: "입찰 No.", type: "text" }, + ] as DataTableAdvancedFilterField[] : []), ...(rfqCategory === "itb" ? [ { id: "smCode", label: "SM 코드", type: "text" }, ] as DataTableAdvancedFilterField[] : []), @@ -414,6 +418,7 @@ export function RfqTable({ {rfqCategory === "general" ? "일반견적" : + rfqCategory === "pre_bidding" ? "사전견적(입찰)" : rfqCategory === "itb" ? "ITB" : "RFQ"} diff --git a/lib/rfq-last/validations.ts b/lib/rfq-last/validations.ts index 6b39d52d..a7f9a405 100644 --- a/lib/rfq-last/validations.ts +++ b/lib/rfq-last/validations.ts @@ -35,12 +35,13 @@ import { RfqLastAttachments } from "@/db/schema"; export const RFQ_CATEGORY_OPTIONS = [ { value: "all", label: "전체" }, { value: "general", label: "일반견적" }, + { value: "pre_bidding", label: "사전견적(입찰)" }, { value: "itb", label: "ITB" }, { value: "rfq", label: "RFQ" }, ]; - + // ============= 메인 검색 파라미터 스키마 ============= - + export const searchParamsRfqCache = createSearchParamsCache({ flags: parseAsArrayOf(z.enum(["advancedTable", "floatingBar"])).withDefault([]), page: parseAsInteger.withDefault(1), @@ -56,7 +57,7 @@ import { RfqLastAttachments } from "@/db/schema"; search: parseAsString.withDefault(""), // RFQ 카테고리 (전체/일반견적/ITB/RFQ) - rfqCategory: parseAsStringEnum(["all", "general", "itb", "rfq"]), + rfqCategory: parseAsStringEnum(["all", "general", "pre_bidding", "itb", "rfq"]), }); // ============= 타입 정의 ============= diff --git a/lib/rfq-last/vendor-response/editor/quotation-items-table.tsx b/lib/rfq-last/vendor-response/editor/quotation-items-table.tsx index 281316eb..f8f11327 100644 --- a/lib/rfq-last/vendor-response/editor/quotation-items-table.tsx +++ b/lib/rfq-last/vendor-response/editor/quotation-items-table.tsx @@ -741,7 +741,7 @@ export default function QuotationItemsTable({ prItems, decimalPlaces = 2 }: Quot {prItem?.materialCode || "-"} {prItem?.acc && ( - ACC: {prItem.acc} + {prItem.acc} )}
-- cgit v1.2.3