Merge remote-tracking branch 'origin/main_laptop_1' into main_desktop_1
This commit is contained in:
@@ -802,6 +802,26 @@
|
||||
"required": false,
|
||||
"phase": "b05"
|
||||
},
|
||||
{
|
||||
"key": "thickness_top_m",
|
||||
"label": "상부 두께",
|
||||
"input": "number",
|
||||
"unit": "m",
|
||||
"default": null,
|
||||
"required": false,
|
||||
"phase": "detail",
|
||||
"empty_means": "비우면 실무 구조물도 식으로 돎(뒷길이 + 0.30) — 값을 넣으면 그 값이 이김"
|
||||
},
|
||||
{
|
||||
"key": "thickness_bottom_m",
|
||||
"label": "하부 두께",
|
||||
"input": "number",
|
||||
"unit": "m",
|
||||
"default": null,
|
||||
"required": false,
|
||||
"phase": "detail",
|
||||
"empty_means": "비우면 상부 + 0.30×(H−1) 로 돎 — 값을 넣으면 그 값이 이김"
|
||||
},
|
||||
{
|
||||
"key": "back_len_cm",
|
||||
"label": "뒷길이",
|
||||
@@ -944,6 +964,26 @@
|
||||
"required": false,
|
||||
"phase": "b05"
|
||||
},
|
||||
{
|
||||
"key": "thickness_top_m",
|
||||
"label": "상부 두께",
|
||||
"input": "number",
|
||||
"unit": "m",
|
||||
"default": null,
|
||||
"required": false,
|
||||
"phase": "detail",
|
||||
"empty_means": "비우면 실무 구조물도 식으로 돎(뒷길이 + 0.30) — 값을 넣으면 그 값이 이김"
|
||||
},
|
||||
{
|
||||
"key": "thickness_bottom_m",
|
||||
"label": "하부 두께",
|
||||
"input": "number",
|
||||
"unit": "m",
|
||||
"default": null,
|
||||
"required": false,
|
||||
"phase": "detail",
|
||||
"empty_means": "비우면 상부 + 0.30×(H−1) 로 돎 — 값을 넣으면 그 값이 이김"
|
||||
},
|
||||
{
|
||||
"key": "back_len_cm",
|
||||
"label": "뒷길이",
|
||||
@@ -1142,6 +1182,26 @@
|
||||
"phase": "b05",
|
||||
"default_basis": "0 = 자동 자리 그대로 — 옮기지 않음을 뜻함(기슭막이와 같은 벌)"
|
||||
},
|
||||
{
|
||||
"key": "thickness_top_m",
|
||||
"label": "상부 두께",
|
||||
"input": "number",
|
||||
"unit": "m",
|
||||
"default": null,
|
||||
"required": false,
|
||||
"phase": "detail",
|
||||
"empty_means": "비우면 실무 구조물도 식으로 돎(뒷길이 + 0.30) — 값을 넣으면 그 값이 이김"
|
||||
},
|
||||
{
|
||||
"key": "thickness_bottom_m",
|
||||
"label": "하부 두께",
|
||||
"input": "number",
|
||||
"unit": "m",
|
||||
"default": null,
|
||||
"required": false,
|
||||
"phase": "detail",
|
||||
"empty_means": "비우면 상부 + 0.30×(H−1) 로 돎 — 값을 넣으면 그 값이 이김"
|
||||
},
|
||||
{
|
||||
"key": "back_len_cm",
|
||||
"label": "뒷길이",
|
||||
@@ -1479,6 +1539,26 @@
|
||||
"required": true,
|
||||
"phase": "detail"
|
||||
},
|
||||
{
|
||||
"key": "thickness_top_m",
|
||||
"label": "상부 두께",
|
||||
"input": "number",
|
||||
"unit": "m",
|
||||
"default": null,
|
||||
"required": false,
|
||||
"phase": "detail",
|
||||
"empty_means": "비우면 실무 구조물도 식으로 돎(뒷길이 + 0.30) — 값을 넣으면 그 값이 이김"
|
||||
},
|
||||
{
|
||||
"key": "thickness_bottom_m",
|
||||
"label": "하부 두께",
|
||||
"input": "number",
|
||||
"unit": "m",
|
||||
"default": null,
|
||||
"required": false,
|
||||
"phase": "detail",
|
||||
"empty_means": "비우면 상부 + 0.30×(H−1) 로 돎 — 값을 넣으면 그 값이 이김"
|
||||
},
|
||||
{
|
||||
"key": "back_len_cm",
|
||||
"label": "뒷길이 ℓ3",
|
||||
@@ -1759,6 +1839,26 @@
|
||||
"required": false,
|
||||
"phase": "b05"
|
||||
},
|
||||
{
|
||||
"key": "thickness_top_m",
|
||||
"label": "상부 두께",
|
||||
"input": "number",
|
||||
"unit": "m",
|
||||
"default": null,
|
||||
"required": false,
|
||||
"phase": "detail",
|
||||
"empty_means": "비우면 실무 구조물도 식으로 돎(뒷길이 + 0.30) — 값을 넣으면 그 값이 이김"
|
||||
},
|
||||
{
|
||||
"key": "thickness_bottom_m",
|
||||
"label": "하부 두께",
|
||||
"input": "number",
|
||||
"unit": "m",
|
||||
"default": null,
|
||||
"required": false,
|
||||
"phase": "detail",
|
||||
"empty_means": "비우면 상부 + 0.30×(H−1) 로 돎 — 값을 넣으면 그 값이 이김"
|
||||
},
|
||||
{
|
||||
"key": "back_len_cm",
|
||||
"label": "뒷길이",
|
||||
|
||||
@@ -277,6 +277,8 @@ export function putStandardSheetSpec(
|
||||
foundation: string | null;
|
||||
stone_coeff_basis: string | null;
|
||||
fill_concrete_mpa: string | null;
|
||||
thickness_top_m: string | null;
|
||||
thickness_bottom_m: string | null;
|
||||
},
|
||||
): Promise<{ status: string; revision: number; changed: number; notes: string[] }> {
|
||||
return requestJson(`/projects/${projectId}/standard-sheets/spec`, {
|
||||
|
||||
@@ -33,6 +33,8 @@ EDITABLE_KEYS: tuple[str, ...] = (
|
||||
"foundation",
|
||||
"stone_coeff_basis",
|
||||
"fill_concrete_mpa",
|
||||
"thickness_top_m",
|
||||
"thickness_bottom_m",
|
||||
)
|
||||
|
||||
#: 기초 갈래 — 정본 xls 탭 제목 그대로(`04.구조도(기슭막이).xls`).
|
||||
@@ -55,6 +57,8 @@ FIELD_LABELS: dict[str, str] = {
|
||||
"foundation": "기초",
|
||||
"stone_coeff_basis": "야면석 계수",
|
||||
"fill_concrete_mpa": "채움 강도",
|
||||
"thickness_top_m": "상부 두께",
|
||||
"thickness_bottom_m": "하부 두께",
|
||||
}
|
||||
|
||||
#: 야면석 계수를 어느 열에서 읽나 — 확정 ⑨ 「품셈 열이 기본, 사용자가 고를 수 있게」.
|
||||
@@ -147,6 +151,26 @@ def clean_spec(spec: dict[str, Any]) -> tuple[dict[str, Any], list[str]]:
|
||||
if note:
|
||||
notes.append(note)
|
||||
|
||||
# 벽 두께 — 비우면 실무 구조물도 식(확정 2차 ② 후반). 값을 넣으면 그 값이 이긴다.
|
||||
for key in ("thickness_top_m", "thickness_bottom_m"):
|
||||
if key not in spec:
|
||||
continue
|
||||
raw = spec.get(key)
|
||||
if raw in (None, ""):
|
||||
cleaned[key] = None
|
||||
continue
|
||||
try:
|
||||
value = float(raw)
|
||||
except (TypeError, ValueError):
|
||||
notes.append(
|
||||
f"{FIELD_LABELS[key]} 「{raw}」를 숫자로 읽지 못했습니다 — 비우면 식으로 돕니다."
|
||||
)
|
||||
continue
|
||||
if value <= 0:
|
||||
notes.append(f"{FIELD_LABELS[key]}는 0보다 커야 합니다 — 비우면 식으로 돕니다.")
|
||||
continue
|
||||
cleaned[key] = value
|
||||
|
||||
return cleaned, notes
|
||||
|
||||
|
||||
|
||||
@@ -73,6 +73,8 @@ class StandardSheetSpecRequest(BaseModel):
|
||||
foundation: str | None = None
|
||||
stone_coeff_basis: str | None = None
|
||||
fill_concrete_mpa: str | None = None
|
||||
thickness_top_m: float | str | None = None
|
||||
thickness_bottom_m: float | str | None = None
|
||||
|
||||
|
||||
@router.put("/{project_id}/standard-sheets/spec")
|
||||
|
||||
@@ -40,11 +40,16 @@ export interface StandardSpecResult {
|
||||
foundation: string | null;
|
||||
stone_coeff_basis: string | null;
|
||||
fill_concrete_mpa: string | null;
|
||||
thickness_top_m: string | null;
|
||||
thickness_bottom_m: string | null;
|
||||
}
|
||||
|
||||
/** 이 종류가 돌쌓기 계열인가 — 옹벽·집수정에는 이 칸들이 뜻이 없다. */
|
||||
const STONE_TYPES = new Set(["masonry_wet", "masonry_dry", "boulder_masonry"]);
|
||||
|
||||
/** 벽 두께 칸을 받는 종류 — **큰돌쌓기는 두께 식이 달라** 등록부에 칸이 없다(2026-09-09). */
|
||||
const THICKNESS_TYPES = new Set(["masonry_wet", "masonry_dry"]);
|
||||
|
||||
function field(label: string, control: HTMLElement, hint?: string): HTMLLabelElement {
|
||||
const wrap = document.createElement("label");
|
||||
wrap.className = "b07-spec__field";
|
||||
@@ -126,6 +131,19 @@ export function buildStandardSpecPanel(
|
||||
slope.placeholder = "비우면 자동";
|
||||
slope.value = options.face_slope_ratio == null ? "" : String(options.face_slope_ratio);
|
||||
|
||||
// 벽 두께 — 비우면 실무 구조물도 식(상부 = 뒷길이 + 0.30 · 하부 = 상부 + 0.30×(H−1)).
|
||||
const thickness = (key: "thickness_top_m" | "thickness_bottom_m"): HTMLInputElement => {
|
||||
const input = document.createElement("input");
|
||||
input.className = "b07-spec__input";
|
||||
input.type = "text";
|
||||
input.inputMode = "decimal";
|
||||
input.placeholder = "비우면 식";
|
||||
input.value = options[key] == null ? "" : String(options[key]);
|
||||
return input;
|
||||
};
|
||||
const topThickness = thickness("thickness_top_m");
|
||||
const bottomThickness = thickness("thickness_bottom_m");
|
||||
|
||||
panel.append(
|
||||
field("돌 종류", kind),
|
||||
field("조달", supply, "비우면 「캔다」로 봅니다."),
|
||||
@@ -139,6 +157,12 @@ export function buildStandardSpecPanel(
|
||||
field("야면석 계수", coeff, "비우면 품셈 열을 씁니다."),
|
||||
field("채움 강도 (MPa)", mpa, "비우면 210. 180 은 국가기준 하한입니다."),
|
||||
);
|
||||
if (THICKNESS_TYPES.has(sheet.type_id)) {
|
||||
panel.append(
|
||||
field("상부 두께 (m)", topThickness, "비우면 뒷길이 + 0.30 으로 섭니다."),
|
||||
field("하부 두께 (m)", bottomThickness, "비우면 상부 + 0.30×(H−1) 로 섭니다."),
|
||||
);
|
||||
}
|
||||
|
||||
const notes = document.createElement("ul");
|
||||
notes.className = "b07-spec__notes";
|
||||
@@ -175,6 +199,12 @@ export function buildStandardSpecPanel(
|
||||
foundation: foundation.value || null,
|
||||
stone_coeff_basis: coeff.value || null,
|
||||
fill_concrete_mpa: mpa.value || null,
|
||||
thickness_top_m: THICKNESS_TYPES.has(sheet.type_id)
|
||||
? topThickness.value.trim() || null
|
||||
: null,
|
||||
thickness_bottom_m: THICKNESS_TYPES.has(sheet.type_id)
|
||||
? bottomThickness.value.trim() || null
|
||||
: null,
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
|
||||
@@ -639,6 +639,56 @@ def _num(value: Any, fallback: float = 0.0) -> float:
|
||||
return float(value) if isinstance(value, (int, float)) else fallback
|
||||
|
||||
|
||||
#: 벽 두께를 **사용자가 덮어쓰는 칸** — 비우면 실무 구조물도 식으로 돈다(확정 2차 ② 후반
|
||||
#: 「사용자가 값을 바꿀 수 있게」). 야면석 계수·채움 강도와 같은 자리다.
|
||||
WALL_THICKNESS_KEYS: tuple[str, str] = ("thickness_top_m", "thickness_bottom_m")
|
||||
|
||||
|
||||
def wall_thickness(
|
||||
options: dict[str, Any], *, back_cm: float, height_m: float
|
||||
) -> tuple[float, float, str]:
|
||||
"""(상부 두께 m, 하부 두께 m, 근거 문구) — **사용자가 넣은 값이 식을 이긴다.**
|
||||
|
||||
⚠ 빈 칸은 「안 정함」이다 — 자동으로 나온 값을 저장에 박지 않는다(`face_slope_ratio`
|
||||
와 같은 태도). 상부만 넣으면 하부는 **그 상부 위에서** 식을 이어 간다 — 벽 모양
|
||||
(아래로 벌어지는 기울기)을 사용자 값 위에 그대로 얹는 것이 자연스럽다.
|
||||
"""
|
||||
|
||||
def given(key: str) -> float | None:
|
||||
raw = options.get(key)
|
||||
if raw in (None, ""):
|
||||
return None
|
||||
try:
|
||||
value = float(raw)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
return value if value > 0 else None
|
||||
|
||||
constants = STONE_MASONRY
|
||||
back_m = back_cm / 100.0
|
||||
top_given = given(WALL_THICKNESS_KEYS[0])
|
||||
top = top_given if top_given is not None else back_m + constants["thickness_top_add_m"]
|
||||
rise = constants["thickness_slope_per_m"] * max(
|
||||
height_m - constants["thickness_height_base_m"], 0.0
|
||||
)
|
||||
bottom_given = given(WALL_THICKNESS_KEYS[1])
|
||||
bottom = bottom_given if bottom_given is not None else top + rise
|
||||
top_basis = (
|
||||
f"상부 {top:.2f}(사용자 입력)"
|
||||
if top_given is not None
|
||||
else f"상부 {top:.2f}(뒷길이 {back_cm:g}㎝ + {constants['thickness_top_add_m']:g})"
|
||||
)
|
||||
bottom_basis = (
|
||||
f"하부 {bottom:.2f}(사용자 입력)"
|
||||
if bottom_given is not None
|
||||
else (
|
||||
f"하부 {bottom:.2f}(상부 + {constants['thickness_slope_per_m']:g}"
|
||||
f"×(H−{constants['thickness_height_base_m']:g}))"
|
||||
)
|
||||
)
|
||||
return top, bottom, f"실무 구조물도 식 — {top_basis} · {bottom_basis}"
|
||||
|
||||
|
||||
#: 큰돌쌓기(품셈 13-6) 직경 갈래 — **저장 제원 `stone_cm` 과 글자까지 같다**(레지스트리 확인).
|
||||
#: ⚠ 돌쌓기(13-4)의 **뒷길이** 축과 섞지 않는다. 앞서 섞여 있어 직경 60~80 짜리가
|
||||
#: 「뒷길이 45㎝」 계수로 돌던 자리다.
|
||||
@@ -871,16 +921,12 @@ def stone_masonry(
|
||||
# 실무 시트의 「정면적 × 1.04」가 바로 이 값이다(1:0.3 에서 1.0440 ≈ 1.04).
|
||||
masonry_area = face_area * math.hypot(1.0, slope_ratio)
|
||||
# 벽 두께 — 실무 구조물도 식(확정 2차 ②). **뒷길이가 들어간다.**
|
||||
back_m = back_cm / 100.0
|
||||
top_thickness = back_m + constants["thickness_top_add_m"]
|
||||
bottom_thickness = top_thickness + constants["thickness_slope_per_m"] * max(
|
||||
height_m - constants["thickness_height_base_m"], 0.0
|
||||
# ⚠ 표준도 제원에 상부·하부 두께 칸이 있으면 **그 값이 식을 이긴다**(확정 ② 후반).
|
||||
back_m = back_cm / 100.0 # 석적(아래)이 뒷길이를 m 로 쓴다
|
||||
top_thickness, bottom_thickness, thickness_basis = wall_thickness(
|
||||
options, back_cm=back_cm, height_m=height_m
|
||||
)
|
||||
thickness = (top_thickness + bottom_thickness) / 2.0
|
||||
thickness_basis = (
|
||||
f"실무 구조물도 식 — 상부 {top_thickness:.2f}(뒷길이 {back_cm}㎝ + 0.30) ·"
|
||||
f" 하부 {bottom_thickness:.2f}(상부 + 0.30×(H−1))"
|
||||
)
|
||||
volume = face_area * thickness # 입적
|
||||
|
||||
components = [
|
||||
|
||||
@@ -90,3 +90,45 @@ def test_안_고르면_계산식으로_서고_그_사실이_적힌다() -> None:
|
||||
assert 돌.source == "" # 관측 출처가 아니다
|
||||
assert "0.77" in 돌.basis and "2.65" in 돌.basis
|
||||
assert any("계산식" in n for n in notes)
|
||||
|
||||
|
||||
def test_두께_칸이_비면_식으로_돈다() -> None:
|
||||
"""확정 2차 ② 후반 「사용자가 값을 바꿀 수 있게」 — 빈 칸은 「안 정함」이다."""
|
||||
from B08_Quantity.B08_Quantity_Engine_UnitQuantity import wall_thickness
|
||||
|
||||
top, bottom, basis = wall_thickness({}, back_cm=45, height_m=2.5)
|
||||
assert abs(top - 0.75) < 1e-9 # 뒷길이 0.45 + 0.30
|
||||
assert abs(bottom - 1.20) < 1e-9 # 상부 + 0.30 × (2.5 − 1.0)
|
||||
assert "뒷길이" in basis and "사용자 입력" not in basis
|
||||
|
||||
|
||||
def test_넣은_두께가_식을_이긴다() -> None:
|
||||
from B08_Quantity.B08_Quantity_Engine_UnitQuantity import wall_thickness
|
||||
|
||||
top, bottom, basis = wall_thickness(
|
||||
{"thickness_top_m": 0.6, "thickness_bottom_m": "1.0"}, back_cm=45, height_m=2.5
|
||||
)
|
||||
assert (top, bottom) == (0.6, 1.0)
|
||||
assert basis.count("사용자 입력") == 2
|
||||
|
||||
|
||||
def test_상부만_넣으면_하부는_그_위에서_식을_잇는다() -> None:
|
||||
from B08_Quantity.B08_Quantity_Engine_UnitQuantity import wall_thickness
|
||||
|
||||
top, bottom, _ = wall_thickness({"thickness_top_m": 0.6}, back_cm=45, height_m=2.5)
|
||||
assert (top, abs(bottom - 1.05) < 1e-9) == (0.6, True) # 0.6 + 0.30 × 1.5
|
||||
|
||||
|
||||
def test_두께가_입적까지_닿는다() -> None:
|
||||
"""칸만 만들고 계산이 안 읽으면 「값이 안 닿는」 옛 사고와 같아진다."""
|
||||
기본, _ = 성분()
|
||||
두꺼움, _ = 성분(thickness_top_m=1.2, thickness_bottom_m=1.6)
|
||||
assert 두꺼움["입적"].amount > 기본["입적"].amount
|
||||
|
||||
|
||||
def test_이상한_값은_식으로_돌린다() -> None:
|
||||
from B08_Quantity.B08_Quantity_Engine_UnitQuantity import wall_thickness
|
||||
|
||||
for bad in ("", None, "두꺼움", -1, 0):
|
||||
top, _, basis = wall_thickness({"thickness_top_m": bad}, back_cm=45, height_m=2.5)
|
||||
assert abs(top - 0.75) < 1e-9 and "사용자 입력" not in basis, bad
|
||||
|
||||
Reference in New Issue
Block a user