From 04659a028fa4c2e37a29f73a0ef9b319ef982c18 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 12 Oct 2026 18:44:11 +0900 Subject: [PATCH] =?UTF-8?q?feat(b09):=20=EC=9A=B4=EB=B0=98=20=EB=8B=A8?= =?UTF-8?q?=EA=B0=80=20=EC=B0=BD=20=EC=99=BC=EC=AA=BD=20=ED=8F=AD=20?= =?UTF-8?q?=EC=A4=84=EC=9E=84=20=C2=B7=20=EB=8B=A8=EA=B0=80=EC=82=B0?= =?UTF-8?q?=EC=B6=9C=EA=B7=BC=EA=B1=B0=20=EB=B0=95=EC=8A=A4=EB=B3=B4?= =?UTF-8?q?=EA=B8=B0=20=EB=A8=B8=EB=A6=AC=20=EC=A4=84=20=3D=20=EC=9E=85?= =?UTF-8?q?=EB=A0=A5=EA=B0=92=20=C2=B7=20=EC=A4=91=EA=B0=84=EA=B0=92=20?= =?UTF-8?q?=EC=B9=B8=20(278-1=C2=B72)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 운반 단가 창 몸 두 칸 비율 — 왼쪽(옵션 값) 약 0.8 배 - 단가산출근거 [박스보기] 머리 줄 = 운반 창 ③ 칸과 같은 꼴(이름 · 값 오른쪽 굵게 · 식 작게) · 출처 = 칸 맨 위 - 줄 나눔 · 그리기 한 벌 — Haul_Steps headSteps 내보내 같이 씀 · 공용 묶음 카드 = 선택 몸 위 칸(top) - 호표 줄 · 총계 · 표 보기 · 다른 탭 카드 그대로 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Se7ytqbohmBQnfYpwYFnYf --- .../B09_Estimation_UI_Haul_Modal.ts | 3 +- .../B09_Estimation_UI_Haul_Steps.css | 8 ++ .../B09_Estimation_UI_Haul_Steps.ts | 75 +++++++---- .../B09_Estimation_UI_Tabs_Groups.ts | 35 ++++- .../B09_Estimation_UI_Tabs_Sangeun.ts | 1 + resources/tester/test_278_1_sg_box_steps.py | 120 ++++++++++++++++++ ui_template/ui_template_group_cards.ts | 4 + 7 files changed, 217 insertions(+), 29 deletions(-) create mode 100644 resources/tester/test_278_1_sg_box_steps.py diff --git a/B09_Estimation/B09_Estimation_UI_Haul_Modal.ts b/B09_Estimation/B09_Estimation_UI_Haul_Modal.ts index 650dd7d32..a932921ab 100644 --- a/B09_Estimation/B09_Estimation_UI_Haul_Modal.ts +++ b/B09_Estimation/B09_Estimation_UI_Haul_Modal.ts @@ -429,10 +429,11 @@ export function openHaulModal(o: HaulModalOpts): void { head.append(ok); body.append( linkBox, + // 278-1 왼쪽(옵션 값) 폭 = 예전 약 0.8 배(칸 · 단추는 줄 안에서 접힘) el("div", { attrs: { style: - "display:grid;grid-template-columns:minmax(300px,1fr) minmax(340px,1.4fr);gap:16px;flex:1;min-height:0", + "display:grid;grid-template-columns:minmax(260px,0.7fr) minmax(340px,1.4fr);gap:16px;flex:1;min-height:0", }, children: [left, right], }), diff --git a/B09_Estimation/B09_Estimation_UI_Haul_Steps.css b/B09_Estimation/B09_Estimation_UI_Haul_Steps.css index 75b5d2191..cf8826d2c 100644 --- a/B09_Estimation/B09_Estimation_UI_Haul_Steps.css +++ b/B09_Estimation/B09_Estimation_UI_Haul_Steps.css @@ -79,3 +79,11 @@ .b09-haulstep__bad { color: var(--color-danger); } +/* 278-2 B09 단가산출근거 박스보기 머리 줄 — 같은 줄 꼴 · 값 굵게 */ +.b09-sgsteps { + padding: 2px var(--spacing-4) var(--spacing-4); + border-bottom: 1px solid var(--color-border); +} +.b09-sgsteps .b09-haulstep__value { + font-weight: 700; +} diff --git a/B09_Estimation/B09_Estimation_UI_Haul_Steps.ts b/B09_Estimation/B09_Estimation_UI_Haul_Steps.ts index ed474d691..559d539af 100644 --- a/B09_Estimation/B09_Estimation_UI_Haul_Steps.ts +++ b/B09_Estimation/B09_Estimation_UI_Haul_Steps.ts @@ -5,6 +5,7 @@ * ③ 단가산출 = 서버 `logic-preview` 의 `blocks` 하나(277-2) — 머리 글 → 입력값 · 중간값(이름 · 값 크게, * 식 작게 흐리게) · 호표 줄(수량 · 금액) · 바닥 단가 + 노무 · 재료 · 경비(빈칸 = 까닭 빨강). * 셈은 서버 그대로 — 여기는 그리기만 · 테마 변수(`B09_Estimation_UI_Haul_Steps.css`). + * 머리 줄 나눔 · 그리기(`headSteps`) = B09 단가산출근거 박스보기도 같이 씀(278-2 · `Tabs_Groups`). * ========================================================================== */ import { el } from "@ui/ui_template_elements"; @@ -45,19 +46,22 @@ interface StepRow { value: string; formula?: string; bad?: boolean; + /** 머리 글 몇째 줄(`splitHead`) */ + at?: number; } const PART_TEXT: Record = { labor: "노무", material: "재료", expense: "경비" }; /** 값 — 천 단위 쉼표 · 빈 값 「—」 */ -export const num = (v: number | null | undefined, digits = 4): string => +export const stepNum = (v: number | null | undefined, digits = 4): string => v == null || !Number.isFinite(v) ? "—" : v.toLocaleString("ko-KR", { maximumFractionDigits: digits }); -const rowEl = (r: StepRow): HTMLElement => +const rowEl = (r: StepRow, ats?: number[]): HTMLElement => el("div", { className: "b09-haulstep__row", + ...(ats && r.at !== undefined ? { attrs: { "data-at": String(ats[r.at]) } } : {}), children: [ el("span", { text: r.name }), el("span", { @@ -68,9 +72,12 @@ const rowEl = (r: StepRow): HTMLElement => ], }); -const group = (label: string, rows: StepRow[]): HTMLElement[] => +const group = (label: string, rows: StepRow[], ats?: number[]): HTMLElement[] => rows.length - ? [el("div", { className: "b09-haulstep__group", text: label }), ...rows.map(rowEl)] + ? [ + el("div", { className: "b09-haulstep__group", text: label }), + ...rows.map((r) => rowEl(r, ats)), + ] : []; /** 칸 하나 — 제목 줄(제목 · 곁글) · 몸 · 바닥(굵게) */ @@ -91,7 +98,7 @@ function box(title: string, sub: string, body: HTMLElement[], foot: HTMLElement[ }); } -const footOf = (name: string, value: string, bad = false): HTMLElement[] => [ +const stepFoot = (name: string, value: string, bad = false): HTMLElement[] => [ el("span", { text: name }), el("span", { className: `b09-haulstep__value${bad ? " b09-haulstep__bad" : ""}`, text: value }), ]; @@ -106,14 +113,14 @@ export function noteBox(text: string, bad = false): HTMLElement { /** ① 거리 — 거리이정표 포장 · 비포장 → 운반거리 */ export function distBox(dist: Dist, input: Record): HTMLElement { const rows = [ - { name: "포장 L1", value: `${num(dist?.L1)} km` }, - { name: "비포장 L2", value: `${num(dist?.L2)} km` }, + { name: "포장 L1", value: `${stepNum(dist?.L1)} km` }, + { name: "비포장 L2", value: `${stepNum(dist?.L2)} km` }, ]; return box( "거리", "거리이정표", - rows.map(rowEl), - footOf("운반거리", `${num(cycleTime(input).L)} km`), + rows.map((r) => rowEl(r)), + stepFoot("운반거리", `${stepNum(cycleTime(input).L)} km`), ); } @@ -123,38 +130,44 @@ export function cycleBox(input: Record): HTMLElement { for (const k of Object.keys(v)) if (typeof input[k] === "number") v[k] = input[k] as number; const r = cycleTime(input); const rows: StepRow[] = [ - { name: "q", value: `${num(r.q)} ㎥`, formula: `T / rt × f = ${v.T} / ${v.rt} × ${v.f}` }, - { name: "n", value: `${num(r.n)} 회`, formula: `q / (qs × k) = ${r.q} / (${v.qs} × ${v.k})` }, + { name: "q", value: `${stepNum(r.q)} ㎥`, formula: `T / rt × f = ${v.T} / ${v.rt} × ${v.f}` }, + { + name: "n", + value: `${stepNum(r.n)} 회`, + formula: `q / (qs × k) = ${r.q} / (${v.qs} × ${v.k})`, + }, { name: "t1", - value: `${num(r.t1)} 분`, + value: `${stepNum(r.t1)} 분`, formula: `Cms × n / (60 × Es) = ${v.Cms} × ${r.n} / (60 × ${v.Es})`, }, { name: "t2", - value: `${num(r.t2)} 분`, + value: `${stepNum(r.t2)} 분`, formula: `(L1/V1 + L1/V2 + L2/V3 + L2/V4) × 60 = (${v.L1}/${v.V1} + ${v.L1}/${v.V2} + ${v.L2}/${v.V3} + ${v.L2}/${v.V4}) × 60`, }, { name: "Cm", - value: `${num(r.cm)} 분`, + value: `${stepNum(r.cm)} 분`, formula: `t1 + t2 + t3 + t4 + t5 + t6 = ${r.t1} + ${r.t2} + ${v.t3} + ${v.t4} + ${v.t5} + ${v.t6}`, }, ]; return box( "사이클시간", "건설품셈 8-2-8 덤프트럭", - rows.map(rowEl), - footOf("Cm", `${num(r.cm)} 분`), + rows.map((r) => rowEl(r)), + stepFoot("Cm", `${stepNum(r.cm)} 분`), ); } +type HeadOf = Pick; + /** 머리 글 → 입력값(「이름 : 뜻 = 값」) · 중간값(「이름 = 식 = 값」 · 「└」 = 앞 줄 식에 붙임) */ -export function splitHead(b: StepBlock): { inputs: StepRow[]; mids: StepRow[] } { +export function splitHead(b: HeadOf): { inputs: StepRow[]; mids: StepRow[] } { const inputs: StepRow[] = []; const mids: StepRow[] = []; let last: StepRow | null = null; - for (const line of b.head) { + for (const [at, line] of b.head.entries()) { if (line.startsWith(`${b.name} / `) || (b.source && line === `${b.source} 적용`)) continue; if (line.startsWith("└") && last) { last.formula = [last.formula, line].filter(Boolean).join("\n"); @@ -166,45 +179,53 @@ export function splitHead(b: StepBlock): { inputs: StepRow[]; mids: StepRow[] } if (colon >= 0 && (eq < 0 || colon < eq)) { const value = end > colon ? line.slice(end + 3) : line.slice(colon + 3); const note = end > colon ? line.slice(colon + 3, end) : ""; - inputs.push((last = { name: line.slice(0, colon), value, formula: note })); + inputs.push((last = { name: line.slice(0, colon), value, formula: note, at })); } else if (eq >= 0) { const formula = end > eq ? line.slice(eq + 3, end) : ""; - mids.push((last = { name: line.slice(0, eq), value: line.slice(end + 3), formula })); - } else mids.push((last = { name: line, value: "" })); + mids.push((last = { name: line.slice(0, eq), value: line.slice(end + 3), formula, at })); + } else mids.push((last = { name: line, value: "", at })); } return { inputs, mids }; } +/** 머리 글 → 「입력값」 · 「중간값」 묶음(③ 칸 몸 · 278-2 B09 단가산출근거 박스보기가 같이 씀) · + * ats = 머리 줄마다 부르는 쪽 줄 번호(주면 줄에 `data-at` — 누름 · 고름) */ +export function headSteps(b: HeadOf, ats?: number[]): HTMLElement[] { + const { inputs, mids } = splitHead(b); + return [...group("입력값", inputs, ats), ...group("중간값", mids, ats)]; +} + /** ③ 단가산출 칸 — 블록 하나(맨 위 = 「단가산출」 · 하위 = 「하위 산근」) */ export function logicBox(b: StepBlock, label = "단가산출"): HTMLElement { const unit = b.unit ? `원/${b.unit.replace(/^원\//, "")}` : "원"; - const { inputs, mids } = splitHead(b); const lines: StepRow[] = b.lines.map((ln) => { const parts = [ln.labor, ln.material, ln.expense]; const money = parts.some((p) => p == null) ? null : parts.reduce((a, p) => a! + p!, 0); return { name: ln.spec ? `${ln.name} ${ln.spec}` : ln.name, - value: ln.why ? `빈칸 — ${ln.why}` : `${num(ln.qty)} ${ln.unit} · ${num(money, 1)} 원`, + value: ln.why + ? `빈칸 — ${ln.why}` + : `${stepNum(ln.qty)} ${ln.unit} · ${stepNum(money, 1)} 원`, formula: (ln.text || []).join("\n"), bad: !!ln.why, }; }); const foot = b.price == null - ? footOf("단가", `빈칸 — ${b.why ?? ""}`, true) + ? stepFoot("단가", `빈칸 — ${b.why ?? ""}`, true) : [ - ...footOf("단가", `${num(b.price, 1)} ${unit}`), + ...stepFoot("단가", `${stepNum(b.price, 1)} ${unit}`), el("span", { className: "b09-haulstep__formula", text: Object.entries(b.parts) - .map(([p, v]) => `${PART_TEXT[p] ?? p} ${num(v, 1)}`) + .map(([p, v]) => `${PART_TEXT[p] ?? p} ${stepNum(v, 1)}`) .join(" · "), }), ]; return box( `${label} ${b.logic} ${b.name}`, [unit, b.source].filter(Boolean).join(" · "), - [...group("입력값", inputs), ...group("중간값", mids), ...group("호표", lines)], + [...headSteps(b), ...group("호표", lines)], foot, ); } diff --git a/B09_Estimation/B09_Estimation_UI_Tabs_Groups.ts b/B09_Estimation/B09_Estimation_UI_Tabs_Groups.ts index 568979122..641818e31 100644 --- a/B09_Estimation/B09_Estimation_UI_Tabs_Groups.ts +++ b/B09_Estimation/B09_Estimation_UI_Tabs_Groups.ts @@ -11,6 +11,8 @@ * 보기 · 가로 열 수 기억 = `b_page_state`(`b09-group-view` · `b09-group-cols` · 탭마다) · 못 읽으면 카드 · 2 * 보기 단추 · 가로 열 수(202-1) = 페이지 보조 탭 줄 오른쪽(`setTools`) · 안내 글 = 표 · 카드 판 맨 아래 * 안내 글 길면 한 줄 줄임표 + 공용 호버로 전문(200-1) + * 278-2 단가산출근거 박스 = 머리 줄(`|머리n`)을 운반 단가 창 ③ 칸 꼴로(`headSteps` 한 벌 · 몸 위 칸) · + * 출처 = 그 칸 맨 위 · 호표 줄 · 총계 = 그대로 · 묶음 자료(`tabGroups`) · 표는 안 바뀜 * ========================================================================== */ import type { DataRow, DataTable } from "@ui/ui_template_data_table"; @@ -26,6 +28,7 @@ import { type GroupParts, } from "@ui/ui_template_group_cards"; import { readState, writeState } from "../A00_Common/b_page_state"; +import { headSteps } from "./B09_Estimation_UI_Haul_Steps"; import { rowIndex, type Link, type TabId, type TabTable } from "./B09_Estimation_UI_Tabs"; export type GroupView = "cards" | "table"; @@ -152,6 +155,33 @@ export function tabGroups(tab: TabId, spec: TabTable): GroupCard[] { return cards; } +const HEAD_LINE = /\|머리\d+$/; + +/** 278-2 단가산출근거 박스 — 머리 줄을 몸 줄에서 빼 입력값 · 중간값 칸(`top`)으로 · 줄 번호 그대로 */ +export function sgHeadCards(cards: GroupCard[], rows: DataRow[]): GroupCard[] { + return cards.map((c) => { + const heads = c.lines.filter((l) => HEAD_LINE.test(str(rows[l.at]?.key))); + if (!heads.length) return c; + const source = str(rows[c.at]?.source); + const top = document.createElement("div"); + top.className = "b09-sgsteps"; + if (source) { + const cite = document.createElement("div"); + cite.className = "b09-haulstep__group"; + cite.textContent = source; + top.append(cite); + } + const head = heads.map((l) => str(rows[l.at].text)); + top.append( + ...headSteps( + { name: c.name, source, head }, + heads.map((l) => l.at), + ), + ); + return { ...c, top, lines: c.lines.filter((l) => !heads.includes(l)) }; + }); +} + /** 고른 호표의 표 줄 구간 [첫 줄, 끝 줄](키 `|` 앞머리가 같은 줄) · 없으면 null */ export function pickRange(rows: DataRow[], focus: string | undefined): [number, number] | null { if (focus === undefined) return null; @@ -245,7 +275,10 @@ export function renderGroupPanel(o: { if (view === "cards") { const first = o.spec.columns[0]?.key ?? ""; const cards = createGroupCards({ - groups: tabGroups(o.tab, o.spec), + groups: + o.tab === "sg" + ? sgHeadCards(tabGroups(o.tab, o.spec), o.spec.rows) + : tabGroups(o.tab, o.spec), columns: readGroupCols(o.tab), onColumns: (n) => rememberGroup("b09-group-cols", o.tab, n), onPick: (i) => o.goto(o.spec.link?.(i, first) ?? null), diff --git a/B09_Estimation/B09_Estimation_UI_Tabs_Sangeun.ts b/B09_Estimation/B09_Estimation_UI_Tabs_Sangeun.ts index fc51ab5d0..8eb60ecdf 100644 --- a/B09_Estimation/B09_Estimation_UI_Tabs_Sangeun.ts +++ b/B09_Estimation/B09_Estimation_UI_Tabs_Sangeun.ts @@ -94,6 +94,7 @@ export const buildSg: TabBuild = ({ sheets }): TabTable | string => { note_raw: b.spec_raw, name: b.name, unit: b.unit, + source: b.source ?? "", // 278-2 박스보기 머리 줄 나눔(출처 줄) }, null, true, diff --git a/resources/tester/test_278_1_sg_box_steps.py b/resources/tester/test_278_1_sg_box_steps.py new file mode 100644 index 000000000..7bd8fe191 --- /dev/null +++ b/resources/tester/test_278_1_sg_box_steps.py @@ -0,0 +1,120 @@ +"""278 B09 단가산출근거 [박스보기] 머리 줄 = 운반 단가 창 ③ 칸 꼴(입력값 · 중간값 · 이름 · 값 · 식) · +줄 나눔 · 그리기 한 벌(`Haul_Steps.headSteps`) · 호표 줄 · 총계 · 다른 탭 카드 그대로 · +운반 단가 창 왼쪽 폭(278-1).""" + +import json +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +if str(ROOT) not in sys.path: + sys.path.insert(0, str(ROOT)) + +from resources.tester.helper_b09_group_cards import COMMON, GROUPS, RAW, TABS, run # noqa: E402 + +B09 = "B09_Estimation/B09_Estimation_UI_" +STEPS = f"{B09}Haul_Steps.ts" + +# 공용 el 짝(가짜 DOM) — data-* = dataset +EL = r""" +const el = (tag, o = {}) => { + const n = document.createElement(tag); + if (o.className) n.className = o.className; + if (o.text !== undefined) n.textContent = o.text; + for (const [k, v] of Object.entries(o.attrs ?? {})) { + n.setAttribute(k, v); + if (k.startsWith("data-")) n.dataset[k.slice(5)] = v; + } + for (const c of o.children ?? []) n.append(c); + return n; +}; +""" + +SG = { + "key": "SG:A", "no": 1, "name": "골재운반", "unit": "㎥", "spec": "Cm=100", "spec_raw": "Cm=100", + "source": "산림품셈 10-3-1", + "head": ["골재운반 / ㎥", "산림품셈 10-3-1 적용", "qt : 15 ㎥", "Cm : 운반 사이클 = 100 분", + "작업량 = 골재운반 덤프트럭 시간당 작업량 = 8.1", "└ 결과 = 반올림(60 * 15 / 100, 3) = 8.1", + "덤프트럭 15ton 수량 = 1 / 작업량 = 1 / 8.1 = 0.1235 시간"], + "lines": [ + {"kind": "labor", "name": "보통인부", "ref": {"tab": "labor", "key": "LB2"}, "why": None, + "labor": 2979.8, "material": 0.0, "expense": 0.0, "text": ["보통인부 : 165,545 * 0.018 = 2,979.8"]}, + ], + "labor": 2979, "material": 0, "expense": 0, "sum": 2979, "why": None, "foot": "계 : 2,979", +} # fmt: skip + +PANEL = """ +const gone = []; +const o = { tab: TAB, spec, hint: null, goto: (l) => gone.push(l), redraw: () => {}, + setTools: () => {}, makeTable: () => ({ root: document.createElement('table'), focusRow: () => {} }) }; +const [view] = renderGroupPanel(o); +const top = view.querySelector('.b09-sgsteps'); +const rows = top ? top.querySelectorAll('.b09-haulstep__row').map((r) => + [r.dataset.at ?? null, ...r.children.map((c) => [c.className, c.textContent])]) : null; +const groups = top ? top.querySelectorAll('.b09-haulstep__group').map((g) => g.textContent) : null; +const body = view.querySelectorAll('tr').filter((tr) => tr.dataset.at !== undefined).map((tr) => [tr.dataset.at, tr.children[0].text]); +const foot = view.querySelector('.ui-group-card__foot')?.dataset.at ?? null; +if (top) top.querySelectorAll('.b09-haulstep__row')[2].click(); +const lit = view.querySelectorAll('.is-focus').map((e) => e.dataset.at); +console.log(JSON.stringify({ rows, groups, body, foot, gone, lit })); +""" + + +def _panel(tab, files, build, sheets, prelude=""): + js = ( + f"\nconst TAB = '{tab}';" + f"\nconst spec = {build}({{ sheets: {json.dumps(sheets, ensure_ascii=False)} }});" + PANEL + ) + return run([COMMON, TABS, *files, STEPS, GROUPS], js, EL + prelude) + + +def test_박스보기_머리_줄_나눔(): + out = _panel("sg", [f"{B09}Tabs_Sangeun.ts"], "buildSg", {"sg": [SG]}) + assert out["groups"] == ["산림품셈 10-3-1", "입력값", "중간값"] # 출처 = 칸 맨 위 + v, f = "b09-haulstep__value", "b09-haulstep__formula" + assert out["rows"] == [ + ["2", ["", "qt"], [v, "15 ㎥"]], # 표 줄 번호 그대로(머리 첫 줄 = 카드 머리 · 출처 줄 = 위) + ["3", ["", "Cm"], [v, "100 분"], [f, "운반 사이클"]], + ["4", ["", "작업량"], [v, "8.1"], + [f, "골재운반 덤프트럭 시간당 작업량\n└ 결과 = 반올림(60 * 15 / 100, 3) = 8.1"]], + ["6", ["", "덤프트럭 15ton 수량"], [v, "0.1235 시간"], [f, "1 / 작업량 = 1 / 8.1"]], + ] # fmt: skip + # 호표 줄 · 총계 그대로(머리 줄은 표 몸에서 빠짐) + assert out["body"] == [ + ["7", "보통인부 : 165,545 * 0.018 = 2,979.8노무"] + ] # 가짜 DOM 글 = 이름 + 꼬리표 + assert out["foot"] == "8" + # 머리 줄 누름 = 고름(is-focus) · 이동 없음(표와 같음) + assert out["lit"] == ["4"] and out["gone"] == [None] + + +def test_다른_탭_카드_그대로(): + dg = { + "id": "DG1", "no": 1, "name": "찰쌓기", "spec": "", "spec_raw": "", "unit": "m2", + "kind": "", "cite": "", + "lines": [{"kind": "labor", "key": "L1", "name": "석공", "qty": 0.072, "unit": "인", + "price": 1, "amount": 1, "ref_no": "", + "prices": {"labor": 1, "material": None, "expense": None}, + "amounts": {"labor": 1, "material": None, "expense": None}}], + "sum": 1, "labor": 1, "material": 0, "expense": 0, "blank": 0, "blank_names": [], + } # fmt: skip + out = _panel("dg", [f"{B09}Tabs_Labor.ts"], "buildDaegaBlocks", {"daega_blocks": [dg]}, RAW) + assert out["rows"] is None and out["body"] == [["1", "석공"]] + + +def test_한_벌_잇기(): + groups = (ROOT / GROUPS).read_text("utf-8") + assert 'import { headSteps } from "./B09_Estimation_UI_Haul_Steps";' in groups + assert 'indexOf(" : ")' not in groups # 줄 나눔 복사 없음 + assert "sgHeadCards(tabGroups(o.tab, o.spec), o.spec.rows)" in groups + steps = (ROOT / STEPS).read_text("utf-8") + assert "export function headSteps" in steps and "[...headSteps(b), ...group(" in steps + common = (ROOT / COMMON).read_text("utf-8") + assert "top?: HTMLElement | null;" in common and "if (g.top) body.append(g.top);" in common + sangeun = (ROOT / f"{B09}Tabs_Sangeun.ts").read_text("utf-8") + assert 'source: b.source ?? ""' in sangeun + + +def test_운반_창_왼쪽_폭(): + modal = (ROOT / f"{B09}Haul_Modal.ts").read_text("utf-8") + assert "grid-template-columns:minmax(260px,0.7fr) minmax(340px,1.4fr)" in modal diff --git a/ui_template/ui_template_group_cards.ts b/ui_template/ui_template_group_cards.ts index 3a7fb9b02..70327f2ba 100644 --- a/ui_template/ui_template_group_cards.ts +++ b/ui_template/ui_template_group_cards.ts @@ -8,6 +8,7 @@ * 접기 없음 — 묶음 모두 펼친 박스 · 같은 가로 줄 카드 = 높이 같음(200-1 · 합계 줄은 아래 붙음) * 줄 번호(`at`) = 부르는 쪽 표 줄 번호 — 누름 = `onPick(at)` · 이동해 온 줄 = 그 박스 붉은 외곽선 * (`is-pick`) + 스크롤만(248-1 빨강 하나) · 누름 = 빨강 풂 · 누른 줄만 고름(`is-focus`) + * 몸 위 칸(`top` · 선택) = 부르는 쪽이 그린 것 그대로(278-2) * ========================================================================== */ import "./ui_template_group_cards.css"; @@ -53,6 +54,8 @@ export interface GroupCard { sum?: number | null; lines: GroupLine[]; foot?: GroupFoot | null; + /** 몸 위 칸(부르는 쪽이 그림 · 줄 `data-at` = 누름 · 고름 같음 · 예 278-2 산근 머리 줄) */ + top?: HTMLElement | null; } export interface GroupCardsOptions { @@ -166,6 +169,7 @@ function card(g: GroupCard, o: Required ); const body = document.createElement("div"); body.className = "ui-group-card__body"; + if (g.top) body.append(g.top); if (g.lines.length) body.append(linesTable(g, o)); box.append(head, body); if (g.foot) box.append(footRow(g.foot, o.money));