From 10bc3686e2348cca2b58303c50926bf6d2fc3bd5 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 5 Oct 2026 11:22:06 +0900 Subject: [PATCH 1/3] =?UTF-8?q?style(B05):=20=EA=B3=84=ED=9A=8D=EB=85=B8?= =?UTF-8?q?=EC=84=A0=20=ED=8E=B8=EC=A7=91=20=ED=9A=A1=EB=8B=A8=20=EA=B8=80?= =?UTF-8?q?=EC=9E=90=20=ED=81=AC=EA=B8=B0=20=EC=A1=B0=EC=A0=95=20(44-2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 원지반 · 기본 계획 횡단 · 좌우 표고 글자 11px → 5.5px(절반) - 측점 표기 글자 = 제목 「횡단」과 같은 크기(inherit) - 화면 시나리오 resources/tester/ui_checks/44-2_횡단_글자.js 추가 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013rJwJMVL4EE31y77TB11AP --- .../B05_Profile_UI_Style_RouteEdit.css | 4 +- resources/tester/ui_checks/44-2_횡단_글자.js | 72 +++++++++++++++++++ 2 files changed, 74 insertions(+), 2 deletions(-) create mode 100644 resources/tester/ui_checks/44-2_횡단_글자.js diff --git a/B05_Profile/B05_Profile_UI_Style_RouteEdit.css b/B05_Profile/B05_Profile_UI_Style_RouteEdit.css index caa6c66c3..a7ebab158 100644 --- a/B05_Profile/B05_Profile_UI_Style_RouteEdit.css +++ b/B05_Profile/B05_Profile_UI_Style_RouteEdit.css @@ -187,7 +187,7 @@ .b05-routeedit__cross-station { color: var(--color-text-secondary); - font-size: var(--text-caption); + font-size: inherit; /* 44-2 — 제목(횡단)과 같은 크기 */ } /* 횡단 판 — 캔버스 그림이 아니라 **SVG 선**이다(2026-09-12 사용자 지시 ③). @@ -235,7 +235,7 @@ .b05-routeedit__cross-key { fill: var(--color-text-secondary, #94a3b8); - font-size: 11px; + font-size: 5.5px; /* 44-2 — 지금(11px)의 절반 */ } .b05-routeedit__cross-key.is-design { diff --git a/resources/tester/ui_checks/44-2_횡단_글자.js b/resources/tester/ui_checks/44-2_횡단_글자.js new file mode 100644 index 000000000..d5d0f35fc --- /dev/null +++ b/resources/tester/ui_checks/44-2_횡단_글자.js @@ -0,0 +1,72 @@ +/* 44-2 — B05 계획노선 편집 · 측점 횡단 글자 크기 + * 시작 화면: B05 종단설계(#/b05-profile) · 환경 검증 프로젝트(OWNERS.md) · 로그인됨 · 계획노선 편집 닫힘 + * 부르는 법: orca eval --expression "$(cat 이 파일)" (async 식 · JSON 글을 돌려줌) + * 하는 일: [계획노선 편집] 열기 → 첫 측점 라벨 누름(앱 모듈의 라벨 상자 자리) → 횡단 판 글자 잼 → [취소]로 닫음 + * 기대 수치: + * - keys = 원지반 · 기본 계획 횡단 · 「좌 … 표고 …」 셋 모두 fontSize 5.5px(옛 11px 의 절반) + * - station.fontSize = title.fontSize(측점 표기 = 제목 「횡단」 크기) + * - 바꾸는 값 없음(취소로 닫음 · 노선 안 고침) + */ +(async () => { + const w = (ms) => new Promise((r) => setTimeout(r, ms)); + const until = async (fn, ms = 15000) => { + for (let t = 0; t < ms; t += 200) { + const v = fn(); + if (v) return v; + await w(200); + } + return null; + }; + const btn = (text) => + [...document.querySelectorAll("button")].find( + (b) => b.offsetParent && b.textContent.trim() === text, + ); + btn("계획노선 편집")?.click(); + const canvas = await until(() => + [...document.querySelectorAll("canvas")].find( + (c) => c.offsetParent && c.closest("[class*='routeedit']"), + ), + ); + if (!canvas) return JSON.stringify({ ok: false, why: "편집 캔버스 없음" }); + // 앱이 실제로 부른 그 모듈 주소(@fs/<폴더>/…) — 같은 주소라야 라벨 상자 WeakMap 을 같이 씀 + const url = performance + .getEntriesByType("resource") + .map((e) => e.name) + .find((n) => n.includes("B05_Profile_UI_RouteEdit_Input.ts")); + const input = await import(url); + const boxes = await until(() => { + const b = input.stationLabelBoxes(canvas); + return b.length ? b : null; + }); + if (!boxes) return JSON.stringify({ ok: false, why: "측점 라벨 상자 없음" }); + const rect = canvas.getBoundingClientRect(); + const box = boxes[Math.floor(boxes.length / 2)]; + const at = { clientX: rect.left + box.x, clientY: rect.top + box.y, button: 0, bubbles: true }; + canvas.dispatchEvent(new PointerEvent("pointerdown", at)); + canvas.dispatchEvent(new PointerEvent("pointerup", at)); + const pane = await until(() => + [...document.querySelectorAll(".b05-routeedit__cross")].find((p) => + p.querySelector(".b05-routeedit__cross-key"), + ), + ); + if (!pane) return JSON.stringify({ ok: false, why: "횡단 판 안 뜸", chainage: box.chainage }); + const fs = (el) => el && getComputedStyle(el).fontSize; + const keys = [...pane.querySelectorAll(".b05-routeedit__cross-key")].map((t) => ({ + text: t.textContent.slice(0, 20), + fontSize: fs(t), + h: +t.getBoundingClientRect().height.toFixed(1), + })); + const title = pane.querySelector(".b05-routeedit__cross-title"); + const station = pane.querySelector(".b05-routeedit__cross-station"); + const out = { + keys, + title: { text: title.textContent, fontSize: fs(title) }, + station: { text: station.textContent, fontSize: fs(station) }, + }; + out.ok = + keys.length === 3 && + keys.every((k) => k.fontSize === "5.5px") && + out.title.fontSize === out.station.fontSize; + btn("취소")?.click(); + return JSON.stringify(out); +})(); From 411ad7de7dc74f521c2cd219744dbb49ab06492a Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 5 Oct 2026 11:23:02 +0900 Subject: [PATCH 2/3] =?UTF-8?q?=EF=BB=BFfeat(M02):=20=EC=9D=BC=EC=9C=84?= =?UTF-8?q?=EB=8C=80=EA=B0=80=20=EB=8B=A8=EA=B0=80=EC=82=B0=EC=B6=9C=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=ED=91=9C=2044-6=20=E2=80=94=20=EC=88=98?= =?UTF-8?q?=EB=9F=89(=EC=B9=B8)=20=ED=91=9C=EA=B8=B0=20=C2=B7=20=EC=98=B5?= =?UTF-8?q?=EC=85=98=20=EA=B0=92=20=EA=B1=B8=EC=B9=9C=20=ED=95=9C=20?= =?UTF-8?q?=EC=A4=84=20=C2=B7=20=EC=A0=84=EC=B2=B4=20=ED=95=A9=EA=B3=84=20?= =?UTF-8?q?=EA=B0=95=EC=A1=B0=20=C2=B7=20=EB=B3=84=EC=B9=AD=20=EC=97=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 칸 연결 표기 「B6 = 12.5」 → 「12.5(B6)」(수량산출 칸 · 수량 모달 지금 수량 · 자재 줄) - 옵션 값 = 줄 아래 이름 ~ 합계 칸에 걸친 한 줄(말줄임 · 전체 글 title) - 총합계 행 전체 합계 칸 테두리 · 색 - 별칭 열(이름 옆 · 칸 안 글상자 · 담은로직 줄 별칭 저장 · 서버 검사 40자 · 복사 · 가져오기 같이 감) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MgChuAcyBqUkhHMTXomQC7 --- M02_MasterTemplete/M02_Labor_Doc.py | 11 ++- .../M02_MasterTemplete_Labor.css | 64 ++++++++++++ .../M02_MasterTemplete_Labor_Api.ts | 3 + .../M02_MasterTemplete_Labor_Detail.ts | 15 ++- .../M02_MasterTemplete_Labor_Rows.ts | 97 ++++++++++++++++--- .../M02_MasterTemplete_Labor_Text.ts | 2 + resources/tester/test_m02_labor_copy.py | 4 +- resources/tester/test_m02_labor_doc.py | 11 ++- resources/tester/test_m02_labor_values.py | 10 +- 9 files changed, 190 insertions(+), 27 deletions(-) diff --git a/M02_MasterTemplete/M02_Labor_Doc.py b/M02_MasterTemplete/M02_Labor_Doc.py index 1dd338ff5..51189cd41 100644 --- a/M02_MasterTemplete/M02_Labor_Doc.py +++ b/M02_MasterTemplete/M02_Labor_Doc.py @@ -1,6 +1,6 @@ """M02 일위대가 문서 `labor/<열id>.json` — 정리 · 검사 · 계산 · 알약(PLAN 39-1a · 설계_정함 1장). -문서 = `{종류, 판, 열, 일위대가번, 단위, 본뜬, 담은로직: [{로직, 메모, 입력값, 수량}], +문서 = `{종류, 판, 열, 일위대가번, 단위, 본뜬, 담은로직: [{로직, 별칭, 메모, 입력값, 수량}], 자재: [{코드, 이름, 규격, 단위, 수량}], 계산값}` — 이름은 구조물 열 이름(따로 안 둠). 수량 = `null`(면적산출 알약 칸 값 · 로직 알약 「이름|단위」 · 자재 알약 「자재|MT키」) | `{손: 수}` (손이 칸보다 앞섬) · 같은 로직 두 번 금지 · 입력값 = 그 로직 입력 이름만(빈 값 = 자동값). @@ -26,6 +26,7 @@ mm = mf.mp.mm COSTS = mf.COST_ITEMS # 노무비 · 재료비 · 경비 MATERIAL_HEAD = "자재|" _MAT_COLS = ("코드", "이름", "규격", "단위") +ALIAS_MAX = 40 # 별칭(44-6) 글자 수 상한 — 화면 칸과 같음 def _bad(text: str) -> store.StoreError: @@ -83,10 +84,16 @@ def clean(doc: Any) -> dict[str, Any]: given = one.get("입력값") or {} if not isinstance(given, dict): raise _bad(f"{where} · 입력값이 묶음 아님") - memo = one.get("메모") + memo, alias = one.get("메모"), one.get("별칭") + if alias is not None and not isinstance(alias, str): + raise _bad(f"{where} · 별칭은 글") + alias = (alias or "").strip() + if len(alias) > ALIAS_MAX: + raise _bad(f"{where} · 별칭은 {ALIAS_MAX}자까지") lines.append( { "로직": code, + "별칭": alias or None, "메모": str(memo) if memo else None, "입력값": {k: v for k, v in given.items() if k in names and v not in (None, "")}, "수량": _qty(one.get("수량"), where), diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor.css b/M02_MasterTemplete/M02_MasterTemplete_Labor.css index 273b16e1f..3a42026b5 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor.css +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor.css @@ -78,6 +78,59 @@ white-space: nowrap; } +/* 옵션 값 줄(44-6) — 이름 ~ 합계에 걸친 한 줄 말줄임(max-width 0 = 표 폭을 안 늘림) · 위 줄과 한 덩이 */ +.m02-labrow__opt { + max-width: 0; + overflow: hidden; + padding-top: 0; + color: var(--color-text-secondary); + font-size: var(--text-caption); + white-space: nowrap; + text-overflow: ellipsis; +} + +.m02-labrow__row:has(+ .m02-labrow__optrow) td { + border-bottom: 0; +} + +.m02-labrow__optrow.is-open { + cursor: pointer; +} + +.m02-labrow__optrow.is-open:hover, +.m02-labrow__row.is-open:hover + .m02-labrow__optrow { + background: var(--color-mist-violet); +} + +/* 별칭 칸(44-6) — 칸 안 글상자 · 누르면 바로 고침 */ +.m02-labrow__alias { + min-width: 7rem; +} + +.m02-labrow__aliasin { + width: 100%; + min-width: 6rem; + padding: 2px var(--spacing-4); + border: 1px dashed var(--color-border); + border-radius: var(--radius-sm, 4px); + background: transparent; + color: inherit; + font: inherit; +} + +.m02-labrow__aliasin:hover, +.m02-labrow__aliasin:focus { + border-color: var(--color-primary); + border-style: solid; +} + +/* 총합계 줄 전체 합계 칸(44-6) — 테두리 · 색 */ +.m02-labrow__sum .m02-labrow__total { + border: 2px solid var(--color-primary); + background: var(--color-mist-violet); + color: var(--color-primary); +} + /* 입력 모달 */ .m02-labin { width: min(40rem, calc(100vw - 32px)); @@ -234,6 +287,17 @@ order: 3; text-align: right; } + + /* 옵션 값 줄 — 한 줄 통째 */ + .m02-labrow__optrow .m02-labrow__no, + .m02-labrow__sum .m02-labrow__alias { + display: none; + } + + .m02-labrow .m02-labrow__opt { + grid-column: 1 / -1; + max-width: none; + } } /* 수량산출 칸(40-1) — 위 결과단위 기준 수량 · 아래 지금 수량 · 누르면 손 입력 · 엑셀에서 고르기 */ diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Api.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Api.ts index 7c4de288a..5e7b50172 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Api.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Api.ts @@ -121,6 +121,8 @@ export type LaborQty = { 손: number } | null; /** 담은 로직 한 줄 — 입력값 = 그 로직 입력 이름만(빈 값 = 자동값) */ export interface LaborRow { 로직: string; + /** 사용자가 지은 이름(44-6 · 빈 것 = null) */ + 별칭?: string | null; 메모: string | null; 입력값: Record; 수량: LaborQty; @@ -153,6 +155,7 @@ export function normalizeLabor(doc: unknown, column: string): LaborDoc { 단위: d.단위 ?? null, 담은로직: (d.담은로직 ?? []).map((r) => ({ 로직: r.로직, + 별칭: r.별칭 ?? null, 메모: r.메모 ?? null, 입력값: r.입력값 ?? {}, 수량: r.수량 ?? null, diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts index 8697f287f..ca0adfab4 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts @@ -135,7 +135,13 @@ export function mountLaborDetail( { rows: cur.담은로직, logics, calc, fail, cells: cells() }, // 읽기만이면 순서 · 빼기 없음(줄 누름 = 보기 모달은 둠) edit - ? { open: openRow, qty: openQty, ...(opt.card ? {} : { move }), remove: removeRow } + ? { + open: openRow, + qty: openQty, + ...(opt.card ? {} : { move }), + remove: removeRow, + alias: setAlias, + } : { open: openRow }, ), ); @@ -262,6 +268,11 @@ export function mountLaborDetail( cur.담은로직.splice(i, 1); changed(); } + /** 별칭(44-6) — 화면 캐시만(저장은 [저장]) · 금액과 무관이라 다시 셈 · 다시 그림 없음 */ + function setAlias(i: number, value: string | null): void { + cur.담은로직[i].별칭 = value; + opt.onChanged(dirty()); + } function removeMat(i: number): void { cur.자재.splice(i, 1); changed(); @@ -311,7 +322,7 @@ export function mountLaborDetail( openLogicPicker((x) => { if (cur.담은로직.some((r) => r.로직 === x.키)) return showToast(tc("Labor_Already").replace("{value}", `${x.키} ${x.이름}`), "error"); - cur.담은로직.push({ 로직: x.키, 메모: null, 입력값: {}, 수량: null }); + cur.담은로직.push({ 로직: x.키, 별칭: null, 메모: null, 입력값: {}, 수량: null }); void loadLogics().then(() => changed()); }), }); diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Rows.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Rows.ts index 0b9f927fe..f2e413ce7 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Rows.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Rows.ts @@ -1,8 +1,9 @@ /* ============================================================================= * M02_MasterTemplete_Labor_Rows.ts * 일위대가 담은 로직 표 · 자재목록 표(39-3) — 상세 · 보기 갈래(면적산출 오른쪽 패널 · 카드)가 같이 씀. - * 담은 로직 = 차례 · 로직(키 · 이름 + 작은 글 「지역 서울 · 규격 25mm · 수량 B6 = 12.5㎡」 · 「빈 값 n」 붉게) · - * 결과 단위 · 금액 칸 넷(노무비 · 재료비 · 경비 · 합계 · 칸마다 위 기준 작게 / 아래 반영) · 맨 아래 합계 줄. + * 담은 로직 = 차례 · 로직(키 · 이름 + 「빈 값 n」 붉게) · 별칭(44-6) · 수량산출(「12.5(B6)」) · + * 결과 단위 · 금액 칸 넷(노무비 · 재료비 · 경비 · 합계 · 칸마다 위 기준 작게 / 아래 반영) · + * 옵션 값(「지역 서울 · 규격 25mm」)은 줄 아래 이름 ~ 합계에 걸친 한 줄 · 맨 아래 합계 줄(전체 합계 강조). * 자재목록 = 코드 · 이름 · 규격 · 수량산출 · 단위 · 가격 다섯(기준 칸 강조) · 기준단가 / 반영 금액(43-3). * 금액은 calc 답(없으면 「…」). * 줄 누름 · 순서 단추는 `on` 이 있을 때만(보기 갈래 = 없음). @@ -31,11 +32,14 @@ const HEAD: Record<(typeof COSTS)[number], Parameters[0]> = { 계: "Prev_Sum", }; +/** 옵션 값 줄이 걸치는 칸 수 — 이름 · 별칭 · 수량산출 · 결과 단위 · 금액 넷 */ +const SPAN = 4 + COSTS.length; + const num = (v: number): string => Number(v.toPrecision(12)).toLocaleString("ko-KR", { maximumFractionDigits: 6 }); -/** 수량 글 — 「수량 손 3 개소」 · 「수량 B6 = 12.5 ㎡」(칸 = 그 알약을 이은 면적산출 칸) · - * 이은 칸이 비었으면 「수량 A1 = 빈 칸」(수량 없음 · 서버와 같음) · 아니면 빈 글 */ +/** 수량 글 — 「수량 손 3 개소」 · 「수량 12.5 ㎡(B6)」(칸 = 그 알약을 이은 면적산출 칸 · 44-6) · + * 이은 칸이 비었으면 「수량 빈 칸(A1)」(수량 없음 · 서버와 같음) · 아니면 빈 글 */ export function qtyText( line: { 수량?: number | null; 수량출처?: string; 알약?: string | null } | undefined, unit: string, @@ -44,12 +48,12 @@ export function qtyText( if (!line) return ""; if (line.수량 === null || line.수량 === undefined) { const at = line.알약 ? cells.get(line.알약) : undefined; - return at ? `${tc("Labor_Qty")} ${at} = ${tc("In_QtyEmpty")}` : ""; + return at ? `${tc("Labor_Qty")} ${tc("In_QtyEmpty")}(${at})` : ""; } const v = `${num(line.수량)}${unit ? ` ${unit}` : ""}`; if (line.수량출처 === "손") return `${tc("Labor_Qty")} ${tc("In_QtyHand")} ${v}`; const cell = (line.알약 && cells.get(line.알약)) || tc("In_QtyCell"); - return `${tc("Labor_Qty")} ${cell} = ${v}`; + return `${tc("Labor_Qty")} ${v}(${cell})`; } /** 결과단위 기준 수량 — 분모 앞 수 · 분모(「10 ㎡」 · 「1 ㎥」) · 분모 없으면 빈 글 */ @@ -60,7 +64,7 @@ export function baseQtyText( return `${num(line.배 || 1)} ${line.분모}`; } -/** 지금 수량 짧은 글 — 「손 5 ㎥」 · 「B6 = 12.5」 · 「B6 = 빈 칸」 · 없으면 「—」 */ +/** 지금 수량 짧은 글 — 「손 5 ㎥」 · 「12.5(B6)」 · 「빈 칸(B6)」 · 없으면 「—」 */ export function qtyShort( line: { 수량?: number | null; 수량출처?: string; 알약?: string | null } | undefined, unit: string, @@ -68,13 +72,13 @@ export function qtyShort( ): string { const at = line?.알약 ? cells.get(line.알약) : undefined; if (!line || line.수량 === null || line.수량 === undefined) - return at ? `${at} = ${tc("In_QtyEmpty")}` : "—"; + return at ? `${tc("In_QtyEmpty")}(${at})` : "—"; if (line.수량출처 === "손") return `${tc("Labor_Hand")} ${num(line.수량)}${unit ? ` ${unit}` : ""}`; - return `${at ?? tc("In_QtyCell")} = ${num(line.수량)}`; + return `${num(line.수량)}(${at ?? tc("In_QtyCell")})`; } -/** 수량산출 칸(40-1) — 위 결과단위 기준 수량(「10 ㎡」) · 아래 지금 수량(「손 5 ㎥」 · 「B6 = 12.5」 · 「—」) · +/** 수량산출 칸(40-1) — 위 결과단위 기준 수량(「10 ㎡」) · 아래 지금 수량(「손 5 ㎥」 · 「12.5(B6)」 · 「—」) · * `open` 이 있으면 단추(줄 누름과 따로) */ function qtyCell( line: LaborCalcLine | undefined, @@ -155,6 +159,42 @@ export interface RowHandlers { qty?: (i: number) => void; move?: (i: number, d: number) => void; remove?: (i: number) => void; + /** 별칭 칸 고침(44-6 · 글자마다 · 빈 글 = null) — 없으면 글만 보임 */ + alias?: (i: number, value: string | null) => void; +} + +/** 별칭 칸(44-6) — 고칠 수 있으면 칸 안 글상자(누르면 바로 고침 · 줄 누름과 따로) · 아니면 글 · 비면 빈 칸 */ +function aliasCell( + value: string | null | undefined, + set: ((v: string | null) => void) | null, +): HTMLElement { + if (!set) + return el("td", { + className: "m02-labrow__alias", + text: value ?? "", + attrs: { "data-label": tc("Col_Alias") }, + }); + const input = el("input", { + className: "m02-labrow__aliasin", + attrs: { + type: "text", + maxlength: "40", // 서버 `ALIAS_MAX` 와 같음 + title: tc("Alias_Edit"), + "aria-label": tc("Col_Alias"), + "data-labor-alias": "", + }, + }); + input.value = value ?? ""; + input.addEventListener("click", (e) => e.stopPropagation()); // 줄 누름(입력 모달)과 따로 + input.addEventListener("input", () => set(input.value.trim() || null)); + input.addEventListener("keydown", (e) => { + if (e.key === "Enter") input.blur(); + }); + return el("td", { + className: "m02-labrow__alias", + attrs: { "data-label": tc("Col_Alias") }, + children: [input], + }); } export interface LogicTableData { @@ -175,7 +215,7 @@ export function logicTable(data: LogicTableData, on?: RowHandlers): HTMLElement const tools = !!(move || remove); const lineOf = (key: string): LaborCalcLine | undefined => data.calc?.줄.find((l) => l.로직 === key); - const trs = data.rows.map((row, i) => { + const trs = data.rows.flatMap((row, i) => { const logic = data.logics.get(row.로직); const line = lineOf(row.로직); const unit = logic?.결과단위 ?? ""; @@ -201,7 +241,6 @@ export function logicTable(data: LogicTableData, on?: RowHandlers): HTMLElement children: [ el("span", { className: "m02-labrow__key", text: row.로직 }), el("strong", { text: logic?.이름 ?? row.로직 }), - ...(note ? [el("small", { className: "m02-labrow__note", text: note })] : []), ...(values.empty ? [ el("small", { @@ -221,6 +260,7 @@ export function logicTable(data: LogicTableData, on?: RowHandlers): HTMLElement : []), ], }), + aliasCell(row.별칭, on?.alias ? (v) => on.alias?.(i, v) : null), qtyCell(line, data.cells, on?.qty ? () => on.qty?.(i) : null), el("td", { className: "m02-labrow__unit", text: unit }), ...COSTS.map((c) => moneyCell(line?.기준?.[c], line?.반영?.[c], wait, tc(HEAD[c]))), @@ -240,7 +280,23 @@ export function logicTable(data: LogicTableData, on?: RowHandlers): HTMLElement ], }); if (on) tr.addEventListener("click", () => on.open(i)); - return tr; + if (!note) return [tr]; + // 옵션 값 줄(44-6) — 이름 ~ 합계 칸에 걸친 한 줄(말줄임 · 전체 글은 title) · 행 높이 안 커짐 + const opt = el("tr", { + className: on ? "m02-labrow__optrow is-open" : "m02-labrow__optrow", + attrs: { "data-labor-opt": row.로직 }, + children: [ + el("td", { className: "m02-labrow__no", text: "" }), + el("td", { + className: "m02-labrow__opt", + text: note, + attrs: { colspan: String(SPAN), title: note }, + }), + ...(tools ? [el("td", { text: "" })] : []), + ], + }); + if (on) opt.addEventListener("click", () => on.open(i)); + return [tr, opt]; }); const sum = (part: Partial | undefined, c: (typeof COSTS)[number]): number | null => part?.[c] ?? null; @@ -251,11 +307,19 @@ export function logicTable(data: LogicTableData, on?: RowHandlers): HTMLElement children: [ el("td", { className: "m02-labrow__no", text: "" }), el("td", { className: "m02-labrow__name", text: tc("Prev_Sum") }), + el("td", { className: "m02-labrow__alias", text: "" }), el("td", { className: "m02-labrow__qty", text: "" }), el("td", { className: "m02-labrow__unit", text: "" }), - ...COSTS.map((c) => - moneyCell(sum(data.calc?.기준합, c), sum(data.calc?.반영합, c), wait, tc(HEAD[c])), - ), + ...COSTS.map((c) => { + const td = moneyCell( + sum(data.calc?.기준합, c), + sum(data.calc?.반영합, c), + wait, + tc(HEAD[c]), + ); + if (c === "계") td.classList.add("m02-labrow__total"); // 전체 합계 강조(44-6) + return td; + }), ...(tools ? [el("td", { text: "" })] : []), ], }), @@ -264,6 +328,7 @@ export function logicTable(data: LogicTableData, on?: RowHandlers): HTMLElement const heads = [ tc("Col_Order"), tc("Col_Name"), + tc("Col_Alias"), tc("Labor_QtyCol"), tc("Col_Unit"), ...COSTS.map((c) => tc(HEAD[c])), diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts index deae683f6..13fe5f3a6 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts @@ -27,6 +27,8 @@ const TEXT = { Logics_Title: ["단가산출 로직", "Unit-price logics"], Col_Order: ["차례", "No."], Col_Name: ["이름", "Name"], + Col_Alias: ["별칭", "Alias"], + Alias_Edit: ["눌러서 별칭 고침", "Click to edit alias"], Col_Unit: ["결과 단위", "Result unit"], Col_Move: ["순서", "Move"], Up: ["올리기", "Up"], diff --git a/resources/tester/test_m02_labor_copy.py b/resources/tester/test_m02_labor_copy.py index 56ca1b727..b2cc4269f 100644 --- a/resources/tester/test_m02_labor_copy.py +++ b/resources/tester/test_m02_labor_copy.py @@ -39,7 +39,7 @@ def world(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> dict[str, Any]: for cid in ("rv", "c1"): store.create_structure(cid) got = store.read("labor", "rv") - logic = [{"로직": TEN, "입력값": {}, "수량": {"손": 2}}] + logic = [{"로직": TEN, "별칭": " 돌붙임 ", "입력값": {}, "수량": {"손": 2}}] store.write("labor", "rv", got["판"], {**got["문서"], "단위": "개소", "담은로직": logic}) personal = layers.personal_dir(7, 42) items_store.ensure(personal) @@ -71,6 +71,7 @@ def test_같은_층_일위대가_하나_복사_번호_열은_받는_쪽(world): c1 = _labor(system, "c1") assert (c1["열"], c1["일위대가번"]) == ("c1", before["일위대가번"]) assert (c1["단위"], c1["담은로직"][0]["로직"]) == ("개소", TEN) + assert c1["담은로직"][0]["별칭"] == "돌붙임" # 별칭도 같이(44-6 · 앞뒤 빈칸 뗌) assert c1["계산값"]["반영합"]["계"] > 0 # 서버가 다시 셈 @@ -80,6 +81,7 @@ def test_다른_층에서_내_개인으로_저장_뒤에도_덮어씀(world): for _ in range(2): # 저장된 데이터도 다시 바꿈 assert client.post(url).status_code == 200 assert _labor(personal, "a")["담은로직"][0]["로직"] == TEN + assert _labor(personal, "a")["담은로직"][0]["별칭"] == "돌붙임" area = URL.format("personal", "area", "a") + "?from=rv&from_layer=system" assert client.post(area).status_code == 200 diff --git a/resources/tester/test_m02_labor_doc.py b/resources/tester/test_m02_labor_doc.py index adee730ee..a2a629fd0 100644 --- a/resources/tester/test_m02_labor_doc.py +++ b/resources/tester/test_m02_labor_doc.py @@ -34,7 +34,7 @@ def test_정리_모르는_입력_빈_값_버림_자재_칸은_M01_것(): ) ) assert got["담은로직"] == [ - {"로직": STONE, "메모": None, "입력값": {"돌": "깬돌"}, "수량": None} + {"로직": STONE, "별칭": None, "메모": None, "입력값": {"돌": "깬돌"}, "수량": None} ] assert got["자재"][0]["이름"] == "치 즐(0.2㎥)" and got["자재"][0]["수량"] == {"손": 2} assert (got["종류"], got["단위"], got["본뜬"]) == ("일위대가", "개소", None) @@ -47,6 +47,8 @@ def test_정리_모르는_입력_빈_값_버림_자재_칸은_M01_것(): _doc(담은로직=[{"로직": "GF999999"}]), # 없는 로직 _doc(담은로직=[{"로직": STONE, "수량": {"손": "열"}}]), # 수 아님 _doc(담은로직=[{"로직": STONE, "수량": 3}]), # 꼴 틀림 + _doc(담은로직=[{"로직": STONE, "별칭": 3}]), # 별칭 글 아님(44-6) + _doc(담은로직=[{"로직": STONE, "별칭": "가" * 41}]), # 별칭 40자 넘음 _doc(자재=[{"코드": "LB000001"}]), # 자재품목 아님 _doc(자재=[{"코드": MAT}, {"코드": MAT}]), # 같은 자재 두 번 "문서 아님", @@ -58,6 +60,13 @@ def test_틀린_문서는_400(bad): assert e.value.status == 400 +def test_별칭은_앞뒤_빈칸_떼고_빈_글은_null(): + got = labor.clean( + _doc(담은로직=[{"로직": STONE, "별칭": " 돌붙임 "}, {"로직": TEN, "별칭": " "}]) + ) + assert [r["별칭"] for r in got["담은로직"]] == ["돌붙임", None] + + def test_빈_일위대가도_됨(): got = labor.server(_doc()) assert got["계산값"]["기준합"]["계"] == 0 and got["계산값"]["반영합"]["계"] == 0 diff --git a/resources/tester/test_m02_labor_values.py b/resources/tester/test_m02_labor_values.py index be396caab..f69a66248 100644 --- a/resources/tester/test_m02_labor_values.py +++ b/resources/tester/test_m02_labor_values.py @@ -144,7 +144,7 @@ console.log(JSON.stringify([ def test_수량_글은_이은_칸_주소를_보임() -> None: - """`Labor_View.setQty(수량, 칸)` 의 칸 주소로 「수량 B6 = 20 ㎡」 — 없으면 「면적산출 칸」.""" + """`Labor_View.setQty(수량, 칸)` 의 칸 주소로 「수량 20 ㎡(B6)」 — 없으면 「면적산출 칸」.""" node = shutil.which("node") if not node: pytest.skip("node 없음") @@ -154,11 +154,11 @@ def test_수량_글은_이은_칸_주소를_보임() -> None: assert out.returncode == 0, out.stderr got = json.loads(out.stdout) # 수량산출 열(40-1) — 위 결과단위 기준 수량 · 아래 지금 수량 짧은 글 - assert got[:7] == ["10 ㎡", "1 ㎥", "", "손 5 ㎥", "B6 = 12.5", "B6 = 빈 칸", "—"] + assert got[:7] == ["10 ㎡", "1 ㎥", "", "손 5 ㎥", "12.5(B6)", "빈 칸(B6)", "—"] assert got[7:] == [ - "수량 B6 = 20 ㎡", - "수량 면적산출 칸 = 20 ㎡", + "수량 20 ㎡(B6)", # 44-6 — 수량(칸 주소) + "수량 20 ㎡(면적산출 칸)", "수량 손 입력 3 ㎡", - "수량 B6 = 빈 칸", # 이은 칸이 비었음 = 수량 없음(39-8 ② · 서버 area_values 와 같음) + "수량 빈 칸(B6)", # 이은 칸이 비었음 = 수량 없음(39-8 ② · 서버 area_values 와 같음) "", ] From a22f03426fd9a80c584ccc24fc363ad2617ea020 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 5 Oct 2026 11:23:22 +0900 Subject: [PATCH 3/3] =?UTF-8?q?feat(m02,b01):=20=EC=9D=BC=EC=9C=84?= =?UTF-8?q?=EB=8C=80=EA=B0=80=20=EC=83=81=EC=84=B8=20=EB=8B=A8=EC=9C=84=20?= =?UTF-8?q?=EB=93=9C=EB=A1=AD=EB=8B=A4=EC=9A=B4=20=C2=B7=20=EB=8C=80?= =?UTF-8?q?=EC=8B=9C=EB=B3=B4=EB=93=9C=20=EC=9D=BC=EC=9C=84=EB=8C=80?= =?UTF-8?q?=EA=B0=80=20=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"],