Files
Aislo/B05_Profile/B05_Profile_UI_Panel_Fields.ts
T
eomsangdonandClaude Sonnet 5.5 2c520537b6 refactor(B05·B06): 좌측 칸을 등록부로 · 아래 단추 넷 · B05 Panel 나눔(24-2b)
- 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
2026-09-29 20:17:35 +09:00

150 lines
6.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =============================================================================
* 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;
}