diff options
Diffstat (limited to 'lib/approval-template/service.ts')
| -rw-r--r-- | lib/approval-template/service.ts | 330 |
1 files changed, 330 insertions, 0 deletions
diff --git a/lib/approval-template/service.ts b/lib/approval-template/service.ts new file mode 100644 index 00000000..5dc989d9 --- /dev/null +++ b/lib/approval-template/service.ts @@ -0,0 +1,330 @@ +'use server'; + +import db from '@/db/db'; +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'; + +// --------------------------------------------- +// Types +// --------------------------------------------- + +export type ApprovalTemplate = typeof approvalTemplates.$inferSelect; +export type ApprovalTemplateVariable = + typeof approvalTemplateVariables.$inferSelect; +export type ApprovalTemplateHistory = + typeof approvalTemplateHistory.$inferSelect; + +export interface ApprovalTemplateWithVariables extends ApprovalTemplate { + variables: ApprovalTemplateVariable[]; +} + +// --------------------------------------------- +// List & read helpers +// --------------------------------------------- + +interface ListInput { + page: number; + perPage: number; + search?: string; + filters?: Record<string, unknown>[]; + joinOperator?: 'and' | 'or'; + sort?: Array<{ id: string; desc: boolean }>; +} + +export async function getApprovalTemplateList(input: ListInput) { + 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); + + const where = + conditions.length === 0 + ? undefined + : conditions.length === 1 + ? conditions[0] + : and(...conditions); + + /* ------------------------------------------------------------------ + * ORDER BY 절 구성 + * ----------------------------------------------------------------*/ + let orderBy; + try { + orderBy = input.sort && input.sort.length > 0 + ? input.sort + .map((item) => { + if (!item || !item.id || typeof item.id !== 'string') return null; + if (!(item.id in approvalTemplates)) return null; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const col = approvalTemplates[item.id]; + return item.desc ? desc(col) : asc(col); + }) + .filter((v): v is Exclude<typeof v, null> => v !== null) + : [desc(approvalTemplates.updatedAt)]; + } catch { + 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<ApprovalTemplateWithVariables | null> { + 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<ApprovalTemplateWithVariables> { + // 중복 이름 체크 (옵션) + 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('생성된 템플릿을 조회할 수 없습니다.'); + 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<ApprovalTemplateWithVariables> { + 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('업데이트된 템플릿을 조회할 수 없습니다.'); + 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, + description: v.description, + })), + }) + + 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)); + return { success: true }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : '삭제에 실패했습니다.', + }; + } +}
\ No newline at end of file |
