summaryrefslogtreecommitdiff
path: root/lib/swp/table/swp-table-toolbar.tsx
diff options
context:
space:
mode:
authorjoonhoekim <26rote@gmail.com>2025-10-29 15:59:04 +0900
committerjoonhoekim <26rote@gmail.com>2025-10-29 15:59:04 +0900
commit2ecdac866c19abea0b5389708fcdf5b3889c969a (patch)
treee02a02cfa0890691fb28a7df3a96ef495b3d4b79 /lib/swp/table/swp-table-toolbar.tsx
parent2fc9e5492e220041ba322d9a1479feb7803228cf (diff)
(김준회) SWP 파일 업로드 취소 기능 추가, 업로드 파일명 검증로직에서 파일명 비필수로 변경
Diffstat (limited to 'lib/swp/table/swp-table-toolbar.tsx')
-rw-r--r--lib/swp/table/swp-table-toolbar.tsx634
1 files changed, 338 insertions, 296 deletions
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>
</>
);
}
-