From 5d61d1de580b89e2cc6a6b6b50da1cc6ec7f7e21 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 5 Oct 2026 13:31:57 +0900 Subject: [PATCH 1/3] =?UTF-8?q?chore(sync):=20=EC=88=98=EC=A0=95=EC=9A=94?= =?UTF-8?q?=EA=B5=AC=20=EC=B6=94=EA=B0=80=202=20=EC=9B=90=EB=AC=B8=20?= =?UTF-8?q?=E2=80=94=20=EA=B3=B5=EC=9C=A0=20docs=20=EB=A1=9C=20=EB=93=A4?= =?UTF-8?q?=EC=96=B4=EC=98=A8=20=EA=B0=99=EC=9D=80=20=EB=82=B4=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016PSXsvyDxjNmufteN5bqmt --- docs/raw/사용자요구/2026-10-05_수정요구.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/raw/사용자요구/2026-10-05_수정요구.md b/docs/raw/사용자요구/2026-10-05_수정요구.md index e48eaa057..eeadef7f7 100644 --- a/docs/raw/사용자요구/2026-10-05_수정요구.md +++ b/docs/raw/사용자요구/2026-10-05_수정요구.md @@ -39,3 +39,12 @@ ## 추가 (10-5 · 원문 그대로) 다음으로 구조물 조합 분류 테이블과 상세컨테이너사이가 공간이 너무 넣어 그리고 구조물 분류 테이블의 하단에 구분선이 있는것같은데 삭제해줘. 컨테이너의 상단에도 구분선이 있어. + +## 추가 2 (10-5 · 원문 그대로) + +1. 구조물 조합의 ?버튼은 설명같은데 미리보기의 ? 버튼을 미리보기 항목 옆으로 이동하고 설명과 지금처럼 단축키와 기능 설명과 사용법을 작성해줘. +2. 구조물 조합의 파일 불러오기 기능은 미리보기에서 보다는 상세 페이지에서 작업할 내용임. 미리보기에서는 항목 삭제 +3. M01의 단가산출 로직의 항목 상세에서 우측 패널에 대한 구현이 잘못됨. 우측 흐름도의 패널과 원문 패널을 별도로 구성하고 사용자가 두개의 패널을 각각 접었다 펼수 있어야함. 단 동시에 두개의 패널을 볼 경우는 없으며 폭정보는 공유하고 있고 둘다 접힌 상태일 수는 있음. +4. 구조물 조합의 단가산출 상세화면에도 원문보기 우측 패널이 있어야함. 우측 패널은 단가산출 로직을 선택할 경우에 원문을 보여주는 기능이며 패널을 접었다 펼수 있어야함. +5. 원문 패널 추가에 따라 현재 단가산출 로직 항목을 선택하면 옵션창이 보이나 선택하면 하이라이트를 하고 원문을 보일수 있게 함. 그리고 옵션설정에 대한 조정은 올리기, 내리기, 빼기 버튼의 이름을 위로, 아래로, 삭제로 변경하고 아래로 와 삭제 사이에 신규 버튼(이름 : 옵션)을 추가하여 선택할 수 있게 변경. 버튼 사이간격에 대해 템플릿화하여 적용. +6. 구조물 조합의 면적산출 우측패널에 별도의 도면 패널 추가(기능은 4번 요청 확인) From b79dd240667d4201a3d079903ce8b87a6b8c1b50 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 5 Oct 2026 13:32:09 +0900 Subject: [PATCH 2/3] =?UTF-8?q?chore(docs):=20=EA=B3=B5=EC=9C=A0=20docs=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=EC=9A=94=EA=B5=AC=20=EC=B6=94=EA=B0=80=202?= =?UTF-8?q?=20=EB=B0=98=EC=98=81=20=E2=80=94=20=EB=B0=9B=EA=B8=B0=20?= =?UTF-8?q?=EC=A0=84=20=EC=9E=91=EC=97=85=20=ED=8A=B8=EB=A6=AC=20=EB=A7=9E?= =?UTF-8?q?=EC=B6=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01W4uKaV4ZBhsJN4GHG7yRj4 --- docs/raw/사용자요구/2026-10-05_수정요구.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/raw/사용자요구/2026-10-05_수정요구.md b/docs/raw/사용자요구/2026-10-05_수정요구.md index e48eaa057..eeadef7f7 100644 --- a/docs/raw/사용자요구/2026-10-05_수정요구.md +++ b/docs/raw/사용자요구/2026-10-05_수정요구.md @@ -39,3 +39,12 @@ ## 추가 (10-5 · 원문 그대로) 다음으로 구조물 조합 분류 테이블과 상세컨테이너사이가 공간이 너무 넣어 그리고 구조물 분류 테이블의 하단에 구분선이 있는것같은데 삭제해줘. 컨테이너의 상단에도 구분선이 있어. + +## 추가 2 (10-5 · 원문 그대로) + +1. 구조물 조합의 ?버튼은 설명같은데 미리보기의 ? 버튼을 미리보기 항목 옆으로 이동하고 설명과 지금처럼 단축키와 기능 설명과 사용법을 작성해줘. +2. 구조물 조합의 파일 불러오기 기능은 미리보기에서 보다는 상세 페이지에서 작업할 내용임. 미리보기에서는 항목 삭제 +3. M01의 단가산출 로직의 항목 상세에서 우측 패널에 대한 구현이 잘못됨. 우측 흐름도의 패널과 원문 패널을 별도로 구성하고 사용자가 두개의 패널을 각각 접었다 펼수 있어야함. 단 동시에 두개의 패널을 볼 경우는 없으며 폭정보는 공유하고 있고 둘다 접힌 상태일 수는 있음. +4. 구조물 조합의 단가산출 상세화면에도 원문보기 우측 패널이 있어야함. 우측 패널은 단가산출 로직을 선택할 경우에 원문을 보여주는 기능이며 패널을 접었다 펼수 있어야함. +5. 원문 패널 추가에 따라 현재 단가산출 로직 항목을 선택하면 옵션창이 보이나 선택하면 하이라이트를 하고 원문을 보일수 있게 함. 그리고 옵션설정에 대한 조정은 올리기, 내리기, 빼기 버튼의 이름을 위로, 아래로, 삭제로 변경하고 아래로 와 삭제 사이에 신규 버튼(이름 : 옵션)을 추가하여 선택할 수 있게 변경. 버튼 사이간격에 대해 템플릿화하여 적용. +6. 구조물 조합의 면적산출 우측패널에 별도의 도면 패널 추가(기능은 4번 요청 확인) From 605cc090ac691a8cb46880e1f2818db257093d41 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 5 Oct 2026 13:38:13 +0900 Subject: [PATCH 3/3] =?UTF-8?q?feat(M02):=2047-1=20=C2=B7=2047-2=20?= =?UTF-8?q?=EA=B5=AC=EC=A1=B0=EB=AC=BC=20=EC=A1=B0=ED=95=A9=20=EC=B9=B4?= =?UTF-8?q?=EB=93=9C=20[=3F]=20=EC=A0=9C=EB=AA=A9=20=EC=98=86=20=C2=B7=20?= =?UTF-8?q?=EB=8F=84=EC=9B=80=EB=A7=90=20=EB=84=A4=20=EC=A0=88=20=C2=B7=20?= =?UTF-8?q?=EB=AF=B8=EB=A6=AC=EB=B3=B4=EA=B8=B0=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EB=B6=88=EB=9F=AC=EC=98=A4=EA=B8=B0=20=EC=97=86=EC=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - [?] = 카드 제목(도면 정보 · 일위대가 · 면적산출) 바로 옆 · 도움말 = 설명 · 단축키 · 기능 · 사용법 - 도움말 글 한 곳(M02_MasterTemplete_CardHelp.ts) · 일위대가 카드에도 [?] - 도면 카드 미리보기 📂 없음(noImport) · 도면 상세는 그대로 - 시험 · 화면 시나리오(ui_checks/47-1_카드_도움말.js) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016PSXsvyDxjNmufteN5bqmt --- .../M02_MasterTemplete_CardHelp.ts | 97 +++++++++++++++++++ .../M02_MasterTemplete_CardShell.ts | 62 +++++++++--- .../M02_MasterTemplete_Drawing.ts | 7 +- .../M02_MasterTemplete_DrawingCard.ts | 3 +- .../M02_MasterTemplete_ItemCards.ts | 32 ++---- .../M02_MasterTemplete_Structure.css | 22 ++++- resources/tester/test_m02_card_shell.py | 33 ++++++- .../tester/ui_checks/47-1_카드_도움말.js | 68 +++++++++++++ 8 files changed, 275 insertions(+), 49 deletions(-) create mode 100644 M02_MasterTemplete/M02_MasterTemplete_CardHelp.ts create mode 100644 resources/tester/ui_checks/47-1_카드_도움말.js diff --git a/M02_MasterTemplete/M02_MasterTemplete_CardHelp.ts b/M02_MasterTemplete/M02_MasterTemplete_CardHelp.ts new file mode 100644 index 000000000..5cb0d4159 --- /dev/null +++ b/M02_MasterTemplete/M02_MasterTemplete_CardHelp.ts @@ -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", + ], + }, +}; diff --git a/M02_MasterTemplete/M02_MasterTemplete_CardShell.ts b/M02_MasterTemplete/M02_MasterTemplete_CardShell.ts index 383533d94..fec69381a 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_CardShell.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_CardShell.ts @@ -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; } diff --git a/M02_MasterTemplete/M02_MasterTemplete_Drawing.ts b/M02_MasterTemplete/M02_MasterTemplete_Drawing.ts index 8aa61bc40..5623e0ffe 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Drawing.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Drawing.ts @@ -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); diff --git a/M02_MasterTemplete/M02_MasterTemplete_DrawingCard.ts b/M02_MasterTemplete/M02_MasterTemplete_DrawingCard.ts index af4077f26..1f310bd07 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_DrawingCard.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_DrawingCard.ts @@ -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), diff --git a/M02_MasterTemplete/M02_MasterTemplete_ItemCards.ts b/M02_MasterTemplete/M02_MasterTemplete_ItemCards.ts index 8f07e44ca..a0e9eb039 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_ItemCards.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_ItemCards.ts @@ -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; diff --git a/M02_MasterTemplete/M02_MasterTemplete_Structure.css b/M02_MasterTemplete/M02_MasterTemplete_Structure.css index 2284bcbfe..de09c0339 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Structure.css +++ b/M02_MasterTemplete/M02_MasterTemplete_Structure.css @@ -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). */ diff --git a/resources/tester/test_m02_card_shell.py b/resources/tester/test_m02_card_shell.py index aada156a2..0d6ff6c9c 100644 --- a/resources/tester/test_m02_card_shell.py +++ b/resources/tester/test_m02_card_shell.py @@ -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") diff --git a/resources/tester/ui_checks/47-1_카드_도움말.js b/resources/tester/ui_checks/47-1_카드_도움말.js new file mode 100644 index 000000000..1e083b4d9 --- /dev/null +++ b/resources/tester/ui_checks/47-1_카드_도움말.js @@ -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); +})();