Files
Aislo/B05_Profile/B05_Profile_UI_Drainage_Live.ts
T
eomsangdonandClaude Opus 5.5 281c71b55b refactor(logic): 348-1 순수 계산 TS 49 파일 <페이지>/logic/ 로 옮김 — 이름 앞머리 그대로 · 동작 고침 없음
- 옮김: B04 2 · B05 23 · B06 11 · A00_Common/design 13 (9,299 줄) · 이미 logic/ 에 있던 16 파일 그대로
- 안 옮김: 묶음 입구(package.json build:*) · 파이썬 · 순수지만 계산 아닌 8 (보기 결선 · 콜백 · API 타입 · 갈아끼움 알림판)
- import 266 줄(142 파일) · 시험 · 주석 · 짝 표시 문자열 경로 고침 · B05 · B06 CLAUDE.md 짝 줄 logic/
- 시험 도우미 새 경로: 가짜 require 열쇠 · B05()/B06() 부름 · 형제 import 자리 · tsc 묶어 돌리는 시험 · 정규식 · rglob
- design_view_cross 줄 한도 시험 685 → 688 (긴 import 경로 줄바꿈 3줄)
- 확인: typecheck 0 · 서버 묶음 넷(corridor · server-calc · b07-extent · b07-note) 다시 지어 코드 같음(//#region 경로 주석만 다름) · 관련 시험 270 파일 — 남은 실패 19 는 옮기기 전과 같음

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014utrjfoW831Vc5wJUd13kL
2026-10-16 13:44:50 +09:00

226 lines
8.3 KiB
TypeScript

/* =============================================================================
* B05_Profile_UI_Drainage_Live.ts
* 배수유역 자동/수동 다시 셈(347-2) — 묶음 drainage 단계 · ⚙ 설정 창 · 흐림 + [업데이트] · 구석
* 「계산 중」 · 「지금 노선으로 셈」 요청(차례 번호 · 끊기). 규칙은 `logic/B05_Profile_Drainage_Live.ts`.
*
* - 단계(`stage`)는 계산 묶음(`design_pipeline` · 붙이기 = sub7)이 노선 초안 · 관이 바뀔 때 부름 —
* 기다리지 않고 null 을 돌려줌(배수유역은 뒤 단계 · 출력 없음).
* - 노선 초안이 있으면 「낡음」 — 자동 = 3 s 조용한 뒤 셈 · 수동 = 칸만 흐림 + 가운데 [업데이트].
* - 다시 조작하면 처음으로 — 떠 있는 요청을 끊고(AbortController) 다시 흐림 또는 다시 3 s.
* - 답은 패널 요청 감싸기(`DetailRunner`, quiet)로 올림 — 옛 차례 · 서버가 그만둔 답은 버림.
* - 덮개 · 잠금 없음 — 다른 칸 조작은 그대로.
* ========================================================================== */
import { API_BASE_URL } from "@config/config_frontend";
import { createProgressCircle } from "@ui/ui_template_progress";
import { openModal } from "@ui/ui_template_modal";
import { readStateRaw, writeStateRaw } from "../A00_Common/b_page_state";
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,
liveRequestBody,
parseDrainageMode,
QUIET_MS,
type DrainageMode,
} 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";
const MODE_KEY = "drainage-mode";
export interface DrainageLiveDeps {
/** 패널 뿌리 — 머리(⚙) · 지도(흐림 · 구석 「계산 중」)를 이 안에서 찾음. */
root: HTMLElement;
/** 제안 목록을 이 앞에 둠. */
basinList: HTMLElement;
pipeRequest: () => DetailPipeInput[];
runner: DetailRunner;
/** [적용] 답이 화면에 오른 뒤 — 세션 초안에 담음. */
rememberPipes: () => void;
}
export interface DrainageLive {
/** 계산 묶음 drainage 단계 — 노선 초안 · 관이 바뀐 차례에 부름(출력 없음). */
stage: StageFn<"drainage">;
/** 낡음을 지금 노선으로 다시 셈(자동 · [업데이트]와 같은 길). */
refresh(): 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;
}
export function readDrainageMode(): DrainageMode {
return parseDrainageMode(readStateRaw(MODE_KEY));
}
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 suggest = createDrainageSuggest(deps.pipeRequest, (points) => {
const projectId = deps.runner.projectId();
if (!projectId) return;
void deps.runner
.run(() => computeDetailBasins(projectId, points))
.then((ok) => ok && deps.rememberPipes());
});
deps.basinList.before(suggest.root);
let mode = readDrainageMode();
let route: unknown = null;
let stale = false;
let generation = 0;
let timer = 0;
let inflight: AbortController | null = null;
const veil = document.createElement("div");
veil.className = "b05-drainage__veil";
veil.hidden = true;
const update = document.createElement("button");
update.type = "button";
update.className = "b05-drainage__analyze b05-drainage__update";
update.textContent = "업데이트";
update.title = "노선 · 관 자리가 바뀌어 배수유역이 낡았습니다 — 지금 노선으로 다시 셉니다.";
veil.append(update);
const busy = createProgressCircle({ size: 22, label: "계산 중" });
busy.root.classList.add("b05-drainage__busy");
busy.root.hidden = true;
viewport.append(veil, busy.root);
const gear = document.createElement("button");
gear.type = "button";
gear.className = "b05-drainage__analyze b05-drainage__tool b05-drainage__gear";
gear.textContent = "⚙";
gear.title = "배수유역 계산 설정 — 수동(기본) / 자동";
header.append(gear);
const paint = (): void => {
deps.root.classList.toggle("is-stale", stale);
veil.hidden = !stale || mode === "auto" || inflight !== null;
busy.root.hidden = inflight === null;
};
function cancel(): void {
window.clearTimeout(timer);
timer = 0;
inflight?.abort();
inflight = null;
}
function refresh(): void {
const projectId = deps.runner.projectId();
if (!projectId) return;
cancel();
const sent = generation;
const controller = new AbortController();
inflight = controller;
paint();
const body = liveRequestBody(sent, route, deps.pipeRequest());
void deps.runner
.run(async () => {
const response = await fetchLive(projectId, body, controller.signal);
// 옛 차례 · 서버가 그만둔 답 — 감싸기가 조용히 버리게 끊김으로 돌려줌.
if (!acceptLive(response, sent, generation))
throw new DOMException("superseded", "AbortError");
return response;
}, true)
.then((applied) => {
if (inflight !== controller) return;
inflight = null;
if (applied) stale = false;
paint();
});
}
/** 평면 · 관 자리 조작 — 처음으로(요청 끊기 · 다시 흐림 또는 다시 3 s). */
function invalidate(): void {
generation += 1;
cancel();
stale = true;
if (mode === "auto") timer = window.setTimeout(refresh, QUIET_MS);
paint();
}
function openSettings(): void {
openModal({
title: "배수유역 계산",
closeLabel: "닫기",
mount(body, close) {
const choices: Array<[DrainageMode, string, string]> = [
["manual", "수동(기본)", "노선 · 관 자리가 바뀌면 칸만 흐려지고 [업데이트]를 누를 때 셈"],
["auto", "자동", "조작이 3초 멈추면 뒤에서 조용히 셈(화면을 막지 않음)"],
];
choices.forEach(([value, label, hint]) => {
const row = document.createElement("label");
row.className = "b05-drainage__mode";
const input = document.createElement("input");
input.type = "radio";
input.name = "b05-drainage-mode";
input.checked = mode === value;
input.addEventListener("change", () => {
setMode(value);
close();
});
const text = document.createElement("span");
text.textContent = `${label} — ${hint}`;
row.append(input, text);
body.append(row);
});
},
});
}
function setMode(next: DrainageMode): void {
mode = next;
writeStateRaw(MODE_KEY, next);
window.clearTimeout(timer);
timer = 0;
if (stale && next === "auto" && !inflight) timer = window.setTimeout(refresh, QUIET_MS);
paint();
}
update.addEventListener("click", refresh);
gear.addEventListener("click", openSettings);
return {
stage(ctx) {
route = ctx.inputs.route;
if (draftMakesStale(route) || stale) invalidate();
return null;
},
refresh,
suggest,
markFresh() {
cancel();
stale = false;
paint();
},
get stale() {
return stale;
},
};
}