feat(structure): 구조물 조합 잇기 줄을 옵션 칸 꼴 2행으로(54-17)

- 위 제목 「구조물 조합」(옵션 칸 제목과 같은 꼴) · 아래 이름 칸(읽기 전용 입력 칸) + [선택] [해제]
- 공용 한 곳(Summary_Row) — 명세 컨테이너 · 정의 그룹 상자 · 종류 단위 줄 모두 · 동작 · 저장 값 그대로
- 옛 한 줄 CSS 걷어 냄 · 옵션 칸 공용 클래스 다시 씀
- 시험 · ui_check 의 이름 읽기를 value 로

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016PSXsvyDxjNmufteN5bqmt
This commit is contained in:
2026-10-06 11:56:47 +09:00
co-authored by Claude Opus 5.5
parent fa4e517b4c
commit b2e5f5adf3
11 changed files with 98 additions and 38 deletions
@@ -199,9 +199,10 @@ out.pipeSet = merge.mergeFacilityOptions(pipePrev, {
const row = buildSummaryRow();
const clicks = [];
row.bind({ pick: () => clicks.push("pick"), clear: () => clicks.push("clear") });
const [labelNode, nameNode, pickBtn, clearBtn] = row.root.children;
const [labelNode, line] = row.root.children;
const [nameNode, pickBtn, clearBtn] = line.children;
out.rowEmpty = {
name: nameNode.textContent,
name: nameNode.value,
assigned: nameNode.classList.contains("is-assigned"),
clearDisabled: clearBtn.disabled,
get: row.get(),
@@ -211,18 +212,18 @@ out.rowEmpty = {
};
row.set(A);
out.rowSet = {
name: nameNode.textContent,
name: nameNode.value,
assigned: nameNode.classList.contains("is-assigned"),
clearDisabled: clearBtn.disabled,
title: nameNode.title,
key: row.get().key,
nameShowsKey: nameNode.textContent.includes("SS000007"),
nameShowsKey: nameNode.value.includes("SS000007"),
};
pickBtn.click();
clearBtn.click();
out.clicks = clicks;
row.set(null);
out.rowCleared = { name: nameNode.textContent, clearDisabled: clearBtn.disabled };
out.rowCleared = { name: nameNode.value, clearDisabled: clearBtn.disabled };
// 목록 줄 — 지정 색 · 툴팁
const list = new FakeNode("ul");
@@ -80,7 +80,7 @@ def test_집계_항목_줄(out):
True,
None,
)
assert (empty["label"], empty["pick"], empty["clear"]) == ("집계 항목", "선택", "해제")
assert (empty["label"], empty["pick"], empty["clear"]) == ("구조물 조합", "선택", "해제")
row = out["rowSet"]
assert row["assigned"] is True and row["clearDisabled"] is False
assert row["name"] == "돌기슭막이 메쌓기 1.2m" and A_KEY in row["title"]
@@ -69,7 +69,7 @@
c: b.dataset.specContainer,
legend: b.querySelector("legend").firstChild.textContent,
items: [...b.querySelectorAll("[data-spec-item]")].map((n) => n.dataset.specItem),
link: b.querySelector(".b05-structure__summary-name")?.textContent,
link: b.querySelector(".b05-structure__summary-name")?.value,
}));
const linkName = (c, i = 0) =>
box(c, i).querySelector(".b05-structure__summary-name").classList.contains("is-assigned");
@@ -65,7 +65,7 @@
values: [...b.querySelectorAll("[data-spec-item]")].map(
(n) => n.querySelector("select, input").value,
),
link: b.querySelector(".b05-structure__summary-name")?.textContent,
link: b.querySelector(".b05-structure__summary-name")?.value,
}));
const linked = (c) =>
box(c).querySelector(".b05-structure__summary-name").classList.contains("is-assigned");
@@ -63,7 +63,7 @@
n.dataset.specItem,
n.querySelector("select, input").value,
]),
link: b.querySelector(".b05-structure__summary-name")?.textContent,
link: b.querySelector(".b05-structure__summary-name")?.value,
}));
const assigned = (c) =>
box(c).querySelector(".b05-structure__summary-name").classList.contains("is-assigned");
@@ -52,7 +52,7 @@
summaryRightAfterPosition: after === summary,
anchor: vis(anchorWrap),
stale: vis(sec().querySelector(".b05-structure__stale")),
linked: summary.querySelector(".b05-structure__summary-name")?.textContent ?? "",
linked: summary.querySelector(".b05-structure__summary-name")?.value ?? "",
};
};
const pickType = async (typeId) => {
@@ -1,4 +1,4 @@
/* 52-6 — 횡단배수 › 맹암거(표 06 · 새 종류 `cross_underdrain`) · B05 · B06 「구조물 배치」
/* 52-6 — 횡단배수 › 맹암거(표 06 · 새 종류 `cross_underdrain`) · B05 · B06 「구조물 배치」
* 시작 화면: B05 종단설계(#/b05-profile) · 아무것도 안 고름 · 측점 3+7 이 비어 있음
* 부르는 법: 단계마다 한 번씩 — 앞에 `window.__m526 = "단계";` 를 붙여
* orca eval --expression "window.__m526='edit'; $(cat 이 파일)" → 종류 목록 · 컨테이너 · 값 · 잇기 · [추가] · 다시 열기
@@ -53,7 +53,7 @@
(n) => `${n.dataset.optionKeys}|${n.querySelector("span").textContent}`,
),
values: ["culvert_height_m", "culvert_slope_pct"].map((k) => input(k)?.value),
link: b?.querySelector(".b05-structure__summary-name").textContent,
link: b?.querySelector(".b05-structure__summary-name").value,
linked: b?.querySelector(".b05-structure__summary-name").classList.contains("is-assigned"),
linkRows: [...sec().querySelectorAll("[data-b05-summary=row]")].filter(vis).length,
};
@@ -0,0 +1,68 @@
/* 54-17 — B05 · B06 좌측 「구조물 배치」 구조물 조합 잇기 줄 = 옵션 칸 꼴 2행
* 시작 화면: B05 종단설계(#/b05-profile) 또는 B06 횡단설계(#/b06-section) · 검증 프로젝트(랩탑_보조)
* 부르는 법: 한 번 — 배관 · 맹암거 · 반사경 · 옹벽을 폼에서 골라(저장 안 함) 보이는 잇기 줄을 잼 ·
* 결과를 sessionStorage `m5417` 에 page 별로 두고 B05 = B06 을 견줌 · 끝나면 폼 고르기를 비움
* 판정: 제목 · 값 두 줄(제목 아래 = 값) · 제목 글꼴/크기/색 = 옵션 칸 제목 · 값 글꼴/크기/높이/테두리 = 입력 칸
* 쓰는 것: 없음(폼 고르기만 · 저장 안 함)
*/
(async () => {
const w = (ms) => new Promise((r) => setTimeout(r, ms));
const page = location.hash.includes("b06") ? "B06" : "B05";
const sec = document.querySelector(".b05-structure-section");
const [g, t] = sec.querySelectorAll(".b05-structure__grid select");
const pick = async (el, v) => {
el.value = v;
el.dispatchEvent(new Event("change", { bubbles: true }));
await w(400);
};
const vis = (el) => !!el && el.getClientRects().length > 0;
const cs = (el) => getComputedStyle(el);
const font = (el) => [cs(el).fontFamily, cs(el).fontSize, cs(el).color].join("|");
const box = (el) =>
[
cs(el).fontFamily,
cs(el).fontSize,
cs(el).height,
cs(el).padding,
cs(el).borderTopWidth,
cs(el).borderRadius,
].join("|");
const TARGETS = {
배관: ["A", "pipe"],
맹암거: ["A", "cross_underdrain"],
반사경: ["E", "mirror"],
옹벽: ["C", "retaining_wall"],
};
const out = {};
for (const [name, [gv, tv]] of Object.entries(TARGETS)) {
await pick(g, gv);
await pick(t, tv);
const rows = [...sec.querySelectorAll("[data-b05-summary=row]")].filter(vis);
const cap = [...sec.querySelectorAll(".b05-route__field > span")].find(
(s) => vis(s) && !s.closest("[data-b05-summary]"),
);
const input = [
...sec.querySelectorAll(".b05-route__field input, .b05-route__field select"),
].find((i) => vis(i) && !i.closest("[data-b05-summary]") && i.type !== "checkbox");
out[name] = rows.map((r) => {
const title = r.children[0];
const nameEl = r.querySelector(".b05-structure__summary-name");
return {
title: title.textContent,
value: nameEl.value,
twoLines: title.getBoundingClientRect().bottom <= nameEl.getBoundingClientRect().top,
titleFont: font(title) === font(cap),
valueBox: box(nameEl) === box(input),
buttons: [...r.querySelectorAll("button")].map((b) => b.textContent).join(","),
fonts: { title: font(title), value: box(nameEl) },
};
});
}
await pick(g, "");
const store = JSON.parse(sessionStorage.getItem("m5417") || "{}");
store[page] = out;
sessionStorage.setItem("m5417", JSON.stringify(store));
const strip = (o) => JSON.stringify(o, (k, v) => (k === "value" ? undefined : v));
const same = store.B05 && store.B06 ? strip(store.B05) === strip(store.B06) : null;
return JSON.stringify({ page, out, B05eqB06: same });
})();
@@ -79,7 +79,7 @@
typeRows: [...section().querySelectorAll(".b05-structure__summary")].filter(
(n) => !n.closest("[data-b05-group]") && visible(n),
).length,
link: box?.querySelector(".b05-structure__summary-name")?.textContent.trim(),
link: box?.querySelector(".b05-structure__summary-name")?.value.trim(),
};
};
const ok = (type, s) =>