summaryrefslogtreecommitdiff
path: root/lib/vendor-document-list/ship
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
parentf0213de0d2fb5fcb931b3ddaddcbb6581cab5d28 (diff)
(대표님) 벤더 문서 변경사항, data-table 변경, sync 변경
Diffstat (limited to 'lib/vendor-document-list/ship')
-rw-r--r--lib/vendor-document-list/ship/enhanced-doc-table-toolbar-actions.tsx80
-rw-r--r--lib/vendor-document-list/ship/enhanced-documents-table.tsx111
-rw-r--r--lib/vendor-document-list/ship/import-from-dolce-button.tsx363
-rw-r--r--lib/vendor-document-list/ship/send-to-shi-button.tsx32
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" />