diff --git a/M02_MasterTemplete/M02_Labor_Ref.py b/M02_MasterTemplete/M02_Labor_Ref.py index 12ecac51d..5c5715cbd 100644 --- a/M02_MasterTemplete/M02_Labor_Ref.py +++ b/M02_MasterTemplete/M02_Labor_Ref.py @@ -353,7 +353,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 aec4101b0..b9974542c 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() }, @@ -413,14 +305,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 21272f3c5..cff2e9cda 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts @@ -79,7 +79,7 @@ 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"], @@ -87,6 +87,7 @@ const TEXT = { Labor_LineApplied: ["반영(수량 곱함)", "Applied (× quantity)"], Labor_LineBase: ["기준", "Base"], Labor_FromLogic: ["원 로직", "Source logic"], + Lib_Precombo: ["사전조합", "Pre-combination"], Lib_System: ["마스터", "Master"], Lib_Company: ["회사", "Company"], Lib_Personal: ["개인", "Personal"], 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 } 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") diff --git a/resources/tester/test_m02_labor_ref.py b/resources/tester/test_m02_labor_ref.py index 913a9877c..d2951479c 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); +})();