feat(M02): 45-3 구조물 조합 분류 표 일위대가 · 도면 · 면적산출 키 줄 숨김

- 표 부품 combo 일 때 마스터 번호 줄 안 그림(보기만 · 장부 · 저장 문서 그대로)
- 번호 글 폭 맞춤(fitMeta) · 전용 CSS 없앰 · 방향키는 머리 줄만
- 시험 test_m02_combo_table 지금 코드에 맞춤 · 시나리오 ui_checks/45-3_분류표_키줄.js

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYJJRCfHfdCGNhmXTjQKV7
This commit is contained in:
2026-10-05 12:28:04 +09:00
co-authored by Claude Opus 5.5
parent a86a03932c
commit 966669bb34
5 changed files with 86 additions and 24 deletions
+9 -4
View File
@@ -64,17 +64,22 @@ def test_표_부품_combo_선택():
assert "combo?: { onHeadEdit?: (colId: string, level: number, name: string) => void };" in sheet
assert '...(ro || opts.combo ? [] : [el("p", { className: "ui-sheet__hint"' in sheet
assert (
"filter((k) => !opts.combo || k !== KEY_UNIT)" in sheet
) # 방향키가 숨은 단위 줄을 안 밟음
"const keys = opts.combo ? [KEY_UNIT] : navRows(state.doc, state.mode);" in sheet
) # 방향키가 숨은 단위 · 번호 줄을 안 밟음
assert 'head?.classList.add("is-head-picked")' in sheet
assert "if (th && opts.combo?.onHeadEdit) editComboHead(th);" in sheet
assert "if (opts.combo && sel.r === KEY_UNIT)" in sheet # F2 · Enter = 머리 이름 고침
render = _text(SHEET / "ui_template_sheet_render.ts")
assert "children: state.combo ? rows : [...rows, unit]" in render # 단위 줄 없음
assert "if (state.combo) fitMeta(state, cols, mins);" in render
# 45-3 일위대가 · 도면 · 면적산출 키 줄 안 보임(보기만)
assert (
'...(mode === "master" && p === 0 && !state.combo ? MASTER_ROWS.map(masterRow) : [])'
in render
)
assert "fitMeta" not in render
assert "열너비: {}" in render # 저장 폭 안 씀 — 자동
css = _text(SHEET / "ui_template_sheet.css")
assert ".ui-sheet--combo .ui-sheet__meta td" in css and "th.is-head-picked" in css
assert ".ui-sheet--combo .ui-sheet__meta td" not in css and "th.is-head-picked" in css
def test_머리_고침은_쓸_수_있는_층만_나무로():
@@ -0,0 +1,72 @@
/* 45-3 — 구조물 조합 상세 분류 표: 일위대가 · 도면 · 면적산출 키 줄 안 보임 · [저장] 뒤 장부 번호 그대로
* 시작 화면: M02 마스터 템플릿 · 마스터 층(#/m02-master-template?open=구조물조합&root=master) · 시스템 관리자
* 부르는 법: orca eval --expression "$(cat 이 파일)" (async 식 · JSON 글 · 20초 안)
* 쓰는 것: 표 [저장] 한 번(고침 없음) — 끝나면 resources/master_template 사본으로 되씀
* 기대 수치:
* - metaRows = 0 · refRowNames = 0(줄 머리 「일위대가 · 도면 · 면적산출」) · refCells = 0(data-r s:price|drawing|area)
* - headPick = true(머리 칸 누르면 열 고름 · 카드 셋) · keyMove = true(방향키로 옆 열)
* - book.same = true(저장 앞뒤 장부 줄의 키 · 열 · 도번 · 면적번 같음) · book.laborAfter = rows(일위대가번 모두 있음)
* (git 장부가 옛 꼴(재료번)이면 저장 때 서버 맞춤이 일위대가번으로 바꿈 — 보기와 무관)
*/
(async () => {
const w = (ms) => new Promise((r) => setTimeout(r, ms));
const until = async (fn, ms = 6000) => {
for (let t = 0; t < ms; t += 150) {
const v = fn();
if (v) return v;
await w(150);
}
return null;
};
const vis = (b) => b && !b.hidden && b.offsetParent !== null;
const sheet = await until(() => document.querySelector(".ui-sheet--combo"));
if (!sheet) return JSON.stringify({ error: "구조물 조합 표 없음" });
const out = {};
const gutters = () =>
[...sheet.querySelectorAll(".ui-sheet__gutter")].map((g) => g.textContent.trim());
out.metaRows = sheet.querySelectorAll(".ui-sheet__meta").length;
out.refRowNames = gutters().filter((t) => ["일위대가", "도면", "면적산출"].includes(t)).length;
out.refCells = sheet.querySelectorAll(
'[data-r="s:price"],[data-r="s:drawing"],[data-r="s:area"]',
).length;
out.gutters = gutters();
// 머리 칸 누르면 열 고름(카드) · 방향키 = 옆 열
const heads = [...sheet.querySelectorAll("thead th[data-col]")].filter(vis);
const deepest = Math.max(...heads.map((h) => Number(h.dataset.level)));
const leaves = heads.filter((h) => Number(h.dataset.level) === deepest);
leaves[0].click();
await w(400);
out.headPick = !!sheet.querySelector("th.is-head-picked");
const first = sheet.querySelector("th.is-head-picked")?.dataset.col;
sheet.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowRight", bubbles: true }));
await w(300);
const second = sheet.querySelector("th.is-head-picked")?.dataset.col;
out.keyMove = !!first && !!second && first !== second;
// [저장] 앞뒤 장부(구조물집계표조합) 번호
const book = () =>
fetch("/api/m02/layers/system/structure-combos", { credentials: "include" })
.then((r) => r.json())
.then((b) =>
(b.줄 ?? []).map((r) => ({
id: JSON.stringify([r.키, r.열, r.도번, r.면적번]),
su: r.일위대가번,
})),
);
const before = await book();
const save = [...document.querySelectorAll("button")].find(
(b) => vis(b) && b.innerText.trim() === "저장",
);
save.click();
await w(2500);
const after = await book();
out.book = {
rows: before.length,
same: JSON.stringify(before.map((r) => r.id)) === JSON.stringify(after.map((r) => r.id)),
laborAfter: after.filter((r) => r.su).length,
sample: after[0],
};
out.metaAfterSave = document.querySelectorAll(".ui-sheet--combo .ui-sheet__meta").length;
return JSON.stringify(out);
})();
-4
View File
@@ -141,10 +141,6 @@
outline-offset: -2px;
}
.ui-sheet--combo .ui-sheet__meta td {
white-space: nowrap;
}
.ui-sheet__table tr.is-fixed td {
border-bottom: 2px solid var(--color-border);
}
+2 -1
View File
@@ -222,7 +222,8 @@ export function createSheet(host: HTMLElement, input: SheetDoc, opts: SheetOptio
opts.onSelect?.(selectionOf(state.doc, r, c));
};
const move = (dr: number, dc: number): void => {
const keys = navRows(state.doc, state.mode).filter((k) => !opts.combo || k !== KEY_UNIT);
// 구조물 조합 = 머리만(단위 · 번호 줄 없음) — 방향키는 열만 옮김(머리 = 단위 줄 자리)
const keys = opts.combo ? [KEY_UNIT] : navRows(state.doc, state.mode);
const cols = state.doc.열;
if (!state.sel) {
if (keys.length && cols.length) select(keys[0], cols[0].id, true);
+3 -15
View File
@@ -47,7 +47,7 @@ export interface RenderState {
pick?: boolean;
/** 머리 · 층 이름을 못 고침(`lockHead`) — 칸 겉모양만. */
headLocked?: boolean;
/** 구조물 조합 표(`combo`) — 단위 줄 없음 · 열 폭 = 머리 · 번호 글이 한 줄에 들게(저장 폭 안 씀) ·
/** 구조물 조합 표(`combo`) — 단위 줄 · 일위대가 · 도면 · 면적산출 줄 없음 · 열 폭 = 머리(저장 폭 안 씀) ·
* `headEditable` = 머리 이름 칸을 고칠 수 있음(나무 이름 바꾸기로). */
combo?: boolean;
headEditable?: boolean;
@@ -68,18 +68,6 @@ const columnWidth = (doc: SheetDoc, col: SheetColumn, mins: Map<string, number>)
mins.get(col.id) ?? 0,
);
/** 구조물 조합 — 일위대가 · 도면 · 면적산출 줄 글(키 · 번호)이 줄바꿈 없이 들게 최소 폭을 넓힘
* (`.ui-sheet__meta td` 글꼴과 같게 잼). */
function fitMeta(state: RenderState, cols: SheetColumn[], mins: Map<string, number>): void {
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;
const font = `400 ${rem * 0.76}px ${getComputedStyle(document.body).fontFamily}`;
for (const col of cols) {
const texts = REF_ROWS.map((key) => refText(state, key, col.id));
const need = Math.ceil(Math.max(...texts.map((t) => textWidth(t, font)))) + 14;
mins.set(col.id, Math.max(mins.get(col.id) ?? 0, need));
}
}
/** 머리 칸 글꼴 — 폭 재기용(`.ui-sheet__table thead` 와 같게). */
function headFont(): string {
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;
@@ -113,7 +101,6 @@ export function renderSheet(state: RenderState): HTMLElement {
const names = columnNames(cols);
const font = headFont();
const mins = headMinWidths(cols, depth, font);
if (state.combo) fitMeta(state, cols, mins);
const widthDoc = state.combo ? { ...doc, 보기: { ...doc.보기, 열너비: {} } } : doc;
// 줄 머리 칸 폭 — 층 이름 · 단위 · 일위대가 · 도면 · 면적산출 · 합계 이름이 안 잘리게
const GUTTER = Math.min(
@@ -275,7 +262,8 @@ ${formulaLabel(formula, names)}`
for (const [p, page] of plan.entries()) {
const body = el("tbody", {
children: [
...(mode === "master" && p === 0 ? MASTER_ROWS.map(masterRow) : []),
// 구조물 조합 = 일위대가 · 도면 · 면적산출 키 줄 안 보임(보기만 · 번호는 장부 · 카드에)
...(mode === "master" && p === 0 && !state.combo ? MASTER_ROWS.map(masterRow) : []),
...page.rows.map((row, i) => dataRow(row, page.first + i)),
...(page.totals ? totalRows() : []),
],