"use server"; import { GetAvlListSchema, GetAvlDetailSchema, GetProjectAvlSchema, GetStandardAvlSchema } from "./validations"; import { AvlListItem, AvlDetailItem, CreateAvlListInput, UpdateAvlListInput, ActionResult, AvlVendorInfoInput } from "./types"; 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, avlListSummaryView } from "@/db/schema/avl/avl"; 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 } from "next/cache"; import { createVendorInfoSnapshot } from "./snapshot-utils"; /** * AVL 리스트 조회 * avlListSummaryView에서 최신 revision별로 집계된 데이터를 조회합니다. */ export const getAvlLists = async (input: GetAvlListSchema) => { try { const offset = (input.page - 1) * input.perPage; debugLog('AVL 리스트 조회 시작', { input, offset }); // 검색 조건 구성 const whereConditions: any[] = []; // 검색어 기반 필터링 if (input.search) { const searchTerm = `%${input.search}%`; whereConditions.push( or( ilike(avlListSummaryView.constructionSector, searchTerm), ilike(avlListSummaryView.projectCode, searchTerm), ilike(avlListSummaryView.shipType, searchTerm), ilike(avlListSummaryView.avlKind, searchTerm) ) ); } // 필터 조건 추가 if (input.isTemplate === "true") { whereConditions.push(eq(avlListSummaryView.isTemplate, true)); } else if (input.isTemplate === "false") { whereConditions.push(eq(avlListSummaryView.isTemplate, false)); } if (input.constructionSector) { whereConditions.push(ilike(avlListSummaryView.constructionSector, `%${input.constructionSector}%`)); } if (input.projectCode) { whereConditions.push(ilike(avlListSummaryView.projectCode, `%${input.projectCode}%`)); } if (input.shipType) { whereConditions.push(ilike(avlListSummaryView.shipType, `%${input.shipType}%`)); } if (input.avlKind) { whereConditions.push(ilike(avlListSummaryView.avlKind, `%${input.avlKind}%`)); } if (input.htDivision) { whereConditions.push(eq(avlListSummaryView.htDivision, input.htDivision)); } if (input.rev) { whereConditions.push(eq(avlListSummaryView.rev, parseInt(input.rev))); } // 정렬 조건 구성 const orderByConditions: any[] = []; input.sort.forEach((sortItem) => { const column = sortItem.id as keyof typeof avlListSummaryView; if (column && avlListSummaryView[column]) { if (sortItem.desc) { orderByConditions.push(sql`${avlListSummaryView[column]} desc`); } else { orderByConditions.push(sql`${avlListSummaryView[column]} asc`); } } }); // 기본 정렬 (등재일 내림차순) if (orderByConditions.length === 0) { orderByConditions.push(desc(avlListSummaryView.createdAt)); } // 총 개수 조회 const totalCount = await db .select({ count: count() }) .from(avlListSummaryView) .where(and(...whereConditions)); // 데이터 조회 const data = await db .select() .from(avlListSummaryView) .where(and(...whereConditions)) .orderBy(...orderByConditions) .limit(input.perPage) .offset(offset); // 데이터 변환 (timestamp -> string) const transformedData: AvlListItem[] = data.map((item, index) => ({ // 기본 필드들 id: item.id!, isTemplate: item.isTemplate || false, constructionSector: item.constructionSector || '', projectCode: item.projectCode || null, shipType: item.shipType || '', avlKind: item.avlKind || '', htDivision: item.htDivision || '', rev: item.rev || 1, vendorInfoSnapshot: undefined, // 뷰에서는 제공하지 않음 createdBy: item.createdBy || null, updatedBy: item.updatedBy || null, // UI 전용 필드들 no: offset + index + 1, selected: false, createdAt: ((item as any).createdAt as Date)?.toISOString().split('T')[0] || '', updatedAt: ((item as any).updatedAt as Date)?.toISOString().split('T')[0] || '', // 기존 필드 매핑 projectInfo: item.projectCode || '', avlType: item.avlKind || '', // 뷰에서 제공하는 집계 필드들 추가 PKG: item.pkgCount || 0, materialGroup: item.materialGroupCount || 0, vendor: item.vendorCount || 0, Tier: item.tierCount || 0, ownerSuggestion: item.ownerSuggestionCount || 0, shiSuggestion: item.shiSuggestionCount || 0, })); const pageCount = Math.ceil(totalCount[0].count / input.perPage); debugSuccess('AVL 리스트 조회 완료', { recordCount: transformedData.length, pageCount }); return { data: transformedData, pageCount }; } catch (err) { debugError('AVL 리스트 조회 실패', { error: err, input }); console.error("Error in getAvlLists:", err); return { data: [], pageCount: 0 }; } }; /** * AVL 상세 정보 조회 (특정 AVL ID의 모든 vendor info) */ export const getAvlDetail = async (input: GetAvlDetailSchema & { avlListId: number }) => { try { const offset = (input.page - 1) * input.perPage; debugLog('AVL 상세 조회 시작', { input, offset }); // 검색 조건 구성 const whereConditions: any[] = []; // AVL 리스트 ID 필터 (필수) whereConditions.push(eq(avlVendorInfo.avlListId, input.avlListId)); // 검색어 기반 필터링 if (input.search) { const searchTerm = `%${input.search}%`; whereConditions.push( or( ilike(avlVendorInfo.disciplineName, searchTerm), ilike(avlVendorInfo.materialNameCustomerSide, searchTerm), ilike(avlVendorInfo.vendorName, searchTerm), ilike(avlVendorInfo.avlVendorName, searchTerm) ) ); } // 필터 조건 추가 if (input.equipBulkDivision) { whereConditions.push(eq(avlVendorInfo.equipBulkDivision, input.equipBulkDivision === "EQUIP" ? "E" : "B")); } if (input.disciplineCode) { whereConditions.push(ilike(avlVendorInfo.disciplineCode, `%${input.disciplineCode}%`)); } if (input.disciplineName) { whereConditions.push(ilike(avlVendorInfo.disciplineName, `%${input.disciplineName}%`)); } if (input.materialNameCustomerSide) { whereConditions.push(ilike(avlVendorInfo.materialNameCustomerSide, `%${input.materialNameCustomerSide}%`)); } if (input.packageCode) { whereConditions.push(ilike(avlVendorInfo.packageCode, `%${input.packageCode}%`)); } if (input.packageName) { whereConditions.push(ilike(avlVendorInfo.packageName, `%${input.packageName}%`)); } if (input.materialGroupCode) { whereConditions.push(ilike(avlVendorInfo.materialGroupCode, `%${input.materialGroupCode}%`)); } if (input.materialGroupName) { whereConditions.push(ilike(avlVendorInfo.materialGroupName, `%${input.materialGroupName}%`)); } if (input.vendorCode) { whereConditions.push(ilike(avlVendorInfo.vendorCode, `%${input.vendorCode}%`)); } if (input.vendorName) { whereConditions.push(ilike(avlVendorInfo.vendorName, `%${input.vendorName}%`)); } if (input.avlVendorName) { whereConditions.push(ilike(avlVendorInfo.avlVendorName, `%${input.avlVendorName}%`)); } if (input.tier) { whereConditions.push(ilike(avlVendorInfo.tier, `%${input.tier}%`)); } if (input.faTarget === "true") { whereConditions.push(eq(avlVendorInfo.faTarget, true)); } else if (input.faTarget === "false") { whereConditions.push(eq(avlVendorInfo.faTarget, false)); } if (input.faStatus) { whereConditions.push(ilike(avlVendorInfo.faStatus, `%${input.faStatus}%`)); } if (input.isAgent === "true") { whereConditions.push(eq(avlVendorInfo.isAgent, true)); } else if (input.isAgent === "false") { whereConditions.push(eq(avlVendorInfo.isAgent, false)); } if (input.contractSignerName) { whereConditions.push(ilike(avlVendorInfo.contractSignerName, `%${input.contractSignerName}%`)); } if (input.headquarterLocation) { whereConditions.push(ilike(avlVendorInfo.headquarterLocation, `%${input.headquarterLocation}%`)); } if (input.manufacturingLocation) { whereConditions.push(ilike(avlVendorInfo.manufacturingLocation, `%${input.manufacturingLocation}%`)); } if (input.hasAvl === "true") { whereConditions.push(eq(avlVendorInfo.hasAvl, true)); } else if (input.hasAvl === "false") { whereConditions.push(eq(avlVendorInfo.hasAvl, false)); } if (input.isBlacklist === "true") { whereConditions.push(eq(avlVendorInfo.isBlacklist, true)); } else if (input.isBlacklist === "false") { whereConditions.push(eq(avlVendorInfo.isBlacklist, false)); } if (input.isBcc === "true") { whereConditions.push(eq(avlVendorInfo.isBcc, true)); } else if (input.isBcc === "false") { whereConditions.push(eq(avlVendorInfo.isBcc, false)); } if (input.techQuoteNumber) { whereConditions.push(ilike(avlVendorInfo.techQuoteNumber, `%${input.techQuoteNumber}%`)); } if (input.quoteCode) { whereConditions.push(ilike(avlVendorInfo.quoteCode, `%${input.quoteCode}%`)); } if (input.quoteCountry) { whereConditions.push(ilike(avlVendorInfo.quoteCountry, `%${input.quoteCountry}%`)); } if (input.remark) { whereConditions.push(ilike(avlVendorInfo.remark, `%${input.remark}%`)); } // 정렬 조건 구성 const orderByConditions: any[] = []; input.sort.forEach((sortItem) => { const column = sortItem.id as keyof typeof avlVendorInfo; if (column && avlVendorInfo[column]) { if (sortItem.desc) { orderByConditions.push(sql`${avlVendorInfo[column]} desc`); } else { orderByConditions.push(sql`${avlVendorInfo[column]} asc`); } } }); // 기본 정렬 if (orderByConditions.length === 0) { orderByConditions.push(asc(avlVendorInfo.id)); } // 총 개수 조회 const totalCount = await db .select({ count: count() }) .from(avlVendorInfo) .where(and(...whereConditions)); // 데이터 조회 const data = await db .select() .from(avlVendorInfo) .where(and(...whereConditions)) .orderBy(...orderByConditions) .limit(input.perPage) .offset(offset); // 데이터 변환 (timestamp -> string, DB 필드 -> UI 필드) const transformedData: AvlDetailItem[] = data.map((item, index) => ({ ...(item as any), no: offset + index + 1, selected: false, createdAt: ((item as any).createdAt as Date)?.toISOString().split('T')[0] || '', updatedAt: ((item as any).updatedAt as Date)?.toISOString().split('T')[0] || '', // UI 표시용 필드 변환 equipBulkDivision: item.equipBulkDivision === "E" ? "EQUIP" : "BULK", faTarget: item.faTarget ?? false, 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); debugSuccess('AVL 상세 조회 완료', { recordCount: transformedData.length, pageCount }); return { data: transformedData, pageCount }; } catch (err) { debugError('AVL 상세 조회 실패', { error: err, input }); console.error("Error in getAvlDetail:", err); return { data: [], pageCount: 0 }; } }; /** * AVL 리스트 상세 정보 조회 (단일) */ export async function getAvlListById(id: number): Promise { try { const data = await db .select() .from(avlList) .where(eq(avlList.id, id)) .limit(1); if (data.length === 0) { return null; } const item = data[0]; // 데이터 변환 const transformedData: AvlListItem = { ...item, no: 1, selected: false, createdAt: ((item as any).createdAt as Date)?.toISOString().split('T')[0] || '', updatedAt: ((item as any).updatedAt as Date)?.toISOString().split('T')[0] || '', projectInfo: item.projectCode || '', shipType: item.shipType || '', avlType: item.avlKind || '', htDivision: item.htDivision || '', rev: item.rev || 1, }; return transformedData; } catch (err) { console.error("Error in getAvlListById:", err); return null; } } /** * AVL Vendor Info 상세 정보 조회 (단일) */ export async function getAvlVendorInfoById(id: number): Promise { try { const data = await db .select() .from(avlVendorInfo) .where(eq(avlVendorInfo.id, id)) .limit(1); if (data.length === 0) { return null; } const item = data[0]; // 데이터 변환 const transformedData: AvlDetailItem = { ...(item as any), no: 1, selected: false, createdAt: ((item as any).createdAt as Date)?.toISOString().split('T')[0] || '', updatedAt: ((item as any).updatedAt as Date)?.toISOString().split('T')[0] || '', equipBulkDivision: item.equipBulkDivision === "E" ? "EQUIP" : "BULK", faTarget: item.faTarget ?? false, 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 || '', }; return transformedData; } catch (err) { console.error("Error in getAvlVendorInfoById:", err); return null; } } /** * AVL 액션 처리 * 신규등록, 일괄입력, 저장 등의 액션을 처리 */ export async function handleAvlAction( action: string, data?: any ): Promise { try { switch (action) { case "new-registration": return { success: true, message: "신규 AVL 등록 모드" }; case "standard-registration": return { success: true, message: "표준 AVL 등재 모드" }; case "project-registration": return { success: true, message: "프로젝트 AVL 등재 모드" }; case "bulk-import": if (!data?.file) { return { success: false, message: "업로드할 파일이 없습니다." }; } console.log("일괄 입력 처리:", data.file); return { success: true, message: "일괄 입력 처리가 시작되었습니다." }; case "save": console.log("변경사항 저장:", data); return { success: true, message: "변경사항이 저장되었습니다." }; case "edit": if (!data?.id) { return { success: false, message: "수정할 항목 ID가 없습니다." }; } return { success: true, message: "수정 모달이 열렸습니다.", data: { id: data.id } }; case "delete": if (!data?.id) { return { success: false, message: "삭제할 항목 ID가 없습니다." }; } // 실제 삭제 처리 const deleteResult = await deleteAvlList(data.id); if (deleteResult) { return { success: true, message: "항목이 삭제되었습니다.", data: { id: data.id } }; } else { return { success: false, message: "항목 삭제에 실패했습니다." }; } case "view-detail": if (!data?.id) { return { success: false, message: "조회할 항목 ID가 없습니다." }; } return { success: true, message: "상세 정보가 조회되었습니다.", data: { id: data.id } }; default: return { success: false, message: `알 수 없는 액션입니다: ${action}` }; } } catch (err) { console.error("Error in handleAvlAction:", err); return { success: false, message: "액션 처리 중 오류가 발생했습니다." }; } } // 클라이언트에서 호출할 수 있는 서버 액션 래퍼들 export async function createAvlListAction(data: CreateAvlListInput): Promise { return await createAvlList(data); } export async function updateAvlListAction(id: number, data: UpdateAvlListInput): Promise { return await updateAvlList(id, data); } export async function deleteAvlListAction(id: number): Promise { return await deleteAvlList(id); } export async function handleAvlActionAction(action: string, data?: any): Promise { return await handleAvlAction(action, data); } /** * AVL 리스트 생성 */ export async function createAvlList(data: CreateAvlListInput): Promise { try { debugLog('AVL 리스트 생성 시작', { inputData: data }); const currentTimestamp = new Date(); // 데이터베이스에 삽입할 데이터 준비 const insertData = { isTemplate: data.isTemplate ?? false, constructionSector: data.constructionSector, projectCode: data.projectCode, shipType: data.shipType, 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, hasVendorSnapshot: !!insertData.vendorInfoSnapshot, snapshotLength: insertData.vendorInfoSnapshot?.length }); // 데이터베이스에 삽입 const result = await db .insert(avlList) .values(insertData) .returning(); if (result.length === 0) { debugError('DB 삽입 실패: 결과가 없음', { insertData }); throw new Error("Failed to create AVL list"); } debugSuccess('DB INSERT 완료', { table: 'avl_list', result: result[0], savedSnapshotLength: result[0].vendorInfoSnapshot?.length }); const createdItem = result[0]; // 생성된 데이터를 AvlListItem 타입으로 변환 const transformedData: AvlListItem = { ...createdItem, no: 1, selected: false, createdAt: createdItem.createdAt ? createdItem.createdAt.toISOString().split('T')[0] : '', updatedAt: createdItem.updatedAt ? createdItem.updatedAt.toISOString().split('T')[0] : '', projectInfo: createdItem.projectCode || '', shipType: createdItem.shipType || '', avlType: createdItem.avlKind || '', htDivision: createdItem.htDivision || '', rev: createdItem.rev || 1, vendorInfoSnapshot: createdItem.vendorInfoSnapshot, // 스냅샷 데이터 포함 }; debugSuccess('AVL 리스트 생성 완료', { result: transformedData }); // 캐시 무효화 revalidateTag('avl-list'); debugSuccess('AVL 캐시 무효화 완료', { tags: ['avl-list'] }); return transformedData; } catch (err) { debugError('AVL 리스트 생성 실패', { error: err, inputData: data }); console.error("Error in createAvlList:", err); return null; } } /** * AVL 리스트 업데이트 */ export async function updateAvlList(id: number, data: UpdateAvlListInput): Promise { try { debugLog('AVL 리스트 업데이트 시작', { id, updateData: data }); const currentTimestamp = new Date(); // 업데이트할 데이터 준비 const updateData: any = {}; if (data.isTemplate !== undefined) updateData.isTemplate = data.isTemplate; if (data.constructionSector !== undefined) updateData.constructionSector = data.constructionSector; if (data.projectCode !== undefined) updateData.projectCode = data.projectCode; if (data.shipType !== undefined) updateData.shipType = data.shipType; if (data.avlKind !== undefined) updateData.avlKind = data.avlKind; if (data.htDivision !== undefined) updateData.htDivision = data.htDivision; if (data.rev !== undefined) updateData.rev = data.rev; if (data.createdBy !== undefined) updateData.createdBy = data.createdBy; if (data.updatedBy !== undefined) updateData.updatedBy = data.updatedBy; updateData.updatedAt = currentTimestamp; // 업데이트할 데이터가 없는 경우 if (Object.keys(updateData).length <= 1) { return await getAvlListById(id); } // 데이터베이스 업데이트 const result = await db .update(avlList) .set(updateData) .where(eq(avlList.id, id)) .returning(); if (result.length === 0) { throw new Error("AVL list not found or update failed"); } const updatedItem = result[0]; // 업데이트된 데이터를 AvlListItem 타입으로 변환 const transformedData: AvlListItem = { ...updatedItem, no: 1, selected: false, createdAt: updatedItem.createdAt ? updatedItem.createdAt.toISOString().split('T')[0] : '', updatedAt: updatedItem.updatedAt ? updatedItem.updatedAt.toISOString().split('T')[0] : '', projectInfo: updatedItem.projectCode || '', shipType: updatedItem.shipType || '', avlType: updatedItem.avlKind || '', htDivision: updatedItem.htDivision || '', rev: updatedItem.rev || 1, }; debugSuccess('AVL 리스트 업데이트 완료', { id, result: transformedData }); // 캐시 무효화 revalidateTag('avl-list'); return transformedData; } catch (err) { debugError('AVL 리스트 업데이트 실패', { error: err, id, updateData: data }); console.error("Error in updateAvlList:", err); return null; } } /** * AVL 리스트 삭제 */ export async function deleteAvlList(id: number): Promise { try { debugLog('AVL 리스트 삭제 시작', { id }); // 데이터베이스에서 삭제 const result = await db .delete(avlList) .where(eq(avlList.id, id)); // 삭제 확인을 위한 재조회 const checkDeleted = await db .select({ id: avlList.id }) .from(avlList) .where(eq(avlList.id, id)) .limit(1); const isDeleted = checkDeleted.length === 0; if (isDeleted) { debugSuccess('AVL 리스트 삭제 완료', { id }); revalidateTag('avl-list'); } else { debugWarn('AVL 리스트 삭제 실패: 항목이 존재함', { id }); } return isDeleted; } catch (err) { debugError('AVL 리스트 삭제 실패', { error: err, id }); console.error("Error in deleteAvlList:", err); return false; } } /** * AVL Vendor Info 생성 */ export async function createAvlVendorInfo(data: AvlVendorInfoInput): Promise { try { debugLog('AVL Vendor Info 생성 시작', { inputData: data }); // UI 필드를 DB 필드로 변환 const insertData: NewAvlVendorInfo = { 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", disciplineCode: data.disciplineCode || null, disciplineName: data.disciplineName, materialNameCustomerSide: data.materialNameCustomerSide, packageCode: data.packageCode || null, packageName: data.packageName || null, materialGroupCode: data.materialGroupCode || null, materialGroupName: data.materialGroupName || null, vendorId: data.vendorId || null, vendorName: data.vendorName || null, vendorCode: data.vendorCode || null, avlVendorName: data.avlVendorName || null, tier: data.tier || null, faTarget: data.faTarget ?? false, faStatus: data.faStatus || null, isAgent: data.isAgent ?? false, contractSignerId: data.contractSignerId || null, contractSignerName: data.contractSignerName || null, contractSignerCode: data.contractSignerCode || null, headquarterLocation: data.headquarterLocation || null, manufacturingLocation: data.manufacturingLocation || null, hasAvl: data.shiAvl ?? false, isBlacklist: data.shiBlacklist ?? false, isBcc: data.shiBcc ?? false, techQuoteNumber: data.salesQuoteNumber || null, quoteCode: data.quoteCode || null, quoteVendorId: data.quoteVendorId || null, quoteVendorName: data.salesVendorInfo || null, quoteVendorCode: data.quoteVendorCode || null, quoteCountry: data.salesCountry || null, quoteTotalAmount: data.totalAmount ? data.totalAmount.replace(/,/g, '') as any : null, quoteReceivedDate: data.quoteReceivedDate || null, recentQuoteDate: data.recentQuoteDate || null, recentQuoteNumber: data.recentQuoteNumber || null, recentOrderDate: data.recentOrderDate || null, recentOrderNumber: data.recentOrderNumber || null, remark: data.remarks || null, }; debugLog('DB INSERT 시작', { table: 'avl_vendor_info', data: insertData }); // 데이터베이스에 삽입 const result = await db .insert(avlVendorInfo) .values(insertData as any) .returning(); if (result.length === 0) { debugError('DB 삽입 실패: 결과가 없음', { insertData }); throw new Error("Failed to create AVL vendor info"); } debugSuccess('DB INSERT 완료', { table: 'avl_vendor_info', result: result[0] }); const createdItem = result[0]; // 생성된 데이터를 AvlDetailItem 타입으로 변환 const transformedData: AvlDetailItem = { ...(createdItem as any), no: 1, selected: false, createdAt: createdItem.createdAt ? createdItem.createdAt.toISOString().split('T')[0] : '', updatedAt: createdItem.updatedAt ? createdItem.updatedAt.toISOString().split('T')[0] : '', equipBulkDivision: createdItem.equipBulkDivision === "E" ? "EQUIP" : "BULK", faTarget: createdItem.faTarget ?? false, agentStatus: createdItem.isAgent ? "예" : "아니오", shiAvl: createdItem.hasAvl ?? false, shiBlacklist: createdItem.isBlacklist ?? false, shiBcc: createdItem.isBcc ?? false, salesQuoteNumber: createdItem.techQuoteNumber || '', quoteCode: createdItem.quoteCode || '', salesVendorInfo: createdItem.quoteVendorName || '', salesCountry: createdItem.quoteCountry || '', totalAmount: createdItem.quoteTotalAmount ? createdItem.quoteTotalAmount.toString() : '', quoteReceivedDate: createdItem.quoteReceivedDate || '', recentQuoteDate: createdItem.recentQuoteDate || '', recentQuoteNumber: createdItem.recentQuoteNumber || '', recentOrderDate: createdItem.recentOrderDate || '', recentOrderNumber: createdItem.recentOrderNumber || '', remarks: createdItem.remark || '', }; debugSuccess('AVL Vendor Info 생성 완료', { result: transformedData }); // 캐시 무효화 revalidateTag('avl-detail'); return transformedData; } catch (err) { debugError('AVL Vendor Info 생성 실패', { error: err, inputData: data }); console.error("Error in createAvlVendorInfo:", err); return null; } } /** * AVL Vendor Info 업데이트 */ export async function updateAvlVendorInfo(id: number, data: Partial): Promise { try { debugLog('AVL Vendor Info 업데이트 시작', { id, data }); // 간단한 필드 매핑 const updateData: any = { updatedAt: new Date() }; // ownerSuggestion과 shiSuggestion 추가 if (data.ownerSuggestion !== undefined) updateData.ownerSuggestion = data.ownerSuggestion; if (data.shiSuggestion !== undefined) updateData.shiSuggestion = data.shiSuggestion; if (data.equipBulkDivision !== undefined) updateData.equipBulkDivision = data.equipBulkDivision === "EQUIP" ? "E" : "B"; if (data.disciplineCode !== undefined) updateData.disciplineCode = data.disciplineCode; if (data.disciplineName !== undefined) updateData.disciplineName = data.disciplineName; if (data.materialNameCustomerSide !== undefined) updateData.materialNameCustomerSide = data.materialNameCustomerSide; if (data.packageCode !== undefined) updateData.packageCode = data.packageCode; if (data.packageName !== undefined) updateData.packageName = data.packageName; if (data.materialGroupCode !== undefined) updateData.materialGroupCode = data.materialGroupCode; if (data.materialGroupName !== undefined) updateData.materialGroupName = data.materialGroupName; if (data.vendorId !== undefined) updateData.vendorId = data.vendorId; if (data.vendorName !== undefined) updateData.vendorName = data.vendorName; if (data.vendorCode !== undefined) updateData.vendorCode = data.vendorCode; if (data.avlVendorName !== undefined) updateData.avlVendorName = data.avlVendorName; if (data.tier !== undefined) updateData.tier = data.tier; if (data.faTarget !== undefined) updateData.faTarget = data.faTarget; if (data.faStatus !== undefined) updateData.faStatus = data.faStatus; if (data.isAgent !== undefined) updateData.isAgent = data.isAgent; if (data.contractSignerId !== undefined) updateData.contractSignerId = data.contractSignerId; if (data.contractSignerName !== undefined) updateData.contractSignerName = data.contractSignerName; if (data.contractSignerCode !== undefined) updateData.contractSignerCode = data.contractSignerCode; if (data.headquarterLocation !== undefined) updateData.headquarterLocation = data.headquarterLocation; if (data.manufacturingLocation !== undefined) updateData.manufacturingLocation = data.manufacturingLocation; if (data.shiAvl !== undefined) updateData.hasAvl = data.shiAvl; if (data.shiBlacklist !== undefined) updateData.isBlacklist = data.shiBlacklist; if (data.shiBcc !== undefined) updateData.isBcc = data.shiBcc; if (data.salesQuoteNumber !== undefined) updateData.techQuoteNumber = data.salesQuoteNumber; if (data.quoteCode !== undefined) updateData.quoteCode = data.quoteCode; if (data.quoteVendorId !== undefined) updateData.quoteVendorId = data.quoteVendorId; if (data.quoteVendorCode !== undefined) updateData.quoteVendorCode = data.quoteVendorCode; if (data.salesCountry !== undefined) updateData.quoteCountry = data.salesCountry; if (data.quoteReceivedDate !== undefined) updateData.quoteReceivedDate = data.quoteReceivedDate; if (data.recentQuoteDate !== undefined) updateData.recentQuoteDate = data.recentQuoteDate; if (data.recentQuoteNumber !== undefined) updateData.recentQuoteNumber = data.recentQuoteNumber; if (data.recentOrderDate !== undefined) updateData.recentOrderDate = data.recentOrderDate; if (data.recentOrderNumber !== undefined) updateData.recentOrderNumber = data.recentOrderNumber; if (data.remarks !== undefined) updateData.remark = data.remarks; // 숫자 변환 if (data.totalAmount !== undefined) { updateData.quoteTotalAmount = data.totalAmount ? parseFloat(data.totalAmount.replace(/,/g, '')) || null : null; } // 문자열 필드 if (data.salesVendorInfo !== undefined) updateData.quoteVendorName = data.salesVendorInfo; debugLog('업데이트할 데이터', { updateData }); // 데이터베이스 업데이트 const result = await db .update(avlVendorInfo) .set(updateData) .where(eq(avlVendorInfo.id, id)) .returning(); if (result.length === 0) { throw new Error("AVL vendor info not found"); } debugSuccess('AVL Vendor Info 업데이트 성공', { id }); revalidateTag('avl-detail'); // 업데이트된 데이터 조회해서 반환 return await getAvlVendorInfoById(id); } catch (err) { debugError('AVL Vendor Info 업데이트 실패', { id, error: err }); console.error("Error in updateAvlVendorInfo:", err); return null; } } /** * AVL Vendor Info 삭제 */ export async function deleteAvlVendorInfo(id: number): Promise { try { debugLog('AVL Vendor Info 삭제 시작', { id }); // 데이터베이스에서 삭제 const result = await db .delete(avlVendorInfo) .where(eq(avlVendorInfo.id, id)); // 삭제 확인을 위한 재조회 const checkDeleted = await db .select({ id: avlVendorInfo.id }) .from(avlVendorInfo) .where(eq(avlVendorInfo.id, id)) .limit(1); const isDeleted = checkDeleted.length === 0; if (isDeleted) { debugSuccess('AVL Vendor Info 삭제 완료', { id }); revalidateTag('avl-detail'); } else { debugWarn('AVL Vendor Info 삭제 실패: 항목이 존재함', { id }); } return isDeleted; } catch (err) { debugError('AVL Vendor Info 삭제 실패', { error: err, id }); console.error("Error in deleteAvlVendorInfo:", err); return false; } } /** * 프로젝트 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 데이터를 조회합니다. */ export const getProjectAvlVendorInfo = async (input: GetProjectAvlSchema) => { try { const offset = (input.page - 1) * input.perPage; debugLog('프로젝트 AVL Vendor Info 조회 시작', { input, offset }); // 기본 JOIN 쿼리 구성 (프로젝트 AVL이므로 isTemplate=false) // 실제 쿼리는 아래에서 구성됨 // 검색 조건 구성 const whereConditions: any[] = []; // 기본 조건 제거 // 필수 필터: 프로젝트 코드 (avlVendorInfo에서 직접 필터링) if (input.projectCode) { whereConditions.push(ilike(avlVendorInfo.projectCode, `%${input.projectCode}%`)); } // 검색어 기반 필터링 if (input.search) { const searchTerm = `%${input.search}%`; whereConditions.push( or( ilike(avlVendorInfo.disciplineName, searchTerm), ilike(avlVendorInfo.materialNameCustomerSide, searchTerm), ilike(avlVendorInfo.vendorName, searchTerm), ilike(avlVendorInfo.avlVendorName, searchTerm), ilike(avlVendorInfo.packageName, searchTerm), ilike(avlVendorInfo.materialGroupName, searchTerm) ) ); } // 추가 필터 조건들 if (input.equipBulkDivision) { whereConditions.push(eq(avlVendorInfo.equipBulkDivision, input.equipBulkDivision === "EQUIP" ? "E" : "B")); } if (input.disciplineCode) { whereConditions.push(ilike(avlVendorInfo.disciplineCode, `%${input.disciplineCode}%`)); } if (input.disciplineName) { whereConditions.push(ilike(avlVendorInfo.disciplineName, `%${input.disciplineName}%`)); } if (input.materialNameCustomerSide) { whereConditions.push(ilike(avlVendorInfo.materialNameCustomerSide, `%${input.materialNameCustomerSide}%`)); } if (input.packageCode) { whereConditions.push(ilike(avlVendorInfo.packageCode, `%${input.packageCode}%`)); } if (input.packageName) { whereConditions.push(ilike(avlVendorInfo.packageName, `%${input.packageName}%`)); } if (input.materialGroupCode) { whereConditions.push(ilike(avlVendorInfo.materialGroupCode, `%${input.materialGroupCode}%`)); } if (input.materialGroupName) { whereConditions.push(ilike(avlVendorInfo.materialGroupName, `%${input.materialGroupName}%`)); } if (input.vendorName) { whereConditions.push(ilike(avlVendorInfo.vendorName, `%${input.vendorName}%`)); } if (input.vendorCode) { whereConditions.push(ilike(avlVendorInfo.vendorCode, `%${input.vendorCode}%`)); } if (input.avlVendorName) { whereConditions.push(ilike(avlVendorInfo.avlVendorName, `%${input.avlVendorName}%`)); } if (input.tier) { whereConditions.push(ilike(avlVendorInfo.tier, `%${input.tier}%`)); } // 정렬 조건 구성 const orderByConditions: any[] = []; input.sort.forEach((sortItem) => { const column = sortItem.id as keyof typeof avlVendorInfo; if (column && avlVendorInfo[column]) { if (sortItem.desc) { orderByConditions.push(sql`${avlVendorInfo[column]} desc`); } else { orderByConditions.push(sql`${avlVendorInfo[column]} asc`); } } }); // 기본 정렬 if (orderByConditions.length === 0) { orderByConditions.push(asc(avlVendorInfo.id)); } // 총 개수 조회 const totalCount = await db .select({ count: count() }) .from(avlVendorInfo) .leftJoin(avlList, eq(avlVendorInfo.avlListId, avlList.id)) .where(and(...whereConditions)); // 데이터 조회 - JOIN 결과에서 필요한 필드들을 명시적으로 선택 const data = await db .select({ // avlVendorInfo의 모든 필드 id: avlVendorInfo.id, 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) .leftJoin(avlList, eq(avlVendorInfo.avlListId, avlList.id)) .where(and(...whereConditions)) .orderBy(...orderByConditions) .limit(input.perPage) .offset(offset); // 데이터 변환 const transformedData: AvlDetailItem[] = data.map((item: any, index) => ({ ...item, no: offset + index + 1, selected: false, createdAt: (item.createdAt as Date)?.toISOString().split('T')[0] || '', updatedAt: (item.updatedAt as Date)?.toISOString().split('T')[0] || '', // UI 표시용 필드 변환 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); debugSuccess('프로젝트 AVL Vendor Info 조회 완료', { recordCount: transformedData.length, pageCount }); return { data: transformedData, pageCount }; } catch (err) { debugError('프로젝트 AVL Vendor Info 조회 실패', { error: err, input }); console.error("Error in getProjectAvlVendorInfo:", err); return { data: [], pageCount: 0 }; } }; /** * 표준 AVL Vendor Info 조회 (선종별 표준 AVL, isTemplate=true) * avl_list와 avlVendorInfo를 JOIN하여 표준 AVL 데이터를 조회합니다. */ export const getStandardAvlVendorInfo = async (input: GetStandardAvlSchema) => { try { const offset = (input.page - 1) * input.perPage; debugLog('표준 AVL Vendor Info 조회 시작', { input, offset }); // 기본 JOIN 쿼리 구성 (표준 AVL이므로 isTemplate=true) // 실제 쿼리는 아래에서 구성됨 // 검색 조건 구성 const whereConditions: any[] = [eq(avlVendorInfo.isTemplate, true)]; // 기본 조건: 표준 AVL // 필수 필터: 표준 AVL용 (공사부문, 선종, AVL종류, H/T) - avlVendorInfo에서 직접 필터링 if (input.constructionSector) { whereConditions.push(ilike(avlVendorInfo.constructionSector, `%${input.constructionSector}%`)); } if (input.shipType) { whereConditions.push(ilike(avlVendorInfo.shipType, `%${input.shipType}%`)); } if (input.avlKind) { whereConditions.push(ilike(avlVendorInfo.avlKind, `%${input.avlKind}%`)); } if (input.htDivision) { whereConditions.push(eq(avlVendorInfo.htDivision, input.htDivision)); } // 검색어 기반 필터링 if (input.search) { const searchTerm = `%${input.search}%`; whereConditions.push( or( ilike(avlVendorInfo.disciplineName, searchTerm), ilike(avlVendorInfo.materialNameCustomerSide, searchTerm), ilike(avlVendorInfo.vendorName, searchTerm), ilike(avlVendorInfo.avlVendorName, searchTerm), ilike(avlVendorInfo.packageName, searchTerm), ilike(avlVendorInfo.materialGroupName, searchTerm) ) ); } // 추가 필터 조건들 if (input.equipBulkDivision) { whereConditions.push(eq(avlVendorInfo.equipBulkDivision, input.equipBulkDivision === "EQUIP" ? "E" : "B")); } if (input.disciplineCode) { whereConditions.push(ilike(avlVendorInfo.disciplineCode, `%${input.disciplineCode}%`)); } if (input.disciplineName) { whereConditions.push(ilike(avlVendorInfo.disciplineName, `%${input.disciplineName}%`)); } if (input.materialNameCustomerSide) { whereConditions.push(ilike(avlVendorInfo.materialNameCustomerSide, `%${input.materialNameCustomerSide}%`)); } if (input.packageCode) { whereConditions.push(ilike(avlVendorInfo.packageCode, `%${input.packageCode}%`)); } if (input.packageName) { whereConditions.push(ilike(avlVendorInfo.packageName, `%${input.packageName}%`)); } if (input.materialGroupCode) { whereConditions.push(ilike(avlVendorInfo.materialGroupCode, `%${input.materialGroupCode}%`)); } if (input.materialGroupName) { whereConditions.push(ilike(avlVendorInfo.materialGroupName, `%${input.materialGroupName}%`)); } if (input.vendorName) { whereConditions.push(ilike(avlVendorInfo.vendorName, `%${input.vendorName}%`)); } if (input.vendorCode) { whereConditions.push(ilike(avlVendorInfo.vendorCode, `%${input.vendorCode}%`)); } if (input.avlVendorName) { whereConditions.push(ilike(avlVendorInfo.avlVendorName, `%${input.avlVendorName}%`)); } if (input.tier) { whereConditions.push(ilike(avlVendorInfo.tier, `%${input.tier}%`)); } // 정렬 조건 구성 const orderByConditions: any[] = []; input.sort.forEach((sortItem) => { const column = sortItem.id as keyof typeof avlVendorInfo; if (column && avlVendorInfo[column]) { if (sortItem.desc) { orderByConditions.push(sql`${avlVendorInfo[column]} desc`); } else { orderByConditions.push(sql`${avlVendorInfo[column]} asc`); } } }); // 기본 정렬 if (orderByConditions.length === 0) { orderByConditions.push(asc(avlVendorInfo.id)); } // 총 개수 조회 const totalCount = await db .select({ count: count() }) .from(avlVendorInfo) .where(and(...whereConditions)); // 데이터 조회 - avlVendorInfo에서 직접 조회 const data = await db .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) .where(and(...whereConditions)) .orderBy(...orderByConditions) .limit(input.perPage) .offset(offset); // 데이터 변환 const transformedData: AvlDetailItem[] = data.map((item: any, index) => ({ ...item, no: offset + index + 1, selected: false, createdAt: (item.createdAt as Date)?.toISOString().split('T')[0] || '', updatedAt: (item.updatedAt as Date)?.toISOString().split('T')[0] || '', // UI 표시용 필드 변환 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); debugSuccess('표준 AVL Vendor Info 조회 완료', { recordCount: transformedData.length, pageCount }); return { data: transformedData, pageCount }; } catch (err) { debugError('표준 AVL Vendor Info 조회 실패', { error: err, input }); console.error("Error in getStandardAvlVendorInfo:", err); return { data: [], pageCount: 0 }; } }; /** * 선종별표준AVL → 프로젝트AVL로 복사 */ export const copyToProjectAvl = async ( selectedIds: number[], targetProjectCode: string, targetAvlListId: number, userName: string ): Promise => { 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 => { 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 => { 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(); 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 => { 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 => { 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 => { 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(); 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 최종 확정 중 오류가 발생했습니다." }; } }