/** * 결재 템플릿 유틸리티 함수 * * 기능: * - 템플릿 이름으로 조회 * - 변수 치환 ({{변수명}}) * - 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 = { " 이름 ": "홍길동" }; // 공백 있어도 OK * const result = await replaceTemplateVariables(content, variables); * // "

홍길동님, 안녕하세요

" * ``` */ export async function replaceTemplateVariables( content: string, variables: Record ): Promise { let result = content; // 변수 키를 trim하여 정규화된 맵 생성 const normalizedVariables: Record = {}; Object.entries(variables).forEach(([key, value]) => { normalizedVariables[key.trim()] = value; }); // 템플릿에서 {{ 변수명 }} 패턴을 찾아 치환 // 공백을 허용하는 정규식: {{\s*변수명\s*}} Object.entries(normalizedVariables).forEach(([key, value]) => { // 변수명 앞뒤에 공백이 있을 수 있으므로 \s*를 추가 const pattern = new RegExp( `\\{\\{\\s*${escapeRegex(key)}\\s*\\}\\}`, '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 listStyle = ordered ? 'list-style-type: decimal; list-style-position: inside; margin: 16px 0; padding-left: 20px;' : 'list-style-type: disc; list-style-position: inside; margin: 16px 0; padding-left: 20px;'; return `<${tag} style="${listStyle}">${listItems}`; } /** * 키-값 쌍을 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}
    `; }