diff options
Diffstat (limited to 'lib/vendor-investigation/table/update-investigation-sheet.tsx')
| -rw-r--r-- | lib/vendor-investigation/table/update-investigation-sheet.tsx | 713 |
1 files changed, 562 insertions, 151 deletions
diff --git a/lib/vendor-investigation/table/update-investigation-sheet.tsx b/lib/vendor-investigation/table/update-investigation-sheet.tsx index fe30c892..69f0d9ae 100644 --- a/lib/vendor-investigation/table/update-investigation-sheet.tsx +++ b/lib/vendor-investigation/table/update-investigation-sheet.tsx @@ -3,7 +3,8 @@ import * as React from "react" import { zodResolver } from "@hookform/resolvers/zod" import { useForm } from "react-hook-form" -import { Loader } from "lucide-react" +import { CalendarIcon, Loader } from "lucide-react" +import { format } from "date-fns" import { toast } from "sonner" import { Button } from "@/components/ui/button" @@ -16,6 +17,7 @@ import { FormMessage, } from "@/components/ui/form" import { Input } from "@/components/ui/input" +import { Textarea } from "@/components/ui/textarea" import { Sheet, SheetClose, @@ -33,33 +35,76 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select" +import { Calendar } from "@/components/ui/calendar" +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover" +import { + Dropzone, + DropzoneZone, + DropzoneUploadIcon, + DropzoneTitle, + DropzoneDescription, + DropzoneInput +} from "@/components/ui/dropzone" import { updateVendorInvestigationSchema, type UpdateVendorInvestigationSchema, } from "../validations" -import { updateVendorInvestigationAction } from "../service" +import { updateVendorInvestigationAction, getInvestigationAttachments, deleteInvestigationAttachment } from "../service" import { VendorInvestigationsViewWithContacts } from "@/config/vendorInvestigationsColumnsConfig" -/** - * The shape of `vendorInvestigation` - * might come from your `vendorInvestigationsView` row - * or your existing type for a single investigation. - */ - interface UpdateVendorInvestigationSheetProps extends React.ComponentPropsWithoutRef<typeof Sheet> { investigation: VendorInvestigationsViewWithContacts | null } +// 첨부파일 정책 정의 +const getFileUploadConfig = (status: string) => { + // 취소된 상태에서만 파일 업로드 비활성화 + if (status === "CANCELED") { + return { + enabled: false, + label: "", + description: "", + accept: undefined, // undefined로 변경 + maxSize: 0, + maxSizeText: "" + } + } + + // 모든 활성 상태에서 동일한 정책 적용 + return { + enabled: true, + label: "실사 관련 첨부파일", + description: "실사와 관련된 모든 문서와 이미지를 첨부할 수 있습니다.", + accept: { + 'application/pdf': ['.pdf'], + 'application/msword': ['.doc'], + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': ['.docx'], + 'application/vnd.ms-excel': ['.xls'], + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': ['.xlsx'], + 'image/*': ['.png', '.jpg', '.jpeg', '.gif'], + }, + maxSize: 10 * 1024 * 1024, // 10MB + maxSizeText: "10MB" + } +} + /** - * A sheet for updating a vendor investigation (plus optional attachments). + * 실사 정보 수정 시트 */ export function UpdateVendorInvestigationSheet({ investigation, ...props }: UpdateVendorInvestigationSheetProps) { const [isPending, startTransition] = React.useTransition() + const [existingAttachments, setExistingAttachments] = React.useState<any[]>([]) + const [loadingAttachments, setLoadingAttachments] = React.useState(false) + const [uploadingFiles, setUploadingFiles] = React.useState(false) // RHF + Zod const form = useForm<UpdateVendorInvestigationSchema>({ @@ -67,138 +112,346 @@ export function UpdateVendorInvestigationSheet({ defaultValues: { investigationId: investigation?.investigationId ?? 0, investigationStatus: investigation?.investigationStatus ?? "PLANNED", - scheduledStartAt: investigation?.scheduledStartAt ?? undefined, - scheduledEndAt: investigation?.scheduledEndAt ?? undefined, + evaluationType: investigation?.evaluationType ?? undefined, + investigationAddress: investigation?.investigationAddress ?? "", + investigationMethod: investigation?.investigationMethod ?? "", + forecastedAt: investigation?.forecastedAt ?? undefined, + requestedAt: investigation?.requestedAt ?? undefined, + confirmedAt: investigation?.confirmedAt ?? undefined, completedAt: investigation?.completedAt ?? undefined, + evaluationScore: investigation?.evaluationScore ?? undefined, + evaluationResult: investigation?.evaluationResult ?? undefined, investigationNotes: investigation?.investigationNotes ?? "", + attachments: undefined, // 파일은 매번 새로 업로드 }, }) + // investigation이 변경될 때마다 폼 리셋 React.useEffect(() => { if (investigation) { form.reset({ investigationId: investigation.investigationId, investigationStatus: investigation.investigationStatus || "PLANNED", - scheduledStartAt: investigation.scheduledStartAt ?? undefined, - scheduledEndAt: investigation.scheduledEndAt ?? undefined, + evaluationType: investigation.evaluationType ?? undefined, + investigationAddress: investigation.investigationAddress ?? "", + investigationMethod: investigation.investigationMethod ?? "", + forecastedAt: investigation.forecastedAt ?? undefined, + requestedAt: investigation.requestedAt ?? undefined, + confirmedAt: investigation.confirmedAt ?? undefined, completedAt: investigation.completedAt ?? undefined, + evaluationScore: investigation.evaluationScore ?? undefined, + evaluationResult: investigation.evaluationResult ?? undefined, investigationNotes: investigation.investigationNotes ?? "", + attachments: undefined, // 파일은 매번 새로 업로드 }) + + // 기존 첨부파일 로드 + loadExistingAttachments(investigation.investigationId) } }, [investigation, form]) - // Format date for form data - const formatDateForFormData = (date: Date | undefined): string | null => { - if (!date) return null; - return date.toISOString(); + // 기존 첨부파일 로드 함수 + const loadExistingAttachments = async (investigationId: number) => { + setLoadingAttachments(true) + try { + const result = await getInvestigationAttachments(investigationId) + if (result.success) { + setExistingAttachments(result.attachments || []) + } else { + toast.error("첨부파일 목록을 불러오는데 실패했습니다.") + } + } catch (error) { + console.error("첨부파일 로드 실패:", error) + toast.error("첨부파일 목록을 불러오는 중 오류가 발생했습니다.") + } finally { + setLoadingAttachments(false) + } } - // Submit handler - async function onSubmit(values: UpdateVendorInvestigationSchema) { - if (!values.investigationId) return - - startTransition(async () => { - // 1) Build a FormData object for the server action - const formData = new FormData() - - // Add text fields - formData.append("investigationId", String(values.investigationId)) - formData.append("investigationStatus", values.investigationStatus) - - // Format dates properly before appending to FormData - if (values.scheduledStartAt) { - const formattedDate = formatDateForFormData(values.scheduledStartAt) - if (formattedDate) formData.append("scheduledStartAt", formattedDate) - } + // 첨부파일 삭제 함수 + const handleDeleteAttachment = async (attachmentId: number) => { + if (!investigation) return + + try { + const response = await fetch(`/api/vendor-investigations/${investigation.investigationId}/attachments?attachmentId=${attachmentId}`, { + method: "DELETE", + }) - if (values.scheduledEndAt) { - const formattedDate = formatDateForFormData(values.scheduledEndAt) - if (formattedDate) formData.append("scheduledEndAt", formattedDate) + if (!response.ok) { + const errorData = await response.json() + throw new Error(errorData.error || "첨부파일 삭제 실패") } - if (values.completedAt) { - const formattedDate = formatDateForFormData(values.completedAt) - if (formattedDate) formData.append("completedAt", formattedDate) - } + toast.success("첨부파일이 삭제되었습니다.") + // 목록 새로고침 + loadExistingAttachments(investigation.investigationId) - if (values.investigationNotes) { - formData.append("investigationNotes", values.investigationNotes) - } + } catch (error) { + console.error("첨부파일 삭제 오류:", error) + toast.error(error instanceof Error ? error.message : "첨부파일 삭제 중 오류가 발생했습니다.") + } + } - // Add attachments (if any) - // Note: If you have multiple files in "attachments", we store them in the form under the same key. - const attachmentValue = form.getValues("attachments"); - if (attachmentValue instanceof FileList) { - for (let i = 0; i < attachmentValue.length; i++) { - formData.append("attachments", attachmentValue[i]); - } - } + // 파일 업로드 섹션 렌더링 + const renderFileUploadSection = () => { + const currentStatus = form.watch("investigationStatus") + const config = getFileUploadConfig(currentStatus) + + if (!config.enabled) return null - const { error } = await updateVendorInvestigationAction(formData) - if (error) { - toast.error(error) - return - } + return ( + <> + {/* 기존 첨부파일 목록 */} + {(existingAttachments.length > 0 || loadingAttachments) && ( + <div className="space-y-2"> + <FormLabel>기존 첨부파일</FormLabel> + <div className="border rounded-md p-3 space-y-2 max-h-32 overflow-y-auto"> + {loadingAttachments ? ( + <div className="flex items-center justify-center py-4"> + <Loader className="h-4 w-4 animate-spin" /> + <span className="ml-2 text-sm text-muted-foreground"> + 첨부파일 로딩 중... + </span> + </div> + ) : existingAttachments.length > 0 ? ( + existingAttachments.map((attachment) => ( + <div key={attachment.id} className="flex items-center justify-between text-sm"> + <div className="flex items-center space-x-2 flex-1 min-w-0"> + <span className="text-xs px-2 py-1 bg-muted rounded"> + {attachment.attachmentType} + </span> + <span className="truncate">{attachment.fileName}</span> + <span className="text-muted-foreground"> + ({Math.round(attachment.fileSize / 1024)}KB) + </span> + </div> + <Button + type="button" + variant="ghost" + size="sm" + onClick={() => handleDeleteAttachment(attachment.id)} + className="text-destructive hover:text-destructive" + disabled={isPending} + > + 삭제 + </Button> + </div> + )) + ) : ( + <div className="text-sm text-muted-foreground text-center py-2"> + 첨부된 파일이 없습니다. + </div> + )} + </div> + </div> + )} - toast.success("Investigation updated!") - form.reset() - props.onOpenChange?.(false) - }) + {/* 새 파일 업로드 */} + <FormField + control={form.control} + name="attachments" + render={({ field: { onChange, ...field } }) => ( + <FormItem> + <FormLabel>{config.label}</FormLabel> + <FormControl> + <Dropzone + onDrop={(acceptedFiles, rejectedFiles) => { + // 거부된 파일에 대한 상세 에러 메시지 + if (rejectedFiles.length > 0) { + rejectedFiles.forEach((file) => { + const error = file.errors[0] + if (error.code === 'file-too-large') { + toast.error(`${file.file.name}: 파일 크기가 ${config.maxSizeText}를 초과합니다.`) + } else if (error.code === 'file-invalid-type') { + toast.error(`${file.file.name}: 지원하지 않는 파일 형식입니다.`) + } else { + toast.error(`${file.file.name}: 파일 업로드에 실패했습니다.`) + } + }) + } + + if (acceptedFiles.length > 0) { + onChange(acceptedFiles) + toast.success(`${acceptedFiles.length}개 파일이 선택되었습니다.`) + } + }} + accept={config.accept} + multiple + maxSize={config.maxSize} + disabled={isPending || uploadingFiles} + > + <DropzoneZone> + <DropzoneUploadIcon /> + <DropzoneTitle> + {isPending || uploadingFiles + ? "파일 업로드 중..." + : "파일을 드래그하거나 클릭하여 업로드" + } + </DropzoneTitle> + <DropzoneDescription> + {config.description} (최대 {config.maxSizeText}) + </DropzoneDescription> + <DropzoneInput /> + </DropzoneZone> + </Dropzone> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + </> + ) } - // Format date value for input field - const formatDateForInput = (date: Date | undefined): string => { - if (!date) return ""; - return date instanceof Date ? date.toISOString().slice(0, 10) : ""; + // 파일 업로드 함수 + const uploadFiles = async (files: File[], investigationId: number) => { + const uploadPromises = files.map(async (file) => { + const formData = new FormData() + formData.append("file", file) + + const response = await fetch(`/api/vendor-investigations/${investigationId}/attachments`, { + method: "POST", + body: formData, + }) + + if (!response.ok) { + const errorData = await response.json() + throw new Error(errorData.error || "파일 업로드 실패") + } + + return await response.json() + }) + + return await Promise.all(uploadPromises) } - // Handle date input change - const handleDateChange = (e: React.ChangeEvent<HTMLInputElement>, onChange: (...event: any[]) => void) => { - const val = e.target.value; - if (val) { - // Ensure proper date handling by setting to noon to avoid timezone issues - const newDate = new Date(`${val}T12:00:00`); - onChange(newDate); - } else { - onChange(undefined); - } + // Submit handler + async function onSubmit(values: UpdateVendorInvestigationSchema) { + if (!values.investigationId) return + + startTransition(async () => { + try { + // 1) 먼저 텍스트 데이터 업데이트 + const formData = new FormData() + + // 필수 필드 + formData.append("investigationId", String(values.investigationId)) + formData.append("investigationStatus", values.investigationStatus) + + // 선택적 필드들 + if (values.evaluationType) { + formData.append("evaluationType", values.evaluationType) + } + + if (values.investigationAddress) { + formData.append("investigationAddress", values.investigationAddress) + } + + if (values.investigationMethod) { + formData.append("investigationMethod", values.investigationMethod) + } + + if (values.forecastedAt) { + formData.append("forecastedAt", values.forecastedAt.toISOString()) + } + + if (values.requestedAt) { + formData.append("requestedAt", values.requestedAt.toISOString()) + } + + if (values.confirmedAt) { + formData.append("confirmedAt", values.confirmedAt.toISOString()) + } + + if (values.completedAt) { + formData.append("completedAt", values.completedAt.toISOString()) + } + + if (values.evaluationScore !== undefined) { + formData.append("evaluationScore", String(values.evaluationScore)) + } + + if (values.evaluationResult) { + formData.append("evaluationResult", values.evaluationResult) + } + + if (values.investigationNotes) { + formData.append("investigationNotes", values.investigationNotes) + } + + // 텍스트 데이터 업데이트 + const { error } = await updateVendorInvestigationAction(formData) + + if (error) { + toast.error(error) + return + } + + // 2) 파일이 있으면 업로드 + if (values.attachments && values.attachments.length > 0) { + setUploadingFiles(true) + + try { + await uploadFiles(values.attachments, values.investigationId) + toast.success(`실사 정보와 ${values.attachments.length}개 파일이 업데이트되었습니다!`) + + // 첨부파일 목록 새로고침 + loadExistingAttachments(values.investigationId) + } catch (fileError) { + toast.error(`데이터는 저장되었지만 파일 업로드 중 오류가 발생했습니다: ${fileError}`) + } finally { + setUploadingFiles(false) + } + } else { + toast.success("실사 정보가 업데이트되었습니다!") + } + + form.reset() + props.onOpenChange?.(false) + + } catch (error) { + console.error("실사 정보 업데이트 오류:", error) + toast.error("실사 정보 업데이트 중 오류가 발생했습니다.") + } + }) } return ( <Sheet {...props}> - <SheetContent className="flex flex-col gap-6 sm:max-w-md"> - <SheetHeader className="text-left"> - <SheetTitle>Update Investigation</SheetTitle> + <SheetContent className="flex flex-col h-full sm:max-w-md"> + <SheetHeader className="text-left flex-shrink-0"> + <SheetTitle>실사 업데이트</SheetTitle> <SheetDescription> - Change the investigation details & attachments + {investigation?.vendorName && ( + <span className="font-medium">{investigation.vendorName}</span> + )}의 실사 정보를 수정합니다. </SheetDescription> </SheetHeader> - <Form {...form}> - <form - onSubmit={form.handleSubmit(onSubmit)} - className="flex flex-col gap-4" - // Must use multipart to support file uploads - encType="multipart/form-data" - > - {/* investigationStatus */} + <div className="flex-1 overflow-y-auto py-4"> + <Form {...form}> + <form + onSubmit={form.handleSubmit(onSubmit)} + className="flex flex-col gap-4" + > + {/* 실사 상태 */} <FormField control={form.control} name="investigationStatus" render={({ field }) => ( <FormItem> - <FormLabel>Status</FormLabel> + <FormLabel>실사 상태</FormLabel> <FormControl> <Select value={field.value} onValueChange={field.onChange}> - <SelectTrigger className="capitalize"> - <SelectValue placeholder="Select a status" /> + <SelectTrigger> + <SelectValue placeholder="상태를 선택하세요" /> </SelectTrigger> <SelectContent> <SelectGroup> - <SelectItem value="PLANNED">PLANNED</SelectItem> - <SelectItem value="IN_PROGRESS">IN_PROGRESS</SelectItem> - <SelectItem value="COMPLETED">COMPLETED</SelectItem> - <SelectItem value="CANCELED">CANCELED</SelectItem> + <SelectItem value="PLANNED">계획됨</SelectItem> + <SelectItem value="IN_PROGRESS">진행 중</SelectItem> + <SelectItem value="COMPLETED">완료됨</SelectItem> + <SelectItem value="CANCELED">취소됨</SelectItem> </SelectGroup> </SelectContent> </Select> @@ -208,37 +461,43 @@ export function UpdateVendorInvestigationSheet({ )} /> - {/* scheduledStartAt */} + {/* 평가 유형 */} <FormField control={form.control} - name="scheduledStartAt" + name="evaluationType" render={({ field }) => ( <FormItem> - <FormLabel>Scheduled Start</FormLabel> + <FormLabel>평가 유형</FormLabel> <FormControl> - <Input - type="date" - value={formatDateForInput(field.value)} - onChange={(e) => handleDateChange(e, field.onChange)} - /> + <Select value={field.value || ""} onValueChange={field.onChange}> + <SelectTrigger> + <SelectValue placeholder="평가 유형을 선택하세요" /> + </SelectTrigger> + <SelectContent> + <SelectGroup> + <SelectItem value="SITE_AUDIT">실사의뢰평가</SelectItem> + <SelectItem value="QM_SELF_AUDIT">QM자체평가</SelectItem> + </SelectGroup> + </SelectContent> + </Select> </FormControl> <FormMessage /> </FormItem> )} /> - {/* scheduledEndAt */} + {/* 실사 주소 */} <FormField control={form.control} - name="scheduledEndAt" + name="investigationAddress" render={({ field }) => ( <FormItem> - <FormLabel>Scheduled End</FormLabel> + <FormLabel>실사 주소</FormLabel> <FormControl> - <Input - type="date" - value={formatDateForInput(field.value)} - onChange={(e) => handleDateChange(e, field.onChange)} + <Textarea + placeholder="실사가 진행될 주소를 입력하세요..." + {...field} + className="min-h-[60px]" /> </FormControl> <FormMessage /> @@ -246,55 +505,200 @@ export function UpdateVendorInvestigationSheet({ )} /> - {/* completedAt */} + {/* 실사 방법 */} <FormField control={form.control} - name="completedAt" + name="investigationMethod" render={({ field }) => ( <FormItem> - <FormLabel>Completed At</FormLabel> + <FormLabel>실사 방법</FormLabel> <FormControl> - <Input - type="date" - value={formatDateForInput(field.value)} - onChange={(e) => handleDateChange(e, field.onChange)} - /> + <Input placeholder="실사 방법을 입력하세요..." {...field} /> </FormControl> <FormMessage /> </FormItem> )} /> - {/* investigationNotes */} + {/* 실사 예정일 */} <FormField control={form.control} - name="investigationNotes" + name="forecastedAt" render={({ field }) => ( - <FormItem> - <FormLabel>Notes</FormLabel> - <FormControl> - <Input placeholder="Notes about the investigation..." {...field} /> - </FormControl> + <FormItem className="flex flex-col"> + <FormLabel>실사 예정일</FormLabel> + <Popover> + <PopoverTrigger asChild> + <FormControl> + <Button + variant="outline" + className={`w-full pl-3 text-left font-normal ${!field.value && "text-muted-foreground"}`} + > + {field.value ? ( + format(field.value, "yyyy년 MM월 dd일") + ) : ( + <span>날짜를 선택하세요</span> + )} + <CalendarIcon className="ml-auto h-4 w-4 opacity-50" /> + </Button> + </FormControl> + </PopoverTrigger> + <PopoverContent className="w-auto p-0" align="start"> + <Calendar + mode="single" + selected={field.value} + onSelect={field.onChange} + initialFocus + /> + </PopoverContent> + </Popover> <FormMessage /> </FormItem> )} /> - {/* attachments: multiple file upload */} + {/* 실사 확정일 */} <FormField control={form.control} - name="attachments" - render={({ field: { value, onChange, ...fieldProps } }) => ( + name="confirmedAt" + render={({ field }) => ( + <FormItem className="flex flex-col"> + <FormLabel>실사 확정일</FormLabel> + <Popover> + <PopoverTrigger asChild> + <FormControl> + <Button + variant="outline" + className={`w-full pl-3 text-left font-normal ${!field.value && "text-muted-foreground"}`} + > + {field.value ? ( + format(field.value, "yyyy년 MM월 dd일") + ) : ( + <span>날짜를 선택하세요</span> + )} + <CalendarIcon className="ml-auto h-4 w-4 opacity-50" /> + </Button> + </FormControl> + </PopoverTrigger> + <PopoverContent className="w-auto p-0" align="start"> + <Calendar + mode="single" + selected={field.value} + onSelect={field.onChange} + initialFocus + /> + </PopoverContent> + </Popover> + <FormMessage /> + </FormItem> + )} + /> + + {/* 실제 실사일 */} + <FormField + control={form.control} + name="completedAt" + render={({ field }) => ( + <FormItem className="flex flex-col"> + <FormLabel>실제 실사일</FormLabel> + <Popover> + <PopoverTrigger asChild> + <FormControl> + <Button + variant="outline" + className={`w-full pl-3 text-left font-normal ${!field.value && "text-muted-foreground"}`} + > + {field.value ? ( + format(field.value, "yyyy년 MM월 dd일") + ) : ( + <span>날짜를 선택하세요</span> + )} + <CalendarIcon className="ml-auto h-4 w-4 opacity-50" /> + </Button> + </FormControl> + </PopoverTrigger> + <PopoverContent className="w-auto p-0" align="start"> + <Calendar + mode="single" + selected={field.value} + onSelect={field.onChange} + initialFocus + /> + </PopoverContent> + </Popover> + <FormMessage /> + </FormItem> + )} + /> + + {/* 평가 점수 - 완료된 상태일 때만 표시 */} + {form.watch("investigationStatus") === "COMPLETED" && ( + <FormField + control={form.control} + name="evaluationScore" + render={({ field }) => ( + <FormItem> + <FormLabel>평가 점수</FormLabel> + <FormControl> + <Input + type="number" + min={0} + max={100} + placeholder="0-100점" + {...field} + value={field.value || ""} + onChange={(e) => { + const value = e.target.value === "" ? undefined : parseInt(e.target.value, 10) + field.onChange(value) + }} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + )} + + {/* 평가 결과 - 완료된 상태일 때만 표시 */} + {form.watch("investigationStatus") === "COMPLETED" && ( + <FormField + control={form.control} + name="evaluationResult" + render={({ field }) => ( + <FormItem> + <FormLabel>평가 결과</FormLabel> + <FormControl> + <Select value={field.value || ""} onValueChange={field.onChange}> + <SelectTrigger> + <SelectValue placeholder="평가 결과를 선택하세요" /> + </SelectTrigger> + <SelectContent> + <SelectGroup> + <SelectItem value="APPROVED">승인</SelectItem> + <SelectItem value="SUPPLEMENT">보완</SelectItem> + <SelectItem value="REJECTED">불가</SelectItem> + </SelectGroup> + </SelectContent> + </Select> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + )} + + {/* QM 의견 */} + <FormField + control={form.control} + name="investigationNotes" + render={({ field }) => ( <FormItem> - <FormLabel>Attachments</FormLabel> + <FormLabel>QM 의견</FormLabel> <FormControl> - <Input - type="file" - multiple - onChange={(e) => { - onChange(e.target.files); // Store the FileList directly - }} - {...fieldProps} + <Textarea + placeholder="실사에 대한 QM 의견을 입력하세요..." + {...field} + className="min-h-[80px]" /> </FormControl> <FormMessage /> @@ -302,22 +706,29 @@ export function UpdateVendorInvestigationSheet({ )} /> - {/* Footer Buttons */} - <SheetFooter className="gap-2 pt-2 sm:space-x-0"> - <SheetClose asChild> - <Button type="button" variant="outline"> - Cancel - </Button> - </SheetClose> - <Button disabled={isPending}> - {isPending && ( - <Loader className="mr-2 h-4 w-4 animate-spin" aria-hidden="true" /> - )} - Save - </Button> - </SheetFooter> - </form> - </Form> + {/* 파일 첨부 섹션 */} + {renderFileUploadSection()} + </form> + </Form> + </div> + + {/* Footer Buttons */} + <SheetFooter className="gap-2 pt-2 sm:space-x-0 flex-shrink-0"> + <SheetClose asChild> + <Button type="button" variant="outline" disabled={isPending || uploadingFiles}> + 취소 + </Button> + </SheetClose> + <Button + disabled={isPending || uploadingFiles} + onClick={form.handleSubmit(onSubmit)} + > + {(isPending || uploadingFiles) && ( + <Loader className="mr-2 h-4 w-4 animate-spin" aria-hidden="true" /> + )} + {uploadingFiles ? "업로드 중..." : isPending ? "저장 중..." : "저장"} + </Button> + </SheetFooter> </SheetContent> </Sheet> ) |
