"use client" import * as React from "react" import type { DataTableFilterField, } from "@/types/table" import { useDataTable } from "@/hooks/use-data-table" import { DataTable } from "@/components/data-table/data-table" import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-advanced-toolbar" import { Button } from "@/components/ui/button" import { toast } from "sonner" import { getColumns } from "./avl-table-columns" import { createAvlListAction, updateAvlListAction, deleteAvlListAction, handleAvlActionAction } from "../service" import type { AvlListItem } from "../types" import { AvlHistoryModal, type AvlHistoryRecord } from "@/lib/avl/components/avl-history-modal" // 테이블 메타 타입 확장 declare module "@tanstack/react-table" { interface TableMeta { onCellUpdate?: (id: string, field: keyof TData, newValue: any) => Promise onCellCancel?: (id: string, field: keyof TData) => void onAction?: (action: string, data?: any) => void onSaveEmptyRow?: (tempId: string) => Promise onCancelEmptyRow?: (tempId: string) => void isEmptyRow?: (id: string) => boolean getPendingChanges?: () => Record> } } 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) { // 단일 선택을 위한 상태 (shi-vendor-po 방식) const [selectedRows, setSelectedRows] = React.useState([]) // 수정사항 추적 (일괄 저장용) const [pendingChanges, setPendingChanges] = React.useState>>({}) const [isSaving, setIsSaving] = React.useState(false) // 빈 행 관리 (신규 등록용) const [emptyRows, setEmptyRows] = React.useState>({}) const [isCreating, setIsCreating] = React.useState(false) // 히스토리 모달 관리 const [historyModalOpen, setHistoryModalOpen] = React.useState(false) const [selectedAvlItem, setSelectedAvlItem] = React.useState(null) // 히스토리 데이터 로드 함수 const loadHistoryData = React.useCallback(async (avlItem: AvlListItem): Promise => { 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 } catch (error) { console.error('히스토리 로드 실패:', error) toast.error("히스토리를 불러오는데 실패했습니다.") return [] } }, []) // 필터 필드 정의 const filterFields: DataTableFilterField[] = [ { id: "isTemplate", label: "AVL 분류", placeholder: "AVL 분류 선택...", options: [ { label: "프로젝트 AVL", value: "false" }, { label: "표준 AVL", value: "true" }, ], }, { id: "constructionSector", label: "공사부문", placeholder: "공사부문 선택...", options: [ { label: "조선", value: "조선" }, { label: "해양", value: "해양" }, ], }, { id: "htDivision", label: "H/T 구분", placeholder: "H/T 구분 선택...", options: [ { label: "H", value: "H" }, { label: "T", value: "T" }, ], }, ] // 인라인 편집 핸들러 (일괄 저장용) const handleCellUpdate = React.useCallback(async (id: string, field: keyof AvlListItem, newValue: any) => { const isEmptyRow = String(id).startsWith('temp-') if (isEmptyRow) { // 빈 행의 경우 emptyRows 상태도 업데이트 setEmptyRows(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) { // 빈 행의 경우 emptyRows와 pendingChanges 모두 취소 setEmptyRows(prev => ({ ...prev, [id]: { ...prev[id], [field]: prev[id][field] // 원래 값으로 복원 (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?: any) => { 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') if (result.success) { toast.success(result.message) onRegistrationModeChange?.('standard') // 등록 모드 변경 콜백 호출 } else { toast.error(result.message) } 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 // 빈 행은 제외 const result = await updateAvlListAction(Number(id), changes as any) 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-')) { window.location.href = `/evcp/avl/${data.id}` } break case 'view-history': // 리비전 히스토리 조회 if (data?.id && !String(data.id).startsWith('temp-')) { setSelectedAvlItem(data as AvlListItem) setHistoryModalOpen(true) } break default: toast.error(`알 수 없는 액션: ${action}`) } } catch (error) { console.error('액션 처리 실패:', error) toast.error("액션 처리 중 오류가 발생했습니다.") } }, [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]) // 행 선택 처리 (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) } } else { setSelectedRows([]) onRowSelect?.(null) } }, [data, emptyRows, isLoading, 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]) // 데이터 테이블 설정 const { table } = useDataTable({ data: allData, columns: getColumns({ selectedRows, onRowSelect: handleRowSelect }), pageCount: pageCount || 1, filterFields, initialState: { sorting: [{ id: "createdAt", desc: true }], columnPinning: { right: ["actions"] }, pagination: { pageIndex: 0, pageSize: 10, }, }, getRowId: (row) => String(row.id), meta: tableMeta, }) // 변경사항이 있는지 확인 const hasPendingChanges = Object.keys(pendingChanges).length > 0 const hasEmptyRows = Object.keys(emptyRows).length > 0 return (
{/* 툴바 */}
{/* 액션 버튼들 */} {/* 저장 버튼 - 변경사항이 있을 때만 활성화 */} {(hasPendingChanges || hasEmptyRows) && ( )} {/* 새로고침 버튼 */}
{/* 데이터 테이블 */} {/* 히스토리 모달 */} { setHistoryModalOpen(false) setSelectedAvlItem(null) }} avlItem={selectedAvlItem} onLoadHistory={loadHistoryData} /> {/* 디버그 정보 (개발 환경에서만 표시) */} {process.env.NODE_ENV === 'development' && (hasPendingChanges || hasEmptyRows) && (
Pending Changes: {Object.keys(pendingChanges).length}
Empty Rows: {Object.keys(emptyRows).length}
)}
) }