feat(est): replace cost calculation text
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
## Codex · EST 독립 테스트 앱 (2026-11-04)
|
||||
- [x] 산출근거 문구 일괄 바꾸기 → 현재 호표·전체 범위, 표지 보존·재계산·실행 취소 확인.
|
||||
- 자체 검증: Tailscale HTTP Edge에서 현재 호표·전체 치환, 일치 없음 안내, 결과·단가 표지 보존과 실행 취소 확인. 산출근거 시험 24개 통과, 수식 변경 후 재계산 확인.
|
||||
- [x] Excel 파일 읽는 동안 파일·셀 선택이 바뀌면 이전 요청 반영 차단 → 지연 응답 시험.
|
||||
- 자체 검증: 지연된 Tailscale HTTP 응답 동안 셀과 파일을 각각 변경하면 이전 요청 반영이 차단되고 다시 실행은 성공. 수량 시험 12개 통과.
|
||||
- 자체 검증: 수량·Excel·설계변경·기성·물가변동·연차·문서·오류 기능 회귀 69개 통과(openpyxl 경고 8건). 원본 동등성 대조는 실행하지 않음.
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
|
||||
행을 선택한 뒤 편집 메뉴의 `선택 행 자릿수 설정`에서 처리방식과 소수 자릿수를 일괄 적용할 수 있습니다. 기존 계산 설정은 실행 취소로 되돌릴 수 있습니다.
|
||||
|
||||
단가산출 화면의 `편집 → 산출근거 문구 바꾸기`는 현재 호표 또는 전체 산출근거에서 문구를 일괄 변경합니다. 결과값·연결 단가 등의 계산 표지는 치환하지 않으며, 자동계산이 켜져 있으면 변경 후 재계산합니다. 실행 취소로 복원할 수 있습니다.
|
||||
|
||||
내역서 편집 메뉴의 `내역 수량 초기화`는 선택한 상세 행 또는 모든 상세 행의 수량을 0으로 바꿉니다. 비율·합계 자동행은 건드리지 않으며 실행 취소할 수 있습니다.
|
||||
|
||||
인쇄 미리보기는 화면의 100행 페이지 제한 없이 검색 결과/현재 호표의 전체 행을 표시합니다. 출력 범위에서 현재 자료의 모든 호표·행도 선택할 수 있습니다. 원본 인쇄 옵션의 41개 항목 중 CAD를 제외한 40개 서식 이름을 선택하며, ESTX에서 가져온 해당 프로젝트의 인쇄 설정을 우선 사용합니다. 내역·원가·호표/총괄·증감·동원·수량·공정은 표 자료로 출력합니다. 표지·갑지·설명서·시방서·심사 문서는 서식별 본문을 직접 입력합니다. 용지·방향·여백·글자 크기·페이지당 행·시작 쪽·업체명을 저장하고 인쇄/PDF로 출력합니다. ESTX 수정 저장은 서식별 여백과 페이지당 행 수를 반영합니다. 다른 웹 인쇄 설정의 ESTX 쓰기와 원본 인쇄 도형·병합·글꼴 배치의 동등성은 지원하지 않습니다.
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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 };
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user