diff options
| -rw-r--r-- | .env.development | 6 | ||||
| -rw-r--r-- | .env.production | 4 | ||||
| -rw-r--r-- | app/api/pos/download/route.ts | 47 | ||||
| -rw-r--r-- | lib/pos/download-pos-file.ts | 162 | ||||
| -rw-r--r-- | lib/pos/get-dcmtm-id.ts | 134 | ||||
| -rw-r--r-- | lib/pos/get-pos.ts | 174 | ||||
| -rw-r--r-- | lib/pos/index.ts | 164 | ||||
| -rw-r--r-- | lib/pos/sync-rfq-pos-files.ts | 407 | ||||
| -rw-r--r-- | lib/pos/types.ts | 87 | ||||
| -rw-r--r-- | lib/rfq-last/table/rfq-attachments-dialog.tsx | 80 | ||||
| -rw-r--r-- | lib/soap/ecc/send/create-po.ts | 8 | ||||
| -rw-r--r-- | lib/users/service.ts | 2 |
12 files changed, 1263 insertions, 12 deletions
diff --git a/.env.development b/.env.development index f4017238..603261b2 100644 --- a/.env.development +++ b/.env.development @@ -177,4 +177,8 @@ READONLY_DB_URL="postgresql://readonly:tempReadOnly_123@localhost:5432/evcp" # ν # === λλ²κ·Έ λ‘κΉ
(lib/debug-utils.ts) === NEXT_PUBLIC_DEBUG=true -SWP_BASE_URL=http://60.100.99.217/DDP/Services/VNDRService.svc
\ No newline at end of file +SWP_BASE_URL=http://60.100.99.217/DDP/Services/VNDRService.svc + +# POS (EMLS, Documentum) +ECC_PO_ENDPOINT="http://60.100.99.122:7700" # νμ§ +POS_APP_CODE="SQ13" # νμ§, μ΄μμ κ²½μ° SO13
\ No newline at end of file diff --git a/.env.production b/.env.production index 5214c212..ed881b6f 100644 --- a/.env.production +++ b/.env.production @@ -180,3 +180,7 @@ READONLY_DB_URL="postgresql://readonly:tempReadOnly_123@localhost:5432/evcp" # ν NEXT_PUBLIC_DEBUG=false SWP_BASE_URL=http://60.100.99.217/DDP/Services/VNDRService.svc + +# POS (EMLS, Documentum) +ECC_PO_ENDPOINT="http://60.100.99.122:7700" # νμ§ +POS_APP_CODE="SQ13" # νμ§, μ΄μμ κ²½μ° SO13
\ No newline at end of file diff --git a/app/api/pos/download/route.ts b/app/api/pos/download/route.ts new file mode 100644 index 00000000..5328dc9d --- /dev/null +++ b/app/api/pos/download/route.ts @@ -0,0 +1,47 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { downloadPosFile } from '@/lib/pos/download-pos-file'; + +export async function GET(request: NextRequest) { + try { + const searchParams = request.nextUrl.searchParams; + const relativePath = searchParams.get('path'); + + if (!relativePath) { + return NextResponse.json( + { error: 'νμΌ κ²½λ‘κ° μ 곡λμ§ μμμ΅λλ€.' }, + { status: 400 } + ); + } + + const result = await downloadPosFile({ relativePath }); + + if (!result.success) { + return NextResponse.json( + { error: result.error }, + { status: 404 } + ); + } + + if (!result.fileBuffer || !result.fileName) { + return NextResponse.json( + { error: 'νμΌμ μ½μ μ μμ΅λλ€.' }, + { status: 500 } + ); + } + + // νμΌ λ€μ΄λ‘λ μλ΅ μμ± + const response = new NextResponse(result.fileBuffer); + + response.headers.set('Content-Type', result.mimeType || 'application/octet-stream'); + response.headers.set('Content-Disposition', `attachment; filename="${encodeURIComponent(result.fileName)}"`); + response.headers.set('Content-Length', result.fileBuffer.length.toString()); + + return response; + } catch (error) { + console.error('POS νμΌ λ€μ΄λ‘λ API μ€λ₯:', error); + return NextResponse.json( + { error: 'μλ² λ΄λΆ μ€λ₯κ° λ°μνμ΅λλ€.' }, + { status: 500 } + ); + } +} diff --git a/lib/pos/download-pos-file.ts b/lib/pos/download-pos-file.ts new file mode 100644 index 00000000..d285c618 --- /dev/null +++ b/lib/pos/download-pos-file.ts @@ -0,0 +1,162 @@ +'use server'; + +import fs from 'fs/promises'; +import path from 'path'; +import type { DownloadPosFileParams, DownloadPosFileResult } from './types'; +import { debugLog, debugError, debugSuccess, debugProcess } from '@/lib/debug-utils'; + +const POS_BASE_PATH = '\\\\60.100.99.123\\ECM_NAS_PRM\\Download'; + +/** + * POS APIμμ λ°νλ κ²½λ‘μ νμΌμ λ΄λΆλ§μμ μ½μ΄μ ν΄λΌμ΄μΈνΈμκ² μ λ¬ + * λ΄λΆλ§ νμΌμ μ§μ μ κ·Όν μ μλ ν΄λΌμ΄μΈνΈλ₯Ό μν νλ‘μ μν + */ +export async function downloadPosFile( + params: DownloadPosFileParams +): Promise<DownloadPosFileResult> { + try { + const { relativePath } = params; + debugLog(`β¬οΈ POS νμΌ λ€μ΄λ‘λ μμ`, { relativePath, basePath: POS_BASE_PATH }); + + if (!relativePath) { + debugError(`β νμΌ κ²½λ‘κ° μ 곡λμ§ μμ`); + return { + success: false, + error: 'νμΌ κ²½λ‘κ° μ 곡λμ§ μμμ΅λλ€.', + }; + } + + // Windows κ²½λ‘ κ΅¬λΆμλ₯Ό μ κ·ν + const normalizedRelativePath = relativePath.replace(/\\/g, path.sep); + debugLog(`π κ²½λ‘ μ κ·ν`, { original: relativePath, normalized: normalizedRelativePath }); + + // μ 체 νμΌ κ²½λ‘ κ΅¬μ± + const fullPath = path.join(POS_BASE_PATH, normalizedRelativePath); + debugLog(`π μ 체 νμΌ κ²½λ‘ κ΅¬μ±`, { fullPath }); + + // κ²½λ‘ λ³΄μ κ²μ¦ (λλ ν 리 νμΆ λ°©μ§) + const resolvedPath = path.resolve(fullPath); + const resolvedBasePath = path.resolve(POS_BASE_PATH); + debugLog(`π κ²½λ‘ λ³΄μ κ²μ¦`, { resolvedPath, resolvedBasePath, isValid: resolvedPath.startsWith(resolvedBasePath) }); + + if (!resolvedPath.startsWith(resolvedBasePath)) { + debugError(`β λλ ν 리 νμΆ μλ κ°μ§`, { resolvedPath, resolvedBasePath }); + return { + success: false, + error: 'μλͺ»λ νμΌ κ²½λ‘μ
λλ€.', + }; + } + + // νμΌ μ‘΄μ¬ μ¬λΆ νμΈ + debugProcess(`π νμΌ μ‘΄μ¬ μ¬λΆ νμΈ μ€...`); + try { + await fs.access(resolvedPath); + debugSuccess(`β
νμΌ μ‘΄μ¬ νμΈλ¨ (${resolvedPath})`); + } catch (accessError) { + debugError(`β νμΌμ μ°Ύμ μ μμ`, { resolvedPath, error: accessError }); + return { + success: false, + error: 'νμΌμ μ°Ύμ μ μμ΅λλ€.', + }; + } + + // νμΌ μ 보 κ°μ Έμ€κΈ° + debugProcess(`π νμΌ μ 보 μ‘°ν μ€...`); + const stats = await fs.stat(resolvedPath); + debugLog(`π νμΌ μ 보`, { + resolvedPath, + size: stats.size, + isFile: stats.isFile(), + isDirectory: stats.isDirectory(), + mtime: stats.mtime + }); + + if (!stats.isFile()) { + debugError(`β λμμ΄ νμΌμ΄ μλ (λλ ν 리)`, { resolvedPath }); + return { + success: false, + error: 'λλ ν 리λ λ€μ΄λ‘λν μ μμ΅λλ€.', + }; + } + + // νμΌ μ½κΈ° + debugProcess(`π νμΌ μ½κΈ° μμ (ν¬κΈ°: ${stats.size} bytes)...`); + const fileBuffer = await fs.readFile(resolvedPath); + const fileName = path.basename(resolvedPath); + debugLog(`π νμΌ μ½κΈ° μλ£`, { + fileName, + bufferSize: fileBuffer.length, + expectedSize: stats.size, + sizeMatch: fileBuffer.length === stats.size + }); + + // MIME νμ
μΆμ + debugProcess(`π MIME νμ
μΆμ μ€...`); + const mimeType = await getMimeType(fileName); + debugLog(`π MIME νμ
μΆμ μλ£`, { fileName, mimeType }); + + debugSuccess(`β
POS νμΌ λ€μ΄λ‘λ μ±κ³΅`, { + fileName, + fileSize: fileBuffer.length, + mimeType + }); + + return { + success: true, + fileName, + fileBuffer, + mimeType, + }; + } catch (error) { + debugError('β POS νμΌ λ€μ΄λ‘λ μ€ν¨', { + relativePath: params.relativePath, + error: error instanceof Error ? error.message : 'μ μ μλ μ€λ₯', + stack: error instanceof Error ? error.stack : undefined + }); + return { + success: false, + error: error instanceof Error ? error.message : 'μ μ μλ μ€λ₯κ° λ°μνμ΅λλ€.', + }; + } +} + +/** + * νμΌ νμ₯μλ₯Ό κΈ°λ°μΌλ‘ MIME νμ
μΆμ + */ +export async function getMimeType(fileName: string): Promise<string> { + const ext = path.extname(fileName).toLowerCase(); + + const mimeTypes: Record<string, string> = { + '.pdf': 'application/pdf', + '.tif': 'image/tiff', + '.tiff': 'image/tiff', + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.png': 'image/png', + '.gif': 'image/gif', + '.bmp': 'image/bmp', + '.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', + '.dwg': 'application/acad', + '.dxf': 'application/dxf', + '.zip': 'application/zip', + '.rar': 'application/x-rar-compressed', + '.7z': 'application/x-7z-compressed', + '.txt': 'text/plain', + }; + + return mimeTypes[ext] || 'application/octet-stream'; +} + +/** + * ν΄λΌμ΄μΈνΈμμ μ¬μ©ν μ μλ λ€μ΄λ‘λ URLμ μμ±νλ ν¬νΌ ν¨μ + * μ€μ λ€μ΄λ‘λλ λ³λμ API μλν¬μΈνΈμμ μ²λ¦¬ + */ +export async function createDownloadUrl(relativePath: string): Promise<string> { + const encodedPath = encodeURIComponent(relativePath); + return `/api/pos/download?path=${encodedPath}`; +} diff --git a/lib/pos/get-dcmtm-id.ts b/lib/pos/get-dcmtm-id.ts new file mode 100644 index 00000000..353c5b1d --- /dev/null +++ b/lib/pos/get-dcmtm-id.ts @@ -0,0 +1,134 @@ +'use server'; + +import { oracleKnex } from '@/lib/oracle-db/db'; +import type { PosFileInfo, GetDcmtmIdParams, GetDcmtmIdResult } from './types'; +import { debugLog, debugError, debugSuccess, debugProcess } from '@/lib/debug-utils'; + +/** + * μμ¬μ½λλ‘ POS νμΌμ DCMTM_IDλ₯Ό μ‘°ννλ μλ² μ‘μ
+ * + * @param params - μμ¬μ½λλ₯Ό ν¬ν¨ν μ‘°ν νλΌλ―Έν° + * @returns POS νμΌ μ 보 λͺ©λ‘ (μ¬λ¬ νμΌμ΄ μμ μ μμ) + */ +export async function getDcmtmIdByMaterialCode( + params: GetDcmtmIdParams +): Promise<GetDcmtmIdResult> { + try { + const { materialCode } = params; + debugLog(`π DCMTM_ID μ‘°ν μμ`, { materialCode }); + + if (!materialCode) { + debugError(`β μμ¬μ½λκ° μ 곡λμ§ μμ`); + return { + success: false, + error: 'μμ¬μ½λκ° μ 곡λμ§ μμμ΅λλ€.', + }; + } + + // Oracle 쿼리 μ€ν + const query = ` + SELECT Y.PROJ_NO, + Y.POS_NO, + Y.POS_REV_NO, + Y.FILE_SER, + Y.FILE_NM, + Y.DCMTM_ID + FROM PLDTB_POS_ITEM X, + PLDTB_POS_ITEM_FILE Y, + PLMTB_EMLS Z + WHERE X.PROJ_NO = Y.PROJ_NO + AND X.POS_NO = Y.POS_NO + AND X.POS_REV_NO = Y.POS_REV_NO + AND X.POS_NO = Z.DOC_NO + AND NVL(X.APP_ID, ' ') <> ' ' + AND NVL(Z.PUR_REQ_NO, ' ') <> ' ' + AND Z.MAT_NO = ? + `; + + debugProcess(`ποΈ Oracle 쿼리 μ€ν`, { materialCode, query: query.trim() }); + const results = await oracleKnex.raw(query, [materialCode]); + debugLog(`ποΈ Oracle 쿼리 κ²°κ³Ό`, { materialCode, resultType: typeof results, hasRows: results && results[0] ? results[0].length : 0 }); + + // Oracle κ²°κ³Ό μ²λ¦¬ (oracledb λλΌμ΄λ²μ κ²°κ³Ό ꡬ쑰μ λ°λΌ) + const rows = results[0] || results.rows || results; + debugLog(`π Oracle κ²°κ³Ό ꡬ쑰 λΆμ`, { + materialCode, + resultStructure: { + hasZeroIndex: !!results[0], + hasRowsProperty: !!results.rows, + rowsType: typeof rows, + isArray: Array.isArray(rows), + rowCount: Array.isArray(rows) ? rows.length : 0 + }, + firstRow: Array.isArray(rows) && rows.length > 0 ? rows[0] : null + }); + + if (!Array.isArray(rows) || rows.length === 0) { + debugLog(`β οΈ POS νμΌμ μ°Ύμ μ μμ (μμ¬μ½λ: ${materialCode})`); + return { + success: false, + error: 'ν΄λΉ μμ¬μ½λμ λν POS νμΌμ μ°Ύμ μ μμ΅λλ€.', + }; + } + + // κ²°κ³Όλ₯Ό PosFileInfo ννλ‘ λ³ν + debugProcess(`π Oracle κ²°κ³Όλ₯Ό PosFileInfoλ‘ λ³ν μ€...`); + const files: PosFileInfo[] = rows.map((row: Record<string, unknown>, index: number) => { + const fileInfo = { + projNo: (row.PROJ_NO || row[0]) as string, + posNo: (row.POS_NO || row[1]) as string, + posRevNo: (row.POS_REV_NO || row[2]) as string, + fileSer: (row.FILE_SER || row[3]) as string, + fileName: (row.FILE_NM || row[4]) as string, + dcmtmId: (row.DCMTM_ID || row[5]) as string, + }; + debugLog(`π νμΌ ${index + 1} λ³ν κ²°κ³Ό`, { materialCode, index, original: row, converted: fileInfo }); + return fileInfo; + }); + + debugSuccess(`β
DCMTM_ID μ‘°ν μ±κ³΅ (μμ¬μ½λ: ${materialCode}, νμΌ μ: ${files.length})`); + return { + success: true, + files, + }; + } catch (error) { + debugError('β DCMTM_ID μ‘°ν μ€ν¨', { + materialCode: params.materialCode, + error: error instanceof Error ? error.message : 'μ μ μλ μ€λ₯', + stack: error instanceof Error ? error.stack : undefined + }); + return { + success: false, + error: error instanceof Error ? error.message : 'λ°μ΄ν°λ² μ΄μ€ μ‘°ν μ€ μ€λ₯κ° λ°μνμ΅λλ€.', + }; + } +} + +/** + * λ¨μΌ DCMTM_IDλ§ νμν κ²½μ°λ₯Ό μν ν¬νΌ ν¨μ + * μ¬λ¬ νμΌμ΄ μμ κ²½μ° μ²« λ²μ§Έ νμΌμ DCMTM_IDλ₯Ό λ°ν + */ +export async function getFirstDcmtmId( + params: GetDcmtmIdParams +): Promise<{ + success: boolean; + dcmtmId?: string; + fileName?: string; + error?: string; +}> { + const result = await getDcmtmIdByMaterialCode(params); + + if (!result.success || !result.files || result.files.length === 0) { + return { + success: false, + error: result.error, + }; + } + + const firstFile = result.files[0]; + return { + success: true, + dcmtmId: firstFile.dcmtmId, + fileName: firstFile.fileName, + }; +} diff --git a/lib/pos/get-pos.ts b/lib/pos/get-pos.ts new file mode 100644 index 00000000..6424b880 --- /dev/null +++ b/lib/pos/get-pos.ts @@ -0,0 +1,174 @@ +'use server'; + +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 { 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\ κ²½λ‘μ μ μ₯λ©λλ€. + */ +export async function getEncryptDocumentumFile( + params: GetEncryptDocumentumFileParams +): Promise<{ + success: boolean; + result?: string; + error?: string; +}> { + try { + const { + objectID, + sabun = 'EVM0236', // context2.txtμ λ°λΌ κΈ°λ³Έκ° μ€μ + appCode = process.env.POS_APP_CODE || 'SO13', // νκ²½λ³μ μ¬μ© + fileCreateMode = 1, // context2.txtμ λ°λΌ κΈ°λ³Έκ° λ³κ²½ + securityLevel = 'SedamsClassID', + isDesign = true, // context2.txtμ λ°λΌ κΈ°λ³Έκ° λ³κ²½ + } = params; + + debugLog(`π POS SOAP API νΈμΆ μμ`, { + objectID, + sabun, + appCode, + fileCreateMode, + securityLevel, + isDesign, + endpoint: POS_SOAP_ENDPOINT + }); + + // 1. SOAP Envelope μμ± (SOAP 1.2) + debugProcess(`π SOAP Envelope μμ± μ€...`); + const envelopeObj = { + 'soap12:Envelope': { + '@_xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', + '@_xmlns:xsd': 'http://www.w3.org/2001/XMLSchema', + '@_xmlns:soap12': 'http://www.w3.org/2003/05/soap-envelope', + 'soap12:Body': { + GetEncryptDocumentumFile: { + '@_xmlns': 'Sedams.WebServices.Documentum', + objectID, + sabun, + appCode, + fileCreateMode: String(fileCreateMode), + securityLevel, + isDesign: String(isDesign), + }, + }, + }, + }; + + const builder = new XMLBuilder({ + ignoreAttributes: false, + attributeNamePrefix: '@_', + format: false, + suppressEmptyNode: true, + }); + + const xmlBody = builder.build(envelopeObj); + debugLog(`π μμ±λ SOAP XML`, { + objectID, + xmlLength: xmlBody.length, + envelope: envelopeObj['soap12:Envelope']['soap12:Body'] + }); + + // 2. SOAP νΈμΆ (λ‘κ·Έ ν¬ν¨) + debugProcess(`π SOAP μμ² μ μ‘ μ€... (${POS_SOAP_ENDPOINT})`); + const responseText = await withSoapLogging( + 'OUTBOUND', + 'SEDAMS Documentum', + 'GetEncryptDocumentumFile', + xmlBody, + async () => { + const res = await fetch(POS_SOAP_ENDPOINT, { + method: 'POST', + headers: { + 'Content-Type': 'text/xml; charset=utf-8', + SOAPAction: + '"Sedams.WebServices.Documentum/GetEncryptDocumentumFile"', + }, + body: xmlBody, + }); + + const text = await res.text(); + debugLog(`π HTTP μλ΅ μμ `, { + objectID, + status: res.status, + statusText: res.statusText, + responseLength: text.length, + headers: Object.fromEntries(res.headers.entries()) + }); + + if (!res.ok) { + debugError(`β HTTP μ€λ₯ μλ΅`, { objectID, status: res.status, statusText: res.statusText, responseBody: text }); + throw new Error(`HTTP ${res.status}: ${res.statusText}`); + } + + return text; + } + ); + + debugSuccess(`β
SOAP μλ΅ μμ μλ£ (μλ΅ κΈΈμ΄: ${responseText.length})`); + + // 3. μλ΅ XML νμ± + debugProcess(`π XML μλ΅ νμ± μ€...`); + const parser = new XMLParser({ + ignoreAttributes: false, + attributeNamePrefix: '@_', + }); + + const parsed = parser.parse(responseText); + debugLog(`π νμ±λ XML ꡬ쑰`, { + objectID, + parsedKeys: Object.keys(parsed), + parsed: parsed + }); + + let result: string | undefined; + + try { + // SOAP 1.2 κ·κ²© + debugProcess(`π SOAP 1.2 ꡬ쑰μμ κ²°κ³Ό μΆμΆ μλ...`); + result = + parsed['soap:Envelope']['soap:Body'][ + 'GetEncryptDocumentumFileResponse' + ]['GetEncryptDocumentumFileResult']; + debugLog(`π― SOAP 1.2μμ κ²°κ³Ό μΆμΆ μ±κ³΅`, { objectID, result }); + } catch (e1) { + debugLog(`β οΈ SOAP 1.2 ꡬ쑰 μΆμΆ μ€ν¨, Fallback μλ...`, { objectID, error: e1 }); + // Fallback: SOAP 1.1 λλ λ€λ₯Έ μλ΅ νν + try { + debugProcess(`π SOAP 1.1 ꡬ쑰μμ κ²°κ³Ό μΆμΆ μλ...`); + result = + parsed['soap:Envelope']['soap:Body'][ + 'GetEncryptDocumentumFileResponse' + ]['GetEncryptDocumentumFileResult']; + debugLog(`π― SOAP 1.1μμ κ²°κ³Ό μΆμΆ μ±κ³΅`, { objectID, result }); + } catch (e2) { + debugLog(`β οΈ SOAP 1.1 ꡬ쑰 μΆμΆ μ€ν¨, λ¨μ string μλ...`, { objectID, error: e2 }); + // HTTP POST μλ΅ νν (λ¨μ string) + try { + debugProcess(`π λ¨μ string ꡬ쑰μμ κ²°κ³Ό μΆμΆ μλ...`); + result = parsed.string; + debugLog(`π― λ¨μ stringμμ κ²°κ³Ό μΆμΆ μ±κ³΅`, { objectID, result }); + } catch (e3) { + debugError(`β λͺ¨λ νμ± λ°©λ² μ€ν¨`, { objectID, errors: [e1, e2, e3], parsedStructure: parsed }); + } + } + } + + debugSuccess(`β
POS API νΈμΆ μ±κ³΅`, { objectID, result }); + return { success: true, result }; + } catch (error) { + debugError('β POS SOAP νΈμΆ μ€ν¨', { + objectID: params.objectID, + 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', + }; + } +} diff --git a/lib/pos/index.ts b/lib/pos/index.ts new file mode 100644 index 00000000..7611d5c5 --- /dev/null +++ b/lib/pos/index.ts @@ -0,0 +1,164 @@ +// POS κ΄λ ¨ λͺ¨λ κΈ°λ₯μ νλλ‘ ν΅ν©νλ μΈλ±μ€ νμΌ + +import { getEncryptDocumentumFile } from './get-pos'; +import { createDownloadUrl } from './download-pos-file'; +import { getDcmtmIdByMaterialCode } from './get-dcmtm-id'; +import type { PosFileInfo } from './types'; + +export { + getEncryptDocumentumFile +} from './get-pos'; + +export { + downloadPosFile, + createDownloadUrl +} from './download-pos-file'; + +export { + getDcmtmIdByMaterialCode, + getFirstDcmtmId +} from './get-dcmtm-id'; + +export { + syncRfqPosFiles +} from './sync-rfq-pos-files'; + +// νμ
λ€μ ./types μμ export +export type * from './types'; + +/** + * POS νμΌμ κ°μ Έμμ λ€μ΄λ‘λ URLμ μμ±νλ ν΅ν© ν¨μ + * + * @example + * ```typescript + * const result = await getPosFileAndCreateDownloadUrl({ + * objectID: "0900746983f2e12a" + * }); + * + * if (result.success && result.downloadUrl) { + * // ν΄λΌμ΄μΈνΈμμ λ€μ΄λ‘λ λ§ν¬ μ¬μ© + * window.open(result.downloadUrl, '_blank'); + * } + * ``` + */ +export async function getPosFileAndCreateDownloadUrl(params: { + objectID: string; + sabun?: string; + appCode?: string; + fileCreateMode?: number; + securityLevel?: string; + isDesign?: boolean; +}): Promise<{ + success: boolean; + downloadUrl?: string; + fileName?: string; + error?: string; +}> { + try { + // 1. POSμμ νμΌ κ²½λ‘ κ°μ Έμ€κΈ° + const posResult = await getEncryptDocumentumFile(params); + + if (!posResult.success || !posResult.result) { + return { + success: false, + error: posResult.error || 'POSμμ νμΌ μ 보λ₯Ό κ°μ Έμ¬ μ μμ΅λλ€.', + }; + } + + // 2. λ€μ΄λ‘λ URL μμ± + const downloadUrl = await createDownloadUrl(posResult.result); + + // νμΌλͺ
μΆμΆ (κ²½λ‘μ λ§μ§λ§ λΆλΆ) + const fileName = posResult.result.split(/[\\\/]/).pop() || 'unknown'; + + return { + success: true, + downloadUrl, + fileName, + }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + }; + } +} + +/** + * μμ¬μ½λλΆν° POS νμΌ λ€μ΄λ‘λκΉμ§ μ 체 μν¬νλ‘μ°λ₯Ό μ²λ¦¬νλ ν΅ν© ν¨μ + * + * @example + * ```typescript + * const result = await getPosFileByMaterialCode({ + * materialCode: "SN2693A6410100001" + * }); + * + * if (result.success && result.downloadUrl) { + * // ν΄λΌμ΄μΈνΈμμ λ€μ΄λ‘λ λ§ν¬ μ¬μ© + * window.open(result.downloadUrl, '_blank'); + * } + * ``` + */ +export async function getPosFileByMaterialCode(params: { + materialCode: string; + /** + * μ¬λ¬ νμΌμ΄ μμ κ²½μ° μ νν νμΌ μΈλ±μ€ (κΈ°λ³Έκ°: 0) + */ + fileIndex?: number; +}): Promise<{ + success: boolean; + downloadUrl?: string; + fileName?: string; + availableFiles?: PosFileInfo[]; + error?: string; +}> { + try { + const { materialCode, fileIndex = 0 } = params; + + // 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, + }; + } + + const selectedFile = dcmtmResult.files[fileIndex]; + + // 2. DCMTM_IDλ‘ POS νμΌ μ 보 κ°μ Έμ€κΈ° + const posResult = await getPosFileAndCreateDownloadUrl({ + objectID: selectedFile.dcmtmId, + }); + + if (!posResult.success) { + return { + success: false, + error: posResult.error, + availableFiles: dcmtmResult.files, + }; + } + + return { + success: true, + downloadUrl: posResult.downloadUrl, + fileName: selectedFile.fileName, // μ€λΌν΄μμ κ°μ Έμ¨ νμΌλͺ
μ¬μ© + availableFiles: dcmtmResult.files, + }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + }; + } +} diff --git a/lib/pos/sync-rfq-pos-files.ts b/lib/pos/sync-rfq-pos-files.ts new file mode 100644 index 00000000..acb34f20 --- /dev/null +++ b/lib/pos/sync-rfq-pos-files.ts @@ -0,0 +1,407 @@ +'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/pos/types.ts b/lib/pos/types.ts new file mode 100644 index 00000000..eb75c94b --- /dev/null +++ b/lib/pos/types.ts @@ -0,0 +1,87 @@ +// POS κ΄λ ¨ νμ
μ μ + +export interface PosFileInfo { + projNo: string; + posNo: string; + posRevNo: string; + fileSer: string; + fileName: string; + dcmtmId: string; +} + +export interface GetDcmtmIdParams { + /** + * μμ¬μ½λ (MATNR) + * μ: 'SN2693A6410100001' + */ + materialCode: string; +} + +export interface GetDcmtmIdResult { + success: boolean; + files?: PosFileInfo[]; + error?: string; +} + +export interface GetEncryptDocumentumFileParams { + objectID: string; + /** + * μ¬λ². κΈ°λ³Έκ°: 'EVM0236' (context2.txt κΈ°μ€) + */ + sabun?: string; + /** + * μ± μ½λ. κΈ°λ³Έκ°: νκ²½λ³μ POS_APP_CODE λλ 'SO13'(νμ§) + */ + appCode?: string; + /** + * νμΌ μμ± λͺ¨λ. κΈ°λ³Έκ°: 1 (context2.txt κΈ°μ€) + * 0: PDF Rendition μ°μ , μ€ν¨μ μλ³Έ + * 1: μλ³Έ νμΌ + * 2: PDF νμΌ + * 3: TIFF νμΌ + */ + fileCreateMode?: number; + /** + * 보μ λ 벨. κΈ°λ³Έκ°: 'SedamsClassID' + */ + securityLevel?: string; + /** + * μ€κ³νμΌ μ¬λΆ. κΈ°λ³Έκ°: true (context2.txt κΈ°μ€) + */ + isDesign?: boolean; +} + +export interface DownloadPosFileParams { + /** + * POS APIμμ λ°νλ μλ κ²½λ‘ + * μ: "asd_as_2509131735233768_OP02\asd_as_2509131735233768_OP02.tif" + */ + relativePath: string; +} + +export interface DownloadPosFileResult { + success: boolean; + fileName?: string; + fileBuffer?: Buffer; + mimeType?: string; + error?: string; +} + +export interface PosFileSyncResult { + success: boolean; + processedCount: number; + successCount: number; + failedCount: number; + errors: string[]; + details: { + materialCode: string; + fileName?: string; + status: 'success' | 'failed' | 'no_files'; + error?: string; + }[]; +} + +// POS SOAP μλν¬μΈνΈ μ 보 +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}`; diff --git a/lib/rfq-last/table/rfq-attachments-dialog.tsx b/lib/rfq-last/table/rfq-attachments-dialog.tsx index 05747c6c..4513b0b0 100644 --- a/lib/rfq-last/table/rfq-attachments-dialog.tsx +++ b/lib/rfq-last/table/rfq-attachments-dialog.tsx @@ -2,7 +2,7 @@ import * as React from "react" import { format } from "date-fns" -import { Download, FileText, Eye, ExternalLink, Loader2 } from "lucide-react" +import { Download, FileText, Eye, ExternalLink, Loader2, RefreshCw } from "lucide-react" import { Dialog, DialogContent, @@ -26,6 +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" // 첨λΆνμΌ νμ
interface RfqAttachment { @@ -55,6 +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 { data: session } = useSession() // 첨λΆνμΌ λͺ©λ‘ λ‘λ React.useEffect(() => { @@ -153,6 +158,49 @@ export function RfqAttachmentsDialog({ isOpen, onClose, rfqData }: RfqAttachment } } + // POS νμΌ λκΈ°ν νΈλ€λ¬ + const handlePosSync = async () => { + if (!session?.user?.id || !rfqData.id) { + toast.error("λ‘κ·ΈμΈμ΄ νμνκ±°λ RFQ μ λ³΄κ° μμ΅λλ€") + return + } + + setIsSyncing(true) + + try { + const result = await syncRfqPosFiles(rfqData.id, parseInt(session.user.id)) + + 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.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) => { switch (type.toLowerCase()) { @@ -167,11 +215,31 @@ export function RfqAttachmentsDialog({ isOpen, onClose, rfqData }: RfqAttachment <Dialog open={isOpen} onOpenChange={onClose}> <DialogContent className="max-w-6xl h-[85vh] flex flex-col"> <DialogHeader> - <DialogTitle>견μ 첨λΆνμΌ</DialogTitle> - <DialogDescription> - {rfqData.rfqCode} - {rfqData.rfqTitle || rfqData.itemName || "견μ "} - {attachments.length > 0 && ` (${attachments.length}κ° νμΌ)`} - </DialogDescription> + <div className="flex items-center justify-between"> + <div className="flex-1"> + <DialogTitle>견μ 첨λΆνμΌ</DialogTitle> + <DialogDescription> + {rfqData.rfqCode} - {rfqData.rfqTitle || rfqData.itemName || "견μ "} + {attachments.length > 0 && ` (${attachments.length}κ° νμΌ)`} + </DialogDescription> + </div> + + {/* POS λκΈ°ν λ²νΌ */} + <Button + variant="outline" + size="sm" + onClick={handlePosSync} + disabled={isSyncing || isLoading} + className="flex items-center gap-2" + > + {isSyncing ? ( + <Loader2 className="h-4 w-4 animate-spin" /> + ) : ( + <RefreshCw className="h-4 w-4" /> + )} + {isSyncing ? "λκΈ°ν μ€..." : "POS νμΌ λκΈ°ν"} + </Button> + </div> </DialogHeader> <ScrollArea className="flex-1"> diff --git a/lib/soap/ecc/send/create-po.ts b/lib/soap/ecc/send/create-po.ts index 444d9cc1..47894d50 100644 --- a/lib/soap/ecc/send/create-po.ts +++ b/lib/soap/ecc/send/create-po.ts @@ -8,14 +8,14 @@ const ECC_PO_ENDPOINT = "http://shii8dvddb01.hec.serp.shi.samsung.net:50000/sap/ // PO ν€λ λ°μ΄ν° νμ
export interface POHeaderData { - ANFNR: string; // Bidding Number (M) + ANFNR: string; // Bidding Number (M) // BiddingNumberμ΄μ§λ§, RFQ/Bidding λͺ¨λ λ°μ ANFNR μ¬μ©νλ©΄ λ©λλ€. (RFQ/Bidding Header Key) LIFNR: string; // Vendor Account Number (M) ZPROC_IND: string; // Purchasing Processing State (M) - ANGNR?: string; // Bidding Number + ANGNR?: string; // Bidding Number // λ μ΄μ μ¬μ©νμ§ μμ. 보λ΄μ§ μμΌλ©΄ λ¨. WAERS: string; // Currency Key (M) ZTERM: string; // Terms of Payment Key (M) - INCO1: string; // Incoterms (Part 1) (M) - INCO2: string; // Incoterms (Part 2) (M) + INCO1: string; // Incoterms (Part 1) (M) //μΈμ½ν
μ¦μ½λ 3μ리 + INCO2: string; // Incoterms (Part 2) (M) //μμ μ
λ ₯28μ리, μΈμ½ν
μ¦ νλ€μ 28μλ¦¬λ‘ μλΌ λ£μΌλ©΄ λ λ―... λ°μ΄ν°μμ: μλ°©μμ λ°λ¦ VSTEL?: string; // Shipping Point LSTEL?: string; // loading Point MWSKZ: string; // Tax on sales/purchases code (M) diff --git a/lib/users/service.ts b/lib/users/service.ts index 7019578f..96bc4719 100644 --- a/lib/users/service.ts +++ b/lib/users/service.ts @@ -14,7 +14,7 @@ import { getErrorMessage } from "@/lib/handle-error"; import { getServerSession } from "next-auth/next" import { authOptions } from "@/app/api/auth/[...nextauth]/route" import { and, or, desc, asc, ilike, eq, isNull, isNotNull, sql, count, inArray, ne, not } from "drizzle-orm"; -import { SaveFileResult, saveFile } from '../file-stroage'; +import { SaveFileResult, saveFile } from '@/lib/file-stroage'; interface AssignUsersArgs { roleId: number |
