diff options
| author | joonhoekim <26rote@gmail.com> | 2025-11-19 17:44:48 +0900 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-11-19 17:44:48 +0900 |
| commit | 60382940bac4ac8309be64be16f4774b6820df22 (patch) | |
| tree | 976909a239b0712de59131ee10055735568447fc | |
| parent | ec74a7862eb33a9da1e3d6ba2877d5b9662dbfca (diff) | |
(김준회) PR 데이터 수신시 Spec 정보도 넣어주도록 수정
| -rw-r--r-- | db/schema/bidding.ts | 1 | ||||
| -rw-r--r-- | db/schema/rfqLast.ts | 4 | ||||
| -rw-r--r-- | lib/rfq-last/shared/rfq-items-dialog.tsx | 32 | ||||
| -rw-r--r-- | lib/soap/ecc/mapper/bidding-and-pr-mapper.ts | 6 | ||||
| -rw-r--r-- | lib/soap/ecc/mapper/common-mapper-utils.ts | 35 | ||||
| -rw-r--r-- | lib/soap/ecc/mapper/rfq-and-pr-mapper.ts | 33 |
6 files changed, 91 insertions, 20 deletions
diff --git a/db/schema/bidding.ts b/db/schema/bidding.ts index 1d1fe50a..8f9f8d84 100644 --- a/db/schema/bidding.ts +++ b/db/schema/bidding.ts @@ -336,6 +336,7 @@ export const prItemsForBidding = pgTable('pr_items_for_bidding', { // SPEC 파일 정보 hasSpecDocument: boolean('has_spec_document').default(false), + specification: varchar('specification', { length: 2000 }), // Specification createdAt: timestamp('created_at').defaultNow().notNull(), updatedAt: timestamp('updated_at').defaultNow().notNull(), diff --git a/db/schema/rfqLast.ts b/db/schema/rfqLast.ts index 7ab03c30..10ec888a 100644 --- a/db/schema/rfqLast.ts +++ b/db/schema/rfqLast.ts @@ -1,4 +1,4 @@ -import {bigint, jsonb, decimal, json,index, pgTable, pgView, serial, varchar, text, timestamp, boolean, integer, numeric, date, alias, check, uniqueIndex, unique } from "drizzle-orm/pg-core"; +import {bigint, jsonb, decimal, json,index, pgTable, pgView, serial, varchar, text, timestamp, boolean, integer, numeric, date, alias, uniqueIndex, unique } from "drizzle-orm/pg-core"; import { eq, sql, relations } from "drizzle-orm"; import { projects } from "./projects"; import { users } from "./users"; @@ -273,6 +273,7 @@ export const rfqPrItems = pgTable( specNo: varchar("spec_no", { length: 255 }), specUrl: varchar("spec_url", { length: 255 }), + specification: varchar("specification", { length: 2000 }), trackingNo: varchar("tracking_no", { length: 255 }), majorYn: boolean("major_yn").default(false), @@ -657,6 +658,7 @@ export const prItemsLastView = pgView("pr_items_last_view").as((qb) => { // Specification specNo: rfqPrItems.specNo, specUrl: rfqPrItems.specUrl, + specification: rfqPrItems.specification, trackingNo: rfqPrItems.trackingNo, // Major flag diff --git a/lib/rfq-last/shared/rfq-items-dialog.tsx b/lib/rfq-last/shared/rfq-items-dialog.tsx index e4f71e79..eed3d154 100644 --- a/lib/rfq-last/shared/rfq-items-dialog.tsx +++ b/lib/rfq-last/shared/rfq-items-dialog.tsx @@ -18,10 +18,15 @@ import { TableHeader, TableRow, } from "@/components/ui/table" -import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" import { Skeleton } from "@/components/ui/skeleton" import { Separator } from "@/components/ui/separator" +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip" import { toast } from "sonner" import { RfqsLastView, VendorQuotationView } from "@/db/schema" import { getRfqItemsAction } from "../service" @@ -267,7 +272,7 @@ export function RfqItemsDialog({ <TableHead className="w-[60px]">아이템</TableHead> <TableHead className="w-[120px]">자재코드</TableHead> <TableHead>자재명</TableHead> - <TableHead className="w-[140px]">사양</TableHead> + <TableHead className="w-[200px]">Specification</TableHead> <TableHead className="w-[80px]">수량</TableHead> <TableHead className="w-[60px]">수량단위</TableHead> <TableHead className="w-[80px]">중량</TableHead> @@ -309,7 +314,7 @@ export function RfqItemsDialog({ <TableHead className="w-[60px]">아이템</TableHead> <TableHead className="w-[120px]">자재코드</TableHead> <TableHead>자재명</TableHead> - <TableHead className="w-[140px]">사양</TableHead> + <TableHead className="w-[200px]">Specification</TableHead> <TableHead className="w-[80px]">수량</TableHead> <TableHead className="w-[60px]">수량단위</TableHead> <TableHead className="w-[80px]">중량</TableHead> @@ -363,9 +368,24 @@ export function RfqItemsDialog({ </div> </TableCell> <TableCell> - <span className="text-sm font-medium"> - {item.specification?.trim() ? item.specification : "-"} - </span> + {item.specification?.trim() ? ( + <TooltipProvider> + <Tooltip> + <TooltipTrigger asChild> + <div className="text-sm font-medium cursor-help line-clamp-2 text-blue-600"> + {item.specification} + </div> + </TooltipTrigger> + <TooltipContent className="max-w-md p-3"> + <p className="text-sm whitespace-pre-wrap break-words"> + {item.specification} + </p> + </TooltipContent> + </Tooltip> + </TooltipProvider> + ) : ( + <span className="text-sm text-muted-foreground">-</span> + )} </TableCell> <TableCell> <span className="text-sm font-medium"> diff --git a/lib/soap/ecc/mapper/bidding-and-pr-mapper.ts b/lib/soap/ecc/mapper/bidding-and-pr-mapper.ts index 4ab9a745..adbb3e1e 100644 --- a/lib/soap/ecc/mapper/bidding-and-pr-mapper.ts +++ b/lib/soap/ecc/mapper/bidding-and-pr-mapper.ts @@ -29,6 +29,7 @@ import { findMaterialNameByMATNR, parseSAPDateTime, parseSAPDateToString, + findSpecificationByMATNR, } from './common-mapper-utils'; // Note: POS 파일은 온디맨드 방식으로 다운로드됩니다. // 자동 동기화 관련 import는 제거되었습니다. @@ -267,6 +268,9 @@ export async function mapECCBiddingItemToPrItemForBidding( // PSPID(Project Code)로 프로젝트 ID 조회 const projectId = await findProjectIdByPSPID(eccItem.PSPID || null); + // Specification 조회 (MATNR 기반) + const specification = await findSpecificationByMATNR(eccItem.MATNR || null); + const mappedData: PrItemForBiddingData = { biddingId, // 부모 Bidding ID itemNumber: eccItem.ANFPS || null, // 아이템 번호 @@ -305,12 +309,14 @@ export async function mapECCBiddingItemToPrItemForBidding( prNumber: eccItem.BANFN || null, // PR번호 hasSpecDocument: false, // 기본값 false + specification, // MATNR로 조회한 Specification }; debugSuccess('ECC Bidding 아이템 매핑 완료', { itemNumber: eccItem.ANFPS, prNumber: eccItem.BANFN, projectId, + specification, }); return mappedData; } diff --git a/lib/soap/ecc/mapper/common-mapper-utils.ts b/lib/soap/ecc/mapper/common-mapper-utils.ts index ed655e0e..9141a29f 100644 --- a/lib/soap/ecc/mapper/common-mapper-utils.ts +++ b/lib/soap/ecc/mapper/common-mapper-utils.ts @@ -14,7 +14,7 @@ import { debugLog, debugSuccess, debugError } from '@/lib/debug-utils'; import db from '@/db/db'; import { users, vendors } from '@/db/schema'; import { projects } from '@/db/schema/projects'; -import { EQUP_MASTER_MATL_CHARASGN } from '@/db/schema/MDG/mdg'; +import { EQUP_MASTER_MATL_CHARASGN, MATERIAL_MASTER_PART_MATL } from '@/db/schema/MDG/mdg'; import { eq } from 'drizzle-orm'; import { oracleKnex } from '@/lib/oracle-db/db'; @@ -386,4 +386,37 @@ export async function findVendorIdByLIFNR(lifnr: string | null | undefined): Pro debugError('Vendor 조회 중 오류 발생', { lifnr, error }); return null; } +} + +/** + * Specification 조회 함수 (MATNR 기반) + * MATNR을 기반으로 MATERIAL_MASTER_PART_MATL 테이블에서 ZZSPEC 조회 + */ +export async function findSpecificationByMATNR(MATNR: string | null): Promise<string | null> { + try { + debugLog('Specification 조회 시작', { MATNR }); + + if (!MATNR) { + debugError('MATNR이 null 또는 undefined', { MATNR }); + return null; + } + + const specResult = await db + .select({ ZZSPEC: MATERIAL_MASTER_PART_MATL.ZZSPEC }) + .from(MATERIAL_MASTER_PART_MATL) + .where(eq(MATERIAL_MASTER_PART_MATL.MATNR, MATNR)) + .limit(1); + + if (specResult.length === 0) { + debugLog('MATNR에 해당하는 Specification을 찾을 수 없음', { MATNR }); + return null; + } + + const specification = specResult[0].ZZSPEC; + debugSuccess('Specification 조회 완료', { MATNR, specification }); + return specification; + } catch (error) { + debugError('Specification 조회 중 오류 발생', { MATNR, error }); + return null; + } }
\ No newline at end of file diff --git a/lib/soap/ecc/mapper/rfq-and-pr-mapper.ts b/lib/soap/ecc/mapper/rfq-and-pr-mapper.ts index d08bc5fb..c0557d0c 100644 --- a/lib/soap/ecc/mapper/rfq-and-pr-mapper.ts +++ b/lib/soap/ecc/mapper/rfq-and-pr-mapper.ts @@ -25,6 +25,7 @@ import { findProjectInfoByPSPID, parseSAPDateTime, findUserInfoByPERNR, + findSpecificationByMATNR, } from './common-mapper-utils'; // ECC 데이터 타입 정의 @@ -241,11 +242,11 @@ export async function mapECCRfqHeaderToRfqLast( /** * ECC RFQ 아이템 데이터를 rfqPrItems 테이블로 매핑 */ -export function mapECCRfqItemToRfqPrItem( +export async function mapECCRfqItemToRfqPrItem( eccItem: ECCBidItem, rfqId: number, isMajor: boolean = false -): RfqPrItemData { +): Promise<RfqPrItemData> { debugLog('ECC RFQ 아이템 매핑 시작', { anfnr: eccItem.ANFNR, anfps: eccItem.ANFPS, @@ -269,6 +270,9 @@ export function mapECCRfqItemToRfqPrItem( } } + // Specification 조회 (MATNR 기반) + const specification = await findSpecificationByMATNR(eccItem.MATNR || null); + const mappedData: RfqPrItemData = { rfqsLastId: rfqId, // 부모 RFQ ID rfqItem: eccItem.ANFPS || null, // RFQ Item 번호 @@ -286,6 +290,7 @@ export function mapECCRfqItemToRfqPrItem( gwUom: eccItem.GEWEI || null, // 중량단위 specNo: null, // ECC에서 제공되지 않음 specUrl: null, // ECC에서 제공되지 않음 + specification, // MATNR로 조회한 Specification trackingNo: null, // ECC에서 제공되지 않음 majorYn: isMajor, // ZCON_NO_PO와 BANFN이 같은 경우 true projectDef: eccItem.PSPID || null, // 프로젝트 정의 @@ -299,6 +304,7 @@ export function mapECCRfqItemToRfqPrItem( debugSuccess('ECC RFQ 아이템 매핑 완료', { rfqItem: eccItem.ANFPS, materialCode: eccItem.MATNR, + specification, }); return mappedData; } @@ -363,7 +369,7 @@ export async function mapAndSaveECCRfqDataToRfqLast( // 4) 모든 새로 삽입된 레코드의 ID 매핑은 이미 완료됨 - // 5) 모든 아이템을 한 번에 생성할 데이터로 변환 + // 5) 모든 아이템을 한 번에 생성할 데이터로 변환 (async 함수로 병렬 처리) const allItemsToInsert: RfqPrItemData[] = []; for (const group of rfqGroups) { const rfqCode = group.rfqCode; @@ -374,15 +380,18 @@ export async function mapAndSaveECCRfqDataToRfqLast( throw new Error(`RFQ ID를 찾을 수 없습니다: ${rfqCode}`); } - for (const eccItem of group.relatedItems) { - // ZCON_NO_PO와 BANFN이 같은 경우 majorYn을 true로 설정 - const isMajor: boolean = !!(eccItem.ZCON_NO_PO && - eccItem.ZCON_NO_PO.trim() && - eccItem.BANFN === eccItem.ZCON_NO_PO.trim()); - - const itemData = mapECCRfqItemToRfqPrItem(eccItem, rfqId, isMajor); - allItemsToInsert.push(itemData); - } + // 각 아이템을 병렬로 매핑 (async 함수로 변경되었으므로) + const itemsData = await Promise.all( + group.relatedItems.map(async eccItem => { + // ZCON_NO_PO와 BANFN이 같은 경우 majorYn을 true로 설정 + const isMajor: boolean = !!(eccItem.ZCON_NO_PO && + eccItem.ZCON_NO_PO.trim() && + eccItem.BANFN === eccItem.ZCON_NO_PO.trim()); + + return await mapECCRfqItemToRfqPrItem(eccItem, rfqId, isMajor); + }) + ); + allItemsToInsert.push(...itemsData); } // 5) 아이템 일괄 삽입 (chunk 처리로 파라미터 제한 회피) |
