diff options
Diffstat (limited to 'lib/project-gtc/service.ts')
| -rw-r--r-- | lib/project-gtc/service.ts | 389 |
1 files changed, 389 insertions, 0 deletions
diff --git a/lib/project-gtc/service.ts b/lib/project-gtc/service.ts new file mode 100644 index 00000000..c65d9364 --- /dev/null +++ b/lib/project-gtc/service.ts @@ -0,0 +1,389 @@ +"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<string, unknown>; + } +) { + 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<ProjectGtcFile | null> { + 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<number[]> { + try { + const result = await db + .select({ projectId: projectGtcFiles.projectId }) + .from(projectGtcFiles); + + return result.map(row => row.projectId); + } catch (error) { + console.error("GTC 파일이 등록된 프로젝트 조회 에러:", error); + return []; + } +}
\ No newline at end of file |
