Files
Aislo/tools/stmate-test/scr_qty_list.js
T
eomsangdonandClaude Opus 5.5 3d8fddfc34 feat(stmate-test): 보고서·제잡비·식언어·수량산출·화면 묶음 합침 · 재계산 후 저장 무변경
- 보고서: 원본 STMATE 엑셀과 셀 대조(목록표·자재단가대비·중기·일위대가·산근 목록·설계내역서·원가계산서) · 조달청 XML 공식 XSD 검증 · PM_Prt 화면
- 제잡비: 원본 계산 순서 4형식(T/S/TA/SA) · 실무 7건 저장 결과키 전수 1원 · 요율표 조회 · 선택지 계산 · 제잡비·제비율 화면
- 식언어·수량: 함수·조건식·금액변수·문법 검사·수량변수·QCODE · 산근·구성·중기공식 편집 화면 · 순환 알림
- 화면: 원본 메뉴 체계·단축키 · 설계내역서 편집(행 넣기·지우기·이동·레벨·보관함) · 초기값 · 단가목록표
- 저장: 초기값(Start8x.Ini) 바뀐 키 저장 · 노임 환산 직종 = 초기값 CONLD · 산근·구성 결과에 적용단가 칸 채우지 않음(원본 0)
- DBF 글 칸 앞 빈칸 보존 · 시험 도우미 표준입력 utf8(한글 조각 깨짐 원인)
- 실무 5건 재계산 후 저장 = 바뀐 칸 0 · 시험 332건 통과

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 19:12:04 +09:00

