From 7da1b2e94b353d1ae59deea64fe5ac201f4a901e Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 5 Oct 2026 19:26:11 +0900 Subject: [PATCH 1/6] =?UTF-8?q?=EF=BB=BFchore(sync):=20=EA=B3=B5=EC=9C=A0?= =?UTF-8?q?=20docs=20=EC=82=AC=EC=9A=A9=EC=9E=90=20=EC=9A=94=EA=B5=AC=20?= =?UTF-8?q?=EC=9B=90=EB=AC=B8=20=EB=8B=B4=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 Claude-Session: https://claude.ai/code/session_01MgChuAcyBqUkhHMTXomQC7 --- docs/raw/사용자요구/2026-10-05_수정요구.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/raw/사용자요구/2026-10-05_수정요구.md b/docs/raw/사용자요구/2026-10-05_수정요구.md index c8dbf6477..b0ef9909c 100644 --- a/docs/raw/사용자요구/2026-10-05_수정요구.md +++ b/docs/raw/사용자요구/2026-10-05_수정요구.md @@ -88,3 +88,9 @@ 8. 구조물 조합의 일위대가 상세화면에서의 로직테이블은 일위대가 목록표임. 여기서 행을 눌렀을때 하단에 상세 내역이 보였으면 함. 상세내역은 M01페이지의 단가산출 로직을 보면 시험계산 컨테이너 내부에 기준단위에 대한 테이블 리스트가 있음. 이걸 B02 구조물 조합의 일위대가 조합에 맞게 넣어주면 됨. 행을 선택했을때만 보여주면 되고 다른 행을 선택하면 닫히면 됨. 9. 구조물 조합의 일위대가 상세화면에 단가산출 로직 테이블의 이름 좌측 열을 추가하고 구분을 추가함. 구분은 출처로 단가산출인지, 일위대가 조합인지, 단가산출을 승격한 정보인지 표기 항목은 4음절로 표기, 일단 상세히 반영 가능성에 대해 상세히 검토후 계획서에 반영 + +## 오른쪽 패널 높이 (10-5 · 원문 그대로) + +하나씩 수정 내용에 대해 요청할께. M01 단가산출 로직 상세와 M02의 구조물 조합의 일위대가와 면적산출에는 우측 패널이 존재함. 이 우측 패널의 높이는 화면에 맞춰서 높이를 결정하게 했으면 함. 스크롤 동작시 움직임. + +추가로 너가 오해한게 있는데 구조물 조합의 일위대가 상세페이지에서 단가산출 로직 행을 선택했을때 나오는 상세정보를 해당행이 아래에 별도의 행으로 구분하여 접었다 폈다하면서 내용을 보줬으면 해. 별도의 공간에 M01처럼 보여달라는게 아니라 테이블에 맞춰서 정보를 넣어달라는 의미였어. From ab599ce3345e77ddca01f245cedb23d1618f067a Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 5 Oct 2026 19:31:01 +0900 Subject: [PATCH 2/6] =?UTF-8?q?fix(M02):=2051-2=20=EC=9D=BC=EC=9C=84?= =?UTF-8?q?=EB=8C=80=EA=B0=80=20=EC=A4=84=20=EC=83=81=EC=84=B8=20=E2=80=94?= =?UTF-8?q?=20=ED=91=9C=20=EC=95=84=EB=9E=98=20=EB=94=B0=EB=A1=9C=20?= =?UTF-8?q?=EC=83=81=EC=9E=90=20=EB=8C=80=EC=8B=A0=20=EA=B3=A0=EB=A5=B8=20?= =?UTF-8?q?=EC=A4=84=20=EB=B0=94=EB=A1=9C=20=EC=95=84=EB=9E=98=20=ED=95=98?= =?UTF-8?q?=EC=9C=84=20=ED=96=89(=EC=A0=91=EA=B3=A0=20=ED=8F=84)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 줄 누름 = 그 줄 아래 하위 행(같은 표 · 같은 칸 · 이름 앞 ▸/▾ · 한 줄만 · 다시 누름 = 닫힘) - 단가산출 · 승격 = 그 로직 재료 · 노무 · 경비 줄 · 사전조합 = 안 줄 · 금액 칸 = 기준 / 반영 두 줄 꼴 - 서버 승격 줄 = 1 단위로 나눔(하위 합 = 위 줄 기준) - 50-4 M01 시험계산 상자 · 안 쓰는 글 걷어냄 - 시험: 승격 줄 합 = 기준 · ui_checks 51-2 시나리오 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MgChuAcyBqUkhHMTXomQC7 --- M02_MasterTemplete/M02_Labor_Ref.py | 11 +- .../M02_MasterTemplete_Labor.css | 17 ++- .../M02_MasterTemplete_Labor_Detail.ts | 126 ++---------------- .../M02_MasterTemplete_Labor_Rows.ts | 62 ++++++++- .../M02_MasterTemplete_Labor_Text.ts | 6 +- resources/tester/test_m02_labor_ref.py | 4 +- resources/tester/ui_checks/51-2_줄_하위행.js | 109 +++++++++++++++ 7 files changed, 204 insertions(+), 131 deletions(-) create mode 100644 resources/tester/ui_checks/51-2_줄_하위행.js diff --git a/M02_MasterTemplete/M02_Labor_Ref.py b/M02_MasterTemplete/M02_Labor_Ref.py index 486b55002..c3b51b3ed 100644 --- a/M02_MasterTemplete/M02_Labor_Ref.py +++ b/M02_MasterTemplete/M02_Labor_Ref.py @@ -336,7 +336,16 @@ class Expand: "로직": line.get("로직"), "기준": {k: Decimal(str(v)) / times for k, v in base.items()} if base else None, "까닭": why, - "줄": got.get("줄"), # 원 로직 표(줄 누름 상세 · 50-3 ④) — 원 로직 결과단위 그대로 + # 원 로직 줄(줄 누름 하위 행 · 51-2) — 1 단위로 나눔(하위 합 = 기준) + "줄": [ + { + **x, + "수량": Decimal(str(x["수량"])) / times, + "금액": Decimal(str(x["금액"])) / times, + "비목": {k: Decimal(str(v)) / times for k, v in (x.get("비목") or {}).items()}, + } + for x in got.get("줄") or [] + ], } def _combo(self, doc: dict, trail: tuple[str, ...]) -> dict[str, Any]: diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor.css b/M02_MasterTemplete/M02_MasterTemplete_Labor.css index 3dd6d2015..b71f69c92 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor.css +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor.css @@ -136,9 +136,20 @@ color: var(--color-text-secondary); } -/* 줄 상세(50-4) — 표 아래 · 고른 줄만 */ -.m02-labor__linedet { - margin-top: var(--spacing-8); +/* 줄 접고 폄 표(51-2) · 고른 줄 하위 행 — 이름 들여쓰기 · 흐린 바탕 */ +.m02-labrow__fold { + margin-right: var(--spacing-4); + color: var(--color-text-secondary); +} + +.m02-labrow__sub td { + background: var(--color-bg); + font-size: var(--text-caption); +} + +.m02-labrow__sub .m02-labrow__subname { + padding-left: var(--spacing-24); + color: var(--color-text-secondary); } /* 별칭 칸(44-6) — 칸 안 글상자 · 누르면 바로 고침 */ diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts index 53c8f16de..9ea2f1cd6 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts @@ -2,7 +2,7 @@ * M02_MasterTemplete_Labor_Detail.ts * 일위대가 상세(39-3) — 구조물 열마다 하나(`labor/<열>.json`) · UI_Main 종류 `labor` 가 이 부품을 붙임 * (양식 길 읽기 · [저장] · [다시 읽기]는 UI_Main). 차례 = 정보(단위 · 본뜬) → 담은 로직 → 자재목록 → 다이어그램. - * · 담은 로직 줄 누름 = 고름(하이라이트 · 다시 그려도 유지 · 47-5) + 표 아래 그 줄 상세(50-4 · M01 시험계산 표) · + * · 담은 로직 줄 누름 = 고름(하이라이트 · 다시 그려도 유지 · 47-5) + 그 줄 바로 아래 하위 행(51-2 · `Labor_Rows`) · * 같은 줄 다시 누르면 닫힘 · [옵션] = 입력 모달(`Labor_Input`) · * [확인] = 화면 캐시만 · 저장은 [저장] * · 금액 = 서버 calc(기준 ③ · 최신 응답만 · 저장 안 함) · 고칠 때마다 뒤에서 다시 셈 @@ -18,9 +18,6 @@ import { ROUTES } from "@config/config_frontend"; import { navigateTo } from "../A00_Common/router"; import { setPendingLogicKey } from "../M01_MasterData/M01_MasterData_UI_Side"; import { createSourceView } from "../M01_MasterData/M01_MasterData_UI_LogicLab_Source"; -import { answerView } from "../M01_MasterData/M01_MasterData_UI_Logic_Calc"; -import type { CalcAnswer } from "../M01_MasterData/M01_MasterData_UI_Logic_Api"; -import { formatMoney } from "../M01_MasterData/M01_MasterData_UI_Logic_Money"; import type { Workbook } from "../A00_Common/spreadsheet/spreadsheet"; import { recalcWorkbook } from "../A00_Common/spreadsheet/spreadsheet_recalc"; import { @@ -32,15 +29,13 @@ import { type Layer, } from "./M02_MasterTemplete_Api_Fetch"; import { linkAreaCell } from "./M02_MasterTemplete_AreaCellPick"; -import { lineId, MONEY_KEYS, settle, type LaborRef } from "./M02_MasterTemplete_Labor_Calc"; +import { lineId, settle, type LaborRef } from "./M02_MasterTemplete_Labor_Calc"; import { areaLinks, calcLabor, fetchLogicInputs, normalizeLabor, type LaborCalc, - type LaborCalcLine, - type LaborCost, type LaborDoc, type LaborQty, type LaborRow, @@ -53,7 +48,7 @@ import { import { openLaborInput, type GoArea } from "./M02_MasterTemplete_Labor_Input"; import { openQtyModal } from "./M02_MasterTemplete_Labor_QtyModal"; import { openLogicPicker } from "./M02_MasterTemplete_Labor_Pick"; -import { kindOf, logicTable, matTable, qtyShort } from "./M02_MasterTemplete_Labor_Rows"; +import { logicTable, matTable, qtyShort } from "./M02_MasterTemplete_Labor_Rows"; import { tc } from "./M02_MasterTemplete_Labor_Text"; import { keepKnown } from "./M02_Labor_Values_Model"; import { pickMaterial } from "./M02_Material_Pick"; @@ -148,109 +143,6 @@ export function mountLaborDetail( const lineOf = (key: string) => calc?.줄.find((l) => lineId(l) === key); const matOf = (code: string) => calc?.자재.find((m) => m.코드 === code); - /** 고른 줄 상세(50-4) — 단가산출 = M01 시험계산 표(재료 · 노무 · 경비 줄 + 기준 합) · - * 단가승격 = 원 로직 표 + 원 로직 키 + 기준(1 단위) · 사전조합 = 안 줄 목록 한 층 + 기준 · - * 셋 다 아래 반영 합 · 고른 줄 없으면 비움 */ - const lineDet = el("div", { - className: "m02-labor__linedet", - attrs: { "data-labor-linedet": "" }, - }); - const sums = (title: string, money: Partial | null | undefined): HTMLElement[] => [ - el("h4", { text: title }), - el("dl", { - className: "m01-logic__sums", - children: MONEY_KEYS.flatMap((k) => [ - el("dt", { text: k === "계" ? tc("Prev_Sum") : k }), - el("dd", { - className: "m01-logic__money", - text: money ? formatMoney(money[k] ?? 0) : "—", - }), - ]), - }), - ]; - /** 사전조합 안 줄 — 구분 · 이름 · 수량 · 금액 넷(= 그 줄 1 단위 기준 × 수량) */ - const innerTable = (inner: NonNullable): HTMLElement => - el("table", { - className: "m01-logic__grid", - attrs: { "data-labor-inner": "" }, - children: [ - el("thead", { - children: [ - el("tr", { - children: [tc("Sub"), tc("Col_Name"), tc("Labor_Qty"), ...MONEY_KEYS].map((h) => - el("th", { text: h === "계" ? tc("Prev_Sum") : h }), - ), - }), - ], - }), - el("tbody", { - children: inner.map((x) => - el("tr", { - children: [ - el("td", { text: kindOf({ 로직: "", 참조: x.참조 }) }), - el("td", { - text: `${x.참조.키} ${x.이름 ?? ""}${x.까닭 ? ` · ${x.까닭}` : ""}`.trim(), - }), - el("td", { text: `${x.수량} ${x.단위 ?? ""}`.trim() }), - ...MONEY_KEYS.map((k) => - el("td", { - className: "m01-logic__money", - text: x.기준 ? formatMoney(x.기준[k] * x.수량) : "—", - }), - ), - ], - }), - ), - }), - ], - }); - function showLine(): void { - if (!picked) return void lineDet.replaceChildren(); - const row = cur.담은로직.find((r) => lineId(r) === picked); - const line = lineOf(picked); - const logic = logics.get(picked); - const name = row?.참조 ? (line?.이름 ?? "") : (logic?.이름 ?? ""); - const head = el("h4", { - text: tc("Labor_LineDetail").replace("{value}", `${row?.참조?.키 ?? picked} ${name}`.trim()), - }); - if (!line) - return void lineDet.replaceChildren( - head, - el("p", { className: "m01-logic__muted", text: fail ?? "…" }), - ); - const stop: CalcAnswer | null = line.까닭 ? { ok: false, reason: line.까닭 } : null; - const unit = `${tc("Labor_LineBase")} (1 ${line.분모 ?? ""})`.trim(); - const body: HTMLElement[] = !row?.참조 - ? answerView( - stop ?? { - ok: true, - lines: line.줄, - sums: line.기준 ?? undefined, - result: line.결과, - middle: {}, - }, - false, - (logic?.결과단위 ?? "").startsWith("원"), - ) - : line.안 - ? [ - innerTable(line.안), - ...(stop ? answerView(stop, false, true) : []), - ...sums(unit, line.기준), - ] - : [ - el("p", { - className: "m01-logic__muted", - text: `${tc("Labor_FromLogic")} ${line.원로직 ?? "—"}`, - }), - ...answerView(stop ?? { ok: true, lines: line.줄, middle: {} }, false, true), - ...sums(unit, line.기준), - ]; - // 반영 = 기준 × 수량 ÷ 배(표 아래 줄과 같음) · 수량 없으면 「—」 · 돈 아닌 로직은 없음 - const applied = line.기준 ? sums(tc("Labor_LineApplied"), line.반영) : []; - lineDet.replaceChildren(head, ...body, ...applied); - } - const drawDg = (): void => { if (opt.card || lib || !dgBox.open) return; // 카드 갈래 · 서고 모드엔 다이어그램 없음 const data = buildLaborDiagram({ @@ -273,7 +165,7 @@ export function mountLaborDetail( }; dgBox.addEventListener("toggle", drawDg); - const draw = (): void => { + const drawLogic = (): void => logicHost.replaceChildren( logicTable( { rows: cur.담은로직, logics, calc, fail, cells: cells(), selected: picked }, @@ -289,9 +181,9 @@ export function mountLaborDetail( } : { open: pick, option: openRow }, ), - lineDet, ); - showLine(); + const draw = (): void => { + drawLogic(); matHost.replaceChildren( matTable( { mats: cur.자재, calc, fail, cells: cells() }, @@ -412,14 +304,12 @@ export function mountLaborDetail( [cur.담은로직[i], cur.담은로직[j]] = [cur.담은로직[j], cur.담은로직[i]]; changed(); } - /** 줄 고름(47-5) — 하이라이트(표만 · 다이어그램 안 다시 그림) + 줄 상세(50-4) + 원문 패널(47-4) · + /** 줄 고름(47-5) — 하이라이트 + 하위 행(51-2 · 표만 다시 그림 · 다이어그램 안 다시 그림) + 원문 패널(47-4) · * 고른 줄 다시 누름 = 닫음 · 화면 캐시 안 바뀜 */ function pick(i: number): void { const key = lineId(cur.담은로직[i]) ?? ""; picked = picked === key ? null : key; - for (const tr of logicHost.querySelectorAll("[data-labor-logic],[data-labor-opt]")) - tr.classList.toggle("is-selected", (tr.dataset.laborLogic ?? tr.dataset.laborOpt) === picked); - showLine(); + drawLogic(); showSource(); } function removeRow(i: number): void { diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Rows.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Rows.ts index f57b1747f..16bb85129 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Rows.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Rows.ts @@ -7,7 +7,7 @@ * 자재목록 = 코드 · 이름 · 규격 · 수량산출 · 단위 · 가격 다섯(기준 칸 강조) · 기준단가 / 반영 금액(43-3). * 금액은 calc 답(없으면 「…」). * 줄 누름 · 줄 단추(위로 · 아래로 · 옵션 · 삭제 · 묶음 `ui-btn-group`)는 `on` 이 있을 때만(보기 갈래 = 없음) · - * 상세는 줄 누름 = 고름(`selected` 줄 하이라이트) · 넣을 값 모달은 [옵션](47-5). + * 상세는 줄 누름 = 고름(`selected` 줄 하이라이트 · 이름 앞 ▸/▾ · 그 줄 바로 아래 하위 행 · 51-2) · 넣을 값 모달은 [옵션](47-5). * ========================================================================== */ import { createButton, el } from "@ui/ui_template_elements"; @@ -228,6 +228,60 @@ export interface LogicTableData { selected?: string | null; } +/** 고른 줄 하위 행(51-2 · 같은 표 · 같은 칸) — 단가산출 · 단가승격 = 그 로직 재료 · 노무 · 경비 줄 · + * 사전조합 = 안 줄 · 금액 칸 = 위 기준(위 줄 기준과 같은 바탕) / 아래 반영(× 위 줄 수량 ÷ 배) · + * 하위 합 = 위 줄 금액 · 줄이 없으면 빈 한 줄(「…」 · 까닭 · 「—」) */ +function subRows(line: LaborCalcLine | undefined, wait: boolean, tools: boolean): HTMLElement[] { + const times = line?.수량 === null || line?.수량 === undefined ? null : line.수량 / (line.배 || 1); + const items = line?.안 + ? line.안.map((x) => ({ + name: `${x.참조.키} ${x.이름 ?? ""}${x.까닭 ? ` · ${x.까닭}` : ""}`.trim(), + qty: x.수량, + unit: x.단위 ?? "", + cost: x.기준 ? Object.fromEntries(COSTS.map((c) => [c, x.기준![c] * x.수량])) : null, + })) + : (line?.줄 ?? []).map((x) => ({ + name: x.출처 ? `${x.이름} (${x.출처.replace(/[A-Z]{2}\d{6}\./g, "")})` : x.이름, + qty: x.수량, + unit: x.단위, + cost: { ...x.비목, 계: x.금액 } as Record, + })); + const blank = (className: string): HTMLElement => el("td", { className, text: "" }); + const none = wait ? "…" : (line?.까닭 ?? "—"); + return (items.length ? items : [null]).map((x) => + el("tr", { + className: "m02-labrow__sub", + attrs: { "data-labor-sub": "" }, + children: [ + blank("m02-labrow__no"), + blank("m02-labrow__kind"), + el("td", { + className: "m02-labrow__name m02-labrow__subname", + text: x?.name ?? none, + attrs: { title: x?.name ?? none }, + }), + blank("m02-labrow__alias"), + el("td", { + className: "m02-labrow__qty", + text: x ? num(x.qty) : "", + attrs: { "data-label": tc("Labor_QtyCol") }, + }), + el("td", { className: "m02-labrow__unit", text: x?.unit ?? "" }), + ...COSTS.map((c) => { + const base = x?.cost ? Number(x.cost[c] ?? 0) : null; + return moneyCell( + base, + base === null || times === null ? null : base * times, + wait, + tc(HEAD[c]), + ); + }), + ...(tools ? [blank("")] : []), + ], + }), + ); +} + /** 담은 로직 표 — 보기 갈래는 `on` 없이 */ export function logicTable(data: LogicTableData, on?: RowHandlers): HTMLElement { const wait = !data.calc && !data.fail; @@ -269,6 +323,7 @@ export function logicTable(data: LogicTableData, on?: RowHandlers): HTMLElement el("td", { className: "m02-labrow__name", children: [ + ...(on ? [el("span", { className: "m02-labrow__fold", text: sel ? "▾" : "▸" })] : []), el("span", { className: "m02-labrow__key", text: row.참조?.키 ?? row.로직 }), el("strong", { text: (row.참조 ? line?.이름 : logic?.이름) ?? row.참조?.키 ?? row.로직, @@ -319,7 +374,8 @@ export function logicTable(data: LogicTableData, on?: RowHandlers): HTMLElement ], }); if (on) tr.addEventListener("click", () => on.open(i)); - if (!note) return [tr]; + const subs = sel ? subRows(line, wait, tools) : []; + if (!note) return [tr, ...subs]; // 옵션 값 줄(44-6) — 이름 ~ 합계 칸에 걸친 한 줄(말줄임 · 전체 글은 title) · 행 높이 안 커짐 const opt = el("tr", { className: (on ? "m02-labrow__optrow is-open" : "m02-labrow__optrow") + sel, @@ -335,7 +391,7 @@ export function logicTable(data: LogicTableData, on?: RowHandlers): HTMLElement ], }); if (on) opt.addEventListener("click", () => on.open(i)); - return [tr, opt]; + return [tr, opt, ...subs]; }); const sum = (part: Partial | undefined, c: (typeof COSTS)[number]): number | null => part?.[c] ?? null; diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts index 48ed9576c..04c8db49a 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts @@ -84,14 +84,10 @@ const TEXT = { Labor_BasePrice: ["기준 단가", "Base price"], Labor_AppliedSum: ["반영 금액", "Applied amount"], Labor_Calc_Fail: ["계산 못 함", "Calculation failed"], - // 구분 · 줄 상세(50-4) + // 구분(50-4) Kind_Logic: ["단가산출", "Logic"], Kind_Promote: ["단가승격", "Promoted"], Kind_Combo: ["일위조합", "Combination"], - Labor_LineDetail: ["상세 — {value}", "Detail — {value}"], - Labor_LineApplied: ["반영(수량 곱함)", "Applied (× quantity)"], - Labor_LineBase: ["기준", "Base"], - Labor_FromLogic: ["원 로직", "Source logic"], Lib_Precombo: ["사전조합", "Pre-combination"], Lib_System: ["마스터", "Master"], Lib_Company: ["회사", "Company"], diff --git a/resources/tester/test_m02_labor_ref.py b/resources/tester/test_m02_labor_ref.py index 176e4c6af..2fdacb49e 100644 --- a/resources/tester/test_m02_labor_ref.py +++ b/resources/tester/test_m02_labor_ref.py @@ -170,7 +170,9 @@ def test_승격_줄_상세는_원_로직_키_표_사전조합은_안_줄만(worl doc = labor.clean({"열": "rv", "담은로직": [{"참조": {**SYS, "키": k}} for k in (sp, sc)]}) promote, combo = labor.calc(doc, {})["줄"] assert promote["원로직"] == TEN and promote["줄"] - assert sum(x["금액"] for x in promote["줄"]) == pytest.approx(float(_base(TEN)["계"])) + assert sum(x["금액"] for x in promote["줄"]) == pytest.approx( + promote["기준"]["계"] + ) # 1 단위(51-2) assert "줄" not in combo and [x["참조"]["키"] for x in combo["안"]] == [sp] diff --git a/resources/tester/ui_checks/51-2_줄_하위행.js b/resources/tester/ui_checks/51-2_줄_하위행.js new file mode 100644 index 000000000..1b20837d9 --- /dev/null +++ b/resources/tester/ui_checks/51-2_줄_하위행.js @@ -0,0 +1,109 @@ +/* 51-2 — 일위대가 단가산출 로직 표 줄 누름 = 그 줄 바로 아래 하위 행(같은 표 · 같은 칸 · 접고 폄) — 카드 · 상세 두 곳 + * 시작 화면: M02 마스터 템플릿 · 마스터 층(#/m02-master-template?open=구조물조합&root=master) · 시스템 관리자 · + * 나무에 item1(구조물 조합 · 일위대가 로직 둘 이상)이 있음 + * 부르는 법: orca eval --expression "$(tr '\n' ' ' < 이 파일)" (async 식 · JSON 글을 돌려줌) + * 하는 일: item1 카드 표 줄마다 누름(하위 행 셈) · 같은 줄 다시 → 일위대가 카드 [수정]으로 상세 → 같은 차례 → item1 다시 + * 기대 수치(카드 · 상세 같음): + * - lines[*].subs ≥ 1 · lines[*].cells = head(칸 수 = 머리 칸 수) · lines[*].next = true(고른 줄 바로 아래) + * - lines[*].base ≈ lines[*].subBase · lines[*].applied ≈ lines[*].subApplied(반올림 차 ≤ 하위 행 수) + * - lines[*].open = 1 · lines[*].arrow = "▾"(한 줄만 열림) + * - again.subs = 0 · again.selected = 0(같은 줄 다시 = 닫힘) · box = 0(따로 상자 없음) + */ +(async () => { + const w = (ms) => new Promise((r) => setTimeout(r, ms)); + const until = async (fn, ms = 8000) => { + for (let t = 0; t < ms; t += 150) { + const v = fn(); + if (v) return v; + await w(150); + } + return null; + }; + const n = (s) => Number(String(s ?? "").replace(/[^0-9.-]/g, "")) || 0; + const rowsOf = (t) => [...t.querySelectorAll("tbody tr[data-labor-logic]")]; + const settled = (get) => + until(() => { + const t = get(); + return t && !/…/.test(t.querySelector("tbody").innerText) ? t : null; + }, 10000); + const total = (tr, part) => { + const tds = tr.querySelectorAll(".m02-labrow__money"); + return n(tds[tds.length - 1].querySelector(part).innerText); + }; + const measure = async (get) => { + const t = await settled(get); + const head = t.querySelectorAll("thead th").length; + const keys = rowsOf(t).map((r) => r.getAttribute("data-labor-logic")); + const click = async (key) => { + get().querySelector(`tr[data-labor-logic="${key}"] .m02-labrow__name`).click(); + await w(300); + }; + const lines = []; + for (const key of keys) { + await click(key); + const tb = get(); + const tr = tb.querySelector(`tr[data-labor-logic="${key}"]`); + const subs = [...tb.querySelectorAll("tbody tr[data-labor-sub]")]; + /* 고른 줄 바로 아래 — 옵션 값 줄(있으면) 다음이 하위 행 */ + let at = tr.nextElementSibling; + if (at?.hasAttribute("data-labor-opt")) at = at.nextElementSibling; + lines.push({ + key, + subs: subs.length, + next: at === subs[0], + cells: [ + ...new Set(subs.map((s) => [...s.children].reduce((a, c) => a + (c.colSpan || 1), 0))), + ], + base: total(tr, ".m02-labrow__base"), + subBase: Math.round(subs.reduce((a, s) => a + total(s, ".m02-labrow__base"), 0)), + applied: total(tr, ".m02-labrow__applied"), + subApplied: Math.round(subs.reduce((a, s) => a + total(s, ".m02-labrow__applied"), 0)), + open: [...tb.querySelectorAll(".m02-labrow__fold")].filter((f) => f.innerText === "▾") + .length, + arrow: tr.querySelector(".m02-labrow__fold")?.innerText, + names: subs.map((s) => s.querySelector(".m02-labrow__subname").innerText).slice(0, 4), + }); + } + await click(keys[keys.length - 1]); + const again = { + subs: get().querySelectorAll("tbody tr[data-labor-sub]").length, + selected: rowsOf(get()).filter((r) => r.classList.contains("is-selected")).length, + }; + return { head, lines, again, box: document.querySelectorAll("[data-labor-linedet]").length }; + }; + const node = await until(() => document.querySelector('[data-node="i|item1"]')); + if (!node) return JSON.stringify({ error: "item1 없음 — 폴더를 펼쳐 둘 것" }); + node.click(); + const card = await until(() => + [...document.querySelectorAll(".m02-struct__card")].find((c) => + c.querySelector("table.m02-labrow tbody tr[data-labor-logic]"), + ), + ); + if (!card) return JSON.stringify({ error: "일위대가 카드 표 없음" }); + await w(1500); + const out = { card: await measure(() => card.querySelector("table.m02-labrow")) }; + const edit = [...card.querySelectorAll(".m02-struct__card-tools button")].find( + (b) => b.innerText.trim() === "수정", + ); + if (!edit) return JSON.stringify({ ...out, error: "일위대가 [수정] 없음" }); + edit.click(); + const dt = await until(() => + [...document.querySelectorAll("table.m02-labrow")].find( + (t) => !t.closest(".m02-struct__card") && t.querySelector("tbody tr[data-labor-logic]"), + ), + ); + if (!dt) return JSON.stringify({ ...out, error: "상세 표 없음" }); + await w(2000); + out.detail = await measure(() => + [...document.querySelectorAll("table.m02-labrow")].find((t) => !t.closest(".m02-struct__card")), + ); + /* 카드 화면으로 돌아감(고름만 했으니 버림 물음 없음 — 뜨면 확인) */ + document.querySelector('[data-node="i|item1"]')?.click(); + await w(800); + [...document.querySelectorAll(".ui-modal button, dialog button")] + .find((b) => /확인|버림|떠나기/.test(b.innerText)) + ?.click(); + await w(1200); + out.back = !!document.querySelector(".m02-struct__card table.m02-labrow"); + return JSON.stringify(out); +})(); From 4a5c2bb2481ac037cbb1f8a54d37b554c8bfed39 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 5 Oct 2026 19:31:09 +0900 Subject: [PATCH 3/6] =?UTF-8?q?chore(sync):=20=EA=B3=B5=EC=9C=A0=20docs=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=EC=9E=90=20=EC=9A=94=EA=B5=AC=20=EC=9B=90?= =?UTF-8?q?=EB=AC=B8=20=EB=8B=B4=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 Claude-Session: https://claude.ai/code/session_01MgChuAcyBqUkhHMTXomQC7 --- docs/raw/사용자요구/2026-10-05_수정요구.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/raw/사용자요구/2026-10-05_수정요구.md b/docs/raw/사용자요구/2026-10-05_수정요구.md index b0ef9909c..b4a22b5f5 100644 --- a/docs/raw/사용자요구/2026-10-05_수정요구.md +++ b/docs/raw/사용자요구/2026-10-05_수정요구.md @@ -94,3 +94,9 @@ 하나씩 수정 내용에 대해 요청할께. M01 단가산출 로직 상세와 M02의 구조물 조합의 일위대가와 면적산출에는 우측 패널이 존재함. 이 우측 패널의 높이는 화면에 맞춰서 높이를 결정하게 했으면 함. 스크롤 동작시 움직임. 추가로 너가 오해한게 있는데 구조물 조합의 일위대가 상세페이지에서 단가산출 로직 행을 선택했을때 나오는 상세정보를 해당행이 아래에 별도의 행으로 구분하여 접었다 폈다하면서 내용을 보줬으면 해. 별도의 공간에 M01처럼 보여달라는게 아니라 테이블에 맞춰서 정보를 넣어달라는 의미였어. + +## 서고 · 고르기 창 (10-5 · 원문 그대로) + +1. 로직고르기 모달창에서 찾기 버튼은 필요없어 보임. 다른 찾기 기능을 보면 없어도 될것같아. 그리고 단가 승격과 사전 조합을 별도의 버튼으로 활성화하는데 이건 그냥 구분정보를 보여줘서 선택을 유도해도 되지 않을까? +2. 일위대가 조합 컨테이너에는 새로, 본떠 만들기, 지우기 버튼은 필요없고 단가산출 승격 이라는 항목과 일위대가 사전조합이라는 항목 두게만 있으면 될것같음. 그리고 해당 항목을 누르면 상세에 테이블형태로 리스트 들을 보여주고 거기에 생성, 복사, 삭제 버튼이 있으면 됨. +3. 사전 조합도 동일함 From c8cb0dab58ca37efeda2af9a4521f9ea46eb2a92 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 5 Oct 2026 19:44:15 +0900 Subject: [PATCH 4/6] =?UTF-8?q?chore(plan):=2010-5=20=EC=84=9C=EA=B3=A0=20?= =?UTF-8?q?=C2=B7=20=EA=B3=A0=EB=A5=B4=EA=B8=B0=20=EC=B0=BD=20=C2=B7=20?= =?UTF-8?q?=EB=8B=A8=EC=9C=84=20=EB=8B=A8=EC=B6=94=20=C2=B7=20=EC=82=AC?= =?UTF-8?q?=EC=A0=84=EC=A1=B0=ED=95=A9=20=EC=A4=84=20=EC=9A=94=EA=B5=AC=20?= =?UTF-8?q?=EC=9B=90=EB=AC=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FJqWQpoB1RE7hiaQ2wH8qV --- docs/raw/사용자요구/2026-10-05_수정요구.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/raw/사용자요구/2026-10-05_수정요구.md b/docs/raw/사용자요구/2026-10-05_수정요구.md index bfc87cfca..50ae3392e 100644 --- a/docs/raw/사용자요구/2026-10-05_수정요구.md +++ b/docs/raw/사용자요구/2026-10-05_수정요구.md @@ -92,3 +92,15 @@ ## 오른쪽 패널 높이 (10-5 · 원문 그대로) 하나씩 수정 내용에 대해 요청할께. M01 단가산출 로직 상세와 M02의 구조물 조합의 일위대가와 면적산출에는 우측 패널이 존재함. 이 우측 패널의 높이는 화면에 맞춰서 높이를 결정하게 했으면 함. 스크롤 동작시 움직임. + +추가로 너가 오해한게 있는데 구조물 조합의 일위대가 상세페이지에서 단가산출 로직 행을 선택했을때 나오는 상세정보를 해당행이 아래에 별도의 행으로 구분하여 접었다 폈다하면서 내용을 보줬으면 해. 별도의 공간에 M01처럼 보여달라는게 아니라 테이블에 맞춰서 정보를 넣어달라는 의미였어. + +## 서고 · 고르기 창 (10-5 · 원문 그대로) + +1. 로직고르기 모달창에서 찾기 버튼은 필요없어 보임. 다른 찾기 기능을 보면 없어도 될것같아. 그리고 단가 승격과 사전 조합을 별도의 버튼으로 활성화하는데 이건 그냥 구분정보를 보여줘서 선택을 유도해도 되지 않을까? +2. 일위대가 조합 컨테이너에는 새로, 본떠 만들기, 지우기 버튼은 필요없고 단가산출 승격 이라는 항목과 일위대가 사전조합이라는 항목 두게만 있으면 될것같음. 그리고 해당 항목을 누르면 상세에 테이블형태로 리스트 들을 보여주고 거기에 생성, 복사, 삭제 버튼이 있으면 됨. +3. 사전 조합도 동일함 + +단위를 선택하게 하는 항목들 중 선택 후 나오는 단위 표기 버튼이 너무 작음 저장같은 버튼과 같은 크기로 변경 + +사전조합에 상세에서 보여지는 단가산출 로직의 선택 기능이 없음 From e4547e4548bdba71c69d5102440b1ba0949db40b Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 5 Oct 2026 19:44:15 +0900 Subject: [PATCH 5/6] =?UTF-8?q?chore(M02):=20=EC=82=AC=EC=9A=A9=EC=9E=90?= =?UTF-8?q?=20=ED=8E=B8=EC=A7=91=20=E2=80=94=20=EB=A7=88=EC=8A=A4=ED=84=B0?= =?UTF-8?q?=20=EC=84=9C=EA=B3=A0=20=EB=8B=A8=EA=B0=80=EC=8A=B9=EA=B2=A9=20?= =?UTF-8?q?=C2=B7=20=EC=82=AC=EC=A0=84=EC=A1=B0=ED=95=A9=20=C2=B7=20?= =?UTF-8?q?=EA=B5=AC=EC=A1=B0=EB=AC=BC=20=EC=A7=91=EA=B3=84=ED=91=9C(?= =?UTF-8?q?=ED=99=94=EB=A9=B4=EC=97=90=EC=84=9C=20=EB=A7=8C=EB=93=A6=20?= =?UTF-8?q?=C2=B7=20=EA=B3=A0=EC=B9=A8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FJqWQpoB1RE7hiaQ2wH8qV --- resources/master_template/precombo/SC-000001.json | 8 ++++++++ resources/master_template/promote/SP-000001.json | 14 ++++++++++++++ resources/master_template/table/구조물집계표.json | 4 ++-- resources/master_template/구조물집계표조합.json | 15 ++++++--------- 4 files changed, 30 insertions(+), 11 deletions(-) create mode 100644 resources/master_template/precombo/SC-000001.json create mode 100644 resources/master_template/promote/SP-000001.json diff --git a/resources/master_template/precombo/SC-000001.json b/resources/master_template/precombo/SC-000001.json new file mode 100644 index 000000000..c24f868cc --- /dev/null +++ b/resources/master_template/precombo/SC-000001.json @@ -0,0 +1,8 @@ +{ + "종류": "사전조합", + "키": "SC-000001", + "이름": "테스트 사전조합", + "단위": null, + "담은로직": [], + "본뜬": null +} diff --git a/resources/master_template/promote/SP-000001.json b/resources/master_template/promote/SP-000001.json new file mode 100644 index 000000000..5f0020627 --- /dev/null +++ b/resources/master_template/promote/SP-000001.json @@ -0,0 +1,14 @@ +{ + "종류": "단가승격", + "키": "SP-000001", + "이름": "돌쌓기 메쌓기(인력)", + "단위": "원/㎡", + "배": 1, + "담은로직": [ + { + "로직": "GF000219", + "입력값": {} + } + ], + "본뜬": null +} diff --git a/resources/master_template/table/구조물집계표.json b/resources/master_template/table/구조물집계표.json index 18996f1df..cb8d516a9 100644 --- a/resources/master_template/table/구조물집계표.json +++ b/resources/master_template/table/구조물집계표.json @@ -103,11 +103,11 @@ "자재조합": null, "경로": [ "기슭막이", - "ㄴㄴ" + "ㄷ2ㄷ" ], "머리": [ "기슭막이", - "ㄴㄴ", + "ㄷ2ㄷ", null, null, null, diff --git a/resources/master_template/구조물집계표조합.json b/resources/master_template/구조물집계표조합.json index 119f6b4be..2afe0f7a5 100644 --- a/resources/master_template/구조물집계표조합.json +++ b/resources/master_template/구조물집계표조합.json @@ -75,10 +75,9 @@ "분류3": "aaa", "단위": "m", "일위대가": "UA000008", - "자재조합": null, "도번": "SD-000001", + "일위대가번": "SU-000001", "면적번": "SA-000001", - "재료번": "SM-000001", "바인딩": null, "펼침": null }, @@ -100,10 +99,9 @@ "분류3": "aaa", "단위": "b", "일위대가": null, - "자재조합": null, "도번": "SD-000002", + "일위대가번": "SU-000002", "면적번": "SA-000002", - "재료번": "SM-000002", "바인딩": null, "펼침": null }, @@ -112,19 +110,18 @@ "열": "item3", "경로": [ "기슭막이", - "ㄴㄴ" + "ㄷ2ㄷ" ], - "이름": "기슭막이 ㄴㄴ", + "이름": "기슭막이 ㄷ2ㄷ", "종류": "기슭막이", - "분류1": "ㄴㄴ", + "분류1": "ㄷ2ㄷ", "분류2": null, "분류3": null, "단위": null, "일위대가": "UA000001", - "자재조합": null, "도번": "SD-000003", + "일위대가번": "SU-000003", "면적번": "SA-000003", - "재료번": "SM-000003", "바인딩": null, "펼침": null } From 7f3506e65e10528584734e0a30fab368b89bb627 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 5 Oct 2026 19:46:13 +0900 Subject: [PATCH 6/6] =?UTF-8?q?test(M02):=20=EC=B9=B4=EB=93=9C=20=EC=8B=9C?= =?UTF-8?q?=ED=97=98=20=EA=B8=B0=EB=8C=80=20=EA=B8=80=20=E2=80=94=2051-6?= =?UTF-8?q?=20=EC=82=AC=EC=A0=84=EC=A1=B0=ED=95=A9=20=EC=A4=84=20=EA=B3=A0?= =?UTF-8?q?=EB=A5=B4=EA=B8=B0(addRef)=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FJqWQpoB1RE7hiaQ2wH8qV --- resources/tester/test_m02_card_shell.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/resources/tester/test_m02_card_shell.py b/resources/tester/test_m02_card_shell.py index 57faaba79..3ffaa76b1 100644 --- a/resources/tester/test_m02_card_shell.py +++ b/resources/tester/test_m02_card_shell.py @@ -80,7 +80,9 @@ def test_일위대가_카드는_상세_카드_갈래(): detail = _text("M02_MasterTemplete_Labor_Detail.ts") assert "...(opt.card ? {} : { move })" in detail # 순서 단추 없음 · 빼기는 그대로 # 서고 모드(50-3 · lib)도 더하기 · 다이어그램 없음 - assert "opt.card || lib ? null : addLogic" in detail + assert ( + "opt.card || promote ? null : lib ? addRef : addLogic" + ) # 51-6 사전조합 = 줄 고르기 in detail assert "...(opt.card || lib ? [] : [dgBox])," in detail assert "opt.card ? null : addMat" in detail # 49-6 — 카드엔 [자재 추가] 없음 · 상세는 그대로 rows = _text("M02_MasterTemplete_Labor_Rows.ts")