'use server'; import db from '@/db/db'; import { revalidateI18nPaths } from '@/lib/revalidate'; import { and, asc, count, desc, eq, ilike, or, } from 'drizzle-orm'; import { approvalTemplates, approvalTemplateVariables, approvalTemplateHistory, } from '@/db/schema/knox/approvals'; import { filterColumns } from '@/lib/filter-columns'; import { GetApprovalTemplateSchema } from './validations'; // --------------------------------------------- // Types // --------------------------------------------- export type ApprovalTemplate = typeof approvalTemplates.$inferSelect; export type ApprovalTemplateVariable = typeof approvalTemplateVariables.$inferSelect; export type ApprovalTemplateHistory = typeof approvalTemplateHistory.$inferSelect; // --------------------------------------------- // Revalidation helpers // --------------------------------------------- async function revalidateApprovalTemplatesPaths() { await revalidateI18nPaths('/evcp/approval/template'); } export interface ApprovalTemplateWithVariables extends ApprovalTemplate { variables: ApprovalTemplateVariable[]; } // --------------------------------------------- // List & read helpers // --------------------------------------------- export async function getApprovalTemplateList(input: GetApprovalTemplateSchema) { const offset = (input.page - 1) * input.perPage; /* ------------------------------------------------------------------ * WHERE 절 구성 * ----------------------------------------------------------------*/ const advancedWhere = filterColumns({ table: approvalTemplates, filters: input.filters, joinOperator: input.joinOperator, }); // 전역 검색 (name, subject, description, category) let globalWhere; if (input.search) { const s = `%${input.search}%`; globalWhere = or( ilike(approvalTemplates.name, s), ilike(approvalTemplates.subject, s), ilike(approvalTemplates.description, s), ilike(approvalTemplates.category, s), ); } const conditions = []; if (advancedWhere) conditions.push(advancedWhere); if (globalWhere) conditions.push(globalWhere); let finalWhere; if (conditions.length > 0) { finalWhere = conditions.length > 1 ? and(...conditions) : conditions[0]; } // 아니면 ilike, inArray, gte 등으로 where 절 구성 const where = finalWhere; /* ------------------------------------------------------------------ * ORDER BY 절 구성 * ----------------------------------------------------------------*/ let orderBy; try { orderBy = input.sort && input.sort.length > 0 ? input.sort .map((item) => { if (!item || !item.id || typeof item.id !== "string" || !(item.id in approvalTemplates)) return null; const col = approvalTemplates[item.id as keyof typeof approvalTemplates]; return item.desc ? desc(col) : asc(col); }) .filter((v): v is Exclude => v !== null) : [desc(approvalTemplates.updatedAt)]; } catch (orderErr) { console.error("Error building order by:", orderErr); orderBy = [desc(approvalTemplates.updatedAt)]; } /* ------------------------------------------------------------------ * 데이터 조회 * ----------------------------------------------------------------*/ const data = await db .select() .from(approvalTemplates) .where(where) .orderBy(...orderBy) .limit(input.perPage) .offset(offset); const totalResult = await db .select({ count: count() }) .from(approvalTemplates) .where(where); const total = totalResult[0]?.count ?? 0; const pageCount = Math.ceil(total / input.perPage); return { data, pageCount, }; } // ---------------------------------------------------- // Get single template + variables // ---------------------------------------------------- export async function getApprovalTemplate(id: string): Promise { const [template] = await db .select() .from(approvalTemplates) .where(eq(approvalTemplates.id, id)) .limit(1); if (!template) return null; const variables = await db .select() .from(approvalTemplateVariables) .where(eq(approvalTemplateVariables.approvalTemplateId, id)) .orderBy(approvalTemplateVariables.variableName); return { ...template, variables, }; } // ---------------------------------------------------- // Create template // ---------------------------------------------------- interface CreateInput { name: string; subject: string; content: string; category?: string; description?: string; createdBy: number; approvalLineId?: string | null; variables?: Array<{ variableName: string; variableType: string; defaultValue?: string; description?: string; }>; } export async function createApprovalTemplate(data: CreateInput): Promise { // 중복 이름 체크 (옵션) const existing = await db .select({ id: approvalTemplates.id }) .from(approvalTemplates) .where(eq(approvalTemplates.name, data.name)) .limit(1); if (existing.length > 0) { throw new Error('이미 존재하는 템플릿 이름입니다.'); } const [newTemplate] = await db .insert(approvalTemplates) .values({ name: data.name, subject: data.subject, content: data.content, category: data.category, description: data.description, approvalLineId: data.approvalLineId ?? null, createdBy: data.createdBy, }) .returning(); if (data.variables?.length) { const variableRows = data.variables.map((v) => ({ approvalTemplateId: newTemplate.id, variableName: v.variableName, variableType: v.variableType, defaultValue: v.defaultValue, description: v.description, createdBy: data.createdBy, })); await db.insert(approvalTemplateVariables).values(variableRows); } const result = await getApprovalTemplate(newTemplate.id); if (!result) throw new Error('생성된 템플릿을 조회할 수 없습니다.'); await revalidateApprovalTemplatesPaths(); return result; } // ---------------------------------------------------- // Update template // ---------------------------------------------------- interface UpdateInput { name?: string; subject?: string; content?: string; description?: string; category?: string; approvalLineId?: string | null; updatedBy: number; } export async function updateApprovalTemplate(id: string, data: UpdateInput): Promise { const existing = await getApprovalTemplate(id); if (!existing) throw new Error('템플릿을 찾을 수 없습니다.'); // 버전 계산 - 현재 히스토리 카운트 + 1 const versionResult = await db .select({ count: count() }) .from(approvalTemplateHistory) .where(eq(approvalTemplateHistory.templateId, id)); const nextVersion = Number(versionResult[0]?.count ?? 0) + 1; // 히스토리 저장 await db.insert(approvalTemplateHistory).values({ templateId: id, version: nextVersion, subject: existing.subject, content: existing.content, changeDescription: '템플릿 업데이트', changedBy: data.updatedBy, }); // 템플릿 업데이트 await db .update(approvalTemplates) .set({ name: data.name ?? existing.name, subject: data.subject ?? existing.subject, content: data.content ?? existing.content, description: data.description ?? existing.description, category: data.category ?? existing.category, approvalLineId: data.approvalLineId === undefined ? existing.approvalLineId : data.approvalLineId, updatedAt: new Date(), }) .where(eq(approvalTemplates.id, id)); const result = await getApprovalTemplate(id); if (!result) throw new Error('업데이트된 템플릿을 조회할 수 없습니다.'); await revalidateApprovalTemplatesPaths(); return result; } // ---------------------------------------------------- // Server Actions // ---------------------------------------------------- export async function updateApprovalTemplateAction(id: string, data: UpdateInput) { try { const updated = await updateApprovalTemplate(id, data) return { success: true, data: updated } } catch (error) { return { success: false, error: error instanceof Error ? error.message : '업데이트에 실패했습니다.' } } } // ---------------------------------------------------- // Duplicate template // ---------------------------------------------------- export async function duplicateApprovalTemplate( id: string, newName: string, createdBy: number, ): Promise<{ success: boolean; error?: string; data?: ApprovalTemplate }> { try { const existing = await getApprovalTemplate(id) if (!existing) return { success: false, error: '템플릿을 찾을 수 없습니다.' } // 새 템플릿 생성 const duplicated = await createApprovalTemplate({ name: newName, subject: existing.subject, content: existing.content, category: existing.category ?? undefined, description: existing.description ? `${existing.description} (복사본)` : undefined, createdBy, variables: existing.variables?.map((v) => ({ variableName: v.variableName, variableType: v.variableType, defaultValue: v.defaultValue ?? undefined, description: v.description ?? undefined, })) as Array<{ variableName: string; variableType: string; defaultValue?: string; description?: string; }>, }) return { success: true, data: duplicated } } catch (error) { return { success: false, error: error instanceof Error ? error.message : '복제에 실패했습니다.' } } } // ---------------------------------------------------- // Delete (soft delete X -> 실제 삭제) // ---------------------------------------------------- export async function deleteApprovalTemplate(id: string): Promise<{ success: boolean; error?: string }> { try { await db.delete(approvalTemplates).where(eq(approvalTemplates.id, id)); await revalidateApprovalTemplatesPaths(); return { success: true }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : '삭제에 실패했습니다.', }; } }