diff --git a/A00_Common/design/design_profile.ts b/A00_Common/design/design_profile.ts index def0e97d1..22322e4c1 100644 --- a/A00_Common/design/design_profile.ts +++ b/A00_Common/design/design_profile.ts @@ -121,7 +121,8 @@ export interface DesignProfileContext { * 계산이 브라우저 안에서 끝나면서(2026-09-03) 서버 왕복이 사라져 250→60ms 로 줄였다 — * 실측 전 측점(128) 재계산 7.5ms 라 손을 떼는 즉시 유토곡선이 따라온다. * 60→16ms(2026-09-06) — ▲ 한 번에 라벨이 서기까지 중앙 130ms 를 쟀는데 그중 60ms 가 - * 이 대기였다. 한 프레임(16ms)이면 연속 입력은 여전히 마지막 값만 계산한다. */ + * 이 대기였다. 한 프레임(16ms)이면 연속 입력은 여전히 마지막 값만 계산한다. + * 횡단 보기는 120ms — 계산마다 카드를 갈아 끼워 길게 누르면 멈춤이 쌓임(9da48d5e7 · 54-22). */ const CROSS_PREVIEW_DEBOUNCE_MS = 16; export function createDesignProfile(ctx: DesignProfileContext) { @@ -384,7 +385,7 @@ export function createDesignProfile(ctx: DesignProfileContext) { detail: () => detail, routeId: () => routeId, edits: () => store.edits(), - debounceMs: CROSS_PREVIEW_DEBOUNCE_MS, + debounceMs: () => (root.dataset.view === "cross" ? 120 : CROSS_PREVIEW_DEBOUNCE_MS), onApplied: (updated) => { draw(); callbacks?.onCrossDesignsUpdated?.(updated); diff --git a/B05_Profile/B05_Profile_UI_Profile_Preview.ts b/B05_Profile/B05_Profile_UI_Profile_Preview.ts index 3e9cbc464..ac59faf63 100644 --- a/B05_Profile/B05_Profile_UI_Profile_Preview.ts +++ b/B05_Profile/B05_Profile_UI_Profile_Preview.ts @@ -18,7 +18,8 @@ export interface CrossPreviewContext { detail: () => SectionDetailResponse | null; routeId: () => number | null; edits: () => AlignmentEdits; - debounceMs: number; + /** 부를 때마다 읽음 — 보기마다 다름. */ + debounceMs: () => number; /** 반영이 끝난 뒤 다시 그린다 — 다시 계산한 측점 누가거리를 넘긴다(횡단 카드 갱신용). */ onApplied: (updated: number[]) => void; } @@ -61,7 +62,7 @@ export function createCrossPreview(ctx: CrossPreviewContext): CrossPreview { .catch(() => { /* 프리뷰 실패는 무시 — 화면의 계획선은 그대로 두고 다음 편집에서 다시 시도한다. */ }); - }, ctx.debounceMs); + }, ctx.debounceMs()); }, dispose() { window.clearTimeout(timer); diff --git a/B06_Section/B06_Section_Cross_Refresh.ts b/B06_Section/B06_Section_Cross_Refresh.ts index 390129c2b..16f1a143b 100644 --- a/B06_Section/B06_Section_Cross_Refresh.ts +++ b/B06_Section/B06_Section_Cross_Refresh.ts @@ -124,8 +124,11 @@ function preserveUserFields( /** * 전 측점 횡단을 현재 계획선으로 다시 계산해 `detail.cross_sections[].design`을 제자리 교체한다. * 돌려주는 값은 실제로 바뀐 측점의 누가거리 목록 — 호출한 쪽이 그 카드만 다시 그리면 된다. + * 계획선 ▲ 한 번에 값이 바뀌는 측점은 몇 개뿐 — 전부 돌려주면 카드를 다 갈아 끼워 멈춤(54-22). */ export async function refreshCrossDesigns(input: CrossRefreshInput): Promise { + const sections = input.detail.cross_sections; + const before = new Map(sections.map((section) => [section, JSON.stringify(section.design)])); const updated = refreshLocally(input) ?? (await refreshFromServer(input)); // 구조물이 선 측점은 폐회로 면적이 정본이다 — 설계를 갈아 끼운 **이 자리에서 한 번** 다시 // 얹는다. 부르는 쪽마다 얹으면 B06 흐름만 얹고 B05 는 안 얹어 두 보기의 유토 뱃지가 @@ -136,7 +139,9 @@ export async function refreshCrossDesigns(input: CrossRefreshInput): Promise JSON.stringify(section.design) !== before.get(section)) + .map((section) => section.chainage_m); } /** diff --git a/B06_Section/B06_Section_UI_Section_View.ts b/B06_Section/B06_Section_UI_Section_View.ts index 47097869c..b857bea3e 100644 --- a/B06_Section/B06_Section_UI_Section_View.ts +++ b/B06_Section/B06_Section_UI_Section_View.ts @@ -121,7 +121,8 @@ export function createSectionView( else revealCard(stationId, "smooth"); }, edit: () => structureEdit, - refreshCards: (chainages) => refreshCards(chainages), + // 종단도는 보낸 쪽(계획고 편집 반영)이 이미 다시 그림 — 카드만(54-22) + refreshCards: (chainages) => refreshCards(chainages, false), }, shared as DesignPageShared, ); @@ -390,10 +391,12 @@ export function createSectionView( * 측점마다 `refreshCard` 를 부르면 측점 수만큼 종단 그래프·유토곡선을 다시 그려, * 계획선 ▲▼ 한 번에 화면이 몇 백 ms 씩 멈췄다(2026-09-12 B05 대비 느림 원인). */ - const refreshCards = (chainages: ReadonlyArray): void => { + const refreshCards = (chainages: ReadonlyArray, redraw = true): void => { let touched = false; for (const chainageM of chainages) if (rebuildCard(chainageM)) touched = true; - if (touched) redrawProfile(); + if (!touched || !currentDetail) return; + if (redraw) redrawProfile(); + else fillSlopeNotice.update(currentDetail.cross_sections, cachedStationInterval); }; const resizeObserver = new ResizeObserver(() => { diff --git a/B06_Section/B06_Section_UI_Section_View_Profile.ts b/B06_Section/B06_Section_UI_Section_View_Profile.ts index 00b985789..9ea0b7731 100644 --- a/B06_Section/B06_Section_UI_Section_View_Profile.ts +++ b/B06_Section/B06_Section_UI_Section_View_Profile.ts @@ -180,7 +180,10 @@ export function createSectionProfile( profile: { onSelectStation: (stationId) => host.selectCard(cardOf(stationId)), callbacks: { - onCrossDesignsUpdated: (updated) => host.refreshCards(updated), + // 숨은 횡단 보기는 건너뜀 — 다시 보일 때 정합이 카드를 맞춤(onShow) + onCrossDesignsUpdated: (updated) => { + if (shared.view() === "cross") host.refreshCards(updated); + }, onStructureMove: (from, to, station) => { if (isPipeStation(station)) host.edit()?.movePipe(from, to); }, diff --git a/resources/tester/ui_checks/54-22_횡단_올림_멈춤.js b/resources/tester/ui_checks/54-22_횡단_올림_멈춤.js new file mode 100644 index 000000000..736645f6c --- /dev/null +++ b/resources/tester/ui_checks/54-22_횡단_올림_멈춤.js @@ -0,0 +1,83 @@ +/* 54-22 — 종단 점 ▲ 누름 멈춤(긴 작업) 재기: B06 은 120ms 디바운스 · 종단도 한 번 · B05 는 실시간 그대로 + * 시작 화면: B06(#/b06-section) 또는 B05(#/b05-profile) · 검증 프로젝트 · 로그인 + * 부르는 법: 단계마다 한 번씩 — 앞에 `window.__m5422 = "단계";` · 긴 단계는 `void` 로 띄우고 + * `sessionStorage.getItem("__5422_out_{단계}")` 로 읽음 + * snap → 처음 초안(세션 원문)을 `__5422_orig` 에 남김(이미 있으면 그대로) + * one → 가운데 측점 ▲ 한 번 — 긴 작업 최장 · 합 · 횡단 재계산까지 걸린 때 + * hold → 가운데 측점 ▲ 2.5초 길게 — 긴 작업 최장 · 합 · 재계산 횟수 + * restore → 처음 초안 원문으로 되돌림 → 그 뒤 orca reload + * 판정: B06 one 최장 ≈200ms 안팎 · hold 합 ≈0.5초 안팎(고치기 전 587 / 2,684ms) · + * B05 one 재계산 때 = 16ms 디바운스 근처(실시간) + * 쓰는 것: sessionStorage(계획선 초안) — 정본(서버)은 안 씀 + */ +(async () => { + const MODE = window.__m5422 || "one"; + const w = (ms) => new Promise((r) => setTimeout(r, ms)); + const DRAFTS = /^aislo:draft:/; + const out = { mode: MODE, page: location.hash }; + const save = () => sessionStorage.setItem(`__5422_out_${MODE}`, JSON.stringify(out)); + + if (MODE === "snap") { + if (!sessionStorage.getItem("__5422_orig")) { + const orig = Object.fromEntries( + Object.keys(sessionStorage) + .filter((k) => DRAFTS.test(k)) + .map((k) => [k, sessionStorage.getItem(k)]), + ); + sessionStorage.setItem("__5422_orig", JSON.stringify(orig)); + } + out.keys = Object.keys(JSON.parse(sessionStorage.getItem("__5422_orig"))); + save(); + return JSON.stringify(out); + } + if (MODE === "restore") { + const orig = JSON.parse(sessionStorage.getItem("__5422_orig") || "null"); + if (!orig) return "no orig"; + for (const k of Object.keys(sessionStorage)) if (DRAFTS.test(k)) sessionStorage.removeItem(k); + for (const [k, v] of Object.entries(orig)) sessionStorage.setItem(k, v); + for (const k of Object.keys(sessionStorage)) + if (k.startsWith("__5422")) sessionStorage.removeItem(k); + return JSON.stringify({ restored: Object.keys(orig) }); + } + + const ups = [...document.querySelectorAll(".b05-profile-edit__btn.is-up")]; + const button = ups[Math.floor(ups.length / 2)]; + if (!button) { + out.error = "▲ 없음"; + save(); + return JSON.stringify(out); + } + out.button = button.title.split("\n")[0]; + const tasks = []; + const observer = new PerformanceObserver((list) => + list.getEntries().forEach((e) => tasks.push(Math.round(e.duration))), + ); + observer.observe({ type: "longtask" }); + // 횡단 재계산이 카드 · 캐시에 닿은 때 — 계획선 초안 말고 다른 캐시 쓰기 + const writes = []; + const t0 = performance.now(); + const origSet = Storage.prototype.setItem; + Storage.prototype.setItem = function (k, v) { + if (this === sessionStorage && !k.startsWith("__5422")) + writes.push([Math.round(performance.now() - t0), k.replace(/:[0-9a-f-]{36}.*/, "")]); + return origSet.call(this, k, v); + }; + // 화면이 바뀐 때(ms) — 누른 뒤 재계산 반영이 닿는 때를 봄(B05 ≈16ms 디바운스 · B06 ≈120ms) + const domAt = new Set(); + const mo = new MutationObserver(() => domAt.add(Math.round(performance.now() - t0))); + mo.observe(document.body, { subtree: true, childList: true, characterData: true }); + button.dispatchEvent(new PointerEvent("pointerdown", { bubbles: true, pointerId: 1 })); + await w(MODE === "hold" ? 2500 : 60); + window.dispatchEvent(new PointerEvent("pointerup", { bubbles: true, pointerId: 1 })); + await w(2500); + Storage.prototype.setItem = origSet; + observer.disconnect(); + mo.disconnect(); + out.domAt = [...domAt].slice(0, 12); + out.longTasks = tasks; + out.max = tasks.length ? Math.max(...tasks) : 0; + out.sum = tasks.reduce((a, b) => a + b, 0); + out.writes = writes; + save(); + return JSON.stringify(out); +})();