diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-06-05 01:53:35 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-06-05 01:53:35 +0000 |
| commit | 610d3bccf1cb640e2a21df28d8d2a954c2bf337e (patch) | |
| tree | e7e6d72fecf14ddcff1b5b52263d14119b7c488c /lib/vendor-document-list | |
| parent | 15969dfedffc4e215c81d507164bc2bb383974e5 (diff) | |
(대표님) 변경사항 0604 - OCR 관련 및 drizzle generated sqls
Diffstat (limited to 'lib/vendor-document-list')
| -rw-r--r-- | lib/vendor-document-list/dolce-upload-service.ts | 655 | ||||
| -rw-r--r-- | lib/vendor-document-list/enhanced-document-service.ts | 2 | ||||
| -rw-r--r-- | lib/vendor-document-list/import-service.ts | 487 | ||||
| -rw-r--r-- | lib/vendor-document-list/sync-service.ts | 265 | ||||
| -rw-r--r-- | lib/vendor-document-list/table/enhanced-doc-table-toolbar-actions.tsx | 57 | ||||
| -rw-r--r-- | lib/vendor-document-list/table/enhanced-documents-table.tsx | 2 | ||||
| -rw-r--r-- | lib/vendor-document-list/table/import-from-dolce-button.tsx | 356 | ||||
| -rw-r--r-- | lib/vendor-document-list/table/swp-workflow-panel.tsx | 370 | ||||
| -rw-r--r-- | lib/vendor-document-list/workflow-service.ts | 195 |
9 files changed, 2167 insertions, 222 deletions
diff --git a/lib/vendor-document-list/dolce-upload-service.ts b/lib/vendor-document-list/dolce-upload-service.ts new file mode 100644 index 00000000..0396e819 --- /dev/null +++ b/lib/vendor-document-list/dolce-upload-service.ts @@ -0,0 +1,655 @@ +// lib/vendor-document-list/dolce-upload-service.ts +import db from "@/db/db" +import { documents, revisions, documentAttachments, contracts, projects, vendors, issueStages } from "@/db/schema" +import { eq, and, desc, sql, inArray, min } from "drizzle-orm" +import { v4 as uuidv4 } from "uuid" + +export interface DOLCEUploadResult { + success: boolean + uploadedDocuments: number + uploadedFiles: number + errors?: string[] + results?: { + documentResults?: any[] + fileResults?: any[] + mappingResults?: any[] + } +} + +interface DOLCEDocument { + Mode: "ADD" | "MOD" + Status: string + RegisterId: number + ProjectNo: string + Discipline: string + DrawingKind: string + DrawingNo: string + DrawingName: string + RegisterGroupId: number + RegisterSerialNo: number + RegisterKind: string + DrawingRevNo: string + Category: string + Receiver: string | null + Manager: string + RegisterDesc: string + UploadId?: string + RegCompanyCode: string +} + +interface DOLCEFileMapping { + CGbn?: string + Category?: string + CheckBox: string + DGbn?: string + DegreeGbn?: string + DeptGbn?: string + Discipline: string + DrawingKind: string + DrawingMoveGbn: string + DrawingName: string + DrawingNo: string + DrawingUsage: string + FileNm: string + JGbn?: string + Manager: string + MappingYN: string + NewOrNot: string + ProjectNo: string + RegisterGroup: number + RegisterGroupId: number + RegisterKindCode: string + RegisterSerialNo: number + RevNo?: string + SGbn?: string + UploadId: string +} + +class DOLCEUploadService { + private readonly BASE_URL = process.env.DOLCE_API_URL || 'http://60.100.99.217:1111' + private readonly UPLOAD_SERVICE_URL = process.env.DOLCE_UPLOAD_URL || 'http://60.100.99.217:1111/PWPUploadService.ashx' + + /** + * 메인 업로드 함수: 변경된 문서와 파일을 DOLCE로 업로드 + */ + async uploadToDoLCE( + contractId: number, + revisionIds: number[], + userId: string, + userName?: string + ): Promise<DOLCEUploadResult> { + try { + console.log(`Starting DOLCE upload for contract ${contractId}, revisions: ${revisionIds.join(', ')}`) + + // 1. 계약 정보 조회 (프로젝트 코드, 벤더 코드 등) + const contractInfo = await this.getContractInfo(contractId) + if (!contractInfo) { + throw new Error(`Contract info not found for ID: ${contractId}`) + } + + // 2. 업로드할 리비전 정보 조회 + const revisionsToUpload = await this.getRevisionsForUpload(revisionIds) + if (revisionsToUpload.length === 0) { + return { + success: true, + uploadedDocuments: 0, + uploadedFiles: 0 + } + } + + // 3. 각 issueStageId별로 첫 번째 revision 정보를 미리 조회 (Mode 결정용) + const firstRevisionMap = await this.getFirstRevisionMap(revisionsToUpload.map(r => r.issueStageId)) + + let uploadedDocuments = 0 + let uploadedFiles = 0 + const errors: string[] = [] + const results: any = { + documentResults: [], + fileResults: [], + mappingResults: [] + } + + // 4. 각 리비전별로 처리 + for (const revision of revisionsToUpload) { + try { + console.log(`Processing revision ${revision.revision} for document ${revision.documentNo}`) + + // 4-1. 파일이 있는 경우 먼저 업로드 + let uploadId: string | undefined + if (revision.attachments && revision.attachments.length > 0) { + const fileUploadResults = await this.uploadFiles(revision.attachments) + + if (fileUploadResults.length > 0) { + uploadId = fileUploadResults[0].uploadId // 첫 번째 파일의 UploadId 사용 + uploadedFiles += fileUploadResults.length + results.fileResults.push(...fileUploadResults) + } + } + + // 4-2. 문서 정보 업로드 + const dolceDoc = this.transformToDoLCEDocument( + revision, + contractInfo, + uploadId, + contractInfo.vendorCode, + firstRevisionMap + ) + + const docResult = await this.uploadDocument([dolceDoc], userId) + if (docResult.success) { + uploadedDocuments++ + results.documentResults.push(docResult) + + // 4-3. 파일이 있는 경우 매핑 정보 전송 + if (uploadId && revision.attachments && revision.attachments.length > 0) { + const mappingData = this.transformToFileMapping( + revision, + contractInfo, + uploadId, + revision.attachments[0].fileName + ) + + const mappingResult = await this.uploadFileMapping([mappingData], userId) + if (mappingResult.success) { + results.mappingResults.push(mappingResult) + } else { + errors.push(`File mapping failed for ${revision.documentNo}: ${mappingResult.error}`) + } + } + + // 4-4. 성공한 리비전의 상태 업데이트 + await this.updateRevisionStatus(revision.id, 'SUBMITTED', uploadId) + + } else { + errors.push(`Document upload failed for ${revision.documentNo}: ${docResult.error}`) + } + + } catch (error) { + const errorMessage = `Failed to process revision ${revision.revision}: ${error instanceof Error ? error.message : 'Unknown error'}` + errors.push(errorMessage) + console.error(errorMessage, error) + } + } + + return { + success: errors.length === 0, + uploadedDocuments, + uploadedFiles, + errors: errors.length > 0 ? errors : undefined, + results + } + + } catch (error) { + console.error('DOLCE upload failed:', error) + throw error + } + } + + /** + * 계약 정보 조회 + */ + private async getContractInfo(contractId: number) { + const [result] = await db + .select({ + projectCode: projects.code, + vendorCode: vendors.vendorCode, + contractNo: contracts.contractNo + }) + .from(contracts) + .innerJoin(projects, eq(contracts.projectId, projects.id)) + .innerJoin(vendors, eq(contracts.vendorId, vendors.id)) + .where(eq(contracts.id, contractId)) + .limit(1) + + return result + } + + /** + * 각 issueStageId별로 첫 번째 revision 정보를 조회 + */ + private async getFirstRevisionMap(issueStageIds: number[]): Promise<Map<number, string>> { + const firstRevisions = await db + .select({ + issueStageId: revisions.issueStageId, + firstRevision: min(revisions.revision) + }) + .from(revisions) + .where(inArray(revisions.issueStageId, issueStageIds)) + .groupBy(revisions.issueStageId) + + const map = new Map<number, string>() + firstRevisions.forEach(item => { + if (item.firstRevision) { + map.set(item.issueStageId, item.firstRevision) + } + }) + + return map + } + + /** + * 업로드할 리비전 정보 조회 (문서 정보 및 첨부파일 포함) + */ + private async getRevisionsForUpload(revisionIds: number[]) { + // revisions → issueStages → documents 순서로 join하여 정보 조회 + const revisionResults = await db + .select({ + // revision 테이블 정보 + id: revisions.id, + revision: revisions.revision, // revisionNo가 아니라 revision + revisionStatus: revisions.revisionStatus, + uploaderId: revisions.uploaderId, + uploaderName: revisions.uploaderName, + submittedDate: revisions.submittedDate, + comment: revisions.comment, + + // ✅ DOLCE 연동 필드들 (새로 추가) + externalUploadId: revisions.externalUploadId, + externalRegisterId: revisions.id, + externalSentAt: revisions.submittedDate, + + // issueStages 테이블 정보 + issueStageId: issueStages.id, + stageName: issueStages.stageName, + documentId: issueStages.documentId, + + // documents 테이블 정보 (DOLCE 업로드에 필요한 모든 필드) + documentNo: documents.docNumber, + documentName: documents.title, + drawingKind: documents.drawingKind, + drawingMoveGbn: documents.drawingMoveGbn, + discipline: documents.discipline, + registerGroupId: documents.registerGroupId, + + // DOLCE B4 전용 필드들 + cGbn: documents.cGbn, + dGbn: documents.dGbn, + degreeGbn: documents.degreeGbn, + deptGbn: documents.deptGbn, + jGbn: documents.jGbn, + sGbn: documents.sGbn, + + // DOLCE 추가 정보 + manager: documents.manager, + managerENM: documents.managerENM, + managerNo: documents.managerNo, + shiDrawingNo: documents.shiDrawingNo, + + // 외부 시스템 연동 정보 + externalDocumentId: documents.externalDocumentId, + externalSystemType: documents.externalSystemType, + externalSyncedAt: documents.externalSyncedAt + }) + .from(revisions) + .innerJoin(issueStages, eq(revisions.issueStageId, issueStages.id)) + .innerJoin(documents, eq(issueStages.documentId, documents.id)) + .where(inArray(revisions.id, revisionIds)) + + // 각 리비전의 첨부파일 정보도 조회 + const revisionsWithAttachments = [] + for (const revision of revisionResults) { + const attachments = await db + .select({ + id: documentAttachments.id, + fileName: documentAttachments.fileName, + filePath: documentAttachments.filePath, + fileType: documentAttachments.fileType, + fileSize: documentAttachments.fileSize, + createdAt: documentAttachments.createdAt + }) + .from(documentAttachments) + .where(eq(documentAttachments.revisionId, revision.id)) + + revisionsWithAttachments.push({ + ...revision, + attachments + }) + } + + return revisionsWithAttachments + } + + /** + * 파일 업로드 (PWPUploadService.ashx) + */ + private async uploadFiles(attachments: any[]): Promise<Array<{uploadId: string, fileId: string, filePath: string}>> { + const uploadResults = [] + + for (const attachment of attachments) { + try { + // UploadId와 FileId 생성 (UUID 형태) + const uploadId = uuidv4() + const fileId = uuidv4() + + // 파일 데이터 읽기 (실제 구현에서는 파일 시스템이나 S3에서 읽어옴) + const fileBuffer = await this.getFileBuffer(attachment.filePath) + + const uploadUrl = `${this.UPLOAD_SERVICE_URL}?UploadId=${uploadId}&FileId=${fileId}` + + const response = await fetch(uploadUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/octet-stream', + }, + body: fileBuffer + }) + + if (!response.ok) { + const errorText = await response.text() + throw new Error(`File upload failed: HTTP ${response.status} - ${errorText}`) + } + + const filePath = await response.text() // DOLCE에서 반환하는 파일 경로 + + uploadResults.push({ + uploadId, + fileId, + filePath + }) + + console.log(`✅ File uploaded successfully: ${attachment.fileName} -> ${filePath}`) + + } catch (error) { + console.error(`❌ File upload failed for ${attachment.fileName}:`, error) + throw error + } + } + + return uploadResults + } + + /** + * 문서 정보 업로드 (DetailDwgReceiptMgmtEdit) + */ + private async uploadDocument(dwgList: DOLCEDocument[], userId: string): Promise<{success: boolean, error?: string, data?: any}> { + try { + const endpoint = `${this.BASE_URL}/Services/VDCSWebService.svc/DetailDwgReceiptMgmtEdit` + + const requestBody = { + DwgList: dwgList, + UserID: userId + } + + console.log('Uploading documents to DOLCE:', JSON.stringify(requestBody, null, 2)) + + const response = await fetch(endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(requestBody) + }) + + if (!response.ok) { + const errorText = await response.text() + throw new Error(`HTTP ${response.status} - ${errorText}`) + } + + const result = await response.json() + + return { + success: true, + data: result + } + + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error' + } + } + } + + /** + * 파일 매핑 정보 업로드 (MatchBatchFileDwgEdit) + */ + private async uploadFileMapping(mappingList: DOLCEFileMapping[], userId: string): Promise<{success: boolean, error?: string, data?: any}> { + try { + const endpoint = `${this.BASE_URL}/Services/VDCSWebService.svc/MatchBatchFileDwgEdit` + + const requestBody = { + mappingSaveLists: mappingList, + UserID: userId + } + + console.log('Uploading file mapping to DOLCE:', JSON.stringify(requestBody, null, 2)) + + const response = await fetch(endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(requestBody) + }) + + if (!response.ok) { + const errorText = await response.text() + throw new Error(`HTTP ${response.status} - ${errorText}`) + } + + const result = await response.json() + + return { + success: true, + data: result + } + + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error' + } + } + } + + /** + * 리비전 데이터를 DOLCE 문서 형태로 변환 (업데이트된 스키마 사용) + */ + private transformToDoLCEDocument( + revision: any, + contractInfo: any, + uploadId?: string, + vendorCode?: string, + firstRevisionMap?: Map<number, string> + ): DOLCEDocument { + // Mode 결정: 해당 issueStageId의 첫 번째 revision인지 확인 + let mode: "ADD" | "MOD" = "MOD" // 기본값은 MOD + + if (firstRevisionMap && firstRevisionMap.has(revision.issueStageId)) { + const firstRevision = firstRevisionMap.get(revision.issueStageId) + if (revision.revision === firstRevision) { + mode = "ADD" + } + } + + // RegisterKind 결정: stageName에 따라 설정 + let registerKind = "APPC" // 기본값 + if (revision.stageName) { + const stageNameLower = revision.stageName.toLowerCase() + if (stageNameLower.includes("pre")) { + registerKind = "RECP" + } else if (stageNameLower.includes("working")) { + registerKind = "RECW" + } + } + + const getSerialNumber = (revisionValue: string): number => { + // 먼저 숫자인지 확인 + const numericValue = parseInt(revisionValue) + if (!isNaN(numericValue)) { + return numericValue + } + + // 문자인 경우 (a=1, b=2, c=3, ...) + if (typeof revisionValue === 'string' && revisionValue.length === 1) { + const charCode = revisionValue.toLowerCase().charCodeAt(0) + if (charCode >= 97 && charCode <= 122) { // a-z + return charCode - 96 // a=1, b=2, c=3, ... + } + } + + // 기본값 + return 1 + } + + + return { + Mode: mode, + Status: revision.revisionStatus || "Standby", + RegisterId: revision.externalRegisterId, // 업데이트된 필드 사용 + ProjectNo: contractInfo.projectCode, + Discipline: revision.discipline, + DrawingKind: revision.drawingKind, + DrawingNo: revision.documentNo, + DrawingName: revision.documentName, + RegisterGroupId: revision.registerGroupId || 0, + RegisterSerialNo: getSerialNumber(revision.revision || "1"), + RegisterKind: registerKind, // stageName에 따라 동적 설정 + DrawingRevNo: revision.revision || "-", + Category: revision.category || "TS", + Receiver: null, + Manager: revision.managerNo || "202206", // 담당자 번호 사용 + RegisterDesc: revision.comment || "System upload", + UploadId: uploadId, + RegCompanyCode: vendorCode || "A0005531" // 벤더 코드 + } + } + /** + * 파일 매핑 데이터 변환 + */ + private transformToFileMapping( + revision: any, + contractInfo: any, + uploadId: string, + fileName: string + ): DOLCEFileMapping { + return { + CGbn: revision.cGbn, + Category: revision.category, + CheckBox: "0", + DGbn: revision.dGbn, + DegreeGbn: revision.degreeGbn, + DeptGbn: revision.deptGbn, + Discipline: revision.discipline || "DL", + DrawingKind: revision.drawingKind || "B4", + DrawingMoveGbn: revision.drawingMoveGbn || "도면입수", + DrawingName: revision.documentName, + DrawingNo: revision.documentNo, + DrawingUsage: "입수용", + FileNm: fileName, + JGbn: revision.jGbn, + Manager: revision.managerNo || "970043", + MappingYN: "Y", + NewOrNot: "N", + ProjectNo: contractInfo.projectCode, + RegisterGroup: 0, + RegisterGroupId: revision.registerGroupId || 0, + RegisterKindCode: "RECW", + RegisterSerialNo: parseInt(revision.revision) || 1, + RevNo: revision.revision, + SGbn: revision.sGbn, + UploadId: uploadId + } + } + + /** + * 파일 버퍼 읽기 (실제 파일 시스템 기반) - 타입 에러 수정 + */ + private async getFileBuffer(filePath: string): Promise<ArrayBuffer> { + try { + console.log(`Reading file from path: ${filePath}`) + + if (filePath.startsWith('http')) { + // URL인 경우 직접 다운로드 + const response = await fetch(filePath) + if (!response.ok) { + throw new Error(`Failed to download file: ${response.status}`) + } + return await response.arrayBuffer() + } else { + // 로컬 파일 경로인 경우 + const fs = await import('fs') + const path = await import('path') + + let actualFilePath: string + + if (filePath.startsWith('/documents/')) { + // DB에 저장된 경로 형태: "/documents/[uuid].ext" + // 실제 파일 시스템 경로로 변환: "public/documents/[uuid].ext" + actualFilePath = path.join(process.cwd(), 'public', filePath) + } else if (filePath.startsWith('/')) { + // 절대 경로인 경우 public 디렉토리 기준으로 변환 + actualFilePath = path.join(process.cwd(), 'public', filePath) + } else { + // 상대 경로인 경우 그대로 사용 + actualFilePath = filePath + } + + // 파일 존재 여부 확인 + if (!fs.existsSync(actualFilePath)) { + throw new Error(`File not found: ${actualFilePath}`) + } + + // 파일 읽기 + const fileBuffer = fs.readFileSync(actualFilePath) + console.log(`✅ File read successfully: ${actualFilePath} (${fileBuffer.length} bytes)`) + + // Buffer를 ArrayBuffer로 변환 (타입 안전성 보장) + return new ArrayBuffer(fileBuffer.length).slice(0).constructor(fileBuffer) + } + } catch (error) { + console.error(`❌ Failed to read file: ${filePath}`, error) + throw error + } + } + + /** + * 리비전 상태 업데이트 (업데이트된 스키마 사용) + */ + private async updateRevisionStatus(revisionId: number, status: string, uploadId?: string) { + const updateData: any = { + revisionStatus: status, + updatedAt: new Date() + } + + // 업로드 성공 시 관련 날짜 설정 + if (status === 'SUBMITTED') { + updateData.submittedDate = new Date().toISOString().slice(0, 10) + // updateData.externalSentAt = new Date().toISOString().slice(0, 10) + } else if (status === 'APPROVED') { + updateData.approvedDate = new Date().toISOString().slice(0, 10) + } + + // DOLCE 업로드 ID 저장 + if (uploadId) { + updateData.externalUploadId = uploadId + } + + await db + .update(revisions) + .set(updateData) + .where(eq(revisions.id, revisionId)) + + console.log(`✅ Updated revision ${revisionId} status to ${status}${uploadId ? ` with upload ID: ${uploadId}` : ''}`) + } + + /** + * 업로드 가능 여부 확인 + */ + isUploadEnabled(): boolean { + const enabled = process.env.DOLCE_UPLOAD_ENABLED + return enabled === 'true' || enabled === '1' + } +} + +export const dolceUploadService = new DOLCEUploadService() + +// 편의 함수 +export async function uploadRevisionsToDOLCE( + contractId: number, + revisionIds: number[], + userId: string, + userName?: string +): Promise<DOLCEUploadResult> { + return dolceUploadService.uploadToDoLCE(contractId, revisionIds, userId, userName) +}
\ No newline at end of file diff --git a/lib/vendor-document-list/enhanced-document-service.ts b/lib/vendor-document-list/enhanced-document-service.ts index d39dfaa4..66758b89 100644 --- a/lib/vendor-document-list/enhanced-document-service.ts +++ b/lib/vendor-document-list/enhanced-document-service.ts @@ -140,8 +140,6 @@ export async function getEnhancedDocuments( return { data, total } }) - console.log(data) - const pageCount = Math.ceil(total / input.perPage) return { data, pageCount, total } diff --git a/lib/vendor-document-list/import-service.ts b/lib/vendor-document-list/import-service.ts new file mode 100644 index 00000000..4a152299 --- /dev/null +++ b/lib/vendor-document-list/import-service.ts @@ -0,0 +1,487 @@ +// lib/vendor-document-list/import-service.ts - DOLCE API 연동 버전 + +import db from "@/db/db" +import { documents, issueStages, contracts, projects, vendors } from "@/db/schema" +import { eq, and, desc, sql } from "drizzle-orm" + +export interface ImportResult { + success: boolean + newCount: number + updatedCount: number + skippedCount: number + errors?: string[] + message?: string +} + +export interface ImportStatus { + lastImportAt?: string + availableDocuments: number + newDocuments: number + updatedDocuments: number + importEnabled: boolean +} + +interface DOLCEDocument { + CGbn?: string + CreateDt: string + CreateUserENM: string + CreateUserId: string + CreateUserNo: string + DGbn?: string + DegreeGbn?: string + DeptGbn?: string + Discipline: string + DrawingKind: string // B3, B4, B5 + DrawingMoveGbn: string + DrawingName: string + DrawingNo: string + GTTInput_PlanDate?: string + GTTInput_ResultDate?: string + GTTPreDwg_PlanDate?: string + GTTPreDwg_ResultDate?: string + GTTWorkingDwg_PlanDate?: string + GTTWorkingDwg_ResultDate?: string + JGbn?: string + Manager: string + ManagerENM: string + ManagerNo: string + ProjectNo: string + RegisterGroup: number + RegisterGroupId: number + SGbn?: string + SHIDrawingNo?: string +} + +class ImportService { + /** + * DOLCE 시스템에서 문서 목록 가져오기 + */ + async importFromExternalSystem( + contractId: number, + sourceSystem: string = 'DOLCE' + ): Promise<ImportResult> { + try { + console.log(`Starting import from ${sourceSystem} for contract ${contractId}`) + + // 1. 계약 정보를 통해 프로젝트 코드와 벤더 코드 조회 + const contractInfo = await this.getContractInfoById(contractId) + if (!contractInfo?.projectCode || !contractInfo?.vendorCode) { + throw new Error(`Project code or vendor code not found for contract ${contractId}`) + } + + // 2. 각 drawingKind별로 데이터 조회 + const allDocuments: DOLCEDocument[] = [] + const drawingKinds = ['B3', 'B4', 'B5'] + + for (const drawingKind of drawingKinds) { + try { + const documents = await this.fetchFromDOLCE( + contractInfo.projectCode, + contractInfo.vendorCode, + drawingKind + ) + allDocuments.push(...documents) + console.log(`Fetched ${documents.length} documents for ${drawingKind}`) + } catch (error) { + console.warn(`Failed to fetch ${drawingKind} documents:`, error) + // 개별 drawingKind 실패는 전체 실패로 처리하지 않음 + } + } + + if (allDocuments.length === 0) { + return { + success: true, + newCount: 0, + updatedCount: 0, + skippedCount: 0, + message: '가져올 새로운 데이터가 없습니다.' + } + } + + let newCount = 0 + let updatedCount = 0 + let skippedCount = 0 + const errors: string[] = [] + + // 3. 각 문서 동기화 처리 + for (const dolceDoc of allDocuments) { + try { + const result = await this.syncSingleDocument(contractId, dolceDoc, sourceSystem) + + if (result === 'NEW') { + newCount++ + // B4 문서의 경우 이슈 스테이지 자동 생성 + if (dolceDoc.DrawingKind === 'B4') { + await this.createIssueStagesForB4Document(dolceDoc.DrawingNo, contractId, dolceDoc) + } + } else if (result === 'UPDATED') { + updatedCount++ + } else { + skippedCount++ + } + + } catch (error) { + errors.push(`Document ${dolceDoc.DrawingNo}: ${error instanceof Error ? error.message : 'Unknown error'}`) + skippedCount++ + } + } + + console.log(`Import completed: ${newCount} new, ${updatedCount} updated, ${skippedCount} skipped`) + + return { + success: errors.length === 0, + newCount, + updatedCount, + skippedCount, + errors: errors.length > 0 ? errors : undefined, + message: `가져오기 완료: 신규 ${newCount}건, 업데이트 ${updatedCount}건` + } + + } catch (error) { + console.error('Import failed:', error) + throw error + } + } + + /** + * 계약 ID로 프로젝트 코드와 벤더 코드 조회 + */ + private async getContractInfoById(contractId: number): Promise<{ + projectCode: string; + vendorCode: string; + } | null> { + const [result] = await db + .select({ + projectCode: projects.code, + vendorCode: vendors.vendorCode + }) + .from(contracts) + .innerJoin(projects, eq(contracts.projectId, projects.id)) + .innerJoin(vendors, eq(contracts.vendorId, vendors.id)) + .where(eq(contracts.id, contractId)) + .limit(1) + + + return result?.projectCode && result?.vendorCode + ? { projectCode: result.projectCode, vendorCode: result.vendorCode } + : null + } + + /** + * DOLCE API에서 데이터 조회 + */ + private async fetchFromDOLCE( + projectCode: string, + vendorCode: string, + drawingKind: string + ): Promise<DOLCEDocument[]> { + const endpoint = process.env.DOLCE_DOC_LIST_API_URL || 'http://60.100.99.217:1111/Services/VDCSWebService.svc/DwgReceiptMgmt' + + const requestBody = { + project: projectCode, + drawingKind: drawingKind, // B3, B4, B5 + drawingMoveGbn: "", + drawingNo: "", + drawingName: "", + drawingVendor: vendorCode + } + + console.log(`Fetching from DOLCE: ${projectCode} - ${drawingKind} = ${vendorCode}`) + + try { + const response = await fetch(endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + // DOLCE API에 특별한 인증이 필요하다면 여기에 추가 + }, + body: JSON.stringify(requestBody) + }) + + if (!response.ok) { + const errorText = await response.text() + throw new Error(`DOLCE API failed: HTTP ${response.status} - ${errorText}`) + } + + const data = await response.json() + + // 응답 구조에 따라 조정 필요 (실제 API 응답 구조 확인 후) + if (Array.isArray(data)) { + return data as DOLCEDocument[] + } else if (data.documents && Array.isArray(data.documents)) { + return data.documents as DOLCEDocument[] + } else if (data.data && Array.isArray(data.data)) { + return data.data as DOLCEDocument[] + } else { + console.warn(`Unexpected DOLCE response structure:`, data) + return [] + } + + } catch (error) { + console.error(`DOLCE API call failed for ${projectCode}/${drawingKind}:`, error) + throw error + } + } + + /** + * 단일 문서 동기화 + */ + private async syncSingleDocument( + contractId: number, + dolceDoc: DOLCEDocument, + sourceSystem: string + ): Promise<'NEW' | 'UPDATED' | 'SKIPPED'> { + // 기존 문서 조회 (문서 번호로) + const existingDoc = await db + .select() + .from(documents) + .where(and( + eq(documents.contractId, contractId), + eq(documents.docNumber, dolceDoc.DrawingNo) + )) + .limit(1) + + // DOLCE 문서를 DB 스키마에 맞게 변환 + const documentData = { + contractId, + docNumber: dolceDoc.DrawingNo, + title: dolceDoc.DrawingName, + status: 'ACTIVE', + + // DOLCE 전용 필드들 + drawingKind: dolceDoc.DrawingKind, + drawingMoveGbn: dolceDoc.DrawingMoveGbn, + discipline: dolceDoc.Discipline, + + // 외부 시스템 정보 + externalDocumentId: dolceDoc.DrawingNo, // DOLCE에서는 DrawingNo가 ID 역할 + externalSystemType: sourceSystem, + externalSyncedAt: new Date(), + + // B4 전용 필드들 + cGbn: dolceDoc.CGbn, + dGbn: dolceDoc.DGbn, + degreeGbn: dolceDoc.DegreeGbn, + deptGbn: dolceDoc.DeptGbn, + jGbn: dolceDoc.JGbn, + sGbn: dolceDoc.SGbn, + + // 추가 정보 + shiDrawingNo: dolceDoc.SHIDrawingNo, + manager: dolceDoc.Manager, + managerENM: dolceDoc.ManagerENM, + managerNo: dolceDoc.ManagerNo, + registerGroup: dolceDoc.RegisterGroup, + registerGroupId: dolceDoc.RegisterGroupId, + + // 생성자 정보 + createUserNo: dolceDoc.CreateUserNo, + createUserId: dolceDoc.CreateUserId, + createUserENM: dolceDoc.CreateUserENM, + + updatedAt: new Date() + } + + if (existingDoc.length > 0) { + // 업데이트 필요 여부 확인 + const existing = existingDoc[0] + const hasChanges = + existing.title !== documentData.title || + existing.drawingMoveGbn !== documentData.drawingMoveGbn || + existing.manager !== documentData.manager + + if (hasChanges) { + await db + .update(documents) + .set(documentData) + .where(eq(documents.id, existing.id)) + + console.log(`Updated document: ${dolceDoc.DrawingNo}`) + return 'UPDATED' + } else { + return 'SKIPPED' + } + } else { + // 새 문서 생성 + const [newDoc] = await db + .insert(documents) + .values({ + ...documentData, + createdAt: new Date() + }) + .returning({ id: documents.id }) + + console.log(`Created new document: ${dolceDoc.DrawingNo}`) + return 'NEW' + } + } + + /** + * B4 문서용 이슈 스테이지 자동 생성 + */ + private async createIssueStagesForB4Document( + drawingNo: string, + contractId: number, + dolceDoc: DOLCEDocument + ): Promise<void> { + try { + // 문서 ID 조회 + const [document] = await db + .select({ id: documents.id }) + .from(documents) + .where(and( + eq(documents.contractId, contractId), + eq(documents.docNumber, drawingNo) + )) + .limit(1) + + if (!document) { + throw new Error(`Document not found: ${drawingNo}`) + } + + const documentId = document.id + + // 기존 이슈 스테이지 확인 + const existingStages = await db + .select() + .from(issueStages) + .where(eq(issueStages.documentId, documentId)) + + const existingStageNames = existingStages.map(stage => stage.stageName) + + // For Pre 스테이지 생성 (GTTPreDwg) + if (!existingStageNames.includes('For Pre')) { + await db.insert(issueStages).values({ + documentId: documentId, + stageName: 'For Pre', + planDate: dolceDoc.GTTPreDwg_PlanDate ? dolceDoc.GTTPreDwg_PlanDate : null, + actualDate: dolceDoc.GTTPreDwg_ResultDate ? dolceDoc.GTTPreDwg_ResultDate : null, + stageStatus: dolceDoc.GTTPreDwg_ResultDate ? 'COMPLETED' : 'PLANNED', + stageOrder: 1, + description: 'GTT 예비 도면 단계' + }) + } + + // For Working 스테이지 생성 (GTTWorkingDwg) + if (!existingStageNames.includes('For Working')) { + await db.insert(issueStages).values({ + documentId: documentId, + stageName: 'For Working', + planDate: dolceDoc.GTTWorkingDwg_PlanDate ? dolceDoc.GTTWorkingDwg_PlanDate : null, + actualDate: dolceDoc.GTTWorkingDwg_ResultDate ? dolceDoc.GTTWorkingDwg_ResultDate : null, + stageStatus: dolceDoc.GTTWorkingDwg_ResultDate ? 'COMPLETED' : 'PLANNED', + stageOrder: 2, + description: 'GTT 작업 도면 단계' + }) + } + + console.log(`Created issue stages for B4 document: ${drawingNo}`) + + } catch (error) { + console.error(`Failed to create issue stages for ${drawingNo}:`, error) + // 이슈 스테이지 생성 실패는 전체 문서 생성을 막지 않음 + } + } + + /** + * 가져오기 상태 조회 + */ + async getImportStatus( + contractId: number, + sourceSystem: string = 'DOLCE' + ): Promise<ImportStatus> { + try { + // 마지막 가져오기 시간 조회 + const [lastImport] = await db + .select({ + lastSynced: sql<string>`MAX(${documents.externalSyncedAt})` + }) + .from(documents) + .where(and( + eq(documents.contractId, contractId), + eq(documents.externalSystemType, sourceSystem) + )) + + // 프로젝트 코드와 벤더 코드 조회 + const contractInfo = await this.getContractInfoById(contractId) + + console.log(contractInfo,"contractInfo") + + if (!contractInfo?.projectCode || !contractInfo?.vendorCode) { + throw new Error(`Project code or vendor code not found for contract ${contractId}`) + } + + let availableDocuments = 0 + let newDocuments = 0 + let updatedDocuments = 0 + + try { + // 각 drawingKind별로 확인 + const drawingKinds = ['B3', 'B4', 'B5'] + + for (const drawingKind of drawingKinds) { + try { + const externalDocs = await this.fetchFromDOLCE( + contractInfo.projectCode, + contractInfo.vendorCode, + drawingKind + ) + availableDocuments += externalDocs.length + + // 신규/업데이트 문서 수 계산 + for (const externalDoc of externalDocs) { + const existing = await db + .select({ id: documents.id, updatedAt: documents.updatedAt }) + .from(documents) + .where(and( + eq(documents.contractId, contractId), + eq(documents.docNumber, externalDoc.DrawingNo) + )) + .limit(1) + + if (existing.length === 0) { + newDocuments++ + } else { + // DOLCE의 CreateDt와 로컬 updatedAt 비교 + if (externalDoc.CreateDt && existing[0].updatedAt) { + const externalModified = new Date(externalDoc.CreateDt) + const localModified = new Date(existing[0].updatedAt) + if (externalModified > localModified) { + updatedDocuments++ + } + } + } + } + } catch (error) { + console.warn(`Failed to check ${drawingKind} for status:`, error) + } + } + } catch (error) { + console.warn(`Failed to fetch external data for status: ${error}`) + } + + return { + lastImportAt: lastImport?.lastSynced ? new Date(lastImport.lastSynced).toISOString() : undefined, + availableDocuments, + newDocuments, + updatedDocuments, + importEnabled: this.isImportEnabled(sourceSystem) + } + + } catch (error) { + console.error('Failed to get import status:', error) + throw error + } + } + + /** + * 가져오기 활성화 여부 확인 + */ + private isImportEnabled(sourceSystem: string): boolean { + const upperSystem = sourceSystem.toUpperCase() + const enabled = process.env[`IMPORT_${upperSystem}_ENABLED`] + return enabled === 'true' || enabled === '1' + } +} + +export const importService = new ImportService()
\ No newline at end of file diff --git a/lib/vendor-document-list/sync-service.ts b/lib/vendor-document-list/sync-service.ts index 1f2872c4..4c1f5786 100644 --- a/lib/vendor-document-list/sync-service.ts +++ b/lib/vendor-document-list/sync-service.ts @@ -1,4 +1,4 @@ -// lib/sync-service.ts (시스템별 분리 버전) +// lib/sync-service.ts (시스템별 분리 버전 - DOLCE 업로드 통합) import db from "@/db/db" import { changeLogs, @@ -29,8 +29,6 @@ export interface SyncResult { class SyncService { private readonly CHUNK_SIZE = 50 - - /** * 동기화 활성화 여부 확인 */ @@ -294,7 +292,7 @@ class SyncService { } /** - * DOLCE 시스템 전용 동기화 수행 + * DOLCE 시스템 전용 동기화 수행 - 실제 업로드 서비스 사용 */ private async performSyncDOLCE( changes: ChangeLog[], @@ -302,143 +300,72 @@ class SyncService { ): Promise<{ success: boolean; successCount: number; failureCount: number; errors?: string[]; endpointResults?: Record<string, any> }> { const errors: string[] = [] const endpointResults: Record<string, any> = {} - let overallSuccess = true - - // 변경사항을 DOLCE 시스템 형태로 변환 - const syncData = await this.transformChangesForDOLCE(changes) - - // DOLCE 엔드포인트 호출들을 직접 정의 - const endpointPromises = [] - - // 1. DOLCE 메인 엔드포인트 - const mainUrl = process.env.SYNC_DOLCE_URL - if (mainUrl) { - endpointPromises.push( - (async () => { - try { - console.log(`Sending to DOLCE main: ${mainUrl}`) - - const transformedData = { - contractId, - systemType: 'DOLCE', - changes: syncData, - batchSize: changes.length, - timestamp: new Date().toISOString(), - source: 'EVCP', - version: '1.0' - } - - // 헤더 구성 (토큰이 있을 때만 Authorization 포함) - const headers: Record<string, string> = { - 'Content-Type': 'application/json', - 'X-API-Version': process.env.SYNC_DOLCE_VERSION || 'v1', - 'X-System': 'DOLCE' - } - - if (process.env.SYNC_DOLCE_TOKEN) { - headers['Authorization'] = `Bearer ${process.env.SYNC_DOLCE_TOKEN}` - } - - const response = await fetch(mainUrl, { - method: 'POST', - headers, - body: JSON.stringify(transformedData) - }) - - if (!response.ok) { - const errorText = await response.text() - throw new Error(`DOLCE main: HTTP ${response.status} - ${errorText}`) - } - - const result = await response.json() - endpointResults['dolce_main'] = result - - console.log(`✅ DOLCE main sync successful`) - return { success: true, endpoint: 'dolce_main', result } - } catch (error) { - const errorMessage = `DOLCE main: ${error instanceof Error ? error.message : 'Unknown error'}` - errors.push(errorMessage) - overallSuccess = false - - console.error(`❌ DOLCE main sync failed:`, error) - return { success: false, endpoint: 'dolce_main', error: errorMessage } - } - })() - ) - } - - // 2. DOLCE 문서 전용 엔드포인트 (선택사항) - const docUrl = process.env.SYNC_DOLCE_DOCUMENT_URL - if (docUrl) { - endpointPromises.push( - (async () => { - try { - console.log(`Sending to DOLCE documents: ${docUrl}`) - - const documentData = { - documents: syncData.filter(item => item.entityType === 'document'), - source: 'EVCP_DOLCE', - timestamp: new Date().toISOString() - } - - // 헤더 구성 (토큰이 있을 때만 Authorization 포함) - const headers: Record<string, string> = { - 'Content-Type': 'application/json' - } - - if (process.env.SYNC_DOLCE_TOKEN) { - headers['Authorization'] = `Bearer ${process.env.SYNC_DOLCE_TOKEN}` - } - - const response = await fetch(docUrl, { - method: 'PUT', - headers, - body: JSON.stringify(documentData) - }) - - if (!response.ok) { - const errorText = await response.text() - throw new Error(`DOLCE documents: HTTP ${response.status} - ${errorText}`) - } + try { + // DOLCE 업로드 서비스 동적 임포트 + const { dolceUploadService } = await import('./dolce-upload-service') + + if (!dolceUploadService.isUploadEnabled()) { + throw new Error('DOLCE upload is not enabled') + } - const result = await response.json() - endpointResults['dolce_documents'] = result + // 변경사항에서 리비전 ID들 추출 + const revisionIds = changes + .filter(change => change.entityType === 'revision') + .map(change => change.entityId) - console.log(`✅ DOLCE documents sync successful`) - return { success: true, endpoint: 'dolce_documents', result } + if (revisionIds.length === 0) { + return { + success: true, + successCount: 0, + failureCount: 0, + endpointResults: { message: 'No revisions to upload' } + } + } - } catch (error) { - const errorMessage = `DOLCE documents: ${error instanceof Error ? error.message : 'Unknown error'}` - errors.push(errorMessage) - overallSuccess = false - - console.error(`❌ DOLCE documents sync failed:`, error) - return { success: false, endpoint: 'dolce_documents', error: errorMessage } - } - })() + // DOLCE 업로드 실행 + const uploadResult = await dolceUploadService.uploadToDoLCE( + contractId, + revisionIds, + 'system_user', // 시스템 사용자 ID + 'System Upload' ) - } - if (endpointPromises.length === 0) { - throw new Error('No DOLCE sync endpoints configured') - } + endpointResults['dolce_upload'] = uploadResult - // 모든 엔드포인트 요청 완료 대기 - const results = await Promise.allSettled(endpointPromises) - - // 결과 집계 - const successfulEndpoints = results.filter(r => r.status === 'fulfilled' && r.value.success).length - const totalEndpoints = endpointPromises.length + if (uploadResult.success) { + console.log(`✅ DOLCE upload successful: ${uploadResult.uploadedDocuments} documents, ${uploadResult.uploadedFiles} files`) + + return { + success: true, + successCount: changes.length, + failureCount: 0, + endpointResults + } + } else { + console.error(`❌ DOLCE upload failed:`, uploadResult.errors) + + return { + success: false, + successCount: 0, + failureCount: changes.length, + errors: uploadResult.errors, + endpointResults + } + } - console.log(`DOLCE endpoint results: ${successfulEndpoints}/${totalEndpoints} successful`) + } catch (error) { + const errorMessage = `DOLCE upload failed: ${error instanceof Error ? error.message : 'Unknown error'}` + errors.push(errorMessage) + console.error(`❌ DOLCE upload error:`, error) - return { - success: overallSuccess && errors.length === 0, - successCount: overallSuccess ? changes.length : 0, - failureCount: overallSuccess ? 0 : changes.length, - errors: errors.length > 0 ? errors : undefined, - endpointResults + return { + success: false, + successCount: 0, + failureCount: changes.length, + errors, + endpointResults + } } } @@ -560,78 +487,6 @@ class SyncService { } /** - * DOLCE 시스템용 데이터 변환 - */ - private async transformChangesForDOLCE(changes: ChangeLog[]): Promise<SyncableEntity[]> { - const syncData: SyncableEntity[] = [] - - for (const change of changes) { - try { - let entityData = null - - // 엔티티 타입별로 현재 데이터 조회 - switch (change.entityType) { - case 'document': - if (change.action !== 'DELETE') { - const [document] = await db - .select() - .from(documents) - .where(eq(documents.id, change.entityId)) - .limit(1) - entityData = document - } - break - - case 'revision': - if (change.action !== 'DELETE') { - const [revision] = await db - .select() - .from(revisions) - .where(eq(revisions.id, change.entityId)) - .limit(1) - entityData = revision - } - break - - case 'attachment': - if (change.action !== 'DELETE') { - const [attachment] = await db - .select() - .from(documentAttachments) - .where(eq(documentAttachments.id, change.entityId)) - .limit(1) - entityData = attachment - } - break - } - - // DOLCE 특화 데이터 구조 - syncData.push({ - entityType: change.entityType as any, - entityId: change.entityId, - action: change.action as any, - data: entityData || change.oldValues, - metadata: { - changeId: change.id, - changedAt: change.createdAt, - changedBy: change.userName, - changedFields: change.changedFields, - // DOLCE 전용 메타데이터 - dolceVersion: '2.0', - processingPriority: change.entityType === 'revision' ? 'HIGH' : 'NORMAL', - requiresApproval: change.action === 'DELETE' - } - }) - - } catch (error) { - console.error(`Failed to transform change ${change.id} for DOLCE:`, error) - } - } - - return syncData - } - - /** * SWP 시스템용 데이터 변환 */ private async transformChangesForSWP(changes: ChangeLog[]): Promise<SyncableEntity[]> { @@ -759,7 +614,7 @@ class SyncService { await db.update(revisions) .set({ revisionStatus: "SUBMITTED", - externalSentAt: new Date().toISOString().slice(0, 10) + submittedDate: new Date().toISOString().slice(0, 10) }) .where(inArray(revisions.id, revisionIds)) } diff --git a/lib/vendor-document-list/table/enhanced-doc-table-toolbar-actions.tsx b/lib/vendor-document-list/table/enhanced-doc-table-toolbar-actions.tsx index fa1b957b..3960bbce 100644 --- a/lib/vendor-document-list/table/enhanced-doc-table-toolbar-actions.tsx +++ b/lib/vendor-document-list/table/enhanced-doc-table-toolbar-actions.tsx @@ -1,8 +1,7 @@ "use client" - import * as React from "react" import { type Table } from "@tanstack/react-table" -import { Download, Upload, Plus, Files } from "lucide-react" +import { Download, Upload, Plus, Files, RefreshCw } from "lucide-react" import { toast } from "sonner" import { exportTableToExcel } from "@/lib/export" @@ -13,6 +12,8 @@ import { DeleteDocumentsDialog } from "./delete-docs-dialog" import { BulkUploadDialog } from "./bulk-upload-dialog" import type { EnhancedDocument } from "@/types/enhanced-documents" import { SendToSHIButton } from "./send-to-shi-button" +import { ImportFromDOLCEButton } from "./import-from-dolce-button" +import { SWPWorkflowPanel } from "./swp-workflow-panel" interface EnhancedDocTableToolbarActionsProps { table: Table<EnhancedDocument> @@ -50,9 +51,17 @@ export function EnhancedDocTableToolbarActions({ }, 500) } + const handleImportComplete = () => { + // 가져오기 완료 후 테이블 새로고침 + table.resetRowSelection() + setTimeout(() => { + window.location.reload() + }, 500) + } + return ( <div className="flex items-center gap-2"> - {/* 기존 액션들 */} + {/* 삭제 버튼 */} {table.getFilteredSelectedRowModel().rows.length > 0 ? ( <DeleteDocumentsDialog documents={table @@ -62,14 +71,27 @@ export function EnhancedDocTableToolbarActions({ /> ) : null} - {/* ✅ AddDocumentListDialog에 필요한 props 전달 */} - <AddDocumentListDialog - projectType={projectType} - contractId={selectedPackageId} - onSuccess={handleDocumentAdded} // ✅ 성공 콜백 추가 - /> - - {/* 일괄 업로드 버튼 */} + {/* projectType에 따른 조건부 렌더링 */} + {projectType === "ship" ? ( + <> + {/* SHIP: DOLCE에서 목록 가져오기 */} + <ImportFromDOLCEButton + contractId={selectedPackageId} + onImportComplete={handleImportComplete} + /> + </> + ) : ( + <> + {/* PLANT: 수동 문서 추가 */} + <AddDocumentListDialog + projectType={projectType} + contractId={selectedPackageId} + onSuccess={handleDocumentAdded} + /> + </> + )} + + {/* 일괄 업로드 버튼 (공통) */} <Button variant="outline" onClick={() => setBulkUploadDialogOpen(true)} @@ -79,7 +101,7 @@ export function EnhancedDocTableToolbarActions({ 일괄 업로드 </Button> - {/* Export 버튼 */} + {/* Export 버튼 (공통) */} <Button variant="outline" size="sm" @@ -95,7 +117,7 @@ export function EnhancedDocTableToolbarActions({ <span className="hidden sm:inline">Export</span> </Button> - {/* ✅ 새로운 Send to SHI 버튼으로 교체 */} + {/* Send to SHI 버튼 (공통) - 내부 → 외부로 보내기 */} <SendToSHIButton contractId={selectedPackageId} documents={allDocuments} @@ -103,6 +125,15 @@ export function EnhancedDocTableToolbarActions({ projectType={projectType} /> + {/* SWP 전용 워크플로우 패널 */} + {projectType === "plant" && ( + <SWPWorkflowPanel + contractId={selectedPackageId} + documents={allDocuments} + onWorkflowUpdate={handleSyncComplete} + /> + )} + {/* 일괄 업로드 다이얼로그 */} <BulkUploadDialog open={bulkUploadDialogOpen} diff --git a/lib/vendor-document-list/table/enhanced-documents-table.tsx b/lib/vendor-document-list/table/enhanced-documents-table.tsx index f840a10c..3bd6668d 100644 --- a/lib/vendor-document-list/table/enhanced-documents-table.tsx +++ b/lib/vendor-document-list/table/enhanced-documents-table.tsx @@ -46,8 +46,6 @@ export function EnhancedDocumentsTable({ // 데이터 로딩 const [{ data, pageCount, total }] = React.use(promises) - console.log(data) - // 상태 관리 const [rowAction, setRowAction] = React.useState<DataTableRowAction<EnhancedDocument> | null>(null) diff --git a/lib/vendor-document-list/table/import-from-dolce-button.tsx b/lib/vendor-document-list/table/import-from-dolce-button.tsx new file mode 100644 index 00000000..519d40cb --- /dev/null +++ b/lib/vendor-document-list/table/import-from-dolce-button.tsx @@ -0,0 +1,356 @@ +"use client" + +import * as React from "react" +import { RefreshCw, Download, Loader2, CheckCircle, AlertTriangle } from "lucide-react" +import { toast } from "sonner" + +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover" +import { Badge } from "@/components/ui/badge" +import { Progress } from "@/components/ui/progress" +import { Separator } from "@/components/ui/separator" + +interface ImportFromDOLCEButtonProps { + contractId: number + onImportComplete?: () => void +} + +interface ImportStatus { + lastImportAt?: string + availableDocuments: number + newDocuments: number + updatedDocuments: number + importEnabled: boolean +} + +export function ImportFromDOLCEButton({ + contractId, + onImportComplete +}: ImportFromDOLCEButtonProps) { + const [isDialogOpen, setIsDialogOpen] = React.useState(false) + const [importProgress, setImportProgress] = React.useState(0) + const [isImporting, setIsImporting] = React.useState(false) + const [importStatus, setImportStatus] = React.useState<ImportStatus | null>(null) + const [statusLoading, setStatusLoading] = React.useState(false) + + // DOLCE 상태 조회 + const fetchImportStatus = async () => { + setStatusLoading(true) + try { + const response = await fetch(`/api/sync/import/status?contractId=${contractId}&sourceSystem=DOLCE`) + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + throw new Error(errorData.message || 'Failed to fetch import status') + } + + const status = await response.json() + setImportStatus(status) + + // 프로젝트 코드가 없는 경우 에러 처리 + if (status.error) { + toast.error(`상태 확인 실패: ${status.error}`) + setImportStatus(null) + } + } catch (error) { + console.error('Failed to fetch import status:', error) + toast.error('DOLCE 상태를 확인할 수 없습니다. 프로젝트 설정을 확인해주세요.') + setImportStatus(null) + } finally { + setStatusLoading(false) + } + } + + // 컴포넌트 마운트 시 상태 조회 + React.useEffect(() => { + fetchImportStatus() + }, [contractId]) + + const handleImport = async () => { + if (!contractId) return + + setImportProgress(0) + setIsImporting(true) + + try { + // 진행률 시뮬레이션 + const progressInterval = setInterval(() => { + setImportProgress(prev => Math.min(prev + 15, 90)) + }, 300) + + const response = await fetch('/api/sync/import', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + contractId, + sourceSystem: 'DOLCE' + }) + }) + + if (!response.ok) { + const errorData = await response.json() + throw new Error(errorData.message || 'Import failed') + } + + const result = await response.json() + + clearInterval(progressInterval) + setImportProgress(100) + + setTimeout(() => { + setImportProgress(0) + setIsDialogOpen(false) + setIsImporting(false) + + if (result?.success) { + const { newCount = 0, updatedCount = 0, skippedCount = 0 } = result + toast.success( + `DOLCE 가져오기 완료`, + { + description: `신규 ${newCount}건, 업데이트 ${updatedCount}건, 건너뜀 ${skippedCount}건 (B3/B4/B5 포함)` + } + ) + } else { + toast.error( + `DOLCE 가져오기 부분 실패`, + { + description: result?.message || '일부 DrawingKind에서 가져오기에 실패했습니다.' + } + ) + } + + fetchImportStatus() // 상태 갱신 + onImportComplete?.() + }, 500) + + } catch (error) { + setImportProgress(0) + setIsImporting(false) + + toast.error('DOLCE 가져오기 실패', { + description: error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.' + }) + } + } + + const getStatusBadge = () => { + if (statusLoading) { + return <Badge variant="secondary">DOLCE 연결 확인 중...</Badge> + } + + if (!importStatus) { + return <Badge variant="destructive">DOLCE 연결 오류</Badge> + } + + if (!importStatus.importEnabled) { + return <Badge variant="secondary">DOLCE 가져오기 비활성화</Badge> + } + + if (importStatus.newDocuments > 0 || importStatus.updatedDocuments > 0) { + return ( + <Badge variant="default" className="gap-1 bg-blue-500 hover:bg-blue-600"> + <AlertTriangle className="w-3 h-3" /> + 업데이트 가능 (B3/B4/B5) + </Badge> + ) + } + + return ( + <Badge variant="default" className="gap-1 bg-green-500 hover:bg-green-600"> + <CheckCircle className="w-3 h-3" /> + DOLCE와 동기화됨 + </Badge> + ) + } + + const canImport = importStatus?.importEnabled && + (importStatus?.newDocuments > 0 || importStatus?.updatedDocuments > 0) + + return ( + <> + <Popover> + <PopoverTrigger asChild> + <div className="flex items-center gap-3"> + <Button + variant="outline" + size="sm" + className="flex items-center border-blue-200 hover:bg-blue-50" + disabled={isImporting || statusLoading} + > + {isImporting ? ( + <Loader2 className="w-4 h-4 animate-spin" /> + ) : ( + <Download className="w-4 h-4" /> + )} + <span className="hidden sm:inline">DOLCE에서 가져오기</span> + {importStatus && (importStatus.newDocuments > 0 || importStatus.updatedDocuments > 0) && ( + <Badge + variant="default" + className="h-5 w-5 p-0 text-xs flex items-center justify-center bg-blue-500" + > + {importStatus.newDocuments + importStatus.updatedDocuments} + </Badge> + )} + </Button> + </div> + </PopoverTrigger> + + <PopoverContent className="w-80"> + <div className="space-y-4"> + <div className="space-y-2"> + <h4 className="font-medium">DOLCE 가져오기 상태</h4> + <div className="flex items-center justify-between"> + <span className="text-sm text-muted-foreground">현재 상태</span> + {getStatusBadge()} + </div> + </div> + + {importStatus && ( + <div className="space-y-3"> + <Separator /> + + <div className="grid grid-cols-2 gap-4 text-sm"> + <div> + <div className="text-muted-foreground">신규 문서</div> + <div className="font-medium">{importStatus.newDocuments || 0}건</div> + </div> + <div> + <div className="text-muted-foreground">업데이트</div> + <div className="font-medium">{importStatus.updatedDocuments || 0}건</div> + </div> + </div> + + <div className="text-sm"> + <div className="text-muted-foreground">DOLCE 전체 문서 (B3/B4/B5)</div> + <div className="font-medium">{importStatus.availableDocuments || 0}건</div> + </div> + + {importStatus.lastImportAt && ( + <div className="text-sm"> + <div className="text-muted-foreground">마지막 가져오기</div> + <div className="font-medium"> + {new Date(importStatus.lastImportAt).toLocaleString()} + </div> + </div> + )} + </div> + )} + + <Separator /> + + <div className="flex gap-2"> + <Button + onClick={() => setIsDialogOpen(true)} + disabled={!canImport || isImporting} + className="flex-1" + size="sm" + > + {isImporting ? ( + <> + <Loader2 className="w-4 h-4 mr-2 animate-spin" /> + 가져오는 중... + </> + ) : ( + <> + <Download className="w-4 h-4 mr-2" /> + 지금 가져오기 + </> + )} + </Button> + + <Button + variant="outline" + size="sm" + onClick={fetchImportStatus} + disabled={statusLoading} + > + {statusLoading ? ( + <Loader2 className="w-4 h-4 animate-spin" /> + ) : ( + <RefreshCw className="w-4 h-4" /> + )} + </Button> + </div> + </div> + </PopoverContent> + </Popover> + + {/* 가져오기 진행 다이얼로그 */} + <Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}> + <DialogContent className="sm:max-w-md"> + <DialogHeader> + <DialogTitle>DOLCE에서 문서 목록 가져오기</DialogTitle> + <DialogDescription> + 삼성중공업 DOLCE 시스템에서 최신 문서 목록을 가져옵니다. + </DialogDescription> + </DialogHeader> + + <div className="space-y-4"> + {importStatus && ( + <div className="rounded-lg border p-4 space-y-3"> + <div className="flex items-center justify-between text-sm"> + <span>가져올 항목</span> + <span className="font-medium"> + {(importStatus.newDocuments || 0) + (importStatus.updatedDocuments || 0)}건 + </span> + </div> + + <div className="text-xs text-muted-foreground"> + 신규 문서와 업데이트된 문서가 포함됩니다. (B3, B4, B5) + <br /> + B4 문서의 경우 GTTPreDwg, GTTWorkingDwg 이슈 스테이지가 자동 생성됩니다. + </div> + + {isImporting && ( + <div className="space-y-2"> + <div className="flex items-center justify-between text-sm"> + <span>진행률</span> + <span>{importProgress}%</span> + </div> + <Progress value={importProgress} className="h-2" /> + </div> + )} + </div> + )} + + <div className="flex justify-end gap-2"> + <Button + variant="outline" + onClick={() => setIsDialogOpen(false)} + disabled={isImporting} + > + 취소 + </Button> + <Button + onClick={handleImport} + disabled={isImporting || !canImport} + > + {isImporting ? ( + <> + <Loader2 className="w-4 h-4 mr-2 animate-spin" /> + 가져오는 중... + </> + ) : ( + <> + <Download className="w-4 h-4 mr-2" /> + 가져오기 시작 + </> + )} + </Button> + </div> + </div> + </DialogContent> + </Dialog> + </> + ) +}
\ No newline at end of file diff --git a/lib/vendor-document-list/table/swp-workflow-panel.tsx b/lib/vendor-document-list/table/swp-workflow-panel.tsx new file mode 100644 index 00000000..ded306e7 --- /dev/null +++ b/lib/vendor-document-list/table/swp-workflow-panel.tsx @@ -0,0 +1,370 @@ +"use client" + +import * as React from "react" +import { Send, Eye, CheckCircle, Clock, RefreshCw, AlertTriangle, Loader2 } from "lucide-react" +import { toast } from "sonner" + +import { Button } from "@/components/ui/button" +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover" +import { Badge } from "@/components/ui/badge" +import { Separator } from "@/components/ui/separator" +import { Progress } from "@/components/ui/progress" +import type { EnhancedDocument } from "@/types/enhanced-documents" + +interface SWPWorkflowPanelProps { + contractId: number + documents: EnhancedDocument[] + onWorkflowUpdate?: () => void +} + +type WorkflowStatus = + | 'IDLE' // 대기 상태 + | 'SUBMITTED' // 목록 전송됨 + | 'UNDER_REVIEW' // 검토 중 + | 'CONFIRMED' // 컨펌됨 + | 'REVISION_REQUIRED' // 수정 요청됨 + | 'RESUBMITTED' // 재전송됨 + | 'APPROVED' // 최종 승인됨 + +interface WorkflowState { + status: WorkflowStatus + lastUpdatedAt?: string + pendingActions: string[] + confirmationData?: any + revisionComments?: string[] + approvalData?: any +} + +export function SWPWorkflowPanel({ + contractId, + documents, + onWorkflowUpdate +}: SWPWorkflowPanelProps) { + const [workflowState, setWorkflowState] = React.useState<WorkflowState | null>(null) + const [isLoading, setIsLoading] = React.useState(false) + const [actionProgress, setActionProgress] = React.useState(0) + + // 워크플로우 상태 조회 + const fetchWorkflowStatus = async () => { + setIsLoading(true) + try { + const response = await fetch(`/api/sync/workflow/status?contractId=${contractId}&targetSystem=SWP`) + if (!response.ok) throw new Error('Failed to fetch workflow status') + + const status = await response.json() + setWorkflowState(status) + } catch (error) { + console.error('Failed to fetch workflow status:', error) + toast.error('워크플로우 상태를 확인할 수 없습니다') + } finally { + setIsLoading(false) + } + } + + // 컴포넌트 마운트 시 상태 조회 + React.useEffect(() => { + fetchWorkflowStatus() + }, [contractId]) + + // 워크플로우 액션 실행 + const executeWorkflowAction = async (action: string) => { + setActionProgress(0) + setIsLoading(true) + + try { + // 진행률 시뮬레이션 + const progressInterval = setInterval(() => { + setActionProgress(prev => Math.min(prev + 20, 90)) + }, 200) + + const response = await fetch('/api/sync/workflow/action', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + contractId, + targetSystem: 'SWP', + action, + documents: documents.map(doc => ({ id: doc.id, documentNo: doc.documentNo })) + }) + }) + + if (!response.ok) { + const errorData = await response.json() + throw new Error(errorData.message || 'Workflow action failed') + } + + const result = await response.json() + + clearInterval(progressInterval) + setActionProgress(100) + + setTimeout(() => { + setActionProgress(0) + + if (result?.success) { + toast.success( + `${getActionLabel(action)} 완료`, + { description: result?.message || '워크플로우가 성공적으로 진행되었습니다.' } + ) + } else { + toast.error( + `${getActionLabel(action)} 실패`, + { description: result?.message || '워크플로우 실행에 실패했습니다.' } + ) + } + + fetchWorkflowStatus() // 상태 갱신 + onWorkflowUpdate?.() + }, 500) + + } catch (error) { + setActionProgress(0) + + toast.error(`${getActionLabel(action)} 실패`, { + description: error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.' + }) + } finally { + setIsLoading(false) + } + } + + const getActionLabel = (action: string): string => { + switch (action) { + case 'SUBMIT_LIST': return '목록 전송' + case 'CHECK_CONFIRMATION': return '컨펌 확인' + case 'RESUBMIT_REVISED': return '수정본 재전송' + case 'CHECK_APPROVAL': return '승인 확인' + default: return action + } + } + + const getStatusBadge = () => { + if (isLoading) { + return <Badge variant="secondary">확인 중...</Badge> + } + + if (!workflowState) { + return <Badge variant="destructive">오류</Badge> + } + + switch (workflowState.status) { + case 'IDLE': + return <Badge variant="secondary">대기</Badge> + case 'SUBMITTED': + return ( + <Badge variant="default" className="gap-1 bg-blue-500"> + <Clock className="w-3 h-3" /> + 전송됨 + </Badge> + ) + case 'UNDER_REVIEW': + return ( + <Badge variant="default" className="gap-1 bg-yellow-500"> + <Eye className="w-3 h-3" /> + 검토 중 + </Badge> + ) + case 'CONFIRMED': + return ( + <Badge variant="default" className="gap-1 bg-green-500"> + <CheckCircle className="w-3 h-3" /> + 컨펌됨 + </Badge> + ) + case 'REVISION_REQUIRED': + return ( + <Badge variant="destructive" className="gap-1"> + <AlertTriangle className="w-3 h-3" /> + 수정 요청 + </Badge> + ) + case 'RESUBMITTED': + return ( + <Badge variant="default" className="gap-1 bg-orange-500"> + <RefreshCw className="w-3 h-3" /> + 재전송됨 + </Badge> + ) + case 'APPROVED': + return ( + <Badge variant="default" className="gap-1 bg-green-600"> + <CheckCircle className="w-3 h-3" /> + 승인 완료 + </Badge> + ) + default: + return <Badge variant="secondary">알 수 없음</Badge> + } + } + + const getAvailableActions = (): string[] => { + if (!workflowState) return [] + + switch (workflowState.status) { + case 'IDLE': + return ['SUBMIT_LIST'] + case 'SUBMITTED': + return ['CHECK_CONFIRMATION'] + case 'UNDER_REVIEW': + return ['CHECK_CONFIRMATION'] + case 'CONFIRMED': + return [] // 컨펌되면 자동으로 다음 단계로 + case 'REVISION_REQUIRED': + return ['RESUBMIT_REVISED'] + case 'RESUBMITTED': + return ['CHECK_APPROVAL'] + case 'APPROVED': + return [] // 완료 상태 + default: + return [] + } + } + + const availableActions = getAvailableActions() + + return ( + <Popover> + <PopoverTrigger asChild> + <div className="flex items-center gap-3"> + <Button + variant="outline" + size="sm" + className="flex items-center border-orange-200 hover:bg-orange-50" + disabled={isLoading} + > + {isLoading ? ( + <Loader2 className="w-4 h-4 animate-spin" /> + ) : ( + <RefreshCw className="w-4 h-4" /> + )} + <span className="hidden sm:inline">SWP 워크플로우</span> + {workflowState?.pendingActions && workflowState.pendingActions.length > 0 && ( + <Badge + variant="destructive" + className="h-5 w-5 p-0 text-xs flex items-center justify-center" + > + {workflowState.pendingActions.length} + </Badge> + )} + </Button> + </div> + </PopoverTrigger> + + <PopoverContent className="w-80"> + <div className="space-y-4"> + <div className="space-y-2"> + <h4 className="font-medium">SWP 워크플로우 상태</h4> + <div className="flex items-center justify-between"> + <span className="text-sm text-muted-foreground">현재 상태</span> + {getStatusBadge()} + </div> + </div> + + {workflowState && ( + <div className="space-y-3"> + <Separator /> + + {/* 대기 중인 액션들 */} + {workflowState.pendingActions && workflowState.pendingActions.length > 0 && ( + <div className="space-y-2"> + <div className="text-sm font-medium">대기 중인 작업</div> + {workflowState.pendingActions.map((action, index) => ( + <Badge key={index} variant="outline" className="mr-1"> + {getActionLabel(action)} + </Badge> + ))} + </div> + )} + + {/* 수정 요청 사항 */} + {workflowState.revisionComments && workflowState.revisionComments.length > 0 && ( + <div className="space-y-2"> + <div className="text-sm font-medium text-red-600">수정 요청 사항</div> + <div className="text-xs text-muted-foreground space-y-1"> + {workflowState.revisionComments.map((comment, index) => ( + <div key={index} className="p-2 bg-red-50 rounded text-red-700"> + {comment} + </div> + ))} + </div> + </div> + )} + + {/* 마지막 업데이트 시간 */} + {workflowState.lastUpdatedAt && ( + <div className="text-sm"> + <div className="text-muted-foreground">마지막 업데이트</div> + <div className="font-medium"> + {new Date(workflowState.lastUpdatedAt).toLocaleString()} + </div> + </div> + )} + + {/* 진행률 표시 */} + {isLoading && actionProgress > 0 && ( + <div className="space-y-2"> + <div className="flex items-center justify-between text-sm"> + <span>진행률</span> + <span>{actionProgress}%</span> + </div> + <Progress value={actionProgress} className="h-2" /> + </div> + )} + </div> + )} + + <Separator /> + + {/* 액션 버튼들 */} + <div className="space-y-2"> + {availableActions.length > 0 ? ( + availableActions.map((action) => ( + <Button + key={action} + onClick={() => executeWorkflowAction(action)} + disabled={isLoading} + className="w-full justify-start" + size="sm" + variant={action.includes('SUBMIT') || action.includes('RESUBMIT') ? 'default' : 'outline'} + > + {isLoading ? ( + <Loader2 className="w-4 h-4 mr-2 animate-spin" /> + ) : ( + <Send className="w-4 h-4 mr-2" /> + )} + {getActionLabel(action)} + </Button> + )) + ) : ( + <div className="text-sm text-muted-foreground text-center py-2"> + {workflowState?.status === 'APPROVED' + ? '워크플로우가 완료되었습니다.' + : '실행 가능한 작업이 없습니다.'} + </div> + )} + + {/* 상태 새로고침 버튼 */} + <Button + variant="outline" + size="sm" + onClick={fetchWorkflowStatus} + disabled={isLoading} + className="w-full" + > + {isLoading ? ( + <Loader2 className="w-4 h-4 mr-2 animate-spin" /> + ) : ( + <RefreshCw className="w-4 h-4 mr-2" /> + )} + 상태 새로고침 + </Button> + </div> + </div> + </PopoverContent> + </Popover> + ) +}
\ No newline at end of file diff --git a/lib/vendor-document-list/workflow-service.ts b/lib/vendor-document-list/workflow-service.ts new file mode 100644 index 00000000..6efff012 --- /dev/null +++ b/lib/vendor-document-list/workflow-service.ts @@ -0,0 +1,195 @@ +// lib/vendor-document-list/workflow-service.ts +class WorkflowService { + /** + * 워크플로우 상태 조회 + */ + async getWorkflowStatus( + contractId: number, + targetSystem: string = 'SWP' + ): Promise<any> { + try { + // 워크플로우 상태를 DB나 외부 시스템에서 조회 + // 실제 구현에서는 workflow_states 테이블이나 외부 API를 사용 + + const mockStatus = { + status: 'IDLE', + lastUpdatedAt: new Date().toISOString(), + pendingActions: ['SUBMIT_LIST'], + confirmationData: null, + revisionComments: [], + approvalData: null + } + + return mockStatus + + } catch (error) { + console.error('Failed to get workflow status:', error) + throw error + } + } + + /** + * 워크플로우 액션 실행 + */ + async executeWorkflowAction( + contractId: number, + targetSystem: string, + action: string, + documents: any[], + userId: number, + userName: string + ): Promise<any> { + try { + console.log(`Executing workflow action: ${action} for contract ${contractId}`) + + switch (action) { + case 'SUBMIT_LIST': + return await this.submitDocumentList(contractId, targetSystem, documents, userId, userName) + + case 'CHECK_CONFIRMATION': + return await this.checkConfirmation(contractId, targetSystem) + + case 'RESUBMIT_REVISED': + return await this.resubmitRevisedList(contractId, targetSystem, documents, userId, userName) + + case 'CHECK_APPROVAL': + return await this.checkApproval(contractId, targetSystem) + + default: + throw new Error(`Unknown workflow action: ${action}`) + } + + } catch (error) { + console.error('Workflow action failed:', error) + throw error + } + } + + /** + * 문서 목록 전송 + */ + private async submitDocumentList( + contractId: number, + targetSystem: string, + documents: any[], + userId: number, + userName: string + ): Promise<any> { + // SWP 시스템으로 문서 목록 전송 + const endpoint = process.env.SWP_SUBMIT_URL + if (!endpoint) { + throw new Error('SWP submit endpoint not configured') + } + + const payload = { + contractId, + documents: documents.map(doc => ({ + documentNo: doc.documentNo, + documentName: doc.documentName, + documentType: doc.documentType, + status: doc.documentStatus + })), + submittedBy: userName, + submittedAt: new Date().toISOString() + } + + const response = await fetch(endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Basic ${Buffer.from(`${process.env.SWP_USER}:${process.env.SWP_PASSWORD}`).toString('base64')}` + }, + body: JSON.stringify(payload) + }) + + if (!response.ok) { + const errorText = await response.text() + throw new Error(`SWP submit failed: HTTP ${response.status} - ${errorText}`) + } + + const result = await response.json() + + // 워크플로우 상태 업데이트 (DB 저장) + // await this.updateWorkflowStatus(contractId, targetSystem, 'SUBMITTED', result) + + return { + success: true, + message: '문서 목록이 성공적으로 전송되었습니다.', + submissionId: result.submissionId + } + } + + /** + * 컨펌 상태 확인 + */ + private async checkConfirmation( + contractId: number, + targetSystem: string + ): Promise<any> { + // SWP 시스템에서 컨펌 상태 조회 + const endpoint = process.env.SWP_STATUS_URL + if (!endpoint) { + throw new Error('SWP status endpoint not configured') + } + + const response = await fetch(`${endpoint}?contractId=${contractId}`, { + method: 'GET', + headers: { + 'Authorization': `Basic ${Buffer.from(`${process.env.SWP_USER}:${process.env.SWP_PASSWORD}`).toString('base64')}` + } + }) + + if (!response.ok) { + const errorText = await response.text() + throw new Error(`SWP status check failed: HTTP ${response.status} - ${errorText}`) + } + + const result = await response.json() + + // 상태에 따른 다음 액션 결정 + let nextStatus = 'UNDER_REVIEW' + let message = '검토가 진행 중입니다.' + + if (result.status === 'CONFIRMED') { + nextStatus = 'CONFIRMED' + message = '문서 목록이 컨펌되었습니다.' + } else if (result.status === 'REVISION_REQUIRED') { + nextStatus = 'REVISION_REQUIRED' + message = '수정이 요청되었습니다.' + } + + return { + success: true, + message, + status: nextStatus, + confirmationData: result + } + } + + /** + * 수정된 목록 재전송 + */ + private async resubmitRevisedList( + contractId: number, + targetSystem: string, + documents: any[], + userId: number, + userName: string + ): Promise<any> { + // 수정된 문서 목록 재전송 로직 + return await this.submitDocumentList(contractId, targetSystem, documents, userId, userName) + } + + /** + * 최종 승인 확인 + */ + private async checkApproval( + contractId: number, + targetSystem: string + ): Promise<any> { + // 최종 승인 상태 확인 로직 + return await this.checkConfirmation(contractId, targetSystem) + } +} + +export const workflowService = new WorkflowService()
\ No newline at end of file |
