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/avl-registration-area.tsx | 316 ++++++++++++++++++++++++++++++--
 1 file changed, 303 insertions(+), 13 deletions(-)

(limited to 'lib/avl/table/avl-registration-area.tsx')

diff --git a/lib/avl/table/avl-registration-area.tsx b/lib/avl/table/avl-registration-area.tsx
index def3d30a..52912a2c 100644
--- a/lib/avl/table/avl-registration-area.tsx
+++ b/lib/avl/table/avl-registration-area.tsx
@@ -5,15 +5,27 @@ import { Card } from "@/components/ui/card"
 import { Button } from "@/components/ui/button"
 import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from "lucide-react"
 import { useAtom } from "jotai"
-import { ProjectAvlTable } from "./project-avl-table"
-import { StandardAvlTable } from "./standard-avl-table"
-import { VendorPoolTable } from "./vendor-pool-table"
+import { ProjectAvlTable, ProjectAvlTableRef } from "./project-avl-table"
+import { StandardAvlTable, StandardAvlTableRef } from "./standard-avl-table"
+import { VendorPoolTable, VendorPoolTableRef } from "./vendor-pool-table"
 import { selectedAvlRecordAtom } from "../avl-atoms"
-import type { AvlListItem } from "../types"
+import { copyToProjectAvl, copyToStandardAvl, copyToVendorPool, copyFromVendorPoolToProjectAvl, copyFromVendorPoolToStandardAvl, copyFromStandardAvlToVendorPool } from "../service"
+import { useSession } from "next-auth/react"
+import { toast } from "sonner"
 
 // 선택된 테이블 타입
 type SelectedTable = 'project' | 'standard' | 'vendor' | null
 
+// TODO: 나머지 테이블들도 ref 지원 추가 시 인터페이스 추가 필요
+// interface StandardAvlTableRef {
+//   getSelectedIds?: () => number[]
+// }
+//
+// interface VendorPoolTableRef {
+//   getSelectedIds?: () => number[]
+// }
+
+
 // 선택 상태 액션 타입
 type SelectionAction =
   | { type: 'SELECT_PROJECT'; count: number }
