Files
Aislo/tools/stmate-test/scr_ui_sys.js
T
eomsangdonandClaude Opus 5.5 3d8fddfc34 feat(stmate-test): 보고서·제잡비·식언어·수량산출·화면 묶음 합침 · 재계산 후 저장 무변경
- 보고서: 원본 STMATE 엑셀과 셀 대조(목록표·자재단가대비·중기·일위대가·산근 목록·설계내역서·원가계산서) · 조달청 XML 공식 XSD 검증 · PM_Prt 화면
- 제잡비: 원본 계산 순서 4형식(T/S/TA/SA) · 실무 7건 저장 결과키 전수 1원 · 요율표 조회 · 선택지 계산 · 제잡비·제비율 화면
- 식언어·수량: 함수·조건식·금액변수·문법 검사·수량변수·QCODE · 산근·구성·중기공식 편집 화면 · 순환 알림
- 화면: 원본 메뉴 체계·단축키 · 설계내역서 편집(행 넣기·지우기·이동·레벨·보관함) · 초기값 · 단가목록표
- 저장: 초기값(Start8x.Ini) 바뀐 키 저장 · 노임 환산 직종 = 초기값 CONLD · 산근·구성 결과에 적용단가 칸 채우지 않음(원본 0)
- DBF 글 칸 앞 빈칸 보존 · 시험 도우미 표준입력 utf8(한글 조각 깨짐 원인)
- 실무 5건 재계산 후 저장 = 바뀐 칸 0 · 시험 332건 통과

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 19:12:04 +09:00

174 lines
9.5 KiB
JavaScript

