summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/api/pos/download-on-demand/route.ts196
-rw-r--r--lib/pos/components/pos-file-selection-dialog.tsx134
-rw-r--r--lib/pos/download-on-demand-action.ts193
-rw-r--r--lib/pos/index.ts24
-rw-r--r--lib/pos/sync-rfq-pos-files.ts407
-rw-r--r--lib/rfq-last/table/rfq-items-dialog.tsx158
-rw-r--r--lib/soap/ecc/mapper/bidding-and-pr-mapper.ts180
-rw-r--r--lib/soap/ecc/mapper/rfq-and-pr-mapper.ts42
8 files changed, 665 insertions, 669 deletions
diff --git a/app/api/pos/download-on-demand/route.ts b/app/api/pos/download-on-demand/route.ts
new file mode 100644
index 00000000..f88e8858
--- /dev/null
+++ b/app/api/pos/download-on-demand/route.ts
@@ -0,0 +1,196 @@
+/**
+ * POS 파일 온디맨드 다운로드 API
+ *
+ * 자재코드(MATNR)로 POS 파일을 찾아서 NFS에서 직접 스트리밍합니다.
+ * 서버 스토리지에 저장하지 않고 실시간으로 다운로드합니다.
+ */
+
+import { NextRequest, NextResponse } from 'next/server';
+import { getDcmtmIdByMaterialCode, getEncryptDocumentumFile, downloadPosFile } from '@/lib/pos';
+
+// 허용된 파일 확장자
+const ALLOWED_EXTENSIONS = new Set([
+ 'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx',
+ 'txt', 'csv', 'png', 'jpg', 'jpeg', 'gif', 'bmp', 'svg',
+ 'dwg', 'dxf', 'zip', 'rar', '7z'
+]);
+
+// 최대 파일 크기 (10240MB)
+const MAX_FILE_SIZE = 10240 * 1024 * 1024;
+
+// 파일 확장자 검증
+function validateFileExtension(fileName: string): boolean {
+ const extension = fileName.split('.').pop()?.toLowerCase() || '';
+ return ALLOWED_EXTENSIONS.has(extension);
+}
+
+// MIME 타입 결정
+function getMimeType(fileName: string): string {
+ const fileExtension = fileName.split('.').pop()?.toLowerCase() || '';
+ const mimeTypes: Record<string, string> = {
+ 'pdf': 'application/pdf',
+ 'doc': 'application/msword',
+ 'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+ 'xls': 'application/vnd.ms-excel',
+ 'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+ 'ppt': 'application/vnd.ms-powerpoint',
+ 'pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
+ 'txt': 'text/plain; charset=utf-8',
+ 'csv': 'text/csv; charset=utf-8',
+ 'png': 'image/png',
+ 'jpg': 'image/jpeg',
+ 'jpeg': 'image/jpeg',
+ 'gif': 'image/gif',
+ 'bmp': 'image/bmp',
+ 'svg': 'image/svg+xml',
+ 'dwg': 'application/acad',
+ 'dxf': 'application/dxf',
+ 'zip': 'application/zip',
+ 'rar': 'application/x-rar-compressed',
+ '7z': 'application/x-7z-compressed',
+ };
+ return mimeTypes[fileExtension] || 'application/octet-stream';
+}
+
+export async function GET(request: NextRequest) {
+ try {
+ const searchParams = request.nextUrl.searchParams;
+ const materialCode = searchParams.get('materialCode');
+ const fileIndex = parseInt(searchParams.get('fileIndex') || '0');
+
+ // 파라미터 검증
+ if (!materialCode || materialCode.trim() === '') {
+ return NextResponse.json(
+ { error: '자재코드(materialCode)가 제공되지 않았습니다.' },
+ { status: 400 }
+ );
+ }
+
+ console.log(`🔍 POS 파일 온디맨드 다운로드 시작 (자재코드: ${materialCode})`);
+
+ // 1. 자재코드로 DCMTM_ID 조회
+ console.log(`📋 DCMTM_ID 조회 중... (자재코드: ${materialCode})`);
+ const dcmtmResult = await getDcmtmIdByMaterialCode({ materialCode });
+
+ if (!dcmtmResult.success || !dcmtmResult.files || dcmtmResult.files.length === 0) {
+ console.warn(`⚠️ POS 파일을 찾을 수 없음 (자재코드: ${materialCode})`);
+ return NextResponse.json(
+ {
+ error: dcmtmResult.error || '해당 자재코드에 대한 POS 파일을 찾을 수 없습니다.',
+ materialCode
+ },
+ { status: 404 }
+ );
+ }
+
+ // 파일 인덱스 범위 검증
+ if (fileIndex >= dcmtmResult.files.length) {
+ return NextResponse.json(
+ {
+ error: `파일 인덱스가 범위를 벗어났습니다. 사용 가능한 파일 수: ${dcmtmResult.files.length}`,
+ availableFilesCount: dcmtmResult.files.length
+ },
+ { status: 400 }
+ );
+ }
+
+ const posFile = dcmtmResult.files[fileIndex];
+ console.log(`✅ DCMTM_ID 조회 완료:`, {
+ materialCode,
+ dcmtmId: posFile.dcmtmId,
+ fileName: posFile.fileName,
+ totalFiles: dcmtmResult.files.length
+ });
+
+ // 2. POS API로 파일 경로 가져오기
+ console.log(`🌐 POS API 호출 중... (DCMTM_ID: ${posFile.dcmtmId})`);
+ const posResult = await getEncryptDocumentumFile({
+ objectID: posFile.dcmtmId
+ });
+
+ if (!posResult.success || !posResult.result) {
+ console.error(`❌ POS API 호출 실패:`, posResult.error);
+ return NextResponse.json(
+ {
+ error: posResult.error || 'POS 파일 경로를 가져올 수 없습니다.',
+ materialCode,
+ dcmtmId: posFile.dcmtmId
+ },
+ { status: 500 }
+ );
+ }
+
+ console.log(`✅ POS API 호출 완료 (경로: ${posResult.result})`);
+
+ // 3. NFS에서 파일 다운로드
+ console.log(`⬇️ 파일 다운로드 중... (NFS 경로: ${posResult.result})`);
+ const downloadResult = await downloadPosFile({
+ relativePath: posResult.result
+ });
+
+ if (!downloadResult.success || !downloadResult.fileBuffer) {
+ console.error(`❌ 파일 다운로드 실패:`, downloadResult.error);
+ return NextResponse.json(
+ {
+ error: downloadResult.error || '파일 다운로드에 실패했습니다.',
+ materialCode,
+ nfsPath: posResult.result
+ },
+ { status: 500 }
+ );
+ }
+
+ const fileName = downloadResult.fileName || posFile.fileName;
+ const fileBuffer = downloadResult.fileBuffer;
+
+ console.log(`✅ 파일 다운로드 완료 (크기: ${fileBuffer.length} bytes)`);
+
+ // 파일 확장자 검증
+ if (!validateFileExtension(fileName)) {
+ console.warn(`🚨 허용되지 않은 파일 확장자: ${fileName}`);
+ return NextResponse.json(
+ { error: '지원하지 않는 파일 형식입니다.', fileName },
+ { status: 403 }
+ );
+ }
+
+ // 파일 크기 검증
+ if (fileBuffer.length > MAX_FILE_SIZE) {
+ console.warn(`🚨 파일 크기 초과: ${fileBuffer.length} bytes`);
+ return NextResponse.json(
+ { error: '파일 크기가 너무 큽니다.', fileSize: fileBuffer.length },
+ { status: 413 }
+ );
+ }
+
+ // MIME 타입 결정
+ const contentType = downloadResult.mimeType || getMimeType(fileName);
+
+ // 파일 스트리밍 응답 생성
+ const response = new NextResponse(fileBuffer);
+
+ response.headers.set('Content-Type', contentType);
+ response.headers.set('Content-Disposition', `attachment; filename*=UTF-8''${encodeURIComponent(fileName)}`);
+ response.headers.set('Content-Length', fileBuffer.length.toString());
+
+ // 보안 헤더
+ response.headers.set('Cache-Control', 'no-cache, no-store, must-revalidate');
+ response.headers.set('Pragma', 'no-cache');
+ response.headers.set('Expires', '0');
+ response.headers.set('X-Content-Type-Options', 'nosniff');
+
+ console.log(`✅ POS 파일 다운로드 성공: ${fileName} (${fileBuffer.length} bytes)`);
+
+ return response;
+ } catch (error) {
+ console.error('❌ POS 파일 온디맨드 다운로드 API 오류:', error);
+ return NextResponse.json(
+ {
+ error: '서버 내부 오류가 발생했습니다.',
+ details: error instanceof Error ? error.message : '알 수 없는 오류'
+ },
+ { status: 500 }
+ );
+ }
+}
+
diff --git a/lib/pos/components/pos-file-selection-dialog.tsx b/lib/pos/components/pos-file-selection-dialog.tsx
new file mode 100644
index 00000000..29936d21
--- /dev/null
+++ b/lib/pos/components/pos-file-selection-dialog.tsx
@@ -0,0 +1,134 @@
+"use client"
+
+import * as React from "react"
+import { Download, FileText } from "lucide-react"
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog"
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table"
+import { Button } from "@/components/ui/button"
+import { ScrollArea } from "@/components/ui/scroll-area"
+import { Badge } from "@/components/ui/badge"
+
+interface PosFileInfo {
+ fileName: string
+ dcmtmId: string
+ projNo: string
+ posNo: string
+ posRevNo: string
+ fileSer: string
+}
+
+interface PosFileSelectionDialogProps {
+ isOpen: boolean
+ onClose: () => void
+ materialCode: string
+ files: PosFileInfo[]
+ onDownload: (fileIndex: number, fileName: string) => void
+ downloadingIndex: number | null
+}
+
+export function PosFileSelectionDialog({
+ isOpen,
+ onClose,
+ materialCode,
+ files,
+ onDownload,
+ downloadingIndex,
+}: PosFileSelectionDialogProps) {
+ return (
+ <Dialog open={isOpen} onOpenChange={onClose}>
+ <DialogContent className="max-w-4xl">
+ <DialogHeader>
+ <DialogTitle className="flex items-center gap-2">
+ <FileText className="h-5 w-5 text-green-600" />
+ POS 파일 선택
+ </DialogTitle>
+ <DialogDescription>
+ 자재코드 <Badge variant="outline" className="font-mono">{materialCode}</Badge>에 대한
+ POS 파일 <Badge variant="secondary">{files.length}개</Badge>가 있습니다.
+ 다운로드할 파일을 선택해주세요.
+ </DialogDescription>
+ </DialogHeader>
+
+ <ScrollArea className="max-h-[60vh]">
+ <Table>
+ <TableHeader>
+ <TableRow>
+ <TableHead className="w-[50px]">번호</TableHead>
+ <TableHead>파일명</TableHead>
+ <TableHead className="w-[120px]">프로젝트</TableHead>
+ <TableHead className="w-[150px]">POS 번호</TableHead>
+ <TableHead className="w-[80px]">리비전</TableHead>
+ <TableHead className="w-[80px]">파일 SEQ</TableHead>
+ <TableHead className="w-[120px] text-center">다운로드</TableHead>
+ </TableRow>
+ </TableHeader>
+ <TableBody>
+ {files.map((file, index) => (
+ <TableRow key={`${file.dcmtmId}-${index}`}>
+ <TableCell className="text-center font-mono text-sm">
+ #{index + 1}
+ </TableCell>
+ <TableCell>
+ <div className="flex items-center gap-2">
+ <FileText className="h-4 w-4 text-blue-500 flex-shrink-0" />
+ <span className="text-sm font-medium truncate" title={file.fileName}>
+ {file.fileName}
+ </span>
+ </div>
+ </TableCell>
+ <TableCell>
+ <span className="text-xs font-mono">{file.projNo}</span>
+ </TableCell>
+ <TableCell>
+ <span className="text-xs font-mono">{file.posNo}</span>
+ </TableCell>
+ <TableCell>
+ <Badge variant="outline" className="text-xs">
+ {file.posRevNo}
+ </Badge>
+ </TableCell>
+ <TableCell>
+ <span className="text-xs font-mono">{file.fileSer}</span>
+ </TableCell>
+ <TableCell className="text-center">
+ <Button
+ size="sm"
+ variant="default"
+ onClick={() => onDownload(index, file.fileName)}
+ disabled={downloadingIndex !== null}
+ className="w-full"
+ >
+ <Download className="h-3 w-3 mr-1" />
+ {downloadingIndex === index ? '다운로드 중...' : '다운로드'}
+ </Button>
+ </TableCell>
+ </TableRow>
+ ))}
+ </TableBody>
+ </Table>
+ </ScrollArea>
+
+ {files.length === 0 && (
+ <div className="text-center py-8 text-muted-foreground">
+ <FileText className="h-12 w-12 mx-auto mb-4 text-muted-foreground" />
+ <p>사용 가능한 POS 파일이 없습니다.</p>
+ </div>
+ )}
+ </DialogContent>
+ </Dialog>
+ )
+}
+
diff --git a/lib/pos/download-on-demand-action.ts b/lib/pos/download-on-demand-action.ts
new file mode 100644
index 00000000..568bae22
--- /dev/null
+++ b/lib/pos/download-on-demand-action.ts
@@ -0,0 +1,193 @@
+/**
+ * POS 파일 온디맨드 다운로드 서버 액션
+ *
+ * 클라이언트 컴포넌트에서 자재코드로 POS 파일을 다운로드할 수 있는 함수들을 제공합니다.
+ */
+
+'use server';
+
+import { getDcmtmIdByMaterialCode } from './get-dcmtm-id';
+
+export interface DownloadPosOnDemandResult {
+ success: boolean;
+ downloadUrl?: string;
+ fileName?: string;
+ availableFiles?: Array<{
+ fileName: string;
+ dcmtmId: string;
+ projNo: string;
+ posNo: string;
+ posRevNo: string;
+ fileSer: string;
+ }>;
+ error?: string;
+}
+
+/**
+ * 자재코드로 POS 파일 다운로드 URL을 생성합니다.
+ *
+ * @param materialCode - 자재코드 (MATNR)
+ * @param fileIndex - 여러 파일이 있을 경우 선택할 파일 인덱스 (기본값: 0)
+ * @returns 다운로드 URL과 파일 정보
+ *
+ * @example
+ * ```typescript
+ * const result = await getDownloadUrlByMaterialCode('SN2693A6410100001');
+ * if (result.success && result.downloadUrl) {
+ * window.open(result.downloadUrl, '_blank');
+ * }
+ * ```
+ */
+export async function getDownloadUrlByMaterialCode(
+ materialCode: string,
+ fileIndex: number = 0
+): Promise<DownloadPosOnDemandResult> {
+ try {
+ if (!materialCode || materialCode.trim() === '') {
+ return {
+ success: false,
+ error: '자재코드가 제공되지 않았습니다.',
+ };
+ }
+
+ // 1. 자재코드로 DCMTM_ID 및 파일 정보 조회
+ const dcmtmResult = await getDcmtmIdByMaterialCode({ materialCode });
+
+ if (!dcmtmResult.success || !dcmtmResult.files || dcmtmResult.files.length === 0) {
+ return {
+ success: false,
+ error: dcmtmResult.error || '해당 자재코드에 대한 POS 파일을 찾을 수 없습니다.',
+ };
+ }
+
+ // 파일 인덱스 범위 검증
+ if (fileIndex >= dcmtmResult.files.length) {
+ return {
+ success: false,
+ error: `파일 인덱스가 범위를 벗어났습니다. 사용 가능한 파일 수: ${dcmtmResult.files.length}`,
+ availableFiles: dcmtmResult.files.map(file => ({
+ fileName: file.fileName,
+ dcmtmId: file.dcmtmId,
+ projNo: file.projNo,
+ posNo: file.posNo,
+ posRevNo: file.posRevNo,
+ fileSer: file.fileSer,
+ })),
+ };
+ }
+
+ const selectedFile = dcmtmResult.files[fileIndex];
+
+ // 2. 다운로드 URL 생성
+ const downloadUrl = `/api/pos/download-on-demand?materialCode=${encodeURIComponent(materialCode)}&fileIndex=${fileIndex}`;
+
+ return {
+ success: true,
+ downloadUrl,
+ fileName: selectedFile.fileName,
+ availableFiles: dcmtmResult.files.map(file => ({
+ fileName: file.fileName,
+ dcmtmId: file.dcmtmId,
+ projNo: file.projNo,
+ posNo: file.posNo,
+ posRevNo: file.posRevNo,
+ fileSer: file.fileSer,
+ })),
+ };
+ } catch (error) {
+ return {
+ success: false,
+ error: error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.',
+ };
+ }
+}
+
+/**
+ * 여러 자재코드에 대한 POS 파일 다운로드 URL을 일괄 조회합니다.
+ *
+ * @param materialCodes - 자재코드 배열
+ * @returns 각 자재코드별 다운로드 정보
+ *
+ * @example
+ * ```typescript
+ * const results = await getDownloadUrlsForMaterialCodes([
+ * 'SN2693A6410100001',
+ * 'SN2693A6410100002'
+ * ]);
+ *
+ * results.forEach(result => {
+ * if (result.success) {
+ * console.log(`${result.materialCode}: ${result.downloadUrl}`);
+ * }
+ * });
+ * ```
+ */
+export async function getDownloadUrlsForMaterialCodes(
+ materialCodes: string[]
+): Promise<Array<DownloadPosOnDemandResult & { materialCode: string }>> {
+ const results = await Promise.all(
+ materialCodes.map(async (materialCode) => {
+ const result = await getDownloadUrlByMaterialCode(materialCode);
+ return {
+ materialCode,
+ ...result,
+ };
+ })
+ );
+
+ return results;
+}
+
+/**
+ * 자재코드의 POS 파일 존재 여부를 확인합니다.
+ *
+ * @param materialCode - 자재코드 (MATNR)
+ * @returns 파일 존재 여부와 파일 수
+ */
+export async function checkPosFileExists(
+ materialCode: string
+): Promise<{
+ exists: boolean;
+ fileCount: number;
+ files?: Array<{
+ fileName: string;
+ dcmtmId: string;
+ projNo: string;
+ posNo: string;
+ posRevNo: string;
+ fileSer: string;
+ }>;
+ error?: string;
+}> {
+ try {
+ const dcmtmResult = await getDcmtmIdByMaterialCode({ materialCode });
+
+ if (!dcmtmResult.success || !dcmtmResult.files) {
+ return {
+ exists: false,
+ fileCount: 0,
+ error: dcmtmResult.error,
+ };
+ }
+
+ return {
+ exists: dcmtmResult.files.length > 0,
+ fileCount: dcmtmResult.files.length,
+ files: dcmtmResult.files.map(file => ({
+ fileName: file.fileName,
+ dcmtmId: file.dcmtmId,
+ projNo: file.projNo,
+ posNo: file.posNo,
+ posRevNo: file.posRevNo,
+ fileSer: file.fileSer,
+ })),
+ };
+ } catch (error) {
+ return {
+ exists: false,
+ fileCount: 0,
+ error: error instanceof Error ? error.message : '알 수 없는 오류',
+ };
+ }
+}
+
diff --git a/lib/pos/index.ts b/lib/pos/index.ts
index 75309bdd..d889f8c1 100644
--- a/lib/pos/index.ts
+++ b/lib/pos/index.ts
@@ -1,4 +1,16 @@
-// POS 관련 모든 기능을 하나로 통합하는 인덱스 파일
+/**
+ * POS (Purchase Order Specification) 파일 관련 기능 통합 모듈
+ *
+ * 주요 기능:
+ * - MATNR(자재코드)로 DCMTM_ID 조회
+ * - SOAP API를 통한 POS 파일 경로 조회
+ * - NFS 네트워크 드라이브에서 파일 다운로드
+ * - 온디맨드 방식의 POS 파일 다운로드 (자동 동기화 제거됨)
+ *
+ * 주요 변경사항:
+ * - syncRfqPosFiles 함수 제거됨 (온디맨드 방식으로 대체)
+ * - getDownloadUrlByMaterialCode 등 새로운 온디맨드 함수 추가
+ */
import { getEncryptDocumentumFile } from './get-pos';
import { createDownloadUrl } from './download-pos-file';
@@ -20,16 +32,18 @@ export {
} from './get-dcmtm-id';
export {
- syncRfqPosFiles
-} from './sync-rfq-pos-files';
-
-export {
getDesignDocumentByMaterialCode,
getDesignDocumentsForRfqItems,
getDesignDocumentByMaterialCodeAction,
getDesignDocumentsForRfqItemsAction
} from './design-document-service';
+export {
+ getDownloadUrlByMaterialCode,
+ getDownloadUrlsForMaterialCodes,
+ checkPosFileExists
+} from './download-on-demand-action';
+
// 타입들은 ./types 에서 export
export type * from './types';
diff --git a/lib/pos/sync-rfq-pos-files.ts b/lib/pos/sync-rfq-pos-files.ts
deleted file mode 100644
index acb34f20..00000000
--- a/lib/pos/sync-rfq-pos-files.ts
+++ /dev/null
@@ -1,407 +0,0 @@
-'use server';
-
-import db from '@/db/db';
-import { rfqPrItems, rfqLastAttachments, rfqLastAttachmentRevisions } from '@/db/schema/rfqLast';
-import { eq, and, ne } from 'drizzle-orm';
-import { getDcmtmIdByMaterialCode } from './get-dcmtm-id';
-import { getEncryptDocumentumFile } from './get-pos';
-import { downloadPosFile } from './download-pos-file';
-import type { PosFileSyncResult } from './types';
-import path from 'path';
-import fs from 'fs/promises';
-import { revalidatePath } from 'next/cache';
-import { debugLog, debugError, debugSuccess, debugProcess, debugWarn } from '@/lib/debug-utils';
-
-
-/**
- * RFQ의 모든 PR Items의 MATNR로 POS 파일을 조회하고 서버에 다운로드하여 저장
- */
-export async function syncRfqPosFiles(
- rfqId: number,
- userId: number
-): Promise<PosFileSyncResult> {
- debugLog(`🚀 POS 파일 동기화 시작`, { rfqId, userId });
-
- const result: PosFileSyncResult = {
- success: false,
- processedCount: 0,
- successCount: 0,
- failedCount: 0,
- errors: [],
- details: []
- };
-
- try {
- // 1. RFQ의 모든 PR Items 조회 (materialCode 중복 제거)
- debugProcess(`📋 RFQ PR Items 조회 시작 (RFQ ID: ${rfqId})`);
-
- const prItems = await db
- .selectDistinct({
- materialCode: rfqPrItems.materialCode,
- materialDescription: rfqPrItems.materialDescription,
- })
- .from(rfqPrItems)
- .where(and(
- eq(rfqPrItems.rfqsLastId, rfqId),
- // materialCode가 null이 아닌 것만
- ))
- .then(items => items.filter(item => item.materialCode && item.materialCode.trim() !== ''));
-
- debugLog(`📦 조회된 PR Items`, {
- totalCount: prItems.length,
- materialCodes: prItems.map(item => item.materialCode)
- });
-
- if (prItems.length === 0) {
- debugWarn(`⚠️ 처리할 자재코드가 없습니다 (RFQ ID: ${rfqId})`);
- result.errors.push('처리할 자재코드가 없습니다.');
- return result;
- }
-
- result.processedCount = prItems.length;
- debugSuccess(`✅ 처리할 자재코드 ${prItems.length}개 발견`);
-
- // 2. 각 자재코드별로 POS 파일 처리
- debugProcess(`🔄 자재코드별 POS 파일 처리 시작`);
-
- for (const prItem of prItems) {
- const materialCode = prItem.materialCode!;
- debugLog(`📋 자재코드 처리 시작: ${materialCode}`);
-
- try {
- // 2-1. 자재코드로 DCMTM_ID 조회
- debugProcess(`🔍 DCMTM_ID 조회 시작 (자재코드: ${materialCode})`);
- const dcmtmResult = await getDcmtmIdByMaterialCode({ materialCode });
-
- debugLog(`🎯 DCMTM_ID 조회 결과`, {
- materialCode,
- success: dcmtmResult.success,
- fileCount: dcmtmResult.files?.length || 0,
- files: dcmtmResult.files
- });
-
- if (!dcmtmResult.success || !dcmtmResult.files || dcmtmResult.files.length === 0) {
- debugWarn(`⚠️ DCMTM_ID 조회 실패 또는 파일 없음 (자재코드: ${materialCode})`, dcmtmResult.error);
- result.details.push({
- materialCode,
- status: 'no_files',
- error: dcmtmResult.error || 'POS 파일을 찾을 수 없음'
- });
- continue;
- }
-
- // 여러 파일이 있을 경우 첫 번째 파일만 처리
- const posFile = dcmtmResult.files[0];
- debugLog(`📁 처리할 POS 파일 선택`, {
- materialCode,
- selectedFile: posFile,
- totalFiles: dcmtmResult.files.length
- });
-
- // 2-2. POS API로 파일 경로 가져오기
- debugProcess(`🌐 POS API 호출 시작 (DCMTM_ID: ${posFile.dcmtmId})`);
- const posResult = await getEncryptDocumentumFile({
- objectID: posFile.dcmtmId
- });
-
- debugLog(`🌐 POS API 호출 결과`, {
- materialCode,
- dcmtmId: posFile.dcmtmId,
- success: posResult.success,
- resultPath: posResult.result,
- error: posResult.error
- });
-
- if (!posResult.success || !posResult.result) {
- debugError(`❌ POS API 호출 실패 (자재코드: ${materialCode})`, posResult.error);
- result.details.push({
- materialCode,
- fileName: posFile.fileName,
- status: 'failed',
- error: posResult.error || 'POS 파일 경로 조회 실패'
- });
- result.failedCount++;
- continue;
- }
-
- // 2-3. 내부망에서 파일 다운로드
- debugProcess(`⬇️ 파일 다운로드 시작 (경로: ${posResult.result})`);
- const downloadResult = await downloadPosFile({
- relativePath: posResult.result
- });
-
- debugLog(`⬇️ 파일 다운로드 결과`, {
- materialCode,
- success: downloadResult.success,
- fileName: downloadResult.fileName,
- fileSize: downloadResult.fileBuffer?.length,
- mimeType: downloadResult.mimeType,
- error: downloadResult.error
- });
-
- if (!downloadResult.success || !downloadResult.fileBuffer) {
- debugError(`❌ 파일 다운로드 실패 (자재코드: ${materialCode})`, downloadResult.error);
- result.details.push({
- materialCode,
- fileName: posFile.fileName,
- status: 'failed',
- error: downloadResult.error || '파일 다운로드 실패'
- });
- result.failedCount++;
- continue;
- }
-
- // 2-4. 서버 파일 시스템에 저장
- debugProcess(`💾 서버 파일 저장 시작 (파일명: ${downloadResult.fileName || `${materialCode}.pdf`})`);
- const saveResult = await saveFileToServer(
- downloadResult.fileBuffer,
- downloadResult.fileName || `${materialCode}.pdf`
- );
-
- debugLog(`💾 서버 파일 저장 결과`, {
- materialCode,
- success: saveResult.success,
- filePath: saveResult.filePath,
- fileName: saveResult.fileName,
- error: saveResult.error
- });
-
- if (!saveResult.success) {
- debugError(`❌ 서버 파일 저장 실패 (자재코드: ${materialCode})`, saveResult.error);
- result.details.push({
- materialCode,
- fileName: posFile.fileName,
- status: 'failed',
- error: saveResult.error || '서버 파일 저장 실패'
- });
- result.failedCount++;
- continue;
- }
-
- // 2-5. 데이터베이스에 첨부파일 정보 저장
- debugProcess(`🗄️ DB 첨부파일 정보 저장 시작 (자재코드: ${materialCode})`);
- const dbResult = await saveAttachmentToDatabase(
- rfqId,
- materialCode,
- { dcmtmId: posFile.dcmtmId, fileName: posFile.fileName },
- saveResult.filePath!,
- saveResult.fileName!,
- downloadResult.fileBuffer.length,
- downloadResult.mimeType || 'application/pdf',
- userId
- );
-
- debugLog(`🗄️ DB 저장 결과`, {
- materialCode,
- success: dbResult.success,
- error: dbResult.error
- });
-
- if (!dbResult.success) {
- debugError(`❌ DB 저장 실패 (자재코드: ${materialCode})`, dbResult.error);
- result.details.push({
- materialCode,
- fileName: posFile.fileName,
- status: 'failed',
- error: dbResult.error || 'DB 저장 실패'
- });
- result.failedCount++;
- continue;
- }
-
- debugSuccess(`✅ 자재코드 ${materialCode} 처리 완료`);
- result.details.push({
- materialCode,
- fileName: posFile.fileName,
- status: 'success'
- });
- result.successCount++;
-
- } catch (error) {
- const errorMessage = error instanceof Error ? error.message : '알 수 없는 오류';
- debugError(`❌ 자재코드 ${materialCode} 처리 중 예외 발생`, { error: errorMessage, stack: error instanceof Error ? error.stack : undefined });
- result.details.push({
- materialCode,
- status: 'failed',
- error: errorMessage
- });
- result.failedCount++;
- result.errors.push(`${materialCode}: ${errorMessage}`);
- }
- }
-
- result.success = result.successCount > 0;
-
- debugLog(`📊 POS 파일 동기화 최종 결과`, {
- rfqId,
- processedCount: result.processedCount,
- successCount: result.successCount,
- failedCount: result.failedCount,
- success: result.success,
- errors: result.errors
- });
-
- // 캐시 무효화
- debugProcess(`🔄 캐시 무효화 (경로: /evcp/rfq-last/${rfqId})`);
- revalidatePath(`/evcp/rfq-last/${rfqId}`);
-
- debugSuccess(`🎉 POS 파일 동기화 완료 (성공: ${result.successCount}건, 실패: ${result.failedCount}건)`);
- return result;
-
- } catch (error) {
- const errorMessage = error instanceof Error ? error.message : '알 수 없는 오류';
- debugError(`❌ POS 파일 동기화 전체 처리 오류`, { error: errorMessage, stack: error instanceof Error ? error.stack : undefined });
- result.errors.push(`전체 처리 오류: ${errorMessage}`);
- return result;
- }
-}
-
-/**
- * 파일을 서버 파일 시스템에 저장
- */
-async function saveFileToServer(
- fileBuffer: Buffer,
- originalFileName: string
-): Promise<{
- success: boolean;
- filePath?: string;
- fileName?: string;
- error?: string;
-}> {
- try {
- // uploads/pos 디렉토리 생성
- const uploadDir = path.join(process.cwd(), 'uploads', 'pos');
-
- try {
- await fs.access(uploadDir);
- } catch {
- await fs.mkdir(uploadDir, { recursive: true });
- }
-
- // 고유한 파일명 생성 (타임스탬프 + 원본명)
- const timestamp = Date.now();
- const sanitizedFileName = originalFileName.replace(/[^a-zA-Z0-9.-]/g, '_');
- const fileName = `${timestamp}_${sanitizedFileName}`;
- const filePath = path.join(uploadDir, fileName);
-
- // 파일 저장
- await fs.writeFile(filePath, fileBuffer);
-
- return {
- success: true,
- filePath: `uploads/pos/${fileName}`, // 상대 경로로 저장
- fileName: originalFileName
- };
- } catch (error) {
- return {
- success: false,
- error: error instanceof Error ? error.message : '파일 저장 실패'
- };
- }
-}
-
-/**
- * 첨부파일 정보를 데이터베이스에 저장
- */
-async function saveAttachmentToDatabase(
- rfqId: number,
- materialCode: string,
- posFileInfo: { dcmtmId: string; fileName: string },
- filePath: string,
- originalFileName: string,
- fileSize: number,
- fileType: string,
- userId: number
-): Promise<{
- success: boolean;
- error?: string;
-}> {
- try {
- await db.transaction(async (tx) => {
- // 1. 기존 동일한 자재코드의 설계 첨부파일이 있는지 확인
- const existingAttachment = await tx
- .select()
- .from(rfqLastAttachments)
- .where(and(
- eq(rfqLastAttachments.rfqId, rfqId),
- eq(rfqLastAttachments.attachmentType, '설계'),
- eq(rfqLastAttachments.serialNo, materialCode)
- ))
- .limit(1);
-
- let attachmentId: number;
-
- if (existingAttachment.length > 0) {
- // 기존 첨부파일이 있으면 업데이트
- attachmentId = existingAttachment[0].id;
-
- await tx
- .update(rfqLastAttachments)
- .set({
- currentRevision: 'Rev.1', // 새 리비전으로 업데이트
- description: `${posFileInfo.fileName} (자재코드: ${materialCode})`,
- updatedAt: new Date()
- })
- .where(eq(rfqLastAttachments.id, attachmentId));
- } else {
- // 새 첨부파일 생성
- const [newAttachment] = await tx
- .insert(rfqLastAttachments)
- .values({
- attachmentType: '설계',
- serialNo: materialCode,
- rfqId,
- currentRevision: 'Rev.0',
- description: `${posFileInfo.fileName} (자재코드: ${materialCode})`,
- createdBy: userId,
- createdAt: new Date(),
- updatedAt: new Date()
- })
- .returning({ id: rfqLastAttachments.id });
-
- attachmentId = newAttachment.id;
- }
-
- // 2. 새 리비전 생성
- const [newRevision] = await tx
- .insert(rfqLastAttachmentRevisions)
- .values({
- attachmentId,
- revisionNo: 'Rev.0',
- fileName: path.basename(filePath),
- originalFileName,
- filePath,
- fileSize,
- fileType,
- isLatest: true,
- revisionComment: `POS 시스템에서 자동 동기화됨 (DCMTM_ID: ${posFileInfo.dcmtmId})`,
- createdBy: userId
- })
- .returning({ id: rfqLastAttachmentRevisions.id });
-
- // 3. 첨부파일의 latestRevisionId 업데이트
- await tx
- .update(rfqLastAttachments)
- .set({
- latestRevisionId: newRevision.id
- })
- .where(eq(rfqLastAttachments.id, attachmentId));
-
- // 4. 기존 리비전들의 isLatest를 false로 업데이트
- await tx
- .update(rfqLastAttachmentRevisions)
- .set({ isLatest: false })
- .where(and(
- eq(rfqLastAttachmentRevisions.attachmentId, attachmentId),
- ne(rfqLastAttachmentRevisions.id, newRevision.id)
- ));
- });
-
- return { success: true };
- } catch (error) {
- return {
- success: false,
- error: error instanceof Error ? error.message : 'DB 저장 실패'
- };
- }
-}
diff --git a/lib/rfq-last/table/rfq-items-dialog.tsx b/lib/rfq-last/table/rfq-items-dialog.tsx
index c640f3bd..5f8e4382 100644
--- a/lib/rfq-last/table/rfq-items-dialog.tsx
+++ b/lib/rfq-last/table/rfq-items-dialog.tsx
@@ -26,8 +26,8 @@ import { Separator } from "@/components/ui/separator"
import { toast } from "sonner"
import { RfqsLastView } from "@/db/schema"
import { getRfqItemsAction } from "../service"
-import { getDesignDocumentsForRfqItemsAction } from "@/lib/pos"
-import { downloadFile } from "@/lib/file-download"
+import { getDownloadUrlByMaterialCode, checkPosFileExists } from "@/lib/pos"
+import { PosFileSelectionDialog } from "@/lib/pos/components/pos-file-selection-dialog"
// 품목 타입
interface RfqItem {
@@ -84,27 +84,29 @@ export function RfqItemsDialog({ isOpen, onClose, rfqData }: RfqItemsDialogProps
const [items, setItems] = React.useState<RfqItem[]>([])
const [statistics, setStatistics] = React.useState<ItemStatistics | null>(null)
const [isLoading, setIsLoading] = React.useState(false)
- // 자재코드별 설계 문서 매핑
- const [designDocuments, setDesignDocuments] = React.useState<Record<string, {
- id: number;
- fileName: string;
- filePath: string;
- fileSize: number | null;
- fileType: string | null;
- description: string | null;
- }>>({})
- const [isLoadingDocs, setIsLoadingDocs] = React.useState(false)
+ // POS 파일 선택 다이얼로그 상태
+ const [posDialogOpen, setPosDialogOpen] = React.useState(false)
+ const [selectedMaterialCode, setSelectedMaterialCode] = React.useState<string>("")
+ const [posFiles, setPosFiles] = React.useState<Array<{
+ fileName: string
+ dcmtmId: string
+ projNo: string
+ posNo: string
+ posRevNo: string
+ fileSer: string
+ }>>([])
+ const [loadingPosFiles, setLoadingPosFiles] = React.useState(false)
+ const [downloadingFileIndex, setDownloadingFileIndex] = React.useState<number | null>(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
)}
</div>
- {/* 설계 문서 다운로드 */}
- {item.materialCode && designDocuments[item.materialCode] && (
+ {/* POS 파일 다운로드 */}
+ {item.materialCode && (
<div className="flex items-center gap-1">
- <FileText className="h-3 w-3 text-blue-500" />
+ <FileText className="h-3 w-3 text-green-500" />
<Button
variant="ghost"
size="sm"
- className="h-5 p-1 text-xs text-blue-600 hover:text-blue-800"
- onClick={() => handleDownloadDesignDoc(
- item.materialCode!,
- designDocuments[item.materialCode!].fileName,
- designDocuments[item.materialCode!].filePath
- )}
- title={`설계문서: ${designDocuments[item.materialCode!].fileName} (${formatFileSize(designDocuments[item.materialCode!].fileSize)})`}
+ className="h-5 p-1 text-xs text-green-600 hover:text-green-800"
+ onClick={() => handleOpenPosDialog(item.materialCode!)}
+ disabled={loadingPosFiles && selectedMaterialCode === item.materialCode}
+ title={`POS 파일 다운로드 (자재코드: ${item.materialCode})`}
>
<Download className="h-3 w-3 mr-1" />
- 설계문서
+ {loadingPosFiles && selectedMaterialCode === item.materialCode ? '조회중...' : 'POS 다운로드'}
</Button>
</div>
)}
@@ -388,13 +423,6 @@ export function RfqItemsDialog({ isOpen, onClose, rfqData }: RfqItemsDialogProps
TRK: {item.trackingNo}
</div>
)}
-
- {/* 설계 문서 로딩 상태 */}
- {isLoadingDocs && item.materialCode && (
- <div className="text-xs text-muted-foreground">
- 설계문서 확인 중...
- </div>
- )}
</div>
</TableCell>
<TableCell>
@@ -435,6 +463,16 @@ export function RfqItemsDialog({ isOpen, onClose, rfqData }: RfqItemsDialogProps
</div>
</div>
)}
+
+ {/* POS 파일 선택 다이얼로그 */}
+ <PosFileSelectionDialog
+ isOpen={posDialogOpen}
+ onClose={handleClosePosDialog}
+ materialCode={selectedMaterialCode}
+ files={posFiles}
+ onDownload={handleDownloadPosFile}
+ downloadingIndex={downloadingFileIndex}
+ />
</DialogContent>
</Dialog>
)
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,