diff options
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/procurement-rfqs/services.ts | 7 | ||||
| -rw-r--r-- | lib/procurement-rfqs/table/rfq-table copy.tsx | 209 | ||||
| -rw-r--r-- | lib/procurement-rfqs/table/rfq-table.tsx | 258 | ||||
| -rw-r--r-- | lib/procurement-rfqs/vendor-response/table/vendor-quotations-table-columns.tsx | 282 | ||||
| -rw-r--r-- | lib/procurement-rfqs/vendor-response/table/vendor-quotations-table.tsx | 31 | ||||
| -rw-r--r-- | lib/utils.ts | 84 |
6 files changed, 711 insertions, 160 deletions
diff --git a/lib/procurement-rfqs/services.ts b/lib/procurement-rfqs/services.ts index 7179b213..facdc9c9 100644 --- a/lib/procurement-rfqs/services.ts +++ b/lib/procurement-rfqs/services.ts @@ -1654,11 +1654,14 @@ export async function getVendorQuotations(input: GetQuotationsSchema, vendorId: offset, limit: perPage, with: { - rfq: true, + rfq: { + with: { + item: true, // 여기서 item 정보도 가져옴 + } + }, vendor: true, } }); - // 전체 개수 조회 const { totalCount } = await db .select({ totalCount: count() }) diff --git a/lib/procurement-rfqs/table/rfq-table copy.tsx b/lib/procurement-rfqs/table/rfq-table copy.tsx new file mode 100644 index 00000000..510f474d --- /dev/null +++ b/lib/procurement-rfqs/table/rfq-table copy.tsx @@ -0,0 +1,209 @@ +"use client" + +import * as React from "react" +import type { + DataTableAdvancedFilterField, + DataTableFilterField, + DataTableRowAction, +} from "@/types/table" +import { useDataTable } from "@/hooks/use-data-table" +import { DataTable } from "@/components/data-table/data-table" +import { getColumns, EditingCellState } from "./rfq-table-column" +import { useEffect } from "react" +import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-advanced-toolbar" +import { RFQTableToolbarActions } from "./rfq-table-toolbar-actions" +import { ProcurementRfqsView } from "@/db/schema" +import { getPORfqs } from "../services" +import { toast } from "sonner" +import { updateRfqRemark } from "@/lib/procurement-rfqs/services" // 구현 필요 + +interface RFQListTableProps { + data?: Awaited<ReturnType<typeof getPORfqs>>; + onSelectRFQ?: (rfq: ProcurementRfqsView | null) => void; + // 데이터 새로고침을 위한 콜백 추가 + onDataRefresh?: () => void; + maxHeight?: string | number; // Add this prop +} + +// 보다 유연한 타입 정의 +type LocalDataType = Awaited<ReturnType<typeof getPORfqs>>; + +export function RFQListTable({ + data, + onSelectRFQ, + onDataRefresh, + maxHeight +}: RFQListTableProps) { + const [rowAction, setRowAction] = React.useState<DataTableRowAction<ProcurementRfqsView> | null>(null) + // 인라인 에디팅을 위한 상태 추가 + const [editingCell, setEditingCell] = React.useState<EditingCellState | null>(null) + // 로컬 데이터를 관리하기 위한 상태 추가 + const [localData, setLocalData] = React.useState<LocalDataType>(data || { data: [], pageCount: 0, total: 0 }); + + // 데이터가 변경될 때 로컬 데이터도 업데이트 + useEffect(() => { + setLocalData(data || { data: [], pageCount: 0, total: 0 }) + }, [data]) + + + // 비고 업데이트 함수 + const updateRemark = async (rfqId: number, remark: string) => { + try { + // 낙관적 UI 업데이트 (로컬 데이터 먼저 갱신) + if (localData && localData.data) { + // 로컬 데이터에서 해당 행 찾기 + const rowIndex = localData.data.findIndex(row => row.id === rfqId); + if (rowIndex >= 0) { + // 불변성을 유지하면서 로컬 데이터 업데이트 + const newData = [...localData.data]; + newData[rowIndex] = { ...newData[rowIndex], remark }; + + // 전체 데이터 구조 복사하여 업데이트 + setLocalData({ ...localData, data: newData } as typeof localData); + } + } + + const result = await updateRfqRemark(rfqId, remark); + + if (result.success) { + toast.success("비고가 업데이트되었습니다"); + + // 서버 데이터 리프레시 호출 + if (onDataRefresh) { + onDataRefresh(); + } + } else { + toast.error(result.message || "업데이트 중 오류가 발생했습니다"); + } + } catch (error) { + console.error("비고 업데이트 오류:", error); + toast.error("업데이트 중 오류가 발생했습니다"); + } + } + + // 행 액션 처리 + useEffect(() => { + if (rowAction) { + // 액션 유형에 따라 처리 + switch (rowAction.type) { + case "select": + // 선택된 문서 처리 + if (onSelectRFQ) { + onSelectRFQ(rowAction.row.original) + } + break; + case "update": + // 업데이트 처리 로직 + console.log("Update rfq:", rowAction.row.original) + break; + case "delete": + // 삭제 처리 로직 + console.log("Delete rfq:", rowAction.row.original) + break; + } + + // 액션 처리 후 rowAction 초기화 + setRowAction(null) + } + }, [rowAction, onSelectRFQ]) + + const columns = React.useMemo( + () => getColumns({ + setRowAction, + editingCell, + setEditingCell, + updateRemark + }), + [setRowAction, editingCell, setEditingCell, updateRemark] + ) + + + // Filter fields + const filterFields: DataTableFilterField<ProcurementRfqsView>[] = [] + + const advancedFilterFields: DataTableAdvancedFilterField<ProcurementRfqsView>[] = [ + { + id: "rfqCode", + label: "RFQ No.", + type: "text", + }, + { + id: "projectCode", + label: "프로젝트", + type: "text", + }, + { + id: "itemCode", + label: "자재그룹", + type: "text", + }, + { + id: "itemName", + label: "자재명", + type: "text", + }, + + { + id: "rfqSealedYn", + label: "RFQ 밀봉여부", + type: "text", + }, + { + id: "majorItemMaterialCode", + label: "자재코드", + type: "text", + }, + { + id: "rfqSendDate", + label: "RFQ 전송일", + type: "date", + }, + { + id: "dueDate", + label: "RFQ 마감일", + type: "date", + }, + { + id: "createdByUserName", + label: "요청자", + type: "text", + }, + ] + + // useDataTable 훅으로 react-table 구성 - 로컬 데이터 사용하도록 수정 + const { table } = useDataTable({ + data: localData?.data || [], + columns, + pageCount: localData?.pageCount || 0, + rowCount: localData?.total || 0, // 총 레코드 수 추가 + filterFields, + enablePinning: true, + enableAdvancedFilter: true, + initialState: { + sorting: [{ id: "updatedAt", desc: true }], + }, + getRowId: (originalRow) => String(originalRow.id), + shallow: false, + clearOnDefault: true, + columnResizeMode: "onEnd", + }) + + return ( + <div className="w-full overflow-auto"> + <DataTable table={table} maxHeight={maxHeight}> + <DataTableAdvancedToolbar + table={table} + filterFields={advancedFilterFields} + shallow={false} + > + <RFQTableToolbarActions + table={table} + localData={localData} + setLocalData={setLocalData} + onSuccess={onDataRefresh} + /> + </DataTableAdvancedToolbar> + </DataTable> + </div> + ) +}
\ No newline at end of file diff --git a/lib/procurement-rfqs/table/rfq-table.tsx b/lib/procurement-rfqs/table/rfq-table.tsx index 510f474d..23cd66fa 100644 --- a/lib/procurement-rfqs/table/rfq-table.tsx +++ b/lib/procurement-rfqs/table/rfq-table.tsx @@ -3,63 +3,96 @@ import * as React from "react" import type { DataTableAdvancedFilterField, - DataTableFilterField, DataTableRowAction, } from "@/types/table" import { useDataTable } from "@/hooks/use-data-table" import { DataTable } from "@/components/data-table/data-table" import { getColumns, EditingCellState } from "./rfq-table-column" -import { useEffect } from "react" +import { useEffect, useCallback, useRef, useMemo } from "react" import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-advanced-toolbar" import { RFQTableToolbarActions } from "./rfq-table-toolbar-actions" import { ProcurementRfqsView } from "@/db/schema" import { getPORfqs } from "../services" import { toast } from "sonner" -import { updateRfqRemark } from "@/lib/procurement-rfqs/services" // 구현 필요 +import { updateRfqRemark } from "@/lib/procurement-rfqs/services" +import { useSearchParams } from "next/navigation" +import { useTablePresets } from "@/components/data-table/use-table-presets" +import { TablePresetManager } from "@/components/data-table/data-table-preset" +import { Loader2 } from "lucide-react" interface RFQListTableProps { data?: Awaited<ReturnType<typeof getPORfqs>>; onSelectRFQ?: (rfq: ProcurementRfqsView | null) => void; - // 데이터 새로고침을 위한 콜백 추가 onDataRefresh?: () => void; - maxHeight?: string | number; // Add this prop + maxHeight?: string | number; } -// 보다 유연한 타입 정의 -type LocalDataType = Awaited<ReturnType<typeof getPORfqs>>; - export function RFQListTable({ data, onSelectRFQ, onDataRefresh, maxHeight }: RFQListTableProps) { + const searchParams = useSearchParams() const [rowAction, setRowAction] = React.useState<DataTableRowAction<ProcurementRfqsView> | null>(null) - // 인라인 에디팅을 위한 상태 추가 const [editingCell, setEditingCell] = React.useState<EditingCellState | null>(null) - // 로컬 데이터를 관리하기 위한 상태 추가 - const [localData, setLocalData] = React.useState<LocalDataType>(data || { data: [], pageCount: 0, total: 0 }); + const [localData, setLocalData] = React.useState<typeof data>(data || { data: [], pageCount: 0, total: 0 }) + const [isMounted, setIsMounted] = React.useState(false) + + // 초기 설정 정의 + 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') || undefined, + to: searchParams.get('to') || undefined, + columnVisibility: {}, + columnOrder: [], + pinnedColumns: { left: [], right: [] }, + groupBy: [], + expandedRows: [] + }), [searchParams]) - // 데이터가 변경될 때 로컬 데이터도 업데이트 + // DB 기반 프리셋 훅 사용 + const { + presets, + activePresetId, + hasUnsavedChanges, + isLoading: presetsLoading, + createPreset, + applyPreset, + updatePreset, + deletePreset, + setDefaultPreset, + renamePreset, + updateClientState, + getCurrentSettings, + } = useTablePresets<ProcurementRfqsView>('rfq-list-table', initialSettings) + + // 클라이언트 마운트 체크 + useEffect(() => { + setIsMounted(true) + }, []) + + // 데이터 변경 감지 useEffect(() => { setLocalData(data || { data: [], pageCount: 0, total: 0 }) }, [data]) - // 비고 업데이트 함수 const updateRemark = async (rfqId: number, remark: string) => { try { - // 낙관적 UI 업데이트 (로컬 데이터 먼저 갱신) if (localData && localData.data) { - // 로컬 데이터에서 해당 행 찾기 const rowIndex = localData.data.findIndex(row => row.id === rfqId); if (rowIndex >= 0) { - // 불변성을 유지하면서 로컬 데이터 업데이트 const newData = [...localData.data]; newData[rowIndex] = { ...newData[rowIndex], remark }; - - // 전체 데이터 구조 복사하여 업데이트 - setLocalData({ ...localData, data: newData } as typeof localData); + setLocalData({ ...localData, data: newData }); } } @@ -67,8 +100,6 @@ export function RFQListTable({ if (result.success) { toast.success("비고가 업데이트되었습니다"); - - // 서버 데이터 리프레시 호출 if (onDataRefresh) { onDataRefresh(); } @@ -80,29 +111,23 @@ export function RFQListTable({ toast.error("업데이트 중 오류가 발생했습니다"); } } - + // 행 액션 처리 useEffect(() => { if (rowAction) { - // 액션 유형에 따라 처리 switch (rowAction.type) { case "select": - // 선택된 문서 처리 if (onSelectRFQ) { onSelectRFQ(rowAction.row.original) } break; case "update": - // 업데이트 처리 로직 console.log("Update rfq:", rowAction.row.original) break; case "delete": - // 삭제 처리 로직 console.log("Delete rfq:", rowAction.row.original) break; } - - // 액션 처리 후 rowAction 초기화 setRowAction(null) } }, [rowAction, onSelectRFQ]) @@ -116,11 +141,8 @@ export function RFQListTable({ }), [setRowAction, editingCell, setEditingCell, updateRemark] ) - - - // Filter fields - const filterFields: DataTableFilterField<ProcurementRfqsView>[] = [] + // 고급 필터 필드 정의 const advancedFilterFields: DataTableAdvancedFilterField<ProcurementRfqsView>[] = [ { id: "rfqCode", @@ -142,7 +164,6 @@ export function RFQListTable({ label: "자재명", type: "text", }, - { id: "rfqSealedYn", label: "RFQ 밀봉여부", @@ -170,38 +191,183 @@ export function RFQListTable({ }, ] - // useDataTable 훅으로 react-table 구성 - 로컬 데이터 사용하도록 수정 + // 현재 설정 가져오기 + const currentSettings = useMemo(() => { + return getCurrentSettings() + }, [getCurrentSettings]) + + // useDataTable 초기 상태 설정 + const initialState = useMemo(() => { + console.log('Setting initial state:', currentSettings) + return { + sorting: initialSettings.sort.filter(sortItem => { + const columnExists = columns.some(col => col.accessorKey === sortItem.id) + return columnExists + }) as any, + columnVisibility: currentSettings.columnVisibility, + columnPinning: currentSettings.pinnedColumns, + } + }, [currentSettings, initialSettings.sort, columns]) + + // useDataTable 훅 설정 const { table } = useDataTable({ data: localData?.data || [], columns, pageCount: localData?.pageCount || 0, - rowCount: localData?.total || 0, // 총 레코드 수 추가 - filterFields, + rowCount: localData?.total || 0, + filterFields: [], enablePinning: true, enableAdvancedFilter: true, - initialState: { - sorting: [{ id: "updatedAt", desc: true }], - }, + initialState, getRowId: (originalRow) => String(originalRow.id), shallow: false, clearOnDefault: true, columnResizeMode: "onEnd", }) + + // 테이블 상태 변경 감지 및 자동 저장 + const lastKnownStateRef = useRef<{ + columnVisibility: string + columnPinning: string + columnOrder: string[] + }>({ + columnVisibility: '{}', + columnPinning: '{"left":[],"right":[]}', + columnOrder: [] + }) + + const checkAndUpdateTableState = useCallback(() => { + if (!presetsLoading && !activePresetId) return + + try { + const currentVisibility = table.getState().columnVisibility + const currentPinning = table.getState().columnPinning + + // 컬럼 순서 가져오기 + const allColumns = table.getAllColumns() + const leftPinned = table.getLeftHeaderGroups()[0]?.headers.map(h => h.column.id) || [] + const rightPinned = table.getRightHeaderGroups()[0]?.headers.map(h => h.column.id) || [] + const center = table.getCenterHeaderGroups()[0]?.headers.map(h => h.column.id) || [] + const currentOrder = [...leftPinned, ...center, ...rightPinned] + + const visibilityString = JSON.stringify(currentVisibility) + const pinningString = JSON.stringify(currentPinning) + const orderString = JSON.stringify(currentOrder) + + // 실제 변경이 있을 때만 업데이트 + if ( + visibilityString !== lastKnownStateRef.current.columnVisibility || + pinningString !== lastKnownStateRef.current.columnPinning || + orderString !== JSON.stringify(lastKnownStateRef.current.columnOrder) + ) { + console.log('Table state changed, updating preset...') + + const newClientState = { + columnVisibility: currentVisibility, + columnOrder: currentOrder, + pinnedColumns: currentPinning, + } + + // 상태 업데이트 전에 기록 + lastKnownStateRef.current = { + columnVisibility: visibilityString, + columnPinning: pinningString, + columnOrder: currentOrder + } + + updateClientState(newClientState) + } + } catch (error) { + console.error('Error checking table state:', error) + } + }, [activePresetId, table, updateClientState, presetsLoading ]) + + // 주기적으로 테이블 상태 체크 + useEffect(() => { + if (!isMounted || !activePresetId) return + + console.log('Starting table state polling') + const intervalId = setInterval(checkAndUpdateTableState, 500) + + return () => { + clearInterval(intervalId) + console.log('Stopped table state polling') + } + }, [isMounted, activePresetId, checkAndUpdateTableState]) + + // 프리셋 적용 시 테이블 상태 업데이트 + useEffect(() => { + if (isMounted && activePresetId && currentSettings) { + const settings = currentSettings + console.log('Applying preset settings to table:', settings) + + const currentVisibility = table.getState().columnVisibility + const currentPinning = table.getState().columnPinning + + if ( + JSON.stringify(currentVisibility) !== JSON.stringify(settings.columnVisibility) || + JSON.stringify(currentPinning) !== JSON.stringify(settings.pinnedColumns) + ) { + console.log('Updating table state to match preset...') + + // 테이블 상태 업데이트 + table.setColumnVisibility(settings.columnVisibility) + table.setColumnPinning(settings.pinnedColumns) + + // 상태 저장소 업데이트 + lastKnownStateRef.current = { + columnVisibility: JSON.stringify(settings.columnVisibility), + columnPinning: JSON.stringify(settings.pinnedColumns), + columnOrder: settings.columnOrder || [] + } + } + } + }, [isMounted, activePresetId, currentSettings, table]) + // 로딩 중일 때는 스켈레톤 표시 + if (!isMounted) { + return ( + <div className="w-full h-96 flex items-center justify-center"> + <div className="flex flex-col items-center gap-2"> + <Loader2 className="h-8 w-8 animate-spin text-muted-foreground" /> + <span className="text-sm text-muted-foreground">테이블 설정을 로드하는 중...</span> + </div> + </div> + ) + } + return ( - <div className="w-full overflow-auto"> + <div className="w-full overflow-auto"> <DataTable table={table} maxHeight={maxHeight}> <DataTableAdvancedToolbar table={table} filterFields={advancedFilterFields} shallow={false} > - <RFQTableToolbarActions - table={table} - localData={localData} - setLocalData={setLocalData} - onSuccess={onDataRefresh} - /> + <div className="flex items-center gap-2"> + {/* DB 기반 테이블 프리셋 매니저 */} + <TablePresetManager<ProcurementRfqsView> + presets={presets} + activePresetId={activePresetId} + currentSettings={currentSettings} + hasUnsavedChanges={hasUnsavedChanges} + isLoading={presetsLoading} + onCreatePreset={createPreset} + onUpdatePreset={updatePreset} + onDeletePreset={deletePreset} + onApplyPreset={applyPreset} + onSetDefaultPreset={setDefaultPreset} + onRenamePreset={renamePreset} + /> + + {/* 기존 툴바 액션들 */} + <RFQTableToolbarActions + table={table} + localData={localData} + setLocalData={setLocalData} + onSuccess={onDataRefresh} + /> + </div> </DataTableAdvancedToolbar> </DataTable> </div> diff --git a/lib/procurement-rfqs/vendor-response/table/vendor-quotations-table-columns.tsx b/lib/procurement-rfqs/vendor-response/table/vendor-quotations-table-columns.tsx index 9eecc72f..bad793c3 100644 --- a/lib/procurement-rfqs/vendor-response/table/vendor-quotations-table-columns.tsx +++ b/lib/procurement-rfqs/vendor-response/table/vendor-quotations-table-columns.tsx @@ -48,9 +48,21 @@ function StatusBadge({ status }: { status: string }) { interface QuotationWithRfqCode extends ProcurementVendorQuotations { rfqCode?: string; rfq?: { + id?: number; rfqCode?: string; + status?: string; dueDate?: Date | string | null; - + rfqSendDate?: Date | string | null; + item?: { + id?: number; + itemCode?: string; + itemName?: string; + } | null; + } | null; + vendor?: { + id?: number; + vendorName?: string; + vendorCode?: string; } | null; } @@ -64,7 +76,7 @@ interface GetColumnsProps { } /** - * tanstack table 컬럼 정의 + * tanstack table 컬럼 정의 (RfqsTable 스타일) */ export function getColumns({ setRowAction, @@ -99,13 +111,10 @@ export function getColumns({ enableHiding: false, } - // ---------------------------------------------------------------- - // 3) 일반 컬럼들 + // 2) actions 컬럼 // ---------------------------------------------------------------- - - // 견적서 액션 컬럼 (아이콘 버튼으로 변경) - const quotationActionColumn: ColumnDef<QuotationWithRfqCode> = { + const actionsColumn: ColumnDef<QuotationWithRfqCode> = { id: "actions", enableHiding: false, cell: ({ row }) => { @@ -134,106 +143,191 @@ export function getColumns({ </TooltipProvider> ) }, - size: 50, // 아이콘으로 변경했으므로 크기 줄임 - } - - // RFQ 번호 컬럼 - const rfqCodeColumn: ColumnDef<QuotationWithRfqCode> = { - accessorKey: "quotationCode", - header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="RFQ 번호" /> - ), - cell: ({ row }) => row.original.quotationCode || "-", - size: 150, + size: 50, } - // RFQ 버전 컬럼 - const quotationVersionColumn: ColumnDef<QuotationWithRfqCode> = { - accessorKey: "quotationVersion", - header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="RFQ 버전" /> - ), - cell: ({ row }) => row.original.quotationVersion || "-", + // ---------------------------------------------------------------- + // 3) 컬럼 정의 배열 + // ---------------------------------------------------------------- + const columnDefinitions = [ + { + id: "quotationCode", + label: "RFQ 번호", + group: null, + size: 150, + minSize: 100, + maxSize: 200, + }, + { + id: "quotationVersion", + label: "RFQ 버전", + group: null, + size: 100, + minSize: 80, + maxSize: 120, + }, + { + id: "itemCode", + label: "자재 그룹 코드", + group: "RFQ 정보", + size: 120, + minSize: 100, + maxSize: 150, + }, + { + id: "itemName", + label: "자재 이름", + group: "RFQ 정보", + // size를 제거하여 유연한 크기 조정 허용 + minSize: 150, + maxSize: 300, + }, + { + id: "rfqSendDate", + label: "RFQ 송부일", + group: "날짜 정보", + size: 150, + minSize: 120, + maxSize: 180, + }, + { + id: "dueDate", + label: "RFQ 마감일", + group: "날짜 정보", + size: 150, + minSize: 120, + maxSize: 180, + }, + { + id: "status", + label: "상태", + group: null, size: 100, + minSize: 80, + maxSize: 120, + }, + { + id: "totalPrice", + label: "총액", + group: null, + size: 120, + minSize: 100, + maxSize: 150, + }, + { + id: "submittedAt", + label: "제출일", + group: "날짜 정보", + size: 120, + minSize: 100, + maxSize: 150, + }, + { + id: "validUntil", + label: "유효기간", + group: "날짜 정보", + size: 120, + minSize: 100, + maxSize: 150, + }, + ]; + + // ---------------------------------------------------------------- + // 4) 그룹별로 컬럼 정리 (중첩 헤더 생성) + // ---------------------------------------------------------------- + const groupMap: Record<string, ColumnDef<QuotationWithRfqCode>[]> = {} + + columnDefinitions.forEach((cfg) => { + const groupName = cfg.group || "_noGroup" + + if (!groupMap[groupName]) { + groupMap[groupName] = [] } - const dueDateColumn: ColumnDef<QuotationWithRfqCode> = { - accessorKey: "dueDate", + // 개별 컬럼 정의 + const columnDef: ColumnDef<QuotationWithRfqCode> = { + accessorKey: cfg.id, + enableResizing: true, header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="RFQ 마감일" /> + <DataTableColumnHeaderSimple column={column} title={cfg.label} /> ), - cell: ({ row }) => { - // 타입 단언 사용 - const rfq = row.original.rfq as any; - const date = rfq?.dueDate as string | null; - return date ? formatDateTime(new Date(date)) : "-"; + cell: ({ row, cell }) => { + // 각 컬럼별 특별한 렌더링 처리 + switch (cfg.id) { + case "quotationCode": + return row.original.quotationCode || "-" + + case "quotationVersion": + return row.original.quotationVersion || "-" + + case "itemCode": + const itemCode = row.original.rfq?.item?.itemCode; + return itemCode ? itemCode : "-"; + + case "itemName": + const itemName = row.original.rfq?.item?.itemName; + return itemName ? itemName : "-"; + + case "rfqSendDate": + const sendDate = row.original.rfq?.rfqSendDate; + return sendDate ? formatDateTime(new Date(sendDate)) : "-"; + + case "dueDate": + const dueDate = row.original.rfq?.dueDate; + return dueDate ? formatDateTime(new Date(dueDate)) : "-"; + + case "status": + return <StatusBadge status={row.getValue("status") as string} /> + + case "totalPrice": + const price = parseFloat(row.getValue("totalPrice") as string || "0") + const currency = row.original.currency + return formatCurrency(price, currency) + + case "submittedAt": + const submitDate = row.getValue("submittedAt") as string | null + return submitDate ? formatDate(new Date(submitDate)) : "-" + + case "validUntil": + const validDate = row.getValue("validUntil") as string | null + return validDate ? formatDate(new Date(validDate)) : "-" + + default: + return row.getValue(cfg.id) ?? "" + } }, - size: 100, + size: cfg.size, + minSize: cfg.minSize, + maxSize: cfg.maxSize, } - - // 상태 컬럼 - const statusColumn: ColumnDef<QuotationWithRfqCode> = { - accessorKey: "status", - header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="상태" /> - ), - cell: ({ row }) => <StatusBadge status={row.getValue("status") as string} />, - size: 100, - } - - // 총액 컬럼 - const totalPriceColumn: ColumnDef<QuotationWithRfqCode> = { - accessorKey: "totalPrice", - header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="총액" /> - ), - cell: ({ row }) => { - const price = parseFloat(row.getValue("totalPrice") as string || "0") - const currency = row.original.currency - - return formatCurrency(price, currency) - }, - size: 120, - } - - // 제출일 컬럼 - const submittedAtColumn: ColumnDef<QuotationWithRfqCode> = { - accessorKey: "submittedAt", - header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="제출일" /> - ), - cell: ({ row }) => { - const date = row.getValue("submittedAt") as string | null - return date ? formatDate(new Date(date)) : "-" - }, - size: 120, - } - - // 유효기간 컬럼 - const validUntilColumn: ColumnDef<QuotationWithRfqCode> = { - accessorKey: "validUntil", - header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="유효기간" /> - ), - cell: ({ row }) => { - const date = row.getValue("validUntil") as string | null - return date ? formatDate(new Date(date)) : "-" - }, - size: 120, - } + + groupMap[groupName].push(columnDef) + }) + + // ---------------------------------------------------------------- + // 5) 그룹별 중첩 컬럼 생성 + // ---------------------------------------------------------------- + const nestedColumns: ColumnDef<QuotationWithRfqCode>[] = [] + Object.entries(groupMap).forEach(([groupName, colDefs]) => { + if (groupName === "_noGroup") { + // 그룹이 없는 컬럼들은 직접 추가 + nestedColumns.push(...colDefs) + } else { + // 그룹이 있는 컬럼들은 중첩 구조로 추가 + nestedColumns.push({ + id: groupName, + header: groupName, + columns: colDefs, + }) + } + }) // ---------------------------------------------------------------- - // 4) 최종 컬럼 배열 + // 6) 최종 컬럼 배열 // ---------------------------------------------------------------- return [ selectColumn, - rfqCodeColumn, - quotationVersionColumn, - dueDateColumn, - statusColumn, - totalPriceColumn, - submittedAtColumn, - validUntilColumn, - quotationActionColumn // 이름을 변경하고 마지막에 배치 + ...nestedColumns, + actionsColumn, ] }
\ No newline at end of file diff --git a/lib/procurement-rfqs/vendor-response/table/vendor-quotations-table.tsx b/lib/procurement-rfqs/vendor-response/table/vendor-quotations-table.tsx index 92bda337..7ea0c69e 100644 --- a/lib/procurement-rfqs/vendor-response/table/vendor-quotations-table.tsx +++ b/lib/procurement-rfqs/vendor-response/table/vendor-quotations-table.tsx @@ -109,7 +109,7 @@ export function VendorQuotationsTable({ promises }: VendorQuotationsTableProps) }, ]; - // useDataTable 훅 사용 + // useDataTable 훅 사용 (RfqsTable 스타일로 개선) const { table } = useDataTable({ data, columns, @@ -117,6 +117,8 @@ export function VendorQuotationsTable({ promises }: VendorQuotationsTableProps) filterFields, enablePinning: true, enableAdvancedFilter: true, + enableColumnResizing: true, // 컬럼 크기 조정 허용 + columnResizeMode: 'onChange', // 실시간 크기 조정 initialState: { sorting: [{ id: "updatedAt", desc: true }], columnPinning: { right: ["actions"] }, @@ -124,22 +126,27 @@ export function VendorQuotationsTable({ promises }: VendorQuotationsTableProps) getRowId: (originalRow) => String(originalRow.id), shallow: false, clearOnDefault: true, + defaultColumn: { + minSize: 50, + maxSize: 500, + }, }); return ( - <div style={{ maxWidth: '100vw' }}> - <DataTable - table={table} - > - <DataTableAdvancedToolbar + <div className="w-full"> + <div className="overflow-x-auto"> + <DataTable table={table} - filterFields={advancedFilterFields} - shallow={false} + className="min-w-full" > - </DataTableAdvancedToolbar> - </DataTable> - - + <DataTableAdvancedToolbar + table={table} + filterFields={advancedFilterFields} + shallow={false} + > + </DataTableAdvancedToolbar> + </DataTable> + </div> </div> ); }
\ No newline at end of file diff --git a/lib/utils.ts b/lib/utils.ts index af9df057..c7015638 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -11,6 +11,28 @@ export function formatDate( opts: Intl.DateTimeFormatOptions = {}, includeTime: boolean = false ) { + const dateObj = new Date(date); + + // 한국 로케일인 경우 하이픈 포맷 사용 + if (locale === "ko-KR" || locale === "KR" || locale === "kr") { + const year = dateObj.getFullYear(); + const month = String(dateObj.getMonth() + 1).padStart(2, "0"); + const day = String(dateObj.getDate()).padStart(2, "0"); + + let result = `${year}-${month}-${day}`; + + // 시간 포함 옵션이 활성화된 경우 + if (includeTime) { + const hour = String(dateObj.getHours()).padStart(2, "0"); + const minute = String(dateObj.getMinutes()).padStart(2, "0"); + const second = String(dateObj.getSeconds()).padStart(2, "0"); + result += ` ${hour}:${minute}:${second}`; + } + + return result; + } + + // 다른 로케일은 기존 방식 유지 return new Intl.DateTimeFormat(locale, { month: opts.month ?? "long", day: opts.day ?? "numeric", @@ -23,20 +45,34 @@ export function formatDate( hour12: opts.hour12 ?? false, // Use 24-hour format by default }), ...opts, // This allows overriding any of the above defaults - }).format(new Date(date)) + }).format(dateObj); } -// Alternative: Create a separate function for date and time +// formatDateTime 함수도 같은 방식으로 수정 export function formatDateTime( - date: Date | string | number| null | undefined, + date: Date | string | number | null | undefined, locale: string = "en-US", opts: Intl.DateTimeFormatOptions = {} ) { - if (date === null || date === undefined || date === '') { return ''; // 또는 '-', 'N/A' 등 원하는 기본값 반환 } - + + const dateObj = new Date(date); + + // 한국 로케일인 경우 하이픈 포맷 사용 + if (locale === "ko-KR" || locale === "KR" || locale === "kr") { + const year = dateObj.getFullYear(); + const month = String(dateObj.getMonth() + 1).padStart(2, "0"); + const day = String(dateObj.getDate()).padStart(2, "0"); + const hour = String(dateObj.getHours()).padStart(2, "0"); + const minute = String(dateObj.getMinutes()).padStart(2, "0"); + const second = String(dateObj.getSeconds()).padStart(2, "0"); + + return `${year}-${month}-${day} ${hour}:${minute}:${second}`; + } + + // 다른 로케일은 기존 방식 유지 return new Intl.DateTimeFormat(locale, { month: opts.month ?? "long", day: opts.day ?? "numeric", @@ -46,7 +82,7 @@ export function formatDateTime( second: opts.second ?? "2-digit", hour12: opts.hour12 ?? false, ...opts, - }).format(new Date(date)) + }).format(dateObj); } export function toSentenceCase(str: string) { @@ -78,3 +114,39 @@ export function composeEventHandlers<E>( } } } + + +/** + * 바이트 단위의 파일 크기를 사람이 읽기 쉬운 형식으로 변환합니다. + * (예: 1024 -> "1 KB", 1536 -> "1.5 KB") + * + * @param bytes 변환할 바이트 크기 + * @param decimals 소수점 자릿수 (기본값: 1) + * @returns 포맷된 파일 크기 문자열 + */ +export const formatFileSize = (bytes: number, decimals: number = 1): string => { + if (bytes === 0) return '0 Bytes'; + + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; + + // 로그 계산으로 적절한 단위 찾기 + const i = Math.floor(Math.log(bytes) / Math.log(k)); + + // 단위에 맞게 값 계산 (소수점 반올림) + const value = parseFloat((bytes / Math.pow(k, i)).toFixed(decimals)); + + return `${value} ${sizes[i]}`; +}; +export function formatCurrency( + value: number, + currency: string | null | undefined = "KRW", + locale: string = "ko-KR" +): string { + return new Intl.NumberFormat(locale, { + style: "currency", + currency: currency ?? "KRW", // null이나 undefined면 "KRW" 사용 + // minimumFractionDigits: 0, + // maximumFractionDigits: 2, + }).format(value) +}
\ No newline at end of file |
