diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab.ts b/M01_MasterData/M01_MasterData_UI_LogicLab.ts index bf8cde20..e718224b 100644 --- a/M01_MasterData/M01_MasterData_UI_LogicLab.ts +++ b/M01_MasterData/M01_MasterData_UI_LogicLab.ts @@ -19,23 +19,20 @@ import { import { ApiError, fetchLogic, - fetchLogicFiles, fetchLogics, fetchLogicSubs, saveFiles, type CalcLine, type ElementBrief, - type LogicFile, type LogicRow, type LogicSummary, type SaveFile, } from "./M01_MasterData_UI_Logic_Api"; import { buildCalc } from "./M01_MasterData_UI_Logic_Calc"; -import { buildEditor } from "./M01_MasterData_UI_Logic_Edit"; import { buildList, logicId, type ListItem, type ListMark } from "./M01_MasterData_UI_Logic_List"; -import { openPicker } from "./M01_MasterData_UI_Logic_Pick"; import type { SideHandle } from "./M01_MasterData_UI_Side"; import { tx } from "./M01_MasterData_UI_Logic_Text"; +import { buildLabDetail } from "./M01_MasterData_UI_LogicLab_Detail"; import "./M01_MasterData_UI_Logic_Style.css"; /** 저장 안 한 로직 하나 — origKey null = 새 로직 · row null = 지움 */ @@ -80,12 +77,11 @@ export async function mountM01LogicLab( openKey?: string, ): Promise { let drafts = loadDrafts(); - let files: LogicFile[] = []; let opened: Opened | null = null; let calcInputs = ""; const editor = el("div", { className: "m01-logic__editor" }); const errors = el("div", { className: "m01-logic__reasons", attrs: { hidden: "" } }); - const calc = el("aside", { className: "m01-logic__calc" }); + const calc = el("div", { className: "m01lab__calc" }); const filterHost = el("div"); const waiting = el("p", { className: "m01-logic__muted", text: tx("Loading") }); let loaded = false; // 첫 목록이 오기 전에는 「0 줄」 빈 표를 보이지 않음 @@ -168,7 +164,6 @@ export async function mountM01LogicLab( list.root.hidden = !listing || !loaded; waiting.hidden = loaded; editor.hidden = back.hidden = listing; - calc.hidden = listing; if (!opened) return; if (!opened.row) { editor.replaceChildren( @@ -177,29 +172,18 @@ export async function mountM01LogicLab( return; } const current = opened; - buildEditor(editor, { + buildLabDetail(editor, { row: current.row as LogicRow, file: current.file, - isNew: current.origKey === null, - files: files.map((f) => f.file), reasons: current.reasons, prices: current.prices, lines: current.lines, + values: current.values, onChange: touch, - onFile: (file) => { - const f = files.find((x) => x.file === file); - if (!f) return; - Object.assign(current, { file: f.file, sub: subOf(f), version: f.version }); - touch(); - }, - onPick: openPicker, + calcHost: calc, }); }; - /** 로직 파일 → 구분(원문 + 부문) — 「공통 03장 토공사」 의 앞말이 부문 */ - const subOf = (f: LogicFile): string => - f.book === "건설품셈" ? `${f.book} ${f.chapter.split(" ")[0]}` : String(f.book); - const show = (next: Opened | null): void => { opened = next; errors.hidden = true; @@ -236,12 +220,7 @@ export async function mountM01LogicLab( let allLogics: LogicSummary[] = []; const reload = async (): Promise => { - const [logics, logicFiles, subs] = await Promise.all([ - fetchLogics(), - fetchLogicFiles(), - fetchLogicSubs(), - ]); - files = logicFiles; + const [logics, subs] = await Promise.all([fetchLogics(), fetchLogicSubs()]); allLogics = logics; list.setItems(logics); // 왼쪽 「전체」 + 구분 → 상세구분 거름 — 고르면 목록으로 돌아와 그 범위만 @@ -263,43 +242,6 @@ export async function mountM01LogicLab( persist(); }; - /** 옛 컨테이너의 「새로 만들기」 — 빈 줄 하나를 초안으로 세움(저장 때 서버가 키를 줌) */ - const onDraftNew = (): void => { - const f = files.find((x) => x.file === opened?.file) ?? files[0]; - if (!f) return; - const id = `new:${Date.now()}`; - // 원문번호는 절 번호만(부문은 구분 칸) · 키는 저장 때 서버가 줌 - const row: LogicRow = { - 키: "", - 원문번호: `${tx("New_Key")} ${Object.keys(drafts).length + 1}`, - 이름: "", - 결과단위: "원/", - 출처: "", - 소유: "공용", - 입력: [], - 중간: [], - 호표: [], - 덧줄: [], - 끝수: null, - }; - const next: Opened = { - id, - sub: subOf(f), - file: f.file, - version: f.version, - origKey: null, - row, - original: null, - prices: {}, - reasons: [], - lines: null, - values: {}, - }; - drafts[id] = pick(next, clone(row)); - persist(); - show(next); - }; - const onDelete = async (): Promise => { if (!opened?.row) return; if ( @@ -389,7 +331,6 @@ export async function mountM01LogicLab( back, el("h2", { text: tx("Title") }), pending, - createButton({ label: tx("Bar_New"), variant: "ghost", onClick: onDraftNew }), createButton({ label: tx("Bar_Delete"), variant: "danger", onClick: () => void onDelete() }), discardButton, saveButton, @@ -403,7 +344,6 @@ export async function mountM01LogicLab( className: "m01-logic__main", children: [bar, errors, waiting, list.root, editor], }), - calc, ], }), ); diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab_Detail.ts b/M01_MasterData/M01_MasterData_UI_LogicLab_Detail.ts new file mode 100644 index 00000000..6ac05a13 --- /dev/null +++ b/M01_MasterData/M01_MasterData_UI_LogicLab_Detail.ts @@ -0,0 +1,290 @@ +/* ============================================================================= + * M01_MasterData_UI_LogicLab_Detail.ts + * 「로직 개선 시험」 상세 화면 — 컨테이너 넷을 위에서 아래로 + * ① 기본정보(정본은 읽기 전용 · 비고만 고침) ② 일위대가 호표(인력·자재·경비·그 밖 묶음 + 소계) + * ③ 텍스트 수식(자리 — `_LogicLab_Formula.ts`) ④ 시험 계산(입력값 + 결과값 — `Logic_Calc` 그대로) + * 호표 줄을 누르면 그 줄이 쓰는 자료 모달(`_LogicLab_Modal.ts`) + * ========================================================================== */ + +import { el } from "@ui/ui_template_elements"; +import type { + CalcLine, + ElementBrief, + HoLine, + LogicRow, + NamedFormula, +} from "./M01_MasterData_UI_Logic_Api"; +import { formatNumber, qtyKind } from "./M01_MasterData_UI_Logic_Edit"; +import { tx } from "./M01_MasterData_UI_Logic_Text"; +import { buildFormula } from "./M01_MasterData_UI_LogicLab_Formula"; +import { openMaterialModal } from "./M01_MasterData_UI_LogicLab_Modal"; +import { tl, type LabTextKey } from "./M01_MasterData_UI_LogicLab_Text"; +import "./M01_MasterData_UI_LogicLab_Style.css"; + +export interface DetailContext { + row: LogicRow; + file: string; + reasons: string[]; + prices: Record; + /** 마지막 시험 계산의 줄 — 호표 차례 뒤에 덧줄 */ + lines: CalcLine[] | null; + values: Record; + onChange: () => void; + /** 시험 계산 칸(`buildCalc` 가 채움) — 넷째 컨테이너 안에 놓음 */ + calcHost: HTMLElement; +} + +type Group = "labor" | "material" | "cost" | "other"; +const GROUPS: Group[] = ["labor", "material", "cost", "other"]; +const OF_KIND: Record = { 인력: "labor", 재료: "material", 기계: "cost" }; +const OF_COST: Record = { 노무비: "labor", 재료비: "material", 경비: "cost" }; + +/** 줄 종류가 셋 중 어디에도 안 들어가면 「그 밖」 — 다른 로직을 부르는 줄은 비목으로 */ +const groupOfHo = (item: HoLine): Group => + OF_KIND[item.종류] ?? (item.종류 === "로직" ? OF_COST[item.비목 ?? ""] : undefined) ?? "other"; + +interface Entry { + group: Group; + name: string; + spec: string; + unit: string; + qty: string; + qtyTag: string; + price: string; + amount: number | null; + /** 덧줄이면 true */ + extra: boolean; + open: () => void; +} + +const groupName = (group: Group): string => tl(`G_${group}` as LabTextKey); + +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: "m01-logic__muted", text: hint })] : []), + ], + }), + ...body, + ], + }); + +function infoBox(ctx: DetailContext): HTMLElement { + const row = ctx.row; + const cell = (label: string, value: string): HTMLElement => + el("div", { + className: "m01lab__cell", + children: [ + el("span", { className: "m01-logic__muted", text: label }), + el("span", { text: value || "—" }), + ], + }); + const note = el("textarea", { className: "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: "m01-logic__reasons", + children: [ + el("strong", { text: tx("Head_Blocked") }), + ...ctx.reasons.map((r) => el("div", { text: r })), + ], + }), + ] + : []; + return section(tl("Info_Title"), [ + el("p", { className: "m01-logic__muted", text: tl("Info_ReadOnly") }), + ...reasons, + el("div", { + className: "m01lab__info", + children: [ + cell(tx("Head_File"), ctx.file), + cell(tx("Head_Key"), row.키), + cell(tx("Head_Number"), row.원문번호), + cell(tx("Head_Name"), row.이름), + cell(tx("Head_Unit"), row.결과단위), + cell(tx("Head_Source"), row.출처), + cell(tx("Head_Owner"), row.소유 ?? ""), + el("label", { + className: "m01lab__cell m01lab__cell--wide", + children: [el("span", { className: "m01-logic__muted", text: tx("Head_Note") }), note], + }), + ], + }), + ]); +} + +function entries(ctx: DetailContext): Entry[] { + const middles: NamedFormula[] = ctx.row.중간 ?? []; + const ho = ctx.row.호표 ?? []; + const out: Entry[] = ho.map((item, i) => { + const line = ctx.lines?.[i]; + const brief = ctx.prices[item.요소]; + const price = line ? line.단가 : brief?.값; + return { + group: groupOfHo(item), + name: item.이름 ?? item.요소, + spec: item.규격 ?? brief?.규격 ?? "", + unit: item.단위 ?? "", + qty: item.수량, + qtyTag: qtyKind(item.수량), + price: price === undefined || price === null ? "" : formatNumber(price), + amount: line ? line.금액 : null, + extra: false, + open: () => + openMaterialModal({ + title: item.이름 ?? item.요소, + expr: item.수량, + element: item.요소, + prices: ctx.prices, + middles, + values: ctx.values, + }), + }; + }); + (ctx.row.덧줄 ?? []).forEach((extra, i) => { + const line = ctx.lines?.[ho.length + i]; + out.push({ + group: OF_COST[extra.비목 ?? ""] ?? "other", + name: extra.이름, + spec: "", + unit: "", + qty: extra.식, + qtyTag: tl("Extra"), + price: "", + amount: line ? line.금액 : null, + extra: true, + open: () => + openMaterialModal({ + title: extra.이름, + expr: extra.식, + prices: ctx.prices, + middles, + values: ctx.values, + }), + }); + }); + return out; +} + +const cellOf = (text: string, className = ""): HTMLElement => el("td", { className, text }); + +function lineRow(e: Entry): HTMLElement { + const tr = el("tr", { + className: `m01lab__line${e.extra ? " m01lab__line--extra" : ""}`, + attrs: { tabindex: "0", "data-line": e.name }, + children: [ + el("td", { + children: [ + el("div", { text: e.name }), + ...(e.spec ? [el("div", { className: "m01-logic__muted", text: e.spec })] : []), + ], + }), + cellOf(e.unit), + el("td", { + children: [ + el("span", { className: "m01-logic__tag", text: e.qtyTag }), + el("span", { className: "m01lab__qty", text: ` ${e.qty}` }), + ], + }), + cellOf(e.price, "m01-logic__money"), + cellOf(e.amount === null ? "" : formatNumber(e.amount), "m01-logic__money"), + ], + }); + tr.addEventListener("click", e.open); + tr.addEventListener("keydown", (ev) => ev.key === "Enter" && e.open()); + return tr; +} + +function groupTable(group: Group, mine: Entry[], calculated: boolean): HTMLElement[] { + const sub = mine.reduce((s, e) => s + (e.amount ?? 0), 0); + const empty = el("tr", { + children: [ + el("td", { + attrs: { colspan: "5" }, + className: "m01-logic__muted", + text: tl("Empty_Group"), + }), + ], + }); + const subtotal = el("tr", { + className: "m01lab__subtotal", + attrs: { "data-group": group, "data-subtotal": calculated ? String(sub) : "" }, + children: [ + el("td", { attrs: { colspan: "4" }, text: `${groupName(group)} ${tl("Subtotal")}` }), + cellOf(calculated ? formatNumber(sub) : "—", "m01-logic__money"), + ], + }); + const headers = [ + tl("Col_Name"), + tl("Col_Unit"), + tl("Col_Qty"), + tl("Col_Price"), + tl("Col_Amount"), + ]; + return [ + el("h4", { className: "m01lab__group", text: groupName(group) }), + el("div", { + className: "m01-logic__scroll", + children: [ + el("table", { + className: "m01-logic__grid m01lab__ho", + children: [ + el("thead", { + children: [el("tr", { children: headers.map((h) => el("th", { text: h })) })], + }), + el("tbody", { children: [...(mine.length ? mine.map(lineRow) : [empty]), subtotal] }), + ], + }), + ], + }), + ]; +} + +function hoBox(ctx: DetailContext): HTMLElement { + const money = !("결과" in ctx.row) && (ctx.row.결과단위 ?? "").startsWith("원"); + if (!money) { + return section(tl("Ho_Title"), [ + el("p", { className: "m01-logic__muted", text: tl("Not_Money") }), + el("pre", { className: "m01lab__expr", text: ctx.row.결과?.식 ?? "" }), + ]); + } + const all = entries(ctx); + const calculated = ctx.lines !== null; + const blocks = GROUPS.flatMap((group) => { + const mine = all.filter((e) => e.group === group); + return group === "other" && !mine.length ? [] : groupTable(group, mine, calculated); // 그 밖은 있을 때만 + }); + const total = all.reduce((s, e) => s + (e.amount ?? 0), 0); + blocks.push( + el("p", { + className: "m01lab__total", + attrs: { "data-sum": calculated ? String(total) : "" }, + text: `${tl("Total")} ${calculated ? formatNumber(total) : "—"}`, + }), + ); + const hint = calculated ? tl("Click_Hint") : `${tl("Click_Hint")} · ${tl("Calc_First")}`; + return section(tl("Ho_Title"), blocks, hint); +} + +/** 상세 화면 넷을 `host` 에 쌓음 */ +export function buildLabDetail(host: HTMLElement, ctx: DetailContext): void { + const formula = el("div", { className: "m01lab__formula" }); + buildFormula(formula, { row: ctx.row, lines: ctx.lines }); + host.replaceChildren( + infoBox(ctx), + hoBox(ctx), + section(tl("Formula_Title"), [formula]), + section(tl("Calc_Title"), [ctx.calcHost]), + ); +} diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab_Formula.ts b/M01_MasterData/M01_MasterData_UI_LogicLab_Formula.ts new file mode 100644 index 00000000..80630f74 --- /dev/null +++ b/M01_MasterData/M01_MasterData_UI_LogicLab_Formula.ts @@ -0,0 +1,19 @@ +/* ============================================================================= + * M01_MasterData_UI_LogicLab_Formula.ts + * 「텍스트 수식」 컨테이너 자리 — 비목 묶음별 `이름 : 단가 * 수량식 = 금액` 줄은 여기서 채움(PLAN 3-2) + * ========================================================================== */ + +import { el } from "@ui/ui_template_elements"; +import type { CalcLine, LogicRow } from "./M01_MasterData_UI_Logic_Api"; +import { tl } from "./M01_MasterData_UI_LogicLab_Text"; + +export interface FormulaContext { + row: LogicRow; + /** 마지막 시험 계산의 줄 — 호표 차례 뒤에 덧줄 */ + lines: CalcLine[] | null; +} + +/** 자리만 — 줄을 채우는 일감이 `host` 안에 그림 */ +export function buildFormula(host: HTMLElement, _ctx: FormulaContext): void { + host.replaceChildren(el("p", { className: "m01-logic__muted", text: tl("Formula_Slot") })); +} diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab_Modal.ts b/M01_MasterData/M01_MasterData_UI_LogicLab_Modal.ts new file mode 100644 index 00000000..c1b7bf78 --- /dev/null +++ b/M01_MasterData/M01_MasterData_UI_LogicLab_Modal.ts @@ -0,0 +1,161 @@ +/* ============================================================================= + * M01_MasterData_UI_LogicLab_Modal.ts + * 호표 줄 자료 모달 — 그 줄이 쓰는 자료를 보임 + * 표(소요량·계수)는 표 그대로 + 지금 넣은 값으로 걸린 줄 강조 · 단일 값(단가)은 값과 출처 + * 표 읽기 = `/elements` → `/table`(`Logic_Note.loadTable`) · 단가 = 로직 화면이 이미 받은 `prices` + * ========================================================================== */ + +import { createButton, el } from "@ui/ui_template_elements"; +import type { ElementBrief, NamedFormula } from "./M01_MasterData_UI_Logic_Api"; +import { formatNumber } from "./M01_MasterData_UI_Logic_Edit"; +import { + loadTable, + matchRow, + relatedFinds, + type RelatedFind, + type TableRow, +} from "./M01_MasterData_UI_Logic_Note"; +import { tl } from "./M01_MasterData_UI_LogicLab_Text"; + +export interface ModalTarget { + title: string; + /** 수량 식(호표 줄) · 덧줄은 그 식 */ + expr: string; + /** 호표 줄의 단가 요소 키 — 덧줄은 없음 */ + element?: string; + prices: Record; + middles: NamedFormula[]; + /** 시험 계산 칸에 넣은 값 — 걸린 줄을 맞히는 데 씀 */ + values: Record; +} + +function tableView( + find: RelatedFind, + table: TableRow, + values: Record, +): HTMLElement { + const hit = matchRow(table, find.find.conds, { values, middle: {} }); + const cols = [...Object.keys(table.조건 ?? {}), ...Object.keys(table.값칸 ?? {})]; + const rows = (table.줄 ?? []).map((line) => + el("tr", { + className: hit === line ? "m01lab__hit" : "", + attrs: hit === line ? { "data-hit": "1" } : {}, + children: cols.map((c) => el("td", { text: formatNumber(line[c]) })), + }), + ); + const use = table.용도 + ? [table.용도.공종, (table.용도.대상 ?? []).join("·")].filter(Boolean).join(" · ") + : ""; + return el("div", { + className: "m01lab__data", + attrs: { "data-table": table.키 }, + children: [ + el("strong", { text: `${table.원문번호 ?? ""} ${table.이름 ?? table.키} · ${table.키}` }), + el("p", { + className: "m01-logic__muted", + text: [`${tl("Modal_Source")} ${table.출처 ?? ""}`, use].filter(Boolean).join(" · "), + }), + el("p", { + className: "m01-logic__muted", + text: `${find.source} → ${find.find.col} · ${hit ? tl("Modal_Hit") : tl("Modal_NoHit")}`, + }), + el("div", { + className: "m01-logic__scroll", + children: [ + el("table", { + className: "m01-logic__grid m01lab__table", + children: [ + el("thead", { + children: [el("tr", { children: cols.map((c) => el("th", { text: c })) })], + }), + el("tbody", { children: rows }), + ], + }), + ], + }), + ], + }); +} + +function priceView(ref: string, brief: ElementBrief): HTMLElement { + const cols = Object.entries(brief.값칸 ?? {}); + return el("div", { + className: "m01lab__data", + attrs: { "data-price": ref }, + children: [ + el("strong", { text: `${brief.이름 ?? ref} · ${ref}` }), + el("dl", { + className: "m01-logic__sums", + children: [ + ...(brief.규격 + ? [el("dt", { text: tl("Modal_Spec") }), el("dd", { text: brief.규격 })] + : []), + el("dt", { text: tl("Modal_Value") }), + el("dd", { className: "m01-logic__money", text: formatNumber(brief.값) }), + ...(brief.file + ? [el("dt", { text: tl("Modal_Source") }), el("dd", { text: brief.file })] + : []), + ...cols.flatMap(([k, v]) => [el("dt", { text: k }), el("dd", { text: String(v) })]), + ], + }), + ], + }); +} + +/** 모달을 엶 */ +export function openMaterialModal(target: ModalTarget): void { + const body = el("div", { className: "m01lab__modal-body" }); + const close = (): void => backdrop.remove(); + const dialog = el("div", { + className: "m01-logic__pick m01lab__modal", + attrs: { role: "dialog", "aria-label": target.title }, + children: [ + el("div", { + className: "m01lab__modal-top", + children: [ + el("h3", { text: target.title }), + createButton({ label: tl("Modal_Close"), variant: "ghost", onClick: close }), + ], + }), + body, + ], + }); + const backdrop = el("div", { className: "m01-logic__backdrop", children: [dialog] }); + backdrop.addEventListener("click", (ev) => ev.target === backdrop && close()); + backdrop.addEventListener("keydown", (ev) => ev.key === "Escape" && close()); + + const parts: HTMLElement[] = [ + el("p", { className: "m01-logic__muted", text: tl("Modal_Qty") }), + el("pre", { className: "m01lab__expr", text: target.expr }), + ]; + const finds = relatedFinds(target.expr, target.middles); + const price = target.element ? target.prices[target.element] : undefined; + if (target.element?.startsWith("로직(")) { + parts.push(el("p", { text: `${tl("Modal_Logic")} · ${target.element}` })); + } else if (price) { + parts.push( + el("p", { className: "m01-logic__muted", text: tl("Modal_Price") }), + priceView(target.element as string, price), + ); + } + const tables = el("div", { className: "m01lab__tables" }); + parts.push(tables); + if (!finds.length && !price && !target.element?.startsWith("로직(")) { + parts.push(el("p", { className: "m01-logic__muted", text: tl("Modal_NoData") })); + } + body.replaceChildren(...parts); + document.body.append(backdrop); + dialog.tabIndex = -1; + dialog.focus(); + + void Promise.all(finds.map(async (f) => ({ f, table: await loadTable(f.find.table) }))).then( + (got) => + tables.replaceChildren( + ...got.map(({ f, table }) => + table + ? tableView(f, table, target.values) + : el("p", { className: "m01-logic__bad", text: `${f.find.table} ?` }), + ), + ), + ); +} diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab_Style.css b/M01_MasterData/M01_MasterData_UI_LogicLab_Style.css new file mode 100644 index 00000000..cd64ea84 --- /dev/null +++ b/M01_MasterData/M01_MasterData_UI_LogicLab_Style.css @@ -0,0 +1,94 @@ +/* M01 로직 개선 시험 — 상세 화면 컨테이너 넷 · 호표 묶음 · 자료 모달 */ +.m01lab__box { + padding: var(--spacing-12); + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); +} + +.m01lab__info { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); + gap: var(--spacing-8); +} + +.m01lab__cell { + display: flex; + flex-direction: column; + gap: var(--spacing-4); +} + +.m01lab__cell--wide { + grid-column: 1 / -1; +} + +.m01lab__group { + margin: var(--spacing-8) 0 var(--spacing-4); +} + +.m01lab__ho th:nth-child(2) { + width: 64px; +} + +.m01lab__ho th:nth-child(4), +.m01lab__ho th:nth-child(5) { + width: 110px; +} + +.m01lab__line { + cursor: pointer; +} + +.m01lab__line:hover, +.m01lab__line:focus { + background: var(--color-paper); +} + +.m01lab__line--extra { + font-style: italic; +} + +.m01lab__qty, +.m01lab__expr { + font-family: var(--font-mono, monospace); + font-size: var(--text-body-sm); + white-space: pre-wrap; + word-break: break-all; +} + +.m01lab__subtotal td { + font-weight: 600; + background: var(--color-paper); +} + +.m01lab__total { + margin: var(--spacing-8) 0 0; + font-weight: 700; + text-align: right; +} + +.m01lab__modal { + overflow: auto; +} + +.m01lab__modal-top { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--spacing-8); +} + +.m01lab__modal-body, +.m01lab__tables { + display: flex; + flex-direction: column; + gap: var(--spacing-8); +} + +.m01lab__hit td { + background: color-mix(in srgb, gold 45%, transparent); + font-weight: 600; +} + +.m01lab__calc > h3 { + display: none; +} diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab_Text.ts b/M01_MasterData/M01_MasterData_UI_LogicLab_Text.ts new file mode 100644 index 00000000..7cf6c637 --- /dev/null +++ b/M01_MasterData/M01_MasterData_UI_LogicLab_Text.ts @@ -0,0 +1,57 @@ +/* ============================================================================= + * M01_MasterData_UI_LogicLab_Text.ts + * 「로직 개선 시험」 컨테이너 글자 — [한국어, 영어] + * ========================================================================== */ + +import { currentLanguageIndex } from "@ui/ui_template_locale"; + +const TEXT = { + Info_Title: ["기본정보", "Basic info"], + Info_ReadOnly: [ + "기본 로직은 품셈 원문 그대로라 여기서 고치지 못함 — 비고만 고칠 수 있음", + "Base logic follows the source book; only the note can be edited", + ], + Ho_Title: ["일위대가 호표", "Unit-cost table"], + Formula_Title: ["텍스트 수식", "Text formula"], + Formula_Slot: ["(준비 중)", "(coming)"], + Calc_Title: ["시험 계산", "Trial calculation"], + G_labor: ["인력", "Labor"], + G_material: ["자재", "Material"], + G_cost: ["경비", "Expense"], + G_other: ["그 밖", "Other"], + Subtotal: ["소계", "Subtotal"], + Total: ["계", "Total"], + Extra: ["덧줄", "Extra"], + Col_Name: ["이름", "Name"], + Col_Unit: ["단위", "Unit"], + Col_Qty: ["수량", "Quantity"], + Col_Price: ["단가", "Price"], + Col_Amount: ["금액", "Amount"], + Empty_Group: ["줄 없음", "No lines"], + Not_Money: ["호표가 없는 로직 — 결과 식", "No unit-cost lines — result formula"], + Calc_First: [ + "금액은 아래 「시험 계산」 을 누르면 채워짐", + "Amounts fill after the trial calculation", + ], + Click_Hint: ["줄을 누르면 그 줄이 쓰는 자료가 뜸", "Click a line to see the data it uses"], + Modal_Close: ["닫기", "Close"], + Modal_Qty: ["수량 식", "Quantity formula"], + Modal_Price: ["단가 자료", "Price source"], + Modal_Value: ["값", "Value"], + Modal_Source: ["출처", "Source"], + Modal_Spec: ["규격", "Spec"], + Modal_Logic: ["다른 로직을 부름", "Calls another logic"], + Modal_NoData: ["표나 단가 자료가 없는 줄 — 수량 식만 있음", "No table or price data"], + Modal_Hit: ["노란 줄 = 지금 넣은 값으로 걸린 줄", "Highlighted row matches the trial values"], + Modal_NoHit: [ + "걸린 줄을 아직 못 맞힘 — 「시험 계산」 에 값을 넣고 다시 누르세요", + "No matching row yet — enter trial values and reopen", + ], +} as const satisfies Record; + +export type LabTextKey = keyof typeof TEXT; + +export function tl(key: LabTextKey): string { + const entry = TEXT[key]; + return entry[currentLanguageIndex as 0 | 1] ?? entry[0]; +}