summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorjoonhoekim <26rote@gmail.com>2025-09-22 19:48:40 +0900
committerjoonhoekim <26rote@gmail.com>2025-09-22 19:48:40 +0900
commitf440aee7ff899a6aa93b8e1b5d98ea78df210872 (patch)
tree5de1c92f9c01a5afacaaecc43a18d187e4a908c4 /lib
parent480ac58010604140d1a52fa2b839aedb6ac15941 (diff)
(김준회) 수정: POS 문서는 ECC에서 들어올 때 PR 아이템 기준으로 가져오도록 변경
Diffstat (limited to 'lib')
-rw-r--r--lib/rfq-last/table/rfq-attachments-dialog.tsx88
-rw-r--r--lib/soap/ecc/mapper/bidding-and-pr-mapper.ts178
-rw-r--r--lib/soap/ecc/mapper/rfq-and-pr-mapper.ts35
3 files changed, 254 insertions, 47 deletions
diff --git a/lib/rfq-last/table/rfq-attachments-dialog.tsx b/lib/rfq-last/table/rfq-attachments-dialog.tsx
index 4513b0b0..161e446a 100644
--- a/lib/rfq-last/table/rfq-attachments-dialog.tsx
+++ b/lib/rfq-last/table/rfq-attachments-dialog.tsx
@@ -26,8 +26,8 @@ import { toast } from "sonner"
import { RfqsLastView } from "@/db/schema"
import { getRfqAttachmentsAction } from "../service"
import { downloadFile, quickPreview, smartFileAction, formatFileSize, getFileInfo } from "@/lib/file-download"
-import { syncRfqPosFiles } from "@/lib/pos"
-import { useSession } from "next-auth/react"
+// import { syncRfqPosFiles } from "@/lib/pos" // 주석 처리: ECC 매핑 시 자동 처리로 변경
+// import { useSession } from "next-auth/react" // 주석 처리: 동기화 UI 제거로 불필요
// 첨부파일 타입
interface RfqAttachment {
@@ -57,9 +57,9 @@ export function RfqAttachmentsDialog({ isOpen, onClose, rfqData }: RfqAttachment
const [attachments, setAttachments] = React.useState<RfqAttachment[]>([])
const [isLoading, setIsLoading] = React.useState(false)
const [downloadingFiles, setDownloadingFiles] = React.useState<Set<number>>(new Set())
- const [isSyncing, setIsSyncing] = React.useState(false)
+ // const [isSyncing, setIsSyncing] = React.useState(false) // 주석 처리: 동기화 UI 제거
- const { data: session } = useSession()
+ // const { data: session } = useSession() // 주석 처리: 동기화 UI 제거로 불필요
// 첨부파일 목록 로드
React.useEffect(() => {
@@ -158,48 +158,48 @@ export function RfqAttachmentsDialog({ isOpen, onClose, rfqData }: RfqAttachment
}
}
- // POS 파일 동기화 핸들러
- const handlePosSync = async () => {
- if (!session?.user?.id || !rfqData.id) {
- toast.error("로그인이 필요하거나 RFQ 정보가 없습니다")
- return
- }
+ // POS 파일 동기화 핸들러 - 주석 처리: ECC 매핑 시 자동 처리로 변경
+ // const handlePosSync = async () => {
+ // if (!session?.user?.id || !rfqData.id) {
+ // toast.error("로그인이 필요하거나 RFQ 정보가 없습니다")
+ // return
+ // }
- setIsSyncing(true)
+ // setIsSyncing(true)
- try {
- const result = await syncRfqPosFiles(rfqData.id, parseInt(session.user.id))
+ // try {
+ // const result = await syncRfqPosFiles(rfqData.id, parseInt(session.user.id))
- if (result.success) {
- toast.success(
- `POS 파일 동기화 완료: 성공 ${result.successCount}건, 실패 ${result.failedCount}건`
- )
+ // if (result.success) {
+ // toast.success(
+ // `POS 파일 동기화 완료: 성공 ${result.successCount}건, 실패 ${result.failedCount}건`
+ // )
- // 성공한 경우 첨부파일 목록 새로고침
- if (result.successCount > 0) {
- const refreshResult = await getRfqAttachmentsAction(rfqData.id)
- if (refreshResult.success) {
- setAttachments(refreshResult.data)
- }
- }
+ // // 성공한 경우 첨부파일 목록 새로고침
+ // if (result.successCount > 0) {
+ // const refreshResult = await getRfqAttachmentsAction(rfqData.id)
+ // if (refreshResult.success) {
+ // setAttachments(refreshResult.data)
+ // }
+ // }
- // 상세 결과 표시
- if (result.details.length > 0) {
- const failedItems = result.details.filter(d => d.status === 'failed')
- if (failedItems.length > 0) {
- console.warn("POS 동기화 실패 항목:", failedItems)
- }
- }
- } else {
- toast.error(`POS 파일 동기화 실패: ${result.errors.join(', ')}`)
- }
- } catch (error) {
- console.error("POS 동기화 오류:", error)
- toast.error("POS 파일 동기화 중 오류가 발생했습니다")
- } finally {
- setIsSyncing(false)
- }
- }
+ // // 상세 결과 표시
+ // if (result.details.length > 0) {
+ // const failedItems = result.details.filter(d => d.status === 'failed')
+ // if (failedItems.length > 0) {
+ // console.warn("POS 동기화 실패 항목:", failedItems)
+ // }
+ // }
+ // } else {
+ // toast.error(`POS 파일 동기화 실패: ${result.errors.join(', ')}`)
+ // }
+ // } catch (error) {
+ // console.error("POS 동기화 오류:", error)
+ // toast.error("POS 파일 동기화 중 오류가 발생했습니다")
+ // } finally {
+ // setIsSyncing(false)
+ // }
+ // }
// 첨부파일 타입별 색상
const getAttachmentTypeBadgeVariant = (type: string) => {
@@ -224,8 +224,8 @@ export function RfqAttachmentsDialog({ isOpen, onClose, rfqData }: RfqAttachment
</DialogDescription>
</div>
- {/* POS 동기화 버튼 */}
- <Button
+ {/* POS 동기화 버튼 - 주석 처리: ECC 매핑 시 자동 처리로 변경 */}
+ {/* <Button
variant="outline"
size="sm"
onClick={handlePosSync}
@@ -238,7 +238,7 @@ export function RfqAttachmentsDialog({ isOpen, onClose, rfqData }: RfqAttachment
<RefreshCw className="h-4 w-4" />
)}
{isSyncing ? "동기화 중..." : "POS 파일 동기화"}
- </Button>
+ </Button> */}
</div>
</DialogHeader>
diff --git a/lib/soap/ecc/mapper/bidding-and-pr-mapper.ts b/lib/soap/ecc/mapper/bidding-and-pr-mapper.ts
index 4db8d451..99373555 100644
--- a/lib/soap/ecc/mapper/bidding-and-pr-mapper.ts
+++ b/lib/soap/ecc/mapper/bidding-and-pr-mapper.ts
@@ -9,6 +9,7 @@ import db from '@/db/db';
import {
biddings,
prItemsForBidding,
+ prDocuments,
} from '@/db/schema/bidding';
import {
PR_INFORMATION_T_BID_HEADER,
@@ -22,6 +23,11 @@ import {
parseSAPDateTime,
parseSAPDateToString,
} from './common-mapper-utils';
+import {
+ getDcmtmIdByMaterialCode,
+ getEncryptDocumentumFile,
+ downloadPosFile
+} from '@/lib/pos';
// ECC 데이터 타입 정의
export type ECCBidHeader = typeof PR_INFORMATION_T_BID_HEADER.$inferInsert;
@@ -31,6 +37,117 @@ export type ECCBidItem = typeof PR_INFORMATION_T_BID_ITEM.$inferInsert;
export type BiddingData = typeof biddings.$inferInsert;
export type PrItemForBiddingData = typeof prItemsForBidding.$inferInsert;
+/**
+ * Bidding용 POS 파일 동기화 함수
+ * 자재코드 기준으로 POS 파일을 찾아서 prDocuments 테이블에 저장
+ */
+async function syncBiddingPosFiles(
+ biddingId: number,
+ materialCodes: string[],
+ userId: string = '1'
+): Promise<{
+ success: boolean;
+ successCount: number;
+ failedCount: number;
+ errors: string[];
+}> {
+ debugLog('Bidding POS 파일 동기화 시작', { biddingId, materialCodes });
+
+ let successCount = 0;
+ let failedCount = 0;
+ const errors: string[] = [];
+
+ // 중복 제거된 자재코드로 처리
+ const uniqueMaterialCodes = [...new Set(materialCodes.filter(code => code && code.trim() !== ''))];
+
+ for (const materialCode of uniqueMaterialCodes) {
+ try {
+ debugLog(`자재코드 ${materialCode} POS 파일 조회 시작`);
+
+ // 1. 자재코드로 DCMTM_ID 조회
+ const dcmtmResult = await getDcmtmIdByMaterialCode({ materialCode });
+
+ if (!dcmtmResult.success || !dcmtmResult.files || dcmtmResult.files.length === 0) {
+ debugLog(`자재코드 ${materialCode}: POS 파일 없음`);
+ continue; // 에러로 카운트하지 않고 스킵
+ }
+
+ // 첫 번째 파일만 처리
+ const posFile = dcmtmResult.files[0];
+
+ // 2. POS API로 파일 경로 가져오기
+ const posResult = await getEncryptDocumentumFile({
+ objectID: posFile.dcmtmId
+ });
+
+ if (!posResult.success || !posResult.result) {
+ errors.push(`${materialCode}: POS 파일 경로 조회 실패`);
+ failedCount++;
+ continue;
+ }
+
+ // 3. 파일 다운로드
+ const downloadResult = await downloadPosFile({
+ relativePath: posResult.result
+ });
+
+ if (!downloadResult.success || !downloadResult.fileBuffer) {
+ errors.push(`${materialCode}: 파일 다운로드 실패`);
+ failedCount++;
+ continue;
+ }
+
+ // 4. 서버에 파일 저장 (uploads/bidding-pos 디렉토리)
+ const path = await import('path');
+ const fs = await import('fs/promises');
+
+ const uploadDir = path.join(process.cwd(), 'uploads', 'bidding-pos');
+ try {
+ await fs.access(uploadDir);
+ } catch {
+ await fs.mkdir(uploadDir, { recursive: true });
+ }
+
+ const timestamp = Date.now();
+ const sanitizedFileName = (downloadResult.fileName || `${materialCode}.pdf`).replace(/[^a-zA-Z0-9.-]/g, '_');
+ const fileName = `${timestamp}_${sanitizedFileName}`;
+ const filePath = path.join(uploadDir, fileName);
+
+ await fs.writeFile(filePath, downloadResult.fileBuffer);
+
+ // 5. prDocuments 테이블에 저장
+ await db.insert(prDocuments).values({
+ biddingId,
+ documentName: `${materialCode} 설계문서`,
+ fileName,
+ originalFileName: posFile.fileName,
+ fileSize: downloadResult.fileBuffer.length,
+ mimeType: downloadResult.mimeType || 'application/pdf',
+ filePath: `uploads/bidding-pos/${fileName}`,
+ registeredBy: userId,
+ description: `POS 시스템에서 자동 동기화됨 (DCMTM_ID: ${posFile.dcmtmId}, 자재코드: ${materialCode})`,
+ version: 'Rev.0'
+ });
+
+ successCount++;
+ debugSuccess(`자재코드 ${materialCode} POS 파일 동기화 완료`);
+
+ } catch (error) {
+ const errorMessage = error instanceof Error ? error.message : '알 수 없는 오류';
+ errors.push(`${materialCode}: ${errorMessage}`);
+ failedCount++;
+ debugError(`자재코드 ${materialCode} POS 파일 동기화 실패`, error);
+ }
+ }
+
+ return {
+ success: successCount > 0,
+ successCount,
+ failedCount,
+ errors
+ };
+}
+
/**
* Bidding 코드 생성 함수 (배치 처리용)
@@ -333,7 +450,12 @@ export async function mapAndSaveECCBiddingData(
const inserted = await tx
.insert(biddings)
.values(biddingRecords)
- .returning({ id: biddings.id, biddingNumber: biddings.biddingNumber });
+ .returning({
+ id: biddings.id,
+ biddingNumber: biddings.biddingNumber,
+ ANFNR: biddings.ANFNR,
+ createdBy: biddings.createdBy
+ });
const biddingNumberToId = new Map<string, number>();
for (const row of inserted) {
@@ -368,13 +490,65 @@ export async function mapAndSaveECCBiddingData(
await tx.insert(prItemsForBidding).values(chunk);
}
- return { processedCount: biddingRecords.length };
+ return {
+ processedCount: biddingRecords.length,
+ insertedBiddings: inserted as Array<{ id: number; biddingNumber: string; ANFNR: string | null; createdBy: string | null }>,
+ allEccItems: eccItems // POS 동기화를 위해 필요
+ };
});
debugSuccess('ECC Bidding 데이터 일괄 처리 완료', {
processedCount: result.processedCount,
});
+ // 7) 각 Bidding에 대해 POS 파일 자동 동기화 (비동기로 실행하여 메인 플로우 블록하지 않음)
+ debugLog('Bidding POS 파일 자동 동기화 시작', { biddingCount: result.insertedBiddings.length });
+
+ // 비동기로 각 Bidding의 POS 파일 동기화 실행 (결과를 기다리지 않음)
+ result.insertedBiddings.forEach(async (bidding) => {
+ try {
+ // 해당 Bidding과 관련된 모든 자재코드 추출
+ const relatedMaterialCodes = result.allEccItems
+ .filter(item => item.ANFNR === bidding.ANFNR)
+ .map(item => item.MATNR)
+ .filter(Boolean) as string[];
+
+ if (relatedMaterialCodes.length === 0) {
+ debugLog(`Bidding ${bidding.biddingNumber}: 자재코드 없음`);
+ return;
+ }
+
+ debugLog(`Bidding ${bidding.biddingNumber} POS 파일 동기화 시작`, {
+ biddingId: bidding.id,
+ materialCodes: relatedMaterialCodes
+ });
+
+ const syncResult = await syncBiddingPosFiles(
+ bidding.id,
+ relatedMaterialCodes,
+ bidding.createdBy || '1'
+ );
+
+ if (syncResult.success) {
+ debugSuccess(`Bidding ${bidding.biddingNumber} POS 파일 동기화 완료`, {
+ biddingId: bidding.id,
+ successCount: syncResult.successCount,
+ failedCount: syncResult.failedCount
+ });
+ } else {
+ debugError(`Bidding ${bidding.biddingNumber} POS 파일 동기화 실패`, {
+ biddingId: bidding.id,
+ errors: syncResult.errors
+ });
+ }
+ } catch (error) {
+ debugError(`Bidding ${bidding.biddingNumber} POS 파일 동기화 중 예외 발생`, {
+ biddingId: bidding.id,
+ error: error instanceof Error ? error.message : '알 수 없는 오류'
+ });
+ }
+ });
+
return {
success: true,
message: `${result.processedCount}개의 Bidding 데이터가 성공적으로 처리되었습니다.`,
diff --git a/lib/soap/ecc/mapper/rfq-and-pr-mapper.ts b/lib/soap/ecc/mapper/rfq-and-pr-mapper.ts
index 8748e244..a517d84c 100644
--- a/lib/soap/ecc/mapper/rfq-and-pr-mapper.ts
+++ b/lib/soap/ecc/mapper/rfq-and-pr-mapper.ts
@@ -21,6 +21,7 @@ import {
findMaterialNameByMATNR,
parseSAPDateTime,
} from './common-mapper-utils';
+import { syncRfqPosFiles } from '@/lib/pos';
// ECC 데이터 타입 정의
export type ECCBidHeader = typeof PR_INFORMATION_T_BID_HEADER.$inferInsert;
@@ -346,13 +347,45 @@ export async function mapAndSaveECCRfqDataToRfqLast(
await tx.insert(rfqPrItems).values(chunk);
}
- return { processedCount: rfqRecords.length };
+ return {
+ processedCount: rfqRecords.length,
+ insertedRfqs: inserted // POS 동기화를 위해 inserted 데이터 반환
+ };
});
debugSuccess('ECC 데이터 일괄 처리 완료 (rfqsLast)', {
processedCount: result.processedCount,
});
+ // 6) 각 RFQ에 대해 POS 파일 자동 동기화 (비동기로 실행하여 메인 플로우 블록하지 않음)
+ debugLog('RFQ POS 파일 자동 동기화 시작', { rfqCount: result.insertedRfqs.length });
+
+ // 비동기로 각 RFQ의 POS 파일 동기화 실행 (결과를 기다리지 않음)
+ result.insertedRfqs.forEach(async (rfq) => {
+ try {
+ debugLog(`RFQ ${rfq.rfqCode} POS 파일 동기화 시작`, { rfqId: rfq.id });
+ const syncResult = await syncRfqPosFiles(rfq.id, 1); // 시스템 사용자 ID = 1
+
+ if (syncResult.success) {
+ debugSuccess(`RFQ ${rfq.rfqCode} POS 파일 동기화 완료`, {
+ rfqId: rfq.id,
+ successCount: syncResult.successCount,
+ failedCount: syncResult.failedCount
+ });
+ } else {
+ debugError(`RFQ ${rfq.rfqCode} POS 파일 동기화 실패`, {
+ rfqId: rfq.id,
+ errors: syncResult.errors
+ });
+ }
+ } catch (error) {
+ debugError(`RFQ ${rfq.rfqCode} POS 파일 동기화 중 예외 발생`, {
+ rfqId: rfq.id,
+ error: error instanceof Error ? error.message : '알 수 없는 오류'
+ });
+ }
+ });
+
return {
success: true,
message: `${result.processedCount}개의 RFQ 데이터가 성공적으로 처리되었습니다.`,