"use client" import * as React from "react" import type { DataTableAdvancedFilterField, DataTableFilterField, } from "@/types/table" import { useSession } from "next-auth/react" 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 { BulkImportDialog } from "./bulk-import-dialog" import { columns, type VendorPoolItem } from "./columns" import { createVendorPool, updateVendorPool, deleteVendorPool } from "../service" import { getVendorByTaxId } from "@/lib/vendors/service" import { getMaterialGroupDetail } from "@/lib/material-groups/services" import type { VendorPool } from "../types" import { cn } from "@/lib/utils" // 테이블 메타 타입 확장 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> onTaxIdChange?: (id: string, taxId: string) => Promise onMaterialGroupCodeChange?: (id: string, materialGroupCode: string) => Promise } } interface VendorPoolTableProps { data: VendorPoolItem[] pageCount?: number onRefresh?: () => void // 데이터 새로고침 콜백 } export function VendorPoolTable({ data, pageCount, onRefresh }: VendorPoolTableProps) { const { data: session } = useSession() // 수정사항 추적 (일괄 저장용) const [pendingChanges, setPendingChanges] = React.useState>>({}) const [isSaving, setIsSaving] = React.useState(false) // 빈 행 관리 (신규 등록용) const [emptyRows, setEmptyRows] = React.useState>({}) const [isCreating, setIsCreating] = React.useState(false) // 일괄입력 다이얼로그 상태 const [bulkImportDialogOpen, setBulkImportDialogOpen] = React.useState(false) // 인라인 편집 핸들러 (일괄 저장용) const handleCellUpdate = React.useCallback(async (id: string, field: keyof VendorPoolItem, 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 } })) }, []) // 사업자번호 변경 시 자동 vendor 검색 핸들러 const handleTaxIdChange = React.useCallback(async (id: string, taxId: string) => { if (!taxId || taxId.trim() === '') return try { const result = await getVendorByTaxId(taxId.trim()) if (result.data) { // vendor 정보가 있으면 vendorCode와 vendorName을 자동으로 설정 await handleCellUpdate(id, 'vendorCode', result.data.vendorCode || '') await handleCellUpdate(id, 'vendorName', result.data.vendorName || '') toast.success(`사업자번호로 '${result.data.vendorName}' 업체 정보를 자동 입력했습니다.`) } else { // vendor 정보가 없으면 vendorCode와 vendorName을 빈 값으로 설정 await handleCellUpdate(id, 'vendorCode', '') await handleCellUpdate(id, 'vendorName', '') } } catch (error) { console.error('사업자번호 검색 실패:', error) toast.error('사업자번호 검색 중 오류가 발생했습니다.') } }, [handleCellUpdate]) // 자재그룹코드 변경 시 자동 materialGroupName 검색 및 Equip/Bulk 구분 설정 핸들러 const handleMaterialGroupCodeChange = React.useCallback(async (id: string, materialGroupCode: string) => { if (!materialGroupCode || materialGroupCode.trim() === '') return const code = materialGroupCode.trim() try { const materialGroup = await getMaterialGroupDetail(code) if (materialGroup) { // 자재그룹 정보가 있으면 materialGroupName을 자동으로 설정 await handleCellUpdate(id, 'materialGroupName', materialGroup.materialGroupDesc || '') toast.success(`자재그룹코드로 '${materialGroup.materialGroupDesc}' 정보를 자동 입력했습니다.`) } else { // 자재그룹 정보가 없으면 materialGroupName을 빈 값으로 설정 await handleCellUpdate(id, 'materialGroupName', '') } // Equip/Bulk 구분 자동 설정 let equipBulkDivision = '' if (code.startsWith('A1')) { equipBulkDivision = 'S' } else if (code.startsWith('A') || code.startsWith('B7') || code === 'SP1328' || code === 'SP1329') { equipBulkDivision = 'B' } else if (code.startsWith('B')) { equipBulkDivision = 'E' } if (equipBulkDivision) { await handleCellUpdate(id, 'equipBulkDivision', equipBulkDivision) toast.success(`자재그룹코드에 따라 Equip/Bulk 구분을 '${equipBulkDivision}'으로 자동 설정했습니다.`) } else { // Equip/Bulk 구분을 빈 값으로 설정하여 pendingChanges에 반영 await handleCellUpdate(id, 'equipBulkDivision', '') toast.info('현 자재그룹코드에 따라 Equip/Bulk 구분을 자동 설정할 수 없습니다.') } } catch (error) { console.error('자재그룹코드 검색 실패:', error) toast.error('자재그룹코드 검색 중 오류가 발생했습니다.') } }, [handleCellUpdate]) // 편집 취소 핸들러 const handleCellCancel = React.useCallback((id: string, field: keyof VendorPoolItem) => { 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 } }) } else { // 기존 데이터의 경우 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 handleBatchSave = React.useCallback(async () => { if (Object.keys(pendingChanges).length === 0) return setIsSaving(true) let successCount = 0 let errorCount = 0 try { // 각 항목의 변경사항을 순차적으로 저장 for (const [id, changes] of Object.entries(pendingChanges)) { try { // changes에서 id 필드 제거 (서버에서 자동 생성) const { id: _, ...updateData } = changes as any // 최종변경자를 현재 세션 사용자 정보로 설정 const updateDataWithModifier = { ...updateData, lastModifier: session?.user?.name || "" } const result = await updateVendorPool(Number(id), updateDataWithModifier as Partial) if (result) { successCount++ } else { errorCount++ } } catch (error) { console.error(`항목 ${id} 저장 실패:`, error) errorCount++ } } // 저장 완료 후 pendingChanges 초기화 setPendingChanges({}) if (successCount > 0) { toast.success(`${successCount}개 항목이 저장되었습니다.`) // 편집 모드 종료를 위해 테이블 상태 리셋 및 데이터 새로고침 table.resetRowSelection() onRefresh?.() // 데이터 새로고침 } if (errorCount > 0) { toast.error(`${errorCount}개 항목 저장에 실패했습니다.`) } } catch (error) { console.error("Batch save error:", error) toast.error("저장 중 오류가 발생했습니다.") } finally { setIsSaving(false) } }, [pendingChanges, onRefresh]) // 수정사항 존재 여부 const hasPendingChanges = Object.keys(pendingChanges).length > 0 // 빈 행 생성 함수 const createEmptyRow = React.useCallback(() => { if (isCreating) return // 이미 생성 중이면 중복 생성 방지 const tempId = `temp-${Date.now()}` const emptyRow: VendorPoolItem = { id: tempId, no: 0, // 나중에 계산 selected: false, constructionSector: "", htDivision: "", designCategoryCode: "", designCategory: "", equipBulkDivision: "", packageCode: "", packageName: "", materialGroupCode: "", materialGroupName: "", smCode: "", similarMaterialNamePurchase: "", similarMaterialNameOther: "", vendorCode: "", vendorName: "", taxId: "", faTarget: false, faStatus: "", faRemark: "", tier: "", isAgent: false, contractSignerCode: "", contractSignerName: "", headquarterLocation: "", manufacturingLocation: "", avlVendorName: "", similarVendorName: "", hasAvl: false, isBlacklist: false, isBcc: false, purchaseOpinion: "", shipTypeCommon: false, shipTypeAmax: false, shipTypeSmax: false, shipTypeVlcc: false, shipTypeLngc: false, shipTypeCont: false, offshoreTypeCommon: false, offshoreTypeFpso: false, offshoreTypeFlng: false, offshoreTypeFpu: false, offshoreTypePlatform: false, offshoreTypeWtiv: false, offshoreTypeGom: false, picName: "", picEmail: "", picPhone: "", agentName: "", agentEmail: "", agentPhone: "", recentQuoteDate: "", recentQuoteNumber: "", recentOrderDate: "", recentOrderNumber: "", registrationDate: "", registrant: session?.user?.name || "", lastModifiedDate: "", lastModifier: session?.user?.name || "", } setEmptyRows(prev => ({ ...prev, [tempId]: emptyRow })) setIsCreating(true) // 빈 행의 초기값들을 pendingChanges에 설정 (임시 저장용) // emptyRow의 실제 값들을 반영 setPendingChanges(prev => ({ ...prev, [tempId]: { ...emptyRow } })) }, [isCreating]) // 빈 행 저장 함수 const saveEmptyRow = React.useCallback(async (tempId: string) => { const rowData = emptyRows[tempId] const changes = pendingChanges[tempId] if (!rowData || !changes) { console.error('rowData 또는 changes가 없음') return } // emptyRows와 pendingChanges를 병합한 최종 데이터 const finalData = { ...rowData, ...changes } // 필수 필드 검증 (최종 데이터 기준) const requiredFields = ['constructionSector', 'htDivision', 'designCategory', 'taxId', 'vendorName', 'materialGroupCode', 'materialGroupName', 'tier', 'contractSignerName', 'headquarterLocation', 'manufacturingLocation', 'avlVendorName'] // 필드명과 한국어 레이블 매핑 const fieldLabels: Record = { constructionSector: '공사부문', htDivision: 'H/T구분', designCategory: '설계기능', taxId: '사업자번호', vendorName: '협력업체명', materialGroupCode: '자재그룹코드', materialGroupName: '자재그룹명', tier: '등급(Tier)', contractSignerName: '계약서명주체명', headquarterLocation: '위치(국가)', manufacturingLocation: '제작/선적지(국가)', avlVendorName: 'AVL등재업체명' } const missingFields = requiredFields.filter(field => { const value = finalData[field as keyof VendorPoolItem] return !value || value === '' }) if (missingFields.length > 0) { const missingFieldLabels = missingFields.map(field => fieldLabels[field]).join(', ') toast.error(`필수 항목을 입력해주세요: ${missingFieldLabels}`) return } try { setIsSaving(true) // id 필드 제거 (서버에서 자동 생성) const { id: _, no: __, selected: ___, ...createData } = finalData const result = await createVendorPool(createData as Omit) if (result) { toast.success("새 항목이 추가되었습니다.") // 빈 행 제거 setEmptyRows(prev => { const newRows = { ...prev } delete newRows[tempId] return newRows }) setPendingChanges(prev => { const newChanges = { ...prev } delete newChanges[tempId] return newChanges }) setIsCreating(false) // 데이터 새로고침 onRefresh?.() } } catch (error) { console.error("빈 행 저장 실패:", error) toast.error("저장 중 오류가 발생했습니다.") } finally { setIsSaving(false) } }, [emptyRows, pendingChanges, onRefresh]) // 빈 행 취소 함수 const cancelEmptyRow = React.useCallback((tempId: string) => { setEmptyRows(prev => { const newRows = { ...prev } delete newRows[tempId] return newRows }) setPendingChanges(prev => { const newChanges = { ...prev } delete newChanges[tempId] return newChanges }) setIsCreating(false) toast.info("새 항목 추가가 취소되었습니다.") }, []) // 필터 필드 정의 const filterFields: DataTableFilterField[] = [ { id: "constructionSector", label: "공사부문", options: [ { label: "조선", value: "조선" }, { label: "해양", value: "해양" }, ] }, { id: "htDivision", label: "H/T구분", options: [ { label: "H", value: "H" }, { label: "T", value: "T" }, { label: "공통", value: "공통" }, ] }, { id: "equipBulkDivision", label: "Equip/Bulk 구분", options: [ { label: "E (Equip)", value: "E" }, { label: "B (Bulk)", value: "B" }, ] }, { id: "faTarget", label: "FA대상", options: [ { label: "대상", value: "true" }, { label: "비대상", value: "false" }, ] }, { id: "isAgent", label: "Agent 여부", options: [ { label: "Agent", value: "true" }, { label: "일반", value: "false" }, ] }, { id: "hasAvl", label: "AVL 존재", options: [ { label: "있음", value: "true" }, { label: "없음", value: "false" }, ] }, { id: "isBlacklist", label: "Blacklist", options: [ { label: "등록됨", value: "true" }, { label: "미등록", value: "false" }, ] }, { id: "isBcc", label: "BCC", options: [ { label: "등록됨", value: "true" }, { label: "미등록", value: "false" }, ] } ] const advancedFilterFields: DataTableAdvancedFilterField[] = [ { id: "designCategoryCode", label: "설계기능코드", type: "text", }, { id: "designCategory", label: "설계기능(공종)", type: "text", }, { id: "packageCode", label: "패키지 코드", type: "text", }, { id: "packageName", label: "패키지 명", type: "text", }, { id: "materialGroupCode", label: "자재그룹 코드", type: "text", }, { id: "materialGroupName", label: "자재그룹 명", type: "text", }, { id: "vendorCode", label: "협력업체 코드", type: "text", }, { id: "vendorName", label: "협력업체 명", type: "text", }, { id: "taxId", label: "사업자번호", type: "text", }, { id: "faStatus", label: "FA현황", type: "text", }, { id: "tier", label: "등급", type: "text", }, { id: "headquarterLocation", label: "본사 위치", type: "text", }, { id: "manufacturingLocation", label: "제작/선적지", type: "text", }, { id: "avlVendorName", label: "AVL 등재업체명", type: "text", }, { id: "registrant", label: "등재자", type: "text", }, { id: "lastModifier", label: "최종변경자", type: "text", }, { id: "registrationDate", label: "등재일", type: "date", }, { id: "lastModifiedDate", label: "최종변경일", type: "date", }, ] // 빈 행들을 기존 데이터와 합치기 (빈 행들을 최상단에 배치) const combinedData = React.useMemo(() => { const existingData = [...data] const emptyRowList = Object.values(emptyRows) // 빈 행들의 no 필드 업데이트 (음수로 설정하여 최상단에 배치) const updatedEmptyRows = emptyRowList.map((row, index) => ({ ...row, no: -(emptyRowList.length - index) // -3, -2, -1 순으로 설정 })) // 기존 데이터의 no 필드도 1부터 재설정 const updatedExistingData = existingData.map((row, index) => { const originalRow = existingData[index] const rowId = String(originalRow.id) const pendingChange = pendingChanges[rowId] // pendingChanges의 값으로 데이터 병합 const mergedRow = pendingChange ? { ...originalRow, ...pendingChange } : originalRow return { ...mergedRow, no: index + 1 } }) // 빈 행들을 최상단에 배치 return [...updatedEmptyRows, ...updatedExistingData] }, [data, emptyRows, pendingChanges]) const { table } = useDataTable({ data: combinedData, columns, pageCount: pageCount || 0, filterFields, enablePinning: true, enableAdvancedFilter: true, enableColumnResizing: true, columnResizeMode: "onChange", initialState: { sorting: [{ id: "registrationDate", desc: true }], columnPinning: { right: ["actions"] }, }, getRowId: (originalRow) => String(originalRow.id), shallow: false, clearOnDefault: true, }) // 액션 핸들러 const handleAction = React.useCallback(async (action: string, data?: any) => { try { switch (action) { case 'new-registration': createEmptyRow() break case 'bulk-import': setBulkImportDialogOpen(true) break case 'save': toast.info('저장 기능은 개발 중입니다.') break case 'fixed-values': toast.info('고정값 설정 기능은 개발 중입니다.') break case 'delete': if (data?.id && confirm('정말 삭제하시겠습니까?')) { const success = await deleteVendorPool(Number(data.id)) if (success) { toast.success('삭제가 완료되었습니다.') onRefresh?.() // 데이터 새로고침 } else { toast.error('삭제에 실패했습니다.') } } break default: console.log('알 수 없는 액션:', action) toast.error('알 수 없는 액션입니다.') } } catch (error) { console.error('액션 처리 실패:', error) toast.error('액션 처리 중 오류가 발생했습니다.') } }, [table, onRefresh]) // 일괄입력 핸들러 const handleBulkImport = React.useCallback(async (bulkData: Record) => { const selectedRows = table.getFilteredSelectedRowModel().rows if (selectedRows.length === 0) { toast.error('일괄 입력할 행이 선택되지 않았습니다.') return } try { // 선택된 각 행에 대해 값 적용 for (const row of selectedRows) { const rowId = String(row.original.id) // 제공된 값들만 적용 (빈 값이나 undefined는 건너뜀) Object.entries(bulkData).forEach(([field, value]) => { if (value !== undefined && value !== null && value !== '') { handleCellUpdate(rowId, field as keyof VendorPoolItem, value) } }) } toast.success(`${selectedRows.length}개 행에 일괄 입력이 적용되었습니다.`) setBulkImportDialogOpen(false) } catch (error) { console.error('일괄입력 처리 실패:', error) toast.error('일괄입력 처리 중 오류가 발생했습니다.') } }, [table, handleCellUpdate]) // 테이블 메타에 핸들러 설정 table.options.meta = { onAction: handleAction, onCellUpdate: handleCellUpdate, onCellCancel: handleCellCancel, onSaveEmptyRow: saveEmptyRow, onCancelEmptyRow: cancelEmptyRow, isEmptyRow: (id: string) => String(id).startsWith('temp-'), getPendingChanges: () => pendingChanges, onTaxIdChange: handleTaxIdChange, onMaterialGroupCodeChange: handleMaterialGroupCodeChange } // 툴바 액션 핸들러들 const handleToolbarAction = React.useCallback((action: string, data?: any) => { handleAction(action, data) }, [handleAction]) // 저장 버튼 핸들러 const handleSaveChanges = React.useCallback(() => { handleBatchSave() }, [handleBatchSave]) return ( <>
{/* */}
) }