무슨 일이 있었나 랩탑 줄의 병합 `20ba886c`(Merge origin/main_desktop_1·main_laptop_1·sub_desktop_1 into sub_laptop_1)가 우리 파일 22개를 떨구고 35개 파일의 내용을 옛것으로 되돌림. 손으로 지운 커밋은 없고 **병합 자체가 떨군 것**임. 그것이 `origin/dev`·`main_laptop_1`·`sub_laptop_1`· `CODEX` 까지 퍼졌고(데스크탑 둘만 무사), 이 창의 병합 `d92c1f2b` 로 들어옴. 잃었던 것 - 공용 — `common_util_provenance.py` · `ui_template_provenance.ts` - B08 — 근거 사전 · 좌측 패널 상자 모듈 · 토량환산계수 칸 - B09 — 근거 사전 셋 - B05 — 계획노선 편집 모듈 아홉 · 지형 라우터 · B04 지도 모듈 - 시험 셋과, 35개 파일 안의 최근 작업(환산계수 고르기 · 근거 호버 배선 등) 어떻게 되살렸나 `611a2b40`(병합 직전, 전부 온전)에서 `git show <커밋>:<경로>` 로 내용만 꺼내 되돌림. 이력은 안 건드림. ⚠ HEAD 에만 있던 「추가 816줄」은 랩탑의 새 작업이 아니라 **되살아난 옛 코드**였음(B05 편집은 모듈로 쪼개기 전 덩어리 · B08 라우터는 환산계수 고르기 전 옛 상수판). 되돌릴 시점 이후의 **진짜 새 커밋은 둘뿐**이라 그 둘만 패치로 다시 얹음 — `9f827bf6`(리로드 빌드 고리 끊기, 데스크탑 보조) · `b9bca6b3`(B06 조정창 1px, 랩탑). 위키 여덟은 코덱스 몫이라 손대지 않음. 자체검증 — 양쪽 작업이 다 살아 있음을 짚어 확인: `main.py` 의 「개발 서버는 살려 둔다」 · `B05_Profile_Engine_Grade.py` 의 `plan_curve_length_limit_m` · `B08_..._EarthworkGrid.ts` 의 `attachProvenance`. `tsc --noEmit` 통과 · `pytest -q` **1317 passed, 28 skipped** (되살리기 전에는 시험 둘이 수집 단계에서 깨져 있었음). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017RANEBHns1S4tkmsYwewtk
132 lines
6.0 KiB
TypeScript
132 lines
6.0 KiB
TypeScript
/* =============================================================================
|
|
* B05_Profile_UI_RouteEdit_Cross.ts
|
|
* 계획노선 편집 중 **측점 횡단** — 메인 창 오른쪽 세로 칸에 **붙박이 두 판**(계획서 0-9 ⑱⑲).
|
|
*
|
|
* · 위 판 = **지금 횡단**. 측점 눈금을 누르면 그 측점을 셈해 여기에 낸다.
|
|
* · 아래 판 = **이전 횡단**. 평소에는 빈 화면이고, 노선을 고쳐 **노드를 놓는 순간**
|
|
* 위 판의 것이 이리로 내려오고 새로 셈한 것이 위로 올라간다 — 전후를 나란히 본다.
|
|
*
|
|
* 보이는 것은 셋뿐이다(2026-09-12 사용자 확정) — **원지반 횡단선 · 기본 계획 횡단선 ·
|
|
* 계획 횡단의 성토사면 길이**. 구조물은 그리지 않는다.
|
|
*
|
|
* ⚠ **계획고는 편집 중에 없다** — [확인] 뒤 전 체인이 낳는 값이다. 그래서 서버가 그 측점의
|
|
* 지반고를 그대로 계획고로 놓고(지반 추종) 사면만 세운 「기본 계획 횡단」을 낸다.
|
|
*
|
|
* 셈은 **B05·B06 정본을 그대로 재사용**한다 — 측점·지반 샘플은 `generate_sections`, 설계선은
|
|
* `compute_cross_design`(서버), 성토사면 길이는 B06 화면이 쓰는 `fillSlopeLengths`.
|
|
* ========================================================================== */
|
|
|
|
import { fetchCrossPreview, type CrossPreviewResponse } from "./B05_Profile_Api_Replan";
|
|
import { drawCross, summarizeCross } from "./B05_Profile_UI_RouteEdit_Cross_Draw";
|
|
import { formatStation } from "./B05_Profile_Util_Station";
|
|
|
|
export interface CrossPreviewParams {
|
|
projectId: string;
|
|
/** 두 판이 들어앉을 오른쪽 세로 칸. */
|
|
side: HTMLElement;
|
|
/** 지금 편집값 — 셈을 부르는 순간에 읽는다. */
|
|
request: () => {
|
|
vertices: Array<{ x: number; y: number; curve: boolean; radius_m: number | null }>;
|
|
min_radius_m: number;
|
|
station_interval_m: number;
|
|
};
|
|
}
|
|
|
|
export interface CrossPreviewWindow {
|
|
/** 그 측점의 횡단을 위 판에 낸다. 같은 측점을 다시 누르면 보던 것을 아래로 내린다. */
|
|
open: (chainageM: number) => Promise<void>;
|
|
/** 노선을 고쳤다 — 보던 측점을 **다시 셈해** 전후로 늘어놓는다. 보던 것이 없으면 아무 일도 없다. */
|
|
refresh: () => Promise<void>;
|
|
}
|
|
|
|
interface CrossPane {
|
|
root: HTMLElement;
|
|
/** 셈해 온 횡단을 그린다. `null` 이면 빈 화면으로 되돌린다. */
|
|
show: (preview: CrossPreviewResponse | null, intervalM: number) => void;
|
|
/** 기다리는 중임을 알린다. */
|
|
wait: (text: string) => void;
|
|
}
|
|
|
|
function createPane(title: string, empty: string): CrossPane {
|
|
const root = document.createElement("section");
|
|
root.className = "b05-routeedit__cross";
|
|
root.innerHTML = `
|
|
<div class="b05-routeedit__cross-head">
|
|
<strong class="b05-routeedit__cross-title">${title}</strong>
|
|
<span class="b05-routeedit__cross-station"></span>
|
|
</div>
|
|
<canvas class="b05-routeedit__cross-canvas" width="420" height="240"></canvas>
|
|
<div class="b05-routeedit__cross-foot">${empty}</div>`;
|
|
const station = root.querySelector<HTMLElement>(".b05-routeedit__cross-station")!;
|
|
const foot = root.querySelector<HTMLElement>(".b05-routeedit__cross-foot")!;
|
|
const canvas = root.querySelector<HTMLCanvasElement>(".b05-routeedit__cross-canvas")!;
|
|
const context = canvas.getContext("2d")!;
|
|
return {
|
|
root,
|
|
show(preview, intervalM) {
|
|
context.clearRect(0, 0, canvas.width, canvas.height);
|
|
if (!preview) {
|
|
station.textContent = "";
|
|
foot.textContent = empty;
|
|
return;
|
|
}
|
|
// 측점은 **누가거리가 아니라 측점 표기**로 낸다(계획서 0-9 ㉑) — B05 왼쪽 아래 구조물
|
|
// 목록이 쓰는 그 규칙이다. 서버가 주는 `STA.0+100.000` 을 그대로 쓰면 표기가 갈린다.
|
|
station.textContent = formatStation(preview.chainage_m, intervalM);
|
|
drawCross(context, canvas, preview);
|
|
foot.textContent = summarizeCross(preview);
|
|
},
|
|
wait(text) {
|
|
context.clearRect(0, 0, canvas.width, canvas.height);
|
|
foot.textContent = text;
|
|
},
|
|
};
|
|
}
|
|
|
|
export function createCrossPreview(params: CrossPreviewParams): CrossPreviewWindow {
|
|
const current = createPane("횡단", "측점 눈금을 누르면 그 측점 횡단이 뜹니다.");
|
|
const previous = createPane("이전 횡단", "노선을 고치면 고치기 전 횡단이 여기 남습니다.");
|
|
params.side.append(current.root, previous.root);
|
|
|
|
/** 지금 보고 있는 측점(누가거리). 아직 없으면 null. */
|
|
let watching: number | null = null;
|
|
/** 위 판에 그려 둔 것 — 다음 번에 아래로 내릴 재료. */
|
|
let shown: CrossPreviewResponse | null = null;
|
|
/** 지금 부른 셈 — 늦게 온 응답을 새 자리에 적지 않으려고 든다. */
|
|
let ticket = 0;
|
|
|
|
async function load(chainageM: number, keepPrevious: boolean): Promise<void> {
|
|
const mine = ++ticket;
|
|
const request = params.request();
|
|
if (keepPrevious && shown) previous.show(shown, request.station_interval_m);
|
|
current.wait("읽는 중…");
|
|
try {
|
|
const preview = await fetchCrossPreview(params.projectId, {
|
|
...request,
|
|
chainage_m: chainageM,
|
|
});
|
|
if (mine !== ticket) return; // 그 사이 다른 측점을 눌렀다.
|
|
shown = preview;
|
|
current.show(preview, request.station_interval_m);
|
|
} catch (error) {
|
|
if (mine !== ticket) return;
|
|
shown = null;
|
|
current.wait(error instanceof Error ? error.message : "횡단을 읽지 못했습니다.");
|
|
}
|
|
}
|
|
|
|
return {
|
|
async open(chainageM) {
|
|
// 다른 측점을 고른 것이라면 전후 비교가 아니다 — 아래 판을 비운다.
|
|
const sameStation = watching !== null && Math.abs(watching - chainageM) < 1e-6;
|
|
if (!sameStation) previous.show(null, params.request().station_interval_m);
|
|
watching = chainageM;
|
|
await load(chainageM, sameStation);
|
|
},
|
|
async refresh() {
|
|
if (watching === null) return;
|
|
await load(watching, true);
|
|
},
|
|
};
|
|
}
|