From 02b1cf005cf3e1df64183d20ba42930eb2767a9f Mon Sep 17 00:00:00 2001 From: dujinkim Date: Thu, 21 Aug 2025 06:57:36 +0000 Subject: (대표님, 최겸) 설계메뉴추가, 작업사항 업데이트 설계메뉴 - 문서관리 설계메뉴 - 벤더 데이터 gtc 메뉴 업데이트 정보시스템 - 메뉴리스트 및 정보 업데이트 파일 라우트 업데이트 엑셀임포트 개선 기본계약 개선 벤더 가입과정 변경 및 개선 벤더 기본정보 - pq 돌체 오류 수정 및 개선 벤더 로그인 과정 이메일 오류 수정 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../table/update-information-dialog.tsx | 380 +++++++++++++-------- 1 file changed, 232 insertions(+), 148 deletions(-) (limited to 'lib/information/table/update-information-dialog.tsx') diff --git a/lib/information/table/update-information-dialog.tsx b/lib/information/table/update-information-dialog.tsx index b4c11e17..a02b6eb1 100644 --- a/lib/information/table/update-information-dialog.tsx +++ b/lib/information/table/update-information-dialog.tsx @@ -2,10 +2,12 @@ import * as React from "react" import { zodResolver } from "@hookform/resolvers/zod" -import { useForm } from "react-hook-form" +import { useForm, useFieldArray } from "react-hook-form" import { toast } from "sonner" -import { Loader, Upload, X } from "lucide-react" -import { useRouter } from "next/navigation" +import { Loader, Download, X, FileText } from "lucide-react" +import { useRouter, useParams } from "next/navigation" +import { useTranslation } from "@/i18n/client" +import { z } from "zod" import { Button } from "@/components/ui/button" import { @@ -24,17 +26,43 @@ import { FormLabel, FormMessage, } from "@/components/ui/form" +import { + Dropzone, + DropzoneZone, + DropzoneInput, + DropzoneUploadIcon, + DropzoneTitle, + DropzoneDescription, +} from "@/components/ui/dropzone" import { Textarea } from "@/components/ui/textarea" import { Switch } from "@/components/ui/switch" -import { updateInformationData } from "@/lib/information/service" -import { updateInformationSchema, type UpdateInformationSchema } from "@/lib/information/validations" -import type { PageInformation } from "@/db/schema/information" +import { + updateInformationData, + uploadInformationAttachment, + deleteInformationAttachmentAction, + downloadInformationAttachment +} from "@/lib/information/service" +import type { PageInformation, InformationAttachment } from "@/db/schema/information" +import { downloadFile } from "@/lib/file-download" +import prettyBytes from "pretty-bytes" + +const MAX_FILE_SIZE = 50 * 1024 * 1024 // 50MB + +// 폼 스키마 +const updateInformationSchema = z.object({ + id: z.number(), + informationContent: z.string().min(1, "인포메이션 내용을 입력해주세요"), + isActive: z.boolean(), + newFiles: z.array(z.any()).optional(), +}) + +type UpdateInformationSchema = z.infer interface UpdateInformationDialogProps { open: boolean onOpenChange: (open: boolean) => void - information?: PageInformation + information?: PageInformation & { attachments?: InformationAttachment[] } onSuccess?: () => void } @@ -45,137 +73,172 @@ export function UpdateInformationDialog({ onSuccess, }: UpdateInformationDialogProps) { const router = useRouter() + const params = useParams() + const lng = (params?.lng as string) || 'ko' + const { t } = useTranslation(lng, 'common') const [isLoading, setIsLoading] = React.useState(false) - const [uploadedFile, setUploadedFile] = React.useState(null) + const [isUploadingFiles, setIsUploadingFiles] = React.useState(false) + const [existingAttachments, setExistingAttachments] = React.useState([]) const form = useForm({ resolver: zodResolver(updateInformationSchema), defaultValues: { id: 0, informationContent: "", - attachmentFileName: "", - attachmentFilePath: "", - attachmentFileSize: "", isActive: true, + newFiles: [], }, }) + const { fields: newFileFields, append: appendFile, remove: removeFile } = useFieldArray({ + control: form.control, + name: "newFiles", + }) + // 인포메이션 데이터가 변경되면 폼 업데이트 React.useEffect(() => { if (information && open) { form.reset({ id: information.id, informationContent: information.informationContent || "", - attachmentFileName: information.attachmentFileName || "", - attachmentFilePath: information.attachmentFilePath || "", - attachmentFileSize: information.attachmentFileSize || "", isActive: information.isActive, + newFiles: [], }) + setExistingAttachments(information.attachments || []) } }, [information, open, form]) - const handleFileSelect = (event: React.ChangeEvent) => { - const file = event.target.files?.[0] - if (file) { - setUploadedFile(file) - // 파일 크기를 MB 단위로 변환 - const sizeInMB = (file.size / (1024 * 1024)).toFixed(2) - form.setValue("attachmentFileName", file.name) - form.setValue("attachmentFileSize", `${sizeInMB} MB`) - } - } - - const removeFile = () => { - setUploadedFile(null) - form.setValue("attachmentFileName", "") - form.setValue("attachmentFilePath", "") - form.setValue("attachmentFileSize", "") - } - - const uploadFile = async (file: File): Promise => { - const formData = new FormData() - formData.append("file", file) + // 파일 드롭 핸들러 + const handleDropAccepted = React.useCallback((acceptedFiles: File[]) => { + acceptedFiles.forEach(file => { + appendFile(file) + }) + }, [appendFile]) - const response = await fetch("/api/upload", { - method: "POST", - body: formData, + const handleDropRejected = React.useCallback((rejectedFiles: any[]) => { + rejectedFiles.forEach(rejection => { + toast.error(`파일 업로드 실패: ${rejection.file.name}`) }) + }, []) - if (!response.ok) { - throw new Error("파일 업로드에 실패했습니다.") + // 기존 첨부파일 다운로드 + const handleDownloadAttachment = async (attachment: InformationAttachment) => { + try { + const result = await downloadInformationAttachment(attachment.id) + if (result.success && result.data) { + await downloadFile(result.data.filePath, result.data.fileName) + toast.success("파일 다운로드가 시작되었습니다.") + } else { + toast.error(result.message || "파일 다운로드에 실패했습니다.") + } + } catch (error) { + console.error("파일 다운로드 오류:", error) + toast.error("파일 다운로드 중 오류가 발생했습니다.") } + } + + // 기존 첨부파일 삭제 + const handleDeleteAttachment = async (attachmentId: number) => { + if (!confirm("정말로 이 첨부파일을 삭제하시겠습니까?")) return - const result = await response.json() - return result.url + try { + const result = await deleteInformationAttachmentAction(attachmentId) + if (result.success) { + setExistingAttachments(prev => prev.filter(att => att.id !== attachmentId)) + toast.success(result.message) + } else { + toast.error(result.message) + } + } catch (error) { + console.error("첨부파일 삭제 오류:", error) + toast.error("첨부파일 삭제 중 오류가 발생했습니다.") + } } const onSubmit = async (values: UpdateInformationSchema) => { setIsLoading(true) try { - const finalValues = { ...values } + // 1. 인포메이션 정보 업데이트 + const updateResult = await updateInformationData({ + id: values.id, + informationContent: values.informationContent, + isActive: values.isActive, + }) - // 새 파일이 있으면 업로드 - if (uploadedFile) { - const filePath = await uploadFile(uploadedFile) - finalValues.attachmentFilePath = filePath + if (!updateResult.success) { + toast.error(updateResult.message) + return } - const result = await updateInformationData(finalValues) - - if (result.success) { - toast.success(result.message) - if (onSuccess) onSuccess() - onOpenChange(false) - router.refresh() - } else { - toast.error(result.message) + // 2. 새 첨부파일 업로드 + if (values.newFiles && values.newFiles.length > 0) { + setIsUploadingFiles(true) + + for (const file of values.newFiles) { + const formData = new FormData() + formData.append("informationId", String(values.id)) + formData.append("file", file) + + const uploadResult = await uploadInformationAttachment(formData) + if (!uploadResult.success) { + toast.error(`파일 업로드 실패: ${file.name} - ${uploadResult.message}`) + } + } + setIsUploadingFiles(false) } + + toast.success("인포메이션이 성공적으로 수정되었습니다.") + if (onSuccess) onSuccess() + onOpenChange(false) + router.refresh() } catch (error) { + console.error("인포메이션 수정 오류:", error) toast.error("인포메이션 수정에 실패했습니다.") - console.error(error) } finally { setIsLoading(false) + setIsUploadingFiles(false) } } const handleClose = () => { - setUploadedFile(null) + form.reset() + setExistingAttachments([]) onOpenChange(false) } - const currentFileName = form.watch("attachmentFileName") - return ( - + - 인포메이션 수정 + {t('information.edit.title', '인포메이션 수정')} - 페이지 인포메이션 정보를 수정합니다. + {t('information.edit.description', '페이지 인포메이션 정보를 수정합니다.')}
- + + {/* 페이지 정보 */}
- 페이지 정보 + {t('information.page.info', '페이지 정보')}
-
-
페이지명: {information?.pageName}
-
경로: {information?.pagePath}
+
+
{t('information.page.name', '페이지명')}: {information?.pageName}
+
{t('information.page.path', '경로')}: {information?.pagePath}
+ {/* 인포메이션 내용 */} ( - 인포메이션 내용 + {t('information.content.label', '인포메이션 내용')}