Merge remote-tracking branch 'origin/dev' into sub_laptop_3
This commit is contained in:
@@ -70,31 +70,28 @@ WRITE_CO = READ_CO.replace("CompanyRead", "CompanyWrite")
|
||||
|
||||
|
||||
@pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음")
|
||||
def test_시스템_관리자_카드_다섯_고른_회사_해시():
|
||||
def test_시스템_관리자_카드_넷_고른_회사_해시():
|
||||
m = "B01_Dashboard_PermMasterWrite"
|
||||
assert _cards({"role": "SYSTEM_ADMIN", "is_master": False}, 3) == [
|
||||
["B01_Dashboard_MasterElements", "B01_Dashboard_PermSourceRead", "m01-master-data"],
|
||||
["B01_Dashboard_CardUnitCombo", m, f"{M02}?open=일위대가&root=master&company=3"],
|
||||
["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"],
|
||||
]
|
||||
# 회사가 없으면 해시에 안 실음
|
||||
assert _cards({"role": "SYSTEM_ADMIN", "is_master": False})[1][2] == (
|
||||
f"{M02}?open=일위대가&root=master"
|
||||
f"{M02}?open=구조물조합&root=master"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음")
|
||||
def test_회사_관리자_개인_카드_둘():
|
||||
def test_회사_관리자_개인_카드_하나():
|
||||
company = [
|
||||
["B01_Dashboard_CardUnitCombo", WRITE_CO, f"{M02}?open=일위대가&root=company"],
|
||||
["B01_Dashboard_CardStructCombo", WRITE_CO, f"{M02}?open=구조물조합&root=company"],
|
||||
]
|
||||
assert _cards({"role": "ADMIN", "is_master": False}, 3) == company # 회사 인자 안 실음
|
||||
assert _cards({"role": "USER", "is_master": True}) == company # 회사 마스터 = 회사 수정
|
||||
assert _cards({"role": "USER", "is_master": False}) == [
|
||||
["B01_Dashboard_CardUnitCombo", READ_CO, f"{M02}?open=일위대가&root=personal"],
|
||||
["B01_Dashboard_CardStructCombo", READ_CO, f"{M02}?open=구조물조합&root=personal"],
|
||||
]
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""B05 · B06 좌측 패널 아래 고정(계획서 44-3) — 단추 묶음이 패널 바닥에 붙고 위 칸만 스크롤.
|
||||
|
||||
공용 쪼개기(`splitSidebarActions`)가 [스크롤 칸][아래 고정 단추] 로 나누는데, 한 페이지(design_page)는
|
||||
좌측 자리를 빈 채 틀에 넘겨 쪼개기가 지나가고, 보기 전환이 패널 클래스를 통째 덮어 `ui-sidebar-fill` 을
|
||||
지웠다. 세 자리가 다시 어긋나면 빨강. 화면 수치는 `ui_checks/44-3_좌측패널_하단고정.js`.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
OVERLAY = ROOT / "ui_template" / "ui_template_overlay.ts"
|
||||
PANEL = ROOT / "A00_Common" / "design" / "design_panel.ts"
|
||||
SHARED = ROOT / "A00_Common" / "design" / "design_shared.ts"
|
||||
|
||||
|
||||
def read(path: Path) -> str:
|
||||
return path.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_split_is_exported_and_idempotent():
|
||||
src = read(OVERLAY)
|
||||
assert "export function splitSidebarActions(" in src
|
||||
assert 'if (body.classList.contains("ui-sidebar-split")) return;' in src
|
||||
|
||||
|
||||
def test_shared_panel_splits_after_attach():
|
||||
src = read(SHARED)
|
||||
attach = src.index("shared.left.append(panel.root);")
|
||||
assert "splitSidebarActions(body)" in src[attach : attach + 400]
|
||||
|
||||
|
||||
def test_panel_keeps_fill_class_and_scroll_host():
|
||||
src = read(PANEL)
|
||||
# 보기 전환이 클래스를 통째 덮으면 ui-sidebar-fill 이 지워져 단추 묶음이 바닥에서 뜬다.
|
||||
assert "root.className = next" not in src
|
||||
# 늦게 서는 칸은 스크롤 칸 안으로 — 패널 루트에 바로 끼우면 스크롤 밖에 남는다.
|
||||
assert ":scope > .ui-sidebar-scroll" in src
|
||||
@@ -80,3 +80,13 @@ def test_일위대가_카드는_상세_카드_갈래():
|
||||
rows = _text("M02_MasterTemplete_Labor_Rows.ts")
|
||||
assert "const tools = !!(move || remove);" in rows and "compactTable" not in rows
|
||||
assert "compact" not in _text("M02_MasterTemplete_Labor_View.ts").split("*/", 1)[1]
|
||||
|
||||
|
||||
def test_일위대가_상세_단위는_표준_단위_고르기():
|
||||
"""44-7 — 상세 머리 「단위」 = 표준 단위 목록(UnitPick) · 지금 값이 목록에 없어도 선택지 ·
|
||||
고르면 캐시만(저장은 상세 [저장]) · 「-」 = 없음."""
|
||||
detail = _text("M02_MasterTemplete_Labor_Detail.ts")
|
||||
assert 'import { createUnitPick, NO_UNIT } from "./DataFlow/M02_DF_UnitPick";' in detail
|
||||
assert "extra: [unit0, cur.단위].filter((u): u is string => !!u)," in detail
|
||||
assert "cur.단위 = u === NO_UNIT ? null : u;" in detail
|
||||
assert "readOnly: !edit," in detail and "maxLength = 20" not in detail
|
||||
|
||||
@@ -39,7 +39,7 @@ def world(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> dict[str, Any]:
|
||||
for cid in ("rv", "c1"):
|
||||
store.create_structure(cid)
|
||||
got = store.read("labor", "rv")
|
||||
logic = [{"로직": TEN, "입력값": {}, "수량": {"손": 2}}]
|
||||
logic = [{"로직": TEN, "별칭": " 돌붙임 ", "입력값": {}, "수량": {"손": 2}}]
|
||||
store.write("labor", "rv", got["판"], {**got["문서"], "단위": "개소", "담은로직": logic})
|
||||
personal = layers.personal_dir(7, 42)
|
||||
items_store.ensure(personal)
|
||||
@@ -71,6 +71,7 @@ def test_같은_층_일위대가_하나_복사_번호_열은_받는_쪽(world):
|
||||
c1 = _labor(system, "c1")
|
||||
assert (c1["열"], c1["일위대가번"]) == ("c1", before["일위대가번"])
|
||||
assert (c1["단위"], c1["담은로직"][0]["로직"]) == ("개소", TEN)
|
||||
assert c1["담은로직"][0]["별칭"] == "돌붙임" # 별칭도 같이(44-6 · 앞뒤 빈칸 뗌)
|
||||
assert c1["계산값"]["반영합"]["계"] > 0 # 서버가 다시 셈
|
||||
|
||||
|
||||
@@ -80,6 +81,7 @@ def test_다른_층에서_내_개인으로_저장_뒤에도_덮어씀(world):
|
||||
for _ in range(2): # 저장된 데이터도 다시 바꿈
|
||||
assert client.post(url).status_code == 200
|
||||
assert _labor(personal, "a")["담은로직"][0]["로직"] == TEN
|
||||
assert _labor(personal, "a")["담은로직"][0]["별칭"] == "돌붙임"
|
||||
area = URL.format("personal", "area", "a") + "?from=rv&from_layer=system"
|
||||
assert client.post(area).status_code == 200
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ def test_정리_모르는_입력_빈_값_버림_자재_칸은_M01_것():
|
||||
)
|
||||
)
|
||||
assert got["담은로직"] == [
|
||||
{"로직": STONE, "메모": None, "입력값": {"돌": "깬돌"}, "수량": None}
|
||||
{"로직": STONE, "별칭": None, "메모": None, "입력값": {"돌": "깬돌"}, "수량": None}
|
||||
]
|
||||
assert got["자재"][0]["이름"] == "치 즐(0.2㎥)" and got["자재"][0]["수량"] == {"손": 2}
|
||||
assert (got["종류"], got["단위"], got["본뜬"]) == ("일위대가", "개소", None)
|
||||
@@ -47,6 +47,8 @@ def test_정리_모르는_입력_빈_값_버림_자재_칸은_M01_것():
|
||||
_doc(담은로직=[{"로직": "GF999999"}]), # 없는 로직
|
||||
_doc(담은로직=[{"로직": STONE, "수량": {"손": "열"}}]), # 수 아님
|
||||
_doc(담은로직=[{"로직": STONE, "수량": 3}]), # 꼴 틀림
|
||||
_doc(담은로직=[{"로직": STONE, "별칭": 3}]), # 별칭 글 아님(44-6)
|
||||
_doc(담은로직=[{"로직": STONE, "별칭": "가" * 41}]), # 별칭 40자 넘음
|
||||
_doc(자재=[{"코드": "LB000001"}]), # 자재품목 아님
|
||||
_doc(자재=[{"코드": MAT}, {"코드": MAT}]), # 같은 자재 두 번
|
||||
"문서 아님",
|
||||
@@ -58,6 +60,13 @@ def test_틀린_문서는_400(bad):
|
||||
assert e.value.status == 400
|
||||
|
||||
|
||||
def test_별칭은_앞뒤_빈칸_떼고_빈_글은_null():
|
||||
got = labor.clean(
|
||||
_doc(담은로직=[{"로직": STONE, "별칭": " 돌붙임 "}, {"로직": TEN, "별칭": " "}])
|
||||
)
|
||||
assert [r["별칭"] for r in got["담은로직"]] == ["돌붙임", None]
|
||||
|
||||
|
||||
def test_빈_일위대가도_됨():
|
||||
got = labor.server(_doc())
|
||||
assert got["계산값"]["기준합"]["계"] == 0 and got["계산값"]["반영합"]["계"] == 0
|
||||
|
||||
@@ -41,10 +41,10 @@ def test_하위_차례_전체_항목_이름():
|
||||
assert '| "labor";' in api and "일위대가번: string;" in api
|
||||
assert "onAll?: (kind: AllKind) => void;" in side # 화면은 ItemList(39-5) · 페이지가 이음
|
||||
assert "if (!opt.onAll || allPicked === kind || !(await opt.confirmLeave())) return;" in side
|
||||
assert 'return void selectAll("labor");' in side # 해시 open=일위대가
|
||||
assert 'selectAll("labor")' not in side # 해시 open=일위대가 길 없앰(44-7)
|
||||
locale = (ROOT / "ui_template" / "ui_template_locale_m2.ts").read_text("utf-8")
|
||||
assert 'M02_KindLabor: ["일위대가"' in locale and 'M02_ItemsAll: ["전체 항목"' in locale
|
||||
assert 'B01_Dashboard_CardUnitCombo: ["일위대가",' in locale
|
||||
assert "B01_Dashboard_CardUnitCombo" not in locale # 대시보드 일위대가 카드 없앰(44-7)
|
||||
css = _text("M02_MasterTemplete_UI_Style.css")
|
||||
assert ".m02-side__rule {" in css and "border-top: 1px solid var(--color-border);" in css
|
||||
|
||||
|
||||
@@ -144,7 +144,7 @@ console.log(JSON.stringify([
|
||||
|
||||
|
||||
def test_수량_글은_이은_칸_주소를_보임() -> None:
|
||||
"""`Labor_View.setQty(수량, 칸)` 의 칸 주소로 「수량 B6 = 20 ㎡」 — 없으면 「면적산출 칸」."""
|
||||
"""`Labor_View.setQty(수량, 칸)` 의 칸 주소로 「수량 20 ㎡(B6)」 — 없으면 「면적산출 칸」."""
|
||||
node = shutil.which("node")
|
||||
if not node:
|
||||
pytest.skip("node 없음")
|
||||
@@ -154,11 +154,11 @@ def test_수량_글은_이은_칸_주소를_보임() -> None:
|
||||
assert out.returncode == 0, out.stderr
|
||||
got = json.loads(out.stdout)
|
||||
# 수량산출 열(40-1) — 위 결과단위 기준 수량 · 아래 지금 수량 짧은 글
|
||||
assert got[:7] == ["10 ㎡", "1 ㎥", "", "손 5 ㎥", "B6 = 12.5", "B6 = 빈 칸", "—"]
|
||||
assert got[:7] == ["10 ㎡", "1 ㎥", "", "손 5 ㎥", "12.5(B6)", "빈 칸(B6)", "—"]
|
||||
assert got[7:] == [
|
||||
"수량 B6 = 20 ㎡",
|
||||
"수량 면적산출 칸 = 20 ㎡",
|
||||
"수량 20 ㎡(B6)", # 44-6 — 수량(칸 주소)
|
||||
"수량 20 ㎡(면적산출 칸)",
|
||||
"수량 손 입력 3 ㎡",
|
||||
"수량 B6 = 빈 칸", # 이은 칸이 비었음 = 수량 없음(39-8 ② · 서버 area_values 와 같음)
|
||||
"수량 빈 칸(B6)", # 이은 칸이 비었음 = 수량 없음(39-8 ② · 서버 area_values 와 같음)
|
||||
"",
|
||||
]
|
||||
|
||||
@@ -87,7 +87,7 @@ def test_주소_인자를_라우트에서_뗌():
|
||||
# 진입 해시 `?open=&root=&owner=`(PLAN 27-1d) · 옛 `?layer=` 도 root 로
|
||||
assert 'query.get("root") ?? query.get("layer")' in layer
|
||||
assert 'raw === "company" || raw === "personal" ? raw : "master"' in layer
|
||||
assert '["구조물조합", "일위대가", "옵션", "도면"]' in layer
|
||||
assert '["구조물조합", "옵션", "도면"]' in layer # 일위대가 = 구조물 조합 안(44-7)
|
||||
picker = _text(ROOT / "ui_template" / "ui_template_item_picker.ts")
|
||||
assert 'open: "구조물조합", root: "personal"' in picker and "layer=personal" not in picker
|
||||
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
/* 44-2 — B05 계획노선 편집 · 측점 횡단 글자 크기
|
||||
* 시작 화면: B05 종단설계(#/b05-profile) · 환경 검증 프로젝트(OWNERS.md) · 로그인됨 · 계획노선 편집 닫힘
|
||||
* 부르는 법: orca eval --expression "$(cat 이 파일)" (async 식 · JSON 글을 돌려줌)
|
||||
* 하는 일: [계획노선 편집] 열기 → 첫 측점 라벨 누름(앱 모듈의 라벨 상자 자리) → 횡단 판 글자 잼 → [취소]로 닫음
|
||||
* 기대 수치:
|
||||
* - keys = 원지반 · 기본 계획 횡단 · 「좌 … 표고 …」 셋 모두 fontSize 5.5px(옛 11px 의 절반)
|
||||
* - station.fontSize = title.fontSize(측점 표기 = 제목 「횡단」 크기)
|
||||
* - 바꾸는 값 없음(취소로 닫음 · 노선 안 고침)
|
||||
*/
|
||||
(async () => {
|
||||
const w = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
const until = async (fn, ms = 15000) => {
|
||||
for (let t = 0; t < ms; t += 200) {
|
||||
const v = fn();
|
||||
if (v) return v;
|
||||
await w(200);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
const btn = (text) =>
|
||||
[...document.querySelectorAll("button")].find(
|
||||
(b) => b.offsetParent && b.textContent.trim() === text,
|
||||
);
|
||||
btn("계획노선 편집")?.click();
|
||||
const canvas = await until(() =>
|
||||
[...document.querySelectorAll("canvas")].find(
|
||||
(c) => c.offsetParent && c.closest("[class*='routeedit']"),
|
||||
),
|
||||
);
|
||||
if (!canvas) return JSON.stringify({ ok: false, why: "편집 캔버스 없음" });
|
||||
// 앱이 실제로 부른 그 모듈 주소(@fs/<폴더>/…) — 같은 주소라야 라벨 상자 WeakMap 을 같이 씀
|
||||
const url = performance
|
||||
.getEntriesByType("resource")
|
||||
.map((e) => e.name)
|
||||
.find((n) => n.includes("B05_Profile_UI_RouteEdit_Input.ts"));
|
||||
const input = await import(url);
|
||||
const boxes = await until(() => {
|
||||
const b = input.stationLabelBoxes(canvas);
|
||||
return b.length ? b : null;
|
||||
});
|
||||
if (!boxes) return JSON.stringify({ ok: false, why: "측점 라벨 상자 없음" });
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const box = boxes[Math.floor(boxes.length / 2)];
|
||||
const at = { clientX: rect.left + box.x, clientY: rect.top + box.y, button: 0, bubbles: true };
|
||||
canvas.dispatchEvent(new PointerEvent("pointerdown", at));
|
||||
canvas.dispatchEvent(new PointerEvent("pointerup", at));
|
||||
const pane = await until(() =>
|
||||
[...document.querySelectorAll(".b05-routeedit__cross")].find((p) =>
|
||||
p.querySelector(".b05-routeedit__cross-key"),
|
||||
),
|
||||
);
|
||||
if (!pane) return JSON.stringify({ ok: false, why: "횡단 판 안 뜸", chainage: box.chainage });
|
||||
const fs = (el) => el && getComputedStyle(el).fontSize;
|
||||
const keys = [...pane.querySelectorAll(".b05-routeedit__cross-key")].map((t) => ({
|
||||
text: t.textContent.slice(0, 20),
|
||||
fontSize: fs(t),
|
||||
h: +t.getBoundingClientRect().height.toFixed(1),
|
||||
}));
|
||||
const title = pane.querySelector(".b05-routeedit__cross-title");
|
||||
const station = pane.querySelector(".b05-routeedit__cross-station");
|
||||
const out = {
|
||||
keys,
|
||||
title: { text: title.textContent, fontSize: fs(title) },
|
||||
station: { text: station.textContent, fontSize: fs(station) },
|
||||
};
|
||||
out.ok =
|
||||
keys.length === 3 &&
|
||||
keys.every((k) => k.fontSize === "5.5px") &&
|
||||
out.title.fontSize === out.station.fontSize;
|
||||
btn("취소")?.click();
|
||||
return JSON.stringify(out);
|
||||
})();
|
||||
@@ -0,0 +1,69 @@
|
||||
/* 44-3 — B05 · B06 좌측 패널 아래 고정(구조물 목록 · 초기화 · 저장 · 보기 전환 단추 묶음)
|
||||
* 시작 화면: B05 종단설계(#/b05-profile) · 좌측 패널 열림 · 프로젝트 = 환경 배정 storage 프로젝트
|
||||
* 부르는 법: orca eval --expression "$(cat 이 파일)" (async 식 · JSON 글을 돌려줌)
|
||||
* 기대 수치(B05 → B06 → B05 · 각 보기에서 보통 · 긴 패널 둘):
|
||||
* - split = true · 패널 루트 자식 = [ui-sidebar-scroll, 단추 묶음] · 칸은 모두 스크롤 칸 안(loose = 0)
|
||||
* - dockGap = 패널 몸통 바닥 − 단추 묶음 바닥 = 0(±1px)
|
||||
* - 긴 패널(스크롤 칸에 2000px 임시 칸): overflow = true · dockGap 그대로 0 · 단추 묶음 top 안 바뀜
|
||||
* - 끝나면 임시 칸 지우고 처음 주소로 되돌림
|
||||
*/
|
||||
(async () => {
|
||||
const w = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
const start = location.hash;
|
||||
const measure = async (tag) => {
|
||||
const panel = document.querySelector(".b05-route__panel, .b06-profile__form");
|
||||
if (!panel) return { tag, error: "패널 없음" };
|
||||
const body = panel.closest(".ui-workflow-overlay__body");
|
||||
const scroll = panel.querySelector(":scope > .ui-sidebar-scroll");
|
||||
const dock = panel.querySelector(":scope > .ui-sidebar-actions");
|
||||
const kids = [...panel.children].map((c) => c.classList[0]);
|
||||
const gap = () =>
|
||||
Math.round(body.getBoundingClientRect().bottom - dock.getBoundingClientRect().bottom);
|
||||
const res = {
|
||||
tag,
|
||||
view: panel.classList.contains("b06-profile__form") ? "B06" : "B05",
|
||||
split: body.classList.contains("ui-sidebar-split"),
|
||||
kids,
|
||||
loose: kids.filter((k) => k !== "ui-sidebar-scroll" && !dock?.classList.contains(k)).length,
|
||||
dockGap: dock ? gap() : null,
|
||||
};
|
||||
if (!scroll || !dock) return res;
|
||||
const top0 = Math.round(dock.getBoundingClientRect().top);
|
||||
const tall = document.createElement("div");
|
||||
tall.style.cssText = "height: 2000px; flex: none";
|
||||
scroll.append(tall);
|
||||
await w(100);
|
||||
res.long = {
|
||||
overflow: scroll.scrollHeight > scroll.clientHeight,
|
||||
dockGap: gap(),
|
||||
dockMoved: Math.round(dock.getBoundingClientRect().top) - top0,
|
||||
};
|
||||
tall.remove();
|
||||
res.ok =
|
||||
res.split &&
|
||||
res.loose === 0 &&
|
||||
Math.abs(res.dockGap) <= 1 &&
|
||||
res.long.overflow &&
|
||||
Math.abs(res.long.dockGap) <= 1 &&
|
||||
res.long.dockMoved === 0;
|
||||
return res;
|
||||
};
|
||||
const go = async (hash) => {
|
||||
location.hash = hash;
|
||||
for (let i = 0; i < 40; i++) {
|
||||
await w(250);
|
||||
const want = hash.includes("b06") ? ".b06-profile__form" : ".b05-route__panel";
|
||||
if (document.querySelector(want + " > .ui-sidebar-actions")) break;
|
||||
}
|
||||
await w(500);
|
||||
};
|
||||
const out = [];
|
||||
await go("#/b05-profile");
|
||||
out.push(await measure("B05"));
|
||||
await go("#/b06-section");
|
||||
out.push(await measure("B06"));
|
||||
await go("#/b05-profile");
|
||||
out.push(await measure("B05 다시"));
|
||||
if (location.hash !== start) await go(start);
|
||||
return JSON.stringify({ ok: out.every((r) => r.ok), out });
|
||||
})();
|
||||
@@ -0,0 +1,78 @@
|
||||
/* 44-7 — ① M02 일위대가 상세 머리 「단위」 = 표준 단위 드롭다운 ② 대시보드 「시스템 설정」 일위대가 카드 없음
|
||||
* 시작 화면: 아무 화면 · 시스템 관리자 로그인 · M02 마스터 층에 구조물 열 하나 이상
|
||||
* 부르는 법: orca eval --expression "$(cat 이 파일)" (async 식 · JSON 글을 돌려줌 · 20초 안)
|
||||
* 기대 수치:
|
||||
* - cards = ["마스터 데이터", "구조물 조합", "구조물 옵션", "도면 양식"] · hasLabor = false
|
||||
* - pick = 1(단추) · textInput = 0(손글 칸 없음) · options > 10 · hasNow = true(지금 값이 선택지에 있음)
|
||||
* - 다른 단위 고름 → after = 고른 값 · dirty = true(다른 열로 가려면 버림 물음 · 「취소」)
|
||||
* - 처음 값 다시 고름 → back = 처음 값 · dirtyBack = false(물음 없이 옮겨 감 · 저장 안 함 · 바꾼 값 되돌림)
|
||||
*/
|
||||
(async () => {
|
||||
const w = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
const until = async (fn, ms = 6000) => {
|
||||
for (let t = 0; t < ms; t += 150) {
|
||||
const v = fn();
|
||||
if (v) return v;
|
||||
await w(150);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
const out = {};
|
||||
// ② 대시보드 시스템 설정 카드
|
||||
location.hash = "#/b01-account";
|
||||
await until(() => document.querySelector(".b01-dashboard__setting-title"));
|
||||
out.cards = [...document.querySelectorAll(".b01-dashboard__setting-title")].map(
|
||||
(e) => e.textContent,
|
||||
);
|
||||
out.hasLabor = out.cards.includes("일위대가");
|
||||
// ① M02 일위대가 상세(좌측 일위대가 하위 첫 줄)
|
||||
location.hash = "#/m02-master-template?open=구조물조합&root=master";
|
||||
// 하위 줄은 다시 그려짐 — 누를 때마다 새로 찾음
|
||||
const row = () =>
|
||||
document
|
||||
.querySelector('[data-all="labor"]')
|
||||
?.parentElement?.querySelector(".m02-side__item:not([data-all])");
|
||||
if (!(await until(row))) return JSON.stringify({ ...out, error: "일위대가 하위 없음" });
|
||||
await w(500);
|
||||
row().click();
|
||||
const field = await until(() => document.querySelector('[data-labor-field="단위"]'));
|
||||
if (!field) return JSON.stringify({ ...out, error: "단위 칸 없음" });
|
||||
// 고침 표시 = 다른 열(하위 둘째 줄)로 갈 때 버림 물음이 뜨는지 — 뜨면 「취소」
|
||||
const asks = async () => {
|
||||
document
|
||||
.querySelector('[data-all="labor"]')
|
||||
.parentElement.querySelectorAll(".m02-side__item:not([data-all])")[1]
|
||||
.click();
|
||||
const ask = await until(() => document.querySelector(".ui-confirm"), 1500);
|
||||
[...(ask?.querySelectorAll("button") ?? [])].find((b) => b.textContent === "취소")?.click();
|
||||
await w(200);
|
||||
return !!ask;
|
||||
};
|
||||
const btn = () => document.querySelector('[data-labor-field="단위"] .m02-df-unitpick > button');
|
||||
const items = async () => {
|
||||
btn().click();
|
||||
return until(() => {
|
||||
const li = document.querySelectorAll('[data-labor-field="단위"] .m02-df-unitpick__item');
|
||||
return li.length ? [...li] : null;
|
||||
});
|
||||
};
|
||||
out.pick = document.querySelectorAll('[data-labor-field="단위"] .m02-df-unitpick').length;
|
||||
out.textInput = field.querySelectorAll("input[type=text]:not(.m02-df-unitpick__filter)").length;
|
||||
const u0 = btn().textContent;
|
||||
out.before = u0;
|
||||
const list = await items();
|
||||
out.options = list.length;
|
||||
out.hasNow = list.some((b) => b.classList.contains("is-now"));
|
||||
const other = list.find((b) => !b.classList.contains("is-now") && !b.textContent.startsWith("-"));
|
||||
other.click();
|
||||
await w(200);
|
||||
out.after = btn().textContent;
|
||||
out.dirty = await asks();
|
||||
// 되돌림 — 처음 값(없음이면 「-」)
|
||||
const again = await items();
|
||||
again.find((b) => (u0 === "-" ? b.textContent.startsWith("-") : b.textContent === u0)).click();
|
||||
await w(200);
|
||||
out.back = btn().textContent;
|
||||
out.dirtyBack = await asks();
|
||||
return JSON.stringify(out);
|
||||
})();
|
||||
Reference in New Issue
Block a user