From bec4258bd304b0e6c22888a2007672c3cb433de4 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 5 Oct 2026 20:31:11 +0900 Subject: [PATCH] =?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; + } +})();