diff --git a/tools/est-test/native-progress-wonga.js b/tools/est-test/native-progress-wonga.js new file mode 100644 index 000000000..4bcb77edc --- /dev/null +++ b/tools/est-test/native-progress-wonga.js @@ -0,0 +1,250 @@ +// 원본 기성 원가계산서(TfrmWongaAccomH 0xd48408 / V 0xd46914) 편집 — 회차별 원가 서식 사본(round.wonga)에만 적용. +// 한줄 삽입·추가(0xd49758: 새 행 Index 0·CalcIndex 0·Checked 끔, 앞 행 끝줄 표지 이어받기), 한줄 삭제(확인 뒤, 재계산 없음), +// 내용 삭제(LinkIndex·Cost·Biul·Youl·VSusic·Diget 비움), 선택항목 절하(0xd094dc, 기성은 도급액 행도 허용), +// 선택데이타 절하(popupTrunc: 직전 자리수를 그대로, CalcBase 행 제외), 공사비역산 자리수(목표 = 10^n 미만 절하 → 이윤 역산), +// 초기화(사본 지움 = 도급 원가 서식), 되살리기(직전 조작 전), 산출근거 약어로 표시(A8: 수식칸 편집 잠금만). +// 원본의 번호·참조 재작성(0xc97398·0xc971e8)은 하지 않는다 — 웹 수식은 Gubun·이름으로 찾으므로 새 행은 기호를 직접 적는다. +const EstProgressWonga = (() => { + const num = (value) => Number(value) || 0; + + function template(project, round) { + return round.contract?.wonga || project.tables.Wonga || []; + } + function edit(project, round, mutate) { + if (round.locked) throw Error("확정한 회차는 고칠 수 없습니다."); + round.wongaUndo = round.wonga ? structuredClone(round.wonga) : null; + round.wonga ||= structuredClone(template(project, round)); + return mutate(round.wonga); + } + function blank(list, at) { + const row = { Index: 0, Gubun: "", Name: "", Youl: "", VSusic: "", Cost: 0, CalcIndex: 0 }; + const prev = list[at - 1], + end = num(prev?.EndLineIndex); + row.EndLineIndex = end === 1 ? 1 : end === 3 ? 3 : 0; + if (end === 3) prev.EndLineIndex = 0; + row.Checked = false; + return row; + } + + const ops = { + insert(project, round, at) { + edit(project, round, (list) => list.splice(at, 0, blank(list, at))); + }, + append(project, round) { + edit(project, round, (list) => list.push(blank(list, list.length))); + }, + remove(project, round, at) { + edit(project, round, (list) => { + if (!list[at]) throw Error("삭제할 줄을 고르세요."); + list.splice(at, 1); + }); + }, + clear(project, round, at) { + edit(project, round, (list) => { + if (!list[at]) throw Error("내용을 지울 줄을 고르세요."); + Object.assign(list[at], { + LinkIndex: 0, + Cost: 0, + Biul: "", + Youl: "", + VSusic: "", + Diget: 0, + Checked: false, + }); + }); + }, + position(project, round, at, method, digits) { + edit(project, round, (list) => + EstWongaTools.position( + { tables: { Wonga: list }, options: project.options }, + [at], + method, + digits, + true, + ), + ); + }, + trunc(project, round, at) { + edit(project, round, (list) => { + if (!list[at]) throw Error("절하할 줄을 고르세요."); + if (!list[at].CalcBase) list[at].Diget = num(project.options.WongaLastDiget); + }); + }, + total(project, round, digits) { + if (round.locked) throw Error("확정한 회차는 고칠 수 없습니다."); + if (!Number.isInteger(digits) || digits < 0 || digits > 9) + throw Error("자리수는 0~9 사이로 입력하세요."); + if (digits) round.wongaTotalDigits = digits; + else delete round.wongaTotalDigits; + }, + init(project, round) { + if (round.locked) throw Error("확정한 회차는 고칠 수 없습니다."); + round.wongaUndo = round.wonga ? structuredClone(round.wonga) : null; + delete round.wonga; + delete round.wongaTotalDigits; + }, + undo(project, round) { + if (round.locked) throw Error("확정한 회차는 고칠 수 없습니다."); + if (!("wongaUndo" in round)) throw Error("되살릴 조작이 없습니다."); + if (round.wongaUndo) round.wonga = round.wongaUndo; + else delete round.wonga; + delete round.wongaUndo; + }, + field(project, round, at, name, value) { + edit(project, round, (list) => { + if (!list[at] || !["Gubun", "Name", "Youl", "VSusic", "Cost"].includes(name)) + throw Error("고칠 칸이 없습니다."); + list[at][name] = name === "Cost" ? num(String(value).replaceAll(",", "")) : value; + }); + }, + }; + + // 화면 + function render(state, round, output) { + const { esc, number } = EstApp, + project = state.project, + list = round.wonga || template(project, round), + locked = round.locked, + lockFormula = project.options.WongaSNameShow, + pick = state.progressWongaRow; + const b = (action, label) => + ``; + const tools = [ + b("pw-insert", "한줄 삽입"), + b("pw-append", "한줄 추가"), + b("pw-delete", "한줄 삭제"), + b("pw-clear", "내용 삭제"), + b("pw-position", "선택항목 절하하기"), + b("pw-trunc", "선택데이타 절하하기"), + b("pw-total", "공사비역산 자리수 맞추기"), + b("pw-init", "원가계산서 초기화"), + b("pw-undo", "되살리기"), + ``, + round.wonga ? "회차 서식 사용 중" : "", + round.wongaTotalDigits ? `역산 ${round.wongaTotalDigits}자리` : "", + ].join(""); + const input = (i, name, value, disabled) => + ``; + const body = (output.costs || []) + .map((row, i) => { + const tpl = list[i] || {}; + return `${input(i, "Gubun", tpl.Gubun, false)}${input(i, "Name", tpl.Name, false)}${input(i, "Youl", tpl.Youl, false)}${input(i, "VSusic", tpl.VSusic, lockFormula)}${num(tpl.Diget) ? `${tpl.Diget}/${num(tpl.DigetIndex)}` : ""}${number(row.ContractCost)}${number(row.JunCost)}${number(row.GumCost)}${number(row.SaCost)}${number(row.TotalCost)}`; + }) + .join(""); + return `

