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:
@@ -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
|
||||
|
||||
@@ -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,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"],
|
||||
|
||||
@@ -55,4 +55,5 @@ export const ui_locales_m2 = {
|
||||
],
|
||||
M02_ActionFailed: ["안 됨 — {value}", "Failed — {value}"],
|
||||
M02_StructureNone: ["없음", "None"],
|
||||
M02_Back: ["돌아가기", "Back"],
|
||||
} as const;
|
||||
|
||||
Reference in New Issue
Block a user