(null)
- // 품목 목록 및 설계 문서 로드
+ // 품목 목록 로드
React.useEffect(() => {
if (!isOpen || !rfqData.id) return
const loadData = async () => {
setIsLoading(true)
- setIsLoadingDocs(true)
try {
- // 1. 품목 목록 로드
+ // 품목 목록 로드
const itemsResult = await getRfqItemsAction(rfqData.id)
if (itemsResult.success) {
@@ -115,26 +117,14 @@ export function RfqItemsDialog({ isOpen, onClose, rfqData }: RfqItemsDialogProps
setItems([])
setStatistics(null)
}
-
- // 2. 설계 문서 매핑 로드
- const docsResult = await getDesignDocumentsForRfqItemsAction(rfqData.id)
-
- if (docsResult.success && docsResult.documents) {
- setDesignDocuments(docsResult.documents)
- } else {
- console.warn("설계 문서 매핑 로드 실패:", docsResult.error)
- setDesignDocuments({})
- }
} catch (error) {
console.error("데이터 로드 오류:", error)
toast.error("데이터를 불러오는데 실패했습니다")
setItems([])
setStatistics(null)
- setDesignDocuments({})
} finally {
setIsLoading(false)
- setIsLoadingDocs(false)
}
}
@@ -146,26 +136,74 @@ export function RfqItemsDialog({ isOpen, onClose, rfqData }: RfqItemsDialogProps
window.open(specUrl, '_blank', 'noopener,noreferrer')
}
- // 설계 문서 다운로드
- const handleDownloadDesignDoc = async (materialCode: string, fileName: string, filePath: string) => {
+ // POS 파일 목록 조회 및 다이얼로그 열기
+ const handleOpenPosDialog = async (materialCode: string) => {
+ if (!materialCode) {
+ toast.error("자재코드가 없습니다")
+ return
+ }
+
+ setLoadingPosFiles(true)
+ setSelectedMaterialCode(materialCode)
+
+ try {
+ toast.loading(`POS 파일 목록 조회 중... (${materialCode})`, { id: `pos-check-${materialCode}` })
+
+ const result = await checkPosFileExists(materialCode)
+
+ if (result.exists && result.files && result.files.length > 0) {
+ // 파일 정보를 상세하게 가져오기 위해 getDownloadUrlByMaterialCode 사용
+ const detailResult = await getDownloadUrlByMaterialCode(materialCode)
+
+ if (detailResult.success && detailResult.availableFiles) {
+ setPosFiles(detailResult.availableFiles)
+ setPosDialogOpen(true)
+ toast.success(`${result.fileCount}개의 POS 파일을 찾았습니다`, { id: `pos-check-${materialCode}` })
+ } else {
+ toast.error('POS 파일 정보를 가져올 수 없습니다', { id: `pos-check-${materialCode}` })
+ }
+ } else {
+ toast.error(result.error || 'POS 파일을 찾을 수 없습니다', { id: `pos-check-${materialCode}` })
+ }
+ } catch (error) {
+ console.error("POS 파일 조회 오류:", error)
+ toast.error("POS 파일 조회에 실패했습니다", { id: `pos-check-${materialCode}` })
+ } finally {
+ setLoadingPosFiles(false)
+ }
+ }
+
+ // POS 파일 다운로드 실행
+ const handleDownloadPosFile = async (fileIndex: number, fileName: string) => {
+ if (!selectedMaterialCode) return
+
+ setDownloadingFileIndex(fileIndex)
+
try {
- await downloadFile(filePath, fileName, {
- action: 'download',
- showToast: true
- })
+ toast.loading(`POS 파일 다운로드 준비 중...`, { id: `download-${fileIndex}` })
+
+ const downloadUrl = `/api/pos/download-on-demand?materialCode=${encodeURIComponent(selectedMaterialCode)}&fileIndex=${fileIndex}`
+
+ toast.success(`POS 파일 다운로드 시작: ${fileName}`, { id: `download-${fileIndex}` })
+ window.open(downloadUrl, '_blank', 'noopener,noreferrer')
+
+ // 다운로드 시작 후 잠시 대기 후 상태 초기화
+ setTimeout(() => {
+ setDownloadingFileIndex(null)
+ }, 1000)
} catch (error) {
- console.error("설계 문서 다운로드 오류:", error)
- toast.error("설계 문서 다운로드에 실패했습니다")
+ console.error("POS 파일 다운로드 오류:", error)
+ toast.error("POS 파일 다운로드에 실패했습니다", { id: `download-${fileIndex}` })
+ setDownloadingFileIndex(null)
}
}
- // 파일 크기 포맷팅
- const formatFileSize = (bytes: number | null) => {
- if (!bytes) return ""
- const sizes = ['B', 'KB', 'MB', 'GB']
- if (bytes === 0) return '0 B'
- const i = Math.floor(Math.log(bytes) / Math.log(1024))
- return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i]
+ // POS 다이얼로그 닫기
+ const handleClosePosDialog = () => {
+ setPosDialogOpen(false)
+ setSelectedMaterialCode("")
+ setPosFiles([])
+ setDownloadingFileIndex(null)
}
@@ -361,23 +399,20 @@ export function RfqItemsDialog({ isOpen, onClose, rfqData }: RfqItemsDialogProps
)}
- {/* 설계 문서 다운로드 */}
- {item.materialCode && designDocuments[item.materialCode] && (
+ {/* POS 파일 다운로드 */}
+ {item.materialCode && (
-
+
)}
@@ -388,13 +423,6 @@ export function RfqItemsDialog({ isOpen, onClose, rfqData }: RfqItemsDialogProps
TRK: {item.trackingNo}
)}
-
- {/* 설계 문서 로딩 상태 */}
- {isLoadingDocs && item.materialCode && (
-
- 설계문서 확인 중...
-
- )}
@@ -435,6 +463,16 @@ export function RfqItemsDialog({ isOpen, onClose, rfqData }: RfqItemsDialogProps
)}
+
+ {/* POS 파일 선택 다이얼로그 */}
+
)
diff --git a/lib/soap/ecc/mapper/bidding-and-pr-mapper.ts b/lib/soap/ecc/mapper/bidding-and-pr-mapper.ts
index 5f3c7e78..af4cecdc 100644
--- a/lib/soap/ecc/mapper/bidding-and-pr-mapper.ts
+++ b/lib/soap/ecc/mapper/bidding-and-pr-mapper.ts
@@ -1,7 +1,13 @@
/**
- * pr 발행 후, pr을 묶어서 rfq, bidding 을 sap ecc에서 생성한 경우
- * ZBSART = AB인 경우, 즉 bidding인 경우 해당 케이스를 soap으로 수신한 뒤 이 함수에서 헤더는 biddings 테이블에, 아이템은 prItemsForBidding 테이블에 매핑
- * ZBSART = AN인 경우, 즉 rfq인 경우 해당 케이스를 soap으로 수신한 뒤 rfq-and-pr-mapper.ts 파일에서 매핑
+ * SAP ECC에서 PR을 묶어 Bidding을 생성한 경우 SOAP으로 수신하여 처리하는 매퍼
+ *
+ * ZBSART = AB (Bidding)인 경우 이 파일에서 처리하여 biddings, prItemsForBidding 테이블에 매핑합니다.
+ * ZBSART = AN (RFQ)인 경우는 rfq-and-pr-mapper.ts 파일에서 매핑합니다.
+ *
+ * 주요 변경사항:
+ * - POS 파일 자동 다운로드 로직 제거됨 (온디맨드 방식으로 변경)
+ * - syncBiddingPosFiles 함수 제거됨
+ * - 사용자가 필요할 때만 POS 파일을 다운로드하도록 개선
*/
import { debugLog, debugSuccess, debugError } from '@/lib/debug-utils';
@@ -23,11 +29,8 @@ import {
parseSAPDateTime,
parseSAPDateToString,
} from './common-mapper-utils';
-import {
- getDcmtmIdByMaterialCode,
- getEncryptDocumentumFile,
- downloadPosFile
-} from '@/lib/pos';
+// Note: POS 파일은 온디맨드 방식으로 다운로드됩니다.
+// 자동 동기화 관련 import는 제거되었습니다.
// ECC 데이터 타입 정의
export type ECCBidHeader = typeof PR_INFORMATION_T_BID_HEADER.$inferInsert;
@@ -37,116 +40,8 @@ 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
- };
-}
+// Note: syncBiddingPosFiles 함수는 제거되었습니다.
+// POS 파일은 온디맨드 방식으로 사용자가 필요할 때 다운로드됩니다.
/**
@@ -501,53 +396,8 @@ export async function mapAndSaveECCBiddingData(
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 : '알 수 없는 오류'
- });
- }
- });
+ // Note: POS 파일은 온디맨드 방식으로 사용자가 필요할 때 다운로드됩니다.
+ // 자동 다운로드는 더 이상 수행하지 않습니다.
return {
success: true,
diff --git a/lib/soap/ecc/mapper/rfq-and-pr-mapper.ts b/lib/soap/ecc/mapper/rfq-and-pr-mapper.ts
index f2683213..cc241aa6 100644
--- a/lib/soap/ecc/mapper/rfq-and-pr-mapper.ts
+++ b/lib/soap/ecc/mapper/rfq-and-pr-mapper.ts
@@ -1,7 +1,12 @@
/**
- * pr 발행 후, pr을 묶어서 rfq, bidding 을 sap ecc에서 생성한 경우
- * ZBSART = AN인 경우, 즉 rfq인 경우 해당 케이스를 soap으로 수신한 뒤 이 함수에서 rqfLast, rfqPrItems 테이블에 매핑
- * bidding인 경우는 bidding-and-pr-mapper.ts 파일에서 매핑
+ * SAP ECC에서 PR을 묶어 RFQ를 생성한 경우 SOAP으로 수신하여 처리하는 매퍼
+ *
+ * ZBSART = AN (RFQ)인 경우 이 파일에서 처리하여 rfqsLast, rfqPrItems 테이블에 매핑합니다.
+ * ZBSART = AB (Bidding)인 경우는 bidding-and-pr-mapper.ts 파일에서 매핑합니다.
+ *
+ * 주요 변경사항:
+ * - POS 파일 자동 다운로드 로직 제거됨 (온디맨드 방식으로 변경)
+ * - 사용자가 필요할 때만 POS 파일을 다운로드하도록 개선
*/
import { debugLog, debugSuccess, debugError } from '@/lib/debug-utils';
@@ -20,7 +25,6 @@ import {
findProjectInfoByPSPID,
parseSAPDateTime,
} from './common-mapper-utils';
-import { syncRfqPosFiles } from '@/lib/pos';
// ECC 데이터 타입 정의
export type ECCBidHeader = typeof PR_INFORMATION_T_BID_HEADER.$inferInsert;
@@ -374,34 +378,8 @@ export async function mapAndSaveECCRfqDataToRfqLast(
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 : '알 수 없는 오류'
- });
- }
- });
+ // Note: POS 파일은 온디맨드 방식으로 사용자가 필요할 때 다운로드됩니다.
+ // 자동 다운로드는 더 이상 수행하지 않습니다.
return {
success: true,
--
cgit v1.2.3