Merge remote-tracking branch 'origin/sub_laptop_2' into main_laptop_1
This commit is contained in:
@@ -12,6 +12,8 @@
|
||||
|
||||
import { API_BASE_URL } from "@config/config_frontend";
|
||||
import type { HaulPlan } from "@util/common_util_mass_haul_balance";
|
||||
import type { StageContext } from "../A00_Common/design/design_pipeline_types";
|
||||
import type { MassHaulStageOutput } from "./logic/B05_Profile_MassHaul_Stage";
|
||||
|
||||
/** 서버가 돌려주는 배분 한 벌 — **화면이 쓰는 꼴 그대로**라 그리기 코드가 손대지 않는다.
|
||||
* `import type` 이라 배분 모듈이 번들에 실리지 않는다(빌드에서 지워진다). */
|
||||
@@ -24,6 +26,84 @@ const TIMEOUT_MS = 20000;
|
||||
/** 열쇠별로 받아 둔 배분 — 되돌리기 · 보기 오감처럼 같은 누가토량이 다시 오면 묻지 않음(342-1). */
|
||||
const KEPT_PLANS = 8;
|
||||
|
||||
/** 받은 배분 — `프로젝트:노선|열쇠` → 배분 · 오래된 것부터 버림 · 선반입 · 묶음 단계가 함께 씀. */
|
||||
const kept = new Map<string, HaulPlanPayload>();
|
||||
|
||||
/**
|
||||
* 입력이 달라졌는지 가리는 표식. 응답이 오면 곡선을 다시 그리고, 그리기가 다시
|
||||
* `schedule` 을 부르므로 **표식이 없으면 요청이 끝없이 되돌아온다**(2026-09-06 실측).
|
||||
* 누가토량 결과 전체 — 끝 · 가운데 누계만 보던 옛 표식은 사이 측점이 바뀌어도 같았다(342-1).
|
||||
*/
|
||||
function keyOf(result: unknown): string {
|
||||
const points = (result as { points?: unknown[] })?.points;
|
||||
if (!Array.isArray(points) || !points.length) return "empty";
|
||||
return JSON.stringify(points);
|
||||
}
|
||||
|
||||
const keptKey = (projectId: string, route: number, key: string): string =>
|
||||
`${projectId}:${route}|${key}`;
|
||||
|
||||
function keep(at: string, plan: HaulPlanPayload): void {
|
||||
kept.delete(at);
|
||||
kept.set(at, plan);
|
||||
if (kept.size > KEPT_PLANS) kept.delete(kept.keys().next().value as string);
|
||||
}
|
||||
|
||||
/** 서버에 배분을 물음 — 받으면 기억 · 못 받으면(실패 · 끊김) undefined. `signal` 이 끊으면 함께 끊음. */
|
||||
async function postHaulPlan(
|
||||
projectId: string,
|
||||
route: number,
|
||||
result: unknown,
|
||||
key: string,
|
||||
signal?: AbortSignal,
|
||||
): Promise<HaulPlanPayload | undefined> {
|
||||
if (signal?.aborted) return undefined;
|
||||
const controller = new AbortController();
|
||||
const abort = window.setTimeout(() => controller.abort(), TIMEOUT_MS);
|
||||
const relay = () => controller.abort();
|
||||
signal?.addEventListener("abort", relay);
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${API_BASE_URL}/projects/${projectId}/sections/${route}/haul-plan`,
|
||||
{
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ result }),
|
||||
signal: controller.signal,
|
||||
},
|
||||
);
|
||||
if (!response.ok) return undefined;
|
||||
const payload = (await response.json()) as { haul_plan?: HaulPlanPayload };
|
||||
const plan = payload.haul_plan ?? null;
|
||||
keep(keptKey(projectId, route, key), plan);
|
||||
return plan;
|
||||
} catch {
|
||||
return undefined; // 배분을 못 받아도 곡선 자체는 그대로 보인다 — 화면을 막지 않는다.
|
||||
} finally {
|
||||
window.clearTimeout(abort);
|
||||
signal?.removeEventListener("abort", relay);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 계산 묶음 `haul` 단계(PLAN 346-2 · 계약 `design_pipeline_types.ts`) — masshaul 의 횡단 기준 결과를 서버
|
||||
* Node 로 배분 · 같은 열쇠면 안 물음 · 새 조작이 오면 `ctx.signal` 로 끊김(옛 번호 결과는 묶음이 버림).
|
||||
* 못 받으면 앞 배분(`ctx.prev`) 그대로.
|
||||
*/
|
||||
export function createHaulStage(projectId: string, routeId: () => number | undefined) {
|
||||
return async (ctx: StageContext<"haul">): Promise<HaulPlanPayload> => {
|
||||
const result = (ctx.out.masshaul as MassHaulStageOutput | null)?.haulInput;
|
||||
const route = routeId();
|
||||
if (!result || !route) return (ctx.prev as HaulPlanPayload) ?? null;
|
||||
const key = keyOf(result);
|
||||
const at = keptKey(projectId, route, key);
|
||||
if (kept.has(at)) return kept.get(at) ?? null;
|
||||
const plan = await postHaulPlan(projectId, route, result, key, ctx.signal);
|
||||
return plan === undefined ? ((ctx.prev as HaulPlanPayload) ?? null) : plan;
|
||||
};
|
||||
}
|
||||
|
||||
export interface HaulPlanPrefetch {
|
||||
/** 새 누가토량 결과가 나왔음을 알린다 — 잠잠해지면 서버에 물어본다. */
|
||||
schedule: (result: unknown) => void;
|
||||
@@ -50,19 +130,6 @@ export function createHaulPlanPrefetch(
|
||||
let sentKey: string | null = null;
|
||||
/** 받은 값을 화면에 반영하려고 다시 그리는 중 — 그 그리기가 거는 요청은 무시한다. */
|
||||
let applying = false;
|
||||
/** 받은 배분 — 열쇠(누가토량 결과 전체) → 배분 · 오래된 것부터 버림. */
|
||||
const kept = new Map<string, HaulPlanPayload>();
|
||||
|
||||
/**
|
||||
* 입력이 달라졌는지 가리는 표식. 응답이 오면 곡선을 다시 그리고, 그리기가 다시
|
||||
* `schedule` 을 부르므로 **표식이 없으면 요청이 끝없이 되돌아온다**(2026-09-06 실측).
|
||||
* 누가토량 결과 전체 — 끝 · 가운데 누계만 보던 옛 표식은 사이 측점이 바뀌어도 같았다(342-1).
|
||||
*/
|
||||
function keyOf(result: unknown): string {
|
||||
const points = (result as { points?: unknown[] })?.points;
|
||||
if (!Array.isArray(points) || !points.length) return "empty";
|
||||
return JSON.stringify(points);
|
||||
}
|
||||
|
||||
/** 못 받은 열쇠는 잊는다 — 다음 그리기가 같은 값으로 다시 보냄(342-1). */
|
||||
function forget(seq: number): void {
|
||||
@@ -72,41 +139,19 @@ export function createHaulPlanPrefetch(
|
||||
async function send(result: unknown, seq: number, key: string): Promise<void> {
|
||||
const route = routeId();
|
||||
if (!route) return forget(seq);
|
||||
const controller = new AbortController();
|
||||
const abort = window.setTimeout(() => controller.abort(), TIMEOUT_MS);
|
||||
const got = await postHaulPlan(projectId, route, result, key);
|
||||
if (got === undefined) return forget(seq);
|
||||
// 늦게 온 응답은 버린다 — 그 사이 사용자가 계획고를 더 만졌을 수 있다.
|
||||
if (seq !== sequence) return;
|
||||
plan = got;
|
||||
// 다시 그리기가 `schedule` 을 되부르므로 그 한 바퀴를 막는다. 표식(`sentKey`)만으로는
|
||||
// 부족했다 — 반영 뒤 누계가 아주 조금 달라지는 경로가 있어 2~3바퀴가 더 돌았다
|
||||
// (2026-09-06 보조 창 실측: 편집이 멈춘 뒤 2.8~6.3초에 걸쳐 3건).
|
||||
applying = true;
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${API_BASE_URL}/projects/${projectId}/sections/${route}/haul-plan`,
|
||||
{
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ result }),
|
||||
signal: controller.signal,
|
||||
},
|
||||
);
|
||||
if (!response.ok) return forget(seq);
|
||||
const payload = (await response.json()) as { haul_plan?: HaulPlanPayload };
|
||||
// 늦게 온 응답은 버린다 — 그 사이 사용자가 계획고를 더 만졌을 수 있다.
|
||||
if (seq !== sequence) return;
|
||||
plan = payload.haul_plan ?? null;
|
||||
kept.delete(key);
|
||||
kept.set(key, plan);
|
||||
if (kept.size > KEPT_PLANS) kept.delete(kept.keys().next().value as string);
|
||||
// 다시 그리기가 `schedule` 을 되부르므로 그 한 바퀴를 막는다. 표식(`sentKey`)만으로는
|
||||
// 부족했다 — 반영 뒤 누계가 아주 조금 달라지는 경로가 있어 2~3바퀴가 더 돌았다
|
||||
// (2026-09-06 보조 창 실측: 편집이 멈춘 뒤 2.8~6.3초에 걸쳐 3건).
|
||||
applying = true;
|
||||
try {
|
||||
onReady();
|
||||
} finally {
|
||||
applying = false;
|
||||
}
|
||||
} catch {
|
||||
// 배분을 못 받아도 곡선 자체는 그대로 보인다 — 화면을 막지 않는다.
|
||||
forget(seq);
|
||||
onReady();
|
||||
} finally {
|
||||
window.clearTimeout(abort);
|
||||
applying = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,7 +166,8 @@ export function createHaulPlanPrefetch(
|
||||
const seq = sequence;
|
||||
window.clearTimeout(timer);
|
||||
// 받아 둔 열쇠면 묻지 않고 그대로 — 그리는 쪽은 이 뒤에 `current()` 를 읽는다.
|
||||
const known = kept.get(key);
|
||||
const route = routeId();
|
||||
const known = route ? kept.get(keptKey(projectId, route, key)) : undefined;
|
||||
if (known !== undefined) return void (plan = known);
|
||||
timer = window.setTimeout(() => void send(pending, seq, key), SETTLE_MS);
|
||||
},
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/* =============================================================================
|
||||
* B05_Profile_MassHaul_Stage.ts
|
||||
* 계산 묶음 `masshaul` 단계(PLAN 346-2 · 계약 `design_pipeline_types.ts`) — cross(측점 열쇠마다 면적) ·
|
||||
* profile → 유토곡선 계열(`computeMassHaulSeries` · 화면 유토곡선 · B06 과 같은 계산).
|
||||
*
|
||||
* 출력 = `MassHaulStageOutput`(계열 + 흙 배분에 보낼 횡단 기준 결과) · 순수 계산(DOM · fetch · 세션 없음).
|
||||
* 토량환산계수 · 자연 사토 기울기는 부르는 쪽이 넘김(`context`) — 없으면 null.
|
||||
* ========================================================================== */
|
||||
|
||||
import { computeMassHaulSeries, type MassHaulSeries } from "@util/common_util_mass_haul";
|
||||
import type { EarthworkConversion, MassHaulSection } from "@util/common_util_mass_haul_types";
|
||||
import type { StageContext } from "../../A00_Common/design/design_pipeline_types";
|
||||
|
||||
export interface MassHaulStageContext {
|
||||
conversion: EarthworkConversion;
|
||||
naturalSpoilMinSlope?: number;
|
||||
}
|
||||
|
||||
export interface MassHaulStageOutput {
|
||||
series: MassHaulSeries[];
|
||||
/** 흙 배분(haul 단계)에 보낼 결과 — 횡단 기준(없으면 첫 계열) · 계열이 없으면 null. */
|
||||
haulInput: MassHaulSeries["result"] | null;
|
||||
}
|
||||
|
||||
/** `masshaul` 단계 만들기 — `context()` 는 부를 때마다 읽음(설정이 바뀌면 다음 계산부터). */
|
||||
export function createMassHaulStage(context: () => MassHaulStageContext | null) {
|
||||
return (ctx: StageContext<"masshaul">): MassHaulStageOutput | null => {
|
||||
const cross = ctx.out.cross;
|
||||
const settings = context();
|
||||
if (!cross || !settings) return null;
|
||||
const sections: MassHaulSection[] = Object.values(cross.byKey)
|
||||
.sort((a, b) => a.chainageM - b.chainageM)
|
||||
.map((entry) => ({
|
||||
station_id: entry.key,
|
||||
chainage_m: entry.chainageM,
|
||||
design: entry.detail as MassHaulSection["design"],
|
||||
}));
|
||||
const samples = ctx.out.profile?.samples ?? [];
|
||||
const longitudinal = {
|
||||
length_m:
|
||||
samples[samples.length - 1]?.chainage_m ?? sections[sections.length - 1]?.chainage_m ?? 0,
|
||||
design_profiles: samples.length ? [{ samples }] : [],
|
||||
};
|
||||
const series = computeMassHaulSeries(
|
||||
longitudinal,
|
||||
sections,
|
||||
settings.conversion,
|
||||
settings.naturalSpoilMinSlope,
|
||||
);
|
||||
const picked = series.find((entry) => entry.basis === "cross") ?? series[0];
|
||||
return { series, haulInput: picked?.result ?? null };
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user