From 6c2ae466c902aadbdeb3b4ab96047ae3b42274c0 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 8 Oct 2026 20:10:19 +0900 Subject: [PATCH] =?UTF-8?q?fix(B05):=20=EC=A2=85=EB=8B=A8=20=C2=B7=20?= =?UTF-8?q?=EB=B0=B0=EC=88=98=EC=9C=A0=EC=97=AD=EB=8F=84=20=C2=B7=203D=20?= =?UTF-8?q?=C2=B7=20B06=20=EC=9A=B0=ED=81=B4=EB=A6=AD=20=EA=B5=AC=EC=A1=B0?= =?UTF-8?q?=EB=AC=BC=EA=B5=B0=20=C2=B7=20=EC=A2=85=EB=A5=98=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20=3D=20=EC=A2=8C=EC=B8=A1=20=ED=8C=A8=EB=84=90=20?= =?UTF-8?q?=EB=93=9C=EB=A1=AD=EB=8B=A4=EC=9A=B4=20=EB=AA=A9=EB=A1=9D=20?= =?UTF-8?q?=ED=95=9C=20=EB=B2=8C(129-1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 패널이 자기 드롭다운 목록(pickGroups — 군 차례 · 정의 이름 · 켜진 종류 · 옛 대피 뺌 · 숨김)을 내고 우클릭 메뉴가 그것을 그대로 씀 - 옛 메뉴 = 꺼진 종류(F 야생동물 이동통로 · 비탈면 녹화 · G 쇄석 포설) · 옛 대피 종류(대피소 · 정차작업장 · 차돌림곳) 더 보임 · 군 이름은 정의(48) 대신 고정 이름 - 시험 test_129(실제 레지스트리 + 정의로 패널 = 메뉴 · B05 · B06) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01UoSr2sqvYtBf1dtQdus88s --- A00_Common/design/design_profile.ts | 7 +- A00_Common/design/design_shared.ts | 1 + B05_Profile/B05_Profile_UI_Page_Structures.ts | 5 +- .../B05_Profile_UI_Profile_Panel_Types.ts | 3 + B05_Profile/B05_Profile_UI_Profile_Render.ts | 9 +- .../B05_Profile_UI_Profile_Structures.ts | 44 ++++----- .../B05_Profile_UI_Structures_Panel.ts | 2 + .../B05_Profile_UI_Structures_Panel_Types.ts | 45 +++++++--- B05_Profile/B05_Profile_UI_Viewer_Menu.ts | 16 ++-- .../B06_Section_UI_Section_View_Menu.ts | 7 +- .../tester/helper_129_menu_equals_panel.cjs | 82 +++++++++++++++++ .../tester/test_129_menu_equals_panel.py | 89 +++++++++++++++++++ 12 files changed, 246 insertions(+), 64 deletions(-) create mode 100644 resources/tester/helper_129_menu_equals_panel.cjs create mode 100644 resources/tester/test_129_menu_equals_panel.py diff --git a/A00_Common/design/design_profile.ts b/A00_Common/design/design_profile.ts index 22322e4c1..fbc325620 100644 --- a/A00_Common/design/design_profile.ts +++ b/A00_Common/design/design_profile.ts @@ -219,10 +219,9 @@ export function createDesignProfile(ctx: DesignProfileContext) { }, onBasinSelected: (chainageM) => callbacks?.onBasinSelected?.(chainageM), onPipeSelected: (chainageM) => callbacks?.onPipeSelected?.(chainageM), - // 배수유역도 우클릭 빈 자리 메뉴 = 종단그래프와 같은 구조물군 → 종류 2단 목록 - // (2026-08-18 일원화). 선택하면 사이드 폼 자동 지정(addAt 경로). + // 배수유역도 우클릭 빈 자리 = 사이드 패널과 같은 군 → 종류 목록(129-1 · addAt 경로). structureMenuItems: (chainage) => - structureGroupMenuItems(structureTypes, (typeId) => + structureGroupMenuItems(callbacks?.structureGroups?.() ?? [], (typeId) => callbacks?.onStructureTypeAdd?.(chainage, typeId), ), }) ?? null; @@ -608,7 +607,7 @@ export function createDesignProfile(ctx: DesignProfileContext) { irregularStations = snapToMasterStations(alignment, stations); draw(); }, - /** 구조물 타입 레지스트리를 받아 마크 색·약호·우클릭 메뉴에 쓴다(최초 1회). */ + /** 구조물 타입 레지스트리를 받아 마크 색·약호에 쓴다(최초 1회 · 우클릭 목록은 사이드 패널 몫). */ setStructureTypes(types: StructureType[]) { structureTypes = types; syncDrainageSpans(); diff --git a/A00_Common/design/design_shared.ts b/A00_Common/design/design_shared.ts index 1042bf51b..7da9b42ab 100644 --- a/A00_Common/design/design_shared.ts +++ b/A00_Common/design/design_shared.ts @@ -172,6 +172,7 @@ export function sharedProfile(shared: DesignPageShared): DesignProfile { onStructureSelect: toView(shared, "onStructureSelect"), onStructureMarkMove: toView(shared, "onStructureMarkMove"), onStructureTypeAdd: toView(shared, "onStructureTypeAdd"), + structureGroups: () => sharedStructures(shared).pickGroups(), onBasinSelected: toView(shared, "onBasinSelected"), onPipeSelected: toView(shared, "onPipeSelected"), }, diff --git a/B05_Profile/B05_Profile_UI_Page_Structures.ts b/B05_Profile/B05_Profile_UI_Page_Structures.ts index ef571f0bc..c52541f09 100644 --- a/B05_Profile/B05_Profile_UI_Page_Structures.ts +++ b/B05_Profile/B05_Profile_UI_Page_Structures.ts @@ -343,7 +343,7 @@ export function createStructuresBridge(deps: StructuresBridgeDeps) { return { /** 3D 노선 우클릭 → 구조물 배치 메뉴(그래프·배수유역도와 동일, 2026-08-19). - * 타입 목록은 레지스트리 로드 후 채워지는 structureTypeMap을 그대로 쓴다. */ + * 목록은 사이드 패널 군 · 종류 드롭다운 그대로(129-1). */ mountViewerMenu( viewer: { root: HTMLElement; @@ -355,8 +355,7 @@ export function createStructuresBridge(deps: StructuresBridgeDeps) { root: viewer.root, modelPointAt: viewer.modelPointAt, routePoints, - structureTypes: () => - [...structureTypeMap].map(([type_id, info]) => ({ type_id, ...info })), + structureGroups: () => deps.panel().structures.pickGroups(), onPick: (chainage, typeId) => deps.panel().structures.addAt(chainage, typeId), }); }, diff --git a/B05_Profile/B05_Profile_UI_Profile_Panel_Types.ts b/B05_Profile/B05_Profile_UI_Profile_Panel_Types.ts index a5380a414..c91aaa6c2 100644 --- a/B05_Profile/B05_Profile_UI_Profile_Panel_Types.ts +++ b/B05_Profile/B05_Profile_UI_Profile_Panel_Types.ts @@ -9,6 +9,7 @@ import type { GroupItems, SummaryItem } from "../common_util/common_util_summary_item"; import type { PipeFacility, PipeSource } from "../B04_PreProcess/B04_PreProcess_Api_Fetch"; import type { IrregularStation } from "./B05_Profile_UI_IrregularStations"; +import type { PickGroup } from "./B05_Profile_UI_Structures_Panel_Types"; /** 종단 테이블 구조물 라인·배수유역도가 Page로 올려 보내는 알림. */ export interface RouteProfilePanelCallbacks { @@ -47,6 +48,8 @@ export interface RouteProfilePanelCallbacks { onStructureMarkMove?: (structureId: string, toChainageM: number) => void; /** 종단 그래프 우클릭으로 레지스트리 타입을 지정해 구조물을 넣음. */ onStructureTypeAdd?: (chainageM: number, typeId: string) => void; + /** 우클릭 군 · 종류 목록 = 사이드 「구조물 배치」 드롭다운 그대로(129-1 · B05 · B06 한 패널). */ + structureGroups?: () => PickGroup[]; /** 배수유역도에서 유역을 고름 — 그 관의 누가거리(해제면 null). 그래프·사이드 패널을 맞춘다. */ onBasinSelected?: (chainageM: number | null) => void; /** 배수유역도에서 관 마커를 고름(유역 없는 관 포함) — 전 화면 동기화용(2026-08-17). */ diff --git a/B05_Profile/B05_Profile_UI_Profile_Render.ts b/B05_Profile/B05_Profile_UI_Profile_Render.ts index 6dd9a018e..b7f45b834 100644 --- a/B05_Profile/B05_Profile_UI_Profile_Render.ts +++ b/B05_Profile/B05_Profile_UI_Profile_Render.ts @@ -305,13 +305,8 @@ export function renderProfile(ctx: ProfileRenderContext): void { onRemove: (station) => callbacks?.onStructureRemove?.(station), onAddPipe: (chainage) => callbacks?.onPipeAdd?.(chainage), onAddStructure: (chainage, type) => callbacks?.onStructureAdd?.(chainage, type), - // 사이드 「구조물 배치」와 같은 전체 목록(A군 포함) — 선택 = 즉시 추가가 아니라 - // 폼 자동 지정이므로 필수 옵션·관리 주체로 거를 이유가 없어졌다(2026-08-18 일원화). - structureTypes: structureTypes.map((type) => ({ - type_id: type.type_id, - group: type.group, - name: type.name, - })), + // 사이드 「구조물 배치」 군 · 종류 드롭다운 그대로(129-1 — 패널이 가진 목록). + structureGroups: callbacks?.structureGroups, onAddStructureType: (chainage, typeId) => callbacks?.onStructureTypeAdd?.(chainage, typeId), }); // 구조물 알약 레인 — 그래프 아래 별도 줄(2026-08-17 사용자 지시 1·3). 자동 배수관도 diff --git a/B05_Profile/B05_Profile_UI_Profile_Structures.ts b/B05_Profile/B05_Profile_UI_Profile_Structures.ts index a9d6ca393..6e2f67b27 100644 --- a/B05_Profile/B05_Profile_UI_Profile_Structures.ts +++ b/B05_Profile/B05_Profile_UI_Profile_Structures.ts @@ -16,31 +16,23 @@ import { type IrregularStation, } from "./B05_Profile_UI_IrregularStations"; import { structureAnchorM, type StructureInstance } from "./B05_Profile_Api_Structures"; -import { GROUP_LABELS } from "./B05_Profile_UI_Structures_Panel"; -import { isPickHidden } from "./B05_Profile_UI_Structures_Panel_Types"; +import type { PickGroup } from "./B05_Profile_UI_Structures_Panel_Types"; -/** 우클릭 메뉴용 최소 타입 정보 — 사이드 「구조물 배치」 종류 목록과 같은 원천. */ -export interface StructureMenuType { - type_id: string; - group: string; - name: string; -} - -/** 사이드 「구조물 배치」와 같은 구조물군 → 종류 2단 메뉴 항목을 만든다 - * (2026-08-18 우클릭 메뉴 일원화 — 종단그래프·배수유역도 공용). 항목 선택 = - * 즉시 추가가 아니라 폼 자동 지정(addAt 경로). */ +/** 사이드 「구조물 배치」 군 · 종류 드롭다운 목록(`section.pickGroups()`)을 그대로 구조물군 → 종류 + * 2단 메뉴 항목으로(129-1 — 종단그래프 · 배수유역도 · 3D · B06 종단 공용 · 가린 군 · 종류는 뺌). + * 항목 선택 = addAt 경로. */ export function structureGroupMenuItems( - types: ReadonlyArray, + groups: ReadonlyArray, onPick: (typeId: string) => void, ): MapContextMenuItem[] { - const pickable = types.filter((type) => !isPickHidden(type)); - const groups = [...new Set(pickable.map((type) => type.group))]; - return groups.map((group) => ({ - label: GROUP_LABELS[group] ?? group, - children: pickable - .filter((type) => type.group === group) - .map((type): [string, () => void] => [type.name, () => onPick(type.type_id)]), - })); + return groups + .filter((group) => !group.hidden) + .map((group) => ({ + label: group.label, + children: group.types + .filter((member) => !member.hidden) + .map(({ type }): [string, () => void] => [type.name, () => onPick(type.type_id)]), + })); } /** 선을 잡았다고 볼 좌우 여유(px). 선 자체는 얇아 그대로는 집기 어렵다. */ @@ -61,8 +53,8 @@ export interface StructureLineOptions { onAddPipe: (chainageM: number) => void; /** 빈 자리에서 우클릭해 배관 외 구조물(기성막이/대피로/기타)을 넣을 때. */ onAddStructure?: (chainageM: number, structureType: "기성막이" | "대피로" | "기타") => void; - /** 레지스트리 타입 목록(구조물군·이름). 우클릭 메뉴의 "구조물 추가" 항목이 된다. */ - structureTypes?: ReadonlyArray<{ type_id: string; group: string; name: string }>; + /** 사이드 패널 군 · 종류 고르기 목록(129-1). 우클릭 메뉴의 "구조물 추가" 항목이 된다. */ + structureGroups?: () => ReadonlyArray; /** 레지스트리 타입을 골라 넣을 때. */ onAddStructureType?: (chainageM: number, typeId: string) => void; } @@ -97,9 +89,9 @@ export function mountStructureMenu(host: HTMLElement, options: StructureLineOpti // 빈 자리 — 사이드 「구조물 배치」와 같은 구조물군 → 종류 2단 메뉴(2026-08-18 // 일원화). 선택 = 폼 자동 지정(addAt 경로, A군은 임시 배치까지). 레지스트리를 // 아직 못 받았으면 구 항목(배관·기성막이 등)으로 대신한다. - const types = options.structureTypes ?? []; - const addItems: MapContextMenuItem[] = types.length - ? structureGroupMenuItems(types, (typeId) => options.onAddStructureType?.(at, typeId)) + const groups = options.structureGroups?.() ?? []; + const addItems: MapContextMenuItem[] = groups.length + ? structureGroupMenuItems(groups, (typeId) => options.onAddStructureType?.(at, typeId)) : [ ["배관 추가", () => options.onAddPipe(at)], ...(["기성막이", "대피로", "기타"] as const).map((type): [string, () => void] => [ diff --git a/B05_Profile/B05_Profile_UI_Structures_Panel.ts b/B05_Profile/B05_Profile_UI_Structures_Panel.ts index 0240f6ecd..bd0f0800f 100644 --- a/B05_Profile/B05_Profile_UI_Structures_Panel.ts +++ b/B05_Profile/B05_Profile_UI_Structures_Panel.ts @@ -56,6 +56,7 @@ export type { PipeFacilityItem, StructuresSection } from "./B05_Profile_UI_Struc import { DEFAULT_INTERVAL_LENGTH_M, groupOptions, + pickGroups, hasComputedRange, isRangeOnly, typeOptions, @@ -543,6 +544,7 @@ export function createStructuresSection( facility: { ...facilityOptions, recount: () => (facilityOptions.recount(), renderList()) }, // 단 수 → 목록 램프(54-15) listRoot: list, hasSelection: () => editingId !== null || selectedPipeChainage !== null, + pickGroups: () => pickGroups(types, canPick), // 우클릭 메뉴 = 이 드롭다운 목록(129-1) setTypes(next, pick = () => true) { types = next; canPick = pick; diff --git a/B05_Profile/B05_Profile_UI_Structures_Panel_Types.ts b/B05_Profile/B05_Profile_UI_Structures_Panel_Types.ts index f97aaea72..7183dc422 100644 --- a/B05_Profile/B05_Profile_UI_Structures_Panel_Types.ts +++ b/B05_Profile/B05_Profile_UI_Structures_Panel_Types.ts @@ -21,6 +21,7 @@ import { import type { DesignSelection, ResolvedSelection } from "../A00_Common/design/design_selection"; import type { GroupItems, SummaryItem } from "../common_util/common_util_summary_item"; import { STRUCTURE_SPECS } from "../A00_Common/design/design_structure_pipe"; +import { pickable } from "./B05_Profile_UI_Structures_Define"; /** 구조물군 표시 이름. 리스트 기호(A~G)만으로는 무엇인지 알기 어렵다. * 우클릭 메뉴(종단그래프·배수유역도)도 같은 이름을 쓴다(2026-08-18 일원화). */ @@ -59,19 +60,41 @@ export function pickOption(text: string, value: string, hidden: boolean): HTMLOp return option; } -/** 군 드롭다운 항목들 — 이름은 정의(48) 먼저 · 뺄 군과 고를 종류가 없는 군은 가림. */ +/** 고르기 목록에서 가릴 종류인가 — 꺼진(pick) 종류 · 뺄 군 · 뺄 종류(드롭다운 · 메뉴 공용). */ +const pickHidden = (type: StructureType, pick: (type: StructureType) => boolean): boolean => + !pick(type) || isPickHidden(type); + +/** 고르기 목록 한 군 — 가린 군 · 종류도 담음(옛 항목을 열 때 값으로 고름). */ +export interface PickGroup { + group: string; + label: string; + hidden: boolean; + types: { type: StructureType; hidden: boolean }[]; +} + +/** 군 → 종류 고르기 목록 한 벌 — 패널 드롭다운과 우클릭 메뉴(종단 · 배수유역도 · 3D · B06)가 + * 같은 자료를 봄(129-1). 군 이름은 정의(48) 먼저 · 옛 대피 종류는 뺌 · 꺼진(pick) 종류 · 뺄 군 · + * 뺄 종류는 가림 · 보일 종류가 없는 군도 가림. */ +export function pickGroups( + types: ReadonlyArray, + pick: (type: StructureType) => boolean, +): PickGroup[] { + const groups = new Map(types.map((t) => [t.group, t.group_name] as const)); + return [...groups].map(([group, name]) => { + const members = types + .filter((t) => t.group === group && pickable(t)) + .map((type) => ({ type, hidden: pickHidden(type, pick) })); + const hidden = members.every((member) => member.hidden); + return { group, label: name ?? GROUP_LABELS[group] ?? group, hidden, types: members }; + }); +} + +/** 군 드롭다운 항목들 — `pickGroups` 그대로. */ export function groupOptions( types: StructureType[], pick: (type: StructureType) => boolean, ): HTMLOptionElement[] { - const groups = new Map(types.map((t) => [t.group, t.group_name] as const)); - return [...groups].map(([g, name]) => - pickOption( - name ?? GROUP_LABELS[g] ?? g, - g, - !types.some((t) => t.group === g && pick(t) && !isPickHidden(t)), - ), - ); + return pickGroups(types, pick).map((g) => pickOption(g.label, g.group, g.hidden)); } /** 종류 드롭다운 항목들 — 고를 수 없거나 뺄 종류는 가림. */ @@ -79,7 +102,7 @@ export function typeOptions( types: StructureType[], pick: (type: StructureType) => boolean, ): HTMLOptionElement[] { - return types.map((t) => pickOption(t.name, t.type_id, !pick(t) || isPickHidden(t))); + return types.map((t) => pickOption(t.name, t.type_id, pickHidden(t, pick))); } /** 구간형 신규 추가 시 기본 구간 길이(m). 사용자가 종점을 바로 고칠 수 있게 짧게 잡는다. */ @@ -134,6 +157,8 @@ export interface StructuresSection { /** 서버에서 받은 타입 목록을 채운다(최초 1회). 이름·폼은 넘긴 종류 전부로 풀고, * 고르기 목록엔 `pick` 이 고른 종류만 보임(빼면 전부). */ setTypes: (types: StructureType[], pick?: (type: StructureType) => boolean) => void; + /** 지금 군 · 종류 드롭다운 목록 그대로 — 우클릭 메뉴(종단 · 배수유역도 · 3D)가 같은 자료를 봄(129-1). */ + pickGroups: () => PickGroup[]; /** 서버 정본으로 목록을 교체한다(진입·복원·저장 후). */ setStructures: (structures: StructureInstance[]) => void; getStructures: () => StructureInstance[]; diff --git a/B05_Profile/B05_Profile_UI_Viewer_Menu.ts b/B05_Profile/B05_Profile_UI_Viewer_Menu.ts index ace56762c..bae5b612a 100644 --- a/B05_Profile/B05_Profile_UI_Viewer_Menu.ts +++ b/B05_Profile/B05_Profile_UI_Viewer_Menu.ts @@ -10,10 +10,8 @@ * ========================================================================== */ import { createMapContextMenu } from "@ui/ui_template_context_menu"; -import { - structureGroupMenuItems, - type StructureMenuType, -} from "./B05_Profile_UI_Profile_Structures"; +import { structureGroupMenuItems } from "./B05_Profile_UI_Profile_Structures"; +import type { PickGroup } from "./B05_Profile_UI_Structures_Panel_Types"; /** 노선 위로 본다 판정 거리(m) — 이보다 멀면 노선 우클릭이 아니다. */ const ROUTE_PICK_DISTANCE_M = 15; @@ -25,8 +23,8 @@ export interface ViewerMenuDeps { modelPointAt: (clientX: number, clientY: number) => { x: number; y: number } | null; /** 노선 폴리라인(모델 좌표, 시점→종점 순). 비어 있으면 메뉴를 띄우지 않는다. */ routePoints: () => ReadonlyArray<{ x: number; y: number }>; - /** 레지스트리 타입 목록 — 사이드 「구조물 배치」 종류와 같은 원천. */ - structureTypes: () => ReadonlyArray; + /** 사이드 「구조물 배치」 군 · 종류 고르기 목록 그대로(129-1). */ + structureGroups: () => ReadonlyArray; /** 종류 선택 — 사이드 폼 자동 지정(addAt 경로, A군은 임시 배치까지). */ onPick: (chainageM: number, typeId: string) => void; } @@ -70,8 +68,8 @@ export function mountViewerStructureMenu(deps: ViewerMenuDeps): void { event.preventDefault(); return; } - const types = deps.structureTypes(); - if (!types.length) return; + const groups = deps.structureGroups(); + if (!groups.length) return; const model = deps.modelPointAt(event.clientX, event.clientY); if (!model) return; const hit = projectToRoute(deps.routePoints(), model.x, model.y); @@ -83,7 +81,7 @@ export function mountViewerStructureMenu(deps: ViewerMenuDeps): void { menu.open( event.clientX - rect.left, event.clientY - rect.top, - structureGroupMenuItems(types, (typeId) => deps.onPick(chainage, typeId)), + structureGroupMenuItems(groups, (typeId) => deps.onPick(chainage, typeId)), ); }); } diff --git a/B06_Section/B06_Section_UI_Section_View_Menu.ts b/B06_Section/B06_Section_UI_Section_View_Menu.ts index b78b31fc5..cfddee991 100644 --- a/B06_Section/B06_Section_UI_Section_View_Menu.ts +++ b/B06_Section/B06_Section_UI_Section_View_Menu.ts @@ -14,6 +14,7 @@ import type { StructureInstance, StructureType } from "../B05_Profile/B05_Profil import { structureAnchorM } from "../B05_Profile/B05_Profile_Api_Structures"; import type { IrregularStation } from "../B05_Profile/B05_Profile_UI_IrregularStations"; import { mountStructureMenu } from "../B05_Profile/B05_Profile_UI_Profile_Structures"; +import { pickGroups } from "../B05_Profile/B05_Profile_UI_Structures_Panel_Types"; /** 종단 그래프에서 구조물을 넣고 빼는 길 — 페이지가 물려 준다. 없으면 메뉴를 안 붙인다. */ export interface SectionStructureEdit { @@ -122,11 +123,7 @@ export function mountSectionStructureMenu( else input.edit.removeStructure(station.id); }, onAddPipe: (chainageM) => input.edit.addPipe(chainageM), - structureTypes: input.types.map((type) => ({ - type_id: type.type_id, - group: type.group, - name: type.name, - })), + structureGroups: () => pickGroups(input.types, () => true), // B06 패널 거르기 = 전부(129-1) onAddStructureType: (chainageM, typeId) => input.edit.addStructureType(chainageM, typeId), }); } diff --git a/resources/tester/helper_129_menu_equals_panel.cjs b/resources/tester/helper_129_menu_equals_panel.cjs new file mode 100644 index 000000000..64e9e46b3 --- /dev/null +++ b/resources/tester/helper_129_menu_equals_panel.cjs @@ -0,0 +1,82 @@ +/* 129-1 우클릭 목록 = 좌측 패널 목록 헬퍼 — 종류 목록 JSON(argv[2] · 서버 응답) · 구조물 정의(argv[3])를 + * 화면처럼 `withDefinition` 으로 얹어 패널 군 · 종류 드롭다운(`groupOptions` · `typeOptions` — `syncTypeOptions` 와 같은 후보)과 우클릭 메뉴 + * (`structureGroupMenuItems(pickGroups)`)를 B05(켜진 종류) · B06(전부) 두 거르기로 뽑아 표준출력 JSON 한 벌로. + * TS 로더는 `helper_116_right_click_add.cjs` 와 같다. + */ +const fs = require("fs"); +const path = require("path"); +const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); + +const ROOT = path.join(__dirname, "..", ".."); +const cache = new Map(); +const STUBS = { "@config/config_frontend": { API_BASE_URL: "/api", API_TIMEOUT_MS: 1000 } }; +// 드롭다운 항목 — 브라우저 `new Option(text, value)` 과 같은 칸만. +globalThis.Option = class { + constructor(text, value) { + this.text = text; + this.value = value; + this.hidden = false; + this.dataset = {}; + } +}; + +function loadTs(file) { + const full = path.resolve(file); + if (cache.has(full)) return cache.get(full); + const js = ts.transpileModule(fs.readFileSync(full, "utf8"), { + compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, + }).outputText; + const box = { exports: {} }; + cache.set(full, box.exports); + const localRequire = (request) => { + if (STUBS[request]) return STUBS[request]; + if (request.startsWith(".")) return loadTs(path.join(path.dirname(full), `${request}.ts`)); + if (request.startsWith("@")) return new Proxy({}, { get: () => () => {} }); + if (request.endsWith(".css")) return {}; + return require(request); + }; + new Function("exports", "module", "require", js)(box.exports, box, localRequire); + cache.set(full, box.exports); + return box.exports; +} + +const B05 = path.join(ROOT, "B05_Profile"); +const T = loadTs(path.join(B05, "B05_Profile_UI_Structures_Panel_Types.ts")); +const M = loadTs(path.join(B05, "B05_Profile_UI_Profile_Structures.ts")); +const D = loadTs(path.join(B05, "B05_Profile_UI_Structures_Define.ts")); + +const read = (file) => JSON.parse(fs.readFileSync(file, "utf8")); +const types = D.withDefinition(read(process.argv[2]), read(process.argv[3])); + +/** 패널 드롭다운에 보이는 것 — 군(차례 · 이름) → 그 군 고르면 보이는 종류(차례 · 이름 · id). */ +function panelList(pick) { + return T.groupOptions(types, pick) + .filter((option) => !option.hidden) + .map((group) => { + const candidates = types.filter((t) => t.group === group.value && D.pickable(t)); + const kinds = T.typeOptions(candidates, pick).filter((option) => !option.hidden); + return { label: group.text, types: kinds.map((option) => [option.text, option.value]) }; + }); +} + +/** 우클릭 메뉴 — 군 라벨 → 종류 이름 · 누르면 넘어가는 id. */ +function menuList(pick) { + const picked = []; + const items = M.structureGroupMenuItems(T.pickGroups(types, pick), (id) => picked.push(id)); + return items.map((item) => ({ + label: item.label, + types: item.children.map(([name, run]) => { + run(); + return [name, picked.pop()]; + }), + })); +} + +const enabled = (t) => t.enabled; +const all = () => true; +process.stdout.write( + JSON.stringify({ + b05: { panel: panelList(enabled), menu: menuList(enabled) }, + b06: { panel: panelList(all), menu: menuList(all) }, + }), +); diff --git a/resources/tester/test_129_menu_equals_panel.py b/resources/tester/test_129_menu_equals_panel.py new file mode 100644 index 000000000..34ce1ff11 --- /dev/null +++ b/resources/tester/test_129_menu_equals_panel.py @@ -0,0 +1,89 @@ +# -*- coding: utf-8 -*- +"""129-1 종단 우클릭 구조물군 · 종류 목록 = 좌측 패널 「구조물 배치」 군 · 종류 드롭다운. + +못박는 것 + - 실제 레지스트리(`with_visible`) + 구조물 정의(`구조물정의.json`)로 화면과 같은 종류 목록을 세워, + 패널 드롭다운에 보이는 군(차례 · 이름) · 종류(차례 · 이름 · id) = 우클릭 메뉴 — B05(켜진 종류) · B06(전부). + - 메뉴 길 넷(B05 종단 · 배수유역도 · 3D · B06 종단 = 한 profile)이 모두 패널 `pickGroups()` 를 봄. + +Node 헬퍼 `helper_129_menu_equals_panel.cjs` 가 패널 · 메뉴 목록을 뽑음. 화면은 사용자 확인. +""" + +import json +import os +import subprocess + +import pytest + +from M02_MasterTemplete.StructOptions import M02_SO_Definition as definition +from M02_MasterTemplete.StructOptions.M02_SO_Options import with_visible + +HERE = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.abspath(os.path.join(HERE, "..", "..")) + + +def _read(*parts: str) -> str: + with open(os.path.join(ROOT, *parts), encoding="utf-8") as f: + return f.read() + + +@pytest.fixture(scope="module") +def out(tmp_path_factory): + folder = tmp_path_factory.mktemp("t129") + types_path = folder / "types.json" + definition_path = folder / "definition.json" + types_path.write_text(json.dumps(with_visible().model_dump()["types"]), "utf-8") + definition_path.write_text(json.dumps(definition.read()), "utf-8") + proc = subprocess.run( + [ + "node", + os.path.join(HERE, "helper_129_menu_equals_panel.cjs"), + str(types_path), + str(definition_path), + ], + capture_output=True, + text=True, + encoding="utf-8", + timeout=60, + ) + assert proc.returncode == 0, proc.stderr + return json.loads(proc.stdout) + + +@pytest.mark.parametrize("page", ["b05", "b06"]) +def test_우클릭_목록은_패널_드롭다운과_같음(out, page): + panel, menu = out[page]["panel"], out[page]["menu"] + assert panel, "패널 목록이 비면 대조가 안 됨" + assert menu == panel # 군 차례 · 이름 · 종류 차례 · 이름 · 누르면 넘어가는 id + + +def test_B05_는_꺼진_종류를_안_보임(out): + shown = {type_id for group in out["b05"]["menu"] for _, type_id in group["types"]} + off = {t["type_id"] for t in with_visible().model_dump()["types"] if not t["enabled"]} + assert off and not shown & off + + +def test_옛_대피_종류는_어느_쪽도_안_보임(out): + for page in ("b05", "b06"): + shown = {type_id for group in out[page]["menu"] for _, type_id in group["types"]} + assert not shown & {"refuge", "work_yard", "turnaround"} + + +def test_메뉴_길_넷이_패널_목록을_봄(): + assert "pickGroups: () => pickGroups(types, canPick)" in _read( + "B05_Profile", "B05_Profile_UI_Structures_Panel.ts" + ) + # B05 · B06 한 profile — 종단그래프 · 배수유역도 우클릭은 공용 콜백 하나 + assert "structureGroups: () => sharedStructures(shared).pickGroups()" in _read( + "A00_Common", "design", "design_shared.ts" + ) + assert "structureGroups: callbacks?.structureGroups," in _read( + "B05_Profile", "B05_Profile_UI_Profile_Render.ts" + ) + assert "structureGroupMenuItems(callbacks?.structureGroups?.() ?? []" in _read( + "A00_Common", "design", "design_profile.ts" + ) + # 3D + assert "structureGroups: () => deps.panel().structures.pickGroups()" in _read( + "B05_Profile", "B05_Profile_UI_Page_Structures.ts" + )