Files
Aislo/A00_Common/spreadsheet/spreadsheet_link_range.ts
T
eomsangdonandClaude Opus 5.5 8352e2e5b8 feat(m02): 면적 아래 알약 막대에 도면 알약 — 범위 · 상태 · 누르면 고름 · × 보내기 취소(114-3)
- 「T1 도면 E7:J17」 + 공용 꼬리표 상태(값 맞음 · 값 다름 · 도면에 표 없음 · 범위 없음) · 테두리 = 시트 위 점선 색
- 상태는 지금 책으로 판정(areaTable → sameValues) · 값이 바뀌면 150ms 모아 다시
- × = 기존 [보내기 취소](화면에서만 · 면적 저장 때 기록) · 시트 위 이름표 그대로
- 보낸 범위 몫을 spreadsheet_link_range.ts 로 나눔(700줄)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgChuAcyBqUkhHMTXomQC7
2026-10-08 18:33:53 +09:00

162 lines
6.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =============================================================================
* spreadsheet_link_range.ts (주인 D)
* 보낸 범위(`LinkOptions.표시범위` · 예: 도면 표) 몫 — `spreadsheet_links` 가 씀.
* 지금 범위 풀기(104-1 · 이름 있으면 이름이 가리키는 범위) · 색 열쇠 ·
* 아래 알약 막대 알약(114-3 · 「T1 도면 E7:J17」 + 상태 꼬리표 · 색 = 시트 위 테두리 색 · 누르면 범위 고름 ·
* × = 보내기 취소) · 상태 모음(값이 바뀌면 150ms 모았다 호스트 `상태` 를 한 번에 부름).
* ========================================================================== */
import { createTag, el } from "@ui/ui_template_elements";
import { parseRange, quoteSheet, rangeToA1 } from "./spreadsheet_address";
import { linkHues } from "./spreadsheet_link_color";
import type { LinkOptions } from "./spreadsheet_links";
import { resolveName } from "./spreadsheet_names";
import { selectSpan } from "./spreadsheet_selection";
import type { CellRange, Workbook } from "./spreadsheet_types";
import type { SpreadsheetContext } from "./spreadsheet_view_types";
export type RangeMark = NonNullable<LinkOptions["표시범위"]>[number];
/** 보낸 범위 상태(114-3) — 꼬리표 글 · 색 갈래(공용 `createTag`) · 마우스 올림 글 */
export interface RangeState {
글: string;
종류: "success" | "warning" | "danger";
설명?: string;
}
const STATE_MS = 150; // 칸 고침이 이어지는 동안 모았다가 한 번에
/** 보낸 범위의 지금 범위 — `범위이름` 있으면 그 이름이 지금 가리키는 범위로(행열 넣기 · 지우기를 따라감) ·
* 이름이 지워졌으면 null(서버가 다음 저장 때 연결을 끊음) · 이름이 없으면(옛 연결) 적힌 `범위` 그대로 */
export function markRange(book: Workbook, mark: RangeMark): { 시트: string; g: CellRange } | null {
if (mark.범위이름) {
const found = resolveName(book, mark.시트, mark.범위이름);
return found && { 시트: found.시트, g: found.범위 };
}
const g = parseRange(mark.범위);
return g && { 시트: mark.시트, g };
}
/** 색 열쇠 — 범위 이름 없으면 시트!범위(표마다 다른 색 · 알약 색과는 다른 자리) */
export const markKey = (m: RangeMark): string => m.범위이름 ?? `${m.시트}!${m.범위}`;
/** 상태 모음 — `schedule` 마다 150ms 모았다 `상태` 있는 표시마다 부름 · 늦은 답은 버림 · 끝나면 `done` */
export function createRangeStates(
marks: () => RangeMark[],
book: () => Workbook,
done: () => void,
): { get: (m: RangeMark) => RangeState | undefined; schedule: () => void; stop: () => void } {
const got = new Map<RangeMark, RangeState>();
let timer: ReturnType<typeof setTimeout> | undefined;
let seq = 0;
async function run(): Promise<void> {
const mine = ++seq;
const list = marks().filter((m) => m.상태);
const b = book();
const out = await Promise.all(
list.map((m) =>
Promise.resolve()
.then(() => m.상태!(b))
.catch(() => undefined),
),
);
if (mine !== seq) return;
got.clear();
list.forEach((m, i) => out[i] && got.set(m, out[i]!));
done();
}
return {
get: (m) => got.get(m),
schedule: () => {
if (!marks().some((m) => m.상태)) return;
clearTimeout(timer);
timer = setTimeout(() => void run(), STATE_MS);
},
stop: () => {
clearTimeout(timer);
seq += 1;
},
};
}
type Box = { x: number; y: number; w: number; h: number };
/** 시트 위 점선 테두리 + 왼위 이름표(104-3) — 겹치면(dup) 테두리는 안쪽으로 · 이름표는 옆으로 · 이벤트는 부른 쪽 */
export function rangeFrame(
mark: RangeMark,
box: Box,
end: Box,
dup: number,
hue: number,
tip: string,
): { frame: HTMLElement; tag: HTMLElement } {
const w = Math.max(box.w, end.x + end.w - box.x);
const h = Math.max(box.h, end.y + end.h - box.y);
const inset = dup * 3;
const tag = el("span", {
className: "ss-link-range__tag",
text: mark.이름,
attrs: { title: mark.설명 ?? tip, role: "button", tabindex: "0" },
});
tag.style.left = `${dup * 44}px`;
const frame = el("div", {
className: "ss-link-range",
attrs: { "data-range": mark.범위 },
children: [tag],
});
frame.style.cssText = `left:${box.x + inset}px;top:${box.y + inset}px;width:${Math.max(w - inset * 2, 0)}px;height:${Math.max(h - inset * 2, 0)}px`;
frame.style.setProperty("--ss-link-h", String(hue));
return { frame, tag };
}
/** 아래 막대 도면 알약 — 「T1 도면 E7:J17」(다른 시트면 시트!범위) · 상태 꼬리표 · 색 = 시트 위 테두리 ·
* 누르면 그 범위 고름(다른 시트면 그 시트로) · × = `cancel`(`취소` 있고 고칠 수 있을 때만) */
export function rangePills(
ctx: SpreadsheetContext,
marks: RangeMark[],
stateOf: (m: RangeMark) => RangeState | undefined,
back: () => void,
cancel: (m: RangeMark) => void,
tips: { pick: string; cancel: string },
): HTMLButtonElement[] {
const hues = linkHues(marks.map(markKey));
return marks.map((mark) => {
const found = markRange(ctx.book, mark);
const sheet = found && ctx.book.시트.find((s) => s.id === found.시트);
const a1 = found && rangeToA1(found.g);
const where =
a1 && sheet && found.시트 !== ctx.selection.시트 ? `${quoteSheet(sheet.이름)}!${a1}` : a1;
const state = stateOf(mark);
const tag = state && createTag(state.글, state.종류);
if (tag && state?.설명) tag.title = state.설명;
const b = el("button", {
className: "ss-link ss-link--range",
attrs: { type: "button", title: `${mark.설명 ?? mark.이름} · ${tips.pick}` },
children: [
el("span", { text: where ? `${mark.이름} ${where}` : mark.이름 }),
...(tag ? [tag] : []),
],
});
b.style.setProperty("--ss-link-h", String(hues.get(markKey(mark)) ?? 0));
b.addEventListener("mousedown", (e) => e.preventDefault()); // 초점은 격자에
b.addEventListener("click", () => {
if (found) {
if (found.시트 !== ctx.selection.시트) ctx.showSheet(found.시트);
const { g } = found;
ctx.select(selectSpan(ctx.sheet(), { r: g.r0, c: g.c0 }, { r: g.r1, c: g.c1 }), true);
}
back();
});
if (mark.취소 && !ctx.readOnly) {
const x = el("span", {
className: "ss-link__x",
text: "×",
attrs: { role: "button", title: tips.cancel },
});
x.addEventListener("click", (e) => (e.stopPropagation(), cancel(mark)));
b.append(x);
}
return b;
});
}