fix(구조물표): 323-6 고침 — 지우고 새로 바꾸는 종류 바꿈 = 새 종류가 놓인 뒤에만 옛 것을 지움(못 놓으면 옛 것 · 옛 폼 그대로 + 알림) · [추가] 무반응 = 막힌 칸 이름 알림

- 반사경 → 가드레일(길이 빈칸)이 옛 것을 먼저 지우고 새 것은 못 놓아 반사경이 사라지던 것 — 패널 addAt 이 놓였는지 돌려줌 · Events replaceAt = 먼저 놓고 확인 뒤 옛 것 지움 · 못 놓으면 옛 기록 폼(관은 관 폼) + 「반사경 그대로 — 가드레일(으)로 못 바꿈」
- commit 의 조용한 return 여섯 곳 = 막힌 칸으로 커서 + [추가] 때 「「길이」 칸이 비었거나 맞지 않아 못 놓음」(실시간 반영은 알림 없음)
- 패널 줄 수 지킴(setter 한 줄로 · 697)
- 시험: test_319 바꿈 막힘 둘(구조물 · 관) · 비동기 바꿈 · test_323_6 막힌 칸 알림

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ji4nuyLVHcxrXWHTdb5nRD
This commit is contained in:
2026-10-14 18:32:46 +09:00
co-authored by Claude Opus 5.5
parent 529baca3ff
commit 6744524669
7 changed files with 119 additions and 65 deletions
@@ -13,6 +13,7 @@ import {
type StructureInstance,
type StructureType,
} from "./B05_Profile_Api_Structures";
import { showToast } from "@ui/ui_template_elements";
import type { PipeFacility } from "../B04_PreProcess/B04_PreProcess_Api_Fetch";
import type { FacilityAttributes, FacilityOptionsForm } from "./B05_Profile_UI_Drainage_Facility";
import type { StationFields } from "./B05_Profile_UI_Structures_Fields";
@@ -249,15 +250,19 @@ export async function commit(ctx: StructuresCommitContext, live = false): Promis
const type = ctx.currentType();
if (!type) return;
const step = ctx.interval();
/** 막힌 칸으로 커서 · [추가](실시간 반영 아님)면 어느 칸인지 알림(323-6 · 무반응이던 것). */
const stop = (input: HTMLElement | null | undefined, field: string): void => {
input?.focus();
if (!live) showToast(`「${field}」 칸이 비었거나 맞지 않아 못 놓음 — 칸을 채우세요.`, "error");
};
const labelOf = (key: string): string =>
type.options.find((option) => option.key === key)?.label ?? key;
// 계곡 통과 시설 — 기준 측점 + 구간 + 부속 옵션을 관 지점 정본으로 보낸다
// (시설 종류 = type_id). 목록에서 골라 온 경우는 수정(기준점 이동 포함)이다.
if (type.managed_by) {
const anchor = ctx.anchorFields.read(step, true);
if (anchor === null) {
ctx.anchorFields.station.focus();
return;
}
if (anchor === null) return stop(ctx.anchorFields.station, "기준 측점");
// 배수관 등 계곡 통과 시설은 점형 — 기준 측점 하나뿐이다(2026-08-17 지시 2).
const attributes: FacilityAttributes = { facility: type.type_id as PipeFacility };
const options = readOptions(ctx);
@@ -288,14 +293,11 @@ export async function commit(ctx: StructuresCommitContext, live = false): Promis
// 범위 계산 타입(C군 등) — 시작 = 기준 − 전길이, 종료 = 시작 + 길이
// (2026-08-19 사용자 지시: 전/후 분할). 전길이가 기준을 넘어 시작이 음수면 거절.
anchor = ctx.anchorFields.read(step, true);
if (anchor === null) {
ctx.anchorFields.station.focus();
return;
}
if (anchor === null) return stop(ctx.anchorFields.station, "기준 측점");
const length = ctx.readLengthM();
if (length === null) {
ctx.optionInputs.find((entry) => entry.key === "length_m")?.input.focus();
return;
const input = ctx.optionInputs.find((entry) => entry.key === "length_m")?.input;
return stop(input, labelOf("length_m"));
}
// 높이는 비워도 놓음 — 자리부터 잡고 치수를 뒤에 넣는 길을 막지 않음. 높이가 물량 밑수인
// 벽은 B08 에서 줄만 서고 미확정(금액 밖)으로 뜸(2026-09-14 브레인 판정 ①, `missing_height_reason`).
@@ -303,8 +305,7 @@ export async function commit(ctx: StructuresCommitContext, live = false): Promis
if (anchor - before < -0.005) {
const beforeInput = ctx.optionInputs.find((entry) => entry.key === "before_m")?.input;
beforeInput?.classList.add("is-invalid");
beforeInput?.focus();
return;
return stop(beforeInput, labelOf("before_m"));
}
// 시작 · 끝도 정수 m · 끝은 1 m 이상(234-1 · 167)
start = snapStructureStation(Math.max(anchor - before, 0));
@@ -312,10 +313,11 @@ export async function commit(ctx: StructuresCommitContext, live = false): Promis
} else if (placement === "interval") {
start = ctx.startFields.read(step, true);
end = ctx.endFields.read(step, true);
if (start === null || end === null || end <= start) {
(start === null ? ctx.startFields.station : ctx.endFields.station).focus();
return;
}
if (start === null || end === null || end <= start)
return stop(
start === null ? ctx.startFields.station : ctx.endFields.station,
"시작 · 끝 측점",
);
// 기준 측점(마킹 위치) — 비우면 시작 측점. 시작~종료 밖은 서버도 거절한다.
// 범위 직접 입력형(노면공 · 87-1) = 기준 칸 없음 → 늘 시점.
anchor = STRUCTURE_SPECS[type.type_id]?.byRange
@@ -323,24 +325,17 @@ export async function commit(ctx: StructuresCommitContext, live = false): Promis
: (ctx.anchorFields.read(step, false) ?? start);
if (anchor < start || anchor > end) {
ctx.anchorFields.station.classList.add("is-invalid");
ctx.anchorFields.station.focus();
return;
return stop(ctx.anchorFields.station, "기준 측점(시작 ~ 끝 안)");
}
} else {
anchor = ctx.anchorFields.read(step, true);
if (anchor === null) {
ctx.anchorFields.station.focus();
return;
}
if (anchor === null) return stop(ctx.anchorFields.station, "기준 측점");
}
// 필수 옵션(미확정 기본값 없음)이 비어 있으면 추가하지 않는다 — 서버도 거절한다.
// 상세(detail) 옵션은 폼에 없으므로 여기 걸리지 않는다(B06/B07에서 받는다).
// 실시간 반영은 막지 않음 — 빈 칸은 표시 · 안내만(`markRequired` · 54-12), 막기는 [추가] · 서버 [저장]
const missing = ctx.optionInputs.find((entry) => entry.required && entry.isEmpty());
if (missing && !live) {
missing.input.focus();
return;
}
if (missing && !live) return stop(missing.input, labelOf(missing.key));
const summary = ctx.summary();
const base = {