Merge remote-tracking branch 'origin/CODEX' into sub_laptop_8
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user