diff options
| author | joonhoekim <26rote@gmail.com> | 2025-09-15 18:59:41 +0900 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-09-15 18:59:41 +0900 |
| commit | d5f26d34c4ac6f3eaac16fbc6069de2c2341a6ff (patch) | |
| tree | ad4ecb476a6fd3b754e741e795bd7a3adbbe03ea /lib/avl/table/avl-registration-area.tsx | |
| parent | 25b916d040a512cd5248dff319d727ae144d0652 (diff) | |
| parent | 2b490956c9752c1b756780a3461bc1c37b6fe0a7 (diff) | |
[Merge] AVL 및 Vendor-Pool 기능 1차 구현
Diffstat (limited to 'lib/avl/table/avl-registration-area.tsx')
| -rw-r--r-- | lib/avl/table/avl-registration-area.tsx | 568 |
1 files changed, 568 insertions, 0 deletions
diff --git a/lib/avl/table/avl-registration-area.tsx b/lib/avl/table/avl-registration-area.tsx new file mode 100644 index 00000000..52912a2c --- /dev/null +++ b/lib/avl/table/avl-registration-area.tsx @@ -0,0 +1,568 @@ +"use client" + +import * as React from "react" +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, 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 { 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 } + | { type: 'SELECT_STANDARD'; count: number } + | { type: 'SELECT_VENDOR'; count: number } + | { type: 'CLEAR_SELECTION' } + +// 선택 상태 +interface SelectionState { + selectedTable: SelectedTable + selectedRowCount: number + resetCounters: { + project: number + standard: number + vendor: number + } +} + +// 선택 상태 리듀서 +const selectionReducer = (state: SelectionState, action: SelectionAction): SelectionState => { + switch (action.type) { + case 'SELECT_PROJECT': + if (action.count > 0) { + return { + selectedTable: 'project', + selectedRowCount: action.count, + resetCounters: { + ...state.resetCounters, + standard: state.selectedTable !== 'project' ? state.resetCounters.standard + 1 : state.resetCounters.standard, + vendor: state.selectedTable !== 'project' ? state.resetCounters.vendor + 1 : state.resetCounters.vendor, + } + } + } else if (state.selectedTable === 'project') { + return { + ...state, + selectedTable: null, + selectedRowCount: 0, + } + } + return state + + case 'SELECT_STANDARD': + if (action.count > 0) { + return { + selectedTable: 'standard', + selectedRowCount: action.count, + resetCounters: { + ...state.resetCounters, + project: state.selectedTable !== 'standard' ? state.resetCounters.project + 1 : state.resetCounters.project, + vendor: state.selectedTable !== 'standard' ? state.resetCounters.vendor + 1 : state.resetCounters.vendor, + } + } + } else if (state.selectedTable === 'standard') { + return { + ...state, + selectedTable: null, + selectedRowCount: 0, + } + } + return state + + case 'SELECT_VENDOR': + if (action.count > 0) { + return { + selectedTable: 'vendor', + selectedRowCount: action.count, + resetCounters: { + ...state.resetCounters, + project: state.selectedTable !== 'vendor' ? state.resetCounters.project + 1 : state.resetCounters.project, + standard: state.selectedTable !== 'vendor' ? state.resetCounters.standard + 1 : state.resetCounters.standard, + } + } + } else if (state.selectedTable === 'vendor') { + return { + ...state, + selectedTable: null, + selectedRowCount: 0, + } + } + return state + + default: + return state + } +} + +interface AvlRegistrationAreaProps { + disabled?: boolean // 비활성화 상태 +} + +export function AvlRegistrationArea({ disabled = false }: AvlRegistrationAreaProps) { + + // 선택된 AVL 레코드 구독 + const [selectedAvlRecord] = useAtom(selectedAvlRecordAtom) + + // 세션 정보 + const { data: session } = useSession() + + // 단일 선택 상태 관리 (useReducer 사용) + const [selectionState, dispatch] = React.useReducer(selectionReducer, { + selectedTable: null, + selectedRowCount: 0, + resetCounters: { + project: 0, + standard: 0, + vendor: 0, + }, + }) + + // 선택 핸들러들 + 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 }) + }, []) + + const handleVendorSelection = React.useCallback((count: number) => { + dispatch({ type: 'SELECT_VENDOR', count }) + }, []) + + const { selectedTable, selectedRowCount, resetCounters } = selectionState + + console.log('selectedTable', selectedTable); + + // 선택된 AVL에 따른 필터 값들 + const [currentProjectCode, setCurrentProjectCode] = React.useState<string>("") + const constructionSector = selectedAvlRecord?.constructionSector || "" + const shipType = selectedAvlRecord?.shipType || "" + const avlKind = selectedAvlRecord?.avlKind || "" + 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 || "") + }, [selectedAvlRecord?.projectCode]) + + // 프로젝트 코드 변경 핸들러 + const handleProjectCodeChange = React.useCallback((projectCode: string) => { + 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' : ''}`}> + {/* 고정 헤더 영역 */} + <div className="sticky top-0 z-10 p-4 border-b"> + <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}> + AVL 불러오기 + </Button> */} + </div> + </div> + </div> + + {/* 스크롤되는 콘텐츠 영역 */} + <div className="overflow-x-auto overflow-y-hidden"> + <div className="grid grid-cols-[2.2fr_2fr_2.5fr] gap-0 min-w-[1200px] w-fit"> + {/* 프로젝트 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 위에 오버레이 */} + <div className="absolute right-0 top-1/2 transform -translate-y-1/2 translate-x-1/2 z-10"> + <div className="flex flex-col gap-2 bg-background border rounded-md p-1 shadow-sm"> + + <Button + variant="outline" + size="sm" + className="w-8 h-8 p-0" + title="프로젝트AVL로 복사" + disabled={disabled || selectedTable === 'project' || selectedRowCount === 0} + onClick={handleCopyToProject} + > + <ChevronLeft className="w-4 h-4" /> + </Button> + + <Button + variant="outline" + size="sm" + className="w-8 h-8 p-0" + title="선종별표준AVL로 복사" + disabled={disabled || selectedTable !== 'project' || selectedRowCount === 0 || !isStandardSearchConditionsComplete} + onClick={handleCopyToStandard} + > + <ChevronRight className="w-4 h-4" /> + </Button> + + <Button + variant="outline" + size="sm" + className="w-8 h-8 p-0" + title="벤더풀로 복사" + disabled={disabled || selectedTable !== 'project' || selectedRowCount === 0} + onClick={handleCopyToVendorPool} + > + <ChevronsRight className="w-4 h-4" /> + </Button> + + </div> + </div> + </div> + + {/* 선종별 표준 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 위에 오버레이 */} + <div className="absolute right-0 top-1/2 transform -translate-y-1/2 translate-x-1/2 z-10"> + <div className="flex flex-col gap-2 bg-background border rounded-md p-1 shadow-sm"> + <Button + variant="outline" + size="sm" + className="w-8 h-8 p-0" + title="벤더풀의 항목을 프로젝트AVL로 복사" + disabled={disabled || selectedTable !== 'vendor' || selectedRowCount === 0 || !currentProjectCode} + onClick={handleCopyFromVendorToProject} + > + <ChevronsLeft className="w-4 h-4" /> + </Button> + + <Button + variant="outline" + size="sm" + className="w-8 h-8 p-0" + title="벤더풀의 항목을 선종별표준AVL로 복사" + disabled={disabled || selectedTable !== 'vendor' || selectedRowCount === 0 || !isStandardSearchConditionsComplete} + onClick={handleCopyFromVendorToStandard} + > + <ChevronLeft className="w-4 h-4" /> + </Button> + <Button + variant="outline" + size="sm" + className="w-8 h-8 p-0" + title="선종별표준AVL의 항목을 벤더풀로 복사" + disabled={disabled || selectedTable !== 'standard' || selectedRowCount === 0} + onClick={handleCopyFromStandardToVendor} + > + <ChevronRight className="w-4 h-4" /> + </Button> + + </div> + </div> + </div> + + {/* Vendor Pool 테이블 - 10개 컬럼 */} + <div className="p-4 relative"> + <VendorPoolTable + ref={vendorTableRef} + onSelectionChange={handleVendorSelection} + resetCounter={resetCounters.vendor} + reloadTrigger={vendorPoolReloadTrigger} + /> + </div> + </div> + </div> + </Card> + ) +} |
