feat(804-5): 줄 쌓기 왼쪽 손질 — 자원 이름 단추 · 항목 띠 · 끌어 옮기기 · X 지움 되돌리기 · 율 번지 읽는 이름
- 인력 · 기계 · 재료 · 단가산출 줄 자원 이름 = 단추(이름 · 규격) → 찾아 넣기 창 → 바꿈 · [찾아 바꾸기] 걷음 - 항목 머리 = 띠(바탕 · 왼쪽 굵은 띠 · 번호) · 줄 = 들여쓰기 + 왼쪽 세로 선 · 가나다 한 단 더 · 접기 - 순서 = 손잡이 끌기(줄 = 항목 안 · 사이 · 항목 끌기 · 자리 선 표시) · [↑][↓] 걷음 - 지움 = X 아이콘 · 확인 없이 지우고 알림 눌러 되돌리기(그 뒤 고침 있으면 못 함) - 산출근거 율 줄 「번지.복사N.노무비」 → 그 조각 이름 · 값 식 = 번지 값 · 합계 덧셈 항 넷 넘으면 항목 소계끼리 - 알림 부품: 누름 동작 알림은 같은 문구 버림 안 함(두 번째 지움 되돌리기 안 뜨던 흠) - 글 · 근거 카드 높이 늘던 흠 고침 · 시험 test_804_4 끌어옮김 · 번지 이름 더함 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+156
-45
@@ -1,31 +1,39 @@
|
||||
/* =============================================================================
|
||||
* line_stack_edit.ts
|
||||
* 줄 쌓기 왼쪽 편집(PLAN 804-2 · 요구 670~677) — 항목(제목 · 가나다 · [+ 줄] · 지움) 안 줄 카드(위 · 아래 · 지움)
|
||||
* 줄 쌓기 왼쪽 편집(PLAN 804-2 · 요구 670~677) — 항목(띠 · 번호 · 제목 · 가나다 · [+ 줄] · 접기 · X) 안 줄 카드(⋮⋮ · X)
|
||||
* 차림(804-5 · 요구 682) = 항목 머리 띠 · 줄 = 들여쓰기 + 왼쪽 세로 선 · 순서 = 손잡이 끌기(`line_stack_drag.ts`) ·
|
||||
* X = 확인 없이 지우고 알림 눌러 되돌리기 한 번 · 자원 이름 = 단추(누르면 찾아 넣기 창 → 바꿈)
|
||||
* 줄 = 인력 · 재료 · 기계 · 다른 단가산출 · 율 · 식(금액 줄 — 이름 · 단위 · 비목 · 수량 식 · 단가 고르기 또는 식)
|
||||
* · 공식 묶음(`line_stack_rows.ts`) · 글 줄
|
||||
* 고침 = 부른 쪽 `change(fn)` — fn 이 문서 · 차림을 고치면 부른 쪽이 `docOf` 로 굳히고 미리 셈 · 다시 그림(입력은 change 때)
|
||||
* 자원 고르기(804-3) = 찾아 넣기 창(`line_stack_pick.ts` — M02 [항목 추가] 창 그대로) · [+ 줄] 인력 · 재료 · 기계 · 다른 단가산출 =
|
||||
* 고르면 줄이 생김(닫으면 안 생김) · 있는 줄 = [찾아 바꾸기]
|
||||
* 고르면 줄이 생김(닫으면 안 생김) · 있는 줄 = 자원 이름 단추
|
||||
* ========================================================================== */
|
||||
|
||||
import { createButton } from "@ui/ui_template_button";
|
||||
import { createButton, createIconButton } from "@ui/ui_template_button";
|
||||
import { createCheckbox } from "@ui/ui_template_checkbox";
|
||||
import { el } from "@ui/ui_template_elements";
|
||||
import { el, showToast } from "@ui/ui_template_elements";
|
||||
import { createInputField, createSelectField } from "@ui/ui_template_input";
|
||||
import { varId, type FormulaDoc, type MoneyLine } from "../common_util/common_util_formula_doc";
|
||||
import type { DocAnswer } from "../common_util/common_util_formula_doc_calc";
|
||||
import type { DocTable } from "../common_util/common_util_formula_doc_table";
|
||||
import {
|
||||
dropSyms,
|
||||
docOf,
|
||||
freshId,
|
||||
kindOf,
|
||||
moveItem,
|
||||
moveLine,
|
||||
newVar,
|
||||
ownedOf,
|
||||
stackOf,
|
||||
type MoneyKind,
|
||||
type StackDoc,
|
||||
type StackItem,
|
||||
} from "../common_util/common_util_line_stack";
|
||||
import { placeNames } from "../common_util/common_util_line_stack_copy";
|
||||
import { addPicked, repick } from "../common_util/common_util_line_stack_pick";
|
||||
import { attachDrag } from "./line_stack_drag";
|
||||
import { openStackPick, type StackTake } from "./line_stack_pick";
|
||||
import { buildBlock, insertTpl, type Tpl } from "./line_stack_rows";
|
||||
import { lt, type LsKey } from "./line_stack_text";
|
||||
@@ -65,8 +73,8 @@ const field = (
|
||||
f.input.addEventListener("change", () => set(f.input.value));
|
||||
return f.root;
|
||||
};
|
||||
const btn = (label: string, onClick: () => void, variant: "ghost" | "danger-ghost" = "ghost") =>
|
||||
createButton({ label, variant, size: 24, onClick });
|
||||
const btn = (label: string, onClick: () => void) =>
|
||||
createButton({ label, variant: "ghost", size: 24, onClick });
|
||||
|
||||
const KIND_KEY: Record<MoneyKind, LsKey> = {
|
||||
인력: "K_인력",
|
||||
@@ -124,18 +132,42 @@ function moneyBody(h: EditHooks, i: number): HTMLElement[] {
|
||||
: mat
|
||||
? String(mat.검색어 ?? mat.이름 ?? mat.상세구분 ?? "")
|
||||
: "";
|
||||
price.push(
|
||||
el("span", { className: "lstk-link", text: label || "—" }),
|
||||
btn(lt("Repick"), () =>
|
||||
openStackPick(kind, (p) => h.change((doc) => repick(doc, i, own, kind, p))),
|
||||
),
|
||||
);
|
||||
const size = (line as { 규격?: string }).규격;
|
||||
const pick = createButton({
|
||||
label: (size && !label.includes(size) ? `${label} ${size}` : label) || "—",
|
||||
variant: "ghost",
|
||||
tone: "link",
|
||||
size: 24,
|
||||
iconStart: "search",
|
||||
onClick: () => openStackPick(kind, (p) => h.change((doc) => repick(doc, i, own, kind, p))),
|
||||
});
|
||||
pick.classList.add("lstk-res");
|
||||
pick.title = lt("Repick");
|
||||
pick.dataset.repick = String(i);
|
||||
price.push(pick);
|
||||
} else price.push(field(lt("Price"), line.단가, (t) => put((l) => (l.단가 = t.trim())), "l"));
|
||||
const why = (h.answer.줄?.[i] as { 까닭?: string } | undefined)?.까닭;
|
||||
// 율 밑 번지(복사해 온 조각) — 칸 아래 읽는 이름(804-5)
|
||||
const places = /번지\./.test(`${line.단가} ${line.수량 ?? ""}`) ? placeNames(d, h.items) : null;
|
||||
const read = (t: unknown): string =>
|
||||
String(t ?? "").replace(/번지\.([^.\s()]+)\./g, (all, p: string) =>
|
||||
places?.[p] ? `${places[p]} ` : all,
|
||||
);
|
||||
return [
|
||||
el("span", { className: "lstk-kind", text: lt(KIND_KEY[kind]) }),
|
||||
head,
|
||||
el("div", { className: "lstk-row", children: [qty, el("span", { text: "×" }), ...price] }),
|
||||
el("div", {
|
||||
className: "lstk-row",
|
||||
children: [qty, el("span", { className: "lstk-unit", text: "×" }), ...price],
|
||||
}),
|
||||
...(places
|
||||
? [
|
||||
el("div", {
|
||||
className: "lstk-hint",
|
||||
text: `= ${[line.수량, line.단가].filter(Boolean).map(read).join(" × ")}`,
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
...(why ? [el("div", { className: "lstk-miss", text: why })] : []),
|
||||
];
|
||||
}
|
||||
@@ -175,15 +207,6 @@ function addLine(h: EditHooks, at: number, kind: string): void {
|
||||
});
|
||||
}
|
||||
|
||||
/** 줄 옮김 — 항목 끝을 넘으면 옆 항목으로 */
|
||||
function move(items: StackItem[], i: number, j: number, by: -1 | 1): void {
|
||||
const [line] = items[i].줄.splice(j, 1);
|
||||
const to = j + by;
|
||||
if (to < 0 && i > 0) items[i - 1].줄.push(line);
|
||||
else if (to > items[i].줄.length && i < items.length - 1) items[i + 1].줄.unshift(line);
|
||||
else items[i].줄.splice(Math.max(0, Math.min(to, items[i].줄.length)), 0, line);
|
||||
}
|
||||
|
||||
/** 줄 지움 — 금액 줄은 그 줄이 가진 연결 기호까지 · 공식 묶음은 기호까지 */
|
||||
function dropLine(d: StackDoc, items: StackItem[], i: number, j: number): void {
|
||||
const line = items[i].줄[j];
|
||||
@@ -221,10 +244,67 @@ export function buildEdit(h: EditHooks): HTMLElement {
|
||||
});
|
||||
return pick.root;
|
||||
};
|
||||
// X 지움 — 확인 없이 · 알림 누르면 되돌림(그 뒤 다른 고침이 없을 때만)
|
||||
const drop = (what: string, fn: (d: StackDoc, items: StackItem[]) => void): void => {
|
||||
const snap = JSON.stringify(h.doc);
|
||||
let after = "";
|
||||
h.change((d, items) => {
|
||||
fn(d, items);
|
||||
after = JSON.stringify(docOf(d, items));
|
||||
});
|
||||
showToast(`${what || lt("K_줄")} ${lt("Dropped")}`, "info", 6000, () =>
|
||||
h.change((d, items) => {
|
||||
if (JSON.stringify(docOf(d, items)) !== after)
|
||||
return void showToast(lt("Undo_Late"), "warning");
|
||||
const back = JSON.parse(snap) as StackDoc;
|
||||
for (const k of Object.keys(d)) delete (d as Record<string, unknown>)[k];
|
||||
Object.assign(d, back);
|
||||
items.splice(0, items.length, ...stackOf(back));
|
||||
}),
|
||||
);
|
||||
};
|
||||
const x = (title: string, onClick: () => void): HTMLElement => {
|
||||
const b = createIconButton({ icon: "x", title, size: 24, onClick });
|
||||
b.classList.add("lstk-x");
|
||||
return b;
|
||||
};
|
||||
const grip = (): HTMLElement =>
|
||||
el("span", { className: "ui-list-row__grip lstk-grip", attrs: { title: lt("Drag") } });
|
||||
const root = el("div", { className: "lstk-edit" });
|
||||
/** 항목 접힘 표시(화면 상태)를 항목 차례 바뀜에 맞춤 */
|
||||
const refold = (fn: (flags: boolean[]) => void): void => {
|
||||
const flags = h.items.map((_, k) => h.open.has(`fold:${k}`));
|
||||
fn(flags);
|
||||
h.items.forEach((_, k) => h.open.delete(`fold:${k}`));
|
||||
flags.forEach((f, k) => f && h.open.add(`fold:${k}`));
|
||||
};
|
||||
// 항목 번호 — 산출근거와 같은 셈(제목 있는 항목 1. 2. · 가나다 = 가. 나.)
|
||||
let n = 0;
|
||||
let sub = 0;
|
||||
const nos = h.items.map((it) => {
|
||||
if (!it.제목.trim()) return "";
|
||||
const no = it.가나다 ? `${"가나다라마바사아자차카타파하"[sub++] ?? "?"}.` : `${++n}.`;
|
||||
if (!it.가나다) sub = 0;
|
||||
return no;
|
||||
});
|
||||
const items = h.items.map((it, i) => {
|
||||
const folded = h.open.has(`fold:${i}`);
|
||||
const itemGrip = grip();
|
||||
const head = el("div", {
|
||||
className: "lstk-ihead",
|
||||
children: [
|
||||
itemGrip,
|
||||
createIconButton({
|
||||
icon: folded ? "chevronRight" : "chevronDown",
|
||||
title: folded ? lt("Unfold_Item") : lt("Fold_Item"),
|
||||
size: 24,
|
||||
onClick: () => {
|
||||
if (folded) h.open.delete(`fold:${i}`);
|
||||
else h.open.add(`fold:${i}`);
|
||||
h.change(() => undefined);
|
||||
},
|
||||
}),
|
||||
el("b", { className: "lstk-no", text: nos[i] }),
|
||||
field(lt("Title"), it.제목, (t) => h.change((_, items) => (items[i].제목 = t.trim())), "l"),
|
||||
createCheckbox(lt("Gana"), {
|
||||
checked: !!it.가나다,
|
||||
@@ -232,25 +312,29 @@ export function buildEdit(h: EditHooks): HTMLElement {
|
||||
onChange: (c) =>
|
||||
h.change((_, items) => (c ? (items[i].가나다 = true) : delete items[i].가나다)),
|
||||
}).root,
|
||||
...(folded
|
||||
? [el("span", { className: "lstk-hint", text: `${lt("K_줄")} ${it.줄.length}` })]
|
||||
: []),
|
||||
add(i),
|
||||
btn(
|
||||
lt("Del_Item"),
|
||||
() =>
|
||||
h.change((d, items) => {
|
||||
for (let j = items[i].줄.length - 1; j >= 0; j--) dropLine(d, items, i, j);
|
||||
items.splice(i, 1);
|
||||
}),
|
||||
"danger-ghost",
|
||||
x(lt("Del_Item"), () =>
|
||||
drop(it.제목.trim() || lt("Item"), (d, items) => {
|
||||
refold((f) => f.splice(i, 1));
|
||||
for (let j = items[i].줄.length - 1; j >= 0; j--) dropLine(d, items, i, j);
|
||||
items.splice(i, 1);
|
||||
}),
|
||||
),
|
||||
],
|
||||
});
|
||||
const lines = it.줄.map((line, j) => {
|
||||
const lineGrip = grip();
|
||||
const tools = el("div", {
|
||||
className: "lstk-ltools",
|
||||
children: [
|
||||
btn("↑", () => h.change((_, items) => move(items, i, j, -1))),
|
||||
btn("↓", () => h.change((_, items) => move(items, i, j, 1))),
|
||||
btn(lt("Del"), () => h.change((d, items) => dropLine(d, items, i, j)), "danger-ghost"),
|
||||
x(lt("Del"), () =>
|
||||
drop(line.꼴 === "금액" ? String(h.doc.금액?.[line.i]?.이름 ?? "") : "", (d, items) =>
|
||||
dropLine(d, items, i, j),
|
||||
),
|
||||
),
|
||||
],
|
||||
});
|
||||
const body =
|
||||
@@ -266,29 +350,56 @@ export function buildEdit(h: EditHooks): HTMLElement {
|
||||
),
|
||||
]
|
||||
: buildBlock(h, i, j, line);
|
||||
const card = el("div", { className: `lstk-line k-${line.꼴}`, children: [tools, ...body] });
|
||||
const card = el("div", {
|
||||
className: `lstk-line k-${line.꼴}`,
|
||||
children: [lineGrip, tools, ...body],
|
||||
});
|
||||
card.dataset.loc = `${i}.${j}`;
|
||||
card.addEventListener("pointerdown", () => h.pair(`${i}.${j}`));
|
||||
attachDrag(lineGrip, {
|
||||
what: "line",
|
||||
root,
|
||||
self: card,
|
||||
drop: (to) => {
|
||||
const [k, p] = to.split(".").map(Number);
|
||||
h.change((_, items) => moveLine(items, i, j, k, p));
|
||||
},
|
||||
});
|
||||
return card;
|
||||
});
|
||||
const box = el("div", {
|
||||
className: "lstk-item",
|
||||
className: `lstk-item${it.가나다 ? " is-sub" : ""}${folded ? " is-folded" : ""}`,
|
||||
attrs: { "data-loc": String(i) },
|
||||
children: [
|
||||
head,
|
||||
...(lines.length
|
||||
? lines
|
||||
: [el("div", { className: "lstk-empty", text: lt("Empty_Item") })]),
|
||||
...(folded
|
||||
? []
|
||||
: [
|
||||
el("div", {
|
||||
className: "lstk-lines",
|
||||
children: lines.length
|
||||
? lines
|
||||
: [el("div", { className: "lstk-empty", text: lt("Empty_Item") })],
|
||||
}),
|
||||
]),
|
||||
],
|
||||
});
|
||||
head.addEventListener("pointerdown", () => h.pair(String(i)));
|
||||
attachDrag(itemGrip, {
|
||||
what: "item",
|
||||
root,
|
||||
self: box,
|
||||
drop: (to) => {
|
||||
const p = Number(to);
|
||||
refold((f) => moveItem(f, i, p));
|
||||
h.change((_, items) => moveItem(items, i, p));
|
||||
},
|
||||
});
|
||||
return box;
|
||||
});
|
||||
return el("div", {
|
||||
className: "lstk-edit",
|
||||
children: [
|
||||
...items,
|
||||
btn(lt("Add_Item"), () => h.change((_, items) => items.push({ 제목: "", 줄: [] }))),
|
||||
],
|
||||
});
|
||||
root.append(
|
||||
...items,
|
||||
btn(lt("Add_Item"), () => h.change((_, items) => items.push({ 제목: "", 줄: [] }))),
|
||||
);
|
||||
return root;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user