feat(B05): C군 전길이 분할·옹벽 명칭 단순화·목록 중복 이름 번호
- C군 5종에 전길이(before_m, 기본 0) 옵션 신설 — 시작 = 기준 − 전길이, 후길이 = 길이 − 전길이 자동. 종단 마크 = 기준점, 띠 = 시작~종료(기존 렌더 유지). 전길이 > 기준(시작 음수)은 칸 붉힘으로 거절. 구 저장분은 폼 로드 시 역산 채움. - 옹벽(철근콘크리트) → "옹벽" (전역 표기는 레지스트리 name 단일 원천). - 좌측 구조물 목록: 같은 이름 여럿이면 노선 시점 가까운 순 "이름 1/2…" 번호 (numberDuplicateNames 공용 헬퍼 — B06 목록 신설 시 재사용, 2026-08-19 사용자 지시). - 검증: pytest 112 통과, tsc, 헤드 브라우저(CDP 캐시 비움 방식) — 옵션 순서 높이→길이→전길이, 전5 범위 2+15.0~3+5.0, 목록 "옹벽 1/2", 원복 확인. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -504,7 +504,7 @@
|
||||
{
|
||||
"type_id": "retaining_wall",
|
||||
"group": "C",
|
||||
"name": "옹벽(철근콘크리트)",
|
||||
"name": "옹벽",
|
||||
"placement": "interval",
|
||||
"style": {
|
||||
"color": "#e07a2e",
|
||||
@@ -530,6 +530,15 @@
|
||||
"required": true,
|
||||
"phase": "b05"
|
||||
},
|
||||
{
|
||||
"key": "before_m",
|
||||
"label": "전길이",
|
||||
"input": "number",
|
||||
"unit": "m",
|
||||
"default": 0,
|
||||
"required": false,
|
||||
"phase": "b05"
|
||||
},
|
||||
{
|
||||
"key": "form",
|
||||
"label": "형식",
|
||||
@@ -570,6 +579,15 @@
|
||||
"required": true,
|
||||
"phase": "b05"
|
||||
},
|
||||
{
|
||||
"key": "before_m",
|
||||
"label": "전길이",
|
||||
"input": "number",
|
||||
"unit": "m",
|
||||
"default": 0,
|
||||
"required": false,
|
||||
"phase": "b05"
|
||||
},
|
||||
{
|
||||
"key": "back_len_cm",
|
||||
"label": "뒷길이",
|
||||
@@ -610,6 +628,15 @@
|
||||
"required": true,
|
||||
"phase": "b05"
|
||||
},
|
||||
{
|
||||
"key": "before_m",
|
||||
"label": "전길이",
|
||||
"input": "number",
|
||||
"unit": "m",
|
||||
"default": 0,
|
||||
"required": false,
|
||||
"phase": "b05"
|
||||
},
|
||||
{
|
||||
"key": "back_len_cm",
|
||||
"label": "뒷길이",
|
||||
@@ -650,6 +677,15 @@
|
||||
"required": true,
|
||||
"phase": "b05"
|
||||
},
|
||||
{
|
||||
"key": "before_m",
|
||||
"label": "전길이",
|
||||
"input": "number",
|
||||
"unit": "m",
|
||||
"default": 0,
|
||||
"required": false,
|
||||
"phase": "b05"
|
||||
},
|
||||
{
|
||||
"key": "form",
|
||||
"label": "형식",
|
||||
@@ -690,6 +726,15 @@
|
||||
"required": true,
|
||||
"phase": "b05"
|
||||
},
|
||||
{
|
||||
"key": "before_m",
|
||||
"label": "전길이",
|
||||
"input": "number",
|
||||
"unit": "m",
|
||||
"default": 0,
|
||||
"required": false,
|
||||
"phase": "b05"
|
||||
},
|
||||
{
|
||||
"key": "stone_cm",
|
||||
"label": "돌규격",
|
||||
|
||||
@@ -40,6 +40,21 @@ export function stationOfStructure(structure: StructureInstance, intervalM: numb
|
||||
: formatStation(structure.chainage_m ?? 0, intervalM);
|
||||
}
|
||||
|
||||
/** 중복 이름 번호 규칙 — 같은 이름이 여럿이면 노선 시점에 가까운 순서로 "이름 1",
|
||||
* "이름 2"…를 붙인다(2026-08-19 사용자 지시, B05·B06 공통 규칙). 입력은 시점
|
||||
* 오름차순으로 정렬된 이름 목록이어야 한다. 하나뿐인 이름은 그대로 둔다. */
|
||||
export function numberDuplicateNames(names: ReadonlyArray<string>): string[] {
|
||||
const totals = new Map<string, number>();
|
||||
names.forEach((name) => totals.set(name, (totals.get(name) ?? 0) + 1));
|
||||
const seen = new Map<string, number>();
|
||||
return names.map((name) => {
|
||||
if ((totals.get(name) ?? 0) < 2) return name;
|
||||
const seq = (seen.get(name) ?? 0) + 1;
|
||||
seen.set(name, seq);
|
||||
return `${name} ${seq}`;
|
||||
});
|
||||
}
|
||||
|
||||
export function renderStructureList(params: StructureListParams): void {
|
||||
const { list, typeMap, intervalM, editingId, selectedPipeChainage } = params;
|
||||
list.replaceChildren();
|
||||
@@ -68,17 +83,25 @@ export function renderStructureList(params: StructureListParams): void {
|
||||
return;
|
||||
}
|
||||
// 한 항목 = [측점(좌측 맞춤)][구조물 이름(우측 맞춤)] — 메모·연동 표기 등 그 외
|
||||
// 정보는 적지 않는다(2026-08-18 사용자 지시).
|
||||
rows.forEach((row) => {
|
||||
// 정보는 적지 않는다(2026-08-18 사용자 지시). 같은 이름이 여럿이면 시점에 가까운
|
||||
// 순서로 번호를 붙인다(2026-08-19 사용자 지시).
|
||||
const displayNames = numberDuplicateNames(
|
||||
rows.map((row) =>
|
||||
row.kind === "structure"
|
||||
? (typeMap.get(row.structure.type_id)?.name ?? row.structure.type_id)
|
||||
: (typeMap.get(row.pipe.facility)?.name ?? row.pipe.facility),
|
||||
),
|
||||
);
|
||||
rows.forEach((row, index) => {
|
||||
const item = document.createElement("li");
|
||||
item.className = "b05-route__irregular-item";
|
||||
const station = document.createElement("strong");
|
||||
const name = document.createElement("span");
|
||||
name.textContent = displayNames[index];
|
||||
if (row.kind === "structure") {
|
||||
const { structure } = row;
|
||||
item.classList.toggle("is-selected", structure.structure_id === editingId);
|
||||
station.textContent = stationOfStructure(structure, intervalM);
|
||||
name.textContent = typeMap.get(structure.type_id)?.name ?? structure.type_id;
|
||||
item.addEventListener("click", () => params.onSelectStructure(structure));
|
||||
if (structure.structure_id === editingId) item.scrollIntoView({ block: "nearest" });
|
||||
} else {
|
||||
@@ -88,7 +111,6 @@ export function renderStructureList(params: StructureListParams): void {
|
||||
selectedPipeChainage !== null && Math.abs(selectedPipeChainage - pipe.chainage_m) < 0.05,
|
||||
);
|
||||
station.textContent = formatStation(pipe.chainage_m, intervalM);
|
||||
name.textContent = typeMap.get(pipe.facility)?.name ?? pipe.facility;
|
||||
item.addEventListener("click", () => params.onSelectPipe(pipe));
|
||||
}
|
||||
item.append(station, name);
|
||||
|
||||
@@ -186,7 +186,19 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu
|
||||
return Number.isFinite(length) && length > 0 ? length : null;
|
||||
}
|
||||
|
||||
/** 측점 범위 표시 갱신 — 기준 측점 + 길이가 다 있으면 "시작 ~ 종료", 아니면 "—". */
|
||||
/** 전길이 옵션(before_m, m) — 기준 측점에서 시점 쪽으로 물러나는 몫. 비우면 0
|
||||
* (= 기준이 곧 시작). 총길이를 넘길 수 없다 — 넘치면 총길이로 자른다(후길이 0). */
|
||||
function readBeforeM(): number {
|
||||
const entry = optionInputs.find((input) => input.key === "before_m");
|
||||
if (!entry || entry.isEmpty()) return 0;
|
||||
const before = Number(entry.read());
|
||||
if (!Number.isFinite(before) || before < 0) return 0;
|
||||
const length = readLengthM();
|
||||
return length !== null && before > length ? length : before;
|
||||
}
|
||||
|
||||
/** 측점 범위 표시 갱신 — 시작 = 기준 − 전길이, 종료 = 시작 + 길이(2026-08-19
|
||||
* 사용자 지시: 전/후 분할, 후길이 = 길이 − 전길이 자동). 계산 불가면 "—". */
|
||||
function syncRangeDisplay(): void {
|
||||
const computed = hasComputedRange(currentType());
|
||||
rangeWrap.hidden = !computed;
|
||||
@@ -194,9 +206,10 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu
|
||||
const step = interval();
|
||||
const anchor = anchorFields.read(step, false);
|
||||
const length = readLengthM();
|
||||
const start = anchor === null ? null : anchor - readBeforeM();
|
||||
rangeValue.textContent =
|
||||
anchor !== null && length !== null
|
||||
? `${formatStation(anchor, step)} ~ ${formatStation(anchor + length, step)}`
|
||||
start !== null && length !== null && start > -0.005
|
||||
? `${formatStation(Math.max(start, 0), step)} ~ ${formatStation(Math.max(start, 0) + length, step)}`
|
||||
: "—";
|
||||
}
|
||||
|
||||
@@ -398,10 +411,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);
|
||||
// 길이·전길이가 바뀌면 측점 범위 표시가 즉시 따라온다(범위 계산 타입, 2026-08-19).
|
||||
if (option.key === "length_m" || option.key === "before_m") {
|
||||
const refresh = (): void => {
|
||||
input.classList.remove("is-invalid");
|
||||
syncRangeDisplay();
|
||||
};
|
||||
input.addEventListener("input", refresh);
|
||||
input.addEventListener("change", refresh);
|
||||
}
|
||||
});
|
||||
syncOptionLock();
|
||||
@@ -505,13 +522,18 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu
|
||||
endFields.write(target.end_m ?? null, step);
|
||||
memoField.value = target.memo ?? "";
|
||||
renderOptionFields(target.options);
|
||||
// 구간 직접 입력 시절에 저장된 항목 호환 — 길이 옵션이 비어 있으면 저장된
|
||||
// 시작~종료 구간 길이로 채워 범위 표시·재저장이 이어지게 한다(2026-08-19).
|
||||
// 구간 직접 입력 시절에 저장된 항목 호환 — 길이·전길이 옵션이 없으면 저장된
|
||||
// 시작~종료·기준점에서 역산해 채워 범위 표시·재저장이 이어지게 한다(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);
|
||||
}
|
||||
const beforeEntry = optionInputs.find((entry) => entry.key === "before_m");
|
||||
if (beforeEntry && target.options.before_m === undefined) {
|
||||
const before = structureAnchorM(target) - target.start_m;
|
||||
if (before > 0.005) beforeEntry.input.value = before.toFixed(1);
|
||||
}
|
||||
}
|
||||
syncFacilityForm(target.options);
|
||||
} else {
|
||||
@@ -624,7 +646,8 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu
|
||||
let start: number | null = null;
|
||||
let end: number | null = null;
|
||||
if (placement === "interval" && hasComputedRange(type)) {
|
||||
// 범위 계산 타입(C군 등) — 시작 = 기준 측점, 종료 = 기준 + 길이(2026-08-19).
|
||||
// 범위 계산 타입(C군 등) — 시작 = 기준 − 전길이, 종료 = 시작 + 길이
|
||||
// (2026-08-19 사용자 지시: 전/후 분할). 전길이가 기준을 넘어 시작이 음수면 거절.
|
||||
anchor = anchorFields.read(step, true);
|
||||
if (anchor === null) {
|
||||
anchorFields.station.focus();
|
||||
@@ -635,8 +658,15 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu
|
||||
optionInputs.find((entry) => entry.key === "length_m")?.input.focus();
|
||||
return;
|
||||
}
|
||||
start = anchor;
|
||||
end = anchor + length;
|
||||
const before = readBeforeM();
|
||||
if (anchor - before < -0.005) {
|
||||
const beforeInput = optionInputs.find((entry) => entry.key === "before_m")?.input;
|
||||
beforeInput?.classList.add("is-invalid");
|
||||
beforeInput?.focus();
|
||||
return;
|
||||
}
|
||||
start = Math.max(anchor - before, 0);
|
||||
end = start + length;
|
||||
} else if (placement === "interval") {
|
||||
start = startFields.read(step, true);
|
||||
end = endFields.read(step, true);
|
||||
|
||||
Reference in New Issue
Block a user