/** * 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);