summaryrefslogtreecommitdiff
path: root/lib/project-gtc/service.ts
diff options
context:
space:
mode:
Diffstat (limited to 'lib/project-gtc/service.ts')
-rw-r--r--lib/project-gtc/service.ts389
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