summaryrefslogtreecommitdiff
path: root/lib/vendor-document-list
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-07-22 02:57:00 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-07-22 02:57:00 +0000
commitee57cc221ff2edafd3c0f12a181214c602ed257e (patch)
tree148f552f503798f7a350d6eff936b889f16be49f /lib/vendor-document-list
parent14f61e24947fb92dd71ec0a7196a6e815f8e66da (diff)
(대표님, 최겸) 이메일 템플릿, 벤더데이터 변경사항 대응, 기술영업 변경요구사항 구현
Diffstat (limited to 'lib/vendor-document-list')
-rw-r--r--lib/vendor-document-list/dolce-upload-service.ts107
-rw-r--r--lib/vendor-document-list/enhanced-document-service.ts11
-rw-r--r--lib/vendor-document-list/import-service.ts287
-rw-r--r--lib/vendor-document-list/ship/enhanced-doc-table-columns.tsx42
-rw-r--r--lib/vendor-document-list/ship/enhanced-documents-table.tsx76
-rw-r--r--lib/vendor-document-list/ship/import-from-dolce-button.tsx86
-rw-r--r--lib/vendor-document-list/ship/send-to-shi-button.tsx286
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">