diff options
| author | joonhoekim <26rote@gmail.com> | 2025-10-23 18:13:41 +0900 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-10-23 18:13:41 +0900 |
| commit | 78c471eec35182959e0029ded18f144974ccaca2 (patch) | |
| tree | 914cdf1c8f406ca3e2aa639b8bb774f7f4e87023 /lib/approval/template-utils.ts | |
| parent | 0be8940580c4a4a4e098b649d198160f9b60420c (diff) | |
(김준회) 결재 템플릿 에디터 및 결재 워크플로 공통함수 작성, 실사의뢰 결재 연결 예시 작성
Diffstat (limited to 'lib/approval/template-utils.ts')
| -rw-r--r-- | lib/approval/template-utils.ts | 215 |
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>`; +} + |
