feat(B06): 세월교 좌측 「구조물 배치」 폼을 조정창 로직에 연결한다

좌측 폼의 세월교 상세 항목(관종·관경·수량·월류 폭·날개벽 유입/유출)이 도면에
전혀 반영되지 않았다. `applyPipeOptionsToCache`가 `section.culvert`만 보고
세월교 측점(`section.ford`)에서는 즉시 return했기 때문이다.

프론트는 구조물 배치의 상세 UI를 쓰고 로직은 조정창 제어기 것을 쓴다
(2026-08-30 사용자 확정).

- `applyFordFormOptions()` 신설 — 폼 옵션 키를 `FordControl.setPipe`/`setWing`
  인자로 옮긴다. 캐시 반영·바닥판 연장 재계산·정본 예약·카드 갱신은 제어기에
  이미 있어 새로 만들지 않았다.
- `FordControl.setPipe` 확장 — `pipe_kind`(관종)·`ford_width_m`(월류 폭 =
  `span_m`) 추가. 두 값은 조정창에 칸이 없고 폼만 보낸다.
- `withFordSpec()` — 폼 기본값을 `section.ford` 스펙에서 읽는다. 두 창이 같은
  스펙을 보므로 어느 쪽에서 만졌든 값이 갈리지 않는다(수량(련) 빈 칸 해소).

월류 높이(`ford_height_m`)는 백엔드 `_ford_set`이 읽지 않는다 — 세월교는 구체
위 노면이라 파임이 없고, 물넘이포장 전용 값이다. 폼에는 남되 도면 반영처는 없다.

자체검증(공용 브라우저 5174, 측점 7+9.7 세월교):
- tsc --noEmit 통과.
- 폼 날개 길이(유입) 2→4 → 조정창 「날개 길이 4.0m」, 바닥판 7.63m→9.04m
  (= +2×cos45°). 수정 전에는 둘 다 그대로였다.
- 폼 수량 3 → 조정창 「3련」, 폼 관경 Ø1200 → 조정창 select 1200.
- 재선택 왕복: 월류폭 12·수량 3·관경 1200·날개길이 4 그대로 되읽힘(스펙 정본).
- 진입 시 수량(련) 칸 = 1(수정 전 빈 칸), 관종 파형강관.
- 조작값은 원래 값(10/1/1000/2)으로 되돌려 놓았다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-30 11:44:54 +09:00
co-authored by Claude Opus 5
parent c8ce7ae02a
commit 03840ff3d6
4 changed files with 99 additions and 4 deletions
@@ -102,6 +102,32 @@ function coversChainage(structure: StructureInstance, chainageM: number): boolea
return Math.abs(structureAnchorM(structure) - chainageM) < PIPE_MATCH_M;
}
/**
* 세월교 폼 옵션을 **지금 그려진 스펙**(`section.ford`)으로 덮는다 — 조정창 조작이
* 이 스펙을 제자리에서 고치므로, 폼이 저장된 옵션을 붙들면 두 창이 갈린다
* (2026-08-30 사용자: 수량(련) 칸이 비어 있고 조정창은 1련이었다).
*/
function withFordSpec(
options: Record<string, string | number> | undefined,
ford: NonNullable<SectionDetailResponse["cross_sections"][number]["ford"]>,
): Record<string, string | number> {
const merged: Record<string, string | number> = { ...(options ?? {}) };
if (ford.pipe_kind) merged.pipe_kind = ford.pipe_kind;
merged.pipe_diameter_mm = Math.round(ford.diameter_m * 1000);
merged.pipe_count = ford.pipe_count;
merged.ford_width_m = ford.span_m;
for (const [wing, prefix] of [
[ford.wing_in, "wing_in"],
[ford.wing_out, "wing_out"],
] as const) {
merged[prefix] = wing.installed ? "있음" : "없음";
if (wing.height_m !== null) merged[`${prefix}_height_m`] = wing.height_m;
if (wing.length_m !== null) merged[`${prefix}_length_m`] = wing.length_m;
if (wing.angle_deg !== null) merged[`${prefix}_angle_deg`] = wing.angle_deg;
}
return merged;
}
/**
* 관 지점 옵션에 **횡단 스펙 값**을 채워 넣는다 — 조정창이 보여주던 값과 폼이
* 어긋나지 않게 한다(2026-08-29 사용자). 옵션에 이미 값이 있으면 그대로 두고,
@@ -117,6 +143,10 @@ function withSpecDefaults(
const owner = detail?.cross_sections.find(
(section) => Math.abs(section.chainage_m - chainageM) < PIPE_MATCH_M,
);
// 세월교는 폼과 조정창이 **같은 캐시**(`section.ford`)를 본다 — 어느 쪽에서 만졌든
// 그 스펙이 지금 그려진 값이라 저장된 옵션보다 앞선다(2026-08-30 사용자: 프론트는
// 구조물 배치 상세 UI, 로직·값은 조정창 것).
if (owner?.ford) return withFordSpec(options, owner.ford);
const culvert = owner?.culvert;
if (!culvert) return options;
const merged: Record<string, string | number> = { ...(options ?? {}) };