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/import-from-dolce-button.tsx | |
| parent | f0213de0d2fb5fcb931b3ddaddcbb6581cab5d28 (diff) | |
(대표님) 벤더 문서 변경사항, data-table 변경, sync 변경
Diffstat (limited to 'lib/vendor-document-list/ship/import-from-dolce-button.tsx')
| -rw-r--r-- | lib/vendor-document-list/ship/import-from-dolce-button.tsx | 363 |
1 files changed, 228 insertions, 135 deletions
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> |
