diff options
| author | joonhoekim <26rote@gmail.com> | 2025-09-15 18:58:07 +0900 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-09-15 18:58:07 +0900 |
| commit | 2b490956c9752c1b756780a3461bc1c37b6fe0a7 (patch) | |
| tree | b0b8a03c8de5dfce4b6c7373a9d608306e9147c0 /lib/avl/table/avl-registration-area.tsx | |
| parent | e7818a457371849e29519497ebf046f385f05ab6 (diff) | |
(김준회) AVL 관리 및 상세 - 기능 구현 1차
+ docker compose 내 오류 수정
Diffstat (limited to 'lib/avl/table/avl-registration-area.tsx')
| -rw-r--r-- | lib/avl/table/avl-registration-area.tsx | 316 |
1 files changed, 303 insertions, 13 deletions
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> |
