Merge remote-tracking branch 'origin/CODEX' into sub_laptop_8

This commit is contained in:
2026-11-08 15:02:17 +09:00
7 changed files with 1285 additions and 57 deletions
@@ -10,6 +10,23 @@ def test_new_progress_name_avoids_existing_names_with_whitespace():
assert result == ["3회 기성", "1회 기성", "1회 기성"]
def test_progress_cannot_lock_when_previous_result_is_missing():
result = run(
"math",
"""(()=>{
const p=EstNative.empty();
p.tables.EstMainDat=[{PkID:1,Code:'R',GN:10,GU:100},{LinkIndex:54}];
p.progress=[{id:'one',locked:true,inputs:{}},{id:'two',inputs:{'1':{Gum:2}}}];
const calculated=EstProgress.calculate(p,'two');
let rejected=false;
try { EstProgress.lock(p,'two'); } catch { rejected=true; }
return {errors:calculated.errors,rejected};
})()""",
)
assert any("전회 기성 결과가 없습니다" in error for error in result["errors"])
assert result["rejected"]
def test_progress_rejects_ambiguous_inputs_without_applying_them_to_multiple_rows():
result = run(
"math",
+750 -24
View File
@@ -31,6 +31,7 @@ def run_js(code: str) -> dict:
const files = [
'tools/est-test/native-math.js',
'tools/est-test/native-formula.js',
'tools/est-test/native-model.js',
'tools/est-test/native-text.js',
'tools/est-test/native-machine.js',
@@ -663,12 +664,18 @@ def test_align_wonga_total_action():
const p = EstNative.empty();
p.tables.Wonga = [
{ Gubun: "A", Name: "직접공사비", Cost: 1000000, LinkIndex: 0, CalcIndex: 0 },
{ Gubun: "B", Name: "일반관리비", Cost: 50000, LinkIndex: 0, CalcIndex: 0 },
{ Gubun: "C", Name: "이윤", Cost: 78564, LinkIndex: 0, CalcIndex: 0 },
{ Gubun: "D", Name: "부가가치세", Cost: 112856, LinkIndex: 0, CalcIndex: 0 },
{ Gubun: "E", Name: "총도급공사비", Cost: 1241420, LinkIndex: 55, CalcIndex: 0 }
{ Gubun: "B", Name: "간접노무비", Cost: 0, VSusic: "A * 0.1", LinkIndex: 0, CalcIndex: 0 },
{ Gubun: "C", Name: "이윤", Cost: 0, VSusic: "(A + B) * 0.1", LinkIndex: 0, CalcIndex: 0 },
{ Gubun: "D", Name: "부가가치세", Cost: 0, VSusic: "(A + B + C) * 0.1", LinkIndex: 0, CalcIndex: 0 },
{ Gubun: "E", Name: "총도급공사비", Cost: 0, VSusic: "A + B + C + D", LinkIndex: 55, CalcIndex: 0 }
];
// 1) 당초 일반 계산: 직접공사비 1,000,000 기준 -> 총도급 1,331,000원, 이윤 110,000원, 부가세 121,000원
const initialCalc = EstWonga.calculate(p.tables.Wonga, { TotalFix: false }, {});
const initialTotal = initialCalc.rows[4].Cost;
const initialProfit = initialCalc.rows[2].Cost;
p.tables.Wonga = initialCalc.rows;
const state = {
project: p,
view: "Wonga",
@@ -677,19 +684,34 @@ def test_align_wonga_total_action():
};
globalThis.EstApp = {
state,
change(fn) { fn(); },
change(fn) {
fn();
// EstApp.change recalculates via EstNative.calculate / EstWonga.calculate
const recalc = EstWonga.calculate(p.tables.Wonga, p.options, {});
p.tables.Wonga = recalc.rows;
},
render() {}
};
// 1) 1,000원 단위(unitDigits: 3) 절하 맞춤 테스트
const alignRes1000 = EstWonga.alignTotal(p.tables.Wonga, 1241420, 3);
const total1000 = p.tables.Wonga[alignRes1000.targetIndex].Cost;
// 2) 10,000원 단위(unitDigits: 4) 역산 맞춤: 1,331,000 -> 1,330,000원으로 절하 맞춤
const alignRes = EstWonga.alignTotal(p.tables.Wonga, 1331000, 4, p.options, {});
p.options.TotalFix = true;
// 2) 10,000원 단위(unitDigits: 4) 절하 맞춤 테스트
const alignRes10000 = EstWonga.alignTotal(p.tables.Wonga, 1241420, 4);
const total10000 = p.tables.Wonga[alignRes10000.targetIndex].Cost;
const alignedTotal = p.tables.Wonga[alignRes.targetIndex].Cost;
const alignedProfit = p.tables.Wonga[alignRes.profitIndex].Cost;
const alignedVat = p.tables.Wonga[3].Cost;
// 전체 명세 합산 일치 검증: 직접(1,000,000) + 간접노무(100,000) + 이윤(109,091) + 부가세(120,909) === 1,330,000
const totalMatch = p.tables.Wonga[0].Cost + p.tables.Wonga[1].Cost + p.tables.Wonga[2].Cost + p.tables.Wonga[3].Cost === alignedTotal;
// 3) LinkIndex 55 부재 시 거부 확인
// 3) JSON 직렬화 & 역직렬화 (저장 재열기) 후 재계산 일관성 검증
const jsonStr = JSON.stringify(p);
const reloadedP = JSON.parse(jsonStr);
const reloadCalc = EstWonga.calculate(reloadedP.tables.Wonga, reloadedP.options, {});
const reloadedTotal = reloadCalc.rows[4].Cost;
const reloadedProfit = reloadCalc.rows[2].Cost;
const reloadedVat = reloadCalc.rows[3].Cost;
// 4) LinkIndex 55 부재 시 거부 확인
let noTargetRejected = false;
try {
EstWonga.alignTotal([{ Name: "항목1", Cost: 100 }], 1000, 3);
@@ -697,7 +719,7 @@ def test_align_wonga_total_action():
noTargetRejected = true;
}
// 4) 이윤 행 부재 시 거부 확인
// 5) 이윤 행 부재 시 거부 확인
let noProfitRejected = false;
try {
EstWonga.alignTotal([{ Name: "총도급", Cost: 100, LinkIndex: 55 }], 1000, 3);
@@ -705,24 +727,728 @@ def test_align_wonga_total_action():
noProfitRejected = true;
}
// 6) 계산 실패 시 기존 rows 상태 보존 검증: 순환 참조 수식 원가표
const circularRows = [
{ Gubun: "01", Name: "순공사비", LinkIndex: 54, CalcIndex: 0, Cost: 1000000 },
{ Gubun: "02", Name: "이윤", LinkIndex: 0, CalcIndex: 0, VSusic: "[부가가치세] * 0.1", Cost: 100000 },
{ Gubun: "03", Name: "부가가치세", LinkIndex: 0, CalcIndex: 0, VSusic: "[이윤] * 0.1", Cost: 10000 },
{ Gubun: "04", Name: "총도급공사비", LinkIndex: 55, CalcIndex: 0, VSusic: "[순공사비] + [이윤] + [부가가치세]", Cost: 1110000 }
];
const origCircularCost = circularRows[3].Cost;
let circularErrorCaught = false;
try {
EstWonga.alignTotal(circularRows, 1100000, 3);
} catch {
circularErrorCaught = true;
}
const circularCostPreserved = circularRows[3].Cost === origCircularCost;
return {
total1000,
total10000,
roundedTarget1000: alignRes1000.roundedTarget,
roundedTarget10000: alignRes10000.roundedTarget,
profitIndex: alignRes1000.profitIndex,
initialTotal,
initialProfit,
alignedTotal,
alignedProfit,
alignedVat,
totalMatch,
reloadedTotal,
reloadedProfit,
reloadedVat,
noTargetRejected,
noProfitRejected
noProfitRejected,
circularErrorCaught,
circularCostPreserved
};
})()"""
result = run_js(code)
assert result["total1000"] == 1241000
assert result["roundedTarget1000"] == 1241000
assert result["total10000"] == 1240000
assert result["roundedTarget10000"] == 1240000
assert result["profitIndex"] == 2
assert result["initialTotal"] == 1331000
assert result["initialProfit"] == 110000
assert result["alignedTotal"] == 1330000
assert result["alignedProfit"] == 109091
assert result["alignedVat"] == 120909
assert result["totalMatch"] is True
assert result["reloadedTotal"] == 1330000
assert result["reloadedProfit"] == 109091
assert result["reloadedVat"] == 120909
assert result["noTargetRejected"] is True
assert result["noProfitRejected"] is True
assert result["circularErrorCaught"] is True
assert result["circularCostPreserved"] is True
def test_level_batch_adjust_actions():
code = """(() => {
const p = EstNative.empty();
p.tables.EstMainDat = [
{ Index: 0, LineData: "공종 1", Level: 1 },
{ Index: 0, LineData: "항목 1-1", Level: 2 },
{ Index: 0, LineData: "항목 1-2", Level: 2 },
{ Index: 0, LineData: "공종 2", Level: 0 },
{ Index: 0, LineData: "공종 9", Level: 9 }
];
const state = {
project: p,
view: "EstMainDat",
selection: new Set([0, 1]),
notice: ""
};
globalThis.EstApp = {
state,
change(fn) { fn(); },
render() {}
};
// 1) 레벨 일괄 증가: 0번행(1->2), 1번행(2->3)
EstBlockActions.adjustLevels(state, 1);
const afterUp0 = p.tables.EstMainDat[0].Level;
const afterUp1 = p.tables.EstMainDat[1].Level;
const upNotice = state.notice;
// 2) 레벨 일괄 감소: 0번행(2->1), 1번행(3->2)
EstBlockActions.adjustLevels(state, -1);
const afterDown0 = p.tables.EstMainDat[0].Level;
const afterDown1 = p.tables.EstMainDat[1].Level;
// 3) 상한(9) 및 하한(0) 클램프 검증:
// 3번행(0) 감소 시 0 유지, 4번행(9) 증가 시 9 유지
state.selection = new Set([3, 4]);
EstBlockActions.adjustLevels(state, -1);
const clampMin = p.tables.EstMainDat[3].Level; // 0 유지
EstBlockActions.adjustLevels(state, 1);
const clampMax = p.tables.EstMainDat[4].Level; // 9 유지
// 4) 지원되지 않는 화면(자재, 노임 등)에서 호출 시 안전 거부
state.view = "StuffcDat";
let viewRejected = false;
try {
EstBlockActions.adjustLevels(state, 1);
} catch {
viewRejected = true;
}
// 5) 선택 행 없을 시 거부
state.view = "EstMainDat";
state.selection = new Set([]);
let emptyRejected = false;
try {
EstBlockActions.adjustLevels(state, 1);
} catch {
emptyRejected = true;
}
return {
afterUp0,
afterUp1,
upNotice,
afterDown0,
afterDown1,
clampMin,
clampMax,
viewRejected,
emptyRejected
};
})()"""
result = run_js(code)
assert result["afterUp0"] == 2
assert result["afterUp1"] == 3
assert "2개 행의 레벨이 일괄 증가" in result["upNotice"]
assert result["afterDown0"] == 1
assert result["afterDown1"] == 2
assert result["clampMin"] == 0
assert result["clampMax"] == 9
assert result["viewRejected"] is True
assert result["emptyRejected"] is True
def test_reset_change_action():
code = """(() => {
const p = EstNative.empty();
p.tables.EstMainDat = [
{ Code: "A001", Name: "토공사", GN: 100, TU: 5000, TC: 500000, Level: 0, LinkIndex: 0, CalcIndex: 0 },
{ Code: "A002", Name: "구조물공사", GN: 50, TU: 10000, TC: 500000, Level: 0, LinkIndex: 0, CalcIndex: 0, _origLinkIndex: 21, _prevSCheck: 1 }
];
// 타 업무(수량 연결 등) 및 원본 필드 모의
p.quantities = [
{ id: "Q1", sheet: "토공", items: [{ code: "A001", qty: 100 }] }
];
// 1) baseline 부재 시 안전 차단 검증
const state = {
project: p,
view: "EstMainDat",
selection: new Set([0]),
notice: ""
};
globalThis.confirm = () => true;
globalThis.EstApp = {
state,
history: [],
future: [],
change(fn) {
// undo history 기록 모의
this.history.push(JSON.stringify(state.project));
fn();
},
render() {}
};
let rejectBeforeBaseline = false;
try {
EstBlockActions.resetChangeAction(state);
} catch {
rejectBeforeBaseline = true;
}
// 2) 당초 확정 (baseline 생성 및 Mode = "설계변경", _ 필드 보존)
state.project = EstBusiness.baseline(state.project);
// 3) 설계변경 작업 수행: 기존 행 변경, 신규 행 추가, 기존 행 삭제, 타 업무 필드 보존 확인
state.project.tables.EstMainDat[0].GN = 150;
state.project.tables.EstMainDat[0].TC = 750000;
// 신규 행 추가
state.project.tables.EstMainDat.push({
Code: "A003",
Name: "신규공종",
GN: 20,
TU: 3000,
TC: 60000,
Level: 0,
LinkIndex: 0,
CalcIndex: 0,
NewItem: true
});
// 1번 행(_origLinkIndex, _prevSCheck 포함)의 변경
state.project.tables.EstMainDat[1].GN = 80;
// 3-1) 신규 행을 참조하는 수량 연결(quantities)이 존재할 때 초기화 차단 검증
state.project.quantities.push({
Code: "Q2",
TargetCode: "A003", // 신규 행 참조
GN: 20
});
let brokenQtyLinkRejected = false;
try {
EstBlockActions.resetChangeAction(state);
} catch (e) {
if (e.message.includes("수량산출 연결 무결성 오류")) {
brokenQtyLinkRejected = true;
}
}
// 차단 확인 후 정상 복원 테스트를 위해 신규 연결 해제
state.project.quantities.pop();
// 3-2) Excel 수량 연결 동일성(Identity) 입증 검증:
// PkID 1(토공사)에 Excel 링크 연결
state.project.tables.EstMainDat[0].PkID = 1;
state.project.baseline.tables.EstMainDat[0].PkID = 1;
state.project.tables.EstMainDat[1].PkID = 2;
state.project.baseline.tables.EstMainDat[1].PkID = 2;
state.project.quantities.push({ TargetKey: "id:999", TargetCode: "A001", GN: 1 });
let missingKeyRejected = false;
try {
EstBlockActions.resetChangeAction(state);
} catch (e) {
missingKeyRejected = e.message.includes("수량산출 연결 무결성 오류");
}
state.project.quantities.pop();
state.project.baseline.tables.EstMainDat.push({ Code: "A001", Level: 0 });
state.project.quantities.push({ TargetCode: "A001", GN: 1 });
let duplicateCodeRejected = false;
try {
EstBlockActions.resetChangeAction(state);
} catch (e) {
duplicateCodeRejected = e.message.includes("수량산출 연결 무결성 오류");
}
state.project.quantities.pop();
state.project.baseline.tables.EstMainDat.pop();
if (!missingKeyRejected || !duplicateCodeRejected) throw Error("수량 연결 보호 실패");
state.project.excelQuantityLinks = [
{ target: 1, file: "calc.xlsx", sheet: "Sheet1", cell: "B10" }
];
// 중간 행 삽입, 삭제, 재정렬 및 중복 코드 상황 모의:
// 신규 행(중복 코드 A001를 가진 다른 항목)을 앞에 삽입해 인덱스 순서 변경
state.project.tables.EstMainDat.unshift({
Code: "A001", // 중복 코드
Name: "중복코드 신규토공",
GN: 10,
TU: 2000,
TC: 20000,
Level: 0,
LinkIndex: 0,
CalcIndex: 0,
PkID: 99,
NewItem: true
});
// PkID 1 행은 이제 1번 인덱스로 밀림. PkID 1의 내용(토공사)은 baseline의 PkID 1(토공사)과 일치함.
// 하지만 Excel 링크가 신규 PkID 99를 가리키도록 변조되었을 때 차단되는지 확인:
state.project.excelQuantityLinks[0].target = 99;
let brokenExcelLinkRejected = false;
try {
EstBlockActions.resetChangeAction(state);
} catch (e) {
if (e.message.includes("Excel 수량 연결 무결성 오류")) {
brokenExcelLinkRejected = true;
}
}
// 동일성 위조 검증: PkID 1을 가리키지만 품명이 변조되어 당초 행과 동일성이 깨진 경우 차단
state.project.excelQuantityLinks[0].target = 1;
const originalName = state.project.tables.EstMainDat[1].Name;
state.project.tables.EstMainDat[1].Name = "변조된 다른항목명";
let identityMismatchRejected = false;
try {
EstBlockActions.resetChangeAction(state);
} catch (e) {
if (e.message.includes("동일성이 입증되지 않아 초기화를 차단")) {
identityMismatchRejected = true;
}
}
// 원래 이름으로 복구하여 정상 연결 상태 복원
state.project.tables.EstMainDat[1].Name = originalName;
// 신규 삽입 행 제거하여 원래 상태 맞춤
state.project.tables.EstMainDat.shift();
// 4) JSON 직렬화 & 역직렬화 (저장 재열기 왕복)
const savedJson = JSON.stringify(state.project);
state.project = JSON.parse(savedJson);
// 5) 설계변경 초기화 (resetChangeAction) 실행
EstBlockActions.resetChangeAction(state);
// 신규 행이 삭제되고 원래 2개 행으로 완전 복원되었는지 확인
const rowCountRestored = state.project.tables.EstMainDat.length === 2;
const restoredGN0 = state.project.tables.EstMainDat[0].GN;
const restoredTC0 = state.project.tables.EstMainDat[0].TC;
const restoredGN1 = state.project.tables.EstMainDat[1].GN;
const restoredTC1 = state.project.tables.EstMainDat[1].TC;
const baselineCleared = state.project.baseline === undefined;
const modeRestored = state.project.options.Mode === "내역서";
// 설계변경 _ 필드는 삭제되었지만, 타 업무 필드(_origLinkIndex, _prevSCheck) 및 quantities는 완전 보존되었는지 검증
const hasBaselineUnderscore = Object.keys(state.project.tables.EstMainDat[0]).some(k => k === "GN_" || k === "TC_");
const preservedOrigLink = state.project.tables.EstMainDat[1]._origLinkIndex === 21;
const preservedPrevSCheck = state.project.tables.EstMainDat[1]._prevSCheck === 1;
const quantitiesPreserved = state.project.quantities.length === 1 && state.project.quantities[0].id === "Q1";
// 6) Undo (실행 취소) 검증: 이전 변경 상태로 롤백 가능 확인
const canUndo = EstApp.history.length > 0;
const lastSnapshot = JSON.parse(EstApp.history.pop());
const undoGN0 = lastSnapshot.tables.EstMainDat[0].GN; // 변경 전의 150 확인
return {
rejectBeforeBaseline,
rowCountRestored,
restoredGN0,
restoredTC0,
restoredGN1,
restoredTC1,
baselineCleared,
modeRestored,
hasBaselineUnderscore,
preservedOrigLink,
preservedPrevSCheck,
quantitiesPreserved,
canUndo,
undoGN0,
brokenQtyLinkRejected,
brokenExcelLinkRejected,
identityMismatchRejected
};
})()"""
result = run_js(code)
assert result["rejectBeforeBaseline"] is True
assert result["rowCountRestored"] is True
assert result["restoredGN0"] == 100
assert result["restoredTC0"] == 500000
assert result["restoredGN1"] == 50
assert result["restoredTC1"] == 500000
assert result["baselineCleared"] is True
assert result["modeRestored"] is True
assert result["hasBaselineUnderscore"] is False
assert result["preservedOrigLink"] is True
assert result["preservedPrevSCheck"] is True
assert result["quantitiesPreserved"] is True
assert result["canUndo"] is True
assert result["undoGN0"] == 150
assert result["brokenQtyLinkRejected"] is True
assert result["brokenExcelLinkRejected"] is True
assert result["identityMismatchRejected"] is True
def test_reset_wonga_action():
code = """(() => {
const p = EstNative.empty();
p.tables.Wonga = [
{ Gubun: "01", Name: "순공사비", LinkIndex: 54, CalcIndex: 0, Cost: 1000000, Cost_: 1000000 },
{ Gubun: "02", Name: "일반관리비", LinkIndex: 0, CalcIndex: 0, VSusic: "[순공사비] * 0.05", VSusic_: "[순공사비] * 0.05", Cost: 50000, Cost_: 50000 },
{ Gubun: "03", Name: "이윤", LinkIndex: 0, CalcIndex: 0, VSusic: "([순공사비] + [일반관리비]) * 0.1", VSusic_: "([순공사비] + [일반관리비]) * 0.1", Cost: 105000, Cost_: 105000 },
{ Gubun: "04", Name: "부가가치세", LinkIndex: 0, CalcIndex: 0, VSusic: "([순공사비] + [일반관리비] + [이윤]) * 0.1", VSusic_: "([순공사비] + [일반관리비] + [이윤]) * 0.1", Cost: 115500, Cost_: 115500 },
{ Gubun: "05", Name: "총도급공사비", LinkIndex: 55, CalcIndex: 0, VSusic: "[순공사비] + [일반관리비] + [이윤] + [부가가치세]", Cost: 1270500, Cost_: 1270500 }
];
const state = {
project: p,
view: "Wonga",
selection: new Set([4]),
notice: ""
};
globalThis.confirm = () => true;
globalThis.EstApp = {
state,
history: [],
change(fn) {
this.history.push(JSON.stringify(state.project));
fn();
const recalc = EstWonga.calculate(p.tables.Wonga, p.options, {});
p.tables.Wonga = recalc.rows;
},
render() {}
};
// 1) 1,250,000원으로 공사비역산 실행 (TotalFix = true, 이윤 및 부가세 수렴 변경)
EstWonga.alignTotal(p.tables.Wonga, 1250000, 3, p.options, {});
p.options.TotalFix = true;
const alignedTotal = p.tables.Wonga[4].Cost;
const alignedProfit = p.tables.Wonga[2].Cost;
// 2) JSON 직렬화 & 역직렬화 (저장 재열기 왕복)
const savedJson = JSON.stringify(p);
state.project = JSON.parse(savedJson);
// 3) 원가계산서 항목 초기화 (resetWongaAction) 실행
EstBlockActions.resetWongaAction(state);
const restoredTotal = state.project.tables.Wonga[4].Cost;
const restoredProfit = state.project.tables.Wonga[2].Cost;
const totalFixCleared = state.project.options.TotalFix === false;
// 4) handle("reset-wonga") 연동 검증
// 다시 alignTotal 후 handle로 초기화
EstWonga.alignTotal(state.project.tables.Wonga, 1240000, 3, state.project.options, {});
state.project.options.TotalFix = true;
const handled = EstBlockActions.handle("reset-wonga", state);
const handledTotal = state.project.tables.Wonga[4].Cost;
return {
alignedTotal,
alignedProfit,
restoredTotal,
restoredProfit,
totalFixCleared,
handled,
handledTotal
};
})()"""
result = run_js(code)
assert result["alignedTotal"] == 1250000
assert result["alignedProfit"] == 86364
assert result["restoredTotal"] == 1270500
assert result["restoredProfit"] == 105000
assert result["totalFixCleared"] is True
assert result["handled"] is True
assert result["handledTotal"] == 1270500
def test_reset_progress_inputs_action():
code = """(() => {
const p = EstNative.empty();
p.tables.EstMainDat = [
{ Code: "A001", Name: "토공사", GN: 100, TU: 5000, TC: 500000, NC: 200000, GC: 200000, JC: 100000, SC: 0, Level: 0, LinkIndex: 0, CalcIndex: 0 }
];
p.progress = [
{
id: "R1",
name: "1회 기성",
locked: false,
approved: false,
inputs: {
"A001": { GumMode: "quantity", Gum: 40, SaMode: "quantity", Sa: 35 }
}
},
{
id: "R2",
name: "2회 기성",
locked: true,
approved: false,
inputs: {
"A001": { GumMode: "quantity", Gum: 30 }
}
}
];
const state = {
project: p,
view: "progress",
roundId: "R1",
selection: new Set([0]),
notice: ""
};
globalThis.confirm = () => true;
globalThis.EstApp = {
state,
history: [],
change(fn) {
this.history.push(JSON.stringify(state.project));
fn();
},
render() {}
};
// 1) 미확정 R1 회차의 입력 존재 확인
const hasInputsBefore = Object.keys(state.project.progress[0].inputs).length > 0;
// 2) JSON 직렬화 & 역직렬화 (저장 재열기 왕복)
const savedJson = JSON.stringify(state.project);
state.project = JSON.parse(savedJson);
// 3) 회차 입력 초기화 실행
EstBlockActions.resetProgressInputsAction(state);
const inputsClearedAfter = Object.keys(state.project.progress[0].inputs).length === 0;
// 4) 확정된 R2 회차 초기화 시도 시 안전 차단 확인
state.roundId = "R2";
let lockedRoundRejected = false;
try {
EstBlockActions.resetProgressInputsAction(state);
} catch {
lockedRoundRejected = true;
}
const lockedInputsPreserved = Object.keys(state.project.progress[1].inputs).length > 0;
state.roundId = "missing";
state.project.progress[0].inputs = { "A001": { Gum: 42 } };
let missingRoundRejected = false;
try {
EstBlockActions.resetProgressInputsAction(state);
} catch {
missingRoundRejected = true;
}
const missingRoundPreserved = state.project.progress[0].inputs.A001.Gum === 42;
// 5) handle("reset-round-inputs") 연동 검증
state.roundId = "R1";
state.project.progress[0].inputs = { "A001": { Gum: 50 } };
const handled = EstBlockActions.handle("reset-round-inputs", state);
const handledInputsCleared = Object.keys(state.project.progress[0].inputs).length === 0;
// 6) 실행 취소(Undo) 가능 여부 확인
const canUndo = EstApp.history.length > 0;
return {
hasInputsBefore,
inputsClearedAfter,
lockedRoundRejected,
lockedInputsPreserved,
missingRoundRejected,
missingRoundPreserved,
handled,
handledInputsCleared,
canUndo
};
})()"""
result = run_js(code)
assert result["hasInputsBefore"] is True
assert result["inputsClearedAfter"] is True
assert result["lockedRoundRejected"] is True
assert result["lockedInputsPreserved"] is True
assert result["missingRoundRejected"] is True
assert result["missingRoundPreserved"] is True
assert result["handled"] is True
assert result["handledInputsCleared"] is True
assert result["canUndo"] is True
def test_reset_annual_action():
code = """(() => {
const p = EstNative.empty();
p.tables.EstMainDat = [
{ Code: "A001", Name: "토공사", GN: 100, TU: 5000, TC: 500000, NC: 200000, GC: 200000, JC: 100000, SC: 0, Level: 0, LinkIndex: 0, CalcIndex: 0 }
];
p.tables.Wonga = [
{ Gubun: "01", Name: "순공사비", LinkIndex: 54, CalcIndex: 0, Cost: 500000 },
{ Gubun: "05", Name: "총도급공사비", LinkIndex: 55, CalcIndex: 0, Cost: 550000 }
];
p.annual = [
{ name: "1차", formula: "[기준]*0.4" },
{ name: "2차", formula: "[기준]-[1차]" }
];
const state = {
project: p,
view: "annual",
selection: new Set([0]),
notice: ""
};
globalThis.confirm = () => true;
globalThis.EstApp = {
state,
history: [],
change(fn) {
this.history.push(JSON.stringify(state.project));
fn();
},
render() {}
};
// 1) 연차 계산 수행 및 정상 차수 확인
const calcBefore = EstBusiness.annual(p, false);
const tc1st = calcBefore.results[0]?.project.calculation.direct.TC; // 200,000원 (40%)
const tc2nd = calcBefore.results[1]?.project.calculation.direct.TC; // 300,000원 (60%)
// 2) JSON 직렬화 & 역직렬화 (저장 재열기 왕복)
const savedJson = JSON.stringify(state.project);
state.project = JSON.parse(savedJson);
// 3) 연차 초기화 실행
EstBlockActions.resetAnnualAction(state);
const annualCleared = state.project.annual.length === 0;
// 4) 기존 내역서 및 원가계산서 데이터 온전 보존 확인
const estDatPreserved = state.project.tables.EstMainDat.length === 1 && state.project.tables.EstMainDat[0].GN === 100;
const wongaPreserved = state.project.tables.Wonga.length === 2 && state.project.tables.Wonga[0].Cost === 500000;
// 5) handle("reset-annual") 연동 검증
state.project.annual = [{ name: "1차", formula: "[기준]*0.5" }];
const handled = EstBlockActions.handle("reset-annual", state);
const handledCleared = state.project.annual.length === 0;
// 6) 빈 annual 상태에서 재호출 시 안전 거부 확인
let emptyRejected = false;
try {
EstBlockActions.resetAnnualAction(state);
} catch {
emptyRejected = true;
}
// 7) 실행 취소(Undo) 가능 여부 확인
const canUndo = EstApp.history.length > 0;
return {
tc1st,
tc2nd,
annualCleared,
estDatPreserved,
wongaPreserved,
handled,
handledCleared,
emptyRejected,
canUndo
};
})()"""
result = run_js(code)
assert result["tc1st"] == 200000
assert result["tc2nd"] == 300000
assert result["annualCleared"] is True
assert result["estDatPreserved"] is True
assert result["wongaPreserved"] is True
assert result["handled"] is True
assert result["handledCleared"] is True
assert result["emptyRejected"] is True
assert result["canUndo"] is True
def test_reset_escalation_action():
code = """(() => {
const p = EstNative.empty();
p.tables.EstMainDat = [
{
Code: "A001",
Name: "토공사",
GN: 100,
TU: 5000,
TC: 500000,
Level: 0,
LinkIndex: 0,
CalcIndex: 0,
EscPlanned: 20,
EscActual: 15,
EscProgress: 25,
EscManualQuantity: 10,
EscConstruction: 5,
EscPS: 0,
EscBefore: 0,
EscAuto: false
}
];
p.options.EscDesignTotal = 520000;
const state = {
project: p,
view: "escalation",
selection: new Set([0]),
notice: ""
};
globalThis.confirm = () => true;
globalThis.EstApp = {
state,
history: [],
change(fn) {
this.history.push(JSON.stringify(state.project));
fn();
},
render() {}
};
// 1) 초기화 전 입력 상태 확인
const plannedBefore = state.project.tables.EstMainDat[0].EscPlanned;
const optBefore = state.project.options.EscDesignTotal;
// 2) JSON 직렬화 & 역직렬화 (저장 재열기 왕복)
const savedJson = JSON.stringify(state.project);
state.project = JSON.parse(savedJson);
// 3) 물가변동 입력 초기화 실행
EstBlockActions.resetEscalationAction(state);
const r = state.project.tables.EstMainDat[0];
const escFieldsCleared =
r.EscPlanned === undefined &&
r.EscActual === undefined &&
r.EscProgress === undefined &&
r.EscManualQuantity === undefined &&
r.EscConstruction === undefined &&
r.EscAuto === undefined;
const optCleared = state.project.options.EscDesignTotal === undefined;
// 기본 본선 데이터(수량, 단가, 금액)는 안전하게 보존되었는지 검증
const mainDatPreserved = r.GN === 100 && r.TU === 5000 && r.TC === 500000;
// 4) handle("reset-escalation") 연동 검증
r.EscPlanned = 30;
state.project.options.EscDesignTotal = 600000;
const handled = EstBlockActions.handle("reset-escalation", state);
const handledPlannedCleared = state.project.tables.EstMainDat[0].EscPlanned === undefined;
const handledOptCleared = state.project.options.EscDesignTotal === undefined;
// 5) 실행 취소(Undo) 가능 여부 확인
const canUndo = EstApp.history.length > 0;
return {
plannedBefore,
optBefore,
escFieldsCleared,
optCleared,
mainDatPreserved,
handled,
handledPlannedCleared,
handledOptCleared,
canUndo
};
})()"""
result = run_js(code)
assert result["plannedBefore"] == 20
assert result["optBefore"] == 520000
assert result["escFieldsCleared"] is True
assert result["optCleared"] is True
assert result["mainDatPreserved"] is True
assert result["handled"] is True
assert result["handledPlannedCleared"] is True
assert result["handledOptCleared"] is True
assert result["canUndo"] is True