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