fix(b06): 물넘이 연동 범위 안 측점에 자기 관도 함께 얹음 — 키별 하나씩 · 같은 키는 소유 먼저 · 흙막이는 자기 관에 밀림 (299-1)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013QyNXY2TUKXCFMwAca2APG
This commit is contained in:
2026-10-12 22:18:40 +09:00
co-authored by Claude Opus 5.5
parent be5ef91f72
commit e2e71ad3ca
3 changed files with 198 additions and 29 deletions
+29 -13
View File
@@ -580,6 +580,12 @@ export function buildCulvertSets(
return sets;
}
/** 차례 비교(파이썬 튜플 `<` 와 같음) — 처음 다른 자리에서 작은 쪽이 먼저. */
function rankBefore(a: readonly number[], b: readonly number[]): boolean {
const at = a.findIndex((value, index) => value !== b[index]);
return at >= 0 && a[at] < b[at];
}
/** 측점 자료에 세트를 얹는다(파이썬 `attach_culvert_sets`). 얹은 개수를 돌려준다. */
export function attachCulvertSets(
sections: Array<Record<string, unknown>>,
@@ -607,25 +613,35 @@ export function attachCulvertSets(
for (const section of sections) {
const chainage = num(section.chainage_m, null);
if (chainage === null) continue;
// ⚠ 키가 다른 시설은 같은 측점에 함께 얹는다(299-1 · 짝: 파이썬 `attach_culvert_sets`) — 첫 시설
// 하나에서 멈추면 물넘이포장 연동 범위 안 측점의 **자기 관**이 빠졌다. 같은 키끼리는 소유 측점
// 것 → 흙막이 아닌 자기 시설 → 가까운 것 → 관 지점 차례.
const best = new Map<string, { rank: number[]; value: CulvertSetSpec }>();
let order = 0;
for (const [pipeChainage, spec] of sets) {
order += 1;
// 연동 대상 종류만 폭의 절반까지 옆 측점에 걸친다.
let reach = CHAINAGE_TOLERANCE_M;
if (SPAN_LINKED_TYPES.has(String(spec.type))) reach += (num(spec.span_m, 0) ?? 0) / 2;
if (Math.abs(chainage - pipeChainage) <= reach || owner.get(pipeChainage) === chainage) {
// ⚠ 스펙에 **그 시설이 놓인 누가거리**를 함께 얹는다(2026-09-08, 짝: 파이썬
// `attach_culvert_sets`). 세트는 폭의 절반까지 옆 측점에도 붙으므로, 이것이 없으면
// 소비처가 「소유 측점」을 못 가려 **같은 시설을 여러 측점에서 센다**.
section[SECTION_KEYS[String(spec.type)] ?? "culvert"] = {
...spec,
chainage_m: pipeChainage,
};
// 세월교 · 박스 흙막이(73-4) — 배관 흙막이와 같은 자리(`culvert`)에 함께 얹음.
if (spec.guard)
section.culvert = { ...(spec.guard as CulvertSetSpec), chainage_m: pipeChainage };
attached += 1;
break;
const owned = owner.get(pipeChainage) === chainage;
if (!(Math.abs(chainage - pipeChainage) <= reach || owned)) continue;
const entries: Array<[string, CulvertSetSpec, number]> = [
[SECTION_KEYS[String(spec.type)] ?? "culvert", spec, 0],
];
// 세월교 · 박스 흙막이(73-4) — 배관 흙막이와 같은 자리(`culvert`) · 자기 관과 부딪히면 관 먼저.
if (spec.guard) entries.push(["culvert", spec.guard as CulvertSetSpec, 1]);
for (const [key, value, isGuard] of entries) {
const rank = [owned ? 0 : 1, isGuard, Math.abs(chainage - pipeChainage), order];
const held = best.get(key)?.rank;
// ⚠ 스펙에 **그 시설이 놓인 누가거리**를 함께 얹는다(2026-09-08). 세트는 폭의 절반까지
// 옆 측점에도 붙으므로, 이것이 없으면 소비처가 「소유 측점」을 못 가려 **같은 시설을 여러
// 측점에서 센다**.
if (!held || rankBefore(rank, held))
best.set(key, { rank, value: { ...value, chainage_m: pipeChainage } });
}
}
for (const [key, { value }] of best) section[key] = value;
if (best.size) attached += 1;
}
return attached;
}