feat(est): move estimate rows safely

This commit is contained in:
2026-11-07 10:33:22 +09:00
parent 0f635cef0f
commit 4f33cafb12
5 changed files with 71 additions and 1 deletions
@@ -1,6 +1,28 @@
from test_est_rea_parity import run
def test_estimate_move_preserves_row_identity_and_blocks_ambiguous_or_total_rows():
result = run(
"math",
"""(()=>{
const rows=[{PkID:1,Code:'A'},{PkID:2,Code:'B'},{PkID:3,Code:'C'},{LinkIndex:54}];
const moved=EstBusiness.moveEstimateRows(rows,new Set([1,2]),-1);
const order=rows.map(row=>row.PkID||0);
const restored=EstBusiness.moveEstimateRows(rows,new Set(moved),1);
let blocked=0;
for (const [data,selection,direction] of [
[[{Code:'A'},{Code:'A'}],[0],1],
[[{PkID:1},{LinkIndex:54}],[0],1],
[[{PkID:1},{PkID:2},{PkID:3}],[0,2],1],
[[{PkID:1},{Name:'unknown'}],[0],1]]) {
try{EstBusiness.moveEstimateRows(data,new Set(selection),direction)}catch{blocked++}
}
return {moved,order,restored,blocked};
})()""",
)
assert result == {"moved": [0, 1], "order": [2, 3, 1, 0], "restored": [1, 2], "blocked": 4}
def test_duplicate_codes_match_distinct_original_rows_and_track_deletions():
result = run(
"math",
+2
View File
@@ -144,6 +144,8 @@
><button data-action="add-row">행 추가</button><button data-action="copy-row">복사</button
><button data-action="paste-row">붙여넣기</button
><button data-action="delete-row">삭제</button
><button id="move-estimate-up" data-action="move-estimate-up" hidden>행 ↑</button
><button id="move-estimate-down" data-action="move-estimate-down" hidden>행 ↓</button
><input
id="search"
type="search"
+15 -1
View File
@@ -45,7 +45,21 @@ const EstBusinessActions = (() => {
const { state } = EstApp,
project = state.project,
round = currentRound();
if (name === "unlock-round") {
if (name === "move-estimate-up" || name === "move-estimate-down") {
if (state.view !== "EstMainDat") throw Error("내역서에서만 행을 이동할 수 있습니다.");
const direction = name === "move-estimate-up" ? -1 : 1;
const indices = EstBusiness.moveEstimateRows(
structuredClone(project.tables.EstMainDat),
state.selection,
direction,
);
if (indices.every((index) => state.selection.has(index))) return;
EstApp.change(() => {
state.selection = new Set(
EstBusiness.moveEstimateRows(project.tables.EstMainDat, state.selection, direction),
);
});
} else if (name === "unlock-round") {
if (confirm("이 회차와 이후 회차의 확정을 해제하시겠습니까? 입력값은 유지됩니다."))
EstApp.change(() => (state.project = EstProgress.unlock(project, round.id)), false);
} else if (name === "delete-round") {
+30
View File
@@ -4,6 +4,35 @@ const EstBusiness = (() => {
const copy = (value) => structuredClone(value);
const rowKey = (row, index) => String(row.PkID ?? (row.Code || `row:${index}`));
const special = (row) => [57, 58, 62, 67].includes(num(row.LinkIndex));
function moveEstimateRows(rows, selection, direction) {
const indices = [...selection].sort((a, b) => a - b);
if (!indices.length) throw Error("이동할 내역 행을 선택하세요.");
if (
![1, -1].includes(direction) ||
indices.some((value, index) => value !== indices[0] + index)
)
throw Error("연속된 내역 행을 선택하세요.");
const first = indices[0],
last = indices.at(-1),
neighbor = direction < 0 ? first - 1 : last + 1;
if (neighbor < 0 || neighbor >= rows.length) return indices;
const affected = rows.slice(Math.min(first, neighbor), Math.max(last, neighbor) + 1);
if (affected.some((row) => [54, 55, 58, 59].includes(num(row.LinkIndex))))
throw Error("합계 행은 이동하거나 건너뛸 수 없습니다.");
const counts = new Map();
rows.forEach((row, index) => {
const key = rowKey(row, index);
counts.set(key, (counts.get(key) || 0) + 1);
});
for (let index = Math.min(first, neighbor); index <= Math.max(last, neighbor); index++) {
const row = rows[index];
if ((row.PkID == null && !row.Code) || counts.get(rowKey(row, index)) !== 1)
throw Error("행 식별자가 없거나 중복되어 연결을 보존할 수 없습니다.");
}
const group = rows.splice(first, indices.length);
rows.splice(first + direction, 0, ...group);
return indices.map((index) => index + direction);
}
function baseline(project) {
const working = { ...project };
delete working.baseline;
@@ -355,6 +384,7 @@ const EstBusiness = (() => {
annual,
newAnnualName,
rowKey,
moveEstimateRows,
importContract,
importBid,
annualCopy,
+2
View File
@@ -97,6 +97,8 @@ const EstViews = (() => {
byId("search").value = state.query;
byId("standard-tools").hidden = !project.tables[view];
byId("add-group").hidden = !grouped;
byId("move-estimate-up").hidden = view !== "EstMainDat";
byId("move-estimate-down").hidden = view !== "EstMainDat";
if (project.tables[view]) {
const all = EstApp.visibleRows(),
start = state.page * 100,