From b2e5f5adf3a8d8bcfe89249c943b1aed54406ba8 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 6 Oct 2026 11:56:47 +0900 Subject: [PATCH] =?UTF-8?q?feat(structure):=20=EA=B5=AC=EC=A1=B0=EB=AC=BC?= =?UTF-8?q?=20=EC=A1=B0=ED=95=A9=20=EC=9E=87=EA=B8=B0=20=EC=A4=84=EC=9D=84?= =?UTF-8?q?=20=EC=98=B5=EC=85=98=20=EC=B9=B8=20=EA=BC=B4=202=ED=96=89?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C(54-17)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 위 제목 「구조물 조합」(옵션 칸 제목과 같은 꼴) · 아래 이름 칸(읽기 전용 입력 칸) + [선택] [해제] - 공용 한 곳(Summary_Row) — 명세 컨테이너 · 정의 그룹 상자 · 종류 단위 줄 모두 · 동작 · 저장 값 그대로 - 옛 한 줄 CSS 걷어 냄 · 옵션 칸 공용 클래스 다시 씀 - 시험 · ui_check 의 이름 읽기를 value 로 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016PSXsvyDxjNmufteN5bqmt --- .../B05_Profile_UI_Structures_Summary_Row.ts | 18 +++-- .../B05_Profile_UI_Style_Structures.css | 23 +------ .../helper_b05_structures_form_summary.cjs | 11 +-- .../test_b05_structures_form_summary.py | 2 +- .../tester/ui_checks/52-1_배관_컨테이너.js | 2 +- .../ui_checks/52-2_박스암거_컨테이너.js | 2 +- .../tester/ui_checks/52-4_세월교_컨테이너.js | 2 +- .../tester/ui_checks/52-5_횡단수로_개거.js | 2 +- resources/tester/ui_checks/52-6_횡단맹암거.js | 4 +- .../tester/ui_checks/54-17_구조물조합_2행.js | 68 +++++++++++++++++++ .../ui_checks/54-6_사면안정_컨테이너.js | 2 +- 11 files changed, 98 insertions(+), 38 deletions(-) create mode 100644 resources/tester/ui_checks/54-17_구조물조합_2행.js diff --git a/B05_Profile/B05_Profile_UI_Structures_Summary_Row.ts b/B05_Profile/B05_Profile_UI_Structures_Summary_Row.ts index 296417088..e0a1027ca 100644 --- a/B05_Profile/B05_Profile_UI_Structures_Summary_Row.ts +++ b/B05_Profile/B05_Profile_UI_Structures_Summary_Row.ts @@ -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; diff --git a/B05_Profile/B05_Profile_UI_Style_Structures.css b/B05_Profile/B05_Profile_UI_Style_Structures.css index cde7f1c91..f1c2b5f1a 100644 --- a/B05_Profile/B05_Profile_UI_Style_Structures.css +++ b/B05_Profile/B05_Profile_UI_Style_Structures.css @@ -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). */ diff --git a/resources/tester/helper_b05_structures_form_summary.cjs b/resources/tester/helper_b05_structures_form_summary.cjs index 0a548359e..09c3e4f7c 100644 --- a/resources/tester/helper_b05_structures_form_summary.cjs +++ b/resources/tester/helper_b05_structures_form_summary.cjs @@ -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"); diff --git a/resources/tester/test_b05_structures_form_summary.py b/resources/tester/test_b05_structures_form_summary.py index b75785843..22ad774a1 100644 --- a/resources/tester/test_b05_structures_form_summary.py +++ b/resources/tester/test_b05_structures_form_summary.py @@ -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"] diff --git a/resources/tester/ui_checks/52-1_배관_컨테이너.js b/resources/tester/ui_checks/52-1_배관_컨테이너.js index 4153fc273..95257df30 100644 --- a/resources/tester/ui_checks/52-1_배관_컨테이너.js +++ b/resources/tester/ui_checks/52-1_배관_컨테이너.js @@ -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"); diff --git a/resources/tester/ui_checks/52-2_박스암거_컨테이너.js b/resources/tester/ui_checks/52-2_박스암거_컨테이너.js index 67c606819..69f87a91e 100644 --- a/resources/tester/ui_checks/52-2_박스암거_컨테이너.js +++ b/resources/tester/ui_checks/52-2_박스암거_컨테이너.js @@ -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"); diff --git a/resources/tester/ui_checks/52-4_세월교_컨테이너.js b/resources/tester/ui_checks/52-4_세월교_컨테이너.js index c5c9143e0..0be28be0b 100644 --- a/resources/tester/ui_checks/52-4_세월교_컨테이너.js +++ b/resources/tester/ui_checks/52-4_세월교_컨테이너.js @@ -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"); diff --git a/resources/tester/ui_checks/52-5_횡단수로_개거.js b/resources/tester/ui_checks/52-5_횡단수로_개거.js index 6745c6ed2..8b7b11211 100644 --- a/resources/tester/ui_checks/52-5_횡단수로_개거.js +++ b/resources/tester/ui_checks/52-5_횡단수로_개거.js @@ -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) => { diff --git a/resources/tester/ui_checks/52-6_횡단맹암거.js b/resources/tester/ui_checks/52-6_횡단맹암거.js index 3519947f5..edd0922a7 100644 --- a/resources/tester/ui_checks/52-6_횡단맹암거.js +++ b/resources/tester/ui_checks/52-6_횡단맹암거.js @@ -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, }; diff --git a/resources/tester/ui_checks/54-17_구조물조합_2행.js b/resources/tester/ui_checks/54-17_구조물조합_2행.js new file mode 100644 index 000000000..04aab2119 --- /dev/null +++ b/resources/tester/ui_checks/54-17_구조물조합_2행.js @@ -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 }); +})(); diff --git a/resources/tester/ui_checks/54-6_사면안정_컨테이너.js b/resources/tester/ui_checks/54-6_사면안정_컨테이너.js index 08d22c00c..080b98047 100644 --- a/resources/tester/ui_checks/54-6_사면안정_컨테이너.js +++ b/resources/tester/ui_checks/54-6_사면안정_컨테이너.js @@ -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) =>