- 보고서: 원본 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>
498 lines
24 KiB
JavaScript
498 lines
24 KiB
JavaScript
/* -*- 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) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[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);
|