From 407481dc8dc0d67d38da2940d86d53484728e269 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 5 Oct 2026 12:32:50 +0900 Subject: [PATCH] =?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: [