diff options
Diffstat (limited to 'lib/avl/service.ts')
| -rw-r--r-- | lib/avl/service.ts | 2363 |
1 files changed, 2363 insertions, 0 deletions
diff --git a/lib/avl/service.ts b/lib/avl/service.ts new file mode 100644 index 00000000..535a0169 --- /dev/null +++ b/lib/avl/service.ts @@ -0,0 +1,2363 @@ +"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 } 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 리스트 조회 + * avl_list 테이블에서 실제 데이터를 조회합니다. + */ +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(avlList.constructionSector, searchTerm), + ilike(avlList.projectCode, searchTerm), + ilike(avlList.shipType, searchTerm), + ilike(avlList.avlKind, searchTerm) + ) + ); + } + + // 필터 조건 추가 + if (input.isTemplate === "true") { + whereConditions.push(eq(avlList.isTemplate, true)); + } else if (input.isTemplate === "false") { + whereConditions.push(eq(avlList.isTemplate, false)); + } + + if (input.constructionSector) { + whereConditions.push(ilike(avlList.constructionSector, `%${input.constructionSector}%`)); + } + if (input.projectCode) { + whereConditions.push(ilike(avlList.projectCode, `%${input.projectCode}%`)); + } + if (input.shipType) { + whereConditions.push(ilike(avlList.shipType, `%${input.shipType}%`)); + } + if (input.avlKind) { + whereConditions.push(ilike(avlList.avlKind, `%${input.avlKind}%`)); + } + if (input.htDivision) { + whereConditions.push(eq(avlList.htDivision, input.htDivision)); + } + if (input.rev) { + whereConditions.push(eq(avlList.rev, parseInt(input.rev))); + } + + // 정렬 조건 구성 + const orderByConditions: any[] = []; + input.sort.forEach((sortItem) => { + const column = sortItem.id as keyof typeof avlList; + + if (column && avlList[column]) { + if (sortItem.desc) { + orderByConditions.push(sql`${avlList[column]} desc`); + } else { + orderByConditions.push(sql`${avlList[column]} asc`); + } + } + }); + + // 기본 정렬 (등재일 내림차순) + if (orderByConditions.length === 0) { + orderByConditions.push(desc(avlList.createdAt)); + } + + // 총 개수 조회 + const totalCount = await db + .select({ count: count() }) + .from(avlList) + .where(and(...whereConditions)); + + // 데이터 조회 + const data = await db + .select() + .from(avlList) + .where(and(...whereConditions)) + .orderBy(...orderByConditions) + .limit(input.perPage) + .offset(offset); + + // 데이터 변환 (timestamp -> string) + const transformedData: AvlListItem[] = data.map((item, index) => ({ + ...item, + 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] || '', + // 추가 필드들 (실제로는 JOIN이나 별도 쿼리로 가져와야 함) + projectInfo: item.projectCode || '', + shipType: item.shipType || '', + avlType: item.avlKind || '', + htDivision: item.htDivision || '', + rev: item.rev || 1, + })); + + 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<AvlListItem | null> { + 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<AvlDetailItem | null> { + 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<ActionResult> { + 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<AvlListItem | null> { + return await createAvlList(data); +} + +export async function updateAvlListAction(id: number, data: UpdateAvlListInput): Promise<AvlListItem | null> { + return await updateAvlList(id, data); +} + +export async function deleteAvlListAction(id: number): Promise<boolean> { + return await deleteAvlList(id); +} + +export async function handleAvlActionAction(action: string, data?: any): Promise<ActionResult> { + return await handleAvlAction(action, data); +} + +/** + * AVL 리스트 생성 + */ +export async function createAvlList(data: CreateAvlListInput): Promise<AvlListItem | null> { + 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<AvlListItem | null> { + 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<boolean> { + 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<AvlDetailItem | null> { + 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<AvlVendorInfoInput>): Promise<AvlDetailItem | null> { + 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<boolean> { + 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<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 최종 확정 중 오류가 발생했습니다." + }; + } +} |
