/* -*- coding: utf-8 -*- */ /** * 보고서 묶음 (report_book.js) — 자재단가대비표(0105) · 목차 · 안내 · 시트 차례 · SET_REP 서식 표 * 근거: 지식DB STmate/old/20_분석 20 §2.3(가격지 슬롯 이름 JMOK · 축약 JMK) · §3(SET_REP 75종) * 19 §3(목차·안내 시트) · old/30_원자료/print_form_map.txt(서식 번호 ↔ 출력 함수) * 정답 대조: resources/tester/test_stmate_report_sheets.py */ const StmateReportBook = (() => { const g = typeof globalThis !== "undefined" ? globalThis : {}; const C = g.STMATE_REPORT_CORE || require("./report_core.js"); const U = g.STMATE_REPORT_UNIT || require("./report_unit.js"); const B = g.STMATE_REPORT_BOQ || require("./report_boq.js"); const Co = g.STMATE_REPORT_COLL || require("./report_coll.js"); const Sa = g.STMATE_REPORT_SAN || require("./report_san.js"); const In = g.STMATE_REPORT_INIT || require("./report_init.js"); const Lk = g.STMATE_REPORT_LINK || require("./report_link.js"); const { num, str, code } = C; /** * 자재단가대비표(0105) — 재료비(M) 전수 · 가격지 5칸(단가·페이지) · 적용 단가와 적용 가격지 축약 * 가격지 이름 = Start8x.Ini JMOK1~6 · 축약 = JMK10~50 · 적용 슬롯 = COSTN PG_1 */ function priceCompareSheet(ctx) { const { dbf, idx, ini, po } = ctx; const sh = C.newSheet("자재단가대비표", "자재단가 대비표", C.projectName(dbf), "R"); const slots = ["E", "G", "I", "K", "M"]; const h3 = { A: po.codeMode ? "코드번호" : "호표", B: "명 칭", C: "규 격", D: "단위", }; const h4 = { _k: "head" }; slots.forEach((c, i) => { h3[c] = str(ini[`JMOK${i + 1}`] || "").trim() || `가격${i + 1}`; h4[c] = "단 가"; h4[String.fromCharCode(c.charCodeAt(0) + 1)] = "페이지"; }); Object.assign(h3, { O: str(ini.JMOK6 || "적용 단가").trim(), Q: "비 고", _k: "head" }); Object.assign(h4, { O: "단 가", P: "페이지" }); C.push(sh, h3); C.push(sh, h4); idx.ofPrefix("M").forEach((r, i) => { const c = code(r); const lbl = po.codeMode ? c : po.padNo ? String(i + 1).padStart(4) : String(i + 1); const cells = { A: lbl, B: str(r.NAME), C: str(r.SIZE), D: C.unitText(r.UNIT) }; Object.assign(cells, { _k: "item", _code: c }); slots.forEach((col, j) => { cells[col] = num(r[`AMT_${j + 1}1`]); const pg = str(r[`PG_${j + 1}1`]).trim(); if (pg) cells[String.fromCharCode(col.charCodeAt(0) + 1)] = pg; }); const p = idx.price(c, 1); cells.O = p.M + p.L + p.S; const s = num(r.PG_1); if (s >= 1 && s <= 5) cells.P = str(ini[`JMK${s}0`] || "").trim(); const rm = U.remarkOf(po, c, lbl, r); if (rm) cells.Q = rm; C.push(sh, cells); }); return sh; } // 시트 차례 — 원본 STMATE 엑셀 내보내기 기본 순서(영월·울진 출력 목차) · 인쇄순서(F1-52)로 바꿈 const ORDER = [ "공사원가계산서", "총괄내역서", "설계내역서", "일위대가목록표", "일위대가표", "일위대가수량금액집계표", "단가산출근거목록표", "단가산출근거", "단가산출근거수량금액집계표", "환율및기초자료", "중기목록표", "중기사용료", "재료비목록표", "노무비목록표", "경비목록표", "일식견적목록표", "표준시장목록표", "실적단가목록표", "자재단가대비표", "재료비수량금액집계표", "노무비수량금액집계표", "경비수량금액집계표", "일식견적수량금액집계표", "표준시장수량금액집계표", "중기시간금액집계표", ]; const TOC = "〓 목 차 〓"; const GUIDE = "※※안내※※"; // 재등록 최소 판 — 24 판 출력은 「w24.02」 · 25 판부터 프로그램 판(원본 목차 D2 실측 · 울진 25.07 은 출력 프로그램 판) function regVersion(ini) { const v = str(ini.PRG_V || "").trim(); return num(v) >= 25 ? `w${v}` : "w24.02"; } /** * 목차 시트(〓 목 차 〓) — 원본 출력 그대로: D1 INITIAL 지움 검사 수식 · B2 제목 · D2~D3 재등록 안내 · * 4행부터 「n. 시트이름」 + 바로가기(→) · 안내 시트가 1번 · INITIAL 은 목록 밖 */ function tocSheet(sheets, dbf) { const ini = (dbf && dbf.start_ini) || {}; const sh = { name: TOC, title: "목 차", rows: [], cols: { B: 25.625, D: 23.625 }, tab: "0000FF", }; const chk = `=IF('${Lk.INIT}'!C4,"※※ 기본정보의 일부가 삭제되어 Stmate로 변환할 수 없습니다 ※※","")`; C.put(sh, 1, { _f: { D: chk }, _sz: { D: 12 }, _bold: { D: true } }); C.put(sh, 2, { B: "목 차", D: `※ 본 엑셀자료는 【STmate ${regVersion(ini)}】버전 이상에서 STmate 자료로 다시 등록할 수 있습니다.`, _sz: { B: 20, D: 9 }, }); C.put(sh, 3, { D: " 단, 임의로 수정한 내용(Sheet삭제, 산근내용변경, 기타계산식변경..)은 반영되지 않을 수 있습니다.", _sz: { D: 9 }, }); sheets.forEach((s, i) => { const cells = { B: `${i + 1}. ${s.name}`, D: `${s.name} →`, _sz: { B: 11, D: 9 } }; cells._links = { D: `${Lk.q(s.name)}!A1` }; const tag = Lk.tagOf(s); cells._f = { D: tag ? `=HYPERLINK("#"&${Lk.q(s.name)}!${tag}2&"!A1","${s.name} →")` : `=HYPERLINK("#${Lk.q(s.name)}!A1","${s.name} →")`, }; C.put(sh, 4 + i, cells); }); return sh; } /** * 안내 시트(※※안내※※) — 원본 출력 글 · 판·산근 수식 갈래 · 아래 알림(안내 및 오류) 줄 * 알림 차례 = 설계내역서 % 단위 → 총괄 이윤보정액(I) · 총공사비(D) → 일위대가표 → 단가산출근거 → 중기사용료 */ function guideSheet(sheets, dbf, k) { const ini = (dbf && dbf.start_ini) || {}; const sh = { name: GUIDE, title: "안내", rows: [], cols: { A: 9.125, B: 4.75 }, tab: "FF0000" }; const by = {}; for (const s of sheets) by[s.id || s.name] = s; C.put(sh, 2, { A: "목차 →", B: "=== 엑셀파일에 작성에 대한 안내문(반드시 읽어보세요) ===", _links: { A: `${Lk.q(TOC)}!B2` }, _f: { A: `=HYPERLINK("#${Lk.q(TOC)}!B2","목차 →")` }, _sz: { A: 9, B: 12 }, _bold: { B: true }, }); const line = (r, t, extra) => C.put(sh, r, Object.assign({ B: t, _sz: { B: 10 } }, extra || {})); line(4, "엑셀파일 수정후 계산식 반영이 안될경우 :【 Ctrl+Alt(+Shift)+F9 】", { _sz: { B: 14 }, _bold: { B: true }, }); [ "◈ 엑셀파일내의 산식은 기초단가, 복합단가, 내역서 및 총괄내역서까지 연결됩니다.", " (모든산식은 단가만을 불러오며, 명칭, 규격, 단위, 비고.. 등은 TEXT로 입력됩니다)", "◈ 각 Sheet우측에는 해당 항목을 찾아가는 위치이동(→) 링크가 있으며, 클릭하면 이동됩니다.", " (Alt + ← 키를 이용하여 찾아갔던 항목을 되돌아 올 수 있습니다.)", "◈ 자재단가 대비표의 적용단가는 수식이 적용되지 않았으나, 우측에 최소단가 수식을 입력하였습니다.", "◈ 일위대가표 및 중기사용료 및 총괄내역서의 양식은 일반양식을 적용합니다.", ].forEach((t, i) => line(6 + i, t)); // 산근 수량 산식 갈래 — report_san 수치 글 다듬기와 같은 판단(FLT_JAB=1 · 원본 5건 실측 · 추정) const sanFml = String(ini.FLT_JAB ?? "2").trim() === "1"; const san = by.단가산출근거; const t12 = "◈ 단가산출근거의 수량부분은 산출근거 우측 연두색 부분에서 수정합니다. →"; if (sanFml && san && Lk.tagOf(san)) line(12, t12, { _links: { B: `${Lk.q(san.name)}!Y3` }, _f: { B: `=HYPERLINK("#"&${Lk.q(san.name)}!${Lk.tagOf(san)}2&"!Y3","${t12}")` }, }); else line(12, "◈ 단가산출근거의 수량부분은 계산식이 입력되지 않았습니다."); let r = 13; const v25 = num(ini.PRG_V) >= 25; if (v25 || sanFml) line( r++, `◈ [목차] 및 [안내]${v25 ? " " : ""}Sheet의 이름을 변경하지 마시고, Sheet이름을 변경할때 공백문자 및 특수문자를 사용하지 마십시요.`, ); r += 1; line(r++, "※※※ 아래의 내용(안내 및 오류)을 확인/검토 하세요 ※※※", { _bold: { B: true }, }); for (const w of guideNotes(by, dbf, k)) { const ref = `${Lk.q(w.sh.name)}!${w.col}${w.row}`; C.put(sh, r++, { B: " → ", C: w.text, _links: { B: ref }, _f: { B: Lk.jump(w.sh, w.col, w.row, " → ") }, _sz: { B: 9, C: 9 }, _bold: w.bold ? { C: true } : undefined, }); } return sh; } /** * 안내 알림 줄 — % 단위 코드 줄(산식 못 넣음 · 손료계산 형식 권고) · 총괄 이윤보정액 · 총공사비 확인 * 총공사비 확인 줄 = 이윤보정액이 천 원 끝전 맞춤 범위(1,000 미만)일 때(원본 5건 실측 · 대흥 905,980 = 없음 · 추정) * 중기 % 줄 = 규격 「주연료의 %」(잡품·잡유)는 연료 줄 밑수 산식이 있어 알림 없음(대흥 실측) */ function guideNotes(by, dbf, k) { const unit = {}; for (const r of (dbf && dbf.costn_rows) || []) unit[code(r)] = str(r.UNIT).trim(); const isP = (c) => /%$/.test(unit[c] || ""); const out = []; const pctMsg = (s, row, c) => `【${s.name}】${row}번째줄 에서 사용된【${c}】코드의 %단위(${unit[c]}) 는 산식을 입력할 수 없으니 손료계산 형식으로 변경하십시요.`; const scan = (s, kind, get, col) => s && s.rows.forEach((r, i) => { const c = r._k === kind && get(r); if (c && isP(c)) out.push({ sh: s, col: col(c), row: i + 1, text: pctMsg(s, i + 1, c) }); }); scan( by.설계내역서, "item", (r) => r._code, () => "A", ); const sm = by.총괄내역서; if (sm) { // 변경 양식(당초·변경 두 줄)이면 둘 다 — 변경 줄 = 이름 칸 빈 다음 줄(영덕 실측) const pair = (rn) => { const nx = sm.rows[rn]; return nx && !nx.A && !nx.B && nx.D != null ? [rn, rn + 1] : [rn]; }; const iy = sm.rows.findIndex((r) => r._k === "cost" && /이\s*윤/.test(String(r.B || ""))) + 1; const n = sm.rows.length; const last = n > 1 && !sm.rows[n - 1].A && !sm.rows[n - 1].B ? [n - 1, n] : [n]; if (iy > 0) for (const r of pair(iy)) out.push({ sh: sm, col: "I", row: r, bold: true, text: `【${sm.name}】${r}번째줄의 I열 이윤보정액은 총공사비 금액이 다를경우 임의조정(±α) 하십시요.`, }); if (k && Math.abs(num(k.pretaxProfitAdj)) < 1000) for (const r of last) out.push({ sh: sm, col: "D", row: r, text: `【${sm.name}】${r}번째줄의 D열에 계산된 총공사비 금액을 확인하십시요.`, }); } const own = { M: "I", L: "G", S: "K" }; scan( by.일위대가표, "line", (r) => r._code, (c) => own[c[0]] || "C", ); scan( by.단가산출근거, "line", (r) => r._ref, () => "B", ); const spec = Lk.specOf({ _dbf: dbf }); const jm = by.중기사용료; if (jm) jm.rows.forEach((r, i) => { if (r._k === "line" && isP(r._code) && !/주연료/.test(spec[r._code] || "")) out.push({ sh: jm, col: own[r._code[0]] || "C", row: i + 1, text: pctMsg(jm, i + 1, r._code), }); }); return out; } /** * 전체 묶음 — dbf(불러온 자료) · state(sim_state) · computed(calculateAll) · n 차수 * 돌려줌: { sheets:[시트 모형…], byName } */ function buildBook(dbf, state, computed, n = 1, opts = {}) { const unit = U.buildUnitSheets(dbf); const boq = B.buildBoqSheets(dbf, state, computed, n, opts); const pc = C.applyForm(priceCompareSheet(unit.ctx), dbf, "0105"); // 집계표(0501~0508) — 변경 차수면 당초·변경 두 줄 양식 · 단가산출근거(0402) 펼침 const base = n > 1 ? B.boqTree(dbf, B.stageOf(state, computed, 1).boq, 1) : null; const coll = Co.buildCollSheets( unit.ctx, boq.설계내역서._tree, Object.assign({ baseTree: base }, opts), ); const san = C.applyForm(Sa.sanSheet(unit.ctx, opts), dbf, "0402"); const all = Object.assign({}, unit, boq, coll, { 자재단가대비표: pc, 단가산출근거: san }); const order = opts.order && opts.order.length ? opts.order : ORDER; const sheets = order.filter((id) => all[id]).map((id) => Object.assign(all[id], { id })); const init = In.initialSheet(dbf, state, sheets, n, opts); Lk.linkBook(sheets, dbf); // 시트 간 참조 수식 · 바로가기(→) · 이름표 수식 const guide = guideSheet(sheets, dbf, boq.stage.kanjub); const book = [tocSheet([guide].concat(sheets), dbf), guide].concat(sheets, [init]); for (const s of book) delete s._tree; return { sheets: book, byName: Object.fromEntries(book.map((s) => [s.name, s])) }; } // SET_REP 서식 → 시트 이름(구현된 것) — 없으면 null const FORM_SHEET = { "0101": "재료비목록표", "0102": "노무비목록표", "0103": "경비목록표", "0105": "자재단가대비표", "0201": "환율및기초자료", "0202": "중기목록표", "0203": "중기사용료", "0301": "일위대가목록표", "0302": "일위대가표", "0401": "단가산출근거목록표", "0402": "단가산출근거", "0104": "일식견적목록표", "0106": "표준시장목록표", "0206": "실적단가목록표", "0501": "재료비수량금액집계표", "0502": "노무비수량금액집계표", "0503": "경비수량금액집계표", "0504": "일식견적수량금액집계표", "0505": "중기시간금액집계표", "0506": "일위대가수량금액집계표", "0507": "단가산출근거수량금액집계표", "0508": "표준시장수량금액집계표", "0601": "총괄내역서", "0603": "설계내역서", "0608": "공사원가계산서", }; return { buildBook, priceCompareSheet, tocSheet, guideSheet, ORDER, FORM_SHEET }; })(); if (typeof window !== "undefined") window.STMATE_REPORT_BOOK = StmateReportBook; if (typeof module !== "undefined" && module.exports) module.exports = StmateReportBook;