- B05 좌측 패널(714줄)을 Fields · Settings · Route · View · Sections 로 나누고 createRoutePanel 은 등록부로 세움 - B06 좌측 칸(횡단 보기 설정 · 표준 횡단면 설정)을 등록부로 · 페이지는 createDesignPanel 만 부름 - 「구조물 배치」는 design_sections 한 정의로 두 보기 - 아래 단추 [초기화][저장][확정][보기 전환] — B05 [확정]은 저장 흐름 이음 전까지 꺼 둠 - [초기화] 는 design_reset 한 함수를 두 보기가 부름 - 칸 DOM 은 고치기 전과 같음(전후 DOM 대조) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EauA2ATmqNRbeV48s9CcBr
150 lines
6.4 KiB
TypeScript
150 lines
6.4 KiB
TypeScript
/* =============================================================================
|
||
* B05_Profile_UI_Panel_Fields.ts
|
||
* 좌측 패널이 함께 쓰는 **값 · 상수 · 입력 칸 만들기** — `B05_Profile_UI_Panel.ts` 가 714줄이라
|
||
* 떼어낸 조각이다(계획서 24-2b). 본문 로직과 수치는 그대로다.
|
||
* ========================================================================== */
|
||
|
||
import { type ButtonVariant, createButton } from "@ui/ui_template_elements";
|
||
import { currentLanguageIndex, ui_locales } from "@ui/ui_template_locale";
|
||
|
||
export interface RoutePanelValues {
|
||
contourInterval: number;
|
||
algorithm: "dijkstra" | "ridge_valley";
|
||
/** 임도 종류 — 현행 3종(간선·산불진화·작업). branch(지선)는 폐지됐으나 기존
|
||
* 저장분 호환을 위해 값으로는 받는다(2026-08-19). 값의 원천은 프로젝트 등록
|
||
* 정보(projects.road_type)이며 화면에서는 고르지 않는다. */
|
||
gradeClass: "trunk" | "fire" | "branch" | "work";
|
||
/** 설계속도(km/h) — 임도는 속도를 낼 수 없는 노선이라 기본 20이다. 간선·산불진화만
|
||
* 20~40에서 고를 수 있고 작업임도는 20 고정(별표2, 2026-08-19 사용자 확정). */
|
||
designSpeed: 20 | 30 | 40;
|
||
paved: boolean;
|
||
minCurveRadius: number | null;
|
||
maxUphillGrade: number | null;
|
||
maxDownhillGrade: number | null;
|
||
minUphillGrade: number | null;
|
||
minDownhillGrade: number | null;
|
||
allowAvoidPassThrough: boolean;
|
||
stationInterval: number | null;
|
||
crossSampleInterval: number | null;
|
||
longSampleInterval: number | null;
|
||
terrainType: "normal" | "special";
|
||
maxGradePct: number | null;
|
||
minVerticalRadius: number | null;
|
||
minTangentLength: number | null;
|
||
startElevationOffset: number | null;
|
||
endElevationOffset: number | null;
|
||
/** 횡단배수 최소 계획고를 계획선·편집에 강제할지 — 기본 해제(2026-09-01 사용자 지시).
|
||
* 해제해도 부족분 경고는 그대로 뜬다. 강제만 풀고 판단은 사용자에게 맡긴다. */
|
||
enforcePipeClearance: boolean;
|
||
}
|
||
|
||
/**
|
||
* 「임도설치 및 관리 등에 관한 규정」[별표 1-2] 기준값.
|
||
* 등급을 바꾸면 이 값이 계획선 폼의 placeholder(미입력 시 서버 기본값)로 반영된다.
|
||
* 실제 기본값 결정은 서버 config가 단일 소스이며 여기서는 안내만 한다.
|
||
*/
|
||
/** 측점 간격(m) — 20m 고정(2026-08-18 사용자 확정, 입력 UI 삭제). */
|
||
export const STATION_INTERVAL_M = 20;
|
||
|
||
/* 종단기울기·종단곡선 반경 — 지식DB `설계제원_총괄 §6·§7`(간선·산불진화 공통,
|
||
* 설계속도 40/30/20 → 7·10 / 8·12 / 9·14 %, 반경 450/250/100 m) 값과 일치한다
|
||
* (2026-08-19 재점검). 서버 `legal_grade_limit_pct`가 같은 표를 쓰므로 화면 안내와
|
||
* 실제 적용값이 어긋나지 않는다.
|
||
*
|
||
* ⚠ 미해결(사용자 협의 필요, 2026-08-19 재점검에서 확인):
|
||
* ① 등급 선택지가 간선·지선·작업인데 현행 규칙은 **지선 폐지**, 3종은 간선·
|
||
* 산불진화·작업이다(설계제원_총괄 §1 [구현] 지침).
|
||
* ② 지식DB의 40/30/20 표는 **설계속도**별이고 간선·산불진화 공통이다. 지금은
|
||
* 등급에 속도를 고정 매핑(trunk=40·branch=30·work=20)했는데, 작업임도는
|
||
* 설계속도 20 이하 + 종단기울기 최대 20% 범위 조정이라는 별도 규정이다.
|
||
* ③ "최소 곡선반경" 입력 칸의 안내값이 **종단**곡선 반경(450/250/100)이다.
|
||
* 평면 최소곡선반지름은 일반지형 60/30/15 (설계제원_총괄 §4).
|
||
* 값을 임의로 바꾸지 않고 남긴다 — 기본값 확정은 사용자 협의 영역이다. */
|
||
export const PROFILE_CRITERIA: Record<
|
||
RoutePanelValues["designSpeed"],
|
||
{ grade: Record<RoutePanelValues["terrainType"], number>; radius: number }
|
||
> = {
|
||
40: { grade: { normal: 7, special: 10 }, radius: 450 },
|
||
30: { grade: { normal: 8, special: 12 }, radius: 250 },
|
||
20: { grade: { normal: 9, special: 14 }, radius: 100 },
|
||
};
|
||
|
||
/** 임도 종류별 표시 이름 — 프로젝트 등록(B02)과 같은 말을 쓴다(2026-08-19 확정). */
|
||
export const ROAD_TYPE_LABELS: Record<string, string> = {
|
||
trunk: "간선임도",
|
||
fire: "산불진화임도",
|
||
work: "작업임도",
|
||
branch: "작업임도(구 지선)",
|
||
};
|
||
|
||
/** 종류별로 고를 수 있는 설계속도 — 작업임도는 20 이하라 20만 둔다(별표2 Ⅰ.3). */
|
||
export const SPEED_CHOICES: Record<string, Array<RoutePanelValues["designSpeed"]>> = {
|
||
trunk: [20, 30, 40],
|
||
fire: [20, 30, 40],
|
||
work: [20],
|
||
branch: [20],
|
||
};
|
||
|
||
export type WrappedInput = HTMLInputElement & { wrapper: HTMLLabelElement };
|
||
|
||
export function L(key: keyof typeof ui_locales): string {
|
||
return ui_locales[key][currentLanguageIndex];
|
||
}
|
||
|
||
export function button(
|
||
label: string,
|
||
onClick: () => void,
|
||
variant: ButtonVariant = "ghost",
|
||
): HTMLButtonElement {
|
||
return createButton({ label, variant, onClick: () => onClick() });
|
||
}
|
||
|
||
export function numberField(label: string, value = ""): WrappedInput {
|
||
const wrapper = document.createElement("label");
|
||
wrapper.className = "b05-route__field";
|
||
const caption = document.createElement("span");
|
||
caption.textContent = label;
|
||
const input = document.createElement("input");
|
||
input.type = "number";
|
||
input.step = "0.01";
|
||
input.value = value;
|
||
wrapper.append(caption, input);
|
||
return Object.assign(input, { wrapper });
|
||
}
|
||
|
||
export function checkbox(label: string, checked: boolean): WrappedInput {
|
||
const wrapper = document.createElement("label");
|
||
wrapper.className = "b05-route__check";
|
||
const input = document.createElement("input");
|
||
input.type = "checkbox";
|
||
input.checked = checked;
|
||
wrapper.append(input, document.createTextNode(label));
|
||
return Object.assign(input, { wrapper });
|
||
}
|
||
|
||
export function toggleButton(
|
||
label: string,
|
||
checked: boolean,
|
||
onChange: (checked: boolean) => void,
|
||
): HTMLButtonElement {
|
||
const element = button(
|
||
label,
|
||
() => {
|
||
const active = !element.classList.contains("is-active");
|
||
element.classList.toggle("is-active", active);
|
||
element.setAttribute("aria-pressed", String(active));
|
||
onChange(active);
|
||
},
|
||
"glass",
|
||
);
|
||
element.classList.toggle("is-active", checked);
|
||
element.setAttribute("aria-pressed", String(checked));
|
||
return element;
|
||
}
|
||
|
||
export function parseOptional(input: HTMLInputElement): number | null {
|
||
if (!input.value.trim()) return null;
|
||
const value = Number(input.value);
|
||
return Number.isFinite(value) ? value : null;
|
||
}
|