diff options
Diffstat (limited to 'lib/vendors')
| -rw-r--r-- | lib/vendors/rfq-history-table/rfq-history-table-columns.tsx | 129 | ||||
| -rw-r--r-- | lib/vendors/rfq-history-table/rfq-history-table.tsx | 172 | ||||
| -rw-r--r-- | lib/vendors/service.ts | 313 |
3 files changed, 409 insertions, 205 deletions
diff --git a/lib/vendors/rfq-history-table/rfq-history-table-columns.tsx b/lib/vendors/rfq-history-table/rfq-history-table-columns.tsx index 66ddee47..8054b128 100644 --- a/lib/vendors/rfq-history-table/rfq-history-table-columns.tsx +++ b/lib/vendors/rfq-history-table/rfq-history-table-columns.tsx @@ -3,7 +3,7 @@ import * as React from "react"
import { type DataTableRowAction } from "@/types/table"
import { type ColumnDef } from "@tanstack/react-table"
-import { Ellipsis } from "lucide-react"
+import { Ellipsis, MoreHorizontal } from "lucide-react"
import { toast } from "sonner"
import { getErrorMessage } from "@/lib/handle-error"
@@ -15,13 +15,6 @@ import { DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
- DropdownMenuRadioGroup,
- DropdownMenuRadioItem,
- DropdownMenuSeparator,
- DropdownMenuShortcut,
- DropdownMenuSub,
- DropdownMenuSubContent,
- DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { DataTableColumnHeader } from "@/components/data-table/data-table-column-header"
@@ -30,7 +23,6 @@ import { VendorItem, vendors } from "@/db/schema/vendors" import { modifyVendor } from "../service"
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header"
-import { getRFQStatusIcon } from "@/lib/tasks/utils"
import { rfqHistoryColumnsConfig } from "@/config/rfqHistoryColumnsConfig"
export interface RfqHistoryRow {
@@ -61,13 +53,13 @@ export interface RfqHistoryRow { interface GetColumnsProps {
setRowAction: React.Dispatch<React.SetStateAction<DataTableRowAction<RfqHistoryRow> | null>>;
- openItemsModal: (rfqId: number) => void;
+ onViewDetails: (rfqId: number) => void;
}
/**
* tanstack table 컬럼 정의 (중첩 헤더 버전)
*/
-export function getColumns({ setRowAction, openItemsModal }: GetColumnsProps): ColumnDef<RfqHistoryRow>[] {
+export function getColumns({ setRowAction, onViewDetails }: GetColumnsProps): ColumnDef<RfqHistoryRow>[] {
// ----------------------------------------------------------------
// 1) select 컬럼 (체크박스)
// ----------------------------------------------------------------
@@ -112,14 +104,14 @@ export function getColumns({ setRowAction, openItemsModal }: GetColumnsProps): C variant="ghost"
className="flex size-8 p-0 data-[state=open]:bg-muted"
>
- <Ellipsis className="size-4" aria-hidden="true" />
+ <span className="text-lg">⋯</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-40">
<DropdownMenuItem
- onSelect={() => setRowAction({ row, type: "update" })}
+ onSelect={() => onViewDetails(row.original.id)}
>
- View Details
+ 견적상세
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
@@ -129,30 +121,60 @@ export function getColumns({ setRowAction, openItemsModal }: GetColumnsProps): C }
// ----------------------------------------------------------------
- // 3) 일반 컬럼들
+ // 3) 개별 컬럼들 (그룹 없음)
// ----------------------------------------------------------------
- const basicColumns: ColumnDef<RfqHistoryRow>[] = rfqHistoryColumnsConfig.map((cfg) => {
+ const basicColumns1: ColumnDef<RfqHistoryRow>[] = []
+ const quotationGroupColumns: ColumnDef<RfqHistoryRow>[] = []
+ const basicColumns2: ColumnDef<RfqHistoryRow>[] = []
+
+ const sortableIds = new Set([
+ "rfqType",
+ "status",
+ "rfqCode",
+ "projectInfo",
+ "packageInfo",
+ "materialInfo",
+ "currency",
+ "totalAmount",
+ "leadTime",
+ "paymentTerms",
+ "incoterms",
+ "shippingLocation",
+ "contractInfo",
+ "rfqSendDate",
+ "submittedAt",
+ "picName",
+ ])
+
+ rfqHistoryColumnsConfig.forEach((cfg) => {
+ const isSortable = sortableIds.has(cfg.id)
const column: ColumnDef<RfqHistoryRow> = {
accessorKey: cfg.id,
header: ({ column }) => (
<DataTableColumnHeaderSimple column={column} title={cfg.label} />
),
+ meta: {
+ excelHeader: cfg.excelHeader,
+ group: cfg.group,
+ type: cfg.type,
+ },
size: cfg.size,
+ enableSorting: isSortable,
}
- if (cfg.id === "description") {
+ if (cfg.id === "materialInfo") {
column.cell = ({ row }) => {
- const description = row.original.description
- if (!description) return null
+ const materialInfo = row.original.materialInfo
+ if (!materialInfo) return null
return (
<Tooltip>
<TooltipTrigger asChild>
<div className="break-words whitespace-normal line-clamp-2">
- {description}
+ {materialInfo}
</div>
</TooltipTrigger>
<TooltipContent side="bottom" className="max-w-[400px] whitespace-pre-wrap break-words">
- {description}
+ {materialInfo}
</TooltipContent>
</Tooltip>
)
@@ -163,10 +185,8 @@ export function getColumns({ setRowAction, openItemsModal }: GetColumnsProps): C column.cell = ({ row }) => {
const statusVal = row.original.status
if (!statusVal) return null
- const Icon = getRFQStatusIcon(statusVal)
return (
- <div className="flex items-center">
- <Icon className="mr-2 size-4 text-muted-foreground" aria-hidden="true" />
+ <div className="whitespace-nowrap">
<span className="capitalize">{statusVal}</span>
</div>
)
@@ -176,48 +196,57 @@ export function getColumns({ setRowAction, openItemsModal }: GetColumnsProps): C if (cfg.id === "totalAmount") {
column.cell = ({ row }) => {
const amount = row.original.totalAmount
- const currency = row.original.currency
- if (!amount || !currency) return null
+ if (!amount) return null
return (
<div className="whitespace-nowrap">
- {`${currency} ${amount.toLocaleString()}`}
+ {amount.toLocaleString()}
</div>
)
}
}
- if (cfg.id === "dueDate" || cfg.id === "createdAt") {
- column.cell = ({ row }) => (
- <div className="whitespace-nowrap">
- {formatDate(row.getValue(cfg.id), "KR")}
- </div>
- )
+ if (cfg.id === "rfqSendDate" || cfg.id === "submittedAt") {
+ column.cell = ({ row }) => {
+ const v = row.getValue(cfg.id) as Date | null
+ if (!v) return <div className="whitespace-nowrap">-</div>
+ return (
+ <div className="whitespace-nowrap">{formatDate(v, "KR")}</div>
+ )
+ }
+ }
+
+ // 컬럼을 적절한 배열에 분류
+ if (cfg.group === "견적정보") {
+ quotationGroupColumns.push(column)
+ } else if (["contractInfo", "rfqSendDate", "submittedAt", "picName"].includes(cfg.id)) {
+ basicColumns2.push(column)
+ } else {
+ basicColumns1.push(column)
}
return column
})
- const itemsColumn: ColumnDef<RfqHistoryRow> = {
- id: "items",
- header: "Items",
- cell: ({ row }) => {
- const rfq = row.original;
- const count = rfq.itemCount || 0;
- return (
- <Button variant="ghost" onClick={() => openItemsModal(rfq.id)}>
- {count === 0 ? "No Items" : `${count} Items`}
- </Button>
- )
- },
- }
-
// ----------------------------------------------------------------
- // 4) 최종 컬럼 배열
+ // 4) 최종 컬럼 배열 (bid-history-table-columns.tsx 방식)
// ----------------------------------------------------------------
+ const createGroupColumn = (groupName: string, columns: ColumnDef<RfqHistoryRow>[]): ColumnDef<RfqHistoryRow> => {
+ return {
+ id: `group-${groupName.replace(/\s+/g, '-')}`,
+ header: groupName,
+ columns: columns,
+ meta: {
+ isGroupColumn: true,
+ groupBorders: true,
+ } as any
+ }
+ }
+
return [
selectColumn,
- ...basicColumns,
- itemsColumn,
+ ...basicColumns1,
+ ...(quotationGroupColumns.length > 0 ? [createGroupColumn("견적정보", quotationGroupColumns)] : []),
+ ...basicColumns2,
actionsColumn,
]
}
\ No newline at end of file diff --git a/lib/vendors/rfq-history-table/rfq-history-table.tsx b/lib/vendors/rfq-history-table/rfq-history-table.tsx index 71830303..11a4bf9d 100644 --- a/lib/vendors/rfq-history-table/rfq-history-table.tsx +++ b/lib/vendors/rfq-history-table/rfq-history-table.tsx @@ -14,26 +14,38 @@ import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-adv import { getColumns } from "./rfq-history-table-columns" import { getRfqHistory } from "../service" import { RfqHistoryTableToolbarActions } from "./rfq-history-table-toolbar-actions" -import { RfqItemsTableDialog } from "./rfq-items-table-dialog" import { getRFQStatusIcon } from "@/lib/tasks/utils" import { TooltipProvider } from "@/components/ui/tooltip" +import { useRouter } from "next/navigation" export interface RfqHistoryRow { id: number; + rfqType: string | null; + status: string; rfqCode: string | null; + projectInfo: string | null; + packageInfo: string | null; + materialInfo: string | null; + // 견적정보 세부 필드들 + currency: string | null; + totalAmount: number | null; + leadTime: string | null; + paymentTerms: string | null; + incoterms: string | null; + shippingLocation: string | null; + contractInfo: string | null; + rfqSendDate: Date | null; + submittedAt: Date | null; + picName: string | null; + // 기존 필드들 (호환성을 위해 유지) projectCode: string | null; projectName: string | null; description: string | null; dueDate: Date; - status: "DRAFT" | "PUBLISHED" | "EVALUATION" | "AWARDED"; vendorStatus: string; - totalAmount: number | null; - currency: string | null; - leadTime: string | null; itemCount: number; tbeResult: string | null; cbeResult: string | null; - createdAt: Date; items: { rfqId: number; id: number; @@ -42,6 +54,7 @@ export interface RfqHistoryRow { quantity: number | null; uom: string | null; }[]; + actions?: any; // actions 컬럼용 } interface RfqHistoryTableProps { @@ -50,68 +63,117 @@ interface RfqHistoryTableProps { Awaited<ReturnType<typeof getRfqHistory>>, ] > + lng: string + vendorId: number } -export function VendorRfqHistoryTable({ promises }: RfqHistoryTableProps) { - const [{ data, pageCount }] = React.use(promises) +export function VendorRfqHistoryTable({ promises, lng, vendorId }: RfqHistoryTableProps) { + const [{ data = [], pageCount = 0 }] = React.use(promises) + const router = useRouter() - const [rowAction, setRowAction] = React.useState<DataTableRowAction<RfqHistoryRow> | null>(null) + const [, setRowAction] = React.useState<DataTableRowAction<RfqHistoryRow> | null>(null) - const [itemsModalOpen, setItemsModalOpen] = React.useState(false); - const [selectedRfq, setSelectedRfq] = React.useState<RfqHistoryRow | null>(null); - - const openItemsModal = React.useCallback((rfqId: number) => { - const rfq = data.find(r => r.id === rfqId); - if (rfq) { - setSelectedRfq(rfq); - setItemsModalOpen(true); - } - }, [data]); + const onViewDetails = React.useCallback((rfqId: number) => { + router.push(`/${lng}/evcp/rfq-last/${rfqId}`); + }, [router, lng]); const columns = React.useMemo(() => getColumns({ setRowAction, - openItemsModal, - }), [setRowAction, openItemsModal]); + onViewDetails, + }), [setRowAction, onViewDetails]); const filterFields: DataTableFilterField<RfqHistoryRow>[] = [ { - id: "rfqCode", - label: "RFQ Code", - placeholder: "Filter RFQ Code...", + id: "rfqType", + label: "견적종류", + options: [ + { label: "ITB", value: "ITB" }, + { label: "RFQ", value: "RFQ" }, + { label: "일반", value: "일반" }, + { label: "정기견적", value: "정기견적" } + ], }, { id: "status", - label: "Status", - options: ["DRAFT", "PUBLISHED", "EVALUATION", "AWARDED"].map((status) => ({ - label: toSentenceCase(status), - value: status, - icon: getRFQStatusIcon(status), - })), + label: "견적상태", + options: [ + { label: "ITB 발송", value: "ITB 발송" }, + { label: "Short List 확정", value: "Short List 확정" }, + { label: "최종업체선정", value: "최종업체선정" }, + { label: "견적접수", value: "견적접수" }, + { label: "견적평가중", value: "견적평가중" }, + { label: "견적완료", value: "견적완료" } + ], }, + { id: "rfqCode", label: "견적번호", placeholder: "견적번호로 검색..." }, + { id: "projectInfo", label: "프로젝트", placeholder: "프로젝트로 검색..." }, + { id: "packageInfo", label: "PKG No.", placeholder: "PKG로 검색..." }, + { id: "materialInfo", label: "자재그룹", placeholder: "자재그룹으로 검색..." }, { - id: "vendorStatus", - label: "Vendor Status", - placeholder: "Filter Vendor Status...", - } + id: "currency", + label: "통화", + options: [ + { label: "USD", value: "USD" }, + { label: "KRW", value: "KRW" }, + { label: "EUR", value: "EUR" }, + { label: "JPY", value: "JPY" } + ], + }, + { id: "paymentTerms", label: "지급조건", placeholder: "지급조건으로 검색..." }, + { id: "incoterms", label: "Incoterms", placeholder: "Incoterms로 검색..." }, + { id: "shippingLocation", label: "선적지", placeholder: "선적지로 검색..." }, + { id: "picName", label: "견적담당자", placeholder: "담당자로 검색..." }, ] const advancedFilterFields: DataTableAdvancedFilterField<RfqHistoryRow>[] = [ - { id: "rfqCode", label: "RFQ Code", type: "text" }, - { id: "projectCode", label: "Project Code", type: "text" }, - { id: "projectName", label: "Project Name", type: "text" }, - { - id: "status", - label: "RFQ Status", + { + id: "rfqType", + label: "견적종류", + type: "multi-select", + options: [ + { label: "ITB", value: "ITB" }, + { label: "RFQ", value: "RFQ" }, + { label: "일반", value: "일반" }, + { label: "정기견적", value: "정기견적" } + ], + }, + { + id: "status", + label: "견적상태", + type: "multi-select", + options: [ + { label: "ITB 발송", value: "ITB 발송" }, + { label: "Short List 확정", value: "Short List 확정" }, + { label: "최종업체선정", value: "최종업체선정" }, + { label: "견적접수", value: "견적접수" }, + { label: "견적평가중", value: "견적평가중" }, + { label: "견적완료", value: "견적완료" } + ], + }, + { id: "rfqCode", label: "견적번호", type: "text" }, + { id: "projectInfo", label: "프로젝트", type: "text" }, + { id: "packageInfo", label: "PKG No.", type: "text" }, + { id: "materialInfo", label: "자재그룹", type: "text" }, + { + id: "currency", + label: "통화", type: "multi-select", - options: ["DRAFT", "PUBLISHED", "EVALUATION", "AWARDED"].map((status) => ({ - label: toSentenceCase(status), - value: status, - icon: getRFQStatusIcon(status), - })), + options: [ + { label: "USD", value: "USD" }, + { label: "KRW", value: "KRW" }, + { label: "EUR", value: "EUR" }, + { label: "JPY", value: "JPY" } + ], }, - { id: "vendorStatus", label: "Vendor Status", type: "text" }, - { id: "dueDate", label: "Due Date", type: "date" }, - { id: "createdAt", label: "Created At", type: "date" }, + { id: "totalAmount", label: "총 견적금액", type: "number" }, + { id: "leadTime", label: "업체 L/T(W)", type: "text" }, + { id: "paymentTerms", label: "지급조건", type: "text" }, + { id: "incoterms", label: "Incoterms", type: "text" }, + { id: "shippingLocation", label: "선적지", type: "text" }, + { id: "contractInfo", label: "PO/계약정보", type: "text" }, + { id: "rfqSendDate", label: "견적요청일", type: "date" }, + { id: "submittedAt", label: "견적회신일", type: "date" }, + { id: "picName", label: "견적담당자", type: "text" }, ] const { table } = useDataTable({ @@ -122,13 +184,13 @@ export function VendorRfqHistoryTable({ promises }: RfqHistoryTableProps) { enablePinning: true, enableAdvancedFilter: true, initialState: { - sorting: [{ id: "createdAt", desc: true }], + sorting: [{ id: "rfqSendDate", desc: true }], columnPinning: { right: ["actions"] }, }, - getRowId: (originalRow) => String(originalRow.id), - shallow: true, + getRowId: (originalRow, index) => originalRow?.id ? String(originalRow.id) : String(index), + shallow: false, clearOnDefault: true, - }) + }); return ( <> @@ -141,15 +203,9 @@ export function VendorRfqHistoryTable({ promises }: RfqHistoryTableProps) { filterFields={advancedFilterFields} shallow={false} > - <RfqHistoryTableToolbarActions table={table} /> </DataTableAdvancedToolbar> </DataTable> - <RfqItemsTableDialog - open={itemsModalOpen} - onOpenChange={setItemsModalOpen} - items={selectedRfq?.items ?? []} - /> </TooltipProvider> </> ) diff --git a/lib/vendors/service.ts b/lib/vendors/service.ts index 9362a88c..596a52a0 100644 --- a/lib/vendors/service.ts +++ b/lib/vendors/service.ts @@ -9,7 +9,8 @@ import crypto from 'crypto'; import { v4 as uuidv4 } from 'uuid'; import { saveDRMFile } from "@/lib/file-stroage"; import { decryptWithServerAction } from "@/components/drm/drmUtils"; - +import { rfqLastVendorResponses, vendorQuotationView } from "@/db/schema/rfqVendor"; +import { rfqsLast, rfqLastDetails } from "@/db/schema/rfqLast"; import { filterColumns } from "@/lib/filter-columns"; import { unstable_cache } from "@/lib/unstable-cache"; import { getErrorMessage } from "@/lib/handle-error"; @@ -1213,117 +1214,235 @@ const removeVendormaterialsSchema = z.object({ }) - export async function getRfqHistory(input: GetRfqHistorySchema, vendorId: number) { - return unstable_cache( - async () => { - try { - logger.info({ vendorId, input }, "Starting getRfqHistory"); + try { + logger.info({ vendorId, input }, "Starting getRfqHistory"); - const offset = (input.page - 1) * input.perPage; + const offset = (input.page - 1) * input.perPage; - // 기본 where 조건 (vendorId) - const vendorWhere = eq(vendorRfqView.vendorId, vendorId); - logger.debug({ vendorWhere }, "Vendor where condition"); + // 기본 where 조건 (vendorId) + const vendorWhere = eq(rfqLastVendorResponses.vendorId, vendorId); + logger.debug({ vendorWhere }, "Vendor where condition"); - // 고급 필터링 - const advancedWhere = filterColumns({ - table: vendorRfqView, - filters: input.filters, - joinOperator: input.joinOperator, - }); - logger.debug({ advancedWhere }, "Advanced where condition"); + // 고급 필터링 + const advancedWhere = filterColumns({ + table: rfqsLast, + filters: input.filters, + joinOperator: input.joinOperator, + customColumnMapping: { + projectCode: { table: projects, column: "code" }, + projectName: { table: projects, column: "name" }, + projectInfo: { table: projects, column: "code" }, + packageInfo: { table: rfqsLast, column: "packageNo" }, + currency: { table: rfqLastVendorResponses, column: "vendorCurrency" }, + totalAmount: { table: rfqLastVendorResponses, column: "totalAmount" }, + paymentTerms: { table: rfqLastVendorResponses, column: "vendorPaymentTermsCode" }, + incoterms: { table: rfqLastVendorResponses, column: "vendorIncotermsCode" }, + shippingLocation: { table: rfqLastVendorResponses, column: "vendorPlaceOfShipping" }, + leadTime: { table: rfqLastVendorResponses, column: "vendorDeliveryDate" }, + contractInfo: { table: rfqLastDetails, column: "contractNo" }, + rfqSendDate: { table: rfqsLast, column: "rfqSendDate" }, + submittedAt: { table: rfqLastVendorResponses, column: "submittedAt" }, + }, + }); + logger.debug({ advancedWhere }, "Advanced where condition"); - // 글로벌 검색 - let globalWhere; - if (input.search) { - const s = `%${input.search}%`; - globalWhere = or( - ilike(vendorRfqView.rfqCode, s), - ilike(vendorRfqView.projectCode, s), - ilike(vendorRfqView.projectName, s) - ); - logger.debug({ globalWhere, search: input.search }, "Global search condition"); - } + // 글로벌 검색 + let globalWhere; + if (input.search) { + const s = `%${input.search}%`; + globalWhere = or( + ilike(rfqsLast.rfqCode, s), + ilike(projects.code, s), + ilike(projects.name, s), + ilike(rfqsLast.rfqType, s), + ilike(rfqsLast.status, s) + ); + logger.debug({ globalWhere, search: input.search }, "Global search condition"); + } - const finalWhere = and( - advancedWhere, - globalWhere, - vendorWhere - ); - logger.debug({ finalWhere }, "Final where condition"); + const finalWhere = and( + advancedWhere, + globalWhere, + vendorWhere + ); + logger.debug({ finalWhere }, "Final where condition"); - // 정렬 조건 - const orderBy = - input.sort.length > 0 - ? input.sort.map((item) => - item.desc ? desc(rfqs[item.id]) : asc(rfqs[item.id]) - ) - : [desc(rfqs.createdAt)]; - logger.debug({ orderBy }, "Order by condition"); + // 정렬 조건 - 동적 매핑 + const sortFieldMap: Record<string, any> = { + rfqType: rfqsLast.rfqType, + status: rfqsLast.status, + rfqCode: rfqsLast.rfqCode, + projectInfo: projects.code, + projectCode: projects.code, + projectName: projects.name, + packageNo: rfqsLast.packageNo, + packageName: rfqsLast.packageName, + packageInfo: rfqsLast.packageNo, + materialInfo: projects.code, + majorItemMaterialCategory: sql<string | null>`COALESCE( + (SELECT material_category FROM rfq_pr_items WHERE rfqs_last_id = ${rfqsLast.id} AND major_yn = true LIMIT 1), + (SELECT material_category FROM rfq_pr_items WHERE rfqs_last_id = ${rfqsLast.id} LIMIT 1) + )`, + majorItemMaterialDescription: sql<string | null>`COALESCE( + (SELECT material_description FROM rfq_pr_items WHERE rfqs_last_id = ${rfqsLast.id} AND major_yn = true LIMIT 1), + (SELECT material_description FROM rfq_pr_items WHERE rfqs_last_id = ${rfqsLast.id} LIMIT 1) + )`, + currency: rfqLastVendorResponses.vendorCurrency, + totalAmount: rfqLastVendorResponses.totalAmount, + leadTime: rfqLastVendorResponses.vendorDeliveryDate, + paymentTerms: rfqLastVendorResponses.vendorPaymentTermsCode, + incoterms: rfqLastVendorResponses.vendorIncotermsCode, + shippingLocation: rfqLastVendorResponses.vendorPlaceOfShipping, + contractInfo: rfqLastDetails.contractNo, + rfqSendDate: rfqsLast.rfqSendDate, + submittedAt: rfqLastVendorResponses.submittedAt, + picName: rfqsLast.picName, + }; - // 트랜잭션으로 데이터 조회 - const { data, total } = await db.transaction(async (tx) => { - logger.debug("Starting transaction for RFQ history query"); + const orderBy = + input.sort.length > 0 + ? input.sort.map((item) => { + const field = sortFieldMap[item.id]; + if (!field) { + logger.warn({ sortField: item.id }, "Unknown sort field, using default"); + return desc(rfqsLast.rfqSendDate); + } + return item.desc ? desc(field) : asc(field); + }) + : [desc(rfqsLast.rfqSendDate)]; + logger.debug({ orderBy }, "Order by condition"); - const data = await selectRfqHistory(tx, { - where: finalWhere, - orderBy, - offset, - limit: input.perPage, - }); - logger.debug({ dataLength: data.length }, "RFQ history data fetched"); + // 트랜잭션으로 데이터 조회 + const { data, total } = await db.transaction(async (tx) => { + logger.debug("Starting transaction for RFQ history query"); - // RFQ 아이템 정보 조회 - const rfqIds = data.map(rfq => rfq.id); - const items = await tx - .select({ - rfqId: rfqItems.rfqId, - id: rfqItems.id, - itemCode: rfqItems.itemCode, - description: rfqItems.description, - quantity: rfqItems.quantity, - uom: rfqItems.uom, - }) - .from(rfqItems) - .where(inArray(rfqItems.rfqId, rfqIds)); + // RFQ History 데이터 조회 - rfqsLast 기준으로 조인 (bid-history와 동일한 패턴) + const rfqHistoryData = await tx + .select({ + id: rfqsLast.id, + rfqType: rfqsLast.rfqType, + status: rfqsLast.status, + rfqCode: rfqsLast.rfqCode, + projectCode: projects.code, + projectName: projects.name, + packageNo: rfqsLast.packageNo, + packageName: rfqsLast.packageName, + majorItemMaterialCategory: sql<string | null>`COALESCE( + (SELECT material_category FROM rfq_pr_items WHERE rfqs_last_id = ${rfqsLast.id} AND major_yn = true LIMIT 1), + (SELECT material_category FROM rfq_pr_items WHERE rfqs_last_id = ${rfqsLast.id} LIMIT 1) + )`, + majorItemMaterialDescription: sql<string | null>`COALESCE( + (SELECT material_description FROM rfq_pr_items WHERE rfqs_last_id = ${rfqsLast.id} AND major_yn = true LIMIT 1), + (SELECT material_description FROM rfq_pr_items WHERE rfqs_last_id = ${rfqsLast.id} LIMIT 1) + )`, + vendorCurrency: rfqLastVendorResponses.vendorCurrency, + totalAmount: rfqLastVendorResponses.totalAmount, + vendorPaymentTermsCode: rfqLastVendorResponses.vendorPaymentTermsCode, + vendorIncotermsCode: rfqLastVendorResponses.vendorIncotermsCode, + vendorPlaceOfShipping: rfqLastVendorResponses.vendorPlaceOfShipping, + vendorDeliveryDate: rfqLastVendorResponses.vendorDeliveryDate, + vendorContractDuration: rfqLastVendorResponses.vendorContractDuration, + contractNo: rfqLastDetails.contractNo, + contractStatus: rfqLastDetails.contractStatus, + contractCreatedAt: rfqLastDetails.contractCreatedAt, + paymentTermsCode: rfqLastDetails.paymentTermsCode, + incotermsCode: rfqLastDetails.incotermsCode, + placeOfShipping: rfqLastDetails.placeOfShipping, + rfqSendDate: rfqsLast.rfqSendDate, + submittedAt: rfqLastVendorResponses.submittedAt, + picName: rfqsLast.picName, + responseStatus: rfqLastVendorResponses.status, + responseVersion: rfqLastVendorResponses.responseVersion, + }) + .from(rfqsLast) + .leftJoin(rfqLastVendorResponses, and( + eq(rfqLastVendorResponses.rfqsLastId, rfqsLast.id), + eq(rfqLastVendorResponses.vendorId, vendorId) + )) + .leftJoin(rfqLastDetails, eq(rfqLastVendorResponses.rfqLastDetailsId, rfqLastDetails.id)) + .leftJoin(projects, eq(rfqsLast.projectId, projects.id)) + .where(and( + advancedWhere, + globalWhere + )) + .orderBy(...orderBy) + .limit(input.perPage) + .offset(offset); - // RFQ 데이터에 아이템 정보 추가 - const dataWithItems = data.map(rfq => ({ - ...rfq, - items: items.filter(item => item.rfqId === rfq.id), - })); + logger.debug({ dataLength: rfqHistoryData.length }, "RFQ history data fetched"); + + // 데이터 변환 + const data = rfqHistoryData.map(row => ({ + id: row.id, + rfqType: row.rfqType, + status: row.status, + rfqCode: ((): string | null => { + if (!row.rfqCode) return null; + const rev = row.responseVersion ? ` (Rev.${row.responseVersion})` : ''; + return `${row.rfqCode}${rev}`; + })(), + projectInfo: row.projectCode && row.projectName ? `${row.projectCode} (${row.projectName})` : row.projectCode || row.projectName, + packageInfo: row.packageNo && row.packageName ? `${row.packageNo} (${row.packageName})` : row.packageNo || row.packageName, + materialInfo: row.majorItemMaterialCategory && row.majorItemMaterialDescription ? `${row.majorItemMaterialCategory} (${row.majorItemMaterialDescription})` : row.majorItemMaterialCategory || row.majorItemMaterialDescription, + // 견적정보 세부 필드들 + currency: row.vendorCurrency, + totalAmount: row.totalAmount, + leadTime: row.vendorDeliveryDate ?? row.vendorContractDuration ?? null, + paymentTerms: row.vendorPaymentTermsCode ?? row.paymentTermsCode ?? null, + incoterms: row.vendorIncotermsCode ?? row.incotermsCode ?? null, + shippingLocation: row.vendorPlaceOfShipping ?? row.placeOfShipping ?? null, + contractInfo: ((): string | null => { + const parts: string[] = []; + if (row.contractNo) parts.push(String(row.contractNo)); + if (row.contractStatus) parts.push(String(row.contractStatus)); + if (row.contractCreatedAt) parts.push(new Date(row.contractCreatedAt).toISOString().split('T')[0]); + return parts.length ? parts.join(' / ') : null; + })(), + rfqSendDate: row.rfqSendDate, + submittedAt: row.submittedAt, + picName: row.picName, + vendorStatus: row.responseStatus ?? '미응답' + })); + + // Total count 조회 - rfqsLast 기준으로 조인 (bid-history와 동일한 패턴) + const total = await tx + .select({ count: sql<number>`count(*)` }) + .from(rfqsLast) + .leftJoin(rfqLastVendorResponses, and( + eq(rfqLastVendorResponses.rfqsLastId, rfqsLast.id), + eq(rfqLastVendorResponses.vendorId, vendorId) + )) + .leftJoin(rfqLastDetails, eq(rfqLastVendorResponses.rfqLastDetailsId, rfqLastDetails.id)) + .leftJoin(projects, eq(rfqsLast.projectId, projects.id)) + .where(and( + advancedWhere, + globalWhere + )); - const total = await countRfqHistory(tx, finalWhere); - logger.debug({ total }, "RFQ history total count"); + const totalCount = total[0]?.count ?? 0; + logger.debug({ totalCount }, "RFQ history total count"); - return { data: dataWithItems, total }; - }); + return { data, total: totalCount }; + }); - const pageCount = Math.ceil(total / input.perPage); - logger.info({ - vendorId, - dataLength: data.length, - total, - pageCount - }, "RFQ history query completed"); + const pageCount = Math.ceil(total / input.perPage); + logger.info({ + vendorId, + dataLength: data.length, + total, + pageCount + }, "RFQ history query completed"); - return { data, pageCount }; - } catch (err) { - logger.error({ - err, - vendorId, - stack: err instanceof Error ? err.stack : undefined - }, 'Error fetching RFQ history'); - return { data: [], pageCount: 0 }; - } - }, - [JSON.stringify({ input, vendorId })], - { - revalidate: 3600, - tags: ["rfq-history"], - } - )(); + return { data, pageCount }; + } catch (err) { + logger.error({ + err, + vendorId, + stack: err instanceof Error ? err.stack : undefined + }, 'Error fetching RFQ history'); + return { data: [], pageCount: 0 }; + } } export async function checkJoinPortal(taxID: string) { |
