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:
@@ -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"] == "모르터")
|
||||||
|
|||||||
Reference in New Issue
Block a user