From 835f9d92904be2f9f24736d8b8959026f65c0f68 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 26 Oct 2026 22:29:22 +0900 Subject: [PATCH] =?UTF-8?q?refactor(B07):=20=EC=83=81=EC=84=B8=20=EC=84=A4?= =?UTF-8?q?=EA=B3=84=20=ED=99=94=EB=A9=B4=20=ED=8E=98=EC=9D=B4=EC=A7=80=20?= =?UTF-8?q?=EC=9E=90=EC=B2=B4=20=EB=B3=80=EC=A2=85=EC=9D=84=20=EB=B6=80?= =?UTF-8?q?=ED=92=88=20=EC=83=81=EC=9E=90=20=EB=B6=80=ED=92=88=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=E2=80=94=20=EB=8F=84=EB=A9=B4=20=EB=8B=A8=EC=B6=94?= =?UTF-8?q?=20=C2=B7=20=EC=9E=A5=20=EB=B2=88=ED=98=B8=20=C2=B7=20=EB=8F=84?= =?UTF-8?q?=EA=B5=AC=20=EB=8B=A8=EC=B6=94=20=C2=B7=20=EC=B9=B4=EB=93=9C=20?= =?UTF-8?q?=C2=B7=20=EB=9D=A0=20=EC=95=8C=EB=A6=BC=20=C2=B7=20=EA=B0=92=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20(PLAN=20553-1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 도면 단추(날 ui-panel-btn) · 장 번호 · 도구 단추 = 단추 부품 · ⚙ = 아이콘 단추 꼴 · 평면도 줄 = 목록 줄 꼴 - 머리 = 패널 머리 + 배지 · 확정 건너뜀 = 띠 알림 · 빈 글 = 빈 상태 · 측점 정보 = 값 목록 · ⚙ 모달 그룹 상자 = 카드 · 종류 이름 칸 = 입력 - window.confirm 둘 → 확인창 부품 · 죽은 CSS 걷음 · 시험 헬퍼 부품 가짜 한 벌(helper_b07_part_stubs) · 153-1 옛 오류(removeAttribute) 고침 · 가이드 선택자 · 지킴 시험 Co-Authored-By: Claude Sonnet 5.5 --- .../B07_DesignDetail_UI_GuideToggle.ts | 2 +- B07_DesignDetail/B07_DesignDetail_UI_Page.ts | 18 +- .../B07_DesignDetail_UI_Panels.ts | 134 +++++++------- .../B07_DesignDetail_UI_PlanKinds.ts | 47 +++-- .../B07_DesignDetail_UI_PlanManage.ts | 30 ++-- .../B07_DesignDetail_UI_Settings.ts | 21 ++- .../B07_DesignDetail_UI_Settings_Scale.ts | 30 ++-- .../B07_DesignDetail_UI_Style.css | 163 +++--------------- .../tester/helper_130_b07_plan_manage.cjs | 8 +- .../tester/helper_153_b07_cross_layout.cjs | 8 +- .../tester/helper_280_5_b07_settings.cjs | 15 +- resources/tester/helper_311_panel_folded.cjs | 7 +- resources/tester/helper_b07_part_stubs.cjs | 60 +++++++ resources/tester/test_130_b07_plan_manage.py | 2 +- .../tester/test_180_3_b07_guide_toggle.py | 4 +- resources/tester/test_553_1_pages.py | 42 +++++ 16 files changed, 312 insertions(+), 279 deletions(-) create mode 100644 resources/tester/helper_b07_part_stubs.cjs diff --git a/B07_DesignDetail/B07_DesignDetail_UI_GuideToggle.ts b/B07_DesignDetail/B07_DesignDetail_UI_GuideToggle.ts index 7fddc1c0c..e81f616cd 100644 --- a/B07_DesignDetail/B07_DesignDetail_UI_GuideToggle.ts +++ b/B07_DesignDetail/B07_DesignDetail_UI_GuideToggle.ts @@ -24,7 +24,7 @@ export function buildGuideToggle(apply: (visible: boolean) => void): HTMLButtonE const button = createButton({ label: labelOf(shown), variant: "ghost", - size: "sm", + size: 28, // ⚙ 설정 단추 줄 한 규격 onClick: () => { shown = !shown; paint(); diff --git a/B07_DesignDetail/B07_DesignDetail_UI_Page.ts b/B07_DesignDetail/B07_DesignDetail_UI_Page.ts index f131d079e..c87fcb543 100644 --- a/B07_DesignDetail/B07_DesignDetail_UI_Page.ts +++ b/B07_DesignDetail/B07_DesignDetail_UI_Page.ts @@ -18,6 +18,7 @@ import { showLoadingOverlay, showToast, } from "@ui/ui_template_elements"; +import { askConfirm } from "@ui/ui_template_modal"; import { createWorkflowLayout } from "@ui/ui_template_workflow_layout"; import { CURRENT_PROJECT_ID_KEY } from "@config/config_frontend"; // 좌측 패널(도면 목록·설계 정보)은 파일이 700줄을 넘어 떼어냈다(2026-09-04). @@ -175,7 +176,7 @@ export async function renderB07DesignDetail(root: HTMLElement): Promise { /** 장 배치 저장(140-3 · ⚙ 횡단 모달) — 서버 쓰기 → 화면 다시 세움 · 같은 장 번호를 다시 엶. */ const saveCrossLayoutAndRebuild = async (next: CrossLayout, sheet: number) => { - if (!projectId || !mayDiscardEdits()) return; + if (!projectId || !(await mayDiscardEdits())) return; showLoadingOverlay(); try { await saveCrossLayout(projectId, next); @@ -218,13 +219,16 @@ export async function renderB07DesignDetail(root: HTMLElement): Promise { }); /** 저장하지 않은 편집이 있으면 묻는다. 버리기로 해야 이동한다. */ - const mayDiscardEdits = (): boolean => { + const mayDiscardEdits = async (): Promise => { if (!cadDirty && !tabs.anyDirty()) return true; - return window.confirm( - "저장하지 않은 편집이 있습니다.\n" + + return askConfirm({ + message: + "저장하지 않은 편집이 있습니다.\n" + "지금 도면을 바꾸면 편집이 사라집니다. 버리고 이동할까요?\n\n" + "남기려면 [취소] — 고친 도면은 페이지를 옮길 때([<] [>]) 저장됩니다.", - ); + okLabel: "버리고 이동", + danger: true, + }); }; /** @@ -389,13 +393,13 @@ export async function renderB07DesignDetail(root: HTMLElement): Promise { }); /** [평면도 관리] 모달을 닫음 — 고쳤으면 목록 · 도면을 서버에서 다시 받아 화면을 다시 세운다. */ - const onPlanClosed = (changed: boolean, openId?: string): void => { + const onPlanClosed = async (changed: boolean, openId?: string): Promise => { if (!changed) { const target = drawings.find((item) => item.id === openId); if (target) selectDrawing(target); return; } - if (!mayDiscardEdits()) return; + if (!(await mayDiscardEdits())) return; reopenDrawingId = openId ?? currentDrawing?.id; cad.destroy(); void renderB07DesignDetail(root); diff --git a/B07_DesignDetail/B07_DesignDetail_UI_Panels.ts b/B07_DesignDetail/B07_DesignDetail_UI_Panels.ts index e4c148abc..969f1d29b 100644 --- a/B07_DesignDetail/B07_DesignDetail_UI_Panels.ts +++ b/B07_DesignDetail/B07_DesignDetail_UI_Panels.ts @@ -6,8 +6,14 @@ * DOM 만 만들고 상태는 갖지 않는다 — 고르기 동작은 인자로 받은 콜백에 넘긴다. * ========================================================================== */ +import { createBadge } from "@ui/ui_template_badge"; +import { createBanner } from "@ui/ui_template_banner"; import { attachCollapsible, createPanelCard } from "@ui/ui_template_collapsible"; +import { createButton } from "@ui/ui_template_elements"; +import { createEmpty } from "@ui/ui_template_empty"; +import { createKv } from "@ui/ui_template_kv"; import { currentLanguageIndex, ui_locales } from "@ui/ui_template_locale"; +import { createPanelHead } from "@ui/ui_template_panel_head"; import type { CrossDesignInfo, DesignDrawingItem } from "./B07_DesignDetail_Api_Fetch"; import { buildDevelopmentSection } from "./B07_DesignDetail_UI_Development"; import { buildPlanSection } from "./B07_DesignDetail_UI_PlanKinds"; @@ -66,41 +72,38 @@ export function buildDrawingSidePanel( const isFolded = (kind: string): boolean => folded.get(kind) ?? true; // 389-3 모든 묶음 기본 닫힘 const panel = document.createElement("div"); panel.className = "b07-drawing-list"; - const heading = document.createElement("div"); - heading.className = "b07-drawing-list__heading"; - const title = document.createElement("strong"); - title.textContent = "설계 도면"; - const count = document.createElement("span"); - count.textContent = `${drawings.length}건`; - heading.append(title, count); + const heading = createPanelHead({ + title: "설계 도면", + actions: [createBadge(`${drawings.length}건`, "neutral")], + divider: true, + }).root; + heading.classList.add("b07-drawing-list__heading"); panel.append(heading); // 확정을 건너뛴 상태라는 것을 **조용히 두지 않는다** — 그냥 열면 다음 사람이 // 「왜 값이 없나」로 헤맨다(2026-09-09). 되돌리기는 B08 좌측 [확정 없이 다음으로] 옆. if (devBypass) { - const notice = document.createElement("p"); - notice.className = "b07-drawing-list__bypass"; - notice.textContent = "확정을 건너뛴 상태입니다 — 개발 전용. 값이 비어 보일 수 있습니다."; - panel.append(notice); + panel.append( + createBanner({ + tone: "warning", + text: "확정을 건너뛴 상태입니다 — 개발 전용. 값이 비어 보일 수 있습니다.", + }), + ); } if (errorMessage || drawings.length === 0) { - const empty = document.createElement("p"); - empty.className = "b07-drawing-list__empty"; - empty.textContent = errorMessage ?? "확정된 종·횡단 도면이 없습니다."; + const empty = createEmpty({ + text: errorMessage ?? "확정된 종·횡단 도면이 없습니다.", + small: true, + }); + empty.classList.add("b07-drawing-list__empty"); panel.append(empty); return panel; } const drawingButton = (drawing: DesignDrawingItem, label: string): HTMLButtonElement => { - const button = document.createElement("button"); - button.type = "button"; - button.className = "b07-drawing-button ui-panel-btn"; + const button = listButton(label); button.dataset.drawingId = drawing.id; button.dataset.confirmed = String(drawing.confirmed); - const name = document.createElement("span"); - name.className = "b07-drawing-button__name"; - name.textContent = label; - button.append(name); button.addEventListener("click", () => onSelect(drawing)); return button; }; @@ -140,16 +143,10 @@ export function buildDrawingSidePanel( const [drawing] = items; // 서버가 빈 도각을 내주지 못한 경우에만 회색 버튼으로 남는다. if (!drawing) { - const placeholder = document.createElement("button"); - placeholder.type = "button"; - placeholder.className = "b07-drawing-button ui-panel-btn"; + const placeholder = listButton(group.label); placeholder.disabled = true; placeholder.title = "준비 중"; placeholder.dataset.pending = "true"; - const name = document.createElement("span"); - name.className = "b07-drawing-button__name"; - name.textContent = group.label; - placeholder.append(name); panel.append(placeholder); continue; } @@ -183,9 +180,11 @@ export function buildDrawingSidePanel( section.append(button); } if (structure && items.length === 0) { - const empty = document.createElement("p"); - empty.className = "b07-drawing-list__empty"; - empty.textContent = "B05 · B06 구조물 배치에 연결된 구조물 도면이 없습니다."; + const empty = createEmpty({ + text: "B05 · B06 구조물 배치에 연결된 구조물 도면이 없습니다.", + small: true, + }); + empty.classList.add("b07-drawing-list__empty"); section.append(empty); } panel.append(section); @@ -194,10 +193,20 @@ export function buildDrawingSidePanel( return panel; } +/** 도면 단추 한 개 — 단추 부품(고스트 32) · 이름 칸 이름만 B07 자리 이름을 더함. */ +function listButton(label: string): HTMLButtonElement { + const button = createButton({ label, variant: "ghost", size: 32 }); + button.classList.add("b07-drawing-button"); + button.children[button.children.length - 1]?.classList.add("b07-drawing-button__name"); + return button; +} + /** 한 장 단추 안 오른쪽 ⚙(280-5) — 단추 안 단추 금지라 span · 누름 · Enter 가 도면 열기로 안 올라감. */ function buttonGear(onSettings: () => void): HTMLElement { const gear = document.createElement("span"); - gear.className = "b07-drawing-button__settings"; + // 아이콘 단추 부품 꼴을 입힘 — 단추 안 단추는 못 둬서 span 으로 둠 + gear.className = "b07-drawing-button__settings ui-icon-btn ui-icon-btn--ghost"; + gear.dataset.size = "20"; gear.setAttribute("role", "button"); gear.tabIndex = 0; gear.textContent = "⚙"; @@ -244,19 +253,6 @@ function ditchLabel(design: CrossDesignInfo): string { return `${ditch.top_width_m.toFixed(2)}×${ditch.depth_m.toFixed(2)}m`; } -function infoRow(label: string, value: string): HTMLElement { - const row = document.createElement("div"); - row.className = "b07-info__row"; - const key = document.createElement("span"); - key.className = "b07-info__key"; - key.textContent = label; - const val = document.createElement("span"); - val.className = "b07-info__val"; - val.textContent = value; - row.append(key, val); - return row; -} - /** * 선택 횡단도의 지반정보/계획정보를 2분할로 렌더한다 (B06 정본 값 · 확정 배지 없음 · 요구 343). * @@ -271,19 +267,16 @@ export function buildDesignInfoPanel( ): HTMLElement { const panel = document.createElement("div"); panel.className = "b07-info"; - const heading = document.createElement("div"); - heading.className = "b07-info__heading"; - const stationName = document.createElement("strong"); const scopeLabel = scope === "sheet" ? L("B07_Info_Sheet") : L("B07_Info_Station"); - stationName.textContent = `${scopeLabel} ${title}`; - heading.append(stationName); - panel.append(heading); + panel.append(createPanelHead({ title: `${scopeLabel} ${title}`, level: 4 }).root); if (!design) { - const empty = document.createElement("p"); - empty.className = "b07-info__empty"; - empty.textContent = scope === "sheet" ? L("B07_Info_SheetHint") : L("B07_Info_NoDesign"); - panel.append(empty); + panel.append( + createEmpty({ + text: scope === "sheet" ? L("B07_Info_SheetHint") : L("B07_Info_NoDesign"), + small: true, + }), + ); return panel; } @@ -293,12 +286,15 @@ export function buildDesignInfoPanel( groundTitle.textContent = L("B07_Info_Ground_Title"); ground.append( groundTitle, - infoRow(L("B07_Info_GroundType"), L(GROUND_TYPE_LABEL[design.ground_type])), - infoRow(L("B07_Info_CutSide"), cutSideLabel(design.section_mode)), - infoRow( - L("B07_Info_DitchSide"), - design.ditch_side === "left" ? L("B06_Design_Ditch_Left") : L("B06_Design_Ditch_Right"), - ), + createKv([ + { label: L("B07_Info_GroundType"), value: L(GROUND_TYPE_LABEL[design.ground_type]) }, + { label: L("B07_Info_CutSide"), value: cutSideLabel(design.section_mode) }, + { + label: L("B07_Info_DitchSide"), + value: + design.ditch_side === "left" ? L("B06_Design_Ditch_Left") : L("B06_Design_Ditch_Right"), + }, + ]), ); const plan = document.createElement("section"); @@ -307,13 +303,15 @@ export function buildDesignInfoPanel( planTitle.textContent = L("B07_Info_Plan_Title"); plan.append( planTitle, - infoRow(L("B07_Info_DesignElevation"), `${design.design_elevation_m.toFixed(2)}m`), - infoRow(L("B07_Info_CutSlope"), `1:${design.cut_slope_ratio}`), - infoRow(L("B07_Info_FillSlope"), `1:${design.fill_slope_ratio}`), - infoRow(L("B07_Info_RoadWidth"), `${design.roadbed_width_m.toFixed(2)}m`), - infoRow(L("B07_Info_Ditch"), ditchLabel(design)), - infoRow(L("B07_Info_CutArea"), `${design.cut_area_m2.toFixed(2)}㎡`), - infoRow(L("B07_Info_FillArea"), `${design.fill_area_m2.toFixed(2)}㎡`), + createKv([ + { label: L("B07_Info_DesignElevation"), value: `${design.design_elevation_m.toFixed(2)}m` }, + { label: L("B07_Info_CutSlope"), value: `1:${design.cut_slope_ratio}` }, + { label: L("B07_Info_FillSlope"), value: `1:${design.fill_slope_ratio}` }, + { label: L("B07_Info_RoadWidth"), value: `${design.roadbed_width_m.toFixed(2)}m` }, + { label: L("B07_Info_Ditch"), value: ditchLabel(design) }, + { label: L("B07_Info_CutArea"), value: `${design.cut_area_m2.toFixed(2)}㎡` }, + { label: L("B07_Info_FillArea"), value: `${design.fill_area_m2.toFixed(2)}㎡` }, + ]), ); panel.append(ground, plan); diff --git a/B07_DesignDetail/B07_DesignDetail_UI_PlanKinds.ts b/B07_DesignDetail/B07_DesignDetail_UI_PlanKinds.ts index 5c25cae18..d64cdeef1 100644 --- a/B07_DesignDetail/B07_DesignDetail_UI_PlanKinds.ts +++ b/B07_DesignDetail/B07_DesignDetail_UI_PlanKinds.ts @@ -10,7 +10,13 @@ import { createCheckbox } from "@ui/ui_template_checkbox"; import { createPanelCard } from "@ui/ui_template_collapsible"; -import { createSelectField, showToast } from "@ui/ui_template_elements"; +import { + createButton, + createInputField, + createSelectField, + showToast, +} from "@ui/ui_template_elements"; +import { askConfirm } from "@ui/ui_template_modal"; import { addPlanKind, deletePlanKind, @@ -37,6 +43,19 @@ function el( return node; } +/** 도구 단추 — 단추 부품(고스트 24) · 자리 이름 `b07-plan-tools__button` 을 더함. */ +export function toolButton(label: string, onClick?: () => void): HTMLButtonElement { + const button = createButton({ label, variant: "ghost", size: 24, onClick }); + button.classList.add("b07-plan-tools__button"); + return button; +} + +/** 단추 글자만 바꿈(단추 부품의 이름 칸은 마지막 자식) */ +export function setButtonText(button: HTMLElement, text: string): void { + const label = button.children[button.children.length - 1]; + if (label) label.textContent = text; +} + /** 실패는 토스트로 알리고 삼킨다 — 성공했을 때만 화면을 다시 세운다. */ export async function run( action: () => Promise, @@ -120,10 +139,10 @@ export function kindForm( compact: true, ariaLabel: "등고선 간격", }); - const name = el("input", "b07-plan-tools__name"); - name.type = "text"; - name.placeholder = "종류 이름"; + const nameField = createInputField({ type: "text", placeholder: "종류 이름", compact: true }); + const name = nameField.input; name.maxLength = 40; + name.classList.add("b07-plan-tools__name"); const boxes = state.pieces.map((piece) => createCheckbox(piece.name, { value: piece.id })); const pieceList = el("div", "b07-plan-tools__pieces"); const grouped = PIECE_GROUPS.map((ids) => boxes.filter(({ input }) => ids.includes(input.value))); @@ -136,10 +155,8 @@ export function kindForm( if (index) pieceList.append(el("hr", "b07-plan-tools__divider")); pieceList.append(...group.map((box) => box.root)); }); - const action = el("button", "b07-plan-tools__button"); - action.type = "button"; - const remove = el("button", "b07-plan-tools__button", "지우기"); - remove.type = "button"; + const action = toolButton(""); + const remove = toolButton("지우기"); // 유역도 양식은 그림이 정해져 있어 얹을 정보를 고르지 않는다. const showPieces = (): void => { const watershed = layout.select.value === "watershed"; @@ -169,7 +186,7 @@ export function kindForm( layout.select.value = kind?.layout ?? "plan"; layout.select.disabled = Boolean(kind); layout.root.hidden = kind?.layout === "plan"; // 평면도는 늘 같은 값 — 새 종류 · 유역도만 보임 - action.textContent = kind ? "업데이트" : "추가"; + setButtonText(action, kind ? "업데이트" : "추가"); remove.hidden = !kind; showPieces(); }; @@ -178,9 +195,13 @@ export function kindForm( remove.addEventListener("click", () => { const kind = state.kinds.find((item) => item.id === pick.select.value); if (!kind) return; - if (!window.confirm(`「${kind.name}」 종류를 지웁니다. 다시 쓰려면 「새 종류」로 추가합니다.`)) - return; - void run(() => deletePlanKind(controls.projectId, kind.id), controls); + void askConfirm({ + message: `「${kind.name}」 종류를 지웁니다. 다시 쓰려면 「새 종류」로 추가합니다.`, + okLabel: "지우기", + danger: true, + }).then((ok) => { + if (ok) void run(() => deletePlanKind(controls.projectId, kind.id), controls); + }); }); action.addEventListener("click", () => { const pieces = boxes.filter((box) => box.input.checked).map((box) => box.input.value); @@ -201,7 +222,7 @@ export function kindForm( actions.append(action, remove); const fields = el("div", "b07-plan-tools__fields"); fields.append(pick.root, layout.root, scale.root, contour.root); - form.append(fields, name, pieceList, actions); + form.append(fields, nameField.root, pieceList, actions); return { root: form, pick: (kindId) => { diff --git a/B07_DesignDetail/B07_DesignDetail_UI_PlanManage.ts b/B07_DesignDetail/B07_DesignDetail_UI_PlanManage.ts index a85a96acc..1045c8a02 100644 --- a/B07_DesignDetail/B07_DesignDetail_UI_PlanManage.ts +++ b/B07_DesignDetail/B07_DesignDetail_UI_PlanManage.ts @@ -10,7 +10,7 @@ * 처음 열 때 = 페이지 목록 · 미리받은 장(`Prefetch.openDrawing`) 그대로 · 같은 목록으로 다시 열면 요청 없음(522-3). * ========================================================================== */ -import { showToast } from "@ui/ui_template_elements"; +import { createButton, showToast } from "@ui/ui_template_elements"; import { openModal } from "@ui/ui_template_modal"; import { createCadHost } from "@ui/cad_host/cad_host"; import { @@ -27,6 +27,7 @@ import { kindForm, run, type KindForm, + toolButton, type PlanKindsControls, } from "./B07_DesignDetail_UI_PlanKinds"; @@ -56,10 +57,10 @@ function el( return node; } -function button(className: string, text: string, onClick: () => void): HTMLButtonElement { - const node = el("button", className, text); - node.type = "button"; - node.addEventListener("click", onClick); +/** 장 번호 · 이름 단추 — 단추 부품(고스트 22) · 자리 이름을 더함. */ +function sheetButton(className: string, text: string, onClick: () => void): HTMLButtonElement { + const node = createButton({ label: text, variant: "ghost", size: 22, onClick }); + node.classList.add(...className.split(" ")); return node; } @@ -105,16 +106,18 @@ export function openPlanManager(controls: PlanManageControls): { close: () => vo const ids = state.kinds.map((kind) => kind.id); let dragId: string | null = null; for (const kind of state.kinds) { - const row = el("div", "b07-plan-kind__head b07-plan-manage__row"); + // 목록 줄 부품 꼴(28) · 끌기 손잡이 — 줄 끌어 놓기는 아래 이벤트가 맡음 + const row = el("div", "b07-plan-kind__head b07-plan-manage__row ui-list-row ui-sidebar-row"); + row.dataset.size = "28"; row.dataset.planKind = kind.id; row.draggable = true; - const grip = el("span", "b07-plan-manage__grip", "≡"); + const grip = el("span", "b07-plan-manage__grip ui-list-row__grip"); grip.title = "끌어 놓아 종류 순서 바꾸기"; const group = kindSheets(kind.id); // 한 장이면 이름이 곧 장 단추 · 여러 장이면 이름 + 작은 장 단추 const name = group.length === 1 - ? button("b07-plan-manage__sheet b07-plan-kind__name", kind.name, () => + ? sheetButton("b07-plan-manage__sheet b07-plan-kind__name", kind.name, () => pickSheet(group[0].drawing, group[0].label), ) : el("span", "b07-plan-kind__name", kind.name); @@ -125,7 +128,7 @@ export function openPlanManager(controls: PlanManageControls): { close: () => vo row.append(grip, name); if (group.length > 1) group.forEach(({ drawing, label }, index) => { - const item = button("b07-plan-manage__sheet", String(index + 1), () => + const item = sheetButton("b07-plan-manage__sheet", String(index + 1), () => pickSheet(drawing, label), ); item.title = label; @@ -166,9 +169,10 @@ export function openPlanManager(controls: PlanManageControls): { close: () => vo side .querySelectorAll(".b07-plan-manage__sheet") .forEach((item) => (item.dataset.active = String(item.dataset.drawingId === picked))); - side - .querySelectorAll(".b07-plan-manage__row") - .forEach((row) => (row.dataset.active = String(row.dataset.planKind === pickedKind))); + side.querySelectorAll(".b07-plan-manage__row").forEach((row) => { + row.dataset.active = String(row.dataset.planKind === pickedKind); + row.classList.toggle("is-active", row.dataset.planKind === pickedKind); + }); }; /** 장 하나를 미리보기에 — 보기 전용 · 확정 풀기는 메인 [수정](140-1). */ @@ -180,7 +184,7 @@ export function openPlanManager(controls: PlanManageControls): { close: () => vo try { const response = await openDrawing(drawing); if (closed || picked !== drawing.id) return; - head.append(button("b07-plan-tools__button", "CAD 에서 열기", () => finish(drawing.id))); + head.append(toolButton("CAD 에서 열기", () => finish(drawing.id))); preview.load( response.drawing, null, diff --git a/B07_DesignDetail/B07_DesignDetail_UI_Settings.ts b/B07_DesignDetail/B07_DesignDetail_UI_Settings.ts index da3261908..fe32340a9 100644 --- a/B07_DesignDetail/B07_DesignDetail_UI_Settings.ts +++ b/B07_DesignDetail/B07_DesignDetail_UI_Settings.ts @@ -15,7 +15,8 @@ import { openModal } from "@ui/ui_template_modal"; import type { CrossLayout, DesignDrawingItem } from "./B07_DesignDetail_Api_Fetch"; import { DRAWING_GROUPS, isCrossSheet } from "./B07_DesignDetail_UI_Panels"; import { openPlanManager, type PlanManageControls } from "./B07_DesignDetail_UI_PlanManage"; -import { mountDrawingScale, settingsGroup } from "./B07_DesignDetail_UI_Settings_Scale"; +import { createText } from "@ui/ui_template_text"; +import { groupBody, mountDrawingScale, settingsGroup } from "./B07_DesignDetail_UI_Settings_Scale"; type Kind = DesignDrawingItem["kind"]; @@ -43,9 +44,8 @@ const CROSS_SCALE_MIN = 0.5; const CROSS_SCALE_MAX = 2; const note = (text: string): HTMLElement => { - const line = document.createElement("p"); - line.className = "b07-settings__note"; - line.textContent = text; + const line = createText(text, "muted", 14); + line.classList.add("b07-settings__note"); return line; }; @@ -84,7 +84,7 @@ export function openDrawingSettings(kind: Kind, deps: DrawingSettingsDeps): void closeModal = close; if (mine) body.append(note(`지금 도면: ${current?.label}`)); if (kind === "structure") body.append(settingsGroup("축척", note("M02 그림 — 축척 없음"))); - const confirm = createButton({ label: "확인", onClick: () => void apply() }); + const confirm = createButton({ label: "확인", size: 28, onClick: () => void apply() }); const refresh = (): void => { confirm.disabled = scale?.read() === "invalid" || cross?.read() === "invalid"; }; @@ -100,13 +100,18 @@ export function openDrawingSettings(kind: Kind, deps: DrawingSettingsDeps): void if (nextLayout) deps.cross.save(nextLayout, cross!.sheet); else if (nextScale) deps.plan?.onClosed(true, deps.current()?.id); // 목록 · 도면 다시 받음 }; - const cancel = createButton({ label: "취소", variant: "ghost", onClick: close }); + const cancel = createButton({ label: "취소", variant: "ghost", size: 28, onClick: close }); cancel.classList.add("b07-settings__cancel"); const footer = document.createElement("div"); footer.className = "b07-settings__actions"; if (cross) footer.append(deps.cross.guideToggle); if (scale) { - const reset = createButton({ label: "기본값", variant: "ghost", onClick: scale.reset }); + const reset = createButton({ + label: "기본값", + variant: "ghost", + size: 28, + onClick: scale.reset, + }); reset.title = "축척 칸을 기본으로 채움 — [확인]으로 씀"; footer.append(reset); } @@ -178,7 +183,7 @@ function mountCross( table.input.value = String(got.table_scale ?? 1); for (const field of [cols, rows, table]) field.input.disabled = false; }) - .catch(() => layoutBox.append(note("배치를 받지 못했습니다."))); + .catch(() => groupBody(layoutBox).append(note("배치를 받지 못했습니다."))); return { sheet, diff --git a/B07_DesignDetail/B07_DesignDetail_UI_Settings_Scale.ts b/B07_DesignDetail/B07_DesignDetail_UI_Settings_Scale.ts index 1e5ef237a..e53363a3c 100644 --- a/B07_DesignDetail/B07_DesignDetail_UI_Settings_Scale.ts +++ b/B07_DesignDetail/B07_DesignDetail_UI_Settings_Scale.ts @@ -10,6 +10,7 @@ * ========================================================================== */ import { + createCard, createInputField, hideLoadingOverlay, showConfirmDialog, @@ -24,6 +25,7 @@ import { fetchDrawingScales, saveDrawingScales, } from "./B07_DesignDetail_Api_Fetch"; +import { createText } from "@ui/ui_template_text"; import type { DrawingSettingsDeps } from "./B07_DesignDetail_UI_Settings"; import { readScaleAxis, scaleToSend } from "./logic/B07_DesignDetail_Scale_Check"; @@ -38,16 +40,18 @@ const AXIS_LABEL: Record = { x: "X(가로) 1/", y: "Y(세로) 1/", const isScaleKind = (kind: string): kind is DrawingScaleKind => kind in LABEL; -/** 모달 안 그룹 상자 — 제목(legend) + 칸. */ -export function settingsGroup(legend: string, ...children: HTMLElement[]): HTMLFieldSetElement { - const box = document.createElement("fieldset"); - box.className = "b07-settings__group"; - const title = document.createElement("legend"); - title.textContent = legend; - box.append(title, ...children); +/** 모달 안 그룹 상자 — 카드 부품(제목 + 칸). 나중에 넣을 칸은 `groupBody(box)` 에. */ +export function settingsGroup(legend: string, ...children: HTMLElement[]): HTMLElement { + const box = createCard({ title: legend, body: children, pad: 12 }); + box.classList.add("b07-settings__group"); return box; } +/** 그룹 상자의 칸 자리 — 카드 부품의 마지막 자식 */ +export function groupBody(box: HTMLElement): HTMLElement { + return (box.children[box.children.length - 1] as HTMLElement | undefined) ?? box; +} + /** 축척 칸 한 벌 — 모달 [확인] · [기본값] 이 씀. */ export interface ScaleFields { /** null = 그대로(못 받음 포함) · "invalid" = 붉은 칸 있음 · 그 밖 = 보낼 값 */ @@ -66,9 +70,8 @@ export function mountDrawingScale( ): ScaleFields | undefined { if (!isScaleKind(kind) || !deps.plan) return undefined; const { projectId } = deps.plan; - const status = document.createElement("p"); - status.className = "b07-settings__note"; - status.textContent = "축척 받는 중…"; + const status = createText("축척 받는 중…", "muted", 14); + status.classList.add("b07-settings__note"); const section = settingsGroup( kind === "cross" ? "축척 — 횡단도 장(표준 횡단면도 뺌)" : `축척 — ${LABEL[kind]}`, status, @@ -137,9 +140,8 @@ function buildScaleFields( }); // 칸 밑 오류 글은 숨기고(틀린 칸만 길어짐) 줄 아래 한 줄로 알림 - const alertLine = document.createElement("p"); - alertLine.className = "b07-scale__error"; - alertLine.setAttribute("role", "alert"); + const alertLine = createText("", "error"); + alertLine.classList.add("b07-scale__error"); alertLine.hidden = true; const check = (): void => { const errors = new Map(); @@ -159,7 +161,7 @@ function buildScaleFields( const row = document.createElement("div"); row.className = "b07-scale__row"; row.append(...fields.map((f) => f.field.root)); - section.append(row, alertLine); + groupBody(section).append(row, alertLine); return { read: () => { const read = fields.map((f) => [f, f.read()] as const); diff --git a/B07_DesignDetail/B07_DesignDetail_UI_Style.css b/B07_DesignDetail/B07_DesignDetail_UI_Style.css index 367d118f0..db3eca1fa 100644 --- a/B07_DesignDetail/B07_DesignDetail_UI_Style.css +++ b/B07_DesignDetail/B07_DesignDetail_UI_Style.css @@ -26,20 +26,6 @@ overflow-y: auto; } -.b07-drawing-list__heading { - display: flex; - align-items: center; - justify-content: space-between; - padding-bottom: var(--spacing-12); - border-bottom: 1px solid var(--color-border); -} - -.b07-drawing-list__heading span, -.b07-drawing-list__empty { - color: var(--color-text-muted); - font-size: var(--text-body-sm); -} - /* 횡단도는 2열 배치 — 단, **칸이 좁아지면 1열로 접는다**(2026-09-08). 좌측 패널이 320 고정이던 동안은 2열이 늘 128.8px 라 문제가 없었으나, 공용 오버레이가 좁은 폭에서 패널을 280·227·182 로 물리면서 칸이 108.8 → 82.4 → 60px @@ -76,16 +62,12 @@ } .b07-drawing-button { - display: flex; - align-items: center; - justify-content: center; width: 100%; /* 격자 칸의 기본 최소폭은 글자 길이라, 이름이 길면 칸이 패널을 밀어내 단추가 오른쪽으로 튀어나갔다(2026-09-04 실측 14px). 0으로 풀면 이름이 `…`로 잘린다. */ min-width: 0; - /* 크기 · 테두리 · 바탕 · 글자 = 공용 `.ui-panel-btn`(269) · 여기선 확정 여부 좌측 색 띠(미확정: 투명)만 */ + /* 크기 · 테두리 · 바탕 · 글자 = 단추 부품(고스트 32) · 여기선 확정 여부 좌측 색 띠(미확정: 투명)만 */ border-left: 3px solid transparent; - text-align: center; } .b07-drawing-button__name { @@ -101,29 +83,19 @@ padding-left: 22px; } +/* ⚙ = 아이콘 단추 부품 꼴(span) · 자리만 단추 안 오른쪽 */ .b07-drawing-button__settings { position: absolute; top: 50%; - right: 6px; + right: 4px; transform: translateY(-50%); - color: var(--color-text-muted); - line-height: 1; cursor: pointer; } -.b07-drawing-button__settings:hover, -.b07-drawing-button__settings:focus-visible { - color: var(--color-primary); -} - -.b07-drawing-button:hover, .b07-drawing-button[data-active="true"] { border-color: var(--color-border); - background: var(--color-mist-violet); -} - -.b07-drawing-button[data-active="true"] { - color: var(--color-primary); + background: var(--color-accent-soft); + color: var(--color-accent); } /* 확정: 좌측 띠 + 측점 글자색을 함께 성공색으로 반영 */ @@ -174,13 +146,6 @@ gap: var(--spacing-8); } -.b07-info__heading { - display: flex; - align-items: center; - justify-content: space-between; - gap: var(--spacing-8); -} - .b07-info__block { display: flex; flex-direction: column; @@ -193,27 +158,6 @@ color: var(--color-text-muted); } -.b07-info__row { - display: flex; - justify-content: space-between; - gap: var(--spacing-8); - font-size: 0.76rem; -} - -.b07-info__key { - color: var(--color-text-muted); -} - -.b07-info__val { - color: var(--color-text); - font-variant-numeric: tabular-nums; -} - -.b07-info__empty { - font-size: 0.76rem; - color: var(--color-text-muted); -} - /* 횡단 장 배치(153-1 · 280-5 ⚙ 횡단 모달 · 523-1 그룹 상자) — 한 줄에 한 일: 「장 배치」 배치 열 · 행 / 적용 · 「표」 표 배율 · 한 줄 안 같은 너비. */ .b07-cross-layout { @@ -250,15 +194,6 @@ min-width: 0; } -/* 확정을 건너뛴 개발 상태 알림 — 눈에 띄되 도면 목록을 밀어내지 않게 한 줄만. */ -.b07-drawing-list__bypass { - margin: var(--spacing-4) 0 0; - padding: var(--spacing-4) var(--spacing-8); - border-left: 3px solid var(--color-warning, #d9a441); - color: var(--color-text-muted); - font-size: var(--text-caption); -} - /* 표준도 제원 입력 모양은 B08 로 이관(2026-09-13) — `B08_Quantity_UI_StructureSheet_Spec.ts` 가 스스로 넣음. */ /* 계획평면도 묶음 — 종류별 이름 줄 + 장 단추, 아래 [평면도 관리](PLAN 60 · 67 · 130장) */ @@ -322,17 +257,7 @@ } /* 「평면도 장」 한 블록(140-2) — 종류마다 한 줄 [≡] 이름 [1][2] · 줄 끌어 놓기 = 종류 순서 */ -.b07-plan-manage__row { - padding: 2px var(--spacing-4); - border: 1px solid transparent; - border-radius: var(--radius-buttons); -} - -.b07-plan-manage__row[data-active="true"] { - border-color: var(--color-border); - background: var(--color-mist-violet); -} - +/* 줄 = 목록 줄 부품(28) · 여기선 끌어 놓을 자리 선만 */ .b07-plan-manage__row[data-drop="before"] { box-shadow: inset 0 2px 0 var(--color-primary); } @@ -341,25 +266,13 @@ box-shadow: inset 0 -2px 0 var(--color-primary); } -.b07-plan-manage__grip { - color: var(--color-text-muted); - cursor: grab; +.b07-plan-manage__sheet.b07-plan-kind__name { + flex: 1 1 auto; + justify-content: flex-start; } .b07-plan-manage__sheet { - min-width: 26px; - padding: 2px var(--spacing-4); - border: 1px solid var(--color-border); border-left: 3px solid transparent; - border-radius: var(--radius-buttons); - background: var(--color-surface); - color: var(--color-text); - font-size: var(--text-caption); - cursor: pointer; -} - -.b07-plan-manage__sheet.b07-plan-kind__name { - text-align: left; } .b07-plan-manage__sheet[data-confirmed="true"] { @@ -367,8 +280,8 @@ } .b07-plan-manage__sheet[data-active="true"] { - color: var(--color-primary); - border-color: var(--color-primary); + color: var(--color-accent); + border-color: var(--color-accent); } .b07-plan-tools__block { @@ -409,15 +322,6 @@ min-width: 0; } -.b07-plan-tools__button { - padding: var(--spacing-4) var(--spacing-8); - border: 1px solid var(--color-border); - border-radius: var(--radius-buttons); - background: var(--color-surface); - color: var(--color-text); - cursor: pointer; -} - /* 목록에서 연 도면 탭 줄 + CAD(82-4) */ .b07-cad-stack { display: flex; @@ -469,30 +373,6 @@ html.is-bm-route .b07-cad-stack > .cad-tabs { white-space: nowrap; } -.b07-settings__note { - margin: 0; - color: var(--color-text-secondary); - font-size: 13px; -} - -.b07-settings__group { - display: flex; - flex-direction: column; - gap: var(--spacing-8); - min-width: 0; - margin: 0; - padding: var(--spacing-8) var(--spacing-12) var(--spacing-12); - border: 1px solid var(--color-border); - border-radius: var(--radius-lg); -} - -.b07-settings__group > legend { - padding: 0 var(--spacing-4); - color: var(--color-text-secondary); - font-size: 12px; - font-weight: 600; -} - .b07-settings__actions { display: flex; align-items: center; @@ -507,9 +387,6 @@ html.is-bm-route .b07-cad-stack > .cad-tabs { /* 단추 줄 한 규격 — 안내선 단추(좌측 패널 작은 꼴)도 같은 키 · 좁은 폭에 맞춘 옆 여백 */ .b07-settings__actions .ui-btn { - padding: var(--spacing-8) var(--spacing-12); - font-size: var(--text-body-sm); - border-radius: var(--radius-buttons); white-space: nowrap; } @@ -533,10 +410,8 @@ html.is-bm-route .b07-cad-stack > .cad-tabs { display: none; } -.b07-scale__error { - margin: 0; - color: var(--color-danger); - font-size: 12px; +.b07-scale__error[hidden] { + display: none; } /* 받지 못한 도면(508-3b) — 미리받기 · 열기가 두 번 다 실패 · 누르면 다시 받음 */ @@ -548,3 +423,15 @@ html.is-bm-route .b07-cad-stack > .cad-tabs { content: " · 다시 받기"; color: var(--color-danger); } + +/* 설정 모달 그룹 상자 = 카드 부품 · 제목은 옛 legend 크기로 작게(모달 h3 규칙이 키우지 않게) */ +.b07-settings__group > .ui-card__title { + margin-bottom: var(--spacing-8); + color: var(--color-text-secondary); + font-size: var(--text-caption); + font-weight: 600; +} + +.b07-settings__group > .ui-card__body { + gap: var(--spacing-8); +} diff --git a/resources/tester/helper_130_b07_plan_manage.cjs b/resources/tester/helper_130_b07_plan_manage.cjs index 2fddd1fbe..45c1bcbbd 100644 --- a/resources/tester/helper_130_b07_plan_manage.cjs +++ b/resources/tester/helper_130_b07_plan_manage.cjs @@ -6,6 +6,7 @@ const fs = require("fs"); const path = require("path"); const { makeLoader } = require("./helper_ts_stub_loader.cjs"); +const { partStubs } = require("./helper_b07_part_stubs.cjs"); const ROOT = path.join(__dirname, "..", ".."); const B07 = path.join(ROOT, "B07_DesignDetail"); @@ -24,6 +25,8 @@ class FakeNode { const self = this; this.classList = { add: (...names) => names.forEach((n) => self._classes.add(n)), + toggle: (name, force) => + (force ?? !self._classes.has(name)) ? self._classes.add(name) : self._classes.delete(name), contains: (name) => self._classes.has(name), }; } @@ -88,7 +91,7 @@ const byText = (node, tag, text) => globalThis.document = { createElement: (tag) => new FakeNode(tag) }; const confirms = []; -globalThis.window = { confirm: (message) => (confirms.push(message), true) }; +const parts = partStubs((tag) => new FakeNode(tag)); // ── 공용 부품 가짜 ───────────────────────────────────────────────────────────── const toasts = []; @@ -96,7 +99,9 @@ const modals = []; const cads = []; const selects = []; const STUBS = { + ...parts.modules, "@ui/ui_template_elements": { + ...parts.elements, showToast: (message, kind) => toasts.push([kind, message]), createSelectField: (opts) => { const root = new FakeNode("select"); @@ -120,6 +125,7 @@ const STUBS = { }, }, "@ui/ui_template_modal": { + askConfirm: async (opts) => (confirms.push(opts.message), true), openModal: (opts) => { const body = new FakeNode("div"); const modal = { opts, body, open: true }; diff --git a/resources/tester/helper_153_b07_cross_layout.cjs b/resources/tester/helper_153_b07_cross_layout.cjs index fa5829c05..3296e322f 100644 --- a/resources/tester/helper_153_b07_cross_layout.cjs +++ b/resources/tester/helper_153_b07_cross_layout.cjs @@ -6,6 +6,7 @@ const fs = require("fs"); const path = require("path"); const { makeLoader } = require("./helper_ts_stub_loader.cjs"); +const { partStubs } = require("./helper_b07_part_stubs.cjs"); const CATALOG = require("./helper_322_catalog_module.cjs"); // 구조물 표(322-1) const ROOT = path.join(__dirname, "..", ".."); @@ -46,6 +47,9 @@ class FakeNode { setAttribute(key, value) { this.attrs[key] = String(value); } + removeAttribute(key) { + delete this.attrs[key]; + } append(...kids) { kids.forEach((k) => this.children.push(k)); } @@ -64,6 +68,7 @@ const all = (node) => [node, ...node.children.flatMap(all)]; const byClass = (node, name) => all(node).filter((n) => n.classList.contains(name)); globalThis.document = { createElement: (tag) => new FakeNode(tag) }; +const parts = partStubs((tag) => new FakeNode(tag)); const toasts = []; const modals = []; @@ -83,7 +88,8 @@ const { loadTs } = makeLoader({ ...loadTs(path.join(UI, "ui_template_elements.ts")), showToast: (message, kind) => toasts.push([kind, message]), }; - if (request === "@ui/ui_template_modal") return { openModal }; + if (request === "@ui/ui_template_modal") return { openModal, askConfirm: async () => true }; + if (parts.modules[request]) return parts.modules[request]; if (request === "./B07_DesignDetail_UI_PlanManage") return {}; if (request === "./B07_DesignDetail_Api_Fetch") return {}; // 공용 카드(269) = 진짜 모듈 diff --git a/resources/tester/helper_280_5_b07_settings.cjs b/resources/tester/helper_280_5_b07_settings.cjs index eb47a7b6c..b245d0cbb 100644 --- a/resources/tester/helper_280_5_b07_settings.cjs +++ b/resources/tester/helper_280_5_b07_settings.cjs @@ -6,6 +6,7 @@ const fs = require("fs"); const path = require("path"); const { makeLoader } = require("./helper_ts_stub_loader.cjs"); +const { partStubs } = require("./helper_b07_part_stubs.cjs"); const CATALOG = require("./helper_322_catalog_module.cjs"); // 구조물 표(322-1) const ROOT = path.join(__dirname, "..", ".."); @@ -122,17 +123,11 @@ const SCALES = { }; let answer = true; -const button = (opts) => { - const node = new FakeNode("button"); - node.className = "ui-btn"; - node.textContent = opts.label; - node.disabled = !!opts.disabled; - if (opts.onClick) node.addEventListener("click", opts.onClick); - return node; -}; +const parts = partStubs((tag) => new FakeNode(tag)); const STUBS = { + ...parts.modules, "@ui/ui_template_elements": { - createButton: button, + ...parts.elements, createInputField: (opts) => { const root = new FakeNode("label"); const input = new FakeNode("input"); @@ -290,7 +285,7 @@ const drawings = [ let current = drawings[4]; // cross_s1 let redraws = 0; const saved = []; - const guideToggle = button({ label: "안내선 숨기기" }); + const guideToggle = parts.elements.createButton({ label: "안내선 숨기기" }); const closedCalls = []; const deps = { drawings, diff --git a/resources/tester/helper_311_panel_folded.cjs b/resources/tester/helper_311_panel_folded.cjs index bcb97353f..78bade099 100644 --- a/resources/tester/helper_311_panel_folded.cjs +++ b/resources/tester/helper_311_panel_folded.cjs @@ -5,6 +5,7 @@ const fs = require("fs"); const path = require("path"); const { makeLoader } = require("./helper_ts_stub_loader.cjs"); +const { partStubs } = require("./helper_b07_part_stubs.cjs"); const ROOT = path.join(__dirname, "..", ".."); @@ -115,14 +116,16 @@ const press = (root, section) => { }; const node = (tag = "div") => new FakeNode(tag); +const parts = partStubs((tag) => new FakeNode(tag)); const STUBS = { + ...parts.modules, + "@ui/ui_template_modal": { askConfirm: async () => true }, "@ui/ui_template_locale": { ui_locales: new Proxy({}, { get: (_, k) => [String(k)] }), currentLanguageIndex: 0, }, "@ui/ui_template_elements": { - createButton: ({ label }) => Object.assign(node("button"), { _text: label }), - createInputField: () => ({ root: node(), input: node("input") }), + ...parts.elements, createSelectField: () => ({ root: node(), select: { value: "" } }), showToast: () => undefined, }, diff --git a/resources/tester/helper_b07_part_stubs.cjs b/resources/tester/helper_b07_part_stubs.cjs new file mode 100644 index 000000000..fea4cc1dd --- /dev/null +++ b/resources/tester/helper_b07_part_stubs.cjs @@ -0,0 +1,60 @@ +/* B07 시험 헬퍼 공용 — 템플릿 부품(단추 · 카드 · 배지 · 띠 알림 · 빈 상태 · 값 목록 · 패널 머리 · 보조 글)의 + * 기록용 가짜. 화면 그릇(FakeNode)은 헬퍼 것을 받아 씀 — 진짜 부품이 내는 클래스 · 자식 차례만 같게 흉내. + * + * const { partStubs } = require("./helper_b07_part_stubs.cjs"); + * const parts = partStubs((tag) => new FakeNode(tag)); + * STUBS["@ui/ui_template_elements"] = { ...parts.elements, 나머지 }; + * STUBS = { ...STUBS, ...parts.modules }; + */ +function partStubs(makeNode) { + const node = (tag, className, text) => { + const made = makeNode(tag); + if (className) made.className = className; + if (text !== undefined) made.textContent = text; + if (!made.dataset) made.dataset = {}; + return made; + }; + /** 단추 부품 — `ui-btn ui-btn--<꼴>` · 마지막 자식 = 이름 칸(`ui-btn__label`) */ + const createButton = (opts) => { + const button = node("button", `ui-btn ui-btn--${opts.variant ?? "filled"}`); + if (opts.size !== undefined) button.dataset.size = String(opts.size); + button.append(node("span", "ui-btn__label", opts.label)); + button.disabled = !!opts.disabled; + if (opts.onClick) button.addEventListener("click", opts.onClick); + return button; + }; + const createInputField = (opts = {}) => { + const input = node("input", opts.compact ? "ui-input ui-input--sm" : "ui-input"); + input.value = opts.value ?? ""; + input.placeholder = opts.placeholder ?? ""; + const root = node("div", "ui-field"); + root.append(input); + return { root, input, setError: () => undefined }; + }; + const createCard = (opts) => { + const card = node("div", "ui-card"); + if (opts.title) card.append(node("h3", "ui-card__title", opts.title)); + const body = node("div", "ui-card__body"); + body.append(...(opts.body ?? [])); + card.append(body); + return card; + }; + return { + node, + elements: { createButton, createInputField, createCard }, + modules: { + "@ui/ui_template_badge": { createBadge: (text) => node("span", "ui-badge", text) }, + "@ui/ui_template_banner": { createBanner: (opts) => node("div", "ui-banner", opts.text) }, + "@ui/ui_template_empty": { createEmpty: (opts) => node("div", "ui-empty", opts.text) }, + "@ui/ui_template_kv": { createKv: () => node("dl", "ui-kv") }, + "@ui/ui_template_panel_head": { + createPanelHead: (opts) => ({ root: node("header", "ui-panel-head", opts.title) }), + }, + "@ui/ui_template_text": { + createText: (text, kind = "muted") => node("p", `ui-text ui-text--${kind}`, text), + }, + }, + }; +} + +module.exports = { partStubs }; diff --git a/resources/tester/test_130_b07_plan_manage.py b/resources/tester/test_130_b07_plan_manage.py index 1ee09ce19..247e35763 100644 --- a/resources/tester/test_130_b07_plan_manage.py +++ b/resources/tester/test_130_b07_plan_manage.py @@ -87,7 +87,7 @@ def test_종류_추가(out): def test_종류_업데이트(out): assert out["updateLabel"] == "업데이트" assert out["update"]["call"] == ["update", "k_new", "sub6임시2", [], 1000] - assert out["update"]["rowText"] == "≡sub6임시2" # 한 장이면 이름이 곧 장 단추 + assert out["update"]["rowText"] == "sub6임시2" # 한 장이면 이름이 곧 장 단추 · 손잡이는 목록 줄 부품의 점(글 없음) def test_종류_지우기(out): diff --git a/resources/tester/test_180_3_b07_guide_toggle.py b/resources/tester/test_180_3_b07_guide_toggle.py index 1dfdf6fa5..6cf529d98 100644 --- a/resources/tester/test_180_3_b07_guide_toggle.py +++ b/resources/tester/test_180_3_b07_guide_toggle.py @@ -1,7 +1,7 @@ """180-3 B07 횡단 장 안내선 보이기 / 숨기기 단추. - 끄는 것 = 점선 작도 영역 + 붉은 넘침 틀(화면 전용) · 인쇄 칸선 · 도면 개체는 그대로 -- 단추 = 배치 칸 줄 · ghost sm · 글 「안내선 숨기기」 ↔ 「안내선 보이기」 · 상태는 모듈 값(저장 안 함) +- 단추 = 설정 단추 줄 · ghost 28 · 글 「안내선 숨기기」 ↔ 「안내선 보이기」 · 상태는 모듈 값(저장 안 함) - 숨김이면 넘침 토스트도 안 띄움 · 메시지 한 줄(`aislo:b08:guide-visible`) · cad_host `setGuideVisible` """ @@ -32,7 +32,7 @@ def test_단추_글_누름_상태_이어짐(): out = json.loads(proc.stdout) assert out["first"] == { "text": "안내선 숨기기", - "cls": "ui-btn ui-btn--ghost ui-btn--sm", + "cls": "ui-btn ui-btn--ghost", "pressed": "false", "shown": True, } diff --git a/resources/tester/test_553_1_pages.py b/resources/tester/test_553_1_pages.py index a554c12f2..86e87108e 100644 --- a/resources/tester/test_553_1_pages.py +++ b/resources/tester/test_553_1_pages.py @@ -120,3 +120,45 @@ def test_b04_css_common_values_only(): assert not OLD_ALIAS.search(css), f"{name}: 옛 보라 별칭" body = re.sub(r"/\*.*?\*/", "", css, flags=re.S) assert not RAW_COLOR.search(body), f"{name}: 날 색" + + +def test_b07_guide_selectors_alive(): + """가이드 촬영 장면이 잡는 B07 자기 클래스(`b07-…`)는 소스에 살아 있어야 함 — CAD 앱 안(`cad-…`)은 다른 몫.""" + text = (ROOT / "A05_EduDetail/guide/B07/_scenes.json").read_text(encoding="utf-8") + for name in set(re.findall(r"\.(b07-[a-z0-9_-]+)", text)): + assert _defined("B07_DesignDetail", name), f"가이드 선택자 .{name} 가 B07 소스에 없음" + + +def test_b07_uses_parts_not_raw_widgets(): + """도면 단추 · 장 번호 · 도구 단추 · 그룹 상자 · 알림 · 값 줄이 부품으로 바뀜 · 시스템 확인창 없음.""" + ts = "".join(_src("B07_DesignDetail", ".ts").values()) + css = "".join(_src("B07_DesignDetail", ".css").values()) + for part in ( + "createButton", + "createBadge", + "createBanner", + "createEmpty", + "createKv", + "createPanelHead", + "createCard", + "createText", + "askConfirm", + ): + assert part in ts, f"B07 이 부품 {part} 를 안 씀" + assert "window.confirm" not in ts + assert 'createElement("button")' not in ts and 'el("button"' not in ts + assert "ui-panel-btn" not in ts and "ui-panel-btn" not in css # 옛 날 단추 꼴 + for old in ( + "b07-info__row", + "b07-info__key", + "b07-plan-tools__button {", + "b07-settings__group {", + ): + assert old not in css and old not in ts, f"옛 변종 {old} 가 남음" + + +def test_b07_css_common_values_only(): + for name, css in _src("B07_DesignDetail", ".css").items(): + assert not OLD_ALIAS.search(css), f"{name}: 옛 보라 별칭" + body = re.sub(r"/\*.*?\*/", "", css, flags=re.S) + assert not RAW_COLOR.search(body), f"{name}: 날 색"