From 40250c61031263606dd073ce7056a3e8e27f18d0 Mon Sep 17 00:00:00 2001
From: joonhoekim <26rote@gmail.com>
Date: Tue, 14 Oct 2025 14:25:28 +0900
Subject: (김준회) AVL 구매요구사항 수정 - AVL 상세 엑셀 익스포트 추가 - 레코드
 이동 멀티선택 추가 - 최종확정처리 오류 수정 - 프로젝트 AVL에 H/T 구분 추가
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 lib/avl/table/project-avl-table.tsx | 173 +++++++++++++++++++++---------------
 1 file changed, 100 insertions(+), 73 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 ad72b221..d15dbb06 100644
--- a/lib/avl/table/project-avl-table.tsx
+++ b/lib/avl/table/project-avl-table.tsx
@@ -12,16 +12,22 @@ import {
   DialogHeader,
   DialogTitle,
 } from "@/components/ui/dialog"
+import {
+  Select,
+  SelectContent,
+  SelectItem,
+  SelectTrigger,
+  SelectValue,
+} from "@/components/ui/select"
 import { AvlVendorAddAndModifyDialog } from "./avl-vendor-add-and-modify-dialog"
-import { getProjectAvlVendorInfo, createAvlVendorInfo, updateAvlVendorInfo, deleteAvlVendorInfo, finalizeProjectAvl } from "../service"
+import { getProjectAvlVendorInfo, createAvlVendorInfo, updateAvlVendorInfo, deleteAvlVendorInfo, finalizeProjectAvl, getProjectAvlVendorInfoCount } from "../service"
 import { useReactTable, getCoreRowModel, getPaginationRowModel, getSortedRowModel, getFilteredRowModel } from "@tanstack/react-table"
 import { GetProjectAvlSchema } from "../validations"
 import { AvlDetailItem, AvlVendorInfoInput } from "../types"
 import { toast } from "sonner"
 import { getProjectAvlColumns } from "./project-avl-table-columns"
 import {
-  ProjectDisplayField,
-  ProjectFileField
+  ProjectDisplayField
 } from "../components/project-field-components"
 import { ProjectSearchStatus } from "../components/project-field-utils"
 import { useSession } from "next-auth/react"
@@ -31,6 +37,12 @@ import { UnifiedProjectSelector, UnifiedProject, getProjectInfoByCode } from "@/
 // 프로젝트 AVL 테이블에서는 AvlDetailItem을 사용
 export type ProjectAvlItem = AvlDetailItem
 
+// H/T 구분 옵션 (공통 없음)
+const htDivisionOptions = [
+  { value: "H", label: "Hull (H)" },
+  { value: "T", label: "Top (T)" },
+]
+
 // ref를 통해 외부에서 접근할 수 있는 메소드들
 export interface ProjectAvlTableRef {
   getSelectedIds: () => number[]
@@ -42,6 +54,7 @@ interface ProjectAvlTableProps {
   projectCode?: string // 프로젝트 코드 필터
   avlListId?: number // AVL 리스트 ID (관리 영역 표시용)
   onProjectCodeChange?: (projectCode: string) => void // 프로젝트 코드 변경 콜백
+  onHtDivisionChange?: (htDivision: string) => void // H/T 구분 변경 콜백
   reloadTrigger?: number
 }
 
@@ -52,6 +65,7 @@ export const ProjectAvlTable = forwardRef<ProjectAvlTableRef, ProjectAvlTablePro
   projectCode,
   avlListId,
   onProjectCodeChange,
+  onHtDivisionChange,
   reloadTrigger
 }, ref) => {
 
@@ -59,7 +73,6 @@ export const ProjectAvlTable = forwardRef<ProjectAvlTableRef, ProjectAvlTablePro
 
   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 || "")
   
   // 프로젝트 선택 상태
@@ -74,9 +87,11 @@ export const ProjectAvlTable = forwardRef<ProjectAvlTableRef, ProjectAvlTablePro
     projectName: string
     constructionSector: string
     shipType: string
-    htDivision: string
   } | null>(null)
 
+  // H/T 구분 상태 (사용자가 직접 선택)
+  const [searchHtDivision, setSearchHtDivision] = React.useState<string>("")
+
   // 프로젝트 검색 상태
   const [projectSearchStatus, setProjectSearchStatus] = React.useState<ProjectSearchStatus>('idle')
 
