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

250 lines
9.8 KiB
JavaScript

/**
* STMATE w 8.0 — 변환·가져오기 끼워 넣기 (scr_conv.js) · 1번(파일) 메뉴
* 조달청 XML 가져오기(Conv_XML_ST8) · XML 변환 설정(uM_XML_Info · uM_ConstType) · 엑셀 → 내역(wM_From_XLS) ·
* 엑셀 역등록(CONV_XLS_2_ST) · 기준 BOQ·자재 기준자료·코드 변경표 엑셀 왕복 · 분리형 변경 STC · JB_* 제비율표
* 서버: api_conv.py — 쓰기는 work_dbf/ 아래만 · 파일은 화면에서 골라 base64 로 보냄
*/
(function (global) {
"use strict";
const PLUG = (global.STMATE_PLUGINS = global.STMATE_PLUGINS || {
screens: {},
actions: {},
menus: [],
});
const app = () => global.STMATE_APP;
async function req(url, body, raw) {
const res = await fetch(url, {
method: body === undefined ? "GET" : "POST",
headers: { "Content-Type": "application/json" },
body: body === undefined ? undefined : JSON.stringify(body),
});
if (!res.ok) throw new Error(await res.text());
return raw ? res.blob() : res.json();
}
const curPath = (ctx) => {
if (!ctx.currentSourcePath) throw new Error("먼저 프로젝트를 불러오세요.");
return ctx.currentSourcePath;
};
const pname = (ctx) => (ctx.state.projectMeta.pname || "공사").slice(0, 28);
// 파일 고르기 → {b64, name}
function pickFile(accept) {
return new Promise((resolve, reject) => {
const inp = document.createElement("input");
inp.type = "file";
inp.accept = accept;
inp.onchange = () => {
const f = inp.files && inp.files[0];
if (!f) return reject(new Error("파일을 고르지 않음"));
const rd = new FileReader();
rd.onload = () => resolve({ b64: String(rd.result), name: f.name });
rd.onerror = () => reject(rd.error);
rd.readAsDataURL(f);
};
inp.click();
});
}
function download(blob, name) {
const a = document.createElement("a");
a.href = URL.createObjectURL(blob);
a.download = name;
document.body.appendChild(a);
a.click();
setTimeout(() => (URL.revokeObjectURL(a.href), a.remove()), 1000);
}
async function openSaved(path) {
const a = app();
const src = await req("/api/sources");
if (src && src.projects) a.ctx.sourcesData = src;
const sel = document.getElementById("top-project-select");
if (sel && src && src.projects)
sel.innerHTML = src.projects
.map((p) => `<option value="${p.path}">${p.name}</option>`)
.join("");
await a.loadProjectFromServer(path, "wPrjList");
}
const fmtRep = (r) =>
Object.entries(r || {})
.filter(([, v]) => typeof v !== "object")
.map(([k, v]) => `${k}: ${v}`)
.join("\n");
// 화면 XML — report_pps 내보내기(지금 계산 결과)
function currentXml(ctx) {
const P = global.STMATE_REPORT_PPS;
if (!P) throw new Error("조달청 XML 모듈(report_pps.js) 없음");
return P.buildPpsXml(ctx.dbfData, ctx.state, ctx.computed, 1);
}
const ACTS = {
async xmlImport(ctx) {
const f = await pickFile(".xml");
const r = await req("/api/conv/xml-import", {
template_path: curPath(ctx),
xml: { b64: f.b64 },
});
alert(
`[조달청 XML 가져오기] ${r.saved_dir}\n${fmtRep(r.report.info)}\n내역 ${r.report.boq_count}행 · 단가 ${r.report.costn_count}건`,
);
await openSaved(r.saved_dir);
},
async xmlSettings(ctx) {
const opt = await req("/api/conv/xml-options");
const types = opt.prj_types || [];
const pick = prompt(
`[XML 변환 설정] 공사유형 번호:\n${types.map((t, i) => `${i + 1}. ${t}`).join("\n")}`,
"1",
);
const prjType = types[parseInt(pick, 10) - 1];
if (!prjType) return;
const settings = { prj_type: prjType };
if (prjType === "건축공사") {
const help = (opt.building || []).map((b) => b.name).join(", ");
const txt = prompt(
`건축공사 사업내용 — 칸=값 을 ; 로 이어 적음(빈 칸 = 0·첫 값)\n${help}`,
"연면적=0",
);
settings.building = Object.fromEntries(
String(txt || "")
.split(";")
.map((s) => s.split("=").map((x) => x.trim()))
.filter((kv) => kv[0]),
);
}
const r = await req("/api/conv/xml-settings", { xml_text: currentXml(ctx), settings });
download(new Blob([r.xml], { type: "application/xml" }), `${pname(ctx)}_조달청.xml`);
alert(
r.validation.ok ? "XSD 검증 통과" : `XSD 위반: ${(r.validation.errors || [])[0] || ""}`,
);
},
async xlsBoq(ctx) {
const f = await pickFile(".xlsx");
const r = await req("/api/conv/xls-boq", {
source_path: curPath(ctx),
xlsx: { b64: f.b64 },
table: (ctx.dbfData && ctx.dbfData.boq_table_name) || "",
});
const unk = r.report.unknown_codes || [];
alert(
`[엑셀 → 내역] ${r.report.sheet} ${r.report.row_count}행 → ${r.report.table}${unk.length ? `\n단가에 없는 코드 ${unk.length}: ${unk.slice(0, 8).join(", ")}` : ""}`,
);
await openSaved(r.saved_dir);
},
async xlsRegister(ctx) {
const f = await pickFile(".xlsx");
const r = await req("/api/conv/xls-register", {
template_path: curPath(ctx),
xlsx: { b64: f.b64 },
});
alert(`[엑셀 역등록] ${r.report.pname}\n${fmtRep(r.report)}`);
await openSaved(r.saved_dir);
},
async boqXlsx(ctx) {
const tb = (ctx.dbfData && ctx.dbfData.boq_table_name) || "";
download(
await req("/api/conv/boq-xlsx", { source_path: curPath(ctx), table: tb }, true),
`${pname(ctx)}_기준BOQ.xlsx`,
);
},
async jajeXlsx(ctx) {
download(
await req("/api/conv/jaje-xlsx", { source_path: curPath(ctx) }, true),
`${pname(ctx)}_자재기준자료.xlsx`,
);
},
async jajeImport(ctx) {
const f = await pickFile(".xlsx");
const r = await req("/api/conv/jaje-import", {
source_path: curPath(ctx),
xlsx: { b64: f.b64 },
});
const rows = ctx.dbfData.costn_rows || (ctx.dbfData.costn_rows = []);
for (const u of r.costn_updates) {
const row = rows.find((x) => String(x.CODE).trim() === u.CODE);
if (row) Object.assign(row, u);
else rows.push(Object.assign({}, u));
}
app().cascadeAndRender && app().cascadeAndRender();
alert(`[자재 기준자료 불러오기] 바뀐 자재 ${r.count}건 — [저장]으로 반영`);
},
async codeXlsx(ctx) {
download(
await req("/api/conv/code-xlsx", { source_path: curPath(ctx) }, true),
`${pname(ctx)}_코드변경표.xlsx`,
);
},
async codeChange(ctx) {
const how = prompt("코드 변경 — 1: 코드 변경표 엑셀 고르기 · 2: 직접 입력(옛=새;옛=새)", "1");
const body = { source_path: curPath(ctx) };
if (how === "1") body.xlsx = { b64: (await pickFile(".xlsx")).b64 };
else if (how === "2") {
const t = prompt("옛코드=새코드 ; 로 이음", "");
if (!t) return;
body.mapping = Object.fromEntries(
t.split(";").map((s) => s.split("=").map((x) => x.trim())),
);
} else return;
const r = await req("/api/conv/code-change", body);
alert(`[코드 변경] ${Object.keys(r.mapping).length}개 코드 · ${r.change_count}곳 바꿈`);
await openSaved(r.saved_dir);
},
async splitChange(ctx) {
if (!confirm("현재안(통합형이면 BOQ12)을 차수1로 하는 다음 변경 차수 STC 를 만들까요?"))
return;
const r = await req("/api/conv/split-change", { source_path: curPath(ctx) });
alert(
`[분리형 변경 STC] 공사 ${r.report.from_pcode} 차수 ${r.report.from_slot} → 새 파일 차수1 · ${r.saved_stc}`,
);
await openSaved(r.saved_dir);
},
async jb(ctx) {
const L = await req("/api/conv/jb-list");
const m = ctx.currentSourcePath
? await req("/api/conv/jb-match", { source_path: ctx.currentSourcePath })
: {};
const by = {};
for (const f of L.files)
by[`${f.kind} ${f.edition}`] = (by[`${f.kind} ${f.edition}`] || 0) + 1;
alert(
`[기준 제비율표 JB_*] ${L.files.length}개 (${L.dir})\n${Object.entries(by)
.map(([k, v]) => `${k}: ${v}`)
.join("\n")}\n\n[이 공사 RATE 표 ↔ 기준]\n` +
Object.entries(m)
.map(([t, v]) => `${t}: ${v.jb} ${v.exact ? "(같음)" : `(다른 칸 ${v.diff_cells})`}`)
.join("\n"),
);
},
};
async function run(act) {
try {
await ACTS[act](app().ctx);
} catch (err) {
alert("변환 실패: " + err.message);
}
}
[
["Act_ConvXmlImport", "조달청 XML 가져오기 (Conv_XML_ST8)", "xmlImport"],
["Act_ConvXmlSettings", "조달청 XML 변환 설정·내보내기 (uM_XML_Info)", "xmlSettings"],
["Act_ConvXlsBoq", "엑셀 → 내역 불러오기 (wM_From_XLS)", "xlsBoq"],
["Act_ConvXlsRegister", "엑셀 역등록 — 새 공사 (CONV_XLS_2_ST)", "xlsRegister"],
["Act_ConvBoqXlsx", "기준 BOQ 엑셀로 내보내기 (MK_BSCBOQ_EXL)", "boqXlsx"],
["Act_ConvJajeXlsx", "자재 기준자료 엑셀로 내보내기 (wM_JajeXLS)", "jajeXlsx"],
["Act_ConvJajeImport", "자재 기준자료 엑셀 불러오기 (wM_JajeXLS)", "jajeImport"],
["Act_ConvCodeXlsx", "코드 변경표 엑셀로 내보내기 (ST_U_ChgCode)", "codeXlsx"],
["Act_ConvCodeChange", "코드 일괄 변경 (ST_U_ChgCode)", "codeChange"],
["Act_ConvSplitChange", "분리형 변경 STC 만들기 — 차수 인계", "splitChange"],
["Act_ConvJb", "기준 제비율표 JB_* 보기", "jb"],
].forEach(([act, label, key]) => {
PLUG.actions[act] = () => run(key);
PLUG.menus.push({ menu: "m1", label, act });
});
})(window);