Files
Aislo/M01_MasterData/M01_MasterData_UI_LogicLab_Copy.ts
eomsangdonandClaude Opus 5.5 09134bab84 refactor(M02): 띠 알림 · 빈 상태 · 보조 글 · 배지 · 탭 · 표 · 목록 줄 · 도구줄 · 체크를 부품으로 (PLAN 553-5)
- DataFlow: 표 넷 = 데이터표 · 함수 판 알림 · 선/알약 경고 · 끊긴 곳 = 띠 알림 · 알약 · 칸 종류 = 배지 · 고르는 줄 = 목록 줄 · 입력 넷 = 입력 · 도구줄 여섯 · 날 색 → 공통 값
- 구조물 옵션 · 구조물 · 도면 · 면적 · 품목 고르기: 판 띠 = 띠 알림 · 한눈/종류별/부품 = 탭 · 표 둘 = 데이터표 · 체크 둘 = 체크 · 카드 · 번호 = 카드 · 배지 · 도면 도구줄
- 일위대가 · 호표 · 실무자료 · 본 화면 · 서고: 알림 = 띠 알림 · 빈 상태 · 보조 글 · 도구줄 여섯 · 옛 보라 별칭 0
- M01 로직 입력칸 = 입력 부품(작은 칸) · 일위대가 상세 줄 수 고정 시험 693 · 지킴 시험 M02 몫 더함

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 23:44:19 +09:00

375 lines
12 KiB
TypeScript

