feat(M02): 면적산출 범위 → 도면 표 화면 틀(41-3) — [도면으로 보내기] · 같은 열 도면에 넘겨 놓기 · 연결표 기록 · 다름 알림 [업데이트]
- 새 M02_MasterTemplete_AreaToDrawing.ts — 범위 · 글자 크기(기본 2.5) 창 · 저장 안 한 고침 먼저 저장 · 표 변환 길(41-1) · placeTable · updateTable(41-2 손잡이) · 표 여럿 = T번호 - UI_Main 갈고리(select · onArea · onDrawing · withLinks) · StructureDoc 연결표 칸 - 시험 test_m02_area_to_drawing.py(Node) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BVPp2o6Mh2FZJr72yW3ofZ
This commit is contained in:
@@ -135,6 +135,8 @@ export interface StructureDoc {
|
||||
열: string;
|
||||
도번: string;
|
||||
도면: { entities: unknown[] } & Record<string, unknown>;
|
||||
/** 면적산출 → 도면 표 연결 기록(41-3 · `AreaToDrawing`) — 표 style.연결 = id · 값 = 마지막 칸 글 */
|
||||
연결표?: { id: string; 시트: string; 범위: string; 글자크기: number; 값: (string | null)[][] }[];
|
||||
}
|
||||
|
||||
/** 시스템 층 구조물 도면 목록 */
|
||||
|
||||
@@ -0,0 +1,281 @@
|
||||
/* =============================================================================
|
||||
* M02_MasterTemplete_AreaToDrawing.ts
|
||||
* 면적산출 범위 → 도면 표(PLAN 41-3) — 편집 칸(`UI_Main`)은 갈고리만 · 몸은 여기.
|
||||
* ① 면적 상세 머리 [도면으로 보내기] — 고른 범위 · 저장 안 한 고침은 먼저 저장 물음 · 작은 창 글자 크기(기본 2.5)
|
||||
* → 서버 표 변환 길(`POST /layers/{층}/area/{열}/table` · 41-1)로 표 객체 받음 → 같은 페이지에서 그 열
|
||||
* 도면 상세로 옮기며 넘김(닫힘 변수 · 캐시 · 서버 안 씀) → 도면 손잡이 `placeTable`(놓기 도구 · 41-2)
|
||||
* ② 도면 [저장] — 문서 맨 위 `연결표[]` = {id(T1…), 시트, 범위, 글자크기, 값(마지막 칸 글)} · 표 style.연결 = id ·
|
||||
* 도면에서 지운 표는 기록에서 뺌
|
||||
* ③ 도면을 열 때 — 연결표마다 지금 면적 값과 마지막 값이 다르면 알림 한 줄 + [업데이트](`updateTable`)
|
||||
* ④ 표 여러 개 — id 마다 한 줄
|
||||
* ========================================================================== */
|
||||
|
||||
import { API_BASE_URL } from "@config/config_frontend";
|
||||
import {
|
||||
createButton,
|
||||
createInputField,
|
||||
el,
|
||||
showConfirmDialog,
|
||||
showToast,
|
||||
} from "@ui/ui_template_elements";
|
||||
import { openModal } from "@ui/ui_template_modal";
|
||||
import { getActiveOwner, type Layer, type StructureDoc } from "./M02_MasterTemplete_Api_Fetch";
|
||||
import type { Selection } from "./M02_MasterTemplete_UI_Main";
|
||||
|
||||
/** 표 칸 하나(CAD `TableCell` 꼴에서 쓰는 것만) */
|
||||
interface TableCell {
|
||||
text?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
/** CAD 표 객체 JSON(`TableEntity.toJson` 꼴에서 쓰는 것만) */
|
||||
interface TableEntityJson {
|
||||
type: string;
|
||||
shapeData: { cells: (TableCell | null)[][]; style?: Record<string, unknown> } & Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
/** 연결 기록 한 줄 — 도면 문서 맨 위 `연결표[]`(값 = 마지막으로 도면에 적힌 칸 글 · 다름 알림 기준) */
|
||||
export type TableLink = NonNullable<StructureDoc["연결표"]>[number];
|
||||
type LinkSpec = Omit<TableLink, "id" | "값">;
|
||||
|
||||
/** 도면 손잡이에 더해지는 표 함수(41-2) — 없으면 단추 · 알림이 조용히 빠짐 */
|
||||
interface TableHandle {
|
||||
placeTable?: (entity: TableEntityJson) => void;
|
||||
updateTable?: (link: string, cells: TableEntityJson["shapeData"]["cells"]) => void;
|
||||
}
|
||||
|
||||
const TEXT = {
|
||||
send: "도면으로 보내기",
|
||||
sendTip: "고른 범위를 이 열 도면에 표로 넣음 — 놓을 자리는 도면에서 누름",
|
||||
title: "도면으로 보내기",
|
||||
range: "범위",
|
||||
size: "글자 크기",
|
||||
ok: "보내기",
|
||||
close: "닫기",
|
||||
badSize: "글자 크기는 0 보다 큰 수",
|
||||
saveFirst: "저장 안 한 고침이 있음 — 저장하고 보낼까요?",
|
||||
saveLabel: "저장하고 보내기",
|
||||
failed: "표를 못 만듦 — {value}",
|
||||
place: "도면에서 표를 놓을 자리를 누름(Esc = 그만)",
|
||||
noPlace: "이 도면 화면은 표 놓기를 아직 못 함",
|
||||
stale: "면적산출 값이 바뀐 표 {n}개",
|
||||
update: "업데이트",
|
||||
updated: "표 {n}개를 지금 면적 값으로 바꿈 — [저장]하면 남음",
|
||||
} as const;
|
||||
|
||||
const SIZE = 2.5; // 표 글자 크기 기본(정함 41)
|
||||
const LINK = "연결"; // 표 style 안 표지 칸
|
||||
|
||||
const fill = (text: string, key: string, value: string | number): string =>
|
||||
text.replace(`{${key}}`, String(value));
|
||||
const why = (error: unknown): string => (error instanceof Error ? error.message : String(error));
|
||||
const enc = encodeURIComponent;
|
||||
|
||||
/** 범위 → 표 객체(41-1 서버 · 저장 안 함) — 지금 보는 층 · 주인 */
|
||||
async function fetchTable(layer: Layer, column: string, spec: LinkSpec): Promise<TableEntityJson> {
|
||||
const owner = getActiveOwner();
|
||||
const q = owner && layer !== "system" && layer !== "project" ? `?owner=${enc(owner)}` : "";
|
||||
const res = await fetch(`${API_BASE_URL}/m02/layers/${layer}/area/${enc(column)}/table${q}`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(spec),
|
||||
});
|
||||
const data = (await res.json().catch(() => ({}))) as {
|
||||
detail?: unknown;
|
||||
entity?: TableEntityJson;
|
||||
} & Partial<TableEntityJson>;
|
||||
if (!res.ok) {
|
||||
const d = data.detail;
|
||||
throw new Error(typeof d === "string" ? d : `HTTP ${res.status}`);
|
||||
}
|
||||
return (data.entity ?? data) as TableEntityJson;
|
||||
}
|
||||
|
||||
const isTable = (e: unknown): e is TableEntityJson =>
|
||||
!!e && typeof e === "object" && (e as { type?: unknown }).type === "Table";
|
||||
const linkOf = (e: TableEntityJson): string | null => {
|
||||
const id = e.shapeData?.style?.[LINK];
|
||||
return typeof id === "string" && id ? id : null;
|
||||
};
|
||||
const textsOf = (cells: TableEntityJson["shapeData"]["cells"]): (string | null)[][] =>
|
||||
(cells ?? []).map((row) => (row ?? []).map((c) => (c ? String(c.text ?? "") : null)));
|
||||
const sameTexts = (a: (string | null)[][], b: (string | null)[][]): boolean =>
|
||||
JSON.stringify(a) === JSON.stringify(b);
|
||||
|
||||
export interface AreaToDrawingOptions {
|
||||
/** 같은 페이지에서 그 열 도면 상세를 엶(`UI_Main.open`) */
|
||||
open: (sel: Selection) => Promise<void>;
|
||||
/** 지금 화면 [저장] */
|
||||
save: () => Promise<void>;
|
||||
/** 저장 안 한 고침이 남았나 */
|
||||
isDirty: () => boolean;
|
||||
/** 편집 칸 알림 줄 */
|
||||
showNotice: (nodes: (HTMLElement | string)[]) => void;
|
||||
}
|
||||
|
||||
export interface AreaToDrawing {
|
||||
/** 면적 시트 고름(`onSelect`) — 범위를 들고 있음 */
|
||||
select: (s: { 시트: string; 범위: string }) => void;
|
||||
/** 면적 상세가 섰음 — 머리 칸에 [도면으로 보내기](고칠 수 있는 층 · 프로젝트 층 아님) */
|
||||
onArea: (at: Selection, readOnly: boolean, slot: HTMLElement) => void;
|
||||
/** 도면 상세가 섰음 — 넘겨받은 표 놓기 · 연결표 다름 알림 */
|
||||
onDrawing: (handle: unknown, at: Selection, doc: StructureDoc) => void;
|
||||
/** 도면 저장 문서 — 맨 위 `연결표` 를 도면의 표에 맞춤 */
|
||||
withLinks: (doc: StructureDoc) => StructureDoc;
|
||||
}
|
||||
|
||||
export function createAreaToDrawing(opt: AreaToDrawingOptions): AreaToDrawing {
|
||||
let range: { 시트: string; 범위: string } | null = null;
|
||||
/** 면적 상세 → 도면 상세로 넘길 표(같은 열 · 같은 층일 때만 놓음) */
|
||||
let pending: { layer: Layer; column: string; entity: TableEntityJson; spec: LinkSpec } | null =
|
||||
null;
|
||||
/** 이 도면에서 새로 놓은 표의 연결(저장 때 기록) · 열어 둔 도면 */
|
||||
let fresh = new Map<string, LinkSpec>();
|
||||
let shown: string | null = null;
|
||||
|
||||
/** 글자 크기 작은 창 — 보내기면 크기 · 닫으면 null */
|
||||
const askSize = (spec: { 범위: string }): Promise<number | null> =>
|
||||
new Promise((resolve) => {
|
||||
let done = false;
|
||||
const finish = (v: number | null): void => {
|
||||
if (!done) resolve(v);
|
||||
done = true;
|
||||
};
|
||||
const size = createInputField({ label: TEXT.size, type: "number", value: String(SIZE) });
|
||||
size.input.step = "0.5";
|
||||
size.input.min = "0.1";
|
||||
const { close } = openModal({
|
||||
title: TEXT.title,
|
||||
closeLabel: TEXT.close,
|
||||
dialogClass: "m02-modal",
|
||||
beforeClose: async () => (finish(null), true),
|
||||
mount: (body) => {
|
||||
const ok = createButton({ label: TEXT.ok, variant: "filled" });
|
||||
ok.addEventListener("click", () => {
|
||||
const v = Number(size.input.value);
|
||||
if (!(v > 0)) return showToast(TEXT.badSize, "error");
|
||||
finish(v);
|
||||
close();
|
||||
});
|
||||
size.input.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Enter" && !event.isComposing) ok.click();
|
||||
});
|
||||
body.append(el("p", { text: `${TEXT.range} ${spec.범위}` }), size.root, ok);
|
||||
size.input.select();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
async function send(at: Selection): Promise<void> {
|
||||
if (!range) return;
|
||||
if (opt.isDirty()) {
|
||||
if (!(await showConfirmDialog(TEXT.saveFirst, TEXT.saveLabel))) return;
|
||||
await opt.save();
|
||||
if (opt.isDirty()) return; // 저장 실패 — 알림은 저장 길이 띄움
|
||||
}
|
||||
const picked = range;
|
||||
const size = await askSize(picked);
|
||||
if (size === null) return;
|
||||
const spec: LinkSpec = { 시트: picked.시트, 범위: picked.범위, 글자크기: size };
|
||||
let entity: TableEntityJson;
|
||||
try {
|
||||
entity = await fetchTable(at.layer, at.name, spec);
|
||||
} catch (error) {
|
||||
return showToast(fill(TEXT.failed, "value", why(error)), "error");
|
||||
}
|
||||
pending = { layer: at.layer, column: at.name, entity, spec };
|
||||
await opt.open({
|
||||
layer: at.layer,
|
||||
projectId: null,
|
||||
kind: "structure",
|
||||
name: at.name,
|
||||
label: at.label,
|
||||
});
|
||||
}
|
||||
|
||||
/** 다음 연결 id — 기록 · 도면 표지에 없는 가장 작은 T번호 */
|
||||
const nextId = (doc: StructureDoc, taken: Set<string>): string => {
|
||||
for (const row of doc.연결표 ?? []) taken.add(row.id);
|
||||
for (const e of doc.도면?.entities ?? []) if (isTable(e)) taken.add(linkOf(e) ?? "");
|
||||
let n = 1;
|
||||
while (taken.has(`T${n}`)) n += 1;
|
||||
return `T${n}`;
|
||||
};
|
||||
|
||||
/** 연결표마다 지금 면적 값 → 마지막 값과 다르면 알림 + [업데이트] */
|
||||
async function checkStale(handle: TableHandle, at: Selection, doc: StructureDoc): Promise<void> {
|
||||
const rows = doc.연결표 ?? [];
|
||||
if (!rows.length || !handle.updateTable) return;
|
||||
const key = `${at.layer}:${at.name}`;
|
||||
const got = await Promise.all(
|
||||
rows.map((row) =>
|
||||
fetchTable(at.layer, at.name, row).then(
|
||||
(e) => ({ row, e }),
|
||||
() => null,
|
||||
),
|
||||
),
|
||||
);
|
||||
if (shown !== key) return; // 그 사이 다른 화면
|
||||
const stale = got.filter(
|
||||
(x): x is { row: TableLink; e: TableEntityJson } =>
|
||||
!!x && !sameTexts(x.row.값 ?? [], textsOf(x.e.shapeData.cells)),
|
||||
);
|
||||
if (!stale.length) return;
|
||||
const update = createButton({ label: TEXT.update, variant: "filled", size: "sm" });
|
||||
update.addEventListener("click", () => {
|
||||
for (const { row, e } of stale) handle.updateTable?.(row.id, e.shapeData.cells);
|
||||
opt.showNotice([]);
|
||||
showToast(fill(TEXT.updated, "n", stale.length), "success");
|
||||
});
|
||||
opt.showNotice([fill(TEXT.stale, "n", stale.length), update]);
|
||||
}
|
||||
|
||||
return {
|
||||
select: (s) => (range = { 시트: s.시트, 범위: s.범위 }),
|
||||
onArea: (at, readOnly, slot) => {
|
||||
range = null;
|
||||
if (readOnly || at.layer === "project") return;
|
||||
const button = createButton({ label: TEXT.send, variant: "ghost", size: "sm" });
|
||||
button.title = TEXT.sendTip;
|
||||
button.dataset.areaToDrawing = "";
|
||||
button.addEventListener("click", () => void send(at));
|
||||
slot.append(button);
|
||||
},
|
||||
onDrawing: (raw, at, doc) => {
|
||||
const handle = raw as TableHandle;
|
||||
shown = `${at.layer}:${at.name}`;
|
||||
fresh = new Map();
|
||||
const give =
|
||||
pending && pending.layer === at.layer && pending.column === at.name ? pending : null;
|
||||
pending = null;
|
||||
if (give) {
|
||||
if (!handle.placeTable) showToast(TEXT.noPlace, "error");
|
||||
else {
|
||||
const id = nextId(doc, new Set());
|
||||
const style = { ...(give.entity.shapeData.style ?? {}), [LINK]: id };
|
||||
fresh.set(id, give.spec);
|
||||
handle.placeTable({ ...give.entity, shapeData: { ...give.entity.shapeData, style } });
|
||||
showToast(TEXT.place, "info");
|
||||
}
|
||||
}
|
||||
void checkStale(handle, at, doc);
|
||||
},
|
||||
withLinks: (doc) => {
|
||||
const old = new Map((doc.연결표 ?? []).map((row) => [row.id, row]));
|
||||
const rows: TableLink[] = [];
|
||||
for (const e of doc.도면?.entities ?? []) {
|
||||
if (!isTable(e)) continue;
|
||||
const id = linkOf(e);
|
||||
const spec = id ? (old.get(id) ?? fresh.get(id)) : undefined;
|
||||
if (!id || !spec || rows.some((r) => r.id === id)) continue;
|
||||
const { 시트, 범위, 글자크기 } = spec;
|
||||
rows.push({ id, 시트, 범위, 글자크기, 값: textsOf(e.shapeData.cells) });
|
||||
}
|
||||
// 기록이 없던 도면은 칸을 안 만듦(옛 문서 그대로)
|
||||
return rows.length || doc.연결표 ? { ...doc, 연결표: rows } : doc;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
} from "./M02_MasterTemplete_Structure";
|
||||
import { askSettle } from "./M02_MasterTemplete_TreePanel_Dialogs";
|
||||
import { buildAreaPanel, type AreaPanel } from "./M02_MasterTemplete_AreaPanel";
|
||||
import { createAreaToDrawing } from "./M02_MasterTemplete_AreaToDrawing";
|
||||
import type { SpreadsheetHandle } from "../A00_Common/spreadsheet/spreadsheet";
|
||||
import type { LinkOptions } from "../A00_Common/spreadsheet/spreadsheet_links";
|
||||
export interface Selection {
|
||||
@@ -220,6 +221,9 @@ export function createMain(
|
||||
back.hidden = !sel || !(sel.kind in NUMBER_FIELD);
|
||||
};
|
||||
|
||||
// 면적산출 범위 → 도면 표(41-3) — 몸은 `AreaToDrawing` · 여기는 갈고리만
|
||||
const toDrawing = createAreaToDrawing({ open, save: doSave, isDirty: busy, showNotice });
|
||||
|
||||
const drop = (): void => {
|
||||
editor?.destroy();
|
||||
editor = null;
|
||||
@@ -275,12 +279,13 @@ export function createMain(
|
||||
dirty = !readOnly;
|
||||
apanel?.refresh(); // 칸 값 · 연결이 바뀜 — 반영 금액을 바로 다시 곱함
|
||||
},
|
||||
onSelect: (s) => (picked = s.칸),
|
||||
onSelect: (s) => ((picked = s.칸), toDrawing.select(s)),
|
||||
links,
|
||||
},
|
||||
);
|
||||
editor = handle;
|
||||
sheetHandle = handle;
|
||||
toDrawing.onArea(at, readOnly, slot); // 머리 [도면으로 보내기]
|
||||
if (at.layer === "project") return;
|
||||
if (kind === "area") {
|
||||
// 오른쪽 일위대가 패널 — 담은 로직 + 자재목록(기준 · 반영) · 줄 누름 = 칸 지정 모달
|
||||
@@ -401,6 +406,7 @@ export function createMain(
|
||||
originMarker: at.kind === "structure",
|
||||
noArea: at.kind === "structure",
|
||||
});
|
||||
if (at.kind === "structure") toDrawing.onDrawing(editor, at, doc as StructureDoc);
|
||||
return;
|
||||
}
|
||||
empty(L("M02_NoDrawing"));
|
||||
@@ -460,7 +466,10 @@ export function createMain(
|
||||
// 면적산출은 스프레드시트 문서로 통째 갈아 끼우되 번호는 계약 밖(엔진이 모름)이라 옮겨 붙임.
|
||||
const doc =
|
||||
at.kind === "structure" && loaded
|
||||
? { ...(loaded as StructureDoc), 도면: edited as StructureDoc["도면"] }
|
||||
? toDrawing.withLinks({
|
||||
...(loaded as StructureDoc),
|
||||
도면: edited as StructureDoc["도면"],
|
||||
})
|
||||
: at.kind === "area" && loaded
|
||||
? { ...(loaded as Record<string, unknown>), ...(edited as Record<string, unknown>) }
|
||||
: edited;
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
"""면적산출 범위 → 도면 표(PLAN 41-3 · `M02_MasterTemplete_AreaToDrawing.ts`) — Node 로 TS 를 돌려 판정.
|
||||
|
||||
못박는 것
|
||||
- 면적 상세 [도면으로 보내기] = 고칠 수 있는 층만(읽기 · 프로젝트 층 단추 0).
|
||||
- 보냄 → 표 변환 길(POST …/area/{열}/table) → 같은 열 도면 상세를 엶 → placeTable(style.연결 = T번호).
|
||||
- 다른 열 도면이면 안 놓음 · 기존 기록 · 표지와 안 겹치는 id(T1 이 있으면 T2).
|
||||
- [저장] 문서 `연결표` = 도면에 남은 표만(지운 표는 빠짐) · 값 = 그 표 칸 글 · 기록 없던 도면은 칸 안 만듦.
|
||||
- 다시 열 때 지금 면적 값 ≠ 마지막 값이면 알림 + [업데이트] → updateTable(id, 칸).
|
||||
"""
|
||||
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
SCRIPT = r"""
|
||||
const fs = require("fs");
|
||||
const ts = require(process.argv[1] + "/config/node_modules/typescript");
|
||||
const src = fs.readFileSync(process.argv[1] + "/M02_MasterTemplete/M02_MasterTemplete_AreaToDrawing.ts", "utf8");
|
||||
const js = ts.transpileModule(src, {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||
}).outputText;
|
||||
// 화면 부품 · 길은 가짜 — 단추 · 창 · 알림만 흉내
|
||||
const made = [];
|
||||
const button = (o) => {
|
||||
const b = { label: o.label, title: "", dataset: {}, on: {}, addEventListener(k, f) { this.on[k] = f; } };
|
||||
made.push(b);
|
||||
return b;
|
||||
};
|
||||
const toasts = [];
|
||||
const stubs = {
|
||||
"@config/config_frontend": { API_BASE_URL: "/api" },
|
||||
"@ui/ui_template_elements": {
|
||||
createButton: button,
|
||||
createInputField: (o) => ({ root: {}, input: { value: o.value, step: "", min: "", select() {}, addEventListener() {} } }),
|
||||
el: (tag, o) => ({ tag, ...o }),
|
||||
showConfirmDialog: async () => true,
|
||||
showToast: (m, k) => toasts.push([k, m]),
|
||||
},
|
||||
"@ui/ui_template_modal": {
|
||||
// 창이 뜨자마자 [보내기]
|
||||
openModal: (o) => {
|
||||
const body = { parts: [], append(...x) { this.parts.push(...x); } };
|
||||
const res = { close() {} };
|
||||
setTimeout(() => o.mount(body, res.close), 0);
|
||||
setTimeout(() => made.at(-1).on.click(), 1);
|
||||
return res;
|
||||
},
|
||||
},
|
||||
"./M02_MasterTemplete_Api_Fetch": { getActiveOwner: () => null },
|
||||
};
|
||||
const box = { exports: {} };
|
||||
new Function("exports", "module", "require", js)(box.exports, box, (n) => stubs[n] ?? require(n));
|
||||
const { createAreaToDrawing } = box.exports;
|
||||
|
||||
const table = (texts, style = {}) => ({
|
||||
type: "Table",
|
||||
shapeData: { cells: texts.map((r) => r.map((t) => ({ text: t }))), style },
|
||||
});
|
||||
let now = [["1.5", "2"]];
|
||||
const calls = [];
|
||||
globalThis.fetch = async (url, init) => {
|
||||
calls.push([url, JSON.parse(init.body)]);
|
||||
return { ok: true, json: async () => table(now) };
|
||||
};
|
||||
|
||||
(async () => {
|
||||
const opened = [];
|
||||
const notices = [];
|
||||
const ad = createAreaToDrawing({
|
||||
open: async (s) => opened.push(s),
|
||||
save: async () => {},
|
||||
isDirty: () => false,
|
||||
showNotice: (n) => notices.push(n),
|
||||
});
|
||||
const slot = { kids: [], append(x) { this.kids.push(x); } };
|
||||
const area = { layer: "system", projectId: null, kind: "area", name: "item1", label: "기슭막이" };
|
||||
ad.onArea({ ...area, layer: "project" }, false, slot);
|
||||
ad.onArea(area, true, slot);
|
||||
const ro = slot.kids.length;
|
||||
ad.onArea(area, false, slot);
|
||||
ad.select({ 시트: "산출", 칸: "B2", 범위: "B2:C2" });
|
||||
await slot.kids[0].on.click();
|
||||
await new Promise((r) => setTimeout(r, 20));
|
||||
const sent = { calls: calls.slice(), opened: opened.slice() };
|
||||
|
||||
// 같은 열 도면 — 기록 T1 이 있으니 새 표는 T2
|
||||
const placed = [];
|
||||
const handle = { placeTable: (e) => placed.push(e), updateTable: (id, c) => placed.push(["u", id, c]) };
|
||||
const old = { id: "T1", 시트: "산출", 범위: "A1:A1", 글자크기: 3, 값: [["옛"]] };
|
||||
const drawing = (ents, links) => ({ 양식: "구조물도면", 종류: "structure", 판: 1, 열: "item1", 도번: "SD-1", 도면: { entities: ents }, ...(links ? { 연결표: links } : {}) });
|
||||
ad.onDrawing(handle, { ...area, kind: "structure" }, drawing([table([["옛"]], { 연결: "T1" })], [old]));
|
||||
await new Promise((r) => setTimeout(r, 20));
|
||||
const notice = notices.at(-1) ?? [];
|
||||
const upd = notice.find((x) => x && x.on);
|
||||
if (upd) upd.on.click();
|
||||
|
||||
// 저장 — T1 은 지움 · T2 만 남음 · 값 = 표 칸 글
|
||||
const saved = ad.withLinks(drawing([{ type: "Line" }, table([["9", "8"]], { 연결: "T2" })], [old]));
|
||||
const plain = ad.withLinks(drawing([table([["x"]])]));
|
||||
// 다른 열 도면이면 안 놓음
|
||||
const before = placed.length;
|
||||
ad.select({ 시트: "산출", 칸: "A1", 범위: "A1" });
|
||||
await slot.kids[0].on.click();
|
||||
await new Promise((r) => setTimeout(r, 20));
|
||||
ad.onDrawing(handle, { ...area, kind: "structure", name: "item2" }, drawing([]));
|
||||
console.log(JSON.stringify({ ro, buttons: slot.kids.length, sent, placed, notice: notice.map((x) => (typeof x === "string" ? x : x.label)), saved: saved.연결표, plain: "연결표" in plain, other: placed.length - before }));
|
||||
})();
|
||||
"""
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def got():
|
||||
if shutil.which("node") is None:
|
||||
pytest.skip("node 가 없음")
|
||||
proc = subprocess.run(
|
||||
["node", "-e", SCRIPT, str(ROOT)], capture_output=True, text=True, encoding="utf-8"
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
return json.loads(proc.stdout.strip().splitlines()[-1])
|
||||
|
||||
|
||||
def test_단추는_고칠_수_있는_층만(got):
|
||||
assert got["ro"] == 0 and got["buttons"] == 1
|
||||
|
||||
|
||||
def test_보냄은_표_변환_길_뒤_같은_열_도면을_엶(got):
|
||||
url, body = got["sent"]["calls"][0]
|
||||
assert url == "/api/m02/layers/system/area/item1/table"
|
||||
assert body == {"시트": "산출", "범위": "B2:C2", "글자크기": 2.5}
|
||||
assert got["sent"]["opened"][0]["kind"] == "structure"
|
||||
assert got["sent"]["opened"][0]["name"] == "item1"
|
||||
|
||||
|
||||
def test_놓을_표는_겹치지_않는_연결_id(got):
|
||||
first = got["placed"][0]
|
||||
assert first["shapeData"]["style"]["연결"] == "T2"
|
||||
|
||||
|
||||
def test_다시_열면_다름_알림과_업데이트(got):
|
||||
assert got["notice"][0].startswith("면적산출 값이 바뀐 표 1")
|
||||
assert got["notice"][1] == "업데이트"
|
||||
assert ["u", "T1", [[{"text": "1.5"}, {"text": "2"}]]] in got["placed"]
|
||||
|
||||
|
||||
def test_저장_연결표는_남은_표만(got):
|
||||
assert got["saved"] == [
|
||||
{"id": "T2", "시트": "산출", "범위": "B2:C2", "글자크기": 2.5, "값": [["9", "8"]]}
|
||||
]
|
||||
assert got["plain"] is False
|
||||
|
||||
|
||||
def test_다른_열_도면에는_안_놓음(got):
|
||||
assert got["other"] == 0
|
||||
|
||||
|
||||
def test_편집_칸은_갈고리만():
|
||||
main = (ROOT / "M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts").read_text("utf-8")
|
||||
assert main.count("toDrawing.") == 4 # select · onArea · onDrawing · withLinks
|
||||
Reference in New Issue
Block a user