Merge remote-tracking branch 'origin/sub_laptop_2' into main_laptop_1

This commit is contained in:
2026-10-16 20:52:54 +09:00
21 changed files with 711 additions and 295 deletions
@@ -1,6 +1,6 @@
/* 224-1b 유토 판 「붙어 있음」 판정 — 진짜 높이 나눔(`createHeightCascade`)과 방향키 판정
/* 224-1b 유토 판 높이 나눔 · 방향키 판정 — 진짜 높이 나눔(`createHeightCascade`)과 방향키 판정
* (`arrowsBelongToProfile` 본문)을 가짜 DOM 위에서 돌려 JSON 으로 냄.
* · 열림 && 뿌리 안 = 자리 차지 · 열림 && 새 창(뿌리 밖) = 자리 0 · 인라인 높이 안 씀
* · 유토곡선 = 종단 열 아래 자기 판(354-1) — 본문 높이에서 이미 빠져 나눔에 안 낌(입력에도 없음)
* · 방향키 = 마지막 누름이 종단 뿌리 · 뗀 유토 · 뗀 배수 판 안이면 종단도 몫 */
const fs = require("fs");
const path = require("path");
@@ -44,33 +44,27 @@ new Function(
)(
heightsExports,
(id) =>
id.includes("MassHaul")
? { MASSHAUL_MIN_HEIGHT: 160 }
: id.includes("TableOverlay")
? { TABLE_OVERLAY_MIN_HEIGHT: 120 }
: id.includes("Structures_Marks")
? { STRUCTURE_LANE_HEIGHT_PX: 40 }
: { frameOf: () => 0 },
id.includes("TableOverlay")
? { TABLE_OVERLAY_MIN_HEIGHT: 120 }
: id.includes("Structures_Marks")
? { STRUCTURE_LANE_HEIGHT_PX: 40 }
: { frameOf: () => 0 },
{ innerHeight: 900 },
);
function run(docked) {
function run(tableDesired) {
const root = el("b05-route-profile");
root.offsetHeight = 500;
const body = root.append(el());
body.clientHeight = 400;
const overlay = el("b05-profile__masshaul-overlay");
if (docked) body.append(overlay);
else el("popup-body").append(overlay); // 새 창 body
const overlay = el("b05-profile__table-overlay");
const cascade = heightsExports.createHeightCascade({
root,
body,
massHaul: { overlay, desiredHeight: () => 300, syncHandle() {} },
tableOverlay: {
overlay: el(),
isOpen: () => false,
desiredHeight: () => 0,
setBottomOffset() {},
overlay,
isOpen: () => tableDesired > 0,
desiredHeight: () => tableDesired,
},
isMainDragging: () => false,
isSubDragging: () => false,
@@ -99,8 +93,9 @@ const card = el("b06-cross-card");
process.stdout.write(
JSON.stringify({
docked: run(true),
popped: run(false),
tableClosed: run(0),
tableFits: run(200),
tableShrinks: run(300),
arrows: {
profile: arrows(profileChild),
mass: arrows(massChild),
@@ -1,8 +1,8 @@
/* 224-1c 유토곡선 · 배수유역 [창 분할] — 진짜 `attachSubPanelPopouts` · `attachProfilePopout`
* (design_profile_popout.ts)를 가짜 DOM · 가짜 창 위에서 돌려 JSON 으로 냄.
* · 단추 자리 · 글 · 클래스 · 툴팁 · 창 이름 셋 다름
* · 뗌 = 새 창 body · 유토 손잡이 숨김 · 인라인 높이 비움 · 배수 rehome
* · 붙임(단추 · 새 창 닫힘 · dispose) = 제자리(유토 손잡이 앞 · content 끝)
* · 단추 = 두 판 머리줄 맨 앞(유토는 접어도 남는 머리줄 · 354-1)
* · 뗌 = 새 창 body · 배수 rehome · 붙임(단추 · 새 창 닫힘 · dispose) = 제자리(종단 열 끝 · content 끝)
* · 종단 새 창 안의 단추 = 그 창에서 엶 */
const fs = require("fs");
const path = require("path");
@@ -122,21 +122,25 @@ new Function("exports", "require", "window", "document", js)(
mainDoc,
);
// ---- 종단 뿌리 = [content = [bodyWrap = [유토 판, 유토 손잡이], 배수 판 = [머리줄 = [제목]]]] ----
// ---- 종단 뿌리 = [content = [열 = [종단 칸, 유토 판 = [머리줄 = [손잡이]]], 배수 판 = [머리줄 = [제목]]]] ----
const slot = el("div");
mainDoc.body.append(slot);
const root = el("div", "b05-route-profile");
slot.append(root);
const content = el("div", "b05-route-profile__content");
const bodyWrap = el("div");
const column = el("div", "b05-route-profile__column");
const bodyWrap = el("div", "b05-route-profile__body-wrap");
const massOverlay = el("div", "b05-profile__masshaul-overlay");
const massHead = el("div", "b05-profile__masshaul-head");
const massHandle = el("div", "b05-profile__masshaul-handle");
bodyWrap.append(massOverlay, massHandle);
massHead.append(massHandle);
massOverlay.append(el("div", "ui-resizer"), massHead);
column.append(bodyWrap, massOverlay);
const drainageRoot = el("aside", "b05-drainage");
const header = el("div", "b05-drainage__header");
header.append(el("h3"));
drainageRoot.append(header);
content.append(bodyWrap, drainageRoot);
content.append(column, drainageRoot);
root.append(content);
let rehomes = 0;
let disposed = 0;
@@ -150,20 +154,18 @@ const profileButton = el("button");
mod.attachProfilePopout(profile, slot, profileButton);
mod.attachSubPanelPopouts(profile);
const massButton = massOverlay.children.find((c) => c.tagName === "BUTTON");
const massButton = massHead.children[0];
const drainButton = header.children[0];
const btn = (b) => ({ text: b.textContent, title: b.title, className: b.className, type: b.type });
const out = { mass: btn(massButton), drainage: btn(drainButton), profile: btn(profileButton) };
out.massButtonInHead = massButton.tagName === "BUTTON" && massHead.children[1] === massHandle;
// 유토 뗌
massOverlay.style.height = "123px";
massButton.click();
const massPopup = massOverlay.parent?.doc?.defaultView;
out.massOpened = {
inPopupBody: massOverlay.parent === massPopup?.document.body,
isPopout: massOverlay.classList.contains("is-popout"),
handleHidden: massHandle.hidden,
inline: massOverlay.style.height,
text: massButton.textContent,
docTitle: massPopup?.document.title,
};
@@ -175,11 +177,10 @@ out.drainOpened = {
rehomes,
text: drainButton.textContent,
};
// 유토 새 창 닫힘 → 제자리(손잡이 바로 앞)
// 유토 새 창 닫힘 → 제자리(종단 열 끝)
massPopup.fire("pagehide");
out.massClosed = {
order: bodyWrap.children.map((c) => c.className),
handleHidden: massHandle.hidden,
order: column.children.map((c) => c.className),
isPopout: massOverlay.classList.contains("is-popout"),
text: massButton.textContent,
};
@@ -205,10 +206,8 @@ profile.dispose();
out.disposed = {
closed: [profilePopup.closed, massPopup2.closed, drainPopup2.closed],
profileInSlot: root.parent === slot,
massBeforeHandle:
bodyWrap.children.indexOf(massOverlay) === bodyWrap.children.indexOf(massHandle) - 1,
massInColumnEnd: column.children[column.children.length - 1] === massOverlay,
drainInContent: drainageRoot.parent === content,
handleHidden: massHandle.hidden,
original: disposed,
};
out.opened = opened;
@@ -0,0 +1,57 @@
/* 354-2 종단 기준 숨김 — 진짜 `readVisible`(B05 유토곡선) · `normalizeVisibleBasis` ·
* `computeMassHaulSeries`(공용)를 잘라 돌려 JSON 으로 냄.
* · 저장된 범례 = 종단 → 횡단으로 맞춤 · 분배 켬은 그대로
* · 기준 ["cross"] = 횡단 계열 하나 · 기본(묶음 단계) = 두 기준 그대로 */
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 transpile = (code) =>
ts.transpileModule(code, {
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
}).outputText;
const block = (src, head) => {
const at = src.indexOf(head);
if (at < 0) throw new Error(`없음: ${head}`);
return src.slice(at, src.indexOf("\n}", at) + 2).replace(/^export /, "");
};
const util = read("common_util", "common_util_mass_haul.ts");
const mass = read("B05_Profile", "B05_Profile_UI_Profile_MassHaul.ts");
let stored = null;
const src = [
'const SERIES_BASES = ["cross", "longitudinal"];',
'const DEFAULT_VISIBLE = ["cross"];',
block(util, "export function normalizeVisibleBasis("),
block(mass, "function readVisible(): Set<string> {"),
block(util, "export function computeMassHaulSeries("),
].join("\n");
const api = new Function(
"readStateRaw",
"designedSections",
"crossAreaSamples",
"longitudinalAreaSamples",
"integrate",
`${transpile(src)}; return { readVisible, computeMassHaulSeries };`,
)(
() => stored,
(sections) => sections,
() => [{ basis: "cross" }],
() => [{ basis: "longitudinal" }],
(samples) => ({ from: samples[0].basis }),
);
const visible = (raw) => ((stored = raw), [...api.readVisible()].sort());
process.stdout.write(
JSON.stringify({
longitudinal: visible('["longitudinal"]'),
longitudinalBalance: visible('["longitudinal","balance"]'),
cross: visible('["cross"]'),
empty: visible(null),
broken: visible("{"),
crossOnly: api.computeMassHaulSeries({}, [], {}, undefined, ["cross"]).map((s) => s.key),
both: api.computeMassHaulSeries({}, [], {}).map((s) => s.key),
}),
);
@@ -0,0 +1,121 @@
/* 354-3 스크롤 동기화 메아리 재현 — 진짜 휠 이동(`attachWheelHorizontalScroll`)과 진짜 잇기
* (`linkScrollLeft`)를 가짜 창 · 가짜 스크롤러 위에서 돌려 JSON 으로 냄.
* 가짜 창은 브라우저 차례 그대로 — scrollLeft 가 바뀌면 scroll 은 **다음 틀**에 오고(틀 앞머리),
* 그 뒤 화면 틀 콜백이 돎. 옛 잇기(동기 플래그)와 새 잇기를 같은 휠로 견줌.
* 경우 넷 = 표 켬/끔 × 분배(유토 전체 맞춤) 켬/끔 · 휠을 굴리는 쪽 = 종단 · 유토 둘. */
const fs = require("fs");
const path = require("path");
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
const ROOT = path.join(__dirname, "..", "..");
const js = ts.transpileModule(
fs.readFileSync(path.join(ROOT, "B05_Profile", "B05_Profile_UI_Profile_Wheel.ts"), "utf8"),
{ compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 } },
).outputText;
function fakeWindow() {
const win = { now: 0, frames: [], seq: 0, pending: new Set() };
win.requestAnimationFrame = (fn) => (win.frames.push({ id: ++win.seq, fn }), win.seq);
win.cancelAnimationFrame = (id) => (win.frames = win.frames.filter((f) => f.id !== id));
win.getSelection = () => ({ removeAllRanges() {} });
win.frame = () => {
win.now += 16;
const targets = [...win.pending];
win.pending.clear();
targets.forEach((el) => el.fire("scroll"));
const due = win.frames;
win.frames = [];
due.forEach((f) => f.fn(win.now));
};
return win;
}
function scroller(win, name, { hidden = false, scrollWidth = 6000 } = {}) {
const calls = {};
let left = 0;
const el = {
name,
ownerDocument: { defaultView: win },
clientWidth: hidden ? 0 : 1000,
scrollWidth: hidden ? 0 : scrollWidth,
scrollTop: 0,
style: {},
addEventListener: (type, fn) => (calls[type] ??= []).push(fn),
removeEventListener() {},
hasPointerCapture: () => false,
fire: (type, ev = {}) => {
const e = { preventDefault() {}, ...ev };
(calls[type] ?? []).forEach((fn) => fn(e));
},
};
Object.defineProperty(el, "scrollLeft", {
get: () => left,
set: (v) => {
const next = Math.min(Math.max(0, v), Math.max(0, el.scrollWidth - el.clientWidth));
if (next !== left) win.pending.add(el);
left = next;
},
});
return el;
}
/** 옛 잇기 — 유토곡선 · 표가 각자 들고 있던 동기 플래그 꼴 그대로. */
function oldLink(a, b, paused = () => false) {
let syncing = false;
const mirror = (from, to) =>
from.addEventListener("scroll", () => {
if (syncing || paused()) return;
syncing = true;
to.scrollLeft = from.scrollLeft;
syncing = false;
});
mirror(a, b);
mirror(b, a);
}
function run(kind, { table, balance, driver }) {
const win = fakeWindow();
const wheel = { exports: {} };
new Function("exports", "module", "require", "window", js)(
wheel.exports,
wheel,
() => ({ viewOf: () => win }),
win,
);
const body = scroller(win, "body");
// 분배 켬 = 유토곡선 전체 맞춤(스크롤 없음) · 잇기는 멈춤
const mass = scroller(win, "mass", { scrollWidth: balance ? 1000 : 6000 });
const tableEl = scroller(win, "table", { hidden: !table });
const link = kind === "old" ? oldLink : wheel.exports.linkScrollLeft;
link(body, mass, () => balance);
link(body, tableEl);
const lead = driver === "mass" && !balance ? mass : body;
[body, mass, tableEl].forEach((el) => wheel.exports.attachWheelHorizontalScroll(el));
const trace = [];
for (let i = 0; i < 60; i += 1) {
if (i < 20 && i % 2 === 0) lead.fire("wheel", { deltaY: 120 });
win.frame();
trace.push(body.scrollLeft);
}
let back = 0;
for (let i = 1; i < trace.length; i += 1) if (trace[i] < trace[i - 1] - 0.01) back += 1;
return {
back,
end: Math.round(body.scrollLeft),
lead: Math.round(lead.scrollLeft),
mass: Math.round(mass.scrollLeft),
table: Math.round(tableEl.scrollLeft),
};
}
const out = {};
for (const kind of ["old", "new"])
for (const driver of ["body", "mass"])
for (const table of [true, false])
for (const balance of [false, true])
out[`${kind}|${driver}|table=${table}|balance=${balance}`] = run(kind, {
table,
balance,
driver,
});
process.stdout.write(JSON.stringify(out));
+17 -15
View File
@@ -1,8 +1,8 @@
# -*- coding: utf-8 -*-
"""224-1b(PLAN 325-2) 유토곡선 판 「붙어 있음」 판정 · 방향키.
"""224-1b(PLAN 325-2 · 354-1) 유토곡선 판 높이 나눔 · 방향키.
- 높이 나눔 · 끌기는 「열림 && 종단 뿌리 안」만 자리를 줌 — 새 창으로 뗀 유토 판은 메인 자리 0 ·
임시 축소 인라인 높이도 안 씀(뗀 창 높이가 굳지 않게).
- 유토곡선 = 종단 열 아래 자기 판(354-1) — 높이 나눔 · 끌기에 안 낌(본문 높이에서 이미 빠짐) ·
인라인 임시 축소도 안 씀. 나눔은 그래프 · 표 둘.
- 뗀 유토 판(`is-popout`)은 메인 보이는 구간 세로 범위(viewRange)를 안 씀 — 노선 전체 세로.
- 방향키 ←→ 판정은 마지막 누름이 종단 뿌리 · 뗀 유토 · 뗀 배수 판 안이면 종단도 몫.
@@ -34,13 +34,12 @@ def r():
return json.loads(proc.stdout)
def test_붙은_유토는_자리를_차지(r):
"""본문 400 − 알약 40 = 360 · 유토 300 은 예산 247 넘어 247 로 줄고 그래프 100."""
assert r["docked"] == {"chartHeight": 100, "inline": "247px"}
def test_뗀_유토는_자리_0_인라인_없음(r):
assert r["popped"] == {"chartHeight": 347, "inline": ""}
def test_유토_판은_나눔에_안_낌(r):
"""본문 400 − 알약 40 = 360 · 손잡이 띠 13 → 표 없음 그래프 347."""
assert r["tableClosed"] == {"chartHeight": 347, "inline": ""}
assert r["tableFits"] == {"chartHeight": 147, "inline": ""}
# 표 300 은 예산 247 넘어 247 로 줄고 그래프 100
assert r["tableShrinks"] == {"chartHeight": 100, "inline": "247px"}
def test_방향키_판정(r):
@@ -53,12 +52,15 @@ def test_방향키_판정(r):
}
def test_끌기도_같은_판정():
def test_끌기도_유토_판_안_셈():
drag = (B05 / "B05_Profile_UI_Profile_Drag.ts").read_text(encoding="utf-8")
assert "massHaul.overlay.hidden" in drag # 판정 함수 한 곳에만
assert drag.count("massHaul.overlay.hidden") == 1
assert drag.count("massDocked()") == 3
assert "root.contains(massHaul.overlay)" in drag
heights = (B05 / "B05_Profile_UI_Profile_Heights.ts").read_text(encoding="utf-8")
for src in (drag, heights):
assert "massDocked" not in src and "MASSHAUL_MIN_HEIGHT" not in src
assert "mass:" not in src
# 유토 판 리사이저 끌기 출처만 봄
assert drag.count("massHaul.overlay") == 1
assert "massHaul" not in heights
def test_뗀_유토는_노선_전체_세로():
@@ -1,10 +1,10 @@
# -*- coding: utf-8 -*-
"""224-1c(PLAN 325-3) 유토곡선 · 배수유역 [창 분할] — 종단 [창 분할]과 같은 양식 · 따로 새 창.
- 단추 = 유토 판 좌상단 · 배수 머리줄 맨 앞 · 글 [창 분할]/[창 결합] · 툴팁에 판 이름.
- 단추 = 두 판 머리줄 맨 앞(유토는 접어도 남는 머리줄 · 354-1) · 글 [창 분할]/[창 결합] · 툴팁에 판 이름.
- 창 이름 셋 다름(같으면 앞 창 body 를 덮어 조각이 사라짐).
- 뗌 = 새 창 body · 유토 손잡이 숨김 · 인라인 높이 비움 · 배수 지도 관찰 rehome.
- 붙임(단추 · 새 창 닫힘 · 페이지 떠남) = 제자리 — 유토 손잡이 바로 앞 · 배수 content 끝.
- 뗌 = 새 창 body · 뗀 유토 판 손잡이 · 리사이저는 CSS 로 감춤 · 배수 지도 관찰 rehome.
- 붙임(단추 · 새 창 닫힘 · 페이지 떠남) = 제자리 — 종단 열 끝(종단 칸 아래) · 배수 content 끝.
- 종단 새 창 안에서 다시 떼면 그 창이 새 창을 엶(메인 창 팝업 차단 피함).
`helper_224_1c_subpanel_popout.cjs` 가 진짜 TS 를 가짜 DOM · 가짜 창 위에서 돌림.
@@ -51,14 +51,13 @@ def test_단추_글_툴팁_클래스(r):
"type": "button",
}
assert "b05-route-profile__popout" in r["profile"]["className"]
assert r["massButtonInHead"] is True
def test_유토_뗌(r):
assert r["massOpened"] == {
"inPopupBody": True,
"isPopout": True,
"handleHidden": True,
"inline": "",
"text": "창 결합",
"docTitle": "유토곡선 — Aislo",
}
@@ -70,8 +69,7 @@ def test_배수_뗌은_rehome(r):
def test_새_창_닫힘과_단추로_제자리(r):
assert r["massClosed"] == {
"order": ["b05-profile__masshaul-overlay", "b05-profile__masshaul-handle"],
"handleHidden": False,
"order": ["b05-route-profile__body-wrap", "b05-profile__masshaul-overlay"],
"isPopout": False,
"text": "창 분할",
}
@@ -90,9 +88,8 @@ def test_페이지_떠나면_셋_다_닫고_제자리(r):
assert r["disposed"] == {
"closed": [True, True, True],
"profileInSlot": True,
"massBeforeHandle": True,
"massInColumnEnd": True,
"drainInContent": True,
"handleHidden": False,
"original": 1,
}
@@ -104,4 +101,5 @@ def test_배선과_양식():
block = re.search(r"\n\.b05-subpanel__popout \{([^}]*)\}", css).group(1)
assert "var(--btn-bar-padding)" in block and "var(--btn-bar-font-size)" in block
assert ".b05-profile__masshaul-overlay.is-popout," in css
assert ".b05-profile__masshaul-overlay.is-popout .b05-profile__masshaul-handle," in css
assert ".b05-drainage.is-popout {" in css
+2 -1
View File
@@ -67,7 +67,8 @@ def test_높이_맞춤(r):
def test_표_유토곡선_크기_끌기_다시_그림(r):
assert r["table"] == {"changed": 1, "handles": 2, "mainTimers": 0}
assert r["massHaul"] == {"changed": 1, "handles": 2, "mainTimers": 0}
# 유토 판 손잡이는 머리줄에 앉아 끌기 때 따라 옮길 것이 없음(354-1)
assert r["massHaul"] == {"changed": 1, "handles": 0, "mainTimers": 0}
def test_유토곡선_세로창_미끄러짐(r):
@@ -0,0 +1,85 @@
# -*- coding: utf-8 -*-
"""354-1 유토곡선 = 배수유역도처럼 자기 판(요구 235-7).
- 종단 열(`b05-route-profile__column`) = [종단 칸(bodyWrap), 유토 판] · content 안이라 종단 [창 분할]
(`.is-popout > :not(content)` 숨김)에도 같이 감 · B05 · B06 이 같은 `design_profile` 하나.
- 유토 판은 흐름 칸(겹침 아님) · 자기 리사이저 높이 · 접으면 머리줄만(손잡이 · [창 분할] 닿음).
- 높이 나눔 · 표 쌓기에서 빠짐(표 bottomOffset 없음) · X 동기화는 그대로.
"""
import os
import re
from pathlib import Path
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = Path(HERE).parents[1]
def read(rel: str) -> str:
return (ROOT / rel).read_text(encoding="utf-8")
def css_block(css: str, selector: str) -> str:
m = re.search(r"(?:^|\n)" + re.escape(selector) + r" \{([^}]*)\}", css)
assert m, selector
return m.group(1)
def test_종단_열_아래_자기_칸():
overlays = read("A00_Common/design/design_profile_overlays.ts")
assert 'column.className = "b05-route-profile__column";' in overlays
assert "column.append(bodyWrap, massHaul.overlay);" in overlays
assert "bodyWrap.append(massHaul.overlay" not in overlays
assert "massHaul.attachScrollSync(body);" in overlays
profile = read("A00_Common/design/design_profile.ts")
assert "content.append(column);" in profile
assert sum(1 for _ in profile.splitlines()) <= 700
def test_종단_창_분할에도_안_숨음():
page = read("A00_Common/design/design_page.css")
assert ".b05-route-profile.is-popout > :not(.b05-route-profile__content)" in page
# 유토 판은 뿌리 바로 아래가 아니라 content > column 안
overlays = read("A00_Common/design/design_profile_overlays.ts")
assert "root.append" not in overlays
def test_흐름_칸_자기_높이_접으면_머리줄():
css = read("B05_Profile/B05_Profile_UI_Style_MassHaul.css")
panel = css_block(css, ".b05-profile__masshaul-overlay")
assert "position: relative;" in panel and "absolute" not in panel
assert "height: var(--b05-masshaul-height, 280px);" in panel
assert "flex: none;" in panel
column = css_block(css, ".b05-route-profile__column")
assert "flex-direction: column;" in column
hide = css_block(
css,
".b05-profile__masshaul-overlay.is-collapsed:not(.is-popout) > :not(.b05-profile__masshaul-head)",
)
assert "display: none;" in hide
assert "display: none;" in css_block(
css, ".b05-route-profile.is-collapsed .b05-profile__masshaul-overlay"
)
mass = read("B05_Profile/B05_Profile_UI_Profile_MassHaul.ts")
assert "head.append(handle);" in mass
assert "overlay.append(resizer.root, head, bar, scroll, legendLayer, pendingChip);" in mass
assert 'overlay.classList.toggle("is-collapsed", !next);' in mass
assert "overlay.hidden" not in mass
# 새 창으로 뗀 판은 접힘이어도 그림
assert 'const shown = (): boolean => open || overlay.classList.contains("is-popout");' in mass
def test_창_분할_단추는_머리줄_맨_앞():
popout = read("A00_Common/design/design_profile_popout.ts")
assert "massHead.prepend(button);" in popout
assert "home: () => column?.append(massOverlay)," in popout
page = read("A00_Common/design/design_page.css")
assert ".b05-profile__masshaul-overlay > .b05-subpanel__popout" not in page
def test_높이_나눔_표_쌓기에서_빠짐():
heights = read("B05_Profile/B05_Profile_UI_Profile_Heights.ts")
table = read("B05_Profile/B05_Profile_UI_Profile_TableOverlay.ts")
assert "massHaul" not in heights
assert "setBottomOffset" not in heights and "setBottomOffset" not in table
assert "bottomOffset" not in table
@@ -0,0 +1,59 @@
# -*- coding: utf-8 -*-
"""354-2 유토곡선 종단 기준 숨김 — 안 셈 · 안 그림 · 범례 단추 없음(코드 남김).
- 그리기 = 횡단 기준만(`["cross"]`) · 범례는 낸 계열마다라 종단 단추가 안 섬.
- 저장된 범례가 종단(`longitudinal`)이면 횡단으로 맞춤 · 분배 켬은 그대로.
- 계산 묶음 단계(MassHaul_Stage)는 두 기준 그대로 · 공용 계산 코드 남김.
`helper_354_2_masshaul_cross_only.cjs` 가 진짜 TS 를 잘라 돌림.
"""
import json
import os
import re
import subprocess
from pathlib import Path
import pytest
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = Path(HERE).parents[1]
@pytest.fixture(scope="module")
def r():
proc = subprocess.run(
["node", os.path.join(HERE, "helper_354_2_masshaul_cross_only.cjs")],
capture_output=True,
text=True,
encoding="utf-8",
timeout=60,
)
assert proc.returncode == 0, proc.stderr
return json.loads(proc.stdout)
def test_저장된_종단_범례는_횡단으로(r):
assert r["longitudinal"] == ["cross"]
assert r["longitudinalBalance"] == ["balance", "cross"]
assert r["cross"] == ["cross"]
assert r["empty"] == ["cross"] and r["broken"] == ["cross"]
def test_기준_횡단만_계산(r):
assert r["crossOnly"] == ["cross"]
assert r["both"] == ["cross", "longitudinal"] # 공용 기본값 = 묶음 단계 그대로
def test_그리기는_횡단만_부름():
mass = (ROOT / "B05_Profile/B05_Profile_UI_Profile_MassHaul.ts").read_text(encoding="utf-8")
calls = re.findall(r"computeMassHaulSeries\(([^;]*?)\)", mass, re.S)
assert calls and all('["cross"]' in call for call in calls)
def test_묶음_단계와_공용_코드는_그대로():
stage = (ROOT / "B05_Profile/logic/B05_Profile_MassHaul_Stage.ts").read_text(encoding="utf-8")
assert '["cross"]' not in stage
util = (ROOT / "common_util/common_util_mass_haul.ts").read_text(encoding="utf-8")
assert 'const SERIES_BASES: MassHaulBasis[] = ["cross", "longitudinal"];' in util
assert "longitudinalAreaSamples(longitudinal, sections)" in util
@@ -0,0 +1,87 @@
# -*- coding: utf-8 -*-
"""354-3 종단 그래프 스크롤 끊김 — 동기화 메아리 · 스크롤마다 유토 다시 셈.
- 메아리: scroll 은 다음 틀에 옴 → 동기 플래그로 못 막아 한 틀 전 값을 되씀 → 뒤로 튐 →
휠 이동이 접힘. 공용 `linkScrollLeft`(Wheel.ts)가 써 넣은 값으로 온 scroll 을 무시.
가짜 창(다음 틀 scroll) 재현 — 옛 잇기는 뒤로 튐 · 멈춤, 새 잇기는 4 경우 × 휠 쪽 둘 모두 튐 0.
- 유토 스크롤 그리기 = 열림 · 분배 맞춤 아님 · 붙음일 때만 · 한 틀 한 번 · 다시 셈 없음.
`helper_354_3_scroll_echo.cjs` 가 진짜 휠 · 잇기 TS 를 가짜 창 위에서 돌림.
"""
import json
import os
import re
import subprocess
from pathlib import Path
import pytest
HERE = os.path.dirname(os.path.abspath(__file__))
B05 = Path(HERE).parents[1] / "B05_Profile"
CASES = [
f"{driver}|table={table}|balance={balance}"
for driver in ("body", "mass")
for table in ("true", "false")
for balance in ("false", "true")
]
@pytest.fixture(scope="module")
def r():
proc = subprocess.run(
["node", os.path.join(HERE, "helper_354_3_scroll_echo.cjs")],
capture_output=True,
text=True,
encoding="utf-8",
timeout=60,
)
assert proc.returncode == 0, proc.stderr
return json.loads(proc.stdout)
def test_옛_잇기는_메아리로_뒤로_튐(r):
# 표 · 분배 끔(사용자 보고 경우)에서 휠 다섯 칸(1,200px) 중 210px 에서 멈춤
assert r["old|body|table=false|balance=false"]["back"] > 0
assert r["old|body|table=false|balance=false"]["end"] < 1200
# 분배 켜고 표 끔 = 잇기 없음 — 사용자 관찰(분배 켜면 매끄러움)과 같음
assert r["old|body|table=false|balance=true"] == {
"back": 0,
"end": 1200,
"lead": 1200,
"mass": 0,
"table": 0,
}
@pytest.mark.parametrize("case", CASES)
def test_새_잇기는_뒤로_튐_0_끝까지(r, case):
got = r[f"new|{case}"]
assert got["back"] == 0
assert got["end"] == 1200 and got["lead"] == 1200
table, balance = "table=true" in case, "balance=true" in case
assert got["table"] == (1200 if table else 0)
assert got["mass"] == (0 if balance else 1200)
def test_유토_표_같은_도우미():
for name in ("MassHaul", "TableOverlay"):
src = (B05 / f"B05_Profile_UI_Profile_{name}.ts").read_text(encoding="utf-8")
assert "linkScrollLeft(main, scroll" in src
assert "syncing" not in src
def test_유토_스크롤_그리기는_창_따를_때만_한_틀_한_번():
mass = (B05 / "B05_Profile_UI_Profile_MassHaul.ts").read_text(encoding="utf-8")
body = mass[mass.index("function drawWindow(") :]
body = body[: body.index("\n }\n")]
assert '!open || fitted || overlay.classList.contains("is-popout") || !lastSeries' in body
assert "windowState.frame" in body and "frameOf(overlay" in body
assert "draw(lastParams, true)" in body
# 다시 셈 없음 · 총괄값 · 범례는 계열이 바뀔 때만
assert re.search(r"reuse && lastSeries\s*\? lastSeries", mass)
assert "if (series !== lastSeries) emitSummary(series);" in mass
# 다시 그려도 제 자리 — 종단 값으로 되쓰는 것은 처음 · 맞춤 뒤 · 창 옮김 뒤만
assert "scroll.scrollLeft = syncedMain.scrollLeft" not in mass
render = (B05 / "B05_Profile_UI_Profile_Render.ts").read_text(encoding="utf-8")
assert "massHaul.drawWindow({" in render