Files
Aislo/A00_Common/line_stack_machine.ts
T
eomsangdonandClaude Opus 5.5 9f8e54f6eb feat(836-1): 단가산출 수식 보기 기계 제어 구역 — 값 칸 · 조종원 인력 줄 · 잡품 재료 줄 · 기계 창 찾기만
- 왼쪽 기계 줄 ▸ = 접는 구역(값 칸 단위 · 고침 표지 · 원래 값 · 조종원 노임 고르기 · 잡품 재료 · 유가 고르기 · 율 %)
- 고침 = 문서 칸 기계덮음(캐시 · 저장 때 서버 다시 셈) · 원본과 같으면 지움 · 같은 기계 줄 한 벌
- 오른쪽 산출근거 펼침 = 덮은 값으로 셈(읽기 그대로) · M01 기초 = 칸 잠김
- 기계 창 = 찾기 · 바꾸기만(양식 · 고쳐 쓰기 → 내 기계 초안 길 걷음)
- 덮음 원본 대조에서 이름 · 단위(화면 글) 뺌 · 조종원 줄 규격 = 직종 이름

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-15 14:01:21 +09:00

347 lines
12 KiB
TypeScript

/* =============================================================================
* line_stack_machine.ts
* 줄 쌓기 기계 창(PLAN 827-1 · 836-1 · 요구 810 · 846 · 853) — 기계 줄 이름 단추 · 옵션 가로 [조회]가 띄움 = 찾기 · 바꾸기만
* 왼쪽 = 기계 찾기 — 맨 위 「지금 이 줄」 · 이 단가산출 옛 초안 · 개인 층 내 기계 · M01 기계(줄마다 시간당)
* 오른쪽 = 고른 기계 이름 · 시간당 · 사본 표지(이 단가산출에 복사된 값 · 원본 새 값) · [이 기계로 바꾸기] = `pick`
* 기계 내부 값 고침 = 단가산출 왼쪽 기계 제어 구역(`line_stack_machine_ctl` · 836-1) — 이 창의 양식 · [고쳐 쓰기] 걷음
* (단가산출 안 고침은 내 기계를 만들지 않음 · 내 기계 목록 · 개인관리 화면은 그대로)
* ========================================================================== */
import { createButton } from "@ui/ui_template_button";
import { createDataTable } from "@ui/ui_template_data_table";
import { createInputField, el, showToast } from "@ui/ui_template_elements";
import { openModal } from "@ui/ui_template_modal";
import { createText } from "@ui/ui_template_text";
import { isDraftNo, type DraftItem } from "../common_util/common_util_line_stack_draft";
import type { Picked } from "../common_util/common_util_line_stack_pick";
import { call, type Layer } from "../M02_MasterTemplete/M02_MasterTemplete_Api_Fetch";
import {
listMyMachines,
machineArg,
machineRows,
type MachineBrief,
type MyHourly,
type MyMachineRow,
} from "../M02_MasterTemplete/M02_MyMachine_Api";
import { hourlyText } from "../M02_MasterTemplete/M02_MyMachine_Form";
import { mt } from "../M02_MasterTemplete/M02_MyMachine_Text";
import { lt } from "./line_stack_text";
/** 사본 줄(서버 `copy_row`) — 키 · 이름 · 규격 */
interface CopyRowData {
키: string;
원문번호?: string;
이름?: string;
규격?: string;
}
export interface CopyAnswer {
사본: CopyRowData | null;
지금: CopyRowData | null;
새값: boolean;
}
export interface MachineCtx {
/** 층 · 주인 — null = M01 단가산출 기초(M01 기계만) */
scope: { layer: Layer; owner: string | null } | null;
/** 이 SK 키 */
me?: string;
/** 이 SK 사본에 박힌 기계 줄(키 있는 M02 SK) */
copyRow?: (no: string) => Promise<CopyAnswer>;
}
export interface MachineWindowOptions {
ctx: MachineCtx;
drafts: DraftItem[];
/** 지금 줄 기계 입력 글(따옴표 뗌 · 원문번호 · 개인 EQP… · 개인 EQP초안-N) · 없음 = null */
now: string | null;
query: string;
/** 없으면 보기만(옵션 가로 조회) */
pick?: (p: Picked) => void;
}
type Kind = "now" | "draft" | "mine" | "m01";
interface Cand {
kind: Kind;
/** 기계 입력 글 */
no: string;
이름: string;
규격: string;
계: number | null;
구분: string;
draft?: DraftItem;
mine?: MyMachineRow;
/** M01 키(EQ…) */
key?: string;
}
interface Shown {
name: string;
spec: string;
lines: HTMLElement[];
logic?: string | null;
}
const kindOf = (no: string): Kind =>
isDraftNo(no) ? "draft" : no.startsWith("개인 ") ? "mine" : "m01";
const hourOf = (h: MyHourly | undefined): number | null => (h?.ok ? (h.계 ?? null) : null);
const same = (a: Cand | undefined, b: Cand | null): boolean =>
!!a && !!b && a.kind === b.kind && a.no === b.no && a.key === b.key;
export function openMachineWindow(o: MachineWindowOptions): void {
const { ctx } = o;
const owner = ctx.scope?.owner ?? null;
const personal = ctx.scope?.layer === "personal";
let mines: MyMachineRow[] = [];
let m01: { ref: string; 이름: string; 규격?: string | null; 구분?: string | null }[] = [];
const briefs = new Map<string, MachineBrief>();
let cands: Cand[] = [];
let sel: Cand | null = null;
let seq = 0;
let showSeq = 0;
let timer = 0;
let close = (): void => {};
const search = createInputField({ type: "search", placeholder: mt("Find") });
search.input.value = o.query;
const hint = createText("");
const right = el("div", { className: "lstk-mwin__form", attrs: { "data-mwin-form": "" } });
const table = createDataTable({
caption: mt("Win_Title"),
columns: [
{ key: "품명", label: mt("Name"), width: 150 },
{ key: "규격", label: mt("Spec"), width: 80 },
{ key: "금액", label: mt("Hourly"), kind: "number", decimals: 0, width: 84 },
{ key: "구분", label: mt("Win_Kind"), width: 110 },
],
rows: [],
rowClass: (i) => (same(cands[i], sel) ? "is-title" : null),
onRowClick: (i) => {
if (cands[i]) void choose(cands[i]);
},
});
/* --- 목록 --- */
const nowCand = (): Cand | null => {
if (!o.now) return null;
const d = o.drafts.find((x) => `개인 ${x.표지}` === o.now);
const m = mines.find((x) => x.원문번호 === o.now);
const b = [...briefs.values()].find((x) => x.원문번호 === o.now);
const name = d?.줄.이름 ?? m?.이름 ?? b?.이름 ?? o.now;
return {
kind: "now",
no: o.now,
이름: name,
규격: d?.줄.규격 ?? m?.규격 ?? b?.규격 ?? "",
계: d ? hourOf(d.시간당) : m ? hourOf(m.시간당) : hourOf(b?.시간당),
구분: mt("Win_Now"),
};
};
const draw = (): void => {
const words = search.input.value.trim().toLowerCase().split(/\s+/).filter(Boolean);
const hit = (t: string): boolean => words.every((w) => t.toLowerCase().includes(w));
const now = nowCand();
cands = [
...(now ? [now] : []),
...o.drafts
.filter((d) => hit(`${d.줄.이름} ${d.줄.규격}`))
.map((d): Cand => ({
kind: "draft",
no: `개인 ${d.표지}`,
이름: d.줄.이름,
규격: d.줄.규격,
계: hourOf(d.시간당),
구분: mt("Win_Draft"),
draft: d,
})),
...mines
.filter((r) => hit(`${r.이름} ${r.규격} ${r.키}`))
.map((r): Cand => ({
kind: "mine",
no: r.원문번호,
이름: r.이름,
규격: r.규격,
계: hourOf(r.시간당),
구분: `${mt("Pick_Group")} · ${mt(r.방식)}`,
mine: r,
})),
...m01.map((x): Cand => ({
kind: "m01",
no: briefs.get(x.ref)?.원문번호 ?? "",
이름: x.이름,
규격: x.규격 ?? "",
계: hourOf(briefs.get(x.ref)?.시간당),
구분: [mt("Win_M01"), x.구분].filter(Boolean).join(" · "),
key: x.ref,
})),
];
hint.textContent = `${cands.length}`;
table.setRows(cands.map((c) => ({ 품명: c.이름, 규격: c.규격, 금액: c.계, 구분: c.구분 })));
void fill();
};
/** M01 줄 시간당 — 보이는 앞 40 줄만 뒤에서(색인 메모 · 없는 것만) */
const fill = async (): Promise<void> => {
const want = cands
.filter((c) => c.kind === "m01" && c.key && !briefs.has(c.key))
.slice(0, 40)
.map((c) => c.key!);
if (!want.length) return;
const mine = seq;
const got = await machineRows(want).catch(() => []);
for (const b of got) briefs.set(b.키, b);
if (mine === seq && got.length) draw();
};
const find = async (): Promise<void> => {
const mine = ++seq;
const q = search.input.value.trim();
const got = await call<{ items: typeof m01 }>(
`/logic-lib/elements?group=${encodeURIComponent("기계")}&q=${encodeURIComponent(q)}&limit=200`,
).catch(() => ({ items: [] }));
if (mine !== seq) return;
m01 = got.items;
draw();
};
search.input.addEventListener("input", () => {
draw();
window.clearTimeout(timer);
timer = window.setTimeout(() => void find(), 250);
});
/* --- 오른쪽 --- */
const briefOf = async (no: string, key?: string): Promise<MachineBrief | undefined> => {
const at = key ?? no;
const have = key ? briefs.get(key) : [...briefs.values()].find((b) => b.원문번호 === no);
if (have) return have;
const [got] = await machineRows([at]).catch(() => []);
if (got) briefs.set(got.키, got);
return got;
};
/** 고른 줄 → 이름 · 글(시간당 · 사본 표지) · 바꿀 식 */
const resolve = async (c: Cand): Promise<Shown> => {
const k = c.kind === "now" ? kindOf(c.no) : c.kind;
const lines: HTMLElement[] = [];
if (k === "draft") {
const d = c.draft ?? o.drafts.find((x) => `개인 ${x.표지}` === c.no);
if (!d) throw new Error(mt("Hourly_None"));
lines.push(createText(mt("Win_Draft"), "muted"));
if (d.시간당)
lines.push(createText(`${mt("Server_Hourly")} ${hourlyText(d.시간당 as MyHourly)}`));
return { name: d.줄.이름, spec: d.줄.규격, lines, logic: d.시간당?.로직 };
}
// 지금 줄 = 이 단가산출 사본 값(있으면)
const copy = c.kind === "now" && ctx.copyRow ? await ctx.copyRow(c.no).catch(() => null) : null;
const row = copy?.사본 ?? null;
if (row) {
lines.push(createText(mt("Win_Copy"), "muted"));
if (copy?.새값) lines.push(createText(mt("Win_Newer"), "warning"));
}
if (k === "mine") {
const m = c.mine ?? mines.find((r) => r.원문번호 === c.no);
if (m) lines.push(createText(`${mt("Server_Hourly")} ${hourlyText(m.시간당)}`));
return {
name: row?.이름 ?? m?.이름 ?? c.이름,
spec: row?.규격 ?? m?.규격 ?? c.규격,
lines,
logic: m?.시간당.로직,
};
}
const b = await briefOf(c.no, c.key);
if (b) lines.push(createText(`${mt("Win_Live")} · ${hourlyText(b.시간당)}`));
if (!row && !b) throw new Error(mt("Hourly_None"));
return {
name: row?.이름 ?? b?.이름 ?? c.이름,
spec: row?.규격 ?? b?.규격 ?? c.규격,
lines,
logic: b?.시간당.ok ? b.시간당.로직 : null,
};
};
const pickedOf = (c: Cand, s: Shown): Picked | null => {
if (!s.logic) return null;
const no = c.kind === "mine" && c.mine ? machineArg(c.mine.원문번호) : `'${c.no}'`;
return { 키: s.logic, 이름: s.name, 입력: { 기계: no }, ...(s.spec ? { 규격: s.spec } : {}) };
};
const choose = async (c: Cand): Promise<void> => {
sel = c;
table.setRows(cands.map((x) => ({ 품명: x.이름, 규격: x.규격, 금액: x.계, 구분: x.구분 })));
await show(c);
};
async function show(c: Cand): Promise<void> {
const mine = ++showSeq;
right.replaceChildren(createText(mt("Server_Hourly"), "muted"));
let s: Shown;
try {
s = await resolve(c);
} catch (error) {
if (mine === showSeq)
right.replaceChildren(
createText(error instanceof Error ? error.message : mt("Failed"), "error"),
);
return;
}
if (mine !== showSeq) return;
const bar: HTMLElement[] = [];
const p = pickedOf(c, s);
if (o.pick && c.kind !== "now") {
const b = createButton({
label: mt("Win_Swap"),
variant: "filled",
size: 28,
disabled: !p,
onClick: () => {
if (!p) return;
close();
o.pick!(p);
},
});
b.dataset.mwinSwap = "";
if (!p) b.title = mt("Pick_Direct");
bar.push(b);
}
right.replaceChildren(
el("div", {
className: "ui-toolbar",
children: [el("strong", { text: `${s.name} ${s.spec}`.trim() }), ...bar],
}),
...s.lines,
createText(lt("Win_Ctl_Note"), "muted"),
);
right.dataset.mwinMode = "view";
}
({ close } = openModal({
title: mt("Win_Title"),
closeLabel: mt("Close"),
width: "1100px",
wide: true,
mount: (body) =>
body.append(
el("div", {
className: "lstk-mwin",
attrs: { "data-machine-window": "" },
children: [
el("div", {
className: "lstk-mwin__list",
children: [
el("div", { className: "ui-toolbar", children: [search.root, hint] }),
table.root,
],
}),
right,
],
}),
),
}));
right.replaceChildren(createText(mt("Win_Hint"), "muted"));
void (async () => {
if (personal) mines = await listMyMachines(owner).catch(() => []);
if (o.now && kindOf(o.now) === "m01") await briefOf(o.now);
await find();
const first = cands[0];
if (first && first.kind === "now") await choose(first);
})().catch((error) => showToast(error instanceof Error ? error.message : mt("Failed"), "error"));
}