feat(b09): 설계내역서 「연결 ▾」 칸 단추 · 우클릭과 같은 연결 메뉴 (182-2)
- 비고 앞 「연결」 칸 — 손 항목 줄만 「연결 ▾」 · 연결됨 = 호표 번호 ▾ · 누름 = 그 칸 아래 연결 메뉴 - 메뉴 = 일위대가 호표 · 단가산출 호표 · 단가산출 로직 + 로직 입력값 · 연결 풀기(166-2 고르기 창 재사용) - 단추 칸 = 이동 안 함 · 묶음 줄 · 자동 줄 = 안 엶 - 시험 test_182 연결 단추(칸 · 메뉴 · 고르기 창 → ref) · node 입력 = 표준입력(명령 줄 길이) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015KYZ2HWWdUwZWYnn3tQz7X
This commit is contained in:
@@ -8,6 +8,7 @@
|
||||
* 줄 누름 = 단가를 낸 탭 줄(자재 → 재료비목록표 · 구조물 → 일위대가목록표)
|
||||
* 우클릭 = 줄 추가 · 지우기 · 연결 · 레벨(`…_UI_Tabs_Bill_Edit`)
|
||||
* 레벨(182) = 줄 `level`(셈이 냄) → class is-lvN · 글자 크기 · 첫 칸 들여쓰기(CSS) · 묶음 줄 = 이름만 고침
|
||||
* 「연결 ▾」 칸(182) = 손 항목 줄만 · 누름 = 우클릭 [연결] 과 같은 메뉴(연결됨 = 호표 번호)
|
||||
* ========================================================================== */
|
||||
|
||||
import type { DataRow } from "@ui/ui_template_data_table";
|
||||
@@ -18,13 +19,17 @@ import {
|
||||
editQty,
|
||||
editText,
|
||||
handEditable,
|
||||
openLinkMenu,
|
||||
type BillTable,
|
||||
} from "./B09_Estimation_UI_Tabs_Bill_Edit";
|
||||
|
||||
export const buildBill: TabBuild = (ctx): BillTable => {
|
||||
const { sheets, user, setUser } = ctx;
|
||||
const bill: Row = sheets.bill || { rows: [], totals: null };
|
||||
const rows: DataRow[] = bill.rows;
|
||||
const rows: DataRow[] = bill.rows.map((r: Row) => ({
|
||||
...r,
|
||||
connect: r.hand && r.kind === "item" ? `${r.note || "연결"} ▾` : "",
|
||||
}));
|
||||
const direct: Row = bill.totals?.direct || {};
|
||||
const pair = (key: string, label: string) => [
|
||||
money(`price_${key}`, "단가", [label]),
|
||||
@@ -40,6 +45,7 @@ export const buildBill: TabBuild = (ctx): BillTable => {
|
||||
...pair("labor", "노무비"),
|
||||
...pair("material", "재료비"),
|
||||
...pair("expense", "경비"),
|
||||
text("connect", "연결", 96),
|
||||
text("note", "비고", 200),
|
||||
],
|
||||
rows,
|
||||
@@ -47,7 +53,7 @@ export const buildBill: TabBuild = (ctx): BillTable => {
|
||||
frozen: 1,
|
||||
hint:
|
||||
"공종 나무 = 엑셀 꼴 고정 · 금액 = ROUNDDOWN(단가 × 수량, 0) 비목마다 · 4 자재대 = 대비표 「별도」 · 관급자재대 = 「관급」 표시 자재 · 조달수수료 율(%)은 수량 칸에 넣음 · " +
|
||||
"수량 칸 = 덮어쓰기(빈칸 = 자동) · 우클릭 = 줄 추가 · 지우기 · 일위대가 · 단가산출 연결(새 산근 단가 = [저장] 뒤) · " +
|
||||
"수량 칸 = 덮어쓰기(빈칸 = 자동) · 우클릭 · 「연결 ▾」 = 줄 추가 · 지우기 · 일위대가 · 단가산출 연결(새 산근 단가 = [저장] 뒤) · " +
|
||||
"레벨 = 우클릭 묶음 줄로 · 하위 줄 추가 · 들여쓰기 · 내어쓰기(묶음 금액 = 아래 항목 합 · 공종 합은 항목만) · " +
|
||||
`빈 줄 ${direct.blank ?? 0} — 계는 아는 금액만 · 원가계산서는 빈 줄이 없을 때만 받음`,
|
||||
editable: (i, key) => {
|
||||
@@ -79,7 +85,11 @@ export const buildBill: TabBuild = (ctx): BillTable => {
|
||||
return [`is-lv${r.level ?? 1}`, title, r.hand ? "is-hand" : ""].filter((c) => c).join(" ");
|
||||
},
|
||||
menu: (i) => billMenu(ctx, rows, i),
|
||||
link: (i) => {
|
||||
onCell: (i, key) => {
|
||||
if (key === "connect") openLinkMenu(ctx, rows, i);
|
||||
},
|
||||
link: (i, key) => {
|
||||
if (key === "connect") return null; // 「연결 ▾」 = 메뉴(이동 안 함)
|
||||
const ref = rows[i]?.ref as Row | null | undefined;
|
||||
return ref?.key ? { tab: ref.tab as TabId, key: String(ref.key) } : null;
|
||||
},
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
* 연결 = 있는 호표(일위대가목록표 dg · 단가산출근거목록표 sg) · M01 로직(새 산근 = 서버 셈 · [저장] 뒤 단가)
|
||||
* 우클릭 = 공용 메뉴(`ui_template_context_menu` · 화면 고정 자리) · 로직 고르기 · 입력값 = M02 창 재사용
|
||||
* 레벨(182) = 손 줄 kind(group = 묶음 줄 · 이름만) · parent(묶음 손 줄 키) — 하위 줄 추가 · 묶음 줄로 ·
|
||||
* 항목으로 · 들여쓰기 · 내어쓰기 · 손 묶음 깊이 2
|
||||
* 항목으로 · 들여쓰기 · 내어쓰기 · 손 묶음 깊이 2 · 「연결 ▾」 칸 = 우클릭 [연결] 과 같은 메뉴
|
||||
* ========================================================================== */
|
||||
|
||||
import type { DataRow } from "@ui/ui_template_data_table";
|
||||
@@ -336,6 +336,17 @@ export function billMenu(ctx: TabContext, rows: DataRow[], i: number): MapContex
|
||||
}
|
||||
|
||||
let shared: MapContextMenu | null = null;
|
||||
/** 마지막으로 누른 칸 — 「연결 ▾」 단추 메뉴 자리 */
|
||||
let lastCell: Element | null = null;
|
||||
|
||||
/** 「연결 ▾」 칸 누름(182) — 우클릭 [연결] 과 같은 메뉴를 그 칸 아래에 엶 */
|
||||
export function openLinkMenu(ctx: TabContext, rows: DataRow[], i: number): void {
|
||||
const row = rows[i];
|
||||
const box = lastCell?.getBoundingClientRect();
|
||||
if (!shared || !box || !row?.hand || row.kind !== "item") return;
|
||||
const [picks, more] = linkItems(ctx, String(row.key));
|
||||
shared.open(box.left, box.bottom, more.length ? [...picks, "-", ...more] : picks);
|
||||
}
|
||||
|
||||
/** 표에 줄 우클릭 메뉴를 붙임(공용 표는 안 고침) — 글 칸 위는 브라우저 메뉴(붙여넣기) 그대로 */
|
||||
export function attachRowMenu(
|
||||
@@ -348,6 +359,12 @@ export function attachRowMenu(
|
||||
ctxMenu.element.style.position = "fixed"; // 화면 기준 자리(clientX · Y)
|
||||
document.body.append(ctxMenu.element);
|
||||
}
|
||||
// 칸 누름보다 먼저(잡기 단계) — 「연결 ▾」 메뉴를 그 칸 자리에 엶
|
||||
root.addEventListener(
|
||||
"click",
|
||||
(event) => (lastCell = (event.target as Element).closest("td")),
|
||||
true,
|
||||
);
|
||||
root.addEventListener("contextmenu", (event) => {
|
||||
const target = event.target as Element;
|
||||
if (target.closest("input")) return;
|
||||
|
||||
@@ -74,10 +74,11 @@ def _rows(sheets):
|
||||
|
||||
|
||||
def _node(source: str) -> str:
|
||||
# 표준입력으로 줌 — 「-e」 는 윈도 명령 줄 길이(32k)를 넘음
|
||||
result = subprocess.run(
|
||||
["node", "--experimental-strip-types", "--no-warnings",
|
||||
"--input-type=module-typescript", "-e", source],
|
||||
capture_output=True, text=True, encoding="utf-8", timeout=60,
|
||||
"--input-type=module-typescript"],
|
||||
input=source, capture_output=True, text=True, encoding="utf-8", timeout=60,
|
||||
) # fmt: skip
|
||||
assert result.returncode == 0, result.stderr
|
||||
return result.stdout
|
||||
@@ -228,3 +229,74 @@ def test_우클릭_레벨_메뉴_TS():
|
||||
user = {"bill": {"rows": [r if r["key"] != "hand:x" else out["indent"] for r in HAND]}}
|
||||
rows2 = _rows(compute_estimation(INPUTS, user, None, BOOKS))
|
||||
assert (rows2["hand:x"]["level"], rows2["hand:g1"]["sum"]) == (4, 12300 + 2460 + 1230)
|
||||
|
||||
|
||||
LINK_JS = """
|
||||
const opened = [];
|
||||
const picked = {};
|
||||
globalThis.createMapContextMenu = () => ({
|
||||
element: { isConnected: true, style: {} },
|
||||
open: (x, y, items) => opened.push({ x, y, items }),
|
||||
});
|
||||
globalThis.requestAnimationFrame = () => 0;
|
||||
globalThis.createFindList = () => ({
|
||||
search: { value: "", addEventListener() {}, focus() {} },
|
||||
hint: {},
|
||||
root: {},
|
||||
setRows: (rows) => (picked.rows = rows),
|
||||
});
|
||||
globalThis.openModal = (o) => {
|
||||
picked.title = o.title;
|
||||
o.mount({ append() {} }, () => (picked.closed = true));
|
||||
};
|
||||
let user = __USER__;
|
||||
const ctx = { sheets: { bill: __BILL__, daega: __DAEGA__ }, payload: {},
|
||||
get user() { return user; }, setUser: (u) => (user = u) };
|
||||
const listeners = {};
|
||||
const root = { addEventListener: (type, fn) => (listeners[type] = fn), contains: () => true };
|
||||
attachRowMenu(root, () => []);
|
||||
const cell = { getBoundingClientRect: () => ({ left: 10, bottom: 40 }) };
|
||||
listeners.click({ target: { closest: () => cell } });
|
||||
const table = buildBill(ctx);
|
||||
const at = (key) => table.rows.findIndex((r) => r.key === key);
|
||||
const name = (x) => (x === "-" ? "-" : x[0]);
|
||||
const out = {
|
||||
columns: table.columns.map((c) => c.key).slice(-2),
|
||||
connect: ["hand:a", "hand:c", "hand:g1", "earth:4"].map((k) => table.rows[at(k)].connect),
|
||||
link: [table.link(at("hand:a"), "connect"), table.link(at("hand:a"), "name")],
|
||||
cls: ["sec:1", "hand:g1", "hand:a", "earth:4"].map((k) => table.rowClass(at(k))),
|
||||
edit: [table.editable(at("hand:g1"), "name"), table.editable(at("hand:g1"), "qty")],
|
||||
};
|
||||
table.onCell(at("hand:a"), "connect");
|
||||
table.onCell(at("hand:g1"), "connect");
|
||||
table.onCell(at("earth:4"), "connect");
|
||||
table.onCell(at("hand:c"), "connect");
|
||||
out.menus = opened.map((m) => [m.x, m.y, m.items.map(name)]);
|
||||
opened[1].items[0][1](); // hand:c → 일위대가 호표 고르기 창
|
||||
picked.rows[0].onPick();
|
||||
out.picked = [picked.title, picked.closed, picked.rows.map((r) => r.tag)];
|
||||
out.ref = user.bill.rows.find((r) => r.key === "hand:c").ref;
|
||||
console.log(JSON.stringify(out));
|
||||
"""
|
||||
|
||||
|
||||
def test_연결_단추_칸_메뉴_고르기_창_TS():
|
||||
sheets = compute_estimation(INPUTS, USER, None, BOOKS)
|
||||
body = (
|
||||
LINK_JS.replace("__USER__", json.dumps(USER, ensure_ascii=False))
|
||||
.replace("__BILL__", json.dumps(sheets["bill"], ensure_ascii=False))
|
||||
.replace("__DAEGA__", json.dumps(BOOKS["daega"], ensure_ascii=False))
|
||||
)
|
||||
src = _ts(["UI_Tabs", "UI_Tabs_Bill_Edit", "UI_Tabs_Bill"])
|
||||
out = json.loads(_node(src + body))
|
||||
assert out["columns"] == ["connect", "note"] # 비고 앞 「연결」 칸
|
||||
assert out["connect"] == ["대가3 ▾", "연결 ▾", "", ""] # 손 항목 줄만 · 연결됨 = 호표 번호
|
||||
assert out["link"] == [None, {"tab": "dg", "key": "DG:a"}] # 단추 칸 = 이동 안 함
|
||||
assert out["cls"] == [
|
||||
"is-lv1 is-title", "is-lv3 is-group is-hand", "is-lv4 is-hand", "is-lv3"] # fmt: skip
|
||||
assert out["edit"] == [True, False] # 묶음 줄 = 이름만
|
||||
picks = ["일위대가 호표", "단가산출 호표", "단가산출 로직(M01 · 새 산근)"]
|
||||
# 누른 칸 아래(left · bottom) · 묶음 줄 · 자동 줄 = 안 엶 · 연결된 줄 = 연결 풀기 더함
|
||||
assert out["menus"] == [[10, 40, [*picks, "-", "연결 풀기"]], [10, 40, picks]]
|
||||
assert out["picked"] == ["일위대가 호표 고르기", True, ["대가3"]] # 166-2 고르기 창 재사용
|
||||
assert out["ref"] == DG
|
||||
|
||||
Reference in New Issue
Block a user