Merge remote-tracking branch 'origin/dev' into sub_laptop_6

This commit is contained in:
2026-10-05 13:42:29 +09:00
8 changed files with 275 additions and 49 deletions
@@ -0,0 +1,97 @@
/* =============================================================================
* M02_MasterTemplete_CardHelp.ts
* 항목 카드 셋(도면 정보 · 일위대가 · 면적산출) [?] 도움말 글(PLAN 47-1) — 설명 · 단축키 · 기능 · 사용법.
* 단축키는 실제로 받는 것만(도면 = `DRAWING_KEYS` · 엑셀 = 스프레드시트 미리보기 `spreadsheet_keys.ts`) ·
* 기능은 카드 제목 줄 단추 차례대로 · 카드 단추 글이 바뀌면 여기도.
* ========================================================================== */
import type { CardHelp } from "./M02_MasterTemplete_CardShell";
import { DRAWING_KEYS } from "./M02_MasterTemplete_DrawingCard";
/** 엑셀 카드 단축키 — 스프레드시트 미리보기(`spreadsheet_keys.ts`)가 받는 것만 */
const SHEET_KEYS: [string, string][] = [
["글자 · Enter · F2", "칸 고침(= 로 시작하면 식)"],
["화살표 · Tab · Enter", "칸 옮기기"],
["Shift+화살표 · Ctrl+A", "고르기 · 전부"],
["Delete · Backspace", "비우기 · 새로 쓰기"],
["Ctrl+C · Ctrl+V", "복사 · 붙여넣기"],
["Ctrl+D · Ctrl+R", "아래로 · 오른쪽으로 채우기"],
["Ctrl+Z · Ctrl+Y", "되돌리기 · 다시 하기"],
["Esc", "고침 멈춤"],
["Ctrl+S", "저장(카드 [저장]과 같음)"],
];
const SAVE: [string, string] = ["저장", "카드에서 고친 것을 저장 — 고쳐야 켜짐 · Ctrl+S 와 같음"];
const take = (what: string): [string, string] => [
"가져오기",
`다른 항목의 ${what}을(를) 골라 이 칸에 덮어씀 — 번호 · 열은 그대로`,
];
const clear = (what: string): [string, string] => [
"삭제",
`이 항목의 ${what}을(를) 비움 — 번호 · 열은 그대로 · 되돌릴 수 없음`,
];
const FOLD: [string, string] = ["제목 줄 누름", "카드 접기 · 펴기"];
export const CARD_HELP: Record<"structure" | "labor" | "area", CardHelp> = {
structure: {
about:
"고른 구조물의 표준 도면 미리보기 — 작은 고침은 카드에서 바로 · 큰 작업은 도면 상세에서.",
keys: DRAWING_KEYS,
features: [
SAVE,
take("도면"),
["수정", "도면 상세(전체 편집 화면)를 엶 — 파일 불러오기(DXF · DWG)는 여기"],
clear("도면"),
FOLD,
],
steps: [
"왼쪽 표에서 구조물 항목을 고름",
"미리보기 안을 누르고 명령 글자(L · C …)를 친 뒤 Enter",
"고쳤으면 [저장] 또는 Ctrl+S",
"파일을 불러오거나 크게 고칠 때는 [수정]",
],
},
labor: {
about: "고른 구조물의 일위대가 — 담은 단가산출 로직 줄과 자재목록 · 금액은 서버가 다시 셈.",
keys: [
["Ctrl+S", "저장(카드 [저장]과 같음)"],
["Enter", "별칭 칸 고침 끝"],
],
features: [
SAVE,
take("일위대가"),
["수정", "일위대가 상세를 엶 — 로직 추가 · 순서 바꾸기 · 다이어그램은 여기"],
clear("일위대가"),
["로직 줄 누름", "넣을 값 창 — 그 로직의 넣을 값 고침"],
["수량산출 칸", "손 입력 · 면적산출 칸 고르기"],
["별칭 칸", "줄에 따로 이름 붙임"],
["줄 끝 단추", "그 줄을 뺌"],
["자재 추가", "자재를 골라 자재목록에 넣음"],
FOLD,
],
steps: [
"왼쪽 표에서 구조물 항목을 고름",
"로직 줄을 눌러 넣을 값을 고침",
"수량산출 칸에서 손 입력 또는 면적산출 칸을 고름",
"[저장] — 로직을 더하거나 순서를 바꿀 때는 [수정]",
],
},
area: {
about: "고른 구조물의 면적산출 엑셀 — 칸을 바로 고침 · 알약 = 일위대가 수량에 잇는 칸 표시.",
keys: SHEET_KEYS,
features: [
["알약", "아래 알약 줄 보임 · 숨김"],
SAVE,
take("면적산출"),
["수정", "면적산출 상세(전체 편집 화면)를 엶"],
clear("면적산출"),
FOLD,
],
steps: [
"왼쪽 표에서 구조물 항목을 고름",
"칸을 고르고 바로 침 — = 로 시작하면 식",
"[알약] 을 켜고 칸을 고른 뒤 아래 알약을 누르면 그 칸에 이음",
"고쳤으면 [저장] 또는 Ctrl+S",
],
},
};
@@ -4,7 +4,7 @@
* 내용만 넣음. 모양 · 높이 · 스크롤은 `M02_MasterTemplete_Structure.css`.
* 머리 = 제목 줄(h3 · 제목 · 저장 안 한 점 ● · 오른쪽 단추 자리) + 한 줄 부제(번호 · 이름 · 넘치면 말줄임)
* 본문 = 카드 높이를 채움 · 넘치면 카드 안 스크롤 · 상태 = 불러오는 중 · 실패 + [다시 불러오기] · 비었음
* [저장](`saveControl`) = 늘 보임 · 고쳐야 켜짐 · 카드 안 Ctrl+S · 알림 글 한 벌 · ?(`helpButton`) = 단축키 상자
* [저장](`saveControl`) = 늘 보임 · 고쳐야 켜짐 · 카드 안 Ctrl+S · 알림 글 한 벌 · ?(`helpButton`) = 제목 옆 · 도움말 상자
* 접기는 부모의 attachCollapsible(제목 줄 클릭) — 오른쪽 단추를 눌러도 접히지 않음.
* ========================================================================== */
@@ -18,6 +18,11 @@ export const CARD_TEXT = {
failed: "저장 못 함 — {value}",
stale: "그 사이 바뀜 — 다시 읽음",
help: "?",
helpTip: "{value} 도움말",
helpAbout: "설명",
helpKeys: "단축키",
helpFeatures: "기능",
helpSteps: "사용법",
};
export interface CardShell {
@@ -25,6 +30,8 @@ export interface CardShell {
body: HTMLElement;
/** 제목 줄 오른쪽 단추 자리 */
tools: HTMLElement;
/** 제목 글 — 바로 옆에 ?(`helpButton`) */
title: HTMLElement;
/** 저장 안 한 고침 — 제목 옆 점(●) */
setDirty: (dirty: boolean) => void;
/** 0.3초 넘으면 본문에 「불러오는 중」 — 돌려받은 함수 = 끝 */
@@ -53,13 +60,14 @@ export const cardNote = (text: string): HTMLElement =>
export function cardShell(opt: CardShellOptions): CardShell {
const dot = el("span", { className: "m02-struct__dot", text: "●", attrs: { hidden: "" } });
const tools = el("span", { className: "m02-struct__card-tools" });
const title = el("span", { text: opt.title });
tools.addEventListener("click", (event) => event.stopPropagation()); // 단추 누름 ≠ 접기
const root = el("section", {
className: `m02-struct__card ${opt.className ?? ""} ui-collapsible ui-sidebar-section ui-sidebar-card`,
children: [
el("h3", {
className: "ui-collapsible__title",
children: [el("span", { text: opt.title }), dot, tools],
children: [title, dot, tools],
}),
...(opt.sub ? [el("div", { className: "m02-struct__sub", children: opt.sub })] : []),
opt.body,
@@ -76,6 +84,7 @@ export function cardShell(opt: CardShellOptions): CardShell {
root,
body: opt.body,
tools,
title,
setDirty: (dirty) => (dot.hidden = !dirty),
loading: () => showLoading(host),
fail: (reason, retry) => host.replaceChildren(loadingError(reason, retry)),
@@ -137,24 +146,49 @@ export function saveControl(
return { button, dirty: () => dirty, mark, run };
}
/** ? — 단축키 상자(카드 오른쪽 위 · 누르면 보이고 숨김) · 상자는 본문 앞에 끼움(본문 채움 규칙 그대로) */
export function helpButton(shell: CardShell, title: string, keys: [string, string][]): HTMLElement {
const help = createButton({ label: CARD_TEXT.help, variant: "ghost", size: "sm" });
help.title = help.ariaLabel = title;
/** [?] 도움말 네 절(47-1) — 설명 · 단축키 · 기능(단추마다 한 줄) · 사용법(차례) */
export interface CardHelp {
about: string;
keys: [string, string][];
features: [string, string][];
steps: string[];
}
const pairs = (rows: [string, string][]): HTMLElement =>
el("dl", {
children: rows.flatMap(([key, what]) => [el("dt", { text: key }), el("dd", { text: what })]),
});
/** ? — 카드 제목 바로 옆 · 누르면 도움말 상자(카드 오른쪽 위 · 보이고 숨김) · 상자는 본문 앞에 끼움 */
export function helpButton(shell: CardShell, title: string, help: CardHelp): 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");
const part = (name: string, heading: string, body: HTMLElement): HTMLElement =>
el("section", {
attrs: { "data-help-part": name },
children: [el("h4", { text: heading }), body],
});
const box = el("div", {
className: "m02-struct__keys",
attrs: { hidden: "" },
children: [
el("strong", { text: title }),
el("dl", {
children: keys.flatMap(([key, what]) => [
el("dt", { text: key }),
el("dd", { text: what }),
]),
}),
part("about", CARD_TEXT.helpAbout, el("p", { text: help.about })),
part("keys", CARD_TEXT.helpKeys, pairs(help.keys)),
part("features", CARD_TEXT.helpFeatures, pairs(help.features)),
part(
"steps",
CARD_TEXT.helpSteps,
el("ol", { children: help.steps.map((text) => el("li", { text })) }),
),
],
});
help.addEventListener("click", () => (box.hidden = !box.hidden));
button.addEventListener("click", (event) => {
event.stopPropagation(); // 누름 ≠ 접기
box.hidden = !box.hidden;
});
shell.title.after(button);
shell.root.insertBefore(box, shell.body);
return help;
return button;
}
@@ -47,6 +47,8 @@ export interface DrawingTemplateOptions {
originMarker?: boolean;
/** 구조물 도면 — 도각이 없으므로 작도 영역 단추 · 자리표(도각 자리) 표시를 없애고, 문서의 작도 영역 값은 무시. */
noArea?: boolean;
/** 파일 불러오기(📂) 단추를 안 둠 — 집계표 카드 미리보기(불러오기는 도면 상세에서만). */
noImport?: boolean;
}
const RECOVERY_PREFIX = "OPEN_WEB_CAD__RECOVERY__";
@@ -222,7 +224,8 @@ export function mountDrawingTemplate(
const actions = document.createElement("div");
actions.className = "m02-drawing__actions";
if (!readOnly) {
const canImport = !readOnly && options.noImport !== true;
if (canImport) {
const fileInput = document.createElement("input");
fileInput.type = "file";
fileInput.accept = ".dxf,.dwg";
@@ -274,7 +277,7 @@ export function mountDrawingTemplate(
toolbar.classList.add("m02-drawing__toolbar--slot");
options.headerSlot.append(toolbar);
root.append(cad.element);
} else root.append(...(noArea && readOnly ? [] : [toolbar]), cad.element);
} else root.append(...(noArea && !canImport ? [] : [toolbar]), cad.element);
container.append(root);
load(doc);
@@ -13,8 +13,6 @@ import type { SaveControl } from "./M02_MasterTemplete_CardShell";
import { mountDrawingTemplate, type DrawingTemplateDoc } from "./M02_MasterTemplete_Drawing";
import type { ItemSource } from "./M02_MasterTemplete_ItemSource";
export const DRAWING_HELP = "단축키 — 글자를 치고 Enter";
/** 단축키 도움말 — 전체 편집기 명령 별칭(openwebcad `commands.*.ts`)과 같은 것만 */
export const DRAWING_KEYS: [string, string][] = [
["L · PL", "선 · 폴리선"],
@@ -98,6 +96,7 @@ export function mountDrawingCard(host: HTMLElement, opt: DrawingCardOptions): Dr
preview: !edit, // 보기만 = 왼쪽 끌기 화면 옮기기 · 고치기 = 누름 고르기 · 단축키
originMarker: true, // 좌표 0,0 기준점 십자
noArea: true, // 구조물도는 도각이 없음
noImport: true, // 파일 불러오기는 도면 상세에서만(47-2)
...(edit
? {
onChanged: (dirty: boolean) => opt.saver.mark(dirty),
@@ -32,9 +32,8 @@ import {
type CardShell,
type SaveControl,
} from "./M02_MasterTemplete_CardShell";
import { CARD_HELP } from "./M02_MasterTemplete_CardHelp";
import {
DRAWING_HELP,
DRAWING_KEYS,
drawingEditable,
mountDrawingCard,
type DrawingCardHandle,
@@ -70,7 +69,6 @@ const TEXT = {
noSheet: "스프레드시트를 못 띄움 — {value}",
readFailed: "{value} 파일을 못 읽음 — {why}",
discard: "카드에 저장 안 한 고침이 있음 — 버리고 다른 항목을 열까요?",
sheetHelp: "단축키 — 칸을 고르고 바로 침",
pills: "알약",
pillsTip: "아래 알약 줄 보임 · 숨김",
take: "가져오기",
@@ -81,19 +79,6 @@ const TEXT = {
takeTip: "다른 항목의 {value} 을(를) 골라 이 칸에 덮어씀 — 번호 · 열은 그대로",
};
/** 엑셀 카드 단축키 — 스프레드시트 미리보기(`spreadsheet_keys.ts`)가 받는 것만 */
const SHEET_KEYS: [string, string][] = [
["글자 · Enter · F2", "칸 고침(= 로 시작하면 식)"],
["화살표 · Tab · Enter", "칸 옮기기"],
["Shift+화살표 · Ctrl+A", "고르기 · 전부"],
["Delete · Backspace", "비우기 · 새로 쓰기"],
["Ctrl+C · Ctrl+V", "복사 · 붙여넣기"],
["Ctrl+D · Ctrl+R", "아래로 · 오른쪽으로 채우기"],
["Ctrl+Z · Ctrl+Y", "되돌리기 · 다시 하기"],
["Esc", "고침 멈춤"],
["Ctrl+S", "저장(카드 [저장]과 같음)"],
];
const why = (error: unknown): string => (error instanceof Error ? error.message : "");
const fill = (text: string, value: string): string => text.replace("{value}", value);
/** 스프레드시트는 면적산출 카드를 열 때만 받음 */
@@ -196,7 +181,7 @@ export function createItemCards(opts: ItemCardsOptions): ItemCardsHandle {
const anyDirty = (): boolean => cards.some((c) => c.saver.dirty());
const reload = (): void => void load(column ? { id: column, label } : null, true);
/** 카드 틀 하나 — 제목 줄 오른쪽 = ? · [저장] · [〇〇 수정](빈 단추 한 모양) · 부제 = 번호 · 이름 */
/** 카드 틀 하나 — 제목 옆 = ? · 제목 줄 오른쪽 = [저장] · [〇〇 수정](빈 단추 한 모양) · 부제 = 번호 · 이름 */
const makeCard = (
kind: CardKind,
title: string,
@@ -207,11 +192,7 @@ export function createItemCards(opts: ItemCardsOptions): ItemCardsHandle {
const name = el("span", { className: "m02-struct__title" });
const shell = cardShell({ title, body, sub: [no, name] });
const saver = saveControl(shell, save, () => opts.onDirty?.(anyDirty()));
const help = helpButton(
shell,
kind === "structure" ? DRAWING_HELP : TEXT.sheetHelp,
kind === "structure" ? DRAWING_KEYS : SHEET_KEYS,
);
const help = helpButton(shell, title, CARD_HELP[kind]); // 제목 바로 옆(47-1)
const edit = createButton({
label: TEXT.edit,
variant: "ghost",
@@ -225,7 +206,7 @@ export function createItemCards(opts: ItemCardsOptions): ItemCardsHandle {
const del = createButton({ label: TEXT.clear, variant: "danger", size: "sm" });
del.title = fill(TEXT.clearTip, PART_WORD[kind]);
del.addEventListener("click", () => void clearCard(kind));
shell.tools.append(help, saver.button, take, edit, del);
shell.tools.append(saver.button, take, edit, del);
return { kind, shell, saver, help, edit, take, del, no, name };
};
@@ -350,15 +331,14 @@ export function createItemCards(opts: ItemCardsOptions): ItemCardsHandle {
});
attachCollapsible(root);
/** [저장] · ? 는 고칠 수 있는 카드에서 늘 보임(고쳐야 켜짐) · [수정] 은 editable 일 때 */
/** [저장] 은 고칠 수 있는 카드에서 늘 보임(고쳐야 켜짐) · ? 는 늘 · [수정] 은 editable 일 때 */
const paintTools = (): void => {
const drawEdit = drawingEditable(opts.editable(), opts.source());
const sheetEdit = !!opts.sheetEdit?.can();
for (const c of cards) {
const can =
c.kind === "structure" ? drawEdit : c.kind === "labor" ? opts.editable() : sheetEdit;
c.saver.button.hidden = c.help.hidden = !can;
if (c.kind === "labor") c.help.hidden = true; // 일위대가 카드엔 단축키 상자 없음
c.saver.button.hidden = !can;
c.edit.hidden = !opts.editable();
c.take.hidden = !opts.editable() || !opts.onImport;
c.del.hidden = !opts.editable() || !opts.onClear;
@@ -157,7 +157,7 @@
min-height: 0;
}
/* ? 단축키 상자 — 카드 오른쪽 위(제목 줄 · 부제 아래 · 도면 · 엑셀 카드) */
/* ? 도움말 상자 — 카드 오른쪽 위(제목 줄 · 부제 아래 · 도면 · 엑셀 카드) */
.m02-struct__card > .m02-struct__keys {
top: calc(var(--spacing-64) + var(--spacing-8));
}
@@ -196,6 +196,26 @@
margin: 0;
}
/* ? — 제목 바로 옆(47-1) · 도움말 네 절(설명 · 단축키 · 기능 · 사용법) */
.m02-struct__help {
flex: none;
font-weight: normal;
}
.m02-struct__keys h4 {
margin: var(--spacing-12) 0 0;
font-size: inherit;
}
.m02-struct__keys p,
.m02-struct__keys ol {
margin: var(--spacing-8) 0 0;
}
.m02-struct__keys ol {
padding-left: 1.5em;
}
/* 면적산출 · 재료산출 미리보기 — `.ss` 는 height:100% 로 부모 높이를 그대로 받는데 부모(이 칸)에 높이가
없으면 0 이 되어 칸이 안 보였음(진짜 원인). 약 30행이 늘 보이게 높이를 정하고
넘치면 격자 안쪽(.ss-grid-host)이 스스로 스크롤(spreadsheet_grid.css). */
+29 -4
View File
@@ -19,7 +19,8 @@ def _text(name: str) -> str:
def test_틀_제목_줄_부제_상태():
shell = _text("M02_MasterTemplete_CardShell.ts")
assert 'children: [el("span", { text: opt.title }), dot, tools],' in shell
assert "children: [title, dot, tools]," in shell
assert 'const title = el("span", { text: opt.title });' in shell
assert 'tools.addEventListener("click", (event) => event.stopPropagation());' in shell
assert 'el("div", { className: "m02-struct__sub", children: opt.sub })' in shell
assert "loading: () => showLoading(host)," in shell
@@ -40,10 +41,12 @@ def test_저장_단추_알림_한_벌():
def test_네_카드가_틀을_씀_수정_단추_한_모양():
cards = _text("M02_MasterTemplete_ItemCards.ts")
assert "const shell = cardShell({ title, body, sub: [no, name] });" in cards
assert "shell.tools.append(help, saver.button, take, edit, del);" in cards # 40-2
assert (
"shell.tools.append(saver.button, take, edit, del);" in cards
) # 40-2 · ? 는 제목 옆(47-1)
assert 'variant: "filled"' not in cards # [수정] 은 모두 빈 단추(ghost)
assert "c.saver.button.hidden = c.help.hidden = !can;" in cards # [저장] · ? 늘 보임
assert "DRAWING_KEYS" in cards and "SHEET_KEYS" in cards
assert "c.saver.button.hidden = !can;" in cards # [저장] 늘 보임 · ? 는 늘
assert "helpButton(shell, title, CARD_HELP[kind])" in cards
inputs = _text("M02_Labor_Values.ts")
assert inputs.count("cardShell({") == 1 # 넣을 값 카드(선택 입력 카드는 없앰 · 39-7b)
assert "shell.fail(" in inputs and "shell.loading()" in inputs
@@ -90,3 +93,25 @@ def test_일위대가_상세_단위는_표준_단위_고르기():
assert "extra: [unit0, cur.단위].filter((u): u is string => !!u)," in detail
assert "cur.단위 = u === NO_UNIT ? null : u;" in detail
assert "readOnly: !edit," in detail and "maxLength = 20" not in detail
def test_도움말_제목_옆_네_절():
"""47-1 — ? = 카드 제목 바로 옆 · 도움말 = 설명 · 단축키 · 기능 · 사용법 · 카드 셋 모두."""
shell = _text("M02_MasterTemplete_CardShell.ts")
assert "shell.title.after(button);" in shell
for part in ("about", "keys", "features", "steps"):
assert f'"{part}"' in shell
assert "event.stopPropagation(); // 누름 ≠ 접기" in shell
help_ = _text("M02_MasterTemplete_CardHelp.ts")
for kind in ("structure:", "labor:", "area:"):
assert kind in help_
assert "keys: DRAWING_KEYS," in help_ and "keys: SHEET_KEYS," in help_
for label in ("가져오기", "수정", "삭제", "저장", "알약"):
assert f'"{label}"' in help_
def test_미리보기_파일_불러오기_없음():
"""47-2 — 도면 카드 미리보기엔 📂 없음 · 도면 상세(전체 편집)는 그대로."""
drawing = _text("M02_MasterTemplete_Drawing.ts")
assert "const canImport = !readOnly && options.noImport !== true;" in drawing
assert "noImport: true," in _text("M02_MasterTemplete_DrawingCard.ts")
@@ -0,0 +1,68 @@
/* 47-1 · 47-2 — 구조물 조합 카드 셋 [?] = 제목 바로 옆 · 도움말 네 절 · 도면 미리보기 📂 없음 · 도면 상세 📂 있음
* 시작 화면: M02 마스터 템플릿 · 마스터 층(#/m02-master-template?open=구조물조합&root=master) · 시스템 관리자 ·
* 나무에 item1(구조물 조합)이 있음
* 부르는 법: orca eval --expression "$(cat 이 파일)" (async 식 · JSON 글을 돌려줌 · 20초 안)
* 쓰는 것: 없음(누르기 · 보기만 · 도움말 상자는 닫고 끝 · 상세에서 [돌아가기])
* 기대 수치:
* - cards = 3(도면 정보 · 일위대가 · 면적산출) · 카드마다 nextToTitle = true(? 가 제목 글 바로 다음) ·
* inTools = false · parts = ["about","keys","features","steps"] · opened = true · closed = true
* - previewImport = 0(카드 미리보기 📂) · detailImport = 1(도면 상세 📂) · 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 imports = () =>
[...document.querySelectorAll('button[title="파일 불러오기 (DXF · DWG)"]')].filter(vis).length;
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 = {};
const cards = [...document.querySelectorAll(".m02-struct > .m02-struct__card")].filter(vis);
out.cards = cards.length;
out.each = [];
for (const card of cards) {
const h3 = card.querySelector(":scope > h3");
const help = h3.querySelector(".m02-struct__help");
const box = card.querySelector(":scope > .m02-struct__keys");
const row = {
title: h3.firstElementChild?.textContent,
nextToTitle: !!help && h3.firstElementChild.nextElementSibling === help && vis(help),
inTools: !!h3.querySelector(".m02-struct__card-tools .m02-struct__help"),
parts: [...(box?.querySelectorAll("[data-help-part]") ?? [])].map((s) => s.dataset.helpPart),
};
help?.click();
await w(150);
row.opened = !!box && !box.hidden && !card.classList.contains("is-collapsed");
row.features = box?.querySelectorAll('[data-help-part="features"] dt').length;
help?.click();
await w(100);
row.closed = !!box?.hidden;
out.each.push(row);
}
out.previewImport = imports();
// 도면 상세 — 도면 정보 카드 [수정]
const edit = [...cards[0].querySelectorAll(".m02-struct__card-tools button")].find(
(b) => vis(b) && b.innerText.trim() === "수정",
);
if (!edit) return JSON.stringify({ ...out, error: "도면 [수정] 없음" });
const hash = location.hash;
edit.click();
out.detailImport = (await until(() => imports(), 8000)) || 0;
[...document.querySelectorAll("button")]
.find((b) => vis(b) && b.innerText.trim() === "돌아가기")
?.click();
out.back = !!(await until(
() => location.hash === hash && document.querySelector(".m02-struct__card"),
));
return JSON.stringify(out);
})();