Files
Aislo/tools/est-test/native-quantity-list.js
T
eomsangdonandClaude Opus 5.5 892285843d feat(est-test): 워드형 수량산출서 표 보존과 편집 도구
- 가져오기가 WPT 표를 버리던 결함 고침: 표 줄(칸·수량 표지·수식·세로병합) 보존, 문서 읽기 native-quantity-wpt.cjs 로 분리
- 표 셀 합·수량 지정·병합/분리·표 만들기·표 모양, 문단·그림·쪽 삽입/삭제·품명/규격 지정·공종명칭 입력·테두리선·산출근거 양식
- 수량 불러오기·수량목록보기·선택수량복사·내역서 수량붙여넣기(연결)
- ESTX 저장은 표 줄을 건너 문단만 맞춤, 표·서식 편집은 거부

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 18:35:50 +09:00

158 lines
8.6 KiB
JavaScript

// 원본 수량 목록(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
? `<table class="sheet">${r.table.map((tr) => `<tr>${tr.map((c) => `<td>${esc(c.text)}</td>`).join("")}</tr>`).join("")}</table>`
: `<div style="white-space:pre-wrap">${esc(EstText.clean(r.LineData || ""))}</div>`,
)
.join("")
: "";
EstApp.showDialog(
"수량 목록",
`<div class="business-tools"><input data-ql-word value="${esc(l.word)}" placeholder="검색어"><button data-action="ql-search">검 색</button><button data-action="ql-all">전체보기</button><button data-action="ql-copy">선택수량복사</button><label><input type="checkbox" data-ql-namesize ${l.nameSize ? "checked" : ""}> 품명,규격입력</label></div><div style="display:flex;gap:8px;height:56vh"><div style="flex:0 0 26%;overflow:auto;border:1px solid #ccc">${sheets.map((s) => `<div><a href="#" data-ql-sheet="${esc(s.Code)}"${s.Code === l.code ? ' style="font-weight:bold"' : ""}>${esc(s.Name)} ${esc(s.Size || "")}</a></div>`).join("")}</div><div style="flex:0 0 34%;overflow:auto;border:1px solid #ccc"><table class="sheet"><thead><tr><th></th><th>명칭</th><th>규격</th><th>단위</th><th>수량</th></tr></thead><tbody>${items.map((i, k) => `<tr><td><input type="checkbox" data-ql-item="${k}" ${l.picked.has(k) ? "checked" : ""}></td><td>${esc(i.Name)}</td><td>${esc(i.Size)}</td><td>${esc(i.Unite)}</td><td>${number(i.GN)}</td></tr>`).join("")}</tbody></table></div><div style="flex:1;overflow:auto;border:1px solid #ccc;padding:4px;font-size:12px">${preview}</div></div>`,
);
}
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 };
})();