From 2fa68b95d8cf692a9deb1486cc5fc8c89ca36f8e Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 12 Oct 2026 19:19:05 +0900 Subject: [PATCH] =?UTF-8?q?feat(b05,b06):=20=EC=A2=85=EB=8B=A8=20=ED=8C=A8?= =?UTF-8?q?=EB=84=90=20=EB=8F=84=EA=B5=AC=EC=A4=84=20=E2=80=94=20[?= =?UTF-8?q?=EB=B0=B0=EC=88=98=EC=9C=A0=EC=97=AD=EB=8F=84]=20=EC=BC=AC?= =?UTF-8?q?=EB=81=94=20=EB=8B=A8=EC=B6=94=20=EC=A7=80=EC=9B=80=20=C2=B7=20?= =?UTF-8?q?[=EC=83=88=20=EC=B0=BD]=20=3D=20[=EC=B4=88=EA=B8=B0=ED=99=94]?= =?UTF-8?q?=20=EC=99=BC=EC=AA=BD=20(282)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 배수유역 판 켬끔(setVisible · drainage-visible 값) 지움 — 판은 늘 보임 · 접기 · 펼치기는 판 손잡이 하나로 · 옛 값 false 무시 - [새 창] 단추 = 도구 묶음 맨 앞(toolLead) — 다시 그릴 때마다 [초기화] 바로 왼쪽에 옮겨 붙임 · 열림 · 닫힘 동작 그대로 - 시험 test_282_1 추가 · 옛 단추를 찾던 시험(187-1 · drainage_side · profile_single) 새 꼴로 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QfHKQs7J3hQELKMWQMUJEy --- A00_Common/b_page_state.ts | 2 - A00_Common/design/design_page.css | 2 +- A00_Common/design/design_profile.ts | 15 +- A00_Common/design/design_profile_popout.ts | 6 +- A00_Common/design/design_shared.ts | 8 +- B05_Profile/B05_Profile_UI_Drainage_Side.ts | 71 +------ .../B05_Profile_UI_Profile_SideTool.ts | 25 +-- B05_Profile/B05_Profile_UI_Profile_Tools.ts | 6 +- B05_Profile/B05_Profile_UI_Style_Table.css | 7 +- .../tester/helper_282_1_profile_toolbar.cjs | 185 ++++++++++++++++++ .../tester/test_187_1_b05_head_button_size.py | 18 +- .../tester/test_282_1_profile_toolbar.py | 67 +++++++ resources/tester/test_b05_drainage_side.py | 15 +- .../tester/test_design_profile_single.py | 2 +- 14 files changed, 296 insertions(+), 133 deletions(-) create mode 100644 resources/tester/helper_282_1_profile_toolbar.cjs create mode 100644 resources/tester/test_282_1_profile_toolbar.py diff --git a/A00_Common/b_page_state.ts b/A00_Common/b_page_state.ts index 48ef93fe1..1a3396462 100644 --- a/A00_Common/b_page_state.ts +++ b/A00_Common/b_page_state.ts @@ -57,8 +57,6 @@ export const STATE_REGISTRY = { legacy: () => "b05-route-drainage-collapsed", }, "drainage-width": { bucket: "pref", scope: "global", legacy: () => "b05-route-drainage-width" }, - /** 종단도 오른쪽 판(배수유역도) 켬 · 끔 — 두 보기가 같은 값(계획서 24-2c · 24-2e). */ - "drainage-visible": { bucket: "pref", scope: "global" }, /** M02 면적산출 오른쪽 일위대가 패널(`AreaPanel`) — 접힘 · 폭(옛 조합 패널 키 그대로) */ "m02-combo-panel-collapsed": { bucket: "pref", scope: "global" }, "m02-combo-panel-width": { bucket: "pref", scope: "global" }, diff --git a/A00_Common/design/design_page.css b/A00_Common/design/design_page.css index 65d0609db..605935355 100644 --- a/A00_Common/design/design_page.css +++ b/A00_Common/design/design_page.css @@ -50,7 +50,7 @@ display: none; } -.b05-route-profile__trailing > .b05-route-profile__popout { +.b05-route-profile__tools > .b05-route-profile__popout { padding: var(--btn-bar-padding); font-size: var(--btn-bar-font-size); } diff --git a/A00_Common/design/design_profile.ts b/A00_Common/design/design_profile.ts index acb2e894e..a15802aa9 100644 --- a/A00_Common/design/design_profile.ts +++ b/A00_Common/design/design_profile.ts @@ -103,8 +103,8 @@ export interface DesignProfileContext { view: DesignView; /** 오른쪽 판 — 없으면 판 없이 종단 열만. */ side?: DesignSideFactory; - /** 오른쪽 판을 켜고 끄는 단추 — 도구줄 오른쪽에 붙는다(판이 있을 때만). */ - sideTool?: (board: DesignSideBoard) => HTMLElement; + /** 도구줄 [초기화] 바로 왼쪽에 놓는 단추([새 창]) — 다시 그려도 그 자리. */ + toolLead?: HTMLElement; /** 측점 선택 변경 — 같은 측점 재선택·빈 곳 클릭이면 null(해제)로 알린다. */ onSelectStation: (stationId: string | null) => void; /** [초기선 복원] 클릭 시 함께 실행(비정규 측점 등 다른 조작값도 초기화하려고 페이지가 넘긴다). */ @@ -260,7 +260,7 @@ export function createDesignProfile(ctx: DesignProfileContext) { planCurve: planCurveWarning(alignment, detail), minCoverViolations, balanceBar, - tools: tools.render(), + tools: withToolLead(tools.render()), trailing, alignment, legacyAlignment: !!detail && hasLegacyAlignment(detail.longitudinal), @@ -282,11 +282,14 @@ export function createDesignProfile(ctx: DesignProfileContext) { let updateElevationWindow: (() => ElevationWindowResult | null) | null = null; /** 계획고 편집 버튼(▲▼)을 누르고 있는 동안 Y 축을 고정한다(`_Profile_Drag` 몫). */ const elevationHold = createElevationHold(body, () => draw()); - // 오른쪽 판 켬 · 끔 단추 — 가로 배율 조작구 옆, 두 보기 같은 자리. - const sideTool = drainagePanel && ctx.sideTool ? ctx.sideTool(drainagePanel) : null; const trailing = document.createElement("div"); trailing.className = "b05-route-profile__trailing"; - trailing.append(profileZoom.bar, ...(sideTool ? [sideTool] : [])); + trailing.append(profileZoom.bar); + /** 도구 묶음은 그릴 때마다 새로 만든다 — 맨 앞([초기화] 왼쪽)에 `toolLead` 를 다시 옮겨 붙인다. */ + const withToolLead = (wrap: HTMLElement): HTMLElement => { + if (ctx.toolLead) wrap.prepend(ctx.toolLead); + return wrap; + }; /* [직선화]·[쉬프트]·되돌리기·방향키 배선은 `_Panel_Tools` 몫(700줄 한계). */ const { tools, history, handleToolPick, setListening } = createPanelTools({ diff --git a/A00_Common/design/design_profile_popout.ts b/A00_Common/design/design_profile_popout.ts index ddf5b743d..5ee992e21 100644 --- a/A00_Common/design/design_profile_popout.ts +++ b/A00_Common/design/design_profile_popout.ts @@ -13,22 +13,20 @@ import type { DesignProfile } from "./design_profile"; /** 메인 창 문서 · 창에 걸린 듣기가 받아야 할 새 창 이벤트. */ const FORWARD_TYPES = ["keydown", "keyup", "pointerdown", "pointerup", "pointercancel"]; -/** 종단 도구줄 [배수유역도] 단추 뒤에 [새 창] 단추를 붙인다 — 두 보기가 같은 한 벌을 쓴다. */ +/** 종단 도구줄 [초기화] 왼쪽 단추(`toolLead`)를 [새 창] 단추로 꾸민다 — 두 보기가 같은 한 벌을 쓴다. */ export function attachProfilePopout( profile: DesignProfile, slot: HTMLElement, - sideTool: HTMLElement | null, + button: HTMLButtonElement, ): void { const { root } = profile; let popup: Window | null = null; - const button = document.createElement("button"); button.type = "button"; button.className = "ui-btn ui-btn--ghost b05-route-profile__popout"; button.textContent = "새 창"; button.title = "종단 하단 패널을 브라우저 새 창으로 띄움"; button.addEventListener("click", () => (popup && !popup.closed ? closePopup() : open())); - sideTool?.after(button); const restore = (): void => { if (!root.classList.contains("is-popout")) return; diff --git a/A00_Common/design/design_shared.ts b/A00_Common/design/design_shared.ts index 7da9b42ab..9296eb899 100644 --- a/A00_Common/design/design_shared.ts +++ b/A00_Common/design/design_shared.ts @@ -21,7 +21,7 @@ import { fetchCurrentRouteId } from "../../B06_Section/B06_Section_Api_Fetch"; import { createStructuresSection, type StructuresSection } from "../b_structures_section"; import type { StructuresCallbacks } from "../../B05_Profile/B05_Profile_UI_Structures_Panel_Types"; import type { RouteProfilePanelCallbacks } from "../../B05_Profile/B05_Profile_UI_Profile_Panel_Types"; -import { drainageSide, drainageSideTool } from "../../B05_Profile/B05_Profile_UI_Profile_SideTool"; +import { drainageSide } from "../../B05_Profile/B05_Profile_UI_Profile_SideTool"; import { createDesignPanel, type DesignPanel } from "./design_panel"; import { createDesignProfile, type DesignProfile } from "./design_profile"; import { attachProfilePopout } from "./design_profile_popout"; @@ -150,12 +150,12 @@ function toAll( /** 종단도 한 벌 — 처음 부를 때 만들어 종단도 자리에 붙인다. */ export function sharedProfile(shared: DesignPageShared): DesignProfile { if (shared.profile) return shared.profile; - let sideTool: HTMLElement | null = null; + const popoutButton = document.createElement("button"); const profile = createDesignProfile({ projectId: shared.projectId, view: shared.view(), side: drainageSide, - sideTool: (board) => (sideTool = drainageSideTool(board)), + toolLead: popoutButton, onSelectStation: (stationId) => bound(shared).profile?.onSelectStation(stationId), onResetAll: () => bound(shared).profile?.onResetAll?.(), callbacks: { @@ -178,7 +178,7 @@ export function sharedProfile(shared: DesignPageShared): DesignProfile { }, }); shared.profileSlot.append(profile.root); - attachProfilePopout(profile, shared.profileSlot, sideTool); + attachProfilePopout(profile, shared.profileSlot, popoutButton); shared.profile = profile; return profile; } diff --git a/B05_Profile/B05_Profile_UI_Drainage_Side.ts b/B05_Profile/B05_Profile_UI_Drainage_Side.ts index ca78ebeb4..08f7476d9 100644 --- a/B05_Profile/B05_Profile_UI_Drainage_Side.ts +++ b/B05_Profile/B05_Profile_UI_Drainage_Side.ts @@ -12,8 +12,7 @@ * · 관 자리 읽기 · 쓰기(되돌리기) = `pipeChainages()` · `setPipeChainages()` * · 우클릭 메뉴의 구조물 종류 = `host.structureMenuItems` * · 구간 띠 · 노선 = `setIntervalSpans()` · `setRoute()` - * 두 보기(종단 · 횡단)에서 같은 판을 끼우고 **켜고 끈다** — 끄면 자리를 비우되 관 · 유역 - * 상태는 그대로 살아 있다(다시 켜면 이어서 보임). 켬 · 끔을 어디에 적을지는 끼우는 쪽 몫이다. + * 두 보기(종단 · 횡단)에서 같은 판을 끼운다 — 판은 늘 보이고 접기 · 펼치기는 판 안 손잡이 하나로. * ========================================================================== */ import "./B05_Profile_UI_Style_Drainage.css"; // 홀로 서려면 판이 자기 모양을 들고 온다. @@ -23,71 +22,11 @@ import { createDrainagePanel } from "./B05_Profile_UI_Drainage_Panel"; /** 종단쪽이 넘기는 이음 — 판 바깥 약속(`DrainagePanelCallbacks`)에 projectId · view 를 더한 것. */ export type DrainageSideHost = DesignSideHost; -/** 종단도 side 판 = 배수유역 판 창구 그대로 + 켜고 끔. */ -export type DrainageSideBoard = DesignSideBoard & { - /** 켜고 끈다. 켜면 숨겼던 사이 바뀐 크기 · 그림을 판이 스스로 다시 맞춘다. */ - setVisible: (visible: boolean) => void; - isVisible: () => boolean; - /** 켬 · 끔이 바뀔 때마다 부른다(단추 표시 · 저장). 해제 함수를 돌려준다. */ - onVisibleChange: (listener: (visible: boolean) => void) => () => void; -}; - -export interface DrainageSideOptions { - /** 처음 켤지(기본 켬). */ - visible?: boolean; -} - -/** `DesignSideFactory` — 종단도를 만들 때 한 번 부른다. */ -export function createDrainageSide( - host: DrainageSideHost, - options: DrainageSideOptions = {}, -): DrainageSideBoard { - const panel = createDrainagePanel(host); - const listeners = new Set<(visible: boolean) => void>(); - let visible = true; - - function setVisible(next: boolean): void { - if (next === visible) return; - visible = next; - panel.root.hidden = !visible; - listeners.forEach((listener) => listener(visible)); - } - // 처음 상태는 알림 없이 맞춘다. - if (options.visible === false) { - visible = false; - panel.root.hidden = true; - } - +/** `DesignSideFactory` — 종단도를 만들 때 한 번 부른다. 접기 · 펼치기는 판 손잡이 몫. */ +export function createDrainageSide(host: DrainageSideHost): DesignSideBoard { return { - ...panel, - // 보기가 바뀌어도 판은 다시 안 만든다 — 켬 · 끔은 끼우는 쪽이 `setVisible` 로 정한다. + ...createDrainagePanel(host), + // 보기가 바뀌어도 판은 다시 안 만든다. setView: () => {}, - setVisible, - isVisible: () => visible, - onVisibleChange(listener) { - listeners.add(listener); - return () => listeners.delete(listener); - }, - dispose() { - listeners.clear(); - panel.dispose(); - }, }; } - -/** 켜고 끄는 단추 — 어느 보기에서든 같은 모양 · 같은 글. 누르면 판을 뒤집는다. */ -export function createDrainageToggleButton(side: DrainageSideBoard): HTMLButtonElement { - const button = document.createElement("button"); - button.type = "button"; - button.className = "ui-btn ui-btn--ghost b05-drainage__toggle"; - button.textContent = "배수유역도"; - button.title = "배수유역도 판을 켜고 끕니다"; - const sync = (visible: boolean): void => { - button.setAttribute("aria-pressed", String(visible)); - button.classList.toggle("is-active", visible); - }; - sync(side.isVisible()); - side.onVisibleChange(sync); - button.addEventListener("click", () => side.setVisible(!side.isVisible())); - return button; -} diff --git a/B05_Profile/B05_Profile_UI_Profile_SideTool.ts b/B05_Profile/B05_Profile_UI_Profile_SideTool.ts index 5565caeab..85e81c343 100644 --- a/B05_Profile/B05_Profile_UI_Profile_SideTool.ts +++ b/B05_Profile/B05_Profile_UI_Profile_SideTool.ts @@ -1,24 +1,11 @@ /* ============================================================================= * B05_Profile_UI_Profile_SideTool.ts - * 종단도 오른쪽 판(배수유역도)을 두 보기에 **같은 모양 · 같은 켬끔 값**으로 끼운다 - * (계획서 24-2c · 24-2e). 판 본체는 `_Drainage_Side`(sub7), 켬 · 끔 값은 `drainage-visible`. + * 종단도 오른쪽 판(배수유역도)을 두 보기에 **같은 모양**으로 끼운다(계획서 24-2c · 24-2e). + * 판 본체는 `_Drainage_Side`(sub7). 판은 늘 보이고 접기 · 펼치기는 판 안 손잡이 하나로(282-1). * ========================================================================== */ -import { readStateRaw, writeStateRaw } from "../A00_Common/b_page_state"; -import type { DesignSideBoard, DesignSideFactory } from "../A00_Common/design/design_profile"; -import { - createDrainageSide, - createDrainageToggleButton, - type DrainageSideBoard, -} from "./B05_Profile_UI_Drainage_Side"; +import type { DesignSideFactory } from "../A00_Common/design/design_profile"; +import { createDrainageSide } from "./B05_Profile_UI_Drainage_Side"; -/** 오른쪽 판 — 켬 · 끔은 두 보기가 같은 값을 쓴다(기본 켬). */ -export const drainageSide: DesignSideFactory = (host) => - createDrainageSide(host, { visible: readStateRaw("drainage-visible") !== "false" }); - -/** 켬 · 끔 단추 — 누를 때마다 값을 적는다. */ -export function drainageSideTool(board: DesignSideBoard): HTMLElement { - const side = board as DrainageSideBoard; - side.onVisibleChange((visible) => writeStateRaw("drainage-visible", String(visible))); - return createDrainageToggleButton(side); -} +/** 오른쪽 판 — 두 보기가 같은 판을 쓴다. */ +export const drainageSide: DesignSideFactory = (host) => createDrainageSide(host); diff --git a/B05_Profile/B05_Profile_UI_Profile_Tools.ts b/B05_Profile/B05_Profile_UI_Profile_Tools.ts index 420d00159..4403fcdda 100644 --- a/B05_Profile/B05_Profile_UI_Profile_Tools.ts +++ b/B05_Profile/B05_Profile_UI_Profile_Tools.ts @@ -1,10 +1,10 @@ /* ============================================================================= * B05_Profile_UI_Profile_Tools.ts - * 종단 계획선 도구줄 — [초기화] · [↶][↷] · [직선화] · [쉬프트] · [▲][▼]. + * 종단 계획선 도구줄 — ([새 창]) · [초기화] · [↶][↷] · [직선화] · [쉬프트] · [▲][▼]. * ([초기화]는 2026-09-03 추가 — 마지막 [저장] 시점 기준이며 전체 초기화가 아니다.) * - * 자리는 절·성토 요약줄의 **맨 앞**(최대 기울기 칩 왼쪽)이고, 버튼 크기는 같은 줄 오른쪽 - * [배수유역도] · [새 창] 과 같다(공용 토큰 `--btn-bar-*`). + * 자리는 절·성토 요약줄의 **맨 앞**(최대 기울기 칩 왼쪽)이고, 버튼 크기는 맨 앞 + * [새 창] 과 같다(공용 토큰 `--btn-bar-*`). [새 창] 은 종단도가 그릴 때마다 맨 앞에 옮겨 붙인다(282-2). * * 선택 흐름: * [직선화] → 측점 2개를 그래프에서 고름 → 두 측점의 라운드에 탄젠트한 직선으로 대체. diff --git a/B05_Profile/B05_Profile_UI_Style_Table.css b/B05_Profile/B05_Profile_UI_Style_Table.css index 6584eb291..1806537e8 100644 --- a/B05_Profile/B05_Profile_UI_Style_Table.css +++ b/B05_Profile/B05_Profile_UI_Style_Table.css @@ -497,7 +497,7 @@ background: color-mix(in srgb, var(--color-surface-raised) 88%, transparent); } -/* 도구줄 오른쪽 묶음 — 가로 배율 조작구 + 오른쪽 판 켬끔 단추(두 보기 같은 자리, 계획서 24-2c). */ +/* 도구줄 오른쪽 묶음 — 가로 배율 조작구(두 보기 같은 자리, 계획서 24-2c). */ .b05-route-profile__trailing { display: flex; flex: 0 0 auto; @@ -506,11 +506,6 @@ margin-left: auto; } -.b05-route-profile__trailing > .b05-drainage__toggle { - padding: var(--btn-bar-padding); - font-size: var(--btn-bar-font-size); -} - .b05-profile__zoom-btn { width: 22px; height: 22px; diff --git a/resources/tester/helper_282_1_profile_toolbar.cjs b/resources/tester/helper_282_1_profile_toolbar.cjs new file mode 100644 index 000000000..e24598ca9 --- /dev/null +++ b/resources/tester/helper_282_1_profile_toolbar.cjs @@ -0,0 +1,185 @@ +/* 282 종단 도구줄 — 진짜 TS(도구 묶음 render · withToolLead · attachProfilePopout · 배수유역 판)를 + * 가짜 DOM 위에서 돌려 JSON 으로 냄. + * · [새 창] = 도구 묶음 맨 앞([초기화] 바로 왼쪽) · 다시 그려도 그 자리 + * · 새 창 열림 · 닫힘 = 글 바뀜 · 제자리 돌림 + * · 옛 값 drainage-visible=false 여도 판(손잡이) 보임 */ +const fs = require("fs"); +const path = require("path"); +const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); + +const ROOT = path.join(__dirname, "..", ".."); +const read = (...p) => fs.readFileSync(path.join(ROOT, ...p), "utf8"); +const load = (code, scope) => { + const js = ts.transpileModule(code, { + compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, + }).outputText; + const exports = {}; + const names = Object.keys(scope); + new Function("exports", ...names, js)(exports, ...names.map((n) => scope[n])); + return exports; +}; + +// ---- 가짜 DOM ---- +function el(tag) { + const node = { + tagName: tag.toUpperCase(), + className: "", + textContent: "", + title: "", + hidden: false, + disabled: false, + attrs: {}, + listeners: {}, + children: [], + parent: null, + dataset: {}, + style: {}, + classList: { + list: () => node.className.split(/\s+/).filter(Boolean), + contains: (c) => node.classList.list().includes(c), + add: (c) => + !node.classList.contains(c) && (node.className = [...node.classList.list(), c].join(" ")), + remove: (c) => + (node.className = node.classList + .list() + .filter((x) => x !== c) + .join(" ")), + toggle: (c, on) => + (on ?? !node.classList.contains(c)) ? node.classList.add(c) : node.classList.remove(c), + }, + detach() { + if (node.parent) node.parent.children = node.parent.children.filter((c) => c !== node); + node.parent = null; + }, + append(...kids) { + kids.forEach((k) => (k.detach(), (k.parent = node), node.children.push(k))); + }, + prepend(...kids) { + kids.reverse().forEach((k) => (k.detach(), (k.parent = node), node.children.unshift(k))); + }, + replaceChildren(...kids) { + node.children.slice().forEach((c) => c.detach()); + node.append(...kids); + }, + setAttribute: (k, v) => (node.attrs[k] = v), + addEventListener: (type, fn) => (node.listeners[type] ??= []).push(fn), + click: () => (node.listeners.click ?? []).forEach((fn) => fn({ stopPropagation() {} })), + }; + return node; +} +const fakeDoc = () => { + const doc = { title: "" }; + doc.createElement = el; + doc.head = el("head"); + doc.body = el("body"); + doc.documentElement = Object.assign(el("html"), { attributes: [] }); + doc.querySelectorAll = () => []; + doc.importNode = (n) => n; + return doc; +}; +const document = fakeDoc(); +const frames = []; +const window = { + listeners: {}, + addEventListener: (t, fn) => (window.listeners[t] ??= []).push(fn), + removeEventListener: () => {}, + open: () => popup, +}; +const popupListeners = {}; +const popup = { + closed: false, + document: fakeDoc(), + close: () => (popup.closed = true), + addEventListener: (t, fn) => (popupListeners[t] ??= []).push(fn), + ResizeObserver: class { + observe() {} + }, + requestAnimationFrame: () => 0, + cancelAnimationFrame: () => {}, +}; + +// ---- 진짜 도구 묶음 ---- +const { createProfileTools } = load(read("B05_Profile", "B05_Profile_UI_Profile_Tools.ts"), { + require: () => ({ sameRun: () => false }), + document, +}); +const tools = createProfileTools({ + onStraighten() {}, + onShift() {}, + onTilt() {}, + onResetToSaved() {}, + canResetToSaved: () => true, + onUndo() {}, + onRedo() {}, + canUndo: () => false, + canRedo: () => false, + hasStraightRuns: () => false, + onChanged() {}, + onModeChange() {}, + freeHint: () => "", + mountOverlay() {}, +}); + +// ---- 진짜 withToolLead(design_profile.ts) ---- +const profileSrc = read("A00_Common", "design", "design_profile.ts"); +const leadAt = profileSrc.indexOf("const withToolLead"); +const leadSrc = profileSrc.slice(leadAt, profileSrc.indexOf("};", leadAt) + 2); +const button = el("button"); +const { withToolLead } = load(`${leadSrc}\nexports.withToolLead = withToolLead;`, { + ctx: { toolLead: button }, +}); + +// ---- 진짜 attachProfilePopout ---- +const { attachProfilePopout } = load(read("A00_Common", "design", "design_profile_popout.ts"), { + require: () => ({ showToast() {} }), + document, + window, + getComputedStyle: () => ({ backgroundColor: "" }), + requestAnimationFrame: (fn) => frames.push(fn), +}); +const slot = el("div"); +const root = el("div"); +slot.append(root); +const balanceBar = el("div"); +root.append(balanceBar); +const profile = { root, redraw() {}, dispose() {} }; +attachProfilePopout(profile, slot, button); + +const render = () => { + const wrap = withToolLead(tools.render()); + balanceBar.replaceChildren(wrap); + return wrap.children.map((c) => c.textContent); +}; +const out = { first: render(), buttonClass: button.className }; +out.again = render(); +button.click(); +out.opened = { text: button.textContent, inPopup: popup.document.body.children[0] === root }; +out.openedRender = render(); +button.click(); +out.closed = { text: button.textContent, inSlot: root.parent === slot, popupClosed: popup.closed }; +out.closedRender = render(); + +// ---- 옛 값 drainage-visible=false — 판 · 손잡이 보임 ---- +const store = { "drainage-visible": "false", "drainage-collapsed": "false" }; +const sideSrc = read("B05_Profile", "B05_Profile_UI_Drainage_Side.ts"); +const panelRoot = Object.assign(el("aside"), { className: "b05-drainage" }); +const handle = el("button"); +panelRoot.append(handle); +const fakePanel = { root: panelRoot, setView() {}, dispose() {} }; +const side = load(sideSrc, { + require: (id) => (id.endsWith(".css") ? {} : { createDrainagePanel: () => fakePanel }), +}); +const sideToolSrc = read("B05_Profile", "B05_Profile_UI_Profile_SideTool.ts"); +const { drainageSide } = load(sideToolSrc, { + require: (id) => + id.includes("b_page_state") + ? { readStateRaw: (k) => store[k] ?? null, writeStateRaw: (k, v) => (store[k] = v) } + : side, +}); +const board = drainageSide({ projectId: "p", view: "profile" }); +out.side = { + rootHidden: board.root.hidden, + handleHidden: handle.hidden, + hasSetVisible: "setVisible" in board, +}; +process.stdout.write(JSON.stringify(out)); diff --git a/resources/tester/test_187_1_b05_head_button_size.py b/resources/tester/test_187_1_b05_head_button_size.py index dbfc07721..53f0f67fa 100644 --- a/resources/tester/test_187_1_b05_head_button_size.py +++ b/resources/tester/test_187_1_b05_head_button_size.py @@ -1,6 +1,6 @@ -"""187-1 B05 종단 그래프 머리 줄 — [초기화]~▼ 단추 크기 = 오른쪽 [배수유역도] · [새 창] 크기. +"""187-1 B05 종단 그래프 머리 줄 — [초기화]~▼ 단추 크기 = [새 창] 크기([배수유역도] 단추는 282-1 로 지움). -- 기준값 = 오른쪽 두 단추의 계산 크기(`.ui-btn` 바탕 + 줄 단추 토큰) — 높이 16 · 글자 12 · 좌우 여백 8 +- 기준값 = [새 창] 단추의 계산 크기(`.ui-btn` 바탕 + 줄 단추 토큰) — 높이 16 · 글자 12 · 좌우 여백 8 - 왼쪽 단추(`b05-route-profile__tool`) = 같은 토큰 · 고정 높이 · 최소 너비 없음 """ @@ -29,13 +29,9 @@ def _decl(block: str, prop: str) -> str: THEME = _read("ui_template/ui_template_theme.css") BASE = _read("ui_template/ui_template_elements_styles.ts") LEFT = _block(_read("B05_Profile/B05_Profile_UI_Style.css"), ".b05-route-profile__tool") -TOGGLE = _block( - _read("B05_Profile/B05_Profile_UI_Style_Table.css"), - ".b05-route-profile__trailing > .b05-drainage__toggle", -) POPOUT = _block( _read("A00_Common/design/design_page.css"), - ".b05-route-profile__trailing > .b05-route-profile__popout", + ".b05-route-profile__tools > .b05-route-profile__popout", ) @@ -62,14 +58,14 @@ def _size(block: str) -> dict: def test_기준값_오른쪽_단추_16_12_8(): - for block in (TOGGLE, POPOUT): + for block in (POPOUT,): assert _size(block) == {"padV": 1, "padH": 8, "font": 12, "height": 16} # 높이 계산의 바탕 = .ui-btn 줄 높이 1 · 테두리 1px assert re.search(r"\.ui-btn \{[^}]*line-height: 1;[^}]*border: 1px solid", BASE) def test_왼쪽_단추_높이_글자_여백이_오른쪽_기준값과_같음(): - assert _size(LEFT) == _size(TOGGLE) == _size(POPOUT) + assert _size(LEFT) == _size(POPOUT) assert _decl(LEFT, "line-height") == "1" assert re.search(r"border:\s*1px solid", LEFT) @@ -80,8 +76,8 @@ def test_왼쪽_단추에_옛_고정_크기_없음(): assert "9px" not in LEFT -def test_세_단추_모두_같은_토큰(): - for block in (LEFT, TOGGLE, POPOUT): +def test_두_단추_모두_같은_토큰(): + for block in (LEFT, POPOUT): assert _decl(block, "padding") == "var(--btn-bar-padding)" assert _decl(block, "font-size") == "var(--btn-bar-font-size)" assert _decl(LEFT, "font-family") == "var(--font-body)" # .ui-btn 바탕과 같은 글꼴 diff --git a/resources/tester/test_282_1_profile_toolbar.py b/resources/tester/test_282_1_profile_toolbar.py new file mode 100644 index 000000000..3dcd826c8 --- /dev/null +++ b/resources/tester/test_282_1_profile_toolbar.py @@ -0,0 +1,67 @@ +"""282 B05 · B06 아래 종단 패널 도구줄 — [배수유역도] 단추 지움 · [새 창] = [초기화] 왼쪽(요구 177). + +- 282-1 [배수유역도] 켬끔 단추 · 켬끔 값(`drainage-visible`) 없음 · 옛 값 false 여도 판 · 손잡이 보임 +- 282-2 [새 창] = 도구 묶음 맨 앞([초기화] 바로 왼쪽) · 다시 그려도 그 자리 · 열림 · 닫힘 동작 그대로 +`helper_282_1_profile_toolbar.cjs` 가 진짜 TS 를 가짜 DOM 위에서 돌림. +""" + +import json +import os +import subprocess +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[2] +HERE = os.path.dirname(os.path.abspath(__file__)) +TOOLS = ["초기화", "↶", "↷", "직선화", "자유지정", "쉬프트", "▲", "▼"] + + +@pytest.fixture(scope="module") +def out(): + proc = subprocess.run( + ["node", os.path.join(HERE, "helper_282_1_profile_toolbar.cjs")], + capture_output=True, + text=True, + encoding="utf-8", + timeout=60, + ) + assert proc.returncode == 0, proc.stderr + return json.loads(proc.stdout) + + +def test_배수유역도_단추_코드_없음(): + for rel in ( + "B05_Profile/B05_Profile_UI_Drainage_Side.ts", + "B05_Profile/B05_Profile_UI_Profile_SideTool.ts", + "A00_Common/design/design_profile.ts", + "A00_Common/design/design_shared.ts", + "A00_Common/design/design_profile_popout.ts", + "A00_Common/b_page_state.ts", + "B05_Profile/B05_Profile_UI_Style_Table.css", + ): + src = (ROOT / rel).read_text(encoding="utf-8") + for gone in ( + "createDrainageToggleButton", + "b05-drainage__toggle", + "drainage-visible", + "sideTool", + ): + assert gone not in src, (rel, gone) + + +def test_새_창_초기화_바로_앞_다시_그려도(out): + assert out["first"] == ["새 창", *TOOLS] + assert out["again"] == ["새 창", *TOOLS] + assert "b05-route-profile__popout" in out["buttonClass"] + + +def test_새_창_열림_닫힘(out): + assert out["opened"] == {"text": "제자리로", "inPopup": True} + assert out["openedRender"] == ["제자리로", *TOOLS] + assert out["closed"] == {"text": "새 창", "inSlot": True, "popupClosed": True} + assert out["closedRender"] == ["새 창", *TOOLS] + + +def test_옛_값_false_여도_판_손잡이_보임(out): + assert out["side"] == {"rootHidden": False, "handleHidden": False, "hasSetVisible": False} diff --git a/resources/tester/test_b05_drainage_side.py b/resources/tester/test_b05_drainage_side.py index 0650f76e3..7ec1d74d7 100644 --- a/resources/tester/test_b05_drainage_side.py +++ b/resources/tester/test_b05_drainage_side.py @@ -6,7 +6,7 @@ - 파일마다 700줄 이하. - 관 보고 만들기(`_Report`)는 상태 없이 같은 결과: ±0.5m 유역 매칭 · 세월교 유역은 관경 null · 시설 정보 · 출처 기본값 user. -- `createDrainageSide` 가 켜고 끄는 창구를 낸다(두 보기에서 같은 판). +- `createDrainageSide` 가 판 창구를 낸다(두 보기에서 같은 판 · 켬끔 없이 손잡이로만 접기 — 282-1). """ import json @@ -43,16 +43,11 @@ def test_배수유역_파일은_700줄_이하(): assert lines <= 700, (path.name, lines) -def test_켜고_끄는_창구(): +def test_판_창구_켬끔_없음(): side = (B05 / "B05_Profile_UI_Drainage_Side.ts").read_text(encoding="utf-8") - for name in ( - "createDrainageSide", - "createDrainageToggleButton", - "setVisible", - "onVisibleChange", - ): - assert name in side - assert "root.hidden" in side or "panel.root.hidden" in side + assert "createDrainageSide" in side + for name in ("createDrainageToggleButton", "setVisible", "onVisibleChange", "root.hidden"): + assert name not in side # 종단도 판 자리 약속(`DesignSideFactory` · design_profile.ts) 모양 그대로 assert "DesignSideBoard" in side and "DesignSideHost" in side assert "export function createDrainageSide(" in side diff --git a/resources/tester/test_design_profile_single.py b/resources/tester/test_design_profile_single.py index 20177abb3..7d028e2ea 100644 --- a/resources/tester/test_design_profile_single.py +++ b/resources/tester/test_design_profile_single.py @@ -39,7 +39,7 @@ def test_두_보기가_같은_종단도와_같은_판(): """종단도는 한 페이지 한 벌(계획서 24-3b) — 두 보기는 같은 노드에 제 알림만 건다.""" shared = _read("A00_Common/design/design_shared.ts") assert shared.count("createDesignProfile(") == 1 - assert "side: drainageSide" in shared and "drainageSideTool(board)" in shared + assert "side: drainageSide" in shared and "toolLead: popoutButton" in shared b05 = _read("B05_Profile/B05_Profile_UI_Profile_Panel.ts") b06 = _read("B06_Section/B06_Section_UI_Section_View_Profile.ts") for source, view in ((b05, "profile"), (b06, "cross")):