From 1363913352722a03e051b15297f72bf16d80106f Mon Sep 17 00:00:00 2001
From: joonhoekim <26rote@gmail.com>
Date: Fri, 7 Nov 2025 17:39:36 +0900
Subject: (김준회) 돌체 업로드 MIME 타입 검증 문제 확장자로 처리
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../ship/import-from-dolce-button.tsx              | 126 ++++++++++++---------
 1 file changed, 70 insertions(+), 56 deletions(-)

(limited to 'lib/vendor-document-list/ship/import-from-dolce-button.tsx')

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 76d66960..dfbd0600 100644
--- a/lib/vendor-document-list/ship/import-from-dolce-button.tsx
+++ b/lib/vendor-document-list/ship/import-from-dolce-button.tsx
@@ -24,21 +24,28 @@ import { Separator } from "@/components/ui/separator"
 import { SimplifiedDocumentsView } from "@/db/schema"
 import { ImportStatus } from "../import-service"
 import { useSession } from "next-auth/react"
-import { getProjectIdsByVendor } from "../service"
+import { getProjectIdsByVendor, getProjectsByIds } from "../service"
 import { useParams } from "next/navigation"
 import { useTranslation } from "@/i18n/client"
 
-// 🔥 API 응답 캐시 (컴포넌트 외부에 선언하여 인스턴스 간 공유)
+// API 응답 캐시 (컴포넌트 외부에 선언하여 인스턴스 간 공유)
 const statusCache = new Map<string, { data: ImportStatus; timestamp: number }>()
 const CACHE_TTL = 2 * 60 * 1000 // 2분 캐시
 
 interface ImportFromDOLCEButtonProps {
   allDocuments: SimplifiedDocumentsView[]
-  projectIds?: number[] // 🔥 미리 계산된 projectIds를 props로 받음
+  projectIds?: number[] // 미리 계산된 projectIds를 props로 받음
   onImportComplete?: () => void
 }
 
