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:
@@ -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:
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user