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<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({
+export const ProjectAvlTable = forwardRef<ProjectAvlTableRef, ProjectAvlTableProps>(({
   onSelectionChange,
   resetCounter,
   projectCode,
   avlListId,
-  onProjectCodeChange
-}: ProjectAvlTableProps) {
+  onProjectCodeChange,
+  reloadTrigger
+}, ref) => {
+
+  const { data: sessionData } = useSession()
+
   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 || "")
 
@@ -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<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>) => {
+  const loadData = React.useCallback(async (searchParams: Partial<GetProjectAvlSchema> = {}) => {
     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<HTMLInputElement>) => {
@@ -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<AvlVendorInfoInput, 'avlListId'>) => {
     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<AvlVendorInfoInput, 'avlListId'>) => {
@@ -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({
             >
               항목 수정
             </Button>
-            <Button variant="outline" size="sm" onClick={() => toast.info("개발 중입니다.")}>
+            {/* <Button variant="outline" size="sm" onClick={() => toast.info("개발 중입니다.")}>
               파일 업로드
             </Button>
             <Button variant="outline" size="sm" onClick={() => toast.info("개발 중입니다.")}>
@@ -548,13 +521,18 @@ export function ProjectAvlTable({
             </Button>
             <Button variant="outline" size="sm" onClick={() => toast.info("개발 중입니다.")}>
               강제 매핑
-            </Button>
+            </Button> */}
             <Button variant="outline" size="sm" onClick={handleDeleteItems}>
               항목 삭제
             </Button>
 
             {/* 최종 확정 버튼 */}
-            <Button variant="outline" size="sm" onClick={() => toast.info("개발 중입니다.")}>
+            <Button 
+              variant="outline" 
+              size="sm" 
+              onClick={handleFinalizeAvl}
+              disabled={!localProjectCode.trim() || !projectInfo || data.length === 0}
+            >
               최종 확정
             </Button>
           </div>
@@ -565,140 +543,84 @@ export function ProjectAvlTable({
       <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 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>
 
             {/* 프로젝트명 */}
-            <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>
+            <ProjectDisplayField
+              label="프로젝트명"
+              value={projectInfo?.projectName || ''}
+              status={projectSearchStatus}
+              minWidth="250px"
+            />
 
             {/* 원본파일 */}
-            <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>
+            <ProjectFileField
+              label="원본파일"
+              originalFile={originalFile}
+              onFileUpload={handleFileUpload}
+            />
 
             {/* 공사부문 */}
-            <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>
+            <ProjectDisplayField
+              label="공사부문"
+              value={projectInfo?.constructionSector || ''}
+              status={projectSearchStatus}
+            />
 
             {/* 선종 */}
-            <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>
+            <ProjectDisplayField
+              label="선종"
+              value={projectInfo?.shipType || ''}
+              status={projectSearchStatus}
+            />
 
             {/* 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>
+            <ProjectDisplayField
+              label="H/T 구분"
+              value={projectInfo?.htDivision || ''}
+              status={projectSearchStatus}
+              minWidth="140px"
+              formatter={(value) =>
+                value === 'H' ? 'Hull (H)' :
+                value === 'T' ? 'Topside (T)' : '-'
+              }
+            />
         </div>
       </div>
 
@@ -707,7 +629,7 @@ export function ProjectAvlTable({
       </div>
 
       {/* 행 추가/수정 다이얼로그 */}
-      <ProjectAvlAddDialog
+      <AvlVendorAddAndModifyDialog
         open={isAddDialogOpen}
         onOpenChange={(open) => {
           setIsAddDialogOpen(open)
@@ -718,7 +640,11 @@ export function ProjectAvlTable({
         onAddItem={handleAddItem}
         editingItem={editingItem}
         onUpdateItem={handleUpdateItem}
+        isTemplate={false} // 프로젝트 AVL 모드
+        initialProjectCode={localProjectCode}
       />
     </div>
   )
-}
+})
+
+ProjectAvlTable.displayName = "ProjectAvlTable"
-- 
cgit v1.2.3

