526 lines
21 KiB
JavaScript
526 lines
21 KiB
JavaScript
/* -*- coding: utf-8 -*- */
|
|
/**
|
|
* 보고서 시트 잇기 (report_link.js) — 원본 STMATE 엑셀의 시트 간 참조 수식 · 바로가기(→) · 시트 이름표 수식
|
|
* 근거: 지식DB STmate/old/20_분석 24 §3(ST_P_XLS Prn_FMLA_INFO) · 19 §3(목차·안내 시트) ·
|
|
* 원본 출력 엑셀 5건 수식 실측(봉화·영월·대흥·울진·영덕 — openpyxl 수식 읽기)
|
|
* 정답 대조: resources/tester/test_stmate_report_link.py(원본 수식 글 그대로)
|
|
* 시트 모형 행에 붙이는 것:
|
|
* _f {열: 값} — 수식 넣기 때만 쓰는 칸(수식 · 숨은 열쇠 글) · 값 칸은 계산 엔진 값 그대로
|
|
* _links {열: '시트!칸'} — 값 모드 바로가기(하이퍼링크)
|
|
*/
|
|
const StmateReportLink = (() => {
|
|
const TOC = "〓 목 차 〓";
|
|
const GUIDE = "※※안내※※";
|
|
const INIT = "〓 INITIAL 〓";
|
|
const FILE_NAME =
|
|
'=MID(CELL("filename",$A$1),FIND("]",CELL("filename",$A$1))+1,LEN(CELL("filename",$A$1)))';
|
|
// 시트 이름 — 한글·영문·숫자만이면 따옴표 없이(원본 꼴)
|
|
const q = (n) =>
|
|
/^[가-힣A-Za-z_][가-힣A-Za-z0-9_]*$/.test(n) ? n : `'${n.replace(/'/g, "''")}'`;
|
|
const colNum = (c) => c.split("").reduce((a, ch) => a * 26 + ch.charCodeAt(0) - 64, 0);
|
|
const colName = (n) =>
|
|
(n > 26 ? String.fromCharCode(64 + Math.floor((n - 1) / 26)) : "") +
|
|
String.fromCharCode(65 + ((n - 1) % 26));
|
|
const isCol = (k) => /^[A-Z]{1,2}$/.test(k);
|
|
|
|
// 시트 이름표 칸 — 2행에서 시트 이름이 적힌 열
|
|
const tagOf = (sh) =>
|
|
sh.rows[1] ? Object.keys(sh.rows[1]).find((k) => isCol(k) && sh.rows[1][k] === sh.name) : null;
|
|
// 목차 바로가기 열 — 숨은 도움 열이 있는 시트는 그 뒤(원본 실측) · 그 밖 이름표 다음 열
|
|
const LINK_COL = {
|
|
총괄내역서: "Q",
|
|
설계내역서: "AC",
|
|
일위대가표: "Z",
|
|
중기사용료: "Z",
|
|
단가산출근거: "Y",
|
|
};
|
|
const NO_LINK = new Set([TOC, GUIDE, INIT, "환율및기초자료"]);
|
|
function linkCol(sh) {
|
|
const id = sh.id || sh.name;
|
|
if (LINK_COL[id]) return LINK_COL[id];
|
|
const t = tagOf(sh);
|
|
return t ? colName(colNum(t) + 1) : null;
|
|
}
|
|
const headRow = (sh) => {
|
|
let h = 0;
|
|
sh.rows.slice(0, 6).forEach((r, i) => r._k === "head" && (h = i + 1));
|
|
return h || 3;
|
|
};
|
|
|
|
const setF = (row, col, v) => ((row._f = row._f || {}), (row._f[col] = v));
|
|
const setLink = (row, col, target, text) => {
|
|
row[col] = text;
|
|
row._links = Object.assign(row._links || {}, { [col]: target });
|
|
};
|
|
|
|
/**
|
|
* 바로가기 수식 — =HYPERLINK("#"&시트!이름표2&"!열"&ROW(시트!열행),"글")
|
|
* 이름표 칸 수식이 시트 이름을 돌려줘 시트 이름을 바꿔도 따라감(원본 꼴)
|
|
*/
|
|
function jump(target, col, row, text) {
|
|
const n = q(target.name);
|
|
return `=HYPERLINK("#"&${n}!${tagOf(target)}2&"!${col}"&ROW(${n}!${col}${row}),"${text}")`;
|
|
}
|
|
/**
|
|
* 바로가기 글 — 코드 · 호표 비고 모드(SH_TYP1=1 · SH_TYP3=1)면 종류 이름 + 목록 차례 5자리(「자재 14」 · 대흥 실측)
|
|
* 종류 이름 = Start8x.Ini COD_FMT_1~6(노무·자재·경비·대가·중기·산근)
|
|
*/
|
|
function codeLabel(by, c) {
|
|
const ini = (by._dbf && by._dbf.start_ini) || {};
|
|
const hop = String(ini.SH_TYP1 ?? "0") !== "0" && String(ini.SH_TYP3 ?? "2") === "1";
|
|
const at = "LMSBXD".indexOf(c[0]);
|
|
if (!hop || at < 0) return `${c} →`;
|
|
const list = by[LIST_OF[c[0]]];
|
|
const n = list
|
|
? list.rows.filter((r) => r._k === "item").findIndex((r) => r._code === c) + 1
|
|
: 0;
|
|
return n > 0 ? `${String(ini[`COD_FMT_${at + 1}`] || "")}${String(n).padStart(5)} →` : `${c} →`;
|
|
}
|
|
function putJump(row, at, target, col, r, text) {
|
|
if (!at || !target || !tagOf(target)) return;
|
|
setLink(row, at, `${q(target.name)}!${col}${r}`, text);
|
|
setF(row, at, jump(target, col, r, text));
|
|
}
|
|
|
|
// 시트 안 코드 → 행 번호(1부터) — kind 가 같은 행만
|
|
function rowsBy(sh, kind) {
|
|
const m = {};
|
|
if (!sh) return m;
|
|
sh.rows.forEach((r, i) => {
|
|
if (r._code && (!kind || r._k === kind) && !(r._code in m)) m[r._code] = i + 1;
|
|
});
|
|
return m;
|
|
}
|
|
|
|
// 머리 행의 비목 열 — 이름 → 열(합계·노무·재료·경비) · BMKNM 차례로 쓴 머리 글에서 찾음
|
|
function bimokCols(ini, head, cols) {
|
|
const C = globalThis.STMATE_REPORT_CORE || require("./report_core.js");
|
|
const out = {};
|
|
for (const k of "TLMS") {
|
|
const lbl = C.bimokLabel(ini, k);
|
|
const c = cols.find((x) => head[x] === lbl);
|
|
if (c) out[k] = c;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
// 코드 → COSTN 단위(M%·L%·…) · 규격
|
|
function costnMap(by, key, field) {
|
|
if (!by[key]) {
|
|
by[key] = {};
|
|
for (const r of (by._dbf && by._dbf.costn_rows) || [])
|
|
by[key][String(r.CODE || "").trim()] = String(r[field] || "").trim();
|
|
}
|
|
return by[key];
|
|
}
|
|
const unitOf = (by) => costnMap(by, "_unit", "UNIT");
|
|
const specOf = (by) => costnMap(by, "_spec", "SIZE");
|
|
|
|
// 코드 머리 → 목록 시트 id
|
|
const LIST_OF = {
|
|
M: "재료비목록표",
|
|
L: "노무비목록표",
|
|
S: "경비목록표",
|
|
B: "일위대가목록표",
|
|
D: "단가산출근거목록표",
|
|
X: "중기목록표",
|
|
W: "일식견적목록표",
|
|
};
|
|
const DETAIL_OF = {
|
|
일위대가목록표: ["일위대가표", "parent"],
|
|
중기목록표: ["중기사용료", "parent"],
|
|
단가산출근거목록표: ["단가산출근거", "hopyo"],
|
|
};
|
|
const COLL_OF = {
|
|
재료비수량금액집계표: "재료비목록표",
|
|
노무비수량금액집계표: "노무비목록표",
|
|
경비수량금액집계표: "경비목록표",
|
|
중기시간금액집계표: "중기목록표",
|
|
일위대가수량금액집계표: "일위대가목록표",
|
|
단가산출근거수량금액집계표: "단가산출근거목록표",
|
|
일식견적수량금액집계표: "일식견적목록표",
|
|
};
|
|
|
|
/**
|
|
* 목록 층 — 재료비목록표 단가 = 자재단가대비표 적용 단가 · 대가·중기·산근 목록 = 호표 합계 줄 ·
|
|
* 수량금액집계표 단가 = 목록 단가 · 비목 집계(중기·대가·산근) = ROUND(수량 × 목록 비목 단가)
|
|
*/
|
|
function linkLists(by, ini) {
|
|
const pc = by.자재단가대비표;
|
|
const mat = by.재료비목록표;
|
|
if (pc && mat) {
|
|
const at = rowsBy(pc, "item");
|
|
const lc = linkCol(mat);
|
|
mat.rows.forEach((r) => {
|
|
const pr = r._k === "item" && at[r._code];
|
|
if (!pr) return;
|
|
setF(r, "E", `=${q(pc.name)}!O${pr}`);
|
|
putJump(r, lc, pc, "A", pr, codeLabel(by, r._code));
|
|
});
|
|
}
|
|
for (const [lid, [did, kind]] of Object.entries(DETAIL_OF)) {
|
|
const list = by[lid];
|
|
const det = by[did];
|
|
if (!list || !det) continue;
|
|
const tot = rowsBy(det, "total");
|
|
const top = rowsBy(det, kind);
|
|
const lcols = bimokCols(ini, list.rows[2] || {}, ["E", "F", "G", "H"]);
|
|
const dcols =
|
|
did === "단가산출근거"
|
|
? bimokCols(ini, det.rows[2] || {}, ["C", "D", "E", "F"])
|
|
: bimokCols(ini, det.rows[2] || {}, ["E", "G", "I", "K"]);
|
|
const lc = "K";
|
|
list.rows.forEach((r) => {
|
|
if (r._k !== "item" || !tot[r._code]) return;
|
|
for (const k of "TLMS") {
|
|
if (!lcols[k] || !dcols[k]) continue;
|
|
const dc = did === "단가산출근거" ? dcols[k] : colName(colNum(dcols[k]) + 1);
|
|
setF(r, lcols[k], `=${q(det.name)}!${dc}${tot[r._code]}`);
|
|
}
|
|
const hr = kind === "parent" ? top[r._code] - 1 : top[r._code];
|
|
if (hr > 0) putJump(r, lc, det, "A", hr, codeLabel(by, r._code));
|
|
});
|
|
}
|
|
for (const [cid, lid] of Object.entries(COLL_OF)) {
|
|
const sh = by[cid];
|
|
const list = by[lid];
|
|
if (!sh || !list) continue;
|
|
const at = rowsBy(list, "item");
|
|
const lc = linkCol(sh);
|
|
const head = sh.rows.slice(0, 4).find((r) => r._k === "head") || {};
|
|
const ccols = bimokCols(ini, head, ["F", "G", "H", "I"]);
|
|
const lcols = bimokCols(ini, list.rows[2] || {}, ["E", "F", "G", "H"]);
|
|
const single = !ccols.L; // 자원 집계 — 단가 한 칸
|
|
sh.rows.forEach((r) => {
|
|
const lr = r._k === "item" && at[r._code];
|
|
if (!lr) return;
|
|
if (single) setF(r, "F", `=${q(list.name)}!E${lr}`);
|
|
else {
|
|
setF(r, ccols.T, `=${ccols.M}{r}+${ccols.L}{r}+${ccols.S}{r}`);
|
|
for (const k of "LMS")
|
|
setF(r, ccols[k], `=ROUND(D{r}*${q(list.name)}!${lcols[k]}${lr},0)`);
|
|
}
|
|
putJump(r, lc, list, "A", lr, codeLabel(by, r._code));
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 구성 호표(일위대가표 · 중기사용료) — 줄 단가 = 목록 단가(노임 시간 환산은 환율및기초자료) ·
|
|
* 줄 금액 = IF(수량=0,0,ROUNDDOWN(단가×수량,1)) · % 단위 = ÷100 · 합계 = 같은 열쇠 줄 SUMIF 원 미만 버림
|
|
* 총액우선(일위대가 · CHBNM=0) = 끝수 배분 도움 열 S~Y(소수 셋째 끝수 큰 비목부터 · 같으면 OPTNM 차례)
|
|
* 숨은 열쇠 — O = 코드_1(줄) · T7_1(합계) · P = T7_1(줄이 속한 합계)
|
|
*/
|
|
function linkComposition(by, sh, ini) {
|
|
if (!sh) return;
|
|
const PAIR = { T: ["E", "F"], L: ["G", "H"], M: ["I", "J"], S: ["K", "L"] };
|
|
const C = globalThis.STMATE_REPORT_CORE || require("./report_core.js");
|
|
const order = C.bimokOrder(ini);
|
|
["E", "G", "I", "K"].forEach((u, i) => (PAIR[order[i]] = [u, colName(colNum(u) + 1)]));
|
|
const tf = (sh.id || sh.name) === "일위대가표" && String(ini.CHBNM ?? "1") === "0";
|
|
const opt = String(ini.OPTNM || "SML")
|
|
.toUpperCase()
|
|
.split("")
|
|
.filter((k) => "MLS".includes(k));
|
|
const nonT = order.filter((k) => k !== "T"); // 도움 열 S·T·U 차례 = 단가 열 차례
|
|
const help = {};
|
|
nonT.forEach((k, i) => (help[k] = { frac: "STU"[i], adj: "WXY"[i] }));
|
|
const lab = by.환율및기초자료 ? rowsBy(by.환율및기초자료) : {};
|
|
const rowsOf = {};
|
|
const sum3 = (side) => `=${PAIR.M[side]}{r}+${PAIR.L[side]}{r}+${PAIR.S[side]}{r}`;
|
|
const helpers = (row, base, d, step) => {
|
|
for (const k of nonT)
|
|
setF(
|
|
row,
|
|
help[k].frac,
|
|
`=ROUND(ROUNDDOWN(${base(PAIR[k][0])},${d + 2})-ROUNDDOWN(${base(PAIR[k][0])},${d}),${d + 2})`,
|
|
);
|
|
const parts = ["M", "L", "S"].map((k) => `ROUNDDOWN(${base(PAIR[k][0])},${d})`).join("+");
|
|
setF(
|
|
row,
|
|
"V",
|
|
`=ROUNDDOWN(ROUND(ROUNDDOWN(${base(PAIR.T[0])},${d})-(${parts}),${d + 2}),${d})`,
|
|
);
|
|
for (const k of nonT) {
|
|
const me = help[k].frac;
|
|
const ahead = opt
|
|
.slice(0, opt.indexOf(k))
|
|
.map((j) => `+IF(${help[j].frac}{r}=${me}{r},1,0)`)
|
|
.join("");
|
|
setF(
|
|
row,
|
|
help[k].adj,
|
|
`=IF(V{r}<0,-1,1)*IF(COUNTIF(R{r}:U{r},IF(V{r}<0,"<",">")&${me}{r})${ahead}<ROUND(ABS(V{r})/${step},0),${step},0)`,
|
|
);
|
|
}
|
|
};
|
|
const isJ = (sh.id || sh.name) === "중기사용료";
|
|
const v25 = Number(ini.PRG_V) >= 25;
|
|
// 중기 % 코드 줄 중 규격 「주연료의 %」(잡품·잡유) = 앞 연료 줄 금액을 밑수 수식으로(대흥 「=J9」 실측) ·
|
|
// 그 밖 % 코드(재료비의 % 등)는 산식 없음(안내 시트 알림)
|
|
const fuelPct = (c) => /주연료/.test(specOf(by)[c] || "");
|
|
const amounts = (r, pct) => {
|
|
setF(r, PAIR.T[0], sum3(0));
|
|
setF(r, PAIR.T[1], sum3(1));
|
|
for (const k of nonT) {
|
|
const [u, a] = PAIR[k];
|
|
const core = `ROUNDDOWN(${u}{r}*C{r}${pct ? "/100" : ""},1)`;
|
|
setF(r, a, `=IF(C{r}=0,0,${core}${tf ? `+${help[k].adj}{r}` : ""})`);
|
|
}
|
|
if (tf) helpers(r, (u) => `${u}{r}*C{r}`, 1, 0.1);
|
|
};
|
|
// 비율 줄 밑수 — 표시 줄(Q=1_01)의 밑수 비목 금액 합 · 25 판부터 칸 더하기(울진 실측)
|
|
const prBase = (r, rn, a, marks, inK, outK) => {
|
|
const inA = PAIR[inK === "T" ? "M" : inK][1];
|
|
const out = PAIR[outK === "T" ? "M" : outK][0];
|
|
if (!marks.length) return;
|
|
if (v25) setF(r, out, "=" + marks.map((m) => `${inA}${m}`).join("+"));
|
|
else setF(r, out, `=SUMIF(Q${a}:Q${rn - 1},"1_01",${inA}${a}:${inA}${rn - 1})`);
|
|
};
|
|
let first = 0;
|
|
let marks = [];
|
|
const lc = linkCol(sh);
|
|
sh.rows.forEach((r, i) => {
|
|
const rn = i + 1;
|
|
if (r._k === "parent") {
|
|
setF(r, "O", r._code);
|
|
first = rn + 1;
|
|
marks = [];
|
|
return;
|
|
}
|
|
if (r._k === "text" && first) {
|
|
// 글 줄 — 합계 밖 열쇠
|
|
setF(r, "O", "_1");
|
|
setF(r, "P", "_1");
|
|
return;
|
|
}
|
|
if (r._k === "line" && r._code === "PR" && r._pr) {
|
|
setF(r, "O", "PRA01_1");
|
|
setF(r, "P", "T7_1");
|
|
amounts(r, true);
|
|
prBase(r, rn, first, marks, r._pr.in, r._pr.out);
|
|
if (r._mark) {
|
|
setF(r, "Q", "1_01");
|
|
marks.push(rn);
|
|
}
|
|
return;
|
|
}
|
|
if (r._k === "line" && r._code && r._code !== "PR") {
|
|
const c = r._code;
|
|
const pct = r.D === "%";
|
|
if (r._mark) {
|
|
setF(r, "Q", "1_01");
|
|
marks.push(rn);
|
|
}
|
|
setF(r, "O", `${c}_1`);
|
|
if (pct && isJ && fuelPct(c)) {
|
|
// 표준 구성 % 줄 — 밑수 = 앞 줄 중 그 비목 금액이 있는 칸 더하기(대흥 「=J9」 실측)
|
|
const k = (unitOf(by)[c] || "M%")[0];
|
|
const kk = k === "T" ? "M" : k;
|
|
const mk = [];
|
|
for (let j = first; j < rn; j++) {
|
|
const x = sh.rows[j - 1];
|
|
if (x._k === "line" && Number(x[PAIR[kk][1]]) !== 0) mk.push(`${PAIR[kk][1]}${j}`);
|
|
}
|
|
if (mk.length) setF(r, PAIR[kk][0], "=" + mk.join("+"));
|
|
}
|
|
setF(r, "P", "T7_1");
|
|
amounts(r, pct);
|
|
const labor = c[0] === "L" && lab[c] && isJ;
|
|
const list = labor ? by.환율및기초자료 : by[LIST_OF[c[0]]];
|
|
if (!list) return;
|
|
const lr = labor
|
|
? lab[c]
|
|
: (rowsOf[list.name] = rowsOf[list.name] || rowsBy(list, "item"))[c];
|
|
if (!lr) return;
|
|
if (!pct) {
|
|
if (labor) setF(r, PAIR.L[0], `=${q(list.name)}!G${lr}`);
|
|
else if ("MLS".includes(c[0])) setF(r, PAIR[c[0]][0], `=${q(list.name)}!E${lr}`);
|
|
else {
|
|
const lcols = bimokCols(ini, list.rows[2] || {}, ["E", "F", "G", "H"]);
|
|
for (const k of nonT) setF(r, PAIR[k][0], `=${q(list.name)}!${lcols[k]}${lr}`);
|
|
}
|
|
}
|
|
putJump(r, lc, list, "A", lr, codeLabel(by, c));
|
|
return;
|
|
}
|
|
if (r._k === "total" && first) {
|
|
const a = first;
|
|
const b = rn - 1;
|
|
const toS = /경비/.test(String(r.A || "")); // 결과적용 비목 경비(SBMOK=S) — 세 비목 합을 경비 칸에
|
|
setF(r, "O", "T7_1");
|
|
const sumif = (col) => `SUMIF(P${a}:P${b},O{r},${col}${a}:${col}${b})`;
|
|
const fin = (k) => `ROUNDDOWN(${sumif(PAIR[k][1])}${tf ? `+${help[k].adj}{r}` : ""},0)`;
|
|
setF(r, PAIR.T[1], sum3(1));
|
|
for (const k of nonT) {
|
|
const [u, am] = PAIR[k];
|
|
if (tf) setF(r, u, `=${sumif(am)}`);
|
|
if (!toS) setF(r, am, `=${fin(k)}`);
|
|
}
|
|
if (toS) setF(r, PAIR.S[1], "=" + ["M", "L", "S"].map(fin).join("+"));
|
|
if (tf) {
|
|
setF(r, PAIR.T[0], sum3(0));
|
|
helpers(r, (u) => `${u}{r}`, 0, 1);
|
|
}
|
|
first = 0;
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 설계내역서 — 묶음 금액 = SUMIF(아래 행 Q 열쇠 = 이 묶음 P 열쇠) · 품목 단가 = 목록 단가 · 금액 = ROUNDDOWN(단가×수량,0)
|
|
* 열쇠 — 묶음 P = E단_1(제외 구역 C단_1 · 윗 묶음에 안 더함) · 품목 P = 코드_1(비율 줄 PR_1) · Q = 윗 묶음 열쇠 ·
|
|
* 빈 줄 P = CMT_1 · 비율 줄 밑수 표시 R = 1_2_01 · 끝 줄 = 맨 위 열쇠(E10_1) 합
|
|
*/
|
|
function linkDesign(by, ini) {
|
|
const sh = by.설계내역서;
|
|
if (!sh) return;
|
|
const C = globalThis.STMATE_REPORT_CORE || require("./report_core.js");
|
|
const PAIR = {};
|
|
const order = C.bimokOrder(ini);
|
|
["F", "H", "J", "L"].forEach((u, i) => (PAIR[order[i]] = [u, colName(colNum(u) + 1)]));
|
|
const nonT = order.filter((k) => k !== "T");
|
|
const sum3 = (side) => `=${PAIR.M[side]}{r}+${PAIR.L[side]}{r}+${PAIR.S[side]}{r}`;
|
|
const lc = linkCol(sh);
|
|
const rowsOf = {};
|
|
let first = 0;
|
|
let grpStart = 0;
|
|
let last = 0;
|
|
sh.rows.forEach((r, i) => {
|
|
const rn = i + 1;
|
|
if (/^T\d$/.test(r._k || "")) {
|
|
if (!first) first = rn;
|
|
grpStart = rn + 1;
|
|
last = rn;
|
|
setF(r, "P", `${r._ex ? "C" : "E"}${r._lv}_1`);
|
|
if (!r._ex && r._pk) setF(r, "Q", r._pk);
|
|
const end = r._end > rn ? r._end : rn;
|
|
for (const k of order) {
|
|
const am = PAIR[k][1];
|
|
if (end > rn) setF(r, am, `=SUMIF(Q${rn + 1}:Q${end},P{r},${am}${rn + 1}:${am}${end})`);
|
|
}
|
|
return;
|
|
}
|
|
if (r._k === "blank") {
|
|
setF(r, "P", "CMT_1");
|
|
if (r._pk) setF(r, "Q", r._pk);
|
|
last = rn;
|
|
return;
|
|
}
|
|
if (r._k !== "item") return;
|
|
last = rn;
|
|
const c = r._code;
|
|
setF(r, "P", c === "PR" ? "PR_1" : `${c}_1`);
|
|
setF(r, "Q", r._pk);
|
|
if (r._mark) setF(r, "R", "1_2_01");
|
|
const pct = r.E === "%";
|
|
if (pct) {
|
|
setF(r, PAIR.T[0], sum3(0));
|
|
setF(r, PAIR.T[1], sum3(1));
|
|
for (const k of nonT) setF(r, PAIR[k][1], `=ROUNDDOWN(${PAIR[k][0]}{r}*D{r}/100,0)`);
|
|
}
|
|
if (c === "PR" && r._pr) {
|
|
const inA = PAIR[r._pr.in === "T" ? "M" : r._pr.in][1];
|
|
const out = PAIR[r._pr.out === "T" ? "M" : r._pr.out][0];
|
|
if (grpStart < rn)
|
|
setF(
|
|
r,
|
|
out,
|
|
`=SUMIF(R${grpStart}:R${rn - 1},"1_2_01",${inA}${grpStart}:${inA}${rn - 1})`,
|
|
);
|
|
return;
|
|
}
|
|
const list = by[LIST_OF[c[0]]];
|
|
if (!list) return;
|
|
const lr = (rowsOf[list.name] = rowsOf[list.name] || rowsBy(list, "item"))[c];
|
|
if (!lr) return;
|
|
if (!pct) {
|
|
if ("MLS".includes(c[0])) setF(r, PAIR[c[0]][0], `=${q(list.name)}!E${lr}`);
|
|
else {
|
|
const lcols = bimokCols(ini, list.rows[2] || {}, ["E", "F", "G", "H"]);
|
|
for (const k of nonT)
|
|
if (lcols[k]) setF(r, PAIR[k][0], `=${q(list.name)}!${lcols[k]}${lr}`);
|
|
}
|
|
}
|
|
putJump(r, lc, list, "A", lr, codeLabel(by, c));
|
|
});
|
|
const fin = sh.rows.findIndex((r) => r._k === "final");
|
|
if (fin >= 0 && first)
|
|
for (const k of order) {
|
|
const am = PAIR[k][1];
|
|
setF(sh.rows[fin], am, `=SUMIF(Q${first}:Q${last},"E10_1",${am}${first}:${am}${last})`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 총괄내역서 공종 줄 · 순공사비 = 설계내역서 묶음 줄(끝 줄) 금액 참조 + 「내역 →」 바로가기
|
|
*/
|
|
function linkSummary(by, ini) {
|
|
const sm = by.총괄내역서;
|
|
const ds = by.설계내역서;
|
|
if (!sm || !ds) return;
|
|
const C = globalThis.STMATE_REPORT_CORE || require("./report_core.js");
|
|
const order = C.bimokOrder(ini);
|
|
const dcol = {};
|
|
["G", "I", "K", "M"].forEach((c, i) => (dcol[order[i]] = c));
|
|
const scol = {};
|
|
["D", "E", "F", "G"].forEach((c, i) => (scol[order[i]] = c));
|
|
// 설계 묶음 줄 — 같은 단·공종 번호·명칭이 차례로 나오는 행을 맞춤
|
|
const groups = ds.rows.map((r, i) => [r, i + 1]).filter(([r]) => /^T\d$/.test(r._k || ""));
|
|
let gi = 0;
|
|
const fin = ds.rows.findIndex((r) => r._k === "final") + 1;
|
|
const lc = linkCol(sm);
|
|
sm.rows.forEach((r) => {
|
|
let target = 0;
|
|
if (/^T\d$/.test(r._k || "")) {
|
|
while (gi < groups.length) {
|
|
const [g, rn] = groups[gi++];
|
|
if (g._k === r._k && g.A === r.A && g.B === r.B) {
|
|
target = rn;
|
|
break;
|
|
}
|
|
}
|
|
} else if (r._k === "sub" && /가\./.test(String(r.A || "")) && fin > 0) target = fin;
|
|
if (!target) return;
|
|
for (const k of order) setF(r, scol[k], `=${q(ds.name)}!${dcol[k]}${target}`);
|
|
putJump(r, lc, ds, "A", target, "내역 →");
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 시트마다 — 이름표 칸 수식(파일 안 시트 이름) · 목차 바로가기(머리 끝 행 · 바로가기 열)
|
|
*/
|
|
function linkFrame(sheets) {
|
|
for (const sh of sheets) {
|
|
if (NO_LINK.has(sh.name) && sh.name !== "환율및기초자료") continue;
|
|
const tag = tagOf(sh);
|
|
if (tag) setF(sh.rows[1], tag, FILE_NAME);
|
|
if (NO_LINK.has(sh.name)) continue;
|
|
const lc = linkCol(sh);
|
|
if (!lc) continue;
|
|
const row = sh.rows[headRow(sh) - 1];
|
|
setLink(row, lc, `${q(TOC)}!B2`, "목차 →");
|
|
setF(row, lc, `=HYPERLINK("#${q(TOC)}!B2","목차 →")`);
|
|
}
|
|
}
|
|
|
|
// 묶음 전체 잇기 — 시트 id 로 찾음(이름은 SET_REP 제목에 따라 바뀜)
|
|
function linkBook(sheets, dbf) {
|
|
const ini = (dbf && dbf.start_ini) || {};
|
|
const by = { _dbf: dbf };
|
|
for (const s of sheets) by[s.id || s.name] = s;
|
|
linkFrame(sheets);
|
|
linkLists(by, ini);
|
|
linkComposition(by, by.일위대가표, ini);
|
|
linkComposition(by, by.중기사용료, ini);
|
|
linkDesign(by, ini);
|
|
linkSummary(by, ini);
|
|
return sheets;
|
|
}
|
|
|
|
return { linkBook, specOf, jump, tagOf, linkCol, headRow, q, colName, colNum, TOC, GUIDE, INIT };
|
|
})();
|
|
|
|
if (typeof window !== "undefined") window.STMATE_REPORT_LINK = StmateReportLink;
|
|
if (typeof module !== "undefined" && module.exports) module.exports = StmateReportLink;
|