fix(B05): 한 번 편집하면 맞춰 둔 반지름이 하한으로 눌리던 것

실화면에서 잡음 — 곡선 하나만 지워도 나머지 반지름이 **R 12~199m → 전부 12m**
로 되돌아갔음.

원인은 편집 모델과 저장 모델이 다른 것이었음. 서버는 처음 만들 때 이어진
꺾임을 한 곡선으로 묶는데, 그 곡선의 교각점은 앞뒤 직선을 늘려 만나는 자리라
**원본 꺾임점 중 어느 것도 아님**. 편집은 「꺾임점 하나 = 곡선 하나」로
표현되므로, 묶인 채로 두면 한 번만 손대도 묶음이 낱개로 흩어지고 각 자리가
하한으로 떨어졌음.

모달이 자료를 읽을 때 **묶인 구간을 그 교각점 하나로 갈아 끼우게** 함 —
안쪽 꺾임점은 그 곡선이 대신하므로 뺌. 앞뒤 직선과 반지름이 그대로라
그려지는 선은 똑같고, 이제 손대도 안 흩어짐. 서버가 고른 반지름도 함께
들고 가 [확인] 때 되돌려 보냄.

시험 tmp/tests/test_route_polyline_apex_roundtrip.py 2건 — 갈아 끼워도 곡선
수·반지름이 같고 선이 **1mm 안**에서 일치, 그리고 안 갈아 끼우면 실제로
하한으로 눌리는 것(왜 필요한지)도 못박음.

실화면 확인(용화) — 되돌린 뒤 곡선 26곳 R 최대 198.9m → 곡선 하나 지우고
[확인] → 곡선 25곳, **R 42.5·51·198.9m 그대로**.

tsc --noEmit 통과 · pytest 432 passed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-07 08:46:09 +09:00
co-authored by Claude Opus 5
parent 20f343902d
commit a0bd9c8775
+39 -10
View File
@@ -667,17 +667,46 @@ export async function openRouteEditModal(
// 잡는 것은 **노드**다 — 폴리라인 정점에는 원호 위 점이 섞여 있어 편집 대상이 아니다
// (2026-09-06 사용자 지시: 노드를 제어해 계획노선을 고친다).
const nodes = plan.nodes ?? [];
planned = nodes.map((node) => [node.x, node.y] as Vertex);
nodeInfo = nodes.map((node) => ({
radius_m: node.radius_m,
inner_angle_deg: node.inner_angle_deg,
violations: node.violations ?? [],
}));
minRadiusM = plan.min_radius_m ?? 0;
curveInfo = plan.curves ?? [];
// 편집값은 「서버가 준 그대로」에서 시작한다 — 곡선이 있는 자리는 켬, 반지름은 자동.
curveOn = planned.map(() => true);
curveRadius = planned.map(() => null);
// 곡선 성분을 **편집할 수 있는 꼴로 펴 둔다**(2026-09-07).
//
// 서버는 처음 만들 때 이어진 꺾임을 한 곡선으로 묶는다 — 그 곡선의 교각점은 앞뒤 직선을
// 늘려 만나는 자리라 **원본 꺾임점 중 어느 것도 아니다**. 그런데 편집은 「꺾임점 하나 =
// 곡선 하나」로 표현되므로, 묶인 곡선을 그대로 두면 한 번만 손대도 그 묶음이 낱개로
// 흩어지고 **맞춰 둔 반지름이 전부 법정 하한으로 되돌아간다**(실측: R 12~199m → 전부 12m).
//
// 그래서 **묶인 구간을 그 교각점 하나로 갈아 끼운다** — 안쪽 꺾임점은 그 곡선이 대신하므로
// 뺀다. 앞뒤 직선과 반지름이 그대로라 **그려지는 선은 똑같고**, 이제 손대도 안 흩어진다.
const curves = plan.curves ?? [];
const replaced = new Map<number, (typeof curves)[number]>();
const dropped = new Set<number>();
curves.forEach((curve) => {
replaced.set(curve.node_first, curve);
for (let index = curve.node_first + 1; index <= curve.node_last; index += 1) {
dropped.add(index);
}
});
planned = [];
nodeInfo = [];
curveOn = [];
curveRadius = [];
curveInfo = [];
nodes.forEach((node, index) => {
if (dropped.has(index)) return;
const curve = replaced.get(index);
const at: Vertex = curve ? [curve.apex[0], curve.apex[1]] : [node.x, node.y];
const seat = planned.length;
planned.push(at);
nodeInfo.push({
radius_m: curve ? curve.radius_m : node.radius_m,
inner_angle_deg: curve ? curve.inner_angle_deg : node.inner_angle_deg,
violations: curve ? (curve.violations ?? []) : (node.violations ?? []),
});
curveOn.push(true);
// 서버가 고른 반지름을 **그대로 들고 간다** — 안 그러면 편집 한 번에 하한으로 눌린다.
curveRadius.push(curve ? Math.round(curve.radius_m * 100) / 100 : null);
if (curve) curveInfo.push({ ...curve, node_first: seat, node_last: seat });
});
picked = -1;
syncCurveBar();
if (!planned.length) planned = plannedLine.map((vertex) => [vertex[0], vertex[1]]);