feat(b06): 횡단 카드 — 단추 줄 빈자리 누름 = 카드 고름 · 중심고 = Y축 라벨 둥근 상자(눈금과 같은 글자 · 소수 첫째 자리 · 축선 앞에서 멈춤 · 카드 · 상세 모달) · 아래 띠 중심고 글 뺌 · 암 경계 왼쪽 · 카드 = 내용 높이(아래 여유 없앰 · 빈 띠 높이 그대로) (318-1~3 · 요구 215 · 216)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C6BNBFXFndbqYxu6sZuknK
This commit is contained in:
2026-10-13 21:24:30 +09:00
co-authored by Claude Opus 5.5
parent 6e981b1c90
commit 6329ea2a3a
6 changed files with 76 additions and 21 deletions
+45 -3
View File
@@ -26,6 +26,8 @@ export interface CrossAxesOptions {
/** 표고 과장 기준점과 배율 — 화면 표고 → 실제 표고 환산에 쓴다. */
elevationMid: number;
exaggeration: number;
/** 중심고(실제 표고) — Y축 라벨 자리에 둥근 상자로(318-2 · 소수 첫째 자리). 없으면 안 그림. */
centerZ?: number | null;
}
/** X 눈금 라벨("-12" 등)이 겹치지 않는 최소 간격(px). */
@@ -36,6 +38,11 @@ const X_TARGET_TICKS = 7;
/** 표고(Y) 눈금 최소 간격(m) — 1m 아래 단위는 표기하지 않는다. */
const MIN_ELEVATION_STEP_M = 1;
const Y_TARGET_TICKS = 10;
/** Y 눈금 글 오른끝 ↔ Y 축선(px). */
const Y_LABEL_GAP_PX = 7;
/** 중심고 상자 — 글자 좌우 여백 · 높이(px). 글자 크기는 눈금과 같음(10px). */
const CENTER_PAD_X = 3;
const CENTER_BOX_H = 14;
/**
* 축 레이어를 그리는 함수를 만든다. 줌·팬이 바뀔 때마다 그 상태로 다시 부르면 된다.
@@ -108,9 +115,16 @@ export function createCrossAxes(
MIN_ELEVATION_STEP_M,
);
const yDecimals = Math.max(0, Math.ceil(-Math.log10(yStep) - 1e-9));
const screenYOf = (raw: number) =>
ty + scale * y(options.elevationMid + (raw - options.elevationMid) * options.exaggeration);
// 중심고 상자 자리 — 보이는 범위 안일 때만 · 그 높이 눈금 글자는 상자에 양보
const centerZ = options.centerZ;
const centerY =
typeof centerZ === "number" && centerZ >= rawBottom && centerZ <= rawTop
? screenYOf(centerZ)
: null;
for (let raw = Math.ceil(rawBottom / yStep) * yStep; raw <= rawTop + 1e-9; raw += yStep) {
const display = options.elevationMid + (raw - options.elevationMid) * options.exaggeration;
const screenY = ty + scale * y(display);
const screenY = screenYOf(raw);
layer.append(
svgElement("line", {
x1: plotLeft,
@@ -119,13 +133,41 @@ export function createCrossAxes(
y2: screenY,
class: "b06-chart__grid",
}),
);
if (centerY !== null && Math.abs(screenY - centerY) < CENTER_BOX_H) continue;
layer.append(
svgText(raw.toFixed(yDecimals), {
x: plotLeft - 7,
x: plotLeft - Y_LABEL_GAP_PX,
y: screenY + 3,
"text-anchor": "end",
class: "b06-chart__tick",
}),
);
}
if (centerY !== null) appendCenterTag(layer, (centerZ as number).toFixed(1), plotLeft, centerY);
};
}
/** 중심고 둥근 상자 — 글 오른끝 = 다른 눈금 자리 · 상자는 Y 축선(`axisX`) 4px 앞에서 멈춤 ·
* 글 폭은 잴 수 있으면 재고(붙은 뒤) 아니면 어림. */
function appendCenterTag(layer: SVGGElement, text: string, axisX: number, centerY: number): void {
const textRight = axisX - Y_LABEL_GAP_PX;
const box = svgElement("rect", {
y: centerY - CENTER_BOX_H / 2,
height: CENTER_BOX_H,
rx: 3,
ry: 3,
class: "b06-chart__center-box",
});
const label = svgText(text, {
x: textRight,
y: centerY + 3.5,
"text-anchor": "end",
class: "b06-chart__tick b06-chart__center-text",
});
layer.append(box, label);
const measured = (label as SVGTextElement).getComputedTextLength?.() ?? 0;
const width = (measured > 0 ? measured : text.length * 5.8) + CENTER_PAD_X * 2;
box.setAttribute("x", String(textRight + CENTER_PAD_X - width));
box.setAttribute("width", String(width));
}
@@ -172,11 +172,9 @@ export function appendCardFooter(
rockBoundary?: RockBoundaryControl,
cutSlope?: CutSlopeControl,
): void {
// 중심고는 그래프 Y축 라벨 상자로 옮김(318-2 · `_Cross_Axes`) — 하단 띠는 암 제어만 · 비어도 띠 높이 그대로
const footer = document.createElement("footer");
const center = document.createElement("span");
center.textContent = `${L("B06_Profile_View_CenterElevation")} ${section.center_z?.toFixed(2) ?? "-"}m`;
footer.append(center);
// 암 경계선 제어는 하단 정보 행 가운데(2026-08-02) — 암 지반만.
// 암 경계선 제어는 하단 정보 행 왼쪽(318-2) — 암 지반만.
if (rockBoundary && section.design?.geometry_preset === "rock") {
const rockControl = buildRockBoundaryControl(section, rockBoundary);
rockControl.classList.add("b06-cross-card__rockb");
+6 -2
View File
@@ -346,8 +346,12 @@ export function buildDesignControls(
};
const bar = document.createElement("div");
bar.className = "b06-design";
// 컨트롤 상호작용이 카드 선택 클릭으로 전파되지 않게 한다.
bar.addEventListener("click", (event) => event.stopPropagation());
// 조작(단추 · 입력 · ⋯ 펼침)만 카드 선택 클릭으로 안 넘김 — 빈자리 누름은 카드를 고름(318-1).
bar.addEventListener("click", (event) => {
if ((event.target as Element).closest("button, input, select, label, details")) {
event.stopPropagation();
}
});
// 경사 방향 선택이 필요한 경우 = 양절(both_cut)·양성(both_fill). 편절은 절토측 자동.
// 양성은 측구가 없어도 노면 기울기 방향(생략된 측구 방향)을 여기서 지정한다(N-2-3).
+1
View File
@@ -387,6 +387,7 @@ export function createCrossSectionCard(
displayMax,
elevationMid,
exaggeration,
centerZ: section.center_z ?? null,
});
renderAxes(cardZoomStates.get(section.station_id) ?? IDENTITY_VIEW);
@@ -599,6 +599,7 @@ export function createSectionView(
displayMax: plot.displayMax,
elevationMid: plot.elevationMid,
exaggeration: Math.max(currentExaggeration, 0.1),
centerZ: section.center_z ?? null,
};
// 칸 맞춤이 넓힌 가로 범위를 처음 배율로 되좁힘 = 카드 반폭(266-1)
const homeScale = base && fitted ? base.pixelsPerMeter / fitted.pixelsPerMeter : 1;
+21 -12
View File
@@ -121,6 +121,8 @@
display: grid;
grid-template-columns: repeat(auto-fill, minmax(min(480px, 100%), 1fr));
gap: var(--spacing-16);
/* 카드 = 내용 높이만큼 — 같은 줄 큰 카드에 맞춰 늘어나 아래 띠 밑이 비던 것(318-3 · 띠 높이 그대로) */
align-items: start;
}
/* 알림 줄(138-3) — 카드 칸 맨 위 고정 · 알림 없어도 줄 유지 · 오른쪽 끝 열 수 칸.
@@ -344,6 +346,11 @@ html.is-bm-route .b06-section__notice-bar {
border-top: 1px solid var(--color-border);
}
/* 빈 띠(토사 측점 · 중심고는 Y축으로 옮김 318-2)도 글 한 줄 높이 그대로 */
.b06-cross-card > footer:empty::before {
content: "\200b";
}
.b06-section__empty {
display: flex;
min-height: 180px;
@@ -388,6 +395,18 @@ html.is-bm-route .b06-section__notice-bar {
font-size: 10px;
}
/* 중심고 Y축 라벨(318-2) — 눈금과 같은 10px · 둥근 상자 · 테마 강조색 · 축선 앞에서 멈춤 */
.b06-chart__center-box {
fill: color-mix(in srgb, var(--color-accent) 14%, var(--color-surface-raised));
stroke: var(--color-accent);
stroke-width: 1;
}
.b06-chart__center-text {
fill: var(--color-accent);
font-weight: var(--font-weight-medium);
}
/* 구조물(비정규) 측점 이름 — 종단면도 측점선 상단 표기. */
.b06-chart__structure-label {
font-size: 9px;
@@ -607,24 +626,14 @@ html.is-bm-route .b06-section__notice-bar {
font-family: var(--font-mono);
}
/* 암 경계선 제어(E-7) — 하단 정보 행 **가운데 맞춤**(2026-08-06 사용자 지시).
방위각 삭제로 space-between 3분할 배치가 깨져 좌측에 붙었다 — 절대 배치로
행 중앙에 고정한다(중심고·반폭 버튼과 겹치지 않는 폭). */
.b06-cross-card > footer {
position: relative;
}
/* 암 경계선 제어(E-7) — 하단 정보 행 **왼쪽 맞춤**(318-2 · 중심고는 그래프 Y축 라벨 상자로 옮김). */
.b06-cross-card__rockb {
position: absolute;
left: 50%;
transform: translateX(-50%);
flex: 0 0 auto;
border-radius: var(--radius-inputs);
padding: 1px 4px;
}
/* 측점별 암 절토 경사(2026-09-07) — 암 경계선이 행 가운데를 절대 배치로 쓰므로
이 칸은 행의 **오른쪽 끝**에 둔다. 중심고(왼쪽)와 겹치지 않는다. */
/* 측점별 암 절토 경사(2026-09-07) — 행의 **오른쪽 끝**(암 경계 = 왼쪽). */
.b06-cross-card__cutslope {
margin-left: auto;
flex: 0 0 auto;