diff options
Diffstat (limited to 'lib/avl/table/avl-table.tsx')
| -rw-r--r-- | lib/avl/table/avl-table.tsx | 219 |
1 files changed, 33 insertions, 186 deletions
diff --git a/lib/avl/table/avl-table.tsx b/lib/avl/table/avl-table.tsx index eb9b2079..45da6268 100644 --- a/lib/avl/table/avl-table.tsx +++ b/lib/avl/table/avl-table.tsx @@ -2,7 +2,7 @@ import * as React from "react" import type { - DataTableFilterField, + DataTableAdvancedFilterField } from "@/types/table" import { useDataTable } from "@/hooks/use-data-table" @@ -12,16 +12,17 @@ import { Button } from "@/components/ui/button" import { toast } from "sonner" import { getColumns } from "./avl-table-columns" -import { createAvlListAction, updateAvlListAction, deleteAvlListAction, handleAvlActionAction } from "../service" +import { updateAvlListAction, deleteAvlListAction, 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" // 테이블 메타 타입 확장 declare module "@tanstack/react-table" { interface TableMeta<TData> { - onCellUpdate?: (id: string, field: keyof TData, newValue: any) => Promise<void> + onCellUpdate?: (id: string, field: keyof TData, newValue: string | boolean) => Promise<void> onCellCancel?: (id: string, field: keyof TData) => void - onAction?: (action: string, data?: any) => void + onAction?: (action: string, data?: Partial<AvlListItem>) => void onSaveEmptyRow?: (tempId: string) => Promise<void> onCancelEmptyRow?: (tempId: string) => void isEmptyRow?: (id: string) => boolean @@ -47,10 +48,6 @@ export function AvlTable({ data, pageCount, onRefresh, isLoading, onRegistration const [pendingChanges, setPendingChanges] = React.useState<Record<string, Partial<AvlListItem>>>({}) const [isSaving, setIsSaving] = React.useState(false) - // 빈 행 관리 (신규 등록용) - const [emptyRows, setEmptyRows] = React.useState<Record<string, AvlListItem>>({}) - const [isCreating, setIsCreating] = React.useState(false) - // 히스토리 모달 관리 const [historyModalOpen, setHistoryModalOpen] = React.useState(false) const [selectedAvlItem, setSelectedAvlItem] = React.useState<AvlListItem | null>(null) @@ -58,36 +55,11 @@ export function AvlTable({ data, pageCount, onRefresh, isLoading, onRegistration // 히스토리 데이터 로드 함수 const loadHistoryData = React.useCallback(async (avlItem: AvlListItem): Promise<AvlHistoryRecord[]> => { try { - // 현재 리비전의 스냅샷 데이터 (실제 저장된 데이터 사용) - const currentSnapshot = avlItem.vendorInfoSnapshot || [] - - const historyData: AvlHistoryRecord[] = [ - { - id: avlItem.id, - rev: avlItem.rev || 1, - createdAt: avlItem.createdAt || new Date().toISOString(), - createdBy: avlItem.createdBy || "system", - vendorInfoSnapshot: currentSnapshot, - changeDescription: "최신 리비전 (확정완료)" - } - ] - - // TODO: 실제 구현에서는 DB에서 이전 리비전들의 스냅샷 데이터를 조회해야 함 - // 현재는 더미 데이터로 이전 리비전들을 시뮬레이션 - if ((avlItem.rev || 1) > 1) { - for (let rev = (avlItem.rev || 1) - 1; rev >= 1; rev--) { - historyData.push({ - id: avlItem.id + rev * 1000, // 임시 ID - rev, - createdAt: new Date(Date.now() - rev * 24 * 60 * 60 * 1000).toISOString(), - createdBy: "system", - vendorInfoSnapshot: [], // 이전 리비전의 스냅샷 데이터 (실제로는 DB에서 조회) - changeDescription: `리비전 ${rev} 변경사항` - }) - } - } - - return historyData + const historyRecords = await getAvlHistory(avlItem) + return historyRecords.map(record => ({ + ...record, + vendorInfoSnapshot: record.vendorInfoSnapshot || [] + })) } catch (error) { console.error('히스토리 로드 실패:', error) toast.error("히스토리를 불러오는데 실패했습니다.") @@ -96,7 +68,7 @@ export function AvlTable({ data, pageCount, onRefresh, isLoading, onRegistration }, []) // 필터 필드 정의 - const filterFields: DataTableFilterField<AvlListItem>[] = [ + const filterFields: DataTableAdvancedFilterField<AvlListItem>[] = [ { id: "isTemplate", label: "AVL 분류", @@ -105,6 +77,7 @@ export function AvlTable({ data, pageCount, onRefresh, isLoading, onRegistration { label: "프로젝트 AVL", value: "false" }, { label: "표준 AVL", value: "true" }, ], + type: "select" }, { id: "constructionSector", @@ -114,6 +87,7 @@ export function AvlTable({ data, pageCount, onRefresh, isLoading, onRegistration { label: "조선", value: "조선" }, { label: "해양", value: "해양" }, ], + type: "select" }, { id: "htDivision", @@ -123,17 +97,18 @@ export function AvlTable({ data, pageCount, onRefresh, isLoading, onRegistration { label: "H", value: "H" }, { label: "T", value: "T" }, ], + type: "select" }, ] // 인라인 편집 핸들러 (일괄 저장용) - const handleCellUpdate = React.useCallback(async (id: string, field: keyof AvlListItem, newValue: any) => { + const handleCellUpdate = React.useCallback(async (id: string, field: keyof AvlListItem, newValue: string | boolean) => { const isEmptyRow = String(id).startsWith('temp-') if (isEmptyRow) { - // 빈 행의 경우 emptyRows 상태도 업데이트 - setEmptyRows(prev => ({ + // 빈 행의 경우 pendingChanges 상태도 업데이트 + setPendingChanges(prev => ({ ...prev, [id]: { ...prev[id], @@ -157,15 +132,7 @@ export function AvlTable({ data, pageCount, onRefresh, isLoading, onRegistration const isEmptyRow = String(id).startsWith('temp-') if (isEmptyRow) { - // 빈 행의 경우 emptyRows와 pendingChanges 모두 취소 - setEmptyRows(prev => ({ - ...prev, - [id]: { - ...prev[id], - [field]: prev[id][field] // 원래 값으로 복원 (pendingChanges의 초기값 사용) - } - })) - + // 빈 행의 경우 pendingChanges 취소 setPendingChanges(prev => { const itemChanges = { ...prev[id] } delete itemChanges[field] @@ -185,39 +152,9 @@ export function AvlTable({ data, pageCount, onRefresh, isLoading, onRegistration }, []) // 액션 핸들러 - const handleAction = React.useCallback(async (action: string, data?: any) => { + const handleAction = React.useCallback(async (action: string, data?: Partial<AvlListItem>) => { try { switch (action) { - case 'new-registration': - // 신규 등록 - 빈 행 추가 - const tempId = `temp-${Date.now()}` - const newEmptyRow: AvlListItem = { - id: tempId as any, - no: 0, - selected: false, - isTemplate: false, - constructionSector: "", - projectCode: "", - shipType: "", - avlKind: "", - htDivision: "", - rev: 1, - vendorInfoSnapshot: null, - createdAt: new Date().toISOString().split('T')[0], - updatedAt: new Date().toISOString().split('T')[0], - createdBy: "system", - updatedBy: "system", - registrant: "system", - lastModifier: "system", - } - - setEmptyRows(prev => ({ - ...prev, - [tempId]: newEmptyRow - })) - toast.success("신규 등록 행이 추가되었습니다.") - break - case 'standard-registration': // 표준 AVL 등록 const result = await handleAvlActionAction('standard-registration') @@ -263,7 +200,13 @@ export function AvlTable({ data, pageCount, onRefresh, isLoading, onRegistration for (const [id, changes] of Object.entries(pendingChanges)) { if (String(id).startsWith('temp-')) continue // 빈 행은 제외 - const result = await updateAvlListAction(Number(id), changes as any) + // id 속성을 명시적으로 추가 + const updateData = { + ...changes, + id: Number(id) + } + + const result = await updateAvlListAction(Number(id), updateData) if (!result) { throw new Error(`항목 ${id} 저장 실패`) } @@ -330,95 +273,20 @@ export function AvlTable({ data, pageCount, onRefresh, isLoading, onRegistration } }, [pendingChanges, onRefresh, onRegistrationModeChange]) - // 빈 행 저장 핸들러 - const handleSaveEmptyRow = React.useCallback(async (tempId: string) => { - const emptyRow = emptyRows[tempId] - if (!emptyRow) return - - try { - setIsCreating(true) - - // 필수 필드 검증 - if (!emptyRow.constructionSector || !emptyRow.avlKind) { - toast.error("공사부문과 AVL 종류는 필수 입력 항목입니다.") - return - } - - // 빈 행 데이터를 생성 데이터로 변환 - const createData = { - isTemplate: emptyRow.isTemplate, - constructionSector: emptyRow.constructionSector, - projectCode: emptyRow.projectCode || undefined, - shipType: emptyRow.shipType || undefined, - avlKind: emptyRow.avlKind, - htDivision: emptyRow.htDivision || undefined, - rev: emptyRow.rev, - createdBy: "system", - updatedBy: "system", - } - - const result = await createAvlListAction(createData as any) - if (result) { - // 빈 행 제거 및 성공 메시지 - setEmptyRows(prev => { - const newRows = { ...prev } - delete newRows[tempId] - return newRows - }) - - // pendingChanges에서도 제거 - setPendingChanges(prev => { - const newChanges = { ...prev } - delete newChanges[tempId] - return newChanges - }) - - toast.success("새 항목이 등록되었습니다.") - onRefresh?.() - } else { - toast.error("등록에 실패했습니다.") - } - } catch (error) { - console.error('빈 행 저장 실패:', error) - toast.error("등록 중 오류가 발생했습니다.") - } finally { - setIsCreating(false) - } - }, [emptyRows, onRefresh]) - - // 빈 행 취소 핸들러 - const handleCancelEmptyRow = React.useCallback((tempId: string) => { - setEmptyRows(prev => { - const newRows = { ...prev } - delete newRows[tempId] - return newRows - }) - - setPendingChanges(prev => { - const newChanges = { ...prev } - delete newChanges[tempId] - return newChanges - }) - - toast.info("등록이 취소되었습니다.") - }, []) - // 빈 행 포함한 전체 데이터 const allData = React.useMemo(() => { // 로딩 중에는 빈 데이터를 표시 if (isLoading) { return [] } - const emptyRowArray = Object.values(emptyRows) - return [...data, ...emptyRowArray] - }, [data, emptyRows, isLoading]) + return [...data] + }, [data, isLoading]) // 행 선택 처리 (1개만 선택 가능 - shi-vendor-po 방식) const handleRowSelect = React.useCallback((id: number, selected: boolean) => { if (selected) { setSelectedRows([id]) // 1개만 선택 // 선택된 레코드 찾아서 부모 콜백 호출 - const allData = isLoading ? [] : [...data, ...Object.values(emptyRows)] const selectedRow = allData.find(row => row.id === id) if (selectedRow) { onRowSelect?.(selectedRow) @@ -427,18 +295,16 @@ export function AvlTable({ data, pageCount, onRefresh, isLoading, onRegistration setSelectedRows([]) onRowSelect?.(null) } - }, [data, emptyRows, isLoading, onRowSelect]) + }, [allData, onRowSelect]) // 테이블 메타 설정 const tableMeta = React.useMemo(() => ({ onCellUpdate: handleCellUpdate, onCellCancel: handleCellCancel, onAction: handleAction, - onSaveEmptyRow: handleSaveEmptyRow, - onCancelEmptyRow: handleCancelEmptyRow, isEmptyRow: (id: string) => String(id).startsWith('temp-'), getPendingChanges: () => pendingChanges, - }), [handleCellUpdate, handleCellCancel, handleAction, handleSaveEmptyRow, handleCancelEmptyRow, pendingChanges]) + }), [handleCellUpdate, handleCellCancel, handleAction, pendingChanges]) // 데이터 테이블 설정 @@ -461,29 +327,19 @@ export function AvlTable({ data, pageCount, onRefresh, isLoading, onRegistration // 변경사항이 있는지 확인 const hasPendingChanges = Object.keys(pendingChanges).length > 0 - const hasEmptyRows = Object.keys(emptyRows).length > 0 return ( <div className="space-y-4"> {/* 툴바 */} <DataTableAdvancedToolbar table={table} - filterFields={filterFields as any} + filterFields={filterFields} > <div className="flex items-center gap-2"> {/* 액션 버튼들 */} <Button variant="outline" size="sm" - onClick={() => handleAction('new-registration')} - disabled={isCreating} - > - 신규등록 - </Button> - - <Button - variant="outline" - size="sm" onClick={() => handleAction('standard-registration')} > 표준AVL등록 @@ -497,16 +353,8 @@ export function AvlTable({ data, pageCount, onRefresh, isLoading, onRegistration 프로젝트AVL등록 </Button> - <Button - variant="outline" - size="sm" - onClick={() => handleAction('bulk-import')} - > - 파일 업로드 - </Button> - {/* 저장 버튼 - 변경사항이 있을 때만 활성화 */} - {(hasPendingChanges || hasEmptyRows) && ( + {(hasPendingChanges) && ( <Button variant="default" size="sm" @@ -543,10 +391,9 @@ export function AvlTable({ data, pageCount, onRefresh, isLoading, onRegistration /> {/* 디버그 정보 (개발 환경에서만 표시) */} - {process.env.NODE_ENV === 'development' && (hasPendingChanges || hasEmptyRows) && ( + {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>Empty Rows: {Object.keys(emptyRows).length}</div> </div> )} </div> |
