fix(B06): 비선택 카드 값 클릭 즉시 반영, 빈 공간 선택 해제, 접기 손잡이 하단 이동
- 면적표 값·머리글을 가운데 맞춤으로 바꾼다.
- 값 칸을 모든 카드에서 버튼으로 만든다. 선택되지 않은 카드에서 눌리면 부모에 측점과 키를
함께 올려, 카드 선택과 면적 강조가 한 번에 반영된다. 강조 상태는 부모가 들고 있다가 카드를
다시 그릴 때 되살리고, 측점이 바뀌면 따라가지 않는다.
- 카드 그리드 빈 자리를 누르면 선택을 해제한다.
- 접어도 빈 배경이 남던 문제: 인라인 min-height가 .is-collapsed { height: auto }를 이겨
360px 상자가 그대로 남았다. CSS 변수로 넘기고 :not(.is-collapsed)에만 건다.
- 접기 손잡이를 헤더 캐럿에서 패널 하단 가운데 삼각형 손잡이로 옮긴다. 좌측 사이드 패널과
같은 공용 컴포넌트를 90° 돌린 모양이며, 위로 접히는 패널이라 collapseToward 옵션에
"up"/"down"을 추가했다(기존 좌우 호출부는 그대로). 리사이저는 손잡이 위로 14px 올렸다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -220,6 +220,10 @@ export function createCrossSectionCard(
|
||||
designElevation?: number,
|
||||
onDesignChange?: DesignChangeHandler,
|
||||
rockBoundary?: RockBoundaryControl,
|
||||
/** 이 카드가 선택 상태일 때 되살릴 면적 강조(없으면 강조 없음). */
|
||||
initialAreaKey?: CrossAreaKey | null,
|
||||
/** 면적 값을 눌렀을 때. 선택되지 않은 카드에서도 눌릴 수 있어 측점 id를 함께 넘긴다. */
|
||||
onAreaSelect?: (stationId: string, key: CrossAreaKey | null) => void,
|
||||
): HTMLElement {
|
||||
const card = document.createElement("article");
|
||||
card.id = `cross-${section.station_id}`;
|
||||
@@ -375,14 +379,21 @@ export function createCrossSectionCard(
|
||||
svg.append(svgElement("polyline", { points, class: "b06-chart__cross-profile" })),
|
||||
);
|
||||
|
||||
// 면적 강조는 선택된 카드에서만 붙인다 — 다른 측점을 고르면 카드가 다시 그려지며 해제된다.
|
||||
let activeArea: CrossAreaKey | null = null;
|
||||
// 면적 밴드는 선택된 카드에만 깐다 — 다른 측점을 고르면 카드가 다시 그려지며 해제된다.
|
||||
// 다만 **값 칸은 어느 카드에서든 눌린다**: 선택 안 된 카드에서 누르면 부모가 그 카드를
|
||||
// 고르면서 누른 면적까지 한 번에 켠다(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) {
|
||||
@@ -476,13 +487,15 @@ export function createCrossSectionCard(
|
||||
const chartWrap = document.createElement("div");
|
||||
chartWrap.className = "b06-cross-card__chart-wrap";
|
||||
attachZoomPan(svg, widthPx, heightPx);
|
||||
// 절·성토 면적값은 그래프 중상단 오버레이로 표시(E-4). 선택된 카드의 칩은 강조 토글이 된다.
|
||||
const readout = buildAreaReadout(
|
||||
section.design,
|
||||
selected && section.design ? toggleArea : undefined,
|
||||
);
|
||||
// 절·성토 면적값은 그래프 중상단 오버레이로 표시(E-4). 값 칸은 항상 강조 토글이다.
|
||||
const readout = buildAreaReadout(section.design, toggleArea);
|
||||
setChipActive = readout.setActive;
|
||||
chartWrap.append(svg, readout.root);
|
||||
// 다시 그려지기 전에 켜져 있던 강조를 되살린다(부모가 들고 있던 값).
|
||||
if (activeArea) {
|
||||
setBandActive(activeArea);
|
||||
setChipActive(activeArea);
|
||||
}
|
||||
// 암 경계선 제어는 그래프 X축 제목 행 우측에 배치(E-7). 암 지반에서만.
|
||||
if (rockBoundary && section.design?.geometry_preset === "rock") {
|
||||
const rockControl = buildRockBoundaryControl(section, rockBoundary);
|
||||
|
||||
@@ -15,12 +15,14 @@
|
||||
|
||||
import { createPanelResizer } from "@ui/ui_template_resizer";
|
||||
import { attachCollapsible } from "@ui/ui_template_collapsible";
|
||||
import { createWorkflowPanelHandle } from "@ui/ui_template_overlay";
|
||||
import type {
|
||||
CrossSection,
|
||||
EarthworkConversion,
|
||||
SectionDetailResponse,
|
||||
} from "./B06_wf3_ProfileCross_Api_Fetch";
|
||||
import type { RockBoundaryControl } from "./B06_wf3_ProfileCross_UI_Cross_Design";
|
||||
import type { CrossAreaKey } from "./B06_wf3_ProfileCross_UI_Cross_Areas";
|
||||
import {
|
||||
createCrossSectionCard,
|
||||
crossCardNaturalHeight,
|
||||
@@ -59,11 +61,11 @@ import {
|
||||
export type { CrossDesignChange, DesignChangeHandler, RockBoundaryControl };
|
||||
|
||||
/**
|
||||
* 헤더(캐럿 행)·요약줄·테두리가 먹는 세로 공간. 패널 높이에서 이만큼 빼야 그래프 몫이 된다.
|
||||
* (`.b06-section__panel-body`가 `overflow: hidden`이라 모자라면 아래가 잘린다.
|
||||
* 범례는 그래프 위에 겹치는 오버레이라 흐름 높이를 먹지 않는다.)
|
||||
* 상태 행·요약줄·하단 접기 손잡이·테두리가 먹는 세로 공간. 패널 높이에서 이만큼 빼야
|
||||
* 그래프 몫이 된다. (`.b06-section__panel-body`가 `overflow: hidden`이라 모자라면 아래가
|
||||
* 잘린다. 범례는 그래프 위에 겹치는 오버레이라 흐름 높이를 먹지 않는다.)
|
||||
*/
|
||||
const PANEL_CHROME_PX = 100;
|
||||
const PANEL_CHROME_PX = 116;
|
||||
/** 손대지 않았을 때의 패널 높이 — 이 값이 축소 비례의 기준(H₀)이 된다. */
|
||||
const BASE_PANEL_HEIGHT = LONG_HEIGHT + MASS_HAUL_HEIGHT + PANEL_CHROME_PX;
|
||||
const MIN_LONG_HEIGHT = 110;
|
||||
@@ -144,6 +146,8 @@ export function createSectionView(
|
||||
root.className = "b06-section";
|
||||
let currentDetail: SectionDetailResponse | null = null;
|
||||
let selectedStationId: string | null = null;
|
||||
/** 선택 카드에서 강조 중인 면적. 카드를 다시 그려도 그대로 살아나야 하므로 여기 둔다. */
|
||||
let activeAreaKey: CrossAreaKey | null = null;
|
||||
let currentExaggeration = 1;
|
||||
let currentCrossHalfWidth: number | undefined;
|
||||
let currentStationInterval: number | undefined;
|
||||
@@ -165,13 +169,13 @@ export function createSectionView(
|
||||
* 매 렌더마다 새로 만들면 리사이저 핸들과 접힘 상태가 초기화된다. */
|
||||
const panel = document.createElement("section");
|
||||
panel.className = "b06-section__panel ui-collapsible";
|
||||
// 최소 높이는 TS 상수 하나로만 정의한다(세션에 저장된 옛 높이가 더 작아도 여기서 걸린다).
|
||||
panel.style.minHeight = `${MIN_PANEL_HEIGHT}px`;
|
||||
// 제목 문구는 없앴다(2026-08-02 사용자 지시). 헤더 행은 접기/펼치기 손잡이 겸 상태 표시줄이며,
|
||||
// 캐럿은 공용 `ui-collapsible` 규칙이 그린다(방향만 B06 CSS에서 상하로 바꾼다).
|
||||
// 최소 높이는 TS 상수 하나로만 정의하되 **접힘 상태에는 걸리지 않아야** 한다 — 인라인
|
||||
// min-height를 두면 접어도 그 높이만큼 빈 배경이 남는다. CSS 변수로 넘겨 펼친 상태에만 건다.
|
||||
panel.style.setProperty("--b06-profile-min-height", `${MIN_PANEL_HEIGHT}px`);
|
||||
// 제목 문구는 없앴다(2026-08-02 사용자 지시). 헤더 행은 상태 문구만 담고, 접기/펼치기는
|
||||
// 패널 하단 가운데 손잡이가 맡는다.
|
||||
const panelHeader = document.createElement("header");
|
||||
panelHeader.className = "ui-collapsible__title";
|
||||
panelHeader.setAttribute("aria-label", L("B06_Profile_View_Longitudinal"));
|
||||
panelHeader.className = "b06-section__panel-status";
|
||||
const panelCount = document.createElement("span");
|
||||
panelHeader.append(panelCount);
|
||||
const chartWrap = document.createElement("div");
|
||||
@@ -179,14 +183,23 @@ export function createSectionView(
|
||||
const panelBody = document.createElement("div");
|
||||
panelBody.className = "b06-section__panel-body";
|
||||
panelBody.append(chartWrap);
|
||||
panel.append(panelHeader, panelBody);
|
||||
// 접기 손잡이는 패널 **하단 가운데**에 둔다(2026-08-02 사용자 지시). 모양은 좌측 사이드
|
||||
// 패널과 같은 삼각형 손잡이를 90° 돌린 것이며, 위로 접히는 패널이라 `collapseToward: "up"`이다.
|
||||
// `ui-collapsible__title` 클래스를 달아 공용 토글 동작을 그대로 쓰고(접혀도 이 손잡이만 남는다),
|
||||
// 그 클래스가 붙이는 캐럿은 B06 CSS에서 지운다(삼각형 아이콘과 겹치므로).
|
||||
const panelToggle = createWorkflowPanelHandle("bottom", "up");
|
||||
panelToggle.root.classList.add("ui-collapsible__title", "b06-section__panel-toggle");
|
||||
panel.append(panelHeader, panelBody, panelToggle.root);
|
||||
attachCollapsible(panel);
|
||||
// 접힘 상태는 세션에만 남긴다(리사이저와 같은 규칙).
|
||||
if (sessionStorage.getItem(PANEL_COLLAPSED_KEY) === "true") panel.classList.add("is-collapsed");
|
||||
// attachCollapsible보다 **뒤에** 붙여야 토글이 끝난 상태를 읽는다(헤더에 붙이면 토글 전 값이 저장된다).
|
||||
panelToggle.setOpen(!panel.classList.contains("is-collapsed"));
|
||||
// attachCollapsible보다 **뒤에** 붙여야 토글이 끝난 상태를 읽는다(먼저 붙이면 토글 전 값이 저장된다).
|
||||
panel.addEventListener("click", (event) => {
|
||||
if (!(event.target as HTMLElement).closest(".ui-collapsible__title")) return;
|
||||
sessionStorage.setItem(PANEL_COLLAPSED_KEY, String(panel.classList.contains("is-collapsed")));
|
||||
const collapsed = panel.classList.contains("is-collapsed");
|
||||
sessionStorage.setItem(PANEL_COLLAPSED_KEY, String(collapsed));
|
||||
panelToggle.setOpen(!collapsed);
|
||||
});
|
||||
|
||||
const panelResizer = createPanelResizer({
|
||||
@@ -264,10 +277,37 @@ export function createSectionView(
|
||||
const selectStation = (stationId: string, scroll: boolean): void => {
|
||||
const deselect = selectedStationId === stationId;
|
||||
selectedStationId = deselect ? null : stationId;
|
||||
// 측점이 바뀌면 면적 강조는 따라가지 않는다(다른 측점의 강조를 물려받으면 오해를 부른다).
|
||||
activeAreaKey = null;
|
||||
draw();
|
||||
if (scroll && !deselect) revealCard(stationId, "smooth");
|
||||
};
|
||||
|
||||
/**
|
||||
* 횡단 카드의 면적 값을 눌렀을 때. **선택되지 않은 카드에서 눌러도** 그 카드를 고르면서
|
||||
* 누른 면적까지 한 번에 강조한다(2026-08-02 사용자 지시).
|
||||
* 이미 선택된 카드라면 카드가 자기 DOM에서 강조를 켜고 끄므로 여기서는 상태만 받아 둔다
|
||||
* (다시 그릴 때 그대로 되살리기 위해).
|
||||
*/
|
||||
const selectArea = (stationId: string, key: CrossAreaKey | null): void => {
|
||||
if (selectedStationId === stationId) {
|
||||
activeAreaKey = key;
|
||||
return;
|
||||
}
|
||||
selectedStationId = stationId;
|
||||
activeAreaKey = key;
|
||||
draw();
|
||||
revealCard(stationId, "smooth");
|
||||
};
|
||||
|
||||
/** 카드 바깥(그리드 빈 자리)을 누르면 선택을 푼다(2026-08-02 사용자 지시). */
|
||||
const clearSelection = (): void => {
|
||||
if (!selectedStationId && !activeAreaKey) return;
|
||||
selectedStationId = null;
|
||||
activeAreaKey = null;
|
||||
draw();
|
||||
};
|
||||
|
||||
const buildCrossCard = (section: CrossSection, forcedHeightPx?: number): HTMLElement =>
|
||||
createCrossSectionCard(
|
||||
section,
|
||||
@@ -283,6 +323,8 @@ export function createSectionView(
|
||||
: undefined,
|
||||
onDesignChange,
|
||||
rockBoundary,
|
||||
section.station_id === selectedStationId ? activeAreaKey : null,
|
||||
selectArea,
|
||||
);
|
||||
|
||||
/** 범례 버튼 — 곡선 하나를 켜고 끈다. 축은 전체 곡선 기준이라 여기서 움직이지 않는다. */
|
||||
@@ -381,6 +423,10 @@ export function createSectionView(
|
||||
// 공간만 먹고, 카드마다 측점이 적혀 있어 무엇을 보는지는 이미 분명하다.
|
||||
const grid = document.createElement("div");
|
||||
grid.className = "b06-section__grid";
|
||||
// 카드 사이 빈 자리를 누르면 선택 해제 — 카드 안 클릭은 카드가 먼저 처리한다.
|
||||
grid.addEventListener("click", (event) => {
|
||||
if (!(event.target as HTMLElement).closest(".b06-cross-card")) clearSelection();
|
||||
});
|
||||
const columnCount = Math.max(
|
||||
1,
|
||||
Math.floor((renderWidth + CROSS_GRID_GAP) / (CROSS_GRID_MIN_WIDTH + CROSS_GRID_GAP)),
|
||||
|
||||
@@ -43,7 +43,13 @@
|
||||
height: var(--b06-profile-height, auto);
|
||||
}
|
||||
|
||||
/* 접었을 때는 저장된 높이를 무시하고 제목 행만 남긴다(클래스 2개라 위 규칙을 이긴다). */
|
||||
/* 최소 높이는 **펼친 상태에만** 건다. 접힘에도 걸리면(인라인 min-height가 그랬다) 접어도
|
||||
그 높이만큼 빈 배경이 남는다. 값은 JS가 `--b06-profile-min-height`로 실어 준다. */
|
||||
.b06-section__panel:not(.is-collapsed) {
|
||||
min-height: var(--b06-profile-min-height, 360px);
|
||||
}
|
||||
|
||||
/* 접었을 때는 저장된 높이를 무시하고 손잡이 행만 남긴다(클래스 2개라 위 규칙을 이긴다). */
|
||||
.b06-section__panel.is-collapsed {
|
||||
height: auto;
|
||||
}
|
||||
@@ -54,7 +60,8 @@
|
||||
잘려 나가 잡히지 않는다 — 안쪽에 붙여 전체가 클릭 영역으로 남게 한다. */
|
||||
.b06-section__panel > .ui-resizer--vertical {
|
||||
top: auto;
|
||||
bottom: 0;
|
||||
/* 하단 가운데 접기 손잡이(14px) 위에 얹는다 — 겹치면 어느 쪽이 잡히는지 헷갈린다. */
|
||||
bottom: 14px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
@@ -83,21 +90,31 @@
|
||||
font-size: var(--text-caption);
|
||||
}
|
||||
|
||||
/* 제목 문구 없이 접기/펼치기 손잡이만 남은 행 — 캐럿을 가운데 두고 높이를 낮춘다. */
|
||||
/* 상태 문구만 담는 행 — 비어 있을 때가 많아 높이를 최소로 둔다. */
|
||||
.b06-section__panel > header {
|
||||
justify-content: center;
|
||||
padding-block: 2px;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
/* 좌측 사이드 패널과 같은 캐럿을 쓰되 **방향만 상하로** 바꾼다(이 패널은 위로 접힌다).
|
||||
펼침 = 위로 접는다(▴) / 접힘 = 아래로 편다(▾). */
|
||||
.b06-section__panel > .ui-collapsible__title::after {
|
||||
content: "▴";
|
||||
/* 접기 손잡이 — 패널 하단 가운데. 모양은 좌측 사이드 패널과 같은 삼각형 손잡이를 90° 돌린
|
||||
것이라 공용 컴포넌트를 그대로 쓰고, 위치만 여기서 흐름 배치로 되돌린다(원본은 absolute).
|
||||
`ui-collapsible__title`이 붙여 주는 캐럿은 삼각형 아이콘과 겹치므로 지운다. */
|
||||
.b06-section__panel-toggle {
|
||||
position: relative;
|
||||
inset: auto;
|
||||
z-index: 3;
|
||||
/* `ui-collapsible__title`이 flex + space-between을 걸어 두므로 아이콘을 가운데로 되돌린다. */
|
||||
justify-content: center;
|
||||
align-self: center;
|
||||
width: var(--spacing-64);
|
||||
height: 14px;
|
||||
margin-top: auto;
|
||||
border-radius: var(--radius-buttons) var(--radius-buttons) 0 0;
|
||||
}
|
||||
|
||||
.b06-section__panel.is-collapsed > .ui-collapsible__title::after {
|
||||
content: "▾";
|
||||
.b06-section__panel-toggle::after {
|
||||
content: none;
|
||||
}
|
||||
|
||||
/* 종단면도와 유토곡선이 이 컨테이너 하나를 공유한다 — 가로 스크롤을 함께 써야
|
||||
@@ -530,7 +547,7 @@
|
||||
.b06-cross-card__areas th,
|
||||
.b06-cross-card__areas td {
|
||||
padding: 0 5px;
|
||||
text-align: right;
|
||||
text-align: center;
|
||||
font-weight: var(--font-weight-regular);
|
||||
}
|
||||
|
||||
|
||||
@@ -32,8 +32,12 @@ export function createWorkflowPanelHandle(
|
||||
* 오른쪽에 도킹해 오른쪽으로 접히는 패널(B05 배수유역도)은 `"right"`를 넘겨야
|
||||
* 화살표가 뒤집히지 않는다 — 도킹 방향이 반대인데 같은 회전을 쓰면 열기/닫기
|
||||
* 표시가 정확히 거꾸로 보인다.
|
||||
*
|
||||
* `placement: "bottom"`은 위아래로 접히는 패널이다. 기본은 아래로 접히는 하단 도킹
|
||||
* 패널(`"down"`)이고, **위로 접히는 상단 도킹 패널**(B06 종단·유토곡선 패널)은 `"up"`을
|
||||
* 넘겨야 한다. 좌우와 같은 이유로, 도킹 방향이 반대인데 같은 회전을 쓰면 표시가 거꾸로 된다.
|
||||
*/
|
||||
collapseToward: "left" | "right" = "left",
|
||||
collapseToward: "left" | "right" | "up" | "down" = "left",
|
||||
): WorkflowPanelHandle {
|
||||
const root = document.createElement("button");
|
||||
root.type = "button";
|
||||
@@ -44,9 +48,10 @@ export function createWorkflowPanelHandle(
|
||||
|
||||
function setOpen(isOpen: boolean): void {
|
||||
// 삼각형은 회전 0°에서 오른쪽(▶)을 가리킨다. 펼친 상태면 접히는 쪽을,
|
||||
// 접힌 상태면 펼쳐질 쪽을 가리키도록 180°를 붙였다 뗀다.
|
||||
// 접힌 상태면 펼쳐질 쪽을 가리키도록 90°/180°를 붙였다 뗀다.
|
||||
const pointsRight = isOpen === (collapseToward === "right");
|
||||
const rotation = placement === "side" ? (pointsRight ? 0 : 180) : isOpen ? 90 : -90;
|
||||
const pointsUp = isOpen === (collapseToward === "up");
|
||||
const rotation = placement === "side" ? (pointsRight ? 0 : 180) : pointsUp ? -90 : 90;
|
||||
icon.style.transform = `rotate(${rotation}deg)`;
|
||||
root.title = t(isOpen ? "Workflow_Overlay_Collapse" : "Workflow_Overlay_Expand");
|
||||
root.setAttribute("aria-label", root.title);
|
||||
|
||||
Reference in New Issue
Block a user