From 2650b7c0bb0ea12b68a58c0439f72d61df04b2f1 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Fri, 25 Jul 2025 07:51:15 +0000 Subject: (대표님) 정기평가 대상, 미들웨어 수정, nextauth 토큰 처리 개선, GTC 등 (최겸) 기술영업 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/tech-vendor-possible-items/service.ts | 893 +++++------------------------- 1 file changed, 148 insertions(+), 745 deletions(-) (limited to 'lib/tech-vendor-possible-items/service.ts') diff --git a/lib/tech-vendor-possible-items/service.ts b/lib/tech-vendor-possible-items/service.ts index c630e33a..48f9b869 100644 --- a/lib/tech-vendor-possible-items/service.ts +++ b/lib/tech-vendor-possible-items/service.ts @@ -1,5 +1,6 @@ -"use server"; // Next.js 서버 액션에서 직접 import하려면 (선택) -import { eq, and, inArray, desc, asc, or, ilike, isNull } from "drizzle-orm"; +"use server"; + +import { eq } from "drizzle-orm"; import db from "@/db/db"; import { techVendors, @@ -7,69 +8,48 @@ import { } from "@/db/schema/techVendors"; import { itemShipbuilding, itemOffshoreTop, itemOffshoreHull } from "@/db/schema/items"; import { unstable_cache } from "@/lib/unstable-cache"; -import { filterColumns } from "@/lib/filter-columns"; -import type { GetTechVendorPossibleItemsSchema } from "./validations"; -import { - selectTechVendorPossibleItemsWithJoin, - countTechVendorPossibleItemsWithJoin, -} from "./repository"; +// 새로운 스키마에 맞는 데이터 인터페이스 export interface TechVendorPossibleItemsData { id: number; vendorId: number; + shipbuildingItemId: number | null; + offshoreTopItemId: number | null; + offshoreHullItemId: number | null; + createdAt: Date; + updatedAt: Date; + // 조인된 벤더 정보 vendorCode: string | null; vendorName: string; vendorEmail: string | null; + vendorStatus: string; techVendorType: string; + // 조인된 아이템 정보 itemCode: string; workType: string | null; shipTypes: string | null; itemList: string | null; subItemList: string | null; - createdAt: Date; - updatedAt: Date; } -export interface CreateTechVendorPossibleItemData { - vendorId: number; // 필수: 벤더 ID (Add Dialog에서 벤더 선택 시 사용) - itemCode: string; // 필수: 아이템 코드 - workType?: string | null; // 공종 (아이템에서 가져온 정보) - shipTypes?: string | null; // 선종 (아이템에서 가져온 정보) - itemList?: string | null; // 아이템리스트 (아이템에서 가져온 정보) - subItemList?: string | null; // 서브아이템리스트 (아이템에서 가져온 정보) -} - -export interface ImportTechVendorPossibleItemData { +export interface GetTechVendorPossibleItemsSchema { + page: number; + perPage: number; + search?: string; + filters?: Array<{ id: string; value: string | number | boolean; operator?: string }>; + joinOperator?: "and" | "or"; + sort?: Array<{ id: string; desc: boolean }>; vendorCode?: string; - vendorEmail: string; // 필수: 벤더 이메일 - itemCode: string; // 필수: 아이템 코드 - workType?: string; - shipTypes?: string; - itemList?: string; - subItemList?: string; -} - -export interface ImportResult { - success: boolean; - totalRows: number; - successCount: number; - failedRows: { - row: number; - error: string; - vendorCode?: string; - vendorEmail?: string; - itemCode?: string; - workType?: string; - shipTypes?: string; - itemList?: string; - subItemList?: string; - }[]; + vendorName?: string; + vendorEmail?: string; + vendorStatus?: string; + itemCode?: string; + vendorType?: string; } - - /** - * 견적프로젝트 패턴에 맞는 메인 조회 함수 + * 새로운 스키마에 맞는 tech vendor possible items 조회 함수 + * 벤더 정보와 각 아이템 테이블별 정보를 조인해서 가져옴 */ export async function getTechVendorPossibleItems(input: GetTechVendorPossibleItemsSchema) { return unstable_cache( @@ -77,39 +57,112 @@ export async function getTechVendorPossibleItems(input: GetTechVendorPossibleIte try { const offset = (input.page - 1) * input.perPage; - // 고급 필터링 (DataTableAdvancedToolbar용) - const advancedWhere = filterColumns({ - table: techVendorPossibleItems, - filters: input.filters, - joinOperator: input.joinOperator, - }); - - // 전역 검색 (search box용) - let globalWhere; + // 한 쿼리로 모든 아이템 조회 (각 레코드는 3개 중 하나의 ID만 가짐) + const rawItems = await db + .select({ + id: techVendorPossibleItems.id, + vendorId: techVendorPossibleItems.vendorId, + shipbuildingItemId: techVendorPossibleItems.shipbuildingItemId, + offshoreTopItemId: techVendorPossibleItems.offshoreTopItemId, + offshoreHullItemId: techVendorPossibleItems.offshoreHullItemId, + createdAt: techVendorPossibleItems.createdAt, + updatedAt: techVendorPossibleItems.updatedAt, + // 벤더 정보 + vendorCode: techVendors.vendorCode, + vendorName: techVendors.vendorName, + vendorEmail: techVendors.email, + vendorStatus: techVendors.status, + techVendorType: techVendors.techVendorType, + // 조선 아이템 정보 (shipbuildingItemId가 있을 때만) + shipItemCode: itemShipbuilding.itemCode, + shipWorkType: itemShipbuilding.workType, + shipItemList: itemShipbuilding.itemList, + shipTypes: itemShipbuilding.shipTypes, + // 해양 TOP 아이템 정보 (offshoreTopItemId가 있을 때만) + topItemCode: itemOffshoreTop.itemCode, + topWorkType: itemOffshoreTop.workType, + topItemList: itemOffshoreTop.itemList, + topSubItemList: itemOffshoreTop.subItemList, + // 해양 HULL 아이템 정보 (offshoreHullItemId가 있을 때만) + hullItemCode: itemOffshoreHull.itemCode, + hullWorkType: itemOffshoreHull.workType, + hullItemList: itemOffshoreHull.itemList, + hullSubItemList: itemOffshoreHull.subItemList, + }) + .from(techVendorPossibleItems) + .innerJoin(techVendors, eq(techVendorPossibleItems.vendorId, techVendors.id)) + .leftJoin(itemShipbuilding, eq(techVendorPossibleItems.shipbuildingItemId, itemShipbuilding.id)) + .leftJoin(itemOffshoreTop, eq(techVendorPossibleItems.offshoreTopItemId, itemOffshoreTop.id)) + .leftJoin(itemOffshoreHull, eq(techVendorPossibleItems.offshoreHullItemId, itemOffshoreHull.id)); + + // 결과를 통합된 형태로 변환 + const allItems: TechVendorPossibleItemsData[] = rawItems.map((item: Record) => ({ + id: item.id, + vendorId: item.vendorId, + shipbuildingItemId: item.shipbuildingItemId, + offshoreTopItemId: item.offshoreTopItemId, + offshoreHullItemId: item.offshoreHullItemId, + createdAt: item.createdAt, + updatedAt: item.updatedAt, + vendorCode: item.vendorCode, + vendorName: item.vendorName, + vendorEmail: item.vendorEmail, + vendorStatus: item.vendorStatus, + techVendorType: item.techVendorType, + // 어떤 타입의 아이템인지에 따라 적절한 값 선택 + itemCode: item.shipItemCode || item.topItemCode || item.hullItemCode || "", + workType: item.shipWorkType || item.topWorkType || item.hullWorkType, + itemList: item.shipItemList || item.topItemList || item.hullItemList, + shipTypes: item.shipTypes, // 조선에만 있음 + subItemList: item.topSubItemList || item.hullSubItemList, // 해양에만 있음 + })); + + let filteredItems = allItems; + + // 필터링 적용 if (input.search) { - const s = `%${input.search}%`; - globalWhere = or( - ilike(techVendors.vendorCode, s), - ilike(techVendors.vendorName, s), - ilike(techVendorPossibleItems.vendorEmail, s), - ilike(techVendorPossibleItems.itemCode, s), - ilike(techVendorPossibleItems.workType, s), - ilike(techVendorPossibleItems.shipTypes, s), - ilike(techVendorPossibleItems.itemList, s), - ilike(techVendorPossibleItems.subItemList, s), + const s = input.search.toLowerCase(); + filteredItems = filteredItems.filter(item => + item.vendorCode?.toLowerCase().includes(s) || + item.vendorName?.toLowerCase().includes(s) || + item.vendorEmail?.toLowerCase().includes(s) || + item.itemCode?.toLowerCase().includes(s) || + item.workType?.toLowerCase().includes(s) || + item.itemList?.toLowerCase().includes(s) || + item.shipTypes?.toLowerCase().includes(s) || + item.subItemList?.toLowerCase().includes(s) ); } - // 기존 호환성을 위한 개별 필터들 - const legacyFilters = []; + // 개별 필터들 if (input.vendorCode) { - legacyFilters.push(ilike(techVendorPossibleItems.vendorCode, `%${input.vendorCode}%`)); + filteredItems = filteredItems.filter(item => + item.vendorCode?.toLowerCase().includes(input.vendorCode!.toLowerCase()) + ); } + if (input.vendorName) { - legacyFilters.push(ilike(techVendors.vendorName, `%${input.vendorName}%`)); + filteredItems = filteredItems.filter(item => + item.vendorName?.toLowerCase().includes(input.vendorName!.toLowerCase()) + ); + } + + if (input.vendorEmail) { + filteredItems = filteredItems.filter(item => + item.vendorEmail?.toLowerCase().includes(input.vendorEmail!.toLowerCase()) + ); } + + if (input.vendorStatus) { + filteredItems = filteredItems.filter(item => + item.vendorStatus === input.vendorStatus + ); + } + if (input.itemCode) { - legacyFilters.push(ilike(techVendorPossibleItems.itemCode, `%${input.itemCode}%`)); + filteredItems = filteredItems.filter(item => + item.itemCode?.toLowerCase().includes(input.itemCode!.toLowerCase()) + ); } // 벤더 타입 필터링 @@ -123,31 +176,36 @@ export async function getTechVendorPossibleItems(input: GetTechVendorPossibleIte const actualVendorType = vendorTypeMap[input.vendorType as keyof typeof vendorTypeMap] || input.vendorType; if (actualVendorType) { - legacyFilters.push(ilike(techVendors.techVendorType, `%${actualVendorType}%`)); + filteredItems = filteredItems.filter(item => + item.techVendorType?.includes(actualVendorType) + ); } } - // 모든 조건 결합 - const finalWhere = and( - advancedWhere, - globalWhere, - ...(legacyFilters.length > 0 ? [and(...legacyFilters)] : []) - ); - - // 정렬 조건 - const orderBy = [desc(techVendorPossibleItems.createdAt)]; - - // 트랜잭션 내에서 Repository 호출 - const { data, total } = await db.transaction(async (tx) => { - const data = await selectTechVendorPossibleItemsWithJoin(tx, finalWhere, orderBy, offset, input.perPage); + // 정렬 + if (input.sort && input.sort.length > 0) { + filteredItems.sort((a, b) => { + for (const sortItem of input.sort!) { + const aVal = (a as Record)[sortItem.id] || ""; + const bVal = (b as Record)[sortItem.id] || ""; + + if (aVal < bVal) return sortItem.desc ? 1 : -1; + if (aVal > bVal) return sortItem.desc ? -1 : 1; + } + return 0; + }); + } else { + // 기본 정렬: createdAt 내림차순 + filteredItems.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()); + } - const total = await countTechVendorPossibleItemsWithJoin(tx, finalWhere); - return { data, total }; - }); + const totalCount = filteredItems.length; + const pageCount = Math.ceil(totalCount / input.perPage); - const pageCount = Math.ceil(total / input.perPage); + // 페이지네이션 적용 + const data = filteredItems.slice(offset, offset + input.perPage); - return { data, pageCount, totalCount: total }; + return { data, pageCount, totalCount }; } catch (error) { console.error("Error fetching tech vendor possible items:", error); return { data: [], pageCount: 0, totalCount: 0 }; @@ -159,659 +217,4 @@ export async function getTechVendorPossibleItems(input: GetTechVendorPossibleIte tags: ["tech-vendor-possible-items"], } )(); -} - - - -/** - * 페이지네이션을 포함한 tech vendor possible items 조회 - */ -// export async function getTechVendorPossibleItemsWithPagination( -// page: number = 1, -// pageSize: number = 50, -// searchTerm?: string, -// vendorType?: string -// ): Promise<{ -// data: TechVendorPossibleItemsData[]; -// totalCount: number; -// totalPages: number; -// }> { -// const whereConditions = []; - -// if (searchTerm) { -// whereConditions.push( -// sql`( -// ${techVendors.vendorName} ILIKE ${`%${searchTerm}%`} OR -// ${techVendors.vendorCode} ILIKE ${`%${searchTerm}%`} OR -// ${techVendorPossibleItems.itemCode} ILIKE ${`%${searchTerm}%`} -// )` -// ); -// } - -// // 벤더 타입 필터링 로직 추가 -// if (vendorType && vendorType !== "all") { -// // URL의 vendorType 파라미터를 실제 벤더 타입으로 매핑 -// const vendorTypeMap = { -// "ship": "조선", -// "top": "해양TOP", -// "hull": "해양HULL" -// }; - -// const actualVendorType = vendorType in vendorTypeMap -// ? vendorTypeMap[vendorType as keyof typeof vendorTypeMap] -// : vendorType; // 매핑되지 않는 경우 원본 값 사용 - -// if (actualVendorType) { -// // techVendorType 필드는 콤마로 구분된 문자열이므로 LIKE 사용 -// whereConditions.push(sql`${techVendors.techVendorType} ILIKE ${`%${actualVendorType}%`}`); -// } -// } - -// const whereClause = whereConditions.length > 0 ? and(...whereConditions) : undefined; - -// // 총 개수 조회 -// const [totalCountResult] = await db -// .select({ count: count() }) -// .from(techVendorPossibleItems) -// .innerJoin(techVendors, eq(techVendorPossibleItems.vendorId, techVendors.id)) -// .where(whereClause); - -// const totalCount = totalCountResult.count; -// const totalPages = Math.ceil(totalCount / pageSize); -// const offset = (page - 1) * pageSize; - -// // 데이터 조회 -// const data = await db -// .select({ -// id: techVendorPossibleItems.id, -// vendorId: techVendorPossibleItems.vendorId, -// vendorCode: techVendors.vendorCode, -// vendorName: techVendors.vendorName, -// techVendorType: techVendors.techVendorType, -// itemCode: techVendorPossibleItems.itemCode, -// createdAt: techVendorPossibleItems.createdAt, -// updatedAt: techVendorPossibleItems.updatedAt, -// }) -// .from(techVendorPossibleItems) -// .innerJoin(techVendors, eq(techVendorPossibleItems.vendorId, techVendors.id)) -// .where(whereClause) -// .orderBy(desc(techVendorPossibleItems.createdAt)) -// .limit(pageSize) -// .offset(offset); - -// return { -// data, -// totalCount, -// totalPages, -// }; -// } - -/** - * tech vendor possible item 생성 (Add Dialog용 - vendorId 기반) - */ -export async function createTechVendorPossibleItem( - data: CreateTechVendorPossibleItemData -): Promise<{ success: boolean; error?: string }> { - try { - // 벤더 ID로 벤더 조회 - const vendor = await db - .select() - .from(techVendors) - .where(eq(techVendors.id, data.vendorId)) - .limit(1); - - if (!vendor[0]) { - return { success: false, error: "벤더를 찾을 수 없습니다." }; - } - - // 중복 체크 (벤더 + 아이템코드 + 공종 + 선종 조합) - const existing = await db - .select() - .from(techVendorPossibleItems) - .where( - and( - eq(techVendorPossibleItems.vendorId, data.vendorId), - eq(techVendorPossibleItems.itemCode, data.itemCode), - data.workType - ? eq(techVendorPossibleItems.workType, data.workType) - : isNull(techVendorPossibleItems.workType), - data.shipTypes - ? eq(techVendorPossibleItems.shipTypes, data.shipTypes) - : isNull(techVendorPossibleItems.shipTypes) - ) - ) - .limit(1); - - if (existing.length > 0) { - return { success: false, error: "이미 존재하는 벤더-아이템 조합입니다." }; - } - - // 새로운 아이템 생성 (선택한 아이템의 정보를 그대로 저장) - await db.insert(techVendorPossibleItems).values({ - vendorId: vendor[0].id, - vendorCode: vendor[0].vendorCode, - vendorEmail: vendor[0].email, - itemCode: data.itemCode, - workType: data.workType, - shipTypes: data.shipTypes, - itemList: data.itemList, - subItemList: data.subItemList, - }); - - return { success: true }; - } catch (error) { - console.error("Failed to create tech vendor possible item:", error); - return { - success: false, - error: error instanceof Error ? error.message : "생성 중 오류가 발생했습니다." - }; - } -} - -/** - * tech vendor possible items 삭제 - */ -export async function deleteTechVendorPossibleItems( - ids: number[] -): Promise<{ success: boolean; error?: string }> { - try { - await db - .delete(techVendorPossibleItems) - .where(inArray(techVendorPossibleItems.id, ids)); - - return { success: true }; - } catch (error) { - console.error("Failed to delete tech vendor possible items:", error); - return { - success: false, - error: error instanceof Error ? error.message : "삭제 중 오류가 발생했습니다." - }; - } -} - -/** - * 벤더 코드로 벤더 정보 조회 - */ -export async function getTechVendorByCode(vendorCode: string) { - const result = await db - .select() - .from(techVendors) - .where(eq(techVendors.vendorCode, vendorCode)) - .limit(1); - - return result[0] || null; -} - -/** - * 벤더 이메일로 벤더 정보 조회 - */ -export async function getTechVendorByEmail(vendorEmail: string) { - const result = await db - .select() - .from(techVendors) - .where(eq(techVendors.email, vendorEmail)) - .limit(1); - - return result[0] || null; -} - -/** - * 벤더 타입에 따라 적절한 아이템 테이블에서 아이템 조회 - */ -export async function getItemByCodeAndVendorType(itemCode: string, vendorType: string) { - try { - switch (vendorType) { - case "조선": - const shipItem = await db - .select() - .from(itemShipbuilding) - .where(eq(itemShipbuilding.itemCode, itemCode)) - .limit(1); - return shipItem[0] ? { - itemCode: shipItem[0].itemCode, - workType: shipItem[0].workType - } : null; - - case "해양TOP": - const topItem = await db - .select() - .from(itemOffshoreTop) - .where(eq(itemOffshoreTop.itemCode, itemCode)) - .limit(1); - return topItem[0] ? { - itemCode: topItem[0].itemCode, - workType: topItem[0].workType - } : null; - - case "해양HULL": - const hullItem = await db - .select() - .from(itemOffshoreHull) - .where(eq(itemOffshoreHull.itemCode, itemCode)) - .limit(1); - return hullItem[0] ? { - itemCode: hullItem[0].itemCode, - workType: hullItem[0].workType - } : null; - - default: - return null; - } - } catch (error) { - console.error("Error fetching item by code and vendor type:", error); - return null; - } -} - -/** - * 아이템 코드로 아이템 정보 조회 (기존 함수 - 호환성 유지) - */ -export async function getItemByCode(itemCode: string) { - // 기존 items 테이블 대신 조선 테이블에서 먼저 조회 시도 - try { - const shipItem = await db - .select() - .from(itemShipbuilding) - .where(eq(itemShipbuilding.itemCode, itemCode)) - .limit(1); - - if (shipItem[0]) { - return { - itemCode: shipItem[0].itemCode, - }; - } - - const topItem = await db - .select() - .from(itemOffshoreTop) - .where(eq(itemOffshoreTop.itemCode, itemCode)) - .limit(1); - - if (topItem[0]) { - return { - itemCode: topItem[0].itemCode, - }; - } - - const hullItem = await db - .select() - .from(itemOffshoreHull) - .where(eq(itemOffshoreHull.itemCode, itemCode)) - .limit(1); - - if (hullItem[0]) { - return { - itemCode: hullItem[0].itemCode, - }; - } - - return null; - } catch (error) { - console.error("Error fetching item by code:", error); - return null; - } -} - -/** - * Import 기능: 벤더이메일과 아이템정보를 통한 batch insert (새로운 스키마 버전) - */ -export async function importTechVendorPossibleItems( - data: ImportTechVendorPossibleItemData[] -): Promise { - const result: ImportResult = { - success: true, - totalRows: data.length, - successCount: 0, - failedRows: [], - }; - - for (let i = 0; i < data.length; i++) { - const row = data[i]; - const rowNumber = i + 1; - - try { - // 벤더 이메일로 벤더 찾기 (필수) - let vendor = null; - - if (row.vendorEmail && row.vendorEmail.trim()) { - vendor = await getTechVendorByEmail(row.vendorEmail); - } else { - result.failedRows.push({ - row: rowNumber, - error: "벤더 이메일은 필수입니다.", - vendorCode: row.vendorCode, - vendorEmail: row.vendorEmail, - itemCode: row.itemCode, - workType: row.workType, - shipTypes: row.shipTypes, - itemList: row.itemList, - subItemList: row.subItemList, - }); - continue; - } - - if (!vendor) { - result.failedRows.push({ - row: rowNumber, - error: `벤더 이메일 '${row.vendorEmail}'을(를) 찾을 수 없습니다.`, - vendorCode: row.vendorCode, - vendorEmail: row.vendorEmail, - itemCode: row.itemCode, - workType: row.workType, - shipTypes: row.shipTypes, - itemList: row.itemList, - subItemList: row.subItemList, - }); - continue; - } - - // 중복 체크 (벤더 + 아이템코드 + 공종 + 선종 조합) - const existing = await db - .select() - .from(techVendorPossibleItems) - .where( - and( - eq(techVendorPossibleItems.vendorId, vendor.id), - eq(techVendorPossibleItems.itemCode, row.itemCode), - row.workType - ? eq(techVendorPossibleItems.workType, row.workType) - : isNull(techVendorPossibleItems.workType), - row.shipTypes - ? eq(techVendorPossibleItems.shipTypes, row.shipTypes) - : isNull(techVendorPossibleItems.shipTypes) - ) - ) - .limit(1); - - if (existing.length > 0) { - result.failedRows.push({ - row: rowNumber, - error: `이미 존재하는 벤더-아이템 조합입니다.`, - vendorCode: row.vendorCode, - vendorEmail: row.vendorEmail, - itemCode: row.itemCode, - workType: row.workType, - shipTypes: row.shipTypes, - itemList: row.itemList, - subItemList: row.subItemList, - }); - continue; - } - - // 새로운 아이템 생성 - await db.insert(techVendorPossibleItems).values({ - vendorId: vendor.id, - vendorCode: vendor.vendorCode, - vendorEmail: vendor.email, - itemCode: row.itemCode, - workType: row.workType || null, - shipTypes: row.shipTypes || null, - itemList: row.itemList || null, - subItemList: row.subItemList || null, - }); - - result.successCount++; - } catch (error) { - result.failedRows.push({ - row: rowNumber, - error: error instanceof Error ? error.message : "알 수 없는 오류", - vendorCode: row.vendorCode, - vendorEmail: row.vendorEmail, - itemCode: row.itemCode, - workType: row.workType, - shipTypes: row.shipTypes, - itemList: row.itemList, - subItemList: row.subItemList, - }); - } - } - - if (result.failedRows.length > 0) { - result.success = false; - } - - return result; -} - -/** - * 모든 기술영업 벤더 조회 (드롭다운용) - */ -export async function getAllTechVendors() { - return await db - .select({ - id: techVendors.id, - vendorCode: techVendors.vendorCode, - vendorName: techVendors.vendorName, - techVendorType: techVendors.techVendorType, - }) - .from(techVendors) - .where(eq(techVendors.status, "ACTIVE")) - .orderBy(asc(techVendors.vendorName)); -} - -/** - * 고유한 벤더 타입 목록 조회 (필터용) - */ -export async function getUniqueTechVendorTypes(): Promise { - try { - const result = await db - .select({ - techVendorType: techVendors.techVendorType, - }) - .from(techVendors) - .where(eq(techVendors.status, "ACTIVE")); - - // techVendorType이 JSON 배열 형태로 저장된 경우를 고려 - const allTypes = new Set(); - - result.forEach(row => { - try { - // techVendorType이 JSON 문자열인지 확인 - if (row.techVendorType && row.techVendorType.startsWith('[')) { - const types = JSON.parse(row.techVendorType); - if (Array.isArray(types)) { - types.forEach(type => { - if (type && typeof type === 'string') { - allTypes.add(type.trim()); - } - }); - } - } else if (row.techVendorType) { - // 단순 문자열인 경우 - row.techVendorType.split(',').forEach(type => { - const trimmedType = type.trim(); - if (trimmedType) { - allTypes.add(trimmedType); - } - }); - } - } catch { - // JSON 파싱 실패시 문자열로 처리 - if (row.techVendorType) { - row.techVendorType.split(',').forEach(type => { - const trimmedType = type.trim(); - if (trimmedType) { - allTypes.add(trimmedType); - } - }); - } - } - }); - - return Array.from(allTypes).sort(); - } catch (error) { - console.error("Error fetching unique tech vendor types:", error); - // 오류 발생시 기본 벤더 타입 반환 - return ["조선", "해양TOP", "해양HULL"]; - } -} - -/** - * 벤더 타입에 따른 아이템 목록 조회 - */ -export async function getItemsByVendorType(vendorTypes: string): Promise<{ - itemCode: string; - itemList: string | null; - workType: string | null; - shipTypes?: string | null; - subItemList?: string | null; -}[]> { - try { - // 벤더 타입 파싱 개선 - let types: string[] = []; - if (!vendorTypes) { - return []; - } - - if (vendorTypes.startsWith('[') && vendorTypes.endsWith(']')) { - // JSON 배열 형태 - try { - const parsed = JSON.parse(vendorTypes); - types = Array.isArray(parsed) ? parsed.filter(Boolean) : [vendorTypes]; - } catch { - types = [vendorTypes]; - } - } else if (vendorTypes.includes(',')) { - // 콤마로 구분된 문자열 - types = vendorTypes.split(',').map(t => t.trim()).filter(Boolean); - } else { - // 단일 문자열 - types = [vendorTypes.trim()].filter(Boolean); - } - // 벤더 타입 정렬 - 조선 > 해양TOP > 해양HULL 순 - const typeOrder = ["조선", "해양TOP", "해양HULL"]; - types.sort((a, b) => { - const indexA = typeOrder.indexOf(a); - const indexB = typeOrder.indexOf(b); - - // 정의된 순서에 있는 경우 우선순위 적용 - if (indexA !== -1 && indexB !== -1) { - return indexA - indexB; - } - // 정의된 순서에 없는 경우 마지막에 배치하고 알파벳 순으로 정렬 - if (indexA !== -1) return -1; - if (indexB !== -1) return 1; - return a.localeCompare(b); - }); - - const allItems: any[] = []; - - // 각 벤더 타입에 따라 해당 아이템 테이블에서 조회 - for (const type of types) { - switch (type) { - case "조선": - const shipItems = await db - .select({ - itemCode: itemShipbuilding.itemCode, - itemList: itemShipbuilding.itemList, - workType: itemShipbuilding.workType, - shipTypes: itemShipbuilding.shipTypes, - }) - .from(itemShipbuilding); - allItems.push(...shipItems); - break; - - case "해양TOP": - const topItems = await db - .select({ - itemCode: itemOffshoreTop.itemCode, - itemList: itemOffshoreTop.itemList, - workType: itemOffshoreTop.workType, - subItemList: itemOffshoreTop.subItemList, - }) - .from(itemOffshoreTop); - allItems.push(...topItems); - break; - - case "해양HULL": - const hullItems = await db - .select({ - itemCode: itemOffshoreHull.itemCode, - itemList: itemOffshoreHull.itemList, - workType: itemOffshoreHull.workType, - subItemList: itemOffshoreHull.subItemList, - }) - .from(itemOffshoreHull); - allItems.push(...hullItems); - break; - } - } - // // 중복 제거 (itemCode 기준) - // const uniqueItems = allItems.filter((item, index, self) => - // index === self.findIndex(i => i.itemCode === item.itemCode) - // ); - - // const finalItems = uniqueItems.filter(item => item.itemCode); // itemCode가 있는 것만 반환 - // console.log("Final items after deduplication and filtering:", finalItems.length); - - return allItems; - } catch (error) { - console.error("Error fetching items by vendor type:", error); - return []; - } -} - -/** - * Excel Export 기능: 기술영업 벤더 가능 아이템 목록 내보내기 - */ -export async function exportTechVendorPossibleItemsToExcel(): Promise<{ - success: boolean; - data?: Array<{ - 벤더코드: string | null; - 벤더명: string; - 벤더이메일: string | null; - 벤더타입: string; - 아이템코드: string; - 공종: string | null; - 선종: string | null; - 아이템리스트: string | null; - 서브아이템리스트: string | null; - 생성일: string; - }>; - error?: string; -}> { - try { - // 모든 데이터 조회 (페이지네이션 없이) - const allData = await db - .select({ - vendorCode: techVendorPossibleItems.vendorCode, - vendorName: techVendors.vendorName, - vendorEmail: techVendorPossibleItems.vendorEmail, - techVendorType: techVendors.techVendorType, - itemCode: techVendorPossibleItems.itemCode, - workType: techVendorPossibleItems.workType, - shipTypes: techVendorPossibleItems.shipTypes, - itemList: techVendorPossibleItems.itemList, - subItemList: techVendorPossibleItems.subItemList, - createdAt: techVendorPossibleItems.createdAt, - }) - .from(techVendorPossibleItems) - .innerJoin(techVendors, eq(techVendorPossibleItems.vendorId, techVendors.id)) - .orderBy(desc(techVendorPossibleItems.createdAt)); - - // Excel 형태로 변환 - const excelData = allData.map(item => ({ - 벤더코드: item.vendorCode, - 벤더명: item.vendorName, - 벤더이메일: item.vendorEmail, - 벤더타입: item.techVendorType, - 아이템코드: item.itemCode, - 공종: item.workType, - 선종: item.shipTypes, - 아이템리스트: item.itemList, - 서브아이템리스트: item.subItemList, - 생성일: item.createdAt.toISOString().split('T')[0], // YYYY-MM-DD 형식 - })); - - return { - success: true, - data: excelData, - }; - } catch (error) { - console.error("Error exporting tech vendor possible items:", error); - return { - success: false, - error: error instanceof Error ? error.message : "내보내기 중 오류가 발생했습니다.", - }; - } -} +} \ No newline at end of file -- cgit v1.2.3