summaryrefslogtreecommitdiff
path: root/lib/vendor-document-list/ship/import-from-dolce-button.tsx
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-08-04 09:36:14 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-08-04 09:36:14 +0000
commit92eda21e45d902663052575aaa4c4f80bfa2faea (patch)
tree8483702edf82932d4359a597a854fa8e1b48e94b /lib/vendor-document-list/ship/import-from-dolce-button.tsx
parentf0213de0d2fb5fcb931b3ddaddcbb6581cab5d28 (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.tsx363
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>