"use client" import * as React from "react" import { useReactTable, getCoreRowModel, getPaginationRowModel, getSortedRowModel, getFilteredRowModel } from "@tanstack/react-table" import { useLayoutEffect, useMemo, forwardRef, useImperativeHandle } from "react" import { DataTable } from "@/components/data-table/data-table" import { Button } from "@/components/ui/button" import { getStandardAvlVendorInfo } from "../service" import { GetStandardAvlSchema } from "../validations" import { AvlDetailItem } from "../types" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select" import { Search } from "lucide-react" import { toast } from "sonner" import { standardAvlColumns } from "./standard-avl-table-columns" import { AvlVendorAddAndModifyDialog } from "./avl-vendor-add-and-modify-dialog" import { createAvlVendorInfo, updateAvlVendorInfo, deleteAvlVendorInfo, finalizeStandardAvl } from "../service" import { AvlVendorInfoInput } from "../types" import { useSession } from "next-auth/react" /** * 조선인 경우, 선종: * A-max, S-max, VLCC, LNGC, CONT * 해양인 경우, 선종: * FPSO, FLNG, FPU, Platform, WTIV, GOM * * AVL종류: * Nearshore, Offshore, IOC, NOC */ // 검색 옵션들 const constructionSectorOptions = [ { value: "조선", label: "조선" }, { value: "해양", label: "해양" }, ] // 공사부문에 따른 선종 옵션들 const getShipTypeOptions = (constructionSector: string) => { if (constructionSector === "조선") { return [ { value: "A-max", label: "A-max" }, { value: "S-max", label: "S-max" }, { value: "VLCC", label: "VLCC" }, { value: "LNGC", label: "LNGC" }, { value: "CONT", label: "CONT" }, ] } else if (constructionSector === "해양") { return [ { value: "FPSO", label: "FPSO" }, { value: "FLNG", label: "FLNG" }, { value: "FPU", label: "FPU" }, { value: "Platform", label: "Platform" }, { value: "WTIV", label: "WTIV" }, { value: "GOM", label: "GOM" }, ] } else { // 공사부문이 선택되지 않은 경우 빈 배열 return [] } } const avlKindOptions = [ { value: "Nearshore", label: "Nearshore" }, { value: "Offshore", label: "Offshore" }, { value: "IOC", label: "IOC" }, { value: "NOC", label: "NOC" }, ] const htDivisionOptions = [ { value: "공통", label: "공통" }, { value: "H", label: "Hull (H)" }, { value: "T", label: "Top (T)" }, ] // 선종별 표준 AVL 테이블에서는 AvlDetailItem을 사용 export type StandardAvlItem = AvlDetailItem // ref를 통해 외부에서 접근할 수 있는 메소드들 export interface StandardAvlTableRef { getSelectedIds: () => number[] } interface StandardAvlTableProps { onSelectionChange?: (count: number) => void resetCounter?: number constructionSector?: string // 공사부문 필터 shipType?: string // 선종 필터 avlKind?: string // AVL 종류 필터 htDivision?: string // H/T 구분 필터 onSearchConditionsChange?: (conditions: { constructionSector: string shipType: string avlKind: string htDivision: string }) => void reloadTrigger?: number } export const StandardAvlTable = forwardRef(({ onSelectionChange, resetCounter, constructionSector: initialConstructionSector, shipType: initialShipType, avlKind: initialAvlKind, htDivision: initialHtDivision, onSearchConditionsChange, reloadTrigger }, ref) => { const { data: sessionData } = useSession() const [data, setData] = React.useState([]) const [loading, setLoading] = React.useState(false) const [pageCount, setPageCount] = React.useState(0) // 다이얼로그 상태 const [isAddDialogOpen, setIsAddDialogOpen] = React.useState(false) const [editingItem, setEditingItem] = React.useState(undefined) // 검색 상태 const [searchConstructionSector, setSearchConstructionSector] = React.useState(initialConstructionSector || "") const [searchShipType, setSearchShipType] = React.useState(initialShipType || "") const [searchAvlKind, setSearchAvlKind] = React.useState(initialAvlKind || "") const [searchHtDivision, setSearchHtDivision] = React.useState(initialHtDivision || "") // 페이지네이션 상태 const [pagination, setPagination] = React.useState({ pageIndex: 0, pageSize: 10, }) const table = useReactTable({ data, columns: standardAvlColumns, 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('StandardAvlTable - Pagination changed:', { currentState: pagination, newPaginationState, isAllSearchConditionsSelected, willLoadData: isAllSearchConditionsSelected }) setPagination(newPaginationState) if (isAllSearchConditionsSelected) { const apiParams = { page: newPaginationState.pageIndex + 1, perPage: newPaginationState.pageSize, } console.log('StandardAvlTable - Loading data with params:', apiParams) loadData(apiParams) } }, }) // 공사부문 변경 시 선종 초기화 const handleConstructionSectorChange = React.useCallback((value: string) => { setSearchConstructionSector(value) // 공사부문이 변경되면 선종을 빈 값으로 초기화 setSearchShipType("") }, []) // 검색 상태 변경 시 부모 컴포넌트에 전달 React.useEffect(() => { onSearchConditionsChange?.({ constructionSector: searchConstructionSector, shipType: searchShipType, avlKind: searchAvlKind, htDivision: searchHtDivision }) }, [searchConstructionSector, searchShipType, searchAvlKind, searchHtDivision, onSearchConditionsChange]) // 현재 공사부문에 따른 선종 옵션들 const currentShipTypeOptions = React.useMemo(() => getShipTypeOptions(searchConstructionSector), [searchConstructionSector] ) // 모든 검색 조건이 선택되었는지 확인 const isAllSearchConditionsSelected = React.useMemo(() => { return ( searchConstructionSector.trim() !== "" && searchShipType.trim() !== "" && searchAvlKind.trim() !== "" && searchHtDivision.trim() !== "" ) }, [searchConstructionSector, searchShipType, searchAvlKind, searchHtDivision]) // 데이터 로드 함수 const loadData = React.useCallback(async (searchParams: Partial = {}) => { try { setLoading(true) const params: GetStandardAvlSchema = { page: searchParams.page ?? 1, perPage: searchParams.perPage ?? 10, sort: searchParams.sort ?? [{ id: "no", desc: false }], flags: searchParams.flags ?? [], constructionSector: searchConstructionSector, shipType: searchShipType, avlKind: searchAvlKind, htDivision: searchHtDivision as "공통" | "H" | "T" | "", 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, } console.log('StandardAvlTable - API call params:', params) const result = await getStandardAvlVendorInfo(params) console.log('StandardAvlTable - 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 { setLoading(false) } }, [searchConstructionSector, searchShipType, searchAvlKind, searchHtDivision]) // reloadTrigger가 변경될 때마다 데이터 리로드 React.useEffect(() => { if (reloadTrigger && reloadTrigger > 0) { console.log('StandardAvlTable - reloadTrigger changed, reloading data') loadData({}) } }, [reloadTrigger, loadData]) // 검색 초기화 핸들러 const handleResetSearch = React.useCallback(() => { setSearchConstructionSector("") setSearchShipType("") setSearchAvlKind("") setSearchHtDivision("") // 초기화 시 빈 데이터로 설정 setData([]) setPageCount(0) }, []) // 검색 핸들러 const handleSearch = React.useCallback(() => { if (isAllSearchConditionsSelected) { // 검색 시 페이지를 1페이지로 리셋 setPagination(prev => ({ ...prev, pageIndex: 0 })) loadData({ page: 1, perPage: pagination.pageSize }) } }, [loadData, isAllSearchConditionsSelected, pagination.pageSize]) // 항목 추가 핸들러 const handleAddItem = React.useCallback(async (itemData: Omit) => { try { const result = await createAvlVendorInfo(itemData) if (result) { toast.success("표준 AVL 항목이 성공적으로 추가되었습니다.") // 데이터 새로고침 loadData({}) } else { toast.error("항목 추가에 실패했습니다.") } } catch (error) { console.error("항목 추가 실패:", error) toast.error("항목 추가 중 오류가 발생했습니다.") } }, [loadData]) // 항목 수정 핸들러 const handleUpdateItem = React.useCallback(async (id: number, itemData: Omit) => { try { const result = await updateAvlVendorInfo(id, itemData) if (result) { toast.success("표준 AVL 항목이 성공적으로 수정되었습니다.") // 데이터 새로고침 loadData({}) // 수정 모드 해제 setEditingItem(undefined) } else { toast.error("항목 수정에 실패했습니다.") } } catch (error) { console.error("항목 수정 실패:", error) toast.error("항목 수정 중 오류가 발생했습니다.") } }, [loadData]) // 항목 수정 핸들러 (버튼 클릭) 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]) // 최종 확정 핸들러 (표준 AVL) const handleFinalizeStandardAvl = React.useCallback(async () => { // 1. 필수 조건 검증 if (!isAllSearchConditionsSelected) { toast.error("검색 조건을 모두 선택해주세요.") return } if (data.length === 0) { toast.error("확정할 표준 AVL 벤더 정보가 없습니다.") return } // 2. 사용자 확인 const confirmed = window.confirm( `현재 표준 AVL을 최종 확정하시겠습니까?\n\n` + `- 공사부문: ${searchConstructionSector}\n` + `- 선종: ${searchShipType}\n` + `- AVL종류: ${searchAvlKind}\n` + `- H/T 구분: ${searchHtDivision}\n` + `- 벤더 정보: ${data.length}개\n\n` + `확정 후에는 수정이 어려울 수 있습니다.` ) if (!confirmed) return try { // 3. 현재 데이터의 모든 ID 수집 const avlVendorInfoIds = data.map(item => item.id) // 4. 최종 확정 실행 const standardAvlInfo = { constructionSector: searchConstructionSector, shipType: searchShipType, avlKind: searchAvlKind, htDivision: searchHtDivision } const result = await finalizeStandardAvl( standardAvlInfo, 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 최종 확정 중 오류가 발생했습니다.") } }, [searchConstructionSector, searchShipType, searchAvlKind, searchHtDivision, isAllSearchConditionsSelected, data, table, loadData, sessionData?.user?.name]) // 초기 데이터 로드 (검색 조건이 모두 입력되었을 때만) React.useEffect(() => { if (isAllSearchConditionsSelected) { // 검색 조건이 모두 입력되면 페이지를 1페이지로 리셋하고 데이터 로드 setPagination(prev => ({ ...prev, pageIndex: 0 })) loadData({ page: 1, perPage: pagination.pageSize }) } else { // 검색 조건이 모두 입력되지 않은 경우 빈 데이터로 설정 setData([]) setPageCount(0) } }, [isAllSearchConditionsSelected, pagination.pageSize, searchConstructionSector, searchShipType, searchAvlKind, searchHtDivision]) // 외부에서 선택된 ID들을 가져올 수 있도록 ref에 메소드 노출 useImperativeHandle(ref, () => ({ getSelectedIds: () => { const selectedRows = table.getFilteredSelectedRowModel().rows return selectedRows.map(row => row.original.id) } })) // 선택된 행 개수 (안정적인 계산을 위해 useMemo 사용) const selectedRows = table.getFilteredSelectedRowModel().rows const selectedRowCount = useMemo(() => { const count = selectedRows.length console.log('StandardAvlTable - selectedRowCount calculated:', count) return count }, [selectedRows]) // 페이지네이션 상태 디버깅 React.useEffect(() => { const paginationState = table.getState().pagination console.log('StandardAvlTable - Current pagination state:', { pageIndex: paginationState.pageIndex, pageSize: paginationState.pageSize, canPreviousPage: table.getCanPreviousPage(), canNextPage: table.getCanNextPage(), pageCount: table.getPageCount(), currentDataLength: data.length }) }, [table, data]) // 선택 상태 변경 시 콜백 호출 useLayoutEffect(() => { console.log('StandardAvlTable - onSelectionChange called with count:', selectedRowCount) onSelectionChange?.(selectedRowCount) }, [selectedRowCount, onSelectionChange]) // 선택 해제 요청이 오면 모든 선택 해제 React.useEffect(() => { if (resetCounter && resetCounter > 0) { table.toggleAllPageRowsSelected(false) } }, [resetCounter, table]) return (

선종별 표준 AVL

{/* */} {/* */}
{/* 검색 UI */}
{/* 공사부문 */}
{/* 선종 */}
{/* AVL종류 */}
{/* H/T */}
{/* 검색 버튼들 */}
{/* 신규업체 추가 다이얼로그 */} { setIsAddDialogOpen(open) if (!open) { setEditingItem(undefined) // 다이얼로그가 닫힐 때 수정 모드 해제 } }} onAddItem={handleAddItem} editingItem={editingItem} onUpdateItem={handleUpdateItem} isTemplate={true} // 표준 AVL 모드 // 검색 조건에서 선택한 값들을 초기값으로 전달 initialConstructionSector={searchConstructionSector} initialShipType={searchShipType} initialAvlKind={searchAvlKind} initialHtDivision={searchHtDivision} />
) }) StandardAvlTable.displayName = "StandardAvlTable"