ERP-node/backend-node/scripts/create-report-templates.js

2136 lines
83 KiB
JavaScript
Raw Normal View History

/**
* 기본 리포트 템플릿 생성 스크립트
* 실행: 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();