From ad511175bfa85cacbae4d40ba8a1adb7f7f6dca6 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 8 Oct 2026 18:28:48 +0900 Subject: [PATCH] =?UTF-8?q?fix(m02):=20=EC=85=80=20=EB=B0=80=EA=B8=B0=20?= =?UTF-8?q?=C2=B7=20=EB=8B=B9=EA=B8=B0=EA=B8=B0=EC=97=90=20=EC=9D=B4?= =?UTF-8?q?=EB=A6=84=20=EB=94=B0=EB=9D=BC=EA=B0=90=20=C2=B7=20=EC=B9=B4?= =?UTF-8?q?=EB=93=9C=20=EB=8F=84=EB=A9=B4=20=EC=A0=80=EC=9E=A5=EB=8F=84=20?= =?UTF-8?q?=ED=91=9C=20=EC=97=86=EB=8A=94=20=EC=97=B0=EA=B2=B0=ED=91=9C=20?= =?UTF-8?q?=EC=A4=84=20=EB=BA=8C(114-2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 칸 삽입 · 삭제 창(밀기 · 당기기) — 띠 안에 통째로 든 이름정의 범위를 같이 옮김 · 되돌리기 한 걸음 - 연결표 맞춤 몸을 fitLinks 로 떼어 도면 상세 · 카드 도면 저장이 같이 씀 - 시험 — 밀기 여섯 갈래 · 카드 저장 줄 빼기 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MgChuAcyBqUkhHMTXomQC7 --- .../spreadsheet/spreadsheet_insert_dialog.ts | 51 +++++++++++++++-- .../M02_MasterTemplete_AreaToDrawing.ts | 37 +++++++----- .../M02_MasterTemplete_DrawingCard.ts | 9 ++- resources/tester/test_m02_area_old_links.py | 56 ++++++++++++++++++- resources/tester/test_m02_drawing_card.py | 4 +- 5 files changed, 133 insertions(+), 24 deletions(-) diff --git a/A00_Common/spreadsheet/spreadsheet_insert_dialog.ts b/A00_Common/spreadsheet/spreadsheet_insert_dialog.ts index d96cf9df3..2ed4581b5 100644 --- a/A00_Common/spreadsheet/spreadsheet_insert_dialog.ts +++ b/A00_Common/spreadsheet/spreadsheet_insert_dialog.ts @@ -18,6 +18,8 @@ import { createButton, el } from "@ui/ui_template_elements"; import { dialogKeys } from "./spreadsheet_actions"; import { parseA1, toA1 } from "./spreadsheet_address"; +import { shiftSpan } from "./spreadsheet_refshift"; +import { MAX_COLS, MAX_ROWS } from "./spreadsheet_types"; import type { Cell, CellLink, CellRange, Command, Sheet } from "./spreadsheet_types"; import type { SpreadsheetContext } from "./spreadsheet_view_types"; @@ -73,7 +75,7 @@ function shiftLinks( const along = axis === "col" ? p.r : p.c; const cross = axis === "col" ? p.c : p.r; if (along < lo || along > hi || cross < at) return [link]; // 범위 밖 · 밀리는 자리 앞 — 그대로 - if (dir < 0 && cross < at + amount) return (changed = true), []; // 지운 자리 — 빠짐 + if (dir < 0 && cross < at + amount) return ((changed = true), []); // 지운 자리 — 빠짐 changed = true; const moved = cross + dir * amount; return [{ ...link, 칸: axis === "col" ? toA1(p.r, moved) : toA1(moved, p.c) }]; @@ -81,7 +83,42 @@ function shiftLinks( return changed ? next : undefined; } -/** 「칸」 명령(값만) 앞에 — 알약이 움직였으면 「시트통째」(연결만 바꿈)를 한 묶음으로(되돌리기 한 걸음) */ +/** 밀기 · 당기기에 이름정의 범위도 따라감(114-2 · 보낸 범위 이름 등) — 이름 범위가 `axis` 범위(띠) 안에 + * 통째로 들 때만 옮김(띠에 걸치기만 하면 그대로 · 엑셀과 같음) · 다 지운 자리면 빠짐. 안 바뀌면 `undefined`. */ +function shiftNames( + sheet: Sheet, + range: CellRange, + axis: "col" | "row", + dir: 1 | -1, + amount: number, +): Sheet["이름정의"] | undefined { + const list = sheet.이름정의; + if (!list?.length) return undefined; + const col = axis === "col"; + const [lo, hi] = col ? [range.r0, range.r1] : [range.c0, range.c1]; + let changed = false; + const next = list.flatMap((n) => { + const g = n.범위; + if ((col ? g.r0 : g.c0) < lo || (col ? g.r1 : g.c1) > hi) return [n]; + const [a, b] = col ? [g.c0, g.c1] : [g.r0, g.r1]; + const to = shiftSpan( + a, + b, + dir > 0, + col ? range.c0 : range.r0, + amount, + col ? MAX_COLS : MAX_ROWS, + ); + if (to && to[0] === a && to[1] === b) return [n]; + changed = true; + if (!to) return []; + return [{ ...n, 범위: col ? { ...g, c0: to[0], c1: to[1] } : { ...g, r0: to[0], r1: to[1] } }]; + }); + return changed ? next : undefined; +} + +/** 「칸」 명령(값만) 앞에 — 알약 · 이름이 움직였으면 「시트통째」(연결 · 이름정의만 바꿈)를 한 묶음으로 + * (되돌리기 한 걸음) */ function dispatchShift( ctx: SpreadsheetContext, sheet: Sheet, @@ -93,10 +130,14 @@ function dispatchShift( ): void { const cellCmd: Command = { 종류: "칸", 시트: ctx.selection.시트, 칸: cells }; const linked = shiftLinks(sheet, range, axis, dir, amount); + const named = shiftNames(sheet, range, axis, dir, amount); + const next = { + ...sheet, + ...(linked ? { 연결: linked } : {}), + ...(named ? { 이름정의: named } : {}), + }; ctx.dispatch( - linked - ? { 종류: "묶음", 명령: [{ 종류: "시트통째", 시트: { ...sheet, 연결: linked } }, cellCmd] } - : cellCmd, + linked || named ? { 종류: "묶음", 명령: [{ 종류: "시트통째", 시트: next }, cellCmd] } : cellCmd, ); } diff --git a/M02_MasterTemplete/M02_MasterTemplete_AreaToDrawing.ts b/M02_MasterTemplete/M02_MasterTemplete_AreaToDrawing.ts index 920783a61..f37ddd082 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_AreaToDrawing.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_AreaToDrawing.ts @@ -401,20 +401,29 @@ export function createAreaToDrawing(opt: AreaToDrawingOptions): AreaToDrawing { withLinks: (doc) => { waiting = false; const { 지운연결: _dropped, ...doc_ } = doc; - doc = doc_; - const old = new Map((doc.연결표 ?? []).map((row) => [row.연결, row])); - const rows: TableLink[] = []; - for (const [sheet, entities] of sheetsOf(doc)) { - for (const e of entities) { - if (!isTable(e)) continue; - const id = linkOf(e); - const row = id ? (old.get(id) ?? fresh.get(id)) : undefined; - if (!id || !row || rows.some((r) => r.연결 === id)) continue; - rows.push({ ...row, 장: sheet, 값: latest.get(id) ?? row.값 }); - } - } - // 기록이 없던 도면은 칸을 안 만듦(옛 문서 그대로) - return rows.length || doc.연결표 ? { ...doc, 연결표: rows } : doc; + return fitLinks(doc_, fresh, latest); }, }; } + +/** 도면 저장 문서 `연결표` 를 도면에 남은 표에 맞춤 — 표 없는 줄은 뺌 · 줄 `장` = 표가 놓인 장 · + * `fresh` = 새로 놓은 표 줄 · `latest` = [업데이트]로 바뀐 값(도면 상세) · 카드 도면 저장도 이 몸(114-2) */ +export function fitLinks( + doc: StructureDoc, + fresh: ReadonlyMap = new Map(), + latest: ReadonlyMap> = new Map(), +): StructureDoc { + const old = new Map((doc.연결표 ?? []).map((row) => [row.연결, row])); + const rows: TableLink[] = []; + for (const [sheet, entities] of sheetsOf(doc)) { + for (const e of entities) { + if (!isTable(e)) continue; + const id = linkOf(e); + const row = id ? (old.get(id) ?? fresh.get(id)) : undefined; + if (!id || !row || rows.some((r) => r.연결 === id)) continue; + rows.push({ ...row, 장: sheet, 값: latest.get(id) ?? row.값 }); + } + } + // 기록이 없던 도면은 칸을 안 만듦(옛 문서 그대로) + return rows.length || doc.연결표 ? { ...doc, 연결표: rows } : doc; +} diff --git a/M02_MasterTemplete/M02_MasterTemplete_DrawingCard.ts b/M02_MasterTemplete/M02_MasterTemplete_DrawingCard.ts index 0f9e2c655..d4b23de5c 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_DrawingCard.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_DrawingCard.ts @@ -10,6 +10,7 @@ * ========================================================================== */ import type { StructureDoc } from "./M02_MasterTemplete_Api_Fetch"; +import { fitLinks } from "./M02_MasterTemplete_AreaToDrawing"; import type { SaveControl } from "./M02_MasterTemplete_CardShell"; import { mountStructureSheets, @@ -130,10 +131,14 @@ export function mountDrawingCard(host: HTMLElement, opt: DrawingCardOptions): Dr let version = opt.version ?? ""; /** 전체 편집기 [저장]과 같은 길 — 읽은 문서에 `도면` · `더한장`(모든 장)을 갈아 끼워 그 항목 파일에 - * (실패는 던짐) · CAD Ctrl+S 가 넘기는 지금 탭 장은 안 씀 — 2장 보다 저장해도 1장 그대로(105-3) */ + * (실패는 던짐) · CAD Ctrl+S 가 넘기는 지금 탭 장은 안 씀 — 2장 보다 저장해도 1장 그대로(105-3) · + * 도면에서 지운 표의 연결표 줄은 뺌(114-2 · 도면 상세 저장과 같은 몸) */ async function doSave(): Promise { if (!write) return; - const next = { ...doc, ...((await handle.getDoc()) as Pick) }; + const next = fitLinks({ + ...doc, + ...((await handle.getDoc()) as Pick), + }); version = (await write("structure", opt.column, version, next)).판; doc = next; } diff --git a/resources/tester/test_m02_area_old_links.py b/resources/tester/test_m02_area_old_links.py index fbb18d232..537119355 100644 --- a/resources/tester/test_m02_area_old_links.py +++ b/resources/tester/test_m02_area_old_links.py @@ -2,6 +2,7 @@ - 114-1 면적 상세를 열 때 화면(`sentRanges` + 책)이 `도면_{연결}` 이름정의를 둠 → 행 넣기 → 면적 [저장] → 서버 `refresh_drawing` 이 그 이름을 연결표 줄에 적고 도면 표 칸 글 = 옮겨진 칸. + - 114-2 셀 밀기 · 당기기(칸 삽입 · 삭제 창)에 이름 따라감 · 카드 도면 저장도 표 없는 연결표 줄 뺌(`fitLinks`). TS 는 Node 로(`AreaToDrawing` 은 그 자리 변환 · 스프레드시트 모듈은 `helper_spreadsheet_d_bundle.cjs`). """ @@ -66,7 +67,36 @@ const { applyCommand } = ss("spreadsheet_commands"); names: book.시트[0].이름정의, }; applyCommand(book, { 종류: "행넣기", 시트: "s1", at: 0, 수: 1 }); // 1행 위에 한 행 - console.log(JSON.stringify({ opened, book })); + // 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")] } }), + }; + console.log(JSON.stringify({ opened, book, shifted, fit })); })().catch((e) => { console.error(e); process.exit(1); @@ -134,3 +164,27 @@ def test_옛_연결_열기_행_넣기_저장_도면_표_값_따라감_114_1(worl 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 # 기록 없던 도면은 칸을 안 만듦 diff --git a/resources/tester/test_m02_drawing_card.py b/resources/tester/test_m02_drawing_card.py index c5591e264..a24d0d6cf 100644 --- a/resources/tester/test_m02_drawing_card.py +++ b/resources/tester/test_m02_drawing_card.py @@ -40,9 +40,9 @@ def test_메뉴는_숨기고_저장은_문서_전체(): assert part in card.split("const HIDE_EDIT")[1].split(";\n")[0] # 도번 · 열 · 판 은 읽은 그대로 · 모든 장(지금 탭 하나가 1장을 덮던 흠 · 105-3) assert ( - 'const next = { ...doc, ...((await handle.getDoc()) as Pick) };' + 'const next = fitLinks({\n ...doc,\n ...((await handle.getDoc()) as Pick),\n });' in card - ) + ) # 지운 표의 연결표 줄은 뺌(114-2 · 도면 상세 저장과 같은 몸) assert "async function doSave(): Promise" in card assert 'write("structure", opt.column, version, next)' in card for key in ("L · PL", "E · Delete", "Ctrl+Z · Ctrl+Y", "Ctrl+S"):