- 원본 대조 = 설치본 실행파일 52개 화면 이름·버튼 글·한글 문자열 - 지움: conv_jb.py · excel_builder.py · view_dan_ilwi.js · view_kanjub_prt.js · /api/conv/jb-* · /api/report/csv · /api/stage/projects · /api/catalog · /api/export-xlsx - 낡은 시험 삭제(지운 기능 전용): conv_change 제비율표 3 · stage_api 공사별 설정 2 · report_sheets CSV 2줄 - 장부 F4-15 · F1-33 · F6-24 설명 고침 · stmate 시험 786 통과 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
228 lines
9.4 KiB
JavaScript
228 lines
9.4 KiB
JavaScript
/**
|
|
* STMATE w 8.0 — 메인 컨트롤러 및 DBF/SMT/STC/JB_R 양방향 브리지 (app.js)
|
|
* TWM_STMATE_START_8X 5대 메뉴 · 9개 메인 화면 · 계산완료(Act_Calc_All) · 모달 제어
|
|
*/
|
|
(function (global) {
|
|
"use strict";
|
|
|
|
const Calc = global.STMATE_CALC;
|
|
const ViewPrjBoq = global.STMATE_VIEW_PRJ_BOQ;
|
|
const { fmt, num } = Calc;
|
|
const $ = (id) => document.getElementById(id),
|
|
closeModal = () => $("stmate-modal-backdrop").classList.add("hidden"),
|
|
onClick = (id, fn) => $(id) && ($(id).onclick = fn),
|
|
onChange = (id, fn) => $(id) && ($(id).onchange = fn),
|
|
bindEach = (sel, ev, fn) =>
|
|
document.querySelectorAll(sel).forEach((el) => (el[ev] = (e) => fn(el, e)));
|
|
|
|
const ctx = {
|
|
state: Calc.createDefaultState(),
|
|
computed: null,
|
|
sourcesData: { projects: [], jbr_files: [], work_root: "" },
|
|
currentSourcePath: "",
|
|
dbfData: null,
|
|
uiState: {
|
|
activeScreen: "wPrjList",
|
|
showBoqPreview: true,
|
|
boqLevelFilter: "ALL",
|
|
boqSearch: "",
|
|
activePocket: 1,
|
|
boqPockets: { 1: [], 2: [], 3: [] },
|
|
danFilter: "ALL",
|
|
danSearch: "",
|
|
ilwiSubFilter: "ALL_BDX",
|
|
selectedIlwiCode: "",
|
|
editedSmtMap: {},
|
|
},
|
|
};
|
|
|
|
const recompute = () => (ctx.computed = Calc.computeAll(ctx.state));
|
|
|
|
// 끼워 넣기 — scr_*.js 가 STMATE_PLUGINS 에 화면·메뉴 동작을 등록(plugins_manifest.js 가 불러옴)
|
|
const PLUG = (global.STMATE_PLUGINS = global.STMATE_PLUGINS || {
|
|
screens: {},
|
|
actions: {},
|
|
menus: [],
|
|
});
|
|
const API = (global.STMATE_APP = {
|
|
ctx,
|
|
Calc,
|
|
fmt,
|
|
num,
|
|
$,
|
|
onClick,
|
|
onChange,
|
|
bindEach,
|
|
closeModal,
|
|
});
|
|
|
|
function renderChromeBars() {
|
|
const prjBar = $("prj-context-bar"),
|
|
stBar = $("stmate-statusbar");
|
|
const kj = ctx.computed.kanjubComputed,
|
|
bt = ctx.computed.boqComputed.totals;
|
|
const dbfInfo = ctx.dbfData
|
|
? `COSTN ${ctx.dbfData.costn_count}건 · SMT산식 ${Object.keys(ctx.dbfData.costn_memos || {}).length}건 · ${ctx.dbfData.boq_table_name} ${ctx.dbfData.boq_count}행`
|
|
: "새 공사 (불러온 파일 없음)";
|
|
if (prjBar) {
|
|
prjBar.innerHTML = `<div class="prj-context-left"><span class="prj-tag">열린 공사</span><strong>${ctx.state.projectMeta.pname}</strong><span>(${ctx.state.projectMeta.pnum || "-"} · 코드 BOQ${ctx.state.projectMeta.pcode || "11"})</span></div><div class="prj-context-right"><span>원본 DBF/SMT: <strong>${dbfInfo}</strong></span><span>원가양식: <strong>${ctx.state.kanjubConfig.scheme}형식</strong></span></div>`;
|
|
}
|
|
if (stBar) {
|
|
stBar.innerHTML = `<div class="st-metrics"><span class="st-kv">순공사원가 재료비:<strong>${fmt(bt.directM)}</strong></span><span class="st-kv">노무비:<strong>${fmt(bt.directL)}</strong></span><span class="st-kv">경비:<strong>${fmt(bt.directS)}</strong></span><span class="st-kv">직접공사비 계:<strong>${fmt(bt.directT)}원</strong></span><span class="st-kv">간접공사비+일반+이윤:<strong>${fmt(kj.TOTAL_COST - kj.DIRECT_COST)}원</strong></span><span class="st-kv">도급액(공급가+부가세):<strong>${fmt(kj.FINAL_SUPPLY)}원</strong></span><span class="st-kv">관급자재대:<strong>${fmt(kj.GOV_MAT)}원</strong></span><span class="st-kv total">총공사비(COSTW1):<strong>${fmt(kj.GRAND_TOTAL)}원</strong></span></div><div>STMATE w 8.0 Full Engine</div>`;
|
|
}
|
|
document.querySelectorAll("#main-toolbar .tb-btn[data-screen]").forEach((btn) => {
|
|
btn.classList.toggle("active", btn.getAttribute("data-screen") === ctx.uiState.activeScreen);
|
|
});
|
|
}
|
|
|
|
function renderWorkspace() {
|
|
recompute();
|
|
renderChromeBars();
|
|
const ws = $("mdi-workspace");
|
|
if (!ws) return;
|
|
const scr = PLUG.screens[ctx.uiState.activeScreen] || PLUG.screens.wPrjList;
|
|
ws.innerHTML = scr ? scr.render(ctx, API) : "";
|
|
if (scr && scr.bind) scr.bind(ctx, API);
|
|
}
|
|
|
|
async function postJson(url, body) {
|
|
const res = await fetch(url, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(body),
|
|
});
|
|
if (!res.ok) throw new Error(await res.text());
|
|
return res.json();
|
|
}
|
|
|
|
async function loadProjectFromServer(sourcePath, jumpScreen) {
|
|
const badge = $("conn-badge");
|
|
try {
|
|
if (badge) badge.textContent = "DBF/SMT 로딩중...";
|
|
const data = await postJson("/api/load", { source_path: sourcePath });
|
|
ctx.currentSourcePath = sourcePath;
|
|
ctx.dbfData = data;
|
|
ctx.state = data.sim_state;
|
|
ctx.uiState.editedSmtMap = {};
|
|
if (jumpScreen) ctx.uiState.activeScreen = jumpScreen;
|
|
const sel = $("top-project-select");
|
|
if (sel) sel.value = sourcePath;
|
|
if (badge) badge.textContent = `DBF/SMT 직결 (${data.costn_count}단가·${data.boq_count}내역)`;
|
|
Object.assign(ctx.uiState, { boqSel: [], sysDraft: null });
|
|
if (API.noteRecent) API.noteRecent(sourcePath);
|
|
renderWorkspace();
|
|
} catch (err) {
|
|
if (badge) badge.textContent = "로드 오류";
|
|
alert("프로젝트 DBF 로드 실패: " + err.message);
|
|
}
|
|
}
|
|
|
|
function cascadeAndRender() {
|
|
if (global.STMATE_ENGINE && global.STMATE_ENGINE.cascadeRecalculateAll)
|
|
global.STMATE_ENGINE.cascadeRecalculateAll(ctx);
|
|
renderWorkspace();
|
|
}
|
|
|
|
async function saveAllToDbfServer() {
|
|
if (!ctx.currentSourcePath || !ctx.dbfData)
|
|
return alert("먼저 PC 또는 실무 STMATE 프로젝트를 로드하세요.");
|
|
const stat =
|
|
global.STMATE_ENGINE && global.STMATE_ENGINE.cascadeRecalculateAll
|
|
? global.STMATE_ENGINE.cascadeRecalculateAll(ctx)
|
|
: { xCount: 0, dCount: 0, bCount: 0 };
|
|
recompute();
|
|
renderChromeBars();
|
|
const kj = ctx.computed.kanjubComputed,
|
|
bt = ctx.computed.boqComputed.totals;
|
|
try {
|
|
const saved = await postSave(decorateSavePayload(ViewPrjBoq.buildSavePayload(ctx)));
|
|
renderWorkspace();
|
|
alert(
|
|
`[계산완료(Act_Calc_All) 및 DBF/SMT/STC 저장 성공]\n• 5단 연쇄재계산: 중기(X) ${stat.xCount}건 · 산근(D) ${stat.dCount}건 · 일위대가(B) ${stat.bCount}건\n• 폴더: ${saved.saved_dir}\n• 아카이브: ${saved.saved_stc} (${fmt(saved.stc_size)}B)\n• 직공비: ${fmt(bt.directT)}원 / 총공사비: ${fmt(kj.GRAND_TOTAL)}원`,
|
|
);
|
|
} catch (err) {
|
|
alert("DBF/STC 저장 실패: " + err.message);
|
|
}
|
|
}
|
|
|
|
// 저장 길 — 코드 관리(지움·코드 바꿈·다른 표 코드)가 있으면 /api/ui/save(구조 덧붙임 · api_ui_code.py) · 아니면 /api/save
|
|
function postSave(p) {
|
|
const UC = global.STMATE_UI_CODE;
|
|
if (UC && UC.needsUiSave(ctx.dbfData))
|
|
return postJson(
|
|
"/api/ui/save",
|
|
Object.assign(p, UC.structPayload(ctx.dbfData), {
|
|
rate_tables: ctx.dbfData.rate_dirty ? ctx.dbfData.rate_tables : {},
|
|
}),
|
|
);
|
|
return postJson("/api/save", p);
|
|
}
|
|
|
|
// 저장 묶음 덧붙임 — 초기값(start_ini) · 공사목록 전체 행(prjlst_rows) · 서버 반영은 파일 담당
|
|
function decorateSavePayload(p) {
|
|
const d = ctx.dbfData;
|
|
if (!d) return p;
|
|
// 산근 편집기 계약단가 표시(BCONT) — 고친 코드만 COSTN 칸 덧붙임
|
|
const extra = ctx.uiState.costnExtra || {};
|
|
(p.costn_updates || []).forEach((u) => Object.assign(u, extra[String(u.CODE).trim()] || {}));
|
|
return Object.assign(p, { start_ini: d.start_ini || {}, prjlst_rows: d.prjlst_all || [] });
|
|
}
|
|
|
|
function openModal(title, bodyHtml) {
|
|
$("modal-title-text").textContent = title;
|
|
$("modal-body-content").innerHTML = bodyHtml;
|
|
$("stmate-modal-backdrop").classList.remove("hidden");
|
|
}
|
|
|
|
// 내역 행 끼워 넣기 — app_modals.js
|
|
const insertBoqRow = (...args) => global.STMATE_APP_MODALS.insertBoqRow(API, ...args);
|
|
|
|
function downloadBlob(content, filename, mime) {
|
|
const a = document.createElement("a");
|
|
a.href = URL.createObjectURL(new Blob([content], { type: mime }));
|
|
a.download = filename;
|
|
a.click();
|
|
URL.revokeObjectURL(a.href);
|
|
}
|
|
|
|
Object.assign(API, { renderWorkspace, recompute, openModal, postJson, downloadBlob });
|
|
Object.assign(API, { cascadeAndRender, loadProjectFromServer, insertBoqRow });
|
|
Object.assign(API, {
|
|
saveAll: saveAllToDbfServer,
|
|
decorateSavePayload,
|
|
postSave,
|
|
});
|
|
|
|
async function initStmateApp() {
|
|
onClick("modal-close-btn", closeModal);
|
|
// 원본 메뉴·툴바·단축키 — ui_app_menu.js(끼워 넣은 메뉴·동작 포함)
|
|
global.STMATE_UI_APP_MENU.install(API);
|
|
|
|
try {
|
|
const srcRes = await fetch("/api/sources");
|
|
if (srcRes.ok) ctx.sourcesData = await srcRes.json();
|
|
const sel = $("top-project-select");
|
|
if (sel && ctx.sourcesData.projects.length) {
|
|
sel.innerHTML = ctx.sourcesData.projects
|
|
.map((p) => `<option value="${p.path}">${p.name}</option>`)
|
|
.join("");
|
|
sel.onchange = () => loadProjectFromServer(sel.value);
|
|
const defaultPrj =
|
|
ctx.sourcesData.projects.find(
|
|
(p) => p.name.includes("[실무 STC]") && p.name.includes("기번41"),
|
|
) ||
|
|
ctx.sourcesData.projects.find((p) => p.name.includes("기번41")) ||
|
|
ctx.sourcesData.projects[0];
|
|
if (defaultPrj) return await loadProjectFromServer(defaultPrj.path, "wPrjList");
|
|
}
|
|
} catch (_e) {
|
|
// 오프라인 기본 폴백
|
|
}
|
|
renderWorkspace();
|
|
}
|
|
|
|
document.readyState === "loading"
|
|
? document.addEventListener("DOMContentLoaded", initStmateApp)
|
|
: initStmateApp();
|
|
})(window);
|