Files
Aislo/resources/tester/test_805_1_rate_views.py
T
eomsangdonandClaude Sonnet 5.5 7c2efde22f feat(805-1): M01 요율 「시트 | 리스트」 보기 단추 · 우클릭 서로 이동 · 시트 ↔ RT 전수 대조
- 요율 화면 오른쪽 위 보기 단추 둘(기본 시트 · 세션 기억) — 한 번에 하나만 보임
- 우클릭: 리스트 줄 「시트에서 보기」 · 시트 칸 「리스트에서 보기」 · 대응 없으면 흐림 + 대응 요율 없음(메뉴 부품에 disabled 항목 · 시트 부품에 cellMenu 옵션)
- 시트 칸 → RT 줄 찾기 순수 함수(UI_RateSheet_Map.ts) · 병합 칸 보정 · 겹치면 좁은 범위
- 책 대응 범위를 상자 표제 칸까지 넓힘(RT000004 값 칸 BR51 누락 바로잡음) — 값 · RT 정본 그대로
- 대조 스크립트 audit_요율대조.py(RT 잎 216 · 시트 칸 없음 8 · 시트에만 있는 칸 1 · B09 사용 표시)
- 시험: test_805_1_rate_views 신설 · test_798_1 대응표 전수 추가 · 옛 「줄 누르면 이동」은 우클릭 메뉴로 대체

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-08 16:15:08 +09:00

188 lines
7.4 KiB
Python

