Merge remote-tracking branch 'origin/main_laptop_1' into sub_laptop_2
This commit is contained in:
@@ -64,9 +64,11 @@ export const STATE_REGISTRY = {
|
||||
"m02-card-pills": { bucket: "pref", scope: "global" },
|
||||
/** M01 단가산출 로직 상세 오른쪽 — 열린 패널 하나(flow · source · none) */
|
||||
"m01-logic-panel": { bucket: "pref", scope: "global" },
|
||||
/** B09 묶음 탭(171-3) 보기 `{탭: cards · table}` · 가로 열 수 `{탭: 1~4}` */
|
||||
/** B09 묶음 탭(171-3) 보기 `cards · table`(304-1 셋 탭 한 값) · 가로 열 수 `{탭: 1~4}` */
|
||||
"b09-group-view": { bucket: "pref", scope: "global" },
|
||||
"b09-group-cols": { bucket: "pref", scope: "global" },
|
||||
/** B09 행 누름(303-1) `popup · link` */
|
||||
"b09-link-mode": { bucket: "pref", scope: "global" },
|
||||
/** B05 계획노선 편집 창 — 보던 화면(회전 · 가운데 · 배율). 프로젝트마다 따로 */
|
||||
"routeedit-view": { bucket: "pref", scope: "project" },
|
||||
/** B05 계획노선 편집 창 — 시점 · 종점 · 교각점 고정. [저장] 대상이 아니라 draft 에 안 둠 */
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
/* =============================================================================
|
||||
* B09_Estimation_UI_Dock.ts
|
||||
* B09 좌측 하단 고정 줄(PLAN 303-1 · 304-1) — 위에서부터
|
||||
* ① [박스보기|호표보기] 묶음 탭 셋 보기(304-1 · 한 값)
|
||||
* ② [하이퍼링크|팝업] 행 누름(303 · 기본 팝업)
|
||||
* ③ `DRAFT.actions` 단추(초안 · 비면 줄 없음)
|
||||
* ④ [?][저장][내역 초기화](도움말 줄 `data-help-row`)
|
||||
*
|
||||
* 스위치 값 = pref global(계정 공용 · [저장] 필요 없음) · 바닥 고정 = 공용 `.ui-sidebar-actions` 한 벌
|
||||
* ========================================================================== */
|
||||
|
||||
import { createButton } from "@ui/ui_template_elements";
|
||||
import { readState, writeState } from "../A00_Common/b_page_state";
|
||||
import { DRAFT } from "./B09_Estimation_UI_Tabs";
|
||||
import {
|
||||
chooseGroupView,
|
||||
groupView,
|
||||
viewLabel,
|
||||
type GroupView,
|
||||
} from "./B09_Estimation_UI_Tabs_Groups";
|
||||
|
||||
export type LinkMode = "popup" | "link";
|
||||
|
||||
const LINK_LABEL: Record<LinkMode, string> = { link: "하이퍼링크", popup: "팝업" };
|
||||
|
||||
/** 행 누름 길 — 모르는 값 · 못 읽음 = 팝업 */
|
||||
export function linkMode(): LinkMode {
|
||||
try {
|
||||
return readState<unknown>("b09-link-mode") === "link" ? "link" : "popup";
|
||||
} catch {
|
||||
return "popup";
|
||||
}
|
||||
}
|
||||
|
||||
function setLinkMode(mode: LinkMode): void {
|
||||
try {
|
||||
writeState("b09-link-mode", mode);
|
||||
} catch {
|
||||
// 기억 못 하면 기본(팝업)으로 돌아감
|
||||
}
|
||||
}
|
||||
|
||||
/** 단추 둘 스위치 — 켜진 쪽 filled · aria-pressed */
|
||||
function switchRow<T extends string>(
|
||||
name: string,
|
||||
values: T[],
|
||||
label: (value: T) => string,
|
||||
current: () => T,
|
||||
pick: (value: T) => void,
|
||||
): HTMLElement {
|
||||
const row = document.createElement("div");
|
||||
row.className = "ui-btn-group b09-dock__row";
|
||||
row.dataset.b09Switch = name;
|
||||
const buttons = values.map((value) => {
|
||||
const button = createButton({
|
||||
label: label(value),
|
||||
variant: "ghost",
|
||||
onClick: () => {
|
||||
if (value === current()) return;
|
||||
pick(value);
|
||||
sync();
|
||||
},
|
||||
});
|
||||
button.dataset.value = value;
|
||||
return button;
|
||||
});
|
||||
const sync = (): void =>
|
||||
buttons.forEach((button, k) => {
|
||||
const on = values[k] === current();
|
||||
button.classList.toggle("ui-btn--filled", on);
|
||||
button.classList.toggle("ui-btn--ghost", !on);
|
||||
button.setAttribute("aria-pressed", String(on));
|
||||
});
|
||||
sync();
|
||||
row.append(...buttons);
|
||||
return row;
|
||||
}
|
||||
|
||||
/** 하단 고정 줄 — onView = 보기 바뀜(묶음 탭이면 다시 그림) */
|
||||
export function createDock(buttons: HTMLElement[], onView: () => void): HTMLElement {
|
||||
const views: GroupView[] = ["cards", "table"];
|
||||
const view = switchRow("view", views, viewLabel, groupView, (value) => {
|
||||
chooseGroupView(value);
|
||||
onView();
|
||||
});
|
||||
const link = switchRow<LinkMode>(
|
||||
"link",
|
||||
["link", "popup"],
|
||||
(mode) => LINK_LABEL[mode],
|
||||
linkMode,
|
||||
setLinkMode,
|
||||
);
|
||||
const extra = DRAFT.actions.map((make) => make());
|
||||
const draft = document.createElement("div");
|
||||
draft.className = "b09-dock__row";
|
||||
draft.dataset.b09DraftActions = "";
|
||||
draft.append(...extra);
|
||||
const main = document.createElement("div");
|
||||
main.className = "b09-dock__row";
|
||||
main.dataset.helpRow = "";
|
||||
main.append(...buttons);
|
||||
const dock = document.createElement("div");
|
||||
dock.className = "ui-sidebar-actions b09-dock";
|
||||
dock.append(view, link, ...(extra.length ? [draft] : []), main);
|
||||
return dock;
|
||||
}
|
||||
@@ -248,3 +248,57 @@
|
||||
.b09-estimation [data-b09-table="bill"] tbody tr.is-drag-src td {
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
/* 303-1 · 304-1 좌측 하단 고정 줄 — 줄 여럿 세로(보기 · 행 누름 스위치 · 초안 단추 · [?][저장][내역 초기화]) */
|
||||
.b09-dock.ui-sidebar-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.b09-dock.ui-sidebar-actions > * {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.b09-dock .b09-dock__row {
|
||||
display: flex;
|
||||
gap: var(--btn-group-gap);
|
||||
}
|
||||
|
||||
.b09-dock .b09-dock__row > .ui-btn:not(.ui-help-btn) {
|
||||
flex: 1 1 0;
|
||||
min-width: auto;
|
||||
padding-inline: var(--spacing-4);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 303-2 행 고름 팝업 — 누른 줄 곁 표 조각 창(겹쳐 쌓임 · 창 안 b09 표 꼴 그대로 · 빨강 테두리) */
|
||||
.b09-estimation.b09-popup {
|
||||
position: fixed;
|
||||
z-index: var(--z-modal);
|
||||
gap: var(--spacing-4);
|
||||
width: max-content;
|
||||
max-width: 90vw;
|
||||
max-height: 80vh;
|
||||
padding: var(--spacing-8);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--color-surface-raised);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.b09-popup__head {
|
||||
display: flex;
|
||||
flex: none;
|
||||
align-items: center;
|
||||
gap: var(--spacing-4);
|
||||
}
|
||||
|
||||
.b09-popup__title {
|
||||
margin-right: auto;
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
|
||||
.b09-popup .ui-data-table {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
/* =============================================================================
|
||||
* B09_Estimation_UI_Popup.ts
|
||||
* B09 행 고름 팝업(PLAN 303-2) — 링크 있는 줄 · 박스 카드 누름 = 대상 탭 표 조각 창(옮기지 않음).
|
||||
*
|
||||
* 창 = 열 제목(머리 여러 줄 그대로) + 강조 범위 줄 전체 · 표 짜기 = `…_UI_Builders`(화면과 같은 옵션)
|
||||
* 강조 범위 = 설계내역서 그 줄 + 자손 · 묶음 탭 호표 구간 · 그 밖 한 줄(하이퍼링크 강조와 같음)
|
||||
* 자리 = 누른 줄 아래(모자라면 위 · 화면 안) · 너비 = 표 너비(최대 화면 90%) · 넘치면 창 안 스크롤
|
||||
* 창 안 링크 줄 누름 = 이어진 창(겹쳐 위에 · 그 줄 곁) · Esc · × = 맨 위 하나 · 탭 바꿈 · 주소 바뀜 = 모두
|
||||
* 머리 [탭으로] = 하이퍼링크처럼 옮김(모두 닫음) · 창 안 = 보기 전용(고칠 칸 · 메뉴 없음)
|
||||
* ========================================================================== */
|
||||
|
||||
import { createDataTable, type DataTableOptions } from "@ui/ui_template_data_table";
|
||||
import { createButton } from "@ui/ui_template_elements";
|
||||
import { placeHoverInfo } from "@ui/ui_template_hover_info";
|
||||
import { tableOptionsOf } from "./B09_Estimation_UI_Builders";
|
||||
import { rowIndex, tabName, type Link, type TabId, type TabTable } from "./B09_Estimation_UI_Tabs";
|
||||
import type { BillTable } from "./B09_Estimation_UI_Tabs_Bill_Edit";
|
||||
import { subtreeEnd } from "./B09_Estimation_UI_Tabs_Bill_Level";
|
||||
import { isGroupTab, pickClass, pickRange } from "./B09_Estimation_UI_Tabs_Groups";
|
||||
|
||||
const EDGE = 8;
|
||||
|
||||
/** 강조 범위 [첫 줄, 끝 줄] — 대상 줄 없음 = null */
|
||||
export function popupRange(tab: TabId, spec: TabTable, key: string): [number, number] | null {
|
||||
if (isGroupTab(tab)) return pickRange(spec.rows, key);
|
||||
const at = rowIndex(spec.rows, key);
|
||||
if (at < 0) return null;
|
||||
return (spec as BillTable).menu ? [at, subtreeEnd(spec.rows, at)] : [at, at];
|
||||
}
|
||||
|
||||
/** 창 표 옵션 — 범위 줄만 · 줄 번호 곁값은 원래 줄 번호로 · 빨강 테두리 · 합계 · 고칠 칸 없음 */
|
||||
export function popupOptions(
|
||||
spec: TabTable,
|
||||
tab: TabId,
|
||||
range: [number, number],
|
||||
): DataTableOptions {
|
||||
const [from, to] = range;
|
||||
const at = (i: number): number => i + from;
|
||||
const level = spec.level;
|
||||
return {
|
||||
...tableOptionsOf(spec, tab),
|
||||
rows: spec.rows.slice(from, to + 1),
|
||||
total: null,
|
||||
editable: undefined,
|
||||
onEdit: undefined,
|
||||
rowClass: (i) =>
|
||||
[spec.rowClass?.(at(i)), pickClass(range, at(i))].filter(Boolean).join(" ") || null,
|
||||
cellClass: spec.cellClass && ((i, key) => spec.cellClass?.(at(i), key)),
|
||||
level: level && {
|
||||
...level,
|
||||
of: (i) => level.of(at(i)),
|
||||
group: level.group && ((i) => level.group?.(at(i)) ?? false),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** 창 자리 — 누른 줄 아래(모자라면 위) · 화면 밖으로 안 나감 */
|
||||
export function popupPlace(
|
||||
anchor: { left: number; top: number; right: number; bottom: number },
|
||||
size: { width: number; height: number },
|
||||
view: { width: number; height: number },
|
||||
): { x: number; y: number } {
|
||||
const { x, y } = placeHoverInfo(anchor, size, view);
|
||||
return { x, y: Math.max(EDGE, Math.min(y, view.height - EDGE - size.height)) };
|
||||
}
|
||||
|
||||
/** 열린 창(아래 → 위) */
|
||||
const stack: HTMLElement[] = [];
|
||||
let listening = false;
|
||||
|
||||
/** from 번째 창부터 위로 모두 닫음(0 = 모두) */
|
||||
export function closePopups(from = 0): void {
|
||||
for (const box of stack.splice(from)) box.remove();
|
||||
}
|
||||
|
||||
export const popupCount = (): number => stack.length;
|
||||
|
||||
function listen(): void {
|
||||
if (listening) return;
|
||||
listening = true;
|
||||
// Esc = 맨 위 하나(뒤 화면의 Esc 풂보다 먼저 · 창 있을 때만 가로챔)
|
||||
window.addEventListener(
|
||||
"keydown",
|
||||
(event) => {
|
||||
if (event.key !== "Escape" || !stack.length) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
closePopups(stack.length - 1);
|
||||
},
|
||||
true,
|
||||
);
|
||||
window.addEventListener("hashchange", () => closePopups());
|
||||
}
|
||||
|
||||
export interface PopupOpen {
|
||||
link: Link;
|
||||
/** 누른 줄 · 카드 줄(자리 기준) — 없으면 화면 가운데 위 */
|
||||
anchor: Element | null;
|
||||
/** 대상 탭 표(화면과 같은 표 짜기) — 없으면 null */
|
||||
specOf: (tab: TabId) => TabTable | null;
|
||||
/** 하이퍼링크처럼 옮김(대상 줄 없음 · [탭으로]) */
|
||||
go: (link: Link) => void;
|
||||
/** 참 = 창 안 누름(이어진 창) · 거짓 = 화면 누름(있던 창 모두 닫고 새로) */
|
||||
chain?: boolean;
|
||||
}
|
||||
|
||||
export function openRowPopup(o: PopupOpen): void {
|
||||
if (!o.chain) closePopups();
|
||||
const { tab, key } = o.link;
|
||||
const spec = o.specOf(tab);
|
||||
const range = spec && popupRange(tab, spec, key);
|
||||
if (!spec || !range) return o.go(o.link); // 보일 줄 없음 = 옮김
|
||||
listen();
|
||||
const level = stack.length;
|
||||
const box = document.createElement("div");
|
||||
box.className = "b09-estimation b09-popup";
|
||||
box.setAttribute("role", "dialog");
|
||||
box.setAttribute("aria-label", tabName(tab));
|
||||
box.dataset.b09Popup = tab;
|
||||
const title = document.createElement("span");
|
||||
title.className = "b09-popup__title";
|
||||
title.textContent = tabName(tab);
|
||||
const goButton = createButton({
|
||||
label: "탭으로",
|
||||
variant: "ghost",
|
||||
size: "sm",
|
||||
onClick: () => {
|
||||
closePopups();
|
||||
o.go(o.link);
|
||||
},
|
||||
});
|
||||
goButton.dataset.b09PopupGo = "";
|
||||
const closeButton = createButton({
|
||||
label: "×",
|
||||
variant: "ghost",
|
||||
size: "sm",
|
||||
onClick: () => closePopups(level),
|
||||
});
|
||||
closeButton.dataset.b09PopupClose = "";
|
||||
closeButton.setAttribute("aria-label", "닫기");
|
||||
const head = document.createElement("div");
|
||||
head.className = "b09-popup__head";
|
||||
head.append(title, goButton, closeButton);
|
||||
const from = range[0];
|
||||
const table = createDataTable({
|
||||
...popupOptions(spec, tab, range),
|
||||
// 창 안 링크 줄 = 이어진 창(고칠 칸 · 「단가연결」 칸은 화면처럼 안 엶)
|
||||
onCellClick: (i, cellKey) => {
|
||||
if (spec.editable?.(i + from, cellKey)) return;
|
||||
const next = spec.link?.(i + from, cellKey);
|
||||
if (next)
|
||||
openRowPopup({
|
||||
...o,
|
||||
link: next,
|
||||
anchor: table.root.querySelector(`tr[data-i="${i}"]`),
|
||||
chain: true,
|
||||
});
|
||||
},
|
||||
});
|
||||
table.root.dataset.b09Table = tab;
|
||||
box.append(head, table.root);
|
||||
document.body.append(box);
|
||||
stack.push(box);
|
||||
const view = { width: window.innerWidth, height: window.innerHeight };
|
||||
const size = box.getBoundingClientRect();
|
||||
const anchor = o.anchor?.getBoundingClientRect() ?? {
|
||||
left: (view.width - size.width) / 2,
|
||||
right: (view.width + size.width) / 2,
|
||||
top: view.height / 4,
|
||||
bottom: view.height / 4,
|
||||
};
|
||||
const { x, y } = popupPlace(anchor, size, view);
|
||||
box.style.left = `${x}px`;
|
||||
box.style.top = `${y}px`;
|
||||
}
|
||||
@@ -33,6 +33,8 @@ import { computeTotal } from "./B09_Estimation_Calc_Total";
|
||||
import { createTabBars } from "./B09_Estimation_UI_TabBar";
|
||||
import { createCheckPanel } from "./B09_Estimation_UI_Check";
|
||||
import { tableOptionsOf, tabTableOf } from "./B09_Estimation_UI_Builders";
|
||||
import { createDock, linkMode } from "./B09_Estimation_UI_Dock";
|
||||
import { closePopups, openRowPopup } from "./B09_Estimation_UI_Popup";
|
||||
import {
|
||||
DRAFT,
|
||||
rowIndex,
|
||||
@@ -154,6 +156,7 @@ export function renderB09Estimation(root: HTMLElement): void {
|
||||
/** 탭 하나 그림 · focus = 이동해 온 줄 키 */
|
||||
const show = (tab: TabId, focus?: string): void => {
|
||||
current = tab;
|
||||
closePopups(); // 303-2 탭 바꿈 · 다시 그림 = 창 모두 닫음
|
||||
bars.draw(tab);
|
||||
bars.setTools([]); // 보기 단추 · 열 수는 묶음 탭만 다시 채움(202-1)
|
||||
if (!projectId) return void panel.replaceChildren(note("프로젝트를 먼저 고르세요."));
|
||||
@@ -162,8 +165,19 @@ export function renderB09Estimation(root: HTMLElement): void {
|
||||
const spec = tabTableOf(tab, ctx);
|
||||
if (!spec) return void panel.replaceChildren(note(LATER[tab] ?? "다음 단계에서 채움"));
|
||||
if (typeof spec === "string") return void panel.replaceChildren(note(spec));
|
||||
const goto = (link: Link | null): void => {
|
||||
if (link) show(link.tab, link.key);
|
||||
// 303 행 누름 = 팝업(기본 · 누른 줄 곁 표 조각) · 하이퍼링크(그 탭 그 줄로)
|
||||
const goto = (link: Link | null, anchor?: Element | null): void => {
|
||||
if (!link) return;
|
||||
if (linkMode() === "link") return show(link.tab, link.key);
|
||||
openRowPopup({
|
||||
link,
|
||||
anchor: anchor ?? null,
|
||||
specOf: (t) => {
|
||||
const made = tabTableOf(t, ctx);
|
||||
return made && typeof made === "object" ? made : null;
|
||||
},
|
||||
go: (to) => show(to.tab, to.key),
|
||||
});
|
||||
};
|
||||
// extra = 줄 class 곁값(181-2 표 보기 호표 구간)
|
||||
const makeTable = (extra?: (index: number) => string | null): DataTable => {
|
||||
@@ -174,7 +188,8 @@ export function renderB09Estimation(root: HTMLElement): void {
|
||||
: spec.rowClass,
|
||||
onCellClick: (i, key) => {
|
||||
if (spec.onCell) spec.onCell(i, key);
|
||||
if (spec.link && !spec.editable?.(i, key)) goto(spec.link(i, key));
|
||||
if (spec.link && !spec.editable?.(i, key))
|
||||
goto(spec.link(i, key), made.root.querySelector(`tr[data-i="${i}"]`));
|
||||
},
|
||||
});
|
||||
made.root.dataset.b09Table = tab;
|
||||
@@ -216,9 +231,8 @@ export function renderB09Estimation(root: HTMLElement): void {
|
||||
return made;
|
||||
};
|
||||
const hint = spec.hint ? lineHint(note(spec.hint)) : null; // 236-1 안내 글 = 한 줄
|
||||
// 171-3 묶음 탭(일위대가표 · 중기사용료 · 단가산출근거) = 묶음 카드 기본 · 머리 단추로 표
|
||||
// 171-3 묶음 탭(일위대가표 · 중기사용료 · 단가산출근거) = 묶음 카드 기본 · 좌측 하단 스위치로 표(304-1)
|
||||
if (isGroupTab(tab)) {
|
||||
const redraw = (): void => show(tab);
|
||||
return void panel.replaceChildren(
|
||||
...renderGroupPanel({
|
||||
tab,
|
||||
@@ -227,7 +241,6 @@ export function renderB09Estimation(root: HTMLElement): void {
|
||||
focus,
|
||||
goto,
|
||||
makeTable,
|
||||
redraw,
|
||||
setTools: bars.setTools,
|
||||
}),
|
||||
);
|
||||
@@ -321,9 +334,8 @@ export function renderB09Estimation(root: HTMLElement): void {
|
||||
// 카드 = 좌측 패널 공용 카드(269 · 「구조물 배치」 양식)
|
||||
const section = createPanelCard("데이터 점검");
|
||||
section.append(refresh, check.list);
|
||||
const actions = document.createElement("div");
|
||||
actions.className = "ui-sidebar-actions";
|
||||
actions.append(save, reset);
|
||||
// 303-1 · 304-1 하단 고정 줄 — [박스보기|호표보기] · [하이퍼링크|팝업] · 초안 단추 · [?][저장][내역 초기화]
|
||||
const actions = createDock([save, reset], () => isGroupTab(current) && show(current));
|
||||
const left = document.createElement("div");
|
||||
left.className = "ui-sidebar-panel b09-options";
|
||||
left.append(section, ...DRAFT.panels.map((make) => make(() => user, setUser)), actions);
|
||||
|
||||
@@ -123,12 +123,14 @@ export type TabBuild = (ctx: TabContext) => TabTable | string;
|
||||
|
||||
/** 초안 탭(252 · `draft/*_UI.ts` 가 TABS · TAB_GROUPS 와 함께 넣음 · 폴더째 지우면 빈 채로) —
|
||||
* builders = 탭 짜기 · extend = 브라우저 셈 곁(셈 결과에 더함) ·
|
||||
* panels = 왼쪽 패널 카드(264 · 「데이터 점검」 아래 · 지금 사용자 값 · 고침 → 캐시 · 다시 셈) */
|
||||
* panels = 왼쪽 패널 카드(264 · 「데이터 점검」 아래 · 지금 사용자 값 · 고침 → 캐시 · 다시 셈) ·
|
||||
* actions = 좌측 하단 고정 줄 단추(303-1 · [저장] 줄 위 한 줄 · 비면 줄 없음) */
|
||||
export const DRAFT: {
|
||||
builders: Record<string, TabBuild>;
|
||||
extend: Array<(payload: Row, user: Row, sheets: Row) => void>;
|
||||
panels: Array<(user: () => Row, setUser: (next: Row) => void) => HTMLElement>;
|
||||
} = { builders: {}, extend: [], panels: [] };
|
||||
actions: Array<() => HTMLElement>;
|
||||
} = { builders: {}, extend: [], panels: [], actions: [] };
|
||||
|
||||
/** 201-1 호표 묶음 레벨(보기 전용) — 머리 · 합계(제목 줄) = 1 · 그 밖 = 2 · key 열을 들여씀 */
|
||||
export const blockLevel = (key: string, top: (index: number) => boolean): TabTable["level"] => ({
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
/* =============================================================================
|
||||
* B09_Estimation_UI_Tabs_Groups.ts
|
||||
* B09 묶음 탭 셋(PLAN 171-3) — 일위대가표 · 중기사용료 · 단가산출근거 = 공용 묶음 카드(171-2) 기본 ·
|
||||
* [표로 보기] = 공용 표 그대로(맞은편 단추 = [박스보기]).
|
||||
* 호표보기 = 공용 표 그대로 · 고름 = 좌측 하단 [박스보기|호표보기] 한 벌(304-1 · 셋 탭 함께).
|
||||
*
|
||||
* 카드 = 탭 표 줄(`TabTable.rows`)을 호표(줄 키 앞머리)마다 묶음 — 값 · 풍선 · 누름 이동은 표와 같음
|
||||
* 머리 줄 = 묶음 첫 줄 · 합계 줄 = 키 끝 `|합계` · `|총계` · 그 밖 = 몸 줄(표 줄 번호 그대로)
|
||||
* 목록표에서 온 호표(181-2) = 박스 붉은 외곽선 · 표 보기 = 그 호표 줄 구간 전체 붉은 외곽선
|
||||
* 248-1 빨강 하나(공용 고름 강조 안 붙임 · 스크롤만) · 줄 · 카드 누름 · 방향키 · Esc = 빨강 풂 ·
|
||||
* 누른 줄만 고름(is-focus) · Esc = 고름도 풂
|
||||
* 보기 · 가로 열 수 기억 = `b_page_state`(`b09-group-view` · `b09-group-cols` · 탭마다) · 못 읽으면 카드 · 2
|
||||
* 보기 단추 · 가로 열 수(202-1) = 페이지 보조 탭 줄 오른쪽(`setTools`) · 안내 글 = 표 · 카드 판 맨 아래
|
||||
* 보기 기억 = `b_page_state`(`b09-group-view` · 한 값 · 옛 탭별 값은 첫 읽기에 하나로) · 가로 열 수 =
|
||||
* `b09-group-cols`(탭마다) · 못 읽으면 카드 · 2
|
||||
* 가로 열 수(202-1) = 페이지 보조 탭 줄 오른쪽(`setTools`) · 안내 글 = 표 · 카드 판 맨 아래
|
||||
* 안내 글 길면 한 줄 줄임표 + 공용 호버로 전문(200-1)
|
||||
* 278-2 단가산출근거 박스 = 머리 줄(`|머리n`)을 운반 단가 창 ③ 칸 꼴로(`headSteps` 한 벌 · 몸 위 칸) ·
|
||||
* 출처 = 그 칸 맨 위 · 호표 줄 · 총계 = 그대로 · 묶음 자료(`tabGroups`) · 표는 안 바뀜
|
||||
* ========================================================================== */
|
||||
|
||||
import type { DataRow, DataTable } from "@ui/ui_template_data_table";
|
||||
import { createButton } from "@ui/ui_template_elements";
|
||||
import { attachHoverInfo } from "@ui/ui_template_hover_info";
|
||||
import {
|
||||
clampColumns,
|
||||
@@ -38,10 +38,8 @@ export const GROUP_TABS: TabId[] = ["dg", "machine_blocks", "sg"];
|
||||
|
||||
export const isGroupTab = (tab: TabId): boolean => GROUP_TABS.includes(tab);
|
||||
|
||||
/** 단추 글 = 바꿀 보기 이름 */
|
||||
export const viewLabel = (view: GroupView): string => (view === "cards" ? "표로 보기" : "박스보기");
|
||||
|
||||
export const flipView = (view: GroupView): GroupView => (view === "cards" ? "table" : "cards");
|
||||
/** 단추 글 = 그 보기 이름(304-1 좌측 스위치) */
|
||||
export const viewLabel = (view: GroupView): string => (view === "cards" ? "박스보기" : "호표보기");
|
||||
|
||||
type Saved = Record<string, unknown> | null;
|
||||
|
||||
@@ -53,19 +51,39 @@ const saved = (name: "b09-group-view" | "b09-group-cols"): Saved => {
|
||||
}
|
||||
};
|
||||
|
||||
/** 고른 보기 — 모르는 값 · 못 읽음 = 카드 */
|
||||
export const readGroupView = (tab: TabId, kept: Saved = saved("b09-group-view")): GroupView =>
|
||||
kept?.[tab] === "table" ? "table" : "cards";
|
||||
/** 고른 보기(셋 탭 한 값) — 옛 탭별 값 = dg · 다른 탭 차례 · 모르는 값 · 못 읽음 = 카드 */
|
||||
export function readGroupView(kept: unknown = saved("b09-group-view")): GroupView {
|
||||
const old = kept && typeof kept === "object" ? (kept as Record<string, unknown>) : null;
|
||||
const value = old ? (old.dg ?? old.machine_blocks ?? old.sg) : kept;
|
||||
return value === "table" ? "table" : "cards";
|
||||
}
|
||||
|
||||
/** 이 화면 안 보기(기억이 안 돼도 고른 보기 유지) */
|
||||
let chosen: GroupView | null = null;
|
||||
|
||||
/** 지금 보기 — 첫 읽기에 옛 탭별 값을 한 값으로 옮겨 적음 */
|
||||
export function groupView(): GroupView {
|
||||
if (chosen) return chosen;
|
||||
const kept = saved("b09-group-view");
|
||||
chosen = readGroupView(kept);
|
||||
if (kept && typeof kept === "object") chooseGroupView(chosen);
|
||||
return chosen;
|
||||
}
|
||||
|
||||
export function chooseGroupView(view: GroupView): void {
|
||||
chosen = view;
|
||||
try {
|
||||
writeState("b09-group-view", view);
|
||||
} catch {
|
||||
// 기억 못 해도 이 화면 안에서는 고른 값 그대로
|
||||
}
|
||||
}
|
||||
|
||||
/** 고른 가로 열 수 — 1~4 · 모르는 값 = 2 */
|
||||
export const readGroupCols = (tab: TabId, kept: Saved = saved("b09-group-cols")): number =>
|
||||
clampColumns(kept?.[tab]);
|
||||
|
||||
export function rememberGroup(
|
||||
name: "b09-group-view" | "b09-group-cols",
|
||||
tab: TabId,
|
||||
value: unknown,
|
||||
): void {
|
||||
export function rememberGroup(name: "b09-group-cols", tab: TabId, value: unknown): void {
|
||||
try {
|
||||
writeState(name, { ...(saved(name) ?? {}), [tab]: value });
|
||||
} catch {
|
||||
@@ -244,33 +262,18 @@ export function unpickOnRelease(root: HTMLElement): void {
|
||||
listenKeys();
|
||||
}
|
||||
|
||||
/** 이 화면 안 보기(기억이 안 돼도 바꾼 보기 유지) */
|
||||
const chosen = new Map<TabId, GroupView>();
|
||||
|
||||
/** 묶음 탭 — 카드 = 카드 판 + 안내 글 · 표 = 표 + 안내 글 · 보기 단추 · 열 수 = `setTools`(보조 탭 줄 오른쪽) */
|
||||
/** 묶음 탭 — 카드 = 카드 판 + 안내 글 · 표 = 표 + 안내 글 · 열 수 = `setTools`(보조 탭 줄 오른쪽) */
|
||||
export function renderGroupPanel(o: {
|
||||
tab: TabId;
|
||||
spec: TabTable;
|
||||
hint: HTMLElement | null;
|
||||
focus?: string;
|
||||
goto: (link: Link | null) => void;
|
||||
goto: (link: Link | null, anchor?: Element | null) => void;
|
||||
makeTable: (rowClass?: (index: number) => string | null) => DataTable;
|
||||
redraw: () => void;
|
||||
setTools: (tools: HTMLElement[]) => void;
|
||||
}): HTMLElement[] {
|
||||
const view = chosen.get(o.tab) ?? readGroupView(o.tab);
|
||||
const view = groupView();
|
||||
const lead = lineHint(o.hint);
|
||||
const toggle = createButton({
|
||||
label: viewLabel(view),
|
||||
variant: "ghost",
|
||||
size: "sm",
|
||||
onClick: () => {
|
||||
chosen.set(o.tab, flipView(view));
|
||||
rememberGroup("b09-group-view", o.tab, flipView(view));
|
||||
o.redraw();
|
||||
},
|
||||
});
|
||||
toggle.dataset.b09GroupView = view;
|
||||
const at = o.focus === undefined ? -1 : rowIndex(o.spec.rows, o.focus);
|
||||
if (view === "cards") {
|
||||
const first = o.spec.columns[0]?.key ?? "";
|
||||
@@ -281,19 +284,18 @@ export function renderGroupPanel(o: {
|
||||
: tabGroups(o.tab, o.spec),
|
||||
columns: readGroupCols(o.tab),
|
||||
onColumns: (n) => rememberGroup("b09-group-cols", o.tab, n),
|
||||
onPick: (i) => o.goto(o.spec.link?.(i, first) ?? null),
|
||||
onPick: (i) =>
|
||||
o.goto(o.spec.link?.(i, first) ?? null, cards.querySelector(`[data-at="${i}"]`)),
|
||||
focus: at,
|
||||
money: numberFormat(o.spec, "sum"),
|
||||
qty: numberFormat(o.spec, "qty"),
|
||||
actions: [toggle],
|
||||
placeTools: o.setTools, // 202-1 가로 열 수 · 보기 단추 = 보조 탭 줄 오른쪽
|
||||
placeTools: o.setTools, // 202-1 가로 열 수 = 보조 탭 줄 오른쪽
|
||||
});
|
||||
cards.dataset.b09Table = o.tab;
|
||||
live = { root: cards, unpick: (esc) => unpickCards(cards, esc) };
|
||||
listenKeys();
|
||||
return [cards, ...(lead ? [lead] : [])];
|
||||
}
|
||||
o.setTools([toggle]);
|
||||
let range = pickRange(o.spec.rows, o.focus);
|
||||
let chose = -1; // 누른 줄(다시 그려도 유지)
|
||||
const table = o.makeTable((i) => (i === chose ? "is-focus" : pickClass(range, i)));
|
||||
|
||||
@@ -62,20 +62,19 @@ def test_157_2_두_보기_같은_값():
|
||||
|
||||
|
||||
def test_157_2_단추_바꿈():
|
||||
out = _node(
|
||||
"\nconsole.log(JSON.stringify([viewLabel('table'), viewLabel('cards'),"
|
||||
" flipView('table'), flipView(flipView('table'))]));"
|
||||
)
|
||||
assert out == ["박스보기", "표로 보기", "cards", "table"]
|
||||
out = _node("\nconsole.log(JSON.stringify([viewLabel('table'), viewLabel('cards')]));")
|
||||
assert out == ["호표보기", "박스보기"] # 304-1 단추 글 = 그 보기 이름
|
||||
|
||||
|
||||
def test_157_2_기억_실패_때_카드():
|
||||
out = _node(
|
||||
"\nconst a = [readGroupView('sg', null), readGroupView('sg', { sg: 'x' }),"
|
||||
" readGroupView('sg', { dg: 'table' }), readGroupView('sg', { sg: 'table' })];"
|
||||
"\nconst a = [readGroupView(null), readGroupView({ sg: 'x' }), readGroupView({ dg: 'table' }),"
|
||||
" readGroupView({ sg: 'table' }), readGroupView('table'), readGroupView('x'),"
|
||||
" readGroupView({ dg: 'cards', sg: 'table' })];"
|
||||
"\nconsole.log(JSON.stringify(a));"
|
||||
)
|
||||
assert out == ["cards", "cards", "cards", "table"]
|
||||
# 304-1 한 값 · 옛 탭별 값 = dg 먼저 · 없으면 다른 탭 · 모르는 값 = 카드
|
||||
assert out == ["cards", "cards", "table", "table", "table", "cards", "cards"]
|
||||
|
||||
|
||||
def test_157_2_화면_잇기():
|
||||
|
||||
@@ -118,27 +118,27 @@ def test_단가산출근거_카드():
|
||||
assert out["links"] == [None, {"tab": "labor", "key": "LB2"}]
|
||||
|
||||
|
||||
def test_보기_기억_탭마다():
|
||||
def test_보기_한_값_열_수_탭마다():
|
||||
out = run(
|
||||
[COMMON, TABS, GROUPS],
|
||||
"\nconst a = [readGroupView('dg'), readGroupCols('dg')];"
|
||||
"\nrememberGroup('b09-group-view', 'dg', 'table');"
|
||||
"\nconst a = [readGroupView(), readGroupCols('dg')];"
|
||||
"\nkept.set('b09-group-view', JSON.stringify({ machine_blocks: 'table' }));" # 옛 탭별 값
|
||||
"\nrememberGroup('b09-group-cols', 'sg', 3);"
|
||||
"\nconst b = [readGroupView('dg'), readGroupView('sg'), readGroupView('machine_blocks'),"
|
||||
"\nconst b = [groupView(), readGroupView(), readGroupView(),"
|
||||
" readGroupCols('sg'), readGroupCols('dg'), readGroupCols('dg', { dg: 9 })];"
|
||||
"\nconsole.log(JSON.stringify({ a, b, kept: Object.fromEntries(kept),"
|
||||
" tabs: GROUP_TABS, other: ['bill', 'sum_material', 'sum_labor', 'sum_expense',"
|
||||
" 'sum_machine', 'dg_list'].map(isGroupTab) }));",
|
||||
)
|
||||
assert out["a"] == ["cards", 2] # 기본 = 카드 · 가로 2
|
||||
assert out["b"] == ["table", "cards", "cards", 3, 2, 2]
|
||||
assert json.loads(out["kept"]["b09-group-view"]) == {"dg": "table"}
|
||||
assert out["b"] == ["table", "table", "table", 3, 2, 2] # 304-1 셋 탭 한 값
|
||||
assert json.loads(out["kept"]["b09-group-view"]) == "table" # 첫 읽기에 옛 값 → 한 값
|
||||
assert json.loads(out["kept"]["b09-group-cols"]) == {"sg": 3}
|
||||
assert out["tabs"] == ["dg", "machine_blocks", "sg"]
|
||||
assert out["other"] == [False] * 6 # 집계표 넷 · 설계내역서 · 목록표 = 대상 밖
|
||||
|
||||
|
||||
def test_화면_보기_바꿈_누름_이동():
|
||||
def test_화면_보기_고름_누름_이동():
|
||||
out = run(
|
||||
[COMMON, TABS, f"{B09}Labor.ts", GROUPS],
|
||||
f"\nconst spec = buildDaegaBlocks({{ sheets: {{ daega_blocks: [{json.dumps(DG, ensure_ascii=False)}] }} }});"
|
||||
@@ -147,26 +147,25 @@ def test_화면_보기_바꿈_누름_이동():
|
||||
" redraw: () => redraws++, setTools: (t) => calls.push(t), makeTable: () => ({ root: document.createElement('table'),"
|
||||
" focusRow: (i) => focused.push(i) }) };"
|
||||
"\nconst [view] = renderGroupPanel(o);"
|
||||
"\nconst btn = calls[0].find((e) => e.dataset.b09GroupView);"
|
||||
"\nconst label = btn.textContent;"
|
||||
"\nconst label = calls.flat().filter((e) => e.dataset.b09GroupView).length;"
|
||||
"\nconst lit = view.querySelector('[data-at=\"3\"]').classList.contains('is-focus');"
|
||||
"\nconst box = view.querySelector('.ui-group-card').classList.contains('is-pick');"
|
||||
"\nview.querySelector('[data-at=\"2\"]').click();"
|
||||
"\nview.querySelector('[data-at=\"0\"]').click();"
|
||||
"\nbtn.click();"
|
||||
"\nconst before = calls.length; chooseGroupView('table');"
|
||||
"\nconst [view2] = renderGroupPanel(o);"
|
||||
"\nconsole.log(JSON.stringify({ label, table: view.dataset.b09Table, gone, lit, box, redraws,"
|
||||
" label2: calls[1].find((e) => e.dataset.b09GroupView).textContent, tag2: view2.tagName, focused,"
|
||||
" tools2: calls.length - before, tag2: view2.tagName, focused,"
|
||||
" kept: Object.fromEntries(kept) }));",
|
||||
RAW,
|
||||
)
|
||||
assert out["label"] == "표로 보기" and out["table"] == "dg" # 기본 = 카드
|
||||
assert out["label"] == 0 and out["table"] == "dg" # 기본 = 카드 · 304-1 탭 줄 보기 단추 없음
|
||||
assert out["gone"] == [{"tab": "labor", "key": "L00005"}, None] # 줄 누름 = 표와 같은 이동
|
||||
assert out["lit"] is False # 248-1 이동해 온 줄 = 빨강 하나(줄 강조 안 붙임)
|
||||
assert out["box"] is True # 그 호표 박스 = 붉은 외곽선(181-2)
|
||||
assert out["redraws"] == 1
|
||||
assert out["label2"] == "박스보기" and out["tag2"] == "TABLE" and out["focused"] == [3]
|
||||
assert json.loads(out["kept"]["b09-group-view"]) == {"dg": "table"}
|
||||
assert out["redraws"] == 0
|
||||
assert out["tools2"] == 0 and out["tag2"] == "TABLE" and out["focused"] == [3]
|
||||
assert json.loads(out["kept"]["b09-group-view"]) == "table"
|
||||
|
||||
|
||||
def test_잇기():
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""181-2 박스보기 — 이름 · 접기 없음 · 목록표에서 고른 호표 = 박스 붉은 외곽선 · 표 보기 = 줄 구간 전체.
|
||||
|
||||
- 단추 글 「박스보기」 ↔ 「표로 보기」 · 요약(접기) 보기 지움
|
||||
- 단추 글 「박스보기」 · 「호표보기」(304-1 좌측 스위치) · 요약(접기) 보기 지움
|
||||
- 표 보기 = 고른 호표 줄 구간(키 `|` 앞머리 같은 줄) 모두 `is-pick` · 첫 줄 `is-pick-top` · 끝 줄 `is-pick-end`
|
||||
- 붉은 색 = `--color-danger`(B06 횡단 카드 고름 꼴)
|
||||
"""
|
||||
@@ -52,7 +52,7 @@ def test_표_보기_줄_class_넘김():
|
||||
"\nconst o = { tab: 'dg', spec, hint: null, focus: 'B|1', goto: () => {}, redraw: () => {}, setTools: () => {},"
|
||||
" makeTable: (e) => { extra = e; return { root: document.createElement('table'),"
|
||||
" focusRow: () => {} }; } };"
|
||||
"\nchosen.set('dg', 'table');"
|
||||
"\nchooseGroupView('table');"
|
||||
"\nrenderGroupPanel(o);"
|
||||
"\nconsole.log(JSON.stringify(spec.rows.map((_, i) => extra(i))));",
|
||||
)
|
||||
@@ -63,7 +63,7 @@ def test_표_보기_줄_class_넘김():
|
||||
|
||||
def test_글_접기_없음_CSS_잇기():
|
||||
groups = (ROOT / GROUPS).read_text("utf-8")
|
||||
assert '"표로 보기" : "박스보기"' in groups and "호표별 보기" not in groups
|
||||
assert '"박스보기" : "호표보기"' in groups and "호표별 보기" not in groups # 304-1 글
|
||||
common = (ROOT / COMMON).read_text("utf-8")
|
||||
for gone in ("isSummary", "GROUP_SUMMARY_OVER", "is-closed", "모두 펼침", "모두 접기"):
|
||||
assert gone not in common
|
||||
|
||||
@@ -286,4 +286,4 @@ def test_내역_초기화_배선():
|
||||
assert "showConfirmDialog(" in block and "if (!ok) return;" in block
|
||||
assert "const { bill: _bill, ...rest } = user;" in block
|
||||
assert 'run("/estimation", "PUT"' in block and "{ user: rest }" in block
|
||||
assert "actions.append(save, reset);" in src
|
||||
assert "createDock([save, reset]," in src # 303-1 하단 고정 줄 맨 아래 줄
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""185-2 B09 묶음 탭 보기 바꿈 단추 글 한 줄 · 가로 열 수 단추와 같은 줄.
|
||||
"""185-2 B09 묶음 탭 가로 열 수 단추 한 줄 · 보기 고름 = 좌측 하단(304-1 · 탭 줄에 없음).
|
||||
|
||||
- 보기 바꿈 단추 · 가로 열 수 = 도구 묶음(`setTools` · 202-1 로 페이지 보조 탭 줄 오른쪽) · 카드 판 머리 띠 = 묶음 수만
|
||||
- 가로 열 수 = 도구 묶음(`setTools` · 202-1 로 페이지 보조 탭 줄 오른쪽) · 카드 판 머리 띠 = 묶음 수만
|
||||
- 단추 · 라벨 = `white-space: nowrap` · 도구 묶음 = 줄바꿈 없음(`flex-wrap: nowrap`)
|
||||
"""
|
||||
|
||||
@@ -31,7 +31,7 @@ def _panel(view: str, hint: str):
|
||||
return run(
|
||||
[COMMON, TABS, f"{B09}Labor.ts", GROUPS],
|
||||
f"\nconst spec = buildDaegaBlocks({{ sheets: {{ daega_blocks: [{json.dumps(DG, ensure_ascii=False)}] }} }});"
|
||||
f"\nchosen.set('dg', '{view}');"
|
||||
f"\nchooseGroupView('{view}');"
|
||||
"\nconst tools = [];"
|
||||
f"\nconst o = {{ tab: 'dg', spec, hint: {hint}, goto: () => {{}}, redraw: () => {{}},"
|
||||
" setTools: (t) => tools.push(...t),"
|
||||
@@ -39,7 +39,7 @@ def _panel(view: str, hint: str):
|
||||
"\nconst out = renderGroupPanel(o);"
|
||||
"\nconst btn = tools.find((e) => e.dataset.b09GroupView);"
|
||||
"\nconst bar = out[0].all('.ui-group-cards__bar')[0] ?? null;"
|
||||
"\nconsole.log(JSON.stringify({ n: out.length, first: out[0].className, label: btn.textContent,"
|
||||
"\nconsole.log(JSON.stringify({ n: out.length, first: out[0].className, label: btn ? btn.textContent : null, nTools: tools.length,"
|
||||
" cols: tools.flatMap((e) => e.all('[data-group-cols]')).map((b) => b.dataset.groupCols),"
|
||||
" barBtn: bar ? bar.all('[data-b09-group-view]').length : null,"
|
||||
" barCols: bar ? bar.all('[data-group-cols]').length : null }));",
|
||||
@@ -49,7 +49,7 @@ def _panel(view: str, hint: str):
|
||||
|
||||
def test_카드_보기_단추가_열수_단추와_같은_도구_묶음에():
|
||||
out = _panel("cards", "null")
|
||||
assert out["label"] == "표로 보기"
|
||||
assert out["label"] is None # 304-1 보기 단추 = 좌측 하단(탭 줄에 없음)
|
||||
assert out["cols"] == ["1", "2", "3", "4"] # 같은 묶음에 열 수 단추 넷
|
||||
assert out["barBtn"] == 0 and out["barCols"] == 0 # 카드 판 머리 띠에는 없음
|
||||
assert out["n"] == 1 # 안내 글 없으면 카드 판 하나뿐
|
||||
@@ -63,8 +63,8 @@ def test_카드_보기_안내_글은_카드_판_아래에():
|
||||
|
||||
def test_표_보기_단추는_도구_묶음에():
|
||||
out = _panel("table", "null")
|
||||
assert out["label"] == "박스보기" and out["n"] == 1
|
||||
assert out["cols"] == [] # 표 보기 = 열 수 단추 없음
|
||||
assert out["label"] is None and out["n"] == 1
|
||||
assert out["cols"] == [] and out["nTools"] == 0 # 표 보기 = 도구 없음
|
||||
|
||||
|
||||
def test_CSS_한_줄_nowrap():
|
||||
@@ -81,4 +81,4 @@ def test_CSS_한_줄_nowrap():
|
||||
page, ".b09-estimation__tools .ui-btn,\n.b09-estimation__tools .ui-group-cards__label"
|
||||
)
|
||||
assert "white-space: nowrap" in tools
|
||||
assert "actions: [toggle]" in (ROOT / GROUPS).read_text("utf-8")
|
||||
assert "toggle" not in (ROOT / GROUPS).read_text("utf-8") # 304-1 탭 줄 토글 걷음
|
||||
|
||||
@@ -36,7 +36,7 @@ def _panel(view: str, hint: str):
|
||||
return run(
|
||||
[COMMON, TABS, f"{B09}Labor.ts", GROUPS],
|
||||
f"\nconst spec = buildDaegaBlocks({{ sheets: {{ daega_blocks: [{json.dumps(DG, ensure_ascii=False)}] }} }});"
|
||||
f"\nchosen.set('dg', '{view}');"
|
||||
f"\nchooseGroupView('{view}');"
|
||||
f"\nconst hint = {hint};"
|
||||
"\nconst o = { tab: 'dg', spec, hint, goto: () => {}, redraw: () => {}, setTools: () => {},"
|
||||
" makeTable: () => ({ root: document.createElement('table'), focusRow: () => {} }) };"
|
||||
|
||||
@@ -80,7 +80,7 @@ def test_묶음_탭_단추가_보조_줄에_안내는_아래에():
|
||||
"\nconst hint = document.createElement('p');"
|
||||
"\nconst res = {};"
|
||||
"\nfor (const view of ['cards', 'table']) {"
|
||||
"\n chosen.set('dg', view);"
|
||||
"\n chooseGroupView(view);"
|
||||
"\n bars.draw('dg'); bars.setTools([]);"
|
||||
"\n const panel = renderGroupPanel({ tab: 'dg', spec, hint, goto: () => {}, redraw: () => {},"
|
||||
" makeTable: () => ({ root: document.createElement('table'), focusRow: () => {} }),"
|
||||
@@ -95,8 +95,9 @@ def test_묶음_탭_단추가_보조_줄에_안내는_아래에():
|
||||
"\nconsole.log(JSON.stringify(res));",
|
||||
RAW,
|
||||
)
|
||||
assert out["cards"]["inRow"] == ["표로 보기"] and out["cards"]["cols"] == 4
|
||||
assert out["table"]["inRow"] == ["박스보기"] and out["table"]["cols"] == 0
|
||||
# 304-1 보기 고름 = 좌측 하단 스위치(보조 줄에 없음) · 가로 열 수 = 보조 줄 그대로
|
||||
assert out["cards"]["inRow"] == [] and out["cards"]["cols"] == 4
|
||||
assert out["table"]["inRow"] == [] and out["table"]["cols"] == 0
|
||||
for view in ("cards", "table"):
|
||||
assert out[view]["inPanel"] == 0 # 판 안에는 단추 없음
|
||||
assert out[view]["hintLast"] and out[view]["n"] == 2 # 판 + 안내 줄(맨 아래)
|
||||
|
||||
@@ -112,8 +112,9 @@ def test_B09_표_만드는_길_한_곳_더블클릭_기본():
|
||||
calls = [
|
||||
p.name for p in _sources("B09_Estimation") if "createDataTable({" in p.read_text("utf-8")
|
||||
]
|
||||
assert calls == ["B09_Estimation_UI_Shell.ts"]
|
||||
shell = (ROOT / "B09_Estimation" / calls[0]).read_text("utf-8")
|
||||
# 303-2 팝업 창 = 보기 전용(고칠 칸 없음) · 옵션 = 같은 표 짜기(Builders)
|
||||
assert calls == ["B09_Estimation_UI_Popup.ts", "B09_Estimation_UI_Shell.ts"]
|
||||
shell = (ROOT / "B09_Estimation" / calls[1]).read_text("utf-8")
|
||||
assert "...tableOptionsOf(spec, tab)," in shell # 303-2 옵션 짜기 = Builders 한 곳
|
||||
builders = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Builders.ts").read_text("utf-8")
|
||||
assert "editOnDouble: spec.editOnDouble ?? true," in builders
|
||||
@@ -135,5 +136,6 @@ def test_B08_고칠_칸_있는_표_모두_editOnDouble():
|
||||
|
||||
def test_고르기_메뉴_이동_그대로():
|
||||
shell = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Shell.ts").read_text("utf-8")
|
||||
assert "if (spec.link && !spec.editable?.(i, key)) goto(spec.link(i, key));" in shell
|
||||
assert "if (spec.link && !spec.editable?.(i, key))" in shell # 303-2 누른 줄 = 팝업 자리
|
||||
assert 'goto(spec.link(i, key), made.root.querySelector(`tr[data-i="${i}"]`));' in shell
|
||||
assert "spec.choices?.(Number(cell.parentElement?.dataset.i), column.key)" in shell
|
||||
|
||||
@@ -60,7 +60,7 @@ def test_표_빨강_하나_누름_키_풂():
|
||||
" return { root, focusRow: (i, mark) => scrolled.push([i, mark]) }; };"
|
||||
"\nconst o = { tab: 'dg', spec, hint: null, focus: 'B|1', goto: () => {}, redraw: () => {},"
|
||||
" setTools: () => {}, makeTable };"
|
||||
"\nchosen.set('dg', 'table');"
|
||||
"\nchooseGroupView('table');"
|
||||
"\nconst marks = () => [root.children.map((tr) => tr.className || null),"
|
||||
" spec.rows.map((_, i) => extra(i))];"
|
||||
"\nconst out = {};"
|
||||
|
||||
@@ -205,6 +205,6 @@ def test_패널_훅_draft_없으면_빈칸():
|
||||
tabs = (ROOT / "B09_Estimation/B09_Estimation_UI_Tabs.ts").read_text("utf-8")
|
||||
shell = (ROOT / "B09_Estimation/B09_Estimation_UI_Shell.ts").read_text("utf-8")
|
||||
ui = (ROOT / "B09_Estimation/draft/B09_Draft_Schedule_UI.ts").read_text("utf-8")
|
||||
assert "{ builders: {}, extend: [], panels: [] }" in tabs # draft 없으면 카드 없음
|
||||
assert "{ builders: {}, extend: [], panels: [], actions: [] }" in tabs # draft 없으면 카드 없음
|
||||
assert "...DRAFT.panels.map((make) => make(() => user, setUser))" in shell
|
||||
assert "DRAFT.panels.push(" in ui
|
||||
|
||||
@@ -0,0 +1,329 @@
|
||||
"""303 B09 행 고름 = 팝업 · 하이퍼링크 스위치 · 좌측 하단 줄.
|
||||
|
||||
303-1 좌측 하단 고정 줄 — [박스보기|호표보기] · [하이퍼링크|팝업] · `DRAFT.actions`(비면 줄 없음) ·
|
||||
[?][저장][내역 초기화](도움말 줄) · 스위치 = pref global · 기본 = 팝업
|
||||
303-2 표 짜기 = `…_UI_Builders`(화면 · 팝업 · 인쇄 같은 옵션)
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from resources.tester.helper_b09_group_cards import COMMON, GROUPS, TABS, run # noqa: E402
|
||||
|
||||
B09 = ROOT / "B09_Estimation"
|
||||
DOCK = "B09_Estimation/B09_Estimation_UI_Dock.ts"
|
||||
BUILDERS = "B09_Estimation/B09_Estimation_UI_Builders.ts"
|
||||
|
||||
DOCK_JS = (
|
||||
"\nconst save = document.createElement('button'); save.textContent = '저장';"
|
||||
"\nconst reset = document.createElement('button'); reset.textContent = '내역 초기화';"
|
||||
"\nconst shape = (dock) => dock.children.map((r) => ({ sw: r.dataset.b09Switch ?? null,"
|
||||
" draft: 'b09DraftActions' in r.dataset, help: 'helpRow' in r.dataset,"
|
||||
" text: r.children.map((b) => b.textContent) }));"
|
||||
)
|
||||
|
||||
|
||||
def test_하단_줄_차례_초안_단추_없으면_줄_없음():
|
||||
out = run(
|
||||
[COMMON, TABS, GROUPS, DOCK],
|
||||
DOCK_JS + "\nconst a = createDock([save, reset], () => {});"
|
||||
"\nDRAFT.actions.push(() => { const b = document.createElement('button');"
|
||||
" b.textContent = '인쇄 미리보기'; return b; });"
|
||||
"\nconst b = createDock([save, reset], () => {});"
|
||||
"\nconsole.log(JSON.stringify({ cls: a.className, a: shape(a), b: shape(b) }));",
|
||||
)
|
||||
assert out["cls"] == "ui-sidebar-actions b09-dock" # 바닥 고정 = 공용 액션 줄 한 벌
|
||||
view = {"sw": "view", "draft": False, "help": False, "text": ["박스보기", "호표보기"]}
|
||||
link = {"sw": "link", "draft": False, "help": False, "text": ["하이퍼링크", "팝업"]}
|
||||
main = {"sw": None, "draft": False, "help": True, "text": ["저장", "내역 초기화"]}
|
||||
assert out["a"] == [view, link, main] # 초안 단추 없음 = 줄 없음
|
||||
draft = {"sw": None, "draft": True, "help": False, "text": ["인쇄 미리보기"]}
|
||||
assert out["b"] == [view, link, draft, main] # 초안 단추 = [저장] 줄 위
|
||||
|
||||
|
||||
def test_스위치_기본_팝업_누름_pref():
|
||||
out = run(
|
||||
[COMMON, TABS, GROUPS, DOCK],
|
||||
DOCK_JS + "\nconst dock = createDock([save, reset], () => {});"
|
||||
"\nconst row = dock.children[1];"
|
||||
"\nconst pressed = () => row.children.map((b) => b.attrs['aria-pressed']);"
|
||||
"\nconst a = [linkMode(), pressed()];"
|
||||
"\nrow.children[0].click();"
|
||||
"\nconst b = [linkMode(), pressed(), kept.get('b09-link-mode')];"
|
||||
"\nrow.children[1].click();"
|
||||
"\nconst c = [linkMode(), pressed(), kept.get('b09-link-mode')];"
|
||||
"\nkept.set('b09-link-mode', '\"x\"');"
|
||||
"\nconsole.log(JSON.stringify({ a, b, c, odd: linkMode() }));",
|
||||
)
|
||||
assert out["a"] == ["popup", ["false", "true"]] # 기본 = 팝업
|
||||
assert out["b"] == ["link", ["true", "false"], '"link"']
|
||||
assert out["c"] == ["popup", ["false", "true"], '"popup"']
|
||||
assert out["odd"] == "popup" # 모르는 값 = 팝업
|
||||
|
||||
|
||||
def test_DRAFT_actions_자리():
|
||||
tabs = (B09 / "B09_Estimation_UI_Tabs.ts").read_text("utf-8")
|
||||
assert "actions: Array<() => HTMLElement>;" in tabs
|
||||
assert "{ builders: {}, extend: [], panels: [], actions: [] }" in tabs
|
||||
state = (ROOT / "A00_Common/b_page_state.ts").read_text("utf-8")
|
||||
assert '"b09-link-mode": { bucket: "pref", scope: "global" },' in state
|
||||
css = (B09 / "B09_Estimation_UI_Page.css").read_text("utf-8")
|
||||
m = re.search(r"\.b09-dock\.ui-sidebar-actions\s*\{([^}]*)\}", css)
|
||||
assert m and "flex-direction: column" in m.group(1) # 줄 여럿 세로
|
||||
|
||||
|
||||
# 표 짜기 — 탭 짜기 18 개 대신 가짜(줄 하나) · 옵션 = 표 그대로 넘김
|
||||
FAKE_BUILDS = "".join(
|
||||
f"\nconst {name} = () => ({{ columns: [], rows: [{{ key: '{name}' }}] }});"
|
||||
for name in re.findall(
|
||||
r":\s*(build\w+),",
|
||||
(ROOT / BUILDERS).read_text("utf-8"),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def test_Builders_화면_같은_옵션():
|
||||
out = run(
|
||||
[TABS, BUILDERS],
|
||||
"\nDRAFT.builders.draft_x = () => '안내';"
|
||||
"\nconst spec = { columns: [{ key: 'a', label: 'A' }], rows: [{ key: 'r' }], frozen: 2,"
|
||||
" editable: () => true, onEdit: () => {}, rowClass: () => 'x', cellClass: () => 'y',"
|
||||
" level: { key: 'a', of: () => 1 }, mergeSame: ['a'], link: () => null, menu: () => [] };"
|
||||
"\nconst o = tableOptionsOf(spec, 'bill');"
|
||||
"\nconst plain = tableOptionsOf({ columns: [], rows: [], editOnDouble: false });"
|
||||
"\nconsole.log(JSON.stringify({ keys: Object.keys(o).sort(), caption: o.caption,"
|
||||
" same: ['columns', 'rows', 'editable', 'onEdit', 'rowClass', 'cellClass', 'level', 'mergeSame']"
|
||||
".every((k) => o[k] === spec[k]), frozen: o.frozen, total: o.total, empty: o.empty,"
|
||||
" dbl: [o.editOnDouble, plain.editOnDouble], key: [o.keyEdit, plain.keyEdit],"
|
||||
" plainCaption: plain.caption ?? null, plainFrozen: plain.frozen,"
|
||||
" bill: tabTableOf('bill', {}).rows[0].key, draft: tabTableOf('draft_x', {}),"
|
||||
" none: tabTableOf('nope', {}) }));",
|
||||
FAKE_BUILDS,
|
||||
)
|
||||
assert out["keys"] == sorted(
|
||||
[
|
||||
"columns",
|
||||
"rows",
|
||||
"total",
|
||||
"frozen",
|
||||
"empty",
|
||||
"caption",
|
||||
"editable",
|
||||
"editOnDouble",
|
||||
"keyEdit",
|
||||
"onEdit",
|
||||
"rowClass",
|
||||
"cellClass",
|
||||
"level",
|
||||
"mergeSame",
|
||||
]
|
||||
) # 누름 · 메뉴 · 풍선 없음(그리는 쪽 몫)
|
||||
assert out["caption"] == "설계내역서" and out["same"] is True
|
||||
assert out["frozen"] == 2 and out["total"] is None and out["empty"] == ""
|
||||
assert out["dbl"] == [True, False] # 243-1 기본 = 더블클릭
|
||||
assert out["key"] == [False, True] # 설계내역서(메뉴) Enter · F2 = 레벨 키 몫
|
||||
assert out["plainCaption"] is None and out["plainFrozen"] == 0
|
||||
assert out["bill"] == "buildBill" and out["draft"] == "안내" and out["none"] is None
|
||||
|
||||
|
||||
def test_화면이_Builders_옵션을_씀():
|
||||
shell = (B09 / "B09_Estimation_UI_Shell.ts").read_text("utf-8")
|
||||
assert "...tableOptionsOf(spec, tab)," in shell
|
||||
assert "const spec = tabTableOf(tab, ctx);" in shell
|
||||
assert "BUILDERS" not in shell
|
||||
|
||||
|
||||
# ── 303-2 팝업 창 ──────────────────────────────────────────────────────────
|
||||
POPUP = "B09_Estimation/B09_Estimation_UI_Popup.ts"
|
||||
|
||||
|
||||
def _fn(path: str, name: str) -> str:
|
||||
"""파일에서 함수 하나 꺼냄(맨 앞 `export` 뗌) — 이어 붙이면 이름이 부딪히는 파일 대신."""
|
||||
src = (ROOT / path).read_text("utf-8")
|
||||
m = re.search(rf"export function {name}\(.*?\n}}\n", src, re.S)
|
||||
assert m, name
|
||||
return m.group(0).replace("export ", "", 1)
|
||||
|
||||
|
||||
POPUP_PRELUDE = (
|
||||
FAKE_BUILDS
|
||||
+ "\nconst GAP = 6;\n"
|
||||
+ _fn("ui_template/ui_template_hover_info.ts", "placeHoverInfo")
|
||||
+ _fn("B09_Estimation/B09_Estimation_UI_Tabs_Bill_Level.ts", "subtreeEnd")
|
||||
+ r"""
|
||||
E.prototype.remove = function () {
|
||||
if (this.parent) this.parent.children = this.parent.children.filter((c) => c !== this);
|
||||
this.parent = null;
|
||||
};
|
||||
E.prototype.getBoundingClientRect = function () {
|
||||
return this.rect ?? { left: 0, top: 0, right: 0, bottom: 0, width: 400, height: 300 };
|
||||
};
|
||||
document.body = new E("body");
|
||||
const winKeys = [];
|
||||
const hashFns = [];
|
||||
globalThis.window = { innerWidth: 1200, innerHeight: 800,
|
||||
addEventListener: (t, f) => (t === "keydown" ? winKeys : hashFns).push(f) };
|
||||
const esc = () => {
|
||||
const ev = { key: "Escape", stopped: false, preventDefault() {}, stopPropagation() { ev.stopped = true; } };
|
||||
winKeys.forEach((f) => f(ev));
|
||||
return ev.stopped;
|
||||
};
|
||||
const tables = [];
|
||||
const createDataTable = (o) => {
|
||||
const root = document.createElement("div");
|
||||
o.rows.forEach((r, i) => {
|
||||
const tr = document.createElement("tr");
|
||||
tr.dataset.i = String(i);
|
||||
tr.rect = { left: 40, top: 300 + i * 28, right: 900, bottom: 328 + i * 28 };
|
||||
root.append(tr);
|
||||
});
|
||||
tables.push(o);
|
||||
return { root };
|
||||
};
|
||||
"""
|
||||
)
|
||||
|
||||
SCENE = r"""
|
||||
const col = [{ key: "name", label: "명칭", group: ["머리"] }];
|
||||
const specs = {
|
||||
bill: { columns: col, menu: () => [], level: { key: "name", of: (i) => [1, 2, 2, 1][i] },
|
||||
rows: [{ key: "b1", level: 1 }, { key: "b2", level: 2 }, { key: "b3", level: 2 }, { key: "b4", level: 1 }],
|
||||
rowClass: (i) => (i === 0 ? "is-title" : null),
|
||||
editable: (i, k) => k === "qty",
|
||||
link: (i) => ({ tab: "labor", key: "L1" }) },
|
||||
labor: { columns: col, rows: [{ key: "L0" }, { key: "L1" }, { key: "L2" }],
|
||||
link: (i) => (i === 1 ? { tab: "dg", key: "D1|1" } : null) },
|
||||
dg: { columns: col, rows: [{ key: "D1" }, { key: "D1|1" }, { key: "D1|합계" }, { key: "D2" }] },
|
||||
};
|
||||
const gone = [];
|
||||
const base = { specOf: (t) => specs[t] ?? null, go: (l) => gone.push(l) };
|
||||
const anchor = document.createElement("tr");
|
||||
anchor.rect = { left: 40, top: 72, right: 900, bottom: 100 };
|
||||
const boxes = () => document.body.children.map((b) => b.dataset.b09Popup);
|
||||
const rows = (k) => tables[k].rows.map((r) => r.key);
|
||||
const cls = (k) => tables[k].rows.map((_, i) => tables[k].rowClass(i));
|
||||
"""
|
||||
|
||||
|
||||
def _popup(body: str):
|
||||
return run([TABS, GROUPS, BUILDERS, POPUP], SCENE + body, POPUP_PRELUDE)
|
||||
|
||||
|
||||
def test_강조_범위_줄_수():
|
||||
out = _popup(
|
||||
"\nconsole.log(JSON.stringify(["
|
||||
"popupRange('bill', specs.bill, 'b1'), popupRange('bill', specs.bill, 'b4'),"
|
||||
"popupRange('labor', specs.labor, 'L1'), popupRange('dg', specs.dg, 'D1|1'),"
|
||||
"popupRange('labor', specs.labor, 'X')]));"
|
||||
)
|
||||
# 설계내역서 = 그 줄 + 자손 · 그 밖 = 한 줄 · 묶음 = 호표 구간 · 없음 = null
|
||||
assert out == [[0, 2], [3, 3], [1, 1], [0, 2], None]
|
||||
|
||||
|
||||
def test_창_열_제목_강조_줄_자리():
|
||||
out = _popup(
|
||||
"\nopenRowPopup({ ...base, link: { tab: 'bill', key: 'b1' }, anchor });"
|
||||
"\nconst box = document.body.children[0];"
|
||||
"\nconst o = tables[0];"
|
||||
"\nconsole.log(JSON.stringify({ boxes: boxes(), n: popupCount(), cls: box.className,"
|
||||
" head: box.children[0].children.map((e) => e.textContent), rows: rows(0), rowCls: cls(0),"
|
||||
" columns: o.columns === specs.bill.columns, caption: o.caption, total: o.total,"
|
||||
" editable: o.editable ?? null, lv: [0, 1, 2].map((i) => o.level.of(i)),"
|
||||
" table: box.children[1].dataset.b09Table, left: box.style.left, top: box.style.top }));"
|
||||
)
|
||||
assert out["boxes"] == ["bill"] and out["n"] == 1
|
||||
assert out["cls"] == "b09-estimation b09-popup" # 창 안 = b09 표 꼴(빨강 테두리)
|
||||
assert out["head"] == ["설계내역서", "탭으로", "×"]
|
||||
assert out["rows"] == ["b1", "b2", "b3"] # 그 줄 + 자손
|
||||
assert out["rowCls"] == ["is-title is-pick is-pick-top", "is-pick", "is-pick is-pick-end"]
|
||||
assert out["columns"] is True and out["caption"] == "설계내역서" # 열 제목(머리 여러 줄) 그대로
|
||||
assert out["total"] is None and out["editable"] is None # 합계 · 고칠 칸 없음
|
||||
assert out["lv"] == [1, 2, 2] and out["table"] == "bill"
|
||||
assert out["left"] == "40px" and out["top"] == "106px" # 누른 줄 아래(간격 6)
|
||||
|
||||
|
||||
def test_이어진_창_겹침_Esc_차례():
|
||||
out = _popup(
|
||||
"\nopenRowPopup({ ...base, link: { tab: 'bill', key: 'b1' }, anchor });"
|
||||
"\ntables[0].onCellClick(1, 'qty');" # 고칠 칸 = 안 엶
|
||||
"\nconst a = boxes();"
|
||||
"\ntables[0].onCellClick(1, 'name');" # 창 안 링크 줄 = 이어진 창
|
||||
"\nconst second = document.body.children[1];"
|
||||
"\ntables[1].onCellClick(0, 'name');" # 창 안 줄 0 = 원래 줄 1(L1) → dg
|
||||
"\nconst b = boxes();"
|
||||
"\nconst dg = rows(2);"
|
||||
"\nconst e1 = esc(); const c = boxes();"
|
||||
"\nconst e2 = esc(); const e3 = esc(); const e4 = esc();"
|
||||
"\nconsole.log(JSON.stringify({ a, b, dg, top2: second.style.top, c, e: [e1, e2, e3, e4],"
|
||||
" d: boxes(), gone }));"
|
||||
)
|
||||
assert out["a"] == ["bill"]
|
||||
assert out["b"] == ["bill", "labor", "dg"] # 겹쳐 위에(뒤 DOM = 위)
|
||||
assert out["dg"] == ["D1", "D1|1", "D1|합계"] # 호표 구간
|
||||
assert out["top2"] == "362px" # 그 줄(창 안 2번째 줄) 곁
|
||||
assert out["c"] == ["bill", "labor"] # Esc = 맨 위 하나
|
||||
assert out["e"] == [True, True, True, False] # 창 없으면 Esc 안 가로챔
|
||||
assert out["d"] == [] and out["gone"] == []
|
||||
|
||||
|
||||
def test_닫기_탭으로_새_누름_주소():
|
||||
out = _popup(
|
||||
"\nconst open = (key = 'b1') => openRowPopup({ ...base, link: { tab: 'bill', key }, anchor });"
|
||||
"\nopen(); tables[0].onCellClick(0, 'name');"
|
||||
"\nconst head = (k) => document.body.children[k].children[0].children;"
|
||||
"\nhead(0)[2].click();" # 아래 창 × = 그 창 + 위 창
|
||||
"\nconst a = boxes();"
|
||||
"\nopen(); tables[2].onCellClick(0, 'name'); open('b4');" # 화면 새 누름 = 있던 창 모두 닫고 새로
|
||||
"\nconst b = [boxes(), rows(4)];"
|
||||
"\nhead(0)[1].click();" # [탭으로] = 옮기고 모두 닫음
|
||||
"\nconst c = [boxes(), [...gone]];"
|
||||
"\nopen(); hashFns.forEach((f) => f());"
|
||||
"\nconst d = boxes();"
|
||||
"\nopenRowPopup({ ...base, link: { tab: 'labor', key: 'X' }, anchor });" # 보일 줄 없음 = 옮김
|
||||
"\nconsole.log(JSON.stringify({ a, b, c, d, e: [boxes(), gone.length] }));"
|
||||
)
|
||||
assert out["a"] == []
|
||||
assert out["b"] == [["bill"], ["b4"]]
|
||||
assert out["c"] == [[], [{"tab": "bill", "key": "b4"}]]
|
||||
assert out["d"] == [] # 주소 바뀜 = 모두 닫음
|
||||
assert out["e"] == [[], 2]
|
||||
|
||||
|
||||
def test_창_자리_화면_안():
|
||||
out = _popup(
|
||||
"\nconst v = { width: 1200, height: 800 };"
|
||||
"\nconsole.log(JSON.stringify(["
|
||||
"popupPlace({ left: 40, top: 700, right: 900, bottom: 728 }, { width: 400, height: 300 }, v),"
|
||||
"popupPlace({ left: 1000, top: 100, right: 1190, bottom: 128 }, { width: 400, height: 300 }, v),"
|
||||
"popupPlace({ left: 40, top: 400, right: 900, bottom: 428 }, { width: 400, height: 700 }, v)]));"
|
||||
)
|
||||
assert out[0] == {"x": 40, "y": 394} # 아래 모자람 = 위
|
||||
assert out[1] == {"x": 794, "y": 134} # 오른쪽 화면 안
|
||||
assert out[2] == {"x": 40, "y": 92} # 위아래 다 모자람 = 화면 안으로 밀어 넣음
|
||||
|
||||
|
||||
def test_화면_잇기_모드별_누름_길():
|
||||
shell = (B09 / "B09_Estimation_UI_Shell.ts").read_text("utf-8")
|
||||
assert (
|
||||
'if (linkMode() === "link") return show(link.tab, link.key);' in shell
|
||||
) # 하이퍼링크 그대로
|
||||
assert "openRowPopup({" in shell and "go: (to) => show(to.tab, to.key)," in shell
|
||||
assert "closePopups(); // 303-2 탭 바꿈" in shell # 탭 바꿈 = 모두 닫음
|
||||
assert 'goto(spec.link(i, key), made.root.querySelector(`tr[data-i="${i}"]`));' in shell
|
||||
assert "if (spec.link && !spec.editable?.(i, key))" in shell # 고칠 칸 = 안 옮김
|
||||
assert (
|
||||
"createCheckPanel(refresh, (link) => show(link.tab, link.key));" in shell
|
||||
) # 연결 없음 = 옮김
|
||||
groups = (B09 / "B09_Estimation_UI_Tabs_Groups.ts").read_text("utf-8")
|
||||
assert 'cards.querySelector(`[data-at="${i}"]`)' in groups # 박스 카드 누름도 같은 길
|
||||
popup = (ROOT / POPUP).read_text("utf-8")
|
||||
assert "...tableOptionsOf(spec, tab)," in popup # 화면과 같은 표 짜기
|
||||
css = (B09 / "B09_Estimation_UI_Page.css").read_text("utf-8")
|
||||
m = re.search(r"\.b09-estimation\.b09-popup\s*\{([^}]*)\}", css)
|
||||
assert m and "max-width: 90vw" in m.group(1) and "position: fixed" in m.group(1)
|
||||
@@ -0,0 +1,103 @@
|
||||
"""304-1 B09 박스보기 · 호표보기 = 좌측 하단 한 설정(묶음 탭 셋 함께).
|
||||
|
||||
- 한 값(`b09-group-view` · pref global)이 dg · machine_blocks · sg 셋에 함께 · 기본 = 박스(카드)
|
||||
- 옛 탭별 값(`{탭: 보기}`) = 첫 읽기에 한 값으로(dg · 없으면 다른 탭 · 없으면 기본) 옮겨 적음
|
||||
- 보조 탭 줄 보기 토글 걷음 · 가로 열 수 단추(박스 보기)는 그대로
|
||||
- 좌측 하단 스위치 [박스보기|호표보기] 누름 = 값 바꿈 · 다시 그림 부름
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from resources.tester.helper_b09_group_cards import COMMON, GROUPS, RAW, TABS, run # noqa: E402
|
||||
|
||||
LABOR = "B09_Estimation/B09_Estimation_UI_Tabs_Labor.ts"
|
||||
DOCK = "B09_Estimation/B09_Estimation_UI_Dock.ts"
|
||||
DG = {"id": "DG1", "no": 1, "name": "찰쌓기", "unit": "m2", "lines": [], "sum": 0}
|
||||
SPEC = (
|
||||
"\nconst spec = buildDaegaBlocks({ sheets: { daega_blocks: ["
|
||||
+ json.dumps(DG, ensure_ascii=False)
|
||||
+ "] } });"
|
||||
"\nconst tools = [];"
|
||||
"\nconst draw = (tab) => renderGroupPanel({ tab, spec, hint: null, goto: () => {},"
|
||||
" setTools: (t) => tools.push(...t),"
|
||||
" makeTable: () => ({ root: document.createElement('table'), focusRow: () => {} }) })[0];"
|
||||
"\nconst kinds = () => ['dg', 'machine_blocks', 'sg'].map((t) => draw(t).tagName);"
|
||||
)
|
||||
|
||||
|
||||
def test_한_설정_셋_탭_함께():
|
||||
out = run(
|
||||
[COMMON, TABS, LABOR, GROUPS],
|
||||
SPEC + "\nconst a = kinds();"
|
||||
"\nchooseGroupView('table');"
|
||||
"\nconst b = kinds();"
|
||||
"\nconsole.log(JSON.stringify({ a, b, kept: Object.fromEntries(kept),"
|
||||
" toggles: tools.filter((e) => e.dataset.b09GroupView).length,"
|
||||
" cols: tools.flatMap((e) => e.all('[data-group-cols]')).length }));",
|
||||
RAW,
|
||||
)
|
||||
assert out["a"] == ["DIV"] * 3 # 기본 = 박스
|
||||
assert out["b"] == ["TABLE"] * 3 # 한 값이 셋 탭 함께
|
||||
assert json.loads(out["kept"]["b09-group-view"]) == "table"
|
||||
assert out["toggles"] == 0 # 탭 줄 보기 토글 없음
|
||||
assert out["cols"] == 4 * 3 # 박스 보기 가로 열 수 단추 = 탭 줄 그대로
|
||||
|
||||
|
||||
def test_옛_탭별_값_첫_읽기에_한_값으로():
|
||||
cases = [
|
||||
({"dg": "table", "sg": "cards"}, "table"),
|
||||
({"dg": "cards", "sg": "table"}, "cards"), # dg 먼저
|
||||
({"sg": "table"}, "table"), # dg 없으면 다른 탭
|
||||
({"machine_blocks": "table", "sg": "cards"}, "table"),
|
||||
({}, "cards"), # 없으면 기본
|
||||
]
|
||||
for old, want in cases:
|
||||
out = run(
|
||||
[COMMON, TABS, LABOR, GROUPS],
|
||||
"\nkept.set('b09-group-view', "
|
||||
+ json.dumps(json.dumps(old))
|
||||
+ ");"
|
||||
+ SPEC
|
||||
+ "\nconst got = kinds();"
|
||||
"\nconsole.log(JSON.stringify({ got, kept: Object.fromEntries(kept) }));",
|
||||
RAW,
|
||||
)
|
||||
assert out["got"] == ["TABLE" if want == "table" else "DIV"] * 3, old
|
||||
assert json.loads(out["kept"]["b09-group-view"]) == want, old # 옮겨 적음
|
||||
|
||||
|
||||
def test_좌측_스위치_누름():
|
||||
out = run(
|
||||
[COMMON, TABS, GROUPS, DOCK],
|
||||
"\nlet redraws = 0;"
|
||||
"\nconst dock = createDock([document.createElement('button')], () => redraws++);"
|
||||
"\nconst row = dock.children[0];"
|
||||
"\nconst pressed = () => row.children.map((b) => b.attrs['aria-pressed']);"
|
||||
"\nconst labels = row.children.map((b) => b.textContent);"
|
||||
"\nconst a = pressed();"
|
||||
"\nrow.children[0].click();" # 이미 박스 = 그대로
|
||||
"\nconst b = [redraws, pressed()];"
|
||||
"\nrow.children[1].click();"
|
||||
"\nconsole.log(JSON.stringify({ name: row.dataset.b09Switch, labels, a, b, c: [redraws, pressed()],"
|
||||
" view: groupView(), kept: Object.fromEntries(kept) }));",
|
||||
)
|
||||
assert out["name"] == "view" and out["labels"] == ["박스보기", "호표보기"]
|
||||
assert out["a"] == ["true", "false"] # 기본 = 박스보기
|
||||
assert out["b"] == [0, ["true", "false"]]
|
||||
assert out["c"] == [1, ["false", "true"]] and out["view"] == "table"
|
||||
assert json.loads(out["kept"]["b09-group-view"]) == "table" # [저장] 없이 pref
|
||||
|
||||
|
||||
def test_잇기():
|
||||
groups = (ROOT / GROUPS).read_text("utf-8")
|
||||
assert "toggle" not in groups and "redraw" not in groups and "createButton" not in groups
|
||||
shell = (ROOT / "B09_Estimation/B09_Estimation_UI_Shell.ts").read_text("utf-8")
|
||||
assert "createDock([save, reset], () => isGroupTab(current) && show(current))" in shell
|
||||
state = (ROOT / "A00_Common/b_page_state.ts").read_text("utf-8")
|
||||
assert '"b09-group-view": { bucket: "pref", scope: "global" },' in state
|
||||
Reference in New Issue
Block a user