diff options
| author | joonhoekim <26rote@gmail.com> | 2025-06-02 02:27:56 +0000 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-06-02 02:27:56 +0000 |
| commit | e5f4a774fabc17b5b18d50c96f5695d89dcabc86 (patch) | |
| tree | b1ef756d93f8e8d1d67998a5694aab379e34b5bc /lib/techsales-rfq/vendor-response | |
| parent | 37611339fea096e47aaa42311a13a6313b4200db (diff) | |
(김준회) 기술영업 조선 RFQ 에러 처리 및 필터와 소팅 처리
Diffstat (limited to 'lib/techsales-rfq/vendor-response')
5 files changed, 430 insertions, 81 deletions
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 ( <div className="flex items-center justify-center h-full"> @@ -112,8 +114,9 @@ export function ProjectInfoTab({ quotation }: ProjectInfoTabProps) { <div className="text-sm">{rfq.materialCode || "N/A"}</div> </div> <div className="space-y-2"> - <div className="text-sm font-medium text-muted-foreground">품목명</div> - <div className="text-sm">{rfq.item?.itemName || "N/A"}</div> + <div className="text-sm font-medium text-muted-foreground">자재명</div> + {/* TODO : 타입 작업 (시연을 위해 빌드 중단 상태임. 추후 수정) */} + <div className="text-sm"><strong>{rfq.itemShipbuilding?.itemList || "N/A"}</strong></div> </div> <div className="space-y-2"> <div className="text-sm font-medium text-muted-foreground">마감일</div> 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 <p>{quotation.rfq.materialCode || "N/A"}</p> </div> <div> - <label className="text-sm font-medium text-muted-foreground">품목명</label> - <p>{quotation.rfq.item?.itemName || "N/A"}</p> + <label className="text-sm font-medium text-muted-foreground">자재명</label> + <p>{quotation.rfq.item?.itemList || "N/A"}</p> </div> <div> <label className="text-sm font-medium text-muted-foreground">마감일</label> 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 }) => ( - <DataTableColumnHeader column={column} title="ID" /> - ), - cell: ({ row }) => ( - <div className="w-20"> - <span className="font-mono text-xs">{row.getValue("id")}</span> - </div> - ), - enableSorting: true, - enableHiding: true, - }, + // { + // accessorKey: "id", + // header: ({ column }) => ( + // <DataTableColumnHeaderSimple column={column} title="ID" /> + // ), + // cell: ({ row }) => ( + // <div className="w-20"> + // <span className="font-mono text-xs">{row.getValue("id")}</span> + // </div> + // ), + // enableSorting: true, + // enableHiding: true, + // }, { accessorKey: "rfqCode", header: ({ column }) => ( - <DataTableColumnHeader column={column} title="RFQ 번호" /> + <DataTableColumnHeaderSimple column={column} title="RFQ 번호" /> ), 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 }) => ( - <DataTableColumnHeader column={column} title="자재 코드" /> - ), - cell: ({ row }) => { - const materialCode = row.getValue("materialCode") as string; - return ( - <div className="min-w-32"> - <span className="font-mono text-sm">{materialCode || "N/A"}</span> - </div> - ); - }, - enableSorting: true, - enableHiding: true, - }, + // { + // accessorKey: "vendorName", + // header: ({ column }) => ( + // <DataTableColumnHeaderSimple column={column} title="벤더명" /> + // ), + // cell: ({ row }) => { + // const vendorName = row.getValue("vendorName") as string; + // return ( + // <div className="min-w-32"> + // <span className="text-sm">{vendorName || "N/A"}</span> + // </div> + // ); + // }, + // enableSorting: true, + // enableHiding: false, + // }, + // { + // accessorKey: "vendorCode", + // header: ({ column }) => ( + // <DataTableColumnHeaderSimple column={column} title="벤더 코드" /> + // ), + // cell: ({ row }) => { + // const vendorCode = row.getValue("vendorCode") as string; + // return ( + // <div className="min-w-24"> + // <span className="font-mono text-sm">{vendorCode || "N/A"}</span> + // </div> + // ); + // }, + // enableSorting: true, + // enableHiding: true, + // }, + // { + // accessorKey: "materialCode", + // header: ({ column }) => ( + // <DataTableColumnHeaderSimple column={column} title="자재 코드" /> + // ), + // cell: ({ row }) => { + // const materialCode = row.getValue("materialCode") as string; + // return ( + // <div className="min-w-32"> + // <span className="font-mono text-sm">{materialCode || "N/A"}</span> + // </div> + // ); + // }, + // enableSorting: true, + // enableHiding: true, + // }, { accessorKey: "itemName", header: ({ column }) => ( - <DataTableColumnHeader column={column} title="품목명" /> + <DataTableColumnHeaderSimple column={column} title="자재명" /> ), cell: ({ row }) => { const itemName = row.getValue("itemName") as string; @@ -134,13 +188,13 @@ export function getColumns({ router, openAttachmentsSheet }: GetColumnsProps): C </div> ); }, - enableSorting: false, + enableSorting: true, enableHiding: true, }, { accessorKey: "projNm", header: ({ column }) => ( - <DataTableColumnHeader column={column} title="프로젝트명" /> + <DataTableColumnHeaderSimple column={column} title="프로젝트명" /> ), cell: ({ row }) => { const projNm = row.getValue("projNm") as string; @@ -161,13 +215,45 @@ export function getColumns({ router, openAttachmentsSheet }: GetColumnsProps): C </div> ); }, - enableSorting: false, + enableSorting: true, enableHiding: true, }, + // { + // accessorKey: "quotationCode", + // header: ({ column }) => ( + // <DataTableColumnHeaderSimple column={column} title="견적서 번호" /> + // ), + // cell: ({ row }) => { + // const quotationCode = row.getValue("quotationCode") as string; + // return ( + // <div className="min-w-32"> + // <span className="font-mono text-sm">{quotationCode || "미부여"}</span> + // </div> + // ); + // }, + // enableSorting: true, + // enableHiding: true, + // }, + // { + // accessorKey: "quotationVersion", + // header: ({ column }) => ( + // <DataTableColumnHeaderSimple column={column} title="버전" /> + // ), + // cell: ({ row }) => { + // const quotationVersion = row.getValue("quotationVersion") as number; + // return ( + // <div className="w-16 text-center"> + // <span className="text-sm">{quotationVersion || 1}</span> + // </div> + // ); + // }, + // enableSorting: true, + // enableHiding: true, + // }, { id: "attachments", header: ({ column }) => ( - <DataTableColumnHeader column={column} title="첨부파일" /> + <DataTableColumnHeaderSimple column={column} title="첨부파일" /> ), cell: ({ row }) => { const quotation = row.original @@ -216,7 +302,7 @@ export function getColumns({ router, openAttachmentsSheet }: GetColumnsProps): C { accessorKey: "status", header: ({ column }) => ( - <DataTableColumnHeader column={column} title="상태" /> + <DataTableColumnHeaderSimple column={column} title="상태" /> ), cell: ({ row }) => { const status = row.getValue("status") as string; @@ -243,7 +329,7 @@ export function getColumns({ router, openAttachmentsSheet }: GetColumnsProps): C { accessorKey: "currency", header: ({ column }) => ( - <DataTableColumnHeader column={column} title="통화" /> + <DataTableColumnHeaderSimple column={column} title="통화" /> ), cell: ({ row }) => { const currency = row.getValue("currency") as string; @@ -259,7 +345,7 @@ export function getColumns({ router, openAttachmentsSheet }: GetColumnsProps): C { accessorKey: "totalPrice", header: ({ column }) => ( - <DataTableColumnHeader column={column} title="총액" /> + <DataTableColumnHeaderSimple column={column} title="총액" /> ), cell: ({ row }) => { const totalPrice = row.getValue("totalPrice") as string; @@ -287,7 +373,7 @@ export function getColumns({ router, openAttachmentsSheet }: GetColumnsProps): C { accessorKey: "validUntil", header: ({ column }) => ( - <DataTableColumnHeader column={column} title="유효기간" /> + <DataTableColumnHeaderSimple column={column} title="유효기간" /> ), cell: ({ row }) => { const validUntil = row.getValue("validUntil") as Date; @@ -305,7 +391,7 @@ export function getColumns({ router, openAttachmentsSheet }: GetColumnsProps): C { accessorKey: "submittedAt", header: ({ column }) => ( - <DataTableColumnHeader column={column} title="제출일" /> + <DataTableColumnHeaderSimple column={column} title="제출일" /> ), 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 }) => ( + // <DataTableColumnHeaderSimple column={column} title="승인일" /> + // ), + // cell: ({ row }) => { + // const acceptedAt = row.getValue("acceptedAt") as Date; + // return ( + // <div className="w-36"> + // <span className="text-sm"> + // {acceptedAt ? formatDateTime(acceptedAt) : "미승인"} + // </span> + // </div> + // ); + // }, + // enableSorting: true, + // enableHiding: true, + // }, { accessorKey: "dueDate", header: ({ column }) => ( - <DataTableColumnHeader column={column} title="마감일" /> + <DataTableColumnHeaderSimple column={column} title="마감일" /> ), 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 }) => ( + // <DataTableColumnHeaderSimple column={column} title="반려사유" /> + // ), + // cell: ({ row }) => { + // const rejectionReason = row.getValue("rejectionReason") as string; + // return ( + // <div className="min-w-48 max-w-64"> + // {rejectionReason ? ( + // <TooltipProvider> + // <Tooltip> + // <TooltipTrigger asChild> + // <span className="truncate block text-sm text-red-600"> + // {rejectionReason} + // </span> + // </TooltipTrigger> + // <TooltipContent> + // <p className="max-w-xs">{rejectionReason}</p> + // </TooltipContent> + // </Tooltip> + // </TooltipProvider> + // ) : ( + // <span className="text-sm text-muted-foreground">N/A</span> + // )} + // </div> + // ); + // }, + // enableSorting: false, + // enableHiding: true, + // }, { accessorKey: "createdAt", header: ({ column }) => ( - <DataTableColumnHeader column={column} title="생성일" /> + <DataTableColumnHeaderSimple column={column} title="생성일" /> ), cell: ({ row }) => { const createdAt = row.getValue("createdAt") as Date; @@ -361,7 +496,7 @@ export function getColumns({ router, openAttachmentsSheet }: GetColumnsProps): C { accessorKey: "updatedAt", header: ({ column }) => ( - <DataTableColumnHeader column={column} title="수정일" /> + <DataTableColumnHeaderSimple column={column} title="수정일" /> ), 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 }) => ( + // <DataTableColumnHeaderSimple column={column} title="생성자" /> + // ), + // cell: ({ row }) => { + // const createdByName = row.getValue("createdByName") as string; + // return ( + // <div className="w-24"> + // <span className="text-sm">{createdByName || "N/A"}</span> + // </div> + // ); + // }, + // enableSorting: true, + // enableHiding: true, + // }, + // { + // accessorKey: "updatedByName", + // header: ({ column }) => ( + // <DataTableColumnHeaderSimple column={column} title="수정자" /> + // ), + // cell: ({ row }) => { + // const updatedByName = row.getValue("updatedByName") as string; + // return ( + // <div className="w-24"> + // <span className="text-sm">{updatedByName || "N/A"}</span> + // </div> + // ); + // }, + // 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 ( + <div className="w-full space-y-3"> + {/* 툴바 스켈레톤 */} + <div className="flex items-center justify-between"> + <div className="flex items-center space-x-2"> + <Skeleton className="h-10 w-[250px]" /> + <Skeleton className="h-10 w-[100px]" /> + </div> + <div className="flex items-center space-x-2"> + <Skeleton className="h-10 w-[120px]" /> + <Skeleton className="h-10 w-[100px]" /> + </div> + </div> + + {/* 테이블 헤더 스켈레톤 */} + <div className="rounded-md border"> + <div className="border-b p-4"> + <div className="flex items-center space-x-4"> + <Skeleton className="h-4 w-[100px]" /> + <Skeleton className="h-4 w-[150px]" /> + <Skeleton className="h-4 w-[120px]" /> + <Skeleton className="h-4 w-[100px]" /> + <Skeleton className="h-4 w-[130px]" /> + <Skeleton className="h-4 w-[100px]" /> + <Skeleton className="h-4 w-[80px]" /> + </div> + </div> + + {/* 테이블 행 스켈레톤 */} + {Array.from({ length: 5 }).map((_, index) => ( + <div key={index} className="border-b p-4 last:border-b-0"> + <div className="flex items-center space-x-4"> + <Skeleton className="h-4 w-[100px]" /> + <Skeleton className="h-4 w-[150px]" /> + <Skeleton className="h-4 w-[120px]" /> + <Skeleton className="h-4 w-[100px]" /> + <Skeleton className="h-4 w-[130px]" /> + <Skeleton className="h-4 w-[100px]" /> + <Skeleton className="h-4 w-[80px]" /> + </div> + </div> + ))} + </div> + + {/* 페이지네이션 스켈레톤 */} + <div className="flex items-center justify-between"> + <Skeleton className="h-8 w-[200px]" /> + <div className="flex items-center space-x-2"> + <Skeleton className="h-8 w-[100px]" /> + <Skeleton className="h-8 w-[60px]" /> + <Skeleton className="h-8 w-[100px]" /> + </div> + </div> + </div> + ) +} - // TODO: 안정화 이후 삭제 - console.log("렌더링 사이클 점검용 로그: VendorQuotationsTable 렌더링됨"); +// 중앙 로딩 인디케이터 컴포넌트 +function CenterLoadingIndicator() { + return ( + <div className="flex flex-col items-center justify-center py-12 space-y-4"> + <div className="relative"> + <div className="w-12 h-12 border-4 border-gray-200 border-t-blue-600 rounded-full animate-spin"></div> + </div> + <div className="text-center space-y-1"> + <p className="text-sm font-medium text-gray-900">데이터를 불러오는 중...</p> + <p className="text-xs text-gray-500">잠시만 기다려주세요.</p> + </div> + </div> + ) +} - 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<ExistingTechSalesAttachment[]>([]) - // 데이터 안정성을 위한 메모이제이션 - 핵심 속성만 비교 + // 데이터 로딩 상태 + const [data, setData] = React.useState<QuotationWithRfqCode[]>([]) + 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<DataTableFilterField<QuotationWithRfqCode>[]>(() => [ { id: "status", @@ -121,9 +273,9 @@ export function VendorQuotationsTable({ promises }: VendorQuotationsTableProps) label: "자재 코드", placeholder: "자재 코드 검색...", } - ], []); + ], []) - // 고급 필터 필드 - 중앙화된 상태 상수 사용 + // 고급 필터 필드 const advancedFilterFields = React.useMemo<DataTableAdvancedFilterField<QuotationWithRfqCode>[]>(() => [ { 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 ( + <div className="w-full"> + <div className="overflow-x-auto"> + <TableLoadingSkeleton /> + </div> + </div> + ) + } return ( <div className="w-full"> <div className="overflow-x-auto"> - <DataTable - table={table} - className="min-w-full" - > - <DataTableAdvancedToolbar + <div className="relative"> + {/* 로딩 오버레이 (재로딩 시) */} + {/* {!isInitialLoad && isLoading && ( + <div className="absolute h-full w-full inset-0 bg-background/90 backdrop-blur-md z-10 flex items-center justify-center"> + <CenterLoadingIndicator /> + </div> + )} */} + + <DataTable table={table} - filterFields={advancedFilterFields} - shallow={false} + className="min-w-full" > - </DataTableAdvancedToolbar> - </DataTable> + <DataTableAdvancedToolbar + table={table} + filterFields={advancedFilterFields} + shallow={false} + > + {!isInitialLoad && isLoading && ( + <div className="flex items-center gap-2 text-sm text-muted-foreground"> + <div className="animate-spin h-4 w-4 border-2 border-current border-t-transparent rounded-full" /> + 데이터 업데이트 중... + </div> + )} + </DataTableAdvancedToolbar> + </DataTable> + </div> </div> {/* 첨부파일 관리 시트 (읽기 전용) */} |
