summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/procurement-rfqs/services.ts7
-rw-r--r--lib/procurement-rfqs/table/rfq-table copy.tsx209
-rw-r--r--lib/procurement-rfqs/table/rfq-table.tsx258
-rw-r--r--lib/procurement-rfqs/vendor-response/table/vendor-quotations-table-columns.tsx282
-rw-r--r--lib/procurement-rfqs/vendor-response/table/vendor-quotations-table.tsx31
-rw-r--r--lib/utils.ts84
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