From 78c471eec35182959e0029ded18f144974ccaca2 Mon Sep 17 00:00:00 2001
From: joonhoekim <26rote@gmail.com>
Date: Thu, 23 Oct 2025 18:13:41 +0900
Subject: (김준회) 결재 템플릿 에디터 및 결재 워크플로 공통함수 작성, 실사의뢰
결재 연결 예시 작성
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
lib/approval/template-utils.ts | 215 +++++++++++++++++++++++++++++++++++++++++
1 file changed, 215 insertions(+)
create mode 100644 lib/approval/template-utils.ts
(limited to 'lib/approval/template-utils.ts')
diff --git a/lib/approval/template-utils.ts b/lib/approval/template-utils.ts
new file mode 100644
index 00000000..a39f8ac4
--- /dev/null
+++ b/lib/approval/template-utils.ts
@@ -0,0 +1,215 @@
+/**
+ * 결재 템플릿 유틸리티 함수
+ *
+ * 기능:
+ * - 템플릿 이름으로 조회
+ * - 변수 치환 ({{변수명}})
+ * - HTML 변환 유틸리티 (테이블, 리스트)
+ */
+
+'use server';
+
+import db from '@/db/db';
+import { eq } from 'drizzle-orm';
+import { approvalTemplates } from '@/db/schema/knox/approvals';
+
+/**
+ * 템플릿 이름으로 조회
+ *
+ * @param name - 템플릿 이름 (한국어)
+ * @returns 템플릿 객체 또는 null
+ */
+export async function getApprovalTemplateByName(name: string) {
+ try {
+ const [template] = await db
+ .select()
+ .from(approvalTemplates)
+ .where(eq(approvalTemplates.name, name))
+ .limit(1);
+
+ return template || null;
+ } catch (error) {
+ console.error(`[Template Utils] Failed to get template: ${name}`, error);
+ return null;
+ }
+}
+
+/**
+ * 템플릿 변수 치환
+ *
+ * {{변수명}} 형태의 변수를 실제 값으로 치환
+ *
+ * @param content - 템플릿 HTML 내용
+ * @param variables - 변수 매핑 객체
+ * @returns 치환된 HTML
+ *
+ * @example
+ * ```typescript
+ * const content = "
{{이름}}님, 안녕하세요
";
+ * const variables = { "이름": "홍길동" };
+ * const result = await replaceTemplateVariables(content, variables);
+ * // "홍길동님, 안녕하세요
"
+ * ```
+ */
+export async function replaceTemplateVariables(
+ content: string,
+ variables: Record
+): Promise {
+ let result = content;
+
+ Object.entries(variables).forEach(([key, value]) => {
+ // {{변수명}} 패턴을 전역으로 치환
+ const pattern = new RegExp(`\\{\\{${escapeRegex(key)}\\}\\}`, 'g');
+ result = result.replace(pattern, value);
+ });
+
+ // 치환되지 않은 변수 로그 (디버깅용)
+ const remainingVariables = result.match(/\{\{[^}]+\}\}/g);
+ if (remainingVariables && remainingVariables.length > 0) {
+ console.warn(
+ '[Template Utils] Unmatched variables found:',
+ remainingVariables
+ );
+ }
+
+ return result;
+}
+
+/**
+ * 정규식 특수문자 이스케이프
+ */
+function escapeRegex(str: string): string {
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+}
+
+/**
+ * 배열 데이터를 HTML 테이블로 변환 (공통 유틸)
+ *
+ * @param data - 테이블 데이터 배열
+ * @param columns - 컬럼 정의 (키, 라벨)
+ * @returns HTML 테이블 문자열
+ *
+ * @example
+ * ```typescript
+ * const data = [
+ * { name: "홍길동", age: 30 },
+ * { name: "김철수", age: 25 }
+ * ];
+ * const columns = [
+ * { key: "name", label: "이름" },
+ * { key: "age", label: "나이" }
+ * ];
+ * const html = await htmlTableConverter(data, columns);
+ * ```
+ */
+export async function htmlTableConverter(
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ data: Array>,
+ columns: Array<{ key: string; label: string }>
+): Promise {
+ if (!data || data.length === 0) {
+ return '데이터가 없습니다.
';
+ }
+
+ const headerRow = columns
+ .map(
+ (col) =>
+ `${col.label} | `
+ )
+ .join('');
+
+ const bodyRows = data
+ .map((row) => {
+ const cells = columns
+ .map((col) => {
+ const value = row[col.key];
+ const displayValue =
+ value !== undefined && value !== null ? String(value) : '-';
+ return `${displayValue} | `;
+ })
+ .join('');
+ return `${cells}
`;
+ })
+ .join('');
+
+ return `
+
+
+ ${headerRow}
+
+
+ ${bodyRows}
+
+
+ `;
+}
+
+/**
+ * 배열을 HTML 리스트로 변환
+ *
+ * @param items - 리스트 아이템 배열
+ * @param ordered - 순서 있는 리스트 여부 (기본: false)
+ * @returns HTML 리스트 문자열
+ *
+ * @example
+ * ```typescript
+ * const items = ["첫 번째", "두 번째", "세 번째"];
+ * const html = await htmlListConverter(items, true);
+ * ```
+ */
+export async function htmlListConverter(
+ items: string[],
+ ordered: boolean = false
+): Promise {
+ if (!items || items.length === 0) {
+ return '항목이 없습니다.
';
+ }
+
+ const listItems = items
+ .map((item) => `${item}`)
+ .join('');
+
+ const tag = ordered ? 'ol' : 'ul';
+ const listClass = ordered
+ ? 'list-decimal list-inside my-4'
+ : 'list-disc list-inside my-4';
+
+ return `<${tag} class="${listClass}">${listItems}${tag}>`;
+}
+
+/**
+ * 키-값 쌍을 HTML 정의 목록으로 변환
+ *
+ * @param items - 키-값 쌍 배열
+ * @returns HTML dl 태그
+ *
+ * @example
+ * ```typescript
+ * const items = [
+ * { label: "협력업체명", value: "ABC 주식회사" },
+ * { label: "사업자등록번호", value: "123-45-67890" }
+ * ];
+ * const html = await htmlDescriptionList(items);
+ * ```
+ */
+export async function htmlDescriptionList(
+ items: Array<{ label: string; value: string }>
+): Promise {
+ if (!items || items.length === 0) {
+ return '정보가 없습니다.
';
+ }
+
+ const listItems = items
+ .map(
+ (item) => `
+
+
${item.label}
+ ${item.value}
+
+ `
+ )
+ .join('');
+
+ return `${listItems}
`;
+}
+
--
cgit v1.2.3