"use client" import * as React from "react" import { useDataTable } from "@/hooks/use-data-table" import { DataTable } from "@/components/data-table/data-table" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { toast } from "sonner" import { columns, type AvlDetailItem } from "./columns-detail" import { createAvlVendorInfo, updateAvlVendorInfo, deleteAvlVendorInfo, handleAvlAction } from "../service" import type { AvlDetailItem as AvlDetailType } from "../types" // 테이블 메타 타입 확장 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 AvlDetailTableProps { data: AvlDetailItem[] pageCount?: number avlListId: number // 상위 AVL 리스트 ID onRefresh?: () => void // 데이터 새로고침 콜백 avlType?: '프로젝트AVL' | '선종별표준AVL' // AVL 타입 projectCode?: string // 프로젝트 코드 shipOwnerName?: string // 선주명 businessType?: string // 사업 유형 (예: 조선/해양) } export function AvlDetailTable({ data, pageCount, avlListId, onRefresh, avlType = '프로젝트AVL', projectCode, shipOwnerName, businessType = '조선' }: AvlDetailTableProps) { // 수정사항 추적 (일괄 저장용) const [pendingChanges, setPendingChanges] = React.useState>>({}) const [isSaving, setIsSaving] = React.useState(false) // 빈 행 관리 (신규 등록용) const [emptyRows, setEmptyRows] = React.useState>({}) const [isCreating, setIsCreating] = React.useState(false) // 검색 상태 const [searchValue, setSearchValue] = React.useState("") // 인라인 편집 핸들러 (일괄 저장용) const handleCellUpdate = React.useCallback(async (id: string, field: keyof AvlDetailItem, 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 AvlDetailItem) => { 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-vendor': // 신규 협력업체 추가 - 빈 행 추가 const tempId = `temp-${Date.now()}` const newEmptyRow: AvlDetailItem = { id: tempId, no: 0, selected: false, avlListId: avlListId, equipBulkDivision: "EQUIP", disciplineCode: "", disciplineName: "", materialNameCustomerSide: "", packageCode: "", packageName: "", materialGroupCode: "", materialGroupName: "", vendorId: undefined, vendorName: "", vendorCode: "", avlVendorName: "", tier: "", faTarget: false, faStatus: "", isAgent: false, agentStatus: "아니오", contractSignerId: undefined, contractSignerName: "", contractSignerCode: "", headquarterLocation: "", manufacturingLocation: "", shiAvl: false, shiBlacklist: false, shiBcc: false, salesQuoteNumber: "", quoteCode: "", salesVendorInfo: "", salesCountry: "", totalAmount: "", quoteReceivedDate: "", recentQuoteDate: "", recentQuoteNumber: "", recentOrderDate: "", recentOrderNumber: "", remarks: "", createdAt: new Date().toISOString().split('T')[0], updatedAt: new Date().toISOString().split('T')[0], } setEmptyRows(prev => ({ ...prev, [tempId]: newEmptyRow })) toast.success("신규 협력업체 행이 추가되었습니다.") break case 'bulk-import': // 일괄 입력 const bulkResult = await handleAvlAction('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 numericId = Number(id) if (isNaN(numericId)) { throw new Error(`유효하지 않은 ID: ${id}`) } const result = await updateAvlVendorInfo(numericId, changes) 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 numericId = Number(data.id) if (isNaN(numericId)) { toast.error("유효하지 않은 항목 ID입니다.") return } const confirmed = window.confirm(`항목 ${data.id}을(를) 삭제하시겠습니까?`) if (!confirmed) return try { const result = await deleteAvlVendorInfo(numericId) if (result) { toast.success("항목이 삭제되었습니다.") onRefresh?.() } else { toast.error("삭제에 실패했습니다.") } } catch (error) { console.error('삭제 실패:', error) toast.error("삭제 중 오류가 발생했습니다.") } break case 'avl-form': // AVL 양식 다운로드/보기 toast.info("AVL 양식을 준비 중입니다.") // TODO: AVL 양식 다운로드 로직 구현 break case 'quote-request': // 견적 요청 toast.info("견적 요청을 처리 중입니다.") // TODO: 견적 요청 로직 구현 break case 'vendor-pool': // Vendor Pool 관리 toast.info("Vendor Pool을 열고 있습니다.") // TODO: Vendor Pool 페이지 이동 또는 모달 열기 로직 구현 break case 'download': // 데이터 다운로드 toast.info("데이터를 다운로드 중입니다.") // TODO: 데이터 다운로드 로직 구현 break default: toast.error(`알 수 없는 액션: ${action}`) } } catch (error) { console.error('액션 처리 실패:', error) toast.error("액션 처리 중 오류가 발생했습니다.") } }, [pendingChanges, onRefresh, avlListId]) // 빈 행 저장 핸들러 const handleSaveEmptyRow = React.useCallback(async (tempId: string) => { const emptyRow = emptyRows[tempId] if (!emptyRow) return try { setIsCreating(true) // 필수 필드 검증 if (!emptyRow.disciplineName || !emptyRow.vendorName) { toast.error("설계공종과 협력업체명은 필수 입력 항목입니다.") return } // 빈 행 데이터를 생성 데이터로 변환 const createData = { avlListId: emptyRow.avlListId, equipBulkDivision: emptyRow.equipBulkDivision, disciplineCode: emptyRow.disciplineCode || undefined, disciplineName: emptyRow.disciplineName, materialNameCustomerSide: emptyRow.materialNameCustomerSide || undefined, packageCode: emptyRow.packageCode || undefined, packageName: emptyRow.packageName || undefined, materialGroupCode: emptyRow.materialGroupCode || undefined, materialGroupName: emptyRow.materialGroupName || undefined, vendorId: emptyRow.vendorId, vendorName: emptyRow.vendorName, vendorCode: emptyRow.vendorCode || undefined, avlVendorName: emptyRow.avlVendorName || undefined, tier: emptyRow.tier || undefined, faTarget: emptyRow.faTarget ?? false, faStatus: emptyRow.faStatus || undefined, isAgent: emptyRow.isAgent ?? false, contractSignerId: emptyRow.contractSignerId, contractSignerName: emptyRow.contractSignerName || undefined, contractSignerCode: emptyRow.contractSignerCode || undefined, headquarterLocation: emptyRow.headquarterLocation || undefined, manufacturingLocation: emptyRow.manufacturingLocation || undefined, hasAvl: emptyRow.shiAvl ?? false, isBlacklist: emptyRow.shiBlacklist ?? false, isBcc: emptyRow.shiBcc ?? false, techQuoteNumber: emptyRow.salesQuoteNumber || undefined, quoteCode: emptyRow.quoteCode || undefined, quoteVendorId: emptyRow.vendorId, quoteVendorName: emptyRow.salesVendorInfo || undefined, quoteVendorCode: emptyRow.vendorCode, quoteCountry: emptyRow.salesCountry || undefined, quoteTotalAmount: emptyRow.totalAmount ? parseFloat(emptyRow.totalAmount.replace(/,/g, '')) : undefined, quoteReceivedDate: emptyRow.quoteReceivedDate || undefined, recentQuoteDate: emptyRow.recentQuoteDate || undefined, recentQuoteNumber: emptyRow.recentQuoteNumber || undefined, recentOrderDate: emptyRow.recentOrderDate || undefined, recentOrderNumber: emptyRow.recentOrderNumber || undefined, remark: emptyRow.remarks || undefined, } const result = await createAvlVendorInfo(createData) 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(() => { const emptyRowArray = Object.values(emptyRows) return [...data, ...emptyRowArray] }, [data, emptyRows]) // 테이블 메타 설정 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, pageCount, initialState: { sorting: [{ id: "no", desc: false }], columnPinning: { right: ["actions"] }, }, getRowId: (row) => String(row.id), meta: tableMeta, }) // 변경사항이 있는지 확인 const hasPendingChanges = Object.keys(pendingChanges).length > 0 const hasEmptyRows = Object.keys(emptyRows).length > 0 return (
{/* 상단 정보 표시 영역 */}

AVL 상세내역

{avlType} [{businessType}] {projectCode || '프로젝트코드'} ({shipOwnerName || '선주명'})
{/* 상단 버튼 및 검색 영역 */}
setSearchValue(e.target.value)} />
{/* 데이터 테이블 */} {/* 디버그 정보 (개발 환경에서만 표시) */} {process.env.NODE_ENV === 'development' && (hasPendingChanges || hasEmptyRows) && (
Pending Changes: {Object.keys(pendingChanges).length}
Empty Rows: {Object.keys(emptyRows).length}
)}
) }