fix(M02): 집계표 흙막이 줄은 선 다단 수까지만(54-11)
- 배관 흙막이 컨테이너 줄 = 서버가 [저장] 때 세운 유출 다단 수(design.extra_walls)까지 · 넘친 상자 값은 파일에 그대로 · 단 수를 늘리면 되살아남 - 집계 항목 미지정 알림도 같은 수로 셈(같은 뿌리) - 다단 목록 없는 옛 측점 = 저장된 것 전부(옛 동작) - 시험 test_52_1b_pipe_links 둘 더 · 화면 점검 ui_checks/54-11_흙막이_집계 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BVPp2o6Mh2FZJr72yW3ofZ
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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("알림", [])]
|
||||
|
||||
@@ -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(
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
})();
|
||||
Reference in New Issue
Block a user