feat(804-3): 줄 쌓기 수정 1차 — 표 값 고르기 · 산출근거 들여쓰기 · 찾아 넣기 창 · 짝 강조 · 기계 이름
- 표 값(요구 678-1): 「표」 꼬리표가 글뿐이라 눌러도 안 열림 → [표 · 고른 줄 · 칸 ▾] 단추 = 품셈 표 창(값 칸 누름 = 그 줄 · 칸) 옵션에 매인 조건(폼타이규격 등) = 그 줄만 고름 · 다른 줄 막힘 + 「위 옵션에서 고름」 안내 · M02 길 표 값 글 수도 고름 - 산출근거 들여쓰기(678-2): 머리 0 · 항목 1 · 가나다 2 · 줄 = 항목 + 1 · 펼친 블록 = 그 줄 + 1(수준 = stackLevels) - 찾아 넣기(678-3): [+ 줄] 인력 · 재료 · 기계 · 다른 단가산출 = M02 [항목 추가] 창 그대로(그 종류만) · 고르면 줄 생김(닫으면 없음) 있는 줄 = [찾아 바꾸기](같은 로직 기계 = 지역 · 보정작업 넘김 그대로) · 옛 찾기 칸 + 고름 목록 · 기종 코드 고르개 걷음 빈 틀 = [찾아 넣기](돈 아닌 단가산출 결과만 — Find_Pick results) - 짝 강조(679-1): 오른쪽 줄 누름 = 왼쪽 카드 강조 + 보이는 자리로 · 왼쪽 누름 = 오른쪽 같은 줄 강조 - 기계 이름(679-2): 기계 줄 = 기종 이름 · 규격 · machine-hourly 답에 로직 키 · 원문번호 더함(기존 칸 그대로) - 모델 common_util_line_stack_pick.ts(boundConds · currentRow · pickCell · addPicked · repick · stackLevels) - 시험 test_804_2 넓힘 3(표 고름 = 값 + 출처 · 서버 셈 같음 · 찾아 넣기 넷 = 요소 키 · 서버 금액 · 다시 고름 · 수준) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+45
-132
@@ -4,7 +4,8 @@
|
||||
* 줄 = 인력 · 재료 · 기계 · 다른 단가산출 · 율 · 식(금액 줄 — 이름 · 단위 · 비목 · 수량 식 · 단가 고르기 또는 식)
|
||||
* · 공식 묶음(`line_stack_rows.ts`) · 글 줄
|
||||
* 고침 = 부른 쪽 `change(fn)` — fn 이 문서 · 차림을 고치면 부른 쪽이 `docOf` 로 굳히고 미리 셈 · 다시 그림(입력은 change 때)
|
||||
* 자원 찾기 = 수식 탭과 같은 길(`/links` 요소 인력 · 재료 · 단가산출) · 기계 = 시간당 기계경비 단가산출 + 기종(그 문서 기계 목록)
|
||||
* 자원 고르기(804-3) = 찾아 넣기 창(`line_stack_pick.ts` — M02 [항목 추가] 창 그대로) · [+ 줄] 인력 · 재료 · 기계 · 다른 단가산출 =
|
||||
* 고르면 줄이 생김(닫으면 안 생김) · 있는 줄 = [찾아 바꾸기]
|
||||
* ========================================================================== */
|
||||
|
||||
import { createButton } from "@ui/ui_template_button";
|
||||
@@ -13,6 +14,7 @@ import { el } 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,
|
||||
freshId,
|
||||
@@ -23,7 +25,8 @@ import {
|
||||
type StackDoc,
|
||||
type StackItem,
|
||||
} from "../common_util/common_util_line_stack";
|
||||
import type { Pick } from "./formula_doc_defs";
|
||||
import { addPicked, repick } from "../common_util/common_util_line_stack_pick";
|
||||
import { openStackPick } from "./line_stack_pick";
|
||||
import { buildBlock, insertTpl, type Tpl } from "./line_stack_rows";
|
||||
import { lt, type LsKey } from "./line_stack_text";
|
||||
|
||||
@@ -32,12 +35,15 @@ export interface EditHooks {
|
||||
answer: DocAnswer;
|
||||
items: StackItem[];
|
||||
change: (fn: (d: StackDoc, items: StackItem[]) => void) => void;
|
||||
ask: (kind: string, words: string, group?: string) => Promise<{ 후보: Pick[] }>;
|
||||
/** 품셈 표 하나(표 고르기 창) */
|
||||
table: (key: string) => Promise<DocTable | null>;
|
||||
subDoc: (key: string) => FormulaDoc | null;
|
||||
name: (key: string) => string;
|
||||
/** 공식 묶음 펼친 자리(`항목.줄`) — 화면 상태만 */
|
||||
open: Set<string>;
|
||||
templates: () => Tpl[];
|
||||
/** 줄 · 항목 누름 — 오른쪽 짝 강조(679) */
|
||||
pair: (loc: string) => void;
|
||||
saveTpl: (tpl: Tpl) => void;
|
||||
}
|
||||
|
||||
@@ -60,47 +66,6 @@ const field = (
|
||||
const btn = (label: string, onClick: () => void, variant: "ghost" | "danger-ghost" = "ghost") =>
|
||||
createButton({ label, variant, size: 24, onClick });
|
||||
|
||||
/** 자원 찾기 — 찾는 말 → 후보 고르기 */
|
||||
function finder(
|
||||
h: EditHooks,
|
||||
kind: string,
|
||||
group: string | undefined,
|
||||
seed: string,
|
||||
onPick: (p: Pick) => void,
|
||||
): HTMLElement {
|
||||
const words = createInputField({
|
||||
placeholder: lt("Find"),
|
||||
value: seed,
|
||||
compact: true,
|
||||
size: 24,
|
||||
type: "search",
|
||||
});
|
||||
const pick = createSelectField({
|
||||
options: [{ value: "", text: lt("Pick") }],
|
||||
compact: true,
|
||||
size: 24,
|
||||
ariaLabel: lt("Pick"),
|
||||
});
|
||||
let found: Pick[] = [];
|
||||
const run = (): void =>
|
||||
void h.ask(kind, words.input.value, group).then((got) => {
|
||||
found = got.후보 ?? [];
|
||||
pick.setOptions([
|
||||
{ value: "", text: found.length ? lt("Pick") : lt("None_Found") },
|
||||
...found.map((p, k) => ({
|
||||
value: String(k),
|
||||
text: [p.이름, p.규격].filter(Boolean).join(" "),
|
||||
})),
|
||||
]);
|
||||
});
|
||||
words.input.addEventListener("change", run);
|
||||
pick.select.addEventListener("change", () => {
|
||||
const p = found[Number(pick.select.value)];
|
||||
if (pick.select.value !== "" && p) onPick(p);
|
||||
});
|
||||
return el("div", { className: "lstk-find", children: [words.root, pick.root] });
|
||||
}
|
||||
|
||||
const KIND_KEY: Record<MoneyKind, LsKey> = {
|
||||
인력: "K_인력",
|
||||
재료: "K_재료",
|
||||
@@ -118,19 +83,6 @@ function moneyBody(h: EditHooks, i: number): HTMLElement[] {
|
||||
const v = own ? d.변수?.find((x) => varId(x) === own) : undefined;
|
||||
const put = (fn: (l: MoneyLine & { 규격?: string }) => void): void =>
|
||||
h.change((doc) => fn(doc.금액![i]));
|
||||
const setVar = (
|
||||
fn: (link: NonNullable<typeof v>["연결"] & object, l: MoneyLine & { 규격?: string }) => void,
|
||||
): void =>
|
||||
h.change((doc) => {
|
||||
const x = doc.변수!.find((y) => varId(y) === own)!;
|
||||
x.연결 ??= {};
|
||||
fn(x.연결, doc.금액![i]);
|
||||
});
|
||||
const named = (p: Pick, l: MoneyLine & { 규격?: string }): void => {
|
||||
l.이름 = p.이름 ?? l.이름;
|
||||
if (p.규격) l.규격 = p.규격;
|
||||
else delete l.규격;
|
||||
};
|
||||
const head = el("div", {
|
||||
className: "lstk-row",
|
||||
children: [
|
||||
@@ -159,54 +111,23 @@ function moneyBody(h: EditHooks, i: number): HTMLElement[] {
|
||||
const price: HTMLElement[] = [];
|
||||
if (own && v) {
|
||||
const link = v.연결 ?? {};
|
||||
const mat = link.재료 as Record<string, unknown> | undefined;
|
||||
const spec = [line.이름, (line as { 규격?: string }).규격].filter(Boolean).join(" ");
|
||||
const label = link.키
|
||||
? link.갈래 === "단가산출"
|
||||
? (h.subDoc(link.키)?.이름 ?? h.name(link.키))
|
||||
: h.name(link.키)
|
||||
: "";
|
||||
price.push(el("span", { className: "lstk-link", text: label || "—" }));
|
||||
if (link.갈래 === "요소")
|
||||
price.push(
|
||||
finder(h, "요소", kind === "인력" ? "인력" : "재료", "", (p) =>
|
||||
setVar((k, l) => {
|
||||
for (const x of Object.keys(k)) delete (k as Record<string, unknown>)[x];
|
||||
Object.assign(k, { 갈래: "요소", 키: p.키 });
|
||||
named(p, l);
|
||||
}),
|
||||
),
|
||||
);
|
||||
else {
|
||||
const sub = link.키 ? h.subDoc(link.키) : null;
|
||||
const codes = sub?.변수?.find((x) => x.열쇠 === "기계")?.연결;
|
||||
if (kind === "기계" && codes?.목록) {
|
||||
const now = String(link.입력?.기계 ?? `'${codes.값 ?? ""}'`).replace(/^'|'$/g, "");
|
||||
price.push(
|
||||
createSelectField({
|
||||
label: lt("Machine"),
|
||||
options: codes.목록.map((x) => ({ value: String(x), text: String(x) })),
|
||||
value: now,
|
||||
compact: true,
|
||||
size: 24,
|
||||
onChange: (x) => setVar((k) => (k.입력 = { ...(k.입력 ?? {}), 기계: `'${x}'` })),
|
||||
}).root,
|
||||
);
|
||||
}
|
||||
price.push(
|
||||
finder(
|
||||
h,
|
||||
"단가산출",
|
||||
undefined,
|
||||
!link.키 && own.startsWith("P_") ? "시간당 기계경비" : "",
|
||||
(p) =>
|
||||
setVar((k, l) => {
|
||||
const keep = k.키 === p.키 ? (k.입력 ?? {}) : {};
|
||||
for (const x of Object.keys(k)) delete (k as Record<string, unknown>)[x];
|
||||
Object.assign(k, { 갈래: "단가산출", 키: p.키, 입력: keep });
|
||||
if (!l.이름) named(p, l);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
? kind === "기계" && spec
|
||||
? spec // 기계 = 기종 이름 · 규격(679 — 코드 번호 아님)
|
||||
: link.갈래 === "단가산출"
|
||||
? (h.subDoc(link.키)?.이름 ?? h.name(link.키))
|
||||
: h.name(link.키)
|
||||
: 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))),
|
||||
),
|
||||
);
|
||||
} else price.push(field(lt("Price"), line.단가, (t) => put((l) => (l.단가 = t.trim())), "l"));
|
||||
const why = (h.answer.줄?.[i] as { 까닭?: string } | undefined)?.까닭;
|
||||
return [
|
||||
@@ -219,6 +140,10 @@ function moneyBody(h: EditHooks, i: number): HTMLElement[] {
|
||||
|
||||
/** [+ 줄] 고르개 값 → 새 줄 */
|
||||
function addLine(h: EditHooks, at: number, kind: string): void {
|
||||
if (["인력", "재료", "기계", "단가산출"].includes(kind))
|
||||
return openStackPick(kind as MoneyKind, (p) =>
|
||||
h.change((d, items) => void addPicked(d, items[at], kind as MoneyKind, p)),
|
||||
);
|
||||
if (kind.startsWith("tpl:")) {
|
||||
const t = h.templates().find((x) => x.id === kind.slice(4));
|
||||
if (t) h.change((d, items) => items[at].줄.push(insertTpl(d, t)));
|
||||
@@ -227,32 +152,11 @@ function addLine(h: EditHooks, at: number, kind: string): void {
|
||||
h.change((d, items) => {
|
||||
const it = items[at];
|
||||
const n = (d.금액 ??= []).length;
|
||||
const money = (line: MoneyLine, id?: string, link?: Record<string, unknown>): void => {
|
||||
if (id) (d.변수 ??= []).push(newVar(id, link as never));
|
||||
const money = (line: MoneyLine): void => {
|
||||
d.금액!.push(line);
|
||||
it.줄.push({ 꼴: "금액", i: n });
|
||||
};
|
||||
if (kind === "인력" || kind === "재료") {
|
||||
const id = freshId(d, kind === "인력" ? "w" : "P", "새");
|
||||
money(
|
||||
{
|
||||
이름: "",
|
||||
단위: kind === "인력" ? "인" : "",
|
||||
비목: kind === "인력" ? "노무비" : "재료비",
|
||||
수량: "1",
|
||||
단가: id,
|
||||
},
|
||||
id,
|
||||
{ 갈래: "요소" },
|
||||
);
|
||||
} else if (kind === "기계" || kind === "단가산출") {
|
||||
const id = freshId(d, kind === "기계" ? "P" : "B", "새");
|
||||
money(
|
||||
{ 이름: "", 단위: kind === "기계" ? "hr" : "", 비목: "묶음", 수량: "1", 단가: id },
|
||||
id,
|
||||
{ 갈래: "단가산출", 입력: {} },
|
||||
);
|
||||
} else if (kind === "율")
|
||||
if (kind === "율")
|
||||
money({ 이름: "공구손료 및 경장비", 단위: "식", 비목: "경비", 단가: "노무비 × 3 %" });
|
||||
else if (kind === "공식") {
|
||||
const id = freshId(d, "x");
|
||||
@@ -287,8 +191,8 @@ function dropLine(d: StackDoc, items: StackItem[], i: number, j: number): void {
|
||||
}
|
||||
|
||||
export function buildEdit(h: EditHooks): HTMLElement {
|
||||
const add = (at: number): HTMLElement =>
|
||||
createSelectField({
|
||||
const add = (at: number): HTMLElement => {
|
||||
const pick = createSelectField({
|
||||
options: [
|
||||
{ value: "", text: `${lt("Add_Line")} ▾` },
|
||||
...(["인력", "재료", "기계", "단가산출", "율"] as MoneyKind[]).map((k) => ({
|
||||
@@ -306,8 +210,13 @@ export function buildEdit(h: EditHooks): HTMLElement {
|
||||
compact: true,
|
||||
size: 24,
|
||||
ariaLabel: lt("Add_Line"),
|
||||
onChange: (x) => x && addLine(h, at, x),
|
||||
}).root;
|
||||
onChange: (x) => {
|
||||
pick.select.value = ""; // 창을 닫아도 [+ 줄 ▾] 로 돌아옴
|
||||
if (x) addLine(h, at, x);
|
||||
},
|
||||
});
|
||||
return pick.root;
|
||||
};
|
||||
const items = h.items.map((it, i) => {
|
||||
const head = el("div", {
|
||||
className: "lstk-ihead",
|
||||
@@ -355,10 +264,12 @@ export function buildEdit(h: EditHooks): HTMLElement {
|
||||
: buildBlock(h, i, j, line);
|
||||
const card = el("div", { className: `lstk-line k-${line.꼴}`, children: [tools, ...body] });
|
||||
card.dataset.loc = `${i}.${j}`;
|
||||
card.addEventListener("pointerdown", () => h.pair(`${i}.${j}`));
|
||||
return card;
|
||||
});
|
||||
return el("div", {
|
||||
const box = el("div", {
|
||||
className: "lstk-item",
|
||||
attrs: { "data-loc": String(i) },
|
||||
children: [
|
||||
head,
|
||||
...(lines.length
|
||||
@@ -366,6 +277,8 @@ export function buildEdit(h: EditHooks): HTMLElement {
|
||||
: [el("div", { className: "lstk-empty", text: lt("Empty_Item") })]),
|
||||
],
|
||||
});
|
||||
head.addEventListener("pointerdown", () => h.pair(String(i)));
|
||||
return box;
|
||||
});
|
||||
return el("div", {
|
||||
className: "lstk-edit",
|
||||
|
||||
Reference in New Issue
Block a user