summaryrefslogtreecommitdiff
path: root/lib/avl/table/project-avl-table.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'lib/avl/table/project-avl-table.tsx')
-rw-r--r--lib/avl/table/project-avl-table.tsx650
1 files changed, 650 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..8664e32b
--- /dev/null
+++ b/lib/avl/table/project-avl-table.tsx
@@ -0,0 +1,650 @@
+"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<ProjectAvlTableRef, ProjectAvlTableProps>(({
+ onSelectionChange,
+ resetCounter,
+ projectCode,
+ avlListId,
+ onProjectCodeChange,
+ reloadTrigger
+}, ref) => {
+
+ const { data: sessionData } = useSession()
+
+ const [data, setData] = React.useState<ProjectAvlItem[]>([])
+ const [pageCount, setPageCount] = React.useState(0)
+ 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<ProjectSearchStatus>('idle')
+
+ // 검색 버튼 클릭 여부 상태
+ const [isSearchClicked, setIsSearchClicked] = React.useState(false)
+
+ // 페이지네이션 상태
+ const [pagination, setPagination] = React.useState({
+ pageIndex: 0,
+ pageSize: 10,
+ })
+
+
+ // 데이터 로드 함수
+ const loadData = React.useCallback(async (searchParams: Partial<GetProjectAvlSchema> = {}) => {
+ 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<HTMLInputElement>) => {
+ 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<AvlVendorInfoInput, 'avlListId'>) => {
+ 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<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,
+ 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 (
+ <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={handleFinalizeAvl}
+ disabled={!localProjectCode.trim() || !projectInfo || data.length === 0}
+ >
+ 최종 확정
+ </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="flex flex-col gap-1 min-w-[200px]">
+ <label className="text-sm font-medium">프로젝트 코드</label>
+ <div className="flex gap-2">
+ <div className="flex-1">
+ <input
+ type="text"
+ value={localProjectCode}
+ onChange={(e) => 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' && (
+ <div className="text-xs mt-1 text-muted-foreground">
+ {projectSearchStatus === 'success-projects' ? '(프로젝트)' :
+ projectSearchStatus === 'success-bidding' ? '(견적프로젝트)' :
+ projectSearchStatus === 'searching' ? '(검색 중...)' :
+ projectSearchStatus === 'error' ? '(찾을 수 없음)' :
+ undefined}
+ </div>
+ )}
+ </div>
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={handleProjectSearch}
+ disabled={!localProjectCode.trim() || projectSearchStatus === 'searching'}
+ className="px-3 h-9"
+ >
+ {projectSearchStatus === 'searching' ? '검색 중...' : '검색'}
+ </Button>
+ </div>
+ </div>
+
+ {/* 프로젝트명 */}
+ <ProjectDisplayField
+ label="프로젝트명"
+ value={projectInfo?.projectName || ''}
+ status={projectSearchStatus}
+ minWidth="250px"
+ />
+
+ {/* 원본파일 */}
+ <ProjectFileField
+ label="원본파일"
+ originalFile={originalFile}
+ onFileUpload={handleFileUpload}
+ />
+
+ {/* 공사부문 */}
+ <ProjectDisplayField
+ label="공사부문"
+ value={projectInfo?.constructionSector || ''}
+ status={projectSearchStatus}
+ />
+
+ {/* 선종 */}
+ <ProjectDisplayField
+ label="선종"
+ value={projectInfo?.shipType || ''}
+ status={projectSearchStatus}
+ />
+
+ {/* H/T 구분 */}
+ <ProjectDisplayField
+ label="H/T 구분"
+ value={projectInfo?.htDivision || ''}
+ status={projectSearchStatus}
+ minWidth="140px"
+ formatter={(value) =>
+ value === 'H' ? 'Hull (H)' :
+ value === 'T' ? 'Topside (T)' : '-'
+ }
+ />
+ </div>
+ </div>
+
+ <div className="flex-1">
+ <DataTable table={table} />
+ </div>
+
+ {/* 행 추가/수정 다이얼로그 */}
+ <AvlVendorAddAndModifyDialog
+ open={isAddDialogOpen}
+ onOpenChange={(open) => {
+ setIsAddDialogOpen(open)
+ if (!open) {
+ setEditingItem(undefined) // 다이얼로그가 닫힐 때 수정 모드 해제
+ }
+ }}
+ onAddItem={handleAddItem}
+ editingItem={editingItem}
+ onUpdateItem={handleUpdateItem}
+ isTemplate={false} // 프로젝트 AVL 모드
+ initialProjectCode={localProjectCode}
+ />
+ </div>
+ )
+})
+
+ProjectAvlTable.displayName = "ProjectAvlTable"