From 90f79a7a691943a496f67f01c1e493256070e4de Mon Sep 17 00:00:00 2001 From: dujinkim Date: Mon, 7 Jul 2025 01:44:45 +0000 Subject: (대표님) 변경사항 20250707 10시 43분 - unstaged 변경사항 추가 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/basic-contract/service.ts | 147 ++++++-------------- .../status/basic-contract-columns.tsx | 153 +++++++++------------ 2 files changed, 109 insertions(+), 191 deletions(-) (limited to 'lib/basic-contract') diff --git a/lib/basic-contract/service.ts b/lib/basic-contract/service.ts index 09f8f119..9890cdfc 100644 --- a/lib/basic-contract/service.ts +++ b/lib/basic-contract/service.ts @@ -14,10 +14,6 @@ import { vendors, type BasicContractTemplate as DBBasicContractTemplate, } from "@/db/schema"; -import { toast } from "sonner"; -import { promises as fs } from "fs"; -import path from "path"; -import crypto from "crypto"; import { GetBasicContractTemplatesSchema, @@ -40,6 +36,7 @@ import { sendEmail } from "../mail/sendEmail"; import { headers } from 'next/headers'; import { filterColumns } from "@/lib/filter-columns"; import { differenceInDays, addYears, isBefore } from "date-fns"; +import { deleteFile, saveFile } from "@/lib/file-stroage"; @@ -72,61 +69,27 @@ export async function addTemplate( error: "유효기간은 1~120개월 사이의 유효한 값이어야 합니다." }; } + const saveResult = await saveFile({file, directory:"basicContract/template" }); - // 원본 파일 이름과 확장자 분리 - const originalFileName = file.name; - const fileExtension = path.extname(originalFileName); - const fileNameWithoutExt = path.basename(originalFileName, fileExtension); - - // 해시된 파일 이름 생성 (타임스탬프 + 랜덤 해시 + 확장자) - const timestamp = Date.now(); - const randomHash = crypto.createHash('md5') - .update(`${fileNameWithoutExt}-${timestamp}-${Math.random()}`) - .digest('hex') - .substring(0, 8); - - const hashedFileName = `${timestamp}-${randomHash}${fileExtension}`; - - // 저장 디렉토리 설정 (uploads/contracts 폴더 사용) - const uploadDir = path.join(process.cwd(), "public", "basicContract", "template"); - - // 디렉토리가 없으면 생성 - try { - await fs.mkdir(uploadDir, { recursive: true }); - } catch (err) { - console.log("Directory already exists or creation failed:", err); + if (!saveResult.success) { + return { success: false, error: saveResult.error }; } - // 파일 경로 설정 - const filePath = path.join(uploadDir, hashedFileName); - const publicFilePath = `/basicContract/template/${hashedFileName}`; - - // 파일을 ArrayBuffer로 변환 - const arrayBuffer = await file.arrayBuffer(); - const buffer = Buffer.from(arrayBuffer); - - // 파일 저장 - await fs.writeFile(filePath, buffer); - // DB에 저장할 데이터 구성 const formattedData = { templateName, status, validityPeriod, // 숫자로 변환된 유효기간 - fileName: originalFileName, // 원본 파일 이름 - filePath: publicFilePath, // 공개 접근 가능한 경로 + fileName: file.name, + filePath: saveResult.publicPath! }; // DB에 저장 const { data, error } = await createBasicContractTemplate(formattedData); if (error) { - // 파일 저장 후 DB 저장 실패 시 저장된 파일 삭제 - try { - await fs.unlink(filePath); - } catch (unlinkError) { - console.error("파일 삭제 실패:", unlinkError); - } + // DB 저장 실패 시 파일 삭제 + await deleteFile(saveResult.publicPath!); return { success: false, error }; } @@ -267,16 +230,20 @@ export const saveSignedContract = async ( ): Promise<{ result: true } | { result: false; error: string }> => { try { const originalName = `${tableRowId}_${templateName}`; - const ext = path.extname(originalName); - const uniqueName = uuidv4() + ext; - - const publicDir = path.join(process.cwd(), "public", "basicContract"); - const relativePath = `/basicContract/${uniqueName}`; - const absolutePath = path.join(publicDir, uniqueName); - const buffer = Buffer.from(fileBuffer); + + // ArrayBuffer를 File 객체로 변환 + const file = new File([fileBuffer], originalName); + + // ✅ 서명된 계약서 저장 + // 개발: /project/public/basicContract/signed/ + // 프로덕션: /nas_evcp/basicContract/signed/ + const saveResult = await saveFile({file,directory: "basicContract/signed" ,originalName:originalName}); + + if (!saveResult.success) { + return { result: false, error: saveResult.error! }; + } - await fs.mkdir(publicDir, { recursive: true }); - await fs.writeFile(absolutePath, buffer); + console.log(`✅ 서명된 계약서 저장됨: ${saveResult.filePath}`); await db.transaction(async (tx) => { await tx @@ -284,7 +251,7 @@ export const saveSignedContract = async ( .set({ status: "COMPLETED", fileName: originalName, - filePath: relativePath, + filePath: saveResult.publicPath, // 웹 접근 경로 저장 }) .where(eq(basicContract.id, tableRowId)); }); @@ -348,18 +315,16 @@ export async function removeTemplates({ // 파일 시스템 삭제는 트랜잭션 성공 후 수행 for (const template of templateFiles) { - if (template.filePath) { - const absoluteFilePath = path.join(process.cwd(), 'public', template.filePath); - - try { - await fs.access(absoluteFilePath); - await fs.unlink(absoluteFilePath); - } catch (fileError) { - console.log(`파일 없음 또는 삭제 실패: ${template.filePath}`, fileError); - // 파일 삭제 실패는 전체 작업 성공에 영향 없음 - } + const deleted = await deleteFile(template.filePath); + + if (deleted) { + console.log(`✅ 파일 삭제됨: ${template.filePath}`); + } else { + console.log(`⚠️ 파일 삭제 실패: ${template.filePath}`); } } + + revalidateTag("basic-contract-templates"); revalidateTag("template-status-counts"); @@ -413,41 +378,11 @@ export async function updateTemplate({ // 파일이 있는 경우 처리 if (file) { - // 원본 파일 이름과 확장자 분리 - const originalFileName = file.name; - const fileExtension = path.extname(originalFileName); - const fileNameWithoutExt = path.basename(originalFileName, fileExtension); - - // 해시된 파일 이름 생성 - const timestamp = Date.now(); - const randomHash = crypto.createHash('md5') - .update(`${fileNameWithoutExt}-${timestamp}-${Math.random()}`) - .digest('hex') - .substring(0, 8); - - const hashedFileName = `${timestamp}-${randomHash}${fileExtension}`; - - // 저장 디렉토리 설정 - const uploadDir = path.join(process.cwd(), "public", "basicContract", "template"); - - // 디렉토리가 없으면 생성 - try { - await fs.mkdir(uploadDir, { recursive: true }); - } catch (err) { - console.log("Directory already exists or creation failed:", err); + const saveResult = await saveFile({file,directory:"basicContract/template"}); + if (!saveResult.success) { + return { success: false, error: saveResult.error }; } - // 파일 경로 설정 - const filePath = path.join(uploadDir, hashedFileName); - const publicFilePath = `/basicContract/template/${hashedFileName}`; - - // 파일을 ArrayBuffer로 변환 - const arrayBuffer = await file.arrayBuffer(); - const buffer = Buffer.from(arrayBuffer); - - // 파일 저장 - await fs.writeFile(filePath, buffer); - // 기존 파일 정보 가져오기 const existingTemplate = await db.query.basicContractTemplates.findFirst({ where: eq(basicContractTemplates.id, id) @@ -455,18 +390,18 @@ export async function updateTemplate({ // 기존 파일이 있다면 삭제 if (existingTemplate?.filePath) { - try { - const existingFilePath = path.join(process.cwd(), "public", existingTemplate.filePath); - await fs.access(existingFilePath); // 파일 존재 확인 - await fs.unlink(existingFilePath); // 파일 삭제 - } catch (error) { - console.log("기존 파일 삭제 실패 또는 파일이 없음:", error); + + const deleted = await deleteFile(existingTemplate.filePath); + if (deleted) { + console.log(`✅ 파일 삭제됨: ${existingTemplate.filePath}`); + } else { + console.log(`⚠️ 파일 삭제 실패: ${existingTemplate.filePath}`); } } // 업데이트 데이터에 파일 정보 추가 - updateData.fileName = originalFileName; - updateData.filePath = publicFilePath; + updateData.fileName = file.name; + updateData.filePath = saveResult.publicPath; } // DB 업데이트 diff --git a/lib/basic-contract/status/basic-contract-columns.tsx b/lib/basic-contract/status/basic-contract-columns.tsx index 6ca4a096..54504be4 100644 --- a/lib/basic-contract/status/basic-contract-columns.tsx +++ b/lib/basic-contract/status/basic-contract-columns.tsx @@ -3,29 +3,16 @@ import * as React from "react" import { type DataTableRowAction } from "@/types/table" import { type ColumnDef } from "@tanstack/react-table" -import { Paperclip } from "lucide-react" -import { toast } from "sonner" -import { getErrorMessage } from "@/lib/handle-error" -import { formatDate, formatDateTime } from "@/lib/utils" +import { 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 { + FileActionsDropdown, + FileNameLink +} from "@/components/ui/file-actions" + import { basicContractColumnsConfig } from "@/config/basicContractColumnsConfig" import { BasicContractView } from "@/db/schema" @@ -34,38 +21,7 @@ interface GetColumnsProps { } /** - * 파일 다운로드 함수 - */ -/** - * 파일 다운로드 함수 - */ -const handleFileDownload = (filePath: string | null, fileName: string | null) => { - if (!filePath || !fileName) { - toast.error("파일 정보가 없습니다."); - return; - } - - 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[] { // ---------------------------------------------------------------- @@ -98,7 +54,7 @@ export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef = { id: "download", @@ -106,39 +62,35 @@ export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef { const template = row.original; + if (!template.filePath || !template.fileName) { + return null; + } + return ( - + /> ); }, maxSize: 30, enableSorting: false, } - // ---------------------------------------------------------------- - // 4) 일반 컬럼들을 "그룹"별로 묶어 중첩 columns 생성 + // 3) 일반 컬럼들을 "그룹"별로 묶어 중첩 columns 생성 // ---------------------------------------------------------------- - // 4-1) groupMap: { [groupName]: ColumnDef[] } const groupMap: Record[]> = {} basicContractColumnsConfig.forEach((cfg) => { - // 만약 group가 없으면 "_noGroup" 처리 const groupName = cfg.group || "_noGroup" if (!groupMap[groupName]) { groupMap[groupName] = [] } - // child column 정의 const childCol: ColumnDef = { accessorKey: cfg.id, enableResizing: true, @@ -157,57 +109,88 @@ export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef - {isActive ? "활성" : "비활성"} - - ) + let variant: "default" | "secondary" | "destructive" | "outline" = "secondary"; + let label = status; + + switch (status) { + case "ACTIVE": + variant = "default"; + label = "활성"; + break; + case "INACTIVE": + variant = "secondary"; + label = "비활성"; + break; + case "PENDING": + variant = "outline"; + label = "대기중"; + break; + case "COMPLETED": + variant = "default"; + label = "완료"; + break; + default: + variant = "secondary"; + label = status; + } + + return {label} + } + + // ✅ 파일 이름 컬럼 (공용 컴포넌트 사용) + if (cfg.id === "fileName") { + const fileName = cell.getValue() as string; + const filePath = row.original.filePath; + + if (fileName && filePath) { + return ( + + ); + } + return fileName || ""; } // 나머지 컬럼은 그대로 값 표시 return row.getValue(cfg.id) ?? "" }, - minSize: 80, - + minSize: 80, } groupMap[groupName].push(childCol) }) // ---------------------------------------------------------------- - // 4-2) groupMap에서 실제 상위 컬럼(그룹)을 만들기 + // 4) 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" 등 + header: groupName, columns: colDefs, }) } }) // ---------------------------------------------------------------- - // 5) 최종 컬럼 배열: select, download, nestedColumns, actions + // 5) 최종 컬럼 배열 // ---------------------------------------------------------------- return [ selectColumn, - downloadColumn, // 다운로드 컬럼 추가 + downloadColumn, // ✅ 공용 파일 액션 컴포넌트 사용 ...nestedColumns, ] -} \ No newline at end of file +} -- cgit v1.2.3