/** * 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) => ``) .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);