# -*- coding: utf-8 -*- """PLAN 298-1 — 신규 관 계열(배관 · 박스암거 · 세월교 · 물넘이포장) [추가] = 횡단도 생김 + 세부유역 뒤에서 다시 나눔. B05 [추가] = 배수유역도 판 `addPipe` → 다시 나눔(이미 있던 길) · B06 [추가] = 관 초안 → 판 `reload`(같은 길). 고친 것 — B06 길도 빈 월류 높이를 채움(126-1 · 판과 같은 기다림 표 `fordWaitsOf`) · 채운 높이를 B06 목록 · 폼이 받음 · B06 알림 글이 「저장 뒤 계산」이라 틀리던 것 · 다시 나눔 실패 = 토스트 한 줄. Node(`helper_298_add_reanalyze.cjs`) + 배선 대조. """ import json import subprocess from pathlib import Path import pytest HERE = Path(__file__).resolve().parent ROOT = HERE.parents[1] def _read(*parts: str) -> str: return ROOT.joinpath(*parts).read_text(encoding="utf-8") def _body(src: str, start: str, end: str) -> str: at = src.index(start) return src[at : src.index(end, at)] @pytest.fixture(scope="module") def out(): proc = subprocess.run( ["node", str(HERE / "helper_298_add_reanalyze.cjs")], capture_output=True, text=True, encoding="utf-8", timeout=60, ) assert proc.returncode == 0, proc.stderr return json.loads(proc.stdout) PANEL = _read("B05_Profile", "B05_Profile_UI_Drainage_Panel.ts") # 세부유역 요청 감싸기 — 347-2 에 판에서 나눔(`createDetailRunner` · 판이 `label` · `apply` 넘김) RUN = _read("B05_Profile", "B05_Profile_UI_Drainage_Run.ts") B06 = _read("B06_Section", "B06_Section_UI_Page_Structures_Panel.ts") CROSS = _read("A00_Common", "design", "design_view_cross.ts") # ── 126-1 채움 — B06 [추가]도 판과 같은 표 ────────────────────────────────── def test_B06_추가가_적은_기다림을_판이_채움(out): assert out["b06Wants"] is True and out["same"] is True assert out["filled"] is True assert out["heights"] == [out["need"], None] # 기다리던 측점만 · 옛 빈칸은 그대로 assert out["left"] == [] def test_다른_프로젝트_같은_측점은_안_채움(out): assert out["otherFilled"] is False assert out["otherHeights"] == [None, None] def test_B06_추가_순간_기다림_적고_초안_씀(): body = _body(B06, " onPipeAdd: (chainageM, attributes) => {", " onPipeUpdate:") order = [ body.index("pipeFacilities.push({"), body.index("if (waitsFordHeight(attributes.facility, attributes.options))"), body.index("fordWaitsOf(deps.projectId).add(at);"), body.index("persistPipes();"), # 초안 씀 = 카드 다시(바뀜 신호 하나 · 323-6) ] assert order == sorted(order) def test_판은_같은_표로_채움(): assert "const fordWaits = () => fordWaitsOf(projectId);" in PANEL assert "new Set()" not in _body(PANEL, "let loadSequence = 0;", "let canvasWidth") def test_채운_높이를_B06_목록_폼이_받음(): adopt = _body(B06, " adoptFordHeights: () => {", " releasePipe:") assert "readPendingPipes(deps.projectId)" in adopt assert "if (entry.options?.ford_height_m !== undefined) return false;" in adopt # 빈 칸만 assert "section.setPipeFacilities(pipeFacilities);" in adopt assert "persistPipes" not in adopt # 받기만 — 초안을 다시 안 씀(되돌이 없음) assert ( "pipesChanged: () => (structuresPanel.adoptFordHeights(), void refreshDetailForStructures())," in CROSS ) view = _read("B06_Section", "B06_Section_UI_Section_View_Profile.ts") sig = _body(view, "const signature = JSON.stringify(", "const changed") assert "p.options?.ford_height_m ?? null" in sig # 늦게 온 유량으로 채운 높이도 알림 # ── 다시 나눔 = 요청 한 번 · 저장 안 함 · 최신 응답만 · 잠금 없음 ──────────── def test_B06_초안_쓰면_판이_한_번_다시_나눔(): written = _body(CROSS, " onPipesWritten: () => {", " detail: () => sectionDetail,") assert "window.clearTimeout(pipeReloadTimer);" in written # 잇단 조작 = 한 번 assert "shared.profile?.side?.reload()" in written persist = _body(B06, " function persistPipes(): void {", " /** 관 옮기기") assert persist.index("writePendingPipes(") < persist.index("deps.onPipesWritten?.();") def test_다시_나눔은_세션_초안으로_계산만_저장_안_함(): load = _body(PANEL, "const loadSaved = ", "const resetPipes") assert "readPendingPipes(projectId as string)" in load assert "computeDetailBasins(projectId as string, pending)" in load analyze = _body(PANEL, "const analyze = ", "analyzeButton.addEventListener") assert "computeDetailBasins(" in analyze run = RUN[RUN.index("async run(") :] assert "saveDetailPipePoints" not in run + load + analyze # B05 [추가] = 판 addPipe → 배치 바뀜 → 초안 + 다시 나눔 commit = _body(PANEL, "const pipeEditor = createPipeEditor(", "(fromM, toM) =>") assert commit.index("rememberPipes();") < commit.index("void analyze();") def test_최신_응답만_잠금_없이_지도_칸_표시(): run = RUN[RUN.index("async run(") :] assert "const seq = ++requestSeq;" in run assert run.index("if (seq !== requestSeq) return false;") < run.index("deps.apply(response)") # 지도 칸 안 작은 표시(문구 · 지도 가운데 서클)만 — 화면 전체 잠금 없음 assert 'status.textContent = label("B05_Drainage_Status_Analyzing");' in run assert 'showProgress(null, label("B05_Drainage_Status_Analyzing"));' in run wire = _body(PANEL, "const runner = createDetailRunner({", "});") assert " apply," in wire and " label: L," in wire assert "Overlay" not in run and "lock" not in run.lower() chrome = _read("B05_Profile", "B05_Profile_UI_Drainage_Chrome.ts") assert "viewport.append(" in chrome and "progress.root," in chrome def test_실패하면_토스트_한_줄(out): run = RUN[RUN.index("async run(") :] assert ( 'status.textContent = analyzeFailed(error, label("B05_Drainage_Status_AnalyzeFailed"));' in run ) failed = out["failed"] assert failed["status"] == "502 Bad Gateway" assert failed["toasts"] == [ ["세부유역 산정에 실패했습니다. 502 Bad Gateway", "error"], ["세부유역 산정에 실패했습니다.", "error"], ] # ── 횡단도 생김 · 알림 글 ──────────────────────────────────────────────── def test_추가_뒤_새_측점_횡단_미리_뜸(): body = _body(B06, " onPipeAdd: (chainageM, attributes) => {", " onPipeUpdate:") assert "persistPipes();" in body # 초안 씀 → 구독(`watchDrafts`)이 카드 다시(323-6) # 카드 다시 = 바뀜 신호 하나(323-6 · 관 · 구조물 초안 구독 · 옛 되부름 갈음) assert ( "watchDrafts(projectId, () => host.visible() && void refreshDetailForStructures())" in CROSS ) drafts = _read("B06_Section", "B06_Section_Section_Drafts.ts") stations = _body( drafts, "export async function withDraftStations(", "parked.set(detail, stale);" ) assert "readPendingPipes(projectId)" in stations assert "previewOf(projectId, routeId, missing)" in stations def test_B06_알림_글_뒤에서_다시_나눔(): for name in ("PIPE_ADD_NOTICE", "PIPE_REMOVE_NOTICE", "PIPE_MOVE_NOTICE"): line = _body(B06, f"const {name} =", ";\n") assert "세부 배수유역은 뒤에서 다시 나눕니다." in line, name assert "[저장]·[확정] 뒤에" not in line, name def test_줄_수(): assert len(PANEL.splitlines()) <= 700 assert len(CROSS.splitlines()) <= 688