feat(B05 배수유역): 369-2 [자동 제안] 두 방식 · [배관 배치 초기화] = 세류선만 초안 · 제안 [적용] = 관경 · 시설만
- 배수유역 머리 [자동 제안] → 고름 창(⚙ 창 틀) ① 전체 자동(all) ② 지정 포함 추가(add) · 낡은 동안 끔 - [배관 배치 초기화] — 저장분 즉시 지움 없앰 · 관 목록 없이 셈(세류선 교차만) · 초안 - 셋 다 답이 오면 세션 초안 + 사진첩 한 걸음(400 ms 넘은 답도 제 걸음) - 제안 목록 [적용] — 관 자리 후보는 안 더함(점선 원 · 목록 그대로) · 관경 · 시설 제안 없으면 꺼짐 - computeDetailBasins(points | null, auto) · 글 = 툴팁 두 값 고침 · 새 글 끝에 새 키 - 시험 — 349 흐름에 자동 제안 · 347_2 · 298 · 312 고쳐 씀 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VmC34fsxxbweEG3RUjs3Mn
This commit is contained in:
@@ -620,14 +620,16 @@ export async function fetchDetailPipePoints(projectId: string): Promise<DetailBa
|
||||
|
||||
/** 편집 중인 관 목록으로 세부유역을 다시 나눈다(저장하지 않는다).
|
||||
*
|
||||
* 빈 목록 = 관 0개(312) · 기본 관 + 자동 보충으로 되돌리기는 `resetDetailPipePoints`(DELETE)만. */
|
||||
* 빈 목록 = 관 0개(312) · null = 세류선 교차 관만(369) · `auto`([자동 제안]) = "all" 전체 자동
|
||||
* (같은 자리 관은 속성 유지) · "add" 지금 관 + 빈 구간 보충. */
|
||||
export async function computeDetailBasins(
|
||||
projectId: string,
|
||||
points: DetailPipeInput[],
|
||||
points: DetailPipeInput[] | null,
|
||||
auto?: "all" | "add",
|
||||
): Promise<DetailBasinResponse> {
|
||||
return requestJson<DetailBasinResponse>(`/projects/${projectId}/drainage/detail-basins`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ points }),
|
||||
body: JSON.stringify(auto ? { points, auto } : { points }),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
* 배선(토글·포인터·그리기)은 전부 본체가 맡는다.
|
||||
* ========================================================================== */
|
||||
|
||||
import { currentLanguageIndex, ui_locales } from "@ui/ui_template_locale";
|
||||
import { createWorkflowPanelHandle } from "@ui/ui_template_overlay";
|
||||
import { createPanelResizer } from "@ui/ui_template_resizer";
|
||||
import { createProgressCircle } from "@ui/ui_template_progress";
|
||||
@@ -20,6 +21,8 @@ export interface DrainageChrome {
|
||||
analyzeButton: HTMLButtonElement;
|
||||
deleteButton: HTMLButtonElement;
|
||||
autoButton: HTMLButtonElement;
|
||||
/** [자동 제안](369) — 고름 창 · 배선은 `B05_Profile_UI_Drainage_Live`. */
|
||||
proposeButton: HTMLButtonElement;
|
||||
viewport: HTMLElement;
|
||||
backgroundImage: HTMLImageElement;
|
||||
canvas: HTMLCanvasElement;
|
||||
@@ -88,13 +91,18 @@ export function createDrainageChrome(
|
||||
deleteButton.textContent = labels.deleteSelected;
|
||||
deleteButton.title = labels.deleteSelectedTip;
|
||||
deleteButton.disabled = true;
|
||||
// 자동 제안으로 되돌리기 — 편집한 배관 배치를 버리고 백엔드 자동 제안으로 재산정.
|
||||
// 배관 배치 초기화 — 편집한 배관 배치를 버리고 세류선 교차 자리만(초안 · 369).
|
||||
const autoButton = document.createElement("button");
|
||||
autoButton.type = "button";
|
||||
autoButton.className = "b05-drainage__analyze b05-drainage__tool";
|
||||
autoButton.textContent = labels.auto;
|
||||
autoButton.title = labels.autoTip;
|
||||
header.append(analyzeButton, deleteButton, autoButton);
|
||||
const proposeButton = document.createElement("button");
|
||||
proposeButton.type = "button";
|
||||
proposeButton.className = "b05-drainage__analyze b05-drainage__tool b05-drainage__propose";
|
||||
proposeButton.textContent = ui_locales.B05_Drainage_Btn_Propose[currentLanguageIndex];
|
||||
proposeButton.title = ui_locales.B05_Drainage_Btn_Propose_Tip[currentLanguageIndex];
|
||||
header.append(analyzeButton, deleteButton, autoButton, proposeButton);
|
||||
|
||||
const viewport = document.createElement("div");
|
||||
viewport.className = "b05-drainage__viewport";
|
||||
@@ -155,6 +163,7 @@ export function createDrainageChrome(
|
||||
analyzeButton,
|
||||
deleteButton,
|
||||
autoButton,
|
||||
proposeButton,
|
||||
viewport,
|
||||
backgroundImage,
|
||||
canvas,
|
||||
|
||||
@@ -14,9 +14,13 @@
|
||||
* - 서버가 「낡음」으로 답하면(노선 [저장] 뒤 등) 같은 흐림 + [업데이트] · 자동이면 3 s 뒤.
|
||||
* - 답은 패널 요청 감싸기(`DetailRunner`, quiet)로 올림 — 옛 차례 · 서버가 그만둔 답은 버림.
|
||||
* - 덮개 · 잠금 없음 — 다른 칸 조작은 그대로.
|
||||
* - [자동 제안](369) — 고름 창 ① 전체 자동(all) ② 지정 포함 추가(add) · [배관 배치 초기화] = 세류선 교차
|
||||
* 관만 · 셋 다 초안(세션) · [저장] 때 정본 · 답이 오면 사진첩 한 걸음(늦은 답도 앞 걸음에 안 묻힘) ·
|
||||
* 낡은 동안 [자동 제안] 끔.
|
||||
* ========================================================================== */
|
||||
|
||||
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 { openModal } from "@ui/ui_template_modal";
|
||||
import { readStateRaw, writeStateRaw } from "../A00_Common/b_page_state";
|
||||
@@ -37,6 +41,7 @@ import {
|
||||
} 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";
|
||||
import { pageHistory } from "./B05_Profile_UI_Profile_History";
|
||||
|
||||
const MODE_KEY = "drainage-mode";
|
||||
|
||||
@@ -47,7 +52,7 @@ export interface DrainageLiveDeps {
|
||||
basinList: HTMLElement;
|
||||
pipeRequest: () => DetailPipeInput[];
|
||||
runner: DetailRunner;
|
||||
/** [적용] 답이 화면에 오른 뒤 — 세션 초안에 담음. */
|
||||
/** [적용] · [자동 제안] 답이 화면에 오른 뒤 — 세션 초안에 담음. */
|
||||
rememberPipes: () => void;
|
||||
/** 저장 배수유역 다시 받기(열 때와 같은 길 · 조용히) — 답을 화면에 올렸으면 true. */
|
||||
loadSaved: () => Promise<boolean>;
|
||||
@@ -62,6 +67,8 @@ export interface DrainageLive {
|
||||
markStale(): void;
|
||||
/** [확정]이 서버에서 다시 셈 — 낡음 지움(떠 있는 요청 · 기다림도 끊음). */
|
||||
markFresh(): void;
|
||||
/** [배관 배치 초기화] — 세류선 교차 관만으로 초안(369 · 저장분은 [저장] 때). */
|
||||
resetToStreams(): void;
|
||||
readonly stale: boolean;
|
||||
suggest: DrainageSuggest;
|
||||
}
|
||||
@@ -88,16 +95,27 @@ export function readDrainageMode(): DrainageMode {
|
||||
return parseDrainageMode(readStateRaw(MODE_KEY));
|
||||
}
|
||||
|
||||
const L = (key: keyof typeof ui_locales): string => ui_locales[key][currentLanguageIndex];
|
||||
|
||||
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 propose = header.querySelector<HTMLButtonElement>(".b05-drainage__propose");
|
||||
/** 관 초안 요청 — 답이 화면에 오르면 세션 초안 + 사진첩 한 걸음(400 ms 넘은 답도 제 걸음). */
|
||||
const draft = (request: (projectId: string) => Promise<DetailBasinResponse>): void => {
|
||||
const projectId = deps.runner.projectId();
|
||||
if (!projectId) return;
|
||||
void deps.runner
|
||||
.run(() => computeDetailBasins(projectId, points))
|
||||
.then((ok) => ok && deps.rememberPipes());
|
||||
});
|
||||
.run(() => request(projectId))
|
||||
.then((ok) => {
|
||||
if (!ok) return;
|
||||
deps.rememberPipes();
|
||||
pageHistory()?.record();
|
||||
});
|
||||
};
|
||||
const suggest = createDrainageSuggest(deps.pipeRequest, (points) =>
|
||||
draft((projectId) => computeDetailBasins(projectId, points)),
|
||||
);
|
||||
deps.basinList.before(suggest.root);
|
||||
let mode = readDrainageMode();
|
||||
let route: unknown = null;
|
||||
@@ -136,6 +154,10 @@ export function createDrainageLive(deps: DrainageLiveDeps): DrainageLive {
|
||||
const paint = (): void => {
|
||||
deps.root.classList.toggle("is-stale", stale && mode === "manual");
|
||||
suggest.setStale(stale);
|
||||
if (propose) {
|
||||
propose.disabled = stale;
|
||||
propose.title = L(stale ? "B05_Drainage_Btn_Propose_Stale" : "B05_Drainage_Btn_Propose_Tip");
|
||||
}
|
||||
veil.hidden = !stale || mode === "auto" || inflight !== null;
|
||||
busy.root.hidden = inflight === null;
|
||||
};
|
||||
@@ -223,6 +245,36 @@ export function createDrainageLive(deps: DrainageLiveDeps): DrainageLive {
|
||||
});
|
||||
}
|
||||
|
||||
/** [자동 제안] 고름 창 — ⚙ 창과 같은 틀 · 고르면 닫고 그 방식으로 초안. */
|
||||
function openPropose(): void {
|
||||
if (stale) return;
|
||||
openModal({
|
||||
title: L("B05_Drainage_Propose_Title"),
|
||||
closeLabel: "닫기",
|
||||
mount(body, close) {
|
||||
const choices: Array<["all" | "add", string, string]> = [
|
||||
["all", L("B05_Drainage_Propose_All"), L("B05_Drainage_Propose_All_Hint")],
|
||||
["add", L("B05_Drainage_Propose_Add"), L("B05_Drainage_Propose_Add_Hint")],
|
||||
];
|
||||
choices.forEach(([value, label, hint], order) => {
|
||||
const row = document.createElement("label");
|
||||
row.className = "b05-drainage__mode";
|
||||
const input = document.createElement("input");
|
||||
input.type = "radio";
|
||||
input.name = "b05-drainage-propose";
|
||||
input.addEventListener("change", () => {
|
||||
close();
|
||||
draft((projectId) => computeDetailBasins(projectId, deps.pipeRequest(), value));
|
||||
});
|
||||
const text = document.createElement("span");
|
||||
text.textContent = `${order === 0 ? "①" : "②"} ${label} — ${hint}`;
|
||||
row.append(input, text);
|
||||
body.append(row);
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function setMode(next: DrainageMode): void {
|
||||
mode = next;
|
||||
writeStateRaw(MODE_KEY, next);
|
||||
@@ -234,6 +286,7 @@ export function createDrainageLive(deps: DrainageLiveDeps): DrainageLive {
|
||||
|
||||
update.addEventListener("click", refresh);
|
||||
gear.addEventListener("click", openSettings);
|
||||
propose?.addEventListener("click", openPropose);
|
||||
|
||||
return {
|
||||
stage(ctx) {
|
||||
@@ -261,6 +314,7 @@ export function createDrainageLive(deps: DrainageLiveDeps): DrainageLive {
|
||||
stale = false;
|
||||
paint();
|
||||
},
|
||||
resetToStreams: () => draft((projectId) => computeDetailBasins(projectId, null)),
|
||||
get stale() {
|
||||
return stale;
|
||||
},
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
computeDetailBasins,
|
||||
fetchDetailPipePoints,
|
||||
getVWorldMapUrl,
|
||||
resetDetailPipePoints,
|
||||
saveDetailPipePoints,
|
||||
type DetailBasin,
|
||||
type DetailBasinResponse,
|
||||
@@ -416,10 +415,6 @@ export function createDrainagePanel(callbacks: DrainagePanelCallbacks = {}): Dra
|
||||
: fetchDetailPipePoints(projectId as string);
|
||||
};
|
||||
|
||||
/** 저장분까지 버리고 자동 배치로 되돌린다("초기화"). 화면만 되돌리면 다시 들어왔을 때
|
||||
* 옛 관이 살아난다(2026-08-02 사용자 보고). */
|
||||
const resetPipes = (): Promise<boolean> => run(() => resetDetailPipePoints(projectId as string));
|
||||
|
||||
/** 세부유역을 다시 나눈다(격자 해석 없음) — 같은 틱의 여러 부름은 한 번(342-3 · 옛 소수 관 옮김).
|
||||
* 시설 종류·구간·옵션은 계산기에서 사라지므로 요청에 되붙인다. */
|
||||
const analyze = batched(() => run(() => computeDetailBasins(projectId as string, pipeRequest())));
|
||||
@@ -435,7 +430,7 @@ export function createDrainagePanel(callbacks: DrainagePanelCallbacks = {}): Dra
|
||||
});
|
||||
analyzeButton.addEventListener("click", () => void analyze());
|
||||
deleteButton.addEventListener("click", () => void pipeEditor.deleteSelected());
|
||||
autoButton.addEventListener("click", () => void resetPipes());
|
||||
autoButton.addEventListener("click", live.resetToStreams); // 세류선만 · 초안(369)
|
||||
|
||||
/** 노선 전체가 보이도록 배율·중심을 맞춘다(초기 보기 = 도로 기준 줌인).
|
||||
*
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* =============================================================================
|
||||
* B05_Profile_UI_Drainage_Suggest.ts
|
||||
* 배수유역 칸 제안 목록(347-3) — 관경 · 시설 종류 · 관 자리 후보를 보여 주고 [적용] 한 번에 관 초안으로.
|
||||
* 배수유역 칸 제안 목록(347-3) — 관경 · 시설 종류 · 관 자리 후보를 보여 주고 [적용] 한 번에 관경 · 시설만
|
||||
* 관 초안으로(관 자리 = [자동 제안] · 369).
|
||||
* 모으기 · 얹기 규칙은 `logic/B05_Profile_Drainage_Suggest.ts` · 자동 바꿈 없음.
|
||||
* 관 자리 후보는 배수유역도에도 점선 원으로 그림(`candidates()` → 그리기 판).
|
||||
* ========================================================================== */
|
||||
@@ -26,7 +27,8 @@ const facilityName = (id: string): string => FACILITY_NAME[id] ?? id;
|
||||
/** 제안 한 줄 글. */
|
||||
export function suggestionText(item: Suggestion): string {
|
||||
const at = `측점 ${Math.round(item.chainageM)} m`;
|
||||
if (item.kind === "position") return `${at} — 관 자리 후보(흐름이 모이는 오목 자리)`;
|
||||
if (item.kind === "position")
|
||||
return `${at} — 관 자리 후보(흐름이 모이는 오목 자리 · [자동 제안])`;
|
||||
if (item.kind === "facility")
|
||||
return `${at} — ${facilityName(item.from)} → ${facilityName(item.to)} 검토`;
|
||||
return `${at} — 관경 ${item.from === null ? "미정" : `Ø${item.from}`} → Ø${item.to}`;
|
||||
@@ -56,13 +58,19 @@ export function createDrainageSuggest(
|
||||
apply.className = "b05-drainage__analyze";
|
||||
apply.textContent = "적용";
|
||||
const applyTitle =
|
||||
"제안을 관 초안에 한 번에 얹습니다(관 더함 · 관경 · 시설 바꿈) — [저장] 때 남습니다.";
|
||||
"관경 · 시설 제안을 관 초안에 한 번에 얹습니다 — [저장] 때 남습니다(관 자리는 [자동 제안]).";
|
||||
apply.title = applyTitle;
|
||||
head.append(title, apply);
|
||||
const list = document.createElement("ul");
|
||||
list.className = "b05-drainage__suggest-list";
|
||||
root.append(head, list);
|
||||
let suggestions: Suggestion[] = [];
|
||||
let stale = false;
|
||||
/** [적용] 켜짐 — 낡지 않고 관경 · 시설 제안이 있을 때(관 자리 후보만이면 할 일 없음). */
|
||||
const paintApply = (): void => {
|
||||
apply.disabled = stale || !suggestions.some((item) => item.kind !== "position");
|
||||
apply.title = stale ? "노선이 바뀌어 제안이 낡음 — 새 계산 뒤 적용" : applyTitle;
|
||||
};
|
||||
|
||||
apply.addEventListener("click", () => {
|
||||
if (suggestions.length && !apply.disabled)
|
||||
@@ -77,6 +85,7 @@ export function createDrainageSuggest(
|
||||
: collectSuggestions(response.pipe_points, response.basins, response.pipe_candidates ?? []);
|
||||
root.hidden = suggestions.length === 0;
|
||||
title.textContent = `제안 ${suggestions.length}건`;
|
||||
paintApply();
|
||||
list.replaceChildren(
|
||||
...suggestions.map((item) => {
|
||||
const row = document.createElement("li");
|
||||
@@ -85,9 +94,9 @@ export function createDrainageSuggest(
|
||||
}),
|
||||
);
|
||||
},
|
||||
setStale(stale) {
|
||||
apply.disabled = stale;
|
||||
apply.title = stale ? "노선이 바뀌어 제안이 낡음 — 새 계산 뒤 적용" : applyTitle;
|
||||
setStale(next) {
|
||||
stale = next;
|
||||
paintApply();
|
||||
},
|
||||
candidates: () =>
|
||||
suggestions.filter((item) => item.kind === "position").map((item) => item.chainageM),
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
/* =============================================================================
|
||||
* B05_Profile/logic/B05_Profile_Drainage_Suggest.ts
|
||||
* 배수유역 제안(347-3) — 관경 · 시설 종류 · 관 자리 후보를 모으고 [적용] 때 관 초안에 한 번에 얹음.
|
||||
* 배수유역 제안(347-3) — 관경 · 시설 종류 · 관 자리 후보를 모으고 [적용] 때 관경 · 시설만 관 초안에 얹음.
|
||||
* 순수 함수만(화면 = `B05_Profile_UI_Drainage_Suggest.ts`).
|
||||
*
|
||||
* - 관경 = 유역 추천이 배관이고 추천 관경이 지금 관경과 다를 때(「Ø→Ø 배관 제안」).
|
||||
* - 시설 = 유역 추천이 BOX암거 · 세월교이고 지금 관이 그 종류가 아닐 때.
|
||||
* - 관 자리 = 서버 `suggest_pipe_positions`(간격 보충 규칙 · 기존 관에서 먼 곳).
|
||||
* - 관 자리 = 서버 `suggest_pipe_positions`(간격 보충 규칙 · 기존 관에서 먼 곳) — 보이기만 · 더하기는
|
||||
* [자동 제안] ②(369 · 서버가 새 관에 등록부 기본값).
|
||||
* - 자동 바꿈 없음 — 모으기만 하고 바꾸는 것은 `applySuggestions`([적용])뿐.
|
||||
* ========================================================================== */
|
||||
|
||||
@@ -60,7 +61,7 @@ export function collectSuggestions(
|
||||
return found.sort((a, b) => a.chainageM - b.chainageM);
|
||||
}
|
||||
|
||||
/** [적용] — 제안을 관 초안에 한 번에(관 더함 · 관경 · 시설 바꿈). 입력 목록은 안 고침. */
|
||||
/** [적용] — 제안을 관 초안에 한 번에(관경 · 시설 바꿈 · 관 자리는 안 더함 · 369). 입력 목록은 안 고침. */
|
||||
export function applySuggestions<P extends PipeLike>(
|
||||
pipes: ReadonlyArray<P>,
|
||||
suggestions: ReadonlyArray<Suggestion>,
|
||||
@@ -69,15 +70,11 @@ export function applySuggestions<P extends PipeLike>(
|
||||
(pipe) => ({ ...pipe, options: pipe.options && { ...pipe.options } }) as P,
|
||||
);
|
||||
suggestions.forEach((item) => {
|
||||
if (item.kind === "position") {
|
||||
if (!next.some((pipe) => near(pipe.chainage_m, item.chainageM)))
|
||||
next.push({ chainage_m: item.chainageM, source: "spacing" } as P);
|
||||
return;
|
||||
}
|
||||
if (item.kind === "position") return;
|
||||
const pipe = next.find((candidate) => near(candidate.chainage_m, item.chainageM));
|
||||
if (!pipe) return;
|
||||
if (item.kind === "facility") pipe.facility = item.to;
|
||||
else pipe.options = { ...(pipe.options ?? {}), pipe_diameter_mm: item.to };
|
||||
});
|
||||
return next.sort((a, b) => a.chainage_m - b.chainage_m);
|
||||
return next;
|
||||
}
|
||||
|
||||
@@ -86,7 +86,9 @@ const flush = async () => {
|
||||
/* ---- 모듈 읽기(그 자리 트랜스파일 · 바깥 모듈은 가짜) ---- */
|
||||
const state = new Map();
|
||||
const detailCalls = [];
|
||||
let modeRow = 1; // ⚙ 창에서 고를 줄 — 0 수동 · 1 자동
|
||||
const detailAutos = []; // [자동 제안](369) 방식 — 없으면 null
|
||||
let records = 0; // 사진첩 한 걸음(369)
|
||||
let modeRow = 1; // ⚙ · [자동 제안] 창에서 고를 줄 — ⚙ 0 수동 · 1 자동 / 제안 0 전체 · 1 지정 포함
|
||||
const stubs = {
|
||||
"@config/config_frontend": { API_BASE_URL: "/api" },
|
||||
"@ui/ui_template_progress": { createProgressCircle: () => ({ root: el("progress") }) },
|
||||
@@ -98,14 +100,20 @@ const stubs = {
|
||||
body.children[modeRow].children[0].fire("change");
|
||||
},
|
||||
},
|
||||
"@ui/ui_template_locale": {
|
||||
currentLanguageIndex: 0,
|
||||
ui_locales: new Proxy({}, { get: (_target, key) => [key, key] }),
|
||||
},
|
||||
"./B05_Profile_UI_Profile_History": { pageHistory: () => ({ record: () => (records += 1) }) },
|
||||
"../A00_Common/b_page_state": {
|
||||
readStateRaw: (key) => state.get(key) ?? null,
|
||||
writeStateRaw: (key, value) => state.set(key, value),
|
||||
},
|
||||
"../B04_PreProcess/B04_PreProcess_Api_Fetch": {
|
||||
computeDetailBasins: async (projectId, points) => {
|
||||
computeDetailBasins: async (projectId, points, auto) => {
|
||||
detailCalls.push(points);
|
||||
return { basins: [], pipe_points: points, stale: null };
|
||||
detailAutos.push(auto ?? null);
|
||||
return { basins: [], pipe_points: points ?? [], stale: null };
|
||||
},
|
||||
},
|
||||
"./B05_Profile_UI_Drainage_Parts": { analyzeFailed: (_error, fallback) => fallback },
|
||||
@@ -135,6 +143,8 @@ async function main() {
|
||||
const viewport = el("viewport");
|
||||
const header = el("header");
|
||||
const root = el("root");
|
||||
const propose = el("button");
|
||||
header.querySelector = (selector) => (selector === ".b05-drainage__propose" ? propose : null);
|
||||
root.querySelector = (selector) =>
|
||||
selector === ".b05-drainage__viewport"
|
||||
? viewport
|
||||
@@ -280,7 +290,12 @@ async function main() {
|
||||
out.autoSavedDone = look();
|
||||
|
||||
// 349-0i 되돌리기 — 자동: 끌기 → 낡음 → Ctrl+Z(초안 없어짐) = 3 s 셈 대신 저장분
|
||||
const savedOk = { basins: [], pipe_points: pipes, stale: null };
|
||||
// 저장분 답에도 관경 제안 하나 — [적용] 켜짐은 낡음 · 관경/시설 제안 유무로 갈림(369)
|
||||
const savedOk = {
|
||||
basins: [{ chainage_m: 100, recommended_facility: "pipe", recommended_diameter_mm: 1200 }],
|
||||
pipe_points: pipes,
|
||||
stale: null,
|
||||
};
|
||||
stage(draft(20, 300));
|
||||
advance(1000);
|
||||
stage(null);
|
||||
@@ -308,6 +323,31 @@ async function main() {
|
||||
savedAnswer({ basins: [], pipe_points: pipes, stale: "배수유역 낡음" });
|
||||
await flush();
|
||||
out.undoSavedStale = { ...look(), savedLoads, mode: state.get("drainage-mode") };
|
||||
// 369 [자동 제안] — 낡은 동안 끔 · ① all · ② add · [배관 배치 초기화] = 관 목록 없이(세류선만)
|
||||
out.proposeStale = { off: propose.disabled, title: propose.title };
|
||||
const beforeStale = detailCalls.length;
|
||||
propose.fire("click");
|
||||
await flush();
|
||||
out.proposeStale.calls = detailCalls.length - beforeStale;
|
||||
live.markFresh();
|
||||
out.proposeFresh = { off: propose.disabled, title: propose.title };
|
||||
const n0 = detailCalls.length;
|
||||
const remembered0 = remembered;
|
||||
const records0 = records;
|
||||
modeRow = 0;
|
||||
propose.fire("click");
|
||||
await flush();
|
||||
modeRow = 1;
|
||||
propose.fire("click");
|
||||
await flush();
|
||||
live.resetToStreams();
|
||||
await flush();
|
||||
out.propose = {
|
||||
autos: detailAutos.slice(n0),
|
||||
points: detailCalls.slice(n0).map((points) => (points === null ? null : points.length)),
|
||||
remembered: remembered - remembered0,
|
||||
records: records - records0,
|
||||
};
|
||||
// 입력 지문 — 판 번호 · 관 차례는 안 봄 · 꼭짓점 · 관 자리(cm) · id 는 봄
|
||||
const { inputKey } = load(path.join(B05, "logic", "B05_Profile_Drainage_Live.ts"));
|
||||
const a = { id: "a1", chainage_m: 100 };
|
||||
|
||||
@@ -104,7 +104,7 @@ def test_B06_초안_쓰면_판이_한_번_다시_나눔():
|
||||
|
||||
|
||||
def test_다시_나눔은_세션_초안으로_계산만_저장_안_함():
|
||||
load = _body(PANEL, "const loadSaved = ", "const resetPipes")
|
||||
load = _body(PANEL, "const loadSaved = ", "const analyze = ")
|
||||
assert "readPendingPipes(projectId as string)" in load
|
||||
assert "computeDetailBasins(projectId as string, pending)" in load
|
||||
analyze = _body(PANEL, "const analyze = ", "analyzeButton.addEventListener")
|
||||
|
||||
@@ -161,7 +161,7 @@ def _body(rel: str, start: str, end: str) -> str:
|
||||
|
||||
def test_화면_초안_빈_목록은_저장분으로_안_넘어감() -> None:
|
||||
load = _body(
|
||||
"B05_Profile/B05_Profile_UI_Drainage_Panel.ts", "const loadSaved = ", "const resetPipes"
|
||||
"B05_Profile/B05_Profile_UI_Drainage_Panel.ts", "const loadSaved = ", "const analyze = "
|
||||
)
|
||||
assert "pending !== null" in load and "pending?.length" not in load
|
||||
# B06 목록도 같은 규칙(`??`)
|
||||
|
||||
@@ -51,8 +51,7 @@ def test_suggestions_collected(r):
|
||||
def test_apply_once_without_touching_input(r):
|
||||
assert r["untouched"]
|
||||
applied = {pipe["chainage_m"]: pipe for pipe in r["applied"]}
|
||||
assert sorted(applied) == [100, 200, 300, 500]
|
||||
assert sorted(applied) == [100, 300, 500] # 관 자리 후보(200)는 안 더함 — [자동 제안](369)
|
||||
assert applied[100]["options"]["pipe_diameter_mm"] == 1200
|
||||
assert applied[200]["source"] == "spacing"
|
||||
assert applied[300]["facility"] == "ford_bridge"
|
||||
assert applied[500]["facility"] == "box_culvert" # 이미 그 종류 — 그대로
|
||||
|
||||
@@ -64,10 +64,25 @@ def test_suggestions_and_apply(r):
|
||||
assert len(r["suggest"]["rows"]) == 2
|
||||
points = {p["chainage_m"]: p for p in r["applied"]["points"]}
|
||||
assert points[100]["options"]["pipe_diameter_mm"] == 1200
|
||||
assert points[200]["source"] == "spacing"
|
||||
assert 200 not in points # 관 자리 후보는 [적용]이 안 더함 — [자동 제안] 몫(369)
|
||||
assert r["applied"]["remembered"] == 1
|
||||
|
||||
|
||||
def test_자동_제안_두_방식_초기화_세류선만_한_걸음(r):
|
||||
assert r["proposeStale"] == {
|
||||
"off": True,
|
||||
"title": "B05_Drainage_Btn_Propose_Stale",
|
||||
"calls": 0, # 낡은 동안 끔 — 눌러도 요청 없음
|
||||
}
|
||||
assert r["proposeFresh"] == {"off": False, "title": "B05_Drainage_Btn_Propose_Tip"}
|
||||
assert r["propose"] == {
|
||||
"autos": ["all", "add", None],
|
||||
"points": [1, 1, None], # ① ② = 지금 관 목록 함께 · 초기화 = 목록 없이(세류선만)
|
||||
"remembered": 3, # 셋 다 초안
|
||||
"records": 3, # 답마다 사진첩 한 걸음
|
||||
}
|
||||
|
||||
|
||||
def test_move_while_busy_cancels(r):
|
||||
moved = r["movedWhileBusy"]
|
||||
assert moved["aborted"] and moved["veil"] and not moved["busy"]
|
||||
|
||||
@@ -34,8 +34,8 @@ export const ui_locales_b2 = {
|
||||
// (2026-08-19 사용자 지시 4).
|
||||
B05_Drainage_Btn_Analyze: ["유역 재분할", "Recompute sub-basins"],
|
||||
B05_Drainage_Btn_Analyze_Tip: [
|
||||
"B04에서 분석해 둔 배수유역을 불러와, 지금 배치된 배관을 기준으로 세부유역(관이 담당하는 구역)을 다시 나눕니다. 관이 부족한 구간은 자동으로 보충합니다. B04 분석 결과가 없으면 B04에서 먼저 실행해야 합니다.",
|
||||
"Reloads the B04 drainage analysis and re-splits sub-basins around the current culverts, adding culverts where spacing requires. Run the analysis in B04 first if none exists.",
|
||||
"B04에서 분석해 둔 배수유역을 불러와, 지금 배치된 배관을 기준으로 세부유역(관이 담당하는 구역)을 다시 나눕니다. 관을 더하지 않습니다 — 간격 보충은 [자동 제안]. B04 분석 결과가 없으면 B04에서 먼저 실행해야 합니다.",
|
||||
"Reloads the B04 drainage analysis and re-splits sub-basins around the current culverts without adding any — use [Auto suggest] to fill spacing gaps. Run the analysis in B04 first if none exists.",
|
||||
],
|
||||
B05_Drainage_Btn_DeleteSelected: ["관 삭제", "Delete selected culvert"],
|
||||
B05_Drainage_Btn_DeleteSelected_Tip: [
|
||||
@@ -44,8 +44,8 @@ export const ui_locales_b2 = {
|
||||
],
|
||||
B05_Drainage_Btn_Auto: ["배관 배치 초기화", "Reset culvert layout"],
|
||||
B05_Drainage_Btn_Auto_Tip: [
|
||||
"손으로 넣거나 옮기거나 지운 배관을 모두 버리고, 계곡부·간격 기준으로 프로그램이 자동 배치한 상태로 되돌립니다. 유역 구획도 함께 다시 나뉩니다.",
|
||||
"Discards every manual culvert edit and restores the automatic placement (valley crossings + spacing rule); sub-basins are re-split as well.",
|
||||
"손으로 넣거나 옮기거나 지운 배관을 모두 버리고, 세류선(계곡)이 노선과 만나는 자리에만 배관을 둡니다. 유역 구획도 함께 다시 나뉩니다. 초안이라 [저장] 때 남습니다.",
|
||||
"Discards every manual culvert edit and keeps culverts only where streams cross the route; sub-basins are re-split as well. This is a draft — it is kept on [Save].",
|
||||
],
|
||||
B05_Drainage_Menu_Add: ["배수관 추가", "Add culvert"],
|
||||
B05_Drainage_Menu_Delete: ["배수관 삭제", "Remove culvert"],
|
||||
@@ -1047,4 +1047,24 @@ export const ui_locales_b2 = {
|
||||
],
|
||||
B06_Std_Note_Rock: ["암", "Rock"],
|
||||
B06_Std_Note_Paved: ["포장", "Paved"],
|
||||
B05_Drainage_Btn_Propose: ["자동 제안", "Auto suggest"],
|
||||
B05_Drainage_Btn_Propose_Tip: [
|
||||
"관 자리를 프로그램이 정합니다(세류선 교차 + 최대 간격 보충) — 두 방식 중 고름. 초안이라 [저장] 때 남습니다.",
|
||||
"Let the program place culverts (stream crossings + max spacing rule) — choose one of two ways. This is a draft — it is kept on [Save].",
|
||||
],
|
||||
B05_Drainage_Btn_Propose_Stale: [
|
||||
"노선 · 관 자리가 바뀌어 배수유역이 낡음 — [업데이트] 뒤 자동 제안",
|
||||
"Drainage basins are out of date — run [Update] first",
|
||||
],
|
||||
B05_Drainage_Propose_Title: ["관 자리 자동 제안", "Auto-suggest culvert positions"],
|
||||
B05_Drainage_Propose_All: ["전체 자동", "Fully automatic"],
|
||||
B05_Drainage_Propose_All_Hint: [
|
||||
"세류선 교차 + 최대 간격 규칙으로 관 자리를 새로 정함(같은 자리 관은 시설 · 관경 유지)",
|
||||
"Re-place every culvert by stream crossings + max spacing (culverts at the same spot keep their facility and size)",
|
||||
],
|
||||
B05_Drainage_Propose_Add: ["지정 포함 추가", "Add to my culverts"],
|
||||
B05_Drainage_Propose_Add_Hint: [
|
||||
"지금 관은 그대로 두고 간격이 먼 구간에만 관을 더함",
|
||||
"Keep the current culverts and add culverts only where the spacing is too long",
|
||||
],
|
||||
} as const satisfies Record<string, LocaleEntry>;
|
||||
|
||||
Reference in New Issue
Block a user