/* -*- coding: utf-8 -*- */ /** * 단가 보고서 (report_unit.js) — 목록표(0101~0103 · 0202 · 0301 · 0401) · 일위대가표(0302) · 중기사용료(0203) · 환율및기초자료(0201) * 근거: 지식DB STmate/old/20_분석 20 §2.4(코드 종류) · §3(SET_REP 서식) · 17(단가조립) · old/30_원자료/print_form_map.txt * 정답 대조: resources/tester/test_stmate_report_sheets.py (원본 STMATE 출력 엑셀 셀 값) */ const StmateReportUnit = (() => { const C = (typeof globalThis !== "undefined" && globalThis.STMATE_REPORT_CORE) || require("./report_core.js"); const { num, str, code, fix } = C; // 비목 열 — 목록표 E~H · 일위대가표 (E,F)~(K,L) 쌍 const LIST_COLS = ["E", "F", "G", "H"]; const PAIR_COLS = [ ["E", "F"], ["G", "H"], ["I", "J"], ["K", "L"], ]; function iniOf(dbf) { return (dbf && dbf.start_ini) || {}; } const slotOf = (r) => (num(r && r.NAGCD) > 0 ? 2 : 1); // 구성 조립 설정 — Start8x.Ini(CHBNM · OPTNM · CONLD · RXXNM · EXLNM1) function compOptions(dbf, parent) { const ini = iniOf(dbf); const p = code(parent); return { totalFirst: p[0] === "B" && String(ini.CHBNM ?? "1") === "0", order: ini.OPTNM || "SML", sbmok: str(parent.SBMOK).trim(), labor: p[0] === "X" ? new Set(String(ini.CONLD || "").match(/.{1,6}/g) || []) : null, rx: ini.RXXNM || "", usdRate: num(ini.EXLNM1), }; } function groupsOf(dbf) { const g = {}; for (const r of (dbf && dbf.bdqty_rows) || []) (g[r.BDCODE] = g[r.BDCODE] || []).push(r); return g; } // 코드 단가 조회 — 낙찰률 품목은 원단가(2칸)로 조립(지식DB 17 · smt_engine 과 같음) function lookupOf(idx) { return (c) => { const r = idx.by[c]; return r ? idx.price(c, slotOf(r)) : null; }; } /** * 출력 설정 — Start8x.Ini * SH_TYP1 = 0 이면 첫 칸 코드 · 그 밖 호표 번호 / SH_TYP3 = 1 이면 비고에 호표 · 그 밖 가격지 위치(BQCODE) * CH_PRN_COD = 1 이면 번호를 자리 맞춤(중기 「제 n호표」 · 자원 「 n」) * 줄 비고 종류 이름 = COD_FMT_1~6(노무·자재·경비·대가·중기·산근 · 지식DB 20 §2.4) */ function printOptions(ini) { const kind = {}; "LMSBXD".split("").forEach((p, i) => (kind[p] = String(ini[`COD_FMT_${i + 1}`] || ""))); return { codeMode: String(ini.SH_TYP1 ?? "0") === "0", remarkHopyo: String(ini.SH_TYP3 ?? "2") === "1", padNo: String(ini.CH_PRN_COD ?? "0") === "1", kind, }; } const pad = (n, w) => String(n).padStart(w); // 호표 비고 글 — 대가·산근 「대가 n호표」 · 그 밖 「노무 n」 const kindNo = (po, p, n) => p === "B" || p === "D" ? `${po.kind[p]}${pad(n, 4)}호표` : `${po.kind[p] || ""}${pad(n, 5)}`; // 목록 첫 칸 호표 글 — 일위대가·산근 「제 n호표」 · 중기·자원 번호 function hopyoLabel(po, prefix, n) { if (prefix === "B" || prefix === "D" || (prefix === "X" && po.padNo)) return `제${pad(n, 4)}호표`; return po.padNo ? pad(n, 4) : String(n); } // 목록표 — 코드 종류(M·L·S·B·D·X)의 COSTN 전수 · 코드순 function listSheet(ctx, name, title, prefix) { const { dbf, idx, ini, order, po } = ctx; const sh = C.newSheet(name, title, C.projectName(dbf), "J"); const head = { A: po.codeMode ? "코드번호" : "호표", B: "명 칭", C: "규 격" }; Object.assign(head, { D: "단위", I: "비 고", _k: "head" }); order.forEach((k, i) => (head[LIST_COLS[i]] = C.bimokLabel(ini, k))); C.push(sh, head); idx.ofPrefix(prefix).forEach((r, i) => { const c = code(r); const tot = ctx.totals[c] || idx.price(c, 1); const lbl = po.codeMode ? c : hopyoLabel(po, prefix, 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 }); const v = { M: tot.M, L: tot.L, S: tot.S, T: tot.M + tot.L + tot.S }; order.forEach((k, j) => (cells[LIST_COLS[j]] = v[k])); const rm = po.remarkHopyo && !po.codeMode ? lbl : str(r.BQCODE).trim(); if (rm) cells.I = rm; C.push(sh, cells); }); return sh; } // 자원 목록표(0101~0103) — 단가 한 칸 function resourceSheet(ctx, name, title, prefix) { const { dbf, idx, po } = ctx; const sh = C.newSheet(name, title, C.projectName(dbf), "G"); const head = { A: po.codeMode ? "코드번호" : "호표", B: "명 칭", C: "규 격" }; C.push(sh, Object.assign(head, { D: "단위", E: "단 가", F: "비 고", _k: "head" })); idx.ofPrefix(prefix).forEach((r, i) => { const c = code(r); const p = idx.price(c, 1); const lbl = po.codeMode ? c : hopyoLabel(po, prefix, i + 1); const cells = { A: lbl, B: str(r.NAME), C: str(r.SIZE), D: C.unitText(r.UNIT) }; Object.assign(cells, { E: fix(p.M + p.L + p.S), _k: "item", _code: c }); const rm = po.remarkHopyo && !po.codeMode ? lbl : str(r.BQCODE).trim(); if (rm) cells.F = rm; C.push(sh, cells); }); return sh; } // 줄 비고 — 코드(뒤 한 칸) · 호표(종류 이름 + 번호 5자리) · 가격지 위치 · 단가 0 이면 「별산」(SET_REP 0302 FLD_N3) function lineRemark(ctx, ref, c, pct) { const { po, hopyoNo } = ctx; const zero = !pct && ref && ref.M + ref.L + ref.S === 0 ? "별산" : ""; if (po.codeMode) return `${zero}${c} `; if (po.remarkHopyo) return `${zero}${kindNo(po, c[0], hopyoNo[c] || "")} `; return ref && ref.row ? str(ref.row.BQCODE).trim() : ""; } /** * 구성 호표 시트 — 일위대가표(B · 0302) · 중기사용료(X · 0203) * 호표 머리 → 구성 줄(단가·금액 쌍) → 합계(합계 줄 원 미만 절사 · 경비 지정이면 경비 한 칸) * 총액우선이면 합계 줄에 끝수 전 합(단가 칸)도 적음 · 글 줄(코드 없음)은 수량 0 빈 줄(명칭은 BDQTY 암호 칸) */ function compositionSheet(ctx, name, title, prefix) { const { dbf, idx, ini, order, po, hopyoNo } = ctx; const isB = prefix === "B"; const sh = C.newSheet(name, title, C.projectName(dbf), "N"); const h3 = { A: "명 칭", B: "규 격", C: "수 량", D: "단위", M: "비 고", _k: "head", }; const h4 = { _k: "head" }; order.forEach((k, i) => { h3[PAIR_COLS[i][0]] = C.bimokLabel(ini, k); h4[PAIR_COLS[i][0]] = "단 가"; h4[PAIR_COLS[i][1]] = "금 액"; }); C.push(sh, h3); C.push(sh, h4); let no = 0; for (const r of idx.ofPrefix(prefix)) { const c = code(r); no += 1; const wide = isB || po.padNo; C.push(sh, { A: wide ? ` 제${pad(no, 5)} 호표` : `제 ${no} 호표`, _k: "hopyo" }); const hd = { A: str(r.NAME), B: str(r.SIZE), D: C.unitText(r.UNIT), _k: "parent", _code: c }; const rm = po.codeMode ? c : po.remarkHopyo ? `${po.kind[prefix]}${pad(no, 5)}` : str(r.BQCODE).trim(); if (rm) hd.M = rm; C.push(sh, hd); const ev = ctx.evals[c]; if (!ev) continue; for (const ln of ev.lines) { if (ln.text) { C.push(sh, { C: 0, _k: "text", M: po.remarkHopyo && !po.codeMode ? " " : undefined, }); continue; } const ref = ln.ref; const cells = { C: ln.qty, D: ln.unit, _k: "line", _code: ln.code }; if (ref) Object.assign(cells, { A: ref.name, B: ref.spec, D: ref.unitDisp }); if (ln.pct) cells.D = "%"; const u = { M: ln.u.M, L: ln.u.L, S: ln.u.S }; u.T = fix(u.M + u.L + u.S); const a = { M: ln.a.M, L: ln.a.L, S: ln.a.S }; a.T = fix(a.M + a.L + a.S, 1); order.forEach((k, i) => { cells[PAIR_COLS[i][0]] = fix(u[k]); cells[PAIR_COLS[i][1]] = fix(a[k], 1); }); if (ln.code !== "PR") { const rm2 = lineRemark(ctx, ref, ln.code, ln.pct); if (rm2) cells.M = rm2; } C.push(sh, cells); } const tot = { A: ev.sbmokS ? "합 계(경비 로적용)" : "합 계", _k: "total", _code: c }; const raw = Object.assign({}, ev.raw, { T: fix(ev.raw.M + ev.raw.L + ev.raw.S, 4) }); const fin = Object.assign({}, ev.tot, { T: ev.T }); order.forEach((k, i) => { if (ev.totalFirst) tot[PAIR_COLS[i][0]] = fix(raw[k], 4); tot[PAIR_COLS[i][1]] = fin[k]; }); C.push(sh, tot); } return sh; } // 환율및기초자료(0201) — 환율 · 노임 시간 환산(CONLD · RXXNM) · 중기 구성에 쓰인 재료 function basicSheet(ctx) { const { dbf, idx, ini, groups, po, hopyoNo } = ctx; const rmk = (c) => (po.remarkHopyo && !po.codeMode ? kindNo(po, c[0], hopyoNo[c]) : c); const sh = { name: "환율및기초자료", title: "1. 환 율", rows: [], cols: {} }; C.put(sh, 1, { A: "1. 환 율", _k: "title" }); C.put(sh, 2, { C: "100,000$ 미만", D: "100,000$ 이상", E: "유로화(€)", G: "엔화(100¥)" }); sh.rows[1].I = "환율및기초자료"; const e3 = { C: num(ini.EXLNM1), D: num(ini.EXHNM1), E: num(ini.EXEUR1), G: num(ini.EXYEN1) }; C.put(sh, 3, e3); C.put(sh, 5, { A: "2. 인 건 비" }); const labor = String(ini.CONLD || "").match(/.{1,6}/g) || []; const rx = String(ini.RXXNM || ""); let r = 6; const lab = labor.filter((c) => idx.by[c]); for (const c of lab) { const p = idx.price(c, 1); const daily = p.M + p.L + p.S; let acc = daily; for (const [, op, n] of rx.replace(/\s+/g, "").matchAll(/([*/])?(\d+(?:\.\d+)?)/g)) acc = op === "/" ? acc / Number(n) : acc * Number(n); C.put(sh, r++, { C: p.name, D: daily, E: `* ${rx} =`, G: C.trunc(acc), H: rmk(c), _code: c, }); } r += 1; C.put(sh, r++, { A: "3. 단가 및 재료비" }); C.put(sh, r++, { B: "NO", C: "단 가 명", D: "규 격", E: "단위", F: "가 격", H: "비 고" }); // 재료 = 중기(X) 구성에 쓰인 재료비(M) 코드 · 코드순 const mats = new Set(); for (const [p, rows] of Object.entries(groups)) if (p[0] === "X") for (const g of rows) { const c = String(g.CODE).trim(); if (/^M/.test(c) && idx.by[c] && !/%$/.test(str(idx.by[c].UNIT).trim())) mats.add(c); } const items = lab.concat([...mats].filter((c) => idx.by[c]).sort()); items.forEach((c, i) => { const p = idx.price(c, 1); const cells = { B: String(i + 1), C: p.name, E: p.unitDisp, F: p.M + p.L + p.S, H: rmk(c), _code: c, }; if (p.spec.trim()) cells.D = p.spec; C.put(sh, r++, cells); }); return sh; } /** * 단가 맥락 — 구성 호표(B·X) 줄별 계산 결과와 목록표 합계 * 구성 없는 호표 = COSTN 단가 그대로 */ function unitContext(dbf) { const idx = C.costnIndex(dbf); const ini = iniOf(dbf); const order = C.bimokOrder(ini); const groups = groupsOf(dbf); const lookup = lookupOf(idx); const evals = {}, totals = {}; for (const p of ["X", "B"]) for (const r of idx.ofPrefix(p)) { const c = code(r); if (!groups[c]) continue; const ev = C.compositionLines(groups[c], lookup, compOptions(dbf, r)); evals[c] = ev; totals[c] = ev.tot; } // 호표 번호 = 종류별 코드순 자리(1부터) const hopyoNo = {}; for (const p of "LMSBXD") idx.ofPrefix(p).forEach((r, i) => (hopyoNo[code(r)] = i + 1)); const po = printOptions(ini); return { dbf, idx, ini, order, groups, evals, totals, hopyoNo, po }; } function buildUnitSheets(dbf) { const ctx = unitContext(dbf); const F = (sh, indx) => C.applyForm(sh, dbf, indx); return { ctx, 일위대가목록표: F(listSheet(ctx, "일위대가목록표", "일위대가 목록표", "B"), "0301"), 일위대가표: F(compositionSheet(ctx, "일위대가표", "일 위 대 가 표", "B"), "0302"), 단가산출근거목록표: F( listSheet(ctx, "단가산출근거목록표", "단가산출근거 목록표", "D"), "0401", ), 환율및기초자료: basicSheet(ctx), 중기목록표: F(listSheet(ctx, "중기목록표", "중기 목록표", "X"), "0202"), 중기사용료: F(compositionSheet(ctx, "중기사용료", "중 기 사 용 료", "X"), "0203"), 재료비목록표: F(resourceSheet(ctx, "재료비목록표", "재료비 목록표", "M"), "0101"), 노무비목록표: F(resourceSheet(ctx, "노무비목록표", "노무비 목록표", "L"), "0102"), 경비목록표: F(resourceSheet(ctx, "경비목록표", "경비 목록표", "S"), "0103"), }; } return { buildUnitSheets, unitContext, compOptions, LIST_COLS, PAIR_COLS }; })(); if (typeof window !== "undefined") window.STMATE_REPORT_UNIT = StmateReportUnit; if (typeof module !== "undefined" && module.exports) module.exports = StmateReportUnit;