From 966669bb3489915de04b416110f71062e8fcb5b2 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 5 Oct 2026 12:28:04 +0900 Subject: [PATCH] =?UTF-8?q?feat(M02):=2045-3=20=EA=B5=AC=EC=A1=B0=EB=AC=BC?= =?UTF-8?q?=20=EC=A1=B0=ED=95=A9=20=EB=B6=84=EB=A5=98=20=ED=91=9C=20?= =?UTF-8?q?=EC=9D=BC=EC=9C=84=EB=8C=80=EA=B0=80=20=C2=B7=20=EB=8F=84?= =?UTF-8?q?=EB=A9=B4=20=C2=B7=20=EB=A9=B4=EC=A0=81=EC=82=B0=EC=B6=9C=20?= =?UTF-8?q?=ED=82=A4=20=EC=A4=84=20=EC=88=A8=EA=B9=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 표 부품 combo 일 때 마스터 번호 줄 안 그림(보기만 · 장부 · 저장 문서 그대로) - 번호 글 폭 맞춤(fitMeta) · 전용 CSS 없앰 · 방향키는 머리 줄만 - 시험 test_m02_combo_table 지금 코드에 맞춤 · 시나리오 ui_checks/45-3_분류표_키줄.js Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EYJJRCfHfdCGNhmXTjQKV7 --- resources/tester/test_m02_combo_table.py | 13 ++-- .../tester/ui_checks/45-3_분류표_키줄.js | 72 +++++++++++++++++++ ui_template/sheet/ui_template_sheet.css | 4 -- ui_template/sheet/ui_template_sheet.ts | 3 +- ui_template/sheet/ui_template_sheet_render.ts | 18 +---- 5 files changed, 86 insertions(+), 24 deletions(-) create mode 100644 resources/tester/ui_checks/45-3_분류표_키줄.js diff --git a/resources/tester/test_m02_combo_table.py b/resources/tester/test_m02_combo_table.py index 11619815b..f6d74b82b 100644 --- a/resources/tester/test_m02_combo_table.py +++ b/resources/tester/test_m02_combo_table.py @@ -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_머리_고침은_쓸_수_있는_층만_나무로(): diff --git a/resources/tester/ui_checks/45-3_분류표_키줄.js b/resources/tester/ui_checks/45-3_분류표_키줄.js new file mode 100644 index 000000000..a62c5f083 --- /dev/null +++ b/resources/tester/ui_checks/45-3_분류표_키줄.js @@ -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); +})(); diff --git a/ui_template/sheet/ui_template_sheet.css b/ui_template/sheet/ui_template_sheet.css index aa97203d9..6f2beb712 100644 --- a/ui_template/sheet/ui_template_sheet.css +++ b/ui_template/sheet/ui_template_sheet.css @@ -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); } diff --git a/ui_template/sheet/ui_template_sheet.ts b/ui_template/sheet/ui_template_sheet.ts index b8f38ebe6..418dcb355 100644 --- a/ui_template/sheet/ui_template_sheet.ts +++ b/ui_template/sheet/ui_template_sheet.ts @@ -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); diff --git a/ui_template/sheet/ui_template_sheet_render.ts b/ui_template/sheet/ui_template_sheet_render.ts index e9992bbf7..8ef33269c 100644 --- a/ui_template/sheet/ui_template_sheet_render.ts +++ b/ui_template/sheet/ui_template_sheet_render.ts @@ -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) mins.get(col.id) ?? 0, ); -/** 구조물 조합 — 일위대가 · 도면 · 면적산출 줄 글(키 · 번호)이 줄바꿈 없이 들게 최소 폭을 넓힘 - * (`.ui-sheet__meta td` 글꼴과 같게 잼). */ -function fitMeta(state: RenderState, cols: SheetColumn[], mins: Map): 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() : []), ],