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:
2026-10-17 12:00:43 +09:00
co-authored by Claude Opus 5.5
parent c52a98a115
commit e5dc007b40
12 changed files with 184 additions and 44 deletions
@@ -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 };
+1 -1
View File
@@ -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")
+1 -1
View File
@@ -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"]