Files
Aislo/B05_Profile/B05_Profile_UI_Panel_Settings.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

235 lines
11 KiB
TypeScript

/* =============================================================================
* B05_Profile_UI_Panel_Settings.ts
* 좌측 패널 「페이지 설정」 칸의 **재료** — 시작 측점 · 샘플 간격 · 등고선 간격 · 임도 종류 · 설계속도 ·
* 지형 구분 · 기준값. 칸 틀은 `B05_Profile_UI_Panel_Sections.ts` 가 씌운다(등록부).
* `B05_Profile_UI_Panel.ts` 에서 떼어냈다(계획서 24-2b). 본문 로직과 수치는 그대로다.
*
* 「페이지 설정」 — 등고선 간격·시작 측점 및 샘플링·종단 설계 기준 세 섹션 병합
* (2026-08-18 사용자 확정). 측점 간격 입력은 삭제 — 20m 고정.
* ========================================================================== */
import { createSelectField } from "@ui/ui_template_elements";
import {
L,
PROFILE_CRITERIA,
ROAD_TYPE_LABELS,
SPEED_CHOICES,
button,
checkbox,
numberField,
parseOptional,
type RoutePanelValues,
} from "./B05_Profile_UI_Panel_Fields";
export interface SettingsPartCallbacks {
onContourApply: (interval: number) => void | Promise<void>;
/** 이어 공사 시작 기준(시작 측점·누가거리 시작)이 바뀔 때. */
onStationDisplayChange: (offset: { station: number; cumulative: number }) => void;
}
export function createSettingsPart(callbacks: SettingsPartCallbacks) {
// 등고선 간격 행 — 「페이지 설정」 섹션에 들어간다(2026-08-18 컨테이너 병합).
const contourInterval = numberField("간격 (m), 최소 0.5m", "1");
const contourRow = document.createElement("div");
contourRow.className = "b05-route__contour-row";
// 등고선 재적용은 화면 전체를 잠그지 않는다(CLAUDE.md 5장) — 누른 버튼만 잠가
// 진행을 알리고, 나머지 조작은 그대로 열어 둔다.
const contourApply = button("재적용", () => {
contourApply.disabled = true;
contourApply.textContent = "적용 중…";
void Promise.resolve(callbacks.onContourApply(Number(contourInterval.value) || 1)).finally(
() => {
contourApply.disabled = false;
contourApply.textContent = "재적용";
},
);
});
contourRow.append(contourInterval.wrapper, contourApply);
// 임도 종류는 **프로젝트 등록(B02)에서 정한 값**을 그대로 쓴다 — 여기서 고치는
// 값이 아니다(2026-08-19 사용자 확정). 화면에는 읽기 전용으로 보여만 준다.
const gradeField = createSelectField({
label: "임도 종류 (프로젝트 등록값)",
options: [
{ value: "trunk", text: ROAD_TYPE_LABELS.trunk },
{ value: "fire", text: ROAD_TYPE_LABELS.fire },
{ value: "work", text: ROAD_TYPE_LABELS.work },
{ value: "branch", text: ROAD_TYPE_LABELS.branch },
],
});
const gradeClass = gradeField.select;
gradeClass.disabled = true;
gradeClass.title = "프로젝트 등록 정보의 임도 종류입니다. 변경은 대시보드에서 하세요.";
// 설계속도 — 임도는 속도를 낼 수 없어 기본 20km/h다. 종류에 따라 선택지가 갈린다.
const speedField = createSelectField({
label: "설계속도 (km/h)",
options: [20, 30, 40].map((value) => ({ value: String(value), text: `${value} km/h` })),
});
const designSpeed = speedField.select;
designSpeed.value = "20";
designSpeed.title =
"종단기울기·종단곡선의 법정 기준은 설계속도로 정해집니다(별표2).\n" +
"간선·산불진화임도는 20~40, 작업임도는 20만 쓸 수 있습니다.";
const crossSampleInterval = numberField(L("B05_Route_Field_CrossSample"));
const longSampleInterval = numberField(L("B05_Route_Field_LongSample"));
// 샘플 간격 두 칸은 2열 한 행 — 종단이 먼저(2026-08-18 사용자 지시).
const sampleRow = document.createElement("div");
sampleRow.className = "b05-route__field-row";
sampleRow.append(longSampleInterval.wrapper, crossSampleInterval.wrapper);
const terrainField = createSelectField({
label: "지형 구분",
options: [
{ value: "normal", text: "일반지형" },
{ value: "special", text: "특수지형" },
],
});
const terrainType = terrainField.select;
// 기본값 특수지형(2026-09-02 사용자 지시) — 임도 대상지는 대개 특수지형이라
// 매번 바꿔야 했다. 저장분이 있으면 `apply()`가 그 값으로 덮는다.
terrainType.value = "special";
// 역기울기(5%) 상한 방향은 서버가 지반 형상에서 자동 판정(main_direction="auto")하므로
// 수동 선택 UI는 두지 않는다. 노선 균형 구역 길이도 자동 산출 기본값(전체 1구역)에 맡긴다.
// 횡단배수 최소고 강제(2026-09-01 사용자 지시) — 기본 해제. 켜면 자동 계획선이 배관
// 자리를 시설 여유만큼 들어 올리고, 그 아래로 내리는 편집도 막힌다. 꺼도 부족분 경고는
// 종단 상단 표시줄에 계속 뜬다.
const enforcePipeClearance = checkbox("횡단배수 최소고 강제", false);
enforcePipeClearance.title =
"켜면 계획선이 배수관·BOX암거·세월교 자리에서 관경(구체 높이)+토피 0.5m 만큼 들립니다.\n" +
"끄면 들어 올리지 않고 부족분은 경고로만 알립니다(기본).";
const maxGradePct = numberField("최대 종단기울기 (%)");
const minVerticalRadius = numberField("종단곡선 최소 반경 (m)");
const minTangentLength = numberField("최소 직선 길이 (m)");
const startElevationOffset = numberField("시점 계획고 조정 (m)");
const endElevationOffset = numberField("종점 계획고 조정 (m)");
const criteriaNote = document.createElement("p");
criteriaNote.className = "b05-route__note";
const gradeAdvanced = document.createElement("details");
const gradeSummary = document.createElement("summary");
gradeSummary.textContent = "기준값 직접 지정";
gradeAdvanced.append(
gradeSummary,
maxGradePct.wrapper,
minVerticalRadius.wrapper,
minTangentLength.wrapper,
startElevationOffset.wrapper,
endElevationOffset.wrapper,
);
// 병합 구획 구분선 — 샘플링 / 등고선 / 종단 설계 기준.
const settingsDivider1 = document.createElement("hr");
settingsDivider1.className = "b05-structure__divider";
const settingsDivider2 = settingsDivider1.cloneNode() as HTMLHRElement;
// 공사 시작 기준 — 이전 공사에 이어 시공할 때 0측점을 임의 측점/누가거리로 시작 표기한다.
// (내부 chainage는 0기준 유지, 측점 라벨·누가거리 "표시"만 이 값만큼 이동.) 기본값 0/0.
// 별도 "공사 시작점" 컨테이너를 없애고 "시작 측점 및 샘플링 설정" 최상단으로 일원화
// (2026-08-06 사용자 지시).
const startStation = numberField("시작 측점", "0");
startStation.step = "1";
startStation.min = "0";
const startCumulative = numberField("시작 누가거리 (m)", "0");
// 2열×2행: 1행 항목명 · 2행 값 입력 / 1열 시작 측점 · 2열 시작 누가거리.
const startRow = document.createElement("div");
startRow.className = "b05-route__field-row";
startRow.append(startStation.wrapper, startCumulative.wrapper);
const stationDisplayOffset = (): { station: number; cumulative: number } => ({
station: Math.max(0, Math.round(Number(startStation.value) || 0)),
cumulative: Number(startCumulative.value) || 0,
});
[startStation, startCumulative].forEach((input) =>
input.addEventListener("change", () =>
callbacks.onStationDisplayChange(stationDisplayOffset()),
),
);
/** 지금 적용되는 종단기울기 상한(%) — 사용자가 값을 넣었으면 그 값, 비웠으면
* 등급·지형의 법정 기준. 종단 그래프의 위반 판정이 이 값을 즉시 따라온다
* (2026-08-19 사용자 지시 6). 특수지형+포장 예외(18%)는 서버가 최종 판정한다. */
function currentSpeed(): RoutePanelValues["designSpeed"] {
const value = Number(designSpeed.value);
return (value === 40 || value === 30 ? value : 20) as RoutePanelValues["designSpeed"];
}
function gradeLimitPct(): number {
const criteria = PROFILE_CRITERIA[currentSpeed()];
const terrain = terrainType.value as RoutePanelValues["terrainType"];
const typed = parseOptional(maxGradePct);
return typed != null && typed > 0 ? typed : criteria.grade[terrain];
}
/** 임도 종류에 맞춰 설계속도 선택지를 갈아 끼운다 — 작업임도는 20만 쓸 수 있다. */
function syncSpeedChoices(): void {
const allowed = SPEED_CHOICES[gradeClass.value] ?? [20];
const keep = allowed.includes(currentSpeed()) ? currentSpeed() : allowed[0];
designSpeed.replaceChildren(
...allowed.map((value) => new Option(`${value} km/h`, String(value))),
);
designSpeed.value = String(keep);
// 고를 것이 하나뿐이면 잠근다 — 열어 두면 바꿀 수 있다는 오해를 준다.
designSpeed.disabled = allowed.length < 2;
}
/** 설계속도·지형 선택에 맞춰 법정 기준값을 placeholder와 안내문에 반영한다. */
function syncCriteria(): void {
const speed = currentSpeed();
const criteria = PROFILE_CRITERIA[speed];
const terrain = terrainType.value as RoutePanelValues["terrainType"];
maxGradePct.placeholder = String(criteria.grade[terrain]);
minVerticalRadius.placeholder = String(criteria.radius);
minTangentLength.placeholder = "20";
const roadType = ROAD_TYPE_LABELS[gradeClass.value] ?? gradeClass.value;
criteriaNote.textContent =
`${roadType} · 설계속도 ${speed}km/h · ${terrain === "special" ? "특수지형" : "일반지형"} 기준 — ` +
`종단기울기 ${criteria.grade[terrain]}% 이하, 종단곡선 반경 ${criteria.radius}m 이상. ` +
"비워두면 이 기준이 적용됩니다.";
}
// 설계속도·지형뿐 아니라 기울기 세부 입력까지 — 바꾸는 즉시 안내문과 위반 기준이
// 따라온다(2026-08-19 사용자 지시 6). onInputChange가 그래프 쪽 갱신을 잇는다.
[designSpeed, terrainType, maxGradePct].forEach((input) =>
input.addEventListener("change", syncCriteria),
);
maxGradePct.addEventListener("input", syncCriteria);
syncSpeedChoices();
syncCriteria();
return {
/** 「페이지 설정」 본문 — 화면 순서대로. */
nodes: [
startRow,
sampleRow,
settingsDivider1,
contourRow,
settingsDivider2,
// 임도 종류(등록값) → 설계속도 → 지형 구분 순 — 법정 기준이 정해지는 순서다
// (2026-08-19 사용자 확정: 종류는 B02에서, 속도·지형은 여기서).
gradeField.root,
speedField.root,
terrainField.root,
criteriaNote,
enforcePipeClearance.wrapper,
gradeAdvanced,
] as HTMLElement[],
fields: {
contourInterval,
gradeClass,
designSpeed,
terrainType,
crossSampleInterval,
longSampleInterval,
maxGradePct,
minVerticalRadius,
minTangentLength,
startElevationOffset,
endElevationOffset,
enforcePipeClearance,
},
stationDisplayOffset,
gradeLimitPct,
currentSpeed,
syncSpeedChoices,
syncCriteria,
};
}
export type SettingsPart = ReturnType<typeof createSettingsPart>;