@@ -100,7 +115,8 @@ export const ProjectAvlTable = forwardRef<ProjectAvlTableRef, ProjectAvlTablePro
         sort: searchParams.sort ?? [{ id: "no", desc: false }],
         flags: searchParams.flags ?? [],
         projectCode: localProjectCode || "",
-        equipBulkDivision: (searchParams.equipBulkDivision as "EQUIP" | "BULK") ?? "EQUIP",
+        htDivision: searchHtDivision as "" | "H" | "T", // H/T 구분 추가
+        equipBulkDivision: searchParams.equipBulkDivision ?? ("" as "" | "EQUIP" | "BULK"),
         disciplineCode: searchParams.disciplineCode ?? "",
         disciplineName: searchParams.disciplineName ?? "",
         materialNameCustomerSide: searchParams.materialNameCustomerSide ?? "",
@@ -113,9 +129,9 @@ export const ProjectAvlTable = forwardRef<ProjectAvlTableRef, ProjectAvlTablePro
         avlVendorName: searchParams.avlVendorName ?? "",
         tier: searchParams.tier ?? "",
         filters: searchParams.filters ?? [],
-        joinOperator: searchParams.joinOperator ?? "and",
+        joinOperator: searchParams.joinOperator ?? ("and" as "and" | "or"),
         search: searchParams.search ?? "",
-      }
+      } as GetProjectAvlSchema
       console.log('ProjectAvlTable - API call params:', params)
       const result = await getProjectAvlVendorInfo(params)
       console.log('ProjectAvlTable - API result:', {
@@ -132,7 +148,7 @@ export const ProjectAvlTable = forwardRef<ProjectAvlTableRef, ProjectAvlTablePro
     } finally {
       // 로딩 상태 처리 완료
     }
-  }, [localProjectCode])
+  }, [localProjectCode, searchHtDivision])
 
 
 
@@ -144,22 +160,25 @@ export const ProjectAvlTable = forwardRef<ProjectAvlTableRef, ProjectAvlTablePro
     }
   }, [reloadTrigger, loadData])
 
-  // 초기 데이터 로드 (검색 버튼이 눌렸을 때만)
+  // 모든 조건이 선택되었는지 확인
+  const isAllConditionsSelected = React.useMemo(() => {
+    return (
+      localProjectCode.trim() !== "" &&
+      searchHtDivision.trim() !== "" &&
+      isSearchClicked
+    )
+  }, [localProjectCode, searchHtDivision, isSearchClicked])
+
+  // 초기 데이터 로드 (모든 조건이 충족되었을 때만)
   React.useEffect(() => {
-    if (localProjectCode && isSearchClicked) {
+    if (isAllConditionsSelected) {
       loadData({})
+    } else {
+      // 조건이 충족되지 않으면 빈 데이터로 설정
+      setData([])
+      setPageCount(0)
     }
-  }, [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)
-    }
-  }, [])
+  }, [loadData, isAllConditionsSelected])
 
   // 프로젝트 검색 함수 (공통 로직)
   const searchProject = React.useCallback(async (projectCode: string) => {
@@ -187,30 +206,11 @@ export const ProjectAvlTable = forwardRef<ProjectAvlTableRef, ProjectAvlTablePro
           constructionSector = projectData.sector === 'S' ? "조선" : "해양"
         }
 
-        // htDivision 동적 결정
-        let htDivision = "" // 기본값
-        if (constructionSector === "조선") {
-          // constructionSector가 '조선'인 경우는 항상 H
-          htDivision = "H"
-        } else if (projectData.source === 'projects') {
-          // projects에서는 TYPE_MDG 컬럼이 Top이면 T, Hull이면 H
-          htDivision = projectData.typeMdg === 'Top' ? "T" : "H"
-        } else if (projectData.source === 'biddingProjects') {
-          if (projectData.sector === 'S') {
-            // biddingProjects에서 sector가 S이면 HtDivision은 항상 H
-            htDivision = "H"
-          } else if (projectData.sector === 'M') {
-            // biddingProjects에서 sector가 M인 경우: pjtType이 TOP이면 'T', HULL이면 'H'
-            htDivision = projectData.pjtType === 'TOP' ? "T" : "H"
-          }
-        }
-
-        // 프로젝트 정보 설정
+        // 프로젝트 정보 설정 (htDivision은 사용자가 직접 선택)
         setProjectInfo({
           projectName: projectData.projectName || "",
           constructionSector: constructionSector,
-          shipType: projectData.shipType || projectData.projectMsrm || "",
-          htDivision: htDivision
+          shipType: projectData.shipType || projectData.projectMsrm || ""
         })
 
         // 검색 상태 설정
@@ -268,8 +268,12 @@ export const ProjectAvlTable = forwardRef<ProjectAvlTableRef, ProjectAvlTablePro
       toast.error("프로젝트 정보를 불러올 수 없습니다.")
       return
     }
