"use client" import * as React from "react" import { zodResolver } from "@hookform/resolvers/zod" import { useForm } from "react-hook-form" import { CalendarIcon, Loader, X, Download } from "lucide-react" import { format } from "date-fns" import { toast } from "sonner" import { Button } from "@/components/ui/button" import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form" import { Input } from "@/components/ui/input" import { Textarea } from "@/components/ui/textarea" import { Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, } from "@/components/ui/sheet" import { Select, SelectContent, SelectGroup, SelectItem, 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 { FileList, FileListAction, FileListHeader, FileListIcon, FileListInfo, FileListItem, FileListName, FileListSize, } from "@/components/ui/file-list" import { updateVendorInvestigationSchema, type UpdateVendorInvestigationSchema, } from "../validations" import { updateVendorInvestigationAction, getInvestigationAttachments, deleteInvestigationAttachment, createVendorInvestigationAttachmentAction } from "../service" import { VendorInvestigationsViewWithContacts } from "@/config/vendorInvestigationsColumnsConfig" import prettyBytes from "pretty-bytes" import { downloadFile } from "@/lib/file-download" interface UpdateVendorInvestigationSheetProps extends React.ComponentPropsWithoutRef { investigation: VendorInvestigationsViewWithContacts | null } // 첨부파일 정책 정의 const getFileUploadConfig = (status: string) => { // 취소된 상태에서만 파일 업로드 비활성화 if (status === "CANCELED") { return { enabled: false, label: "", description: "", accept: 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" } } /** * 실사 결과 입력 시트 */ export function UpdateVendorInvestigationSheet({ investigation, ...props }: UpdateVendorInvestigationSheetProps) { const [isPending, startTransition] = React.useTransition() const [existingAttachments, setExistingAttachments] = React.useState([]) const [loadingAttachments, setLoadingAttachments] = React.useState(false) const [uploadingFiles, setUploadingFiles] = React.useState(false) // RHF + Zod const form = useForm({ resolver: zodResolver(updateVendorInvestigationSchema), defaultValues: { investigationId: investigation?.investigationId ?? 0, investigationStatus: investigation?.investigationStatus ?? "PLANNED", investigationAddress: investigation?.investigationAddress ?? "", investigationMethod: investigation?.investigationMethod ?? undefined, 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", investigationAddress: investigation.investigationAddress ?? "", investigationMethod: investigation.investigationMethod ?? undefined, 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]) // 기존 첨부파일 로드 함수 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) } } // 첨부파일 삭제 함수 const handleDeleteAttachment = async (attachmentId: number) => { if (!investigation) return try { await deleteInvestigationAttachment(attachmentId) toast.success("첨부파일이 삭제되었습니다.") // 목록 새로고침 loadExistingAttachments(investigation.investigationId) } catch (error) { console.error("첨부파일 삭제 오류:", error) toast.error(error instanceof Error ? error.message : "첨부파일 삭제 중 오류가 발생했습니다.") } } // 첨부파일 다운로드 함수 const handleDownloadAttachment = async (attachment: any) => { if (!attachment.filePath || !attachment.fileName) { toast.error("첨부파일 정보가 올바르지 않습니다.") return } try { await downloadFile(attachment.filePath, attachment.fileName, { showToast: true, action: 'download' }) } catch (error) { console.error("첨부파일 다운로드 오류:", error) toast.error("첨부파일 다운로드 중 오류가 발생했습니다.") } } // 선택된 파일에서 특정 파일 제거 const handleRemoveSelectedFile = (indexToRemove: number) => { const currentFiles = form.getValues("attachments") || [] const updatedFiles = currentFiles.filter((_: File, index: number) => index !== indexToRemove) form.setValue("attachments", updatedFiles.length > 0 ? updatedFiles : undefined) if (updatedFiles.length === 0) { toast.success("모든 선택된 파일이 제거되었습니다.") } else { toast.success("파일이 제거되었습니다.") } } // 파일 업로드 섹션 렌더링 const renderFileUploadSection = () => { const currentStatus = form.watch("investigationStatus") const selectedFiles = form.watch("attachments") as File[] | undefined const config = getFileUploadConfig(currentStatus) if (!config.enabled) return null return ( <> {/* 기존 첨부파일 목록 */} {(existingAttachments.length > 0 || loadingAttachments) && (
기존 첨부파일
{loadingAttachments ? (
첨부파일 로딩 중...
) : existingAttachments.length > 0 ? ( existingAttachments.map((attachment) => (
{attachment.attachmentType} {attachment.fileName} ({Math.round(attachment.fileSize / 1024)}KB)
)) ) : (
첨부된 파일이 없습니다.
)}
)} {/* 새 파일 업로드 */} ( {config.label} { // 거부된 파일에 대한 상세 에러 메시지 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) { // 기존 파일들과 새로 선택된 파일들을 합치기 const currentFiles = form.getValues("attachments") || [] const newFiles = [...currentFiles, ...acceptedFiles] onChange(newFiles) toast.success(`${acceptedFiles.length}개 파일이 추가되었습니다.`) } }} accept={config.accept} multiple maxSize={config.maxSize} disabled={isPending || uploadingFiles} > {isPending || uploadingFiles ? "파일 업로드 중..." : "파일을 드래그하거나 클릭하여 업로드" } {config.description} (최대 {config.maxSizeText}) )} /> {/* 선택된 파일 목록 */} {selectedFiles && selectedFiles.length > 0 && (
{/* 선택된 파일 ({selectedFiles.length}개) */} 업로드 예정 파일 ({selectedFiles.length}개) {selectedFiles.map((file, index) => ( {/* 왼쪽 아이콘 */} {/* 가운데 이름 + 사이즈 */} {file.name} {file.size} {/* 오른쪽 삭제 버튼 */} ))}
)} ) } // 파일 업로드 함수 const uploadFiles = async (files: File[], investigationId: number) => { const uploadPromises = files.map(async (file) => { try { // 서버 액션을 호출하여 파일 저장 및 DB 레코드 생성 const result = await createVendorInvestigationAttachmentAction({ investigationId, file, userId: undefined // 필요시 사용자 ID 추가 }); if (!result.success) { throw new Error(result.error || "파일 업로드 실패"); } return result.attachment; } catch (error) { console.error(`파일 업로드 실패: ${file.name}`, error); throw error; } }); return await Promise.all(uploadPromises); } // Submit handler async function onSubmit(values: UpdateVendorInvestigationSchema) { console.log("onSubmit 호출됨:", values) if (!values.investigationId) { console.log("investigationId가 없음:", values.investigationId) return } startTransition(async () => { try { console.log("startTransition 시작") // 1) 먼저 텍스트 데이터 업데이트 const formData = new FormData() // 필수 필드 formData.append("investigationId", String(values.investigationId)) formData.append("investigationStatus", values.investigationStatus) 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) { console.error("파일 업로드 에러:", fileError) toast.error(`데이터는 저장되었지만 파일 업로드 중 오류가 발생했습니다: ${fileError}`) } finally { setUploadingFiles(false) } } else { toast.success("실사 정보가 업데이트되었습니다!") } form.reset() props.onOpenChange?.(false) } catch (error) { console.error("실사 정보 업데이트 오류:", error) toast.error("실사 정보 업데이트 중 오류가 발생했습니다.") } }) } // 디버깅을 위한 버튼 클릭 핸들러 const handleSaveClick = async () => { console.log("저장 버튼 클릭됨") console.log("현재 폼 값:", form.getValues()) console.log("폼 에러:", form.formState.errors) // 폼 검증 실행 const isValid = await form.trigger() console.log("폼 검증 결과:", isValid) if (isValid) { form.handleSubmit(onSubmit)() } else { console.log("폼 검증 실패, 에러:", form.formState.errors) } } return ( 실사 결과 입력 {investigation?.vendorName && ( {investigation.vendorName} )}의 실사 결과를 입력합니다.
{/* 실사 상태 - 주석처리 (실사 결과 입력에서는 자동으로 완료됨/취소됨/보완요구됨으로 변경) */} {/* ( 실사 상태 )} /> */} {/* 실사 주소 - 주석처리 (실사 진행 관리에서 처리) */} {/* ( 실사 주소