498 lines
24 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* -*- coding: utf-8 -*- */
/**
* 수량산출서 화면 (scr_qty_list.js) — wQtyList
* 탭1 수량산출서 목록표(기본 수량변수 · QCODE) · 탭2 수량산출서 보기(확장 수량변수 · 셀별명 · 내역서로 가져가기) ·
* 탭3 코드 종류별 수량(9종) · 내역↔BDQTY 수량 연결(자원 역집계)
* 근거: REA 28 TWQTYLIST(Tab1 · Tab2 · sBtnTrans 기본 수량변수 전송 · sBtnSaveToFile · ActDef* 필드 설정 ·
* ActSheetToBoqByName/ByCode · LBoxGoTo 변수셀 찾기 · LBoxQty 수량변수 산근에 전송 · cBoxCode 9종) ·
* 지식DB 28 §2 · 02 QCODE · 10 ST_U_ConBD · 계산 = scr_qty_sheet.js
*/
(function (global) {
"use strict";
const P = (global.STMATE_PLUGINS = global.STMATE_PLUGINS || {
screens: {},
actions: {},
menus: [],
});
const S = () => global.STMATE_QTY_SHEET;
const num = (v) => Number(String(v ?? "").replace(/,/g, "")) || 0;
const esc = (s) =>
String(s == null ? "" : s).replace(
/[&<>"]/g,
(c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c],
);
const ROLES = [
["name", "내역명칭"],
["size", "내역규격"],
["unit", "내역단위"],
["qty", "내역수량"],
["code", "내역코드"],
["mamt", "자재단가"],
["lamt", "노무단가"],
["samt", "경비단가"],
["no", "공종번호"],
];
const TABS = [
["list", "수량산출서 목록표 (기본 수량변수)"],
["sheet", "수량산출서 보기 (확장 수량변수)"],
["code", "코드별 수량 · 자원 역집계"],
];
function state(ctx, app) {
const u = ctx.uiState;
const d = ctx.dbfData;
if (d && !d.qcode_rows && !u.qcodeLoading && ctx.currentSourcePath) {
u.qcodeLoading = true;
app
.postJson("/api/qty/qcode", { source_path: ctx.currentSourcePath })
.then((r) => ((d.qcode_rows = r.rows || []), app.renderWorkspace()))
.catch(() => (d.qcode_rows = []));
}
u.qtySheets = u.qtySheets || {};
const key = ctx.currentSourcePath || "-";
u.qtySheets[key] = u.qtySheets[key] || S().newSheet();
return { sheet: u.qtySheets[key], key, qrows: (d && d.qcode_rows) || [] };
}
function render(ctx, app) {
const { sheet, qrows } = state(ctx, app);
const tab = ctx.uiState.qtyTab || "list";
const body =
tab === "list"
? renderList(ctx, app, qrows)
: tab === "sheet"
? renderSheet(ctx, app, sheet)
: renderCode(ctx, app);
return `
<div class="win-form">
<div class="win-form-header"><span>수량산출서 [wQtyList · QCODE.DBF ${qrows.length}건]</span><span>${ctx.dbfData ? "" : "프로젝트를 먼저 여세요"}</span></div>
<div class="win-subbar">${TABS.map(([k, l]) => `<button class="win-btn qty-tab ${tab === k ? "active" : ""}" data-tab="${k}">${l}</button>`).join("")}
<button class="win-btn" id="qty-file-save" style="margin-left:auto;">수량산출서 파일로 저장</button>
<button class="win-btn" id="qty-file-load">수량산출파일 불러오기</button><input type="file" id="qty-file-input" accept=".json,.stq" style="display:none">
<button class="win-btn" id="qty-file-reset">수량산출파일 초기화</button></div>
${body}
</div>`;
}
// 탭1 — 수량코드(기본 수량변수)
function renderList(ctx, app, qrows) {
const u = ctx.uiState;
const sel = u.qtyQSel ?? -1;
const f = (r, k) =>
`<input class="cell-input num qty-q" data-i="${qrows.indexOf(r)}" data-k="${k}" value="${esc(String(r[k] ?? "").replace(/\.0+$/, ""))}" style="width:62px">`;
const errs = u.qtyCheck || [];
return `
<div class="win-subbar" style="flex-wrap:wrap;gap:4px;">
<button class="win-btn" id="qty-q-add">수량코드 추가</button><button class="win-btn" id="qty-q-del">삭제</button>
<button class="win-btn" id="qty-q-check">수량코드 검사</button>
<button class="win-btn" id="qty-q-assign">확장수량변수 → 기본수량변수 할당</button>
<span>전송</span><select id="qty-q-field">${Array.from({ length: 10 }, (_v, k) => `<option ${(u.qtyQField || "QTY1") === `QTY${k}` ? "selected" : ""}>QTY${k}</option>`).join("")}</select>
<span>→ 내역</span><select id="qty-q-dest"><option value="QTY1">당초 수량(QTY1)</option><option value="QTY2" ${u.qtyQDest === "QTY2" ? "selected" : ""}>변경 수량(QTY2)</option></select>
<button class="win-btn" id="qty-q-trans">기본 수량변수 전송</button>
<button class="win-btn primary" id="qty-q-save" style="margin-left:auto;">수량코드 저장(STC)</button>
</div>
${errs.length ? `<div style="color:#b91c1c;font-size:12px;padding:2px 8px;max-height:80px;overflow:auto;">${errs.map(esc).join("<br>")}</div>` : u.qtyCheck ? `<div style="color:#15803d;font-size:12px;padding:2px 8px;">검사 통과</div>` : ""}
<div class="split-horiz">
<div class="pane-box" style="flex:1;"><div class="grid-scroll"><table class="dbgrid">
<thead><tr><th style="width:24px"></th><th style="width:66px">수량코드</th><th>명칭</th><th style="width:80px">내역코드</th>${Array.from({ length: 10 }, (_v, k) => `<th style="width:66px">QTY${k}</th>`).join("")}</tr></thead>
<tbody>${qrows
.map(
(
r,
i,
) => `<tr class="${i === sel ? "selected" : ""}"><td class="center"><input type="radio" name="qty-q-sel" class="qty-q-sel" data-i="${i}" ${i === sel ? "checked" : ""}></td>
<td><input class="cell-input qty-q" data-i="${i}" data-k="CODE" value="${esc(r.CODE)}" style="width:60px"></td>
<td><input class="cell-input qty-q" data-i="${i}" data-k="NAME" value="${esc(r.NAME)}" style="width:100%"></td>
<td><input class="cell-input qty-q" data-i="${i}" data-k="BQCODE" value="${esc(r.BQCODE)}" style="width:74px"></td>
${Array.from({ length: 10 }, (_v, k) => `<td>${f(r, `QTY${k}`)}</td>`).join("")}</tr>`,
)
.join("")}</tbody></table></div></div>
${
sel >= 0 && qrows[sel]
? `<div class="pane-box" style="width:300px;">
<div class="pane-title"><span>${esc(qrows[sel].CODE)} 연결식(IFORM · QTYn=셀별명 식)</span></div>
<textarea class="smt-editor-area qty-q-memo" data-k="IFORM" style="height:110px">${esc(qrows[sel].IFORM || "")}</textarea>
<div class="pane-title"><span>확장 수량변수(XCODE · 이름=값)</span></div>
<textarea class="smt-editor-area qty-q-memo" data-k="XCODE" style="height:150px">${esc(qrows[sel].XCODE || "")}</textarea>
<div class="pane-title"><span>수량변수 산근에 전송 — 산출근거에 넣을 글</span></div>
<div style="font-family:Consolas,monospace;font-size:12px;">${varTokens(qrows[sel]).map(esc).join("<br>") || "-"}</div>
</div>`
: ""
}
</div>`;
}
const varTokens = (r) => {
const c = String(r.CODE || "").trim();
const out = [];
for (let k = 0; k < 10; k++) if (String(r[`QTY${k}`] ?? "").trim()) out.push(`~${c}.QTY${k}~`);
for (const ln of String(r.XCODE || "").split(/\r?\n/))
if (ln.includes("=")) out.push(`~${c}.${ln.split("=")[0].trim()}~`);
return out;
};
// 탭2 — 시트(셀별명 · 필드 설정 · 내역서로 가져가기)
function renderSheet(ctx, app, sheet) {
const u = ctx.uiState;
const ev = S().evalSheet(sheet);
const cur = u.qtyCell || "A1";
const aliasAt = {};
for (const [n, a] of Object.entries(sheet.aliases)) aliasAt[a.toUpperCase()] = n;
const roleOpt = (k) =>
`<select class="qty-role" data-k="${k}"><option value="">-</option>${Array.from({ length: sheet.cols }, (_v, c) => `<option ${sheet.roles[k] === S().colName(c) ? "selected" : ""}>${S().colName(c)}</option>`).join("")}</select>`;
const kind = u.qtyPickKind || "ALL";
const test = (S().KINDS.find((k) => k[0] === kind) || S().KINDS[0])[2];
const picks = ((ctx.dbfData && ctx.dbfData.costn_rows) || [])
.filter((r) => test(String(r.CODE).trim()))
.slice(0, 400);
return `
<div class="win-subbar" style="flex-wrap:wrap;gap:4px;">
<span>셀 <strong>${esc(cur)}</strong></span><input id="qty-cell-edit" value="${esc(sheet.cells[cur] ?? "")}" style="width:240px" placeholder="값 또는 =식(A1·셀별명·함수)">
<input id="qty-alias" placeholder="셀별명" style="width:90px" value="${esc(aliasAt[cur] || "")}"><button class="win-btn" id="qty-alias-set">셀별명 설정</button>
<button class="win-btn" id="qty-row-add">행 추가</button><button class="win-btn" id="qty-col-add">컬럼삽입</button>
<span>시작줄</span><input id="qty-start" type="number" style="width:50px" value="${sheet.start + 1}"><span>끝줄</span><input id="qty-end" type="number" style="width:50px" value="${sheet.end + 1}">
${ROLES.map(([k, l]) => `<label>${l} ${roleOpt(k)}</label>`).join("")}
<button class="win-btn primary" id="qty-to-boq-code">내역서로 가져가기(코드)</button>
<button class="win-btn primary" id="qty-to-boq-name">내역서로 가져가기(명칭)</button>
</div>
<div class="split-horiz">
<div class="pane-box" style="flex:1;"><div class="grid-scroll"><table class="dbgrid qty-sheet">
<thead><tr><th style="width:34px"></th>${Array.from(
{ length: sheet.cols },
(_v, c) =>
`<th>${S().colName(c)}${Object.entries(sheet.roles)
.filter(([, v]) => v === S().colName(c))
.map(([k]) => ` (${(ROLES.find((x) => x[0] === k) || [])[1]})`)
.join("")}</th>`,
).join("")}</tr></thead>
<tbody>${Array.from({ length: sheet.rows }, (_v, r) => {
const inRange =
r >= Math.min(sheet.start, sheet.end) && r <= Math.max(sheet.start, sheet.end);
return `<tr><td class="center" style="${inRange ? "background:#e0f2fe;" : ""}">${r + 1}</td>${Array.from(
{ length: sheet.cols },
(_w, c) => {
const a = S().addr(r, c);
const v = ev.values[a];
const shown =
v === undefined
? ""
: typeof v === "number"
? app.fmt(v, Number.isInteger(v) ? 0 : 3)
: v;
return `<td class="qty-cell ${a === cur ? "selected" : ""}" data-a="${a}" title="${esc(sheet.cells[a] ?? "")}${ev.errors[a] ? " — " + esc(ev.errors[a]) : ""}" style="cursor:cell;${ev.errors[a] ? "background:#fee2e2;" : ""}">${aliasAt[a] ? `<sup style="color:#1d4ed8">${esc(aliasAt[a])}</sup> ` : ""}${esc(shown)}</td>`;
},
).join("")}</tr>`;
}).join("")}</tbody></table></div></div>
<div class="pane-box" style="width:270px;">
<div class="pane-title"><span>변수셀 찾기(셀별명)</span></div>
<div style="max-height:150px;overflow:auto;font-size:12px;">${
Object.entries(sheet.aliases)
.map(
([n, a]) =>
`<div class="qty-goto" data-a="${esc(a)}" style="cursor:pointer;">${esc(n)} = ${esc(a)} → ${app.fmt(ev.aliasValues[n.toUpperCase()] || 0, 3)} <a class="qty-alias-del" data-n="${esc(n)}" style="color:#b91c1c;">×</a></div>`,
)
.join("") || "-"
}</div>
<div class="pane-title"><span>내역코드란에 단가코드 입력</span></div>
<select id="qty-pick-kind">${S()
.KINDS.map(
([k, l]) => `<option value="${k}" ${k === kind ? "selected" : ""}>${l}</option>`,
)
.join("")}</select>
<label style="font-size:12px;"><input type="checkbox" id="qty-pick-name" ${u.qtyPickName ? "checked" : ""}> 코드우측에 명칭/규격/단위 입력</label>
<div style="max-height:260px;overflow:auto;font-size:12px;">${picks.map((r) => `<div class="qty-pick" data-c="${esc(String(r.CODE).trim())}" style="cursor:pointer;">${esc(String(r.CODE).trim())} ${esc(r.NAME)} ${esc(r.SIZE)}</div>`).join("")}</div>
</div>
</div>`;
}
// 탭3 — 코드 종류별 내역 수량 · 내역↔BDQTY 자원 역집계
function renderCode(ctx, app) {
const u = ctx.uiState;
const kind = u.qtyCodeKind || "ALL";
const field = u.qtyCodeField || "QTY1";
const boq = (ctx.dbfData && ctx.dbfData.boq_rows) || [];
const list = S().qtyByCode(boq, kind, field);
const roll = S().rollup(boq, (ctx.dbfData && ctx.dbfData.bdqty_rows) || [], field);
const test = (S().KINDS.find((k) => k[0] === kind) || S().KINDS[0])[2];
const by = Object.fromEntries(
((ctx.dbfData && ctx.dbfData.costn_rows) || []).map((r) => [String(r.CODE).trim(), r]),
);
const res = Object.entries(roll.totals)
.filter(([c]) => test(c))
.sort(([a], [b]) => a.localeCompare(b));
return `
<div class="win-subbar"><select id="qty-code-kind">${S()
.KINDS.map(([k, l]) => `<option value="${k}" ${k === kind ? "selected" : ""}>${l}</option>`)
.join("")}</select>
<select id="qty-code-field"><option value="QTY1">당초 수량</option><option value="QTY2" ${field === "QTY2" ? "selected" : ""}>변경 수량</option></select>
${roll.cycles.length ? `<span style="color:#b91c1c;">순환참조 ${roll.cycles.length}건 — ${esc(roll.cycles[0])}</span>` : ""}</div>
<div class="split-horiz">
<div class="pane-box" style="flex:1;"><div class="pane-title"><span>내역 코드별 수량 ${list.length}건</span></div><div class="grid-scroll"><table class="dbgrid">
<thead><tr><th>코드</th><th>명칭</th><th>규격</th><th>단위</th><th>내역 행</th><th>수량 계</th></tr></thead>
<tbody>${list.map((o) => `<tr><td class="center">${esc(o.code)}</td><td>${esc(o.name)}</td><td>${esc(o.size)}</td><td class="center">${esc(o.unit)}</td><td class="num">${o.rows}</td><td class="num">${app.fmt(o.qty, 3)}</td></tr>`).join("")}</tbody></table></div></div>
<div class="pane-box" style="flex:1;"><div class="pane-title"><span>내역↔BDQTY 수량 연결(자원 소요량) ${res.length}건</span></div><div class="grid-scroll"><table class="dbgrid">
<thead><tr><th>코드</th><th>명칭</th><th>규격</th><th>단위</th><th>소요 수량</th></tr></thead>
<tbody>${res.map(([c, q]) => `<tr><td class="center">${esc(c)}</td><td>${esc((by[c] || {}).NAME)}</td><td>${esc((by[c] || {}).SIZE)}</td><td class="center">${esc((by[c] || {}).UNIT)}</td><td class="num">${app.fmt(q, 4)}</td></tr>`).join("")}</tbody></table></div></div>
</div>`;
}
// 내역 반영 — 코드가 같은 행 수량 바꿈 · 없으면 끝에 새 행(STMATE_UI_BOQ 모델)
function applyToBoq(ctx, items, dest) {
const B = global.STMATE_UI_BOQ;
const model = {
rows: ctx.dbfData.boq_rows,
items: ctx.state.boqItems,
deleted: (ctx.dbfData.boq_deleted = ctx.dbfData.boq_deleted || []),
};
const by = Object.fromEntries(
(ctx.dbfData.costn_rows || []).map((r) => [String(r.CODE).trim(), r]),
);
const used = new Set();
let upd = 0,
add = 0;
const fresh = [];
for (const it of items) {
const hit =
it.code &&
model.rows.find((r) => !used.has(r) && String(r.BDCODE1 || "").trim() === it.code);
if (hit) {
used.add(hit);
if (dest === "QTY1" && B) B.setCell(model, hit._recno, "QTY1", it.qty);
else hit[dest] = String(it.qty);
upd++;
} else if (B) {
const cr = by[it.code] || {};
fresh.push(
B.makeRow(model, {
BDCODE1: it.code,
NAME1: it.name || cr.NAME || "",
SIZE1: it.size || cr.SIZE || "",
UNIT1: it.unit || cr.UNIT || "",
[dest]: String(it.qty),
ITNUM: it.itnum || "",
}),
);
add++;
}
}
if (fresh.length) {
const res = B.insertRows(model, model.rows.length, fresh);
if (res && res.ok === false) alert(res.error);
}
return { upd, add };
}
function bind(ctx, app) {
const $ = (id) => document.getElementById(id);
const u = ctx.uiState;
const { sheet, key, qrows } = state(ctx, app);
const redraw = () => app.renderWorkspace();
document
.querySelectorAll(".qty-tab")
.forEach((b) => (b.onclick = () => ((u.qtyTab = b.dataset.tab), redraw())));
$("qty-file-save").onclick = () =>
app.downloadBlob(S().serialize(sheet, qrows), "수량산출서.stq.json", "application/json");
$("qty-file-load").onclick = () => $("qty-file-input").click();
$("qty-file-input").onchange = (e) => {
const f = e.target.files && e.target.files[0];
if (!f) return;
const rd = new FileReader();
rd.onload = () => {
const d = S().deserialize(String(rd.result));
if (d.error) return alert(d.error);
u.qtySheets[key] = d.sheet;
if (d.qcode.length && ctx.dbfData) ctx.dbfData.qcode_rows = d.qcode;
redraw();
};
rd.readAsText(f);
};
$("qty-file-reset").onclick = () =>
confirm("수량산출파일을 초기화할까요?") && ((u.qtySheets[key] = S().newSheet()), redraw());
const tab = u.qtyTab || "list";
if (tab === "list") bindList(ctx, app, qrows, sheet);
else if (tab === "sheet") bindSheet(ctx, app, sheet);
else {
$("qty-code-kind").onchange = (e) => ((u.qtyCodeKind = e.target.value), redraw());
$("qty-code-field").onchange = (e) => ((u.qtyCodeField = e.target.value), redraw());
}
}
function bindList(ctx, app, qrows, sheet) {
const $ = (id) => document.getElementById(id);
const u = ctx.uiState;
const redraw = () => app.renderWorkspace();
const d = ctx.dbfData;
if (!d) return;
document
.querySelectorAll(".qty-q")
.forEach(
(el) =>
(el.onchange = () => (
(qrows[Number(el.dataset.i)][el.dataset.k] = el.value.trim()),
(u.qtyCheck = null)
)),
);
document
.querySelectorAll(".qty-q-sel")
.forEach((el) => (el.onchange = () => ((u.qtyQSel = Number(el.dataset.i)), redraw())));
document
.querySelectorAll(".qty-q-memo")
.forEach((el) => (el.onchange = () => (qrows[u.qtyQSel][el.dataset.k] = el.value)));
$("qty-q-add").onclick = () => {
d.qcode_rows = qrows;
qrows.push({ CODE: S().nextQcode(qrows), NAME: "", BQCODE: "", IFORM: "", XCODE: "" });
u.qtyQSel = qrows.length - 1;
redraw();
};
$("qty-q-del").onclick = () => {
if (!(u.qtyQSel >= 0) || !confirm("선택한 수량코드를 지울까요?")) return;
qrows.splice(u.qtyQSel, 1);
u.qtyQSel = -1;
redraw();
};
$("qty-q-check").onclick = () => (
(u.qtyCheck = S().checkQcodes(
qrows,
new Set((d.costn_rows || []).map((r) => String(r.CODE).trim())),
)),
redraw()
);
$("qty-q-assign").onclick = () => {
const res = S().assignFromSheet(qrows, sheet);
d.qcode_rows = res.rows;
const se = Object.entries(res.sheetErrors);
u.qtyCheck = res.errors
.map((e) => `${e.code}: ${e.msg}`)
.concat(
se.length
? [`셀참조 에러가 있는 수량변수(셀별명) ${se.length}건 — ${se[0][0]} ${se[0][1]}`]
: [],
);
redraw();
};
$("qty-q-field").onchange = (e) => (u.qtyQField = e.target.value);
$("qty-q-dest").onchange = (e) => (u.qtyQDest = e.target.value);
$("qty-q-trans").onclick = () => {
const fld = $("qty-q-field").value,
dest = $("qty-q-dest").value;
const items = qrows
.filter((r) => String(r.BQCODE || "").trim())
.map((r) => ({ code: String(r.BQCODE).trim(), name: r.NAME, qty: num(r[fld]) }));
if (!items.length) return alert("내역코드(BQCODE)가 있는 수량코드가 없음");
const r = applyToBoq(ctx, items, dest);
app.cascadeAndRender();
alert(`기본 수량변수 전송 — 내역 ${r.upd}행 수량 바꿈 · ${r.add}행 새로 넣음`);
};
$("qty-q-save").onclick = async () => {
const errs = S().checkQcodes(
qrows,
new Set((d.costn_rows || []).map((r) => String(r.CODE).trim())),
);
if (errs.length) return ((u.qtyCheck = errs), redraw());
try {
const res = await app.postJson("/api/qty/qcode-save", {
source_path: ctx.currentSourcePath,
target_folder_name: (ctx.state.projectMeta.pname || "stmate_edited").slice(0, 28),
rows: qrows,
});
alert(`수량코드 ${res.count}건 저장 — ${res.saved_stc}`);
} catch (err) {
alert("수량코드 저장 실패: " + err.message);
}
};
}
function bindSheet(ctx, app, sheet) {
const $ = (id) => document.getElementById(id);
const u = ctx.uiState;
const redraw = () => app.renderWorkspace();
const cur = u.qtyCell || "A1";
document
.querySelectorAll(".qty-cell")
.forEach((td) => (td.onclick = () => ((u.qtyCell = td.dataset.a), redraw())));
$("qty-cell-edit").onchange = (e) => {
const v = e.target.value;
if (v === "") delete sheet.cells[cur];
else sheet.cells[cur] = v;
const p = S().parseAddr(cur);
if (p && p.r + 1 < sheet.rows) u.qtyCell = S().addr(p.r + 1, p.c);
redraw();
};
$("qty-alias-set").onclick = () => {
const n = $("qty-alias").value.trim();
for (const [k, a] of Object.entries(sheet.aliases)) if (a === cur) delete sheet.aliases[k];
if (n) {
if (!/^[A-Za-z가-힣_][A-Za-z0-9가-힣_]*$/.test(n) || S().parseAddr(n))
return alert("셀별명은 글자로 시작 · 셀 주소 꼴은 안 됨");
sheet.aliases[n] = cur;
}
redraw();
};
document
.querySelectorAll(".qty-goto")
.forEach((el) => (el.ondblclick = el.onclick = () => ((u.qtyCell = el.dataset.a), redraw())));
document
.querySelectorAll(".qty-alias-del")
.forEach(
(el) =>
(el.onclick = (e) => (e.stopPropagation(), delete sheet.aliases[el.dataset.n], redraw())),
);
$("qty-row-add").onclick = () => ((sheet.rows += 10), redraw());
$("qty-col-add").onclick = () => ((sheet.cols += 1), redraw());
$("qty-start").onchange = (e) => (
(sheet.start = Math.max(0, num(e.target.value) - 1)),
redraw()
);
$("qty-end").onchange = (e) => ((sheet.end = Math.max(0, num(e.target.value) - 1)), redraw());
document
.querySelectorAll(".qty-role")
.forEach((el) => (el.onchange = () => ((sheet.roles[el.dataset.k] = el.value), redraw())));
$("qty-pick-kind").onchange = (e) => ((u.qtyPickKind = e.target.value), redraw());
$("qty-pick-name").onchange = (e) => (u.qtyPickName = e.target.checked);
document.querySelectorAll(".qty-pick").forEach(
(el) =>
(el.ondblclick = el.onclick =
() => {
const c = el.dataset.c;
sheet.cells[cur] = c;
const p = S().parseAddr(cur);
if (u.qtyPickName && p) {
const r =
(ctx.dbfData.costn_rows || []).find((x) => String(x.CODE).trim() === c) || {};
[r.NAME, r.SIZE, r.UNIT].forEach(
(v, k) => v && (sheet.cells[S().addr(p.r, p.c + k + 1)] = String(v).trim()),
);
}
if (p && p.r + 1 < sheet.rows) u.qtyCell = S().addr(p.r + 1, p.c);
redraw();
}),
);
const toBoq = (mode) => {
const res = S().sheetToBoq(sheet, mode);
if (res.error) return alert(res.error);
if (!res.items.length) return alert("시작줄~끝줄 안에 가져갈 행이 없음");
const by = Object.fromEntries(
(ctx.dbfData.costn_rows || []).map((r) => [String(r.CODE).trim(), r]),
);
const bad = mode === "code" ? res.items.filter((it) => !by[it.code]) : [];
if (bad.length)
return alert(
`단가목록에 없는 내역코드: ${bad.map((b) => `${b.row}행 ${b.code}`).join(", ")}`,
);
const r = applyToBoq(ctx, res.items, "QTY1");
app.cascadeAndRender();
alert(`내역서로 가져가기 — ${r.upd}행 수량 바꿈 · ${r.add}행 새로 넣음`);
};
$("qty-to-boq-code").onclick = () => toBoq("code");
$("qty-to-boq-name").onclick = () => toBoq("name");
}
P.screens.wQtyList = { render, bind };
P.actions.Act_Qty_List = (ctx, app) => (
(ctx.uiState.activeScreen = "wQtyList"),
app.renderWorkspace()
);
if (!P.menus.some((m) => m.act === "Act_Qty_List"))
P.menus.push({ menu: "m3", label: "수량산출서 (wQtyList)", act: "Act_Qty_List" });
global.STMATE_QTY_LIST = { applyToBoq };
})(window);