summaryrefslogtreecommitdiff
path: root/lib/basic-contract
diff options
context:
space:
mode:
Diffstat (limited to 'lib/basic-contract')
-rw-r--r--lib/basic-contract/service.ts147
-rw-r--r--lib/basic-contract/status/basic-contract-columns.tsx153
2 files changed, 109 insertions, 191 deletions
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<BasicContractView>[] {
// ----------------------------------------------------------------
@@ -98,7 +54,7 @@ export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<BasicCo
}
// ----------------------------------------------------------------
- // 2) 파일 다운로드 컬럼 (아이콘)
+ // 2) 파일 다운로드 컬럼 (공용 컴포넌트 사용)
// ----------------------------------------------------------------
const downloadColumn: ColumnDef<BasicContractView> = {
id: "download",
@@ -106,39 +62,35 @@ export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<BasicCo
cell: ({ row }) => {
const template = row.original;
+ if (!template.filePath || !template.fileName) {
+ return null;
+ }
+
return (
- <Button
+ <FileActionsDropdown
+ filePath={template.filePath}
+ fileName={template.fileName}
variant="ghost"
size="icon"
- onClick={() => handleFileDownload(template.filePath, template.fileName)}
- title={`${template.fileName} 다운로드`}
- className="hover:bg-muted"
- >
- <Paperclip className="h-4 w-4" />
- <span className="sr-only">다운로드</span>
- </Button>
+ />
);
},
maxSize: 30,
enableSorting: false,
}
-
// ----------------------------------------------------------------
- // 4) 일반 컬럼들을 "그룹"별로 묶어 중첩 columns 생성
+ // 3) 일반 컬럼들을 "그룹"별로 묶어 중첩 columns 생성
// ----------------------------------------------------------------
- // 4-1) groupMap: { [groupName]: ColumnDef<BasicContractView>[] }
const groupMap: Record<string, ColumnDef<BasicContractView>[]> = {}
basicContractColumnsConfig.forEach((cfg) => {
- // 만약 group가 없으면 "_noGroup" 처리
const groupName = cfg.group || "_noGroup"
if (!groupMap[groupName]) {
groupMap[groupName] = []
}
- // child column 정의
const childCol: ColumnDef<BasicContractView> = {
accessorKey: cfg.id,
enableResizing: true,
@@ -157,57 +109,88 @@ export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<BasicCo
return formatDateTime(dateVal)
}
- // Status 컬럼에 Badge 적용
+ // Status 컬럼에 Badge 적용 (확장)
if (cfg.id === "status") {
const status = row.getValue(cfg.id) as string
- const isActive = status === "ACTIVE"
- return (
- <Badge
- variant={isActive ? "default" : "secondary"}
- >
- {isActive ? "활성" : "비활성"}
- </Badge>
- )
+ 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 <Badge variant={variant}>{label}</Badge>
+ }
+
+ // ✅ 파일 이름 컬럼 (공용 컴포넌트 사용)
+ if (cfg.id === "fileName") {
+ const fileName = cell.getValue() as string;
+ const filePath = row.original.filePath;
+
+ if (fileName && filePath) {
+ return (
+ <FileNameLink
+ filePath={filePath}
+ fileName={fileName}
+ maxLength={200}
+ showIcon={true}
+ />
+ );
+ }
+ return fileName || "";
}
// 나머지 컬럼은 그대로 값 표시
return row.getValue(cfg.id) ?? ""
},
- minSize: 80,
-
+ minSize: 80,
}
groupMap[groupName].push(childCol)
})
// ----------------------------------------------------------------
- // 4-2) groupMap에서 실제 상위 컬럼(그룹)을 만들기
+ // 4) groupMap에서 실제 상위 컬럼(그룹)을 만들기
// ----------------------------------------------------------------
const nestedColumns: ColumnDef<BasicContractView>[] = []
- // 순서를 고정하고 싶다면 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
+}