fix(B06): 선택해도 카드를 다시 만들지 않게, RR 밴드 폴백 불일치 해결
- 선택 반영이 아직 카드 재생성이라 그 카드의 viewBox(휠 줌·팬)가 초기화됐다. 카드 요소에 applySelection() 핸들을 달아 클래스와 강조 상태만 갈아 끼운다. 재생성을 없애기 위해 면적 밴드는 선택 여부와 무관하게 항상 깔아 둔다(평소 투명이라 화면 변화는 없다). - RR 절토를 눌러도 단면이 안 켜지던 원인은 구 데이터 폴백 불일치였다. 표는 분리 필드가 없으면 절토 전량을 측점 지반유형으로 돌려 RR에 값을 넣는데, 밴드 쪽은 cut_rock_kind가 없으면 곧장 null을 반환해 암반 밴드를 만들지 않았다. rockKindOf()로 표·밴드·강조 매핑의 폴백 규칙을 하나로 통일한다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -133,9 +133,21 @@ function bandPolygons(
|
||||
return polygons;
|
||||
}
|
||||
|
||||
/** 토사층 두께(m). 토사 측점은 암반 경계선이 없어 null, 경계선 값이 없는 암 측점은 0(전량 암). */
|
||||
/**
|
||||
* 그 측점 절토의 암종. 분리 필드가 없는 **구 데이터**는 지반유형을 그대로 쓴다 —
|
||||
* 표(`cutBreakdown`)는 그 규칙으로 RR/BR에 값을 넣는데 여기서만 null로 보면
|
||||
* 암반 밴드가 아예 안 만들어져 **표에는 값이 있는데 그림이 안 켜진다**(2026-08-02 사용자 지적).
|
||||
*/
|
||||
function rockKindOf(design: CrossDesign): CrossDesign["cut_rock_kind"] {
|
||||
if (design.cut_rock_kind) return design.cut_rock_kind;
|
||||
return design.ground_type === "soil" ? null : design.ground_type;
|
||||
}
|
||||
|
||||
/** 토사층 두께(m). 토사 측점은 암반 경계선이 없어 null, 분리 근거가 없는 암 측점은 0(전량 암). */
|
||||
function soilDepth(design: CrossDesign): number | null {
|
||||
if (!design.cut_rock_kind) return null;
|
||||
if (!rockKindOf(design)) return null;
|
||||
// 구 데이터(분리 면적 없음)는 경계선을 신뢰할 수 없으므로 전량 암으로 본다 — 표와 같은 규칙.
|
||||
if (design.cut_soil_area_m2 === undefined || design.cut_rock_area_m2 === undefined) return 0;
|
||||
return Math.abs(design.rock_boundary_offset_m ?? 0);
|
||||
}
|
||||
|
||||
@@ -200,7 +212,7 @@ export function appendCrossAreaBands(
|
||||
}
|
||||
|
||||
// 표에서 암반 칸을 눌렀을 때 이 측점의 암종과 맞는 쪽만 밴드를 켠다.
|
||||
const rockCell: CrossAreaKey = design.cut_rock_kind === "blasting_rock" ? "cut_br" : "cut_rr";
|
||||
const rockCell: CrossAreaKey = rockKindOf(design) === "blasting_rock" ? "cut_br" : "cut_rr";
|
||||
const groups = new Map<BandKey, SVGGElement>();
|
||||
for (const band of bands) {
|
||||
let pieces = bandPolygons(xs, band.top, band.bottom, x, toDisplayY);
|
||||
@@ -249,10 +261,11 @@ function cutBreakdown(design: CrossDesign): {
|
||||
};
|
||||
}
|
||||
const rock = design.cut_rock_area_m2;
|
||||
const kind = rockKindOf(design);
|
||||
return {
|
||||
ea: design.cut_soil_area_m2,
|
||||
rr: design.cut_rock_kind === "ripping_rock" ? rock : 0,
|
||||
br: design.cut_rock_kind === "blasting_rock" ? rock : 0,
|
||||
rr: kind === "ripping_rock" ? rock : 0,
|
||||
br: kind === "blasting_rock" ? rock : 0,
|
||||
total,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -36,6 +36,14 @@ import {
|
||||
validElevation,
|
||||
} from "./B06_wf3_ProfileCross_UI_Section_Common";
|
||||
|
||||
/**
|
||||
* 횡단 카드 요소. 선택 표시와 면적 강조를 **카드를 다시 만들지 않고** 갈아 끼우는 핸들을 단다
|
||||
* — 카드를 새로 만들면 사용자가 맞춰 둔 휠 줌·팬(viewBox)이 초기화되기 때문이다.
|
||||
*/
|
||||
export interface CrossCardElement extends HTMLElement {
|
||||
applySelection?: (selected: boolean, areaKey: CrossAreaKey | null) => void;
|
||||
}
|
||||
|
||||
// 절·성토 값 오버레이(그래프 상단 고정)가 계획고 선을 가리는 드문 경우를 대비해, Y 플롯
|
||||
// 최대값에 더하는 상단 여유(px). 오버레이·라벨은 손대지 않고 데이터를 그만큼 아래로 내린다.
|
||||
// 오버레이가 칩 한 줄에서 표(머리글 + 절토 + 성토 3행)로 바뀌어 그만큼 키웠다.
|
||||
@@ -238,8 +246,8 @@ export function createCrossSectionCard(
|
||||
initialAreaKey?: CrossAreaKey | null,
|
||||
/** 면적 값을 눌렀을 때. 선택되지 않은 카드에서도 눌릴 수 있어 측점 id를 함께 넘긴다. */
|
||||
onAreaSelect?: (stationId: string, key: CrossAreaKey | null) => void,
|
||||
): HTMLElement {
|
||||
const card = document.createElement("article");
|
||||
): CrossCardElement {
|
||||
const card: CrossCardElement = document.createElement("article");
|
||||
card.id = `cross-${section.station_id}`;
|
||||
card.className = `b06-cross-card${selected ? " b06-cross-card--selected" : ""}`;
|
||||
card.tabIndex = 0;
|
||||
@@ -248,6 +256,31 @@ export function createCrossSectionCard(
|
||||
if (event.key === "Enter" || event.key === " ") onSelect(section.station_id);
|
||||
});
|
||||
|
||||
// 선택 표시·면적 강조는 **DOM만 갈아 끼운다**. 카드를 새로 만들면 휠 줌·팬(viewBox)이
|
||||
// 초기화돼 사용자가 맞춰 둔 배율이 날아간다(2026-08-02 사용자 지적).
|
||||
let isSelected = selected;
|
||||
let activeArea: CrossAreaKey | null = selected ? (initialAreaKey ?? null) : null;
|
||||
let setBandActive: AreaHighlightSetter = () => undefined;
|
||||
let setChipActive: AreaHighlightSetter = () => undefined;
|
||||
card.applySelection = (nextSelected, areaKey) => {
|
||||
isSelected = nextSelected;
|
||||
card.classList.toggle("b06-cross-card--selected", nextSelected);
|
||||
activeArea = nextSelected ? (areaKey ?? null) : null;
|
||||
setBandActive(activeArea);
|
||||
setChipActive(activeArea);
|
||||
};
|
||||
const toggleArea = (key: CrossAreaKey): void => {
|
||||
if (!isSelected) {
|
||||
// 아직 선택되지 않은 카드 — 부모가 카드를 고르면서 이 면적까지 한 번에 켠다.
|
||||
onAreaSelect?.(section.station_id, key);
|
||||
return;
|
||||
}
|
||||
activeArea = activeArea === key ? null : key;
|
||||
setBandActive(activeArea);
|
||||
setChipActive(activeArea);
|
||||
onAreaSelect?.(section.station_id, activeArea);
|
||||
};
|
||||
|
||||
// 제목행 1행 구조(E-2/E-3): 측점 위치표기 → 단면유형 pill → 지반유형 → 구조물 → 측점정보.
|
||||
const header = document.createElement("header");
|
||||
const title = document.createElement("div");
|
||||
@@ -393,37 +426,20 @@ export function createCrossSectionCard(
|
||||
svg.append(svgElement("polyline", { points, class: "b06-chart__cross-profile" })),
|
||||
);
|
||||
|
||||
// 면적 밴드는 선택된 카드에만 깐다 — 다른 측점을 고르면 카드가 다시 그려지며 해제된다.
|
||||
// 다만 **값 칸은 어느 카드에서든 눌린다**: 선택 안 된 카드에서 누르면 부모가 그 카드를
|
||||
// 고르면서 누른 면적까지 한 번에 켠다(2026-08-02 사용자 지시).
|
||||
let activeArea: CrossAreaKey | null = selected ? (initialAreaKey ?? null) : null;
|
||||
let setBandActive: AreaHighlightSetter = () => undefined;
|
||||
let setChipActive: AreaHighlightSetter = () => undefined;
|
||||
const toggleArea = (key: CrossAreaKey): void => {
|
||||
if (!selected) {
|
||||
onAreaSelect?.(section.station_id, key);
|
||||
return;
|
||||
}
|
||||
activeArea = activeArea === key ? null : key;
|
||||
setBandActive(activeArea);
|
||||
setChipActive(activeArea);
|
||||
onAreaSelect?.(section.station_id, activeArea);
|
||||
};
|
||||
|
||||
if (section.design) {
|
||||
const toDisplayY = (elevation: number): number =>
|
||||
y(elevationMid + (elevation - elevationMid) * exaggeration);
|
||||
// 면적 밴드는 지면선 위·설계선 아래에 깔아 선이 밴드에 가리지 않게 한다.
|
||||
if (selected) {
|
||||
setBandActive = appendCrossAreaBands(
|
||||
svg,
|
||||
section.design,
|
||||
sourceSamples,
|
||||
x,
|
||||
toDisplayY,
|
||||
toggleArea,
|
||||
);
|
||||
}
|
||||
// 선택 여부와 무관하게 항상 깐다 — 평소에는 투명이고, 선택이 바뀔 때 카드를 새로 만들지
|
||||
// 않고 클래스만 켜면 되므로 줌·팬이 살아남는다.
|
||||
setBandActive = appendCrossAreaBands(
|
||||
svg,
|
||||
section.design,
|
||||
sourceSamples,
|
||||
x,
|
||||
toDisplayY,
|
||||
toggleArea,
|
||||
);
|
||||
// 포장층 → 설계선 → 암 경계선 순으로 겹쳐, 설계선이 포장 박스 위에 오게 한다.
|
||||
appendPavementOverlay(svg, section.design, x, toDisplayY);
|
||||
appendCrossDesignOverlay(svg, section.design, x, toDisplayY, sourceSamples);
|
||||
|
||||
@@ -25,6 +25,7 @@ import type { CrossAreaKey } from "./B06_wf3_ProfileCross_UI_Cross_Areas";
|
||||
import {
|
||||
createCrossSectionCard,
|
||||
crossCardNaturalHeight,
|
||||
type CrossCardElement,
|
||||
} from "./B06_wf3_ProfileCross_UI_Cross_View";
|
||||
import {
|
||||
createLongitudinalProfile,
|
||||
@@ -320,26 +321,22 @@ export function createSectionView(
|
||||
document.getElementById(`cross-${stationId}`)?.scrollIntoView({ behavior, block: "nearest" });
|
||||
};
|
||||
|
||||
/** 측점 id로 카드 하나만 새로 만들어 교체한다(행 높이는 draw가 정해 둔 값을 그대로 쓴다). */
|
||||
const rebuildCard = (stationId: string | null): void => {
|
||||
if (!stationId || !currentDetail) return;
|
||||
const section = currentDetail.cross_sections.find(
|
||||
(candidate) => candidate.station_id === stationId,
|
||||
);
|
||||
const existing = section && document.getElementById(`cross-${stationId}`);
|
||||
if (section && existing) {
|
||||
existing.replaceWith(buildCrossCard(section, cachedRowHeight.get(stationId)));
|
||||
}
|
||||
/** 카드 하나의 선택 표시를 갱신한다 — **다시 만들지 않는다**(줌·팬이 살아남아야 한다). */
|
||||
const markCard = (stationId: string | null): void => {
|
||||
if (!stationId) return;
|
||||
const card = document.getElementById(`cross-${stationId}`) as CrossCardElement | null;
|
||||
card?.applySelection?.(stationId === selectedStationId, activeAreaKey);
|
||||
};
|
||||
|
||||
/**
|
||||
* 선택이 바뀐 흔적만 화면에 반영한다 — **전체 재렌더 금지**.
|
||||
* `draw()`로 모든 카드를 다시 만들면 사용자가 카드마다 맞춰 둔 휠 줌·팬(viewBox)이 전부
|
||||
* 초기화된다(2026-08-02 사용자 지적). 선택 표시가 바뀌는 카드는 이전·현재 둘뿐이다.
|
||||
* 선택이 바뀐 흔적만 화면에 반영한다 — **재렌더 금지**.
|
||||
* 카드를 새로 만들면(전체든 한 장이든) 사용자가 맞춰 둔 휠 줌·팬(viewBox)이 초기화된다
|
||||
* (2026-08-02 사용자 지적). 선택 표시가 바뀌는 카드는 이전·현재 둘뿐이고, 그 둘도 클래스와
|
||||
* 강조 상태만 갈아 끼운다.
|
||||
*/
|
||||
const applySelection = (previousStationId: string | null): void => {
|
||||
rebuildCard(previousStationId);
|
||||
if (selectedStationId !== previousStationId) rebuildCard(selectedStationId);
|
||||
markCard(previousStationId);
|
||||
if (selectedStationId !== previousStationId) markCard(selectedStationId);
|
||||
drawPanel();
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user