feat(b07): 버림 콘크리트 끄는 칸 · 물빼기 주석 구조물 값 · 표준도 [여기서 고치기]
확정 8-2·14 — 버림 콘크리트 「넣음/안 넣음」 칸을 등록부(돌쌓기 찰·메·큰돌쌓기·기슭막이)와 표준도 입력(EDITABLE_KEYS)·제원 폼에 세움. 기본은 넣음(KDS 44 90 00 100㎜). 셈은 이미 wants_blinding 으로 서 있었고 끄는 자리만 없던 것임. 확정 5차 작은 것 2 — 표준도 그림 물빼기 주석이 기본 상수를 적던 것을 구조물 제원(weep_hole_*)에서 읽게 바꿈. 수량표와 같은 함수(weep_hole_spec)를 씀. 확정 6 — 표준도 장에서만 뜨는 [여기서 고치기] 단추를 고정 액션 줄에 둠. 표는 CAD 안이라 단추를 못 달고, 제원 폼은 도면 목록 아래라 밀림. 값 복사본은 만들지 않음 — 폼 한 벌이 그대로 정본 창구임. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fe1QWPTfw11PaKh2LjwXdR
This commit is contained in:
@@ -924,6 +924,17 @@
|
||||
"required": false,
|
||||
"phase": "detail",
|
||||
"empty_means": "비우면 국가기준 2㎡당 1개로 돎 — 사용자 확정 「국가기준 + 숫자 변경 가능하게」"
|
||||
},
|
||||
{
|
||||
"key": "blinding_concrete",
|
||||
"label": "버림 콘크리트",
|
||||
"input": "select",
|
||||
"choices": ["넣음", "안 넣음"],
|
||||
"default": "넣음",
|
||||
"default_basis": "사용자 확정 ⑭ 「기본은 넣음」 — 두께는 KDS 44 90 00 의 100㎜",
|
||||
"required": false,
|
||||
"phase": "detail",
|
||||
"empty_means": "비워도 넣음 — 「안 넣음」을 골라야 그 줄이 빠짐(B08 `wants_blinding`)"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -1086,6 +1097,17 @@
|
||||
"required": false,
|
||||
"phase": "detail",
|
||||
"empty_means": "비우면 국가기준 2㎡당 1개로 돎 — 사용자 확정 「국가기준 + 숫자 변경 가능하게」"
|
||||
},
|
||||
{
|
||||
"key": "blinding_concrete",
|
||||
"label": "버림 콘크리트",
|
||||
"input": "select",
|
||||
"choices": ["넣음", "안 넣음"],
|
||||
"default": "넣음",
|
||||
"default_basis": "사용자 확정 ⑭ 「기본은 넣음」 — 두께는 KDS 44 90 00 의 100㎜",
|
||||
"required": false,
|
||||
"phase": "detail",
|
||||
"empty_means": "비워도 넣음 — 「안 넣음」을 골라야 그 줄이 빠짐(B08 `wants_blinding`)"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -1441,6 +1463,17 @@
|
||||
"required": false,
|
||||
"phase": "detail",
|
||||
"empty_means": "비우면 국가기준 2㎡당 1개로 돎 — 사용자 확정 「국가기준 + 숫자 변경 가능하게」"
|
||||
},
|
||||
{
|
||||
"key": "blinding_concrete",
|
||||
"label": "버림 콘크리트",
|
||||
"input": "select",
|
||||
"choices": ["넣음", "안 넣음"],
|
||||
"default": "넣음",
|
||||
"default_basis": "사용자 확정 ⑭ 「기본은 넣음」 — 두께는 KDS 44 90 00 의 100㎜",
|
||||
"required": false,
|
||||
"phase": "detail",
|
||||
"empty_means": "비워도 넣음 — 「안 넣음」을 골라야 그 줄이 빠짐(B08 `wants_blinding`)"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -1991,6 +2024,17 @@
|
||||
"required": false,
|
||||
"phase": "detail",
|
||||
"empty_means": "비우면 국가기준 2㎡당 1개로 돎 — 사용자 확정 「국가기준 + 숫자 변경 가능하게」"
|
||||
},
|
||||
{
|
||||
"key": "blinding_concrete",
|
||||
"label": "버림 콘크리트",
|
||||
"input": "select",
|
||||
"choices": ["넣음", "안 넣음"],
|
||||
"default": "넣음",
|
||||
"default_basis": "사용자 확정 ⑭ 「기본은 넣음」 — 두께는 KDS 44 90 00 의 100㎜",
|
||||
"required": false,
|
||||
"phase": "detail",
|
||||
"empty_means": "비워도 넣음 — 「안 넣음」을 골라야 그 줄이 빠짐(B08 `wants_blinding`)"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -279,6 +279,7 @@ export function putStandardSheetSpec(
|
||||
fill_concrete_mpa: string | null;
|
||||
thickness_top_m: string | null;
|
||||
thickness_bottom_m: string | null;
|
||||
blinding_concrete: string | null;
|
||||
},
|
||||
): Promise<{ status: string; revision: number; changed: number; notes: string[] }> {
|
||||
return requestJson(`/projects/${projectId}/standard-sheets/spec`, {
|
||||
|
||||
@@ -37,7 +37,11 @@ from B07_DesignDetail.B07_DesignDetail_Engine_Cad import (
|
||||
_text_entity,
|
||||
polyline_entity,
|
||||
)
|
||||
from B08_Quantity.B08_Quantity_Engine_UnitQuantity import STONE_MASONRY, face_slope_ratio
|
||||
from B08_Quantity.B08_Quantity_Engine_UnitQuantity import (
|
||||
STONE_MASONRY,
|
||||
face_slope_ratio,
|
||||
weep_hole_spec,
|
||||
)
|
||||
from common_util.common_util_excavation import (
|
||||
WALL_BLINDING_DEPTH_M,
|
||||
WALL_BLINDING_WIDTH_M,
|
||||
@@ -223,6 +227,9 @@ def build_figure(
|
||||
entities.append(base)
|
||||
|
||||
# 물구멍 — 벽을 가로지르는 짧은 선 하나. 개소 간격은 글자로 적는다(면적당이라 그림에 못 씀).
|
||||
# ⚠ 값은 **그 장의 제원**에서 읽는다 — 수량표와 같은 함수를 쓴다(2026-09-13). 기본 상수를
|
||||
# 적으면 사용자가 지름·면적을 고쳐도 그림 주석만 옛 값으로 남아 표와 갈린다.
|
||||
weep_area_m2, weep_diameter_mm, _weep_basis = weep_hole_spec(sheet.get("options") or {})
|
||||
weep_y = height_m * 0.5
|
||||
entities.append(
|
||||
_line_entity(
|
||||
@@ -252,7 +259,7 @@ def build_figure(
|
||||
"left",
|
||||
),
|
||||
(
|
||||
f"물구멍 — {STONE_MASONRY['weep_hole_area_m2']:g}㎡당 1개소",
|
||||
f"물구멍 Ø{weep_diameter_mm} — {weep_area_m2:g}㎡당 1개소",
|
||||
(slope * weep_y + top_t + 0.16, weep_y),
|
||||
"left",
|
||||
),
|
||||
|
||||
@@ -35,6 +35,7 @@ EDITABLE_KEYS: tuple[str, ...] = (
|
||||
"fill_concrete_mpa",
|
||||
"thickness_top_m",
|
||||
"thickness_bottom_m",
|
||||
"blinding_concrete",
|
||||
)
|
||||
|
||||
#: 기초 갈래 — 정본 xls 탭 제목 그대로(`04.구조도(기슭막이).xls`).
|
||||
@@ -59,6 +60,7 @@ FIELD_LABELS: dict[str, str] = {
|
||||
"fill_concrete_mpa": "채움 강도",
|
||||
"thickness_top_m": "상부 두께",
|
||||
"thickness_bottom_m": "하부 두께",
|
||||
"blinding_concrete": "버림 콘크리트",
|
||||
}
|
||||
|
||||
#: 야면석 계수를 어느 열에서 읽나 — 확정 ⑨ 「품셈 열이 기본, 사용자가 고를 수 있게」.
|
||||
@@ -68,6 +70,10 @@ STONE_COEFF_CHOICES: tuple[str, ...] = ("품셈", "실무 관행")
|
||||
#: 180 은 국가기준 하한(돌쌓기 전용), 210 은 콘크리트 구조물 몸체 쪽 기준.
|
||||
FILL_CONCRETE_CHOICES: tuple[str, ...] = ("180", "210")
|
||||
|
||||
#: 버림 콘크리트 넣음/안 넣음 — 확정 ⑭ 「기본은 넣고, 빼고 싶으면 뺀다」(두께는 KDS 100㎜).
|
||||
#: ⚠ 문구는 B08 `wants_blinding` 이 「빼기」로 읽는 말과 같아야 한다 — 다르면 골라도 안 빠진다.
|
||||
BLINDING_CHOICES: tuple[str, ...] = ("넣음", "안 넣음")
|
||||
|
||||
|
||||
def _clean_slope(value: Any) -> tuple[float | None, str | None]:
|
||||
"""전면 기울기 — `(값, 안내)`. 비면 `(None, None)` 이고 그것이 「자동」의 뜻이다."""
|
||||
@@ -134,6 +140,7 @@ def clean_spec(spec: dict[str, Any]) -> tuple[dict[str, Any], list[str]]:
|
||||
for key, choices in (
|
||||
("stone_coeff_basis", STONE_COEFF_CHOICES),
|
||||
("fill_concrete_mpa", FILL_CONCRETE_CHOICES),
|
||||
("blinding_concrete", BLINDING_CHOICES),
|
||||
):
|
||||
if key not in spec:
|
||||
continue
|
||||
|
||||
@@ -75,6 +75,7 @@ class StandardSheetSpecRequest(BaseModel):
|
||||
fill_concrete_mpa: str | None = None
|
||||
thickness_top_m: float | str | None = None
|
||||
thickness_bottom_m: float | str | None = None
|
||||
blinding_concrete: str | None = None
|
||||
|
||||
|
||||
@router.put("/{project_id}/standard-sheets/spec")
|
||||
|
||||
@@ -213,13 +213,33 @@ export async function renderB07DesignDetail(root: HTMLElement): Promise<void> {
|
||||
const infoPanelHost = document.createElement("div");
|
||||
infoPanelHost.className = "b07-info-host";
|
||||
|
||||
/**
|
||||
* [여기서 고치기] — 도면 하단표에서 **값을 정하는 자리**로 데려간다(확정 6 ㉮
|
||||
* 「한 곳에서 정하고 도면은 비추기만」). 표는 CAD 안에 그려져 단추를 못 달고, 제원 폼은
|
||||
* 도면 목록 아래라 장이 많으면 화면 밖으로 밀린다 — 그래서 **고정 액션 줄**에 둔다
|
||||
* (떠 있는 띠는 CAD 리본과 겹쳐 2026-09-02 에 걷어낸 자리라 다시 띄우지 않는다).
|
||||
* ⚠ 값을 여기로 복사하지 않는다 — 폼 한 벌이 그대로 정본 창구다.
|
||||
*/
|
||||
const specJumpButton = createButton({
|
||||
label: "여기서 고치기",
|
||||
variant: "ghost",
|
||||
onClick: () => {
|
||||
infoPanelHost.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
||||
infoPanelHost.querySelector<HTMLElement>("select, input")?.focus();
|
||||
},
|
||||
});
|
||||
specJumpButton.title = "이 표의 값을 정하는 제원 칸으로 갑니다.";
|
||||
specJumpButton.hidden = true;
|
||||
|
||||
const showStandardSpec = (drawing: DesignDrawingItem): void => {
|
||||
const index = Number(/_(\d+)$/.exec(drawing.id)?.[1] ?? "1") - 1;
|
||||
const sheet = standardSheets[index];
|
||||
if (!projectId || !sheet) {
|
||||
infoPanelHost.replaceChildren();
|
||||
specJumpButton.hidden = true;
|
||||
return;
|
||||
}
|
||||
specJumpButton.hidden = false;
|
||||
// 판정된 기울기는 **칸에 적지 않고 도움말로만** 비춘다 — 적어 두면 「안 정함」이 사라진다.
|
||||
const judged = /1:([\d.]+)/.exec(drawing.label)?.[1] ?? null;
|
||||
infoPanelHost.replaceChildren(
|
||||
@@ -259,6 +279,7 @@ export async function renderB07DesignDetail(root: HTMLElement): Promise<void> {
|
||||
showStandardSpec(drawing);
|
||||
return;
|
||||
}
|
||||
specJumpButton.hidden = true; // 표준도 장이 아니면 갈 곳이 없다
|
||||
if (drawing.kind !== "cross") {
|
||||
infoPanelHost.replaceChildren();
|
||||
return;
|
||||
@@ -658,7 +679,7 @@ export async function renderB07DesignDetail(root: HTMLElement): Promise<void> {
|
||||
// (2026-09-02 사용자 지시 — CAD 리본과 겹치던 떠 있는 띠를 여기로 옮김).
|
||||
const confirmButtonRow = document.createElement("div");
|
||||
confirmButtonRow.className = "b07-drawing-actions__row";
|
||||
confirmButtonRow.append(frameEditor.button, confirmButton);
|
||||
confirmButtonRow.append(specJumpButton, frameEditor.button, confirmButton);
|
||||
confirmActions.append(frameEditor.banner, confirmButtonRow);
|
||||
|
||||
drawingPanel.append(infoPanelHost, confirmActions);
|
||||
|
||||
@@ -21,6 +21,8 @@ const FOUNDATIONS = ["기초유", "기초버림"] as const;
|
||||
const COEFF_BASES = ["품셈", "실무 관행"] as const;
|
||||
/** 확정 2차 ⑩ — 기본 210. 180 은 국가기준 하한(돌쌓기 전용). */
|
||||
const FILL_MPA = ["180", "210"] as const;
|
||||
/** 확정 8-2·14 — 기본은 넣음(KDS 100㎜), 빼고 싶으면 「안 넣음」. 문구가 곧 서버가 읽는 말이다. */
|
||||
const BLINDINGS = ["넣음", "안 넣음"] as const;
|
||||
|
||||
/** 표준도 장 하나 — 서버가 낸 것 중 이 폼이 쓰는 것만. */
|
||||
export interface StandardSheetSpec {
|
||||
@@ -42,6 +44,7 @@ export interface StandardSpecResult {
|
||||
fill_concrete_mpa: string | null;
|
||||
thickness_top_m: string | null;
|
||||
thickness_bottom_m: string | null;
|
||||
blinding_concrete: string | null;
|
||||
}
|
||||
|
||||
/** 이 종류가 돌쌓기 계열인가 — 옹벽·집수정에는 이 칸들이 뜻이 없다. */
|
||||
@@ -123,6 +126,7 @@ export function buildStandardSpecPanel(
|
||||
const foundation = select(FOUNDATIONS, options.foundation, "— 안 정함 —");
|
||||
const coeff = select(COEFF_BASES, options.stone_coeff_basis, "— 안 정함(품셈) —");
|
||||
const mpa = select(FILL_MPA, options.fill_concrete_mpa, "— 안 정함(210) —");
|
||||
const blinding = select(BLINDINGS, options.blinding_concrete, "— 안 정함(넣음) —");
|
||||
|
||||
const slope = document.createElement("input");
|
||||
slope.className = "b07-spec__input";
|
||||
@@ -156,6 +160,7 @@ export function buildStandardSpecPanel(
|
||||
),
|
||||
field("야면석 계수", coeff, "비우면 품셈 열을 씁니다."),
|
||||
field("채움 강도 (MPa)", mpa, "비우면 210. 180 은 국가기준 하한입니다."),
|
||||
field("버림 콘크리트", blinding, "비우면 넣습니다(두께 100㎜) — 빼려면 「안 넣음」."),
|
||||
);
|
||||
if (THICKNESS_TYPES.has(sheet.type_id)) {
|
||||
panel.append(
|
||||
@@ -199,6 +204,7 @@ export function buildStandardSpecPanel(
|
||||
foundation: foundation.value || null,
|
||||
stone_coeff_basis: coeff.value || null,
|
||||
fill_concrete_mpa: mpa.value || null,
|
||||
blinding_concrete: blinding.value || null,
|
||||
thickness_top_m: THICKNESS_TYPES.has(sheet.type_id)
|
||||
? topThickness.value.trim() || null
|
||||
: null,
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""표준도 제원 — 버림 콘크리트 칸(확정 8-2·14)과 물빼기 그림 주석(확정 5차 작은 것 2).
|
||||
|
||||
**버림 콘크리트** — 셈은 이미 「기본은 넣고, 「안 넣음」이면 뺀다」로 서 있었는데
|
||||
(`wants_blinding`) **끄는 칸이 화면에 없었다**. 등록부 · 표준도 입력 · 폼 세 자리를 함께
|
||||
이어 놓은 것을 굳힌다.
|
||||
⚠ **문구가 폴리시다** — 폼이 보내는 말이 `wants_blinding` 이 「빼기」로 읽는 말과 한 글자라도
|
||||
다르면 골라도 안 빠진다. 그래서 선택지 문구를 **엔진에 직접 물어** 확인한다.
|
||||
|
||||
**물빼기 주석** — 그림이 기본 상수를 적고 있어, 구조물별로 지름·면적을 고쳐도 **표는 바뀌고
|
||||
그림 글자만 옛 값**으로 남았다(확정 10 「구조물별로 다르게도 가능」과 어긋남).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_StandardFigure import ( # noqa: E402
|
||||
build_figure,
|
||||
)
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Standard_Edit import ( # noqa: E402
|
||||
BLINDING_CHOICES,
|
||||
EDITABLE_KEYS,
|
||||
clean_spec,
|
||||
)
|
||||
from B08_Quantity.B08_Quantity_Engine_UnitQuantity import wants_blinding # noqa: E402
|
||||
|
||||
#: 버림이 서는 종류 — 돌쌓기 식(`stone_masonry`)·큰돌쌓기를 타는 것들.
|
||||
BLINDING_TYPES = ("masonry_wet", "masonry_dry", "boulder_masonry", "revetment")
|
||||
|
||||
|
||||
def _registry() -> dict[str, dict]:
|
||||
payload = json.loads(
|
||||
(ROOT / "B05_Profile" / "B05_Profile_Structure_Types.json").read_text(encoding="utf-8")
|
||||
)
|
||||
return {item["type_id"]: item for item in payload["types"]}
|
||||
|
||||
|
||||
def test_등록부에_버림_칸이_있다() -> None:
|
||||
"""칸이 없으면 저장소가 그 제원을 거절한다(`drop_unregistered` 의 그 자리)."""
|
||||
registry = _registry()
|
||||
for type_id in BLINDING_TYPES:
|
||||
options = {option["key"]: option for option in registry[type_id]["options"]}
|
||||
field = options.get("blinding_concrete")
|
||||
assert field is not None, f"{type_id} 등록부에 버림 칸이 없다"
|
||||
assert field["choices"] == list(BLINDING_CHOICES)
|
||||
# 기본은 **넣음** — 확정 ⑭. 빈 칸도 넣음이지만 화면에 보여야 한다.
|
||||
assert field["default"] == "넣음"
|
||||
assert field["phase"] == "detail"
|
||||
|
||||
|
||||
def test_표준도가_버림_칸을_받는다() -> None:
|
||||
assert "blinding_concrete" in EDITABLE_KEYS
|
||||
spec, notes = clean_spec({"blinding_concrete": "안 넣음"})
|
||||
assert spec == {"blinding_concrete": "안 넣음"} and notes == []
|
||||
# 빈 값은 「정한 적 없음」 — 키를 지운다(그러면 기본인 「넣음」으로 돈다).
|
||||
spec, _ = clean_spec({"blinding_concrete": ""})
|
||||
assert spec == {"blinding_concrete": None}
|
||||
# 없는 갈래는 막지 않고 **알린다**(표준도 입력의 규칙).
|
||||
_spec, notes = clean_spec({"blinding_concrete": "빼기도"})
|
||||
assert notes and "버림 콘크리트" in notes[0]
|
||||
|
||||
|
||||
def test_선택지_문구가_엔진이_읽는_말과_같다() -> None:
|
||||
"""⚠ 이 시험이 없으면 「제외」처럼 다른 말로 바뀌어도 아무 데서도 안 걸린다."""
|
||||
넣음, 안넣음 = BLINDING_CHOICES
|
||||
assert wants_blinding({"blinding_concrete": 넣음}) is True
|
||||
assert wants_blinding({"blinding_concrete": 안넣음}) is False
|
||||
assert wants_blinding({}) is True # 정한 적 없음 = 넣음
|
||||
|
||||
|
||||
def _figure_labels(**options) -> list[str]:
|
||||
sheet = {
|
||||
"key": "masonry_wet",
|
||||
"title": "돌쌓기(찰)",
|
||||
"type_id": "masonry_wet",
|
||||
"height_m": 2.0,
|
||||
"options": {"height_m": 2.0, "back_len_cm": 45, **options},
|
||||
}
|
||||
entities, _height = build_figure(
|
||||
"d1", sheet, "layer", (0.0, 0.0), "#000000", "#111111", "#222222"
|
||||
)
|
||||
return [str((item.get("shapeData") or {}).get("label") or "") for item in entities]
|
||||
|
||||
|
||||
def test_물빼기_주석이_구조물_값을_쓴다() -> None:
|
||||
labels = _figure_labels(weep_hole_diameter_mm=75, weep_hole_area_m2=3)
|
||||
물구멍 = [text for text in labels if "물구멍" in text]
|
||||
assert 물구멍, labels
|
||||
assert "Ø75" in 물구멍[0] and "3㎡당" in 물구멍[0], 물구멍[0]
|
||||
|
||||
|
||||
def test_안_정하면_국가기준_기본이_뜬다() -> None:
|
||||
물구멍 = [text for text in _figure_labels() if "물구멍" in text]
|
||||
assert 물구멍 and "Ø50" in 물구멍[0] and "2㎡당" in 물구멍[0], 물구멍
|
||||
|
||||
|
||||
def test_여기서_고치기_단추가_고정_액션_줄에_있다() -> None:
|
||||
"""확정 6 ㉮ — 표는 CAD 안이라 단추를 못 달아 **고정 액션 줄**에 둔다(값 복사본 없음)."""
|
||||
page = (ROOT / "B07_DesignDetail" / "B07_DesignDetail_UI_Page.ts").read_text(encoding="utf-8")
|
||||
assert '"여기서 고치기"' in page
|
||||
assert "confirmButtonRow.append(specJumpButton" in page
|
||||
# 표준도 장에서만 뜬다 — 다른 도면에는 갈 곳이 없다.
|
||||
assert "specJumpButton.hidden = false" in page
|
||||
assert "specJumpButton.hidden = true" in page
|
||||
# 폼에 버림 칸이 섰다 — 끄는 자리가 화면에 있다(확정 8-2).
|
||||
form = (ROOT / "B07_DesignDetail" / "B07_DesignDetail_UI_StandardSpec.ts").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
assert "blinding_concrete" in form
|
||||
Reference in New Issue
Block a user