feat(B05): 노선 추천 — 되돌리기 · 다시하기가 「현재 추천」 글을 함께 되살림(걸음마다 추천을 실음) (PLAN 23-2)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019BVZd1M6WUf1YEy9gbAE1G
This commit is contained in:
2026-09-29 19:45:22 +09:00
co-authored by Claude Sonnet 5.5
parent 77754ad5f0
commit be93410299
5 changed files with 56 additions and 3 deletions
+15 -3
View File
@@ -368,7 +368,16 @@ export async function openRouteEditModal(
/** 지금 편집값을 사진 한 벌로 담는다 — 되돌리기가 쌓아 두는 것. */
function snapshotNow(): RouteEditSnapshot {
return { planned, curveOn, curveRadius, curveLock, curveArc, apexLock, picked };
return {
planned,
curveOn,
curveRadius,
curveLock,
curveArc,
apexLock,
picked,
rec: initial.tag(),
};
}
/** 사진 한 벌을 편집값으로 되살린다. 되돌리기는 새 걸음이 아니고, 초기값 싣기는 새 걸음이다. */
@@ -380,7 +389,10 @@ export async function openRouteEditModal(
const historyControls = bindHistoryControls({
overlay,
getHistory: () => history,
restore: (snapshot, message) => restoreSnapshot(snapshot, message, false),
restore: (snapshot, message) => {
initial.restoreTag(snapshot.rec); // 「현재 추천」도 그 걸음 것으로 — 노선과 짝.
restoreSnapshot(snapshot, message, false);
},
});
// ── 조작 — 노드 끌기 · 집기는 `_Pointer` 몫 / 두 번 클릭 삽입 / 오른쪽 클릭 삭제 ──
@@ -494,9 +506,9 @@ export async function openRouteEditModal(
({ nodes: nodeInfo, curves: curveInfo } = flat);
picked = -1;
// 여기가 [연 상태로]가 돌아갈 자리다 — 창을 연 그대로.
initial.ready(snapshotNow(), plan.edited, plan.current_initial ?? null);
history = createRouteEditHistory(snapshotNow());
historyControls.sync();
initial.ready(snapshotNow(), plan.edited, plan.current_initial ?? null);
syncCurveBar();
if (!planned.length) planned = plannedLine.map((vertex) => [vertex[0], vertex[1]]);
meta = drainage.meta;
@@ -24,6 +24,8 @@ export interface RouteEditSnapshot {
/** 교각점(꺾임점) 자리를 못 박았나 — 켜진 자리는 끌어도 안 움직인다(2026-09-12 지시 ①). */
apexLock: boolean[];
picked: number;
/** 이 걸음의 「현재 추천」 — `_Initial` 이 적고 되돌릴 때 되살린다(뜻은 그쪽만 안다). 노선과 짝. */
rec?: unknown;
}
/** 쌓아 둘 사진 수 상한 — 넘으면 오래된 것부터 버린다. */
@@ -54,6 +56,7 @@ function clone(snapshot: RouteEditSnapshot): RouteEditSnapshot {
curveArc: [...snapshot.curveArc],
apexLock: [...snapshot.apexLock],
picked: snapshot.picked,
rec: snapshot.rec,
};
}
@@ -57,6 +57,9 @@ export interface InitialPanel {
weights: RouteInitialWeights | null;
criteria: RouteInitialCriteria | null;
};
/** 지금 추천 — 편집 걸음(사진)에 실어 둔다. 되돌리기 · 다시하기가 `restoreTag` 로 되살린다. */
tag: () => InitialChoice | null;
restoreTag: (tag: unknown) => void;
/** [취소] — 이 창에서 바꾼 추천을 창을 열 때 것으로 되돌린다. */
cancel: () => void;
/** [확인] 뒤 결과 캐시를 비웠다 — 추천은 새 노선의 출발점이라 다시 적는다. */
@@ -199,6 +202,13 @@ export function createInitialPanel(params: InitialPanelParams): InitialPanel {
const criteria = choice ? choice.criteriaOverrides : base.criteria;
return { mode: now.logic, weights: now.weights, criteria };
},
tag: () => choice,
restoreTag(tag) {
if (tag === undefined) return; // 추천을 안 실은 옛 걸음 — 그대로 둔다.
choice = tag as InitialChoice | null;
writeInitialChoice(projectId, choice);
render();
},
cancel: () => writeInitialChoice(projectId, openingChoice),
keep: () => writeInitialChoice(projectId, choice),
};
@@ -238,6 +238,23 @@ const mapLines = {
oneDot: model.mapPaths([{ kind: "preview", points: [[5, 5]] }], 460, 340),
};
// ⑦ 되돌리기 · 다시하기가 걸음마다 실은 「현재 추천」을 되살린다(노선과 짝).
const history = loadTs(path.join(ROOT, "B05_Profile", "B05_Profile_UI_RouteEdit_History.ts"));
const step = (x, rec) => ({
planned: [[x, 0], [x + 1, 1]],
curveOn: [true, true],
curveRadius: [null, null],
curveLock: [null, null],
curveArc: [null, null],
apexLock: [false, false],
picked: -1,
rec,
});
const trail = history.createRouteEditHistory(step(0, { logic: "follow" }));
trail.commit(step(1, { logic: "earthwork" }));
trail.commit(step(2, null));
const historyRecs = [trail.undo(), trail.undo(), trail.redo(), trail.redo()].map((one) => one.rec);
// ④ 캐시 왕복 — 결과 통 프로젝트 칸. 꼴이 틀린 값은 안 읽는다.
model.writeInitialChoice("p-1", plain);
const readBack = model.readInitialChoice("p-1");
@@ -278,5 +295,6 @@ process.stdout.write(
sameAsServer,
initialRoute,
mapLines,
historyRecs,
}),
);
@@ -182,3 +182,13 @@ def test_도구_막대는_노선_추천_하나만():
).read()
for name in ("follow", "earthwork", "gentle", "weighted"):
assert f'"{name}"' in recommend
def test_되돌리기_다시하기가_현재_추천을_함께_되살림(results):
"""걸음마다 「현재 추천」이 노선과 짝 — 추천 없음(null)도 그대로 되돌아온다."""
assert results["historyRecs"] == [
{"logic": "earthwork"},
{"logic": "follow"},
{"logic": "earthwork"},
None,
]