Files
Aislo/M01_MasterData/M01_MasterData_UI_LogicLab_Detail.ts
T
eomsangdonandClaude Sonnet 5.5 c9bdf6d891 feat(799-2): M01 로직 상세 · M02 단가산출 상세 옛 탭 넷(시트 · 인력/재료/경비 · 시험 계산 · 텍스트 수식) 걷음 — 보기는 수식 · 단가산출 상세 · 셈 구동 따로
- 셈 구동 떼기: 새 M01_MasterData_UI_Logic_Drive.ts(driveRun · previewText · autoFill · createDrive) — M01 상세는 숨은 시험 계산 칸 단추 대신 구동 함수 · 시험 계산 칸(Logic_Calc)은 로직 복사 화면이 쓰므로 남기고 같은 구동을 부름 · 재료 고름은 rerun 고리
- M02: 새 _Sangeun_Tabs.ts(수식 · 단가산출 상세 보기 고르기) · Detail 의 시험 계산 · 인력/재료/경비 걷음
- 걷은 파일(git rm): M01 LogicLab_Sheet · Logic_Sheet_Model · LogicLab_Formula(.ts .css) · FormulaTokens · LogicLab_Est / M02 Sangeun_Sheet(.ts .css) · SheetEdit · SheetFormula · logic Sheet_Model · Const_Model · Formula_Model · 부품 시트 줄(createCalcSheetRow) · Cover_Model.sheetGroups · Calc 의 M02 고리(answer · shown · held) · 죽은 CSS · 글
- 서버 길 · 문서 칸(변수줄 · 글줄 · 덮개)은 남김 — 금액 그대로
- 낡은 시험: test_790_1(시트 화면 전부) · test_714_3 · test_748_2(+ helper)는 화면이 없어져 지움 · test_794_1 · test_790_2 는 화면 · TS 거울만 덜고 서버 시험 남김 · test_714_2 · 717_2b · 721 · 722(원래 틀린 한 줄 고침) · 734 · 737 · 750_2 · 793_3b · 796_2 · 796_2b 는 보기 둘 · 새 파일에 맞춤 · 새 test_799_2_old_tabs_gone

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-07 12:57:30 +09:00

241 lines
9.7 KiB
TypeScript

