- 요율 조회(scr_kanjub_rate.lookup) — 적용기준 1 이면 rateMLookup(ST_KanJub 0x47a1bc) 값으로 KANNO·ILBAN·IYUN · 그 밖 RATE_R 그대로 - 적용기준 콤보를 바꾸면 제비율표 다시 조회(요율 축에 I_SET_BASE) - 브라우저 확인(봉화2024): 행자부 → 간접노무비 14.5 → 14.6(RATE_M 11줄 RATE1) · 지방계약법 되돌림 14.5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
495 lines
23 KiB
JavaScript
495 lines
23 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,…]
|
||
* 원본 조회(ST_KanJub 0x47a1bc · 0x47fa24 · 지식DB REA 34)
|
||
* 건설기계 = 50번째 줄(Go 0x32) · 구분 1~5 → RATE_06~10 · 구분 0 → 공사종류(0 RATE_01 · 1·4 RATE_04 · 2 RATE_02 · 3 RATE_03 · 그 밖 0)
|
||
* · 구분 −1 · 공사종류 5(국가유산)·8·9 → 0(0x47fd74~0x47fecb)
|
||
* 퇴직공제 = 47번째 줄 RATE_(16+구분) · 구분 0~2 만 · 3(기타)은 손대지 않음(0x47ae79)
|
||
* 공사기간 「30일 미만」(I_KONG_KIKAN −1 = 화면 0번 − 1 · 0x470eee) → 건강·장기요양·연금 율 0(0x47b376)
|
||
* 안전관리비 — 열 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];
|
||
}
|
||
|
||
// 원본 차수·조회 규칙 묶음(kanjub_parts.js) — 브라우저 = 전역 · node = require
|
||
function kanjubNative() {
|
||
const g = typeof globalThis !== "undefined" ? globalThis : {};
|
||
if (g.STMATE_KANJUB_PARTS) return g.STMATE_KANJUB_PARTS.NATIVE;
|
||
try {
|
||
return require("./kanjub_parts.js").NATIVE;
|
||
} catch {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
// 요율 조회 — 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) => {
|
||
const x = v === undefined ? cell(m, r, c) : v;
|
||
return ((rates[code] = x), (cells[code] = [r, c]), x);
|
||
};
|
||
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);
|
||
// 공사기간 30일 미만(−1) → 건강·연금·장기요양 0 (ST_KanJub 0x47b376)
|
||
const short = n0(opts.kikan) < 0;
|
||
put("KUNBO", 46, 1, short ? 0 : undefined);
|
||
put("YUNBO", 46, 6, short ? 0 : undefined);
|
||
put("SLVBO", 46, 11, short ? 0 : undefined);
|
||
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;
|
||
// 건설기계대여금 지급보증 — ST_KanJub 0x47fa24(분기표 0x47fdad · 공사종류 분기표 0x47fe5f)
|
||
const eq = opts.eqip == null ? 0 : n0(opts.eqip),
|
||
kt = n0(opts.kongType);
|
||
const eqCol =
|
||
eq < 0 || kt === 5 || kt === 8 || kt === 9
|
||
? 0
|
||
: eq >= 1 && eq <= 5
|
||
? eq + 5
|
||
: ({ 0: 1, 1: 4, 2: 2, 3: 3, 4: 4 }[kt] ?? 0);
|
||
if (eqCol) put("EQIP", 49, eqCol);
|
||
else rates.EQIP = 0;
|
||
// 퇴직공제부금비 — 46행 16~18열(토목·준설·건축) · 3(기타)은 조회하지 않음(0x47ae79) · 사용자 선택일 때만 넣음
|
||
const tb = n0(opts.tbugum);
|
||
if (tb >= 0 && tb < 3) put("TBUGUM", 46, 16 + tb);
|
||
for (const [c, v] of Object.entries(rates))
|
||
if (
|
||
!v &&
|
||
cells[c] &&
|
||
!["KOBO", "HWAN", "EQIP", "TBUGUM"].includes(c) &&
|
||
!(short && ["KUNBO", "YUNBO", "SLVBO"].includes(c))
|
||
)
|
||
notes.push(`${c} — 표 칸 [${cells[c]}] 이 비어 있음(직접 입력 요율 유지)`);
|
||
const zeroed = short ? ["KUNBO", "YUNBO", "SLVBO"] : [];
|
||
// 적용기준 행자부(I_SET_BASE 1) — 간접노무비·일반관리비·이윤만 RATE_M 에서(ST_KanJub 0x47a1bc · REA 36 §4.2)
|
||
const NATIVE = kanjubNative();
|
||
if (n0(opts.setBase) === 1 && NATIVE && (opts.rateMRows || []).length) {
|
||
const rm = NATIVE.rateMLookup(opts.rateMRows, {
|
||
scale: opts.scale,
|
||
kongType: opts.kongType,
|
||
ilban: opts.ilbanType,
|
||
kikan: opts.kikan,
|
||
});
|
||
for (const [c, v] of Object.entries(rm.rates)) {
|
||
rates[c] = v;
|
||
cells[c] = ["RATE_M", ...rm.cells[c]];
|
||
}
|
||
}
|
||
return { rates, cells, amounts, band, anjunBand: ab, notes, zeroed };
|
||
}
|
||
|
||
// 설정 → 조회 입력(직접공사비 · 안전관리비 대상액은 계산 결과에서)
|
||
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),
|
||
setBase: n0(k.I_SET_BASE),
|
||
};
|
||
}
|
||
|
||
// 조회 결과 → 설정(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) && !(res.zeroed || []).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 계약 ①~③)
|
||
// 수동 확정(cfg.rateConfirm.manual)이 있으면 다시 고르지 않음 · 왕복이면 오간 조회 결과(choices)를 돌려줌
|
||
function applyWithLoopCheck(
|
||
cfg,
|
||
m,
|
||
calcFn,
|
||
{ scaleOf = (kj) => kj.dT, maxIter = 6, withTbugum = false, extra = {} } = {},
|
||
) {
|
||
let kj = calcFn(cfg);
|
||
if (cfg.rateConfirm && cfg.rateConfirm.manual) return { cfg, kj, loop: false, manual: true };
|
||
const seen = [],
|
||
found = {};
|
||
for (let i = 0; i < maxIter; i++) {
|
||
const res = lookup(m, Object.assign(optsFromCfg(cfg, kj), extra, { scale: scaleOf(kj) }));
|
||
const sig = `${res.band.row}/${res.anjunBand.row}`;
|
||
if (seen.includes(sig) && seen[seen.length - 1] !== sig) {
|
||
const choices = seen.slice(seen.indexOf(sig)).map((g) => found[g]);
|
||
const msg =
|
||
"공사규모에 따른 제비율 변경으로 공사규모가 계속 변경되는 무한루프 — 일반관리비·이윤 등 요율을 직접 수정";
|
||
return { cfg, kj, loop: true, msg, choices };
|
||
}
|
||
if (seen[seen.length - 1] === sig) return { cfg, kj, loop: false };
|
||
seen.push(sig);
|
||
found[sig] = res;
|
||
applyToCfg(cfg, res, { withTbugum });
|
||
kj = calcFn(cfg);
|
||
}
|
||
return {
|
||
cfg,
|
||
kj,
|
||
loop: true,
|
||
msg: "요율 확정이 수렴하지 않음 — 직접 확정",
|
||
choices: Object.values(found),
|
||
};
|
||
}
|
||
|
||
// 수동 확정(06 §2 계약 ③④) — 고른 조회 결과의 요율을 넣고 확정 상태·표 좌표·사용자 확정 여부를 설정에 남김
|
||
// 확정을 풀면(clearManual) 다음 적용 때 다시 자동으로 고름
|
||
function confirmManual(cfg, res, opts = {}) {
|
||
applyToCfg(cfg, res, opts);
|
||
cfg.rateConfirm = {
|
||
manual: true,
|
||
band: res.band.row,
|
||
bandName: res.band.name,
|
||
anjunBand: res.anjunBand.row,
|
||
cells: Object.assign({}, res.cells),
|
||
};
|
||
return cfg;
|
||
}
|
||
const clearManual = (cfg) => (delete cfg.rateConfirm, cfg);
|
||
|
||
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, confirmManual, clearManual });
|
||
})();
|
||
|
||
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>`;
|
||
}
|
||
|
||
// 조회 입력 + 행자부기준용 RATE_M 행(그 STC 의 표)
|
||
const optsOf = (ctx, cfg, kj) =>
|
||
Object.assign(R.optsFromCfg(cfg, kj), {
|
||
rateMRows: ((ctx.dbfData && ctx.dbfData.rate_tables) || {})["RATE_M.DBF"] || [],
|
||
});
|
||
|
||
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, optsOf(ctx, 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>
|
||
<button class="win-btn" id="kr-undo" ${u.krOrig && u.krOrig[src] ? "" : "disabled"}>수정 취소</button>
|
||
<button class="win-btn" id="kr-print">출력</button>
|
||
<span class="stm-note">${ctx.dbfData && ctx.dbfData.rate_dirty ? "고친 칸은 [계산완료·저장] 때 공사 STC 의 이 표에 씀" : ""}</span></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;
|
||
// 처음 고칠 때 원래 표를 남김(수정 취소)
|
||
const rows0 = ctx.dbfData && ctx.dbfData.rate_tables && ctx.dbfData.rate_tables[src];
|
||
u.krOrig = u.krOrig || {};
|
||
if (!u.krOrig[src])
|
||
u.krOrig[src] = {
|
||
m: JSON.parse(JSON.stringify(m)),
|
||
rows: rows0 && JSON.parse(JSON.stringify(rows0)),
|
||
};
|
||
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));
|
||
ctx.dbfData.rate_dirty = true; // 저장 길 = /api/ui/save rate_tables(api_ui_code._rates)
|
||
}
|
||
re();
|
||
});
|
||
api.onClick("kr-undo", () => {
|
||
const o = u.krOrig && u.krOrig[src];
|
||
if (!o) return;
|
||
u.krMats[src] = o.m;
|
||
if (o.rows && ctx.dbfData && ctx.dbfData.rate_tables) ctx.dbfData.rate_tables[src] = o.rows;
|
||
delete u.krOrig[src];
|
||
if (ctx.dbfData) ctx.dbfData.rate_dirty = Object.keys(u.krOrig).length > 0;
|
||
re();
|
||
});
|
||
api.onClick("kr-print", () => {
|
||
const w = global.open("", "_blank");
|
||
if (!w) return;
|
||
const t = m
|
||
.map((row, i) => `<tr><th>${i}</th>${row.map((v) => `<td>${v}</td>`).join("")}</tr>`)
|
||
.join("");
|
||
w.document.write(
|
||
`<html><head><meta charset="utf-8"><title>원가계산 제비율표 ${esc(src)}</title><style>td,th{border:1px solid #999;padding:2px 4px;font:11px monospace;text-align:right}table{border-collapse:collapse}</style></head><body><h3>원가계산 제비율표 — ${esc(src)}</h3><table>${t}</table></body></html>`,
|
||
);
|
||
w.document.close();
|
||
w.print();
|
||
});
|
||
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, optsOf(ctx, 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);
|