Merge remote-tracking branch 'origin/dev' into main_laptop_1
This commit is contained in:
@@ -222,20 +222,20 @@ def check_saved(files: dict, changed: list[str], book: dict, narrowed=None) -> l
|
||||
|
||||
def _labor_files() -> list[tuple[str, Path]]:
|
||||
"""모든 층(마스터 · 회사 · 개인)의 일위대가 `labor/<열>.json` · 단가승격 `promote/<키>.json`
|
||||
— (어디, 파일)."""
|
||||
· 사전조합 `precombo/<키>.json`(51-11) — (어디, 파일)."""
|
||||
from M02_MasterTemplete import M02_Template_Layers as layers # M02 가 M01 을 부름 — 늦게
|
||||
|
||||
return [
|
||||
(f"{what} {where}", path)
|
||||
for kind, what in (("labor", "일위대가"), ("promote", "단가승격"))
|
||||
for kind, what in (("labor", "일위대가"), ("promote", "단가승격"), ("precombo", "사전조합"))
|
||||
for where, path in layers.layer_files(kind)
|
||||
]
|
||||
|
||||
|
||||
def labor_errors(whole) -> list[str]:
|
||||
"""일위대가 · 단가승격 문서가 담은 로직이 마스터에 없음(39-1d · 50-3) — 로직 지우기를 막음 ·
|
||||
어디서 쓰는지 알림 · 참조 줄(로직 없음)은 건너뜀. 전부터 깨진 것은 `save` 가 고치기 전 검사와
|
||||
견주어 뺌."""
|
||||
"""일위대가 · 단가승격 · 사전조합 문서가 담은 로직이 마스터에 없음(39-1d · 50-3) —
|
||||
로직 지우기를 막음 · 어디서 쓰는지 알림 · 참조 줄(로직 없음)은 건너뜀. 전부터 깨진 것은
|
||||
`save` 가 고치기 전 검사와 견주어 뺌."""
|
||||
out = []
|
||||
for where, path in _labor_files():
|
||||
try:
|
||||
|
||||
@@ -5,8 +5,9 @@
|
||||
`{종류:"단가승격", 키, 이름, 단위, 배, 본뜬, 담은로직:[{로직, 입력값}]}` —
|
||||
줄 하나 · 입력값은 승격 때 고정 · 값 = 로직 기준 ÷ 배(1 단위 값 · `배` 는 저장 때)
|
||||
precombo `{층}/precombo/SC-######.json` 사전조합
|
||||
`{종류:"사전조합", 키, 이름, 단위, 본뜬, 담은로직:[{참조, 수량:{손}, 메모}]}` —
|
||||
승격 · 사전조합만 · 수량은 손(기본 1) · 값 = Σ 안 줄 값 × 수량
|
||||
`{종류:"사전조합", 키, 이름, 단위, 본뜬, 담은로직:[{참조, 수량:{손}, 메모}
|
||||
| {로직, 입력값, 메모, 수량:{손}}]}` — 승격 · 사전조합 · 단가산출 로직(51-11) ·
|
||||
수량은 손(기본 1) · 값 = Σ 안 줄 1 단위 값 × 수량(로직 줄 = 기준 ÷ 배)
|
||||
참조 = `{층, 주인, 키}`(마스터 주인 null · 회사 `C` · 개인 `C/U`) — 종류는 키 머리(SP · SC) ·
|
||||
위 층만 가리킴(개인 → 그 개인 · 그 회사 · 마스터 / 회사 → 그 회사 · 마스터 / 마스터 → 마스터) ·
|
||||
같은 참조 두 번 · 순환(422)은 저장 때 막음 · 계산 때도 깊이 상한(`DEPTH`)에서 멈추고 까닭.
|
||||
@@ -162,27 +163,21 @@ def clean_doc(kind: str, doc: Any) -> dict[str, Any]:
|
||||
|
||||
|
||||
def _combo_lines(doc: dict, key: str) -> list[dict[str, Any]]:
|
||||
"""참조 줄 `{참조, 수량, 메모}` · 로직 줄 `{로직, 입력값, 메모, 수량}`(51-11) — 검사는
|
||||
일위대가와 같은 길(`labor.clean` · 같은 로직 · 같은 참조 두 번 · 입력값) · 수량 기본 {손: 1}."""
|
||||
from M02_MasterTemplete import M02_Labor_Doc as labor
|
||||
|
||||
at_, seen, lines = here(), set(), []
|
||||
for n, one in enumerate(doc.get("담은로직") or [], start=1):
|
||||
at = f"사전조합 {n}째"
|
||||
if not isinstance(one, dict) or one.get("로직") or one.get("참조") is None:
|
||||
raise _bad(f"{at} · 사전조합은 단가승격 · 사전조합만 담음")
|
||||
ref = check_ref(one["참조"], at, at_)
|
||||
if ref_id(ref) in seen:
|
||||
raise _bad(f"{at} · 같은 참조를 두 번 담음 「{ref['키']}」")
|
||||
seen.add(ref_id(ref))
|
||||
memo = one.get("메모")
|
||||
lines.append(
|
||||
{
|
||||
"참조": ref,
|
||||
"수량": labor._qty(one.get("수량"), at) or {"손": 1},
|
||||
"메모": str(memo) if memo else None,
|
||||
}
|
||||
)
|
||||
lines = []
|
||||
for one in labor.clean({"담은로직": doc.get("담은로직") or []})["담은로직"]:
|
||||
head = {"참조": one["참조"]} if "참조" in one else {"로직": one["로직"]}
|
||||
if "로직" in one:
|
||||
head["입력값"] = one["입력값"]
|
||||
lines.append({**head, "수량": one["수량"] or {"손": 1}, "메모": one["메모"]})
|
||||
at_ = here()
|
||||
me = {"층": at_[0], "주인": at_[1], "키": key}
|
||||
for one in lines:
|
||||
if "참조" not in one:
|
||||
continue
|
||||
if ref_id(one["참조"]) == ref_id(me):
|
||||
raise _bad(f"순환 — 자기 자신을 담음 「{key}」", 422)
|
||||
_no_loop(one["참조"], [me, one["참조"]])
|
||||
@@ -365,19 +360,44 @@ class Expand:
|
||||
],
|
||||
}
|
||||
|
||||
def _combo(self, doc: dict, trail: tuple[str, ...]) -> dict[str, Any]:
|
||||
def _logic(self, one: dict, pill: dict | None) -> dict[str, Any]:
|
||||
"""사전조합 안 로직 줄 — 일위대가 로직 줄과 같은 `run_line` · 배는 물량 알약 것."""
|
||||
from M02_MasterTemplete import M02_Labor_Doc as labor
|
||||
|
||||
pill = pill or {}
|
||||
got = labor.run_line(self.whole, one["로직"], one.get("입력값"))
|
||||
times = labor._number(pill.get("배")) or Decimal(1)
|
||||
base = got.get("기준")
|
||||
return {
|
||||
"이름": pill.get("이름") or one["로직"],
|
||||
"단위": pill.get("단위"),
|
||||
"기준": {k: Decimal(str(v)) / times for k, v in base.items()} if base else None,
|
||||
"까닭": got.get("까닭") or (None if base else "돈 아닌 로직"),
|
||||
}
|
||||
|
||||
def _combo(self, doc: dict, trail: tuple[str, ...]) -> dict[str, Any]:
|
||||
from M01_MasterData import M01_MasterData_Store_Combo as combo_store
|
||||
from M02_MasterTemplete import M02_Labor_Doc as labor
|
||||
|
||||
lines = [one for one in doc.get("담은로직") or [] if isinstance(one, dict)]
|
||||
by_logic = {
|
||||
code: pill
|
||||
for pill in combo_store.combo_pills({"담은로직": lines})["물량"]
|
||||
for code in pill["로직"]
|
||||
}
|
||||
inner, rows, why = [], [], None
|
||||
for one in doc.get("담은로직") or []:
|
||||
ref = one.get("참조") if isinstance(one, dict) else None
|
||||
if not isinstance(ref, dict):
|
||||
for one in lines:
|
||||
ref = one.get("참조")
|
||||
if isinstance(ref, dict):
|
||||
head, sub = {"참조": ref}, self(ref, trail)
|
||||
elif one.get("로직"): # 로직 줄(51-11) = 1 단위 값(기준 ÷ 배 · 승격과 같음)
|
||||
head, sub = {"로직": one["로직"]}, self._logic(one, by_logic.get(one["로직"]))
|
||||
else:
|
||||
continue
|
||||
sub = self(ref, trail)
|
||||
qty = labor._number((one.get("수량") or {}).get("손"))
|
||||
qty = Decimal(1) if qty is None else qty
|
||||
keep = ("이름", "단위", "기준", "까닭")
|
||||
inner.append({"참조": ref, "수량": qty, **{k: sub.get(k) for k in keep}})
|
||||
inner.append({**head, "수량": qty, **{k: sub.get(k) for k in keep}})
|
||||
if sub.get("기준") is None:
|
||||
why = why or f"「{sub.get('이름')}」 {sub.get('까닭')}"
|
||||
continue
|
||||
@@ -424,11 +444,10 @@ def frozen(doc: dict[str, Any]) -> dict[str, dict[str, Any]]:
|
||||
|
||||
|
||||
def frozen_logics(copies: dict[str, Any]) -> list[str]:
|
||||
"""굳힌 단가승격이 담은 로직 키 — 프로젝트 단가 묶음에 넣음."""
|
||||
"""굳힌 단가승격 · 사전조합이 담은 로직 키 — 프로젝트 단가 묶음에 넣음."""
|
||||
return [
|
||||
str(line["로직"])
|
||||
for rid, doc in copies.items()
|
||||
if kind_of(rid.rsplit("|", 1)[-1]) == "promote"
|
||||
for line in (doc.get("담은로직") or [])[:1]
|
||||
for doc in copies.values()
|
||||
for line in doc.get("담은로직") or []
|
||||
if isinstance(line, dict) and line.get("로직")
|
||||
]
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
* · 같은 로직 두 번 · 같은 자재 두 번 막음 · 빈 일위대가도 저장 됨
|
||||
* · 오른쪽 「원문」 접는 패널(47-4 · 상세만 · 공용 `ui_template_side_panels`) = 고른 줄 로직의 출처 원문
|
||||
* · 서고 모드(50-3 · `mode`) = 단가승격 · 사전조합 — 이름 칸 · 단가산출 로직 표 · 원문만(자재 · 면적 · 다이어그램 없음) ·
|
||||
* 승격은 로직 한 줄(입력값 고정 · 더하기 · 순서 · 수량 없음) · 사전조합 = [로직 추가](같은 고르기 창 · 서고 줄만) · 수량 손 입력(기본 1) · 순서 · 삭제
|
||||
* 승격은 로직 한 줄(입력값 고정 · 더하기 · 순서 · 수량 없음) · 사전조합 = 일위대가 상세와 같은 [로직 추가] · 표 · 동작(51-11) · 수량 손 입력(기본 1)
|
||||
* ========================================================================== */
|
||||
|
||||
import { createButton, createInputField, el, showToast } from "@ui/ui_template_elements";
|
||||
@@ -386,11 +386,13 @@ export function mountLaborDetail(
|
||||
(x) => {
|
||||
if (cur.담은로직.some((r) => r.로직 === x.키))
|
||||
return showToast(tc("Labor_Already").replace("{value}", `${x.키} ${x.이름}`), "error");
|
||||
cur.담은로직.push({ 로직: x.키, 별칭: null, 메모: null, 입력값: {}, 수량: null });
|
||||
// 사전조합 = 면적산출 없음 → 손 1(51-11)
|
||||
const 수량 = lib === "precombo" ? { 손: 1 } : null;
|
||||
cur.담은로직.push({ 로직: x.키, 별칭: null, 메모: null, 입력값: {}, 수량 });
|
||||
void loadLogics().then(() => changed());
|
||||
// 사전조합 = 같은 창 · 서고 줄만(위 층만 · 자기 자신 · 순환은 [저장] 때 서버 422)
|
||||
},
|
||||
lib ? { ...library, only: true } : library,
|
||||
// 사전조합도 같은 창 · 같은 목록(51-11 · 자기 자신 · 순환은 [저장] 때 서버 422)
|
||||
library,
|
||||
),
|
||||
});
|
||||
const addMat = createButton({
|
||||
@@ -476,7 +478,7 @@ export function mountLaborDetail(
|
||||
],
|
||||
}),
|
||||
]),
|
||||
// 승격 = 로직 한 줄(새로 만들 때 고름) · 사전조합 = 같은 [로직 추가] · 서고 줄만(51-10)
|
||||
// 승격 = 로직 한 줄(새로 만들 때 고름) · 사전조합 = 같은 [로직 추가](51-11)
|
||||
section(tc("Logics_Title"), opt.card || promote ? null : addLogic, logicHost),
|
||||
...(lib ? [] : [section(tc("Labor_Mats"), opt.card ? null : addMat, matHost)]),
|
||||
...(opt.card || lib ? [] : [dgBox]),
|
||||
|
||||
@@ -32,8 +32,6 @@ export interface LibraryPick {
|
||||
layer: Layer;
|
||||
owner: string | null;
|
||||
pick: (ref: LaborRef, name: string) => void;
|
||||
/** 서고 줄만(사전조합 상세 [추가] · 51-6) — 단가산출 로직 · 구분 거름 없음 */
|
||||
only?: boolean;
|
||||
}
|
||||
|
||||
/** 고를 수 있는 층 — 위 층만(마스터 → 마스터 · 회사 → 그 회사 · 마스터 · 개인 → 그 개인 · 그 회사 · 마스터) */
|
||||
@@ -72,7 +70,7 @@ export function openLogicPicker(done: (logic: LogicSummary) => void, lib?: Libra
|
||||
/* 공용 찾기 목록 — 구분 · 상세구분 2열 · 이름 왼쪽 맞춤 · ↑ ↓ Enter */
|
||||
const finder = createFindList({
|
||||
placeholder: tc("Pick_Search"),
|
||||
filters: lib?.only ? [] : [sub.root, detail.root],
|
||||
filters: [sub.root, detail.root],
|
||||
extra: [layerBar],
|
||||
});
|
||||
|
||||
@@ -200,7 +198,6 @@ export function openLogicPicker(done: (logic: LogicSummary) => void, lib?: Libra
|
||||
document.body.append(backdrop);
|
||||
finder.search.focus();
|
||||
|
||||
if (lib?.only) return;
|
||||
void Promise.all([fetchLogicSubs(), fetchLogics()])
|
||||
.then(([s, l]) => {
|
||||
subs.push(...s);
|
||||
|
||||
@@ -80,9 +80,9 @@ def test_일위대가_카드는_상세_카드_갈래():
|
||||
detail = _text("M02_MasterTemplete_Labor_Detail.ts")
|
||||
assert "...(opt.card ? {} : { move })" in detail # 순서 단추 없음 · 빼기는 그대로
|
||||
# 서고 모드(50-3 · lib)도 더하기 · 다이어그램 없음
|
||||
# 51-10 사전조합 = 같은 [로직 추가] · 서고 줄만
|
||||
# 51-11 사전조합 = 일위대가 상세와 같은 [로직 추가] · 같은 목록(걸러냄 없음)
|
||||
assert "opt.card || promote ? null : addLogic" in detail
|
||||
assert "lib ? { ...library, only: true } : library" in detail
|
||||
assert "only" not in _text("M02_MasterTemplete_Labor_Pick.ts")
|
||||
assert "...(opt.card || lib ? [] : [dgBox])," in detail
|
||||
assert "opt.card ? null : addMat" in detail # 49-6 — 카드엔 [자재 추가] 없음 · 상세는 그대로
|
||||
rows = _text("M02_MasterTemplete_Labor_Rows.ts")
|
||||
|
||||
@@ -95,7 +95,9 @@ def test_사전조합_틀린_줄_400_순환_422(world):
|
||||
sp = _promote(world["system"])
|
||||
one = {"참조": {**SYS, "키": sp}}
|
||||
for lines in (
|
||||
[{"로직": TEN}], # 로직은 못 담음(승격 · 사전조합만)
|
||||
[{"로직": "GF999999"}], # 모르는 로직
|
||||
[{"로직": TEN}, {"로직": TEN}], # 같은 로직 두 번(51-11)
|
||||
[{"로직": TEN, "입력값": [1]}], # 입력값 꼴
|
||||
[one, one], # 같은 참조 두 번
|
||||
[{"참조": {**SYS, "키": "SP-000009"}}], # 없는 참조
|
||||
[{"참조": {**SYS, "키": "SU-000001"}}], # 서고 키 아님
|
||||
@@ -342,6 +344,52 @@ def test_층_목록_서고_줄은_이름_단위_기준금액을_실음(world):
|
||||
assert "기준금액" not in rows[2]
|
||||
|
||||
|
||||
def test_사전조합_로직_줄_섞임_합은_손셈_지우기_409_굳힘(world):
|
||||
from M01_MasterData import M01_MasterData_Store_Cache as cache
|
||||
from M02_MasterTemplete import M02_Item_Prices as prices
|
||||
from M02_MasterTemplete import M02_Item_Source as source
|
||||
|
||||
sp = _promote(world["system"])
|
||||
lines = [
|
||||
{"참조": {**SYS, "키": sp}, "수량": {"손": 2}},
|
||||
{"로직": STONE, "입력값": {"없는칸": 1}, "수량": {"손": 3}, "메모": "돌"},
|
||||
{"로직": TEN},
|
||||
]
|
||||
sc = _combo(world["system"], lines, "섞음")
|
||||
saved = _read(world["system"], "precombo", sc)["담은로직"]
|
||||
assert saved[1:] == [
|
||||
{"로직": STONE, "입력값": {}, "수량": {"손": 3}, "메모": "돌"},
|
||||
{"로직": TEN, "입력값": {}, "수량": {"손": 1}, "메모": None},
|
||||
]
|
||||
ten, stone = (Decimal(str(_base(c)["계"])) for c in (TEN, STONE))
|
||||
want = ten / 10 * 2 + stone * 3 + ten / 10 * 1 # 손셈 — 승격 · TEN 은 원/10㎡(배 10)
|
||||
ref = {**SYS, "키": sc}
|
||||
with store.at(world["system"]):
|
||||
doc = labor.clean({"열": "rv", "담은로직": [{"참조": ref, "수량": {"손": 5}}]})
|
||||
one = labor.calc(doc, {})["줄"][0]
|
||||
assert one["기준"]["계"] == pytest.approx(float(want))
|
||||
assert one["반영"]["계"] == pytest.approx(float(want * 5))
|
||||
assert [x.get("로직") or x["참조"]["키"] for x in one["안"]] == [sp, STONE, TEN]
|
||||
with store.at(world["system"]), pytest.raises(store.StoreError) as e:
|
||||
store.delete("promote", sp)
|
||||
assert e.value.status == 409 and "사전조합 마스터 「섞음」" in e.value.detail
|
||||
|
||||
class Gone: # STONE 을 지운 마스터
|
||||
def logic(self, code):
|
||||
if code == STONE:
|
||||
raise cache.cm.mf.FormulaError("없음")
|
||||
|
||||
got = cache.labor_errors(Gone())
|
||||
assert f"사전조합 마스터 「{sc}」 이 로직 「{STONE}」 을 씀 — 거기서 먼저 뺌" in got
|
||||
rows = source.labor_doc_rows(doc)
|
||||
assert sorted(refs.frozen_logics(rows["참조사본"])) == sorted([TEN, STONE, TEN])
|
||||
bundle = prices.to_json(prices.collect([TEN, STONE]))
|
||||
for key in (sc, sp): # 원본이 사라져도 굳힌 사본으로 같은 값
|
||||
(world["system"] / refs.kind_of(key) / f"{key}.json").unlink()
|
||||
got = prices.calc(rows, bundle, {}, {}, None)["줄"][0]
|
||||
assert got["계"] == pytest.approx(float(want), rel=1e-9)
|
||||
|
||||
|
||||
def test_사전조합이_자기_자신을_담으면_422_수량_기본_1(world):
|
||||
sp = _promote(world["system"])
|
||||
a = _combo(world["system"], [{"참조": {**SYS, "키": sp}}], "가")
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
/* 51-11 — 서고 사전조합 상세 [로직 추가] = 일위대가 상세와 같은 고르기 창 · 같은 표 · 같은 동작
|
||||
* 시작 화면: M02 마스터 템플릿 · 마스터 층(#/m02-master-template?open=일위대가조합&root=master) · 시스템 관리자
|
||||
* 부르는 법: orca eval --expression "$(cat 이 파일)" (async 식 · JSON 글을 돌려줌)
|
||||
* 쓰기 없음 — 서고 목록 · 문서 읽기 · [저장] PUT 은 가짜 응답(fetch 바꿔 끼움 · 끝에 되돌림) · calc 만 서버
|
||||
* 기대 수치:
|
||||
* - picker = { filters: 2, kinds: ["단가산출", "단가승격", "일위조합"] }(걸러냄 없음 · 일위대가 상세와 같음)
|
||||
* - added = 2 · logic = { option: true, modal: true, qty: "1", sheet: false, sub: ≥1 }
|
||||
* - sum = 줄 계 합 == 합계 줄 계 · moved = ["GC000421", "SP-000001"](참조 = 마스터 실제 SP-000001 · calc 가 읽음)
|
||||
* - sent = { 로직: "GC000421", 수량: { 손: 1 } } · removed = 1 · left = 버림 확인 뒤 목록
|
||||
*/
|
||||
(async () => {
|
||||
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
const until = async (fn, ms = 6000) => {
|
||||
for (let t = 0; t < ms; t += 100) {
|
||||
const v = fn();
|
||||
if (v) return v;
|
||||
await wait(100);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
const SYS = { 층: "system", 주인: null };
|
||||
const LOGIC = "GC000421";
|
||||
const rows = [
|
||||
{
|
||||
종류: "precombo",
|
||||
이름: "SC-999901",
|
||||
판: "c",
|
||||
수정일: "2026-10-05T12:00:00",
|
||||
제목: "시험 묶음",
|
||||
단위: "개소",
|
||||
기준금액: 100,
|
||||
},
|
||||
];
|
||||
const docs = {
|
||||
"precombo/SC-999901": {
|
||||
종류: "사전조합",
|
||||
키: "SC-999901",
|
||||
이름: "시험 묶음",
|
||||
단위: "개소",
|
||||
담은로직: [{ 참조: { ...SYS, 키: "SP-000001" }, 수량: { 손: 1 } }],
|
||||
},
|
||||
};
|
||||
const json = (body, status = 200) =>
|
||||
new Response(JSON.stringify(body), { status, headers: { "Content-Type": "application/json" } });
|
||||
const real = window.fetch;
|
||||
let putBody = null;
|
||||
window.fetch = async (input, init = {}) => {
|
||||
const url = String(input?.url ?? input);
|
||||
const method = (init.method ?? "GET").toUpperCase();
|
||||
if (method === "GET" && /\/m02\/layers\/system\/templates(\?|$)/.test(url)) {
|
||||
const got = await (await real(input, init)).json();
|
||||
return json({ ...got, 양식: [...got.양식.filter((r) => r.이름 !== "SC-999901"), ...rows] });
|
||||
}
|
||||
const hit = Object.keys(docs).find((k) => url.includes(`/templates/${k}`));
|
||||
if (hit && method === "GET" && !url.includes("/calc"))
|
||||
return json({ 종류: hit.split("/")[0], 이름: docs[hit].키, 판: "a", 문서: docs[hit] });
|
||||
if (hit && method === "PUT") {
|
||||
putBody = JSON.parse(init.body);
|
||||
return json({ 이름: "SC-999901", 판: "z" });
|
||||
}
|
||||
if (method !== "GET" && !url.includes("/calc"))
|
||||
return json({ detail: "시험 — 쓰기 막음" }, 400);
|
||||
return real(input, init);
|
||||
};
|
||||
const sec = [...document.querySelectorAll(".m02-side__lists > section")].find(
|
||||
(s) => s.querySelector(".ui-collapsible__title")?.textContent?.trim() === "일위대가 조합",
|
||||
);
|
||||
const items = [...sec.querySelectorAll("[data-precombo]")];
|
||||
const btn = (root, text) =>
|
||||
[...(root?.querySelectorAll("button") ?? [])].find((b) => b.textContent === text);
|
||||
const edit = () => document.querySelector(".m02-main__host [data-labor]");
|
||||
const lines = () => [...(edit()?.querySelectorAll("[data-labor-logic]") ?? [])];
|
||||
const keys = () => lines().map((tr) => tr.querySelector(".m02-labrow__key")?.textContent);
|
||||
const num = (s) => Number(String(s ?? "").replace(/[^\d.-]/g, "")) || 0;
|
||||
const applied = (tr) =>
|
||||
[...tr.querySelectorAll(".m02-labrow__money .m02-labrow__applied")].pop()?.textContent;
|
||||
const calcDone = () =>
|
||||
until(() => {
|
||||
const t = edit()?.querySelector(".m02-labrow__total .m02-labrow__applied")?.textContent;
|
||||
return t && !t.includes("…") ? t : null;
|
||||
});
|
||||
try {
|
||||
items[1].click();
|
||||
(
|
||||
await until(() => document.querySelector('[data-lib-key="SC-999901"] td:nth-child(3)'))
|
||||
).click();
|
||||
await until(() => lines().length);
|
||||
await wait(300);
|
||||
// 고르기 창 — 일위대가 상세와 같음(구분 · 상세구분 거름 · 구분 셋)
|
||||
btn(edit(), "로직 추가").click();
|
||||
const pick = await until(() => document.querySelector(".m01-logic__pick"));
|
||||
await until(() => {
|
||||
const k = new Set([...pick.querySelectorAll("[data-pick-kind]")].map((x) => x.textContent));
|
||||
return k.size >= 3;
|
||||
}, 20000);
|
||||
const picker = {
|
||||
filters: pick.querySelectorAll(".ui-find__filters select").length,
|
||||
kinds: [...new Set([...pick.querySelectorAll("[data-pick-kind]")].map((x) => x.textContent))],
|
||||
};
|
||||
const search = pick.querySelector("input");
|
||||
search.value = LOGIC;
|
||||
search.dispatchEvent(new Event("input"));
|
||||
(await until(() => pick.querySelector(`[data-pick="${LOGIC}"]`))).click();
|
||||
await until(() => lines().length === 2);
|
||||
const added = lines().length;
|
||||
const row = () => lines().find((tr) => tr.dataset.laborLogic === LOGIC);
|
||||
// [옵션] → 넣을 값 모달
|
||||
const option = btn(row(), "옵션");
|
||||
option?.click();
|
||||
const modal = await until(() => document.querySelector(".m02-labin"));
|
||||
btn(modal, "닫기")?.click();
|
||||
await wait(200);
|
||||
// 수량 — 손 입력 기본 1 · 시트 없음
|
||||
row().querySelector("[data-labor-qty-open]").click();
|
||||
const hand = await until(() => document.querySelector('.m02-labqty [data-labor-qty="hand"]'));
|
||||
const logic = {
|
||||
option: !!option,
|
||||
modal: !!modal,
|
||||
qty: hand?.value,
|
||||
sheet: !!document.querySelector(".m02-labqty [data-labor-qty-sheet]"),
|
||||
};
|
||||
btn(document.querySelector(".m02-labqty"), "닫기")?.click();
|
||||
await wait(200);
|
||||
// 합 — 줄 계(반영) 합 == 합계 줄
|
||||
const total = await calcDone();
|
||||
const sum = {
|
||||
lines: lines().reduce((a, tr) => a + num(applied(tr)), 0),
|
||||
total: num(total),
|
||||
};
|
||||
// 줄 누름 → 하위 행
|
||||
row().querySelector(".m02-labrow__name").click();
|
||||
await until(() => edit().querySelector("[data-labor-sub]"));
|
||||
await until(() => !edit().querySelector("[data-labor-sub]")?.textContent.includes("…"));
|
||||
logic.sub = edit().querySelectorAll("[data-labor-sub]").length;
|
||||
// 위로
|
||||
btn(row(), "위로").click();
|
||||
await wait(300);
|
||||
const moved = keys();
|
||||
// [저장] 본문(가짜 PUT)
|
||||
const save = btn(document.querySelector(".m02-main__head"), "저장");
|
||||
save.click();
|
||||
await until(() => putBody);
|
||||
const s = putBody?.문서?.담은로직?.find((x) => x.로직 === LOGIC);
|
||||
const sent = s ? { 로직: s.로직, 수량: s.수량 } : null;
|
||||
// 삭제 — 로직 줄 뺌
|
||||
btn(row(), "삭제").click();
|
||||
await wait(300);
|
||||
const removed = lines().length;
|
||||
// 떠남 — 버림 확인 → 목록(되돌림)
|
||||
items[1].click();
|
||||
const ok = await until(
|
||||
() =>
|
||||
document.querySelector(
|
||||
".ui-confirm__actions .ui-button--danger, .ui-confirm__actions button:last-child",
|
||||
),
|
||||
1500,
|
||||
);
|
||||
ok?.click();
|
||||
const left = !!(await until(() => document.querySelector('[data-lib-list="precombo"]')));
|
||||
return JSON.stringify({ picker, added, logic, sum, moved, sent, removed, left });
|
||||
} finally {
|
||||
window.fetch = real;
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user