From 4ff81f43f19e8df323be588c632c9bc3ad8419d2 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 5 Oct 2026 16:33:40 +0900 Subject: [PATCH] =?UTF-8?q?feat(M02):=2049-7=20=EC=83=81=EC=84=B8=20?= =?UTF-8?q?=EC=85=8B(=EB=8F=84=EB=A9=B4=20=C2=B7=20=EC=9D=BC=EC=9C=84?= =?UTF-8?q?=EB=8C=80=EA=B0=80=20=C2=B7=20=EB=A9=B4=EC=A0=81=EC=82=B0?= =?UTF-8?q?=EC=B6=9C)=20=EB=A8=B8=EB=A6=AC=20=EC=A0=9C=EB=AA=A9=20?= =?UTF-8?q?=EC=98=86=20[=3F]=20=EB=8F=84=EC=9B=80=EB=A7=90=20=E2=80=94=20?= =?UTF-8?q?=EC=B9=B4=EB=93=9C=EC=99=80=20=EA=B0=99=EC=9D=80=20=EA=B8=80=20?= =?UTF-8?q?=ED=95=9C=20=EB=B2=8C=20=C2=B7=20=EC=83=81=EC=84=B8=20=EC=A0=84?= =?UTF-8?q?=EC=9A=A9=20=EC=A4=84(=ED=8C=8C=EC=9D=BC=20=EB=B6=88=EB=9F=AC?= =?UTF-8?q?=EC=98=A4=EA=B8=B0=20=C2=B7=20=EB=8F=84=EB=A9=B4=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=B4=EB=82=B4=EA=B8=B0=20=C2=B7=20=EB=8F=8C?= =?UTF-8?q?=EC=95=84=EA=B0=80=EA=B8=B0)=20=EB=8D=94=ED=95=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_016PSXsvyDxjNmufteN5bqmt --- .../M02_MasterTemplete_CardHelp.ts | 21 ++++ .../M02_MasterTemplete_CardShell.ts | 13 ++- .../M02_MasterTemplete_UI_Main.ts | 16 ++- .../M02_MasterTemplete_UI_Style.css | 15 +++ .../tester/ui_checks/49-7_상세_도움말.js | 98 +++++++++++++++++++ 5 files changed, 160 insertions(+), 3 deletions(-) create mode 100644 resources/tester/ui_checks/49-7_상세_도움말.js diff --git a/M02_MasterTemplete/M02_MasterTemplete_CardHelp.ts b/M02_MasterTemplete/M02_MasterTemplete_CardHelp.ts index 5cb0d4159..1547bfc40 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_CardHelp.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_CardHelp.ts @@ -95,3 +95,24 @@ export const CARD_HELP: Record<"structure" | "labor" | "area", CardHelp> = { ], }, }; + +/** 상세에만 있는 단추 줄(49-7) — 카드 도움말 글 한 벌 뒤에 더함 */ +const DETAIL_ONLY: Record = { + structure: [["파일 불러오기", "DXF · DWG 파일을 도면으로 불러옴(상세 머리 📂)"]], + labor: [], + area: [["도면으로 보내기", "고른 범위를 도면 표로 보냄(상세 머리)"]], +}; +const BACK: [string, string] = ["돌아가기", "구조물집계표로 · 그 열을 고른 채"]; + +/** 상세 머리 [?] 글 — 카드 글 그대로 + 상세에만 있는 줄 */ +export const detailHelp = (kind: keyof typeof CARD_HELP): CardHelp => ({ + ...CARD_HELP[kind], + features: [...CARD_HELP[kind].features, ...DETAIL_ONLY[kind], BACK], +}); + +/** 도움말 상자 제목 — 카드 제목과 같은 글 */ +export const HELP_TITLE: Record = { + structure: "도면 정보", + labor: "일위대가", + area: "면적산출", +}; diff --git a/M02_MasterTemplete/M02_MasterTemplete_CardShell.ts b/M02_MasterTemplete/M02_MasterTemplete_CardShell.ts index fec69381a..e87486094 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_CardShell.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_CardShell.ts @@ -159,8 +159,11 @@ const pairs = (rows: [string, string][]): HTMLElement => children: rows.flatMap(([key, what]) => [el("dt", { text: key }), el("dd", { text: what })]), }); -/** ? — 카드 제목 바로 옆 · 누르면 도움말 상자(카드 오른쪽 위 · 보이고 숨김) · 상자는 본문 앞에 끼움 */ -export function helpButton(shell: CardShell, title: string, help: CardHelp): HTMLElement { +/** ? 단추 + 도움말 상자 한 쌍(카드 · 상세 머리 공용 · 49-7) — 놓는 자리는 부른 쪽이 정함 */ +export function helpParts( + title: string, + help: CardHelp, +): { button: HTMLElement; box: HTMLElement } { const button = createButton({ label: CARD_TEXT.help, variant: "ghost", size: "sm" }); button.title = button.ariaLabel = CARD_TEXT.helpTip.replace("{value}", title); button.classList.add("m02-struct__help"); @@ -188,6 +191,12 @@ export function helpButton(shell: CardShell, title: string, help: CardHelp): HTM event.stopPropagation(); // 누름 ≠ 접기 box.hidden = !box.hidden; }); + return { button, box }; +} + +/** ? — 카드 제목 바로 옆 · 누르면 도움말 상자(카드 오른쪽 위 · 보이고 숨김) · 상자는 본문 앞에 끼움 */ +export function helpButton(shell: CardShell, title: string, help: CardHelp): HTMLElement { + const { button, box } = helpParts(title, help); shell.title.after(button); shell.root.insertBefore(box, shell.body); return button; diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts b/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts index 77d6b3146..44b836d92 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts @@ -29,6 +29,8 @@ import { import { askSettle } from "./M02_MasterTemplete_TreePanel_Dialogs"; import { buildAreaPanel, type AreaPanel } from "./M02_MasterTemplete_AreaPanel"; import { createAreaToDrawing } from "./M02_MasterTemplete_AreaToDrawing"; +import { detailHelp, HELP_TITLE } from "./M02_MasterTemplete_CardHelp"; +import { helpParts } from "./M02_MasterTemplete_CardShell"; import type { SpreadsheetHandle } from "../A00_Common/spreadsheet/spreadsheet"; import type { LinkOptions } from "../A00_Common/spreadsheet/spreadsheet_links"; export interface Selection { @@ -132,6 +134,7 @@ export function createMain( const save = createButton({ label: L("M02_Save"), variant: "filled" }); // 구조물 도면 · 일위대가 · 면적산출 상세 화면 → 구조물집계표(그 열 고른 채) · 일반 단추(43-5 · 알약 · ghost 아님) const back = createButton({ label: L("M02_Back") }); + const helpSlot = el("span", { className: "m02-main__help" }); // 상세 [?](49-7) — 제목 바로 옆 const slot = el("div", { className: "m02-main__slot" }); // 도면 양식의 작도 영역 칸이 들어옴 const notice = el("div", { className: "m02-main__notice", attrs: { hidden: "" } }); const host = el("div", { className: "m02-main__host" }); @@ -143,7 +146,15 @@ export function createMain( el("div", { className: "m02-main__head", // M01 머리 꼴(39-5) — ← 는 제목 앞 · 제목 18px 왼쪽 · 단추는 오른쪽(slot 이 늘어나 밂) - children: [back, title, slot, reload, save, ...(extras.newItem ? [extras.newItem] : [])], + children: [ + back, + title, + helpSlot, + slot, + reload, + save, + ...(extras.newItem ? [extras.newItem] : []), + ], }), ...(extras.search ? [extras.search] : []), notice, @@ -220,6 +231,9 @@ export function createMain( reload.hidden = !sel; save.hidden = !sel || !canEdit(sel.layer, isAdmin); back.hidden = !sel || !(sel.kind in NUMBER_FIELD); + const kind = sel && sel.kind in HELP_TITLE ? (sel.kind as keyof typeof HELP_TITLE) : null; + const help = kind && helpParts(HELP_TITLE[kind], detailHelp(kind)); // 열 때마다 새로 — 상자는 닫힘 + helpSlot.replaceChildren(...(help ? [help.button, help.box] : [])); }; // 면적산출 범위 → 도면 표(41-3) — 몸은 `AreaToDrawing` · 여기는 갈고리만 diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Style.css b/M02_MasterTemplete/M02_MasterTemplete_UI_Style.css index 730b3775d..143db13d8 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Style.css +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Style.css @@ -65,6 +65,7 @@ } .m02-main__head { + position: relative; /* 상세 [?] 도움말 상자의 기준(49-7) */ display: flex; flex-wrap: wrap; align-items: center; @@ -84,6 +85,20 @@ white-space: nowrap; } +/* 상세 [?] — 단추는 제목 바로 옆 흐름 · 상자는 머리 아래 오른쪽에 뜸 */ +.m02-main__help { + display: contents; +} + +.m02-main__head > .m02-main__help > .m02-struct__keys { + top: calc(100% + var(--spacing-8)); + right: 0; + width: max-content; + max-width: min(36rem, 100%); + max-height: 70vh; + z-index: 20; +} + .m02-main__slot { display: flex; flex: 1 1 auto; diff --git a/resources/tester/ui_checks/49-7_상세_도움말.js b/resources/tester/ui_checks/49-7_상세_도움말.js new file mode 100644 index 000000000..90fe7ca76 --- /dev/null +++ b/resources/tester/ui_checks/49-7_상세_도움말.js @@ -0,0 +1,98 @@ +/* 49-7 — 구조물 조합 상세 셋(도면 정보 · 일위대가 · 면적산출) 머리 [?] = 제목 바로 옆 · 도움말 네 절 · 카드와 같은 글 + * 시작 화면: M02 마스터 템플릿 · 마스터 층(#/m02-master-template?open=구조물조합&root=master) · 시스템 관리자 · + * 나무에 item1(구조물 조합)이 있음 + * 부르는 법: orca eval --page <탭> --expression "$(cat 이 파일)" (async 식 · JSON 글을 돌려줌 · 40초 안) + * 쓰는 것: 없음(누르기 · 보기만 · 도움말 상자는 닫고 끝 · 상세에서 [돌아가기]) + * 기대 수치(상세 셋 모두): + * - nextToTitle = true(? 가 제목 글 바로 다음) · headOk = true(돌아가기 · 제목 · ? 가 한 줄 · 제목 줄 안 접힘) · + * parts = ["about","keys","features","steps"] · opened = true · closed = true · + * sameText = true(설명 · 단축키 · 사용법 = 카드와 같은 글 · 기능 = 카드 줄 그대로 + 상세 줄) · + * detailOnly = 상세에만 있는 줄 이름들(도면 = 파일 불러오기 · 돌아가기 / 면적 = 도면으로 보내기 · 돌아가기 / 일위대가 = 돌아가기) · back = true + */ +(async () => { + const w = (ms) => new Promise((r) => setTimeout(r, ms)); + const until = async (fn, ms = 8000) => { + for (let t = 0; t < ms; t += 150) { + const v = fn(); + if (v) return v; + await w(150); + } + return null; + }; + const vis = (b) => b && !b.hidden && b.offsetParent !== null; + const textOf = (box, part) => + [...(box?.querySelector(`[data-help-part="${part}"]`)?.children ?? [])] + .slice(1) + .map((n) => n.textContent) + .join("|"); + const rows = (box, part) => + [...(box?.querySelectorAll(`[data-help-part="${part}"] dt`) ?? [])].map( + (dt) => `${dt.textContent}=${dt.nextElementSibling?.textContent}`, + ); + const node = await until(() => document.querySelector('[data-node="i|item1"]')); + if (!node) return JSON.stringify({ error: "item1 없음 — 폴더를 펼쳐 둘 것" }); + node.click(); + await until(() => document.querySelector(".m02-struct__preview iframe")); + await w(2500); + const out = { each: [] }; + const hash = location.hash; + for (let i = 0; i < 3; i++) { + const cards = [...document.querySelectorAll(".m02-struct > .m02-struct__card")].filter(vis); + const card = cards[i]; + if (!card) return JSON.stringify({ ...out, error: `카드 ${i} 없음` }); + const cardBox = card.querySelector(":scope > .m02-struct__keys"); + const cardSnap = { + about: textOf(cardBox, "about"), + keys: rows(cardBox, "keys"), + features: rows(cardBox, "features"), + steps: textOf(cardBox, "steps"), + }; + const edit = [...card.querySelectorAll(".m02-struct__card-tools button")].find( + (b) => vis(b) && b.innerText.trim() === "수정", + ); + if (!edit) return JSON.stringify({ ...out, error: `카드 ${i} [수정] 없음` }); + edit.click(); + const head = await until(() => { + const h = document.querySelector(".m02-main__head"); + return h && !h.parentElement.hidden && h.querySelector(".m02-struct__help") ? h : null; + }, 12000); + const row = { card: card.querySelector(":scope > h3").firstElementChild.textContent }; + if (!head) { + out.each.push({ ...row, error: "상세 ? 없음" }); + continue; + } + await w(800); + const title = head.querySelector(".m02-main__title"); + const help = head.querySelector(".m02-struct__help"); + const box = head.querySelector(".m02-struct__keys"); + const back = [...head.querySelectorAll("button")].find((b) => b.innerText.trim() === "돌아가기"); + const tb = title.getBoundingClientRect(); + const hb = help.getBoundingClientRect(); + row.detailTitle = title.textContent; + row.nextToTitle = title.nextElementSibling?.contains(help) && vis(help); + row.headOk = !!back && vis(back) && Math.abs(tb.top + tb.height / 2 - (hb.top + hb.height / 2)) < 24 && hb.left >= tb.right - 1 && head.getBoundingClientRect().height < 80; + row.parts = [...box.querySelectorAll("[data-help-part]")].map((s) => s.dataset.helpPart); + help.click(); + await w(150); + const bb = box.getBoundingClientRect(); + row.opened = !box.hidden && bb.width > 100 && bb.right <= window.innerWidth + 1; + const features = rows(box, "features"); + row.sameText = + textOf(box, "about") === cardSnap.about && + textOf(box, "steps") === cardSnap.steps && + JSON.stringify(rows(box, "keys")) === JSON.stringify(cardSnap.keys) && + JSON.stringify(features.slice(0, cardSnap.features.length)) === JSON.stringify(cardSnap.features); + row.detailOnly = features.slice(cardSnap.features.length).map((f) => f.split("=")[0]); + help.click(); + await w(100); + row.closed = box.hidden; + out.each.push(row); + back?.click(); + row.back = !!(await until( + () => location.hash === hash && document.querySelector(".m02-struct__card"), + 10000, + )); + await w(1500); + } + return JSON.stringify(out); +})();