diff --git a/resources/tester/test_est_progress_workflow.py b/resources/tester/test_est_progress_workflow.py index 6b88b1989..565b9a03e 100644 --- a/resources/tester/test_est_progress_workflow.py +++ b/resources/tester/test_est_progress_workflow.py @@ -1,6 +1,23 @@ from test_est_rea_parity import run +def test_progress_rejects_ambiguous_inputs_without_applying_them_to_multiple_rows(): + result = run( + "math", + """(()=>{ + const p=EstNative.empty();p.options.SourceRefresh=false; + p.tables.EstMainDat=[{Code:'A',GN:10,GU:100},{Code:'A',GN:20,GU:100}, + {PkID:3,Code:'B',GN:10,GU:100},{LinkIndex:54}]; + p.progress=[{id:'one',inputs:{A:{Gum:2},'3':{Gum:3}}}]; + const out=EstProgress.calculate(EstNative.calculate(p),'one');let rejected=false; + try{EstProgress.lock(EstNative.calculate(p),'one')}catch{rejected=true} + return {out,rejected}; + })()""", + ) + assert result["rejected"] and result["out"]["errors"] + assert [row["GumTC"] for row in result["out"]["rows"][:3]] == [0, 0, 300] + + def test_progress_amount_input_preserves_exact_money_and_component_total(): result = run( "math", diff --git a/tools/est-test/native-actions.js b/tools/est-test/native-actions.js index 0943f3bed..dc6eed120 100644 --- a/tools/est-test/native-actions.js +++ b/tools/est-test/native-actions.js @@ -552,6 +552,8 @@ const EstActions = (() => { if (node.matches("[data-progress]")) { const round = project.progress.find((r) => r.id === state.roundId); if (round.locked) throw Error("확정 회차입니다."); + if (EstProgress.ambiguousKeys(project.tables.EstMainDat).has(node.dataset.key)) + throw Error("기성 내역 식별자가 중복되어 입력할 수 없습니다."); const value = node.type === "number" ? Number(node.value) : node.value; if (node.type === "number" && !Number.isFinite(value)) throw Error("수치 입력 오류"); EstApp.change(() => { diff --git a/tools/est-test/native-business-actions.js b/tools/est-test/native-business-actions.js index 69eb997f7..3f9fe3281 100644 --- a/tools/est-test/native-business-actions.js +++ b/tools/est-test/native-business-actions.js @@ -109,9 +109,19 @@ const EstBusinessActions = (() => { }, { name: "ratio", label: "비율 %", type: "number", value: 100 }, ], - (values) => + (values) => { + const value = numeric(values.ratio); + const ambiguous = EstProgress.ambiguousKeys(project.tables.EstMainDat); + const hasAmbiguous = project.tables.EstMainDat.some( + (row, index) => + Number(row.Level || 0) <= 0 && + Number(row.CalcIndex || 0) < 2 && + ![54, 55, 58, 59, 62, 67].includes(Number(row.LinkIndex)) && + !(values.mode === "full" && Number(row.LinkIndex) === 57) && + ambiguous.has(EstBusiness.rowKey(row, index)), + ); + if (hasAmbiguous) throw Error("기성 내역 식별자가 중복되어 일괄 입력할 수 없습니다."); EstApp.change(() => { - const value = numeric(values.ratio); project.tables.EstMainDat.forEach((row, index) => { if ( Number(row.Level) > 0 || @@ -127,7 +137,8 @@ const EstBusinessActions = (() => { round.inputs[key][values.prefix] = values.mode === "full" ? Number(row.GN) || 0 : value; }); - }, false), + }, false); + }, ); } else if (name === "load-progress-exclusion") { const locked = project.progress.filter((round) => round.locked && round.result); diff --git a/tools/est-test/native-progress.js b/tools/est-test/native-progress.js index c71f624f7..b149e162f 100644 --- a/tools/est-test/native-progress.js +++ b/tools/est-test/native-progress.js @@ -8,6 +8,14 @@ const EstProgress = (() => { return Math.max(0, (mantissa.split(".")[1]?.length || 0) - Number(exponent || 0)); }; const ordinary = (row) => num(row.Level) === 0 && ![54, 55, 58, 59].includes(num(row.LinkIndex)); + function ambiguousKeys(rows) { + const counts = new Map(); + rows.forEach((row, index) => { + const key = EstBusiness.rowKey(row, index); + counts.set(key, (counts.get(key) || 0) + 1); + }); + return new Set([...counts].filter(([, count]) => count > 1).map(([key]) => key)); + } function distribute(amount, row, digits) { const parts = components.map((field) => num(row[field])); const base = sum(row); @@ -31,6 +39,7 @@ const EstProgress = (() => { const errors = [], position = project.progress.indexOf(round), key = EstBusiness.rowKey; + const ambiguous = ambiguousKeys(rows); const earlier = project.progress.slice(0, position); if (earlier.some((item) => !item.locked)) errors.push("전회 기성 회차를 먼저 확정하세요."); const previous = earlier.at(-1)?.result, @@ -39,8 +48,10 @@ const EstProgress = (() => { const separated = round.separateComponents !== false; rows.forEach((row, index) => { const id = key(row, index), - input = round.inputs?.[id] || {}, - old = oldRows.get(id); + input = ambiguous.has(id) ? {} : round.inputs?.[id] || {}, + old = ambiguous.has(id) ? null : oldRows.get(id); + if (ambiguous.has(id) && (round.inputs?.[id] || oldRows.has(id))) + errors.push(`기성 내역 식별자가 중복됩니다: ${id}`); for (const field of ["GN", "TC", "Ratio", ...components]) row["Jun" + field] = num(old?.["Total" + field]); if (num(row.Level) > 0 || [54, 59].includes(num(row.LinkIndex))) { @@ -301,5 +312,5 @@ const EstProgress = (() => { } return project; } - return { calculate, lock, unlock, rebuild }; + return { calculate, lock, unlock, rebuild, ambiguousKeys }; })();