기성 원가계산서

${tools}
${body}
기호원가 비목요율산출근거자리도급금액전회금회사정누계
`; + } + + function round(state) { + return state.project.progress.find((r) => r.id === state.roundId); + } + function at(state) { + const i = state.progressWongaRow; + if (!Number.isInteger(i)) throw Error("기성 원가계산서에서 줄을 고르세요."); + return i; + } + + const names = new Set([ + "pw-insert", + "pw-append", + "pw-delete", + "pw-clear", + "pw-position", + "pw-trunc", + "pw-total", + "pw-init", + "pw-undo", + ]); + function handle(name, state) { + if (!names.has(name)) return false; + const r = round(state); + if (!r) throw Error("기성 회차를 고르세요."); + const run = (fn) => EstApp.change((project) => fn(project, project.progress.find((x) => x.id === r.id))); + if (name === "pw-insert") run((p, x) => ops.insert(p, x, at(state))); + else if (name === "pw-append") run((p, x) => ops.append(p, x)); + else if (name === "pw-delete") { + const i = at(state); + if (confirm("현재 줄을 삭제하시겠습니까?")) { + run((p, x) => ops.remove(p, x, i)); + state.progressWongaRow = undefined; + } + } else if (name === "pw-clear") run((p, x) => ops.clear(p, x, at(state))); + else if (name === "pw-trunc") run((p, x) => ops.trunc(p, x, at(state))); + else if (name === "pw-init") { + if (confirm("이 회차의 원가계산서를 도급 원가 서식으로 되돌리시겠습니까?")) run(ops.init); + } else if (name === "pw-undo") run(ops.undo); + else if (name === "pw-position") { + const i = at(state); + EstActions.form( + "선택항목 절하하기", + [ + { + name: "method", + label: "처리방식", + value: 2, + choices: [ + [1, "사사오입"], + [2, "절하"], + [3, "절상"], + [0, "초기화"], + ], + }, + { + name: "digits", + label: "자리 (0 원 · 1 십원 … 6 백만원)", + type: "number", + value: num(state.project.options.WongaLastDiget), + }, + ], + (values) => + run((p, x) => ops.position(p, x, i, Number(values.method), Number(values.digits))), + ); + } else if (name === "pw-total") + EstActions.form( + "공사비역산 자리수 맞추기", + [ + { + name: "digits", + label: "도급액 절하 자리 (0 해제 · 3 천원 · 4 만원 …)", + type: "number", + value: r.wongaTotalDigits ?? 3, + }, + ], + (values) => run((p, x) => ops.total(p, x, Number(values.digits))), + ); + return true; + } + + function install() { + document.addEventListener("change", (event) => { + const t = event.target, + state = EstApp.state; + if (t.dataset.pwPick !== undefined) { + state.progressWongaRow = Number(t.dataset.pwPick); + return; + } + if (t.dataset.pwSname !== undefined) { + EstApp.change((project) => (project.options.WongaSNameShow = t.checked), false); + return; + } + if (t.dataset.pwField === undefined) return; + const r = round(state); + try { + EstApp.change((project) => + ops.field( + project, + project.progress.find((x) => x.id === r.id), + Number(t.dataset.i), + t.dataset.pwField, + t.value, + ), + ); + } catch (error) { + state.notice = error.message; + EstApp.render(); + } + }); + } + + if (typeof document !== "undefined" && document.addEventListener) install(); + return { template, ops, render, handle }; +})();