From ba53c7726de03458be66e5683873825967a291e2 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Fri, 9 Oct 2026 19:08:22 +0900 Subject: [PATCH] =?UTF-8?q?feat(B06):=20=EB=B3=B4=EB=9D=BC=20=EC=84=A0=20?= =?UTF-8?q?=EC=B9=98=EC=88=98=20=EB=91=90=20=EC=A4=84=20=C2=B7=20=EC=82=AC?= =?UTF-8?q?=EB=A9=B4=20=EC=9D=B4=EB=8F=99=20=ED=8C=A8=EB=84=90(151-1=20?= =?UTF-8?q?=C2=B7=20151-3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 치수 글 두 줄(사면길이 · 수평 / 높이 · 구배 · 이동) · 좁은 카드는 면적표 아래 높이로 - [◀ ▶] 지움 → 공용 조정창 꼴 이동 패널(◀ ↺ ▶ · 방향키 ←→ · 0.1 m · ✕ = 고름 풂) - 시험 — test_137_design_pick · test_139_6_line_shift(패널 · 방향키 · ↺ · 숨김) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018jonfdxn1Xit9bdkNJpE4G --- .../B06_Section_UI_Cross_Design_Pick.ts | 88 +++++++++++++------ B06_Section/B06_Section_UI_Cross_View.ts | 1 + .../B06_Section_UI_Style_Cross_Controls.css | 35 ++++++-- resources/tester/helper_137_design_pick.cjs | 2 + resources/tester/helper_139_6_line_shift.cjs | 72 ++++++++++++--- resources/tester/test_137_design_pick.py | 16 +++- resources/tester/test_139_6_line_shift.py | 27 ++++-- 7 files changed, 186 insertions(+), 55 deletions(-) diff --git a/B06_Section/B06_Section_UI_Cross_Design_Pick.ts b/B06_Section/B06_Section_UI_Cross_Design_Pick.ts index 485538bb5..2c4204455 100644 --- a/B06_Section/B06_Section_UI_Cross_Design_Pick.ts +++ b/B06_Section/B06_Section_UI_Cross_Design_Pick.ts @@ -4,15 +4,22 @@ * * 그려진 보라 선(`b06-chart__design-cross`)을 화면 좌표에서 m 로 되돌려 같은 방향 토막을 * 직선 조각 하나로 합치고, 벽 · 본체 폴리곤에 가린 부분을 뺀 **보이는 조각**마다 투명 판정 - * 선을 구조물 판정면 아래에 깐다. 고르면 그 조각을 강조하고 [+ − ⤢] 왼쪽에 치수를 적는다. + * 선을 구조물 판정면 아래에 깐다. 고르면 그 조각을 강조하고 [+ − ⤢] 왼쪽에 치수를 두 줄로 적는다. * 구역 판정(`crossZoneAt` · 도로 · 절토 · 성토)은 설계선 쉬프트(139-6)도 쓴다. * 고름 상태는 페이지에 한 칸(측점 + 조각 번호) — 카드를 다시 그려도 되살아난다. - * 고른 조각의 쪽 노견을 [◀ ▶] 0.1 m 로 넓히고 좁힘(139-6 쉬프트 · 노폭 안쪽 금지 = 0 에서 멈춤). + * 고른 조각 쪽 사면을 노견째 이동 패널(◀ ↺ ▶ · 방향키 · 0.1 m)로 옮김(139-6 쉬프트 · 151-3 · + * 노폭 안쪽 금지 = 0 에서 멈춤). * ========================================================================== */ import type { CrossDesign } from "./B06_Section_Api_Fetch"; import { clipSegment, type XY } from "../A00_Common/b_polygon_clip"; import { sideRole } from "@util/common_util_cross_design_geometry"; +import { + bindArrowKeys, + buildPanelShell, + dpadButton, + makeRow, +} from "./B06_Section_UI_Cross_Panel_Base"; const SVG_NS = "http://www.w3.org/2000/svg"; const DESIGN_CLASS = "b06-chart__design-cross"; @@ -205,12 +212,13 @@ export function pieceMeasure( return { lengthM: Math.hypot(horizontalM, heightM), horizontalM, heightM, grade }; } +/** 두 줄(151-1) — 1줄 = 길이 · 수평 · 2줄 = 높이 · 구배(줄바꿈 글자로 가름). */ export function pieceLabel(design: ZoneDesign, piece: DesignPiece): string { const { zone } = pieceZone(design, piece); const m = pieceMeasure(piece, zone); return ( `${zone === "road" ? "길이" : "사면길이"} ${m.lengthM.toFixed(2)}m · ` + - `수평 ${m.horizontalM.toFixed(2)}m · 높이 ${m.heightM.toFixed(2)}m · ${m.grade}` + `수평 ${m.horizontalM.toFixed(2)}m\n높이 ${m.heightM.toFixed(2)}m · ${m.grade}` ); } @@ -239,7 +247,7 @@ const pointsOf = (element: Element): XY[] => { .filter((p) => p.length === 2 && p.every(Number.isFinite)); }; -/** 설계선 쉬프트(139-6) — 쪽마다 노견 확장(m · ≥ 0). */ +/** 설계선 쉬프트(139-6) — 쪽마다 사면 이동(m · ≥ 0 · 노견째 · 151-2). */ export interface ShoulderShift { valueOf: (side: CrossSide) => number; set: (side: CrossSide, valueM: number) => void; @@ -247,7 +255,7 @@ export interface ShoulderShift { /** 쉬프트 걸음(m). */ export const SHIFT_STEP_M = 0.1; -/** 화면 ◀(−1) · ▶(+1) 한 걸음 → 그 쪽 노견 확장 새 값(0.1 m 눈금 · 0 아래 금지). +/** 화면 ◀(−1) · ▶(+1) 한 걸음 → 그 쪽 사면 이동 새 값(0.1 m 눈금 · 0 아래 금지). * `kx` = 화면 x / offset 기울기(+offset = 좌 · 화면 왼쪽이면 kx < 0). */ export function shiftedExtend(now: number, side: CrossSide, screenDir: -1 | 1, kx: number): number { const offsetDelta = screenDir * Math.sign(kx) * SHIFT_STEP_M; @@ -259,6 +267,8 @@ export interface DesignLinePick { pieces: DesignPiece[]; /** 치수 칸 — 카드가 [+ − ⤢] 앞에 붙임 · 고름 없으면 숨김. */ label: HTMLElement; + /** 사면 이동 패널(151-3) — 카드가 그림 칸에 붙임 · `shift` 없거나 고름 없으면 숨김. */ + panel: HTMLElement; setActive: (index: number | null) => void; } @@ -272,7 +282,7 @@ export function attachDesignLinePick(input: { x: (offset: number) => number; toDisplayY: (elevation: number) => number; onPick: (index: number) => void; - /** 있으면 고른 조각 쪽 노견을 [◀ ▶] 로 옮김(139-6). */ + /** 있으면 고른 조각 쪽 사면을 이동 패널로 옮김(139-6 · 151-3). */ shift?: ShoulderShift; }): DesignLinePick { const { layer, design, x, toDisplayY } = input; @@ -319,40 +329,62 @@ export function attachDesignLinePick(input: { const label = document.createElement("span"); label.className = "b06-cross-card__line-dims"; label.hidden = true; - const text = document.createElement("span"); - label.append(text); let side: CrossSide | null = null; + let active: number | null = null; const { shift } = input; - if (shift) { - const button = (glyph: string, dir: -1 | 1, tip: string): HTMLButtonElement => { - const el = document.createElement("button"); - el.type = "button"; - el.className = "b06-cross-card__zoom-btn b06-cross-card__line-shift"; - el.textContent = glyph; - el.title = tip; - el.addEventListener("click", (event) => { - event.stopPropagation(); - if (side) shift.set(side, shiftedExtend(shift.valueOf(side), side, dir, kx)); - }); - return el; - }; - label.append( - button("◀", -1, "고른 선 쪽 노견을 화면 왼쪽으로 0.1 m(노폭 안쪽은 못 감)"), - button("▶", 1, "고른 선 쪽 노견을 화면 오른쪽으로 0.1 m(노폭 안쪽은 못 감)"), - ); - } + + // 사면 이동 패널(151-3) — B06 공용 조정창 꼴(머리 · 「이동」 줄 십자 · 방향키 ←→). + // ✕ = 같은 조각 다시 고름(카드가 풂) · ↺ = 그 쪽 0. + const shell = buildPanelShell(() => { + if (active !== null) input.onPick(active); + }); + const readout = document.createElement("div"); + readout.className = "b06-structure-panel__value"; + const move = makeRow("이동"); + move.controls.classList.add("b06-structure-panel__buttons"); + const step = (dir: -1 | 1) => () => { + if (side && shift) shift.set(side, shiftedExtend(shift.valueOf(side), side, dir, kx)); + }; + move.controls.append( + dpadButton( + "◀", + "left", + "고른 사면을 화면 왼쪽으로 0.1 m(노견째 · 노폭 안쪽은 못 감)", + step(-1), + ), + dpadButton("↺", "reset", "이 쪽 이동 0", () => { + if (side && shift) shift.set(side, 0); + }), + dpadButton( + "▶", + "right", + "고른 사면을 화면 오른쪽으로 0.1 m(노견째 · 노폭 안쪽은 못 감)", + step(1), + ), + ); + shell.root.append(readout, move.row); + const arrows = bindArrowKeys(shell.root, () => [move.controls]); + return { pieces, label, + panel: shell.root, setActive: (index) => { const piece = index === null ? undefined : pieces[index]; hits.forEach((hit, i) => hit.classList.toggle("is-active", !!piece && i === index)); label.hidden = !piece; + active = piece ? index : null; side = piece ? pieceZone(design, piece).side : null; const extend = side && shift ? shift.valueOf(side) : 0; - text.textContent = piece - ? pieceLabel(design, piece) + (extend > 0 ? ` · 노견 +${extend.toFixed(1)}m` : "") + label.textContent = piece + ? pieceLabel(design, piece) + (extend > 0 ? ` · 이동 +${extend.toFixed(1)}m` : "") : ""; + const open = !!(piece && shift); + shell.root.classList.toggle("is-hidden", !open); + shell.title.textContent = side === "left" ? "사면 이동 (좌)" : "사면 이동 (우)"; + readout.textContent = `이동 ${extend.toFixed(1)}m`; + arrows.detach(); + if (open) arrows.attach(); }, }; } diff --git a/B06_Section/B06_Section_UI_Cross_View.ts b/B06_Section/B06_Section_UI_Cross_View.ts index ab9d0181d..0729de1e4 100644 --- a/B06_Section/B06_Section_UI_Cross_View.ts +++ b/B06_Section/B06_Section_UI_Cross_View.ts @@ -662,6 +662,7 @@ export function createCrossSectionCard( const zoomBar = buildZoomControls(zoomPan, widthActions); if (linePick) zoomBar.prepend(linePick.label); // 치수 = [+ − ⤢] 왼쪽(137-2) chartWrap.append(svg, readout.root, zoomBar, panel.root); + if (linePick) chartWrap.append(linePick.panel); // 사면 이동 패널(151-3) for (const panel of bodies.panels) chartWrap.append(panel.root); if (activeArea) { setBandActive(activeArea); diff --git a/B06_Section/B06_Section_UI_Style_Cross_Controls.css b/B06_Section/B06_Section_UI_Style_Cross_Controls.css index b0e894daf..d12b689e4 100644 --- a/B06_Section/B06_Section_UI_Style_Cross_Controls.css +++ b/B06_Section/B06_Section_UI_Style_Cross_Controls.css @@ -223,14 +223,39 @@ background: color-mix(in srgb, var(--color-surface-raised) 88%, transparent); } -/* 고른 보라 선 치수(137-2) — [+ − ⤢] 왼쪽 같은 줄. */ +/* 고른 보라 선 치수(137-2) — [+ − ⤢] 왼쪽 · 두 줄(151-1 · 길이 · 수평 / 높이 · 구배). */ .b06-cross-card__line-dims { - display: flex; - align-items: center; - padding: 0 var(--spacing-4); + align-self: center; + padding: 1px var(--spacing-4); color: var(--color-royal-amethyst); font-size: 0.75rem; - white-space: nowrap; + line-height: 1.25; + text-align: right; + white-space: pre-line; +} + +/* 좁은 카드(3열 등)는 막대 옆에 두면 왼쪽 면적표를 덮음 — 표 아래 높이로 내려 오른쪽에 붙임(151-1). */ +.b06-cross-card__chart-wrap { + container-type: inline-size; +} + +@container (max-width: 620px) { + .b06-cross-card__zoom { + overflow: visible; + } + + .b06-cross-card__line-dims { + position: absolute; + top: calc(var(--spacing-8) + var(--b06-areas-height, 3.6rem)); + right: -1px; + border: 1px solid var(--color-border); + border-radius: var(--radius-inputs); + background: color-mix(in srgb, var(--color-surface-raised) 88%, transparent); + } + + .b06-cross-card__line-dims + .b06-cross-card__zoom-btn { + border-left: none; + } } .b06-cross-card__line-dims[hidden] { diff --git a/resources/tester/helper_137_design_pick.cjs b/resources/tester/helper_137_design_pick.cjs index e6db8f38b..b30e6e80e 100644 --- a/resources/tester/helper_137_design_pick.cjs +++ b/resources/tester/helper_137_design_pick.cjs @@ -112,6 +112,8 @@ class FakeNode { globalThis.document = { createElement: (tag) => new FakeNode(tag), createElementNS: (_ns, tag) => new FakeNode(tag), + addEventListener: () => {}, // 조정창 공용 부품(사면 이동 패널 · 151-3)이 불러올 때 닮 + removeEventListener: () => {}, }; function loadTs(file) { diff --git a/resources/tester/helper_139_6_line_shift.cjs b/resources/tester/helper_139_6_line_shift.cjs index 97e9207ff..4d146fc17 100644 --- a/resources/tester/helper_139_6_line_shift.cjs +++ b/resources/tester/helper_139_6_line_shift.cjs @@ -1,5 +1,5 @@ -/* 139-6 고른 설계선 [◀ ▶] 노견 확장 헬퍼 — `_Cross_Design_Pick.attachDesignLinePick` 을 그 자리에서 - * 트랜스파일해 흉내 DOM 으로 돌림. 짝 = `test_139_6_line_shift.py`. +/* 139-6 고른 설계선 사면 이동 헬퍼 — `_Cross_Design_Pick.attachDesignLinePick` 을 그 자리에서 + * 트랜스파일해 흉내 DOM 으로 돌림 · 151-3 이동 패널(◀ ↺ ▶ · 방향키 ←→). 짝 = `test_139_6_line_shift.py`. */ const fs = require("fs"); const path = require("path"); @@ -15,6 +15,7 @@ class Node { this.attrs = {}; this.listeners = {}; this.hidden = false; + this.isConnected = true; this._text = ""; this._cls = new Set(); const self = this; @@ -24,6 +25,16 @@ class Node { contains: (c) => self._cls.has(c), }; } + get descendants() { + return this.children.flatMap((c) => [c, ...c.descendants]); + } + /** `.이름` 한 개만 — 패널 공용 부품이 슬롯 단추를 찾는 꼴. */ + querySelector(sel) { + return this.descendants.find((n) => n._cls.has(sel.slice(1))) ?? null; + } + getClientRects() { + return this._cls.has("is-hidden") ? [] : [{}]; + } set className(v) { this._cls = new Set(String(v).split(/\s+/).filter(Boolean)); } @@ -56,10 +67,19 @@ class Node { (this.listeners.click ?? []).forEach((fn) => fn({ stopPropagation: () => undefined })); } } +const docListeners = {}; globalThis.document = { createElement: (t) => new Node(t), createElementNS: (_n, t) => new Node(t), + addEventListener: (t, fn) => (docListeners[t] ??= []).push(fn), + removeEventListener: (t, fn) => { + docListeners[t] = (docListeners[t] ?? []).filter((f) => f !== fn); + }, }; +const key = (name) => + [...(docListeners.keydown ?? [])].forEach((fn) => + fn({ key: name, target: null, preventDefault: () => undefined }), + ); function loadTs(file) { const full = path.resolve(file); @@ -115,37 +135,65 @@ layer.append( ]), ); const values = { left: 0, right: 0 }; +const picks = []; const handle = pick.attachDesignLinePick({ layer, design, x, toDisplayY: y, - onPick: () => undefined, + onPick: (i) => picks.push(i), shift: { valueOf: (side) => values[side], set: (side, v) => (values[side] = v), }, }); -const [left, right] = handle.label.children.filter((c) => c.tagName === "button"); +const panel = handle.panel; +const slot = (name) => panel.querySelector(`.b06-structure-panel__btn--${name}`); +const close = panel.querySelector(".b06-structure-panel__close"); const pieceAt = (offset) => handle.pieces.findIndex( (p) => Math.min(p.from.offset, p.to.offset) <= offset && offset <= Math.max(p.from.offset, p.to.offset), ); +const hiddenAtStart = panel.classList.contains("is-hidden"); const log = []; handle.setActive(pieceAt(4)); // 좌 사면 -left.click(); // 화면 왼쪽 = 좌측 바깥 → +0.1 -left.click(); +const titleLeft = panel.descendants.find((n) => + n._cls.has("b06-structure-panel__title"), +).textContent; +slot("left").click(); // 화면 왼쪽 = 좌측 바깥 → +0.1 +key("ArrowLeft"); // 방향키 = 십자 log.push({ ...values }); -right.click(); // 안쪽 → 0.1 -right.click(); -right.click(); // 0 아래 금지 +key("ArrowRight"); // 안쪽 → 0.1 +slot("right").click(); +key("ArrowRight"); // 0 아래 금지 log.push({ ...values }); handle.setActive(pieceAt(-4)); // 우 사면 -left.click(); // 화면 왼쪽 = 우측 안쪽 → 0 에서 멈춤 -right.click(); // 우측 바깥 → +0.1 +key("ArrowLeft"); // 화면 왼쪽 = 우측 안쪽 → 0 에서 멈춤 +slot("right").click(); // 우측 바깥 → +0.1 log.push({ ...values }); handle.setActive(pieceAt(-4)); const text = handle.label.textContent; -process.stdout.write(JSON.stringify({ log, text, buttons: [left.textContent, right.textContent] })); +const readout = panel.descendants.find((n) => n._cls.has("b06-structure-panel__value")).textContent; +slot("reset").click(); // ↺ = 그 쪽 0 +const afterReset = { ...values }; +close.click(); // ✕ = 같은 조각 다시 고름(카드가 풂) +handle.setActive(null); +key("ArrowRight"); // 숨은 패널은 방향키 안 먹음 +process.stdout.write( + JSON.stringify({ + log, + text, + readout, + titleLeft, + afterReset, + afterHidden: { ...values }, + picks, + pieceRight: pieceAt(-4), + hiddenAtStart, + hiddenAfter: panel.classList.contains("is-hidden"), + labelButtons: handle.label.children.length, + slots: ["left", "reset", "right"].map((n) => slot(n).textContent), + }), +); diff --git a/resources/tester/test_137_design_pick.py b/resources/tester/test_137_design_pick.py index c75c230bf..0e88ef6c9 100644 --- a/resources/tester/test_137_design_pick.py +++ b/resources/tester/test_137_design_pick.py @@ -57,8 +57,9 @@ def test_치수_사면길이_수평_높이_구배(out) -> None: assert abs(fill["lengthM"] - 13**0.5) < 1e-9 assert out["measure"]["road"]["grade"] == "2.0%" # 노면 = % assert out["measure"]["flatSlope"] == "0.0%" - assert out["label"]["fill"] == "사면길이 3.61m · 수평 3.00m · 높이 2.00m · 1:1.50" - assert out["label"]["road"] == "길이 2.50m · 수평 2.50m · 높이 0.05m · 2.0%" + # 두 줄(151-1) — 1줄 = 길이 · 수평 · 2줄 = 높이 · 구배 + assert out["label"]["fill"] == "사면길이 3.61m · 수평 3.00m\n높이 2.00m · 1:1.50" + assert out["label"]["road"] == "길이 2.50m · 수평 2.50m\n높이 0.05m · 2.0%" def test_구역_도로_절토_성토(out) -> None: @@ -100,10 +101,21 @@ def test_카드_배선_하나만() -> None: assert view.count("dropLine();") >= 4 # 본체 · 벽 · 카드 풂 · 면적 assert "setLineActive(pickedLineAt(section.station_id));" in view # 다시 그려도 되살림 assert "zoomBar.prepend(linePick.label);" in view # 치수 = [+ − ⤢] 왼쪽 + assert "chartWrap.append(linePick.panel);" in view # 사면 이동 패널(151-3) css = _src("B06_Section/B06_Section_UI_Style_Cross_Areas.css") assert ".b06-chart__design-hit.is-active {" in css +def test_치수_두_줄_좁은_카드는_표_아래로() -> None: + """151-1 — 줄바꿈 글자를 살리고 오른쪽 맞춤 · 좁은 카드(3열 등)는 표 높이 아래로 내려 표를 안 덮음.""" + css = _src("B06_Section/B06_Section_UI_Style_Cross_Controls.css") + dims = css.split(".b06-cross-card__line-dims {", 1)[1].split("}", 1)[0] + assert "white-space: pre-line;" in dims and "text-align: right;" in dims + assert "container-type: inline-size;" in css + narrow = css.split("@container (max-width: 620px) {", 1)[1] + assert "top: calc(var(--spacing-8) + var(--b06-areas-height, 3.6rem));" in narrow + + def test_폴리곤_자르기는_공용_한_벌() -> None: b07 = _src("B07_DesignDetail/B07_DesignDetail_Cross_Overlay.ts") assert "function clipSegment(" not in b07 and "function pointInPolygon(" not in b07 diff --git a/resources/tester/test_139_6_line_shift.py b/resources/tester/test_139_6_line_shift.py index ae56ecd91..2430c9a19 100644 --- a/resources/tester/test_139_6_line_shift.py +++ b/resources/tester/test_139_6_line_shift.py @@ -1,4 +1,4 @@ -"""139-6 고른 설계선 [◀ ▶] — 고른 조각 쪽 노견 확장 0.1 m 걸음 · 노폭 안쪽 금지(0 에서 멈춤). +"""139-6 · 151-3 고른 설계선 사면 이동 패널 — 고른 조각 쪽만 0.1 m 걸음 · 노폭 안쪽 금지(0 에서 멈춤). 화면 TS(`_Cross_Design_Pick`)를 흉내 DOM 으로 돌린 값(`helper_139_6_line_shift.cjs`). """ @@ -26,16 +26,27 @@ def out() -> dict: return json.loads(done.stdout) -def test_화면_방향대로_그_쪽_노견만(out: dict) -> None: +def test_화면_방향대로_그_쪽만_단추와_방향키(out: dict) -> None: first, back, right = out["log"] - assert first == {"left": 0.2, "right": 0} # 좌 사면 고름 · ◀ 두 번 = 좌 바깥 0.2 - assert back == {"left": 0, "right": 0} # ▶ 세 번 = 0 에서 멈춤(노폭 안쪽 금지) - assert right == {"left": 0, "right": 0.1} # 우 사면 · ◀ = 안쪽 0 · ▶ = 바깥 0.1 + assert first == {"left": 0.2, "right": 0} # 좌 사면 · ◀ 단추 + ← 키 = 좌 바깥 0.2 + assert back == {"left": 0, "right": 0} # → 키 · ▶ · → 키 = 0 에서 멈춤(노폭 안쪽 금지) + assert right == {"left": 0, "right": 0.1} # 우 사면 · ← = 안쪽 0 · ▶ = 바깥 0.1 -def test_치수_줄에_노견_확장_표기(out: dict) -> None: - assert "노견 +0.1m" in out["text"] - assert out["buttons"] == ["◀", "▶"] +def test_치수_줄과_패널에_이동_표기(out: dict) -> None: + assert "이동 +0.1m" in out["text"] and "노견 +" not in out["text"] + assert out["readout"] == "이동 0.1m" + assert out["titleLeft"] == "사면 이동 (좌)" + + +def test_단추는_패널로_옮김_재설정_닫기(out: dict) -> None: + """151-3 — 치수 칸 [◀ ▶] 지움 · 패널 ◀ ↺ ▶ · ↺ = 그 쪽 0 · ✕ = 고름 풂 · 숨으면 방향키 안 먹음.""" + assert out["labelButtons"] == 0 + assert out["slots"] == ["◀", "↺", "▶"] + assert out["afterReset"] == {"left": 0, "right": 0} + assert out["picks"] == [out["pieceRight"]] + assert out["hiddenAtStart"] is True and out["hiddenAfter"] is True + assert out["afterHidden"] == {"left": 0, "right": 0} def test_페이지가_같은_창구로_세션_재계산() -> None: