- 판정 = 자체 로직 편집 · 시험 값 고침 · M02 줄 덮음 · 비목 박스 소계 모두 수식 보기로 됨(살릴 것 없음) - M01 상세: 숨은 단가산출 상세 덩이 · 자체 로직 시트 편집기 · 시험 고침 · 셈 구동(createDrive) · 보기 이름표(View.ts) · data-view CSS 걷음 - M02 불러오기 모달 머리 [항목 추가] · [율 줄 추가] 단추와 그 창(Rate.ts · openSangeunLinePicker) 걷음 - 글 정리 = Info_Own 새 뜻 · 죽은 글 키 · No_Key · 항목 추가 글 걷음 - 낡은 시험 고침 = 722(시험 고침 · 덮기 셈) · 799-2 · 804-2 · 804-6 · 804-7 · 806-1 · 717-2b · 717-4 · 721 · 728-2 — 걷은 것이 없음 확인으로 - 남김 = M02 수식 문서 없는 줄의 단가산출 상세 몸(cover · buildHo) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
163 lines
6.5 KiB
TypeScript
163 lines
6.5 KiB
TypeScript
/* =============================================================================
|
|
* M01_MasterData_UI_LogicLab_Detail.ts
|
|
* 「단가산출 로직」 상세 화면 — 컨테이너를 위에서 아래로
|
|
* ① 기본정보(정본은 읽기 전용 · 비고만 고침 · 자체 로직은 이름 · 단위 · 출처도) ② 수식(줄 쌓기 804-2 · 804-6)
|
|
* ③ M02 수식 문서 없는 줄(일위대가 줄 모달 등)만 단가산출 상세 몸(`cover` — 공용 부품 `_LogicLab_Ho.ts`)
|
|
* 단가산출 상세 보기 · 자체 로직 시트 편집기 · 시험 고침은 840-1 에 걷음 — 자체 로직도 수식(줄 쌓기)에서 만들고 고침
|
|
* ========================================================================== */
|
|
|
|
import { el } from "@ui/ui_template_elements";
|
|
import type { LogicRow } from "./M01_MasterData_UI_Logic_Api";
|
|
import { safeReason, tx } from "./M01_MasterData_UI_Logic_Text";
|
|
import { tl } from "./M01_MasterData_UI_LogicLab_Text";
|
|
import "./M01_MasterData_UI_LogicLab_Style.css";
|
|
import "./M01_MasterData_UI_LogicLab_Pill.css";
|
|
|
|
/** 원문 절 머리 개정 연도 글 — [2008, 2020, 2025] → 「'08 · '20 · '25」(없으면 빈 글) */
|
|
const revisionText = (row: LogicRow): string =>
|
|
(row.개정 ?? []).map((y) => `'${String(y).slice(-2)}`).join(" · ");
|
|
|
|
export interface DetailContext {
|
|
row: LogicRow;
|
|
file: string;
|
|
reasons: string[];
|
|
/** 기본정보 접힘 기억 자리 — 다시 그려도 같은 칸을 씀 */
|
|
ownHost: HTMLElement;
|
|
onChange: () => void;
|
|
/** 시스템 관리자 — 기본 로직 안내 글이 다름(시트에서 고침 / 보기만) */
|
|
admin?: boolean;
|
|
/** 717-2b M02 단가산출 — 수식 문서 없는 줄의 단가산출 상세 덩이(SK 셈 답 · 고침 표) */
|
|
cover?: { ho: HTMLElement };
|
|
/** 수식 탭 = 줄 쌓기 그릇(`A00_Common/line_stack_screen.ts` · 804-2 · 804-6) */
|
|
stack?: 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");
|
|
|
|
/** 상세 화면(기본정보 · 수식 · M02 단가산출 상세 몸)을 `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;
|
|
host.replaceChildren(
|
|
...infoBox(ctx),
|
|
...(ctx.stack ? [ctx.stack] : []),
|
|
...(ctx.cover ? [section(tl("Ho_Title"), [ctx.cover.ho])] : []),
|
|
);
|
|
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);
|
|
}
|
|
}
|