+    if (!searchHtDivision.trim()) {
+      toast.error("H/T 구분을 선택해주세요.")
+      return
+    }
     setIsAddDialogOpen(true)
-  }, [localProjectCode, projectInfo])
+  }, [localProjectCode, projectInfo, searchHtDivision])
 
 
   // 다이얼로그에서 항목 추가 핸들러
@@ -279,6 +283,7 @@ export const ProjectAvlTable = forwardRef<ProjectAvlTableRef, ProjectAvlTablePro
       const saveData: AvlVendorInfoInput = {
         ...itemData,
         projectCode: localProjectCode, // 현재 프로젝트 코드 저장
+        htDivision: searchHtDivision, // H/T 구분 저장
         avlListId: avlListId || undefined // 있으면 설정, 없으면 undefined (null로 저장됨)
       }
 
@@ -297,7 +302,7 @@ export const ProjectAvlTable = forwardRef<ProjectAvlTableRef, ProjectAvlTablePro
       console.error("항목 추가 실패:", error)
       toast.error("항목 추가 중 오류가 발생했습니다.")
     }
-  }, [avlListId, loadData, localProjectCode])
+  }, [avlListId, loadData, localProjectCode, searchHtDivision])
 
   // 다이얼로그에서 항목 수정 핸들러
   const handleUpdateItem = React.useCallback(async (id: number, itemData: Omit<AvlVendorInfoInput, 'avlListId'>) => {
@@ -353,7 +358,7 @@ export const ProjectAvlTable = forwardRef<ProjectAvlTableRef, ProjectAvlTablePro
 
       setPagination(newPaginationState)
 
-      if (localProjectCode && isSearchClicked) {
+      if (isAllConditionsSelected) {
         const apiParams = {
           page: newPaginationState.pageIndex + 1,
           perPage: newPaginationState.pageSize,
@@ -423,6 +428,7 @@ export const ProjectAvlTable = forwardRef<ProjectAvlTableRef, ProjectAvlTablePro
 
   // 최종 확정 다이얼로그 상태
   const [isConfirmDialogOpen, setIsConfirmDialogOpen] = React.useState(false)
+  const [totalVendorInfoCount, setTotalVendorInfoCount] = React.useState<number>(0)
 
   // 최종 확정 핸들러
   const handleFinalizeAvl = React.useCallback(async () => {
@@ -437,36 +443,45 @@ export const ProjectAvlTable = forwardRef<ProjectAvlTableRef, ProjectAvlTablePro
       return
     }
 
-    if (data.length === 0) {
+    if (!searchHtDivision.trim()) {
+      toast.error("H/T 구분을 선택해주세요.")
+      return
+    }
+
+    // 2. 실제 확정될 레코드 건수 조회
+    const count = await getProjectAvlVendorInfoCount(localProjectCode, searchHtDivision)
+    
+    if (count === 0) {
       toast.error("확정할 AVL 벤더 정보가 없습니다.")
       return
     }
 
-    // 2. 확인 다이얼로그 열기
+    setTotalVendorInfoCount(count)
+
+    // 3. 확인 다이얼로그 열기
     setIsConfirmDialogOpen(true)
-  }, [localProjectCode, projectInfo, data.length])
+  }, [localProjectCode, projectInfo, searchHtDivision])
 
   // 실제 최종 확정 실행 함수
   const executeFinalizeAvl = React.useCallback(async () => {
     try {
-      // 3. 현재 데이터의 모든 ID 수집 (전체 레코드 기준)
-      const avlVendorInfoIds = data.map(item => item.id)
-
-      // 4. 최종 확정 실행
+      // 최종 확정 실행 (서버에서 DB의 모든 레코드를 조회하여 확정)
       const result = await finalizeProjectAvl(
         localProjectCode,
-        projectInfo!,
-        avlVendorInfoIds,
+        {
+          ...projectInfo!,
+          htDivision: searchHtDivision // 사용자가 선택한 H/T 구분 사용
+        },
         sessionData?.user?.name || ""
       )
 
       if (result.success) {
         toast.success(result.message)
         
-        // 5. 데이터 새로고침
+        // 데이터 새로고침
         loadData({})
         
-        // 6. 선택 해제
+        // 선택 해제
         table.toggleAllPageRowsSelected(false)
       } else {
         toast.error(result.message)
@@ -477,7 +492,7 @@ export const ProjectAvlTable = forwardRef<ProjectAvlTableRef, ProjectAvlTablePro
     } finally {
       setIsConfirmDialogOpen(false)
     }
-  }, [localProjectCode, projectInfo, data, table, loadData, sessionData?.user?.name])
+  }, [localProjectCode, projectInfo, searchHtDivision, table, loadData, sessionData?.user?.name])
 
   // 선택된 행 개수 (안정적인 계산을 위해 useMemo 사용)
   const selectedRows = table.getFilteredSelectedRowModel().rows
@@ -500,6 +515,11 @@ export const ProjectAvlTable = forwardRef<ProjectAvlTableRef, ProjectAvlTablePro
     }
   }, [resetCounter, table])
 
