feat(est-test): 수량산출 누계 계산과 넣기

- 원본 누계 표지(F·a)를 읽어 계산마다 소계로 다시 구함
- 산출 줄 커서 자리에 누계 넣기, 표지 반올림 방식·자리 반영

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-11 20:06:29 +09:00
co-authored by Claude Opus 5.5
parent 6f32263ea3
commit c0d49e0bbb
9 changed files with 148 additions and 10 deletions
@@ -0,0 +1,55 @@
"""EST 테스트 앱 수량산출 누계(원본 ActionAdd Ctrl+U 0x11155c4, 재계산 때 F·a 표지 다시 계산)."""
import json
import subprocess
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
FILES = [
"native-math.js",
"native-formula.js",
"native-text.js",
"native-quantity-total.js",
"native-quantity.cjs",
]
SCRIPT = r"""(() => {
const sheet = { Code: 'N1', options: { digits: 2, roundMode: 2 }, items: [
{ Code: 'r000001' }, { Code: 'r000002' }, { Code: 'r000003' }, { Code: 'r000004' }],
rows: [
{ LineData: ' \x0e터파기\x0e 2×3 = \x126\x12', markers: ['r000001'] },
{ LineData: ' 1.5×2 = \x123\x12', markers: ['r000002'] },
{ LineData: ' \x16999\x16', markers: [], totals: ['F000001'] },
{ LineData: ' 4×1 = \x124\x12', markers: ['r000003'] },
{ LineData: ' 0.3333×1 = \x120.33\x12', markers: ['r000004'] },
{ LineData: ' \x160\x16', markers: [], totals: ['F00000222'] } ] };
const out = EstQuantity.calculate(sheet);
const totals = out.rows.map((r) => (r.LineData.match(/\x16([^\x16]*)\x16/) || [])[1]).filter(Boolean);
sheet.rows[0].LineData = sheet.rows[0].LineData.replace('2×3', '200×30');
const big = EstQuantity.calculate(sheet).rows[2].LineData.match(/\x16([^\x16]*)\x16/)[1];
const fresh = { Code: 'N2', options: { digits: 1, roundMode: 1 }, items: [], rows: [{ LineData: ' 끝', markers: [] }] };
const name = EstQuantityTotal.insert(fresh, 0, 1);
let inside = ''; try { EstQuantityTotal.insert(fresh, 0, 4); } catch (e) { inside = e.message.slice(0, 5); }
const parsed = EstQuantity.fromWpt('N9', '시험',
'<div><begin Tag=1 Name:"F00000312"/>7<end Tag=1/></div>', [], {});
return { totals, big, name, line: EstText.display(fresh.rows[0].LineData), edited: fresh.markerEdited,
inside, parsed: [parsed.rows[0]?.LineData, parsed.rows[0]?.totals], errors: out.errors };
})()"""
def test_total_is_subtotal_since_name_or_previous_total():
runner = f"""
const fs = require('fs'), vm = require('vm');
const ctx = vm.createContext({{ structuredClone, console }});
for (const f of {json.dumps(FILES)})
vm.runInContext(fs.readFileSync('tools/est-test/' + f, 'utf8'), ctx, {{ filename: f }});
process.stdout.write(JSON.stringify(vm.runInContext({json.dumps(SCRIPT)}, ctx)));
"""
result = json.loads(subprocess.check_output(["node", "-e", runner], cwd=ROOT).decode("utf-8"))
assert result["errors"] == []
assert result["totals"] == ["9", "4.33"]
assert result["big"] == "6,003"
assert result["name"] == "F00000111"
assert result["line"] == " ⟦누계⟧0⟦누계⟧끝"
assert result["edited"] is True
assert result["inside"] == "누계 표지"
assert result["parsed"] == ["\x167\x16", ["F00000312"]]
+2
View File
@@ -473,6 +473,8 @@ ative-standard.js에 표준시장단가 원천(SilJukDat)과 내역 행(EstMainD
- [x] 1단계 읽기 — `xls-read.cjs`(복합문서·SST/CONTINUE·LABELSST·NUMBER·RK·MULRK·LABEL·BOOLERR·FORMULA 저장값), 가져올 때 `sheet.excel`, 수량산출 화면 읽기 전용 격자(수식 셀 굵게). - [x] 1단계 읽기 — `xls-read.cjs`(복합문서·SST/CONTINUE·LABELSST·NUMBER·RK·MULRK·LABEL·BOOLERR·FORMULA 저장값), 가져올 때 `sheet.excel`, 수량산출 화면 읽기 전용 격자(수식 셀 굵게).
- 검증완료: `test_est_xls_read.py` 2개(설치 main.estx 엑셀형 6개·토적계산표 B2·No.1·20·수식 16개, OLE 아님 거부·격자 그리기), 수량 시험 포함 22개 통과. 엑셀형 붙인 뒤에도 ESTX 원본 그대로 저장 동일. ORCA 브라우저에서 토적계산표 격자 표시·수식 칸 16개 확인 후 샘플 복구. - 검증완료: `test_est_xls_read.py` 2개(설치 main.estx 엑셀형 6개·토적계산표 B2·No.1·20·수식 16개, OLE 아님 거부·격자 그리기), 수량 시험 포함 22개 통과. 엑셀형 붙인 뒤에도 ESTX 원본 그대로 저장 동일. ORCA 브라우저에서 토적계산표 격자 표시·수식 칸 16개 확인 후 샘플 복구.
- 남음: 셀 편집·수식 계산·4방향 합계·품명/규격/수량 지정(항목과 셀 대응 근거 미확인), .xls 저장. - 남음: 셀 편집·수식 계산·4방향 합계·품명/규격/수량 지정(항목과 셀 대응 근거 미확인), .xls 저장.
- [x] 수량산출 누계(원본 TfrmMulWord ActionAdd Ctrl+U → 0x11155c4, 재계산 0x111c0b0 — 디스어셈블 근거) — 원본 F·a 표지를 `` 쌍·`row.totals` 로 읽고, 위쪽 품명 줄(포함)/앞 누계(제외)부터 줄마다 첫 결과 합을 표지 반올림(F+6자리+방식+자리)으로 매 계산 갱신, 커서 자리 누계 넣기. 원본 TFRMMULNAMEINPUT 「누계계산」 단추는 실제로 지정해제와 같은 처리라 따로 만들지 않음.
- 검증완료: `test_est_quantity_total.py`(소계 범위·앞 누계 제외·반올림 표지·값 갱신 6,003·넣기 이름·표지 안 거부·원본 F 표지 읽기) 통과, 수량 시험 포함 26개 통과. ORCA 브라우저에서 「소계」 줄 끝에 누계 → ⟦누계⟧9⟦누계⟧(6+3), 샘플 복구.
- 대조 합계(CAD 제외 882개): 구현 283 · 부분 193 · 없음 273 · 제외 133. - 대조 합계(CAD 제외 882개): 구현 283 · 부분 193 · 없음 273 · 제외 133.
- [ ] 남은 핵심(실무 빈도순): - [ ] 남은 핵심(실무 빈도순):
3. 수량산출서 — 누계·4방향 합계, 제출용 전체 인쇄. 3. 수량산출서 — 누계·4방향 합계, 제출용 전체 인쇄.
+2
View File
@@ -48,6 +48,8 @@
`편집 → 찾기·다음찾기`(Ctrl+F)는 원본 찾기 창처럼 검색어를 처음부터 또는 현재 위치부터 찾아 해당 행으로 이동합니다. 일위대가·단가산출·기계경비는 찾은 행의 호표로 넘어갑니다. 단가산출에서는 찾은 행의 문구를 한 건씩 바꾸고 다음 위치로 이동하며, 모두 바꾸기는 `산출근거 문구 바꾸기`를 씁니다. 표 위 검색창은 걸러 보기로 그대로 남습니다. `편집 → 찾기·다음찾기`(Ctrl+F)는 원본 찾기 창처럼 검색어를 처음부터 또는 현재 위치부터 찾아 해당 행으로 이동합니다. 일위대가·단가산출·기계경비는 찾은 행의 호표로 넘어갑니다. 단가산출에서는 찾은 행의 문구를 한 건씩 바꾸고 다음 위치로 이동하며, 모두 바꾸기는 `산출근거 문구 바꾸기`를 씁니다. 표 위 검색창은 걸러 보기로 그대로 남습니다.
산출 줄 옆 `누계`는 원본 누계(Ctrl+U)처럼 커서 자리에 누계 값을 넣습니다. 값은 가장 가까운 위쪽 품명 줄(포함) 또는 앞 누계 줄(제외)부터 바로 위 줄까지 줄마다 첫 결과 수량의 합(소계)이며, 계산할 때마다 다시 구합니다. 원본 파일의 누계 표지도 읽어 다시 계산합니다.
원본의 엑셀형 수량산출서(본문이 옛 Excel .xls)는 가져올 때 셀 값을 읽어 수량산출 화면 아래에 읽기 전용 격자로 보입니다. 수식 셀은 Excel이 저장해 둔 계산값을 굵게 보이며, 셀 편집·다시 계산은 아직 하지 않습니다. 원본의 엑셀형 수량산출서(본문이 옛 Excel .xls)는 가져올 때 셀 값을 읽어 수량산출 화면 아래에 읽기 전용 격자로 보입니다. 수식 셀은 Excel이 저장해 둔 계산값을 굵게 보이며, 셀 편집·다시 계산은 아직 하지 않습니다.
산출 줄 옆 `수량 지정`은 줄 글에서 드래그해 고른 숫자를 결과 표지로 감싸고 수량 항목을 새로 만듭니다. 품명·규격·단위 표지가 있으면 항목 이름에 옮깁니다. `지정 해제`는 커서가 놓인 결과 표지를 풀고, 다른 줄에서 쓰지 않는 항목을 지웁니다. 내역에 연결된 항목은 먼저 연결을 비워야 합니다. 이 변경이 있는 작업은 ESTX 수정 저장을 거부합니다. 산출 줄 옆 `수량 지정`은 줄 글에서 드래그해 고른 숫자를 결과 표지로 감싸고 수량 항목을 새로 만듭니다. 품명·규격·단위 표지가 있으면 항목 이름에 옮깁니다. `지정 해제`는 커서가 놓인 결과 표지를 풀고, 다른 줄에서 쓰지 않는 항목을 지웁니다. 내역에 연결된 항목은 먼저 연결을 비워야 합니다. 이 변경이 있는 작업은 ESTX 수정 저장을 거부합니다.
+1
View File
@@ -10,6 +10,7 @@
<script defer src="native-machine.js"></script> <script defer src="native-machine.js"></script>
<script defer src="native-rows.js"></script> <script defer src="native-rows.js"></script>
<script defer src="native-text.js"></script> <script defer src="native-text.js"></script>
<script defer src="native-quantity-total.js"></script>
<script defer src="native-quantity.cjs"></script> <script defer src="native-quantity.cjs"></script>
<script defer src="native-wonga.js"></script> <script defer src="native-wonga.js"></script>
<script defer src="native-jungsang.js"></script> <script defer src="native-jungsang.js"></script>
+9
View File
@@ -120,6 +120,15 @@ const EstQuantitySheets = (() => {
if (index < 0 || index + step < 0 || index + step >= sheets.length) if (index < 0 || index + step < 0 || index + step >= sheets.length)
throw Error("더 옮길 수 없습니다."); throw Error("더 옮길 수 없습니다.");
EstApp.change((project) => move(project, code, step), false); EstApp.change((project) => move(project, code, step), false);
} else if (name === "quantity-total") {
const sheet = state.project.quantitySheets?.find((next) => next.Code === code),
index = Number(node?.dataset.index),
editor = lineEditor(index);
if (!sheet) throw Error("수량산출서를 고르세요.");
EstApp.change(
() => EstQuantityTotal.insert(sheet, index, editor.selectionStart ?? 0, editor.value),
false,
);
} else if (name === "quantity-mark" || name === "quantity-free") { } else if (name === "quantity-mark" || name === "quantity-free") {
const sheet = state.project.quantitySheets?.find((next) => next.Code === code), const sheet = state.project.quantitySheets?.find((next) => next.Code === code),
index = Number(node?.dataset.index), index = Number(node?.dataset.index),
+61
View File
@@ -0,0 +1,61 @@
// 원본 수량산출 누계(TfrmMulWord ActionAdd Ctrl+U → 0x11155c4, 재계산 0x111c0b0 에서 F·a 표지 다시 계산).
// 누계 값 = 가장 가까운 위쪽 품명 줄(포함) 또는 앞 누계 줄(제외)부터 누계 줄 바로 위까지, 줄마다 첫 결과 수량의 합.
// 앞 누계는 더하지 않는 소계다. 반올림은 표지 이름 뒤 방식·자리(F000003 + 1 + 2), 없으면 산출서 설정.
// 웹은 누계를 \x16 쌍으로 적고 이름을 row.totals 에 둔다. 「누계」·「∴」 같은 글자는 넣지 않는다.
const EstQuantityTotal = (() => {
const first = (line) => {
const match = String(line ?? "").match(/\x12([-\d.,]+)\x12/);
return match ? Number(match[1].replaceAll(",", "")) || 0 : 0;
};
function format(value, digits) {
return value.toLocaleString("en-US", { maximumFractionDigits: digits, useGrouping: true });
}
function rounding(sheet, name) {
const match = String(name ?? "").match(/^[Fa]\d{6}([123])(\d)$/);
return {
method: match ? Number(match[1]) : (sheet.options?.roundMode ?? 2),
digits: match ? Number(match[2]) : (sheet.options?.digits ?? 2),
};
}
// rows: 계산이 끝난 줄(LineData 의 결과 값이 갱신된 것). 누계 줄의 \x16 값을 고쳐 쓴다.
function apply(sheet, rows) {
let start = 0;
rows.forEach((row, i) => {
const line = String(row.LineData ?? "");
if (line.includes("\x16")) {
let sum = 0;
for (let j = start; j < i; j++) sum += first(rows[j].LineData);
const names = sheet.rows[i]?.totals || [];
let part = 0;
row.LineData = line.replace(/\x16[^\x16]*\x16/g, () => {
const { method, digits } = rounding(sheet, names[part++]);
return `\x16${format(EstMath.fixed(sum, digits, method), digits)}\x16`;
});
start = i + 1;
} else if (line.includes("\x0e")) start = i;
});
return rows;
}
// 커서 자리에 새 누계를 넣는다. text 는 화면 표기(⟦누계⟧) 줄, caret 은 그 안의 위치.
function insert(sheet, rowIndex, caret, text) {
const row = sheet.rows[rowIndex];
if (!row) throw Error("누계를 넣을 산출 줄을 고르세요.");
const used = new Set(sheet.rows.flatMap((r) => (r.totals || []).map((n) => n.slice(1, 7))));
let n = 1;
while (used.has(String(n).padStart(6, "0"))) n++;
const { method, digits } = rounding(sheet, "");
const name = `F${String(n).padStart(6, "0")}${method}${digits}`,
shown = text ?? EstText.display(row.LineData),
at = Math.max(0, Math.min(caret, shown.length)),
position = shown.slice(0, at).split("⟦누계⟧").length - 1;
const before = shown.slice(0, at);
if (position % 2 || before.lastIndexOf("⟦") > before.lastIndexOf("⟧"))
throw Error("누계 표지 안에는 넣을 수 없습니다.");
row.LineData = EstText.encode(`${shown.slice(0, at)}⟦누계⟧0⟦누계⟧${shown.slice(at)}`);
row.totals = [...(row.totals || [])];
row.totals.splice(position / 2, 0, name);
sheet.markerEdited = true;
return name;
}
return { apply, insert, rounding };
})();
+15 -9
View File
@@ -26,16 +26,19 @@ const EstQuantity = (() => {
if (name) tags.set(match[1], name); if (name) tags.set(match[1], name);
} }
for (const match of text.matchAll(/<div\b[^>]*>([\s\S]*?)<\/div>/gi)) { for (const match of text.matchAll(/<div\b[^>]*>([\s\S]*?)<\/div>/gi)) {
const markers = []; const markers = [],
totals = [];
let raised = false; let raised = false;
const tag = (name) => const tag = (name) =>
/^r\d+$/.test(name) || /^L\d+$/.test(name) /^r\d+$/.test(name) || /^L\d+$/.test(name)
? "\x12" ? "\x12"
: /^N\d+$/.test(name) : /^[Fa]\d+$/.test(name)
? "\x0e" ? "\x16"
: /^S\d+$/.test(name) : /^N\d+$/.test(name)
? "\x13" ? "\x0e"
: ""; : /^S\d+$/.test(name)
? "\x13"
: "";
let body = match[1].replace(/<c\s+nr=(\d+)\s*\/>/gi, (_, id) => { let body = match[1].replace(/<c\s+nr=(\d+)\s*\/>/gi, (_, id) => {
const next = superscripts.has(id), const next = superscripts.has(id),
value = next && !raised ? "^(" : !next && raised ? ")" : ""; value = next && !raised ? "^(" : !next && raised ? ")" : "";
@@ -47,11 +50,12 @@ const EstQuantity = (() => {
.replace(/<begin\b[^>]*\bTag=(\d+)[^>]*>/gi, (_, id) => { .replace(/<begin\b[^>]*\bTag=(\d+)[^>]*>/gi, (_, id) => {
const name = tags.get(id) || ""; const name = tags.get(id) || "";
if (tag(name) === "\x12") markers.push(name); if (tag(name) === "\x12") markers.push(name);
if (tag(name) === "\x16") totals.push(name);
return tag(name); return tag(name);
}) })
.replace(/<end\b[^>]*\bTag=(\d+)[^>]*>/gi, (_, id) => tag(tags.get(id) || "")); .replace(/<end\b[^>]*\bTag=(\d+)[^>]*>/gi, (_, id) => tag(tags.get(id) || ""));
body = entities(body.replace(/<[^>]*>/g, "").replace(/\r?\n/g, "")); body = entities(body.replace(/<[^>]*>/g, "").replace(/\r?\n/g, ""));
if (body.trim()) rows.push({ LineData: body, markers }); if (body.trim()) rows.push({ LineData: body, markers, ...(totals.length && { totals }) });
} }
return { Code: code, Name: name, rows, items, options }; return { Code: code, Name: name, rows, items, options };
} }
@@ -78,7 +82,7 @@ const EstQuantity = (() => {
}; };
return { return {
...row, ...row,
LineData: row.LineData.replace(/\*개소당|\bPIPE\b|소\s*계|∴/g, ""), LineData: row.LineData.replace(/\*개소당|\bPIPE\b|소\s*계|∴|\x16[^\x16]*\x16/g, ""),
Index: 0, Index: 0,
SCheck: 0, SCheck: 0,
LinkIndex: 0, LinkIndex: 0,
@@ -112,6 +116,8 @@ const EstQuantity = (() => {
(whole) => actual[part++]?.[0] || whole, (whole) => actual[part++]?.[0] || whole,
); );
}); });
if (typeof EstQuantityTotal !== "undefined")
EstQuantityTotal.apply(sheet, result.rows.slice(1));
return { return {
rows: result.rows.slice(1), rows: result.rows.slice(1),
items: sheet.items.map((item) => ({ ...item, GN: values.get(item.Code) ?? item.GN })), items: sheet.items.map((item) => ({ ...item, GN: values.get(item.Code) ?? item.GN })),
@@ -315,7 +321,7 @@ const EstQuantity = (() => {
{ esc, number, byId } = EstApp; { esc, number, byId } = EstApp;
byId("work-content").innerHTML = byId("work-content").innerHTML =
selector(state) + selector(state) +
`<div class="business-tools"><label>산출서명 <input data-quantity-name value="${esc(sheet.Name)}"></label><button data-quantity-add-item>수량 항목 추가</button><button data-quantity-add>산출 행 추가</button><button data-quantity-apply>내역 수량 반영</button><button data-action="quantity-sheet-copy">산출서 복사</button><button data-action="quantity-sheet-up">산출서 위로</button><button data-action="quantity-sheet-down">산출서 아래로</button><button data-quantity-delete-sheet>산출서 삭제</button></div><table class="sheet"><thead><tr><th>산출코드</th><th>품명</th><th>규격</th><th>단위</th><th>저장 수량</th><th>재계산 수량</th><th>연결 내역코드</th><th>끝수 처리</th><th>작업</th></tr></thead><tbody>${output.items.map((item, index) => `<tr><td>${esc(item.Code)}</td><td><input data-quantity-item="Name" data-index="${index}" value="${esc(item.Name || "")}" aria-label="품명"></td><td><input data-quantity-item="Size" data-index="${index}" value="${esc(item.Size || "")}" aria-label="규격"></td><td><input data-quantity-item="Unite" data-index="${index}" value="${esc(item.Unite || "")}" aria-label="단위"></td><td>${number(sheet.items[index]?.GN ?? 0)}</td><td>${number(item.GN)}</td><td style="white-space:normal"><input data-quantity-target="${index}" value="${esc(item.TargetCode ?? item.LinkCode ?? "")}" aria-label="연결 내역코드">${targetSelect(state.project, item, "sheet", index)}</td><td>${formatSelect(sheet.items[index], index)}</td><td><button data-quantity-delete-item="${index}">삭제</button></td></tr>`).join("")}</tbody></table>${typeof EstQuantitySheets !== "undefined" ? EstQuantitySheets.excelView(sheet) : ""}<p class="business-note">${esc(output.errors.join(" / "))} 산출 행의 결과 표지로 수량 항목을 연결합니다. 신규 산출서와 지원되지 않는 원본 구조 변경은 JSON으로 저장합니다.</p><table class="sheet"><thead><tr><th>줄</th><th>산출근거 편집</th><th>작업</th></tr></thead><tbody>${sheet.rows.map((row, index) => `<tr><td>${index + 1}</td><td><textarea data-quantity-line="${index}" rows="1" style="min-width:750px" aria-label="산출근거 ${index + 1}">${esc(EstText.display(output.rows[index].LineData))}</textarea></td><td><button data-quantity-add data-index="${index}">아래 추가</button><button data-quantity-move="-1" data-index="${index}" ${index === 0 ? "disabled" : ""}>↑</button><button data-quantity-move="1" data-index="${index}" ${index === sheet.rows.length - 1 ? "disabled" : ""}>↓</button><button data-action="quantity-mark" data-index="${index}">수량 지정</button><button data-action="quantity-free" data-index="${index}">지정 해제</button><button data-quantity-delete-row="${index}">삭제</button></td></tr>`).join("")}</tbody></table>`; `<div class="business-tools"><label>산출서명 <input data-quantity-name value="${esc(sheet.Name)}"></label><button data-quantity-add-item>수량 항목 추가</button><button data-quantity-add>산출 행 추가</button><button data-quantity-apply>내역 수량 반영</button><button data-action="quantity-sheet-copy">산출서 복사</button><button data-action="quantity-sheet-up">산출서 위로</button><button data-action="quantity-sheet-down">산출서 아래로</button><button data-quantity-delete-sheet>산출서 삭제</button></div><table class="sheet"><thead><tr><th>산출코드</th><th>품명</th><th>규격</th><th>단위</th><th>저장 수량</th><th>재계산 수량</th><th>연결 내역코드</th><th>끝수 처리</th><th>작업</th></tr></thead><tbody>${output.items.map((item, index) => `<tr><td>${esc(item.Code)}</td><td><input data-quantity-item="Name" data-index="${index}" value="${esc(item.Name || "")}" aria-label="품명"></td><td><input data-quantity-item="Size" data-index="${index}" value="${esc(item.Size || "")}" aria-label="규격"></td><td><input data-quantity-item="Unite" data-index="${index}" value="${esc(item.Unite || "")}" aria-label="단위"></td><td>${number(sheet.items[index]?.GN ?? 0)}</td><td>${number(item.GN)}</td><td style="white-space:normal"><input data-quantity-target="${index}" value="${esc(item.TargetCode ?? item.LinkCode ?? "")}" aria-label="연결 내역코드">${targetSelect(state.project, item, "sheet", index)}</td><td>${formatSelect(sheet.items[index], index)}</td><td><button data-quantity-delete-item="${index}">삭제</button></td></tr>`).join("")}</tbody></table>${typeof EstQuantitySheets !== "undefined" ? EstQuantitySheets.excelView(sheet) : ""}<p class="business-note">${esc(output.errors.join(" / "))} 산출 행의 결과 표지로 수량 항목을 연결합니다. 신규 산출서와 지원되지 않는 원본 구조 변경은 JSON으로 저장합니다.</p><table class="sheet"><thead><tr><th>줄</th><th>산출근거 편집</th><th>작업</th></tr></thead><tbody>${sheet.rows.map((row, index) => `<tr><td>${index + 1}</td><td><textarea data-quantity-line="${index}" rows="1" style="min-width:750px" aria-label="산출근거 ${index + 1}">${esc(EstText.display(output.rows[index].LineData))}</textarea></td><td><button data-quantity-add data-index="${index}">아래 추가</button><button data-quantity-move="-1" data-index="${index}" ${index === 0 ? "disabled" : ""}>↑</button><button data-quantity-move="1" data-index="${index}" ${index === sheet.rows.length - 1 ? "disabled" : ""}>↓</button><button data-action="quantity-mark" data-index="${index}">수량 지정</button><button data-action="quantity-free" data-index="${index}">지정 해제</button><button data-action="quantity-total" data-index="${index}">누계</button><button data-quantity-delete-row="${index}">삭제</button></td></tr>`).join("")}</tbody></table>`;
} }
// 원본 수량목록 자리수 메뉴(mnuAuto·mnuTrunc·mnuUp·mnuRound): 결과 코드 뒤 방식(1 반올림·2 절사·3 올림)+자리수. // 원본 수량목록 자리수 메뉴(mnuAuto·mnuTrunc·mnuUp·mnuRound): 결과 코드 뒤 방식(1 반올림·2 절사·3 올림)+자리수.
function resultFormat(code) { function resultFormat(code) {
+2 -1
View File
@@ -615,11 +615,12 @@ const EstText = (() => {
"\x12": "결과", "\x12": "결과",
"\x13": "규격", "\x13": "규격",
"\x15": "단위", "\x15": "단위",
"\x16": "누계",
"\x1a": "합계", "\x1a": "합계",
}; };
const display = (text) => const display = (text) =>
String(text || "").replace( String(text || "").replace(
/[\x03\x06\x07\x0c\x0e\x11\x12\x13\x15\x1a]/g, /[\x03\x06\x07\x0c\x0e\x11\x12\x13\x15\x16\x1a]/g,
(ch) => `⟦${names[ch]}⟧`, (ch) => `⟦${names[ch]}⟧`,
); );
const encode = (text) => const encode = (text) =>
+1
View File
@@ -21,6 +21,7 @@ const scripts = [
"native-machine.js", "native-machine.js",
"native-rows.js", "native-rows.js",
"native-text.js", "native-text.js",
"native-quantity-total.js",
"native-quantity.cjs", "native-quantity.cjs",
"native-wonga.js", "native-wonga.js",
"native-jungsang.js", "native-jungsang.js",