- 가져오기가 WPT 표를 버리던 결함 고침: 표 줄(칸·수량 표지·수식·세로병합) 보존, 문서 읽기 native-quantity-wpt.cjs 로 분리 - 표 셀 합·수량 지정·병합/분리·표 만들기·표 모양, 문단·그림·쪽 삽입/삭제·품명/규격 지정·공종명칭 입력·테두리선·산출근거 양식 - 수량 불러오기·수량목록보기·선택수량복사·내역서 수량붙여넣기(연결) - ESTX 저장은 표 줄을 건너 문단만 맞춤, 표·서식 편집은 거부 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
158 lines
8.6 KiB
JavaScript
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 };
|
|
})();
|