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

This commit is contained in:
2026-11-01 15:19:50 +09:00
12 changed files with 265 additions and 107 deletions
+5 -1
View File
@@ -1,6 +1,10 @@
/* 쪽 단추(pager) — 표 바로 아래 한 줄 · 글은 보조 색 */
/* 쪽 단추(pager) — 표 바로 아래 한 줄(표와 간격 한 칸) · 글은 보조 색 */
.ui-pager {
gap: var(--spacing-12);
color: var(--color-text-secondary);
font-size: var(--text-body-sm);
}
.ui-table-wrap + .ui-pager {
margin-top: var(--spacing-12);
}
+81 -21
View File
@@ -5,7 +5,8 @@
* 크기 = 도구줄 단 없음(안 단추 = 보조 단추 기본 높이)
* 상태 = 첫 쪽 이전 막힘 · 끝 쪽 다음 막힘 (`disabled`)
* 쓰는 법 = createPager(total, size, page, (next) => …) — 표 **바로 아래**에 놓음
* 표 줄 수 = `fitRows(남은 높이)` 순수 셈 · `watchFit(표 칸, (줄 수) => …)` 가 화면 크기 바뀔 때 다시 알림
* 표 줄 수 = `fitRows(남은 높이)` 순수 셈 · `watchFit(바깥 칸, 표 칸 꺼내기, (줄 수) => …)` 가 그린 뒤 · 화면 크기 바뀔 때 알림
* 한 장 표(쪽 없음) = `watchHeight(표 칸)` 화면 아래까지 높이 묶고 안에서 스크롤
* 옛 이름 = M01 `buildPager`(`M01_MasterData_UI_Cells.ts`) 는 이 부품을 부르는 얇은 껍데기
* ========================================================================== */
@@ -16,8 +17,8 @@ import "./ui_template_pager.css";
/** 표 한 줄 높이(px) · 머리 높이 — 실제 줄이 있으면 잰 값을 씀 */
export const ROW_H = 40;
/** 표 밑 쪽 단추 줄 + 여백(px) */
export const PAGER_ROOM = 56;
/** 표 밑 쪽 단추 줄(32) + 간격(12) + 판 아래 여백(16) + 여유(4) */
export const PAGER_ROOM = 64;
/** 표가 보일 수 있는 줄 수 아래 한도 · 위 한도 */
export const MIN_ROWS = 5;
export const MAX_ROWS = 200;
@@ -36,39 +37,98 @@ 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;
/** 표 칸 위쪽 자리 ~ 화면 아래 사이로 들어가는 줄 수. 표 칸이 화면에 없으면 null. */
export function measureRows(grid: HTMLElement): number | null {
if (!grid.isConnected) return null;
const top = grid.getBoundingClientRect().top;
if (!grid.offsetParent && getComputedStyle(grid).position !== "fixed") return null; // 숨은 칸
const row = grid.querySelector<HTMLElement>("tbody tr");
const head = grid.querySelector<HTMLElement>("thead");
const rowH = row?.offsetHeight || ROW_H;
const headH = head?.offsetHeight || ROW_H;
return fitRows(window.innerHeight - top - PAGER_ROOM, rowH, headH);
/* ── 아래부터 화면 재기(순수 셈 아님) ── */
export interface FitOptions {
/** 화면 위치 대신 쓸 높이 한도(px) — 모달처럼 위치가 줄 수에 따라 움직이는 곳 */
cap?: () => number;
/** 사라지면(떼어지면) 알림을 그침 — 열 때마다 새로 만드는 목록용 */
once?: boolean;
}
/** 표 칸이 붙은 뒤 줄 수를 재서 알림 + 창 크기 바뀔 때마다 다시. 돌려받은 함수 = 해제. */
export function watchFit(grid: HTMLElement, onRows: (rows: number) => void): () => void {
/** 표 칸 위쪽 자리 ~ 화면 아래 사이로 들어가는 줄 수. 표 칸이 안 보이면 null. */
function measureRows(grid: HTMLElement, opt: FitOptions): number | null {
if (!grid.isConnected || !grid.offsetParent) return null;
const body = grid.querySelector<HTMLTableSectionElement>("tbody");
const count = body?.rows.length ?? 0;
const head = grid.querySelector<HTMLElement>("thead");
const room = opt.cap ? opt.cap() : window.innerHeight - grid.getBoundingClientRect().top;
// 줄 높이 = 그린 줄들의 평균(작은 글 · 두 줄 칸이 섞여도 맞음) · 줄 없으면 40
const rowH = count ? body!.offsetHeight / count : ROW_H;
return fitRows(room - PAGER_ROOM, rowH, head?.offsetHeight || ROW_H);
}
export interface Fit {
/** 그린 뒤 다시 재라고 알림 */
ask: () => void;
stop: () => void;
}
/** 표가 그려진 뒤 · 창 크기 바뀔 때 줄 수를 재서 **바뀌었을 때만** 알림. `root` = 늘 붙어 있는 바깥 칸. */
export function watchFit(
root: HTMLElement,
getGrid: () => HTMLElement | null,
onRows: (rows: number) => void,
opt: FitOptions = {},
): Fit {
let last = 0;
let frame = 0;
let seen = false;
let late = 0;
const stop = (): void => {
window.clearTimeout(late);
window.removeEventListener("resize", ask);
watcher?.disconnect();
if (frame) cancelAnimationFrame(frame);
};
const check = (): void => {
frame = 0;
const n = measureRows(grid);
if (opt.once) {
if (root.isConnected) seen = true;
else if (seen) return stop();
}
const grid = getGrid();
if (grid) watcher?.observe(grid); // 글꼴이 늦게 실려 줄 높이가 바뀌어도 다시 잼
const n = grid ? measureRows(grid, opt) : null;
if (n !== null && n !== last) {
last = n;
onRows(n);
}
};
const ask = (): void => {
function ask(): void {
if (!frame) frame = requestAnimationFrame(check);
};
// 위 칸(거름 · 찾기)이 늦게 자리 잡아 표 위치만 밀리면 크기 변화가 안 잡힘 — 한 번 더 잼
window.clearTimeout(late);
late = window.setTimeout(check, 400);
}
window.addEventListener("resize", ask);
const watcher = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(ask);
watcher?.observe(root); // 숨었다 다시 보일 때 · 위 칸이 늘고 줄 때
ask();
return () => {
window.removeEventListener("resize", ask);
if (frame) cancelAnimationFrame(frame);
return { ask, stop };
}
/** 첫 그림 전에 쓸 줄 수 어림 — `above` = 표 칸 위에 올 줄(px). */
export function guessRows(host: HTMLElement, above = 0, cap?: () => number): number {
const room = cap ? cap() : window.innerHeight - host.getBoundingClientRect().top - above;
return fitRows(room - PAGER_ROOM);
}
/** 쪽 없는 한 장 표 — 표 칸 위 ~ 화면 아래로 높이를 묶고 안에서 스크롤. 돌려받은 함수 = 해제. */
export function watchHeight(grid: HTMLElement, pad = 16): () => void {
const set = (): void => {
if (!grid.isConnected || !grid.offsetParent) return;
// 표 아래에 붙은 칸(주 · 안내)이 있으면 그 높이 + 칸 사이 간격(12)만큼 비워 둠
let below = 0;
for (let n = grid.nextElementSibling; n; n = n.nextElementSibling)
below += (n as HTMLElement).offsetHeight + 12;
const room = window.innerHeight - grid.getBoundingClientRect().top - pad - below;
grid.style.maxHeight = `${Math.max(120, Math.floor(room))}px`;
};
const frame = (): number => requestAnimationFrame(set);
window.addEventListener("resize", set);
frame();
return () => window.removeEventListener("resize", set);
}
/** 쪽 나눔 띠 — 이전·다음 · 「n / N 쪽 · 전체 줄」. */
+2 -2
View File
@@ -148,9 +148,9 @@ const pickDemo = (s: KitState): HTMLElement | null => {
if (s === "disabled") [one.root, many.root].forEach((b) => (b.disabled = true));
return row(one.root, many.root);
};
/** 709-1 견본 — 쪽 단추(가운데 쪽 · 한 장뿐이라 둘 다 막힘) · 수평 구분선(소제목 사이) */
/** 709-1 견본 — 쪽 단추(가운데 쪽 · 한 장뿐이라 이전 · 다음 다 막힘) · 수평 구분선(소제목 사이) */
const pagerDemo = (s: KitState): HTMLElement | null =>
s === "default" ? createPager(1234, 30, 2, noop) : s === "disabled" ? createPager(12, 30, 1, noop) : null;
s === "default" ? row(createPager(1234, 30, 2, noop), createPager(12, 30, 1, noop)) : null;
const dividerDemo = (s: KitState): HTMLElement | null => {
if (s !== "default") return null;
const box = document.createElement("div");