perf(B05/B06): 계획선 프리뷰 응답 1.09MB → 6KB, 곡선 셀 기본 표시 원복
프리뷰 성능 - 느린 원인은 응답 크기였다(영구저장소 저장·재로드는 하지 않는다). 상세를 통째로 돌려주느라 1.09MB였고, 유토곡선이 실제로 읽는 설계 필드만 남겨 6KB로 줄였다. 제외: 지반선 원시 샘플, 설계선 좌표(250KB), 선형 구조(87KB), 계획선 샘플(85KB). 계획선은 화면이 이미 같은 규칙으로 계산해 들고 있고, 설계선 좌표는 B06 상세가 준다. - 프론트는 온 필드만 덮어쓰는 부분 갱신으로 바꿔 안 온 값을 지우지 않는다. 디바운스 400 → 250ms. 곡선 L·R 표시 원복 - 배관 자리는 구조물 측점이라 테이블에 기본으로 안 나오는 것이 맞다(사용자 정정). 격자 밖 변화점 셀을 걷어냈다 — 선택하면 buildSelectedColumn이 곡선 L·R을 보여 준다. 검증: 응답 1,091,003 B → 6,007 B, 계산값 동일(절토 16.4㎡ / 성토 48.3㎡). typecheck·vite build·ruff·B03 테스트 통과. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -91,7 +91,7 @@ const MAX_PANEL_HEIGHT_RATIO = 0.9;
|
||||
const TABLE_ROW_COUNT = 12;
|
||||
/** 계획고 편집 후 횡단 재계산을 서버에 묻기까지 기다리는 시간(ms).
|
||||
* ▲/▼ 길게 누르기(초당 10회)로 요청이 쏟아지지 않게 마지막 값만 보낸다. */
|
||||
const CROSS_PREVIEW_DEBOUNCE_MS = 400;
|
||||
const CROSS_PREVIEW_DEBOUNCE_MS = 250;
|
||||
|
||||
/**
|
||||
* 저장된 계획선 선형을 읽되 **모양을 먼저 검증한다**.
|
||||
@@ -432,9 +432,17 @@ export function createRouteProfilePanel(
|
||||
.then((next) => {
|
||||
if (seq !== crossPreviewSeq || !detail || routeId !== targetRouteId) return;
|
||||
// 공유 캐시가 들고 있는 **같은 객체**를 제자리 갱신한다 — B06이 이 객체를 그대로
|
||||
// 보므로 페이지를 넘어가도 다시 받을 필요가 없다.
|
||||
detail.cross_sections = next.cross_sections;
|
||||
detail.longitudinal.design_profiles = next.longitudinal.design_profiles;
|
||||
// 보므로 페이지를 넘어가도 다시 받을 필요가 없다. 응답에는 바뀌는 값(설계·계획선)만
|
||||
// 오므로 지반선 샘플은 건드리지 않는다.
|
||||
const designByChainage = new Map(
|
||||
next.designs.map((entry) => [entry.chainage_m.toFixed(3), entry.design]),
|
||||
);
|
||||
for (const section of detail.cross_sections) {
|
||||
const patch = designByChainage.get(section.chainage_m.toFixed(3));
|
||||
// 부분 갱신 — 온 필드만 덮어쓴다. 설계선 좌표 등 안 온 값은 B06이 상세를
|
||||
// 받을 때 정확한 값으로 채워지므로 여기서 지우지 않는다.
|
||||
if (patch && section.design) section.design = { ...section.design, ...patch };
|
||||
}
|
||||
draw();
|
||||
})
|
||||
.catch(() => {
|
||||
|
||||
@@ -292,12 +292,7 @@ function curveTitle(curve: AlignmentCurve): string {
|
||||
* 곡선이 없는 측점에도 **빈 칸을 만든다**. 변화점에만 셀을 두면 세로 구분선이 띄엄띄엄
|
||||
* 끊겨 위쪽 측점값 행들과 격자가 맞지 않는다.
|
||||
*/
|
||||
function buildCurveRows(
|
||||
options: ProfileTableOptions,
|
||||
centers: number[],
|
||||
x: (chainage: number) => number,
|
||||
cellWidth: number,
|
||||
): HTMLElement[] {
|
||||
function buildCurveRows(options: ProfileTableOptions, centers: number[]): HTMLElement[] {
|
||||
const { alignment, onCurveRadiusChange } = options;
|
||||
const lengthRow = createRow(
|
||||
"b05-profile-table__row--curve is-group-start",
|
||||
@@ -344,42 +339,6 @@ function buildCurveRows(
|
||||
placeCell(radiusRow, centers[index], radiusCell);
|
||||
});
|
||||
|
||||
// 규칙 측점 격자에 없는 변화점(배관 자리처럼 임의 chainage에 승격된 점)의 곡선도
|
||||
// **기본으로** 값을 보여 준다 — 격자에만 셀을 두면 배관 변화점의 R·L이 표에서 통째로
|
||||
// 빠져 "R이 안 들어간 것"처럼 읽힌다(2026-08-03 사용자 보고).
|
||||
const stationKeys = new Set(alignment.stations.map((row) => row.chainage_m.toFixed(3)));
|
||||
alignment.curves.forEach((curve) => {
|
||||
const key = curve.chainage_m.toFixed(3);
|
||||
if (stationKeys.has(key)) return;
|
||||
const lengthCell = element("span", "b05-profile-table__curve is-floating");
|
||||
const radiusCell = element("span", "b05-profile-table__curve is-floating");
|
||||
radiusCell.append(
|
||||
curveInput(
|
||||
curve,
|
||||
curve.r_m.toFixed(1),
|
||||
`종단곡선 반경 R (m)
|
||||
${curveTitle(curve)}`,
|
||||
(r) => r,
|
||||
),
|
||||
);
|
||||
lengthCell.append(
|
||||
curveInput(
|
||||
curve,
|
||||
curve.l_m.toFixed(2),
|
||||
`종단곡선 길이 L (m)
|
||||
${curveTitle(curve)}`,
|
||||
(l) => (curve.l_m > 1e-9 ? (l * curve.r_m) / curve.l_m : null),
|
||||
),
|
||||
);
|
||||
[lengthCell, radiusCell].forEach((cell) => {
|
||||
if (curve.skip_allowed) cell.classList.add("is-optional");
|
||||
if (curve.omitted) cell.classList.add("is-omitted");
|
||||
cell.style.width = `${cellWidth}px`;
|
||||
});
|
||||
placeCell(lengthRow, x(curve.chainage_m), lengthCell);
|
||||
placeCell(radiusRow, x(curve.chainage_m), radiusCell);
|
||||
});
|
||||
|
||||
function curveInput(
|
||||
curve: AlignmentCurve,
|
||||
value: string,
|
||||
@@ -581,7 +540,7 @@ export function createProfileTable(options: ProfileTableOptions): HTMLElement {
|
||||
});
|
||||
table.append(row);
|
||||
});
|
||||
table.append(...buildCurveRows(options, centers, x, cellWidth));
|
||||
table.append(...buildCurveRows(options, centers));
|
||||
|
||||
// 선택된 측점(규칙·비정규 공용)을 **값 열 오버레이**로 강조한다.
|
||||
// 규칙 측점은 종점 이동을 반영한 `centers[index]`, 비정규 측점은 `x(chainage)`를 중심으로 쓴다.
|
||||
|
||||
@@ -1146,10 +1146,3 @@
|
||||
background: color-mix(in srgb, var(--color-primary) 14%, transparent);
|
||||
outline: 1px solid var(--color-primary);
|
||||
}
|
||||
|
||||
/* 규칙 측점 격자 밖 변화점(배관 자리)의 곡선 R·L 셀 — 격자 셀 위에 얹혀 값을 보인다. */
|
||||
.b05-profile-table__curve.is-floating {
|
||||
z-index: 2;
|
||||
background: var(--color-surface-raised);
|
||||
outline: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
@@ -457,6 +457,16 @@ export interface ProfileAlignmentEdits {
|
||||
curve_radii: Record<string, number>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 계획선 편집 프리뷰 결과 — **바뀌는 것만** 온다.
|
||||
* 횡단 지반선 원시 샘플은 계획고와 무관해 그대로이므로 싣지 않는다(응답 1MB → 수십 KB).
|
||||
*/
|
||||
export interface CrossDesignPreviewResponse {
|
||||
status: string;
|
||||
/** 유토곡선이 읽는 설계 필드만 담긴 부분 갱신값(설계선 좌표 등은 오지 않는다). */
|
||||
designs: Array<{ chainage_m: number; design: Partial<CrossDesign> }>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 계획선 편집을 반영해 **계획선 + 전 측점 횡단 설계**를 다시 계산해 받는다(서버 저장 없음).
|
||||
*
|
||||
@@ -469,8 +479,8 @@ export async function previewCrossDesigns(
|
||||
routeId: number,
|
||||
edits: ProfileAlignmentEdits,
|
||||
standardCrossSection?: StandardCrossSection,
|
||||
): Promise<SectionDetailResponse> {
|
||||
return requestJson<SectionDetailResponse>(
|
||||
): Promise<CrossDesignPreviewResponse> {
|
||||
return requestJson<CrossDesignPreviewResponse>(
|
||||
`/projects/${projectId}/sections/${routeId}/cross-design/preview`,
|
||||
{
|
||||
method: "POST",
|
||||
|
||||
@@ -39,6 +39,7 @@ from B06_wf3_ProfileCross.B06_wf3_ProfileCross_Schema import (
|
||||
CompanyStandardProject,
|
||||
CompanyStandardResponse,
|
||||
CrossDesignPreviewRequest,
|
||||
CrossDesignPreviewResponse,
|
||||
CrossDesignRequest,
|
||||
CrossDesignResponse,
|
||||
HaulEquipmentLimit,
|
||||
@@ -486,6 +487,21 @@ def _attach_default_designs(
|
||||
continue
|
||||
|
||||
|
||||
#: 계획선 프리뷰 응답에 실을 설계 필드 — 유토곡선 계산(면적·지반유형·자연방토)과
|
||||
#: 계획고 변경 감지에 쓰는 것만. 나머지(설계선 좌표 등)는 B06이 상세를 받을 때 온다.
|
||||
_PREVIEW_DESIGN_FIELDS = (
|
||||
"ground_type",
|
||||
"roadbed_width_m",
|
||||
"cut_area_m2",
|
||||
"cut_soil_area_m2",
|
||||
"cut_rock_area_m2",
|
||||
"cut_rock_kind",
|
||||
"fill_area_m2",
|
||||
"fill_ground_slope",
|
||||
"design_elevation_m",
|
||||
)
|
||||
|
||||
|
||||
def _recompute_designs_for_alignment(
|
||||
longitudinal: dict[str, Any],
|
||||
cross_sections: list[dict[str, Any]],
|
||||
@@ -534,11 +550,11 @@ def _recompute_designs_for_alignment(
|
||||
|
||||
@router.post(
|
||||
"/{project_id}/sections/{route_id}/cross-design/preview",
|
||||
response_model=SectionDetailResponse,
|
||||
response_model=CrossDesignPreviewResponse,
|
||||
)
|
||||
async def preview_cross_designs(
|
||||
project_id: UUID, route_id: int, request: CrossDesignPreviewRequest
|
||||
) -> SectionDetailResponse | JSONResponse:
|
||||
) -> CrossDesignPreviewResponse | JSONResponse:
|
||||
"""계획선 편집 델타로 계획선과 전 측점 횡단 설계를 다시 계산해 돌려준다(저장 없음).
|
||||
|
||||
B05에서 계획고를 끄는 동안 횡단 단면적·유토곡선이 함께 움직이게 하는 프리뷰 경로다.
|
||||
@@ -574,8 +590,22 @@ async def preview_cross_designs(
|
||||
)
|
||||
|
||||
await asyncio.to_thread(rebuild)
|
||||
return SectionDetailResponse(
|
||||
**detail, balloon_offsets=_read_balloon_offsets(longitudinal_row)
|
||||
# 계획고를 끄는 동안 오가는 값이라 **유토곡선이 실제로 읽는 필드만** 싣는다.
|
||||
# 지반선 샘플·설계선 좌표(design_line)·선형 구조를 다 담으면 1MB가 넘어 편집이 굼떠진다
|
||||
# (2026-08-03 사용자 지적). 계획선 자체는 화면이 이미 같은 규칙으로 계산해 들고 있다.
|
||||
return CrossDesignPreviewResponse(
|
||||
designs=[
|
||||
{
|
||||
"chainage_m": section.get("chainage_m"),
|
||||
"design": {
|
||||
key: section["design"].get(key)
|
||||
for key in _PREVIEW_DESIGN_FIELDS
|
||||
if key in section["design"]
|
||||
},
|
||||
}
|
||||
for section in detail["cross_sections"]
|
||||
if section.get("design")
|
||||
],
|
||||
)
|
||||
except FileNotFoundError as exc:
|
||||
return JSONResponse(status_code=404, content={"status": "error", "message": str(exc)})
|
||||
|
||||
@@ -191,3 +191,15 @@ class CrossDesignPreviewRequest(BaseModel):
|
||||
|
||||
def edits(self) -> dict[str, Any]:
|
||||
return {"station_offsets": self.station_offsets, "curve_radii": self.curve_radii}
|
||||
|
||||
|
||||
class CrossDesignPreviewResponse(BaseModel):
|
||||
"""계획선 편집 프리뷰 결과 — **바뀌는 것만** 담는다.
|
||||
|
||||
계획고를 끄는 동안 오가는 값이라 **유토곡선이 실제로 읽는 설계 필드만** 담는다.
|
||||
지반선 샘플·설계선 좌표·선형 구조까지 실으면 1MB가 넘어 편집이 굼떠지고
|
||||
(2026-08-03 사용자 지적), 계획선 자체는 화면이 같은 규칙으로 이미 계산해 들고 있다.
|
||||
"""
|
||||
|
||||
status: str = "ok"
|
||||
designs: list[dict[str, Any]] = Field(default_factory=list)
|
||||
|
||||
Reference in New Issue
Block a user