/**
* STMATE w 8.0 — 초기값 입력및 수정 화면(wM_Set_Sys) 끼워 넣기 (scr_ui_sys.js)
* 배치 근거: resource-TWM_SET_SYS.dfm.json — 환율/노임계수 · 소수점 적용기준 · 계산방식 · 금액 보정순서
* · 단가의 조사시점 · 재료비 적용단가 일괄적용 · 공사원가호환규정(XML) 계산방식 일괄적용
* 편집 = ctx.uiState.sysDraft(초안) → [전체 재계산후 종료]·[작업 종료] 때 dbfData.start_ini 와 state.sysConfig 에 반영
*/
(function (global) {
"use strict";
const P = (global.STMATE_PLUGINS = global.STMATE_PLUGINS || {
screens: {},
actions: {},
menus: [],
});
const S = global.STMATE_UI_SYS;
const U = global.STMATE_UI_DAN;
const esc = (s) =>
String(s == null ? "" : s).replace(
/[&<>"]/g,
(c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c],
);
function draft(ctx) {
const u = ctx.uiState;
const ini = ctx.dbfData.start_ini || (ctx.dbfData.start_ini = {});
if (!u.sysDraft || u.sysDraftSrc !== ctx.currentSourcePath) {
u.sysDraft = S.draftFromIni(ini);
u.sysDraftSrc = ctx.currentSourcePath;
u.sysRx = u.sysRx || "T";
}
return u.sysDraft;
}
function field(f, d, n) {
const k = S.keyOf(f, n || d.ARENM);
const v = d[k] == null ? "" : d[k];
const title = f.hint ? ` title="${esc(f.hint)}"` : "";
if (f.type === "sel")
return `<select class="win-select sys-f" data-k="${k}"${title}>${f.opts
.map(
([o, t]) =>
`<option value="${o}" ${String(v) === o ? "selected" : ""}>${esc(t)}</option>`,
)
.join(
"",
)}${f.opts.some(([o]) => o === String(v)) ? "" : `<option value="${esc(v)}" selected>(원본 값 ${esc(v) || "없음"})</option>`}</select>`;
if (f.type === "codes")
return `<input class="win-input sys-f" data-k="${k}" value="${esc(S.splitCodes(v).join(", "))}" style="width:240px;"${title} placeholder="L00008, L00009" />`;
return `<input class="win-input sys-f" data-k="${k}" value="${esc(v)}" style="width:${f.type === "num" ? 110 : 240}px;"${title} />`;
}
const row = (f, d, n) =>
`<div class="stm-row"><label${f.hint ? ` title="${esc(f.hint)}"` : ""}>${esc(f.cap)}</label>${field(f, d, n)}</div>`;
function render(ctx) {
if (!ctx.dbfData)
return `<div class="win-form"><div class="win-form-header"><span>초기값 입력및 수정 [wM_Set_Sys]</span></div><p class="stm-note" style="padding:12px;">열린 프로젝트 없음 — 초기값(Start8x.Ini)은 프로젝트를 연 뒤 고칩니다.</p></div>`;
const d = draft(ctx),
u = ctx.uiState,
ini = ctx.dbfData.start_ini;
const changed = S.applyDraft(Object.assign({}, ini), d);
const rx = S.RX_SETS.find((s) => s.id === u.sysRx) || S.RX_SETS[0];
const rxv = S.checkFormula(d.RXXNM);
const slots = U.slotNames(ini);
const errs = S.validate(d);
return `<div class="win-form" id="sys-form">
<div class="win-form-header"><span>초기값 입력및 수정 [wM_Set_Sys · Start8x.Ini]</span><span>바뀐 칸 ${changed.length}개${changed.length ? ` (${esc(changed.join(", "))})` : ""}</span></div>
<div class="win-subbar"><strong>※ 수정하기 원하는 항목을 고치세요 ※</strong>
<button class="win-btn primary" id="sys-recalc">전체 재계산후 종료(X)</button><button class="win-btn" id="sys-close">작 업 종 료 (Q)</button>
<button class="win-btn" id="sys-cancel">수정내용 모두 취소</button>
<button class="win-btn" id="sys-xml" title="${esc("공사원가 호환규정(XML) 계산방식 기준 — 환율 10만$ 미만/이상 동일 · 범위 전체 · D 1·1·0 · E 1·1 · F 2 · 3원미만 버림 · G·H 비목별 먼저")}">공사원가호환규정(XML,조달청) 계산방식 일괄적용</button>
${errs.length ? `<span class="stm-warn">${esc(errs.join(" · "))}</span>` : ""}</div>
<div class="stm-sys-grid">
<fieldset class="stm-group"><legend>환 율 / 노 임 계 수</legend>
<div class="stm-row"><label>K. 단가의 조사시점</label><select class="win-select sys-f" data-k="ARENM"><option value="1" ${d.ARENM === "1" ? "selected" : ""}>당초시점</option><option value="2" ${d.ARENM === "2" ? "selected" : ""}>변경시점</option></select><span class="stm-note">환율 칸 ${d.ARENM === "2" ? "2(변경)" : "1(당초)"} 편집</span></div>
${S.FIELDS.rate.map((f) => row(f, d)).join("")}
<div class="stm-note">노임환산식 값: ${rxv.ok && rxv.value != null ? rxv.value.toFixed(8) : esc(rxv.error || "-")}</div>
<div class="stm-row"><label>기타 노임산출계수</label><select class="win-select" id="sys-rx-sel">${S.RX_SETS.map((s) => `<option value="${s.id}" ${s.id === rx.id ? "selected" : ""}>${esc(s.cap)}</option>`).join("")}</select></div>
<div class="stm-row"><label>${esc(rx.btn)}</label>${field({ key: rx.rx, type: "text" }, d)}</div>
<div class="stm-row"><label> └ 적용코드</label>${field({ key: rx.con, type: "codes" }, d)}</div>
</fieldset>
<fieldset class="stm-group"><legend>소 수 점 적 용 기 준</legend>${S.FIELDS.digits.map((f) => row(f, d)).join("")}</fieldset>
<fieldset class="stm-group"><legend>계 산 방 식</legend>${S.FIELDS.calc.map((f) => row(f, d)).join("")}
<div class="stm-row"><label title="계산방식이 【총액 먼저 계산】일 때 총액과 비목별 합계의 차액을 보정순서대로 보정">L 금액 보정순서</label><span class="stm-order">${d.OPTNM.split(
"",
)
.map(
(c, i) =>
`<span>${i + 1}. ${S.ORDER_NAMES[c]} <button class="win-btn sys-opt" data-i="${i}" data-dir="-1">◀</button><button class="win-btn sys-opt" data-i="${i}" data-dir="1">▶</button></span>`,
)
.join(
"",
)}</span><button class="win-btn" id="sys-opt-df" title="경비, 재료비, 노무비 순">기 본</button></div>
</fieldset>
<fieldset class="stm-group"><legend>재료비 적용단가 일괄적용</legend>
<div class="stm-row"><label>M. 적용할 물가지</label><select class="win-select" id="sys-selm"><option value="none">적용 안함</option>${slots.list.map((s) => `<option value="${s.slot}">${esc(s.name)}</option>`).join("")}<option value="min">최소 단가</option></select></div>
<div class="stm-row"><label><input type="checkbox" id="sys-skip0" checked /> 비교표의 단가가 0원인 것은 적용안함</label></div>
<div class="stm-row"><label><input type="checkbox" id="sys-keep" checked /> 사용자가 직접수정한 적용페이지 유지</label></div>
<div class="stm-row"><button class="win-btn primary" id="sys-selm-ok">확인 — 재료비 적용단가 일괄적용</button><span class="stm-note" id="sys-selm-msg">${esc(u.sysMsg || "")}</span></div>
<p class="stm-note">직접수정 판정 = 적용단가가 후보 5칸 어디에도 없음</p>
</fieldset>
</div>
</div>`;
}
function commit(ctx, api, recalc) {
const d = draft(ctx);
const errs = S.validate(d);
if (errs.length) return alert("고칠 칸: " + errs.join(" · "));
const changed = S.applyDraft(ctx.dbfData.start_ini, d);
ctx.state.sysConfig = S.sysConfigFromIni(ctx.dbfData.start_ini, ctx.state.sysConfig);
ctx.uiState.sysDraft = null;
ctx.uiState.sysMsg = "";
ctx.uiState.activeScreen = "wPrjList";
if (recalc) api.cascadeAndRender();
else api.renderWorkspace();
if (changed.length)
ctx.uiState.boqMsg = `초기값 ${changed.length}칸 반영(${changed.join(", ")})`;
}
function bind(ctx, api) {
if (!ctx.dbfData) return;
const d = draft(ctx),
u = ctx.uiState,
$ = (id) => document.getElementById(id);
document.querySelectorAll("#sys-form .sys-f").forEach(
(el) =>
(el.onchange = () => {
const k = el.getAttribute("data-k");
d[k] = /^CON/.test(k)
? S.joinCodes(String(el.value).split(/[,\s]+/))
: String(el.value).trim();
api.renderWorkspace();
}),
);
$("sys-rx-sel").onchange = (e) => ((u.sysRx = e.target.value), api.renderWorkspace());
document
.querySelectorAll("#sys-form .sys-opt")
.forEach(
(b) =>
(b.onclick = () => (
(d.OPTNM = S.moveOrder(
d.OPTNM,
Number(b.getAttribute("data-i")),
Number(b.getAttribute("data-dir")),
)),
api.renderWorkspace()
)),
);
$("sys-opt-df").onclick = () => ((d.OPTNM = "SML"), api.renderWorkspace());
$("sys-xml").onclick = () => (S.xmlPreset(d), api.renderWorkspace());
$("sys-cancel").onclick = () => ((u.sysDraft = null), api.renderWorkspace());
$("sys-recalc").onclick = () => commit(ctx, api, true);
$("sys-close").onclick = () => commit(ctx, api, false);
$("sys-selm-ok").onclick = () => {
const how = $("sys-selm").value;
if (how === "none") return;
const ch = U.adoptAll(ctx.dbfData.costn_rows, how, {
kinds: "M",
skipZero: $("sys-skip0").checked,
keepUser: $("sys-keep").checked,
});
ch.forEach((c) => {
const res = (ctx.state.resources || []).find((r) => r.code === c.code);
if (res) Object.assign(res, { adoptMode: `slot${c.slot}`, manualPrice: c.to });
});
u.sysMsg = `재료비 ${ch.length}건 적용단가 바꿈`;
api.cascadeAndRender();
};
}
P.screens.wM_Set_Sys = { render, bind };
})(window);