"use client" import * as React from "react" import { useReactTable, getCoreRowModel, getPaginationRowModel, getSortedRowModel, getFilteredRowModel } from "@tanstack/react-table" import { forwardRef, useImperativeHandle, useLayoutEffect, useMemo } from "react" import { DataTable } from "@/components/data-table/data-table" import { Button } from "@/components/ui/button" import { AvlVendorAddAndModifyDialog } from "./avl-vendor-add-and-modify-dialog" import { getProjectAvlVendorInfo, createAvlVendorInfo, updateAvlVendorInfo, deleteAvlVendorInfo, finalizeProjectAvl } from "../service" import { getProjectInfoByProjectCode as getProjectInfoFromProjects } from "../../projects/service" import { getProjectInfoByProjectCode as getProjectInfoFromBiddingProjects } from "../../bidding-projects/service" import { GetProjectAvlSchema } from "../validations" import { AvlDetailItem, AvlVendorInfoInput } from "../types" import { toast } from "sonner" import { getProjectAvlColumns } from "./project-avl-table-columns" import { ProjectDisplayField, ProjectFileField } from "../components/project-field-components" import { ProjectSearchStatus } from "../components/project-field-utils" import { useSession } from "next-auth/react" // 프로젝트 AVL 테이블에서는 AvlDetailItem을 사용 export type ProjectAvlItem = AvlDetailItem // ref를 통해 외부에서 접근할 수 있는 메소드들 export interface ProjectAvlTableRef { getSelectedIds: () => number[] } interface ProjectAvlTableProps { onSelectionChange?: (count: number) => void resetCounter?: number projectCode?: string // 프로젝트 코드 필터 avlListId?: number // AVL 리스트 ID (관리 영역 표시용) onProjectCodeChange?: (projectCode: string) => void // 프로젝트 코드 변경 콜백 reloadTrigger?: number } export const ProjectAvlTable = forwardRef(({ onSelectionChange, resetCounter, projectCode, avlListId, onProjectCodeChange, reloadTrigger }, ref) => { const { data: sessionData } = useSession() const [data, setData] = React.useState([]) const [pageCount, setPageCount] = React.useState(0) 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') // 검색 버튼 클릭 여부 상태 const [isSearchClicked, setIsSearchClicked] = React.useState(false) // 페이지네이션 상태 const [pagination, setPagination] = React.useState({ pageIndex: 0, pageSize: 10, }) // 데이터 로드 함수 const loadData = React.useCallback(async (searchParams: Partial = {}) => { try { const params = { page: searchParams.page ?? 1, perPage: searchParams.perPage ?? 10, sort: searchParams.sort ?? [{ id: "no", desc: false }], flags: searchParams.flags ?? [], projectCode: localProjectCode || "", equipBulkDivision: (searchParams.equipBulkDivision as "EQUIP" | "BULK") ?? "EQUIP", 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 ?? "", } console.log('ProjectAvlTable - API call params:', params) const result = await getProjectAvlVendorInfo(params) console.log('ProjectAvlTable - API result:', { dataCount: result.data.length, pageCount: result.pageCount, requestedPage: params.page }) setData(result.data) setPageCount(result.pageCount) } catch (error) { console.error("프로젝트 AVL 데이터 로드 실패:", error) setData([]) setPageCount(0) } finally { // 로딩 상태 처리 완료 } }, [localProjectCode]) // reloadTrigger가 변경될 때마다 데이터 리로드 React.useEffect(() => { if (reloadTrigger && reloadTrigger > 0) { console.log('ProjectAvlTable - reloadTrigger changed, reloading data') loadData({}) } }, [reloadTrigger, loadData]) // 초기 데이터 로드 (검색 버튼이 눌렸을 때만) React.useEffect(() => { if (localProjectCode && isSearchClicked) { loadData({}) } }, [loadData, localProjectCode, isSearchClicked]) // 파일 업로드 핸들러 const handleFileUpload = React.useCallback((event: React.ChangeEvent) => { const file = event.target.files?.[0] if (file) { setOriginalFile(file.name) // TODO: 실제 파일 업로드 로직 구현 console.log("파일 업로드:", file.name) } }, []) // 프로젝트 검색 함수 (공통 로직) const searchProject = React.useCallback(async (projectCode: string) => { if (!projectCode.trim()) { setProjectInfo(null) setProjectSearchStatus('idle') setData([]) setPageCount(0) return } setProjectSearchStatus('searching') // 검색 시작 상태로 변경 try { // 1. projects 테이블에서 먼저 검색 let projectData: { projectName?: string | null; shipType?: string; projectMsrm?: string | null; projectHtDivision?: string | null; } | null = null let searchSource = 'projects' try { projectData = await getProjectInfoFromProjects(projectCode.trim()) // projects에서 찾았을 때만 즉시 성공 상태로 변경 setProjectSearchStatus('success-projects') } catch { // projects 테이블에 없는 경우 biddingProjects 테이블에서 검색 try { projectData = await getProjectInfoFromBiddingProjects(projectCode.trim()) if (projectData) { searchSource = 'bidding-projects' setProjectSearchStatus('success-bidding') // bidding에서 찾았을 때 성공 상태로 변경 } else { // 둘 다 실패한 경우에만 에러 상태로 변경 setProjectInfo(null) setProjectSearchStatus('error') setData([]) setPageCount(0) toast.error("입력하신 프로젝트 코드를 찾을 수 없습니다.") return } } catch { // 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 데이터 로드 트리거 setIsSearchClicked(true) } } catch (error) { console.error("프로젝트 정보 조회 실패:", error) setProjectInfo(null) setProjectSearchStatus('error') setData([]) setPageCount(0) toast.error("프로젝트 정보를 불러오는데 실패했습니다.") } }, [setIsSearchClicked]) // 프로젝트 코드 변경 핸들러 (입력만 처리) const handleProjectCodeChange = React.useCallback((value: string) => { setLocalProjectCode(value) onProjectCodeChange?.(value) // 입력이 변경되면 검색 상태를 idle로 초기화하고 검색 클릭 상태를 리셋 if (!value.trim()) { setProjectInfo(null) setProjectSearchStatus('idle') setIsSearchClicked(false) setData([]) setPageCount(0) } else { // 새로운 프로젝트 코드가 입력되면 검색 클릭 상태를 리셋 (다시 검색 버튼을 눌러야 함) setIsSearchClicked(false) } }, [onProjectCodeChange]) // 프로젝트 검색 버튼 핸들러 const handleProjectSearch = React.useCallback(async () => { // 검색 시 페이지를 1페이지로 리셋 setPagination(prev => ({ ...prev, pageIndex: 0 })) // 프로젝트 정보 검색 (성공 시 내부에서 AVL 데이터 로드 트리거) await searchProject(localProjectCode) }, [localProjectCode, searchProject]) // 행 추가 핸들러 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 { // DB에 실제 저장할 데이터 준비 (avlListId는 나중에 최종 확정 시 설정) const saveData: AvlVendorInfoInput = { ...itemData, projectCode: localProjectCode, // 현재 프로젝트 코드 저장 avlListId: avlListId || undefined // 있으면 설정, 없으면 undefined (null로 저장됨) } // DB에 저장 const result = await createAvlVendorInfo(saveData) if (result) { toast.success("새 항목이 성공적으로 추가되었습니다.") // 데이터 새로고침 loadData({}) } else { toast.error("항목 추가에 실패했습니다.") } } catch (error) { console.error("항목 추가 실패:", error) toast.error("항목 추가 중 오류가 발생했습니다.") } }, [avlListId, loadData, localProjectCode]) // 다이얼로그에서 항목 수정 핸들러 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, state: { pagination, }, onPaginationChange: (updater) => { // 페이지네이션 상태 업데이트 const newPaginationState = typeof updater === 'function' ? updater(pagination) : updater console.log('ProjectAvlTable - Pagination changed:', { currentState: pagination, newPaginationState, localProjectCode, isSearchClicked, willLoadData: localProjectCode && isSearchClicked }) setPagination(newPaginationState) if (localProjectCode && isSearchClicked) { const apiParams = { page: newPaginationState.pageIndex + 1, perPage: newPaginationState.pageSize, } console.log('ProjectAvlTable - Loading data with params:', apiParams) loadData(apiParams) } }, meta: tableMeta, }) // 외부에서 선택된 ID들을 가져올 수 있도록 ref에 메소드 노출 useImperativeHandle(ref, () => ({ getSelectedIds: () => { const selectedRows = table.getFilteredSelectedRowModel().rows return selectedRows.map(row => row.original.id) } })) // 항목 수정 핸들러 (버튼 클릭) 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]) // 최종 확정 핸들러 const handleFinalizeAvl = React.useCallback(async () => { // 1. 필수 조건 검증 if (!localProjectCode.trim()) { toast.error("프로젝트 코드를 먼저 입력해주세요.") return } if (!projectInfo) { toast.error("프로젝트 정보를 불러올 수 없습니다. 프로젝트 코드를 다시 확인해주세요.") return } if (data.length === 0) { toast.error("확정할 AVL 벤더 정보가 없습니다.") return } // 2. 사용자 확인 const confirmed = window.confirm( `현재 프로젝트(${localProjectCode})의 AVL을 최종 확정하시겠습니까?\n\n` + `- 프로젝트명: ${projectInfo.projectName}\n` + `- 벤더 정보: ${data.length}개\n` + `- 공사부문: ${projectInfo.constructionSector}\n` + `- 선종: ${projectInfo.shipType}\n` + `- H/T 구분: ${projectInfo.htDivision}\n\n` + `확정 후에는 수정이 어려울 수 있습니다.` ) if (!confirmed) return try { // 3. 현재 데이터의 모든 ID 수집 const avlVendorInfoIds = data.map(item => item.id) // 4. 최종 확정 실행 const result = await finalizeProjectAvl( localProjectCode, projectInfo, avlVendorInfoIds, sessionData?.user?.name || "" ) if (result.success) { toast.success(result.message) // 5. 데이터 새로고침 loadData({}) // 6. 선택 해제 table.toggleAllPageRowsSelected(false) } else { toast.error(result.message) } } catch (error) { console.error("AVL 최종 확정 실패:", error) toast.error("AVL 최종 확정 중 오류가 발생했습니다.") } }, [localProjectCode, projectInfo, data, table, loadData, sessionData?.user?.name]) // 선택된 행 개수 (안정적인 계산을 위해 useMemo 사용) const selectedRows = table.getFilteredSelectedRowModel().rows const selectedRowCount = useMemo(() => { const count = selectedRows.length console.log('ProjectAvlTable - selectedRowCount calculated:', count) return count }, [selectedRows]) // 선택 상태 변경 시 콜백 호출 useLayoutEffect(() => { console.log('ProjectAvlTable - onSelectionChange called with count:', selectedRowCount) onSelectionChange?.(selectedRowCount) }, [selectedRowCount, onSelectionChange]) // 선택 해제 요청이 오면 모든 선택 해제 React.useEffect(() => { if (resetCounter && resetCounter > 0) { table.toggleAllPageRowsSelected(false) } }, [resetCounter, table]) return (

