From 2b490956c9752c1b756780a3461bc1c37b6fe0a7 Mon Sep 17 00:00:00 2001 From: joonhoekim <26rote@gmail.com> Date: Mon, 15 Sep 2025 18:58:07 +0900 Subject: (김준회) AVL 관리 및 상세 - 기능 구현 1차 + docker compose 내 오류 수정 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/avl/table/project-avl-table.tsx | 720 ++++++++++++++++-------------------- 1 file changed, 323 insertions(+), 397 deletions(-) (limited to 'lib/avl/table/project-avl-table.tsx') diff --git a/lib/avl/table/project-avl-table.tsx b/lib/avl/table/project-avl-table.tsx index c6dd8064..8664e32b 100644 --- a/lib/avl/table/project-avl-table.tsx +++ b/lib/avl/table/project-avl-table.tsx @@ -1,204 +1,57 @@ "use client" import * as React from "react" -import { useReactTable, getCoreRowModel, getPaginationRowModel, getSortedRowModel, getFilteredRowModel, type ColumnDef } from "@tanstack/react-table" +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 { 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 { 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, AvlListItem, AvlVendorInfoInput } from "../types" +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 } -// 프로젝트 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({ +export const ProjectAvlTable = forwardRef(({ onSelectionChange, resetCounter, projectCode, avlListId, - onProjectCodeChange -}: ProjectAvlTableProps) { + onProjectCodeChange, + reloadTrigger +}, ref) => { + + const { data: sessionData } = useSession() + 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 || "") @@ -215,20 +68,28 @@ export function ProjectAvlTable({ } | null>(null) // 프로젝트 검색 상태 - const [projectSearchStatus, setProjectSearchStatus] = React.useState<'idle' | 'searching' | 'success-projects' | 'success-bidding' | 'error'>('idle') + 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) => { + const loadData = React.useCallback(async (searchParams: Partial = {}) => { try { - setLoading(true) - const params: any = { + 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 ?? "", + equipBulkDivision: (searchParams.equipBulkDivision as "EQUIP" | "BULK") ?? "EQUIP", disciplineCode: searchParams.disciplineCode ?? "", disciplineName: searchParams.disciplineName ?? "", materialNameCustomerSide: searchParams.materialNameCustomerSide ?? "", @@ -244,7 +105,13 @@ export function ProjectAvlTable({ 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) { @@ -252,32 +119,26 @@ export function ProjectAvlTable({ setData([]) setPageCount(0) } finally { - setLoading(false) + // 로딩 상태 처리 완료 } }, [localProjectCode]) - // AVL 리스트 정보 로드 + + + // reloadTrigger가 변경될 때마다 데이터 리로드 React.useEffect(() => { - const loadAvlListInfo = async () => { - if (avlListId) { - try { - const info = await getAvlListById(avlListId) - setAvlListInfo(info) - } catch (error) { - console.error("AVL 리스트 정보 로드 실패:", error) - } - } + if (reloadTrigger && reloadTrigger > 0) { + console.log('ProjectAvlTable - reloadTrigger changed, reloading data') + loadData({}) } + }, [reloadTrigger, loadData]) - loadAvlListInfo() - }, [avlListId]) - - // 초기 데이터 로드 + // 초기 데이터 로드 (검색 버튼이 눌렸을 때만) React.useEffect(() => { - if (localProjectCode) { + if (localProjectCode && isSearchClicked) { loadData({}) } - }, [loadData, localProjectCode]) + }, [loadData, localProjectCode, isSearchClicked]) // 파일 업로드 핸들러 const handleFileUpload = React.useCallback((event: React.ChangeEvent) => { @@ -289,42 +150,41 @@ export function ProjectAvlTable({ } }, []) - // 프로젝트 코드 변경 핸들러 - const handleProjectCodeChange = React.useCallback(async (value: string) => { - setLocalProjectCode(value) - onProjectCodeChange?.(value) + // 프로젝트 검색 함수 (공통 로직) + const searchProject = React.useCallback(async (projectCode: string) => { + if (!projectCode.trim()) { + setProjectInfo(null) + setProjectSearchStatus('idle') + setData([]) + setPageCount(0) + return + } - // 프로젝트 코드가 입력된 경우 프로젝트 정보 조회 - if (value.trim()) { - setProjectSearchStatus('searching') // 검색 시작 상태로 변경 + setProjectSearchStatus('searching') // 검색 시작 상태로 변경 - try { - // 1. projects 테이블에서 먼저 검색 - let projectData = null - let searchSource = 'projects' + 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 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에서도 에러가 발생한 경우 + projectData = await getProjectInfoFromBiddingProjects(projectCode.trim()) + if (projectData) { + searchSource = 'bidding-projects' + setProjectSearchStatus('success-bidding') // bidding에서 찾았을 때 성공 상태로 변경 + } else { + // 둘 다 실패한 경우에만 에러 상태로 변경 setProjectInfo(null) setProjectSearchStatus('error') setData([]) @@ -332,39 +192,67 @@ export function ProjectAvlTable({ 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 || "" - }) + if (projectData) { + setProjectInfo({ + projectName: projectData.projectName || "", + constructionSector: "조선", // 기본값으로 조선 설정 (필요시 로직 변경) + shipType: projectData.shipType || projectData.projectMsrm || "", + htDivision: projectData.projectHtDivision || "" + }) - const sourceMessage = searchSource === 'projects' ? '프로젝트' : '견적프로젝트' - toast.success(`${sourceMessage}에서 프로젝트 정보를 성공적으로 불러왔습니다.`) + const sourceMessage = searchSource === 'projects' ? '프로젝트' : '견적프로젝트' + toast.success(`${sourceMessage}에서 프로젝트 정보를 성공적으로 불러왔습니다.`) - // 프로젝트 검증 성공 시 해당 프로젝트의 AVL 데이터 로드 - loadData({}) - } - } catch (error) { - console.error("프로젝트 정보 조회 실패:", error) - setProjectInfo(null) - setProjectSearchStatus('error') - setData([]) - setPageCount(0) - toast.error("프로젝트 정보를 불러오는데 실패했습니다.") + // 검색 성공 시 AVL 데이터 로드 트리거 + setIsSearchClicked(true) } - } else { - // 프로젝트 코드가 비어있는 경우 프로젝트 정보 초기화 및 데이터 클리어 + } 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()) { @@ -382,16 +270,11 @@ export function ProjectAvlTable({ // 다이얼로그에서 항목 추가 핸들러 const handleAddItem = React.useCallback(async (itemData: Omit) => { try { - // AVL 리스트 ID 확인 - if (!avlListId) { - toast.error("AVL 리스트가 선택되지 않았습니다.") - return - } - - // DB에 실제 저장할 데이터 준비 + // DB에 실제 저장할 데이터 준비 (avlListId는 나중에 최종 확정 시 설정) const saveData: AvlVendorInfoInput = { ...itemData, - avlListId: avlListId // 현재 AVL 리스트 ID 설정 + projectCode: localProjectCode, // 현재 프로젝트 코드 저장 + avlListId: avlListId || undefined // 있으면 설정, 없으면 undefined (null로 저장됨) } // DB에 저장 @@ -409,7 +292,7 @@ export function ProjectAvlTable({ console.error("항목 추가 실패:", error) toast.error("항목 추가 중 오류가 발생했습니다.") } - }, [avlListId, loadData]) + }, [avlListId, loadData, localProjectCode]) // 다이얼로그에서 항목 수정 핸들러 const handleUpdateItem = React.useCallback(async (id: number, itemData: Omit) => { @@ -447,21 +330,43 @@ export function ProjectAvlTable({ getFilteredRowModel: getFilteredRowModel(), manualPagination: true, pageCount, - initialState: { - pagination: { - pageSize: 10, - }, + state: { + pagination, }, onPaginationChange: (updater) => { - const newState = typeof updater === 'function' ? updater(table.getState().pagination) : updater - loadData({ - page: newState.pageIndex + 1, - perPage: newState.pageSize, + // 페이지네이션 상태 업데이트 + 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 @@ -510,11 +415,79 @@ export function ProjectAvlTable({ } }, [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]) + // 선택 상태 변경 시 콜백 호출 - React.useEffect(() => { - const selectedRows = table.getFilteredSelectedRowModel().rows - onSelectionChange?.(selectedRows.length) - }, [table.getFilteredSelectedRowModel().rows.length, onSelectionChange]) + useLayoutEffect(() => { + console.log('ProjectAvlTable - onSelectionChange called with count:', selectedRowCount) + onSelectionChange?.(selectedRowCount) + }, [selectedRowCount, onSelectionChange]) // 선택 해제 요청이 오면 모든 선택 해제 React.useEffect(() => { @@ -540,7 +513,7 @@ export function ProjectAvlTable({ > 항목 수정 - + */} {/* 최종 확정 버튼 */} - @@ -565,140 +543,84 @@ export function ProjectAvlTable({
{/* 프로젝트 코드 */} -
- - 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' : - '' - }`} - /> +
+ +
+
+ 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} +
+ )} +
+ +
{/* 프로젝트명 */} -
- -
- {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)' : '-')} -
-
+ + value === 'H' ? 'Hull (H)' : + value === 'T' ? 'Topside (T)' : '-' + } + />
@@ -707,7 +629,7 @@ export function ProjectAvlTable({
{/* 행 추가/수정 다이얼로그 */} - { setIsAddDialogOpen(open) @@ -718,7 +640,11 @@ export function ProjectAvlTable({ onAddItem={handleAddItem} editingItem={editingItem} onUpdateItem={handleUpdateItem} + isTemplate={false} // 프로젝트 AVL 모드 + initialProjectCode={localProjectCode} /> ) -} +}) + +ProjectAvlTable.displayName = "ProjectAvlTable" -- cgit v1.2.3