From 1062d732ea1a61da86981d08b38330088db91cb4 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 8 Oct 2026 18:32:58 +0900 Subject: [PATCH] =?UTF-8?q?feat(B08):=20=EA=B5=AC=EC=A1=B0=EB=AC=BC=20?= =?UTF-8?q?=EC=A7=91=EA=B3=84=ED=91=9C=20=ED=9A=A1=EB=8B=A8=EB=B0=B0?= =?UTF-8?q?=EC=88=98=20BOX=20=C2=B7=20=EC=84=B8=EC=9B=94=EA=B5=90=20=C2=B7?= =?UTF-8?q?=20=EB=AC=BC=EB=84=98=EC=9D=B4=20=C2=B7=20=EA=B0=9C=EA=B1=B0=20?= =?UTF-8?q?=C2=B7=20=EB=85=B8=EC=B6=9C=ED=98=95=20=C2=B7=20=EB=A7=B9?= =?UTF-8?q?=EC=95=94=EA=B1=B0=20=EA=B0=88=EB=9E=98=20=C2=B7=20B06=20BOX=20?= =?UTF-8?q?=EB=B3=B8=EC=B2=B4=20=EA=B8=B8=EC=9D=B4=20=ED=82=A4(120-1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - B06 box_length_m(두 끝 offset 차) — STRUCTURE_ROW_KEYS ↔ _AREA_KEYS ↔ CrossSectionPatch (110 길) - 서버 집계 — BOX 본체 m · 날개벽 개소 · 세월교 월류 폭 m · 개소 · 날개벽 · 물넘이 H 별 월류 폭 · B05 개거 · 노출형 · 맹암거 개소 - 흙막이 · 돌붙임 열 관 · 박스 · 세월교 공용 · 세월교 빈 높이 = 미입력 열 · 같은 측점 박스 + 관 키 겹침 막음 · 응답 types Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EYJJRCfHfdCGNhmXTjQKV7 --- B06_Section/B06_Section_Api_Types.ts | 2 + B06_Section/B06_Section_Schema.py | 2 + .../B06_Section_Server_Calc_Prebuild.py | 1 + B06_Section/B06_Section_Structure_Layouts.ts | 13 + B08_Quantity/B08_Quantity_Router_Summary.py | 371 ++++++++++++++---- resources/tester/test_b06_design_key_split.py | 2 + 6 files changed, 323 insertions(+), 68 deletions(-) diff --git a/B06_Section/B06_Section_Api_Types.ts b/B06_Section/B06_Section_Api_Types.ts index 36ffe91d1..a9fcba04a 100644 --- a/B06_Section/B06_Section_Api_Types.ts +++ b/B06_Section/B06_Section_Api_Types.ts @@ -569,6 +569,8 @@ export interface CrossSectionPatch { /** B08 하상경사 원천(110) — 관 바닥 두 끝 경사 · 박스 두 끝 원지반 경사(%) · 관 길이와 같은 길. */ pipe_slope_pct?: number; box_ground_slope_pct?: number; + /** B08 구조물 집계표 BOX 본체 길이(m · 120) — 박스 두 끝 offset 차 · 같은 길. */ + box_length_m?: number; inlet_structure?: "auto" | "revet" | "I" | "L" | "U"; basin_adjust?: { innerWidthM: number; diff --git a/B06_Section/B06_Section_Schema.py b/B06_Section/B06_Section_Schema.py index 0cfea5cf6..c70f5dff8 100644 --- a/B06_Section/B06_Section_Schema.py +++ b/B06_Section/B06_Section_Schema.py @@ -188,6 +188,8 @@ class CrossSectionPatch(BaseModel): # (관 길이와 같은 길). 0 · 음수도 그대로 — 빈칸 판정은 B08 몫. pipe_slope_pct: float | None = None box_ground_slope_pct: float | None = None + # B08 구조물 집계표 BOX 본체 길이(120) — 박스 두 끝 offset 차(m) · 같은 길. + box_length_m: float | None = Field(default=None, ge=0) class SectionConfirmRequest(BaseModel): diff --git a/B06_Section/B06_Section_Server_Calc_Prebuild.py b/B06_Section/B06_Section_Server_Calc_Prebuild.py index aa6b89415..248cc7b9c 100644 --- a/B06_Section/B06_Section_Server_Calc_Prebuild.py +++ b/B06_Section/B06_Section_Server_Calc_Prebuild.py @@ -68,6 +68,7 @@ _AREA_KEYS = ( "pipe_length_m", "pipe_slope_pct", "box_ground_slope_pct", + "box_length_m", ) diff --git a/B06_Section/B06_Section_Structure_Layouts.ts b/B06_Section/B06_Section_Structure_Layouts.ts index 4a5b91695..fe697f0bd 100644 --- a/B06_Section/B06_Section_Structure_Layouts.ts +++ b/B06_Section/B06_Section_Structure_Layouts.ts @@ -159,6 +159,8 @@ export const STRUCTURE_ROW_KEYS = [ // B08 하상경사 원천(110) — 관 바닥 두 끝 경사 · 박스 두 끝 원지반 경사(%) · 같은 길로 실음. "pipe_slope_pct", "box_ground_slope_pct", + // B08 구조물 집계표 BOX 본체 길이(120) — 박스 두 끝(구체 끝) offset 차 · 같은 길로 실음. + "box_length_m", ] as const; /** 박스 두 끝(구체 끝) 원지반 경사(%) — 유입 − 유출 ÷ 수평거리. 본체는 늘 수평이라 원지반으로(110). */ @@ -171,6 +173,14 @@ function boxGroundSlopePct(section: CrossSection, box: BoxLayout): number | null return ((groundAt(inlet.offset) - groundAt(outlet.offset)) / run) * 100; } +/** 박스 본체 길이(m) — 두 끝(구체 끝) offset 차 · 한쪽이라도 없으면 null(120). */ +function boxLengthM(box: BoxLayout): number | null { + const inlet = box.sides.find((side) => side.wingRole === "inlet"); + const outlet = box.sides.find((side) => side.wingRole === "outlet"); + const run = inlet && outlet ? Math.abs(inlet.offset - outlet.offset) : 0; + return run > 0 ? run : null; +} + /** 폐회로 면적 입력(설계선·지반선·트림) — 트림이나 설계선이 모자라면 null. */ function areaInputOf(section: CrossSection, layouts: StoredLayouts) { const trim = trimOfLayouts(layouts); @@ -228,6 +238,9 @@ function areaRowOf( ? boxGroundSlopePct(section, layouts.box) : null; if (boxSlope !== null) ownRow.box_ground_slope_pct = Number(boxSlope.toFixed(4)); + const boxLength = + layouts.box && isOwner(section.box?.chainage_m) ? boxLengthM(layouts.box) : null; + if (boxLength !== null) ownRow.box_length_m = Number(boxLength.toFixed(4)); const pipeRow = Object.keys(ownRow).length > 1 ? ownRow : null; const areaInput = areaInputOf(section, layouts); if (!areaInput) return pipeRow; diff --git a/B08_Quantity/B08_Quantity_Router_Summary.py b/B08_Quantity/B08_Quantity_Router_Summary.py index c70159575..14745f646 100644 --- a/B08_Quantity/B08_Quantity_Router_Summary.py +++ b/B08_Quantity/B08_Quantity_Router_Summary.py @@ -1,7 +1,7 @@ """B08 구조물 집계표 길(PLAN 98-1) — 읽기만 · 계산 서버 단독(CLAUDE.md 5장 ③). GET /api/projects/{id}/quantity/summary - 배관(횡단배수관) 한 곳마다 컨테이너 값 → 실무 구조물집계표 꼴 열 · 측점 줄 · 계 + 횡단배수 한 곳마다 컨테이너 값 → 실무 구조물집계표 꼴 열 · 측점 줄 · 계 열 = 설계에 쓰인 값만 펼침 · 머리 = `group`[구조물, 형식, 규격] + `label`(단위 줄): 흙막이 › 형태 › H=높이 m 유입(흙막이일 때) · 유출 · 흙막이 n(실제 선 단까지) @@ -10,7 +10,15 @@ 관 공 › Φ{관경}m/m › 관매설 m B06 관 길이(1 m 올림 그대로) › 커플링 ea 올림(L ÷ 1본) − 1 · 1본 = 프로젝트 층 M02 구조물집계표 변수 › 관보호공 모양 실제로 선 집수정 모양마다 1 개소(98-2 `inlet_basin`) -머리 글 = M02 정의 이름(`구조물정의.json` 종류.pipe.이름 — 사용자가 M02 에서 고침) · + BOX암거 › {가로×세로} › 본체 m B06 새 키 `box_length_m`(두 끝 offset 차 · 120) + › 날개벽 › 유입 · 유출 개소 + 세월교 › Φ{관경}×{n}련 › 월류 폭 m · 본체 개소 · 날개벽 › 유입 · 유출 개소 + 물넘이포장 › 월류 폭 › H={월류 높이} m + 노출형 횡단수로 · 개거 · 맹암거(B05 구조물) 개소 + 흙막이 · 돌붙임 열은 관 · 박스 · 세월교 공용(실무 꼴) — 빈 높이: 관 = 관경 최소 · + 박스 = 박스 높이 · 세월교 = 「미입력」(본체 끝 높이는 기하에만) +열 id 접두 `pipe:` = 이 집계표(횡단배수) 이름공간 · 같은 측점 박스 + 관은 종류로 갈라 셈. +머리 글 = M02 정의 이름(`구조물정의.json` types.name · 종류.{종류}.이름 — 사용자가 M02 에서 고침) · 열마다 `combos` = 그 값을 낸 컨테이너에 이은 조합 id(`{층}|{주인}|{키}` · 95-3 모달 · 맨 윗 머리). 설계 = B08 토공과 같은 길(확정 노선 먼저) · 관 · 컨테이너 줄 = M02 채움 `collect_items` 재사용. """ @@ -47,6 +55,19 @@ ONE_LENGTH_VAR = "파형강관_1본_m" #: 필수 컨테이너 잇기 키(`design_structure_pipe.ts` link) — 관 · 유입 · 유출 LINKS = {"pipe": "grv6lny", "inlet": "gnbf9m9", "outlet": "g9lq9uk"} SOIL = "기슭막이" # 저장 값(화면 이름 흙막이) +GUARD_SOIL = "흙막이" # 박스 · 세월교 유입 · 유출 구조 값 +WING = "날개벽" +#: 박스 · 세월교 필수 컨테이너 잇기 키(`design_structure_box` · `_ford`) · 물넘이 경사각도 +BOX_LINKS = {"body": "gboxbody", "inlet": "gboxin", "outlet": "gboxout"} +FORD_LINKS = {"body": "gford", "inlet": "gfordin", "outlet": "gfordout"} +PAVE_LINKS = {"body": "gfordpaveslope"} +#: 옛 날개벽 설치 칸(「있음」 · 「없음」) +WING_KEYS = {"inlet": "wing_in", "outlet": "wing_out"} +SIDE_NAMES = {"inlet": "유입", "outlet": "유출"} +#: 개소 1 로 세는 B05 구조물(structures.json · 옵션 거의 없음) +SIMPLE = ("cross_drain_exposed", "open_ditch", "cross_underdrain") +#: 이 집계표가 펼치는 종류 — 화면은 이 종류를 조합 열에서 뺌(응답 `types`) +DRAIN_TYPES = ("pipe", "box_culvert", "ford_bridge", "ford_pavement", *SIMPLE) FORMS = ("돌쌓기(찰)", "돌쌓기(메)", "콘크리트", "돌망태", "통나무·목재틀", "바자") SHAPES = ("I", "L", "U") BONDS = ("찰붙임", "메붙임") @@ -95,6 +116,31 @@ def _basins(designs: Any) -> dict[float, str]: return found +def _station_values(designs: Any, key: str) -> dict[float, float]: + """설계 → `{측점: 양수 값}` — 값 없는 측점은 안 담음.""" + found: dict[float, float] = {} + for row in designs or []: + design = row.get("design") if isinstance(row, dict) else None + value = _num(design.get(key)) if isinstance(design, dict) else None + if value and value > 0: + found[round(float(row.get("chainage_m") or 0.0), 3)] = value + return found + + +def _guard(type_id: str, options: dict[str, Any], side: str) -> str: + """박스 · 세월교 유입 · 유출 구조(없음 · 날개벽 · 흙막이) — 옛 설치 칸 「있음」 = 날개벽. + 짝(TS): 박스 `boxOptionsIn` · 세월교 `withLegacyGuards`(`A00_Common/design/`).""" + wing = options.get(WING_KEYS[side]) + if type_id == "box_culvert": + return WING if wing == "있음" else str(wing or "") + kind = str(options.get(f"{side}_type") or "") + if wing == "있음": + return WING + if wing == "없음" and (kind or WING) == WING: + return "없음" + return kind + + def _revet_length(options: dict[str, Any], head: str, unit: str) -> float | None: """흙막이 길이 = 전 + 후 · 둘 다 비면 길이 칸.""" before = _num(options.get(f"{head}before{unit}")) @@ -150,46 +196,281 @@ def _order(values: tuple[str, ...], value: str) -> int: return values.index(value) if value in values else len(values) +def _revets( + sheet: _Sheet, + at: float, + options: dict[str, Any], + links: dict[str, str | None], + sides: list[tuple[str, str | None]], + empty_height: float | None, + names: dict[str, str], +) -> None: + """흙막이 — 유입 · 유출(흙막이일 때 · `sides`) + 흙막이 n(container_rows 가 선 단까지만 냄). + + 저장 키 = 필수는 `{쪽}_revet_{칸}_m` · 선택 흙막이는 `soil_add_{n}_{칸}`(명세 인스턴스 키) · + 빈 높이 = `empty_height`(None 이면 「미입력」 열) · 머리 글은 관 정의 이름(공용 열). + """ + sources = [(f"{side}_revet_", "_m", side, combo) for side, combo in sides] + sources += [ + (f"soil_add_{link[len('gsoiladd') :]}_", "", "soil_add", combo) + for link, combo in links.items() + if link.startswith("gsoiladd") + ] + for head, unit, container, combo in sources: + form = str(options.get(f"{head}form") or "") + height = _num(options.get(f"{head}height{unit}")) + if height is None: + height = empty_height + height_text = fill._label("height_m", height) + sheet.add( + at, + f"revet|{form or fill.EMPTY_LABEL}|{height_text}", + (0, _order(FORMS, form), form, math.inf if height is None else height), + [ + names.get(f"outlet.structure={SOIL}", SOIL), + names.get(f"{container}.form={form}", form) or fill.EMPTY_TEXT, + fill.EMPTY_TEXT if height is None else f"H={height_text}", + ], + "m", + _revet_length(options, head, unit), + [combo], + ) + + +def _chutes( + sheet: _Sheet, + at: float, + options: dict[str, Any], + links: dict[str, str | None], + names: dict[str, str], +) -> None: + """돌붙임 — 붙임 형식 · 면적이 다 있을 때만(설계자가 넣음).""" + for link, combo in links.items(): + if not link.startswith("gchuteadd"): + continue + head = f"chute_add_{link[len('gchuteadd') :]}_" + bond = str(options.get(f"{head}bond") or "") + area = _num(options.get(f"{head}area")) + if options.get(f"{head}structure") != "돌붙임" or not bond or not area: + continue + sheet.add( + at, + f"chute|{bond}", + (1, _order(BONDS, bond), bond), + [ + names.get("chute_add.structure=돌붙임", "돌붙임"), + names.get(f"chute_add.bond={bond}", bond), + ], + "㎡", + area, + [combo], + ) + + +def _wings( + sheet: _Sheet, + at: float, + type_id: str, + sort: int, + title: str, + options: dict[str, Any], + combos: dict[str, str | None], + names: dict[str, str], +) -> list[tuple[str, str | None]]: + """박스 · 세월교 날개벽(유입 · 유출 개소) — 흙막이 쪽은 돌려줌(공용 흙막이 열로).""" + soils: list[tuple[str, str | None]] = [] + for index, side in enumerate(("inlet", "outlet")): + kind = _guard(type_id, options, side) + if kind == GUARD_SOIL: + soils.append((side, combos.get(side))) + elif kind == WING: + sheet.add( + at, + f"{type_id}|wing|{side}", + (sort, 9, index), + [title, names.get(f"{side}.structure={WING}", WING), SIDE_NAMES[side]], + "개소", + 1.0, + [combos.get(side)], + ) + return soils + + +def _box( + sheet: _Sheet, + at: float, + base: dict[str, Any], + combos: dict[str, str | None], + names: dict[str, str], + length: float | None, +) -> float | None: + """BOX암거 본체(가로 × 세로마다 m) — 흙막이 빈 높이로 쓸 박스 높이를 돌려줌.""" + width = _num(base["options"].get("body_width_m")) + height = _num(base["options"].get("body_height_m")) + size = ( + f"{fill._label('width_m', width)}×{fill._label('height_m', height)}" + if width or height + else fill.EMPTY_LABEL + ) + sheet.add( + at, + f"box|body|{size}", + (3, 0, width or 0.0, height or 0.0), + [ + names["종류"], + names.get(f"body.size={size}", fill.EMPTY_TEXT if size == fill.EMPTY_LABEL else size), + "본체", + ], + "m", + length, + [_combo(base.get("summary_item")), combos.get("body")], + ) + return height + + +def _ford( + sheet: _Sheet, + at: float, + base: dict[str, Any], + combos: dict[str, str | None], + names: dict[str, str], +) -> None: + """세월교 — Φ관경×n련마다 월류 폭 m · 본체 개소.""" + diameter = int(_num(base["options"].get("pipe_diameter_mm")) or 1000) + count = int(_num(base["options"].get("pipe_count")) or 1) + spec = f"Φ{names.get(f'ford.diameter={diameter}', diameter)}×{count}련" + width_name = names.get("ford.width", "월류 폭").split(" (")[0] + cells = ( + ("width", width_name, "m", _num(base["options"].get("ford_width_m"))), + ("count", "본체", "개소", 1.0), + ) + for index, (key, label, unit, value) in enumerate(cells): + sheet.add( + at, + f"ford|{diameter}|{count}|{key}", + (4, 0, diameter, count, index), + [names["종류"], spec, label], + unit, + value, + [_combo(base.get("summary_item")), combos.get("body")], + ) + + +def _pavement( + sheet: _Sheet, + at: float, + base: dict[str, Any], + combos: dict[str, str | None], + names: dict[str, str], +) -> None: + """물넘이 포장 — 월류 높이(H)마다 월류 폭 m(실무 「물넘이 포장 H=1.0」).""" + height = _num(base["options"].get("ford_height_m")) + height_text = fill._label("height_m", height) + sheet.add( + at, + f"pave|{height_text}", + (5, math.inf if height is None else height), + [ + names["종류"], + names.get("slope.width", "월류 폭").split(" (")[0], + fill.EMPTY_TEXT if height is None else f"H={height_text}", + ], + "m", + _num(base["options"].get("ford_width_m")), + [_combo(base.get("summary_item")), combos.get("body")], + ) + + +def _drain( + sheet: _Sheet, + at: float, + type_id: str, + base: dict[str, Any], + options: dict[str, Any], + links: dict[str, str | None], + group: dict[str, Any], + kinds: dict[str, Any], + pipe_names: dict[str, str], + box_length: float | None, +) -> None: + """관 밖 횡단배수 한 곳 — 종류마다 본체 열 + 박스 · 세월교는 날개벽 · 흙막이 · 돌붙임(공용).""" + title = next( + (t["name"] for t in kinds.get("types") or [] if t.get("type_id") == type_id), type_id + ) + names = {**(((kinds.get("종류") or {}).get(type_id) or {}).get("이름") or {}), "종류": title} + if type_id in SIMPLE: + combos = [_combo(base.get("summary_item")), *links.values()] + sheet.add(at, type_id, (6, SIMPLE.index(type_id)), [title], "개소", 1.0, combos) + return + # 필수 컨테이너 잇기 — 정의 그룹에 없는 것(세월교)은 저장된 잇기에서 + required = {"box_culvert": BOX_LINKS, "ford_bridge": FORD_LINKS}.get(type_id, PAVE_LINKS) + combos = {part: links.get(link) or _combo(group.get(link)) for part, link in required.items()} + if type_id == "ford_pavement": + _pavement(sheet, at, base, combos, names) + return + if type_id == "box_culvert": + empty_height = _box(sheet, at, base, combos, names, box_length) + else: + _ford(sheet, at, base, combos, names) + empty_height = None # 본체 끝 높이는 기하에만 — 「미입력」 열 + sort = 3 if type_id == "box_culvert" else 4 + soils = _wings(sheet, at, type_id, sort, title, base["options"], combos, names) + _revets(sheet, at, options, links, soils, empty_height, pipe_names) + _chutes(sheet, at, options, links, pipe_names) + + def pipe_summary( project_root: str | Path, designs: list[dict[str, Any]], names: dict[str, str], one_m: float | None, + kinds: dict[str, Any] | None = None, ) -> dict[str, Any]: - """배관 한 곳마다 컨테이너 값 → 집계표 열 · 줄 · 계(저장 안 함).""" + """횡단배수 한 곳마다 컨테이너 값 → 집계표 열 · 줄 · 계(저장 안 함). + + `names` = 관 정의 이름(공용 흙막이 · 돌붙임 머리) · `kinds` = M02 정의(비면 읽음). + """ + kinds = kinds or definition.read() items = fill.collect_items( project_root, fill.pipe_lengths_from_designs(designs), soil_tiers=soil_tiers_from_designs(designs), ) # 높이 등 빈 칸은 정의 기본값이 아니라 그림 규칙을 따라야 해 저장 그대로를 따로 읽음 + # 같은 측점 박스 + 관은 복사본 id(`pipe@측점`)가 같음 — 종류로 가름 raw = { - f"pipe@{round(float(point.chainage_m), 3)}": dict(point.options or {}) + (point.facility or "pipe", f"pipe@{round(float(point.chainage_m), 3)}"): point for point in read_pipe_points_file(pipe_points_path_in(Path(project_root))) - if (point.facility or "pipe") == "pipe" } basins = _basins(designs) - by_owner: dict[str, dict[str, Any]] = {} + box_lengths = _station_values(designs, "box_length_m") + by_owner: dict[tuple[str, str], dict[str, Any]] = {} for item in items: - if item["type_id"] != "pipe": + if item["type_id"] not in DRAIN_TYPES: continue owner, _, link = item["복사본"].partition(GROUP_JOIN) - entry = by_owner.setdefault(owner, {"links": {}}) + entry = by_owner.setdefault((item["type_id"], owner), {"links": {}}) if link: entry["links"][link] = _combo(item.get("summary_item")) else: entry["base"] = item sheet = _Sheet() - no_length = no_basin = 0 - soil_name = names.get(f"outlet.structure={SOIL}", SOIL) - for owner, entry in by_owner.items(): + no_length = no_basin = no_box = 0 + for (type_id, owner), entry in by_owner.items(): base = entry.get("base") if base is None: continue links = entry["links"] - options = raw.get(owner, {}) + point = raw.get((type_id, owner)) + options = dict(point.options or {}) if point else {} at = float(base["chainage_m"]) + if type_id != "pipe": + box_length = fill._nearest(box_lengths, at) if type_id == "box_culvert" else None + no_box += type_id == "box_culvert" and not box_length + group = (point.group_items if point else None) or {} + _drain(sheet, at, type_id, base, options, links, group, kinds, names, box_length) + continue diameter = int(_num(base["options"].get("pipe_diameter_mm")) or 1000) pipe_head = ["관 공", f"Φ{names.get(f'pipe.diameter={diameter}', diameter)}m/m"] pipe_combos = [_combo(base.get("summary_item")), links.get(LINKS["pipe"])] @@ -226,59 +507,10 @@ def pipe_summary( [links.get(LINKS["inlet"])], ) - # 흙막이 — 유입(흙막이일 때) · 유출 · 흙막이 n(container_rows 가 실제 선 단까지만 냄) - # 저장 키 = 필수는 `{쪽}_revet_{칸}_m` · 선택 흙막이는 `soil_add_{n}_{칸}`(명세 인스턴스 키) - sources = [ - (f"{side}_revet_", "_m", side, links.get(LINKS[side])) - for side in ("inlet", "outlet") - if options.get(f"{side}_type") == SOIL - ] - sources += [ - (f"soil_add_{link[len('gsoiladd') :]}_", "", "soil_add", combo) - for link, combo in links.items() - if link.startswith("gsoiladd") - ] - for head, unit, container, combo in sources: - form = str(options.get(f"{head}form") or "") - height = _num(options.get(f"{head}height{unit}")) - if height is None: - height = pipe_wall_min_height_m(diameter / 1000) - height_text = fill._label("height_m", height) - sheet.add( - at, - f"revet|{form or fill.EMPTY_LABEL}|{height_text}", - (0, _order(FORMS, form), form, height), - [ - soil_name, - names.get(f"{container}.form={form}", form) or fill.EMPTY_TEXT, - f"H={height_text}", - ], - "m", - _revet_length(options, head, unit), - [combo], - ) - - # 돌붙임 — 붙임 형식 · 면적이 다 있을 때만(설계자가 넣음) - for link, combo in links.items(): - if not link.startswith("gchuteadd"): - continue - head = f"chute_add_{link[len('gchuteadd') :]}_" - bond = str(options.get(f"{head}bond") or "") - area = _num(options.get(f"{head}area")) - if options.get(f"{head}structure") != "돌붙임" or not bond or not area: - continue - sheet.add( - at, - f"chute|{bond}", - (1, _order(BONDS, bond), bond), - [ - names.get("chute_add.structure=돌붙임", "돌붙임"), - names.get(f"chute_add.bond={bond}", bond), - ], - "㎡", - area, - [combo], - ) + sides = [side for side in ("inlet", "outlet") if options.get(f"{side}_type") == SOIL] + sources = [(side, links.get(LINKS[side])) for side in sides] + _revets(sheet, at, options, links, sources, pipe_wall_min_height_m(diameter / 1000), names) + _chutes(sheet, at, options, links, names) out = sheet.result() notes = [] @@ -286,14 +518,17 @@ def pipe_summary( notes.append(f"관 연장 없음 {no_length}곳 — B06 횡단 [저장] 뒤 채워짐") if no_basin: notes.append(f"집수정 모양 기록 없음 {no_basin}곳 — B06 횡단 [저장] 뒤 채워짐") + if no_box: + notes.append(f"BOX 본체 길이 없음 {no_box}곳 — B06 횡단 [저장] 뒤 채워짐") if one_m is None: notes.append(f"1본 길이 변수({ONE_LENGTH_VAR}) 빈칸 — 커플링 빈칸") - return {**out, "notes": notes, "one_length_m": one_m} + return {**out, "notes": notes, "one_length_m": one_m, "types": list(DRAIN_TYPES)} def _summary(root: Path, designs: list[dict[str, Any]]) -> dict[str, Any]: - names = ((definition.read().get("종류") or {}).get("pipe") or {}).get("이름") or {} - return pipe_summary(root, designs, names, one_pipe_length(root)) + kinds = definition.read() + names = ((kinds.get("종류") or {}).get("pipe") or {}).get("이름") or {} + return pipe_summary(root, designs, names, one_pipe_length(root), kinds) @router.get("/{project_id}/quantity/summary") diff --git a/resources/tester/test_b06_design_key_split.py b/resources/tester/test_b06_design_key_split.py index e30b220df..cb0bfee71 100644 --- a/resources/tester/test_b06_design_key_split.py +++ b/resources/tester/test_b06_design_key_split.py @@ -43,6 +43,8 @@ _COMPUTED_FIELDS = { # B08 하상경사 원천(110) — 관 경사 · 박스 원지반 경사(%) · 관 길이와 같은 다리. "pipe_slope_pct", "box_ground_slope_pct", + # BOX 본체 길이(120) — 같은 다리. + "box_length_m", }