feat(spreadsheet): 알약 줄 끝 연결 모두 해제(81-4)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MPcuokNm1ZR2kqgGGDfaJA
This commit is contained in:
2026-10-08 13:04:09 +09:00
co-authored by Claude Sonnet 5
parent 3f5dfd47b1
commit e0cbb1f17d
5 changed files with 83 additions and 8 deletions
@@ -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<T extends Workbook>(
@@ -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;
+40 -7
View File
@@ -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<Command, { 종류: "행넣기" | "행지우기" | "열넣기" | "열지우기" }>;
@@ -313,10 +316,8 @@ export function mountLinks(
}
}
function renderBar(): void {
if (!opts) return;
align();
const map = found();
/** 알약 줄에 보일 전부(목록 항목 + 문서에만 있는 연결, 풀 수 있게) — [연결 모두 해제] 도 같은 목록을 씀. */
function shownItems(map: Map<string, { sheet: Sheet; link: CellLink }>): LinkItem[] {
const shown: LinkItem[] = [];
const seen = new Set<string>();
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<void> {
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" });
@@ -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 }));
@@ -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_몸():