- 보고서: 원본 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>
223 lines
8.5 KiB
JavaScript
223 lines
8.5 KiB
JavaScript
/**
|
|
* STMATE w 8.0 — 메인 메뉴·툴바·단축키·자동백업·최근 작업폴더·화면 크기 동작 (ui_app_menu.js)
|
|
* 메뉴 자료 = ui_menu.js(원본 M_Menu1 체계) · 동작 우선순위 = 끼워 넣기(STMATE_PLUGINS.actions) → 여기 H 표
|
|
* 설정 저장 = 브라우저 localStorage("stmate_ui_prefs") — 원본 Start8x.Ini 의 창 위치·보기 설정 자리
|
|
*/
|
|
(function (global) {
|
|
"use strict";
|
|
|
|
const M = global.STMATE_UI_MENU;
|
|
const PREF_KEY = "stmate_ui_prefs";
|
|
const DEF = { autoBackDaily: false, autoBack30: false, prnPdf: false, useName2: false };
|
|
Object.assign(DEF, { dflIndex: "C", dflWidth: 40, screenMode: 0, scale: 100, recent: [] });
|
|
Object.assign(DEF, { lastBackDay: "", lastBack30: 0 });
|
|
|
|
function loadPrefs() {
|
|
try {
|
|
return Object.assign({}, DEF, JSON.parse(global.localStorage.getItem(PREF_KEY) || "{}"));
|
|
} catch (_e) {
|
|
return Object.assign({}, DEF);
|
|
}
|
|
}
|
|
const prefs = loadPrefs();
|
|
const savePrefs = () => {
|
|
try {
|
|
global.localStorage.setItem(PREF_KEY, JSON.stringify(prefs));
|
|
} catch (_e) {
|
|
// 저장 공간 없음 — 이번 실행 동안만 유지
|
|
}
|
|
};
|
|
|
|
const pad = (n) => String(n).padStart(2, "0");
|
|
const today = () => {
|
|
const d = new Date();
|
|
return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}`;
|
|
};
|
|
const nowTag = () => {
|
|
const d = new Date();
|
|
return `${today()}_${pad(d.getHours())}${pad(d.getMinutes())}`;
|
|
};
|
|
|
|
let API = null;
|
|
const PLUG = () => global.STMATE_PLUGINS || { screens: {}, actions: {}, menus: [] };
|
|
|
|
function go(screen) {
|
|
API.ctx.uiState.activeScreen = screen;
|
|
API.renderWorkspace();
|
|
}
|
|
|
|
// 백업 — 계산 결과 저장 묶음을 「프로젝트_백업_날짜」 폴더·STC 로(서버 work_dbf)
|
|
async function backup(kind) {
|
|
const { ctx } = API;
|
|
if (!ctx.dbfData || !ctx.currentSourcePath) return alert("열린 프로젝트가 없음");
|
|
API.recompute();
|
|
const payload = global.STMATE_VIEW_PRJ_BOQ.buildSavePayload(ctx);
|
|
API.decorateSavePayload(payload);
|
|
const base = String(payload.target_folder_name || "stmate").slice(0, 20);
|
|
payload.target_folder_name = `${base}_백업_${kind === "day" ? today() : nowTag()}`;
|
|
try {
|
|
const res = await API.postJson("/api/save", payload);
|
|
if (kind === "day") prefs.lastBackDay = today();
|
|
if (kind === "30") prefs.lastBack30 = Date.now();
|
|
savePrefs();
|
|
return res;
|
|
} catch (err) {
|
|
alert("자동백업 실패: " + err.message);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// 재계산 뒤 30분 백업
|
|
async function afterCalc() {
|
|
if (prefs.autoBack30 && Date.now() - (prefs.lastBack30 || 0) >= 30 * 60 * 1000)
|
|
await backup("30");
|
|
}
|
|
|
|
function applyView() {
|
|
const shell = document.getElementById("stmate-shell");
|
|
if (shell) shell.setAttribute("data-screen-mode", String(prefs.screenMode));
|
|
const ws = document.getElementById("mdi-workspace");
|
|
if (ws) ws.style.zoom = prefs.scale === 100 ? "" : `${prefs.scale}%`;
|
|
document.documentElement.style.setProperty("--stm-name-w", `${prefs.dflWidth * 7}px`);
|
|
}
|
|
|
|
function quit(withBackup) {
|
|
const finish = () => {
|
|
API.ctx.uiState.activeScreen = "wQuit";
|
|
API.renderWorkspace();
|
|
};
|
|
if (withBackup && prefs.autoBackDaily && prefs.lastBackDay !== today())
|
|
return backup("day").then(finish);
|
|
finish();
|
|
}
|
|
|
|
const toggle = (k) => () => ((prefs[k] = !prefs[k]), savePrefs(), render());
|
|
const radio = (k, v) => () => (
|
|
(prefs[k] = v),
|
|
savePrefs(),
|
|
applyView(),
|
|
render(),
|
|
API.renderWorkspace()
|
|
);
|
|
|
|
const H = {
|
|
Act_Maneger: () => go("wPrjMgr"),
|
|
Act_Sys: () => go("wM_Set_Sys"),
|
|
Act_Dan: () => go("wM_JabPum"),
|
|
Act_iLWi_San: () => go("wM_Edit_iLWi"),
|
|
Act_PrjLst: () => go("wPrjList"),
|
|
Act_Prt: () => go("PM_Prt"),
|
|
Act_Calc_Grass: async () => (await API.saveAll(), afterCalc()),
|
|
Act_Calc_All: () => (API.cascadeAndRender(), afterCalc()),
|
|
Act_Refresh: () =>
|
|
API.ctx.currentSourcePath && API.loadProjectFromServer(API.ctx.currentSourcePath),
|
|
Act_Arc_Project: () => API.saveAll(),
|
|
Act_Close_Table: () => {
|
|
Object.assign(API.ctx, {
|
|
state: API.Calc.createDefaultState(),
|
|
dbfData: null,
|
|
currentSourcePath: "",
|
|
});
|
|
go("wPrjMgr");
|
|
},
|
|
mi_AutoBack: toggle("autoBackDaily"),
|
|
mi_Auto_Time_Back: toggle("autoBack30"),
|
|
Act_Arc_Now: async () => {
|
|
const r = await backup("now");
|
|
if (r) alert(`현재상태 백업 완료\n• 폴더: ${r.saved_dir}\n• 압축: ${r.saved_stc}`);
|
|
},
|
|
mi_Del_Recent: () => ((prefs.recent = []), savePrefs(), render()),
|
|
Act_Open_Recent: (btn) => API.loadProjectFromServer(btn.getAttribute("data-path"), "wPrjList"),
|
|
Act_Quit: () => quit(true),
|
|
Act_Halt: () => quit(false),
|
|
Act_UseName2: toggle("useName2"),
|
|
N44: () => ((prefs.scale = 100), (prefs.screenMode = 0), savePrefs(), applyView(), render()),
|
|
N55: () => {
|
|
Object.assign(API.ctx.uiState, {
|
|
boqLevelFilter: "ALL",
|
|
boqSearch: "",
|
|
boqLevelMin: 0,
|
|
boqSel: [],
|
|
});
|
|
API.renderWorkspace();
|
|
},
|
|
Act_information: () => {
|
|
const c = API.ctx;
|
|
const files = (c.dbfData && c.dbfData.file_list) || [];
|
|
API.openModal(
|
|
"제품 및 기타정보",
|
|
`<div class="stm-info"><p><strong>STMATE w 8.0 재현판</strong> — 원본 메뉴·화면 체계를 웹으로 다시 만든 시험판</p><p>열린 프로젝트: ${M.esc(c.currentSourcePath || "(없음)")}</p><p>자료 파일 ${files.length}개: ${M.esc(files.join(" · "))}</p><p>설정 저장: 브라우저(localStorage) · 자동백업 폴더: 서버 work_dbf</p></div>`,
|
|
);
|
|
},
|
|
Act_SetScreen: () => radio("screenMode", (prefs.screenMode + 1) % 4)(),
|
|
Act_Scale_Dfl: () => radio("scale", 100)(),
|
|
Act_Scale_Up: () => radio("scale", Math.min(200, prefs.scale + 25))(),
|
|
Act_Scale_Dn: () => radio("scale", Math.max(50, prefs.scale - 25))(),
|
|
};
|
|
[0, 1, 2, 3].forEach((i) => (H[`Act_SetScreen_${i}`] = () => radio("screenMode", i)()));
|
|
["C", "N", "I", "E"].forEach((k) => (H[`Act_DFL_INDX_${k}`] = () => radio("dflIndex", k)()));
|
|
[20, 30, 40, 50, 60].forEach((w) => (H[`Act_DFL_WIDTH_${w}`] = () => radio("dflWidth", w)()));
|
|
|
|
const enabled = (act) => Boolean(PLUG().actions[act] || H[act]);
|
|
|
|
function run(act, btn) {
|
|
const p = PLUG().actions[act];
|
|
if (p) return p(API.ctx, API, btn);
|
|
if (H[act]) return H[act](btn);
|
|
}
|
|
|
|
// 메뉴·툴바 다시 그림 — 끼워 넣은 메뉴(m.menu = m1~m5)는 해당 메뉴 끝에
|
|
function render() {
|
|
const host = document.getElementById("menu-host");
|
|
if (host)
|
|
host.innerHTML = M.renderMenuBar({
|
|
enabled,
|
|
checked: (n) => (n.check ? Boolean(prefs[n.check]) : prefs[n.radio[0]] === n.radio[1]),
|
|
recent: prefs.recent,
|
|
});
|
|
PLUG().menus.forEach((m) => {
|
|
const box = document.querySelector(`#main-menubar [data-menu="${m.menu}"] > .menu-dropdown`);
|
|
if (box && !box.querySelector(`[data-act="${m.act}"]`))
|
|
box.insertAdjacentHTML("beforeend", `<button data-act="${m.act}">${m.label}</button>`);
|
|
});
|
|
const tb = document.getElementById("main-toolbar");
|
|
if (tb) tb.innerHTML = M.renderToolbar(API.ctx.uiState.activeScreen);
|
|
}
|
|
|
|
function noteRecent(path) {
|
|
const src = (API.ctx.sourcesData.projects || []).find((p) => p.path === path);
|
|
const name = src ? src.name : path.split(/[\\/]/).pop();
|
|
prefs.recent = [{ path, name }].concat(prefs.recent.filter((r) => r.path !== path)).slice(0, 8);
|
|
savePrefs();
|
|
render();
|
|
}
|
|
|
|
const typing = (el) => el && /^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName);
|
|
|
|
function install(api) {
|
|
API = api;
|
|
const scMap = M.shortcutMap();
|
|
render();
|
|
applyView();
|
|
document.getElementById("main-menubar").addEventListener("click", (e) => {
|
|
const btn = e.target.closest("[data-act]");
|
|
if (!btn || btn.disabled) return;
|
|
run(btn.getAttribute("data-act"), btn);
|
|
});
|
|
document.getElementById("main-toolbar").addEventListener("click", (e) => {
|
|
const btn = e.target.closest("[data-tb-act]");
|
|
if (btn) run(btn.getAttribute("data-tb-act"), btn);
|
|
});
|
|
document.addEventListener("keydown", (e) => {
|
|
const act = scMap[M.eventShortcut(e)];
|
|
if (!act || !enabled(act)) return;
|
|
if (typing(document.activeElement) && !/^F\d+$/.test(e.key) && e.key !== "q") return;
|
|
e.preventDefault();
|
|
run(act);
|
|
});
|
|
Object.assign(api, { noteRecent, refreshMenus: render, uiPrefs: prefs, backup });
|
|
}
|
|
|
|
global.STMATE_UI_APP_MENU = { install, prefs, H, enabled };
|
|
})(window);
|