feat(B09): 설계내역서 입력 칸 테두리 되돌림 · 우클릭 「항목 추가」 하나 · 연결 이름 셋 · 「줄 삭제」 · 하위 메뉴 옆 판 (246-1 · 246-2)
- 공용 표 입력 칸 = 241 앞 모양(accent 5% 채움 · td is-can-edit 표시 지움 · 더블클릭 입력 그대로) - 우클릭 더함 넷 → 「항목 추가」 — 항목 = 바로 아래 같은 부모 · 폴더 = 첫 하위 · 1단 = ← 내어쓰기(직접비 · 관급자재대 앞 · 자재 줄 막음) - 연결 이름 = 구조물 조합 · 일위대가 사전조합 · 단가산출 · 들여오기 알림 글 쉬운 말 · 「줄 지우기」 → 「줄 삭제」 · 안내 글 - 공용 우클릭 메뉴 children = 「글 ▸」 한 줄 + 오른쪽 옆 판(올림 · 누름 · → 열기 · ← 닫기 · 화면 끝 = 왼쪽) - 시험 기대값 고침(241_1 · 243_1 · 166_2 · 182 · 184_3 · 204_2) · 새 시험 246_1 · 246_2 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SE9MxeXTu2uaDZ55BJowAY
This commit is contained in:
@@ -64,7 +64,7 @@ export const buildBill: TabBuild = (ctx): BillTable => {
|
||||
total: { name: "순공사비 계(관급 제외)", ...direct },
|
||||
frozen: 1,
|
||||
hint:
|
||||
"줄을 우클릭하면 추가 · 지움 · 옮김 · 「연결 ▾」로 일위대가 · 단가산출 연결" +
|
||||
"줄을 우클릭하면 항목 추가 · 줄 삭제 · 옮김 · 「연결 ▾」로 일위대가 · 단가산출 연결" +
|
||||
(direct.blank
|
||||
? ` · 빈 줄 ${direct.blank} — 계는 아는 금액만 · 원가계산서는 빈 줄이 없을 때만 받음`
|
||||
: ""),
|
||||
|
||||
@@ -5,12 +5,14 @@
|
||||
* 저장 = 사용자 값 `user.bill` v2(캐시 → [저장] 때 `user_values.json`) — 셈은 짝 `Calc_Bill` 이 바로 다시
|
||||
* {v: 2, rows: [표 차례 {key · parent(없음 = 맨 위) · 고친 칸만}], gone: [지운 자동 줄 키]}
|
||||
* 자동 줄 = 키 · 부모 + 고친 칸(name · spec · unit · qty · ref · null = 일부러 빈칸 · 없음 = 원천)
|
||||
* 새 줄 = hand:… · 모든 칸 + kind group(묶음) | 없음 = 항목 · 새 공종 = 맨 위 묶음 · cost direct
|
||||
* 새 줄 = hand:… · 모든 칸 + kind group(묶음) | 없음 = 항목 · 새 공종 = 맨 위로 내어 쓴 줄(묶음 · cost direct)
|
||||
* 첫 손질 = 보이는 줄의 키 · 부모 · 차례만 v2 로 굳힘(옛 꼴 qty · 손 줄 칸은 옮김 · 자동 값은 안 옮김)
|
||||
* 계약 = `Calc_Bill.py` 머리 — 셈 출력 줄 parent · auto(기본 로직 줄) · auto_{칸}(고친 칸의 자동 값) · level ·
|
||||
* 막음 = 조달수수료 · 관급자재대 제목 옮기기 · 지우기 · 자재(mat:) 줄 맨 위 묶음 돈 갈래(관급 · 직접비)
|
||||
* 바뀌는 옮김 · 자재 줄 지우기(주인 = 대비표 표시) · 깊이 5 넘음 · 자식 있는 맨 위 묶음 지우기
|
||||
* 바뀌는 옮김 · 자재 줄 삭제(주인 = 대비표 표시) · 깊이 5 넘음 · 자식 있는 맨 위 묶음 삭제
|
||||
* 연결 고르기 창 = `…_UI_Tabs_Bill_Link` · 우클릭 = 공용 메뉴(`ui_template_context_menu` · 화면 고정 자리)
|
||||
* 더함(246) = 「항목 추가」 하나 — 누른 줄이 항목 = 바로 아래 같은 부모 · 폴더 = 그 폴더 첫 하위 ·
|
||||
* 맨 위 1단(새 공종) = 레벨 단추 · ← 로 내어 씀(관급자재대 앞)
|
||||
* 묶음 자동(204) = 줄 아래로 들임 · 끌어 놓음 → 그 줄 저절로 묶음(셈) · 자식이 다 빠진 새 묶음 = 항목(kind 지움 ·
|
||||
* 맨 위 공종은 그대로) · 줄을 옮기면 자손이 함께 감(부모 키 그대로) · 끌어 놓기 = `dropMove`(`…_Bill_Drag`)
|
||||
* ========================================================================== */
|
||||
@@ -33,7 +35,6 @@ export type BillTable = TabTable & {
|
||||
};
|
||||
|
||||
const NEW_NAME = "새 줄";
|
||||
const NEW_SECTION = "새 공종";
|
||||
/** 고칠 칸 — 자동 줄은 덮개 */
|
||||
const FIELDS = ["name", "spec", "unit", "qty", "ref"];
|
||||
/** 깊이 한도 — 1 = 맨 위 공종 */
|
||||
@@ -242,26 +243,13 @@ export function billMenu(ctx: TabContext, rows: DataRow[], i: number): MapContex
|
||||
if (run) parts[part].push([label, run]);
|
||||
};
|
||||
|
||||
// 더함 — 위 · 아래(같은 부모) · 하위(묶음) · 새 공종(맨 위 · 관급자재대 앞)
|
||||
add(0, "위에 줄 추가", (w, e) => {
|
||||
if (!e.parent) return false;
|
||||
const at = find(w.bill.rows, key);
|
||||
w.bill.rows.splice(at < 0 ? w.bill.rows.length : at, 0, newItem(e.parent));
|
||||
});
|
||||
add(0, "아래에 줄 추가", (w, e) => {
|
||||
if (!e.parent || key === "fee") return false;
|
||||
w.bill.rows.splice(find(w.bill.rows, key) + 1, 0, newItem(e.parent));
|
||||
});
|
||||
add(0, "아래에 하위 줄 추가", (w) => {
|
||||
if (!isGroup(w, key)) return false;
|
||||
w.bill.rows.push(newItem(key));
|
||||
});
|
||||
add(0, "새 공종 추가(1단)", (w, e) => {
|
||||
// 항목 추가 — 폴더 = 첫 하위 · 항목 = 바로 아래 같은 부모(조달수수료 = 관급 묶음 끝 · 셈이 그 뒤에 둠)
|
||||
add(0, "항목 추가", (w, e) => {
|
||||
const list = w.bill.rows;
|
||||
const from = find(list, key) < 0 ? String(e.parent) : key;
|
||||
const top = chain(list, from).at(-1)!;
|
||||
const at = find(list, top.key) + (costOf(w, list, from) === "supply" ? 0 : 1);
|
||||
list.splice(at, 0, { key: newKey(), kind: "group", cost: "direct", name: NEW_SECTION });
|
||||
const at = find(list, key);
|
||||
if (isGroup(w, key)) list.splice(at + 1, 0, newItem(key));
|
||||
else if (!e.parent) return false;
|
||||
else list.splice(at < 0 ? list.length : at + 1, 0, newItem(e.parent));
|
||||
});
|
||||
if (linkable(row)) {
|
||||
const [picks, more] = linkFor(ctx, rows, i);
|
||||
@@ -292,10 +280,18 @@ export function billMenu(ctx: TabContext, rows: DataRow[], i: number): MapContex
|
||||
});
|
||||
add(2, "내어쓰기 ←", (w, e) => {
|
||||
const up = e.parent ? entryOf(w, e.parent) : undefined;
|
||||
if (!up?.parent) return false; // 맨 위로는 안 냄(새 공종 = 메뉴)
|
||||
e.parent = up.parent;
|
||||
if (!up || (!up.parent && !canHold(key))) return false; // 자재 줄 = 1단 안 됨
|
||||
pull(w.bill.rows, key);
|
||||
w.bill.rows.splice(find(w.bill.rows, up.key) + 1, 0, e);
|
||||
if (up.parent) e.parent = up.parent;
|
||||
else {
|
||||
// 새 공종(1단) = 직접비 맨 위 · 손 줄은 묶음(자재 줄 돈 갈래 바뀜 = fits 가 막음)
|
||||
delete e.parent;
|
||||
e.cost = "direct";
|
||||
if (!row.auto) Object.assign(e, { kind: "group", qty: null, ref: null });
|
||||
}
|
||||
// 맨 위로 = 관급자재대 앞 · 그 밖 = 옛 부모 바로 뒤
|
||||
const top = !up.parent && costOf(w, w.bill.rows, up.key) === "supply";
|
||||
w.bill.rows.splice(find(w.bill.rows, up.key) + (top ? 0 : 1), 0, e);
|
||||
});
|
||||
}
|
||||
if (!row.auto && row.kind === "item")
|
||||
@@ -315,7 +311,7 @@ export function billMenu(ctx: TabContext, rows: DataRow[], i: number): MapContex
|
||||
});
|
||||
// 지움 — 자식은 지운 줄 부모로 · 자동 줄 = gone · 자식 있는 맨 위 묶음은 못 지움
|
||||
if (!fixed(key) && !key.startsWith("mat:"))
|
||||
add(3, "줄 지우기", (w, e) => {
|
||||
add(3, "줄 삭제", (w, e) => {
|
||||
const list = w.bill.rows;
|
||||
if (!e.parent && list.some((c) => c.parent === key)) return false;
|
||||
for (const c of list) if (c.parent === key) c.parent = e.parent;
|
||||
|
||||
@@ -100,7 +100,7 @@ async function importLib(
|
||||
): Promise<void> {
|
||||
const projectId = localStorage.getItem(CURRENT_PROJECT_ID_KEY);
|
||||
if (!projectId) return;
|
||||
showToast("서고에서 들여오는 중…", "info");
|
||||
showToast("가져오는 중…", "info");
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/projects/${projectId}/estimation/library`, {
|
||||
method: "POST",
|
||||
@@ -119,11 +119,11 @@ async function importLib(
|
||||
const from = ((rest.sheets?.daega as Row[]) || []).find((r) => r.key === got.key);
|
||||
apply({ tab: "dg", key: got.key, lib: got.폴더 }, from);
|
||||
showToast(
|
||||
got.만듦 ? "새 호표로 연결함 — [저장]해야 연결이 남음" : "들여온 사본 호표로 연결함",
|
||||
got.만듦 ? "새 호표로 연결함 — [저장]해야 연결이 남음" : "가져온 호표로 연결함",
|
||||
"success",
|
||||
);
|
||||
} catch (error) {
|
||||
showToast(`서고에서 못 들여옴 — ${error instanceof Error ? error.message : error}`, "error");
|
||||
showToast(`못 가져옴 — ${error instanceof Error ? error.message : error}`, "error");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,12 +157,9 @@ export function linkItems(ctx: TabContext, ref: Row | null, apply: ApplyRef): [L
|
||||
const picks: Leaf[] = [
|
||||
["일위대가 호표", () => pickHopyo(ctx, apply, "dg")],
|
||||
["단가산출 호표", () => pickHopyo(ctx, apply, "sg")],
|
||||
["서고 구조물조합(마스터 · 회사 · 개인)", () => pickCombo(ctx, apply)],
|
||||
["서고 일위대가 사전조합(마스터 · 회사 · 개인)", () => pickPrecombo(ctx, apply)],
|
||||
[
|
||||
"단가산출 로직(M01 · 새 산근)",
|
||||
() => openLogicPicker((logic) => askInputs(apply, logic.키, logic.이름)),
|
||||
],
|
||||
["구조물 조합", () => pickCombo(ctx, apply)],
|
||||
["일위대가 사전조합", () => pickPrecombo(ctx, apply)],
|
||||
["단가산출", () => openLogicPicker((logic) => askInputs(apply, logic.키, logic.이름))],
|
||||
];
|
||||
return [picks, more];
|
||||
}
|
||||
|
||||
@@ -194,7 +194,7 @@ def test_새_산근_사본_뒤_번호_호표_탭_집계표(tmp_path):
|
||||
assert qty["LB000033"] >= round(10 * 0.195, 2) # 석공 = 손 줄 수량 × 0.195(+ 낙석망)
|
||||
|
||||
|
||||
def test_우클릭_메뉴_줄_추가_지우기_연결_TS():
|
||||
def test_우클릭_메뉴_항목_추가_줄_삭제_연결_TS():
|
||||
# 184-3 — 모든 줄 메뉴 · 셈 출력 줄 = 계약 꼴(parent · auto · auto_{칸}) · 첫 손질 = v2 로 굳힘
|
||||
src = _ts(["UI_Tabs", "UI_Tabs_Bill_Link", "UI_Tabs_Bill_Edit"])
|
||||
rows = [
|
||||
@@ -219,24 +219,24 @@ const run = (i, label) => {{
|
||||
return user.bill;
|
||||
}};
|
||||
const out = {{ title: labels(1), auto: labels(2), hand: labels(3) }};
|
||||
out.above = run(2, "위에 줄 추가");
|
||||
out.below = run(2, "항목 추가");
|
||||
out.reset = run(2, "자동값으로 되돌리기");
|
||||
out.left = run(3, "줄 지우기");
|
||||
out.left = run(3, "줄 삭제");
|
||||
console.log(JSON.stringify(out));
|
||||
"""
|
||||
out = json.loads(_node(src + body))
|
||||
add = ["위에 줄 추가", "아래에 줄 추가"]
|
||||
assert out["title"] == [*add, "아래에 하위 줄 추가", "새 공종 추가(1단)", "-", "줄 지우기"]
|
||||
assert out["auto"] == [*add, "새 공종 추가(1단)", "-", "연결", "-", "아래로 ↓", "내어쓰기 ←",
|
||||
"-", "자동값으로 되돌리기", "줄 지우기"] # fmt: skip
|
||||
# 246-1 더함 = 「항목 추가」 하나 · 지움 = 「줄 삭제」 · 2단 제목 ← = 1단으로
|
||||
assert out["title"] == ["항목 추가", "-", "내어쓰기 ←", "-", "줄 삭제"]
|
||||
assert out["auto"] == ["항목 추가", "-", "연결", "-", "아래로 ↓", "내어쓰기 ←",
|
||||
"-", "자동값으로 되돌리기", "줄 삭제"] # fmt: skip
|
||||
# 204 — 위 항목 아래로 들여쓰기 됨(그 항목 = 저절로 묶음)
|
||||
assert out["hand"] == [*add, "새 공종 추가(1단)", "-", "연결", "연결 풀기", "-", "위로 ↑",
|
||||
"들여쓰기 →", "내어쓰기 ←", "묶음 줄로 바꾸기", "-", "줄 지우기"] # fmt: skip
|
||||
# 위에 줄 = 같은 부모 앞 · 옛 꼴 → v2(옛 손 줄 칸 · 수량 덮개 옮김)
|
||||
above = out["above"]
|
||||
assert [e["key"][:5] for e in above["rows"]] == ["sec:1", "sec:1", "hand:", "earth", "hand:"]
|
||||
new, earth, hand = above["rows"][2:]
|
||||
assert (new["name"], new["parent"], above["v"]) == ("새 줄", "sec:1.2", 2)
|
||||
assert out["hand"] == ["항목 추가", "-", "연결", "연결 풀기", "-", "위로 ↑",
|
||||
"들여쓰기 →", "내어쓰기 ←", "묶음 줄로 바꾸기", "-", "줄 삭제"] # fmt: skip
|
||||
# 항목 추가(항목 줄) = 바로 아래 같은 부모 · 옛 꼴 → v2(옛 손 줄 칸 · 수량 덮개 옮김)
|
||||
below = out["below"]
|
||||
assert [e["key"][:5] for e in below["rows"]] == ["sec:1", "sec:1", "earth", "hand:", "hand:"]
|
||||
earth, new, hand = below["rows"][2:]
|
||||
assert (new["name"], new["parent"], below["v"]) == ("새 줄", "sec:1.2", 2)
|
||||
assert earth == {"key": "earth:4", "parent": "sec:1.2", "qty": 100}
|
||||
assert (hand["key"], hand["qty"], hand["ref"]) == ("hand:b", 4, HAND[1]["ref"])
|
||||
assert "qty" not in out["reset"]["rows"][2] # 자동값으로 = 수량 덮개 지움
|
||||
|
||||
@@ -189,15 +189,15 @@ const out = {
|
||||
g1: labels("hand:g1"), a: labels("hand:a"), g2: labels("hand:g2"),
|
||||
b: labels("hand:b"), x: labels("hand:x"), auto: labels("earth:4"),
|
||||
};
|
||||
out.child = order(run("hand:g2", "아래에 하위 줄 추가"));
|
||||
out.below = order(run("hand:g1", "아래에 줄 추가"));
|
||||
out.belowB = order(run("hand:b", "아래에 줄 추가"));
|
||||
out.child = order(run("hand:g2", "항목 추가"));
|
||||
out.below = order(run("hand:g1", "항목 추가"));
|
||||
out.belowB = order(run("hand:b", "항목 추가"));
|
||||
out.indentBill = run("hand:x", "들여쓰기 →");
|
||||
out.indent = order(out.indentBill);
|
||||
out.outdent = order(run("hand:g2", "내어쓰기 ←"));
|
||||
out.toItem = run("hand:g2", "항목으로 바꾸기").rows.filter((r) => ["hand:g2", "hand:b"].includes(r.key));
|
||||
out.toGroup = run("hand:x", "묶음 줄로 바꾸기").rows.find((r) => r.key === "hand:x");
|
||||
out.removed = order(run("hand:g1", "줄 지우기"));
|
||||
out.removed = order(run("hand:g1", "줄 삭제"));
|
||||
console.log(JSON.stringify(out));
|
||||
"""
|
||||
|
||||
@@ -221,15 +221,16 @@ def test_우클릭_레벨_메뉴_TS():
|
||||
out = json.loads(_node(_ts(["UI_Tabs", "UI_Tabs_Bill_Link", "UI_Tabs_Bill_Edit"]) + body))
|
||||
level = {"묶음 줄로 바꾸기", "항목으로 바꾸기", "들여쓰기 →", "내어쓰기 ←"}
|
||||
pick = lambda key: [x for x in out[key] if x in level] # noqa: E731
|
||||
assert out["g1"][:3] == ["위에 줄 추가", "아래에 줄 추가", "아래에 하위 줄 추가"]
|
||||
assert [x for x in out["g1"] if "추가" in x] == ["항목 추가"] # 246-1 더함 하나
|
||||
assert "연결" not in out["g1"] and pick("g1") == ["내어쓰기 ←", "항목으로 바꾸기"]
|
||||
assert pick("a") == ["내어쓰기 ←", "묶음 줄로 바꾸기"] # 위 형제 없음 → 들여쓰기 없음
|
||||
assert pick("g2") == ["내어쓰기 ←", "항목으로 바꾸기"] # 위 형제 a = 항목
|
||||
assert pick("b") == ["내어쓰기 ←", "묶음 줄로 바꾸기"] # 레벨 5 묶음까지
|
||||
assert pick("x") == ["들여쓰기 →", "내어쓰기 ←", "묶음 줄로 바꾸기"] # 위 형제 g1
|
||||
assert pick("auto") == ["내어쓰기 ←"] and "줄 지우기" in out["auto"] # 자동 줄도 지움
|
||||
assert out["child"] == _put(BASE, 6, ["NEW", "hand:g2"]) # g2 마지막 자식 뒤
|
||||
assert out["below"] == _put(BASE, 6, ["NEW", "sec:1.2"]) # g1 자손 끝 뒤 · 같은 부모
|
||||
assert pick("auto") == ["내어쓰기 ←"] and "줄 삭제" in out["auto"] # 자동 줄도 지움
|
||||
# 246-1 항목 추가 — 폴더 = 그 폴더 첫 하위 · 항목 = 바로 아래 같은 부모
|
||||
assert out["child"] == _put(BASE, 4, ["NEW", "hand:g2"])
|
||||
assert out["below"] == _put(BASE, 2, ["NEW", "hand:g1"])
|
||||
assert out["belowB"] == _put(BASE, 5, ["NEW", "hand:g2"])
|
||||
assert out["indent"] == [*BASE[:6], ["hand:x", "hand:g1"], BASE[7]] # g1 마지막 자식
|
||||
assert out["outdent"] == [*BASE[:3], ["hand:g2", "sec:1.2"], *BASE[4:]] # g1 뒤
|
||||
@@ -320,10 +321,10 @@ def test_연결_단추_칸_메뉴_고르기_창_TS():
|
||||
picks = [
|
||||
"일위대가 호표",
|
||||
"단가산출 호표",
|
||||
"서고 구조물조합(마스터 · 회사 · 개인)",
|
||||
"서고 일위대가 사전조합(마스터 · 회사 · 개인)",
|
||||
"단가산출 로직(M01 · 새 산근)",
|
||||
] # 204-2
|
||||
"구조물 조합",
|
||||
"일위대가 사전조합",
|
||||
"단가산출",
|
||||
] # 204-2 · 246-1 이름 셋
|
||||
# 누른 칸 아래(left · bottom) · 묶음 줄 = 안 엶 · 연결된 줄(자동 줄도) = 연결 풀기 더함
|
||||
unlink = [10, 40, [*picks, "-", "연결 풀기"]]
|
||||
assert out["menus"] == [unlink, unlink, [10, 40, picks]]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""184-3 B09 설계내역서 자유 편집 화면 — 모든 줄 우클릭 메뉴 · 칸 고침 → 덮개 · 지움 → gone · 옮김 · 새 공종 ·
|
||||
"""184-3 · 246-1 B09 설계내역서 자유 편집 화면 — 모든 줄 우클릭 메뉴 · 칸 고침 → 덮개 · 삭제 → gone · 옮김 · 새 공종(←) ·
|
||||
자동값으로 · 관급 옮김 막힘 · 깊이 5 · 고친 칸 강조 · [내역 초기화] 배선.
|
||||
|
||||
- 셈 출력 줄 = 계약 꼴(`Calc_Bill.py` 머리 · parent · auto · auto_{칸} · level · no · cost) 가짜 줄
|
||||
@@ -119,26 +119,26 @@ ORDER = [(r["key"], r["parent"]) for r in ROWS if r["key"] != "fee"]
|
||||
def test_메뉴_모든_줄():
|
||||
out = _menu({}, labels=["sec:1", "earth:4", "earth:5", "hand:g", "hand:a", "fee", "mat:M2",
|
||||
"mat:M1", "sec:G"])["labels"] # fmt: skip
|
||||
add = ["위에 줄 추가", "아래에 줄 추가"]
|
||||
# 맨 위 제목 = 하위 줄 · 새 공종 · 아래로(자식 있는 맨 위 = 못 지움)
|
||||
assert out["sec:1"] == ["아래에 하위 줄 추가", "새 공종 추가(1단)", "-", "아래로 ↓"]
|
||||
# 자동 항목 = 더함 · 연결 · 옮김 · 지움(고친 칸 없음 = 자동값으로 없음)
|
||||
assert out["earth:4"] == [*add, "새 공종 추가(1단)", "-", "연결", "연결 풀기", "-",
|
||||
"아래로 ↓", "내어쓰기 ←", "-", "줄 지우기"] # fmt: skip
|
||||
add = ["항목 추가"] # 246-1 더함 하나
|
||||
# 맨 위 제목 = 항목 추가(첫 하위) · 아래로(자식 있는 맨 위 = 못 지움 · 맨 위 = 내어쓰기 없음)
|
||||
assert out["sec:1"] == [*add, "-", "아래로 ↓"]
|
||||
# 자동 항목 = 더함 · 연결 · 옮김 · 삭제(고친 칸 없음 = 자동값으로 없음)
|
||||
assert out["earth:4"] == [*add, "-", "연결", "연결 풀기", "-",
|
||||
"아래로 ↓", "내어쓰기 ←", "-", "줄 삭제"] # fmt: skip
|
||||
# 204 — 위 형제가 항목이어도 들여쓰기 됨(그 항목 = 저절로 묶음)
|
||||
assert out["earth:5"] == [*add, "새 공종 추가(1단)", "-", "연결", "-", "위로 ↑", "아래로 ↓",
|
||||
assert out["earth:5"] == [*add, "-", "연결", "-", "위로 ↑", "아래로 ↓",
|
||||
"들여쓰기 →", "내어쓰기 ←", "-", "자동값으로 되돌리기",
|
||||
"줄 지우기"] # fmt: skip
|
||||
assert out["hand:g"] == [*add, "아래에 하위 줄 추가", "새 공종 추가(1단)", "-", "위로 ↑",
|
||||
"아래로 ↓", "들여쓰기 →", "내어쓰기 ←", "항목으로 바꾸기", "-",
|
||||
"줄 지우기"] # fmt: skip
|
||||
assert out["hand:a"] == [*add, "새 공종 추가(1단)", "-", "연결", "연결 풀기", "-", "내어쓰기 ←",
|
||||
"묶음 줄로 바꾸기", "-", "줄 지우기"] # fmt: skip
|
||||
# 조달수수료 · 관급 자재 = 연결 · 옮김 · 지움 없음 · 관급자재대 제목 = 옮김 · 지움 없음
|
||||
assert out["fee"] == ["위에 줄 추가", "새 공종 추가(1단)"] # 조달수수료 = 관급자재대 끝 고정
|
||||
assert out["mat:M2"] == [*add, "새 공종 추가(1단)"]
|
||||
assert out["mat:M1"] == [*add, "새 공종 추가(1단)"]
|
||||
assert out["sec:G"] == ["아래에 하위 줄 추가", "새 공종 추가(1단)"]
|
||||
"줄 삭제"] # fmt: skip
|
||||
assert out["hand:g"] == [*add, "-", "위로 ↑", "아래로 ↓", "들여쓰기 →", "내어쓰기 ←",
|
||||
"항목으로 바꾸기", "-", "줄 삭제"] # fmt: skip
|
||||
assert out["hand:a"] == [*add, "-", "연결", "연결 풀기", "-", "내어쓰기 ←",
|
||||
"묶음 줄로 바꾸기", "-", "줄 삭제"] # fmt: skip
|
||||
# 조달수수료 · 관급 자재 = 연결 · 옮김 · 삭제 없음 · 관급자재대 제목 = 옮김 · 삭제 없음 ·
|
||||
# 자재 줄 = 1단으로 못 냄(별도 자재 mat:M1 도)
|
||||
assert out["fee"] == add # 조달수수료 = 관급자재대 끝 고정
|
||||
assert out["mat:M2"] == add
|
||||
assert out["mat:M1"] == add
|
||||
assert out["sec:G"] == add
|
||||
|
||||
|
||||
def test_칸_고침_덮개_옛_꼴_굳힘():
|
||||
@@ -166,38 +166,34 @@ V2 = {"bill": {"v": 2, "rows": [{"key": k, **({"parent": p} if p else {})} for k
|
||||
if k != "hand:b"], "gone": []}} # fmt: skip
|
||||
|
||||
|
||||
def test_지움_옮김_새_공종_자동값으로():
|
||||
def test_삭제_옮김_새_공종_자동값으로():
|
||||
user = {"bill": {**V2["bill"], "rows": [
|
||||
{**e, **({"qty": 9} if e["key"] == "earth:5" else {})} for e in V2["bill"]["rows"]]}} # fmt: skip
|
||||
runs = [("earth:4", "줄 지우기"), ("hand:g", "줄 지우기"), ("earth:5", "위로 ↑"),
|
||||
("earth:4", "아래로 ↓"), ("earth:4", "새 공종 추가(1단)"),
|
||||
("mat:M2", "새 공종 추가(1단)"), ("earth:5", "자동값으로 되돌리기"),
|
||||
("hand:b", "들여쓰기 →"), ("hand:a", "내어쓰기 ←"), ("sec:1.2", "아래에 하위 줄 추가"),
|
||||
("earth:4", "위에 줄 추가"), ("hand:a", "묶음 줄로 바꾸기"), ("hand:g", "항목으로 바꾸기"),
|
||||
("earth:4", "연결 풀기"), ("earth:4", "내어쓰기 ←"), ("fee", "위에 줄 추가"),
|
||||
("fee", "새 공종 추가(1단)")] # fmt: skip
|
||||
runs = [("earth:4", "줄 삭제"), ("hand:g", "줄 삭제"), ("earth:5", "위로 ↑"),
|
||||
("earth:4", "아래로 ↓"), ("sec:1.2", "내어쓰기 ←"),
|
||||
("mat:M2", "내어쓰기 ←"), ("earth:5", "자동값으로 되돌리기"),
|
||||
("hand:b", "들여쓰기 →"), ("hand:a", "내어쓰기 ←"), ("sec:1.2", "항목 추가"),
|
||||
("earth:4", "항목 추가"), ("hand:a", "묶음 줄로 바꾸기"), ("hand:g", "항목으로 바꾸기"),
|
||||
("earth:4", "연결 풀기"), ("earth:4", "내어쓰기 ←"), ("fee", "항목 추가")] # fmt: skip
|
||||
out = _menu(user, runs=runs)["runs"]
|
||||
keys = lambda b: [k for k, _ in _order(b)] # noqa: E731
|
||||
# 덮개에 없는 보이는 줄(hand:b · 새 자동 줄) = 보이는 앞 줄 뒤에 굳힘
|
||||
base = keys(out["earth:5|위로 ↑"])
|
||||
assert "hand:b" in base and base.index("hand:b") == base.index("hand:a") + 1
|
||||
# 지움 — 자동 줄 = gone · 새 줄 = 자식은 지운 줄 부모로
|
||||
b = out["earth:4|줄 지우기"]
|
||||
b = out["earth:4|줄 삭제"]
|
||||
assert b["gone"] == ["earth:4"] and "earth:4" not in keys(b)
|
||||
b = out["hand:g|줄 지우기"]
|
||||
b = out["hand:g|줄 삭제"]
|
||||
assert b["gone"] == [] and ("hand:a", "sec:1.2") in _order(b) and "hand:g" not in keys(b)
|
||||
# 위로 · 아래로 = 같은 부모 형제와 자리 바꿈
|
||||
for k in ("earth:5|위로 ↑", "earth:4|아래로 ↓"):
|
||||
got = keys(out[k])
|
||||
assert got[got.index("earth:5") + 1] == "earth:4"
|
||||
# 새 공종 = 맨 위 묶음 · cost direct · 그 맨 위 공종 뒤 / 관급자재대 앞
|
||||
b = out["earth:4|새 공종 추가(1단)"]
|
||||
new = [e for e in b["rows"] if e["key"].startswith("hand:") and e.get("cost")][0]
|
||||
assert (new["kind"], new["cost"], new["name"], "parent" in new) == (
|
||||
"group", "direct", "새 공종", False) # fmt: skip
|
||||
assert keys(b)[keys(b).index(new["key"]) + 1] == "sec:4"
|
||||
b = out["mat:M2|새 공종 추가(1단)"]
|
||||
assert keys(b)[keys(b).index("sec:G") - 1].startswith("hand:")
|
||||
# 246-1 새 공종 = 2단을 ← 로 맨 위(cost direct · 옛 부모 바로 뒤) · 관급 자재 = 못 냄
|
||||
b = out["sec:1.2|내어쓰기 ←"]
|
||||
assert [k for k, p in _order(b) if p is None] == ["sec:1", "sec:1.2", "sec:4", "sec:G"]
|
||||
assert {e["key"]: e for e in b["rows"]}["sec:1.2"]["cost"] == "direct"
|
||||
assert out["mat:M2|내어쓰기 ←"] is None
|
||||
# 자동값으로 = 고친 칸 지움(차례 · 부모 그대로)
|
||||
b = out["earth:5|자동값으로 되돌리기"]
|
||||
assert {e["key"]: e for e in b["rows"]}["earth:5"] == {"key": "earth:5", "parent": "sec:1.2"}
|
||||
@@ -207,23 +203,22 @@ def test_지움_옮김_새_공종_자동값으로():
|
||||
assert got[got.index(("hand:g", "sec:1.2")) + 1] == ("hand:a", "sec:1.2")
|
||||
got = _order(out["earth:4|내어쓰기 ←"]) # 1.1 → 1 아래(제목 뒤)
|
||||
assert got[got.index(("sec:1.2", "sec:1")) + 1 :].index(("earth:4", "sec:1")) >= 0
|
||||
# 하위 줄 = 마지막 자식 뒤 · 위에 줄 = 같은 부모 앞 · 새 줄 = kind 없음(항목)
|
||||
got = out["sec:1.2|아래에 하위 줄 추가"]["rows"]
|
||||
# 246-1 항목 추가 — 폴더 = 첫 하위 · 항목 = 바로 아래 같은 부모 · 새 줄 = kind 없음(항목)
|
||||
got = out["sec:1.2|항목 추가"]["rows"]
|
||||
kids = [e for e in got if e.get("parent") == "sec:1.2"]
|
||||
assert kids[-1]["name"] == "새 줄" and "kind" not in kids[-1]
|
||||
got = out["earth:4|위에 줄 추가"]["rows"]
|
||||
assert kids[0]["name"] == "새 줄" and "kind" not in kids[0]
|
||||
got = out["earth:4|항목 추가"]["rows"]
|
||||
at = [e["key"] for e in got].index("earth:4")
|
||||
assert (got[at - 1]["name"], got[at - 1]["parent"]) == ("새 줄", "sec:1.2")
|
||||
assert (got[at + 1]["name"], got[at + 1]["parent"]) == ("새 줄", "sec:1.2")
|
||||
a = {e["key"]: e for e in out["hand:a|묶음 줄로 바꾸기"]["rows"]}["hand:a"]
|
||||
assert (a["kind"], a["qty"], a["ref"]) == ("group", None, None)
|
||||
got = {e["key"]: e for e in out["hand:g|항목으로 바꾸기"]["rows"]}
|
||||
assert "kind" not in got["hand:g"] and got["hand:a"]["parent"] == "sec:1.2"
|
||||
# 자동 줄 연결 풀기 = ref 덮개 null
|
||||
assert {e["key"]: e for e in out["earth:4|연결 풀기"]["rows"]}["earth:4"]["ref"] is None
|
||||
# 조달수수료 위 = 관급자재대 마지막 자식(조달수수료는 셈이 끝에 둠) · 새 공종 = 관급자재대 앞
|
||||
got = out["fee|위에 줄 추가"]["rows"]
|
||||
# 조달수수료 항목 추가 = 관급자재대 마지막 자식(조달수수료는 셈이 끝에 둠)
|
||||
got = out["fee|항목 추가"]["rows"]
|
||||
assert (got[-1]["name"], got[-1]["parent"], got[-2]["key"]) == ("새 줄", "sec:G", "mat:M2")
|
||||
assert keys(out["fee|새 공종 추가(1단)"])[-3].startswith("hand:") # … 새 공종 · sec:G · mat:M2
|
||||
|
||||
|
||||
def test_관급_옮김_막힘_깊이_5():
|
||||
@@ -237,15 +232,15 @@ def test_관급_옮김_막힘_깊이_5():
|
||||
out = _menu(user, labels=["sec:4", "hand:n"], rows=rows)["labels"]
|
||||
assert "들여쓰기 →" not in out["sec:4"] # 별도 자재가 관급자재대 안으로 → 막음
|
||||
assert "들여쓰기 →" in out["hand:n"] # 자재 없는 묶음 = 들여쓰기 됨(위 형제 4 자재대)
|
||||
# 깊이 5 — 레벨 5 묶음에 하위 줄 · 레벨 5 줄 들여쓰기 = 막음
|
||||
# 깊이 5 — 레벨 5 묶음에 항목 추가(첫 하위) · 레벨 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),
|
||||
] # fmt: skip
|
||||
out = _menu({}, labels=["hand:g3", "hand:z", "hand:g2"], rows=deep)["labels"]
|
||||
assert "아래에 하위 줄 추가" not in out["hand:g3"] and "들여쓰기 →" not in out["hand:z"]
|
||||
assert "아래에 하위 줄 추가" in out["hand:g2"]
|
||||
assert "항목 추가" not in out["hand:g3"] and "들여쓰기 →" not in out["hand:z"]
|
||||
assert "항목 추가" in out["hand:g2"]
|
||||
|
||||
|
||||
TABLE_JS = """
|
||||
|
||||
@@ -202,9 +202,9 @@ const applied = [];
|
||||
const apply = (ref, from) => applied.push([ref, from && from.name]);
|
||||
const [picks, more] = linkItems(ctx, { tab: "dg", key: "DG:a" }, apply);
|
||||
const out = { picks: picks.map((x) => x[0]), more: more.map((x) => x[0]) };
|
||||
picks.find((x) => x[0].startsWith("서고 일위대가"))[1]();
|
||||
picks.find((x) => x[0] === "일위대가 사전조합")[1]();
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
picks.find((x) => x[0].startsWith("서고 구조물"))[1]();
|
||||
picks.find((x) => x[0] === "구조물 조합")[1]();
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
Object.assign(out, { sent, applied, payload: ctx.payload, toasts });
|
||||
@@ -226,10 +226,10 @@ def test_화면_출처_다섯_사전조합_창_들여오기_줄_연결_TS():
|
||||
assert out["picks"] == [
|
||||
"일위대가 호표",
|
||||
"단가산출 호표",
|
||||
"서고 구조물조합(마스터 · 회사 · 개인)",
|
||||
"서고 일위대가 사전조합(마스터 · 회사 · 개인)",
|
||||
"단가산출 로직(M01 · 새 산근)",
|
||||
]
|
||||
"구조물 조합",
|
||||
"일위대가 사전조합",
|
||||
"단가산출",
|
||||
] # 246-1 이름 셋(서고 · 층 · M01 글 없음)
|
||||
assert out["more"] == ["연결 풀기"]
|
||||
pre, (url, body) = out["sent"][0], out["sent"][1]
|
||||
assert pre == ["pre", "personal", True, "일위대가 사전조합 고르기"] # 서고 줄만 · 층 셋
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""PLAN 241-1 · 123 — B08 · B09 표 입력 칸 = 채움 색 대신 칸 안쪽 테두리."""
|
||||
"""PLAN 241-1 · 123 → 246-1 · 128 — B08 · B09 표 입력 칸 = 테두리 되돌림(241 앞 채움 색)."""
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
@@ -13,17 +13,13 @@ def _rule(css: str, selector: str) -> str:
|
||||
return m.group(1)
|
||||
|
||||
|
||||
def test_입력_칸_채움_색_없고_안쪽_테두리():
|
||||
def test_입력_칸_채움_색_테두리_없음():
|
||||
body = _rule(CSS.read_text(encoding="utf-8"), ".ui-data-table__input")
|
||||
assert "background: transparent;" in body
|
||||
assert "background: color-mix" not in body # 옛 accent 5% 채움 없음
|
||||
# 241-2 두 테마 — 진한 accent 60% + 캔버스(밝음 · 어두움 모두 캔버스 대비 3:1 넘음 · 초점 2px 와 갈림)
|
||||
assert (
|
||||
"box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--color-accent) 60%, var(--color-canvas));"
|
||||
in body
|
||||
)
|
||||
# 246-1 — 241 앞 모양(accent 5% 채움) · 안쪽 테두리 없음
|
||||
assert "background: color-mix(in srgb, var(--color-accent) 5%, var(--color-canvas));" in body
|
||||
assert "box-shadow" not in body
|
||||
assert "#" not in body # 하드코딩 색 없음 — 테마 변수만
|
||||
assert "border: 0;" in body # 테두리는 inset — 칸 크기 · 줄 높이 불변
|
||||
assert "border: 0;" in body
|
||||
assert "height: var(--dt-row-h);" in body
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""243-1 · 126 — B08 · B09 값 고칠 수 있는 표 전부 더블클릭 입력 · 고칠 칸 테두리 늘 보임.
|
||||
"""243-1 · 126 — B08 · B09 값 고칠 수 있는 표 전부 더블클릭 입력 · 고칠 칸 테두리 = 246-1 에서 지움.
|
||||
|
||||
- 공용 표 editOnDouble = 한 번 누름 = 글 상자 안 열림 · 더블클릭 · 칸 누른 뒤 Enter · F2 = 그 칸 글 상자
|
||||
- 고칠 수 있는 칸(td) = 글일 때도 `is-can-edit`(입력 칸 테두리) · 못 고치는 칸 · 옛 방식 표 = 없음
|
||||
- 고칠 수 있는 칸(td) 표시 `is-can-edit` 없음(246-1 · 241 앞 모양)
|
||||
- B09 = 표 만드는 길 한 곳(Shell) · B08 = 고칠 칸 있는 표(자재 집계표 셋) 모두 editOnDouble
|
||||
- 칸 누름으로 여는 메뉴 · 고르기 · 이동(onCellClick · choices · link)은 그대로
|
||||
"""
|
||||
@@ -74,9 +74,9 @@ setTimeout(() => {
|
||||
"""
|
||||
|
||||
|
||||
def test_공용_표_더블클릭_Enter_F2_고칠_칸_테두리():
|
||||
def test_공용_표_더블클릭_Enter_F2():
|
||||
out = json.loads(_node(TABLE + HARNESS + SCENE_JS))
|
||||
assert out["can"] == ["3"] # 고칠 수 있는 칸만 · 글일 때도 표시
|
||||
assert out["can"] == [] # 246-1 고칠 칸 td 표시 없음
|
||||
assert out["start"] == [] # 처음 = 글 상자 없음
|
||||
assert out["click"] == [[], ["click 1:qty"]] # 한 번 누름 = 안 열림 · 누름 알림은 그대로
|
||||
assert out["f2"] == ["1:qty"] # F2 = 누른 칸 입력
|
||||
@@ -90,18 +90,10 @@ def test_공용_표_더블클릭_Enter_F2_고칠_칸_테두리():
|
||||
assert out["old"] == [["1:qty"], []] # editOnDouble 없는 표 = 예전대로(테두리 class 없음)
|
||||
|
||||
|
||||
def test_고칠_칸_테두리_CSS_테마_변수만():
|
||||
def test_고칠_칸_테두리_CSS_없음():
|
||||
css = (ROOT / "ui_template" / "ui_template_data_table.css").read_text("utf-8")
|
||||
m = re.search(r"\.ui-data-table__table td\.is-can-edit \{([^}]*)\}", css)
|
||||
assert m
|
||||
body = m.group(1)
|
||||
# 글 상자와 같은 색(accent 60% · canvas) — 줄 고름 box-shadow 와 안 겹치게 outline 안쪽
|
||||
assert (
|
||||
"outline: 1px solid color-mix(in srgb, var(--color-accent) 60%, var(--color-canvas));"
|
||||
in body
|
||||
)
|
||||
assert "outline-offset: -1px;" in body
|
||||
assert "#" not in body
|
||||
ts = (ROOT / "ui_template" / "ui_template_data_table.ts").read_text("utf-8")
|
||||
assert "is-can-edit" not in css and "is-can-edit" not in ts # 246-1 되돌림
|
||||
|
||||
|
||||
def _sources(*folders: str) -> list[Path]:
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
"""246-1 · 128 — B09 설계내역서 우클릭 메뉴 줄임 · 입력 칸 테두리 되돌림.
|
||||
|
||||
- 입력 칸 = 241 앞 모양(글 상자 accent 5% 채움 · td 표시 없음)
|
||||
- 더함 = 「항목 추가」 하나 — 항목 = 바로 아래 같은 부모 · 폴더 = 그 폴더 첫 하위
|
||||
- 맨 위 1단(새 공종) = ← 내어쓰기 — 직접비 · 손 줄은 묶음 · 관급자재대 앞 · 자재 줄은 못 냄
|
||||
- 연결 이름 = 「구조물 조합」 · 「일위대가 사전조합」 · 「단가산출」 · 지움 = 「줄 삭제」
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
from test_184_3_b09_bill_menu import DG, ROWS, _menu, _order, _row # noqa: E402
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
OLD_ADDS = ["위에 줄 추가", "아래에 줄 추가", "아래에 하위 줄 추가", "새 공종 추가(1단)"]
|
||||
|
||||
|
||||
def test_입력_칸_테두리_없음():
|
||||
css = (ROOT / "ui_template" / "ui_template_data_table.css").read_text("utf-8")
|
||||
body = css.split(".ui-data-table__input {")[1].split("}")[0]
|
||||
assert "background: color-mix(in srgb, var(--color-accent) 5%, var(--color-canvas));" in body
|
||||
assert "box-shadow" not in body and "is-can-edit" not in css
|
||||
|
||||
|
||||
def test_더함_하나_줄_삭제_연결_이름_셋():
|
||||
keys = ["sec:1", "earth:4", "hand:g", "hand:a", "fee", "sec:G"]
|
||||
out = _menu({}, labels=keys)["labels"]
|
||||
for key in keys:
|
||||
assert [x for x in out[key] if "추가" in x] == ["항목 추가"], key
|
||||
assert not set(OLD_ADDS) & set(out[key]) and "줄 지우기" not in out[key]
|
||||
assert "줄 삭제" in out["earth:4"] and "줄 삭제" in out["hand:a"]
|
||||
link = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Tabs_Bill_Link.ts").read_text("utf-8")
|
||||
picks = link.split("const picks: Leaf[] = [")[1].split("];")[0]
|
||||
for name in ["일위대가 호표", "단가산출 호표", "구조물 조합", "일위대가 사전조합", "단가산출"]:
|
||||
assert f'["{name}",' in picks
|
||||
for word in ["서고", "마스터", "회사", "개인", "M01", "새 산근", "로직"]:
|
||||
assert word not in picks
|
||||
toasts = [part.split(",")[0] for part in link.split("showToast(")[1:]]
|
||||
assert toasts and not any("서고" in t for t in toasts) # 알림 글 쉬운 말
|
||||
bill = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Tabs_Bill.ts").read_text("utf-8")
|
||||
assert "항목 추가 · 줄 삭제" in bill # 표 아래 안내 글
|
||||
|
||||
|
||||
#: 관급자재대 안 손 묶음(hand:s · 자재 없음) · 손 항목(hand:t)
|
||||
SUPPLY = [*ROWS[:-1], _row("hand:s", "sec:G", 2, "group", cost="supply"),
|
||||
_row("hand:t", "hand:s", 3, cost="supply", qty=1, ref=DG), ROWS[-1]] # fmt: skip
|
||||
|
||||
|
||||
def test_자리_규칙():
|
||||
runs = [("hand:g", "항목 추가"), ("hand:a", "항목 추가"), ("hand:b", "항목 추가"),
|
||||
("hand:a", "내어쓰기 ←"), ("hand:g", "내어쓰기 ←"), ("hand:s", "내어쓰기 ←"),
|
||||
("hand:t", "내어쓰기 ←"), ("mat:M1", "내어쓰기 ←")] # fmt: skip
|
||||
out = _menu({}, runs=runs, rows=SUPPLY)["runs"]
|
||||
new = lambda b: [(e.get("parent"), e.get("name", e["key"])) for e in b["rows"]] # noqa: E731
|
||||
# 폴더 = 그 폴더 첫 하위 · 항목 = 바로 아래 같은 부모
|
||||
got = new(out["hand:g|항목 추가"])
|
||||
assert got[got.index(("sec:1.2", "hand:g")) + 1] == ("hand:g", "새 줄")
|
||||
got = [e["key"] for e in out["hand:a|항목 추가"]["rows"]]
|
||||
row = out["hand:a|항목 추가"]["rows"][got.index("hand:a") + 1]
|
||||
assert (row["name"], row["parent"]) == ("새 줄", "hand:g")
|
||||
got = out["hand:b|항목 추가"]["rows"]
|
||||
row = got[[e["key"] for e in got].index("hand:b") + 1]
|
||||
assert (row["name"], row["parent"], "kind" in row) == ("새 줄", "sec:1.2", False)
|
||||
# ← = 한 단씩 — 레벨 4 → 3(sec:1.2 아래) · 레벨 3 → 2(sec:1 아래)
|
||||
assert ("hand:a", "sec:1.2") in _order(out["hand:a|내어쓰기 ←"])
|
||||
assert ("hand:g", "sec:1") in _order(out["hand:g|내어쓰기 ←"])
|
||||
# 관급 안 손 묶음 ← = 새 공종(직접비) · 관급자재대 앞
|
||||
b = out["hand:s|내어쓰기 ←"]
|
||||
tops = [k for k, p in _order(b) if p is None]
|
||||
assert tops == ["sec:1", "sec:4", "hand:s", "sec:G"]
|
||||
s = {e["key"]: e for e in b["rows"]}["hand:s"]
|
||||
assert (s["kind"], s["cost"]) == ("group", "direct")
|
||||
# 손 항목 ← 관급 묶음 밖(sec:G 아래) · 자재 줄 = 1단 안 됨
|
||||
assert ("hand:t", "sec:G") in _order(out["hand:t|내어쓰기 ←"])
|
||||
assert out["mat:M1|내어쓰기 ←"] is None
|
||||
# 1단 손 항목 → 묶음(수량 · 연결 지움) — 2단 손 항목을 두 번 냄
|
||||
one = _menu({"bill": out["hand:t|내어쓰기 ←"]}, runs=[("hand:t", "내어쓰기 ←")],
|
||||
rows=SUPPLY)["runs"]["hand:t|내어쓰기 ←"] # fmt: skip
|
||||
t = {e["key"]: e for e in one["rows"]}["hand:t"]
|
||||
assert ("parent" in t, t["kind"], t["qty"], t["ref"], t["cost"]) == (
|
||||
False, "group", None, None, "direct") # fmt: skip
|
||||
@@ -0,0 +1,168 @@
|
||||
"""246-2 · 129 — 공용 우클릭 메뉴 하위 메뉴(children) = 「글 ▸」 한 줄 + 오른쪽 옆 새 판.
|
||||
|
||||
- 올림 · 누름 · → = 옆 판(머리 줄 높이 · 메뉴 오른쪽) · 다른 줄 올림 · ← = 닫힘(← 는 머리로 초점)
|
||||
- 화면 오른쪽 끝이면 메뉴 왼쪽 · 옆 판 고름 = 메뉴 닫고 동작 · 바깥 누름 · Esc = 모두 닫힘
|
||||
- 옛 아코디언(같은 판 안 펼침 · ▾) 없음 · B09 「연결」 = children 그대로(옆 판으로 감)
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
IMPORTS = re.compile(r"^import[^;]*;\r?\n", re.MULTILINE)
|
||||
|
||||
DOM_JS = """
|
||||
const docOn = {};
|
||||
class El {
|
||||
constructor(tag) { Object.assign(this, { tag, kids: [], parent: null, on: {}, style: {},
|
||||
className: "", hidden: false, textContent: "", attrs: {} }); }
|
||||
get classList() {
|
||||
const el = this;
|
||||
const list = () => el.className.split(" ").filter(Boolean);
|
||||
return { add: (c) => { if (!list().includes(c)) el.className = [...list(), c].join(" "); },
|
||||
remove: (c) => (el.className = list().filter((x) => x !== c).join(" ")),
|
||||
contains: (c) => list().includes(c) };
|
||||
}
|
||||
get ownerDocument() { return { defaultView: globalThis.view }; }
|
||||
get firstElementChild() { return this.kids[0] ?? null; }
|
||||
get lastElementChild() { return this.kids.at(-1) ?? null; }
|
||||
setAttribute(k, v) { this.attrs[k] = v; }
|
||||
append(...xs) { for (const x of xs) { x.parent = this; this.kids.push(x); } }
|
||||
replaceChildren() { this.kids = []; }
|
||||
remove() { if (this.parent) this.parent.kids = this.parent.kids.filter((x) => x !== this); }
|
||||
contains(x) { for (let at = x; at; at = at.parent) if (at === this) return true; return false; }
|
||||
addEventListener(t, f) { (this.on[t] ??= []).push(f); }
|
||||
fire(t, ev = {}) { for (const f of this.on[t] ?? []) f({ target: this, preventDefault() {},
|
||||
stopPropagation() {}, ...ev }); }
|
||||
focus() { doc.activeElement = this; }
|
||||
all() { return this.kids.flatMap((k) => [k, ...k.all()]); }
|
||||
match(sel) { return sel.startsWith(".") ? sel.slice(1).split(".").every((c) => this.classList.contains(c))
|
||||
: this.tag === sel; }
|
||||
querySelectorAll(sel) {
|
||||
let found = [this];
|
||||
for (const part of sel.split(" ")) found = found.flatMap((el) => el.all().filter((k) => k.match(part)));
|
||||
return found;
|
||||
}
|
||||
querySelector(sel) { return this.querySelectorAll(sel)[0] ?? null; }
|
||||
getBoundingClientRect() {
|
||||
if (this.style.left !== undefined) {
|
||||
const side = this.classList.contains("ui-ctx-menu__side");
|
||||
const left = parseFloat(this.style.left), top = parseFloat(this.style.top);
|
||||
const width = side ? 150 : 160;
|
||||
const height = 8 + 30 * this.kids.filter((k) => k.tag === "button").length;
|
||||
return { left, top, width, height, right: left + width, bottom: top + height };
|
||||
}
|
||||
const box = this.parent.getBoundingClientRect();
|
||||
const top = box.top + 4 + 30 * this.parent.kids.filter((k) => k.tag === "button").indexOf(this);
|
||||
return { left: box.left + 4, top, width: box.width - 8, height: 30, right: box.right - 4, bottom: top + 30 };
|
||||
}
|
||||
}
|
||||
const doc = { activeElement: null, createElement: (t) => new El(t),
|
||||
addEventListener: (t, f) => (docOn[t] ??= []).push(f) };
|
||||
globalThis.document = doc;
|
||||
globalThis.Node = El;
|
||||
globalThis.view = { innerWidth: 1000, innerHeight: 800 };
|
||||
globalThis.window = globalThis.view;
|
||||
"""
|
||||
|
||||
SCENE_JS = """
|
||||
const log = [];
|
||||
const menu = createMapContextMenu("b09-estimation");
|
||||
const items = [["A", () => log.push("A")],
|
||||
{ label: "연결", children: [["x", () => log.push("x")], ["y", () => log.push("y")]] }, "-",
|
||||
["B", () => log.push("B")]];
|
||||
const el = menu.element;
|
||||
const btns = () => el.kids.filter((k) => k.tag === "button");
|
||||
const side = () => el.querySelector(".ui-ctx-menu__side");
|
||||
const sideText = () => side()?.kids.map((k) => k.textContent) ?? null;
|
||||
const at = () => [parseFloat(side().style.left), parseFloat(side().style.top), side().style.position];
|
||||
const out = {};
|
||||
menu.open(100, 100, items);
|
||||
const head = btns()[1];
|
||||
out.open = [el.kids.map((k) => k.textContent || k.className), sideText(), head.attrs["aria-haspopup"]];
|
||||
head.fire("pointerenter");
|
||||
out.hover = [sideText(), at(), head.classList.contains("is-open"), side().className];
|
||||
out.firstRow = side().kids[0].getBoundingClientRect().top === head.getBoundingClientRect().top;
|
||||
out.contains = menu.contains(side().kids[0]);
|
||||
btns()[0].fire("pointerenter");
|
||||
out.leave = [sideText(), head.classList.contains("is-open")];
|
||||
head.fire("click");
|
||||
out.click = [sideText(), doc.activeElement?.textContent];
|
||||
side().fire("keydown", { key: "ArrowLeft" });
|
||||
out.left = [sideText(), doc.activeElement === head];
|
||||
head.fire("keydown", { key: "ArrowRight" });
|
||||
out.right = [sideText(), doc.activeElement?.textContent];
|
||||
side().kids[1].fire("click");
|
||||
out.pick = [el.hidden, el.kids.length, [...log]];
|
||||
// 화면 오른쪽 끝 — 메뉴는 안쪽으로 당김 · 옆 판 = 메뉴 왼쪽
|
||||
view.innerWidth = 400;
|
||||
menu.open(300, 100, items);
|
||||
btns()[1].fire("pointerenter");
|
||||
out.edge = [parseFloat(el.style.left), at()[0]];
|
||||
// 아래 끝 — 옆 판 올림
|
||||
view.innerWidth = 1000; view.innerHeight = 100;
|
||||
menu.open(100, 10, items);
|
||||
btns()[1].fire("pointerenter");
|
||||
out.bottom = at()[1];
|
||||
view.innerHeight = 800;
|
||||
// Esc · 바깥 누름 = 모두 닫힘
|
||||
menu.open(100, 100, items);
|
||||
btns()[1].fire("pointerenter");
|
||||
for (const f of docOn.keydown) f({ key: "Escape", stopPropagation() {}, preventDefault() {} });
|
||||
out.esc = [el.hidden, side()];
|
||||
menu.open(100, 100, items);
|
||||
btns()[1].fire("pointerenter");
|
||||
const inside = side().kids[0];
|
||||
for (const f of docOn.pointerdown) f({ target: inside });
|
||||
out.insideDown = el.hidden;
|
||||
for (const f of docOn.pointerdown) f({ target: new El("div") });
|
||||
out.outsideDown = el.hidden;
|
||||
console.log(JSON.stringify(out));
|
||||
"""
|
||||
|
||||
|
||||
def _node(source: str) -> str:
|
||||
result = subprocess.run(
|
||||
["node", "--experimental-strip-types", "--no-warnings",
|
||||
"--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
|
||||
|
||||
|
||||
def _src() -> str:
|
||||
return (ROOT / "ui_template" / "ui_template_context_menu.ts").read_text("utf-8")
|
||||
|
||||
|
||||
def test_옆_판_열기_닫기_자리():
|
||||
out = json.loads(_node(DOM_JS + IMPORTS.sub("", _src()) + SCENE_JS))
|
||||
# 「연결 ▸」 한 줄 · 처음엔 옆 판 없음(같은 판 안 펼침 없음)
|
||||
assert out["open"] == [["A", "연결 ▸", "ui-ctx-menu__sep", "B"], None, "menu"]
|
||||
# 올림 = 옆 판 다섯 자리 — 메뉴 오른쪽(100 + 160) · 첫 줄 = 머리 줄 높이(134 - 4) · fixed
|
||||
names = "b09-estimation__context-menu-sub ui-ctx-menu ui-ctx-menu__side"
|
||||
assert out["hover"] == [["x", "y"], [260, 130, "fixed"], True, names]
|
||||
assert out["firstRow"] is True and out["contains"] is True
|
||||
assert out["leave"] == [None, False] # 다른 줄 올림 = 닫힘
|
||||
assert out["click"] == [["x", "y"], "x"] # 누름 = 열고 첫 줄 초점
|
||||
assert out["left"] == [None, True] # ← = 닫고 머리 줄로
|
||||
assert out["right"] == [["x", "y"], "x"] # → = 열기
|
||||
assert out["pick"] == [True, 0, ["y"]] # 옆 판 고름 = 메뉴 닫고 동작
|
||||
# 오른쪽 끝 — 메뉴 = 400 - 4 - 160 · 옆 판 = 메뉴 왼쪽(236 - 150)
|
||||
assert out["edge"] == [236, 86]
|
||||
# 아래 끝(높이 100) — 옆 판(68) 바닥 = 100 - 4
|
||||
assert out["bottom"] == 100 - 4 - 68
|
||||
assert out["esc"] == [True, None]
|
||||
assert out["insideDown"] is False and out["outsideDown"] is True
|
||||
|
||||
|
||||
def test_옛_아코디언_없음_공용_꼴():
|
||||
src = _src()
|
||||
assert "ui-ctx-menu__sub" not in src and "▾" not in src and "is-sub" not in src
|
||||
css = (ROOT / "ui_template" / "ui_template_context_menu.css").read_text("utf-8")
|
||||
assert ".ui-ctx-menu__sub" not in css and ".ui-ctx-menu__item.is-open" in css
|
||||
# B09 연결 = children 꼴 그대로(공용 메뉴가 옆 판으로)
|
||||
edit = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Tabs_Bill_Edit.ts").read_text("utf-8")
|
||||
assert 'parts[1].push({ label: "연결", children: picks }, ...more);' in edit
|
||||
@@ -47,22 +47,13 @@
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* 하위 메뉴(아코디언) — 머리글 굵게 · 종류는 들여쓰기 */
|
||||
/* 하위 메뉴(246-2) — 머리 줄 굵게 · 연 동안 머리 줄 칠함 · 옆 판 = 같은 메뉴 꼴(자리 = fixed · 스크립트) */
|
||||
.ui-ctx-menu__item.is-group {
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
.ui-ctx-menu__sub {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.ui-ctx-menu__sub[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ui-ctx-menu__item.is-sub {
|
||||
padding-left: var(--spacing-24);
|
||||
.ui-ctx-menu__item.is-open {
|
||||
background: var(--color-surface-sunken, var(--color-mist-violet));
|
||||
}
|
||||
|
||||
/* 구분선(`"-"` 항목) */
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
|
||||
import "./ui_template_context_menu.css";
|
||||
|
||||
/** 평면 항목 = [문구, 동작]. 하위 메뉴 항목 = {label, children} — 누르면 바로 아래로
|
||||
* 펼쳐지는 아코디언(구조물군 → 종류, 2026-08-18 B05 우클릭 메뉴 일원화).
|
||||
/** 평면 항목 = [문구, 동작]. 하위 메뉴 항목 = {label, children} — 「글 ▸」 한 줄 · 올리거나 누르면
|
||||
* 오른쪽 옆 새 판(하위 메뉴 · 화면 오른쪽 끝이면 왼쪽) · → 열기 · ← 닫기(PLAN 246-2).
|
||||
* 구분선 = `"-"` — 맨 앞 · 맨 뒤 · 겹친 것은 안 그림. */
|
||||
export type MapContextMenuItem =
|
||||
| [label: string, action: () => void]
|
||||
@@ -104,7 +104,7 @@ export function createMapContextMenu(blockClass: string): MapContextMenu {
|
||||
const entry = document.createElement("button");
|
||||
entry.type = "button";
|
||||
entry.className = `${blockClass}__context-menu-item ui-ctx-menu__item`;
|
||||
if (sub) entry.classList.add("is-sub");
|
||||
if (!sub) entry.addEventListener("pointerenter", closeSide); // 다른 줄로 옮기면 옆 판 닫힘
|
||||
entry.textContent = label;
|
||||
entry.addEventListener("click", () => {
|
||||
close();
|
||||
@@ -113,7 +113,72 @@ export function createMapContextMenu(blockClass: string): MapContextMenu {
|
||||
return entry;
|
||||
}
|
||||
|
||||
/** 화면 밖으로 나가면 안쪽으로 당김(오른쪽 · 아래 끝) — 열 때 · 하위 메뉴를 펼칠 때 */
|
||||
/** 옆 판(하위 메뉴) 닫음 — 머리 줄 표시도 풂 */
|
||||
function closeSide(): void {
|
||||
element.querySelector(".ui-ctx-menu__side")?.remove();
|
||||
element
|
||||
.querySelectorAll<HTMLElement>(".ui-ctx-menu__item.is-open")
|
||||
.forEach((head) => head.classList.remove("is-open"));
|
||||
}
|
||||
|
||||
/** 옆 판 자리 — 머리 줄 오른쪽(넘치면 메뉴 왼쪽) · 아래 끝은 올림 · fixed(메뉴 overflow 에 안 잘림) ·
|
||||
* 변환된 그릇 안이면 실제 자리와 어긋난 만큼 바로잡음 */
|
||||
function placeSide(side: HTMLElement, head: HTMLElement): void {
|
||||
const view = element.ownerDocument.defaultView ?? window;
|
||||
const menu = element.getBoundingClientRect();
|
||||
let x = menu.right;
|
||||
let y = head.getBoundingClientRect().top;
|
||||
side.style.left = `${x}px`;
|
||||
side.style.top = `${y}px`;
|
||||
const box = side.getBoundingClientRect();
|
||||
const [ox, oy] = [box.left - x, box.top - y];
|
||||
y -= (side.firstElementChild?.getBoundingClientRect().top ?? box.top) - box.top; // 첫 줄 = 머리 줄 높이
|
||||
if (x + box.width > view.innerWidth - 4) x = Math.max(4, menu.left - box.width);
|
||||
if (y + box.height > view.innerHeight - 4) y = Math.max(4, view.innerHeight - 4 - box.height);
|
||||
side.style.left = `${x - ox}px`;
|
||||
side.style.top = `${y - oy}px`;
|
||||
}
|
||||
|
||||
/** 하위 메뉴 머리 줄 — 올림 · 누름 · → = 옆 판 · 옆 판 ← = 닫고 머리로 */
|
||||
function buildGroup(label: string, children: Array<[string, () => void]>): HTMLButtonElement {
|
||||
const head = document.createElement("button");
|
||||
head.type = "button";
|
||||
head.className = `${blockClass}__context-menu-item ui-ctx-menu__item is-group`;
|
||||
head.setAttribute("aria-haspopup", "menu");
|
||||
head.textContent = `${label} ▸`;
|
||||
const show = (focus: boolean): void => {
|
||||
if (!head.classList.contains("is-open")) {
|
||||
closeSide();
|
||||
const side = document.createElement("div");
|
||||
side.className = `${blockClass}__context-menu-sub ui-ctx-menu ui-ctx-menu__side`;
|
||||
side.style.position = "fixed";
|
||||
side.setAttribute("role", "menu");
|
||||
children.forEach(([text, action]) => side.append(buildLeaf(text, action, true)));
|
||||
side.addEventListener("keydown", (event) => {
|
||||
if (event.key !== "ArrowLeft") return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
closeSide();
|
||||
head.focus();
|
||||
});
|
||||
element.append(side);
|
||||
head.classList.add("is-open");
|
||||
placeSide(side, head);
|
||||
}
|
||||
if (focus) element.querySelector<HTMLElement>(".ui-ctx-menu__side button")?.focus();
|
||||
};
|
||||
head.addEventListener("pointerenter", () => show(false));
|
||||
head.addEventListener("click", () => show(true));
|
||||
head.addEventListener("keydown", (event) => {
|
||||
if (event.key !== "ArrowRight") return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
show(true);
|
||||
});
|
||||
return head;
|
||||
}
|
||||
|
||||
/** 화면 밖으로 나가면 안쪽으로 당김(오른쪽 · 아래 끝) — 열 때 */
|
||||
function fit(): void {
|
||||
const x = parseFloat(element.style.left);
|
||||
const y = parseFloat(element.style.top);
|
||||
@@ -144,29 +209,8 @@ export function createMapContextMenu(blockClass: string): MapContextMenu {
|
||||
element.append(buildLeaf(item[0], item[1]));
|
||||
return;
|
||||
}
|
||||
// 하위 메뉴(아코디언) — 머리글을 누르면 종류 목록이 바로 아래로 펼쳐진다.
|
||||
// 한 번에 한 군만 연다(다른 군을 열면 앞의 군은 접힘).
|
||||
const head = document.createElement("button");
|
||||
head.type = "button";
|
||||
head.className = `${blockClass}__context-menu-item ui-ctx-menu__item is-group`;
|
||||
head.textContent = `${item.label} ▸`;
|
||||
const body = document.createElement("div");
|
||||
body.className = `${blockClass}__context-menu-sub ui-ctx-menu__sub`;
|
||||
body.hidden = true;
|
||||
item.children.forEach(([label, action]) => body.append(buildLeaf(label, action, true)));
|
||||
head.addEventListener("click", () => {
|
||||
const willOpen = body.hidden;
|
||||
element
|
||||
.querySelectorAll<HTMLElement>(`.${blockClass}__context-menu-sub`)
|
||||
.forEach((other) => (other.hidden = true));
|
||||
element
|
||||
.querySelectorAll<HTMLElement>(`.${blockClass}__context-menu-item.is-group`)
|
||||
.forEach((other) => (other.textContent = other.textContent!.replace("▾", "▸")));
|
||||
body.hidden = !willOpen;
|
||||
head.textContent = `${item.label} ${willOpen ? "▾" : "▸"}`;
|
||||
fit(); // 펼친 하위가 화면 아래로 넘치면 메뉴를 올림
|
||||
});
|
||||
element.append(head, body);
|
||||
// 하위 메뉴 = 머리 줄 하나 · 옆 판(한 번에 하나)
|
||||
element.append(buildGroup(item.label, item.children));
|
||||
});
|
||||
if (element.lastElementChild?.classList.contains("ui-ctx-menu__sep"))
|
||||
element.lastElementChild.remove();
|
||||
|
||||
@@ -158,7 +158,7 @@
|
||||
box-shadow: inset 0 -2px 0 var(--color-accent);
|
||||
}
|
||||
|
||||
/* 고칠 칸(PLAN 121 · 241-1) — 칸 가득 글 상자 · 숫자 오른쪽 맞춤 · 입력 칸 표시 = 채움 색 없이 칸 안쪽 테두리(칸 크기 불변) */
|
||||
/* 고칠 칸(PLAN 121) — 칸 가득 글 상자 · 숫자 오른쪽 맞춤 */
|
||||
.ui-data-table__table td.is-edit {
|
||||
padding: 0;
|
||||
}
|
||||
@@ -168,20 +168,13 @@
|
||||
height: var(--dt-row-h);
|
||||
padding: 0 var(--spacing-8);
|
||||
border: 0;
|
||||
background: transparent;
|
||||
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--color-accent) 60%, var(--color-canvas));
|
||||
background: color-mix(in srgb, var(--color-accent) 5%, var(--color-canvas));
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
font-variant-numeric: tabular-nums;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* 고칠 수 있는 칸(PLAN 243-1 · editOnDouble) — 글일 때도 같은 입력 칸 테두리(outline 안쪽 · 줄 고름 box-shadow 와 안 겹침) */
|
||||
.ui-data-table__table td.is-can-edit {
|
||||
outline: 1px solid color-mix(in srgb, var(--color-accent) 60%, var(--color-canvas));
|
||||
outline-offset: -1px;
|
||||
}
|
||||
|
||||
.ui-data-table__input:focus {
|
||||
outline: 2px solid var(--color-focus-ring);
|
||||
outline-offset: -2px;
|
||||
|
||||
@@ -17,8 +17,7 @@
|
||||
* 나무(PLAN 235-1 · tree) = 접히지 않는 나무 안내선(├ is-tee · └ is-end · │ is-bar) + 폴더 · 파일 아이콘(공용 나무 꼴)
|
||||
* 더블클릭 입력(PLAN 235-1 · editOnDouble) = 고칠 칸도 글로 그림 · 더블클릭 · t.edit(줄) = 그 칸만 글 상자 ·
|
||||
* Enter(조합 끝난 뒤) · 초점 떠남 = 입력 끝(onEdit) — 한 번 누름 = 줄 고름 · 끌기
|
||||
* 고칠 수 있는 칸 = 글일 때도 칸에 입력 칸 테두리 class `is-can-edit`(PLAN 243-1) ·
|
||||
* 칸 누른 뒤 Enter · F2 = 그 칸 입력 시작(keyEdit false = 표 밖이 맡음)
|
||||
* 칸 누른 뒤 Enter · F2 = 그 칸 입력 시작(PLAN 243-1 · keyEdit false = 표 밖이 맡음)
|
||||
* 같은 값 세로 병합(PLAN 201-1) = mergeSame [열] → 위아래로 같은 값이 이어지면 한 칸(rowSpan) · 본문만
|
||||
* 앞 병합 열이 갈리면 뒤 병합 열도 갈림 · 빈 값 · 고칠 칸은 안 합침 · 칸 → 열 = `cellColumn`(병합 표는 data-c)
|
||||
* ========================================================================== */
|
||||
@@ -184,7 +183,7 @@ function rowHtml(
|
||||
more: RowMore = {},
|
||||
): string {
|
||||
let tr = "";
|
||||
const { lv, spans, can } = more;
|
||||
const { lv, spans } = more;
|
||||
for (let j = 0; j < cols.length; j++) {
|
||||
const key = cols[j].key;
|
||||
const span = spans?.[j] ?? 1;
|
||||
@@ -210,7 +209,6 @@ function rowHtml(
|
||||
frz && left[j + 1] == null && "is-edge",
|
||||
empty && "is-empty",
|
||||
cellClass?.(key),
|
||||
can?.(key) && "is-can-edit",
|
||||
lvc.trim(),
|
||||
span > 1 && "is-merged",
|
||||
]
|
||||
@@ -243,8 +241,6 @@ function rowHtml(
|
||||
interface RowMore {
|
||||
lv?: { key: string; n: number; node: boolean; tree?: string };
|
||||
spans?: number[];
|
||||
/** 고칠 수 있는 칸인지(editOnDouble · 글로 그린 칸에 `is-can-edit` 표시) */
|
||||
can?: (key: string) => boolean;
|
||||
}
|
||||
|
||||
/** 칸 → 열 번호 — 병합 표는 칸이 빠져 cellIndex 가 밀리므로 data-c */
|
||||
@@ -360,7 +356,6 @@ export function renderDataTableHtml(o: DataTableOptions, win?: RowWindow): strin
|
||||
{
|
||||
...(o.level ? { lv: { key: o.level.key, n: lvN, node, tree: tree?.[i] } } : {}),
|
||||
...(spans ? { spans: spans[i - from] } : {}),
|
||||
...(o.editOnDouble && o.editable ? { can: (key: string) => o.editable!(i, key) } : {}),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user