feat(est): reorder annual work stages

This commit is contained in:
2026-11-07 07:53:54 +09:00
parent b279b964ce
commit 4e50e2fe94
5 changed files with 48 additions and 3 deletions
@@ -55,3 +55,13 @@ def test_annual_rejects_names_that_shadow_contract_references():
assert all(item["errors"] for item in result[:3])
assert result[3]["errors"] == []
assert result[3]["results"][1]["project"]["tables"]["EstMainDat"][0]["TC"] == 50
def test_new_annual_name_does_not_reuse_existing_or_referenced_names():
result = run(
"math",
"""[EstBusiness.newAnnualName([{name:'2차',formula:'[기준]-[1차]'}]),
EstBusiness.newAnnualName([{name:'2차',formula:'[기준]'}]),
EstBusiness.newAnnualName([])]""",
)
assert result == ["3차", "1차", "1차"]
+2
View File
@@ -49,6 +49,8 @@ Ctrl+S는 입력 중인 셀도 확정한 뒤 저장합니다. 연속 저장 요
연차공사의 `[기준]*0.5` 수식은 수량과 각 성분 금액에 각각 적용됩니다. 금액은 정수 절사하고, 다음 차수의 `[기준]-[1차]`로 절사 후 잔액을 배분합니다. 차수 사본은 단가를 비운 고정 금액으로 저장하여 다시 열어도 금액을 유지합니다. `[계약수량]`은 이전 작업 파일과의 호환 표기로 `[기준]`과 같은 역할을 합니다. 참조가 충돌하지 않도록 차수 이름에는 `기준`, `계약`, `계약수량`을 사용할 수 없습니다.
차수는 위·아래로 이동할 수 있습니다. 앞서 계산되지 않은 차수를 참조하면 오류를 표시하며, 새 차수는 이미 사용 중이거나 수식에서 참조 중인 이름을 피해서 만듭니다.
연차의 조달수수료는 원본처럼 연결 종류 99로 전환해 금액을 배분하며 요율은 유지합니다. 관급 합계와 안전관리비의 수수료 제외 계산에 배분액을 반영하고, 합산 제외 행은 제외 상태를 유지합니다. 차수 내역 열기는 해당 내역서를 표시하며, 저장하면 원본과 다른 파일 ID를 부여해 원본을 덮어쓰지 않습니다.
동원 집계는 비목별 낙찰률과 단위환산을 하위 산근·일위대가에도 전달합니다. 연결 단가가 없는 금액 입력분은 원본 전용 코드로 따로 합산하고, 장비 시간으로 바꿀 수 없는 경비는 `제외금액` 단위로 보존합니다. 산근에서 연결을 해제한 단가는 앞 행의 원천을 가져오지 않습니다.
+16 -2
View File
@@ -432,10 +432,24 @@ const EstActions = (() => {
}
} else if (name === "add-annual")
EstApp.change(
() => project.annual.push({ name: `${project.annual.length + 1}차`, formula: "[기준]" }),
() =>
project.annual.push({
name: EstBusiness.newAnnualName(project.annual),
formula: "[기준]",
}),
false,
);
else if (name === "open-annual") {
else if (name === "move-annual") {
const index = Number(node.dataset.row),
next = index + Number(node.dataset.direction);
if (index >= 0 && index < project.annual.length && next >= 0 && next < project.annual.length)
EstApp.change(() => {
[project.annual[index], project.annual[next]] = [
project.annual[next],
project.annual[index],
];
}, false);
} else if (name === "open-annual") {
const result = EstBusiness.annualCopy(project, Number(node.dataset.row));
if (confirm("현재 화면을 차수 내역 사본으로 바꾸시겠습니까?")) {
EstApp.setProject(result);
+9
View File
@@ -216,6 +216,14 @@ const EstBusiness = (() => {
function escalation(project) {
return EstEscalation.calculate(project);
}
function newAnnualName(years) {
const names = new Set(years.map((year) => String(year.name || "").trim()));
for (const year of years)
for (const match of String(year.formula || "").matchAll(/\[([^\]]+)\]/g)) names.add(match[1]);
let index = 1;
while (names.has(`${index}차`)) index++;
return `${index}차`;
}
function annual(project, full = true) {
const errors = [],
results = [];
@@ -295,6 +303,7 @@ const EstBusiness = (() => {
progress,
escalation,
annual,
newAnnualName,
rowKey,
importContract,
importBid,
+11 -1
View File
@@ -278,7 +278,17 @@ const EstExtra = (() => {
number(output.results[index]?.project.calculation.direct.TC),
number(output.results[index]?.project.calculation.total),
button("open-annual", "차수 내역 열기", `data-row="${index}"`),
button("delete-extra", "삭제", `data-section="annual" data-row="${index}"`),
button(
"move-annual",
"↑",
`data-row="${index}" data-direction="-1" ${index === 0 ? "disabled" : ""}`,
) +
button(
"move-annual",
"↓",
`data-row="${index}" data-direction="1" ${index === project.annual.length - 1 ? "disabled" : ""}`,
) +
button("delete-extra", "삭제", `data-section="annual" data-row="${index}"`),
]),
),
`수식 예: [기준]*0.5, [기준]-[1차]. 같은 수식을 수량과 각 성분 금액에 따로 적용하고 금액은 원 미만 절사합니다. ${esc(output.errors.join(" / "))}`,