From f44789c33c97582480287e9ecdb7c4e70f5b75ec Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 8 Oct 2026 18:26:34 +0900 Subject: [PATCH] =?UTF-8?q?fix(m02):=20=EC=9D=B4=EB=A6=84=20=EC=97=86?= =?UTF-8?q?=EB=8A=94=20=EC=98=9B=20=EB=B3=B4=EB=82=B8=20=EB=B2=94=EC=9C=84?= =?UTF-8?q?=20=EC=97=B0=EA=B2=B0=20=E2=80=94=20=EB=A9=B4=EC=A0=81=20?= =?UTF-8?q?=EC=97=B4=20=EB=95=8C=20=EB=8F=84=EB=A9=B4=5F{=EC=97=B0?= =?UTF-8?q?=EA=B2=B0}=20=EC=9D=B4=EB=A6=84=20=C2=B7=20=EB=A9=B4=EC=A0=81?= =?UTF-8?q?=20=EC=A0=80=EC=9E=A5=20=EB=95=8C=20=EC=97=B0=EA=B2=B0=ED=91=9C?= =?UTF-8?q?=20=EC=A4=84=EC=97=90=20=EC=9D=B4=EB=A6=84(114-1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 면적 상세를 열 때 화면이 이름 없는 보낸 범위마다 책에 이름정의를 둠(고침 표시 없음) · 그 이름으로 점선 - 면적 [저장] 때 서버 refresh_drawing 이 책에 도면_{연결} 이 있으면 연결표 줄에 이름 적음 - 옛 줄 시험 기대 뒤집음 · 새 시험(옛 연결 → 열기 → 행 넣기 → 저장 → 도면 표 값 따라감) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MgChuAcyBqUkhHMTXomQC7 --- M02_MasterTemplete/M02_Area_Table.py | 19 +++ .../M02_MasterTemplete_AreaToDrawing.ts | 30 +++- .../M02_MasterTemplete_UI_Main.ts | 3 +- resources/tester/test_m02_area_old_links.py | 136 ++++++++++++++++++ resources/tester/test_m02_area_table.py | 13 +- 5 files changed, 195 insertions(+), 6 deletions(-) create mode 100644 resources/tester/test_m02_area_old_links.py diff --git a/M02_MasterTemplete/M02_Area_Table.py b/M02_MasterTemplete/M02_Area_Table.py index 50c5139db..f89081ecf 100644 --- a/M02_MasterTemplete/M02_Area_Table.py +++ b/M02_MasterTemplete/M02_Area_Table.py @@ -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, diff --git a/M02_MasterTemplete/M02_MasterTemplete_AreaToDrawing.ts b/M02_MasterTemplete/M02_MasterTemplete_AreaToDrawing.ts index 873c8c2a6..920783a61 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_AreaToDrawing.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_AreaToDrawing.ts @@ -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 { + 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> { 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), // 화면에서만 — 서버는 면적 [저장] 때 diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts b/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts index 4c8f5d10d..180b36cbf 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts @@ -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[2]), ]); if (mine !== seq) return; if (pills.notice) showNotice([pills.notice]); diff --git a/resources/tester/test_m02_area_old_links.py b/resources/tester/test_m02_area_old_links.py new file mode 100644 index 000000000..fbb18d232 --- /dev/null +++ b/resources/tester/test_m02_area_old_links.py @@ -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"] diff --git a/resources/tester/test_m02_area_table.py b/resources/tester/test_m02_area_table.py index 84412ebce..8a699f868 100644 --- a/resources/tester/test_m02_area_table.py +++ b/resources/tester/test_m02_area_table.py @@ -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):