feat(측점열쇠): 346-2 덧 2 열쇠 한 벌 — 구간형 :start · :end · 묶음 측점 종류(grid · end) · 같은 열쇠 둘째 규칙 공용

- common_util_station_key(ts · py 짝): role 덧붙임(구간형 시작 · 끝 = structure:<id>:start · :end · 기준은 structure:<id> 그대로) · grid(원래 격자 번호) · end(BP/EP) 종류 · dedupeStationKeys(같은 열쇠 둘째부터 at:<누가거리>)
- B06 stationKeysOf 가 같은 규칙(측점 계획 · 구조물 목록의 시작 · 끝 자리 · 공용 dedupe)
- 거울 시험에 새 갈래(구간형 · grid · end · dedupe)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QfHKQs7J3hQELKMWQMUJEy
This commit is contained in:
2026-10-16 12:20:06 +09:00
co-authored by Claude Opus 5.5
parent f9c70757b4
commit 69d84e5ca9
4 changed files with 119 additions and 28 deletions
+33 -13
View File
@@ -18,7 +18,13 @@
import { readState, writeState } from "../A00_Common/b_page_state";
import { pipeIdAt } from "../A00_Common/design/design_pipe_ids";
import { showToast } from "@ui/ui_template_elements";
import { chainageKey, looseChainage, stationKey } from "@util/common_util_station_key";
import {
chainageKey,
dedupeStationKeys,
looseChainage,
stationKey,
type StationKeyRole,
} from "@util/common_util_station_key";
import { inferStationInterval } from "@util/common_util_svg";
import type { CrossSection, SectionDetailResponse } from "./B06_Section_Api_Fetch";
@@ -46,7 +52,8 @@ type Raw = Record<string, unknown> & { [HINT]?: Record<string, number> };
const lists = new Map<string, Keyed[]>();
/** 프로젝트마다 구조물 측점 → id(측점 계획 · 구조물 목록). */
const owners = new Map<string, Array<{ id: string; kind: "pipe" | "structure"; at: number }>>();
type Owner = { id: string; kind: "pipe" | "structure"; at: number; role: StationKeyRole };
const owners = new Map<string, Owner[]>();
let current: { projectId: string; routeId: number } | null = null;
const scopeOf = (projectId: string, routeId: number): string => `${projectId}:${routeId}`;
@@ -54,7 +61,7 @@ const scopeOf = (projectId: string, routeId: number): string => `${projectId}:${
/** 측점 계획(초안 · `planStations`)이 낸 덮는 구조물 id — 관 세션 id 는 `pipe-` 머리. */
export function useStationOwners(
projectId: string,
planned: ReadonlyArray<{ chainage_m: number; owners: string[] }>,
planned: ReadonlyArray<{ chainage_m: number; owners: string[]; role?: StationKeyRole }>,
): void {
owners.set(
projectId,
@@ -63,6 +70,7 @@ export function useStationOwners(
id,
kind: id.startsWith("pipe-") ? ("pipe" as const) : ("structure" as const),
at: station.chainage_m,
role: station.role ?? "anchor",
})),
),
);
@@ -71,12 +79,25 @@ export function useStationOwners(
/** 구조물 목록(횡단 보기) — 한 자리 구조물의 id. 관 자리는 관 장부가 정함. */
export function useStructureIds(
projectId: string,
structures: ReadonlyArray<{ structure_id?: string | null; chainage_m?: number | null }>,
structures: ReadonlyArray<{
structure_id?: string | null;
chainage_m?: number | null;
start_m?: number | null;
end_m?: number | null;
}>,
): void {
const known = (owners.get(projectId) ?? []).filter((owner) => owner.kind === "pipe");
for (const item of structures) {
if (item.structure_id && typeof item.chainage_m === "number")
known.push({ id: item.structure_id, kind: "structure", at: item.chainage_m });
const id = item.structure_id;
if (!id) continue;
// 기준 측점 = structure:<id> · 구간형 시작 · 끝 = :start · :end(열쇠 한 벌 · common_util_station_key)
const marks: Array<[StationKeyRole, number | null | undefined]> = [
["anchor", item.chainage_m],
["start", item.start_m],
["end", item.end_m],
];
for (const [role, at] of marks)
if (typeof at === "number") known.push({ id, kind: "structure", at, role });
}
owners.set(projectId, known);
}
@@ -84,20 +105,19 @@ export function useStructureIds(
const looksLikePipe = (section: CrossSection): boolean =>
Boolean(section.culvert || section.box) || /^.+ D\d+/.test(section.structure ?? "");
/** 측점 목록 → 열쇠(같은 열쇠가 둘이면 뒤엣것은 `at:`). */
/** 측점 목록 → 열쇠(같은 열쇠가 둘이면 뒤엣것은 `at:` · `dedupeStationKeys`). */
export function stationKeysOf(
projectId: string | null,
sections: ReadonlyArray<CrossSection>,
intervalM: number | null,
): Keyed[] {
const known = projectId ? (owners.get(projectId) ?? []) : [];
const seen = new Set<string>();
return sections.map((section) => {
const keys = sections.map((section) => {
const at = section.chainage_m;
let key: string;
if (section.kind === "irregular") {
const owner = known.find((entry) => Math.abs(entry.at - at) < NEAR_M);
if (owner) key = stationKey("irregular", at, intervalM, owner.id, owner.kind);
if (owner) key = stationKey("irregular", at, intervalM, owner.id, owner.kind, owner.role);
else if (looksLikePipe(section))
key = stationKey("pipe", at, intervalM, pipeIdAt(at, projectId));
else
@@ -105,10 +125,10 @@ export function stationKeysOf(
? stationKey("regular", at, intervalM)
: stationKey("irregular", at, intervalM);
} else key = stationKey(section.kind, at, intervalM);
if (seen.has(key)) key = `at:${chainageKey(at)}`;
seen.add(key);
return { key, chainageM: at };
return key;
});
const chainages = sections.map((section) => section.chainage_m);
return dedupeStationKeys(keys, chainages).map((key, i) => ({ key, chainageM: chainages[i] }));
}
type KeyedDetail = Pick<SectionDetailResponse, "cross_sections" | "longitudinal">;