fix(est): apply selected estimate classification safely
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
## Codex · EST 독립 테스트 앱 (2026-11-04)
|
||||
- [x] 원천 재조회가 꺼진 내역의 비목 지정 변경을 해당 행에만 반영 → 자동·수동 계산, 미연결·다른 행 보존 시험.
|
||||
- 자체 검증: 현재 단가를 유지한 비목 지정·일치하는 원천으로 복원, 다른 행·재조회 옵션 보존. 원천 누락·중복·단가 불일치는 복원 차단. Edge에서 자동·수동 계산 확인, 관련 시험 32개 통과.
|
||||
- [x] 산출근거 문구 일괄 바꾸기 → 현재 호표·전체 범위, 표지 보존·재계산·실행 취소 확인.
|
||||
- 자체 검증: Tailscale HTTP Edge에서 현재 호표·전체 치환, 일치 없음 안내, 결과·단가 표지 보존과 실행 취소 확인. 산출근거 시험 24개 통과, 수식 변경 후 재계산 확인.
|
||||
- [x] Excel 파일 읽는 동안 파일·셀 선택이 바뀌면 이전 요청 반영 차단 → 지연 응답 시험.
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
|
||||
내역서 편집 메뉴의 `내역 수량 초기화`는 선택한 상세 행 또는 모든 상세 행의 수량을 0으로 바꿉니다. 비율·합계 자동행은 건드리지 않으며 실행 취소할 수 있습니다.
|
||||
|
||||
원천 자료에 연결된 내역 행의 결과비목을 재료·노무·경비로 바꾸면 현재 단가 합계를 유지해 지정한 비목으로 옮깁니다. 비목별 구성으로 되돌릴 때 원천 단가의 합계가 현재 단가와 다르거나 원천이 불명확하면 임의로 복원하지 않고 오류를 표시합니다. 자동계산이 꺼져 있으면 금액은 자동 재계산을 켤 때까지 유지합니다.
|
||||
|
||||
인쇄 미리보기는 화면의 100행 페이지 제한 없이 검색 결과/현재 호표의 전체 행을 표시합니다. 출력 범위에서 현재 자료의 모든 호표·행도 선택할 수 있습니다. 원본 인쇄 옵션의 41개 항목 중 CAD를 제외한 40개 서식 이름을 선택하며, ESTX에서 가져온 해당 프로젝트의 인쇄 설정을 우선 사용합니다. 내역·원가·호표/총괄·증감·동원·수량·공정은 표 자료로 출력합니다. 표지·갑지·설명서·시방서·심사 문서는 서식별 본문을 직접 입력합니다. 용지·방향·여백·글자 크기·페이지당 행·시작 쪽·업체명을 저장하고 인쇄/PDF로 출력합니다. ESTX 수정 저장은 서식별 여백과 페이지당 행 수를 반영합니다. 다른 웹 인쇄 설정의 ESTX 쓰기와 원본 인쇄 도형·병합·글꼴 배치의 동등성은 지원하지 않습니다.
|
||||
|
||||
별도 인쇄 서식으로 기성·물가변동의 내역서와 원가계산서, 연차 배분표와 연차별 내역서, 설계변경 원가 증감표를 선택할 수 있습니다. 기성은 선택한 회차를 출력하고 확정된 회차는 확정 당시 결과를 사용합니다.
|
||||
|
||||
@@ -621,7 +621,9 @@ const EstActions = (() => {
|
||||
const previous = row.Code;
|
||||
EstModel.editCode(project, node.dataset.table, Number(node.dataset.row), value);
|
||||
if (state.group === previous && Number(row.Index) > 0) state.group = row.Code;
|
||||
} else row[key] = value;
|
||||
} else if (node.dataset.table === "EstMainDat" && key === "SCheck")
|
||||
EstBusinessActions.setEstimateClassification(project, row, node.dataset.row, value);
|
||||
else row[key] = value;
|
||||
if (state.view === "escalation" && EstMath.units.includes(key))
|
||||
project.options.SourceRefresh = false;
|
||||
if (["StuffcDat", "LaborcDat", "EstiMateDat", "SilJukDat"].includes(node.dataset.table)) {
|
||||
|
||||
@@ -1,4 +1,75 @@
|
||||
const EstBusinessActions = (() => {
|
||||
function setEstimateClassification(project, row, index, value) {
|
||||
index = Number(index);
|
||||
if (!Number.isInteger(index) || project.tables.EstMainDat[index] !== row)
|
||||
throw Error("내역 행을 다시 선택하세요.");
|
||||
const kind = Number(row.LinkIndex),
|
||||
table = EstNative.sourceTables[kind];
|
||||
if (
|
||||
!table ||
|
||||
Number(row.Level) ||
|
||||
[1, 3].includes(Number(row.CalcIndex)) ||
|
||||
![0, 1, 2, 3].includes(value)
|
||||
) {
|
||||
row.SCheck = value;
|
||||
return;
|
||||
}
|
||||
const total = EstMath.units.reduce((sum, key) => sum + EstMath.num(row[key]), 0);
|
||||
if (!Number.isFinite(total) || Math.abs(total) > Number.MAX_SAFE_INTEGER)
|
||||
throw Error("현재 단가 합계가 유효하지 않습니다.");
|
||||
if (value !== 0) {
|
||||
const target = { 1: "GU", 2: "NU", 3: "JU" }[value];
|
||||
for (const key of EstMath.units) row[key] = key === target ? total : 0;
|
||||
row.SCheck = value;
|
||||
return;
|
||||
}
|
||||
if (!Number(row.SCheck)) {
|
||||
row.SCheck = value;
|
||||
return;
|
||||
}
|
||||
const sources = project.tables[table] || [];
|
||||
let matches = row.Code ? sources.filter((source) => source.Code === row.Code) : [];
|
||||
if (!matches.length && row.Code)
|
||||
matches = sources.filter(
|
||||
(source) => Array.isArray(source.PreviousCodes) && source.PreviousCodes.includes(row.Code),
|
||||
);
|
||||
if (!matches.length && row.Name)
|
||||
matches = sources.filter(
|
||||
(source) =>
|
||||
EstNative.normalize(source.Name) === EstNative.normalize(row.Name) &&
|
||||
(!row.Size || EstNative.normalize(source.Size) === EstNative.normalize(row.Size)) &&
|
||||
(!row.Unite || EstNative.normalize(source.Unite) === EstNative.normalize(row.Unite)),
|
||||
);
|
||||
if (matches.length > 1) throw Error(`원천 코드가 중복되었습니다: ${row.Code}`);
|
||||
const had = Object.hasOwn(row, "SCheck"),
|
||||
before = row.SCheck;
|
||||
row.SCheck = value;
|
||||
try {
|
||||
let failures = null;
|
||||
const calculated = EstNative.calculate(project, {
|
||||
refreshEstimateRows: [index],
|
||||
onEstimateRefresh: (errors) => (failures = errors),
|
||||
});
|
||||
if (!failures) throw Error("내역 비목 재계산을 확인할 수 없습니다.");
|
||||
if (failures.length) throw Error(failures.join(" / "));
|
||||
const refreshed = calculated.tables.EstMainDat[index];
|
||||
const refreshedTotal = EstMath.units.reduce(
|
||||
(sum, key) => sum + EstMath.num(refreshed[key]),
|
||||
0,
|
||||
);
|
||||
if (
|
||||
!Number.isFinite(refreshedTotal) ||
|
||||
Math.abs(refreshedTotal - total) >
|
||||
Number.EPSILON * Math.max(1, Math.abs(total), Math.abs(refreshedTotal)) * 4
|
||||
)
|
||||
throw Error("현재 단가와 원천 단가가 달라 비목별 단가를 복원할 수 없습니다.");
|
||||
for (const key of EstMath.units) row[key] = refreshed[key];
|
||||
} catch (error) {
|
||||
if (had) row.SCheck = before;
|
||||
else delete row.SCheck;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
let purpose = "",
|
||||
documentKey;
|
||||
const currentRound = () =>
|
||||
@@ -364,5 +435,5 @@ const EstBusinessActions = (() => {
|
||||
}
|
||||
});
|
||||
}
|
||||
return { handle, change, install };
|
||||
return { handle, change, install, setEstimateClassification };
|
||||
})();
|
||||
|
||||
@@ -110,7 +110,7 @@ const EstNative = (() => {
|
||||
}
|
||||
return amount;
|
||||
}
|
||||
function calculate(input) {
|
||||
function calculate(input, request = {}) {
|
||||
const project = clone(input),
|
||||
tables = project.tables,
|
||||
options = project.options || {},
|
||||
@@ -251,7 +251,13 @@ const EstNative = (() => {
|
||||
"HeadTitleDat",
|
||||
[...heads.keys()].map((code) => completed.get(`103:${code}`)).filter(Boolean),
|
||||
);
|
||||
const estimate = EstRows.estimate(tables.EstMainDat, options, resolve);
|
||||
const estimate = EstRows.estimate(
|
||||
tables.EstMainDat,
|
||||
options,
|
||||
resolve,
|
||||
request.refreshEstimateRows,
|
||||
);
|
||||
request.onEstimateRefresh?.(estimate.refreshErrors);
|
||||
tables.EstMainDat = estimate.rows;
|
||||
errors.push(...estimate.errors);
|
||||
const direct = estimate.direct;
|
||||
|
||||
@@ -134,13 +134,14 @@ const EstRows = (() => {
|
||||
finish();
|
||||
return { rows: output, titles, errors };
|
||||
}
|
||||
function estimate(rows, options = {}, resolve = () => null) {
|
||||
function estimate(rows, options = {}, resolve = () => null, refreshRows = []) {
|
||||
const output = structuredClone(rows),
|
||||
errors = [];
|
||||
errors = [],
|
||||
refreshErrors = [];
|
||||
let segment = [],
|
||||
prior = [],
|
||||
boundary = false;
|
||||
for (const row of output) {
|
||||
for (const [index, row] of output.entries()) {
|
||||
const level = num(row.Level),
|
||||
link = num(row.LinkIndex);
|
||||
if (level > 0) {
|
||||
@@ -160,12 +161,15 @@ const EstRows = (() => {
|
||||
if (link === 57) procurement(row, segment);
|
||||
else if ([62, 67, 58].includes(link)) percent(row, segment, options.EstDigits || 0);
|
||||
else {
|
||||
if (options.SourceRefresh && ![1, 3].includes(num(row.CalcIndex))) {
|
||||
const requested = refreshRows.includes(index);
|
||||
if ((options.SourceRefresh || requested) && ![1, 3].includes(num(row.CalcIndex))) {
|
||||
try {
|
||||
const price = resolve(row);
|
||||
if (requested && !price) throw Error("원천 단가를 찾을 수 없습니다.");
|
||||
if (price) units.forEach((key, i) => (row[key] = num(price[components[i]])));
|
||||
} catch (error) {
|
||||
errors.push(`${row.Name}: ${error.message}`);
|
||||
if (requested) refreshErrors.push(error.message);
|
||||
}
|
||||
}
|
||||
amounts(row, options.EstDigits || 0, options.TotalFit);
|
||||
@@ -186,7 +190,9 @@ const EstRows = (() => {
|
||||
Object.assign(row, add(children));
|
||||
}
|
||||
const direct = output.find((r) => num(r.LinkIndex) === 54) || add(prior);
|
||||
return { rows: output, direct, errors };
|
||||
return refreshRows.length
|
||||
? { rows: output, direct, errors, refreshErrors }
|
||||
: { rows: output, direct, errors };
|
||||
}
|
||||
return { head, estimate, percent, procurement, subtotal, baseKind };
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user