feat(spreadsheet): 손잡이 setLink — 알약 키를 칸에 잇기 · 풀기 · 알약 누름과 같은 규칙 · 되돌리기 됨 (PLAN 39-4 D4)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W4uKaV4ZBhsJN4GHG7yRj4
This commit is contained in:
2026-10-04 17:23:01 +09:00
co-authored by Claude Opus 5.5
parent 1871b6ffc2
commit 8739148bba
4 changed files with 228 additions and 0 deletions
+13
View File
@@ -24,6 +24,7 @@ import { createHistory } from "./spreadsheet_history";
import { attachKeys } from "./spreadsheet_keys";
import type { ConditionOptions } from "./spreadsheet_condition";
import type { LinkOptions, Links } from "./spreadsheet_links";
import { linkCommand, type LinkInfo } from "./spreadsheet_link_set";
import { attachMenu } from "./spreadsheet_menu";
import { attachMouse } from "./spreadsheet_mouse";
import { LAST_C, LAST_R, selectCell } from "./spreadsheet_selection";
@@ -48,6 +49,7 @@ import type {
export type { Workbook, CalcValues, CalcValue, LinkMaterial } from "./spreadsheet_types";
export type { LinkItem, LinkOptions } from "./spreadsheet_links";
export type { LinkInfo } from "./spreadsheet_link_set";
export interface SpreadsheetOptions {
/** 읽기만 — 도구 모음 · 메뉴 없음 · 편집 · 붙여넣기 · 끌기 막음(고르기 · 복사는 됨) */
@@ -76,6 +78,9 @@ export interface SpreadsheetHandle {
recalc(): CalcValues;
/** 알약 키 → 연결 칸 값(빈 칸 0) */
getLinkedValues(): Record<string, CalcValue>;
/** 알약 키를 지금 시트 `칸`(A1)에 잇기 · 풀기(null) — 알약 누름과 같은 규칙 · 되돌리기 됨 ·
* 주소가 틀리거나 읽기만이면 false */
setLink(key: string, cell: string | null, info?: LinkInfo): boolean;
destroy(): void;
}
@@ -288,6 +293,14 @@ export function createSpreadsheet(
}
return out;
},
setLink(key, cell, info) {
if (readOnly) return false;
const command = linkCommand(ctx.book, ctx.sheet().id, key, cell, info);
if (command === false) return false;
if (command) dispatch(command);
loadLinks(); // 처음 연결이면 알약 막대 · 칸 세모를 불러옴
return true;
},
destroy() {
clearTimeout(toastTimer);
detachKeys();
@@ -0,0 +1,55 @@
/* =============================================================================
* spreadsheet_link_set.ts (주인 D)
* 알약 키를 칸에 잇기 · 풀기 — 밖(M02 칸 지정 모달)에서 부르는 길(손잡이 `setLink`).
* 알약 누름(`spreadsheet_links.ts` `link`)과 같은 규칙: 키 하나 = 칸 하나 · 칸 하나 = 키 하나 ·
* 다른 시트에 있던 같은 키 연결은 빠짐. 바꿈은 `시트통째`(되돌리기 짝) · DOM 을 모름(시험이 Node 로 부름).
* ========================================================================== */
import { parseA1, toA1 } from "./spreadsheet_address";
import type { CellLink, Command, LinkMaterial, Workbook } from "./spreadsheet_types";
/** 연결에 같이 적을 알약 정보 — 없으면 있던 연결 것을 그대로 */
export interface LinkInfo {
이름?: string;
단위?: string;
자재?: LinkMaterial;
}
/** `칸`(A1 · null = 풀기)을 `sheetId` 시트에 이을 명령 — 바뀔 것 없으면 null · 칸 주소가 틀리면 false */
export function linkCommand(
book: Workbook,
sheetId: string,
key: string,
cell: string | null,
info: LinkInfo = {},
): Command | null | false {
const at = cell === null ? null : parseA1(cell.trim().toUpperCase());
if (cell !== null && !at) return false;
const a1 = at ? toA1(at.r, at.c) : null;
const old = book.시트.flatMap((s) => s.연결 ?? []).find((l) => l.키 === key);
const home = book.시트.find((s) => s.id === sheetId);
if (a1 && !home) return false;
if (a1 && home?.연결?.some((l) => l.키 === key && l.칸 === a1)) return null; // 이미 그 칸
const one: CellLink | null = a1 ? { 키: key, 칸: a1 } : null;
if (one) {
const 이름 = info.이름 ?? old?.이름;
const 단위 = info.단위 ?? old?.단위;
const 자재 = info.자재 ?? old?.자재;
if (이름) one.이름 = 이름;
if (단위) one.단위 = 단위;
if (자재) one.자재 = 자재;
}
const commands: Command[] = [];
for (const s of book.시트) {
const was = s.연결 ?? [];
const rest = was.filter((l) => l.키 !== key && !(s === home && a1 && l.칸 === a1));
const next = s === home && one ? [...rest, one] : rest;
if (next.length === was.length && next.every((l, i) => l === was[i])) 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 };
}