"use client"; import { useState, useTransition, useMemo, useEffect, useRef } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; import { Label } from "@/components/ui/label"; import { Search, X, Check, ChevronsUpDown, Upload, RefreshCw } from "lucide-react"; import { useToast } from "@/hooks/use-toast"; import { cn } from "@/lib/utils"; import { SwpUploadHelpDialog } from "./swp-help-dialog"; import { SwpUploadResultDialog } from "./swp-upload-result-dialog"; import { SwpUploadValidationDialog, validateFileName } from "./swp-upload-validation-dialog"; import { SwpUploadedFilesDialog } from "./swp-uploaded-files-dialog"; import { getDocumentClassInfoByProjectCode } from "@/lib/swp/swp-upload-server-actions"; import type { DocumentListItem } from "@/lib/swp/document-service"; interface SwpTableFilters { docNo?: string; docTitle?: string; pkgNo?: string; stage?: string; status?: string; } interface SwpTableToolbarProps { projNo: string; filters: SwpTableFilters; onProjNoChange: (projNo: string) => void; onFiltersChange: (filters: SwpTableFilters) => void; onRefresh: () => void; isRefreshing: boolean; projects?: Array<{ PROJ_NO: string; PROJ_NM: string | null }>; vendorCode?: string; droppedFiles?: File[]; onFilesProcessed?: () => void; documents?: DocumentListItem[]; // 업로드 권한 검증 + DOC_CLS (Document Class) 확인용 문서 목록 userId?: string; // 파일 취소 시 필요 } export function SwpTableToolbar({ projNo, filters, onProjNoChange, onFiltersChange, onRefresh, isRefreshing, projects = [], vendorCode, droppedFiles = [], onFilesProcessed, documents = [], userId, }: SwpTableToolbarProps) { const [isUploading, startUpload] = useTransition(); const [localFilters, setLocalFilters] = useState(filters); const { toast } = useToast(); const [projectSearchOpen, setProjectSearchOpen] = useState(false); const [projectSearch, setProjectSearch] = useState(""); const fileInputRef = useRef(null); const [uploadResults, setUploadResults] = useState>([]); const [showResultDialog, setShowResultDialog] = useState(false); // 검증 다이얼로그 상태 const [validationResults, setValidationResults] = useState>([]); const [showValidationDialog, setShowValidationDialog] = useState(false); // EVCP DB에서 조회한 문서 정보 (vendorDocNumber → Document Class 매핑) const [vendorDocNumberToDocClassMap, setVendorDocNumberToDocClassMap] = useState>({}); // Document Class별 허용 Stage 목록 const [documentClassStages, setDocumentClassStages] = useState>({}); /** * 업로드 가능한 문서번호 목록 추출 (OWN_DOC_NO 기준) * SWP API의 OWN_DOC_NO가 EVCP DB의 vendorDocNumber와 매핑되는지 확인 */ const availableDocNos = useMemo(() => { return documents .map(doc => doc.OWN_DOC_NO) .filter((ownDocNo): ownDocNo is string => { // OWN_DOC_NO가 있고, EVCP DB에 등록된 문서인지 확인 return ownDocNo !== null && ownDocNo !== undefined && vendorDocNumberToDocClassMap[ownDocNo] !== undefined; }); }, [documents, vendorDocNumberToDocClassMap]); /** * 벤더 모드 여부 (벤더 코드가 있으면 벤더 모드) */ const isVendorMode = !!vendorCode; /** * 프로젝트 변경 시 EVCP DB에서 문서 정보 로드 * - vendorDocNumber → docClass 매핑 * - Document Class별 허용 Stage 목록 */ useEffect(() => { if (!projNo) { setVendorDocNumberToDocClassMap({}); setDocumentClassStages({}); return; } let isCancelled = false; const loadDocumentClassInfo = async () => { try { console.log(`[SwpTableToolbar] 프로젝트 ${projNo} 문서 정보 로드 시작`); // 서버 액션 호출 const result = await getDocumentClassInfoByProjectCode(projNo); if (!isCancelled) { if (result.success) { setVendorDocNumberToDocClassMap(result.vendorDocNumberToDocClassMap); setDocumentClassStages(result.documentClassStages); console.log(`[SwpTableToolbar] 문서 정보 로드 완료:`, { vendorDocNumbers: Object.keys(result.vendorDocNumberToDocClassMap).length, documentClassStages: result.documentClassStages, }); } else { console.warn(`[SwpTableToolbar] 문서 정보 로드 실패:`, result.error); setVendorDocNumberToDocClassMap({}); setDocumentClassStages({}); toast({ variant: "destructive", title: "문서 정보 로드 실패", description: result.error || "문서 정보를 가져올 수 없습니다.", }); } } } catch (error) { if (!isCancelled) { console.error('[SwpTableToolbar] 문서 정보 로드 실패:', error); setVendorDocNumberToDocClassMap({}); setDocumentClassStages({}); toast({ variant: "destructive", title: "문서 정보 로드 실패", description: "문서 정보를 가져올 수 없습니다. 페이지를 새로고침해주세요.", }); } } }; loadDocumentClassInfo(); return () => { isCancelled = true; }; }, [projNo, toast]); /** * 드롭된 파일 처리 - useEffect로 감지하여 자동 검증 */ useEffect(() => { if (droppedFiles.length > 0) { // 프로젝트와 벤더 코드 검증 if (!projNo) { toast({ variant: "destructive", title: "프로젝트 선택 필요", description: "파일을 업로드할 프로젝트를 먼저 선택해주세요.", }); onFilesProcessed?.(); return; } if (!vendorCode) { toast({ variant: "destructive", title: "업체 코드 오류", description: "벤더 정보를 가져올 수 없습니다.", }); onFilesProcessed?.(); return; } // 파일명 검증 (문서번호 권한 + Stage 검증 포함) const results = droppedFiles.map((file) => { const validation = validateFileName( file.name, availableDocNos, isVendorMode, vendorDocNumberToDocClassMap, documentClassStages ); return { file, valid: validation.valid, parsed: validation.parsed, error: validation.error, }; }); setValidationResults(results); setShowValidationDialog(true); onFilesProcessed?.(); } }, [droppedFiles, projNo, vendorCode, toast, onFilesProcessed, availableDocNos, isVendorMode, vendorDocNumberToDocClassMap, documentClassStages]); /** * 파일 업로드 핸들러 */ const handleUploadFiles = () => { if (!projNo) { toast({ variant: "destructive", title: "프로젝트 선택 필요", description: "파일을 업로드할 프로젝트를 먼저 선택해주세요.", }); return; } if (!vendorCode) { toast({ variant: "destructive", title: "업체 코드 오류", description: "벤더 정보를 가져올 수 없습니다.", }); return; } fileInputRef.current?.click(); }; /** * 파일 선택 핸들러 - 검증만 수행 */ const handleFileChange = (event: React.ChangeEvent) => { const selectedFiles = event.target.files; if (!selectedFiles || selectedFiles.length === 0) { return; } // 각 파일의 파일명 검증 (문서번호 권한 + Stage 검증 포함) const results = Array.from(selectedFiles).map((file) => { const validation = validateFileName( file.name, availableDocNos, isVendorMode, vendorDocNumberToDocClassMap, documentClassStages ); return { file, valid: validation.valid, parsed: validation.parsed, error: validation.error, }; }); setValidationResults(results); setShowValidationDialog(true); // input 초기화 (같은 파일 재선택 가능하도록) if (fileInputRef.current) { fileInputRef.current.value = ""; } }; /** * 검증 완료 후 실제 업로드 실행 */ const handleConfirmUpload = async (validFiles: File[]) => { startUpload(async () => { try { toast({ title: "파일 업로드 시작", description: `${validFiles.length}개 파일을 업로드합니다...`, }); const formData = new FormData(); formData.append("projNo", projNo); formData.append("vndrCd", vendorCode!); validFiles.forEach((file) => { formData.append("files", file); }); const response = await fetch("/api/swp/upload", { method: "POST", body: formData, }); if (!response.ok) { throw new Error(`업로드 실패: ${response.statusText}`); } const result = await response.json(); // 검증 다이얼로그 닫기 setShowValidationDialog(false); // 결과 다이얼로그 표시 setUploadResults(result.details || []); setShowResultDialog(true); toast({ title: result.success ? "업로드 완료" : "일부 업로드 실패", description: result.message, }); } catch (error) { console.error("파일 업로드 실패:", error); // 검증 다이얼로그 닫기 setShowValidationDialog(false); const errorResults = validFiles.map((file) => ({ fileName: file.name, success: false, error: error instanceof Error ? error.message : "알 수 없는 오류", })); setUploadResults(errorResults); setShowResultDialog(true); } }); }; // 검색 적용 const handleSearch = () => { onFiltersChange(localFilters); }; // 검색 초기화 const handleReset = () => { const resetFilters: SwpTableFilters = { docNo: "", docTitle: "", pkgNo: "", stage: "", }; setLocalFilters(resetFilters); onFiltersChange(resetFilters); }; // 프로젝트 필터링 const filteredProjects = useMemo(() => { if (!projectSearch) return projects; const search = projectSearch.toLowerCase(); return projects.filter( (proj) => proj.PROJ_NO.toLowerCase().includes(search) || (proj.PROJ_NM?.toLowerCase().includes(search) ?? false) ); }, [projects, projectSearch]); return ( <> {/* 업로드 검증 다이얼로그 */} {/* 업로드 결과 다이얼로그 */}
{/* 상단 액션 바 */} {vendorCode && (
{/* 별도 탭으로 분리하고 메인 테이블로 변경하였음. */} {/* {userId && ( )} */}
)} {/* 검색 필터 */}

검색 필터

{/* 프로젝트 번호 */}
{projects.length > 0 ? (
setProjectSearch(e.target.value)} className="border-0 focus-visible:ring-0 focus-visible:ring-offset-0" />
{filteredProjects.map((proj) => ( ))} {filteredProjects.length === 0 && (
검색 결과가 없습니다.
)}
) : ( )}
{/* 문서 번호 */}
setLocalFilters({ ...localFilters, docNo: e.target.value }) } />
{/* 문서 제목 */}
setLocalFilters({ ...localFilters, docTitle: e.target.value }) } />
{/* 패키지 번호 */}
setLocalFilters({ ...localFilters, pkgNo: e.target.value }) } />
{/* 스테이지 */}
setLocalFilters({ ...localFilters, stage: e.target.value }) } />
{/* 상태 */}
setLocalFilters({ ...localFilters, status: e.target.value }) } />
); }