프로젝트 AVL

{/* */} {/* 최종 확정 버튼 */}
{/* 조회대상 관리영역 */}
{/* 프로젝트 코드 */}
handleProjectCodeChange(e.target.value)} placeholder="프로젝트 코드를 입력하세요" className={`flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 ${ projectSearchStatus === 'error' ? 'border-red-500' : projectSearchStatus === 'success-projects' || projectSearchStatus === 'success-bidding' ? 'border-green-500' : projectSearchStatus === 'searching' ? 'border-blue-500' : '' }`} disabled={projectSearchStatus === 'searching'} /> {projectSearchStatus !== 'idle' && (
{projectSearchStatus === 'success-projects' ? '(프로젝트)' : projectSearchStatus === 'success-bidding' ? '(견적프로젝트)' : projectSearchStatus === 'searching' ? '(검색 중...)' : projectSearchStatus === 'error' ? '(찾을 수 없음)' : undefined}
)}
{/* 프로젝트명 */} {/* 원본파일 */} {/* 공사부문 */} {/* 선종 */} {/* H/T 구분 */} value === 'H' ? 'Hull (H)' : value === 'T' ? 'Topside (T)' : '-' } />
{/* 행 추가/수정 다이얼로그 */} { setIsAddDialogOpen(open) if (!open) { setEditingItem(undefined) // 다이얼로그가 닫힐 때 수정 모드 해제 } }} onAddItem={handleAddItem} editingItem={editingItem} onUpdateItem={handleUpdateItem} isTemplate={false} // 프로젝트 AVL 모드 initialProjectCode={localProjectCode} />
) }) ProjectAvlTable.displayName = "ProjectAvlTable"