feat(est): apply progress to schedule
This commit is contained in:
@@ -1,6 +1,29 @@
|
||||
from test_est_rea_parity import run
|
||||
|
||||
|
||||
def test_schedule_refreshes_progress_from_latest_locked_round_without_losing_dates():
|
||||
result = run(
|
||||
"math",
|
||||
"""(()=>{
|
||||
const p=EstNative.empty();p.tables.EstMainDat=[{PkID:1,Code:'A',GN:10},{PkID:2,Code:'B',GN:0}];
|
||||
p.schedule=[{id:'a',SourceKey:'id:1',Code:'A',Name:'work',start:'2026-01-01',end:'2026-02-01',weight:100,progress:10},
|
||||
{id:'b',SourceKey:'id:2',Code:'B',Name:'zero',start:'',end:'',weight:0,progress:40},
|
||||
{id:'c',Code:'X',Name:'manual',start:'',end:'',weight:10,progress:20}];
|
||||
p.progress=[{id:'one',locked:true,result:{rows:[{PkID:1,Code:'A',GN:10,TotalGN:2}]}},
|
||||
{id:'two',locked:true,result:{rows:[{PkID:1,Code:'A',GN:10,TotalGN:3},{PkID:2,Code:'B',GN:0,TotalGN:0}]}}];
|
||||
const before=JSON.stringify(p), updated=EstSchedule.refreshProgress(p);let bad=0;
|
||||
p.progress[1].result.rows[0].TotalGN=11;try{EstSchedule.refreshProgress(p)}catch{bad++}
|
||||
p.progress=[];try{EstSchedule.refreshProgress(p)}catch{bad++}
|
||||
return {updated,bad,unchanged:JSON.parse(before).schedule};
|
||||
})()""",
|
||||
)
|
||||
assert [row["progress"] for row in result["updated"]] == [30, 40, 20]
|
||||
assert result["updated"][0]["start"] == "2026-01-01"
|
||||
assert result["updated"][0]["weight"] == 100
|
||||
assert result["unchanged"][0]["progress"] == 10
|
||||
assert result["bad"] == 2
|
||||
|
||||
|
||||
def test_annual_print_contains_each_stage_and_calculation_errors():
|
||||
result = run(
|
||||
"math",
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
- **JDL 기본 표**: 원본 검사 DLL의 필드 정의와 EST 처리기의 TAB/ENQ·CP949 표 구조에 따라 ZIP 안의 Global.ini·Main.txt·자원/공종/내역/원가 표를 생성·읽습니다. 일위대가 상세는 기본 플래그와 연결 자원이 확인된 행을 저장 금액과 함께 불러오며 특수 행은 원문에 보존하고 경고합니다. 자재·노무로 구성된 기본 일위대가는 상세표로 내보냅니다. 가져온 작업을 변경하지 않았다면 원본 JDL을 바이트 그대로 내보내 확장 자료를 보존합니다. 변경된 작업의 특수 일위·산근·기계는 저장 금액의 고정 단가로 전개하며 제외 행은 생략 사실을 알립니다. 확장 산식·기성/물가 파일의 완전 변환과 원본 검사 DLL 인증은 미확인입니다.
|
||||
- **원본 구조 유지 ESTX 수정 저장**: 가져온 파일의 공사명·계산 옵션과 행 수 및 인덱스 키가 유지된 단가·수량·문구·금액, 기존 BLOB과 원가 서식을 기록하고 재읽기로 검산합니다. 수량산출서는 기존 서식 구간 안의 텍스트 편집을 저장하며 표지와 문서 구조는 보존합니다. 한 줄에서 표지로 구분된 여러 구간을 함께 수정하거나 연속 글자 서식에 걸친 텍스트를 수정할 수도 있습니다. 새 텍스트에는 변경 시작 위치의 서식이 적용됩니다. 원본 결과가 기존 계산과 모두 일치하면 수정된 계산 결과 표지와 수량 목록을 함께 갱신합니다. 불일치나 계산 오류가 있으면 저장을 거부합니다. 기존 BLOB은 공간이 부족하면 블록을 추가합니다. 원본 파일을 덮어쓰지 않으며 `.idx`와 CAD 바이너리를 보존합니다. 새 문서, 행 추가·삭제·인덱스 키 변경, 표지나 위첨자 등 복잡한 구조를 가로지르는 편집과 목록·연결 변경, 웹 기성·연차·당초 사본은 지원하지 않아 저장을 거부합니다. 이 범위는 JSON으로 보관합니다. 원본 EST 실행 검증은 못했습니다.
|
||||
- **예정공정표**: 내역 행을 공정으로 일괄 추가하고 내역 금액으로 가중치를 갱신할 수 있습니다. 코드가 없거나 같은 코드가 반복된 행은 내역의 행 식별자로 구분합니다. 기존 공정의 기간·진도·이름은 유지합니다. 공정별 기간 막대와 입력 진도의 가중평균을 표시하며, 날짜 역전·잘못된 진도를 차단합니다. 공휴일/공기 산정 기준을 임의 적용하지 않습니다.
|
||||
- **기성 진도 반영**: 예정공정표에서 최근 확정 회차의 누계 수량을 해당 회차 계약 수량으로 나누어 연결된 공정의 진도를 갱신할 수 있습니다. 수량이 0이거나 기성 내역에 연결되지 않은 공정은 기존 진도를 유지합니다. 0~100%를 벗어나는 결과는 갱신을 중단합니다.
|
||||
- **공종별 동원 출력**: 한 번의 원천 계산에서 공종별 인원·자재·장비·실적·견적을 나누며 전체 합계와 금액/수량을 대조합니다. 문서 본문과 인쇄 설정도 저장·복원·취소 대상입니다.
|
||||
- **계약심사**: 작업모드의 계약심사 화면에서 심사 수량과 비목별 단가·심사 사유를 직접 입력하고 기존액·심사액·차액을 비교합니다. 고정금액 행은 비목별 금액을 입력하며, 원가계산서도 다시 계산합니다. 항목별 입력을 초기화하거나 실행 취소할 수 있습니다. 심사 입력은 JSON에 저장하고, 확인한 결과는 원래 작업과 별도인 심사 내역 사본으로 열 수 있습니다. 심사결과요약서에는 내역별 비교·사유와 도급액 차액을 출력합니다. 심사율이나 법적 기준은 자동으로 정하지 않습니다.
|
||||
|
||||
|
||||
@@ -109,24 +109,53 @@ const EstSchedule = (() => {
|
||||
return { ...row, weight: key ? Math.max(0, Number(rows.get(key).TC) || 0) : row.weight };
|
||||
});
|
||||
}
|
||||
function refreshProgress(project) {
|
||||
const round = (project.progress || []).findLast((item) => item.locked && item.result);
|
||||
if (!round) throw Error("확정된 기성 회차가 없습니다.");
|
||||
const { sourceKey } = estimateRows(project),
|
||||
saved = new Map();
|
||||
for (const row of round.result.rows) {
|
||||
if (
|
||||
Number(row.Level) > 0 ||
|
||||
Number(row.CalcIndex) >= 2 ||
|
||||
[54, 55, 58, 59].includes(Number(row.LinkIndex))
|
||||
)
|
||||
continue;
|
||||
const key = row.PkID != null ? `id:${row.PkID}` : row.Code ? `code:${row.Code}` : "";
|
||||
if (!key) continue;
|
||||
if (saved.has(key)) throw Error(`기성 내역 식별자 중복: ${key}`);
|
||||
saved.set(key, row);
|
||||
}
|
||||
return (project.schedule || []).map((entry) => {
|
||||
const row = saved.get(sourceKey(entry));
|
||||
if (!row || !Number(row.GN)) return { ...entry };
|
||||
const progress = (Number(row.TotalGN) / Number(row.GN)) * 100;
|
||||
if (!Number.isFinite(progress) || progress < 0 || progress > 100)
|
||||
throw Error(`${entry.Name || entry.Code}: 기성 누계 진도가 0~100% 범위를 벗어났습니다.`);
|
||||
return { ...entry, progress };
|
||||
});
|
||||
}
|
||||
function render() {
|
||||
const { esc, number, state } = EstApp,
|
||||
output = calculate(state.project);
|
||||
EstApp.byId("work-content").innerHTML =
|
||||
`<div class="business-tools"><button data-action="schedule-add">공정 추가</button><button data-action="schedule-import">내역 일괄 추가</button><button data-action="schedule-weights">내역 금액으로 가중치 갱신</button><span>시작 ${esc(output.start)} · 종료 ${esc(output.end)} · 가중치 합계 ${number(output.weight)} · 입력 진도 가중평균 ${number(output.progress)}%</span></div><p>기간과 진도는 직접 입력합니다. 가중치는 공종 금액 또는 사용자가 정한 값을 사용합니다.</p><table class="sheet"><thead><tr>${["코드", "공정명", "시작일", "종료일", "가중치", "진도(%)", "기간", "작업"].map((h) => `<th>${h}</th>`).join("")}</tr></thead><tbody>${output.rows.map((r, i) => `<tr>${["Code", "Name", "start", "end", "weight", "progress"].map((k) => `<td><input data-schedule="${k}" data-index="${i}" type="${["start", "end"].includes(k) ? "date" : ["weight", "progress"].includes(k) ? "number" : "text"}" value="${esc(r[k])}" step="any"></td>`).join("")}<td style="min-width:180px"><div style="height:12px;background:#ddd"><div style="margin-left:${r.left}%;width:${r.width}%;height:12px;background:#316ea5"></div></div></td><td><button data-action="schedule-delete" data-index="${i}">삭제</button></td></tr>`).join("")}</tbody></table>`;
|
||||
`<div class="business-tools"><button data-action="schedule-add">공정 추가</button><button data-action="schedule-import">내역 일괄 추가</button><button data-action="schedule-weights">내역 금액으로 가중치 갱신</button><button data-action="schedule-progress">최근 확정 기성으로 진도 갱신</button><span>시작 ${esc(output.start)} · 종료 ${esc(output.end)} · 가중치 합계 ${number(output.weight)} · 입력 진도 가중평균 ${number(output.progress)}%</span></div><p>기간과 진도는 직접 입력합니다. 가중치는 공종 금액 또는 사용자가 정한 값을 사용합니다.</p><table class="sheet"><thead><tr>${["코드", "공정명", "시작일", "종료일", "가중치", "진도(%)", "기간", "작업"].map((h) => `<th>${h}</th>`).join("")}</tr></thead><tbody>${output.rows.map((r, i) => `<tr>${["Code", "Name", "start", "end", "weight", "progress"].map((k) => `<td><input data-schedule="${k}" data-index="${i}" type="${["start", "end"].includes(k) ? "date" : ["weight", "progress"].includes(k) ? "number" : "text"}" value="${esc(r[k])}" step="any"></td>`).join("")}<td style="min-width:180px"><div style="height:12px;background:#ddd"><div style="margin-left:${r.left}%;width:${r.width}%;height:12px;background:#316ea5"></div></div></td><td><button data-action="schedule-delete" data-index="${i}">삭제</button></td></tr>`).join("")}</tbody></table>`;
|
||||
}
|
||||
function install() {
|
||||
document.addEventListener("click", (event) => {
|
||||
const node = event.target.closest("[data-action]");
|
||||
if (!node) return;
|
||||
const p = EstApp.state.project;
|
||||
if (["schedule-import", "schedule-weights"].includes(node.dataset.action)) {
|
||||
if (
|
||||
["schedule-import", "schedule-weights", "schedule-progress"].includes(node.dataset.action)
|
||||
) {
|
||||
try {
|
||||
if (node.dataset.action === "schedule-import") {
|
||||
const added = fromEstimate(p, () => crypto.randomUUID());
|
||||
EstApp.change(() => (p.schedule ||= []).push(...added), false);
|
||||
} else {
|
||||
const rows = refreshWeights(p);
|
||||
const rows =
|
||||
node.dataset.action === "schedule-progress" ? refreshProgress(p) : refreshWeights(p);
|
||||
EstApp.change(() => (p.schedule = rows), false);
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -172,5 +201,5 @@ const EstSchedule = (() => {
|
||||
}
|
||||
});
|
||||
}
|
||||
return { validate, calculate, fromEstimate, refreshWeights, render, install };
|
||||
return { validate, calculate, fromEstimate, refreshWeights, refreshProgress, render, install };
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user