diff --git a/resources/tester/ui_checks/51-1_오른쪽패널_높이.js b/resources/tester/ui_checks/51-1_오른쪽패널_높이.js new file mode 100644 index 000000000..ba3fabdd3 --- /dev/null +++ b/resources/tester/ui_checks/51-1_오른쪽패널_높이.js @@ -0,0 +1,54 @@ +/* 51-1 — 오른쪽 패널(공용 ui_template_side_panels) 높이 = innerHeight − 머리띠 · 스크롤 처음/중간/끝 화면 안 + * 시작 화면: 패널이 이미 열린 상세 화면 하나(M02 일위대가 상세 · M02 면적산출 · M01 로직 상세 중 하나) · 화면마다 한 번씩 부름 + * 부르는 법: orca eval --expression "$(tr '\n' ' ' < 이 파일)" (async 식 · JSON 글을 돌려줌) + * 기대: ok = true · h = want(innerHeight − max(머리띠, 칸 위)) ±2 · 칸이 올라가 붙은 뒤는 innerHeight − 머리띠 · inView = 처음·중간·끝 모두 true + */ +(async () => { + const w = (ms) => new Promise((r) => setTimeout(r, ms)); + const sp = [...document.querySelectorAll(".ui-side-panels")].find( + (s) => !s.classList.contains("is-collapsed") && s.getClientRects().length, + ); + if (!sp) return JSON.stringify({ error: "펴진 패널 없음" }); + const head = document.querySelector(".app-header")?.getBoundingClientRect().bottom ?? 0; + const scroller = (() => { + for (let n = sp.parentElement; n; n = n.parentElement) { + const y = getComputedStyle(n).overflowY; + if ((y === "auto" || y === "scroll") && n.scrollHeight > n.clientHeight + 1) return n; + } + return document.scrollingElement; + })(); + const max = scroller.scrollHeight - scroller.clientHeight; + const sample = async (name, y) => { + scroller.scrollTop = y; + window.dispatchEvent(new Event("scroll")); // 뒤에 가린 탭은 스크롤 알림이 안 와서 직접 + await w(150); + const r = sp.getBoundingClientRect(); + const want = Math.round( + innerHeight - Math.max(head, sp.parentElement.getBoundingClientRect().top), + ); + const body = sp.querySelector(".ui-side-panels__panel:not([hidden]) .ui-side-panels__body"); + return { + name, + top: Math.round(r.top), + bottom: Math.round(r.bottom), + h: Math.round(r.height), + want, + inView: r.top >= head - 2 && r.bottom <= innerHeight + 2, + bodyScrolls: body ? body.scrollHeight > body.clientHeight : null, + }; + }; + const was = scroller.scrollTop; + const rows = [ + await sample("처음", 0), + await sample("중간", Math.round(max / 2)), + await sample("끝", max), + ]; + scroller.scrollTop = was; + return JSON.stringify({ + innerHeight, + head: Math.round(head), + scrollMax: Math.round(max), + rows, + ok: rows.every((r) => Math.abs(r.h - r.want) <= 2 && r.inView), + }); +})(); diff --git a/ui_template/ui_template_side_panels.ts b/ui_template/ui_template_side_panels.ts index 8ba93babb..2041a5c04 100644 --- a/ui_template/ui_template_side_panels.ts +++ b/ui_template/ui_template_side_panels.ts @@ -180,7 +180,7 @@ function scroller(node: HTMLElement): HTMLElement | null { } /** - * 패널 묶음을 보이는 칸 안에 붙임 — 높이 = 보이는 칸 · 본문을 굴려도 그 자리(몸만 따로 스크롤) + * 패널 묶음을 보이는 칸 안에 붙임 — 높이 = 보이는 창 높이 − 머리띠(칸이 아래에 있으면 칸 위부터) · 본문을 굴려도 그 자리(몸만 따로 스크롤) * CSS sticky 는 위 칸의 overflow 가 막아 JS 로 — 어느 칸이 굴러도(capture) 다시 잼 */ function followView(node: HTMLElement): void { @@ -191,13 +191,16 @@ function followView(node: HTMLElement): void { if (!root.isConnected) return; const box = scroller(root)?.getBoundingClientRect(); const head = document.querySelector(".app-header")?.getBoundingClientRect().bottom ?? 0; - const top = Math.max(head, box?.top ?? 0) + 8; - const bottom = Math.min(window.innerHeight, box?.bottom ?? window.innerHeight) - 8; + const view = Math.max(head, box?.top ?? 0); + const bottom = Math.min(window.innerHeight, box?.bottom ?? window.innerHeight); const row = root.parentElement?.getBoundingClientRect(); if (!row) return; - const height = Math.max(240, bottom - top); + // 칸이 아직 화면 아래에 있으면 칸 위에 맞춤 · 올라가 붙으면 보이는 칸 전체 높이 + const top = Math.max(view, row.top); + // 칸 바닥(+ 쪽 아래 여백)을 넘지 않음 · 너무 줄면 최소 높이로 두고 칸 바닥에 맞춰 올라감 + const height = Math.max(240, Math.min(bottom, row.bottom + 48) - top); root.style.height = `${height}px`; - const shift = Math.min(Math.max(0, top - row.top), Math.max(0, row.height - height)); + const shift = Math.min(Math.max(0, view - row.top), Math.max(0, row.height + 48 - height)); root.style.transform = shift ? `translateY(${Math.round(shift)}px)` : ""; }; const stop = (): void => {