refactor(B05·B06): 종단도 700줄 정리 — 오버레이 · 스크롤 듣개 새 파일로 떼어 641줄(213-3)
- design_profile_overlays.ts — 유토곡선 · 테이블 오버레이 · 유토 총괄 배지 만들기 - design_profile_viewport.ts — 가로 스크롤 다시 그림 · 휠 가로 스크롤 · 크기 바뀜 듣개(dispose 때 뗌) - 동작 그대로 · test_215_1 줄 수 기준 = 700(규칙) · 새 두 파일도 검사 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3mdb91125mPoC6tpsQx4P
This commit is contained in:
@@ -15,13 +15,10 @@ import { routeSpansFromStructures } from "../../B05_Profile/B05_Profile_UI_Drain
|
||||
import { readState, writeState } from "../b_page_state";
|
||||
import type { SectionDetailResponse } from "../../B06_Section/B06_Section_Api_Fetch";
|
||||
import { createWorkflowPanelHandle } from "@ui/ui_template_overlay";
|
||||
import { createMassHaulBadge } from "@util/common_util_mass_haul_badge";
|
||||
import { createHaulPlanPrefetch } from "../../B05_Profile/B05_Profile_Api_HaulPlan";
|
||||
import type {
|
||||
StructureInstance,
|
||||
StructureType,
|
||||
} from "../../B05_Profile/B05_Profile_Api_Structures";
|
||||
import { createProfileTableOverlay } from "../../B05_Profile/B05_Profile_UI_Profile_TableOverlay";
|
||||
import {
|
||||
hasLegacyAlignment,
|
||||
readAlignment,
|
||||
@@ -56,11 +53,7 @@ import {
|
||||
structureIdKeeping,
|
||||
} from "../../B05_Profile/B05_Profile_UI_Profile_Structures";
|
||||
import { configureBalloonOffsets } from "@util/common_util_mass_haul_balance_view";
|
||||
import {
|
||||
createRouteMassHaulPanel,
|
||||
type MassHaulSummaryValues,
|
||||
type RouteMassHaulContext,
|
||||
} from "../../B05_Profile/B05_Profile_UI_Profile_MassHaul";
|
||||
import type { RouteMassHaulContext } from "../../B05_Profile/B05_Profile_UI_Profile_MassHaul";
|
||||
import { renderBalanceBar } from "../../B05_Profile/B05_Profile_UI_Profile_Balance";
|
||||
import { createHeightCascade } from "../../B05_Profile/B05_Profile_UI_Profile_Heights";
|
||||
import { renderProfile } from "../../B05_Profile/B05_Profile_UI_Profile_Render";
|
||||
@@ -83,10 +76,11 @@ import {
|
||||
snapToMasterStations,
|
||||
} from "../../B05_Profile/B05_Profile_UI_Profile_Guard";
|
||||
import type { RouteProfilePanelCallbacks } from "../../B05_Profile/B05_Profile_UI_Profile_Panel_Types";
|
||||
import { attachWheelHorizontalScroll } from "../../B05_Profile/B05_Profile_UI_Profile_Wheel";
|
||||
import { needsFullRedraw, type ElevationWindowResult } from "@util/common_util_chart_ywindow";
|
||||
import type { ElevationWindowResult } from "@util/common_util_chart_ywindow";
|
||||
import type { DesignSideBoard, DesignSideFactory, DesignView } from "./design_profile_slots";
|
||||
import { noticeDroppedEdits } from "./design_save";
|
||||
import { createProfileOverlays } from "./design_profile_overlays";
|
||||
import { bindProfileViewport } from "./design_profile_viewport";
|
||||
import "../../B06_Section/B06_Section_UI_Style.css";
|
||||
// SVG 차트 색상(.b06-chart__*)의 정의처는 _Style_Cross.css다. 이걸 빼면 B05로 바로 진입했을 때
|
||||
// 배경 rect가 브라우저 기본 fill(검정)로 그려진다 — B06을 먼저 방문해야 정상으로 보이던 원인.
|
||||
@@ -162,40 +156,14 @@ export function createDesignProfile(ctx: DesignProfileContext) {
|
||||
if (drag.isSubDragging()) scheduleLightSync();
|
||||
else draw();
|
||||
};
|
||||
/** 유토곡선 총괄값 — **그래프 좌측 상단 배지**가 보인다(2026-09-06 사용자 지시).
|
||||
* 유토곡선을 접어도 이 값은 남아야 한다. */
|
||||
const massBadge = createMassHaulBadge();
|
||||
// 그래프 위에 도구줄(상단줄)이 한 줄 있다 — 그 아래로 내려 겹치지 않게 한다.
|
||||
massBadge.root.style.setProperty("--mass-haul-badge-top", "34px");
|
||||
bodyWrap.append(massBadge.root);
|
||||
let massHaulSummary: MassHaulSummaryValues | null = null;
|
||||
// 유토 배분은 **서버가 만든다**(2026-09-06) — 편집이 멈추면 뒤에서 받아 두고, 도착하면
|
||||
// 곡선을 다시 그린다. 그 산식이 브라우저 번들에 안 실리는 것이 이 구조의 목적이다.
|
||||
const haulPrefetch = createHaulPlanPrefetch(
|
||||
// 유토곡선 · 테이블 오버레이 · 유토 배지는 `design_profile_overlays` 몫(700줄 한계).
|
||||
const { massHaul, tableOverlay } = createProfileOverlays({
|
||||
projectId,
|
||||
() => routeId ?? undefined,
|
||||
subPanelChanged,
|
||||
);
|
||||
const massHaul = createRouteMassHaulPanel(
|
||||
subPanelChanged,
|
||||
(summary) => {
|
||||
const before = massHaulSummary;
|
||||
massHaulSummary = summary;
|
||||
if (before?.finalM3 !== summary?.finalM3) massBadge.set(summary);
|
||||
},
|
||||
haulPrefetch,
|
||||
);
|
||||
bodyWrap.append(massHaul.overlay, massHaul.handle);
|
||||
// 오버레이의 가로 스크롤을 종단 스크롤러와 양방향 동기화 — 측점 세로선 정렬 유지.
|
||||
massHaul.attachScrollSync(body);
|
||||
// 테이블도 유토곡선과 같은 오버레이 서브패널 — 유토곡선 위에 쌓인다(2026-08-05 사용자 지시).
|
||||
const tableOverlay = createProfileTableOverlay(subPanelChanged);
|
||||
bodyWrap.append(tableOverlay.overlay, tableOverlay.handle);
|
||||
tableOverlay.attachScrollSync(body);
|
||||
// 유토곡선·테이블 영역에서 브라우저 기본 우클릭 메뉴를 막는다(2026-08-05 사용자 지시).
|
||||
// 종단 그래프의 구조물 메뉴는 chartWrap에서 이미 preventDefault 후 자체 메뉴를 띄운다.
|
||||
body.addEventListener("contextmenu", (event) => event.preventDefault());
|
||||
massHaul.overlay.addEventListener("contextmenu", (event) => event.preventDefault());
|
||||
routeId: () => routeId,
|
||||
bodyWrap,
|
||||
body,
|
||||
onChange: subPanelChanged,
|
||||
});
|
||||
const content = document.createElement("div");
|
||||
content.className = "b05-route-profile__content";
|
||||
/** 횡단배수 최소 계획고 대상(배수관·BOX암거) — 관 목록이 바뀔 때 갱신한다. */
|
||||
@@ -463,44 +431,17 @@ export function createDesignProfile(ctx: DesignProfileContext) {
|
||||
});
|
||||
}
|
||||
|
||||
/** 그래프를 통째로 다시 만들 때까지 기다리는 시간(ms) — 눈금 갱신으로도 못 살릴 때만. */
|
||||
const SCROLL_SETTLE_MS = 80;
|
||||
/** 마지막으로 통째로 다시 만든 가로 위치 — 같은 자리면 다시 그리지 않는다. */
|
||||
let settledScrollLeft = 0;
|
||||
let scrollSettleTimer = 0;
|
||||
// 세로 맞춤은 **그리기가 아니라 변환**이다(2026-09-04 사용자 확정) — 스크롤마다 겹 하나의
|
||||
// 변환만 갈아 끼우므로 실시간으로 따라온다. 눈금 간격이 어긋나면 눈금층(요소 20개 안팎)만
|
||||
// 새로 만든다. 그래프를 통째로 다시 만드는 34ms 짜리 재구성은 그래도 안 될 때만 남는다.
|
||||
body.addEventListener("scroll", () => {
|
||||
if (elevationHold.editing()) return;
|
||||
const fitted = updateElevationWindow?.() ?? null;
|
||||
window.clearTimeout(scrollSettleTimer);
|
||||
if (!needsFullRedraw(fitted)) return;
|
||||
scrollSettleTimer = window.setTimeout(() => {
|
||||
if (elevationHold.editing() || Math.abs(body.scrollLeft - settledScrollLeft) < 1) return;
|
||||
settledScrollLeft = body.scrollLeft;
|
||||
draw();
|
||||
}, SCROLL_SETTLE_MS);
|
||||
// 가로 스크롤 · 휠 · 크기 바뀜은 `design_profile_viewport` 몫(700줄 한계).
|
||||
const unbindViewport = bindProfileViewport({
|
||||
body,
|
||||
editing: () => elevationHold.editing(),
|
||||
fitElevation: () => updateElevationWindow?.() ?? null,
|
||||
draw: () => draw(),
|
||||
lastSize: () => ({ width: lastWidth, height: lastHeight }),
|
||||
scheduleLightSync,
|
||||
scheduleDraw: () => drag.scheduleDraw(),
|
||||
});
|
||||
|
||||
// 종단면도는 가로로 매우 길다. 세로 휠을 가로 스크롤로 돌려 스크롤바를 잡지 않고도
|
||||
// 노선을 훑을 수 있게 한다 (Shift+휠은 브라우저 기본 가로 스크롤이라 그대로 둔다).
|
||||
attachWheelHorizontalScroll(body);
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
if (
|
||||
body.clientWidth <= 0 ||
|
||||
body.clientHeight <= 0 ||
|
||||
(Math.abs(body.clientWidth - lastWidth) < 1 && Math.abs(body.clientHeight - lastHeight) < 1)
|
||||
)
|
||||
return;
|
||||
// 끌리는 동안엔 프레임당 경량 높이 동기화만 — 무거운 전체 재구성은 손을 뗀 뒤 한 번.
|
||||
// (150ms 디바운스만 있던 예전 방식은 중간 프레임이 없어 툭툭 끊겼다. 2026-08-05 사용자 보고)
|
||||
scheduleLightSync();
|
||||
drag.scheduleDraw();
|
||||
});
|
||||
resizeObserver.observe(body);
|
||||
|
||||
// 접기 — 두 보기가 같은 키 하나를 쓴다(`_Profile_Drag` 몫).
|
||||
bindProfileCollapse(root, panelHandle, () => requestAnimationFrame(draw));
|
||||
|
||||
@@ -691,7 +632,7 @@ export function createDesignProfile(ctx: DesignProfileContext) {
|
||||
setListening(false);
|
||||
drag.dispose();
|
||||
crossPreview.dispose();
|
||||
resizeObserver.disconnect();
|
||||
unbindViewport();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
/* =============================================================================
|
||||
* design_profile_overlays.ts
|
||||
* 종단도 바닥 오버레이 서브패널 — 유토곡선 · 테이블 · 유토 총괄 배지(213-3 · 700줄 한계로 뗌).
|
||||
* 만들어 `bodyWrap` 에 붙이고 종단 스크롤과 잇는 데까지 — 그리기 · 높이는 종단도 몫.
|
||||
* ========================================================================== */
|
||||
|
||||
import { createMassHaulBadge } from "@util/common_util_mass_haul_badge";
|
||||
import { createHaulPlanPrefetch } from "../../B05_Profile/B05_Profile_Api_HaulPlan";
|
||||
import { createProfileTableOverlay } from "../../B05_Profile/B05_Profile_UI_Profile_TableOverlay";
|
||||
import {
|
||||
createRouteMassHaulPanel,
|
||||
type MassHaulSummaryValues,
|
||||
} from "../../B05_Profile/B05_Profile_UI_Profile_MassHaul";
|
||||
|
||||
export interface ProfileOverlaysDeps {
|
||||
projectId: string;
|
||||
routeId: () => number | null;
|
||||
/** 종단 열(요약 줄 · 본문 · 서클을 담은 칸) — 오버레이가 그 위를 덮는다. */
|
||||
bodyWrap: HTMLElement;
|
||||
/** 종단 스크롤러 — 오버레이 가로 스크롤을 이것과 맞춘다. */
|
||||
body: HTMLElement;
|
||||
/** 서브패널이 바뀜(펼침 · 범례 · 리사이저 · 유토 배분 도착). */
|
||||
onChange: () => void;
|
||||
}
|
||||
|
||||
export function createProfileOverlays(deps: ProfileOverlaysDeps) {
|
||||
const { bodyWrap, body, onChange } = deps;
|
||||
/** 유토곡선 총괄값 — **그래프 좌측 상단 배지**가 보인다(2026-09-06 사용자 지시).
|
||||
* 유토곡선을 접어도 이 값은 남아야 한다. */
|
||||
const massBadge = createMassHaulBadge();
|
||||
// 그래프 위에 도구줄(상단줄)이 한 줄 있다 — 그 아래로 내려 겹치지 않게 한다.
|
||||
massBadge.root.style.setProperty("--mass-haul-badge-top", "34px");
|
||||
bodyWrap.append(massBadge.root);
|
||||
let massHaulSummary: MassHaulSummaryValues | null = null;
|
||||
// 유토 배분은 **서버가 만든다**(2026-09-06) — 편집이 멈추면 뒤에서 받아 두고, 도착하면
|
||||
// 곡선을 다시 그린다. 그 산식이 브라우저 번들에 안 실리는 것이 이 구조의 목적이다.
|
||||
const haulPrefetch = createHaulPlanPrefetch(
|
||||
deps.projectId,
|
||||
() => deps.routeId() ?? undefined,
|
||||
onChange,
|
||||
);
|
||||
const massHaul = createRouteMassHaulPanel(
|
||||
onChange,
|
||||
(summary) => {
|
||||
const before = massHaulSummary;
|
||||
massHaulSummary = summary;
|
||||
if (before?.finalM3 !== summary?.finalM3) massBadge.set(summary);
|
||||
},
|
||||
haulPrefetch,
|
||||
);
|
||||
bodyWrap.append(massHaul.overlay, massHaul.handle);
|
||||
// 오버레이의 가로 스크롤을 종단 스크롤러와 양방향 동기화 — 측점 세로선 정렬 유지.
|
||||
massHaul.attachScrollSync(body);
|
||||
// 테이블도 유토곡선과 같은 오버레이 서브패널 — 유토곡선 위에 쌓인다(2026-08-05 사용자 지시).
|
||||
const tableOverlay = createProfileTableOverlay(onChange);
|
||||
bodyWrap.append(tableOverlay.overlay, tableOverlay.handle);
|
||||
tableOverlay.attachScrollSync(body);
|
||||
// 유토곡선·테이블 영역에서 브라우저 기본 우클릭 메뉴를 막는다(2026-08-05 사용자 지시).
|
||||
// 종단 그래프의 구조물 메뉴는 chartWrap에서 이미 preventDefault 후 자체 메뉴를 띄운다.
|
||||
body.addEventListener("contextmenu", (event) => event.preventDefault());
|
||||
massHaul.overlay.addEventListener("contextmenu", (event) => event.preventDefault());
|
||||
return { massHaul, tableOverlay };
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/* =============================================================================
|
||||
* design_profile_viewport.ts
|
||||
* 종단 스크롤러의 가로 스크롤 · 휠 · 크기 바뀜 듣개(213-3 · 700줄 한계로 뗌).
|
||||
* 다시 그리기 · 높이 동기화는 종단도가 넘긴 함수를 부른다.
|
||||
* ========================================================================== */
|
||||
|
||||
import { attachWheelHorizontalScroll } from "../../B05_Profile/B05_Profile_UI_Profile_Wheel";
|
||||
import { needsFullRedraw, type ElevationWindowResult } from "@util/common_util_chart_ywindow";
|
||||
|
||||
/** 그래프를 통째로 다시 만들 때까지 기다리는 시간(ms) — 눈금 갱신으로도 못 살릴 때만. */
|
||||
const SCROLL_SETTLE_MS = 80;
|
||||
|
||||
export interface ProfileViewportDeps {
|
||||
body: HTMLElement;
|
||||
/** ▲▼ 계획고 편집으로 Y 축을 잡고 있는 중. */
|
||||
editing: () => boolean;
|
||||
/** 세로 창 갱신기 — 그릴 때마다 렌더러가 새로 준다(없으면 null). */
|
||||
fitElevation: () => ElevationWindowResult | null;
|
||||
draw: () => void;
|
||||
/** 마지막으로 그린 본문 크기 — 같으면 크기 바뀜을 무시한다. */
|
||||
lastSize: () => { width: number; height: number };
|
||||
scheduleLightSync: () => void;
|
||||
scheduleDraw: () => void;
|
||||
}
|
||||
|
||||
/** 듣개를 붙이고 떼는 함수를 돌려준다. */
|
||||
export function bindProfileViewport(deps: ProfileViewportDeps): () => void {
|
||||
const { body } = deps;
|
||||
/** 마지막으로 통째로 다시 만든 가로 위치 — 같은 자리면 다시 그리지 않는다. */
|
||||
let settledScrollLeft = 0;
|
||||
let scrollSettleTimer = 0;
|
||||
// 세로 맞춤은 **그리기가 아니라 변환**이다(2026-09-04 사용자 확정) — 스크롤마다 겹 하나의
|
||||
// 변환만 갈아 끼우므로 실시간으로 따라온다. 눈금 간격이 어긋나면 눈금층(요소 20개 안팎)만
|
||||
// 새로 만든다. 그래프를 통째로 다시 만드는 34ms 짜리 재구성은 그래도 안 될 때만 남는다.
|
||||
body.addEventListener("scroll", () => {
|
||||
if (deps.editing()) return;
|
||||
const fitted = deps.fitElevation();
|
||||
window.clearTimeout(scrollSettleTimer);
|
||||
if (!needsFullRedraw(fitted)) return;
|
||||
scrollSettleTimer = window.setTimeout(() => {
|
||||
if (deps.editing() || Math.abs(body.scrollLeft - settledScrollLeft) < 1) return;
|
||||
settledScrollLeft = body.scrollLeft;
|
||||
deps.draw();
|
||||
}, SCROLL_SETTLE_MS);
|
||||
});
|
||||
|
||||
// 종단면도는 가로로 매우 길다. 세로 휠을 가로 스크롤로 돌려 스크롤바를 잡지 않고도
|
||||
// 노선을 훑을 수 있게 한다 (Shift+휠은 브라우저 기본 가로 스크롤이라 그대로 둔다).
|
||||
attachWheelHorizontalScroll(body);
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
const last = deps.lastSize();
|
||||
if (
|
||||
body.clientWidth <= 0 ||
|
||||
body.clientHeight <= 0 ||
|
||||
(Math.abs(body.clientWidth - last.width) < 1 && Math.abs(body.clientHeight - last.height) < 1)
|
||||
)
|
||||
return;
|
||||
// 끌리는 동안엔 프레임당 경량 높이 동기화만 — 무거운 전체 재구성은 손을 뗀 뒤 한 번.
|
||||
// (150ms 디바운스만 있던 예전 방식은 중간 프레임이 없어 툭툭 끊겼다. 2026-08-05 사용자 보고)
|
||||
deps.scheduleLightSync();
|
||||
deps.scheduleDraw();
|
||||
});
|
||||
resizeObserver.observe(body);
|
||||
return () => resizeObserver.disconnect();
|
||||
}
|
||||
@@ -147,6 +147,7 @@ def test_줄_수_늘리지_않음():
|
||||
"A00_Common/design/design_profile.ts",
|
||||
"A00_Common/design/design_profile_free.ts",
|
||||
"A00_Common/design/design_profile_free_view.ts",
|
||||
"A00_Common/design/design_profile_overlays.ts",
|
||||
"A00_Common/design/design_profile_viewport.ts",
|
||||
):
|
||||
assert len(_read(relative).splitlines()) <= 700
|
||||
assert len(_read("A00_Common/design/design_profile.ts").splitlines()) <= 698
|
||||
assert len(_read(relative).splitlines()) <= 700 # 소스 700줄 초과 금지(213-3)
|
||||
|
||||
Reference in New Issue
Block a user