-// 🔥 디바운스 훅
+// 프로젝트 정보 타입
+interface ProjectInfo {
+  id: number
+  code: string
+  name: string
+}
+
+// 디바운스 훅
 function useDebounce<T>(value: T, delay: number): T {
   const [debouncedValue, setDebouncedValue] = React.useState(value)
 
@@ -67,6 +74,7 @@ export function ImportFromDOLCEButton({
   const [statusLoading, setStatusLoading] = React.useState(false)
   const [vendorProjectIds, setVendorProjectIds] = React.useState<number[]>([])
   const [loadingVendorProjects, setLoadingVendorProjects] = React.useState(false)
+  const [projectsMap, setProjectsMap] = React.useState<Map<number, ProjectInfo>>(new Map())
   
   const { data: session } = useSession()
   const vendorId = session?.user.companyId
@@ -75,15 +83,15 @@ export function ImportFromDOLCEButton({
   const lng = (params?.lng as string) || "ko"
   const { t } = useTranslation(lng, "engineering")
 
-  // 🔥 allDocuments에서 projectIds 추출 (props로 전달받은 경우 사용)
+  // allDocuments에서 projectIds 추출 (props로 전달받은 경우 사용)
   const documentsProjectIds = React.useMemo(() => {
     if (propProjectIds) return propProjectIds // props로 받은 경우 그대로 사용
     
-    const uniqueIds = [...new Set(allDocuments.map(doc => doc.projectId).filter(Boolean))]
+    const uniqueIds = [...new Set(allDocuments.map(doc => doc.projectId).filter((id): id is number => id !== null))]
     return uniqueIds.sort()
   }, [allDocuments, propProjectIds])
 
-  // 🔥 최종 projectIds (변경 빈도 최소화)
+  // 최종 projectIds (변경 빈도 최소화)
   const projectIds = React.useMemo(() => {
     if (documentsProjectIds.length > 0) {
       return documentsProjectIds
@@ -91,28 +99,10 @@ export function ImportFromDOLCEButton({
     return vendorProjectIds
   }, [documentsProjectIds, vendorProjectIds])
 
-  // 🔥 projectIds 디바운싱 (API 호출 과다 방지)
+  // projectIds 디바운싱 (API 호출 과다 방지)
   const debouncedProjectIds = useDebounce(projectIds, 300)
 
-  // 🔥 주요 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.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] || projectIds[0] || 0)
-    }
-    
-    return projectIds[0] || 0
-  }, [projectIds, allDocuments])
-
-  // 🔥 캐시된 API 호출 함수
+  // 캐시된 API 호출 함수
   const fetchImportStatusCached = React.useCallback(async (projectId: number): Promise<ImportStatus | null> => {
     const cacheKey = `import-status-${projectId}`
     const cached = statusCache.get(cacheKey)
@@ -148,7 +138,7 @@ export function ImportFromDOLCEButton({
     }
   }, [])
 
-  // 🔥 모든 projectId에 대한 상태 조회 (최적화된 버전)
+  // 모든 projectId에 대한 상태 조회 (최적화된 버전)
   const fetchAllImportStatus = React.useCallback(async () => {
     if (debouncedProjectIds.length === 0) return
     
@@ -156,9 +146,9 @@ export function ImportFromDOLCEButton({
     const statusMap = new Map<number, ImportStatus>()
     
     try {
-      // 🔥 병렬 처리하되 동시 연결 수 제한 (3개씩)
+      // 병렬 처리하되 동시 연결 수 제한 (3개씩)
       const batchSize = 3
-      const batches = []
+      const batches: number[][] = []
       
       for (let i = 0; i < debouncedProjectIds.length; i += batchSize) {
         batches.push(debouncedProjectIds.slice(i, i + batchSize))
@@ -194,7 +184,7 @@ export function ImportFromDOLCEButton({
     }
   }, [debouncedProjectIds, fetchImportStatusCached, t])
 
-  // 🔥 vendorId로 projects 가져오기 (최적화)
+  // vendorId로 projects 가져오기 (최적화)
   React.useEffect(() => {
     let isCancelled = false;
   
@@ -206,7 +196,7 @@ export function ImportFromDOLCEButton({
       .then((projectIds) => {
         if (!isCancelled) setVendorProjectIds(projectIds);
       })
-      .catch((error) => {
+      .catch(() => {
         if (!isCancelled) toast.error(t('dolceImport.messages.projectFetchError'));
       })
       .finally(() => {
@@ -215,15 +205,36 @@ export function ImportFromDOLCEButton({
   
     return () => { isCancelled = true; };
   }, [allDocuments, vendorId, t]);
+
+  // projectIds로 프로젝트 정보 가져오기 (서버 액션 사용)
+  React.useEffect(() => {
+    if (projectIds.length === 0) return;
+
+    const fetchProjectsInfo = async () => {
+      try {
+        const projectsData = await getProjectsByIds(projectIds);
+
+        const newProjectsMap = new Map<number, ProjectInfo>();
+        projectsData.forEach((project) => {
+          newProjectsMap.set(project.id, project);
+        });
+        setProjectsMap(newProjectsMap);
+      } catch (error) {
+        console.error('프로젝트 정보 조회 실패:', error);
+      }
+    };
+
+    fetchProjectsInfo();
+  }, [projectIds]);
   
-  // 🔥 컴포넌트 마운트 시 상태 조회 (디바운싱 적용)
+  // 컴포넌트 마운트 시 상태 조회 (디바운싱 적용)
   React.useEffect(() => {
     if (debouncedProjectIds.length > 0) {
       fetchAllImportStatus()
     }
   }, [debouncedProjectIds, fetchAllImportStatus])
 
-  // 🔥 전체 통계 메모이제이션 - 리비전과 첨부파일 추가
+  // 전체 통계 메모이제이션 - 리비전과 첨부파일 추가
   const totalStats = React.useMemo(() => {
     const statuses = Array.from(importStatusMap.values())
     return statuses.reduce((acc, status) => ({
@@ -251,12 +262,7 @@ export function ImportFromDOLCEButton({
     })
   }, [importStatusMap])
 
-  // 🔥 주요 상태 메모이제이션
-  const primaryImportStatus = React.useMemo(() => {
-    return importStatusMap.get(primaryProjectId)
-  }, [importStatusMap, primaryProjectId])
-
-  // 🔥 가져오기 실행 함수 최적화
+  // 가져오기 실행 함수 최적화
   const handleImport = React.useCallback(async () => {
     if (projectIds.length === 0) return
     
@@ -268,8 +274,14 @@ export function ImportFromDOLCEButton({
         setImportProgress(prev => Math.min(prev + 10, 85))
       }, 500)
 
-      // 🔥 순차 처리로 서버 부하 방지
-      const results = []
+      // 순차 처리로 서버 부하 방지
+      const results: Array<{
+        success: boolean
+        newCount?: number
+        updatedCount?: number
+        skippedCount?: number
+        error?: string
+      }> = []
       for (const projectId of projectIds) {
         try {
           const response = await fetch('/api/sync/import', {
@@ -304,14 +316,14 @@ export function ImportFromDOLCEButton({
       
       // 결과 집계
       const totalResult = results.reduce((acc, result) => ({
-        newCount: acc.newCount + (result.newCount || 0),
-        updatedCount: acc.updatedCount + (result.updatedCount || 0),
-        skippedCount: acc.skippedCount + (result.skippedCount || 0),
+        newCount: (acc.newCount || 0) + (result.newCount || 0),
+        updatedCount: (acc.updatedCount || 0) + (result.updatedCount || 0),
+        skippedCount: (acc.skippedCount || 0) + (result.skippedCount || 0),
         success: acc.success && result.success
       }), {
-        newCount: 0,
-        updatedCount: 0,
-        skippedCount: 0,
+        newCount: 0 as number,
+        updatedCount: 0 as number,
+        skippedCount: 0 as number,
         success: true
       })
       
@@ -341,7 +353,7 @@ export function ImportFromDOLCEButton({
           )
         }
         
-        // 🔥 캐시 무효화
+        // 캐시 무효화
         statusCache.clear()
         fetchAllImportStatus()
         onImportComplete?.()
@@ -357,14 +369,14 @@ export function ImportFromDOLCEButton({
     }
   }, [projectIds, fetchAllImportStatus, onImportComplete, t])
 
-  // 🔥 전체 변경 사항 계산
+  // 전체 변경 사항 계산
   const totalChanges = React.useMemo(() => {
     return totalStats.newDocuments + totalStats.updatedDocuments + 
            totalStats.newRevisions + totalStats.updatedRevisions + 
            totalStats.newAttachments + totalStats.updatedAttachments
   }, [totalStats])
 
-  // 🔥 상태 뱃지 메모이제이션 - 리비전과 첨부파일 포함
+  // 상태 뱃지 메모이제이션 - 리비전과 첨부파일 포함
   const statusBadge = React.useMemo(() => {
     if (loadingVendorProjects) {
       return <Badge variant="secondary">{t('dolceImport.status.loadingProjectInfo')}</Badge>
@@ -399,16 +411,16 @@ export function ImportFromDOLCEButton({
     )
   }, [loadingVendorProjects, statusLoading, importStatusMap.size, totalStats.importEnabled, totalChanges, projectIds.length, t])
 
-  // 🔥 가져오기 가능 여부 - 리비전과 첨부파일도 체크
+  // 가져오기 가능 여부 - 리비전과 첨부파일도 체크
   const canImport = totalStats.importEnabled && totalChanges > 0
 
-  // 🔥 새로고침 핸들러 최적화
+  // 새로고침 핸들러 최적화
   const handleRefresh = React.useCallback(() => {
     statusCache.clear() // 캐시 무효화
     fetchAllImportStatus()
   }, [fetchAllImportStatus])
 
-  // 🔥 자동 동기화 실행 (기존 useEffect들 다음에 추가)
+  // 자동 동기화 실행 (기존 useEffect들 다음에 추가)
   React.useEffect(() => {
     // 조건: 가져오기 가능하고, 동기화할 항목이 있고, 현재 진행중이 아닐 때
     if (canImport && totalChanges > 0 && !isImporting && !isDialogOpen) {
@@ -496,7 +508,7 @@ export function ImportFromDOLCEButton({
                 <div className="text-muted-foreground">{t('dolceImport.labels.targetProjects')}</div>
                 <div className="font-medium">{t('dolceImport.labels.projectCount', { count: projectIds.length })}</div>
                 <div className="text-xs text-muted-foreground">
-                  {t('dolceImport.labels.projectIds')}: {projectIds.join(', ')}
+                  {t('dolceImport.labels.projectIds')}: {projectIds.map(id => projectsMap.get(id)?.code || id).join(', ')}
                 </div>
               </div>
             )}
@@ -584,9 +596,11 @@ export function ImportFromDOLCEButton({
                     <div className="mt-2 space-y-2 pl-2 border-l-2 border-muted">
                       {projectIds.map(projectId => {
                         const status = importStatusMap.get(projectId)
+                        const projectInfo = projectsMap.get(projectId)
+                        const projectLabel = projectInfo?.code || projectId
                         return (
                           <div key={projectId} className="text-xs">
-                            <div className="font-medium">{t('dolceImport.labels.projectLabel', { projectId })}</div>
+                            <div className="font-medium">{t('dolceImport.labels.projectLabel', { projectId: projectLabel })}</div>
                             {status ? (
                               <div className="text-muted-foreground space-y-1">
                                 <div>
-- 
cgit v1.2.3

