diff options
Diffstat (limited to 'lib/avl')
| -rw-r--r-- | lib/avl/service.ts | 167 | ||||
| -rw-r--r-- | lib/avl/table/avl-detail-table.tsx | 64 | ||||
| -rw-r--r-- | lib/avl/table/avl-registration-area.tsx | 25 | ||||
| -rw-r--r-- | lib/avl/table/columns-detail.tsx | 54 | ||||
| -rw-r--r-- | lib/avl/table/project-avl-table-columns.tsx | 30 | ||||
| -rw-r--r-- | lib/avl/table/project-avl-table.tsx | 173 | ||||
| -rw-r--r-- | lib/avl/table/standard-avl-table-columns.tsx | 30 | ||||
| -rw-r--r-- | lib/avl/table/standard-avl-table.tsx | 40 | ||||
| -rw-r--r-- | lib/avl/table/vendor-pool-table-columns.tsx | 30 | ||||
| -rw-r--r-- | lib/avl/validations.ts | 5 |
10 files changed, 411 insertions, 207 deletions
diff --git a/lib/avl/service.ts b/lib/avl/service.ts index 1f781486..5d7c2418 100644 --- a/lib/avl/service.ts +++ b/lib/avl/service.ts @@ -941,9 +941,71 @@ export async function deleteAvlVendorInfo(id: number): Promise<boolean> { } /** + * 프로젝트 AVL 벤더 정보 건수 조회 + */ +export async function getProjectAvlVendorInfoCount( + projectCode: string, + htDivision?: string +): Promise<number> { + try { + const conditions = [ + eq(avlVendorInfo.projectCode, projectCode), + eq(avlVendorInfo.isTemplate, false) + ]; + + // H/T 구분이 있으면 조건에 추가 + if (htDivision) { + conditions.push(eq(avlVendorInfo.htDivision, htDivision)); + } + + const result = await db + .select({ count: count() }) + .from(avlVendorInfo) + .where(and(...conditions)); + + return result[0]?.count || 0; + } catch (error) { + console.error("Error in getProjectAvlVendorInfoCount:", error); + return 0; + } +} + +/** + * 표준 AVL 벤더 정보 건수 조회 + */ +export async function getStandardAvlVendorInfoCount( + standardAvlInfo: { + constructionSector: string; + shipType: string; + avlKind: string; + htDivision: string; + } +): Promise<number> { + try { + const result = await db + .select({ count: count() }) + .from(avlVendorInfo) + .where( + and( + eq(avlVendorInfo.isTemplate, true), + eq(avlVendorInfo.constructionSector, standardAvlInfo.constructionSector), + eq(avlVendorInfo.shipType, standardAvlInfo.shipType), + eq(avlVendorInfo.avlKind, standardAvlInfo.avlKind), + eq(avlVendorInfo.htDivision, standardAvlInfo.htDivision) + ) + ); + + return result[0]?.count || 0; + } catch (error) { + console.error("Error in getStandardAvlVendorInfoCount:", error); + return 0; + } +} + +/** * 프로젝트 AVL 최종 확정 * 1. 주어진 프로젝트 정보로 avlList에 레코드를 생성한다. - * 2. 현재 avlVendorInfo 레코드들의 avlListId를 새로 생성된 AVL 리스트 ID로 업데이트한다. + * 2. 해당 프로젝트 코드의 모든 avlVendorInfo 레코드들의 avlListId를 새로 생성된 AVL 리스트 ID로 업데이트한다. */ export async function finalizeProjectAvl( projectCode: string, @@ -953,23 +1015,48 @@ export async function finalizeProjectAvl( shipType: string; htDivision: string; }, - avlVendorInfoIds: number[], currentUser?: string ): Promise<{ success: boolean; avlListId?: number; message: string }> { try { debugLog('프로젝트 AVL 최종 확정 시작', { projectCode, projectInfo, - avlVendorInfoIds: avlVendorInfoIds.length, currentUser }); - // 1. 기존 AVL 리스트의 최고 revision 확인 + // 1. DB에서 해당 프로젝트 코드와 H/T 구분의 모든 avlVendorInfo 레코드 ID 조회 + const allVendorInfoRecords = await db + .select({ id: avlVendorInfo.id }) + .from(avlVendorInfo) + .where( + and( + eq(avlVendorInfo.projectCode, projectCode), + eq(avlVendorInfo.htDivision, projectInfo.htDivision), + eq(avlVendorInfo.isTemplate, false) + ) + ); + + const avlVendorInfoIds = allVendorInfoRecords.map(record => record.id); + + if (avlVendorInfoIds.length === 0) { + return { + success: false, + message: "해당 프로젝트의 AVL 벤더 정보가 없습니다." + }; + } + + debugLog('프로젝트 AVL 벤더 정보 조회 완료', { + projectCode, + totalVendorInfoCount: avlVendorInfoIds.length + }); + + // 2. 기존 AVL 리스트의 최고 revision 확인 (프로젝트 코드 + H/T 구분별로) const existingAvlLists = await db .select({ rev: avlList.rev }) .from(avlList) .where(and( eq(avlList.projectCode, projectCode), + eq(avlList.htDivision, projectInfo.htDivision), eq(avlList.isTemplate, false) )) .orderBy(desc(avlList.rev)) @@ -983,7 +1070,7 @@ export async function finalizeProjectAvl( nextRevision }); - // 2. AVL 리스트 생성을 위한 데이터 준비 + // 3. AVL 리스트 생성을 위한 데이터 준비 const createAvlListData: CreateAvlListInput = { isTemplate: false, // 프로젝트 AVL이므로 false constructionSector: projectInfo.constructionSector, @@ -998,7 +1085,7 @@ export async function finalizeProjectAvl( debugLog('AVL 리스트 생성 데이터', { createAvlListData }); - // 2. AVL Vendor Info 스냅샷 생성 (AVL 리스트 생성 전에 현재 상태 저장) + // 4. AVL Vendor Info 스냅샷 생성 (AVL 리스트 생성 전에 현재 상태 저장) debugLog('AVL Vendor Info 스냅샷 생성 시작', { vendorInfoIdsCount: avlVendorInfoIds.length, vendorInfoIds: avlVendorInfoIds @@ -1017,7 +1104,7 @@ export async function finalizeProjectAvl( snapshotLength: createAvlListData.vendorInfoSnapshot?.length }); - // 3. AVL 리스트 생성 + // 5. AVL 리스트 생성 const newAvlList = await createAvlList(createAvlListData); if (!newAvlList) { @@ -1026,7 +1113,7 @@ export async function finalizeProjectAvl( debugSuccess('AVL 리스트 생성 완료', { avlListId: newAvlList.id }); - // 3. avlVendorInfo 레코드들의 avlListId 업데이트 + // 6. avlVendorInfo 레코드들의 avlListId 업데이트 if (avlVendorInfoIds.length > 0) { debugLog('AVL Vendor Info 업데이트 시작', { count: avlVendorInfoIds.length, @@ -1071,7 +1158,7 @@ export async function finalizeProjectAvl( } } - // 4. 캐시 무효화 + // 7. 캐시 무효화 revalidateTag('avl-list'); revalidateTag('avl-vendor-info'); @@ -1123,6 +1210,11 @@ export const getProjectAvlVendorInfo = async (input: GetProjectAvlSchema) => { whereConditions.push(ilike(avlVendorInfo.projectCode, `%${input.projectCode}%`)); } + // 필수 필터: H/T 구분 (avlVendorInfo에서 직접 필터링) + if (input.htDivision) { + whereConditions.push(eq(avlVendorInfo.htDivision, input.htDivision)); + } + // 검색어 기반 필터링 if (input.search) { const searchTerm = `%${input.search}%`; @@ -1513,11 +1605,12 @@ export const getStandardAvlVendorInfo = async (input: GetStandardAvlSchema) => { export const copyToProjectAvl = async ( selectedIds: number[], targetProjectCode: string, + targetHtDivision: string, targetAvlListId: number, userName: string ): Promise<ActionResult> => { try { - debugLog('선종별표준AVL → 프로젝트AVL 복사 시작', { selectedIds, targetProjectCode, targetAvlListId }); + debugLog('선종별표준AVL → 프로젝트AVL 복사 시작', { selectedIds, targetProjectCode, targetHtDivision, targetAvlListId }); if (!selectedIds.length) { return { success: false, message: "복사할 항목을 선택해주세요." }; @@ -1544,12 +1637,12 @@ export const copyToProjectAvl = async ( id: undefined, // 새 ID 생성 isTemplate: false, // 프로젝트 AVL로 변경 projectCode: targetProjectCode, // 대상 프로젝트 코드 + htDivision: targetHtDivision, // 프로젝트 AVL에서 선택한 H/T 구분 적용 avlListId: targetAvlListId, // 대상 AVL 리스트 ID // 표준 AVL 필드들은 null로 설정 (프로젝트 AVL에서는 사용하지 않음) constructionSector: null, shipType: null, avlKind: null, - htDivision: null, createdAt: undefined, updatedAt: undefined, })); @@ -1887,11 +1980,12 @@ export const copyToVendorPool = async ( export const copyFromVendorPoolToProjectAvl = async ( selectedIds: number[], targetProjectCode: string, + targetHtDivision: string, targetAvlListId: number, userName: string ): Promise<ActionResult> => { try { - debugLog('벤더풀 → 프로젝트AVL 복사 시작', { selectedIds, targetProjectCode, targetAvlListId }); + debugLog('벤더풀 → 프로젝트AVL 복사 시작', { selectedIds, targetProjectCode, targetHtDivision, targetAvlListId }); if (!selectedIds.length) { return { success: false, message: "복사할 항목을 선택해주세요." }; @@ -1913,6 +2007,7 @@ export const copyFromVendorPoolToProjectAvl = async ( const recordsToInsert: NewAvlVendorInfo[] = selectedRecords.map(record => ({ // 프로젝트 AVL용 필드들 projectCode: targetProjectCode, + htDivision: targetHtDivision, // 프로젝트 AVL에서 선택한 H/T 구분 적용 avlListId: targetAvlListId, isTemplate: false, @@ -1921,9 +2016,8 @@ export const copyFromVendorPoolToProjectAvl = async ( vendorName: record.vendorName, vendorCode: record.vendorCode, - // 기본 정보 (벤더풀의 데이터 활용) - constructionSector: record.constructionSector, - htDivision: record.htDivision, + // 기본 정보 (프로젝트 AVL에서는 constructionSector 사용 안함) + constructionSector: null, // 자재그룹 정보 materialGroupCode: record.materialGroupCode, @@ -2332,6 +2426,7 @@ export const copyFromStandardAvlToVendorPool = async ( /** * 표준 AVL 최종 확정 * 표준 AVL을 최종 확정하여 AVL 리스트에 등록합니다. + * DB에서 해당 조건에 맞는 모든 avlVendorInfo 레코드를 조회하여 확정합니다. */ export async function finalizeStandardAvl( standardAvlInfo: { @@ -2340,17 +2435,43 @@ export async function finalizeStandardAvl( avlKind: string; htDivision: string; }, - avlVendorInfoIds: number[], currentUser?: string ): Promise<{ success: boolean; avlListId?: number; message: string }> { try { debugLog('표준 AVL 최종 확정 시작', { standardAvlInfo, - avlVendorInfoIds: avlVendorInfoIds.length, currentUser }); - // 1. 기존 표준 AVL 리스트의 최고 revision 확인 + // 1. DB에서 해당 조건의 모든 avlVendorInfo 레코드 ID 조회 + const allVendorInfoRecords = await db + .select({ id: avlVendorInfo.id }) + .from(avlVendorInfo) + .where( + and( + eq(avlVendorInfo.isTemplate, true), + eq(avlVendorInfo.constructionSector, standardAvlInfo.constructionSector), + eq(avlVendorInfo.shipType, standardAvlInfo.shipType), + eq(avlVendorInfo.avlKind, standardAvlInfo.avlKind), + eq(avlVendorInfo.htDivision, standardAvlInfo.htDivision) + ) + ); + + const avlVendorInfoIds = allVendorInfoRecords.map(record => record.id); + + if (avlVendorInfoIds.length === 0) { + return { + success: false, + message: "해당 조건의 표준 AVL 벤더 정보가 없습니다." + }; + } + + debugLog('표준 AVL 벤더 정보 조회 완료', { + standardAvlInfo, + totalVendorInfoCount: avlVendorInfoIds.length + }); + + // 2. 기존 표준 AVL 리스트의 최고 revision 확인 const existingAvlLists = await db .select({ rev: avlList.rev }) .from(avlList) @@ -2372,7 +2493,7 @@ export async function finalizeStandardAvl( nextRevision }); - // 2. AVL 리스트 생성을 위한 데이터 준비 + // 3. AVL 리스트 생성을 위한 데이터 준비 const createAvlListData: CreateAvlListInput = { isTemplate: true, // 표준 AVL이므로 true constructionSector: standardAvlInfo.constructionSector, @@ -2387,7 +2508,7 @@ export async function finalizeStandardAvl( debugLog('표준 AVL 리스트 생성 데이터', { createAvlListData }); - // 2-1. AVL Vendor Info 스냅샷 생성 (AVL 리스트 생성 전에 현재 상태 저장) + // 4. AVL Vendor Info 스냅샷 생성 (AVL 리스트 생성 전에 현재 상태 저장) debugLog('표준 AVL Vendor Info 스냅샷 생성 시작', { vendorInfoIdsCount: avlVendorInfoIds.length, vendorInfoIds: avlVendorInfoIds @@ -2406,7 +2527,7 @@ export async function finalizeStandardAvl( snapshotLength: createAvlListData.vendorInfoSnapshot?.length }); - // 3. AVL 리스트 생성 + // 5. AVL 리스트 생성 const newAvlList = await createAvlList(createAvlListData); if (!newAvlList) { @@ -2415,7 +2536,7 @@ export async function finalizeStandardAvl( debugSuccess('표준 AVL 리스트 생성 완료', { avlListId: newAvlList.id }); - // 4. avlVendorInfo 레코드들의 avlListId 업데이트 + // 6. avlVendorInfo 레코드들의 avlListId 업데이트 if (avlVendorInfoIds.length > 0) { debugLog('표준 AVL Vendor Info 업데이트 시작', { count: avlVendorInfoIds.length, @@ -2459,7 +2580,7 @@ export async function finalizeStandardAvl( } } - // 5. 캐시 무효화 + // 7. 캐시 무효화 revalidateTag('avl-list'); revalidateTag('avl-vendor-info'); diff --git a/lib/avl/table/avl-detail-table.tsx b/lib/avl/table/avl-detail-table.tsx index ca3ba7e7..407535db 100644 --- a/lib/avl/table/avl-detail-table.tsx +++ b/lib/avl/table/avl-detail-table.tsx @@ -10,6 +10,7 @@ import { toast } from "sonner" import { columns } from "./columns-detail" import type { AvlDetailItem } from "../types" import { BackButton } from "@/components/ui/back-button" +import { exportTableToExcel } from "@/lib/export_all" interface AvlDetailTableProps { data: AvlDetailItem[] @@ -33,7 +34,42 @@ export function AvlDetailTable({ }: AvlDetailTableProps) { - // 액션 핸들러 + // 데이터 테이블 설정 (초기 meta는 없음) + const { table } = useDataTable({ + data, + columns, + pageCount: pageCount ?? 1, + initialState: { + sorting: [{ id: "no", desc: false }], + pagination: { + pageIndex: 0, + pageSize: 10, + }, + }, + getRowId: (row) => String(row.id), + // meta는 useEffect에서 설정 + // 정적 데이터이므로 무한 스크롤 설정하지 않음 (클라이언트 측 소팅 활성화) + infiniteScrollConfig: undefined, + }) + + // Excel export 핸들러 + const handleExcelExport = React.useCallback(async () => { + try { + toast.info("엑셀 파일을 생성 중입니다...") + await exportTableToExcel(table, { + filename: `AVL_상세내역_${new Date().toISOString().split('T')[0]}`, + allPages: true, + excludeColumns: ["select", "actions"], + useGroupHeader: true, + }) + toast.success("엑셀 파일이 다운로드되었습니다.") + } catch (error) { + console.error("Excel export error:", error) + toast.error("엑셀 파일 생성 중 오류가 발생했습니다.") + } + }, [table]) + + // 액션 핸들러 (table을 직접 사용하지 않는 액션들) const handleAction = React.useCallback(async (action: string) => { switch (action) { case 'vendor-pool': @@ -55,29 +91,15 @@ export function AvlDetailTable({ } }, []) - // 테이블 메타 설정 const tableMeta = React.useMemo(() => ({ onAction: handleAction, }), [handleAction]) - // 데이터 테이블 설정 - const { table } = useDataTable({ - data, - columns, - pageCount: pageCount ?? 1, - initialState: { - sorting: [{ id: "no", desc: false }], - pagination: { - pageIndex: 0, - pageSize: 10, - }, - }, - getRowId: (row) => String(row.id), - meta: tableMeta, - // 정적 데이터이므로 무한 스크롤 설정하지 않음 (클라이언트 측 소팅 활성화) - infiniteScrollConfig: undefined, - }) + // table의 meta 설정 + React.useEffect(() => { + table.options.meta = tableMeta + }, [table, tableMeta]) return ( <div className="space-y-4"> @@ -101,6 +123,10 @@ export function AvlDetailTable({ {/* 상단 버튼 영역 */} <div className="flex items-center gap-2 ml-auto justify-end"> + {/* Excel Export 버튼 */} + <Button variant="outline" size="sm" onClick={handleExcelExport}> + Excel Export + </Button> {/* 단순 이동 버튼 */} <Button variant="outline" size="sm" onClick={() => handleAction('vendor-pool')}> Vendor Pool diff --git a/lib/avl/table/avl-registration-area.tsx b/lib/avl/table/avl-registration-area.tsx index 6c7eba9d..1fa4ab5d 100644 --- a/lib/avl/table/avl-registration-area.tsx +++ b/lib/avl/table/avl-registration-area.tsx @@ -145,6 +145,7 @@ export function AvlRegistrationArea({ disabled = false }: AvlRegistrationAreaPro // 선택된 AVL에 따른 필터 값들 const [currentProjectCode, setCurrentProjectCode] = React.useState<string>("") + const [currentProjectHtDivision, setCurrentProjectHtDivision] = React.useState<string>("") // 프로젝트 AVL의 H/T 구분 const constructionSector = selectedAvlRecord?.constructionSector || "" const shipType = selectedAvlRecord?.shipType || "" const avlKind = selectedAvlRecord?.avlKind || "" @@ -193,6 +194,11 @@ export function AvlRegistrationArea({ disabled = false }: AvlRegistrationAreaPro setCurrentProjectCode(projectCode) }, []) + // 프로젝트 H/T 구분 변경 핸들러 + const handleProjectHtDivisionChange = React.useCallback((htDivision: string) => { + setCurrentProjectHtDivision(htDivision) + }, []) + // 선택된 ID들을 가져오는 헬퍼 함수들 const getSelectedIds = React.useCallback((tableType: 'project' | 'standard' | 'vendor') => { // 각 테이블 컴포넌트에서 선택된 행들의 ID를 가져오는 로직 @@ -223,16 +229,24 @@ export function AvlRegistrationArea({ disabled = false }: AvlRegistrationAreaPro return } + if (!currentProjectHtDivision) { + toast.error("프로젝트 AVL의 H/T 구분이 설정되지 않았습니다.") + return + } + try { const result = await copyToProjectAvl( selectedIds, currentProjectCode, + currentProjectHtDivision, parseInt(avlListId) || 1, session?.user?.name || "unknown" ) if (result.success) { toast.success(result.message) + // 프로젝트 AVL 데이터 리로드 + setProjectAvlReloadTrigger(prev => prev + 1) // 선택 해제 dispatch({ type: 'CLEAR_SELECTION' }) } else { @@ -242,7 +256,7 @@ export function AvlRegistrationArea({ disabled = false }: AvlRegistrationAreaPro console.error('프로젝트AVL로 복사 실패:', error) toast.error("복사 중 오류가 발생했습니다.") } - }, [selectedTable, selectedRowCount, getSelectedIds, currentProjectCode, avlListId, session]) + }, [selectedTable, selectedRowCount, getSelectedIds, currentProjectCode, currentProjectHtDivision, avlListId, session]) const handleCopyToStandard = React.useCallback(async () => { if (selectedTable !== 'project' || selectedRowCount === 0) return @@ -327,10 +341,16 @@ export function AvlRegistrationArea({ disabled = false }: AvlRegistrationAreaPro return } + if (!currentProjectHtDivision) { + toast.error("프로젝트 AVL의 H/T 구분이 설정되지 않았습니다.") + return + } + try { const result = await copyFromVendorPoolToProjectAvl( selectedIds, currentProjectCode, + currentProjectHtDivision, parseInt(avlListId) || 1, session?.user?.name || "unknown" ) @@ -348,7 +368,7 @@ export function AvlRegistrationArea({ disabled = false }: AvlRegistrationAreaPro console.error('벤더풀 → 프로젝트AVL 복사 실패:', error) toast.error("복사 중 오류가 발생했습니다.") } - }, [selectedTable, selectedRowCount, getSelectedIds, currentProjectCode, avlListId, session]) + }, [selectedTable, selectedRowCount, getSelectedIds, currentProjectCode, currentProjectHtDivision, avlListId, session]) const handleCopyFromVendorToStandard = React.useCallback(async () => { if (selectedTable !== 'vendor' || selectedRowCount === 0) return @@ -444,6 +464,7 @@ export function AvlRegistrationArea({ disabled = false }: AvlRegistrationAreaPro projectCode={currentProjectCode} avlListId={parseInt(avlListId) || 1} onProjectCodeChange={handleProjectCodeChange} + onHtDivisionChange={handleProjectHtDivisionChange} reloadTrigger={projectAvlReloadTrigger} /> diff --git a/lib/avl/table/columns-detail.tsx b/lib/avl/table/columns-detail.tsx index b354dccd..f70f3c97 100644 --- a/lib/avl/table/columns-detail.tsx +++ b/lib/avl/table/columns-detail.tsx @@ -16,6 +16,9 @@ export const columns: ColumnDef<AvlDetailItem>[] = [ <DataTableColumnHeaderSimple column={column} title="No." /> ), size: 60, + meta: { + excelHeader: "No.", + }, }, { accessorKey: "equipBulkDivision", @@ -31,6 +34,9 @@ export const columns: ColumnDef<AvlDetailItem>[] = [ ) }, size: 120, + meta: { + excelHeader: "Equip/Bulk 구분", + }, }, { accessorKey: "disciplineName", @@ -42,6 +48,9 @@ export const columns: ColumnDef<AvlDetailItem>[] = [ return <span>{value || "-"}</span> }, size: 120, + meta: { + excelHeader: "설계공종", + }, }, { accessorKey: "materialNameCustomerSide", @@ -53,6 +62,9 @@ export const columns: ColumnDef<AvlDetailItem>[] = [ return <span>{value || "-"}</span> }, size: 150, + meta: { + excelHeader: "고객사 AVL 자재명", + }, }, { accessorKey: "packageName", @@ -64,6 +76,9 @@ export const columns: ColumnDef<AvlDetailItem>[] = [ return <span>{value || "-"}</span> }, size: 130, + meta: { + excelHeader: "패키지 정보", + }, }, { accessorKey: "materialGroupCode", @@ -75,6 +90,9 @@ export const columns: ColumnDef<AvlDetailItem>[] = [ return <span>{value || "-"}</span> }, size: 120, + meta: { + excelHeader: "자재그룹코드", + }, }, { accessorKey: "materialGroupName", @@ -86,6 +104,9 @@ export const columns: ColumnDef<AvlDetailItem>[] = [ return <span>{value || "-"}</span> }, size: 130, + meta: { + excelHeader: "자재그룹명", + }, }, { accessorKey: "vendorCode", @@ -97,6 +118,9 @@ export const columns: ColumnDef<AvlDetailItem>[] = [ return <span>{value || "-"}</span> }, size: 120, + meta: { + excelHeader: "협력업체코드", + }, }, { accessorKey: "vendorName", @@ -108,6 +132,9 @@ export const columns: ColumnDef<AvlDetailItem>[] = [ return <span className="font-medium">{value || "-"}</span> }, size: 140, + meta: { + excelHeader: "협력업체명", + }, }, { accessorKey: "avlVendorName", @@ -119,6 +146,9 @@ export const columns: ColumnDef<AvlDetailItem>[] = [ return <span>{value || "-"}</span> }, size: 140, + meta: { + excelHeader: "AVL 등재업체명", + }, }, { accessorKey: "tier", @@ -128,13 +158,13 @@ export const columns: ColumnDef<AvlDetailItem>[] = [ cell: ({ row }) => { const value = row.getValue("tier") as string if (!value) return <span>-</span> - + const tierColor = { "Tier 1": "bg-green-100 text-green-800", - "Tier 2": "bg-yellow-100 text-yellow-800", + "Tier 2": "bg-yellow-100 text-yellow-800", "Tier 3": "bg-red-100 text-red-800" }[value] || "bg-gray-100 text-gray-800" - + return ( <Badge className={tierColor}> {value} @@ -142,6 +172,9 @@ export const columns: ColumnDef<AvlDetailItem>[] = [ ) }, size: 100, + meta: { + excelHeader: "등급 (Tier)", + }, }, ], }, @@ -163,6 +196,9 @@ export const columns: ColumnDef<AvlDetailItem>[] = [ ) }, size: 80, + meta: { + excelHeader: "FA 대상", + }, }, { accessorKey: "faStatus", @@ -174,6 +210,9 @@ export const columns: ColumnDef<AvlDetailItem>[] = [ return <span>{value || "-"}</span> }, size: 100, + meta: { + excelHeader: "FA 현황", + }, }, ], }, @@ -195,6 +234,9 @@ export const columns: ColumnDef<AvlDetailItem>[] = [ ) }, size: 80, + meta: { + excelHeader: "AVL", + }, }, { accessorKey: "shiBlacklist", @@ -210,6 +252,9 @@ export const columns: ColumnDef<AvlDetailItem>[] = [ ) }, size: 100, + meta: { + excelHeader: "Blacklist", + }, }, { accessorKey: "shiBcc", @@ -225,6 +270,9 @@ export const columns: ColumnDef<AvlDetailItem>[] = [ ) }, size: 80, + meta: { + excelHeader: "BCC", + }, }, ], }, diff --git a/lib/avl/table/project-avl-table-columns.tsx b/lib/avl/table/project-avl-table-columns.tsx index c052e6f7..f74612ec 100644 --- a/lib/avl/table/project-avl-table-columns.tsx +++ b/lib/avl/table/project-avl-table-columns.tsx @@ -17,29 +17,13 @@ export const getProjectAvlColumns = (): ColumnDef<ProjectAvlItem>[] => [ aria-label="Select all" /> ), - cell: ({ row, table }) => { - // 프로젝트 AVL 테이블의 단일 선택 핸들러 - const handleRowSelection = (checked: boolean) => { - if (checked) { - // 다른 모든 행의 선택 해제 - table.getRowModel().rows.forEach(r => { - if (r !== row && r.getIsSelected()) { - r.toggleSelected(false) - } - }) - } - // 현재 행 선택/해제 - row.toggleSelected(checked) - } - - return ( - <Checkbox - checked={row.getIsSelected()} - onCheckedChange={handleRowSelection} - aria-label="Select row" - /> - ) - }, + cell: ({ row }) => ( + <Checkbox + checked={row.getIsSelected()} + onCheckedChange={(value) => row.toggleSelected(!!value)} + aria-label="Select row" + /> + ), enableSorting: false, enableHiding: false, size: 50, 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> */} diff --git a/lib/avl/table/standard-avl-table-columns.tsx b/lib/avl/table/standard-avl-table-columns.tsx index 903d2590..650220f5 100644 --- a/lib/avl/table/standard-avl-table-columns.tsx +++ b/lib/avl/table/standard-avl-table-columns.tsx @@ -16,29 +16,13 @@ export const standardAvlColumns: ColumnDef<StandardAvlItem>[] = [ aria-label="Select all" /> ), - cell: ({ row, table }) => { - // 선종별 표준 AVL 테이블의 단일 선택 핸들러 - const handleRowSelection = (checked: boolean) => { - if (checked) { - // 다른 모든 행의 선택 해제 - table.getRowModel().rows.forEach(r => { - if (r !== row && r.getIsSelected()) { - r.toggleSelected(false) - } - }) - } - // 현재 행 선택/해제 - row.toggleSelected(checked) - } - - return ( - <Checkbox - checked={row.getIsSelected()} - onCheckedChange={handleRowSelection} - aria-label="Select row" - /> - ) - }, + cell: ({ row }) => ( + <Checkbox + checked={row.getIsSelected()} + onCheckedChange={(value) => row.toggleSelected(!!value)} + aria-label="Select row" + /> + ), enableSorting: false, enableHiding: false, size: 50, diff --git a/lib/avl/table/standard-avl-table.tsx b/lib/avl/table/standard-avl-table.tsx index 06fa6931..c638bd7f 100644 --- a/lib/avl/table/standard-avl-table.tsx +++ b/lib/avl/table/standard-avl-table.tsx @@ -27,7 +27,7 @@ import { Search } from "lucide-react" import { toast } from "sonner" import { standardAvlColumns } from "./standard-avl-table-columns" import { AvlVendorAddAndModifyDialog } from "./avl-vendor-add-and-modify-dialog" -import { createAvlVendorInfo, updateAvlVendorInfo, deleteAvlVendorInfo, finalizeStandardAvl } from "../service" +import { createAvlVendorInfo, updateAvlVendorInfo, deleteAvlVendorInfo, finalizeStandardAvl, getStandardAvlVendorInfoCount } from "../service" import { AvlVendorInfoInput } from "../types" import { useSession } from "next-auth/react" import { ShipTypeSelector, ShipTypeItem } from "@/components/common/ship-type" @@ -341,6 +341,7 @@ export const StandardAvlTable = forwardRef<StandardAvlTableRef, StandardAvlTable // 최종 확정 다이얼로그 상태 const [isConfirmDialogOpen, setIsConfirmDialogOpen] = React.useState(false) + const [totalVendorInfoCount, setTotalVendorInfoCount] = React.useState<number>(0) // 최종 확정 핸들러 (표준 AVL) const handleFinalizeStandardAvl = React.useCallback(async () => { @@ -350,22 +351,31 @@ export const StandardAvlTable = forwardRef<StandardAvlTableRef, StandardAvlTable return } - if (data.length === 0) { + // 2. 실제 확정될 레코드 건수 조회 + const standardAvlInfo = { + constructionSector: searchConstructionSector, + shipType: selectedShipType?.CD || "", + avlKind: searchAvlKind, + htDivision: searchHtDivision + } + + const count = await getStandardAvlVendorInfoCount(standardAvlInfo) + + if (count === 0) { toast.error("확정할 표준 AVL 벤더 정보가 없습니다.") return } - // 2. 확인 다이얼로그 열기 + setTotalVendorInfoCount(count) + + // 3. 확인 다이얼로그 열기 setIsConfirmDialogOpen(true) - }, [isAllSearchConditionsSelected, data.length]) + }, [isAllSearchConditionsSelected, searchConstructionSector, selectedShipType, searchAvlKind, searchHtDivision]) // 실제 최종 확정 실행 함수 const executeFinalizeStandardAvl = React.useCallback(async () => { try { - // 3. 현재 데이터의 모든 ID 수집 (전체 레코드 기준) - const avlVendorInfoIds = data.map(item => item.id) - - // 4. 최종 확정 실행 + // 최종 확정 실행 (서버에서 DB의 모든 레코드를 조회하여 확정) const standardAvlInfo = { constructionSector: searchConstructionSector, shipType: selectedShipType?.CD || "", @@ -375,17 +385,16 @@ export const StandardAvlTable = forwardRef<StandardAvlTableRef, StandardAvlTable const result = await finalizeStandardAvl( standardAvlInfo, - avlVendorInfoIds, sessionData?.user?.name || "" ) if (result.success) { toast.success(result.message) - // 5. 데이터 새로고침 + // 데이터 새로고침 loadData({}) - // 6. 선택 해제 + // 선택 해제 table.toggleAllPageRowsSelected(false) } else { toast.error(result.message) @@ -396,7 +405,7 @@ export const StandardAvlTable = forwardRef<StandardAvlTableRef, StandardAvlTable } finally { setIsConfirmDialogOpen(false) } - }, [searchConstructionSector, selectedShipType, searchAvlKind, searchHtDivision, data, table, loadData, sessionData?.user?.name]) + }, [searchConstructionSector, selectedShipType, searchAvlKind, searchHtDivision, table, loadData, sessionData?.user?.name]) // 초기 데이터 로드 (검색 조건이 모두 입력되었을 때만) React.useEffect(() => { @@ -626,10 +635,9 @@ export const StandardAvlTable = forwardRef<StandardAvlTableRef, StandardAvlTable <div>• 선종: {selectedShipType?.CD || ""}</div> <div>• AVL종류: {searchAvlKind}</div> <div>• H/T 구분: {searchHtDivision}</div> - <div>• 벤더 정보: {data.length}개 (전체 레코드)</div> - {/* <div className="text-amber-600 font-medium mt-4"> - ⚠️ 확정 후 내용 수정을 필요로 하는 경우 동일 건을 다시 최종확정해 revision 처리로 수정해야 합니다. - </div> */} + <div className="font-semibold text-primary mt-4"> + • 확정될 벤더 정보: {totalVendorInfoCount}개 + </div> </div> <DialogFooter> <Button variant="outline" onClick={() => setIsConfirmDialogOpen(false)}> diff --git a/lib/avl/table/vendor-pool-table-columns.tsx b/lib/avl/table/vendor-pool-table-columns.tsx index 53db1059..25ebbfb8 100644 --- a/lib/avl/table/vendor-pool-table-columns.tsx +++ b/lib/avl/table/vendor-pool-table-columns.tsx @@ -16,29 +16,13 @@ export const vendorPoolColumns: ColumnDef<VendorPoolItem>[] = [ aria-label="Select all" /> ), - cell: ({ row, table }) => { - // Vendor Pool 테이블의 단일 선택 핸들러 - const handleRowSelection = (checked: boolean) => { - if (checked) { - // 다른 모든 행의 선택 해제 - table.getRowModel().rows.forEach(r => { - if (r !== row && r.getIsSelected()) { - r.toggleSelected(false) - } - }) - } - // 현재 행 선택/해제 - row.toggleSelected(checked) - } - - return ( - <Checkbox - checked={row.getIsSelected()} - onCheckedChange={handleRowSelection} - aria-label="Select row" - /> - ) - }, + cell: ({ row }) => ( + <Checkbox + checked={row.getIsSelected()} + onCheckedChange={(value) => row.toggleSelected(!!value)} + aria-label="Select row" + /> + ), enableSorting: false, enableHiding: false, size: 50, diff --git a/lib/avl/validations.ts b/lib/avl/validations.ts index 6f09cdfd..84c3dd1a 100644 --- a/lib/avl/validations.ts +++ b/lib/avl/validations.ts @@ -100,11 +100,12 @@ export const projectAvlSearchParamsCache = createSearchParamsCache({ { id: "no", desc: false }, ]), - // 필수 필터: 프로젝트 코드 + // 필수 필터: 프로젝트 코드 및 H/T 구분 projectCode: parseAsString.withDefault(""), + htDivision: parseAsStringEnum(["H", "T", ""]).withDefault(""), // 추가 필터들 - equipBulkDivision: parseAsStringEnum(["EQUIP", "BULK"]).withDefault(""), + equipBulkDivision: parseAsStringEnum(["EQUIP", "BULK", ""]).withDefault(""), disciplineCode: parseAsString.withDefault(""), disciplineName: parseAsString.withDefault(""), materialNameCustomerSide: parseAsString.withDefault(""), |
