fix(m02): 이름 없는 옛 보낸 범위 연결 — 면적 열 때 도면_{연결} 이름 · 면적 저장 때 연결표 줄에 이름(114-1)
- 면적 상세를 열 때 화면이 이름 없는 보낸 범위마다 책에 이름정의를 둠(고침 표시 없음) · 그 이름으로 점선
- 면적 [저장] 때 서버 refresh_drawing 이 책에 도면_{연결} 이 있으면 연결표 줄에 이름 적음
- 옛 줄 시험 기대 뒤집음 · 새 시험(옛 연결 → 열기 → 행 넣기 → 저장 → 도면 표 값 따라감)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgChuAcyBqUkhHMTXomQC7
This commit is contained in:
@@ -25,6 +25,18 @@ DROPPED = "지운연결"
|
||||
CANCELS = "보내기취소" # 면적 [저장] 문서의 임시 칸 — 저장 안 함(112-2)
|
||||
# spreadsheet_area_table.ts `NAME_GONE` 과 같은 글자 — 이름이 지워졌을 때 까닭
|
||||
NAME_GONE = "이름없음"
|
||||
NAME = "도면_" # 보낸 범위 이름 앞머리 — AreaToDrawing.ts `NAME` 과 같은 글자
|
||||
|
||||
|
||||
def _book_names(book: Any) -> set[str]:
|
||||
"""책 모든 시트 이름정의 이름(작은 글자 · 화면 `resolveName` 과 같이 대소문자 무시)."""
|
||||
return {
|
||||
str(n.get("이름", "")).lower()
|
||||
for s in (book.get("시트") or [] if isinstance(book, dict) else [])
|
||||
if isinstance(s, dict)
|
||||
for n in s.get("이름정의") or []
|
||||
if isinstance(n, dict)
|
||||
}
|
||||
|
||||
|
||||
def to_table(
|
||||
@@ -119,6 +131,7 @@ def refresh_drawing(column: str, unlink: Any = ()) -> list[str]:
|
||||
"""지금 층 그 열 도면(모든 장)의 연결표마다 칸 글 다시 · 마지막 값 갱신 → 칸을 간 표의 `연결` 목록.
|
||||
`unlink` = 면적 [저장] 때 [보내기 취소]한 연결 — 그 줄을 연결표에서 뺌
|
||||
(도면 표는 남음 · 알림 없음 · 112-2).
|
||||
이름 없는 옛 줄은 책에 `도면_{연결}` 이름이 있으면 그 이름을 줄에 적음(114-1 · 면적 화면이 열 때 둠).
|
||||
이름(104-1)이 있으면 그 이름이 지금 가리키는 범위로 `범위` 도 갱신 — 이름이 지워졌으면(범위가
|
||||
통째로 지워짐) 그 연결을 끊고(도면 표는 남음) `지운연결` 목록에 적어 다음에 열 때 알림."""
|
||||
try:
|
||||
@@ -137,6 +150,12 @@ def refresh_drawing(column: str, unlink: Any = ()) -> list[str]:
|
||||
atomic_write_json(drawing, doc)
|
||||
return []
|
||||
book = json.loads(area.read_bytes())
|
||||
names = _book_names(book)
|
||||
for (
|
||||
x
|
||||
) in links: # 이름 없는 옛 연결(114-1) — 면적 화면이 책에 둔 `도면_{연결}` 이 있으면 줄에 적음
|
||||
if not x.get("이름") and f"{NAME}{x['연결']}".lower() in names:
|
||||
x["이름"] = f"{NAME}{x['연결']}"
|
||||
asks = [
|
||||
{
|
||||
"book": book,
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
type StructureDoc,
|
||||
} from "./M02_MasterTemplete_Api_Fetch";
|
||||
import type { LinkOptions } from "../A00_Common/spreadsheet/spreadsheet";
|
||||
import type { Workbook } from "../A00_Common/spreadsheet/spreadsheet_types";
|
||||
import type { Selection } from "./M02_MasterTemplete_UI_Main";
|
||||
|
||||
/** 표 칸 하나(CAD `TableCell` 꼴에서 쓰는 것만) */
|
||||
@@ -141,18 +142,41 @@ export const sameValues = (
|
||||
return ka.length === Object.keys(b).length && ka.every((k) => a[k] === b[k]);
|
||||
};
|
||||
|
||||
/** 면적 시트에 겹쳐 보일 「도면으로 보낸 범위」 — 같은 열 도면 `연결표` 에서(받기 실패 = 없음 · 조용히) */
|
||||
/** 이름 없는 옛 연결(114-1) — 책에 `도면_{연결}` 이름정의를 적힌 범위로 둠(이미 있으면 그대로) → 그 이름.
|
||||
* 책을 바로 고침(화면에 싣기 전 · 「저장 안 한 고침」 아님) · 서버는 면적 [저장] 때 그 이름을 연결표 줄에 적음 */
|
||||
async function nameOld(book: Workbook, row: TableLink): Promise<string | undefined> {
|
||||
const name = `${NAME}${row.연결}`;
|
||||
const low = name.toLowerCase();
|
||||
if (book.시트.some((s) => s.이름정의?.some((n) => n.이름.toLowerCase() === low))) return name;
|
||||
const { parseRange } = await import("../A00_Common/spreadsheet/spreadsheet_address");
|
||||
const sheet = book.시트.find((s) => s.id === row.시트);
|
||||
const g = parseRange(row.범위);
|
||||
if (!sheet || !g) return undefined; // 못 풂 — 적힌 범위로(옛 그대로)
|
||||
sheet.이름정의 = [...(sheet.이름정의 ?? []), { 이름: name, 범위: g }];
|
||||
return name;
|
||||
}
|
||||
|
||||
/** 면적 시트에 겹쳐 보일 「도면으로 보낸 범위」 — 같은 열 도면 `연결표` 에서(받기 실패 = 없음 · 조용히) ·
|
||||
* `book`(면적 책 · 화면에 싣기 전) 있으면 이름 없는 옛 연결에 이름을 둠(114-1 · 행열 넣기를 따라감) */
|
||||
export async function sentRanges(
|
||||
at: Selection,
|
||||
onCancel: (연결: string) => void,
|
||||
book?: Workbook,
|
||||
): Promise<NonNullable<LinkOptions["표시범위"]>> {
|
||||
try {
|
||||
const got = await readTemplate(at.layer, "structure", at.name, at.projectId);
|
||||
const rows = (got.문서 as StructureDoc | undefined)?.연결표 ?? [];
|
||||
return rows.map((row) => ({
|
||||
const names = await Promise.all(
|
||||
rows.map((row) =>
|
||||
row.이름 || !book || !Array.isArray(book.시트)
|
||||
? row.이름
|
||||
: nameOld(book, row).catch(() => undefined),
|
||||
),
|
||||
);
|
||||
return rows.map((row, i) => ({
|
||||
시트: row.시트,
|
||||
범위: row.범위,
|
||||
범위이름: row.이름,
|
||||
범위이름: names[i],
|
||||
이름: fill(TEXT.tag, "id", row.연결),
|
||||
설명: fill(fill(fill(TEXT.tip, "id", row.연결), "range", row.범위), "size", row.글자크기),
|
||||
취소: async () => (onCancel(row.연결), true), // 화면에서만 — 서버는 면적 [저장] 때
|
||||
|
||||
@@ -298,7 +298,8 @@ export function createMain(
|
||||
const [{ createSpreadsheet }, pills, sent] = await Promise.all([
|
||||
import("../A00_Common/spreadsheet/spreadsheet"),
|
||||
pillsFor(kind, at.name),
|
||||
sentRanges(at, toDrawing.cancel), // 도면으로 보낸 범위 — 시트 위 점선 테두리
|
||||
// 도면으로 보낸 범위 — 시트 위 점선 테두리 · 이름 없는 옛 연결은 책에 이름을 둠(114-1)
|
||||
sentRanges(at, toDrawing.cancel, doc as Parameters<typeof sentRanges>[2]),
|
||||
]);
|
||||
if (mine !== seq) return;
|
||||
if (pills.notice) showNotice([pills.notice]);
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
"""114 — 이름 없는 옛 보낸 범위 연결이 행 넣기를 따라감(화면 · 서버 한 바퀴).
|
||||
|
||||
- 114-1 면적 상세를 열 때 화면(`sentRanges` + 책)이 `도면_{연결}` 이름정의를 둠 → 행 넣기 → 면적 [저장]
|
||||
→ 서버 `refresh_drawing` 이 그 이름을 연결표 줄에 적고 도면 표 칸 글 = 옮겨진 칸.
|
||||
TS 는 Node 로(`AreaToDrawing` 은 그 자리 변환 · 스프레드시트 모듈은 `helper_spreadsheet_d_bundle.cjs`).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from common_util import common_util_spreadsheet as sheet_node
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
from resources.tester.test_m02_area_table import world # noqa: F401 — 같은 임시 층
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
SCRIPT = r"""
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ROOT = process.argv[1];
|
||||
const { load, SS } = require(path.join(ROOT, "resources/tester/spreadsheet/helper_spreadsheet_d_bundle.cjs"));
|
||||
const ts = require(path.join(ROOT, "config/node_modules/typescript"));
|
||||
const input = JSON.parse(fs.readFileSync(0, "utf8"));
|
||||
const ss = (name) => load(path.join(SS, name + ".ts"));
|
||||
const tsLoad = (file, stubs) => {
|
||||
const src = fs.readFileSync(path.join(ROOT, file), "utf8");
|
||||
const js = ts.transpileModule(src, {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||
}).outputText;
|
||||
const box = { exports: {} };
|
||||
new Function("exports", "module", "require", js)(box.exports, box, (n) => {
|
||||
if (!(n in stubs)) throw new Error("모르는 가져오기 " + n);
|
||||
return stubs[n];
|
||||
});
|
||||
return box.exports;
|
||||
};
|
||||
const toDrawing = tsLoad("M02_MasterTemplete/M02_MasterTemplete_AreaToDrawing.ts", {
|
||||
"@config/config_frontend": { API_BASE_URL: "/api" },
|
||||
"@ui/ui_template_elements": {},
|
||||
"@ui/ui_template_modal": {},
|
||||
"./M02_MasterTemplete_Api_Fetch": {
|
||||
getActiveOwner: () => null,
|
||||
readTemplate: async () => ({ 문서: input.drawing }),
|
||||
},
|
||||
"../A00_Common/spreadsheet/spreadsheet_address": ss("spreadsheet_address"),
|
||||
"../A00_Common/spreadsheet/spreadsheet_area_table": ss("spreadsheet_area_table"),
|
||||
});
|
||||
const { applyCommand } = ss("spreadsheet_commands");
|
||||
|
||||
(async () => {
|
||||
const book = input.book;
|
||||
const area = { layer: "system", projectId: null, kind: "area", name: "rv", label: "x" };
|
||||
const before = JSON.stringify(book);
|
||||
const noBook = await toDrawing.sentRanges(area, () => {});
|
||||
const unchanged = JSON.stringify(book) === before;
|
||||
const marks = await toDrawing.sentRanges(area, () => {}, book);
|
||||
const opened = {
|
||||
marks: marks.map((m) => [m.이름, m.범위이름 ?? null]),
|
||||
noBook: noBook.map((m) => m.범위이름 ?? null),
|
||||
unchanged,
|
||||
names: book.시트[0].이름정의,
|
||||
};
|
||||
applyCommand(book, { 종류: "행넣기", 시트: "s1", at: 0, 수: 1 }); // 1행 위에 한 행
|
||||
console.log(JSON.stringify({ opened, book }));
|
||||
})().catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
"""
|
||||
|
||||
T1_TABLE = {
|
||||
"type": "Table",
|
||||
"shapeData": {
|
||||
"origin": {"x": 0, "y": 0},
|
||||
"columnWidths": [9, 9],
|
||||
"rowHeights": [3, 3],
|
||||
"cells": [[{"text": "낡음"}, {"text": "낡음"}], [None, {"text": "낡음"}]],
|
||||
"style": {"fontSize": 2.5, "연결": "T1"},
|
||||
},
|
||||
}
|
||||
OLD_ROW = {"연결": "T1", "시트": "s1", "범위": "A1:B2", "글자크기": 2.5, "값": {}}
|
||||
|
||||
|
||||
def _node(payload: dict) -> dict:
|
||||
if shutil.which("node") is None:
|
||||
pytest.skip("node 가 없음")
|
||||
proc = subprocess.run(
|
||||
["node", "-e", SCRIPT, str(ROOT)],
|
||||
input=json.dumps(payload, ensure_ascii=False),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=120,
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
return json.loads(proc.stdout.strip().splitlines()[-1])
|
||||
|
||||
|
||||
def _texts(cells: list) -> list:
|
||||
return [[c and c.get("text") for c in row] for row in cells]
|
||||
|
||||
|
||||
def test_옛_연결_열기_행_넣기_저장_도면_표_값_따라감_114_1(world): # noqa: F811
|
||||
system = world["system"]
|
||||
with store.at(system):
|
||||
got = store.read("structure", "rv")
|
||||
drawing = {**got["문서"], "연결표": [OLD_ROW]}
|
||||
drawing["도면"] = {**drawing["도면"], "entities": [T1_TABLE]}
|
||||
store.write("structure", "rv", got["판"], drawing)
|
||||
area = store.read("area", "rv")
|
||||
want = sheet_node.area_tables([{"book": area["문서"], "시트": "s1", "범위": "A1:B2"}])[0]
|
||||
out = _node({"book": area["문서"], "drawing": drawing})
|
||||
opened = out["opened"]
|
||||
assert opened["noBook"] == [None] and opened["unchanged"] # 책 없으면 안 고침(옛 길)
|
||||
assert opened["marks"] == [["T1 도면", "도면_T1"]] # 그 이름으로 점선
|
||||
assert opened["names"] == [{"이름": "도면_T1", "범위": {"r0": 0, "c0": 0, "r1": 1, "c1": 1}}]
|
||||
book = out["book"]
|
||||
assert book["시트"][0]["이름정의"][0]["범위"] == {
|
||||
"r0": 1,
|
||||
"c0": 0,
|
||||
"r1": 2,
|
||||
"c1": 1,
|
||||
} # 행 넣기 따라감
|
||||
with store.at(system):
|
||||
store.write("area", "rv", area["판"], book) # 면적 [저장] → refresh_drawing
|
||||
doc = store.read("structure", "rv")["문서"]
|
||||
row = doc["연결표"][0]
|
||||
assert (row["이름"], row["범위"]) == ("도면_T1", "A2:B3")
|
||||
cells = doc["도면"]["entities"][0]["shapeData"]["cells"]
|
||||
assert _texts(cells) == _texts(want["표"]["shapeData"]["cells"]) # 옮겨진 칸 = 보낸 그 칸
|
||||
assert _texts(cells)[0] == ["제목", "1,234.50"]
|
||||
@@ -300,11 +300,20 @@ def test_판_충돌로_도면_저장이_막히면_이름도_안_생김_112_2(wor
|
||||
assert (system / "area" / "rv.json").read_bytes() == before
|
||||
|
||||
|
||||
def test_이름_없는_옛_연결_줄은_이름을_안_둠_112_2(world):
|
||||
def test_이름_없는_옛_연결_줄은_면적_저장_때_책에_이름_있으면_줄에_적음_114_1(world):
|
||||
system = world["system"]
|
||||
with store.at(system):
|
||||
_save_links([{"연결": "T1", "시트": "s1", "범위": "A1:B2", "값": {}}])
|
||||
assert _area_names(system) == []
|
||||
assert _area_names(system) == [] # 도면 저장은 옛 줄 이름을 안 만듦(112-2 그대로)
|
||||
drawing = store.read("structure", "rv")["문서"]
|
||||
assert "이름" not in drawing["연결표"][0] # 책에 이름 없으면 적힌 범위 그대로
|
||||
got = store.read(
|
||||
"area", "rv"
|
||||
) # 면적 화면이 열 때 둔 이름(114-1) — 이름이 가리키는 범위는 B1:C2
|
||||
got["문서"]["시트"][0]["이름정의"] = [T1_NAME]
|
||||
store.write("area", "rv", got["판"], got["문서"])
|
||||
row = store.read("structure", "rv")["문서"]["연결표"][0]
|
||||
assert (row["이름"], row["범위"]) == ("도면_T1", "B1:C2")
|
||||
|
||||
|
||||
def test_면적_저장_보내기취소_줄과_이름을_뺌_도면_표는_남음_112_2(world):
|
||||
|
||||
Reference in New Issue
Block a user