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}`; +} + +/** + * 키-값 쌍을 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