feat(b09): 설계내역서 수량 칸 · 손 줄 칸 고침 · 우클릭 메뉴 연결 · 시험(166-2)
- UI_Tabs_Bill — 항목 수량 덮어쓰기(빈칸 = 자동 · 풍선 자동 값) · 손 줄 명칭 · 규격 · 단위 · 수량 - UI_Shell — 내역서 표 우클릭 메뉴 붙임 · 고친 뒤 보던 자리 지킴 - Calc_Bill 짝 — 손 줄 자리(after) 놓침 고침 - 손 줄 띠 표시(CSS) · 새 시험 test_166_2(셈 · 거울 · 다듬기 · 새 산근 · 집계표 · 메뉴) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015KYZ2HWWdUwZWYnn3tQz7X
This commit is contained in:
@@ -153,9 +153,8 @@ def hand_items(user: dict[str, Any], books: dict[str, Any], hopyo: dict) -> list
|
||||
return out
|
||||
|
||||
|
||||
def _place(items: list[dict[str, Any]], hand: dict[str, Any]) -> None:
|
||||
def _place(items: list[dict[str, Any]], hand: dict[str, Any], after: str) -> None:
|
||||
"""손 줄 끼우기 — `after` 줄 바로 뒤 · 제목(`sec:`) = 그 공종 첫 자리 · 못 찾으면 끝."""
|
||||
after = hand.get("after") or ""
|
||||
at = next((n + 1 for n, row in enumerate(items) if row["key"] == after), None)
|
||||
if at is None and after == f"sec:{hand['section']}":
|
||||
at = next((n for n, row in enumerate(items) if row["section"] == hand["section"]), None)
|
||||
@@ -214,7 +213,7 @@ def compute_bill(
|
||||
prices = None if price is None else {"labor": 0, "material": price, "expense": 0}
|
||||
items.append(_item_row(item, prices, gaps, qty_of))
|
||||
for item, prices, why in hand_items(user, books, hopyo):
|
||||
_place(items, _item_row({**item, "why": why}, prices, gaps))
|
||||
_place(items, _item_row({**item, "why": why}, prices, gaps), item.get("after") or "")
|
||||
owner = [row for row in items if row["section"] == "G"]
|
||||
net = 0.0
|
||||
for row in owner:
|
||||
|
||||
@@ -144,8 +144,7 @@ export function handItems(user: Row, books: Row, hopyo: Row): Array<[Row, Row |
|
||||
}
|
||||
|
||||
/** 손 줄 끼우기 — after 줄 바로 뒤 · 제목(sec:) = 그 공종 첫 자리 · 못 찾으면 끝 */
|
||||
function place(items: Row[], hand: Row): void {
|
||||
const after = hand.after || "";
|
||||
function place(items: Row[], hand: Row, after: string): void {
|
||||
let at = items.findIndex((row) => row.key === after);
|
||||
if (at >= 0) at += 1;
|
||||
else if (after === `sec:${hand.section}`) at = items.findIndex((r) => r.section === hand.section);
|
||||
@@ -206,7 +205,7 @@ export function computeBill(
|
||||
items.push(itemRow(item, prices, gaps, qtyOf));
|
||||
}
|
||||
for (const [item, prices, why] of handItems(user, book, hopyo))
|
||||
place(items, itemRow({ ...item, why }, prices, gaps));
|
||||
place(items, itemRow({ ...item, why }, prices, gaps), item.after || "");
|
||||
const owner = items.filter((row) => row.section === "G");
|
||||
let net = 0;
|
||||
for (const row of owner) net += row.material || 0;
|
||||
|
||||
@@ -110,3 +110,8 @@
|
||||
.b09-sg-card__cols .b09-sg-card__num {
|
||||
font-variant-numeric: normal;
|
||||
}
|
||||
|
||||
/* 166-2 설계내역서 손 줄 — 사용자가 더한 줄 표시(왼쪽 띠) */
|
||||
.b09-estimation .ui-data-table__table tbody tr.is-hand td:first-child {
|
||||
background-image: linear-gradient(to right, var(--color-primary) 3px, transparent 3px);
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ import {
|
||||
buildMaterial,
|
||||
} from "./B09_Estimation_UI_Tabs_Base";
|
||||
import { buildBill } from "./B09_Estimation_UI_Tabs_Bill";
|
||||
import { attachRowMenu, type BillTable } from "./B09_Estimation_UI_Tabs_Bill_Edit";
|
||||
import {
|
||||
buildSumExpense,
|
||||
buildSumLabor,
|
||||
@@ -170,7 +171,11 @@ export function renderB09Estimation(root: HTMLElement): void {
|
||||
user = next;
|
||||
if (projectId) writeState("b09-estimation", user, projectId);
|
||||
recompute();
|
||||
// 고친 뒤 다시 그려도 보던 자리 지킴(166-2 내역서 수량 · 손 줄)
|
||||
const top = panel.querySelector<HTMLElement>(".ui-data-table")?.scrollTop ?? 0;
|
||||
show(current);
|
||||
const box = panel.querySelector<HTMLElement>(".ui-data-table");
|
||||
if (box && top) box.scrollTop = top;
|
||||
};
|
||||
|
||||
/** 탭 하나 그림 · focus = 이동해 온 줄 키 */
|
||||
@@ -217,6 +222,9 @@ export function renderB09Estimation(root: HTMLElement): void {
|
||||
},
|
||||
});
|
||||
made.root.dataset.b09Table = tab;
|
||||
// 166-2 줄 우클릭 메뉴(설계내역서) — 공용 표는 그대로 · 표 root 에 붙임
|
||||
const menu = (spec as BillTable).menu;
|
||||
if (menu) attachRowMenu(made.root, menu);
|
||||
// 157-3 칸 풍선 — 공용 표는 그대로 · 올린 칸에 title
|
||||
if (spec.tip)
|
||||
made.root.addEventListener("mouseover", (event) => {
|
||||
|
||||
@@ -3,23 +3,25 @@
|
||||
* B09 설계내역서 탭(PLAN 145-3) — 실무 엑셀 꼴(공종 · 명칭 · 규격 · 수량 · 단위 · 합계 · 노무 · 재료 · 경비 · 비고).
|
||||
*
|
||||
* 값 = `B09_Estimation_Calc_Bill`(조작 중 브라우저 · [저장] 때 서버 정본)
|
||||
* 고칠 칸 = 관급자재대 조달수수료 율(%) · 빈칸 = 「연결 없음」(수량 · 단가 원천 없음)
|
||||
* 고칠 칸 = 관급자재대 조달수수료 율(%) · 항목 수량(166-2 · 덮어쓰기 · 빈칸 = 자동) ·
|
||||
* 손 줄 명칭 · 규격 · 단위 · 수량 · 빈칸 = 「연결 없음」(수량 · 단가 원천 없음)
|
||||
* 줄 누름 = 단가를 낸 탭 줄(자재 → 재료비목록표 · 구조물 → 일위대가목록표)
|
||||
* 우클릭 = 줄 추가 · 지우기 · 연결(`…_UI_Tabs_Bill_Edit`)
|
||||
* ========================================================================== */
|
||||
|
||||
import type { DataRow } from "@ui/ui_template_data_table";
|
||||
import type { Row } from "./B09_Estimation_Calc_Base";
|
||||
import { money, parseNumber, qty, text, type TabBuild, type TabId } from "./B09_Estimation_UI_Tabs";
|
||||
import {
|
||||
money,
|
||||
parseNumber,
|
||||
qty,
|
||||
text,
|
||||
type TabBuild,
|
||||
type TabId,
|
||||
type TabTable,
|
||||
} from "./B09_Estimation_UI_Tabs";
|
||||
billMenu,
|
||||
editQty,
|
||||
editText,
|
||||
handEditable,
|
||||
type BillTable,
|
||||
} from "./B09_Estimation_UI_Tabs_Bill_Edit";
|
||||
|
||||
export const buildBill: TabBuild = ({ sheets, user, setUser }): TabTable => {
|
||||
export const buildBill: TabBuild = (ctx): BillTable => {
|
||||
const { sheets, user, setUser } = ctx;
|
||||
const bill: Row = sheets.bill || { rows: [], totals: null };
|
||||
const rows: DataRow[] = bill.rows.map((r: Row) => ({
|
||||
...r,
|
||||
@@ -47,14 +49,30 @@ export const buildBill: TabBuild = ({ sheets, user, setUser }): TabTable => {
|
||||
frozen: 1,
|
||||
hint:
|
||||
"공종 나무 = 엑셀 꼴 고정 · 금액 = ROUNDDOWN(단가 × 수량, 0) 비목마다 · 4 자재대 = 대비표 「별도」 · 관급자재대 = 「관급」 표시 자재 · 조달수수료 율(%)은 수량 칸에 넣음 · " +
|
||||
"수량 칸 = 덮어쓰기(빈칸 = 자동) · 우클릭 = 줄 추가 · 지우기 · 일위대가 · 단가산출 연결(새 산근 단가 = [저장] 뒤) · " +
|
||||
`빈 줄 ${direct.blank ?? 0} — 계는 아는 금액만 · 원가계산서는 빈 줄이 없을 때만 받음`,
|
||||
editable: (i, key) => key === "qty" && rows[i]?.key === "fee",
|
||||
editable: (i, key) => {
|
||||
const r = rows[i];
|
||||
if (!r || r.kind !== "item") return false;
|
||||
return r.hand ? handEditable(key) : key === "qty";
|
||||
},
|
||||
onEdit: (i, key, value) => {
|
||||
if (key !== "qty" || rows[i]?.key !== "fee") return;
|
||||
const r = rows[i];
|
||||
if (!r) return;
|
||||
if (r.hand && key !== "qty") return editText(ctx, String(r.key), key, value);
|
||||
if (key !== "qty") return;
|
||||
const v = parseNumber(value);
|
||||
if (v === undefined) return;
|
||||
if (r.key !== "fee") return editQty(ctx, r, v);
|
||||
setUser({ ...user, estimate: { ...(user.estimate || {}), procurement_fee_pct: v } });
|
||||
},
|
||||
tip: (i, key) => {
|
||||
const r = rows[i];
|
||||
if (key !== "qty" || !r || !("qty_auto" in r)) return null;
|
||||
return `자동 값 ${r.qty_auto ?? "빈칸"} — 우클릭 [수량 자동으로 되돌리기]`;
|
||||
},
|
||||
rowClass: (i) => (rows[i]?.hand ? "is-hand" : null),
|
||||
menu: (i) => billMenu(ctx, rows, i),
|
||||
link: (i) => {
|
||||
const ref = rows[i]?.ref as Row | null | undefined;
|
||||
return ref?.key ? { tab: ref.tab as TabId, key: String(ref.key) } : null;
|
||||
|
||||
Reference in New Issue
Block a user