From 9da48d5e7f0314976a45d75068f896d5605c5c2f Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sat, 12 Sep 2026 20:34:26 +0900 Subject: [PATCH] =?UTF-8?q?perf(b06):=20=EA=B3=84=ED=9A=8D=EC=84=A0=20?= =?UTF-8?q?=E2=96=B2=E2=96=BC=20=EB=A5=BC=20=EB=88=84=EB=A5=B8=20=EC=A6=89?= =?UTF-8?q?=EC=8B=9C=20=EB=B0=98=EC=9D=91=20=E2=80=94=20=EC=B8=A1=EC=A0=90?= =?UTF-8?q?=20=EC=88=98=EB=A7=8C=ED=81=BC=20=EB=8B=A4=EC=8B=9C=20=EA=B7=B8?= =?UTF-8?q?=EB=A6=AC=EB=8D=98=20=EA=B2=83=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 재계산 결과 반영을 `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) Claude-Session: https://claude.ai/code/session_01Fe1QWPTfw11PaKh2LjwXdR --- B06_Section/B06_Section_UI_Page.ts | 37 ++++++++++++++++++---- B06_Section/B06_Section_UI_Section_View.ts | 28 +++++++++++++--- 2 files changed, 55 insertions(+), 10 deletions(-) diff --git a/B06_Section/B06_Section_UI_Page.ts b/B06_Section/B06_Section_UI_Page.ts index 78a37c90..ab248927 100644 --- a/B06_Section/B06_Section_UI_Page.ts +++ b/B06_Section/B06_Section_UI_Page.ts @@ -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 { 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 { */ 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[0]> > => { @@ -517,10 +531,21 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise { 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(); }, }; }; diff --git a/B06_Section/B06_Section_UI_Section_View.ts b/B06_Section/B06_Section_UI_Section_View.ts index 49afaa2f..3fbfdff2 100644 --- a/B06_Section/B06_Section_UI_Section_View.ts +++ b/B06_Section/B06_Section_UI_Section_View.ts @@ -107,6 +107,8 @@ export interface SectionViewController { ) => void; /** 측점 하나의 카드만 새로 만들어 교체한다 (전체 재렌더 없이 설계 변경 반영). */ refreshCard: (chainageM: number) => void; + /** 여러 측점을 한꺼번에 교체한다 — 상단 패널은 **마지막에 한 번만** 다시 그린다. */ + refreshCards: (chainages: ReadonlyArray) => 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): 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");