/** * 기본 리포트 템플릿 생성 스크립트 * 실행: cd backend-node && node scripts/create-report-templates.js * * 8가지 기본 템플릿: * 1. 견적서 (Quotation) * 2. 발주서 (Purchase Order) * 3. 수주 확인서 (Sales Order Confirmation) * 4. 거래명세서 (Delivery Note) * 5. 작업지시서 (Work Order) * 6. 검사 성적서 (Inspection Report) * 7. 세금계산서 (Tax Invoice) * 8. 생산계획 현황표 (Production Plan Status) */ const https = require("https"); const http = require("http"); const API_BASE = "http://localhost:8080/api"; // ─── JWT 토큰 획득 ───────────────────────────────────────────────────────────── async function getToken() { return new Promise((resolve, reject) => { const body = JSON.stringify({ userId: "wace", password: "qlalfqjsgh11" }); const req = http.request( { hostname: "localhost", port: 8080, path: "/api/auth/login", method: "POST", headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body), }, }, (res) => { let data = ""; res.on("data", (chunk) => (data += chunk)); res.on("end", () => { try { const json = JSON.parse(data); resolve(json.data?.token || json.token); } catch (e) { reject(new Error("Login failed: " + data)); } }); } ); req.on("error", reject); req.write(body); req.end(); }); } // ─── 템플릿 생성 API 호출 ──────────────────────────────────────────────────── async function createTemplate(token, templateData) { return new Promise((resolve, reject) => { const body = JSON.stringify(templateData); const req = http.request( { hostname: "localhost", port: 8080, path: "/api/admin/reports/templates", method: "POST", headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body), Authorization: `Bearer ${token}`, }, }, (res) => { let data = ""; res.on("data", (chunk) => (data += chunk)); res.on("end", () => { try { resolve(JSON.parse(data)); } catch (e) { reject(new Error("Parse error: " + data)); } }); } ); req.on("error", reject); req.write(body); req.end(); }); } // ─── 기존 시스템 템플릿 삭제 후 재생성 ──────────────────────────────────────── async function getTemplates(token) { return new Promise((resolve, reject) => { const req = http.request( { hostname: "localhost", port: 8080, path: "/api/admin/reports/templates", method: "GET", headers: { Authorization: `Bearer ${token}` }, }, (res) => { let data = ""; res.on("data", (chunk) => (data += chunk)); res.on("end", () => { try { resolve(JSON.parse(data)); } catch (e) { reject(new Error("Parse error: " + data)); } }); } ); req.on("error", reject); req.end(); }); } async function deleteTemplate(token, templateId) { return new Promise((resolve, reject) => { const req = http.request( { hostname: "localhost", port: 8080, path: `/api/admin/reports/templates/${templateId}`, method: "DELETE", headers: { Authorization: `Bearer ${token}` }, }, (res) => { let data = ""; res.on("data", (chunk) => (data += chunk)); res.on("end", () => resolve()); } ); req.on("error", reject); req.end(); }); } // ─── 헬퍼: 컴포넌트 ID 생성 ───────────────────────────────────────────────── let _idCounter = 0; function cid() { return `tpl_comp_${++_idCounter}`; } // ─── 공통 스타일 상수 ──────────────────────────────────────────────────────── const COLORS = { primary: "#1E3A5F", primaryLight: "#EFF6FF", border: "#E2E8F0", headerBorder: "#1E3A5F", textDark: "#0F172A", textGray: "#64748B", yellow: "#FEF3C7", yellowBorder: "#F59E0B", white: "#FFFFFF", rowAlt: "#F8F9FC", }; // ─── 공통 헤더 블록 컴포넌트 생성 ──────────────────────────────────────────── /** * 문서 상단 헤더 (진한 파란색 배경 + 제목 + 영문명 + 로고) * @param {string} titleKor - 한국어 제목 (예: "견 적 서") * @param {string} titleEng - 영문 부제목 (예: "QUOTATION") * @param {number} startZ - 시작 zIndex * @returns {object[]} 컴포넌트 배열 */ function makeHeader(titleKor, titleEng, startZ = 0) { return [ // 헤더 배경 { id: cid(), type: "text", x: 0, y: 0, width: 840, height: 100, zIndex: startZ, defaultValue: "", backgroundColor: COLORS.primary, borderWidth: 0, borderColor: COLORS.primary, fontSize: 14, fontColor: COLORS.white, textAlign: "left", padding: 0, }, // 한국어 제목 { id: cid(), type: "text", x: 48, y: 16, width: 500, height: 44, zIndex: startZ + 1, defaultValue: titleKor, backgroundColor: "transparent", borderWidth: 0, fontSize: 26, fontWeight: "bold", fontColor: COLORS.white, textAlign: "left", padding: 0, letterSpacing: 6, }, // 영문 부제목 { id: cid(), type: "text", x: 48, y: 64, width: 400, height: 26, zIndex: startZ + 1, defaultValue: titleEng, backgroundColor: "transparent", borderWidth: 0, fontSize: 14, fontColor: "rgba(255,255,255,0.85)", textAlign: "left", padding: 0, letterSpacing: 2, }, // 회사 로고 이미지 { id: cid(), type: "image", x: 744, y: 14, width: 72, height: 72, zIndex: startZ + 1, backgroundColor: COLORS.white, borderWidth: 0, borderRadius: 8, objectFit: "contain", imageUrl: "", imageAlt: "회사 로고", }, ]; } /** * 승인란 (결재란) - gridMode 테이블 * @param {string[]} roles - 결재 역할 배열 (예: ["담당", "검토", "승인"]) * @param {number} x - 우측 정렬 시작 x 좌표 * @param {number} y - y 좌표 * @param {number} startZ - 시작 zIndex * @returns {object} 컴포넌트 객체 */ function makeApprovalTable(roles, x, y, startZ = 5) { const cellW = 76; const totalW = cellW * roles.length; const headerH = 28; const bodyH = 72; const totalH = headerH + bodyH; const cells = []; // 헤더 행 (역할명) roles.forEach((role, i) => { cells.push({ id: cid(), row: 0, col: i, cellType: "static", value: role, align: "center", verticalAlign: "middle", fontWeight: "bold", fontSize: 13, backgroundColor: COLORS.primaryLight, textColor: COLORS.primary, borderStyle: "thin", }); }); // 본문 행 (인) roles.forEach((_, i) => { cells.push({ id: cid(), row: 1, col: i, cellType: "static", value: "(인)", align: "center", verticalAlign: "middle", fontSize: 13, backgroundColor: COLORS.white, textColor: COLORS.textGray, borderStyle: "thin", }); }); return { id: cid(), type: "table", x, y, width: totalW, height: totalH, zIndex: startZ, gridMode: true, gridRowCount: 2, gridColCount: roles.length, gridColWidths: roles.map(() => cellW), gridRowHeights: [headerH, bodyH], gridHeaderRows: 1, gridHeaderCols: 0, gridCells: cells, borderWidth: 1, borderColor: COLORS.primary, backgroundColor: COLORS.white, showBorder: true, }; } /** * 문서 정보 행 (문서번호, 일자 등 - gridMode 테이블) * 레이블(상단)과 값(하단)을 하나의 gridMode 테이블로 묶어 데이터 바인딩 * @param {object[]} fields - [{ label, fieldName, defaultValue }] * @param {number} x - 시작 x * @param {number} y - 시작 y * @param {number} colWidth - 열 너비 * @param {number} startZ * @returns {object} gridMode 테이블 컴포넌트 (단일 객체) */ function makeDocInfoRow(fields, x, y, colWidth = 140, startZ = 5, options = {}) { const totalW = colWidth * fields.length; const labelH = options.labelH || 22; const valueH = options.valueH || 26; const totalH = labelH + valueH; const cells = []; fields.forEach((f, i) => { cells.push({ id: cid(), row: 0, col: i, cellType: "static", value: f.label, align: "left", verticalAlign: "middle", fontSize: 13, fontWeight: "normal", backgroundColor: "transparent", textColor: COLORS.textGray, borderStyle: "none", }); cells.push({ id: cid(), row: 1, col: i, cellType: "field", field: f.fieldName, value: f.defaultValue || "", align: "left", verticalAlign: "middle", fontSize: 15, fontWeight: "normal", backgroundColor: "transparent", textColor: COLORS.textDark, borderStyle: "none", }); }); return { id: cid(), type: "table", x, y, width: totalW, height: totalH, zIndex: startZ, gridMode: true, gridRowCount: 2, gridColCount: fields.length, gridColWidths: fields.map(() => colWidth), gridRowHeights: [labelH, valueH], gridHeaderRows: 1, gridHeaderCols: 0, gridCells: cells, borderWidth: 0, borderColor: "transparent", backgroundColor: "transparent", showBorder: false, }; } /** * 섹션 헤더 (좌측 진한 파란색 보더 + 연한 파란색 배경) * @param {string} title - 섹션 제목 * @param {number} x, y, w * @param {number} startZ * @returns {object} 컴포넌트 */ function makeSectionHeader(title, x, y, w, startZ = 5) { return { id: cid(), type: "text", x, y, width: w, height: 32, zIndex: startZ, defaultValue: title, backgroundColor: COLORS.primaryLight, borderWidth: 0, borderColor: COLORS.primary, fontSize: 15, fontWeight: "bold", fontColor: COLORS.primary, textAlign: "left", padding: 8, }; } /** * 인포 그리드 (레이블-값 쌍 세로 나열 - gridMode 테이블) * 레이블(좌)과 값(우)을 하나의 gridMode 테이블로 묶어 데이터 바인딩 * @param {object[]} rows - [{ label, fieldName, defaultValue }] * @param {number} x, y, w * @param {number} labelW - 레이블 너비 * @param {number} rowH - 행 높이 * @param {number} startZ * @returns {object} gridMode 테이블 컴포넌트 (단일 객체) */ function makeInfoRows(rows, x, y, w, labelW = 88, rowH = 24, startZ = 5) { const valueW = w - labelW; const totalH = rowH * rows.length; const cells = []; rows.forEach((row, i) => { cells.push({ id: cid(), row: i, col: 0, cellType: "static", value: row.label, align: "left", verticalAlign: "middle", fontSize: 14, fontWeight: "normal", backgroundColor: "transparent", textColor: COLORS.textGray, borderStyle: "none", }); cells.push({ id: cid(), row: i, col: 1, cellType: "field", field: row.fieldName, value: row.defaultValue || "", align: "left", verticalAlign: "middle", fontSize: 14, fontWeight: "normal", backgroundColor: "transparent", textColor: COLORS.textDark, borderStyle: "none", }); }); return { id: cid(), type: "table", x, y, width: w, height: totalH, zIndex: startZ, gridMode: true, gridRowCount: rows.length, gridColCount: 2, gridColWidths: [labelW, valueW], gridRowHeights: rows.map(() => rowH), gridHeaderRows: 0, gridHeaderCols: 1, gridCells: cells, borderWidth: 0, borderColor: "transparent", backgroundColor: "transparent", showBorder: false, }; } /** * 요약 바 (공급가액, 세액, 합계 - 노란색 배경 gridMode 테이블) * 레이블(상단)과 값(하단)을 하나의 gridMode 테이블로 묶어 데이터 바인딩 * @param {object[]} items - [{ label, fieldName, queryId }] * @param {number} x, y, w, h * @param {number} startZ * @returns {object} gridMode 테이블 컴포넌트 (단일 객체) */ function makeSummaryBar(items, x, y, w, h = 48, startZ = 5) { const itemW = Math.floor(w / items.length); const labelH = 18; const valueH = h - labelH; const cells = []; items.forEach((item, i) => { const isLast = i === items.length - 1; cells.push({ id: cid(), row: 0, col: i, cellType: "static", value: item.label, align: isLast ? "right" : "left", verticalAlign: "bottom", fontSize: 11, fontWeight: "normal", backgroundColor: COLORS.yellow, textColor: COLORS.textGray, borderStyle: "none", }); cells.push({ id: cid(), row: 1, col: i, cellType: "field", field: item.fieldName, value: "₩0", align: "right", verticalAlign: "middle", fontSize: isLast ? 18 : 15, fontWeight: isLast ? "bold" : "normal", backgroundColor: COLORS.yellow, textColor: isLast ? COLORS.primary : COLORS.textDark, borderStyle: "none", }); }); return { id: cid(), type: "table", x, y, width: w, height: h, zIndex: startZ, gridMode: true, gridRowCount: 2, gridColCount: items.length, gridColWidths: items.map(() => itemW), gridRowHeights: [labelH, valueH], gridHeaderRows: 1, gridHeaderCols: 0, gridCells: cells, borderWidth: 1, borderColor: COLORS.yellowBorder, backgroundColor: COLORS.yellow, showBorder: true, }; } /** * 품목 테이블 (data-bound table) * @param {object[]} columns - tableColumns 배열 * @param {number} x, y, w, h * @param {string} queryId - 연결할 쿼리 ID (빈 문자열로 설정) * @param {number} startZ * @returns {object} 컴포넌트 */ function makeItemsTable(columns, x, y, w, h, queryId = "", startZ = 5) { return { id: cid(), type: "table", x, y, width: w, height: h, zIndex: startZ, queryId, tableColumns: columns, showFooter: true, headerBackgroundColor: COLORS.primaryLight, headerTextColor: COLORS.primary, showBorder: true, borderWidth: 1, borderColor: COLORS.primary, rowHeight: 32, fontSize: 14, }; } /** * 비고 섹션 (섹션 헤더 + 텍스트 영역) * @param {string} title - 섹션 제목 * @param {number} x, y, w, h * @param {number} startZ * @returns {object[]} 컴포넌트 배열 */ function makeNotesSection(title, x, y, w, h = 88, startZ = 5) { return [ makeSectionHeader(title, x, y, w, startZ), { id: cid(), type: "text", x, y: y + 36, width: w, height: h - 36, zIndex: startZ, defaultValue: "", fieldName: "notes", backgroundColor: COLORS.white, borderWidth: 1, borderColor: COLORS.border, borderRadius: 4, fontSize: 14, fontColor: COLORS.textGray, textAlign: "left", padding: 8, }, ]; } /** * 회사 직인 (오른쪽 하단) * @param {number} startZ * @returns {object} 컴포넌트 */ function makeCompanySeal(startZ = 10) { return { id: cid(), type: "stamp", x: 716, y: 1080, width: 88, height: 88, zIndex: startZ, showLabel: true, labelText: "(직인)", labelPosition: "top", personName: "", borderWidth: 2, borderColor: COLORS.primary, borderRadius: 50, backgroundColor: "transparent", }; } // ═══════════════════════════════════════════════════════════════════════════════ // 템플릿 1: 견적서 // ═══════════════════════════════════════════════════════════════════════════════ function buildQuotationTemplate() { const components = []; // 헤더 components.push(...makeHeader("견 적 서", "QUOTATION")); // 문서 정보 + 승인란 components.push( makeDocInfoRow( [ { label: "문서번호", fieldName: "doc_number", defaultValue: "" }, { label: "작성일자", fieldName: "doc_date", defaultValue: "" }, { label: "유효기간", fieldName: "valid_until", defaultValue: "" }, ], 40, 114, 152 ) ); components.push(makeApprovalTable(["담당", "검토", "승인"], 564, 112)); // 구분선 components.push({ id: cid(), type: "divider", x: 0, y: 210, width: 840, height: 2, zIndex: 5, orientation: "horizontal", lineStyle: "solid", lineWidth: 1, lineColor: COLORS.border, }); // 수신자 정보 (좌) const recipientX = 40, recipientY = 222; components.push(makeSectionHeader("수신자 정보", recipientX, recipientY, 370, 5)); components.push( makeInfoRows( [ { label: "회사명", fieldName: "recipient_company" }, { label: "담당자", fieldName: "recipient_manager" }, { label: "사업자번호", fieldName: "recipient_biz_no" }, { label: "연락처", fieldName: "recipient_tel" }, ], recipientX, recipientY + 38, 370 ) ); // 공급자 정보 (우) const supplierX = 432, supplierY = 222; components.push(makeSectionHeader("공급자 정보", supplierX, supplierY, 368, 5)); components.push( makeInfoRows( [ { label: "회사명", fieldName: "supplier_company" }, { label: "대표자", fieldName: "supplier_ceo" }, { label: "사업자번호", fieldName: "supplier_biz_no" }, { label: "연락처", fieldName: "supplier_tel" }, ], supplierX, supplierY + 38, 368 ) ); // 구분선 components.push({ id: cid(), type: "divider", x: 0, y: 362, width: 840, height: 2, zIndex: 5, orientation: "horizontal", lineStyle: "solid", lineWidth: 1, lineColor: COLORS.border, }); // 합계 요약 바 (높이 2배: 48 -> 96) components.push( makeSummaryBar( [ { label: "공급가액", fieldName: "total_supply" }, { label: "세액", fieldName: "total_tax" }, { label: "합계금액", fieldName: "total_amount" }, ], 40, 376, 760, 96 ) ); // 품목 테이블 (y 위치 조정: 438 -> 486) components.push( makeItemsTable( [ { field: "row_no", header: "No", width: 40, align: "center" }, { field: "item_name", header: "품명", width: 140, align: "left" }, { field: "item_spec", header: "규격", width: 100, align: "left" }, { field: "quantity", header: "수량", width: 60, align: "right", numberFormat: "comma" }, { field: "unit", header: "단위", width: 50, align: "center" }, { field: "unit_price", header: "단가", width: 90, align: "right", numberFormat: "comma" }, { field: "supply_amount", header: "공급가액", width: 100, align: "right", numberFormat: "comma" }, { field: "tax_amount", header: "세액", width: 80, align: "right", numberFormat: "comma" }, { field: "remarks", header: "비고", width: 100, align: "left" }, ], 40, 486, 760, 320 ) ); // 비고 섹션 (y 위치 조정, 높이 2배: 88 -> 176, 텍스트 크기 2배) components.push( ...makeNotesSection("비고 및 특이사항", 40, 820, 760, 176) ); // 직인 components.push(makeCompanySeal()); return components; } // ═══════════════════════════════════════════════════════════════════════════════ // 템플릿 1-B: 견적서 (영문 - Export Quotation) // ═══════════════════════════════════════════════════════════════════════════════ function buildExportQuotationTemplate() { const components = []; components.push(...makeHeader("QUOTATION", "EXPORT QUOTATION")); components.push( makeDocInfoRow( [ { label: "Doc No.", fieldName: "doc_number", defaultValue: "" }, { label: "Date", fieldName: "doc_date", defaultValue: "" }, { label: "Valid Until", fieldName: "valid_until", defaultValue: "" }, ], 40, 114, 152 ) ); components.push(makeApprovalTable(["Prepared", "Reviewed", "Approved"], 564, 112)); components.push({ id: cid(), type: "divider", x: 0, y: 210, width: 840, height: 2, zIndex: 5, orientation: "horizontal", lineStyle: "solid", lineWidth: 1, lineColor: COLORS.border, }); const recipientX = 40, recipientY = 222; components.push(makeSectionHeader("Ship To / Buyer", recipientX, recipientY, 370, 5)); components.push( makeInfoRows( [ { label: "Company", fieldName: "recipient_company" }, { label: "Contact", fieldName: "recipient_manager" }, { label: "Reg. No.", fieldName: "recipient_biz_no" }, { label: "Tel", fieldName: "recipient_tel" }, ], recipientX, recipientY + 38, 370 ) ); const supplierX = 432, supplierY = 222; components.push(makeSectionHeader("Supplier", supplierX, supplierY, 368, 5)); components.push( makeInfoRows( [ { label: "Company", fieldName: "supplier_company" }, { label: "CEO", fieldName: "supplier_ceo" }, { label: "Reg. No.", fieldName: "supplier_biz_no" }, { label: "Tel", fieldName: "supplier_tel" }, ], supplierX, supplierY + 38, 368 ) ); components.push({ id: cid(), type: "divider", x: 0, y: 362, width: 840, height: 2, zIndex: 5, orientation: "horizontal", lineStyle: "solid", lineWidth: 1, lineColor: COLORS.border, }); components.push( makeSummaryBar( [ { label: "Subtotal", fieldName: "total_supply" }, { label: "VAT", fieldName: "total_tax" }, { label: "Grand Total", fieldName: "total_amount" }, ], 40, 376, 760, 96 ) ); components.push( makeItemsTable( [ { field: "row_no", header: "No", width: 40, align: "center" }, { field: "item_name", header: "Description", width: 160, align: "left" }, { field: "item_spec", header: "Spec", width: 100, align: "left" }, { field: "quantity", header: "Qty", width: 60, align: "right", numberFormat: "comma" }, { field: "unit", header: "Unit", width: 50, align: "center" }, { field: "unit_price", header: "Unit Price", width: 90, align: "right", numberFormat: "comma" }, { field: "supply_amount", header: "Amount", width: 100, align: "right", numberFormat: "comma" }, { field: "tax_amount", header: "VAT", width: 80, align: "right", numberFormat: "comma" }, { field: "remarks", header: "Remarks", width: 80, align: "left" }, ], 40, 486, 760, 320 ) ); components.push( ...makeNotesSection("Terms & Conditions", 40, 820, 760, 176) ); components.push(makeCompanySeal()); return components; } // ═══════════════════════════════════════════════════════════════════════════════ // 템플릿 2: 발주서 // ═══════════════════════════════════════════════════════════════════════════════ function buildPurchaseOrderTemplate() { const components = []; components.push(...makeHeader("발 주 서", "PURCHASE ORDER")); // 문서 정보 + 승인란 (4명) - 테이블 1.5배 (labelH: 33, valueH: 39) components.push( makeDocInfoRow( [ { label: "발주번호", fieldName: "po_number" }, { label: "발주일자", fieldName: "po_date" }, { label: "납기일", fieldName: "delivery_date" }, ], 40, 114, 152, 5, { labelH: 33, valueH: 39 } ) ); components.push(makeApprovalTable(["담당", "부서장", "임원", "사장"], 512, 112)); components.push({ id: cid(), type: "divider", x: 0, y: 230, width: 840, height: 2, zIndex: 5, orientation: "horizontal", lineStyle: "solid", lineWidth: 1, lineColor: COLORS.border, }); // 발주처 정보 (좌) const vendorX = 40, vendorY = 242; components.push(makeSectionHeader("발주처 정보 (공급업체)", vendorX, vendorY, 370, 5)); components.push( makeInfoRows( [ { label: "업체명", fieldName: "vendor_company" }, { label: "대표자", fieldName: "vendor_ceo" }, { label: "사업자번호", fieldName: "vendor_biz_no" }, { label: "주소", fieldName: "vendor_address" }, { label: "연락처", fieldName: "vendor_tel" }, ], vendorX, vendorY + 38, 370 ) ); // 자사 정보 (우) const compX = 432, compY = 242; components.push(makeSectionHeader("자사 정보", compX, compY, 368, 5)); components.push( makeInfoRows( [ { label: "회사명", fieldName: "company_name" }, { label: "대표자", fieldName: "company_ceo" }, { label: "사업자번호", fieldName: "company_biz_no" }, { label: "납품장소", fieldName: "delivery_place" }, { label: "납기일", fieldName: "delivery_date" }, ], compX, compY + 38, 368 ) ); components.push({ id: cid(), type: "divider", x: 0, y: 410, width: 840, height: 2, zIndex: 5, orientation: "horizontal", lineStyle: "solid", lineWidth: 1, lineColor: COLORS.border, }); // 품목 테이블 components.push( makeItemsTable( [ { field: "row_no", header: "No", width: 36, align: "center" }, { field: "item_code", header: "품목코드", width: 88, align: "left" }, { field: "item_name", header: "품명", width: 120, align: "left" }, { field: "item_spec", header: "규격", width: 100, align: "left" }, { field: "unit", header: "단위", width: 48, align: "center" }, { field: "quantity", header: "수량", width: 64, align: "right", numberFormat: "comma" }, { field: "unit_price", header: "단가", width: 88, align: "right", numberFormat: "comma" }, { field: "amount", header: "금액", width: 100, align: "right", numberFormat: "comma" }, { field: "remarks", header: "비고", width: 116, align: "left" }, ], 40, 424, 760, 300 ) ); // 합계 (높이 2배: 48 -> 96) components.push( makeSummaryBar( [ { label: "공급가액", fieldName: "total_supply" }, { label: "세액(10%)", fieldName: "total_tax" }, { label: "합계", fieldName: "total_amount" }, ], 40, 738, 760, 96 ) ); // 발주 조건 (높이 2배: 88 -> 176) components.push( ...makeNotesSection("발주 조건 / 특기사항", 40, 848, 760, 176) ); components.push(makeCompanySeal()); return components; } // ═══════════════════════════════════════════════════════════════════════════════ // 템플릿 3: 수주 확인서 // ═══════════════════════════════════════════════════════════════════════════════ function buildSalesOrderTemplate() { const components = []; components.push(...makeHeader("수 주 확 인 서", "SALES ORDER CONFIRMATION")); // 문서 정보 + 승인란 components.push( makeDocInfoRow( [ { label: "수주번호", fieldName: "so_number" }, { label: "수주일자", fieldName: "so_date" }, { label: "납기일자", fieldName: "delivery_date" }, ], 40, 114, 152 ) ); components.push(makeApprovalTable(["담당", "검토", "승인"], 564, 112)); components.push({ id: cid(), type: "divider", x: 0, y: 210, width: 840, height: 2, zIndex: 5, orientation: "horizontal", lineStyle: "solid", lineWidth: 1, lineColor: COLORS.border, }); // 발주처 정보 (좌) const clientX = 40, clientY = 222; components.push(makeSectionHeader("발주처 정보", clientX, clientY, 370, 5)); components.push( makeInfoRows( [ { label: "회사명", fieldName: "client_company" }, { label: "사업자번호", fieldName: "client_biz_no" }, { label: "담당자", fieldName: "client_manager" }, { label: "연락처", fieldName: "client_tel" }, ], clientX, clientY + 38, 370 ) ); // 자사 정보 (우) const ownX = 432, ownY = 222; components.push(makeSectionHeader("자사 정보", ownX, ownY, 368, 5)); components.push( makeInfoRows( [ { label: "회사명", fieldName: "company_name" }, { label: "사업자번호", fieldName: "company_biz_no" }, { label: "대표자", fieldName: "company_ceo" }, { label: "연락처", fieldName: "company_tel" }, ], ownX, ownY + 38, 368 ) ); components.push({ id: cid(), type: "divider", x: 0, y: 362, width: 840, height: 2, zIndex: 5, orientation: "horizontal", lineStyle: "solid", lineWidth: 1, lineColor: COLORS.border, }); // 품목 테이블 components.push( makeItemsTable( [ { field: "row_no", header: "No", width: 36, align: "center" }, { field: "item_code", header: "품번", width: 80, align: "left" }, { field: "item_name", header: "품명", width: 120, align: "left" }, { field: "item_spec", header: "규격", width: 88, align: "left" }, { field: "quantity", header: "수량", width: 56, align: "right", numberFormat: "comma" }, { field: "unit", header: "단위", width: 48, align: "center" }, { field: "unit_price", header: "단가", width: 80, align: "right", numberFormat: "comma" }, { field: "amount", header: "금액", width: 96, align: "right", numberFormat: "comma" }, { field: "delivery_date", header: "납기", width: 72, align: "center" }, { field: "remarks", header: "비고", width: 84, align: "left" }, ], 40, 376, 760, 320 ) ); // 합계 (높이 2배: 48 -> 96, 글씨 크기 키움 + 볼드) const soTotalCells = [ { id: cid(), row: 0, col: 0, cellType: "static", value: "수주 합계 금액", align: "left", verticalAlign: "bottom", fontSize: 15, fontWeight: "bold", backgroundColor: COLORS.yellow, textColor: COLORS.textGray, borderStyle: "none", }, { id: cid(), row: 1, col: 0, cellType: "field", field: "total_amount", value: "₩0", align: "right", verticalAlign: "middle", fontSize: 24, fontWeight: "bold", backgroundColor: COLORS.yellow, textColor: COLORS.primary, borderStyle: "none", }, ]; components.push({ id: cid(), type: "table", x: 40, y: 710, width: 760, height: 96, zIndex: 5, gridMode: true, gridRowCount: 2, gridColCount: 1, gridColWidths: [760], gridRowHeights: [30, 66], gridHeaderRows: 1, gridHeaderCols: 0, gridCells: soTotalCells, borderWidth: 1, borderColor: COLORS.yellowBorder, backgroundColor: COLORS.yellow, showBorder: true, }); // 특기사항 (높이 2배: 88 -> 176) components.push( ...makeNotesSection("특기사항 / 납품조건", 40, 820, 760, 176) ); components.push(makeCompanySeal()); return components; } // ═══════════════════════════════════════════════════════════════════════════════ // 템플릿 4: 거래명세서 // ═══════════════════════════════════════════════════════════════════════════════ function buildDeliveryNoteTemplate() { const components = []; // 헤더 (중앙 정렬) components.push({ id: cid(), type: "text", x: 0, y: 0, width: 840, height: 88, zIndex: 0, defaultValue: "", backgroundColor: COLORS.primary, borderWidth: 0, fontSize: 14, fontColor: COLORS.white, padding: 0, }); components.push({ id: cid(), type: "text", x: 40, y: 12, width: 760, height: 42, zIndex: 1, defaultValue: "거래명세서", backgroundColor: "transparent", borderWidth: 0, fontSize: 28, fontWeight: "bold", fontColor: COLORS.white, textAlign: "center", padding: 0, letterSpacing: 8, }); components.push({ id: cid(), type: "text", x: 40, y: 58, width: 760, height: 24, zIndex: 1, defaultValue: "TRANSACTION STATEMENT", backgroundColor: "transparent", borderWidth: 0, fontSize: 14, fontColor: "rgba(255,255,255,0.85)", textAlign: "center", padding: 0, }); // 문서 정보 행 (높이 확대: labelH: 28, valueH: 36 으로 데이터 맵핑 공간 확보) components.push( makeDocInfoRow( [ { label: "문서번호", fieldName: "doc_number" }, { label: "거래일자", fieldName: "doc_date" }, ], 40, 104, 200, 5, { labelH: 28, valueH: 36 } ) ); // 보관 구분 배지 components.push({ id: cid(), type: "text", x: 648, y: 112, width: 152, height: 30, zIndex: 5, defaultValue: "공급자 보관용", backgroundColor: COLORS.primaryLight, borderWidth: 1, borderColor: COLORS.primary, borderRadius: 4, fontSize: 14, fontColor: COLORS.primary, textAlign: "center", padding: 4, }); components.push({ id: cid(), type: "divider", x: 0, y: 178, width: 840, height: 3, zIndex: 5, orientation: "horizontal", lineStyle: "solid", lineWidth: 2, lineColor: COLORS.primary, }); // 공급자 정보 (좌) - y 위치 조정 const supplierX = 40, supplierY = 190; components.push(makeSectionHeader("공급자 정보", supplierX, supplierY, 360, 5)); components.push( makeInfoRows( [ { label: "등록번호", fieldName: "supplier_biz_no" }, { label: "상호", fieldName: "supplier_company" }, { label: "대표자", fieldName: "supplier_ceo" }, { label: "사업장주소", fieldName: "supplier_address" }, { label: "업태", fieldName: "supplier_business_type" }, { label: "종목", fieldName: "supplier_item" }, ], supplierX, supplierY + 32, 360, 88, 20, 5 ) ); // 공급받는자 정보 (우) - y 위치 조정 const buyerX = 432, buyerY = 190; components.push(makeSectionHeader("공급받는자 정보", buyerX, buyerY, 368, 5)); components.push( makeInfoRows( [ { label: "등록번호", fieldName: "buyer_biz_no" }, { label: "상호", fieldName: "buyer_company" }, { label: "대표자", fieldName: "buyer_ceo" }, { label: "사업장주소", fieldName: "buyer_address" }, { label: "업태", fieldName: "buyer_business_type" }, { label: "종목", fieldName: "buyer_item" }, ], buyerX, buyerY + 32, 368, 88, 20, 5 ) ); components.push({ id: cid(), type: "divider", x: 0, y: 350, width: 840, height: 2, zIndex: 5, orientation: "horizontal", lineStyle: "solid", lineWidth: 1, lineColor: COLORS.border, }); // 품목 테이블 - y 위치 조정 components.push( makeItemsTable( [ { field: "row_no", header: "No", width: 40, align: "center" }, { field: "date_md", header: "월일", width: 56, align: "center" }, { field: "item_name", header: "품목", width: 140, align: "left" }, { field: "item_spec", header: "규격", width: 100, align: "left" }, { field: "quantity", header: "수량", width: 64, align: "right", numberFormat: "comma" }, { field: "unit_price", header: "단가", width: 88, align: "right", numberFormat: "comma" }, { field: "supply_amount", header: "공급가액", width: 108, align: "right", numberFormat: "comma" }, { field: "tax_amount", header: "세액", width: 80, align: "right", numberFormat: "comma" }, { field: "remarks", header: "비고", width: 84, align: "left" }, ], 40, 362, 760, 300 ) ); // 합계 박스 (진한 파란색 - gridMode 테이블) - y 위치 조정 const totalCells = [ { id: cid(), row: 0, col: 0, cellType: "static", value: "공급가액", align: "center", verticalAlign: "bottom", fontSize: 12, fontWeight: "normal", backgroundColor: COLORS.primary, textColor: "rgba(255,255,255,0.75)", borderStyle: "none" }, { id: cid(), row: 0, col: 1, cellType: "static", value: "세액", align: "center", verticalAlign: "bottom", fontSize: 12, fontWeight: "normal", backgroundColor: COLORS.primary, textColor: "rgba(255,255,255,0.75)", borderStyle: "none" }, { id: cid(), row: 0, col: 2, cellType: "static", value: "합계금액", align: "right", verticalAlign: "bottom", fontSize: 12, fontWeight: "normal", backgroundColor: COLORS.primary, textColor: "rgba(255,255,255,0.75)", borderStyle: "none" }, { id: cid(), row: 1, col: 0, cellType: "field", field: "total_supply", value: "₩0", align: "center", verticalAlign: "middle", fontSize: 16, fontWeight: "normal", backgroundColor: COLORS.primary, textColor: COLORS.white, borderStyle: "none" }, { id: cid(), row: 1, col: 1, cellType: "field", field: "total_tax", value: "₩0", align: "center", verticalAlign: "middle", fontSize: 16, fontWeight: "normal", backgroundColor: COLORS.primary, textColor: COLORS.white, borderStyle: "none" }, { id: cid(), row: 1, col: 2, cellType: "field", field: "total_amount", value: "₩0", align: "right", verticalAlign: "middle", fontSize: 22, fontWeight: "bold", backgroundColor: COLORS.primary, textColor: COLORS.white, borderStyle: "none" }, ]; components.push({ id: cid(), type: "table", x: 40, y: 674, width: 760, height: 68, zIndex: 5, gridMode: true, gridRowCount: 2, gridColCount: 3, gridColWidths: [200, 200, 360], gridRowHeights: [20, 48], gridHeaderRows: 1, gridHeaderCols: 0, gridCells: totalCells, borderWidth: 0, borderColor: COLORS.primary, backgroundColor: COLORS.primary, showBorder: false, }); // 인수 서명란 (서명을 텍스트 박스 안에 겹치게 배치) components.push({ id: cid(), type: "text", x: 40, y: 756, width: 760, height: 76, zIndex: 5, defaultValue: "상기 금액을 정히 인수함", backgroundColor: COLORS.white, borderWidth: 2, borderColor: COLORS.border, borderRadius: 4, fontSize: 15, fontColor: COLORS.textGray, textAlign: "left", padding: 12, }); components.push({ id: cid(), type: "signature", x: 600, y: 762, width: 180, height: 64, zIndex: 6, showLabel: true, labelText: "(인수자 서명)", labelPosition: "top", borderWidth: 1, borderColor: COLORS.primary, }); // 직인 components.push(makeCompanySeal()); return components; } // ═══════════════════════════════════════════════════════════════════════════════ // 템플릿 5: 작업지시서 // ═══════════════════════════════════════════════════════════════════════════════ function buildWorkOrderTemplate() { const components = []; // 헤더 (바코드 영역 포함) components.push({ id: cid(), type: "text", x: 0, y: 0, width: 840, height: 100, zIndex: 0, defaultValue: "", backgroundColor: COLORS.primary, borderWidth: 0, fontSize: 14, fontColor: COLORS.white, padding: 0, }); components.push({ id: cid(), type: "text", x: 48, y: 16, width: 480, height: 44, zIndex: 1, defaultValue: "작업지시서", backgroundColor: "transparent", borderWidth: 0, fontSize: 26, fontWeight: "bold", fontColor: COLORS.white, textAlign: "left", padding: 0, letterSpacing: 4, }); components.push({ id: cid(), type: "text", x: 48, y: 64, width: 400, height: 26, zIndex: 1, defaultValue: "WORK ORDER", backgroundColor: "transparent", borderWidth: 0, fontSize: 14, fontColor: "rgba(255,255,255,0.85)", textAlign: "left", padding: 0, }); // 바코드 components.push({ id: cid(), type: "barcode", x: 720, y: 8, width: 104, height: 80, zIndex: 1, barcodeType: "CODE128", barcodeValue: "WO-000000", barcodeFieldName: "wo_number", showBarcodeText: true, barcodeColor: COLORS.primary, barcodeBackground: COLORS.white, backgroundColor: COLORS.white, borderRadius: 4, padding: 4, }); // 승인란 components.push(makeApprovalTable(["작성", "확인", "승인"], 568, 108)); // 구분선 - 인장 밑 공간 확보 (y: 196 -> 236, +40px) components.push({ id: cid(), type: "divider", x: 0, y: 236, width: 840, height: 2, zIndex: 5, orientation: "horizontal", lineStyle: "solid", lineWidth: 1, lineColor: COLORS.border, }); // 작업지시 정보 카드 (전체 40px 아래로) const woInfoY = 246; components.push(makeSectionHeader("작업지시 정보", 40, woInfoY, 760, 5)); components.push( makeDocInfoRow( [ { label: "지시번호", fieldName: "wo_number" }, { label: "지시일자", fieldName: "wo_date" }, { label: "생산라인", fieldName: "production_line" }, { label: "담당자", fieldName: "manager_name" }, { label: "납기일", fieldName: "due_date" }, ], 40, woInfoY + 32, 152 ) ); components.push({ id: cid(), type: "divider", x: 0, y: 330, width: 840, height: 2, zIndex: 5, orientation: "horizontal", lineStyle: "solid", lineWidth: 1, lineColor: COLORS.border, }); // 제품 정보 카드 const prodInfoY = 340; components.push(makeSectionHeader("제품 정보", 40, prodInfoY, 760, 5)); components.push( makeDocInfoRow( [ { label: "품번", fieldName: "item_code" }, { label: "품명", fieldName: "item_name" }, { label: "규격", fieldName: "item_spec" }, { label: "지시수량", fieldName: "order_qty" }, { label: "단위", fieldName: "unit" }, ], 40, prodInfoY + 32, 152 ) ); components.push({ id: cid(), type: "divider", x: 0, y: 430, width: 840, height: 2, zIndex: 5, orientation: "horizontal", lineStyle: "solid", lineWidth: 1, lineColor: COLORS.border, }); // 자재 소요 테이블 const matY = 440; components.push(makeSectionHeader("자재 소요 내역", 40, matY, 760, 5)); components.push( makeItemsTable( [ { field: "row_no", header: "No", width: 40, align: "center" }, { field: "mat_code", header: "자재코드", width: 88, align: "left" }, { field: "mat_name", header: "자재명", width: 150, align: "left" }, { field: "mat_spec", header: "규격", width: 100, align: "left" }, { field: "required_qty", header: "소요량", width: 80, align: "right", numberFormat: "comma" }, { field: "unit", header: "단위", width: 56, align: "center" }, { field: "stock_qty", header: "재고량", width: 80, align: "right", numberFormat: "comma" }, { field: "remarks", header: "비고", width: 166, align: "left" }, ], 40, matY + 32, 760, 200 ) ); // 작업 공정 테이블 const procY = 680; components.push(makeSectionHeader("작업 공정", 40, procY, 760, 5)); components.push( makeItemsTable( [ { field: "proc_seq", header: "순서", width: 56, align: "center" }, { field: "proc_name", header: "공정명", width: 150, align: "left" }, { field: "equipment", header: "설비", width: 140, align: "left" }, { field: "work_time", header: "작업시간", width: 88, align: "center" }, { field: "worker", header: "작업자", width: 100, align: "center" }, { field: "remarks", header: "비고", width: 226, align: "left" }, ], 40, procY + 32, 760, 180 ) ); // 특기사항 (높이 2배: 80 -> 160) components.push( ...makeNotesSection("특기사항 / 주의사항", 40, 900, 760, 160) ); // 작성자 정보 (좌하단) components.push({ id: cid(), type: "text", x: 40, y: 1110, width: 400, height: 28, zIndex: 5, defaultValue: "작성자: ", fieldName: "created_by", backgroundColor: "transparent", borderWidth: 0, fontSize: 13, fontColor: COLORS.textGray, textAlign: "left", padding: 0, }); // QR 코드 components.push({ id: cid(), type: "barcode", x: 720, y: 1080, width: 80, height: 80, zIndex: 5, barcodeType: "QR", barcodeValue: "", barcodeFieldName: "wo_number", showBarcodeText: false, barcodeColor: COLORS.primary, barcodeBackground: COLORS.white, }); return components; } // ═══════════════════════════════════════════════════════════════════════════════ // 템플릿 6: 검사 성적서 // ═══════════════════════════════════════════════════════════════════════════════ function buildInspectionReportTemplate() { const components = []; // 헤더 (합격 배지 - conditionalStyles로 배경색 동적 변경) components.push(...makeHeader("검 사 성 적 서", "INSPECTION REPORT")); components.push({ id: cid(), type: "text", x: 660, y: 33, width: 76, height: 30, zIndex: 2, defaultValue: "", fieldName: "inspection_result", backgroundColor: "#16A34A", borderWidth: 0, borderRadius: 8, fontSize: 15, fontWeight: "bold", fontColor: COLORS.white, textAlign: "center", padding: 4, conditionalStyles: [ { value: "합격", backgroundColor: "#16A34A", fontColor: "#FFFFFF" }, { value: "불합격", backgroundColor: "#DC2626", fontColor: "#FFFFFF" }, { value: "조건부합격", backgroundColor: "#F59E0B", fontColor: "#FFFFFF" }, ], }); // 승인란 components.push(makeApprovalTable(["검사자", "확인", "승인"], 568, 108)); // 구분선 - 인장 밑 공간 확보 (y: 196 -> 236, +40px) components.push({ id: cid(), type: "divider", x: 0, y: 236, width: 840, height: 2, zIndex: 5, orientation: "horizontal", lineStyle: "solid", lineWidth: 1, lineColor: COLORS.border, }); // 검사 정보 카드 (데이터 공간 확보: labelH: 28, valueH: 36) const insInfoY = 246; components.push(makeSectionHeader("검사 정보", 40, insInfoY, 760, 5)); components.push( makeDocInfoRow( [ { label: "검사번호", fieldName: "ins_number" }, { label: "검사일자", fieldName: "ins_date" }, { label: "검사유형", fieldName: "ins_type" }, { label: "검사자", fieldName: "inspector_name" }, ], 40, insInfoY + 32, 190, 5, { labelH: 28, valueH: 36 } ) ); components.push({ id: cid(), type: "divider", x: 0, y: 350, width: 840, height: 2, zIndex: 5, orientation: "horizontal", lineStyle: "solid", lineWidth: 1, lineColor: COLORS.border, }); // 제품 정보 카드 (데이터 공간 확보: labelH: 28, valueH: 36) const prodInfoY = 360; components.push(makeSectionHeader("제품 정보", 40, prodInfoY, 760, 5)); components.push( makeDocInfoRow( [ { label: "품번", fieldName: "item_code" }, { label: "품명", fieldName: "item_name" }, { label: "로트번호", fieldName: "lot_number" }, { label: "검사수량", fieldName: "ins_qty" }, { label: "규격", fieldName: "item_spec" }, ], 40, prodInfoY + 32, 152, 5, { labelH: 28, valueH: 36 } ) ); components.push({ id: cid(), type: "divider", x: 0, y: 464, width: 840, height: 2, zIndex: 5, orientation: "horizontal", lineStyle: "solid", lineWidth: 1, lineColor: COLORS.border, }); // 검사 항목 테이블 const insTableY = 474; components.push(makeSectionHeader("검사 항목 및 결과", 40, insTableY, 760, 5)); components.push( makeItemsTable( [ { field: "row_no", header: "No", width: 36, align: "center" }, { field: "ins_category", header: "검사항목", width: 100, align: "left" }, { field: "ins_detail", header: "세부항목", width: 110, align: "left" }, { field: "ins_method", header: "검사방법", width: 88, align: "center" }, { field: "standard_value", header: "기준값", width: 80, align: "center" }, { field: "measured_1", header: "측정값-1", width: 80, align: "center" }, { field: "measured_2", header: "측정값-2", width: 80, align: "center" }, { field: "result", header: "판정", width: 72, align: "center" }, { field: "remarks", header: "비고", width: 114, align: "left" }, ], 40, insTableY + 32, 760, 260 ) ); // 종합 판정 박스 (체크박스 1행 배치, 동적 값 연동) const judgeY = 774; components.push({ id: cid(), type: "text", x: 40, y: judgeY, width: 760, height: 76, zIndex: 5, defaultValue: "종합 판정", backgroundColor: "#F0FDF4", borderWidth: 2, borderColor: "#16A34A", borderRadius: 8, fontSize: 14, fontColor: COLORS.textGray, textAlign: "left", padding: 10, }); // 체크박스 1행 배치 (가로 간격 충분히 확보) components.push({ id: cid(), type: "checkbox", x: 60, y: judgeY + 34, width: 80, height: 28, zIndex: 6, checkboxChecked: true, checkboxLabel: "합격", checkboxColor: "#16A34A", checkboxLabelPosition: "right", checkboxSize: 20, backgroundColor: "transparent", borderWidth: 0, fontSize: 15, fontColor: "#16A34A", fieldName: "check_pass", }); components.push({ id: cid(), type: "checkbox", x: 170, y: judgeY + 34, width: 90, height: 28, zIndex: 6, checkboxChecked: false, checkboxLabel: "불합격", checkboxColor: "#DC2626", checkboxLabelPosition: "right", checkboxSize: 20, backgroundColor: "transparent", borderWidth: 0, fontSize: 15, fontColor: "#DC2626", fieldName: "check_fail", }); components.push({ id: cid(), type: "checkbox", x: 290, y: judgeY + 34, width: 120, height: 28, zIndex: 6, checkboxChecked: false, checkboxLabel: "조건부합격", checkboxColor: COLORS.yellowBorder, checkboxLabelPosition: "right", checkboxSize: 20, backgroundColor: "transparent", borderWidth: 0, fontSize: 15, fontColor: COLORS.yellowBorder, fieldName: "check_conditional", }); // 종합 판정 결과 (우측) - conditionalStyles로 글자색 동적 변경 components.push({ id: cid(), type: "text", x: 580, y: judgeY + 8, width: 180, height: 60, zIndex: 6, defaultValue: "", fieldName: "inspection_result", backgroundColor: "transparent", borderWidth: 0, fontSize: 30, fontWeight: "bold", fontColor: "#16A34A", textAlign: "right", padding: 4, conditionalStyles: [ { value: "합격", fontColor: "#16A34A" }, { value: "불합격", fontColor: "#DC2626" }, { value: "조건부합격", fontColor: "#F59E0B" }, ], }); // 비고 (높이 2배: 72 -> 144) components.push( ...makeNotesSection("비고 / 특이사항", 40, 858, 760, 144) ); // 서명란 (검사자, 승인자) - placeholder 빈 상태 const sigY = 1020; components.push({ id: cid(), type: "text", x: 40, y: sigY, width: 200, height: 20, zIndex: 5, defaultValue: "검사자 서명", backgroundColor: "transparent", borderWidth: 0, fontSize: 12, fontColor: COLORS.textGray, textAlign: "center", padding: 0, }); components.push({ id: cid(), type: "signature", x: 40, y: sigY + 22, width: 200, height: 64, zIndex: 5, showLabel: false, labelText: "", borderWidth: 1, borderColor: COLORS.border, }); components.push({ id: cid(), type: "text", x: 40, y: sigY + 90, width: 200, height: 22, zIndex: 5, defaultValue: "", fieldName: "inspector_name", backgroundColor: "transparent", borderWidth: 0, fontSize: 14, fontColor: COLORS.textGray, textAlign: "center", padding: 0, }); components.push({ id: cid(), type: "text", x: 596, y: sigY, width: 200, height: 20, zIndex: 5, defaultValue: "승인자 서명", backgroundColor: "transparent", borderWidth: 0, fontSize: 12, fontColor: COLORS.textGray, textAlign: "center", padding: 0, }); components.push({ id: cid(), type: "signature", x: 596, y: sigY + 22, width: 200, height: 64, zIndex: 5, showLabel: false, labelText: "", borderWidth: 1, borderColor: COLORS.border, }); components.push({ id: cid(), type: "text", x: 596, y: sigY + 90, width: 200, height: 22, zIndex: 5, defaultValue: "", fieldName: "approver_name", backgroundColor: "transparent", borderWidth: 0, fontSize: 14, fontColor: COLORS.textGray, textAlign: "center", padding: 0, }); return components; } // ═══════════════════════════════════════════════════════════════════════════════ // 템플릿 7: 세금계산서 // ═══════════════════════════════════════════════════════════════════════════════ function buildTaxInvoiceTemplate() { const components = []; // 헤더 (중앙 정렬) components.push({ id: cid(), type: "text", x: 0, y: 0, width: 840, height: 80, zIndex: 0, defaultValue: "", backgroundColor: COLORS.primary, borderWidth: 0, fontSize: 14, fontColor: COLORS.white, padding: 0, }); components.push({ id: cid(), type: "text", x: 40, y: 10, width: 760, height: 40, zIndex: 1, defaultValue: "세금계산서", backgroundColor: "transparent", borderWidth: 0, fontSize: 28, fontWeight: "bold", fontColor: COLORS.white, textAlign: "center", padding: 0, letterSpacing: 8, }); components.push({ id: cid(), type: "text", x: 40, y: 52, width: 760, height: 22, zIndex: 1, defaultValue: "(공급자 보관용)", backgroundColor: "transparent", borderWidth: 0, fontSize: 14, fontColor: "rgba(255,255,255,0.85)", textAlign: "center", padding: 0, }); // 승인번호 + 작성일자 행 (높이 2배: 44 -> 88) const approvalRowCells = [ { id: cid(), row: 0, col: 0, cellType: "static", value: "승인번호", align: "left", verticalAlign: "middle", fontSize: 12, fontWeight: "normal", backgroundColor: COLORS.rowAlt, textColor: COLORS.textGray, borderStyle: "none" }, { id: cid(), row: 0, col: 1, cellType: "static", value: "작성일자", align: "left", verticalAlign: "middle", fontSize: 12, fontWeight: "normal", backgroundColor: COLORS.rowAlt, textColor: COLORS.textGray, borderStyle: "none" }, { id: cid(), row: 0, col: 2, cellType: "static", value: "", align: "right", verticalAlign: "middle", fontSize: 12, backgroundColor: COLORS.rowAlt, textColor: COLORS.textGray, borderStyle: "none" }, { id: cid(), row: 1, col: 0, cellType: "field", field: "approval_number", value: "", align: "left", verticalAlign: "middle", fontSize: 18, fontWeight: "normal", backgroundColor: COLORS.rowAlt, textColor: COLORS.textDark, borderStyle: "none" }, { id: cid(), row: 1, col: 1, cellType: "field", field: "issue_date", value: "", align: "left", verticalAlign: "middle", fontSize: 18, fontWeight: "normal", backgroundColor: COLORS.rowAlt, textColor: COLORS.textDark, borderStyle: "none" }, { id: cid(), row: 1, col: 2, cellType: "static", value: "전자세금계산서", align: "right", verticalAlign: "middle", fontSize: 14, fontWeight: "normal", backgroundColor: COLORS.rowAlt, textColor: "#2563EB", borderStyle: "none" }, ]; components.push({ id: cid(), type: "table", x: 0, y: 80, width: 840, height: 88, zIndex: 5, gridMode: true, gridRowCount: 2, gridColCount: 3, gridColWidths: [300, 300, 240], gridRowHeights: [28, 60], gridHeaderRows: 1, gridHeaderCols: 0, gridCells: approvalRowCells, borderWidth: 0, borderColor: "transparent", backgroundColor: COLORS.rowAlt, showBorder: false, }); // 공급가액 / 세액 요약 (크기 키움: 64 -> 96) components.push({ id: cid(), type: "divider", x: 0, y: 168, width: 840, height: 2, zIndex: 5, orientation: "horizontal", lineStyle: "solid", lineWidth: 2, lineColor: COLORS.primary, }); const taxSummaryCells = [ { id: cid(), row: 0, col: 0, cellType: "static", value: "공급가액", align: "center", verticalAlign: "bottom", fontSize: 14, fontWeight: "normal", backgroundColor: "transparent", textColor: COLORS.textGray, borderStyle: "none" }, { id: cid(), row: 0, col: 1, cellType: "static", value: "세액", align: "center", verticalAlign: "bottom", fontSize: 14, fontWeight: "normal", backgroundColor: "transparent", textColor: COLORS.textGray, borderStyle: "none" }, { id: cid(), row: 1, col: 0, cellType: "field", field: "total_supply", value: "₩0", align: "center", verticalAlign: "middle", fontSize: 26, fontWeight: "bold", backgroundColor: "transparent", textColor: COLORS.textDark, borderStyle: "none" }, { id: cid(), row: 1, col: 1, cellType: "field", field: "total_tax", value: "₩0", align: "center", verticalAlign: "middle", fontSize: 26, fontWeight: "bold", backgroundColor: "transparent", textColor: COLORS.primary, borderStyle: "none" }, ]; components.push({ id: cid(), type: "table", x: 40, y: 178, width: 760, height: 96, zIndex: 5, gridMode: true, gridRowCount: 2, gridColCount: 2, gridColWidths: [380, 380], gridRowHeights: [28, 68], gridHeaderRows: 1, gridHeaderCols: 0, gridCells: taxSummaryCells, borderWidth: 0, borderColor: "transparent", backgroundColor: "transparent", showBorder: false, }); components.push({ id: cid(), type: "divider", x: 0, y: 274, width: 840, height: 2, zIndex: 5, orientation: "horizontal", lineStyle: "solid", lineWidth: 2, lineColor: COLORS.primary, }); // 공급자 정보 (간격 2배 확보: 기존 대비 아래로 이동) const supY = 290; components.push(makeSectionHeader("공급자", 40, supY, 760, 5)); const supCells = [ { id: cid(), row: 0, col: 0, cellType: "static", value: "등록번호", align: "center", fontWeight: "bold", backgroundColor: COLORS.primaryLight, textColor: COLORS.primary, borderStyle: "thin" }, { id: cid(), row: 0, col: 1, cellType: "field", field: "supplier_biz_no", align: "left", borderStyle: "thin", colSpan: 3 }, { id: cid(), row: 0, col: 2, cellType: "static", value: "", merged: true, mergedBy: "r0c1", borderStyle: "thin" }, { id: cid(), row: 0, col: 3, cellType: "static", value: "", merged: true, mergedBy: "r0c1", borderStyle: "thin" }, { id: cid(), row: 1, col: 0, cellType: "static", value: "상호(법인명)", align: "center", fontWeight: "bold", backgroundColor: COLORS.primaryLight, textColor: COLORS.primary, borderStyle: "thin" }, { id: cid(), row: 1, col: 1, cellType: "field", field: "supplier_company", align: "left", borderStyle: "thin" }, { id: cid(), row: 1, col: 2, cellType: "static", value: "성명(대표자)", align: "center", fontWeight: "bold", backgroundColor: COLORS.primaryLight, textColor: COLORS.primary, borderStyle: "thin" }, { id: cid(), row: 1, col: 3, cellType: "field", field: "supplier_ceo", align: "left", borderStyle: "thin" }, { id: cid(), row: 2, col: 0, cellType: "static", value: "사업장주소", align: "center", fontWeight: "bold", backgroundColor: COLORS.primaryLight, textColor: COLORS.primary, borderStyle: "thin" }, { id: cid(), row: 2, col: 1, cellType: "field", field: "supplier_address", align: "left", borderStyle: "thin", colSpan: 3 }, { id: cid(), row: 2, col: 2, cellType: "static", value: "", merged: true, mergedBy: "r2c1", borderStyle: "thin" }, { id: cid(), row: 2, col: 3, cellType: "static", value: "", merged: true, mergedBy: "r2c1", borderStyle: "thin" }, { id: cid(), row: 3, col: 0, cellType: "static", value: "업태", align: "center", fontWeight: "bold", backgroundColor: COLORS.primaryLight, textColor: COLORS.primary, borderStyle: "thin" }, { id: cid(), row: 3, col: 1, cellType: "field", field: "supplier_business_type", align: "left", borderStyle: "thin" }, { id: cid(), row: 3, col: 2, cellType: "static", value: "종목", align: "center", fontWeight: "bold", backgroundColor: COLORS.primaryLight, textColor: COLORS.primary, borderStyle: "thin" }, { id: cid(), row: 3, col: 3, cellType: "field", field: "supplier_item", align: "left", borderStyle: "thin" }, ]; components.push({ id: cid(), type: "table", x: 40, y: supY + 36, width: 760, height: 140, zIndex: 5, gridMode: true, gridRowCount: 4, gridColCount: 4, gridColWidths: [130, 250, 130, 250], gridRowHeights: [34, 34, 34, 34], gridHeaderRows: 0, gridHeaderCols: 1, gridCells: supCells, borderWidth: 2, borderColor: COLORS.primary, showBorder: true, fontSize: 14, }); // 공급받는자 정보 (간격 2배) const buyY = supY + 36 + 140 + 32; components.push(makeSectionHeader("공급받는자", 40, buyY, 760, 5)); const buyCells = [ { id: cid(), row: 0, col: 0, cellType: "static", value: "등록번호", align: "center", fontWeight: "bold", backgroundColor: COLORS.primaryLight, textColor: COLORS.primary, borderStyle: "thin" }, { id: cid(), row: 0, col: 1, cellType: "field", field: "buyer_biz_no", align: "left", borderStyle: "thin", colSpan: 3 }, { id: cid(), row: 0, col: 2, cellType: "static", value: "", merged: true, mergedBy: "r0c1", borderStyle: "thin" }, { id: cid(), row: 0, col: 3, cellType: "static", value: "", merged: true, mergedBy: "r0c1", borderStyle: "thin" }, { id: cid(), row: 1, col: 0, cellType: "static", value: "상호(법인명)", align: "center", fontWeight: "bold", backgroundColor: COLORS.primaryLight, textColor: COLORS.primary, borderStyle: "thin" }, { id: cid(), row: 1, col: 1, cellType: "field", field: "buyer_company", align: "left", borderStyle: "thin" }, { id: cid(), row: 1, col: 2, cellType: "static", value: "성명(대표자)", align: "center", fontWeight: "bold", backgroundColor: COLORS.primaryLight, textColor: COLORS.primary, borderStyle: "thin" }, { id: cid(), row: 1, col: 3, cellType: "field", field: "buyer_ceo", align: "left", borderStyle: "thin" }, { id: cid(), row: 2, col: 0, cellType: "static", value: "사업장주소", align: "center", fontWeight: "bold", backgroundColor: COLORS.primaryLight, textColor: COLORS.primary, borderStyle: "thin" }, { id: cid(), row: 2, col: 1, cellType: "field", field: "buyer_address", align: "left", borderStyle: "thin", colSpan: 3 }, { id: cid(), row: 2, col: 2, cellType: "static", value: "", merged: true, mergedBy: "r2c1", borderStyle: "thin" }, { id: cid(), row: 2, col: 3, cellType: "static", value: "", merged: true, mergedBy: "r2c1", borderStyle: "thin" }, { id: cid(), row: 3, col: 0, cellType: "static", value: "업태", align: "center", fontWeight: "bold", backgroundColor: COLORS.primaryLight, textColor: COLORS.primary, borderStyle: "thin" }, { id: cid(), row: 3, col: 1, cellType: "field", field: "buyer_business_type", align: "left", borderStyle: "thin" }, { id: cid(), row: 3, col: 2, cellType: "static", value: "종목", align: "center", fontWeight: "bold", backgroundColor: COLORS.primaryLight, textColor: COLORS.primary, borderStyle: "thin" }, { id: cid(), row: 3, col: 3, cellType: "field", field: "buyer_item", align: "left", borderStyle: "thin" }, ]; components.push({ id: cid(), type: "table", x: 40, y: buyY + 36, width: 760, height: 140, zIndex: 5, gridMode: true, gridRowCount: 4, gridColCount: 4, gridColWidths: [130, 250, 130, 250], gridRowHeights: [34, 34, 34, 34], gridHeaderRows: 0, gridHeaderCols: 1, gridCells: buyCells, borderWidth: 2, borderColor: COLORS.primary, showBorder: true, fontSize: 14, }); // 품목 테이블 (간격 확보) const itemY = buyY + 36 + 140 + 24; components.push( makeItemsTable( [ { field: "date_md", header: "월일", width: 56, align: "center" }, { field: "item_name", header: "품목", width: 160, align: "left" }, { field: "item_spec", header: "규격", width: 100, align: "left" }, { field: "quantity", header: "수량", width: 64, align: "right", numberFormat: "comma" }, { field: "unit_price", header: "단가", width: 88, align: "right", numberFormat: "comma" }, { field: "supply_amount", header: "공급가액", width: 108, align: "right", numberFormat: "comma" }, { field: "tax_amount", header: "세액", width: 80, align: "right", numberFormat: "comma" }, { field: "remarks", header: "비고", width: 104, align: "left" }, ], 40, itemY, 760, 200 ) ); // 합계 금액 (한글) const totalY = itemY + 200 + 16; components.push({ id: cid(), type: "text", x: 40, y: totalY, width: 760, height: 44, zIndex: 5, defaultValue: "합계금액 (한글): ", fieldName: "total_korean", backgroundColor: COLORS.rowAlt, borderWidth: 2, borderColor: COLORS.primary, fontSize: 16, fontColor: COLORS.textDark, textAlign: "left", padding: 8, }); // 결제방법 테이블 (간격 2배) const payY = totalY + 56; const payCells = [ { id: cid(), row: 0, col: 0, cellType: "static", value: "현금", align: "center", fontWeight: "bold", backgroundColor: COLORS.primaryLight, textColor: COLORS.primary, borderStyle: "thin" }, { id: cid(), row: 0, col: 1, cellType: "static", value: "수표", align: "center", fontWeight: "bold", backgroundColor: COLORS.primaryLight, textColor: COLORS.primary, borderStyle: "thin" }, { id: cid(), row: 0, col: 2, cellType: "static", value: "어음", align: "center", fontWeight: "bold", backgroundColor: COLORS.primaryLight, textColor: COLORS.primary, borderStyle: "thin" }, { id: cid(), row: 0, col: 3, cellType: "static", value: "외상미수금", align: "center", fontWeight: "bold", backgroundColor: COLORS.primaryLight, textColor: COLORS.primary, borderStyle: "thin" }, { id: cid(), row: 1, col: 0, cellType: "field", field: "pay_cash", align: "center", borderStyle: "thin" }, { id: cid(), row: 1, col: 1, cellType: "field", field: "pay_check", align: "center", borderStyle: "thin" }, { id: cid(), row: 1, col: 2, cellType: "field", field: "pay_bill", align: "center", borderStyle: "thin" }, { id: cid(), row: 1, col: 3, cellType: "field", field: "pay_credit", align: "center", borderStyle: "thin" }, ]; components.push({ id: cid(), type: "table", x: 40, y: payY, width: 760, height: 80, zIndex: 5, gridMode: true, gridRowCount: 2, gridColCount: 4, gridColWidths: [190, 190, 190, 190], gridRowHeights: [32, 48], gridHeaderRows: 1, gridHeaderCols: 0, gridCells: payCells, borderWidth: 2, borderColor: COLORS.primary, showBorder: true, fontSize: 14, }); // 이 금액을 청구함 const statY = payY + 92; components.push({ id: cid(), type: "text", x: 40, y: statY, width: 760, height: 48, zIndex: 5, defaultValue: "이 금액을 청구 함", backgroundColor: COLORS.rowAlt, borderWidth: 2, borderColor: COLORS.border, fontSize: 17, fontColor: COLORS.textDark, textAlign: "center", padding: 8, }); components.push(makeCompanySeal()); return components; } // ═══════════════════════════════════════════════════════════════════════════════ // 템플릿 8: 생산계획 현황표 // ═══════════════════════════════════════════════════════════════════════════════ function buildProductionPlanTemplate() { const components = []; // 헤더 (기간 정보 포함) components.push({ id: cid(), type: "text", x: 0, y: 0, width: 840, height: 100, zIndex: 0, defaultValue: "", backgroundColor: COLORS.primary, borderWidth: 0, fontSize: 14, fontColor: COLORS.white, padding: 0, }); components.push({ id: cid(), type: "text", x: 48, y: 14, width: 480, height: 44, zIndex: 1, defaultValue: "생산계획 현황표", backgroundColor: "transparent", borderWidth: 0, fontSize: 26, fontWeight: "bold", fontColor: COLORS.white, textAlign: "left", padding: 0, letterSpacing: 4, }); components.push({ id: cid(), type: "text", x: 48, y: 62, width: 400, height: 26, zIndex: 1, defaultValue: "PRODUCTION PLAN STATUS", backgroundColor: "transparent", borderWidth: 0, fontSize: 14, fontColor: "rgba(255,255,255,0.85)", textAlign: "left", padding: 0, }); // 대상 기간 (우측) components.push({ id: cid(), type: "text", x: 560, y: 18, width: 240, height: 22, zIndex: 1, defaultValue: "대상기간", backgroundColor: "transparent", borderWidth: 0, fontSize: 14, fontColor: "rgba(255,255,255,0.75)", textAlign: "right", padding: 0, }); components.push({ id: cid(), type: "text", x: 500, y: 44, width: 300, height: 32, zIndex: 1, defaultValue: "", fieldName: "plan_period", backgroundColor: "transparent", borderWidth: 0, fontSize: 18, fontWeight: "bold", fontColor: COLORS.white, textAlign: "right", padding: 0, }); // 문서 정보 행 components.push( makeDocInfoRow( [ { label: "작성일자", fieldName: "doc_date" }, { label: "작성부서", fieldName: "dept_name" }, { label: "작성자", fieldName: "created_by" }, ], 40, 108, 200 ) ); components.push({ id: cid(), type: "divider", x: 0, y: 172, width: 840, height: 2, zIndex: 5, orientation: "horizontal", lineStyle: "solid", lineWidth: 1, lineColor: COLORS.border, }); // 요약 카드 3개 (gridMode 테이블로 통합) const cardY = 184; const summaryCardCells = [ { id: cid(), row: 0, col: 0, cellType: "static", value: "총 계획수량", align: "center", verticalAlign: "bottom", fontSize: 13, fontWeight: "normal", backgroundColor: COLORS.primaryLight, textColor: COLORS.textGray, borderStyle: "thin" }, { id: cid(), row: 0, col: 1, cellType: "static", value: "총 생산수량", align: "center", verticalAlign: "bottom", fontSize: 13, fontWeight: "normal", backgroundColor: "#F0FDF4", textColor: COLORS.textGray, borderStyle: "thin" }, { id: cid(), row: 0, col: 2, cellType: "static", value: "평균 달성률", align: "center", verticalAlign: "bottom", fontSize: 13, fontWeight: "normal", backgroundColor: COLORS.yellow, textColor: COLORS.textGray, borderStyle: "thin" }, { id: cid(), row: 1, col: 0, cellType: "field", field: "total_planned_qty", value: "0 EA", align: "center", verticalAlign: "middle", fontSize: 18, fontWeight: "bold", backgroundColor: COLORS.primaryLight, textColor: COLORS.primary, borderStyle: "thin" }, { id: cid(), row: 1, col: 1, cellType: "field", field: "total_actual_qty", value: "0 EA", align: "center", verticalAlign: "middle", fontSize: 18, fontWeight: "bold", backgroundColor: "#F0FDF4", textColor: "#16A34A", borderStyle: "thin" }, { id: cid(), row: 1, col: 2, cellType: "field", field: "avg_achievement_rate", value: "0%", align: "center", verticalAlign: "middle", fontSize: 18, fontWeight: "bold", backgroundColor: COLORS.yellow, textColor: COLORS.yellowBorder, borderStyle: "thin" }, ]; components.push({ id: cid(), type: "table", x: 40, y: cardY, width: 740, height: 80, zIndex: 5, gridMode: true, gridRowCount: 2, gridColCount: 3, gridColWidths: [240, 240, 240], gridRowHeights: [28, 52], gridHeaderRows: 1, gridHeaderCols: 0, gridCells: summaryCardCells, borderWidth: 2, borderColor: COLORS.border, backgroundColor: COLORS.white, showBorder: true, }); // 구분선 - 간격 2~3배 확보 (y: 272 -> 300) components.push({ id: cid(), type: "divider", x: 0, y: 300, width: 840, height: 2, zIndex: 5, orientation: "horizontal", lineStyle: "solid", lineWidth: 1, lineColor: COLORS.border, }); // 생산계획 현황 테이블 (아래로 이동) components.push( makeItemsTable( [ { field: "row_no", header: "No", width: 40, align: "center" }, { field: "item_code", header: "품번", width: 88, align: "left" }, { field: "item_name", header: "품명", width: 140, align: "left" }, { field: "planned_qty", header: "계획수량", width: 80, align: "right", numberFormat: "comma" }, { field: "actual_qty", header: "생산수량", width: 80, align: "right", numberFormat: "comma" }, { field: "achievement_rate", header: "달성률", width: 64, align: "center" }, { field: "remaining_qty", header: "잔여수량", width: 80, align: "right", numberFormat: "comma" }, { field: "status", header: "상태", width: 64, align: "center" }, { field: "remarks", header: "비고", width: 124, align: "left" }, ], 40, 316, 760, 340 ) ); // 비고 섹션 (텍스트 크기 2배: 88 -> 176) components.push( ...makeNotesSection("비고 및 특이사항", 40, 670, 760, 176) ); // 상태 범례 (테이블로 구성, 위와 공간 2배 이상 확보) const legendY = 880; components.push(makeSectionHeader("상태 범례", 40, legendY, 760, 5)); const legendCells = [ { id: cid(), row: 0, col: 0, cellType: "static", value: "완료 (100%)", align: "center", verticalAlign: "middle", fontSize: 14, fontWeight: "bold", backgroundColor: "#F0FDF4", textColor: "#16A34A", borderStyle: "thin" }, { id: cid(), row: 0, col: 1, cellType: "static", value: "진행중 (70-99%)", align: "center", verticalAlign: "middle", fontSize: 14, fontWeight: "bold", backgroundColor: COLORS.primaryLight, textColor: "#2563EB", borderStyle: "thin" }, { id: cid(), row: 0, col: 2, cellType: "static", value: "지연 (70% 미만)", align: "center", verticalAlign: "middle", fontSize: 14, fontWeight: "bold", backgroundColor: "#FEF2F2", textColor: "#DC2626", borderStyle: "thin" }, ]; components.push({ id: cid(), type: "table", x: 40, y: legendY + 36, width: 760, height: 40, zIndex: 5, gridMode: true, gridRowCount: 1, gridColCount: 3, gridColWidths: [253, 253, 254], gridRowHeights: [40], gridHeaderRows: 0, gridHeaderCols: 0, gridCells: legendCells, borderWidth: 2, borderColor: COLORS.border, backgroundColor: COLORS.white, showBorder: true, }); // 하단 작성자 정보 (gridMode 테이블) const footerInfoCells = [ { id: cid(), row: 0, col: 0, cellType: "static", value: "생산관리팀", align: "left", verticalAlign: "middle", fontSize: 13, fontWeight: "normal", backgroundColor: "transparent", textColor: COLORS.textGray, borderStyle: "none" }, { id: cid(), row: 0, col: 1, cellType: "field", field: "created_by", value: "", align: "left", verticalAlign: "middle", fontSize: 13, fontWeight: "normal", backgroundColor: "transparent", textColor: COLORS.textGray, borderStyle: "none" }, { id: cid(), row: 0, col: 2, cellType: "static", value: "작성일", align: "right", verticalAlign: "middle", fontSize: 13, fontWeight: "normal", backgroundColor: "transparent", textColor: COLORS.textGray, borderStyle: "none" }, { id: cid(), row: 0, col: 3, cellType: "field", field: "doc_date", value: "", align: "right", verticalAlign: "middle", fontSize: 13, fontWeight: "normal", backgroundColor: "transparent", textColor: COLORS.textGray, borderStyle: "none" }, ]; components.push({ id: cid(), type: "table", x: 40, y: 1110, width: 760, height: 24, zIndex: 5, gridMode: true, gridRowCount: 1, gridColCount: 4, gridColWidths: [120, 260, 120, 260], gridRowHeights: [24], gridHeaderRows: 0, gridHeaderCols: 0, gridCells: footerInfoCells, borderWidth: 0, borderColor: "transparent", backgroundColor: "transparent", showBorder: false, }); return components; } // ─── 템플릿 정의 목록 ───────────────────────────────────────────────────────── const PAGE_SETTINGS = { width: 210, height: 297, orientation: "portrait", margins: { top: 10, bottom: 10, left: 10, right: 10 }, }; const TEMPLATE_DEFINITIONS = [ { templateNameKor: "견적서", templateNameEng: "Quotation", templateType: "QUOTATION", description: "견적서 기본 템플릿 - 헤더, 결재란(담당/검토/승인), 수신자/공급자 정보, 품목 테이블, 합계, 비고, 직인 포함", buildFn: buildQuotationTemplate, sortOrder: 1, }, { templateNameKor: "견적서 (영문)", templateNameEng: "Export Quotation", templateType: "EXPORT_QUOTATION", description: "영문 견적서 템플릿 - 해외 거래처용 (Ship To/Supplier, Description, Terms & Conditions)", buildFn: buildExportQuotationTemplate, sortOrder: 2, }, { templateNameKor: "발주서", templateNameEng: "Purchase Order", templateType: "PURCHASE_ORDER", description: "발주서 기본 템플릿 - 헤더, 결재란(담당/부서장/임원/사장), 발주처/자사 정보, 품목 테이블, 합계, 발주조건 포함", buildFn: buildPurchaseOrderTemplate, sortOrder: 2, }, { templateNameKor: "수주 확인서", templateNameEng: "Sales Order Confirmation", templateType: "SALES_ORDER", description: "수주확인서 기본 템플릿 - 헤더, 결재란(담당/검토/승인), 발주처/자사 정보, 품목 테이블, 합계, 특기사항 포함", buildFn: buildSalesOrderTemplate, sortOrder: 3, }, { templateNameKor: "거래명세서", templateNameEng: "Delivery Note", templateType: "DELIVERY_NOTE", description: "거래명세서 기본 템플릿 - 헤더(중앙), 공급자/공급받는자 정보, 품목 테이블, 합계, 인수 서명란 포함", buildFn: buildDeliveryNoteTemplate, sortOrder: 4, }, { templateNameKor: "작업지시서", templateNameEng: "Work Order", templateType: "WORK_ORDER", description: "작업지시서 기본 템플릿 - 헤더(바코드), 결재란(작성/확인/승인), 작업지시/제품 정보, 자재소요/작업공정 테이블, QR코드 포함", buildFn: buildWorkOrderTemplate, sortOrder: 5, }, { templateNameKor: "검사 성적서", templateNameEng: "Inspection Report", templateType: "INSPECTION_REPORT", description: "검사성적서 기본 템플릿 - 헤더(합격 배지), 결재란(검사자/확인/승인), 검사/제품 정보, 검사항목 테이블, 종합판정, 검사자/승인자 서명란 포함", buildFn: buildInspectionReportTemplate, sortOrder: 6, }, { templateNameKor: "세금계산서", templateNameEng: "Tax Invoice", templateType: "TAX_INVOICE", description: "세금계산서 기본 템플릿 - 헤더(중앙), 승인번호/일자, 공급가액/세액, 공급자/공급받는자 정보 그리드, 품목 테이블, 합계, 결제방법 포함", buildFn: buildTaxInvoiceTemplate, sortOrder: 7, }, { templateNameKor: "생산계획 현황표", templateNameEng: "Production Plan Status", templateType: "PRODUCTION_PLAN", description: "생산계획 현황표 기본 템플릿 - 헤더(기간), 작성정보, 요약 카드(계획/생산/달성률), 생산계획 테이블, 비고, 상태 범례 포함", buildFn: buildProductionPlanTemplate, sortOrder: 8, }, ]; // ─── 메인 실행 ─────────────────────────────────────────────────────────────── async function main() { console.log("🔐 로그인 중..."); let token; try { token = await getToken(); console.log("✅ 로그인 성공"); } catch (e) { console.error("❌ 로그인 실패:", e.message); process.exit(1); } // 기존 커스텀 템플릿 중 동일 이름 삭제 console.log("📋 기존 템플릿 조회 중..."); let existingTemplates = []; try { const resp = await getTemplates(token); existingTemplates = [...(resp.data?.system || []), ...(resp.data?.custom || [])]; console.log(` 총 ${existingTemplates.length}개 템플릿 발견`); } catch (e) { console.warn(" 템플릿 조회 실패 (무시):", e.message); } const targetNames = TEMPLATE_DEFINITIONS.map((t) => t.templateNameKor); for (const tpl of existingTemplates) { if ( targetNames.includes(tpl.template_name_kor) && tpl.is_system === "N" ) { console.log(` 🗑️ 기존 템플릿 삭제: ${tpl.template_name_kor} (${tpl.template_id})`); try { await deleteTemplate(token, tpl.template_id); } catch (e) { console.warn(" 삭제 실패 (무시):", e.message); } } } // 시스템 템플릿 삭제 시도 (is_system=Y 는 API에서 막히므로 스킵) for (const tpl of existingTemplates) { if (targetNames.includes(tpl.template_name_kor) && tpl.is_system === "Y") { console.log(` ⚠️ 시스템 템플릿 삭제 불가 (건너뜀): ${tpl.template_name_kor}`); } } // 템플릿 생성 console.log("\n🏗️ 템플릿 생성 시작..."); let successCount = 0; let failCount = 0; for (const def of TEMPLATE_DEFINITIONS) { _idCounter = 0; // 각 템플릿마다 ID 카운터 초기화 console.log(`\n 📄 ${def.templateNameKor} (${def.templateNameEng}) 생성 중...`); const components = def.buildFn(); console.log(` 컴포넌트 수: ${components.length}개`); const layoutConfig = { pageSettings: PAGE_SETTINGS, components, }; const templateData = { templateNameKor: def.templateNameKor, templateNameEng: def.templateNameEng, templateType: def.templateType, description: def.description, layoutConfig, sortOrder: def.sortOrder, }; try { const result = await createTemplate(token, templateData); if (result.success) { console.log(` ✅ 성공: ${def.templateNameKor} (ID: ${result.data?.templateId})`); successCount++; } else { console.error(` ❌ 실패: ${def.templateNameKor}`, result.message || result.error); failCount++; } } catch (e) { console.error(` ❌ 오류: ${def.templateNameKor}`, e.message); failCount++; } } console.log(`\n✨ 완료! 성공: ${successCount}개, 실패: ${failCount}개`); } main();