From 12feefadb55f3340c3038982680b4e89e6697455 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sat, 10 Oct 2026 15:08:18 +0900 Subject: [PATCH 1/2] =?UTF-8?q?chore(=EC=9A=94=EA=B5=AC):=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=EC=9E=90=20=EC=9A=94=EA=B5=AC=201010=5F2=20=EB=A1=9C?= =?UTF-8?q?=EC=BB=AC=20=EC=B6=94=EA=B0=80=EB=B6=84(53~74)=20=EB=B3=B4?= =?UTF-8?q?=EA=B4=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/raw/요구_1010_2/사용자요구_1010_2.md | 24 +++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/docs/raw/요구_1010_2/사용자요구_1010_2.md b/docs/raw/요구_1010_2/사용자요구_1010_2.md index 9cd35f849..32f0ba0a3 100644 --- a/docs/raw/요구_1010_2/사용자요구_1010_2.md +++ b/docs/raw/요구_1010_2/사용자요구_1010_2.md @@ -73,3 +73,27 @@ 50. 대시보드의 구조물옵션 정의가 제대로 연결안됨. 51. B08의 구조물 집계표에서 미연결 상태표시는 행의 좌측에 하나의 램프로 표기, 미지정 열을 필요없어 보임. 52. B08의 페이지들을 보면 합계가 나오는 행들을 보면 초기에는 행들이 추가되다가 특정 컨테이너 높이가 되면 합계 행을 고정하고 내부를 스크롤 방식으로 변경하고 있음. 이방법 자체는 좋은데 그렇다면 컨테이너의 높이제약을 현재 브라우져가 스크롤이 안되는 지점을 기준으로 적용하는게 조작 편의성이 좋아보임. 그럼 행들이 추가됨에 따라 밀리더라도 스크롤은 페이지스크롤은 없을테니 +53. B09의 박스보기 버튼과 가로 숫자 버튼, 그리고 그위의 계산들에 대한 안내는 같은행으로 변경 +54. 박스보기의 경우 지정된 카드 행들은 높이를 같게 변경 +55. 설계내역서의 공종 및 명칭의 글자들은 좌측 맞춤이고 좌측에 들여쓰기로 레벨이 표현되는게 맞아보임. 그리고 레벨 제어를 어떻게 하는지 모르겠음. +56. 박스보기에서 제목과 합계 정보열의 색은 다른색으로 지정하는게 테마고려하여 선택 +57. B08/B09의 테이블 양식이 에서 레벨이 필요한 정보들은 설계내역서의 레벨지정 양식을 따라가되 제어가 안되게 반영하면 일원화되지 않을까? 그리고 셀에 같은 값으로 표현되는 행들은 결국 병합인것같은데 맞지 않아? +58. 제비율표는 일력하는 공사원가계산서와 총괄설계내역서의 내용에서 요율을 원문 제비율표를 보고 누락이 된게 없는지 봐줘. (조사만) +59. 현재 B08/B09의 테이블 컨테이너의 양식을 보면 제일상단에 경고줄이 있어 이걸제일하단으로 옮겨줘. 이때 박스보기와 가로 카드수 정의는 보조탭 행에 우측맞춤으로 이동 +60. 다음으로 B09의 좌측패널의 원가 단가 컨테이너의 이름을 데이터 점검으로 이름을 변경하고 내부의 쓸대없는 설명은 삭제, 단가 갱신버튼의 제목을 업데이트, 업데이트가 필요한경우 버튼에 오버레이 마크 추가, 상세 페이지의 연결점검 탭은 데이터 점검 컨테이너 내부로 이동 +61. B09의 설계내역서에서 행들을 레벨이나 이동을 드래그나 키보드의 방향키로 제어가 되어야함. 구현하고 결국 내어쓰기와 들여쓰기는 결국 레벨이 되어야함. 상위 레벨로 이동이 되면 자연스럽게 하위항목의 합계가 세부항목이 되면 상위로 데이터를 전달하는 역할을 그리고 만약 기존 상위에 있던것들이 하위로 이동할경우에는 같이 이동되는 기능도 있어야함. 다음으로 일위대가와 단가산출의 정보를 연결할때 중요한건 마스터/회사/개인의 정보중 구조물조합, 일위대가사전조합, 현재처럼 벌써 들어와 있는 일위대가와 호표를 지정할 수도 있어야함. +62. B09의 좌측 패널의 컨테이너를 공용양식을 사용한건지 확인 (확인만) +63. B05/B06의 구조물배치 컨테이너에서 삭제버튼과 리셋버튼이 높이사이즈를 하위 컨테이너의 버튼 높이와 동일하게 구현하고 내부 컨테이너에서 삭제와 리셋버튼이 항상보이게 구현 +64. [사진] 현재 검증 서브1 프로젝트의 1+0 측점의 내용임. 이때 원래는 측구가 없어야하는 성토사면에 존재함. 이때 측구버튼과 일반L형 버튼이 없음. 숨기기 결과로 보임. 이런 현상의 원인이 궁금함. 한번 좌우를 바꾸면 재현안됨. 확실히 조사하여 반영할것. (사진: 1+0 · 양측 성토 · 성토사면 좌 18.61m · 우 0.26m · 좌|우 단추 중 「우」 켜짐 · 오른쪽 노견 바깥에 작은 측구 모양 · 측구 · 일반/L형 단추 안 보임) +65. B06의 상단의 성토 5m 이상, 포장 권장, 소단 검토에서 버튼의 형상을 사각 라운드로 변경하고 누를시 나오는 버튼들은 현재를 유지, 그리고 메인 페이지의 스크롤이 제일 상단에 있을경우 상단 버튼으로 내역을 열면 페이지가 아래로 밀림. 안밀리게 변경, 그리고 항목을 선택하면 이동하면서 해당 버튼셋은 접힘으로 그리고 만약 선택된 상태에서 다시 열면 하이라이트 기능, 다시 말해 사용자가 측점 횡단도를 누른상태에서 상단 3버튼을 누르면 하이라이트 되게 변경 +66. M01의 단가산출로직들의 항목에 신규열인 별칭 열이 필요함 설계자들이 찾을때 사용하는 명칭과 다른 경우가 있음. 이를 보완하기 위해 별칭열을 추가해줘. 내가 별칭을 있을수도 있고 없을 수도 있지만 값은 내가 넣을거야. 그리고 이때 프로그램 전역에서 단가산출 관련 검색은 별칭도 같이 검색이 되게 해야함. + +## 셋째 붙임 (10-10 오후) +67. B06 횡단도 상세 모달 — 셀에 지정된 면적과 길이값을 선택하게 하였는데 안잡히는 경우가 있어 개선이 필요함. 상세 모달창에서 선들이 교차되는 지점들을 기준으로 선들이 분할되어야함. 그리고 선택할때는 다중 선택을 하면 원지반도 가능해야함. 그럼 문제 없어보임. 그럼 면적에서도 폐쇄 영역을 지정하기 좋아보임. 추가로 선택영 지점의 면적과 선들의 길이의 값을 보는 기능이 있었으면 함. 이는 메인페이지의 기능이 있으니 확인, 잘 반영되었다면 메인페이지에도 구현한 생각임. +68. 간혹가다 B06의 상단에 누락된 구조물 측점이라고 측점추가 버튼이 나오는데 이유는? (조사) +69. 관보호공 유입 측 흙막이의 경우에 기본 높이를 배관 + 0.5m로 지정되어 있을것임. 이걸 0.7m로 변경 +70. 도수로/돌붙입의 구조물 형상을 잡고 이동하는 경우 토스트 메세지로 '기준측점을 옮겼습니다 ....' 나오는데 나는 도수로와 돌붙임의 이동에 대해 요청한건 없음. 왜그런지 분석하고 알려줘. (조사) +71. B07 (기반영요청사항) 횡단도의 격자에 배치를 옮기기 위해 선을 선택할때 원지반선이 안잡힘 +72. B07 (기반영요청사항) 추가로 레이어별로 측점의 원지반선과 설계선을 잡을 수 있는 레이어 자동 생성 +73. B07 횡단도의 붉은색 격사선을 하이드쇼할수 있는 버튼 기능은 좋은데 뒤에 검은 격자선이 하나더 있음. 확인 후 삭제 +74. B07 횡단도 표준 옵션의 버튼들이 양식화와 일원화 안되어 있음. 확인후 정리할 수 있는 양식으로 적용 From 0c729d656a73806ed025208cd4af4a396663768a Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sat, 10 Oct 2026 15:17:40 +0900 Subject: [PATCH 2/2] =?UTF-8?q?feat(B05=C2=B7B06=C2=B7B08):=20=EA=B4=80=20?= =?UTF-8?q?=EB=B3=B4=ED=98=B8=EA=B3=B5=20=EC=9C=A0=EC=9E=85=20=ED=9D=99?= =?UTF-8?q?=EB=A7=89=EC=9D=B4=20=EB=B9=88=20=EC=B9=B8=20=EA=B8=B0=EB=B3=B8?= =?UTF-8?q?=20=EB=86=92=EC=9D=B4=20=EA=B4=80=EA=B2=BD=20+=200.7=20m(?= =?UTF-8?q?=EC=9C=A0=EC=B6=9C=20=C2=B7=20=EC=A7=91=EC=88=98=EC=A0=95=200.5?= =?UTF-8?q?=20=EA=B7=B8=EB=8C=80=EB=A1=9C)(211-1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_018jonfdxn1Xit9bdkNJpE4G --- .../B04_PreProcess_Router_Basins.py | 7 +- B06_Section/B06_Section_Engine_Culvert.py | 15 +- .../B06_Section_UI_Cross_Culvert_Const.ts | 14 +- B08_Quantity/B08_Quantity_Router_Summary.py | 11 +- common_util/common_util_culvert_sets.ts | 36 ++- .../tester/test_139_1_pipe_height_follow.py | 9 +- .../tester/test_211_1_inlet_revet_height.py | 219 ++++++++++++++++++ .../tester/test_b05_new_structure_defaults.py | 7 +- .../test_b06_pipe_wall_height_mirror.py | 15 +- 9 files changed, 298 insertions(+), 35 deletions(-) create mode 100644 resources/tester/test_211_1_inlet_revet_height.py diff --git a/B04_PreProcess/B04_PreProcess_Router_Basins.py b/B04_PreProcess/B04_PreProcess_Router_Basins.py index 20429f91a..bbbb4973c 100644 --- a/B04_PreProcess/B04_PreProcess_Router_Basins.py +++ b/B04_PreProcess/B04_PreProcess_Router_Basins.py @@ -91,7 +91,7 @@ def _build( return detail, carried -# 관 보호공 · 유입 집수정 높이 칸 — 새 관 기본 = 관경 + 0.5(139-1) +# 관 보호공 · 유입 집수정 높이 칸 — 새 관 기본 = 관경 + 0.5(139-1) · 유입 흙막이만 + 0.7(211-1) PIPE_HEIGHT_KEYS = ("inlet_revet_height_m", "outlet_revet_height_m", "inlet_basin_height_m") @@ -130,7 +130,10 @@ def _apply_new_defaults( if diameter_m > 0: for key in PIPE_HEIGHT_KEYS: if key in keys: - options.setdefault(key, pipe_wall_min_height_m(diameter_m)) + side = ( + "inlet" if key == "inlet_revet_height_m" else "" + ) # 유입 흙막이 + 0.7(211-1) + options.setdefault(key, pipe_wall_min_height_m(diameter_m, side)) if point.facility == "ford_bridge" and "ford_height_m" not in options: flow = (flows or {}).get(round(point.chainage_m, 2)) height = ford_height_m(flow, float(options.get("ford_width_m") or 0)) diff --git a/B06_Section/B06_Section_Engine_Culvert.py b/B06_Section/B06_Section_Engine_Culvert.py index 24ce66c42..4f50a3c08 100644 --- a/B06_Section/B06_Section_Engine_Culvert.py +++ b/B06_Section/B06_Section_Engine_Culvert.py @@ -78,19 +78,23 @@ REVET_FACE_SLOPE = 0.3 # `test_b06_pipe_wall_height_mirror`. REVET_HEIGHT_STEP_M = 0.1 # 높이 눈금 REVET_EMBED_DEPTH_M = 0.5 # 근입 깊이 +# 관 보호공 **유입** 흙막이 빈 칸 기본 = 관경(올림) + 0.7 — 유출 · 집수정은 근입 0.5 그대로(211-1). +# ⚠ TS 짝 `common_util_culvert_sets.INLET_REVET_ADD_M` · `_Cross_Culvert_Const.INLET_REVET_ADD_M`. +INLET_REVET_ADD_M = 0.7 # 옛 정의 기본 높이(10-6 표 · 관경 무관) — 관경 > 1.0 관에 이 값이면 관 윗면보다 낮은 벽이라 # 읽을 때 관경 기준 값으로 봄(139-1). OLD_PIPE_WALL_DEFAULT_M = 1.5 -def pipe_wall_min_height_m(diameter_m: float) -> float: +def pipe_wall_min_height_m(diameter_m: float, side: str = "") -> float: """관 벽 **순수 높이**(근입 포함) 기본값 = 관경(0.1m 로 올림) + 근입 0.5 — 벽 윗면 = 관 윗면(139-1). - 관 · 유입 집수정 높이 칸의 새 기본값이자 빈 칸 값. + 관 · 유입 집수정 높이 칸의 새 기본값이자 빈 칸 값. `side="inlet"`(유입 흙막이) = + 0.7(211-1). """ raw = float(diameter_m) steps = math.ceil(raw / REVET_HEIGHT_STEP_M - 1e-9) - return round(steps * REVET_HEIGHT_STEP_M + REVET_EMBED_DEPTH_M, 3) + add = INLET_REVET_ADD_M if side == "inlet" else REVET_EMBED_DEPTH_M + return round(steps * REVET_HEIGHT_STEP_M + add, 3) def pipe_wall_height_m(stored: float | None, diameter_m: float) -> float | None: @@ -211,10 +215,11 @@ def _side_spec( spec["basin_form"] = str(options.get("inlet_structure") or "") or None return spec - # 높이를 안 적었으면 **관경 기준 높이**(관경 + 0.5 · 139-1) · 옛 1.5 도 관경 > 1.0 이면 그 값. + # 높이를 안 적었으면 **관경 기준 높이**(관경 + 0.5 · 유입 + 0.7 · 139-1 · 211-1) · + # 옛 1.5 도 관경 > 1.0 이면 관경 + 0.5(저장 값 해석 · 안 바뀜). height = pipe_wall_height_m(_number(options.get(f"{side}_revet_height_m"), None), diameter_m) if height is None: - height = pipe_wall_min_height_m(diameter_m) + height = pipe_wall_min_height_m(diameter_m, side) length = _number( options.get(f"{side}_revet_length_m"), None, diff --git a/B06_Section/B06_Section_UI_Cross_Culvert_Const.ts b/B06_Section/B06_Section_UI_Cross_Culvert_Const.ts index 673bfeaaf..a6ac1bf2b 100644 --- a/B06_Section/B06_Section_UI_Cross_Culvert_Const.ts +++ b/B06_Section/B06_Section_UI_Cross_Culvert_Const.ts @@ -71,6 +71,10 @@ export function pipeWallThicknessM(kind: string | null, diameterM: number): numb */ export const REVET_EMBED_DEPTH_M = 0.5; +/** 관 보호공 **유입** 흙막이 빈 칸 기본 순수 높이 = 관경(올림) + 0.7(211-1) — 유출은 근입 0.5 그대로. + * ⚠ 파이썬 짝 `INLET_REVET_ADD_M` · 세트 짝 `common_util_culvert_sets.INLET_REVET_ADD_M`(거울 시험). */ +export const INLET_REVET_ADD_M = 0.7; + /* 기슭막이 높이는 형태와 무관하게 **사용자 입력 그대로** — 형태별 높이 한계·원지반 맞춤 * 자동 자르기 없음. 형태가 바꾸는 것은 안 무늬뿐. 높이 기준은 **순수 높이**(벽 바닥~상단, * 근입 0.5m 포함). 단 하나 — 벽 윗면은 노면 위로 올라오지 않음(`buildRevetWallGeometry`). */ @@ -138,7 +142,7 @@ export function revetWallSpec( // 관경 기준 높이는 기본값만 — 하한은 그림이 서는 값뿐(54-51 · 사용자가 고름). const preferred = hiddenPipe ? (spec.revet_height_m ?? autoHeightM ?? REVET_EMBED_DEPTH_M) - : (spec.revet_height_m ?? pipeWallMinPureHeight(diameterM)); // 칸 값 → 그림(139-1) + : (spec.revet_height_m ?? pipeWallMinPureHeight(diameterM, spec.role)); // 칸 값 → 그림(139-1) const pureHeight = Math.max(adjust.h ?? preferred, WALL_MIN_PURE_HEIGHT_M); return { form, @@ -155,10 +159,12 @@ export const PIPE_CONNECT_GRADE = Math.tan((3 * Math.PI) / 180); /** * 배관 기슭막이 관 하부 여유(m) — 관 invert 아래 벽 바닥까지의 깊이 = 근입 0.5. * **순수 높이 기본 = 관경 + 0.5**(땅속 근입 0.5 포함 벽 전체 높이 · 벽 윗면 = 관 윗면 · - * 10-6 표 Ø1000 = 1.5 · 139-1). 칸을 비우면 이 값. + * 10-6 표 Ø1000 = 1.5 · 139-1 · 유입 흙막이 = 관경 + 0.7 · 211-1). 칸을 비우면 이 값. */ -export function pipeWallMinPureHeight(diameterM: number): number { - return revetTargetHeight(diameterM) + REVET_EMBED_DEPTH_M; +export function pipeWallMinPureHeight(diameterM: number, side = ""): number { + return ( + revetTargetHeight(diameterM) + (side === "inlet" ? INLET_REVET_ADD_M : REVET_EMBED_DEPTH_M) + ); } /** 벽 순수 높이 하한(m) — 0 이하는 그림이 안 섬. 관경 · 근입 하한 없음(54-51). */ export const WALL_MIN_PURE_HEIGHT_M = 0.1; diff --git a/B08_Quantity/B08_Quantity_Router_Summary.py b/B08_Quantity/B08_Quantity_Router_Summary.py index 3ac853296..104bcc4ac 100644 --- a/B08_Quantity/B08_Quantity_Router_Summary.py +++ b/B08_Quantity/B08_Quantity_Router_Summary.py @@ -187,11 +187,13 @@ def _revets( sides: list[tuple[str, str | None]], empty_height: float | None, names: dict[str, str], + inlet_empty_height: float | None = None, ) -> None: """흙막이 — 유입 · 유출(흙막이일 때 · `sides`) + 흙막이 n(container_rows 가 선 단까지만 냄). 저장 키 = 필수는 `{쪽}_revet_{칸}_m` · 선택 흙막이는 `soil_add_{n}_{칸}`(명세 인스턴스 키) · - 빈 높이 = `empty_height`(None 이면 「미입력」 열) · 머리 글은 관 정의 이름(공용 열). + 빈 높이 = `empty_height`(None 이면 「미입력」 열 · 관 유입 쪽만 `inlet_empty_height` · 211-1) · + 머리 글은 관 정의 이름(공용 열). """ sources = [(f"{side}_revet_", "_m", side, combo) for side, combo in sides] sources += [ @@ -203,7 +205,9 @@ def _revets( form = str(options.get(f"{head}form") or "") height = _num(options.get(f"{head}height{unit}")) if height is None: - height = empty_height + height = ( + inlet_empty_height if container == "inlet" and inlet_empty_height else empty_height + ) height_text = fill._label("height_m", height) sheet.add( at, @@ -588,7 +592,8 @@ def pipe_summary( ) wall_options = {**options, **moved} wall = pipe_wall_min_height_m(diameter / 1000) if diameter else None - _revets(sheet, at, wall_options, links, sources, wall, names) + wall_in = pipe_wall_min_height_m(diameter / 1000, "inlet") if diameter else None + _revets(sheet, at, wall_options, links, sources, wall, names, wall_in) _chutes(sheet, at, options, links, names) out = sheet.result() diff --git a/common_util/common_util_culvert_sets.ts b/common_util/common_util_culvert_sets.ts index be2df5118..e1a92706a 100644 --- a/common_util/common_util_culvert_sets.ts +++ b/common_util/common_util_culvert_sets.ts @@ -51,12 +51,15 @@ export const BOX_COVER_M = 0.5; export const REVET_HEIGHT_STEP_M = 0.1; export const REVET_EMBED_DEPTH_M = 0.5; export const OLD_PIPE_WALL_DEFAULT_M = 1.5; +/** 관 보호공 유입 흙막이 빈 칸 기본 = 관경(올림) + 0.7(211-1) — 유출 · 집수정은 근입 0.5 그대로. */ +export const INLET_REVET_ADD_M = 0.7; -/** 관 벽 순수 높이(근입 포함) 기본값 — 파이썬 `pipe_wall_min_height_m`. */ -export function pipeWallMinHeightM(diameterM: number): number { +/** 관 벽 순수 높이(근입 포함) 기본값 — 파이썬 `pipe_wall_min_height_m`. `side="inlet"` = + 0.7. */ +export function pipeWallMinHeightM(diameterM: number, side = ""): number { const raw = diameterM; const steps = Math.ceil(raw / REVET_HEIGHT_STEP_M - 1e-9); - return round(steps * REVET_HEIGHT_STEP_M + REVET_EMBED_DEPTH_M, 3); + const add = side === "inlet" ? INLET_REVET_ADD_M : REVET_EMBED_DEPTH_M; + return round(steps * REVET_HEIGHT_STEP_M + add, 3); } /** 관 벽 · 유입 집수정 높이 칸 읽기 — 옛 기본 1.5 이고 관경 > 1.0 이면 관경 기준 값(139-1) · @@ -71,7 +74,8 @@ export function pipeWallHeightM(stored: number | null, diameterM: number): numbe return stored; } -/** 관 보호공 · 유입 집수정 높이 칸 — 새 관 기본 = 관경 + 0.5(139-1 · 서버 짝 `PIPE_HEIGHT_KEYS`). */ +/** 관 보호공 · 유입 집수정 높이 칸 — 새 관 기본 = 관경 + 0.5(139-1) · 유입 흙막이만 + 0.7(211-1) + * (서버 짝 `PIPE_HEIGHT_KEYS`). */ export const PIPE_HEIGHT_KEYS = [ "inlet_revet_height_m", "outlet_revet_height_m", @@ -86,12 +90,14 @@ export function fillPipeHeights( const diameterM = Number(values.pipe_diameter_mm) / 1000; if (!(diameterM > 0)) return; for (const key of PIPE_HEIGHT_KEYS) - if (keys.includes(key)) values[key] ??= pipeWallMinHeightM(diameterM); + if (keys.includes(key)) + values[key] ??= pipeWallMinHeightM(diameterM, key === "inlet_revet_height_m" ? "inlet" : ""); } -/** 배관 관 보호공 높이 칸(유입 · 유출 · 유입 집수정 = 항목 `height`) — 관경 + 0.5 규칙(139-1). +/** 배관 관 보호공 높이 칸(유입 · 유출 · 유입 집수정 = 항목 `height`) — 관경 + 0.5 규칙(139-1) · + * 유입 흙막이 + 0.7(211-1). * `previousMm` 없음 = 읽을 때(옛 1.5 · 관경 > 1.0 옮김) · 있음 = 관경을 바꿈(옛 관경 규칙 값 · 옛 1.5 - * 칸만 새 규칙 값으로 · 손 값 · 빈 칸은 그대로). */ + * 칸만 새 규칙 값으로 · 손 값 · 빈 칸은 그대로 · 유입은 옛 + 0.5 값도 새 + 0.7 로 따라감). */ export function followPipeHeights( state: Record> | undefined>, diameterMm: string, @@ -99,7 +105,13 @@ export function followPipeHeights( ): void { const d = Number(diameterMm) / 1000; if (!(d > 0)) return; - const prev = previousMm ? pipeWallMinHeightM(Number(previousMm) / 1000) : null; + const prevOf = (id: string): Array => { + if (!previousMm) return []; + const p = Number(previousMm) / 1000; + return id === "inlet" + ? [pipeWallMinHeightM(p, id), pipeWallMinHeightM(p)] + : [pipeWallMinHeightM(p)]; + }; for (const id of ["inlet", "outlet"]) { const values = state[id]?.[0]; const raw = values?.height ?? ""; @@ -108,8 +120,10 @@ export function followPipeHeights( const moved = previousMm === undefined ? pipeWallHeightM(now, d) - : [prev, OLD_PIPE_WALL_DEFAULT_M].some((v) => v !== null && Math.abs(now - v) < 1e-9) - ? pipeWallMinHeightM(d) + : [...prevOf(id), OLD_PIPE_WALL_DEFAULT_M].some( + (v) => v !== null && Math.abs(now - v) < 1e-9, + ) + ? pipeWallMinHeightM(d, id) : now; if (moved !== null && moved !== now) values.height = String(moved); } @@ -170,7 +184,7 @@ function sideSpec( const height = pipeWallHeightM(num(options[`${side}_revet_height_m`], null), diameterM) ?? - pipeWallMinHeightM(diameterM); + pipeWallMinHeightM(diameterM, side); const length = num(options[`${side}_revet_length_m`], null); const form = text(options[`${side}_revet_form`]); const before = num(options[`${side}_revet_before_m`], null); diff --git a/resources/tester/test_139_1_pipe_height_follow.py b/resources/tester/test_139_1_pipe_height_follow.py index 978ac0f69..ddc9e2c0f 100644 --- a/resources/tester/test_139_1_pipe_height_follow.py +++ b/resources/tester/test_139_1_pipe_height_follow.py @@ -59,13 +59,16 @@ def test_화면_높이_규칙(tmp_path: Path) -> None: assert run.returncode == 0, run.stderr[-2000:] got = json.loads(result.read_text(encoding="utf-8")) - for key in ("inlet_revet_height_m", "outlet_revet_height_m", "inlet_basin_height_m"): + for key in ("outlet_revet_height_m", "inlet_basin_height_m"): assert got["fresh"][key] == pytest.approx(1.7), key # Φ1200 → 1.7 + assert got["fresh"]["inlet_revet_height_m"] == pytest.approx(1.9) # 유입 흙막이 + 0.7(211-1) assert "inlet_revet_height_m" not in got["noPipe"] # 관경 없음 = 안 채움 assert got["read"]["inlet"][0]["height"] == "1.7" # 옛 1.5 · Φ1200 → 1.7 assert got["read"]["outlet"][0]["height"] == "1.6" # 손 값 그대로 assert got["small"]["inlet"][0]["height"] == "1.5" # Φ800 의 1.5 는 그대로 - assert got["change"]["inlet"][0]["height"] == "2" # 옛 규칙 값(1.7) → 새 규칙(2.0) + assert ( + got["change"]["inlet"][0]["height"] == "2.2" + ) # 옛 + 0.5 값(1.7) → 유입 새 규칙(1.5 + 0.7) assert got["change"]["outlet"][0]["height"] == "2.2" # 손 값 그대로 - assert got["old"]["inlet"][0]["height"] == "1.3" # 옛 1.5 → Φ800 규칙 + assert got["old"]["inlet"][0]["height"] == "1.5" # 옛 1.5 → Φ800 유입 규칙(0.8 + 0.7) assert got["old"]["outlet"][0]["height"] == "" # 빈 칸 그대로 diff --git a/resources/tester/test_211_1_inlet_revet_height.py b/resources/tester/test_211_1_inlet_revet_height.py new file mode 100644 index 000000000..50fb28551 --- /dev/null +++ b/resources/tester/test_211_1_inlet_revet_height.py @@ -0,0 +1,219 @@ +"""211-1 관 보호공 **유입** 흙막이 빈 칸 기본 높이 = 관경(0.1 올림) + 0.7 — 유출 · 집수정은 + 0.5 그대로. + +못박는 것 + - 파이썬 `pipe_wall_min_height_m(d, "inlet")` · 새 관 기본값(`_apply_new_defaults`) · 관 세트 스펙 · B08 집계 빈 높이. + - TS 짝 `buildCulvertSets` ↔ 파이썬 `_culvert_set`(유입 · 유출 스펙 같은 값) · 횡단도 `revetWallSpec`. + - 저장된 높이는 그대로 · 유출 · 집수정 무변 · 흙막이 아닌 시설(숨은 관)은 본체 높이 그대로. +""" + +from __future__ import annotations + +import json +import subprocess +from pathlib import Path + +import pytest + +from B04_PreProcess import B04_PreProcess_Router_Basins as basins +from B05_Profile.B05_Profile_Structures_Schema import structure_type_map +from B06_Section import B06_Section_Engine_Culvert as culvert +from B08_Quantity import B08_Quantity_Router_Summary as summary +from common_util.common_util_drainage_pipes import PipePoint +from M02_MasterTemplete import M02_Item_Snapshot as snapshots +from M02_MasterTemplete.StructOptions import M02_SO_Definition as definition + +ROOT = Path(__file__).resolve().parents[2] +TSC = ROOT / "config" / "node_modules" / "typescript" / "bin" / "tsc" + + +def _revet(side: str, height: float | None = None) -> dict: + found = { + f"{side}_type": "기슭막이", + f"{side}_revet_form": "돌쌓기(메)", + f"{side}_revet_before_m": 5, + f"{side}_revet_after_m": 5, + } + if height is not None: + found[f"{side}_revet_height_m"] = height + return found + + +def test_파이썬_기본_높이_유입만_0_7(): + h = culvert.pipe_wall_min_height_m + assert h(0.8, "inlet") == 1.5 and h(0.8, "outlet") == 1.3 + assert h(1.2, "inlet") == 1.9 and h(1.2) == 1.7 # 쪽 안 줌 = 옛 값(집수정) + assert h(1.0, "inlet") == 1.7 and h(1.0, "outlet") == 1.5 + assert h(0.45, "inlet") == 1.2 # 0.5 로 올림 + 0.7 + + +def test_새_관_기본값_유입_흙막이만_0_7(): + fresh = PipePoint(chainage_m=10.0, source="stream", options={"pipe_diameter_mm": 1200}) + basins._apply_new_defaults([fresh]) + assert fresh.options["inlet_revet_height_m"] == pytest.approx(1.9) + assert fresh.options["outlet_revet_height_m"] == pytest.approx(1.7) + assert fresh.options["inlet_basin_height_m"] == pytest.approx(1.7) + # 이미 적은 높이(추천 · 손 값)는 그대로 · 나머지 칸만 채움 + kept = PipePoint( + chainage_m=20.0, + source="stream", + options={"pipe_diameter_mm": 1200, "inlet_revet_height_m": 1.2}, + ) + basins._apply_new_defaults([kept]) + assert kept.options["inlet_revet_height_m"] == 1.2 + assert kept.options["outlet_revet_height_m"] == pytest.approx(1.7) + + +def test_관_세트_빈_높이_유입_0_7_저장값_그대로(): + blank = culvert._culvert_set({"pipe_diameter_mm": 1200, **_revet("inlet"), **_revet("outlet")}) + assert blank["inlet"]["revet_height_m"] == pytest.approx(1.9) + assert blank["outlet"]["revet_height_m"] == pytest.approx(1.7) + stored = culvert._culvert_set( + {"pipe_diameter_mm": 1200, **_revet("inlet", 1.2), **_revet("outlet", 2.4)} + ) + assert stored["inlet"]["revet_height_m"] == 1.2 and stored["outlet"]["revet_height_m"] == 2.4 + # 옛 기본 1.5 · 관경 > 1.0 = 저장 값 해석(139-1) — 관경 + 0.5 그대로 + legacy = culvert._culvert_set({"pipe_diameter_mm": 1200, **_revet("inlet", 1.5)}) + assert legacy["inlet"]["revet_height_m"] == pytest.approx(1.7) + + +_RUNNER = """ +import { readFileSync, writeFileSync } from "node:fs"; +import { buildCulvertSets } from "./common_util_culvert_sets.js"; +const input = JSON.parse(readFileSync(process.argv[2], "utf8")); +const sets = buildCulvertSets(input.points, input.registry); +writeFileSync(process.argv[3], JSON.stringify([...sets.entries()])); +""" + +_WALL_RUNNER = """ +import { writeFileSync } from "node:fs"; +import { revetWallSpec } from "./B06_Section_UI_Cross_Culvert_Const.js"; +const pick = (spec, hidden, auto) => revetWallSpec(spec, {}, hidden, 1.2, auto).pureHeight; +const base = { structure: "기슭막이" }; +writeFileSync(process.argv[2], JSON.stringify({ + inlet: pick({ ...base, role: "inlet" }, false), + outlet: pick({ ...base, role: "outlet" }, false), + stored: pick({ ...base, role: "inlet", revet_height_m: 1.2 }, false), + hidden: pick({ ...base, role: "inlet" }, true, 2.0), +})); +""" + + +def _tsc(source: Path, out: Path) -> None: + subprocess.run( # noqa: S603 — 고정 실행 파일 · 타입 오류가 있어도 JS 는 나옴 + [ + "node", + str(TSC), + str(source), + "--outDir", + str(out), + "--module", + "esnext", + "--target", + "es2022", + "--moduleResolution", + "bundler", + "--skipLibCheck", + "--ignoreConfig", + ], + cwd=str(ROOT), + capture_output=True, + ) + + +@pytest.mark.skipif(not TSC.is_file(), reason="프론트엔드 의존성 미설치") +def test_TS_세트_거울(tmp_path: Path) -> None: + points = [ + { + "chainage_m": 20.0, + "facility": "pipe", + "options": {"pipe_diameter_mm": 1200, **_revet("inlet"), **_revet("outlet")}, + }, + { + "chainage_m": 40.0, + "facility": "pipe", + "options": {"pipe_diameter_mm": "800", **_revet("inlet", 1.2), **_revet("outlet")}, + }, + { + "chainage_m": 60.0, + "facility": "pipe", + "options": {"pipe_diameter_mm": 1200, **_revet("inlet", 1.5)}, # 옛 1.5 + }, + ] + registry = { + type_id: {o.key: o.default for o in entry.options} + for type_id, entry in structure_type_map().items() + if type_id in ("pipe", "ford_bridge", "ford_pavement", "box_culvert") + } + out = tmp_path / "js" + _tsc(ROOT / "common_util" / "common_util_culvert_sets.ts", out) + (out / "runner.mjs").write_text(_RUNNER, encoding="utf-8") + payload, result = tmp_path / "in.json", tmp_path / "out.json" + payload.write_text(json.dumps({"points": points, "registry": registry}), encoding="utf-8") + run = subprocess.run( # noqa: S603 + ["node", str(out / "runner.mjs"), str(payload), str(result)], + capture_output=True, + text=True, + ) + assert run.returncode == 0, run.stderr[-2000:] + got = {at: spec for at, spec in json.loads(result.read_text(encoding="utf-8"))} + for point in points: + want = culvert._culvert_set(point["options"]) + spec = got[point["chainage_m"]] + for side in ("inlet", "outlet"): + assert spec[side].get("revet_height_m") == want[side].get("revet_height_m"), point + assert got[20.0]["inlet"]["revet_height_m"] == pytest.approx(1.9) + assert got[20.0]["outlet"]["revet_height_m"] == pytest.approx(1.7) + assert got[40.0]["inlet"]["revet_height_m"] == 1.2 # 저장 값 그대로 + assert got[40.0]["outlet"]["revet_height_m"] == pytest.approx(1.3) + assert got[60.0]["inlet"]["revet_height_m"] == pytest.approx(1.7) # 옛 1.5 → 관경 + 0.5 + + +@pytest.mark.skipif(not TSC.is_file(), reason="프론트엔드 의존성 미설치") +def test_횡단도_벽_기본_높이(tmp_path: Path) -> None: + out = tmp_path / "js" + _tsc(ROOT / "B06_Section" / "B06_Section_UI_Cross_Culvert_Const.ts", out) + built = next( + out.rglob("B06_Section_UI_Cross_Culvert_Const.js") + ) # 가져온 파일 따라 경로가 깊어짐 + runner = built.parent / "runner.mjs" + runner.write_text(_WALL_RUNNER, encoding="utf-8") + result = tmp_path / "out.json" + run = subprocess.run( # noqa: S603 + ["node", str(runner), str(result)], capture_output=True, text=True + ) + assert run.returncode == 0, run.stderr[-2000:] + got = json.loads(result.read_text(encoding="utf-8")) + assert got["inlet"] == pytest.approx(1.9) # Φ1200 + 0.7 + assert got["outlet"] == pytest.approx(1.7) # 무변 + assert got["stored"] == 1.2 # 칸 값 그대로 + assert got["hidden"] == 2.0 # 숨은 관(세월교 · 박스) = 본체 높이 그대로 + + +def test_B08_집계_빈_높이_유입만_0_7(tmp_path: Path) -> None: + combo = {"layer": "system", "owner": None, "key": "SS000011", "name": "횡단배수 배관"} + root = tmp_path / "proj" + edits = root / "B04_PreProcess/drainage/edits" + edits.mkdir(parents=True) + point = { + "chainage_m": 1433.0, + "source": "user", + "options": {"pipe_diameter_mm": 800, **_revet("inlet"), **_revet("outlet")}, + "summary_item": combo, + "group_items": {"gnbf9m9": combo, "g9lq9uk": combo}, + } + (edits / "pipe_points.json").write_text( + json.dumps({"points": [point]}, ensure_ascii=False), encoding="utf-8" + ) + designs = [ + { + "chainage_m": 1433.0, + "design": {"pipe_length_m": 18, "inlet_basin": None, "extra_walls": []}, + } + ] + names = definition.read()["종류"]["pipe"]["이름"] + linked = {owner: {} for owner in snapshots.assignments(root)} + out = summary.pipe_summary(root, designs, names, 6.0, linked=linked) + ids = [c["id"] for c in out["columns"] if c["id"].startswith("pipe:revet")] + assert ids == ["pipe:revet|돌쌓기(메)|1.3", "pipe:revet|돌쌓기(메)|1.5"], ( + ids + ) # 유출 1.3 · 유입 1.5 diff --git a/resources/tester/test_b05_new_structure_defaults.py b/resources/tester/test_b05_new_structure_defaults.py index b85ce87ae..d6a60d202 100644 --- a/resources/tester/test_b05_new_structure_defaults.py +++ b/resources/tester/test_b05_new_structure_defaults.py @@ -34,8 +34,8 @@ def test_새_관만_기본값_빈_키에만(): fresh = PipePoint(chainage_m=10.0, source="stream", options={"pipe_diameter_mm": 800}) basins._apply_new_defaults([fresh]) # 추천(먼저 들어간 값)이 이김 · 집수정 모양 L 도 실제 값(54-39) - # 관 보호공 · 유입 집수정 높이 = 관경 + 0.5(139-1 · Φ800 → 1.3) - heights = dict.fromkeys(basins.PIPE_HEIGHT_KEYS, 1.3) + # 관 보호공 · 유입 집수정 높이 = 관경 + 0.5(139-1 · Φ800 → 1.3) · 유입 흙막이만 + 0.7(211-1 → 1.5) + heights = {**dict.fromkeys(basins.PIPE_HEIGHT_KEYS, 1.3), "inlet_revet_height_m": 1.5} expected = {**_defaults("pipe"), "pipe_diameter_mm": 800, "inlet_structure": "L", **heights} assert fresh.options == expected assert fresh.options["pipe_kind"] == "파형강관" and fresh.options["inlet_type"] == "집수정" @@ -66,7 +66,8 @@ def test_원본에_있던_관은_빈칸_그대로(monkeypatch): by = {p.chainage_m: p for p in points} assert by[100.0].options is None # 옛 빈칸 그대로 # 새로 보충된 관 = 기본값 + 집수정 모양 L(54-39) - heights = dict.fromkeys(basins.PIPE_HEIGHT_KEYS, 1.5) # 기본 관경 1000 → 1.5(139-1) + # 기본 관경 1000 → 1.5(139-1) · 유입 흙막이 1.7(211-1) + heights = {**dict.fromkeys(basins.PIPE_HEIGHT_KEYS, 1.5), "inlet_revet_height_m": 1.7} assert by[300.0].options == {**_defaults("pipe"), "inlet_structure": "L", **heights} diff --git a/resources/tester/test_b06_pipe_wall_height_mirror.py b/resources/tester/test_b06_pipe_wall_height_mirror.py index 75707760c..ad8b2b0fa 100644 --- a/resources/tester/test_b06_pipe_wall_height_mirror.py +++ b/resources/tester/test_b06_pipe_wall_height_mirror.py @@ -26,7 +26,7 @@ def _ts_const(source: str, name: str) -> float: def test_상수_셋이_TS_와_같다() -> None: - for name in ("REVET_HEIGHT_STEP_M", "REVET_EMBED_DEPTH_M"): + for name in ("REVET_HEIGHT_STEP_M", "REVET_EMBED_DEPTH_M", "INLET_REVET_ADD_M"): assert getattr(culvert, name) == _ts_const(TS, name) == _ts_const(SETS, name), name assert culvert.OLD_PIPE_WALL_DEFAULT_M == _ts_const(SETS, "OLD_PIPE_WALL_DEFAULT_M") @@ -35,9 +35,16 @@ def test_TS_식_모양이_그대로다() -> None: """식을 바꾸면 여기서 잡힘 — 파이썬 식도 함께 고칠 것.""" assert "const raw = diameterM;" in TS and "const raw = diameterM;" in SETS assert "Math.ceil(raw / REVET_HEIGHT_STEP_M - 1e-9) * REVET_HEIGHT_STEP_M" in TS - assert "return revetTargetHeight(diameterM) + REVET_EMBED_DEPTH_M;" in TS - # 그림이 칸 값을 읽음(139-1) — 배관 기본 = 칸 → 관경 기준 - assert "(spec.revet_height_m ?? pipeWallMinPureHeight(diameterM))" in TS + assert ( + 'revetTargetHeight(diameterM) + (side === "inlet" ? INLET_REVET_ADD_M : REVET_EMBED_DEPTH_M)' + in ( + " ".join(TS.split()) # 줄바꿈 · 들여쓰기 무시(포맷터가 접음) + ) + .replace("( ", "(") + .replace(" )", ")") + ) + # 그림이 칸 값을 읽음(139-1) — 배관 기본 = 칸 → 관경 기준(유입 + 0.7 · 211-1) + assert "(spec.revet_height_m ?? pipeWallMinPureHeight(diameterM, spec.role))" in TS # 옛 1.5 옮김 — TS 세트 짝 식 assert "Math.abs(stored - OLD_PIPE_WALL_DEFAULT_M) < 1e-9 &&" in SETS assert "diameterM > 1.0 + 1e-9" in SETS