feat(B05): 노선 추천 모달 오른쪽 설명 칸 — 세로 구분선 · 갈래 여덟 · 공통 제약 넷 · 결과 줄 읽는 법
- 기존 칸 그대로 · 모달 폭 960 → 1320px · 좁은 폭(1000px 이하)은 아래로 - 글은 서버 엔진 실제 동작 기준(만드는 법 · 이럴 때 · 주의) — `_Recommend_Help.ts` 로 나눔 - 고른 갈래 설명 강조 · 보이는 자리로 굴림 - 검증 시나리오 ui_checks/48-2 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015KYZ2HWWdUwZWYnn3tQz7X
This commit is contained in:
@@ -43,6 +43,7 @@ import {
|
||||
type WeightKey,
|
||||
} from "./B05_Profile_UI_RouteEdit_Initial_Model";
|
||||
import { createRecommendMap, RECOMMEND_MAP_HTML } from "./B05_Profile_UI_RouteEdit_Recommend_Map";
|
||||
import { recommendHelpHtml, syncRecommendHelp } from "./B05_Profile_UI_RouteEdit_Recommend_Help";
|
||||
import "./B05_Profile_UI_Style_RouteRecommend.css";
|
||||
|
||||
type Vertex = [number, number];
|
||||
@@ -225,7 +226,8 @@ export function openRecommendModal(params: RecommendParams): void {
|
||||
<span class="b05-routerec__state" data-state></span>
|
||||
<button type="button" class="ui-btn ui-btn--filled" data-act="apply" disabled>적용</button>
|
||||
<button type="button" class="ui-btn ui-btn--ghost" data-act="close">닫기</button>
|
||||
</div>`;
|
||||
</div>
|
||||
${recommendHelpHtml()}`;
|
||||
|
||||
const q = <T extends HTMLElement>(sel: string): T => body.querySelector<T>(sel)!;
|
||||
map = createRecommendMap(q<HTMLElement>(".b05-routerec__mapwrap"));
|
||||
@@ -342,6 +344,7 @@ export function openRecommendModal(params: RecommendParams): void {
|
||||
? criteriaLine(preview.criteria, params.planMinRadiusM, preview.logic === "gradeline")
|
||||
: "";
|
||||
line("criteria").hidden = !preview;
|
||||
syncRecommendHelp(body, logic);
|
||||
drawMap();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
/* =============================================================================
|
||||
* B05_Profile_UI_RouteEdit_Recommend_Help.ts
|
||||
* [노선 추천] 모달 오른쪽 설명 칸(계획서 48-2) — 갈래 여덟 · 공통 제약 넷 · 결과 줄 읽는 법.
|
||||
*
|
||||
* 글은 서버 엔진(`B05_Profile_Engine_RouteInitial*`)이 실제로 하는 일을 따라 적는다 — 엔진을
|
||||
* 바꾸면 여기 글도 같이 고친다. 고른 갈래는 강조하고 보이는 자리로 굴린다.
|
||||
* ========================================================================== */
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const LOGIC_HELP: Record<RouteInitialLogic, HelpEntry> = {
|
||||
follow: {
|
||||
summary: "전처리 때 만든 예상노선(점선)을 그대로 따라가는 노선",
|
||||
how: [
|
||||
"새로 길을 찾지 않음 — 예상노선 꺾임점에 최소 곡선반지름으로 곡선만 넣음",
|
||||
"전처리 초기 계획노선과 같은 규칙이라 같은 선이 나옴",
|
||||
"종단기울기 · 계류 이격은 막지 않고, 넘은 곳만 「못 지킨 구간」에 적음",
|
||||
],
|
||||
when: "현장 답사로 정한 노선을 출발점으로 삼을 때 · 가장 빨리 나옴",
|
||||
caution: "법정 기울기 · 계류 이격을 보장하지 않음 — 못 지킨 구간을 꼭 확인",
|
||||
},
|
||||
gradeline: {
|
||||
summary: "정한 기울기로 등고선을 비스듬히 건너는 등경사선(영선) 노선",
|
||||
how: [
|
||||
"구간마다 기울기가 「목표 기울기」에 가장 가깝게 붙도록 길을 찾음",
|
||||
"목표 칸을 비우면 종단기울기 상한이 목표 · 상한보다 크게 넣으면 상한으로 잘림",
|
||||
"후보 중 목표와의 차(측점 20m 기울기 평균)가 가장 작은 것을 고름",
|
||||
],
|
||||
when: "일정한 기울기로 오르내리는 노선이 필요할 때(지형도 위 영선 잡기와 같은 일)",
|
||||
caution:
|
||||
"오르막 · 내리막을 가리지 않음 · 목표를 비우면 상한에 붙은 가파른 노선이 되기 쉬움 — 목표를 상한보다 조금 낮게 넣기를 권함",
|
||||
},
|
||||
earthwork: {
|
||||
summary: "깎고 쌓는 흙(절토 + 성토)이 가장 적은 노선",
|
||||
how: [
|
||||
"옆경사가 급한 곳 · 지반이 상한보다 가파른 곳을 비싸게 보고 길을 찾음",
|
||||
"후보 중 절토 + 성토 어림 양이 가장 적은 것을 고름",
|
||||
"흙 양 = 계획고를 상한 안으로 맞춘 선과 옆경사 단면(노폭 4m)으로 어림",
|
||||
],
|
||||
when: "토공량 · 사토 처리를 줄여야 할 때",
|
||||
caution: "비탈면 흙은 빠진 어림값 · 연장이 길어지는지 비교값으로 함께 볼 것",
|
||||
},
|
||||
gentle: {
|
||||
summary: "종단기울기가 가장 순한 노선",
|
||||
how: [
|
||||
"기울기가 상한에 가까울수록 크게 벌점을 매겨 길을 찾음",
|
||||
"후보 중 가장 가파른 구간의 기울기가 가장 낮은 것을 고름(같으면 평균 기울기)",
|
||||
],
|
||||
when: "차량 주행 · 노면 침식이 걱정되는 노선",
|
||||
caution: "거리 비용이 작아 돌아가는 긴 노선이 나올 수 있음 — 연장을 확인",
|
||||
},
|
||||
safe: {
|
||||
summary: "급한 옆경사 · 계류 가까운 곳을 피하는 노선",
|
||||
how: [
|
||||
"옆경사 35°(약 70%) 이상 · 계류 이격 거리 안을 위험 구간으로 보고 피함",
|
||||
"후보 중 위험 구간을 지나는 길이가 가장 짧은 것을 고름(같으면 평균 옆경사)",
|
||||
"35° 기준 = 「35° 이상 전폭 절토」 기술 기준",
|
||||
],
|
||||
when: "붕괴 · 유실 위험이 큰 급경사지 · 계곡 가까운 지형",
|
||||
caution:
|
||||
"계곡을 따라가는 지형에서는 위험 구간이 어디서나 길어 다른 갈래와 차이가 작음 — 계류 이격 값을 줄여 다시 셈해 볼 것",
|
||||
},
|
||||
crossings: {
|
||||
summary: "계류를 건너는 횟수가 가장 적은 노선",
|
||||
how: [
|
||||
"계류 위를 지나는 칸에 벌점을 매겨 길을 찾음",
|
||||
"후보 중 계류 횡단 수가 가장 적은 것을 고름(같으면 계류에서 더 먼 쪽)",
|
||||
"10m 안에서 여러 번 건너는 것은 한 번으로 셈",
|
||||
],
|
||||
when: "배수관 · 세월교 같은 횡단 구조물을 줄이고 싶을 때",
|
||||
caution: "소하천 · 지방하천 · 국가하천만 셈 — 작은 세류는 빠짐",
|
||||
},
|
||||
cost: {
|
||||
summary: "토공 · 연장 · 계류 횡단을 한 숫자로 묶은 공사비 어림이 가장 낮은 노선",
|
||||
how: [
|
||||
"공사비 어림 = 흙 1㎥ × 1 + 연장 1m × 2 + 계류 횡단 1곳 × 400(흙 ㎥ 상당)",
|
||||
"같은 비율을 1m 당 값으로 바꿔 길을 찾고, 후보 중 어림값이 가장 낮은 것을 고름",
|
||||
],
|
||||
when: "토공 · 연장 · 구조물 수를 한꺼번에 비교하고 싶을 때",
|
||||
caution: "실제 원가가 아닌 상대값(임시 비율) — 갈래끼리 비교용으로만 볼 것",
|
||||
},
|
||||
weighted: {
|
||||
summary: "예상 · 토공 · 기울기 세 비중(합 100)을 섞어 찾은 노선",
|
||||
how: [
|
||||
"예상 = 예상노선에서 떨어지지 않기 · 토공 = 흙 양 줄이기 · 기울기 = 순한 기울기",
|
||||
"비중대로 섞은 비용 하나로 한 번 길을 찾음 — 한쪽이 100 이면 그 갈래와 같은 노선",
|
||||
"자물쇠로 비중 하나를 고정하면 나머지 둘만 움직임 · 기본 50 / 25 / 25",
|
||||
],
|
||||
when: "예상노선을 어느 정도 살리면서 토공 · 기울기도 챙기고 싶을 때",
|
||||
caution: "제약은 벌점일 뿐이라 넘는 구간이 남을 수 있음 — 못 지킨 구간을 확인",
|
||||
},
|
||||
};
|
||||
|
||||
const CRITERIA_HELP: Array<{ name: string; text: string }> = [
|
||||
{
|
||||
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: RouteInitialLogic[] = [
|
||||
"follow",
|
||||
"gradeline",
|
||||
"earthwork",
|
||||
"gentle",
|
||||
"safe",
|
||||
"crossings",
|
||||
"cost",
|
||||
"weighted",
|
||||
];
|
||||
|
||||
/** 설명 칸 HTML — 모달 본문 끝에 붙는다(세로 구분선 오른쪽). */
|
||||
export function recommendHelpHtml(): string {
|
||||
const logics = LOGIC_ORDER.map((key) => {
|
||||
const entry = LOGIC_HELP[key];
|
||||
return `<article class="b05-routerec__help-item" data-help-logic="${key}">
|
||||
<h4>${LOGIC_LABEL[key]}</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>
|
||||
<p class="b05-routerec__help-caution"><b>주의</b> ${entry.caution}</p>
|
||||
</article>`;
|
||||
}).join("");
|
||||
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>
|
||||
${logics}
|
||||
<h3>공통 제약</h3>
|
||||
<p class="b05-routerec__help-lead">예상노선 추적 · 비중 계산을 뺀 갈래는 넘지 않음 · 칸을 비우면 기본값.</p>
|
||||
${list(CRITERIA_HELP)}
|
||||
<h3>결과 줄 읽는 법</h3>
|
||||
${list(LINES_HELP)}
|
||||
</aside>`;
|
||||
}
|
||||
|
||||
/** 고른 갈래 설명을 강조하고 보이는 자리로 굴린다. */
|
||||
export function syncRecommendHelp(body: HTMLElement, logic: RouteInitialLogic): void {
|
||||
body.querySelectorAll<HTMLElement>("[data-help-logic]").forEach((item) => {
|
||||
const on = item.dataset.helpLogic === logic;
|
||||
if (on && !item.classList.contains("is-on")) item.scrollIntoView({ block: "nearest" });
|
||||
item.classList.toggle("is-on", on);
|
||||
});
|
||||
}
|
||||
@@ -14,8 +14,9 @@
|
||||
z-index: calc(var(--z-modal, 1000) + 2);
|
||||
}
|
||||
|
||||
/* 오른쪽 설명 칸(계획서 48-2)만큼 넓힘 — 왼쪽 기존 칸은 예전 폭 그대로. */
|
||||
.ui-modal.b05-routerec {
|
||||
width: min(960px, 94vw);
|
||||
width: min(1320px, 96vw);
|
||||
--modal-max: 92vh;
|
||||
font-size: var(--text-body-sm);
|
||||
}
|
||||
@@ -25,6 +26,119 @@
|
||||
font-size: var(--text-body-sm);
|
||||
}
|
||||
|
||||
/* 왼쪽 = 기존 칸 넷(알약 · 지도 · 결과 줄 · 바닥) · 오른쪽 = 세로 구분선 + 설명 칸. */
|
||||
.ui-modal.b05-routerec .b05-routerec__body {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(280px, 340px);
|
||||
align-content: start;
|
||||
column-gap: var(--spacing-16, 16px);
|
||||
}
|
||||
|
||||
.b05-routerec__body > :not(.b05-routerec__help) {
|
||||
grid-column: 1;
|
||||
}
|
||||
|
||||
/* 설명 칸 — 왼쪽 높이를 따라가고(제 글 길이로 창을 키우지 않음) 넘치면 제 안에서 굴림. */
|
||||
.b05-routerec__help {
|
||||
grid-column: 2;
|
||||
grid-row: 1 / span 4;
|
||||
contain: size;
|
||||
overflow-y: auto;
|
||||
padding-left: var(--spacing-16, 16px);
|
||||
border-left: 1px solid var(--color-border);
|
||||
color: var(--color-text-body);
|
||||
font-size: var(--text-body-sm);
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.b05-routerec__help h3 {
|
||||
margin: var(--spacing-12) 0 var(--spacing-4, 4px);
|
||||
color: var(--color-text-heading, var(--color-text-body));
|
||||
font-size: var(--text-body);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.b05-routerec__help h3:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.b05-routerec__help-lead {
|
||||
margin: 0 0 var(--spacing-8);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.b05-routerec__help-item {
|
||||
margin-bottom: var(--spacing-8);
|
||||
padding: var(--spacing-8);
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--radius-8, 6px);
|
||||
}
|
||||
|
||||
/* 고른 갈래 — 알약과 같은 강조색 테두리. */
|
||||
.b05-routerec__help-item.is-on {
|
||||
border-color: var(--color-primary);
|
||||
background: var(--color-surface);
|
||||
}
|
||||
|
||||
.b05-routerec__help-item h4 {
|
||||
margin: 0 0 2px;
|
||||
font-size: var(--text-body-sm);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.b05-routerec__help-item.is-on h4 {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.b05-routerec__help-item p,
|
||||
.b05-routerec__help dd {
|
||||
margin: 2px 0;
|
||||
}
|
||||
|
||||
.b05-routerec__help-item ul {
|
||||
margin: 2px 0;
|
||||
padding-left: 1.1em;
|
||||
}
|
||||
|
||||
.b05-routerec__help-summary {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.b05-routerec__help-caution {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.b05-routerec__help dl {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.b05-routerec__help dt {
|
||||
margin-top: var(--spacing-8);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.b05-routerec__help dd {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
/* 좁은 폭 — 설명 칸은 아래로(가로 구분선 · 제 높이 한도 안에서 굴림). */
|
||||
@media (max-width: 1000px) {
|
||||
.ui-modal.b05-routerec .b05-routerec__body {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.b05-routerec__help {
|
||||
grid-column: 1;
|
||||
grid-row: auto;
|
||||
contain: none;
|
||||
max-height: 280px;
|
||||
padding-top: var(--spacing-12);
|
||||
padding-left: 0;
|
||||
border-top: 1px solid var(--color-border);
|
||||
border-left: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* 알약 — 하나만 고른 상태(라디오 묶음). */
|
||||
/* 여덟 알약 — 네 칸 두 줄로 고르게(좁은 폭은 두 칸 네 줄). */
|
||||
.b05-routerec__pills {
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/* 48-2 · B05 계획노선 편집 [노선 추천] 모달 — 오른쪽 세로 구분선 + 설명 칸
|
||||
* 시작 화면: B05 종단설계 · [계획노선 편집] 창이 떠 있음(노선 추천 모달은 닫힘)
|
||||
* 부르는 법: orca eval --expression "$(cat 이 파일)" (async 식 · JSON 글을 돌려줌)
|
||||
* 하는 일: [노선 추천] 열기 → 설명 칸 자리 · 구분선 · 갈래 여덟 확인 → 「비중 계산」 누름 → 강조가 따라가고
|
||||
* 보이는 자리에 있는지 확인 → 셈이 끝나면 [닫기](미리 보기 버림 · 편집값 안 바뀜)
|
||||
* 기대 수치: 설명 칸이 왼쪽 칸 오른쪽(left > 지도 right) · 왼쪽 테두리 1px · 갈래 글 8 · 처음 강조 follow
|
||||
* · 누른 뒤 강조 weighted 1개 · 강조 글이 설명 칸 안에 보임 · 설명 칸 높이 = 본문 높이(창을 키우지 않음)
|
||||
*/
|
||||
(async () => {
|
||||
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
const editor = document.querySelector(".b05-routeedit");
|
||||
[...editor.querySelectorAll("button")].find((b) => b.textContent.trim() === "노선 추천").click();
|
||||
await wait(1500);
|
||||
const modal = document.querySelector(".ui-modal.b05-routerec");
|
||||
const body = modal.querySelector(".b05-routerec__body");
|
||||
const help = modal.querySelector(".b05-routerec__help");
|
||||
const map = modal.querySelector(".b05-routerec__main").getBoundingClientRect();
|
||||
const box = help.getBoundingClientRect();
|
||||
const out = {
|
||||
rightOfLeft: box.left > map.right,
|
||||
border: getComputedStyle(help).borderLeftWidth,
|
||||
items: help.querySelectorAll("[data-help-logic]").length,
|
||||
first: help.querySelector(".is-on")?.dataset.helpLogic,
|
||||
sameHeight: Math.abs(box.height - body.clientHeight) <= 2,
|
||||
};
|
||||
modal.querySelector('[data-logic="weighted"]').click();
|
||||
await wait(300);
|
||||
const on = help.querySelectorAll(".is-on");
|
||||
const onBox = on[0].getBoundingClientRect();
|
||||
out.after = on[0].dataset.helpLogic;
|
||||
out.onCount = on.length;
|
||||
out.visible = onBox.top >= box.top - 1 && onBox.bottom <= box.bottom + 1;
|
||||
for (let i = 0; i < 300 && modal.querySelector('[data-logic="weighted"]').disabled; i += 1)
|
||||
await wait(100);
|
||||
modal.querySelector('[data-act="close"]').click();
|
||||
out.closed = !document.querySelector(".ui-modal.b05-routerec");
|
||||
out.pass =
|
||||
out.rightOfLeft &&
|
||||
out.border === "1px" &&
|
||||
out.items === 8 &&
|
||||
out.first === "follow" &&
|
||||
out.sameHeight &&
|
||||
out.after === "weighted" &&
|
||||
out.onCount === 1 &&
|
||||
out.visible &&
|
||||
out.closed;
|
||||
return JSON.stringify(out);
|
||||
})();
|
||||
Reference in New Issue
Block a user