feat(b09): 운반 단가 창 왼쪽 폭 줄임 · 단가산출근거 박스보기 머리 줄 = 입력값 · 중간값 칸 (278-1·2)
- 운반 단가 창 몸 두 칸 비율 — 왼쪽(옵션 값) 약 0.8 배 - 단가산출근거 [박스보기] 머리 줄 = 운반 창 ③ 칸과 같은 꼴(이름 · 값 오른쪽 굵게 · 식 작게) · 출처 = 칸 맨 위 - 줄 나눔 · 그리기 한 벌 — Haul_Steps headSteps 내보내 같이 씀 · 공용 묶음 카드 = 선택 몸 위 칸(top) - 호표 줄 · 총계 · 표 보기 · 다른 탭 카드 그대로 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Se7ytqbohmBQnfYpwYFnYf
This commit is contained in:
@@ -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],
|
||||
}),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<string, string> = { 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<string, unknown>): 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<string, unknown>): 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<StepBlock, "name" | "source" | "head">;
|
||||
|
||||
/** 머리 글 → 입력값(「이름 : 뜻 = 값」) · 중간값(「이름 = 식 = 값」 · 「└」 = 앞 줄 식에 붙임) */
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
@@ -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<Pick<GroupCardsOptions, "money" | "qty">
|
||||
);
|
||||
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));
|
||||
|
||||
Reference in New Issue
Block a user