diff options
Diffstat (limited to 'lib/project-gtc/table/add-project-dialog.tsx')
| -rw-r--r-- | lib/project-gtc/table/add-project-dialog.tsx | 296 |
1 files changed, 296 insertions, 0 deletions
diff --git a/lib/project-gtc/table/add-project-dialog.tsx b/lib/project-gtc/table/add-project-dialog.tsx new file mode 100644 index 00000000..616ab950 --- /dev/null +++ b/lib/project-gtc/table/add-project-dialog.tsx @@ -0,0 +1,296 @@ +"use client" + +import * as React from "react" +import { useForm } from "react-hook-form" +import { zodResolver } from "@hookform/resolvers/zod" +import { z } from "zod" +import { toast } from "sonner" +import { Upload, X } from "lucide-react" + +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" +import { ProjectSelector } from "@/components/ProjectSelector" +import { uploadProjectGtcFile, getProjectsWithGtcFiles } from "../service" +import { type Project } from "@/lib/rfqs/service" + +const addProjectSchema = z.object({ + projectId: z.number().min(1, "프로젝트 선택은 필수입니다."), + gtcFile: z.instanceof(File, { message: "GTC 파일은 필수입니다." }).optional(), +}) + +type AddProjectFormValues = z.infer<typeof addProjectSchema> + +interface AddProjectDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + onSuccess?: () => void +} + +export function AddProjectDialog({ + open, + onOpenChange, + onSuccess, +}: AddProjectDialogProps) { + const [isLoading, setIsLoading] = React.useState(false) + const [selectedProject, setSelectedProject] = React.useState<Project | null>(null) + const [selectedFile, setSelectedFile] = React.useState<File | null>(null) + const [excludedProjectIds, setExcludedProjectIds] = React.useState<number[]>([]) + + const form = useForm<AddProjectFormValues>({ + resolver: zodResolver(addProjectSchema), + defaultValues: { + projectId: 0, + gtcFile: undefined, + }, + }) + + // 이미 GTC 파일이 등록된 프로젝트 ID 목록 로드 + React.useEffect(() => { + async function loadExcludedProjects() { + try { + const excludedIds = await getProjectsWithGtcFiles(); + setExcludedProjectIds(excludedIds); + } catch (error) { + console.error("제외할 프로젝트 목록 로드 오류:", error); + } + } + + if (open) { + loadExcludedProjects(); + } + }, [open]); + + // 프로젝트 선택 시 폼에 자동으로 채우기 + const handleProjectSelect = (project: Project) => { + // 이미 GTC 파일이 등록된 프로젝트인지 확인 + if (excludedProjectIds.includes(project.id)) { + toast.error("이미 GTC 파일이 등록된 프로젝트입니다."); + // 선택된 프로젝트 정보 초기화 + setSelectedProject(null); + form.setValue("projectId", 0); + return; + } + + setSelectedProject(project) + form.setValue("projectId", project.id) + } + + // 파일 선택 처리 + const handleFileSelect = (event: React.ChangeEvent<HTMLInputElement>) => { + const file = event.target.files?.[0] + if (file) { + // PDF 파일만 허용 + if (file.type !== 'application/pdf') { + toast.error("PDF 파일만 업로드 가능합니다.") + return + } + + setSelectedFile(file) + form.setValue("gtcFile", file) + } + } + + // 파일 제거 + const handleRemoveFile = () => { + setSelectedFile(null) + form.setValue("gtcFile", undefined) + // input 요소의 value도 초기화 + const fileInput = document.getElementById('gtc-file-input') as HTMLInputElement + if (fileInput) { + fileInput.value = '' + } + } + + const onSubmit = async (data: AddProjectFormValues) => { + // 프로젝트가 선택되지 않았으면 에러 + if (!selectedProject) { + toast.error("프로젝트를 선택해주세요.") + return + } + + // 이미 GTC 파일이 등록된 프로젝트인지 다시 한번 확인 + if (excludedProjectIds.includes(selectedProject.id)) { + toast.error("이미 GTC 파일이 등록된 프로젝트입니다.") + return + } + + // GTC 파일이 없으면 에러 + if (!data.gtcFile) { + toast.error("GTC 파일은 필수입니다.") + return + } + + setIsLoading(true) + try { + // GTC 파일 업로드 + const fileResult = await uploadProjectGtcFile(selectedProject.id, data.gtcFile) + + if (!fileResult.success) { + toast.error(fileResult.error || "GTC 파일 업로드에 실패했습니다.") + return + } + + toast.success("GTC 파일이 성공적으로 업로드되었습니다.") + form.reset() + setSelectedProject(null) + setSelectedFile(null) + onOpenChange(false) + onSuccess?.() + } catch (error) { + console.error("GTC 파일 업로드 오류:", error) + toast.error("GTC 파일 업로드 중 오류가 발생했습니다.") + } finally { + setIsLoading(false) + } + } + + const handleOpenChange = (newOpen: boolean) => { + if (!newOpen) { + form.reset() + setSelectedProject(null) + setSelectedFile(null) + } + onOpenChange(newOpen) + } + + return ( + <Dialog open={open} onOpenChange={handleOpenChange}> + <DialogContent className="sm:max-w-[500px]"> + <DialogHeader> + <DialogTitle>GTC 파일 추가</DialogTitle> + <DialogDescription> + 기존 프로젝트를 선택하고 GTC 파일을 업로드합니다. (이미 GTC 파일이 등록된 프로젝트는 제외됩니다) + </DialogDescription> + </DialogHeader> + + <Form {...form}> + <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4"> + {/* 프로젝트 선택 (필수) */} + <FormField + control={form.control} + name="projectId" + render={() => ( + <FormItem> + <FormLabel>프로젝트 선택 *</FormLabel> + <FormControl> + <ProjectSelector + selectedProjectId={selectedProject?.id} + onProjectSelect={handleProjectSelect} + placeholder="프로젝트를 선택하세요..." + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + {/* 선택된 프로젝트 정보 표시 (읽기 전용) */} + {selectedProject && ( + <div className="p-4 bg-muted rounded-lg space-y-2"> + <h4 className="font-medium text-sm">선택된 프로젝트 정보</h4> + <div className="space-y-1 text-sm"> + <div className="flex justify-between"> + <span className="text-muted-foreground">프로젝트 코드:</span> + <span className="font-medium">{selectedProject.projectCode}</span> + </div> + <div className="flex justify-between"> + <span className="text-muted-foreground">프로젝트명:</span> + <span className="font-medium">{selectedProject.projectName}</span> + </div> + </div> + </div> + )} + + {/* GTC 파일 업로드 */} + <FormField + control={form.control} + name="gtcFile" + render={() => ( + <FormItem> + <FormLabel>GTC 파일 *</FormLabel> + <div className="space-y-2"> + {!selectedFile ? ( + <div className="flex items-center justify-center w-full"> + <label + htmlFor="gtc-file-input" + className="flex flex-col items-center justify-center w-full h-32 border-2 border-dashed border-gray-300 rounded-lg cursor-pointer bg-gray-50 hover:bg-gray-100" + > + <div className="flex flex-col items-center justify-center pt-5 pb-6"> + <Upload className="w-8 h-8 mb-4 text-gray-500" /> + <p className="mb-2 text-sm text-gray-500"> + <span className="font-semibold">클릭하여 파일 선택</span> 또는 드래그 앤 드롭 + </p> + <p className="text-xs text-gray-500"> + PDF 파일만 + </p> + </div> + <input + id="gtc-file-input" + type="file" + className="hidden" + accept=".pdf" + onChange={handleFileSelect} + disabled={isLoading} + /> + </label> + </div> + ) : ( + <div className="flex items-center justify-between p-3 border rounded-lg bg-gray-50"> + <div className="flex items-center space-x-2"> + <Upload className="w-4 h-4 text-gray-500" /> + <span className="text-sm font-medium">{selectedFile.name}</span> + <span className="text-xs text-gray-500"> + ({(selectedFile.size / 1024 / 1024).toFixed(2)} MB) + </span> + </div> + <Button + type="button" + variant="ghost" + size="sm" + onClick={handleRemoveFile} + disabled={isLoading} + > + <X className="w-4 h-4" /> + </Button> + </div> + )} + </div> + <FormMessage /> + </FormItem> + )} + /> + + <DialogFooter> + <Button + type="button" + variant="outline" + onClick={() => handleOpenChange(false)} + disabled={isLoading} + > + 취소 + </Button> + <Button type="submit" disabled={isLoading || !selectedProject}> + {isLoading ? "업로드 중..." : "GTC 파일 업로드"} + </Button> + </DialogFooter> + </form> + </Form> + </DialogContent> + </Dialog> + ) +}
\ No newline at end of file |
