"""일위대가 실무자료 화면(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_Sheet", "Hopyo_Check", "Hopyo_CheckRows", "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}, {"전국": True, "배": 1}, # 361-1 — 키 없음 · 이름은 서버가 ] assert got["newest"] == [False, True, True] # 같은 줄 늦은 응답 버림 · 다른 줄은 따로 assert got["dropped"] is False # 만들기 성공 = 캐시 지움 def test_고르기_창_도우미(): got = _check()["choose"] assert got["hint"] == ["레미콘타설", "고임돌"] # 품명 첫 뜻 낱말(358-1 · 조항 번호 아님) assert got["groups"]["groups"] == [ ["1위", False, ["A"]], ["출처 같음", False, ["C"]], ["이름 닮은 로직", False, ["E"]], ["숫자만 닮음", True, ["D", "B"]], # 뜻 낱말 안 겹치는 「후보」 = 끝 ] assert got["groups"]["text"][:2] == ["되메우기 · A · 산림", "되메우기 · C · 건설"] assert got["groups"]["material"] == ["1위", "그 밖 후보"] nat = got["groups"]["national"] # 361-1 assert nat["groups"] == [["1위", False, ["전국"]], ["지역 변형", True, ["M1", "M2"]]] assert nat["text"] == "레미콘(전국)-관급 25-21-120 [㎥] · 키 없음" assert nat["region"] == [{"자재": "M2", "입력값": {}, "배": 1, "몫": None}] assert nat["back"] == [{"전국": True, "입력값": {}, "배": 1, "몫": None}] assert nat["hit"] == [ # 줄 글 지역 = 1위 · 전국 = 되돌릴 자리 · 남은 변형 접힘 ["1위", ["M2"]], ["전국", ["전국"]], ["지역 변형", ["M1"]], ] assert got["groups"]["pick"] == [True, False, True] # 재료비 있는 단가산출 줄도 자재 검색 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_엑셀_보기_창_하위_호표_먼저(): got = _check()["sheet"] assert got["order"] == [46, 45, 3, 7, 8] # 순환은 남은 차례 # 다 연결만 녹색 · 0/0 색 없음(359-2) · 358-3 연결할 · 연결된 두 칸이 있으면 그것 assert got["linked"] == [True, False, False, False, True, False, False] 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 "확인하고" not in check # 359-4 명사형 assert "showLoadingOverlay()" in check and "dropDraft(k)" in check assert "m02-hopyo-check:" in check and "POLL_MS" in check sheet = texts["Hopyo_Sheet"] # 359-2 — 펼침 표 · [확인 창 열기] · [이대로 n개 만들기] 없음 · [저장] = 물음 한 번 뒤 체크 줄로 만들기 for gone in ("확인 창 열기", "이대로", "review(", "확인하고 만들기"): assert gone not in sheet, gone save = sheet.split("async function save(")[1].split("async function makeAll(")[0] assert 'showConfirmDialog(ask, "저장")' in save and "hasDraft(" in save assert "건너뜀 — 체크 줄 없음" in save and "await makeAll(ready, skipped);" in save assert "buildOrder(" in sheet and "error instanceof StaleError" in sheet # 하위 먼저 · 409 안내 edit = sheet.split("const editButton =")[1].split("function paintList(")[0] assert '"수정"' in edit and "e.stopPropagation();" in edit and "openHopyoCheck({" in edit row = sheet.split("function paintList(")[1] assert row.index("m02-hopyo-sheet__no") < row.index("editButton(h)") < row.index("__name") assert 'linked.classList.toggle("is-linked", allLinked(h));' in row pick = texts["Hopyo_Pick"] assert "if (row.확인)" in pick and 'import("./M02_MasterTemplete_Hopyo_Check")' in pick assert not (M02 / "M02_MasterTemplete_Hopyo_View.ts").exists() # 본 칸 표 없음(359-1) 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") # 359-1 — [파일 업로드] = 구조물 조합 층 단추 꼴(보통 크기 · m02-layers__btn) · 파일 줄 = 바로 엑셀 보기 up = hopyo_side.split("const upload = createButton({")[1].split("upload.title")[0] assert 'label: "파일 업로드"' in up and 'upload.classList.add("m02-layers__btn");' in up assert "size:" not in up and "엑셀 올리기" not in hopyo_side assert "buildHopyoView" not in hopyo_side assert 'row.addEventListener("click", () => void showSheet(f));' in hopyo_side assert "onClose:" in hopyo_side and "onDeleted: () => void refresh()," in hopyo_side 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()) == 695 # 줄 수 그대로(361-2 전국 자재 부르는 줄 둘) assert "withHopyo(library)," in detail pick = _text("M02_MasterTemplete_Labor_Pick.ts") assert "more?: () => Promise;" 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 "await showSheet(got);" in side # 올린 직후 자동 assert lazy in side.split("async function showSheet(")[1].split("async function showLedger(")[0] check = _text("M02_MasterTemplete_Hopyo_Check.ts") assert lazy in check and 'label: "엑셀 보기"' in check and "호표: opt.no" in check assert "엑셀에서 보기" not 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, 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 assert "nth-child(9)" not in css # 359-3 — 칸 차례 대신 칸 이름 assert ".m02-hopyo-ledger__table td:nth-child(1)" in css assert "m02-hopyo-ledger__table" in _text("M02_MasterTemplete_Hopyo_Ledger.ts") rows = _text("M02_MasterTemplete_Hopyo_CheckRows.ts") assert r'text: compare.split(" · ").join("\n")' in rows # 품 비교 = 항목마다 한 줄 assert "m02-hopyo__opts" in rows # 옵션 = 항목마다 한 줄 check = _text("M02_MasterTemplete_Hopyo_Check.ts") 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 def test_엑셀_보기_머리_막대_셈_끝까지_다시_읽음_359(): """339 덧고침 2 → 359-1 — 옛 본 칸 표 몫(시트 · 호표 수 · 검산 · 「제안 n / 줄 m」 · 「셈 중」 · [삭제])은 엑셀 보기 창 머리 · 셈 중이면 3 초마다 다시 읽음(머리 · 목록 함께).""" sheet = _text("M02_MasterTemplete_Hopyo_Sheet.ts") head = sheet.split("function paintHead(")[1].split("const dropButton")[0] for word in ("시트 ", "호표 ", "검산 어긋남", "검산 맞음", "제안 ", "숫자 판정 셈 중"): assert word in head, word assert "opt.canWrite && opt.onDeleted ? [dropButton()]" in head drop = sheet.split("const dropButton =")[1].split("/** 목록 강조")[0] assert 'label: "삭제"' in drop and 'showConfirmDialog(ask, "삭제")' in drop assert drop.index("deleteHopyo(") < drop.index("modal.close();") < drop.index("opt.onDeleted") poll = sheet.split("function poll(")[1].split("async function load(")[0] assert "paintHead(next);" in poll and "paintList();" in poll and "poll(next);" in poll def test_확인_창_표_일위대가_상세_꼴_359(): """359-3 — 로직 표 · 자재 표(일위대가 상세 꼴) + 품 비교 · 단추 · 드롭다운 글(저장 값 그대로) · 금액 = 줄마다 서버 calc 미리 셈(`logic/` 문서 꼴 · Build 와 같은 수량 규칙).""" got = _check()["draft"] assert got["logic"] == { "종류": "일위대가", "열": "", "단위": None, "담은로직": [ # 같은 로직 둘째 · 호표 · 배 없음 = 뺌 {"로직": "GC000198", "별칭": None, "메모": None, "입력값": {"소량감률": 50}, "수량": {"손": 1}}, ], "자재": [], } # fmt: skip assert got["mat"]["자재"] == [ # 같은 자재 = 2 × 1 + 2 × 0.5 {"코드": "MT1", "이름": None, "규격": None, "단위": None, "수량": {"손": 3}, "별도계상": "별도계상"}, ] # fmt: skip assert got["direct"]["별도계상"] == "직접계산" and got["direct"]["수량"] == {"손": 0} assert got["none"] == [None, None, None] # 전국 자재(361-1) = 키 없어 미리 셈 없음 assert got["table"] == ["mat", "logic", "logic", "mat", "logic", "mat"] rows = _text("M02_MasterTemplete_Hopyo_CheckRows.ts") assert '["", "추가"],' in rows and '["우리에 없음", "로직없음"],' in rows assert '["안 씀", "사용안함"],' in rows and "뺌 — " not in rows for word in ('"고르기"', '"원문 보기"', '"엑셀 보기"', '"옵션"', '"품 비교"'): assert word in rows, word assert 'import { moneyCell, PRICE_SLOTS } from "./M02_MasterTemplete_Labor_Rows";' in rows assert "...markCells(p?.세칸 ?? { 별도계상:" in rows # 361-2 서버 세 칸 먼저 assert "...markHeads()," in rows for head in ("Col_Order", "Sub", "Col_Name", "Col_Alias", "Labor_QtyCol", "Col_Unit"): assert f'tc("{head}")' in rows, head check = _text("M02_MasterTemplete_Hopyo_Check.ts") assert "calcLabor(target.layer, DRAFT_COLUMN, doc, {}, target.owner)" in check assert "checkTables(h.줄, ctx())" in check and "draftDoc(line, partsOf(line, d))" in check assert "function lineRow(" not in check and "function partCell(" not in check draft = _text("logic/M02_MasterTemplete_Hopyo_Draft.ts") assert "M02_Hopyo_Build" in draft # 정본 짝 표시 code = draft.split("*/", 1)[1] # 머리 글 밖 for gone in ("document", "fetch(", "sessionStorage", "@ui/"): assert gone not in code, gone def test_명사형_단추와_별칭_장부_풀이_359(): """359-4 — 별칭 장부 뜻 한 줄(좌측 단추 title · 화면 머리) · M02 실무자료 단추 = 명사형 · 갈래 글 = 확인 창 드롭다운과 같음(저장 값 그대로).""" ledger = _text("M02_MasterTemplete_Hopyo_Ledger.ts") hint = "엑셀 품명 ↔ 우리 로직 · 자재 짝을 기억해 두었다가 같은 품명이 나오면 먼저 제안" assert hint in ledger and "note(`${LEDGER_HINT}" in ledger side = _text("M02_MasterTemplete_Hopyo_Side.ts") assert "title: LEDGER_HINT" in side for label in ('"변경 취소"', '"로직 추가"', '"내 층 복사"', '"삭제"'): assert f"label: {label}" in ledger, label assert '"우리에 없음": "로직없음"' in ledger and '"안 씀": "사용안함"' in ledger mine = ("Hopyo_Side", "Hopyo_Sheet", "Hopyo_Check", "Hopyo_CheckRows", "Hopyo_Ledger") every = "\n".join(_text(f"M02_MasterTemplete_{n}.ts") for n in mine) for gone in ( "고친 것 버림", "로직 더함", "내 층으로 고쳐 씀", "확인하고 사전조합 만들기", "엑셀 올리기", "엑셀에서 보기", "모두 체크 (", 'label: "지우기"', ): assert gone not in every, gone