From 74843fe598702a9a55f914f2d2d291368a5abb13 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Wed, 1 Oct 2025 10:31:23 +0000 Subject: (대표님) dolce 수정, spreadjs 수정 등 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/vendor-document-list/dolce-upload-service.ts | 49 ++++-- lib/vendor-document-list/import-service.ts | 8 +- .../plant/document-stages-columns.tsx | 20 +++ .../ship/enhanced-documents-table.tsx | 184 +++++++++++++++++++-- .../ship/send-to-shi-button.tsx | 108 +++++++++++- lib/vendor-document-list/sync-service.ts | 167 +++++++++++-------- 6 files changed, 433 insertions(+), 103 deletions(-) (limited to 'lib/vendor-document-list') diff --git a/lib/vendor-document-list/dolce-upload-service.ts b/lib/vendor-document-list/dolce-upload-service.ts index 85085d80..7717877b 100644 --- a/lib/vendor-document-list/dolce-upload-service.ts +++ b/lib/vendor-document-list/dolce-upload-service.ts @@ -293,6 +293,8 @@ class DOLCEUploadService { externalRegisterId: revisions.id, externalSentAt: revisions.submittedDate, + serialNo: revisions.serialNo, + // issueStages 테이블 정보 issueStageId: issueStages.id, stageName: issueStages.stageName, @@ -644,40 +646,53 @@ class DOLCEUploadService { if (revision.usage && revision.usage !== 'DEFAULT') { switch (revision.usage) { + case "APPROVAL": - if (revision.usageType === "Full") { - registerKind = "APPR" - } else if (revision.usageType === "Partial") { - registerKind = "APPR-P" - } else { - registerKind = "APPR" // 기본값 + if (revision.drawingKind === "B3") { + if (revision.usageType === "Full") { + registerKind = "APPR" + } else if (revision.usageType === "Partial") { + registerKind = "APPR-P" + } else { + registerKind = "APPR" // 기본값 + } } break case "WORKING": - if (revision.usageType === "Full") { - registerKind = "WORK" - } else if (revision.usageType === "Partial") { - registerKind = "WORK-P" - } else { - registerKind = "WORK" // 기본값 + if (revision.drawingKind === "B3") { + if (revision.usageType === "Full") { + registerKind = "WORK" + } else if (revision.usageType === "Partial") { + registerKind = "WORK-P" + } else { + registerKind = "WORK" // 기본값 + } } break case "The 1st": - registerKind = "FMEA-1" + if (revision.drawingKind === "B5") { + registerKind = "FMEA-1" + } break case "The 2nd": - registerKind = "FMEA-2" + if (revision.drawingKind === "B5") { + registerKind = "FMEA-2" + } break case "Pre": - registerKind = "RECP" + if (revision.drawingKind === "B3") { + registerKind = "RECP" + } break case "Working": - registerKind = "RECW" + if (revision.drawingKind === "B3") { + registerKind = "RECW" + } break case "Mark-Up": @@ -742,7 +757,7 @@ class DOLCEUploadService { DrawingNo: revision.documentNo, DrawingName: revision.documentName, RegisterGroupId: revision.registerGroupId || 0, - RegisterSerialNo: getSerialNumber(revision.revision || "1"), + RegisterSerialNo: revision.serialNo || getSerialNumber(revision.revision || "1"), RegisterKind: registerKind, // usage/usageType에 따라 동적 설정 DrawingRevNo: revision.revision || "-", Category: revision.category || "TS", diff --git a/lib/vendor-document-list/import-service.ts b/lib/vendor-document-list/import-service.ts index 13c51824..fb4db85e 100644 --- a/lib/vendor-document-list/import-service.ts +++ b/lib/vendor-document-list/import-service.ts @@ -1082,13 +1082,14 @@ class ImportService { issueStageId, revision: detailDoc.DrawingRevNo, uploaderType, - uploaderName: detailDoc.CreateUserNM, + registerSerialNoMax:detailDoc.RegisterSerialNoMax, + // uploaderName: detailDoc.CreateUserNM, usage, usageType, revisionStatus: detailDoc.Status, externalUploadId: detailDoc.UploadId, registerId: detailDoc.RegisterId, // 🆕 항상 최신 registerId로 업데이트 - comment: detailDoc.RegisterDesc, + comment: detailDoc.SHINote, submittedDate: this.convertDolceDateToDate(detailDoc.CreateDt), updatedAt: new Date() } @@ -1098,7 +1099,8 @@ class ImportService { const hasChanges = existingRevision.revision !== revisionData.revision || existingRevision.revisionStatus !== revisionData.revisionStatus || - existingRevision.uploaderName !== revisionData.uploaderName || + existingRevision.registerSerialNoMax !== revisionData.registerSerialNoMax || + // existingRevision.uploaderName !== revisionData.uploaderName || existingRevision.serialNo !== revisionData.serialNo || existingRevision.registerId !== revisionData.registerId // 🆕 registerId 변경 확인 diff --git a/lib/vendor-document-list/plant/document-stages-columns.tsx b/lib/vendor-document-list/plant/document-stages-columns.tsx index d71ecc0f..9a53b55b 100644 --- a/lib/vendor-document-list/plant/document-stages-columns.tsx +++ b/lib/vendor-document-list/plant/document-stages-columns.tsx @@ -347,6 +347,26 @@ export function getDocumentStagesColumns({ }, }, + { + accessorKey: "buyerSystemComment", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const doc = row.original + + return ( +
+ {doc.buyerSystemComment} +
+ ) + }, + size: 180, + enableResizing: true, + meta: { + excelHeader: "Document Comment" + }, + }, // { // accessorKey: "buyerSystemStatus", // header: ({ column }) => ( diff --git a/lib/vendor-document-list/ship/enhanced-documents-table.tsx b/lib/vendor-document-list/ship/enhanced-documents-table.tsx index 24ab42fb..cae0fe06 100644 --- a/lib/vendor-document-list/ship/enhanced-documents-table.tsx +++ b/lib/vendor-document-list/ship/enhanced-documents-table.tsx @@ -1,4 +1,4 @@ -// simplified-documents-table.tsx - 최적화된 버전 +// simplified-documents-table.tsx - Project Code 필터 기능 추가 "use client" import React from "react" @@ -13,7 +13,8 @@ import { getUserVendorDocuments, getUserVendorDocumentStats } from "@/lib/vendor import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-advanced-toolbar" import { toast } from "sonner" import { Badge } from "@/components/ui/badge" -import { FileText } from "lucide-react" +import { Button } from "@/components/ui/button" +import { FileText, FileInput, FileOutput, FolderOpen, Building2 } from "lucide-react" import { Label } from "@/components/ui/label" import { DataTable } from "@/components/data-table/data-table" @@ -21,6 +22,15 @@ import { SimplifiedDocumentsView } from "@/db/schema" import { getSimplifiedDocumentColumns } from "./enhanced-doc-table-columns" import { EnhancedDocTableToolbarActions } from "./enhanced-doc-table-toolbar-actions" +// 🔥 Project Code 필터를 위한 Select 컴포넌트 import 추가 +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" + // DrawingKind별 설명 매핑 const DRAWING_KIND_INFO = { B3: { @@ -60,17 +70,61 @@ export function SimplifiedDocumentsTable({ const { data, pageCount, total, drawingKind, vendorInfo } = React.useMemo(() => documentResult, [documentResult]) const { stats, totalDocuments, primaryDrawingKind } = React.useMemo(() => statsResult, [statsResult]) + // 🔥 B4 필터 상태 추가 + const [b4FilterType, setB4FilterType] = React.useState<'all' | 'gtt_deliverable' | 'shi_input'>('all') + + // 🔥 Project Code 필터 상태 추가 + const [selectedProjectCode, setSelectedProjectCode] = React.useState('all') + + // 🔥 고유한 Project Code 목록 추출 및 카운트 메모이제이션 + const projectCodeStats = React.useMemo(() => { + const projectMap = new Map() + + data.forEach(doc => { + const projectCode = doc.projectCode || 'Unknown' + projectMap.set(projectCode, (projectMap.get(projectCode) || 0) + 1) + }) + + // 정렬된 배열로 변환 (프로젝트 코드 알파벳순) + return Array.from(projectMap.entries()) + .sort((a, b) => a[0].localeCompare(b[0])) + .map(([code, count]) => ({ code, count })) + }, [data]) + // 🔥 데이터 로드 콜백을 useCallback으로 최적화 const handleDataLoaded = React.useCallback((loadedData: SimplifiedDocumentsView[]) => { onDataLoaded?.(loadedData) }, [onDataLoaded]) - // 🔥 데이터가 로드되면 콜백 호출 (의존성 최적화) + // 🔥 B4 및 Project Code 필터링된 데이터 메모이제이션 + const filteredData = React.useMemo(() => { + let result = data + + // B4 필터 적용 + if (b4FilterType !== 'all') { + if (b4FilterType === 'gtt_deliverable') { + result = result.filter(doc => doc.drawingMoveGbn === '도면입수') + } else if (b4FilterType === 'shi_input') { + result = result.filter(doc => doc.drawingMoveGbn === '도면제출') + } + } + + // Project Code 필터 적용 + if (selectedProjectCode !== 'all') { + result = result.filter(doc => + (doc.projectCode || 'Unknown') === selectedProjectCode + ) + } + + return result + }, [data, b4FilterType, selectedProjectCode]) + + // 🔥 데이터가 로드되면 콜백 호출 (필터링된 데이터 사용) React.useEffect(() => { - if (data && handleDataLoaded) { - handleDataLoaded(data) + if (filteredData && handleDataLoaded) { + handleDataLoaded(filteredData) } - }, [data, handleDataLoaded]) + }, [filteredData, handleDataLoaded]) // 🔥 상태들을 안정적으로 관리 const [rowAction, setRowAction] = React.useState | null>(null) @@ -81,7 +135,7 @@ export function SimplifiedDocumentsTable({ () => getSimplifiedDocumentColumns({ setRowAction, }), - [] // setRowAction은 항상 동일한 함수이므로 의존성에서 제외 + [] ) // 🔥 필터 필드들을 메모이제이션 @@ -238,7 +292,7 @@ export function SimplifiedDocumentsTable({ const getRowId = React.useCallback((originalRow: SimplifiedDocumentsView) => String(originalRow.documentId), []) const { table } = useDataTable({ - data, + data: filteredData, columns, pageCount, enablePinning: true, @@ -260,6 +314,21 @@ export function SimplifiedDocumentsTable({ return activeDrawingKind ? DRAWING_KIND_INFO[activeDrawingKind] : null }, [activeDrawingKind]) + // 🔥 B4 문서 통계 계산 + const b4Stats = React.useMemo(() => { + if (!hasB4Documents) return null + + const gttDeliverableCount = data.filter(doc => + doc.drawingKind === 'B4' && doc.drawingMoveGbn === '도면입수' + ).length + + const shiInputCount = data.filter(doc => + doc.drawingKind === 'B4' && doc.drawingMoveGbn === '도면제출' + ).length + + return { gttDeliverableCount, shiInputCount } + }, [data, hasB4Documents]) + return (
{/* DrawingKind 정보 간단 표시 */} @@ -270,12 +339,107 @@ export function SimplifiedDocumentsTable({
- {total} documents + {filteredData.length} documents
)} + {/* 🔥 필터 섹션 - Project Code 필터와 B4 필터를 함께 배치 */} +
+ {/* Project Code 필터 드롭다운 */} +
+
+ + +
+ + + {selectedProjectCode !== 'all' && ( + + )} +
+ + {/* B4 필터 버튼 - 기존 코드 유지 */} + {hasB4Documents && b4Stats && ( +
+ +
+ + + +
+
+ )} +
+ {/* 테이블 */}
@@ -287,7 +451,7 @@ export function SimplifiedDocumentsTable({ 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 52874702..7bb85710 100644 --- a/lib/vendor-document-list/ship/send-to-shi-button.tsx +++ b/lib/vendor-document-list/ship/send-to-shi-button.tsx @@ -325,21 +325,123 @@ export function SendToSHIButton({
+ {/* 전체 통계 */}
{t('shiSync.labels.pending')}
-
{t('shiSync.labels.itemCount', { count: totalStats.totalPending })}
+
+ {t('shiSync.labels.itemCount', { count: totalStats.totalPending })} +
{t('shiSync.labels.synced')}
-
{t('shiSync.labels.itemCount', { count: totalStats.totalSynced })}
+
+ {t('shiSync.labels.itemCount', { count: totalStats.totalSynced })} +
{t('shiSync.labels.failed')}
-
{t('shiSync.labels.itemCount', { count: totalStats.totalFailed })}
+
+ {t('shiSync.labels.itemCount', { count: totalStats.totalFailed })} +
+ {/* EntityType별 상세 통계 추가 */} + {totalStats.entityTypeDetailsTotals && ( + <> + +
+
+ {t('shiSync.labels.detailsByType')} + + {t('shiSync.labels.experimental')} + +
+ +
+ {/* Document 통계 */} + {totalStats.entityTypeDetailsTotals.document && ( +
+ + {t('shiSync.labels.documents')} + +
+ {totalStats.entityTypeDetailsTotals.document.pending > 0 && ( + + {totalStats.entityTypeDetailsTotals.document.pending} {t('shiSync.labels.pendingShort')} + + )} + {totalStats.entityTypeDetailsTotals.document.synced > 0 && ( + + {totalStats.entityTypeDetailsTotals.document.synced} {t('shiSync.labels.syncedShort')} + + )} + {totalStats.entityTypeDetailsTotals.document.failed > 0 && ( + + {totalStats.entityTypeDetailsTotals.document.failed} {t('shiSync.labels.failedShort')} + + )} +
+
+ )} + + {/* Revision 통계 */} + {totalStats.entityTypeDetailsTotals.revision && ( +
+ + {t('shiSync.labels.revisions')} + +
+ {totalStats.entityTypeDetailsTotals.revision.pending > 0 && ( + + {totalStats.entityTypeDetailsTotals.revision.pending} {t('shiSync.labels.pendingShort')} + + )} + {totalStats.entityTypeDetailsTotals.revision.synced > 0 && ( + + {totalStats.entityTypeDetailsTotals.revision.synced} {t('shiSync.labels.syncedShort')} + + )} + {totalStats.entityTypeDetailsTotals.revision.failed > 0 && ( + + {totalStats.entityTypeDetailsTotals.revision.failed} {t('shiSync.labels.failedShort')} + + )} +
+
+ )} + + {/* Attachment 통계 */} + {totalStats.entityTypeDetailsTotals.attachment && ( +
+ + {t('shiSync.labels.attachments')} + +
+ {totalStats.entityTypeDetailsTotals.attachment.pending > 0 && ( + + {totalStats.entityTypeDetailsTotals.attachment.pending} {t('shiSync.labels.pendingShort')} + + )} + {totalStats.entityTypeDetailsTotals.attachment.synced > 0 && ( + + {totalStats.entityTypeDetailsTotals.attachment.synced} {t('shiSync.labels.syncedShort')} + + )} + {totalStats.entityTypeDetailsTotals.attachment.failed > 0 && ( + + {totalStats.entityTypeDetailsTotals.attachment.failed} {t('shiSync.labels.failedShort')} + + )} +
+
+ )} +
+
+ + )} + {/* 계약별 상세 상태 */} {contractStatuses.length > 1 && (
diff --git a/lib/vendor-document-list/sync-service.ts b/lib/vendor-document-list/sync-service.ts index cdbf489f..c3ddfcca 100644 --- a/lib/vendor-document-list/sync-service.ts +++ b/lib/vendor-document-list/sync-service.ts @@ -101,7 +101,7 @@ class SyncService { * 동기화할 변경사항 조회 (증분) */ async getPendingChanges( - vendorId: number, + userId: number, targetSystem: string = 'DOLCE', limit?: number ): Promise { @@ -109,7 +109,7 @@ class SyncService { .select() .from(changeLogs) .where(and( - eq(changeLogs.vendorId, vendorId), + eq(changeLogs.userId, userId), eq(changeLogs.isSynced, false), lt(changeLogs.syncAttempts, 3), sql`(${changeLogs.targetSystems} IS NULL OR ${changeLogs.targetSystems} @> ${JSON.stringify([targetSystem])})` @@ -176,10 +176,11 @@ class SyncService { } const vendorId = Number(session.user.companyId) + const userId = Number(session.user.id) // 2. 대기 중인 변경사항 조회 (전체) - const pendingChanges = await this.getPendingChanges(vendorId, targetSystem) + const pendingChanges = await this.getPendingChanges(userId, targetSystem) if (pendingChanges.length === 0) { return { @@ -457,79 +458,105 @@ class SyncService { .where(inArray(changeLogs.id, changeIds)) } - /** - * 동기화 상태 조회 - */ - async getSyncStatus(projectId: number, targetSystem: string = 'DOLCE') { - try { +/** + * 동기화 상태 조회 - entityType별 상세 통계 포함 + */ +async getSyncStatus(projectId: number, targetSystem: string = 'DOLCE') { + try { + const session = await getServerSession(authOptions) + if (!session?.user?.companyId) { + throw new Error("인증이 필요합니다.") + } - const session = await getServerSession(authOptions) - if (!session?.user?.companyId) { - throw new Error("인증이 필요합니다.") + const vendorId = Number(session.user.companyId) + const userId = Number(session.user.id) + + // 기본 조건 + const baseConditions = and( + eq(changeLogs.userId, userId), + sql`(${changeLogs.targetSystems} IS NULL OR ${changeLogs.targetSystems} @> ${JSON.stringify([targetSystem])})` + ) + + // entityType별 통계를 위한 쿼리 + const entityStats = await db + .select({ + entityType: changeLogs.entityType, + pendingCount: sql`COUNT(*) FILTER (WHERE ${changeLogs.isSynced} = false AND ${changeLogs.syncAttempts} < 3)`, + syncedCount: sql`COUNT(*) FILTER (WHERE ${changeLogs.isSynced} = true)`, + failedCount: sql`COUNT(*) FILTER (WHERE ${changeLogs.isSynced} = false AND ${changeLogs.syncAttempts} >= 3)`, + totalCount: sql`COUNT(*)` + }) + .from(changeLogs) + .where(baseConditions) + .groupBy(changeLogs.entityType) + + // 전체 통계 계산 + const totals = entityStats.reduce((acc, stat) => ({ + pendingChanges: acc.pendingChanges + Number(stat.pendingCount), + syncedChanges: acc.syncedChanges + Number(stat.syncedCount), + failedChanges: acc.failedChanges + Number(stat.failedCount), + totalChanges: acc.totalChanges + Number(stat.totalCount) + }), { + pendingChanges: 0, + syncedChanges: 0, + failedChanges: 0, + totalChanges: 0 + }) + + // entityType별 상세 정보 구성 + const entityTypeDetails = { + document: { + pending: 0, + synced: 0, + failed: 0, + total: 0 + }, + revision: { + pending: 0, + synced: 0, + failed: 0, + total: 0 + }, + attachment: { + pending: 0, + synced: 0, + failed: 0, + total: 0 } - - const vendorId = Number(session.user.companyId) - - - // 대기 중인 변경사항 수 조회 - const pendingCount = await db.$count( - changeLogs, - and( - eq(changeLogs.vendorId, vendorId), - eq(changeLogs.isSynced, false), - lt(changeLogs.syncAttempts, 3), - sql`(${changeLogs.targetSystems} IS NULL OR ${changeLogs.targetSystems} @> ${JSON.stringify([targetSystem])})` - ) - ) - - // 동기화된 변경사항 수 조회 - const syncedCount = await db.$count( - changeLogs, - and( - eq(changeLogs.vendorId, vendorId), - eq(changeLogs.isSynced, true), - sql`(${changeLogs.targetSystems} IS NULL OR ${changeLogs.targetSystems} @> ${JSON.stringify([targetSystem])})` - ) - ) - - // 실패한 변경사항 수 조회 - const failedCount = await db.$count( - changeLogs, - and( - eq(changeLogs.vendorId, vendorId), - eq(changeLogs.isSynced, false), - sql`${changeLogs.syncAttempts} >= 3`, - sql`(${changeLogs.targetSystems} IS NULL OR ${changeLogs.targetSystems} @> ${JSON.stringify([targetSystem])})` - ) - ) - - // 마지막 성공한 배치 조회 - const [lastSuccessfulBatch] = await db - .select() - .from(syncBatches) - .where(and( - eq(syncBatches.vendorId, vendorId), - eq(syncBatches.targetSystem, targetSystem), - eq(syncBatches.status, 'SUCCESS') - )) - .orderBy(desc(syncBatches.completedAt)) - .limit(1) + } - return { - vendorId, - targetSystem, - totalChanges: pendingCount + syncedCount + failedCount, - pendingChanges: pendingCount, - syncedChanges: syncedCount, - failedChanges: failedCount, - lastSyncAt: lastSuccessfulBatch?.completedAt?.toISOString() || null, - syncEnabled: this.isSyncEnabled(targetSystem) + // 통계 데이터를 entityTypeDetails에 매핑 + entityStats.forEach(stat => { + const entityType = stat.entityType as 'document' | 'revision' | 'attachment' + if (entityTypeDetails[entityType]) { + entityTypeDetails[entityType] = { + pending: Number(stat.pendingCount), + synced: Number(stat.syncedCount), + failed: Number(stat.failedCount), + total: Number(stat.totalCount) + } } - } catch (error) { - console.error('Failed to get sync status:', error) - throw error + }) + + + return { + projectId, + vendorId, + targetSystem, + ...totals, + entityTypeDetails, // entityType별 상세 통계 + syncEnabled: this.isSyncEnabled(targetSystem), + // 추가 메타데이터 + hasPendingChanges: totals.pendingChanges > 0, + hasFailedChanges: totals.failedChanges > 0, + syncHealthy: totals.failedChanges === 0 && totals.pendingChanges < 100, + requiresSync: totals.pendingChanges > 0 } + } catch (error) { + console.error('Failed to get sync status:', error) + throw error } +} /** * 최근 동기화 배치 목록 조회 -- cgit v1.2.3