Files
Aislo/tools/stmate-test/scr_ui_prj.js
T

397 lines
20 KiB
JavaScript

/**
* STMATE w 8.0 — 프로젝트 관리(Act_Maneger) · 공사목록표(wPrjList · TWPRJLIST) · 종료 화면 끼워 넣기 (scr_ui_prj.js)
* 근거: REA 분석/09_보조분석/artifacts/resource-TWPRJLIST.dfm.json
* PnTop 단추(등록정보 수정 · 제잡비 계산 · 해당분 수정 · 복사등록 · 정렬 · 해당분만 압축하기 · 미리보기)
* MainMenu1(1. 수정 · 2. 내역서 등록 · 3. 해당분압축/조달청포맷 생성 · 4. 보기설정)
* RGrdPrjList 열(공사번호 · 공사명 · 코드 · 공사종류 · 직공비 · 총공사비 · 복사원본) · RDbGrid_Boq(편집 불가 미리보기)
* 공사목록 = PRJLST.DBF 행(prjlst_all) — 행마다 내역서 BOQ{PCODE}.DBF 하나
* wPrjList 는 다른 끼워 넣기가 먼저 등록했으면 그쪽을 씀(파일 담당 화면 우선)
*/
(function (global) {
"use strict";
const P = (global.STMATE_PLUGINS = global.STMATE_PLUGINS || {
screens: {},
actions: {},
menus: [],
});
const esc = (s) =>
String(s == null ? "" : s).replace(
/[&<>"]/g,
(c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c],
);
const $ = (id) => document.getElementById(id);
const num = (v) => Number(String(v == null ? "" : v).replace(/,/g, "")) || 0;
const go = (ctx, api, scr, extra) => (
Object.assign(ctx.uiState, { activeScreen: scr }, extra || {}),
api.renderWorkspace()
);
const activeCode = (ctx) =>
String((ctx.dbfData && ctx.dbfData.boq_table_name) || "").replace(/^BOQ|\.DBF$/g, "");
// ── 프로젝트 관리 ──
function renderMgr(ctx) {
const src = (ctx.sourcesData && ctx.sourcesData.projects) || [];
const kinds = { folder: "PC 폴더", stc: "STC 압축", work_folder: "작업본 폴더" };
return `<div class="win-form" id="mgr-form">
<div class="win-form-header"><span>프로젝트 관리 [ 열기, 등록, 백업, 삭제 ]</span><span>${src.length}개</span></div>
<div class="win-subbar"><button class="win-btn primary" id="mgr-open" ${ctx.uiState.mgrSel ? "" : "disabled"}>열기</button>
<button class="win-btn" id="mgr-back" ${ctx.dbfData ? "" : "disabled"}>현재 프로젝트 백업</button>
<button class="win-btn" data-file-act="new" ${ctx.dbfData ? "" : "disabled"} title="지금 프로젝트의 요율·서식·설정을 틀로 빈 공사 등록">등록(새 공사)</button>
<button class="win-btn" data-file-act="backups" ${ctx.dbfData ? "" : "disabled"}>백업·복원</button>
<button class="win-btn" data-file-act="check" ${ctx.dbfData ? "" : "disabled"}>STC 검사</button>
<button class="win-btn" disabled title="프로젝트 지우기는 서버(파일) 기능 대기">삭제</button>
<span class="stm-note">열린 프로젝트: ${esc(ctx.currentSourcePath || "(없음)")}</span></div>
<div class="split-vert"><div class="pane-box" style="flex:1;"><div class="grid-scroll"><table class="dbgrid"><thead><tr><th>No</th><th>구분</th><th>프로젝트</th><th>경로</th></tr></thead><tbody>
${src
.map(
(p, i) =>
`<tr class="mgr-row ${p.path === ctx.currentSourcePath ? "selected" : ""} ${p.path === ctx.uiState.mgrSel ? "boq-sel" : ""}" data-path="${esc(p.path)}"><td class="center">${i + 1}</td><td class="center">${kinds[p.kind] || esc(p.kind)}</td><td><strong>${esc(p.name)}</strong></td><td><small>${esc(p.path)}</small></td></tr>`,
)
.join("")}</tbody></table></div></div></div></div>`;
}
function bindMgr(ctx, api) {
document.querySelectorAll("#mgr-form .mgr-row").forEach((tr) => {
tr.onclick = () => (
(ctx.uiState.mgrSel = tr.getAttribute("data-path")),
api.renderWorkspace()
);
tr.ondblclick = () => api.loadProjectFromServer(tr.getAttribute("data-path"), "wPrjList");
});
$("mgr-open").onclick = () => api.loadProjectFromServer(ctx.uiState.mgrSel, "wPrjList");
$("mgr-back").onclick = async () => {
const r = await api.backup("now");
if (r) alert(`백업 완료\n• 폴더: ${r.saved_dir}\n• 압축: ${r.saved_stc}`);
};
}
// ── 공사목록표 ──
const DISABLED = "아직 만들지 않은 기능";
// 파일 담당(scr_file.js) 동작 — data-file-act 단추는 문서 전체 클릭으로 그쪽이 받음 · 공사 코드 = prj-pcode 라디오
const FILE_ACT = { ActAddCopy: "copy", ActHae_Arc: "pack" };
const PRJ_MENU = [
[
"1. 수 정",
[
["ActEditBoq", "내역서 수정(&S)"],
["ActSagp", "관(사)급 내역 생성/수정"],
["Act_ED_PNUM", "등록정보 수정 (공사번호/공사명/공사종류)"],
["Act_DN_Chasu", "설계변경내역서 변경차수 내리기"],
null,
["Act_Ed_Jab", "제잡비 계산"],
["Act_CalBoq", "선택내역서 재계산"],
["Act_Prn", "선택된 항목의 기본내역서 인쇄"],
null,
["ActHaeCost", "해당분 단가 수정작업"],
["Act_Calc_Hae", "해당분 복합단가 전체 재계산"],
["Act_Find_Jod", "조달청제출용 일위대가 내용검증(손료부분,산근포함)"],
["Act_Find_XML", "조달청제출용 대가/산근 내용검증(XML, 손료부분 or 대가수량소수점)"],
null,
["Act_Del_Prj", "내역서 삭제"],
["Act_Mer_Prj", "내역서 병합"],
null,
["Act_Sagp_Del", "관/사급 내역 삭제"],
],
],
[
"2. 내역서 등록",
[
["ActNewBoq", "신규등록(&A)"],
["ActAddCopy", "복사등록(&C)"],
null,
["ActAddTo5", "할증내역으로 등록"],
["ActMkCont00", "계약내역 등록"],
["Act_MkCont41", "실행예산 복사등록"],
["ActMkCont10", "기성내역 등록"],
null,
["ActMkCont21", "설계변경 등록 (당초/변경)"],
["ActMkCont20", "차수별 설계변경 등록"],
["ActMkCont31", "굴착변경 등록"],
null,
["Act_Mk_Boq10", "공량내역(기계,전기..) 등록"],
],
],
[
"3. 해당분압축/조달청포맷 생성",
[
["ActHae_Arc", "선택된 해당분만 압축하기[내역, 대가, 기초단가 ... ]"],
null,
["Act_St8_XML", "공사원가호환규정 포맷(XML, 조달청) 변환(해당분)"],
["Act_St8_Jodal1", "JDL 포맷(TEXT) 변환(해당분)"],
],
],
[
"4. 보기설정",
[
["Act_Find", "문자열 찾기"],
["Act_View_BOQ", "내역서 미리보기/숨기기"],
null,
["ActIndex", "공사목록표 내림차순 정렬"],
null,
["Act_Indx_PCODE", "공사목록표 코드순 정렬"],
["Act_Indx_PNAME", "공사목록표 공사명순 정렬"],
["Act_Indx_PNUM", "공사목록표 공사번호순 정렬"],
null,
["Act_List_UP", "내역목록 위로이동"],
["Act_List_Down", "내역목록 아래로 이동"],
],
],
];
const prjRows = (ctx) => (ctx.dbfData && ctx.dbfData.prjlst_all) || [];
const selRow = (ctx) =>
prjRows(ctx).find((r) => String(r.PCODE).trim() === ctx.uiState.prjSel) || prjRows(ctx)[0];
// 계약·실행·기성·설계변경 — 단계 담당(scr_stage.js) 탭 동작으로
const stage = (act) => (ctx, api) =>
P.actions[act] ? P.actions[act](ctx, api) : go(ctx, api, "wStage");
const sortBy = (key, desc) => (ctx, api) => {
const rows = prjRows(ctx);
rows.sort(
(a, b) =>
String(a[key] || "").localeCompare(String(b[key] || ""), "ko", { numeric: true }) *
(desc ? -1 : 1),
);
api.renderWorkspace();
};
const moveSel = (dir) => (ctx, api) => {
const rows = prjRows(ctx),
i = rows.indexOf(selRow(ctx)),
j = i + dir;
if (i < 0 || j < 0 || j >= rows.length) return;
[rows[i], rows[j]] = [rows[j], rows[i]];
api.renderWorkspace();
};
// 단계 등록(기성 6 · 설계변경 7 / 차수별 0 · 당초/변경 1) — 원본 PCLASS·K_ORD 로 새 K_ORD(api_ui_project.py) → 새 공사 열고 단계 탭
const stageReg = (op, flag, act) => async (ctx, api) => {
const r = selRow(ctx);
if (!r || !ctx.currentSourcePath) return stage(act)(ctx, api);
const what = op === 6 ? "기성내역" : flag ? "설계변경(당초/변경)" : "차수별 설계변경";
if (!confirm(`[${String(r.PCODE).trim()}] ${r.PNAME} 을(를) ${what} 으로 등록할까요?`))
return stage(act)(ctx, api);
try {
const res = await api.postJson("/api/ui/stage-register", {
source_path: ctx.currentSourcePath,
src_pcode: String(r.PCODE).trim(),
op,
flag,
pname: `${r.PNAME} (${what})`,
});
await api.loadProjectFromServer(res.saved_stc, "wPrjList");
ctx.uiState.prjSel = res.new_pcode;
ctx.uiState.boqMsg = `${what} 등록 — PCODE ${res.new_pcode} · K_ORD ${res.k_ord}`;
stage(act)(ctx, api);
} catch (err) {
alert(`${what} 등록 실패: ${err.message}`);
}
};
// 다른 내역서 열기 — 고른 공사 행의 BOQ{PCODE}.DBF 로 바꿔 불러옴(/api/ui/load-boq · api_ui_code.py)
async function openBoq(ctx, api, pcode) {
const d = ctx.dbfData;
const dirty =
d && (d.ui_dirty || (d.boq_deleted || []).length || (d.boq_rows || []).some((r) => r._new));
if (dirty && !confirm("지금 내역서에 저장 안 한 손질이 있음 — 버리고 다른 내역서를 열까요?"))
return false;
try {
const data = await api.postJson("/api/ui/load-boq", {
source_path: ctx.currentSourcePath,
pcode,
});
ctx.dbfData = data;
ctx.state = data.sim_state;
Object.assign(ctx.uiState, { editedSmtMap: {}, boqSel: [], sysDraft: null, prjSel: pcode });
return true;
} catch (err) {
alert("내역서 열기 실패: " + err.message);
return false;
}
}
// 고른 공사가 열린 내역서가 아니면 먼저 연 뒤 이어서
const withActive = (fn) => (ctx, api) => {
const r = selRow(ctx);
const pc = r ? String(r.PCODE).trim() : "";
if (!r || pc === activeCode(ctx)) return fn(ctx, api);
openBoq(ctx, api, pc).then((ok) => ok && fn(ctx, api));
};
function editMeta(ctx, api) {
const r = selRow(ctx);
if (!r) return;
api.openModal(
"등록정보 수정 (공사번호/공사명/공사종류)",
`<div class="stm-form"><div class="stm-row"><label>공사번호(PNUM)</label><input id="pm-pnum" class="win-input" value="${esc(r.PNUM)}" style="width:300px;" /></div>
<div class="stm-row"><label>공사명(PNAME)</label><input id="pm-pname" class="win-input" value="${esc(r.PNAME)}" style="width:420px;" /></div>
<div class="stm-row"><label>공사종류(PCLASS 코드)</label><input id="pm-pclass" class="win-input" type="number" value="${esc(r.PCLASS)}" style="width:80px;" /><span class="stm-note">코드 뜻은 원본 분석 미확정 — 숫자 그대로</span></div>
<div class="stm-row"><button class="win-btn primary" id="pm-ok">확인</button></div></div>`,
);
$("pm-ok").onclick = () => {
Object.assign(r, {
PNUM: $("pm-pnum").value,
PNAME: $("pm-pname").value,
PCLASS: String(num($("pm-pclass").value)),
});
if (String(r.PCODE).trim() === activeCode(ctx)) {
Object.assign(ctx.state.projectMeta, {
pname: r.PNAME,
pnum: r.PNUM,
pclass: num(r.PCLASS),
});
if (ctx.dbfData.prjlst && ctx.dbfData.prjlst !== r)
Object.assign(ctx.dbfData.prjlst, { PNUM: r.PNUM, PNAME: r.PNAME, PCLASS: r.PCLASS });
}
api.closeModal();
api.renderWorkspace();
};
}
const H = {
ActEditBoq: withActive((ctx, api) => go(ctx, api, "wBoq")),
ActHaeCost: (ctx, api) => go(ctx, api, "wM_JabPum"),
Act_ED_PNUM: editMeta,
Act_Ed_Jab: withActive((ctx, api) => go(ctx, api, "wM_KanJub")),
Act_CalBoq: withActive((ctx, api) => api.cascadeAndRender()),
Act_Calc_Hae: (ctx, api) => api.cascadeAndRender(),
Act_Prn: (ctx, api) => go(ctx, api, "PM_Prt"),
ActMkCont00: stage("Act_Stg_Contract"),
Act_MkCont41: stage("Act_Stg_Exec"),
ActMkCont10: stageReg(6, 0, "Act_Stg_Prog"),
ActMkCont21: stageReg(7, 1, "Act_Stg_Change"),
ActMkCont20: stageReg(7, 0, "Act_Stg_Change"),
Act_St8_XML: (ctx, api) => go(ctx, api, "PM_Prt"),
Act_St8_Jodal1: (ctx, api) => go(ctx, api, "PM_Prt"),
Act_Find: (ctx, api) => {
const kw = prompt("찾을 문자열(공사명·공사번호)", ctx.uiState.prjFind || "");
if (kw == null) return;
ctx.uiState.prjFind = kw.trim();
api.renderWorkspace();
},
Act_View_BOQ: (ctx, api) => (
(ctx.uiState.showBoqPreview = ctx.uiState.showBoqPreview === false),
api.renderWorkspace()
),
ActIndex: (ctx, api) => (
(ctx.uiState.prjDesc = !ctx.uiState.prjDesc),
sortBy("PCODE", ctx.uiState.prjDesc)(ctx, api)
),
Act_Indx_PCODE: sortBy("PCODE"),
Act_Indx_PNAME: sortBy("PNAME"),
Act_Indx_PNUM: sortBy("PNUM"),
// 내역서 삭제(Act_Del_PrjExecute) — 고른 공사 행·BOQ1n 을 뺀 작업본 STC 를 쓰고 그것을 다시 엶(api_ui_project.py)
Act_Del_Prj: async (ctx, api) => {
const r = selRow(ctx);
if (!r || !ctx.currentSourcePath) return;
const pc = String(r.PCODE).trim();
if (prjRows(ctx).length < 2) return alert("마지막 공사는 지울 수 없음");
if (!confirm(`[${pc}] ${r.PNAME} 내역서를 삭제할까요? (작업본 STC 에 반영 · 원본은 그대로)`))
return;
try {
const res = await api.postJson("/api/ui/project-delete", {
source_path: ctx.currentSourcePath,
pcode: pc,
});
ctx.uiState.prjSel = "";
await api.loadProjectFromServer(res.saved_stc, "wPrjList");
} catch (err) {
alert("내역서 삭제 실패: " + err.message);
}
},
Act_List_UP: moveSel(-1),
Act_List_Down: moveSel(1),
};
const enabled = (act) => Boolean(P.actions[act] || H[act]);
const actBtn = (a, t, cls) =>
FILE_ACT[a] && !P.actions[a]
? `<button class="${cls}" data-file-act="${FILE_ACT[a]}">${t}</button>`
: `<button class="${cls}" data-prj-act="${a}" ${enabled(a) ? "" : `disabled title="${DISABLED}"`}>${t}</button>`;
const run = (act, ctx, api, btn) =>
P.actions[act] ? P.actions[act](ctx, api, btn) : H[act] && H[act](ctx, api);
function previewRows(ctx, code) {
if (code === activeCode(ctx)) return global.STMATE_VIEW_PRJ_BOQ.evaluateFullBoqRows(ctx);
return ((ctx.dbfData.boq_tables || {})[`BOQ${code}.DBF`] || []).map((r) =>
Object.assign({}, r),
);
}
function renderList(ctx, api) {
if (!ctx.dbfData) return renderMgr(ctx);
const kw = String(ctx.uiState.prjFind || "").toLowerCase();
const rows = prjRows(ctx).filter(
(r) => !kw || `${r.PNUM} ${r.PNAME}`.toLowerCase().includes(kw),
);
const cur = selRow(ctx),
code = cur ? String(cur.PCODE).trim() : "";
const act = activeCode(ctx);
const { fmt } = api;
const bt = ctx.computed.boqComputed.totals,
kj = ctx.computed.kanjubComputed;
const show = ctx.uiState.showBoqPreview !== false;
const prev = show ? previewRows(ctx, code) : [];
const menu = `<div class="sub-menubar">${PRJ_MENU.map(
([cap, items]) =>
`<div class="menu-item"><span>${cap}</span><div class="menu-dropdown">${items
.map((x) => (x ? actBtn(x[0], esc(x[1]), "") : "<hr />"))
.join("")}</div></div>`,
).join("")}</div>`;
const btn = (a, t) => actBtn(a, t, "win-btn");
return `<div class="win-form" id="prj-form">
<div class="win-form-header"><span>공사목록표 [wPrjList · PRJLST.DBF] — ${prjRows(ctx).length}건</span><span>${esc(ctx.currentSourcePath)}</span></div>
${menu}
<div class="win-subbar">${btn("Act_ED_PNUM", "등록정보 수정")}${btn("Act_Ed_Jab", "제잡비 계산")}${btn("ActEditBoq", "해당분 수정")}${btn("ActAddCopy", "복 사 등 록")}${btn("ActIndex", ctx.uiState.prjDesc ? "올림차순" : "내림차순")}${btn("ActHae_Arc", "해당분만 압축하기")}${btn("Act_View_BOQ", show ? "미리보기 숨기기" : "내역서 미리보기")}${kw ? `<span class="stm-note">찾기: ${esc(kw)}</span>` : ""}</div>
<div class="split-vert">
<div class="pane-box" style="flex:${show ? "0 0 32%" : "1"};"><div class="grid-scroll"><table class="dbgrid"><thead><tr><th>공사번호</th><th>공 사 명</th><th>코드</th><th>공사종류</th><th>직공비</th><th>총공사비</th><th>복사원본</th><th>지금 계산(직공비 / 총공사비)</th></tr></thead><tbody>
${rows
.map((r) => {
const c = String(r.PCODE).trim();
const live = c === act ? `${fmt(bt.directT)} / ${fmt(kj.GRAND_TOTAL)}` : "";
return `<tr class="prj-pick ${c === code ? "boq-sel" : ""}" data-code="${esc(c)}"><td><input type="radio" name="prj-pcode" value="${esc(c)}" ${c === code ? "checked" : ""} /> ${esc(r.PNUM)}</td><td><strong>${esc(r.PNAME)}</strong></td><td class="center">${esc(c)}${c === act ? " ●" : ""}</td><td class="center">${esc(r.PCLASS)} · 차수 ${esc(r.K_ORD)}</td><td class="num">${fmt(num(r.TCOST1))}</td><td class="num">${fmt(num(r.COSTW1))}</td><td class="center">${num(r.CCODE) ? esc(r.CCODE) : ""}</td><td class="num">${live}</td></tr>`;
})
.join("")}</tbody></table></div></div>
${
show
? `<div class="pane-box" style="flex:1;"><div class="pane-title"><span>BOQ${esc(code)}.DBF 미리보기 — ${prev.length}행 (이 화면에서는 편집할 수 없음)</span><span>${code === act ? "지금 계산 금액" : "파일 저장 금액"}</span></div><div class="grid-scroll"><table class="dbgrid"><thead><tr><th>공종</th><th>명 칭</th><th>규 격</th><th>수 량</th><th>단위</th><th>단가</th><th>금액</th><th>비고</th></tr></thead><tbody>
${prev
.map(
(r) =>
`<tr class="${r._rowClass || ""}"><td class="center">${esc(r.ITNUM)}</td><td>${esc(r.NAME1)}</td><td>${esc(r.SIZE1)}</td><td class="num">${num(r.QTY1) ? fmt(num(r.QTY1), 4) : ""}</td><td class="center">${esc(r.UNIT1)}</td><td class="num">${num(r.TAMT1) ? fmt(num(r.TAMT1)) : ""}</td><td class="num">${num(r.T_AMT1) ? fmt(num(r.T_AMT1)) : ""}</td><td class="center">${esc(String(r.BDCODE1 || "").trim())}</td></tr>`,
)
.join("")}</tbody></table></div></div>`
: ""
}
</div>
<div class="win-subbar"><span>PrjBar — 선택: ${esc(cur ? cur.PNAME : "")} (BOQ${esc(code)}) · 열린 내역서 BOQ${esc(act)}</span></div>
</div>`;
}
function bindList(ctx, api) {
if (!ctx.dbfData) return bindMgr(ctx, api);
if (!ctx.uiState.prjSel) ctx.uiState.prjSel = activeCode(ctx);
ctx.uiState.filePcode = ctx.uiState.prjSel;
document
.querySelectorAll("#prj-form [data-prj-act]")
.forEach(
(b) => (b.onclick = () => !b.disabled && run(b.getAttribute("data-prj-act"), ctx, api, b)),
);
document.querySelectorAll("#prj-form .prj-pick").forEach((tr) => {
tr.onclick = () => (
(ctx.uiState.prjSel = tr.getAttribute("data-code")),
api.renderWorkspace()
);
tr.ondblclick = () => (
(ctx.uiState.prjSel = tr.getAttribute("data-code")),
run("ActEditBoq", ctx, api)
);
});
}
P.screens.wPrjMgr = { render: renderMgr, bind: bindMgr };
if (!P.screens.wPrjList) P.screens.wPrjList = { render: renderList, bind: bindList };
P.screens.wQuit = {
render: () =>
`<div class="win-form"><div class="win-form-header"><span>시스템종료</span></div><div class="stm-quit"><p>STMATE 작업을 마쳤음.</p><p><button class="win-btn primary" id="quit-back">다시 시작 — 프로젝트 관리</button></p></div></div>`,
bind: (ctx, api) => ($("quit-back").onclick = () => go(ctx, api, "wPrjMgr")),
};
})(window);