diff --git a/A00_Common/spreadsheet/spreadsheet_link_range.ts b/A00_Common/spreadsheet/spreadsheet_link_range.ts new file mode 100644 index 000000000..a8fa5c7af --- /dev/null +++ b/A00_Common/spreadsheet/spreadsheet_link_range.ts @@ -0,0 +1,161 @@ +/* ============================================================================= + * 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[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(); + let timer: ReturnType | undefined; + let seq = 0; + async function run(): Promise { + 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; + }); +} diff --git a/A00_Common/spreadsheet/spreadsheet_links.css b/A00_Common/spreadsheet/spreadsheet_links.css index 6d3f1c637..3e63d0d85 100644 --- a/A00_Common/spreadsheet/spreadsheet_links.css +++ b/A00_Common/spreadsheet/spreadsheet_links.css @@ -279,6 +279,12 @@ cursor: pointer; } +/* 아래 막대 도면 알약(114-3) — 테두리 = 시트 위 그 범위 테두리 색(점선) · 상태는 공용 꼬리표(ui-tag) */ +.ss-link--range { + align-items: center; + border: 2px dashed hsl(var(--ss-link-h, 0) var(--ss-link-s) var(--ss-link-l)); +} + .ss-link-range__tag:hover, .ss-link-range__tag:focus-visible { filter: brightness(1.15); diff --git a/A00_Common/spreadsheet/spreadsheet_links.ts b/A00_Common/spreadsheet/spreadsheet_links.ts index 2c9e68581..6fec08291 100644 --- a/A00_Common/spreadsheet/spreadsheet_links.ts +++ b/A00_Common/spreadsheet/spreadsheet_links.ts @@ -15,13 +15,15 @@ import "./spreadsheet_links.css"; import { createMapContextMenu } from "@ui/ui_template_context_menu"; import { el, showConfirmDialog, showToast } from "@ui/ui_template_elements"; -import { inRange, parseA1, parseRange, quoteSheet, toA1 } from "./spreadsheet_address"; +import { inRange, parseA1, quoteSheet, toA1 } from "./spreadsheet_address"; import { applyCommand } from "./spreadsheet_commands"; import { askShare } from "./spreadsheet_link_ask"; import { linkHues } from "./spreadsheet_link_color"; +import { createRangeStates, markKey, markRange } from "./spreadsheet_link_range"; +import { rangeFrame, rangePills } from "./spreadsheet_link_range"; +import type { RangeMark, RangeState } from "./spreadsheet_link_range"; import { clearLinksCommand, linkCommand, linksAt } from "./spreadsheet_link_set"; import type { slotOf } from "./spreadsheet_editor"; -import { resolveName } from "./spreadsheet_names"; import { shiftSpan } from "./spreadsheet_refshift"; import { selectCell, selectSpan } from "./spreadsheet_selection"; import { MAX_COLS, MAX_ROWS } from "./spreadsheet_types"; @@ -60,7 +62,8 @@ export interface LinkOptions { 확정?: boolean; /** 다른 곳으로 보낸 칸 범위 표시(예: 도면 표 · 104-3) — 시트 id · 범위(이름이 없거나 지워졌으면 이 범위) · * `범위이름`(104-1) 있으면 이름정의로 지금 범위를 다시 품(행열 넣기 · 지우기를 따라감) · 이름표 · 마우스 올림 글 · - * `취소`(있으면 이름표 메뉴에 [보내기 취소]) — 연결만 끊고(true) 화면에서 뗌 · 실패(false)면 그대로 */ + * `취소`(있으면 이름표 메뉴 · 아래 막대 알약 × 에 [보내기 취소]) — 연결만 끊고(true) 화면에서 뗌 · 실패(false)면 + * 그대로 · `상태`(114-3 · 있으면 아래 막대 알약 꼬리표) — 지금 책으로 판정(값이 바뀌면 150ms 모아 다시) */ 표시범위?: { 시트: string; 범위: string; @@ -68,6 +71,7 @@ export interface LinkOptions { 이름: string; 설명?: string; 취소?: () => Promise; + 상태?: (book: Workbook) => RangeState | Promise; }[]; } @@ -176,18 +180,21 @@ export function mountLinks( /** 누른 보낸범위 테두리(104-3) — 다음에 누르면 강조를 옮김 · 다시 그릴 때 비움(DOM 이 새로 생김) */ let pickedFrame: HTMLElement | null = null; - /** 보낸 범위 마크의 지금 범위(104-1 · 104-3) — `범위이름` 있으면 그 이름이 지금 가리키는 범위로 - * (행열 넣기 · 지우기를 따라감) · 이름이 지워졌으면 null(서버가 다음 저장 때 연결을 끊음) · - * 이름이 없으면(옛 연결) 적힌 `범위` 그대로 */ - function markRange( - mark: NonNullable[number], - ): { 시트: string; g: CellRange } | null { - if (mark.범위이름) { - const found = resolveName(ctx.book, mark.시트, mark.범위이름); - return found && { 시트: found.시트, g: found.범위 }; - } - const g = parseRange(mark.범위); - return g && { 시트: mark.시트, g }; + /** 보낸 범위 상태(114-3) — 값이 바뀌면 모아 판정 → 아래 막대 다시 */ + const states = createRangeStates( + () => opts?.표시범위 ?? [], + () => ctx.book, + () => renderBar(), + ); + + /** [보내기 취소](이름표 메뉴 · 아래 막대 ×) — 연결만 끊고 화면에서 뗌 · 실패면 그대로 */ + function cancelMark(mark: RangeMark): void { + void mark.취소?.().then((ok) => { + if (!ok) return showToast(TEXT.cancelFail, "error"); + if (opts) opts.표시범위 = opts.표시범위?.filter((m) => m !== mark); + placeDots(); + renderBar(); + }); } /** 키 → 연결(시트 · 줄) */ @@ -388,6 +395,12 @@ export function mountLinks( 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); }); + pills.push( + ...rangePills(ctx, opts.표시범위 ?? [], states.get, back, cancelMark, { + pick: TEXT.rangeTip, + cancel: `${TEXT.cancelSend} — ${TEXT.cancelTip}`, + }), + ); if (opts.모드 === "자재" && opts.자재찾기 && !ctx.readOnly) { const more = el("button", { className: "ss-link ss-link--add", @@ -439,7 +452,7 @@ export function mountLinks( cellSlots.push({ list, layer, box }); } const rangeSlots: Array<{ - mark: NonNullable[number]; + mark: RangeMark; g: CellRange; layer: HTMLElement; box: ReturnType["box"]; @@ -450,10 +463,10 @@ export function mountLinks( }> = []; const marks = opts?.표시범위 ?? []; // 색 = 범위 이름(104-1) 없으면 시트!범위 — 표마다 다르게(104-3 · 알약 색과는 다른 자리) - const rangeHues = linkHues(marks.map((m) => m.범위이름 ?? `${m.시트}!${m.범위}`)); + const rangeHues = linkHues(marks.map(markKey)); const dupCount = new Map(); // 왼위 자리 키 → 지금까지 그 자리에 둔 수 for (const mark of marks) { - const found = markRange(mark); + const found = markRange(ctx.book, mark); if (!found || found.시트 !== ctx.sheet().id) continue; const { g } = found; const { layer, box } = slot(ctx, g.r0, g.c0); @@ -462,7 +475,7 @@ export function mountLinks( const key = `${g.r0},${g.c0}`; const dup = dupCount.get(key) ?? 0; dupCount.set(key, dup + 1); - const hue = rangeHues.get(mark.범위이름 ?? `${mark.시트}!${mark.범위}`) ?? 0; + const hue = rangeHues.get(markKey(mark)) ?? 0; rangeSlots.push({ mark, g, layer, box, end, dup, hue }); } @@ -489,22 +502,7 @@ export function mountLinks( // 보낸 범위(도면 표 등 · 104-1 · 104-3) — 채움 없는 점선 테두리 + 왼위 이름표 · 표마다 다른 색 · // 같은 자리가 겹치면(dup) 테두리는 안쪽으로 · 이름표는 옆으로 비킴 · 이름표만 누를 수 있음 for (const { mark, g, layer, box, end, dup, hue } of rangeSlots) { - 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.설명 ?? TEXT.rangeTip, 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)); + const { frame, tag } = rangeFrame(mark, box, end, dup, hue, TEXT.rangeTip); tag.addEventListener("click", (e) => { e.stopPropagation(); pickedFrame?.classList.remove("is-picked"); @@ -517,15 +515,7 @@ export function mountLinks( e.preventDefault(); const o = ctx.root.getBoundingClientRect(); menu.open(e.clientX - o.left, e.clientY - o.top, [ - [ - TEXT.cancelSend, - () => - void mark.취소!().then((ok) => { - if (!ok) return showToast(TEXT.cancelFail, "error"); - if (opts) opts.표시범위 = opts.표시범위?.filter((m) => m !== mark); - placeDots(); - }), - ] as [string, () => void], + [TEXT.cancelSend, () => cancelMark(mark)] as [string, () => void], ]); }); } @@ -544,7 +534,7 @@ export function mountLinks( : []; if (hit && hit.r >= 0 && hit.c >= 0) for (const m of opts?.표시범위 ?? []) { - const found = markRange(m); + const found = markRange(ctx.book, m); if (found && found.시트 === ctx.sheet().id && inRange(found.g, hit.r, hit.c)) names.push(m.설명 ?? m.이름); } @@ -677,10 +667,12 @@ export function mountLinks( renderBar(); if (dataChanged || !dotsReady) { placeDots(); + states.schedule(); // 보낸 범위 상태(114-3) — 값이 바뀌면 다시 dotsReady = true; } }, destroy() { + states.stop(); ctx.grid.root.removeEventListener("aislo-grid-zoom", onZoom); ctx.grid.root.removeEventListener("dragover", onDragOver); ctx.grid.root.removeEventListener("dragleave", onDragLeave); diff --git a/M02_MasterTemplete/M02_MasterTemplete_AreaToDrawing.ts b/M02_MasterTemplete/M02_MasterTemplete_AreaToDrawing.ts index f37ddd082..ccdd2b03a 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_AreaToDrawing.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_AreaToDrawing.ts @@ -13,6 +13,8 @@ * 면적 [저장] 문서에 `보내기취소[]` 를 실어 서버가 그 연결표 줄 · 이름을 뺌(도면 표는 남음 · 112-2) * ⑤ 여러 장(82-3) — 놓을 장 = 지금 탭 · 새 T번호 = 모든 장에서 안 쓴 것 · 연결표 줄 `장` = 표가 놓인 장id * (1장 = 빈 값) · 안 실린 장 표 갱신 = 도면 손잡이(받아 둔 장) · 면적 [저장] 때는 서버가 모든 장 + * ⑦ 이름 없는 옛 연결 — 면적을 열 때 책에 `도면_{연결}` 이름(114-1 · 면적 [저장] 때 서버가 줄에 적음) · + * 아래 막대 알약 상태 = 도면에 표 있나 · 지금 값 = 마지막 도면 값인가(114-3) * ========================================================================== */ import { API_BASE_URL } from "@config/config_frontend"; @@ -31,6 +33,7 @@ import { type StructureDoc, } from "./M02_MasterTemplete_Api_Fetch"; import type { LinkOptions } from "../A00_Common/spreadsheet/spreadsheet"; +import type { RangeState } from "../A00_Common/spreadsheet/spreadsheet_link_range"; import type { Workbook } from "../A00_Common/spreadsheet/spreadsheet_types"; import type { Selection } from "./M02_MasterTemplete_UI_Main"; @@ -87,6 +90,14 @@ const TEXT = { tip: "도면 표 {id} 로 보낸 범위 {range} · 글자 {size}", updated: "표 {n}개를 지금 면적 값으로 바꿈 — [저장]하면 남음", rangeGone: "보낸 범위가 지워져 연결을 끊음 — {ids}(도면 표는 남음)", + same: "값 맞음", + sameTip: "도면 표 칸 글 = 지금 면적 값", + diff: "값 다름", + diffTip: "면적 값이 도면 표와 다름 — 면적 [저장] 때 도면 표를 맞춤", + noTable: "도면에 표 없음", + noTableTip: "도면(저장본)에 이 연결 표가 없음 — × 로 보내기 취소", + gone: "범위 없음", + goneTip: "보낸 범위가 지워짐 — 면적 [저장] 때 연결을 끊음", } as const; const SIZE = 2.5; // 표 글자 크기 기본(정함 41) @@ -156,6 +167,25 @@ async function nameOld(book: Workbook, row: TableLink): Promise { + if (!placed) return { 글: TEXT.noTable, 종류: "danger", 설명: TEXT.noTableTip }; + const { areaTable } = await import("../A00_Common/spreadsheet/spreadsheet_area_table"); + try { + const got = areaTable({ book, 시트: row.시트, 범위: row.범위, 이름, 글자크기: row.글자크기 }); + return sameValues(row.값, got.값) + ? { 글: TEXT.same, 종류: "success", 설명: TEXT.sameTip } + : { 글: TEXT.diff, 종류: "warning", 설명: TEXT.diffTip }; + } catch { + return { 글: TEXT.gone, 종류: "danger", 설명: TEXT.goneTip }; + } +} + /** 면적 시트에 겹쳐 보일 「도면으로 보낸 범위」 — 같은 열 도면 `연결표` 에서(받기 실패 = 없음 · 조용히) · * `book`(면적 책 · 화면에 싣기 전) 있으면 이름 없는 옛 연결에 이름을 둠(114-1 · 행열 넣기를 따라감) */ export async function sentRanges( @@ -165,7 +195,12 @@ export async function sentRanges( ): Promise> { try { const got = await readTemplate(at.layer, "structure", at.name, at.projectId); - const rows = (got.문서 as StructureDoc | undefined)?.연결표 ?? []; + const doc = got.문서 as StructureDoc | undefined; + const rows = doc?.연결표 ?? []; + // 도면에 그 표가 있나(114-3 · 모든 장) + const placed = new Set( + doc ? sheetsOf(doc).flatMap(([, es]) => es.filter(isTable).map(linkOf)) : [], + ); const names = await Promise.all( rows.map((row) => row.이름 || !book || !Array.isArray(book.시트) @@ -180,6 +215,7 @@ export async function sentRanges( 이름: fill(TEXT.tag, "id", row.연결), 설명: fill(fill(fill(TEXT.tip, "id", row.연결), "range", row.범위), "size", row.글자크기), 취소: async () => (onCancel(row.연결), true), // 화면에서만 — 서버는 면적 [저장] 때 + 상태: (now: Workbook) => stateOf(now, row, names[i], placed.has(row.연결)), })); } catch { return []; diff --git a/resources/tester/test_m02_area_old_links.py b/resources/tester/test_m02_area_old_links.py index 537119355..1473d69d1 100644 --- a/resources/tester/test_m02_area_old_links.py +++ b/resources/tester/test_m02_area_old_links.py @@ -3,6 +3,8 @@ - 114-1 면적 상세를 열 때 화면(`sentRanges` + 책)이 `도면_{연결}` 이름정의를 둠 → 행 넣기 → 면적 [저장] → 서버 `refresh_drawing` 이 그 이름을 연결표 줄에 적고 도면 표 칸 글 = 옮겨진 칸. - 114-2 셀 밀기 · 당기기(칸 삽입 · 삭제 창)에 이름 따라감 · 카드 도면 저장도 표 없는 연결표 줄 뺌(`fitLinks`). + - 114-3 아래 막대 도면 알약(`spreadsheet_link_range.rangePills`) · 상태(표 있음/없음 · 값 맞음/다름 · 범위 없음) + · 누르면 범위 고름 · × = 보내기 취소 · 상태는 150ms 모아 한 번. TS 는 Node 로(`AreaToDrawing` 은 그 자리 변환 · 스프레드시트 모듈은 `helper_spreadsheet_d_bundle.cjs`). """ @@ -28,6 +30,7 @@ const { load, SS } = require(path.join(ROOT, "resources/tester/spreadsheet/helpe const ts = require(path.join(ROOT, "config/node_modules/typescript")); const input = JSON.parse(fs.readFileSync(0, "utf8")); const ss = (name) => load(path.join(SS, name + ".ts")); +let drawingNow = input.drawing; const tsLoad = (file, stubs) => { const src = fs.readFileSync(path.join(ROOT, file), "utf8"); const js = ts.transpileModule(src, { @@ -46,7 +49,7 @@ const toDrawing = tsLoad("M02_MasterTemplete/M02_MasterTemplete_AreaToDrawing.ts "@ui/ui_template_modal": {}, "./M02_MasterTemplete_Api_Fetch": { getActiveOwner: () => null, - readTemplate: async () => ({ 문서: input.drawing }), + readTemplate: async () => ({ 문서: drawingNow }), }, "../A00_Common/spreadsheet/spreadsheet_address": ss("spreadsheet_address"), "../A00_Common/spreadsheet/spreadsheet_area_table": ss("spreadsheet_area_table"), @@ -96,7 +99,61 @@ const { applyCommand } = ss("spreadsheet_commands"); kept: toDrawing.fitLinks({ 도면: { entities: [tbl("T1")] }, 연결표: [row("T1"), row("T2")] }).연결표, plain: "연결표" in toDrawing.fitLinks({ 도면: { entities: [tbl("T9")] } }), }; - console.log(JSON.stringify({ opened, book, shifted, fit })); + // 114-3 — 아래 막대 도면 알약 · 상태(표 있음/없음 · 값 맞음/다름 · 범위 없음) + const states = {}; + const pills = {}; + if (input.drawing3) { + drawingNow = input.drawing3; + const book3 = input.book3; + const marks3 = await toDrawing.sentRanges(area, () => {}, book3); + const st = await Promise.all(marks3.map((m) => m.상태(book3))); + states.first = st.map((s) => [s.글, s.종류]); + book3.시트[0].칸.B1 = { 값: 99, 서식: 1 }; // 저장 안 한 고침 + states.edited = [(await marks3[0].상태(book3)).글]; + const mk = (tag, o = {}) => ({ tag, className: o.className ?? "", text: o.text ?? "", + attrs: o.attrs ?? {}, children: [...(o.children ?? [])], title: "", on: {}, + style: { props: {}, setProperty(k, v) { this.props[k] = v; } }, + addEventListener(k, f) { this.on[k] = f; }, append(...x) { this.children.push(...x); } }); + const lr = tsLoad("A00_Common/spreadsheet/spreadsheet_link_range.ts", { + "@ui/ui_template_elements": { el: mk, createTag: (l, v) => mk("span", { className: `ui-tag ui-tag--${v}`, text: l }) }, + "./spreadsheet_address": ss("spreadsheet_address"), + "./spreadsheet_link_color": ss("spreadsheet_link_color"), + "./spreadsheet_names": ss("spreadsheet_names"), + "./spreadsheet_selection": ss("spreadsheet_selection"), + }); + const got = new Map(marks3.map((m, i) => [m, st[i]])); + const picked = []; + const cancelled = []; + let backs = 0; + const ctx = { book: book3, readOnly: false, selection: { 시트: "s1" }, sheet: () => book3.시트[0], + showSheet: () => {}, select: (s) => picked.push(s) }; + const made = lr.rangePills(ctx, marks3, (m) => got.get(m), () => (backs += 1), + (m) => cancelled.push(m.이름), { pick: "누르면 고름", cancel: "보내기 취소" }); + made[0].on.click(); + made[1].children.find((c) => c.className === "ss-link__x").on.click({ stopPropagation() {} }); + const ro = lr.rangePills({ ...ctx, readOnly: true }, marks3, () => undefined, () => {}, () => {}, { pick: "", cancel: "" }); + pills.made = made.map((p) => ({ + text: p.children[0].text, + tag: p.children[1] && p.children[1].className.startsWith("ui-tag") ? [p.children[1].className, p.children[1].text] : null, + x: p.children.some((c) => c.className === "ss-link__x"), + cls: p.className, + hue: typeof p.style.props["--ss-link-h"], + })); + pills.picked = picked.map((s) => s.범위[0]); + pills.backs = backs; + pills.cancelled = cancelled; + pills.roX = ro.map((p) => p.children.some((c) => c.className === "ss-link__x")); + // 상태 모음 — 잇단 schedule 은 150ms 뒤 한 번만 + let calls = 0; + let done = 0; + const fake = [{ 상태: () => (calls += 1, { 글: "x", 종류: "success" }) }]; + const rs = lr.createRangeStates(() => fake, () => book3, () => (done += 1)); + rs.schedule(); rs.schedule(); rs.schedule(); + const early = calls; + await new Promise((r) => setTimeout(r, 250)); + pills.gather = { early, calls, done, got: rs.get(fake[0]) }; + } + console.log(JSON.stringify({ opened, book, shifted, fit, states, pills })); })().catch((e) => { console.error(e); process.exit(1); @@ -188,3 +245,43 @@ def test_카드_도면_저장도_표_없는_연결표_줄_뺌_114_2(world): # n fit = _node({"book": area["문서"], "drawing": {"연결표": []}})["fit"] assert fit["kept"] == [{"연결": "T1", "시트": "s1", "범위": "A1", "값": {}, "장": ""}] assert fit["plain"] is False # 기록 없던 도면은 칸을 안 만듦 + + +def test_아래_막대_도면_알약_상태_누르면_고름_x_는_보내기_취소_114_3(world): # noqa: F811 + with store.at(world["system"]): + area = store.read("area", "rv") + book = area["문서"] + now = sheet_node.area_tables([{"book": book, "시트": "s1", "범위": "A1:B2"}])[0]["값"] + drawing3 = { + "도면": {"entities": [T1_TABLE, {**T1_TABLE, "shapeData": {"style": {"연결": "T3"}}}]}, + "연결표": [ + {**OLD_ROW, "값": now}, # 이름 없는 옛 연결 · 표 있음 · 값 같음 + {"연결": "T2", "시트": "s1", "범위": "B1", "값": {}}, # 도면에 표 없음 + { + "연결": "T3", + "시트": "s1", + "범위": "A1", + "이름": "도면_없음", + "값": {}, + }, # 이름 지워짐 + ], + } + out = _node({"book": book, "drawing": {"연결표": []}, "drawing3": drawing3, "book3": book}) + assert out["states"]["first"] == [ + ["값 맞음", "success"], + ["도면에 표 없음", "danger"], + ["범위 없음", "danger"], + ] + assert out["states"]["edited"] == ["값 다름"] # 저장 전 칸 고침 = 지금 책으로 판정 + p = out["pills"] + assert [x["text"] for x in p["made"]] == ["T1 도면 A1:B2", "T2 도면 B1", "T3 도면"] + assert p["made"][0]["tag"] == ["ui-tag ui-tag--success", "값 맞음"] # 공용 꼬리표 + assert p["made"][1]["tag"] == ["ui-tag ui-tag--danger", "도면에 표 없음"] + assert all(x["x"] and x["cls"] == "ss-link ss-link--range" for x in p["made"]) + assert all(x["hue"] == "string" for x in p["made"]) # 색 = 시트 위 테두리 색 + assert ( + p["picked"] == [{"r0": 0, "c0": 0, "r1": 1, "c1": 1}] and p["backs"] == 1 + ) # 누르면 범위 고름 + assert p["cancelled"] == ["T2 도면"] # × = 보내기 취소(화면에서만 · 저장 때 기록) + assert p["roX"] == [False, False, False] # 읽기 = × 없음 + assert p["gather"] == {"early": 0, "calls": 1, "done": 1, "got": {"글": "x", "종류": "success"}}