feat(종단보기): 345-4 노선 추천 → 좌측 새 칸 「노선 추천」(지운 「계획노선」 자리) — [노선 추천] · 현재 추천 · [초기화] · 추천 창 공용 모달 하나 · [적용] = 본문 평면 되돌리기 한 칸

- 새 _Panel_Recommend(칸 재료) · 칸 차례 30 · 편집 머리에서 단추 셋 지움
- _RouteEdit_Initial 이 칸 요소를 밖에서 받음 · 추천 창 「지금 노선」 = 본문 편집기 지금 노드
- 추천 창 키 막기 주석 정리(뒤 판 Ctrl+Z 끊음 그대로)
- 시험: test_345_4_recommend_panel 4 · 칸 · 머리 시험 고쳐 씀 · 관련 415 통과(옛 3 실패 그대로) · typecheck 통과

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VmC34fsxxbweEG3RUjs3Mn
This commit is contained in:
2026-10-16 11:27:32 +09:00
co-authored by Claude Opus 5.5
parent d985c24f5c
commit 58c86e525f
15 changed files with 165 additions and 35 deletions
+1
View File
@@ -487,6 +487,7 @@ export async function mountProfileView(
contourIntervalM: latestResponse.surface_params.contour_interval_m,
smooth: latestResponse.surface_params.smooth,
tools: [view3d.modal.button],
recommend: panel.recommend,
});
// ③ 종단면·횡단 자료 — 하단 패널을 3D보다 먼저 채운다.
+5 -1
View File
@@ -23,6 +23,7 @@ import {
parseOptional,
type RoutePanelValues,
} from "./B05_Profile_UI_Panel_Fields";
import { createRecommendPart } from "./B05_Profile_UI_Panel_Recommend";
import { createSettingsPart } from "./B05_Profile_UI_Panel_Settings";
import { createViewControls } from "./B05_Profile_UI_Panel_View";
@@ -71,6 +72,7 @@ interface PanelCallbacks {
export function createRoutePanel(callbacks: PanelCallbacks, shared: DesignPageShared) {
const { viewControls, setCorridorPending } = createViewControls(callbacks);
const settings = createSettingsPart(callbacks);
const recommend = createRecommendPart(); // 노선 추천 칸 — 동작은 노선 편집기가 건다(345-4)
const {
contourInterval,
gradeClass,
@@ -119,11 +121,13 @@ export function createRoutePanel(callbacks: PanelCallbacks, shared: DesignPageSh
// 좌측 패널 = 한 벌 등록부(`design_shared`) — 종단 보기 칸 재료만 넣는다. 아래 단추 넷 · 저장 ·
// [확정]은 한 벌이 두 보기 몫을 모아 낸다.
Object.assign(shared.parts, { settings });
Object.assign(shared.parts, { settings, recommend });
sharedPanel(shared);
return {
viewControls,
/** 「노선 추천」 칸 요소 — 노선 편집기(`_RouteEdit_Initial`)에 넘긴다. */
recommend,
/** 「구조물 배치」 섹션 API(타입 주입·목록 교체·계곡 시설 병합·선택·추가/이동/삭제). */
structures: structures as StructuresSection,
/** 이어 공사 시작 기준(측점번호·누가거리 오프셋) 현재값. */
@@ -0,0 +1,29 @@
/* =============================================================================
* B05_Profile_UI_Panel_Recommend.ts
* 좌측 「노선 추천」 칸 재료(345-4 · 지운 「계획노선」 자리) — [노선 추천] 단추 · 「현재 추천: ○○」 ·
* [초기화]. 노선 편집 머리에 있던 것을 옮겼다. 동작(추천 창 · 초기값 싣기)은 편집기의
* `_RouteEdit_Initial` 이 이 요소를 받아 건다 — 여기는 생김새만.
* ========================================================================== */
export interface RecommendPart {
recommendButton: HTMLButtonElement;
resetButton: HTMLButtonElement;
nowLine: HTMLElement;
/** 칸 몸 — 화면 순서대로. */
nodes: HTMLElement[];
}
export function createRecommendPart(): RecommendPart {
const nowLine = document.createElement("span");
nowLine.className = "b05-routeedit__rec-now";
const actions = document.createElement("div");
actions.className = "b05-route__rec-actions";
actions.innerHTML = `
<button type="button" class="ui-btn ui-btn--ghost" data-act="recommend" disabled
title="예상노선 추적 · 영선 · 토공 최소 · 기울기 최소 · 비중 계산 중 하나로 노선을 추천받아 미리 봅니다 — [적용]은 평면에 되돌리기 한 칸">노선 추천</button>
<button type="button" class="ui-btn ui-btn--ghost" data-act="reset" disabled
title="전처리 때 만든 초기값 노선(예상노선 추적)으로 되돌립니다 — 다시 계산은 [노선 반영]에서">초기화</button>`;
const recommendButton = actions.querySelector<HTMLButtonElement>('[data-act="recommend"]')!;
const resetButton = actions.querySelector<HTMLButtonElement>('[data-act="reset"]')!;
return { recommendButton, resetButton, nowLine, nodes: [nowLine, actions] };
}
+17 -3
View File
@@ -1,16 +1,18 @@
/* =============================================================================
* B05_Profile_UI_Panel_Sections.ts
* 종단 보기 좌측 패널의 **칸 정의**(등록부 `registerPanelSection`) — 페이지 설정.
* 종단 보기 좌측 패널의 **칸 정의**(등록부 `registerPanelSection`) — 페이지 설정 · 노선 추천.
* 「구조물 배치」는 두 보기 공용이라 `design_sections.ts` 에 있다.
*
* 칸 차례(2026-08-18 사용자 확정): 구조물 배치 > 페이지 설정 > 아래 고정 영역.
* 칸 차례(2026-08-18 사용자 확정): 구조물 배치 > 페이지 설정 > 노선 추천 > 아래 고정 영역.
* 「선택 포인트 상세 설정」(3D 노선 점 옮김 · 지움 · 저장 안 되는 길)은 지움(345-2) ·
* 「계획노선」 칸 · [계획노선 편집]은 지움(345-3 — 노선 편집은 가운데 위 본문).
* 「계획노선」 칸 · [계획노선 편집]은 지움(345-3 — 노선 편집은 가운데 위 본문) · 그 자리에
* 「노선 추천」(345-4 · 편집 머리에서 옮김 · 재료 `_Panel_Recommend`).
* 「경로 계산 설정」은 `Old/` 로 옮김(54-24). 재료는 `_Panel_Settings` 가 만들어 `ctx.parts` 로 건넨다.
* ========================================================================== */
import "../A00_Common/design/design_sections";
import { createSectionFrame, part, registerPanelSection } from "../A00_Common/design/design_panel";
import type { RecommendPart } from "./B05_Profile_UI_Panel_Recommend";
import type { SettingsPart } from "./B05_Profile_UI_Panel_Settings";
registerPanelSection({
@@ -24,3 +26,15 @@ registerPanelSection({
return frame.root;
},
});
registerPanelSection({
id: "route-recommend",
title: "노선 추천",
views: ["profile"],
order: 30,
build: (ctx, view) => {
const frame = createSectionFrame(view, "노선 추천");
frame.body.append(...part<RecommendPart>(ctx, "recommend").nodes);
return frame.root;
},
});
+6 -3
View File
@@ -67,7 +67,7 @@ import {
routeLocksOf,
writeRouteLocks,
} from "./B05_Profile_UI_RouteEdit_Lock";
import { createInitialPanel } from "./B05_Profile_UI_RouteEdit_Initial";
import { createInitialPanel, type InitialPanelParams } from "./B05_Profile_UI_RouteEdit_Initial";
import { createProfileStrip } from "./B05_Profile_UI_RouteEdit_ProfileStrip";
import {
applyArcLocks,
@@ -104,6 +104,8 @@ export interface RouteEditOptions {
smooth?: boolean;
/** 머리 도구줄 끝에 앉힐 부른 쪽 단추([3D] 등). */
tools?: HTMLElement[];
/** 좌측 「노선 추천」 칸 요소 — [노선 추천] · [초기화] · 현재 추천(345-4). */
recommend: InitialPanelParams["controls"];
}
/** 편집기가 사는 보기 — 보임 · 숨김 · 떠남. */
@@ -115,7 +117,7 @@ export async function mountRouteEditor(
host: RouteEditHost,
projectId: string,
onApplied: () => void | Promise<void>,
options: RouteEditOptions = {},
options: RouteEditOptions,
): Promise<void> {
const stationIntervalM = options.stationIntervalM ?? 20;
const chrome = createRouteEditChrome(parent, options.tools);
@@ -269,8 +271,9 @@ export async function mountRouteEditor(
};
/** 초기노선 갈래 — 로직 단추 · [초기화] · 비교값(계획서 20). 서버 묻기는 `_Initial` 몫. */
const initial = createInitialPanel({
overlay,
controls: options.recommend,
projectId,
planned: () => planned,
planMinRadiusM: () => minRadiusM,
apply: (snapshot, message) => restoreSnapshot(snapshot, message, true),
});
@@ -13,7 +13,8 @@ import { createButton } from "@ui/ui_template_elements";
*
* 자리(345-3) — 모달이 아니라 종단 보기 가운데 위 본문(옛 3D 자리)에 붙는다. 덮개 · 전체 화면 ·
* 닫기 단추는 걷었다 · [취소] → [반영 전으로] · [확인] → [노선 반영] · 오른쪽 횡단 칸은 접는 칸.
* 머리 끝 `tools` 자리에 부른 쪽 단추([3D])가 앉는다.
* 머리 `tools` 자리에 부른 쪽 단추([3D])가 앉는다. [노선 추천] · 현재 추천 · [초기화]는 좌측
* 「노선 추천」 칸으로 옮겼다(345-4 · `_Panel_Recommend`).
* ========================================================================== */
/** 회전 단추 아이콘 — **반만 도는 화살표**(2026-09-12 사용자 지시 ㉙). 한 바퀴를 다 그린
@@ -53,11 +54,8 @@ export function createRouteEditChrome(
<div class="b05-routeedit__box" aria-label="계획노선 편집">
<div class="b05-routeedit__head">
<h3>계획노선</h3>
<span class="b05-routeedit__rec-now" data-rec-now></span>
<span class="b05-routeedit__actions">
<span data-slot="tools"></span>
<button type="button" class="ui-btn ui-btn--ghost" data-act="recommend" disabled
title="예상노선 추적 · 영선 · 토공 최소 · 기울기 최소 · 비중 계산 중 하나로 노선을 추천받아 미리 봅니다">노선 추천</button>
<i class="b05-routeedit__divider" aria-hidden="true"></i>
<label class="b05-routeedit__terrain" hidden
title="편집 창 분석(등고선 · 지반고 · 기울기 · 횡단 미리보기 · 종단 띠 · 추천)에 쓸 지형 — 확정 지표면 · 종횡단은 안 바뀝니다">분석 지형
@@ -79,8 +77,6 @@ export function createRouteEditChrome(
title="되돌리기 (Ctrl+Z)" disabled>↶ 되돌리기</button>
<button type="button" class="ui-btn ui-btn--ghost" data-act="redo"
title="다시하기 (Ctrl+Y)" disabled>↷ 다시하기</button>
<button type="button" class="ui-btn ui-btn--ghost" data-act="reset" disabled
title="전처리 때 만든 초기값 노선(예상노선 추적)으로 되돌립니다 — 다시 계산은 [노선 반영]에서">초기화</button>
<button type="button" class="ui-btn ui-btn--ghost" data-act="cancel"
title="마지막 [노선 반영] 뒤 고친 노선을 버리고 그때 노선으로 되돌립니다 — 되돌리기로 다시 살릴 수 있습니다">반영 전으로</button>
<button type="button" class="ui-btn ui-btn--filled" data-act="apply"
@@ -1,7 +1,8 @@
/* =============================================================================
* B05_Profile_UI_RouteEdit_Initial.ts
* 계획노선 편집 창의 **노선 추천 이음**(계획서 20 · 23-2) — 도구 막대 [노선 추천] 단추 →
* 모달(`_Recommend`) · [초기화] · 「현재 추천: ○○」 · [확인]에 실을 지금 추천.
* 계획노선 편집의 **노선 추천 이음**(계획서 20 · 23-2) — 좌측 「노선 추천」 칸(345-4) [노선 추천] 단추 →
* 모달(`_Recommend`) · [초기화] · 「현재 추천: ○○」 · [노선 반영]에 실을 지금 추천. 칸 요소는
* 밖(`_Panel_Recommend`)에서 받는다.
*
* 모달의 알약이 서버에 갈래를 묻고(`/route/initial`) 미리 보기만 한다. [적용]을 눌러야 받은 노드를
* **편집값에 싣고**(되돌리기 한 걸음) 캐시(`routeedit-initial`)에 적는다. [초기화]는 서버를 다시
@@ -42,8 +43,15 @@ import {
import { openRecommendModal } from "./B05_Profile_UI_RouteEdit_Recommend";
export interface InitialPanelParams {
overlay: HTMLElement;
/** 단추 · 글 요소 — 좌측 「노선 추천」 칸 것(345-4). */
controls: {
recommendButton: HTMLButtonElement;
resetButton: HTMLButtonElement;
nowLine: HTMLElement;
};
projectId: string;
/** 지금 편집기 노드 — 추천 창이 「지금 노선」으로 같이 그린다. */
planned: () => Array<[number, number]>;
/** `/route/plan` 이 준 최소 R — 서버 갈래 기준값이 오기 전 보일 값. */
planMinRadiusM: () => number;
/** 받은 노선을 편집값으로 싣는다 — 되돌리기 한 걸음으로 쌓인다. */
@@ -75,10 +83,8 @@ const copy = (snapshot: RouteEditSnapshot): RouteEditSnapshot =>
JSON.parse(JSON.stringify(snapshot)) as RouteEditSnapshot;
export function createInitialPanel(params: InitialPanelParams): InitialPanel {
const { overlay, projectId } = params;
const recommendButton = overlay.querySelector<HTMLButtonElement>('[data-act="recommend"]')!;
const resetButton = overlay.querySelector<HTMLButtonElement>('[data-act="reset"]')!;
const nowLine = overlay.querySelector<HTMLElement>("[data-rec-now]")!;
const { projectId } = params;
const { recommendButton, resetButton, nowLine } = params.controls;
/** 창을 열 때 캐시에 있던 추천 — [취소]가 돌아갈 자리. */
let openingChoice = readInitialChoice(projectId);
@@ -146,7 +152,7 @@ export function createInitialPanel(params: InitialPanelParams): InitialPanel {
logic: (choice ?? base).logic,
planMinRadiusM: params.planMinRadiusM(),
expected: (plan?.expected ?? []).map(([x, y]) => [x, y]),
current: opening!.planned.map(([x, y]) => [x, y]),
current: params.planned().map(([x, y]) => [x, y]),
request,
onApply: (picked) => {
choice = picked;
@@ -557,7 +557,8 @@ export function openRecommendModal(params: RecommendParams): void {
},
});
// 편집 창 단축키(Ctrl+Z 등)가 뒤에서 노선을 되돌리지 않게 막는다. 칸 안의 글자 되돌리기는 그대로.
// 본문 평면 · 종단도 단축키(Ctrl+Z 등 · 창에서 들음)가 뒤에서 되돌리지 않게 막는다 — 칸 안 글자
// 되돌리기는 그대로(345-4 · 이제 모달은 이 창 하나).
(bodyEl as HTMLElement | null)
?.closest(".ui-modal__backdrop")
?.addEventListener("keydown", (event) => {
@@ -1,15 +1,20 @@
/* 노선 추천 모달(계획서 23-2) — 알약 넷 · 미리 보기 지도 · 비중 · 제약 칸 · 비교값.
색은 전부 테마 토큰을 쓴다(밝음 · 어두움). 계획노선 편집 창(`.b05-routeedit`) 위에 뜬다. */
색은 전부 테마 토큰을 쓴다(밝음 · 어두움). 종단 보기 화면 위에 뜬다(모달 하나 · 345-4). */
/* 편집 창 제목 옆 「현재 추천: ○○」. */
/* 좌측 「노선 추천」 칸(345-4) — 「현재 추천: ○○」 + [노선 추천] [초기화]. */
.b05-routeedit__rec-now {
flex: none;
color: var(--color-text-secondary);
font-size: var(--text-caption);
white-space: nowrap;
}
/* 곡선 라벨 패널(`.b05-routeedit__label` = 모달 + 1) 위로 — 모달과 뒤 막이 함께 올라간다. */
.b05-route__rec-actions {
display: flex;
flex-wrap: wrap;
gap: var(--spacing-8);
}
/* 곡선 라벨 패널 · 페이지 막들 위로 — 모달과 뒤 막이 함께 올라간다. */
.ui-modal__backdrop.b05-routerec-backdrop {
z-index: calc(var(--z-modal, 1000) + 2);
}
@@ -176,6 +176,7 @@ const panel = design.createDesignPanel(
parts: {
structures: { root: node("section"), listRoot: node() },
settings: { nodes: [text("a")] },
recommend: { nodes: [text("rec")] },
crossView: { row: text("cross-row") },
standardSlot: text("std"),
},
@@ -197,6 +197,7 @@ const built = panel.createDesignPanel(
parts: {
structures: sample(),
settings: { nodes: [text("a"), text("b")] },
recommend: { nodes: [text("rec")] },
crossView: { row: text("cross-row") },
standardSlot: text("std"),
},
@@ -42,6 +42,7 @@ def out():
def test_B05_B06_첫_그림_접힘(out):
assert out["profileFirst"] == {
"페이지 설정": True,
"노선 추천": False, # 345-4 단추 칸 — 펼친 채
}
assert out["crossFirst"]["B06_View_Title"] is True
assert out["crossFirst"]["B06_Std_Title"] is True # 옛 기본 접힘 그대로
@@ -0,0 +1,61 @@
"""345-4 노선 추천 → 좌측 새 칸 「노선 추천」(지운 「계획노선」 자리) — [노선 추천] · 현재 추천 · [초기화].
· 칸 요소는 `_Panel_Recommend` 가 만들고 편집기(`_RouteEdit_Initial`)가 밖에서 받아 건다
· 추천 창은 공용 모달 하나 · [적용] = 본문 평면에 되돌리기 한 칸(`restoreSnapshot(…, true)` → 이력 한 걸음)
· 추천 창의 「지금 노선」은 본문 편집기 지금 노드
칸 생김새 · 차례는 `test_design_view_parity` · `test_311_panel_folded` 가 봄(화면 확인은 브레인).
"""
from __future__ import annotations
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
def _read(rel: str) -> str:
return (ROOT / rel).read_text(encoding="utf-8")
def test_칸_자리와_재료():
sections = _read("B05_Profile/B05_Profile_UI_Panel_Sections.ts")
at = sections.index('id: "route-recommend"')
block = sections[at : at + 300]
assert 'title: "노선 추천"' in block and "order: 30," in block # 지운 계획노선 자리
assert 'part<RecommendPart>(ctx, "recommend").nodes' in block
panel = _read("B05_Profile/B05_Profile_UI_Panel.ts")
assert "const recommend = createRecommendPart();" in panel
assert "Object.assign(shared.parts, { settings, recommend });" in panel
view = _read("A00_Common/design/design_view_profile.ts")
assert "recommend: panel.recommend," in view
def test_편집기가_칸_요소를_밖에서_받음():
initial = _read("B05_Profile/B05_Profile_UI_RouteEdit_Initial.ts")
assert "overlay" not in initial
assert "const { recommendButton, resetButton, nowLine } = params.controls;" in initial
assert "current: params.planned().map(([x, y]) => [x, y])," in initial # 지금 노선
main = _read("B05_Profile/B05_Profile_UI_RouteEdit.ts")
assert "controls: options.recommend," in main
assert "planned: () => planned," in main
assert len(main.splitlines()) <= 700
def test_적용은_본문_평면에_되돌리기_한_칸():
main = _read("B05_Profile/B05_Profile_UI_RouteEdit.ts")
assert "apply: (snapshot, message) => restoreSnapshot(snapshot, message, true)," in main
restore = main[main.index("function restoreSnapshot(") :]
restore = restore[: restore.index("\n }\n")]
assert "applyEdit(message, record);" in restore
apply_edit = main[main.index("function applyEdit(") :]
assert "if (record) history?.commit(snapshotNow());" in apply_edit[:600]
initial = _read("B05_Profile/B05_Profile_UI_RouteEdit_Initial.ts")
on_apply = initial[initial.index("onApply: (picked) => {") :]
assert on_apply[:500].count("params.apply(") == 1
def test_추천_창은_공용_모달_하나():
rec = _read("B05_Profile/B05_Profile_UI_RouteEdit_Recommend.ts")
assert rec.count("openModal(") == 1
# 뒤 판(평면 · 종단도) 단축키가 창 안 Ctrl+Z 를 받지 않게 — 창 막에서 끊음
assert '?.closest(".ui-modal__backdrop")' in rec and "key.stopPropagation();" in rec
+13 -7
View File
@@ -167,13 +167,18 @@ def test_미리_보기_지도_선(results):
def test_도구_막대는_노선_추천_하나만():
"""편집 창 도구 막대 — [노선 추천] 하나 · 추천 묶음(로직 단추 · 슬라이더 · 제약 칸) 없음 ·
「연 상태로」 없음. 그 묶음은 모달(`_Recommend`)로 옮겼다."""
"""[노선 추천] 하나 · 추천 묶음(로직 단추 · 슬라이더 · 제약 칸) 없음 · 「연 상태로」 없음. 그 묶음은
모달(`_Recommend`)로 옮겼다. 345-4 — [노선 추천] · [초기화] · 현재 추천은 편집 머리에서 좌측
「노선 추천」 칸(`_Panel_Recommend`)으로 옮김."""
page = os.path.join(HERE, "..", "..", "B05_Profile")
chrome = open(os.path.join(page, "B05_Profile_UI_RouteEdit_Chrome.ts"), encoding="utf-8").read()
assert chrome.count('data-act="recommend"') == 1
for gone in ('data-act="recommend"', 'data-act="reset"', "data-rec-now"):
assert gone not in chrome
assert "연 상태로" not in chrome and "history-reset" not in chrome
assert chrome.index('data-act="recommend"') < chrome.index('data-act="contour-minor"')
part = open(os.path.join(page, "B05_Profile_UI_Panel_Recommend.ts"), encoding="utf-8").read()
assert part.count('data-act="recommend" disabled') == 1 # 단추 하나
assert part.count('data-act="reset" disabled') == 1
assert part.index('data-act="recommend" disabled') < part.index('data-act="reset" disabled')
initial = open(
os.path.join(page, "B05_Profile_UI_RouteEdit_Initial.ts"), encoding="utf-8"
).read()
@@ -203,9 +208,10 @@ def test_노선_추천_다듬기_23_3():
return open(os.path.join(page, name), encoding="utf-8").read()
chrome = read("B05_Profile_UI_RouteEdit_Chrome.ts")
rec = chrome.index('data-act="recommend"')
divider = chrome.index("b05-routeedit__divider", rec)
assert rec < divider < chrome.index('data-act="contour-minor"')
# 345-4 — [노선 추천]은 좌측 칸 · 머리 구분선은 [3D](tools) 뒤 · 되돌리기 앞 둘 그대로
tools = chrome.index('data-slot="tools"')
divider = chrome.index("b05-routeedit__divider", tools)
assert tools < divider < chrome.index('data-act="contour-minor"')
assert chrome.count("b05-routeedit__divider") == 2
shared = read("B05_Profile_UI_RouteEdit_Recommend_Map.ts")
for name in ("bindRouteEditNavigation", "createMapRotation", "fitCanvas", "metricToScreen"):
+3 -2
View File
@@ -59,8 +59,8 @@ def results():
def test_보기를_오가도_좌측_패널_단추_구조물_배치는_같은_노드(results):
assert results["sameNodes"] == {"cross": True, "back": True}
# 보기마다 새로 만들지 않는다 — 칸은 처음 보일 때 한 번만 서고 뒤로는 숨김 · 보임만.
# 아래 고정 영역 + 종단 칸 둘 + 횡단 칸 둘(345-2 선택 포인트 · 345-3 계획노선 칸 지움)
assert results["childCount"] == 1 + 2 + 2
# 아래 고정 영역 + 종단 칸 셋 + 횡단 칸 둘(345-2 선택 포인트 · 345-3 계획노선 지움 · 345-4 노선 추천)
assert results["childCount"] == 1 + 3 + 2
def test_구조물_배치는_두_보기_모두_맨_위_같은_DOM(results):
@@ -80,6 +80,7 @@ def test_보기마다_제_칸만_보인다(results):
assert views["profile"]["shownTitles"] == [
"구조물 배치",
"페이지 설정",
"노선 추천",
]
assert views["cross"]["shownTitles"] == ["구조물 배치", "횡단 보기 설정", "표준 횡단면 설정"]
assert views["back"]["shownTitles"] == views["profile"]["shownTitles"]