fix(829-1): 모바일 찾기 칸 되풀이 다시 그리기 — 늦은 옛 응답 덮어쓰기 막음 · 한글 조합 중 검색 보류 · 키보드로 높이만 바뀐 뒤 줄 수 다시 재기 거름

- 원인 셋: ① 목록 불러오기 응답 순서 가드 없음(Rows · Machine · Tables · 복사 고르기 · 고를 재료 판) → 느린 회선에서 앞 글자 응답이 뒤 글자 목록을 덮음 ② 글자 칸 조합마다 input 이 검색을 걸음 ③ 키보드가 뜨고 닫을 때마다 watchFit 이 줄 수를 다시 재 목록을 다시 받아 그림 · 첫 그림 어림(6)과 잰 값(5) 어긋남으로 한 글자에 두 번 받음
- 공용 고침: ui_template_pager(holdForKeyboard · 직전 잰 줄 수 어림) · device_context(찾기 칸 조합 보류) · M01 Rows · Machine · Tables · Page · Pick · LogicLab_Copy
- 시험: test_709_pager_fit 에 holdForKeyboard 1 건 더함

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-13 22:06:27 +09:00
co-authored by Claude Sonnet 5.5
parent af171f67e9
commit a7c48aae9a
9 changed files with 78 additions and 3 deletions
+23
View File
@@ -37,8 +37,22 @@ export function fitRows(room: number, rowH = ROW_H, headH = ROW_H): number {
export const pageKeepFirst = (page: number, oldSize: number, newSize: number): number =>
Math.floor(((Math.max(1, page) - 1) * oldSize) / Math.max(1, newSize)) + 1;
/** 가상 키보드 흉내 가름(829-1) — 글자를 넣는 중 · 터치 기기 · 폭 그대로면 높이만 바뀐 것 = 줄 수를 다시 재지 않음(키보드가 뜨고 닫을 때마다 목록이 다시 그려지는 것 막음). */
export const holdForKeyboard = (typing: boolean, touch: boolean, widthSame: boolean): boolean =>
typing && touch && widthSame;
/* ── 아래부터 화면 재기(순수 셈 아님) ── */
const TEXT_TYPES = ["text", "search", "email", "number", "password", "tel", "url", "date"];
const typingNow = (): boolean => {
const a = document.activeElement;
if (a instanceof HTMLTextAreaElement) return true;
if (a instanceof HTMLInputElement) return TEXT_TYPES.includes(a.type);
return a instanceof HTMLElement && a.isContentEditable;
};
const touchNow = (): boolean =>
window.matchMedia("(pointer: coarse)").matches || document.documentElement.dataset.touch === "on";
export interface FitOptions {
/** 화면 위치 대신 쓸 높이 한도(px) — 모달처럼 위치가 줄 수에 따라 움직이는 곳 */
cap?: () => number;
@@ -75,9 +89,11 @@ export function watchFit(
let frame = 0;
let seen = false;
let late = 0;
let lastW = window.innerWidth;
const stop = (): void => {
window.clearTimeout(late);
window.removeEventListener("resize", ask);
document.removeEventListener("focusout", ask);
watcher?.disconnect();
if (frame) cancelAnimationFrame(frame);
};
@@ -87,9 +103,13 @@ export function watchFit(
if (root.isConnected) seen = true;
else if (seen) return stop();
}
const widthSame = window.innerWidth === lastW;
lastW = window.innerWidth;
if (holdForKeyboard(typingNow(), touchNow(), widthSame)) return; // 키보드가 닫히면(focusout) 그때 잼
const grid = getGrid();
if (grid) watcher?.observe(grid); // 글꼴이 늦게 실려 줄 높이가 바뀌어도 다시 잼
const n = grid ? measureRows(grid, opt) : null;
if (n !== null) root.dataset.fitRows = String(n); // 같은 칸에 다시 그릴 때(찾기) 첫 줄 수 어림으로 씀
if (n !== null && n !== last) {
last = n;
onRows(n);
@@ -102,6 +122,7 @@ export function watchFit(
late = window.setTimeout(check, 400);
}
window.addEventListener("resize", ask);
document.addEventListener("focusout", ask);
const watcher = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(ask);
watcher?.observe(root); // 숨었다 다시 보일 때 · 위 칸이 늘고 줄 때
ask();
@@ -110,6 +131,8 @@ export function watchFit(
/** 첫 그림 전에 쓸 줄 수 어림 — `above` = 표 칸 위에 올 줄(px). */
export function guessRows(host: HTMLElement, above = 0, cap?: () => number): number {
const known = Number(host.dataset.fitRows); // 바로 앞에 잰 값 — 어림과 어긋나 두 번 받아 오는 것 막음
if (known >= MIN_ROWS) return Math.min(MAX_ROWS, known);
const room = cap ? cap() : window.innerHeight - host.getBoundingClientRect().top - above;
return fitRows(room - PAGER_ROOM);
}