fix(b06): 신규 관 계열 [추가] — B06 길도 빈 월류 높이 채움 · 알림 글 「뒤에서 다시 나눔」 · 다시 나눔 실패 토스트 (298-1)

- B05 · B06 [추가] = 그 측점 횡단(카드 · 측점선) 바로 + 세부유역 뒤에서 다시 나눔(세션 초안 · 저장 안 함 · 최신 응답만 · 지도 칸 표시만) — 있던 길 그대로
- 까닭 1: B06 [추가]는 높이 기다림을 판에 안 알려 새 세월교 · 물넘이 월류 높이(126-1)가 빈칸 — 기다림 표를 프로젝트마다 한 벌(fordWaitsOf)로 판 · B06 이 같이 씀 · 채운 높이를 B06 목록 · 폼이 받음(adoptFordHeights)
- 까닭 2: B06 알림 글이 「세부 배수유역은 [저장]·[확정] 뒤 계산」 — 실제는 뒤에서 바로 다시 나눔 · 넣기 · 빼기 · 옮기기 글 고침
- 다시 나눔 실패 = 지도 칸 문구 + 토스트 한 줄(analyzeFailed)
- 시험: test_298_add_reanalyze 새로 · test_126 · test_291_1 · helper_165 · helper_231_1 맞춤

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VmC34fsxxbweEG3RUjs3Mn
This commit is contained in:
2026-10-12 22:09:00 +09:00
co-authored by Claude Opus 5.5
parent 7ac250a23a
commit 8ce01dfa35
12 changed files with 329 additions and 21 deletions
@@ -30,6 +30,7 @@ import {
} from "../B05_Profile/B05_Profile_Api_Structures";
import { readPendingPipes, writePendingPipes } from "../B05_Profile/B05_Profile_Api_Pipes_Draft";
import { mergeFacilityOptions } from "../B05_Profile/B05_Profile_UI_Drainage_Facility_Merge";
import { fordWaitsOf, waitsFordHeight } from "../B05_Profile/B05_Profile_UI_Drainage_Ford_Fill";
import { fetchDetailPipePoints } from "../B04_PreProcess/B04_PreProcess_Api_Fetch";
import {
linkFields,
@@ -61,15 +62,14 @@ import { withSpecDefaults } from "./B06_Section_UI_Page_Structures_Spec";
const PIPE_ADD_GUIDE = "계곡 통과 시설을 넣고 빼려면 프로젝트를 먼저 여세요.";
const PIPE_MOVE_GUIDE = "기준 측점 이동은 배수유역을 다시 나눠야 해 B05(종단) 화면에서 합니다.";
/** B05·B06 은 한 페이지라 넣고 빼는 자리도 같아야 한다(2026-09-12 사용자). 재분할은 저장 뒤. */
/** B05·B06 은 한 페이지라 넣고 빼는 자리도 같아야 한다(2026-09-12 사용자). 재분할은 뒤에서 바로(298-1). */
const PIPE_ADD_NOTICE =
"계곡 통과 시설을 넣었습니다 — 횡단도는 바로 보이고, 세부 배수유역은 [저장]·[확정] 뒤에 다시 계산됩니다.";
const PIPE_REMOVE_NOTICE =
"계곡 통과 시설을 뺐습니다 — 세부 배수유역은 [저장]·[확정] 뒤에 다시 계산됩니다.";
"계곡 통과 시설을 넣었습니다 — 횡단도는 바로 보이고, 세부 배수유역은 뒤에서 다시 나눕니다.";
const PIPE_REMOVE_NOTICE = "계곡 통과 시설을 뺐습니다 — 세부 배수유역은 뒤에서 다시 나눕니다.";
/** 고른 것 없이 폼만 만졌을 때 — 값이 어디로도 가지 않으므로 이유를 알린다. */
const PIPE_PICK_GUIDE = "먼저 횡단도나 목록에서 구조물을 고르세요 — 고른 것에만 값이 반영됩니다.";
const PIPE_MOVE_NOTICE =
"기준 측점을 옮겼습니다 — 횡단도는 바로 보이고, 세부 배수유역은 [저장]·[확정] 뒤에 다시 계산됩니다.";
"기준 측점을 옮겼습니다 — 횡단도는 바로 보이고, 세부 배수유역은 뒤에서 다시 나눕니다.";
export interface B06StructuresPanelDeps {
projectId: string | null;
@@ -132,6 +132,8 @@ export interface B06StructuresPanel {
selectStructure: (structureId: string) => void;
/** 측점 없는 관(물넘이포장) 하나를 폼에 올림 — 카드 안 고름 · 스크롤만 · 알약 · 고름 값에 알약 id(요구 178). */
selectPipeAt: (chainageM: number) => void;
/** 배수유역도가 관 초안에 채운 빈 월류 높이(126-1)를 목록 · 폼에 받는다(298-1). */
adoptFordHeights: () => void;
/** 지금 목록의 구조물 정본(미저장분 포함) — 카드 찾기에 쓴다. */
getStructures: () => StructureInstance[];
/** 지금 폼에 올라온 계곡 통과 시설의 누가거리(없으면 null). */
@@ -259,6 +261,9 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
});
pipeFacilities.sort((left, right) => left.chainage_m - right.chainage_m);
currentChainageM = at;
// 빈 월류 높이 = 다시 나눈 유량으로 채움(126-1) — 배수유역도 판이 이 표를 보고 채움(298-1)
if (waitsFordHeight(attributes.facility, attributes.options))
fordWaitsOf(deps.projectId).add(at);
persistPipes();
section.setPipeFacilities(pipeFacilities);
pushMarks();
@@ -266,7 +271,7 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
showToast(PIPE_ADD_NOTICE, "success");
},
// 값 수정·기준점 이동 모두 B06 에서 받는다 — 이동은 종단 끌기와 **같은 함수**를 탄다
// (2026-09-12 일원화). 재분할은 [저장]·[확정] 뒤 서버 몫이다.
// (2026-09-12 일원화). 세부유역은 관 초안을 쓰면 배수유역도가 뒤에서 다시 나눈다(298-1).
onPipeUpdate: (fromChainageM, toChainageM, attributes) => {
// 값만 고치면 폼이 옛 측점을 그대로 돌려줌(안 고친 칸) — 0.005m 넘으면 옮긴 것(80.17 → 80 도 · 234-1)
if (Math.abs(fromChainageM - toChainageM) > 0.005) movePipeTo(fromChainageM, toChainageM);
@@ -548,6 +553,23 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
section.selectPipeByChainage(hit.chainage_m);
},
getStructures: () => structures,
adoptFordHeights: () => {
// 배수유역도가 초안에 채운 월류 높이(126-1)를 목록 · 폼에 받음 — 빈 칸만(298-1)
const draft = deps.projectId ? readPendingPipes(deps.projectId) : null;
const filled = pipeFacilities.filter((entry) => {
if (entry.options?.ford_height_m !== undefined) return false;
const height = draft?.find(
(pipe) => Math.abs(pipe.chainage_m - entry.chainage_m) < PIPE_MATCH_M,
)?.options?.ford_height_m;
if (height === undefined) return false;
entry.options = { ...entry.options, ford_height_m: height };
return true;
});
if (!filled.length) return;
section.setPipeFacilities(pipeFacilities);
const open = filled.find((entry) => entry.chainage_m === currentChainageM);
if (open) section.selectPipeByChainage(open.chainage_m);
},
releasePipe: () => {
currentChainageM = null;
section.selectPipeByChainage(null);