Files
Aislo/tools/stmate-test/ui_sys.js
T

292 lines
11 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("");
}
/**
* 시간당 노임산출계수 적용코드(wM_Set_JLab · Grd_BDLst → Grd_CONLTD) — 노무비(L) 코드 목록 고르기
* conKey = CONLD|CONTD|CON1D~4D · 짝 환산식 = RXXNM|RXTNM|RX1NM~4NM(빈 칸이면 RXXNM)
* 적용단가 = 일 노임(LAMT1) × 환산식(왼쪽부터) · JCDNM 자리 미만 버림(smt_eval.hourlyWage)
* 돌려줌 { formula, target: [{code,name,size,unit,price,hourly}], source: [고를 수 있는 L 코드 행] }
*/
const rxOf = (conKey) =>
conKey === "CONLD" ? "RXXNM" : (RX_SETS.find((x) => x.con === conKey) || {}).rx || "RXXNM";
function jlab(costnRows, ini, conKey, hourly) {
const i = ini || {};
const formula = String(i[rxOf(conKey)] || i.RXXNM || "").trim();
const digits = Number(String(i.JCDNM || "0").trim()) || 0;
const by = new Map((costnRows || []).map((r) => [String(r.CODE || "").trim(), r]));
const list = splitCodes(i[conKey]);
const rowOf = (c) => {
const r = by.get(c) || {};
const price = Number(r.LAMT1) || 0;
const h = hourly ? hourly(price, formula, digits) : null;
return {
code: c,
name: r.NAME || "",
size: r.SIZE || "",
unit: r.UNIT || "",
price,
hourly: h,
};
};
const inList = new Set(list);
const source = (costnRows || [])
.map((r) => String(r.CODE || "").trim())
.filter((c) => /^L\d{5}$/.test(c) && !inList.has(c))
.map(rowOf);
return { formula, target: list.map(rowOf), source };
}
// 선택코드 추가·삭제 — 차례 유지 · 겹침 없음 · 결과 = 이어 붙인 글(Start8x.Ini 값)
function jlabEdit(cur, add, del) {
const gone = new Set((del || []).map((c) => String(c).trim().toUpperCase()));
const out = splitCodes(cur).filter((c) => !gone.has(c));
for (const c of add || []) {
const k = String(c).trim().toUpperCase();
if (/^[A-Z]\d{5}$/.test(k) && !out.includes(k)) out.push(k);
}
return joinCodes(out);
}
global.STMATE_UI_SYS = {
jlab,
jlabEdit,
rxOf,
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);