diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-08-21 06:57:36 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-08-21 06:57:36 +0000 |
| commit | 02b1cf005cf3e1df64183d20ba42930eb2767a9f (patch) | |
| tree | e932c54d5260b0e6fda2b46be2a6ba1c3ee30434 /lib/information/table | |
| parent | d78378ecd7ceede1429359f8058c7a99ac34b1b7 (diff) | |
(대표님, 최겸) 설계메뉴추가, 작업사항 업데이트
설계메뉴 - 문서관리
설계메뉴 - 벤더 데이터
gtc 메뉴 업데이트
정보시스템 - 메뉴리스트 및 정보 업데이트
파일 라우트 업데이트
엑셀임포트 개선
기본계약 개선
벤더 가입과정 변경 및 개선
벤더 기본정보 - pq
돌체 오류 수정 및 개선
벤더 로그인 과정 이메일 오류 수정
Diffstat (limited to 'lib/information/table')
| -rw-r--r-- | lib/information/table/update-information-dialog.tsx | 380 |
1 files changed, 232 insertions, 148 deletions
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 |
