Merge remote-tracking branch 'origin/dev' into sub_laptop_4
This commit is contained in:
@@ -1,10 +1,14 @@
|
||||
"""197-1 대시보드 「구조물 옵션」 카드 → M02 「구조물 옵션 정의」 화면이 바로 열림 시험 — 조작은 사용자.
|
||||
"""197-1 · 716-1 대시보드 「구조물 옵션 정의」 줄 → M01 「구조물 옵션 정의」 화면이 바로 열림 시험 — 조작은 ORCA.
|
||||
|
||||
716-1 — 화면 입구가 M02 에서 M01 로 옮겨감(화면 코드 · 서버 길은 M02 폴더 그대로) · 옛 M02 `open=옵션` 주소는
|
||||
시스템 관리자면 M01 `?open=옵션` 으로 돌려보냄.
|
||||
|
||||
원인 — 진입 해시 `open=옵션` 이 옵션 줄을 눌러 정의 화면을 띄우는 사이 첫 화면(집계표)의 `mount` 가
|
||||
모듈 · 번호를 기다리다 뒤늦게 끝나 정의 화면 위에 집계표를 덮음(`mount` 에 낡음 검사 없었음).
|
||||
|
||||
· 카드 셋 → 해시(open=구조물조합 · 옵션 · 도면) · 해시 → 진입(`entryFromHash`) Node 로 바로 돌림
|
||||
· `reveal` 가 옵션만 줄을 누르고 도면은 펼치기만 · 구조물조합은 아무것도 안 함
|
||||
· 줄 → 해시(M01 open=로직 · 옵션 / M02 open=일위대가조합 · 구조물조합 · 도면) · 해시 → 진입(`entryFromHash`) Node
|
||||
· M02 `reveal` 가 옵션만 줄을 누르고 도면은 펼치기만 · 구조물조합은 아무것도 안 함 · M01 `reveal` 도 같은 꼴
|
||||
· 옛 `open=옵션` 돌려보냄(`redirectOldOptions`)
|
||||
· `mount` 집계표 갈래(진짜 소스 조각)를 Node 로 돌림 — 기다리는 사이 다른 화면이 열리면 그리지 않음
|
||||
"""
|
||||
|
||||
@@ -41,16 +45,20 @@ def _run(script: str, *args: str) -> object:
|
||||
|
||||
|
||||
@needs_node
|
||||
def test_카드_셋_해시():
|
||||
def test_줄_해시_옵션은_M01():
|
||||
from test_b01_settings_cards import _cards
|
||||
|
||||
cards = _cards({"role": "SYSTEM_ADMIN", "is_master": False}, 3)
|
||||
assert [c[2].split("?")[1].split("&")[0] for c in cards[1:4]] == [
|
||||
"open=구조물조합",
|
||||
"open=옵션",
|
||||
"open=도면",
|
||||
heads = [c[2].split("?")[0] + "?" + c[2].split("?")[1].split("&")[0] for c in cards[1:7]]
|
||||
assert heads == [
|
||||
"m01-master-data?open=로직",
|
||||
"m01-master-data?open=옵션",
|
||||
"m01-master-data?open=별칭", # 718-3
|
||||
"m02-master-template?open=일위대가조합",
|
||||
"m02-master-template?open=구조물조합",
|
||||
"m02-master-template?open=도면",
|
||||
]
|
||||
assert all("&root=master" in c[2] for c in cards[1:4])
|
||||
assert all("&root=master" in c[2] for c in cards[4:7])
|
||||
|
||||
|
||||
@needs_node
|
||||
@@ -69,7 +77,7 @@ def test_해시_진입_셋():
|
||||
+ fn.group(0).replace("export function", "function")
|
||||
+ "\nconsole.log(JSON.stringify(entryFromHash(null)));"
|
||||
)
|
||||
for want in ("구조물조합", "옵션", "도면"):
|
||||
for want in ("구조물조합", "옵션", "도면", "일위대가조합"):
|
||||
got = _run(script, f"open={want}&root=master&company=3")
|
||||
assert got["open"] == want and got["root"] == "master" and got["company"] == "3"
|
||||
|
||||
@@ -139,3 +147,38 @@ def test_집계표_갈래_기다리는_사이_다른_화면이_열리면_그리
|
||||
assert got["none"] == ["create", "panel:set"] # 안 끼어들면 그대로 그림
|
||||
assert got["mod"] == ["panel:null"] # 모듈 기다리는 사이 다른 화면 — 그리지 않음
|
||||
assert got["num"] == ["panel.destroy", "panel:null"] # 번호 기다리는 사이 — 패널도 버림
|
||||
|
||||
|
||||
@needs_node
|
||||
def test_옛_옵션_주소_M01로_돌려보냄():
|
||||
layer = _text(M02 / "M02_MasterTemplete_Layer.ts")
|
||||
fn = re.search(r"^export function redirectOldOptions\([\s\S]*?^}", layer, re.M)
|
||||
assert fn, "redirectOldOptions 를 못 찾음"
|
||||
script = (
|
||||
"const currentQuery = () => new URLSearchParams(process.argv[1]);\n"
|
||||
"const ROUTES = { M01_MASTER_DATA: 'm01' }; const sent: string[] = [];\n"
|
||||
"const navigateTo = (r: string, q: any) => sent.push(r + JSON.stringify(q));\n"
|
||||
"type SessionUser = any;\n"
|
||||
+ fn.group(0).replace("export function", "function")
|
||||
+ "\nconst a = redirectOldOptions({ role: 'SYSTEM_ADMIN' });"
|
||||
+ "\nconst b = redirectOldOptions({ role: 'USER' });"
|
||||
+ "\nconst c = redirectOldOptions(null);"
|
||||
+ "\nconsole.log(JSON.stringify([a, b, c, sent]));"
|
||||
)
|
||||
assert _run(script, "open=옵션&root=master") == [True, False, False, ['m01{"open":"옵션"}']]
|
||||
# 옵션이 아니면 안 돌려보냄
|
||||
assert _run(script, "open=도면&root=master") == [False, False, False, []]
|
||||
page = _text(M02 / "M02_MasterTemplete_UI_Page.ts")
|
||||
assert "if (redirectOldOptions(user))" in page
|
||||
assert 'mountSettingsDrawer(user ?? {}, "m02", entry.company);' in page
|
||||
|
||||
|
||||
def test_M01_open_인자_로직_옵션():
|
||||
m01 = ROOT / "M01_MasterData"
|
||||
page = _text(m01 / "M01_MasterData_UI_Page.ts")
|
||||
assert 'if (want === "로직" || want === "옵션" || want === "별칭") side.reveal(want);' in page
|
||||
assert "setLeaveGuard(leaveOptions)" in page # 저장 안 한 정의 고침 — 떠날 때 물음
|
||||
side = _text(m01 / "M01_MasterData_UI_Side.ts")
|
||||
assert "buildOptionsSection({ ...optionsOpt, isAdmin: true })" in side
|
||||
# 로직 칸 아래 구분선 부품 + 옵션 칸
|
||||
assert "logic, createDivider(), optionsSec, aliasSec" in side # 718-3 별칭 장부 칸
|
||||
|
||||
@@ -263,7 +263,7 @@ def test_카드_시스템_관리자만_글_다_있음():
|
||||
links = nav.split("export function mountSettingsDrawer")[0]
|
||||
admin, rest = links.split('if (user.role === "SYSTEM_ADMIN") {')[1].split("// 회사 수정 =")
|
||||
assert 'key: "calcSites"' in admin and "calcSites" not in rest
|
||||
assert 'settingLinks(user, companyId).filter((link) => link.key !== "calcSites")' in nav
|
||||
assert "calcSites" not in nav.split("export function pageLinks")[1] # ☰ 는 페이지 둘만(716-2)
|
||||
page = _text("B01_Dashboard/B01_Dashboard_UI_View_SysDefaults.ts")
|
||||
assert 'link.key === "calcSites" ? void openCalcSitesDialog()' in page
|
||||
locale = _text("ui_template/ui_template_locale_m1.ts")
|
||||
|
||||
@@ -94,8 +94,10 @@ def test_실무_xlsx_넷_붙여넣기_만듦_합계_같음(client, tag):
|
||||
assert got["같음"] == FILES[tag][1] and all(abs(x["차이"]) <= 1 for x in got["다름"])
|
||||
assert len(got["만듦"]) == got["호표"] and got["고정"] + got["호표참조"] == got["줄"]
|
||||
assert master_hash() == before
|
||||
rows = client.get("/api/m02/layers/personal/hopyo").json()["줄"]
|
||||
assert rows[0]["파일"].startswith("붙여넣기") and rows[0]["시트"] == "붙여넣기"
|
||||
# 718-1 — 붙여넣기 실무자료 = 지문 없음 → 불러오기 목록엔 숨김 · 파일은 남음
|
||||
assert client.get("/api/m02/layers/personal/hopyo").json()["줄"] == []
|
||||
doc = client.get(f"/api/m02/layers/personal/hopyo/{got['키']}").json()
|
||||
assert doc["파일"].startswith("붙여넣기") and doc["시트"] == "붙여넣기"
|
||||
|
||||
|
||||
def _block(rows, no):
|
||||
|
||||
@@ -211,6 +211,9 @@ def test_못_잇는_짝(geo):
|
||||
)
|
||||
def test_기억_층(tmp_path, monkeypatch, role, layer):
|
||||
monkeypatch.setattr(route.access, "folder", lambda got, create=False: tmp_path / got.layer)
|
||||
monkeypatch.setattr(route.m02_store, "FOLDER", tmp_path / "system")
|
||||
master = {"a||": {"확정": "earth:9"}, "m||": {"확정": "earth:1"}} # 시스템 승인(718-2)
|
||||
hopyo_store.remember(tmp_path / "system", master, lk.MEMORY)
|
||||
session = {"company_id": 7, "user_id": 3, "role": role}
|
||||
assert route.keep(session, {"a||": {"확정": "earth:4", "이름": "절토(토사)"}}) == layer
|
||||
assert route.keep(session, {}) is None
|
||||
@@ -220,7 +223,8 @@ def test_기억_층(tmp_path, monkeypatch, role, layer):
|
||||
got = route.recalled(session)
|
||||
assert got["a||"]["확정"] == ("earth:4" if layer == "personal" else "earth:5")
|
||||
assert got["b||"]["확정"] == "x"
|
||||
assert route.recalled({"user_id": 3, "role": role}) == {} # 회사 없음 = 층 없음
|
||||
assert got["m||"]["확정"] == "earth:1" # 회사 · 개인 줄이 마스터보다 앞섬(a = 아래 층)
|
||||
assert route.recalled({"user_id": 3, "role": role}) == master # 회사 없음 = 마스터만
|
||||
|
||||
|
||||
def _same(a, b):
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
"""709 쪽 단추 부품 지킴 시험 — 쪽 수 · 화면 높이 → 줄 수 · 쪽 크기 바뀔 때 첫 줄 유지(순수 함수).
|
||||
|
||||
`ui_template/ui_template_pager.ts` 의 「순수 셈」 구간만 떼어 node 로 돌림(화면 · 서버 없음).
|
||||
부품 모음 견본 · 옛 이름 `buildPager` 가 새 부품을 부르는지도 글로 잡음.
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
PAGER = ROOT / "ui_template" / "ui_template_pager.ts"
|
||||
|
||||
pytestmark = pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음")
|
||||
|
||||
|
||||
def _pure() -> str:
|
||||
text = PAGER.read_text(encoding="utf-8")
|
||||
start = text.index("export const ROW_H")
|
||||
end = text.index("/* ── 아래부터 화면 재기")
|
||||
return text[start:end]
|
||||
|
||||
|
||||
def _run(body: str) -> object:
|
||||
script = _pure() + "\n" + body
|
||||
out = subprocess.run(
|
||||
["node", "--experimental-strip-types", "--no-warnings",
|
||||
"--input-type=module-typescript", "-e", script],
|
||||
capture_output=True, text=True, encoding="utf-8", timeout=60,
|
||||
) # fmt: skip
|
||||
assert out.returncode == 0, out.stderr
|
||||
return json.loads(out.stdout)
|
||||
|
||||
|
||||
def test_쪽_수():
|
||||
got = _run(
|
||||
"console.log(JSON.stringify([pageCount(0,30), pageCount(30,30), pageCount(31,30), pageCount(1469,30)]));"
|
||||
)
|
||||
assert got == [1, 1, 2, 49]
|
||||
|
||||
|
||||
def test_화면_높이_줄_수():
|
||||
# 머리 40 + 줄 40 · 표 칸 위 200 · 쪽 단추 자리 64 → 1080: (816-40)/40=19 · 768: (504-40)/40=11
|
||||
got = _run(
|
||||
"console.log(JSON.stringify(["
|
||||
"fitRows(1080-200-PAGER_ROOM), fitRows(768-200-PAGER_ROOM), "
|
||||
"fitRows(100), fitRows(100000), fitRows(Number.NaN), fitRows(520, 36, 40)]));"
|
||||
)
|
||||
assert got == [19, 11, 5, 200, 5, 13]
|
||||
|
||||
|
||||
def test_높이가_클수록_줄이_많음():
|
||||
got = _run(
|
||||
"const a=[]; for (let h=300; h<=1400; h+=50) a.push(fitRows(h)); "
|
||||
"console.log(JSON.stringify(a.every((v,i)=>i===0||v>=a[i-1])));"
|
||||
)
|
||||
assert got is True
|
||||
|
||||
|
||||
def test_쪽_크기_바뀌어도_첫_줄_유지():
|
||||
# 30줄 쪽의 3쪽(첫 줄 61번째) → 20줄 쪽이면 4쪽(61~80) · 50줄 쪽이면 2쪽(51~100)
|
||||
got = _run(
|
||||
"console.log(JSON.stringify([pageKeepFirst(3,30,20), pageKeepFirst(3,30,50), pageKeepFirst(1,30,12)]));"
|
||||
)
|
||||
assert got == [4, 2, 1]
|
||||
|
||||
|
||||
def test_옛_이름은_새_부품():
|
||||
cells = (ROOT / "M01_MasterData" / "M01_MasterData_UI_Cells.ts").read_text(encoding="utf-8")
|
||||
assert re.search(r"export const buildPager = createPager;", cells)
|
||||
assert "createButton" not in cells
|
||||
parts = (ROOT / "ui_template" / "ui_template_ui_kit_parts.ts").read_text(encoding="utf-8")
|
||||
assert 'file: "ui_template_pager.ts"' in parts and 'file: "ui_template_divider.ts"' in parts
|
||||
@@ -0,0 +1,179 @@
|
||||
"""M02 단가산출(SK) 문서 꼴 · 줄 정체 · 고친 줄(PLAN 715-1a · `M02_Sangeun_Doc`) — 엔진 안 부름."""
|
||||
|
||||
from decimal import Decimal
|
||||
|
||||
import pytest
|
||||
|
||||
from M02_MasterTemplete import M02_Sangeun_Doc as sd
|
||||
|
||||
ROW = {
|
||||
"키": "GX000001",
|
||||
"결과단위": "원/㎡",
|
||||
"입력": [{"이름": "높이"}, {"이름": "돌", "고르기": ["깬잡석", "견치돌"]}],
|
||||
"호표": [
|
||||
{
|
||||
"종류": "인력",
|
||||
"요소": "LB000033",
|
||||
"이름": "석공",
|
||||
"수량": "높이 * 0.1",
|
||||
"비목": "노무비",
|
||||
},
|
||||
{"종류": "재료", "요소": {"검색어": "모래"}, "이름": "모래", "수량": "1", "비목": "재료비"},
|
||||
{"종류": "인력", "요소": "LB000033", "이름": "석공", "수량": "0.2", "비목": "노무비"},
|
||||
{"종류": "재료", "요소": {"검색어": "자갈"}, "이름": "자갈", "수량": "2", "비목": "재료비"},
|
||||
{
|
||||
"종류": "로직",
|
||||
"요소": "로직(GC000276, 기계='5401-0017')",
|
||||
"이름": "굴착기",
|
||||
"수량": "0.5",
|
||||
},
|
||||
],
|
||||
"덧줄": [{"이름": "공구손료", "식": "(석공 * LB000033) * 3 / 100", "비목": "경비"}],
|
||||
}
|
||||
|
||||
|
||||
def _doc(**extra):
|
||||
return {
|
||||
"키": "SK-000001",
|
||||
"이름": "돌쌓기 내 값",
|
||||
"단위": "㎡",
|
||||
"바탕": {"로직": "GX000001"},
|
||||
**extra,
|
||||
}
|
||||
|
||||
|
||||
def test_line_ids_kind_key_n():
|
||||
assert sd.line_ids(ROW) == [
|
||||
"labor:LB000033#0",
|
||||
"material:모래#0",
|
||||
"labor:LB000033#1",
|
||||
"material:자갈#0",
|
||||
"logic:GC000276#0",
|
||||
"pct:공구손료#0",
|
||||
]
|
||||
|
||||
|
||||
def test_clean_shape_and_refusals():
|
||||
got = sd.clean(_doc(입력값={"높이": 3, "모름": 1, "돌": ""}, 원값=1), ROW)
|
||||
assert got["입력값"] == {"높이": 3} and got["종류"] == "단가산출" and got["뺀줄"] == []
|
||||
for bad, why in (
|
||||
({"키": "SC-000001"}, "SK-"),
|
||||
({"이름": " "}, "이름"),
|
||||
({"단위": ""}, "단위"),
|
||||
({"바탕": {"참조": {"키": "SC-000001"}}}, "SK-"),
|
||||
({"덮개": {"labor:LB000033#0": {"qty": "식"}}}, "수가 아님"),
|
||||
({"덮개": {"labor:LB000033#0": {"qty": 1, "배율": 2}}}, "하나"),
|
||||
({"덮개": {"labor:LB#x": {"qty": 1}}}, "kind:키#n"),
|
||||
({"뺀줄": ["material:모래#0", "material:모래#0"]}, "두 번"),
|
||||
({"열린입력": ["높이"], "입력값": {"높이": 3}}, "열린입력"),
|
||||
({"더한줄": [{"종류": "인력", "요소": "LB000002"}]}, "수량"),
|
||||
({"더한줄": [{"종류": "식", "수량": 1}]}, "종류"),
|
||||
):
|
||||
with pytest.raises(sd.DocError, match=why):
|
||||
sd.clean(_doc(**bad), ROW)
|
||||
# 원값 · 바탕판 = 서버 몫 — 브라우저 값 버림
|
||||
got = sd.clean(_doc(덮개={"labor:LB000033#0": {"qty": 0.5, "원값": 9}}), ROW)
|
||||
assert got["덮개"] == {"labor:LB000033#0": {"qty": 0.5}}
|
||||
|
||||
|
||||
def test_open_inputs_only_ratio():
|
||||
with pytest.raises(sd.DocError, match="배율만"):
|
||||
sd.clean(_doc(열린입력=["돌"], 덮개={"labor:LB000033#0": {"qty": 1}}), ROW)
|
||||
got = sd.clean(_doc(열린입력=["돌"], 덮개={"labor:LB000033#0": {"배율": 1.1}}), ROW)
|
||||
assert got["덮개"]["labor:LB000033#0"] == {"배율": 1.1}
|
||||
|
||||
|
||||
def test_patched_cover_qty_price_ratio():
|
||||
doc = sd.clean(
|
||||
_doc(
|
||||
덮개={
|
||||
"labor:LB000033#1": {"qty": 0.25, "labor": 200000},
|
||||
"labor:LB000033#0": {"배율": 1.5},
|
||||
"logic:GC000276#0": {"expense": 1000},
|
||||
"pct:공구손료#0": {"qty": 5},
|
||||
}
|
||||
),
|
||||
ROW,
|
||||
)
|
||||
got = sd.patched(ROW, doc)
|
||||
rows = got["줄"]["호표"]
|
||||
assert rows[0]["수량"] == "(높이 * 0.1) * 1.5"
|
||||
assert rows[2]["수량"] == "0.25" and rows[2]["단가"] == Decimal(200000)
|
||||
assert rows[4]["값묶음"] == {"경비": Decimal(1000)}
|
||||
assert got["줄"]["덧줄"][0]["식"] == "(석공 * LB000033) * 5 / 100"
|
||||
assert got["짝잃음"] == [] and ROW["호표"][2]["수량"] == "0.2" # 바탕은 안 바뀜
|
||||
|
||||
|
||||
def test_drop_keeps_pick_on_its_line():
|
||||
"""뺀줄 뒤 고름 제자리 — 자갈(바탕 3째) 고름이 뺀줄 뒤 새 2째로 옮겨 감 · 모래 줄에 안 붙음."""
|
||||
doc = sd.clean(_doc(뺀줄=["material:모래#0"], 고름={"material:자갈#0": "MT000010"}), ROW)
|
||||
got = sd.patched(ROW, doc)
|
||||
names = [x["이름"] for x in got["줄"]["호표"]]
|
||||
assert names == ["석공", "석공", "자갈", "굴착기"]
|
||||
assert got["고름"] == {"2": "MT000010"}
|
||||
assert got["정체"][:4] == [
|
||||
"labor:LB000033#0",
|
||||
"labor:LB000033#1",
|
||||
"material:자갈#0",
|
||||
"logic:GC000276#0",
|
||||
]
|
||||
|
||||
|
||||
def test_added_lines_before_extras():
|
||||
doc = sd.clean(
|
||||
_doc(
|
||||
더한줄=[
|
||||
{"종류": "인력", "요소": "LB000002", "이름": "보통인부", "단위": "인", "수량": 0.1},
|
||||
{
|
||||
"종류": "로직",
|
||||
"로직": "GC000001",
|
||||
"입력값": {"규격": "가", "두께": 2},
|
||||
"수량": 1,
|
||||
},
|
||||
{"참조": {"층": "system", "주인": None, "키": "SK-000002"}, "수량": 2},
|
||||
]
|
||||
),
|
||||
ROW,
|
||||
)
|
||||
got = sd.patched(ROW, doc)
|
||||
added = got["줄"]["호표"][5:]
|
||||
assert added[0]["비목"] == "노무비" and added[0]["수량"] == "0.1"
|
||||
assert added[1]["요소"] == "로직(GC000001, 규격='가', 두께=2)"
|
||||
assert added[2]["참조"]["키"] == "SK-000002" and added[2]["값묶음"] is None
|
||||
assert got["정체"][5:] == ["add:#0", "add:#1", "add:#2", "pct:공구손료#0"]
|
||||
|
||||
|
||||
def test_lost_covers_kept_and_listed():
|
||||
"""짝 잃은 덮개 = 문서에 남김 + 목록(지우지 않음) · 비목 다른 단가 · 재료 아닌 고름."""
|
||||
doc = sd.clean(
|
||||
_doc(
|
||||
덮개={"labor:LB999999#0": {"qty": 1}, "labor:LB000033#0": {"material": 5}},
|
||||
고름={"labor:LB000033#1": "MT000010"},
|
||||
뺀줄=["material:없음#0"],
|
||||
),
|
||||
ROW,
|
||||
)
|
||||
got = sd.patched(ROW, doc)
|
||||
why = {x["cid"]: x["까닭"] for x in got["짝잃음"]}
|
||||
assert why["labor:LB999999#0"].startswith("덮개") and "비목 다름" in why["labor:LB000033#0"]
|
||||
assert why["labor:LB000033#1"].startswith("고름") and why["material:없음#0"].startswith("뺀줄")
|
||||
assert "labor:LB999999#0" in doc["덮개"]
|
||||
|
||||
|
||||
def test_chain_moves_base_picks():
|
||||
"""바탕 SK 사슬 — 바탕 쪽 차례 고름이 아래 SK 뺀줄 뒤에도 그 줄에."""
|
||||
first = sd.patched(ROW, sd.clean(_doc(고름={"material:자갈#0": "MT000010"}), ROW))
|
||||
child = sd.clean({**_doc(뺀줄=["labor:LB000033#0"]), "바탕": None}, first["줄"])
|
||||
got = sd.patched(first["줄"], child, first["고름"])
|
||||
assert got["고름"] == {"2": "MT000010"} and got["줄"]["호표"][2]["이름"] == "자갈"
|
||||
|
||||
|
||||
def test_empty_base():
|
||||
row = sd.empty_row({"키": "SK-000003", "이름": "자체", "단위": "식"})
|
||||
doc = sd.clean(
|
||||
{"키": "SK-000003", "이름": "자체", "단위": "식", "바탕": None,
|
||||
"더한줄": [{"종류": "재료", "요소": "MT000010", "수량": 3, "단가": 1200}]},
|
||||
row,
|
||||
) # fmt: skip
|
||||
got = sd.patched(row, doc)
|
||||
assert got["줄"]["호표"][0]["단가"] == Decimal(1200) and got["줄"]["결과단위"] == "원/식"
|
||||
@@ -0,0 +1,191 @@
|
||||
"""M02 단가산출(SK) 서고 · 셈(PLAN 715-1b · 715-2 · `M02_Sangeun_Calc` · `M02_Labor_Ref`) — 임시 storage.
|
||||
|
||||
고친 줄 값 = 손셈 · 덧줄 % 엔진이 다시 셈 · 뺀줄 뒤 고름 제자리 · 짝 잃음 남김 · 원값 서버 ·
|
||||
순환 422 · 쓰는 곳 지우기 409 · 사전조합이 SK 를 담음 · 층 복사 왕복 같은 값.
|
||||
"""
|
||||
|
||||
from decimal import Decimal
|
||||
|
||||
import pytest
|
||||
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
from M02_MasterTemplete import M02_Labor_Ref as refs
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
from M02_MasterTemplete import M02_Sangeun_Calc as calc
|
||||
from resources.tester.test_m02_labor_ref import COMPANY, MINE, world # noqa: F401 — 같은 임시 층
|
||||
|
||||
PCT = "GC001109" # 원/㎡ · 인력 둘 + 「노무비 * 3 / 100」
|
||||
TWO_MATS = "GC000565" # 원/m · 재료 줄 둘(검색어)
|
||||
|
||||
|
||||
def _sk(folder, **doc):
|
||||
with store.at(folder):
|
||||
return refs.create("sangeun", {"이름": "내 단가산출", "단위": "㎡", "더한줄": [], **doc})[
|
||||
"이름"
|
||||
]
|
||||
|
||||
|
||||
def _doc(folder, key):
|
||||
with store.at(folder):
|
||||
return store.read("sangeun", key)["문서"]
|
||||
|
||||
|
||||
def _calc(folder, key, given=None):
|
||||
with store.at(folder):
|
||||
return calc.calc_doc(_doc(folder, key), given)
|
||||
|
||||
|
||||
def _whole():
|
||||
return m01.loaded()[1]
|
||||
|
||||
|
||||
def _bare(code):
|
||||
from M02_MasterTemplete import M02_Labor_Doc as labor
|
||||
|
||||
return labor.run_line(_whole(), code, {})
|
||||
|
||||
|
||||
def test_qty_cover_hand_and_pct_recomputed(world):
|
||||
"""품 덮개 = 손셈 · 「노무비 * 3 / 100」 덧줄이 덮인 노무비에서 다시 셈 · 원값 = 서버가 그때 M01 값."""
|
||||
bare = _bare(PCT)
|
||||
first, second = bare["줄"][0], bare["줄"][1]
|
||||
key = _sk(
|
||||
world["personal"], 바탕={"로직": PCT}, 덮개={"labor:LB000036#0": {"qty": 0.5, "원값": 9}}
|
||||
)
|
||||
doc = _doc(world["personal"], key)
|
||||
assert doc["덮개"]["labor:LB000036#0"]["원값"]["qty"] == pytest.approx(float(first["수량"]))
|
||||
assert len(doc["바탕판"]) == 16
|
||||
got = _calc(world["personal"], key)
|
||||
labor_sum = Decimal("0.5") * Decimal(str(first["단가"])) + Decimal(str(second["금액"]))
|
||||
assert got["기준"]["노무비"] == pytest.approx(float(labor_sum))
|
||||
assert got["기준"]["경비"] == pytest.approx(float(labor_sum * 3 / 100), abs=1)
|
||||
assert got["줄"][0]["덮개"] == ["qty"] and got["줄"][0]["cid"] == "labor:LB000036#0"
|
||||
assert got["짝잃음"] == []
|
||||
# 덧줄 율 덮개 = 5 %
|
||||
key = _sk(
|
||||
world["personal"],
|
||||
바탕={"로직": PCT},
|
||||
덮개={"pct:" + bare["줄"][2]["이름"] + "#0": {"qty": 5}},
|
||||
)
|
||||
got = _calc(world["personal"], key)
|
||||
assert got["기준"]["경비"] == pytest.approx(float(bare["기준"]["노무비"]) * 0.05, abs=1)
|
||||
|
||||
|
||||
def test_drop_then_pick_stays_on_its_line(world):
|
||||
"""뺀줄 뒤 고름 제자리 — 둘째 재료 줄을 고른 자재가 첫 재료 줄을 뺀 뒤에도 그 줄 품목."""
|
||||
import master_pick as mp
|
||||
|
||||
whole = _whole()
|
||||
row = whole.logic(TWO_MATS)
|
||||
item = row["호표"][2]
|
||||
picks = mp.search(whole, item["요소"], {}, item.get("단위"))
|
||||
want = picks[1] if len(picks) > 1 else picks[0]
|
||||
first_cid = "material:" + row["호표"][1]["이름"] + "#0"
|
||||
second_cid = "material:" + item["이름"] + "#0"
|
||||
key = _sk(
|
||||
world["personal"],
|
||||
단위="m",
|
||||
바탕={"로직": TWO_MATS},
|
||||
뺀줄=[first_cid],
|
||||
고름={second_cid: want},
|
||||
자재표시={second_cid: {"별도계상": "직접계산"}}, # 기본 = 별도계상(222-1) 줄 빠짐
|
||||
)
|
||||
got = _calc(world["personal"], key)
|
||||
names = [x["이름"] for x in got["줄"]]
|
||||
assert row["호표"][1]["이름"] not in names
|
||||
line = next(x for x in got["줄"] if x["cid"] == second_cid)
|
||||
assert line["품목"]["키"] == want
|
||||
|
||||
|
||||
def test_lost_cover_kept_and_listed(world):
|
||||
key = _sk(world["personal"], 바탕={"로직": PCT}, 덮개={"labor:LB999999#0": {"qty": 1}})
|
||||
got = _calc(world["personal"], key)
|
||||
assert got["짝잃음"][0]["cid"] == "labor:LB999999#0"
|
||||
assert "labor:LB999999#0" in _doc(world["personal"], key)["덮개"]
|
||||
assert got["기준"]["계"] == pytest.approx(float(_bare(PCT)["기준"]["계"]))
|
||||
|
||||
|
||||
def test_chain_loop_422_and_delete_409(world):
|
||||
"""바탕 사슬 A ← B · B 를 A 의 바탕으로 = 422 · 쓰는 곳 있는 A 지우기 = 409(이름)."""
|
||||
a = _sk(world["personal"], 바탕={"로직": PCT})
|
||||
b = _sk(
|
||||
world["personal"], 바탕={"참조": {**MINE, "키": a}}, 덮개={"labor:LB000002#0": {"qty": 0}}
|
||||
)
|
||||
got = _calc(world["personal"], b)
|
||||
bare = _bare(PCT)
|
||||
assert got["기준"]["노무비"] == pytest.approx(float(bare["줄"][0]["금액"]))
|
||||
with store.at(world["personal"]):
|
||||
doc = store.read("sangeun", a)
|
||||
with pytest.raises(store.StoreError) as e:
|
||||
store.write(
|
||||
"sangeun", a, doc["판"], {**doc["문서"], "바탕": {"참조": {**MINE, "키": b}}}
|
||||
)
|
||||
assert e.value.status == 422
|
||||
with pytest.raises(store.StoreError) as e:
|
||||
store.delete("sangeun", a)
|
||||
assert e.value.status == 409 and "단가산출 개인 7/42" in str(e.value.detail)
|
||||
|
||||
|
||||
def test_precombo_holds_sangeun(world):
|
||||
"""사전조합 참조 줄이 SK 를 담음 — 1 단위 값 × 수량 · SK 지우기 막음."""
|
||||
from M02_MasterTemplete import M02_Labor_Doc as labor
|
||||
|
||||
sk = _sk(world["personal"], 바탕={"로직": PCT}, 덮개={"labor:LB000036#0": {"qty": 0.5}})
|
||||
with store.at(world["personal"]):
|
||||
sc = refs.create(
|
||||
"precombo",
|
||||
{"이름": "조합", "담은로직": [{"참조": {**MINE, "키": sk}, "수량": {"손": 2}}]},
|
||||
)["이름"]
|
||||
got = labor.calc({"담은로직": [{"참조": {**MINE, "키": sc}, "수량": {"손": 1}}]}, {})
|
||||
with pytest.raises(store.StoreError) as e:
|
||||
store.delete("sangeun", sk)
|
||||
one = _calc(world["personal"], sk)["1단위"]["계"]
|
||||
assert got["반영합"]["계"] == pytest.approx(one * 2, abs=1)
|
||||
assert e.value.status == 409 and "사전조합 개인 7/42" in str(e.value.detail)
|
||||
|
||||
|
||||
def test_layer_copy_round_trip(world):
|
||||
"""개인 B(바탕 = 개인 A) → 회사로 복사 = A 도 같이(새 키 · 본뜬) → 개인으로 다시 = 같은 값."""
|
||||
a = _sk(world["personal"], 바탕={"로직": PCT}, 덮개={"labor:LB000036#0": {"qty": 0.5}})
|
||||
b = _sk(world["personal"], 바탕={"참조": {**MINE, "키": a}}, 뺀줄=["labor:LB000002#0"])
|
||||
want = _calc(world["personal"], b)["기준"]
|
||||
src = _doc(world["personal"], b)
|
||||
with store.at(world["company"]):
|
||||
made = refs.create(
|
||||
"sangeun", refs.carry(refs.origin_mark(src, {**MINE, "키": b}), ("personal", "7/42"))
|
||||
)
|
||||
copy = _doc(world["company"], made["이름"])
|
||||
assert copy["바탕"]["참조"]["층"] == "company" and copy["본뜬"]["키"] == b
|
||||
assert _calc(world["company"], made["이름"])["기준"] == want
|
||||
with store.at(world["personal"]):
|
||||
back = refs.create(
|
||||
"sangeun",
|
||||
refs.carry(refs.origin_mark(copy, {**COMPANY, "키": made["이름"]}), ("company", "7")),
|
||||
)
|
||||
assert _doc(world["personal"], back["이름"])["바탕"]["참조"]["층"] == "company"
|
||||
assert _calc(world["personal"], back["이름"])["기준"] == want
|
||||
|
||||
|
||||
def test_refusals(world):
|
||||
with store.at(world["personal"]):
|
||||
for bad, status in (
|
||||
({"이름": "x", "더한줄": []}, 400), # 단위 빔
|
||||
({"이름": "x", "단위": "㎡", "더한줄": [], "바탕": {"로직": "GC999999"}}, 400),
|
||||
(
|
||||
{
|
||||
"이름": "x",
|
||||
"단위": "㎡",
|
||||
"더한줄": [],
|
||||
"바탕": {"참조": {**MINE, "키": "SK-000999"}},
|
||||
},
|
||||
400,
|
||||
),
|
||||
({"이름": "x", "단위": "㎡"}, 400), # 뼈대(더한줄) 없음
|
||||
):
|
||||
with pytest.raises(store.StoreError) as e:
|
||||
refs.create("sangeun", bad)
|
||||
assert e.value.status == status, bad
|
||||
key = _sk(world["personal"], 바탕={"로직": PCT})
|
||||
with store.at(world["personal"]), pytest.raises(store.StoreError) as e:
|
||||
calc.calc_doc({**_doc(world["personal"], key), "바탕": {"참조": {**MINE, "키": key}}})
|
||||
assert e.value.status == 422
|
||||
@@ -0,0 +1,67 @@
|
||||
"""일위대가 불러오기 한시(PLAN 718-1) — 올림 지문 · [끝내기] 지움 · 겹침 막음.
|
||||
|
||||
같은 엑셀 작업 중 다시 올림 = 그 파일 이어서(새 키 없음) · [끝내기] = HP · 격자 · 맞춤셈 캐시 0 ·
|
||||
만든 일위대가 · 별칭 장부 그대로 · 끝낸 같은 엑셀 다시 올림 409 · 만든 것 없이 지움 = 다시 올림 됨 ·
|
||||
옛 HP(지문 없음) = 파일 그대로 목록에서만 숨김 · 키 번 = 지운 뒤에도 다시 안 씀. 층 = 임시 폴더.
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
from M02_MasterTemplete import M02_Hopyo_Fit as fit
|
||||
from M02_MasterTemplete import M02_Hopyo_Store as hs
|
||||
from M02_MasterTemplete import M02_Template_Layers as layers
|
||||
from resources.tester.test_m02_hopyo_excel import NORTH, URL, _post, client # noqa: F401
|
||||
|
||||
ALL = "/api/m02/layers/personal/hopyo-build-all"
|
||||
|
||||
|
||||
def _keys(client):
|
||||
return [r["키"] for r in client.get(URL.format("personal")).json()["줄"]]
|
||||
|
||||
|
||||
def test_이어서_끝내기_지움_겹침_막음(client):
|
||||
folder = layers.personal_dir(7, 42)
|
||||
first = _post(client).json()
|
||||
assert first["키"] == "HP-000001" and len(first["지문"]) == 64
|
||||
again = _post(client).json()
|
||||
assert again["키"] == "HP-000001" and again["이어서"] is True # 작업 중 같은 엑셀 = 새 키 없음
|
||||
assert _keys(client) == ["HP-000001"]
|
||||
made = client.post(ALL, json={"층": "personal", "키": "HP-000001"}).json()
|
||||
assert len(made["만듦"]) == 68
|
||||
hs.remember(folder, {"장부 줄": {"갈래": "안 씀"}}) # 별칭 장부 = 끝내기 뒤 남음
|
||||
cache = fit.cache_path(folder, "HP-000001")
|
||||
cache.write_text("{}", encoding="utf-8")
|
||||
old_cache = cache.with_name("_".join(cache.name.split("_")[:2]) + "_옛판_1.json")
|
||||
old_cache.write_text("{}", encoding="utf-8")
|
||||
sc_before = sorted((folder / "precombo").glob("SC-*.json"))
|
||||
|
||||
res = client.delete(URL.format("personal") + "/HP-000001")
|
||||
assert res.status_code == 200 and res.json() == {"지움": "HP-000001", "만든": 68}
|
||||
assert not list((folder / hs.DIRNAME).glob("HP-*.json")) # 이번 HP 0
|
||||
assert not list((folder / hs.DIRNAME / hs.GRID).glob("HP-*.json"))
|
||||
assert not cache.exists() and not old_cache.exists()
|
||||
assert hs.memory(folder) == {"장부 줄": {"갈래": "안 씀"}}
|
||||
assert sorted((folder / "precombo").glob("SC-*.json")) == sc_before # 만든 일위대가 그대로
|
||||
|
||||
block = _post(client) # 끝낸 같은 엑셀 = 막음 · 안내
|
||||
assert block.status_code == 409 and "이미 불러온 엑셀" in block.json()["detail"]
|
||||
assert "일위대가 68" in block.json()["detail"] and _keys(client) == []
|
||||
assert sorted((folder / "precombo").glob("SC-*.json")) == sc_before
|
||||
|
||||
|
||||
def test_만든_것_없이_지움_다시_올림_키_번_안_되씀(client):
|
||||
assert _post(client).json()["키"] == "HP-000001"
|
||||
assert client.delete(URL.format("personal") + "/HP-000001").json()["만든"] == 0
|
||||
got = _post(client) # 만든 것 없음 = 막지 않음 · 번은 이어서
|
||||
assert got.status_code == 200 and got.json()["키"] == "HP-000002"
|
||||
|
||||
|
||||
def test_옛_HP_목록에서만_숨김(client):
|
||||
folder = layers.personal_dir(7, 42) / hs.DIRNAME
|
||||
folder.mkdir(parents=True)
|
||||
old = folder / "HP-000007.json"
|
||||
old.write_text(json.dumps({"키": "HP-000007", "파일": "옛.xls", "호표": []}), encoding="utf-8")
|
||||
assert _keys(client) == []
|
||||
assert client.get("/api/m02/layers/personal/hopyo-picks").json()["줄"] == []
|
||||
assert _post(client).json()["키"] == "HP-000008" # 옛 번 뒤로
|
||||
assert old.is_file() and _keys(client) == ["HP-000008"]
|
||||
@@ -0,0 +1,134 @@
|
||||
"""별칭 장부 시스템 모으기(PLAN 718-2 · `M01_MasterData_Alias`) — 회사 · 개인 층 맞춤 +
|
||||
내역맞춤 모음 · 같은 짝 묶기 · 회사 수 · 사람 수(익명) · 충돌 · 승인 → 마스터 줄 ·
|
||||
반려 기억 · 다른 회사 새 사용자가 마스터 줄을 씀
|
||||
· 시스템 관리자만 · master_data 원본 그대로. 층 · storage 는 임시 폴더.
|
||||
"""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from fastapi import Depends, FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from B09_Estimation import B09_Estimation_Bill_Link as lk
|
||||
from B09_Estimation import B09_Estimation_Router_Link as route
|
||||
from common_util.common_util_auth import require_system_admin, verify_session
|
||||
from M01_MasterData import M01_MasterData_Alias as alias
|
||||
from M02_MasterTemplete import M02_Hopyo_Ledger as ledger
|
||||
from M02_MasterTemplete import M02_Hopyo_Store as hs
|
||||
from M02_MasterTemplete import M02_Template_Layers as layers
|
||||
|
||||
# 올리기 길 시험의 `client` 마당(임시 storage · 마스터 층)을 같이 씀
|
||||
# ruff: noqa: F811
|
||||
from resources.tester.test_m02_hopyo_excel import client, master_hash # noqa: F401
|
||||
|
||||
DIG = {"이름": "구조물터파기(인력100%)", "규격": "토사", "단위": "㎥"}
|
||||
DIG_KEY = "구조물터파기(인력100%)|토사|m3"
|
||||
FORM_KEY = "합판거푸집|4회|m2"
|
||||
CUT_KEY = "토사절취||m3"
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def _pair(code):
|
||||
return ledger.check(DIG_KEY, {"갈래": "짝", "짝": [{"로직": code, "배": 1}]})
|
||||
|
||||
|
||||
def _seed():
|
||||
"""회사 7(개인 42 · 43 = 같은 짝) · 회사 8(개인 50 = 다른 짝 · 회사 층 = 안 씀) · 내역맞춤."""
|
||||
for user, who in ((42, "갑"), (43, "을")):
|
||||
mine = layers.personal_dir(7, user)
|
||||
ledger.write(mine, {DIG_KEY: _pair("GF000101")}, {"id": user, "이름": who}, "가공사.xls")
|
||||
ledger.write(layers.personal_dir(8, 50), {DIG_KEY: _pair("GF000103")}, {"id": 50}, "나.xls")
|
||||
skip = ledger.check(FORM_KEY, {"갈래": "안 씀"})
|
||||
ledger.write(layers.company_dir(8), {FORM_KEY: skip}, {"id": 51, "이름": "병"}, "다.xls")
|
||||
live = {"확정": "earth:4", "이름": "절토(토사)"}
|
||||
hs.remember(layers.personal_dir(7, 42), {CUT_KEY: live, "a||": {lk.FORGOT: True}}, lk.MEMORY)
|
||||
hs.remember(layers.company_dir(8), {CUT_KEY: live}, lk.MEMORY)
|
||||
|
||||
|
||||
def _one(got, kind, key, code=None):
|
||||
rows = [x for x in got["후보"] if (x["종류"], x["열쇠"]) == (kind, key)]
|
||||
if code:
|
||||
rows = [x for x in rows if x["짝"][0]["로직"] == code]
|
||||
assert len(rows) == 1, rows
|
||||
return rows[0]
|
||||
|
||||
|
||||
def test_모음_묶기_익명_충돌(client):
|
||||
before = master_hash()
|
||||
_seed()
|
||||
got = alias.gather()
|
||||
assert got["모은곳"] == {"회사": 2, "사람": 5} and got["반려"] == 0
|
||||
assert len(got["후보"]) == 4 # 터파기 둘(충돌) · 거푸집 안 씀 · 토사절취 내역맞춤(같은 짝 하나)
|
||||
same = _one(got, alias.ALIAS, DIG_KEY, "GF000101")
|
||||
assert (same["회사"], same["사람"], same["충돌"], same["마스터"]) == (1, 2, True, None)
|
||||
assert _one(got, alias.ALIAS, DIG_KEY, "GF000103")["사람"] == 1
|
||||
form = _one(got, alias.ALIAS, FORM_KEY)
|
||||
assert (form["갈래"], form["충돌"], form["사람"]) == ("안 씀", False, 1)
|
||||
cut = _one(got, alias.BILL, CUT_KEY)
|
||||
assert (cut["회사"], cut["사람"], cut["확정"], cut["충돌"]) == (2, 2, "earth:4", False)
|
||||
text = json.dumps(got, ensure_ascii=False)
|
||||
for secret in ("갑", "을", "병", "가공사.xls", "누가", "처음파일", "주인"):
|
||||
assert secret not in text # 익명 — 회사 수 · 사람 수만
|
||||
assert master_hash() == before
|
||||
|
||||
|
||||
def test_승인_마스터_줄_반려_기억(client, tmp_path, monkeypatch):
|
||||
before = master_hash()
|
||||
_seed()
|
||||
newbie = layers.personal_dir(9, 99) # 처음 쓰는 다른 회사 사용자
|
||||
assert ledger.recall(DIG, newbie) is None
|
||||
sig = _one(alias.gather(), alias.ALIAS, DIG_KEY, "GF000101")["서명"]
|
||||
got = alias.approve(alias.ALIAS, DIG_KEY, sig, {"id": 1, "이름": "관리자"})
|
||||
hit = ledger.recall(DIG, newbie)
|
||||
assert (hit["층"], hit["짝"][0]["로직"]) == ("system", "GF000101") # 승인 → 모두가 씀
|
||||
master = ledger.read(client.system)
|
||||
assert master[DIG_KEY]["처음파일"] is None # 마스터 줄에도 남의 파일 이름 없음
|
||||
other = _one(got, alias.ALIAS, DIG_KEY) # 승인한 짝 = 마스터와 같음 → 후보에서 빠짐
|
||||
assert other["짝"][0]["로직"] == "GF000103"
|
||||
assert (other["마스터"], other["충돌"]) == ("다름", True)
|
||||
got = alias.reject(alias.ALIAS, DIG_KEY, other["서명"], {"id": 1})
|
||||
assert got["반려"] == 1 and not [x for x in got["후보"] if x["열쇠"] == DIG_KEY]
|
||||
assert alias.gather()["반려"] == 1 # 반려 기억 = 다시 모아도 안 보임
|
||||
with pytest.raises(alias.store.StoreError) as error:
|
||||
alias.approve(alias.ALIAS, DIG_KEY, other["서명"], {"id": 1})
|
||||
assert error.value.status == 404
|
||||
with pytest.raises(alias.store.StoreError):
|
||||
alias.approve("모름", DIG_KEY, sig, {"id": 1})
|
||||
# 내역맞춤 승인 → 마스터 기억 → 다른 회사 새 사용자 B09 잇기 기억에 나옴
|
||||
cut = _one(alias.gather(), alias.BILL, CUT_KEY)
|
||||
alias.approve(alias.BILL, CUT_KEY, cut["서명"], {"id": 1})
|
||||
assert [r["열쇠"] for r in alias.master_bill()] == [CUT_KEY]
|
||||
monkeypatch.setattr(route.access, "folder", lambda got, create=False: tmp_path / got.layer)
|
||||
user = {"company_id": 9, "user_id": 99, "role": "USER"}
|
||||
assert route.recalled(user)[CUT_KEY]["확정"] == "earth:4"
|
||||
assert alias.forget_bill(CUT_KEY) == [] and CUT_KEY not in route.recalled(user)
|
||||
assert master_hash() == before
|
||||
|
||||
|
||||
def test_길_시스템_관리자만(client):
|
||||
_seed()
|
||||
app = FastAPI()
|
||||
app.include_router(alias.router, dependencies=[Depends(require_system_admin)])
|
||||
who = {"user_id": 1, "company_id": None, "role": "USER", "name": "갑"}
|
||||
app.dependency_overrides[verify_session] = lambda: who
|
||||
web = TestClient(app)
|
||||
for role in ("USER", "ADMIN"):
|
||||
who["role"] = role
|
||||
assert web.get("/api/m01/alias-ledger").status_code == 403
|
||||
who["role"] = "SYSTEM_ADMIN"
|
||||
got = web.get("/api/m01/alias-ledger").json()
|
||||
assert len(got["후보"]) == 4 and got["마스터내역"] == []
|
||||
form = _one(got, alias.ALIAS, FORM_KEY)
|
||||
pick = {"종류": alias.ALIAS, "열쇠": FORM_KEY, "서명": form["서명"]}
|
||||
res = web.post("/api/m01/alias-ledger/approve", json=pick)
|
||||
assert res.status_code == 200 and len(res.json()["후보"]) == 3
|
||||
assert ledger.read(client.system)[FORM_KEY]["갈래"] == "안 씀"
|
||||
assert web.post("/api/m01/alias-ledger/approve", json=pick).status_code == 404
|
||||
assert web.post("/api/m01/alias-ledger/reject", json={**pick, "서명": "x"}).status_code == 422
|
||||
gone = web.post("/api/m01/alias-ledger/master-bill/forget", json={"열쇠": CUT_KEY})
|
||||
assert gone.status_code == 404
|
||||
main = (ROOT / "main.py").read_text(encoding="utf-8")
|
||||
at = main.index("M01_MasterData.M01_MasterData_Alias")
|
||||
assert "dependencies=system_admin_only" in main[at : at + 120]
|
||||
@@ -58,7 +58,36 @@ def _cards(user: dict, company_id: int | None = None) -> list:
|
||||
return json.loads(result.stdout)
|
||||
|
||||
|
||||
def _pages(user: dict, company_id: int | None = None) -> list:
|
||||
# ☰ 메뉴 = pageLinks(settingLinks 의 구조물 조합 줄을 바탕으로 페이지 둘만)
|
||||
script = (
|
||||
"const ROUTES = { M01_MASTER_DATA: 'm01-master-data',"
|
||||
" M02_MASTER_TEMPLATE: 'm02-master-template', B01_ACCOUNT: 'b01-account' };\n"
|
||||
"const L = (k: string) => k;\n"
|
||||
"let went = '';\n"
|
||||
"const navigateTo = (r: string, q?: Record<string, string>) =>"
|
||||
" { went = r + (q ? '?' + new URLSearchParams(q) : ''); };\n"
|
||||
+ _fn(_text(NAV), "settingLinks")
|
||||
+ "\n"
|
||||
+ _fn(_text(NAV), "pageLinks")
|
||||
+ "\nconst user = JSON.parse(process.argv[1]);"
|
||||
+ "\nconst links = pageLinks(user, JSON.parse(process.argv[2]));"
|
||||
+ "\nconsole.log(JSON.stringify(links.map((l: any) => {"
|
||||
+ " navigateTo(l.route, l.query); return [l.key, l.title, decodeURIComponent(went)]; })));"
|
||||
)
|
||||
result = subprocess.run(
|
||||
["node", "--experimental-strip-types", "--no-warnings",
|
||||
"--input-type=module-typescript", "-e", script, json.dumps(user), json.dumps(company_id)],
|
||||
capture_output=True, text=True, encoding="utf-8", timeout=60,
|
||||
) # fmt: skip
|
||||
assert result.returncode == 0, result.stderr
|
||||
return json.loads(result.stdout)
|
||||
|
||||
|
||||
M01 = "m01-master-data"
|
||||
M02 = "m02-master-template"
|
||||
READ_SRC = "B01_Dashboard_PermSourceRead"
|
||||
WRITE_M = "B01_Dashboard_PermMasterWrite"
|
||||
# 계산 자리(504-2) — 시스템 관리자만 · 이동 대신 창
|
||||
CALC_SITES = ["B01_CalcSites_Title", "B01_CalcSites_Desc", "calc-sites-dialog"]
|
||||
READ_CO = (
|
||||
@@ -68,21 +97,42 @@ WRITE_CO = READ_CO.replace("CompanyRead", "CompanyWrite")
|
||||
|
||||
|
||||
@pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음")
|
||||
def test_시스템_관리자_카드_다섯_고른_회사_해시():
|
||||
m = "B01_Dashboard_PermMasterWrite"
|
||||
def test_시스템_관리자_줄_칸별_여덟_고른_회사_해시():
|
||||
# 716-2 · 718-3 — 대시보드 설계 기본값 = 칸별 줄: M01 4(마스터 요소 · 단가산출 로직 · 구조물 옵션 정의 ·
|
||||
# 별칭 장부) +
|
||||
# M02 3(일위대가 · 구조물 조합 · 도면 양식) + 계산 자리
|
||||
assert _cards({"role": "SYSTEM_ADMIN", "is_master": False}, 3) == [
|
||||
["B01_Dashboard_MasterElements", "B01_Dashboard_PermSourceRead", "m01-master-data"],
|
||||
["B01_Dashboard_CardStructCombo", m, f"{M02}?open=구조물조합&root=master&company=3"],
|
||||
["B01_Dashboard_CardStructOption", m, f"{M02}?open=옵션&root=master&company=3"],
|
||||
["B01_Dashboard_CardDrawing", m, f"{M02}?open=도면&root=master&company=3"],
|
||||
["M01_Title", READ_SRC, M01],
|
||||
["B01_Dashboard_CardLogic", READ_SRC, f"{M01}?open=로직"],
|
||||
["B01_Dashboard_CardStructOptionDef", WRITE_M, f"{M01}?open=옵션"],
|
||||
["B01_Dashboard_CardAliasLedger", WRITE_M, f"{M01}?open=별칭"],
|
||||
["B01_Dashboard_CardPrecombo", WRITE_M, f"{M02}?open=일위대가조합&root=master&company=3"],
|
||||
["B01_Dashboard_CardStructCombo", WRITE_M, f"{M02}?open=구조물조합&root=master&company=3"],
|
||||
["B01_Dashboard_CardDrawing", WRITE_M, f"{M02}?open=도면&root=master&company=3"],
|
||||
CALC_SITES,
|
||||
]
|
||||
# 회사가 없으면 해시에 안 실음
|
||||
assert _cards({"role": "SYSTEM_ADMIN", "is_master": False})[1][2] == (
|
||||
assert _cards({"role": "SYSTEM_ADMIN", "is_master": False})[5][2] == (
|
||||
f"{M02}?open=구조물조합&root=master"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음")
|
||||
def test_메뉴_페이지_둘만_역할별():
|
||||
# 716-2 — ☰ = M01 · M02 두 페이지(시스템 관리자) / M02 한 페이지(그 밖) · 열쇠 m01 · m02
|
||||
admin = _pages({"role": "SYSTEM_ADMIN", "is_master": False, "company_id": 1}, 3)
|
||||
assert admin == [
|
||||
["m01", "B01_Dashboard_MasterElements", M01],
|
||||
["m02", "M02_Title", f"{M02}?open=구조물조합&root=personal&company=3"],
|
||||
]
|
||||
assert _pages({"role": "ADMIN", "is_master": False, "company_id": None}) == [
|
||||
["m02", "M02_Title", f"{M02}?open=구조물조합&root=company"],
|
||||
]
|
||||
assert _pages({"role": "USER", "is_master": False, "company_id": 1}) == [
|
||||
["m02", "M02_Title", f"{M02}?open=구조물조합&root=personal"],
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음")
|
||||
def test_회사_관리자_개인_카드_하나():
|
||||
company = [
|
||||
@@ -191,14 +241,12 @@ def test_회사_찾기_거르기():
|
||||
def test_구조물_조합_기본_층_개인():
|
||||
"""123-1 — 회사 있는 사람은 역할 무관 구조물 조합 = 개인 · 회사 없으면 지금 규칙 · 다른 곳 그대로 ·
|
||||
134-4 — 층 없는 주소도 개인(회사 없으면 마스터) · 사전조합 기본 층도 같은 규칙."""
|
||||
m = "B01_Dashboard_PermMasterWrite"
|
||||
combo = f"{M02}?open=구조물조합&root=personal"
|
||||
assert _cards({"role": "SYSTEM_ADMIN", "is_master": False, "company_id": 1}, 3) == [
|
||||
["B01_Dashboard_MasterElements", "B01_Dashboard_PermSourceRead", "m01-master-data"],
|
||||
["B01_Dashboard_CardStructCombo", m, f"{combo}&company=3"],
|
||||
["B01_Dashboard_CardStructOption", m, f"{M02}?open=옵션&root=master&company=3"],
|
||||
["B01_Dashboard_CardDrawing", m, f"{M02}?open=도면&root=master&company=3"],
|
||||
CALC_SITES,
|
||||
rows = _cards({"role": "SYSTEM_ADMIN", "is_master": False, "company_id": 1}, 3)
|
||||
assert rows[4:7] == [
|
||||
["B01_Dashboard_CardPrecombo", WRITE_M, f"{M02}?open=일위대가조합&root=personal&company=3"],
|
||||
["B01_Dashboard_CardStructCombo", WRITE_M, f"{combo}&company=3"],
|
||||
["B01_Dashboard_CardDrawing", WRITE_M, f"{M02}?open=도면&root=master&company=3"],
|
||||
]
|
||||
assert _cards({"role": "ADMIN", "is_master": False, "company_id": 1}) == [
|
||||
["B01_Dashboard_CardStructCombo", WRITE_CO, combo],
|
||||
@@ -206,7 +254,7 @@ def test_구조물_조합_기본_층_개인():
|
||||
assert _cards({"role": "USER", "is_master": True, "company_id": 1})[0][2] == combo
|
||||
assert _cards({"role": "USER", "is_master": False, "company_id": 1})[0][2] == combo
|
||||
# 개인 층 없음(회사 없음) = 지금 규칙
|
||||
assert _cards({"role": "SYSTEM_ADMIN", "is_master": False, "company_id": None})[1][2] == (
|
||||
assert _cards({"role": "SYSTEM_ADMIN", "is_master": False, "company_id": None})[5][2] == (
|
||||
f"{M02}?open=구조물조합&root=master"
|
||||
)
|
||||
assert _cards({"role": "ADMIN", "is_master": False, "company_id": None})[0][2] == (
|
||||
|
||||
@@ -179,8 +179,10 @@ def test_올리기_목록_읽기_지우기(client):
|
||||
assert (folder / hs.DIRNAME / "HP-000001.json").is_file()
|
||||
assert all(r["종류"] in layers.KINDS for r in layers.list_templates(folder)) # 양식에 안 섞임
|
||||
assert not any("실무자료" in str(p) for _, p in layers.layer_files("precombo"))
|
||||
assert _post(client).json()["키"] == "HP-000002"
|
||||
again = _post(client).json() # 718-1 — 작업 중 같은 엑셀 = 그 파일 이어서
|
||||
assert again["키"] == "HP-000001" and again["이어서"] is True
|
||||
assert client.delete(URL.format("personal") + "/HP-000001").status_code == 200
|
||||
assert _post(client).json()["키"] == "HP-000002" # 지운 번 다시 안 씀
|
||||
assert [r["키"] for r in client.get(URL.format("personal")).json()["줄"]] == ["HP-000002"]
|
||||
assert client.get(URL.format("personal") + "/HP-000001").status_code == 404
|
||||
assert client.get(URL.format("personal") + "/../x").status_code == 404
|
||||
|
||||
@@ -105,8 +105,8 @@ def test_끝까지_확인한_줄만_별칭(client):
|
||||
keys = {r["열쇠"]: r for r in rows}
|
||||
assert {p["로직"] for p in keys[rebar["맞춤"]["열쇠"]]["짝"]} == {"GC000206", "GC000207"}
|
||||
# 다시 올림 = 확인한 줄은 별칭 · 체크 · 만든 것이 보임
|
||||
_up(client, "HP-000002")
|
||||
again = {x["이름"]: x["맞춤"] for x in _one(client, "HP-000002", no)["줄"]}
|
||||
assert _post(client).json()["이어서"] is True # 718-1 — 같은 엑셀 = 그 파일 이어서(새 키 없음)
|
||||
again = {x["이름"]: x["맞춤"] for x in _one(client, "HP-000001", no)["줄"]}
|
||||
assert again["레미콘타설(장비사용)"]["상태"] == "별칭" and again["레미콘타설(장비사용)"]["체크"]
|
||||
assert again["철근가공조립(현장)"]["상태"] == "별칭"
|
||||
assert again["철근가공조립(현장)"]["대상"][0]["출처"] == {
|
||||
@@ -138,7 +138,9 @@ def test_막는_길(client):
|
||||
"/api/m02/layers/personal/hopyo-build",
|
||||
json={"층": "personal", "키": "HP-000001", "번": 1, "줄": []},
|
||||
)
|
||||
assert empty.status_code == 200 and empty.json()["줄수"]["품셈"] == 0 # 빈 몸 = 모두 고정(705-3)
|
||||
assert (
|
||||
empty.status_code == 200 and empty.json()["줄수"]["품셈"] == 0
|
||||
) # 빈 몸 = 모두 고정(705-3)
|
||||
money = client.post(
|
||||
"/api/m02/layers/personal/hopyo-build",
|
||||
json={"층": "personal", "키": "HP-000001", "번": 1, "줄": [{"차례": 1, "금액": 1}]},
|
||||
|
||||
@@ -84,13 +84,15 @@ def test_올리기_격자_200_404_지우기(client):
|
||||
path.unlink() # 옛 파일(324) = 격자 없음
|
||||
old = client.get(GRID.format("personal", "HP-000001"))
|
||||
assert old.status_code == 404 and old.json()["detail"] == "격자 없음 — 다시 올리면 보임"
|
||||
assert _post(client).status_code == 200
|
||||
assert client.delete(URL.format("personal") + "/HP-000001").status_code == 200
|
||||
assert _post(client).status_code == 200 # 718-1 — 같은 엑셀은 지운 뒤 새 번
|
||||
assert client.get(GRID.format("personal", "HP-000002")).status_code == 200
|
||||
assert client.delete(URL.format("personal") + "/HP-000002").status_code == 200
|
||||
assert not (folder / hs.DIRNAME / hs.GRID / "HP-000002.json").exists()
|
||||
assert client.get(GRID.format("personal", "HP-000002")).status_code == 404
|
||||
assert client.get(GRID.format("personal", "../x")).status_code == 404
|
||||
assert client.get(URL.format("personal") + "/HP-000001/3").status_code == 200 # 길 안 부딪힘
|
||||
gone = client.get(URL.format("personal") + "/HP-000001/3") # 길 안 부딪힘(718-1 지운 뒤 = 없음)
|
||||
assert gone.status_code == 404 and "없는 실무자료" in gone.json()["detail"]
|
||||
assert master_hash() == before
|
||||
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ NEW = (
|
||||
"Hopyo_CheckRows",
|
||||
"Hopyo_Choose",
|
||||
"Hopyo_Ledger",
|
||||
"Hopyo_Wizard",
|
||||
)
|
||||
ROW = {
|
||||
"층": "system",
|
||||
@@ -301,7 +302,7 @@ def test_옛_즉시_확정과_바로_만들기_없음():
|
||||
"근거 맞음 모두 확정",
|
||||
'"확정"',
|
||||
'"사전조합 만들기"',
|
||||
"불러오기",
|
||||
'"불러오기" 옛 단추', # 718-1 — 「불러오기」 = 칸 이름 · 마법사 단추라 낱말은 뺌
|
||||
):
|
||||
assert label not in every, label
|
||||
users = sorted(name for name, text in texts.items() if "buildHopyo(" in text)
|
||||
@@ -342,20 +343,18 @@ def test_좌측_판_자리와_권한():
|
||||
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: opt.canWrite ? () => void refresh() : undefined," 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
|
||||
# 718-1 — 제목 = 화면 글 키 · [불러오기] = 구조물 조합 층 단추 꼴 → 본 칸 마법사 · 옛 파일 목록 없음
|
||||
assert 'L("M02_HopyoImport")' in hopyo_side and "일위대가 실무자료" not in hopyo_side
|
||||
start = hopyo_side.split("const start = createButton({")[1].split("start.title")[0]
|
||||
assert 'label: "불러오기"' in start and 'start.classList.add("m02-layers__btn");' in start
|
||||
assert "buildHopyoWizard()" in hopyo_side and "listHopyo" not in hopyo_side
|
||||
assert "uploadHopyo" not in hopyo_side and "openHopyoSheet" not in hopyo_side
|
||||
assert "buildHopyoLedger({ scope, canWrite: opt.canWrite })" in hopyo_side # [별칭 장부]
|
||||
assert hopyo_side.index("list, ledger]") > 0
|
||||
wizard = _text("M02_MasterTemplete_Hopyo_Wizard.ts")
|
||||
assert 'accept: ".xls,.xlsx"' in wizard and "input.hidden = true;" in wizard
|
||||
assert "accessFor(layer, null, user).canWrite" in wizard # 결과 층 = 쓸 수 있는 층만
|
||||
assert 'let pick: ManageLayer = "personal";' in wizard # 기본 개인
|
||||
assert "export const SANGEUN_READY = false;" in wizard # 단가산출 결과 = 715-2 뒤 켬
|
||||
|
||||
|
||||
def test_로직_추가_한_곳과_파일_규칙():
|
||||
@@ -379,9 +378,9 @@ def test_로직_추가_한_곳과_파일_규칙():
|
||||
|
||||
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]
|
||||
side = _text("M02_MasterTemplete_Hopyo_Wizard.ts")
|
||||
assert "second(pick, got);" in side # 올린 직후 ② 걸음
|
||||
assert lazy in side.split("function second(")[1].split("async function end(")[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
|
||||
@@ -517,6 +516,7 @@ def test_명사형_단추와_별칭_장부_풀이_359():
|
||||
assert f"label: {label}" in ledger, label
|
||||
assert '"우리에 없음": "로직없음"' in ledger and '"안 씀": "사용안함"' in ledger
|
||||
mine = ("Hopyo_Side", "Hopyo_Sheet", "Hopyo_Check", "Hopyo_CheckRows", "Hopyo_Ledger")
|
||||
mine += ("Hopyo_Wizard",)
|
||||
every = "\n".join(_text(f"M02_MasterTemplete_{n}.ts") for n in mine)
|
||||
for gone in (
|
||||
"고친 것 버림",
|
||||
@@ -532,8 +532,8 @@ def test_명사형_단추와_별칭_장부_풀이_359():
|
||||
|
||||
|
||||
def test_372_2_저장은_개인_층에_삭제는_화면_층_권한():
|
||||
side = _text("M02_MasterTemplete_Hopyo_Side.ts")
|
||||
open_ = side.split("sheet.openHopyoSheet({")[1].split("});")[0]
|
||||
assert 'target: { layer: "personal", owner: null },' in open_
|
||||
assert "canWrite: true," in open_ # 만들기 = 개인 층 · 화면 층 무관
|
||||
assert "onDeleted: opt.canWrite ? () => void refresh() : undefined," in open_
|
||||
"""718-1 — 만들 층 = 마법사에서 고른 층(올린 층과 같음) · [삭제] = 끝내기 → 처음 걸음."""
|
||||
side = _text("M02_MasterTemplete_Hopyo_Wizard.ts")
|
||||
open_ = side.split("m.openHopyoSheet({")[1].split("}),")[0]
|
||||
assert "target: scope," in open_ and "canWrite: true," in open_
|
||||
assert "onDeleted: () => void first()," in open_
|
||||
|
||||
@@ -85,7 +85,7 @@ def _base(code):
|
||||
|
||||
# ── 자료 틀 ────────────────────────────────────────────────────────────
|
||||
def test_단가승격_서고는_없음_층_종류에서_뺌(world):
|
||||
assert "promote" not in layers.KINDS and layers.LIBRARY_KINDS == ("precombo",)
|
||||
assert "promote" not in layers.KINDS and layers.LIBRARY_KINDS == ("precombo", "sangeun")
|
||||
with store.at(world["system"]), pytest.raises(KeyError):
|
||||
refs.create("promote", {"담은로직": [{"로직": TEN}]})
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ def test_사전조합_층_단추는_페이지를_다시_안_세움():
|
||||
|
||||
def test_고름에_owner_권한_서고_길에_명시_owner():
|
||||
side = _text("M02_MasterTemplete_UI_Side_Precombo.ts")
|
||||
assert 'export const PRECOMBO_TITLE = "일위대가 사전조합";' in side
|
||||
assert "PRECOMBO_TITLE" not in side # 716-3 — 제목은 locale 키 「일위대가」
|
||||
assert "owner: scope.owner," in side and "canWrite: scope.canWrite," in side
|
||||
assert "listTemplates(at.layer, null, at.owner)" in side
|
||||
main = _text("M02_MasterTemplete_UI_Main.ts")
|
||||
@@ -133,3 +133,20 @@ def test_목록표_빈_구분_미분류_고르기_창_새_분류_거름():
|
||||
# 거름은 칸 값 그대로 — 품셈 구분을 고르면 서고 줄 빠지고 새 분류를 고르면 로직 줄 빠짐
|
||||
assert "(!sub.select.value || x.row.구분 === sub.select.value)" in pick
|
||||
assert "(!sub.select.value || x.구분 === sub.select.value)" in pick
|
||||
|
||||
|
||||
def test_왼쪽_칸_차례_일위대가_구조물조합_구분선_불러오기_716_3():
|
||||
side = _text("M02_MasterTemplete_UI_Side.ts")
|
||||
order = side[side.index("m02-side__lists") :]
|
||||
marks = [
|
||||
"precomboSec,",
|
||||
"summary,",
|
||||
"createDivider()",
|
||||
"hopyo.title",
|
||||
"drawing ?",
|
||||
"practice ?",
|
||||
]
|
||||
at = [order.index(m) for m in marks]
|
||||
assert at == sorted(at)
|
||||
|
||||
assert 'L("M02_PrecomboTitle")' in side and "buildOptionsSection" not in side
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* 쓰기 없음 — 서고 목록 · 문서 읽기는 가짜 응답(fetch 바꿔 끼움 · 끝에 되돌림)
|
||||
* 기대 수치:
|
||||
* - table.wrap = "m01-master__grid-wrap" · table.table = "m01-master__grid" · table.row = "m01-master__table-row"
|
||||
* - table.filters = true(찾기 칸 + 쪽 `m01-master__pager`) · table.sameCell = true(M01 표 칸과 padding · 글자 · 밑줄 같음)
|
||||
* - table.filters = true(찾기 칸 + 표 바로 아래 쪽 단추 `ui-pager`) · table.sameCell = true(M01 표 칸과 padding · 글자 · 밑줄 같음)
|
||||
* - table.search = 1(「값 없음」 치면 한 줄)
|
||||
* - buttons.variants = ["filled", "ghost", "danger"](M01 [신규 로직] · [로직 복사/생성] · [로직 지우기] 와 같음) · buttons.redDelete = true
|
||||
* - precombo.add = "로직 추가" · picker.label = "로직 고르기" · picker.kinds ⊂ {단가승격, 일위조합} · picker.logicRows = 0
|
||||
@@ -100,7 +100,7 @@
|
||||
row: wrap.querySelector("[data-lib-key]").className,
|
||||
filters:
|
||||
!!wrap.parentElement.querySelector(".m01-head__filters input[type=search]") &&
|
||||
!!wrap.parentElement.querySelector(".m01-head__filters .m01-master__pager"),
|
||||
wrap.nextElementSibling?.classList.contains("ui-pager") === true,
|
||||
sameCell: box(td) === box(ref.querySelector("td")),
|
||||
cell: box(td),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user