From 28e535f4905e998b79db53dd135ce32d2171b851 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 5 Oct 2026 20:47:49 +0900 Subject: [PATCH 1/5] =?UTF-8?q?feat(M02):=20=EB=A1=9C=EC=A7=81=20=EA=B3=A0?= =?UTF-8?q?=EB=A5=B4=EA=B8=B0=20=EC=B0=BD=20=EA=B5=AC=EB=B6=84=20=C2=B7=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=EA=B5=AC=EB=B6=84=20=EA=B1=B0=EB=A6=84?= =?UTF-8?q?=EC=97=90=20=EB=8B=A8=EA=B0=80=EC=8A=B9=EA=B2=A9=20=C2=B7=20?= =?UTF-8?q?=EC=82=AC=EC=A0=84=EC=A1=B0=ED=95=A9=20=EC=A4=84=20=ED=8F=AC?= =?UTF-8?q?=ED=95=A8=20(51-12=20=ED=99=94=EB=A9=B4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 구분 · 상세구분 고르기 칸 = 로직 구분 + 서고 줄(승격 · 조합) 구분 · 같은 값은 한 칸 - 구분을 고르면 서고 줄도 같은 값으로 걸러짐(이전 = 서고 줄 숨김) - 서고 줄 보조 글에 구분 · 상세구분 · 사전조합 상세 [추가] 창도 같은 창 - 확인 시나리오 resources/tester/ui_checks/51-12_고르기_구분.js Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01W4uKaV4ZBhsJN4GHG7yRj4 --- .../M02_MasterTemplete_Labor_Api.ts | 3 + .../M02_MasterTemplete_Labor_Pick.ts | 38 ++++++-- .../tester/ui_checks/51-12_고르기_구분.js | 97 +++++++++++++++++++ 3 files changed, 130 insertions(+), 8 deletions(-) create mode 100644 resources/tester/ui_checks/51-12_고르기_구분.js diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Api.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Api.ts index ebf4db18c..c896cb70a 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Api.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Api.ts @@ -233,6 +233,9 @@ export interface LibraryRow { 이름: string; 제목?: string | null; 단위?: string | null; + /** 단가승격 = 원 로직 것 · 사전조합 = 사용자가 고름(51-12) */ + 구분?: string | null; + 상세구분?: string | null; } /** 서고 목록 — 그 층(주인) 단가승격 `promote` · 사전조합 `precombo` 문서 */ export const fetchLibrary = async ( diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Pick.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Pick.ts index 445b48d45..02331349f 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Pick.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Pick.ts @@ -82,9 +82,12 @@ export function openLogicPicker(done: (logic: LogicSummary) => void, lib?: Libra (!detail.select.value || x.상세구분 === detail.select.value) && (!q || `${x.키} ${x.이름}`.toLowerCase().includes(q)), ); - const libFound = sub.select.value - ? [] - : libRows.filter((x) => !q || `${x.row.이름} ${x.row.제목 ?? ""}`.toLowerCase().includes(q)); + const libFound = libRows.filter( + (x) => + (!sub.select.value || x.row.구분 === sub.select.value) && + (!detail.select.value || x.row.상세구분 === detail.select.value) && + (!q || `${x.row.이름} ${x.row.제목 ?? ""}`.toLowerCase().includes(q)), + ); const total = all.length + libRows.length; const shown = found.length + libFound.length; finder.hint.textContent = loading @@ -110,7 +113,7 @@ export function openLogicPicker(done: (logic: LogicSummary) => void, lib?: Libra key: x.row.이름, tag: tc(x.kind), name: x.row.제목 || x.row.이름, - sub: x.row.이름, + sub: `${x.row.이름} · ${x.row.구분 ?? ""} ${x.row.상세구분 ?? ""}`.trim(), unit: x.row.단위 ?? "", onPick: () => { close(); @@ -142,6 +145,7 @@ export function openLogicPicker(done: (logic: LogicSummary) => void, lib?: Libra showToast(error instanceof Error ? error.message : tc("Load_Failed"), "error"); } loading = false; + drawSubs(); find(); }; const drawLayers = (): void => @@ -158,10 +162,28 @@ export function openLogicPicker(done: (logic: LogicSummary) => void, lib?: Libra }), ), ); - sub.select.addEventListener("change", () => { - const d = subs.find((s) => s.name === sub.select.value)?.details ?? []; - detail.setOptions(opts(d), ""); + /** 구분 → 상세구분 — 로직 구분에 서고 줄(승격 · 조합) 것을 더함 · 같은 값은 한 칸 */ + const subMap = (): Map> => { + const m = new Map(subs.map((s) => [s.name, new Set(s.details)])); + for (const { row } of libRows) { + if (!row.구분) continue; + if (!m.has(row.구분)) m.set(row.구분, new Set()); + if (row.상세구분) m.get(row.구분)!.add(row.상세구분); + } + return m; + }; + const drawDetails = (keep: string): void => { + const d = [...(subMap().get(sub.select.value) ?? [])]; + detail.setOptions(opts(d), d.includes(keep) ? keep : ""); detail.select.disabled = d.length === 0; + }; + const drawSubs = (): void => { + const keep = detail.select.value; + sub.setOptions(opts([...subMap().keys()]), sub.select.value); + drawDetails(keep); + }; + sub.select.addEventListener("change", () => { + drawDetails(""); find(); }); detail.select.addEventListener("change", find); @@ -202,8 +224,8 @@ export function openLogicPicker(done: (logic: LogicSummary) => void, lib?: Libra .then(([s, l]) => { subs.push(...s); all = l; + drawSubs(); find(); - sub.setOptions(opts(s.map((x) => x.name)), ""); }) .catch((error) => showToast(error instanceof Error ? error.message : tc("Load_Failed"), "error"), diff --git a/resources/tester/ui_checks/51-12_고르기_구분.js b/resources/tester/ui_checks/51-12_고르기_구분.js new file mode 100644 index 000000000..9361d5031 --- /dev/null +++ b/resources/tester/ui_checks/51-12_고르기_구분.js @@ -0,0 +1,97 @@ +/* 51-12 — 로직 고르기 창 구분 · 상세구분 거름에 단가승격 · 사전조합 줄도 들어감 + * 시작 화면: M02 마스터 템플릿 · 마스터 층(#/m02-master-template?open=구조물조합&root=master) · 시스템 관리자 · item1 있음 + * 부르는 법: orca eval --expression "$(tr ' +' ' ' < 이 파일)" (async 식 · JSON 글을 돌려줌) + * 하는 일: item1 → 일위대가 [수정] → [로직 추가] → 서고 줄마다 구분 · 상세구분으로 걸러 봄 → 창 닫음(저장 안 함) + * 기대 수치: + * - libRows ≥ 2(SP-000001 · SC-000001 포함) + * - lib[키].sub = 한 구분(빈 값 아님) · lib[키].detail = 한 상세구분 · lib[키].subText 에 둘이 다 보임 + * - lib[키].withLogic = true(같은 구분 칸에 단가산출 줄도 함께 — 같은 값은 한 칸) + * - subDup = 0(구분 칸 겹침 없음) + */ +(async () => { + const w = (ms) => new Promise((r) => setTimeout(r, ms)); + const until = async (fn, ms = 8000) => { + for (let t = 0; t < ms; t += 150) { + const v = fn(); + if (v) return v; + await w(150); + } + return null; + }; + const out = {}; + const node = await until(() => document.querySelector('[data-node="i|item1"]')); + if (!node) return JSON.stringify({ error: "item1 없음" }); + node.click(); + const card = await until(() => + [...document.querySelectorAll(".m02-struct__card")].find((c) => + c.querySelector("table.m02-labrow tbody tr[data-labor-logic]"), + ), + ); + if (!card) return JSON.stringify({ error: "일위대가 카드 없음" }); + [...card.querySelectorAll(".m02-struct__card-tools button")] + .find((b) => b.innerText.trim() === "수정") + ?.click(); + const table = () => + [...document.querySelectorAll("table.m02-labrow")].find( + (t) => !t.closest(".m02-struct__card") && t.querySelector("tr[data-labor-logic]"), + ); + await until(() => table() && !/…/.test(table().querySelector("tbody").innerText), 12000); + const host = table().closest("[data-labor]"); + [...host.querySelectorAll(".m01-logic__section-head button")] + .find((b) => b.innerText.trim() === "로직 추가") + .click(); + const dlg = await until(() => document.querySelector(".m01-logic__pick")); + const rows = () => [...dlg.querySelectorAll(".ui-find__row")]; + const isLib = (r) => r.querySelector(".ui-find__tag")?.innerText.trim() !== "단가산출"; + await until(() => rows().some(isLib) && rows().some((r) => !isLib(r)), 20000); + await w(500); + const [subSel, detailSel] = dlg.querySelectorAll(".ui-find__filters select"); + const set = async (sel, v) => { + sel.value = v; + sel.dispatchEvent(new Event("change", { bubbles: true })); + await w(120); + }; + const libKeys = rows() + .filter(isLib) + .map((r) => r.dataset.pick); + out.libRows = libKeys.length; + const subVals = [...subSel.options].map((o) => o.value).filter(Boolean); + out.subDup = subVals.length - new Set(subVals).size; + const lib = {}; + for (const k of libKeys) + lib[k] = { + subText: rows() + .find((r) => r.dataset.pick === k) + ?.querySelector(".ui-find__sub")?.innerText, + sub: [], + detail: [], + withLogic: false, + }; + for (const s of subVals) { + await set(subSel, s); + const shown = rows(); + const hasLogic = shown.some((r) => !isLib(r)); + for (const r of shown.filter(isLib)) { + if (!lib[r.dataset.pick]) continue; + lib[r.dataset.pick].sub.push(s); + lib[r.dataset.pick].withLogic ||= hasLogic; + } + for (const d of [...detailSel.options].map((o) => o.value).filter(Boolean)) { + await set(detailSel, d); + for (const r of rows().filter(isLib)) lib[r.dataset.pick]?.detail.push(d); + } + await set(detailSel, ""); + } + await set(subSel, ""); + out.lib = lib; + [...dlg.querySelectorAll("button")].find((b) => b.innerText.trim() === "닫기")?.click(); + out.closed = !document.querySelector(".m01-logic__pick"); + document.querySelector('[data-node="i|item1"]')?.click(); + await w(800); + const ask = [...document.querySelectorAll(".ui-modal button, dialog button")].find((x) => + /확인|버림|떠나기/.test(x.innerText), + ); + ask?.click(); + return JSON.stringify(out); +})(); From 0b1133394229d50602dc315c88b9998b062f751e Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 5 Oct 2026 20:48:54 +0900 Subject: [PATCH 2/5] =?UTF-8?q?feat(M02):=20=EB=8B=A8=EA=B0=80=EC=8A=B9?= =?UTF-8?q?=EA=B2=A9=20=C2=B7=20=EC=82=AC=EC=A0=84=EC=A1=B0=ED=95=A9=20?= =?UTF-8?q?=EB=AC=B8=EC=84=9C=20=EA=B5=AC=EB=B6=84=20=C2=B7=20=EC=83=81?= =?UTF-8?q?=EC=84=B8=EA=B5=AC=EB=B6=84=20=C2=B7=20=EC=97=B0=EA=B2=B0=20?= =?UTF-8?q?=EC=B9=B8=20=E2=80=94=2051-12=20=EC=84=9C=EB=B2=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 단가승격 = 저장 때 원 로직의 구분 · 상세구분 · 연결 {로직 키, 출처 글} 적음(화면 값 안 받음) - 사전조합 = 화면이 고른 구분 · 상세구분 저장(빈 글 허용 · 처음 값은 화면 몫) - 서고 목록 길 · 고르기 길(층 양식 목록) 줄에 구분 · 상세구분 · 연결 실음 · 양식 읽기 길도 - 옛 문서(칸 없음) = 읽을 때 채움 — 승격은 원 로직 것 · 사전조합은 빈 글 - 시험 test_m02_labor_ref 23 통과 · 층 읽기 시험 9 통과 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EYJJRCfHfdCGNhmXTjQKV7 --- M02_MasterTemplete/M02_Labor_Ref.py | 36 +++++++++++++++++-- .../M02_MasterTemplete_Router_Layers.py | 1 + resources/tester/test_m02_labor_ref.py | 27 ++++++++++++++ 3 files changed, 61 insertions(+), 3 deletions(-) diff --git a/M02_MasterTemplete/M02_Labor_Ref.py b/M02_MasterTemplete/M02_Labor_Ref.py index 2479ef3d8..a1c43145e 100644 --- a/M02_MasterTemplete/M02_Labor_Ref.py +++ b/M02_MasterTemplete/M02_Labor_Ref.py @@ -153,15 +153,43 @@ def clean_doc(kind: str, doc: Any) -> dict[str, Any]: 단위=unit or pill.get("단위") or None, 배=pill.get("배") or 1, # ponytail: 저장 때 적음 · 마스터 결과단위가 바뀌면 다시 저장 담은로직=[{"로직": line["로직"], "입력값": line["입력값"]}], + **origin(line["로직"]), ) else: if not name: raise _bad("사전조합 이름이 빔") out.update(이름=name, 단위=unit or None, 담은로직=_combo_lines(doc, key)) + out.update({k: str(doc.get(k) or "").strip() for k in ("구분", "상세구분")}) out["본뜬"] = doc["본뜬"] if isinstance(doc.get("본뜬"), dict) else None return out +def origin(code: str) -> dict[str, Any]: + """단가승격 칸 — 원 로직의 구분 · 상세구분 + 연결 {로직 키, 출처 글}(51-12 · 없으면 빈 글).""" + from M01_MasterData import M01_MasterData_Store as m01 + + try: + got = m01.loaded()[1].logic(code) + except m01.cm.mf.FormulaError: + got = {} + return { + "구분": got.get("구분") or "", + "상세구분": got.get("상세구분") or "", + "연결": {"로직": code, "출처": got.get("출처") or ""}, + } + + +def filled(kind: str, doc: Any) -> Any: + """옛 서고 문서(51-12 앞) 읽을 때 칸 채움 — 승격 = 원 로직 것 · 사전조합 = 빈 글(화면 몫).""" + if not isinstance(doc, dict) or kind not in PREFIX or "구분" in doc: + return doc + if kind == "precombo": + return {**doc, "구분": "", "상세구분": ""} + line = (doc.get("담은로직") or [{}])[0] + code = str(line.get("로직") or "") if isinstance(line, dict) else "" + return {**doc, **origin(code)} + + def _combo_lines(doc: dict, key: str) -> list[dict[str, Any]]: """참조 줄 `{참조, 수량, 메모}` · 로직 줄 `{로직, 입력값, 메모, 수량}`(51-11) — 검사는 일위대가와 같은 길(`labor.clean` · 같은 로직 · 같은 참조 두 번 · 입력값) · 수량 기본 {손: 1}.""" @@ -217,12 +245,14 @@ def create(kind: str, doc: dict[str, Any]) -> dict[str, Any]: def summary(folder: Path, kind: str, key: str, expand: Expand | None = None) -> dict[str, Any]: - """층 목록 한 줄에 붙일 것 — `{제목, 단위}` · `expand` 면 1 단위 기준 계(`기준금액` · 51-4).""" + """층 목록 한 줄에 붙일 것 — `{제목, 단위, 구분, 상세구분, 연결}`(51-12) · `expand` 면 + 1 단위 기준 계(`기준금액` · 51-4).""" try: - doc = json.loads((Path(folder) / kind / f"{key}.json").read_bytes()) + doc = filled(kind, json.loads((Path(folder) / kind / f"{key}.json").read_bytes())) except (OSError, ValueError): return {} - out = {"제목": doc.get("이름"), "단위": doc.get("단위")} + keep = ("구분", "상세구분", "연결") + out = {"제목": doc.get("이름"), "단위": doc.get("단위"), **{k: doc.get(k) for k in keep}} if expand is not None: layer, owner = where(folder) base = expand({"층": layer, "주인": owner, "키": key}).get("기준") or {} diff --git a/M02_MasterTemplete/M02_MasterTemplete_Router_Layers.py b/M02_MasterTemplete/M02_MasterTemplete_Router_Layers.py index 303a250ae..f6f44ca31 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Router_Layers.py +++ b/M02_MasterTemplete/M02_MasterTemplete_Router_Layers.py @@ -309,6 +309,7 @@ async def read_layer_template( raise HTTPException(status_code=404, detail="양식을 찾을 수 없습니다.") found["층"] = layer found["출처"] = layers.read_manifest(folder).get(f"{kind}/{name}") + found["문서"] = refs.filled(kind, found["문서"]) # 옛 서고 문서 구분 · 상세구분(51-12) return found diff --git a/resources/tester/test_m02_labor_ref.py b/resources/tester/test_m02_labor_ref.py index 56f074b61..5dc00d437 100644 --- a/resources/tester/test_m02_labor_ref.py +++ b/resources/tester/test_m02_labor_ref.py @@ -407,3 +407,30 @@ def test_사전조합이_자기_자신을_담으면_422_수량_기본_1(world): }, ) assert e.value.status == 422 and "자기 자신" in e.value.detail + + +def test_서고_구분_상세구분_연결_승격은_원_로직_사전조합은_고른_값_옛_문서는_읽을_때(world): + import M01_MasterData.M01_MasterData_Store as m01 + + row = m01.loaded()[1].logic(TEN) + sp = _promote(world["system"], 구분="틀림", 상세구분="틀림", 연결={"로직": "x"}) + doc = _read(world["system"], "promote", sp) + assert row["구분"] and row["상세구분"] + assert (doc["구분"], doc["상세구분"]) == (row["구분"], row["상세구분"]) # 화면 값 안 받음 + assert doc["연결"] == {"로직": TEN, "출처": row.get("출처") or ""} + sc = _combo(world["system"], [{"참조": {**SYS, "키": sp}}]) + assert (_read(world["system"], "precombo", sc)["구분"], "") == ("", "") + with store.at(world["system"]): + have = store.read("precombo", sc) + store.write("precombo", sc, have["판"], {**have["문서"], "구분": " 가 ", "상세구분": "나"}) + got = _read(world["system"], "precombo", sc) + assert (got["구분"], got["상세구분"]) == ("가", "나") + # 옛 문서(칸 없음) — 승격은 원 로직 것 · 사전조합은 빈 글 + old = {k: v for k, v in doc.items() if k not in ("구분", "상세구분", "연결")} + (world["system"] / "promote" / f"{sp}.json").write_text(json.dumps(old), encoding="utf-8") + assert refs.filled("promote", old)["연결"] == doc["연결"] + assert refs.filled("precombo", {"담은로직": []})["구분"] == "" + rows = [{"종류": "promote", "이름": sp}, {"종류": "precombo", "이름": sc}] + refs.summaries(world["system"], rows) + assert (rows[0]["구분"], rows[0]["상세구분"]) == (row["구분"], row["상세구분"]) + assert (rows[1]["구분"], rows[1]["상세구분"], rows[1]["연결"]) == ("가", "나", None) From 0793543803d568f90d6bd05444def0110f8ff2d3 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 5 Oct 2026 20:49:53 +0900 Subject: [PATCH 3/5] =?UTF-8?q?fix(M02):=20=EA=B3=A0=EB=A5=B4=EA=B8=B0=20?= =?UTF-8?q?=EC=B0=BD=20=EC=84=9C=EA=B3=A0=20=EC=A4=84=20=EB=B3=B4=EC=A1=B0?= =?UTF-8?q?=20=EA=B8=80=20=E2=80=94=20=EA=B5=AC=EB=B6=84=20=EC=97=86?= =?UTF-8?q?=EC=9C=BC=EB=A9=B4=20=EB=81=9D=20=E3=80=8C=C2=B7=E3=80=8D=20?= =?UTF-8?q?=EC=95=88=20=EB=B6=99=EC=9E=84=20(51-12=20=ED=99=94=EB=A9=B4)?= 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_01W4uKaV4ZBhsJN4GHG7yRj4 --- M02_MasterTemplete/M02_MasterTemplete_Labor_Pick.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Pick.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Pick.ts index 02331349f..ebf61477e 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Pick.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Pick.ts @@ -113,7 +113,9 @@ export function openLogicPicker(done: (logic: LogicSummary) => void, lib?: Libra key: x.row.이름, tag: tc(x.kind), name: x.row.제목 || x.row.이름, - sub: `${x.row.이름} · ${x.row.구분 ?? ""} ${x.row.상세구분 ?? ""}`.trim(), + sub: [x.row.이름, `${x.row.구분 ?? ""} ${x.row.상세구분 ?? ""}`.trim()] + .filter(Boolean) + .join(" · "), unit: x.row.단위 ?? "", onPick: () => { close(); From 86d4f3e88260568555bd7a06cb1d047f3ee71632 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 5 Oct 2026 20:55:11 +0900 Subject: [PATCH 4/5] =?UTF-8?q?fix(M02):=2051-13=20=EC=9D=BC=EC=9C=84?= =?UTF-8?q?=EC=A1=B0=ED=95=A9=20=EC=A4=84=20=ED=95=98=EC=9C=84=20=ED=96=89?= =?UTF-8?q?=20=E2=80=94=20=EC=84=B8=EB=B6=80=20=ED=95=AD=EB=AA=A9=20?= =?UTF-8?q?=ED=95=9C=20=EC=B8=B5(=EC=B0=B8=EC=A1=B0=20=EC=A4=84=20=C2=B7?= =?UTF-8?q?=20=EB=A1=9C=EC=A7=81=20=EC=A4=84)=20=EB=85=B8=EB=AC=B4?= =?UTF-8?q?=EB=B9=84=20=C2=B7=20=EC=9E=AC=EB=A3=8C=EB=B9=84=20=C2=B7=20?= =?UTF-8?q?=EA=B2=BD=EB=B9=84=20=3D=20=EC=95=88=20=EC=A4=84=20=EA=B8=B0?= =?UTF-8?q?=EC=A4=80=20=C3=97=20=EC=95=88=20=EC=A4=84=20=EC=88=98=EB=9F=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 하위 행 이름이 참조 키만 읽어 로직 안 줄(51-11)에서 표가 깨짐 → 참조 키 없으면 로직 키 - calc 답 「안」 타입에 로직 줄 자리 추가 - 확인 시나리오 resources/tester/ui_checks/51-13_일위조합_하위행.js (calc 답 바꿔 끼움 · 서고 저장 없음) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MgChuAcyBqUkhHMTXomQC7 --- .../M02_MasterTemplete_Labor_Calc.ts | 6 +- .../M02_MasterTemplete_Labor_Rows.ts | 19 ++- .../tester/ui_checks/51-13_일위조합_하위행.js | 132 ++++++++++++++++++ 3 files changed, 148 insertions(+), 9 deletions(-) create mode 100644 resources/tester/ui_checks/51-13_일위조합_하위행.js diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Calc.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Calc.ts index 0e8ee2a49..54e3b3e2e 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Calc.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Calc.ts @@ -36,11 +36,13 @@ export interface CalcLine { 결과?: unknown; /** 그 로직 재료 · 노무 · 경비 줄(M01 시험계산 표와 같은 모양 · 50-4) */ 줄?: LogicLine[]; - /** 참조 줄 — 가리킨 문서 이름 · 단가승격의 원 로직 키 · 사전조합 안 줄(한 층 · 50-3 ④) */ + /** 참조 줄 — 가리킨 문서 이름 · 단가승격의 원 로직 키 · 사전조합 안 줄(한 층 · 50-3 ④ · + * 참조 줄 = `참조` · 로직 줄(51-11) = `로직`) */ 이름?: string | null; 원로직?: string | null; 안?: { - 참조: LaborRef; + 참조?: LaborRef; + 로직?: string; 수량: number; 이름?: string | null; 단위?: string | null; diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Rows.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Rows.ts index 94b8ed902..5d108131b 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Rows.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Rows.ts @@ -229,17 +229,22 @@ export interface LogicTableData { } /** 고른 줄 하위 행(51-2 · 같은 표 · 같은 칸) — 단가산출 · 단가승격 = 그 로직 재료 · 노무 · 경비 줄 · - * 사전조합 = 안 줄 · 금액 칸 = 위 기준(위 줄 기준과 같은 바탕) / 아래 반영(× 위 줄 수량 ÷ 배) · + * 일위조합 = 세부 항목 한 층(51-13 · 참조 줄 · 로직 줄 · 안을 다시 펼치지 않음 · 금액 = 안 줄 기준 × 안 줄 수량) · + * 금액 칸 = 위 기준(위 줄 기준과 같은 바탕) / 아래 반영(× 위 줄 수량 ÷ 배) · * 하위 합 = 위 줄 금액 · 줄이 없으면 빈 한 줄(「…」 · 까닭 · 「—」) */ function subRows(line: LaborCalcLine | undefined, wait: boolean, tools: boolean): HTMLElement[] { const times = line?.수량 === null || line?.수량 === undefined ? null : line.수량 / (line.배 || 1); const items = line?.안 - ? line.안.map((x) => ({ - name: `${x.참조.키} ${x.이름 ?? ""}${x.까닭 ? ` · ${x.까닭}` : ""}`.trim(), - qty: x.수량, - unit: x.단위 ?? "", - cost: x.기준 ? Object.fromEntries(COSTS.map((c) => [c, x.기준![c] * x.수량])) : null, - })) + ? line.안.map((x) => { + const key = x.참조?.키 ?? x.로직 ?? ""; + const label = x.이름 && x.이름 !== key ? `${key} ${x.이름}` : key; // 로직 줄 이름 없으면 키만 + return { + name: `${label}${x.까닭 ? ` · ${x.까닭}` : ""}`, + qty: x.수량, + unit: x.단위 ?? "", + cost: x.기준 ? Object.fromEntries(COSTS.map((c) => [c, x.기준![c] * x.수량])) : null, + }; + }) : (line?.줄 ?? []).map((x) => ({ name: x.출처 ? `${x.이름} (${x.출처.replace(/[A-Z]{2}\d{6}\./g, "")})` : x.이름, qty: x.수량, diff --git a/resources/tester/ui_checks/51-13_일위조합_하위행.js b/resources/tester/ui_checks/51-13_일위조합_하위행.js new file mode 100644 index 000000000..c40dc1b7b --- /dev/null +++ b/resources/tester/ui_checks/51-13_일위조합_하위행.js @@ -0,0 +1,132 @@ +/* 51-13 — 일위대가 표 구분 「일위조합」 줄 누름 = 하위 행 = 그 조합 세부 항목 한 층(참조 줄 · 로직 줄 51-11) — 카드 · 상세 두 곳 + * 시작 화면: M02 마스터 템플릿 · 마스터 층(#/m02-master-template?open=구조물조합&root=master) · 시스템 관리자 · + * 나무에 item3(단가승격 SP-000001 · 일위조합 SC-000001 줄이 있음) + * 부르는 법: orca eval --expression "$(cat 이 파일)" (async 식 · JSON 글을 돌려줌) + * 쓰기 없음 — item3 calc 답만 바꿔 끼움(fetch · 끝에 되돌림): SC 줄 「안」 = [SP-000001 × 2 · 첫 단가산출 로직 줄 × 1.5] + * (안 줄 기준 = 같은 답의 그 줄 기준 · SC 기준 = 안 기준 × 수량 합 · SC 수량 손 3 · 반영 = 기준 × 3 · 서고 저장 없음) + * 기대 수치(카드 · 상세 같음): + * - sc.subs = sc.inner(2) · sc.cells = [head] · sc.next = true + * - sc.base ≈ sc.subBase(위 줄 기준 = 하위 합 · 반올림 차 ≤ 2) · sc.applied ≈ sc.subApplied ≈ sc.base × 3 + * - sc.names[0] 「SP-000001 …」 · sc.names[1] 「<로직 키>」(로직 줄 · 깨지지 않음) + * - sc.qty = ["2", "1.5"](안 줄 수량) · sp.subs ≥ 1(단가승격 하위 행 그대로) + */ +(async () => { + const w = (ms) => new Promise((r) => setTimeout(r, ms)); + const until = async (fn, ms = 8000) => { + for (let t = 0; t < ms; t += 150) { + const v = fn(); + if (v) return v; + await w(150); + } + return null; + }; + const SC = "참조|system||SC-000001"; + const SP = "참조|system||SP-000001"; + const KEYS = ["노무비", "재료비", "경비", "계"]; + const real = window.fetch; + let inner = 0; + window.fetch = async (input, init) => { + const res = await real(input, init); + const url = typeof input === "string" ? input : input.url; + if (!/\/labor\/item3\/calc/.test(url) || !res.ok) return res; + const j = await res.clone().json(); + const sc = j.줄.find((l) => l.참조?.키 === "SC-000001"); + const sp = j.줄.find((l) => l.참조?.키 === "SP-000001"); + const lg = j.줄.find((l) => !l.참조 && l.기준); + if (sc && sp?.기준 && lg) { + sc.안 = [ + { 참조: sp.참조, 수량: 2, 이름: sp.이름, 단위: sp.분모, 기준: sp.기준, 까닭: null }, + { 로직: lg.로직, 수량: 1.5, 이름: lg.로직, 단위: null, 기준: lg.기준, 까닭: null }, + ]; + sc.기준 = Object.fromEntries(KEYS.map((k) => [k, sp.기준[k] * 2 + lg.기준[k] * 1.5])); + Object.assign(sc, { 수량: 3, 수량출처: "손" }); // 위 줄 수량 3 — 반영 = 기준 × 3 + sc.반영 = Object.fromEntries(KEYS.map((k) => [k, sc.기준[k] * 3])); + inner = sc.안.length; + } + return new Response(JSON.stringify(j), { status: 200, headers: res.headers }); + }; + const n = (s) => Number(String(s ?? "").replace(/[^0-9.-]/g, "")) || 0; + const total = (tr, part) => { + const tds = tr.querySelectorAll(".m02-labrow__money"); + return n(tds[tds.length - 1].querySelector(part).innerText); + }; + const settled = (get) => + until(() => { + const t = get(); + return t && !/…/.test(t.querySelector("tbody").innerText) ? t : null; + }, 10000); + const open = async (get, key) => { + get().querySelector(`tr[data-labor-logic="${key}"] .m02-labrow__name`).click(); + await w(300); + const tb = get(); + const tr = tb.querySelector(`tr[data-labor-logic="${key}"]`); + const subs = [...tb.querySelectorAll("tbody tr[data-labor-sub]")]; + let at = tr.nextElementSibling; + if (at?.hasAttribute("data-labor-opt")) at = at.nextElementSibling; + return { tb, tr, subs, next: at === subs[0] }; + }; + const measure = async (get) => { + const t = await settled(get); + const head = t.querySelectorAll("thead th").length; + const sp = await open(get, SP); + const { tr, subs, next } = await open(get, SC); + const out = { + head, + sp: { subs: sp.subs.length }, + sc: { + inner, + subs: subs.length, + next, + cells: [ + ...new Set(subs.map((s) => [...s.children].reduce((a, c) => a + (c.colSpan || 1), 0))), + ], + base: total(tr, ".m02-labrow__base"), + subBase: Math.round(subs.reduce((a, s) => a + total(s, ".m02-labrow__base"), 0)), + applied: total(tr, ".m02-labrow__applied"), + subApplied: Math.round(subs.reduce((a, s) => a + total(s, ".m02-labrow__applied"), 0)), + names: subs.map((s) => s.querySelector(".m02-labrow__subname").innerText), + qty: subs.map((s) => s.querySelector(".m02-labrow__qty").innerText), + units: subs.map((s) => s.querySelector(".m02-labrow__unit").innerText), + }, + }; + await open(get, SC); // 같은 줄 다시 = 닫힘 + return out; + }; + try { + const node = await until(() => document.querySelector('[data-node="i|item3"]')); + if (!node) return JSON.stringify({ error: "item3 없음 — 폴더를 펼쳐 둘 것" }); + document.querySelector('[data-node="i|item1"]')?.click(); // 다시 읽게 다른 항목 거침 + await w(1500); + node.click(); + const card = await until(() => + [...document.querySelectorAll(".m02-struct__card")].find((c) => + c.querySelector(`table.m02-labrow tbody tr[data-labor-logic="${SC}"]`), + ), + ); + if (!card) return JSON.stringify({ error: "일위조합 줄 카드 없음" }); + await w(1500); + const out = { card: await measure(() => card.querySelector("table.m02-labrow")) }; + const edit = [...card.querySelectorAll(".m02-struct__card-tools button")].find( + (b) => b.innerText.trim() === "수정", + ); + if (!edit) return JSON.stringify({ ...out, error: "일위대가 [수정] 없음" }); + edit.click(); + const detail = () => + [...document.querySelectorAll("table.m02-labrow")].find( + (t) => !t.closest(".m02-struct__card") && t.querySelector(`tr[data-labor-logic="${SC}"]`), + ); + if (!(await until(detail))) return JSON.stringify({ ...out, error: "상세 표 없음" }); + await w(2000); + out.detail = await measure(detail); + document.querySelector('[data-node="i|item3"]')?.click(); + await w(800); + [...document.querySelectorAll(".ui-modal button, dialog button")] + .find((b) => /확인|버림|떠나기/.test(b.innerText)) + ?.click(); + await w(1200); + out.back = !!document.querySelector(".m02-struct__card table.m02-labrow"); + return JSON.stringify(out); + } finally { + window.fetch = real; + } +})(); From f6bcfa52ce3b541d3e3c9e609244dc35390093a5 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 5 Oct 2026 20:55:15 +0900 Subject: [PATCH 5/5] =?UTF-8?q?chore(sync):=20=EA=B3=B5=EC=9C=A0=20docs=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EB=8B=B4=EC=9D=8C?= 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_01MgChuAcyBqUkhHMTXomQC7 --- docs/raw/사용자요구/2026-10-05_수정요구.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/raw/사용자요구/2026-10-05_수정요구.md b/docs/raw/사용자요구/2026-10-05_수정요구.md index 14f1cf7f7..292d015d7 100644 --- a/docs/raw/사용자요구/2026-10-05_수정요구.md +++ b/docs/raw/사용자요구/2026-10-05_수정요구.md @@ -112,3 +112,7 @@ 단가산출과 일위대가 사전조합에서 테이블의 양식도 M01에 공통테이블 양식이 있을것같고 버튼들도 M01에 템플릿이 있을거야. 확인해봐 그리고 사전조합의 경우에 검색은 별도의 창이 아니라 구조물 조합의 일위대가 테이블의 조건 추가와 같은 기능이어야. 일위대가 사전조합에서 나오는 모당창의 형식 뿐만아니라 내용도 구조물조합의 일위대가의 상세 화면에서 로직 추가와 같아야된다고 선택되는 항목도 + +어차피 신규로 키값을 만들지 않았음? 그럼 당연히 해당 연결정보와 구분, 상세구분을 만들어야하지 않음? + +다음으로 구조물 조합에서 일위대가의 상세페이지에서 테이블 행 선택에서 단가승격이야 단가산출과 같은 항목이라 문제가 없는데 일위대가 사전조합의 경우에는 전체를 다 표현하는데 문제가 있어. 일위조합인 경우에는 조합의 세부항목을 기준으로 노무비, 재료비, 경비를 값을 가져와서 사용하면 가능하지 않을까?