Files
Aislo/tools/stmate-test/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

244 lines
9.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* STMATE w 8.0 — 초기값 입력및 수정(wM_Set_Sys) 순수 계산 (ui_sys.js) · Start8x.Ini(dbfData.start_ini) 편집
* 근거: REA 분석/14_설정계수추적/resource-TWM_SET_SYS.dfm.json(항목 차례·캡션·선택지·호환규정 기준 메모)
* · 작업메모(cmBx_CHN ItemIndex 0 = 비목별 먼저 = 값 참 · cmBx_ARE 당초/변경 = 환율 칸 1/2 · OPTNM 기본 SML)
* · 22_UI_폼_분석 §3.4(KCD·ACD·ZCD·JCD·ECD·BPN = 소수 자리수)
* CUTNM 값 대응(1 = 「1 자리」 · 0 = 「버림」)은 CHGNM 과 같은 참/거짓 꼴로 본 추정
*/
(function (global) {
"use strict";
const D012 = [
["0", "0 자리"],
["1", "1 자리"],
["2", "2 자리"],
];
const CALC = [
["1", "비목별금액 먼저계산"],
["0", "총액 먼저 계산"],
];
// 환율·노임계수 — 칸 이름의 {n} 은 단가 조사시점(ARENM 1 당초 · 2 변경)
const RX_SETS = [
{ id: "T", rx: "RXTNM", con: "CONTD", cap: "계수 ;터널구간", btn: "T 기타 노임산출계수" },
{ id: "1", rx: "RX1NM", con: "CON1D", cap: "계수-1;장대터널", btn: "[1] 기타 노임산출계수" },
{ id: "2", rx: "RX2NM", con: "CON2D", cap: "계수-2;펌프준설선", btn: "[2] 기타 노임산출계수" },
{ id: "3", rx: "RX3NM", con: "CON3D", cap: "계수-3;GRAB준설선", btn: "[3] 기타 노임산출계수" },
{ id: "4", rx: "RX4NM", con: "CON4D", cap: "계수-4;버켓준설선", btn: "[4] 기타 노임산출계수" },
];
const FIELDS = {
rate: [
{
key: "EXLNM{n}",
cap: "A 미국 USD (₩/$)",
type: "num",
hint: "외산장비중 $장비에 적용될 미국 USD 환율(경비중 단위가『$』인 장비)",
},
{
key: "EXHNM{n}",
cap: " └ 10만$ 이상 장비",
type: "num",
hint: "외산장비『$』(10만$이상)에 적용될 환율",
},
{ key: "EXEUR{n}", cap: "유로 EUR (₩/€)", type: "num", hint: "경비중 단위가『€』인 장비" },
{
key: "EXYEN{n}",
cap: "일본 JPY100(₩/100¥)",
type: "num",
hint: "경비중 단위가『¥』인 장비",
},
{
key: "EXRNM",
cap: "B 환율 자동적용 범위",
type: "sel",
opts: [
["0", "시간당중기사용료 단독적용"],
["1", "중기, 대가, 산근, 내역 전체"],
],
},
{
key: "RXXNM",
cap: "C 시간당 노임산출계수",
type: "text",
hint: "시간당 노임산출계수(ex. 일반구간)",
},
{ key: "CONLD", cap: " └ 적용코드", type: "codes" },
],
digits: [
{ key: "KCDNM", cap: "D 중기사용료 금액소수점선택", type: "sel", opts: D012 },
{
key: "JCDNM",
cap: "중기 노임 단가소수점선택",
type: "sel",
opts: D012,
hint: '"X" 코드에서 "L(중기노임)"코드 사용시 적용',
},
{
key: "ECDNM",
cap: "중기$장비 단가소수점선택",
type: "sel",
opts: D012,
hint: '"X" 코드에서 "S($장비)"코드 사용시 적용',
},
{ key: "ACDNM", cap: "E 단가산출서 금액소수점선택", type: "sel", opts: D012 },
{ key: "BPNNM", cap: "일위대가표 금액소수점선택", type: "sel", opts: D012 },
{
key: "ZCDNM",
cap: "F 단가산출서 수량소수점선택",
type: "sel",
opts: [
["2", "2 자리"],
["3", "3 자리"],
],
},
{
key: "CUTNM",
cap: "계금액 3원미만 결과소수점",
type: "sel",
opts: [
["1", "1 자리"],
["0", " 버림"],
],
hint: "대가,산근 계산결과 3원미만일 때 결과금액 자리수",
},
],
calc: [
{ key: "CHGNM", cap: "G 단가산출근거", type: "sel", opts: CALC },
{ key: "CHBNM", cap: "일위대가표", type: "sel", opts: CALC },
{ key: "CHNNM", cap: "H 내역서", type: "sel", opts: CALC },
],
};
// 호환규정(XML·조달청) 계산방식 일괄적용 — Memo_Hint_XML 원문 값
const XML_PRESET = { EXRNM: "1", KCDNM: "1", JCDNM: "1", ECDNM: "0", ACDNM: "1", BPNNM: "1" };
Object.assign(XML_PRESET, { ZCDNM: "2", CUTNM: "0", CHGNM: "1", CHBNM: "1", CHNNM: "1" });
const ORDER_NAMES = { S: "경 비", M: "재 료 비", L: "노 무 비" };
const keyOf = (f, n) => f.key.replace("{n}", String(n));
const bool01 = (v) => (/^(1|t|true)$/i.test(String(v == null ? "" : v).trim()) ? "1" : "0");
// 편집 초안 — 원본 칸 값을 문자열로 복사(없는 칸은 빈 값)
function draftFromIni(ini) {
const src = ini || {};
const d = {};
const take = (k) => (d[k] = src[k] == null ? "" : String(src[k]));
[1, 2].forEach((n) => FIELDS.rate.forEach((f) => f.key.includes("{n}") && take(keyOf(f, n))));
FIELDS.rate.forEach((f) => !f.key.includes("{n}") && take(f.key));
[...FIELDS.digits, ...FIELDS.calc].forEach((f) => take(f.key));
RX_SETS.forEach((s) => (take(s.rx), take(s.con)));
["ARENM", "OPTNM"].forEach(take);
["CHGNM", "CHBNM", "CHNNM", "CUTNM", "EXRNM"].forEach(
(k) => d[k] !== "" && (d[k] = bool01(d[k])),
);
if (!/^[SML]{3}$/.test(d.OPTNM) || new Set(d.OPTNM).size !== 3) d.OPTNM = "SML";
if (d.ARENM !== "2") d.ARENM = "1";
return d;
}
// 적용코드 — 6자리 코드를 이어 붙인 글(CONLD=L00008L00009L00010)
const splitCodes = (s) => String(s || "").match(/[A-Z]\d{5}/g) || [];
const joinCodes = (list) =>
(list || [])
.map((c) => String(c).trim().toUpperCase())
.filter((c) => /^[A-Z]\d{5}$/.test(c))
.join("");
// 노임 환산식 검사 — 숫자·사칙·괄호·공백만
function checkFormula(s) {
const t = String(s || "").trim();
if (!t) return { ok: true, value: null };
if (!/^[\d.\s+\-*/()]+$/.test(t))
return { ok: false, error: "숫자와 + - * / ( ) 만 쓸 수 있음" };
try {
const v = Function(`"use strict";return (${t});`)();
return Number.isFinite(v) ? { ok: true, value: v } : { ok: false, error: "계산 불가" };
} catch (_e) {
return { ok: false, error: "식 문법 오류" };
}
}
// 검사 — 환율 숫자 · 환산식 · 적용코드
function validate(d) {
const errs = [];
[1, 2].forEach((n) =>
FIELDS.rate.forEach((f) => {
if (f.type !== "num") return;
const v = d[keyOf(f, n)];
if (v !== "" && !Number.isFinite(Number(v))) errs.push(`${keyOf(f, n)} 숫자 아님`);
}),
);
["RXXNM", ...RX_SETS.map((s) => s.rx)].forEach((k) => {
const c = checkFormula(d[k]);
if (!c.ok) errs.push(`${k}: ${c.error}`);
});
["CONLD", ...RX_SETS.map((s) => s.con)].forEach((k) => {
const raw = String(d[k] || "");
if (raw && joinCodes(splitCodes(raw)) !== raw.replace(/\s/g, ""))
errs.push(`${k}: 코드는 영문 1자 + 숫자 5자리`);
});
return errs;
}
// 반영 — 바뀐 칸만 ini 에 씀 · 바뀐 칸 목록
function applyDraft(ini, d) {
const changed = [];
Object.keys(d).forEach((k) => {
const before = ini[k] == null ? "" : String(ini[k]);
const isBool = ["CHGNM", "CHBNM", "CHNNM", "CUTNM", "EXRNM"].includes(k);
if ((isBool && before !== "" ? bool01(before) : before) === d[k]) return;
ini[k] = d[k];
changed.push(k);
});
return changed;
}
function xmlPreset(d) {
Object.assign(d, XML_PRESET);
[1, 2].forEach((n) => (d[`EXHNM${n}`] = d[`EXLNM${n}`]));
return d;
}
// 계산 원천(state.sysConfig) 맞춤 — calc.js 가 읽는 칸
function sysConfigFromIni(ini, prev) {
const s = Object.assign({}, prev || {});
if (ini.RXXNM) s.rxFormula = String(ini.RXXNM).replace(/\s/g, "");
s.condCodes = splitCodes(ini.CONLD).join(", ");
if (ini.EXLNM1 !== undefined && ini.EXLNM1 !== "") s.usdRate1 = Number(ini.EXLNM1);
if (ini.EXLNM2 !== undefined && ini.EXLNM2 !== "") s.usdRate2 = Number(ini.EXLNM2);
s.useChangedUsd = String(ini.ARENM) === "2";
if (ini.ECDNM !== undefined && ini.ECDNM !== "") s.ecdDigits = Number(ini.ECDNM) || 0;
if (ini.CHNNM !== undefined && ini.CHNNM !== "")
s.chnMode = bool01(ini.CHNNM) === "0" ? "total" : "bimok";
if (/^[SML]{3}$/.test(String(ini.OPTNM || ""))) s.optOrder = ini.OPTNM;
return s;
}
// 보정순서 한 칸 옮김 — OPTNM 글자 차례
function moveOrder(order, i, dir) {
const a = String(order).split("");
const j = i + dir;
if (j < 0 || j >= a.length) return order;
[a[i], a[j]] = [a[j], a[i]];
return a.join("");
}
global.STMATE_UI_SYS = {
FIELDS,
RX_SETS,
XML_PRESET,
ORDER_NAMES,
keyOf,
bool01,
draftFromIni,
splitCodes,
joinCodes,
checkFormula,
validate,
applyDraft,
xmlPreset,
sysConfigFromIni,
moveOrder,
};
if (typeof module !== "undefined" && module.exports) module.exports = global.STMATE_UI_SYS;
})(typeof window !== "undefined" ? window : globalThis);