diff --git a/B05_wf2_Route/B05_wf2_Route_UI_Profile_Panel.ts b/B05_wf2_Route/B05_wf2_Route_UI_Profile_Panel.ts index 1a01a8a1..90aa340b 100644 --- a/B05_wf2_Route/B05_wf2_Route_UI_Profile_Panel.ts +++ b/B05_wf2_Route/B05_wf2_Route_UI_Profile_Panel.ts @@ -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(() => { diff --git a/B05_wf2_Route/B05_wf2_Route_UI_Profile_Table.ts b/B05_wf2_Route/B05_wf2_Route_UI_Profile_Table.ts index dc52c473..956864df 100644 --- a/B05_wf2_Route/B05_wf2_Route_UI_Profile_Table.ts +++ b/B05_wf2_Route/B05_wf2_Route_UI_Profile_Table.ts @@ -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)`를 중심으로 쓴다. diff --git a/B05_wf2_Route/B05_wf2_Route_UI_Style.css b/B05_wf2_Route/B05_wf2_Route_UI_Style.css index 8e0dba4b..2fa07c92 100644 --- a/B05_wf2_Route/B05_wf2_Route_UI_Style.css +++ b/B05_wf2_Route/B05_wf2_Route_UI_Style.css @@ -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); -} diff --git a/B06_wf3_ProfileCross/B06_wf3_ProfileCross_Api_Fetch.ts b/B06_wf3_ProfileCross/B06_wf3_ProfileCross_Api_Fetch.ts index 4e7744d7..63d63b29 100644 --- a/B06_wf3_ProfileCross/B06_wf3_ProfileCross_Api_Fetch.ts +++ b/B06_wf3_ProfileCross/B06_wf3_ProfileCross_Api_Fetch.ts @@ -457,6 +457,16 @@ export interface ProfileAlignmentEdits { curve_radii: Record; } +/** + * 계획선 편집 프리뷰 결과 — **바뀌는 것만** 온다. + * 횡단 지반선 원시 샘플은 계획고와 무관해 그대로이므로 싣지 않는다(응답 1MB → 수십 KB). + */ +export interface CrossDesignPreviewResponse { + status: string; + /** 유토곡선이 읽는 설계 필드만 담긴 부분 갱신값(설계선 좌표 등은 오지 않는다). */ + designs: Array<{ chainage_m: number; design: Partial }>; +} + /** * 계획선 편집을 반영해 **계획선 + 전 측점 횡단 설계**를 다시 계산해 받는다(서버 저장 없음). * @@ -469,8 +479,8 @@ export async function previewCrossDesigns( routeId: number, edits: ProfileAlignmentEdits, standardCrossSection?: StandardCrossSection, -): Promise { - return requestJson( +): Promise { + return requestJson( `/projects/${projectId}/sections/${routeId}/cross-design/preview`, { method: "POST", diff --git a/B06_wf3_ProfileCross/B06_wf3_ProfileCross_Router.py b/B06_wf3_ProfileCross/B06_wf3_ProfileCross_Router.py index bc3ef125..006f43b7 100644 --- a/B06_wf3_ProfileCross/B06_wf3_ProfileCross_Router.py +++ b/B06_wf3_ProfileCross/B06_wf3_ProfileCross_Router.py @@ -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)}) diff --git a/B06_wf3_ProfileCross/B06_wf3_ProfileCross_Schema.py b/B06_wf3_ProfileCross/B06_wf3_ProfileCross_Schema.py index d0b72c85..551b6496 100644 --- a/B06_wf3_ProfileCross/B06_wf3_ProfileCross_Schema.py +++ b/B06_wf3_ProfileCross/B06_wf3_ProfileCross_Schema.py @@ -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)