From 31874ea3a69070e7cce96036eb593034d9ea6298 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 5 Oct 2026 20:29:39 +0900 Subject: [PATCH 1/3] =?UTF-8?q?=EF=BB=BFfeat(m02):=20=EC=82=AC=EC=A0=84?= =?UTF-8?q?=EC=A1=B0=ED=95=A9=20=EB=8B=B4=EC=9D=80=EB=A1=9C=EC=A7=81?= =?UTF-8?q?=EC=97=90=20=EB=8B=A8=EA=B0=80=EC=82=B0=EC=B6=9C=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=A4=84=20=ED=97=88=EC=9A=A9=20=E2=80=94=2051-11?= =?UTF-8?q?=20=EC=84=9C=EB=B2=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 줄 = {참조, 수량, 메모} | {로직, 입력값, 메모, 수량} · 수량 기본 {손: 1} - 검사 = 일위대가 labor.clean 같은 길(모르는 로직 · 같은 로직 · 같은 참조 두 번 · 입력값 꼴) - 펼침 계산 = 로직 줄 run_line 기준 ÷ 배 × 수량 · 안 줄에 로직 줄 - 로직 지우기 검사에 사전조합 포함 · 굳힘 로직 목록에 사전조합 로직 포함 - 시험 test_m02_labor_ref 22 통과(섞인 사전조합 합 = 손셈 · 지우기 409) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EYJJRCfHfdCGNhmXTjQKV7 --- M01_MasterData/M01_MasterData_Store_Cache.py | 10 +-- M02_MasterTemplete/M02_Labor_Ref.py | 77 ++++++++++++-------- resources/tester/test_m02_labor_ref.py | 50 ++++++++++++- 3 files changed, 102 insertions(+), 35 deletions(-) diff --git a/M01_MasterData/M01_MasterData_Store_Cache.py b/M01_MasterData/M01_MasterData_Store_Cache.py index bd98cddc8..ac1be8ad8 100644 --- a/M01_MasterData/M01_MasterData_Store_Cache.py +++ b/M01_MasterData/M01_MasterData_Store_Cache.py @@ -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: diff --git a/M02_MasterTemplete/M02_Labor_Ref.py b/M02_MasterTemplete/M02_Labor_Ref.py index 5c5715cbd..2479ef3d8 100644 --- a/M02_MasterTemplete/M02_Labor_Ref.py +++ b/M02_MasterTemplete/M02_Labor_Ref.py @@ -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("로직") ] diff --git a/resources/tester/test_m02_labor_ref.py b/resources/tester/test_m02_labor_ref.py index d2951479c..56f074b61 100644 --- a/resources/tester/test_m02_labor_ref.py +++ b/resources/tester/test_m02_labor_ref.py @@ -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}}], "가") From bec4258bd304b0e6c22888a2007672c3cb433de4 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 5 Oct 2026 20:31:11 +0900 Subject: [PATCH 2/3] =?UTF-8?q?feat(M02):=20=EC=82=AC=EC=A0=84=EC=A1=B0?= =?UTF-8?q?=ED=95=A9=20[=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80]=20=3D=20?= =?UTF-8?q?=EC=9D=BC=EC=9C=84=EB=8C=80=EA=B0=80=20=EC=83=81=EC=84=B8?= =?UTF-8?q?=EC=99=80=20=EA=B0=99=EC=9D=80=20=EA=B3=A0=EB=A5=B4=EA=B8=B0=20?= =?UTF-8?q?=EC=B0=BD=20=C2=B7=20=EA=B0=99=EC=9D=80=20=EC=A4=84=20=EB=8F=99?= =?UTF-8?q?=EC=9E=91(51-11=20=ED=99=94=EB=A9=B4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 고르기 창 서고 줄만 거름(only) 없앰 — 단가산출 · 단가승격 · 일위조합 모두 · 구분 · 상세구분 거름 - 사전조합 단가산출 로직 줄 = [옵션] · 수량(손 · 기본 1) · 위로 · 아래로 · 삭제 · 하위 행 일위대가 상세와 같음 - 시험 기대 글 고침(card_shell) · 화면 시나리오 51-11 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016PSXsvyDxjNmufteN5bqmt --- .../M02_MasterTemplete_Labor_Detail.ts | 12 +- .../M02_MasterTemplete_Labor_Pick.ts | 5 +- resources/tester/test_m02_card_shell.py | 4 +- .../tester/ui_checks/51-11_사전조합_고르기.js | 164 ++++++++++++++++++ 4 files changed, 174 insertions(+), 11 deletions(-) create mode 100644 resources/tester/ui_checks/51-11_사전조합_고르기.js diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts index 441a2a223..d0bbc892b 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts @@ -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]), diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Pick.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Pick.ts index 44756fbb0..445b48d45 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Pick.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Pick.ts @@ -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); diff --git a/resources/tester/test_m02_card_shell.py b/resources/tester/test_m02_card_shell.py index 178871a91..8882439d8 100644 --- a/resources/tester/test_m02_card_shell.py +++ b/resources/tester/test_m02_card_shell.py @@ -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") diff --git a/resources/tester/ui_checks/51-11_사전조합_고르기.js b/resources/tester/ui_checks/51-11_사전조합_고르기.js new file mode 100644 index 000000000..490b84dc0 --- /dev/null +++ b/resources/tester/ui_checks/51-11_사전조합_고르기.js @@ -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; + } +})(); From b3cf5559a27cf5bfc43630fa96c1e3c30830e178 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 5 Oct 2026 20:33:40 +0900 Subject: [PATCH 3/3] =?UTF-8?q?chore(plan):=2010-5=20=EC=82=AC=EC=A0=84?= =?UTF-8?q?=EC=A1=B0=ED=95=A9=20=EA=B3=A0=EB=A5=B4=EA=B8=B0=20=EC=A0=95?= =?UTF-8?q?=EC=A0=95=20=EC=9B=90=EB=AC=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FJqWQpoB1RE7hiaQ2wH8qV --- docs/raw/사용자요구/2026-10-05_수정요구.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/raw/사용자요구/2026-10-05_수정요구.md b/docs/raw/사용자요구/2026-10-05_수정요구.md index 1499240a3..14f1cf7f7 100644 --- a/docs/raw/사용자요구/2026-10-05_수정요구.md +++ b/docs/raw/사용자요구/2026-10-05_수정요구.md @@ -110,3 +110,5 @@ 그리고 로직 고르기 모달창에서 구분과 상세 구분은 2열 구조로 변경하고 찾을때 나온 항목들의 이름들은 좌측 맞춤으로 글자를 맞춰야지. 찾기의 공용화 양식이 없는거야?템플릿 있을것같은데? 단가산출과 일위대가 사전조합에서 테이블의 양식도 M01에 공통테이블 양식이 있을것같고 버튼들도 M01에 템플릿이 있을거야. 확인해봐 그리고 사전조합의 경우에 검색은 별도의 창이 아니라 구조물 조합의 일위대가 테이블의 조건 추가와 같은 기능이어야. + +일위대가 사전조합에서 나오는 모당창의 형식 뿐만아니라 내용도 구조물조합의 일위대가의 상세 화면에서 로직 추가와 같아야된다고 선택되는 항목도