feat(M02): 면적 값 고침이 저장 전 도면 패널 표에 바로 비침 · 이름 · 보내기 취소는 [저장] 때 서버가 기록(112-1·112-2)

- 112-1 면적 표 변환을 공용 파일(spreadsheet_area_table.ts)로 빼 서버 Node 와 브라우저가 한 벌로 씀 · 도면 패널이 칸 고침마다(값 바뀐 표만 · 펼친 동안만) CAD 연결 표 갈기를 보기 전용 갈음(preview)으로 보냄
- 112-2 보낸 범위 이름은 도면 [저장] 요청 안에서 서버가 면적 문서에 둠(놓기만 하고 저장 안 하면 안 생김) · [보내기 취소]는 화면에서만 빼고 면적 [저장] 때 서버가 연결표 줄 · 이름을 뺌 · /name · /unlink 길 삭제

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgChuAcyBqUkhHMTXomQC7
This commit is contained in:
2026-10-08 17:36:44 +09:00
co-authored by Claude Sonnet 5.5
parent 2a830980c7
commit 5638bd5101
17 changed files with 797 additions and 322 deletions
+72 -30
View File
@@ -22,7 +22,8 @@ from M02_MasterTemplete.M02_Template_Layers import drawing_sheets
logger = logging.getLogger(__name__)
LINKS = "연결표"
DROPPED = "지운연결"
# common_util_spreadsheet_node.ts `NAME_GONE` 과 같은 글자 — 이름이 지워졌을 때 까닭
CANCELS = "보내기취소" # 면적 [저장] 문서의 임시 칸 — 저장 안 함(112-2)
# spreadsheet_area_table.ts `NAME_GONE` 과 같은 글자 — 이름이 지워졌을 때 까닭
NAME_GONE = "이름없음"
@@ -43,35 +44,69 @@ def to_table(
return {k: got[0][k] for k in ("표", "시트", "범위", "값")}
def define_name(column: str, sheet: str, rng: str, name: str) -> None:
"""처음 도면으로 보낼 때(T 번호 정해진 뒤) 면적 시트에 보낸 범위 이름을 더함/갱신(104-1) —
`applyLinked`(행열 넣기 · 지우기)가 이 이름을 그대로 따라가게 함."""
book = store.read("area", column)["문서"]
try:
got = sheet_node.define_names([{"book": book, "시트": sheet, "범위": rng, "이름": name}])
except sheet_node.RecalcError as e:
raise store.StoreError(503, str(e)) from e
if not got[0].get("ok"):
raise store.StoreError(400, got[0].get("까닭"))
def fresh_names(path: Any, doc: dict[str, Any]) -> list[dict[str, Any]]:
"""도면 [저장] 문서의 새 연결 줄(저장돼 있던 파일에 없던 `연결`) 중 `이름`이 있는 것 —
면적 문서에 둘 이름(112-2)."""
old: set[Any] = set()
if path.is_file():
old = {
x.get("연결")
for x in json.loads(path.read_bytes()).get(LINKS) or []
if isinstance(x, dict)
}
rows = [x for x in doc.get(LINKS) or [] if isinstance(x, dict)]
return [x for x in rows if x.get("이름") and x.get("연결") not in old]
def define_names(column: str, rows: list[dict[str, Any]]) -> None:
"""도면 [저장] 요청 안에서(112-2) 새 연결 줄의 보낸 범위 이름을 면적 시트에 더함/갱신(104-1) —
`applyLinked`(행열 넣기 · 지우기)가 이 이름을 따라가게 함. 면적 문서가 없거나 못 풀면 건너뜀
(이름 없는 연결은 적힌 범위로 감)."""
path = store._path("area", column)
with store._LOCK:
atomic_write_json(path, got[0]["book"]) # 디스크 글 자체가 책(`store.read` 의 "문서")
def unlink(column: str, 연결: str) -> None:
"""이름표 메뉴 [보내기 취소](104-3) — 연결표에서 그 줄만 빼고 저장 · 도면 표(CAD 엔티티)는 그대로 남음
(지우기는 도면에서 — `withLinks` 가 다음 저장 때 표 없는 기록을 거둠)."""
path = store._path("structure", column)
if not path.is_file():
if not rows or not path.is_file():
return
book = json.loads(path.read_bytes())
try:
for row in rows:
ask = {
"book": book,
"시트": row.get("시트"),
"범위": row.get("범위"),
"이름": row["이름"],
}
got = sheet_node.define_names([ask])[0]
if got.get("ok"):
book = got["book"]
else:
logger.warning(
"보낸 범위 이름 건너뜀 %s %s — %s", column, row["이름"], got.get("까닭")
)
except sheet_node.RecalcError as e:
logger.warning("보낸 범위 이름 건너뜀 %s — %s", column, e)
return
with store._LOCK:
doc = json.loads(path.read_bytes())
rows = doc.get(LINKS) or []
kept = [x for x in rows if x.get("연결") != 연결]
if len(kept) == len(rows):
return
doc[LINKS] = kept
atomic_write_json(path, doc)
atomic_write_json(path, book) # 디스크 글 자체가 책(`store.read` 의 "문서")
def apply_cancels(column: str, doc: Any) -> tuple[Any, list[str]]:
"""면적 [저장] 문서의 `보내기취소[]`(112-2 · 이름표 메뉴 [보내기 취소])를 떼어 냄 — 그 연결 줄의 `이름`을
문서의 이름정의에서 뺌 → (문서, 연결 목록). 줄을 빼는 일은 `refresh_drawing(unlink=…)`."""
if not isinstance(doc, dict) or CANCELS not in doc:
return doc, []
ids = [x for x in doc[CANCELS] or [] if isinstance(x, str)]
doc = {k: v for k, v in doc.items() if k != CANCELS}
path = store._path("structure", column)
rows = json.loads(path.read_bytes()).get(LINKS) or [] if ids and path.is_file() else []
names = {x.get("이름") for x in rows if isinstance(x, dict) and x.get("연결") in ids}
names.discard(None)
if names:
doc["시트"] = [
{**s, "이름정의": [n for n in s["이름정의"] if n.get("이름") not in names]}
if isinstance(s, dict) and s.get("이름정의")
else s
for s in doc.get("시트") or []
]
return doc, ids
def _style_link(entity: Any) -> Any:
@@ -80,8 +115,10 @@ def _style_link(entity: Any) -> Any:
return ((entity.get("shapeData") or {}).get("style") or {}).get("연결")
def refresh_drawing(column: str) -> list[str]:
def refresh_drawing(column: str, unlink: Any = ()) -> list[str]:
"""지금 층 그 열 도면(모든 장)의 연결표마다 칸 글 다시 · 마지막 값 갱신 → 칸을 간 표의 `연결` 목록.
`unlink` = 면적 [저장] 때 [보내기 취소]한 연결 — 그 줄을 연결표에서 뺌
(도면 표는 남음 · 알림 없음 · 112-2).
이름(104-1)이 있으면 그 이름이 지금 가리키는 범위로 `범위` 도 갱신 — 이름이 지워졌으면(범위가
통째로 지워짐) 그 연결을 끊고(도면 표는 남음) `지운연결` 목록에 적어 다음에 열 때 알림."""
try:
@@ -91,8 +128,13 @@ def refresh_drawing(column: str) -> list[str]:
if not drawing.is_file() or not area.is_file():
return []
doc = json.loads(drawing.read_bytes())
links = [x for x in doc.get(LINKS) or [] if isinstance(x, dict) and x.get("연결")]
every = [x for x in doc.get(LINKS) or [] if isinstance(x, dict) and x.get("연결")]
links = [x for x in every if x["연결"] not in unlink]
if not links:
if len(links) != len(every): # 취소로 줄이 다 빠짐
doc[LINKS] = links
with store._LOCK:
atomic_write_json(drawing, doc)
return []
book = json.loads(area.read_bytes())
asks = [
@@ -139,7 +181,7 @@ def refresh_drawing(column: str) -> list[str]:
if dropped:
doc[LINKS] = kept
doc[DROPPED] = sorted(set(doc.get(DROPPED) or []) | set(dropped))
elif len(kept) != len(links):
elif len(kept) != len(every):
doc[LINKS] = kept
with store._LOCK:
atomic_write_json(drawing, doc)
@@ -5,12 +5,17 @@
* 83-3 — 머리 [면적산출로 보내기] = CAD 에 고른 범위 요청(`aislo:b08:export-table-request`) → 받은 표 자료
* (`aislo:b08:export-table` · CAD `integration/aislo-table-export.ts`)를 `onTable` 로(새 시트 만들기는 부른 쪽).
* CAD 명령 TOAREA 로 보낸 것도 같은 길. 고칠 수 없는 면적(읽기 층)이면 단추 없음.
* 112-1 — 면적 칸을 고치면(저장 전) 연결 표 값이 바로 따라 바뀜: 지금 시트의 칸 글을 서버와 같은 코드
* (`spreadsheet_area_table.ts`)로 브라우저가 만들어 CAD 연결 표 갈기 메시지(보기 전용 갈음)로 보냄 ·
* 패널이 펼쳐진 동안 · 값이 바뀐 표만 · 도면 문서는 안 고침([저장] 때는 서버 refresh_drawing 이 정본).
* ========================================================================== */
import { createButton, el } from "@ui/ui_template_elements";
import { readTemplate, type Layer, type StructureDoc } from "./M02_MasterTemplete_Api_Fetch";
import { mountStructureSheets } from "./M02_MasterTemplete_Drawing";
import type { DrawingTable } from "./M02_MasterTemplete_AreaFromDrawing";
import { sameValues, type TableLink } from "./M02_MasterTemplete_AreaToDrawing";
import type { Workbook } from "../A00_Common/spreadsheet/spreadsheet_types";
const TEXT = {
loading: "도면 불러오는 중…",
@@ -44,12 +49,25 @@ function hideChrome(frame: HTMLIFrameElement): void {
export interface AreaDrawingOptions {
/** 받은 표 자료 — 없으면(읽기 층) 보내기 단추 없음 */
onTable?: (table: DrawingTable) => void;
/** 면적 화면에서 [보내기 취소]한 연결(저장 전) — 실시간 값 갈기에서 뺌 */
cancelled?: () => ReadonlySet<string>;
}
/** 도면 손잡이 중 실시간 갈기에 쓰는 것(`mountStructureSheets` 의 `updateTable`) */
interface LiveHandle {
updateTable?: (link: string, cells: unknown[][], preview?: boolean) => Promise<number>;
}
const LIVE_MS = 150; // 칸 고침이 이어지는 동안 모았다가 한 번에
export interface AreaDrawing {
root: HTMLElement;
/** 패널이 펼쳐짐 — 처음 한 번만 읽고 그림 */
show: () => void;
/** 패널이 접힘 — 접힌 동안은 실시간 갈기 건너뜀 */
hide: () => void;
/** 면적 시트가 바뀜(저장 전) — 지금 책을 주면 연결 표 값을 따라 바꿈 */
sync: (book: () => Workbook | null | undefined) => void;
destroy: () => void;
}
@@ -64,6 +82,12 @@ export function createAreaDrawing(
let frame: HTMLIFrameElement | null = null;
let started = false;
let dead = false;
let open = false;
let rows: TableLink[] = [];
let bookOf: (() => Workbook | null | undefined) | null = null;
let timer: number | undefined;
/** 연결별로 마지막에 도면에 비춘 칸 글(없으면 저장된 `값`) */
const pushed = new Map<string, Record<string, string>>();
const note = (text: string): void =>
stage.replaceChildren(el("p", { className: "m02-apanel__note", text }));
@@ -85,8 +109,38 @@ export function createAreaDrawing(
};
window.addEventListener("message", onMessage);
/** 지금 시트 → 연결 표마다 칸 글을 만들어 달라진 것만 도면에 비춤(서버 `areaTable` 과 같은 코드) */
async function push(): Promise<void> {
const book = bookOf?.();
const handle = view as LiveHandle | null;
if (dead || !open || !book || !handle?.updateTable || !rows.length) return;
const { areaTable } = await import("../A00_Common/spreadsheet/spreadsheet_area_table");
for (const row of rows) {
if (opts.cancelled?.().has(row.연결)) continue;
try {
const got = areaTable({
book,
시트: row.시트,
범위: row.범위,
이름: row.이름,
글자크기: row.글자크기,
});
if (sameValues(pushed.get(row.연결) ?? row.값, got.값)) continue;
pushed.set(row.연결, got.값);
await handle.updateTable(row.연결, got.표.shapeData.cells, true).catch(() => 0);
} catch {
// 이름이 지워짐 · 범위 못 바꿈 — 표는 그대로(저장 때 서버가 판정)
}
}
}
const later = (): void => {
window.clearTimeout(timer);
timer = window.setTimeout(() => void push(), LIVE_MS);
};
const show = (): void => {
if (started) return;
open = true;
if (started) return later(); // 접힌 사이 바뀐 값을 따라잡음
started = true;
note(TEXT.loading);
readTemplate(layer, "structure", column, null)
@@ -95,6 +149,7 @@ export function createAreaDrawing(
const doc = got.문서 as StructureDoc | null;
if (!doc?.도면) return note(TEXT.none);
stage.replaceChildren();
rows = doc.연결표 ?? [];
// 보기 전용 + 고르기(누름 · 끌기) — 미리보기(끌기 = 화면 옮기기)는 고르기가 꺼져 있어 안 씀
const sheets = doc as unknown as Parameters<typeof mountStructureSheets>[1];
view = mountStructureSheets(stage, sheets, {
@@ -111,6 +166,7 @@ export function createAreaDrawing(
frame = stage.querySelector("iframe");
if (frame) hideChrome(frame);
send.disabled = !frame;
later();
})
.catch(() => !dead && note(TEXT.none));
};
@@ -118,8 +174,11 @@ export function createAreaDrawing(
return {
root,
show,
hide: () => (open = false),
sync: (book) => ((bookOf = book), later()),
destroy: () => {
dead = true;
window.clearTimeout(timer);
window.removeEventListener("message", onMessage);
view?.destroy();
root.remove();
@@ -4,7 +4,8 @@
* · 「일위대가」 = 그 열 일위대가의 담은 로직 + 자재목록(기준 · 반영) · 몸 = 일위대가 보기 갈래(`Labor_View`)
* · 「도면」 = 그 열 구조물 도면 보기 · 고르기(`AreaDrawing` · 처음 펼 때 그림) · [면적산출로 보내기] = 새 시트(83-3)
* · 줄 누름 = 칸 지정 모달(`AreaLink` · 그 줄 알약 키를 면적 칸에 잇기 · 풀기)
* · 시트 칸 값 · 연결이 바뀌면 `refresh` — 받은 기준에 바로 곱함 · 칸 주소도 넘김(서버 안 부름 · `Labor_View.setQty`)
* · 시트 칸 값 · 연결이 바뀌면 `refresh` — 받은 기준에 바로 곱함 · 칸 주소도 넘김(서버 안 부름 · `Labor_View.setQty`) ·
* 도면 패널의 연결 표 값도 저장 전에 바로 따라 바뀜(112-1 · `AreaDrawing.sync`)
* ========================================================================== */
import { showToast } from "@ui/ui_template_elements";
@@ -36,6 +37,8 @@ export interface AreaPanelOptions {
/** 알약 막대 항목 — 칸 지정 때 연결에 같이 적을 이름 · 단위 · 자재 */
pills: () => LinkItem[];
readOnly: boolean;
/** 면적 화면에서 [보내기 취소]한 연결(저장 전) — 도면 패널 실시간 갈기에서 뺌 */
cancelled?: () => ReadonlySet<string>;
}
export interface AreaPanel {
@@ -99,6 +102,7 @@ export function buildAreaPanel(opts: AreaPanelOptions): AreaPanel {
// 도면 → 면적 새 시트(83-3 · 한 번 · 연동 아님) — 고칠 수 없는 면적이면 보내기 단추 없음
const drawing = createAreaDrawing(opts.layer, opts.column, {
onTable: opts.readOnly ? undefined : (table) => void importDrawingTable(opts.sheet(), table),
cancelled: opts.cancelled,
});
const panels = createSidePanels({
prefKey: "m02-area-panels",
@@ -108,13 +112,16 @@ export function buildAreaPanel(opts: AreaPanelOptions): AreaPanel {
{ id: "labor", title: TEXT.labor, body: view.root },
{ id: "drawing", title: TEXT.drawing, body: drawing.root },
],
onChange: (open) => open === "drawing" && drawing.show(),
onChange: (open) => (open === "drawing" ? drawing.show() : drawing.hide()),
});
if (panels.current() === "drawing") drawing.show();
const root = panels.root;
// 처음 = 저장 안 한 시트 칸 값으로 셈(서버 calc 한 번)
const refresh = (): void => view.setQty(quantities(opts.sheet()), cellsOf(opts.sheet()));
const refresh = (): void => {
view.setQty(quantities(opts.sheet()), cellsOf(opts.sheet()));
drawing.sync(() => opts.sheet()?.getDoc()); // 저장 전 칸 값 → 도면 패널 표(112-1)
};
void view.show(opts.layer, opts.column, quantities(opts.sheet()));
refresh(); // 칸 주소는 지금 시트 것 — 보기가 저장본 대신 씀
return {
@@ -4,10 +4,13 @@
* ① 면적 상세 머리 [도면으로 보내기] — 고른 범위 · 저장 안 한 고침은 먼저 저장 물음 · 작은 창 글자 크기(기본 2.5)
* → 서버 표 변환 길(`POST /layers/{층}/area/{열}/table` · 41-1)로 표 객체 받음 → 같은 페이지에서 그 열
* 도면 상세로 옮기며 넘김(닫힘 변수 · 캐시 · 서버 안 씀) → 도면 손잡이 `placeTable`(놓기 도구 · 41-2)
* ② 도면 [저장] — 문서 맨 위 `연결표[]` = {연결(T1…), 시트, 범위, 글자크기, 값(마지막 칸 글 `{시트!A1: 글}`)} ·
* 표 style.연결 = 연결 · 도면에서 지운 표는 기록에서 뺌(면적 [저장] 때 서버가 칸 글 · 값을 다시 적음 · 41-1)
* ② 도면 [저장] — 문서 맨 위 `연결표[]` = {연결(T1…), 시트, 범위, 이름, 글자크기, 값(마지막 칸 글 `{시트!A1: 글}`)} ·
* 표 style.연결 = 연결 · 도면에서 지운 표는 기록에서 뺌(면적 [저장] 때 서버가 칸 글 · 값을 다시 적음 · 41-1) ·
* 새 연결 줄의 보낸 범위 이름은 이 [저장] 요청 안에서 서버가 면적 문서에 둠(112-2 · 놓고 저장 안 하면 안 생김)
* ③ 도면을 열 때 — 연결표마다 지금 면적 값과 마지막 값이 다르면 알림 한 줄 + [업데이트](`updateTable`)
* ④ 표 여러 개 — id 마다 한 줄
* ⑥ 이름표 메뉴 [보내기 취소] — 화면에서만 점선 · 이름표를 빼고 면적 화면 「저장 안 한 고침」 ·
* 면적 [저장] 문서에 `보내기취소[]` 를 실어 서버가 그 연결표 줄 · 이름을 뺌(도면 표는 남음 · 112-2)
* ⑤ 여러 장(82-3) — 놓을 장 = 지금 탭 · 새 T번호 = 모든 장에서 안 쓴 것 · 연결표 줄 `장` = 표가 놓인 장id
* (1장 = 빈 값) · 안 실린 장 표 갱신 = 도면 손잡이(받아 둔 장) · 면적 [저장] 때는 서버가 모든 장
* ========================================================================== */
@@ -117,25 +120,6 @@ async function fetchTable(layer: Layer, column: string, spec: LinkSpec): Promise
return data as AreaTable;
}
/** 보낸 범위 이름을 면적 시트에 더함/갱신(104-1 · 처음 놓을 때 1번) — 실패해도 놓기는 이미 끝났으니 토스트만 */
async function defineAreaName(
layer: Layer,
column: string,
시트: string,
범위: string,
이름: string,
): Promise<void> {
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)}/name${q}`, {
method: "POST",
credentials: "include",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ 시트, 범위, 이름 }),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
}
/** 장마다 `[장id, entities]` — 1장 = 빈 값(서버 `drawing_sheets` 와 같음) */
const sheetsOf = (doc: StructureDoc): [string, unknown[]][] => [
["", doc.도면?.entities ?? []],
@@ -149,29 +133,19 @@ const linkOf = (e: TableEntityJson): string | null => {
return typeof id === "string" && id ? id : null;
};
/** 칸 글 묶음 견줌 — 열쇠 차례와 무관 */
const sameValues = (a: Record<string, string> = {}, b: Record<string, string> = {}): boolean => {
export const sameValues = (
a: Record<string, string> = {},
b: Record<string, string> = {},
): boolean => {
const ka = Object.keys(a);
return ka.length === Object.keys(b).length && ka.every((k) => a[k] === b[k]);
};
/** 이름표 메뉴 [보내기 취소](104-3) — 연결표에서 그 줄만 빼고 저장(도면 표는 남음) */
async function unlinkTable(at: Selection, 연결: string): Promise<boolean> {
const owner = getActiveOwner();
const q = owner && at.layer !== "system" && at.layer !== "project" ? `?owner=${enc(owner)}` : "";
const res = await fetch(
`${API_BASE_URL}/m02/layers/${at.layer}/structures/${enc(at.name)}/unlink${q}`,
{
method: "POST",
credentials: "include",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ 연결 }),
},
);
return res.ok;
}
/** 면적 시트에 겹쳐 보일 「도면으로 보낸 범위」 — 같은 열 도면 `연결표` 에서(받기 실패 = 없음 · 조용히) */
export async function sentRanges(at: Selection): Promise<NonNullable<LinkOptions["표시범위"]>> {
export async function sentRanges(
at: Selection,
onCancel: (연결: string) => void,
): Promise<NonNullable<LinkOptions["표시범위"]>> {
try {
const got = await readTemplate(at.layer, "structure", at.name, at.projectId);
const rows = (got.문서 as StructureDoc | undefined)?.연결표 ?? [];
@@ -181,7 +155,7 @@ export async function sentRanges(at: Selection): Promise<NonNullable<LinkOptions
범위이름: row.이름,
이름: fill(TEXT.tag, "id", row.연결),
설명: fill(fill(fill(TEXT.tip, "id", row.연결), "range", row.범위), "size", row.글자크기),
취소: () => unlinkTable(at, row.연결).catch(() => false),
취소: async () => (onCancel(row.연결), true), // 화면에서만 — 서버는 면적 [저장] 때
}));
} catch {
return [];
@@ -208,8 +182,14 @@ export interface AreaToDrawing {
onDrawing: (handle: unknown, at: Selection, doc: StructureDoc) => void;
/** 보낸 표 놓기 기다리는 중 — 편집 칸 「저장 안 한 고침」 에 듦 */
waiting: () => boolean;
/** 떠나기로 함(물음 [확인]) — 기다리던 표 버림 */
/** 떠나기로 함(물음 [확인]) · 저장 끝 — 기다리던 표 · 저장 전 보내기 취소를 버림 */
drop: () => void;
/** 이름표 메뉴 [보내기 취소] — 저장 전 목록에 담음(면적 화면 「저장 안 한 고침」) */
cancel: (연결: string) => void;
/** 저장 전 보내기 취소한 연결 */
cancelled: () => ReadonlySet<string>;
/** 면적 저장 문서 — 취소한 연결이 있으면 `보내기취소[]` 를 실음(서버가 줄 · 이름을 뺌) */
withCancels: <T extends object>(doc: T) => T;
/** 도면에서 Esc 로 놓지 않고 그만둠 — 기다림을 풀고 그 보내기 기록도 뺌 */
cancelPlace: () => void;
/** 도면 저장 문서 — 맨 위 `연결표` 를 도면의 표에 맞춤 */
@@ -227,6 +207,8 @@ export function createAreaToDrawing(opt: AreaToDrawingOptions): AreaToDrawing {
/** 보낸 표가 도면에 놓이길 기다림 — 떠날 때 「저장 안 한 고침」 물음(`waiting`) */
let waiting = false;
let waitingId = "";
/** 면적 화면에서 [보내기 취소]한 연결 — 면적 [저장] 때 서버가 끊음 */
const cancels = new Set<string>();
/** 글자 크기 작은 창 — 보내기면 크기 · 닫으면 null */
const askSize = (spec: { 범위: string }): Promise<number | null> =>
@@ -339,14 +321,21 @@ export function createAreaToDrawing(opt: AreaToDrawingOptions): AreaToDrawing {
return {
select: (s) => (range = { 시트: s.시트, 범위: s.범위 }),
waiting: () => waiting,
drop: () => (waiting = false),
waiting: () => waiting || cancels.size > 0,
drop: () => ((waiting = false), cancels.clear()),
cancel: (연결) => void cancels.add(연결),
cancelled: () => cancels,
withCancels: (doc) => {
const { 보내기취소: _old, ...rest } = doc as typeof doc & { 보내기취소?: unknown }; // 앞 저장 것은 떼고
return (cancels.size ? { ...rest, 보내기취소: [...cancels] } : rest) as typeof doc;
},
cancelPlace: () => {
waiting = false;
fresh.delete(waitingId);
},
onArea: (at, readOnly, slot) => {
waiting = false;
cancels.clear();
// 범위는 그대로 — 시트가 처음 고름(A1)을 이 갈고리보다 먼저 알림
if (readOnly || at.layer === "project") return;
const button = createButton({ label: TEXT.send }); // 머리 일반 단추(43-5 · 돌아가기와 같은 높이)
@@ -360,6 +349,7 @@ export function createAreaToDrawing(opt: AreaToDrawingOptions): AreaToDrawing {
const handle = raw as TableHandle;
shown = `${at.layer}:${at.name}`;
range = null; // 면적 상세를 떠남
cancels.clear();
fresh = new Map();
latest = new Map();
const give =
@@ -377,11 +367,11 @@ export function createAreaToDrawing(opt: AreaToDrawingOptions): AreaToDrawing {
waitingId = id;
waiting = true; // 놓기 전 떠나도 물음(놓으면 도면 고침 · [저장] 때 풂)
showToast(TEXT.place, "info");
// 보낸 범위 이름을 면적 시트에 — 행열 넣기 · 지우기를 따라가게(104-1) · 실패해도 놓기는 그대로
void defineAreaName(at.layer, at.name, 시트, 범위, 이름).catch(() => {});
// 보낸 범위 이름(104-1)은 도면 [저장] 요청 안에서 서버가 면적 문서에 둠(112-2 · 여기서 안 씀)
}
}
if (doc.지운연결?.length) showToast(fill(TEXT.rangeGone, "ids", doc.지운연결.join(" · ")), "info");
if (doc.지운연결?.length)
showToast(fill(TEXT.rangeGone, "ids", doc.지운연결.join(" · ")), "info");
void checkStale(handle, at, doc);
},
withLinks: (doc) => {
@@ -94,7 +94,7 @@ export interface DrawingTemplateHandle {
getDoc: () => Promise<DrawingTemplateDoc>;
/** 표 놓기 · 칸 갈기(41장) — `cad_host` 그대로 */
placeTable: (entity: Record<string, unknown>, link: string) => void;
updateTable: (link: string, cells: unknown[][]) => Promise<number>;
updateTable: (link: string, cells: unknown[][], preview?: boolean) => Promise<number>;
/** 다른 장을 같은 CAD 에 실음(82-1) — 백업 칸 · 도각 도번도 그 장 것으로 */
loadSheet: (
drawing: DrawingTemplateDoc,
@@ -537,7 +537,7 @@ export function mountStructureSheets(
drawing.placeTable(entity, link);
},
// 실린 장은 CAD · 안 실린 장은 받아 둔 편집본에서(82-3)
updateTable: async (link, cells) => {
updateTable: async (link, cells, preview) => {
const now = tabs.active();
let count = 0;
for (const tab of tabs.tabs()) {
@@ -547,7 +547,7 @@ export function mountStructureSheets(
count += changed;
}
if (count) report();
return count + (await drawing.updateTable(link, cells).catch(() => 0));
return count + (await drawing.updateTable(link, cells, preview).catch(() => 0));
},
destroy: drawing.destroy,
};
@@ -337,52 +337,6 @@ def post_area_table(
)
class AreaNameBody(BaseModel):
"""보낸 범위 이름 정의/갱신(104-1 · 처음 도면으로 보낼 때 1번) — `이름` = `도면_T1` 꼴."""
시트: str
범위: str
이름: str
@router.post("/layers/{layer}/area/{column}/name")
def post_area_name(
layer: Layer,
column: str,
body: AreaNameBody,
owner: str | None = None,
session: Session = Depends(verify_session),
) -> dict:
"""면적 시트에 보낸 범위 이름을 더함/갱신 — 쓰기 권한."""
from M02_MasterTemplete import M02_Area_Table as area_table
folder = _root(session, layer, owner, write=True)
_call(folder, area_table.define_name, column, body.시트, body.범위, body.이름)
return {}
class UnlinkBody(BaseModel):
"""이름표 메뉴 [보내기 취소](104-3) — `연결` = T번호."""
연결: str
@router.post("/layers/{layer}/structures/{column}/unlink")
def post_structure_unlink(
layer: Layer,
column: str,
body: UnlinkBody,
owner: str | None = None,
session: Session = Depends(verify_session),
) -> dict:
"""연결표에서 그 줄만 빼고 저장(104-3) — 도면 표는 남음 · 쓰기 권한."""
from M02_MasterTemplete import M02_Area_Table as area_table
folder = _root(session, layer, owner, write=True)
_call(folder, area_table.unlink, column, body.연결)
return {}
@router.post("/layers/{layer}/{kind}/{column}/clear")
def post_clear(
layer: Layer,
@@ -345,7 +345,9 @@ async def save_layer_template(
return await asyncio.to_thread(
_store_save, folder, kind, name, body.판 or "", document, layer
)
cancels: list[str] = []
if kind == "area":
document, cancels = await asyncio.to_thread(_apply_cancels, folder, name, document)
# 면적산출 계산값은 서버가 같은 TS 엔진을 Node 로 돌려 새로 적음
# (브라우저 값 버림 · 5장 ②)
try:
@@ -369,15 +371,23 @@ async def save_layer_template(
except ValueError as error:
raise _bad(error) from error
if kind == "area": # 그 열 도면 연결표 칸 글도(41-1 · 시스템 · 회사 · 개인 층은 Store.write 가)
await asyncio.to_thread(_refresh_drawing, folder, name)
await asyncio.to_thread(_refresh_drawing, folder, name, cancels)
return {"종류": kind, "이름": name, "판": version, "층": layer}
def _refresh_drawing(folder: Path, column: str) -> None:
def _apply_cancels(folder: Path, column: str, doc: Any) -> tuple[Any, list[str]]:
"""면적 저장 문서의 `보내기취소[]`(112-2)를 떼고 그 이름을 이름정의에서 뺌."""
from M02_MasterTemplete import M02_Area_Table as area_table
with store.at(folder):
area_table.refresh_drawing(column)
return area_table.apply_cancels(column, doc)
def _refresh_drawing(folder: Path, column: str, unlink: list[str] | tuple[()] = ()) -> None:
from M02_MasterTemplete import M02_Area_Table as area_table
with store.at(folder):
area_table.refresh_drawing(column, unlink)
def _store_save(folder: Path, kind: str, name: str, version: str, doc: Any, layer: str) -> dict:
+22 -5
View File
@@ -150,8 +150,15 @@ def write(kind: str, name: str, version: str, doc: Any, screen: bool = False) ->
표 열의 `본뜬열`(분류 더하기로 복사한 새 열)은 적지 않음 — 집계표면 아직 파일이 없는 새 열이
원본 열의 일위대가 키와 세 파일 내용을 받음(새 번호 · 그 뒤로는 열마다 따로).
`screen` = 화면 표 [저장](PLAN 26-7e) — 있던 항목의 경로 · 층 이름 · 빈 폴더는 저장본 그대로
(정본 = 나무 · 머리는 경로에서 다시 만듦 · `tree.keep_paths`)."""
(정본 = 나무 · 머리는 경로에서 다시 만듦 · `tree.keep_paths`).
도면↔면적(112-2) — 구조물 도면 저장 때 새 연결 줄의 보낸 범위 이름을 같은 요청 안에서 면적 문서에 두고 ·
면적 저장 문서의 `보내기취소[]` 연결은 도면 연결표 줄 · 이름을 뺌."""
path = _path(kind, name)
cancels: list[str] = []
if kind == "area":
from M02_MasterTemplete import M02_Area_Table as area_table
doc, cancels = area_table.apply_cancels(name, doc)
_check_skeleton(kind, doc)
if kind in layers.LIBRARY_KINDS: # 서고 문서 키 = 파일 이름(50-3)
doc = {**doc, "키": name}
@@ -159,10 +166,15 @@ def write(kind: str, name: str, version: str, doc: Any, screen: bool = False) ->
if kind == "table" and name == STRUCTURE_TABLE:
doc = tree.normalize_table(doc) # 경로 · 머리 줄 수 · 빈 폴더(표 저장 · 나무 고침 한 줄기)
doc = _server(kind, doc)
fresh: list[dict[str, Any]] = []
with _LOCK:
have = version_of(path.read_bytes()) if path.is_file() else ""
if have != (version or ""):
raise StoreError(409, {"stale": [name], "판": have})
if kind == "structure":
from M02_MasterTemplete import M02_Area_Table as area_table
fresh = area_table.fresh_names(path, doc)
if screen and have and kind == "table" and name == STRUCTURE_TABLE:
doc = tree.normalize_table(tree.keep_paths(doc, json.loads(path.read_bytes())))
if have and kind in _KEY:
@@ -172,7 +184,11 @@ def write(kind: str, name: str, version: str, doc: Any, screen: bool = False) ->
info = _info(kind, path)
if kind == "area":
_refresh_labor(name) # 면적 칸 값이 바뀌면 일위대가 계산값(목록 합계)도
_refresh_drawing(name) # 도면 연결표 칸 글도(41-1)
_refresh_drawing(name, cancels) # 도면 연결표 칸 글도(41-1) · 보내기 취소 줄 뺌(112-2)
if fresh:
from M02_MasterTemplete import M02_Area_Table as area_table
area_table.define_names(name, fresh) # 새 연결 줄의 보낸 범위 이름(112-2)
if kind == "table" and name == STRUCTURE_TABLE:
for column, source in origins.items(): # 빈 파일 채우기 앞 — 원본 내용으로 만듦
for part in _KEY:
@@ -562,11 +578,12 @@ def _server(kind: str, doc: Any, source: Path | None = None) -> Any:
return doc
def _refresh_drawing(column: str) -> None:
"""그 열 도면의 면적 연결표 칸 글을 지금 면적 값으로(`M02_Area_Table` · 41-1)."""
def _refresh_drawing(column: str, unlink: list[str] | tuple[()] = ()) -> None:
"""그 열 도면의 면적 연결표 칸 글을 지금 면적 값으로(`M02_Area_Table` · 41-1) ·
`unlink` = 보내기 취소 줄."""
from M02_MasterTemplete import M02_Area_Table as area_table
area_table.refresh_drawing(column)
area_table.refresh_drawing(column, unlink)
def _refresh_labor(column: str) -> None:
@@ -298,7 +298,7 @@ export function createMain(
const [{ createSpreadsheet }, pills, sent] = await Promise.all([
import("../A00_Common/spreadsheet/spreadsheet"),
pillsFor(kind, at.name),
sentRanges(at), // 도면으로 보낸 범위 — 시트 위 점선 테두리
sentRanges(at, toDrawing.cancel), // 도면으로 보낸 범위 — 시트 위 점선 테두리
]);
if (mine !== seq) return;
if (pills.notice) showNotice([pills.notice]);
@@ -332,6 +332,7 @@ export function createMain(
picked: () => picked,
pills: () => links.항목,
readOnly,
cancelled: toDrawing.cancelled,
});
host.append(apanel.root);
return;
@@ -555,12 +556,16 @@ export function createMain(
...(edited as Pick<StructureDoc, "도면" | "더한장">),
})
: at.kind === "area" && loaded
? { ...(loaded as Record<string, unknown>), ...(edited as Record<string, unknown>) }
? toDrawing.withCancels({
...(loaded as Record<string, unknown>),
...(edited as Record<string, unknown>),
}) // + 저장 전 [보내기 취소] 연결(112-2)
: edited;
const info = await saveTemplate(at.layer, at.kind, at.name, at.projectId, version, doc);
version = info.판;
loaded = doc;
dirty = false;
toDrawing.drop(); // 놓기 기다림 · 보내기 취소는 이 저장으로 끝
// 구조물집계표 — 서버가 새 열 파일 · 번호를 만듦 · 번호를 다시 받아 도면 줄 · 카드를 새로
// (안 하면 새 열은 다시 열 때까지 「도면 없음」 · 번호 빈칸)
if (panel && sheet) {