"use client" import * as React from "react" import { useReactTable, getCoreRowModel, getPaginationRowModel, getSortedRowModel, getFilteredRowModel, type ColumnDef } from "@tanstack/react-table" import { DataTable } from "@/components/data-table/data-table" import { Button } from "@/components/ui/button" import { Checkbox } from "@/components/ui/checkbox" import { Input } from "@/components/ui/input" import { ProjectAvlAddDialog } from "./project-avl-add-dialog" import { getProjectAvlVendorInfo, getAvlListById, createAvlVendorInfo, updateAvlVendorInfo, deleteAvlVendorInfo } from "../service" import { getProjectInfoByProjectCode as getProjectInfoFromProjects } from "../../projects/service" import { getProjectInfoByProjectCode as getProjectInfoFromBiddingProjects } from "../../bidding-projects/service" import { GetProjectAvlSchema } from "../validations" import { AvlDetailItem, AvlListItem, AvlVendorInfoInput } from "../types" import { toast } from "sonner" // 프로젝트 AVL 테이블에서는 AvlDetailItem을 사용 export type ProjectAvlItem = AvlDetailItem interface ProjectAvlTableProps { onSelectionChange?: (count: number) => void resetCounter?: number projectCode?: string // 프로젝트 코드 필터 avlListId?: number // AVL 리스트 ID (관리 영역 표시용) onProjectCodeChange?: (projectCode: string) => void // 프로젝트 코드 변경 콜백 } // 프로젝트 AVL 테이블 컬럼 const getProjectAvlColumns = (): ColumnDef[] => [ { id: "select", header: ({ table }) => ( table.toggleAllPageRowsSelected(!!value)} aria-label="Select all" /> ), cell: ({ row, table }) => { // 프로젝트 AVL 테이블의 단일 선택 핸들러 const handleRowSelection = (checked: boolean) => { if (checked) { // 다른 모든 행의 선택 해제 table.getRowModel().rows.forEach(r => { if (r !== row && r.getIsSelected()) { r.toggleSelected(false) } }) } // 현재 행 선택/해제 row.toggleSelected(checked) } return ( ) }, enableSorting: false, enableHiding: false, size: 50, }, { accessorKey: "no", header: "No.", size: 60, cell: ({ row }) => { return ( {row.original.no} ) }, }, { accessorKey: "disciplineName", header: "설계공종", size: 120, cell: ({ row }) => { return ( {row.original.disciplineName} ) }, }, { accessorKey: "materialNameCustomerSide", header: "고객사 AVL 자재명", size: 150, cell: ({ row }) => { return ( {row.original.materialNameCustomerSide} ) }, }, { accessorKey: "materialGroupCode", header: "자재그룹 코드", size: 120, cell: ({ row }) => { return ( {row.original.materialGroupCode} ) }, }, { accessorKey: "materialGroupName", header: "자재그룹 명", size: 130, cell: ({ row }) => { return ( {row.original.materialGroupName} ) }, }, { accessorKey: "avlVendorName", header: "AVL 등재업체명", size: 140, cell: ({ row }) => { return ( {row.original.avlVendorName} ) }, }, { accessorKey: "vendorCode", header: "협력업체 코드", size: 120, cell: ({ row }) => { return ( {row.original.vendorCode} ) }, }, { accessorKey: "vendorName", header: "협력업체 명", size: 130, cell: ({ row }) => { return ( {row.original.vendorName} ) }, }, { accessorKey: "ownerSuggestion", header: "선주제안", size: 100, cell: ({ row }) => { return ( {row.original.ownerSuggestion ? "예" : "아니오"} ) }, }, { accessorKey: "shiSuggestion", header: "SHI 제안", size: 100, cell: ({ row }) => { return ( {row.original.shiSuggestion ? "예" : "아니오"} ) }, }, ] export function ProjectAvlTable({ onSelectionChange, resetCounter, projectCode, avlListId, onProjectCodeChange }: ProjectAvlTableProps) { const [data, setData] = React.useState([]) const [loading, setLoading] = React.useState(false) const [pageCount, setPageCount] = React.useState(0) const [avlListInfo, setAvlListInfo] = React.useState(null) const [originalFile, setOriginalFile] = React.useState("") const [localProjectCode, setLocalProjectCode] = React.useState(projectCode || "") // 행 추가/수정 다이얼로그 상태 const [isAddDialogOpen, setIsAddDialogOpen] = React.useState(false) const [editingItem, setEditingItem] = React.useState(undefined) // 프로젝트 정보 상태 const [projectInfo, setProjectInfo] = React.useState<{ projectName: string constructionSector: string shipType: string htDivision: string } | null>(null) // 프로젝트 검색 상태 const [projectSearchStatus, setProjectSearchStatus] = React.useState<'idle' | 'searching' | 'success-projects' | 'success-bidding' | 'error'>('idle') // 데이터 로드 함수 const loadData = React.useCallback(async (searchParams: Partial) => { try { setLoading(true) const params: any = { page: searchParams.page ?? 1, perPage: searchParams.perPage ?? 10, sort: searchParams.sort ?? [{ id: "no", desc: false }], flags: searchParams.flags ?? [], projectCode: localProjectCode || "", equipBulkDivision: searchParams.equipBulkDivision ?? "", disciplineCode: searchParams.disciplineCode ?? "", disciplineName: searchParams.disciplineName ?? "", materialNameCustomerSide: searchParams.materialNameCustomerSide ?? "", packageCode: searchParams.packageCode ?? "", packageName: searchParams.packageName ?? "", materialGroupCode: searchParams.materialGroupCode ?? "", materialGroupName: searchParams.materialGroupName ?? "", vendorName: searchParams.vendorName ?? "", vendorCode: searchParams.vendorCode ?? "", avlVendorName: searchParams.avlVendorName ?? "", tier: searchParams.tier ?? "", filters: searchParams.filters ?? [], joinOperator: searchParams.joinOperator ?? "and", search: searchParams.search ?? "", } const result = await getProjectAvlVendorInfo(params) setData(result.data) setPageCount(result.pageCount) } catch (error) { console.error("프로젝트 AVL 데이터 로드 실패:", error) setData([]) setPageCount(0) } finally { setLoading(false) } }, [localProjectCode]) // AVL 리스트 정보 로드 React.useEffect(() => { const loadAvlListInfo = async () => { if (avlListId) { try { const info = await getAvlListById(avlListId) setAvlListInfo(info) } catch (error) { console.error("AVL 리스트 정보 로드 실패:", error) } } } loadAvlListInfo() }, [avlListId]) // 초기 데이터 로드 React.useEffect(() => { if (localProjectCode) { loadData({}) } }, [loadData, localProjectCode]) // 파일 업로드 핸들러 const handleFileUpload = React.useCallback((event: React.ChangeEvent) => { const file = event.target.files?.[0] if (file) { setOriginalFile(file.name) // TODO: 실제 파일 업로드 로직 구현 console.log("파일 업로드:", file.name) } }, []) // 프로젝트 코드 변경 핸들러 const handleProjectCodeChange = React.useCallback(async (value: string) => { setLocalProjectCode(value) onProjectCodeChange?.(value) // 프로젝트 코드가 입력된 경우 프로젝트 정보 조회 if (value.trim()) { setProjectSearchStatus('searching') // 검색 시작 상태로 변경 try { // 1. projects 테이블에서 먼저 검색 let projectData = null let searchSource = 'projects' try { projectData = await getProjectInfoFromProjects(value.trim()) // projects에서 찾았을 때만 즉시 성공 상태로 변경 setProjectSearchStatus('success-projects') } catch (projectsError) { // projects 테이블에 없는 경우 biddingProjects 테이블에서 검색 try { projectData = await getProjectInfoFromBiddingProjects(value.trim()) if (projectData) { searchSource = 'bidding-projects' setProjectSearchStatus('success-bidding') // bidding에서 찾았을 때 성공 상태로 변경 } else { // 둘 다 실패한 경우에만 에러 상태로 변경 setProjectInfo(null) setProjectSearchStatus('error') setData([]) setPageCount(0) toast.error("입력하신 프로젝트 코드를 찾을 수 없습니다.") return } } catch (biddingError) { // biddingProjects에서도 에러가 발생한 경우 setProjectInfo(null) setProjectSearchStatus('error') setData([]) setPageCount(0) toast.error("입력하신 프로젝트 코드를 찾을 수 없습니다.") return } } if (projectData) { setProjectInfo({ projectName: projectData.projectName || "", constructionSector: "조선", // 기본값으로 조선 설정 (필요시 로직 변경) shipType: projectData.shipType || projectData.projectMsrm || "", htDivision: projectData.projectHtDivision || "" }) const sourceMessage = searchSource === 'projects' ? '프로젝트' : '견적프로젝트' toast.success(`${sourceMessage}에서 프로젝트 정보를 성공적으로 불러왔습니다.`) // 프로젝트 검증 성공 시 해당 프로젝트의 AVL 데이터 로드 loadData({}) } } catch (error) { console.error("프로젝트 정보 조회 실패:", error) setProjectInfo(null) setProjectSearchStatus('error') setData([]) setPageCount(0) toast.error("프로젝트 정보를 불러오는데 실패했습니다.") } } else { // 프로젝트 코드가 비어있는 경우 프로젝트 정보 초기화 및 데이터 클리어 setProjectInfo(null) setProjectSearchStatus('idle') setData([]) setPageCount(0) } }, [onProjectCodeChange]) // 행 추가 핸들러 const handleAddRow = React.useCallback(() => { if (!localProjectCode.trim()) { toast.error("프로젝트 코드를 먼저 입력해주세요.") return } if (!projectInfo) { toast.error("프로젝트 정보를 불러올 수 없습니다.") return } setIsAddDialogOpen(true) }, [localProjectCode, projectInfo]) // 다이얼로그에서 항목 추가 핸들러 const handleAddItem = React.useCallback(async (itemData: Omit) => { try { // AVL 리스트 ID 확인 if (!avlListId) { toast.error("AVL 리스트가 선택되지 않았습니다.") return } // DB에 실제 저장할 데이터 준비 const saveData: AvlVendorInfoInput = { ...itemData, avlListId: avlListId // 현재 AVL 리스트 ID 설정 } // DB에 저장 const result = await createAvlVendorInfo(saveData) if (result) { toast.success("새 항목이 성공적으로 추가되었습니다.") // 데이터 새로고침 loadData({}) } else { toast.error("항목 추가에 실패했습니다.") } } catch (error) { console.error("항목 추가 실패:", error) toast.error("항목 추가 중 오류가 발생했습니다.") } }, [avlListId, loadData]) // 다이얼로그에서 항목 수정 핸들러 const handleUpdateItem = React.useCallback(async (id: number, itemData: Omit) => { try { // DB에 실제 수정 const result = await updateAvlVendorInfo(id, itemData) if (result) { toast.success("항목이 성공적으로 수정되었습니다.") // 데이터 새로고침 loadData({}) // 다이얼로그 닫기 및 수정 모드 해제 setIsAddDialogOpen(false) setEditingItem(undefined) } else { toast.error("항목 수정에 실패했습니다.") } } catch (error) { console.error("항목 수정 실패:", error) toast.error("항목 수정 중 오류가 발생했습니다.") } }, [loadData]) // 테이블 메타 설정 const tableMeta = React.useMemo(() => ({}), []) const table = useReactTable({ data, columns: getProjectAvlColumns(), getCoreRowModel: getCoreRowModel(), getPaginationRowModel: getPaginationRowModel(), getSortedRowModel: getSortedRowModel(), getFilteredRowModel: getFilteredRowModel(), manualPagination: true, pageCount, initialState: { pagination: { pageSize: 10, }, }, onPaginationChange: (updater) => { const newState = typeof updater === 'function' ? updater(table.getState().pagination) : updater loadData({ page: newState.pageIndex + 1, perPage: newState.pageSize, }) }, meta: tableMeta, }) // 항목 수정 핸들러 (버튼 클릭) const handleEditItem = React.useCallback(() => { const selectedRows = table.getFilteredSelectedRowModel().rows if (selectedRows.length !== 1) { toast.error("수정할 항목을 하나만 선택해주세요.") return } const selectedItem = selectedRows[0].original setEditingItem(selectedItem) setIsAddDialogOpen(true) }, [table]) // 항목 삭제 핸들러 const handleDeleteItems = React.useCallback(async () => { const selectedRows = table.getFilteredSelectedRowModel().rows if (selectedRows.length === 0) { toast.error("삭제할 항목을 선택해주세요.") return } // 사용자 확인 const confirmed = window.confirm(`선택한 ${selectedRows.length}개 항목을 정말 삭제하시겠습니까?`) if (!confirmed) return try { // 선택된 항목들을 DB에서 삭제 const deletePromises = selectedRows.map(async (row) => { await deleteAvlVendorInfo(row.original.id) }) await Promise.all(deletePromises) toast.success(`${selectedRows.length}개 항목이 삭제되었습니다.`) // 데이터 새로고침 loadData({}) // 선택 해제 table.toggleAllPageRowsSelected(false) } catch (error) { console.error("항목 삭제 실패:", error) toast.error("항목 삭제 중 오류가 발생했습니다.") } }, [table, loadData]) // 선택 상태 변경 시 콜백 호출 React.useEffect(() => { const selectedRows = table.getFilteredSelectedRowModel().rows onSelectionChange?.(selectedRows.length) }, [table.getFilteredSelectedRowModel().rows.length, onSelectionChange]) // 선택 해제 요청이 오면 모든 선택 해제 React.useEffect(() => { if (resetCounter && resetCounter > 0) { table.toggleAllPageRowsSelected(false) } }, [resetCounter, table]) return (

