summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--lib/tech-project-avl/table/accepted-quotations-table-columns.tsx238
-rw-r--r--lib/tech-project-avl/table/accepted-quotations-table-toolbar-actions.tsx1
-rw-r--r--lib/techsales-rfq/service.ts128
3 files changed, 272 insertions, 95 deletions
diff --git a/lib/tech-project-avl/table/accepted-quotations-table-columns.tsx b/lib/tech-project-avl/table/accepted-quotations-table-columns.tsx
index dae19395..d38981f6 100644
--- a/lib/tech-project-avl/table/accepted-quotations-table-columns.tsx
+++ b/lib/tech-project-avl/table/accepted-quotations-table-columns.tsx
@@ -9,12 +9,20 @@ import {
} from "@/components/ui/dropdown-menu"
import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header"
+// RFQ 아이템 정보 타입
+export interface RfqItemInfo {
+ itemCode: string
+ workType: string
+ itemList: string
+ subItemList: string
+ shipTypes: string
+}
+
// Accepted Quotation 타입 정의
export interface AcceptedQuotationItem {
id: number
rfqId: number
vendorId: number
- quotationCode: string | null
quotationVersion: number | null
totalPrice: string | null
currency: string | null
@@ -44,6 +52,19 @@ export interface AcceptedQuotationItem {
projNm: string | null
pspid: string | null
sector: string | null
+
+ // RFQ 아이템 정보
+ rfqItems: RfqItemInfo[]
+
+ // 확장된 아이템 정보
+ itemIndex: number
+ totalItems: number
+ isExpanded: boolean
+ itemCode: string
+ workType: string
+ itemList: string
+ subItemList: string
+ shipTypes: string
}
export function getColumns(): ColumnDef<AcceptedQuotationItem>[] {
@@ -77,129 +98,89 @@ export function getColumns(): ColumnDef<AcceptedQuotationItem>[] {
}
// ----------------------------------------------------------------
- // 2) actions 컬럼 (Dropdown 메뉴)
- // ----------------------------------------------------------------
- // const actionsColumn: ColumnDef<AcceptedQuotationItem> = {
- // id: "actions",
- // cell: ({ row }) => (
- // <DropdownMenu>
- // <DropdownMenuTrigger asChild>
- // <Button
- // aria-label="Open menu"
- // variant="ghost"
- // className="flex size-8 p-0 data-[state=open]:bg-muted"
- // >
- // <Ellipsis className="size-4" aria-hidden="true" />
- // </Button>
- // </DropdownMenuTrigger>
- // <DropdownMenuContent align="end" className="w-40">
- // <DropdownMenuItem
- // onSelect={() => setRowAction({ row, type: "open" })}
- // >
- // 견적서 보기
- // </DropdownMenuItem>
- // </DropdownMenuContent>
- // </DropdownMenu>
- // ),
- // size: 40,
- // enableSorting: false,
- // enableHiding: false,
- // }
-
- // ----------------------------------------------------------------
// 3) 데이터 컬럼들 정의
// ----------------------------------------------------------------
const dataColumns: ColumnDef<AcceptedQuotationItem>[] = [
+ // 프로젝트 관련 컬럼
{
- accessorKey: "rfqCode",
+ accessorKey: "pspid",
header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="RFQ 코드" />
+ <DataTableColumnHeaderSimple column={column} title="프로젝트 코드" />
),
cell: ({ row }) => (
<div className="font-medium">
- {row.original.rfqCode || "-"}
+ {row.original.pspid || "-"}
</div>
),
enableSorting: true,
enableHiding: true,
meta: {
- excelHeader: "RFQ 코드",
+ excelHeader: "프로젝트 코드",
},
},
{
- accessorKey: "description",
+ accessorKey: "projNm",
header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="RFQ 설명" />
+ <DataTableColumnHeaderSimple column={column} title="프로젝트명" />
),
cell: ({ row }) => (
<div className="max-w-32 truncate">
- {row.original.description || "-"}
+ {row.original.projNm || "-"}
</div>
),
enableSorting: true,
enableHiding: true,
meta: {
- excelHeader: "RFQ 설명",
+ excelHeader: "프로젝트명",
},
},
+ // RFQ quotation 관련 컬럼
{
- accessorKey: "rfqType",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="RFQ 타입" />
- ),
- cell: ({ row }) => (
- <div>
- {row.original.rfqType || "-"}
- </div>
- ),
- enableSorting: true,
- },
- {
- accessorKey: "vendorName",
+ accessorKey: "rfqCode",
header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="업체명" />
+ <DataTableColumnHeaderSimple column={column} title="RFQ 코드" />
),
cell: ({ row }) => (
<div className="font-medium">
- {row.original.vendorName}
+ {row.original.rfqCode || "-"}
</div>
),
enableSorting: true,
enableHiding: true,
meta: {
- excelHeader: "업체명",
+ excelHeader: "RFQ 코드",
},
},
{
- accessorKey: "vendorCode",
+ accessorKey: "description",
header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="업체 코드" />
+ <DataTableColumnHeaderSimple column={column} title="RFQ 제목" />
),
cell: ({ row }) => (
- <div>
- {row.original.vendorCode || "-"}
+ <div className="max-w-32 truncate">
+ {row.original.description || "-"}
</div>
),
enableSorting: true,
enableHiding: true,
meta: {
- excelHeader: "업체 코드",
+ excelHeader: "RFQ 설명",
},
},
{
- accessorKey: "quotationCode",
+ accessorKey: "rfqType",
header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="견적서 코드" />
+ <DataTableColumnHeaderSimple column={column} title="RFQ 타입" />
),
cell: ({ row }) => (
<div>
- {row.original.quotationCode || "-"}
+ {row.original.rfqType || "-"}
</div>
),
enableSorting: true,
enableHiding: true,
meta: {
- excelHeader: "견적서 코드",
+ excelHeader: "RFQ 타입",
},
},
{
@@ -238,42 +219,43 @@ export function getColumns(): ColumnDef<AcceptedQuotationItem>[] {
excelHeader: "상태",
},
},
+ // 협력업체 관련 컬럼
{
- accessorKey: "projNm",
+ accessorKey: "vendorName",
header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="프로젝트명" />
+ <DataTableColumnHeaderSimple column={column} title="업체명" />
),
cell: ({ row }) => (
- <div className="max-w-32 truncate">
- {row.original.projNm || "-"}
+ <div className="font-medium">
+ {row.original.vendorName}
</div>
),
enableSorting: true,
enableHiding: true,
meta: {
- excelHeader: "프로젝트명",
+ excelHeader: "업체명",
},
},
{
- accessorKey: "materialCode",
+ accessorKey: "vendorCode",
header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="자재 코드" />
+ <DataTableColumnHeaderSimple column={column} title="업체 코드" />
),
cell: ({ row }) => (
- <div className="max-w-32 truncate">
- {row.original.materialCode || "-"}
+ <div>
+ {row.original.vendorCode || "-"}
</div>
),
enableSorting: true,
enableHiding: true,
meta: {
- excelHeader: "자재 코드",
+ excelHeader: "업체 코드",
},
},
{
accessorKey: "vendorCountry",
header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="국가" />
+ <DataTableColumnHeaderSimple column={column} title="업체 국가" />
),
cell: ({ row }) => (
<div>
@@ -283,39 +265,131 @@ export function getColumns(): ColumnDef<AcceptedQuotationItem>[] {
enableSorting: true,
enableHiding: true,
meta: {
- excelHeader: "국가",
+ excelHeader: "업체 국가",
+ },
+ },
+ // 아이템 관련 컬럼
+ {
+ accessorKey: "materialGroup",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="자재그룹" />
+ ),
+ cell: ({ row }) => {
+ return (
+ <div className="max-w-32 truncate">
+ {row.original.itemCode || "-"}
+ </div>
+ );
+ },
+ enableSorting: false,
+ enableHiding: true,
+ meta: {
+ excelHeader: "자재그룹",
+ },
+ },
+ {
+ accessorKey: "workType",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="공종" />
+ ),
+ cell: ({ row }) => {
+ return (
+ <div className="max-w-32 truncate">
+ {row.original.workType || "-"}
+ </div>
+ );
+ },
+ enableSorting: false,
+ enableHiding: true,
+ meta: {
+ excelHeader: "공종",
+ },
+ },
+ {
+ accessorKey: "shipType",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="선종" />
+ ),
+ cell: ({ row }) => {
+ return (
+ <div className="max-w-32 truncate">
+ {row.original.shipTypes || "-"}
+ </div>
+ );
+ },
+ enableSorting: false,
+ enableHiding: true,
+ meta: {
+ excelHeader: "선종",
+ },
+ },
+ {
+ accessorKey: "itemName",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="자재명" />
+ ),
+ cell: ({ row }) => {
+ return (
+ <div className="max-w-32 truncate">
+ {row.original.itemList || "-"}
+ </div>
+ );
+ },
+ enableSorting: false,
+ enableHiding: true,
+ meta: {
+ excelHeader: "자재명",
+ },
+ },
+ {
+ accessorKey: "itemDetail",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="자재명(상세)" />
+ ),
+ cell: ({ row }) => {
+ return (
+ <div className="max-w-32 truncate">
+ {row.original.subItemList || "-"}
+ </div>
+ );
+ },
+ enableSorting: false,
+ enableHiding: true,
+ meta: {
+ excelHeader: "자재명(상세)",
},
},
+ // metadata 관련 컬럼
{
accessorKey: "dueDate",
header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="마감일" />
+ <DataTableColumnHeaderSimple column={column} title="RFQ 마감일" />
),
cell: ({ row }) => (
<div>
- {row.original.dueDate ? formatDate(row.original.dueDate, "KR") : "-"}
+ {row.original.dueDate ? formatDate(row.original.dueDate) : "-"}
</div>
),
enableSorting: true,
enableHiding: true,
meta: {
- excelHeader: "마감일",
+ excelHeader: "RFQ 마감일",
},
},
{
accessorKey: "acceptedAt",
header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="승인일" />
+ <DataTableColumnHeaderSimple column={column} title="RFQ 승인일" />
),
cell: ({ row }) => (
<div>
- {row.original.acceptedAt ? formatDate(row.original.acceptedAt, "KR") : "-"}
+ {row.original.acceptedAt ? formatDate(row.original.acceptedAt) : "-"}
</div>
),
enableSorting: true,
enableHiding: true,
meta: {
- excelHeader: "승인일",
+ excelHeader: "RFQ 승인일",
},
},
]
diff --git a/lib/tech-project-avl/table/accepted-quotations-table-toolbar-actions.tsx b/lib/tech-project-avl/table/accepted-quotations-table-toolbar-actions.tsx
index ae9aea60..4e2eb578 100644
--- a/lib/tech-project-avl/table/accepted-quotations-table-toolbar-actions.tsx
+++ b/lib/tech-project-avl/table/accepted-quotations-table-toolbar-actions.tsx
@@ -34,7 +34,6 @@ export function AcceptedQuotationsTableToolbarActions({
<Download className="size-4" aria-hidden="true" />
<span className="hidden sm:inline">Excel 내보내기</span>
</Button>
-
{onRefresh && (
<Button
variant="outline"
diff --git a/lib/techsales-rfq/service.ts b/lib/techsales-rfq/service.ts
index e3543752..348d31ff 100644
--- a/lib/techsales-rfq/service.ts
+++ b/lib/techsales-rfq/service.ts
@@ -3640,7 +3640,6 @@ export async function getAcceptedTechSalesVendorQuotations(input: {
id: techSalesVendorQuotations.id,
rfqId: techSalesVendorQuotations.rfqId,
vendorId: techSalesVendorQuotations.vendorId,
- quotationCode: techSalesVendorQuotations.quotationCode,
quotationVersion: techSalesVendorQuotations.quotationVersion,
totalPrice: techSalesVendorQuotations.totalPrice,
currency: techSalesVendorQuotations.currency,
@@ -3680,20 +3679,126 @@ export async function getAcceptedTechSalesVendorQuotations(input: {
.limit(input.perPage)
.offset(offset);
- // 총 개수 조회
- const totalCount = await db
- .select({ count: count() })
- .from(techSalesVendorQuotations)
- .leftJoin(techSalesRfqs, eq(techSalesVendorQuotations.rfqId, techSalesRfqs.id))
- .leftJoin(sql`vendors`, eq(techSalesVendorQuotations.vendorId, sql`vendors.id`))
- .leftJoin(biddingProjects, eq(techSalesRfqs.biddingProjectId, biddingProjects.id))
- .where(whereCondition);
+ // RFQ 아이템 정보 조회
+ const rfqIds = [...new Set(data.map(item => item.rfqId))];
+ const rfqItemsMap = new Map();
+
+ if (rfqIds.length > 0) {
+ const rfqItems = await db
+ .select({
+ rfqId: techSalesRfqItems.rfqId,
+ itemType: techSalesRfqItems.itemType,
+ // 조선 아이템 정보
+ shipItemCode: itemShipbuilding.itemCode,
+ shipWorkType: itemShipbuilding.workType,
+ shipItemList: itemShipbuilding.itemList,
+ shipShipTypes: itemShipbuilding.shipTypes,
+ // 해양 TOP 아이템 정보
+ topItemCode: itemOffshoreTop.itemCode,
+ topWorkType: itemOffshoreTop.workType,
+ topItemList: itemOffshoreTop.itemList,
+ topSubItemList: itemOffshoreTop.subItemList,
+ // 해양 HULL 아이템 정보
+ hullItemCode: itemOffshoreHull.itemCode,
+ hullWorkType: itemOffshoreHull.workType,
+ hullItemList: itemOffshoreHull.itemList,
+ hullSubItemList: itemOffshoreHull.subItemList,
+ })
+ .from(techSalesRfqItems)
+ .leftJoin(itemShipbuilding, eq(techSalesRfqItems.itemShipbuildingId, itemShipbuilding.id))
+ .leftJoin(itemOffshoreTop, eq(techSalesRfqItems.itemOffshoreTopId, itemOffshoreTop.id))
+ .leftJoin(itemOffshoreHull, eq(techSalesRfqItems.itemOffshoreHullId, itemOffshoreHull.id))
+ .where(inArray(techSalesRfqItems.rfqId, rfqIds));
- const total = totalCount[0]?.count ?? 0;
+ // RFQ별로 아이템 정보 그룹화
+ rfqItems.forEach(item => {
+ if (!rfqItemsMap.has(item.rfqId)) {
+ rfqItemsMap.set(item.rfqId, []);
+ }
+
+ let itemInfo = {
+ itemCode: '',
+ workType: '',
+ itemList: '',
+ subItemList: '',
+ shipTypes: '',
+ };
+
+ switch (item.itemType) {
+ case 'SHIP':
+ itemInfo = {
+ itemCode: item.shipItemCode || '',
+ workType: item.shipWorkType || '',
+ itemList: item.shipItemList || '',
+ subItemList: '',
+ shipTypes: item.shipShipTypes || '',
+ };
+ break;
+ case 'TOP':
+ itemInfo = {
+ itemCode: item.topItemCode || '',
+ workType: item.topWorkType || '',
+ itemList: item.topItemList || '',
+ subItemList: item.topSubItemList || '',
+ shipTypes: '',
+ };
+ break;
+ case 'HULL':
+ itemInfo = {
+ itemCode: item.hullItemCode || '',
+ workType: item.hullWorkType || '',
+ itemList: item.hullItemList || '',
+ subItemList: item.hullSubItemList || '',
+ shipTypes: '',
+ };
+ break;
+ }
+
+ rfqItemsMap.get(item.rfqId).push(itemInfo);
+ });
+ }
+
+ // 각 RFQ-벤더 조합에 대해 아이템별로 별도 행 생성
+ const expandedData: any[] = [];
+
+ data.forEach(item => {
+ const rfqItems = rfqItemsMap.get(item.rfqId) || [];
+
+ if (rfqItems.length === 0) {
+ // 아이템이 없는 경우 기본 행 하나만 추가
+ expandedData.push({
+ ...item,
+ rfqItems: [],
+ itemIndex: 0,
+ totalItems: 0,
+ isExpanded: false,
+ });
+ } else {
+ // 각 아이템별로 별도 행 생성
+ rfqItems.forEach((rfqItem, index) => {
+ expandedData.push({
+ ...item,
+ rfqItems: [rfqItem], // 단일 아이템만 포함
+ itemIndex: index,
+ totalItems: rfqItems.length,
+ isExpanded: index === 0, // 첫 번째 아이템만 확장된 것으로 표시
+ // 아이템 정보를 직접 포함
+ itemCode: rfqItem.itemCode,
+ workType: rfqItem.workType,
+ itemList: rfqItem.itemList,
+ subItemList: rfqItem.subItemList,
+ shipTypes: rfqItem.shipTypes,
+ });
+ });
+ }
+ });
+
+ // 총 개수 조회 (확장된 데이터 기준)
+ const total = expandedData.length;
const pageCount = Math.ceil(total / input.perPage);
return {
- data,
+ data: expandedData,
pageCount,
total,
};
@@ -3703,7 +3808,6 @@ export async function getAcceptedTechSalesVendorQuotations(input: {
throw new Error(`Accepted quotations 조회 실패: ${getErrorMessage(error)}`);
}
}
-
export async function getBidProjects(pjtType: 'SHIP' | 'TOP' | 'HULL'): Promise<Project[]> {
try {
// 트랜잭션을 사용하여 프로젝트 데이터 조회