@@ -105,9 +117,13 @@ interface AvlRegistrationAreaProps {
 }
 
 export function AvlRegistrationArea({ disabled = false }: AvlRegistrationAreaProps) {
+
   // 선택된 AVL 레코드 구독
   const [selectedAvlRecord] = useAtom(selectedAvlRecordAtom)
 
+  // 세션 정보
+  const { data: session } = useSession()
+
   // 단일 선택 상태 관리 (useReducer 사용)
   const [selectionState, dispatch] = React.useReducer(selectionReducer, {
     selectedTable: null,
@@ -121,10 +137,12 @@ export function AvlRegistrationArea({ disabled = false }: AvlRegistrationAreaPro
 
   // 선택 핸들러들
   const handleProjectSelection = React.useCallback((count: number) => {
+    console.log('handleProjectSelection called with count:', count)
     dispatch({ type: 'SELECT_PROJECT', count })
   }, [])
 
   const handleStandardSelection = React.useCallback((count: number) => {
+    console.log('handleStandardSelection called with count:', count)
     dispatch({ type: 'SELECT_STANDARD', count })
   }, [])
 
@@ -134,6 +152,8 @@ export function AvlRegistrationArea({ disabled = false }: AvlRegistrationAreaPro
 
   const { selectedTable, selectedRowCount, resetCounters } = selectionState
 
+  console.log('selectedTable', selectedTable);
+
   // 선택된 AVL에 따른 필터 값들
   const [currentProjectCode, setCurrentProjectCode] = React.useState<string>("")
   const constructionSector = selectedAvlRecord?.constructionSector || ""
@@ -142,6 +162,38 @@ export function AvlRegistrationArea({ disabled = false }: AvlRegistrationAreaPro
   const htDivision = selectedAvlRecord?.htDivision || ""
   const avlListId = selectedAvlRecord?.id ? String(selectedAvlRecord.id) : ""
 
+  // 선종별 표준 AVL 검색 조건 상태 (복사 버튼 활성화용)
+  const [standardSearchConditions, setStandardSearchConditions] = React.useState({
+    constructionSector: "",
+    shipType: "",
+    avlKind: "",
+    htDivision: ""
+  })
+
+  // 검색 조건이 모두 입력되었는지 확인
+  const isStandardSearchConditionsComplete = React.useMemo(() => {
+    return (
+      standardSearchConditions.constructionSector.trim() !== "" &&
+      standardSearchConditions.shipType.trim() !== "" &&
+      standardSearchConditions.avlKind.trim() !== "" &&
+      standardSearchConditions.htDivision.trim() !== ""
+    )
+  }, [standardSearchConditions])
+
+  // 벤더 풀 리로드 트리거
+  const [vendorPoolReloadTrigger, setVendorPoolReloadTrigger] = React.useState(0)
+
+  // 선종별 표준 AVL 리로드 트리거
+  const [standardAvlReloadTrigger, setStandardAvlReloadTrigger] = React.useState(0)
+
+  // 프로젝트 AVL 리로드 트리거
+  const [projectAvlReloadTrigger, setProjectAvlReloadTrigger] = React.useState(0)
+
+  // 테이블 ref들 (선택된 행 정보 가져오기용)
+  const projectTableRef = React.useRef<ProjectAvlTableRef>(null)
+  const standardTableRef = React.useRef<StandardAvlTableRef>(null)
+  const vendorTableRef = React.useRef<VendorPoolTableRef>(null)
+
   // 선택된 AVL 레코드가 변경될 때 프로젝트 코드 초기화
   React.useEffect(() => {
     setCurrentProjectCode(selectedAvlRecord?.projectCode || "")
@@ -152,6 +204,231 @@ export function AvlRegistrationArea({ disabled = false }: AvlRegistrationAreaPro
     setCurrentProjectCode(projectCode)
   }, [])
 
+  // 선택된 ID들을 가져오는 헬퍼 함수들
+  const getSelectedIds = React.useCallback((tableType: 'project' | 'standard' | 'vendor') => {
+    // 각 테이블 컴포넌트에서 선택된 행들의 ID를 가져오는 로직
+    switch (tableType) {
+      case 'project':
+        return projectTableRef.current?.getSelectedIds?.() || []
+      case 'standard':
+        return standardTableRef.current?.getSelectedIds?.() || []
+      case 'vendor':
+        return vendorTableRef.current?.getSelectedIds?.() || []
+      default:
+        return []
+    }
+  }, [])
+
+  // 복사 버튼 핸들러들
+  const handleCopyToProject = React.useCallback(async () => {
+    if (selectedTable !== 'standard' || selectedRowCount === 0) return
+
+    const selectedIds = getSelectedIds('standard')
+    if (!selectedIds.length) {
+      toast.error("복사할 항목을 선택해주세요.")
+      return
+    }
+
+    if (!currentProjectCode) {
+      toast.error("프로젝트 코드가 설정되지 않았습니다.")
+      return
+    }
+
+    try {
+      const result = await copyToProjectAvl(
+        selectedIds,
+        currentProjectCode,
+        parseInt(avlListId) || 1,
+        session?.user?.name || "unknown"
+      )
+
+      if (result.success) {
+        toast.success(result.message)
+        // 선택 해제
+        dispatch({ type: 'CLEAR_SELECTION' })
+      } else {
+        toast.error(result.message)
+      }
+    } catch (error) {
+      console.error('프로젝트AVL로 복사 실패:', error)
+      toast.error("복사 중 오류가 발생했습니다.")
+    }
+  }, [selectedTable, selectedRowCount, getSelectedIds, currentProjectCode, avlListId, session])
+
+  const handleCopyToStandard = React.useCallback(async () => {
+    if (selectedTable !== 'project' || selectedRowCount === 0) return
+
+    const selectedIds = getSelectedIds('project')
+    if (!selectedIds.length) {
+      toast.error("복사할 항목을 선택해주세요.")
+      return
+    }
+
+    const targetStandardInfo = {
+      constructionSector: standardSearchConditions.constructionSector || "조선",
+      shipType: standardSearchConditions.shipType || "",
+      avlKind: standardSearchConditions.avlKind || "",
+      htDivision: standardSearchConditions.htDivision || "H"
+    }
+
+    try {
+      const result = await copyToStandardAvl(
+        selectedIds,
+        targetStandardInfo,
+        session?.user?.name || "unknown"
+      )
+
+      if (result.success) {
+        toast.success(result.message)
+        // 선종별 표준 AVL 데이터 리로드
+        setStandardAvlReloadTrigger(prev => prev + 1)
+        // 선택 해제
+        dispatch({ type: 'CLEAR_SELECTION' })
+      } else {
+        toast.error(result.message)
+      }
+    } catch (error) {
+      console.error('선종별표준AVL로 복사 실패:', error)
+      toast.error("복사 중 오류가 발생했습니다.")
+    }
+  }, [selectedTable, selectedRowCount, getSelectedIds, standardSearchConditions, session])
+
+  const handleCopyToVendorPool = React.useCallback(async () => {
+    if (selectedTable !== 'project' || selectedRowCount === 0) return
+
+    const selectedIds = getSelectedIds('project')
+    if (!selectedIds.length) {
+      toast.error("복사할 항목을 선택해주세요.")
+      return
+    }
+
+    try {
+      const result = await copyToVendorPool(
+        selectedIds,
+        session?.user?.name || "unknown"
+      )
+
+      if (result.success) {
+        toast.success(result.message)
+        // 벤더 풀 데이터 리로드
+        setVendorPoolReloadTrigger(prev => prev + 1)
+        // 선택 해제
+        dispatch({ type: 'CLEAR_SELECTION' })
+      } else {
+        toast.error(result.message)
+      }
+    } catch (error) {
+      console.error('벤더풀로 복사 실패:', error)
+      toast.error("복사 중 오류가 발생했습니다.")
+    }
+  }, [selectedTable, selectedRowCount, getSelectedIds, session])
+
+  // 추가 복사 버튼 핸들러들
+  const handleCopyFromVendorToProject = React.useCallback(async () => {
+    if (selectedTable !== 'vendor' || selectedRowCount === 0) return
+
+    const selectedIds = getSelectedIds('vendor')
+    if (!selectedIds.length) {
+      toast.error("복사할 항목을 선택해주세요.")
+      return
+    }
+
+    if (!currentProjectCode) {
+      toast.error("프로젝트 코드가 설정되지 않았습니다.")
+      return
+    }
+
+    try {
+      const result = await copyFromVendorPoolToProjectAvl(
+        selectedIds,
+        currentProjectCode,
+        parseInt(avlListId) || 1,
+        session?.user?.name || "unknown"
+      )
+
+      if (result.success) {
+        toast.success(result.message)
+        // 프로젝트 AVL 리로드
+        setProjectAvlReloadTrigger(prev => prev + 1)
+        // 선택 해제
+        dispatch({ type: 'CLEAR_SELECTION' })
+      } else {
+        toast.error(result.message)
+      }
+    } catch (error) {
+      console.error('벤더풀 → 프로젝트AVL 복사 실패:', error)
+      toast.error("복사 중 오류가 발생했습니다.")
+    }
+  }, [selectedTable, selectedRowCount, getSelectedIds, currentProjectCode, avlListId, session])
+
+  const handleCopyFromVendorToStandard = React.useCallback(async () => {
+    if (selectedTable !== 'vendor' || selectedRowCount === 0) return
+
+    const selectedIds = getSelectedIds('vendor')
+    if (!selectedIds.length) {
+      toast.error("복사할 항목을 선택해주세요.")
+      return
+    }
+
+    const targetStandardInfo = {
+      constructionSector: standardSearchConditions.constructionSector || "조선",
+      shipType: standardSearchConditions.shipType || "",
+      avlKind: standardSearchConditions.avlKind || "",
+      htDivision: standardSearchConditions.htDivision || "H"
+    }
+
+    try {
+      const result = await copyFromVendorPoolToStandardAvl(
+        selectedIds,
+        targetStandardInfo,
+        session?.user?.name || "unknown"
+      )
+
+      if (result.success) {
+        toast.success(result.message)
+        // 선종별 표준 AVL 리로드
+        setStandardAvlReloadTrigger(prev => prev + 1)
+        // 선택 해제
+        dispatch({ type: 'CLEAR_SELECTION' })
+      } else {
+        toast.error(result.message)
+      }
+    } catch (error) {
+      console.error('벤더풀 → 선종별표준AVL 복사 실패:', error)
+      toast.error("복사 중 오류가 발생했습니다.")
+    }
+  }, [selectedTable, selectedRowCount, getSelectedIds, standardSearchConditions, session])
+
+  const handleCopyFromStandardToVendor = React.useCallback(async () => {
+    if (selectedTable !== 'standard' || selectedRowCount === 0) return
+
+    const selectedIds = getSelectedIds('standard')
+    if (!selectedIds.length) {
+      toast.error("복사할 항목을 선택해주세요.")
+      return
+    }
+
+    try {
+      const result = await copyFromStandardAvlToVendorPool(
+        selectedIds,
+        session?.user?.name || "unknown"
+      )
+
+      if (result.success) {
+        toast.success(result.message)
+        // 벤더 풀 리로드
+        setVendorPoolReloadTrigger(prev => prev + 1)
+        // 선택 해제
+        dispatch({ type: 'CLEAR_SELECTION' })
+      } else {
+        toast.error(result.message)
+      }
+    } catch (error) {
+      console.error('선종별표준AVL → 벤더풀 복사 실패:', error)
+      toast.error("복사 중 오류가 발생했습니다.")
+    }
+  }, [selectedTable, selectedRowCount, getSelectedIds, session])
+
   return (
     <Card className={`h-full min-w-full overflow-visible ${disabled ? 'opacity-50 pointer-events-none' : ''}`}>
       {/* 고정 헤더 영역 */}
@@ -159,9 +436,9 @@ export function AvlRegistrationArea({ disabled = false }: AvlRegistrationAreaPro
         <div className="flex items-center justify-between">
           <h3 className="text-lg font-semibold">AVL 등록 {disabled ? "(비활성화)" : ""}</h3>
           <div className="flex gap-2">
-            <Button variant="outline" size="sm" disabled={disabled}>
+            {/* <Button variant="outline" size="sm" disabled={disabled}>
               AVL 불러오기
-            </Button>
+            </Button> */}
           </div>
         </div>
       </div>
@@ -172,11 +449,13 @@ export function AvlRegistrationArea({ disabled = false }: AvlRegistrationAreaPro
           {/* 프로젝트 AVL 테이블 - 9개 컬럼 */}
           <div className="p-4 border-r relative">
             <ProjectAvlTable
+              ref={projectTableRef}
               onSelectionChange={handleProjectSelection}
               resetCounter={resetCounters.project}
               projectCode={currentProjectCode}
               avlListId={parseInt(avlListId) || 1}
               onProjectCodeChange={handleProjectCodeChange}
+              reloadTrigger={projectAvlReloadTrigger}
             />
 
             {/* 이동 버튼들 - 첫 번째 border 위에 오버레이 */}
@@ -188,7 +467,8 @@ export function AvlRegistrationArea({ disabled = false }: AvlRegistrationAreaPro
                   size="sm"
                   className="w-8 h-8 p-0"
                   title="프로젝트AVL로 복사"
-                  disabled={disabled || selectedTable !== 'standard' || selectedRowCount === 0}
+                  disabled={disabled || selectedTable === 'project' || selectedRowCount === 0}
+                  onClick={handleCopyToProject}
                 >
                   <ChevronLeft className="w-4 h-4" />
                 </Button>
@@ -198,7 +478,8 @@ export function AvlRegistrationArea({ disabled = false }: AvlRegistrationAreaPro
                   size="sm"
                   className="w-8 h-8 p-0"
                   title="선종별표준AVL로 복사"
-                  disabled={disabled || selectedTable !== 'project' || selectedRowCount === 0}
+                  disabled={disabled || selectedTable !== 'project' || selectedRowCount === 0 || !isStandardSearchConditionsComplete}
+                  onClick={handleCopyToStandard}
                 >
                   <ChevronRight className="w-4 h-4" />
                 </Button>
@@ -209,6 +490,7 @@ export function AvlRegistrationArea({ disabled = false }: AvlRegistrationAreaPro
                   className="w-8 h-8 p-0"
                   title="벤더풀로 복사"
                   disabled={disabled || selectedTable !== 'project' || selectedRowCount === 0}
+                  onClick={handleCopyToVendorPool}
                 >
                   <ChevronsRight className="w-4 h-4" />
                 </Button>
@@ -220,12 +502,15 @@ export function AvlRegistrationArea({ disabled = false }: AvlRegistrationAreaPro
           {/* 선종별 표준 AVL 테이블 - 8개 컬럼 */}
           <div className="p-4 border-r relative">
             <StandardAvlTable
+              ref={standardTableRef}
               onSelectionChange={handleStandardSelection}
               resetCounter={resetCounters.standard}
               constructionSector={constructionSector}
               shipType={shipType}
               avlKind={avlKind}
               htDivision={htDivision}
+              onSearchConditionsChange={setStandardSearchConditions}
+              reloadTrigger={standardAvlReloadTrigger}
             />
 
             {/* 이동 버튼들 - 두 번째 border 위에 오버레이 */}
@@ -235,8 +520,9 @@ export function AvlRegistrationArea({ disabled = false }: AvlRegistrationAreaPro
                   variant="outline"
                   size="sm"
                   className="w-8 h-8 p-0"
-                  title="프로젝트AVL로 복사"
-                  disabled={disabled || selectedTable !== 'vendor' || selectedRowCount === 0}
+                  title="벤더풀의 항목을 프로젝트AVL로 복사"
+                  disabled={disabled || selectedTable !== 'vendor' || selectedRowCount === 0 || !currentProjectCode}
+                  onClick={handleCopyFromVendorToProject}
                 >
                   <ChevronsLeft className="w-4 h-4" />
                 </Button>
@@ -245,8 +531,9 @@ export function AvlRegistrationArea({ disabled = false }: AvlRegistrationAreaPro
                   variant="outline"
                   size="sm"
                   className="w-8 h-8 p-0"
-                  title="선종별표준AVL로 복사"
-                  disabled={disabled || selectedTable !== 'vendor' || selectedRowCount === 0}
+                  title="벤더풀의 항목을 선종별표준AVL로 복사"
+                  disabled={disabled || selectedTable !== 'vendor' || selectedRowCount === 0 || !isStandardSearchConditionsComplete}
+                  onClick={handleCopyFromVendorToStandard}
                 >
                   <ChevronLeft className="w-4 h-4" />
                 </Button>
@@ -254,8 +541,9 @@ export function AvlRegistrationArea({ disabled = false }: AvlRegistrationAreaPro
                   variant="outline"
                   size="sm"
                   className="w-8 h-8 p-0"
-                  title="벤더풀로 복사"
+                  title="선종별표준AVL의 항목을 벤더풀로 복사"
                   disabled={disabled || selectedTable !== 'standard' || selectedRowCount === 0}
+                  onClick={handleCopyFromStandardToVendor}
                 >
                   <ChevronRight className="w-4 h-4" />
                 </Button>
@@ -267,8 +555,10 @@ export function AvlRegistrationArea({ disabled = false }: AvlRegistrationAreaPro
           {/* Vendor Pool 테이블 - 10개 컬럼 */}
           <div className="p-4 relative">
             <VendorPoolTable
+              ref={vendorTableRef}
               onSelectionChange={handleVendorSelection}
               resetCounter={resetCounters.vendor}
+              reloadTrigger={vendorPoolReloadTrigger}
             />
           </div>
         </div>
-- 
cgit v1.2.3

