feat(est): replace cost calculation text

This commit is contained in:
2026-11-07 16:18:30 +09:00
parent cc1c1c659d
commit c530ac694c
6 changed files with 106 additions and 2 deletions
+24
View File
@@ -3,6 +3,30 @@
from test_est_rea_parity import run
def test_replace_cost_words_preserves_markers_and_checks_lengths_before_changes():
result = run(
"math",
r"""(()=>{
const rows=[{index:3,row:{LineData:'\x0e모래\x0e + 모래 = \x121\x12'}},{index:5,row:{LineData:'유지'}}];
const before=JSON.stringify(rows),changed=EstText.replaceWords(rows,'모래','자갈');let rejected=0;
for(const [a,b] of [['','x'],['모래','\x12'],['모래','x'.repeat(200001)]]){
try{EstText.replaceWords(rows,a,b)}catch{rejected++}
}
const markers=EstText.replaceWords([{index:7,row:{LineData:'Q=1 = \x121\x12; cost \x031\x03'}}],'1','2');
const calcRows=[{Index:1,Code:'H',LineData:'head'},{LineData:'Q=1 = \x121\x12'}];
const update=EstText.replaceWords([{row:calcRows[1],index:1}],'1','2')[0];calcRows[1].LineData=update.value;
const recalculated=EstText.calculate(calcRows);
return {changed,rejected,markers,unchanged:JSON.stringify(rows)===before,
crossing:EstText.replaceWords(rows,'모래 +','x').length,recalculated:recalculated.rows[1].LineData,errors:recalculated.errors};
})()""",
)
assert result["changed"] == [{"index": 3, "value": "\x0e자갈\x0e + 자갈 = \x121\x12"}]
assert result["rejected"] == 3
assert result["markers"] == [{"index": 7, "value": "Q=2 = \x121\x12; cost \x031\x03"}]
assert "\x122\x12" in result["recalculated"] and not result["errors"]
assert result["unchanged"] and result["crossing"] == 0
def test_text_units_and_description_columns_preserve_numeric_assignments():
result = run(
"math",
+2
View File
@@ -1,4 +1,6 @@
## Codex · EST 독립 테스트 앱 (2026-11-04)
- [x] 산출근거 문구 일괄 바꾸기 → 현재 호표·전체 범위, 표지 보존·재계산·실행 취소 확인.
- 자체 검증: Tailscale HTTP Edge에서 현재 호표·전체 치환, 일치 없음 안내, 결과·단가 표지 보존과 실행 취소 확인. 산출근거 시험 24개 통과, 수식 변경 후 재계산 확인.
- [x] Excel 파일 읽는 동안 파일·셀 선택이 바뀌면 이전 요청 반영 차단 → 지연 응답 시험.
- 자체 검증: 지연된 Tailscale HTTP 응답 동안 셀과 파일을 각각 변경하면 이전 요청 반영이 차단되고 다시 실행은 성공. 수량 시험 12개 통과.
- 자체 검증: 수량·Excel·설계변경·기성·물가변동·연차·문서·오류 기능 회귀 69개 통과(openpyxl 경고 8건). 원본 동등성 대조는 실행하지 않음.
+2
View File
@@ -14,6 +14,8 @@
행을 선택한 뒤 편집 메뉴의 `선택 행 자릿수 설정`에서 처리방식과 소수 자릿수를 일괄 적용할 수 있습니다. 기존 계산 설정은 실행 취소로 되돌릴 수 있습니다.
단가산출 화면의 `편집 → 산출근거 문구 바꾸기`는 현재 호표 또는 전체 산출근거에서 문구를 일괄 변경합니다. 결과값·연결 단가 등의 계산 표지는 치환하지 않으며, 자동계산이 켜져 있으면 변경 후 재계산합니다. 실행 취소로 복원할 수 있습니다.
내역서 편집 메뉴의 `내역 수량 초기화`는 선택한 상세 행 또는 모든 상세 행의 수량을 0으로 바꿉니다. 비율·합계 자동행은 건드리지 않으며 실행 취소할 수 있습니다.
인쇄 미리보기는 화면의 100행 페이지 제한 없이 검색 결과/현재 호표의 전체 행을 표시합니다. 출력 범위에서 현재 자료의 모든 호표·행도 선택할 수 있습니다. 원본 인쇄 옵션의 41개 항목 중 CAD를 제외한 40개 서식 이름을 선택하며, ESTX에서 가져온 해당 프로젝트의 인쇄 설정을 우선 사용합니다. 내역·원가·호표/총괄·증감·동원·수량·공정은 표 자료로 출력합니다. 표지·갑지·설명서·시방서·심사 문서는 서식별 본문을 직접 입력합니다. 용지·방향·여백·글자 크기·페이지당 행·시작 쪽·업체명을 저장하고 인쇄/PDF로 출력합니다. ESTX 수정 저장은 서식별 여백과 페이지당 행 수를 반영합니다. 다른 웹 인쇄 설정의 ESTX 쓰기와 원본 인쇄 도형·병합·글꼴 배치의 동등성은 지원하지 않습니다.
+1
View File
@@ -75,6 +75,7 @@
><button data-action="delete-row">선택 행 삭제</button>
<hr />
<button data-action="find">찾기</button>
<button data-action="replace-cost-words">산출근거 문구 바꾸기</button>
<button data-action="set-row-precision">선택 행 자릿수 설정</button>
<button data-action="clear-estimate-quantities">내역 수량 초기화</button>
</div>
+50 -1
View File
@@ -45,7 +45,56 @@ const EstBusinessActions = (() => {
const { state } = EstApp,
project = state.project,
round = currentRound();
if (name === "clear-estimate-quantities") {
if (name === "replace-cost-words") {
if (state.view !== "CostMainDat") throw Error("단가산출 화면에서 문구를 바꾸세요.");
const revision = state.revision,
groupCode = state.group;
EstActions.form(
"산출근거 문구 바꾸기",
[
{ name: "search", label: "찾을 문구", value: "" },
{ name: "replacement", label: "바꿀 문구", value: "" },
{
name: "scope",
label: "범위",
value: "group",
choices: [
["group", "현재 호표"],
["all", "전체 산출근거"],
],
},
],
(values) => {
if (
state.project !== project ||
state.revision !== revision ||
state.view !== "CostMainDat" ||
state.group !== groupCode
)
throw Error("작업이 바뀌었습니다. 문구 바꾸기를 다시 열어주세요.");
const rows = project.tables.CostMainDat,
group = EstNative.groups(rows).get(groupCode),
input = document.querySelector('#dialog-content [name="search"]');
if (values.scope === "group" && !group) throw Error("현재 호표를 선택하세요.");
input.setCustomValidity(values.search ? "" : "찾을 문구를 입력하세요.");
if (!input.reportValidity()) throw Error("찾을 문구를 입력하세요.");
const candidates = rows
.map((row, index) => ({ row, index }))
.filter(
({ index }) =>
values.scope === "all" ||
(index >= group.start && index < group.start + group.rows.length),
),
updates = EstText.replaceWords(candidates, values.search, values.replacement);
input.setCustomValidity(updates.length ? "" : "변경할 문구가 없습니다.");
if (!input.reportValidity()) throw Error("변경할 문구가 없습니다.");
if (confirm(`${updates.length}개 행의 문구를 바꾸시겠습니까?`))
EstApp.change(() =>
updates.forEach(({ index, value }) => (rows[index].LineData = value)),
);
},
);
} else if (name === "clear-estimate-quantities") {
if (state.view !== "EstMainDat") throw Error("내역서에서 수량을 초기화하세요.");
const rows = project.tables.EstMainDat,
selected = [...state.selection];
+27 -1
View File
@@ -582,5 +582,31 @@ const EstText = (() => {
/⟦([^⟦⟧]+)⟧/g,
(whole, name) => Object.keys(names).find((key) => names[key] === name) || whole,
);
return { calculate, clean, display, encode };
function replaceWords(rows, search, replacement) {
if (
typeof search !== "string" ||
typeof replacement !== "string" ||
!search ||
/[\x00-\x1f]/.test(search + replacement)
)
throw Error("찾을 문구를 입력하고 제어 문자를 제외하세요.");
const protectedMarkers = new Set(["\x03", "\x06", "\x07", "\x0c", "\x11", "\x12"]);
return rows.flatMap(({ row, index }) => {
if (typeof row.LineData !== "string") return [];
let protectedMarker = "";
const next = row.LineData.split(/([\x00-\x1f])/)
.map((part, i) => {
if (i % 2) {
if (part === protectedMarker) protectedMarker = "";
else if (!protectedMarker && protectedMarkers.has(part)) protectedMarker = part;
return part;
}
return protectedMarker ? part : part.split(search).join(replacement);
})
.join("");
if (next.length > 200000) throw Error("변경 후 산출근거의 길이가 한도를 초과합니다.");
return next === row.LineData ? [] : [{ index, value: next }];
});
}
return { calculate, clean, display, encode, replaceWords };
})();