From f8338194c6f83cbc8df3d1dfd79a9b96bf054e42 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 1 Nov 2026 16:32:32 +0900 Subject: [PATCH 1/3] =?UTF-8?q?refactor(717-3):=20=EC=9D=BC=EC=9C=84?= =?UTF-8?q?=EB=8C=80=EA=B0=80=20=EC=83=81=EC=84=B8=20=C2=B7=20=EC=A4=84=20?= =?UTF-8?q?=ED=91=9C=20=EB=82=98=EB=88=94=20=E2=80=94=20=EC=9E=90=EC=9E=AC?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=ED=91=9C(Labor=5FMatTable)=20=C2=B7=20?= =?UTF-8?q?=EC=84=9C=EA=B3=A0=20=EA=B5=AC=EB=B6=84=20=EA=B3=A0=EB=A5=B4?= =?UTF-8?q?=EA=B8=B0(Labor=5FLibMeta)=20=EB=96=BC=EC=96=B4=20=EB=83=84=20?= =?UTF-8?q?=C2=B7=20=EB=8F=99=EC=9E=91=20=EA=B0=99=EC=9D=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Labor_Rows 671 → 566 줄 · Labor_Detail 700 → 650 줄(717-3 모달 단추 자리) - 낡은 시험 셋 고침: mat_marks · precombo_class = 옮긴 파일을 읽음 · hopyo_screen 의 Detail 「꼭 700 줄」 = 「700 줄 넘지 않음」(나눔으로 줄어듦) - 시험 158 통과 · 1 실패(test_175 B09 겹 대가 = Store_Combo._PER 없음 · 나눔 전부터 · 719-2 몫) Co-Authored-By: Claude Opus 5.5 --- .../M02_MasterTemplete_Labor_Detail.ts | 64 +-------- .../M02_MasterTemplete_Labor_LibMeta.ts | 56 ++++++++ .../M02_MasterTemplete_Labor_MatTable.ts | 127 ++++++++++++++++++ .../M02_MasterTemplete_Labor_Rows.ts | 113 +--------------- .../M02_MasterTemplete_Labor_View.ts | 3 +- resources/tester/test_m02_hopyo_screen.py | 4 +- resources/tester/test_m02_labor_mat_marks.py | 2 +- resources/tester/test_m02_precombo_class.py | 2 +- 8 files changed, 199 insertions(+), 172 deletions(-) create mode 100644 M02_MasterTemplete/M02_MasterTemplete_Labor_LibMeta.ts create mode 100644 M02_MasterTemplete/M02_MasterTemplete_Labor_MatTable.ts diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts index c7286f964..d4707fb31 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts @@ -16,13 +16,7 @@ * · 맨 위 엑셀 꼴 격자(704-3 · `Labor_Grid`) · 읽기만 카드는 격자만 · 규격 표(704-4 · `Labor_Spec`) * ========================================================================== */ -import { - createButton, - createInputField, - createSelectField, - el, - showToast, -} from "@ui/ui_template_elements"; +import { createButton, createInputField, el, showToast } from "@ui/ui_template_elements"; import { createSidePanels } from "@ui/ui_template_side_panels"; import { ROUTES } from "@config/config_frontend"; import { navigateTo } from "../A00_Common/router"; @@ -64,15 +58,15 @@ import { afterRowDrag } from "./M02_MasterTemplete_Labor_RowDrag"; import { openLogicPicker, withHopyo } from "./M02_MasterTemplete_Labor_Pick"; import { logicMatRows } from "./M02_MasterTemplete_Labor_LogicMats"; import { setLogicMark, setMark } from "./M02_MasterTemplete_Labor_MatMarks"; -import { logicTable, matTable, qtyShort } from "./M02_MasterTemplete_Labor_Rows"; +import { logicTable, qtyShort } from "./M02_MasterTemplete_Labor_Rows"; +import { matTable } from "./M02_MasterTemplete_Labor_MatTable"; import { laborGrid } from "./M02_MasterTemplete_Labor_Grid"; import { tc } from "./M02_MasterTemplete_Labor_Text"; import { insideKeys, releaseTied } from "./M02_Labor_LogicMats_Model"; import { keepKnown } from "./M02_Labor_Values_Model"; import { addMaterialButton } from "./M02_Material_Pick"; import { createUnitPick, NO_UNIT } from "./DataFlow/M02_DF_UnitPick"; -import { fetchStructureDef } from "./StructOptions/M02_SO_Api"; -import { precomboClasses } from "./M02_MasterTemplete_UI_Side_Precombo"; +import { libMeta } from "./M02_MasterTemplete_Labor_LibMeta"; import "../M01_MasterData/M01_MasterData_UI_Logic_Style.css"; import "../M01_MasterData/M01_MasterData_UI_LogicLab_Style.css"; import "@ui/ui_template_general_layout.css"; @@ -574,52 +568,6 @@ export function mountLaborDetail( opt.onChanged(dirty()); }); - /** 서고 구분 · 상세구분(51-12 · 134-5) — 구조물군 › 종류 + 공통(`precomboClasses`)에서 고름 · - * 구분 바꾸면 상세구분 = 그 첫 줄(공통) · 빈 값 = 미분류 */ - const meta = cur as { 구분?: string | null; 상세구분?: string | null }; - const libMeta = (): HTMLElement[] => { - const sub = createSelectField({ options: [], label: tc("Sub"), disabled: !edit }); - const detail = createSelectField({ options: [], label: tc("Detail"), disabled: !edit }); - sub.root.dataset.laborField = "구분"; - detail.root.dataset.laborField = "상세구분"; - let subs: { name: string; details: string[] }[] = []; - // 목록에 없는 지금 값(빈 것 = 「—」 · 옛 문서)도 선택지로 둠 - const opts = (names: string[], now: string | null | undefined) => - [...new Set([now ?? "", ...names])] - .filter((v, i) => v || i === 0) - .map((v) => ({ value: v, text: v || "—" })); - const fill = (): void => { - sub.setOptions( - opts( - subs.map((s) => s.name), - meta.구분, - ), - meta.구분 ?? "", - ); - const details = subs.find((s) => s.name === meta.구분)?.details ?? []; - detail.setOptions(opts(details, meta.상세구분), meta.상세구분 ?? ""); - }; - sub.select.addEventListener("change", () => { - meta.구분 = sub.select.value; - meta.상세구분 = subs.find((s) => s.name === meta.구분)?.details[0] ?? null; - fill(); - opt.onChanged(dirty()); - }); - detail.select.addEventListener("change", () => { - meta.상세구분 = detail.select.value; - opt.onChanged(dirty()); - }); - fill(); - void fetchStructureDef().then( - (def) => { - subs = precomboClasses(def); - fill(); - }, - () => undefined, - ); - return [sub.root, detail.root]; - }; - const editor = (body: HTMLElement): HTMLElement => side ? el("div", { className: "ui-side-panels-row", children: [body, side.root] }) : body; host.replaceChildren( @@ -637,7 +585,9 @@ export function mountLaborDetail( el("div", { className: "m01lab__info", children: [ - ...(lib ? [nameField.root, ...libMeta()] : []), + ...(lib + ? [nameField.root, ...libMeta(cur, edit, () => opt.onChanged(dirty()))] + : []), unit.root, origin.root, ], diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_LibMeta.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_LibMeta.ts new file mode 100644 index 000000000..d65966386 --- /dev/null +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_LibMeta.ts @@ -0,0 +1,56 @@ +/* ============================================================================= + * M02_MasterTemplete_Labor_LibMeta.ts + * 서고 모드(일위대가 · 사전조합) 정보 칸 구분 · 상세구분 고르기(51-12 · 134-5 · `Labor_Detail` 에서 뗌 · 717-3) + * 고침 = 문서 `구분` · `상세구분` 바로(화면 캐시) · 바꾼 뒤 `changed()`. + * ========================================================================== */ + +import { createSelectField } from "@ui/ui_template_elements"; +import { fetchStructureDef } from "./StructOptions/M02_SO_Api"; +import { precomboClasses } from "./M02_MasterTemplete_UI_Side_Precombo"; +import { tc } from "./M02_MasterTemplete_Labor_Text"; + +/** 서고 구분 · 상세구분(51-12 · 134-5) — 구조물군 › 종류 + 공통(`precomboClasses`)에서 고름 · + * 구분 바꾸면 상세구분 = 그 첫 줄(공통) · 빈 값 = 미분류 */ +export function libMeta(doc: unknown, edit: boolean, changed: () => void): HTMLElement[] { + const meta = doc as { 구분?: string | null; 상세구분?: string | null }; + const sub = createSelectField({ options: [], label: tc("Sub"), disabled: !edit }); + const detail = createSelectField({ options: [], label: tc("Detail"), disabled: !edit }); + sub.root.dataset.laborField = "구분"; + detail.root.dataset.laborField = "상세구분"; + let subs: { name: string; details: string[] }[] = []; + // 목록에 없는 지금 값(빈 것 = 「—」 · 옛 문서)도 선택지로 둠 + const opts = (names: string[], now: string | null | undefined) => + [...new Set([now ?? "", ...names])] + .filter((v, i) => v || i === 0) + .map((v) => ({ value: v, text: v || "—" })); + const fill = (): void => { + sub.setOptions( + opts( + subs.map((s) => s.name), + meta.구분, + ), + meta.구분 ?? "", + ); + const details = subs.find((s) => s.name === meta.구분)?.details ?? []; + detail.setOptions(opts(details, meta.상세구분), meta.상세구분 ?? ""); + }; + sub.select.addEventListener("change", () => { + meta.구분 = sub.select.value; + meta.상세구분 = subs.find((s) => s.name === meta.구분)?.details[0] ?? null; + fill(); + changed(); + }); + detail.select.addEventListener("change", () => { + meta.상세구분 = detail.select.value; + changed(); + }); + fill(); + void fetchStructureDef().then( + (def) => { + subs = precomboClasses(def); + fill(); + }, + () => undefined, + ); + return [sub.root, detail.root]; +} diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_MatTable.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_MatTable.ts new file mode 100644 index 000000000..e4f785c45 --- /dev/null +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_MatTable.ts @@ -0,0 +1,127 @@ +/* ============================================================================= + * M02_MasterTemplete_Labor_MatTable.ts + * 일위대가 자재목록 표(43-3 · `Labor_Rows` 에서 뗌 · 717-3) — 상세 · 보기 갈래가 같이 씀. + * 자재목록 = 코드 · 이름 · 규격 · 수량산출 · 단위 · 가격 다섯(기준 칸 강조) · 기준단가 / 반영 금액. + * 금액은 calc 답(없으면 「…」) · 줄 누름 · [삭제]는 `on` 이 있을 때만. + * ========================================================================== */ + +import { createButton, el } from "@ui/ui_template_elements"; +import { formatMoney } from "../M01_MasterData/M01_MasterData_UI_Logic_Money"; +import type { + LaborCalc, + LaborCalcLine, + LaborCalcMat, + LaborMat, +} from "./M02_MasterTemplete_Labor_Api"; +import { markCells, markHeads } from "./M02_MasterTemplete_Labor_MatMarks"; +import { + moneyCell, + PRICE_SLOTS, + qtyCell, + table, + titled, + type RowHandlers, +} from "./M02_MasterTemplete_Labor_Rows"; +import { tc } from "./M02_MasterTemplete_Labor_Text"; + +export interface MatTableData { + mats: LaborMat[]; + /** 로직 자재 줄(103-1 · `Labor_LogicMats`) — 사용자 자재 앞 · 같은 칸 */ + lead?: HTMLElement[]; + calc: LaborCalc | null; + fail?: string | null; + cells?: ReadonlyMap; +} + +/** 자재목록 표(43-3 · 로직 줄과 같은 꼴) — 코드 · 이름 · 규격 · 수량산출(위 기준 수량 1 단위 / 아래 지금 + * 수량) · 단위 · 가격 다섯(빈 것 「—」 · 기준단가가 된 칸 강조) · 금액(위 기준단가 / 아래 반영 = 기준단가 × 수량) · + * 보기 갈래는 `on` 없이 */ +export function matTable(data: MatTableData, on?: Omit): HTMLElement { + const wait = !data.calc && !data.fail; + const remove = on?.remove; + const show = (v: number | null | undefined): string => + wait ? "…" : v === null || v === undefined ? "—" : formatMoney(v); + const trs = data.mats.map((mat, i) => { + const line: LaborCalcMat | undefined = data.calc?.자재.find((m) => m.코드 === mat.코드); + // 수량산출 칸은 로직 줄 칸을 그대로 씀 — 기준 수량 = 자재 단위 1 + const qtyLine = { ...line, 배: 1, 분모: mat.단위 ?? "" } as LaborCalcLine; + const priceCell = (slot: (typeof PRICE_SLOTS)[number]): HTMLElement => { + const base = !!line?.기준칸 && line.기준칸 === slot; + return el("td", { + className: `m02-labrow__num m02-labrow__price${base ? " is-base-price" : ""}`, + text: show(line?.가격?.[slot]), + attrs: { + "data-label": slot, + "data-price-slot": slot, + ...(base ? { title: tc("Labor_BasePrice") } : {}), + }, + }); + }; + const tr = el("tr", { + className: on ? "m02-labrow__row is-open" : "m02-labrow__row", + attrs: { "data-labor-mat": mat.코드 }, + children: [ + el("td", { className: "m02-labrow__code", text: mat.코드 }), + titled( + el("td", { + className: "m02-labrow__name", + children: [ + el("strong", { text: mat.이름 ?? "" }), + ...(line?.까닭 + ? [el("small", { className: "ui-text ui-text--error", text: line.까닭 })] + : []), + ], + }), + ), + el("td", { + className: "m02-labrow__spec", + text: mat.규격 ?? "", + attrs: { title: mat.규격 ?? "" }, + }), + qtyCell(qtyLine, data.cells, on?.qty ? () => on.qty?.(i) : null), + el("td", { + className: "m02-labrow__unit", + text: mat.단위 ?? "", + attrs: { "data-label": tc("Labor_Unit") }, + }), + ...markCells(mat, on?.mark ? (k, v) => on.mark?.(i, k, v) : undefined), + ...PRICE_SLOTS.map(priceCell), + moneyCell(line?.기준단가, line?.반영, wait, tc("Labor_AppliedSum")), + ...(remove + ? [ + el("td", { + className: "m02-labrow__move", + children: [ + createButton({ + label: tc("Remove"), + variant: "danger", + onClick: (e) => { + e.stopPropagation(); + remove(i); + }, + }), + ], + }), + ] + : []), + ], + }); + if (on) tr.addEventListener("click", () => on.open(i)); + return tr; + }); + const heads = [ + tc("Labor_Code"), + tc("Col_Name"), + tc("Labor_Spec"), + tc("Labor_QtyCol"), + tc("Labor_Unit"), + ...markHeads(), + ...PRICE_SLOTS, + `${tc("Labor_BasePrice")} · ${tc("Labor_AppliedSum")}`, + ...(remove ? [tc("Col_Move")] : []), + ]; + const lead = data.lead ?? []; + return data.mats.length || lead.length + ? table(heads, [...lead, ...trs], []) + : el("p", { className: "ui-text ui-text--muted", text: tc("Labor_NoMat") }); +} diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Rows.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Rows.ts index 84f9ca0fd..d3bc1ad1c 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Rows.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Rows.ts @@ -5,7 +5,7 @@ * 결과 단위 · 금액 칸 넷(노무비 · 재료비 · 경비 · 합계 · 칸마다 위 기준 작게 / 아래 반영) · * 옵션 값(「지역 서울 · 규격 25mm」)은 줄 아래 구분 ~ 별칭에 걸친 한 줄(나머지 칸은 위 줄과 두 줄 병합 · 113-1) · * 맨 아래 합계 줄(전체 합계 강조). - * 자재목록 = 코드 · 이름 · 규격 · 수량산출 · 단위 · 가격 다섯(기준 칸 강조) · 기준단가 / 반영 금액(43-3). + * 자재목록 표 = `Labor_MatTable`(717-3 나눔). * 금액은 calc 답(없으면 「…」). * 줄 누름 · 줄 단추는 `on` 이 있을 때만(보기 갈래 = 없음) — 순서 칸 2행(51-14 · 위 [위로] [아래로] / 아래 [옵션] 또는 [수정] · [삭제] · * 묶음 `ui-btn-group` · 순서 없는 카드 갈래 = 아래 줄만) · 일위조합 줄 = [수정](`ref` · 서고 원본 · 옛 단가승격 참조 줄 = 단추 없음) · @@ -20,14 +20,11 @@ import type { ItemLogic } from "./M02_MasterTemplete_Api_Fetch"; import type { LaborCalc, LaborCalcLine, - LaborCalcMat, LaborCost, - LaborMat, LaborRow, MatMarkKey, } from "./M02_MasterTemplete_Labor_Api"; import { lineId } from "./M02_MasterTemplete_Labor_Calc"; -import { markCells, markHeads } from "./M02_MasterTemplete_Labor_MatMarks"; import { attachRowDrag } from "./M02_MasterTemplete_Labor_RowDrag"; import { tc } from "./M02_MasterTemplete_Labor_Text"; import "@ui/ui_template_general_layout.css"; @@ -124,7 +121,7 @@ export function qtyShort( /** 수량산출 칸(40-1) — 위 결과단위 기준 수량(「10 ㎡」) · 아래 지금 수량(「수기 5 ㎥」 · 「12.5(B6)」 · 「—」) · * `open` 이 있으면 단추(줄 누름과 따로) */ -function qtyCell( +export function qtyCell( line: LaborCalcLine | undefined, cells: ReadonlyMap | undefined, open: (() => void) | null, @@ -180,12 +177,12 @@ export function moneyCell( } /** 이름 칸 글마다 title = 전체 글(한 줄 말줄임 · 45-7) */ -const titled = (td: HTMLElement): HTMLElement => { +export const titled = (td: HTMLElement): HTMLElement => { for (const c of td.children) c.setAttribute("title", c.textContent ?? ""); return td; }; -const table = (heads: string[], rows: HTMLElement[], foot: HTMLElement[]): HTMLElement => +export const table = (heads: string[], rows: HTMLElement[], foot: HTMLElement[]): HTMLElement => el("div", { className: "ui-general-block__table-wrap", children: [ @@ -565,107 +562,5 @@ export function logicTable(data: LogicTableData, on?: RowHandlers): HTMLElement return out; } -export interface MatTableData { - mats: LaborMat[]; - /** 로직 자재 줄(103-1 · `Labor_LogicMats`) — 사용자 자재 앞 · 같은 칸 */ - lead?: HTMLElement[]; - calc: LaborCalc | null; - fail?: string | null; - cells?: ReadonlyMap; -} - /** M01 자재품목 값 열 다섯(이름 그대로 · 43-3) — 서버 calc 답 `가격` 열쇠와 같음 */ export const PRICE_SLOTS = ["물가자료", "유통물가", "물가정보", "거래가격", "관급"] as const; - -/** 자재목록 표(43-3 · 로직 줄과 같은 꼴) — 코드 · 이름 · 규격 · 수량산출(위 기준 수량 1 단위 / 아래 지금 - * 수량) · 단위 · 가격 다섯(빈 것 「—」 · 기준단가가 된 칸 강조) · 금액(위 기준단가 / 아래 반영 = 기준단가 × 수량) · - * 보기 갈래는 `on` 없이 */ -export function matTable(data: MatTableData, on?: Omit): HTMLElement { - const wait = !data.calc && !data.fail; - const remove = on?.remove; - const show = (v: number | null | undefined): string => - wait ? "…" : v === null || v === undefined ? "—" : formatMoney(v); - const trs = data.mats.map((mat, i) => { - const line: LaborCalcMat | undefined = data.calc?.자재.find((m) => m.코드 === mat.코드); - // 수량산출 칸은 로직 줄 칸을 그대로 씀 — 기준 수량 = 자재 단위 1 - const qtyLine = { ...line, 배: 1, 분모: mat.단위 ?? "" } as LaborCalcLine; - const priceCell = (slot: (typeof PRICE_SLOTS)[number]): HTMLElement => { - const base = !!line?.기준칸 && line.기준칸 === slot; - return el("td", { - className: `m02-labrow__num m02-labrow__price${base ? " is-base-price" : ""}`, - text: show(line?.가격?.[slot]), - attrs: { - "data-label": slot, - "data-price-slot": slot, - ...(base ? { title: tc("Labor_BasePrice") } : {}), - }, - }); - }; - const tr = el("tr", { - className: on ? "m02-labrow__row is-open" : "m02-labrow__row", - attrs: { "data-labor-mat": mat.코드 }, - children: [ - el("td", { className: "m02-labrow__code", text: mat.코드 }), - titled( - el("td", { - className: "m02-labrow__name", - children: [ - el("strong", { text: mat.이름 ?? "" }), - ...(line?.까닭 - ? [el("small", { className: "ui-text ui-text--error", text: line.까닭 })] - : []), - ], - }), - ), - el("td", { - className: "m02-labrow__spec", - text: mat.규격 ?? "", - attrs: { title: mat.규격 ?? "" }, - }), - qtyCell(qtyLine, data.cells, on?.qty ? () => on.qty?.(i) : null), - el("td", { - className: "m02-labrow__unit", - text: mat.단위 ?? "", - attrs: { "data-label": tc("Labor_Unit") }, - }), - ...markCells(mat, on?.mark ? (k, v) => on.mark?.(i, k, v) : undefined), - ...PRICE_SLOTS.map(priceCell), - moneyCell(line?.기준단가, line?.반영, wait, tc("Labor_AppliedSum")), - ...(remove - ? [ - el("td", { - className: "m02-labrow__move", - children: [ - createButton({ - label: tc("Remove"), - variant: "danger", - onClick: (e) => { - e.stopPropagation(); - remove(i); - }, - }), - ], - }), - ] - : []), - ], - }); - if (on) tr.addEventListener("click", () => on.open(i)); - return tr; - }); - const heads = [ - tc("Labor_Code"), - tc("Col_Name"), - tc("Labor_Spec"), - tc("Labor_QtyCol"), - tc("Labor_Unit"), - ...markHeads(), - ...PRICE_SLOTS, - `${tc("Labor_BasePrice")} · ${tc("Labor_AppliedSum")}`, - ...(remove ? [tc("Col_Move")] : []), - ]; - const lead = data.lead ?? []; - return data.mats.length || lead.length - ? table(heads, [...lead, ...trs], []) - : el("p", { className: "ui-text ui-text--muted", text: tc("Labor_NoMat") }); -} diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_View.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_View.ts index 0fdbd43a0..aefc57fb2 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_View.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_View.ts @@ -21,7 +21,8 @@ import { } from "./M02_MasterTemplete_Labor_Api"; import { lineId, settle } from "./M02_MasterTemplete_Labor_Calc"; import { logicMatRows } from "./M02_MasterTemplete_Labor_LogicMats"; -import { logicTable, matTable } from "./M02_MasterTemplete_Labor_Rows"; +import { logicTable } from "./M02_MasterTemplete_Labor_Rows"; +import { matTable } from "./M02_MasterTemplete_Labor_MatTable"; import { tc } from "./M02_MasterTemplete_Labor_Text"; import "./M02_MasterTemplete_Labor.css"; diff --git a/resources/tester/test_m02_hopyo_screen.py b/resources/tester/test_m02_hopyo_screen.py index 909b26079..dfd6fc510 100644 --- a/resources/tester/test_m02_hopyo_screen.py +++ b/resources/tester/test_m02_hopyo_screen.py @@ -361,9 +361,7 @@ def test_좌측_판_자리와_권한(): def test_로직_추가_한_곳과_파일_규칙(): detail = _text("M02_MasterTemplete_Labor_Detail.ts") - assert ( - len(detail.splitlines()) == 700 - ) # 줄 수 그대로(361-2 전국 자재 부르는 줄 둘 · 553-5 보조 글 부품 -2 · 704-3 격자 +7) + assert len(detail.splitlines()) <= 700 # 700 줄 넘지 않음(717-3 나눔으로 줄어듦) assert "withHopyo(library)," in detail pick = _text("M02_MasterTemplete_Labor_Pick.ts") assert "more?: () => Promise;" in pick and "...later" in pick diff --git a/resources/tester/test_m02_labor_mat_marks.py b/resources/tester/test_m02_labor_mat_marks.py index e788663a8..556c47686 100644 --- a/resources/tester/test_m02_labor_mat_marks.py +++ b/resources/tester/test_m02_labor_mat_marks.py @@ -121,7 +121,7 @@ def test_화면_칸_부품_사용자_줄_로직_줄_같은_칸(): marks = _src("M02_MasterTemplete_Labor_MatMarks.ts") assert '"—"' not in marks # 「—」 항목 없음(162-1) assert "e.stopPropagation()" in marks # 줄 누름과 따로 - rows = _src("M02_MasterTemplete_Labor_Rows.ts") + rows = _src("M02_MasterTemplete_Labor_MatTable.ts") # 717-3 자재목록 표 나눔 assert "...markCells(mat, on?.mark" in rows and "...markHeads()," in rows assert "...markCells(" in _src("M02_MasterTemplete_Labor_LogicMats.ts") detail = _src("M02_MasterTemplete_Labor_Detail.ts") diff --git a/resources/tester/test_m02_precombo_class.py b/resources/tester/test_m02_precombo_class.py index 73eb21215..b80d9bc82 100644 --- a/resources/tester/test_m02_precombo_class.py +++ b/resources/tester/test_m02_precombo_class.py @@ -62,7 +62,7 @@ def test_옛_품셈_분류는_미분류_새_값은_그대로(): def test_화면_원천은_구조물_정의_새_문서는_미분류(): - detail = (M02 / "M02_MasterTemplete_Labor_Detail.ts").read_text("utf-8") + detail = (M02 / "M02_MasterTemplete_Labor_LibMeta.ts").read_text("utf-8") # 717-3 나눔 assert "subs = precomboClasses(def);" in detail and "fetchLibLogicSubs" not in detail assert '[...new Set([now ?? "", ...names])]' in detail # 지금 값도 선택지 그대로 lib = (M02 / "M02_MasterTemplete_Library_List.ts").read_text("utf-8") From 6126f5ee207accc5531b2ce5935186572c7d67a8 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 1 Nov 2026 16:33:17 +0900 Subject: [PATCH 2/3] =?UTF-8?q?fix(719-2):=20=EC=97=85=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8=20=EB=92=A4=20=EC=84=9C=EA=B3=A0=20=EC=82=AC=EB=B3=B8?= =?UTF-8?q?=20=EC=A4=84=20=EC=97=B0=EA=B2=B0=20=EB=8B=A4=EC=8B=9C=20?= =?UTF-8?q?=EC=9D=B4=EC=9D=8C=20=C2=B7=20=EB=8B=A8=EA=B0=80=EC=82=B0?= =?UTF-8?q?=EC=B6=9C=20=EB=B8=94=EB=A1=9D=20=EC=95=88=EB=82=B4=20=EA=B8=80?= =?UTF-8?q?=20=EB=90=98=EB=8F=8C=EB=A6=BC=20=EC=9D=B4=EB=A6=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Shell: [업데이트]가 서고 사본을 다시 굳혀 블록 id 가 바뀌면 화면 캐시 줄 연결을 사본 폴더(lib)로 다시 이음(짝 Bill_Library.fix_keys) — 캐시가 옛 id 를 잡아 단가 빈칸 되던 것 - Bill_Fold: 단가산출 블록 안내 글 「우클릭 [단가산출 값으로]」 Co-Authored-By: Claude Opus 5.5 --- B09_Estimation/B09_Estimation_UI_Shell.ts | 7 +++++++ B09_Estimation/B09_Estimation_UI_Tabs_Bill_Fold.ts | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/B09_Estimation/B09_Estimation_UI_Shell.ts b/B09_Estimation/B09_Estimation_UI_Shell.ts index 6786947ae..62d8dcfb6 100644 --- a/B09_Estimation/B09_Estimation_UI_Shell.ts +++ b/B09_Estimation/B09_Estimation_UI_Shell.ts @@ -319,6 +319,13 @@ export function renderB09Estimation(root: HTMLElement): void { payload = next; const draft = projectId && !fromServer ? readState("b09-estimation", projectId) : null; user = draft ?? next.user ?? {}; + // 719-2 [업데이트]가 서고 사본을 다시 굳히면 블록 id 가 바뀜 → 사본 폴더(lib)로 다시 이음(짝 `Bill_Library.fix_keys`) + const libs: Row = next.inputs?.daega?.libs || {}; + const stale = ((user.bill?.rows || []) as Row[]).filter( + (r) => r?.ref?.tab === "dg" && libs[r.ref.lib] && r.ref.key !== libs[r.ref.lib], + ); + for (const r of stale) r.ref.key = libs[r.ref.lib]; + if (stale.length && draft && projectId) writeState("b09-estimation", user, projectId); recompute(); show(current); }; diff --git a/B09_Estimation/B09_Estimation_UI_Tabs_Bill_Fold.ts b/B09_Estimation/B09_Estimation_UI_Tabs_Bill_Fold.ts index 15dfbce47..f253eab01 100644 --- a/B09_Estimation/B09_Estimation_UI_Tabs_Bill_Fold.ts +++ b/B09_Estimation/B09_Estimation_UI_Tabs_Bill_Fold.ts @@ -228,7 +228,7 @@ function foldCard( body: [ table.root, createText( - "품 · 수량 · 단가 칸을 두 번 눌러 고침 · 색 칸 = 고친 칸(이 공사만 · 새 단가 판 · [재적용] 뒤에도 남음) · 우클릭 [품셈 값으로] · [저장] 때 서버가 다시 셈", + `품 · 수량 · 단가 칸을 두 번 눌러 고침 · 색 칸 = 고친 칸(이 공사만 · 새 단가 판 · [재적용] 뒤에도 남음) · 우클릭 [${backName((block.lines || []).find((l: Row) => l.sk))}으로] · [저장] 때 서버가 다시 셈`, "muted", ), ], From 328cb46d92fbb1eccacb8defeefd5358e38bff19 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 1 Nov 2026 16:38:24 +0900 Subject: [PATCH 3/3] =?UTF-8?q?fix(719-2):=20=EC=82=B0=EA=B7=BC=20?= =?UTF-8?q?=EA=B2=B0=EA=B3=BC=EB=8B=A8=EC=9C=84=20=EB=B0=B0=20=3D=20712-2?= =?UTF-8?q?=20=EB=8B=A8=EC=9C=84=20=ED=95=9C=20=EB=B2=8C(master=5Funit.per?= =?UTF-8?q?)=20=E2=80=94=20=EB=B0=9B=EC=9D=80=20712-2=20=EA=B0=80=20Store?= =?UTF-8?q?=5FCombo.=5FPER=20=EB=A5=BC=20=EA=B1=B7=EC=9D=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- B09_Estimation/B09_Estimation_Sangeun.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/B09_Estimation/B09_Estimation_Sangeun.py b/B09_Estimation/B09_Estimation_Sangeun.py index 9815913f2..dedd80e90 100644 --- a/B09_Estimation/B09_Estimation_Sangeun.py +++ b/B09_Estimation/B09_Estimation_Sangeun.py @@ -208,12 +208,11 @@ class _Prices: def _unit(row: dict) -> tuple[str, Decimal]: """결과단위 1 단위 — (단위, 배)(「원/10㎡」 = ㎡ · 10) · 알약 규칙(`combo_pills`) 그대로 · 줄 = 사본 단가 묶음 것(M01 다시 안 읽음 · 719-2).""" - per = combo_store._PER.findall(str(row.get("결과단위") or "")) + per = combo_store.mu.per(row.get("결과단위")) # 712-2 단위 한 벌 if not per: return "", Decimal(1) - times = per[0][0].replace(",", "") unit = " · ".join(dict.fromkeys(u for _, u in per)) - return unit, Decimal(times) if len(per) == 1 and times else Decimal(1) + return unit, Decimal(per[0][0]) if len(per) == 1 else Decimal(1) def unit_of(whole: Any, code: str) -> str: