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/bulk-b4-upload-dialog.tsx | 51 ++++++++-
.../ship/import-from-dolce-button.tsx | 126 ++++++++++++---------
2 files changed, 119 insertions(+), 58 deletions(-)
(limited to 'lib/vendor-document-list/ship')
diff --git a/lib/vendor-document-list/ship/bulk-b4-upload-dialog.tsx b/lib/vendor-document-list/ship/bulk-b4-upload-dialog.tsx
index 3ff2f467..be656a48 100644
--- a/lib/vendor-document-list/ship/bulk-b4-upload-dialog.tsx
+++ b/lib/vendor-document-list/ship/bulk-b4-upload-dialog.tsx
@@ -163,9 +163,53 @@ export function BulkB4UploadDialog({
setPendingProjectId("")
}
+ // 파일 검증 함수
+ const validateFile = (file: File): { valid: boolean; error?: string } => {
+ const MAX_FILE_SIZE = 1024 * 1024 * 1024 // 1GB
+ const FORBIDDEN_EXTENSIONS = ['exe', 'com', 'dll', 'vbs', 'js', 'asp', 'aspx', 'bat', 'cmd']
+
+ // 파일 크기 검증
+ if (file.size > MAX_FILE_SIZE) {
+ return {
+ valid: false,
+ error: `파일 크기가 1GB를 초과합니다 (${(file.size / (1024 * 1024 * 1024)).toFixed(2)}GB)`
+ }
+ }
+
+ // 파일 확장자 검증
+ const extension = file.name.split('.').pop()?.toLowerCase()
+ if (extension && FORBIDDEN_EXTENSIONS.includes(extension)) {
+ return {
+ valid: false,
+ error: `금지된 파일 형식입니다 (.${extension})`
+ }
+ }
+
+ return { valid: true }
+ }
+
// 파일 선택 시 파싱
const handleFilesChange = (files: File[]) => {
- const parsed = files.map(file => {
+ const validFiles: File[] = []
+ const invalidFiles: string[] = []
+
+ // 파일 검증
+ files.forEach(file => {
+ const validation = validateFile(file)
+ if (validation.valid) {
+ validFiles.push(file)
+ } else {
+ invalidFiles.push(`${file.name}: ${validation.error}`)
+ }
+ })
+
+ // 유효하지 않은 파일이 있으면 토스트 표시
+ if (invalidFiles.length > 0) {
+ invalidFiles.forEach(msg => toast.error(msg))
+ }
+
+ // 유효한 파일만 파싱
+ const parsed = validFiles.map(file => {
const { docNumber, revision } = parseFileName(file.name)
return {
file,
@@ -429,7 +473,10 @@ export function BulkB4UploadDialog({
}
- PDF, DOC, DOCX, XLS, XLSX, DWG, DXF
+ PDF, DOC, DOCX, XLS, XLSX, DWG, DXF (max 1GB per file)
+
+
+ Forbidden: .exe, .com, .dll, .vbs, .js, .asp, .aspx, .bat, .cmd
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()
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(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([])
const [loadingVendorProjects, setLoadingVendorProjects] = React.useState(false)
+ const [projectsMap, setProjectsMap] = React.useState