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:
@@ -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;
|
||||
|
||||
@@ -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_몸():
|
||||
|
||||
Reference in New Issue
Block a user