/* =============================================================================
* M01_MasterData_UI_LogicLab_Detail.ts
* 「단가산출 로직」 상세 화면 — 컨테이너를 위에서 아래로(수식 탭 796-2 포함)
* ① 기본정보(정본은 읽기 전용 · 비고만 고침) ② 수식(796-2) ③ 단가산출 상세(공용 부품 `_LogicLab_Ho.ts` — 하위 박스 · 계산식 · 박스 옵션 · 그 자리 고침)
* 시트 · 인력/재료/경비 · 시험 계산 · 텍스트 수식 보기는 799-2 에 걷음 — 셈은 화면 칸 없이 `_Logic_Drive.ts` 구동
* 그 자리 고침(722-2) = 시험 값만(`_LogicLab_Cover.ts` trial — 줄 사본으로 셈 · 정본 · 요소 · 캐시 안 바뀜)
* ========================================================================== */
import { el } from "@ui/ui_template_elements";
import type { LogicRow } from "./M01_MasterData_UI_Logic_Api";
import { buildEditor } from "./M01_MasterData_UI_Logic_Edit";
import { openPicker } from "./M01_MasterData_UI_Logic_Pick";
import { safeReason, tx } from "./M01_MasterData_UI_Logic_Text";
import { buildHo, lineOpener, type HoContext, type HoEdit } from "./M01_MasterData_UI_LogicLab_Ho";
import { trial } from "./M01_MasterData_UI_LogicLab_Cover";
import { tl } from "./M01_MasterData_UI_LogicLab_Text";
import "./M01_MasterData_UI_LogicLab_Style.css";
import "./M01_MasterData_UI_LogicLab_Pill.css";
import { block } from "./M01_MasterData_UI_LogicLab_View";
/** 원문 절 머리 개정 연도 글 — [2008, 2020, 2025] → 「'08 · '20 · '25」(없으면 빈 글) */
const revisionText = (row: LogicRow): string =>
(row.개정 ?? []).map((y) => `'${String(y).slice(-2)}`).join(" · ");
export { trial };
export interface DetailContext extends HoContext {
file: string;
reasons: string[];
/** 자체 로직 편집기 자리 — 다시 그려도 같은 칸을 씀(고치던 값이 안 날아감) */
ownHost: HTMLElement;
onChange: () => void;
/** 시스템 관리자 — 기본 로직 안내 글이 다름(시트에서 고침 / 보기만) */
admin?: boolean;
/** 722-2 시험 고침 뒤 — 구동을 고친 줄로 다시 시작(있으면 그 자리 고침 켜짐 · 셈은 `rerun`) */
onTrial?: () => void;
/** 717-2b M02 단가산출 — 단가산출 상세 덩이를 이 칸으로(SK 셈 답 · 고침 표) */
cover?: { ho: HTMLElement; open?: ReturnType<typeof lineOpener> };
/** 796-2 수식 탭(`_LogicLab_Equation`) 그릇 — 있으면 「수식」 덩이(M02 는 795-1 사본 꼴 뒤에 이음) */
equation?: HTMLElement;
}
const section = (title: string, body: HTMLElement[], hint = ""): HTMLElement =>
el("section", {
className: "m01-logic__section m01lab__box",
children: [
el("div", {
className: "m01-logic__section-head",
children: [
el("h3", { text: title }),
...(hint ? [el("span", { className: "ui-text ui-text--muted", text: hint })] : []),
],
}),
...body,
],
});
function infoBox(ctx: DetailContext): HTMLElement[] {
const row = ctx.row;
const own = isOwnLogic(row);
const field = (label: string, key: "이름" | "원문번호" | "결과단위" | "출처"): HTMLElement => {
if (!own) return cell(label, row[key] ?? "");
const box = el("input", {
className: "ui-input ui-input--sm m01-logic__input",
attrs: { "data-own": key },
});
box.value = row[key] ?? "";
box.addEventListener("input", () => {
row[key] = box.value;
ctx.onChange();
});
return el("label", {
className: "m01lab__cell",
children: [el("span", { className: "ui-text ui-text--muted", text: label }), box],
});
};
const cell = (label: string, value: string): HTMLElement =>
el("div", {
className: "m01lab__cell",
children: [
el("span", { className: "ui-text ui-text--muted", text: label }),
el("span", { text: value || "—" }),
],
});
const note = el("textarea", {
className: "ui-input ui-input--sm m01-logic__input m01-logic__note",
});
note.value = row.비고 ?? "";
note.addEventListener("input", () => {
if (note.value.trim()) row.비고 = note.value;
else delete row.비고;
ctx.onChange();
});
const reasons = ctx.reasons.length
? [
el("div", {
className: "ui-banner ui-banner--danger m01-logic__reasons",
children: [
el("strong", { text: tx("Head_Blocked") }),
...ctx.reasons.map((r) => el("div", { text: safeReason(r) })),
],
}),
]
: [];
const grid = el("div", {
className: "m01lab__info",
children: [
cell(tx("Head_File"), ctx.file),
cell(tx("Head_Key"), row.키),
field(tx("Head_Number"), "원문번호"),
field(tx("Head_Name"), "이름"),
field(tx("Head_Unit"), "결과단위"),
field(tx("Head_Source"), "출처"),
cell(tx("Head_Revision"), revisionText(row)),
cell(tx("Head_Owner"), row.소유 ?? ""),
el("label", {
className: "m01lab__cell m01lab__cell--wide",
children: [
el("span", { className: "ui-text ui-text--muted", text: tx("Head_Note") }),
note,
],
}),
],
});
// 늘 맨 위 접힌 한 줄 — 펼침은 이 로직 안에서 기억(다시 그려도) · 막힘 까닭은 접어도 보임
const head = [row.키, row.원문번호, row.이름, row.결과단위].filter(Boolean).join(" · ");
const fold = el("details", {
className: "m01lab__box m01lab__infofold",
attrs: ctx.ownHost.dataset.info ? { open: "" } : {},
children: [
el("summary", {
children: [
el("strong", { text: tl("Info_Title") }),
el("span", { className: "ui-text ui-text--muted", text: head }),
],
}),
el("p", {
className: "ui-text ui-text--muted",
text: tl(own ? "Info_Own" : ctx.admin ? "Info_Base" : "Info_ReadOnly"),
}),
grid,
],
});
fold.addEventListener("toggle", () => (ctx.ownHost.dataset.info = fold.open ? "1" : ""));
return [fold, ...reasons];
}
/** 자체 로직(키 GX…) — 정본은 아님 */
export const isOwnLogic = (row: LogicRow): boolean => row.키.startsWith("GX");
/** 자체 로직은 정본 화면과 같은 편집기를 호표 아래에 폄(머리 칸은 기본정보에서 고치므로 숨김) */
function ownEditor(ctx: DetailContext): HTMLElement {
if (!ctx.ownHost.childElementCount)
buildEditor(ctx.ownHost, {
row: ctx.row,
file: ctx.file,
isNew: false,
files: [],
reasons: [],
prices: ctx.prices,
lines: ctx.lines,
onChange: ctx.onChange,
onFile: () => undefined,
onPick: openPicker,
});
return el("details", {
className: "m01lab__edit",
attrs: { open: "" },
children: [
el("summary", { text: tl("Edit_Title") }),
el("p", { className: "ui-text ui-text--muted", text: tl("Edit_SheetHint") }),
ctx.ownHost,
],
});
}
/** M01 그 자리 고침 고리 — 시험 값(줄 객체 열쇠) · 고치면 시험 계산을 다시 그리고 셈 */
function trialEdit(ctx: DetailContext): HoEdit | undefined {
const redraw = ctx.onTrial;
if (!redraw) return undefined;
const covers = trial.of(ctx.row);
return {
covers,
scope: "m01",
set: (idx, cover) => {
if (cover) covers[idx] = cover;
else delete covers[idx];
redraw();
ctx.rerun?.();
},
};
}
function hoBox(ctx: DetailContext): HTMLElement {
const money = !("결과" in ctx.row) && (ctx.row.결과단위 ?? "").startsWith("원");
if (!money) {
return block(
section(tl("Ho_Title"), [
el("p", { className: "ui-text ui-text--muted", text: tl("Not_Money") }),
el("pre", { className: "m01lab__expr", text: ctx.row.결과?.식 ?? "" }),
]),
"ho",
);
}
const body = buildHo({ ...ctx, edit: trialEdit(ctx) });
const totalLine = body.pop()!; // 계 줄 — 자체 로직은 접힌 표 밖에 둠
// 자체 로직 — 고치는 자리는 편집기 하나 · 읽기용 박스는 접어 둠(펼치면 소계가 보임)
const fold = el("details", {
className: "m01lab__fold",
attrs: ctx.ownHost.dataset.fold ? { open: "" } : {},
children: [el("summary", { text: tl("Ho_Fold") }), ...body],
});
fold.addEventListener("toggle", () => (ctx.ownHost.dataset.fold = fold.open ? "1" : ""));
const blocks = isOwnLogic(ctx.row) ? [ownEditor(ctx), fold, totalLine] : [...body, totalLine];
const calculated = ctx.lines !== null;
const hint = calculated ? tl("Click_Hint") : `${tl("Click_Hint")} · ${tl("Calc_First")}`;
return block(section(tl("Ho_Title"), blocks, hint), "ho");
}
/** 상세 화면(기본정보 · 수식 · 단가산출 상세)을 `host` 에 쌓음 */
export function buildLabDetail(host: HTMLElement, ctx: DetailContext): void {
// 다시 그리면 포커스가 빠짐 — 고치던 칸(편집기 · 기본정보)을 기억해 되돌림
const active = document.activeElement;
const kept =
active instanceof HTMLInputElement || active instanceof HTMLTextAreaElement
? { node: active, from: active.selectionStart, to: active.selectionEnd }
: null;
// 보기 고르기(`_View.ts`) — 덩이마다 이름표 · 고른 덩이만 보임
host.replaceChildren(
...infoBox(ctx),
...(ctx.equation ? [block(ctx.equation, "equation")] : []),
ctx.cover ? block(section(tl("Ho_Title"), [ctx.cover.ho]), "ho") : hoBox(ctx),
);
const own = kept?.node.dataset.own; // 기본정보 칸은 새로 만들어지므로 이름표로 다시 찾음
const back = own ? host.querySelector<HTMLInputElement>(`[data-own="${own}"]`) : kept?.node;
if (kept && back && (own || ctx.ownHost.contains(back))) {
back.focus();
if (kept.from !== null) back.setSelectionRange(kept.from, kept.to);
}
}