summaryrefslogtreecommitdiff
path: root/lib/pos
diff options
context:
space:
mode:
authorjoonhoekim <26rote@gmail.com>2025-09-26 14:13:20 +0900
committerjoonhoekim <26rote@gmail.com>2025-09-26 14:13:20 +0900
commitf8fc02e175f93466cd7693eb6e549c45362e785b (patch)
tree1037ec1f9225b0a0142defd6a27c68c3e6a47009 /lib/pos
parent11bc8239ad474a8f31c1c73de51f7d0f101594df (diff)
(김준회) POS 및 구매 피드백 처리
- 요구사항 28.(0.1) 24번 행 (prItem번호 별도 표기) - pos nfs 경로에서 가져오도록 수정개발
Diffstat (limited to 'lib/pos')
-rw-r--r--lib/pos/design-document-service.ts188
-rw-r--r--lib/pos/download-pos-file.ts54
-rw-r--r--lib/pos/get-pos.ts238
-rw-r--r--lib/pos/index.ts7
-rw-r--r--lib/pos/types.ts22
5 files changed, 488 insertions, 21 deletions
diff --git a/lib/pos/design-document-service.ts b/lib/pos/design-document-service.ts
new file mode 100644
index 00000000..8f213d57
--- /dev/null
+++ b/lib/pos/design-document-service.ts
@@ -0,0 +1,188 @@
+'use server';
+
+import db from '@/db/db';
+import { rfqLastAttachments, rfqLastAttachmentRevisions } from '@/db/schema/rfqLast';
+import { eq, and } from 'drizzle-orm';
+
+/**
+ * 자재코드별 설계 문서 조회
+ */
+export async function getDesignDocumentByMaterialCode(
+ rfqId: number,
+ materialCode: string
+): Promise<{
+ success: boolean;
+ document?: {
+ id: number;
+ attachmentType: string;
+ serialNo: string;
+ description: string | null;
+ latestRevisionId: number | null;
+ fileName: string;
+ filePath: string;
+ fileSize: number | null;
+ fileType: string | null;
+ };
+ error?: string;
+}> {
+ try {
+ const result = await db
+ .select({
+ attachmentId: rfqLastAttachments.id,
+ attachmentType: rfqLastAttachments.attachmentType,
+ serialNo: rfqLastAttachments.serialNo,
+ description: rfqLastAttachments.description,
+ latestRevisionId: rfqLastAttachments.latestRevisionId,
+ fileName: rfqLastAttachmentRevisions.fileName,
+ filePath: rfqLastAttachmentRevisions.filePath,
+ fileSize: rfqLastAttachmentRevisions.fileSize,
+ fileType: rfqLastAttachmentRevisions.fileType,
+ })
+ .from(rfqLastAttachments)
+ .leftJoin(
+ rfqLastAttachmentRevisions,
+ eq(rfqLastAttachments.latestRevisionId, rfqLastAttachmentRevisions.id)
+ )
+ .where(
+ and(
+ eq(rfqLastAttachments.rfqId, rfqId),
+ eq(rfqLastAttachments.attachmentType, '설계'),
+ eq(rfqLastAttachments.serialNo, materialCode)
+ )
+ )
+ .limit(1);
+
+ if (result.length === 0) {
+ return {
+ success: false,
+ error: '설계 문서를 찾을 수 없습니다.'
+ };
+ }
+
+ const doc = result[0];
+ return {
+ success: true,
+ document: {
+ id: doc.attachmentId,
+ attachmentType: doc.attachmentType,
+ serialNo: doc.serialNo,
+ description: doc.description,
+ latestRevisionId: doc.latestRevisionId,
+ fileName: doc.fileName || '',
+ filePath: doc.filePath || '',
+ fileSize: doc.fileSize,
+ fileType: doc.fileType,
+ }
+ };
+ } catch (error) {
+ console.error('설계 문서 조회 오류:', error);
+ return {
+ success: false,
+ error: error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.'
+ };
+ }
+}
+
+/**
+ * RFQ의 모든 PR Items에 대한 설계 문서 매핑 조회
+ */
+export async function getDesignDocumentsForRfqItems(
+ rfqId: number
+): Promise<{
+ success: boolean;
+ documents?: Record<string, {
+ id: number;
+ fileName: string;
+ filePath: string;
+ fileSize: number | null;
+ fileType: string | null;
+ description: string | null;
+ }>;
+ error?: string;
+}> {
+ try {
+ const result = await db
+ .select({
+ materialCode: rfqLastAttachments.serialNo,
+ attachmentId: rfqLastAttachments.id,
+ description: rfqLastAttachments.description,
+ fileName: rfqLastAttachmentRevisions.fileName,
+ filePath: rfqLastAttachmentRevisions.filePath,
+ fileSize: rfqLastAttachmentRevisions.fileSize,
+ fileType: rfqLastAttachmentRevisions.fileType,
+ })
+ .from(rfqLastAttachments)
+ .leftJoin(
+ rfqLastAttachmentRevisions,
+ eq(rfqLastAttachments.latestRevisionId, rfqLastAttachmentRevisions.id)
+ )
+ .where(
+ and(
+ eq(rfqLastAttachments.rfqId, rfqId),
+ eq(rfqLastAttachments.attachmentType, '설계')
+ )
+ );
+
+ const documentsMap: Record<string, any> = {};
+
+ for (const doc of result) {
+ if (doc.materialCode) {
+ documentsMap[doc.materialCode] = {
+ id: doc.attachmentId,
+ fileName: doc.fileName || '',
+ filePath: doc.filePath || '',
+ fileSize: doc.fileSize,
+ fileType: doc.fileType,
+ description: doc.description,
+ };
+ }
+ }
+
+ return {
+ success: true,
+ documents: documentsMap
+ };
+ } catch (error) {
+ console.error('설계 문서 매핑 조회 오류:', error);
+ return {
+ success: false,
+ error: error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.'
+ };
+ }
+}
+
+/**
+ * 자재코드별 설계 문서 조회 서버 액션
+ */
+export async function getDesignDocumentByMaterialCodeAction(
+ rfqId: number,
+ materialCode: string
+) {
+ try {
+ const result = await getDesignDocumentByMaterialCode(rfqId, materialCode);
+ return result;
+ } catch (error) {
+ console.error("Error in getDesignDocumentByMaterialCodeAction:", error);
+ return {
+ success: false,
+ error: "설계 문서를 불러오는데 실패했습니다.",
+ };
+ }
+}
+
+/**
+ * RFQ Items 설계 문서 매핑 조회 서버 액션
+ */
+export async function getDesignDocumentsForRfqItemsAction(rfqId: number) {
+ try {
+ const result = await getDesignDocumentsForRfqItems(rfqId);
+ return result;
+ } catch (error) {
+ console.error("Error in getDesignDocumentsForRfqItemsAction:", error);
+ return {
+ success: false,
+ error: "설계 문서 매핑을 불러오는데 실패했습니다.",
+ documents: {},
+ };
+ }
+}
diff --git a/lib/pos/download-pos-file.ts b/lib/pos/download-pos-file.ts
index d285c618..9fd6867f 100644
--- a/lib/pos/download-pos-file.ts
+++ b/lib/pos/download-pos-file.ts
@@ -3,20 +3,29 @@
import fs from 'fs/promises';
import path from 'path';
import type { DownloadPosFileParams, DownloadPosFileResult } from './types';
+import { DOCUMENTUM_NFS_PATH } from './types';
import { debugLog, debugError, debugSuccess, debugProcess } from '@/lib/debug-utils';
-const POS_BASE_PATH = '\\\\60.100.99.123\\ECM_NAS_PRM\\Download';
+// 레거시: Windows 네트워크 경로 (하위 호환성을 위해 유지)
+const POS_BASE_PATH_LEGACY = '\\\\60.100.99.123\\ECM_NAS_PRM\\Download';
+
+// NFS 마운트 경로 사용
+const POS_BASE_PATH = path.posix.join(DOCUMENTUM_NFS_PATH, 'Download');
/**
- * POS API에서 반환된 경로의 파일을 내부망에서 읽어서 클라이언트에게 전달
- * 내부망 파일을 직접 접근할 수 없는 클라이언트를 위한 프록시 역할
+ * POS API에서 반환된 경로의 파일을 NFS 마운트를 통해 읽어서 클라이언트에게 전달
+ * NFS 마운트된 Documentum 저장소에서 파일을 직접 접근하여 제공
*/
export async function downloadPosFile(
params: DownloadPosFileParams
): Promise<DownloadPosFileResult> {
try {
const { relativePath } = params;
- debugLog(`⬇️ POS 파일 다운로드 시작`, { relativePath, basePath: POS_BASE_PATH });
+ debugLog(`⬇️ POS 파일 다운로드 시작 (NFS)`, {
+ relativePath,
+ nfsBasePath: DOCUMENTUM_NFS_PATH,
+ basePath: POS_BASE_PATH
+ });
if (!relativePath) {
debugError(`❌ 파일 경로가 제공되지 않음`);
@@ -26,21 +35,28 @@ export async function downloadPosFile(
};
}
- // Windows 경로 구분자를 정규화
- const normalizedRelativePath = relativePath.replace(/\\/g, path.sep);
- debugLog(`📁 경로 정규화`, { original: relativePath, normalized: normalizedRelativePath });
+ // Windows 경로 구분자를 Unix 스타일로 정규화 (NFS용)
+ const normalizedRelativePath = relativePath.replace(/\\/g, '/');
+ debugLog(`📁 경로 정규화 (NFS)`, {
+ original: relativePath,
+ normalized: normalizedRelativePath
+ });
- // 전체 파일 경로 구성
- const fullPath = path.join(POS_BASE_PATH, normalizedRelativePath);
- debugLog(`📍 전체 파일 경로 구성`, { fullPath });
+ // NFS 마운트 경로와 상대 경로를 결합
+ const fullPath = path.posix.join(POS_BASE_PATH, normalizedRelativePath);
+ debugLog(`📍 NFS 전체 파일 경로 구성`, { fullPath });
// 경로 보안 검증 (디렉토리 탈출 방지)
const resolvedPath = path.resolve(fullPath);
- const resolvedBasePath = path.resolve(POS_BASE_PATH);
- debugLog(`🔒 경로 보안 검증`, { resolvedPath, resolvedBasePath, isValid: resolvedPath.startsWith(resolvedBasePath) });
+ const resolvedBasePath = path.resolve(DOCUMENTUM_NFS_PATH);
+ debugLog(`🔒 경로 보안 검증 (NFS)`, {
+ resolvedPath,
+ resolvedBasePath,
+ isValid: resolvedPath.startsWith(resolvedBasePath)
+ });
if (!resolvedPath.startsWith(resolvedBasePath)) {
- debugError(`❌ 디렉토리 탈출 시도 감지`, { resolvedPath, resolvedBasePath });
+ debugError(`❌ 디렉토리 탈출 시도 감지 (NFS)`, { resolvedPath, resolvedBasePath });
return {
success: false,
error: '잘못된 파일 경로입니다.',
@@ -48,7 +64,7 @@ export async function downloadPosFile(
}
// 파일 존재 여부 확인
- debugProcess(`🔍 파일 존재 여부 확인 중...`);
+ debugProcess(`🔍 NFS 파일 존재 여부 확인 중...`);
try {
await fs.access(resolvedPath);
debugSuccess(`✅ 파일 존재 확인됨 (${resolvedPath})`);
@@ -95,10 +111,11 @@ export async function downloadPosFile(
const mimeType = await getMimeType(fileName);
debugLog(`🔍 MIME 타입 추정 완료`, { fileName, mimeType });
- debugSuccess(`✅ POS 파일 다운로드 성공`, {
+ debugSuccess(`✅ NFS를 통한 POS 파일 다운로드 성공`, {
fileName,
fileSize: fileBuffer.length,
- mimeType
+ mimeType,
+ nfsPath: resolvedPath
});
return {
@@ -108,8 +125,9 @@ export async function downloadPosFile(
mimeType,
};
} catch (error) {
- debugError('❌ POS 파일 다운로드 실패', {
- relativePath: params.relativePath,
+ debugError('❌ NFS를 통한 POS 파일 다운로드 실패', {
+ relativePath: params.relativePath,
+ nfsBasePath: DOCUMENTUM_NFS_PATH,
error: error instanceof Error ? error.message : '알 수 없는 오류',
stack: error instanceof Error ? error.stack : undefined
});
diff --git a/lib/pos/get-pos.ts b/lib/pos/get-pos.ts
index c24c1dab..a77de247 100644
--- a/lib/pos/get-pos.ts
+++ b/lib/pos/get-pos.ts
@@ -2,14 +2,23 @@
import { XMLBuilder, XMLParser } from 'fast-xml-parser';
import { withSoapLogging } from '@/lib/soap/utils';
-import type { GetEncryptDocumentumFileParams } from './types';
-import { POS_SOAP_ENDPOINT } from './types';
+import fs from 'fs/promises';
+import path from 'path';
+import type { GetEncryptDocumentumFileParams, AccessNfsFileParams, AccessNfsFileResult } from './types';
+import { POS_SOAP_ENDPOINT, DOCUMENTUM_NFS_PATH } from './types';
import { debugLog, debugError, debugSuccess, debugProcess } from '@/lib/debug-utils';
/**
* 문서 암호화 파일을 서버에 다운로드하고 경로를 반환하는 POS(Documentum) SOAP 액션
* 반환값은 서버 내 파일 다운로드 경로입니다. (예: "asd_as_2509131735233768_OP02\asd_as_2509131735233768_OP02.tif")
- * 실제 파일은 \\60.100.99.123\ECM_NAS_PRM\Download\ 경로에 저장됩니다.
+ *
+ * 실제 파일은 DOCUMENTUM_NFS 환경변수로 지정된 NFS 마운트 경로에서 접근할 수 있습니다.
+ * - 환경변수: DOCUMENTUM_NFS (기본값: "/mnt/nfs-documentum/")
+ * - 실제 파일 경로: ${DOCUMENTUM_NFS}/Download/${반환된_상대_경로}
+ *
+ * 예시:
+ * - SOAP 반환값: "asd_as_2509131735233768_OP02\asd_as_2509131735233768_OP02.tif"
+ * - 실제 NFS 파일 경로: "/mnt/nfs-documentum/Download/asd_as_2509131735233768_OP02/asd_as_2509131735233768_OP02.tif"
*/
export async function getEncryptDocumentumFile(
params: GetEncryptDocumentumFileParams
@@ -173,3 +182,226 @@ export async function getEncryptDocumentumFile(
};
}
}
+
+/**
+ * NFS 마운트를 통해 POS 파일에 직접 접근하는 함수
+ * POS API에서 반환된 상대 경로를 사용하여 NFS 마운트된 경로에서 파일을 읽습니다
+ */
+export async function accessNfsFile(
+ params: AccessNfsFileParams
+): Promise<AccessNfsFileResult> {
+ try {
+ const { relativePath } = params;
+ debugLog(`📁 NFS를 통한 POS 파일 접근 시작`, {
+ relativePath,
+ nfsBasePath: DOCUMENTUM_NFS_PATH
+ });
+
+ if (!relativePath) {
+ debugError(`❌ 파일 경로가 제공되지 않음`);
+ return {
+ success: false,
+ error: '파일 경로가 제공되지 않았습니다.',
+ };
+ }
+
+ // Windows 경로 구분자를 Unix 스타일로 정규화
+ const normalizedRelativePath = relativePath.replace(/\\/g, '/');
+ debugLog(`📁 경로 정규화`, {
+ original: relativePath,
+ normalized: normalizedRelativePath
+ });
+
+ // NFS 마운트 경로와 상대 경로를 결합
+ // Download 폴더를 추가하여 전체 경로 구성
+ const fullPath = path.posix.join(DOCUMENTUM_NFS_PATH, 'Download', normalizedRelativePath);
+ debugLog(`📍 NFS 전체 파일 경로 구성`, { fullPath });
+
+ // 경로 보안 검증 (디렉토리 탈출 방지)
+ const resolvedPath = path.resolve(fullPath);
+ const resolvedBasePath = path.resolve(DOCUMENTUM_NFS_PATH);
+ debugLog(`🔒 경로 보안 검증`, {
+ resolvedPath,
+ resolvedBasePath,
+ isValid: resolvedPath.startsWith(resolvedBasePath)
+ });
+
+ if (!resolvedPath.startsWith(resolvedBasePath)) {
+ debugError(`❌ 디렉토리 탈출 시도 감지`, { resolvedPath, resolvedBasePath });
+ return {
+ success: false,
+ error: '잘못된 파일 경로입니다.',
+ };
+ }
+
+ // 파일 존재 여부 확인
+ debugProcess(`🔍 NFS 파일 존재 여부 확인 중...`);
+ try {
+ await fs.access(resolvedPath);
+ debugSuccess(`✅ NFS 파일 접근 가능 확인`);
+ } catch (accessError) {
+ debugError(`❌ NFS 파일 접근 불가`, {
+ path: resolvedPath,
+ error: accessError instanceof Error ? accessError.message : 'Unknown error'
+ });
+ return {
+ success: false,
+ error: `파일을 찾을 수 없습니다: ${resolvedPath}`,
+ };
+ }
+
+ // 파일 정보 확인
+ debugProcess(`📊 NFS 파일 정보 확인 중...`);
+ const stats = await fs.stat(resolvedPath);
+ debugLog(`📊 NFS 파일 정보`, {
+ size: stats.size,
+ isFile: stats.isFile(),
+ modified: stats.mtime
+ });
+
+ if (!stats.isFile()) {
+ debugError(`❌ 대상이 파일이 아님`, { path: resolvedPath });
+ return {
+ success: false,
+ error: '대상이 파일이 아닙니다.',
+ };
+ }
+
+ // 파일 크기 제한 (10000MB)
+ const maxSize = 10000 * 1024 * 1024; // 10000MB
+ if (stats.size > maxSize) {
+ debugError(`❌ 파일 크기 초과`, {
+ fileSize: stats.size,
+ maxSize,
+ path: resolvedPath
+ });
+ return {
+ success: false,
+ error: `파일 크기가 너무 큽니다 (최대 10000MB). 현재 크기: ${Math.round(stats.size / 1024 / 1024)}MB`,
+ };
+ }
+
+ // 파일 읽기
+ debugProcess(`📖 NFS 파일 읽기 중... (크기: ${stats.size} bytes)`);
+ const fileBuffer = await fs.readFile(resolvedPath);
+ debugSuccess(`✅ NFS 파일 읽기 완료 (${fileBuffer.length} bytes)`);
+
+ // 파일명과 MIME 타입 추출
+ const fileName = path.basename(resolvedPath);
+ const fileExtension = path.extname(fileName).toLowerCase();
+
+ // 확장자별 MIME 타입 설정
+ let mimeType = 'application/octet-stream'; // 기본값
+ switch (fileExtension) {
+ case '.pdf':
+ mimeType = 'application/pdf';
+ break;
+ case '.tif':
+ case '.tiff':
+ mimeType = 'image/tiff';
+ break;
+ case '.png':
+ mimeType = 'image/png';
+ break;
+ case '.jpg':
+ case '.jpeg':
+ mimeType = 'image/jpeg';
+ break;
+ case '.doc':
+ mimeType = 'application/msword';
+ break;
+ case '.docx':
+ mimeType = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
+ break;
+ case '.xls':
+ mimeType = 'application/vnd.ms-excel';
+ break;
+ case '.xlsx':
+ mimeType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
+ break;
+ case '.dwg':
+ mimeType = 'application/acad';
+ break;
+ case '.zip':
+ mimeType = 'application/zip';
+ break;
+ }
+
+ debugSuccess(`✅ NFS를 통한 POS 파일 접근 성공`, {
+ fileName,
+ mimeType,
+ fileSize: fileBuffer.length,
+ fullPath: resolvedPath
+ });
+
+ return {
+ success: true,
+ fileName,
+ fileBuffer,
+ mimeType,
+ fullPath: resolvedPath,
+ };
+
+ } catch (error) {
+ debugError('❌ NFS를 통한 POS 파일 접근 실패', {
+ relativePath: params.relativePath,
+ error: error instanceof Error ? error.message : 'Unknown error',
+ stack: error instanceof Error ? error.stack : undefined
+ });
+ return {
+ success: false,
+ error: error instanceof Error ? error.message : 'Unknown error',
+ };
+ }
+}
+
+/**
+ * POS 파일 접근 방법을 테스트하는 함수
+ * 개발/테스트 환경에서 NFS 마운트가 제대로 작동하는지 확인하는 용도
+ */
+export async function testNfsAccess(): Promise<{
+ success: boolean;
+ nfsPath: string;
+ accessible: boolean;
+ error?: string;
+}> {
+ try {
+ debugLog(`🧪 NFS 접근 테스트 시작`, { nfsPath: DOCUMENTUM_NFS_PATH });
+
+ // NFS 마운트 기본 경로 접근 테스트
+ const testPath = path.posix.join(DOCUMENTUM_NFS_PATH, 'Download');
+ debugLog(`🔍 테스트 경로 확인`, { testPath });
+
+ try {
+ await fs.access(testPath);
+ debugSuccess(`✅ NFS 마운트 경로 접근 성공`, { testPath });
+ return {
+ success: true,
+ nfsPath: DOCUMENTUM_NFS_PATH,
+ accessible: true,
+ };
+ } catch (accessError) {
+ debugError(`❌ NFS 마운트 경로 접근 실패`, {
+ testPath,
+ error: accessError instanceof Error ? accessError.message : 'Unknown error'
+ });
+ return {
+ success: false,
+ nfsPath: DOCUMENTUM_NFS_PATH,
+ accessible: false,
+ error: `NFS 마운트 경로에 접근할 수 없습니다: ${testPath}`,
+ };
+ }
+ } catch (error) {
+ debugError('❌ NFS 접근 테스트 실패', {
+ error: error instanceof Error ? error.message : 'Unknown error',
+ stack: error instanceof Error ? error.stack : undefined
+ });
+ return {
+ success: false,
+ nfsPath: DOCUMENTUM_NFS_PATH,
+ accessible: false,
+ error: error instanceof Error ? error.message : 'Unknown error',
+ };
+ }
+}
diff --git a/lib/pos/index.ts b/lib/pos/index.ts
index 7611d5c5..75309bdd 100644
--- a/lib/pos/index.ts
+++ b/lib/pos/index.ts
@@ -23,6 +23,13 @@ export {
syncRfqPosFiles
} from './sync-rfq-pos-files';
+export {
+ getDesignDocumentByMaterialCode,
+ getDesignDocumentsForRfqItems,
+ getDesignDocumentByMaterialCodeAction,
+ getDesignDocumentsForRfqItemsAction
+} from './design-document-service';
+
// 타입들은 ./types 에서 export
export type * from './types';
diff --git a/lib/pos/types.ts b/lib/pos/types.ts
index eb75c94b..2a1c6076 100644
--- a/lib/pos/types.ts
+++ b/lib/pos/types.ts
@@ -59,6 +59,25 @@ export interface DownloadPosFileParams {
relativePath: string;
}
+/**
+ * NFS 마운트를 통한 파일 접근 파라미터
+ */
+export interface AccessNfsFileParams {
+ /**
+ * POS API에서 반환된 상대 경로
+ */
+ relativePath: string;
+}
+
+export interface AccessNfsFileResult {
+ success: boolean;
+ fileName?: string;
+ fileBuffer?: Buffer;
+ mimeType?: string;
+ fullPath?: string;
+ error?: string;
+}
+
export interface DownloadPosFileResult {
success: boolean;
fileName?: string;
@@ -85,3 +104,6 @@ export interface PosFileSyncResult {
export const POS_SOAP_SEGMENT = '/Documentum/PlmFileBroker.asmx';
export const POS_SOAP_BASE_URL = process.env.POS_SOAP_ENDPOINT || 'http://60.100.99.122:7700';
export const POS_SOAP_ENDPOINT = `${POS_SOAP_BASE_URL}${POS_SOAP_SEGMENT}`;
+
+// NFS 마운트 관련 설정
+export const DOCUMENTUM_NFS_PATH = process.env.DOCUMENTUM_NFS || '/mnt/nfs-documentum/';