feat(b05,b06): 종단 패널 도구줄 — [배수유역도] 켬끔 단추 지움 · [새 창] = [초기화] 왼쪽 (282)

- 배수유역 판 켬끔(setVisible · drainage-visible 값) 지움 — 판은 늘 보임 · 접기 · 펼치기는 판 손잡이 하나로 · 옛 값 false 무시
- [새 창] 단추 = 도구 묶음 맨 앞(toolLead) — 다시 그릴 때마다 [초기화] 바로 왼쪽에 옮겨 붙임 · 열림 · 닫힘 동작 그대로
- 시험 test_282_1 추가 · 옛 단추를 찾던 시험(187-1 · drainage_side · profile_single) 새 꼴로

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QfHKQs7J3hQELKMWQMUJEy
This commit is contained in:
2026-10-12 19:19:05 +09:00
co-authored by Claude Opus 5.5
parent eaaa5ab71f
commit 2fa68b95d8
14 changed files with 296 additions and 133 deletions
-2
View File
@@ -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" },
+1 -1
View File
@@ -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);
}
+9 -6
View File
@@ -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({
+2 -4
View File
@@ -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;
+4 -4
View File
@@ -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<K extends keyof RouteProfilePanelCallbacks>(
/** 종단도 한 벌 — 처음 부를 때 만들어 종단도 자리에 붙인다. */
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;
}
+5 -66
View File
@@ -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;
}
+6 -19
View File
@@ -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);
+3 -3
View File
@@ -1,10 +1,10 @@
/* =============================================================================
* B05_Profile_UI_Profile_Tools.ts
* 종단 계획선 도구줄 — [초기화] · [↶][↷] · [직선화] · [쉬프트] · [▲][▼].
* 종단 계획선 도구줄 — ([새 창]) · [초기화] · [↶][↷] · [직선화] · [쉬프트] · [▲][▼].
* ([초기화]는 2026-09-03 추가 — 마지막 [저장] 시점 기준이며 전체 초기화가 아니다.)
*
* 자리는 절·성토 요약줄의 **맨 앞**(최대 기울기 칩 왼쪽)이고, 버튼 크기는 같은 줄 오른쪽
* [배수유역도] · [새 창] 과 같다(공용 토큰 `--btn-bar-*`).
* 자리는 절·성토 요약줄의 **맨 앞**(최대 기울기 칩 왼쪽)이고, 버튼 크기는 맨 앞
* [새 창] 과 같다(공용 토큰 `--btn-bar-*`). [새 창] 은 종단도가 그릴 때마다 맨 앞에 옮겨 붙인다(282-2).
*
* 선택 흐름:
* [직선화] → 측점 2개를 그래프에서 고름 → 두 측점의 라운드에 탄젠트한 직선으로 대체.
+1 -6
View File
@@ -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;
@@ -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));
@@ -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 바탕과 같은 글꼴
@@ -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}
+5 -10
View File
@@ -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
@@ -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")):