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

This commit is contained in:
2026-10-06 11:58:26 +09:00
11 changed files with 98 additions and 38 deletions
@@ -23,13 +23,18 @@ export interface SummaryRow {
}
export function buildSummaryRow(): SummaryRow {
// 옵션 칸과 같은 2행 꼴(54-17) — 위 제목(`b05-route__field > span`) · 아래 [이름 칸][선택][해제]
const root = document.createElement("div");
root.className = "b05-structure__summary";
root.className = "b05-route__field b05-structure__summary";
root.dataset.b05Summary = "row";
const label = document.createElement("span");
label.className = "b05-structure__summary-label";
label.textContent = "집계 항목";
const name = document.createElement("span");
label.textContent = "구조물 조합";
// 이름 = 읽기 전용 입력 칸 — 옵션 입력 칸과 글꼴 · 크기 · 칸 모양이 같음
const name = document.createElement("input");
name.type = "text";
name.readOnly = true;
name.tabIndex = -1;
name.className = "b05-structure__summary-name";
const pick = document.createElement("button");
pick.type = "button";
@@ -41,12 +46,15 @@ export function buildSummaryRow(): SummaryRow {
clear.className = "b05-route__irregular-btn";
clear.textContent = "해제";
clear.dataset.b05Summary = "clear";
root.append(label, name, pick, clear);
const line = document.createElement("div");
line.className = "b05-route__irregular-actions";
line.append(name, pick, clear);
root.append(label, line);
let current: SummaryItem | null = null;
const set = (item: SummaryItem | null): void => {
current = item;
name.textContent = summaryLabel(item);
name.value = summaryLabel(item);
name.title = summaryTooltip(item);
name.classList.toggle("is-assigned", item !== null);
clear.disabled = item === null;
@@ -439,33 +439,16 @@
border-color: var(--color-warning, #c98a00);
}
/* 집계 항목 줄(PLAN 21-4) — 옵션 · 시설 서브폼 아래 [이름][고르기][해제] 한 줄. 이름이 남는 폭을
* 먹고 말줄임 · 키는 툴팁에만. 지정되면 성공색(`.is-assigned`) — 목록 줄과 같은 색. */
.b05-structure__summary {
display: flex;
align-items: center;
gap: var(--spacing-8);
min-width: 0;
}
/* 구조물 조합 잇기 줄(PLAN 21-4 · 54-17) — 옵션 칸 꼴(`b05-route__field`) 2행 · 아래 줄은
* [이름 칸][선택][해제] · 이름 칸이 남는 폭을 먹고 말줄임 · 키는 툴팁에만. 지정되면 성공색. */
.b05-structure__summary[hidden] {
display: none;
}
.b05-structure__summary-label {
flex: none;
color: var(--color-text-secondary);
font-size: var(--text-caption);
}
.b05-structure__summary-name {
flex: 1 1 auto;
min-width: 0;
overflow: hidden;
color: var(--color-text-secondary);
font-size: var(--text-caption);
align-self: center;
text-overflow: ellipsis;
white-space: nowrap;
}
/* 글자는 성공색에 본문색을 섞어 대비 4.5 를 넘김(성공색 그대로는 밝은 테마에서 4.4). */
@@ -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) =>