Merge remote-tracking branch 'origin/sub_laptop_1' into main_laptop_1
This commit is contained in:
@@ -0,0 +1,203 @@
|
||||
/* 로직 만들기 판 — 큰 창 · 시작 화면 · 위 띠 · 세 칸 */
|
||||
|
||||
/* 공용 모달(위 고정)을 화면 대부분으로 — 높이 고정 · 칸마다 따로 굴림 */
|
||||
.ui-modal.m01-lb {
|
||||
--modal-max: 92vh;
|
||||
width: min(1680px, 98vw);
|
||||
height: var(--modal-max);
|
||||
font-size: var(--text-body);
|
||||
}
|
||||
|
||||
.m01-lb .ui-modal__body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
gap: var(--spacing-12);
|
||||
}
|
||||
|
||||
.m01-lb__muted {
|
||||
margin: 0;
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--text-body-sm);
|
||||
}
|
||||
|
||||
.m01-lb__note {
|
||||
margin: 0;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--text-body-sm);
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.m01-lb__note:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.m01-lb__big {
|
||||
min-height: 56px;
|
||||
padding: 0 var(--spacing-24);
|
||||
font-size: var(--text-body);
|
||||
}
|
||||
|
||||
.m01-lb .is-pending {
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
/* 시작 화면 */
|
||||
.m01-lb__start {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
|
||||
gap: var(--spacing-24);
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.m01-lb__start-col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-8);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.m01-lb__start-col h4 {
|
||||
margin: var(--spacing-8) 0 0;
|
||||
}
|
||||
|
||||
.m01-lb__list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-4);
|
||||
}
|
||||
|
||||
.m01-lb__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-8);
|
||||
width: 100%;
|
||||
padding: var(--spacing-8) var(--spacing-12);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.m01-lb__item:hover {
|
||||
border-color: var(--color-accent);
|
||||
}
|
||||
|
||||
.m01-lb__item-name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.m01-lb__item-unit {
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--text-body-sm);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 위 띠 */
|
||||
.m01-lb__strip {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-end;
|
||||
gap: var(--spacing-12);
|
||||
padding-bottom: var(--spacing-12);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.m01-lb__name {
|
||||
flex: 1 1 280px;
|
||||
}
|
||||
|
||||
.m01-lb__applied {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--text-body-sm);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.m01-lb__actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-8);
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* 세 칸 — 원문 · 산출근거 · 결과 */
|
||||
.m01-lb__cols {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1.2fr) minmax(0, 1fr);
|
||||
gap: var(--spacing-12);
|
||||
}
|
||||
|
||||
.m01-lb__col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
|
||||
.m01-lb__col-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-8);
|
||||
padding: var(--spacing-8) var(--spacing-12);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.m01-lb__col-head h4 {
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.m01-lb__col-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-8);
|
||||
padding: var(--spacing-12);
|
||||
}
|
||||
|
||||
.m01-lb__col-body h5 {
|
||||
margin: var(--spacing-8) 0 0;
|
||||
}
|
||||
|
||||
.m01-lb__total {
|
||||
font-size: var(--text-subheading);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.m01-lb__examples {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-8);
|
||||
padding: var(--spacing-8);
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--color-surface);
|
||||
}
|
||||
|
||||
.m01-lb__example-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-8);
|
||||
}
|
||||
|
||||
.m01-lb__example {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-4);
|
||||
font-size: var(--text-body-sm);
|
||||
}
|
||||
|
||||
/* 좁은 화면 — 세 칸을 위아래로 */
|
||||
@media (max-width: 1100px) {
|
||||
.m01-lb__cols {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,505 @@
|
||||
/* =============================================================================
|
||||
* M01_LB_Entry.ts
|
||||
* 로직 만들기 판(시험) — 들어가는 단추 · 큰 창 · 시작 화면 · 세 칸 틀(원문 · 산출근거 · 결과) · 위 띠
|
||||
* 모양은 `docs/raw/M01_쉬운단가/로직판_A_정함.md` · 이 폴더째 지워도 됨(폴더 밖 참조는 LogicLab.ts 두 줄).
|
||||
* 1걸음 = 틀 — 원문 · 산출근거 칸은 자리만 · 결과 칸은 EasyCalc 계산 · 일위대가표로 바로 보임.
|
||||
* 2걸음에 Blocks · Api · Source · Steps · Grid · Wizard 를 이음.
|
||||
* ========================================================================== */
|
||||
|
||||
import { currentLanguageIndex } from "@ui/ui_template_locale";
|
||||
import {
|
||||
createButton,
|
||||
createInputField,
|
||||
createTag,
|
||||
el,
|
||||
showConfirmDialog,
|
||||
showToast,
|
||||
} from "@ui/ui_template_elements";
|
||||
import { openModal } from "@ui/ui_template_modal";
|
||||
import type { CalcAnswer, LogicInput, LogicRow } from "../M01_MasterData_UI_Logic_Api";
|
||||
import { formatMoney } from "../M01_MasterData_UI_Logic_Money";
|
||||
import { formatNumber } from "../M01_MasterData_UI_Logic_Edit";
|
||||
import {
|
||||
calcLater,
|
||||
copyEasy,
|
||||
listEasy,
|
||||
openEasy,
|
||||
plainError,
|
||||
type EasyListItem,
|
||||
} from "../EasyCalc/M01_EasyCalc_Api";
|
||||
import { renderSheet } from "../EasyCalc/M01_EasyCalc_Sheet";
|
||||
import { lbCalcLater, type LbTables } from "./M01_LB_Api";
|
||||
import { readLogic, type LbLogic } from "./M01_LB_Blocks";
|
||||
import "./M01_LB_Entry.css";
|
||||
|
||||
const TEXT = {
|
||||
Open: ["로직 만들기(시험)", "Logic builder (trial)"],
|
||||
Title: ["로직 만들기", "Logic builder"],
|
||||
CloseWindow: ["창 닫기", "Close window"],
|
||||
Close: ["닫기", "Close"],
|
||||
Loading: ["불러오는 중", "Loading"],
|
||||
LoadFailed: ["목록을 못 불러옴 — {v}", "Could not load the list — {v}"],
|
||||
Start_Trial: ["먼저 볼 시험 로직", "Trial logics to start with"],
|
||||
Start_Search: ["이름으로 찾기 — 예: 돌쌓기", "Search by name — e.g. stone wall"],
|
||||
Start_Found: ["찾은 것", "Found"],
|
||||
Start_Count: ["{n} 개 중 {m} 개 보임 — 더 좁혀 찾기", "Showing {m} of {n} — narrow the search"],
|
||||
Start_None: ["찾은 것 없음", "Nothing found"],
|
||||
Start_Field: ["현장 일위대가", "Field unit prices"],
|
||||
Start_FieldNone: ["아직 없음", "None yet"],
|
||||
Start_Wizard: ["단계 안내로 새로 만들기", "Create step by step"],
|
||||
Standard: ["표준", "Standard"],
|
||||
Field: ["현장", "Field"],
|
||||
Per: ["{u} 당", "per {u}"],
|
||||
Name: ["이름", "Name"],
|
||||
Applied: ["적용 품셈", "Applied standard"],
|
||||
Undo: ["실행 취소", "Undo"],
|
||||
Save: ["저장", "Save"],
|
||||
CopyEdit: ["복사 편집", "Copy and edit"],
|
||||
Copying: ["현장 일위대가로 복사하는 중", "Copying to a field unit price"],
|
||||
Copied: ["현장 일위대가로 복사했음 — 이제 고칠 수 있음", "Copied — you can edit it now"],
|
||||
Unsaved: ["저장 안 함", "Not saved"],
|
||||
StandardNote: [
|
||||
"표준은 읽기만 — 고치려면 「복사 편집」으로 현장 일위대가를 만듦",
|
||||
"Standard items are read-only — use “Copy and edit” to make a field copy",
|
||||
],
|
||||
Col_Source: ["원문", "Source"],
|
||||
Col_Basis: ["산출근거", "Basis"],
|
||||
Col_Result: ["결과", "Result"],
|
||||
View_Sentence: ["문장", "Sentences"],
|
||||
View_Grid: ["격자", "Grid"],
|
||||
Money: ["{u} 만드는 데 {v} 원", "{v} won to make {u}"],
|
||||
Value: ["결과 {v} {u}", "Result {v} {u}"],
|
||||
Examples: ["예시값", "Example values"],
|
||||
Examples_Note: [
|
||||
"예시값은 계산을 보려는 값 — 저장 안 됨",
|
||||
"Example values are only for trying — not saved",
|
||||
],
|
||||
Pick: ["선택", "Choose"],
|
||||
Sheet: ["일위대가표", "Unit-cost sheet"],
|
||||
NotYet: ["준비 중", "Coming soon"],
|
||||
Leave_Ask: [
|
||||
"저장 안 한 것이 있음 — 저장하지 않고 닫을까요?",
|
||||
"There are unsaved changes — close without saving?",
|
||||
],
|
||||
Leave_Ok: ["저장 안 하고 닫기", "Close without saving"],
|
||||
} as const;
|
||||
|
||||
type TextKey = keyof typeof TEXT;
|
||||
|
||||
const te = (key: TextKey, vars: Record<string, string | number> = {}): string =>
|
||||
Object.entries(vars).reduce(
|
||||
(text, [k, v]) => text.replace(`{${k}}`, String(v)),
|
||||
(TEXT[key][currentLanguageIndex as 0 | 1] ?? TEXT[key][0]) as string,
|
||||
);
|
||||
|
||||
/** 시작 화면에 먼저 보이는 시험 로직 여섯(정함 10) */
|
||||
const TRIAL_KEYS = ["GF000208", "GF000219", "GF000025", "GF000249", "GF000133", "GC000051"];
|
||||
/** 찾은 것 목록에 한 번에 보이는 줄 수 */
|
||||
const SHOWN = 50;
|
||||
|
||||
/** 「원/㎡」 → 「㎡」 */
|
||||
const unitOf = (resultUnit: string): string => resultUnit.replace(/^원\//, "");
|
||||
/** 「㎡」 → 「1㎡」 · 「100㎡」 은 그대로 */
|
||||
const amountOf = (resultUnit: string): string => {
|
||||
const unit = unitOf(resultUnit);
|
||||
return /^\d/.test(unit) ? unit : `1${unit}`;
|
||||
};
|
||||
const isMoney = (resultUnit: string): boolean => resultUnit.startsWith("원/");
|
||||
|
||||
/** 고치는 화면이 쥔 것 — row = 저장할 로직 줄 · lb = 블록(못 읽으면 null) · tables = 고친 표 사본(저장 안 함) */
|
||||
interface Paper {
|
||||
key: string;
|
||||
version: string;
|
||||
isBase: boolean;
|
||||
row: LogicRow;
|
||||
trial: Record<string, unknown>;
|
||||
lb: LbLogic | null;
|
||||
tables: LbTables;
|
||||
}
|
||||
|
||||
/* ponytail: Blocks(sub7) · LB Api(sub4) 속이 「준비 중」 인 동안만 대신함 — 채워지면 두 catch 를 지움 */
|
||||
const blocksOf = (row: LogicRow, key: string, version: string, isBase: boolean): LbLogic | null => {
|
||||
try {
|
||||
return readLogic(row, { key, version, isBase });
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
const calcWith = (
|
||||
p: Paper,
|
||||
onDone: (ans: CalcAnswer | null, plain: string | null) => void,
|
||||
): void => {
|
||||
try {
|
||||
lbCalcLater(p.row, p.trial, p.tables, onDone);
|
||||
} catch {
|
||||
calcLater(p.row, p.trial, onDone);
|
||||
}
|
||||
};
|
||||
|
||||
/** 「쉬운 단가산출(시험)」 옆에 놓는 단추 — 누르면 큰 창 */
|
||||
export function createLogicBuilderButton(): HTMLButtonElement {
|
||||
return createButton({ label: te("Open"), variant: "filled", onClick: openLogicBuilder });
|
||||
}
|
||||
|
||||
function openLogicBuilder(): void {
|
||||
let body: HTMLElement;
|
||||
let paper: Paper | null = null;
|
||||
let dirty = false;
|
||||
let logics: EasyListItem[] | null = null;
|
||||
let query = "";
|
||||
let view: "sentence" | "grid" = "sentence";
|
||||
|
||||
const mayLeave = async (): Promise<boolean> =>
|
||||
!paper || !dirty || showConfirmDialog(te("Leave_Ask"), te("Leave_Ok"));
|
||||
|
||||
const notYet = (): void => showToast(te("NotYet"), "info");
|
||||
|
||||
/* ---------- 시작 화면 ---------- */
|
||||
|
||||
const listRow = (item: EasyListItem): HTMLElement => {
|
||||
const row = el("button", {
|
||||
className: "m01-lb__item",
|
||||
attrs: { type: "button", title: item.where },
|
||||
children: [
|
||||
el("span", { className: "m01-lb__item-name", text: item.name }),
|
||||
createTag(item.isBase ? te("Standard") : te("Field"), item.isBase ? "neutral" : "accent"),
|
||||
el("span", {
|
||||
className: "m01-lb__item-unit",
|
||||
text: isMoney(item.resultUnit)
|
||||
? te("Per", { u: amountOf(item.resultUnit) })
|
||||
: item.resultUnit,
|
||||
}),
|
||||
],
|
||||
});
|
||||
row.addEventListener("click", () => void openPaper(item.key));
|
||||
return row;
|
||||
};
|
||||
|
||||
const showStart = async (): Promise<void> => {
|
||||
paper = null;
|
||||
dirty = false;
|
||||
const status = el("p", { className: "m01-lb__muted", text: te("Loading") });
|
||||
const trialHost = el("div", { className: "m01-lb__list" });
|
||||
const fieldHost = el("div", { className: "m01-lb__list" });
|
||||
const foundHost = el("div", { className: "m01-lb__list" });
|
||||
const search = createInputField({
|
||||
type: "search",
|
||||
placeholder: te("Start_Search"),
|
||||
value: query,
|
||||
onInput: (v) => {
|
||||
query = v;
|
||||
drawFound();
|
||||
},
|
||||
});
|
||||
|
||||
const drawFound = (): void => {
|
||||
if (!logics) return;
|
||||
const words = query.trim().toLowerCase().split(/\s+/).filter(Boolean);
|
||||
const hits = words.length
|
||||
? logics.filter((x) => words.every((w) => x.name.toLowerCase().includes(w)))
|
||||
: [];
|
||||
status.textContent = !words.length
|
||||
? ""
|
||||
: !hits.length
|
||||
? te("Start_None")
|
||||
: hits.length > SHOWN
|
||||
? te("Start_Count", { n: hits.length.toLocaleString(), m: SHOWN })
|
||||
: "";
|
||||
foundHost.replaceChildren(...hits.slice(0, SHOWN).map(listRow));
|
||||
};
|
||||
|
||||
body.replaceChildren(
|
||||
el("div", {
|
||||
className: "m01-lb__start",
|
||||
children: [
|
||||
el("div", {
|
||||
className: "m01-lb__start-col",
|
||||
children: [
|
||||
el("h4", { text: te("Start_Trial") }),
|
||||
trialHost,
|
||||
search.root,
|
||||
el("h4", { text: te("Start_Found") }),
|
||||
status,
|
||||
foundHost,
|
||||
],
|
||||
}),
|
||||
el("div", {
|
||||
className: "m01-lb__start-col",
|
||||
children: [
|
||||
bigButton(te("Start_Wizard"), notYet),
|
||||
el("h4", { text: te("Start_Field") }),
|
||||
fieldHost,
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
);
|
||||
search.input.focus();
|
||||
|
||||
try {
|
||||
logics ??= await listEasy("");
|
||||
} catch (error) {
|
||||
status.textContent = te("LoadFailed", { v: plainError(error) });
|
||||
return;
|
||||
}
|
||||
const byKey = new Map(logics.map((x) => [x.key, x]));
|
||||
trialHost.replaceChildren(
|
||||
...TRIAL_KEYS.flatMap((k) => {
|
||||
const item = byKey.get(k);
|
||||
return item ? [listRow(item)] : [];
|
||||
}),
|
||||
);
|
||||
const field = logics.filter((x) => !x.isBase);
|
||||
fieldHost.replaceChildren(
|
||||
...(field.length
|
||||
? field.map(listRow)
|
||||
: [el("p", { className: "m01-lb__muted", text: te("Start_FieldNone") })]),
|
||||
);
|
||||
drawFound();
|
||||
};
|
||||
|
||||
/* ---------- 세 칸 화면 ---------- */
|
||||
|
||||
const openPaper = async (key: string): Promise<void> => {
|
||||
body.replaceChildren(el("p", { className: "m01-lb__muted", text: te("Loading") }));
|
||||
try {
|
||||
const opened = await openEasy(key);
|
||||
const lb = blocksOf(opened.row, key, opened.version, opened.isBase);
|
||||
paper = { key, ...opened, lb, tables: {} };
|
||||
dirty = false;
|
||||
showPaper();
|
||||
} catch (error) {
|
||||
showToast(plainError(error), "error");
|
||||
void showStart();
|
||||
}
|
||||
};
|
||||
|
||||
const showPaper = (): void => {
|
||||
const p = paper;
|
||||
if (!p) return;
|
||||
let ans: CalcAnswer | null = null;
|
||||
let busy = false;
|
||||
|
||||
const unsaved = createTag(te("Unsaved"), "warning");
|
||||
unsaved.hidden = !dirty;
|
||||
const total = el("div", { className: "m01-lb__total" });
|
||||
const note = el("p", { className: "m01-lb__note", attrs: { role: "status" } });
|
||||
const sheetHost = el("div");
|
||||
const examplesHost = el("div", { className: "m01-lb__examples" });
|
||||
const sourceHost = el("div", { className: "m01-lb__slot" });
|
||||
const basisHost = el("div", { className: "m01-lb__slot" });
|
||||
|
||||
const name = createInputField({ label: te("Name"), value: p.row.이름 });
|
||||
name.input.disabled = true; // ponytail: 이름 고치기는 2걸음(Blocks · writeLogic) 에 이음
|
||||
name.root.classList.add("m01-lb__name");
|
||||
|
||||
const drawResult = (pending: boolean): void => {
|
||||
const unit = p.row.결과단위 ?? "";
|
||||
const value = ans?.ok ? (ans.result ?? ans.sums?.["계"]) : undefined;
|
||||
total.textContent = isMoney(unit)
|
||||
? te("Money", { u: amountOf(unit), v: value === undefined ? "—" : formatMoney(value) })
|
||||
: te("Value", { u: unit, v: value === undefined ? "—" : formatNumber(value) });
|
||||
total.classList.toggle("is-pending", pending);
|
||||
sheetHost.classList.toggle("is-pending", pending);
|
||||
renderSheet(sheetHost, { name: p.row.이름, resultUnit: unit }, ans);
|
||||
};
|
||||
|
||||
const calc = (): void => {
|
||||
drawResult(true);
|
||||
calcWith(p, (next, plain) => {
|
||||
ans = next;
|
||||
note.textContent = plain ?? "";
|
||||
drawResult(false);
|
||||
});
|
||||
};
|
||||
|
||||
const doCopy = async (): Promise<void> => {
|
||||
if (busy) return;
|
||||
busy = true;
|
||||
note.textContent = te("Copying");
|
||||
try {
|
||||
const saved = await copyEasy(p.key);
|
||||
paper = {
|
||||
key: saved.key,
|
||||
version: saved.version,
|
||||
isBase: false,
|
||||
row: saved.row,
|
||||
trial: p.trial,
|
||||
lb: blocksOf(saved.row, saved.key, saved.version, false),
|
||||
tables: {},
|
||||
};
|
||||
logics = null;
|
||||
showToast(te("Copied"), "success");
|
||||
showPaper();
|
||||
} catch (error) {
|
||||
note.textContent = plainError(error);
|
||||
} finally {
|
||||
busy = false;
|
||||
}
|
||||
};
|
||||
|
||||
const undo = createButton({ label: te("Undo"), variant: "ghost", onClick: notYet });
|
||||
undo.disabled = true;
|
||||
const actions = el("div", {
|
||||
className: "m01-lb__actions",
|
||||
children: [
|
||||
...(p.isBase
|
||||
? [createButton({ label: te("CopyEdit"), onClick: () => void doCopy() })]
|
||||
: [undo, createButton({ label: te("Save"), onClick: notYet })]),
|
||||
createButton({
|
||||
label: te("Close"),
|
||||
variant: "ghost",
|
||||
onClick: () =>
|
||||
void (async () => {
|
||||
if (await mayLeave()) void showStart();
|
||||
})(),
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const viewButton = (which: "sentence" | "grid", label: string): HTMLButtonElement => {
|
||||
const b = createButton({
|
||||
label,
|
||||
variant: view === which ? "filled" : "ghost",
|
||||
size: "sm",
|
||||
onClick: () => {
|
||||
view = which;
|
||||
showPaper();
|
||||
},
|
||||
});
|
||||
b.setAttribute("aria-pressed", String(view === which));
|
||||
return b;
|
||||
};
|
||||
|
||||
sourceHost.replaceChildren(el("p", { className: "m01-lb__muted", text: te("NotYet") }));
|
||||
basisHost.replaceChildren(el("p", { className: "m01-lb__muted", text: te("NotYet") }));
|
||||
drawExamples(examplesHost, p.row.입력 ?? [], p.trial, (next) => {
|
||||
p.trial = next;
|
||||
calc();
|
||||
});
|
||||
|
||||
body.replaceChildren(
|
||||
el("div", {
|
||||
className: "m01-lb__strip",
|
||||
children: [
|
||||
name.root,
|
||||
createTag(p.isBase ? te("Standard") : te("Field"), p.isBase ? "neutral" : "accent"),
|
||||
el("span", {
|
||||
className: "m01-lb__applied",
|
||||
text: `${te("Applied")} · ${p.row.출처 || "—"}`,
|
||||
}),
|
||||
unsaved,
|
||||
actions,
|
||||
],
|
||||
}),
|
||||
note,
|
||||
...(p.isBase ? [el("p", { className: "m01-lb__muted", text: te("StandardNote") })] : []),
|
||||
el("div", {
|
||||
className: "m01-lb__cols",
|
||||
children: [
|
||||
column(te("Col_Source"), [sourceHost]),
|
||||
column(
|
||||
te("Col_Basis"),
|
||||
[basisHost],
|
||||
[viewButton("sentence", te("View_Sentence")), viewButton("grid", te("View_Grid"))],
|
||||
),
|
||||
column(te("Col_Result"), [
|
||||
total,
|
||||
examplesHost,
|
||||
el("h5", { text: te("Sheet") }),
|
||||
sheetHost,
|
||||
]),
|
||||
],
|
||||
}),
|
||||
);
|
||||
calc();
|
||||
};
|
||||
|
||||
// 화면 함수들을 다 만든 뒤에 엶 — mount 가 바로 showStart 를 부름
|
||||
openModal({
|
||||
title: te("Title"),
|
||||
closeLabel: te("CloseWindow"),
|
||||
dialogClass: "m01-lb",
|
||||
beforeClose: mayLeave,
|
||||
mount: (host) => {
|
||||
body = host;
|
||||
void showStart();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** 세 칸 가운데 하나 — 머리(제목 · 단추) + 몸 */
|
||||
function column(title: string, children: HTMLElement[], tools: HTMLElement[] = []): HTMLElement {
|
||||
return el("section", {
|
||||
className: "m01-lb__col",
|
||||
children: [
|
||||
el("div", {
|
||||
className: "m01-lb__col-head",
|
||||
children: [el("h4", { text: title }), ...tools],
|
||||
}),
|
||||
el("div", { className: "m01-lb__col-body", children }),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
/** 예시값 띠 — 입력값마다 선택 또는 숫자 · 바꾸면 onChange(새 예시값) · 저장 안 함 */
|
||||
function drawExamples(
|
||||
host: HTMLElement,
|
||||
inputs: LogicInput[],
|
||||
trial: Record<string, unknown>,
|
||||
onChange: (trial: Record<string, unknown>) => void,
|
||||
): void {
|
||||
host.hidden = !inputs.length;
|
||||
if (!inputs.length) return void host.replaceChildren();
|
||||
const state = { ...trial };
|
||||
const field = (spec: LogicInput): HTMLElement => {
|
||||
const label = spec.단위 ? `${spec.이름} (${spec.단위})` : spec.이름;
|
||||
let box: HTMLInputElement | HTMLSelectElement;
|
||||
if (spec.고르기?.length) {
|
||||
box = el("select", {
|
||||
className: "ui-select ui-select--sm",
|
||||
children: [
|
||||
el("option", { attrs: { value: "" }, text: te("Pick") }),
|
||||
...spec.고르기.map((v) => el("option", { attrs: { value: String(v) }, text: String(v) })),
|
||||
],
|
||||
});
|
||||
box.value = state[spec.이름] === undefined ? "" : String(state[spec.이름]);
|
||||
box.addEventListener("change", () => {
|
||||
const picked = spec.고르기?.find((v) => String(v) === box.value);
|
||||
if (picked === undefined) delete state[spec.이름];
|
||||
else state[spec.이름] = picked;
|
||||
onChange({ ...state });
|
||||
});
|
||||
} else {
|
||||
box = el("input", {
|
||||
className: "ui-input ui-input--sm",
|
||||
attrs: {
|
||||
type: "number",
|
||||
...(spec.범위 ? { min: String(spec.범위[0]), max: String(spec.범위[1]) } : {}),
|
||||
},
|
||||
});
|
||||
box.value = state[spec.이름] == null ? "" : String(state[spec.이름]);
|
||||
box.addEventListener("change", () => {
|
||||
if (box.value === "") delete state[spec.이름];
|
||||
else state[spec.이름] = Number(box.value);
|
||||
onChange({ ...state });
|
||||
});
|
||||
}
|
||||
box.setAttribute("aria-label", spec.이름);
|
||||
return el("label", {
|
||||
className: "m01-lb__example",
|
||||
children: [el("span", { text: label }), box],
|
||||
});
|
||||
};
|
||||
host.replaceChildren(
|
||||
el("strong", { text: te("Examples") }),
|
||||
el("div", { className: "m01-lb__example-list", children: inputs.map(field) }),
|
||||
el("p", { className: "m01-lb__muted", text: te("Examples_Note") }),
|
||||
);
|
||||
}
|
||||
|
||||
function bigButton(label: string, onClick: () => void): HTMLButtonElement {
|
||||
const button = createButton({ label, variant: "filled", onClick });
|
||||
button.classList.add("m01-lb__big");
|
||||
return button;
|
||||
}
|
||||
@@ -41,6 +41,8 @@ import { buildNewLogic } from "./M01_MasterData_UI_LogicLab_New";
|
||||
import { tn } from "./M01_MasterData_UI_LogicLab_New_Text";
|
||||
// 쉬운 단가산출 시험 길 — 폴더째 지우면 이 두 줄도 지움
|
||||
import { createEasyCalcButton } from "./EasyCalc/M01_EasyCalc_Entry";
|
||||
// 로직 만들기 시험 길 — 폴더째 지우면 이 두 줄도 지움
|
||||
import { createLogicBuilderButton } from "./LogicBuilder/M01_LB_Entry";
|
||||
import "./M01_MasterData_UI_Logic_Style.css";
|
||||
|
||||
/** 저장 안 한 로직 하나 — origKey null = 새 로직 · row null = 지움 */
|
||||
@@ -402,6 +404,8 @@ export async function mountM01LogicLab(
|
||||
createButton({ label: tn("Open"), variant: "filled", onClick: startNew }),
|
||||
// 쉬운 단가산출 시험 길 — 폴더째 지우면 이 두 줄도 지움
|
||||
createEasyCalcButton(),
|
||||
// 로직 만들기 시험 길 — 폴더째 지우면 이 두 줄도 지움
|
||||
createLogicBuilderButton(),
|
||||
createButton({ label: tx("Bar_Delete"), variant: "danger", onClick: () => void onDelete() }),
|
||||
discardButton,
|
||||
saveButton,
|
||||
|
||||
Reference in New Issue
Block a user