diff options
| author | joonhoekim <26rote@gmail.com> | 2025-09-15 01:23:00 +0000 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-09-15 01:23:00 +0000 |
| commit | e7818a457371849e29519497ebf046f385f05ab6 (patch) | |
| tree | 9bf08ba1b31a512c481dc521c9dd7c90091a75b8 /lib/avl/table/project-avl-table.tsx | |
| parent | 3f293c90beb58ce206a66ff444d7acfc41b56429 (diff) | |
(김준회) AVL 기능 구현 1차 및 벤더풀 E/B 구분 개선
Diffstat (limited to 'lib/avl/table/project-avl-table.tsx')
| -rw-r--r-- | lib/avl/table/project-avl-table.tsx | 724 |
1 files changed, 724 insertions, 0 deletions
diff --git a/lib/avl/table/project-avl-table.tsx b/lib/avl/table/project-avl-table.tsx new file mode 100644 index 00000000..c6dd8064 --- /dev/null +++ b/lib/avl/table/project-avl-table.tsx @@ -0,0 +1,724 @@ +"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<ProjectAvlItem>[] => [ + { + id: "select", + header: ({ table }) => ( + <Checkbox + checked={ + table.getIsAllPageRowsSelected() || + (table.getIsSomePageRowsSelected() && "indeterminate") + } + onCheckedChange={(value) => 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 ( + <Checkbox + checked={row.getIsSelected()} + onCheckedChange={handleRowSelection} + aria-label="Select row" + /> + ) + }, + enableSorting: false, + enableHiding: false, + size: 50, + }, + { + accessorKey: "no", + header: "No.", + size: 60, + cell: ({ row }) => { + return ( + <span> + {row.original.no} + </span> + ) + }, + }, + { + accessorKey: "disciplineName", + header: "설계공종", + size: 120, + cell: ({ row }) => { + return ( + <span> + {row.original.disciplineName} + </span> + ) + }, + }, + { + accessorKey: "materialNameCustomerSide", + header: "고객사 AVL 자재명", + size: 150, + cell: ({ row }) => { + return ( + <span> + {row.original.materialNameCustomerSide} + </span> + ) + }, + }, + { + accessorKey: "materialGroupCode", + header: "자재그룹 코드", + size: 120, + cell: ({ row }) => { + return ( + <span> + {row.original.materialGroupCode} + </span> + ) + }, + }, + { + accessorKey: "materialGroupName", + header: "자재그룹 명", + size: 130, + cell: ({ row }) => { + return ( + <span> + {row.original.materialGroupName} + </span> + ) + }, + }, + { + accessorKey: "avlVendorName", + header: "AVL 등재업체명", + size: 140, + cell: ({ row }) => { + return ( + <span> + {row.original.avlVendorName} + </span> + ) + }, + }, + { + accessorKey: "vendorCode", + header: "협력업체 코드", + size: 120, + cell: ({ row }) => { + return ( + <span> + {row.original.vendorCode} + </span> + ) + }, + }, + { + accessorKey: "vendorName", + header: "협력업체 명", + size: 130, + cell: ({ row }) => { + return ( + <span> + {row.original.vendorName} + </span> + ) + }, + }, + { + accessorKey: "ownerSuggestion", + header: "선주제안", + size: 100, + cell: ({ row }) => { + return ( + <span> + {row.original.ownerSuggestion ? "예" : "아니오"} + </span> + ) + }, + }, + { + accessorKey: "shiSuggestion", + header: "SHI 제안", + size: 100, + cell: ({ row }) => { + return ( + <span> + {row.original.shiSuggestion ? "예" : "아니오"} + </span> + ) + }, + }, +] + +export function ProjectAvlTable({ + onSelectionChange, + resetCounter, + projectCode, + avlListId, + onProjectCodeChange +}: ProjectAvlTableProps) { + const [data, setData] = React.useState<ProjectAvlItem[]>([]) + const [loading, setLoading] = React.useState(false) + const [pageCount, setPageCount] = React.useState(0) + const [avlListInfo, setAvlListInfo] = React.useState<AvlListItem | null>(null) + const [originalFile, setOriginalFile] = React.useState<string>("") + const [localProjectCode, setLocalProjectCode] = React.useState<string>(projectCode || "") + + // 행 추가/수정 다이얼로그 상태 + const [isAddDialogOpen, setIsAddDialogOpen] = React.useState(false) + const [editingItem, setEditingItem] = React.useState<AvlDetailItem | undefined>(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<GetProjectAvlSchema>) => { + 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<HTMLInputElement>) => { + 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<AvlVendorInfoInput, 'avlListId'>) => { + 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<AvlVendorInfoInput, 'avlListId'>) => { + 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 ( + <div className="h-full flex flex-col"> + <div className="mb-2"> + <div className="flex items-center justify-between mb-2"> + <h4 className="font-medium">프로젝트 AVL</h4> + <div className="flex gap-1"> + <Button variant="outline" size="sm" onClick={handleAddRow}> + 행 추가 + </Button> + <Button + variant="outline" + size="sm" + onClick={handleEditItem} + disabled={table.getFilteredSelectedRowModel().rows.length !== 1} + > + 항목 수정 + </Button> + <Button variant="outline" size="sm" onClick={() => toast.info("개발 중입니다.")}> + 파일 업로드 + </Button> + <Button variant="outline" size="sm" onClick={() => toast.info("개발 중입니다.")}> + 자동 매핑 + </Button> + <Button variant="outline" size="sm" onClick={() => toast.info("개발 중입니다.")}> + 강제 매핑 + </Button> + <Button variant="outline" size="sm" onClick={handleDeleteItems}> + 항목 삭제 + </Button> + + {/* 최종 확정 버튼 */} + <Button variant="outline" size="sm" onClick={() => toast.info("개발 중입니다.")}> + 최종 확정 + </Button> + </div> + </div> + </div> + + {/* 조회대상 관리영역 */} + <div className="mb-4 p-4 border rounded-lg bg-muted/50"> + <div className="flex gap-4 overflow-x-auto pb-2"> + {/* 프로젝트 코드 */} + <div className="space-y-2 min-w-[250px] flex-shrink-0"> + <label className={`text-sm font-medium ${ + projectSearchStatus === 'error' ? 'text-red-600' : + projectSearchStatus === 'success-projects' || projectSearchStatus === 'success-bidding' ? 'text-green-600' : + projectSearchStatus === 'searching' ? 'text-blue-600' : + 'text-muted-foreground' + }`}> + 프로젝트 코드 + {projectSearchStatus === 'success-projects' && <span className="ml-1 text-xs">(프로젝트)</span>} + {projectSearchStatus === 'success-bidding' && <span className="ml-1 text-xs">(견적프로젝트)</span>} + {projectSearchStatus === 'searching' && <span className="ml-1 text-xs">(검색 중...)</span>} + {projectSearchStatus === 'error' && <span className="ml-1 text-xs">(찾을 수 없음)</span>} + </label> + <Input + value={localProjectCode} + onChange={(e) => 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' : + '' + }`} + /> + </div> + + {/* 프로젝트명 */} + <div className="space-y-2 min-w-[250px] flex-shrink-0"> + <label className={`text-sm font-medium ${ + projectSearchStatus === 'error' ? 'text-red-600' : + projectSearchStatus === 'success-projects' || projectSearchStatus === 'success-bidding' ? 'text-green-600' : + projectSearchStatus === 'searching' ? 'text-blue-600' : + 'text-muted-foreground' + }`}> + 프로젝트명 + {projectSearchStatus === 'searching' && <span className="ml-1 text-xs">(조회 중...)</span>} + </label> + <div className={`text-sm font-medium min-h-[32px] flex items-center border rounded-md px-3 bg-background ${ + projectSearchStatus === 'error' ? 'border-red-300' : + projectSearchStatus === 'success-projects' || projectSearchStatus === 'success-bidding' ? 'border-green-300' : + projectSearchStatus === 'searching' ? 'border-blue-300' : + 'border-input' + }`}> + {projectSearchStatus === 'searching' ? '조회 중...' : (projectInfo?.projectName || '-')} + </div> + </div> + + {/* 원본파일 */} + <div className="space-y-2 min-w-[200px] flex-shrink-0"> + <label className="text-sm font-medium text-muted-foreground">원본파일</label> + <div className="flex items-center gap-2 min-h-[32px]"> + {originalFile ? ( + <span className="text-sm text-blue-600">{originalFile}</span> + ) : ( + <div className="relative"> + <input + type="file" + onChange={handleFileUpload} + className="absolute inset-0 w-full h-full opacity-0 cursor-pointer" + accept=".xlsx,.xls,.csv" + /> + <Button variant="outline" size="sm" className="text-xs"> + 파일 선택 + </Button> + </div> + )} + </div> + </div> + + {/* 공사부문 */} + <div className="space-y-2 min-w-[120px] flex-shrink-0"> + <label className={`text-sm font-medium ${ + projectSearchStatus === 'error' ? 'text-red-600' : + projectSearchStatus === 'success-projects' || projectSearchStatus === 'success-bidding' ? 'text-green-600' : + projectSearchStatus === 'searching' ? 'text-blue-600' : + 'text-muted-foreground' + }`}> + 공사부문 + {projectSearchStatus === 'searching' && <span className="ml-1 text-xs">(조회 중...)</span>} + </label> + <div className={`text-sm font-medium min-h-[32px] flex items-center border rounded-md px-3 bg-background ${ + projectSearchStatus === 'error' ? 'border-red-300' : + projectSearchStatus === 'success-projects' || projectSearchStatus === 'success-bidding' ? 'border-green-300' : + projectSearchStatus === 'searching' ? 'border-blue-300' : + 'border-input' + }`}> + {projectSearchStatus === 'searching' ? '조회 중...' : (projectInfo?.constructionSector || '-')} + </div> + </div> + + {/* 선종 */} + <div className="space-y-2 min-w-[120px] flex-shrink-0"> + <label className={`text-sm font-medium ${ + projectSearchStatus === 'error' ? 'text-red-600' : + projectSearchStatus === 'success-projects' || projectSearchStatus === 'success-bidding' ? 'text-green-600' : + projectSearchStatus === 'searching' ? 'text-blue-600' : + 'text-muted-foreground' + }`}> + 선종 + {projectSearchStatus === 'searching' && <span className="ml-1 text-xs">(조회 중...)</span>} + </label> + <div className={`text-sm font-medium min-h-[32px] flex items-center border rounded-md px-3 bg-background ${ + projectSearchStatus === 'error' ? 'border-red-300' : + projectSearchStatus === 'success-projects' || projectSearchStatus === 'success-bidding' ? 'border-green-300' : + projectSearchStatus === 'searching' ? 'border-blue-300' : + 'border-input' + }`}> + {projectSearchStatus === 'searching' ? '조회 중...' : (projectInfo?.shipType || '-')} + </div> + </div> + + {/* H/T 구분 */} + <div className="space-y-2 min-w-[140px] flex-shrink-0"> + <label className={`text-sm font-medium ${ + projectSearchStatus === 'error' ? 'text-red-600' : + projectSearchStatus === 'success-projects' || projectSearchStatus === 'success-bidding' ? 'text-green-600' : + projectSearchStatus === 'searching' ? 'text-blue-600' : + 'text-muted-foreground' + }`}> + H/T 구분 + {projectSearchStatus === 'searching' && <span className="ml-1 text-xs">(조회 중...)</span>} + </label> + <div className={`text-sm font-medium min-h-[32px] flex items-center border rounded-md px-3 bg-background ${ + projectSearchStatus === 'error' ? 'border-red-300' : + projectSearchStatus === 'success-projects' || projectSearchStatus === 'success-bidding' ? 'border-green-300' : + projectSearchStatus === 'searching' ? 'border-blue-300' : + 'border-input' + }`}> + {projectSearchStatus === 'searching' ? '조회 중...' : + (projectInfo?.htDivision === 'H' ? 'Hull (H)' : + projectInfo?.htDivision === 'T' ? 'Topside (T)' : '-')} + </div> + </div> + </div> + </div> + + <div className="flex-1"> + <DataTable table={table} /> + </div> + + {/* 행 추가/수정 다이얼로그 */} + <ProjectAvlAddDialog + open={isAddDialogOpen} + onOpenChange={(open) => { + setIsAddDialogOpen(open) + if (!open) { + setEditingItem(undefined) // 다이얼로그가 닫힐 때 수정 모드 해제 + } + }} + onAddItem={handleAddItem} + editingItem={editingItem} + onUpdateItem={handleUpdateItem} + /> + </div> + ) +} |
