summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/avl/service.ts167
-rw-r--r--lib/avl/table/avl-detail-table.tsx64
-rw-r--r--lib/avl/table/avl-registration-area.tsx25
-rw-r--r--lib/avl/table/columns-detail.tsx54
-rw-r--r--lib/avl/table/project-avl-table-columns.tsx30
-rw-r--r--lib/avl/table/project-avl-table.tsx173
-rw-r--r--lib/avl/table/standard-avl-table-columns.tsx30
-rw-r--r--lib/avl/table/standard-avl-table.tsx40
-rw-r--r--lib/avl/table/vendor-pool-table-columns.tsx30
-rw-r--r--lib/avl/validations.ts5
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(""),