feat(종단도): 354-1 유토곡선 자기 판 — 종단 열 아래 칸 · 자기 리사이저 높이 · 접으면 머리줄([창 분할] · 손잡이) · 높이 나눔 · 표 쌓기에서 빠짐 · 종단 [창 분할]에도 함께 · B05 · B06 공용

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-16 20:50:03 +09:00
co-authored by Claude Opus 5.5
parent 5417cde435
commit bf3aaaa41f
15 changed files with 279 additions and 240 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;
+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