fix(479 B06 횡단 정보 칸): 처음 빈 칸 · 암 정보는 암 점선 고를 때만 — 안 고름 암 갈래 · 그림 없는 카드 암 칸 · 죽은 buildRockBoundaryControl 걷음 · 시험 370-3 · 318 · 137 고쳐 씀

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-19 22:03:40 +09:00
co-authored by Claude Opus 5.5
parent afe6e4a799
commit 37e3ee6126
10 changed files with 54 additions and 75 deletions
@@ -7,13 +7,8 @@
* ========================================================================== */
import type { CrossSection } from "./B06_Section_Api_Fetch";
import type { RockBoundaryControl } from "./B06_Section_UI_Cross_Design";
import { buildCutSlopeControl, type CutSlopeControl } from "./B06_Section_UI_Cross_CutSlope";
import {
buildDesignControls,
buildRockBoundaryControl,
sectionModeLabel,
} from "./B06_Section_UI_Cross_Design";
import { buildDesignControls, sectionModeLabel } from "./B06_Section_UI_Cross_Design";
import { type FillSlopeLength, fillSlopeLengths } from "./logic/B06_Section_UI_Cross_Fit";
import {
type DesignChangeHandler,
@@ -159,27 +154,20 @@ export function appendCardHeader(
card.append(header);
}
/** 하단 정보행 — 왼쪽 = 선 공용 칸(370-3 · 안 고르면 암 경계선) · 오른쪽 = (암 지반일 때) 암 절토 경사. */
/** 하단 정보행 — 왼쪽 = 선 공용 칸(370-3 · 안 고르면 빈 칸 · 479) · 오른쪽 = (암 지반일 때) 암 절토 경사. */
export function appendCardFooter(
card: HTMLElement,
section: CrossSection,
rockBoundary?: RockBoundaryControl,
cutSlope?: CutSlopeControl,
/** 설계선 고름 공용 칸(370-3 · `_Cross_Line_Cell`) — 있으면 왼쪽 칸(안 고르면 암 · 고르면 그 선). */
/** 설계선 고름 공용 칸(370-3 · `_Cross_Line_Cell`) — 있으면 왼쪽 칸(안 고르면 빈 칸 · 고르면 그 선). */
lineCell?: HTMLElement,
): void {
// 중심고는 그래프 Y축 라벨 상자로 옮김(318-2 · `_Cross_Axes`) — 하단 띠는 암 제어만 · 비어도 띠 높이 그대로
// 중심고는 그래프 Y축 라벨 상자로 옮김(318-2 · `_Cross_Axes`) — 비어도 띠 높이 그대로
const footer = document.createElement("footer");
if (lineCell) {
lineCell.classList.add("b06-cross-card__rockb");
footer.append(lineCell);
}
// 암 경계선 제어는 하단 정보 행 왼쪽(318-2) — 암 지반만 · 설계선 고름이 없는 카드.
else if (rockBoundary && section.design?.geometry_preset === "rock") {
const rockControl = buildRockBoundaryControl(section, rockBoundary);
rockControl.classList.add("b06-cross-card__rockb");
footer.append(rockControl);
}
// 암 절토 경사(각도)는 암 측점에만 — 토사 측점은 암반이 없어 쓰이지 않는다
// (2026-09-07 사용자 확정). 전체를 바꾸는 자리는 좌측 [표준 횡단면 설정]이다.
if (cutSlope && section.design?.geometry_preset === "rock") {
+2 -13
View File
@@ -28,7 +28,7 @@ export {
} from "./B06_Section_UI_Cross_Design_Surface";
export { appendCrossDesignOverlay } from "./B06_Section_UI_Cross_Design_Line";
import { openCrossDetail, refreshCrossDetail } from "./B06_Section_UI_Cross_Detail";
import { buildLineCell, rockCellState, type RockCellSpec } from "./B06_Section_UI_Cross_Line_Cell";
import type { RockCellSpec } from "./B06_Section_UI_Cross_Line_Cell";
/** 넘침 정리 한 장 몫 — 쓰기·읽기·쓰기 세 토막으로 갈라 두어 프레임 단위로 묶는다. */
interface ReflowCard {
@@ -207,18 +207,7 @@ function toggle(
return { wrap, button };
}
/** 암 경계선 상/하/리셋 컨트롤 — 아래 띠 공용 칸(370-3 · `_Cross_Line_Cell`)의 「안 고름」 꼴. 설계선 고름이
* 없는 카드(설계 없음)에서 띠 왼쪽 칸으로 씀. */
export function buildRockBoundaryControl(
section: CrossSection,
control: RockBoundaryControl,
): HTMLElement {
const cell = buildLineCell();
cell.show(rockCellState(rockCellSpec(section, control)));
return cell.root;
}
/** 암 경계선 칸 제원 — 공용 칸(안 고름 · 암 고름)이 같이 씀. */
/** 암 경계선 칸 제원 — 공용 칸 암 고름(479 — 안 고르면 빈 칸). */
export function rockCellSpec(section: CrossSection, control: RockBoundaryControl): RockCellSpec {
return {
legend: L("B06_Design_RockBoundary_Legend"),
+28 -29
View File
@@ -5,7 +5,7 @@
* 그려진 보라 선(`b06-chart__design-cross`)을 화면 좌표에서 m 로 되돌려 같은 방향 토막을
* 직선 조각 하나로 합치고, 벽 · 본체 폴리곤에 가린 부분을 뺀 **보이는 조각**마다 투명 판정
* 선을 구조물 판정면 아래에 깐다. 고르면 그 조각을 강조하고 아래 띠 공용 칸(370-3 · `_Cross_Line_Cell`)에
* 제목 · 축에 맞는 버튼 · 치수 한 줄을 적는다(안 고르면 암 경계선 칸 · 떠 있는 선 조정창 · 우측 위 라벨 걷음).
* 제목 · 축에 맞는 버튼 · 치수 한 줄을 적는다(안 고르면 빈 칸 · 479 · 떠 있는 선 조정창 · 우측 위 라벨 걷음).
* 구역 판정(`crossZoneAt` · 도로 · 절토 · 성토)은 설계선 쉬프트(139-6)도 쓴다.
* 고름 상태는 페이지에 한 칸(측점 + 조각 번호) — 카드를 다시 그려도 되살아난다.
* 고른 조각 쪽 사면을 노견째 ◀ ↺ ▶ · 방향키 · 0.1 m 로 옮김(139-6 쉬프트 · 151-3 ·
@@ -444,7 +444,7 @@ export function attachDesignLinePick(input: {
toes?: ReturnType<typeof toePoints>;
/** 있으면 쪽마다 노견 끝 = 원지반(절·성토 없음 · 379-1) — 손잡이 값 앞 알림. */
bare?: ReturnType<typeof edgeOnGround>;
/** 있으면 암 경계선 고름(키 `rock` · ▲▼↺ · 세로 끌기) · 안 고른 때 공용 칸 = 암(370-3). */
/** 있으면 암 경계선 고름(키 `rock` · ▲▼↺ · 세로 끌기 · 370-3) · 안 고르면 빈 칸(479). */
rock?: RockCellSpec;
}): DesignLinePick {
const { layer, design, x, toDisplayY } = input;
@@ -627,33 +627,32 @@ export function attachDesignLinePick(input: {
const toe = toeSide ? input.toes?.[toeSide] : null;
const dims = piece ? pieceLabel(design, piece).replace("\n", " · ") : "";
const bare = toeSide && input.bare?.[toeSide] ? "절·성토 없음 · " : ""; // 379-1
// 아래 띠 공용 칸(370-3) — 안 고르면 암(암 지반만) · 고르면 그 선 제목 · 버튼 · 값 한 줄
const state: LineCellState | null =
onRock || (!chosen && rock)
? rockCellState(rock as RockCellSpec)
: onCenter
? { title: "중심점", value: CENTER_HINT, buttons: {} }
: toe
? {
title: `사면 이동 ${sideName}`,
value: `${bare}노선에서 ${Math.abs(toe.offset_m).toFixed(2)}m${moved}`,
buttons: shift ? sideways(true) : {},
}
: !piece
? null
: axis === "y"
? { title: "노면 높이", value: `${dims} · ▲▼ = 종단 계획고`, buttons: {} }
: axis === "depth" && side
? {
title: `소단 높이 ${sideName}`,
value: `도로 아래 ${bermDepthOf(design, side).toFixed(1)}m · ${dims}`,
buttons: berm ? lift : {},
}
: {
title: `사면 이동 ${sideName}`,
value: dims + moved,
buttons: shift ? sideways(true) : {},
};
// 아래 띠 공용 칸(370-3) — 안 고르면 빈 칸(479) · 고르면 그 선 제목 · 버튼 · 값 한 줄
const state: LineCellState | null = onRock
? rockCellState(rock as RockCellSpec)
: onCenter
? { title: "중심점", value: CENTER_HINT, buttons: {} }
: toe
? {
title: `사면 이동 ${sideName}`,
value: `${bare}노선에서 ${Math.abs(toe.offset_m).toFixed(2)}m${moved}`,
buttons: shift ? sideways(true) : {},
}
: !piece
? null
: axis === "y"
? { title: "노면 높이", value: `${dims} · ▲▼ = 종단 계획고`, buttons: {} }
: axis === "depth" && side
? {
title: `소단 높이 ${sideName}`,
value: `도로 아래 ${bermDepthOf(design, side).toFixed(1)}m · ${dims}`,
buttons: berm ? lift : {},
}
: {
title: `사면 이동 ${sideName}`,
value: dims + moved,
buttons: shift ? sideways(true) : {},
};
cell.show(state);
const open =
onCenter ||
@@ -2,7 +2,7 @@
* B06_Section_UI_Cross_Line_Cell.ts
* 횡단 카드 아래 띠 왼쪽 **공용 칸**(370-3) — 고른 선의 제목 · 축에 맞는 버튼 · 값 한 줄.
*
* · 안 고르면 암 경계선 ▲▼↺ · 값(암 지반만 · 토사는 빈 칸 · 띠 높이 그대로)
* · 안 고르면 빈 칸(479 · 띠 높이 그대로)
* · 고르면 그 선(노면 · 사면 · 소단 · 중심점 · 교차점 · 암) — `_Cross_Design_Pick` 이 상태를 넣음
* · 버튼 다섯 칸(◀ ▲ ↺ ▼ ▶)은 한 벌 · 상태마다 보임 · 툴팁 · 동작만 바꿈 · 방향키는 보이는 슬롯만 찾음
* · ▲▼ 길게 누름 = 0.5 초 뒤 0.1 초마다 반복(암 · B05 종단 선 제어 꼴)
+1 -1
View File
@@ -680,6 +680,6 @@ export function createCrossSectionCard(
card.append(chartWrap);
}
appendCardFooter(card, section, rockBoundary, cutSlope, linePick?.cell); // 왼쪽 = 선 공용 칸(370-3)
appendCardFooter(card, section, cutSlope, linePick?.cell); // 왼쪽 = 선 공용 칸(370-3)
return card;
}
+3 -4
View File
@@ -400,7 +400,7 @@ const out = {};
out.draftProfile = { samples: profile.samples, id: profile.id };
}
// ── 2. 아래 띠(318-2 · 318-3) — 중심고 글 없음 · 암 경계 맨 왼쪽 · 토사도 띠 남음 ─────
// ── 2. 아래 띠(318-2 · 318-3) — 중심고 글 없음 · 선 칸 없으면 암 절토만(479) · 토사도 띠 남음 ─
{
const control = (cls) => () => {
const node = new FakeNode("div");
@@ -410,7 +410,6 @@ const out = {};
const load = makeLoader([B06("B06_Section_UI_Cross_Card_Chrome")], {
"./B06_Section_UI_Cross_CutSlope": { buildCutSlopeControl: control("cutslope") },
"./B06_Section_UI_Cross_Design": {
buildRockBoundaryControl: control("rockb"),
buildDesignControls: () => ({ bar: new FakeNode("div"), groundSegment: new FakeNode("div") }),
sectionModeLabel: () => "",
},
@@ -424,7 +423,7 @@ const out = {};
const { appendCardFooter } = load(B06("B06_Section_UI_Cross_Card_Chrome"));
const footerOf = (preset) => {
const card = new FakeNode("article");
appendCardFooter(card, { center_z: 876.57, design: { geometry_preset: preset } }, {}, {});
appendCardFooter(card, { center_z: 876.57, design: { geometry_preset: preset } }, {});
return card.querySelector("footer");
};
const rock = footerOf("rock");
@@ -440,7 +439,7 @@ const out = {};
const cell = new FakeNode("div");
cell.className = "linecell";
const cellCard = new FakeNode("article");
appendCardFooter(cellCard, { design: { geometry_preset: "rock" } }, {}, {}, cell);
appendCardFooter(cellCard, { design: { geometry_preset: "rock" } }, {}, cell);
out.footer.cell_classes = cellCard.querySelector("footer").children.map((kid) => kid.className);
}
+2 -2
View File
@@ -226,7 +226,7 @@ function card(rockOn = true) {
const out = {};
// ① 안 고름 — 공용 칸 = 암 ▲▼↺ · 값 · 방향키는 안 먹음(종단 계획고로 그대로)
// ① 안 고름 — 공용 칸 = 빈 칸(479) · 숨은 단추 눌러도 암 안 움직임 · 방향키는 종단 계획고로 그대로
{
const c = card();
c.handle.setActive(null);
@@ -283,7 +283,7 @@ const out = {};
sent: c.offsets.slice(before),
clickSwallowed: (winListeners.click ?? []).length === 1,
};
// 풀면 다시 암 칸 · 방향키 안 먹음
// 풀면 빈 칸(479) · 방향키 안 먹음
c.handle.setActive(null);
out.released = { ...c.state(), upPassed: !press("ArrowUp") };
// 사면을 고르면 그 선 칸(제목 · ◀↺▶ · 값 한 줄)
+3 -1
View File
@@ -102,7 +102,9 @@ def test_카드_배선_하나만() -> None:
assert "setLineActive(pickedLineAt(section.station_id));" in view # 다시 그려도 되살림
# 치수 · 이동 = 아래 띠 왼쪽 공용 칸(370-3 · 옛 [+ − ⤢] 왼쪽 치수 · 떠 있는 이동 패널 걷음)
assert "linePick.label" not in view and "linePick.panel" not in view
assert "appendCardFooter(card, section, rockBoundary, cutSlope, linePick?.cell);" in view
assert (
"appendCardFooter(card, section, cutSlope, linePick?.cell);" in view
) # 479 — 암 칸 인자 걷음
css = _src("B06_Section/B06_Section_UI_Style_Cross_Areas.css")
assert ".b06-chart__design-hit.is-active {" in css
+2 -2
View File
@@ -118,8 +118,8 @@ def test_footer_without_center_text(result: dict) -> None:
footer = result["footer"]
assert "중심고" not in footer["rock_text"]
assert "876" not in footer["rock_text"]
assert footer["rock_first"].split()[-1] == "b06-cross-card__rockb" # 암 경계 = 왼쪽 첫 칸
assert footer["rock_classes"][-1].split()[-1] == "b06-cross-card__cutslope"
# 479 — 선 공용 칸이 없는 암 카드 = 암 절토 칸만(암 경계 칸 따로 안 만듦)
assert footer["rock_classes"] == ["cutslope b06-cross-card__cutslope"]
assert footer["soil_exists"] and footer["soil_children"] == 0 # 토사도 띠는 남음(빈 띠)
# 370-3 — 선 공용 칸 = 왼쪽 첫 칸(암 꼴 클래스) · 암 절토 경사 = 오른쪽 그대로
assert footer["cell_classes"] == [
+8 -6
View File
@@ -1,5 +1,7 @@
"""PLAN 370-3 — B06 암 경계선 고름 · 방향키 · 세로 끌기 · 아래 띠 공용 칸(요구 240-22).
479(요구 280-1) — 안 고르면 공용 칸 = 빈 칸 · 암 정보는 암 점선을 고를 때만.
짝: `B06_Section/B06_Section_UI_Cross_Design_Pick.ts`(rock 축 · 판정 선) ·
`B06_Section/B06_Section_UI_Cross_Line_Cell.ts`(공용 칸) · `B06_Section_UI_Cross_Center.ts`(`bindAxisDrag`).
실제 TS 를 `helper_370_3_rock_cell.cjs` 가 Node 로 돌린다.
@@ -31,12 +33,11 @@ def out() -> dict:
return json.loads(done.stdout)
def test_안_고르면_공용_칸은_암_버튼과_값(out: dict) -> None:
def test_안_고르면_공용_칸은_빈_칸(out: dict) -> None:
idle = out["idle"]
assert idle["title"] == "암 경계선" and idle["value"] == "-0.5m"
assert idle["slots"] == ["up", "reset", "down"]
assert idle["empty"] is True and idle["slots"] == [] # 479 — 암 지반도 빈 칸
assert idle["upPassed"] is True # 안 고르면 ▲▼ = 종단 계획고 그대로
assert idle["offsets"] == [0.1, -0.1] and idle["resets"] == 1
assert idle["offsets"] == [] and idle["resets"] == 0 # 숨은 단추 눌러도 암 안 움직임
def test_암_경계선_판정_선(out: dict) -> None:
@@ -67,8 +68,8 @@ def test_세로_끌기는_놓을_때_한_번(out: dict) -> None:
assert out["idleDrag"]["stopped"] is False # 안 고르면 끌기 아님
def test_풀면_암_칸_사면_고르면_그_선_칸(out: dict) -> None:
assert out["released"]["title"] == "암 경계선" and out["released"]["upPassed"]
def test_풀면_빈_칸_사면_고르면_그_선_칸(out: dict) -> None:
assert out["released"]["empty"] is True and out["released"]["upPassed"]
slope = out["slope"]
assert slope["title"] == "사면 이동 (좌)" and slope["slots"] == ["left", "reset", "right"]
assert slope["value"].startswith("길이 ") and "\n" not in slope["value"]
@@ -87,6 +88,7 @@ def test_배선_떠_있는_선_조정창_걷음() -> None:
assert "rockBoundary && rockCellSpec(section, rockBoundary)" in view
chrome = (B06 / "B06_Section_UI_Cross_Card_Chrome.ts").read_text(encoding="utf-8")
assert "if (lineCell) {" in chrome
assert "buildRockBoundaryControl" not in chrome # 479 — 그림 없는 암 카드도 빈 칸
detail = (B06 / "B06_Section_UI_Cross_Detail_Pick.ts").read_text(encoding="utf-8")
assert ".b06-chart__design-hit:not(.b06-chart__rock-hit)" in detail # 상세 표 강조에 안 섞임
css = (B06 / "B06_Section_UI_Style_Cross.css").read_text(encoding="utf-8")