feat(M02): 표 도구 막대 줄 더하기 · 줄 지우기 · 손 입력 열 뺌 · 상세 화면 돌아가기

- 표 부품 도구 막대에서 세 단추를 빼고 쓰이지 않게 된 addRow · deleteRow · canDeleteRow · 글자 정리(데이터 규칙 · 시험 견본은 그대로)
- 구조물 도면 · 면적산출 · 재료산출 상세 화면 제목 오른쪽 [돌아가기] — 구조물집계표로 돌아가 그 열을 고른 채(카드 셋) · 저장 안 한 고침은 떠나기 확인 창
- 표 부품 손잡이 selectColumn

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EUypcnp5d1gU2aeKh9F2H7
This commit is contained in:
2026-09-29 08:41:02 +09:00
co-authored by Claude Opus 5.5
parent 7cac596afb
commit afcb59d11b
6 changed files with 39 additions and 65 deletions
@@ -53,6 +53,7 @@ interface EditorHandle {
interface SheetEditorHandle extends EditorHandle {
getDoc: () => { 열: StructureColumn[] };
recalc: () => void;
selectColumn: (colId: string) => void;
}
type Mods = Record<string, () => Promise<unknown>>;
@@ -104,6 +105,8 @@ export function createMain(
const title = el("h2", { className: "m02-main__title", text: L("M02_PickTemplate") });
const reload = createButton({ label: L("M02_Reload"), variant: "ghost" });
const save = createButton({ label: L("M02_Save"), variant: "filled" });
// 구조물 도면 · 면적산출 · 재료산출 상세 화면 → 구조물집계표(그 열 고른 채)
const back = createButton({ label: L("M02_Back"), variant: "ghost", size: "sm" });
const slot = el("div", { className: "m02-main__slot" }); // 도면 양식의 작도 영역 칸이 들어옴
const notice = el("div", { className: "m02-main__notice", attrs: { hidden: "" } });
const comboSlot = el("div", { className: "m02-main__combo" }); // 면적산출 — 알약 막대 위 조합 줄
@@ -112,7 +115,7 @@ export function createMain(
const root = el("div", {
className: "m02-main",
children: [
el("div", { className: "m02-main__head", children: [title, slot, reload, save] }),
el("div", { className: "m02-main__head", children: [title, back, slot, reload, save] }),
notice,
comboSlot,
host,
@@ -163,6 +166,7 @@ export function createMain(
const bar = (): void => {
reload.hidden = !sel;
save.hidden = !sel || !canEdit(sel.layer, isAdmin);
back.hidden = !sel || sel.layer !== "system" || !(sel.kind in NUMBER_FIELD);
};
const drop = (): void => {
@@ -457,6 +461,12 @@ export function createMain(
}
save.addEventListener("click", () => void doSave());
back.addEventListener("click", async () => {
const column = sel?.name;
if (!column || !(await confirmLeave())) return;
await open({ layer: "system", projectId: null, kind: "table", name: STRUCTURE_TABLE });
if (sel?.kind === "table") sheet?.selectColumn(column); // 그 사이 다른 것을 열었으면 안 고름
});
reload.addEventListener("click", async () => {
if (sel && (await confirmLeave())) void open(sel);
});
@@ -168,3 +168,20 @@ def test_리본_접힘_모드에서_탭으로_편_것은_밖을_누르면_다시
assert "if (collapsed) setPeek(true);" in ribbon # 탭 = 잠깐 펼침(접힘 모드 유지)
assert "addEventListener('pointerdown', close, true)" in ribbon # 리본 밖 = 다시 접힘
assert "setPeek(false);" in ribbon and "setRibbonCollapsed(!collapsed);" in ribbon # 단추
def test_표_도구_막대에_줄_더하기_줄_지우기_손_입력_열_없음():
sheet = _text(ROOT / "ui_template/sheet/ui_template_sheet.ts")
text = _text(ROOT / "ui_template/sheet/ui_template_sheet_text.ts")
for key in ("Row_Add", "Row_Delete", "Col_Hand"):
assert key not in sheet and key not in text, key
assert "selectColumn: (colId) =>" in sheet
def test_상세_화면_돌아가기는_집계표로_그_열_고른_채():
main = _text(MAIN)
assert "children: [title, back, slot, reload, save]" in main # 제목 바로 오른쪽
assert "!(sel.kind in NUMBER_FIELD)" in main # 구조물 도면 · 면적산출 · 재료산출만
body = main.split('back.addEventListener("click"')[1]
assert "confirmLeave()" in body and "STRUCTURE_TABLE" in body
assert "sheet?.selectColumn(column)" in body
+9 -39
View File
@@ -1,10 +1,10 @@
/* =============================================================================
* ui_template_sheet.ts
* 엑셀처럼 도는 표 부품 — `createSheet(칸, 문서, {mode, onChange, drawingLabel, areaLabel, materialLabel, onSelect, onPick})`
* → `{getDoc, setDoc, recalc, destroy}`.
* → `{getDoc, setDoc, recalc, selectColumn, destroy}`.
*
* master = 시스템 관리자 양식 고치기 — 머리 · 층 이름 · 단위 · 일위대가 · 줄 더하기·지우기 ·
* 층마다 열 더하기(「분류1 더하기」 … 층 이름 + 더하기) · 열 지우기 · 손 열.
* master = 시스템 관리자 양식 고치기 — 머리 · 층 이름 · 단위 · 일위대가 ·
* 층마다 열 더하기(「분류1 더하기」 … 층 이름 + 더하기) · 열 지우기.
* 도면 · 면적산출 · 재료산출 줄 = `drawingLabel` · `areaLabel` · `materialLabel` 글 ·
* 칸을 고르면 `onSelect` · `onPick` 을 주면 일위대가 · 도면 · 면적산출 · 재료산출 칸은
* 치지 않고 조회 후 선택.
@@ -18,13 +18,8 @@ import { createButton, el } from "@ui/ui_template_elements";
import { startEdit } from "../../M01_MasterData/M01_MasterData_UI_Cells";
import {
addLevel,
addRow,
canDeleteRow,
deleteColumn,
deleteRow,
isBlankPick,
isBoundColumn,
isCalcColumn,
KEY_PRICE,
KEY_UNIT,
navRows,
@@ -67,6 +62,8 @@ export interface SheetHandle {
getDoc(): SheetDoc;
setDoc(doc: SheetDoc): void;
recalc(): SheetResult;
/** 그 열 단위 칸을 고름(보이게 굴림 · `onSelect` 부름) — 없는 열이면 그대로 */
selectColumn(colId: string): void;
destroy(): void;
}
@@ -93,26 +90,10 @@ export function createSheet(host: HTMLElement, input: SheetDoc, opts: SheetOptio
host.replaceChildren(root);
const colOf = (id: string): SheetColumn | undefined => state.doc.열.find((c) => c.id === id);
const rowIdOf = (key: string | undefined): string | null =>
key?.startsWith("d:") ? key.slice(2) : null;
// ── 단추 ──────────────────────────────────────────────────────────
const button = (label: string, run: () => void): HTMLButtonElement =>
createButton({ label, variant: "ghost", onClick: run });
const rowAdd = button(st("Row_Add"), () => {
const id = addRow(state.doc, state.mode, rowIdOf(state.sel?.r));
state.sel = { r: `d:${id}`, c: state.sel?.c ?? state.doc.열[0]?.id ?? "" };
changed();
});
const rowDelete = button(st("Row_Delete"), () => {
const id = rowIdOf(state.sel?.r);
if (!id) return;
const keys = navRows(state.doc, state.mode);
deleteRow(state.doc, id);
const next = keys[keys.indexOf(`d:${id}`) + 1] ?? keys[keys.indexOf(`d:${id}`) - 1];
state.sel = next && state.sel ? { r: next, c: state.sel.c } : null;
changed();
});
/** `level` 층 새 칸 열 — 고른 열 무리 끝에(층 이름이 있으면 「새 분류2」 · 없으면 「새 열」) */
const addAt = (level: number): void => {
const at = state.sel?.c;
@@ -133,15 +114,7 @@ export function createSheet(host: HTMLElement, input: SheetDoc, opts: SheetOptio
state.sel = state.sel && next ? { r: state.sel.r, c: next.id } : null;
changed();
});
const colHand = button(st("Col_Hand"), () => {
const col = state.sel && colOf(state.sel.c);
if (!col) return;
if (col.손) delete col.손;
else col.손 = true;
changed();
});
toolbar.append(rowAdd, rowDelete);
if (opts.mode === "master") toolbar.append(colDelete, colHand);
if (opts.mode === "master") toolbar.append(colDelete);
// 층마다 열 더하기 단추 — 층이 둘 이상이면 둘째 층부터(「분류1 더하기」 …) · 하나면 「열 더하기」 ·
// 층 이름 · 층 수가 바뀌면 다시 만듦
@@ -164,15 +137,9 @@ export function createSheet(host: HTMLElement, input: SheetDoc, opts: SheetOptio
};
const syncToolbar = (): void => {
const rowId = rowIdOf(state.sel?.r);
const row = state.doc.줄.find((r) => r.id === rowId);
rowDelete.disabled = !row || !canDeleteRow(state.mode, row);
const col = state.sel ? colOf(state.sel.c) : undefined;
if (opts.mode === "master") syncLevels(!!col);
colDelete.disabled = !col || state.doc.열.length <= 1;
// 바인딩 · 계산 열은 손 입력이 안 먹음(프로젝트에서 잠김) — 켜지 못하게 · 이미 켜진 것은 끌 수 있게
colHand.disabled = !col || (!col.손 && (isBoundColumn(col) || isCalcColumn(col)));
colHand.classList.toggle("is-on", !!col?.손);
};
// ── 그리기 · 고름 ─────────────────────────────────────────────────
@@ -401,6 +368,9 @@ export function createSheet(host: HTMLElement, input: SheetDoc, opts: SheetOptio
render();
return structuredClone(state.result);
},
selectColumn: (colId) => {
if (colOf(colId)) select(KEY_UNIT, colId, true);
},
destroy: () => {
root.remove();
drag = null;
+1 -22
View File
@@ -1,6 +1,6 @@
/* =============================================================================
* ui_template_sheet_ops.ts
* 표 문서 고치기 — 줄 · 열 더하기 · 지우기 · 칸 값. 문서를 그 자리에서 바꿈(부른 쪽이 다시 그림).
* 표 문서 고치기 — 열 더하기 · 지우기 · 칸 값. 문서를 그 자리에서 바꿈(부른 쪽이 다시 그림).
* 화면(DOM)을 모름 — 격자(`ui_template_sheet.ts`)가 부름.
* ⚠ 타입 말고는 import 하지 않음 — 시험이 Node 로 이 파일을 바로 돌림(`test_sheet_pages.py`).
* ========================================================================== */
@@ -95,27 +95,6 @@ export function cellEditable(mode: SheetMode, col: SheetColumn, row: SheetRow):
return !!col.손 || (!!row.손 && col.꼴 === "글");
}
export function canDeleteRow(mode: SheetMode, row: SheetRow): boolean {
if (mode === "master") return true;
return !!row.손 && row.고정 !== ROW_FIXED_ALL;
}
/** `after` 줄 뒤에 빈 줄(없으면 맨 끝) — 새 줄 id. project 에서 더한 줄은 `손`. */
export function addRow(doc: SheetDoc, mode: SheetMode, after: string | null): string {
const id = freshId(
"r",
doc.줄.map((r) => r.id),
);
const row: SheetRow = { id, 값: {}, ...(mode === "project" ? { 손: true } : {}) };
const at = doc.줄.findIndex((r) => r.id === after);
doc.줄.splice(at < 0 ? doc.줄.length : at + 1, 0, row);
return id;
}
export function deleteRow(doc: SheetDoc, id: string): void {
doc.줄 = doc.줄.filter((r) => r.id !== id);
}
/** `level` 층 새 칸 열 — `after` 열과 그 층 위 머리(`머리[0..level-1]`)가 같은 이웃 끝에 둠 ·
* `[…위 머리, label, "" …]`(아래 층은 빈칸) · 같은 무리에 같은 글이 있으면 뒤에 수를 붙임
* (같은 머리는 가로로 합쳐져 새 열이 안 보임) · 새 열 id(`after` 가 없으면 null). */
@@ -6,11 +6,8 @@
import { currentLanguageIndex } from "@ui/ui_template_locale";
const TEXT = {
Row_Add: ["줄 더하기", "Add row"],
Row_Delete: ["줄 지우기", "Delete row"],
Col_Add: ["열 더하기", "Add column"],
Col_Delete: ["열 지우기", "Delete column"],
Col_Hand: ["손 입력 열", "Manual column"],
Col_New: ["새 열", "New column"],
Row_Unit: ["단위", "Unit"],
Row_Unit_Price: ["일위대가", "Unit price"],
+1
View File
@@ -55,4 +55,5 @@ export const ui_locales_m2 = {
],
M02_ActionFailed: ["안 됨 — {value}", "Failed — {value}"],
M02_StructureNone: ["없음", "None"],
M02_Back: ["돌아가기", "Back"],
} as const;