"""114 — 이름 없는 옛 보낸 범위 연결이 행 넣기를 따라감(화면 · 서버 한 바퀴). - 114-1 면적 상세를 열 때 화면(`sentRanges` + 책)이 `도면_{연결}` 이름정의를 둠 → 행 넣기 → 면적 [저장] → 서버 `refresh_drawing` 이 그 이름을 연결표 줄에 적고 도면 표 칸 글 = 옮겨진 칸. - 114-2 셀 밀기 · 당기기(칸 삽입 · 삭제 창)에 이름 따라감 · 카드 도면 저장도 표 없는 연결표 줄 뺌(`fitLinks`). - 114-3 아래 막대 도면 알약(`spreadsheet_link_range.rangePills`) · 상태(표 있음/없음 · 값 맞음/다름 · 범위 없음) · 누르면 범위 고름 · × = 보내기 취소 · 상태는 150ms 모아 한 번. 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")); let drawingNow = input.drawing; 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 () => ({ 문서: drawingNow }), }, "../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행 위에 한 행 // 114-2 — 셀 밀기 · 당기기(칸 삽입 · 삭제 창)에 이름 따라감 · 되돌리기 한 걸음 const dlg = ss("spreadsheet_insert_dialog"); const E7J17 = { r0: 6, c0: 4, r1: 16, c1: 9 }; const shift = (fn, range) => { const b = { 종류: "통합문서", 판: 1, 열: "t", 서식: [{}], 시트: [ { id: "s1", 이름: "s1", 칸: { E7: { 값: 1 } }, 이름정의: [{ 이름: "도면_T1", 범위: E7J17 }] }, ] }; const sent = []; const ctx = { book: b, readOnly: false, selection: { 시트: "s1" }, sheet: () => b.시트[0], dispatch: (c) => (sent.push(c.종류 === "묶음" ? c.명령.map((x) => x.종류).join("+") : c.종류), applyCommand(b, c)) }; dlg[fn](ctx, range); const n = b.시트[0].이름정의.map((x) => x.범위); return { names: n, sent, cell: Object.keys(b.시트[0].칸) }; }; const shifted = { downInside: shift("insertShiftDown", { r0: 9, c0: 4, r1: 9, c1: 9 }), downAbove: shift("insertShiftDown", { r0: 4, c0: 4, r1: 4, c1: 9 }), downPartial: shift("insertShiftDown", { r0: 4, c0: 4, r1: 4, c1: 5 }), upAll: shift("deleteShiftUp", E7J17), right: shift("insertShiftRight", { r0: 6, c0: 3, r1: 16, c1: 3 }), left: shift("deleteShiftLeft", { r0: 6, c0: 0, r1: 16, c1: 1 }), }; // 114-2 — 카드 도면 저장도 표 없는 연결표 줄을 뺌(도면 상세 저장과 같은 몸) const tbl = (id) => ({ type: "Table", shapeData: { cells: [[{ text: "x" }]], style: { 연결: id } } }); const row = (id) => ({ 연결: id, 시트: "s1", 범위: "A1", 값: {} }); const fit = { kept: toDrawing.fitLinks({ 도면: { entities: [tbl("T1")] }, 연결표: [row("T1"), row("T2")] }).연결표, plain: "연결표" in toDrawing.fitLinks({ 도면: { entities: [tbl("T9")] } }), }; // 114-3 — 아래 막대 도면 알약 · 상태(표 있음/없음 · 값 맞음/다름 · 범위 없음) const states = {}; const pills = {}; if (input.drawing3) { drawingNow = input.drawing3; const book3 = input.book3; const marks3 = await toDrawing.sentRanges(area, () => {}, book3); const st = await Promise.all(marks3.map((m) => m.상태(book3))); states.first = st.map((s) => [s.글, s.종류]); book3.시트[0].칸.B1 = { 값: 99, 서식: 1 }; // 저장 안 한 고침 states.edited = [(await marks3[0].상태(book3)).글]; const mk = (tag, o = {}) => ({ tag, className: o.className ?? "", text: o.text ?? "", attrs: o.attrs ?? {}, children: [...(o.children ?? [])], title: "", on: {}, style: { props: {}, setProperty(k, v) { this.props[k] = v; } }, addEventListener(k, f) { this.on[k] = f; }, append(...x) { this.children.push(...x); } }); const lr = tsLoad("A00_Common/spreadsheet/spreadsheet_link_range.ts", { "@ui/ui_template_elements": { el: mk, createTag: (l, v) => mk("span", { className: `ui-tag ui-tag--${v}`, text: l }) }, "./spreadsheet_address": ss("spreadsheet_address"), "./spreadsheet_link_color": ss("spreadsheet_link_color"), "./spreadsheet_names": ss("spreadsheet_names"), "./spreadsheet_selection": ss("spreadsheet_selection"), }); const got = new Map(marks3.map((m, i) => [m, st[i]])); const picked = []; const cancelled = []; let backs = 0; const ctx = { book: book3, readOnly: false, selection: { 시트: "s1" }, sheet: () => book3.시트[0], showSheet: () => {}, select: (s) => picked.push(s) }; const made = lr.rangePills(ctx, marks3, (m) => got.get(m), () => (backs += 1), (m) => cancelled.push(m.이름), { pick: "누르면 고름", cancel: "보내기 취소" }); made[0].on.click(); made[1].children.find((c) => c.className === "ss-link__x").on.click({ stopPropagation() {} }); const ro = lr.rangePills({ ...ctx, readOnly: true }, marks3, () => undefined, () => {}, () => {}, { pick: "", cancel: "" }); pills.made = made.map((p) => ({ text: p.children[0].text, tag: p.children[1] && p.children[1].className.startsWith("ui-tag") ? [p.children[1].className, p.children[1].text] : null, x: p.children.some((c) => c.className === "ss-link__x"), cls: p.className, hue: typeof p.style.props["--ss-link-h"], })); pills.picked = picked.map((s) => s.범위[0]); pills.backs = backs; pills.cancelled = cancelled; pills.roX = ro.map((p) => p.children.some((c) => c.className === "ss-link__x")); // 상태 모음 — 잇단 schedule 은 150ms 뒤 한 번만 let calls = 0; let done = 0; const fake = [{ 상태: () => (calls += 1, { 글: "x", 종류: "success" }) }]; const rs = lr.createRangeStates(() => fake, () => book3, () => (done += 1)); rs.schedule(); rs.schedule(); rs.schedule(); const early = calls; await new Promise((r) => setTimeout(r, 250)); pills.gather = { early, calls, done, got: rs.get(fake[0]) }; } console.log(JSON.stringify({ opened, book, shifted, fit, states, pills })); })().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"] def test_셀_밀기_당기기에_이름_따라감_띠에_걸치기만_하면_그대로_114_2(world): # noqa: F811 with store.at(world["system"]): area = store.read("area", "rv") s = _node({"book": area["문서"], "drawing": {"연결표": []}})["shifted"] box = lambda r0, c0, r1, c1: [{"r0": r0, "c0": c0, "r1": r1, "c1": c1}] # noqa: E731 assert s["downInside"]["names"] == box(6, 4, 17, 9) # 범위 안에 밀어 넣음 = 늘어남 E7:J18 assert s["downAbove"]["names"] == box(7, 4, 17, 9) # 위에서 밀림 = 통째로 E8:J18 assert s["downPartial"]["names"] == box(6, 4, 16, 9) # 띠(E:F)에 걸치기만 = 그대로 assert s["upAll"]["names"] == [] # 다 지운 자리 = 이름 빠짐(저장 때 연결 끊김) assert s["right"]["names"] == box(6, 5, 16, 10) # F7:K17 assert s["left"]["names"] == box(6, 2, 16, 7) # C7:H17 assert s["downInside"]["sent"] == ["시트통째+칸"] # 한 묶음 = 되돌리기 한 걸음 assert s["downPartial"]["sent"] == ["칸"] # 안 바뀌면 시트통째 안 끼움 assert s["downAbove"]["cell"] == ["E8"] # 값도 함께 밀림 def test_카드_도면_저장도_표_없는_연결표_줄_뺌_114_2(world): # noqa: F811 with store.at(world["system"]): area = store.read("area", "rv") fit = _node({"book": area["문서"], "drawing": {"연결표": []}})["fit"] assert fit["kept"] == [{"연결": "T1", "시트": "s1", "범위": "A1", "값": {}, "장": ""}] assert fit["plain"] is False # 기록 없던 도면은 칸을 안 만듦 def test_아래_막대_도면_알약_상태_누르면_고름_x_는_보내기_취소_114_3(world): # noqa: F811 with store.at(world["system"]): area = store.read("area", "rv") book = area["문서"] now = sheet_node.area_tables([{"book": book, "시트": "s1", "범위": "A1:B2"}])[0]["값"] drawing3 = { "도면": {"entities": [T1_TABLE, {**T1_TABLE, "shapeData": {"style": {"연결": "T3"}}}]}, "연결표": [ {**OLD_ROW, "값": now}, # 이름 없는 옛 연결 · 표 있음 · 값 같음 {"연결": "T2", "시트": "s1", "범위": "B1", "값": {}}, # 도면에 표 없음 { "연결": "T3", "시트": "s1", "범위": "A1", "이름": "도면_없음", "값": {}, }, # 이름 지워짐 ], } out = _node({"book": book, "drawing": {"연결표": []}, "drawing3": drawing3, "book3": book}) assert out["states"]["first"] == [ ["✓", "success"], # 값 맞음 · 다름 = 기호(128-2) ["도면에 표 없음", "danger"], ["범위 없음", "danger"], ] assert out["states"]["edited"] == ["✗"] # 저장 전 칸 고침 = 지금 책으로 판정 p = out["pills"] assert [x["text"] for x in p["made"]] == ["T1 도면 A1:B2", "T2 도면 B1", "T3 도면"] assert p["made"][0]["tag"] == ["ui-tag ui-tag--success", "✓"] # 공용 꼬리표 assert p["made"][1]["tag"] == ["ui-tag ui-tag--danger", "도면에 표 없음"] assert all(x["x"] and x["cls"] == "ss-link ss-link--range" for x in p["made"]) assert all(x["hue"] == "string" for x in p["made"]) # 색 = 시트 위 테두리 색 assert ( p["picked"] == [{"r0": 0, "c0": 0, "r1": 1, "c1": 1}] and p["backs"] == 1 ) # 누르면 범위 고름 assert p["cancelled"] == ["T2 도면"] # × = 보내기 취소(화면에서만 · 저장 때 기록) assert p["roX"] == [False, False, False] # 읽기 = × 없음 assert p["gather"] == {"early": 0, "calls": 1, "done": 1, "got": {"글": "x", "종류": "success"}}