Files
Aislo/resources/tester/test_m02_hopyo_screen.py
T
eomsangdonandClaude Opus 5.5 6466904997 feat(M02 일위대가): 361-2 전국 자재 줄 — 상세 자재목록 아래 보기 전용 표 · 확인 창 세 칸 = 만들기에 적힐 값
- 새 파일 `M02_MasterTemplete_Labor_National`: 사전조합 `본뜬.전국자재` 를 자재목록 아래 표로(코드 「전국」 · 이름 · 규격 · 수량 · 단위 · 세 칸 글 · 「지역은 프로젝트에서」 · 금액 칸 비움 · 단추 없음) · 없으면 안 그림 · 셈 · 문서 꼴 · 저장 길 그대로
- 상세(`Labor_Detail`) 자재 칸에 부르는 줄 하나 · `Labor_Api` 본뜬 타입에 `전국자재` 칸
- 확인 창 자재 표 세 칸: 서버가 대상마다 `세칸`(`Material.marks` · 만들기와 같은 값)을 실음 → 전국-관급 줄 = 관급(옛 기본값 「사급」)
- 시험: 전국 자재 줄 있는 것 · 없는 것 · 단추 없음 · 글 명사형 · 읽기 길 · 세 칸

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XWh5wNNycMkbwMpACz1rte
2026-10-16 23:42:18 +09:00

527 lines
27 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_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<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 "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