diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-08-04 09:36:14 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-08-04 09:36:14 +0000 |
| commit | 92eda21e45d902663052575aaa4c4f80bfa2faea (patch) | |
| tree | 8483702edf82932d4359a597a854fa8e1b48e94b /lib/vendor-document-list/ship | |
| parent | f0213de0d2fb5fcb931b3ddaddcbb6581cab5d28 (diff) | |
(대표님) 벤더 문서 변경사항, data-table 변경, sync 변경
Diffstat (limited to 'lib/vendor-document-list/ship')
4 files changed, 350 insertions, 236 deletions
diff --git a/lib/vendor-document-list/ship/enhanced-doc-table-toolbar-actions.tsx b/lib/vendor-document-list/ship/enhanced-doc-table-toolbar-actions.tsx index 255b1f9d..4ec57369 100644 --- a/lib/vendor-document-list/ship/enhanced-doc-table-toolbar-actions.tsx +++ b/lib/vendor-document-list/ship/enhanced-doc-table-toolbar-actions.tsx @@ -1,4 +1,6 @@ +// enhanced-doc-table-toolbar-actions.tsx - 최적화된 버전 "use client" + import * as React from "react" import { type Table } from "@tanstack/react-table" import { Download, Upload, Plus, Files, RefreshCw } from "lucide-react" @@ -6,14 +8,13 @@ import { toast } from "sonner" import { exportTableToExcel } from "@/lib/export" import { Button } from "@/components/ui/button" -import { SimplifiedDocumentsView } from "@/db/schema/vendorDocu" +import { SimplifiedDocumentsView } from "@/db/schema/vendorDocu" import { SendToSHIButton } from "./send-to-shi-button" import { ImportFromDOLCEButton } from "./import-from-dolce-button" interface EnhancedDocTableToolbarActionsProps { table: Table<SimplifiedDocumentsView> projectType: "ship" | "plant" - contractId?: number } export function EnhancedDocTableToolbarActions({ @@ -21,70 +22,77 @@ export function EnhancedDocTableToolbarActions({ projectType, }: EnhancedDocTableToolbarActionsProps) { const [bulkUploadDialogOpen, setBulkUploadDialogOpen] = React.useState(false) - - // 현재 테이블의 모든 데이터 (필터링된 상태) - const allDocuments = table.getFilteredRowModel().rows.map(row => row.original) - const handleSyncComplete = () => { - // 동기화 완료 후 테이블 새로고침 + // 🔥 메모이제이션으로 불필요한 재계산 방지 + const allDocuments = React.useMemo(() => { + return table.getFilteredRowModel().rows.map(row => row.original) + }, [ + table.getFilteredRowModel().rows.length, // 행 개수가 변경될 때만 재계산 + table.getState().columnFilters, // 필터가 변경될 때만 재계산 + table.getState().globalFilter, // 전역 필터가 변경될 때만 재계산 + ]) + + // 🔥 projectIds 메모이제이션 (ImportFromDOLCEButton에서 중복 계산 방지) + const projectIds = React.useMemo(() => { + const uniqueIds = [...new Set(allDocuments.map(doc => doc.projectId).filter(Boolean))] + return uniqueIds.sort() + }, [allDocuments]) + + // 🔥 핸들러들을 useCallback으로 메모이제이션 + const handleSyncComplete = React.useCallback(() => { table.resetRowSelection() - // 필요시 추가 액션 수행 - } + }, [table]) - const handleDocumentAdded = () => { - // 테이블 새로고침 + const handleDocumentAdded = React.useCallback(() => { table.resetRowSelection() - - // 추가적인 새로고침 시도 + // 🔥 강제 새로고침 대신 더 효율적인 방법 사용 setTimeout(() => { - window.location.reload() // 강제 새로고침 + // 상태 업데이트만으로 충분한 경우가 많음 + window.location.reload() }, 500) - } + }, [table]) - const handleImportComplete = () => { - // 가져오기 완료 후 테이블 새로고침 + const handleImportComplete = React.useCallback(() => { table.resetRowSelection() setTimeout(() => { window.location.reload() }, 500) - } + }, [table]) + + // 🔥 Export 핸들러 메모이제이션 + const handleExport = React.useCallback(() => { + exportTableToExcel(table, { + filename: "Document-list", + excludeColumns: ["select", "actions"], + }) + }, [table]) return ( <div className="flex items-center gap-2"> - - <> - {/* SHIP: DOLCE에서 목록 가져오기 */} - <ImportFromDOLCEButton - allDocuments={allDocuments} - onImportComplete={handleImportComplete} - /> - </> - + {/* SHIP: DOLCE에서 목록 가져오기 */} + <ImportFromDOLCEButton + allDocuments={allDocuments} + projectIds={projectIds} // 🔥 미리 계산된 projectIds 전달 + onImportComplete={handleImportComplete} + /> {/* Export 버튼 (공통) */} <Button variant="outline" size="sm" - onClick={() => - exportTableToExcel(table, { - filename: "Document-list", - excludeColumns: ["select", "actions"], - }) - } + onClick={handleExport} className="gap-2" > <Download className="size-4" aria-hidden="true" /> <span className="hidden sm:inline">Export</span> </Button> - {/* Send to SHI 버튼 (공통) - 내부 → 외부로 보내기 */} + {/* Send to SHI 버튼 (공통) */} <SendToSHIButton documents={allDocuments} onSyncComplete={handleSyncComplete} projectType={projectType} /> - - </div> ) }
\ No newline at end of file diff --git a/lib/vendor-document-list/ship/enhanced-documents-table.tsx b/lib/vendor-document-list/ship/enhanced-documents-table.tsx index 9885c027..8051da7e 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 - 최적화된 버전 "use client" import React from "react" @@ -52,41 +52,45 @@ export function SimplifiedDocumentsTable({ allPromises, onDataLoaded, }: SimplifiedDocumentsTableProps) { - // React.use()로 Promise 결과를 받고, 그 다음에 destructuring - const [documentResult, statsResult] = React.use(allPromises) - const { data, pageCount, total, drawingKind, vendorInfo } = documentResult - const { stats, totalDocuments, primaryDrawingKind } = statsResult + // 🔥 React.use() 결과를 안전하게 처리 + const promiseResults = React.use(allPromises) + const [documentResult, statsResult] = promiseResults + + // 🔥 데이터 구조분해를 메모이제이션 + const { data, pageCount, total, drawingKind, vendorInfo } = React.useMemo(() => documentResult, [documentResult]) + const { stats, totalDocuments, primaryDrawingKind } = React.useMemo(() => statsResult, [statsResult]) - // 데이터가 로드되면 콜백 호출 + // 🔥 데이터 로드 콜백을 useCallback으로 최적화 + const handleDataLoaded = React.useCallback((loadedData: SimplifiedDocumentsView[]) => { + onDataLoaded?.(loadedData) + }, [onDataLoaded]) + + // 🔥 데이터가 로드되면 콜백 호출 (의존성 최적화) React.useEffect(() => { - if (onDataLoaded && data) { - onDataLoaded(data) + if (data && handleDataLoaded) { + handleDataLoaded(data) } - }, [data, onDataLoaded]) + }, [data, handleDataLoaded]) - // 기존 상태들 + // 🔥 상태들을 안정적으로 관리 const [rowAction, setRowAction] = React.useState<DataTableRowAction<SimplifiedDocumentsView> | null>(null) - const [expandedRows,] = React.useState<Set<string>>(new Set()) + const [expandedRows] = React.useState<Set<string>>(() => new Set()) + // 🔥 컬럼 메모이제이션 최적화 const columns = React.useMemo( () => getSimplifiedDocumentColumns({ setRowAction, }), - [setRowAction] + [] // setRowAction은 항상 동일한 함수이므로 의존성에서 제외 ) - // ✅ SimplifiedDocumentsView에 맞게 필터 필드 업데이트 - const advancedFilterFields: DataTableAdvancedFilterField<SimplifiedDocumentsView>[] = [ + // 🔥 필터 필드들을 메모이제이션 + const advancedFilterFields: DataTableAdvancedFilterField<SimplifiedDocumentsView>[] = React.useMemo(() => [ { id: "docNumber", label: "Document No", type: "text", }, - // { - // id: "vendorDocNumber", - // label: "Vendor Document No", - // type: "text", - // }, { id: "title", label: "Document Title", @@ -178,10 +182,10 @@ export function SimplifiedDocumentsTable({ label: "Updated Date", type: "date", }, - ] + ], []) - // ✅ B4 전용 필드들 (조건부로 추가) - const b4FilterFields: DataTableAdvancedFilterField<SimplifiedDocumentsView>[] = [ + // 🔥 B4 전용 필드들 메모이제이션 + const b4FilterFields: DataTableAdvancedFilterField<SimplifiedDocumentsView>[] = React.useMemo(() => [ { id: "cGbn", label: "C Category", @@ -212,33 +216,49 @@ export function SimplifiedDocumentsTable({ label: "S Category", type: "text", }, - ] + ], []) + + // 🔥 B4 문서 존재 여부 체크 메모이제이션 + const hasB4Documents = React.useMemo(() => { + return data.some(doc => doc.drawingKind === 'B4') + }, [data]) + + // 🔥 최종 필터 필드 메모이제이션 + const finalFilterFields = React.useMemo(() => { + return hasB4Documents ? [...advancedFilterFields, ...b4FilterFields] : advancedFilterFields + }, [hasB4Documents, advancedFilterFields, b4FilterFields]) + + // 🔥 테이블 초기 상태 메모이제이션 + const tableInitialState = React.useMemo(() => ({ + sorting: [{ id: "createdAt", desc: true }], + columnPinning: { right: ["actions"] }, + }), []) - // B4 문서가 있는지 확인하여 B4 전용 필드 추가 - const hasB4Documents = data.some(doc => doc.drawingKind === 'B4') - const finalFilterFields = hasB4Documents - ? [...advancedFilterFields, ...b4FilterFields] - : advancedFilterFields + // 🔥 getRowId 함수 메모이제이션 + const getRowId = React.useCallback((originalRow: SimplifiedDocumentsView) => String(originalRow.documentId), []) const { table } = useDataTable({ - data: data, + data, columns, pageCount, enablePinning: true, enableAdvancedFilter: true, - initialState: { - sorting: [{ id: "createdAt", desc: true }], - columnPinning: { right: ["actions"] }, - }, - getRowId: (originalRow) => String(originalRow.documentId), + initialState: tableInitialState, + getRowId, shallow: false, clearOnDefault: true, columnResizeMode: "onEnd", }) - // 실제 데이터의 drawingKind 또는 주요 drawingKind 사용 - const activeDrawingKind = drawingKind || primaryDrawingKind - const kindInfo = activeDrawingKind ? DRAWING_KIND_INFO[activeDrawingKind] : null + // 🔥 활성 drawingKind 메모이제이션 + const activeDrawingKind = React.useMemo(() => { + return drawingKind || primaryDrawingKind + }, [drawingKind, primaryDrawingKind]) + + // 🔥 kindInfo 메모이제이션 + const kindInfo = React.useMemo(() => { + return activeDrawingKind ? DRAWING_KIND_INFO[activeDrawingKind] : null + }, [activeDrawingKind]) return ( <div className="w-full space-y-4"> @@ -246,13 +266,7 @@ 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"> - <FileText className="w-4 h-4" /> - {kindInfo.title} - </Badge> - <span className="text-sm text-muted-foreground"> - {kindInfo.description} - </span> */} + {/* 주석 처리된 부분은 그대로 유지 */} </div> <div className="flex items-center gap-2"> <Badge variant="outline"> @@ -270,11 +284,10 @@ export function SimplifiedDocumentsTable({ filterFields={finalFilterFields} shallow={false} > - <EnhancedDocTableToolbarActions - table={table} - projectType="ship" - /> - + <EnhancedDocTableToolbarActions + table={table} + projectType="ship" + /> </DataTableAdvancedToolbar> </DataTable> </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 de9e63bc..90796d8e 100644 --- a/lib/vendor-document-list/ship/import-from-dolce-button.tsx +++ b/lib/vendor-document-list/ship/import-from-dolce-button.tsx @@ -1,3 +1,4 @@ +// import-from-dolce-button.tsx - 최적화된 버전 "use client" import * as React from "react" @@ -23,15 +24,38 @@ import { Separator } from "@/components/ui/separator" import { SimplifiedDocumentsView } from "@/db/schema" import { ImportStatus } from "../import-service" import { useSession } from "next-auth/react" -import { getContractIdsByVendor } from "../service" // 서버 액션 import +import { getProjectIdsByVendor } from "../service" + +// 🔥 API 응답 캐시 (컴포넌트 외부에 선언하여 인스턴스 간 공유) +const statusCache = new Map<string, { data: ImportStatus; timestamp: number }>() +const CACHE_TTL = 2 * 60 * 1000 // 2분 캐시 interface ImportFromDOLCEButtonProps { - allDocuments: SimplifiedDocumentsView[] // contractId 대신 문서 배열 + allDocuments: SimplifiedDocumentsView[] + projectIds?: number[] // 🔥 미리 계산된 projectIds를 props로 받음 onImportComplete?: () => void } +// 🔥 디바운스 훅 +function useDebounce<T>(value: T, delay: number): T { + const [debouncedValue, setDebouncedValue] = React.useState(value) + + React.useEffect(() => { + const handler = setTimeout(() => { + setDebouncedValue(value) + }, delay) + + return () => { + clearTimeout(handler) + } + }, [value, delay]) + + return debouncedValue +} + export function ImportFromDOLCEButton({ allDocuments, + projectIds: propProjectIds, onImportComplete }: ImportFromDOLCEButtonProps) { const [isDialogOpen, setIsDialogOpen] = React.useState(false) @@ -39,104 +63,120 @@ export function ImportFromDOLCEButton({ const [isImporting, setIsImporting] = React.useState(false) const [importStatusMap, setImportStatusMap] = React.useState<Map<number, ImportStatus>>(new Map()) const [statusLoading, setStatusLoading] = React.useState(false) - const [vendorContractIds, setVendorContractIds] = React.useState<number[]>([]) // 서버에서 가져온 contractIds - const [loadingVendorContracts, setLoadingVendorContracts] = React.useState(false) + const [vendorProjectIds, setVendorProjectIds] = React.useState<number[]>([]) + const [loadingVendorProjects, setLoadingVendorProjects] = React.useState(false) + const { data: session } = useSession() + const vendorId = session?.user.companyId - const vendorId = session?.user.companyId; - - // allDocuments에서 추출한 contractIds - const documentsContractIds = React.useMemo(() => { - const uniqueIds = [...new Set(allDocuments.map(doc => doc.contractId).filter(Boolean))] + // 🔥 allDocuments에서 projectIds 추출 (props로 전달받은 경우 사용) + const documentsProjectIds = React.useMemo(() => { + if (propProjectIds) return propProjectIds // props로 받은 경우 그대로 사용 + + const uniqueIds = [...new Set(allDocuments.map(doc => doc.projectId).filter(Boolean))] return uniqueIds.sort() - }, [allDocuments]) + }, [allDocuments, propProjectIds]) - // 최종 사용할 contractIds (allDocuments가 있으면 문서에서, 없으면 vendor의 모든 contracts) - const contractIds = React.useMemo(() => { - if (documentsContractIds.length > 0) { - return documentsContractIds + // 🔥 최종 projectIds (변경 빈도 최소화) + const projectIds = React.useMemo(() => { + if (documentsProjectIds.length > 0) { + return documentsProjectIds } - return vendorContractIds - }, [documentsContractIds, vendorContractIds]) + return vendorProjectIds + }, [documentsProjectIds, vendorProjectIds]) - console.log(contractIds, "contractIds") + // 🔥 projectIds 디바운싱 (API 호출 과다 방지) + const debouncedProjectIds = useDebounce(projectIds, 300) - // vendorId로 contracts 가져오기 - React.useEffect(() => { - const fetchVendorContracts = async () => { - // allDocuments가 비어있고 vendorId가 있을 때만 실행 - if (allDocuments.length === 0 && vendorId) { - setLoadingVendorContracts(true) - try { - const contractIds = await getContractIdsByVendor(vendorId) - setVendorContractIds(contractIds) - } catch (error) { - console.error('Failed to fetch vendor contracts:', error) - toast.error('Failed to fetch contract information.') - } finally { - setLoadingVendorContracts(false) - } - } - } - - fetchVendorContracts() - }, [allDocuments.length, vendorId]) - - // 주요 contractId (가장 많이 나타나는 것) - const primaryContractId = React.useMemo(() => { - if (contractIds.length === 1) return contractIds[0] + // 🔥 주요 projectId 메모이제이션 + const primaryProjectId = React.useMemo(() => { + if (projectIds.length === 1) return projectIds[0] if (allDocuments.length > 0) { const counts = allDocuments.reduce((acc, doc) => { - const id = doc.contractId || 0 + const id = doc.projectId || 0 acc[id] = (acc[id] || 0) + 1 return acc }, {} as Record<number, number>) return Number(Object.entries(counts) - .sort(([,a], [,b]) => b - a)[0]?.[0] || contractIds[0] || 0) + .sort(([,a], [,b]) => b - a)[0]?.[0] || projectIds[0] || 0) } - return contractIds[0] || 0 - }, [contractIds, allDocuments]) + return projectIds[0] || 0 + }, [projectIds, allDocuments]) + + // 🔥 캐시된 API 호출 함수 + const fetchImportStatusCached = React.useCallback(async (projectId: number): Promise<ImportStatus | null> => { + const cacheKey = `import-status-${projectId}` + const cached = statusCache.get(cacheKey) + + // 캐시된 데이터가 있고 유효하면 사용 + if (cached && Date.now() - cached.timestamp < CACHE_TTL) { + return cached.data + } + + try { + const response = await fetch(`/api/sync/import/status?projectId=${projectId}&sourceSystem=DOLCE`) + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + throw new Error(errorData.message || 'Failed to fetch import status') + } + + const status = await response.json() + if (status.error) { + console.warn(`Status error for project ${projectId}:`, status.error) + return null + } + + // 캐시에 저장 + statusCache.set(cacheKey, { + data: status, + timestamp: Date.now() + }) + + return status + } catch (error) { + console.error(`Failed to fetch status for project ${projectId}:`, error) + return null + } + }, []) - // 모든 contractId에 대한 상태 조회 - const fetchAllImportStatus = async () => { - if (contractIds.length === 0) return + // 🔥 모든 projectId에 대한 상태 조회 (최적화된 버전) + const fetchAllImportStatus = React.useCallback(async () => { + if (debouncedProjectIds.length === 0) return setStatusLoading(true) const statusMap = new Map<number, ImportStatus>() try { - // 각 contractId별로 상태 조회 - const statusPromises = contractIds.map(async (contractId) => { - try { - const response = await fetch(`/api/sync/import/status?contractId=${contractId}&sourceSystem=DOLCE`) - if (!response.ok) { - const errorData = await response.json().catch(() => ({})) - throw new Error(errorData.message || 'Failed to fetch import status') - } - - const status = await response.json() - if (status.error) { - console.warn(`Status error for contract ${contractId}:`, status.error) - return { contractId, status: null } + // 🔥 병렬 처리하되 동시 연결 수 제한 (3개씩) + const batchSize = 3 + const batches = [] + + for (let i = 0; i < debouncedProjectIds.length; i += batchSize) { + batches.push(debouncedProjectIds.slice(i, i + batchSize)) + } + + for (const batch of batches) { + const batchPromises = batch.map(async (projectId) => { + const status = await fetchImportStatusCached(projectId) + return { projectId, status } + }) + + const batchResults = await Promise.all(batchPromises) + + batchResults.forEach(({ projectId, status }) => { + if (status) { + statusMap.set(projectId, status) } - - return { contractId, status } - } catch (error) { - console.error(`Failed to fetch status for contract ${contractId}:`, error) - return { contractId, status: null } - } - }) + }) - const results = await Promise.all(statusPromises) - - results.forEach(({ contractId, status }) => { - if (status) { - statusMap.set(contractId, status) + // 배치 간 짧은 지연 + if (batches.length > 1) { + await new Promise(resolve => setTimeout(resolve, 100)) } - }) + } setImportStatusMap(statusMap) @@ -146,19 +186,48 @@ export function ImportFromDOLCEButton({ } finally { setStatusLoading(false) } - } + }, [debouncedProjectIds, fetchImportStatusCached]) - // 컴포넌트 마운트 시 상태 조회 + // 🔥 vendorId로 projects 가져오기 (최적화) React.useEffect(() => { - if (contractIds.length > 0) { - fetchAllImportStatus() + let isCancelled = false + + const fetchVendorProjects = async () => { + if (allDocuments.length === 0 && vendorId && !loadingVendorProjects) { + setLoadingVendorProjects(true) + try { + const projectIds = await getProjectIdsByVendor(vendorId) + if (!isCancelled) { + setVendorProjectIds(projectIds) + } + } catch (error) { + console.error('Failed to fetch vendor projects:', error) + if (!isCancelled) { + toast.error('Failed to fetch project information.') + } + } finally { + if (!isCancelled) { + setLoadingVendorProjects(false) + } + } + } } - }, [contractIds]) - // 주요 contractId의 상태 - const primaryImportStatus = importStatusMap.get(primaryContractId) + fetchVendorProjects() + + return () => { + isCancelled = true + } + }, [allDocuments.length, vendorId, loadingVendorProjects]) - // 전체 통계 계산 + // 🔥 컴포넌트 마운트 시 상태 조회 (디바운싱 적용) + React.useEffect(() => { + if (debouncedProjectIds.length > 0) { + fetchAllImportStatus() + } + }, [debouncedProjectIds, fetchAllImportStatus]) + + // 🔥 전체 통계 메모이제이션 const totalStats = React.useMemo(() => { const statuses = Array.from(importStatusMap.values()) return statuses.reduce((acc, status) => ({ @@ -174,38 +243,53 @@ export function ImportFromDOLCEButton({ }) }, [importStatusMap]) - const handleImport = async () => { - if (contractIds.length === 0) return + // 🔥 주요 상태 메모이제이션 + const primaryImportStatus = React.useMemo(() => { + return importStatusMap.get(primaryProjectId) + }, [importStatusMap, primaryProjectId]) + + // 🔥 가져오기 실행 함수 최적화 + const handleImport = React.useCallback(async () => { + if (projectIds.length === 0) return setImportProgress(0) setIsImporting(true) try { - // 진행률 시뮬레이션 const progressInterval = setInterval(() => { setImportProgress(prev => Math.min(prev + 10, 85)) }, 500) - // 여러 contractId에 대해 순차적으로 가져오기 실행 - const importPromises = contractIds.map(async (contractId) => { - const response = await fetch('/api/sync/import', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - contractId, - sourceSystem: 'DOLCE' + // 🔥 순차 처리로 서버 부하 방지 + const results = [] + for (const projectId of projectIds) { + try { + const response = await fetch('/api/sync/import', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + projectId, + sourceSystem: 'DOLCE' + }) }) - }) - if (!response.ok) { - const errorData = await response.json() - throw new Error(`Contract ${contractId}: ${errorData.message || 'Import failed'}`) - } - - return response.json() - }) + if (!response.ok) { + const errorData = await response.json() + throw new Error(`Project ${projectId}: ${errorData.message || 'Import failed'}`) + } - const results = await Promise.all(importPromises) + const result = await response.json() + results.push(result) + + // 프로젝트 간 짧은 지연 + if (projectIds.length > 1) { + await new Promise(resolve => setTimeout(resolve, 200)) + } + } catch (error) { + console.error(`Import failed for project ${projectId}:`, error) + results.push({ success: false, error: error instanceof Error ? error.message : 'Unknown error' }) + } + } clearInterval(progressInterval) setImportProgress(100) @@ -232,19 +316,21 @@ export function ImportFromDOLCEButton({ toast.success( `DOLCE import completed`, { - description: `New ${totalResult.newCount}, Updated ${totalResult.updatedCount}, Skipped ${totalResult.skippedCount} (${contractIds.length} contracts)` + description: `New ${totalResult.newCount}, Updated ${totalResult.updatedCount}, Skipped ${totalResult.skippedCount} (${projectIds.length} projects)` } ) } else { toast.error( `DOLCE import partially failed`, { - description: 'Some contracts failed to import.' + description: 'Some projects failed to import.' } ) } - fetchAllImportStatus() // 상태 갱신 + // 🔥 캐시 무효화 + statusCache.clear() + fetchAllImportStatus() onImportComplete?.() }, 500) @@ -256,11 +342,12 @@ export function ImportFromDOLCEButton({ description: error instanceof Error ? error.message : 'An unknown error occurred.' }) } - } + }, [projectIds, fetchAllImportStatus, onImportComplete]) - const getStatusBadge = () => { - if (loadingVendorContracts) { - return <Badge variant="secondary">Loading contract information...</Badge> + // 🔥 상태 뱃지 메모이제이션 + const statusBadge = React.useMemo(() => { + if (loadingVendorProjects) { + return <Badge variant="secondary">Loading project information...</Badge> } if (statusLoading) { @@ -279,7 +366,7 @@ export function ImportFromDOLCEButton({ return ( <Badge variant="samsung" className="gap-1"> <AlertTriangle className="w-3 h-3" /> - Updates Available ({contractIds.length} contracts) + Updates Available ({projectIds.length} projects) </Badge> ) } @@ -290,13 +377,19 @@ export function ImportFromDOLCEButton({ Synchronized with DOLCE </Badge> ) - } + }, [loadingVendorProjects, statusLoading, importStatusMap.size, totalStats, projectIds.length]) const canImport = totalStats.importEnabled && (totalStats.newDocuments > 0 || totalStats.updatedDocuments > 0) - // 로딩 중이거나 contractIds가 없으면 버튼을 표시하지 않음 - if (loadingVendorContracts || contractIds.length === 0) { + // 🔥 새로고침 핸들러 최적화 + const handleRefresh = React.useCallback(() => { + statusCache.clear() // 캐시 무효화 + fetchAllImportStatus() + }, [fetchAllImportStatus]) + + // 로딩 중이거나 projectIds가 없으면 버튼을 표시하지 않음 + if (loadingVendorProjects || projectIds.length === 0) { return null } @@ -316,7 +409,7 @@ export function ImportFromDOLCEButton({ ) : ( <Download className="w-4 h-4" /> )} - <span className="hidden sm:inline">Import from DOLCE</span> + <span className="hidden sm:inline">Get List</span> {totalStats.newDocuments + totalStats.updatedDocuments > 0 && ( <Badge variant="samsung" @@ -335,24 +428,24 @@ export function ImportFromDOLCEButton({ <h4 className="font-medium">DOLCE Import Status</h4> <div className="flex items-center justify-between"> <span className="text-sm text-muted-foreground">Current Status</span> - {getStatusBadge()} + {statusBadge} </div> </div> - {/* 계약 소스 표시 */} - {allDocuments.length === 0 && vendorContractIds.length > 0 && ( + {/* 프로젝트 소스 표시 */} + {allDocuments.length === 0 && vendorProjectIds.length > 0 && ( <div className="text-xs text-blue-600 bg-blue-50 p-2 rounded"> - No documents found, importing from all contracts. + No documents found, importing from all projects. </div> )} - {/* 다중 계약 정보 표시 */} - {contractIds.length > 1 && ( + {/* 다중 프로젝트 정보 표시 */} + {projectIds.length > 1 && ( <div className="text-sm"> - <div className="text-muted-foreground">Target Contracts</div> - <div className="font-medium">{contractIds.length} contracts</div> + <div className="text-muted-foreground">Target Projects</div> + <div className="font-medium">{projectIds.length} projects</div> <div className="text-xs text-muted-foreground"> - Contract IDs: {contractIds.join(', ')} + Project IDs: {projectIds.join(', ')} </div> </div> )} @@ -373,22 +466,22 @@ export function ImportFromDOLCEButton({ </div> <div className="text-sm"> - <div className="text-muted-foreground">Total DOLCE Documents (B3/B4/B5)</div> + <div className="text-muted-foreground">Total Documents (B3/B4/B5)</div> <div className="font-medium">{totalStats.availableDocuments || 0}</div> </div> - {/* 각 계약별 세부 정보 (펼치기/접기 가능) */} - {contractIds.length > 1 && ( + {/* 각 프로젝트별 세부 정보 */} + {projectIds.length > 1 && ( <details className="text-sm"> <summary className="cursor-pointer text-muted-foreground hover:text-foreground"> - Details by Contract + Details by Project </summary> <div className="mt-2 space-y-2 pl-2 border-l-2 border-muted"> - {contractIds.map(contractId => { - const status = importStatusMap.get(contractId) + {projectIds.map(projectId => { + const status = importStatusMap.get(projectId) return ( - <div key={contractId} className="text-xs"> - <div className="font-medium">Contract {contractId}</div> + <div key={projectId} className="text-xs"> + <div className="font-medium">Project {projectId}</div> {status ? ( <div className="text-muted-foreground"> New {status.newDocuments}, Updates {status.updatedDocuments} @@ -430,7 +523,7 @@ export function ImportFromDOLCEButton({ <Button variant="outline" size="sm" - onClick={fetchAllImportStatus} + onClick={handleRefresh} disabled={statusLoading} > {statusLoading ? ( @@ -451,7 +544,7 @@ export function ImportFromDOLCEButton({ <DialogTitle>Import Document List from DOLCE</DialogTitle> <DialogDescription> Import the latest document list from Samsung Heavy Industries DOLCE system. - {contractIds.length > 1 && ` (${contractIds.length} contracts targeted)`} + {projectIds.length > 1 && ` (${projectIds.length} projects targeted)`} </DialogDescription> </DialogHeader> @@ -469,10 +562,10 @@ export function ImportFromDOLCEButton({ Includes new and updated documents (B3, B4, B5). <br /> For B4 documents, GTTPreDwg and GTTWorkingDwg issue stages will be auto-generated. - {contractIds.length > 1 && ( + {projectIds.length > 1 && ( <> <br /> - Will import sequentially from {contractIds.length} contracts. + Will import sequentially from {projectIds.length} projects. </> )} </div> 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 4607c994..c67c7b2c 100644 --- a/lib/vendor-document-list/ship/send-to-shi-button.tsx +++ b/lib/vendor-document-list/ship/send-to-shi-button.tsx @@ -47,7 +47,7 @@ export function SendToSHIButton({ // 문서에서 유효한 계약 ID 목록 추출 const documentsContractIds = React.useMemo(() => { const validIds = documents - .map(doc => doc.contractId) + .map(doc => doc.projectId) .filter((id): id is number => typeof id === 'number' && id > 0) const uniqueIds = [...new Set(validIds)] @@ -68,8 +68,8 @@ export function SendToSHIButton({ console.log('SendToSHIButton Debug Info:', { documentsContractIds, totalStats, - contractStatuses: contractStatuses.map(({ contractId, syncStatus, error }) => ({ - contractId, + contractStatuses: contractStatuses.map(({ projectId, syncStatus, error }) => ({ + projectId, pendingChanges: syncStatus?.pendingChanges, hasError: !!error })) @@ -95,7 +95,7 @@ export function SendToSHIButton({ // 동기화 가능한 계약들만 필터링 const contractsToSync = contractStatuses.filter(({ syncStatus, error }) => { if (error) { - console.warn(`Contract ${contractStatuses.find(c => c.error === error)?.contractId} has error:`, error) + console.warn(`Contract ${contractStatuses.find(c => c.error === error)?.projectId} has error:`, error) return false } if (!syncStatus) return false @@ -114,32 +114,32 @@ export function SendToSHIButton({ // 각 contract별로 순차 동기화 for (let i = 0; i < contractsToSync.length; i++) { - const { contractId } = contractsToSync[i] - setCurrentSyncingContract(contractId) + const { projectId } = contractsToSync[i] + setCurrentSyncingContract(projectId) try { - console.log(`Syncing contract ${contractId}...`) + console.log(`Syncing contract ${projectId}...`) const result = await triggerSync({ - contractId, + projectId, targetSystem }) if (result?.success) { successfulSyncs++ totalSuccessCount += result.successCount || 0 - console.log(`Contract ${contractId} sync successful:`, result) + console.log(`Contract ${projectId} sync successful:`, result) } else { failedSyncs++ totalFailureCount += result?.failureCount || 0 const errorMsg = result?.errors?.[0] || result?.message || 'Unknown sync error' - errors.push(`Contract ${contractId}: ${errorMsg}`) - console.error(`Contract ${contractId} sync failed:`, result) + errors.push(`Contract ${projectId}: ${errorMsg}`) + console.error(`Contract ${projectId} 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) + errors.push(`Contract ${projectId}: ${errorMessage}`) + console.error(`Contract ${projectId} sync exception:`, error) } // 진행률 업데이트 @@ -338,9 +338,9 @@ export function SendToSHIButton({ <div className="text-sm font-medium">계약별 상태</div> <ScrollArea className="h-32"> <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 className="font-medium">Contract {contractId}</span> + {contractStatuses.map(({ projectId, syncStatus, isLoading, error }) => ( + <div key={projectId} className="flex items-center justify-between text-xs p-2 rounded border"> + <span className="font-medium">Contract {projectId}</span> {isLoading ? ( <Badge variant="secondary" className="text-xs"> <Loader2 className="w-3 h-3 mr-1 animate-spin" /> |
