fix(b08): 고친 식을 장 이름 대신 양식 + 프로젝트에 묶음 — 제원을 고쳐도 식이 따라감

- 저장 칸 structure_formula_overrides 의 키를 장 이름에서 양식 type_id 로 바꿈(브레인 판정, PLAN 10장)
- 구조물도·build_table 모두 type_id 로 찾음 · 같은 양식의 장이 모두 같은 고친 식을 받음
- 화면 안내에 「같은 양식의 장 모두에 걸림」 · 시험: 뒷길이를 고쳐 장 이름이 바뀌어도 사용자 식 남음

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq
This commit is contained in:
2026-09-13 18:02:50 +09:00
co-authored by Claude Opus 5
parent f18059b23f
commit 2ffb913c36
5 changed files with 35 additions and 13 deletions
@@ -5,8 +5,9 @@
⚠ 규격(높이·뒷길이·돌종류)마다 항목을 늘리지 않음 — 제원은 `vars` 로 들어가고 같은 식이 ⚠ 규격(높이·뒷길이·돌종류)마다 항목을 늘리지 않음 — 제원은 `vars` 로 들어가고 같은 식이
수량을 다시 냄(명세 16장 「한 조합 + 규격별 수량표」). 수량을 다시 냄(명세 16장 「한 조합 + 규격별 수량표」).
⚠ 사용자가 고친 식(PLAN 3장 ⑤)은 **프로젝트 단위** — 산출 조건 `quantity` 구획의 ⚠ 사용자가 고친 식(PLAN 3장 ⑤)은 **양식 + 프로젝트 단위** — 산출 조건 `quantity` 구획의
`structure_formula_overrides` = `{장 이름: {차례: {"formula": 식}}}`. 고친 줄은 출처 `user`. `structure_formula_overrides` = `{양식 type_id: {차례: {"formula": 식}}}`. 고친 줄은 출처 `user`.
장 이름(제원 조합)에 묶지 않음 — 높이를 고치면 식이 사라짐. 제원별로 달리 쓰려면 `when` 칸.
""" """
from __future__ import annotations from __future__ import annotations
@@ -155,10 +156,10 @@ def replace_with_templates(
⚠ 전개가 성분을 하나도 못 낸 구조물(표에 없는 뒷길이 등)은 그대로 둠 — 전개 사유가 이미 드러냄. ⚠ 전개가 성분을 하나도 못 낸 구조물(표에 없는 뒷길이 등)은 그대로 둠 — 전개 사유가 이미 드러냄.
⚠ Node 가 안 돌면 전개 값을 두고 **사유를 남김** — 조용히 섞이지 않게. ⚠ Node 가 안 돌면 전개 값을 두고 **사유를 남김** — 조용히 섞이지 않게.
⚠ 오류 난 양식 줄은 성분에서 빼고 사유로 — 전개도 원문 「-」 줄을 안 세우고 사유로 둠. ⚠ 오류 난 양식 줄은 성분에서 빼고 사유로 — 전개도 원문 「-」 줄을 안 세우고 사유로 둠.
⚠ 사용자가 고친 식은 **그 구조물이 속한 장 이름**으로 찾음 — 구조물도와 같은 값이 되게. ⚠ 사용자가 고친 식은 **양식(type_id)** 으로 찾음 — 구조물도와 같은 값이 되게.
""" """
from B08_Quantity.B08_Quantity_Engine_Formula import evaluate_sheets from B08_Quantity.B08_Quantity_Engine_Formula import evaluate_sheets
from B08_Quantity.B08_Quantity_Engine_StructureSheet import sheet_key, slope_of from B08_Quantity.B08_Quantity_Engine_StructureSheet import slope_of
from B08_Quantity.B08_Quantity_Engine_UnitQuantity import Component, _section_mode_at from B08_Quantity.B08_Quantity_Engine_UnitQuantity import Component, _section_mode_at
from common_util.common_util_structure_face_role import structure_face_role from common_util.common_util_structure_face_role import structure_face_role
@@ -185,7 +186,7 @@ def replace_with_templates(
"options": options, "options": options,
} }
values = template_vars(template, structure, slope_of(sheet)[0], settings) values = template_vars(template, structure, slope_of(sheet)[0], settings)
overrides = (formula_overrides or {}).get(sheet_key(sheet)) overrides = (formula_overrides or {}).get(quantity.type_id)
targets.append((quantity, template_sheet(template, values, overrides))) targets.append((quantity, template_sheet(template, values, overrides)))
if not targets: if not targets:
return return
@@ -261,7 +262,7 @@ def apply_templates(payload: dict[str, Any], settings: dict[str, Any] | None = N
"options": sheet.get("options"), "options": sheet.get("options"),
} }
values = template_vars(template, structure, slope_of(sheet)[0], settings) values = template_vars(template, structure, slope_of(sheet)[0], settings)
body = template_sheet(template, values, all_overrides.get(sheet.get("key"))) body = template_sheet(template, values, all_overrides.get(sheet.get("type_id")))
targets.append((sheet, body)) targets.append((sheet, body))
if not targets: if not targets:
@@ -290,7 +291,7 @@ def save_sheet_overrides(
template: dict[str, Any], template: dict[str, Any],
edits: list[dict[str, Any]], edits: list[dict[str, Any]],
) -> tuple[dict[str, Any], int]: ) -> tuple[dict[str, Any], int]:
""" 하나의 고친 식을 갈아 끼운 **새 전체 값**과 바뀐 줄 수. """양식 하나(`key` = type_id)의 고친 식을 갈아 끼운 **새 전체 값**과 바뀐 줄 수.
⚠ 빈 식·양식 식과 같은 식은 **지움**(「고친 적 없음」으로 되돌림) — 같은 값을 박아 두면 ⚠ 빈 식·양식 식과 같은 식은 **지움**(「고친 적 없음」으로 되돌림) — 같은 값을 박아 두면
양식이 바뀌어도 그 장만 옛 식으로 남음. 양식이 바뀌어도 그 장만 옛 식으로 남음.
@@ -1517,7 +1517,7 @@ def build_table(
⭐ 2026-09-13(PLAN 3장 ④-2) — **양식이 있는 종류는 양식 풀이 값으로 성분을 갈음** ⭐ 2026-09-13(PLAN 3장 ④-2) — **양식이 있는 종류는 양식 풀이 값으로 성분을 갈음**
(`B08_Quantity_Engine_StructureTemplate.replace_with_templates`). 원단위·자재총괄·인계가 (`B08_Quantity_Engine_StructureTemplate.replace_with_templates`). 원단위·자재총괄·인계가
구조물도와 같은 값을 보게 함. `use_templates=False` 는 대조 시험이 **전개만** 볼 때 씀. 구조물도와 같은 값을 보게 함. `use_templates=False` 는 대조 시험이 **전개만** 볼 때 씀.
⭐ `structure_formulas` — 사용자가 마다 고친 식(산출 조건 `structure_formula_overrides`, ⭐ `structure_formulas` — 사용자가 양식마다 고친 식(산출 조건 `structure_formula_overrides`,
PLAN 3장 ⑤). 부르는 쪽이 산출 조건에서 넘김 — 안 넘기면 구조물도와 값이 갈림. PLAN 3장 ⑤). 부르는 쪽이 산출 조건에서 넘김 — 안 넘기면 구조물도와 값이 갈림.
""" """
observed = load_observed_table() observed = load_observed_table()
@@ -123,7 +123,7 @@ class FormulaEdit(BaseModel):
class StructureSheetFormulaRequest(BaseModel): class StructureSheetFormulaRequest(BaseModel):
"""구조물도 장 하나에서 고친 식들(PLAN 3장 ⑤ · 프로젝트 단위).""" """구조물도 장 하나에서 고친 식들(PLAN 3장 ⑤) — 저장은 그 장의 **양식 + 프로젝트** 단위."""
model_config = ConfigDict(extra="forbid") model_config = ConfigDict(extra="forbid")
@@ -161,8 +161,9 @@ async def put_structure_sheet_formulas(
) )
try: try:
current = quantity_settings(project_root).get(OVERRIDES_KEY) or {} current = quantity_settings(project_root).get(OVERRIDES_KEY) or {}
# 장 이름이 아니라 **양식**에 묶음 — 제원을 고쳐도 식이 따라가고 같은 양식의 장이 모두 받음.
merged, changed = save_sheet_overrides( merged, changed = save_sheet_overrides(
current, payload.sheet_key, template, [row.model_dump() for row in payload.rows] current, str(picked["type_id"]), template, [row.model_dump() for row in payload.rows]
) )
except ValueError as exc: except ValueError as exc:
return JSONResponse(status_code=400, content={"status": "error", "message": str(exc)}) return JSONResponse(status_code=400, content={"status": "error", "message": str(exc)})
@@ -8,7 +8,7 @@
* · . * · .
* [ ] (`structures.json`) B07 . * [ ] (`structures.json`) B07 .
* (PLAN 3 ) ** ** ( ). * (PLAN 3 ) ** ** ( ).
* [ ] ** Node **( ). . * [ ] ** Node **( ). + .
* ========================================================================== */ * ========================================================================== */
import { API_BASE_URL } from "@config/config_frontend"; import { API_BASE_URL } from "@config/config_frontend";
@@ -273,7 +273,7 @@ function formulaTable(
saveButton.disabled = !dirty; saveButton.disabled = !dirty;
discardButton.disabled = !dirty; discardButton.disabled = !dirty;
status.textContent = dirty status.textContent = dirty
? "저장 안 한 식 있음 — 값은 이 화면 계산(저장하면 서버가 다시 셈)" ? "저장 안 한 식 있음 — 값은 이 화면 계산(저장하면 서버가 다시 셈 · 같은 양식의 장 모두에 걸림)"
: ""; : "";
onDirty(dirty); onDirty(dirty);
}; };
@@ -132,7 +132,8 @@ def test_식을_고쳐_저장하면_서버가_다시_풀고_되돌릴_수_있다
assert after["모르터"]["default_formula"] == 원래["formula"] assert after["모르터"]["default_formula"] == 원래["formula"]
assert after["돌쌓기"]["source"] == "library" # 안 고친 줄은 그대로 양식 assert after["돌쌓기"]["source"] == "library" # 안 고친 줄은 그대로 양식
stored = json.loads((project / "project_settings.json").read_text(encoding="utf-8")) stored = json.loads((project / "project_settings.json").read_text(encoding="utf-8"))
assert stored["quantity"]["structure_formula_overrides"][sheet["key"]] == { # 장 이름이 아니라 양식(type_id)에 묶임 — 브레인 판정(PLAN 3장 ⑤ ⓒ).
assert stored["quantity"]["structure_formula_overrides"]["masonry_wet"] == {
str(원래["no"]): {"formula": "A*0.018"} str(원래["no"]): {"formula": "A*0.018"}
} }
@@ -166,6 +167,25 @@ def test_식을_고쳐_저장하면_서버가_다시_풀고_되돌릴_수_있다
assert stored["quantity"]["structure_formula_overrides"] == {} assert stored["quantity"]["structure_formula_overrides"] == {}
def test_제원을_고쳐도_고친_식이_따라간다(client: TestClient) -> None:
"""고친 식은 양식 + 프로젝트에 묶임 — 뒷길이를 고쳐 장 이름이 바뀌어도 사용자 식이 남음."""
sheet = _sheet(client)
seq = next(row["no"] for row in sheet["rows"] if row["name"] == "모르터")
client.put(
f"{SHEETS}/formulas",
json={"sheet_key": sheet["key"], "rows": [{"seq": seq, "formula": "A*0.018"}]},
)
spec = client.put(
f"{SHEETS}/spec",
json={"sheet_key": sheet["key"], "base_revision": 1, "back_len_cm": "55"},
)
assert spec.status_code == 200, spec.text
after = _sheet(client)
assert after["key"] != sheet["key"]
mortar = next(row for row in after["rows"] if row["name"] == "모르터")
assert mortar["source"] == "user" and mortar["formula"] == "A*0.018"
def test_틀린_식도_막지_않고_저장하되_오류를_돌려준다(client: TestClient) -> None: def test_틀린_식도_막지_않고_저장하되_오류를_돌려준다(client: TestClient) -> None:
sheet = _sheet(client) sheet = _sheet(client)
seq = next(row["no"] for row in sheet["rows"] if row["name"] == "모르터") seq = next(row["no"] for row in sheet["rows"] if row["name"] == "모르터")