243 lines
11 KiB
Python
243 lines
11 KiB
Python
"""204-1 B09 설계내역서 — 방향키 · 끌기 레벨 · 이동 · 자손 함께 · 묶음 자동 · 합 전달.
|
|
|
|
- 방향키 = 고른 줄(편집 중 아님) → 들여 · ← 내어 · ↑↓ 옮김(200-2 메뉴 항목 그대로) · Alt+←→ 안 받음
|
|
- 끌기 자리 = `dropAt`(틈 · 바라는 레벨 → 부모 · 앞 줄) ·
|
|
놓기 = 표 drop(`dropMove` · 메뉴와 같은 지킬 것)
|
|
- 묶음 자동 = 셈(짝 `Calc_Bill` · 거울) — 자식이 생긴 항목 = 묶음(자손 합 · 위로 전달) ·
|
|
자식이 빠지면 다시 항목(제 수량 · 단가) · 자식이 다 빠진 새 묶음 = 항목(화면 · 맨 위 공종 빼고)
|
|
"""
|
|
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
from test_184_2_b09_bill_free import ( # noqa: E402
|
|
BASE,
|
|
BOOKS,
|
|
DG,
|
|
INPUTS,
|
|
_calc,
|
|
_freeze,
|
|
_put,
|
|
_rows,
|
|
)
|
|
from test_184_2_b09_bill_free import _node as _node_calc # noqa: E402
|
|
from test_184_3_b09_bill_menu import ROWS, _node, _row, _ts # noqa: E402
|
|
from test_200_2_b09_bill_level_bar import BAR_JS # noqa: E402
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
if str(ROOT) not in sys.path:
|
|
sys.path.insert(0, str(ROOT))
|
|
|
|
from B09_Estimation import B09_Estimation_Store as store # noqa: E402
|
|
from B09_Estimation.B09_Estimation_Calc_Bill import compute_estimation # noqa: E402
|
|
|
|
SRC = ["UI_Tabs", "UI_Tabs_Bill_Link", "UI_Tabs_Bill_Edit", "UI_Tabs_Bill", "UI_Tabs_Bill_Drag",
|
|
"UI_Tabs_Bill_Level"] # fmt: skip
|
|
|
|
|
|
def _order(bill):
|
|
return [(e["key"], e.get("parent"), e.get("kind")) for e in (bill or {}).get("rows", [])]
|
|
|
|
|
|
# --- 방향키 -----------------------------------------------------------------------------
|
|
|
|
KEYS_JS = """
|
|
const out = {};
|
|
const press = (pickKey, k, more = {}) => {
|
|
user = {}; draw(); pick(pickKey); root.focus();
|
|
return [key(k, more), order()];
|
|
};
|
|
out.right = press("earth:5", "ArrowRight");
|
|
out.left = press("earth:4", "ArrowLeft");
|
|
out.up = press("earth:5", "ArrowUp");
|
|
out.down = press("earth:4", "ArrowDown");
|
|
out.altLeft = press("earth:4", "ArrowLeft", { altKey: true });
|
|
user = {}; draw(); pick("earth:5");
|
|
doc.activeElement = tr("earth:5").kids[0].kids[0];
|
|
out.editing = [key("ArrowRight"), key("ArrowUp"), order()];
|
|
out.guide = bar.guide.textContent;
|
|
console.log(JSON.stringify(out));
|
|
"""
|
|
|
|
|
|
def test_방향키_넷_편집_중엔_안_먹음():
|
|
head = BAR_JS[: BAR_JS.index("const out = {};")]
|
|
body = head.replace("__ROWS__", json.dumps(ROWS, ensure_ascii=False)) + KEYS_JS
|
|
out = json.loads(_node(_ts(SRC) + body))
|
|
stopped, order = out["right"] # → = 위 항목 아래로(earth:4 = 저절로 묶음)
|
|
assert stopped and ["earth:5", "earth:4"] in order
|
|
stopped, order = out["left"] # ← = 1.1 밖으로(1 아래)
|
|
assert stopped and ["earth:4", "sec:1"] in order
|
|
for name in ("up", "down"): # ↑↓ = 같은 부모 안 자리 바꿈
|
|
stopped, order = out[name]
|
|
keys = [k for k, _ in order]
|
|
assert stopped and keys.index("earth:5") + 1 == keys.index("earth:4"), name
|
|
assert out["altLeft"] == [False, []] # Alt+← = 브라우저 뒤로 — 안 받음
|
|
assert out["editing"] == [False, False, []] # 글 칸 편집 중 = 글 칸 커서
|
|
assert "←→" in out["guide"] and "↑↓" in out["guide"] and "끌기" in out["guide"]
|
|
|
|
|
|
# --- 끌기 자리 · 놓기 ------------------------------------------------------------------------
|
|
|
|
DROP_JS = """
|
|
const start = __USER__;
|
|
let user = start;
|
|
const ctx = { sheets: { bill: { rows: __ROWS__, totals: null } }, payload: {},
|
|
get user() { return user; }, setUser: (u) => (user = u) };
|
|
const table = buildBill(ctx);
|
|
const at = (k) => table.rows.findIndex((r) => r.key === k);
|
|
const order = (b) => (b?.rows ?? []).map((e) => [e.key, e.parent ?? null, e.kind ?? null]);
|
|
const out = { at: {}, drop: {} };
|
|
for (const [name, k, gap, want] of __ATS__) out.at[name] = dropAt(table.rows, at(k), gap, want);
|
|
for (const [name, k, parent, before] of __DROPS__) {
|
|
user = start;
|
|
const run = table.drop(at(k), parent, before);
|
|
if (run) run();
|
|
out.drop[name] = run ? order(user.bill) : null;
|
|
}
|
|
console.log(JSON.stringify(out));
|
|
"""
|
|
|
|
#: ROWS + 맨 위 새 공종(hand:n · 자식 hand:k) — 4 자재대 뒤
|
|
ROWS2 = [*ROWS[:9], _row("hand:n", None, 1, "group", no="5"), _row("hand:k", "hand:n", 2),
|
|
*ROWS[9:]] # fmt: skip
|
|
|
|
|
|
def _drag(ats=(), drops=(), rows=ROWS2, user=None):
|
|
body = (
|
|
DROP_JS.replace("__ROWS__", json.dumps(rows, ensure_ascii=False))
|
|
.replace("__USER__", json.dumps(user or {}, ensure_ascii=False))
|
|
.replace("__ATS__", json.dumps([list(a) for a in ats], ensure_ascii=False))
|
|
.replace("__DROPS__", json.dumps([list(d) for d in drops], ensure_ascii=False))
|
|
)
|
|
return json.loads(_node(_ts(SRC) + body))
|
|
|
|
|
|
def test_끌기_자리_레벨():
|
|
# 차례 0 sec:1 · 1 sec:1.2 · 2 earth:4 · 3 earth:5 · 4 hand:g · 5 hand:a · 6 hand:b · 7 sec:4 ·
|
|
# 8 mat:M1 · 9 hand:n · 10 hand:k · 11 sec:G · 12 mat:M2 · 13 fee
|
|
out = _drag(
|
|
ats=[
|
|
("오른쪽", "earth:5", 3, 4), # 제자리 + 한 칸 오른쪽 = 위 항목 아래로
|
|
("제자리", "earth:5", 3, 3),
|
|
("제자리끝", "earth:5", 4, 3),
|
|
("자손안", "hand:g", 5, 3),
|
|
("위로", "hand:g", 2, 3),
|
|
("멀리오른쪽", "hand:g", 2, 9), # 레벨 = 위 줄 + 1 까지
|
|
("자재아래", "earth:5", 9, 3), # 자재 줄 = 자식 못 둠 → 형제로
|
|
("맨위안됨", "earth:5", 7, 1), # 항목 = 맨 위(1) 못 감 → 2
|
|
("맨위공종", "sec:4", 0, 1),
|
|
]
|
|
)["at"]
|
|
assert out["오른쪽"] == {"parent": "earth:4", "before": None, "level": 4}
|
|
assert out["제자리"] is None and out["제자리끝"] is None and out["자손안"] is None
|
|
assert out["위로"] == {"parent": "sec:1.2", "before": "earth:4", "level": 3}
|
|
assert out["멀리오른쪽"] == out["위로"]
|
|
assert out["자재아래"] == {"parent": "sec:4", "before": None, "level": 2}
|
|
assert out["맨위안됨"] == {"parent": "sec:1", "before": None, "level": 2}
|
|
assert out["맨위공종"] == {"parent": None, "before": "sec:1", "level": 1}
|
|
|
|
|
|
def test_놓기_자손_함께_묶음_자동_막음():
|
|
out = _drag(
|
|
drops=[
|
|
("자손", "hand:g", "sec:1.2", "earth:4"),
|
|
("항목아래", "earth:5", "earth:4", None),
|
|
("하나빠짐", "hand:a", "sec:1.2", "hand:b"),
|
|
("맨위남음", "hand:k", "sec:4", None),
|
|
("관급밖", "mat:M2", "sec:1", None),
|
|
("수수료", "fee", "sec:1", None),
|
|
("관급제목", "sec:G", None, "sec:1"),
|
|
("자재아래", "earth:5", "mat:M1", None),
|
|
("자기자손", "hand:g", "hand:a", None),
|
|
]
|
|
)["drop"]
|
|
keys = [k for k, *_ in out["자손"]]
|
|
# 묶음 통째 — 자식 hand:a 는 부모 키 그대로 · 차례 = hand:g 바로 뒤
|
|
assert keys[keys.index("hand:g") + 1] == "hand:a" and keys.index("hand:g") < keys.index(
|
|
"earth:4"
|
|
)
|
|
assert ["hand:a", "hand:g", None] in out["자손"]
|
|
assert ["earth:5", "earth:4", None] in out["항목아래"] # 저장 = 부모만(묶음 = 셈이 정함)
|
|
# 자식이 다 빠진 새 묶음 = 항목(kind 지움) · 맨 위 공종 = 묶음 그대로
|
|
assert ["hand:g", "sec:1.2", None] in out["하나빠짐"]
|
|
assert ["hand:n", None, "group"] in out["맨위남음"]
|
|
for name in ("관급밖", "수수료", "관급제목", "자재아래", "자기자손"):
|
|
assert out[name] is None, name
|
|
|
|
|
|
def test_깊이_5_넘는_놓기_막음():
|
|
deep = [
|
|
_row("sec:1", None, 1, "title"), _row("sec:1.2", "sec:1", 2, "title"),
|
|
_row("hand:g", "sec:1.2", 3, "group"), _row("hand:g2", "hand:g", 4, "group"),
|
|
_row("hand:g3", "hand:g2", 5, "group"), _row("hand:z", "hand:g2", 5),
|
|
_row("hand:y", "sec:1.2", 3),
|
|
] # fmt: skip
|
|
out = _drag(drops=[("깊음", "hand:g", "hand:y", None), ("됨", "hand:z", "hand:y", None)],
|
|
rows=deep)["drop"] # fmt: skip
|
|
assert out["깊음"] is None
|
|
assert ["hand:z", "hand:y", None] in out["됨"]
|
|
|
|
|
|
# --- 셈 — 묶음 ↔ 항목 자동 · 합 전달 · 거울 -----------------------------------------------------
|
|
|
|
HANDS = [
|
|
{"key": "hand:a", "parent": "sec:3", "name": "a", "unit": "㎡", "qty": 2, "ref": DG},
|
|
{"key": "hand:c", "parent": "hand:a", "name": "c", "unit": "㎡", "qty": 1, "ref": DG},
|
|
]
|
|
|
|
|
|
def _bills():
|
|
frozen = _freeze(_calc())
|
|
under = _put(frozen, "earth:1", parent="earth:4") # 1.1 규준틀 → 절토(토사) 아래
|
|
hands = {**frozen, "rows": frozen["rows"] + HANDS}
|
|
alone = {**frozen, "rows": frozen["rows"] + HANDS[:1]}
|
|
mat = _put(frozen, "earth:5", parent="mat:b08:sand")
|
|
return frozen, under, hands, alone, mat
|
|
|
|
|
|
def test_자식_생긴_항목_묶음_합_전달_빠지면_다시_항목():
|
|
plain = _calc()
|
|
frozen, under, hands, alone, mat = _bills()
|
|
base = _rows(plain)
|
|
rows = _rows(_calc(under))
|
|
e1, e4 = rows["earth:1"], rows["earth:4"]
|
|
assert (e4["kind"], e4["auto"], e4["qty"], e4["ref"], e4["price_sum"]) == (
|
|
"group", True, None, None, None) # fmt: skip
|
|
assert (e1["parent"], e1["level"], e4["level"]) == ("earth:4", 4, 3)
|
|
# 묶음 금액 = 자손 합 → 1.2 · 1 로 전달 · 제 금액은 빠짐
|
|
assert e4["sum"] == e1["sum"] == base["earth:1"]["sum"]
|
|
assert rows["sec:1.2"]["sum"] == e1["sum"] and rows["sec:1.1"]["sum"] == 0
|
|
assert rows["sec:1"]["sum"] == base["sec:1"]["sum"] - base["earth:4"]["sum"]
|
|
gaps = _calc(under)["bill"]["gaps"]
|
|
assert not [g for g in gaps if "earth:4" in json.dumps(g, ensure_ascii=False)]
|
|
# 자식이 빠지면(원래 부모로) = 다시 항목 · 제 수량 · 단가 · 표 그대로
|
|
assert _calc(_put(under, "earth:1", parent="sec:1.1"))["bill"] == plain["bill"]
|
|
# 새 항목(수량 2 · 대가3) — 자식이 생기면 묶음(자식 합) · 빠지면 제 값 다시
|
|
rows = _rows(_calc(hands))
|
|
assert (rows["hand:a"]["kind"], rows["hand:a"]["sum"], rows["hand:c"]["level"]) == (
|
|
"group", 1230, 3) # fmt: skip
|
|
a = _rows(_calc(alone))["hand:a"]
|
|
assert (a["kind"], a["qty"], a["sum"]) == ("item", 2, 2460)
|
|
# 저장 다듬기 = 자식 있는 새 항목도 제 수량 · 연결 지킴(묶음은 셈이 정함)
|
|
kept = {r["key"]: r for r in store.clean_bill(hands)["rows"]}["hand:a"]
|
|
assert (kept.get("kind"), kept["qty"], kept["ref"]) == (None, 2, DG)
|
|
# 자재 줄 = 부모 못 됨 → 기본 부모
|
|
rows = _rows(_calc(mat))
|
|
assert rows["earth:5"]["parent"] == "sec:1.2" and rows["mat:b08:sand"]["kind"] == "item"
|
|
|
|
|
|
def test_거울_TS_가_같은_값():
|
|
calc = _ts(["Calc_Base", "Calc_Bill"])
|
|
users = [{**BASE, "bill": b} for b in _bills()]
|
|
body = (
|
|
f"\nconst i = {json.dumps(INPUTS, ensure_ascii=False)};"
|
|
f"\nconst us = {json.dumps(users, ensure_ascii=False)};"
|
|
f"\nconst b = {json.dumps(BOOKS, ensure_ascii=False)};"
|
|
"\nconsole.log(JSON.stringify(us.map((u) => computeEstimation(i, u, null, b))));"
|
|
)
|
|
got = json.loads(_node_calc(calc + body))
|
|
assert got == [compute_estimation(INPUTS, u, None, BOOKS) for u in users]
|