summaryrefslogtreecommitdiff
path: root/lib/avl/table/project-avl-table.tsx
diff options
context:
space:
mode:
authorjoonhoekim <26rote@gmail.com>2025-10-14 14:25:28 +0900
committerjoonhoekim <26rote@gmail.com>2025-10-14 14:25:28 +0900
commit40250c61031263606dd073ce7056a3e8e27f18d0 (patch)
tree0ea566507b3b341825e9825f9cee43f470957292 /lib/avl/table/project-avl-table.tsx
parent6d3752d34dfdf2c3870b9f6ffe431cfa98e302c9 (diff)
(김준회) AVL 구매요구사항 수정
- AVL 상세 엑셀 익스포트 추가 - 레코드 이동 멀티선택 추가 - 최종확정처리 오류 수정 - 프로젝트 AVL에 H/T 구분 추가
Diffstat (limited to 'lib/avl/table/project-avl-table.tsx')
-rw-r--r--lib/avl/table/project-avl-table.tsx173
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> */}