- 새 파일 `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
527 lines
27 KiB
Python
527 lines
27 KiB
Python
"""일위대가 실무자료 화면(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
|