feat(est-test): 갑지·표지·공사설명서·시방서 편집과 인쇄
- 원본 문서 레코드(MIDAS) 읽기, 공사명 동기화, 공사금액 자동입력·한글금액 - 설명서·시방서 갑지 붙여넣기, 인쇄 미리보기 반영, 편집 시 ESTX 저장 거부 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,216 @@
|
||||
// 원본 갑지(TFRMGABGI)·표지(TFRMCOVER)·공사설명서/시방서(TFRMESTEDITOR) 웹 편집 — project.docs.
|
||||
// 공사명은 열 때 공사 이름을 따르고 고치면 공사 이름에도 적는다(원본 TCommonOption.ProjectName 동기화).
|
||||
// 공사금액 자동입력: 원가계산서에서 고른 비목을 두 방식으로 적는다(원본 c_cb2e78·c_cb3080, 한글금액 c_931b44).
|
||||
// 1 = 「도급액 : 금 일천오백만원(₩15,000,000)」 · 2 = 「도급액 : 금15,000,000원(금일천오백만원)」, 「일」을 줄이지 않는다.
|
||||
// 설명서·시방서 붙여넣기는 갑지의 공사명·공사위치·공사개요·공사금액을 커서 자리에 넣는다. 서식·여백은 다루지 않는다.
|
||||
const EstDocs = (() => {
|
||||
const digits = ["", "일", "이", "삼", "사", "오", "육", "칠", "팔", "구"];
|
||||
const places = ["천", "백", "십", ""];
|
||||
const groups = ["", "만", "억", "조"];
|
||||
|
||||
function korean(value) {
|
||||
const n = Math.round(Number(value) || 0);
|
||||
if (!n) return "영";
|
||||
const text = String(Math.abs(n)),
|
||||
padded = text.padStart(Math.ceil(text.length / 4) * 4, " "),
|
||||
count = padded.length / 4;
|
||||
let out = "";
|
||||
for (let g = 0; g < count; g++) {
|
||||
const part = padded.slice(g * 4, g * 4 + 4);
|
||||
let chunk = "";
|
||||
[...part].forEach((ch, i) => {
|
||||
const d = Number(ch);
|
||||
if (ch !== " " && d) chunk += digits[d] + places[i];
|
||||
});
|
||||
if (chunk) out += chunk + groups[count - 1 - g];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
const comma = (value) => Math.round(Number(value) || 0).toLocaleString("en-US");
|
||||
|
||||
function costLine(name, cost, method) {
|
||||
return method === 2
|
||||
? `${name} : 금${comma(cost)}원(금${korean(cost)}원)`
|
||||
: `${name} : 금 ${korean(cost)}원(₩${comma(cost)})`;
|
||||
}
|
||||
function costText(project, indices, method) {
|
||||
const rows = project.tables.Wonga || [];
|
||||
const picked = indices.map((i) => rows[i]).filter(Boolean);
|
||||
if (!picked.length) throw Error("공사금액에 넣을 원가 비목을 고르세요.");
|
||||
const width = Math.max(...picked.map((r) => String(r.Name ?? "").trim().length));
|
||||
return picked
|
||||
.map((r) =>
|
||||
costLine(
|
||||
String(r.Name ?? "")
|
||||
.trim()
|
||||
.padEnd(width, " "),
|
||||
r.Cost,
|
||||
method,
|
||||
),
|
||||
)
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
const defaults = () => ({
|
||||
gabgi: {
|
||||
Name: "",
|
||||
Position: "",
|
||||
CompanyName: "",
|
||||
Explan: "",
|
||||
Cost: "",
|
||||
Gulga: ["과 장", "계 장", "심사자", "설계자"],
|
||||
Date: "",
|
||||
YearShow: true,
|
||||
DayShow: false,
|
||||
CompanyPos: 1,
|
||||
},
|
||||
cover: { Name: "", Name2: "", CompanyName: "", Date: "", YearShow: true, CompanyPos: 1 },
|
||||
explan: { text: "" },
|
||||
sibang: { text: "" },
|
||||
});
|
||||
function doc(project, kind) {
|
||||
project.docs ||= {};
|
||||
const value = { ...defaults()[kind], ...(project.docs[kind] || {}) };
|
||||
if ((kind === "gabgi" || kind === "cover") && project.name) value.Name = project.name;
|
||||
return value;
|
||||
}
|
||||
function year(value) {
|
||||
const text = String(value ?? "")
|
||||
.split("년")[0]
|
||||
.trim();
|
||||
return `${text || new Date().getFullYear()}년도`;
|
||||
}
|
||||
|
||||
// 인쇄 미리보기(서식 8 갑지 · 9 표지 · 10 시방서 · 11 공사설명서)
|
||||
function html(form, project) {
|
||||
const esc = (v) =>
|
||||
String(v ?? "").replace(
|
||||
/[&<>"]/g,
|
||||
(c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c],
|
||||
);
|
||||
const align = (pos) => ["left", "center", "right"][Number(pos)] || "center";
|
||||
if (form === 9) {
|
||||
const c = doc(project, "cover");
|
||||
return `<div class="document-cover" style="text-align:center;padding:40px 0">${c.YearShow ? `<p style="font-size:15pt">${esc(year(c.Date))}</p>` : ""}<h1 style="font-size:32pt;margin:80px 0 20px">${esc(c.Name)}</h1>${c.Name2 ? `<h2>${esc(c.Name2)}</h2>` : ""}<p style="font-size:15pt;margin-top:160px;text-align:${align(c.CompanyPos)}">${esc(c.CompanyName)}</p></div>`;
|
||||
}
|
||||
if (form === 8) {
|
||||
const g = doc(project, "gabgi");
|
||||
const gulga = g.Gulga.length
|
||||
? `<table class="sheet" style="float:right;margin-bottom:20px"><tr>${g.Gulga.map((n) => `<th>${esc(n)}</th>`).join("")}</tr><tr>${g.Gulga.map(() => "<td style='height:48px;min-width:60px'></td>").join("")}</tr></table>`
|
||||
: "";
|
||||
return `<div class="document-gabgi">${gulga}<p style="clear:both">${esc(g.Date)}</p>${g.Position ? `<p>공사위치 : ${esc(g.Position)}</p>` : ""}<h1 style="font-size:26pt;text-align:center;margin:40px 0">${esc(g.Name)}</h1>${g.Explan ? `<p>공사개요 :</p><pre class="document-text">${esc(g.Explan)}</pre>` : ""}${g.Cost ? `<p>공사금액 :</p><pre class="document-text">${esc(g.Cost)}</pre>` : ""}<p style="margin-top:60px;text-align:${align(g.CompanyPos)}">${esc(g.CompanyName)}</p></div>`;
|
||||
}
|
||||
if (form === 10 || form === 11) {
|
||||
const text = doc(project, form === 10 ? "sibang" : "explan").text;
|
||||
return text ? `<pre class="document-text">${esc(text)}</pre>` : "";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
// 화면
|
||||
const field = (name) => document.querySelector(`#dialog-content [name="${name}"]`);
|
||||
function editor(state, kind) {
|
||||
const { esc } = EstApp,
|
||||
d = doc(state.project, kind);
|
||||
const input = (name, label, value) =>
|
||||
`<label>${label}<input name="doc-${name}" value="${esc(value ?? "")}"></label>`;
|
||||
const area = (name, label, value, rows = 4) =>
|
||||
`<label>${label}<textarea name="doc-${name}" rows="${rows}" style="width:100%">${esc(value ?? "")}</textarea></label>`;
|
||||
const pos = (value) =>
|
||||
`<label>발주처 위치<select name="doc-CompanyPos">${["좌측", "중앙", "우측"].map((l, i) => `<option value="${i}" ${Number(value) === i ? "selected" : ""}>${l}</option>`).join("")}</select></label>`;
|
||||
let body = "";
|
||||
if (kind === "cover")
|
||||
body =
|
||||
input("Name", "공사명", d.Name) +
|
||||
input("Name2", "부제", d.Name2) +
|
||||
input("Date", "설계년도", d.Date) +
|
||||
input("CompanyName", "발주처", d.CompanyName) +
|
||||
pos(d.CompanyPos);
|
||||
else if (kind === "gabgi") {
|
||||
const rows = (state.project.tables.Wonga || [])
|
||||
.map((r, i) => [r, i])
|
||||
.filter(([r]) => Number(r.Cost) && String(r.Name ?? "").trim());
|
||||
const fixed = rows.find(([r]) => Number(r.LinkIndex) === 55)?.[1];
|
||||
body =
|
||||
input("Name", "공사명", d.Name) +
|
||||
input("Position", "공사위치", d.Position) +
|
||||
input("CompanyName", "발주처", d.CompanyName) +
|
||||
input("Date", "설계일자", d.Date) +
|
||||
pos(d.CompanyPos) +
|
||||
area("Explan", "공사개요", d.Explan) +
|
||||
area("Cost", "공사금액", d.Cost, 3) +
|
||||
`<p>공사금액 자동입력: <select name="doc-rows" multiple size="4">${rows.map(([r, i]) => `<option value="${i}" ${i === fixed ? "selected" : ""}>${esc(r.Name)} ${EstApp.number(r.Cost)}</option>`).join("")}</select> <select name="doc-method"><option value="1">금 일천…원(₩…)</option><option value="2">금…원(금 일천…원)</option></select> <button data-action="doc-cost">공사금액 만들기</button></p>` +
|
||||
area("Gulga", "결재란(한 줄에 하나)", d.Gulga.join("\n"), 4);
|
||||
} else
|
||||
body =
|
||||
area("text", kind === "explan" ? "공사설명서" : "시방서", d.text, 14) +
|
||||
`<p>갑지에서 붙여넣기: ${[
|
||||
["Name", "공사명"],
|
||||
["Position", "공사위치"],
|
||||
["Explan", "공사개요"],
|
||||
["Cost", "공사금액"],
|
||||
]
|
||||
.map(([k, l]) => `<button data-action="doc-paste" data-field="${k}">${l}</button>`)
|
||||
.join(" ")}</p>`;
|
||||
const title = { gabgi: "갑지", cover: "표지", explan: "공사설명서", sibang: "시방서" }[kind];
|
||||
EstApp.showDialog(
|
||||
`${title} 편집`,
|
||||
`<div class="form-fields">${body}</div><button data-action="doc-save" data-kind="${kind}">저장</button>`,
|
||||
);
|
||||
}
|
||||
function collect(kind) {
|
||||
const value = (name) => field(`doc-${name}`)?.value ?? "";
|
||||
if (kind === "cover")
|
||||
return {
|
||||
Name: value("Name"),
|
||||
Name2: value("Name2"),
|
||||
Date: value("Date"),
|
||||
CompanyName: value("CompanyName"),
|
||||
CompanyPos: Number(value("CompanyPos")),
|
||||
};
|
||||
if (kind === "gabgi")
|
||||
return {
|
||||
Name: value("Name"),
|
||||
Position: value("Position"),
|
||||
CompanyName: value("CompanyName"),
|
||||
Date: value("Date"),
|
||||
CompanyPos: Number(value("CompanyPos")),
|
||||
Explan: value("Explan"),
|
||||
Cost: value("Cost"),
|
||||
Gulga: value("Gulga")
|
||||
.split(/\r?\n/)
|
||||
.map((l) => l.trim())
|
||||
.filter(Boolean),
|
||||
};
|
||||
return { text: value("text") };
|
||||
}
|
||||
|
||||
function handle(name, state, node) {
|
||||
if (name === "doc-edit") editor(state, node.dataset.kind);
|
||||
else if (name === "doc-save") {
|
||||
const kind = node.dataset.kind,
|
||||
next = collect(kind);
|
||||
EstApp.byId("dialog").close();
|
||||
EstApp.change((project) => {
|
||||
project.docs ||= {};
|
||||
project.docs[kind] = { ...doc(project, kind), ...next };
|
||||
if (next.Name && next.Name !== project.name) project.name = next.Name;
|
||||
}, false);
|
||||
state.notice = "문서를 저장했습니다.";
|
||||
} else if (name === "doc-cost") {
|
||||
const indices = [...field("doc-rows").selectedOptions].map((o) => Number(o.value));
|
||||
field("doc-Cost").value = costText(state.project, indices, Number(field("doc-method").value));
|
||||
} else if (name === "doc-paste") {
|
||||
const key = node.dataset.field,
|
||||
g = doc(state.project, "gabgi"),
|
||||
text = String(g[key] ?? "").trim();
|
||||
if (!text) throw Error("갑지에서 입력을 하지 않았습니다.");
|
||||
const area = field("doc-text"),
|
||||
at = area.selectionStart ?? area.value.length;
|
||||
area.value = area.value.slice(0, at) + text + area.value.slice(area.selectionEnd ?? at);
|
||||
} else return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
return { korean, costLine, costText, html, doc, handle };
|
||||
})();
|
||||
Reference in New Issue
Block a user