fix(B05): 종단 기준 절·성토 불균형 표시 삭제 + 유토곡선 깜빡임 제거 + 쉬프트 안내

사용자 지적 3건.

1. 상단 표시줄의 「절·성토 불균형」은 **종단 기준** 값이었음 — 계획선과 지반선 사이 세로
   면적(㎡)의 비라 실제 토량이 아니고 판단 근거가 안 됨. 삭제함. 의미 있는 균형은 횡단
   단면적을 쌓아 부피(㎥)로 내는 하단 유토곡선 요약이고 그쪽은 그대로 둠.

2. 계획고를 연속으로 누르면 유토곡선이 매번 사라졌다 다시 서서 깜빡였음. 재계산 대기
   중에 곡선을 **지우지 않고 직전 것을 그대로 두며**, 요약줄 끝에 「다시 계산 중…」
   칩만 붙임. 재계산이 브라우저 안에서 수십 ms에 끝나므로 화면이 비는 구간이 없어짐.
   아직 한 번도 못 그렸을 때만 예전처럼 문구로 자리를 채움.

3. [쉬프트]에서 아무것도 안 골라지던 것은 **직선화된 구간이 없어서**였음(초기 계획선은
   마디마다 기울기가 달라 직선 구간이 0개). 안내를 갈라 적음 —
   「직선화된 구간이 없습니다 — [직선화]로 먼저 만드세요」 / 「…고르세요 (여러 개 가능)
   — 끝내려면 [쉬프트]를 다시 누르세요」. 직선화 안내에도 취소 방법을 덧붙임.

