From 918e7c12dab4400b07573910d945b3d9683588f1 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 8 Sep 2026 21:01:46 +0900 Subject: [PATCH] =?UTF-8?q?fix(B07):=20=ED=91=9C=EC=A4=80=EB=8F=84?= =?UTF-8?q?=EA=B0=80=20=EB=8B=A8=EB=A9=B4=EC=9C=A0=ED=98=95=EC=9D=84=20?= =?UTF-8?q?=EC=95=88=20=EB=84=98=EA=B2=A8=20=EA=B8=B0=EC=9A=B8=EA=B8=B0=20?= =?UTF-8?q?=ED=8C=90=EC=A0=95=EC=9D=B4=20=ED=86=B5=EC=A7=B8=EB=A1=9C=20?= =?UTF-8?q?=EA=BA=BC=EC=A0=B8=20=EC=9E=88=EB=8D=98=20=EA=B2=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 표준도가 `build_table` 을 직접 부르면서 `section_modes` 를 안 넘겨, 성절토를 가를 근거가 없다며 **전 구조물이 종전값 1:0.3** 으로 섰음. 값이 없는 게 아니라 **안 넘긴 것**이었음. `section_modes_from_designs` 한 벌을 그대로 씀(부르는 쪽마다 다시 짜면 B08 과 갈림). 노선은 `get_workflow_route_context` 로 찾고, 못 찾으면 빈 표 — 성토로 눅이지 않음. ⚠ 고치고 나니 **제목·그림이 표와 갈렸음** — 표는 판정된 1:0.35, 제목은 다시 판정하며 근거를 못 받아 1:0.3. 실측으로 잡았음. 그래서 **표를 만든 그 판정을 시트에 실어** 제목·그림이 같은 값을 쓰게 함(`face`·`face_reason`). 자리를 고르는 것도 B08 이 쓰는 `_section_mode_at` 과 판정 한 벌 `structure_face_role` 을 그대로 부름. 실측 (프로젝트 936be972) — 제목과 표가 **같은 값** ``` 큰돌쌓기 H=2.5 제목 1:0.3 표 0.3 면적 2.6101 돌쌓기(메) H=2.0 제목 1:0.35 표 0.35 면적 2.1190 ← 메쌓기가 갈림 돌쌓기(찰) H=2.5 제목 1:0.3 표 0.3 면적 2.6101 옹벽 H=2.0 기울기 없음(판정 대상 아님) ``` 표 산출근거에 판정 경로가 그대로 뜸 — 「품셈 13-4-4 [주]⑪ 표준경사 · 메쌓기 성토 · 직고 2m ≤3m → 1:0.35 · right_cut · 자동(성토 쪽) → 성토면」. 앞서 `face_slope_ratio({}, wet=…)` 로 확인한 「근거 없음」은 `face` 를 안 준 호출이라 **정상 동작**이었음 — 함수가 성토로 안 눅이게 짠 결과. 자체검증 — 회귀 580 통과 · 0 실패. 세 라우터 모두 700줄 안(588 · 190 · 692). Co-Authored-By: Claude Opus 5 (1M context) --- ..._DesignDetail_Engine_Cad_StandardFigure.py | 10 ++++- ...7_DesignDetail_Engine_Cad_StandardSheet.py | 25 ++++++++--- .../B07_DesignDetail_Engine_Standard_Sheet.py | 16 ++++++- B07_DesignDetail/B07_DesignDetail_Router.py | 13 +++++- .../B07_DesignDetail_Router_Standard.py | 43 +++++++++++++++++-- .../B07_DesignDetail_Router_Support.py | 9 ++-- 6 files changed, 100 insertions(+), 16 deletions(-) diff --git a/B07_DesignDetail/B07_DesignDetail_Engine_Cad_StandardFigure.py b/B07_DesignDetail/B07_DesignDetail_Engine_Cad_StandardFigure.py index 30b56f71..8ce117a6 100644 --- a/B07_DesignDetail/B07_DesignDetail_Engine_Cad_StandardFigure.py +++ b/B07_DesignDetail/B07_DesignDetail_Engine_Cad_StandardFigure.py @@ -58,7 +58,15 @@ def slope_of(sheet: dict[str, Any]) -> tuple[float, str]: wet = options.get("bond") != "메쌓기" else: wet = True - return face_slope_ratio(options, wet=wet, height_m=float(sheet.get("height_m") or 0.0)) + return face_slope_ratio( + options, + wet=wet, + height_m=float(sheet.get("height_m") or 0.0), + # ⚠ 표를 만든 그 판정을 그대로 넘긴다 — 여기서 다시 가르면 근거를 못 받아 + # 종전값으로 떨어지고 **표와 갈린다**(2026-09-09 실측). + face=sheet.get("face"), + face_reason=str(sheet.get("face_reason") or ""), + ) def section_points(height_m: float, slope_ratio: float) -> list[tuple[float, float]]: diff --git a/B07_DesignDetail/B07_DesignDetail_Engine_Cad_StandardSheet.py b/B07_DesignDetail/B07_DesignDetail_Engine_Cad_StandardSheet.py index 08c60564..4e2389d9 100644 --- a/B07_DesignDetail/B07_DesignDetail_Engine_Cad_StandardSheet.py +++ b/B07_DesignDetail/B07_DesignDetail_Engine_Cad_StandardSheet.py @@ -204,7 +204,9 @@ def build_standard_drawing(drawing_id: str, label: str, payload: dict[str, Any]) } -def standard_payload(project_root: Path) -> dict[str, Any]: +def standard_payload( + project_root: Path, section_modes: dict[float, str] | None = None +) -> dict[str, Any]: """프로젝트 구조물을 제원 조합으로 묶은 장 목록. 실패해도 **빈 목록**을 낸다. ⚠ 늦게 부른다(함수 안 import) — B08 은 B05 를 부르고 B05 는 다시 B07 을 부를 수 있어 @@ -216,18 +218,24 @@ def standard_payload(project_root: Path) -> dict[str, Any]: try: structures, names, _skipped = _collect_structures(str(project_root)) - return build_standard_sheets(build_unit_table(structures, names)) + # ⚠ 단면유형을 넘겨야 성절토가 갈리고 표준경사 판정이 돈다. 안 넘기면 전 구조물이 + # 「가를 근거 없음」으로 떨어져 종전값 1:0.3 으로 선다(2026-09-09 실측). + return build_standard_sheets( + build_unit_table(structures, names, section_modes), section_modes + ) except Exception: logger.exception("B07 표준도 장 목록 실패 — 빈 목록으로 둔다: %s", project_root) return {"sheets": [], "structure_count": 0} -def sheet_items(project_root: Path) -> list[tuple[str, str]]: +def sheet_items( + project_root: Path, section_modes: dict[float, str] | None = None +) -> list[tuple[str, str]]: """좌측 목록에 설 `(도면 id, 이름)`. 장이 없으면 **빈 장 하나**를 남긴다. 구조물이 없다고 단추가 통째로 사라지면 「없어진 것」처럼 보인다 — 눌러서 사유를 읽게 한다. """ - sheets = standard_payload(project_root).get("sheets") or [] + sheets = standard_payload(project_root, section_modes).get("sheets") or [] if not sheets: return [(SHEET_ID_PREFIX, "표준도")] total = len(sheets) @@ -237,7 +245,12 @@ def sheet_items(project_root: Path) -> list[tuple[str, str]]: ] -def standard_drawing_for(project_root: Path, drawing_id: str, label: str) -> dict[str, Any]: +def standard_drawing_for( + project_root: Path, + drawing_id: str, + label: str, + section_modes: dict[float, str] | None = None, +) -> dict[str, Any]: """프로젝트 구조물을 읽어 표준도 한 장을 만든다 — 라우터가 부르는 문. ⚠ 실패해도 도면은 연다 — 구조물 정본을 못 읽었다고 화면이 비면 사용자는 「고장」으로 @@ -245,7 +258,7 @@ def standard_drawing_for(project_root: Path, drawing_id: str, label: str) -> dic ⚠ 늦게 부른다(함수 안 import) — B08 은 B05 를 부르고 B05 는 다시 B07 을 부를 수 있어 모듈 맨 위에서 부르면 맞물린다. """ - payload = standard_payload(project_root) + payload = standard_payload(project_root, section_modes) sheets = payload.get("sheets") or [] # 이 도면 id 가 가리키는 장 하나만 남긴다 — 한 면에 한 장(2026-09-09, 그림이 붙으면서). total = len(sheets) diff --git a/B07_DesignDetail/B07_DesignDetail_Engine_Standard_Sheet.py b/B07_DesignDetail/B07_DesignDetail_Engine_Standard_Sheet.py index 61fc58da..4bfe8988 100644 --- a/B07_DesignDetail/B07_DesignDetail_Engine_Standard_Sheet.py +++ b/B07_DesignDetail/B07_DesignDetail_Engine_Standard_Sheet.py @@ -139,13 +139,24 @@ def _rows_of(structure: dict[str, Any]) -> list[dict[str, Any]]: return rows -def build_standard_sheets(unit_table: dict[str, Any]) -> dict[str, Any]: +def build_standard_sheets( + unit_table: dict[str, Any], section_modes: dict[float, str] | None = None +) -> dict[str, Any]: """B08 원단위 전개(`build_unit_table` 결과)를 **표준도 장 목록**으로 접는다. ⚠ 계산을 다시 하지 않는다 — 들어온 전개를 제원 조합으로 묶고 단위당으로 나눌 뿐이다. """ + from B08_Quantity.B08_Quantity_Engine_UnitQuantity import _section_mode_at + from common_util.common_util_structure_face_role import structure_face_role + groups: dict[str, dict[str, Any]] = {} for structure in unit_table.get("structures") or []: + # ⚠ 성절토는 **표를 만든 그 판정**을 그대로 물고 온다 — 제목·그림이 다시 판정하면 + # 근거를 못 받아 종전값으로 떨어져 **표와 갈린다**(2026-09-09 실측: 표 1:0.35 · + # 제목 1:0.3). 자리를 고르는 것도 B08 이 쓰는 그 함수를 쓴다. + mode = _section_mode_at(structure, section_modes) + face, face_reason = structure_face_role(mode, (structure.get("options") or {}).get("side")) + structure = {**structure, "face": face, "face_reason": face_reason} key = sheet_key(structure) sheet = groups.get(key) if sheet is None: @@ -156,6 +167,9 @@ def build_standard_sheets(unit_table: dict[str, Any]) -> dict[str, Any]: "type_id": structure.get("type_id") or "", "height_m": float(structure.get("height_m") or 0.0), "options": _sheet_options(structure), + # 판정 근거 — 제목·그림이 표와 **같은 기울기**를 쓰게 하는 열쇠. + "face": structure.get("face"), + "face_reason": structure.get("face_reason") or "", # 실무 시트 머리의 「m당」·「개소당」·「㎡당」. 종류마다 다르다 — 통일하지 않는다. "unit_label": f"{unit}당", "billing_unit": unit, diff --git a/B07_DesignDetail/B07_DesignDetail_Router.py b/B07_DesignDetail/B07_DesignDetail_Router.py index 953548e1..a31b9b49 100644 --- a/B07_DesignDetail/B07_DesignDetail_Router.py +++ b/B07_DesignDetail/B07_DesignDetail_Router.py @@ -31,6 +31,7 @@ from B07_DesignDetail.B07_DesignDetail_Engine_Template import ( use_company_templates, use_title_fields, ) +from B07_DesignDetail.B07_DesignDetail_Router_Standard import section_modes_of from B07_DesignDetail.B07_DesignDetail_Router_Support import ( CROSS_STANDARD_ID, LANDUSE_ID, @@ -270,7 +271,10 @@ async def get_design_drawing_list( try: route_id, project_root, longitudinal_path, bypass = await _confirmed_source(project_id) designs = await _designs_by_chainage(route_id) - drawings = await asyncio.to_thread(_drawing_list, project_root, longitudinal_path, designs) + modes = await section_modes_of(route_id) + drawings = await asyncio.to_thread( + _drawing_list, project_root, longitudinal_path, designs, modes + ) return DesignDrawingListResponse( project_id=str(project_id), route_id=route_id, drawings=drawings, dev_bypass=bypass ) @@ -363,7 +367,12 @@ async def get_design_drawing( longitudinal = await asyncio.to_thread(_read_json, longitudinal_path) source_design = await asyncio.to_thread(plan_source, context, longitudinal, drawing_id) kind, label, drawing, confirmed, quantity_table = await asyncio.to_thread( - _read_drawing, project_root, longitudinal_path, drawing_id, source_design + _read_drawing, + project_root, + longitudinal_path, + drawing_id, + source_design, + await section_modes_of(route_id), ) return DesignDrawingResponse( project_id=str(project_id), diff --git a/B07_DesignDetail/B07_DesignDetail_Router_Standard.py b/B07_DesignDetail/B07_DesignDetail_Router_Standard.py index 0c6c4625..32159a2c 100644 --- a/B07_DesignDetail/B07_DesignDetail_Router_Standard.py +++ b/B07_DesignDetail/B07_DesignDetail_Router_Standard.py @@ -25,6 +25,40 @@ logger = logging.getLogger(__name__) router = APIRouter(prefix="/api/projects", tags=["B07 Design Detail"]) +async def section_modes_of_project(project_id: UUID) -> dict[float, str]: + """프로젝트에서 노선을 찾아 단면유형 표를 낸다 — 노선을 모르면 빈 표.""" + from B06_Section.B06_Section_Repository import get_workflow_route_context + from config.config_db import run_with_connection + + try: + context = await run_with_connection(get_workflow_route_context, project_id) + route_id = int((context or {}).get("route_id") or 0) + except Exception: + logger.exception("B07 노선 조회 실패: project_id=%s", project_id) + return {} + return await section_modes_of(route_id) if route_id else {} + + +async def section_modes_of(route_id: int) -> dict[float, str]: + """측점별 단면유형(`left_cut` 등) — 구조물이 **성토면인가 절토면인가**를 가르는 근거. + + ⚠ 이것을 안 넘기면 판정이 통째로 「가를 근거 없음」으로 떨어져 **전 구조물이 종전값 + 1:0.3 으로 선다**(2026-09-09 실측). 값이 없는 것이 아니라 **안 넘긴 것**이었다. + ⚠ 표를 만드는 셈은 `section_modes_from_designs` 한 벌을 쓴다 — 부르는 쪽마다 다시 + 짜면 B08 과 갈린다. + """ + from B06_Section.B06_Section_Repository import get_cross_section_designs + from B08_Quantity.B08_Quantity_Engine_UnitQuantity import section_modes_from_designs + from config.config_db import run_with_connection + + try: + designs = await run_with_connection(get_cross_section_designs, route_id) + except Exception: + logger.exception("B07 단면유형 조회 실패: route_id=%s", route_id) + return {} + return section_modes_from_designs(designs) + + class StandardSheetSpecRequest(BaseModel): """표준도 장 하나의 제원. **빈 값(null)은 「정한 적 없음」**이라 그 칸을 지운다.""" @@ -68,7 +102,9 @@ async def put_standard_sheet_spec( content={"status": "error", "message": "프로젝트 저장 폴더를 찾지 못했습니다."}, ) - sheets = (await asyncio.to_thread(standard_payload, project_root)).get("sheets") or [] + modes = await section_modes_of_project(project_id) + payload_sheets = await asyncio.to_thread(standard_payload, project_root, modes) + sheets = payload_sheets.get("sheets") or [] picked = next((s for s in sheets if s.get("key") == payload.sheet_key), None) if picked is None: return JSONResponse( @@ -135,9 +171,10 @@ async def get_standard_sheets(project_id: UUID) -> JSONResponse: content={"status": "error", "message": "프로젝트 저장 폴더를 찾지 못했습니다."}, ) + modes = await section_modes_of_project(project_id) try: structures, names, skipped = await asyncio.to_thread(_collect_structures, project_root) - unit_table = await asyncio.to_thread(build_unit_table, structures, names) + unit_table = await asyncio.to_thread(build_unit_table, structures, names, modes) except Exception: logger.exception("B07 표준도 전개 실패: project_id=%s", project_id) return JSONResponse( @@ -145,7 +182,7 @@ async def get_standard_sheets(project_id: UUID) -> JSONResponse: content={"status": "error", "message": "구조물 원단위를 전개하지 못했습니다."}, ) - payload = build_standard_sheets(unit_table) + payload = build_standard_sheets(unit_table, modes) payload["status"] = "success" payload["project_id"] = str(project_id) # 왜 안 실렸는지 — 「구조물이 없다」와 「걸러졌다」를 화면이 가릴 수 있어야 한다. diff --git a/B07_DesignDetail/B07_DesignDetail_Router_Support.py b/B07_DesignDetail/B07_DesignDetail_Router_Support.py index 556b8cdf..23182542 100644 --- a/B07_DesignDetail/B07_DesignDetail_Router_Support.py +++ b/B07_DesignDetail/B07_DesignDetail_Router_Support.py @@ -113,6 +113,7 @@ def _drawing_list( project_root: Path, longitudinal_path: Path, designs: dict[int, dict[str, Any]] | None = None, + section_modes: dict[float, str] | None = None, ) -> list[DesignDrawingItem]: longitudinal = _read_json(longitudinal_path) station_by_chainage = _station_map(longitudinal) @@ -206,7 +207,7 @@ def _drawing_list( ) # 표준도 — **제원 조합마다 한 장**이라 장이 나뉜다(2026-09-09). 구조물이 없어도 한 장은 # 남긴다: 단추가 사라지면 「없어진 것」처럼 보이고 사유를 읽을 자리도 없어진다. - for drawing_id, label in sheet_items(project_root): + for drawing_id, label in sheet_items(project_root, section_modes): drawings.append(DesignDrawingItem(id=drawing_id, kind="standard", label=label)) _store_drawing_numbers(project_root, drawings) return drawings @@ -409,6 +410,7 @@ def _read_drawing( longitudinal_path: Path, drawing_id: str, stored_design: dict[str, Any] | None = None, + section_modes: dict[float, str] | None = None, ) -> tuple[str, str, dict[str, Any], bool, dict[str, float | None] | None]: """(kind, label, drawing, confirmed, quantity_table)를 반환한다. @@ -442,8 +444,9 @@ def _read_drawing( return kind, label, saved, True, table if drawing_id == SHEET_ID_PREFIX or drawing_id.startswith(f"{SHEET_ID_PREFIX}_"): # 표준도 — 제원 조합 한 벌이 한 장. 도각은 두르지 않는다(2026-09-08 사용자 지시). - label = dict(sheet_items(project_root)).get(drawing_id, "표준도") - return "standard", label, standard_drawing_for(project_root, drawing_id, label), False, None + label = dict(sheet_items(project_root, section_modes)).get(drawing_id, "표준도") + drawing = standard_drawing_for(project_root, drawing_id, label, section_modes) + return "standard", label, drawing, False, None if drawing_id == COVER_ID: # 표지는 설계 자료를 쓰지 않는다 — 템플릿 한 장이 곧 도면이다.