// 원본 수량 목록(TFRMMULLANGLIST — spec_pa §2·3)·수량붙여넣기(연결)(TFRMEST ActionMullangInput §8.2)·
// 품명·규격 지정(TFRMMULNAMEINPUT btnSetName·btnSetSize §4)·공종명칭 입력(TFRMMULTITLENAMEINPUT §5).
// 수량 목록: 왼쪽 모든 산출서(검색 = 명칭·규격에서 빈칸·|·괄호 뺀 부분일치, 전체보기) · 가운데 고른 산출서 수량 항목 · 오른쪽 원문.
// 선택수량복사: 고른 수량 항목을 앱 안에 담아 둠(클립보드 아님). 내역서 「수량붙여넣기(연결)」: 고른 줄부터 아래로 차례로
// 수량을 넣고 그 항목을 내역 줄에 연결(「품명,규격입력」 체크면 품명·규격·단위도). 행은 더하지 않음.
const EstQuantityList = (() => {
const norm = (s) => String(s ?? "").replace(/[\s|()]/g, "");
const RESULT_NAMES = { name: ["\x0e", "품명"], size: ["\x13", "규격"] };
function open(state) {
const l = (state.qList ||= { word: "", code: null, nameSize: false, picked: new Set() });
const { esc, number } = EstApp,
sheets = (state.project.quantitySheets || []).filter(
(s) => !l.word || norm(s.Name).includes(l.word) || norm(s.Size).includes(l.word),
),
sheet = state.project.quantitySheets?.find((s) => s.Code === l.code) || sheets[0];
l.code = sheet?.Code;
const items = sheet ? EstQuantity.calculate(sheet).items : [];
const preview = sheet
? sheet.rows
.map((r) =>
r.table
? `
${r.table.map((tr) => `${tr.map((c) => `| ${esc(c.text)} | `).join("")}
`).join("")}
`
: `${esc(EstText.clean(r.LineData || ""))}
`,
)
.join("")
: "";
EstApp.showDialog(
"수량 목록",
`${sheets.map((s) => `
`).join("")}
${preview}
`,
);
}
function paste(state) {
const clip = state.mulClip;
if (!clip?.items.length) throw Error("수량 목록에서 선택수량복사를 먼저 하세요.");
if (state.view !== "EstMainDat") throw Error("내역서에서 붙여넣을 줄을 고르세요.");
const rows = state.project.tables.EstMainDat,
start = [...state.selection].sort((a, b) => a - b)[0];
if (!Number.isInteger(start)) throw Error("내역서에서 붙여넣을 줄을 고르세요.");
EstApp.change((project) => {
const sheet = project.quantitySheets.find((s) => s.Code === clip.code);
clip.items.forEach((src, k) => {
const row = project.tables.EstMainDat[Math.min(start + k, rows.length - 1)],
item = sheet?.items.find((i) => i.Code === src.Code);
if (clip.nameSize) Object.assign(row, { Name: src.Name, Size: src.Size, Unite: src.Unite });
row.GN = Number(src.GN) || 0;
row.MullangIndex = 9999;
if (item && row.PkID != null)
Object.assign(item, { TargetKey: `id:${row.PkID}`, TargetCode: row.Code || "" });
});
});
state.notice = `수량 ${clip.items.length}개를 붙여넣고 연결했습니다.`;
}
// 품명·규격 지정: 산출 줄 글에서 고른 글자를 ⟦품명⟧·⟦규격⟧ 표지로(한 줄에 하나, 40자 이하, 제목 표지 위 금지)
function nameSize(state, kind, index) {
const sheet = state.project.quantitySheets?.find((s) => s.Code === state.quantitySheet),
editor = document.querySelector(`[data-quantity-line="${index}"]`);
if (!sheet?.rows[index] || !editor) throw Error("산출 줄을 고르세요.");
const [mark, label] = RESULT_NAMES[kind],
text = editor.value;
let a = editor.selectionStart ?? 0,
b = editor.selectionEnd ?? a;
if (a === b) {
while (a > 0 && /[^\s⟦⟧]/.test(text[a - 1])) a--;
while (b < text.length && /[^\s⟦⟧]/.test(text[b])) b++;
}
const picked = text.slice(a, b);
if (EstText.encode(text).includes(mark))
throw Error(`한라인에 ${label}을두번이상 지정할수 없습니다.`);
if (sheet.rows[index].title) throw Error("제목표시에는 지정할수 없습니다.");
if (picked.length > 40) throw Error("선택한 범위가 너무 큽니다.");
if (!picked.trim())
throw Error(
`${label}으로 지정할 문자를 드래그하여 선택하던지커서를 선택할위치로 옮겨 놓으세요.`,
);
const token = `⟦${label}⟧`;
EstApp.change(() => {
sheet.rows[index].LineData = EstText.encode(
text.slice(0, a) + token + picked + token + text.slice(b),
);
sheet.markerEdited = true;
}, false);
}
// 공종명칭 입력: 목록 명칭·규격을 바꾸고 문서 머리에 「공백5 + 명칭(규격) + 공백5」 제목 줄(굵게 큰 글자)을 넣거나 바꿈
function title(state) {
const sheet = state.project.quantitySheets?.find((s) => s.Code === state.quantitySheet);
if (!sheet) throw Error("수량산출서를 고르세요.");
const head = sheet.rows[0]?.title ? sheet.rows[0] : null;
const name = prompt("명칭", sheet.Name ?? "");
if (name == null) return;
const size = prompt("규격", sheet.Size ?? "") ?? "";
EstApp.change(() => {
sheet.Name = name.trim();
sheet.Size = size.trim();
const text = ` ${name.trim()}${size.trim() ? `(${size.trim()})` : ""} `;
if (head) head.LineData = text;
else
sheet.rows.unshift({ LineData: text, markers: [], title: true, para: { align: "center" } });
sheet.tableEdited = true;
}, false);
}
function install() {
document.addEventListener("click", (event) => {
const a = event.target.closest("[data-ql-sheet]");
if (!a) return;
event.preventDefault();
const l = EstApp.state.qList;
l.code = a.dataset.qlSheet;
l.picked = new Set();
open(EstApp.state);
});
document.addEventListener("change", (event) => {
const t = event.target,
l = EstApp.state.qList;
if (!l) return;
if (t.dataset.qlItem !== undefined) {
const k = Number(t.dataset.qlItem);
if (t.checked) l.picked.add(k);
else l.picked.delete(k);
} else if (t.dataset.qlNamesize !== undefined) l.nameSize = t.checked;
else if (t.dataset.qlWord !== undefined) {
l.word = norm(t.value);
open(EstApp.state);
}
});
}
if (typeof document !== "undefined" && document.addEventListener) install();
function handle(name, state, node) {
if (name === "quantity-list") open(state);
else if (name === "ql-search") {
state.qList.word = norm(document.querySelector("[data-ql-word]")?.value);
open(state);
} else if (name === "ql-all") {
state.qList.word = "";
open(state);
} else if (name === "ql-copy") {
const l = state.qList,
sheet = state.project.quantitySheets.find((s) => s.Code === l.code),
items = EstQuantity.calculate(sheet).items.filter((_, k) => l.picked.has(k));
if (!items.length) throw Error("복사할 수량 항목을 고르세요.");
state.mulClip = { code: sheet.Code, items, nameSize: l.nameSize };
state.notice = `수량 ${items.length}개를 담았습니다. 내역서에서 수량붙여넣기(연결)를 누르세요.`;
EstApp.render();
} else if (name === "mul-paste") paste(state);
else if (name === "quantity-name" || name === "quantity-size")
nameSize(state, name === "quantity-name" ? "name" : "size", Number(node.dataset.index));
else if (name === "quantity-title") title(state);
else return false;
return true;
}
return { handle };
})();