perf(b06): 계획선 ▲▼ 를 누른 즉시 반응 — 측점 수만큼 다시 그리던 것 제거
- 재계산 결과 반영을 `refreshCards` 로 묶음. 측점마다 `refreshCard` 를 부르면 그때마다 종단 그래프·유토곡선까지 다시 그려, 62 측점 기준 한 번 누를 때 종단도를 62 번 다시 그림. - ▲▼ 는 계획선(`design_profiles`)만 먼저 갈아 끼우고 다시 그림 — 선이 즉시 움직임. - 전 측점 횡단 재계산은 120ms 디바운스로 미룸(B05 프리뷰와 같은 규칙) — 길게 누르는 동안은 선만 따라오고, 손을 뗀 뒤 한 번만 돎. 실측(62 측점, 같은 노선·같은 조작) - 한 번 누름: 화면 갱신 920~955ms → 17~23ms · 최장 멈춤 879~971ms → 180~229ms - 1.5초 길게 누름: 멈춤 954ms×3회·누적 2,577ms → 최장 213ms·누적 473ms, 적용 단수 3 단 → 10 단(제 속도) - 값 동일성: 누가토량 배지·카드 3장·계획고 라벨·세션 초안 전부 변경 전과 일치 - `npm run typecheck` 통과 · `pytest resources/tester/` 1,323 passed / 22 skipped Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fe1QWPTfw11PaKh2LjwXdR
This commit is contained in:
@@ -40,7 +40,7 @@ import {
|
||||
readAlignmentDraft,
|
||||
type ProfileEditStore,
|
||||
} from "../B05_Profile/B05_Profile_UI_Profile_Edit";
|
||||
import { readAlignment } from "../B05_Profile/B05_Profile_UI_Profile_Data";
|
||||
import { readAlignment, toDesignProfile } from "../B05_Profile/B05_Profile_UI_Profile_Data";
|
||||
import {
|
||||
adjustStation,
|
||||
buildAlignment,
|
||||
@@ -340,7 +340,9 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise<void> {
|
||||
detail: sectionDetail,
|
||||
edits,
|
||||
});
|
||||
for (const chainageM of updated) sectionView.refreshCard(chainageM);
|
||||
// 카드는 한꺼번에 갈아 끼운다 — 측점마다 `refreshCard` 를 부르면 그때마다 종단
|
||||
// 그래프·유토곡선까지 다시 그려 측점 수만큼 화면이 멈췄다(2026-09-12).
|
||||
sectionView.refreshCards(updated);
|
||||
} catch (error) {
|
||||
const detail = error instanceof Error ? ` ${error.message}` : "";
|
||||
showToast(`${L("B06_Design_Failed")}${detail}`, "error");
|
||||
@@ -498,6 +500,18 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise<void> {
|
||||
*/
|
||||
let gradeStore: ProfileEditStore | null = null;
|
||||
let gradeRouteId: number | null = null;
|
||||
/** ▲▼ 길게 누르기는 초당 10회(`HOLD_INTERVAL_MS`) 들어온다 — 그보다 길게 잡아
|
||||
* 누르는 동안은 선만 움직이고, 손을 뗀 뒤 재계산이 한 번 돈다. */
|
||||
const GRADE_RECONCILE_DEBOUNCE_MS = 120;
|
||||
let gradeReconcileTimer = 0;
|
||||
const scheduleGradeReconcile = (): void => {
|
||||
window.clearTimeout(gradeReconcileTimer);
|
||||
gradeReconcileTimer = window.setTimeout(() => {
|
||||
void reconcileStaleDesigns({ force: true }).then(() =>
|
||||
sectionView.setGradeEdit(gradeEditFor),
|
||||
);
|
||||
}, GRADE_RECONCILE_DEBOUNCE_MS);
|
||||
};
|
||||
const gradeEditFor = (): ReturnType<
|
||||
NonNullable<Parameters<typeof sectionView.setGradeEdit>[0]>
|
||||
> => {
|
||||
@@ -517,10 +531,21 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise<void> {
|
||||
stepM: alignment.policy.edit_step_m,
|
||||
onStation: (chainageM, delta) => {
|
||||
store.replace(adjustStation(base, store.edits(), chainageM, delta));
|
||||
// 편집분은 세션 초안에 있으므로 재계산이 그것을 그대로 읽는다(강제로 한 번).
|
||||
void reconcileStaleDesigns({ force: true }).then(() =>
|
||||
sectionView.setGradeEdit(gradeEditFor),
|
||||
);
|
||||
// ① 선은 **그 자리에서** 움직인다 — 그래프가 읽는 계획선(`design_profiles`)만
|
||||
// 갈아 끼우고 다시 그린다(재계산을 기다리면 누른 뒤 한참 뒤에 움직였다).
|
||||
const detail = sectionDetail;
|
||||
if (detail) {
|
||||
detail.longitudinal.design_profiles = [
|
||||
toDesignProfile(
|
||||
buildAlignment(base, store.edits()),
|
||||
detail.longitudinal.design_profiles?.[0],
|
||||
),
|
||||
];
|
||||
}
|
||||
sectionView.setGradeEdit(gradeEditFor);
|
||||
// ② 전 측점 횡단 재계산·카드 갱신은 무겁다 — 마지막 한 번만(B05 프리뷰와 같은 규칙).
|
||||
// 편집분은 세션 초안에 있으므로 재계산이 그것을 그대로 읽는다.
|
||||
scheduleGradeReconcile();
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -107,6 +107,8 @@ export interface SectionViewController {
|
||||
) => void;
|
||||
/** 측점 하나의 카드만 새로 만들어 교체한다 (전체 재렌더 없이 설계 변경 반영). */
|
||||
refreshCard: (chainageM: number) => void;
|
||||
/** 여러 측점을 한꺼번에 교체한다 — 상단 패널은 **마지막에 한 번만** 다시 그린다. */
|
||||
refreshCards: (chainages: ReadonlyArray<number>) => void;
|
||||
/** 좌측 구조물 목록에서 고른 측점 카드를 선택하고 화면에 드러낸다 — 재클릭 토글 없음
|
||||
* (2026-08-29 B05/B06 일원화: 목록 클릭 → 해당 카드 스크롤·강조). */
|
||||
focusStation: (stationId: string) => void;
|
||||
@@ -601,18 +603,35 @@ export function createSectionView(
|
||||
chartWrap.scrollLeft = keepScrollLeft;
|
||||
};
|
||||
|
||||
const refreshCard = (chainageM: number): void => {
|
||||
if (!currentDetail) return;
|
||||
/** 카드 한 장만 갈아 끼운다(상단 패널은 안 건드린다). 못 찾으면 false. */
|
||||
const rebuildCard = (chainageM: number): boolean => {
|
||||
if (!currentDetail) return false;
|
||||
const section = currentDetail.cross_sections.find(
|
||||
(candidate) => Math.abs(candidate.chainage_m - chainageM) < 0.01,
|
||||
);
|
||||
if (!section) return;
|
||||
if (!section) return false;
|
||||
const existing = document.getElementById(`cross-${section.station_id}`);
|
||||
// 단건 갱신은 draw에서 정해둔 행 높이를 재사용해 같은 행 카드와 높이를 유지한다.
|
||||
if (existing)
|
||||
existing.replaceWith(buildCrossCard(section, cachedRowHeight.get(section.station_id)));
|
||||
return true;
|
||||
};
|
||||
|
||||
const refreshCard = (chainageM: number): void => {
|
||||
// 단면적이 바뀌면 유토곡선도 함께 흔들리므로 상단 패널만 다시 그린다(카드 전체 재렌더 없음).
|
||||
drawPanel();
|
||||
if (rebuildCard(chainageM)) drawPanel();
|
||||
};
|
||||
|
||||
/**
|
||||
* 여러 측점을 한 번에 갈아 끼운다 — **상단 패널은 끝에 한 번만** 다시 그린다.
|
||||
*
|
||||
* 측점마다 `refreshCard` 를 부르면 측점 수만큼 종단 그래프·유토곡선을 다시 그려,
|
||||
* 계획선 ▲▼ 한 번에 화면이 몇 백 ms 씩 멈췄다(2026-09-12 B05 대비 느림 원인).
|
||||
*/
|
||||
const refreshCards = (chainages: ReadonlyArray<number>): void => {
|
||||
let touched = false;
|
||||
for (const chainageM of chainages) if (rebuildCard(chainageM)) touched = true;
|
||||
if (touched) drawPanel();
|
||||
};
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
@@ -658,6 +677,7 @@ export function createSectionView(
|
||||
if (renderWidth <= 0) requestAnimationFrame(() => resizeObserver.observe(root));
|
||||
},
|
||||
refreshCard,
|
||||
refreshCards,
|
||||
focusStation(stationId) {
|
||||
if (selectedStationId !== stationId) selectStation(stationId, true);
|
||||
else revealCard(stationId, "smooth");
|
||||
|
||||
Reference in New Issue
Block a user