- B04 Router_Watershed — 분석 응답 · 단계 산출물 저장을 Output.write_analysis 로 뗌(primary-region 같은 길) · 도엽 좌표 저장본 메모리 보관(2 벌) - B04 Router_Watershed_Live — 저장 노선 기준(초안 없음 · 지문 같음) 답이면 마지막 끊김 확인 뒤 분석을 남김 · design/save 잠금 중이면 안 남김 · 남길 때 같은 잠금 쥠 · POST /drainage/live/cancel(그 차례가 마지막일 때만 지움) · 셈 멈춤 로그 - B05 Drainage_Live — 요청 끊을 때 서버에 끊김 알림 · 저장 노선 [업데이트] 답이 저장분 낡음 걷음 · 되돌리기로 초안이 없어지면 저장분의 마지막 낡음으로 표시 바로 - 시험 test_349_drainage_live_flow 고침 — 되돌리기 직후 낡음 기대가 옛 동작(답 올 때까지 낡음) · 새 동작 = 저장분 마지막 낡음(여기선 꺼짐) + 끊김 알림 확인 - 시험 test_347_2 — 「작업본에 안 씀」 옛 설명 고침 · 남김 · 초안 안 남김 · 잠금 중 안 남김 · 끊김 알림 시험 더함 · test_347_1 메모리 보관 확인 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
247 lines
10 KiB
TypeScript
247 lines
10 KiB
TypeScript
/* =============================================================================
|
|
* B05_Profile_UI_Drainage_Live.ts
|
|
* 배수유역 [업데이트](요구 326-6 · 347-2) — 묶음 drainage 단계 · 낡음 표시 · 구석 「계산 중」 ·
|
|
* 「지금 노선으로 셈」 요청(차례 번호 · 끊기). 규칙은 `logic/B05_Profile_Drainage_Live.ts`.
|
|
*
|
|
* - 단계(`stage`)는 계산 묶음(`design_pipeline` · 붙이기 = sub7)이 노선 초안 · 관이 바뀔 때 부름 —
|
|
* 기다리지 않고 null 을 돌려줌(배수유역은 뒤 단계 · 출력 없음).
|
|
* - 노선 초안이 있으면 「낡음」 — 저절로 셈하지 않음 · 옛 그림 그대로 · [업데이트] 단추에 표시만
|
|
* (508-2 [3D 업데이트] 꼴). 낡은 동안 제안 [적용] · [자동 제안]은 끔(새 답이 오면 다시 켬).
|
|
* - [업데이트] = 늘 지금 노선(초안 · 저장 노선)으로 배수유역을 처음부터 셈 — 노선이 그대로여도.
|
|
* 뒤에서 조용히(덮개 · 잠금 없음) · 답이 오면 그림을 갈아 끼움.
|
|
* - 다시 조작하면 떠 있는 요청을 끊고(AbortController) 다시 낡음 · 서버에도 끊김을 알려 셈을 멈춤
|
|
* (`/drainage/live/cancel` · 520-2). 묶음이 같은 노선 · 관으로 다시 돌면(입력 지문 같음) 그대로 둠.
|
|
* - 저장 노선 [업데이트] 답은 서버가 남김(520-2) — 그 뒤 관 조작은 세부 유역만 자동.
|
|
* - 초안이 없어지면(되돌리기 · 버림) 저장분의 마지막 낡음으로 표시를 바로 바꾸고 저장 배수유역을 다시
|
|
* 받아(열 때와 같은 길 · 계산 아님) 그 답의 낡음대로 — 떠 있던 초안용 답은 버림.
|
|
* - 서버가 「낡음」으로 답하면(노선 [저장] 뒤 등) 같은 낡음 표시.
|
|
* - 답은 패널 요청 감싸기(`DetailRunner`, quiet)로 올림 — 옛 차례 · 서버가 그만둔 답은 버림.
|
|
* ========================================================================== */
|
|
|
|
import { API_BASE_URL } from "@config/config_frontend";
|
|
import { currentLanguageIndex, ui_locales } from "@ui/ui_template_locale";
|
|
import { createProgressCircle } from "@ui/ui_template_progress";
|
|
import { setDrainageStale } from "../A00_Common/design/design_structure_ford_calc";
|
|
import type { StageFn } from "../A00_Common/design/logic/design_pipeline_types";
|
|
import {
|
|
computeDetailBasins,
|
|
type DetailBasinResponse,
|
|
type DetailPipeInput,
|
|
} from "../B04_PreProcess/B04_PreProcess_Api_Fetch";
|
|
import {
|
|
acceptLive,
|
|
draftMakesStale,
|
|
inputKey,
|
|
liveRequestBody,
|
|
} from "./logic/B05_Profile_Drainage_Live";
|
|
import type { DetailRunner } from "./B05_Profile_UI_Drainage_Run";
|
|
import { createDrainageSuggest, type DrainageSuggest } from "./B05_Profile_UI_Drainage_Suggest";
|
|
|
|
export interface DrainageLiveDeps {
|
|
/** 패널 뿌리 — 머리([자동 제안]) · 지도(구석 「계산 중」)를 이 안에서 찾음. */
|
|
root: HTMLElement;
|
|
/** [업데이트] 단추(머리 줄 · `_Drainage_Chrome`). */
|
|
updateButton: HTMLButtonElement;
|
|
/** 제안 목록을 이 앞에 둠. */
|
|
basinList: HTMLElement;
|
|
pipeRequest: () => DetailPipeInput[];
|
|
runner: DetailRunner;
|
|
/** [적용] 답이 화면에 오른 뒤 — 세션 초안에 담고 사진첩 한 걸음(패널이 콜백으로 이음). */
|
|
rememberPipes: () => void;
|
|
/** 저장 배수유역 다시 받기(열 때와 같은 길 · 조용히) — 답을 화면에 올렸으면 true. */
|
|
loadSaved: () => Promise<boolean>;
|
|
/** 노선 · 관 자리가 바뀌어 낡음 — [자동 제안] 목록 닫기(510). */
|
|
onStale?: () => void;
|
|
}
|
|
|
|
export interface DrainageLive {
|
|
/** 계산 묶음 drainage 단계 — 노선 초안 · 관이 바뀐 차례에 부름(출력 없음). */
|
|
stage: StageFn<"drainage">;
|
|
/** [업데이트] — 지금 노선으로 처음부터 셈 · 답을 화면에 올렸으면 true. */
|
|
refresh(): Promise<boolean>;
|
|
/** 서버 답이 「낡음」 — 낡음 표시. 이미 낡음이면 그대로. */
|
|
markStale(): void;
|
|
/** [확정]이 서버에서 다시 셈 — 낡음 지움(떠 있는 요청도 끊음). */
|
|
markFresh(): void;
|
|
readonly stale: boolean;
|
|
suggest: DrainageSuggest;
|
|
}
|
|
|
|
/** 「지금 노선으로 셈」 — 서버 `post_drainage_live` · 작업본에 안 씀. */
|
|
async function fetchLive(
|
|
projectId: string,
|
|
body: ReturnType<typeof liveRequestBody>,
|
|
signal: AbortSignal,
|
|
): Promise<DetailBasinResponse & { status?: string; generation?: number }> {
|
|
const response = await fetch(`${API_BASE_URL}/projects/${projectId}/drainage/live`, {
|
|
method: "POST",
|
|
credentials: "include",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(body),
|
|
signal,
|
|
});
|
|
const payload = await response.json();
|
|
if (!response.ok) throw new Error(payload.message ?? `HTTP ${response.status}`);
|
|
return payload;
|
|
}
|
|
|
|
/** 끊은 차례를 서버에 알림 — 서버가 다음 단계 사이에서 셈을 멈춤(답 안 기다림 · 실패 무시). */
|
|
function cancelLive(projectId: string, generation: number): void {
|
|
void fetch(`${API_BASE_URL}/projects/${projectId}/drainage/live/cancel`, {
|
|
method: "POST",
|
|
credentials: "include",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ generation }),
|
|
}).catch(() => undefined);
|
|
}
|
|
|
|
const L = (key: keyof typeof ui_locales): string => ui_locales[key][currentLanguageIndex];
|
|
|
|
const UPDATE_LABEL = "업데이트";
|
|
const UPDATE_TIP =
|
|
"지금 노선으로 배수유역을 처음부터 다시 셉니다 — 뒤에서 셈 · 끝나면 그림을 바꿈.";
|
|
const UPDATE_STALE_TIP =
|
|
"노선 · 관 자리가 바뀌어 배수유역도가 낡았습니다 — 누르면 지금 노선으로 다시 셈.";
|
|
|
|
export function createDrainageLive(deps: DrainageLiveDeps): DrainageLive {
|
|
const viewport = deps.root.querySelector<HTMLElement>(".b05-drainage__viewport") as HTMLElement;
|
|
const header = deps.root.querySelector<HTMLElement>(".b05-drainage__header") as HTMLElement;
|
|
const propose = header.querySelector<HTMLButtonElement>(".b05-drainage__propose");
|
|
const update = deps.updateButton;
|
|
const suggest = createDrainageSuggest(deps.pipeRequest, (points) => {
|
|
const projectId = deps.runner.projectId();
|
|
if (!projectId) return;
|
|
void deps.runner
|
|
.run(() => computeDetailBasins(projectId, points))
|
|
.then((ok) => {
|
|
if (ok) deps.rememberPipes();
|
|
});
|
|
});
|
|
deps.basinList.before(suggest.root);
|
|
let route: unknown = null;
|
|
let stale = false;
|
|
/** 마지막 단계에 노선 초안이 있었나 — 없어지면 저장분으로 돌아옴. */
|
|
let drafted = false;
|
|
/** 저장분의 마지막 낡음 — 저장분 답이 「낡음」이면 `markStale` 이 세우고 저장 노선 [업데이트] 답이 걷음. */
|
|
let savedStale = false;
|
|
let generation = 0;
|
|
/** 마지막 단계 입력 지문 — 같으면 묶음이 다시 돈 것뿐. */
|
|
let key = "";
|
|
let inflight: AbortController | null = null;
|
|
/** 떠 있는 요청의 차례 — 끊을 때 서버에 알림. */
|
|
let inflightGeneration = 0;
|
|
|
|
const busy = createProgressCircle({ size: 22, label: "계산 중" });
|
|
busy.root.classList.add("b05-drainage__busy");
|
|
busy.root.hidden = true;
|
|
viewport.append(busy.root);
|
|
|
|
const paint = (): void => {
|
|
deps.root.classList.toggle("is-stale", stale);
|
|
setDrainageStale(stale); // 관 카드 설계유량 가림(요구 328-3)
|
|
update.classList.toggle("is-pending", stale);
|
|
update.disabled = inflight !== null;
|
|
update.textContent = inflight ? "계산 중…" : UPDATE_LABEL;
|
|
update.title = stale ? UPDATE_STALE_TIP : UPDATE_TIP;
|
|
suggest.setStale(stale);
|
|
if (propose) {
|
|
propose.disabled = stale;
|
|
propose.title = L(stale ? "B05_Drainage_Btn_Propose_Stale" : "B05_Drainage_Btn_Propose_Tip");
|
|
}
|
|
busy.root.hidden = inflight === null;
|
|
};
|
|
|
|
function cancel(): void {
|
|
if (!inflight) return;
|
|
inflight.abort();
|
|
inflight = null;
|
|
const projectId = deps.runner.projectId();
|
|
if (projectId) cancelLive(projectId, inflightGeneration);
|
|
}
|
|
|
|
/** 지금 노선으로 처음부터 셈 — 노선 초안이 있으면 그 노선 · 없으면 저장 노선. */
|
|
function refresh(): Promise<boolean> {
|
|
const projectId = deps.runner.projectId();
|
|
if (!projectId) return Promise.resolve(false);
|
|
cancel();
|
|
const sent = { generation: ++generation, key };
|
|
const sentRoute = route;
|
|
const controller = new AbortController();
|
|
inflight = controller;
|
|
inflightGeneration = sent.generation;
|
|
paint();
|
|
const body = liveRequestBody(sent.generation, route, deps.pipeRequest());
|
|
return deps.runner
|
|
.run(async () => {
|
|
const response = await fetchLive(projectId, body, controller.signal);
|
|
// 옛 차례 · 서버가 그만둔 답 — 감싸기가 조용히 버리게 끊김으로 돌려줌.
|
|
if (!acceptLive(response, sent, { generation, key }))
|
|
throw new DOMException("superseded", "AbortError");
|
|
return response;
|
|
}, true)
|
|
.then((applied) => {
|
|
if (inflight !== controller) return applied;
|
|
inflight = null;
|
|
if (applied) stale = false;
|
|
if (applied && !draftMakesStale(sentRoute)) savedStale = false; // 서버가 남김(520-2)
|
|
paint();
|
|
return applied;
|
|
});
|
|
}
|
|
|
|
/** 평면 · 관 자리 조작 — 떠 있는 요청을 끊고 낡음(저절로 셈하지 않음). */
|
|
function invalidate(): void {
|
|
cancel();
|
|
stale = true;
|
|
deps.onStale?.();
|
|
paint();
|
|
}
|
|
|
|
/** 초안이 없어짐 — 표시는 저장분의 마지막 낡음으로 바로 · 저장분을 다시 받아 그 답의 낡음대로. */
|
|
function backToSaved(): void {
|
|
cancel();
|
|
const sent = ++generation;
|
|
stale = savedStale;
|
|
savedStale = false; // 답이 「낡음」이면 apply 가 markStale 로 세움
|
|
paint();
|
|
void deps.loadSaved().then((applied) => {
|
|
if (sent !== generation) return;
|
|
stale = !applied || savedStale;
|
|
paint();
|
|
});
|
|
}
|
|
|
|
update.addEventListener("click", () => void refresh());
|
|
paint();
|
|
|
|
return {
|
|
stage(ctx) {
|
|
route = ctx.inputs.route;
|
|
const next = inputKey(route, ctx.inputs.pipes);
|
|
if (next === key) return null;
|
|
key = next;
|
|
const wasDraft = drafted;
|
|
drafted = draftMakesStale(route);
|
|
if (drafted || (stale && !wasDraft)) invalidate();
|
|
else if (wasDraft) backToSaved();
|
|
return null;
|
|
},
|
|
refresh,
|
|
suggest,
|
|
markStale() {
|
|
savedStale = true;
|
|
if (stale) return;
|
|
stale = true;
|
|
paint();
|
|
},
|
|
markFresh() {
|
|
cancel();
|
|
stale = false;
|
|
paint();
|
|
},
|
|
get stale() {
|
|
return stale;
|
|
},
|
|
};
|
|
}
|