From e5f4a774fabc17b5b18d50c96f5695d89dcabc86 Mon Sep 17 00:00:00 2001 From: joonhoekim <26rote@gmail.com> Date: Mon, 2 Jun 2025 02:27:56 +0000 Subject: (김준회) 기술영업 조선 RFQ 에러 처리 및 필터와 소팅 처리 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vendor-response/detail/project-info-tab.tsx | 9 +- .../vendor-response/detail/quotation-tabs.tsx | 2 +- .../vendor-response/quotation-editor.tsx | 6 +- .../table/vendor-quotations-table-columns.tsx | 255 +++++++++++++++++---- .../table/vendor-quotations-table.tsx | 239 ++++++++++++++++--- 5 files changed, 430 insertions(+), 81 deletions(-) (limited to 'lib/techsales-rfq/vendor-response') diff --git a/lib/techsales-rfq/vendor-response/detail/project-info-tab.tsx b/lib/techsales-rfq/vendor-response/detail/project-info-tab.tsx index 7ba3320d..e4b1b8c3 100644 --- a/lib/techsales-rfq/vendor-response/detail/project-info-tab.tsx +++ b/lib/techsales-rfq/vendor-response/detail/project-info-tab.tsx @@ -48,7 +48,7 @@ interface ProjectInfoTabProps { item?: { id: number itemCode: string | null - itemName: string | null + itemList: string | null } | null biddingProject?: { id: number @@ -74,6 +74,8 @@ export function ProjectInfoTab({ quotation }: ProjectInfoTabProps) { const projectSnapshot = rfq?.projectSnapshot const seriesSnapshot = rfq?.seriesSnapshot + console.log("rfq: ", rfq) + if (!rfq) { return (
@@ -112,8 +114,9 @@ export function ProjectInfoTab({ quotation }: ProjectInfoTabProps) {
{rfq.materialCode || "N/A"}
-
품목명
-
{rfq.item?.itemName || "N/A"}
+
자재명
+ {/* TODO : 타입 작업 (시연을 위해 빌드 중단 상태임. 추후 수정) */} +
{rfq.itemShipbuilding?.itemList || "N/A"}
마감일
diff --git a/lib/techsales-rfq/vendor-response/detail/quotation-tabs.tsx b/lib/techsales-rfq/vendor-response/detail/quotation-tabs.tsx index a800dd95..97bba2bd 100644 --- a/lib/techsales-rfq/vendor-response/detail/quotation-tabs.tsx +++ b/lib/techsales-rfq/vendor-response/detail/quotation-tabs.tsx @@ -56,7 +56,7 @@ interface QuotationData { item?: { id: number itemCode: string | null - itemName: string | null + itemList: string | null } | null biddingProject?: { id: number diff --git a/lib/techsales-rfq/vendor-response/quotation-editor.tsx b/lib/techsales-rfq/vendor-response/quotation-editor.tsx index f3fab10d..b30f612c 100644 --- a/lib/techsales-rfq/vendor-response/quotation-editor.tsx +++ b/lib/techsales-rfq/vendor-response/quotation-editor.tsx @@ -101,7 +101,7 @@ interface TechSalesVendorQuotation { item?: { id: number itemCode: string | null - itemName: string | null + itemList: string | null } | null biddingProject?: { id: number @@ -342,8 +342,8 @@ export default function TechSalesQuotationEditor({ quotation }: TechSalesQuotati

{quotation.rfq.materialCode || "N/A"}

- -

{quotation.rfq.item?.itemName || "N/A"}

+ +

{quotation.rfq.item?.itemList || "N/A"}

diff --git a/lib/techsales-rfq/vendor-response/table/vendor-quotations-table-columns.tsx b/lib/techsales-rfq/vendor-response/table/vendor-quotations-table-columns.tsx index 109698ea..cf1dac42 100644 --- a/lib/techsales-rfq/vendor-response/table/vendor-quotations-table-columns.tsx +++ b/lib/techsales-rfq/vendor-response/table/vendor-quotations-table-columns.tsx @@ -13,24 +13,46 @@ import { TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip" -import { DataTableColumnHeader } from "@/components/data-table/data-table-column-header" import { TechSalesVendorQuotations, TECH_SALES_QUOTATION_STATUS_CONFIG } from "@/db/schema" import { AppRouterInstance } from "next/dist/shared/lib/app-router-context.shared-runtime" +import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header" interface QuotationWithRfqCode extends TechSalesVendorQuotations { + // RFQ 관련 정보 rfqCode?: string; materialCode?: string; dueDate?: Date; rfqStatus?: string; + + // 아이템 정보 itemName?: string; + itemShipbuildingId?: number; + + // 프로젝트 정보 projNm?: string; + pspid?: string; + sector?: string; + + // 벤더 정보 + vendorName?: string; + vendorCode?: string; + + // 사용자 정보 + createdByName?: string | null; + updatedByName?: string | null; + + // 견적 코드 및 버전 quotationCode?: string | null; quotationVersion?: number | null; + + // 추가 상태 정보 rejectionReason?: string | null; acceptedAt?: Date | null; + + // 첨부파일 개수 attachmentCount?: number; } @@ -65,23 +87,23 @@ export function getColumns({ router, openAttachmentsSheet }: GetColumnsProps): C enableSorting: false, enableHiding: false, }, - { - accessorKey: "id", - header: ({ column }) => ( - - ), - cell: ({ row }) => ( -
- {row.getValue("id")} -
- ), - enableSorting: true, - enableHiding: true, - }, + // { + // accessorKey: "id", + // header: ({ column }) => ( + // + // ), + // cell: ({ row }) => ( + //
+ // {row.getValue("id")} + //
+ // ), + // enableSorting: true, + // enableHiding: true, + // }, { accessorKey: "rfqCode", header: ({ column }) => ( - + ), cell: ({ row }) => { const rfqCode = row.getValue("rfqCode") as string; @@ -94,26 +116,58 @@ export function getColumns({ router, openAttachmentsSheet }: GetColumnsProps): C enableSorting: true, enableHiding: false, }, - { - accessorKey: "materialCode", - header: ({ column }) => ( - - ), - cell: ({ row }) => { - const materialCode = row.getValue("materialCode") as string; - return ( -
- {materialCode || "N/A"} -
- ); - }, - enableSorting: true, - enableHiding: true, - }, + // { + // accessorKey: "vendorName", + // header: ({ column }) => ( + // + // ), + // cell: ({ row }) => { + // const vendorName = row.getValue("vendorName") as string; + // return ( + //
+ // {vendorName || "N/A"} + //
+ // ); + // }, + // enableSorting: true, + // enableHiding: false, + // }, + // { + // accessorKey: "vendorCode", + // header: ({ column }) => ( + // + // ), + // cell: ({ row }) => { + // const vendorCode = row.getValue("vendorCode") as string; + // return ( + //
+ // {vendorCode || "N/A"} + //
+ // ); + // }, + // enableSorting: true, + // enableHiding: true, + // }, + // { + // accessorKey: "materialCode", + // header: ({ column }) => ( + // + // ), + // cell: ({ row }) => { + // const materialCode = row.getValue("materialCode") as string; + // return ( + //
+ // {materialCode || "N/A"} + //
+ // ); + // }, + // enableSorting: true, + // enableHiding: true, + // }, { accessorKey: "itemName", header: ({ column }) => ( - + ), cell: ({ row }) => { const itemName = row.getValue("itemName") as string; @@ -134,13 +188,13 @@ export function getColumns({ router, openAttachmentsSheet }: GetColumnsProps): C
); }, - enableSorting: false, + enableSorting: true, enableHiding: true, }, { accessorKey: "projNm", header: ({ column }) => ( - + ), cell: ({ row }) => { const projNm = row.getValue("projNm") as string; @@ -161,13 +215,45 @@ export function getColumns({ router, openAttachmentsSheet }: GetColumnsProps): C ); }, - enableSorting: false, + enableSorting: true, enableHiding: true, }, + // { + // accessorKey: "quotationCode", + // header: ({ column }) => ( + // + // ), + // cell: ({ row }) => { + // const quotationCode = row.getValue("quotationCode") as string; + // return ( + //
+ // {quotationCode || "미부여"} + //
+ // ); + // }, + // enableSorting: true, + // enableHiding: true, + // }, + // { + // accessorKey: "quotationVersion", + // header: ({ column }) => ( + // + // ), + // cell: ({ row }) => { + // const quotationVersion = row.getValue("quotationVersion") as number; + // return ( + //
+ // {quotationVersion || 1} + //
+ // ); + // }, + // enableSorting: true, + // enableHiding: true, + // }, { id: "attachments", header: ({ column }) => ( - + ), cell: ({ row }) => { const quotation = row.original @@ -216,7 +302,7 @@ export function getColumns({ router, openAttachmentsSheet }: GetColumnsProps): C { accessorKey: "status", header: ({ column }) => ( - + ), cell: ({ row }) => { const status = row.getValue("status") as string; @@ -243,7 +329,7 @@ export function getColumns({ router, openAttachmentsSheet }: GetColumnsProps): C { accessorKey: "currency", header: ({ column }) => ( - + ), cell: ({ row }) => { const currency = row.getValue("currency") as string; @@ -259,7 +345,7 @@ export function getColumns({ router, openAttachmentsSheet }: GetColumnsProps): C { accessorKey: "totalPrice", header: ({ column }) => ( - + ), cell: ({ row }) => { const totalPrice = row.getValue("totalPrice") as string; @@ -287,7 +373,7 @@ export function getColumns({ router, openAttachmentsSheet }: GetColumnsProps): C { accessorKey: "validUntil", header: ({ column }) => ( - + ), cell: ({ row }) => { const validUntil = row.getValue("validUntil") as Date; @@ -305,7 +391,7 @@ export function getColumns({ router, openAttachmentsSheet }: GetColumnsProps): C { accessorKey: "submittedAt", header: ({ column }) => ( - + ), cell: ({ row }) => { const submittedAt = row.getValue("submittedAt") as Date; @@ -320,10 +406,28 @@ export function getColumns({ router, openAttachmentsSheet }: GetColumnsProps): C enableSorting: true, enableHiding: true, }, + // { + // accessorKey: "acceptedAt", + // header: ({ column }) => ( + // + // ), + // cell: ({ row }) => { + // const acceptedAt = row.getValue("acceptedAt") as Date; + // return ( + //
+ // + // {acceptedAt ? formatDateTime(acceptedAt) : "미승인"} + // + //
+ // ); + // }, + // enableSorting: true, + // enableHiding: true, + // }, { accessorKey: "dueDate", header: ({ column }) => ( - + ), cell: ({ row }) => { const dueDate = row.getValue("dueDate") as Date; @@ -340,10 +444,41 @@ export function getColumns({ router, openAttachmentsSheet }: GetColumnsProps): C enableSorting: true, enableHiding: true, }, + // { + // accessorKey: "rejectionReason", + // header: ({ column }) => ( + // + // ), + // cell: ({ row }) => { + // const rejectionReason = row.getValue("rejectionReason") as string; + // return ( + //
+ // {rejectionReason ? ( + // + // + // + // + // {rejectionReason} + // + // + // + //

{rejectionReason}

+ //
+ //
+ //
+ // ) : ( + // N/A + // )} + //
+ // ); + // }, + // enableSorting: false, + // enableHiding: true, + // }, { accessorKey: "createdAt", header: ({ column }) => ( - + ), cell: ({ row }) => { const createdAt = row.getValue("createdAt") as Date; @@ -361,7 +496,7 @@ export function getColumns({ router, openAttachmentsSheet }: GetColumnsProps): C { accessorKey: "updatedAt", header: ({ column }) => ( - + ), cell: ({ row }) => { const updatedAt = row.getValue("updatedAt") as Date; @@ -376,6 +511,38 @@ export function getColumns({ router, openAttachmentsSheet }: GetColumnsProps): C enableSorting: true, enableHiding: true, }, + // { + // accessorKey: "createdByName", + // header: ({ column }) => ( + // + // ), + // cell: ({ row }) => { + // const createdByName = row.getValue("createdByName") as string; + // return ( + //
+ // {createdByName || "N/A"} + //
+ // ); + // }, + // enableSorting: true, + // enableHiding: true, + // }, + // { + // accessorKey: "updatedByName", + // header: ({ column }) => ( + // + // ), + // cell: ({ row }) => { + // const updatedByName = row.getValue("updatedByName") as string; + // return ( + //
+ // {updatedByName || "N/A"} + //
+ // ); + // }, + // enableSorting: true, + // enableHiding: true, + // }, { id: "actions", header: "작업", diff --git a/lib/techsales-rfq/vendor-response/table/vendor-quotations-table.tsx b/lib/techsales-rfq/vendor-response/table/vendor-quotations-table.tsx index e1b82579..e98d6bdc 100644 --- a/lib/techsales-rfq/vendor-response/table/vendor-quotations-table.tsx +++ b/lib/techsales-rfq/vendor-response/table/vendor-quotations-table.tsx @@ -2,6 +2,7 @@ "use client" import * as React from "react" +import { useSearchParams } from "next/navigation" import { type DataTableAdvancedFilterField, type DataTableFilterField } from "@/types/table" import { useDataTable } from "@/hooks/use-data-table" import { DataTable } from "@/components/data-table/data-table" @@ -10,41 +11,192 @@ import { TechSalesVendorQuotations, TECH_SALES_QUOTATION_STATUSES, TECH_SALES_QU import { useRouter } from "next/navigation" import { getColumns } from "./vendor-quotations-table-columns" import { TechSalesRfqAttachmentsSheet, ExistingTechSalesAttachment } from "../../table/tech-sales-rfq-attachments-sheet" -import { getTechSalesRfqAttachments } from "@/lib/techsales-rfq/service" +import { getTechSalesRfqAttachments, getVendorQuotations } from "@/lib/techsales-rfq/service" import { toast } from "sonner" +import { Skeleton } from "@/components/ui/skeleton" interface QuotationWithRfqCode extends TechSalesVendorQuotations { - rfqCode?: string; - materialCode?: string; + rfqCode?: string | null; + materialCode?: string | null; dueDate?: Date; rfqStatus?: string; - itemName?: string; - projNm?: string; + itemName?: string | null; + projNm?: string | null; quotationCode?: string | null; - quotationVersion: number | null; + quotationVersion?: number | null; rejectionReason?: string | null; acceptedAt?: Date | null; attachmentCount?: number; } interface VendorQuotationsTableProps { - promises: Promise<[{ data: QuotationWithRfqCode[], pageCount: number, total?: number }]>; + vendorId: string; } -export function VendorQuotationsTable({ promises }: VendorQuotationsTableProps) { +// 로딩 스켈레톤 컴포넌트 +function TableLoadingSkeleton() { + return ( +
+ {/* 툴바 스켈레톤 */} +
+
+ + +
+
+ + +
+
+ + {/* 테이블 헤더 스켈레톤 */} +
+
+
+ + + + + + + +
+
+ + {/* 테이블 행 스켈레톤 */} + {Array.from({ length: 5 }).map((_, index) => ( +
+
+ + + + + + + +
+
+ ))} +
+ + {/* 페이지네이션 스켈레톤 */} +
+ +
+ + + +
+
+
+ ) +} - // TODO: 안정화 이후 삭제 - console.log("렌더링 사이클 점검용 로그: VendorQuotationsTable 렌더링됨"); +// 중앙 로딩 인디케이터 컴포넌트 +function CenterLoadingIndicator() { + return ( +
+
+
+
+
+

데이터를 불러오는 중...

+

잠시만 기다려주세요.

+
+
+ ) +} - const [{ data, pageCount }] = React.use(promises); - const router = useRouter(); +export function VendorQuotationsTable({ vendorId }: VendorQuotationsTableProps) { + const searchParams = useSearchParams() + const router = useRouter() // 첨부파일 시트 상태 const [attachmentsOpen, setAttachmentsOpen] = React.useState(false) const [selectedRfqForAttachments, setSelectedRfqForAttachments] = React.useState<{ id: number; rfqCode: string | null; status: string } | null>(null) const [attachmentsDefault, setAttachmentsDefault] = React.useState([]) - // 데이터 안정성을 위한 메모이제이션 - 핵심 속성만 비교 + // 데이터 로딩 상태 + const [data, setData] = React.useState([]) + const [pageCount, setPageCount] = React.useState(0) + const [total, setTotal] = React.useState(0) + const [isLoading, setIsLoading] = React.useState(true) + const [isInitialLoad, setIsInitialLoad] = React.useState(true) // 최초 로딩 구분 + + // URL 파라미터에서 설정 읽기 + const initialSettings = React.useMemo(() => ({ + page: parseInt(searchParams?.get('page') || '1'), + perPage: parseInt(searchParams?.get('perPage') || '10'), + sort: searchParams?.get('sort') ? JSON.parse(searchParams.get('sort')!) : [{ id: "updatedAt", desc: true }], + filters: searchParams?.get('filters') ? JSON.parse(searchParams.get('filters')!) : [], + joinOperator: (searchParams?.get('joinOperator') as "and" | "or") || "and", + basicFilters: searchParams?.get('basicFilters') ? JSON.parse(searchParams.get('basicFilters')!) : [], + basicJoinOperator: (searchParams?.get('basicJoinOperator') as "and" | "or") || "and", + search: searchParams?.get('search') || '', + from: searchParams?.get('from') || '', + to: searchParams?.get('to') || '', + }), [searchParams]) + + // 데이터 로드 함수 + const loadData = React.useCallback(async () => { + try { + setIsLoading(true) + + console.log('🔍 [VendorQuotationsTable] 데이터 로드 요청:', { + vendorId, + settings: initialSettings + }) + + const result = await getVendorQuotations({ + page: initialSettings.page, + perPage: initialSettings.perPage, + sort: initialSettings.sort, + filters: initialSettings.filters, + joinOperator: initialSettings.joinOperator, + basicFilters: initialSettings.basicFilters, + basicJoinOperator: initialSettings.basicJoinOperator, + search: initialSettings.search, + from: initialSettings.from, + to: initialSettings.to, + }, vendorId) + + console.log('🔍 [VendorQuotationsTable] 데이터 로드 결과:', { + dataLength: result.data.length, + pageCount: result.pageCount, + total: result.total + }) + + setData(result.data as QuotationWithRfqCode[]) + setPageCount(result.pageCount) + setTotal(result.total) + } catch (error) { + console.error('데이터 로드 오류:', error) + toast.error('데이터를 불러오는 중 오류가 발생했습니다.') + } finally { + setIsLoading(false) + setIsInitialLoad(false) + } + }, [vendorId, initialSettings]) + + // URL 파라미터 변경 감지 및 데이터 재로드 (초기 로드 포함) + React.useEffect(() => { + loadData() + }, [ + searchParams?.get('page'), + searchParams?.get('perPage'), + searchParams?.get('sort'), + searchParams?.get('filters'), + searchParams?.get('joinOperator'), + searchParams?.get('basicFilters'), + searchParams?.get('basicJoinOperator'), + searchParams?.get('search'), + searchParams?.get('from'), + searchParams?.get('to'), + // vendorId 변경도 감지 + vendorId + ]) + + // 데이터 안정성을 위한 메모이제이션 const stableData = React.useMemo(() => { return data; }, [data.length, data.map(item => `${item.id}-${item.status}-${item.updatedAt}`).join(',')]); @@ -95,13 +247,13 @@ export function VendorQuotationsTable({ promises }: VendorQuotationsTableProps) } }, [data]) - // 테이블 컬럼 정의 - router는 안정적이므로 한 번만 생성 + // 테이블 컬럼 정의 const columns = React.useMemo(() => getColumns({ router, openAttachmentsSheet, - }), [router, openAttachmentsSheet]); + }), [router, openAttachmentsSheet]) - // 필터 필드 - 중앙화된 상태 상수 사용 + // 필터 필드 const filterFields = React.useMemo[]>(() => [ { id: "status", @@ -121,9 +273,9 @@ export function VendorQuotationsTable({ promises }: VendorQuotationsTableProps) label: "자재 코드", placeholder: "자재 코드 검색...", } - ], []); + ], []) - // 고급 필터 필드 - 중앙화된 상태 상수 사용 + // 고급 필터 필드 const advancedFilterFields = React.useMemo[]>(() => [ { id: "rfqCode", @@ -154,20 +306,21 @@ export function VendorQuotationsTable({ promises }: VendorQuotationsTableProps) label: "제출일", type: "date", }, - ], []); + ], []) // useDataTable 훅 사용 const { table } = useDataTable({ data: stableData, columns, pageCount, + rowCount: total, filterFields, enablePinning: true, enableAdvancedFilter: true, enableColumnResizing: true, columnResizeMode: 'onChange', initialState: { - sorting: [{ id: "updatedAt", desc: true }], + sorting: initialSettings.sort, columnPinning: { right: ["actions"] }, }, getRowId: (originalRow) => String(originalRow.id), @@ -177,22 +330,48 @@ export function VendorQuotationsTable({ promises }: VendorQuotationsTableProps) minSize: 50, maxSize: 500, }, - }); + }) + + // 최초 로딩 시 전체 스켈레톤 표시 + if (isInitialLoad && isLoading) { + return ( +
+
+ +
+
+ ) + } return (
- - + {/* 로딩 오버레이 (재로딩 시) */} + {/* {!isInitialLoad && isLoading && ( +
+ +
+ )} */} + + -
-
+ + {!isInitialLoad && isLoading && ( +
+
+ 데이터 업데이트 중... +
+ )} + + +
{/* 첨부파일 관리 시트 (읽기 전용) */} -- cgit v1.2.3