diff --git a/A00_Common/spreadsheet/spreadsheet_link_set.ts b/A00_Common/spreadsheet/spreadsheet_link_set.ts index 0a2db64b1..4a8a2de5c 100644 --- a/A00_Common/spreadsheet/spreadsheet_link_set.ts +++ b/A00_Common/spreadsheet/spreadsheet_link_set.ts @@ -70,6 +70,24 @@ export function linkCommand( return commands.length === 1 ? commands[0] : { 종류: "묶음", 명령: commands }; } +/** 알약 여러 키의 연결을 모든 시트에서 한꺼번에 뺌(81-4 [연결 모두 해제]) — + * 바뀐 시트가 둘 이상이면 「묶음」(되돌리기 한 번) · 바뀐 것 없으면 null. */ +export function unlinkAllCommand(book: Workbook, keys: string[]): Command | null { + const keySet = new Set(keys); + const commands: Command[] = []; + for (const s of book.시트) { + const was = s.연결 ?? []; + const next = was.filter((l) => !keySet.has(l.키)); + if (next.length === was.length) continue; + const copy = structuredClone(s); + if (next.length) copy.연결 = next; + else delete copy.연결; + commands.push({ 종류: "시트통째", 시트: copy }); + } + if (!commands.length) return null; + return commands.length === 1 ? commands[0] : { 종류: "묶음", 명령: commands }; +} + /** 면적 문서에 알약 키를 잇기 · 풀기(`칸` null) — 활성 시트에 · 새 문서를 돌려줌(받은 문서는 그대로 · * 번호 등 계약 밖 칸도 그대로) · 칸 주소가 틀리면 null · 같은 칸 다른 알약은 함께 둠(`replace` 면 뺌). 일위대가 [저장] 이 면적 문서를 함께 쓸 때 부름 */ export function linkAreaCell( diff --git a/A00_Common/spreadsheet/spreadsheet_links.css b/A00_Common/spreadsheet/spreadsheet_links.css index 30694cd8e..6b3e5768c 100644 --- a/A00_Common/spreadsheet/spreadsheet_links.css +++ b/A00_Common/spreadsheet/spreadsheet_links.css @@ -89,6 +89,13 @@ color: var(--color-text-secondary); } +/* [연결 모두 해제](81-4) — 자재 추가와 같은 결이나 덜 강조 */ +.ss-link--clear { + border-style: dashed; + color: var(--color-text-secondary); + margin-left: auto; +} + /* 연결 칸 왼아래 세모(초록 오류 세모는 왼위 · 빨강 메모는 오른위) */ .ss-link-dot { position: absolute; diff --git a/A00_Common/spreadsheet/spreadsheet_links.ts b/A00_Common/spreadsheet/spreadsheet_links.ts index e2d31e23a..378402ef9 100644 --- a/A00_Common/spreadsheet/spreadsheet_links.ts +++ b/A00_Common/spreadsheet/spreadsheet_links.ts @@ -14,12 +14,12 @@ import "./spreadsheet_links.css"; import { createMapContextMenu } from "@ui/ui_template_context_menu"; -import { el, showToast } from "@ui/ui_template_elements"; +import { el, showConfirmDialog, showToast } from "@ui/ui_template_elements"; import { inRange, parseA1, parseRange, quoteSheet, toA1 } from "./spreadsheet_address"; import { applyCommand } from "./spreadsheet_commands"; import { askShare } from "./spreadsheet_link_ask"; import { linkHues } from "./spreadsheet_link_color"; -import { linkCommand, linksAt } from "./spreadsheet_link_set"; +import { linkCommand, linksAt, unlinkAllCommand } from "./spreadsheet_link_set"; import type { slotOf } from "./spreadsheet_editor"; import { shiftSpan } from "./spreadsheet_refshift"; import { selectCell } from "./spreadsheet_selection"; @@ -81,6 +81,9 @@ const TEXT = { dup: "이미 목록에 있는 자재", orphan: "일위대가에 없는 알약 — 연결을 풀면 [저장] 때 남음", orphanX: "연결 풀기", + unlinkAll: "연결 모두 해제", + unlinkAllConfirm: "이 줄의 알약 연결을 모두 풀까요? 되돌리기로 되돌릴 수 있습니다.", + unlinkAllDone: "연결을 모두 풀었습니다", }; type RowCol = Extract; @@ -313,10 +316,8 @@ export function mountLinks( } } - function renderBar(): void { - if (!opts) return; - align(); - const map = found(); + /** 알약 줄에 보일 전부(목록 항목 + 문서에만 있는 연결, 풀 수 있게) — [연결 모두 해제] 도 같은 목록을 씀. */ + function shownItems(map: Map): LinkItem[] { const shown: LinkItem[] = []; const seen = new Set(); const add = (item: LinkItem) => { @@ -325,13 +326,35 @@ export function mountLinks( shown.push(item); }; for (const item of items) add(item); - // 문서에 있는 연결(목록에서 빠진 것) — 풀 수 있게 늘 보임 for (const { link } of map.values()) add( link.자재 ? ofMaterial(link.자재) : { 키: link.키, 이름: link.이름 ?? link.키, 단위: link.단위 }, ); + return shown; + } + + /** [연결 모두 해제] — 편집 가능 + 이 줄에 연결 1개 이상일 때만 · 물음 한 번 · 되돌리기 한 번. */ + async function unlinkAll(): Promise { + if (ctx.readOnly) return; + const map = found(); + const keys = shownItems(map) + .map((i) => i.키) + .filter((k) => map.has(k)); + if (!keys.length) return; + if (!(await showConfirmDialog(TEXT.unlinkAllConfirm, TEXT.unlinkAll))) return; + const command = unlinkAllCommand(ctx.book, keys); + if (!command) return; + ctx.dispatch(command); + showToast(TEXT.unlinkAllDone, "info"); + } + + function renderBar(): void { + if (!opts) return; + align(); + const map = found(); + const shown = shownItems(map); const hues = linkHues(map.keys()); const listed = new Set(items.map((i) => i.키)); const sure = opts.모드 === "입력" && opts.확정 === true; @@ -353,6 +376,16 @@ export function mountLinks( more.addEventListener("click", () => openPick(more)); pills.push(more); } + // [연결 모두 해제] — 편집 가능 + 이 줄에 연결 1개 이상일 때만(81-4) + if (!ctx.readOnly && shown.some((item) => map.has(item.키))) { + const clear = el("button", { + className: "ss-link ss-link--clear", + text: TEXT.unlinkAll, + attrs: { type: "button" }, + }); + clear.addEventListener("click", () => void unlinkAll()); + pills.push(clear); + } bar.replaceChildren(...pills); if (fresh) bar.querySelector(".is-here")?.scrollIntoView?.({ block: "nearest", inline: "nearest" }); diff --git a/resources/tester/spreadsheet/spreadsheet_link_set_entry.ts b/resources/tester/spreadsheet/spreadsheet_link_set_entry.ts index 45acbf3be..73e4bc101 100644 --- a/resources/tester/spreadsheet/spreadsheet_link_set_entry.ts +++ b/resources/tester/spreadsheet/spreadsheet_link_set_entry.ts @@ -6,6 +6,7 @@ import { linkAreaCell, linkCommand, linksAt, + unlinkAllCommand, } from "../../../A00_Common/spreadsheet/spreadsheet_link_set"; import type { Command, Workbook } from "../../../A00_Common/spreadsheet/spreadsheet_types"; @@ -152,4 +153,20 @@ eq( linkHues(["철근|ton"]).get("철근|ton"), ); +// unlinkAllCommand(81-4) — 여러 시트에 걸친 키를 한꺼번에 뺌 · 되돌리기 한 번 +b = book(); +undo = run(b, unlinkAllCommand(b, ["깔기|㎡", "치기|㎥"])); +eq("모두 해제 — 두 시트 다 빠짐(연결 칸 없어짐)", links(b), [null, null]); +run(b, undo); +eq("모두 해제 되돌림", links(b), links(book())); + +b = book(); +run(b, unlinkAllCommand(b, ["깔기|㎡"])); +eq("모두 해제 — 한 시트만 바뀌면 시트통째 하나", links(b), [ + null, + [{ 키: "치기|㎥", 이름: "치기", 칸: "C3" }], +]); + +eq("모두 해제 — 바뀔 것 없음 = null", unlinkAllCommand(book(), ["없음"]), null); + console.log(JSON.stringify({ pass, fail: failures.length, failures })); diff --git a/resources/tester/test_spreadsheet_set_link.py b/resources/tester/test_spreadsheet_set_link.py index fbb65186c..02f70679b 100644 --- a/resources/tester/test_spreadsheet_set_link.py +++ b/resources/tester/test_spreadsheet_set_link.py @@ -55,7 +55,7 @@ def test_setLink_순수_길(): assert run.stdout.strip(), run.stderr result = json.loads(run.stdout) assert result["fail"] == 0, result["failures"] - assert result["pass"] == 29 + assert result["pass"] == 33 def test_손잡이_setLink_몸():