From ef4c533ebacc2cdc97e518f30e9a9350004fcdfb Mon Sep 17 00:00:00 2001 From: dujinkim Date: Mon, 28 Apr 2025 02:13:30 +0000 Subject: ~20250428 작업사항 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../add-basic-contract-template-dialog.tsx | 359 +++++++++++++++++++++ .../template/basic-contract-template-columns.tsx | 245 ++++++++++++++ .../template/basic-contract-template.tsx | 104 ++++++ .../basicContract-table-toolbar-actions.tsx | 53 +++ .../template/delete-basicContract-dialog.tsx | 149 +++++++++ .../template/update-basicContract-sheet.tsx | 300 +++++++++++++++++ 6 files changed, 1210 insertions(+) create mode 100644 lib/basic-contract/template/add-basic-contract-template-dialog.tsx create mode 100644 lib/basic-contract/template/basic-contract-template-columns.tsx create mode 100644 lib/basic-contract/template/basic-contract-template.tsx create mode 100644 lib/basic-contract/template/basicContract-table-toolbar-actions.tsx create mode 100644 lib/basic-contract/template/delete-basicContract-dialog.tsx create mode 100644 lib/basic-contract/template/update-basicContract-sheet.tsx (limited to 'lib/basic-contract/template') diff --git a/lib/basic-contract/template/add-basic-contract-template-dialog.tsx b/lib/basic-contract/template/add-basic-contract-template-dialog.tsx new file mode 100644 index 00000000..cf0986f0 --- /dev/null +++ b/lib/basic-contract/template/add-basic-contract-template-dialog.tsx @@ -0,0 +1,359 @@ +"use client"; + +import * as React from "react"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import * as z from "zod"; +import { toast } from "sonner"; +import { v4 as uuidv4 } from 'uuid'; +import { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@/components/ui/dialog"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, + FormDescription, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + Dropzone, + DropzoneZone, + DropzoneUploadIcon, + DropzoneTitle, + DropzoneDescription, + DropzoneInput +} from "@/components/ui/dropzone"; +import { Progress } from "@/components/ui/progress"; +import { useRouter } from "next/navigation" + +// 유효기간 필드가 추가된 계약서 템플릿 스키마 정의 +const templateFormSchema = z.object({ + templateName: z.string().min(1, "템플릿 이름은 필수입니다."), + validityPeriod: z.coerce + .number({ invalid_type_error: "유효기간은 숫자여야 합니다." }) + .int("유효기간은 정수여야 합니다.") + .min(1, "유효기간은 최소 1개월 이상이어야 합니다.") + .max(120, "유효기간은 최대 120개월(10년)을 초과할 수 없습니다.") + .default(12), + file: z + .instanceof(File, { message: "파일을 업로드해주세요." }) + .refine((file) => file.size <= 100 * 1024 * 1024, { + message: "파일 크기는 100MB 이하여야 합니다.", + }) + .refine( + (file) => file.type === 'application/pdf', + { message: "PDF 파일만 업로드 가능합니다." } + ), + status: z.enum(["ACTIVE", "DISPOSED"]).default("ACTIVE"), +}); + +type TemplateFormValues = z.infer; + +export function AddTemplateDialog() { + const [open, setOpen] = React.useState(false); + const [isLoading, setIsLoading] = React.useState(false); + const [selectedFile, setSelectedFile] = React.useState(null); + const [uploadProgress, setUploadProgress] = React.useState(0); + const [showProgress, setShowProgress] = React.useState(false); + const router = useRouter() + + // 기본값 설정 + const defaultValues: Partial = { + templateName: "", + validityPeriod: 12, // 기본값 1년 + status: "ACTIVE", + }; + + // 폼 초기화 + const form = useForm({ + resolver: zodResolver(templateFormSchema), + defaultValues, + mode: "onChange", + }); + + // 폼 값 감시 + const templateName = form.watch("templateName"); + const validityPeriod = form.watch("validityPeriod"); + const file = form.watch("file"); + + // 파일 선택 핸들러 + const handleFileChange = (files: File[]) => { + if (files.length > 0) { + const file = files[0]; + setSelectedFile(file); + form.setValue("file", file); + } + }; + + // 청크 크기 설정 (1MB) + const CHUNK_SIZE = 1 * 1024 * 1024; + + // 파일을 청크로 분할하여 업로드하는 함수 + const uploadFileInChunks = async (file: File, fileId: string) => { + const totalChunks = Math.ceil(file.size / CHUNK_SIZE); + setShowProgress(true); + setUploadProgress(0); + + for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) { + const start = chunkIndex * CHUNK_SIZE; + const end = Math.min(start + CHUNK_SIZE, file.size); + const chunk = file.slice(start, end); + + const formData = new FormData(); + formData.append('chunk', chunk); + formData.append('filename', file.name); + formData.append('chunkIndex', chunkIndex.toString()); + formData.append('totalChunks', totalChunks.toString()); + formData.append('fileId', fileId); + + try { + const response = await fetch('/api/upload/basicContract/chunk', { + method: 'POST', + body: formData, + }); + + if (!response.ok) { + throw new Error(`청크 업로드 실패: ${response.statusText}`); + } + + // 진행률 업데이트 + const progress = Math.round(((chunkIndex + 1) / totalChunks) * 100); + setUploadProgress(progress); + + const result = await response.json(); + + // 마지막 청크인 경우 파일 경로 반환 + if (chunkIndex === totalChunks - 1) { + return result; + } + } catch (error) { + console.error(`청크 ${chunkIndex} 업로드 오류:`, error); + throw error; + } + } + }; + + // 폼 제출 핸들러 + async function onSubmit(formData: TemplateFormValues) { + setIsLoading(true); + try { + if (!formData.file) { + throw new Error("파일이 선택되지 않았습니다."); + } + + // 고유 파일 ID 생성 + const fileId = uuidv4(); + + // 파일 청크 업로드 + const uploadResult = await uploadFileInChunks(formData.file, fileId); + + if (!uploadResult.success) { + throw new Error("파일 업로드에 실패했습니다."); + } + + // 메타데이터 저장 + const saveResponse = await fetch('/api/upload/basicContract/complete', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + templateName: formData.templateName, + validityPeriod: formData.validityPeriod, // 유효기간 추가 + status: formData.status, + fileName: uploadResult.fileName, + filePath: uploadResult.filePath, + }), + next: { tags: ["basic-contract-templates"] }, + }); + + const saveResult = await saveResponse.json(); + + if (!saveResult.success) { + throw new Error("템플릿 정보 저장에 실패했습니다."); + } + + toast.success('템플릿이 성공적으로 추가되었습니다.'); + form.reset(); + setSelectedFile(null); + setOpen(false); + setShowProgress(false); + + router.refresh(); + } catch (error) { + console.error("Submit error:", error); + toast.error("템플릿 추가 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + } + + // 모달이 닫힐 때 폼 초기화 + React.useEffect(() => { + if (!open) { + form.reset(); + setSelectedFile(null); + setShowProgress(false); + setUploadProgress(0); + } + }, [open, form]); + + function handleDialogOpenChange(nextOpen: boolean) { + if (!nextOpen) { + form.reset(); + } + setOpen(nextOpen); + } + + // 유효기간 선택 옵션 + const validityOptions = [ + { value: "3", label: "3개월" }, + { value: "6", label: "6개월" }, + { value: "12", label: "1년" }, + { value: "24", label: "2년" }, + { value: "36", label: "3년" }, + { value: "60", label: "5년" }, + ]; + + return ( + + + + + + + 새 기본계약서 템플릿 추가 + + 템플릿 이름을 입력하고 계약서 파일을 업로드하세요. + * 표시된 항목은 필수 입력사항입니다. + + +
+ + ( + + + 템플릿 이름 * + + + + + + + )} + /> + + ( + + + 계약 유효기간 * + + + + 계약서의 유효 기간을 설정합니다. 이 기간이 지나면 재계약이 필요합니다. + + + + )} + /> + + ( + + + 계약서 파일 * + + + + + + + {selectedFile ? selectedFile.name : "PDF 파일을 여기에 드래그하세요"} + + + {selectedFile + ? `파일 크기: ${(selectedFile.size / (1024 * 1024)).toFixed(2)} MB` + : "또는 클릭하여 PDF 파일을 선택하세요 (최대 100MB)"} + + + + + + + + )} + /> + + {showProgress && ( +
+
+ 업로드 진행률 + {uploadProgress}% +
+ +
+ )} + + + + + + + +
+
+ ); +} \ No newline at end of file diff --git a/lib/basic-contract/template/basic-contract-template-columns.tsx b/lib/basic-contract/template/basic-contract-template-columns.tsx new file mode 100644 index 00000000..b0486fe4 --- /dev/null +++ b/lib/basic-contract/template/basic-contract-template-columns.tsx @@ -0,0 +1,245 @@ +"use client" + +import * as React from "react" +import { type DataTableRowAction } from "@/types/table" +import { type ColumnDef } from "@tanstack/react-table" +import { Download, Ellipsis, Paperclip } from "lucide-react" +import { toast } from "sonner" + +import { getErrorMessage } from "@/lib/handle-error" +import { formatDate, formatDateTime } from "@/lib/utils" +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { Checkbox } from "@/components/ui/checkbox" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" + +import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header" +import { basicContractTemplateColumnsConfig } from "@/config/basicContractColumnsConfig" +import { BasicContractTemplate } from "@/db/schema" + +interface GetColumnsProps { + setRowAction: React.Dispatch | null>> +} + +/** + * 파일 다운로드 함수 + */ +const handleFileDownload = (filePath: string, fileName: string) => { + try { + // 전체 URL 생성 + const fullUrl = `${window.location.origin}${filePath}`; + + // a 태그를 생성하여 다운로드 실행 + const link = document.createElement('a'); + link.href = fullUrl; + link.download = fileName; // 다운로드될 파일명 설정 + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + + toast.success("파일 다운로드를 시작합니다."); + } catch (error) { + console.error("파일 다운로드 오류:", error); + toast.error("파일 다운로드 중 오류가 발생했습니다."); + } +}; + +/** + * tanstack table 컬럼 정의 (중첩 헤더 버전) + */ +export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef[] { + // ---------------------------------------------------------------- + // 1) select 컬럼 (체크박스) + // ---------------------------------------------------------------- + const selectColumn: ColumnDef = { + id: "select", + header: ({ table }) => ( + table.toggleAllPageRowsSelected(!!value)} + aria-label="Select all" + className="translate-y-0.5" + /> + ), + cell: ({ row }) => ( + row.toggleSelected(!!value)} + aria-label="Select row" + className="translate-y-0.5" + /> + ), + maxSize: 30, + enableSorting: false, + enableHiding: false, + } + + // ---------------------------------------------------------------- + // 2) 파일 다운로드 컬럼 (아이콘) + // ---------------------------------------------------------------- + const downloadColumn: ColumnDef = { + id: "download", + header: "", + cell: ({ row }) => { + const template = row.original; + + return ( + + ); + }, + maxSize: 30, + enableSorting: false, + } + + // ---------------------------------------------------------------- + // 3) actions 컬럼 (Dropdown 메뉴) + // ---------------------------------------------------------------- + const actionsColumn: ColumnDef = { + id: "actions", + enableHiding: false, + cell: function Cell({ row }) { + const [isUpdatePending, startUpdateTransition] = React.useTransition() + + return ( + + + + + + setRowAction({ row, type: "update" })} + > + Edit + + + + setRowAction({ row, type: "delete" })} + > + Delete + ⌘⌫ + + + + ) + }, + maxSize: 30, + } + + // ---------------------------------------------------------------- + // 4) 일반 컬럼들을 "그룹"별로 묶어 중첩 columns 생성 + // ---------------------------------------------------------------- + // 4-1) groupMap: { [groupName]: ColumnDef[] } + const groupMap: Record[]> = {} + + basicContractTemplateColumnsConfig.forEach((cfg) => { + // 만약 group가 없으면 "_noGroup" 처리 + const groupName = cfg.group || "_noGroup" + + if (!groupMap[groupName]) { + groupMap[groupName] = [] + } + + // child column 정의 + const childCol: ColumnDef = { + accessorKey: cfg.id, + enableResizing: true, + header: ({ column }) => ( + + ), + meta: { + excelHeader: cfg.excelHeader, + group: cfg.group, + type: cfg.type, + }, + cell: ({ row, cell }) => { + // 날짜 형식 처리 + if (cfg.id === "createdAt" || cfg.id === "updatedAt") { + const dateVal = cell.getValue() as Date + return formatDateTime(dateVal) + } + + // Status 컬럼에 Badge 적용 + if (cfg.id === "status") { + const status = row.getValue(cfg.id) as string + const isActive = status === "ACTIVE" + + return ( + + {isActive ? "활성" : "비활성"} + + ) + } + + // 나머지 컬럼은 그대로 값 표시 + return row.getValue(cfg.id) ?? "" + }, + minSize:80 + } + + groupMap[groupName].push(childCol) + }) + + // ---------------------------------------------------------------- + // 4-2) groupMap에서 실제 상위 컬럼(그룹)을 만들기 + // ---------------------------------------------------------------- + const nestedColumns: ColumnDef[] = [] + + // 순서를 고정하고 싶다면 group 순서를 미리 정의하거나 sort해야 함 + // 여기서는 그냥 Object.entries 순서 + Object.entries(groupMap).forEach(([groupName, colDefs]) => { + if (groupName === "_noGroup") { + // 그룹 없음 → 그냥 최상위 레벨 컬럼 + nestedColumns.push(...colDefs) + } else { + // 상위 컬럼 + nestedColumns.push({ + id: groupName, + header: groupName, // "Basic Info", "Metadata" 등 + columns: colDefs, + }) + } + }) + + // ---------------------------------------------------------------- + // 5) 최종 컬럼 배열: select, download, nestedColumns, actions + // ---------------------------------------------------------------- + return [ + selectColumn, + downloadColumn, // 다운로드 컬럼 추가 + ...nestedColumns, + actionsColumn, + ] +} \ No newline at end of file diff --git a/lib/basic-contract/template/basic-contract-template.tsx b/lib/basic-contract/template/basic-contract-template.tsx new file mode 100644 index 00000000..0cca3a41 --- /dev/null +++ b/lib/basic-contract/template/basic-contract-template.tsx @@ -0,0 +1,104 @@ +"use client"; + +import * as React from "react"; +import { DataTable } from "@/components/data-table/data-table"; +import { useDataTable } from "@/hooks/use-data-table"; +import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-advanced-toolbar"; +import type { + DataTableAdvancedFilterField, + DataTableRowAction, +} from "@/types/table" +import { getBasicContractTemplates} from "../service"; +import { getColumns } from "./basic-contract-template-columns"; +import { DeleteTemplatesDialog } from "./delete-basicContract-dialog"; +import { UpdateTemplateSheet } from "./update-basicContract-sheet"; +import { TemplateTableToolbarActions } from "./basicContract-table-toolbar-actions"; +import { BasicContractTemplate } from "@/db/schema"; + + +interface BasicTemplateTableProps { + promises: Promise< + [ + Awaited>, + ] + > +} + + +export function BasicContractTemplateTable({ promises }: BasicTemplateTableProps) { + + + const [rowAction, setRowAction] = + React.useState | null>(null) + + + const [{ data, pageCount }] = + React.use(promises) + + // 컬럼 설정 - 외부 파일에서 가져옴 + const columns = React.useMemo( + () => getColumns({ setRowAction }), + [setRowAction] + ) + + // config 기반으로 필터 필드 설정 + const advancedFilterFields: DataTableAdvancedFilterField[] = [ + { id: "templateName", label: "템플릿명", type: "text" }, + { + id: "status", label: "상태", type: "select", options: [ + { label: "활성", value: "ACTIVE" }, + { label: "비활성", value: "INACTIVE" }, + ] + }, + { id: "fileName", label: "파일명", type: "text" }, + { id: "createdAt", label: "생성일", type: "date" }, + { id: "updatedAt", label: "수정일", type: "date" }, + ]; + + const { table } = useDataTable({ + data, + columns, + pageCount, + // filterFields, + enablePinning: true, + enableAdvancedFilter: true, + initialState: { + sorting: [{ id: "createdAt", desc: true }], + columnPinning: { right: ["actions"] }, + }, + getRowId: (originalRow) => String(originalRow.id), + shallow: false, + clearOnDefault: true, + }) + + return ( + <> + + + + + + + + + setRowAction(null)} + templates={rowAction?.row.original ? [rowAction?.row.original] : []} + showTrigger={false} + onSuccess={() => rowAction?.row.toggleSelected(false)} + /> + + setRowAction(null)} + template={rowAction?.row.original ?? null} + /> + + + + ); +} \ No newline at end of file diff --git a/lib/basic-contract/template/basicContract-table-toolbar-actions.tsx b/lib/basic-contract/template/basicContract-table-toolbar-actions.tsx new file mode 100644 index 00000000..439fea26 --- /dev/null +++ b/lib/basic-contract/template/basicContract-table-toolbar-actions.tsx @@ -0,0 +1,53 @@ +"use client" + +import * as React from "react" +import { type Task } from "@/db/schema/tasks" +import { type Table } from "@tanstack/react-table" +import { Download, Upload } from "lucide-react" + +import { exportTableToExcel } from "@/lib/export" +import { Button } from "@/components/ui/button" +import { DeleteTemplatesDialog } from "./delete-basicContract-dialog" +import { AddTemplateDialog } from "./add-basic-contract-template-dialog" +import { BasicContractTemplate } from "@/db/schema" + +interface TemplateTableToolbarActionsProps { + table: Table +} + +export function TemplateTableToolbarActions({ table }: TemplateTableToolbarActionsProps) { + // 파일 input을 숨기고, 버튼 클릭 시 참조해 클릭하는 방식 + + + return ( +
+ {/** 1) 선택된 로우가 있으면 삭제 다이얼로그 */} + {table.getFilteredSelectedRowModel().rows.length > 0 ? ( + row.original)} + onSuccess={() => table.toggleAllRowsSelected(false)} + /> + ) : null} + + + + {/** 4) Export 버튼 */} + +
+ ) +} \ No newline at end of file diff --git a/lib/basic-contract/template/delete-basicContract-dialog.tsx b/lib/basic-contract/template/delete-basicContract-dialog.tsx new file mode 100644 index 00000000..307bd9aa --- /dev/null +++ b/lib/basic-contract/template/delete-basicContract-dialog.tsx @@ -0,0 +1,149 @@ +"use client" + +import * as React from "react" +import { type Row } from "@tanstack/react-table" +import { Loader, Trash } from "lucide-react" +import { toast } from "sonner" + +import { useMediaQuery } from "@/hooks/use-media-query" +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { + Drawer, + DrawerClose, + DrawerContent, + DrawerDescription, + DrawerFooter, + DrawerHeader, + DrawerTitle, + DrawerTrigger, +} from "@/components/ui/drawer" + +import { removeTemplates } from "../service" +import { BasicContractTemplate } from "@/db/schema" + +interface DeleteBasicContractsDialogProps + extends React.ComponentPropsWithoutRef { + templates: Row["original"][] + showTrigger?: boolean + onSuccess?: () => void +} + +export function DeleteTemplatesDialog({ + templates, + showTrigger = true, + onSuccess, + ...props +}: DeleteBasicContractsDialogProps) { + const [isDeletePending, startDeleteTransition] = React.useTransition() + const isDesktop = useMediaQuery("(min-width: 640px)") + + function onDelete() { + startDeleteTransition(async () => { + const { error } = await removeTemplates({ + ids: templates.map((template) => template.id), + }) + + if (error) { + toast.error(error) + return + } + + props.onOpenChange?.(false) + toast.success("Templates deleted") + onSuccess?.() + }) + } + + if (isDesktop) { + return ( + + {showTrigger ? ( + + + + ) : null} + + + Are you absolutely sure? + + This action cannot be undone. This will permanently delete your{" "} + {templates.length} + {templates.length === 1 ? " template" : " templates"} from our servers. + + + + + + + + + + + ) + } + + return ( + + {showTrigger ? ( + + + + ) : null} + + + Are you absolutely sure? + + This action cannot be undone. This will permanently delete your{" "} + {templates.length} + {templates.length === 1 ? " template" : " templates"} from our servers. + + + + + + + + + + + ) +} diff --git a/lib/basic-contract/template/update-basicContract-sheet.tsx b/lib/basic-contract/template/update-basicContract-sheet.tsx new file mode 100644 index 00000000..2c6efc9b --- /dev/null +++ b/lib/basic-contract/template/update-basicContract-sheet.tsx @@ -0,0 +1,300 @@ +"use client" + +import * as React from "react" +import { zodResolver } from "@hookform/resolvers/zod" +import { Loader } from "lucide-react" +import { useForm } from "react-hook-form" +import { toast } from "sonner" +import * as z from "zod" + +import { Button } from "@/components/ui/button" +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, + FormDescription, +} from "@/components/ui/form" +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { + Sheet, + SheetClose, + SheetContent, + SheetDescription, + SheetFooter, + SheetHeader, + SheetTitle, +} from "@/components/ui/sheet" +import { Input } from "@/components/ui/input" +import { + Dropzone, + DropzoneZone, + DropzoneUploadIcon, + DropzoneTitle, + DropzoneDescription, + DropzoneInput +} from "@/components/ui/dropzone" +import { updateTemplate } from "../service" +import { BasicContractTemplate } from "@/db/schema" + +// 업데이트 템플릿 스키마 정의 (유효기간 필드 추가) +export const updateTemplateSchema = z.object({ + templateName: z.string().min(1, "템플릿 이름은 필수입니다."), + validityPeriod: z.coerce + .number({ invalid_type_error: "유효기간은 숫자여야 합니다." }) + .int("유효기간은 정수여야 합니다.") + .min(1, "유효기간은 최소 1개월 이상이어야 합니다.") + .max(120, "유효기간은 최대 120개월(10년)을 초과할 수 없습니다.") + .default(12), + status: z.enum(["ACTIVE", "INACTIVE"], { + required_error: "상태는 필수 선택사항입니다.", + }), + file: z.instanceof(File, { message: "파일을 업로드해주세요." }).optional(), +}) + +export type UpdateTemplateSchema = z.infer + +interface UpdateTemplateSheetProps + extends React.ComponentPropsWithRef { + template: BasicContractTemplate | null + onSuccess?: () => void +} + +export function UpdateTemplateSheet({ template, onSuccess, ...props }: UpdateTemplateSheetProps) { + const [isUpdatePending, startUpdateTransition] = React.useTransition() + const [selectedFile, setSelectedFile] = React.useState(null) + + // 템플릿 데이터 확인을 위한 로그 + console.log(template) + + const form = useForm({ + resolver: zodResolver(updateTemplateSchema), + defaultValues: { + templateName: template?.templateName ?? "", + validityPeriod: template?.validityPeriod ?? 12, // 기본값 12개월 + status: (template?.status as "ACTIVE" | "INACTIVE") || "ACTIVE" + }, + mode: "onChange" + }) + + // 파일 선택 핸들러 + const handleFileChange = (files: File[]) => { + if (files.length > 0) { + const file = files[0]; + setSelectedFile(file); + form.setValue("file", file); + } + }; + + // 템플릿 변경 시 폼 값 업데이트 + React.useEffect(() => { + if (template) { + form.reset({ + templateName: template.templateName, + validityPeriod: template.validityPeriod ?? 12, // 기존 값이 없으면 기본값 12개월 + status: template.status as "ACTIVE" | "INACTIVE", + }); + } + }, [template, form]); + + // 유효기간 선택 옵션 + const validityOptions = [ + { value: "3", label: "3개월" }, + { value: "6", label: "6개월" }, + { value: "12", label: "1년" }, + { value: "24", label: "2년" }, + { value: "36", label: "3년" }, + { value: "60", label: "5년" }, + ]; + + function onSubmit(input: UpdateTemplateSchema) { + startUpdateTransition(async () => { + if (!template) return + + // FormData 객체 생성하여 파일과 데이터를 함께 전송 + const formData = new FormData(); + formData.append("templateName", input.templateName); + formData.append("validityPeriod", input.validityPeriod.toString()); // 유효기간 추가 + formData.append("status", input.status); + + if (input.file) { + formData.append("file", input.file); + } + + try { + // 서비스 함수 호출 + const { error } = await updateTemplate({ + id: template.id, + formData, + }); + + if (error) { + toast.error(error); + return; + } + + form.reset(); + setSelectedFile(null); + props.onOpenChange?.(false); + toast.success("템플릿이 성공적으로 업데이트되었습니다."); + onSuccess?.(); + } catch (error) { + console.error("Update error:", error); + toast.error("템플릿 업데이트 중 오류가 발생했습니다."); + } + }); + } + + return ( + + + + 템플릿 업데이트 + + 템플릿 정보를 수정하고 변경사항을 저장하세요 + + +
+ + ( + + 템플릿 이름 + + + + + + )} + /> + + ( + + 계약 유효기간 + + + 계약서의 유효 기간을 설정합니다. 이 기간이 지나면 재계약이 필요합니다. + + + + )} + /> + + ( + + 상태 + + + + )} + /> + + ( + + 템플릿 파일 (선택사항) + + + + + + {selectedFile + ? selectedFile.name + : template?.fileName + ? `현재 파일: ${template.fileName}` + : "새 파일을 드래그하세요"} + + + {selectedFile + ? `파일 크기: ${(selectedFile.size / 1024).toFixed(2)} KB` + : "또는 클릭하여 파일을 선택하세요 (선택사항)"} + + + + + + + + )} + /> + + + + + + + + + +
+
+ ) +} \ No newline at end of file -- cgit v1.2.3