From 02b1cf005cf3e1df64183d20ba42930eb2767a9f Mon Sep 17 00:00:00 2001
From: dujinkim <dujin.kim@dtsolution.co.kr>
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<typeof updateInformationSchema>
 
 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<File | null>(null)
+  const [isUploadingFiles, setIsUploadingFiles] = React.useState(false)
+  const [existingAttachments, setExistingAttachments] = React.useState<InformationAttachment[]>([])
 
   const form = useForm<UpdateInformationSchema>({
     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<HTMLInputElement>) => {
-    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<string> => {
-    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 (
     <Dialog open={open} onOpenChange={onOpenChange}>
-      <DialogContent className="max-w-2xl">
+      <DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
         <DialogHeader>
-          <DialogTitle>인포메이션 수정</DialogTitle>
+          <DialogTitle>{t('information.edit.title', '인포메이션 수정')}</DialogTitle>
           <DialogDescription>
-            페이지 인포메이션 정보를 수정합니다.
+            {t('information.edit.description', '페이지 인포메이션 정보를 수정합니다.')}
           </DialogDescription>
         </DialogHeader>
 
         <Form {...form}>
-          <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
+          <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
+            {/* 페이지 정보 */}
             <div className="bg-gray-50 p-4 rounded-lg">
               <div className="flex items-center gap-2 mb-2">
-                <span className="font-medium">페이지 정보</span>
+                <span className="font-medium">{t('information.page.info', '페이지 정보')}</span>
               </div>
-              <div className="text-sm ">
-                <div><strong>페이지명:</strong> {information?.pageName}</div>
-                <div><strong>경로:</strong> {information?.pagePath}</div>
+              <div className="text-sm">
+                <div><strong>{t('information.page.name', '페이지명')}:</strong> {information?.pageName}</div>
+                <div><strong>{t('information.page.path', '경로')}:</strong> {information?.pagePath}</div>
               </div>
             </div>
 
+            {/* 인포메이션 내용 */}
             <FormField
               control={form.control}
               name="informationContent"
               render={({ field }) => (
                 <FormItem>
-                  <FormLabel>인포메이션 내용</FormLabel>
+                  <FormLabel>{t('information.content.label', '인포메이션 내용')}</FormLabel>
                   <FormControl>
                     <Textarea 
-                      placeholder="인포메이션 내용을 입력하세요" 
+                      placeholder={t('information.content.placeholder', '인포메이션 내용을 입력하세요')} 
                       rows={6}
                       {...field} 
                     />
@@ -185,91 +248,112 @@ export function UpdateInformationDialog({
               )}
             />
 
-            <div>
-              <FormLabel>첨부파일</FormLabel>
-              <div className="mt-2">
-                {uploadedFile ? (
-                  <div className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
-                    <div className="flex items-center gap-2">
-                      <span className="text-sm font-medium">{uploadedFile.name}</span>
-                      <span className="text-xs text-gray-500">
-                        ({(uploadedFile.size / (1024 * 1024)).toFixed(2)} MB)
-                      </span>
-                      <span className="text-xs">(새 파일)</span>
-                    </div>
-                    <Button
-                      type="button"
-                      variant="ghost"
-                      size="sm"
-                      onClick={removeFile}
-                    >
-                      <X className="h-4 w-4" />
-                    </Button>
-                  </div>
-                ) : currentFileName ? (
-                  <div className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
-                    <div className="flex items-center gap-2">
-                      <span className="text-sm font-medium">{currentFileName}</span>
-                      {form.watch("attachmentFileSize") && (
-                        <span className="text-xs text-gray-500">
-                          ({form.watch("attachmentFileSize")})
-                        </span>
-                      )}
-                    </div>
-                    <div className="flex gap-2">
-                      <label
-                        htmlFor="file-upload-update"
-                        className="cursor-pointer text-sm"
-                      >
-                        변경
-                      </label>
-                      <Button
-                        type="button"
-                        variant="ghost"
-                        size="sm"
-                        onClick={removeFile}
-                      >
-                        <X className="h-4 w-4" />
-                      </Button>
-                    </div>
-                  </div>
-                ) : (
-                  <div className="border-2 border-dashed border-gray-300 rounded-lg p-4">
-                    <div className="text-center">
-                      <Upload className="mx-auto h-8 w-8 text-gray-400" />
-                      <div className="mt-2">
-                        <label
-                          htmlFor="file-upload-update"
-                          className="cursor-pointer text-sm"
+            {/* 기존 첨부파일 */}
+            {existingAttachments.length > 0 && (
+              <div className="space-y-3">
+                <FormLabel>{t('information.attachment.existing', '기존 첨부파일')}</FormLabel>
+                <div className="grid gap-2">
+                  {existingAttachments.map((attachment) => (
+                    <div key={attachment.id} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
+                      <div className="flex items-center gap-2">
+                        <FileText className="h-4 w-4" />
+                        <span className="text-sm font-medium">{attachment.fileName}</span>
+                        <span className="text-xs text-gray-500">({attachment.fileSize})</span>
+                      </div>
+                      <div className="flex gap-2">
+                        <Button
+                          type="button"
+                          variant="ghost"
+                          size="sm"
+                          onClick={() => handleDownloadAttachment(attachment)}
                         >
-                          파일을 선택하세요
-                        </label>
+                          <Download className="h-4 w-4" />
+                        </Button>
+                        <Button
+                          type="button"
+                          variant="ghost"
+                          size="sm"
+                          onClick={() => handleDeleteAttachment(attachment.id)}
+                        >
+                          <X className="h-4 w-4" />
+                        </Button>
                       </div>
-                      <p className="text-xs text-gray-500 mt-1">
-                        PDF, DOC, DOCX, XLSX, PPT, PPTX, TXT, ZIP 파일만 업로드 가능
-                      </p>
                     </div>
-                  </div>
-                )}
-                <input
-                  id="file-upload-update"
-                  type="file"
-                  className="hidden"
-                  onChange={handleFileSelect}
-                  accept=".pdf,.doc,.docx,.xlsx,.ppt,.pptx,.txt,.zip"
-                />
+                  ))}
+                </div>
               </div>
+            )}
+
+            {/* 새 첨부파일 업로드 */}
+            <div className="space-y-3">
+              <FormLabel>{t('information.attachment.new', '새 첨부파일')}</FormLabel>
+              
+              <Dropzone
+                maxSize={MAX_FILE_SIZE}
+                multiple
+                onDropAccepted={handleDropAccepted}
+                onDropRejected={handleDropRejected}
+                disabled={isLoading || isUploadingFiles}
+              >
+                {({ maxSize }) => (
+                  <DropzoneZone className="flex justify-center">
+                    <DropzoneInput />
+                    <div className="flex items-center gap-6">
+                      <DropzoneUploadIcon />
+                      <div className="grid gap-0.5">
+                        <DropzoneTitle>
+                          {t('information.attachment.select', '파일을 선택하세요')}
+                        </DropzoneTitle>
+                        <DropzoneDescription>
+                          드래그 앤 드롭하거나 클릭하여 파일을 선택하세요
+                          {maxSize && ` (최대: ${prettyBytes(maxSize)})`}
+                        </DropzoneDescription>
+                      </div>
+                    </div>
+                  </DropzoneZone>
+                )}
+              </Dropzone>
+
+              {/* 새로 선택된 파일 목록 */}
+              {newFileFields.length > 0 && (
+                <div className="grid gap-2">
+                  {newFileFields.map((field, index) => {
+                    const file = form.getValues(`newFiles.${index}`)
+                    if (!file) return null
+                    
+                    return (
+                      <div key={field.id} className="flex items-center justify-between p-3 bg-blue-50 rounded-lg">
+                        <div className="flex items-center gap-2">
+                          <FileText className="h-4 w-4" />
+                          <span className="text-sm font-medium">{file.name}</span>
+                          <span className="text-xs text-gray-500">({prettyBytes(file.size)})</span>
+                          <span className="text-xs text-blue-600">({t('information.attachment.new', '새 파일')})</span>
+                        </div>
+                        <Button
+                          type="button"
+                          variant="ghost"
+                          size="sm"
+                          onClick={() => removeFile(index)}
+                        >
+                          <X className="h-4 w-4" />
+                        </Button>
+                      </div>
+                    )
+                  })}
+                </div>
+              )}
             </div>
 
+            {/* 활성 상태 */}
             <FormField
               control={form.control}
               name="isActive"
               render={({ field }) => (
                 <FormItem className="flex flex-row items-center justify-between rounded-lg border p-3">
                   <div className="space-y-0.5">
-                    <FormLabel className="text-base">활성 상태</FormLabel>
+                    <FormLabel className="text-base">{t('information.status.label', '활성 상태')}</FormLabel>
                     <div className="text-sm text-muted-foreground">
-                      활성화하면 해당 페이지에서 인포메이션 버튼이 표시됩니다.
+                      {t('information.status.description', '활성화하면 해당 페이지에서 인포메이션 버튼이 표시됩니다.')}
                     </div>
                   </div>
                   <FormControl>
@@ -287,13 +371,13 @@ export function UpdateInformationDialog({
                 type="button"
                 variant="outline"
                 onClick={handleClose}
-                disabled={isLoading}
+                disabled={isLoading || isUploadingFiles}
               >
-                취소
+                {t('common.cancel', '취소')}
               </Button>
-              <Button type="submit" disabled={isLoading}>
-                {isLoading && <Loader className="mr-2 h-4 w-4 animate-spin" />}
-                수정
+              <Button type="submit" disabled={isLoading || isUploadingFiles}>
+                {(isLoading || isUploadingFiles) && <Loader className="mr-2 h-4 w-4 animate-spin" />}
+                {isUploadingFiles ? "파일 업로드 중..." : t('common.save', '수정')}
               </Button>
             </DialogFooter>
           </form>
@@ -301,4 +385,4 @@ export function UpdateInformationDialog({
       </DialogContent>
     </Dialog>
   )
-} 
\ No newline at end of file
+}
\ No newline at end of file
-- 
cgit v1.2.3