전체 테스트 183건 통과, `tsc --noEmit` 통과.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-03 20:32:46 +09:00
co-authored by Claude Opus 5
parent 0f068a6c1d
commit b8aa5ce014
6 changed files with 66 additions and 14 deletions
@@ -4,9 +4,9 @@
*
* 표시 항목(별표2 근거):
* 최대 기울기 x.x% (상한 y%) — Ⅰ.2.라: 설계속도·지형별 상한 준수 판정
* 불균형 z% / 허용 w% — .1.나.(4)(다): 시공계획고 절·성토 균형
* 곡선 필요 n곳 — Ⅰ.2.마: 대수차 5% 초과 변화점의 종단곡선 삽입
* 절·성토 면적은 하단 유토곡선이 부피로 더 정확히 보여 주고, 변화점·종단곡선 개수와
* 절·성토 균형은 **하단 유토곡선**이 횡단 기준 부피(㎥)로 보여 준다 — 여기 있던 종단 기준
* 불균형(㎡ 비율)은 실제 토량이 아니라 의미가 없어 뺐다(2026-09-03). 변화점·종단곡선 개수와
* 기본 곡선길이 L은 판단 기준이 없어 뺐다(2026-08-19 사용자 지적 + 지식DB 대조).
* [저장선 복원]은 상단에서 내려 그래프 조작부로 옮겼다.
*
@@ -53,7 +53,7 @@ export function renderBalanceBar(params: BalanceBarParams): void {
return;
}
const { alignment } = params;
const { balance, policy, violations } = alignment;
const { policy, violations } = alignment;
// 최대 종단기울기 — 법정 상한 대비가 이 화면의 첫 판정 항목이다(별표2 Ⅰ.2.라).
const steepest = alignment.segments.reduce(
(worst, segment) => Math.max(worst, Math.abs(segment.grade_percent)),
@@ -70,12 +70,10 @@ export function renderBalanceBar(params: BalanceBarParams): void {
`${steepest.toFixed(1)} % / 상한 ${policy.max_grade_pct.toFixed(1)} %`,
violations.length ? "over" : undefined,
],
[
"절·성토 불균형",
`${balance.imbalance_percent.toFixed(1)} % / 허용 ${balance.tolerance_percent.toFixed(0)} %`,
balance.within_tolerance ? undefined : "over",
],
];
// 절·성토 불균형은 2026-09-03 사용자 지시로 뺐다. 이 값은 **종단 기준**이었다 —
// 계획선과 지반선 사이 세로 면적(㎡)의 절·성토 비이지 실제 토량이 아니다. 의미 있는
// 균형은 횡단 단면적을 쌓아 부피로 내는 하단 유토곡선 요약(㎥)이고, 그쪽이 이미 있다.
// 필요한 곳이 없으면 적지 않는다 — "0곳"은 화면 폭만 먹는다.
if (curvesNeeded) entries.push(["종단곡선 필요", `${curvesNeeded}`, "over"]);
// 횡단배수 최소고 표시는 2026-09-02 사용자 지시로 삭제했다. 편집 차단(강제)은
+23 -4
View File
@@ -293,7 +293,11 @@ export function createRouteMassHaulPanel(onChanged: () => void): RouteMassHaulPa
mirror(scroll, main);
}
/** 곡선을 한 번이라도 그렸는지 — 재계산 대기 중 화면을 비우지 않기 위한 표시. */
let drawnOnce = false;
function note(message: string): void {
drawnOnce = false;
bar.replaceChildren();
scroll.replaceChildren();
legendLayer.replaceChildren();
@@ -312,11 +316,25 @@ export function createRouteMassHaulPanel(onChanged: () => void): RouteMassHaulPa
note("토량환산계수를 불러오지 못해 유토곡선을 계산할 수 없습니다.");
return;
}
// 저장된 횡단이 지금 계획선과 어긋나 있으면 그 면적은 **옛 계획고로 만든 값**이다.
// 그걸 잠깐 그렸다가 재계산 결과로 갈아 끼우면 사용자가 옛 그림을 본다(2026-09-03
// 사용자 확정: 「새 값만 보여주기」). Panel이 이미 재계산을 예약해 두므로 기다린다.
// 저장된 횡단이 지금 계획선과 어긋나 있으면 그 면적은 옛 계획고로 만든 값이다 —
// 새 값이 올 때까지 기다린다(2026-09-03 사용자 확정: 「새 값만 보여주기」).
//
// 다만 **곡선을 지우지는 않는다**. 계획고를 연속으로 누르면(초당 10회) 매번 곡선이
// 사라졌다 다시 서서 화면이 깜빡였다(2026-09-03 사용자 보고). 재계산은 브라우저
// 안에서 끝나 수십 ms면 끝나므로, 그동안 **직전 곡선을 그대로 두고** 상태줄로만
// 알린다. 아직 한 번도 못 그렸을 때만 문구로 자리를 채운다.
if (params.pendingRecalc) {
note("횡단을 지금 계획선에 맞춰 다시 계산하는 중입니다.");
if (drawnOnce) {
const pending = bar.querySelector(".b05-profile__masshaul-pending");
if (!pending) {
const chip = document.createElement("span");
chip.className = "b05-profile__masshaul-pending";
chip.textContent = "다시 계산 중…";
bar.append(chip);
}
} else {
note("횡단을 지금 계획선에 맞춰 다시 계산하는 중입니다.");
}
return;
}
// B06과 같은 계산 — 횡단 기준(정식)과 종단 기준(개략 비교)을 함께 낸다.
@@ -386,6 +404,7 @@ export function createRouteMassHaulPanel(onChanged: () => void): RouteMassHaulPa
onChanged();
}),
);
drawnOnce = true;
}
return {
@@ -19,6 +19,7 @@ import { createProfileHistory, type ProfileHistory } from "./B05_Profile_UI_Prof
import { createProfileTools, type ProfileTools } from "./B05_Profile_UI_Profile_Tools";
import {
detectStraightRun,
hasStraightRuns,
shiftStraightRuns,
straightenBetween,
tiltStraightRun,
@@ -118,6 +119,10 @@ export function createPanelTools(ctx: PanelToolsContext): PanelTools {
onRedo: () => inner.redo(),
canUndo: () => inner.canUndo(),
canRedo: () => inner.canRedo(),
hasStraightRuns: () => {
const alignment = ctx.alignment();
return alignment ? hasStraightRuns(alignment) : false;
},
onChanged: ctx.refresh,
});
@@ -114,6 +114,22 @@ export function detectStraightRun(
};
}
/**
* 지금 계획선에 **직선화된 구간이 하나라도 있는가** — [쉬프트] 안내 문구용.
*
* 초기 계획선은 모든 측점이 변화점이라 마디마다 기울기가 달라 직선 구간이 없다. 그때
* [쉬프트]로 아무 데나 눌러도 잡히는 것이 없어 「선택이 안 된다」로 보였다
* (2026-09-03 사용자 보고). 안내를 나누려면 이 판정이 필요하다.
*/
export function hasStraightRuns(alignment: ProfileAlignment): boolean {
for (let index = 1; index < alignment.segments.length; index += 1) {
if (Math.abs(gradeAt(alignment, index) - gradeAt(alignment, index - 1)) < COLLINEAR_EPSILON) {
return true;
}
}
return false;
}
/** 두 측점 사이(끝 제외)의 변화점 chainage. */
function interiorNodes(alignment: ProfileAlignment, fromM: number, toM: number): number[] {
return pviChainages(alignment).filter(
+8 -2
View File
@@ -28,6 +28,8 @@ export interface ProfileToolsCallbacks {
onRedo: () => void;
canUndo: () => boolean;
canRedo: () => boolean;
/** 지금 계획선에 직선화된 구간이 있는가 — [쉬프트] 안내 문구를 가른다. */
hasStraightRuns: () => boolean;
/** 선택 표시를 갱신해야 할 때(모드·선택 변화) 호출된다. */
onChanged: () => void;
}
@@ -166,12 +168,16 @@ export function createProfileTools(callbacks: ProfileToolsCallbacks): ProfileToo
if (mode === "straighten" && pending !== null) {
const hint = document.createElement("em");
hint.className = "b05-route-profile__tool-hint";
hint.textContent = `${pending.toFixed(1)}m 선택 — 두 번째 측점을 고르세요`;
hint.textContent = `${pending.toFixed(1)}m 선택 — 두 번째 측점을 고르세요 (취소는 [직선화] 다시 누르기)`;
wrap.append(hint);
} else if (mode === "shift" && !runs.length) {
const hint = document.createElement("em");
hint.className = "b05-route-profile__tool-hint";
hint.textContent = "직선화된 라인을 고르세요";
// 고를 것이 아예 없는 경우와 있는데 안 고른 경우를 갈라 적는다 — 초기 계획선은
// 마디마다 기울기가 달라 직선 구간이 없어, 눌러도 안 잡히는 것이 정상이다.
hint.textContent = callbacks.hasStraightRuns()
? "직선화된 라인을 고르세요 (여러 개 가능) — 끝내려면 [쉬프트]를 다시 누르세요"
: "직선화된 구간이 없습니다 — [직선화]로 먼저 만드세요";
wrap.append(hint);
}
return wrap;
@@ -112,6 +112,14 @@
padding-top: 0;
}
/* 재계산 대기 표시 — 곡선을 지우지 않고 요약줄 끝에만 붙인다(2026-09-03 깜빡임 제거). */
.b05-profile__masshaul-pending {
margin-left: auto;
color: var(--color-text-secondary);
font-size: var(--text-caption);
opacity: 0.8;
}
.b05-profile__masshaul-empty {
padding: var(--spacing-8) var(--spacing-16);
color: var(--color-text-secondary);