From a22f03426fd9a80c584ccc24fc363ad2617ea020 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 5 Oct 2026 11:23:22 +0900 Subject: [PATCH] =?UTF-8?q?feat(m02,b01):=20=EC=9D=BC=EC=9C=84=EB=8C=80?= =?UTF-8?q?=EA=B0=80=20=EC=83=81=EC=84=B8=20=EB=8B=A8=EC=9C=84=20=EB=93=9C?= =?UTF-8?q?=EB=A1=AD=EB=8B=A4=EC=9A=B4=20=C2=B7=20=EB=8C=80=EC=8B=9C?= =?UTF-8?q?=EB=B3=B4=EB=93=9C=20=EC=9D=BC=EC=9C=84=EB=8C=80=EA=B0=80=20?= =?UTF-8?q?=EC=B9=B4=EB=93=9C=20=EC=97=86=EC=95=B0(44-7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 일위대가 상세 머리 「단위」 = 표준 단위 고르기(UnitPick) · 지금 값이 목록에 없어도 선택지 · 저장은 상세 [저장] - 대시보드 시스템 설정의 「일위대가」 카드 · 진입 해시 open=일위대가 길 · 안 쓰는 글 키 없앰 - 시험 고침 · ui_checks/44-7 화면 판정 스크립트 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016PSXsvyDxjNmufteN5bqmt --- B01_Dashboard/B01_Dashboard_UI_Page.ts | 6 +- .../M02_MasterTemplete_Labor_Detail.ts | 31 +++++--- .../M02_MasterTemplete_Layer.ts | 6 +- .../M02_MasterTemplete_UI_Page.ts | 2 +- .../M02_MasterTemplete_UI_Side.ts | 11 +-- resources/tester/test_b01_settings_cards.py | 9 +-- resources/tester/test_m02_card_shell.py | 10 +++ resources/tester/test_m02_labor_tree.py | 4 +- resources/tester/test_m02_layer_screen.py | 2 +- .../tester/ui_checks/44-7_단위_드롭다운.js | 78 +++++++++++++++++++ ui_template/ui_template_locale_m2.ts | 1 - 11 files changed, 123 insertions(+), 37 deletions(-) create mode 100644 resources/tester/ui_checks/44-7_단위_드롭다운.js diff --git a/B01_Dashboard/B01_Dashboard_UI_Page.ts b/B01_Dashboard/B01_Dashboard_UI_Page.ts index 0c677c35d..4443b06a8 100644 --- a/B01_Dashboard/B01_Dashboard_UI_Page.ts +++ b/B01_Dashboard/B01_Dashboard_UI_Page.ts @@ -178,7 +178,6 @@ function buildSystemSettingsPanel(user: DashboardUser, companyId: number | null) settingCard(L("B01_Dashboard_MasterElements"), L("B01_Dashboard_PermSourceRead"), () => navigateTo(ROUTES.M01_MASTER_DATA), ), - m02(L("B01_Dashboard_CardUnitCombo"), "일위대가", "master", desc), m02(L("B01_Dashboard_CardStructCombo"), "구조물조합", "master", desc), m02(L("B01_Dashboard_CardStructOption"), "옵션", "master", desc), m02(L("B01_Dashboard_CardDrawing"), "도면", "master", desc), @@ -193,10 +192,7 @@ function buildSystemSettingsPanel(user: DashboardUser, companyId: number | null) L(companyAdmin ? "B01_Dashboard_PermCompanyWrite" : "B01_Dashboard_PermCompanyRead"), L("B01_Dashboard_PermPersonalWrite"), ].join(" · "); - wrap.append( - m02(L("B01_Dashboard_CardUnitCombo"), "일위대가", root, desc), - m02(L("B01_Dashboard_CardStructCombo"), "구조물조합", root, desc), - ); + wrap.append(m02(L("B01_Dashboard_CardStructCombo"), "구조물조합", root, desc)); return wrap; } diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts index 8697f287f..e4e63a645 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts @@ -43,6 +43,7 @@ import { logicTable, matTable, qtyShort } from "./M02_MasterTemplete_Labor_Rows" import { tc } from "./M02_MasterTemplete_Labor_Text"; import { keepKnown } from "./M02_Labor_Values_Model"; import { pickMaterial } from "./M02_Material_Pick"; +import { createUnitPick, NO_UNIT } from "./DataFlow/M02_DF_UnitPick"; import "../M01_MasterData/M01_MasterData_UI_Logic_Style.css"; import "../M01_MasterData/M01_MasterData_UI_LogicLab_Style.css"; import "@ui/ui_template_general_layout.css"; @@ -330,15 +331,27 @@ export function mountLaborDetail( }), }); - const unit = createInputField({ type: "text", label: tc("Labor_Unit") }); - unit.input.value = cur.단위 ?? ""; - unit.input.readOnly = !edit; - unit.input.maxLength = 20; - unit.input.setAttribute("data-labor-field", "단위"); - unit.input.addEventListener("input", () => { - cur.단위 = unit.input.value.trim() || null; - opt.onChanged(dirty()); - }); + // 단위 = 표준 단위 목록에서 고르기(지금 값이 목록에 없어도 선택지로 둠) · 저장은 [저장] + const unit = { + root: el("div", { className: "ui-field", attrs: { "data-labor-field": "단위" } }), + }; + const unit0 = cur.단위; + const drawUnit = (): void => + unit.root.replaceChildren( + el("label", { className: "ui-field__label", text: tc("Labor_Unit") }), + createUnitPick({ + value: cur.단위 || NO_UNIT, + extra: [unit0, cur.단위].filter((u): u is string => !!u), + readOnly: !edit, + ariaLabel: tc("Labor_Unit"), + onPick: (u) => { + cur.단위 = u === NO_UNIT ? null : u; + opt.onChanged(dirty()); + drawUnit(); + }, + }), + ); + drawUnit(); const origin = createInputField({ type: "text", label: tc("Labor_Origin") }); origin.input.value = cur.본뜬?.키 ? `${cur.본뜬.층} ${cur.본뜬.키}` : tc("Origin_Self"); origin.input.readOnly = true; diff --git a/M02_MasterTemplete/M02_MasterTemplete_Layer.ts b/M02_MasterTemplete/M02_MasterTemplete_Layer.ts index c1860e584..47fd72a35 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Layer.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Layer.ts @@ -1,7 +1,7 @@ /* ============================================================================= * M02_MasterTemplete_Layer.ts * M02 한 화면의 뿌리 셋(마스터 · 회사 · 개인 · PLAN 27-1) — 진입 해시 - * `#/m02-master-template?open=구조물조합|일위대가|옵션|도면&root=master|company|personal&owner=…&company=…` + * `#/m02-master-template?open=구조물조합|옵션|도면&root=master|company|personal&owner=…&company=…` * (옛 `?layer=` 도 root 로 읽음) · 고칠 권한(`accessFor` — 서버 `M02_Layer_Access` 와 짝 · 27-1c) · * 회사 없음 안내 · 회사용 읽기만일 때 [개인용으로 복사] 막대. * ========================================================================== */ @@ -16,7 +16,7 @@ import { copyItemToPersonal, fetchStructureCombos } from "./M02_MasterTemplete_A export type ManageLayer = "system" | "company" | "personal"; /** 해시 · 화면의 뿌리 이름 — 서버 층 이름은 `layerOfRoot` */ export type Root = "master" | "company" | "personal"; -export type OpenTarget = "구조물조합" | "일위대가" | "옵션" | "도면"; +export type OpenTarget = "구조물조합" | "옵션" | "도면"; export interface Entry { root: Root; @@ -27,7 +27,7 @@ export interface Entry { company: string | null; } -const OPENS: OpenTarget[] = ["구조물조합", "일위대가", "옵션", "도면"]; +const OPENS: OpenTarget[] = ["구조물조합", "옵션", "도면"]; export const layerOfRoot = (root: Root): ManageLayer => (root === "master" ? "system" : root); diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts b/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts index cfa2c9eeb..21b36e7fe 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts @@ -226,7 +226,7 @@ export async function renderM02MasterTemplate(root: HTMLElement): Promise void; /** 진입 해시 `open`(PLAN 27-1d) — 그 컨테이너를 펼침(옵션은 열기까지) · 옵션 · 도면은 시스템 층에만 */ - reveal: (target: "옵션" | "도면" | "일위대가") => void; + reveal: (target: "옵션" | "도면") => void; } /** 서버 이름 규칙과 같게 — 화면에서 먼저 막음 */ @@ -414,14 +414,7 @@ export function buildSide(opt: SideOptions): SideHandle { } } - const reveal = (target: "옵션" | "도면" | "일위대가"): void => { - // 일위대가 = 구조물 조합(처음부터 펼침) 안 일위대가 하위를 펼치고 「전체 항목」 - if (target === "일위대가") { - columnSections[COLUMN_KINDS.findIndex((k) => k.kind === "labor")].classList.remove( - "is-collapsed", - ); - return void selectAll("labor"); - } + const reveal = (target: "옵션" | "도면"): void => { extras?.[target].classList.remove("is-collapsed"); if (target === "옵션") options.body.querySelector("button")?.click(); }; diff --git a/resources/tester/test_b01_settings_cards.py b/resources/tester/test_b01_settings_cards.py index 97a09c3a2..f5872516f 100644 --- a/resources/tester/test_b01_settings_cards.py +++ b/resources/tester/test_b01_settings_cards.py @@ -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"], ] diff --git a/resources/tester/test_m02_card_shell.py b/resources/tester/test_m02_card_shell.py index 1b7c067a6..aada156a2 100644 --- a/resources/tester/test_m02_card_shell.py +++ b/resources/tester/test_m02_card_shell.py @@ -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 diff --git a/resources/tester/test_m02_labor_tree.py b/resources/tester/test_m02_labor_tree.py index 50f51fcad..02df52984 100644 --- a/resources/tester/test_m02_labor_tree.py +++ b/resources/tester/test_m02_labor_tree.py @@ -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 diff --git a/resources/tester/test_m02_layer_screen.py b/resources/tester/test_m02_layer_screen.py index 9e3fb2227..280859a46 100644 --- a/resources/tester/test_m02_layer_screen.py +++ b/resources/tester/test_m02_layer_screen.py @@ -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 diff --git a/resources/tester/ui_checks/44-7_단위_드롭다운.js b/resources/tester/ui_checks/44-7_단위_드롭다운.js new file mode 100644 index 000000000..450efdcfe --- /dev/null +++ b/resources/tester/ui_checks/44-7_단위_드롭다운.js @@ -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); +})(); diff --git a/ui_template/ui_template_locale_m2.ts b/ui_template/ui_template_locale_m2.ts index e6817a238..ee9999c55 100644 --- a/ui_template/ui_template_locale_m2.ts +++ b/ui_template/ui_template_locale_m2.ts @@ -4,7 +4,6 @@ * ========================================================================== */ export const ui_locales_m2 = { - B01_Dashboard_CardUnitCombo: ["일위대가", "Unit costs"], B01_Dashboard_CardStructCombo: ["구조물 조합", "Structure combos"], B01_Dashboard_CardStructOption: ["구조물 옵션", "Structure options"], B01_Dashboard_CardDrawing: ["도면 양식", "Drawing templates"],