"use server"; import { revalidateTag } from "next/cache"; import db from "@/db/db"; import { unstable_cache } from "@/lib/unstable-cache"; import { asc, desc, ilike, or, eq, count, and, ne, sql } from "drizzle-orm"; import { projectGtcFiles, projectGtcView, type ProjectGtcFile, projects, } from "@/db/schema"; import { promises as fs } from "fs"; import path from "path"; import crypto from "crypto"; import { revalidatePath } from 'next/cache'; // Project GTC 목록 조회 export async function getProjectGtcList( input: { page: number; perPage: number; search?: string; sort: Array<{ id: string; desc: boolean }>; filters?: Record; } ) { return unstable_cache( async () => { try { const offset = (input.page - 1) * input.perPage; const { data, total } = await db.transaction(async (tx) => { let whereCondition = undefined; // GTC 파일이 있는 프로젝트만 필터링 const gtcFileCondition = sql`${projectGtcView.gtcFileId} IS NOT NULL`; if (input.search) { const s = `%${input.search}%`; const searchCondition = or( ilike(projectGtcView.code, s), ilike(projectGtcView.name, s), ilike(projectGtcView.type, s), ilike(projectGtcView.originalFileName, s) ); whereCondition = and(gtcFileCondition, searchCondition); } else { whereCondition = gtcFileCondition; } const orderBy = input.sort.length > 0 ? input.sort.map((item) => item.desc ? desc( projectGtcView[ item.id as keyof typeof projectGtcView ] as never ) : asc( projectGtcView[ item.id as keyof typeof projectGtcView ] as never ) ) : [desc(projectGtcView.projectCreatedAt)]; const dataResult = await tx .select() .from(projectGtcView) .where(whereCondition) .orderBy(...orderBy) .limit(input.perPage) .offset(offset); const totalCount = await tx .select({ count: count() }) .from(projectGtcView) .where(whereCondition); return { data: dataResult, total: totalCount[0]?.count || 0, }; }); const pageCount = Math.ceil(total / input.perPage); return { data, pageCount, }; } catch (error) { console.error("getProjectGtcList 에러:", error); throw new Error("Project GTC 목록을 가져오는 중 오류가 발생했습니다."); } }, [`project-gtc-list-${JSON.stringify(input)}`], { tags: ["project-gtc"], revalidate: false, } )(); } // Project GTC 파일 업로드 export async function uploadProjectGtcFile( projectId: number, file: File ): Promise<{ success: boolean; data?: ProjectGtcFile; error?: string }> { try { // 유효성 검사 if (!projectId) { return { success: false, error: "프로젝트 ID는 필수입니다." }; } if (!file) { return { success: false, error: "파일은 필수입니다." }; } // 허용된 파일 타입 검사 const allowedTypes = [ 'application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'text/plain' ]; if (!allowedTypes.includes(file.type)) { return { success: false, error: "PDF, Word, 또는 텍스트 파일만 업로드 가능합니다." }; } // 원본 파일 이름과 확장자 분리 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", "project-gtc"); // 디렉토리가 없으면 생성 try { await fs.mkdir(uploadDir, { recursive: true }); } catch (err) { console.log("Directory already exists or creation failed:", err); } // 파일 경로 설정 const filePath = path.join(uploadDir, hashedFileName); const publicFilePath = `/project-gtc/${hashedFileName}`; // 파일을 ArrayBuffer로 변환 const arrayBuffer = await file.arrayBuffer(); const buffer = Buffer.from(arrayBuffer); // 파일 저장 await fs.writeFile(filePath, buffer); // 기존 파일이 있으면 삭제 const existingFile = await db.query.projectGtcFiles.findFirst({ where: eq(projectGtcFiles.projectId, projectId) }); if (existingFile) { // 기존 파일 삭제 try { const filePath = path.join(process.cwd(), "public", existingFile.filePath); await fs.unlink(filePath); } catch { console.error("파일 삭제 실패"); } // DB에서 기존 파일 정보 삭제 await db.delete(projectGtcFiles) .where(eq(projectGtcFiles.id, existingFile.id)); } // DB에 새 파일 정보 저장 const newFile = await db.insert(projectGtcFiles).values({ projectId, fileName: hashedFileName, filePath: publicFilePath, originalFileName, fileSize: file.size, mimeType: file.type, }).returning(); revalidateTag("project-gtc"); revalidatePath("/evcp/project-gtc"); return { success: true, data: newFile[0] }; } catch (error) { console.error("Project GTC 파일 업로드 에러:", error); return { success: false, error: error instanceof Error ? error.message : "파일 업로드 중 오류가 발생했습니다." }; } } // Project GTC 파일 삭제 export async function deleteProjectGtcFile( projectId: number ): Promise<{ success: boolean; error?: string }> { try { return await db.transaction(async (tx) => { const existingFile = await tx.query.projectGtcFiles.findFirst({ where: eq(projectGtcFiles.projectId, projectId) }); if (!existingFile) { return { success: false, error: "삭제할 파일이 없습니다." }; } // 파일 시스템에서 파일 삭제 try { const filePath = path.join(process.cwd(), "public", existingFile.filePath); await fs.unlink(filePath); } catch (error) { console.error("파일 시스템에서 파일 삭제 실패:", error); throw new Error("파일 시스템에서 파일 삭제에 실패했습니다."); } // DB에서 파일 정보 삭제 await tx.delete(projectGtcFiles) .where(eq(projectGtcFiles.id, existingFile.id)); return { success: true }; }); } catch (error) { console.error("Project GTC 파일 삭제 에러:", error); return { success: false, error: error instanceof Error ? error.message : "파일 삭제 중 오류가 발생했습니다." }; } finally { // 트랜잭션 성공/실패와 관계없이 캐시 무효화 revalidateTag("project-gtc"); revalidatePath("/evcp/project-gtc"); } } // 프로젝트별 GTC 파일 정보 조회 export async function getProjectGtcFile(projectId: number): Promise { try { const file = await db.query.projectGtcFiles.findFirst({ where: eq(projectGtcFiles.projectId, projectId) }); return file || null; } catch (error) { console.error("Project GTC 파일 조회 에러:", error); return null; } } // 프로젝트 생성 서버 액션 export async function createProject( input: { code: string; name: string; type: string; } ): Promise<{ success: boolean; data?: typeof projects.$inferSelect; error?: string }> { try { // 유효성 검사 if (!input.code?.trim()) { return { success: false, error: "프로젝트 코드는 필수입니다." }; } if (!input.name?.trim()) { return { success: false, error: "프로젝트명은 필수입니다." }; } if (!input.type?.trim()) { return { success: false, error: "프로젝트 타입은 필수입니다." }; } // 프로젝트 코드 중복 검사 const existingProject = await db.query.projects.findFirst({ where: eq(projects.code, input.code.trim()) }); if (existingProject) { return { success: false, error: "이미 존재하는 프로젝트 코드입니다." }; } // 프로젝트 생성 const newProject = await db.insert(projects).values({ code: input.code.trim(), name: input.name.trim(), type: input.type.trim(), }).returning(); revalidateTag("project-gtc"); revalidatePath("/evcp/project-gtc"); return { success: true, data: newProject[0] }; } catch (error) { console.error("프로젝트 생성 에러:", error); return { success: false, error: error instanceof Error ? error.message : "프로젝트 생성 중 오류가 발생했습니다." }; } } // 프로젝트 정보 수정 서버 액션 export async function updateProject( input: { id: number; code: string; name: string; type: string; } ): Promise<{ success: boolean; error?: string }> { try { if (!input.id) { return { success: false, error: "프로젝트 ID는 필수입니다." }; } if (!input.code?.trim()) { return { success: false, error: "프로젝트 코드는 필수입니다." }; } if (!input.name?.trim()) { return { success: false, error: "프로젝트명은 필수입니다." }; } if (!input.type?.trim()) { return { success: false, error: "프로젝트 타입은 필수입니다." }; } // 프로젝트 코드 중복 검사 (본인 제외) const existingProject = await db.query.projects.findFirst({ where: and( eq(projects.code, input.code.trim()), ne(projects.id, input.id) ) }); if (existingProject) { return { success: false, error: "이미 존재하는 프로젝트 코드입니다." }; } // 업데이트 await db.update(projects) .set({ code: input.code.trim(), name: input.name.trim(), type: input.type.trim(), }) .where(eq(projects.id, input.id)); revalidateTag("project-gtc"); revalidatePath("/evcp/project-gtc"); return { success: true }; } catch (error) { console.error("프로젝트 수정 에러:", error); return { success: false, error: error instanceof Error ? error.message : "프로젝트 수정 중 오류가 발생했습니다." }; } } // 이미 GTC 파일이 등록된 프로젝트 ID 목록 조회 export async function getProjectsWithGtcFiles(): Promise { try { const result = await db .select({ projectId: projectGtcFiles.projectId }) .from(projectGtcFiles); return result.map(row => row.projectId); } catch (error) { console.error("GTC 파일이 등록된 프로젝트 조회 에러:", error); return []; } }