Files
Aislo/tools/stmate-test/scr_ui_code.js
T
eomsangdonandClaude Opus 5.5 086750b6d4 feat(stmate-test): 2차 화면·코드관리 합침 · % 줄(조달수수료) 계산·저장 재계산 · 줄 고정 단가 보존 · 장부
- 코드 등록·삭제·변경·병합·데이타병합·일괄변경·검색·동일코드·풀어넣기 끝전 등(ui_code · ui_merge · api_ui_code)
- 코드 붙은 % 줄 = 같은 상위 공종 앞 말단 줄 비목 합 × 율(server pct_row_base · calc.js) · 저장 때 다시 셈(stc_save _pct_rows) — 실무 7건 저장값 그대로
- 내역 줄 단가 ≠ 단가목록이면 줄 고정 단가(fixedUnit)
- 메뉴 94개 눌러 오류 0 · 시험 502 통과

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 20:10:16 +09:00

296 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* STMATE w 8.0 — 코드 관리 창 끼워 넣기 (scr_ui_code.js) · 코드변경 · 항목교체 · 코드삭제 · +α · 신규등록
* 근거: DFM 원문 — uM_ChgCode(현재코드 · 바뀔코드 · * 사용 · 코드변환실행 · 코드일괄변환 · 현재이후 자동입력 · 바뀔코드 초기화)
* · uM_ReplCode(대상코드 · 항목교체실행 · 동일항목(명칭, 규격... 단가) 대상코드 자동입력) · uM_DelCode(선 택 · 선택코드
* 삭제실행 · 코드일괄삭제(중기비제외/포함) · 사용안된 "천원" "인" 일위대가 전체선택 · 동일항목 자동선택)
* · uM_Chg_Plus(종류별 현재최종 + α) · wM_Boxa Act_CopyNew(신규등록) · Act_DEL_CODE · Act_CH_CODE · Act_RP_CODE
* 계산 = ui_code.js · 저장 = app.js postSave(/api/ui/save · api_ui_code.py)
*/
(function (global) {
"use strict";
const P = (global.STMATE_PLUGINS = global.STMATE_PLUGINS || {
screens: {},
actions: {},
menus: [],
});
const UC = global.STMATE_UI_CODE;
const esc = (s) =>
String(s == null ? "" : s).replace(
/[&<>"]/g,
(c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c],
);
const $ = (id) => document.getElementById(id);
const KINDS = ["L", "M", "S", "B", "X", "D", "W"];
const env = (ctx) => ({
d: ctx.dbfData,
state: ctx.state,
memo: ctx.uiState.editedSmtMap || (ctx.uiState.editedSmtMap = {}),
});
const need = (ctx) => (ctx.dbfData ? true : (alert("열린 프로젝트가 없음"), false));
const done = (ctx, api, msg) => {
ctx.uiState.danMsg = msg;
api.closeModal();
api.cascadeAndRender();
};
const fail = (res) => alert((res.errors || [res.error || "실패"]).slice(0, 12).join("\n"));
const TITLE = {
change: "코드변경 [uM_ChgCode] — 현재코드 → 바뀔코드",
replace: "항목교체(내역포함) [uM_ReplCode] — 현재코드 → 대상코드",
delete: "코드삭제 [uM_DelCode]",
};
function st(ctx, mode) {
const u = ctx.uiState;
if (!u.codeMgr || u.codeMgr.mode !== mode)
u.codeMgr = { mode, kind: "L", map: {}, sel: {}, kw: "" };
return u.codeMgr;
}
function priceOf(r, k) {
const f = { L: "LAMT1", M: "MAMT1", S: "SAMT1" }[k] || "TAMT1";
return Number(r[f]) || 0;
}
function mgrHtml(ctx, api) {
const s = ctx.uiState.codeMgr,
e = env(ctx),
used = UC.usedSet(e);
const rows = (ctx.dbfData.costn_rows || []).filter((r) => {
const c = UC.codeOf(r);
if (c[0] !== s.kind) return false;
const kw = String(s.kw || "")
.trim()
.toLowerCase();
return (
!kw || `${c} ${r.NAME || ""} ${r.SIZE || ""} ${r.BQCODE || ""}`.toLowerCase().includes(kw)
);
});
const cnt = (k) => (ctx.dbfData.costn_rows || []).filter((r) => UC.codeOf(r)[0] === k).length;
const tabs = KINDS.map(
(k) =>
`<button class="win-btn cm-kind${s.kind === k ? " primary" : ""}" data-k="${k}">${UC.KIND_NAME[k]}(${cnt(k)})</button>`,
).join("");
const col = s.mode === "delete" ? "선 택" : s.mode === "replace" ? "대상코드" : "바뀔코드";
const body = rows
.map((r) => {
const c = UC.codeOf(r);
const cell =
s.mode === "delete"
? `<input type="checkbox" class="cm-sel" data-c="${c}" ${s.sel[c] ? "checked" : ""} />`
: `<input type="checkbox" class="cm-sel" data-c="${c}" ${s.sel[c] ? "checked" : ""} /> <input class="cell-input cm-map" data-c="${c}" value="${esc(s.map[c] || "")}" style="width:70px;" />`;
return `<tr><td class="center"><strong>${c}</strong></td><td class="center">${cell}</td><td>${esc(r.NAME)}</td><td>${esc(r.SIZE)}</td><td class="center">${esc(r.UNIT)}</td><td class="center">${esc(r.BQCODE || "")}</td><td class="center">${esc(r.EQCODE || "")}</td><td class="num">${api.fmt(priceOf(r, s.kind))}</td><td class="center">${used.has(c) ? "*" : ""}</td></tr>`;
})
.join("");
const btn = (id, t) => `<button class="win-btn" id="${id}">${t}</button>`;
const acts = {
change: [
btn("cm-run-sel", "코드변환실행(선택)"),
btn("cm-run-all", "코드일괄변환"),
btn("cm-auto", "현재이후 자동입력"),
btn("cm-clear", "바뀔코드 초기화"),
btn("cm-plus", "현재코드 + α"),
],
replace: [
btn("cm-run-all", "항목교체실행"),
btn("cm-same", "동일항목 대상코드 자동입력"),
btn("cm-clear", "대상코드 초기화"),
],
delete: [
btn("cm-del", "선택코드 삭제실행"),
btn("cm-del-x", "코드일괄삭제 (중기비제외)"),
btn("cm-del-t", "코드일괄삭제 (중기비포함)"),
btn("cm-1000", '사용안된 "천원" "인" 일위대가 선택'),
btn("cm-same", "동일항목 자동선택"),
btn("cm-clear", "선택코드 초기화"),
],
}[s.mode];
return `<div class="stm-form"><div class="stm-row">${tabs}</div>
<div class="stm-row"><input id="cm-kw" class="win-input" value="${esc(s.kw)}" placeholder="코드,명칭,분류번호" style="width:180px;" />${acts.join("")}</div>
<div class="grid-scroll" style="max-height:52vh;"><table class="dbgrid"><thead><tr><th>현재코드</th><th>${col}</th><th>명칭</th><th>규격</th><th>단위</th><th>분류번호</th><th>시설코드</th><th>단가</th><th>*</th></tr></thead><tbody>${body}</tbody></table></div>
<p class="stm-note">* = 내역서·구성표·산출근거 식에 쓰임 · 쓰이는 코드는 지우지 않음 · 바꾼 뒤 [계산 완료] 저장 때 작업본에 씀</p></div>`;
}
function openMgr(ctx, api, mode) {
if (!need(ctx)) return;
const s = st(ctx, mode);
api.openModal(TITLE[mode], mgrHtml(ctx, api));
const re = () => openMgr(ctx, api, mode);
document
.querySelectorAll(".cm-kind")
.forEach((b) => (b.onclick = () => ((s.kind = b.getAttribute("data-k")), re())));
$("cm-kw").onchange = (e) => ((s.kw = e.target.value), re());
document
.querySelectorAll(".cm-map")
.forEach(
(i) =>
(i.onchange = () => (s.map[i.getAttribute("data-c")] = i.value.trim().toUpperCase())),
);
document
.querySelectorAll(".cm-sel")
.forEach((i) => (i.onchange = () => (s.sel[i.getAttribute("data-c")] = i.checked)));
const on = (id, fn) => $(id) && ($(id).onclick = fn);
const pick = (onlySel) =>
Object.fromEntries(Object.entries(s.map).filter(([c, v]) => v && (!onlySel || s.sel[c])));
const run = (onlySel) => {
const m = pick(onlySel);
const res = mode === "replace" ? UC.replaceCodes(env(ctx), m) : UC.changeCodes(env(ctx), m);
if (!res.ok) return fail(res);
s.map = {};
s.sel = {};
done(
ctx,
api,
`${mode === "replace" ? "항목교체" : "코드변경"} ${res.count}건 · 내역 ${res.refs.boq} · 구성 ${res.refs.comp} · 식 ${res.refs.memo}곳`,
);
};
on("cm-run-sel", () => run(true));
on("cm-run-all", () => run(false));
on("cm-clear", () => {
const k = s.kind;
Object.keys(s.map).forEach((c) => c[0] === k && delete s.map[c]);
Object.keys(s.sel).forEach((c) => c[0] === k && delete s.sel[c]);
re();
});
on("cm-auto", () => {
const list = (ctx.dbfData.costn_rows || []).map(UC.codeOf).filter((c) => c[0] === s.kind);
const from = list.find((c) => s.sel[c]) || list[0];
const start = prompt(`${from} 부터 차례로 넣을 첫 바뀔코드`, from);
if (!start || !UC.CODE_RE.test(start.trim().toUpperCase())) return;
let n = UC.codeNum(start.trim().toUpperCase());
list.slice(list.indexOf(from)).forEach((c) => (s.map[c] = UC.mkCode(s.kind, n++)));
re();
});
on("cm-plus", () => openPlus(ctx, api));
on("cm-same", () => {
const m = UC.autoReplaceMap(ctx.dbfData.costn_rows, s.kind);
if (mode === "delete") Object.keys(m).forEach((c) => (s.sel[c] = true));
else Object.assign(s.map, m);
if (!Object.keys(m).length) alert("동일항목 없음");
re();
});
on(
"cm-1000",
() => (UC.unusedBUnit(env(ctx)).forEach((c) => (s.sel[c] = true)), (s.kind = "B"), re()),
);
const del = (codes, what) => {
if (!codes.length) return alert(`${what} 없음`);
if (
!confirm(
`${what} ${codes.length}건을 지움\n${codes.slice(0, 20).join(", ")}${codes.length > 20 ? " …" : ""}`,
)
)
return;
const res = UC.deleteCodes(env(ctx), codes);
if (!res.ok) return fail(res);
s.sel = {};
done(
ctx,
api,
`코드삭제 ${res.count}건${res.kept.length ? ` · 쓰이는 코드 ${res.kept.length}건은 남김` : ""}`,
);
};
on("cm-del", () =>
del(
Object.keys(s.sel).filter((c) => s.sel[c]),
"선택코드",
),
);
on("cm-del-x", () => del(UC.unusedCodes(env(ctx), false), "안 쓰이는 코드(중기비제외)"));
on("cm-del-t", () => del(UC.unusedCodes(env(ctx), true), "안 쓰이는 코드(중기비포함)"));
}
// 코드번호 변경(현재코드 + α)
function openPlus(ctx, api) {
if (!need(ctx)) return;
const last = UC.lastCodes(ctx.dbfData.costn_rows);
const kinds = ["L", "M", "S", "B", "X", "D", "W"];
api.openModal(
"코드번호 변경 ( 현재코드 + α ) [uM_Chg_Plus]",
`<div class="stm-form">${kinds
.map(
(k) =>
`<div class="stm-row"><label style="width:260px;">${UC.KIND_NAME[k]} ( 현재최종 : ${last[k] || "-"} ) +</label><input id="cp-${k}" type="number" class="win-input" value="0" style="width:90px;" /></div>`,
)
.join(
"",
)}<div class="stm-row"><button class="win-btn primary" id="cp-ok">코드번호 변경(O)</button><button class="win-btn" id="cp-no">취 소(C)</button></div></div>`,
);
$("cp-no").onclick = api.closeModal;
$("cp-ok").onclick = () => {
const alpha = Object.fromEntries(kinds.map((k) => [k, Number($(`cp-${k}`).value) || 0]));
const res = UC.changeCodes(env(ctx), UC.plusMap(ctx.dbfData.costn_rows, alpha));
if (!res.ok) return fail(res);
done(ctx, api, `코드번호 + α ${res.count}건`);
};
}
// ── 단가목록표 한 코드 동작(신규등록 · 현재코드 삭제 · 코드번호 변경 · 항목교체) ──
function copyNew(ctx, api, code) {
if (!need(ctx)) return;
const kind = code
? code[0]
: (prompt("새 코드 종류 글자(L M S B X D W)", "M") || "").trim().toUpperCase();
if (!kind) return;
const def = UC.nextCode(ctx.dbfData.costn_rows, kind);
const nc = (prompt(code ? `${code} 를 복사한 새 코드` : "새 코드", def) || "")
.trim()
.toUpperCase();
if (!nc) return;
const fields = code
? {}
: {
NAME: prompt("명칭", "") || "",
SIZE: prompt("규격", "") || "",
UNIT: prompt("단위", "") || "",
};
const res = UC.addCode(env(ctx), { from: code || null, kind, code: nc, fields });
if (!res.ok) return fail(res);
const s = ctx.uiState.dan || (ctx.uiState.dan = {});
Object.assign(s, { code: res.code, kind: "ALL", kw: res.code });
ctx.uiState.danMsg = `${res.code} 신규등록${code ? ` (${code} 복사)` : ""}`;
api.cascadeAndRender();
}
function delOne(ctx, api, code) {
if (!code || !confirm(`${code} 삭제?`)) return;
const res = UC.deleteCodes(env(ctx), [code]);
if (!res.ok) return fail(res);
ctx.uiState.dan.code = "";
ctx.uiState.danMsg = `${code} 삭제`;
api.cascadeAndRender();
}
function chgOne(ctx, api, code, replace) {
if (!code) return;
const to = (
prompt(replace ? `${code} 를 쓰는 자리를 바꿀 대상코드` : `${code} → 바뀔코드`, "") || ""
)
.trim()
.toUpperCase();
if (!to) return;
const res = replace
? UC.replaceCodes(env(ctx), { [code]: to })
: UC.changeCodes(env(ctx), { [code]: to });
if (!res.ok) return fail(res);
ctx.uiState.dan.code = replace ? code : to;
ctx.uiState.danMsg = `${code} → ${to} ${replace ? "항목교체" : "코드변경"} · 내역 ${res.refs.boq} · 구성 ${res.refs.comp} · 식 ${res.refs.memo}곳`;
api.cascadeAndRender();
}
Object.assign(P.actions, {
Act_Chg_Code_Sel: (ctx, api) => openMgr(ctx, api, "change"),
Act_Chg_Code_All: (ctx, api) => openMgr(ctx, api, "change"),
Act_Chg_Plus: (ctx, api) => openPlus(ctx, api),
Act_Del_Code_Sel: (ctx, api) => openMgr(ctx, api, "delete"),
Act_Del_Code_All_Minus: (ctx, api) => (
openMgr(ctx, api, "delete"),
$("cm-del-x") && $("cm-del-x").click()
),
Act_Del_Code_All_Plus: (ctx, api) => (
openMgr(ctx, api, "delete"),
$("cm-del-t") && $("cm-del-t").click()
),
Act_Chg_Item: (ctx, api) => openMgr(ctx, api, "replace"),
});
global.STMATE_UI_CODE_SCR = { openMgr, openPlus, copyNew, delOne, chgOne, env };
})(window);