From 766f95945a7ca0fdb258d6a83229593e4fcccfa6 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Thu, 3 Jul 2025 02:50:02 +0000 Subject: (최겸) 기술영업 RFQ 견적 프로젝트별 생성 기능 추가 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/items-tech/service.ts | 59 +++++++++-------- lib/items-tech/table/import-excel-button.tsx | 6 +- lib/techsales-rfq/service.ts | 74 +++++++++++++++++++++- lib/techsales-rfq/table/create-rfq-hull-dialog.tsx | 4 +- lib/techsales-rfq/table/create-rfq-ship-dialog.tsx | 8 +-- lib/techsales-rfq/table/create-rfq-top-dialog.tsx | 2 +- 6 files changed, 115 insertions(+), 38 deletions(-) (limited to 'lib') diff --git a/lib/items-tech/service.ts b/lib/items-tech/service.ts index be65f5dd..0cc08d23 100644 --- a/lib/items-tech/service.ts +++ b/lib/items-tech/service.ts @@ -13,21 +13,21 @@ import { GetShipbuildingSchema, GetOffshoreTopSchema, GetOffshoreHullSchema, Shi import { itemShipbuilding, itemOffshoreTop, itemOffshoreHull } from "@/db/schema/items"; // 타입 정의 추가 -type WorkType = '기장' | '전장' | '선실' | '배관' | '철의'; -type OffshoreTopWorkType = 'TM' | 'TS' | 'TE' | 'TP'; -type OffshoreHullWorkType = 'HA' | 'HE' | 'HH' | 'HM' | 'NC'; +export type ShipbuildingWorkType = '기장' | '전장' | '선실' | '배관' | '철의'; +export type OffshoreTopWorkType = 'TM' | 'TS' | 'TE' | 'TP'; +export type OffshoreHullWorkType = 'HA' | 'HE' | 'HH' | 'HM' | 'NC'; -interface ShipbuildingItem { +export interface ShipbuildingItem { id: number; itemCode: string; - workType: WorkType; + workType: ShipbuildingWorkType; itemList: string; shipTypes: string; createdAt: Date; updatedAt: Date; } -interface OffshoreTopTechItem { +export interface OffshoreTopTechItem { id: number; itemCode: string; workType: OffshoreTopWorkType; @@ -37,7 +37,7 @@ interface OffshoreTopTechItem { updatedAt: Date; } -interface OffshoreHullTechItem { +export interface OffshoreHullTechItem { id: number; itemCode: string; workType: OffshoreHullWorkType; @@ -409,14 +409,19 @@ export async function createShipbuildingImportItem(input: { } } - // 기존 아이템 확인 + // 기존 아이템 및 선종 확인 const existingItem = await db.select().from(itemShipbuilding) - .where(eq(itemShipbuilding.itemCode, input.itemCode)); + .where( + and( + eq(itemShipbuilding.itemCode, input.itemCode), + eq(itemShipbuilding.shipTypes, input.shipTypes || '') + ) + ); if (existingItem.length > 0) { return { success: false, - message: "이미 존재하는 아이템 코드입니다", + message: "이미 존재하는 아이템 코드 및 선종입니다", data: null, error: "중복 키 오류" } @@ -444,7 +449,7 @@ export async function createShipbuildingImportItem(input: { if (err instanceof Error && err.message.includes("unique constraint")) { return { success: false, - message: "이미 존재하는 아이템 코드입니다", + message: "이미 존재하는 아이템 코드 및 선종입니다", data: null, error: "중복 키 오류" } @@ -845,7 +850,7 @@ export async function removeOffshoreHullItems(input: DeleteItemsInput) { ----------------------------------------------------- */ // 조선 공종별 아이템 조회 -export async function getShipbuildingItemsByWorkType(workType?: WorkType, shipType?: string) { +export async function getShipbuildingItemsByWorkType(workType?: ShipbuildingWorkType, shipType?: string) { try { const query = db .select({ @@ -955,7 +960,7 @@ export async function getOffshoreHullItemsByWorkType(workType?: OffshoreHullWork } // 아이템 검색 -export async function searchShipbuildingItems(searchQuery: string, workType?: WorkType, shipType?: string) { +export async function searchShipbuildingItems(searchQuery: string, workType?: ShipbuildingWorkType, shipType?: string) { try { const searchConditions = [ ilike(itemShipbuilding.itemCode, `%${searchQuery}%`), @@ -1096,32 +1101,32 @@ export async function searchOffshoreHullItems(searchQuery: string, workType?: Of // 모든 공종 목록 조회 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: '선체 강재' }, + { code: '기장' as ShipbuildingWorkType, name: '기장'}, + { code: '전장' as ShipbuildingWorkType, name: '전장'}, + { code: '선실' as ShipbuildingWorkType, name: '선실'}, + { code: '배관' as ShipbuildingWorkType, name: '배관'}, + { code: '철의' as ShipbuildingWorkType, name: '철의'}, ] } // 해양 TOP 공종 목록 조회 export async function getOffshoreTopWorkTypes() { return [ - { code: 'TM' as OffshoreTopWorkType, name: 'TM', description: 'Topside Manufacturing' }, - { code: 'TS' as OffshoreTopWorkType, name: 'TS', description: 'Topside Steel' }, - { code: 'TE' as OffshoreTopWorkType, name: 'TE', description: 'Topside Equipment' }, - { code: 'TP' as OffshoreTopWorkType, name: 'TP', description: 'Topside Piping' }, + { code: 'TM' as OffshoreTopWorkType, name: 'TM'}, + { code: 'TS' as OffshoreTopWorkType, name: 'TS'}, + { code: 'TE' as OffshoreTopWorkType, name: 'TE'}, + { code: 'TP' as OffshoreTopWorkType, name: 'TP'}, ] } // 해양 HULL 공종 목록 조회 export async function getOffshoreHullWorkTypes() { return [ - { code: 'HA' as OffshoreHullWorkType, name: 'HA', description: 'Hull Assembly' }, - { code: 'HE' as OffshoreHullWorkType, name: 'HE', description: 'Hull Equipment' }, - { code: 'HH' as OffshoreHullWorkType, name: 'HH', description: 'Hull Heating' }, - { code: 'HM' as OffshoreHullWorkType, name: 'HM', description: 'Hull Manufacturing' }, - { code: 'NC' as OffshoreHullWorkType, name: 'NC', description: 'No Category' }, + { code: 'HA' as OffshoreHullWorkType, name: 'HA'}, + { code: 'HE' as OffshoreHullWorkType, name: 'HE'}, + { code: 'HH' as OffshoreHullWorkType, name: 'HH'}, + { code: 'HM' as OffshoreHullWorkType, name: 'HM'}, + { code: 'NC' as OffshoreHullWorkType, name: 'NC'}, ] } diff --git a/lib/items-tech/table/import-excel-button.tsx b/lib/items-tech/table/import-excel-button.tsx index 3281823c..02736664 100644 --- a/lib/items-tech/table/import-excel-button.tsx +++ b/lib/items-tech/table/import-excel-button.tsx @@ -102,7 +102,7 @@ export function ImportItemButton({ itemType, onSuccess }: ImportItemButtonProps) worksheet.eachRow((row, rowNumber) => { const values = row.values as (string | null)[]; - if (!headerRow && values.some(v => v === "아이템 코드" || v === "itemCode" || v === "item_code")) { + if (!headerRow && values.some(v => v === "자재 그룹" || v === "itemCode" || v === "item_code")) { headerRowIndex = rowNumber; headerRow = row; headerValues = [...values]; @@ -122,10 +122,10 @@ export function ImportItemButton({ itemType, onSuccess }: ImportItemButtonProps) }); // 필수 헤더 확인 (타입별 구분) - const requiredHeaders: string[] = ["아이템 코드", "기능(공종)"]; + const requiredHeaders: string[] = ["자재 그룹", "기능(공종)"]; const alternativeHeaders = { - "아이템 코드": ["itemCode", "item_code"], + "자재 그룹": ["itemCode", "item_code"], "기능(공종)": ["workType"], "자재명": ["itemList"], "자재명(상세)": ["subItemList"] diff --git a/lib/techsales-rfq/service.ts b/lib/techsales-rfq/service.ts index e658747b..d5cb8efe 100644 --- a/lib/techsales-rfq/service.ts +++ b/lib/techsales-rfq/service.ts @@ -39,7 +39,12 @@ import { decryptWithServerAction } from "@/components/drm/drmUtils"; // eslint-disable-next-line @typescript-eslint/no-explicit-any type OrderByType = any; - +export type Project = { + id: number; + projectCode: string; + projectName: string; + pjtType: "SHIP" | "TOP" | "HULL"; +} /** * 연도별 순차 RFQ 코드 생성 함수 (다중 생성 지원) @@ -2745,6 +2750,40 @@ export async function addTechVendorsToTechSalesRfq(input: { }) .returning({ id: techSalesVendorQuotations.id }); + // 🆕 RFQ의 아이템 코드들을 tech_vendor_possible_items에 추가 + try { + // RFQ의 아이템들 조회 + const rfqItemsResult = await getTechSalesRfqItems(input.rfqId); + + if (rfqItemsResult.data && rfqItemsResult.data.length > 0) { + const itemCodes = rfqItemsResult.data + .map(item => item.itemCode) + .filter(code => code); // 빈 코드 제외 + + // 각 아이템 코드에 대해 tech_vendor_possible_items에 추가 (중복 체크) + for (const itemCode of itemCodes) { + // 이미 존재하는지 확인 + const existing = await tx.query.techVendorPossibleItems.findFirst({ + where: and( + eq(techVendorPossibleItems.vendorId, vendorId), + eq(techVendorPossibleItems.itemCode, itemCode) + ) + }); + + // 존재하지 않으면 추가 + if (!existing) { + await tx.insert(techVendorPossibleItems).values({ + vendorId: vendorId, + itemCode: itemCode, + }); + } + } + } + } catch (possibleItemError) { + // tech_vendor_possible_items 추가 실패는 전체 실패로 처리하지 않음 + console.warn(`벤더 ${vendorId}의 가능 아이템 추가 실패:`, possibleItemError); + } + results.push({ id: quotation.id, vendorId, vendorName: vendor.vendorName }); } catch (vendorError) { console.error(`Error adding vendor ${vendorId}:`, vendorError); @@ -3379,4 +3418,37 @@ export async function getAcceptedTechSalesVendorQuotations(input: { console.error("getAcceptedTechSalesVendorQuotations 오류:", error); throw new Error(`Accepted quotations 조회 실패: ${getErrorMessage(error)}`); } +} + +export async function getBidProjects(pjtType: 'SHIP' | 'TOP' | 'HULL'): Promise { + try { + // 트랜잭션을 사용하여 프로젝트 데이터 조회 + const projectList = await db.transaction(async (tx) => { + // 기본 쿼리 구성 + const query = tx + .select({ + id: biddingProjects.id, + projectCode: biddingProjects.pspid, + projectName: biddingProjects.projNm, + pjtType: biddingProjects.pjtType, + }) + .from(biddingProjects) + .where(eq(biddingProjects.pjtType, pjtType)); + + const results = await query.orderBy(biddingProjects.id); + return results; + }); + + // Handle null projectName values and ensure pjtType is not null + const validProjectList = projectList.map(project => ({ + ...project, + projectName: project.projectName || '', // Replace null with empty string + pjtType: project.pjtType as "SHIP" | "TOP" | "HULL" // Type assertion since WHERE filters ensure non-null + })); + + return validProjectList; + } catch (error) { + console.error("프로젝트 목록 가져오기 실패:", error); + return []; // 오류 발생 시 빈 배열 반환 + } } \ No newline at end of file diff --git a/lib/techsales-rfq/table/create-rfq-hull-dialog.tsx b/lib/techsales-rfq/table/create-rfq-hull-dialog.tsx index 7bbbfa75..23c57491 100644 --- a/lib/techsales-rfq/table/create-rfq-hull-dialog.tsx +++ b/lib/techsales-rfq/table/create-rfq-hull-dialog.tsx @@ -57,7 +57,7 @@ import { getOffshoreHullWorkTypes, getAllOffshoreHullItemsForCache, type OffshoreHullWorkType, - type OffshoreHullTechItem + type OffshoreHullTechItem, } from "@/lib/items-tech/service" // 해양 HULL 아이템 타입 정의 (이미 service에서 import하므로 제거) @@ -83,7 +83,6 @@ type CreateHullRfqFormValues = z.infer interface WorkTypeOption { code: OffshoreHullWorkType name: string - description: string } interface CreateHullRfqDialogProps { @@ -355,6 +354,7 @@ export function CreateHullRfqDialog({ onCreated }: CreateHullRfqDialogProps) { selectedProjectId={field.value} onProjectSelect={handleProjectSelect} placeholder="입찰 프로젝트를 선택하세요" + pjtType="HULL" /> diff --git a/lib/techsales-rfq/table/create-rfq-ship-dialog.tsx b/lib/techsales-rfq/table/create-rfq-ship-dialog.tsx index b616f526..efa4e164 100644 --- a/lib/techsales-rfq/table/create-rfq-ship-dialog.tsx +++ b/lib/techsales-rfq/table/create-rfq-ship-dialog.tsx @@ -50,7 +50,7 @@ import { getAllShipbuildingItemsForCache, getShipTypes, type ShipbuildingItem, - type WorkType + type ShipbuildingWorkType } from "@/lib/items-tech/service" @@ -73,9 +73,8 @@ type CreateShipRfqFormValues = z.infer // 공종 타입 정의 interface WorkTypeOption { - code: WorkType + code: ShipbuildingWorkType name: string - description: string } interface CreateShipRfqDialogProps { @@ -90,7 +89,7 @@ export function CreateShipRfqDialog({ onCreated }: CreateShipRfqDialogProps) { // 검색 및 필터링 상태 const [itemSearchQuery, setItemSearchQuery] = React.useState("") - const [selectedWorkType, setSelectedWorkType] = React.useState(null) + const [selectedWorkType, setSelectedWorkType] = React.useState(null) const [selectedShipType, setSelectedShipType] = React.useState(null) const [selectedItems, setSelectedItems] = React.useState([]) @@ -370,6 +369,7 @@ export function CreateShipRfqDialog({ onCreated }: CreateShipRfqDialogProps) { selectedProjectId={field.value} onProjectSelect={handleProjectSelect} placeholder="입찰 프로젝트를 선택하세요" + pjtType="SHIP" /> diff --git a/lib/techsales-rfq/table/create-rfq-top-dialog.tsx b/lib/techsales-rfq/table/create-rfq-top-dialog.tsx index 6536e230..ef2229ac 100644 --- a/lib/techsales-rfq/table/create-rfq-top-dialog.tsx +++ b/lib/techsales-rfq/table/create-rfq-top-dialog.tsx @@ -75,7 +75,6 @@ type CreateTopRfqFormValues = z.infer interface WorkTypeOption { code: OffshoreTopWorkType name: string - description: string } interface CreateTopRfqDialogProps { @@ -346,6 +345,7 @@ export function CreateTopRfqDialog({ onCreated }: CreateTopRfqDialogProps) { selectedProjectId={field.value} onProjectSelect={handleProjectSelect} placeholder="입찰 프로젝트를 선택하세요" + pjtType="TOP" /> -- cgit v1.2.3