Merge remote-tracking branch 'origin/dev' into sub_laptop_2
This commit is contained in:
@@ -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<string>): Map<string, number> {
|
||||
const out = new Map<string, number>();
|
||||
const used = new Set<number>();
|
||||
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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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<void> => {
|
||||
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();
|
||||
|
||||
@@ -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<void>;
|
||||
/** 일위대가 카드(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<void> {
|
||||
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<void> => {
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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 }));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 # 반짝임 그대로
|
||||
|
||||
Reference in New Issue
Block a user