diff --git a/A00_Common/line_stack_text.ts b/A00_Common/line_stack_text.ts index ff8df78b7..7d712d363 100644 --- a/A00_Common/line_stack_text.ts +++ b/A00_Common/line_stack_text.ts @@ -14,7 +14,7 @@ const TEXT = { Add_Item: ["+ 그룹", "+ Group"], Add_Line: ["줄 더함", "Add line"], Add_율: ["율 — 그룹 소계의 %", "Rate — % of subtotal"], - Add_식: ["식 — 공식 틀", "Formula"], + Add_식: ["식", "Formula"], Del_Item: ["그룹 지움", "Delete group"], Del: ["지움", "Delete"], Item: ["그룹", "Group"], diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab.ts b/M01_MasterData/M01_MasterData_UI_LogicLab.ts index 319f5f606..7f1f1d0d8 100644 --- a/M01_MasterData/M01_MasterData_UI_LogicLab.ts +++ b/M01_MasterData/M01_MasterData_UI_LogicLab.ts @@ -571,7 +571,13 @@ export async function mountM01LogicLab( showLoadingOverlay(); const openKeyOn = async (key: string): Promise => { if (!(await leave())) return; - const found = allLogics.find((x) => x.키 === key); + // 목록에 없는 부품 문서(813-2) = 키로 읽어 엶 + const found = + allLogics.find((x) => x.키 === key) ?? + (await fetchLogic(key).then( + (o) => o.logic as unknown as { 구분: string; 키: string }, + () => null, + )); if (found) await open(logicId(found.구분, found.키), found.구분, found.키); else showToast(tx("Load_Failed"), "error"); }; diff --git a/M01_MasterData/M01_MasterData_UI_Page.ts b/M01_MasterData/M01_MasterData_UI_Page.ts index 1e093674f..decbba1ce 100644 --- a/M01_MasterData/M01_MasterData_UI_Page.ts +++ b/M01_MasterData/M01_MasterData_UI_Page.ts @@ -48,6 +48,7 @@ import { } from "./M01_MasterData_UI_Side"; import { loadTable } from "./M01_MasterData_UI_Logic_Note"; import { tl } from "./M01_MasterData_UI_LogicLab_Text"; +import { renderParts } from "./M01_MasterData_UI_Parts"; import { renderTables } from "./M01_MasterData_UI_Tables"; import type { LogicHandle } from "./M01_MasterData_UI_LogicLab"; import "./M01_MasterData_UI_Style.css"; @@ -84,6 +85,7 @@ function buildPage(): HTMLElement { attrs: { hidden: "" }, }); const body = el("div", { className: "m01-master__body" }); + const partsHost = el("div"); // 기계 · 계수 표 아래 부품 문서(813-2) // 제목 줄 · 찾기 줄 = 「단가산출 로직」 화면과 같은 공용 조각 const headBar = buildHeadBar({ title, status: [summary], buttons: [drop, save] }); const head = el("div", { @@ -99,6 +101,7 @@ function buildPage(): HTMLElement { notice, el("p", { className: "m01-master__hint", text: L("M01_WideHint") }), body, + partsHost, ], }); @@ -173,6 +176,7 @@ function buildPage(): HTMLElement { : pick.group === "요율" ? renderRateRows(body, pick, query) : renderRows(body, pick, query); + renderParts(partsHost, pick, (key) => openLogicTab(key)); showNotice(isStale(pick.file.file, pick.file.version) ? [L("M01_FileStale")] : []); }; diff --git a/M01_MasterData/M01_MasterData_UI_Parts.ts b/M01_MasterData/M01_MasterData_UI_Parts.ts new file mode 100644 index 000000000..e6b864cc3 --- /dev/null +++ b/M01_MasterData/M01_MasterData_UI_Parts.ts @@ -0,0 +1,105 @@ +/* ============================================================================= + * M01_MasterData_UI_Parts.ts + * M01 「기계」 · 「계수」 표 아래 부품 문서(PLAN 813-2 · 요구 722-3 · 724~732) — 단가산출 기초 목록에서 뺀 보조 문서 자리 + * 기계 · 상세구분(기종) 고름 = 「시간당 기계경비 계산식」(그 기종 기계가 쓰는 방식 · 기계 수) · 「작업량 공식 틀」(그 기종 틀) + * 계수 = 「계수식」(기계 무관 · 돈 아닌 식 넷) + * 이름 누름 = 줄 쌓기 읽기 창(`createStackScreen` · 고침 없음) · [단가산출 기초에서 열기] = 지금 수식 탭 그대로 고침 + * 서버 = `GET /api/m01/parts?group&detail`(`M01_MasterData_Parts.of_kind`) + * ========================================================================== */ + +import { API_BASE_URL } from "@config/config_frontend"; +import { createButton } from "@ui/ui_template_button"; +import { el } from "@ui/ui_template_elements"; +import { openModal } from "@ui/ui_template_modal"; +import { currentLanguageIndex } from "@ui/ui_template_locale"; +import { createText } from "@ui/ui_template_text"; +import { createStackScreen } from "../A00_Common/line_stack_screen"; +import type { Pick } from "./M01_MasterData_UI_Side"; + +interface Part { + 키: string; + 이름: string; + 결과단위: string; + 기계수?: number; +} +type Parts = Partial>; + +const T = { + 기계경비식: ["시간당 기계경비 계산식", "Hourly machine cost formula"], + "작업량 틀": ["작업량 공식 틀", "Output formula templates"], + 계수식: ["계수식", "Coefficient formulas"], + Open: ["단가산출 기초에서 열기", "Open in unit price basis"], + Machines: ["기계", "machines"], + None: ["없음", "None"], + Close: ["닫기", "Close"], +} as const; +const tx = (k: keyof typeof T): string => T[k][currentLanguageIndex === 1 ? 1 : 0]; +const KINDS = ["기계경비식", "작업량 틀", "계수식"] as const; + +/** 표 아래 부품 칸 — 기계(상세구분 있음) · 계수만 · 그 밖 = 비움 */ +export function renderParts(host: HTMLElement, pick: Pick, openLogic: (key: string) => void): void { + host.replaceChildren(); + const group = pick.group === "계수" ? "계수식" : pick.group === "기계" ? "기계" : ""; + if (!group || (group === "기계" && !pick.detail)) return; + let stack: ReturnType | null = null; + /** 줄 쌓기 읽기 창 — 고침 없음 · [단가산출 기초에서 열기] = 수식 탭에서 고침 */ + const show = (p: Part): void => { + stack ??= createStackScreen({ base: "/m01/logic/doc", admin: false, cache: "m01_parts_view" }); + const view = stack; + openModal({ + title: `${p.키} · ${p.이름}`, + closeLabel: tx("Close"), + width: "1100px", + wide: true, + mount: (body, close, actions) => { + actions.append( + createButton({ + label: tx("Open"), + variant: "ghost", + size: 28, + onClick: () => { + close(); + openLogic(p.키); + }, + }), + ); + body.append(view.root); + view.draw(p.키, true); + }, + }); + }; + const q = new URLSearchParams({ group, detail: pick.detail ?? "" }); + const mine = (host.dataset.partsAsk = q.toString()); // 늦게 온 옛 답은 버림 + void fetch(`${API_BASE_URL}/m01/parts?${q}`, { credentials: "include" }) + .then((r): Promise | Parts => (r.ok ? (r.json() as Promise) : {})) + .catch((): Parts => ({})) + .then((got) => { + if (host.dataset.partsAsk !== mine) return; + const blocks = KINDS.filter((k) => k in got).map((k) => { + const list = got[k] ?? []; + const items = list.map((p) => { + const b = createButton({ + label: `${p.이름}${p.기계수 ? ` · ${tx("Machines")} ${p.기계수}` : ""}`, + variant: "ghost", + tone: "link", + size: 28, + onClick: () => show(p), + }); + b.dataset.part = p.키; + return b; + }); + return el("section", { + className: "m01-parts__block", + attrs: { "data-parts": k }, + children: [ + el("h3", { className: "m01-parts__title", text: tx(k) }), + items.length + ? el("div", { className: "m01-parts__list", children: items }) + : createText(tx("None")), + ], + }); + }); + if (blocks.length) + host.replaceChildren(el("div", { className: "m01-parts", children: blocks })); + }); +} diff --git a/M01_MasterData/M01_MasterData_UI_Style.css b/M01_MasterData/M01_MasterData_UI_Style.css index 277e16c7f..71f77c7ae 100644 --- a/M01_MasterData/M01_MasterData_UI_Style.css +++ b/M01_MasterData/M01_MasterData_UI_Style.css @@ -170,3 +170,21 @@ table.m01-master__grid { gap: var(--spacing-8); padding: var(--spacing-4) var(--spacing-8); } + +/* 부품 문서(813-2) — 기계 · 계수 표 아래 */ +.m01-parts { + display: flex; + flex-direction: column; + gap: var(--spacing-12); + margin-top: var(--spacing-16); +} +.m01-parts__title { + margin: 0 0 var(--spacing-4); + font-size: var(--text-body); + color: var(--color-text); +} +.m01-parts__list { + display: flex; + flex-wrap: wrap; + gap: var(--spacing-4); +}