From 2ecdac866c19abea0b5389708fcdf5b3889c969a Mon Sep 17 00:00:00 2001
From: joonhoekim <26rote@gmail.com>
Date: Wed, 29 Oct 2025 15:59:04 +0900
Subject: (김준회) SWP 파일 업로드 취소 기능 추가, 업로드 파일명 검증로직에서
 파일명 비필수로 변경
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 lib/swp/table/swp-table-toolbar.tsx | 634 +++++++++++++++++++-----------------
 1 file changed, 338 insertions(+), 296 deletions(-)

(limited to 'lib/swp/table/swp-table-toolbar.tsx')

diff --git a/lib/swp/table/swp-table-toolbar.tsx b/lib/swp/table/swp-table-toolbar.tsx
index fefff091..0fd29fd3 100644
--- a/lib/swp/table/swp-table-toolbar.tsx
+++ b/lib/swp/table/swp-table-toolbar.tsx
@@ -1,6 +1,6 @@
 "use client";
 
-import { useState, useTransition, useMemo } from "react";
+import { useState, useTransition, useMemo, useEffect, useRef } from "react";
 import { Button } from "@/components/ui/button";
 import { Input } from "@/components/ui/input";
 import {
@@ -9,94 +9,137 @@ import {
   PopoverTrigger,
 } from "@/components/ui/popover";
 import { Label } from "@/components/ui/label";
-import { RefreshCw, Search, X, Check, ChevronsUpDown, Upload } from "lucide-react";
-import { syncSwpProjectAction, type SwpTableFilters } from "../actions";
+import { Search, X, Check, ChevronsUpDown, Upload, RefreshCw } from "lucide-react";
 import { useToast } from "@/hooks/use-toast";
-import { useRouter } from "next/navigation";
 import { cn } from "@/lib/utils";
-import { useRef } from "react";
 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";
+
+interface SwpTableFilters {
+  docNo?: string;
+  docTitle?: string;
+  pkgNo?: string;
+  stage?: string;
+}
 
 interface SwpTableToolbarProps {
+  projNo: string;
   filters: SwpTableFilters;
+  onProjNoChange: (projNo: string) => void;
   onFiltersChange: (filters: SwpTableFilters) => void;
-  projects?: Array<{ PROJ_NO: string; PROJ_NM: string }>;
-  vendorCode?: string; // 벤더가 접속했을 때 고정할 벤더 코드
+  onRefresh: () => void;
+  isRefreshing: boolean;
+  projects?: Array<{ PROJ_NO: string; PROJ_NM: string | null }>;
+  vendorCode?: string;
+  droppedFiles?: File[];
+  onFilesProcessed?: () => void;
+  documents?: Array<{ DOC_NO: string }>; // 업로드 권한 검증용 문서 목록
+  userId?: string; // 파일 취소 시 필요
 }
 
 export function SwpTableToolbar({
+  projNo,
   filters,
+  onProjNoChange,
   onFiltersChange,
+  onRefresh,
+  isRefreshing,
   projects = [],
   vendorCode,
+  droppedFiles = [],
+  onFilesProcessed,
+  documents = [],
+  userId,
 }: SwpTableToolbarProps) {
-  const [isSyncing, startSync] = useTransition();
   const [isUploading, startUpload] = useTransition();
-  const [localFilters, setLocalFilters] = useState<SwpTableFilters>(filters);
+  const [localFilters, setLocalFilters] = useState(filters);
   const { toast } = useToast();
-  const router = useRouter();
   const [projectSearchOpen, setProjectSearchOpen] = useState(false);
   const [projectSearch, setProjectSearch] = useState("");
   const fileInputRef = useRef<HTMLInputElement>(null);
   const [uploadResults, setUploadResults] = useState<Array<{ fileName: string; success: boolean; error?: string }>>([]);
   const [showResultDialog, setShowResultDialog] = useState(false);
+  
+  // 검증 다이얼로그 상태
+  const [validationResults, setValidationResults] = useState<Array<{
+    file: File;
+    valid: boolean;
+    parsed?: {
+      ownDocNo: string;
+      revNo: string;
+      stage: string;
+      fileName: string;
+      extension: string;
+    };
+    error?: string;
+  }>>([]);
+  const [showValidationDialog, setShowValidationDialog] = useState(false);
 
-  // 동기화 핸들러
-  const handleSync = () => {
-    const projectNo = localFilters.projNo;
-    
-    if (!projectNo) {
-      toast({
-        variant: "destructive",
-        title: "프로젝트 선택 필요",
-        description: "동기화할 프로젝트를 먼저 선택해주세요.",
-      });
-      return;
-    }
+  /**
+   * 업로드 가능한 문서번호 목록 추출
+   */
+  const availableDocNos = useMemo(() => {
+    return documents.map(doc => doc.DOC_NO);
+  }, [documents]);
 
-    startSync(async () => {
-      try {
+  /**
+   * 벤더 모드 여부 (벤더 코드가 있으면 벤더 모드)
+   */
+  const isVendorMode = !!vendorCode;
+
+  /**
+   * 드롭된 파일 처리 - useEffect로 감지하여 자동 검증
+   */
+  useEffect(() => {
+    if (droppedFiles.length > 0) {
+      // 프로젝트와 벤더 코드 검증
+      if (!projNo) {
         toast({
-          title: "동기화 시작",
-          description: `프로젝트 ${projectNo} 동기화를 시작합니다...`,
+          variant: "destructive",
+          title: "프로젝트 선택 필요",
+          description: "파일을 업로드할 프로젝트를 먼저 선택해주세요.",
         });
+        onFilesProcessed?.();
+        return;
+      }
 
-        const result = await syncSwpProjectAction(projectNo, "V");
-
-        if (result.success) {
-          toast({
-            title: "동기화 완료",
-            description: `문서 ${result.stats.documents.total}개, 파일 ${result.stats.files.total}개 동기화 완료`,
-          });
-          
-          // 페이지 새로고침
-          router.refresh();
-        } else {
-          throw new Error(result.errors.join(", "));
-        }
-      } catch (error) {
-        console.error("동기화 실패:", error);
+      if (!vendorCode) {
         toast({
           variant: "destructive",
-          title: "동기화 실패",
-          description: error instanceof Error ? error.message : "알 수 없는 오류",
+          title: "업체 코드 오류",
+          description: "벤더 정보를 가져올 수 없습니다.",
         });
+        onFilesProcessed?.();
+        return;
       }
-    });
-  };
+
+      // 파일명 검증 (문서번호 권한 포함)
+      const results = droppedFiles.map((file) => {
+        const validation = validateFileName(file.name, availableDocNos, isVendorMode);
+        return {
+          file,
+          valid: validation.valid,
+          parsed: validation.parsed,
+          error: validation.error,
+        };
+      });
+
+      setValidationResults(results);
+      setShowValidationDialog(true);
+      onFilesProcessed?.();
+    }
+  }, [droppedFiles, projNo, vendorCode, toast, onFilesProcessed, availableDocNos, isVendorMode]);
 
   /**
    * 파일 업로드 핸들러
-   * 1) 네트워크 드라이브에 정해진 규칙대로, 파일이름 기반으로 파일 업로드하기
-   * 2) 1~N개 파일 받아서, 파일 이름 기준으로 파싱해서 SaveInBoxList API를 통해 업로드 처리
-  */ 
+   */
   const handleUploadFiles = () => {
-    // 프로젝트와 벤더 코드 체크
-    const projectNo = localFilters.projNo;
-    const vndrCd = vendorCode || localFilters.vndrCd;
-
-    if (!projectNo) {
+    if (!projNo) {
       toast({
         variant: "destructive",
         title: "프로젝트 선택 필요",
@@ -105,48 +148,66 @@ export function SwpTableToolbar({
       return;
     }
 
-    if (!vndrCd) {
+    if (!vendorCode) {
       toast({
         variant: "destructive",
-        title: "업체 코드 입력 필요",
-        description: "파일을 업로드할 업체 코드를 입력해주세요.",
+        title: "업체 코드 오류",
+        description: "벤더 정보를 가져올 수 없습니다.",
       });
       return;
     }
 
-    // 파일 선택 다이얼로그 열기
     fileInputRef.current?.click();
   };
 
   /**
-   * 파일 선택 핸들러
+   * 파일 선택 핸들러 - 검증만 수행
    */
-  const handleFileChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
+  const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
     const selectedFiles = event.target.files;
     if (!selectedFiles || selectedFiles.length === 0) {
       return;
     }
 
-    const projectNo = localFilters.projNo!;
-    const vndrCd = vendorCode || localFilters.vndrCd!;
+    // 각 파일의 파일명 검증 (문서번호 권한 포함)
+    const results = Array.from(selectedFiles).map((file) => {
+      const validation = validateFileName(file.name, availableDocNos, isVendorMode);
+      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: `${selectedFiles.length}개 파일을 업로드합니다...`,
+          description: `${validFiles.length}개 파일을 업로드합니다...`,
         });
 
-        // FormData 생성 (바이너리 직접 전송)
         const formData = new FormData();
-        formData.append("projNo", projectNo);
-        formData.append("vndrCd", vndrCd);
-        
-        Array.from(selectedFiles).forEach((file) => {
+        formData.append("projNo", projNo);
+        formData.append("vndrCd", vendorCode!);
+
+        validFiles.forEach((file) => {
           formData.append("files", file);
         });
 
-        // API Route 호출
         const response = await fetch("/api/swp/upload", {
           method: "POST",
           body: formData,
@@ -158,31 +219,31 @@ export function SwpTableToolbar({
 
         const result = await response.json();
 
-        // 결과 저장 및 다이얼로그 표시
+        // 검증 다이얼로그 닫기
+        setShowValidationDialog(false);
+
+        // 결과 다이얼로그 표시
         setUploadResults(result.details || []);
         setShowResultDialog(true);
 
-        // 성공한 파일이 있으면 페이지 새로고침
-        if (result.successCount > 0) {
-          router.refresh();
-        }
+        toast({
+          title: result.success ? "업로드 완료" : "일부 업로드 실패",
+          description: result.message,
+        });
       } catch (error) {
         console.error("파일 업로드 실패:", error);
-        
-        // 예외 발생 시에도 결과 다이얼로그 표시
-        const errorResults = Array.from(selectedFiles).map((file) => ({
+
+        // 검증 다이얼로그 닫기
+        setShowValidationDialog(false);
+
+        const errorResults = validFiles.map((file) => ({
           fileName: file.name,
           success: false,
           error: error instanceof Error ? error.message : "알 수 없는 오류",
         }));
-        
+
         setUploadResults(errorResults);
         setShowResultDialog(true);
-      } finally {
-        // 파일 입력 초기화
-        if (fileInputRef.current) {
-          fileInputRef.current.value = "";
-        }
       }
     });
   };
@@ -194,7 +255,12 @@ export function SwpTableToolbar({
 
   // 검색 초기화
   const handleReset = () => {
-    const resetFilters: SwpTableFilters = {};
+    const resetFilters: SwpTableFilters = {
+      docNo: "",
+      docTitle: "",
+      pkgNo: "",
+      stage: "",
+    };
     setLocalFilters(resetFilters);
     onFiltersChange(resetFilters);
   };
@@ -202,17 +268,28 @@ export function SwpTableToolbar({
   // 프로젝트 필터링
   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)
+        (proj.PROJ_NM?.toLowerCase().includes(search) ?? false)
     );
   }, [projects, projectSearch]);
 
   return (
     <>
+      {/* 업로드 검증 다이얼로그 */}
+      <SwpUploadValidationDialog
+        open={showValidationDialog}
+        onOpenChange={setShowValidationDialog}
+        validationResults={validationResults}
+        onConfirmUpload={handleConfirmUpload}
+        isUploading={isUploading}
+        availableDocNos={availableDocNos}
+        isVendorMode={isVendorMode}
+      />
+
       {/* 업로드 결과 다이얼로그 */}
       <SwpUploadResultDialog
         open={showResultDialog}
@@ -220,240 +297,205 @@ export function SwpTableToolbar({
         results={uploadResults}
       />
 
-      <div className="space-y-4">
+      <div className="space-y-4 w-full">
         {/* 상단 액션 바 */}
-        <div className="flex items-center justify-between">
-          <div className="flex items-center gap-2">
+        {vendorCode && (
+          <div className="flex items-center justify-end gap-2">
+            <input
+              ref={fileInputRef}
+              type="file"
+              multiple
+              className="hidden"
+              onChange={handleFileChange}
+              accept="*/*"
+            />
             <Button
-              onClick={handleSync}
-              disabled={isSyncing || !localFilters.projNo}
+              variant="outline"
               size="sm"
+              onClick={onRefresh}
+              disabled={isRefreshing || !projNo}
             >
-              <RefreshCw className={`h-4 w-4 mr-2 ${isSyncing ? "animate-spin" : ""}`} />
-              {isSyncing ? "동기화 중..." : "SWP 동기화"}
+              <RefreshCw className={`h-4 w-4 mr-2 ${isRefreshing ? "animate-spin" : ""}`} />
+              새로고침
             </Button>
-          </div>
-          
-          <div className="flex items-center gap-2">
-            {/* 벤더만 파일 업로드 기능 사용 가능 */}
-            {vendorCode && (
-              <>
-                <input
-                  ref={fileInputRef}
-                  type="file"
-                  multiple
-                  className="hidden"
-                  onChange={handleFileChange}
-                  accept="*/*"
-                />
-                <Button
-                  variant="outline"
-                  size="sm"
-                  onClick={handleUploadFiles}
-                  disabled={isUploading || !localFilters.projNo || (!vendorCode && !localFilters.vndrCd)}
-                >
-                  <Upload className={`h-4 w-4 mr-2 ${isUploading ? "animate-pulse" : ""}`} />
-                  {isUploading ? "업로드 중..." : "파일 업로드"}
-                </Button>
-
-                <SwpUploadHelpDialog />
-              </>
+            <Button
+              variant="outline"
+              size="sm"
+              onClick={handleUploadFiles}
+              disabled={isUploading || !projNo}
+            >
+              <Upload className={`h-4 w-4 mr-2 ${isUploading ? "animate-pulse" : ""}`} />
+              {isUploading ? "업로드 중..." : "파일 업로드"}
+            </Button>
+
+            {userId && (
+              <SwpUploadedFilesDialog
+                projNo={projNo}
+                vndrCd={vendorCode}
+                userId={userId}
+              />
             )}
+
+            <SwpUploadHelpDialog />
           </div>
-        </div>
+        )}
 
-      {/* 검색 필터 */}
-      <div className="rounded-lg border p-4 space-y-4">
-        <div className="flex items-center justify-between">
-          <h3 className="text-sm font-semibold">검색 필터</h3>
-          <Button
-            variant="ghost"
-            size="sm"
-            onClick={handleReset}
-            className="h-8"
-          >
-            <X className="h-4 w-4 mr-1" />
-            초기화
-          </Button>
-        </div>
+        {/* 검색 필터 */}
+        <div className="rounded-lg border p-4 space-y-4">
+          <div className="flex items-center justify-between">
+            <h3 className="text-sm font-semibold">검색 필터</h3>
+            <Button
+              variant="ghost"
+              size="sm"
+              onClick={handleReset}
+              className="h-8"
+            >
+              <X className="h-4 w-4 mr-1" />
+              초기화
+            </Button>
+          </div>
 
-        <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
-          {/* 프로젝트 번호 */}
-          <div className="space-y-2">
-            <Label htmlFor="projNo">프로젝트 번호</Label>
-            {projects.length > 0 ? (
-              <Popover open={projectSearchOpen} onOpenChange={setProjectSearchOpen}>
-                <PopoverTrigger asChild>
-                  <Button
-                    variant="outline"
-                    role="combobox"
-                    aria-expanded={projectSearchOpen}
-                    className="w-full justify-between"
-                  >
-                    {localFilters.projNo ? (
-                      <span>
-                        {projects.find((p) => p.PROJ_NO === localFilters.projNo)?.PROJ_NO || localFilters.projNo}
-                        {" ["}
-                        {projects.find((p) => p.PROJ_NO === localFilters.projNo)?.PROJ_NM}
-                        {"]"}
-                      </span>
-                    ) : (
-                      <span className="text-muted-foreground">프로젝트 선택</span>
-                    )}
-                    <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
-                  </Button>
-                </PopoverTrigger>
-                <PopoverContent className="w-[400px] p-0" align="start">
-                  <div className="p-2">
-                    <div className="flex items-center border rounded-md px-3">
-                      <Search className="h-4 w-4 mr-2 opacity-50" />
-                      <Input
-                        placeholder="프로젝트 번호 또는 이름으로 검색..."
-                        value={projectSearch}
-                        onChange={(e) => setProjectSearch(e.target.value)}
-                        className="border-0 focus-visible:ring-0 focus-visible:ring-offset-0"
-                      />
-                    </div>
-                  </div>
-                  <div className="max-h-[300px] overflow-y-auto">
-                    <div className="p-1">
-                      <Button
-                        variant="ghost"
-                        className="w-full justify-start font-normal"
-                        onClick={() => {
-                          setLocalFilters({ ...localFilters, projNo: undefined });
-                          setProjectSearchOpen(false);
-                          setProjectSearch("");
-                        }}
-                      >
-                        <Check
-                          className={cn(
-                            "mr-2 h-4 w-4",
-                            !localFilters.projNo ? "opacity-100" : "opacity-0"
-                          )}
-                        />
-                        전체
-                      </Button>
-                      {filteredProjects.map((proj) => (
-                        <Button
-                          key={proj.PROJ_NO}
-                          variant="ghost"
-                          className="w-full justify-start font-normal"
-                          onClick={() => {
-                            setLocalFilters({ ...localFilters, projNo: proj.PROJ_NO });
-                            setProjectSearchOpen(false);
-                            setProjectSearch("");
-                          }}
-                        >
-                          <Check
-                            className={cn(
-                              "mr-2 h-4 w-4",
-                              localFilters.projNo === proj.PROJ_NO ? "opacity-100" : "opacity-0"
-                            )}
-                          />
-                          <span className="font-mono text-sm">{proj.PROJ_NO} [{proj.PROJ_NM}]</span>
-                        </Button>
-                      ))}
-                      {filteredProjects.length === 0 && (
-                        <div className="py-6 text-center text-sm text-muted-foreground">
-                          검색 결과가 없습니다.
-                        </div>
+          <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
+            {/* 프로젝트 번호 */}
+            <div className="space-y-2">
+              <Label htmlFor="projNo">프로젝트 번호</Label>
+              {projects.length > 0 ? (
+                <Popover open={projectSearchOpen} onOpenChange={setProjectSearchOpen}>
+                  <PopoverTrigger asChild>
+                    <Button
+                      variant="outline"
+                      role="combobox"
+                      aria-expanded={projectSearchOpen}
+                      className="w-full justify-between"
+                    >
+                      {projNo ? (
+                        <span>
+                          {projects.find((p) => p.PROJ_NO === projNo)?.PROJ_NO || projNo}
+                          {" ["}
+                          {projects.find((p) => p.PROJ_NO === projNo)?.PROJ_NM}
+                          {"]"}
+                        </span>
+                      ) : (
+                        <span className="text-muted-foreground">프로젝트 선택</span>
                       )}
+                      <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
+                    </Button>
+                  </PopoverTrigger>
+                  <PopoverContent className="w-[400px] p-0" align="start">
+                    <div className="p-2">
+                      <div className="flex items-center border rounded-md px-3">
+                        <Search className="h-4 w-4 mr-2 opacity-50" />
+                        <Input
+                          placeholder="프로젝트 번호 또는 이름으로 검색..."
+                          value={projectSearch}
+                          onChange={(e) => setProjectSearch(e.target.value)}
+                          className="border-0 focus-visible:ring-0 focus-visible:ring-offset-0"
+                        />
+                      </div>
+                    </div>
+                    <div className="max-h-[300px] overflow-y-auto">
+                      <div className="p-1">
+                        {filteredProjects.map((proj) => (
+                          <Button
+                            key={proj.PROJ_NO}
+                            variant="ghost"
+                            className="w-full justify-start font-normal"
+                            onClick={() => {
+                              onProjNoChange(proj.PROJ_NO);
+                              setProjectSearchOpen(false);
+                              setProjectSearch("");
+                            }}
+                          >
+                            <Check
+                              className={cn(
+                                "mr-2 h-4 w-4",
+                                projNo === proj.PROJ_NO ? "opacity-100" : "opacity-0"
+                              )}
+                            />
+                            <span className="font-mono text-sm">{proj.PROJ_NO} [{proj.PROJ_NM || ""}]</span>
+                          </Button>
+                        ))}
+                        {filteredProjects.length === 0 && (
+                          <div className="py-6 text-center text-sm text-muted-foreground">
+                            검색 결과가 없습니다.
+                          </div>
+                        )}
+                      </div>
                     </div>
-                  </div>
-                </PopoverContent>
-              </Popover>
-            ) : (
+                  </PopoverContent>
+                </Popover>
+              ) : (
+                <Input
+                  id="projNo"
+                  placeholder="계약된 프로젝트가 없습니다"
+                  value={projNo}
+                  disabled
+                  className="bg-muted"
+                />
+              )}
+            </div>
+
+            {/* 문서 번호 */}
+            <div className="space-y-2">
+              <Label htmlFor="docNo">문서 번호</Label>
               <Input
-                id="projNo"
-                placeholder="계약된 프로젝트가 없습니다"
-                value={localFilters.projNo || ""}
+                id="docNo"
+                placeholder="문서 번호 검색"
+                value={localFilters.docNo || ""}
                 onChange={(e) =>
-                  setLocalFilters({ ...localFilters, projNo: e.target.value })
+                  setLocalFilters({ ...localFilters, docNo: e.target.value })
                 }
-                disabled
-                className="bg-muted"
               />
-            )}
-          </div>
-
-          {/* 문서 번호 */}
-          <div className="space-y-2">
-            <Label htmlFor="docNo">문서 번호</Label>
-            <Input
-              id="docNo"
-              placeholder="문서 번호 검색"
-              value={localFilters.docNo || ""}
-              onChange={(e) =>
-                setLocalFilters({ ...localFilters, docNo: e.target.value })
-              }
-            />
-          </div>
+            </div>
 
-          {/* 문서 제목 */}
-          <div className="space-y-2">
-            <Label htmlFor="docTitle">문서 제목</Label>
-            <Input
-              id="docTitle"
-              placeholder="제목 검색"
-              value={localFilters.docTitle || ""}
-              onChange={(e) =>
-                setLocalFilters({ ...localFilters, docTitle: e.target.value })
-              }
-            />
-          </div>
+            {/* 문서 제목 */}
+            <div className="space-y-2">
+              <Label htmlFor="docTitle">문서 제목</Label>
+              <Input
+                id="docTitle"
+                placeholder="제목 검색"
+                value={localFilters.docTitle || ""}
+                onChange={(e) =>
+                  setLocalFilters({ ...localFilters, docTitle: e.target.value })
+                }
+              />
+            </div>
 
-          {/* 패키지 번호 */}
-          <div className="space-y-2">
-            <Label htmlFor="pkgNo">패키지</Label>
-            <Input
-              id="pkgNo"
-              placeholder="패키지 번호"
-              value={localFilters.pkgNo || ""}
-              onChange={(e) =>
-                setLocalFilters({ ...localFilters, pkgNo: e.target.value })
-              }
-            />
-          </div>
+            {/* 패키지 번호 */}
+            <div className="space-y-2">
+              <Label htmlFor="pkgNo">패키지</Label>
+              <Input
+                id="pkgNo"
+                placeholder="패키지 번호"
+                value={localFilters.pkgNo || ""}
+                onChange={(e) =>
+                  setLocalFilters({ ...localFilters, pkgNo: e.target.value })
+                }
+              />
+            </div>
 
-          {/* 업체 코드 */}
-          <div className="space-y-2">
-            <Label htmlFor="vndrCd">업체 코드</Label>
-            <Input
-              id="vndrCd"
-              placeholder="업체 코드"
-              value={vendorCode || localFilters.vndrCd || ""}
-              onChange={(e) =>
-                setLocalFilters({ ...localFilters, vndrCd: e.target.value })
-              }
-              disabled={!!vendorCode} // 벤더 코드가 제공되면 입력 비활성화
-              className={vendorCode ? "bg-muted" : ""}
-            />
+            {/* 스테이지 */}
+            <div className="space-y-2">
+              <Label htmlFor="stage">스테이지</Label>
+              <Input
+                id="stage"
+                placeholder="스테이지 입력 (예: IFC, IFA)"
+                value={localFilters.stage || ""}
+                onChange={(e) =>
+                  setLocalFilters({ ...localFilters, stage: e.target.value })
+                }
+              />
+            </div>
           </div>
 
-          {/* 스테이지 */}
-          <div className="space-y-2">
-            <Label htmlFor="stage">스테이지</Label>
-            <Input
-              id="stage"
-              placeholder="스테이지 입력"
-              value={localFilters.stage || ""}
-              onChange={(e) =>
-                setLocalFilters({ ...localFilters, stage: e.target.value })
-              }
-            />
+          <div className="flex justify-end">
+            <Button onClick={handleSearch} size="sm">
+              <Search className="h-4 w-4 mr-2" />
+              검색
+            </Button>
           </div>
         </div>
-
-        <div className="flex justify-end">
-          <Button onClick={handleSearch} size="sm">
-            <Search className="h-4 w-4 mr-2" />
-            검색
-          </Button>
-        </div>
-        </div>
       </div>
     </>
   );
 }
-
-- 
cgit v1.2.3

