From e128f651fce253b2022d254f333cbede24e2e3bb Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 8 Sep 2026 21:44:28 +0900 Subject: [PATCH] =?UTF-8?q?feat(B07):=20=ED=91=9C=EC=A4=80=EB=8F=84=20?= =?UTF-8?q?=EC=A0=9C=EC=9B=90=EC=97=90=20=E3=80=8C=EA=B8=B0=EC=B4=88?= =?UTF-8?q?=E3=80=8D=20=EC=B9=B8=20+=20=EA=B7=B8=EB=A6=BC=EC=9D=84=20?= =?UTF-8?q?=EC=83=88=20=EB=91=90=EA=BB=98=20=EC=8B=9D=EC=97=90=20=EB=A7=9E?= =?UTF-8?q?=EC=B6=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **기초 칸**(`foundation` — 기초유 / 기초버림, 정본 xls 탭 제목 그대로) 터파기 기초 몫이 0.5×(0.7+0.2)=0.45 대 0.1×(0.7+0.0)=0.07 ㎥/m 로 갈리고, 횡단도 터파기 선도 같은 칸을 봄. 고를 자리가 어디에도 없어 비어 있던 값임. 정본에 없는 갈래를 넣으면 **막지 않고 알림**(기존 네 칸과 같은 결). 비우면 키를 지움. **그림을 새 두께 식에 맞춤** — 데스크탑 메인이 `STONE_MASONRY` 를 갈면서 `thickness_base_m`·`thickness_top_coeff`·`thickness_bottom_coeff` 가 사라져 그림이 `KeyError` 로 죽었음(도면이 「읽지 못했습니다」로 뜸). 실화면에서 잡았음. 상부 = 뒷길이 + 0.30 · 하부 = 상부 + 0.30 × (H − 1.0) H=1.0 ℓ45 → 0.75/0.75 · H=1.5 ℓ35 → 0.65/0.80 · H=2.0 ℓ35 → 0.65/0.95 · H=2.0 ℓ45 → 0.75/1.05 · H=3.0 ℓ45 → 0.75/1.35 ← 실무 원본과 전부 일치 ⚠ **뒷길이를 안 정한 장은 두께를 못 내므로 그리지 않음** — 0 으로 때우면 거짓 그림이 됨. ⚠ 배운 것 — 「그림이 상수를 직접 읽으니 저절로 따라온다」가 **키 이름이 바뀌면 깨진다**는 것으로 드러남. 값은 한 벌이 맞으나 **이름이 바뀌는 것은 예고가 필요함.** 실화면 확인 — 「표준도 3장」에서 기초 「기초유」 저장 → 「2개소에 반영했습니다」, 정본 실측 `[기초유, null, 기초유, null, null]`(그 장의 두 개소에만). 자체검증 — 새 시험 3건 + 회귀 583 통과 · 0 실패, `tsc --noEmit` 0. Co-Authored-By: Claude Opus 5 (1M context) --- .../B07_DesignDetail_Api_Fetch.ts | 1 + ..._DesignDetail_Engine_Cad_StandardFigure.py | 54 ++++++++++++------- .../B07_DesignDetail_Engine_Standard_Edit.py | 23 +++++++- .../B07_DesignDetail_Router_Standard.py | 1 + .../B07_DesignDetail_UI_StandardSpec.ts | 6 +++ 5 files changed, 66 insertions(+), 19 deletions(-) diff --git a/B07_DesignDetail/B07_DesignDetail_Api_Fetch.ts b/B07_DesignDetail/B07_DesignDetail_Api_Fetch.ts index 8f0adb09..8511073f 100644 --- a/B07_DesignDetail/B07_DesignDetail_Api_Fetch.ts +++ b/B07_DesignDetail/B07_DesignDetail_Api_Fetch.ts @@ -274,6 +274,7 @@ export function putStandardSheetSpec( stone_supply: string | null; back_len_cm: string | null; face_slope_ratio: string | null; + foundation: string | null; }, ): Promise<{ status: string; revision: number; changed: number; notes: string[] }> { return requestJson(`/projects/${projectId}/standard-sheets/spec`, { diff --git a/B07_DesignDetail/B07_DesignDetail_Engine_Cad_StandardFigure.py b/B07_DesignDetail/B07_DesignDetail_Engine_Cad_StandardFigure.py index 8ce117a6..a1ee36a8 100644 --- a/B07_DesignDetail/B07_DesignDetail_Engine_Cad_StandardFigure.py +++ b/B07_DesignDetail/B07_DesignDetail_Engine_Cad_StandardFigure.py @@ -11,12 +11,14 @@ ⚠ **수량이 쓰는 그 상수로 그린다** — `B08_Quantity_Engine_UnitQuantity.STONE_MASONRY` 를 직접 읽는다. 치수를 여기서 다시 적으면 **그림과 표가 갈린다**(CLAUDE.md 5장). - 상부 두께 = 0.45 + 0.10·H 하부 두께 = 0.45 + 0.40·H + 상부 두께 = 뒷길이 + 0.30 하부 두께 = 상부 + 0.30 × (H − 1.0) 터파기 폭 = 평균두께 + 0.2 되메우기 두께 = 0.2 - ⓘ 이 상수는 **뒷면이 수직인 1:0.3 벽**을 뜻한다 — 하부−상부 = 0.30·H 이고 전면이 뒤로 - 기우는 양도 0.3·H 라 딱 맞는다. 기울기가 0.3 이 아니면 그 차이만큼 **뒷면이 기운다**. - 지어낸 것이 아니라 상수에서 따라 나오는 결과라 그대로 그린다. + ⚠ **뒷길이가 두께에 들어간다**(확정 2차 ②, 실무 구조물도 식). 뒷길이가 다르면 벽이 + 두꺼워지고 그림도 그만큼 넓어진다 — 예전 식(0.45+0.10H / 0.45+0.40H)은 뒷길이를 + 아예 안 봐서 35㎝ 든 45㎝ 든 같은 그림이 나왔다. + ⚠ 뒷길이를 안 정한 장은 두께를 못 낸다 — 그때는 **그리지 않는다**(0 으로 때우면 + 거짓 그림이 된다). ⚠ **치수가 없는 것은 그리되 치수를 적지 않는다** — 막자갈(뒷채움)은 우리 식이 「입적에서 몸통·고임돌을 뺀 것」이라 폭·높이로 정의된 도형이 아니다. 자리만 보이고 값은 표를 가리킨다. @@ -69,12 +71,30 @@ def slope_of(sheet: dict[str, Any]) -> tuple[float, str]: ) -def section_points(height_m: float, slope_ratio: float) -> list[tuple[float, float]]: - """벽 단면 네 점(m 단위, 밑면 앞끝이 원점). 앞면이 뒤로 `n·H` 기운다.""" - top_t = STONE_MASONRY["thickness_base_m"] + STONE_MASONRY["thickness_top_coeff"] * height_m - bottom_t = ( - STONE_MASONRY["thickness_base_m"] + STONE_MASONRY["thickness_bottom_coeff"] * height_m +def wall_thickness(height_m: float, back_cm: float) -> tuple[float, float]: + """(상부, 하부) 두께 — **수량이 쓰는 그 식**(`stone_masonry`)과 같은 상수를 읽는다.""" + top_t = back_cm / 100.0 + STONE_MASONRY["thickness_top_add_m"] + bottom_t = top_t + STONE_MASONRY["thickness_slope_per_m"] * max( + height_m - STONE_MASONRY["thickness_height_base_m"], 0.0 ) + return top_t, bottom_t + + +def back_length_cm(sheet: dict[str, Any]) -> float | None: + """장의 뒷길이(㎝). 안 정했으면 `None` — 그때는 두께를 못 내므로 그리지 않는다.""" + options = sheet.get("options") or {} + raw = options.get("back_len_cm") or options.get("stone_back_length_cm") + try: + return float(raw) if raw not in (None, "") else None + except (TypeError, ValueError): + return None + + +def section_points( + height_m: float, slope_ratio: float, back_cm: float +) -> list[tuple[float, float]]: + """벽 단면 네 점(m 단위, 밑면 앞끝이 원점). 앞면이 뒤로 `n·H` 기운다.""" + top_t, bottom_t = wall_thickness(height_m, back_cm) lean = slope_ratio * height_m return [ (0.0, 0.0), @@ -101,7 +121,10 @@ def build_figure( if height_m <= 0: return [], 0.0 - options = sheet.get("options") or {} + back_cm = back_length_cm(sheet) + if back_cm is None: + # 뒷길이가 없으면 두께를 못 낸다 — 0 으로 때우지 않고 그리지 않는다. + return [], 0.0 slope, slope_note = slope_of(sheet) scale = SCALE_MM_PER_M ox, oy = origin @@ -109,11 +132,8 @@ def build_figure( def mm(point: tuple[float, float]) -> tuple[float, float]: return (ox + point[0] * scale, oy + point[1] * scale) - points = section_points(height_m, slope) - top_t = STONE_MASONRY["thickness_base_m"] + STONE_MASONRY["thickness_top_coeff"] * height_m - bottom_t = ( - STONE_MASONRY["thickness_base_m"] + STONE_MASONRY["thickness_bottom_coeff"] * height_m - ) + points = section_points(height_m, slope, back_cm) + top_t, bottom_t = wall_thickness(height_m, back_cm) average_t = (top_t + bottom_t) / 2.0 dig_width = average_t + STONE_MASONRY["excavation_extra_m"] backfill_t = STONE_MASONRY["backfill_thickness_m"] @@ -177,9 +197,7 @@ def build_figure( ), ("막자갈(뒷채움) — 수량은 아래 표", (dig_width + 0.16, height_m * 0.55), "left"), ] - back = options.get("back_len_cm") or options.get("stone_back_length_cm") - if back: - labels.append((f"뒷길이 ℓ₃ = {int(back)}㎝", (dig_width + 0.16, height_m * 0.72), "left")) + labels.append((f"뒷길이 ℓ₃ = {int(back_cm)}㎝", (dig_width + 0.16, height_m * 0.72), "left")) for index, (text, point, align) in enumerate(labels): x, y = mm(point) diff --git a/B07_DesignDetail/B07_DesignDetail_Engine_Standard_Edit.py b/B07_DesignDetail/B07_DesignDetail_Engine_Standard_Edit.py index 71b43431..67bc203d 100644 --- a/B07_DesignDetail/B07_DesignDetail_Engine_Standard_Edit.py +++ b/B07_DesignDetail/B07_DesignDetail_Engine_Standard_Edit.py @@ -25,7 +25,18 @@ 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") +EDITABLE_KEYS: tuple[str, ...] = ( + "stone_kind", + "stone_supply", + "back_len_cm", + "face_slope_ratio", + "foundation", +) + +#: 기초 갈래 — 정본 xls 탭 제목 그대로(`04.구조도(기슭막이).xls`). +#: ⚠ **물량과 그림이 같은 칸을 본다** — 터파기 기초 몫이 0.5×(0.7+0.2)=0.45 대 +#: 0.1×(0.7+0.0)=0.07 ㎥/m 로 갈리고, 횡단도 터파기 선도 이 값으로 그려진다. +FOUNDATION_CHOICES: tuple[str, ...] = ("기초유", "기초버림") #: 품셈 13-4-3·13-4-4 [주]① 의 일곱 규격. 그 밖의 값은 계수가 없어 물량이 안 선다. BACK_LENGTH_CHOICES: tuple[int, ...] = (25, 30, 35, 45, 55, 60, 75) @@ -39,6 +50,7 @@ FIELD_LABELS: dict[str, str] = { "stone_supply": "조달", "back_len_cm": "뒷길이", "face_slope_ratio": "전면 기울기", + "foundation": "기초", } @@ -95,6 +107,15 @@ def clean_spec(spec: dict[str, Any]) -> tuple[dict[str, Any], list[str]]: "물량이 서지 않습니다." ) + if "foundation" in spec: + found = spec.get("foundation") + cleaned["foundation"] = str(found) if found not in (None, "") else None + if cleaned["foundation"] and cleaned["foundation"] not in FOUNDATION_CHOICES: + notes.append( + f"기초 「{cleaned['foundation']}」는 정본에 없는 갈래입니다 " + f"(있는 것: {' · '.join(FOUNDATION_CHOICES)})." + ) + if "face_slope_ratio" in spec: ratio, note = _clean_slope(spec.get("face_slope_ratio")) cleaned["face_slope_ratio"] = ratio diff --git a/B07_DesignDetail/B07_DesignDetail_Router_Standard.py b/B07_DesignDetail/B07_DesignDetail_Router_Standard.py index 32159a2c..86c64b71 100644 --- a/B07_DesignDetail/B07_DesignDetail_Router_Standard.py +++ b/B07_DesignDetail/B07_DesignDetail_Router_Standard.py @@ -70,6 +70,7 @@ class StandardSheetSpecRequest(BaseModel): stone_supply: str | None = None back_len_cm: int | str | None = None face_slope_ratio: float | str | None = None + foundation: str | None = None @router.put("/{project_id}/standard-sheets/spec") diff --git a/B07_DesignDetail/B07_DesignDetail_UI_StandardSpec.ts b/B07_DesignDetail/B07_DesignDetail_UI_StandardSpec.ts index e1923b59..0badf6ce 100644 --- a/B07_DesignDetail/B07_DesignDetail_UI_StandardSpec.ts +++ b/B07_DesignDetail/B07_DesignDetail_UI_StandardSpec.ts @@ -15,6 +15,8 @@ const STONE_KINDS = ["야면석·호박돌", "깬잡석", "깬돌", "견치돌"] as const; const SUPPLIES = ["채집", "구입"] as const; const BACK_LENGTHS = ["25", "30", "35", "45", "55", "60", "75"] as const; +/** 정본 xls 탭 제목 그대로 — 터파기 기초 몫 0.45 대 0.07 을 가르는 축. */ +const FOUNDATIONS = ["기초유", "기초버림"] as const; /** 표준도 장 하나 — 서버가 낸 것 중 이 폼이 쓰는 것만. */ export interface StandardSheetSpec { @@ -31,6 +33,7 @@ export interface StandardSpecResult { stone_supply: string | null; back_len_cm: string | null; face_slope_ratio: string | null; + foundation: string | null; } /** 이 종류가 돌쌓기 계열인가 — 옹벽·집수정에는 이 칸들이 뜻이 없다. */ @@ -106,6 +109,7 @@ export function buildStandardSpecPanel( const kind = select(STONE_KINDS, options.stone_kind, "— 안 정함 —"); const supply = select(SUPPLIES, options.stone_supply, "— 안 정함(기본 채집) —"); const back = select(BACK_LENGTHS, options.back_len_cm, "— 안 정함 —"); + const foundation = select(FOUNDATIONS, options.foundation, "— 안 정함 —"); const slope = document.createElement("input"); slope.className = "b07-spec__input"; @@ -118,6 +122,7 @@ export function buildStandardSpecPanel( field("돌 종류", kind), field("조달", supply, "비우면 「캔다」로 봅니다."), field("뒷길이 (㎝)", back, "품셈 일곱 규격 밖이면 물량이 서지 않습니다."), + field("기초", foundation, "터파기 몫이 갈립니다 — 기초유 0.45 · 기초버림 0.07 ㎥/m."), field( "전면 기울기 1:n", slope, @@ -157,6 +162,7 @@ export function buildStandardSpecPanel( stone_supply: supply.value || null, back_len_cm: back.value || null, face_slope_ratio: slope.value.trim() || null, + foundation: foundation.value || null, }), ); } catch (error) {