Files
Aislo/B05_Profile/B05_Profile_UI_RouteEdit_Recommend_Help.ts
T
eomsangdonandClaude Opus 5.5 6aa77e1bb7 feat(B05): 노선 추천 정리 — 알약 다섯 한 줄 · 종단은 갈래가 정함 · 오른쪽 위 [적용]/[닫기] · 지도 안 옵션 겹창 · 추천 토량에 곡선부 확폭(77)
- 알약 = 예상노선 추적(종단 없음) · 영선 · 토공 최소 · 기울기 최소 · 비중 계산(넷은 종단 늘 반영) · 안전 우선 · 계류 횡단 최소 · 공사비 어림 최소 화면에서 뺌
- 「종단 반영」 켬/끔 단추 없앰(추천 창 · 종단 띠) — 띠 · [확인]은 지금 추천 갈래로 종단 여부
- ✕ 없애고 [적용] · [닫기]를 제목줄 오른쪽 위로 · 옵션 칸은 지도 왼쪽 위 작은 겹창 · 창 폭 넓힘
- 적용한 추천 갈래가 있으면 창을 다시 열 때 그 갈래가 골라진 채
- 도움말: 종단 반영 = 측점마다 횡단을 보고 정한 종단(적분값 아님)
- 균형 종단 단면적에 곡선부 확폭(B05 측점 생성과 같은 셈) — 빼서 확정 뒤 성토가 1.7배 벌어졌음(노선 267 같은 계획고: 성 10,102 → 17,819 · 본 설계 16,140)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019EMfGERKUg5nMMNQsyVZ2j
2026-10-07 21:29:25 +09:00

200 lines
10 KiB
TypeScript

