From 40ff4a551c066975509d9d47768b42938ea60347 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 5 Oct 2026 12:23:19 +0900 Subject: [PATCH 1/9] auto: 2026-10-05 12:23 (ESD_LAPTOP) --- docs/raw/사용자요구/2026-10-05_수정요구.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/raw/사용자요구/2026-10-05_수정요구.md b/docs/raw/사용자요구/2026-10-05_수정요구.md index eec77747b..305ec2bf6 100644 --- a/docs/raw/사용자요구/2026-10-05_수정요구.md +++ b/docs/raw/사용자요구/2026-10-05_수정요구.md @@ -29,3 +29,9 @@ - CAD 프로그램 : 한글입력 안됨. 계획서를 작성하고 코드작업을 진행해줘. 난 이제 자리를 비워야하니까 참고해. + +## 추가작업 (10-5 · 원문 그대로) + +1. M02 페이지의 전체영역에서 알약 템플릿으로 프론트엔드에 사용중인데 버튼들은 기본 버튼 템플릿을 사용하게 변경해서 알약 템플릿은 알약에만 사용하는게 맞아. +2. M02 페이지의 좌측패널 내부 별도의 서브 컨테이너로 구조물도면, 일위대가, 면적산출 구성되어 있는데 이건 필요없어 보임 접근은 구조물 조합을 선택하면 바로 3개의 항목 연결이 보이니 불필요해보여. +3. 구조물 조합 상세 페이지의 구조물 분류 테이블에 일위대가 도면 면적산출 행에 키값들을 노출하고 있는데 이건 필요없어보여. From 1ca99ed3ae5fea6fe4a1c0b251f377f2fa80cb4a Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 5 Oct 2026 12:25:12 +0900 Subject: [PATCH 2/9] =?UTF-8?q?fix(M01):=20=EB=A1=9C=EC=A7=81=20=EC=83=81?= =?UTF-8?q?=EC=84=B8=20=EC=98=A4=EB=A5=B8=EC=AA=BD=20=EB=91=90=20=ED=8C=A8?= =?UTF-8?q?=EB=84=90=20=EA=B3=B5=EC=9A=A9=20=EC=A0=91=EA=B8=B0=20=EC=B9=B4?= =?UTF-8?q?=EB=93=9C=EB=A1=9C=20=E2=80=94=20=EC=A0=9C=EB=AA=A9=20=ED=96=89?= =?UTF-8?q?=20=EB=88=84=EB=A6=84=20=C2=B7=20=EC=BA=90=EB=9F=BF=20=C2=B7=20?= =?UTF-8?q?=ED=95=98=EB=82=98=EB=A7=8C=20=EC=97=B4=EB=A6=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 흐름도 · 원문 = ui-collapsible 카드(M02 면적산출 오른쪽 패널 꼴) · attachCollapsible 토글 위에 하나만 열림 규칙 - 따로 만든 원문 토글 단추 · 얇은 띠 제거 · 흐름도 패널 자체 접기 단추는 M01 에서 숨김 - 둘 다 접힘 = 제목 행 둘(칸 140px) · 취향 기억(m01-logic-panel) 그대로 - 44-4 화면 시험 카드 꼴로 고침 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01NPJGtfXo6GMbcpiboQC4uK --- .../M01_MasterData_UI_LogicLab_Side.css | 24 +++--- .../M01_MasterData_UI_LogicLab_Side.ts | 77 ++++++++++--------- .../tester/ui_checks/44-4_로직상세_패널.js | 27 ++++--- 3 files changed, 74 insertions(+), 54 deletions(-) diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab_Side.css b/M01_MasterData/M01_MasterData_UI_LogicLab_Side.css index 711326450..853a55b29 100644 --- a/M01_MasterData/M01_MasterData_UI_LogicLab_Side.css +++ b/M01_MasterData/M01_MasterData_UI_LogicLab_Side.css @@ -1,22 +1,28 @@ -/* 로직 상세 오른쪽 두 패널 — 흐름도 · 원문 · 접힌 패널은 얇은 띠 */ +/* 로직 상세 오른쪽 두 패널 — 흐름도 · 원문 · 공용 접기 카드(접힌 카드 = 제목 행) */ .m01lab-side { - display: flex; - align-items: stretch; min-width: 0; } +.m01lab-side__card, .m01lab-side__slot { - flex: 0 0 40px; min-width: 0; } -.m01lab-side__slot.is-open { - flex: 1 1 auto; +/* 카드 제목 행이 접기 단추 — 흐름도 패널 자체 접기 단추 · 테두리는 뺌 */ +.m01lab-side .m02-dfp__toggle { + display: none; } -/* 접힌 흐름도 자리 — 띠만(기다림 글 · 변환 알림은 숨김) */ -.m01lab-side__slot:not(.is-open) > :not(.m01lab-flow__panel, .m01lab-src) { - display: none; +.m01lab-side .m02-dfp { + border-left: 0; + background: none; +} + +.m01lab-src { + display: flex; + flex-direction: column; + gap: var(--spacing-8); + min-width: 0; } .m01lab-src__body { diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab_Side.ts b/M01_MasterData/M01_MasterData_UI_LogicLab_Side.ts index 13f8da260..3077d01bb 100644 --- a/M01_MasterData/M01_MasterData_UI_LogicLab_Side.ts +++ b/M01_MasterData/M01_MasterData_UI_LogicLab_Side.ts @@ -1,12 +1,14 @@ /* ============================================================================= * M01_MasterData_UI_LogicLab_Side.ts * 로직 상세 오른쪽 두 패널 — 「흐름도」(다이어그램 · 상세 내용 = M02_DF_Panel) · 「원문」(품셈 본문 절) - * 둘 중 하나만 열림(하나 열면 다른 하나 접힘) · 둘 다 접기 됨 · 접힌 패널 = 얇은 띠(머리 단추 그대로) + * 공용 접기 카드(`ui-collapsible` · 제목 행 누름 · 캐럿) 두 장 — attachCollapsible 토글 위에 하나만 열림 규칙 + * 둘 다 접기 됨 · 접힌 카드 = 제목 행만 * 열린 패널은 계정 취향(`m01-logic-panel`)으로 기억 · 원문은 출처 글로 서버가 찾음(`GET /m01/logic/source`) * ========================================================================== */ import { currentLanguageIndex } from "@ui/ui_template_locale"; -import { createButton, el } from "@ui/ui_template_elements"; +import { attachCollapsible } from "@ui/ui_template_collapsible"; +import { el } from "@ui/ui_template_elements"; import { API_BASE_URL } from "@config/config_frontend"; import { readStateRaw, writeStateRaw } from "../A00_Common/b_page_state"; import type { SplitHandle } from "./M01_MasterData_UI_LogicLab_Flow"; @@ -14,9 +16,8 @@ import "../M02_MasterTemplete/DataFlow/M02_DF_Panel.css"; import "./M01_MasterData_UI_LogicLab_Side.css"; const TEXT = { - Toggle: ["원문", "Source"], - Open: ["원문 펴기", "Open the source panel"], - Close: ["원문 접기", "Collapse the source panel"], + Flow: ["흐름도", "Flow"], + Source: ["원문", "Source"], Loading: ["원문 찾는 중", "Looking up the source"], None: ["원문 연결 없음", "No linked source"], From: ["출처", "Source"], @@ -27,8 +28,8 @@ const TEXT = { const ts = (key: keyof typeof TEXT, v = ""): string => TEXT[key][currentLanguageIndex === 1 ? 1 : 0].replace("{v}", v); -/** 접힌 띠 너비 — M02_DF_Panel 의 STRIP_WIDTH 와 같음(패널 모듈은 늦게 받으므로 따로 둠) */ -const STRIP = 40; +/** 둘 다 접힘 — 제목 행 둘이 들어가는 칸 너비 */ +const FOLDED = 140; export type PanelPick = "flow" | "source" | "none"; @@ -86,21 +87,28 @@ export function buildLogicSide(split: SplitHandle): LogicSide { const flowSlot = el("div", { className: "m01lab-side__slot" }); const body = el("div", { className: "m01lab-src__body" }); const title = el("strong", { className: "m02-dfp__title" }); - const toggle = createButton({ - label: ts("Toggle"), - size: "sm", - variant: "ghost", - onClick: () => pick(now === "source" ? "none" : "source"), - }); - toggle.classList.add("m02-dfp__toggle"); - const head = el("header", { className: "m02-dfp__head", children: [toggle, title] }); - const open = el("div", { className: "m02-dfp__open", children: [head, body] }); - const strip = el("div", { className: "m02-dfp__strip" }); - const source = el("aside", { className: "m02-dfp m01lab-src", children: [open, strip] }); - const sourceSlot = el("div", { className: "m01lab-side__slot", children: [source] }); - split.right.replaceChildren( - el("div", { className: "m01lab-side", children: [flowSlot, sourceSlot] }), + const card = (label: string, inner: HTMLElement): HTMLElement => + el("section", { + className: "m01lab-side__card ui-collapsible ui-sidebar-section ui-sidebar-card", + children: [el("h3", { className: "ui-collapsible__title", text: label }), inner], + }); + const flowCard = card(ts("Flow"), flowSlot); + const sourceCard = card( + ts("Source"), + el("div", { className: "m01lab-src", children: [title, body] }), ); + const side = el("div", { + className: "m01lab-side ui-sidebar-panel", + children: [flowCard, sourceCard], + }); + attachCollapsible(side); + // 공용 토글 뒤 — 연 카드만 남기고 다른 카드는 접음(접었으면 둘 다 닫힘) + side.addEventListener("click", (ev) => { + const at = (ev.target as HTMLElement).closest(".ui-collapsible__title")?.parentElement; + if (at !== flowCard && at !== sourceCard) return; + pick(at.classList.contains("is-collapsed") ? "none" : at === flowCard ? "flow" : "source"); + }); + split.right.replaceChildren(side); let now = readPick(); let flowPx: number | null = null; @@ -108,14 +116,22 @@ export function buildLogicSide(split: SplitHandle): LogicSide { let want = ""; let shown: string | null = null; - /** 칸 너비 — 원문(또는 흐름도가 아직 안 뜸) = 두 칸 비율 · 흐름도 = 패널 너비 + 띠 · 둘 다 접힘 = 띠 둘 */ + /** 카드 안팎 여백(좌우 안쪽 + 테두리) — 흐름도 패널 너비에 더함 */ + const pad = (): number => { + const s = getComputedStyle(flowCard); + return ["paddingLeft", "paddingRight", "borderLeftWidth", "borderRightWidth"].reduce( + (n, k) => n + (parseFloat(s[k as "paddingLeft"]) || 0), + 0, + ); + }; + /** 칸 너비 — 원문(또는 흐름도가 아직 안 뜸) = 두 칸 비율 · 흐름도 = 패널 너비 + 카드 여백 · 둘 다 접힘 = 제목 행 */ const width = (): void => split.setRight( now === "source" || (now === "flow" && flowPx === null) ? null : now === "flow" - ? (flowPx ?? 0) + STRIP - : STRIP * 2, + ? (flowPx ?? 0) + pad() + : FOLDED, ); const load = (): void => { @@ -149,21 +165,12 @@ export function buildLogicSide(split: SplitHandle): LogicSide { }); }; - const fold = (on: boolean): void => { - open.hidden = on; - strip.hidden = !on; - (on ? strip : head).prepend(toggle); - toggle.setAttribute("aria-expanded", String(!on)); - toggle.title = ts(on ? "Open" : "Close"); - }; - const pick = (next: PanelPick): void => { if (next !== now) writeStateRaw("m01-logic-panel", next); now = next; flowFold?.(now !== "flow"); // 칸을 줄이기 전에 — 흐름도 패널이 접기 전 너비를 재어 둠 - flowSlot.classList.toggle("is-open", now === "flow"); - sourceSlot.classList.toggle("is-open", now === "source"); - fold(now !== "source"); + flowCard.classList.toggle("is-collapsed", now !== "flow"); + sourceCard.classList.toggle("is-collapsed", now !== "source"); width(); load(); }; diff --git a/resources/tester/ui_checks/44-4_로직상세_패널.js b/resources/tester/ui_checks/44-4_로직상세_패널.js index 682c0204e..df8d948c0 100644 --- a/resources/tester/ui_checks/44-4_로직상세_패널.js +++ b/resources/tester/ui_checks/44-4_로직상세_패널.js @@ -4,10 +4,11 @@ * 로직 GC000277(건설품셈 토목 1-1-2 유도선 설치 및 해체)을 엶 · 고치지 않음 · 끝에 패널 취향을 처음 값으로 되돌림. * 기대 수치: * order = ["기본정보","시험 계산","단가산출 상세","텍스트 수식"](흐름 로직이면 셋째 · 넷째는 흐름 보기 안) - * flow = {flow:true, source:false, pref:"flow"} · 오른쪽 칸 너비 > 300 - * source = {flow:false, source:true, pref:"source"} · srcHead 에 「1-1-2 유도선」 · srcTable 에 「특별인부」 - * none = {flow:false, source:false, pref:"none"} · 오른쪽 칸 너비 = 80(띠 둘) - * back = {flow:true, source:false, pref:"flow"}(흐름도 띠 단추로 다시 폄) + * 두 패널 = 공용 접기 카드(ui-collapsible · 제목 행 누름) · 흐름도 패널 자체 접기 단추는 안 보임(dfpToggle 0) + * flow = {flow:true, source:false, pref:"flow", caret:["▾","▸"]} · 오른쪽 칸 너비 > 300 + * source = {flow:false, source:true, pref:"source", caret:["▸","▾"]} · srcHead 에 「1-1-2 유도선」 · srcTable 에 「특별인부」 + * none = {flow:false, source:false, pref:"none", caret:["▸","▸"]} · 오른쪽 칸 너비 = 140(제목 행 둘) + * back = {flow:true, source:false, pref:"flow"}(흐름도 제목 행으로 다시 폄) * noLink = 「원문 연결 없음」 + 「출처 · …」(서버가 빈 목록을 줄 때 · fetch 를 잠시 바꿔 봄) * restored = 처음 취향 값 */ @@ -43,17 +44,23 @@ out.order = $$(".m01lab-flow__left .m01lab__box > .m01-logic__section-head h3").map((h) => h.innerText.trim(), ); - const slots = () => $$(".m01lab-side__slot"); + const cards = () => $$(".m01lab-side__card"); + const heads = () => cards().map((c) => $(".ui-collapsible__title", c)); + const shown = (n) => !!n && n.getBoundingClientRect().height > 0; const state = () => ({ flow: - slots()[0].classList.contains("is-open") && - !$(".m02-dfp", slots()[0]).classList.contains("is-folded"), - source: slots()[1].classList.contains("is-open") && !$(".m02-dfp__open", slots()[1]).hidden, + !cards()[0].classList.contains("is-collapsed") && + shown($(".m02-dfp__open", cards()[0])) && + !$(".m02-dfp", cards()[0]).classList.contains("is-folded"), + source: !cards()[1].classList.contains("is-collapsed") && shown($(".m01lab-src", cards()[1])), pref: localStorage.getItem(PREF), + caret: heads().map((h) => getComputedStyle(h, "::after").content.replace(/"/g, "")), width: Math.round($(".m01lab-flow__right").getBoundingClientRect().width), }); - const flowToggle = () => $(".m02-dfp__toggle", slots()[0]); - const srcToggle = () => $(".m02-dfp__toggle", slots()[1]); + const flowToggle = () => heads()[0]; + const srcToggle = () => heads()[1]; + out.titles = heads().map((h) => h.innerText.trim()); + out.dfpToggle = $$(".m01lab-side .m02-dfp__toggle").filter(shown).length; // 처음 = 흐름도 열림으로 맞춤 if (!state().flow) { if (state().source) srcToggle().click(); From 9b4b56edc64907584ddbd7b0067c7bd9c298b6d2 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 5 Oct 2026 12:26:29 +0900 Subject: [PATCH 3/9] =?UTF-8?q?fix(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=ED=8C=90?= =?UTF-8?q?=20=ED=81=AC=EA=B8=B0=20=EB=B0=94=EB=80=9C=20=EB=95=8C=20?= =?UTF-8?q?=EB=8B=A4=EC=8B=9C=20=EC=A7=80=EC=96=B4=20=EB=B0=B0=EC=9C=A8=20?= =?UTF-8?q?1=20=C2=B7=20=EA=B8=80=EC=9E=90=2011px=20=EB=B3=B5=EA=B7=80=20(?= =?UTF-8?q?44-2=20=EB=8D=A7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 판이 접힌 · 좁은 때 지은 viewBox 가 펼쳐질 때 통째로 늘어나 글자가 커 보이던 길 없앰 - ResizeObserver 로 판 크기가 바뀌면 실제 크기로 다시 지음 · 확대 · 이동 상태 유지 - 최소치(240×150)는 판이 0 일 때만 · 범례 · 좌우 표고 글 11px - 44-2 시나리오 — 접힘 · 좁음 · 넓음 · 줄임 네 갈래 배율 · 글자 높이 잼 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013rJwJMVL4EE31y77TB11AP --- B05_Profile/B05_Profile_UI_RouteEdit_Cross.ts | 61 ++++--- .../B05_Profile_UI_Style_RouteEdit.css | 2 +- resources/tester/ui_checks/44-2_횡단_글자.js | 151 ++++++++++++++---- 3 files changed, 158 insertions(+), 56 deletions(-) diff --git a/B05_Profile/B05_Profile_UI_RouteEdit_Cross.ts b/B05_Profile/B05_Profile_UI_RouteEdit_Cross.ts index def15d290..1c6a3648c 100644 --- a/B05_Profile/B05_Profile_UI_RouteEdit_Cross.ts +++ b/B05_Profile/B05_Profile_UI_RouteEdit_Cross.ts @@ -16,7 +16,7 @@ * `compute_cross_design`(서버), 성토사면 길이는 B06 화면이 쓰는 `fillSlopeLengths`. * ========================================================================== */ -import { attachSvgZoomPan } from "../A00_Common/b_svg_zoom_pan"; +import { attachSvgZoomPan, type ZoomPanState } from "../A00_Common/b_svg_zoom_pan"; import { fetchCrossPreview, type CrossPreviewResponse } from "./B05_Profile_Api_Replan"; import { buildCrossSvg, @@ -97,10 +97,47 @@ function createPane(title: string, empty: string): CrossPane { const station = root.querySelector(".b05-routeedit__cross-station")!; const foot = root.querySelector(".b05-routeedit__cross-foot")!; const box = root.querySelector(".b05-routeedit__cross-box")!; + /** 그려 둔 횡단 · 그때 판 크기 · 확대·이동 — 판 크기가 바뀌면 이것으로 다시 짓는다. */ + let drawn: CrossPreviewResponse | null = null; + let size = { width: 0, height: 0 }; + let zoom: ZoomPanState | undefined; + function draw(preview: CrossPreviewResponse): void { + // 판의 실제 크기로 짓는다 — `viewBox` 가 픽셀과 1:1 이라야 휠·팬이 커서를 따라가고 + // 글자가 늘어나 보이지 않는다. 칸이 접혀 있으면(1500px 미만) 0 이 나오므로 최소치를 깔고, + // 펼쳐지면 아래 ResizeObserver 가 실제 크기로 다시 짓는다. + const rect = box.getBoundingClientRect(); + size = { width: Math.round(rect.width), height: Math.round(rect.height) }; + const plot = buildCrossSvg(preview, size.width || 240, size.height || 150); + box.replaceChildren(plot.svg); + observer.observe(box); // 창에 붙은 뒤에 지켜본다 — 붙기 전에는 isConnected 가 거짓이다. + // 확대·이동은 **공용 조각**이 맡는다 — B06 횡단 카드와 한 코드다(2026-09-12 지시 ③). + // 판이 둘뿐이라 여기서는 휠을 줌에 묶는다(카드 목록인 B06 은 안 묶는다). + attachSvgZoomPan({ + svg: plot.svg, + layer: plot.layer, + widthPx: plot.width, + heightPx: plot.height, + pad: CROSS_PLOT_PAD, + content: plot.content, + wheelZoom: true, + initial: zoom, + onChange: (state) => (zoom = state), + }); + } + const observer = new ResizeObserver(() => { + if (!box.isConnected) return observer.disconnect(); // 편집 창이 닫혔다. + const rect = box.getBoundingClientRect(); + const width = Math.round(rect.width); + const height = Math.round(rect.height); + if (!drawn || width === 0 || (width === size.width && height === size.height)) return; + draw(drawn); + }); return { root, show(preview, intervalM) { box.replaceChildren(); + drawn = preview; + zoom = undefined; if (!preview) { station.textContent = ""; foot.textContent = empty; @@ -109,30 +146,12 @@ function createPane(title: string, empty: string): CrossPane { // 측점은 **누가거리가 아니라 측점 표기**로 낸다(계획서 0-9 ㉑) — B05 왼쪽 아래 구조물 // 목록이 쓰는 그 규칙이다. 서버가 주는 `STA.0+100.000` 을 그대로 쓰면 표기가 갈린다. station.textContent = formatStation(preview.chainage_m, intervalM); - // 판의 실제 크기로 짓는다 — `viewBox` 가 픽셀과 1:1 이라야 휠·팬이 커서를 따라간다. - // 칸이 접혀 있으면(1500px 미만) 0 이 나오므로 최소치를 깐다. - const rect = box.getBoundingClientRect(); - const plot = buildCrossSvg( - preview, - Math.max(Math.round(rect.width), 240), - Math.max(Math.round(rect.height), 150), - ); - box.append(plot.svg); - // 확대·이동은 **공용 조각**이 맡는다 — B06 횡단 카드와 한 코드다(2026-09-12 지시 ③). - // 판이 둘뿐이라 여기서는 휠을 줌에 묶는다(카드 목록인 B06 은 안 묶는다). - attachSvgZoomPan({ - svg: plot.svg, - layer: plot.layer, - widthPx: plot.width, - heightPx: plot.height, - pad: CROSS_PLOT_PAD, - content: plot.content, - wheelZoom: true, - }); + draw(preview); foot.textContent = summarizeCross(preview); }, wait(text) { box.replaceChildren(); + drawn = null; foot.textContent = text; }, }; diff --git a/B05_Profile/B05_Profile_UI_Style_RouteEdit.css b/B05_Profile/B05_Profile_UI_Style_RouteEdit.css index a7ebab158..506fa6eb1 100644 --- a/B05_Profile/B05_Profile_UI_Style_RouteEdit.css +++ b/B05_Profile/B05_Profile_UI_Style_RouteEdit.css @@ -235,7 +235,7 @@ .b05-routeedit__cross-key { fill: var(--color-text-secondary, #94a3b8); - font-size: 5.5px; /* 44-2 — 지금(11px)의 절반 */ + font-size: 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 index d5d0f35fc..0e69f47d7 100644 --- a/resources/tester/ui_checks/44-2_횡단_글자.js +++ b/resources/tester/ui_checks/44-2_횡단_글자.js @@ -1,11 +1,16 @@ -/* 44-2 — B05 계획노선 편집 · 측점 횡단 글자 크기 +/* 44-2 — B05 계획노선 편집 · 측점 횡단 글자 크기 · 판 크기 바뀜에도 배율 1 * 시작 화면: B05 종단설계(#/b05-profile) · 환경 검증 프로젝트(OWNERS.md) · 로그인됨 · 계획노선 편집 닫힘 + * · 창 너비 1500px 이상(그 아래면 오른쪽 칸이 접힘) * 부르는 법: orca eval --expression "$(cat 이 파일)" (async 식 · JSON 글을 돌려줌) - * 하는 일: [계획노선 편집] 열기 → 첫 측점 라벨 누름(앱 모듈의 라벨 상자 자리) → 횡단 판 글자 잼 → [취소]로 닫음 - * 기대 수치: - * - keys = 원지반 · 기본 계획 횡단 · 「좌 … 표고 …」 셋 모두 fontSize 5.5px(옛 11px 의 절반) + * 하는 일: [계획노선 편집] 열기 → 오른쪽 칸 크기를 세 갈래로 둔 채 측점 라벨 누름 → 칸을 원래대로 → 잼 + * folded = 칸 접힘(display none) 상태로 셈 → 펼침 · narrow = 260×300 으로 셈 → 원래 + * wide = 원래 크기로 셈 · shrink = wide 뒤 260×300 으로 줄임 → [취소]로 닫음 + * · 탭이 화면에 안 보여 프레임이 없으면(frames = false) ResizeObserver 대역(100ms 재기)을 깔고 돎 + * 기대 수치(갈래마다): + * - sx · sy = 화면 크기 ÷ viewBox = 1(±0.02 · 글자가 늘어나 보이지 않음) + * - keys = 원지반 · 기본 계획 횡단 · 「좌 … 표고 …」 셋 모두 fontSize 11px · screenPx(11 × sy) 10~12 * - station.fontSize = title.fontSize(측점 표기 = 제목 「횡단」 크기) - * - 바꾸는 값 없음(취소로 닫음 · 노선 안 고침) + * - 바꾸는 값 없음(칸 크기 임시 style 지움 · 취소로 닫음 · 노선 안 고침) */ (async () => { const w = (ms) => new Promise((r) => setTimeout(r, ms)); @@ -21,13 +26,50 @@ [...document.querySelectorAll("button")].find( (b) => b.offsetParent && b.textContent.trim() === text, ); + // 화면에 안 보이는 ORCA 탭은 프레임을 안 그려 ResizeObserver 가 안 옴 — 그때만 100ms 마다 + // 크기를 재어 같은 방식(처음 한 번 · 바뀔 때마다)으로 부르는 대역을 편집 창 열기 전에 깖 + const OrigRO = window.ResizeObserver; + const frames = await Promise.race([ + new Promise((r) => requestAnimationFrame(() => r(true))), + w(500).then(() => false), + ]); + if (!frames) + window.ResizeObserver = class { + constructor(cb) { + this.cb = cb; + this.t = new Map(); + this.iv = setInterval(() => { + for (const [el, last] of this.t) { + const r = el.getBoundingClientRect(); + const k = `${r.width}x${r.height}`; + if (k === last) continue; + this.t.set(el, k); + this.cb([{ target: el }], this); + } + }, 100); + } + observe(el) { + if (!this.t.has(el)) this.t.set(el, null); + } + unobserve(el) { + this.t.delete(el); + } + disconnect() { + this.t.clear(); + clearInterval(this.iv); + } + }; btn("계획노선 편집")?.click(); const canvas = await until(() => [...document.querySelectorAll("canvas")].find( (c) => c.offsetParent && c.closest("[class*='routeedit']"), ), ); + window.ResizeObserver = OrigRO; // 편집 창(횡단 판)은 다 지어짐 if (!canvas) return JSON.stringify({ ok: false, why: "편집 캔버스 없음" }); + const side = document.querySelector(".b05-routeedit__side"); + if (!side || !side.offsetParent) + return JSON.stringify({ ok: false, why: "오른쪽 칸 접힘", innerWidth }); // 앱이 실제로 부른 그 모듈 주소(@fs/<폴더>/…) — 같은 주소라야 라벨 상자 WeakMap 을 같이 씀 const url = performance .getEntriesByType("resource") @@ -39,34 +81,75 @@ 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) }, + const pane = side.querySelector(".b05-routeedit__cross"); + const draw = async () => { + const old = pane.querySelector("svg"); + 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)); + return until(() => { + const svg = pane.querySelector("svg"); + return svg && svg !== old && svg.querySelector(".b05-routeedit__cross-key"); + }); }; - out.ok = - keys.length === 3 && - keys.every((k) => k.fontSize === "5.5px") && - out.title.fontSize === out.station.fontSize; - btn("취소")?.click(); - return JSON.stringify(out); + const fs = (el) => el && getComputedStyle(el).fontSize; + const measure = (tag) => { + const svg = pane.querySelector("svg"); + const vb = svg.viewBox.baseVal; + const r = svg.getBoundingClientRect(); + const sx = +(r.width / vb.width).toFixed(3); + const sy = +(r.height / vb.height).toFixed(3); + const keys = [...pane.querySelectorAll(".b05-routeedit__cross-key")].map((t) => ({ + text: t.textContent.slice(0, 12), + fontSize: fs(t), + screenPx: +(parseFloat(fs(t)) * sy).toFixed(1), + h: +t.getBoundingClientRect().height.toFixed(1), + })); + const title = pane.querySelector(".b05-routeedit__cross-title"); + const station = pane.querySelector(".b05-routeedit__cross-station"); + const res = { + tag, + viewBox: [vb.width, vb.height], + box: [Math.round(r.width), Math.round(r.height)], + sx, + sy, + keys, + title: fs(title), + station: fs(station), + }; + res.ok = + Math.abs(sx - 1) <= 0.02 && + Math.abs(sy - 1) <= 0.02 && + keys.length === 3 && + keys.every((k) => k.fontSize === "11px" && k.screenPx >= 10 && k.screenPx <= 12) && + res.title === res.station; + return res; + }; + const narrow = () => Object.assign(side.style, { width: "260px", height: "300px" }); + const reset = () => Object.assign(side.style, { display: "", width: "", height: "" }); + const out = []; + try { + side.style.display = "none"; + if (!(await draw())) return JSON.stringify({ ok: false, why: "접힘 · 횡단 안 뜸" }); + reset(); + await w(600); + out.push(measure("folded")); + narrow(); + if (!(await draw())) return JSON.stringify({ ok: false, why: "좁음 · 횡단 안 뜸" }); + reset(); + await w(600); + out.push(measure("narrow")); + if (!(await draw())) return JSON.stringify({ ok: false, why: "넓음 · 횡단 안 뜸" }); + await w(300); + out.push(measure("wide")); + narrow(); + await w(600); + out.push(measure("shrink")); + } finally { + reset(); + btn("취소")?.click(); + } + return JSON.stringify({ ok: out.every((o) => o.ok), innerWidth, frames, out }); })(); From b94259f3b5fd599306a0b3f18298babc387a1bf7 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 5 Oct 2026 12:26:41 +0900 Subject: [PATCH 4/9] auto: 2026-10-05 12:26 (ESD_LAPTOP) --- docs/raw/사용자요구/2026-10-05_수정요구.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/raw/사용자요구/2026-10-05_수정요구.md b/docs/raw/사용자요구/2026-10-05_수정요구.md index eec77747b..305ec2bf6 100644 --- a/docs/raw/사용자요구/2026-10-05_수정요구.md +++ b/docs/raw/사용자요구/2026-10-05_수정요구.md @@ -29,3 +29,9 @@ - CAD 프로그램 : 한글입력 안됨. 계획서를 작성하고 코드작업을 진행해줘. 난 이제 자리를 비워야하니까 참고해. + +## 추가작업 (10-5 · 원문 그대로) + +1. M02 페이지의 전체영역에서 알약 템플릿으로 프론트엔드에 사용중인데 버튼들은 기본 버튼 템플릿을 사용하게 변경해서 알약 템플릿은 알약에만 사용하는게 맞아. +2. M02 페이지의 좌측패널 내부 별도의 서브 컨테이너로 구조물도면, 일위대가, 면적산출 구성되어 있는데 이건 필요없어 보임 접근은 구조물 조합을 선택하면 바로 3개의 항목 연결이 보이니 불필요해보여. +3. 구조물 조합 상세 페이지의 구조물 분류 테이블에 일위대가 도면 면적산출 행에 키값들을 노출하고 있는데 이건 필요없어보여. From 966669bb3489915de04b416110f71062e8fcb5b2 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 5 Oct 2026 12:28:04 +0900 Subject: [PATCH 5/9] =?UTF-8?q?feat(M02):=2045-3=20=EA=B5=AC=EC=A1=B0?= =?UTF-8?q?=EB=AC=BC=20=EC=A1=B0=ED=95=A9=20=EB=B6=84=EB=A5=98=20=ED=91=9C?= =?UTF-8?q?=20=EC=9D=BC=EC=9C=84=EB=8C=80=EA=B0=80=20=C2=B7=20=EB=8F=84?= =?UTF-8?q?=EB=A9=B4=20=C2=B7=20=EB=A9=B4=EC=A0=81=EC=82=B0=EC=B6=9C=20?= =?UTF-8?q?=ED=82=A4=20=EC=A4=84=20=EC=88=A8=EA=B9=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 표 부품 combo 일 때 마스터 번호 줄 안 그림(보기만 · 장부 · 저장 문서 그대로) - 번호 글 폭 맞춤(fitMeta) · 전용 CSS 없앰 · 방향키는 머리 줄만 - 시험 test_m02_combo_table 지금 코드에 맞춤 · 시나리오 ui_checks/45-3_분류표_키줄.js Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EYJJRCfHfdCGNhmXTjQKV7 --- resources/tester/test_m02_combo_table.py | 13 ++-- .../tester/ui_checks/45-3_분류표_키줄.js | 72 +++++++++++++++++++ ui_template/sheet/ui_template_sheet.css | 4 -- ui_template/sheet/ui_template_sheet.ts | 3 +- ui_template/sheet/ui_template_sheet_render.ts | 18 +---- 5 files changed, 86 insertions(+), 24 deletions(-) create mode 100644 resources/tester/ui_checks/45-3_분류표_키줄.js diff --git a/resources/tester/test_m02_combo_table.py b/resources/tester/test_m02_combo_table.py index 11619815b..f6d74b82b 100644 --- a/resources/tester/test_m02_combo_table.py +++ b/resources/tester/test_m02_combo_table.py @@ -64,17 +64,22 @@ def test_표_부품_combo_선택(): assert "combo?: { onHeadEdit?: (colId: string, level: number, name: string) => void };" in sheet assert '...(ro || opts.combo ? [] : [el("p", { className: "ui-sheet__hint"' in sheet assert ( - "filter((k) => !opts.combo || k !== KEY_UNIT)" in sheet - ) # 방향키가 숨은 단위 줄을 안 밟음 + "const keys = opts.combo ? [KEY_UNIT] : navRows(state.doc, state.mode);" in sheet + ) # 방향키가 숨은 단위 · 번호 줄을 안 밟음 assert 'head?.classList.add("is-head-picked")' in sheet assert "if (th && opts.combo?.onHeadEdit) editComboHead(th);" in sheet assert "if (opts.combo && sel.r === KEY_UNIT)" in sheet # F2 · Enter = 머리 이름 고침 render = _text(SHEET / "ui_template_sheet_render.ts") assert "children: state.combo ? rows : [...rows, unit]" in render # 단위 줄 없음 - assert "if (state.combo) fitMeta(state, cols, mins);" in render + # 45-3 일위대가 · 도면 · 면적산출 키 줄 안 보임(보기만) + assert ( + '...(mode === "master" && p === 0 && !state.combo ? MASTER_ROWS.map(masterRow) : [])' + in render + ) + assert "fitMeta" not in render assert "열너비: {}" in render # 저장 폭 안 씀 — 자동 css = _text(SHEET / "ui_template_sheet.css") - assert ".ui-sheet--combo .ui-sheet__meta td" in css and "th.is-head-picked" in css + assert ".ui-sheet--combo .ui-sheet__meta td" not in css and "th.is-head-picked" in css def test_머리_고침은_쓸_수_있는_층만_나무로(): diff --git a/resources/tester/ui_checks/45-3_분류표_키줄.js b/resources/tester/ui_checks/45-3_분류표_키줄.js new file mode 100644 index 000000000..a62c5f083 --- /dev/null +++ b/resources/tester/ui_checks/45-3_분류표_키줄.js @@ -0,0 +1,72 @@ +/* 45-3 — 구조물 조합 상세 분류 표: 일위대가 · 도면 · 면적산출 키 줄 안 보임 · [저장] 뒤 장부 번호 그대로 + * 시작 화면: M02 마스터 템플릿 · 마스터 층(#/m02-master-template?open=구조물조합&root=master) · 시스템 관리자 + * 부르는 법: orca eval --expression "$(cat 이 파일)" (async 식 · JSON 글 · 20초 안) + * 쓰는 것: 표 [저장] 한 번(고침 없음) — 끝나면 resources/master_template 사본으로 되씀 + * 기대 수치: + * - metaRows = 0 · refRowNames = 0(줄 머리 「일위대가 · 도면 · 면적산출」) · refCells = 0(data-r s:price|drawing|area) + * - headPick = true(머리 칸 누르면 열 고름 · 카드 셋) · keyMove = true(방향키로 옆 열) + * - book.same = true(저장 앞뒤 장부 줄의 키 · 열 · 도번 · 면적번 같음) · book.laborAfter = rows(일위대가번 모두 있음) + * (git 장부가 옛 꼴(재료번)이면 저장 때 서버 맞춤이 일위대가번으로 바꿈 — 보기와 무관) + */ +(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 vis = (b) => b && !b.hidden && b.offsetParent !== null; + const sheet = await until(() => document.querySelector(".ui-sheet--combo")); + if (!sheet) return JSON.stringify({ error: "구조물 조합 표 없음" }); + const out = {}; + const gutters = () => + [...sheet.querySelectorAll(".ui-sheet__gutter")].map((g) => g.textContent.trim()); + out.metaRows = sheet.querySelectorAll(".ui-sheet__meta").length; + out.refRowNames = gutters().filter((t) => ["일위대가", "도면", "면적산출"].includes(t)).length; + out.refCells = sheet.querySelectorAll( + '[data-r="s:price"],[data-r="s:drawing"],[data-r="s:area"]', + ).length; + out.gutters = gutters(); + + // 머리 칸 누르면 열 고름(카드) · 방향키 = 옆 열 + const heads = [...sheet.querySelectorAll("thead th[data-col]")].filter(vis); + const deepest = Math.max(...heads.map((h) => Number(h.dataset.level))); + const leaves = heads.filter((h) => Number(h.dataset.level) === deepest); + leaves[0].click(); + await w(400); + out.headPick = !!sheet.querySelector("th.is-head-picked"); + const first = sheet.querySelector("th.is-head-picked")?.dataset.col; + sheet.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowRight", bubbles: true })); + await w(300); + const second = sheet.querySelector("th.is-head-picked")?.dataset.col; + out.keyMove = !!first && !!second && first !== second; + + // [저장] 앞뒤 장부(구조물집계표조합) 번호 + const book = () => + fetch("/api/m02/layers/system/structure-combos", { credentials: "include" }) + .then((r) => r.json()) + .then((b) => + (b.줄 ?? []).map((r) => ({ + id: JSON.stringify([r.키, r.열, r.도번, r.면적번]), + su: r.일위대가번, + })), + ); + const before = await book(); + const save = [...document.querySelectorAll("button")].find( + (b) => vis(b) && b.innerText.trim() === "저장", + ); + save.click(); + await w(2500); + const after = await book(); + out.book = { + rows: before.length, + same: JSON.stringify(before.map((r) => r.id)) === JSON.stringify(after.map((r) => r.id)), + laborAfter: after.filter((r) => r.su).length, + sample: after[0], + }; + out.metaAfterSave = document.querySelectorAll(".ui-sheet--combo .ui-sheet__meta").length; + return JSON.stringify(out); +})(); diff --git a/ui_template/sheet/ui_template_sheet.css b/ui_template/sheet/ui_template_sheet.css index aa97203d9..6f2beb712 100644 --- a/ui_template/sheet/ui_template_sheet.css +++ b/ui_template/sheet/ui_template_sheet.css @@ -141,10 +141,6 @@ outline-offset: -2px; } -.ui-sheet--combo .ui-sheet__meta td { - white-space: nowrap; -} - .ui-sheet__table tr.is-fixed td { border-bottom: 2px solid var(--color-border); } diff --git a/ui_template/sheet/ui_template_sheet.ts b/ui_template/sheet/ui_template_sheet.ts index b8f38ebe6..418dcb355 100644 --- a/ui_template/sheet/ui_template_sheet.ts +++ b/ui_template/sheet/ui_template_sheet.ts @@ -222,7 +222,8 @@ export function createSheet(host: HTMLElement, input: SheetDoc, opts: SheetOptio opts.onSelect?.(selectionOf(state.doc, r, c)); }; const move = (dr: number, dc: number): void => { - const keys = navRows(state.doc, state.mode).filter((k) => !opts.combo || k !== KEY_UNIT); + // 구조물 조합 = 머리만(단위 · 번호 줄 없음) — 방향키는 열만 옮김(머리 = 단위 줄 자리) + const keys = opts.combo ? [KEY_UNIT] : navRows(state.doc, state.mode); const cols = state.doc.열; if (!state.sel) { if (keys.length && cols.length) select(keys[0], cols[0].id, true); diff --git a/ui_template/sheet/ui_template_sheet_render.ts b/ui_template/sheet/ui_template_sheet_render.ts index e9992bbf7..8ef33269c 100644 --- a/ui_template/sheet/ui_template_sheet_render.ts +++ b/ui_template/sheet/ui_template_sheet_render.ts @@ -47,7 +47,7 @@ export interface RenderState { pick?: boolean; /** 머리 · 층 이름을 못 고침(`lockHead`) — 칸 겉모양만. */ headLocked?: boolean; - /** 구조물 조합 표(`combo`) — 단위 줄 없음 · 열 폭 = 머리 · 번호 글이 한 줄에 들게(저장 폭 안 씀) · + /** 구조물 조합 표(`combo`) — 단위 줄 · 일위대가 · 도면 · 면적산출 줄 없음 · 열 폭 = 머리(저장 폭 안 씀) · * `headEditable` = 머리 이름 칸을 고칠 수 있음(나무 이름 바꾸기로). */ combo?: boolean; headEditable?: boolean; @@ -68,18 +68,6 @@ const columnWidth = (doc: SheetDoc, col: SheetColumn, mins: Map) mins.get(col.id) ?? 0, ); -/** 구조물 조합 — 일위대가 · 도면 · 면적산출 줄 글(키 · 번호)이 줄바꿈 없이 들게 최소 폭을 넓힘 - * (`.ui-sheet__meta td` 글꼴과 같게 잼). */ -function fitMeta(state: RenderState, cols: SheetColumn[], mins: Map): void { - const rem = parseFloat(getComputedStyle(document.documentElement).fontSize) || 16; - const font = `400 ${rem * 0.76}px ${getComputedStyle(document.body).fontFamily}`; - for (const col of cols) { - const texts = REF_ROWS.map((key) => refText(state, key, col.id)); - const need = Math.ceil(Math.max(...texts.map((t) => textWidth(t, font)))) + 14; - mins.set(col.id, Math.max(mins.get(col.id) ?? 0, need)); - } -} - /** 머리 칸 글꼴 — 폭 재기용(`.ui-sheet__table thead` 와 같게). */ function headFont(): string { const rem = parseFloat(getComputedStyle(document.documentElement).fontSize) || 16; @@ -113,7 +101,6 @@ export function renderSheet(state: RenderState): HTMLElement { const names = columnNames(cols); const font = headFont(); const mins = headMinWidths(cols, depth, font); - if (state.combo) fitMeta(state, cols, mins); const widthDoc = state.combo ? { ...doc, 보기: { ...doc.보기, 열너비: {} } } : doc; // 줄 머리 칸 폭 — 층 이름 · 단위 · 일위대가 · 도면 · 면적산출 · 합계 이름이 안 잘리게 const GUTTER = Math.min( @@ -275,7 +262,8 @@ ${formulaLabel(formula, names)}` for (const [p, page] of plan.entries()) { const body = el("tbody", { children: [ - ...(mode === "master" && p === 0 ? MASTER_ROWS.map(masterRow) : []), + // 구조물 조합 = 일위대가 · 도면 · 면적산출 키 줄 안 보임(보기만 · 번호는 장부 · 카드에) + ...(mode === "master" && p === 0 && !state.combo ? MASTER_ROWS.map(masterRow) : []), ...page.rows.map((row, i) => dataRow(row, page.first + i)), ...(page.totals ? totalRows() : []), ], From 407481dc8dc0d67d38da2940d86d53484728e269 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 5 Oct 2026 12:32:50 +0900 Subject: [PATCH 6/9] =?UTF-8?q?feat(M02):=20=EC=A2=8C=EC=B8=A1=20=EA=B5=AC?= =?UTF-8?q?=EC=A1=B0=EB=AC=BC=20=EC=A1=B0=ED=95=A9=20=EC=95=88=20=ED=95=98?= =?UTF-8?q?=EC=9C=84=20=EC=BB=A8=ED=85=8C=EC=9D=B4=EB=84=88=20=EC=85=8B=20?= =?UTF-8?q?=C2=B7=20=E3=80=8C=EC=A0=84=EC=B2=B4=20=ED=95=AD=EB=AA=A9?= =?UTF-8?q?=E3=80=8D=20=EA=B8=B8=20=EC=97=86=EC=95=B0(45-2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 좌측 구조물 조합 = 층 단추 · 나무만 · 구조물 도면 · 일위대가 · 면적산출 하위와 수평선 지움 - 「전체 항목」 목록 화면 길(onAll · buildAllItems) 지움 · 상세는 카드 [수정]으로 - 쓰는 곳 없는 structureLabel · 로케일 키 다섯 · 하위 CSS 지움 - 낡은 글 찾기 시험을 지금 코드에 맞춤 · 화면 시나리오 ui_checks/45-2 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01BVPp2o6Mh2FZJr72yW3ofZ --- .../M02_MasterTemplete_ItemList.ts | 20 +-- .../M02_MasterTemplete_Structure.ts | 4 - .../M02_MasterTemplete_UI_Page.ts | 32 +---- .../M02_MasterTemplete_UI_Side.ts | 121 +----------------- .../M02_MasterTemplete_UI_Style.css | 12 -- resources/tester/test_m02_labor_tree.py | 36 +++--- .../tester/test_m02_structure_options.py | 9 +- resources/tester/test_m02_structure_panel.py | 43 ++----- .../tester/ui_checks/45-2_좌측_하위_없앰.js | 60 +++++++++ ui_template/ui_template_locale_m2.ts | 5 - 10 files changed, 110 insertions(+), 232 deletions(-) create mode 100644 resources/tester/ui_checks/45-2_좌측_하위_없앰.js diff --git a/M02_MasterTemplete/M02_MasterTemplete_ItemList.ts b/M02_MasterTemplete/M02_MasterTemplete_ItemList.ts index a51a88c67..b5b6bd70c 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_ItemList.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_ItemList.ts @@ -1,7 +1,7 @@ /* ============================================================================= * M02_MasterTemplete_ItemList.ts - * 구조물 항목 목록 하나(PLAN 39-5) — 「전체 항목」(도면 · 일위대가 · 면적산출) · 복사 생성 · 항목 하나 - * 복사가 같이 씀. 꼴 = M01 마스터 화면 — 찾기 줄(`buildHeadFilters`) · 쪽(`buildPager`) · 표 + * 구조물 항목 목록 하나(PLAN 39-5) — 복사 생성 · 항목 하나(도면 · 일위대가 · 면적산출) 복사가 같이 씀. + * 꼴 = M01 마스터 화면 — 찾기 줄(`buildHeadFilters`) · 쪽(`buildPager`) · 표 * (`m01-master__grid`) 를 M01 파일에서 그대로 import(옮기지 않음). * 자료 = 층 나무 항목(`GET /layers/{층}/tree` — 서버 `items` 의 `수정일` · `labor` 칸) · 칸은 종류마다. * 찾기 = 보이는 칸 글 전부에서 띄어쓰기 AND(나무 찾기와 같은 `matchText`). @@ -142,22 +142,6 @@ export function pageOf( export const loadItems = async (layer: Layer, owner: string | null): Promise => (await fetchTree(layer, owner)).items as ItemRow[]; -/** 「전체 항목」 화면(39-5) — 한 층 · 한 하위 목록 · 줄 누름 = 그 항목 상세(좌측 열 줄 누름과 같음) */ -export function buildAllItems( - kind: ListKind, - layer: Layer, - owner: string | null, - onOpen: (row: ItemRow) => void, -): HTMLElement { - const list = buildItemList(); - loadItems(layer, owner).then( - (rows) => list.show(rows, ITEM_COLS[kind], onOpen), - (error: unknown) => - list.message(`${TEXT.failed} — ${error instanceof Error ? error.message : String(error)}`), - ); - return list.root; -} - export interface ItemListHandle { root: HTMLElement; /** 줄 · 칸 · 누를 일을 바꿔 그림(찾는 글은 그대로 · 쪽은 1) */ diff --git a/M02_MasterTemplete/M02_MasterTemplete_Structure.ts b/M02_MasterTemplete/M02_MasterTemplete_Structure.ts index 0bfca0ffe..11c49c3d3 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Structure.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Structure.ts @@ -143,10 +143,6 @@ export function headRenameEdit(col: StructureColumn, level: number, name: string export const columnHead = (col: StructureColumn): string => col.머리.filter((part): part is string => !!part && part.trim() !== "").join(" ") || col.id; -/** 번호가 맨 앞 — 「SD010101 돌기슭막이 H=1.0」(번호가 없으면 이름만) */ -export const structureLabel = (number: string, col: StructureColumn): string => - [number, columnHead(col)].filter(Boolean).join(" "); - /** 면적산출 편집 화면 아래 알약 막대 — 그 열 일위대가의 로직 물량 「이름|단위」 + 자재 「자재|MT키」 * (서버 pills 길 · 39-4 · 다 `고정` — 더하기 · 빼기는 일위대가 쪽). * 알약이 없으면 까닭 글(`notice`)을 같이 돌려줌 */ diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts b/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts index 21b36e7fe..e18034463 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts @@ -15,7 +15,7 @@ import { t as L } from "@ui/ui_template_locale"; import { createWorkflowOverlays } from "@ui/ui_template_overlay"; import { navigateTo, type MountedPage } from "../A00_Common/router"; import { fetchSessionUser } from "../A06_Login/A06_Login_Api_Fetch"; -import { fetchOwners, setActiveLayer, type Kind } from "./M02_MasterTemplete_Api_Fetch"; +import { fetchOwners, setActiveLayer } from "./M02_MasterTemplete_Api_Fetch"; import { accessFor, buildReadOnlyBar, @@ -40,8 +40,7 @@ import { seesOthers, } from "./M02_MasterTemplete_TreePanel_Roots"; import { canEdit, createMain, newItemButton, type Selection } from "./M02_MasterTemplete_UI_Main"; -import { buildSide, type AllKind } from "./M02_MasterTemplete_UI_Side"; -import { buildAllItems } from "./M02_MasterTemplete_ItemList"; +import { buildSide } from "./M02_MasterTemplete_UI_Side"; import { buildTwoWay, pickedName } from "./M02_MasterTemplete_ItemPick"; import "./M02_MasterTemplete_UI_Style.css"; @@ -49,13 +48,6 @@ const NEW_NOTE = "새 항목이 들어갈 자리 — {value} · 나무에서 폴 const NEW_ASK = "{from} 「{name}」 을(를) 복사해 「{to}」 에 새 항목으로 만듦 — 새 번호 · 원래 이름"; -/** 「전체 항목」 화면 제목 앞마디 — 하위 이름 */ -const ALL_TITLE = { - structure: "M02_KindStructure", - labor: "M02_KindLabor", - area: "M02_KindArea", -} as const satisfies Record; - export async function renderM02MasterTemplate(root: HTMLElement): Promise { const user = await fetchSessionUser().catch(() => null); const entry = entryFromHash(); @@ -97,9 +89,9 @@ export async function renderM02MasterTemplate(root: HTMLElement): Promise void main.openTable(layer, item.열), settle: () => main.settle(), - // 나무가 바뀜(새 항목 · 복사 생성 · 붙여넣기 · 지우기 · 이름 · 옮기기) — 집계표 + 좌측 하위 열 줄 · 번호 + // 나무가 바뀜(새 항목 · 복사 생성 · 붙여넣기 · 지우기 · 이름 · 옮기기) — 집계표 onChanged: async (colId) => { - await Promise.all([main.openTable(layer, colId, true), side.refresh()]); + await main.openTable(layer, colId, true); }, }); const go = async (next: Root, nextOwner: string | null, open: OpenTarget): Promise => { @@ -110,7 +102,7 @@ export async function renderM02MasterTemplate(root: HTMLElement): Promise void): HTMLElement => @@ -193,20 +185,6 @@ export async function renderM02MasterTemplate(root: HTMLElement): Promise void main.openTable(layer)), peopleTree("구조물조합", tree.root), ], - // 「전체 항목」(39-5) — 메인 칸에 그 하위 목록 · 줄 누름 = 그 항목 상세(좌측 열 줄과 같은 `open`) - onAll: (kind) => - void main.showView( - `${L(ALL_TITLE[kind])} · ${L("M02_ItemsAll")}`, - buildAllItems(kind, layer, owner, (row) => { - void main.open({ - layer, - projectId: null, - kind: kind as Kind, - name: row.열, - label: row.이름, - }); - }), - ), }); const layout = el("div", { className: "ui-workflow-layout m02-master" }); diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Side.ts b/M02_MasterTemplete/M02_MasterTemplete_UI_Side.ts index 0d2b83eb8..ad7e1b396 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Side.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Side.ts @@ -1,11 +1,8 @@ /* ============================================================================= * M02_MasterTemplete_UI_Side.ts - * 마스터 템플릿 좌측 패널 — 「구조물 조합」(층 단추 · 나무 → 수평선 → 하위 구조물 도면 · 일위대가 · - * 면적산출 · 하위마다 맨 위 「전체 항목」) · 「도면 양식」(새로 · 본떠 · 지우기). + * 마스터 템플릿 좌측 패널 — 「구조물 조합」(층 단추 · 나무) · 「도면 양식」(새로 · 본떠 · 지우기). * 서버 길은 계약 `6_계약.md`. - * 구조물 도면 · 일위대가 · 면적산출 줄은 집계표 구조물 열(꼴=수)마다 하나씩 — - * 서버가 미리 만들어 둠(도번 SD · 일위대가번 SU · 면적번 SA + 같은 뒷자리) · - * 줄 글은 번호가 맨 앞(「SD01010100 돌기슭막이 …」). + * 구조물 도면 · 일위대가 · 면적산출 상세는 좌측 줄이 아니라 항목 카드 [수정]으로(PLAN 45-2). * 화면 이름 「구조물 조합」(PLAN 26-7a) — 파일 이름 · 양식 키 · API 길은 「구조물집계표」 그대로. * 구조물 라이브러리는 좌측이 아니라 「구조물 조합」 상세 화면 왼쪽(PLAN 26-7c · 페이지가 붙임). * ========================================================================== */ @@ -22,23 +19,15 @@ import { t as L } from "@ui/ui_template_locale"; import { openModal } from "@ui/ui_template_modal"; import { deleteTemplate, - fetchStructureNumbers, listTemplates, readTemplate, saveTemplate, StaleError, type Kind, type Layer, - type StructureNumbers, type TemplateInfo, } from "./M02_MasterTemplete_Api_Fetch"; -import { - STRUCTURE_TABLE, - columnHead, - isStructureColumn, - structureLabel, - type StructureColumn, -} from "./M02_MasterTemplete_Structure"; +import { STRUCTURE_TABLE } from "./M02_MasterTemplete_Structure"; import { buildOptionsSection } from "./M02_MasterTemplete_Options"; import { buildPracticeSection } from "./M02_MasterTemplete_Practice"; import { canEdit, type Selection } from "./M02_MasterTemplete_UI_Main"; @@ -47,21 +36,6 @@ const KINDS: { kind: Kind; label: () => string }[] = [ { kind: "table", label: () => L("M02_KindTable") }, { kind: "drawing", label: () => L("M02_KindDrawing") }, ]; -/** 구조물 열마다 한 줄씩인 하위 목록 — 종류(편집 칸이 여는 `Kind`) · 번호 칸 · 제목 · - * `all` = 맨 위 「전체 항목」 줄(PLAN 39-2) · 차례 = 구조물 도면 → 일위대가(SU) → 면적산출 */ -type ColumnKind = "structure" | "labor" | "area"; -const COLUMN_KINDS: { - kind: ColumnKind; - key: keyof StructureNumbers; - label: () => string; - all: boolean; -}[] = [ - { kind: "structure", key: "도번", label: () => L("M02_KindStructure"), all: true }, - { kind: "labor", key: "일위대가번", label: () => L("M02_KindLabor"), all: true }, - { kind: "area", key: "면적번", label: () => L("M02_KindArea"), all: true }, -]; -/** 「전체 항목」 줄이 있는 하위 — 누르면 H 목록 화면(`onAll`) */ -export type AllKind = "structure" | "labor" | "area"; /** 새 도면 양식의 첫 문서 — 빈 웹캐드 문서 */ const STUB_DRAWING = { format: 6, @@ -84,8 +58,6 @@ export interface SideOptions { showView: (label: string, node: HTMLElement) => (dirty: boolean) => void; /** 「구조물 조합」 줄 아래 — 층 단추 · 고른 층의 구조물 라이브러리 나무(PLAN 29-2 · 페이지가 만듦) */ tree?: HTMLElement[]; - /** 하위 맨 위 「전체 항목」 줄 — 메인 칸에 그 하위 목록(화면 = `ItemList` · 페이지가 이음) */ - onAll?: (kind: AllKind) => void; } export interface SideHandle { @@ -111,10 +83,6 @@ const failed = (error: unknown): void => export function buildSide(opt: SideOptions): SideHandle { const layer = opt.layer; let items: TemplateInfo[] = []; - let columns: StructureColumn[] = []; - let numbers: Record = {}; - /** 지금 연 「전체 항목」 — 양식 · 다른 보기 화면이 열리면 지움 */ - let allPicked: AllKind | null = null; let seq = 0; const section = (title: string, body: HTMLElement, ...more: HTMLElement[]): HTMLElement => @@ -125,9 +93,6 @@ export function buildSide(opt: SideOptions): SideHandle { const bodies = new Map( KINDS.map((k) => [k.kind, el("div", { className: "m02-side__items ui-sidebar-list" })]), ); - const columnBodies = COLUMN_KINDS.map(() => - el("div", { className: "m02-side__items ui-sidebar-list" }), - ); /** 새로 · 본떠 만들기 · 지우기 — 도면 양식 컨테이너 안(구조물 집계 양식은 집계표 하나라 없음) · 관리자만 */ const editButtons = new Map(); const actionRow = (kind: Kind): HTMLElement[] => { @@ -146,7 +111,7 @@ export function buildSide(opt: SideOptions): SideHandle { isAdmin: canEdit(layer, opt.isAdmin), confirmLeave: opt.confirmLeave, show: opt.showView, - onOpen: () => (practice?.clear(), (allPicked = null), paint()), + onOpen: () => (practice?.clear(), paint()), }); /** 컨테이너 제목을 펼치면 그 화면을 엶 — 같은 이름 줄 단추를 갈음(PLAN 29-6) */ const openOnExpand = (sec: HTMLElement, run: () => void): HTMLElement => { @@ -155,17 +120,13 @@ export function buildSide(opt: SideOptions): SideHandle { }); return sec; }; - const columnSections = COLUMN_KINDS.map((k, i) => section(k.label(), columnBodies[i])); - // 구조물 집계 양식 — 층 단추 · 나무 → 수평선 → 하위 컨테이너(들여 · 같이 접힘) · 처음부터 펼침(집계표가 열려 있음) · + // 구조물 집계 양식 — 층 단추 · 나무 · 처음부터 펼침(집계표가 열려 있음) · // 같은 이름 줄 단추 없음(제목이 그 이름 · 29-6) const summary = section( L("M02_KindTable"), el("div", { className: "m02-side__items ui-sidebar-list", children: opt.tree ?? [] }), - el("hr", { className: "m02-side__rule" }), - ...columnSections, ); summary.classList.remove("is-collapsed"); - summary.classList.add("m02-side__summary"); openOnExpand(summary, async () => { if (!isPicked("table", STRUCTURE_TABLE) && (await opt.confirmLeave())) opt.onOpen(null); }); @@ -183,7 +144,7 @@ export function buildSide(opt: SideOptions): SideHandle { ? buildPracticeSection({ confirmLeave: opt.confirmLeave, show: opt.showView, - onOpen: () => (options.clear(), (allPicked = null), paint()), + onOpen: () => (options.clear(), paint()), }) : null; const listHost = el("div", { @@ -234,94 +195,27 @@ export function buildSide(opt: SideOptions): SideHandle { } } - /** 맨 위 「전체 항목」 줄(PLAN 39-2) — 누르면 그 하위 목록(`onAll`) */ - function allRow(kind: AllKind): HTMLElement { - const b = el("button", { - className: `m02-side__item m02-side__all ui-sidebar-row${allPicked === kind ? " is-active" : ""}`, - text: L("M02_ItemsAll"), - attrs: { type: "button", "data-all": kind }, - }); - b.addEventListener("click", () => void selectAll(kind)); - return b; - } - - /** 하위 줄 — 「전체 항목」 + 구조물 열마다 하나 · 「번호 이름 변수값」(번호가 없으면 「없음」) */ - function paintRows(body: HTMLElement, k: (typeof COLUMN_KINDS)[number]): void { - const { kind, key } = k; - const head = k.all ? [allRow(kind as AllKind)] : []; - body.replaceChildren( - ...head, - ...(columns.length - ? columns.map((col) => { - const num = numbers[col.id]?.[key] || L("M02_StructureNone"); - const b = el("button", { - className: `m02-side__item ui-sidebar-row${isPicked(kind, col.id) ? " is-active" : ""}`, - text: structureLabel(num, col), - attrs: { type: "button" }, - }); - b.addEventListener("click", () => void selectColumn(col, kind)); - return b; - }) - : [el("p", { className: "m02-side__empty", text: L("M02_NoTemplates") })]), - ); - } - const paintColumns = (): void => COLUMN_KINDS.forEach((k, i) => paintRows(columnBodies[i], k)); - /** 본떠 만들기 · 지우기 — 그 컨테이너에서 고른(열린) 양식이 있을 때만 켜짐 */ function paintButtons(): void { const cur = currentInfo(); for (const [kind, b] of editButtons) b.copy.disabled = b.del.disabled = cur?.종류 !== kind; } - /** 고름 표시는 여기서만 — 목록 · 열 줄 · 옵션 줄을 함께 그려야 두 줄이 동시에 안 남음 + /** 고름 표시는 여기서만 — 목록 · 옵션 줄을 함께 그려야 두 줄이 동시에 안 남음 * (양식이 열려 있으면 옵션 · 일위대가 조합 고름은 지움) */ function paint(): void { - if (opt.getOpen()) allPicked = null; paintList(); - paintColumns(); paintButtons(); if (opt.getOpen()) options.clear(); if (opt.getOpen()) practice?.clear(); } - async function selectAll(kind: AllKind): Promise { - if (!opt.onAll || allPicked === kind || !(await opt.confirmLeave())) return; - options.clear(); - practice?.clear(); - allPicked = kind; - opt.onAll(kind); // 메인 칸 보기 화면 — 연 뒤 `paint` 가 이 줄을 고른 채 그림 - paint(); - } - // 여는 것은 편집 칸(`onOpen`) — 편집 칸이 열 때마다 `paint` 를 부름(페이지가 이음) async function select(info: TemplateInfo): Promise { if (!isOpen(info) && !(await opt.confirmLeave())) return; opt.onOpen({ layer, projectId: null, kind: info.종류, name: info.이름 }); } - async function selectColumn(col: StructureColumn, kind: ColumnKind): Promise { - if (!isPicked(kind, col.id) && !(await opt.confirmLeave())) return; - // 제목은 편집 화면이 문서 번호를 앞에 붙임(「SD010101 돌기슭막이 …」) - opt.onOpen({ layer, projectId: null, kind, name: col.id, label: columnHead(col) }); - } - - async function refreshStructure(): Promise { - try { - const [table, nums] = await Promise.all([ - readTemplate(layer, "table", STRUCTURE_TABLE, null), - fetchStructureNumbers(), - ]); - const all = ((table.문서 as { 열?: StructureColumn[] }).열 ?? []) as StructureColumn[]; - columns = all.filter(isStructureColumn); - numbers = nums; - } catch (error) { - columns = []; - numbers = {}; - showToast(L("M02_LoadFailed").replace("{value}", why(error)), "error"); - } - paintColumns(); - } - async function refresh(): Promise { const mine = ++seq; paintButtons(); @@ -334,7 +228,6 @@ export function buildSide(opt: SideOptions): SideHandle { if (mine !== seq) return; paintList(); paintButtons(); - await refreshStructure(); } /* --- 시스템 층 새로 · 본떠 · 지우기(도면 양식만 — 구조물 도면은 집계표 열이 정함) --- */ diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Style.css b/M02_MasterTemplete/M02_MasterTemplete_UI_Style.css index 63caa709f..a5b38543d 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Style.css +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Style.css @@ -10,18 +10,6 @@ font-size: var(--text-body-sm); } -/* 구조물 집계 양식 하위 컨테이너(구조물 도면 · 면적산출 · 재료산출 · 자재 조합) — 들여 씀 */ -.m02-side__summary > .ui-sidebar-card { - margin-left: var(--spacing-8); -} - -/* 구조물 조합 안 층 단추 · 나무와 하위 컨테이너 사이 수평선(PLAN 39-2) */ -.m02-side__rule { - margin: var(--spacing-8) 0; - border: 0; - border-top: 1px solid var(--color-border); -} - /* 실무문서 화면(PLAN 39-2) — 위 막대(개수 · 통째 지우기 / 목록으로 · 이름 · 정보) + 표 */ .m02-practice { display: flex; diff --git a/resources/tester/test_m02_labor_tree.py b/resources/tester/test_m02_labor_tree.py index 02df52984..4675902fc 100644 --- a/resources/tester/test_m02_labor_tree.py +++ b/resources/tester/test_m02_labor_tree.py @@ -29,24 +29,30 @@ def test_나무_부품에_일위대가_전용_칸_없음(): assert gone not in panel, gone -def test_하위_차례_전체_항목_이름(): +def test_좌측_하위_컨테이너와_전체_항목_길_없음(): + """45-2 — 구조물 조합 안 하위(구조물 도면 · 일위대가 · 면적산출 · 각 「전체 항목」) 없앰 · + 상세는 항목 카드 [수정]으로.""" side = _text("M02_MasterTemplete_UI_Side.ts") - kinds = side.split("const COLUMN_KINDS")[1].split("];")[0] - order = [kinds.index(f'kind: "{k}"') for k in ("structure", "labor", "area")] - assert order == sorted(order) # 구조물 도면 → 일위대가 → 면적산출 - assert 'kind: "labor", key: "일위대가번", label: () => L("M02_KindLabor"), all: true' in kinds - # 일위대가 줄 = 열마다 「SU-… 이름」 · 누름 = onOpen({kind: "labor", name: 열}) — 같은 selectColumn 길 - assert "opt.onOpen({ layer, projectId: null, kind, name: col.id" in side + for gone in ( + "COLUMN_KINDS", + "columnSections", + "onAll", + "allPicked", + "selectColumn", + "m02-side__rule", + ): + assert gone not in side, gone + page = _text("M02_MasterTemplete_UI_Page.ts") + assert "buildAllItems" not in page and "onAll" not in page and "ALL_TITLE" not in page + assert "buildAllItems" not in _text("M02_MasterTemplete_ItemList.ts") api = _text("M02_MasterTemplete_Api_Fetch.ts") 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 '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 + for gone in ("M02_ItemsAll", "M02_KindLabor", "M02_KindStructure", "M02_KindArea"): + assert gone not in locale, gone 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 + assert ".m02-side__rule" not in css and ".m02-side__summary" not in css def test_층_단추는_구조물_조합_한_벌(): @@ -55,11 +61,11 @@ def test_층_단추는_구조물_조합_한_벌(): assert 'layerBar("구조물조합", tree.add, () => void main.openTable(layer)),' in page -def test_나무가_바뀌면_좌측_하위_열_줄도_다시_받음(): +def test_나무가_바뀌면_집계표를_다시_엶(): """39-8 H1 — 새 항목 · 복사 생성 · 붙여넣기 · 지우기 · 이름 · 옮기기 = 나무 `apply` → - `onChanged` 한 길 · 집계표만 다시 열면 좌측 하위 줄이 새로고침 전까지 안 따라옴.""" + `onChanged` 한 길 · 좌측 하위 열 줄은 없앰(45-2).""" page = _text("M02_MasterTemplete_UI_Page.ts") changed = page.split("onChanged: async (colId) => {")[1].split("},")[0] - assert "main.openTable(layer, colId, true)" in changed and "side.refresh()" in changed + assert "main.openTable(layer, colId, true)" in changed and "side.refresh()" not in changed panel = _text("M02_MasterTemplete_TreePanel.ts") assert panel.count("await opt.onChanged(") == 1 # 나무 고침은 모두 apply 한 곳을 거침 diff --git a/resources/tester/test_m02_structure_options.py b/resources/tester/test_m02_structure_options.py index 943b45132..cd525adb7 100644 --- a/resources/tester/test_m02_structure_options.py +++ b/resources/tester/test_m02_structure_options.py @@ -125,15 +125,16 @@ def test_좌측_줄은_구조물_집계_양식_아래_한_줄_같은_한_곳에 assert ( order.index("summary") < order.index("extras.옵션, extras.도면") < order.index("practice") ) - # 실무문서를 열면 이 줄 · 「전체 항목」 고름 지움 / 이 줄을 열면 실무문서 · … 지움(자재 조합 없앰) - assert "onOpen: () => (options.clear(), (allPicked = null), paint())" in side - assert "onOpen: () => (practice?.clear(), (allPicked = null), paint())" in side + # 실무문서를 열면 이 줄 고름 지움 / 이 줄을 열면 실무문서 고름 지움(자재 조합 · 「전체 항목」 없앰) + assert "onOpen: () => (options.clear(), paint())" in side + assert "onOpen: () => (practice?.clear(), paint())" in side assert "matset" not in side paint = side.split("function paint(): void {")[1].split("\n }\n")[0] assert "if (opt.getOpen()) options.clear();" in paint # 양식을 열면 이 줄 고름 지움 api = (ROOT / "M02_MasterTemplete/M02_MasterTemplete_Api_Fetch.ts").read_text("utf-8") assert ( - 'call("/structure-options")' in api and 'call("/structure-options", { method: "PUT"' in api + 'call("/layers/system/structure-options")' in api + and 'call("/structure-options", { method: "PUT"' in api ) ui = (ROOT / "M02_MasterTemplete/M02_MasterTemplete_Options.ts").read_text("utf-8") assert 'o.phase === "detail"' in ui # B06 전용 표시 diff --git a/resources/tester/test_m02_structure_panel.py b/resources/tester/test_m02_structure_panel.py index d73ce71e0..c1fd55cea 100644 --- a/resources/tester/test_m02_structure_panel.py +++ b/resources/tester/test_m02_structure_panel.py @@ -51,10 +51,6 @@ def test_소스가_700줄을_넘지_않음(): ), # 산출근거 저장은 컨테이너가 안 함 — [산출근거 수정] 이 여는 메인 칸 전체 편집이 함 ("M02_MasterTemplete_Api_Fetch.ts", ["saveTemplate"], MAIN), - # 좌측 구조물 도면 · 면적산출 줄 번호는 한 번에 받음 - # (`{열 id: {도번, 면적번}}`) - ("M02_MasterTemplete_Api_Fetch.ts", ["fetchStructureNumbers"], SIDE), - ("M02_MasterTemplete_Structure.ts", ["structureLabel", "isStructureColumn"], SIDE), # 도면 카드(31-4)는 카드 파일에서 뗀 `DrawingCard` 가 부름 ("M02_MasterTemplete_Drawing.ts", ["mountDrawingTemplate"], DRAWING_CARD), ("../A00_Common/spreadsheet/spreadsheet.ts", ["createSpreadsheet"], CARDS), @@ -101,21 +97,15 @@ def test_집계표만_컨테이너를_붙임(): @pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음") def test_열_머리_글_잇기(): - """`columnHead` — 빈 층은 건너뛰고 빈칸으로 이음 · 다 비면 열 id · - `structureLabel` — 번호가 맨 앞(「SD010101 돌기슭막이 …」) · 번호 없으면 이름만.""" - funcs = [ - re.search(rf"^export const {name}[\s\S]*?;$", _text(PANEL), re.M) - for name in ("columnHead", "structureLabel") - ] - assert all(funcs), "columnHead · structureLabel 을 못 찾음" + """`columnHead` — 빈 층은 건너뛰고 빈칸으로 이음 · 다 비면 열 id.""" + funcs = [re.search(r"^export const columnHead[\s\S]*?;$", _text(PANEL), re.M)] + assert all(funcs), "columnHead 를 못 찾음" # 부품 파일은 화면 모듈을 부르므로 통째로 못 싣음 — 그 함수만 떼어 Node 가 타입을 벗겨 돌림 script = ( "interface StructureColumn { id: string; 머리: (string | null)[] }\n" + "\n".join(f.group(0).replace("export ", "") for f in funcs) + "\nconsole.log(JSON.stringify(process.argv.slice(1)" - + ".map((a) => columnHead(JSON.parse(a)))" - + ".concat([structureLabel('SD010101', {id: 'rv', 머리: ['돌기슭막이', null, 'H=1.0']})," - + " structureLabel('', {id: 'rv', 머리: ['돌기슭막이', null, null]})])));" + + ".map((a) => columnHead(JSON.parse(a)))));" ) cases = [ {"id": "pv_l", "머리": ["콘크리트포장", "T=(두께별)", "L"]}, @@ -133,8 +123,6 @@ def test_열_머리_글_잇기(): "콘크리트포장 T=(두께별) L", "NO", "etc", - "SD010101 돌기슭막이 H=1.0", - "돌기슭막이", ] @@ -159,16 +147,10 @@ def test_좌측_구조물_집계_양식_아래_하위_넷_새로_본떠_지우 assert "createSelectField" not in side # 새로 = 종류 고르기 없이 이름만 assert "cur?.종류 !== kind" in side # 본떠 · 지우기 = 그 컨테이너에서 고른 것이 있을 때만 assert "i.이름 === STRUCTURE_TABLE" in side # 표는 구조물집계표 한 줄만 - # 구조물 도면 · 일위대가 · 면적산출 = 구조물 집계 양식 컨테이너 안(같이 접힘) · 자재 조합 없앰 · - # 층 단추 · 나무와 하위 사이 수평선(39-2) + # 구조물 조합 컨테이너 = 층 단추 · 나무만(하위 구조물 도면 · 일위대가 · 면적산출 없앰 · 45-2) summary = side.split("const summary = section(")[1].split(");")[0] - assert ( - summary.index("opt.tree") - < summary.index("m02-side__rule") - < summary.index("...columnSections") - ) + assert "opt.tree" in summary and "columnSections" not in summary and '"hr"' not in summary assert "matset" not in side - assert "const columnSections = COLUMN_KINDS.map(" in side assert 'M02_KindTable: ["구조물 조합"' in _text( # 화면 이름(26-7a) ROOT / "ui_template" / "ui_template_locale_m2.ts" ) @@ -197,17 +179,12 @@ def test_첫_화면과_닫은_뒤는_구조물집계표(): def test_좌측_고름_표시는_열린_것_한_줄뿐(): - """카드 [도면 수정] 등 무엇으로 열든 편집 칸이 좌측 `paint` 를 부름 — 목록 · 열 줄을 함께 그림.""" + """카드 [도면 수정] 등 무엇으로 열든 편집 칸이 좌측 `paint` 를 부름 — 목록 · 옵션 줄을 함께 그림.""" side = _text(SIDE) assert "picked" not in side # 따로 든 고름 상태 없음 — 편집 칸의 지금 열린 것만 봄 - assert re.search( - r"function paint\(\): void \{\s*if \(opt\.getOpen\(\)\) allPicked = null;\s*" - r"paintList\(\);\s*paintColumns\(\);", - side, - ) # 양식이 열리면 「전체 항목」 고름도 지움(39-2) - for fn in ("async function select(", "async function selectColumn("): - body = side.split(fn)[1].split("\n }\n")[0] - assert "paint" not in body, fn # 여기서 한쪽만 그리면 두 줄이 남음 + assert re.search(r"function paint\(\): void \{\s*paintList\(\);", side) + body = side.split("async function select(")[1].split("\n }\n")[0] + assert "paint" not in body # 여기서 한쪽만 그리면 두 줄이 남음 assert re.search(r"sel = next;\s*onOpened\?\.\(\);", _text(MAIN)) # 옵션 상세도 같은 한 곳 — 열면 양식 · 열 줄 고름 지움 · 양식을 열면 옵션 고름 지움 assert re.search(r"sel = null;\s*onOpened\?\.\(\);", _text(MAIN)) diff --git a/resources/tester/ui_checks/45-2_좌측_하위_없앰.js b/resources/tester/ui_checks/45-2_좌측_하위_없앰.js new file mode 100644 index 000000000..cf7c58b55 --- /dev/null +++ b/resources/tester/ui_checks/45-2_좌측_하위_없앰.js @@ -0,0 +1,60 @@ +/* 45-2 — M02 좌측 「구조물 조합」 안 하위 컨테이너 셋(구조물 도면 · 일위대가 · 면적산출 · 「전체 항목」) 없앰 · + * 상세 길 = 나무 항목 → 카드 셋 [수정] → 상세 → [돌아가기] + * 시작 화면: M02 마스터 템플릿 · 마스터 층(#/m02-master-template?open=구조물조합&root=master) · 시스템 관리자 · + * 나무에 item1 이 있음 + * 부르는 법: orca eval --expression "$(cat 이 파일)" (async 식 · JSON 글을 돌려줌 · 20초 안) + * 기대 수치: + * - side.nested = 0 · side.all = 0 · side.rule = 0(구조물 조합 안 하위 카드 · 「전체 항목」 · 수평선 없음) + * - side.titles = ["구조물 조합", 옵션 줄, "도면 양식", "실무문서"] · side.gap > 0(카드 사이 틈 = 구분) + * - cards = 3 · edits = 3(카드마다 [수정]) + * - detail 셋 모두 title 있음 · back = true · 돌아가기 뒤 cardsAfter = 3 + */ +(async () => { + const w = (ms) => new Promise((r) => setTimeout(r, ms)); + const until = async (fn, ms = 5000) => { + for (let t = 0; t < ms; t += 100) { + const v = fn(); + if (v) return v; + await w(100); + } + return null; + }; + const vis = (b) => b && !b.hidden && b.offsetParent !== null; + const lists = document.querySelector(".m02-side__lists"); + const tops = [...lists.children].filter((c) => c.matches("section")); + const summary = tops[0]; + const box = (e) => e.getBoundingClientRect(); + const out = { + side: { + titles: tops.map((s) => s.querySelector("h3")?.textContent.trim()), + nested: summary.querySelectorAll(".ui-sidebar-card").length, + all: document.querySelectorAll("[data-all], .m02-side__all").length, + rule: summary.querySelectorAll("hr").length, + gap: tops[1] ? Math.round(box(tops[1]).top - box(summary).bottom) : null, + }, + }; + const edits = () => + [...document.querySelectorAll(".m02-struct button")].filter( + (b) => vis(b) && b.textContent.trim() === "수정", + ); + document.querySelector('[data-node="i|item1"]').click(); + await until(() => edits().length === 3); + out.cards = document.querySelectorAll(".m02-struct .ui-sidebar-card, .m02-struct > *").length; + out.edits = edits().map((b) => b.title); + out.detail = []; + for (let i = 0; i < 3; i += 1) { + const b = await until(() => edits()[i]); + if (!b) break; + b.click(); + const back = await until(() => + [...document.querySelectorAll(".m02-main__head button")].find( + (x) => vis(x) && x.textContent.trim() === "돌아가기", + ), + ); + const title = document.querySelector(".m02-main__title")?.textContent.trim() ?? null; + back?.click(); + const again = await until(() => edits().length === 3); + out.detail.push({ title, back: !!back, cardsAfter: again ? 3 : edits().length }); + } + return JSON.stringify(out); +})(); diff --git a/ui_template/ui_template_locale_m2.ts b/ui_template/ui_template_locale_m2.ts index ee9999c55..15a1000a0 100644 --- a/ui_template/ui_template_locale_m2.ts +++ b/ui_template/ui_template_locale_m2.ts @@ -10,8 +10,6 @@ export const ui_locales_m2 = { M02_Title: ["마스터 템플릿", "Master Templates"], M02_KindTable: ["구조물 조합", "Structure combos"], M02_KindDrawing: ["도면 양식", "Drawing templates"], - M02_KindStructure: ["구조물 도면", "Structure drawings"], - M02_KindArea: ["면적산출", "Area takeoff"], M02_NoTemplates: ["양식 없음", "None"], M02_New: ["새로", "New"], M02_Copy: ["본떠 만들기", "Copy as new"], @@ -56,7 +54,6 @@ export const ui_locales_m2 = { "Unsaved changes — discard and continue?", ], M02_ActionFailed: ["안 됨 — {value}", "Failed — {value}"], - M02_StructureNone: ["없음", "None"], M02_Back: ["돌아가기", "Back"], M02_TitleCompany: ["구조물 관리(회사)", "Structure management (company)"], M02_TitlePersonal: ["구조물 관리(개인)", "Structure management (personal)"], @@ -165,8 +162,6 @@ export const ui_locales_m2 = { M02_TreeSaveGo: ["저장하고 계속", "Save and continue"], M02_TreeDiscardGo: ["버리고 계속", "Discard and continue"], M02_TreeDone: ["나무를 고침", "Tree updated"], - M02_KindLabor: ["일위대가", "Unit costs"], - M02_ItemsAll: ["전체 항목", "All items"], M02_PracticeTitle: ["실무문서", "Practice documents"], M02_PracticeRow: ["실무문서 목록", "Practice document list"], M02_PracticeCount: [ From f5a972d0e7a85dd51e289d3002b6cf119a0d508b Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 5 Oct 2026 12:34:10 +0900 Subject: [PATCH 7/9] =?UTF-8?q?fix(M02):=20=EB=8B=A8=EC=B6=94=EB=A5=BC=20?= =?UTF-8?q?=EA=B3=B5=EC=9A=A9=20=EA=B8=B0=EB=B3=B8=20=EB=8B=A8=EC=B6=94?= =?UTF-8?q?=EB=A1=9C=20=C2=B7=20=EC=95=8C=EC=95=BD=20=EA=BC=B4=EC=9D=80=20?= =?UTF-8?q?=EC=A7=84=EC=A7=9C=20=EC=95=8C=EC=95=BD=EC=97=90=EB=A7=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 공용 작은 단추(ui-btn--sm) 모서리 8px → 4px — 낮은 키에 8px 이면 알약 꼴 - 층 단추 · 고르기 창 출처 단추 · 나무 「+」 = createButton(켜짐 filled · 나머지 ghost) · 전용 단추 꼴 CSS 지움 - 데이터 흐름 입력 패널 갈래 · 방식 단추 pill → ghost - 연결 알약 · 다이어그램 알약 · 칩 · 이름표는 그대로 - 시험: 층 단추 글 찾기 들여쓰기 맞춤 · 화면 시나리오 45-1 추가 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016PSXsvyDxjNmufteN5bqmt --- .../DataFlow/M02_DF_InputPanel.ts | 4 +- .../DataFlow/M02_DF_InputPanel_Material.ts | 2 +- .../M02_MasterTemplete_ItemPick.ts | 16 +++-- .../M02_MasterTemplete_TreePanel.css | 44 ------------- .../M02_MasterTemplete_TreePanel.ts | 20 ++++-- .../M02_MasterTemplete_TreePanel_Roots.ts | 20 +++--- resources/tester/test_m02_layer_screen.py | 2 +- .../tester/ui_checks/45-1_M02_단추_꼴.js | 64 +++++++++++++++++++ ui_template/ui_template_elements_styles.ts | 3 +- 9 files changed, 104 insertions(+), 71 deletions(-) create mode 100644 resources/tester/ui_checks/45-1_M02_단추_꼴.js diff --git a/M02_MasterTemplete/DataFlow/M02_DF_InputPanel.ts b/M02_MasterTemplete/DataFlow/M02_DF_InputPanel.ts index 6f068462c..6e03e078e 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_InputPanel.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_InputPanel.ts @@ -147,7 +147,7 @@ export function renderInputPanel( children: [...GROUPS, "다른 로직" as const, "변수" as const].map((k) => createButton({ label: k === "변수" ? t("Variable") : k === "다른 로직" ? t("Logic") : t(k), - variant: k === kind ? "filled" : "pill", + variant: k === kind ? "filled" : "ghost", size: "sm", onClick: () => k !== kind && on.change(setKind(lg, boxId, k)), }), @@ -193,7 +193,7 @@ function renderGroup( ...(["fixed", "rule"] as const).map((m) => createButton({ label: m === "fixed" ? t("ModeFixed") : t("ModeRule"), - variant: mode === m ? "filled" : "pill", + variant: mode === m ? "filled" : "ghost", size: "sm", onClick: () => ((mode = m), drawChips(), draw()), }), diff --git a/M02_MasterTemplete/DataFlow/M02_DF_InputPanel_Material.ts b/M02_MasterTemplete/DataFlow/M02_DF_InputPanel_Material.ts index 0d5ec6384..6203674e2 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_InputPanel_Material.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_InputPanel_Material.ts @@ -445,7 +445,7 @@ export function renderMaterial( ...(["fixed", "choice", "rule"] as CfMaterialMode[]).map((m) => createButton({ label: m === "fixed" ? t("Fixed") : m === "choice" ? t("Choice") : t("Rule"), - variant: mode === m ? "filled" : "pill", + variant: mode === m ? "filled" : "ghost", size: "sm", onClick: () => ((mode = m), drawChips(), drawBody()), }), diff --git a/M02_MasterTemplete/M02_MasterTemplete_ItemPick.ts b/M02_MasterTemplete/M02_MasterTemplete_ItemPick.ts index 69d7b1ef4..385de7bbd 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_ItemPick.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_ItemPick.ts @@ -92,18 +92,20 @@ export function buildPicker(opt: PickerOptions): { root: HTMLElement } { let seq = 0; const buttons = tabs.map((tab) => { - const b = el("button", { - className: "m02-layers__btn", - text: tab.label, - attrs: { type: "button", "data-source": tab.id }, - }); - b.addEventListener("click", () => void show(tab)); + const b = createButton({ label: tab.label, variant: "ghost", onClick: () => void show(tab) }); + b.classList.add("m02-layers__btn"); + b.dataset.source = tab.id; return b; }); async function show(tab: Source): Promise { const mine = ++seq; - buttons.forEach((b) => b.classList.toggle("is-active", b.dataset.source === tab.id)); + buttons.forEach((b) => { + const on = b.dataset.source === tab.id; + b.classList.toggle("is-active", on); + b.classList.toggle("ui-btn--filled", on); + b.classList.toggle("ui-btn--ghost", !on); + }); people.root.hidden = tab.id !== "people"; list.message(); try { diff --git a/M02_MasterTemplete/M02_MasterTemplete_TreePanel.css b/M02_MasterTemplete/M02_MasterTemplete_TreePanel.css index 9d656bd9f..760776347 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_TreePanel.css +++ b/M02_MasterTemplete/M02_MasterTemplete_TreePanel.css @@ -42,28 +42,6 @@ gap: var(--spacing-4); } -.m02-layers__btn { - padding: var(--spacing-4) var(--spacing-8); - border: 1px solid var(--color-border); - border-radius: var(--radius-buttons); - background: var(--color-surface); - color: var(--color-text); - font: inherit; - font-size: var(--text-body-sm); - cursor: pointer; -} - -.m02-layers__btn:hover { - background: var(--color-mist-violet); -} - -.m02-layers__btn.is-active { - border-color: var(--color-accent); - background: var(--color-mist-violet); - color: var(--color-accent); - font-weight: 600; -} - /* 층 단추 줄 오른쪽 끝 — 「N개 고름」 · 「+」(PLAN 30-2) */ .m02-tree__tools { display: inline-flex; @@ -94,28 +72,6 @@ background: var(--color-mist-violet); } -/* 층 단추 옆 「+」 — 나무 맨 위 폴더 · 항목 더하기(PLAN 29-6) */ -.m02-tree__add { - min-width: 1.75rem; - padding: var(--spacing-4) var(--spacing-8); - border: 1px dashed var(--color-border); - border-radius: var(--radius-buttons); - background: none; - color: var(--color-text-secondary); - font: inherit; - font-weight: 600; - cursor: pointer; -} - -.m02-tree__add:hover { - border-color: var(--color-accent); - color: var(--color-accent); -} - -.m02-tree__add[hidden] { - display: none; -} - /* 남의 회사 「개인」 사람 나무 — 고른 사람 줄 아래 그 사람 라이브러리(PLAN 29-6) */ .m02-people__panel { padding-left: var(--spacing-16); diff --git a/M02_MasterTemplete/M02_MasterTemplete_TreePanel.ts b/M02_MasterTemplete/M02_MasterTemplete_TreePanel.ts index 99402a371..3fa73d61b 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_TreePanel.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_TreePanel.ts @@ -10,7 +10,13 @@ * ========================================================================== */ import "./M02_MasterTemplete_TreePanel.css"; -import { createInputField, el, showConfirmDialog, showToast } from "@ui/ui_template_elements"; +import { + createButton, + createInputField, + el, + showConfirmDialog, + showToast, +} from "@ui/ui_template_elements"; import { loadingError, showLoading } from "@ui/ui_template_loading"; import { t as L } from "@ui/ui_template_locale"; import { @@ -141,13 +147,15 @@ export function createTreePanel(opt: TreePanelOptions): TreePanelHandle { const root = el("div", { className: "m02-tree", children: [body] }); search.root.classList.add("m02-tree__search"); // 층 단추 옆 「+」 — 맨 위에 폴더 · 항목 더하기(뿌리 줄 오른쪽 클릭을 갈음) - const add = el("button", { - className: "m02-tree__add", - text: "+", - attrs: { type: "button", title: L("M02_TreeAddTop"), "aria-label": L("M02_TreeAddTop") }, + const add = createButton({ + label: "+", + variant: "ghost", + onClick: (event) => openTreeMenu(event, topMenu()), }); + add.classList.add("m02-tree__add"); + add.title = L("M02_TreeAddTop"); + add.setAttribute("aria-label", L("M02_TreeAddTop")); add.hidden = !opt.canEdit; - add.addEventListener("click", (event) => openTreeMenu(event, topMenu())); // 「N개 고름」 · 「+」 — 층 단추 줄 오른쪽 끝(30-2 · 부르는 쪽이 붙임) const status = el("span", { className: "m02-tree__picked", attrs: { hidden: "" } }); const tools = el("span", { className: "m02-tree__tools", children: [status, add] }); diff --git a/M02_MasterTemplete/M02_MasterTemplete_TreePanel_Roots.ts b/M02_MasterTemplete/M02_MasterTemplete_TreePanel_Roots.ts index 4c4ba094e..039deddc4 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_TreePanel_Roots.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_TreePanel_Roots.ts @@ -9,7 +9,7 @@ * 누르면 `go`(부르는 쪽이 해시로 다시 엶). * ========================================================================== */ -import { el } from "@ui/ui_template_elements"; +import { createButton, el } from "@ui/ui_template_elements"; import { t as L } from "@ui/ui_template_locale"; import { createTreeRow } from "@ui/ui_template_tree"; import type { SessionUser } from "../A06_Login/A06_Login_Api_Fetch"; @@ -67,15 +67,17 @@ export function createLayerBar(opt: LayerBarOptions): HTMLElement { : "M02_PickerPersonal", ); const current = root === active.root && (root !== "personal" || people || !active.owner); - const b = el("button", { - className: `m02-layers__btn${current ? " is-active" : ""}`, - text: readOnly ? `${name} · ${L("M02_RootReadOnly")}` : name, - attrs: { type: "button", "data-root": root, "aria-pressed": String(current) }, - }); - b.addEventListener("click", () => { - if (!current) opt.go(root, owner); - else opt.reselect?.(); + const b = createButton({ + label: readOnly ? `${name} · ${L("M02_RootReadOnly")}` : name, + variant: current ? "filled" : "ghost", + onClick: () => { + if (!current) opt.go(root, owner); + else opt.reselect?.(); + }, }); + b.classList.add("m02-layers__btn"); + b.dataset.root = root; + b.setAttribute("aria-pressed", String(current)); return b; } diff --git a/resources/tester/test_m02_layer_screen.py b/resources/tester/test_m02_layer_screen.py index 280859a46..17fcd6f02 100644 --- a/resources/tester/test_m02_layer_screen.py +++ b/resources/tester/test_m02_layer_screen.py @@ -282,7 +282,7 @@ def test_켜진_층_단추는_목록으로_복사_끌기(): """30-4 — 켜진 층 단추를 다시 누르면 그 층 목록(구조물 = 집계표 · 일위대가 단추 벌은 39-2 에서 지움) · 30-2 · 39-6 「〇〇로 복사…」 = 쓸 수 있는 층(지금 층 뺌 · 폴더 고름) · 끌어다 옮기기는 쓸 수 있는 층만.""" roots = _text(M02 / "M02_MasterTemplete_TreePanel_Roots.ts") - assert "if (!current) opt.go(root, owner);\n else opt.reselect?.();" in roots + assert "if (!current) opt.go(root, owner);\n else opt.reselect?.();" in roots page = _text(M02 / "M02_MasterTemplete_UI_Page.ts") assert 'layerBar("구조물조합", tree.add, () => void main.openTable(layer))' in page # 39-6 — 쓸 수 있는 층 전부(볼 회사 주인까지 · 지금 층 뺌) · 대상 폴더 고르는 창 diff --git a/resources/tester/ui_checks/45-1_M02_단추_꼴.js b/resources/tester/ui_checks/45-1_M02_단추_꼴.js new file mode 100644 index 000000000..17bc5b9e2 --- /dev/null +++ b/resources/tester/ui_checks/45-1_M02_단추_꼴.js @@ -0,0 +1,64 @@ +/* 45-1 — M02 단추 = 공용 기본 단추(알약 꼴 아님) · 알약은 그대로 알약 + * 시작 화면: M02 마스터 템플릿 · 마스터 층(#/m02-master-template?open=구조물조합&root=master) · 시스템 관리자 · + * 나무에 item1(구조물 조합)이 있음 + * 부르는 법: orca eval --expression "$(cat 이 파일)" (async 식 · JSON 글을 돌려줌) + * 알약 꼴 = 모서리 반지름 ≥ 높이 / 2 - 1(캡슐) · 진짜 알약 = 알약 · 칩 · 이름표 클래스(.ss-link · *__pill · *-chip · .ui-tag) + * 기대 수치: + * - pillButtons = [](M02 안 보이는 단추 중 알약 꼴 0 · 머리 공용 메뉴는 M02 밖이라 뺌) + * - layers = 층 단추가 ui-btn · 켜진 층은 filled · add = 「+」 가 ui-btn--ghost + * - smRadius = 4(작은 단추 모서리 = 8 / 2) · buttons ≥ 20 + * - pillOn: [알약] 켬 → realPills > 0 · 모두 알약 꼴(realRound = realPills) → 끔으로 되돌림(pressedAfter = "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 vis = (b) => b && !b.hidden && b.offsetParent !== null; + const round = (e) => { + const r = e.getBoundingClientRect(); + return parseFloat(getComputedStyle(e).borderTopLeftRadius) >= r.height / 2 - 1; + }; + const REAL = ".ss-link, [class*='__pill'], [class*='-chip'], [class*='__chip'], .ui-tag"; + const scope = () => + [...document.querySelectorAll("button")].filter( + (b) => vis(b) && !b.closest("header, .app-header") && !b.matches(REAL), + ); + const out = {}; + const node = await until(() => document.querySelector('[data-node="i|item1"]')); + if (!node) return JSON.stringify({ error: "item1 없음 — 기슭막이 폴더를 펼쳐 둘 것" }); + node.click(); + await until(() => document.querySelector(".m02-struct__card-tools .ui-btn")); + await w(1500); + const all = scope(); + out.buttons = all.length; + out.pillButtons = all + .filter((b) => round(b) && b.getBoundingClientRect().height > 0) + .map((b) => `${b.innerText.trim().slice(0, 12)} | ${b.className}`); + out.layers = [...document.querySelectorAll(".m02-layers__btn")].map( + (b) => `${b.innerText.trim()}:${b.classList.contains("ui-btn--filled") ? "filled" : b.classList.contains("ui-btn--ghost") ? "ghost" : b.className}`, + ); + out.add = document.querySelector(".m02-tree__add")?.className ?? null; + const sm = all.find((b) => b.classList.contains("ui-btn--sm")); + out.smRadius = sm ? parseFloat(getComputedStyle(sm).borderTopLeftRadius) : null; + // [알약] 켬 → 진짜 알약은 알약 꼴 그대로 → 끔으로 되돌림 + const pillBtn = [...document.querySelectorAll(".m02-struct__card-tools button")].find( + (b) => vis(b) && b.innerText.trim() === "알약", + ); + if (pillBtn) { + const before = pillBtn.getAttribute("aria-pressed"); + if (before !== "true") pillBtn.click(); + await w(600); + const real = [...document.querySelectorAll(".ss-link")].filter(vis); + out.pillOn = { realPills: real.length, realRound: real.filter(round).length }; + if (before !== "true") pillBtn.click(); + await w(300); + out.pillOn.pressedAfter = pillBtn.getAttribute("aria-pressed"); + } else out.pillOn = "알약 단추 없음"; + return JSON.stringify(out); +})() diff --git a/ui_template/ui_template_elements_styles.ts b/ui_template/ui_template_elements_styles.ts index 0eb579239..ec674b701 100644 --- a/ui_template/ui_template_elements_styles.ts +++ b/ui_template/ui_template_elements_styles.ts @@ -33,10 +33,11 @@ const BASE_CSS = ` .ui-btn:disabled { opacity: 0.5; cursor: not-allowed; } .ui-btn__icon { display: inline-flex; width: 16px; height: 16px; } -/* 작은 크기 — 다섯 모양 모두에 먹음(padding · 글자만 바꿈) */ +/* 작은 크기 — 다섯 모양 모두에 먹음(padding · 글자 · 모서리) · 낮은 키에 8px 모서리면 알약 꼴이라 반으로 */ .ui-btn--sm { padding: 2px var(--spacing-8); font-size: var(--text-caption); + border-radius: calc(var(--radius-buttons) / 2); } .ui-btn--filled { From 08e2b211a9a9ff96ef8f1216242a434263df49bc Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 5 Oct 2026 12:38:58 +0900 Subject: [PATCH 8/9] auto: 2026-10-05 12:38 (ESD_LAPTOP) --- resources/master_template/area/item1.json | 121 ++++++++++++++++++--- resources/master_template/labor/item1.json | 14 +++ 2 files changed, 117 insertions(+), 18 deletions(-) diff --git a/resources/master_template/area/item1.json b/resources/master_template/area/item1.json index 02dfac0e2..7ae03696e 100644 --- a/resources/master_template/area/item1.json +++ b/resources/master_template/area/item1.json @@ -9,27 +9,112 @@ { "id": "s1", "이름": "면적산출", - "칸": {}, - "연결": [ - { - "키": "콘크리트 표층 인력포설|㎥", - "이름": "콘크리트 표층 인력포설", - "칸": "B6", - "단위": "㎥" + "칸": { + "E7": { + "값": "구 분" }, - { - "키": "와이어메시 바닥깔기|㎡", - "이름": "와이어메시 바닥깔기", - "칸": "D15", - "단위": "㎡" + "F7": { + "값": "규 격" }, - { - "키": "기계비빔타설|㎥", - "이름": "기계비빔타설", - "칸": "F14", - "단위": "㎥" + "G7": { + "값": "수 량" + }, + "H7": { + "값": "단 위" + }, + "I7": { + "값": "산 출 근 거" + }, + "J7": { + "값": "비 고" + }, + "E8": { + "값": "야면석" + }, + "E9": { + "값": "고임돌량" + }, + "E10": { + "값": "뒤채움돌" + }, + "E11": { + "값": "구조물터파기량" + }, + "E12": { + "값": "되메우기량" + }, + "E13": { + "값": "잔토량" + }, + "F8": { + "값": "뒤길이35cm" + }, + "F10": { + "값": "9~15cm" + }, + "G8": { + "값": 2.65 + }, + "G9": { + "값": 0.24 + }, + "G10": { + "값": 0.94 + }, + "G11": { + "값": 0.38 + }, + "G12": { + "값": 0.11 + }, + "G13": { + "값": 0.26 + }, + "H8": { + "값": "㎡" + }, + "H9": { + "값": "㎥" + }, + "H10": { + "값": "㎥" + }, + "H11": { + "값": "㎥" + }, + "H12": { + "값": "㎥" + }, + "H13": { + "값": "㎥" + }, + "I8": { + "값": "2.5m * 1.059m / 1.0 m * 1.0 m = 2.6475m" + }, + "I9": { + "값": "2.6475m * 0.09m /1.0 m = 0.2383㎥" + }, + "I10": { + "값": "(0.45 + 0.3)/2 * 2.5 * 1.0 = 0.9375" + }, + "I11": { + "값": "(1.1+1.4)/2 * 0.3 * 1.0 = 0.375" + }, + "I12": { + "값": "(0.3+0.45)/2 * 0.3 * 1.0 = 0.1125" + }, + "I13": { + "값": "0.375 - 0.1125 = 0.2625" } - ] + }, + "열": { + "E": { + "폭": 14.460318429129464 + }, + "I": { + "폭": 39.60316685267857 + } + } } ], "활성": "s1", diff --git a/resources/master_template/labor/item1.json b/resources/master_template/labor/item1.json index b9889e9e3..77ea48848 100644 --- a/resources/master_template/labor/item1.json +++ b/resources/master_template/labor/item1.json @@ -11,84 +11,98 @@ "담은로직": [ { "로직": "GC000198", + "별칭": null, "메모": "레미콘타설(장비) 무근,소형", "입력값": {}, "수량": null }, { "로직": "GC000214", + "별칭": null, "메모": "유로폼 설치 및 해체 보통", "입력값": {}, "수량": null }, { "로직": "GC000206", + "별칭": null, "메모": "철근가공 및 조립 Type-Ⅰ-1,현장가공조립", "입력값": {}, "수량": null }, { "로직": "GC000207", + "별칭": null, "메모": "철근가공 및 조립 Type-Ⅰ-1,현장가공조립", "입력값": {}, "수량": null }, { "로직": "GF000218", + "별칭": null, "메모": "기초다짐 및 뒤채움 소형 장비사용 시공 (실무 수는 옛 판 값)", "입력값": {}, "수량": null }, { "로직": "GF000225", + "별칭": null, "메모": "깬잡석찰붙임 L3=30cm내외", "입력값": {}, "수량": null }, { "로직": "GF000156", + "별칭": null, "메모": "콘크리트(기계비빔) 무근구조물 (실무 수는 옛 판 값)", "입력값": {}, "수량": null }, { "로직": "GF000226", + "별칭": null, "메모": "큰돌메쌓기 뒷길이 0.6~0.8m", "입력값": {}, "수량": null }, { "로직": "GF000214", + "별칭": null, "메모": "야면석채집 L3=45cm내외 (실무 수는 옛 판 값)", "입력값": {}, "수량": null }, { "로직": "GF000222", + "별칭": null, "메모": "찰쌓기(켜쌓기) L3=55cm이하 (실무 수는 옛 판 값)", "입력값": {}, "수량": null }, { "로직": "GC000677", + "별칭": null, "메모": "모르터바르기 1:3", "입력값": {}, "수량": null }, { "로직": "GF000221", + "별칭": null, "메모": "돌쌓기 찰(켜)쌓기,L3=55cm이하 (실무 수는 옛 판 값)", "입력값": {}, "수량": null }, { "로직": "GC000088", + "별칭": null, "메모": "구조물터파기(토사) 굴착기0.6m3", "입력값": {}, "수량": null }, { "로직": "GF000113", + "별칭": null, "메모": "구조물되메우기 굴착기0.6m3 (실무 수는 옛 판 값)", "입력값": {}, "수량": null From 6eb3c551be93b8de6604bfa75a6f148d9350435e Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 5 Oct 2026 12:39:35 +0900 Subject: [PATCH 9/9] =?UTF-8?q?chore(plan):=2010-5=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=EC=9B=90=EB=AC=B8(=EA=B5=AC=EC=A1=B0=EB=AC=BC=20=EC=A1=B0?= =?UTF-8?q?=ED=95=A9=20=EC=83=81=EC=84=B8=20=EA=B3=B5=EA=B0=84=20=C2=B7=20?= =?UTF-8?q?=EA=B5=AC=EB=B6=84=EC=84=A0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FJqWQpoB1RE7hiaQ2wH8qV --- docs/raw/사용자요구/2026-10-05_수정요구.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/raw/사용자요구/2026-10-05_수정요구.md b/docs/raw/사용자요구/2026-10-05_수정요구.md index 305ec2bf6..e48eaa057 100644 --- a/docs/raw/사용자요구/2026-10-05_수정요구.md +++ b/docs/raw/사용자요구/2026-10-05_수정요구.md @@ -35,3 +35,7 @@ 1. M02 페이지의 전체영역에서 알약 템플릿으로 프론트엔드에 사용중인데 버튼들은 기본 버튼 템플릿을 사용하게 변경해서 알약 템플릿은 알약에만 사용하는게 맞아. 2. M02 페이지의 좌측패널 내부 별도의 서브 컨테이너로 구조물도면, 일위대가, 면적산출 구성되어 있는데 이건 필요없어 보임 접근은 구조물 조합을 선택하면 바로 3개의 항목 연결이 보이니 불필요해보여. 3. 구조물 조합 상세 페이지의 구조물 분류 테이블에 일위대가 도면 면적산출 행에 키값들을 노출하고 있는데 이건 필요없어보여. + +## 추가 (10-5 · 원문 그대로) + +다음으로 구조물 조합 분류 테이블과 상세컨테이너사이가 공간이 너무 넣어 그리고 구조물 분류 테이블의 하단에 구분선이 있는것같은데 삭제해줘. 컨테이너의 상단에도 구분선이 있어.