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>
This commit is contained in:
+80
-14
@@ -28,22 +28,79 @@
|
||||
|
||||
const FIELDS = {
|
||||
rate: [
|
||||
{ key: "EXLNM{n}", cap: "A 미국 USD (₩/$)", type: "num", hint: "외산장비중 $장비에 적용될 미국 USD 환율(경비중 단위가『$』인 장비)" },
|
||||
{ key: "EXHNM{n}", cap: " └ 10만$ 이상 장비", type: "num", hint: "외산장비『$』(10만$이상)에 적용될 환율" },
|
||||
{
|
||||
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: "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: "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원미만일 때 결과금액 자리수" },
|
||||
{
|
||||
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 },
|
||||
@@ -70,7 +127,9 @@
|
||||
[...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])));
|
||||
["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;
|
||||
@@ -78,13 +137,18 @@
|
||||
|
||||
// 적용코드 — 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("");
|
||||
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: "숫자와 + - * / ( ) 만 쓸 수 있음" };
|
||||
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: "계산 불가" };
|
||||
@@ -109,7 +173,8 @@
|
||||
});
|
||||
["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자리`);
|
||||
if (raw && joinCodes(splitCodes(raw)) !== raw.replace(/\s/g, ""))
|
||||
errs.push(`${k}: 코드는 영문 1자 + 숫자 5자리`);
|
||||
});
|
||||
return errs;
|
||||
}
|
||||
@@ -142,7 +207,8 @@
|
||||
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 (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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user