- 보고서: 원본 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>
244 lines
9.0 KiB
JavaScript
244 lines
9.0 KiB
JavaScript
/**
|
||
* 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);
|