"""805-1 M01 요율 「시트 | 리스트」 보기 · 우클릭 이동 · 시트 ↔ RT 전수 대조.
서버 · 화면 없이 도는 지킴 시험.
① 화면 소스: 기본 보기 = 시트 · 보기 단추 둘 · 세션 기억 · 우클릭 양방향 · 흐림 줄
② 시트 칸 → RT 줄 찾기(`M01_MasterData_UI_RateSheet_Map.ts`)를 node 로 직접 돌려 대응표 전수 확인
③ 대조 스크립트(`audit_요율대조.py`): 값 다른 RT 잎 0 · 칸 없는 잎 · 시트에만 있는 칸 = 알려진 목록
"""
from __future__ import annotations
import json
import shutil
import subprocess
import sys
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / "resources/master_data/scripts"))
import audit_요율대조 as audit # noqa: E402
PAGE = ROOT / "M01_MasterData/M01_MasterData_UI_RateSheet.ts"
MAP_TS = ROOT / "M01_MasterData/M01_MasterData_UI_RateSheet_Map.ts"
def _src(path: Path) -> str:
return path.read_text(encoding="utf-8")
def test_default_view_is_sheet_and_remembered():
src = _src(PAGE)
assert 'getItem(VIEW_KEY) === "list" ? "list" : "sheet"' in src # 기본 = 시트
assert "setItem(VIEW_KEY, next)" in src # 고른 보기 세션에 기억
assert 'value: "sheet", label: L("M01_RateViewSheet")' in src
assert 'value: "list", label: L("M01_RateViewList")' in src
# 한 번에 하나만 보임
assert 'sheetHost.hidden = next !== "sheet"' in src
assert 'rowsHost.hidden = next !== "list"' in src
def test_context_menus_both_ways_with_dimmed_fallback():
src = _src(PAGE)
assert 'L("M01_RateGoList")' in src and 'L("M01_RateGoSheet")' in src
assert src.count("{ disabled: true }") == 2 # 시트 칸 · 리스트 줄 둘 다 대응 없으면 흐림
assert "M01_RateNoRate" in src and "M01_RateNoCellShort" in src
# 옛 「줄 누르면 이동」은 오른쪽 클릭 메뉴로 대체
assert 'rowsHost.addEventListener("click"' not in src
assert 'rowsHost.addEventListener("contextmenu"' in src
def _node_keys(tmp: Path, cells: list[list], merges: list[str], spots: dict) -> list:
node = shutil.which("node")
if not node:
pytest.skip("node 없음")
script = tmp / "run.mjs"
script.write_text(
f"""
import {{ readFileSync }} from "node:fs";
import {{ keyAt }} from {json.dumps(MAP_TS.as_uri())};
const cin = JSON.parse(readFileSync(process.argv[2], "utf-8"));
const parse = (a1) => {{
const [p, q = p] = a1.split(":");
const one = (s) => {{
const m = /^([A-Z]+)(\d+)$/.exec(s);
if (!m) return null;
let c = 0;
for (const ch of m[1]) c = c * 26 + ch.charCodeAt(0) - 64;
return [Number(m[2]) - 1, c - 1];
}};
const a = one(p);
const b = one(q);
if (!a || !b) return null;
const [r0, r1] = [Math.min(a[0], b[0]), Math.max(a[0], b[0])];
const [c0, c1] = [Math.min(a[1], b[1]), Math.max(a[1], b[1])];
return {{ r0, c0, r1, c1 }};
}};
const out = cin.cells.map(([sheet, r, c]) => keyAt(cin.spots, parse, cin.merges, sheet, r, c));
console.log(JSON.stringify(out));
""",
encoding="utf-8",
)
data = tmp / "in.json"
data.write_text(
json.dumps({"cells": cells, "merges": merges, "spots": spots}), encoding="utf-8"
)
run = subprocess.run( # noqa: S603 — 고정 node 실행
[node, "--experimental-strip-types", "--no-warnings", str(script), str(data)],
capture_output=True,
text=True,
cwd=ROOT,
check=False,
)
assert run.returncode == 0, run.stderr[-800:]
return json.loads(run.stdout)
def _cells_of(a1_range: str) -> list[tuple[int, int]]:
a, _, b = a1_range.partition(":")
(ca, ra), (cb, rb) = audit.split(a), audit.split(b or a)
return [
(r - 1, c - 1)
for r in range(ra, rb + 1)
for c in range(audit.col_index(ca), audit.col_index(cb) + 1)
]
def test_every_mapped_cell_finds_its_rt_row(tmp_path):
book, sheet, rates = audit.load()
spots = book["대응"]
# 대응표 전수 — 부가세(RT000019)만 시트 밖
assert set(spots) == {r["키"] for r in rates} - {"RT000019"}
cells, expect = [], []
for key, spot in spots.items():
for r, c in _cells_of(spot["범위"]):
cells.append([spot["시트"], r, c])
expect.append(key)
got = _node_keys(tmp_path, cells, sheet["병합"], spots)
# 겹침: RT000006 범위 안의 기초액 칸(FA27:FA33)은 더 좁은 RT000007 이 가져감
for (_s, r, c), want, have in zip(cells, expect, got):
if want == "RT000006" and have == "RT000007":
assert 26 <= r <= 33 and c == audit.col_index("FA") - 1
else:
assert have == want, (audit.col_name(c + 1), r + 1, want, have)
def test_merged_inside_and_outside_cells(tmp_path):
book, sheet, _ = audit.load()
probe = {
"AI16": "RT000008", # 간접노무비 값 칸 병합(AI15:AK16) 안쪽
"AT68": "RT000002", # 고용보험 1등급 병합(AH67:AT68) 오른쪽 끝
"AT82": "RT000002", # 고용보험 7등급 병합(AH79:AT82) 아래 끝
"BR20": "RT000017", # 일반관리비 전문 병합 안쪽
"B2": None, # 머리글
"M15": None, # 규모 · 기간 글 칸
"BZ47": "RT000001", # 상자 표제 칸([산재보험료])
"BD47": "RT000005", # 상자 표제 칸([노인장기요양보험료])
"DK100": None, # 산업안전보건 주석 글(어느 상자도 아님)
"BR51": "RT000004", # 연금 2026 율
}
cells = []
for a1 in probe:
col, row = audit.split(a1)
cells.append([MAIN, row - 1, audit.col_index(col) - 1])
got = _node_keys(tmp_path, cells, sheet["병합"], book["대응"])
for (a1, want), have in zip(probe.items(), got):
assert have == want, (a1, want, have)
MAIN = audit.MAIN
def test_audit_known_gaps_only():
result = audit.audit()
leaves = result["leaves"]
assert [e for e in leaves if e["판정"] == "값 다름"] == [] # 시트와 RT 값이 다른 잎 없음
no_cell = [e for e in leaves if e["판정"] == "시트 칸 없음"]
assert {e["RT"] for e in no_cell} == {"RT000002", "RT000004", "RT000019"}
assert [e["잎"] for e in no_cell if e["RT"] == "RT000002"] == ["7등급 미만"]
assert len([e for e in no_cell if e["RT"] == "RT000004"]) == 6 # 2027 ~ 2032 (법령 부칙)
# 시트에만 있는 값 칸 — 산업안전보건관리비 「도급자관급 포함」 식의 ×1.2
assert [o["시트칸"] for o in result["orphans"]] == ["EB13"]
# B09 가 읽는 길 표시 — 자동 / 후보 글만 / 안 씀
by_key = {r["RT"]: r["B09"] for r in result["rows"]}
assert by_key["RT000008"] == "자동 셈에 씀"
assert by_key["RT000016"] == "후보 글만(값 직접 입력)"
assert by_key["RT000007"] == "안 씀"
def test_audit_b09_flags_match_rate_table():
"""대조표의 「자동 셈에 씀」 줄 = B09 `rate_table()` 이 실제로 읽는 RT 줄."""
sys.path.insert(0, str(ROOT))
from B09_Estimation import B09_Estimation_Rates as rates
used = {v["rt"] for v in rates.rate_table().values()}
assert used == set(audit.B09_AUTO)
def test_xlsx_writes_four_sheets(tmp_path):
import openpyxl
out = tmp_path / "list.xlsx"
audit.write_xlsx(out, audit.audit())
wb = openpyxl.load_workbook(out)
assert len(wb.sheetnames) == 4
gap = wb["RT에 있고 시트 칸 없음 · 다름"]
assert gap.max_row == 1 + 8 # 머리 + 시트 칸 없음 8 (파생은 칸 없음 아님)