diff --git a/A00_Common/design/design_reset.ts b/A00_Common/design/design_reset.ts index 90de228bb..44b660b48 100644 --- a/A00_Common/design/design_reset.ts +++ b/A00_Common/design/design_reset.ts @@ -14,7 +14,7 @@ import { showLoadingOverlay, showToast, } from "@ui/ui_template_elements"; -import { clearDrafts, clearResults } from "./../b_page_state"; +import { clearResults, clearState, namesInBucket } from "./../b_page_state"; import { clearRouteLatestCache, resetRouteDesign } from "../../B05_Profile/B05_Profile_Api_Fetch"; import { clearAlignmentDrafts } from "../../B05_Profile/B05_Profile_UI_Profile_Edit"; import { invalidateSectionDetail } from "../../B06_Section/B06_Section_Section_Store"; @@ -43,7 +43,10 @@ export async function resetDesign( // 예전에는 파일마다 따로 지워 새 값이 늘 때 빠뜨리기 쉬웠다. 노선 범위 초안은 // 노선이 바뀌면 키가 달라져 자연히 딸려 오지 않는다. // 복원은 새 노선 번호를 내므로 모든 노선의 초안 · 결과를 버린다(PLAN 77). - clearDrafts(projectId); + // 저장 안 한 B08 · B09 초안은 남김 — [초기화]는 B05 · B06 범위만(418). + namesInBucket("draft") + .filter((name) => !name.startsWith("b08-") && !name.startsWith("b09-")) + .forEach((name) => clearState(name, projectId)); clearResults(projectId); onCleared?.(); // 계획선 편집 초안도 함께 버린다 — 남기면 초기값 위에 옛 편집이 다시 얹혀 계획선이 diff --git a/B05_Profile/B05_Profile_Api_Replan.ts b/B05_Profile/B05_Profile_Api_Replan.ts index c051965d5..fe5ca67a0 100644 --- a/B05_Profile/B05_Profile_Api_Replan.ts +++ b/B05_Profile/B05_Profile_Api_Replan.ts @@ -121,8 +121,6 @@ export interface RoutePlanResponse { station_snap_step?: number | null; /** 현재 초기값 — 서버가 [확인] 때 프로젝트에 적어 둔 초기노선 갈래(계획서 20). */ current_initial?: { logic: string; weights: Record | null }; - /** 전처리가 만든 초기 노선(예상노선 추적)의 노드 · 곡선 — [초기화]가 재계산 없이 놓는다. */ - initial_route?: { nodes: RoutePlanNode[]; curves: RoutePlanCurve[] }; } export interface RouteReplanResponse { diff --git a/B05_Profile/B05_Profile_Router_Replan.py b/B05_Profile/B05_Profile_Router_Replan.py index 64964fc03..51e6cdced 100644 --- a/B05_Profile/B05_Profile_Router_Replan.py +++ b/B05_Profile/B05_Profile_Router_Replan.py @@ -225,24 +225,6 @@ def _read_curves(path: Path) -> list[dict]: return curves if isinstance(curves, list) else [] -def _initial_shape(project_root: Path, fallback: list, radius_m: float) -> dict[str, list]: - """전처리가 만든 **초기 노선**의 노드 · 곡선 — 화면 [초기화]가 재계산 없이 놓는 자리. - - 수정본이 있어도 초기 폴리라인 파일은 그대로라 항상 같은 노선이 나온다. - """ - path = planned_route_initial_path(project_root) - saved_nodes = _vertices_of(_nodes_path(path)) - outline = build_planned_polyline( - [(x, y) for x, y in (saved_nodes or _vertices_of(path) or fallback)], - min_radius_m=radius_m, - simplify=not saved_nodes, - ) - return { - "nodes": [node.as_dict() for node in outline.nodes], - "curves": _read_curves(path) or [curve.as_dict() for curve in outline.curves], - } - - def _write_planned_polyline( path: Path, points: list[tuple[float, float]], @@ -545,7 +527,6 @@ async def read_route_plan(project_id: UUID) -> dict[str, Any] | JSONResponse: "station_snap": await asyncio.to_thread(load_station_snap, project_root), "station_snap_step": await asyncio.to_thread(load_station_snap_step, project_root), "current_initial": await asyncio.to_thread(read_initial_choice, project_root), - "initial_route": await asyncio.to_thread(_initial_shape, project_root, expected, radius_m), } diff --git a/B05_Profile/B05_Profile_UI_Panel_Recommend.ts b/B05_Profile/B05_Profile_UI_Panel_Recommend.ts index 7c9590928..f06e6fa7d 100644 --- a/B05_Profile/B05_Profile_UI_Panel_Recommend.ts +++ b/B05_Profile/B05_Profile_UI_Panel_Recommend.ts @@ -1,13 +1,12 @@ /* ============================================================================= * B05_Profile_UI_Panel_Recommend.ts - * 좌측 「노선 추천」 칸 재료(345-4 · 지운 「계획노선」 자리) — [노선 추천] 단추 · 「현재 추천: ○○」 · - * [초기화]. 노선 편집 머리에 있던 것을 옮겼다. 동작(추천 창 · 초기값 싣기)은 편집기의 - * `_RouteEdit_Initial` 이 이 요소를 받아 건다 — 여기는 생김새만. + * 좌측 「노선 추천」 칸 재료(345-4 · 지운 「계획노선」 자리) — [노선 추천] 단추 · 「현재 추천: ○○」. + * 노선 편집 머리에 있던 것을 옮겼다. 동작(추천 창)은 편집기의 `_RouteEdit_Initial` 이 이 요소를 + * 받아 건다 — 여기는 생김새만. 칸 [초기화]는 지움(418) — 되돌림은 좌측 아래 [초기화] 하나. * ========================================================================== */ export interface RecommendPart { recommendButton: HTMLButtonElement; - resetButton: HTMLButtonElement; nowLine: HTMLElement; /** 칸 몸 — 화면 순서대로. */ nodes: HTMLElement[]; @@ -20,10 +19,7 @@ export function createRecommendPart(): RecommendPart { actions.className = "b05-route__rec-actions"; actions.innerHTML = ` - `; + title="예상노선 추적 · 영선 · 토공 최소 · 기울기 최소 · 비중 계산 중 하나로 노선을 추천받아 미리 봅니다 — [적용]은 바로 저장 · 새 초기값">노선 추천`; const recommendButton = actions.querySelector('[data-act="recommend"]')!; - const resetButton = actions.querySelector('[data-act="reset"]')!; - return { recommendButton, resetButton, nowLine, nodes: [nowLine, actions] }; + return { recommendButton, nowLine, nodes: [nowLine, actions] }; } diff --git a/B05_Profile/B05_Profile_UI_RouteEdit.ts b/B05_Profile/B05_Profile_UI_RouteEdit.ts index b85081c91..ca0abbb99 100644 --- a/B05_Profile/B05_Profile_UI_RouteEdit.ts +++ b/B05_Profile/B05_Profile_UI_RouteEdit.ts @@ -618,7 +618,7 @@ export async function mountRouteEditor( ({ nodes: nodeInfo, curves: curveInfo } = flat); picked = -1; // 여기가 [연 상태로]가 돌아갈 자리다 — 창을 연 그대로. - initial.ready(snapshotNow(), plan.edited, plan.current_initial ?? null); + initial.ready(snapshotNow(), plan.current_initial ?? null); history = createRouteEditHistory(snapshotNow()); history = draftLink.watch(history, options.draft, snap.draft); draftLink.restore(options.draft, restoreSnapshot); diff --git a/B05_Profile/B05_Profile_UI_RouteEdit_Initial.ts b/B05_Profile/B05_Profile_UI_RouteEdit_Initial.ts index db204402d..4eaf30210 100644 --- a/B05_Profile/B05_Profile_UI_RouteEdit_Initial.ts +++ b/B05_Profile/B05_Profile_UI_RouteEdit_Initial.ts @@ -1,13 +1,13 @@ /* ============================================================================= * B05_Profile_UI_RouteEdit_Initial.ts * 계획노선 편집의 **노선 추천 이음**(계획서 20 · 23-2) — 좌측 「노선 추천」 칸(345-4) [노선 추천] 단추 → - * 모달(`_Recommend`) · [초기화] · 「현재 추천: ○○」 · [저장]에 실을 지금 추천. 칸 요소는 + * 모달(`_Recommend`) · 「현재 추천: ○○」 · [저장]에 실을 지금 추천. 칸 요소는 * 밖(`_Panel_Recommend`)에서 받는다. * * 모달의 알약이 서버에 갈래를 묻고(`/route/initial`) 미리 보기만 한다. [적용]을 눌러야 받은 노드를 - * **편집값에 싣고**(되돌리기 한 걸음) 캐시(`routeedit-initial`)에 적는다. [초기화]는 서버를 다시 - * 셈하지 않고 **전처리 때 만든 초기값 노선**(예상노선 추적)을 놓는다 — 그 노드는 `GET /route/plan` 의 - * `initial_route` 한 곳에서 읽는다. 손으로 고친 노선의 다시 계산은 페이지 [저장]에서(`_Apply` · 346-3) — + * **편집값에 싣고**(되돌리기 한 걸음) 캐시(`routeedit-initial`)에 적는다. 칸 [초기화]는 없음(418) — + * 되돌림은 좌측 아래 [초기화](`design_reset` · 마지막 [적용] 노선). 손으로 고친 노선의 다시 계산은 + * 페이지 [저장]에서(`_Apply` · 346-3) — * 추천 [적용]만 그 자리에서 서버 노선을 `as_initial` 로 저장한다(초기값 = 추천 노선 · 사용자 확정 · 잠금 막 · * 끝나면 페이지 다시 세움). 비교값 · 기준값은 서버가 준 것을 보이기만 한다. * @@ -17,7 +17,6 @@ * 요청했을 때 서버가 답으로 준 값(진짜 기본값)을 기억해 둔 것이다. * ========================================================================== */ -import { showToast } from "@ui/ui_template_elements"; import { fetchRoutePlan, type RoutePlanResponse } from "./B05_Profile_Api_Replan"; import { fetchPrecomputeProgress, @@ -31,7 +30,6 @@ import { choiceLabel, DEFAULT_WEIGHTS, NO_WEIGHT_LOCKS, - initialRouteSnapshot, normalizeRouteInitial, readInitialChoice, sameBase, @@ -49,7 +47,6 @@ export interface InitialPanelParams { /** 단추 · 글 요소 — 좌측 「노선 추천」 칸 것(345-4). */ controls: { recommendButton: HTMLButtonElement; - resetButton: HTMLButtonElement; nowLine: HTMLElement; }; projectId: string; @@ -62,8 +59,8 @@ export interface InitialPanelParams { } export interface InitialPanel { - /** 노선을 읽었다 — 창을 연 편집값 · 「고친 노선인가」 · 서버가 적어 둔 현재 초기값(`current_initial`). */ - ready: (opening: RouteEditSnapshot, edited: boolean, server?: unknown) => void; + /** 노선을 읽었다 — 창을 연 편집값 · 서버가 적어 둔 현재 초기값(`current_initial`). */ + ready: (opening: RouteEditSnapshot, server?: unknown) => void; /** [저장]에 실어 보낼 지금 추천. */ current: () => { mode: RouteInitialLogic; @@ -87,7 +84,7 @@ const copy = (snapshot: RouteEditSnapshot): RouteEditSnapshot => export function createInitialPanel(params: InitialPanelParams): InitialPanel { const { projectId } = params; - const { recommendButton, resetButton, nowLine } = params.controls; + const { recommendButton, nowLine } = params.controls; /** 창을 열 때 캐시에 있던 추천 — [취소]가 돌아갈 자리. */ let openingChoice = readInitialChoice(projectId); @@ -97,14 +94,13 @@ export function createInitialPanel(params: InitialPanelParams): InitialPanel { /** 칸을 비운 채 처음 물었을 때 서버가 답한 값 — placeholder 로 보인다(진짜 기본값). */ const defaults: Partial> = {}; let opening: RouteEditSnapshot | null = null; - let edited = false; let busy = false; /** 비중 · 고정 — 모달을 닫았다 열어도 이 편집 창이 사는 동안 남는다. */ const weightMemory = { weights: null as RouteInitialWeights | null, locks: { ...NO_WEIGHT_LOCKS }, }; - /** `/route/plan` 한 번 읽은 것 — 예상노선(지도 바탕) · 초기값 노선([초기화]). 창이 사는 동안 안 바뀐다. */ + /** `/route/plan` 한 번 읽은 것 — 예상노선(지도 바탕). 창이 사는 동안 안 바뀐다. */ let planRead: Promise | null = null; function loadPlan(): Promise { @@ -119,7 +115,6 @@ export function createInitialPanel(params: InitialPanelParams): InitialPanel { const now = choice ?? base; nowLine.textContent = `현재 추천: ${choiceLabel(now.logic, now.weights)}`; recommendButton.disabled = busy || !opening; - resetButton.disabled = busy || !opening; } /** 모달 알약 하나 — 서버에 갈래를 묻고 편집값 한 벌로 펴 돌려준다. 싣는 것은 [적용] 뒤. */ @@ -182,46 +177,11 @@ export function createInitialPanel(params: InitialPanelParams): InitialPanel { })(); }); - // [초기화] — 재계산 없이 전처리 때 만든 초기값 노선(예상노선 추적)을 다시 놓는다. - resetButton.addEventListener("click", () => { - void (async () => { - if (busy || !opening) return; - busy = true; - render(); - try { - const plan = await loadPlan().catch(() => null); - // 서버가 초기값 노선을 못 주면, 고친 적 없는 노선일 때만 창을 연 노선이 곧 초기값이다. - const snapshot = initialRouteSnapshot(plan?.initial_route) ?? (edited ? null : opening); - if (!snapshot) { - showToast("전처리 초기값 노선을 읽지 못했습니다.", "error"); - return; - } - if (!resetButton.isConnected) return; - choice = { - logic: "follow", - weights: null, - snapshot: copy(snapshot), - metrics: null, - criteria: null, - criteriaOverrides: null, - }; - writeInitialChoice(projectId, choice); - params.apply( - copy(snapshot), - "초기값(예상노선 추적)으로 되돌렸습니다 — 다시 계산은 [저장]에서.", - ); - } finally { - busy = false; - if (resetButton.isConnected) render(); - } - })(); - }); render(); return { - ready(snapshot, wasEdited, server) { + ready(snapshot, server) { opening = copy(snapshot); - edited = wasEdited; // 「현재 추천」은 서버가 [확인] 때 적어 둔 것이 정본이다 — 캐시가 다르면(확인 없이 닫음) // 캐시를 버린다. if (server) { diff --git a/B05_Profile/B05_Profile_UI_RouteEdit_Initial_Model.ts b/B05_Profile/B05_Profile_UI_RouteEdit_Initial_Model.ts index 49d0bda3a..213a3491a 100644 --- a/B05_Profile/B05_Profile_UI_RouteEdit_Initial_Model.ts +++ b/B05_Profile/B05_Profile_UI_RouteEdit_Initial_Model.ts @@ -233,13 +233,6 @@ function snapshotOf(nodes: unknown[], curves: unknown): RouteEditSnapshot { }; } -/** 서버 `/route/plan` 의 `initial_route`(전처리 초기값 노선)를 편집값으로 편다. 없거나 모자라면 null. */ -export function initialRouteSnapshot(initialRoute: unknown): RouteEditSnapshot | null { - const body = (initialRoute ?? {}) as Record; - if (!Array.isArray(body.nodes) || body.nodes.length < 2) return null; - return snapshotOf(body.nodes, body.curves); -} - /** 미리 보기 지도 한 겹 — 점 목록과 종류. */ export interface MapLayer { kind: "expected" | "current" | "preview"; diff --git a/resources/tester/helper_418_reset_drafts.cjs b/resources/tester/helper_418_reset_drafts.cjs new file mode 100644 index 000000000..ef1ac63a0 --- /dev/null +++ b/resources/tester/helper_418_reset_drafts.cjs @@ -0,0 +1,102 @@ +/* 418 좌측 아래 [초기화] 헬퍼 — 진짜 `design_reset` · `b_page_state` 를 Node 로 돌려 남은 초안을 + * 표준출력 JSON 한 벌로. 서버 · 확인창 · 다른 캐시 지우기는 가짜로 막음. + */ +const fs = require("fs"); +const path = require("path"); +const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); + +const ROOT = path.join(__dirname, "..", ".."); +const calls = []; +const STUBS = { + "@ui/ui_template_elements": { + showConfirmDialog: async () => true, + showLoadingOverlay: () => {}, + hideLoadingOverlay: () => {}, + showToast: (message, kind) => calls.push(["toast", kind]), + }, + "@ui/ui_template_locale": { + currentLanguageIndex: 0, + ui_locales: new Proxy({}, { get: (_, key) => [String(key)] }), + }, + B05_Profile_Api_Fetch: { + resetRouteDesign: async () => calls.push(["resetRouteDesign"]), + clearRouteLatestCache: () => {}, + }, + B05_Profile_UI_Profile_Edit: { clearAlignmentDrafts: () => {} }, + B06_Section_Section_Store: { invalidateSectionDetail: () => {} }, +}; + +const cache = new Map(); +const load = (file) => { + if (cache.has(file)) return cache.get(file); + const js = ts.transpileModule(fs.readFileSync(file, "utf8"), { + compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, + }).outputText; + const box = { exports: {} }; + cache.set(file, box.exports); + const near = (m) => { + const stub = STUBS[m] ?? STUBS[path.basename(m)]; + if (stub) return stub; + if (m.startsWith(".")) return load(path.join(path.dirname(file), `${m}.ts`)); + throw new Error(`안 막은 import: ${m}`); + }; + new Function("exports", "module", "require", js)(box.exports, box, near); + return box.exports; +}; + +class Store { + constructor() { + this.map = new Map(); + } + get length() { + return this.map.size; + } + key(i) { + return [...this.map.keys()][i] ?? null; + } + getItem(k) { + return this.map.has(k) ? this.map.get(k) : null; + } + setItem(k, v) { + this.map.set(k, String(v)); + } + removeItem(k) { + this.map.delete(k); + } +} +const session = new Store(); +globalThis.window = { sessionStorage: session, localStorage: new Store() }; +globalThis.location = { reload: () => calls.push(["reload"]) }; + +const state = load(path.join(ROOT, "A00_Common", "b_page_state.ts")); +const { resetDesign } = load(path.join(ROOT, "A00_Common", "design", "design_reset.ts")); + +const P = "p418"; +const OTHER = "q418"; +const SEED = { + "b08-materials": { units: { a: 1 } }, + "b08-hydro": { "0+020": { area: 1 } }, + "b08-options": { soft_rock_pct: 10 }, + "b08-milestone": { site: "x" }, + "b09-estimation": { estimate: { b: 2 } }, + "route-draft": { rev: 3 }, + "std-cross": { width: 4 }, +}; +for (const [name, value] of Object.entries(SEED)) { + state.writeState(name, value, P); + state.writeState(name, value, OTHER); +} +state.writeState("crossdesign", { "0+020": "cut" }, P, 7); // 노선 범위 B06 초안 + +(async () => { + await resetDesign(P, 7); + const left = {}; + for (const name of [...Object.keys(SEED), "crossdesign"]) { + left[name] = state.readState(name, P, name === "crossdesign" ? 7 : undefined); + } + const other = Object.fromEntries(Object.keys(SEED).map((n) => [n, state.readState(n, OTHER)])); + process.stdout.write(JSON.stringify({ left, other, seed: SEED, calls })); +})().catch((error) => { + process.stderr.write(String(error?.stack ?? error)); + process.exit(1); +}); diff --git a/resources/tester/helper_b05_routeedit_initial.cjs b/resources/tester/helper_b05_routeedit_initial.cjs index 1e673fdc1..a2e195fbd 100644 --- a/resources/tester/helper_b05_routeedit_initial.cjs +++ b/resources/tester/helper_b05_routeedit_initial.cjs @@ -41,7 +41,8 @@ function loadTs(file) { const localRequire = (request) => { if (STUBS[request]) return STUBS[request]; if (request.endsWith(".css")) return {}; - if (request.startsWith("@ui/")) return loadTs(path.join(ROOT, "ui_template", `${request.slice(4)}.ts`)); + if (request.startsWith("@ui/")) + return loadTs(path.join(ROOT, "ui_template", `${request.slice(4)}.ts`)); if (request.startsWith(".")) return loadTs(path.join(path.dirname(full), `${request}.ts`)); return require(request); }; @@ -215,18 +216,7 @@ const sameAsServer = { weightedOther: model.sameBase(planShape, bases.weighted), }; -// ⑥ 전처리 초기값 노선(`/route/plan` 의 `initial_route`)과 미리 보기 지도 선. -const initialRoute = { - ok: model.initialRouteSnapshot({ - nodes: [ - { x: 0, y: 0 }, - { x: 10, y: 5, radius_m: 20 }, - ], - curves: [], - }), - none: model.initialRouteSnapshot(undefined), - tooShort: model.initialRouteSnapshot({ nodes: [{ x: 0, y: 0 }], curves: [] }), -}; +// ⑥ 미리 보기 지도 선. const mapLines = { fit: model.mapPaths( [ @@ -548,7 +538,6 @@ process.stdout.write( cleared, bases, sameAsServer, - initialRoute, mapLines, historyRecs, sweep, diff --git a/resources/tester/test_345_4_recommend_panel.py b/resources/tester/test_345_4_recommend_panel.py index 20845504b..60d6915e6 100644 --- a/resources/tester/test_345_4_recommend_panel.py +++ b/resources/tester/test_345_4_recommend_panel.py @@ -33,7 +33,9 @@ def test_칸_자리와_재료(): def test_편집기가_칸_요소를_밖에서_받음(): initial = _read("B05_Profile/B05_Profile_UI_RouteEdit_Initial.ts") assert "overlay" not in initial - assert "const { recommendButton, resetButton, nowLine } = params.controls;" in initial + assert ( + "const { recommendButton, nowLine } = params.controls;" in initial + ) # 칸 [초기화] 지움(418) assert "current: params.planned().map(([x, y]) => [x, y])," in initial # 지금 노선 main = _read("B05_Profile/B05_Profile_UI_RouteEdit.ts") assert "controls: options.recommend," in main diff --git a/resources/tester/test_346_3_route_save_screen.py b/resources/tester/test_346_3_route_save_screen.py index a12e4f001..53e856ab3 100644 --- a/resources/tester/test_346_3_route_save_screen.py +++ b/resources/tester/test_346_3_route_save_screen.py @@ -129,7 +129,7 @@ def test_넘김_알림은_결과_한_줄에_합침(): def test_노선_반영_글자_없음(): recommend = _read("B05_Profile/B05_Profile_UI_Panel_Recommend.ts") - assert "노선 반영" not in recommend and "다시 계산은 [저장]에서" in recommend + assert "노선 반영" not in recommend # 칸 [초기화](「다시 계산은 [저장]에서」 글) 지움(418) for rel in ( "B05_Profile/B05_Profile_UI_RouteEdit.ts", "B05_Profile/B05_Profile_UI_RouteEdit_History.ts", diff --git a/resources/tester/test_418_reset_cleanup.py b/resources/tester/test_418_reset_cleanup.py new file mode 100644 index 000000000..0d4971071 --- /dev/null +++ b/resources/tester/test_418_reset_cleanup.py @@ -0,0 +1,124 @@ +"""418 초기화 정리(요구 254-3 · 253-6). + +- 「노선 추천」 칸 [초기화] 없음 · 그 단추만 쓰던 길(편집기 handler · `initial_route`) 없음 +- 좌측 아래 [초기화] — 저장 안 한 B08 · B09 초안 남김 · B05 · B06 초안만 지움(진짜 TS) +- 추천 [적용] → 고쳐 저장 → 아래 [초기화] = [적용]한 노선(진짜 `refresh_initial_snapshot`) +""" + +import asyncio +import json +import subprocess +from contextlib import asynccontextmanager +from pathlib import Path +from uuid import uuid4 + +import pytest + +from common_util.common_util_initial_snapshot import ( + restore_snapshot_files, + save_initial_snapshot, +) + +ROOT = Path(__file__).resolve().parents[2] +HELPER = Path(__file__).with_name("helper_418_reset_drafts.cjs") + + +def _read(rel: str) -> str: + return (ROOT / rel).read_text(encoding="utf-8") + + +def test_노선_추천_칸에_초기화_단추_없음(): + part = _read("B05_Profile/B05_Profile_UI_Panel_Recommend.ts") + assert 'data-act="reset"' not in part and "resetButton" not in part + assert part.count('data-act="recommend" disabled') == 1 + initial = _read("B05_Profile/B05_Profile_UI_RouteEdit_Initial.ts") + assert "resetButton" not in initial and "initialRouteSnapshot" not in initial + assert "initial_route" not in _read("B05_Profile/B05_Profile_Api_Replan.ts") + replan = _read("B05_Profile/B05_Profile_Router_Replan.py") + assert '"initial_route"' not in replan and "_initial_shape" not in replan + + +def test_아래_초기화는_B08_B09_초안_남김(): + out = json.loads( + subprocess.run( + ["node", str(HELPER)], capture_output=True, text=True, encoding="utf-8", check=True + ).stdout + ) + seed, left = out["seed"], out["left"] + for name in ("b08-materials", "b08-hydro", "b08-options", "b08-milestone", "b09-estimation"): + assert left[name] == seed[name], name + # B05 · B06 초안(프로젝트 범위 · 노선 범위)은 지움 + assert left["route-draft"] is None and left["std-cross"] is None + assert left["crossdesign"] is None + # 다른 프로젝트 초안은 안 건드림 + assert out["other"] == seed + assert [c[0] for c in out["calls"]] == ["resetRouteDesign", "toast", "reload"] + + +class _Cursor: + def __init__(self): + self.lastrowid = 0 + self._rows = [] + + async def __aenter__(self): + return self + + async def __aexit__(self, *_): + return False + + async def execute(self, sql, params=None): + self._rows = [{"id": 7, "project_id": "p1"}] if "FROM routes" in sql else [] + + async def fetchone(self): + return self._rows[0] if self._rows else None + + async def fetchall(self): + return self._rows + + +class _Connection: + def cursor(self, *_args, **_kwargs): + return _Cursor() + + +class _Pool: + @asynccontextmanager + async def acquire(self): + yield _Connection() + + +@pytest.fixture +def project(tmp_path: Path) -> Path: + (tmp_path / "B05_Profile/route").mkdir(parents=True) + return tmp_path + + +def _write_route(root: Path, tag: str) -> None: + (root / "B05_Profile/route/planned_route.csv").write_text(f"x,y\n{tag}\n", encoding="utf-8") + + +def _route(root: Path) -> str: + return (root / "B05_Profile/route/planned_route.csv").read_text(encoding="utf-8") + + +def test_추천_적용_뒤_고쳐도_아래_초기화는_적용_노선(project, monkeypatch): + from B05_Profile import B05_Profile_Router_RouteInitial as initial_router + + monkeypatch.setattr(initial_router, "get_db_pool", lambda: _Pool()) + # ① 자동설계 체인 직후 초기값 + _write_route(project, "auto") + asyncio.run(save_initial_snapshot(_Connection(), project, 7)) + # ② 추천 [적용](as_initial) — 그 노선으로 초기값 다시 뜸 + _write_route(project, "applied") + asyncio.run(initial_router.refresh_initial_snapshot(uuid4(), project, 7)) + # ③ 손으로 고쳐 [저장] — 초기값 그대로(as_initial 아님 · 스냅샷 안 부름) + _write_route(project, "edited") + # ④ 아래 [초기화] = 초기값 파일 복원 + restore_snapshot_files(project) + assert _route(project) == "x,y\napplied\n" + # ⑤ 다른 추천으로 바꿔 [적용] → 그것이 새 초기화 자리 + _write_route(project, "applied2") + asyncio.run(initial_router.refresh_initial_snapshot(uuid4(), project, 7)) + _write_route(project, "edited2") + restore_snapshot_files(project) + assert _route(project) == "x,y\napplied2\n" diff --git a/resources/tester/test_b05_routeedit_initial.py b/resources/tester/test_b05_routeedit_initial.py index 9eb2fefb2..e85e406b0 100644 --- a/resources/tester/test_b05_routeedit_initial.py +++ b/resources/tester/test_b05_routeedit_initial.py @@ -146,13 +146,12 @@ def test_캐시는_결과_통_프로젝트_칸(results): def test_전처리_초기값_노선을_편집값으로(results): - snap = results["initialRoute"]["ok"] - assert snap["planned"] == [[0, 0], [10, 5]] - assert snap["curveRadius"] == [None, 20] - assert snap["picked"] == -1 - # 서버가 안 주거나 노드가 모자라면 null — 화면이 [초기화]를 안 한다. - assert results["initialRoute"]["none"] is None - assert results["initialRoute"]["tooShort"] is None + """칸 [초기화]만 쓰던 전처리 초기 노선 펴기는 걷음(418) — 되돌림은 좌측 아래 [초기화] 하나.""" + assert "initialRoute" not in results + model = os.path.join( + HERE, "..", "..", "B05_Profile", "B05_Profile_UI_RouteEdit_Initial_Model.ts" + ) + assert "initialRouteSnapshot" not in open(model, encoding="utf-8").read() def test_미리_보기_지도_선(results): @@ -177,8 +176,7 @@ def test_도구_막대는_노선_추천_하나만(): assert "연 상태로" not in chrome and "history-reset" not in chrome part = open(os.path.join(page, "B05_Profile_UI_Panel_Recommend.ts"), encoding="utf-8").read() assert part.count('data-act="recommend" disabled') == 1 # 단추 하나 - assert part.count('data-act="reset" disabled') == 1 - assert part.index('data-act="recommend" disabled') < part.index('data-act="reset" disabled') + assert 'data-act="reset"' not in part # 칸 [초기화] 지움(418) initial = open( os.path.join(page, "B05_Profile_UI_RouteEdit_Initial.ts"), encoding="utf-8" ).read() diff --git a/resources/tester/test_b05_routeedit_initial_roundtrip.py b/resources/tester/test_b05_routeedit_initial_roundtrip.py index f30b98b36..d6c878eb3 100644 --- a/resources/tester/test_b05_routeedit_initial_roundtrip.py +++ b/resources/tester/test_b05_routeedit_initial_roundtrip.py @@ -5,7 +5,7 @@ ① `POST /route/initial` → 비교값이 다 있음 ② 화면 코드로 [확인] 몸통을 만들어 `POST /route/replan` → 서버가 initial 을 적음 ③ `GET /route/plan` → 현재 초기값 = 보낸 것 · 다시 연 노드 = 보낸 노드 - ④ [초기화] — 서버 `initial_route`(전처리 초기값 노선)를 놓음 · 재계산 없음 + ④ 칸 [초기화]용 `initial_route` 는 없음(418) — 되돌림은 좌측 아래 [초기화](초기값 스냅샷) 화면 쪽 셈은 `helper_b05_routeedit_initial_apply.cjs` 가 화면 코드 그대로 돌린다. 화면 제약 칸(종단기울기 · 최소 반지름 · 계류 이격 · 지반 고름, 계획서 20 확정)에 겹친 값도 @@ -267,19 +267,11 @@ def test_다시_연_노선은_확인한_노선(rounds): def test_초기화는_전처리_초기값_노선으로(rounds): - """[초기화] 는 재계산 없이 `GET /route/plan` 의 `initial_route` 를 놓는다 — 무엇을 확인했든 - 같은 노선이고, 확인 전(수정본 없음) 노드와 같다.""" - before = rounds["before"] - assert before["edited"] is False - first = [[n["x"], n["y"]] for n in before["nodes"]] + """칸 [초기화] 지움(418) — `GET /route/plan` 이 전처리 초기 노선(`initial_route`)을 안 준다.""" + assert rounds["before"]["edited"] is False for item in rounds["rounds"]: assert item["plan"]["edited"] is True - initial = item["plan"]["initial_route"] - assert len(initial["nodes"]) >= 2 and isinstance(initial["curves"], list) - assert _close([[n["x"], n["y"]] for n in initial["nodes"]], first), item["logic"] - # 확인한 마지막 노선은 초기값과 다르다 — 되돌림이 실제로 노선을 바꾼다. - last = rounds["rounds"][-1] - assert not _close(last["reopened"]["planned"], first) + assert "initial_route" not in item["plan"], item["logic"] def test_제약_겹침도_확인_뒤_그대로(api, rounds): diff --git a/resources/tester/test_design_page_single.py b/resources/tester/test_design_page_single.py index e44538814..5b4f0e082 100644 --- a/resources/tester/test_design_page_single.py +++ b/resources/tester/test_design_page_single.py @@ -100,7 +100,9 @@ def test_진행_단계_두_칸이_보기를_따라감(): def test_초기화_계획노선_편집은_다시_세움(): """[초기화] · [노선 반영](옛 계획노선 편집 [확인]) = 새 초기값 — 옛 캐시를 다 버리고 페이지를 통째로 다시 읽는다(PLAN 77).""" reset = _read("A00_Common/design/design_reset.ts") - assert "clearDrafts(projectId);" in reset and "clearResults(projectId);" in reset + assert ( + 'namesInBucket("draft")' in reset and "clearResults(projectId);" in reset + ) # B08 · B09 초안 남김(418) assert "location.reload()" in reset view = _read("A00_Common/design/design_view_profile.ts") assert "location.reload()" not in view # [노선 반영]은 새로고침 대신 다시 세움(345-3)