diff options
Diffstat (limited to 'lib/vendor-document-list')
| -rw-r--r-- | lib/vendor-document-list/dolce-upload-service.ts | 107 | ||||
| -rw-r--r-- | lib/vendor-document-list/enhanced-document-service.ts | 11 | ||||
| -rw-r--r-- | lib/vendor-document-list/import-service.ts | 287 | ||||
| -rw-r--r-- | lib/vendor-document-list/ship/enhanced-doc-table-columns.tsx | 42 | ||||
| -rw-r--r-- | lib/vendor-document-list/ship/enhanced-documents-table.tsx | 76 | ||||
| -rw-r--r-- | lib/vendor-document-list/ship/import-from-dolce-button.tsx | 86 | ||||
| -rw-r--r-- | lib/vendor-document-list/ship/send-to-shi-button.tsx | 286 |
7 files changed, 530 insertions, 365 deletions
diff --git a/lib/vendor-document-list/dolce-upload-service.ts b/lib/vendor-document-list/dolce-upload-service.ts index d98a4c70..032b028c 100644 --- a/lib/vendor-document-list/dolce-upload-service.ts +++ b/lib/vendor-document-list/dolce-upload-service.ts @@ -17,6 +17,19 @@ export interface DOLCEUploadResult { } } +interface ResultData { + FileId: string; + UploadId: string; + FileSeq: number; + FileName: string; + FileRelativePath: string; + FileSize: number; + FileCreateDT: string; // ISO string format + FileWriteDT: string; // ISO string format + OwnerUserId: string; +} + + interface FileReaderConfig { baseDir: string; isProduction: boolean; @@ -339,8 +352,10 @@ private async uploadFiles( uploadId: string // 이미 생성된 UploadId를 매개변수로 받음 ): Promise<Array<{ uploadId: string, fileId: string, filePath: string }>> { const uploadResults = [] + const resultDataArray: ResultData[] = [] - for (const attachment of attachments) { + for (let i = 0; i < attachments.length; i++) { + const attachment = attachments[i] try { // FileId만 새로 생성 (UploadId는 이미 생성된 것 사용) const fileId = uuidv4() @@ -386,6 +401,23 @@ private async uploadFiles( filePath: dolceFilePath }) + // ResultData 객체 생성 (PWPUploadResultService 호출용) + const fileStats = await this.getFileStats(attachment.filePath) // 파일 통계 정보 조회 + + const resultData: ResultData = { + FileId: fileId, + UploadId: uploadId, + FileSeq: i + 1, // 1부터 시작하는 시퀀스 + FileName: attachment.fileName, + FileRelativePath: dolceFilePath, + FileSize: fileStats.size, + FileCreateDT: fileStats.birthtime.toISOString(), + FileWriteDT: fileStats.mtime.toISOString(), + OwnerUserId: userId + } + + resultDataArray.push(resultData) + console.log(`✅ File uploaded successfully: ${attachment.fileName} -> ${dolceFilePath}`) console.log(`✅ DB updated for attachment ID: ${attachment.id}`) @@ -395,9 +427,82 @@ private async uploadFiles( } } + // 모든 파일 업로드가 완료된 후 PWPUploadResultService 호출 + if (resultDataArray.length > 0) { + try { + await this.finalizeUploadResult(resultDataArray) + console.log(`✅ Upload result finalized for UploadId: ${uploadId}`) + } catch (error) { + console.error(`❌ Failed to finalize upload result for UploadId: ${uploadId}`, error) + // 파일 업로드는 성공했지만 결과 저장 실패 - 로그만 남기고 계속 진행 + } + } + return uploadResults } + +private async finalizeUploadResult(resultDataArray: ResultData[]): Promise<void> { + const url = `${this.UPLOAD_SERVICE_URL}/PWPUploadResultService.ashx` + + try { + const jsonData = JSON.stringify(resultDataArray) + const dataBuffer = Buffer.from(jsonData, 'utf-8') + + console.log(`Calling PWPUploadResultService with ${resultDataArray.length} files`) + console.log('ResultData:', JSON.stringify(resultDataArray, null, 2)) + + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: dataBuffer + }) + + if (!response.ok) { + const errorText = await response.text() + throw new Error(`PWPUploadResultService failed: HTTP ${response.status} - ${errorText}`) + } + + const result = await response.text() + + if (result !== 'Success') { + throw new Error(`PWPUploadResultService returned unexpected result: ${result}`) + } + + console.log('✅ PWPUploadResultService call successful') + + } catch (error) { + console.error('❌ PWPUploadResultService call failed:', error) + throw error + } +} + +// 파일 통계 정보 조회 헬퍼 메서드 (파일시스템에서 파일 정보를 가져옴) +private async getFileStats(filePath: string): Promise<{ size: number, birthtime: Date, mtime: Date }> { + try { + // Node.js 환경이라면 fs.stat 사용 + const fs = require('fs').promises + const stats = await fs.stat(filePath) + + return { + size: stats.size, + birthtime: stats.birthtime, + mtime: stats.mtime + } + } catch (error) { + console.warn(`Could not get file stats for ${filePath}, using defaults`) + // 파일 정보를 가져올 수 없는 경우 기본값 사용 + const now = new Date() + return { + size: 0, + birthtime: now, + mtime: now + } + } +} + /** * 문서 정보 업로드 (DetailDwgReceiptMgmtEdit) */ diff --git a/lib/vendor-document-list/enhanced-document-service.ts b/lib/vendor-document-list/enhanced-document-service.ts index e01283dc..6fe8feb7 100644 --- a/lib/vendor-document-list/enhanced-document-service.ts +++ b/lib/vendor-document-list/enhanced-document-service.ts @@ -20,7 +20,7 @@ import type { Revision } from "@/types/enhanced-documents" import { GetVendorShipDcoumentsSchema } from "./validations" -import { contracts, users } from "@/db/schema" +import { contracts, users, vendors } from "@/db/schema" // 스키마 타입 정의 export interface GetEnhancedDocumentsSchema { @@ -1092,11 +1092,12 @@ export async function getDocumentDetails(documentId: number) { // 벤더 정보 조회 const [vendorInfo] = await tx .select({ - vendorName: simplifiedDocumentsView.vendorName, - vendorCode: simplifiedDocumentsView.vendorCode, + vendorName: vendors.vendorName, + vendorCode: vendors.vendorCode, }) - .from(simplifiedDocumentsView) - .where(eq(simplifiedDocumentsView.contractId, contractIds[0])) + .from(contracts) + .leftJoin(vendors, eq(contracts.vendorId, vendors.id)) + .where(eq(contracts.id, contractIds[0])) .limit(1) return { data, total, drawingKind, vendorInfo } diff --git a/lib/vendor-document-list/import-service.ts b/lib/vendor-document-list/import-service.ts index bc384ea2..c7ba041a 100644 --- a/lib/vendor-document-list/import-service.ts +++ b/lib/vendor-document-list/import-service.ts @@ -1332,156 +1332,189 @@ class ImportService { /** * 가져오기 상태 조회 */ - 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) - )) + /** + * 가져오기 상태 조회 - 에러 시 안전한 기본값 반환 + */ +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) + // 프로젝트 코드와 벤더 코드 조회 + const contractInfo = await this.getContractInfoById(contractId) - if (!contractInfo?.projectCode || !contractInfo?.vendorCode) { - throw new Error(`Project code or vendor code not found for contract ${contractId}`) + // 🔥 계약 정보가 없으면 기본 상태 반환 (에러 throw 하지 않음) + if (!contractInfo?.projectCode || !contractInfo?.vendorCode) { + console.warn(`Project code or vendor code not found for contract ${contractId}`) + return { + lastImportAt: lastImport?.lastSynced ? new Date(lastImport.lastSynced).toISOString() : undefined, + availableDocuments: 0, + newDocuments: 0, + updatedDocuments: 0, + availableRevisions: 0, + newRevisions: 0, + updatedRevisions: 0, + availableAttachments: 0, + newAttachments: 0, + updatedAttachments: 0, + importEnabled: false, // 🔥 계약 정보가 없으면 import 비활성화 + error: `Contract ${contractId}에 대한 프로젝트 코드 또는 벤더 코드를 찾을 수 없습니다.` // 🔥 에러 메시지 추가 } + } - let availableDocuments = 0 - let newDocuments = 0 - let updatedDocuments = 0 - let availableRevisions = 0 - let newRevisions = 0 - let updatedRevisions = 0 - let availableAttachments = 0 - let newAttachments = 0 - let updatedAttachments = 0 + let availableDocuments = 0 + let newDocuments = 0 + let updatedDocuments = 0 + let availableRevisions = 0 + let newRevisions = 0 + let updatedRevisions = 0 + let availableAttachments = 0 + let newAttachments = 0 + let updatedAttachments = 0 - try { - // 각 drawingKind별로 확인 - const drawingKinds = ['B3', 'B4', 'B5'] + 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++ - } + 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++ } } + } - // revisions 및 attachments 상태도 확인 - try { - const detailDocs = await this.fetchDetailFromDOLCE( - externalDoc.ProjectNo, - externalDoc.DrawingNo, - externalDoc.Discipline, - externalDoc.DrawingKind - ) - availableRevisions += detailDocs.length - - for (const detailDoc of detailDocs) { - const existingRevision = await db - .select({ id: revisions.id }) - .from(revisions) - .where(eq(revisions.registerId, detailDoc.RegisterId)) - .limit(1) - - if (existingRevision.length === 0) { - newRevisions++ - } else { - updatedRevisions++ - } + // revisions 및 attachments 상태도 확인 + try { + const detailDocs = await this.fetchDetailFromDOLCE( + externalDoc.ProjectNo, + externalDoc.DrawingNo, + externalDoc.Discipline, + externalDoc.DrawingKind + ) + availableRevisions += detailDocs.length + + for (const detailDoc of detailDocs) { + const existingRevision = await db + .select({ id: revisions.id }) + .from(revisions) + .where(eq(revisions.registerId, detailDoc.RegisterId)) + .limit(1) + + if (existingRevision.length === 0) { + newRevisions++ + } else { + updatedRevisions++ + } - // FS Category 문서의 첨부파일 확인 - if (detailDoc.Category === 'FS' && detailDoc.UploadId) { - try { - const fileInfos = await this.fetchFileInfoFromDOLCE(detailDoc.UploadId) - availableAttachments += fileInfos.filter(f => f.UseYn === 'Y').length - - for (const fileInfo of fileInfos) { - if (fileInfo.UseYn !== 'Y') continue - - const existingAttachment = await db - .select({ id: documentAttachments.id }) - .from(documentAttachments) - .where(eq(documentAttachments.fileId, fileInfo.FileId)) - .limit(1) - - if (existingAttachment.length === 0) { - newAttachments++ - } else { - updatedAttachments++ - } + // FS Category 문서의 첨부파일 확인 + if (detailDoc.Category === 'FS' && detailDoc.UploadId) { + try { + const fileInfos = await this.fetchFileInfoFromDOLCE(detailDoc.UploadId) + availableAttachments += fileInfos.filter(f => f.UseYn === 'Y').length + + for (const fileInfo of fileInfos) { + if (fileInfo.UseYn !== 'Y') continue + + const existingAttachment = await db + .select({ id: documentAttachments.id }) + .from(documentAttachments) + .where(eq(documentAttachments.fileId, fileInfo.FileId)) + .limit(1) + + if (existingAttachment.length === 0) { + newAttachments++ + } else { + updatedAttachments++ } - } catch (error) { - console.warn(`Failed to check files for ${detailDoc.UploadId}:`, error) } + } catch (error) { + console.warn(`Failed to check files for ${detailDoc.UploadId}:`, error) } } - } catch (error) { - console.warn(`Failed to check revisions for ${externalDoc.DrawingNo}:`, error) } + } catch (error) { + console.warn(`Failed to check revisions for ${externalDoc.DrawingNo}:`, error) } - } catch (error) { - console.warn(`Failed to check ${drawingKind} for status:`, error) } + } catch (error) { + console.warn(`Failed to check ${drawingKind} for status:`, error) } - } catch (error) { - console.warn(`Failed to fetch external data for status: ${error}`) } + } catch (error) { + console.warn(`Failed to fetch external data for status: ${error}`) + // 🔥 외부 API 호출 실패 시에도 기본값 반환 + } - return { - lastImportAt: lastImport?.lastSynced ? new Date(lastImport.lastSynced).toISOString() : undefined, - availableDocuments, - newDocuments, - updatedDocuments, - availableRevisions, - newRevisions, - updatedRevisions, - availableAttachments, - newAttachments, - updatedAttachments, - importEnabled: this.isImportEnabled(sourceSystem) - } + return { + lastImportAt: lastImport?.lastSynced ? new Date(lastImport.lastSynced).toISOString() : undefined, + availableDocuments, + newDocuments, + updatedDocuments, + availableRevisions, + newRevisions, + updatedRevisions, + availableAttachments, + newAttachments, + updatedAttachments, + importEnabled: this.isImportEnabled(sourceSystem) + } - } catch (error) { - console.error('Failed to get import status:', error) - throw error + } catch (error) { + // 🔥 최종적으로 모든 에러를 catch하여 안전한 기본값 반환 + console.error('Failed to get import status:', error) + return { + lastImportAt: undefined, + availableDocuments: 0, + newDocuments: 0, + updatedDocuments: 0, + availableRevisions: 0, + newRevisions: 0, + updatedRevisions: 0, + availableAttachments: 0, + newAttachments: 0, + updatedAttachments: 0, + importEnabled: false, + error: error instanceof Error ? error.message : 'Unknown error occurred' } } +} /** * 가져오기 활성화 여부 확인 diff --git a/lib/vendor-document-list/ship/enhanced-doc-table-columns.tsx b/lib/vendor-document-list/ship/enhanced-doc-table-columns.tsx index 9c13573c..51c104dc 100644 --- a/lib/vendor-document-list/ship/enhanced-doc-table-columns.tsx +++ b/lib/vendor-document-list/ship/enhanced-doc-table-columns.tsx @@ -59,7 +59,7 @@ export function getSimplifiedDocumentColumns({ id: "select", header: ({ table }) => ( <div className="flex items-center justify-center"> - <span className="text-xs text-gray-500">선택</span> + <span className="text-xs text-gray-500">Select</span> </div> ), cell: ({ row }) => { @@ -78,7 +78,7 @@ export function getSimplifiedDocumentColumns({ { accessorKey: "docNumber", header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="문서번호" /> + <DataTableColumnHeaderSimple column={column} title="Document No" /> ), cell: ({ row }) => { const doc = row.original @@ -90,7 +90,7 @@ export function getSimplifiedDocumentColumns({ size: 120, enableResizing: true, meta: { - excelHeader: "문서번호" + excelHeader: "Document No" }, }, @@ -98,7 +98,7 @@ export function getSimplifiedDocumentColumns({ { accessorKey: "title", header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="문서명" /> + <DataTableColumnHeaderSimple column={column} title="Title" /> ), cell: ({ row }) => { const doc = row.original @@ -110,7 +110,7 @@ export function getSimplifiedDocumentColumns({ enableResizing: true, maxSize:300, meta: { - excelHeader: "문서명" + excelHeader: "Title" }, }, @@ -118,7 +118,7 @@ export function getSimplifiedDocumentColumns({ { accessorKey: "projectCode", header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="프로젝트" /> + <DataTableColumnHeaderSimple column={column} title="Project" /> ), cell: ({ row }) => { const projectCode = row.original.projectCode @@ -131,7 +131,7 @@ export function getSimplifiedDocumentColumns({ maxSize:100, meta: { - excelHeader: "프로젝트" + excelHeader: "Project" }, }, @@ -141,7 +141,7 @@ export function getSimplifiedDocumentColumns({ header: ({ table }) => { // 첫 번째 행의 firstStageName을 그룹 헤더로 사용 const firstRow = table.getRowModel().rows[0]?.original - const stageName = firstRow?.firstStageName || "1차 스테이지" + const stageName = firstRow?.firstStageName || "First Stage" return ( <div className="text-center font-medium text-gray-700"> {stageName} @@ -152,27 +152,27 @@ export function getSimplifiedDocumentColumns({ { accessorKey: "firstStagePlanDate", header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="계획일" /> + <DataTableColumnHeaderSimple column={column} title="Planned Date" /> ), cell: ({ row }) => { return <FirstStagePlanDateCell row={row} /> }, enableResizing: true, meta: { - excelHeader: "1차 계획일" + excelHeader: "First Planned Date" }, }, { accessorKey: "firstStageActualDate", header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="실제일" /> + <DataTableColumnHeaderSimple column={column} title="Actual Date" /> ), cell: ({ row }) => { return <FirstStageActualDateCell row={row} /> }, enableResizing: true, meta: { - excelHeader: "1차 실제일" + excelHeader: "First Actual Date" }, }, ], @@ -184,7 +184,7 @@ export function getSimplifiedDocumentColumns({ header: ({ table }) => { // 첫 번째 행의 secondStageName을 그룹 헤더로 사용 const firstRow = table.getRowModel().rows[0]?.original - const stageName = firstRow?.secondStageName || "2차 스테이지" + const stageName = firstRow?.secondStageName || "Second Stage" return ( <div className="text-center font-medium text-gray-700"> {stageName} @@ -195,27 +195,27 @@ export function getSimplifiedDocumentColumns({ { accessorKey: "secondStagePlanDate", header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="계획일" /> + <DataTableColumnHeaderSimple column={column} title="Planned Date" /> ), cell: ({ row }) => { return <SecondStagePlanDateCell row={row} /> }, enableResizing: true, meta: { - excelHeader: "2차 계획일" + excelHeader: "Second Planned Date" }, }, { accessorKey: "secondStageActualDate", header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="실제일" /> + <DataTableColumnHeaderSimple column={column} title="Actual Date" /> ), cell: ({ row }) => { return <SecondStageActualDateCell row={row} /> }, enableResizing: true, meta: { - excelHeader: "2차 실제일" + excelHeader: "Second Actual Date" }, }, ], @@ -225,7 +225,7 @@ export function getSimplifiedDocumentColumns({ { accessorKey: "attachmentCount", header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="파일" /> + <DataTableColumnHeaderSimple column={column} title="Files" /> ), cell: ({ row }) => { const count = row.original.attachmentCount || 0 @@ -237,7 +237,7 @@ export function getSimplifiedDocumentColumns({ size: 60, enableResizing: true, meta: { - excelHeader: "첨부파일" + excelHeader: "Attachments" }, }, @@ -245,7 +245,7 @@ export function getSimplifiedDocumentColumns({ { accessorKey: "updatedAt", header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="업데이트" /> + <DataTableColumnHeaderSimple column={column} title="Updated" /> ), cell: ({ cell, row }) => { return ( @@ -254,7 +254,7 @@ export function getSimplifiedDocumentColumns({ }, enableResizing: true, meta: { - excelHeader: "업데이트" + excelHeader: "Updated" }, }, diff --git a/lib/vendor-document-list/ship/enhanced-documents-table.tsx b/lib/vendor-document-list/ship/enhanced-documents-table.tsx index 2354a9be..9885c027 100644 --- a/lib/vendor-document-list/ship/enhanced-documents-table.tsx +++ b/lib/vendor-document-list/ship/enhanced-documents-table.tsx @@ -25,17 +25,17 @@ import { EnhancedDocTableToolbarActions } from "./enhanced-doc-table-toolbar-act const DRAWING_KIND_INFO = { B3: { title: "B3 Vendor", - description: "Approval → Work 단계로 진행되는 승인 중심 도면", + description: "Approval-focused drawings progressing through Approval → Work stages", color: "bg-blue-50 text-blue-700 border-blue-200" }, B4: { title: "B4 GTT", - description: "Pre → Work 단계로 진행되는 DOLCE 연동 도면", + description: "DOLCE-integrated drawings progressing through Pre → Work stages", color: "bg-green-50 text-green-700 border-green-200" }, B5: { title: "B5 FMEA", - description: "First → Second 단계로 진행되는 순차적 도면", + description: "Sequential drawings progressing through First → Second stages", color: "bg-purple-50 text-purple-700 border-purple-200" } } as const @@ -79,22 +79,22 @@ export function SimplifiedDocumentsTable({ const advancedFilterFields: DataTableAdvancedFilterField<SimplifiedDocumentsView>[] = [ { id: "docNumber", - label: "문서번호", - type: "text", - }, - { - id: "vendorDocNumber", - label: "벤더 문서번호", + label: "Document No", type: "text", }, + // { + // id: "vendorDocNumber", + // label: "Vendor Document No", + // type: "text", + // }, { id: "title", - label: "문서제목", + label: "Document Title", type: "text", }, { id: "drawingKind", - label: "문서종류", + label: "Document Type", type: "select", options: [ { label: "B3", value: "B3" }, @@ -104,78 +104,78 @@ export function SimplifiedDocumentsTable({ }, { id: "projectCode", - label: "프로젝트 코드", + label: "Project Code", type: "text", }, { id: "vendorName", - label: "벤더명", + label: "Vendor Name", type: "text", }, { id: "vendorCode", - label: "벤더 코드", + label: "Vendor Code", type: "text", }, { id: "pic", - label: "담당자", + label: "PIC", type: "text", }, { id: "status", - label: "문서 상태", + label: "Document Status", type: "select", options: [ - { label: "활성", value: "ACTIVE" }, - { label: "비활성", value: "INACTIVE" }, - { label: "보류", value: "PENDING" }, - { label: "완료", value: "COMPLETED" }, + { label: "Active", value: "ACTIVE" }, + { label: "Inactive", value: "INACTIVE" }, + { label: "Pending", value: "PENDING" }, + { label: "Completed", value: "COMPLETED" }, ], }, { id: "firstStageName", - label: "1차 스테이지", + label: "First Stage", type: "text", }, { id: "secondStageName", - label: "2차 스테이지", + label: "Second Stage", type: "text", }, { id: "firstStagePlanDate", - label: "1차 계획일", + label: "First Planned Date", type: "date", }, { id: "firstStageActualDate", - label: "1차 실제일", + label: "First Actual Date", type: "date", }, { id: "secondStagePlanDate", - label: "2차 계획일", + label: "Second Planned Date", type: "date", }, { id: "secondStageActualDate", - label: "2차 실제일", + label: "Second Actual Date", type: "date", }, { id: "issuedDate", - label: "발행일", + label: "Issue Date", type: "date", }, { id: "createdAt", - label: "생성일", + label: "Created Date", type: "date", }, { id: "updatedAt", - label: "수정일", + label: "Updated Date", type: "date", }, ] @@ -184,32 +184,32 @@ export function SimplifiedDocumentsTable({ const b4FilterFields: DataTableAdvancedFilterField<SimplifiedDocumentsView>[] = [ { id: "cGbn", - label: "C 구분", + label: "C Category", type: "text", }, { id: "dGbn", - label: "D 구분", + label: "D Category", type: "text", }, { id: "degreeGbn", - label: "Degree 구분", + label: "Degree Category", type: "text", }, { id: "deptGbn", - label: "Dept 구분", + label: "Dept Category", type: "text", }, { id: "jGbn", - label: "J 구분", + label: "J Category", type: "text", }, { id: "sGbn", - label: "S 구분", + label: "S Category", type: "text", }, ] @@ -246,17 +246,17 @@ export function SimplifiedDocumentsTable({ {kindInfo && ( <div className="flex items-center justify-between"> <div className="flex items-center gap-4"> - <Badge variant="default" className="flex items-center gap-1 text-sm"> + {/* <Badge variant="default" className="flex items-center gap-1 text-sm"> <FileText className="w-4 h-4" /> {kindInfo.title} </Badge> <span className="text-sm text-muted-foreground"> {kindInfo.description} - </span> + </span> */} </div> <div className="flex items-center gap-2"> <Badge variant="outline"> - {total}개 문서 + {total} documents </Badge> </div> </div> diff --git a/lib/vendor-document-list/ship/import-from-dolce-button.tsx b/lib/vendor-document-list/ship/import-from-dolce-button.tsx index d4728d22..de9e63bc 100644 --- a/lib/vendor-document-list/ship/import-from-dolce-button.tsx +++ b/lib/vendor-document-list/ship/import-from-dolce-button.tsx @@ -72,7 +72,7 @@ export function ImportFromDOLCEButton({ setVendorContractIds(contractIds) } catch (error) { console.error('Failed to fetch vendor contracts:', error) - toast.error('계약 정보를 가져오는데 실패했습니다.') + toast.error('Failed to fetch contract information.') } finally { setLoadingVendorContracts(false) } @@ -142,7 +142,7 @@ export function ImportFromDOLCEButton({ } catch (error) { console.error('Failed to fetch import statuses:', error) - toast.error('상태를 확인할 수 없습니다. 프로젝트 설정을 확인해주세요.') + toast.error('Unable to check status. Please verify project settings.') } finally { setStatusLoading(false) } @@ -230,16 +230,16 @@ export function ImportFromDOLCEButton({ if (totalResult.success) { toast.success( - `DOLCE 가져오기 완료`, + `DOLCE import completed`, { - description: `신규 ${totalResult.newCount}건, 업데이트 ${totalResult.updatedCount}건, 건너뜀 ${totalResult.skippedCount}건 (${contractIds.length}개 계약)` + description: `New ${totalResult.newCount}, Updated ${totalResult.updatedCount}, Skipped ${totalResult.skippedCount} (${contractIds.length} contracts)` } ) } else { toast.error( - `DOLCE 가져오기 부분 실패`, + `DOLCE import partially failed`, { - description: '일부 계약에서 가져오기에 실패했습니다.' + description: 'Some contracts failed to import.' } ) } @@ -252,34 +252,34 @@ export function ImportFromDOLCEButton({ setImportProgress(0) setIsImporting(false) - toast.error('DOLCE 가져오기 실패', { - description: error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.' + toast.error('DOLCE import failed', { + description: error instanceof Error ? error.message : 'An unknown error occurred.' }) } } const getStatusBadge = () => { if (loadingVendorContracts) { - return <Badge variant="secondary">계약 정보 로딩 중...</Badge> + return <Badge variant="secondary">Loading contract information...</Badge> } if (statusLoading) { - return <Badge variant="secondary">DOLCE 연결 확인 중...</Badge> + return <Badge variant="secondary">Checking DOLCE connection...</Badge> } if (importStatusMap.size === 0) { - return <Badge variant="destructive">DOLCE 연결 오류</Badge> + return <Badge variant="destructive">DOLCE Connection Error</Badge> } if (!totalStats.importEnabled) { - return <Badge variant="secondary">DOLCE 가져오기 비활성화</Badge> + return <Badge variant="secondary">DOLCE Import Disabled</Badge> } if (totalStats.newDocuments > 0 || totalStats.updatedDocuments > 0) { return ( <Badge variant="samsung" className="gap-1"> <AlertTriangle className="w-3 h-3" /> - 업데이트 가능 ({contractIds.length}개 계약) + Updates Available ({contractIds.length} contracts) </Badge> ) } @@ -287,7 +287,7 @@ export function ImportFromDOLCEButton({ return ( <Badge variant="default" className="gap-1 bg-green-500 hover:bg-green-600"> <CheckCircle className="w-3 h-3" /> - DOLCE와 동기화됨 + Synchronized with DOLCE </Badge> ) } @@ -316,7 +316,7 @@ export function ImportFromDOLCEButton({ ) : ( <Download className="w-4 h-4" /> )} - <span className="hidden sm:inline">DOLCE에서 가져오기</span> + <span className="hidden sm:inline">Import from DOLCE</span> {totalStats.newDocuments + totalStats.updatedDocuments > 0 && ( <Badge variant="samsung" @@ -332,9 +332,9 @@ export function ImportFromDOLCEButton({ <PopoverContent className="w-96"> <div className="space-y-4"> <div className="space-y-2"> - <h4 className="font-medium">DOLCE 가져오기 상태</h4> + <h4 className="font-medium">DOLCE Import Status</h4> <div className="flex items-center justify-between"> - <span className="text-sm text-muted-foreground">현재 상태</span> + <span className="text-sm text-muted-foreground">Current Status</span> {getStatusBadge()} </div> </div> @@ -342,15 +342,15 @@ export function ImportFromDOLCEButton({ {/* 계약 소스 표시 */} {allDocuments.length === 0 && vendorContractIds.length > 0 && ( <div className="text-xs text-blue-600 bg-blue-50 p-2 rounded"> - 문서가 없어서 전체 계약에서 가져오기를 진행합니다. + No documents found, importing from all contracts. </div> )} {/* 다중 계약 정보 표시 */} {contractIds.length > 1 && ( <div className="text-sm"> - <div className="text-muted-foreground">대상 계약</div> - <div className="font-medium">{contractIds.length}개 계약</div> + <div className="text-muted-foreground">Target Contracts</div> + <div className="font-medium">{contractIds.length} contracts</div> <div className="text-xs text-muted-foreground"> Contract IDs: {contractIds.join(', ')} </div> @@ -363,25 +363,25 @@ export function ImportFromDOLCEButton({ <div className="grid grid-cols-2 gap-4 text-sm"> <div> - <div className="text-muted-foreground">신규 문서</div> - <div className="font-medium">{totalStats.newDocuments || 0}건</div> + <div className="text-muted-foreground">New Documents</div> + <div className="font-medium">{totalStats.newDocuments || 0}</div> </div> <div> - <div className="text-muted-foreground">업데이트</div> - <div className="font-medium">{totalStats.updatedDocuments || 0}건</div> + <div className="text-muted-foreground">Updates</div> + <div className="font-medium">{totalStats.updatedDocuments || 0}</div> </div> </div> <div className="text-sm"> - <div className="text-muted-foreground">DOLCE 전체 문서 (B3/B4/B5)</div> - <div className="font-medium">{totalStats.availableDocuments || 0}건</div> + <div className="text-muted-foreground">Total DOLCE Documents (B3/B4/B5)</div> + <div className="font-medium">{totalStats.availableDocuments || 0}</div> </div> {/* 각 계약별 세부 정보 (펼치기/접기 가능) */} {contractIds.length > 1 && ( <details className="text-sm"> <summary className="cursor-pointer text-muted-foreground hover:text-foreground"> - 계약별 세부 정보 + Details by Contract </summary> <div className="mt-2 space-y-2 pl-2 border-l-2 border-muted"> {contractIds.map(contractId => { @@ -391,10 +391,10 @@ export function ImportFromDOLCEButton({ <div className="font-medium">Contract {contractId}</div> {status ? ( <div className="text-muted-foreground"> - 신규 {status.newDocuments}건, 업데이트 {status.updatedDocuments}건 + New {status.newDocuments}, Updates {status.updatedDocuments} </div> ) : ( - <div className="text-destructive">상태 확인 실패</div> + <div className="text-destructive">Status check failed</div> )} </div> ) @@ -417,12 +417,12 @@ export function ImportFromDOLCEButton({ {isImporting ? ( <> <Loader2 className="w-4 h-4 mr-2 animate-spin" /> - 가져오는 중... + Importing... </> ) : ( <> <Download className="w-4 h-4 mr-2" /> - 지금 가져오기 + Import Now </> )} </Button> @@ -448,10 +448,10 @@ export function ImportFromDOLCEButton({ <Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}> <DialogContent className="sm:max-w-md"> <DialogHeader> - <DialogTitle>DOLCE에서 문서 목록 가져오기</DialogTitle> + <DialogTitle>Import Document List from DOLCE</DialogTitle> <DialogDescription> - 삼성중공업 DOLCE 시스템에서 최신 문서 목록을 가져옵니다. - {contractIds.length > 1 && ` (${contractIds.length}개 계약 대상)`} + Import the latest document list from Samsung Heavy Industries DOLCE system. + {contractIds.length > 1 && ` (${contractIds.length} contracts targeted)`} </DialogDescription> </DialogHeader> @@ -459,20 +459,20 @@ export function ImportFromDOLCEButton({ {totalStats && ( <div className="rounded-lg border p-4 space-y-3"> <div className="flex items-center justify-between text-sm"> - <span>가져올 항목</span> + <span>Items to Import</span> <span className="font-medium"> - {totalStats.newDocuments + totalStats.updatedDocuments}건 + {totalStats.newDocuments + totalStats.updatedDocuments} </span> </div> <div className="text-xs text-muted-foreground"> - 신규 문서와 업데이트된 문서가 포함됩니다. (B3, B4, B5) + Includes new and updated documents (B3, B4, B5). <br /> - B4 문서의 경우 GTTPreDwg, GTTWorkingDwg 이슈 스테이지가 자동 생성됩니다. + For B4 documents, GTTPreDwg and GTTWorkingDwg issue stages will be auto-generated. {contractIds.length > 1 && ( <> <br /> - {contractIds.length}개 계약에서 순차적으로 가져옵니다. + Will import sequentially from {contractIds.length} contracts. </> )} </div> @@ -480,7 +480,7 @@ export function ImportFromDOLCEButton({ {isImporting && ( <div className="space-y-2"> <div className="flex items-center justify-between text-sm"> - <span>진행률</span> + <span>Progress</span> <span>{importProgress}%</span> </div> <Progress value={importProgress} className="h-2" /> @@ -495,7 +495,7 @@ export function ImportFromDOLCEButton({ onClick={() => setIsDialogOpen(false)} disabled={isImporting} > - 취소 + Cancel </Button> <Button onClick={handleImport} @@ -504,12 +504,12 @@ export function ImportFromDOLCEButton({ {isImporting ? ( <> <Loader2 className="w-4 h-4 mr-2 animate-spin" /> - 가져오는 중... + Importing... </> ) : ( <> <Download className="w-4 h-4 mr-2" /> - 가져오기 시작 + Start Import </> )} </Button> diff --git a/lib/vendor-document-list/ship/send-to-shi-button.tsx b/lib/vendor-document-list/ship/send-to-shi-button.tsx index 61893da5..4607c994 100644 --- a/lib/vendor-document-list/ship/send-to-shi-button.tsx +++ b/lib/vendor-document-list/ship/send-to-shi-button.tsx @@ -1,8 +1,8 @@ -// components/sync/send-to-shi-button.tsx (다중 계약 버전) +// components/sync/send-to-shi-button.tsx (최종 완성 버전) "use client" import * as React from "react" -import { Send, Loader2, CheckCircle, AlertTriangle, Settings } from "lucide-react" +import { Send, Loader2, CheckCircle, AlertTriangle, Settings, RefreshCw } from "lucide-react" import { toast } from "sonner" import { Button } from "@/components/ui/button" @@ -22,7 +22,9 @@ import { Badge } from "@/components/ui/badge" import { Progress } from "@/components/ui/progress" import { Separator } from "@/components/ui/separator" import { ScrollArea } from "@/components/ui/scroll-area" -import { useSyncStatus, useTriggerSync } from "@/hooks/use-sync-status" +import { Alert, AlertDescription } from "@/components/ui/alert" +// ✅ 업데이트된 Hook import +import { useClientSyncStatus, useTriggerSync, syncUtils } from "@/hooks/use-sync-status" import type { EnhancedDocument } from "@/types/enhanced-documents" interface SendToSHIButtonProps { @@ -31,13 +33,6 @@ interface SendToSHIButtonProps { projectType: "ship" | "plant" } -interface ContractSyncStatus { - contractId: number - syncStatus: any - isLoading: boolean - error: any -} - export function SendToSHIButton({ documents = [], onSyncComplete, @@ -49,75 +44,45 @@ export function SendToSHIButton({ const targetSystem = projectType === 'ship' ? "DOLCE" : "SWP" - // documents에서 contractId 목록 추출 + // 문서에서 유효한 계약 ID 목록 추출 const documentsContractIds = React.useMemo(() => { - const uniqueIds = [...new Set(documents.map(doc => doc.contractId).filter(Boolean))] + const validIds = documents + .map(doc => doc.contractId) + .filter((id): id is number => typeof id === 'number' && id > 0) + + const uniqueIds = [...new Set(validIds)] return uniqueIds.sort() }, [documents]) - // 각 contract별 동기화 상태 조회 - const contractStatuses = React.useMemo(() => { - return documentsContractIds.map(contractId => { - const { - syncStatus, - isLoading, - error, - refetch - } = useSyncStatus(contractId, targetSystem) - - return { - contractId, - syncStatus, - isLoading, - error, - refetch - } - }) - }, [documentsContractIds, targetSystem]) - - const { - triggerSync, - isLoading: isSyncing, - error: syncError - } = useTriggerSync() - - // 전체 통계 계산 - const totalStats = React.useMemo(() => { - let totalPending = 0 - let totalSynced = 0 - let totalFailed = 0 - let hasError = false - let isLoading = false - - contractStatuses.forEach(({ syncStatus, error, isLoading: loading }) => { - if (error) hasError = true - if (loading) isLoading = true - if (syncStatus) { - totalPending += syncStatus.pendingChanges || 0 - totalSynced += syncStatus.syncedChanges || 0 - totalFailed += syncStatus.failedChanges || 0 - } - }) - - return { - totalPending, - totalSynced, - totalFailed, - hasError, - isLoading, - canSync: totalPending > 0 && !hasError - } - }, [contractStatuses]) + // ✅ 클라이언트 전용 Hook 사용 (서버 사이드 렌더링 호환) + const { contractStatuses, totalStats, refetchAll } = useClientSyncStatus( + documentsContractIds, + targetSystem + ) + + const { triggerSync, isLoading: isSyncing, error: syncError } = useTriggerSync() - // 에러 상태 표시 + // 개발 환경에서 디버깅 정보 React.useEffect(() => { - if (totalStats.hasError) { - console.warn('Failed to load sync status for some contracts') + if (process.env.NODE_ENV === 'development') { + console.log('SendToSHIButton Debug Info:', { + documentsContractIds, + totalStats, + contractStatuses: contractStatuses.map(({ contractId, syncStatus, error }) => ({ + contractId, + pendingChanges: syncStatus?.pendingChanges, + hasError: !!error + })) + }) } - }, [totalStats.hasError]) + }, [documentsContractIds, totalStats, contractStatuses]) + // 동기화 실행 함수 const handleSync = async () => { - if (documentsContractIds.length === 0) return + if (documentsContractIds.length === 0) { + toast.info('동기화할 계약이 없습니다.') + return + } setSyncProgress(0) let successfulSyncs = 0 @@ -127,9 +92,17 @@ export function SendToSHIButton({ const errors: string[] = [] try { - const contractsToSync = contractStatuses.filter( - ({ syncStatus, error }) => !error && syncStatus?.syncEnabled && syncStatus?.pendingChanges > 0 - ) + // 동기화 가능한 계약들만 필터링 + const contractsToSync = contractStatuses.filter(({ syncStatus, error }) => { + if (error) { + console.warn(`Contract ${contractStatuses.find(c => c.error === error)?.contractId} has error:`, error) + return false + } + if (!syncStatus) return false + if (!syncStatus.syncEnabled) return false + if (syncStatus.pendingChanges <= 0) return false + return true + }) if (contractsToSync.length === 0) { toast.info('동기화할 변경사항이 없습니다.') @@ -137,12 +110,15 @@ export function SendToSHIButton({ return } + console.log(`Starting sync for ${contractsToSync.length} contracts`) + // 각 contract별로 순차 동기화 for (let i = 0; i < contractsToSync.length; i++) { const { contractId } = contractsToSync[i] setCurrentSyncingContract(contractId) try { + console.log(`Syncing contract ${contractId}...`) const result = await triggerSync({ contractId, targetSystem @@ -151,17 +127,19 @@ export function SendToSHIButton({ if (result?.success) { successfulSyncs++ totalSuccessCount += result.successCount || 0 + console.log(`Contract ${contractId} sync successful:`, result) } else { failedSyncs++ totalFailureCount += result?.failureCount || 0 - if (result?.errors?.[0]) { - errors.push(`Contract ${contractId}: ${result.errors[0]}`) - } + const errorMsg = result?.errors?.[0] || result?.message || 'Unknown sync error' + errors.push(`Contract ${contractId}: ${errorMsg}`) + console.error(`Contract ${contractId} sync failed:`, result) } } catch (error) { failedSyncs++ const errorMessage = error instanceof Error ? error.message : '알 수 없는 오류' errors.push(`Contract ${contractId}: ${errorMessage}`) + console.error(`Contract ${contractId} sync exception:`, error) } // 진행률 업데이트 @@ -170,6 +148,7 @@ export function SendToSHIButton({ setCurrentSyncingContract(null) + // 결과 처리 및 토스트 표시 setTimeout(() => { setSyncProgress(0) setIsDialogOpen(false) @@ -185,7 +164,7 @@ export function SendToSHIButton({ toast.warning( `부분 동기화 완료: ${successfulSyncs}개 성공, ${failedSyncs}개 실패`, { - description: errors[0] || '일부 계약 동기화에 실패했습니다.' + description: errors.slice(0, 3).join(', ') + (errors.length > 3 ? ' 외 더보기...' : '') } ) } else { @@ -198,7 +177,7 @@ export function SendToSHIButton({ } // 모든 contract 상태 갱신 - contractStatuses.forEach(({ refetch }) => refetch?.()) + refetchAll() onSyncComplete?.() }, 500) @@ -206,19 +185,32 @@ export function SendToSHIButton({ setSyncProgress(0) setCurrentSyncingContract(null) + const errorMessage = syncUtils.formatError(error as any) toast.error('동기화 실패', { - description: error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.' + description: errorMessage }) + console.error('Sync process failed:', error) } } + // 동기화 상태에 따른 뱃지 생성 const getSyncStatusBadge = () => { if (totalStats.isLoading) { - return <Badge variant="secondary">확인 중...</Badge> + return ( + <Badge variant="secondary" className="gap-1"> + <Loader2 className="w-3 h-3 animate-spin" /> + 확인 중... + </Badge> + ) } if (totalStats.hasError) { - return <Badge variant="destructive">오류</Badge> + return ( + <Badge variant="destructive" className="gap-1"> + <AlertTriangle className="w-3 h-3" /> + 연결 오류 + </Badge> + ) } if (documentsContractIds.length === 0) { @@ -246,10 +238,6 @@ export function SendToSHIButton({ return <Badge variant="secondary">변경사항 없음</Badge> } - const refreshAllStatuses = () => { - contractStatuses.forEach(({ refetch }) => refetch?.()) - } - return ( <> <Popover> @@ -258,7 +246,7 @@ export function SendToSHIButton({ <Button variant="default" size="sm" - className="flex items-center bg-blue-600 hover:bg-blue-700" + className="flex items-center gap-2 bg-blue-600 hover:bg-blue-700" disabled={isSyncing || totalStats.isLoading || documentsContractIds.length === 0} > {isSyncing ? ( @@ -270,7 +258,7 @@ export function SendToSHIButton({ {totalStats.totalPending > 0 && ( <Badge variant="destructive" - className="h-5 w-5 p-0 text-xs flex items-center justify-center" + className="h-5 w-5 p-0 text-xs flex items-center justify-center ml-1" > {totalStats.totalPending} </Badge> @@ -279,33 +267,66 @@ export function SendToSHIButton({ </div> </PopoverTrigger> - <PopoverContent className="w-96"> + <PopoverContent className="w-96" align="end"> <div className="space-y-4"> <div className="space-y-2"> - <h4 className="font-medium">SHI 동기화 상태</h4> + <div className="flex items-center justify-between"> + <h4 className="font-medium">SHI 동기화 상태</h4> + <Button + variant="ghost" + size="sm" + onClick={refetchAll} + disabled={totalStats.isLoading} + className="h-6 w-6 p-0" + > + {totalStats.isLoading ? ( + <Loader2 className="w-3 h-3 animate-spin" /> + ) : ( + <RefreshCw className="w-3 h-3" /> + )} + </Button> + </div> + <div className="flex items-center justify-between"> <span className="text-sm text-muted-foreground">전체 상태</span> {getSyncStatusBadge()} </div> + <div className="text-xs text-muted-foreground"> - {documentsContractIds.length}개 계약 대상 + {documentsContractIds.length}개 계약 대상 • {targetSystem} 시스템 </div> </div> + {/* 에러 상태 표시 */} + {totalStats.hasError && ( + <Alert variant="destructive"> + <AlertTriangle className="h-4 w-4" /> + <AlertDescription> + 일부 계약의 동기화 상태를 확인할 수 없습니다. 네트워크 연결을 확인해주세요. + {process.env.NODE_ENV === 'development' && ( + <div className="text-xs mt-1 font-mono"> + Debug: {contractStatuses.filter(({ error }) => error).length}개 계약에서 오류 + </div> + )} + </AlertDescription> + </Alert> + )} + + {/* 정상 상태일 때 통계 표시 */} {!totalStats.hasError && documentsContractIds.length > 0 && ( <div className="space-y-3"> <Separator /> <div className="grid grid-cols-3 gap-4 text-sm"> - <div> + <div className="text-center"> <div className="text-muted-foreground">대기 중</div> - <div className="font-medium">{totalStats.totalPending}건</div> + <div className="font-medium text-orange-600">{totalStats.totalPending}건</div> </div> - <div> + <div className="text-center"> <div className="text-muted-foreground">동기화됨</div> - <div className="font-medium">{totalStats.totalSynced}건</div> + <div className="font-medium text-green-600">{totalStats.totalSynced}건</div> </div> - <div> + <div className="text-center"> <div className="text-muted-foreground">실패</div> <div className="font-medium text-red-600">{totalStats.totalFailed}건</div> </div> @@ -319,17 +340,26 @@ export function SendToSHIButton({ <div className="space-y-2"> {contractStatuses.map(({ contractId, syncStatus, isLoading, error }) => ( <div key={contractId} className="flex items-center justify-between text-xs p-2 rounded border"> - <span>Contract {contractId}</span> + <span className="font-medium">Contract {contractId}</span> {isLoading ? ( - <Badge variant="secondary" className="text-xs">로딩...</Badge> + <Badge variant="secondary" className="text-xs"> + <Loader2 className="w-3 h-3 mr-1 animate-spin" /> + 로딩... + </Badge> ) : error ? ( - <Badge variant="destructive" className="text-xs">오류</Badge> - ) : syncStatus?.pendingChanges > 0 ? ( + <Badge variant="destructive" className="text-xs"> + <AlertTriangle className="w-3 h-3 mr-1" /> + 오류 + </Badge> + ) : syncStatus && syncStatus.pendingChanges > 0 ? ( <Badge variant="destructive" className="text-xs"> {syncStatus.pendingChanges}건 대기 </Badge> ) : ( - <Badge variant="secondary" className="text-xs">동기화됨</Badge> + <Badge variant="secondary" className="text-xs"> + <CheckCircle className="w-3 h-3 mr-1" /> + 최신 + </Badge> )} </div> ))} @@ -340,28 +370,18 @@ export function SendToSHIButton({ </div> )} - {totalStats.hasError && ( - <div className="space-y-2"> - <Separator /> - <div className="text-sm text-red-600"> - <div className="font-medium">연결 오류</div> - <div className="text-xs">일부 계약의 동기화 상태를 확인할 수 없습니다.</div> - </div> - </div> - )} - + {/* 계약 정보가 없는 경우 */} {documentsContractIds.length === 0 && ( - <div className="space-y-2"> - <Separator /> - <div className="text-sm text-muted-foreground"> - <div className="font-medium">계약 정보 없음</div> - <div className="text-xs">동기화할 문서가 없습니다.</div> - </div> - </div> + <Alert> + <AlertDescription> + 동기화할 문서가 없습니다. 문서를 선택해주세요. + </AlertDescription> + </Alert> )} <Separator /> + {/* 액션 버튼들 */} <div className="flex gap-2"> <Button onClick={() => setIsDialogOpen(true)} @@ -385,8 +405,9 @@ export function SendToSHIButton({ <Button variant="outline" size="sm" - onClick={refreshAllStatuses} + onClick={refetchAll} disabled={totalStats.isLoading} + className="px-3" > {totalStats.isLoading ? ( <Loader2 className="w-4 h-4 animate-spin" /> @@ -403,9 +424,12 @@ export function SendToSHIButton({ <Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}> <DialogContent className="sm:max-w-md"> <DialogHeader> - <DialogTitle>SHI 시스템으로 동기화</DialogTitle> + <DialogTitle className="flex items-center gap-2"> + <Send className="w-5 h-5" /> + SHI 시스템으로 동기화 + </DialogTitle> <DialogDescription> - {documentsContractIds.length}개 계약의 변경된 문서 데이터를 SHI 시스템으로 전송합니다. + {documentsContractIds.length}개 계약의 변경된 문서 데이터를 {targetSystem} 시스템으로 전송합니다. </DialogDescription> </DialogHeader> @@ -434,7 +458,8 @@ export function SendToSHIButton({ </div> <Progress value={syncProgress} className="h-2" /> {currentSyncingContract && ( - <div className="text-xs text-muted-foreground"> + <div className="text-xs text-muted-foreground flex items-center gap-1"> + <Loader2 className="w-3 h-3 animate-spin" /> 현재 처리 중: Contract {currentSyncingContract} </div> )} @@ -444,19 +469,20 @@ export function SendToSHIButton({ )} {totalStats.hasError && ( - <div className="rounded-lg border border-red-200 p-4"> - <div className="text-sm text-red-600"> - 일부 계약의 동기화 상태를 확인할 수 없습니다. 네트워크 연결을 확인해주세요. - </div> - </div> + <Alert variant="destructive"> + <AlertTriangle className="h-4 w-4" /> + <AlertDescription> + 일부 계약의 동기화 상태를 확인할 수 없습니다. 네트워크 연결을 확인하고 다시 시도해주세요. + </AlertDescription> + </Alert> )} {documentsContractIds.length === 0 && ( - <div className="rounded-lg border border-yellow-200 p-4"> - <div className="text-sm text-yellow-700"> + <Alert> + <AlertDescription> 동기화할 계약이 없습니다. 문서를 선택해주세요. - </div> - </div> + </AlertDescription> + </Alert> )} <div className="flex justify-end gap-2"> |
