diff --git a/A00_Common/spreadsheet/spreadsheet_links.css b/A00_Common/spreadsheet/spreadsheet_links.css index 01a82b2c9..30694cd8e 100644 --- a/A00_Common/spreadsheet/spreadsheet_links.css +++ b/A00_Common/spreadsheet/spreadsheet_links.css @@ -195,3 +195,72 @@ transform: translate(-100%, 0); pointer-events: none; } + +/* 칸을 골랐을 때 그 칸의 알약 — 굵은 테두리 + 바깥 고리(칸이 바뀐 첫 그림만 한 번 반짝) */ +.ss-link.is-here { + outline: 2px solid var(--color-accent); + outline-offset: 2px; + font-weight: 600; +} + +.ss-link.is-pop { + animation: ss-link-pop 1.2s ease-out 1 both; +} + +@keyframes ss-link-pop { + 0%, + 40%, + 80% { + box-shadow: 0 0 0 4px color-mix(in srgb, var(--color-accent) 55%, transparent); + } + 20%, + 60%, + 100% { + box-shadow: 0 0 0 0 transparent; + } +} + +/* 일위대가에 없는 알약 — ⚠ + 점선 붉은 테두리 + [×] */ +.ss-link.is-orphan { + border: 1px dashed var(--color-danger); + color: var(--color-danger); +} + +.ss-link__x { + margin-left: 2px; + padding: 0 4px; + border-radius: 50%; + font-weight: 700; + cursor: pointer; +} + +.ss-link__x:hover { + background: color-mix(in srgb, var(--color-danger) 20%, transparent); +} + +.ss-link-cell.is-orphan { + outline: 1px dashed var(--color-danger); + outline-offset: -2px; +} + +/* 보낸 범위(도면 표) — 점선 테두리(채움 없음) + 왼위 이름표 */ +.ss-link-range { + position: absolute; + z-index: 3; + box-sizing: border-box; + border: 2px dashed var(--color-accent); + pointer-events: none; +} + +.ss-link-range__tag { + position: absolute; + left: -2px; + bottom: 100%; + padding: 0 4px; + border-radius: 3px 3px 0 0; + background: var(--color-accent); + color: var(--color-canvas); + font-size: 10px; + line-height: 14px; + white-space: nowrap; +} diff --git a/A00_Common/spreadsheet/spreadsheet_links.ts b/A00_Common/spreadsheet/spreadsheet_links.ts index 978a9c8b4..7525085a4 100644 --- a/A00_Common/spreadsheet/spreadsheet_links.ts +++ b/A00_Common/spreadsheet/spreadsheet_links.ts @@ -15,7 +15,7 @@ import "./spreadsheet_links.css"; import { createMapContextMenu } from "@ui/ui_template_context_menu"; import { el, showToast } from "@ui/ui_template_elements"; -import { parseA1, quoteSheet, toA1 } from "./spreadsheet_address"; +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"; @@ -53,6 +53,11 @@ export interface LinkOptions { onItemsChange?: (항목: LinkItem[]) => void; /** 자재DB 찾기(빈 글 = 처음 목록) — 자재 모드만 */ 자재찾기?: (글: string) => Promise; + /** 입력 모드 — 호스트가 항목 목록을 믿을 수 있게 받았음(받기 실패 · 빈 목록 구분) · true 일 때만 + * 목록에 없는 연결 알약에 ⚠ + [×] */ + 확정?: boolean; + /** 다른 곳으로 보낸 칸 범위 표시(예: 도면 표) — 시트 id · 범위 · 이름표 · 마우스 올림 글 */ + 표시범위?: { 시트: string; 범위: string; 이름: string; 설명?: string }[]; } export interface Links extends PartHandle { @@ -74,6 +79,8 @@ const TEXT = { fail: "자재 목록을 못 받음", picked: "칸을 고른 뒤 새 알약을 누르면 연결", dup: "이미 목록에 있는 자재", + orphan: "일위대가에 없는 알약 — 연결을 풀면 [저장] 때 남음", + orphanX: "연결 풀기", }; type RowCol = Extract; @@ -146,6 +153,8 @@ export function mountLinks( let dragging: LinkItem | null = null; const dropMark = el("div", { className: "ss-link-drop" }); let pick: HTMLElement | null = null; + /** 마지막으로 강조를 판정한 칸(시트 id!칸) — 바뀔 때만 반짝 · 스크롤 */ + let lastAt = ""; /** 키 → 연결(시트 · 줄) */ function found(): Map { @@ -204,6 +213,8 @@ export function mountLinks( item: LinkItem, hit: { sheet: Sheet; link: CellLink } | undefined, hue: number | undefined, + orphan: boolean, + here: string, ) { const where = hit ? hit.sheet.id === ctx.selection.시트 @@ -211,15 +222,30 @@ export function mountLinks( : `${quoteSheet(hit.sheet.이름)}!${hit.link.칸}` : ""; const b = el("button", { - className: hit ? "ss-link is-linked" : "ss-link", - attrs: { type: "button", title: hit ? TEXT.linked : TEXT.free, "data-key": item.키 }, + className: `ss-link${hit ? " is-linked" : ""}${orphan ? " is-orphan" : ""}${ + here ? ` is-here${here === "new" ? " is-pop" : ""}` : "" + }`, + attrs: { + type: "button", + title: orphan ? TEXT.orphan : hit ? TEXT.linked : TEXT.free, + "data-key": item.키, + }, children: [ - el("span", { text: item.이름 }), + el("span", { text: orphan ? `⚠ ${item.이름}` : item.이름 }), ...(item.단위 ? [el("small", { text: item.단위 })] : []), ...(where ? [el("code", { text: where })] : []), ], }); if (hue !== undefined) b.style.setProperty("--ss-link-h", String(hue)); + if (orphan && !ctx.readOnly) { + const x = el("span", { + className: "ss-link__x", + text: "×", + attrs: { role: "button", title: TEXT.orphanX }, + }); + x.addEventListener("click", (e) => (e.stopPropagation(), unlink(), back())); + b.append(x); + } b.draggable = !ctx.readOnly; b.addEventListener("dragstart", (e) => { dragging = item; @@ -233,13 +259,13 @@ export function mountLinks( // 초점은 격자에 둠 — 끌 수 있는 알약은 막으면 끌기가 안 돼 그대로(누른 뒤 `back` 이 격자로 돌림) if (!b.draggable) b.addEventListener("mousedown", (e) => e.preventDefault()); b.addEventListener("click", () => (hit ? goTo(hit.sheet, hit.link.칸) : void link(item))); - const unlink = (): void => { + function unlink(): void { if (hit) put( hit.sheet, (hit.sheet.연결 ?? []).filter((l) => l.키 !== item.키), ); - }; + } b.addEventListener("contextmenu", (e) => { e.preventDefault(); const inList = opts?.모드 === "자재" && !item.고정 && items.some((i) => i.키 === item.키); @@ -307,7 +333,17 @@ export function mountLinks( : { 키: link.키, 이름: link.이름 ?? link.키, 단위: link.단위 }, ); const hues = linkHues(map.keys()); - const pills = shown.map((item) => pill(item, map.get(item.키), hues.get(item.키))); + const listed = new Set(items.map((i) => i.키)); + const sure = opts.모드 === "입력" && opts.확정 === true; + // 칸을 고르면 그 칸에 이은 알약 강조 — 칸이 바뀐 첫 그림만 한 번 반짝("new") + const at = `${ctx.selection.시트}!${toA1(ctx.selection.활성.r, ctx.selection.활성.c)}`; + const fresh = at !== lastAt; + lastAt = at; + const pills = shown.map((item) => { + const hit = map.get(item.키); + const here = hit && `${hit.sheet.id}!${hit.link.칸}` === at ? (fresh ? "new" : "same") : ""; + return pill(item, hit, hues.get(item.키), sure && !!hit && !listed.has(item.키), here); + }); if (opts.모드 === "자재" && opts.자재찾기 && !ctx.readOnly) { const more = el("button", { className: "ss-link ss-link--add", @@ -318,6 +354,8 @@ export function mountLinks( pills.push(more); } bar.replaceChildren(...pills); + if (fresh) + bar.querySelector(".is-here")?.scrollIntoView?.({ block: "nearest", inline: "nearest" }); } /** 이은 칸 — 알약 색 옅은 배경 + 왼아래 작은 세모 · 칸이 든 판 층에 얹어 스크롤을 따라감 */ @@ -325,6 +363,8 @@ export function mountLinks( for (const d of dots) d.remove(); dots = []; const hues = linkHues(found().keys()); + const listed = new Set(items.map((i) => i.키)); + const sure = opts?.모드 === "입력" && opts.확정 === true; // 칸마다 이은 알약(이은 차례) — 색 = 먼저 이은 알약 · 둘 넘으면 수 const byCell = new Map(); for (const l of ctx.sheet().연결 ?? []) byCell.set(l.칸, [...(byCell.get(l.칸) ?? []), l]); @@ -340,6 +380,7 @@ export function mountLinks( dot.style.left = `${box.x}px`; dot.style.top = `${box.y + box.h - 7}px`; const marks = [tint, dot]; + if (sure && !listed.has(first.키)) tint.classList.add("is-orphan"); if (list.length > 1) { const count = el("div", { className: "ss-link-count", text: String(list.length) }); count.style.left = `${box.x + box.w}px`; @@ -350,6 +391,22 @@ export function mountLinks( layer.append(...marks); dots.push(...marks); } + // 보낸 범위(도면 표 등) — 채움 없는 점선 테두리 + 왼위 이름표(알약 색 채움과 안 겹침) + for (const mark of opts?.표시범위 ?? []) { + const g = mark.시트 === ctx.sheet().id ? parseRange(mark.범위) : null; + if (!g) continue; + const { layer, box } = slot(ctx, g.r0, g.c0); + const end = slot(ctx, g.r1, g.c1).box; + if (!box.w || !box.h) continue; + const frame = el("div", { + className: "ss-link-range", + attrs: { "data-range": mark.범위 }, + children: [el("span", { className: "ss-link-range__tag", text: mark.이름 })], + }); + frame.style.cssText = `left:${box.x}px;top:${box.y}px;width:${Math.max(box.w, end.x + end.w - box.x)}px;height:${Math.max(box.h, end.y + end.h - box.y)}px`; + layer.append(frame); + dots.push(frame); + } } /** 칸에 마우스를 올리면 이은 알약 이름들(격자 title) — 이은 칸이 아니면 지움 */ @@ -360,6 +417,11 @@ export function mountLinks( const names = a1 ? (ctx.sheet().연결 ?? []).filter((l) => l.칸 === a1).map((l) => l.이름 ?? l.키) : []; + if (hit && hit.r >= 0 && hit.c >= 0) + for (const m of opts?.표시범위 ?? []) { + const g = m.시트 === ctx.sheet().id ? parseRange(m.범위) : null; + if (g && inRange(g, hit.r, hit.c)) names.push(m.설명 ?? m.이름); + } if (names.length) ctx.grid.root.title = names.join(" · "); else if (tipped) ctx.grid.root.removeAttribute("title"); tipped = names.length > 0; diff --git a/M02_MasterTemplete/M02_MasterTemplete_AreaToDrawing.ts b/M02_MasterTemplete/M02_MasterTemplete_AreaToDrawing.ts index f686ed1e3..da16b9918 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_AreaToDrawing.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_AreaToDrawing.ts @@ -19,7 +19,13 @@ import { showToast, } from "@ui/ui_template_elements"; import { openModal } from "@ui/ui_template_modal"; -import { getActiveOwner, type Layer, type StructureDoc } from "./M02_MasterTemplete_Api_Fetch"; +import { + getActiveOwner, + readTemplate, + type Layer, + type StructureDoc, +} from "./M02_MasterTemplete_Api_Fetch"; +import type { LinkOptions } from "../A00_Common/spreadsheet/spreadsheet"; import type { Selection } from "./M02_MasterTemplete_UI_Main"; /** 표 칸 하나(CAD `TableCell` 꼴에서 쓰는 것만) */ @@ -71,6 +77,8 @@ const TEXT = { noPlace: "이 도면 화면은 표 놓기를 아직 못 함", stale: "면적산출 값이 바뀐 표 {n}개", update: "업데이트", + tag: "{id} 도면", + tip: "도면 표 {id} 로 보낸 범위 {range} · 글자 {size}", updated: "표 {n}개를 지금 면적 값으로 바꿈 — [저장]하면 남음", } as const; @@ -112,6 +120,22 @@ const sameValues = (a: Record = {}, b: Record = return ka.length === Object.keys(b).length && ka.every((k) => a[k] === b[k]); }; +/** 면적 시트에 겹쳐 보일 「도면으로 보낸 범위」 — 같은 열 도면 `연결표` 에서(받기 실패 = 없음 · 조용히) */ +export async function sentRanges(at: Selection): Promise> { + try { + const got = await readTemplate(at.layer, "structure", at.name, at.projectId); + const rows = (got.문서 as StructureDoc | undefined)?.연결표 ?? []; + return rows.map((row) => ({ + 시트: row.시트, + 범위: row.범위, + 이름: fill(TEXT.tag, "id", row.연결), + 설명: fill(fill(fill(TEXT.tip, "id", row.연결), "range", row.범위), "size", row.글자크기), + })); + } catch { + return []; + } +} + export interface AreaToDrawingOptions { /** 같은 페이지에서 그 열 도면 상세를 엶(`UI_Main.open`) */ open: (sel: Selection) => Promise; diff --git a/M02_MasterTemplete/M02_MasterTemplete_Structure.ts b/M02_MasterTemplete/M02_MasterTemplete_Structure.ts index 11c49c3d3..8bf976944 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Structure.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Structure.ts @@ -160,7 +160,9 @@ export async function pillsFor( ...(p.자재 ? { 자재: p.자재 } : {}), 고정: true, })); - return 항목.length ? { links: { 항목, 모드: "입력" } } : none(TEXT.noPills); + // 받기 성공 = 목록 확정 — 이 목록에 없는 연결 알약만 ⚠(받기 실패 때는 표시 안 함) + const links: LinkOptions = { 항목, 모드: "입력", 확정: true }; + return 항목.length ? { links } : { ...none(TEXT.noPills), links }; } catch (error) { return none(fill(TEXT.pillsFailed, why(error))); } diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts b/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts index 6ebe70b4c..82d6282bb 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts @@ -28,7 +28,7 @@ import { } from "./M02_MasterTemplete_Structure"; import { askSettle } from "./M02_MasterTemplete_TreePanel_Dialogs"; import { buildAreaPanel, type AreaPanel } from "./M02_MasterTemplete_AreaPanel"; -import { createAreaToDrawing } from "./M02_MasterTemplete_AreaToDrawing"; +import { createAreaToDrawing, sentRanges } from "./M02_MasterTemplete_AreaToDrawing"; import { detailHelp, HELP_TITLE } from "./M02_MasterTemplete_CardHelp"; import { helpParts } from "./M02_MasterTemplete_CardShell"; import { isPrecombo } from "./M02_MasterTemplete_UI_Side_Precombo"; @@ -274,13 +274,14 @@ export function createMain( ): Promise { const mine = seq; try { - const [{ createSpreadsheet }, pills] = await Promise.all([ + const [{ createSpreadsheet }, pills, sent] = await Promise.all([ import("../A00_Common/spreadsheet/spreadsheet"), pillsFor(kind, at.name), + sentRanges(at), // 도면으로 보낸 범위 — 시트 위 점선 테두리 ]); if (mine !== seq) return; if (pills.notice) showNotice([pills.notice]); - const links: LinkOptions = pills.links; + const links: LinkOptions = { ...pills.links, 표시범위: sent }; const handle: SpreadsheetHandle = createSpreadsheet( host, doc as Parameters[1], diff --git a/resources/tester/spreadsheet/harness_links.ts b/resources/tester/spreadsheet/harness_links.ts index ea6b0ff85..6572337da 100644 --- a/resources/tester/spreadsheet/harness_links.ts +++ b/resources/tester/spreadsheet/harness_links.ts @@ -6,7 +6,10 @@ import { createSpreadsheet } from "../../../A00_Common/spreadsheet/spreadsheet"; import type { LinkMaterial, Workbook } from "../../../A00_Common/spreadsheet/spreadsheet"; const w = window as unknown as Record; -const mode = new URLSearchParams(location.search).get("mode") === "자재" ? "자재" : "입력"; +const q = new URLSearchParams(location.search); +const mode = q.get("mode") === "자재" ? "자재" : "입력"; +// ?ghost=1 — 목록 확정 + 목록에 없는 연결 알약 X(B1) · 목록 알약 L(B2) / ?range=1 — 도면 보낸 범위 A1:B2 +const ghost = q.get("ghost") === "1"; const book: Workbook = { 종류: "통합문서", @@ -29,6 +32,11 @@ const book: Workbook = { ], 활성: "s1", }; +if (ghost) + book.시트[0].연결 = [ + { 키: "X", 칸: "B1", 이름: "지운로직", 단위: "m" }, + { 키: "L", 칸: "B2", 이름: "길이", 단위: "m" }, + ]; const MATERIALS: LinkMaterial[] = [ { 코드: "M001", 이름: "레미콘", 규격: "25-21-12", 단위: "m³" }, @@ -59,6 +67,14 @@ w.ss = createSpreadsheet(document.getElementById("host")!, book, { { 키: "H", 이름: "높이", 단위: "m" }, { 키: "A", 이름: "면적", 단위: "m²" }, ], + ...(ghost ? { 확정: true } : {}), + ...(q.get("range") === "1" + ? { + 표시범위: [ + { 시트: "s1", 범위: "A1:B2", 이름: "T1 도면", 설명: "도면 표 T1 로 보낸 범위" }, + ], + } + : {}), onItemsChange: (목록) => (w.ssItems as unknown[]).push(목록), 자재찾기: async (글) => { asked.push(글); diff --git a/resources/tester/test_m02_area_marks.py b/resources/tester/test_m02_area_marks.py new file mode 100644 index 000000000..f542edb8c --- /dev/null +++ b/resources/tester/test_m02_area_marks.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +"""50-1 면적산출 알약 · 보낸 범위 표시 — 원문 규칙 점검(화면 수치는 ui_checks/50-1_*.js 를 ORCA 로).""" + +import os + +HERE = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.join(HERE, "..", "..") + + +def _read(*parts: str) -> str: + with open(os.path.join(ROOT, *parts), encoding="utf-8") as f: + return f.read() + + +def test_orphan_mark_only_when_list_confirmed(): + # ⚠ 는 호스트가 목록을 확정했을 때만 — 알약 받기 실패(확정 없음)는 표시 안 함 + links = _read("A00_Common", "spreadsheet", "spreadsheet_links.ts") + assert 'opts.모드 === "입력" && opts.확정 === true' in links + structure = _read("M02_MasterTemplete", "M02_MasterTemplete_Structure.ts") + assert "확정: true" in structure + assert structure.count("확정: true") == 1 # 실패 길(none)은 확정 없음 + + +def test_cell_pick_highlights_pill_and_range_is_unfilled(): + links = _read("A00_Common", "spreadsheet", "spreadsheet_links.ts") + assert "is-here" in links and "scrollIntoView" in links + css = _read("A00_Common", "spreadsheet", "spreadsheet_links.css") + block = css.split(".ss-link-range {", 1)[1].split("}", 1)[0] + assert "dashed" in block and "background" not in block # 채움 없음 = 알약 색과 안 겹침 + + +def test_sent_ranges_from_same_column_drawing(): + src = _read("M02_MasterTemplete", "M02_MasterTemplete_AreaToDrawing.ts") + assert 'readTemplate(at.layer, "structure", at.name, at.projectId)' in src + assert '"{id} 도면"' in src diff --git a/resources/tester/ui_checks/50-1_보낸범위_읽기.js b/resources/tester/ui_checks/50-1_보낸범위_읽기.js new file mode 100644 index 000000000..0f4353814 --- /dev/null +++ b/resources/tester/ui_checks/50-1_보낸범위_읽기.js @@ -0,0 +1,42 @@ +/* 50-1 면적 시트 「도면으로 보낸 범위」 읽기(`sentRanges`) — orca eval 한 번(서버 쓰기 없음 · 가짜 응답). + * 시작 화면: 로그인한 M02(#/m02-master-template?open=구조물조합&root=master) 아무 화면. + * 하는 일: 도면 읽기 길만 가짜 연결표(T1 · T3)로 바꿔 부름 → 그 답 · 실패 때(500) 빈 목록 → fetch 되돌림. + * 기대(out): rows = 2 · tag = ["T1 도면","T3 도면"] · tip0 = 「도면 표 T1 로 보낸 범위 A1:B2 · 글자 2.5」 · + * sheet0 = 가짜 시트 id · failed = [] . + */ +(async () => { + const mod = + await import("/@fs/C:/Aislo_wt/sub_laptop_3/M02_MasterTemplete/M02_MasterTemplete_AreaToDrawing.ts"); + const real = window.fetch; + const at = { layer: "system", projectId: null, kind: "area", name: "item1", label: "item1" }; + const out = {}; + try { + window.fetch = async (url, init) => { + if (!String(url).includes("/templates/structure/item1")) return real(url, init); + return new Response( + JSON.stringify({ + 종류: "structure", + 이름: "item1", + 판: "1", + 문서: { + 연결표: [ + { 연결: "T1", 시트: "s9", 범위: "A1:B2", 글자크기: 2.5, 값: {} }, + { 연결: "T3", 시트: "s9", 범위: "C3:D5", 글자크기: 3, 값: {} }, + ], + }, + }), + { status: 200, headers: { "Content-Type": "application/json" } }, + ); + }; + const rows = await mod.sentRanges(at); + out.rows = rows.length; + out.tag = rows.map((r) => r.이름); + out.tip0 = rows[0]?.설명; + out.sheet0 = rows[0]?.시트; + window.fetch = async () => new Response("{}", { status: 500 }); + out.failed = await mod.sentRanges(at); + } finally { + window.fetch = real; + } + return JSON.stringify(out); +})(); diff --git a/resources/tester/ui_checks/50-1_알약_강조_표시.js b/resources/tester/ui_checks/50-1_알약_강조_표시.js new file mode 100644 index 000000000..10004fb5b --- /dev/null +++ b/resources/tester/ui_checks/50-1_알약_강조_표시.js @@ -0,0 +1,52 @@ +/* 50-1 면적산출 알약 · 칸 · 보낸 범위 표시 — orca eval 한 번에 도는 시나리오(저장 · 서버 없음). + * 시작 화면: /@fs/C:/Aislo_wt/sub_laptop_3/resources/tester/spreadsheet/harness_links.html?ghost=1&range=1 (알약 L · H · A · 연결 X=B1 · L=B2 · 범위 A1:B2). + * 하는 일: ① 알약 줄 ② 보낸 범위 상자 ③ 칸 B1 · B2 를 눌러 그 칸 알약 강조 ④ X 의 [×] 로 연결 풀기 + * 기대(out): orphan = X 만 ⚠ · 목록 알약 L 은 아님 · rangeTag "T1 도면" · rangeFill 투명 · rangeDashed true · + * hereB1 = ["X"] · hereB2 = ["L"] · popOnce true(칸이 바뀐 첫 그림만) · xGone true. + */ +(async () => { + const W = (t) => new Promise((r) => setTimeout(r, t)); + const q = (s) => document.querySelector(s); + const all = (s) => [...document.querySelectorAll(s)]; + const click = (el) => { + const r = el.getBoundingClientRect(); + const at = { + bubbles: true, + button: 0, + clientX: r.left + r.width / 2, + clientY: r.top + r.height / 2, + }; + const t = document.elementFromPoint(at.clientX, at.clientY) ?? el; + for (const type of ["mousedown", "mouseup", "click"]) t.dispatchEvent(new MouseEvent(type, at)); + }; + await W(800); + const out = {}; + const pill = (k) => q(`.ss-link[data-key="${k}"]`); + out.orphan = all(".ss-link.is-orphan").map((b) => b.dataset.key); + out.xText = pill("X")?.textContent; + out.lOrphan = pill("L")?.classList.contains("is-orphan"); + const range = q(".ss-link-range"); + const cs = range && getComputedStyle(range); + out.rangeTag = range?.querySelector(".ss-link-range__tag")?.textContent; + out.rangeFill = cs?.backgroundColor; + out.rangeDashed = cs?.borderTopStyle === "dashed"; + out.rangeSize = range && [Math.round(range.offsetWidth), Math.round(range.offsetHeight)]; + const byKey = (k) => all(".ss-link-cell").find((t) => t.dataset.key === k); // 칸 표시는 고를 때마다 새로 얹힘 + out.cellOrphan = byKey("X")?.classList.contains("is-orphan"); + const here = () => all(".ss-link.is-here").map((b) => b.dataset.key); + click(byKey("X")); // B1 + await W(300); + out.hereB1 = here(); + out.popOnce = all(".ss-link.is-pop").length === 1; + click(byKey("L")); // B2 + await W(300); + out.hereB2 = here(); + const doc0 = window.ss.getDoc(); + const n0 = doc0.시트[0].연결.length; + click(pill("X").querySelector(".ss-link__x")); + await W(300); + const n1 = window.ss.getDoc().시트[0].연결.length; + out.xGone = n1 === n0 - 1 && !pill("X"); + out.xCell = !all(".ss-link-cell").some((t) => t.dataset.key === "X"); + return out; +})();