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.ts2363
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 최종 확정 중 오류가 발생했습니다."
+ };
+ }
+}