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