/* =============================================================================
* B05_Profile_UI_RouteEdit_Recommend_Help.ts
* [노선 추천] 모달 오른쪽 설명 칸(계획서 48-2 · 64-2 · 77) — 갈래 다섯 · 종단 반영 · 공통 제약 넷 · 결과 줄 읽는 법.
*
* 글은 서버 엔진(`B05_Profile_Engine_RouteInitial*`)이 실제로 하는 일을 따라 적는다 — 엔진을
* 바꾸면 여기 글도 같이 고친다. 갈래 설명은 **한 번에 한 갈래**(쪽) — 위 ◀ ▶ 단추와 「n / 전체」로
* 넘기고, 고른 갈래(알약)를 바꾸면 그 갈래 쪽으로 간다.
* ========================================================================== */
import type { RouteInitialLogic } from "./B05_Profile_Api_RouteInitial";
import { LOGIC_LABEL } from "./B05_Profile_UI_RouteEdit_Initial_Model";
interface HelpEntry {
/** 한 줄 요약. */
summary: string;
/** 노선을 만드는 법. */
how: string[];
/** 이럴 때 고름. */
when: string;
/** 주의할 점. */
caution: string;
/** 종단 반영이 이 갈래에 하는 일. */
profile?: string;
}
type ShownLogic = "follow" | "gradeline" | "earthwork" | "gentle" | "weighted";
const LOGIC_HELP: Record<ShownLogic, HelpEntry> = {
follow: {
summary: "전처리 때 만든 예상노선(점선)을 그대로 따라가는 노선",
how: [
"새로 길을 찾지 않음 — 예상노선 꺾임점에 최소 곡선반지름으로 곡선만 넣음",
"전처리 초기 계획노선과 같은 규칙이라 같은 선이 나옴",
"종단기울기 · 계류 이격은 막지 않고, 넘은 곳만 「못 지킨 구간」에 적음",
],
when: "현장 답사로 정한 노선을 출발점으로 삼을 때 · 가장 빨리 나옴",
caution: "법정 기울기 · 계류 이격을 보장하지 않음 — 못 지킨 구간을 꼭 확인",
profile: "없음 — 전처리 계획선처럼 계획고 = 지반고",
},
gradeline: {
summary: "정한 기울기로 등고선을 비스듬히 건너는 등경사선(영선) 노선",
how: [
"구간마다 기울기가 「목표 기울기」에 가장 가깝게 붙도록 길을 찾음",
"목표 칸을 비우면 종단기울기 상한이 목표 · 상한보다 크게 넣으면 상한으로 잘림",
"후보 중 목표와의 차(측점 20m 기울기 평균)가 가장 작은 것을 고름",
],
when: "일정한 기울기로 오르내리는 노선이 필요할 때(지형도 위 영선 잡기와 같은 일)",
caution:
"오르막 · 내리막을 가리지 않음 · 목표를 비우면 상한에 붙은 가파른 노선이 되기 쉬움 — 목표를 상한보다 조금 낮게 넣기를 권함",
},
earthwork: {
summary: "깎고 쌓는 흙(절토 + 성토)이 가장 적은 노선",
how: [
"옆경사가 급한 곳 · 지반이 상한보다 가파른 곳을 비싸게 보고 길을 찾음",
"후보 중 절토 + 성토 어림 양이 가장 적은 것을 고름",
"흙 양 = 계획고를 상한 안으로 맞춘 선과 옆경사 단면(노폭 4m)으로 어림",
],
when: "토공량 · 사토 처리를 줄여야 할 때",
caution: "비탈면 흙은 빠진 어림값 · 연장이 길어지는지 비교값으로 함께 볼 것",
profile: "후보마다 균형 종단을 세워 절 + 성 + 반입 + 사토 ㎥ 로 비교",
},
gentle: {
summary: "종단기울기가 가장 순한 노선",
how: [
"기울기가 상한에 가까울수록 크게 벌점을 매겨 길을 찾음",
"후보 중 가장 가파른 구간의 기울기가 가장 낮은 것을 고름(같으면 평균 기울기)",
],
when: "차량 주행 · 노면 침식이 걱정되는 노선",
caution: "거리 비용이 작아 돌아가는 긴 노선이 나올 수 있음 — 연장을 확인",
},
weighted: {
summary: "예상 · 토공 · 기울기 세 비중(합 100)을 섞어 찾은 노선",
how: [
"예상 = 예상노선에서 떨어지지 않기 · 토공 = 흙 양 줄이기 · 기울기 = 순한 기울기",
"비중대로 섞은 비용 하나로 한 번 길을 찾음 — 한쪽이 100 이면 그 갈래와 같은 노선",
"자물쇠로 비중 하나를 고정하면 나머지 둘만 움직임 · 기본 50 / 25 / 25",
],
when: "예상노선을 어느 정도 살리면서 토공 · 기울기도 챙기고 싶을 때",
caution: "제약은 벌점일 뿐이라 넘는 구간이 남을 수 있음 — 못 지킨 구간을 확인",
},
};
const CRITERIA_HELP: Array<{ name: string; text: string }> = [
{
name: "종단 반영",
text: "예상노선 추적을 뺀 네 갈래는 늘 반영. 원지반선과 계획선 사이 넓이를 적분한 값이 아니라, 측점(20m)마다 횡단을 세워 절토 · 성토 면적을 보고 그 측점 계획고를 정한 종단 — 그래서 갈래 · 노선에 따라 값의 편차가 큼. 구조물을 뺀 빠른 셈이라 [확인] 뒤 본 설계 값과 차이가 남. [확인]하면 편집 창 아래 띠에 보인 종단이 그대로 첫 계획선이 됨",
},
{
name: "종단기울기 상한(%)",
text: "이보다 가파른 구간을 만들지 않음. 비우면 임도 종류 · 지형 · 설계속도 법정 값(20km/h 일반 9% · 특수 14%). 곡선을 넣은 뒤 넘으면 상한을 조금씩 낮춰 다시 찾음",
},
{
name: "최소 반지름(m)",
text: "꺾는 곳마다 이 반지름의 곡선이 들어갈 만큼 곧게 간 뒤에만 다시 꺾음. 비우면 설계속도 · 지형 법정 값(20km/h 15m · 12m)",
},
{
name: "계류 이격(m)",
text: "계류에서 이 거리 안으로는 따라 걷지 않고 건너거나 빠져나가는 방향으로만 움직임. 비우면 100m(타당성평가 「산지계류 100m 이내」 기준)",
},
{
name: "지반 고름 폭(m)",
text: "기울기를 잴 때 지형 잔굴곡을 고르는 폭. 클수록 매끈한 지반으로 봄. 비우면 5m",
},
];
const LINES_HELP: Array<{ name: string; text: string }> = [
{
name: "비교값",
text: "연장 · 기울기 최대 · 평균 · 옆경사 · 절토 · 성토 · 계류 횡단 · 공사비 어림 — 갈래를 바꿔 가며 견줌",
},
{
name: "못 지킨 구간",
text: "제약을 넘은 측점(5곳까지 · 나머지는 「외 N곳」, 전체는 마우스를 올리면 보임)",
},
{
name: "상한을 풂",
text: "지형상 상한 안으로는 시점 · 종점을 이을 수 없어 꼭 필요한 만큼만 상한을 올린 경우",
},
{
name: "거의 같은 노선",
text: "제약 안에서 갈 길이 좁은 띠 하나뿐이라 갈래가 달라도 노선이 겹치는 경우 — 제약 값을 바꾸면 벌어질 수 있음",
},
];
const LOGIC_ORDER: ShownLogic[] = ["follow", "gradeline", "earthwork", "gentle", "weighted"];
/** 갈래 설명 쪽 하나 — 고른 갈래(`is-on`)는 테두리로 알린다. */
function logicPage(key: ShownLogic): string {
const entry = LOGIC_HELP[key];
const profile = entry.profile ? `<p><b>종단 반영</b> ${entry.profile}</p>` : "";
return `<article class="b05-routerec__help-item" data-help-logic="${key}" hidden>
<h4>${LOGIC_LABEL[key]} <span class="b05-routerec__help-picked">고른 갈래</span></h4>
<p class="b05-routerec__help-summary">${entry.summary}</p>
<ul>${entry.how.map((line) => `<li>${line}</li>`).join("")}</ul>
<p><b>이럴 때</b> ${entry.when}</p>
${profile}
<p class="b05-routerec__help-caution"><b>주의</b> ${entry.caution}</p>
</article>`;
}
/** 설명 칸 HTML — 모달 본문 끝에 붙는다(세로 구분선 오른쪽). */
export function recommendHelpHtml(): string {
const list = (items: Array<{ name: string; text: string }>): string =>
`<dl>${items.map((item) => `<dt>${item.name}</dt><dd>${item.text}</dd>`).join("")}</dl>`;
return `<aside class="b05-routerec__help" aria-label="노선 추천 설명">
<h3>추천 갈래</h3>
<p class="b05-routerec__help-lead">제약을 지키는 후보 노선 여러 벌을 찾은 뒤, 갈래마다 자기 기준이 가장 좋은 것을 고름.</p>
<div class="b05-routerec__help-pager" role="group" aria-label="갈래 설명 쪽 넘기기">
<button type="button" class="ui-btn ui-btn--ghost" data-help-nav="-1"
aria-label="앞 갈래 설명">◀</button>
<span data-help-page aria-live="polite"></span>
<button type="button" class="ui-btn ui-btn--ghost" data-help-nav="1"
aria-label="다음 갈래 설명">▶</button>
</div>
${LOGIC_ORDER.map(logicPage).join("")}
<h3>공통 제약</h3>
<p class="b05-routerec__help-lead">예상노선 추적 · 비중 계산을 뺀 갈래는 넘지 않음 · 칸을 비우면 기본값.</p>
${list(CRITERIA_HELP)}
<h3>결과 줄 읽는 법</h3>
${list(LINES_HELP)}
</aside>`;
}
/** 설명 쪽을 `index` 번째 갈래로 — 쪽 수는 갈래 수. */
function showHelpPage(aside: HTMLElement, index: number): void {
const page = Math.min(Math.max(index, 0), LOGIC_ORDER.length - 1);
aside.dataset.page = String(page);
aside.querySelectorAll<HTMLElement>("[data-help-logic]").forEach((item) => {
item.hidden = item.dataset.helpLogic !== LOGIC_ORDER[page];
});
aside.querySelector<HTMLElement>("[data-help-page]")!.textContent =
`${page + 1} / ${LOGIC_ORDER.length}`;
aside.querySelector<HTMLButtonElement>('[data-help-nav="-1"]')!.disabled = page === 0;
aside.querySelector<HTMLButtonElement>('[data-help-nav="1"]')!.disabled =
page === LOGIC_ORDER.length - 1;
aside.scrollTop = 0;
}
/** ◀ ▶ 단추 — 모달 본문에 한 번만 건다. */
export function bindRecommendHelp(body: HTMLElement): void {
body.addEventListener("click", (event) => {
const button = (event.target as HTMLElement).closest<HTMLElement>("[data-help-nav]");
const aside = body.querySelector<HTMLElement>(".b05-routerec__help");
if (!button || !aside) return;
showHelpPage(aside, Number(aside.dataset.page ?? 0) + Number(button.dataset.helpNav));
});
}
/** 고른 갈래를 표시하고, 고른 갈래가 바뀐 때만 그 갈래 쪽으로 간다(넘겨 보던 쪽은 그대로 둠). */
export function syncRecommendHelp(body: HTMLElement, logic: RouteInitialLogic): void {
const aside = body.querySelector<HTMLElement>(".b05-routerec__help");
if (!aside) return;
aside.querySelectorAll<HTMLElement>("[data-help-logic]").forEach((item) => {
item.classList.toggle("is-on", item.dataset.helpLogic === logic);
});
if (aside.dataset.pickedLogic === logic) return;
aside.dataset.pickedLogic = logic;
showHelpPage(aside, Math.max(0, LOGIC_ORDER.indexOf(logic as ShownLogic)));
}