diff options
| author | joonhoekim <26rote@gmail.com> | 2025-09-23 20:05:46 +0900 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-09-23 20:05:46 +0900 |
| commit | 3cde5c2c7d157bb0fe353de5e67e4b35506bb4e2 (patch) | |
| tree | ea2dd4927ce79543317ad5fc37a45d4bf193f670 /lib/avl | |
| parent | a72c894c5b65b45f99fe03770f2d54098c5507c3 (diff) | |
(김준회) Vendorpool 수정요청사항
- 컬럼리사이징 관련 문제 해결
- 공통컴포넌트에 columnResizeMode: "onChange" 속성 추가
Diffstat (limited to 'lib/avl')
| -rw-r--r-- | lib/avl/table/avl-table-columns.tsx | 568 | ||||
| -rw-r--r-- | lib/avl/table/avl-table.tsx | 187 |
2 files changed, 301 insertions, 454 deletions
diff --git a/lib/avl/table/avl-table-columns.tsx b/lib/avl/table/avl-table-columns.tsx index d95a29b0..6ec2c3db 100644 --- a/lib/avl/table/avl-table-columns.tsx +++ b/lib/avl/table/avl-table-columns.tsx @@ -1,7 +1,7 @@ import { Checkbox } from "@/components/ui/checkbox" import { Button } from "@/components/ui/button" import { Badge } from "@/components/ui/badge" -import { Eye, Edit, Trash2, History } from "lucide-react" +import { Eye, History } from "lucide-react" import { type ColumnDef } from "@tanstack/react-table" import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header" import { AvlListItem } from "../types" @@ -26,303 +26,325 @@ declare module "@tanstack/react-table" { // 테이블 컬럼 정의 함수 export function getColumns({ selectedRows = [], onRowSelect }: GetColumnsProps): ColumnDef<AvlListItem>[] { const columns: ColumnDef<AvlListItem>[] = [ - // 기본 정보 그룹 + // select 컬럼 { - header: "AVL 정보", - columns: [ - { - id: "select", - header: () => <div className="text-center">선택</div>, - cell: ({ row }) => ( - <div className="flex justify-center"> - <Checkbox - checked={selectedRows.includes(row.original.id)} - onCheckedChange={(checked) => { - onRowSelect?.(row.original.id, !!checked) - }} - aria-label="행 선택" - className="translate-y-[2px]" - /> + id: "select", + header: () => <div className="text-center">선택</div>, + cell: ({ row }) => ( + <div className="flex justify-center"> + <Checkbox + checked={selectedRows.includes(row.original.id)} + onCheckedChange={(checked) => { + onRowSelect?.(row.original.id, !!checked) + }} + aria-label="행 선택" + className="translate-y-[2px]" + /> + </div> + ), + enableSorting: false, + enableHiding: false, + enableResizing: false, + size: 10, + minSize: 10, + maxSize: 10, + }, + // No 컬럼 + { + accessorKey: "no", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="No" /> + ), + cell: ({ getValue }) => <div className="text-center">{getValue() as number}</div>, + enableResizing: true, + size: 60, + }, + // AVL 분류 컬럼 + { + accessorKey: "isTemplate", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="AVL 분류" /> + ), + cell: ({ getValue }) => { + const value = getValue() as boolean + return ( + <div className="text-center"> + {value ? "표준 AVL" : "프로젝트 AVL"} </div> - ), - enableSorting: false, - enableHiding: false, - size: 40, - }, - { - accessorKey: "no", - header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="No" /> - ), - cell: ({ getValue }) => <div className="text-center">{getValue() as number}</div>, - size: 60, - }, - { - accessorKey: "isTemplate", - header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="AVL 분류" /> - ), - cell: ({ getValue }) => { - const value = getValue() as boolean - return ( - <div className="text-center"> - {value ? "표준 AVL" : "프로젝트 AVL"} - </div> - ) - }, - size: 120, + ) }, - { - accessorKey: "constructionSector", - header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="공사부문" /> - ), - cell: ({ getValue }) => { - const value = getValue() as string - return ( - <div className="text-center"> - {value} - </div> - ) - }, - size: 100, - }, - { - accessorKey: "projectCode", - header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="프로젝트 코드" /> - ), - cell: ({ getValue }) => { - const value = getValue() as string - return ( - <div className="text-center"> - {value} - </div> - ) - }, - size: 140, - }, - { - accessorKey: "shipType", - header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="선종" /> - ), - cell: ({ getValue }) => { - const value = getValue() as string - return ( - <div className="text-center"> - {value} - </div> - ) - }, - size: 100, - }, - { - accessorKey: "avlKind", - header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="AVL 종류" /> - ), - cell: ({ getValue }) => { - const value = getValue() as string - return ( - <div className="text-center"> - {value} - </div> - ) - }, - size: 120, - }, - { - accessorKey: "htDivision", - header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="H/T 구분" /> - ), - cell: ({ getValue }) => { - const value = getValue() as string - return ( - <div className="text-center"> - {value} - </div> - ) - }, - size: 80, - }, - { - accessorKey: "rev", - header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="Rev" /> - ), - cell: ({ getValue, row, table }) => { - const value = getValue() as number - return ( - <div className="flex items-center gap-1"> - <Badge variant="outline" className="font-mono"> - {value || 1} - </Badge> - <Button - variant="ghost" - size="sm" - className="h-6 w-6 p-0" - onClick={() => table.options.meta?.onAction?.('view-history', row.original)} - title="리비전 히스토리 보기" - > - <History className="h-3 w-3" /> - </Button> - </div> - ) - }, - size: 100, - }, - ], - }, - - // 집계 그룹 - { - header: "등록정보", - columns: [ - { - accessorKey: "PKG", - header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="PKG" /> - ), + enableResizing: true, + size: 120, + }, + // 공사부문 컬럼 + { + accessorKey: "constructionSector", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="공사부문" /> + ), + cell: ({ getValue }) => { + const value = getValue() as string + return ( + <div className="text-center"> + {value} + </div> + ) }, - { - accessorKey: "materialGroup", - header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="자재그룹" /> - ), + enableResizing: true, + size: 100, + }, + // 프로젝트 코드 컬럼 + { + accessorKey: "projectCode", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="프로젝트 코드" /> + ), + cell: ({ getValue }) => { + const value = getValue() as string + return ( + <div className="text-center"> + {value} + </div> + ) }, - { - accessorKey: "vendor", - header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="협력업체" /> - ), + enableResizing: true, + size: 140, + }, + // 선종 컬럼 + { + accessorKey: "shipType", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="선종" /> + ), + cell: ({ getValue }) => { + const value = getValue() as string + return ( + <div className="text-center"> + {value} + </div> + ) }, - { - accessorKey: "Tier", - header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="Tier" /> - ), + enableResizing: true, + size: 100, + }, + // AVL 종류 컬럼 + { + accessorKey: "avlKind", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="AVL 종류" /> + ), + cell: ({ getValue }) => { + const value = getValue() as string + return ( + <div className="text-center"> + {value} + </div> + ) }, - { - accessorKey: "ownerSuggestion", - header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="선주 제안" /> - ), + enableResizing: true, + size: 120, + }, + // H/T 구분 컬럼 + { + accessorKey: "htDivision", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="H/T 구분" /> + ), + cell: ({ getValue }) => { + const value = getValue() as string + return ( + <div className="text-center"> + {value} + </div> + ) }, - { - accessorKey: "shiSuggestion", - header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="SHI 제안" /> - ), + enableResizing: true, + size: 80, + }, + // Rev 컬럼 + { + accessorKey: "rev", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="Rev" /> + ), + cell: ({ getValue, row, table }) => { + const value = getValue() as number + return ( + <div className="flex items-center gap-1"> + <Badge variant="outline" className="font-mono"> + {value || 1} + </Badge> + <Button + variant="ghost" + size="sm" + className="h-6 w-6 p-0" + onClick={() => table.options.meta?.onAction?.('view-history', row.original)} + title="리비전 히스토리 보기" + > + <History className="h-3 w-3" /> + </Button> + </div> + ) }, - ], - }, - - // 등록 정보 그룹 - { - header: "작성정보", - columns: [ - { - accessorKey: "createdAt", - header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="등록일" /> - ), - cell: ({ getValue }) => { - const date = getValue() as string - return <div className="text-center text-sm">{date}</div> - }, - size: 100, + enableResizing: true, + size: 100, + }, + // PKG 컬럼 + { + accessorKey: "PKG", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="PKG" /> + ), + enableResizing: true, + size: 80, + }, + // 자재그룹 컬럼 + { + accessorKey: "materialGroup", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="자재그룹" /> + ), + enableResizing: true, + size: 120, + }, + // 협력업체 컬럼 + { + accessorKey: "vendor", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="협력업체" /> + ), + enableResizing: true, + size: 150, + }, + // Tier 컬럼 + { + accessorKey: "Tier", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="Tier" /> + ), + enableResizing: true, + size: 60, + }, + // 선주 제안 컬럼 + { + accessorKey: "ownerSuggestion", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="선주 제안" /> + ), + enableResizing: true, + size: 100, + }, + // SHI 제안 컬럼 + { + accessorKey: "shiSuggestion", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="SHI 제안" /> + ), + enableResizing: true, + size: 100, + }, + // 등록일 컬럼 + { + accessorKey: "createdAt", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="등록일" /> + ), + cell: ({ getValue }) => { + const date = getValue() as string + return <div className="text-center text-sm">{date}</div> }, - { - accessorKey: "createdBy", - header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="등록자" /> - ), - cell: ({ getValue }) => { - const date = getValue() as string - return <div className="text-center text-sm">{date}</div> - }, - size: 100, + enableResizing: true, + size: 100, + }, + // 등록자 컬럼 + { + accessorKey: "createdBy", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="등록자" /> + ), + cell: ({ getValue }) => { + const value = getValue() as string + return <div className="text-center text-sm">{value}</div> }, - { - accessorKey: "updatedAt", - header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="최종변경일" /> - ), - cell: ({ getValue }) => { - const date = getValue() as string - return <div className="text-center text-sm">{date}</div> - }, - size: 100, + enableResizing: true, + size: 100, + }, + // 최종변경일 컬럼 + { + accessorKey: "updatedAt", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="최종변경일" /> + ), + cell: ({ getValue }) => { + const date = getValue() as string + return <div className="text-center text-sm">{date}</div> }, - { - accessorKey: "updatedBy", - header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="최종변경자" /> - ), - cell: ({ getValue }) => { - const date = getValue() as string - return <div className="text-center text-sm">{date}</div> - }, - size: 100, + enableResizing: true, + size: 100, + }, + // 최종변경자 컬럼 + { + accessorKey: "updatedBy", + header: ({ column }) => ( + <DataTableColumnHeaderSimple column={column} title="최종변경자" /> + ), + cell: ({ getValue }) => { + const value = getValue() as string + return <div className="text-center text-sm">{value}</div> }, - ], - }, - - // 액션 그룹 - { - id: "actions", - header: "액션", - columns: [ - { - id: "actions", - header: () => <div className="text-center">액션</div>, - cell: ({ row, table }) => { - const isEmptyRow = table.options.meta?.isEmptyRow?.(row.id) || false - - if (isEmptyRow) { - return ( - <div className="flex items-center justify-center gap-1"> - <Button - variant="ghost" - size="sm" - onClick={() => table.options.meta?.onSaveEmptyRow?.(row.id)} - className="h-8 w-8 p-0" - > - 저장 - </Button> - <Button - variant="ghost" - size="sm" - onClick={() => table.options.meta?.onCancelEmptyRow?.(row.id)} - className="h-8 w-8 p-0 text-destructive hover:text-destructive" - > - 취소 - </Button> - </div> - ) - } + enableResizing: true, + size: 100, + }, + // 액션 컬럼 + { + id: "actions", + header: () => <div className="text-center">액션</div>, + cell: ({ row, table }) => { + const isEmptyRow = table.options.meta?.isEmptyRow?.(row.id) || false + if (isEmptyRow) { return ( <div className="flex items-center justify-center gap-1"> <Button variant="ghost" size="sm" - onClick={() => table.options.meta?.onAction?.("view-detail", { id: row.original.id })} + onClick={() => table.options.meta?.onSaveEmptyRow?.(row.id)} className="h-8 w-8 p-0" - title="상세보기" > - <Eye className="h-4 w-4" /> + 저장 + </Button> + <Button + variant="ghost" + size="sm" + onClick={() => table.options.meta?.onCancelEmptyRow?.(row.id)} + className="h-8 w-8 p-0 text-destructive hover:text-destructive" + > + 취소 </Button> </div> ) - }, - enableSorting: false, - enableHiding: false, - size: 120, + } + + return ( + <div className="flex items-center justify-center gap-1"> + <Button + variant="ghost" + size="sm" + onClick={() => table.options.meta?.onAction?.("view-detail", { id: row.original.id })} + className="h-8 w-8 p-0" + title="상세보기" + > + <Eye className="h-4 w-4" /> + </Button> + </div> + ) }, - ], + enableSorting: false, + enableHiding: false, + enableResizing: false, + size: 120, + minSize: 120, + maxSize: 120, }, ] diff --git a/lib/avl/table/avl-table.tsx b/lib/avl/table/avl-table.tsx index 45da6268..61db658d 100644 --- a/lib/avl/table/avl-table.tsx +++ b/lib/avl/table/avl-table.tsx @@ -12,7 +12,7 @@ import { Button } from "@/components/ui/button" import { toast } from "sonner" import { getColumns } from "./avl-table-columns" -import { updateAvlListAction, deleteAvlListAction, handleAvlActionAction } from "../service" +import { handleAvlActionAction } from "../service" import type { AvlListItem } from "../types" import { AvlHistoryModal, type AvlHistoryRecord } from "@/lib/avl/components/avl-history-modal" import { getAvlHistory } from "@/lib/avl/history-service" @@ -33,21 +33,16 @@ declare module "@tanstack/react-table" { interface AvlTableProps { data: AvlListItem[] pageCount?: number - onRefresh?: () => void // 데이터 새로고침 콜백 isLoading?: boolean // 로딩 상태 onRegistrationModeChange?: (mode: 'standard' | 'project') => void // 등록 모드 변경 콜백 onRowSelect?: (selectedRow: AvlListItem | null) => void // 행 선택 콜백 } -export function AvlTable({ data, pageCount, onRefresh, isLoading, onRegistrationModeChange, onRowSelect }: AvlTableProps) { +export function AvlTable({ data, pageCount, isLoading, onRegistrationModeChange, onRowSelect }: AvlTableProps) { // 단일 선택을 위한 상태 (shi-vendor-po 방식) const [selectedRows, setSelectedRows] = React.useState<number[]>([]) - // 수정사항 추적 (일괄 저장용) - const [pendingChanges, setPendingChanges] = React.useState<Record<string, Partial<AvlListItem>>>({}) - const [isSaving, setIsSaving] = React.useState(false) - // 히스토리 모달 관리 const [historyModalOpen, setHistoryModalOpen] = React.useState(false) const [selectedAvlItem, setSelectedAvlItem] = React.useState<AvlListItem | null>(null) @@ -101,56 +96,6 @@ export function AvlTable({ data, pageCount, onRefresh, isLoading, onRegistration }, ] - - // 인라인 편집 핸들러 (일괄 저장용) - const handleCellUpdate = React.useCallback(async (id: string, field: keyof AvlListItem, newValue: string | boolean) => { - const isEmptyRow = String(id).startsWith('temp-') - - if (isEmptyRow) { - // 빈 행의 경우 pendingChanges 상태도 업데이트 - setPendingChanges(prev => ({ - ...prev, - [id]: { - ...prev[id], - [field]: newValue - } - })) - } - - // pendingChanges에 변경사항 저장 (실시간 표시용) - setPendingChanges(prev => ({ - ...prev, - [id]: { - ...prev[id], - [field]: newValue - } - })) - }, []) - - // 편집 취소 핸들러 - const handleCellCancel = React.useCallback((id: string, field: keyof AvlListItem) => { - const isEmptyRow = String(id).startsWith('temp-') - - if (isEmptyRow) { - // 빈 행의 경우 pendingChanges 취소 - setPendingChanges(prev => { - const itemChanges = { ...prev[id] } - delete itemChanges[field] - - if (Object.keys(itemChanges).length === 0) { - const newChanges = { ...prev } - delete newChanges[id] - return newChanges - } - - return { - ...prev, - [id]: itemChanges - } - }) - } - }, []) - // 액션 핸들러 const handleAction = React.useCallback(async (action: string, data?: Partial<AvlListItem>) => { try { @@ -166,89 +111,6 @@ export function AvlTable({ data, pageCount, onRefresh, isLoading, onRegistration } break - case 'project-registration': - // 프로젝트 AVL 등록 - const projectResult = await handleAvlActionAction('project-registration') - if (projectResult.success) { - toast.success(projectResult.message) - onRegistrationModeChange?.('project') // 등록 모드 변경 콜백 호출 - } else { - toast.error(projectResult.message) - } - break - - case 'bulk-import': - // 일괄 입력 - const bulkResult = await handleAvlActionAction('bulk-import') - if (bulkResult.success) { - toast.success(bulkResult.message) - } else { - toast.error(bulkResult.message) - } - break - - case 'save': - // 변경사항 저장 - if (Object.keys(pendingChanges).length === 0) { - toast.info("저장할 변경사항이 없습니다.") - return - } - - setIsSaving(true) - try { - // 각 변경사항을 순차적으로 저장 - for (const [id, changes] of Object.entries(pendingChanges)) { - if (String(id).startsWith('temp-')) continue // 빈 행은 제외 - - // id 속성을 명시적으로 추가 - const updateData = { - ...changes, - id: Number(id) - } - - const result = await updateAvlListAction(Number(id), updateData) - if (!result) { - throw new Error(`항목 ${id} 저장 실패`) - } - } - - setPendingChanges({}) - toast.success("변경사항이 저장되었습니다.") - onRefresh?.() - } catch (error) { - console.error('저장 실패:', error) - toast.error("저장 중 오류가 발생했습니다.") - } finally { - setIsSaving(false) - } - break - - case 'edit': - // 수정 모달 열기 (현재는 간단한 토스트로 처리) - toast.info(`${data?.id} 항목 수정`) - break - - case 'delete': - // 삭제 확인 및 실행 - if (!data?.id || String(data.id).startsWith('temp-')) return - - const confirmed = window.confirm(`항목 ${data.id}을(를) 삭제하시겠습니까?`) - if (!confirmed) return - - try { - const result = await deleteAvlListAction(Number(data.id)) - if (result) { - toast.success("항목이 삭제되었습니다.") - onRefresh?.() - } else { - toast.error("삭제에 실패했습니다.") - } - } catch (error) { - console.error('삭제 실패:', error) - toast.error("삭제 중 오류가 발생했습니다.") - } - break - case 'view-detail': // 상세 조회 (페이지 이동) if (data?.id && !String(data.id).startsWith('temp-')) { @@ -271,7 +133,7 @@ export function AvlTable({ data, pageCount, onRefresh, isLoading, onRegistration console.error('액션 처리 실패:', error) toast.error("액션 처리 중 오류가 발생했습니다.") } - }, [pendingChanges, onRefresh, onRegistrationModeChange]) + }, [onRegistrationModeChange]) // 빈 행 포함한 전체 데이터 const allData = React.useMemo(() => { @@ -297,15 +159,6 @@ export function AvlTable({ data, pageCount, onRefresh, isLoading, onRegistration } }, [allData, onRowSelect]) - // 테이블 메타 설정 - const tableMeta = React.useMemo(() => ({ - onCellUpdate: handleCellUpdate, - onCellCancel: handleCellCancel, - onAction: handleAction, - isEmptyRow: (id: string) => String(id).startsWith('temp-'), - getPendingChanges: () => pendingChanges, - }), [handleCellUpdate, handleCellCancel, handleAction, pendingChanges]) - // 데이터 테이블 설정 const { table } = useDataTable({ @@ -320,14 +173,12 @@ export function AvlTable({ data, pageCount, onRefresh, isLoading, onRegistration pageIndex: 0, pageSize: 10, }, + // 기본 컬럼 사이즈 설정 + columnSizing: {}, }, getRowId: (row) => String(row.id), - meta: tableMeta, }) - // 변경사항이 있는지 확인 - const hasPendingChanges = Object.keys(pendingChanges).length > 0 - return ( <div className="space-y-4"> {/* 툴바 */} @@ -353,31 +204,11 @@ export function AvlTable({ data, pageCount, onRefresh, isLoading, onRegistration 프로젝트AVL등록 </Button> - {/* 저장 버튼 - 변경사항이 있을 때만 활성화 */} - {(hasPendingChanges) && ( - <Button - variant="default" - size="sm" - onClick={() => handleAction('save')} - disabled={isSaving} - > - {isSaving ? "저장 중..." : "저장"} - </Button> - )} - - {/* 새로고침 버튼 */} - <Button - variant="outline" - size="sm" - onClick={onRefresh} - > - 새로고침 - </Button> </div> </DataTableAdvancedToolbar> {/* 데이터 테이블 */} - <DataTable table={table} /> + <DataTable table={table} autoSizeColumns={false} /> {/* 히스토리 모달 */} <AvlHistoryModal @@ -390,12 +221,6 @@ export function AvlTable({ data, pageCount, onRefresh, isLoading, onRegistration onLoadHistory={loadHistoryData} /> - {/* 디버그 정보 (개발 환경에서만 표시) */} - {process.env.NODE_ENV === 'development' && (hasPendingChanges) && ( - <div className="text-xs text-muted-foreground p-2 bg-muted rounded"> - <div>Pending Changes: {Object.keys(pendingChanges).length}</div> - </div> - )} </div> ) } |
