From 5a9491bd6964c1f314581ab27abe8c3fc062af99 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 4 Oct 2026 22:28:55 +0900 Subject: [PATCH 1/5] =?UTF-8?q?chore(plan):=2041=EC=9E=A5=20=EB=A9=B4?= =?UTF-8?q?=EC=A0=81=EC=82=B0=EC=B6=9C=20=ED=91=9C=20=E2=86=92=20=EB=8F=84?= =?UTF-8?q?=EB=A9=B4=20=ED=91=9C=20=E2=80=94=20=EC=82=AC=EC=9A=A9=EC=9E=90?= =?UTF-8?q?=20=EC=9A=94=EA=B5=AC=20=EC=9B=90=EB=AC=B8=20=EB=8D=94=ED=95=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FJqWQpoB1RE7hiaQ2wH8qV --- docs/raw/M02_구조물조합개편/사용자요구_1004_2.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/raw/M02_구조물조합개편/사용자요구_1004_2.md b/docs/raw/M02_구조물조합개편/사용자요구_1004_2.md index 30d4b5a29..3651b8c87 100644 --- a/docs/raw/M02_구조물조합개편/사용자요구_1004_2.md +++ b/docs/raw/M02_구조물조합개편/사용자요구_1004_2.md @@ -19,3 +19,7 @@ 3. 면적산출 상세 - 알약지정은 현재 방법 + 그래그 & 드랍 방식을 추가 - 지정된 알약정보를 색상 자동지정 및 지정된 셀은 색상으로 구분을 할수 있게 반영, 반짝임은 유지 + +## 추가 기능 (10-4 밤 · 원문 그대로) + +추가기능 부여. 면적산출에서 특정 영역을 선정하고 도면영역으로 전달을 할 경우 도면에 테이블을 자동으로 넣고 셀의 내용을 싱크하는 기능을 부여. 만약 동기화가 어려운 경우 업데이트 버튼을 추가해도 무관, 복수개의 테이블이 넘어갈수도 잇으며 위치는 도면에서 사용자가 드래그하여 지정 From 3ad007683ced9f382af2180d4761b9b3d24cfbdc Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 4 Oct 2026 22:29:26 +0900 Subject: [PATCH 2/5] =?UTF-8?q?feat(spreadsheet):=20=EC=95=8C=EC=95=BD=20?= =?UTF-8?q?=EB=81=8C=EC=96=B4=20=EC=B9=B8=EC=97=90=20=EB=86=93=EA=B8=B0=20?= =?UTF-8?q?=C2=B7=20=EC=9D=B4=EC=9D=80=20=EC=95=8C=EC=95=BD=20=EC=83=89=20?= =?UTF-8?q?=EC=9E=90=EB=8F=99=20=E2=80=94=20=ED=82=A4=EB=A1=9C=20=EC=A0=95?= =?UTF-8?q?=ED=95=B4=EC=A7=80=EB=8A=94=20=EC=83=89(24=20=EC=83=89=20=C2=B7?= =?UTF-8?q?=20=EB=91=90=20=ED=85=8C=EB=A7=88=20=EB=B0=9D=EA=B8=B0)=20?= =?UTF-8?q?=C2=B7=20=EC=9D=B4=EC=9D=80=20=EC=B9=B8=20=EB=B0=B0=EA=B2=BD=20?= =?UTF-8?q?=EA=B0=99=EC=9D=80=20=EC=83=89=20=EC=98=85=EA=B2=8C=20=C2=B7=20?= =?UTF-8?q?=EB=8B=A4=EB=A5=B8=20=EC=95=8C=EC=95=BD=20=EC=B9=B8=EC=97=90=20?= =?UTF-8?q?=EB=86=93=EC=9C=BC=EB=A9=B4=20=EB=B0=94=EA=BF=80=EC=A7=80=20?= =?UTF-8?q?=EB=AC=BC=EC=9D=8C=20=C2=B7=20=EB=B0=98=EC=A7=9D=EC=9E=84=20?= =?UTF-8?q?=EA=B7=B8=EB=8C=80=EB=A1=9C=20(PLAN=2040-3=20=E2=91=A1=20?= =?UTF-8?q?=E2=91=A2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01W4uKaV4ZBhsJN4GHG7yRj4 --- .../spreadsheet/spreadsheet_link_color.ts | 38 +++++++ A00_Common/spreadsheet/spreadsheet_links.css | 55 ++++++++-- A00_Common/spreadsheet/spreadsheet_links.ts | 103 +++++++++++++++--- .../spreadsheet/spreadsheet_link_set_entry.ts | 24 ++++ resources/tester/test_spreadsheet_set_link.py | 18 ++- 5 files changed, 217 insertions(+), 21 deletions(-) create mode 100644 A00_Common/spreadsheet/spreadsheet_link_color.ts diff --git a/A00_Common/spreadsheet/spreadsheet_link_color.ts b/A00_Common/spreadsheet/spreadsheet_link_color.ts new file mode 100644 index 000000000..7e77b8a02 --- /dev/null +++ b/A00_Common/spreadsheet/spreadsheet_link_color.ts @@ -0,0 +1,38 @@ +/* ============================================================================= + * spreadsheet_link_color.ts (주인 D) + * 이은 알약 색 — 알약 키로 정해지는 색상(hue). 알약 · 이은 칸 배경 · 칸 세모가 같은 색(PLAN 40-3). + * 칸 24 개(30° 바퀴 12 + 15° 어긋난 바퀴 12) — 키 해시로 첫 자리 · 겹치면 다음 빈 자리(같은 문서 안에서 서로 다르게). + * 밝기 · 채도는 테마마다 CSS(`spreadsheet_links.css` `--ss-link-l`) · 여기는 색상만 · DOM 모름(시험이 Node 로 부름). + * ========================================================================== */ + +const SLOTS = 24; + +/** FNV-1a 32비트 — 키 글자로 늘 같은 수 */ +function hash(text: string): number { + let h = 0x811c9dc5; + for (let i = 0; i < text.length; i += 1) { + h ^= text.charCodeAt(i); + h = Math.imul(h, 0x01000193) >>> 0; + } + return h; +} + +/** 자리 → 색상(°) — 0~11 = 30° 간격 · 12~23 = 그 사이(15° 어긋남) */ +const hueOf = (slot: number): number => (slot % 12) * 30 + (slot >= 12 ? 15 : 0); + +/** 이은 알약 키들 → 키마다 색상(°) — 키 차례와 무관(정렬해 자리 배정) · 24 개 넘으면 다시 돎 */ +export function linkHues(keys: Iterable): Map { + const out = new Map(); + const used = new Set(); + for (const key of [...new Set(keys)].sort()) { + if (used.size >= SLOTS) used.clear(); + const start = hash(key) % 12; + // 첫 바퀴(30°)를 다 쓴 뒤에야 둘째 바퀴 — 적을 때 서로 30° 넘게 벌어짐 + let slot = start; + for (let i = 1; i < SLOTS && used.has(slot); i += 1) + slot = (i < 12 ? 0 : 12) + ((start + i) % 12); + used.add(slot); + out.set(key, hueOf(slot)); + } + return out; +} diff --git a/A00_Common/spreadsheet/spreadsheet_links.css b/A00_Common/spreadsheet/spreadsheet_links.css index b61fdf11e..edecb144f 100644 --- a/A00_Common/spreadsheet/spreadsheet_links.css +++ b/A00_Common/spreadsheet/spreadsheet_links.css @@ -33,14 +33,55 @@ color: var(--color-accent); } -/* 연결됨 — 흐림(연결 현황) */ -.ss-link.is-linked { - opacity: 0.5; - border-style: dashed; +/* 이은 알약 색(40-3) — 색상 = 알약 키(`--ss-link-h` · spreadsheet_link_color) · 밝기 · 옅기 = 테마 */ +.ss { + --ss-link-s: 70%; + --ss-link-l: 31%; + --ss-link-fill: 0.16; } -.ss-link.is-linked:hover { - opacity: 0.8; +:root[data-theme="dark"] .ss { + --ss-link-l: 66%; + --ss-link-fill: 0.26; +} + +@media (prefers-color-scheme: dark) { + :root:not([data-theme="light"]) .ss { + --ss-link-l: 66%; + --ss-link-fill: 0.26; + } +} + +/* 연결됨 — 알약 색 테두리 · 왼쪽 띠 · 옅은 배경(글은 본문 색 그대로 · 읽힘) */ +.ss-link.is-linked { + border-color: hsl(var(--ss-link-h, 0) var(--ss-link-s) var(--ss-link-l)); + background: hsl(var(--ss-link-h, 0) var(--ss-link-s) 50% / var(--ss-link-fill)); + box-shadow: inset 4px 0 0 hsl(var(--ss-link-h, 0) var(--ss-link-s) var(--ss-link-l)); +} + +.ss-link.is-linked code { + color: inherit; +} + +.ss-link[draggable="true"] { + cursor: grab; +} + +/* 이은 칸 — 알약 색 옅게(칸 글 위 · 누름은 칸으로 지나감) */ +.ss-link-cell { + position: absolute; + z-index: 2; + background: hsl(var(--ss-link-h, 0) var(--ss-link-s) 50% / var(--ss-link-fill)); + pointer-events: none; +} + +/* 알약을 끌 때 놓일 칸 */ +.ss-link-drop { + position: absolute; + z-index: 4; + box-sizing: border-box; + border: 2px dashed var(--color-accent); + pointer-events: none; } .ss-link--add { @@ -55,7 +96,7 @@ width: 0; height: 0; border-top: 6px solid transparent; - border-left: 6px solid var(--color-accent); + border-left: 6px solid hsl(var(--ss-link-h, 0) var(--ss-link-s) var(--ss-link-l)); pointer-events: none; } diff --git a/A00_Common/spreadsheet/spreadsheet_links.ts b/A00_Common/spreadsheet/spreadsheet_links.ts index 2890bef60..45e9a1572 100644 --- a/A00_Common/spreadsheet/spreadsheet_links.ts +++ b/A00_Common/spreadsheet/spreadsheet_links.ts @@ -1,8 +1,9 @@ /* ============================================================================= * spreadsheet_links.ts (주인 D) * 아래 알약 막대(표 · 탭 · 합계 줄 아래 맨 끝) — 면적산출(입력값) · 재료산출(자재) 알약을 칸에 연결(PLAN 13장). - * 칸을 고른 채 밝은 알약을 누르면 그 칸에 연결(알약 흐려짐 + 칸 주소 · 칸 왼아래 작은 세모) · - * 흐린 알약을 누르면 연결 칸으로 · 우클릭 [연결 풀기] · 자재 모드는 [자재 더하기] → 고르기 창 → 새 알약. + * 칸을 고른 채 밝은 알약을 누르면 그 칸에 연결 · 알약을 끌어 칸에 놓아도 연결(놓은 칸에 다른 알약이 있으면 + * 바꿀지 물음 · 40-3) · 이은 알약 = 키로 정해지는 색(`spreadsheet_link_color`) · 이은 칸 배경 같은 색 옅게 + + * 칸 왼아래 작은 세모 · 이은 알약을 누르면 연결 칸으로 · 우클릭 [연결 풀기] · 자재 모드는 [자재 더하기] → 고르기 창 → 새 알약. * 자재 모드 알약 목록 = 호스트(M02)가 `항목` 으로 줌(자재 정보 포함 · 저장은 마스터 자재 조합) — [자재 더하기] · * 우클릭 [목록에서 빼기] 때 `onItemsChange(목록)` 으로 알림(부른 쪽이 마스터에 적음 · 여기는 안 적음). * 저장 = 시트 `연결`(계약) · 바꿈은 `시트통째` 명령(되돌리기 짝) · 행열 넣기 · 지우기는 `applyLinked` 가 @@ -11,9 +12,10 @@ 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 { parseA1, quoteSheet, toA1 } from "./spreadsheet_address"; import { applyCommand } from "./spreadsheet_commands"; +import { linkHues } from "./spreadsheet_link_color"; import type { slotOf } from "./spreadsheet_editor"; import { shiftSpan } from "./spreadsheet_refshift"; import { selectCell } from "./spreadsheet_selection"; @@ -55,8 +57,10 @@ export interface Links extends PartHandle { } const TEXT = { - bar: "알약 — 칸을 고른 채 누르면 연결 · 흐린 알약은 누르면 연결 칸으로 · 우클릭 풀기", - free: "칸을 고른 채 누르면 이 칸에 연결", + bar: "알약 — 칸을 고른 채 누르거나 끌어 칸에 놓으면 연결 · 색 알약은 누르면 연결 칸으로 · 우클릭 풀기", + free: "칸을 고른 채 누르거나 끌어 칸에 놓으면 이 칸에 연결", + swap: "{cell} 은 이미 「{name}」 알약 칸 — 이 알약으로 바꿈?", + swapOk: "바꿈", linked: "누르면 연결 칸으로 · 우클릭 연결 풀기", unlink: "연결 풀기", remove: "목록에서 빼기", @@ -136,6 +140,9 @@ export function mountLinks( /** 알약 목록 — 호스트가 준 항목 + 더한 자재(자재 모드 · 바뀌면 호스트에 알림) */ const items: LinkItem[] = opts ? [...opts.항목] : []; let dots: HTMLElement[] = []; + /** 끄는 중인 알약 · 놓을 칸 표시 */ + let dragging: LinkItem | null = null; + const dropMark = el("div", { className: "ss-link-drop" }); let pick: HTMLElement | null = null; /** 키 → 연결(시트 · 줄) */ @@ -153,10 +160,11 @@ export function mountLinks( ctx.dispatch({ 종류: "시트통째", 시트: next }); } - function link(item: LinkItem): void { + /** 알약을 칸에 이음 — `at` 없으면 지금 고른 칸 */ + function link(item: LinkItem, at?: string): void { if (ctx.readOnly) return; const sheet = ctx.sheet(); - const 칸 = toA1(ctx.selection.활성.r, ctx.selection.활성.c); + const 칸 = at ?? toA1(ctx.selection.활성.r, ctx.selection.활성.c); const rest = (sheet.연결 ?? []).filter((l) => l.칸 !== 칸 && l.키 !== item.키); const one: CellLink = { 키: item.키, 이름: item.이름, 칸 }; if (item.단위) one.단위 = item.단위; @@ -184,7 +192,11 @@ export function mountLinks( const commit = (): void => opts?.onItemsChange?.(items.map((i) => ({ ...i }))); - function pill(item: LinkItem, hit: { sheet: Sheet; link: CellLink } | undefined) { + function pill( + item: LinkItem, + hit: { sheet: Sheet; link: CellLink } | undefined, + hue: number | undefined, + ) { const where = hit ? hit.sheet.id === ctx.selection.시트 ? hit.link.칸 @@ -199,7 +211,19 @@ export function mountLinks( ...(where ? [el("code", { text: where })] : []), ], }); - b.addEventListener("mousedown", (e) => e.preventDefault()); // 초점은 격자에 둠 + if (hue !== undefined) b.style.setProperty("--ss-link-h", String(hue)); + b.draggable = !ctx.readOnly; + b.addEventListener("dragstart", (e) => { + dragging = item; + e.dataTransfer?.setData("text/plain", item.키); + if (e.dataTransfer) e.dataTransfer.effectAllowed = "link"; + }); + b.addEventListener("dragend", () => { + dragging = null; + dropMark.remove(); + }); + // 초점은 격자에 둠 — 끌 수 있는 알약은 막으면 끌기가 안 돼 그대로(누른 뒤 `back` 이 격자로 돌림) + if (!b.draggable) b.addEventListener("mousedown", (e) => e.preventDefault()); b.addEventListener("click", () => (hit ? goTo(hit.sheet, hit.link.칸) : link(item))); const unlink = (): void => { if (hit) @@ -274,7 +298,8 @@ export function mountLinks( ? ofMaterial(link.자재) : { 키: link.키, 이름: link.이름 ?? link.키, 단위: link.단위 }, ); - const pills = shown.map((item) => pill(item, map.get(item.키))); + const hues = linkHues(map.keys()); + const pills = shown.map((item) => pill(item, map.get(item.키), hues.get(item.키))); if (opts.모드 === "자재" && opts.자재찾기 && !ctx.readOnly) { const more = el("button", { className: "ss-link ss-link--add", @@ -287,23 +312,67 @@ export function mountLinks( bar.replaceChildren(...pills); } - /** 칸 왼아래 작은 세모 — 칸이 든 판 층에 얹어 스크롤을 따라감 */ + /** 이은 칸 — 알약 색 옅은 배경 + 왼아래 작은 세모 · 칸이 든 판 층에 얹어 스크롤을 따라감 */ function placeDots(): void { for (const d of dots) d.remove(); dots = []; + const hues = linkHues(found().keys()); for (const l of ctx.sheet().연결 ?? []) { const p = parseA1(l.칸); if (!p) continue; const { layer, box } = slot(ctx, p.r, p.c); if (!box.w || !box.h) continue; // 숨긴 행열 + const tint = el("div", { className: "ss-link-cell", attrs: { "data-key": l.키 } }); + tint.style.cssText = `left:${box.x}px;top:${box.y}px;width:${box.w}px;height:${box.h}px`; const dot = el("div", { className: "ss-link-dot" }); dot.style.left = `${box.x}px`; dot.style.top = `${box.y + box.h - 7}px`; - layer.append(dot); - dots.push(dot); + for (const d of [tint, dot]) d.style.setProperty("--ss-link-h", String(hues.get(l.키) ?? 0)); + layer.append(tint, dot); + dots.push(tint, dot); } } + // ── 알약 끌어 칸에 놓기(40-3) ────────────────────────────────────────── + /** 끄는 알약 아래 칸(A1) — 머리 · 격자 밖이면 null */ + function cellAt(e: DragEvent): { r: number; c: number } | null { + const hit = ctx.grid.hitTest(e.clientX, e.clientY); + return hit && hit.r >= 0 && hit.c >= 0 ? { r: hit.r, c: hit.c } : null; + } + const onDragOver = (e: DragEvent): void => { + const at = dragging && !ctx.readOnly ? cellAt(e) : null; + if (!at) return void dropMark.remove(); + e.preventDefault(); // 놓을 수 있음 + if (e.dataTransfer) e.dataTransfer.dropEffect = "link"; + const { layer, box } = slot(ctx, at.r, at.c); + dropMark.style.cssText = `left:${box.x}px;top:${box.y}px;width:${box.w}px;height:${box.h}px`; + if (dropMark.parentElement !== layer) layer.append(dropMark); + }; + const onDragLeave = (e: DragEvent): void => { + if (!ctx.grid.root.contains(e.relatedTarget as Node | null)) dropMark.remove(); + }; + const onDrop = async (e: DragEvent): Promise => { + const item = dragging; + const at = item && !ctx.readOnly ? cellAt(e) : null; + dropMark.remove(); + dragging = null; + if (!item || !at) return; + e.preventDefault(); + const 칸 = toA1(at.r, at.c); + const other = (ctx.sheet().연결 ?? []).find((l) => l.칸 === 칸 && l.키 !== item.키); + const name = other?.이름 ?? other?.키 ?? ""; + if ( + other && + !(await showConfirmDialog( + TEXT.swap.replace("{cell}", 칸).replace("{name}", name), + TEXT.swapOk, + )) + ) + return back(); + ctx.select(selectCell(ctx.sheet(), at.r, at.c), true); + link(item, 칸); + }; + // ── 자재 고르기 창 ────────────────────────────────────────────────────── function closePick(): void { pick?.remove(); @@ -384,6 +453,10 @@ export function mountLinks( const onZoom = (): void => placeDots(); ctx.grid.root.addEventListener("aislo-grid-zoom", onZoom); + const onDropEvent = (e: DragEvent): void => void onDrop(e); + ctx.grid.root.addEventListener("dragover", onDragOver); + ctx.grid.root.addEventListener("dragleave", onDragLeave); + ctx.grid.root.addEventListener("drop", onDropEvent); return { root: opts ? bar : null, @@ -394,6 +467,10 @@ export function mountLinks( }, destroy() { ctx.grid.root.removeEventListener("aislo-grid-zoom", onZoom); + ctx.grid.root.removeEventListener("dragover", onDragOver); + ctx.grid.root.removeEventListener("dragleave", onDragLeave); + ctx.grid.root.removeEventListener("drop", onDropEvent); + dropMark.remove(); closePick(); for (const d of dots) d.remove(); menu.element.remove(); diff --git a/resources/tester/spreadsheet/spreadsheet_link_set_entry.ts b/resources/tester/spreadsheet/spreadsheet_link_set_entry.ts index 032bfb659..a2ba19fb1 100644 --- a/resources/tester/spreadsheet/spreadsheet_link_set_entry.ts +++ b/resources/tester/spreadsheet/spreadsheet_link_set_entry.ts @@ -1,6 +1,7 @@ /* setLink 순수 길(`spreadsheet_link_set.ts`) 시험 진입 — vite --ssr 로 번들해 Node 로 돌림. * 결과 = JSON {pass, fail, failures} 한 줄(`test_spreadsheet_set_link.py` 가 읽음) */ import { applyCommand } from "../../../A00_Common/spreadsheet/spreadsheet_commands"; +import { linkHues } from "../../../A00_Common/spreadsheet/spreadsheet_link_color"; import { linkAreaCell, linkCommand } from "../../../A00_Common/spreadsheet/spreadsheet_link_set"; import type { Command, Workbook } from "../../../A00_Common/spreadsheet/spreadsheet_types"; @@ -100,4 +101,27 @@ eq("틀린 주소", linkAreaCell(b, "철근|ton", "9C"), null); eq("풀기 · 시트 넘어", links(linkAreaCell(b, "치기|㎥", null)!), [links(book())[0], null]); eq("바뀔 것 없음 = 사본", JSON.stringify(linkAreaCell(b, "깔기|㎡", "B6")), before); +// linkHues — 키로 정해짐 · 차례 무관 · 24 개까지 서로 다름 · 12 개까지 30° 바퀴 · 15° 눈금 +const keys = Array.from({ length: 24 }, (_, i) => `알약${i}|㎡`); +const h1 = linkHues(keys); +const h2 = linkHues([...keys].reverse()); +eq("차례 무관", [...h1.entries()].sort(), [...h2.entries()].sort()); +eq("24 개 서로 다름", new Set(h1.values()).size, 24); +eq( + "15° 눈금", + [...h1.values()].every((h) => h % 15 === 0 && h >= 0 && h < 360), + true, +); +const few = linkHues(keys.slice(0, 12)); +eq( + "12 개 = 30° 바퀴", + [...few.values()].every((h) => h % 30 === 0), + true, +); +eq( + "같은 키 같은 색", + linkHues(["철근|ton", "깔기|㎡"]).get("철근|ton"), + linkHues(["철근|ton"]).get("철근|ton"), +); + console.log(JSON.stringify({ pass, fail: failures.length, failures })); diff --git a/resources/tester/test_spreadsheet_set_link.py b/resources/tester/test_spreadsheet_set_link.py index 000430baf..554ca3aa9 100644 --- a/resources/tester/test_spreadsheet_set_link.py +++ b/resources/tester/test_spreadsheet_set_link.py @@ -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"] == 19 + assert result["pass"] == 24 def test_손잡이_setLink_몸(): @@ -66,3 +66,19 @@ def test_손잡이_setLink_몸(): assert "linkCommand(ctx.book, ctx.sheet().id, key, cell, info)" in body assert "if (command) dispatch(command);" in body # 되돌리기 이력에 쌓임(setDoc 아님) assert "loadLinks();" in body + + +def test_알약_끌어_놓기_색(): + """40-3 ② ③ — 알약 끌어 칸에 놓기(다른 알약 칸이면 물음) · 이은 알약 · 칸 = 키 색 · 두 테마 밝기.""" + src = (ROOT / "A00_Common" / "spreadsheet" / "spreadsheet_links.ts").read_text(encoding="utf-8") + assert "b.draggable = !ctx.readOnly;" in src and 'addEventListener("drop", onDropEvent)' in src + assert "showConfirmDialog(" in src and "link(item, 칸);" in src + assert 'if (!b.draggable) b.addEventListener("mousedown"' in src # 끌기 막지 않음 + assert "const hues = linkHues(found().keys());" in src and 'className: "ss-link-cell"' in src + css = (ROOT / "A00_Common" / "spreadsheet" / "spreadsheet_links.css").read_text( + encoding="utf-8" + ) + assert ( + "--ss-link-l: 31%;" in css and css.count("--ss-link-l: 66%;") == 2 + ) # 밝음 · 어두움(설정 · 시스템) + assert "ss-link-flash" in css # 반짝임 그대로 From ad90e7226006fb4193c6449a3ad6b01ef57eb286 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 4 Oct 2026 22:29:27 +0900 Subject: [PATCH 3/5] auto: 2026-10-04 22:29 (ESD_LAPTOP) --- docs/raw/M02_구조물조합개편/사용자요구_1004_2.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/raw/M02_구조물조합개편/사용자요구_1004_2.md b/docs/raw/M02_구조물조합개편/사용자요구_1004_2.md index 30d4b5a29..3651b8c87 100644 --- a/docs/raw/M02_구조물조합개편/사용자요구_1004_2.md +++ b/docs/raw/M02_구조물조합개편/사용자요구_1004_2.md @@ -19,3 +19,7 @@ 3. 면적산출 상세 - 알약지정은 현재 방법 + 그래그 & 드랍 방식을 추가 - 지정된 알약정보를 색상 자동지정 및 지정된 셀은 색상으로 구분을 할수 있게 반영, 반짝임은 유지 + +## 추가 기능 (10-4 밤 · 원문 그대로) + +추가기능 부여. 면적산출에서 특정 영역을 선정하고 도면영역으로 전달을 할 경우 도면에 테이블을 자동으로 넣고 셀의 내용을 싱크하는 기능을 부여. 만약 동기화가 어려운 경우 업데이트 버튼을 추가해도 무관, 복수개의 테이블이 넘어갈수도 잇으며 위치는 도면에서 사용자가 드래그하여 지정 From e3236558bdbadb323ac5d0d27c536f91d5f00b7c Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 4 Oct 2026 22:37:22 +0900 Subject: [PATCH 4/5] =?UTF-8?q?feat(M02):=20=EA=B5=AC=EC=A1=B0=EB=AC=BC=20?= =?UTF-8?q?=EC=A1=B0=ED=95=A9=20=EC=B9=B4=EB=93=9C=20[=EC=82=AD=EC=A0=9C]?= =?UTF-8?q?=20=C2=B7=20=EC=9D=BC=EC=9C=84=EB=8C=80=EA=B0=80=20=EC=B9=B4?= =?UTF-8?q?=EB=93=9C=20=ED=95=9C=20=EC=A4=84=20=ED=91=9C=20=E2=80=94=20?= =?UTF-8?q?=EA=B7=B8=20=ED=95=AD=EB=AA=A9=20=EB=B9=84=EC=9A=B0=EA=B8=B0(?= =?UTF-8?q?=EB=B2=88=ED=98=B8=20=C2=B7=20=EC=97=B4=20=EA=B7=B8=EB=8C=80?= =?UTF-8?q?=EB=A1=9C=20=C2=B7=20=EB=AC=BC=EC=9D=8C=20=ED=95=9C=20=EB=B2=88?= =?UTF-8?q?=20=C2=B7=20=EB=B9=84=EC=9A=B4=20=EB=92=A4=20=EB=B9=88=20?= =?UTF-8?q?=EC=B9=B4=EB=93=9C)=20=C2=B7=20=EC=A2=81=EC=9D=80=20=EC=B9=B4?= =?UTF-8?q?=EB=93=9C=20=EB=8B=A8=EC=B6=94=20=EC=A4=84=20=EB=B0=94=EA=BF=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 40-2 ④ ⑤ — clearPart(40-4) 잇기 · 찬 카드 · 쓸 수 있을 때만 · 실패 = 카드 안 까닭 · Labor_View compact(40-1). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016PSXsvyDxjNmufteN5bqmt --- .../M02_MasterTemplete_ItemCards.ts | 40 +++++++++++++++++-- .../M02_MasterTemplete_Structure.css | 5 ++- .../M02_MasterTemplete_Structure.ts | 5 +++ resources/tester/test_m02_card_shell.py | 2 +- 4 files changed, 47 insertions(+), 5 deletions(-) diff --git a/M02_MasterTemplete/M02_MasterTemplete_ItemCards.ts b/M02_MasterTemplete/M02_MasterTemplete_ItemCards.ts index 04f748bf9..f62db757e 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_ItemCards.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_ItemCards.ts @@ -69,6 +69,10 @@ const TEXT = { pills: "알약", pillsTip: "아래 알약 줄 보임 · 숨김", take: "가져오기", + clear: "삭제", + clearTip: "이 항목의 {value} 을(를) 비움 — 번호 · 열은 그대로", + clearAsk: "「{name}」 의 {value} 을(를) 비울까? — 번호 · 열은 그대로 · 되돌릴 수 없음", + clearFailed: "비우지 못함 — {value}", takeTip: "다른 항목의 {value} 을(를) 골라 이 칸에 덮어씀 — 번호 · 열은 그대로", blank: "비어 있음 — 새로 만들거나 다른 항목에서 복사해 옴", makeNew: "새로 만들기", @@ -117,6 +121,9 @@ export interface ItemCardsOptions { sheetEdit?: SheetEditOptions; /** 「복사해 가져오기」 · 빈 카드 [복사 생성](39-5) — 다른 항목의 이 칸을 골라 덮어씀 · 없으면 단추 없음 */ onImport?: (kind: CardKind) => void; + /** 카드 [삭제](40-2) — 그 항목 비우기(번호 · 열 그대로 · 물음은 카드가 한 번) · 없으면 단추 없음 · + * 끝나면 카드를 다시 읽음(빈 카드 = [새로 만들기] [복사 생성]) */ + onClear?: (kind: CardKind) => Promise; /** 일위대가 카드(39-5 · 몸 = 일위대가 보기 갈래 `Labor_View`) — 도면 다음 · 지금 층에서 읽음 · * [일위대가 수정] · 빈 카드 [새로 만들기] = 이것(일위대가 상세) */ labor?: () => void; @@ -174,6 +181,8 @@ interface Card { edit: HTMLButtonElement; /** 「복사해 가져오기」 — 찬 카드만 */ take: HTMLButtonElement; + /** [삭제] — 찬 카드만 · 쓸 수 있을 때만 */ + del: HTMLButtonElement; no: HTMLElement; name: HTMLElement; } @@ -218,14 +227,35 @@ export function createItemCards(opts: ItemCardsOptions): ItemCardsHandle { const take = createButton({ label: TEXT.take, variant: "ghost", size: "sm" }); take.title = fill(TEXT.takeTip, PART_WORD[kind]); take.addEventListener("click", () => opts.onImport?.(kind)); - shell.tools.append(help, saver.button, take, edit); - return { kind, shell, saver, help, edit, take, no, name }; + const del = createButton({ label: TEXT.clear, variant: "ghost", size: "sm" }); + del.title = fill(TEXT.clearTip, PART_WORD[kind]); + del.addEventListener("click", () => void clearCard(kind)); + shell.tools.append(help, saver.button, take, edit, del); + return { kind, shell, saver, help, edit, take, del, no, name }; }; + /** 카드 [삭제](40-2) — 물음 한 번(저장 안 한 카드 고침도 함께 버려짐) → 비움 → 카드 다시 읽음(빈 카드가 결과) */ + async function clearCard(kind: CardKind): Promise { + if (!opts.onClear || !column) return; + const ask = fill(TEXT.clearAsk, PART_WORD[kind]).replace("{name}", label); + if (!(await showConfirmDialog(ask))) return; + try { + await opts.onClear(kind); + } catch (error) { + // 알림은 카드 틀 안(토스트 안 씀) — 까닭 + [다시 불러오기] + const card = cards.find((c) => c.kind === kind); + return void card?.shell.fail(fill(TEXT.clearFailed, why(error)), reload); + } + for (const c of cards) c.saver.mark(false); + started.delete(kind); + reload(); + } + /** 빈 카드(39-5) — 고칠 수 있고(가져오기 길이 있는 M02 화면) [새로 만들기] 를 안 눌렀으면 두 단추 · 아니면 미리보기 */ const blank = (c: Card, empty: boolean): boolean => { const show = empty && opts.editable() && !!opts.onImport && !started.has(c.kind); c.take.hidden = show || !opts.editable() || !opts.onImport; + c.del.hidden = show || !opts.editable() || !opts.onClear; // 빈 카드엔 비울 것이 없음 return show; }; /** 빈 카드 몸 — [새로 만들기] = 그 자리에서 빈 미리보기 편집 · [복사 생성] = 가져오기 창 */ @@ -307,7 +337,10 @@ export function createItemCards(opts: ItemCardsOptions): ItemCardsHandle { const paintLabor = async (at: string, mine: number): Promise => { if (!laborCard) return; try { - laborView ??= (await import("./M02_MasterTemplete_Labor_View")).createLaborView(); + // 카드 = 한 줄 표(항목 · 수량산출 · 단위 · 금액 넷 · 40-2) — 자세한 것은 [수정] 상세 + laborView ??= (await import("./M02_MasterTemplete_Labor_View")).createLaborView(undefined, { + compact: true, + }); } catch (error) { if (mine === seq) laborCard.shell.fail(fill(TEXT.noSheet, why(error)), reload); return; @@ -345,6 +378,7 @@ export function createItemCards(opts: ItemCardsOptions): ItemCardsHandle { const can = c.kind === "structure" ? drawEdit : c.kind !== "labor" && sheetEdit; c.saver.button.hidden = c.help.hidden = !can; c.edit.hidden = !opts.editable(); + c.del.hidden = !opts.editable() || !opts.onClear; // 빈 카드는 그린 뒤 `blank` 가 다시 숨김 } pills.hidden = !sheetEdit; }; diff --git a/M02_MasterTemplete/M02_MasterTemplete_Structure.css b/M02_MasterTemplete/M02_MasterTemplete_Structure.css index 133551038..aabaaa5e8 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Structure.css +++ b/M02_MasterTemplete/M02_MasterTemplete_Structure.css @@ -90,7 +90,10 @@ .m02-struct__card-tools { display: inline-flex; - flex: none; + flex: 0 1 auto; /* 좁은 카드(1열 · 400px)에서 단추가 많으면 줄 바꿈 — 카드 밖으로 안 넘침(40-2) */ + flex-wrap: wrap; + justify-content: flex-end; + min-width: 0; align-items: center; gap: var(--spacing-8); margin-left: auto; diff --git a/M02_MasterTemplete/M02_MasterTemplete_Structure.ts b/M02_MasterTemplete/M02_MasterTemplete_Structure.ts index e03126161..44750848b 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Structure.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Structure.ts @@ -17,6 +17,7 @@ import { showConfirmDialog, showToast } from "@ui/ui_template_elements"; import type { LinkOptions } from "../A00_Common/spreadsheet/spreadsheet_links"; import { + clearPart, copyPartFrom, fetchLaborPills, fetchStructureNumbers, @@ -222,6 +223,10 @@ export function createStructurePanel(opts: StructurePanelOptions): StructurePane warnOnFail: (id) => !!numbers[id], // 「복사해 가져오기」 · 빈 카드 [복사 생성](39-5) — 집계표 줄 「조회 후 선택」 과 같은 길 onImport: (kind) => void pickCopy(kind, shown ?? undefined), + // 카드 [삭제](40-2) — 그 항목 비우기(서버 clear 길 · 번호 · 열 그대로 · 쓰기 권한 없으면 403) + onClear: async (kind) => { + if (column) await clearPart(kind, column); + }, labor: () => void openEdit("labor"), // 일위대가 카드(보기 갈래 · 39-3) — 도면 다음 // 면적산출 카드 미리보기 수정(31-3) — 전체 편집기와 같은 알약 · 저장 길 sheetEdit: { diff --git a/resources/tester/test_m02_card_shell.py b/resources/tester/test_m02_card_shell.py index ae89a8b8e..aae597a1d 100644 --- a/resources/tester/test_m02_card_shell.py +++ b/resources/tester/test_m02_card_shell.py @@ -40,7 +40,7 @@ def test_저장_단추_알림_한_벌(): def test_네_카드가_틀을_씀_수정_단추_한_모양(): cards = _text("M02_MasterTemplete_ItemCards.ts") assert "const shell = cardShell({ title, body, sub: [no, name] });" in cards - assert "shell.tools.append(help, saver.button, take, edit);" in cards # 가져오기(39-5 · 40-2) + assert "shell.tools.append(help, saver.button, take, edit, del);" in cards # 40-2 assert 'variant: "filled"' not in cards # [수정] 은 모두 빈 단추(ghost) assert "c.saver.button.hidden = c.help.hidden = !can;" in cards # [저장] · ? 늘 보임 assert "DRAWING_KEYS" in cards and "SHEET_KEYS" in cards From d9276467b1bdbd7fd0e3b80b021c5a6094e55d1b Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 4 Oct 2026 22:37:28 +0900 Subject: [PATCH 5/5] auto: 2026-10-04 22:37 (ESD_LAPTOP) --- docs/raw/M02_구조물조합개편/사용자요구_1004_2.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/raw/M02_구조물조합개편/사용자요구_1004_2.md b/docs/raw/M02_구조물조합개편/사용자요구_1004_2.md index 30d4b5a29..3651b8c87 100644 --- a/docs/raw/M02_구조물조합개편/사용자요구_1004_2.md +++ b/docs/raw/M02_구조물조합개편/사용자요구_1004_2.md @@ -19,3 +19,7 @@ 3. 면적산출 상세 - 알약지정은 현재 방법 + 그래그 & 드랍 방식을 추가 - 지정된 알약정보를 색상 자동지정 및 지정된 셀은 색상으로 구분을 할수 있게 반영, 반짝임은 유지 + +## 추가 기능 (10-4 밤 · 원문 그대로) + +추가기능 부여. 면적산출에서 특정 영역을 선정하고 도면영역으로 전달을 할 경우 도면에 테이블을 자동으로 넣고 셀의 내용을 싱크하는 기능을 부여. 만약 동기화가 어려운 경우 업데이트 버튼을 추가해도 무관, 복수개의 테이블이 넘어갈수도 잇으며 위치는 도면에서 사용자가 드래그하여 지정