/* =============================================================================
* M01_MasterData_UI_LogicLab_Copy.ts
* 복사해서 만들기(PLAN 3-4 · 계약 7장) — 정본·자체 로직을 별칭 꼴 식으로 펴 보이고
* 줄을 고치거나 더한 뒤 자체 로직(GX)으로 저장.
*
* [본떠 만들기] → GET /logic/formula(별칭 폼) → 별칭 표 바꾸기·로직 부르기 갈래 고르기·
* 식 줄 고치기·더하기 → [시험 계산](POST /logic/formula/preview 로 원문 되돌린 뒤 /calc 재사용)
* → [저장](POST /logic/formula/save) → afterSave 로 새 키를 알림(정본은 손대지 않음).
* ========================================================================== */
import {
createButton,
createInputField,
createSelectField,
el,
hideLoadingOverlay,
showLoadingOverlay,
showToast,
} from "@ui/ui_template_elements";
import {
copyLogic,
fetchLogicFormula,
previewLogicFormula,
saveLogicFormula,
type FormulaAlias,
type FormulaLine,
type LogicRow,
} from "./M01_MasterData_UI_Logic_Api";
import { buildCalc } from "./M01_MasterData_UI_Logic_Calc";
import { fetchTables, type TableHead } from "./M01_MasterData_Api_Fetch";
import { tx } from "./M01_MasterData_UI_Logic_Text";
import { tl } from "./M01_MasterData_UI_LogicLab_Text";
import "./M01_MasterData_UI_LogicLab_Style.css";
import { createBadge } from "@ui/ui_template_badge";
import { createText } from "@ui/ui_template_text";
import { openModal } from "@ui/ui_template_modal";
const OWNERS = ["현장", "공용"] as const;
const TABLE_GROUPS = ["소요량", "계수"] as const;
const GROUP_LABEL: Record<FormulaLine["갈래"], string> = {
중간: "중간",
호표: "호표",
덧줄: "덧줄",
결과: "결과",
};
export interface CopyOptions {
/** 본뜰 원본 키(정본·자체 둘 다) */
key: string;
/** 저장 뒤 새 GX 키를 알림 — 밖(LogicLab)이 목록 새로고침·그 키 열기를 함 */
afterSave: (newKey: string) => void | Promise<void>;
}
/** 자리표(`호표[0].수량` 등)로 로직 안 그 칸을 읽고 씀 — `master_copy.slots()` 와 같은 자리 */
function field(row: LogicRow, place: string): { get: () => string; set: (v: string) => void } {
if (place === "결과.식") {
return {
get: () => row.결과?.식 ?? "",
set: (v) => {
if (row.결과) row.결과.식 = v;
},
};
}
const m = /^(호표|중간|덧줄)\[(\d+)\]\.(.+)$/.exec(place);
if (!m) return { get: () => "", set: () => {} };
const [, group, idxStr, col] = m;
const idx = Number(idxStr);
const arr = row[group] as Record<string, unknown>[] | undefined;
return {
get: () => (arr?.[idx] ? String(arr[idx][col] ?? "") : ""),
set: (v) => {
if (arr?.[idx]) arr[idx][col] = v;
},
};
}
/** 별칭 원문의 키만 갈아 끼움 — 「찾기(QF000421, …)」 → 「찾기(QF999999, …)」(인자는 그대로) */
function swapKey(raw: string, newKey: string): string {
return raw.replace(/^(찾기|로직)\(\s*[A-Za-z0-9]+/, `$1(${newKey}`);
}
/** 표 바꾸기 모달 — 소요량·계수 두 그룹을 함께 찾음 */
function openTablePick(onPick: (table: TableHead) => void): void {
const search = createInputField({ type: "search", placeholder: tl("Copy_PickSearch") });
const list = el("div", { className: "m01-logic__pick-list" });
// 모달 부품 — 제목줄 [닫기] · 바깥 누르기 · Esc · 위 고정 · 찾기 칸에 커서
const { close } = openModal({
title: tl("Copy_PickTitle"),
closeLabel: tl("Modal_Close"),
dialogClass: "m01-logic__pick",
backdropClass: "m01-logic__backdrop",
mount: (body) => body.append(search.root, list),
});
let timer: number | undefined;
const run = async (): Promise<void> => {
const q = search.input.value.trim();
const pages = await Promise.all(
TABLE_GROUPS.map((group) => fetchTables(group, "", "", q, 1, 20)),
);
const hits = pages.flatMap((p) => p.tables);
list.replaceChildren(
...(hits.length
? hits.map((t) => {
const b = el("button", {
className: "m01-logic__pick-row ui-list-row ui-sidebar-row",
attrs: { type: "button", "data-size": "32" },
children: [
el("span", { text: `${t.이름 || t.키} · ${t.원문번호 || ""}` }),
el("span", { className: "ui-text ui-text--muted", text: t.키 }),
],
});
b.addEventListener("click", () => {
onPick(t);
close();
});
return b;
})
: [el("p", { className: "ui-text ui-text--muted", text: tl("Copy_PickNone") })]),
);
};
search.input.addEventListener("input", () => {
window.clearTimeout(timer);
timer = window.setTimeout(() => void run(), 250);
});
void run();
}
function aliasRow(a: FormulaAlias, onSwap: (raw: string) => void, sub: HTMLElement): HTMLElement {
const about =
a.종류 === "찾기"
? [a.밑이름 || a.참조, Object.keys(a.값칸).join("·")].filter(Boolean).join(" · ")
: [a.밑이름 || a.참조, a.결과단위].filter(Boolean).join(" · ");
return el("div", {
className: "m01lab__box",
children: [
el("div", {
className: "m01-logic__section-head",
children: [
el("strong", { text: a.이름 }),
el("span", { className: "ui-text ui-text--muted", text: about }),
],
}),
el("pre", { className: "m01lab__expr", text: a.원문 }),
sub,
...(a.종류 === "찾기"
? [
createButton({
label: tl("Copy_Alias_Swap"),
variant: "ghost",
onClick: () => openTablePick((t) => onSwap(swapKey(a.원문, t.키))),
}),
]
: []),
],
});
}
function logicAliasControls(
a: FormulaAlias,
onForked: (raw: string) => void,
): { root: HTMLElement } {
let forked = false;
const original = a.원문;
const status = el("span", { className: "ui-text ui-text--muted" });
const refBtn = createButton({
label: tl("Copy_Alias_Ref"),
variant: "ghost",
onClick: () => {
forked = false;
a.원문 = original;
refresh();
},
});
const forkBtn = createButton({
label: tl("Copy_Alias_Fork"),
variant: "ghost",
onClick: () => void doFork(),
});
const refresh = (): void => {
refBtn.disabled = !forked;
forkBtn.disabled = forked;
status.textContent = forked ? tl("Copy_Alias_Forked") : "";
};
const doFork = async (): Promise<void> => {
try {
const got = await copyLogic(a.참조);
forked = true;
a.원문 = swapKey(original, got.key);
onForked(a.원문);
refresh();
} catch (error) {
showToast(error instanceof Error ? error.message : tx("Load_Failed"), "error");
}
};
refresh();
return { root: el("div", { children: [refBtn, forkBtn, status] }) };
}
function lineBox(row: LogicRow, line: FormulaLine, onChange: () => void): HTMLElement {
const f = field(row, line.자리);
const box = el("textarea", { className: "ui-input ui-input--sm m01-logic__input m01lab__qty" });
box.value = f.get();
box.rows = Math.max(1, Math.ceil(box.value.length / 60));
box.addEventListener("input", () => {
f.set(box.value);
onChange();
});
const tag = [GROUP_LABEL[line.갈래], line.비목].filter(Boolean).join(" · ");
return el("div", {
className: "m01lab__box",
children: [
el("div", {
className: "m01-logic__section-head",
children: [
el("strong", { text: line.이름 || line.자리 }),
createBadge(tag, "accent"),
...(line.단위
? [el("span", { className: "ui-text ui-text--muted", text: line.단위 })]
: []),
],
}),
box,
],
});
}
function addExtra(row: LogicRow, onChange: () => void, redraw: () => void): void {
row.덧줄 = row.덧줄 ?? [];
row.덧줄.push({ 이름: tl("Copy_ExtraName"), 식: "0", 비목: "재료비" });
onChange();
redraw();
}
/** 복사 화면을 전체 덮는 시트로 엶 */
export async function openCopyScreen(opt: CopyOptions): Promise<void> {
showLoadingOverlay();
let got;
try {
got = await fetchLogicFormula(opt.key);
} catch (error) {
hideLoadingOverlay();
showToast(error instanceof Error ? error.message : tx("Load_Failed"), "error");
return;
}
hideLoadingOverlay();
const row: LogicRow = got.로직;
const marks: FormulaAlias[] = got.별칭;
const nameField = createInputField({ label: tl("Copy_Name"), value: `${row.이름} 복사본` });
const ownerField = createSelectField({
label: tl("Copy_Owner"),
options: OWNERS.map((o) => ({
value: o,
text: o === "현장" ? tl("Copy_Site") : tl("Copy_Shared"),
})),
value: "현장",
});
const aliasHost = el("div", { className: "m01-logic__scroll" });
const linesHost = el("div", { className: "m01-logic__scroll" });
const calcHost = el("div", { className: "m01lab__calc" });
const values: Record<string, string> = {};
let timer: number | undefined;
const refreshCalc = (): void => {
window.clearTimeout(timer);
timer = window.setTimeout(() => void runPreview(), 300);
};
const runPreview = async (): Promise<void> => {
try {
const preview = await previewLogicFormula(row, marks);
buildCalc(calcHost, {
savedKey: null,
file: got.file,
row: preview.logic,
dirty: () => true,
values,
onLines: () => undefined,
});
} catch (error) {
calcHost.replaceChildren(createText(error instanceof Error ? error.message : "", "error"));
}
};
const drawLines = (): void => {
linesHost.replaceChildren(...got.줄.map((line) => lineBox(row, line, refreshCalc)));
};
const drawAliases = (): void => {
aliasHost.replaceChildren(
...marks.map((a) => {
const sub =
a.종류 === "로직" ? logicAliasControls(a, refreshCalc).root : el("span", { text: "" });
return aliasRow(
a,
(raw) => {
a.원문 = raw;
refreshCalc();
},
sub,
);
}),
);
};
drawLines();
drawAliases();
refreshCalc();
let close = (): void => {};
const onSave = async (): Promise<void> => {
showLoadingOverlay();
try {
const saved = await saveLogicFormula({
로직: row,
별칭: marks,
owner: ownerField.select.value,
이름: nameField.input.value.trim() || row.이름,
본뜬키: opt.key,
});
showToast(tl("Copy_Saved"), "success");
close();
await opt.afterSave(saved.key);
} catch (error) {
showToast(error instanceof Error ? error.message : tx("Save_Failed"), "error");
} finally {
hideLoadingOverlay();
}
};
// 모달 부품 — 제목줄 [취소] · 바깥 누르기 · Esc · 위 고정 · 폭만 복사 시트 곁값
({ close } = openModal({
title: `${tl("Copy_Title")} — ${row.이름}(${opt.key})`,
closeLabel: tl("Copy_Cancel"),
dialogClass: "m01-logic__pick m01copy__sheet",
backdropClass: "m01-logic__backdrop",
mount: (body) =>
body.append(
el("div", {
className: "m01copy__head",
children: [nameField.root, ownerField.root],
}),
el("section", {
className: "m01-logic__section",
children: [el("h3", { text: tl("Copy_Aliases") }), aliasHost],
}),
el("section", {
className: "m01-logic__section",
children: [
el("div", {
className: "m01-logic__section-head",
children: [
el("h3", { text: tl("Copy_Lines") }),
createButton({
label: tl("Copy_AddExtra"),
variant: "ghost",
onClick: () => addExtra(row, refreshCalc, drawLines),
}),
],
}),
linesHost,
],
}),
el("section", { className: "m01-logic__section", children: [calcHost] }),
el("div", {
className: "m01-logic__pick-bar",
children: [
el("span", {}),
el("span", {}),
createButton({ label: tl("Copy_Save"), onClick: () => void onSave() }),
],
}),
),
}));
}