Merge remote-tracking branch 'origin/dev' into sub_laptop_1

This commit is contained in:
2026-10-05 19:43:27 +09:00
2 changed files with 62 additions and 5 deletions
@@ -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),
});
})();
+8 -5
View File
@@ -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 => {