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:
@@ -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" },
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
/* =============================================================================
|
||||
* B05_Profile_UI_Profile_Tools.ts
|
||||
* 종단 계획선 도구줄 — [초기화] · [↶][↷] · [직선화] · [쉬프트] · [▲][▼].
|
||||
* 종단 계획선 도구줄 — ([새 창]) · [초기화] · [↶][↷] · [직선화] · [쉬프트] · [▲][▼].
|
||||
* ([초기화]는 2026-09-03 추가 — 마지막 [저장] 시점 기준이며 전체 초기화가 아니다.)
|
||||
*
|
||||
* 자리는 절·성토 요약줄의 **맨 앞**(최대 기울기 칩 왼쪽)이고, 버튼 크기는 같은 줄 오른쪽
|
||||
* [배수유역도] · [새 창] 과 같다(공용 토큰 `--btn-bar-*`).
|
||||
* 자리는 절·성토 요약줄의 **맨 앞**(최대 기울기 칩 왼쪽)이고, 버튼 크기는 맨 앞
|
||||
* [새 창] 과 같다(공용 토큰 `--btn-bar-*`). [새 창] 은 종단도가 그릴 때마다 맨 앞에 옮겨 붙인다(282-2).
|
||||
*
|
||||
* 선택 흐름:
|
||||
* [직선화] → 측점 2개를 그래프에서 고름 → 두 측점의 라운드에 탄젠트한 직선으로 대체.
|
||||
|
||||
@@ -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}
|
||||
@@ -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")):
|
||||
|
||||
Reference in New Issue
Block a user