From 14965f811b8087b3b240d4b204f6ef717fbe6bd1 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 6 Oct 2026 11:26:43 +0900 Subject: [PATCH 1/3] =?UTF-8?q?fix(M02):=20=EC=A7=91=EA=B3=84=ED=91=9C=20?= =?UTF-8?q?=ED=9D=99=EB=A7=89=EC=9D=B4=20=EC=A4=84=EC=9D=80=20=EC=84=A0=20?= =?UTF-8?q?=EB=8B=A4=EB=8B=A8=20=EC=88=98=EA=B9=8C=EC=A7=80=EB=A7=8C(54-11?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 배관 흙막이 컨테이너 줄 = 서버가 [저장] 때 세운 유출 다단 수(design.extra_walls)까지 · 넘친 상자 값은 파일에 그대로 · 단 수를 늘리면 되살아남 - 집계 항목 미지정 알림도 같은 수로 셈(같은 뿌리) - 다단 목록 없는 옛 측점 = 저장된 것 전부(옛 동작) - 시험 test_52_1b_pipe_links 둘 더 · 화면 점검 ui_checks/54-11_흙막이_집계 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01BVPp2o6Mh2FZJr72yW3ofZ --- .../M02_MasterTemplete_Router_Layers.py | 8 +- M02_MasterTemplete/M02_Table_Fill.py | 5 +- M02_MasterTemplete/M02_Table_Fill_Spec.py | 16 ++ resources/tester/test_52_1b_pipe_links.py | 39 +++++ .../tester/ui_checks/54-11_흙막이_집계.js | 138 ++++++++++++++++++ 5 files changed, 203 insertions(+), 3 deletions(-) create mode 100644 resources/tester/ui_checks/54-11_흙막이_집계.js diff --git a/M02_MasterTemplete/M02_MasterTemplete_Router_Layers.py b/M02_MasterTemplete/M02_MasterTemplete_Router_Layers.py index f6f44ca31..0baf219ef 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Router_Layers.py +++ b/M02_MasterTemplete/M02_MasterTemplete_Router_Layers.py @@ -32,6 +32,7 @@ from M02_MasterTemplete import M02_MasterTemplete_Router_Items as items_router from M02_MasterTemplete import M02_MasterTemplete_Store as store from M02_MasterTemplete import M02_Table_Fill as fill from M02_MasterTemplete import M02_Template_Layers as layers +from M02_MasterTemplete.M02_Table_Fill_Spec import soil_tiers_from_designs logger = logging.getLogger(__name__) @@ -595,7 +596,10 @@ async def filled_table( raise _bad(error) from error if found is None: raise HTTPException(status_code=404, detail="양식을 찾을 수 없습니다.") - lengths = fill.pipe_lengths_from_designs(await _cross_designs(project_id)) - document = await asyncio.to_thread(fill.fill_table, project["root"], found["문서"], lengths) + designs = await _cross_designs(project_id) + lengths = fill.pipe_lengths_from_designs(designs) + document = await asyncio.to_thread( + fill.fill_table, project["root"], found["문서"], lengths, soil_tiers_from_designs(designs) + ) result = await asyncio.to_thread(fill.recalc, document) return {"이름": name, "판": found["판"], "문서": document, "결과": result} diff --git a/M02_MasterTemplete/M02_Table_Fill.py b/M02_MasterTemplete/M02_Table_Fill.py index 862138d91..4d41ea46b 100644 --- a/M02_MasterTemplete/M02_Table_Fill.py +++ b/M02_MasterTemplete/M02_Table_Fill.py @@ -85,6 +85,7 @@ def collect_items( project_root: str | Path, pipe_lengths: dict[float, float] | None = None, types: dict[str, StructureType] | None = None, + soil_tiers: dict[float, int] | None = None, ) -> list[dict[str, Any]]: """설계 정본 → 구조물 목록. @@ -133,6 +134,7 @@ def collect_items( "end_m": None, "options": _with_defaults(types.get(type_id), dict(point.options or {})), "관연장": _nearest(lengths, chainage) if type_id == "pipe" else None, + "흙막이단": _nearest(soil_tiers or {}, chainage), # 54-11 "summary_item": point.summary_item, "복사본": f"pipe@{round(chainage, 3)}", "group_items": point.group_items, @@ -672,10 +674,11 @@ def fill_table( project_root: str | Path, document: dict[str, Any], pipe_lengths: dict[float, float] | None = None, + soil_tiers: dict[float, int] | None = None, ) -> dict[str, Any]: """프로젝트 설계로 표 문서를 채움 — 저장 안 함 · 지정 항목 모드(미지정은 빈칸).""" types = structure_type_map() - items = collect_items(project_root, pipe_lengths, types) + items = collect_items(project_root, pipe_lengths, types, soil_tiers) doc, chosen, notes = assign_items(project_root, document, items) filled = fill_document(doc, chosen, types, assigned=True) filled["알림"] = [*notes, *filled.get("알림", [])] diff --git a/M02_MasterTemplete/M02_Table_Fill_Spec.py b/M02_MasterTemplete/M02_Table_Fill_Spec.py index 6523e0926..bda74e9a9 100644 --- a/M02_MasterTemplete/M02_Table_Fill_Spec.py +++ b/M02_MasterTemplete/M02_Table_Fill_Spec.py @@ -7,6 +7,7 @@ 짝: `A00_Common/design/design_structure_pipe.ts` · `_road.ts` 선택 컨테이너(머리 · 잇기 · 제목) — 시험이 대조. 기타(52-14) = 사용자가 적은 상세 종류 이름이 집계 건 이름(`그룹`) · 비면 「기타」. +배관 흙막이 = 지형이 세운 유출 다단 수(`흙막이단`)까지만 — 넘친 컨테이너 값은 파일에만(54-11). """ from __future__ import annotations @@ -35,6 +36,18 @@ def etc_label(type_id: str, options: dict[str, Any] | None) -> dict[str, str]: return {"그룹": str((options or {}).get("name") or "").strip() or "기타"} +def soil_tiers_from_designs(designs: Any) -> dict[float, int]: + """B06 횡단 설계 → `{주인 측점: 선 유출 다단 수}` — 서버가 세운 `extra_walls` 목록이 있는 측점만 + (B06 `pipeExtraCount` 와 같은 수 · 54-11).""" + found: dict[float, int] = {} + for row in designs or []: + walls = (row.get("design") or {}).get("extra_walls") if isinstance(row, dict) else None + if isinstance(walls, list): + outlet = [w for w in walls if isinstance(w, dict) and w.get("side") == "outlet"] + found[round(float(row.get("chainage_m") or 0.0), 3)] = len(outlet) + return found + + def _as_options(values: dict[str, Any]) -> dict[str, Any]: """컨테이너 값 → 열이 읽는 칸 — 구조 = `structure` · `{쪽}_type` · 나머지 = 맨 칸 + `{쪽}_revet_*`.""" @@ -53,6 +66,7 @@ def _as_options(values: dict[str, Any]) -> dict[str, Any]: def container_rows(item: dict[str, Any], links: dict[str, Any]) -> list[dict[str, Any]]: """한 구조물의 선택 컨테이너 인스턴스마다 한 건(n 차례) — 컨테이너 없는 종류는 빈 목록.""" rows: list[dict[str, Any]] = [] + tiers = item.get("흙막이단") # None = 모름(저장된 것 그대로) for prefix, link, title in SPEC_CONTAINERS.get(item["type_id"], ()): head = re.compile(rf"^{prefix}_(\d+)_(.+)$") found: dict[int, dict[str, Any]] = {} @@ -61,6 +75,8 @@ def container_rows(item: dict[str, Any], links: dict[str, Any]) -> list[dict[str if hit: found.setdefault(int(hit.group(1)), {})[hit.group(2)] = value for n in sorted(found): + if prefix == "soil_add" and tiers is not None and n > tiers: + continue gid = f"{link}{n}" rows.append( { diff --git a/resources/tester/test_52_1b_pipe_links.py b/resources/tester/test_52_1b_pipe_links.py index 87ce7e553..8f6898236 100644 --- a/resources/tester/test_52_1b_pipe_links.py +++ b/resources/tester/test_52_1b_pipe_links.py @@ -82,3 +82,42 @@ def test_서버_컨테이너_표는_배관_명세와_짝(): r'title: "([^"]+)",\s*required: false,\s*prefix: "(\w+)",\s*link: "(\w+)"', text ) assert [(p, link, t) for t, p, link in found] == list(SPEC_CONTAINERS["pipe"]) + + +def test_54_11_흙막이는_선_단_수까지만(): + """단 수 2 → 1 = 흙막이 줄 2 → 1(값은 옵션에 그대로) · 다시 2 = 되살아남 · 모름 = 저장된 것 전부.""" + from M02_MasterTemplete.M02_Table_Fill_Spec import soil_tiers_from_designs + + item = {"type_id": "pipe", "options": SOIL, "복사본": "pipe@40.0"} + + def soil(tiers): + rows = container_rows({**item, "흙막이단": tiers}, {}) + return [r["그룹"] for r in rows if r["그룹"].startswith("흙막이")] + + assert soil(2) == ["흙막이 1", "흙막이 2"] + assert soil(1) == ["흙막이 1"] and "soil_add_2_structure" in item["options"] + assert soil(0) == [] and soil(2) == ["흙막이 1", "흙막이 2"] and len(soil(None)) == 2 + assert len(container_rows({**item, "흙막이단": 0}, {})) == 1 # 도수로/돌붙임은 그대로 + walls = [{"side": "outlet"}, {"side": "basin"}, {"side": "outlet"}] + designs = [ + {"chainage_m": 40.0, "design": {"extra_walls": walls}}, + {"chainage_m": 60.0, "design": {"extra_walls": []}}, + {"chainage_m": 80.0, "design": {"pipe_length_m": 9}}, + ] + assert soil_tiers_from_designs(designs) == {40.0: 2, 60.0: 0} + + +def test_54_11_집계표_미지정_알림도_선_단_수만(world): + root, system = world["root"], world["system"] + point = {**_pipe(PIPE), "group_items": {"gsoiladd1": RV}} + point["options"] = {**point["options"], **SOIL} + _save(root, [], [point]) + table = json.loads((system / "table/구조물집계표.json").read_text("utf-8")) + + def note(tiers): + found = fill.fill_table(root, table, None, tiers)["알림"] + return next((n for n in found if n.startswith("집계 항목 미지정")), "") + + many, one = note(None), note({40.0: 1}) + assert many != one # 숨은 흙막이 2 가 미지정 알림에서 빠짐 + assert int(many.split()[3].rstrip("건")) - int(one.split()[3].rstrip("건")) == 1 diff --git a/resources/tester/ui_checks/54-11_흙막이_집계.js b/resources/tester/ui_checks/54-11_흙막이_집계.js new file mode 100644 index 000000000..e6f94bb91 --- /dev/null +++ b/resources/tester/ui_checks/54-11_흙막이_집계.js @@ -0,0 +1,138 @@ +/* 54-11 — 집계표 · 미지정 알림은 지금 보이는 흙막이 상자(단 수 안)만 셈 + * 시작 화면: B06(#/b06-section) · 검증 프로젝트(sub5_검증) · 목록에 다단이 서는 관(기본 「배관 2」 12+18) + * 부르는 법: orca eval --expression "window.__m5411={mode:'단계'}; $(cat 이 파일)" + * look → 단 수 · 보이는 흙막이 상자 · 파일의 흙막이 값 번호 · 집계표 줄 수 · 미지정 알림 + * prep → 보이는 흙막이 상자마다 구조물 조합 잇기 · 마지막 상자 형태 = form → [저장] → look + * tier+ / tier- → 유출 「추가 기슭막이(단)」 + / - → [저장] → look + * 기대: 단 수 2 → 1 에 상자 2 → 1 · 집계 줄(또는 미지정 건) 1 줄어듦 · 파일 값 그대로 · + * 다시 2 에 줄 · 상자 값 되살아남 + */ +(async () => { + const ARGS = window.__m5411 || { mode: "look" }; + const PIPE = ARGS.pipe || "배관 2"; + const w = (ms) => new Promise((r) => setTimeout(r, ms)); + const until = async (fn, ms = 20000) => { + for (let t = 0; t < ms; t += 200) { + if (fn()) return true; + await w(200); + } + return false; + }; + const projectId = localStorage.getItem("frd_current_project_id"); + const modUrl = (part) => + performance + .getEntriesByType("resource") + .map((e) => e.name) + .filter((n) => n.includes(part)) + .pop(); + const fetchUrl = modUrl("/B04_PreProcess/B04_PreProcess_Api_Fetch.ts"); + const fetchApi = await import(fetchUrl); + const m02Api = await import( + fetchUrl.replace( + /B04_PreProcess\/B04_PreProcess_Api_Fetch\.ts.*$/, + "M02_MasterTemplete/M02_MasterTemplete_Api_Fetch.ts", + ) + ); + const spec = () => + [...document.querySelectorAll(".b05-structure__spec")].find((n) => n.offsetParent !== null); + const soils = () => [...(spec()?.querySelectorAll('[data-spec-container="soil_add"]') ?? [])]; + const items = () => [...document.querySelectorAll(".b05-route__irregular-list li")]; + const station = () => + items() + .find((l) => l.textContent.trim().endsWith(PIPE)) + ?.textContent.trim() + .split(PIPE)[0]; + const pickPipe = async () => { + await until(() => items().length > 0); + const chosen = document.querySelector(".b05-route__irregular-list li.is-selected"); + if (spec() && chosen?.textContent.trim().endsWith(PIPE)) return; + const li = items().find((l) => l.textContent.trim().endsWith(PIPE)); + (li.querySelector("button") ?? li).click(); + await until(() => spec(), 5000); + await w(800); + }; + const tierRow = () => + spec() + ?.querySelector('[data-spec-container="outlet"]') + ?.querySelector(".b06-structure-panel__struct--tier:not(.is-hidden)"); + const look = async () => { + const filled = (await m02Api.readFilled(projectId, "구조물집계표")).문서; + const points = (await fetchApi.fetchDetailPipePoints(projectId)).pipe_points ?? []; + const sta = station(); + // 흙막이 값이 있는 관 지점마다 파일의 흙막이 번호 + const fileSoil = Object.fromEntries( + points + .map((p) => [ + p.chainage_m, + [ + ...new Set( + Object.keys(p.options ?? {}) + .map((k) => /^soil_add_(\d+)_/.exec(k)?.[1]) + .filter(Boolean), + ), + ].sort(), + ]) + .filter(([, list]) => list.length), + ); + return { + station: sta, + tier: tierRow()?.querySelector(".b06-structure-panel__hval")?.textContent ?? null, + soilBoxes: soils().length, + soilValues: soils().map( + (b) => + b.querySelector( + '[data-spec-item="soil_add.form"] select, [data-spec-item="soil_add.form"] input', + )?.value ?? "", + ), + fileSoil, + rows: filled.줄.filter((r) => !r.고정).length, + notes: (filled.알림 ?? []).filter((n) => n.startsWith("집계 항목")), + }; + }; + const pickLink = async (box) => { + box.querySelector('[data-b05-summary="pick"]').click(); + await until(() => document.querySelector(".ui-item-picker__tree .ui-tree__row")); + for (let k = 0; k < 8; k++) { + const rows = [...document.querySelectorAll(".ui-item-picker__tree .ui-tree__row")]; + const leaf = rows.find((r) => !r.classList.contains("is-branch")); + if (leaf) { + leaf.click(); + break; + } + rows.find((r) => r.querySelector(".ui-tree__caret")?.textContent === "▸")?.click(); + await w(300); + } + await w(300); + [...document.querySelectorAll(".ui-item-picker__actions button")] + .find((b) => b.textContent.includes("적용")) + .click(); + await w(500); + }; + const save = async () => { + document.querySelector('[data-dock-action="save"]').click(); + await w(500); + out.saved = await until(() => !document.querySelector(".ui-overlay.is-active"), 90000); + await w(1500); + }; + await pickPipe(); + const out = { mode: ARGS.mode, pipe: PIPE }; + // prep — 보이는 흙막이 상자마다 구조물 조합 잇기 · 마지막 상자 형태 = ARGS.form → [저장] + if (ARGS.mode === "prep") { + for (const box of soils()) await pickLink(box); + const el = soils().at(-1)?.querySelector('[data-spec-item="soil_add.form"] select'); + if (el && ARGS.form) { + el.value = ARGS.form; + el.dispatchEvent(new Event("change", { bubbles: true })); + await w(1500); + } + await save(); + } + if (ARGS.mode === "tier+" || ARGS.mode === "tier-") { + const btns = [...(tierRow()?.querySelectorAll("button") ?? [])]; + btns.find((b) => b.textContent === (ARGS.mode === "tier+" ? "+" : "-"))?.click(); + await w(1500); + await save(); + } + Object.assign(out, await look()); + return JSON.stringify(out); +})(); From fe2aa55bdbcc256cfd924b31cd7576063c76c34b Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 6 Oct 2026 11:36:21 +0900 Subject: [PATCH 2/3] =?UTF-8?q?chore(plan):=2010-6=20=EC=95=8C=EC=95=BD=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=EB=9D=BC=EB=B2=A8=20=EC=9A=94=EC=B2=AD=20?= =?UTF-8?q?=EC=9B=90=EB=AC=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FJqWQpoB1RE7hiaQ2wH8qV --- docs/raw/사용자요구/2026-10-05_수정요구.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/raw/사용자요구/2026-10-05_수정요구.md b/docs/raw/사용자요구/2026-10-05_수정요구.md index 740f9584f..a5689c2db 100644 --- a/docs/raw/사용자요구/2026-10-05_수정요구.md +++ b/docs/raw/사용자요구/2026-10-05_수정요구.md @@ -160,3 +160,6 @@ (10-6 사용자 · 추가 요청) - 안전부대용지 그룹의 사토장의 기준측점 표현이 예전 코드입. 토취장과 동일하게 구성 + +(10-6 사용자 · 다음 수정) + - B05/B06의 종단도 내널에서 구조물 알약을 선택하면 정보 라벨이 나오는데 위치가 상하 제어 버튼에 가려져서 안보여 최상위로 올려주고 그럼 버튼이 가려지니까 Y축 방향으로 조금 쉬프트 해줘. 그리고 안에 내용은 중요한 정보만 보여주면 됨. 지금은 너무 많이 나와. 중요한 정보라면 아마 기준 측점 정보와 컨테이너가 있는 경우 제일 앞에 있는 옵션값만 있어도 될것같아. From fb426fb17e2e07e29ea37fd9b35893518688f7b6 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 6 Oct 2026 11:43:32 +0900 Subject: [PATCH 3/3] =?UTF-8?q?chore(plan):=2010-6=20=EA=B5=AC=EC=A1=B0?= =?UTF-8?q?=EB=AC=BC=20=EB=AA=A9=EB=A1=9D=20=EC=9E=87=EA=B8=B0=20=EB=9E=A8?= =?UTF-8?q?=ED=94=84=20=EC=9A=94=EC=B2=AD=20=EC=9B=90=EB=AC=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FJqWQpoB1RE7hiaQ2wH8qV --- docs/raw/사용자요구/2026-10-05_수정요구.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/raw/사용자요구/2026-10-05_수정요구.md b/docs/raw/사용자요구/2026-10-05_수정요구.md index a5689c2db..241734bd4 100644 --- a/docs/raw/사용자요구/2026-10-05_수정요구.md +++ b/docs/raw/사용자요구/2026-10-05_수정요구.md @@ -163,3 +163,6 @@ (10-6 사용자 · 다음 수정) - B05/B06의 종단도 내널에서 구조물 알약을 선택하면 정보 라벨이 나오는데 위치가 상하 제어 버튼에 가려져서 안보여 최상위로 올려주고 그럼 버튼이 가려지니까 Y축 방향으로 조금 쉬프트 해줘. 그리고 안에 내용은 중요한 정보만 보여주면 됨. 지금은 너무 많이 나와. 중요한 정보라면 아마 기준 측점 정보와 컨테이너가 있는 경우 제일 앞에 있는 옵션값만 있어도 될것같아. + +(10-6 사용자 · 신규 기능) +다음은 신규 기능이야. 구조물 조합을 지정해야하는 경우에가 있는데 이때 좌측 패널의 구조물 리스트의 항목들에 측점정보 좌측에 연결되었거나 연결되지 않은 정보를 알려주는 기능이 있었으면 함. 이때 구조물에 따라 선택이나 필수 정보들에 의해 램프와 램프의 색상으로 표현되게 하면 될것같아. 램프영역에 의해 글자들의 배열이 깨지는건 싫으니 별도의 영역을 먼저지정하고 램프는 3행 다열 구조로 구성해줘. 램프의 사이즈는 작아도 되니 현재의 리스트 높이를 깨지 않는 범위에서 진행해줘.