diff options
Diffstat (limited to 'lib/avl/table/project-avl-table.tsx')
| -rw-r--r-- | lib/avl/table/project-avl-table.tsx | 173 |
1 files changed, 100 insertions, 73 deletions
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> */} |
