- 시험 615 통과 · 브라우저 화면 13개·메뉴 90개 오류 0 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
423 lines
20 KiB
JavaScript
423 lines
20 KiB
JavaScript
/* -*- coding: utf-8 -*- */
|
|
/**
|
|
* 대가표·산근 편집 화면 (scr_edit_san.js) — wM_Edit_iLWi 자리 바꿔 끼움
|
|
* 왼쪽 B·D·X 목록 · 오른쪽 산출근거 편집기(D · 산식 있는 코드) 또는 구성행 편집(B · X, scr_edit_comp.js)
|
|
* 산출근거 편집기: 계산(F9) · 문법 오류 줄 표시 · 함수 입력 · 계표시 · 코드 입력(꼴 5택 · 연산 / * · 변수 Q A T N ·
|
|
* 0원 비목 입력안함) · 수량변수(~Q00001.QTY1~) · 중기공식 불러오기 · 금액결과 삭제 · 결과적용 비목 · 결과금액조정 ·
|
|
* 문법 강조 · 문법설명
|
|
* 근거: REA 30 TWM_EDIT_SAN · TWM_EDIT_EQ · 지식DB 26 §3 · 28 §1·§4 · 23
|
|
*/
|
|
(function (global) {
|
|
"use strict";
|
|
const P = (global.STMATE_PLUGINS = global.STMATE_PLUGINS || {
|
|
screens: {},
|
|
actions: {},
|
|
menus: [],
|
|
});
|
|
const num = (v) => Number(v) || 0;
|
|
const L = () => global.STMATE_SMT_LANG;
|
|
const X = () => global.STMATE_SMT_EXPR;
|
|
const ENG = () => global.STMATE_ENGINE;
|
|
const COMP = () => global.STMATE_EDIT_COMP;
|
|
const esc = (s) => L().esc(s);
|
|
const SB = [
|
|
["", "0 각 해당비목 적용"],
|
|
["M", "1 재료비로 적용"],
|
|
["L", "2 노무비로 적용"],
|
|
["S", "3 경 비로 적용"],
|
|
];
|
|
const FILTERS = [
|
|
["ALL_BDX", "전체 대가·산근·중기"],
|
|
["SMT_ONLY", "산식 보유"],
|
|
["D", "단가산출 D"],
|
|
["B", "일위대가 B"],
|
|
["X", "중기사용료 X"],
|
|
];
|
|
|
|
const memoText = (ctx, code) => {
|
|
const ed = ctx.uiState.editedSmtMap || {};
|
|
if (ed[code] !== undefined) return ed[code];
|
|
const m = ((ctx.dbfData && ctx.dbfData.costn_memos) || {})[code];
|
|
return m ? m.text : "";
|
|
};
|
|
|
|
// 수량코드·중기공식 — 처음 한 번 서버에서 받아 둠
|
|
function preload(ctx, app) {
|
|
const d = ctx.dbfData;
|
|
if (d && !d.qcode_rows && !ctx.uiState.qcodeLoading && ctx.currentSourcePath) {
|
|
ctx.uiState.qcodeLoading = true;
|
|
app
|
|
.postJson("/api/qty/qcode", { source_path: ctx.currentSourcePath })
|
|
.then((r) => ((d.qcode_rows = r.rows || []), app.renderWorkspace()))
|
|
.catch(() => (d.qcode_rows = []));
|
|
}
|
|
if (!ctx.uiState.eqItems && !ctx.uiState.eqLoading) {
|
|
ctx.uiState.eqLoading = true;
|
|
fetch("/api/qty/eq")
|
|
.then((r) => r.json())
|
|
.then((r) => ((ctx.uiState.eqItems = r.items || []), app.renderWorkspace()))
|
|
.catch(() => (ctx.uiState.eqItems = []));
|
|
}
|
|
}
|
|
|
|
function listRows(ctx) {
|
|
const memos = (ctx.dbfData && ctx.dbfData.costn_memos) || {};
|
|
const f = ctx.uiState.ilwiSubFilter || "ALL_BDX";
|
|
return ((ctx.dbfData && ctx.dbfData.costn_rows) || []).filter((r) => {
|
|
const c = String(r.CODE || "").trim();
|
|
if (!c) return false;
|
|
if (f === "ALL_BDX") return /^[BDX]/.test(c) || Boolean(memos[c]);
|
|
if (f === "SMT_ONLY") return Boolean(memos[c]);
|
|
return c[0] === f;
|
|
});
|
|
}
|
|
|
|
function render(ctx, app) {
|
|
preload(ctx, app);
|
|
const fmt = app.fmt;
|
|
const rows = listRows(ctx);
|
|
const memos = (ctx.dbfData && ctx.dbfData.costn_memos) || {};
|
|
let code = ctx.uiState.selectedIlwiCode;
|
|
if (!rows.some((r) => String(r.CODE).trim() === code))
|
|
code = rows.length ? String(rows[0].CODE).trim() : "";
|
|
ctx.uiState.selectedIlwiCode = code;
|
|
const row = rows.find((r) => String(r.CODE).trim() === code) || {};
|
|
const hasComp = ((ctx.dbfData && ctx.dbfData.bdqty_rows) || []).some((r) => r.BDCODE === code);
|
|
const isText = code[0] === "D" || Boolean(memos[code]) || !/^[BX]/.test(code) || !hasComp;
|
|
const f = ctx.uiState.ilwiSubFilter || "ALL_BDX";
|
|
const cyc = ctx.uiState.cycleErrors || [];
|
|
return `
|
|
<div class="win-form">
|
|
<div class="win-form-header">
|
|
<span>${code[0] === "D" ? "단가산출근거 수정" : code[0] === "X" ? "중기사용료 수정" : "일위대가표 수정"} [ ${esc(code)}: ${esc(row.NAME || "")} ${esc(row.SIZE || "")} ]</span>
|
|
<span>${esc(row.UNIT || "")} · 저장 단가 재 ${fmt(num(row.MAMT1))} / 노 ${fmt(num(row.LAMT1))} / 경 ${fmt(num(row.SAMT1))} = ${fmt(num(row.TAMT1))}원${num(row.NAGCD) ? ` · 낙찰율 ${num(row.NAGCD)}%` : ""}</span>
|
|
</div>
|
|
<div class="win-subbar">
|
|
${FILTERS.map(([k, l]) => `<button class="win-btn ilwi-sub-btn ${f === k ? "active" : ""}" data-sub="${k}">${l}</button>`).join("")}
|
|
<button class="win-btn" id="san-btn-eq" style="margin-left:auto;">표준중기공식 편집</button>
|
|
</div>
|
|
${cyc.length ? `<div style="color:#b91c1c;padding:3px 8px;">순환참조 ${cyc.length}건 — ${esc(cyc.slice(0, 3).join(" · "))}</div>` : ""}
|
|
<div class="split-horiz">
|
|
<div class="pane-box" style="width:400px;">
|
|
<div class="pane-title"><span>대가·산근·중기 목록</span><span>${rows.length}건</span></div>
|
|
<div class="grid-scroll"><table class="dbgrid">
|
|
<thead><tr><th style="width:62px">코드</th><th>명칭</th><th style="width:90px">규격</th><th style="width:78px">합계단가</th><th style="width:34px">식</th></tr></thead>
|
|
<tbody>${rows
|
|
.map((r) => {
|
|
const c = String(r.CODE).trim();
|
|
return `<tr class="ilwi-master-row ${c === code ? "selected" : ""}" data-code="${esc(c)}" style="cursor:pointer;"><td class="center"><strong>${esc(c)}</strong></td><td>${esc(r.NAME)}</td><td>${esc(r.SIZE)}</td><td class="num">${fmt(num(r.TAMT1))}</td><td class="center">${memos[c] ? "산식" : /^[BX]/.test(c) ? "구성" : "-"}</td></tr>`;
|
|
})
|
|
.join("")}</tbody>
|
|
</table></div>
|
|
</div>
|
|
<div class="split-vert" style="flex:1;padding:0;">
|
|
${code ? (isText ? renderText(ctx, app, code, row) : `<div class="pane-box" style="flex:1;">${COMP().render(ctx, code, app)}</div>`) : "<div class='pane-box'>항목 없음</div>"}
|
|
</div>
|
|
</div>
|
|
</div>`;
|
|
}
|
|
|
|
// 산출근거 편집기
|
|
function renderText(ctx, app, code, row) {
|
|
const fmt = app.fmt;
|
|
const text = memoText(ctx, code);
|
|
const ev = ENG().evaluateSmtFormulaText(text, ctx, code);
|
|
const cyc = cycleErrors(ctx, code);
|
|
const errs = ev.errors.concat(cyc.map((msg) => ({ lineNo: 0, msg })));
|
|
const u = ctx.uiState;
|
|
const sel = (id, opts, cur) =>
|
|
`<select id="${id}">${opts.map(([v, l]) => `<option value="${esc(v)}" ${String(cur) === String(v) ? "selected" : ""}>${esc(l)}</option>`).join("")}</select>`;
|
|
const marks = [["", "계표시(F5)"]].concat(
|
|
L().MARK_NAMES.map((n, i) => [L().marker(i + 1, false), `${n} ${L().marker(i + 1, false)}`]),
|
|
L().MARK_NAMES.map((n, i) => [
|
|
L().marker(i + 1, true),
|
|
`${n}(제외금액) ${L().marker(i + 1, true)}`,
|
|
]),
|
|
);
|
|
const qrows = (ctx.dbfData && ctx.dbfData.qcode_rows) || [];
|
|
const qopts = [["", qrows.length ? "수량코드" : "수량코드 없음"]];
|
|
for (const q of qrows) {
|
|
const c = String(q.CODE).trim();
|
|
for (let k = 0; k < 10; k++)
|
|
if (String(q[`QTY${k}`] ?? "").trim())
|
|
qopts.push([`~${c}.QTY${k}~`, `${c}.QTY${k} = ${q[`QTY${k}`]} ${q.NAME || ""}`]);
|
|
for (const ln of String(q.XCODE || "").split(/\r?\n/))
|
|
if (ln.includes("="))
|
|
qopts.push([`~${c}.${ln.split("=")[0].trim()}~`, `${c}.${ln.trim()}`]);
|
|
}
|
|
const eq = u.eqItems || [];
|
|
const adjOn = String(row.S_ADJ || "").trim() === "T";
|
|
const lines = ev.linesOut;
|
|
return `
|
|
<div class="win-subbar" style="flex-wrap:wrap;gap:4px;">
|
|
<button class="win-btn primary" id="san-calc">계산(F9) · 단가 반영</button>
|
|
<button class="win-btn" id="san-strip">금액결과 삭제</button>
|
|
<button class="win-btn ${u.sanHelp ? "active" : ""}" id="san-help">문법설명 보기</button>
|
|
<button class="win-btn ${u.sanHilite === false ? "" : "active"}" id="san-hilite">문법 강조</button>
|
|
${sel("san-func", [["", "각종 지원함수"]].concat(X().FUNC_LIST.map((f) => [f, f])), "")}
|
|
${sel("san-mark", marks, "")}
|
|
${sel("san-qty", qopts, "")}
|
|
${sel(
|
|
"san-img",
|
|
[["", "그림코드(Ctrl+I)"]].concat(
|
|
((ctx.dbfData && ctx.dbfData.costn_rows) || [])
|
|
.filter((r) => /^I\d{5}$/.test(String(r.CODE).trim()))
|
|
.map((r) => [String(r.CODE).trim(), `${String(r.CODE).trim()} ${r.NAME || ""}`]),
|
|
),
|
|
"",
|
|
)}
|
|
${sel("san-eq", [["", eq.length ? "중기공식 불러오기" : "등록된 중기공식 없음"]].concat(eq.map((e, i) => [String(i), `${e.name} (${e.ins_x ? "공식+코드" : "공식"})`])), "")}
|
|
</div>
|
|
<div class="win-subbar" style="flex-wrap:wrap;gap:4px;">
|
|
<input id="san-code" list="san-code-list" placeholder="코드(F7)" style="width:80px" value="${esc(u.sanCode || "")}">
|
|
<datalist id="san-code-list">${((ctx.dbfData && ctx.dbfData.costn_rows) || []).map((r) => `<option value="${esc(String(r.CODE).trim())}">${esc(r.NAME)} ${esc(r.SIZE)}</option>`).join("")}</datalist>
|
|
${sel(
|
|
"san-trans",
|
|
L().TRANS.map((t, i) => [String(i), t]),
|
|
u.sanTrans ?? "1",
|
|
)}
|
|
<span>중기비 코드</span>${sel(
|
|
"san-op",
|
|
[
|
|
["/", "/"],
|
|
["*", "*"],
|
|
],
|
|
u.sanOp || "/",
|
|
)}${sel(
|
|
"san-qv",
|
|
["Q", "A", "T", "N"].map((v) => [v, v]),
|
|
u.sanQv || "Q",
|
|
)}<span>(으)로 입력</span>
|
|
<label><input type="checkbox" id="san-skip0" ${u.sanSkip0 ? "checked" : ""}> 단가가 0원인 비목은 입력안함</label>
|
|
<button class="win-btn" id="san-ins">코드 삽입</button>
|
|
<span style="margin-left:auto;">결과적용 비목 ${sel("san-sbmok", SB, String(row.SBMOK || "").trim())}</span>
|
|
<span title="Ed_NAGCD — 낙찰율(%) · 0 = 없음">낙찰율 <input id="san-nag" type="number" step="0.001" style="width:70px" value="${num(row.NAGCD) || ""}">%</span>
|
|
<label title="소수점을 정리한 결과값에 낙찰율을 적용하여 금액을 산출"><input type="checkbox" id="san-uc" ${String(row.UC || "").trim() === "T" ? "checked" : ""}> 결과값에 낙찰율적용</label>
|
|
<label title="인쇄시 낙찰율이 표시되지 않고 『계약단가』로 인쇄"><input type="checkbox" id="san-cont" ${String(row.BCONT || "").trim() === "T" ? "checked" : ""}> 계약단가</label>
|
|
<label><input type="checkbox" id="san-adj" ${adjOn ? "checked" : ""}> 결과금액조정</label>
|
|
${adjOn ? ["M", "L", "S"].map((k) => `${k}:x <input class="san-adjv" data-k="${k}" type="number" step="0.00001" style="width:64px" value="${num(row[`ADJ_${k}`]) || 1}">`).join(" ") : ""}
|
|
</div>
|
|
${u.sanHelp ? `<div class="pane-box" style="max-height:170px;overflow:auto;font-size:12px;padding:4px 8px;white-space:pre;">${L().GRAMMAR_HELP.map(esc).join("\n")}</div>` : ""}
|
|
<div class="pane-box" style="flex:1;min-height:180px;">
|
|
<textarea id="smt-formula-textarea" class="smt-editor-area" data-code="${esc(code)}" spellcheck="false">${esc(text)}</textarea>
|
|
</div>
|
|
<div class="pane-box" style="max-height:110px;overflow:auto;">
|
|
<div class="pane-title"><span>문법 오류 ${errs.length}건</span></div>
|
|
${errs.map((e) => `<div class="san-err" data-line="${e.lineNo}" style="color:#b91c1c;cursor:pointer;font-size:12px;">${e.lineNo ? `${e.lineNo}번째줄: ` : ""}${esc(e.msg)}</div>`).join("") || "<div style='font-size:12px;color:#15803d;'>오류 없음</div>"}
|
|
</div>
|
|
<div class="pane-box" style="flex:1;">
|
|
<div class="pane-title"><span>계산결과(Alt+F9) — 변수 ${esc(
|
|
Object.entries(ev.vars)
|
|
.map(([k, v]) => `${k}=${v}`)
|
|
.join(", ") || "없음",
|
|
)}</span>
|
|
<span>재 ${fmt(ev.sumM)} + 노 ${fmt(ev.sumL)} + 경 ${fmt(ev.sumS)} = <strong>${fmt(ev.sumT)}원</strong></span></div>
|
|
<div class="grid-scroll"><table class="dbgrid">
|
|
<thead><tr><th style="width:36px">줄</th><th>산식</th><th style="width:140px">대입·금액변수</th><th style="width:150px">참조</th><th style="width:86px">참조단가</th><th style="width:90px">금액</th></tr></thead>
|
|
<tbody>${lines
|
|
.map(
|
|
(
|
|
ln,
|
|
) => `<tr style="${ln.errors.length ? "background:#fee2e2;" : ln.excluded ? "color:#6b7280;" : ""}"><td class="center">${ln.lineNo}</td>
|
|
<td style="font-family:Consolas,monospace;">${u.sanHilite === false ? esc(ln.raw) : L().highlight(ln.raw)}</td>
|
|
<td>${esc(ln.assigned || "")}${ln.excluded ? " (제외)" : ""}</td>
|
|
<td>${ln.refInfo ? `${esc(ln.refInfo.refCode)}[${ln.refInfo.compTag}] ${esc(ln.refInfo.refName)}` : ""}</td>
|
|
<td class="num">${ln.refInfo ? fmt(ln.refInfo.baseUnitPrice) : ""}</td>
|
|
<td class="num">${ln.refInfo ? fmt(ln.refInfo.calcAmt, 1) : ""}</td></tr>`,
|
|
)
|
|
.join("")}</tbody>
|
|
</table></div>
|
|
</div>`;
|
|
}
|
|
|
|
/**
|
|
* 낙찰율(Ed_NAGCD) · 결과값에 낙찰율적용(Ck_UC → COSTN UC) — 26 §3·§4 · REA 30 TWM_EDIT_SAN
|
|
* 처음 걸 때 지금 1칸 단가를 2칸(원단가) 자리로 옮기고, 지우면 2칸 원단가를 1칸으로 되돌림
|
|
* 다시 셈(계산완료)은 smt_engine — 2칸 = 산근 결과 · 1칸 = 비목별 원단가 × 낙찰율 버림(실무 7건 UC=T ⇔ NAGCD>0)
|
|
*/
|
|
function setBidRate(row, rate, uc = true) {
|
|
const r = num(rate),
|
|
had = num(row.NAGCD) > 0;
|
|
const cols = ["T", "M", "L", "S"];
|
|
if (r > 0 && !had) cols.forEach((k) => (row[`${k}AMT2`] = String(row[`${k}AMT1`] || "0")));
|
|
if (!(r > 0) && had) cols.forEach((k) => (row[`${k}AMT1`] = String(row[`${k}AMT2`] || "0")));
|
|
row.NAGCD = String(r > 0 ? r : 0);
|
|
row.UC = r > 0 && uc ? "T" : "F";
|
|
return row;
|
|
}
|
|
|
|
// 순환 — 이 산근이 부르는 코드가 다시 이 산근에 닿는지(REA 30 mode1 순환참조 검사)
|
|
function cycleErrors(ctx, code) {
|
|
const C = COMP();
|
|
const deps = C.depsOf(ctx, code);
|
|
const cyc = ENG().findCycles(deps, new Set([code, ...C.reach(deps, code)]));
|
|
return cyc.filter((p) => p.includes(code)).map((p) => `순환참조: ${p.join(" → ")}`);
|
|
}
|
|
|
|
function insertAt(area, text) {
|
|
const s = area.selectionStart ?? area.value.length,
|
|
e = area.selectionEnd ?? s;
|
|
area.value = area.value.slice(0, s) + text + area.value.slice(e);
|
|
area.selectionStart = area.selectionEnd = s + text.length;
|
|
area.focus();
|
|
}
|
|
// 줄 머리에 끼움 — 커서가 있는 줄 다음에 새 줄로
|
|
function insertLines(area, lines) {
|
|
const v = area.value,
|
|
at = v.indexOf("\n", area.selectionStart ?? v.length);
|
|
const pos = at < 0 ? v.length : at + 1;
|
|
const add = (at < 0 && v && !v.endsWith("\n") ? "\r\n" : "") + lines.join("\r\n") + "\r\n";
|
|
area.value = v.slice(0, pos) + add + v.slice(pos);
|
|
area.selectionStart = area.selectionEnd = pos + add.length;
|
|
area.focus();
|
|
}
|
|
|
|
function bind(ctx, app) {
|
|
const $ = (id) => document.getElementById(id);
|
|
const code = ctx.uiState.selectedIlwiCode;
|
|
const u = ctx.uiState;
|
|
u.editedSmtMap = u.editedSmtMap || {};
|
|
document
|
|
.querySelectorAll(".ilwi-sub-btn")
|
|
.forEach(
|
|
(b) => (b.onclick = () => ((u.ilwiSubFilter = b.dataset.sub), app.renderWorkspace())),
|
|
);
|
|
document
|
|
.querySelectorAll(".ilwi-master-row")
|
|
.forEach(
|
|
(tr) =>
|
|
(tr.onclick = () => (
|
|
(u.selectedIlwiCode = tr.dataset.code),
|
|
(u.compSel = []),
|
|
app.renderWorkspace()
|
|
)),
|
|
);
|
|
if ($("san-btn-eq"))
|
|
$("san-btn-eq").onclick = () => ((u.activeScreen = "wM_Edit_EQ"), app.renderWorkspace());
|
|
const area = $("smt-formula-textarea");
|
|
if (!area) return COMP().bind(ctx, code, app);
|
|
const keep = () => (u.editedSmtMap[code] = area.value);
|
|
area.oninput = keep;
|
|
const redo = () => (keep(), app.renderWorkspace());
|
|
area.onkeydown = (e) => {
|
|
if (e.key === "F9") (e.preventDefault(), $("san-calc").click());
|
|
if (e.ctrlKey && (e.key === "i" || e.key === "I")) (e.preventDefault(), $("san-img").focus());
|
|
};
|
|
const pick = (id, fn) =>
|
|
$(id) &&
|
|
($(id).onchange = (e) => {
|
|
if (e.target.value === "") return;
|
|
fn(e.target.value);
|
|
e.target.value = "";
|
|
redo();
|
|
});
|
|
pick("san-func", (v) => insertAt(area, v));
|
|
pick("san-mark", (v) => insertLines(area, [v]));
|
|
pick("san-qty", (v) => insertAt(area, v));
|
|
// 그림 넣기 — 「IMG(I00001)」 한 줄(문법설명 (5) · 그림코드는 한 줄에 하나)
|
|
pick("san-img", (v) => insertLines(area, [`IMG(${v})`]));
|
|
pick("san-eq", (v) => {
|
|
const it = (u.eqItems || [])[Number(v)];
|
|
if (!it) return;
|
|
// 「공식」 = 계산식 줄만 · 「공식+코드」 = 단가코드 줄까지
|
|
const ls = String(it.text || "").split(/\r?\n/);
|
|
insertLines(area, it.ins_x ? ls : ls.filter((l) => !/&[^&]+&/.test(l)));
|
|
});
|
|
const keepOpt = (id, key) =>
|
|
$(id) &&
|
|
($(id).onchange = (e) =>
|
|
(u[key] = e.target.type === "checkbox" ? e.target.checked : e.target.value));
|
|
[
|
|
["san-trans", "sanTrans"],
|
|
["san-op", "sanOp"],
|
|
["san-qv", "sanQv"],
|
|
["san-skip0", "sanSkip0"],
|
|
["san-code", "sanCode"],
|
|
].forEach(([i, k]) => keepOpt(i, k));
|
|
$("san-ins").onclick = () => {
|
|
const c = $("san-code").value.trim().toUpperCase();
|
|
const r = ENG().costnLookup(ctx).byCode[c];
|
|
if (!r) return alert(`${c || "(빈 코드)"} 는 등록된 단가코드가 아님`);
|
|
if (c === code) return alert("자기 자신은 넣을 수 없음(순환참조)");
|
|
insertLines(
|
|
area,
|
|
L().codeLines(c, r, {
|
|
trans: $("san-trans").value,
|
|
op: $("san-op").value,
|
|
qtyVar: $("san-qv").value,
|
|
skipZero: $("san-skip0").checked,
|
|
}),
|
|
);
|
|
redo();
|
|
};
|
|
$("san-strip").onclick = () => ((area.value = L().stripResults(area.value)), redo());
|
|
$("san-help").onclick = () => ((u.sanHelp = !u.sanHelp), app.renderWorkspace());
|
|
$("san-hilite").onclick = () => ((u.sanHilite = u.sanHilite === false), app.renderWorkspace());
|
|
document.querySelectorAll(".san-err").forEach(
|
|
(el) =>
|
|
(el.onclick = () => {
|
|
const n = Number(el.dataset.line);
|
|
if (!n) return;
|
|
const ls = area.value.split("\n");
|
|
const s = ls.slice(0, n - 1).join("\n").length + (n > 1 ? 1 : 0);
|
|
area.focus();
|
|
area.setSelectionRange(s, s + (ls[n - 1] || "").length);
|
|
}),
|
|
);
|
|
const row = ENG().costnLookup(ctx).byCode[code] || {};
|
|
$("san-sbmok").onchange = (e) => ((row.SBMOK = e.target.value), redo());
|
|
const bid = () => (
|
|
keep(),
|
|
setBidRate(row, $("san-nag").value, $("san-uc").checked),
|
|
app.cascadeAndRender()
|
|
);
|
|
$("san-nag").onchange = bid;
|
|
$("san-uc").onchange = bid;
|
|
// 계약단가 표시(BCONT) — 인쇄용 · 저장 묶음에 덧붙임(app.js decorateSavePayload)
|
|
$("san-cont").onchange = (e) => {
|
|
row.BCONT = e.target.checked ? "T" : "F";
|
|
(u.costnExtra = u.costnExtra || {})[code] = { BCONT: row.BCONT };
|
|
};
|
|
$("san-adj").onchange = (e) => {
|
|
row.S_ADJ = e.target.checked ? "T" : "F";
|
|
for (const k of "MLS") if (!num(row[`ADJ_${k}`])) row[`ADJ_${k}`] = "1";
|
|
redo();
|
|
};
|
|
document
|
|
.querySelectorAll(".san-adjv")
|
|
.forEach(
|
|
(el) =>
|
|
(el.onchange = () => ((row[`ADJ_${el.dataset.k}`] = String(num(el.value))), redo())),
|
|
);
|
|
$("san-calc").onclick = () => {
|
|
keep();
|
|
const txt = area.value;
|
|
const ev = ENG().evaluateSmtFormulaText(txt, ctx, code);
|
|
const cyc = cycleErrors(ctx, code);
|
|
const n = ev.errors.length + cyc.length;
|
|
if (
|
|
n &&
|
|
!confirm(
|
|
`문법 오류 ${n}건 — 그래도 계산 결과를 단가에 반영할까요?\n${ev.errors
|
|
.slice(0, 5)
|
|
.map((e) => `${e.lineNo}번째줄: ${e.msg}`)
|
|
.concat(cyc)
|
|
.join("\n")}`,
|
|
)
|
|
)
|
|
return app.renderWorkspace();
|
|
const memos = ctx.dbfData.costn_memos || (ctx.dbfData.costn_memos = {});
|
|
memos[code] = Object.assign({ code, blockNo: 0 }, memos[code], {
|
|
text: txt,
|
|
byteLen: txt.length,
|
|
});
|
|
global.STMATE_VIEW_PRJ_BOQ.syncCostnUnitPrice(ctx, code, ev.sumM, ev.sumL, ev.sumS);
|
|
app.cascadeAndRender();
|
|
};
|
|
}
|
|
|
|
P.screens.wM_Edit_iLWi = { render, bind };
|
|
global.STMATE_EDIT_SAN = { render, bind, cycleErrors, insertLines, setBidRate };
|
|
if (typeof module !== "undefined" && module.exports) module.exports = global.STMATE_EDIT_SAN;
|
|
})(typeof window !== "undefined" ? window : globalThis);
|