- 보고서: 원본 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>
375 lines
18 KiB
JavaScript
375 lines
18 KiB
JavaScript
/* -*- coding: utf-8 -*- */
|
||
/**
|
||
* 제비율표(wM_KAN_RATE) 화면 + 요율 조회 (scr_kanjub_rate.js)
|
||
* RATE_R(50×35) 행렬 = 5행 묶음(값 4행 + 빈 1행) · 열 1~5 간접노무비 · 6~10 기타경비(토목·산설토목·조경·건축·산설건축)
|
||
* 묶음 시작 행 = 공사규모(20 50억 미만 · 30 50~300억 · 35 300~1000억 · 40 1000억 이상 · 10 소규모 — 값이 있는 표만)
|
||
* 묶음 안 행 = 공사기간(I_KONG_KIKAN 화면 순번) · 일반관리비 열 11 · 이윤 열 16(묶음 첫 행)
|
||
* 부속표 — 산재[45,1] · 건강[46,1] · 연금[46,6] · 장기요양[46,11] · 고용 등급[45,10+등급] · 환경[47,공종] · 건설기계[49,구분+1]
|
||
* 안전관리비 — 열 17 율 · 18 기초액(천원) · 행 = 규모 묶음(10 5억 미만 · 20 5~50억 · 25 50억 이상) + 공종 자리(건축0·토목1·특수2·철도3·중건설4)
|
||
* 근거: 지식DB STmate/old/20_분석 06 §1~2 · 10 §4 · 12 §8~10 · 15 §4(좌표 확정) · 30_원자료/TWM_KAN_RATE.json
|
||
* 순환: 공사규모 밑수가 총공사비면 요율 → 금액 → 규모 → 요율 고리 — 왕복하면 자동 확정하지 않고 알림(06 §2 · 11 §8)
|
||
* 정답 대조: resources/tester/test_stmate_kanjub_rate.py(실무 7건 저장 요율)
|
||
*/
|
||
const StmateRate = (() => {
|
||
const n0 = (v) => Number(String(v ?? "").replace(/,/g, "")) || 0;
|
||
// 공사종류 내부값(cmBx_Kong_Type 0 토목·2 조경·4 산설토목·3 건축·1 산설건축·5 국가유산) → 간접노무비 열
|
||
const KONG_COL = { 0: 1, 4: 2, 2: 3, 3: 4, 1: 5, 5: 21 };
|
||
const KONG_NAME = {
|
||
0: "토목",
|
||
4: "산업설비(토목)",
|
||
2: "조경",
|
||
3: "건축",
|
||
1: "산업설비(건축)",
|
||
5: "국가유산",
|
||
};
|
||
// 공사규모 묶음(간접노무비·기타경비·일반관리비·이윤) — 시작 행 · 상한(원)
|
||
const BANDS = [
|
||
{ row: 10, max: 5e8, name: "5억 미만(소규모)", optional: true },
|
||
{ row: 20, max: 50e8, name: "50억 미만" },
|
||
{ row: 30, max: 300e8, name: "50억~300억 미만" },
|
||
{ row: 35, max: 1000e8, name: "300억~1000억 미만" },
|
||
{ row: 40, max: Infinity, name: "1000억 이상" },
|
||
];
|
||
const KIKAN = ["6개월 이하", "7개월~12개월 이하", "13개월~36개월 이하", "36개월 이상"];
|
||
// 안전관리비 — 공종 내부값(cmBx_AnJun_Type 건축0·토목1·중건설3·특수4) → 행 자리(옛 철도·궤도 = 2)
|
||
const ANJUN_OFF = { 0: 0, 1: 1, 4: 2, 2: 3, 3: 4 };
|
||
const ANJUN_BANDS = [
|
||
{ row: 10, max: 5e8, name: "5억 미만" },
|
||
{ row: 20, max: 50e8, name: "5억~50억 미만", base: true },
|
||
{ row: 25, max: Infinity, name: "50억 이상" },
|
||
];
|
||
const HEALTH_ROW = 35; // 보건관리자 선임 대상(800억 이상)
|
||
|
||
// DBF 행(RATE_01..·RATE0..) → 숫자 행렬
|
||
function matrixFromRows(rows) {
|
||
if (!Array.isArray(rows) || !rows.length) return [];
|
||
if (Array.isArray(rows[0])) return rows.map((r) => r.map(n0));
|
||
const keys = Object.keys(rows[0])
|
||
.filter((k) => /^RATE_?\d+$/.test(k))
|
||
.sort((a, b) => Number(a.replace(/\D/g, "")) - Number(b.replace(/\D/g, "")));
|
||
return rows.map((r) => keys.map((k) => n0(r[k])));
|
||
}
|
||
const cell = (m, r, c) => (m[r] && m[r][c - 1] != null ? n0(m[r][c - 1]) : 0);
|
||
|
||
// 규모 묶음 고르기 — 소규모 묶음은 그 표에 값이 있을 때만
|
||
function bandOf(m, scale, col) {
|
||
for (const b of BANDS) {
|
||
if (b.optional && !cell(m, b.row, col)) continue;
|
||
if (scale < b.max) return b;
|
||
}
|
||
return BANDS[BANDS.length - 1];
|
||
}
|
||
|
||
// 요율 조회 — opts: kongType · kikan · scale(직접공사비) · anjunType · anjunScale · koboType · hwan · eqip · tbugum
|
||
function lookup(m, opts = {}) {
|
||
const rates = {},
|
||
cells = {},
|
||
amounts = {},
|
||
notes = [];
|
||
const put = (code, r, c, v = cell(m, r, c)) => ((rates[code] = v), (cells[code] = [r, c]), v);
|
||
const col = KONG_COL[opts.kongType ?? 0] ?? 1,
|
||
kik = Math.min(3, Math.max(0, n0(opts.kikan)));
|
||
const band = bandOf(m, n0(opts.scale), col);
|
||
if (col === 21) {
|
||
// 국가유산 — 행 0~19 4묶음(0·5·10·15) · 열 21 간접노무비 · 26 기타경비(10 §4)
|
||
const b = [0, 5, 10, 15][Math.max(0, BANDS.indexOf(band) - 1)] ?? 0;
|
||
put("KANNO", b + kik, 21);
|
||
put("KITA", b + kik, 26);
|
||
} else {
|
||
put("KANNO", band.row + kik, col);
|
||
put("KITA", band.row + kik, col + 5);
|
||
}
|
||
const head = band.optional ? 20 : band.row;
|
||
put("ILBAN", head, opts.ilbanType === 1 ? 15 : 11);
|
||
put("IYUN", head, 16);
|
||
put("SANBO", 45, 1);
|
||
put("KUNBO", 46, 1);
|
||
put("YUNBO", 46, 6);
|
||
put("SLVBO", 46, 11);
|
||
const kg = n0(opts.koboType);
|
||
if (kg > 0) put("KOBO", 45, 10 + Math.min(7, kg));
|
||
else rates.KOBO = 0;
|
||
// 안전관리비 — 대상액 규모 · 공종 · 5~50억은 기초액(천원 → 원)
|
||
const off = ANJUN_OFF[n0(opts.anjunType)] ?? 1,
|
||
as = n0(opts.anjunScale);
|
||
const ab = opts.health ? { row: HEALTH_ROW } : ANJUN_BANDS.find((b) => as < b.max);
|
||
for (const c of ["ANJUN", "ANJUN1", "ANJUN2"]) put(c, ab.row + off, 17);
|
||
const g = ab.base ? cell(m, ab.row + off, 18) * 1000 : 0;
|
||
Object.assign(amounts, { C_ANJUN_G: g, C_ANJUN1_G: g, C_ANJUN2_G: g });
|
||
const hw = n0(opts.hwan);
|
||
if (hw > 0) put("HWAN", 47, hw);
|
||
else rates.HWAN = 0;
|
||
const eq = opts.eqip == null ? 0 : n0(opts.eqip);
|
||
if (eq >= 0) put("EQIP", 49, eq + 1);
|
||
else rates.EQIP = 0;
|
||
// 퇴직공제부금비 — 46행 16~19열(토목·준설·건축·기타) · 사용자 선택일 때만 넣음
|
||
put("TBUGUM", 46, 16 + Math.min(3, Math.max(0, n0(opts.tbugum))));
|
||
for (const [c, v] of Object.entries(rates))
|
||
if (!v && cells[c] && !["KOBO", "HWAN", "EQIP", "TBUGUM"].includes(c))
|
||
notes.push(`${c} — 표 칸 [${cells[c]}] 이 비어 있음(직접 입력 요율 유지)`);
|
||
return { rates, cells, amounts, band, anjunBand: ab, notes };
|
||
}
|
||
|
||
// 설정 → 조회 입력(직접공사비 · 안전관리비 대상액은 계산 결과에서)
|
||
function optsFromCfg(cfg, kj) {
|
||
const k = (cfg && cfg.keys) || {};
|
||
return {
|
||
kongType: n0(k.I_KONG_TYPE),
|
||
kikan: n0(k.I_KONG_KIKAN),
|
||
scale: kj ? n0(kj.dT) : 0,
|
||
anjunType: n0(k.I_ANJUN_TYPE),
|
||
anjunScale: kj ? n0(kj.anjunBaseRaw) : 0,
|
||
koboType: n0(k.I_KOBO_TYPE),
|
||
hwan: n0(k.I_HWAN),
|
||
eqip: k.I_EQIP == null ? 0 : n0(k.I_EQIP),
|
||
tbugum: n0(k.I_TBUGUM),
|
||
ilbanType: n0(k.I_ILBAN),
|
||
};
|
||
}
|
||
|
||
// 조회 결과 → 설정(R_·C_ANJUN_G 키와 옛 rates 칸 함께) · 빈 칸은 건너뜀 · 퇴직공제는 withTbugum 일 때만
|
||
function applyToCfg(cfg, res, { withTbugum = false } = {}) {
|
||
cfg.rates = cfg.rates || {};
|
||
cfg.keys = cfg.keys || {};
|
||
const RK = { KITA: "T_KITA", EQIP: "EQIPSU" };
|
||
for (const [c, v] of Object.entries(res.rates)) {
|
||
if (c === "TBUGUM" && !withTbugum) continue;
|
||
if (!v && !["KOBO", "HWAN", "EQIP", "TBUGUM"].includes(c)) continue;
|
||
cfg.rates[c] = v;
|
||
cfg.keys["R_" + (RK[c] || c)] = v;
|
||
}
|
||
Object.assign(cfg.keys, res.amounts);
|
||
cfg.anjunBaseAdd = res.amounts.C_ANJUN_G;
|
||
return cfg;
|
||
}
|
||
|
||
// 순환 점검 — 규모 밑수를 계산 결과에서 다시 잡을 때 묶음이 왕복하면 멈추고 알림(06 §2 계약)
|
||
function applyWithLoopCheck(cfg, m, calcFn, { scaleOf = (kj) => kj.dT, maxIter = 6 } = {}) {
|
||
const seen = [];
|
||
let kj = calcFn(cfg);
|
||
for (let i = 0; i < maxIter; i++) {
|
||
const res = lookup(m, Object.assign(optsFromCfg(cfg, kj), { scale: scaleOf(kj) }));
|
||
const sig = `${res.band.row}/${res.anjunBand.row}`;
|
||
if (seen.includes(sig) && seen[seen.length - 1] !== sig)
|
||
return {
|
||
cfg,
|
||
kj,
|
||
loop: true,
|
||
msg: "공사규모에 따른 제비율 변경으로 공사규모가 계속 변경되는 무한루프 — 일반관리비·이윤 등 요율을 직접 수정",
|
||
};
|
||
if (seen[seen.length - 1] === sig) return { cfg, kj, loop: false };
|
||
seen.push(sig);
|
||
applyToCfg(cfg, res);
|
||
kj = calcFn(cfg);
|
||
}
|
||
return { cfg, kj, loop: true, msg: "요율 확정이 수렴하지 않음 — 직접 확정" };
|
||
}
|
||
|
||
const api = { KONG_COL, KONG_NAME, BANDS, KIKAN, ANJUN_OFF, ANJUN_BANDS, HEALTH_ROW };
|
||
Object.assign(api, { matrixFromRows, cell, bandOf, lookup, optsFromCfg, applyToCfg });
|
||
return Object.assign(api, { applyWithLoopCheck });
|
||
})();
|
||
|
||
if (typeof window !== "undefined") window.StmateRate = StmateRate;
|
||
if (typeof module !== "undefined" && module.exports) module.exports = StmateRate;
|
||
|
||
// ── 화면 wM_KAN_RATE — 표 고르기(공사 안 RATE_* · 기준 JB_R) · 이름 붙은 표(규모·기간 · 부속표) · 원본 행렬 · 조회·적용
|
||
(function (global) {
|
||
if (!global.document) return;
|
||
const P = (global.STMATE_PLUGINS = global.STMATE_PLUGINS || {
|
||
screens: {},
|
||
actions: {},
|
||
menus: [],
|
||
});
|
||
const R = StmateRate;
|
||
const n0 = (v) => Number(String(v ?? "").replace(/,/g, "")) || 0;
|
||
const esc = (s) =>
|
||
String(s ?? "").replace(
|
||
/[&<>"]/g,
|
||
(c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c],
|
||
);
|
||
const STC_TABLES = ["RATE_R.DBF", "RATE_RA.DBF", "RATE_M.DBF", "RATE.DBF"];
|
||
|
||
function current(ctx) {
|
||
const u = ctx.uiState;
|
||
u.krMats = u.krMats || {};
|
||
const tables = (ctx.dbfData && ctx.dbfData.rate_tables) || {};
|
||
for (const t of STC_TABLES)
|
||
if (tables[t] && !u.krMats[t]) u.krMats[t] = R.matrixFromRows(tables[t]);
|
||
const src = u.krSrc && u.krMats[u.krSrc] ? u.krSrc : Object.keys(u.krMats)[0] || "";
|
||
return { u, src, m: u.krMats[src] || [] };
|
||
}
|
||
const box = (m, r, c, w = 54) =>
|
||
`<input class="kr-cell" data-r="${r}" data-c="${c}" value="${R.cell(m, r, c) || ""}" style="width:${w}px;text-align:right" title="[${r},${c}]">`;
|
||
|
||
function scaleGrid(m) {
|
||
const kongs = [
|
||
[1, "토목"],
|
||
[2, "산설(토)"],
|
||
[3, "조경"],
|
||
[4, "건축"],
|
||
[5, "산설(건)"],
|
||
[21, "국가유산"],
|
||
];
|
||
const head = `<tr><th>공사규모</th><th>공사기간</th>${kongs.map(([, l]) => `<th>간노 ${l}</th>`).join("")}${kongs.map(([, l]) => `<th>기타 ${l}</th>`).join("")}<th>일반관리비(일반)</th><th>일반관리비(전문)</th><th>이윤</th></tr>`;
|
||
const body = R.BANDS.flatMap((b) =>
|
||
R.KIKAN.map((k, p) => {
|
||
const row = b.row + p,
|
||
rr = (c) => (c === 21 ? Math.max(0, row - 20) : row);
|
||
const cells =
|
||
kongs.map(([c]) => `<td>${box(m, rr(c), c)}</td>`).join("") +
|
||
kongs.map(([c]) => `<td>${box(m, rr(c), c + 5)}</td>`).join("");
|
||
const tail =
|
||
p === 0
|
||
? `<td>${box(m, row, 11)}</td><td>${box(m, row, 15)}</td><td>${box(m, row, 16)}</td>`
|
||
: "<td></td><td></td><td></td>";
|
||
return `<tr>${p === 0 ? `<td rowspan="4">${esc(b.name)}</td>` : ""}<td>${k}</td>${cells}${tail}</tr>`;
|
||
}),
|
||
);
|
||
return `<table class="dbgrid"><thead>${head}</thead><tbody>${body.join("")}</tbody></table>`;
|
||
}
|
||
|
||
function annexGrid(m) {
|
||
const ins = [
|
||
["산재보험료", 45, 1],
|
||
["건강보험료", 46, 1],
|
||
["연금보험료", 46, 6],
|
||
["노인장기요양", 46, 11],
|
||
]
|
||
.map(([l, r, c]) => `<tr><td>${l}</td><td>${box(m, r, c)}</td></tr>`)
|
||
.join("");
|
||
const kobo = [1, 2, 3, 4, 5, 6, 7]
|
||
.map(
|
||
(g) =>
|
||
`<tr><td>${g < 7 ? `${g} 등급` : "7 등급이하"}</td><td>${box(m, 45, 10 + g)}</td></tr>`,
|
||
)
|
||
.join("");
|
||
const ag = [
|
||
["건축", 0],
|
||
["토목", 1],
|
||
["중건설", 3],
|
||
["특수건설", 4],
|
||
]
|
||
.map(([l, t]) => {
|
||
const o = R.ANJUN_OFF[t];
|
||
return `<tr><td>${l}</td><td>${box(m, 10 + o, 17)}</td><td>${box(m, 20 + o, 17)} + ${box(m, 20 + o, 18, 60)}천원</td><td>${box(m, 25 + o, 17)}</td><td>${box(m, R.HEALTH_ROW + o, 17)}</td></tr>`;
|
||
})
|
||
.join("");
|
||
const strip = (r, c0, nn, lab) =>
|
||
Array.from({ length: nn }, (_, i) => `<td>${lab(i)}<br>${box(m, r, c0 + i, 44)}</td>`).join(
|
||
"",
|
||
);
|
||
return `<div style="display:flex;gap:12px;flex-wrap:wrap">
|
||
<table class="dbgrid"><thead><tr><th>보험</th><th>요율</th></tr></thead><tbody>${ins}</tbody></table>
|
||
<table class="dbgrid"><thead><tr><th>고용보험 등급</th><th>요율</th></tr></thead><tbody>${kobo}</tbody></table>
|
||
<table class="dbgrid"><thead><tr><th>안전 공종</th><th>5억 미만</th><th>5~50억 미만 + 기초액</th><th>50억 이상</th><th>보건관리자 대상</th></tr></thead><tbody>${ag}</tbody></table></div>
|
||
<table class="dbgrid"><thead><tr><th colspan="14">환경보전비 (재료비+직접노무비+산출경비) × 율 — 공종 번호</th></tr></thead><tbody><tr>${strip(47, 1, 14, (i) => i + 1)}</tr></tbody></table>
|
||
<table class="dbgrid"><thead><tr><th colspan="10">건설기계대여금 지급보증 — 구분 번호(0 일반공사)</th></tr></thead><tbody><tr>${strip(49, 1, 10, (i) => i)}</tr></tbody></table>
|
||
<table class="dbgrid"><thead><tr><th colspan="4">퇴직공제부금비 직접노무비 × 율</th></tr></thead><tbody><tr>${strip(46, 16, 4, (i) => ["토목", "준설", "건축", "기타"][i])}</tr></tbody></table>`;
|
||
}
|
||
|
||
function rawGrid(m) {
|
||
const cols = m[0] ? m[0].length : 0;
|
||
const head = Array.from({ length: cols }, (_, c) => `<th>${c + 1}</th>`).join("");
|
||
const body = m.map(
|
||
(row, r) =>
|
||
`<tr><td>${r}</td>${row.map((_, c) => `<td>${box(m, r, c + 1, 46)}</td>`).join("")}</tr>`,
|
||
);
|
||
return `<table class="dbgrid"><thead><tr><th>행</th>${head}</tr></thead><tbody>${body.join("")}</tbody></table>`;
|
||
}
|
||
|
||
function lookupPanel(ctx, m) {
|
||
const S = global.STMATE_KANJUB_SCREEN;
|
||
if (!S || !m.length) return "";
|
||
const { n, cfg } = S.stageCfg(ctx);
|
||
const kj =
|
||
(ctx.computed.stages && ctx.computed.stages[n] && ctx.computed.stages[n].kanjub) ||
|
||
ctx.computed.kanjubComputed;
|
||
const res = R.lookup(m, R.optsFromCfg(cfg, kj));
|
||
const rows = Object.entries(res.rates)
|
||
.map(([c, v]) => {
|
||
const cur = cfg.rates[c];
|
||
return `<tr><td>${c}</td><td>${res.cells[c] ? `[${res.cells[c]}]` : "-"}</td><td style="text-align:right">${v}</td><td style="text-align:right">${cur ?? ""}</td><td>${cur != null && cur !== v ? "다름(수동값)" : ""}</td></tr>`;
|
||
})
|
||
.join("");
|
||
return `<div class="win-subbar"><b>${n}차수 조회</b> — 규모 ${esc(res.band.name)} · 안전 ${esc(res.anjunBand.name || "보건관리자 대상")} · 기초액 ${res.amounts.C_ANJUN_G}
|
||
<label><input type="checkbox" id="kr-tbugum"> 퇴직공제 포함</label><button class="win-btn primary" id="kr-apply">현재 차수에 적용</button></div>
|
||
<table class="dbgrid"><thead><tr><th>항목</th><th>칸</th><th>표 값</th><th>지금 요율</th><th></th></tr></thead><tbody>${rows}</tbody></table>
|
||
${res.notes.map((t) => `<div style="color:#a60">${esc(t)}</div>`).join("")}`;
|
||
}
|
||
|
||
function render(ctx) {
|
||
const { u, src, m } = current(ctx);
|
||
const jbr = (ctx.sourcesData && ctx.sourcesData.jbr_files) || [];
|
||
const tab = u.krTab || "scale";
|
||
const srcOpts = Object.keys(u.krMats)
|
||
.map((t) => `<option ${t === src ? "selected" : ""}>${esc(t)}</option>`)
|
||
.join("");
|
||
const tabs = [
|
||
["scale", "규모·기간(Rate_R/R1)"],
|
||
["annex", "부속표(보험·안전·환경·보증)"],
|
||
["raw", "원본 행렬"],
|
||
]
|
||
.map(
|
||
([t, l]) =>
|
||
`<button class="win-btn kr-tab2 ${t === tab ? "primary" : ""}" data-t="${t}">${l}</button>`,
|
||
)
|
||
.join("");
|
||
const body = !m.length
|
||
? "<div>표 없음 — 공사를 불러오거나 기준 JB_R 을 엶</div>"
|
||
: tab === "annex"
|
||
? annexGrid(m)
|
||
: tab === "raw"
|
||
? rawGrid(m)
|
||
: scaleGrid(m);
|
||
return `<div class="win-form"><div class="win-form-header"><span>원가계산 제비율표 [TWM_KAN_RATE · ${esc(src)} ${m.length}행×${m[0] ? m[0].length : 0}열]</span></div>
|
||
<div class="win-subbar"><label>표</label><select id="kr-src" class="win-select">${srcOpts}</select>
|
||
<select id="kr-jbr" class="win-select" style="width:200px">${jbr.map((f) => `<option value="${esc(f.path)}">${esc(f.name)}</option>`).join("")}</select>
|
||
<button class="win-btn" id="kr-load">기준 JB_R 열기</button><button class="win-btn" id="kr-back">제잡비 계산으로</button></div>
|
||
${lookupPanel(ctx, m)}<div class="win-subbar">${tabs}</div><div style="overflow:auto;max-height:520px">${body}</div></div>`;
|
||
}
|
||
|
||
function bind(ctx, api) {
|
||
const re = () => api.renderWorkspace();
|
||
const { u, src, m } = current(ctx);
|
||
api.onChange("kr-src", (e) => ((u.krSrc = e.target.value), re()));
|
||
api.bindEach(".kr-tab2", "onclick", (b) => ((u.krTab = b.getAttribute("data-t")), re()));
|
||
api.onClick("kr-back", () => ((u.activeScreen = "wM_KanJub"), re()));
|
||
api.onClick("kr-load", async () => {
|
||
const p = (api.$("kr-jbr") || {}).value;
|
||
if (!p) return;
|
||
const d = await api.postJson("/api/load-jbr", { jbr_path: p });
|
||
u.krMats[d.name] = R.matrixFromRows(d.rows);
|
||
u.krSrc = d.name;
|
||
re();
|
||
});
|
||
api.bindEach(".kr-cell", "onchange", (el) => {
|
||
const r = n0(el.getAttribute("data-r")),
|
||
c = n0(el.getAttribute("data-c"));
|
||
if (!m[r]) return;
|
||
m[r][c - 1] = n0(el.value);
|
||
// 공사 안 표면 원본 행(RATE_xx 칸)에도 적음
|
||
const rows = ctx.dbfData && ctx.dbfData.rate_tables && ctx.dbfData.rate_tables[src];
|
||
if (rows && rows[r]) {
|
||
const keys = Object.keys(rows[r])
|
||
.filter((k) => /^RATE_?\d+$/.test(k))
|
||
.sort((a, b) => n0(a.replace(/\D/g, "")) - n0(b.replace(/\D/g, "")));
|
||
if (keys[c - 1]) rows[r][keys[c - 1]] = String(n0(el.value));
|
||
}
|
||
re();
|
||
});
|
||
api.onClick("kr-apply", () => {
|
||
const S = global.STMATE_KANJUB_SCREEN,
|
||
{ n, cfg } = S.stageCfg(ctx);
|
||
api.recompute();
|
||
const kj = (ctx.computed.stages[n] || {}).kanjub || ctx.computed.kanjubComputed;
|
||
const res = R.lookup(m, R.optsFromCfg(cfg, kj));
|
||
R.applyToCfg(cfg, res, { withTbugum: !!(api.$("kr-tbugum") || {}).checked });
|
||
u.kjMsg = res.notes.join(" · ");
|
||
re();
|
||
});
|
||
}
|
||
|
||
P.screens.wM_KAN_RATE = { render, bind };
|
||
})(typeof window !== "undefined" ? window : globalThis);
|