summaryrefslogtreecommitdiff
path: root/lib/approval-template/service.ts
diff options
context:
space:
mode:
authorjoonhoekim <26rote@gmail.com>2025-08-11 09:34:40 +0000
committerjoonhoekim <26rote@gmail.com>2025-08-11 09:34:40 +0000
commitbcd462d6e60871b86008e072f4b914138fc5c328 (patch)
treec22876fd6c6e7e48254587848b9dff50cdb8b032 /lib/approval-template/service.ts
parentcbb4c7fe0b94459162ad5e998bc05cd293e0ff96 (diff)
(김준회) 리치텍스트에디터 (결재템플릿을 위한 공통컴포넌트), command-menu 에러 수정, 결재 템플릿 관리, 결재선 관리, ECC RFQ+PR Item 수신시 비즈니스테이블(ProcurementRFQ) 데이터 적재, WSDL 오류 수정
Diffstat (limited to 'lib/approval-template/service.ts')
-rw-r--r--lib/approval-template/service.ts330
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