summaryrefslogtreecommitdiff
path: root/lib/approval/template-utils.ts
diff options
context:
space:
mode:
authorjoonhoekim <26rote@gmail.com>2025-10-23 18:13:41 +0900
committerjoonhoekim <26rote@gmail.com>2025-10-23 18:13:41 +0900
commit78c471eec35182959e0029ded18f144974ccaca2 (patch)
tree914cdf1c8f406ca3e2aa639b8bb774f7f4e87023 /lib/approval/template-utils.ts
parent0be8940580c4a4a4e098b649d198160f9b60420c (diff)
(김준회) 결재 템플릿 에디터 및 결재 워크플로 공통함수 작성, 실사의뢰 결재 연결 예시 작성
Diffstat (limited to 'lib/approval/template-utils.ts')
-rw-r--r--lib/approval/template-utils.ts215
1 files changed, 215 insertions, 0 deletions
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 = "<p>{{이름}}님, 안녕하세요</p>";
+ * const variables = { "이름": "홍길동" };
+ * const result = await replaceTemplateVariables(content, variables);
+ * // "<p>홍길동님, 안녕하세요</p>"
+ * ```
+ */
+export async function replaceTemplateVariables(
+ content: string,
+ variables: Record<string, string>
+): Promise<string> {
+ 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<Record<string, any>>,
+ columns: Array<{ key: string; label: string }>
+): Promise<string> {
+ if (!data || data.length === 0) {
+ return '<p class="text-gray-500">데이터가 없습니다.</p>';
+ }
+
+ const headerRow = columns
+ .map(
+ (col) =>
+ `<th class="border border-gray-300 px-4 py-2 bg-gray-100 font-semibold text-left">${col.label}</th>`
+ )
+ .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 `<td class="border border-gray-300 px-4 py-2">${displayValue}</td>`;
+ })
+ .join('');
+ return `<tr>${cells}</tr>`;
+ })
+ .join('');
+
+ return `
+ <table class="w-full border-collapse border border-gray-300 my-4">
+ <thead>
+ <tr>${headerRow}</tr>
+ </thead>
+ <tbody>
+ ${bodyRows}
+ </tbody>
+ </table>
+ `;
+}
+
+/**
+ * 배열을 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<string> {
+ if (!items || items.length === 0) {
+ return '<p class="text-gray-500">항목이 없습니다.</p>';
+ }
+
+ const listItems = items
+ .map((item) => `<li class="mb-1">${item}</li>`)
+ .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<string> {
+ if (!items || items.length === 0) {
+ return '<p class="text-gray-500">정보가 없습니다.</p>';
+ }
+
+ const listItems = items
+ .map(
+ (item) => `
+ <div class="flex border-b border-gray-200 py-2">
+ <dt class="w-1/3 font-semibold text-gray-700">${item.label}</dt>
+ <dd class="w-2/3 text-gray-900">${item.value}</dd>
+ </div>
+ `
+ )
+ .join('');
+
+ return `<dl class="my-4">${listItems}</dl>`;
+}
+