`phase: "detail"` 칸(돌 종류·조달·뒷길이·전면 기울기)을 **그리는 화면이 없었음**
(2026-09-09 실측: B06 구조물 폼은 b05 phase 열 칸만 그림. 화면 글자 전수에서 「조달」·
「뒷길이」·「돌 종류」 0건). 그 자리를 표준도가 맡음 — PLAN 4-5b 「표준도는 입력 화면이기도 함」.
B06 은 「어디에·몇 m」(배치), 표준도는 「어떤 제원」.
**장 하나 = 제원 조합 하나**라 한 번 고치면 그 조합의 개소 전부에 걸림.
지킨 것
· **자동값을 저장에 박지 않음** — 빈 칸이 「정한 적 없음」의 뜻(확정 ⑨). 비우면 그 키를
지움. 판정된 기울기는 칸이 아니라 **회색 도움말**로만 비춤.
· **막지 않음** — 실무 도면에 1:0.7·1:0.8 이 실재(구조물도 53장). 품셈 범위(0.20~0.50)
밖이어도 값은 받고 안내만.
· **값을 여기서 셈하지 않음** — 정본에 적기만 하고 표·그림은 다음 조회에서 그 정본으로
다시 섬. 표준도가 두 번째 정본이 되면 안 됨.
⚠ 만들다 잡은 것 셋
① `face_slope_ratio` 가 **구조물 등록부에 없어** 저장이 통째로 거절됐음
(`ValueError: 돌쌓기(찰)에 정의되지 않은 옵션입니다`). 한 칸 때문에 전부 못 저장되는
것은 나쁨 — 등록부에 없는 칸은 **빼고 이름으로 알림**(조용히 버리면 저장된 줄 앎).
그 칸 신설은 랩탑 메인 몫.
② 저장 뒤 표·그림을 다시 받으면서 **폼이 새로 그려져 안내가 지워졌음.** 밖에서 들고
있다가 다시 넣음.
③ **장 제목에 제원이 들어 있는데 좌측 단추 글자가 안 따라왔음.** 목록을 통째로 다시
받지 않고 표준도 줄만 갈아 끼움.
실화면 확인 (프로젝트 936be972, 개발 우회로로 열어서)
「표준도 2장」에서 돌 종류 견치돌·뒷길이 55 로 저장 →
단추 글자가 「…뒷길이 35㎝ 야면석·호박돌」 → 「…뒷길이 55㎝ 견치돌」로 **즉시 바뀜**
기울기 0.7 저장 → 안내 셋: 「1개소에 반영했습니다」 · 「1:0.7 는 품셈 표준경사 표
범위(1:0.2~1:0.5) 밖입니다 — 값은 그대로 씁니다」 · 「「전면 기울기」 칸이 masonry_dry
등록부에 아직 없어 저장하지 못했습니다」
조달 「구입」 저장 → 그 장의 **2개소에만** 들어가고 다른 장은 그대로(정본 실측)
확인 뒤 **지정받은 제원으로 되돌리고 우회로도 닫음**(목록 409 복귀).
700줄 제한으로 표준도 라우터를 `B07_DesignDetail_Router_Standard.py` 로 뗌(579 + 153).
자체검증 — 새 시험 7건 + 회귀 580 통과 · 0 실패, `tsc --noEmit` 0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
161 lines
7.3 KiB
Python
161 lines
7.3 KiB
Python
"""표준도 **입력** — 장 하나의 제원을 고쳐 그 조합의 구조물 전부에 반영한다.
|
|
|
|
왜 여기가 입력 자리인가 (PLAN 4-5b · 2026-09-09)
|
|
`phase: "detail"` 칸(돌 종류·조달·뒷길이·전면 기울기)을 **그리는 화면이 없었다**
|
|
(2026-09-09 실측: B06 구조물 폼은 b05 phase 열 칸만 그림). 그 값들이 곧 표준도가
|
|
받는 값이고, **장 하나 = 제원 조합 하나**라 여기서 한 번 고치면 그 조합 전부에 걸린다.
|
|
B06 폼은 「어디에·몇 m」(배치), 표준도는 「어떤 제원」 — 축이 갈린다.
|
|
|
|
⚠ **자동값을 저장에 박지 않는다.** 빈 칸은 「정한 적 없음」의 뜻이다. 기울기를 비우면
|
|
품셈 표준경사 판정이 돌고, 채우면 그 값이 이긴다(확정 ⑨). 그래서 빈 값이 오면
|
|
**키를 지운다** — 0 이나 판정값을 적어 두면 그 구별이 사라진다.
|
|
|
|
⚠ **막지 않는다.** 실무 도면에 `S0.7`·`0.8` 이 실재하는데 품셈 표준경사 범위는 0.20~0.50 이다
|
|
(2026-09-09 구조물도 53장 확인). 범위 밖이면 **안내만** 하고 값은 받는다.
|
|
|
|
⚠ **고치면 장이 갈릴 수 있다** — 그 조합 전부에 같은 값을 넣으므로 장은 통째로 옮겨 가고
|
|
쪼개지지 않는다. 한 개소만 다르게 하려면 그 구조물을 따로 고쳐야 하고, 그때 새 조합이
|
|
되어 장이 하나 는다(PLAN 4-5b).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
from B05_Profile.B05_Profile_Structures_Schema import StructureInstance
|
|
|
|
#: 표준도에서 받는 칸 — `키 → (이름, 검사)`. 여기 없는 칸은 표준도가 안 만진다.
|
|
EDITABLE_KEYS: tuple[str, ...] = ("stone_kind", "stone_supply", "back_len_cm", "face_slope_ratio")
|
|
|
|
#: 품셈 13-4-3·13-4-4 [주]① 의 일곱 규격. 그 밖의 값은 계수가 없어 물량이 안 선다.
|
|
BACK_LENGTH_CHOICES: tuple[int, ...] = (25, 30, 35, 45, 55, 60, 75)
|
|
|
|
#: 품셈 표준경사 표가 덮는 범위. **막는 선이 아니라 안내 선**이다.
|
|
SLOPE_TABLE_RANGE: tuple[float, float] = (0.20, 0.50)
|
|
|
|
#: 안내 문구에 쓸 사람 말.
|
|
FIELD_LABELS: dict[str, str] = {
|
|
"stone_kind": "돌 종류",
|
|
"stone_supply": "조달",
|
|
"back_len_cm": "뒷길이",
|
|
"face_slope_ratio": "전면 기울기",
|
|
}
|
|
|
|
|
|
def _clean_slope(value: Any) -> tuple[float | None, str | None]:
|
|
"""전면 기울기 — `(값, 안내)`. 비면 `(None, None)` 이고 그것이 「자동」의 뜻이다."""
|
|
if value in (None, ""):
|
|
return None, None
|
|
try:
|
|
ratio = float(value)
|
|
except (TypeError, ValueError):
|
|
return None, f"전면 기울기 「{value}」를 숫자로 읽지 못했습니다 — 비워 두면 자동입니다."
|
|
if ratio <= 0:
|
|
return None, "전면 기울기는 0보다 커야 합니다 — 비워 두면 자동입니다."
|
|
low, high = SLOPE_TABLE_RANGE
|
|
if not (low <= ratio <= high):
|
|
return ratio, (
|
|
f"1:{ratio:g} 는 품셈 표준경사 표 범위(1:{low:g}~1:{high:g}) 밖입니다 — "
|
|
"실무 도면에 1:0.7·1:0.8 이 실재하므로 값은 그대로 씁니다."
|
|
)
|
|
return ratio, None
|
|
|
|
|
|
def clean_spec(spec: dict[str, Any]) -> tuple[dict[str, Any], list[str]]:
|
|
"""받은 제원을 **저장할 꼴**로 다듬는다 — `(고칠 값, 안내 문구)`.
|
|
|
|
값이 `None`(또는 빈 문자열)이면 **그 키를 지우라**는 뜻으로 `None` 을 담아 돌려준다.
|
|
"""
|
|
cleaned: dict[str, Any] = {}
|
|
notes: list[str] = []
|
|
|
|
if "stone_kind" in spec:
|
|
kind = spec.get("stone_kind")
|
|
cleaned["stone_kind"] = str(kind) if kind not in (None, "") else None
|
|
|
|
if "stone_supply" in spec:
|
|
supply = spec.get("stone_supply")
|
|
cleaned["stone_supply"] = str(supply) if supply not in (None, "") else None
|
|
|
|
if "back_len_cm" in spec:
|
|
raw = spec.get("back_len_cm")
|
|
if raw in (None, ""):
|
|
cleaned["back_len_cm"] = None
|
|
else:
|
|
try:
|
|
back = int(float(raw))
|
|
except (TypeError, ValueError):
|
|
back = None
|
|
notes.append(f"뒷길이 「{raw}」를 숫자로 읽지 못했습니다.")
|
|
if back is not None:
|
|
cleaned["back_len_cm"] = back
|
|
if back not in BACK_LENGTH_CHOICES:
|
|
notes.append(
|
|
f"뒷길이 {back}㎝ 는 품셈 표(25·30·35·45·55·60·75㎝)에 없어 "
|
|
"물량이 서지 않습니다."
|
|
)
|
|
|
|
if "face_slope_ratio" in spec:
|
|
ratio, note = _clean_slope(spec.get("face_slope_ratio"))
|
|
cleaned["face_slope_ratio"] = ratio
|
|
if note:
|
|
notes.append(note)
|
|
|
|
return cleaned, notes
|
|
|
|
|
|
def drop_unregistered(
|
|
type_id: str, spec: dict[str, Any], allowed: set[str]
|
|
) -> tuple[dict[str, Any], list[str]]:
|
|
"""등록부에 **칸이 없는** 제원은 빼고 알린다 — `(남긴 값, 안내)`.
|
|
|
|
⚠ 저장소가 「정의되지 않은 옵션」을 거절하므로, 그대로 넘기면 **한 칸 때문에 전부**
|
|
저장이 안 된다(2026-09-09 실측: `face_slope_ratio` 가 등록부에 없어 저장 전체 실패).
|
|
한 칸을 못 받는 것과 아무것도 못 받는 것은 다르다 — 나머지는 살리고 **못 받은 칸을
|
|
이름으로 말한다**. 조용히 버리면 사용자는 저장된 줄 안다.
|
|
"""
|
|
kept: dict[str, Any] = {}
|
|
notes: list[str] = []
|
|
for key, value in spec.items():
|
|
if key in allowed:
|
|
kept[key] = value
|
|
continue
|
|
if value is None:
|
|
# 지우라는 뜻인데 칸 자체가 없다 — 이미 없으므로 조용히 넘어간다.
|
|
continue
|
|
notes.append(
|
|
f"「{FIELD_LABELS.get(key, key)}」 칸이 {type_id} 등록부에 아직 없어 "
|
|
"저장하지 못했습니다 — 다른 칸은 저장했습니다."
|
|
)
|
|
return kept, notes
|
|
|
|
|
|
def apply_spec(
|
|
structures: list[StructureInstance], member_ids: set[str], spec: dict[str, Any]
|
|
) -> tuple[list[StructureInstance], int]:
|
|
"""그 장에 속한 구조물마다 제원을 갈아 끼운다 — `(새 목록, 바뀐 개소 수)`.
|
|
|
|
⚠ **개소 id 로 고른다** — 장 이름(`sheet_key`)을 여기서 다시 셈하지 않는다. 그 이름은
|
|
B08 이 편 결과(`height_m` 이 위로 올라온 꼴) 위에서 나오는데, 정본
|
|
`StructureInstance` 는 높이가 `options` 안에 있어 **같은 이름이 안 나온다**.
|
|
장 목록이 이미 `members[].structure_id` 를 실어 주므로 그것을 그대로 쓴다.
|
|
"""
|
|
changed = 0
|
|
out: list[StructureInstance] = []
|
|
for item in structures:
|
|
payload = item.model_dump()
|
|
if str(payload.get("structure_id") or "") not in member_ids:
|
|
out.append(item)
|
|
continue
|
|
options = dict(payload.get("options") or {})
|
|
for key, value in spec.items():
|
|
if value is None:
|
|
# ⚠ 지운다 — 「정한 적 없음」과 「그 값으로 정함」이 구별돼야 한다.
|
|
options.pop(key, None)
|
|
else:
|
|
options[key] = value
|
|
payload["options"] = options
|
|
out.append(StructureInstance.model_validate(payload))
|
|
changed += 1
|
|
return out, changed
|