+  // H/T 구분 변경 시 부모 컴포넌트에 알림
+  React.useEffect(() => {
+    onHtDivisionChange?.(searchHtDivision)
+  }, [searchHtDivision, onHtDivisionChange])
+
 
   return (
     <div className="h-full flex flex-col">
@@ -536,7 +556,7 @@ export const ProjectAvlTable = forwardRef<ProjectAvlTableRef, ProjectAvlTablePro
               variant="outline" 
               size="sm" 
               onClick={handleFinalizeAvl}
-              disabled={!localProjectCode.trim() || !projectInfo || data.length === 0}
+              disabled={!isAllConditionsSelected || data.length === 0}
             >
               최종 확정
             </Button>
@@ -568,11 +588,11 @@ export const ProjectAvlTable = forwardRef<ProjectAvlTableRef, ProjectAvlTablePro
             />
 
             {/* 원본파일 */}
-            <ProjectFileField
+            {/* <ProjectFileField
               label="원본파일"
               originalFile={originalFile}
               onFileUpload={handleFileUpload}
-            />
+            /> */}
 
             {/* 공사부문 */}
             <ProjectDisplayField
@@ -589,16 +609,21 @@ export const ProjectAvlTable = forwardRef<ProjectAvlTableRef, ProjectAvlTablePro
             />
 
             {/* H/T 구분 */}
-            <ProjectDisplayField
-              label="H/T 구분"
-              value={projectInfo?.htDivision || ''}
-              status={projectSearchStatus}
-              minWidth="140px"
-              formatter={(value) =>
-                value === 'H' ? 'Hull (H)' :
-                value === 'T' ? 'Topside (T)' : '-'
-              }
-            />
+            <div className="flex flex-col gap-1 min-w-[140px]">
+              <label className="text-sm font-medium">H/T 구분</label>
+              <Select value={searchHtDivision} onValueChange={setSearchHtDivision}>
+                <SelectTrigger className="h-9 bg-background">
+                  <SelectValue placeholder="선택" />
+                </SelectTrigger>
+                <SelectContent>
+                  {htDivisionOptions.map((option) => (
+                    <SelectItem key={option.value} value={option.value}>
+                      {option.label}
+                    </SelectItem>
+                  ))}
+                </SelectContent>
+              </Select>
+            </div>
         </div>
       </div>
 
@@ -636,8 +661,10 @@ export const ProjectAvlTable = forwardRef<ProjectAvlTableRef, ProjectAvlTablePro
             <div>• 프로젝트명: {projectInfo?.projectName || ""}</div>
             <div>• 공사부문: {projectInfo?.constructionSector || ""}</div>
             <div>• 선종: {projectInfo?.shipType || ""}</div>
-            <div>• H/T 구분: {projectInfo?.htDivision || ""}</div>
-            <div>• 벤더 정보: {data.length}개 (전체 레코드)</div>
+            <div>• H/T 구분: {searchHtDivision === 'H' ? 'Hull (H)' : searchHtDivision === 'T' ? 'Top (T)' : searchHtDivision}</div>
+            <div className="font-semibold text-primary mt-4">
+              • 확정될 벤더 정보: {totalVendorInfoCount}개
+            </div>
             {/* <div className="text-amber-600 font-medium mt-4">
               ⚠️ 확정 후 내용 수정을 필요로 하는 경우 동일 건을 다시 최종확정해 revision 처리로 수정해야 합니다.
             </div> */}
-- 
cgit v1.2.3

