summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--db/schema/bidding.ts1
-rw-r--r--db/schema/rfqLast.ts4
-rw-r--r--lib/rfq-last/shared/rfq-items-dialog.tsx32
-rw-r--r--lib/soap/ecc/mapper/bidding-and-pr-mapper.ts6
-rw-r--r--lib/soap/ecc/mapper/common-mapper-utils.ts35
-rw-r--r--lib/soap/ecc/mapper/rfq-and-pr-mapper.ts33
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 처리로 파라미터 제한 회피)