프로젝트 AVL

{/* 최종 확정 버튼 */}
{/* 조회대상 관리영역 */}
{/* 프로젝트 코드 */}
handleProjectCodeChange(e.target.value)} placeholder="프로젝트 코드를 입력하세요" // disabled={projectSearchStatus === 'searching'} className={`h-8 text-sm ${ projectSearchStatus === 'error' ? 'border-red-300 focus:border-red-500 focus:ring-red-500/20' : projectSearchStatus === 'success-projects' || projectSearchStatus === 'success-bidding' ? 'border-green-300 focus:border-green-500 focus:ring-green-500/20' : projectSearchStatus === 'searching' ? 'border-blue-300 focus:border-blue-500 focus:ring-blue-500/20' : '' }`} />
{/* 프로젝트명 */}
{projectSearchStatus === 'searching' ? '조회 중...' : (projectInfo?.projectName || '-')}
{/* 원본파일 */}
{originalFile ? ( {originalFile} ) : (
)}
{/* 공사부문 */}
{projectSearchStatus === 'searching' ? '조회 중...' : (projectInfo?.constructionSector || '-')}
{/* 선종 */}
{projectSearchStatus === 'searching' ? '조회 중...' : (projectInfo?.shipType || '-')}
{/* H/T 구분 */}
{projectSearchStatus === 'searching' ? '조회 중...' : (projectInfo?.htDivision === 'H' ? 'Hull (H)' : projectInfo?.htDivision === 'T' ? 'Topside (T)' : '-')}
{/* 행 추가/수정 다이얼로그 */} { setIsAddDialogOpen(open) if (!open) { setEditingItem(undefined) // 다이얼로그가 닫힐 때 수정 모드 해제 } }} onAddItem={handleAddItem} editingItem={editingItem} onUpdateItem={handleUpdateItem} />
) }