diff options
Diffstat (limited to 'lib/avl/service.ts')
| -rw-r--r-- | lib/avl/service.ts | 1244 |
1 files changed, 1161 insertions, 83 deletions
diff --git a/lib/avl/service.ts b/lib/avl/service.ts index 6a873ac1..535a0169 100644 --- a/lib/avl/service.ts +++ b/lib/avl/service.ts @@ -2,18 +2,21 @@ import { GetAvlListSchema, GetAvlDetailSchema, GetProjectAvlSchema, GetStandardAvlSchema } from "./validations"; import { AvlListItem, AvlDetailItem, CreateAvlListInput, UpdateAvlListInput, ActionResult, AvlVendorInfoInput } from "./types"; -import type { NewAvlVendorInfo, AvlVendorInfo } from "@/db/schema/avl/avl"; +import type { NewAvlVendorInfo } from "@/db/schema/avl/avl"; +import type { NewVendorPool } from "@/db/schema/avl/vendor-pool"; import db from "@/db/db"; import { avlList, avlVendorInfo } from "@/db/schema/avl/avl"; -import { eq, and, or, ilike, count, desc, asc, sql } from "drizzle-orm"; +import { vendorPool } from "@/db/schema/avl/vendor-pool"; +import { eq, and, or, ilike, count, desc, asc, sql, inArray } from "drizzle-orm"; import { debugLog, debugError, debugSuccess, debugWarn } from "@/lib/debug-utils"; -import { revalidateTag, unstable_cache } from "next/cache"; +import { revalidateTag } from "next/cache"; +import { createVendorInfoSnapshot } from "./snapshot-utils"; /** * AVL 리스트 조회 * avl_list 테이블에서 실제 데이터를 조회합니다. */ -const _getAvlLists = async (input: GetAvlListSchema) => { +export const getAvlLists = async (input: GetAvlListSchema) => { try { const offset = (input.page - 1) * input.perPage; @@ -125,20 +128,11 @@ const _getAvlLists = async (input: GetAvlListSchema) => { } }; -// 캐시된 버전 export - 동일한 입력에 대해 캐시 사용 -export const getAvlLists = unstable_cache( - _getAvlLists, - ['avl-list'], - { - tags: ['avl-list'], - revalidate: 300, // 5분 캐시 - } -); /** * AVL 상세 정보 조회 (특정 AVL ID의 모든 vendor info) */ -const _getAvlDetail = async (input: GetAvlDetailSchema & { avlListId: number }) => { +export const getAvlDetail = async (input: GetAvlDetailSchema & { avlListId: number }) => { try { const offset = (input.page - 1) * input.perPage; @@ -326,15 +320,6 @@ const _getAvlDetail = async (input: GetAvlDetailSchema & { avlListId: number }) } }; -// 캐시된 버전 export -export const getAvlDetail = unstable_cache( - _getAvlDetail, - ['avl-detail'], - { - tags: ['avl-detail'], - revalidate: 300, // 5분 캐시 - } -); /** * AVL 리스트 상세 정보 조회 (단일) @@ -522,11 +507,17 @@ export async function createAvlList(data: CreateAvlListInput): Promise<AvlListIt avlKind: data.avlKind, htDivision: data.htDivision, rev: data.rev ?? 1, + vendorInfoSnapshot: data.vendorInfoSnapshot, // 스냅샷 데이터 추가 createdBy: data.createdBy || 'system', updatedBy: data.updatedBy || 'system', }; - debugLog('DB INSERT 시작', { table: 'avl_list', data: insertData }); + debugLog('DB INSERT 시작', { + table: 'avl_list', + data: insertData, + hasVendorSnapshot: !!insertData.vendorInfoSnapshot, + snapshotLength: insertData.vendorInfoSnapshot?.length + }); // 데이터베이스에 삽입 const result = await db @@ -539,7 +530,11 @@ export async function createAvlList(data: CreateAvlListInput): Promise<AvlListIt throw new Error("Failed to create AVL list"); } - debugSuccess('DB INSERT 완료', { table: 'avl_list', result: result[0] }); + debugSuccess('DB INSERT 완료', { + table: 'avl_list', + result: result[0], + savedSnapshotLength: result[0].vendorInfoSnapshot?.length + }); const createdItem = result[0]; @@ -555,6 +550,7 @@ export async function createAvlList(data: CreateAvlListInput): Promise<AvlListIt avlType: createdItem.avlKind || '', htDivision: createdItem.htDivision || '', rev: createdItem.rev || 1, + vendorInfoSnapshot: createdItem.vendorInfoSnapshot, // 스냅샷 데이터 포함 }; debugSuccess('AVL 리스트 생성 완료', { result: transformedData }); @@ -684,11 +680,15 @@ export async function createAvlVendorInfo(data: AvlVendorInfoInput): Promise<Avl try { debugLog('AVL Vendor Info 생성 시작', { inputData: data }); - const currentTimestamp = new Date(); - // UI 필드를 DB 필드로 변환 const insertData: NewAvlVendorInfo = { - avlListId: data.avlListId, + isTemplate: data.isTemplate ?? false, // AVL 타입 구분 + constructionSector: data.constructionSector || null, // 표준 AVL용 + shipType: data.shipType || null, // 표준 AVL용 + avlKind: data.avlKind || null, // 표준 AVL용 + htDivision: data.htDivision || null, // 표준 AVL용 + projectCode: data.projectCode || null, // 프로젝트 코드 저장 + avlListId: data.avlListId || null, // nullable - 나중에 프로젝트별로 묶어줄 때 설정 ownerSuggestion: data.ownerSuggestion ?? false, shiSuggestion: data.shiSuggestion ?? false, equipBulkDivision: data.equipBulkDivision === "EQUIP" ? "E" : "B", @@ -907,10 +907,172 @@ export async function deleteAvlVendorInfo(id: number): Promise<boolean> { } /** + * 프로젝트 AVL 최종 확정 + * 1. 주어진 프로젝트 정보로 avlList에 레코드를 생성한다. + * 2. 현재 avlVendorInfo 레코드들의 avlListId를 새로 생성된 AVL 리스트 ID로 업데이트한다. + */ +export async function finalizeProjectAvl( + projectCode: string, + projectInfo: { + projectName: string; + constructionSector: string; + 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 확인 + const existingAvlLists = await db + .select({ rev: avlList.rev }) + .from(avlList) + .where(and( + eq(avlList.projectCode, projectCode), + eq(avlList.isTemplate, false) + )) + .orderBy(desc(avlList.rev)) + .limit(1); + + const nextRevision = existingAvlLists.length > 0 ? (existingAvlLists[0].rev || 0) + 1 : 1; + + debugLog('AVL 리스트 revision 계산', { + projectCode, + existingRevision: existingAvlLists.length > 0 ? existingAvlLists[0].rev : 0, + nextRevision + }); + + // 2. AVL 리스트 생성을 위한 데이터 준비 + const createAvlListData: CreateAvlListInput = { + isTemplate: false, // 프로젝트 AVL이므로 false + constructionSector: projectInfo.constructionSector, + projectCode: projectCode, + shipType: projectInfo.shipType, + avlKind: "프로젝트 AVL", // 기본값으로 설정 + htDivision: projectInfo.htDivision, + rev: nextRevision, // 계산된 다음 리비전 + createdBy: currentUser || 'system', + updatedBy: currentUser || 'system', + }; + + debugLog('AVL 리스트 생성 데이터', { createAvlListData }); + + // 2. AVL Vendor Info 스냅샷 생성 (AVL 리스트 생성 전에 현재 상태 저장) + debugLog('AVL Vendor Info 스냅샷 생성 시작', { + vendorInfoIdsCount: avlVendorInfoIds.length, + vendorInfoIds: avlVendorInfoIds + }); + const vendorInfoSnapshot = await createVendorInfoSnapshot(avlVendorInfoIds); + debugSuccess('AVL Vendor Info 스냅샷 생성 완료', { + snapshotCount: vendorInfoSnapshot.length, + snapshotSize: JSON.stringify(vendorInfoSnapshot).length, + sampleSnapshot: vendorInfoSnapshot.slice(0, 1) // 첫 번째 항목만 로깅 + }); + + // 스냅샷을 AVL 리스트 생성 데이터에 추가 + createAvlListData.vendorInfoSnapshot = vendorInfoSnapshot; + debugLog('스냅샷이 createAvlListData에 추가됨', { + hasSnapshot: !!createAvlListData.vendorInfoSnapshot, + snapshotLength: createAvlListData.vendorInfoSnapshot?.length + }); + + // 3. AVL 리스트 생성 + const newAvlList = await createAvlList(createAvlListData); + + if (!newAvlList) { + throw new Error("AVL 리스트 생성에 실패했습니다."); + } + + debugSuccess('AVL 리스트 생성 완료', { avlListId: newAvlList.id }); + + // 3. avlVendorInfo 레코드들의 avlListId 업데이트 + if (avlVendorInfoIds.length > 0) { + debugLog('AVL Vendor Info 업데이트 시작', { + count: avlVendorInfoIds.length, + newAvlListId: newAvlList.id + }); + + const updateResults = await Promise.all( + avlVendorInfoIds.map(async (vendorInfoId) => { + try { + const result = await db + .update(avlVendorInfo) + .set({ + avlListId: newAvlList.id, + projectCode: projectCode, + updatedAt: new Date() + }) + .where(eq(avlVendorInfo.id, vendorInfoId)) + .returning({ id: avlVendorInfo.id }); + + return { id: vendorInfoId, success: true, result }; + } catch (error) { + debugError('AVL Vendor Info 업데이트 실패', { vendorInfoId, error }); + return { id: vendorInfoId, success: false, error }; + } + }) + ); + + // 업데이트 결과 검증 + const successCount = updateResults.filter(r => r.success).length; + const failCount = updateResults.filter(r => !r.success).length; + + debugLog('AVL Vendor Info 업데이트 결과', { + total: avlVendorInfoIds.length, + success: successCount, + failed: failCount + }); + + if (failCount > 0) { + debugWarn('일부 AVL Vendor Info 업데이트 실패', { + failedIds: updateResults.filter(r => !r.success).map(r => r.id) + }); + } + } + + // 4. 캐시 무효화 + revalidateTag('avl-list'); + revalidateTag('avl-vendor-info'); + + debugSuccess('프로젝트 AVL 최종 확정 완료', { + avlListId: newAvlList.id, + projectCode, + vendorInfoCount: avlVendorInfoIds.length + }); + + return { + success: true, + avlListId: newAvlList.id, + message: `프로젝트 AVL이 성공적으로 확정되었습니다. (AVL ID: ${newAvlList.id}, 벤더 정보: ${avlVendorInfoIds.length}개)` + }; + + } catch (err) { + debugError('프로젝트 AVL 최종 확정 실패', { + projectCode, + error: err + }); + + console.error("Error in finalizeProjectAvl:", err); + + return { + success: false, + message: err instanceof Error ? err.message : "프로젝트 AVL 최종 확정 중 오류가 발생했습니다." + }; + } +} + +/** * 프로젝트 AVL Vendor Info 조회 (프로젝트별, isTemplate=false) * avl_list와 avlVendorInfo를 JOIN하여 프로젝트별 AVL 데이터를 조회합니다. */ -const _getProjectAvlVendorInfo = async (input: GetProjectAvlSchema) => { +export const getProjectAvlVendorInfo = async (input: GetProjectAvlSchema) => { try { const offset = (input.page - 1) * input.perPage; @@ -920,11 +1082,11 @@ const _getProjectAvlVendorInfo = async (input: GetProjectAvlSchema) => { // 실제 쿼리는 아래에서 구성됨 // 검색 조건 구성 - const whereConditions: any[] = [eq(avlList.isTemplate, false)]; // 기본 조건 + const whereConditions: any[] = []; // 기본 조건 제거 - // 필수 필터: 프로젝트 코드 + // 필수 필터: 프로젝트 코드 (avlVendorInfo에서 직접 필터링) if (input.projectCode) { - whereConditions.push(ilike(avlList.projectCode, `%${input.projectCode}%`)); + whereConditions.push(ilike(avlVendorInfo.projectCode, `%${input.projectCode}%`)); } // 검색어 기반 필터링 @@ -1002,7 +1164,7 @@ const _getProjectAvlVendorInfo = async (input: GetProjectAvlSchema) => { const totalCount = await db .select({ count: count() }) .from(avlVendorInfo) - .innerJoin(avlList, eq(avlVendorInfo.avlListId, avlList.id)) + .leftJoin(avlList, eq(avlVendorInfo.avlListId, avlList.id)) .where(and(...whereConditions)); // 데이터 조회 - JOIN 결과에서 필요한 필드들을 명시적으로 선택 @@ -1010,6 +1172,7 @@ const _getProjectAvlVendorInfo = async (input: GetProjectAvlSchema) => { .select({ // avlVendorInfo의 모든 필드 id: avlVendorInfo.id, + projectCode: avlVendorInfo.projectCode, avlListId: avlVendorInfo.avlListId, ownerSuggestion: avlVendorInfo.ownerSuggestion, shiSuggestion: avlVendorInfo.shiSuggestion, @@ -1054,7 +1217,7 @@ const _getProjectAvlVendorInfo = async (input: GetProjectAvlSchema) => { updatedAt: avlVendorInfo.updatedAt, }) .from(avlVendorInfo) - .innerJoin(avlList, eq(avlVendorInfo.avlListId, avlList.id)) + .leftJoin(avlList, eq(avlVendorInfo.avlListId, avlList.id)) .where(and(...whereConditions)) .orderBy(...orderByConditions) .limit(input.perPage) @@ -1103,21 +1266,12 @@ const _getProjectAvlVendorInfo = async (input: GetProjectAvlSchema) => { } }; -// 캐시된 버전 export -export const getProjectAvlVendorInfo = unstable_cache( - _getProjectAvlVendorInfo, - ['project-avl-vendor-info'], - { - tags: ['project-avl-vendor-info'], - revalidate: 300, // 5분 캐시 - } -); /** * 표준 AVL Vendor Info 조회 (선종별 표준 AVL, isTemplate=true) * avl_list와 avlVendorInfo를 JOIN하여 표준 AVL 데이터를 조회합니다. */ -const _getStandardAvlVendorInfo = async (input: GetStandardAvlSchema) => { +export const getStandardAvlVendorInfo = async (input: GetStandardAvlSchema) => { try { const offset = (input.page - 1) * input.perPage; @@ -1127,20 +1281,20 @@ const _getStandardAvlVendorInfo = async (input: GetStandardAvlSchema) => { // 실제 쿼리는 아래에서 구성됨 // 검색 조건 구성 - const whereConditions: any[] = [eq(avlList.isTemplate, true)]; // 기본 조건 + const whereConditions: any[] = [eq(avlVendorInfo.isTemplate, true)]; // 기본 조건: 표준 AVL - // 필수 필터: 표준 AVL용 (공사부문, 선종, AVL종류, H/T) + // 필수 필터: 표준 AVL용 (공사부문, 선종, AVL종류, H/T) - avlVendorInfo에서 직접 필터링 if (input.constructionSector) { - whereConditions.push(ilike(avlList.constructionSector, `%${input.constructionSector}%`)); + whereConditions.push(ilike(avlVendorInfo.constructionSector, `%${input.constructionSector}%`)); } if (input.shipType) { - whereConditions.push(ilike(avlList.shipType, `%${input.shipType}%`)); + whereConditions.push(ilike(avlVendorInfo.shipType, `%${input.shipType}%`)); } if (input.avlKind) { - whereConditions.push(ilike(avlList.avlKind, `%${input.avlKind}%`)); + whereConditions.push(ilike(avlVendorInfo.avlKind, `%${input.avlKind}%`)); } if (input.htDivision) { - whereConditions.push(eq(avlList.htDivision, input.htDivision)); + whereConditions.push(eq(avlVendorInfo.htDivision, input.htDivision)); } // 검색어 기반 필터링 @@ -1218,14 +1372,59 @@ const _getStandardAvlVendorInfo = async (input: GetStandardAvlSchema) => { const totalCount = await db .select({ count: count() }) .from(avlVendorInfo) - .innerJoin(avlList, eq(avlVendorInfo.avlListId, avlList.id)) .where(and(...whereConditions)); - // 데이터 조회 + // 데이터 조회 - avlVendorInfo에서 직접 조회 const data = await db - .select() + .select({ + // avlVendorInfo의 모든 필드 + id: avlVendorInfo.id, + isTemplate: avlVendorInfo.isTemplate, + projectCode: avlVendorInfo.projectCode, + avlListId: avlVendorInfo.avlListId, + ownerSuggestion: avlVendorInfo.ownerSuggestion, + shiSuggestion: avlVendorInfo.shiSuggestion, + equipBulkDivision: avlVendorInfo.equipBulkDivision, + disciplineCode: avlVendorInfo.disciplineCode, + disciplineName: avlVendorInfo.disciplineName, + materialNameCustomerSide: avlVendorInfo.materialNameCustomerSide, + packageCode: avlVendorInfo.packageCode, + packageName: avlVendorInfo.packageName, + materialGroupCode: avlVendorInfo.materialGroupCode, + materialGroupName: avlVendorInfo.materialGroupName, + vendorId: avlVendorInfo.vendorId, + vendorName: avlVendorInfo.vendorName, + vendorCode: avlVendorInfo.vendorCode, + avlVendorName: avlVendorInfo.avlVendorName, + tier: avlVendorInfo.tier, + faTarget: avlVendorInfo.faTarget, + faStatus: avlVendorInfo.faStatus, + isAgent: avlVendorInfo.isAgent, + contractSignerId: avlVendorInfo.contractSignerId, + contractSignerName: avlVendorInfo.contractSignerName, + contractSignerCode: avlVendorInfo.contractSignerCode, + headquarterLocation: avlVendorInfo.headquarterLocation, + manufacturingLocation: avlVendorInfo.manufacturingLocation, + hasAvl: avlVendorInfo.hasAvl, + isBlacklist: avlVendorInfo.isBlacklist, + isBcc: avlVendorInfo.isBcc, + techQuoteNumber: avlVendorInfo.techQuoteNumber, + quoteCode: avlVendorInfo.quoteCode, + quoteVendorId: avlVendorInfo.quoteVendorId, + quoteVendorName: avlVendorInfo.quoteVendorName, + quoteVendorCode: avlVendorInfo.quoteVendorCode, + quoteCountry: avlVendorInfo.quoteCountry, + quoteTotalAmount: avlVendorInfo.quoteTotalAmount, + quoteReceivedDate: avlVendorInfo.quoteReceivedDate, + recentQuoteDate: avlVendorInfo.recentQuoteDate, + recentQuoteNumber: avlVendorInfo.recentQuoteNumber, + recentOrderDate: avlVendorInfo.recentOrderDate, + recentOrderNumber: avlVendorInfo.recentOrderNumber, + remark: avlVendorInfo.remark, + createdAt: avlVendorInfo.createdAt, + updatedAt: avlVendorInfo.updatedAt, + }) .from(avlVendorInfo) - .innerJoin(avlList, eq(avlVendorInfo.avlListId, avlList.id)) .where(and(...whereConditions)) .orderBy(...orderByConditions) .limit(input.perPage) @@ -1233,30 +1432,30 @@ const _getStandardAvlVendorInfo = async (input: GetStandardAvlSchema) => { // 데이터 변환 const transformedData: AvlDetailItem[] = data.map((item: any, index) => ({ - ...(item.avl_vendor_info || item), + ...item, no: offset + index + 1, selected: false, - createdAt: ((item.avl_vendor_info || item).createdAt as Date)?.toISOString().split('T')[0] || '', - updatedAt: ((item.avl_vendor_info || item).updatedAt as Date)?.toISOString().split('T')[0] || '', + createdAt: (item.createdAt as Date)?.toISOString().split('T')[0] || '', + updatedAt: (item.updatedAt as Date)?.toISOString().split('T')[0] || '', // UI 표시용 필드 변환 - equipBulkDivision: (item.avl_vendor_info || item).equipBulkDivision === "E" ? "EQUIP" : "BULK", - faTarget: (item.avl_vendor_info || item).faTarget ?? false, - faStatus: (item.avl_vendor_info || item).faStatus || '', - agentStatus: (item.avl_vendor_info || item).isAgent ? "예" : "아니오", - shiAvl: (item.avl_vendor_info || item).hasAvl ?? false, - shiBlacklist: (item.avl_vendor_info || item).isBlacklist ?? false, - shiBcc: (item.avl_vendor_info || item).isBcc ?? false, - salesQuoteNumber: (item.avl_vendor_info || item).techQuoteNumber || '', - quoteCode: (item.avl_vendor_info || item).quoteCode || '', - salesVendorInfo: (item.avl_vendor_info || item).quoteVendorName || '', - salesCountry: (item.avl_vendor_info || item).quoteCountry || '', - totalAmount: (item.avl_vendor_info || item).quoteTotalAmount ? (item.avl_vendor_info || item).quoteTotalAmount.toString() : '', - quoteReceivedDate: (item.avl_vendor_info || item).quoteReceivedDate || '', - recentQuoteDate: (item.avl_vendor_info || item).recentQuoteDate || '', - recentQuoteNumber: (item.avl_vendor_info || item).recentQuoteNumber || '', - recentOrderDate: (item.avl_vendor_info || item).recentOrderDate || '', - recentOrderNumber: (item.avl_vendor_info || item).recentOrderNumber || '', - remarks: (item.avl_vendor_info || item).remark || '', + equipBulkDivision: item.equipBulkDivision === "E" ? "EQUIP" : "BULK", + faTarget: item.faTarget ?? false, + faStatus: item.faStatus || '', + agentStatus: item.isAgent ? "예" : "아니오", + shiAvl: item.hasAvl ?? false, + shiBlacklist: item.isBlacklist ?? false, + shiBcc: item.isBcc ?? false, + salesQuoteNumber: item.techQuoteNumber || '', + quoteCode: item.quoteCode || '', + salesVendorInfo: item.quoteVendorName || '', + salesCountry: item.quoteCountry || '', + totalAmount: item.quoteTotalAmount ? item.quoteTotalAmount.toString() : '', + quoteReceivedDate: item.quoteReceivedDate || '', + recentQuoteDate: item.recentQuoteDate || '', + recentQuoteNumber: item.recentQuoteNumber || '', + recentOrderDate: item.recentOrderDate || '', + recentOrderNumber: item.recentOrderNumber || '', + remarks: item.remark || '', })); const pageCount = Math.ceil(totalCount[0].count / input.perPage); @@ -1274,12 +1473,891 @@ const _getStandardAvlVendorInfo = async (input: GetStandardAvlSchema) => { } }; -// 캐시된 버전 export -export const getStandardAvlVendorInfo = unstable_cache( - _getStandardAvlVendorInfo, - ['standard-avl-vendor-info'], - { - tags: ['standard-avl-vendor-info'], - revalidate: 300, // 5분 캐시 +/** + * 선종별표준AVL → 프로젝트AVL로 복사 + */ +export const copyToProjectAvl = async ( + selectedIds: number[], + targetProjectCode: string, + targetAvlListId: number, + userName: string +): Promise<ActionResult> => { + try { + debugLog('선종별표준AVL → 프로젝트AVL 복사 시작', { selectedIds, targetProjectCode, targetAvlListId }); + + if (!selectedIds.length) { + return { success: false, message: "복사할 항목을 선택해주세요." }; + } + + // 선택된 레코드들 조회 (선종별표준AVL에서) + const selectedRecords = await db + .select() + .from(avlVendorInfo) + .where( + and( + eq(avlVendorInfo.isTemplate, true), + inArray(avlVendorInfo.id, selectedIds) + ) + ); + + if (!selectedRecords.length) { + return { success: false, message: "선택된 항목을 찾을 수 없습니다." }; + } + + // 복사할 데이터 준비 + const recordsToInsert: NewAvlVendorInfo[] = selectedRecords.map(record => ({ + ...record, + id: undefined, // 새 ID 생성 + isTemplate: false, // 프로젝트 AVL로 변경 + projectCode: targetProjectCode, // 대상 프로젝트 코드 + avlListId: targetAvlListId, // 대상 AVL 리스트 ID + // 표준 AVL 필드들은 null로 설정 (프로젝트 AVL에서는 사용하지 않음) + constructionSector: null, + shipType: null, + avlKind: null, + htDivision: null, + createdAt: undefined, + updatedAt: undefined, + })); + + // 벌크 인서트 + await db.insert(avlVendorInfo).values(recordsToInsert); + + debugSuccess('선종별표준AVL → 프로젝트AVL 복사 완료', { + copiedCount: recordsToInsert.length, + targetProjectCode, + userName + }); + + // 캐시 무효화 + revalidateTag('avl-lists'); + revalidateTag('avl-vendor-info'); + + return { + success: true, + message: `${recordsToInsert.length}개의 항목이 프로젝트 AVL로 복사되었습니다.` + }; + + } catch (error) { + debugError('선종별표준AVL → 프로젝트AVL 복사 실패', { error, selectedIds, targetProjectCode }); + return { + success: false, + message: "프로젝트 AVL로 복사 중 오류가 발생했습니다." + }; } -); +}; + +/** + * 프로젝트AVL → 선종별표준AVL로 복사 + */ +export const copyToStandardAvl = async ( + selectedIds: number[], + targetStandardInfo: { + constructionSector: string; + shipType: string; + avlKind: string; + htDivision: string; + }, + userName: string +): Promise<ActionResult> => { + try { + debugLog('프로젝트AVL → 선종별표준AVL 복사 시작', { selectedIds, targetStandardInfo }); + + if (!selectedIds.length) { + return { success: false, message: "복사할 항목을 선택해주세요." }; + } + + // 선종별 표준 AVL 검색 조건 검증 + if (!targetStandardInfo.constructionSector?.trim() || + !targetStandardInfo.shipType?.trim() || + !targetStandardInfo.avlKind?.trim() || + !targetStandardInfo.htDivision?.trim()) { + return { success: false, message: "선종별 표준 AVL 검색 조건을 모두 입력해주세요." }; + } + + // 선택된 레코드들 조회 (프로젝트AVL에서) + const selectedRecords = await db + .select() + .from(avlVendorInfo) + .where( + and( + eq(avlVendorInfo.isTemplate, false), + inArray(avlVendorInfo.id, selectedIds) + ) + ); + + if (!selectedRecords.length) { + return { success: false, message: "선택된 항목을 찾을 수 없습니다." }; + } + + // 복사할 데이터 준비 + const recordsToInsert: NewAvlVendorInfo[] = selectedRecords.map(record => ({ + ...record, + id: undefined, // 새 ID 생성 + isTemplate: true, // 표준 AVL로 변경 + // 프로젝트 AVL 필드들은 null로 설정 + projectCode: null, + avlListId: null, + // 표준 AVL 필드들 설정 + constructionSector: targetStandardInfo.constructionSector, + shipType: targetStandardInfo.shipType, + avlKind: targetStandardInfo.avlKind, + htDivision: targetStandardInfo.htDivision, + createdAt: undefined, + updatedAt: undefined, + })); + + // 벌크 인서트 + await db.insert(avlVendorInfo).values(recordsToInsert); + + debugSuccess('프로젝트AVL → 선종별표준AVL 복사 완료', { + copiedCount: recordsToInsert.length, + targetStandardInfo, + userName + }); + + // 캐시 무효화 + revalidateTag('avl-lists'); + revalidateTag('avl-vendor-info'); + + return { + success: true, + message: `${recordsToInsert.length}개의 항목이 선종별표준AVL로 복사되었습니다.` + }; + + } catch (error) { + debugError('프로젝트AVL → 선종별표준AVL 복사 실패', { error, selectedIds, targetStandardInfo }); + return { + success: false, + message: "선종별표준AVL로 복사 중 오류가 발생했습니다." + }; + } +}; + +/** + * 프로젝트AVL → 벤더풀로 복사 + */ +export const copyToVendorPool = async ( + selectedIds: number[], + userName: string +): Promise<ActionResult> => { + try { + debugLog('프로젝트AVL → 벤더풀 복사 시작', { selectedIds }); + + if (!selectedIds.length) { + return { success: false, message: "복사할 항목을 선택해주세요." }; + } + + // 선택된 레코드들 조회 (프로젝트AVL에서) + const selectedRecords = await db + .select() + .from(avlVendorInfo) + .where( + and( + eq(avlVendorInfo.isTemplate, false), + inArray(avlVendorInfo.id, selectedIds) + ) + ); + + if (!selectedRecords.length) { + return { success: false, message: "선택된 항목을 찾을 수 없습니다." }; + } + + // 벤더풀 테이블로 복사할 데이터 준비 (필드 매핑) + const recordsToInsert: NewVendorPool[] = selectedRecords.map(record => ({ + // 기본 정보 (프로젝트 AVL에서 추출 또는 기본값 설정) + constructionSector: record.constructionSector || "조선", // 기본값 설정 + htDivision: record.htDivision || "H", // 기본값 설정 + + // 설계 정보 + designCategoryCode: "XX", // 기본값 (실제로는 적절한 값으로 매핑 필요) + designCategory: record.disciplineName || "기타", + equipBulkDivision: record.equipBulkDivision || "E", + + // 패키지 정보 + packageCode: record.packageCode, + packageName: record.packageName, + + // 자재그룹 정보 + materialGroupCode: record.materialGroupCode, + materialGroupName: record.materialGroupName, + + // 자재 관련 정보 (빈 값으로 설정) + smCode: null, + similarMaterialNamePurchase: null, + similarMaterialNameOther: null, + + // 협력업체 정보 + vendorCode: record.vendorCode, + vendorName: record.vendorName, + + // 사업 및 인증 정보 + taxId: null, // 벤더풀에서 별도 관리 + faTarget: record.faTarget, + faStatus: record.faStatus, + faRemark: null, + tier: record.tier, + isAgent: record.isAgent, + + // 계약 정보 + contractSignerCode: record.contractSignerCode, + contractSignerName: record.contractSignerName, + + // 위치 정보 + headquarterLocation: null, // 별도 관리 필요 + manufacturingLocation: null, // 별도 관리 필요 + + // AVL 관련 정보 + avlVendorName: record.avlVendorName, + similarVendorName: null, + hasAvl: record.hasAvl, + + // 상태 정보 + isBlacklist: record.isBlacklist, + isBcc: record.isBcc, + purchaseOpinion: null, + + // AVL 적용 선종 (기본값으로 설정 - 실제로는 로직 필요) + shipTypeCommon: true, // 공통으로 설정 + shipTypeAmax: false, + shipTypeSmax: false, + shipTypeVlcc: false, + shipTypeLngc: false, + shipTypeCont: false, + + // AVL 적용 선종(해양) - 기본값 + offshoreTypeCommon: false, + offshoreTypeFpso: false, + offshoreTypeFlng: false, + offshoreTypeFpu: false, + offshoreTypePlatform: false, + offshoreTypeWtiv: false, + offshoreTypeGom: false, + + // eVCP 미등록 정보 - 빈 값 + picName: null, + picEmail: null, + picPhone: null, + agentName: null, + agentEmail: null, + agentPhone: null, + + // 업체 실적 현황 + recentQuoteDate: record.recentQuoteDate, + recentQuoteNumber: record.recentQuoteNumber, + recentOrderDate: record.recentOrderDate, + recentOrderNumber: record.recentOrderNumber, + + // 업데이트 히스토리 + registrationDate: undefined, // 현재 시간으로 자동 설정 + registrant: userName, + lastModifiedDate: undefined, + lastModifier: userName, + })); + + // 입력 데이터에서 중복 제거 (메모리에서 처리) + const seen = new Set<string>(); + const uniqueRecords = recordsToInsert.filter(record => { + if (!record.vendorCode || !record.materialGroupCode) return true; // 필수 필드가 없는 경우는 추가 + const key = `${record.vendorCode}:${record.materialGroupCode}`; + if (seen.has(key)) return false; + seen.add(key); + return true; + }); + + // 중복 제거된 레코드 수 계산 + const duplicateCount = recordsToInsert.length - uniqueRecords.length; + + if (uniqueRecords.length === 0) { + return { success: false, message: "복사할 유효한 항목이 없습니다." }; + } + + // 벌크 인서트 + await db.insert(vendorPool).values(uniqueRecords); + + debugSuccess('프로젝트AVL → 벤더풀 복사 완료', { + copiedCount: uniqueRecords.length, + duplicateCount, + userName + }); + + // 캐시 무효화 + revalidateTag('vendor-pool'); + revalidateTag('vendor-pool-list'); + revalidateTag('vendor-pool-stats'); + + return { + success: true, + message: `${uniqueRecords.length}개의 항목이 벤더풀로 복사되었습니다.${duplicateCount > 0 ? ` (${duplicateCount}개 입력 데이터 중복 제외)` : ''}` + }; + + } catch (error) { + debugError('프로젝트AVL → 벤더풀 복사 실패', { error, selectedIds }); + return { + success: false, + message: "벤더풀로 복사 중 오류가 발생했습니다." + }; + } +}; + +/** + * 벤더풀 → 프로젝트AVL로 복사 + */ +export const copyFromVendorPoolToProjectAvl = async ( + selectedIds: number[], + targetProjectCode: string, + targetAvlListId: number, + userName: string +): Promise<ActionResult> => { + try { + debugLog('벤더풀 → 프로젝트AVL 복사 시작', { selectedIds, targetProjectCode, targetAvlListId }); + + if (!selectedIds.length) { + return { success: false, message: "복사할 항목을 선택해주세요." }; + } + + // 선택된 레코드들 조회 (벤더풀에서) + const selectedRecords = await db + .select() + .from(vendorPool) + .where( + inArray(vendorPool.id, selectedIds) + ); + + if (!selectedRecords.length) { + return { success: false, message: "선택된 항목을 찾을 수 없습니다." }; + } + + // 벤더풀 데이터를 AVL Vendor Info로 변환하여 복사 + const recordsToInsert: NewAvlVendorInfo[] = selectedRecords.map(record => ({ + // 프로젝트 AVL용 필드들 + projectCode: targetProjectCode, + avlListId: targetAvlListId, + isTemplate: false, + + // 벤더풀 데이터를 AVL Vendor Info로 매핑 + vendorId: null, // 벤더풀에서는 vendorId가 없을 수 있음 + vendorName: record.vendorName, + vendorCode: record.vendorCode, + + // 기본 정보 (벤더풀의 데이터 활용) + constructionSector: record.constructionSector, + htDivision: record.htDivision, + + // 자재그룹 정보 + materialGroupCode: record.materialGroupCode, + materialGroupName: record.materialGroupName, + + // 설계 정보 (벤더풀의 데이터 활용) + designCategory: record.designCategory, + equipBulkDivision: record.equipBulkDivision, + + // 패키지 정보 + packageCode: record.packageCode, + packageName: record.packageName, + + // 계약 정보 + contractSignerCode: record.contractSignerCode, + contractSignerName: record.contractSignerName, + + // 위치 정보 + headquarterLocation: record.headquarterLocation, + manufacturingLocation: record.manufacturingLocation, + + // AVL 관련 정보 + avlVendorName: record.avlVendorName, + hasAvl: record.hasAvl, + + // 상태 정보 + isBlacklist: record.isBlacklist, + isBcc: record.isBcc, + + // 기본값들 + ownerSuggestion: false, + shiSuggestion: false, + faTarget: record.faTarget, + faStatus: record.faStatus, + isAgent: record.isAgent, + + // 나머지 필드들은 null 또는 기본값 + disciplineCode: null, + disciplineName: null, + materialNameCustomerSide: null, + tier: record.tier, + filters: [], + joinOperator: "and", + search: "", + + createdAt: undefined, + updatedAt: undefined, + })); + + // 벌크 인서트 + await db.insert(avlVendorInfo).values(recordsToInsert); + + debugSuccess('벤더풀 → 프로젝트AVL 복사 완료', { + copiedCount: recordsToInsert.length, + targetProjectCode, + userName + }); + + // 캐시 무효화 + revalidateTag('avl-lists'); + revalidateTag('avl-vendor-info'); + + return { + success: true, + message: `${recordsToInsert.length}개의 항목이 프로젝트 AVL로 복사되었습니다.` + }; + + } catch (error) { + debugError('벤더풀 → 프로젝트AVL 복사 실패', { error, selectedIds, targetProjectCode }); + return { + success: false, + message: "프로젝트 AVL로 복사 중 오류가 발생했습니다." + }; + } +}; + +/** + * 벤더풀 → 선종별표준AVL로 복사 + */ +export const copyFromVendorPoolToStandardAvl = async ( + selectedIds: number[], + targetStandardInfo: { + constructionSector: string; + shipType: string; + avlKind: string; + htDivision: string; + }, + userName: string +): Promise<ActionResult> => { + try { + debugLog('벤더풀 → 선종별표준AVL 복사 시작', { selectedIds, targetStandardInfo }); + + if (!selectedIds.length) { + return { success: false, message: "복사할 항목을 선택해주세요." }; + } + + // 선택된 레코드들 조회 (벤더풀에서) + const selectedRecords = await db + .select() + .from(vendorPool) + .where( + inArray(vendorPool.id, selectedIds) + ); + + if (!selectedRecords.length) { + return { success: false, message: "선택된 항목을 찾을 수 없습니다." }; + } + + // 벤더풀 데이터를 AVL Vendor Info로 변환하여 복사 + const recordsToInsert: NewAvlVendorInfo[] = selectedRecords.map(record => ({ + // 선종별 표준 AVL용 필드들 + isTemplate: true, + constructionSector: targetStandardInfo.constructionSector, + shipType: targetStandardInfo.shipType, + avlKind: targetStandardInfo.avlKind, + htDivision: targetStandardInfo.htDivision, + + // 벤더풀 데이터를 AVL Vendor Info로 매핑 + vendorId: null, + vendorName: record.vendorName, + vendorCode: record.vendorCode, + + // 자재그룹 정보 + materialGroupCode: record.materialGroupCode, + materialGroupName: record.materialGroupName, + + // 설계 정보 + disciplineName: record.designCategory, + equipBulkDivision: record.equipBulkDivision, + + // 패키지 정보 + packageCode: record.packageCode, + packageName: record.packageName, + + // 계약 정보 + contractSignerCode: record.contractSignerCode, + contractSignerName: record.contractSignerName, + + // 위치 정보 + headquarterLocation: record.headquarterLocation, + manufacturingLocation: record.manufacturingLocation, + + // AVL 관련 정보 + avlVendorName: record.avlVendorName, + hasAvl: record.hasAvl, + + // 상태 정보 + isBlacklist: record.isBlacklist, + isBcc: record.isBcc, + + // 기본값들 + ownerSuggestion: false, + shiSuggestion: false, + faTarget: record.faTarget, + faStatus: record.faStatus, + isAgent: record.isAgent, + + // 선종별 표준 AVL에서는 사용하지 않는 필드들 + projectCode: null, + avlListId: null, + + // 나머지 필드들은 null 또는 기본값 + disciplineCode: null, + materialNameCustomerSide: null, + tier: record.tier, + filters: [], + joinOperator: "and", + search: "", + + createdAt: undefined, + updatedAt: undefined, + })); + + // 벌크 인서트 + await db.insert(avlVendorInfo).values(recordsToInsert); + + debugSuccess('벤더풀 → 선종별표준AVL 복사 완료', { + copiedCount: recordsToInsert.length, + targetStandardInfo, + userName + }); + + // 캐시 무효화 + revalidateTag('avl-lists'); + revalidateTag('avl-vendor-info'); + + return { + success: true, + message: `${recordsToInsert.length}개의 항목이 선종별표준AVL로 복사되었습니다.` + }; + + } catch (error) { + debugError('벤더풀 → 선종별표준AVL 복사 실패', { error, selectedIds, targetStandardInfo }); + return { + success: false, + message: "선종별표준AVL로 복사 중 오류가 발생했습니다." + }; + } +}; + +/** + * 선종별표준AVL → 벤더풀로 복사 + */ +export const copyFromStandardAvlToVendorPool = async ( + selectedIds: number[], + userName: string +): Promise<ActionResult> => { + try { + debugLog('선종별표준AVL → 벤더풀 복사 시작', { selectedIds }); + + if (!selectedIds.length) { + return { success: false, message: "복사할 항목을 선택해주세요." }; + } + + // 선택된 레코드들 조회 (선종별표준AVL에서) + const selectedRecords = await db + .select() + .from(avlVendorInfo) + .where( + and( + eq(avlVendorInfo.isTemplate, true), + inArray(avlVendorInfo.id, selectedIds) + ) + ); + + if (!selectedRecords.length) { + return { success: false, message: "선택된 항목을 찾을 수 없습니다." }; + } + + // AVL Vendor Info 데이터를 벤더풀로 변환하여 복사 + const recordsToInsert: NewVendorPool[] = selectedRecords.map(record => ({ + // 기본 정보 + constructionSector: record.constructionSector || "조선", + htDivision: record.htDivision || "H", + + // 설계 정보 + designCategoryCode: "XX", // 기본값 + designCategory: record.disciplineName || "기타", + equipBulkDivision: record.equipBulkDivision || "E", + + // 패키지 정보 + packageCode: record.packageCode, + packageName: record.packageName, + + // 자재그룹 정보 + materialGroupCode: record.materialGroupCode, + materialGroupName: record.materialGroupName, + + // 협력업체 정보 + vendorCode: record.vendorCode, + vendorName: record.vendorName, + + // 사업 및 인증 정보 + taxId: null, + faTarget: record.faTarget, + faStatus: record.faStatus, + faRemark: null, + tier: record.tier, + isAgent: record.isAgent, + + // 계약 정보 + contractSignerCode: record.contractSignerCode, + contractSignerName: record.contractSignerName, + + // 위치 정보 + headquarterLocation: record.headquarterLocation, + manufacturingLocation: record.manufacturingLocation, + + // AVL 관련 정보 + avlVendorName: record.avlVendorName, + similarVendorName: null, + hasAvl: record.hasAvl, + + // 상태 정보 + isBlacklist: record.isBlacklist, + isBcc: record.isBcc, + purchaseOpinion: null, + + // AVL 적용 선종 (기본값) + shipTypeCommon: true, + shipTypeAmax: false, + shipTypeSmax: false, + shipTypeVlcc: false, + shipTypeLngc: false, + shipTypeCont: false, + + // AVL 적용 선종(해양) - 기본값 + offshoreTypeCommon: false, + offshoreTypeFpso: false, + offshoreTypeFlng: false, + offshoreTypeFpu: false, + offshoreTypePlatform: false, + offshoreTypeWtiv: false, + offshoreTypeGom: false, + + // eVCP 미등록 정보 + picName: null, + picEmail: null, + picPhone: null, + agentName: null, + agentEmail: null, + agentPhone: null, + + // 업체 실적 현황 + recentQuoteDate: record.recentQuoteDate, + recentQuoteNumber: record.recentQuoteNumber, + recentOrderDate: record.recentOrderDate, + recentOrderNumber: record.recentOrderNumber, + + // 업데이트 히스토리 + registrationDate: undefined, + registrant: userName, + lastModifiedDate: undefined, + lastModifier: userName, + })); + + // 중복 체크를 위한 고유한 vendorCode + materialGroupCode 조합 생성 + const uniquePairs = new Set<string>(); + const validRecords = recordsToInsert.filter(record => { + if (!record.vendorCode || !record.materialGroupCode) return false; + const key = `${record.vendorCode}:${record.materialGroupCode}`; + if (uniquePairs.has(key)) return false; + uniquePairs.add(key); + return true; + }); + + if (validRecords.length === 0) { + return { success: false, message: "복사할 유효한 항목이 없습니다." }; + } + + // 벌크 인서트 + await db.insert(vendorPool).values(validRecords); + + const duplicateCount = recordsToInsert.length - validRecords.length; + + debugSuccess('선종별표준AVL → 벤더풀 복사 완료', { + copiedCount: validRecords.length, + duplicateCount, + userName + }); + + // 캐시 무효화 + revalidateTag('vendor-pool'); + + return { + success: true, + message: `${validRecords.length}개의 항목이 벤더풀로 복사되었습니다.${duplicateCount > 0 ? ` (${duplicateCount}개 입력 데이터 중복 제외)` : ''}` + }; + + } catch (error) { + debugError('선종별표준AVL → 벤더풀 복사 실패', { error, selectedIds }); + return { + success: false, + message: "벤더풀로 복사 중 오류가 발생했습니다." + }; + } +}; + +/** + * 표준 AVL 최종 확정 + * 표준 AVL을 최종 확정하여 AVL 리스트에 등록합니다. + */ +export async function finalizeStandardAvl( + standardAvlInfo: { + constructionSector: string; + shipType: string; + 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 확인 + const existingAvlLists = await db + .select({ rev: avlList.rev }) + .from(avlList) + .where(and( + eq(avlList.constructionSector, standardAvlInfo.constructionSector), + eq(avlList.shipType, standardAvlInfo.shipType), + eq(avlList.avlKind, standardAvlInfo.avlKind), + eq(avlList.htDivision, standardAvlInfo.htDivision), + eq(avlList.isTemplate, true) + )) + .orderBy(desc(avlList.rev)) + .limit(1); + + const nextRevision = existingAvlLists.length > 0 ? (existingAvlLists[0].rev || 0) + 1 : 1; + + debugLog('표준 AVL 리스트 revision 계산', { + standardAvlInfo, + existingRevision: existingAvlLists.length > 0 ? existingAvlLists[0].rev : 0, + nextRevision + }); + + // 2. AVL 리스트 생성을 위한 데이터 준비 + const createAvlListData: CreateAvlListInput = { + isTemplate: true, // 표준 AVL이므로 true + constructionSector: standardAvlInfo.constructionSector, + projectCode: null, // 표준 AVL은 프로젝트 코드가 없음 + shipType: standardAvlInfo.shipType, + avlKind: standardAvlInfo.avlKind, + htDivision: standardAvlInfo.htDivision, + rev: nextRevision, // 계산된 다음 리비전 + createdBy: currentUser || 'system', + updatedBy: currentUser || 'system', + }; + + debugLog('표준 AVL 리스트 생성 데이터', { createAvlListData }); + + // 2-1. AVL Vendor Info 스냅샷 생성 (AVL 리스트 생성 전에 현재 상태 저장) + debugLog('표준 AVL Vendor Info 스냅샷 생성 시작', { + vendorInfoIdsCount: avlVendorInfoIds.length, + vendorInfoIds: avlVendorInfoIds + }); + const vendorInfoSnapshot = await createVendorInfoSnapshot(avlVendorInfoIds); + debugSuccess('표준 AVL Vendor Info 스냅샷 생성 완료', { + snapshotCount: vendorInfoSnapshot.length, + snapshotSize: JSON.stringify(vendorInfoSnapshot).length, + sampleSnapshot: vendorInfoSnapshot.slice(0, 1) // 첫 번째 항목만 로깅 + }); + + // 스냅샷을 AVL 리스트 생성 데이터에 추가 + createAvlListData.vendorInfoSnapshot = vendorInfoSnapshot; + debugLog('표준 AVL 스냅샷이 createAvlListData에 추가됨', { + hasSnapshot: !!createAvlListData.vendorInfoSnapshot, + snapshotLength: createAvlListData.vendorInfoSnapshot?.length + }); + + // 3. AVL 리스트 생성 + const newAvlList = await createAvlList(createAvlListData); + + if (!newAvlList) { + throw new Error("표준 AVL 리스트 생성에 실패했습니다."); + } + + debugSuccess('표준 AVL 리스트 생성 완료', { avlListId: newAvlList.id }); + + // 4. avlVendorInfo 레코드들의 avlListId 업데이트 + if (avlVendorInfoIds.length > 0) { + debugLog('표준 AVL Vendor Info 업데이트 시작', { + count: avlVendorInfoIds.length, + newAvlListId: newAvlList.id + }); + + const updateResults = await Promise.all( + avlVendorInfoIds.map(async (vendorInfoId) => { + try { + const result = await db + .update(avlVendorInfo) + .set({ + avlListId: newAvlList.id, + updatedAt: new Date() + }) + .where(eq(avlVendorInfo.id, vendorInfoId)) + .returning({ id: avlVendorInfo.id }); + + return { id: vendorInfoId, success: true, result }; + } catch (error) { + debugError('표준 AVL Vendor Info 업데이트 실패', { vendorInfoId, error }); + return { id: vendorInfoId, success: false, error }; + } + }) + ); + + // 업데이트 결과 검증 + const successCount = updateResults.filter(r => r.success).length; + const failCount = updateResults.filter(r => !r.success).length; + + debugLog('표준 AVL Vendor Info 업데이트 결과', { + total: avlVendorInfoIds.length, + success: successCount, + failed: failCount + }); + + if (failCount > 0) { + debugWarn('일부 표준 AVL Vendor Info 업데이트 실패', { + failedIds: updateResults.filter(r => !r.success).map(r => r.id) + }); + } + } + + // 5. 캐시 무효화 + revalidateTag('avl-list'); + revalidateTag('avl-vendor-info'); + + debugSuccess('표준 AVL 최종 확정 완료', { + avlListId: newAvlList.id, + standardAvlInfo, + vendorInfoCount: avlVendorInfoIds.length + }); + + return { + success: true, + avlListId: newAvlList.id, + message: `표준 AVL이 성공적으로 확정되었습니다. (AVL ID: ${newAvlList.id}, 벤더 정보: ${avlVendorInfoIds.length}개)` + }; + + } catch (err) { + debugError('표준 AVL 최종 확정 실패', { + standardAvlInfo, + error: err + }); + + console.error("Error in finalizeStandardAvl:", err); + + return { + success: false, + message: err instanceof Error ? err.message : "표준 AVL 최종 확정 중 오류가 발생했습니다." + }; + } +} |
