feat(B06): 구조물 선택 = 카드 선택·면적 강조와 같은 레벨, 붉은 강조

- 강조 색을 --color-danger(붉은색)로 변경.
- 구조물을 고르면 그 측점 카드도 같이 선택된다. 이미 선택된 카드 안에서 다른
  구조물을 고를 때는 강조만 옮겨 가고 카드 선택은 건드리지 않는다.
- 구조물 선택과 절·성토 면적 강조는 같은 레벨 — 하나를 고르면 다른 하나가 풀린다.
  카드가 선택에서 빠져도 구조물 선택은 풀린다.
- 조정 버튼이 안 보이던 원인: .b06-cross-card__revetctl 스타일과 .is-hidden 규칙이
  아예 없었다. 스타일을 넣고 "기슭막이" 라벨을 붙여 표시 반폭 그룹과 구분했다.

미선택 카드에서 구조물이 먹히지 않던 버그도 잡았다 — 면적 비우기(onAreaSelect)가
먼저 카드를 선택해 버려, 뒤이은 onSelect가 "같은 측점 재클릭 = 해제"로 상쇄됐다.

검증(공용 브라우저 4+4.3): 미선택 카드에서 구조물 클릭 → 벽 강조 + 화살표 + 카드
선택. 같은 카드 다른 구조물 → 강조만 이동. 면적칩 → 구조물 해제. 다시 구조물 →
면적 해제. tsc 통과, pytest 148 pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-21 16:28:27 +09:00
co-authored by Claude Opus 5
parent 5eed5e801c
commit 83a1309a37
4 changed files with 49 additions and 4 deletions
+26 -2
View File
@@ -211,8 +211,14 @@ export function createCrossSectionCard(
let activeRevet: RevetKey | null = revetOffset?.selectedFor(section) ?? null;
let setRevetActive: RevetHighlightSetter = () => undefined;
let showRevetControl: (visible: boolean) => void = () => undefined;
/** 기슭막이를 고르면 면적 강조는 끈다 — **구조물 선택이 우선**(2026-08-21 사용자 ①). */
/**
* 기슭막이를 고른다. 구조물 선택과 절·성토 면적 강조는 **같은 레벨**이라 하나를
* 고르면 다른 하나는 풀린다(2026-08-21 사용자). 또 구조물을 고르면 그 **측점 카드도
* 같이 선택**된다 — 이미 선택된 카드 안에서 다른 구조물을 고를 때는 선택이 옮겨만
* 가고 카드 선택은 건드리지 않는다.
*/
const toggleRevet = (key: RevetKey): void => {
const wasSelected = isSelected;
activeRevet = activeRevet === key ? null : key;
setRevetActive(activeRevet);
showRevetControl(activeRevet !== null);
@@ -221,7 +227,14 @@ export function createCrossSectionCard(
activeArea = null;
setBandActive(null);
setChipActive(null);
onAreaSelect?.(section.station_id, null);
}
if (activeRevet !== null) {
// 미선택 카드는 **카드 선택만** 부른다. 여기서 면적 비우기(onAreaSelect)를 먼저
// 부르면 그쪽이 카드를 선택해 버려, 뒤이은 onSelect가 "같은 측점 재클릭 = 해제"로
// 먹혀 아무 일도 일어나지 않는다(2026-08-21 화면 확인). 카드 선택 경로가 부모의
// 면적 강조를 이미 비운다.
if (wasSelected) onAreaSelect?.(section.station_id, null);
else onSelect(section.station_id);
}
};
card.applySelection = (nextSelected, areaKey) => {
@@ -230,6 +243,13 @@ export function createCrossSectionCard(
activeArea = nextSelected ? (areaKey ?? null) : null;
setBandActive(activeArea);
setChipActive(activeArea);
// 면적이 켜졌거나 카드가 선택에서 빠지면 구조물 선택은 풀린다(같은 레벨).
if ((activeArea !== null || !nextSelected) && activeRevet !== null) {
activeRevet = null;
setRevetActive(null);
showRevetControl(false);
revetOffset?.select(section.chainage_m, null);
}
};
const toggleArea = (key: CrossAreaKey): void => {
if (!isSelected) {
@@ -613,7 +633,11 @@ export function createCrossSectionCard(
revetOffset.adjust(section.chainage_m, activeRevet, screenDeltaM * outwardOf(activeRevet));
}
};
const revetLabel = document.createElement("span");
revetLabel.className = "b06-cross-card__revetctl-label";
revetLabel.textContent = L("B06_Cross_Revet_Label");
revetControl.append(
revetLabel,
makeButton("◀", L("B06_Cross_Revet_Left"), nudge(0.1)),
makeButton("▶", L("B06_Cross_Revet_Right"), nudge(-0.1)),
makeButton("↺", L("B06_Cross_Revet_Reset"), () => {
@@ -563,6 +563,26 @@
margin-left: auto;
}
/* 기슭막이 X 자리 ◀/▶/↺(2026-08-21) — 벽을 골랐을 때만 보인다. 표시 반폭 그룹과
나란히 서므로 어느 쪽 조작구인지 라벨로 가른다. */
.b06-cross-card__revetctl {
display: flex;
flex: 0 0 auto;
align-items: center;
gap: 2px;
margin-left: var(--spacing-8);
}
.b06-cross-card__revetctl.is-hidden {
display: none;
}
.b06-cross-card__revetctl-label {
color: var(--color-danger);
font-size: var(--text-caption);
margin-right: 2px;
}
/* 줌 버튼 — 그래프 우측 상단 오버레이. 휠 대신 쓰는 조작구다(2026-08-02 사용자 확정). */
.b06-cross-card__zoom {
position: absolute;
@@ -193,8 +193,8 @@
}
.b06-chart__culvert-revet.is-active {
fill: color-mix(in srgb, var(--color-royal-amethyst) 45%, transparent);
stroke: var(--color-royal-amethyst);
fill: color-mix(in srgb, var(--color-danger) 45%, transparent);
stroke: var(--color-danger);
stroke-width: 2.2;
}
+1
View File
@@ -239,6 +239,7 @@ export const ui_locales_b2 = {
"기슭막이 선택 — 화살표로 자리 조절",
"Select revetment — nudge with arrows",
],
B06_Cross_Revet_Label: ["기슭막이", "Revetment"],
B06_Cross_Revet_Left: ["기슭막이 0.1m 왼쪽으로", "Move revetment 0.1m left"],
B06_Cross_Revet_Right: ["기슭막이 0.1m 오른쪽으로", "Move revetment 0.1m right"],
B06_Cross_Revet_Reset: ["기슭막이 자동 자리로 초기화", "Reset revetment to solved position"],