// src/lib/items-ship/service.ts "use server"; // Next.js 서버 액션에서 직접 import하려면 (선택) import { revalidateTag, unstable_noStore } from "next/cache"; import db from "@/db/db"; import { filterColumns } from "@/lib/filter-columns"; import { unstable_cache } from "@/lib/unstable-cache"; import { getErrorMessage } from "@/lib/handle-error"; import { asc, desc, ilike, and, or, eq, count, inArray, sql } from "drizzle-orm"; import { GetShipbuildingSchema, GetOffshoreTopSchema, GetOffshoreHullSchema, UpdateItemSchema, ShipbuildingItemCreateData, TypedItemCreateData, OffshoreTopItemCreateData, OffshoreHullItemCreateData } from "./validations"; import { Item, items, itemShipbuilding, itemOffshoreTop, itemOffshoreHull, ItemOffshoreTop, ItemOffshoreHull } from "@/db/schema/items"; import { findAllItems } from "./repository"; import { findAllOffshoreItems } from "./repository"; /* ----------------------------------------------------- 1) 조회 관련 ----------------------------------------------------- */ /** * 복잡한 조건으로 Item 목록을 조회 (+ pagination) 하고, * 총 개수에 따라 pageCount를 계산해서 리턴. * Next.js의 unstable_cache를 사용해 일정 시간 캐시. */ export async function getShipbuildingItems(input: GetShipbuildingSchema) { return unstable_cache( async () => { try { const offset = (input.page - 1) * input.perPage; // advancedTable 모드면 filterColumns()로 where 절 구성 const advancedWhere = filterColumns({ table: items, filters: input.filters.filter(filter => { // enum 필드에 대한 isEmpty/isNotEmpty는 제외 return !((filter.id === 'workType' || filter.id === 'shipTypes') && (filter.operator === 'isEmpty' || filter.operator === 'isNotEmpty')) }), joinOperator: input.joinOperator, joinedTables: { itemShipbuilding }, customColumnMapping: { workType: { table: itemShipbuilding, column: "workType" }, shipTypes: { table: itemShipbuilding, column: "shipTypes" }, itemList: { table: itemShipbuilding, column: "itemList" }, }, }); let globalWhere; if (input.search) { const s = `%${input.search}%`; globalWhere = or( ilike(items.itemCode, s), ilike(itemShipbuilding.itemList, s) ); } // enum 필드에 대한 isEmpty/isNotEmpty 처리 const enumConditions = input.filters .filter(filter => (filter.id === 'workType' || filter.id === 'shipTypes') && (filter.operator === 'isEmpty' || filter.operator === 'isNotEmpty')) .map(filter => { const column = itemShipbuilding[filter.id]; return filter.operator === 'isEmpty' ? sql`${column} is null` : sql`${column} is not null`; }); const finalWhere = and( advancedWhere, globalWhere, ...enumConditions ); const where = finalWhere; const orderBy = input.sort.length > 0 ? input.sort.map((item) => { const column = item.id === "workType" || item.id === "shipTypes" || item.id === "itemList" ? itemShipbuilding[item.id] : items[item.id]; return item.desc ? desc(column) : asc(column); }) : [desc(items.createdAt)]; // 조선 아이템 테이블과 기본 아이템 테이블 조인하여 조회 const result = await db.select({ id: itemShipbuilding.id, itemCode: items.itemCode, workType: itemShipbuilding.workType, shipTypes: itemShipbuilding.shipTypes, itemList: itemShipbuilding.itemList, itemName: items.itemName, description: items.description, createdAt: itemShipbuilding.createdAt, updatedAt: itemShipbuilding.updatedAt, }) .from(itemShipbuilding) .innerJoin(items, eq(itemShipbuilding.itemCode, items.itemCode)) .where(where) .orderBy(...orderBy) .offset(offset) .limit(input.perPage); // 전체 데이터 개수 조회 const [{ count: total }] = await db.select({ count: count() }) .from(itemShipbuilding) .innerJoin(items, eq(itemShipbuilding.itemCode, items.itemCode)) .where(where); const pageCount = Math.ceil(Number(total) / input.perPage); return { data: result, pageCount }; } catch (err) { console.error("Error fetching shipbuilding items:", err); return { data: [], pageCount: 0 }; } }, [JSON.stringify(input)], { revalidate: 3600, tags: ["items"], } )(); } export async function getOffshoreTopItems(input: GetOffshoreTopSchema) { return unstable_cache( async () => { try { const offset = (input.page - 1) * input.perPage; // advancedTable 모드면 filterColumns()로 where 절 구성 const advancedWhere = filterColumns({ table: items, filters: input.filters.filter(filter => { // enum 필드에 대한 isEmpty/isNotEmpty는 제외 return !((filter.id === 'workType') && (filter.operator === 'isEmpty' || filter.operator === 'isNotEmpty')) }), joinOperator: input.joinOperator, joinedTables: { itemOffshoreTop }, customColumnMapping: { workType: { table: itemOffshoreTop, column: "workType" }, itemList: { table: itemOffshoreTop, column: "itemList" }, subItemList: { table: itemOffshoreTop, column: "subItemList" }, }, }); let globalWhere; if (input.search) { const s = `%${input.search}%`; globalWhere = or( ilike(items.itemCode, s), ilike(itemOffshoreTop.itemList, s), ilike(itemOffshoreTop.subItemList, s) ); } // enum 필드에 대한 isEmpty/isNotEmpty 처리 const enumConditions = input.filters .filter(filter => (filter.id === 'workType') && (filter.operator === 'isEmpty' || filter.operator === 'isNotEmpty')) .map(filter => { const column = itemOffshoreTop.workType; return filter.operator === 'isEmpty' ? sql`${column} is null` : sql`${column} is not null`; }); const finalWhere = and( advancedWhere, globalWhere, ...enumConditions ); const where = finalWhere; const orderBy = input.sort.length > 0 ? input.sort.map((item) => { const column = item.id === "workType" || item.id === "itemList" || item.id === "subItemList" ? itemOffshoreTop[item.id] : items[item.id]; return item.desc ? desc(column) : asc(column); }) : [desc(items.createdAt)]; // 해양 TOP 아이템 테이블과 기본 아이템 테이블 조인하여 조회 const result = await db.select({ id: itemOffshoreTop.id, itemCode: items.itemCode, workType: itemOffshoreTop.workType, itemList: itemOffshoreTop.itemList, subItemList: itemOffshoreTop.subItemList, itemName: items.itemName, description: items.description, createdAt: itemOffshoreTop.createdAt, updatedAt: itemOffshoreTop.updatedAt, }) .from(itemOffshoreTop) .innerJoin(items, eq(itemOffshoreTop.itemCode, items.itemCode)) .where(where) .orderBy(...orderBy) .offset(offset) .limit(input.perPage); // 전체 데이터 개수 조회 const [{ count: total }] = await db.select({ count: count() }) .from(itemOffshoreTop) .innerJoin(items, eq(itemOffshoreTop.itemCode, items.itemCode)) .where(where); const pageCount = Math.ceil(Number(total) / input.perPage); return { data: result, pageCount }; } catch (err) { console.error("Error fetching offshore top items:", err); return { data: [], pageCount: 0 }; } }, [JSON.stringify(input)], { revalidate: 3600, tags: ["items"], } )(); } export async function getOffshoreHullItems(input: GetOffshoreHullSchema) { return unstable_cache( async () => { try { const offset = (input.page - 1) * input.perPage; // advancedTable 모드면 filterColumns()로 where 절 구성 const advancedWhere = filterColumns({ table: items, filters: input.filters.filter(filter => { // enum 필드에 대한 isEmpty/isNotEmpty는 제외 return !((filter.id === 'workType') && (filter.operator === 'isEmpty' || filter.operator === 'isNotEmpty')) }), joinOperator: input.joinOperator, joinedTables: { itemOffshoreHull }, customColumnMapping: { workType: { table: itemOffshoreHull, column: "workType" }, itemList: { table: itemOffshoreHull, column: "itemList" }, subItemList: { table: itemOffshoreHull, column: "subItemList" }, }, }); let globalWhere; if (input.search) { const s = `%${input.search}%`; globalWhere = or( ilike(items.itemCode, s), ilike(itemOffshoreHull.itemList, s), ilike(itemOffshoreHull.subItemList, s) ); } // enum 필드에 대한 isEmpty/isNotEmpty 처리 const enumConditions = input.filters .filter(filter => (filter.id === 'workType') && (filter.operator === 'isEmpty' || filter.operator === 'isNotEmpty')) .map(filter => { const column = itemOffshoreHull.workType; return filter.operator === 'isEmpty' ? sql`${column} is null` : sql`${column} is not null`; }); const finalWhere = and( advancedWhere, globalWhere, ...enumConditions ); const where = finalWhere; const orderBy = input.sort.length > 0 ? input.sort.map((item) => { const column = item.id === "workType" || item.id === "itemList" || item.id === "subItemList" ? itemOffshoreHull[item.id] : items[item.id]; return item.desc ? desc(column) : asc(column); }) : [desc(items.createdAt)]; // 해양 HULL 아이템 테이블과 기본 아이템 테이블 조인하여 조회 const result = await db.select({ id: itemOffshoreHull.id, itemCode: items.itemCode, workType: itemOffshoreHull.workType, itemList: itemOffshoreHull.itemList, subItemList: itemOffshoreHull.subItemList, itemName: items.itemName, description: items.description, createdAt: itemOffshoreHull.createdAt, updatedAt: itemOffshoreHull.updatedAt, }) .from(itemOffshoreHull) .innerJoin(items, eq(itemOffshoreHull.itemCode, items.itemCode)) .where(where) .orderBy(...orderBy) .offset(offset) .limit(input.perPage); // 전체 데이터 개수 조회 const [{ count: total }] = await db.select({ count: count() }) .from(itemOffshoreHull) .innerJoin(items, eq(itemOffshoreHull.itemCode, items.itemCode)) .where(where); const pageCount = Math.ceil(Number(total) / input.perPage); return { data: result, pageCount }; } catch (err) { console.error("Error fetching offshore hull items:", err); return { data: [], pageCount: 0 }; } }, [JSON.stringify(input)], { revalidate: 3600, tags: ["items"], } )(); } /* ----------------------------------------------------- 2) 생성(Create) ----------------------------------------------------- */ /** * Item 생성 - 아이템 타입에 따라 해당 테이블에 데이터 삽입 */ export async function createShipbuildingItem(input: TypedItemCreateData) { unstable_noStore() try { if (!input.itemCode) { return { success: false, message: "아이템 코드는 필수입니다", data: null, error: "필수 필드 누락" } } // itemName이 없으면 "기술영업"으로 설정 if (!input.itemName) { input.itemName = "기술영업" } const result = await db.transaction(async (tx) => { // 1. itemCode로 직접 쿼리 const existRows = await tx.select().from(items) .where(eq(items.itemCode, input.itemCode)); const existingItem = existRows[0]; let itemResult: any; if (existingItem) { // 기존 아이템이 있으면 업데이트하지 않고 그대로 사용 itemResult = [existingItem]; // 배열 형태로 반환 } else { // 없으면 새로 생성 // 현재 가장 큰 ID 값 가져오기 const maxIdResult = await tx.select({ maxId: sql`MAX(id)` }).from(items); const maxId = maxIdResult[0]?.maxId || 0; const newId = Number(maxId) + 1; // 새 ID로 아이템 생성 itemResult = await tx.insert(items).values({ id: newId, itemCode: input.itemCode, itemName: input.itemName, description: input.description, }).returning(); } const shipData = input as ShipbuildingItemCreateData; const typeResult = await tx.insert(itemShipbuilding).values({ itemCode: input.itemCode, workType: shipData.workType ? (shipData.workType as '기장' | '전장' | '선실' | '배관' | '철의') : '기장', shipTypes: shipData.shipTypes || '', itemList: shipData.itemList || null, createdAt: new Date(), updatedAt: new Date() }).returning(); return { itemData: itemResult[0], shipbuildingData: typeResult[0] }; }) revalidateTag("items") return { success: true, data: result || null, error: null } } catch (err) { console.error("아이템 생성/업데이트 오류:", err) if (err instanceof Error && err.message.includes("unique constraint")) { return { success: false, message: "이미 존재하는 아이템 코드입니다", data: null, error: "중복 키 오류" } } return { success: false, message: getErrorMessage(err), data: null, error: getErrorMessage(err) } } } /** * Excel import를 위한 조선 아이템 생성 함수 * 하나의 아이템 코드에 대해 여러 선종을 처리 (1:N 관계) */ export async function createShipbuildingImportItem(input: { itemCode: string; itemName: string; workType: '기장' | '전장' | '선실' | '배관' | '철의'; description?: string | null; itemList?: string | null; shipTypes: Record; }) { unstable_noStore(); try { if (!input.itemCode) { return { success: false, message: "아이템 코드는 필수입니다", data: null, error: "필수 필드 누락" } } // itemName이 없을 경우 "기술영업"으로 설정 if (!input.itemName) { input.itemName = "기술영업"; } const results = await db.transaction(async (tx) => { // 1. itemCode로 직접 쿼리 const existRows = await tx.select().from(items) .where(eq(items.itemCode, input.itemCode)); const existingItem = existRows[0]; console.log('DB에서 직접 조회한 기존 아이템:', existingItem); let itemResult: any; if (existingItem) { console.log('기존 아이템 사용, itemCode:', input.itemCode); } else { // 없으면 새로 생성 // 현재 가장 큰 ID 값 가져오기 const maxIdResult = await tx.select({ maxId: sql`MAX(id)` }).from(items); const maxId = maxIdResult[0]?.maxId || 0; const newId = Number(maxId) + 1; console.log('새 아이템 생성을 위한 ID 계산:', { maxId, newId }); // 새 ID로 아이템 생성 const insertResult = await tx.insert(items).values({ id: newId, itemCode: input.itemCode, itemName: input.itemName, description: input.description, }).returning(); console.log('새 아이템 생성 완료, itemCode:', input.itemCode); } const createdItems = []; for (const shipType of Object.keys(input.shipTypes)) { // 그대로 선종명 string으로 저장 const existShip = await tx.select().from(itemShipbuilding) .where( and( eq(itemShipbuilding.itemCode, input.itemCode), eq(itemShipbuilding.shipTypes, shipType) ) ); if (!existShip[0]) { const shipbuildingResult = await tx.insert(itemShipbuilding).values({ itemCode: input.itemCode, workType: input.workType, shipTypes: shipType, itemList: input.itemList || null, createdAt: new Date(), updatedAt: new Date() }).returning(); createdItems.push({ ...shipbuildingResult[0] }); console.log('조선아이템 생성:', shipType, shipbuildingResult[0]); } else { console.log('이미 존재하는 조선아이템:', shipType); } } return createdItems; }); revalidateTag("items"); return { success: true, data: results, error: null } } catch (err) { return { success: false, message: getErrorMessage(err), data: null, error: getErrorMessage(err) } } } export async function createOffshoreTopItem(data: OffshoreTopItemCreateData) { unstable_noStore() try { if (!data.itemCode) { return { success: false, message: "아이템 코드는 필수입니다", data: null, error: "필수 필드 누락" } } // itemName이 없으면 "기술영업"으로 설정 if (!data.itemName) { data.itemName = "기술영업" } // 트랜잭션 내에서 처리 const result = await db.transaction(async (tx) => { // 1. itemCode로 직접 쿼리 const existRows = await tx.select().from(items) .where(eq(items.itemCode, data.itemCode)); const existingItem = existRows[0]; let itemResult: any; if (existingItem) { // 기존 아이템이 있으면 업데이트하지 않고 그대로 사용 itemResult = [existingItem]; // 배열 형태로 반환 } else { // 없으면 새로 생성 // 현재 가장 큰 ID 값 가져오기 const maxIdResult = await tx.select({ maxId: sql`MAX(id)` }).from(items); const maxId = maxIdResult[0]?.maxId || 0; const newId = Number(maxId) + 1; // 새 ID로 아이템 생성 itemResult = await tx.insert(items).values({ id: newId, itemCode: data.itemCode, itemName: data.itemName, description: data.description, }).returning(); } const [offshoreTop] = await tx .insert(itemOffshoreTop) .values({ itemCode: data.itemCode, workType: data.workType, itemList: data.itemList, subItemList: data.subItemList, createdAt: new Date(), updatedAt: new Date() }) .returning(); return { itemData: itemResult[0], offshoreTopData: offshoreTop }; }) revalidateTag("items") return { success: true, data: result, error: null } } catch (err) { console.error("아이템 생성/업데이트 오류:", err) if (err instanceof Error && err.message.includes("unique constraint")) { return { success: false, message: "이미 존재하는 아이템 코드입니다", data: null, error: "중복 키 오류" } } return { success: false, message: getErrorMessage(err), data: null, error: getErrorMessage(err) } } } export async function createOffshoreHullItem(data: OffshoreHullItemCreateData) { unstable_noStore() try { if (!data.itemCode) { return { success: false, message: "아이템 코드는 필수입니다", data: null, error: "필수 필드 누락" } } // itemName이 없으면 "기술영업"으로 설정 if (!data.itemName) { data.itemName = "기술영업" } // 트랜잭션 내에서 처리 const result = await db.transaction(async (tx) => { // 1. itemCode로 직접 쿼리 const existRows = await tx.select().from(items) .where(eq(items.itemCode, data.itemCode)); const existingItem = existRows[0]; let itemResult: any; if (existingItem) { // 기존 아이템이 있으면 업데이트하지 않고 그대로 사용 itemResult = [existingItem]; // 배열 형태로 반환 } else { // 없으면 새로 생성 // 현재 가장 큰 ID 값 가져오기 const maxIdResult = await tx.select({ maxId: sql`MAX(id)` }).from(items); const maxId = maxIdResult[0]?.maxId || 0; const newId = Number(maxId) + 1; // 새 ID로 아이템 생성 itemResult = await tx.insert(items).values({ id: newId, itemCode: data.itemCode, itemName: data.itemName, description: data.description, }).returning(); } const [offshoreHull] = await tx .insert(itemOffshoreHull) .values({ itemCode: data.itemCode, workType: data.workType, itemList: data.itemList, subItemList: data.subItemList, createdAt: new Date(), updatedAt: new Date() }) .returning(); return { itemData: itemResult[0], offshoreHullData: offshoreHull }; }) revalidateTag("items") return { success: true, data: result, error: null } } catch (err) { console.error("아이템 생성/업데이트 오류:", err) if (err instanceof Error && err.message.includes("unique constraint")) { return { success: false, message: "이미 존재하는 아이템 코드입니다", data: null, error: "중복 키 오류" } } return { success: false, message: getErrorMessage(err), data: null, error: getErrorMessage(err) } } } /* ----------------------------------------------------- 3) 업데이트 ----------------------------------------------------- */ // 업데이트 타입 정의 인터페이스 interface UpdateShipbuildingItemInput extends UpdateItemSchema { id: number; workType?: string; shipTypes?: string; itemList?: string; itemCode?: string; itemName?: string; description?: string; } /** 단건 업데이트 */ export async function modifyShipbuildingItem(input: UpdateShipbuildingItemInput) { unstable_noStore(); try { const result = await db.transaction(async (tx) => { // 기존 아이템 조회 const existingShipbuilding = await tx.query.itemShipbuilding.findFirst({ where: eq(itemShipbuilding.id, input.id), with: { item: true } }); if (!existingShipbuilding) { throw new Error("아이템을 찾을 수 없습니다."); } // 세부 아이템 테이블만 업데이트 (items 테이블은 변경하지 않음) const updateData: Record = {}; if (input.workType) updateData.workType = input.workType as '기장' | '전장' | '선실' | '배관' | '철의'; if (input.shipTypes) updateData.shipTypes = input.shipTypes; if (input.itemList !== undefined) updateData.itemList = input.itemList; if (Object.keys(updateData).length > 0) { await tx.update(itemShipbuilding) .set(updateData) .where(eq(itemShipbuilding.id, input.id)); } return { data: { id: input.id }, error: null, success: true, message: "아이템이 성공적으로 업데이트되었습니다." }; }); // 캐시 무효화 revalidateTag("items"); return result; } catch (err) { return { data: null, error: getErrorMessage(err), success: false, message: "아이템 업데이트 중 오류가 발생했습니다." }; } } // Offshore TOP 업데이트 타입 정의 인터페이스 interface UpdateOffshoreTopItemInput extends UpdateItemSchema { id: number; workType?: string; itemList?: string; subItemList?: string; itemCode?: string; itemName?: string; description?: string; } /** Offshore TOP 단건 업데이트 */ export async function modifyOffshoreTopItem(input: UpdateOffshoreTopItemInput) { unstable_noStore(); try { const result = await db.transaction(async (tx) => { // 기존 아이템 조회 const existingOffshoreTop = await tx.query.itemOffshoreTop.findFirst({ where: eq(itemOffshoreTop.id, input.id), with: { item: true } }); if (!existingOffshoreTop) { throw new Error("아이템을 찾을 수 없습니다."); } // 세부 아이템 테이블만 업데이트 (items 테이블은 변경하지 않음) const updateData: Record = {}; if (input.workType) updateData.workType = input.workType as 'TM' | 'TS' | 'TE' | 'TP'; if (input.itemList !== undefined) updateData.itemList = input.itemList; if (input.subItemList !== undefined) updateData.subItemList = input.subItemList; if (Object.keys(updateData).length > 0) { await tx.update(itemOffshoreTop) .set(updateData) .where(eq(itemOffshoreTop.id, input.id)); } return { data: { id: input.id }, error: null, success: true, message: "아이템이 성공적으로 업데이트되었습니다." }; }); // 캐시 무효화 revalidateTag("items"); return result; } catch (err) { return { data: null, error: getErrorMessage(err), success: false, message: "아이템 업데이트 중 오류가 발생했습니다." }; } } // Offshore HULL 업데이트 타입 정의 인터페이스 interface UpdateOffshoreHullItemInput extends UpdateItemSchema { id: number; workType?: string; itemList?: string; subItemList?: string; itemCode?: string; itemName?: string; description?: string; } /** Offshore HULL 단건 업데이트 */ export async function modifyOffshoreHullItem(input: UpdateOffshoreHullItemInput) { unstable_noStore(); try { const result = await db.transaction(async (tx) => { // 기존 아이템 조회 const existingOffshoreHull = await tx.query.itemOffshoreHull.findFirst({ where: eq(itemOffshoreHull.id, input.id), with: { item: true } }); if (!existingOffshoreHull) { throw new Error("아이템을 찾을 수 없습니다."); } // 세부 아이템 테이블만 업데이트 (items 테이블은 변경하지 않음) const updateData: Record = {}; if (input.workType) updateData.workType = input.workType as 'HA' | 'HE' | 'HH' | 'HM' | 'NC'; if (input.itemList !== undefined) updateData.itemList = input.itemList; if (input.subItemList !== undefined) updateData.subItemList = input.subItemList; if (Object.keys(updateData).length > 0) { await tx.update(itemOffshoreHull) .set(updateData) .where(eq(itemOffshoreHull.id, input.id)); } return { data: { id: input.id }, error: null, success: true, message: "아이템이 성공적으로 업데이트되었습니다." }; }); // 캐시 무효화 revalidateTag("items"); return result; } catch (err) { return { data: null, error: getErrorMessage(err), success: false, message: "아이템 업데이트 중 오류가 발생했습니다." }; } } /* ----------------------------------------------------- 4) 삭제 ----------------------------------------------------- */ // 삭제 타입 정의 인터페이스 interface DeleteItemInput { id: number; } interface DeleteItemsInput { ids: number[]; } /** 단건 삭제 */ export async function removeShipbuildingItem(input: DeleteItemInput) { unstable_noStore(); try { await db.transaction(async (tx) => { // 세부 아이템만 삭제 (아이템 테이블은 유지) await tx.delete(itemShipbuilding) .where(eq(itemShipbuilding.id, input.id)); }); revalidateTag("items"); return { data: null, error: null, success: true, message: "아이템이 성공적으로 삭제되었습니다." }; } catch (err) { return { data: null, error: getErrorMessage(err), success: false, message: "아이템 삭제 중 오류가 발생했습니다." }; } } /** 복수 삭제 */ export async function removeShipbuildingItems(input: DeleteItemsInput) { unstable_noStore(); try { await db.transaction(async (tx) => { if (input.ids.length > 0) { // 세부 아이템만 삭제 (아이템 테이블은 유지) await tx.delete(itemShipbuilding) .where(inArray(itemShipbuilding.id, input.ids)); } }); revalidateTag("items"); return { data: null, error: null, success: true, message: "아이템이 성공적으로 삭제되었습니다." }; } catch (err) { return { data: null, error: getErrorMessage(err), success: false, message: "아이템 삭제 중 오류가 발생했습니다." }; } } /** Offshore TOP 단건 삭제 */ export async function removeOffshoreTopItem(input: DeleteItemInput) { unstable_noStore(); try { await db.transaction(async (tx) => { // 세부 아이템만 삭제 (아이템 테이블은 유지) await tx.delete(itemOffshoreTop) .where(eq(itemOffshoreTop.id, input.id)); }); revalidateTag("items"); return { data: null, error: null, success: true, message: "아이템이 성공적으로 삭제되었습니다." }; } catch (err) { return { data: null, error: getErrorMessage(err), success: false, message: "아이템 삭제 중 오류가 발생했습니다." }; } } /** Offshore TOP 복수 삭제 */ export async function removeOffshoreTopItems(input: DeleteItemsInput) { unstable_noStore(); try { await db.transaction(async (tx) => { if (input.ids.length > 0) { // 세부 아이템만 삭제 (아이템 테이블은 유지) await tx.delete(itemOffshoreTop) .where(inArray(itemOffshoreTop.id, input.ids)); } }); revalidateTag("items"); return { data: null, error: null, success: true, message: "아이템이 성공적으로 삭제되었습니다." }; } catch (err) { return { data: null, error: getErrorMessage(err), success: false, message: "아이템 삭제 중 오류가 발생했습니다." }; } } /** Offshore HULL 단건 삭제 */ export async function removeOffshoreHullItem(input: DeleteItemInput) { unstable_noStore(); try { await db.transaction(async (tx) => { // 세부 아이템만 삭제 (아이템 테이블은 유지) await tx.delete(itemOffshoreHull) .where(eq(itemOffshoreHull.id, input.id)); }); revalidateTag("items"); return { data: null, error: null, success: true, message: "아이템이 성공적으로 삭제되었습니다." }; } catch (err) { return { data: null, error: getErrorMessage(err), success: false, message: "아이템 삭제 중 오류가 발생했습니다." }; } } /** Offshore HULL 복수 삭제 */ export async function removeOffshoreHullItems(input: DeleteItemsInput) { unstable_noStore(); try { await db.transaction(async (tx) => { if (input.ids.length > 0) { // 세부 아이템만 삭제 (아이템 테이블은 유지) await tx.delete(itemOffshoreHull) .where(inArray(itemOffshoreHull.id, input.ids)); } }); revalidateTag("items"); return { data: null, error: null, success: true, message: "아이템이 성공적으로 삭제되었습니다." }; } catch (err) { return { data: null, error: getErrorMessage(err), success: false, message: "아이템 삭제 중 오류가 발생했습니다." }; } } export async function getAllShipbuildingItems(): Promise { try { return await findAllItems(); } catch (error) { console.error("Failed to get items:", error); throw new Error("Failed to get items"); } } export async function getAllOffshoreItems(): Promise<(ItemOffshoreHull | ItemOffshoreTop)[]> { try { return await findAllOffshoreItems(); } catch (err) { throw new Error("Failed to get items"); } } // ----------------------------------------------------------- // 기술영업을 위한 로직 // ----------------------------------------------------------- // 조선 공종 타입 export type WorkType = '기장' | '전장' | '선실' | '배관' | '철의' // 조선 아이템 with 공종 정보 export interface ShipbuildingItem { id: number itemCode: string workType: WorkType itemList: string | null // 실제 아이템명 shipTypes: string createdAt: Date updatedAt: Date } // 공종별 아이템 조회 export async function getShipbuildingItemsByWorkType(workType?: WorkType, shipType?: string) { try { const query = db .select({ id: itemShipbuilding.id, itemCode: itemShipbuilding.itemCode, itemName: items.itemName, description: items.description, workType: itemShipbuilding.workType, itemList: itemShipbuilding.itemList, shipTypes: itemShipbuilding.shipTypes, createdAt: itemShipbuilding.createdAt, updatedAt: itemShipbuilding.updatedAt, }) .from(itemShipbuilding) .leftJoin(items, eq(itemShipbuilding.itemCode, items.itemCode)) const conditions = [] if (workType) { conditions.push(eq(itemShipbuilding.workType, workType)) } if (shipType) { conditions.push(eq(itemShipbuilding.shipTypes, shipType)) } if (conditions.length > 0) { query.where(and(...conditions)) } const result = await query return { data: result as ShipbuildingItem[], error: null } } catch (error) { console.error("조선 아이템 조회 오류:", error) return { data: null, error: error instanceof Error ? error.message : "알 수 없는 오류" } } } // 아이템 검색 export async function searchShipbuildingItems(searchQuery: string, workType?: WorkType, shipType?: string) { try { const searchConditions = [ ilike(itemShipbuilding.itemCode, `%${searchQuery}%`), ilike(items.itemName, `%${searchQuery}%`), ilike(items.description, `%${searchQuery}%`), ilike(itemShipbuilding.itemList, `%${searchQuery}%`) ] let whereCondition = or(...searchConditions) const filterConditions = [] if (workType) { filterConditions.push(eq(itemShipbuilding.workType, workType)) } if (shipType) { filterConditions.push(eq(itemShipbuilding.shipTypes, shipType)) } if (filterConditions.length > 0) { whereCondition = and( and(...filterConditions), or(...searchConditions) ) } const result = await db .select({ id: itemShipbuilding.id, itemCode: itemShipbuilding.itemCode, itemName: items.itemName, description: items.description, workType: itemShipbuilding.workType, itemList: itemShipbuilding.itemList, shipTypes: itemShipbuilding.shipTypes, createdAt: itemShipbuilding.createdAt, updatedAt: itemShipbuilding.updatedAt, }) .from(itemShipbuilding) .leftJoin(items, eq(itemShipbuilding.itemCode, items.itemCode)) .where(whereCondition) return { data: result as ShipbuildingItem[], error: null } } catch (error) { console.error("조선 아이템 검색 오류:", error) return { data: null, error: error instanceof Error ? error.message : "알 수 없는 오류" } } } // 모든 공종 목록 조회 export async function getWorkTypes() { return [ { code: '기장' as WorkType, name: '기장', description: '기계 장치' }, { code: '전장' as WorkType, name: '전장', description: '전기 장치' }, { code: '선실' as WorkType, name: '선실', description: '선실' }, { code: '배관' as WorkType, name: '배관', description: '배관' }, { code: '철의' as WorkType, name: '철의', description: '선체 강재' }, ] } // 특정 아이템 코드들로 아이템 조회 export async function getShipbuildingItemsByCodes(itemCodes: string[]) { try { const result = await db .select({ id: itemShipbuilding.id, itemCode: itemShipbuilding.itemCode, itemName: items.itemName, description: items.description, workType: itemShipbuilding.workType, itemList: itemShipbuilding.itemList, shipTypes: itemShipbuilding.shipTypes, createdAt: itemShipbuilding.createdAt, updatedAt: itemShipbuilding.updatedAt, }) .from(itemShipbuilding) .leftJoin(items, eq(itemShipbuilding.itemCode, items.itemCode)) .where( or(...itemCodes.map(code => eq(itemShipbuilding.itemCode, code))) ) return { data: result as ShipbuildingItem[], error: null } } catch (error) { console.error("조선 아이템 코드별 조회 오류:", error) return { data: null, error: error instanceof Error ? error.message : "알 수 없는 오류" } } } // 전체 조선 아이템 조회 (캐싱용) export async function getAllShipbuildingItemsForCache() { try { const result = await db .select({ id: itemShipbuilding.id, itemCode: itemShipbuilding.itemCode, workType: itemShipbuilding.workType, itemList: itemShipbuilding.itemList, shipTypes: itemShipbuilding.shipTypes, createdAt: itemShipbuilding.createdAt, updatedAt: itemShipbuilding.updatedAt, }) .from(itemShipbuilding) return { data: result as ShipbuildingItem[], error: null } } catch (error) { console.error("전체 조선 아이템 조회 오류:", error) return { data: null, error: error instanceof Error ? error.message : "알 수 없는 오류" } } } // 선종 목록 가져오기 export async function getShipTypes() { try { const result = await db .selectDistinct({ shipTypes: itemShipbuilding.shipTypes }) .from(itemShipbuilding) .orderBy(itemShipbuilding.shipTypes) return { data: result.map(item => item.shipTypes), error: null } } catch (error) { console.error("선종 목록 조회 오류:", error) return { data: null, error: error instanceof Error ? error.message : "알 수 없는 오류" } } } // ----------------------------------------------------------- // 기술영업을 위한 로직 끝 // -----------------------------------------------------------