Files
Aislo/resources/tester/test_m02_hopyo_screen.py
T

415 lines
20 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""일위대가 실무자료 화면(PLAN 324-4 · 338-3) — 좌측 판 · 본 화면 · 확인 창 · 고르기 창 ·
별칭 장부 · [로직 추가] 「실무 호표」 줄 · 엑셀 보기 창.
- 확인 창 도우미(Node · fetch · sessionStorage 대역 · `helper_m02_hopyo_check.cjs`):
체크 → 몸(체크한 줄만 · 뺌 · 고른 대상 · 몫 · 배 · 호표) · 빠질 줄 까닭 · 캐시 키 ·
최신 응답만 · 품 비교 · 만들기 · 통째 · 만들 층 · 미리 셈 · 장부 길 · 장부 화면 합침 ·
하위 호표 먼저 · 확인된 것 = 바로 참조
- 고르기 창 덧줄(`helper_m02_hopyo_pick.cjs`): 이 층 · 위 층 호표 줄 · 종류 고르기 ·
프로젝트 층엔 없음 · 글 도우미 · 근거 꼬리표 · 엑셀 보기 창 행 → 호표
- 소스: 옛 즉시 확정 · 바로 만들기 없음(saveHopyoMemory · [확정] · [같음 모두 확정] ·
[근거 맞음 모두 확정] · [사전조합 만들기]) · `buildHopyo(` 는 확인 창 · 엑셀 보기 창 펼침에서만 ·
좌측 판 자리 · 700 줄 · 색 = 공용 토큰
"""
import json
import os
import re
import subprocess
from pathlib import Path
HERE = os.path.dirname(os.path.abspath(__file__))
M02 = Path(HERE).parents[1] / "M02_MasterTemplete"
NEW = (
"Hopyo_Api",
"Hopyo_Pick",
"Hopyo_Side",
"Hopyo_View",
"Hopyo_Sheet",
"Hopyo_Check",
"Hopyo_Choose",
"Hopyo_Ledger",
)
ROW = {
"층": "system",
"주인": None,
"키": "HP-000001",
"파일": "북부청.xls",
"번": 3,
"이름": "임도표지석",
"규격": "910×600×150",
"단위": "개소",
"맞춤": [2, 4],
}
SURE = {"로직": "GC000198", "이름": "장비사용타설", "입력값": {"소량감률": 50}, "배": 1, "몫": None}
QTY = {
"엑셀": {"인력": {"콘크리트공": 0.1091}, "장비": [{"갈래": "굴착기", "시간": 0.29}]},
"우리": {"인력": {"콘크리트공": 0.1091}, "장비": [{"갈래": "굴착기", "시간": 0.31}]},
"같음": {"콘크리트공": True, "굴착기": False},
}
QTY_MANY = { # 같은 장비 여러 줄(229-4 ②) — 합쳐 한 줄로 견줌
"엑셀": {"장비": [{"갈래": "굴착기", "시간": t} for t in (0.0334, 0.0371, 0.0889)]},
"우리": {"장비": [{"갈래": "굴착기", "시간": 0.0889}]},
"같음": {"굴착기": False},
}
def _text(name: str) -> str:
return (M02 / name).read_text(encoding="utf-8")
def _node(helper: str, payload: dict) -> dict:
proc = subprocess.run(
["node", os.path.join(HERE, helper)],
input=json.dumps(payload, ensure_ascii=False),
capture_output=True,
text=True,
encoding="utf-8",
check=False,
)
assert proc.returncode == 0, proc.stderr
return json.loads(proc.stdout)
def _line(no, state, check, parts, **extra):
match = {"열쇠": f"줄{no}||m3", "상태": state, "체크": check, "대상": parts, **extra}
return {"차례": no, "이름": f"줄{no}", "규격": "", "수량": 1, "단위": "㎥", "갈래": "단가산출",
"맞춤": match} # fmt: skip
def _check() -> dict:
lines = [
_line(0, "후보", False, [{"자재": "MT004258", "입력값": {}, "배": 1}]),
_line(1, "맞음", True, [SURE], 품=QTY),
_line(2, "별칭", True, [{"로직": "GF000101", "입력값": {}, "배": 1}]),
_line(3, "다른 점", False, [{"로직": "GF000103", "입력값": {}, "배": 1}], 품=QTY_MANY),
_line(4, "후보", False, []),
_line(5, "%", False, []),
_line(6, "호표", True, [{"호표": 46, "입력값": {}, "배": 2.3}]),
_line(7, "노임", False, [{"로직": "GF000300", "입력값": {}, "배": None}]),
]
ledger = [
{"열쇠": "가|b|m3", "원래": {"품명": "가", "규격": "b", "단위": "m3"}, "갈래": "짝",
"짝": [{"로직": "GC000198", "입력값": {}, "배": 1, "몫": None}], "층": "personal",
"주인": "7/42", "고침": True},
{"열쇠": "나||m3", "원래": {"품명": "나", "규격": "", "단위": "m3"}, "갈래": "짝",
"짝": [{"로직": "GF000101", "입력값": {}, "배": 1, "몫": None}], "층": "personal",
"주인": "7/42", "고침": True},
{"열쇠": "다||개", "원래": {"품명": "다", "규격": "", "단위": "개"}, "갈래": "짝",
"짝": [{"자재": "MT1", "입력값": {}, "배": 1, "몫": None}], "층": "system",
"주인": None, "고침": False},
] # fmt: skip
rows = [
{**ROW, "확인": {"층": "company", "주인": "7", "키": "SC-000009"}},
{**ROW, "번": 4, "이름": "배수관", "확인": None},
]
return _node("helper_m02_hopyo_check.cjs", {"lines": lines, "ledger": ledger, "rows": rows})
def test_확인_창_체크한_줄만_몸():
got = _check()["check"]
assert got["key"] == "m02-hopyo-check:personal::HP-000001:3"
assert got["suggested"] == [ # 서버 체크 제안 그대로 · 배 없는 줄 · % 줄 · 후보는 안 감
{"차례": 1, "대상": [{"로직": "GC000198", "입력값": {"소량감률": 50}, "배": 1}]},
{"차례": 2, "대상": [{"로직": "GF000101", "입력값": {}, "배": 1}]},
{"차례": 6, "대상": [{"호표": 46, "배": 2.3}]},
]
assert got["edited"] == [ # 캐시 — 맞음 줄 체크 끔 · 뺌 · 고른 대상(몫 · 배)
{"차례": 2, "대상": [{"로직": "GF000101", "입력값": {}, "배": 1}]},
{"차례": 3, "뺌": "우리에 없음"},
{
"차례": 4,
"대상": [{"로직": "GF000211", "입력값": {"유형": "보통"}, "배": 0.1, "몫": 0.1}],
},
{"차례": 6, "대상": [{"호표": 46, "배": 2.3}]},
]
assert got["leftOut"] == {"확인 안 함": 3, "뺌": 1, "% 줄": 1}
assert got["checkable"] == [True, True, True, True, False, False, True, False]
assert got["sure"] == [1, 2] # [맞음 · 별칭 모두 체크]
assert got["compare"] == "콘크리트공 0.1091 → 0.1091 ✓ · 굴착기 0.29 → 0.31 ≠"
assert got["compareMany"] == "굴착기 0.1594 → 0.0889 ≠" # 같은 장비 여러 줄 = 합쳐 한 줄
assert got["toTarget"] == [{"호표": 46, "배": 2.3}, {"자재": "MT1", "배": 1}]
assert got["newest"] == [False, True, True] # 같은 줄 늦은 응답 버림 · 다른 줄은 따로
assert got["dropped"] is False # 만들기 성공 = 캐시 지움
def test_고르기_창_도우미():
got = _check()["choose"]
assert got["hint"] == ["6-1-1", "고임돌"] # 인용 조항 번호 → 없으면 품명 첫 뜻 낱말
assert got["picked"] == [
{"로직": "GC000206", "입력값": {}, "배": 1, "몫": None},
{"로직": "GF000211", "입력값": {}, "배": 0.1, "몫": 0.1},
]
assert got["inputs"] == "소량감률=50(블록), 지역=서울 · ⚠ 못 가름: 측압"
assert got["word"] == ["만듦", "있던 것을 확인한 내용으로 덮음"]
assert got["qty"] == "콘크리트공 0.1091 · 굴착기 0.29hr"
assert got["target"] == "제46호표 [㎡]"
def test_별칭_장부_화면_합침():
got = _check()["ledger"]
assert got["key"] == "m02-hopyo-ledger:company:7"
assert got["merged"] == [
["나||m3", "personal", True, True, "안 씀"], # 고친 줄
["다||개", "personal", True, True, "짝"], # 위 층 → 내 층으로 고쳐 씀
["다||개", "system", False, False, "짝"], # 위 층 = 그대로 읽기만
] # 지운 줄(가) 없음
assert got["filter"] == ["다||개", "다||개"] and got["kind"] == ["나||m3"]
assert got["pair"] == "GC000198 × 1 · 몫 0.1 · 소량감률=50"
assert got["entry"]["짝"] == [] and "층" not in got["entry"]
def test_길_만들기_통째_만들_층_미리_셈_장부():
calls = _check()["calls"]
build = "/api/m02/layers/company/hopyo-build?owner=7"
head = {"층": "system", "주인": None, "키": "HP-000001", "번": 3}
assert calls[0]["url"] == build and calls[0]["body"]["줄"][0]["차례"] == 1
assert {k: calls[0]["body"][k] for k in head} == head and "새로" not in calls[0]["body"]
assert calls[1]["body"] == {**head, "통째": {"대상": [{"로직": "GF000225", "배": 1}]}}
assert calls[2] == {
"url": "/api/m02/layers/system/hopyo/HP-000001/3?to=company&to_owner=7",
"method": "GET",
"body": None,
}
assert calls[3]["url"] == "/api/m02/layers/personal/hopyo/HP-000001/3/guess"
assert calls[3]["body"]["대상"][0]["몫"] == 0.1
assert calls[4]["url"] == "/api/m02/layers/company/hopyo-ledger?owner=7"
assert calls[5]["method"] == "PUT" and calls[5]["body"] == {"줄": {"가|b|m3": None}}
def test_로직_추가_확인된_것은_바로_참조_아니면_확인_창():
got = _check()["pick"]
assert got["picked"] == [
{
"ref": {"층": "company", "주인": "7", "키": "SC-000009"},
"name": "임도표지석",
"alias": "임도표지석",
}
]
assert got["calls"] == [] # 확인 없이 만들지 않음
assert got["opened"] == ["확인 — 북부청.xls 제4호표 배수관"]
def test_엑셀_보기_창_하위_호표_먼저():
assert _check()["sheet"]["order"] == [46, 45, 3, 7, 8] # 순환은 남은 차례
def _pick() -> dict:
row = {**ROW, "확인": {"층": "company", "주인": "7", "키": "SC-000009"}}
return _node("helper_m02_hopyo_pick.cjs", {"rows": [row], "built": {}})
def test_고르기_창_실무_호표_줄():
got = _pick()
assert got["calls"][0] == {
"url": "/api/m02/layers/company/hopyo-picks?owner=7",
"method": "GET",
"body": None,
}
assert got["kinds"] == ["logic", "precombo", "hopyo"]
assert got["rows"] == [
{
"kind": "hopyo",
"key": "system||HP-000001|3",
"tag": "실무 호표",
"name": "임도표지석 910×600×150",
"sub": "북부청.xls · 제3호표 · 마스터",
"unit": "개소 · 확인 SC-000009",
},
{
"kind": "hopyo",
"key": "system||HP-000001|엑셀",
"tag": "실무 호표",
"name": "엑셀로 보기 · 여러 호표",
"sub": "북부청.xls · 마스터",
"unit": "호표 1",
},
]
assert got["picked"][0]["ref"]["키"] == "SC-000009"
assert got["project"] is True # 프로젝트 층 = 덧줄 없음
def test_글_도우미와_근거_꼬리표():
got = _pick()
view = got["view"]
assert view["target"] == ["구조물터파기 [㎥]", "레미콘 25-21-120 [㎥]", "제46호표 [㎡]"]
assert view["inputs"] == "횟수=4회사용시, 지역=서울 · ⚠ 못 가름: 보정작업"
basis = got["basis"]
assert basis["ok"] == [True, False, False, False, False] # 조항 같음 · 직종 · 단위 모두
assert basis["plain"] == [True, True] # 줄 직종이 보통인부뿐 · 없음 = 직종 안 봄
assert basis["tag"] == [
"조항 건설 공통 3-2-1 · 직종 특별인부 · 단위 같음",
"조항 건설 공통 3-2-1 · 단위 같음",
"",
]
assert basis["source"] == [
"출처 건설표준품셈 3-2-1 · 직종 특별인부 · 단가산출 93–101행",
"단가산출 7행",
"",
]
def test_엑셀_보기_창_행과_호표():
got = _pick()
assert got["calls"][1] == {
"url": "/api/m02/layers/company/hopyo-grid/HP-000001?owner=7",
"method": "GET",
"body": None,
}
sheet = got["sheet"]
assert sheet["at"] == [None, 3, 3, 3, 4, 4, None] # 22행 → 제3호표 · 범위 밖 = 없음
assert sheet["span"] == "B20:O24" # 제3호표 영역 = B열 ~ 끝 열(열 폭 정보 O)
assert sheet["last"] == ["AA", "A"]
assert sheet["rows"] == ["4345:4345", "93:101"] # 단가산출 행 전체
# ── 소스 ──────────────────────────────────────────────────────────────
def test_옛_즉시_확정과_바로_만들기_없음():
texts = {name: _text(f"M02_MasterTemplete_{name}.ts") for name in NEW}
every = "\n".join(texts.values())
for gone in ("saveHopyoMemory", "hopyo-memory", "HopyoMemory", "memoryOf", "basisPick"):
assert gone not in every, gone
for label in (
"같음 모두 확정",
"근거 맞음 모두 확정",
'"확정"',
'"사전조합 만들기"',
"불러오기",
):
assert label not in every, label
users = sorted(name for name, text in texts.items() if "buildHopyo(" in text)
assert users == ["Hopyo_Check", "Hopyo_Sheet"] # 확인 창 · 펼침 뒤 한 번
check = texts["Hopyo_Check"]
assert '"확인하고 사전조합 만들기"' in check and "맞음 · 별칭 모두 체크" in check
assert "showLoadingOverlay()" in check and "dropDraft(k)" in check
assert "m02-hopyo-check:" in check and "POLL_MS" in check
sheet = texts["Hopyo_Sheet"]
assert "확인하고 만들기" in sheet and "이대로 ${ready.length}개 만들기" in sheet
assert "확인 창 열기" in sheet and "buildOrder(" in sheet
pick = texts["Hopyo_Pick"]
assert "if (row.확인)" in pick and 'import("./M02_MasterTemplete_Hopyo_Check")' in pick
view = texts["Hopyo_View"]
assert "buildHopyoCheck(" in view and "choose(" not in view
ledger = texts["Hopyo_Ledger"]
assert "m02-hopyo-ledger:" in ledger and "saveLedger(" in ledger
for gone in ("발주처", "회사로 올리기", "쓰인 횟수"):
assert gone not in ledger.split("*/", 1)[1] # 머리 글 「안 둠」 밖
def test_좌측_판_자리와_권한():
side = _text("M02_MasterTemplete_UI_Side.ts")
assert 'import { buildHopyoSection } from "./M02_MasterTemplete_Hopyo_Side";' in side
host = side.split('className: "m02-side__lists ui-sidebar-panel"')[1]
assert host.index("precomboSec,") < host.index("section(hopyo.title, hopyo.body),")
assert host.index("section(hopyo.title, hopyo.body),") < host.index("extras ?")
assert "canWrite: canEdit(layer, opt.isAdmin)," in side # 구조물 조합 층 쓰기 권한
assert "hopyo.paint();" in side.split("function paint(): void {")[1]
hopyo_side = _text("M02_MasterTemplete_Hopyo_Side.ts")
assert 'accept: ".xls,.xlsx"' in hopyo_side and "input.hidden = true;" in hopyo_side
assert "disabled: !opt.canWrite," in hopyo_side and "pageScope()" in hopyo_side
assert "buildHopyoLedger({ scope, canWrite: opt.canWrite })" in hopyo_side # [별칭 장부]
assert hopyo_side.index("list, ledger]") > 0
def test_로직_추가_한_곳과_파일_규칙():
detail = _text("M02_MasterTemplete_Labor_Detail.ts")
assert len(detail.splitlines()) == 693 # 줄 수 그대로
assert "withHopyo(library)," in detail
pick = _text("M02_MasterTemplete_Labor_Pick.ts")
assert "more?: () => Promise<PickExtra[]>;" in pick and "...later" in pick
assert "if (pre?.query) finder.search.value = pre.query;" in pick # 찾기 글 미리 넣음
for name in NEW:
text = _text(f"M02_MasterTemplete_{name}.ts")
assert len(text.splitlines()) <= 700, name
css = _text("M02_MasterTemplete_Hopyo.css")
assert "var(--color-" in css and not re.search(r"#[0-9a-fA-F]{3,6}\b", css)
for state in ("맞음", "별칭", "다른점", "후보"):
assert f".m02-hopyo__state.is-{state}" in css
assert '"./M02_MasterTemplete_Hopyo.css"' in _text("M02_MasterTemplete_Hopyo_Check.ts")
def test_엑셀_보기_창_여는_자리와_늦게_불러옴():
lazy = 'import("./M02_MasterTemplete_Hopyo_Sheet")'
side = _text("M02_MasterTemplete_Hopyo_Side.ts")
assert lazy in side.split("await show(got);")[1].split("})")[0] # 올린 직후 자동
view = _text("M02_MasterTemplete_Hopyo_View.ts")
assert lazy in view and '"엑셀로 보기"' in view
check = _text("M02_MasterTemplete_Hopyo_Check.ts")
assert lazy in check and '"엑셀에서 보기"' in check and "호표: opt.no" in check
assert "단산행: 행" in check
pick = _text("M02_MasterTemplete_Hopyo_Pick.ts")
assert lazy in pick
static = r'^import (?!type )[^(;]*"\./M02_MasterTemplete_Hopyo_Sheet"'
for text in (side, view, pick, check):
assert not re.search(static, text, re.M) # 정적 import 없음(타입만)
sheet = _text("M02_MasterTemplete_Hopyo_Sheet.ts")
assert 'import("../A00_Common/spreadsheet/spreadsheet")' in sheet
assert not re.search(
r'^import \{[^}]*\} from "\.\./A00_Common/spreadsheet/spreadsheet"', sheet, re.M
)
assert "readOnly: true" in sheet and "wide: true" in sheet
def test_실물_응답_대역_확인_창():
"""338-4 — 가 창 실물 응답(`fixtures/m02_hopyo_view_sample.json`)을 확인 창 · 고르기 창 도우미에
그대로 먹임 — 체크 = 서버 제안 · 몸 꼴 · 품 비교 · 옵션 출처 · 후보 판정 꼬리표."""
sample = json.loads((Path(HERE) / "fixtures" / "m02_hopyo_view_sample.json").read_bytes())
lines = sample["호표"]["호표"]["줄"]
got = _node("helper_m02_hopyo_check.cjs", {"real": lines})
by = {x["이름"]: x for x in got["lines"]}
remicon = by["레미콘타설(장비사용)"]
assert remicon["상태"] == "맞음" and remicon["checked"] and remicon["checkable"]
assert (
remicon["compare"]
== "콘크리트공 0.1091 → 0.1091 ✓ · 보통인부 0.0364 → 0.0364 ✓ · 굴착기 0.29 → 0.2909 ✓"
)
assert (
remicon["inputs"]
== "타설방법=장비사용타설(규격), 구조물=철근구조물(규격), 소량감률=50(블록)"
)
assert remicon["qty"] == "콘크리트공 0.1091 · 보통인부 0.0364 · 굴착기 0.29hr"
assert remicon["cands"][0][1] == "맞음" and remicon["cands"][0][2].startswith("조항 건설품셈")
rebar = by["철근가공조립(현장)"]
assert rebar["상태"] == "다른 점" and not rebar["checked"] and rebar["checkable"]
assert [x["차례"] for x in got["body"]] == [x["차례"] for x in lines if x["맞춤"]["체크"]]
assert all({"로직", "입력값", "배"} <= set(t) for x in got["body"] for t in x["대상"])
assert got["sure"] == [x["차례"] for x in lines if x["맞춤"]["상태"] == "맞음"]
def test_확인_창_손질_339():
"""339 덧고침 — ① 줄 표 꼴(한글 낱말 줄바꿈 · 옵션 · 품 비교 한 줄씩 · 단추 세로) · 장부 품명 · 규격 폭
② 만들기 뒤 좌측 사전조합 목록 다시 ③ 엑셀 보기 창 목록 셈 끝까지 다시 읽음 · 펼침 표 숫자 판정 칸."""
css = _text("M02_MasterTemplete_Hopyo.css")
assert "word-break: keep-all;" in css and "white-space: pre-line;" in css
assert (
".m02-hopyo__lines .m02-hopyo__acts > *" in css
and "text-overflow"
not in css.split(".m02-hopyo__lines td:is(:nth-child(9)")[-1].split("}")[0]
)
assert ".m02-hopyo-ledger__table td:nth-child(1)" in css
assert "m02-hopyo-ledger__table" in _text("M02_MasterTemplete_Hopyo_Ledger.ts")
check = _text("M02_MasterTemplete_Hopyo_Check.ts")
assert r'td(opts.join("\n"))' in check and r'.split(" · ")' in check
assert r'.join("\n"),' in check # 품 비교 = 항목마다 한 줄(판정 글 함수는 그대로)
make = check.split("async function make(")[1].split("function builtBlock(")[0]
assert make.index("document.dispatchEvent(new Event(HOPYO_BUILT));") < make.index("opt.onBuilt")
side = _text("M02_MasterTemplete_UI_Side.ts")
assert "document.addEventListener(HOPYO_BUILT, onBuilt);" in side
assert "void precombo.refresh()" in side.split("const onBuilt")[1].split(";")[0]
sheet = _text("M02_MasterTemplete_Hopyo_Sheet.ts")
assert "poll(file);" in sheet and "poll(next);" in sheet and "POLL_MS" in sheet
assert "if (ok) document.dispatchEvent(new Event(HOPYO_BUILT));" in sheet
assert '`맞음 ${tally("맞음")} · 다른 점 ${tally("다른 점")}`' in sheet
def test_본_칸_호표_목록_셈_끝까지_다시_읽음_339():
"""339 덧고침 2 — 본 칸 호표 표 「제안 n / 줄 m」 · 머리 「셈 중」 — 셈 중이면 3 초마다 다시 읽음."""
view = _text("M02_MasterTemplete_Hopyo_View.ts")
assert "poll(file);" in view and "poll(next);" in view and "POLL_MS" in view
assert "hopyos = next.호표;" in view and "...hopyos" in view
assert (
"const live = (): boolean => my === nav && view.isConnected;" in view
) # 호표 하나로 가면 멈춤
assert "숫자 판정 셈 중" in view