feat(B05): C군 사면안정 5종 범위 입력 개편 — 기준 측점+길이 계산 표시
- 레지스트리: C군 5종(옹벽·돌쌓기 찰/메·흙막이·큰돌쌓기)에 길이 옵션 신설, 높이와 함께 B05 필수 입력(phase b05)으로 전환. 순서 = 높이 → 길이. - 패널: 구간 직접 입력(시작·종료) 삭제, 기준 측점 위 '측점 범위' 표시 행 신설 — 기준+길이로 "시작 ~ 종료" 실시간 계산, 저장 시 시작=기준·종료=기준+길이. - 구간 직입 시절 저장분 호환: 폼에 열면 길이를 시작~종료에서 자동 역산. - 검증: pytest 111 통과(tmp/tests 정책·저장소 테스트 갱신), tsc, 헤드 브라우저 저장 왕복(60~90·len=30·h=2.5) 확인. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -512,15 +512,6 @@
|
||||
},
|
||||
"drawing_views": ["profile", "cross_section", "detail", "quantity"],
|
||||
"options": [
|
||||
{
|
||||
"key": "form",
|
||||
"label": "형식",
|
||||
"input": "select",
|
||||
"choices": ["중력식", "반중력식", "캔틸레버식", "부벽식"],
|
||||
"default": null,
|
||||
"required": true,
|
||||
"phase": "detail"
|
||||
},
|
||||
{
|
||||
"key": "height_m",
|
||||
"label": "높이",
|
||||
@@ -528,6 +519,24 @@
|
||||
"unit": "m",
|
||||
"default": null,
|
||||
"required": true,
|
||||
"phase": "b05"
|
||||
},
|
||||
{
|
||||
"key": "length_m",
|
||||
"label": "길이",
|
||||
"input": "number",
|
||||
"unit": "m",
|
||||
"default": null,
|
||||
"required": true,
|
||||
"phase": "b05"
|
||||
},
|
||||
{
|
||||
"key": "form",
|
||||
"label": "형식",
|
||||
"input": "select",
|
||||
"choices": ["중력식", "반중력식", "캔틸레버식", "부벽식"],
|
||||
"default": null,
|
||||
"required": true,
|
||||
"phase": "detail"
|
||||
}
|
||||
]
|
||||
@@ -550,7 +559,16 @@
|
||||
"unit": "m",
|
||||
"default": null,
|
||||
"required": true,
|
||||
"phase": "detail"
|
||||
"phase": "b05"
|
||||
},
|
||||
{
|
||||
"key": "length_m",
|
||||
"label": "길이",
|
||||
"input": "number",
|
||||
"unit": "m",
|
||||
"default": null,
|
||||
"required": true,
|
||||
"phase": "b05"
|
||||
},
|
||||
{
|
||||
"key": "back_len_cm",
|
||||
@@ -581,7 +599,16 @@
|
||||
"unit": "m",
|
||||
"default": null,
|
||||
"required": true,
|
||||
"phase": "detail"
|
||||
"phase": "b05"
|
||||
},
|
||||
{
|
||||
"key": "length_m",
|
||||
"label": "길이",
|
||||
"input": "number",
|
||||
"unit": "m",
|
||||
"default": null,
|
||||
"required": true,
|
||||
"phase": "b05"
|
||||
},
|
||||
{
|
||||
"key": "back_len_cm",
|
||||
@@ -605,15 +632,6 @@
|
||||
},
|
||||
"drawing_views": ["profile", "cross_section", "quantity"],
|
||||
"options": [
|
||||
{
|
||||
"key": "form",
|
||||
"label": "형식",
|
||||
"input": "select",
|
||||
"choices": ["콘크리트", "돌(찰)", "돌(메)", "블록쌓기", "돌망태", "흙포대", "통나무쌓기"],
|
||||
"default": null,
|
||||
"required": true,
|
||||
"phase": "detail"
|
||||
},
|
||||
{
|
||||
"key": "height_m",
|
||||
"label": "높이",
|
||||
@@ -621,6 +639,24 @@
|
||||
"unit": "m",
|
||||
"default": null,
|
||||
"required": true,
|
||||
"phase": "b05"
|
||||
},
|
||||
{
|
||||
"key": "length_m",
|
||||
"label": "길이",
|
||||
"input": "number",
|
||||
"unit": "m",
|
||||
"default": null,
|
||||
"required": true,
|
||||
"phase": "b05"
|
||||
},
|
||||
{
|
||||
"key": "form",
|
||||
"label": "형식",
|
||||
"input": "select",
|
||||
"choices": ["콘크리트", "돌(찰)", "돌(메)", "블록쌓기", "돌망태", "흙포대", "통나무쌓기"],
|
||||
"default": null,
|
||||
"required": true,
|
||||
"phase": "detail"
|
||||
}
|
||||
]
|
||||
@@ -643,7 +679,16 @@
|
||||
"unit": "m",
|
||||
"default": null,
|
||||
"required": true,
|
||||
"phase": "detail"
|
||||
"phase": "b05"
|
||||
},
|
||||
{
|
||||
"key": "length_m",
|
||||
"label": "길이",
|
||||
"input": "number",
|
||||
"unit": "m",
|
||||
"default": null,
|
||||
"required": true,
|
||||
"phase": "b05"
|
||||
},
|
||||
{
|
||||
"key": "stone_cm",
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
} from "./B05_Profile_UI_Drainage_Facility";
|
||||
import { field, numberInput, select, stationFields } from "./B05_Profile_UI_Structures_Fields";
|
||||
import { renderStructureList } from "./B05_Profile_UI_Structures_List";
|
||||
import { formatStation } from "./B05_Profile_Util_Station";
|
||||
|
||||
/** 구조물군 표시 이름. 리스트 기호(A~G)만으로는 무엇인지 알기 어렵다.
|
||||
* 우클릭 메뉴(종단그래프·배수유역도)도 같은 이름을 쓴다(2026-08-18 일원화). */
|
||||
@@ -128,10 +129,18 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu
|
||||
memoField.type = "text";
|
||||
memoField.placeholder = "메모(선택)";
|
||||
|
||||
// 측점 범위(계산 표시) — C군 사면안정처럼 길이 옵션으로 구간이 정해지는 타입은
|
||||
// 시작·종료를 직접 입력하지 않고 기준 측점 + 길이로 계산해 보여만 준다
|
||||
// (2026-08-19 사용자 지시). 표기는 하단 구조물 목록과 같은 시작 ~ 종료 측점.
|
||||
const rangeValue = document.createElement("span");
|
||||
rangeValue.className = "b05-structure__range-value";
|
||||
const rangeWrap = field("측점 범위", rangeValue);
|
||||
|
||||
// 측점은 3행 — 한 행이 [라벨][측점번호][잔여거리](2026-08-17 사용자 지시 2).
|
||||
// 범위 계산 타입은 [측점 범위][기준 측점]만 보인다(범위 행이 기준 측점 위).
|
||||
const positionRow = document.createElement("div");
|
||||
positionRow.className = "b05-structure__position-row";
|
||||
positionRow.append(startFields.wrap, anchorFields.wrap, endFields.wrap);
|
||||
positionRow.append(rangeWrap, startFields.wrap, anchorFields.wrap, endFields.wrap);
|
||||
|
||||
// 기본 인터페이스는 2열 격자(2026-08-17 사용자 지시 1).
|
||||
const typeRow = document.createElement("div");
|
||||
@@ -157,6 +166,40 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu
|
||||
return type.type_id === "revetment" ? "revetment" : null;
|
||||
}
|
||||
|
||||
/** 구간을 직접 입력하지 않는 타입인가 — C군 사면안정 5종처럼 B05 길이 옵션이
|
||||
* 있으면 시작·종료 입력 칸을 없애고 기준 측점 + 길이로 범위를 계산해 보여만
|
||||
* 준다(2026-08-19 사용자 지시). */
|
||||
function hasComputedRange(type: StructureType | null): boolean {
|
||||
return (
|
||||
!!type &&
|
||||
!type.managed_by &&
|
||||
type.placement === "interval" &&
|
||||
type.options.some((option) => option.key === "length_m" && isB05Option(option))
|
||||
);
|
||||
}
|
||||
|
||||
/** 길이 옵션(length_m) 값(m). 비었거나 0 이하면 null — 범위를 계산할 수 없다. */
|
||||
function readLengthM(): number | null {
|
||||
const entry = optionInputs.find((input) => input.key === "length_m");
|
||||
if (!entry || entry.isEmpty()) return null;
|
||||
const length = Number(entry.read());
|
||||
return Number.isFinite(length) && length > 0 ? length : null;
|
||||
}
|
||||
|
||||
/** 측점 범위 표시 갱신 — 기준 측점 + 길이가 다 있으면 "시작 ~ 종료", 아니면 "—". */
|
||||
function syncRangeDisplay(): void {
|
||||
const computed = hasComputedRange(currentType());
|
||||
rangeWrap.hidden = !computed;
|
||||
if (!computed) return;
|
||||
const step = interval();
|
||||
const anchor = anchorFields.read(step, false);
|
||||
const length = readLengthM();
|
||||
rangeValue.textContent =
|
||||
anchor !== null && length !== null
|
||||
? `${formatStation(anchor, step)} ~ ${formatStation(anchor + length, step)}`
|
||||
: "—";
|
||||
}
|
||||
|
||||
const primary = document.createElement("button");
|
||||
primary.type = "button";
|
||||
primary.className = "b05-route__irregular-btn is-primary";
|
||||
@@ -223,11 +266,13 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu
|
||||
|
||||
/** 배치형태·관리 주체에 맞춰 위치 칸을 바꾼다. 계곡 통과 시설(배수관 등)은 점형이라
|
||||
* 기준 측점 하나만 받는다(2026-08-17 사용자 지시 2 — 시작·종료 불필요).
|
||||
* 범위 계산 타입(C군 등)도 기준 측점만 받고 범위는 표시 행이 대신한다(2026-08-19).
|
||||
* 종류 미선택(빈값)이면 입력 칸 전체를 숨긴다 — 리셋 직후 상태. */
|
||||
function syncPlacementFields(): void {
|
||||
const type = currentType();
|
||||
const managed = !!type?.managed_by;
|
||||
const isInterval = !!type && !managed && type.placement === "interval";
|
||||
const isInterval =
|
||||
!!type && !managed && type.placement === "interval" && !hasComputedRange(type);
|
||||
positionRow.hidden = !type;
|
||||
positionDivider.hidden = !type;
|
||||
startFields.wrap.hidden = !isInterval;
|
||||
@@ -236,6 +281,7 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu
|
||||
anchorFields.wrap.querySelector("span")!.textContent = isInterval
|
||||
? "기준 측점 (비우면 시작)"
|
||||
: "기준 측점";
|
||||
syncRangeDisplay();
|
||||
}
|
||||
|
||||
/** 종류에 맞춰 부속 옵션 서브폼을 켠다(계곡 통과 시설·독립 기슭막이, 값은 인자로). */
|
||||
@@ -247,12 +293,13 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu
|
||||
syncOptionLock();
|
||||
}
|
||||
|
||||
/** 위치(측점)가 채워졌는지 — 상세 옵션을 여는 조건(2026-08-18 입력 순서 가이드). */
|
||||
/** 위치(측점)가 채워졌는지 — 상세 옵션을 여는 조건(2026-08-18 입력 순서 가이드).
|
||||
* 범위 계산 타입은 기준 측점 하나가 곧 위치다(2026-08-19). */
|
||||
function hasPosition(): boolean {
|
||||
const type = currentType();
|
||||
if (!type) return false;
|
||||
const step = interval();
|
||||
if (!type.managed_by && type.placement === "interval") {
|
||||
if (!type.managed_by && type.placement === "interval" && !hasComputedRange(type)) {
|
||||
return startFields.read(step, false) !== null && endFields.read(step, false) !== null;
|
||||
}
|
||||
return anchorFields.read(step, false) !== null;
|
||||
@@ -298,6 +345,7 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu
|
||||
function handleStationInput(): void {
|
||||
if (!hasPosition()) positionConfirmed = false;
|
||||
syncOptionLock();
|
||||
syncRangeDisplay();
|
||||
}
|
||||
|
||||
/** 위치 입력 확정 — 측점번호+잔여거리를 다 받고 입력군을 빠져나온 순간.
|
||||
@@ -306,6 +354,7 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu
|
||||
positionConfirmed = hasPosition();
|
||||
commitTempPipe();
|
||||
syncOptionLock();
|
||||
syncRangeDisplay();
|
||||
}
|
||||
|
||||
/** 타입의 옵션 스키마대로 입력 칸을 다시 그린다 — 상세(detail) 옵션은 B06/B07
|
||||
@@ -349,8 +398,14 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu
|
||||
read: () => (option.input === "number" ? Number(input.value) || 0 : input.value),
|
||||
isEmpty: () => input.value.trim() === "",
|
||||
});
|
||||
// 길이가 바뀌면 측점 범위 표시가 즉시 따라온다(범위 계산 타입, 2026-08-19).
|
||||
if (option.key === "length_m") {
|
||||
input.addEventListener("input", syncRangeDisplay);
|
||||
input.addEventListener("change", syncRangeDisplay);
|
||||
}
|
||||
});
|
||||
syncOptionLock();
|
||||
syncRangeDisplay();
|
||||
}
|
||||
|
||||
/** 고른 구조물군의 타입을 종류 목록에 채운다 — A군은 계곡 통과 시설(managed_by)도
|
||||
@@ -450,6 +505,14 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu
|
||||
endFields.write(target.end_m ?? null, step);
|
||||
memoField.value = target.memo ?? "";
|
||||
renderOptionFields(target.options);
|
||||
// 구간 직접 입력 시절에 저장된 항목 호환 — 길이 옵션이 비어 있으면 저장된
|
||||
// 시작~종료 구간 길이로 채워 범위 표시·재저장이 이어지게 한다(2026-08-19).
|
||||
if (target.start_m != null && target.end_m != null && target.end_m > target.start_m) {
|
||||
const lengthEntry = optionInputs.find((entry) => entry.key === "length_m");
|
||||
if (lengthEntry && lengthEntry.isEmpty()) {
|
||||
lengthEntry.input.value = (target.end_m - target.start_m).toFixed(1);
|
||||
}
|
||||
}
|
||||
syncFacilityForm(target.options);
|
||||
} else {
|
||||
anchorFields.write(null, step);
|
||||
@@ -560,7 +623,21 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu
|
||||
let anchor: number | null;
|
||||
let start: number | null = null;
|
||||
let end: number | null = null;
|
||||
if (placement === "interval") {
|
||||
if (placement === "interval" && hasComputedRange(type)) {
|
||||
// 범위 계산 타입(C군 등) — 시작 = 기준 측점, 종료 = 기준 + 길이(2026-08-19).
|
||||
anchor = anchorFields.read(step, true);
|
||||
if (anchor === null) {
|
||||
anchorFields.station.focus();
|
||||
return;
|
||||
}
|
||||
const length = readLengthM();
|
||||
if (length === null) {
|
||||
optionInputs.find((entry) => entry.key === "length_m")?.input.focus();
|
||||
return;
|
||||
}
|
||||
start = anchor;
|
||||
end = anchor + length;
|
||||
} else if (placement === "interval") {
|
||||
start = startFields.read(step, true);
|
||||
end = endFields.read(step, true);
|
||||
if (start === null || end === null || end <= start) {
|
||||
@@ -792,7 +869,8 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu
|
||||
positionConfirmed = true; // 우클릭은 측점이 확정돼 들어온다 — 옵션도 바로 연다.
|
||||
const placement = placementOf(typeId);
|
||||
const step = interval();
|
||||
const isInterval = !type.managed_by && placement === "interval";
|
||||
// 범위 계산 타입은 시작·종료를 쓰지 않는다 — 길이 옵션이 정한다(2026-08-19).
|
||||
const isInterval = !type.managed_by && placement === "interval" && !hasComputedRange(type);
|
||||
anchorFields.write(chainageM, step);
|
||||
startFields.write(isInterval ? chainageM : null, step);
|
||||
endFields.write(isInterval ? chainageM + DEFAULT_INTERVAL_LENGTH_M : null, step);
|
||||
|
||||
@@ -222,6 +222,14 @@
|
||||
background: rgba(224, 91, 91, 0.08);
|
||||
}
|
||||
|
||||
/* 측점 범위(계산 표시) — C군 등 길이 옵션 기반 구간 타입. 입력 칸이 아니라 기준
|
||||
* 측점 + 길이로 계산된 "시작 ~ 종료"를 보여만 준다(2026-08-19 사용자 지시). */
|
||||
.b05-structure__position-row > .b05-route__field > .b05-structure__range-value {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 측점번호 + 잔여거리 두 칸 입력(2026-08-17 통합 — 구 비정규 측점 UI와 같은 방식). */
|
||||
.b05-structure__station-pair {
|
||||
display: flex;
|
||||
|
||||
Reference in New Issue
Block a user