Merge remote-tracking branch 'origin/main_laptop_1' into sub_laptop_4

This commit is contained in:
2026-10-06 11:44:34 +09:00
5 changed files with 203 additions and 3 deletions
@@ -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}
+4 -1
View File
@@ -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("알림", [])]
+16
View File
@@ -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(
{
+39
View File
@@ -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);
})();