fix(B05): 종단 · 배수유역도 · 3D · B06 우클릭 구조물군 · 종류 목록 = 좌측 패널 드롭다운 목록 한 벌(129-1)

- 패널이 자기 드롭다운 목록(pickGroups — 군 차례 · 정의 이름 · 켜진 종류 · 옛 대피 뺌 · 숨김)을 내고 우클릭 메뉴가 그것을 그대로 씀
- 옛 메뉴 = 꺼진 종류(F 야생동물 이동통로 · 비탈면 녹화 · G 쇄석 포설) · 옛 대피 종류(대피소 · 정차작업장 · 차돌림곳) 더 보임 · 군 이름은 정의(48) 대신 고정 이름
- 시험 test_129(실제 레지스트리 + 정의로 패널 = 메뉴 · B05 · B06)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UoSr2sqvYtBf1dtQdus88s
This commit is contained in:
2026-10-08 20:10:19 +09:00
co-authored by Claude Opus 5.5
parent 422b5d41ca
commit 6c2ae466c9
12 changed files with 246 additions and 64 deletions
+3 -4
View File
@@ -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();
+1
View File
@@ -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"),
},
@@ -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),
});
},
@@ -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). */
+2 -7
View File
@@ -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). 자동 배수관도
@@ -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<StructureMenuType>,
groups: ReadonlyArray<PickGroup>,
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<PickGroup>;
/** 레지스트리 타입을 골라 넣을 때. */
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] => [
@@ -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;
@@ -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<StructureType>,
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[];
+7 -9
View File
@@ -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<StructureMenuType>;
/** 사이드 「구조물 배치」 군 · 종류 고르기 목록 그대로(129-1). */
structureGroups: () => ReadonlyArray<PickGroup>;
/** 종류 선택 — 사이드 폼 자동 지정(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)),
);
});
}
@@ -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),
});
}
@@ -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) },
}),
);
@@ -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"
)