Merge remote-tracking branch 'origin/sub_laptop_7' into sub_laptop_3
This commit is contained in:
@@ -108,3 +108,29 @@
|
||||
padding: 4px 6px;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
/* 알약으로 이동한 칸 — 굵은 테두리 + 채움 깜빡임(1.6초 · 끝나면 스스로 지워짐) */
|
||||
.ss-link-flash {
|
||||
position: absolute;
|
||||
z-index: 4;
|
||||
box-sizing: border-box;
|
||||
border: 3px solid var(--color-accent);
|
||||
pointer-events: none;
|
||||
animation: ss-link-flash 1.6s ease-out 1 both;
|
||||
}
|
||||
|
||||
@keyframes ss-link-flash {
|
||||
0%,
|
||||
40%,
|
||||
80% {
|
||||
background: color-mix(in srgb, var(--color-accent) 40%, transparent);
|
||||
}
|
||||
20%,
|
||||
60% {
|
||||
background: transparent;
|
||||
}
|
||||
100% {
|
||||
background: transparent;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
* 위 알약 막대 — 면적산출(입력값) · 재료산출(자재) 알약을 칸에 연결(PLAN 13장).
|
||||
* 칸을 고른 채 밝은 알약을 누르면 그 칸에 연결(알약 흐려짐 + 칸 주소 · 칸 왼아래 작은 세모) ·
|
||||
* 흐린 알약을 누르면 연결 칸으로 · 우클릭 [연결 풀기] · 자재 모드는 [자재 더하기] → 고르기 창 → 새 알약.
|
||||
* 자재 모드 알약 목록 = 호스트(M02)가 `항목` 으로 줌(자재 정보 포함 · 저장은 마스터 자재 조합) — [자재 더하기] ·
|
||||
* 우클릭 [목록에서 빼기] 때 `onItemsChange(목록)` 으로 알림(부른 쪽이 마스터에 적음 · 여기는 안 적음).
|
||||
* 저장 = 시트 `연결`(계약) · 바꿈은 `시트통째` 명령(되돌리기 짝) · 행열 넣기 · 지우기는 `applyLinked` 가
|
||||
* 연결 칸을 옮김(지운 칸이면 빠짐). `createSpreadsheet` 가 `links` 선택 또는 연결 있는 문서일 때만 불러옴(크기 예산).
|
||||
* ========================================================================== */
|
||||
@@ -31,12 +33,16 @@ export interface LinkItem {
|
||||
키: string;
|
||||
이름: string;
|
||||
단위?: string;
|
||||
/** 자재 알약 — 연결하면 이 정보가 칸 연결에 들어감 */
|
||||
자재?: LinkMaterial;
|
||||
}
|
||||
|
||||
export interface LinkOptions {
|
||||
항목: LinkItem[];
|
||||
/** 입력 = 항목이 알약 · 자재 = 연결된 자재 + [자재 더하기] 로 고른 자재 */
|
||||
/** 입력 = 항목이 알약 · 자재 = 항목(자재 정보 포함 · 호스트 목록) + [자재 더하기] 로 더한 자재 */
|
||||
모드: "입력" | "자재";
|
||||
/** 자재 모드에서 목록이 바뀔 때(더하기 · 빼기) 새 목록을 알림 — 저장은 부른 쪽 몫 */
|
||||
onItemsChange?: (항목: LinkItem[]) => void;
|
||||
/** 자재DB 찾기(빈 글 = 처음 목록) — 자재 모드만 */
|
||||
자재찾기?: (글: string) => Promise<LinkMaterial[]>;
|
||||
}
|
||||
@@ -51,12 +57,15 @@ const TEXT = {
|
||||
free: "칸을 고른 채 누르면 이 칸에 연결",
|
||||
linked: "누르면 연결 칸으로 · 우클릭 연결 풀기",
|
||||
unlink: "연결 풀기",
|
||||
remove: "목록에서 빼기",
|
||||
removed: "목록에서 뺌 — 연결도 풀림",
|
||||
add: "+ 자재 더하기",
|
||||
search: "자재 이름 · 규격 · 코드",
|
||||
searching: "찾는 중…",
|
||||
none: "맞는 자재 없음",
|
||||
fail: "자재 목록을 못 받음",
|
||||
picked: "칸을 고른 뒤 새 알약을 누르면 연결",
|
||||
dup: "이미 목록에 있는 자재",
|
||||
};
|
||||
|
||||
type RowCol = Extract<Command, { 종류: "행넣기" | "행지우기" | "열넣기" | "열지우기" }>;
|
||||
@@ -102,6 +111,14 @@ export function applyLinked(book: Workbook, command: Command): CommandEffect {
|
||||
return { ...eff, undo: { 종류: "묶음", 명령: [eff.undo, ...restore] } };
|
||||
}
|
||||
|
||||
/** 자재 → 알약(키 = 자재 코드) */
|
||||
const ofMaterial = (m: LinkMaterial): LinkItem => ({
|
||||
키: m.코드,
|
||||
이름: m.규격 ? `${m.이름} ${m.규격}` : m.이름,
|
||||
단위: m.단위,
|
||||
자재: m,
|
||||
});
|
||||
|
||||
export function mountLinks(
|
||||
ctx: SpreadsheetContext,
|
||||
opts: LinkOptions | undefined,
|
||||
@@ -113,8 +130,8 @@ export function mountLinks(
|
||||
if (opts) ctx.root.prepend(bar);
|
||||
const menu = createMapContextMenu("ss");
|
||||
ctx.root.append(menu.element);
|
||||
/** 이번에 고른 자재(아직 칸 없음 — 연결하면 문서에 들어감) */
|
||||
const picked = new Map<string, LinkMaterial>();
|
||||
/** 알약 목록 — 호스트가 준 항목 + 더한 자재(자재 모드 · 바뀌면 호스트에 알림) */
|
||||
const items: LinkItem[] = opts ? [...opts.항목] : [];
|
||||
let dots: HTMLElement[] = [];
|
||||
let pick: HTMLElement | null = null;
|
||||
|
||||
@@ -133,31 +150,38 @@ export function mountLinks(
|
||||
ctx.dispatch({ 종류: "시트통째", 시트: next });
|
||||
}
|
||||
|
||||
function link(item: LinkItem, 자재?: LinkMaterial): void {
|
||||
function link(item: LinkItem): void {
|
||||
if (ctx.readOnly) return;
|
||||
const sheet = ctx.sheet();
|
||||
const 칸 = toA1(ctx.selection.활성.r, ctx.selection.활성.c);
|
||||
const rest = (sheet.연결 ?? []).filter((l) => l.칸 !== 칸 && l.키 !== item.키);
|
||||
const one: CellLink = { 키: item.키, 이름: item.이름, 칸 };
|
||||
if (item.단위) one.단위 = item.단위;
|
||||
if (자재) one.자재 = 자재;
|
||||
if (item.자재) one.자재 = item.자재;
|
||||
put(sheet, [...rest, one]);
|
||||
back();
|
||||
}
|
||||
|
||||
/** 그 칸으로 가서 눈에 띄게 깜빡임(굵은 테두리 + 채움 · 테마 색 · 끝나면 스스로 사라짐) */
|
||||
function goTo(sheet: Sheet, a1: string): void {
|
||||
const p = parseA1(a1);
|
||||
if (!p) return;
|
||||
if (sheet.id !== ctx.selection.시트) ctx.showSheet(sheet.id);
|
||||
ctx.select(selectCell(ctx.sheet(), p.r, p.c), true);
|
||||
back();
|
||||
requestAnimationFrame(() => {
|
||||
const { layer, box } = slot(ctx, p.r, p.c);
|
||||
if (!box.w || !box.h) return;
|
||||
const f = el("div", { className: "ss-link-flash" });
|
||||
f.style.cssText = `left:${box.x}px;top:${box.y}px;width:${box.w}px;height:${box.h}px`;
|
||||
f.addEventListener("animationend", () => f.remove());
|
||||
layer.append(f);
|
||||
});
|
||||
}
|
||||
|
||||
function pill(
|
||||
item: LinkItem,
|
||||
hit: { sheet: Sheet; link: CellLink } | undefined,
|
||||
자재?: LinkMaterial,
|
||||
) {
|
||||
const commit = (): void => opts?.onItemsChange?.(items.map((i) => ({ ...i })));
|
||||
|
||||
function pill(item: LinkItem, hit: { sheet: Sheet; link: CellLink } | undefined) {
|
||||
const where = hit
|
||||
? hit.sheet.id === ctx.selection.시트
|
||||
? hit.link.칸
|
||||
@@ -173,21 +197,35 @@ export function mountLinks(
|
||||
],
|
||||
});
|
||||
b.addEventListener("mousedown", (e) => e.preventDefault()); // 초점은 격자에 둠
|
||||
b.addEventListener("click", () => (hit ? goTo(hit.sheet, hit.link.칸) : link(item, 자재)));
|
||||
b.addEventListener("click", () => (hit ? goTo(hit.sheet, hit.link.칸) : link(item)));
|
||||
const unlink = (): void => {
|
||||
if (hit)
|
||||
put(
|
||||
hit.sheet,
|
||||
(hit.sheet.연결 ?? []).filter((l) => l.키 !== item.키),
|
||||
);
|
||||
};
|
||||
b.addEventListener("contextmenu", (e) => {
|
||||
e.preventDefault();
|
||||
if (!hit || ctx.readOnly) return;
|
||||
const inList = opts?.모드 === "자재" && items.some((i) => i.키 === item.키);
|
||||
if (ctx.readOnly || !(hit || inList)) return;
|
||||
menu.open(e.clientX, e.clientY, [
|
||||
[
|
||||
TEXT.unlink,
|
||||
() => {
|
||||
put(
|
||||
hit.sheet,
|
||||
(hit.sheet.연결 ?? []).filter((l) => l.키 !== item.키),
|
||||
);
|
||||
back();
|
||||
},
|
||||
],
|
||||
...(hit ? [[TEXT.unlink, () => (unlink(), back())] as [string, () => void]] : []),
|
||||
...(inList
|
||||
? [
|
||||
[
|
||||
TEXT.remove,
|
||||
() => {
|
||||
unlink();
|
||||
items.splice(0, items.length, ...items.filter((i) => i.키 !== item.키));
|
||||
commit();
|
||||
renderBar();
|
||||
if (hit) showToast(TEXT.removed, "info");
|
||||
back();
|
||||
},
|
||||
] as [string, () => void],
|
||||
]
|
||||
: []),
|
||||
]);
|
||||
});
|
||||
return b;
|
||||
@@ -196,29 +234,22 @@ export function mountLinks(
|
||||
function renderBar(): void {
|
||||
if (!opts) return;
|
||||
const map = found();
|
||||
const items: [LinkItem, LinkMaterial?][] = [];
|
||||
const shown: LinkItem[] = [];
|
||||
const seen = new Set<string>();
|
||||
const add = (item: LinkItem, 자재?: LinkMaterial) => {
|
||||
const add = (item: LinkItem) => {
|
||||
if (seen.has(item.키)) return;
|
||||
seen.add(item.키);
|
||||
items.push([item, 자재]);
|
||||
shown.push(item);
|
||||
};
|
||||
const ofMaterial = (m: LinkMaterial): LinkItem => ({
|
||||
키: m.코드,
|
||||
이름: m.규격 ? `${m.이름} ${m.규격}` : m.이름,
|
||||
단위: m.단위,
|
||||
});
|
||||
if (opts.모드 === "입력") for (const item of opts.항목) add(item);
|
||||
// 문서에 있는 연결(항목에서 빠진 것 · 자재) — 풀 수 있게 늘 보임
|
||||
for (const item of items) add(item);
|
||||
// 문서에 있는 연결(목록에서 빠진 것) — 풀 수 있게 늘 보임
|
||||
for (const { link } of map.values())
|
||||
add(
|
||||
link.자재
|
||||
? ofMaterial(link.자재)
|
||||
: { 키: link.키, 이름: link.이름 ?? link.키, 단위: link.단위 },
|
||||
link.자재,
|
||||
);
|
||||
for (const m of picked.values()) add(ofMaterial(m), m);
|
||||
const pills = items.map(([item, 자재]) => pill(item, map.get(item.키), 자재));
|
||||
const pills = shown.map((item) => pill(item, map.get(item.키)));
|
||||
if (opts.모드 === "자재" && opts.자재찾기 && !ctx.readOnly) {
|
||||
const more = el("button", {
|
||||
className: "ss-link ss-link--add",
|
||||
@@ -277,10 +308,14 @@ export function mountLinks(
|
||||
let seq = 0;
|
||||
let timer = 0;
|
||||
const choose = (m: LinkMaterial) => {
|
||||
picked.set(m.코드, m);
|
||||
closePick();
|
||||
renderBar();
|
||||
showToast(TEXT.picked, "info");
|
||||
if (items.some((i) => i.키 === m.코드)) showToast(TEXT.dup, "info");
|
||||
else {
|
||||
items.push(ofMaterial(m));
|
||||
commit();
|
||||
renderBar();
|
||||
showToast(TEXT.picked, "info");
|
||||
}
|
||||
back();
|
||||
};
|
||||
const search = async (): Promise<void> => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* 알약 막대 시험 틀 — 진짜 부품(createSpreadsheet)에 `links` 선택 · 가짜 입력값 · 가짜 자재 목록.
|
||||
/* 알약 막대 시험 틀 — 진짜 부품(createSpreadsheet)에 `links` 선택 · 가짜 입력값 · 가짜 자재 목록(호스트 항목 + 자재찾기).
|
||||
* Vite 개발 서버가 바로 풂: /resources/tester/spreadsheet/harness_links.html?mode=입력|자재
|
||||
* window.ss = 부품 손잡이(getDoc · getLinkedValues) · window.ssFind = 자재찾기 부른 글 목록 */
|
||||
import "../../../ui_template/ui_template_theme.css"; // 앱 색 · 간격 토큰
|
||||
@@ -37,17 +37,29 @@ const MATERIALS: LinkMaterial[] = [
|
||||
];
|
||||
const asked: string[] = [];
|
||||
w.ssFind = asked;
|
||||
w.ssItems = [] as unknown[]; // onItemsChange 로 받은 목록들(부른 순서)
|
||||
// 자재 모드 — 호스트가 주는 처음 목록(자재 정보 포함): 레미콘 하나
|
||||
const hostItems = MATERIALS.slice(0, 1).map((m) => ({
|
||||
키: m.코드,
|
||||
이름: `${m.이름} ${m.규격}`,
|
||||
단위: m.단위,
|
||||
자재: m,
|
||||
}));
|
||||
|
||||
w.ss = createSpreadsheet(document.getElementById("host")!, book, {
|
||||
tabs: false,
|
||||
onChange: () => (w.ssChanges = ((w.ssChanges as number) ?? 0) + 1),
|
||||
links: {
|
||||
모드: mode,
|
||||
항목: [
|
||||
{ 키: "L", 이름: "길이", 단위: "m" },
|
||||
{ 키: "H", 이름: "높이", 단위: "m" },
|
||||
{ 키: "A", 이름: "면적", 단위: "m²" },
|
||||
],
|
||||
항목:
|
||||
mode === "자재"
|
||||
? hostItems
|
||||
: [
|
||||
{ 키: "L", 이름: "길이", 단위: "m" },
|
||||
{ 키: "H", 이름: "높이", 단위: "m" },
|
||||
{ 키: "A", 이름: "면적", 단위: "m²" },
|
||||
],
|
||||
onItemsChange: (목록) => (w.ssItems as unknown[]).push(목록),
|
||||
자재찾기: async (글) => {
|
||||
asked.push(글);
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
|
||||
Reference in New Issue
Block a user