From 7cc5c9ee91b7def273b0b784d6c98c8ee394bec3 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Wed, 21 Oct 2026 17:08:33 +0900 Subject: [PATCH 1/3] =?UTF-8?q?test(508-1a):=20=EB=8A=98=20=EB=B9=A8?= =?UTF-8?q?=EA=B0=84=20=EC=8B=9C=ED=97=98=2016=20=EA=B0=80=EB=A6=BC=20?= =?UTF-8?q?=E2=80=94=20233=5F4=20=EC=A7=9D=20=EB=B3=B8=EB=AC=B8=20?= =?UTF-8?q?=EC=B0=BE=EA=B8=B0=20=C2=B7=20349=20=EA=B3=84=EC=97=B4=20?= =?UTF-8?q?=EA=B8=B0=EB=8B=A4=EB=A6=BC=20=C2=B7=20=EB=82=A1=EC=9D=80=20?= =?UTF-8?q?=EA=B8=B0=EB=8C=80=20=EA=B3=A0=EC=B9=A8=20=C2=B7=20xfail=20=3D?= =?UTF-8?q?=20=EC=A0=9C=ED=92=88=20=ED=9D=A0=20=EB=91=98=EB=A7=8C=20=C2=B7?= =?UTF-8?q?=20309-2=20=ED=99=94=EB=A9=B4=20=EC=8B=9C=ED=97=98=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=20=ED=96=89=20=EC=A7=80=EC=9B=80=20=C2=B7=20=ED=8F=AC?= =?UTF-8?q?=ED=8A=B8=20=3D=20=EC=9D=B4=20=ED=8F=B4=EB=8D=94=20=EC=A4=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 233_4 다섯: 500-2 누름 잡는 단계(capture) 본문 찾기 - 0d: 410 좌표 낡음 판정 — 가짜 구조물 정수 측점 · 그 측점을 낳는 옆 좌표 - 0l · 0s · 0q · 0o 되돌리기: 472-5 덧 그리기 멈춤 풀림(pipelineHolding)까지 기다림 - pipeline_once: 77be773c0 상세 가게 칸 채움(keepSavedDetail) - 0u: 저장 곡선 반지름 초안 · 끈 꼭짓점 다음 꺾임점 기준 - 남은 xfail 둘(0o 끌기 · 횡단 보기 진입) = cross 군더더기 차례 제품 흠(a02367e61) - 309-2 화면: 표식 user_agent 로그 행 지움 · 포트 = OWNERS.md 이 폴더 줄(다른 폴더 사본이 8004 판을 고치던 흠) Co-Authored-By: Claude Opus 5.5 --- resources/tester/helper_233_4_empty_click.cjs | 17 ++++- resources/tester/helper_349_0l_undo_draw.cjs | 9 ++- resources/tester/helper_349_0o_redo_same.cjs | 8 ++- resources/tester/helper_349_0q_undo_echo.cjs | 6 +- resources/tester/helper_349_0s_draw_work.cjs | 9 ++- .../tester/helper_349_0u_follow_draft_xy.cjs | 9 ++- resources/tester/helper_349_pipeline_once.cjs | 3 + resources/tester/helper_verify_project.py | 4 +- .../tester/test_309_2_calc_sites_browser.py | 63 +++++++++++++++---- resources/tester/test_349_0d_follow_store.py | 21 ++++--- resources/tester/test_349_0l_undo_draw.py | 4 +- resources/tester/test_349_0o_redo_same.py | 1 - resources/tester/test_349_0q_undo_echo.py | 3 +- resources/tester/test_349_0s_draw_work.py | 5 +- .../tester/test_349_0u_follow_draft_xy.py | 22 +++++-- resources/tester/test_349_pipeline_once.py | 4 +- 16 files changed, 133 insertions(+), 55 deletions(-) diff --git a/resources/tester/helper_233_4_empty_click.cjs b/resources/tester/helper_233_4_empty_click.cjs index a9afb744a..027be2be3 100644 --- a/resources/tester/helper_233_4_empty_click.cjs +++ b/resources/tester/helper_233_4_empty_click.cjs @@ -33,7 +33,12 @@ const target = (inStation = false, inCard = false) => ({ /* ① 종단 그래프 빈 곳 누름 */ const render = read("B05_Profile", "B05_Profile_UI_Profile_Render.ts"); -const chartClick = block(render, ` chartWrap.addEventListener("click", (event) => {`, "\n });"); +// 500-2 잡는 단계(capture) 셋째 인자 — 본문은 `true,` 로 닫음 +const chartClick = block( + render, + ' chartWrap.addEventListener(\n "click",', + "\n true,\n );", +); const calls = []; let tool = false; const ctx = { @@ -52,7 +57,7 @@ const chartWrap = { getBoundingClientRect: () => ({ left: 0 }), scrollLeft: 0, }; -// 368-3 원지반선 근처 누름 = 작은 창 — 여기 빈 곳은 원지반선에서 멂(groundPickAt = null) +// 368-3 원지반선 근처 누름 = 작은 창 — 여기 빈 곳은 원지반선에서 멂(crossPickFromEvent = null · 500-2) const groundCalls = []; run( chartClick, @@ -64,9 +69,12 @@ run( "longitudinal", "width", "layout", - "groundPickAt", + "crossPickFromEvent", "openCrossModal", "graphData", + "body", + "graphLongitudinal", + "stationIntervalM", ], [ ctx, @@ -79,6 +87,9 @@ run( () => null, () => groundCalls.push("modal"), { samples: [] }, + {}, + { stations: [] }, + 20, ], ); handler({ target: target(), clientX: 42 }); diff --git a/resources/tester/helper_349_0l_undo_draw.cjs b/resources/tester/helper_349_0l_undo_draw.cjs index 3b3655992..7248b18be 100644 --- a/resources/tester/helper_349_0l_undo_draw.cjs +++ b/resources/tester/helper_349_0l_undo_draw.cjs @@ -183,14 +183,17 @@ const profileMod = load("A00_Common", "design", "design_profile.ts"); const tick = (ms = 0) => new Promise((resolve) => setTimeout(resolve, ms)); const runs = () => globalThis.__aisloPipeline?.runs ?? []; +const PID = "proj-349l"; /** 묶음 · 상세 가게 · 다시 셈이 조용해질 때까지. */ async function quiet() { let seen = -1; for (let i = 0; i < 400; i += 1) { await tick(30); const last = runs()[runs().length - 1]; - const count = runs().length + refreshCalls * 1000 + draws.length * 1e6; - if (count === seen && (!last || last.outcome !== "running")) return; + // 버림 그리기 멈춤(`pipelineHolding`)이 풀리고 그 다음 틀 그림까지(472-5 덧 — 묶음이 조용해진 뒤 풂) + const held = pageMod.pipelineHolding(PID); + const count = runs().length + refreshCalls * 1000 + draws.length * 1e6 + (held ? 0.5 : 0); + if (count === seen && !held && (!last || last.outcome !== "running")) return; seen = count; } throw new Error("묶음이 조용해지지 않음"); @@ -248,7 +251,7 @@ function settleDesigns(detail) { } async function run() { - const P = "proj-349l"; + const P = PID; const R = 7; window.localStorage.setItem("frd_current_project_id", P); opened = await sectionStore.loadSectionDetail(P, R); diff --git a/resources/tester/helper_349_0o_redo_same.cjs b/resources/tester/helper_349_0o_redo_same.cjs index ad39ac7ca..b95cd24e7 100644 --- a/resources/tester/helper_349_0o_redo_same.cjs +++ b/resources/tester/helper_349_0o_redo_same.cjs @@ -4,6 +4,7 @@ * 횡단 보기 진입 다시 셈 판정(`B06_Section_UI_Page_Design_Sync`). 화면 부품 · 서버 창구만 가짜 · 다시 셈 창구는 * 부른 수를 세고 면적을 두 배로(서버와 다른 값 흉내) · 낡음 판정은 진짜 화면 선 기준 그대로. * 되돌리기 · 다시 하기 = 화면 꼴(사진첩이 세션 노선 초안 칸을 먼저 씀 · 알림 두 번 · 349-0m). + * 잴 때 = 묶음 조용 + 초안 버림 그리기 멈춤이 풀린 뒤(472-5 덧 · 풀기 전엔 마지막 그림이 초안 상세). * 입력 argv[2] JSON · argv[3] 격자 이진. 결과 = 표준출력 JSON. 짝 = `test_349_0o_redo_same.py`. */ const fs = require("fs"); const path = require("path"); @@ -205,16 +206,18 @@ const stationKeys = load("B06_Section", "B06_Section_Station_Keys.ts"); // 진짜 다시 셈 창구(묶음 새 노선 상세를 안 고치는지만 봄 · 다른 곳은 위 가짜를 부름) const realRefresh = loadTs(path.join(ROOT, "B06_Section", "B06_Section_Cross_Refresh.ts")); +const P = "proj-349o"; const tick = (ms = 0) => new Promise((resolve) => setTimeout(resolve, ms)); const runs = () => globalThis.__aisloPipeline?.runs ?? []; -/** 묶음 · 상세 가게 · 다시 셈이 조용해질 때까지. */ +/** 묶음 · 상세 가게 · 다시 셈이 조용해질 때까지 — 초안 버림 그리기 멈춤(`pipelineHolding`)이 풀린 뒤까지(472-5 덧). */ async function quiet() { let seen = -1; for (let i = 0; i < 400; i += 1) { await tick(30); const last = runs()[runs().length - 1]; const count = runs().length + refreshCalls * 1000 + draws.length * 1e6; - if (count === seen && (!last || last.outcome !== "running")) return; + if (count === seen && (!last || last.outcome !== "running") && !pageMod.pipelineHolding(P)) + return; seen = count; } throw new Error("묶음이 조용해지지 않음"); @@ -272,7 +275,6 @@ function settleDesigns(detail) { } async function run() { - const P = "proj-349o"; const R = 7; window.localStorage.setItem("frd_current_project_id", P); opened = await sectionStore.loadSectionDetail(P, R); diff --git a/resources/tester/helper_349_0q_undo_echo.cjs b/resources/tester/helper_349_0q_undo_echo.cjs index 758af8ae2..9146264e3 100644 --- a/resources/tester/helper_349_0q_undo_echo.cjs +++ b/resources/tester/helper_349_0q_undo_echo.cjs @@ -247,14 +247,16 @@ const realRefresh = loadTs(path.join(ROOT, "B06_Section", "B06_Section_Cross_Ref const tick = (ms = 0) => new Promise((resolve) => setTimeout(resolve, ms)); const runs = () => globalThis.__aisloPipeline?.runs ?? []; -/** 묶음 · 상세 가게 · 다시 셈이 조용해질 때까지. */ +/** 묶음 · 상세 가게 · 다시 셈이 조용해질 때까지 — 초안 버림의 그리기 멈춤이 풀릴 때까지도(472-5 덧 · + * 풂 = 묶음 조용 50 ms 뒤 한 번 그림 · 30 ms 두 번만 보면 풀기 전에 끝나 다음 조작 창에 그림이 섞였음). */ async function quiet() { let seen = -1; for (let i = 0; i < 400; i += 1) { await tick(30); const last = runs()[runs().length - 1]; const count = runs().length + refreshCalls * 1000 + draws.length * 1e6; - if (count === seen && (!last || last.outcome !== "running")) return; + const held = pageMod.pipelineHolding("proj-349q"); + if (count === seen && (!last || last.outcome !== "running") && !held) return; seen = count; } throw new Error("묶음이 조용해지지 않음"); diff --git a/resources/tester/helper_349_0s_draw_work.cjs b/resources/tester/helper_349_0s_draw_work.cjs index ca4d987ab..83562a574 100644 --- a/resources/tester/helper_349_0s_draw_work.cjs +++ b/resources/tester/helper_349_0s_draw_work.cjs @@ -204,14 +204,17 @@ const realRefresh = loadTs(path.join(ROOT, "B06_Section", "B06_Section_Cross_Ref const tick = (ms = 0) => new Promise((resolve) => setTimeout(resolve, ms)); const runs = () => globalThis.__aisloPipeline?.runs ?? []; +const PID = "proj-349s"; /** 묶음 · 상세 가게 · 다시 셈이 조용해질 때까지. */ async function quiet() { let seen = -1; for (let i = 0; i < 400; i += 1) { await tick(30); const last = runs()[runs().length - 1]; - const count = runs().length + refreshCalls * 1000 + draws.length * 1e6; - if (count === seen && (!last || last.outcome !== "running")) return; + // 버림 그리기 멈춤(`pipelineHolding`)이 풀리고 그 다음 틀 그림까지(472-5 덧 — 묶음이 조용해진 뒤 풂) + const held = pageMod.pipelineHolding(PID); + const count = runs().length + refreshCalls * 1000 + draws.length * 1e6 + (held ? 0.5 : 0); + if (count === seen && !held && (!last || last.outcome !== "running")) return; seen = count; } throw new Error("묶음이 조용해지지 않음"); @@ -269,7 +272,7 @@ function settleDesigns(detail) { } async function run() { - const P = "proj-349s"; + const P = PID; const R = 7; window.localStorage.setItem("frd_current_project_id", P); opened = await sectionStore.loadSectionDetail(P, R); diff --git a/resources/tester/helper_349_0u_follow_draft_xy.cjs b/resources/tester/helper_349_0u_follow_draft_xy.cjs index 426aeac3b..0c1459cf4 100644 --- a/resources/tester/helper_349_0u_follow_draft_xy.cjs +++ b/resources/tester/helper_349_0u_follow_draft_xy.cjs @@ -119,10 +119,15 @@ async function quiet() { throw new Error("묶음이 조용해지지 않음"); } const n = job.nodes.length; +// 저장 곡선 그대로 연 편집기 칸(`flattenServerPlan` 꼴) — 꼭짓점 = 곡선 꼭지 · R = 저장 R(0.01 m) · 곡선 없는 자리 = 끔 +// (전부 켬 · R 비움이면 안 끈 자리 곡선도 기본 R 로 다시 깔려 노선이 통째로 바뀜) +const savedCurve = job.nodes.map((node) => + job.curves.find((c) => Math.hypot(c.apex[0] - node[0], c.apex[1] - node[1]) < 1e-3), +); const draftOf = (nodes) => ({ nodes, - curveOn: Array(n).fill(true), - curveRadius: Array(n).fill(null), + curveOn: savedCurve.map((curve) => curve !== undefined), + curveRadius: savedCurve.map((curve) => (curve ? Math.round(curve.radius_m * 100) / 100 : null)), curveLock: Array(n).fill(null), curveArc: Array(n).fill(null), stationSnap: job.stationSnap, diff --git a/resources/tester/helper_349_pipeline_once.cjs b/resources/tester/helper_349_pipeline_once.cjs index 8d7cf79ff..8f07aebc7 100644 --- a/resources/tester/helper_349_pipeline_once.cjs +++ b/resources/tester/helper_349_pipeline_once.cjs @@ -106,6 +106,8 @@ const pageMod = load("A00_Common", "design", "design_pipeline_page.ts"); const storeMod = load("A00_Common", "design", "design_store.ts"); const alignment = load("B05_Profile", "logic", "B05_Profile_UI_Profile_Alignment.ts"); const page = load("A00_Common", "b_page_state.ts"); +// 상세 가게 칸(472-5) — 실제는 상세 가게가 받을 때 채움 · 여기선 종단도가 내주는 저장 상세 그대로(갈아끼움 제외) +const slots = load("B06_Section", "B06_Section_Section_Canon.ts"); const tick = (ms = 0) => new Promise((resolve) => setTimeout(resolve, ms)); const runs = () => globalThis.__aisloPipeline?.runs ?? []; @@ -139,6 +141,7 @@ async function openOnce(P) { const savedAlignment = job.detail.longitudinal.profile_alignment; const savedBase = alignment.toAlignmentBase(savedAlignment); let detail = job.detail; + slots.keepSavedDetail(P, 7, job.detail); const swaps = []; const grades = []; const link = { diff --git a/resources/tester/helper_verify_project.py b/resources/tester/helper_verify_project.py index e5a367d9a..e2998d0ee 100644 --- a/resources/tester/helper_verify_project.py +++ b/resources/tester/helper_verify_project.py @@ -57,8 +57,8 @@ def find_project(*rels: str) -> Path: return min(found, key=lambda project: _touched(project, rels), default=NONE) -#: 검증 프로젝트로 처음 돌려 본 때(507-4) 이미 빨강 — 대부분 프로젝트와 무관(여섯 프로젝트 같음) · 옛 기대가 낡았는지 제품 흠인지 가려질 때까지(0q 메아리 = 돌 때마다 흔들림) +#: 제품 흠으로 보고한 시험만(508-1a) — 고치면 그 시험에서 걷음 DRIFT = pytest.mark.xfail( strict=False, - reason="507-4: 검증 프로젝트로 돌리니 빨강 · 흔들림 — 기대 낡음 또는 제품 흠(브레인 가림 전)", + reason="508-1a 제품 흠: 노선 초안 자리 칸 넘김이 cross 입력을 바꿔 cross 군더더기 차례(a02367e61)", ) diff --git a/resources/tester/test_309_2_calc_sites_browser.py b/resources/tester/test_309_2_calc_sites_browser.py index 1f880bac4..11f1eb950 100644 --- a/resources/tester/test_309_2_calc_sites_browser.py +++ b/resources/tester/test_309_2_calc_sites_browser.py @@ -3,13 +3,17 @@ - 시스템 관리자 = 「계산 자리」 카드 · 창(밝음 · 어두움 한 장씩 · tmp/shots_504_2) - 회사 관리자 · 일반 = 카드 없음 · PUT 403 - G1 체크 끔 → [저장] → B05 열기(검증_sub2) → 지반 표본이 서버 길(`/route/elevations`) · 판 기본값으로 되돌림 +- 시험 브라우저 = 표식 문자열(`MARK`) · 끝나면 그 표식 시스템 로그 행만 지움(508-1a) +- 포트 = OWNERS.md 이 폴더 줄 — 다른 폴더 사본이 남의 서버 판을 안 고침 ./venv/Scripts/python.exe -m pytest resources/tester/test_309_2_calc_sites_browser.py -q """ from __future__ import annotations +import asyncio import json +import re import subprocess import urllib.request from pathlib import Path @@ -17,12 +21,26 @@ from pathlib import Path import pytest ROOT = Path(__file__).resolve().parents[2] -BASE = "http://localhost:5177" -API = "http://localhost:8004" # 5177 이 프록시하는 이 폴더 백엔드(OWNERS.md) — 다른 폴더로 가면 그 폴더 마스터를 씀 + + +def _own_ports() -> list[str]: + """OWNERS.md 표에서 이 폴더 줄의 (API, vite) 포트(508-1a).""" + for line in (ROOT / "OWNERS.md").read_text(encoding="utf-8-sig").splitlines(): + cells = [cell.strip().strip("`") for cell in line.strip().strip("|").split("|")] + if len(cells) >= 5 and Path(cells[2]).resolve() == ROOT.resolve(): + return re.findall(r"\d{4,5}", cells[4]) + return [] + + +_PORTS = _own_ports() +API, BASE = ( + [f"http://localhost:{port}" for port in _PORTS] if len(_PORTS) == 2 else ["", ""] +) # 이 폴더 백엔드 · 그 백엔드를 프록시하는 vite BOARD = ROOT / "resources" / "master_template" / "calc_sites.json" PROJECT = "cb15892a-5788-4a26-9316-20a15421f68b" # 검증_sub2(OWNERS.md) SHOTS = ROOT / "tmp" / "shots_504_2" CARD = "계산 자리" +MARK = "AisloTest-309-2" # 시험 브라우저 문자열 — 시스템 로그 user_agent 칸으로 시험 행을 가림 def _started(base: str) -> str | None: @@ -33,10 +51,10 @@ def _started(base: str) -> str | None: return None -_UP = _started(BASE) -# 5177 이 이 폴더 8004 가 아닌 서버(예 8000 = 메인 폴더)로 가면 PUT 이 남의 판을 고침 → 돌리지 않음 +_UP = _started(BASE) if BASE else None +# vite 가 이 폴더 백엔드가 아닌 서버(예 8000 = 메인 폴더)로 가면 PUT 이 남의 판을 고침 → 돌리지 않음 pytestmark = pytest.mark.skipif( - _UP is None or _UP != _started(API), reason="5177 서버 없음 · 5177 이 8004 로 안 감" + _UP is None or _UP != _started(API), reason="이 폴더 서버 없음 · vite 가 이 폴더 백엔드로 안 감" ) @@ -47,6 +65,22 @@ def _cred(who: str) -> dict: return {k: accounts[who][k] for k in ("email", "password")} +def _drop_test_logs() -> None: + """시험이 남긴 시스템 로그(로그인 · CALC_SITE_CHANGE) = 표식 브라우저 문자열 행만 지움.""" + from config.config_db import close_db_pool, init_db_pool + + async def run(): + pool = await init_db_pool() + try: + async with pool.acquire() as connection, connection.cursor() as cursor: + await cursor.execute("DELETE FROM system_audit_logs WHERE user_agent = %s", (MARK,)) + await connection.commit() + finally: + await close_db_pool() + + asyncio.run(run()) + + @pytest.fixture(scope="module") def browser(): sync = pytest.importorskip("playwright.sync_api") @@ -54,17 +88,20 @@ def browser(): ["git", "show", "HEAD:resources/master_template/calc_sites.json"], cwd=ROOT, capture_output=True, check=True, ).stdout # fmt: skip - with sync.sync_playwright() as play: - chrome = play.chromium.launch(channel="msedge", headless=True) - try: - yield chrome - finally: # 실패해도 판 = 커밋한 기본값 그대로 - chrome.close() - BOARD.write_bytes(committed) + try: + with sync.sync_playwright() as play: + chrome = play.chromium.launch(channel="msedge", headless=True) + try: + yield chrome + finally: + chrome.close() + finally: # 실패해도 판 = 커밋한 기본값 그대로 · 로그 지움은 Playwright 고리 밖에서 + BOARD.write_bytes(committed) + _drop_test_logs() def _context(browser, who: str): - ctx = browser.new_context(viewport={"width": 1400, "height": 900}) + ctx = browser.new_context(viewport={"width": 1400, "height": 900}, user_agent=MARK) r = ctx.request.post(f"{BASE}/api/auth/login/request", data=_cred(who), timeout=60000) assert r.ok, f"로그인 실패 {r.status}" # 비밀번호는 안 찍음 return ctx diff --git a/resources/tester/test_349_0d_follow_store.py b/resources/tester/test_349_0d_follow_store.py index 07ec598ef..5e750a141 100644 --- a/resources/tester/test_349_0d_follow_store.py +++ b/resources/tester/test_349_0d_follow_store.py @@ -4,6 +4,7 @@ · 꼭짓점 한 번 끌기 = 묶음 한 번 · 가게 관 · 구조물 측점 바뀜 · 사진첩이 찍기 전(setTimeout 0)에 씀(한 걸음) · 새 노선에서 20 m 넘게 먼 구조물 수 알림 + · 좌표가 그 누가거리를 낳는 구조물만(아니면 410 낡음 = 노선 위로 · 알림 0) · 되돌리기(사진첩이 세션을 되씀) · 초안만 버림 = 초안 전 값(가게 비움 · 입력 = 저장 관) · 계산 · 알림 안 입력 쓰기 0 짝: `helper_349_0d_follow_store.cjs`(실제 TS 를 Node 로 · 서버 창구만 가짜 · 검증_sub7 노선 · 관). @@ -20,13 +21,14 @@ import tempfile from pathlib import Path import pytest +from shapely.geometry import LineString, Point HERE = Path(__file__).resolve().parent ROOT = HERE.parents[1] if str(ROOT) not in sys.path: sys.path.insert(0, str(ROOT)) -from helper_verify_project import DRIFT, ROUTE_SET, SKIP_REASON, find_project # noqa: E402 +from helper_verify_project import ROUTE_SET, SKIP_REASON, find_project # noqa: E402 from B05_Profile.B05_Profile_Structures_Schema import structure_type_map # noqa: E402 from common_util.common_util_surface_sampler import dtm_grid_bytes # noqa: E402 @@ -54,22 +56,29 @@ def _json(path: Path): def _structure(sid: str, line: list[list[float]], ratio: float, off: float) -> dict: - """노선 위(off 0) · 옆(off m) 점 구조물 — 저장 노선 누가거리 · 좌표.""" + """노선 위(off 0) · 옆(off m) 점 구조물 — 저장 노선 정수 m 누가거리 · 좌표. + 좌표가 그 누가거리를 낳는 쪽 옆(410 낡음 판정 밖 · 굽은 노선은 한쪽 옆이 + 다른 구간에 더 가까움).""" lengths = [math.dist(a, b) for a, b in zip(line, line[1:])] - at = sum(lengths) * ratio + at = float(round(sum(lengths) * ratio)) walked = 0.0 for (a, b), length in zip(zip(line, line[1:]), lengths): if walked + length >= at: t = (at - walked) / length dx, dy = (b[0] - a[0]) / length, (b[1] - a[1]) / length - x, y = a[0] + dx * length * t - dy * off, a[1] + dy * length * t + dx * off + foot = (a[0] + dx * length * t, a[1] + dy * length * t) break walked += length + route = LineString(line) + side = lambda s: (foot[0] - dy * s, foot[1] + dx * s) # noqa: E731 + x, y = next( + (side(s) for s in (off, -off) if round(route.project(Point(side(s)))) == at), side(off) + ) return { "structure_id": sid, "type_id": "erosion_check", "placement": "point", - "chainage_m": round(at, 2), + "chainage_m": at, "start_m": None, "end_m": None, "x": x, @@ -134,7 +143,6 @@ def test_열기만_하면_가게_안_씀(r): assert o["followed"] == [] -@DRIFT def test_꼭짓점_끌기_가게에_새_측점_한_번(r): d = r["drag"] assert d["runs"] == [["route"]], d["runs"] @@ -147,7 +155,6 @@ def test_꼭짓점_끌기_가게에_새_측점_한_번(r): assert d["blocked"] == 0 -@DRIFT def test_되돌리기_초안_전_값(r): u = r["undo"] assert u["runs"] == [["route"]], u["runs"] diff --git a/resources/tester/test_349_0l_undo_draw.py b/resources/tester/test_349_0l_undo_draw.py index f27cf70da..e73feb13d 100644 --- a/resources/tester/test_349_0l_undo_draw.py +++ b/resources/tester/test_349_0l_undo_draw.py @@ -4,6 +4,7 @@ · 진짜 종단도 낡음 판정 · crossPreview 예약을 거침 — 되돌린 뒤 다시 셈 0 · 낡음 0 · 묶음 기록에 되돌림 길(restored · 까닭) · 되돌리기 = 묶음 한 번 · 되돌리기 = 화면 Ctrl+Z 꼴(사진첩이 세션 노선 초안 칸을 먼저 비움 · 알림 두 번 · 349-0m) +· 되돌린 뒤 값 = 버림 그리기 멈춤이 풀린 다음 틀 그림(묶음이 조용해진 뒤 한 번 · 472-5 덧) 짝: `helper_349_0l_undo_draw.cjs`(실제 TS 를 Node 로 · 화면 부품 · 서버 창구만 가짜). """ @@ -23,7 +24,7 @@ ROOT = HERE.parents[1] if str(ROOT) not in sys.path: sys.path.insert(0, str(ROOT)) -from helper_verify_project import DRIFT, ROUTE_SET, SKIP_REASON, find_project # noqa: E402 +from helper_verify_project import ROUTE_SET, SKIP_REASON, find_project # noqa: E402 from B05_Profile.B05_Profile_Structures_Schema import structure_type_map # noqa: E402 from common_util.common_util_surface_sampler import dtm_grid_bytes # noqa: E402 @@ -130,7 +131,6 @@ def test_끌기_새_측점(r): assert d["final"] != r["open"]["final"] -@DRIFT def test_되돌리기_열기_값_다시_셈_0(r): u, o = r["undo"], r["open"] assert [(x["sources"], x["restored"], x["why"]) for x in u["runs"]] == [ diff --git a/resources/tester/test_349_0o_redo_same.py b/resources/tester/test_349_0o_redo_same.py index 813af1ec3..6b3afb5c3 100644 --- a/resources/tester/test_349_0o_redo_same.py +++ b/resources/tester/test_349_0o_redo_same.py @@ -157,7 +157,6 @@ def test_끌기_새_노선(r): assert d["final"] != r["open"]["final"] -@DRIFT def test_되돌리기_열기_값_측점_다름은_기록(r): o = r["open"] for u in (r["back1"], r["back2"]): diff --git a/resources/tester/test_349_0q_undo_echo.py b/resources/tester/test_349_0q_undo_echo.py index a270f1b3c..471700663 100644 --- a/resources/tester/test_349_0q_undo_echo.py +++ b/resources/tester/test_349_0q_undo_echo.py @@ -24,7 +24,7 @@ ROOT = HERE.parents[1] if str(ROOT) not in sys.path: sys.path.insert(0, str(ROOT)) -from helper_verify_project import DRIFT, ROUTE_SET, SKIP_REASON, find_project # noqa: E402 +from helper_verify_project import ROUTE_SET, SKIP_REASON, find_project # noqa: E402 from B05_Profile.B05_Profile_Structures_Schema import structure_type_map # noqa: E402 from common_util.common_util_surface_sampler import dtm_grid_bytes # noqa: E402 @@ -161,7 +161,6 @@ def test_되돌리기_열기_객체_그대로(r): assert d["pipeline"] > d["saved"] and isinstance(d["pipelineM"], float) -@DRIFT def test_메아리_차례_갈아끼움_없음_열기_값(r): o, e = r["open"], r["echo"] assert [x["outcome"] for x in e["runs"]], e["runs"] # 묶음이 실제로 돎 diff --git a/resources/tester/test_349_0s_draw_work.py b/resources/tester/test_349_0s_draw_work.py index cccd06b5f..90642b0e4 100644 --- a/resources/tester/test_349_0s_draw_work.py +++ b/resources/tester/test_349_0s_draw_work.py @@ -3,7 +3,7 @@ · 꼭짓점 끌기 다섯 걸음 · 되돌리기 — 걸음마다 종단도 그리기 ≤ 2(종단 먼저 · 횡단 · 유토곡선 다음 틀) (옛 길 = 갈아끼움 · 정책 · 이어 공사 기준 · 비정규 측점 · 새 상세 알림마다 바로 그림) · runs[].work — 가게 · 새 상세 · 종단도 · 쉼 이름별 ms(알림 받는 쪽 · 그리기 몫) -· 되돌리기 = 열기 값 규칙 그대로(restored) +· 되돌리기 = 열기 값 규칙 그대로(restored) · 그리기 = 버림 그리기 멈춤이 풀린 다음 틀(472-5 덧) 짝: `helper_349_0s_draw_work.cjs`(실제 TS 를 Node 로 · 화면 부품 · 서버 창구만 가짜). """ @@ -23,7 +23,7 @@ ROOT = HERE.parents[1] if str(ROOT) not in sys.path: sys.path.insert(0, str(ROOT)) -from helper_verify_project import DRIFT, ROUTE_SET, SKIP_REASON, find_project # noqa: E402 +from helper_verify_project import ROUTE_SET, SKIP_REASON, find_project # noqa: E402 from B05_Profile.B05_Profile_Structures_Schema import structure_type_map # noqa: E402 from common_util.common_util_surface_sampler import dtm_grid_bytes # noqa: E402 @@ -149,7 +149,6 @@ def test_그리기_몫_기록(r): assert x["totalMs"] >= x["stagesMs"] * 0.99 -@DRIFT def test_되돌리기_그대로(r): u = r["undo"] assert [x["why"] for x in u["runs"]] == ["saved-diff"], u["runs"] # 세월교 하나 더(0o 꼴) diff --git a/resources/tester/test_349_0u_follow_draft_xy.py b/resources/tester/test_349_0u_follow_draft_xy.py index d95a604fd..881c6a7ae 100644 --- a/resources/tester/test_349_0u_follow_draft_xy.py +++ b/resources/tester/test_349_0u_follow_draft_xy.py @@ -23,7 +23,7 @@ ROOT = HERE.parents[1] if str(ROOT) not in sys.path: sys.path.insert(0, str(ROOT)) -from helper_verify_project import DRIFT, ROUTE_SET, SKIP_REASON, find_project # noqa: E402 +from helper_verify_project import ROUTE_SET, SKIP_REASON, find_project # noqa: E402 from B05_Profile.B05_Profile_Structures_Schema import structure_type_map # noqa: E402 from common_util.common_util_drainage_pipes import ( # noqa: E402 @@ -134,14 +134,25 @@ def r() -> dict: timeout=300, ) assert proc.returncode == 0, proc.stderr - return json.loads(proc.stdout) + out = json.loads(proc.stdout) + # 끈 꼭짓점(1) 다음 꺾임점 — 그 뒤는 선 모양 그대로 + out["afterM"] = _near_m(line, job["nodes"][2]) + return out + + +def _near_m(line: list[list[float]], point: list[float]) -> float: + """저장 노선 위 그 점에 가장 가까운 꼭짓점의 누가거리.""" + walked, best = 0.0, (math.inf, 0.0) + for i, xy in enumerate(line): + walked += math.dist(line[i - 1], xy) if i else 0.0 + best = min(best, (math.dist(xy, point), walked)) + return best[1] def _moved(before: list, after: list) -> list[float]: return [b[1] - a[1] for a, b in zip(before, after)] -@DRIFT def test_앞쪽_꺾임점_끌기_측점_밀림(r): d = r["drag"] assert d["runs"] == [["route"]], d["runs"] @@ -150,15 +161,14 @@ def test_앞쪽_꺾임점_끌기_측점_밀림(r): assert grow > 5, grow # 앞쪽 길이 늚 pipes = _moved(r["draftPipes"], d["pipes"]) assert len(d["pipes"]) == len(r["draftPipes"]) - # 끈 꼭짓점 뒤 관 = 길이 는 만큼(정수 m 맞춤 · 1 m 안) — 대부분 그 뒤 - tail = [m for (_, c), m in zip(r["draftPipes"], pipes) if c > r["draftPipes"][2][1]] + # 끈 꼭짓점 다음 꺾임점 뒤 관 = 길이 는 만큼(정수 m 맞춤 · 1 m 안) — 관 수 · 자리는 프로젝트마다 + tail = [m for (_, c), m in zip(r["draftPipes"], pipes) if c > r["afterM"]] assert tail and all(abs(m - grow) <= 1.0 for m in tail), (grow, pipes) s = _moved(r["draftStructures"], d["structures"]) assert all(abs(m - grow) <= 1.0 for m in s), (grow, s) assert len(d["followed"]) == 1 -@DRIFT def test_되돌리기_초안_값(r): for u in (r["undo"], r["undo2"]): assert u["runs"] == [["route"]], u["runs"] diff --git a/resources/tester/test_349_pipeline_once.py b/resources/tester/test_349_pipeline_once.py index 295ce6ef5..60ab28a1c 100644 --- a/resources/tester/test_349_pipeline_once.py +++ b/resources/tester/test_349_pipeline_once.py @@ -23,7 +23,7 @@ ROOT = HERE.parents[1] if str(ROOT) not in sys.path: sys.path.insert(0, str(ROOT)) -from helper_verify_project import DRIFT, ROUTE_SET, SKIP_REASON, find_project # noqa: E402 +from helper_verify_project import ROUTE_SET, SKIP_REASON, find_project # noqa: E402 from B05_Profile.B05_Profile_Structures_Schema import structure_type_map # noqa: E402 from common_util.common_util_surface_sampler import dtm_grid_bytes # noqa: E402 @@ -103,7 +103,6 @@ def _same(a: list[float], b: list[float]) -> bool: return len(a) == len(b) and all(abs(x - y) < 1e-6 for x, y in zip(a, b)) -@DRIFT def test_열_때_한_번_저장_측점_그대로(r): o = r["open"] assert [x["outcome"] for x in o["runs"]] == ["done"], o["runs"] @@ -121,7 +120,6 @@ def test_꼭짓점_끌기_한_번(r): assert d["swaps"] == 1 # 새 노선 측점으로 갈아끼움은 한 번(고리 아님) -@DRIFT def test_되돌리기_한_번_저장값으로(r): u = r["undo"] assert [x["sources"] for x in u["runs"]] == [["route"]], u["runs"] From 396207035b0e65f9f507ac379861a1a5fa39604d Mon Sep 17 00:00:00 2001 From: umsangdon Date: Wed, 21 Oct 2026 17:18:30 +0900 Subject: [PATCH 2/3] =?UTF-8?q?fix(508-1c):=20=EB=A7=9E=EC=B6=A4=20cross?= =?UTF-8?q?=20=EA=B2=AC=EC=A4=8C=20=3D=20=EC=A7=80=EA=B8=88=20=EC=B8=A1?= =?UTF-8?q?=EC=A0=90=20=EC=9E=90=EB=A6=AC=EB=A1=9C=20=EA=B0=80=EB=A5=B8=20?= =?UTF-8?q?=EB=92=A4=20=E2=80=94=20=EB=85=B8=EC=84=A0=20=EC=B4=88=EC=95=88?= =?UTF-8?q?=20=EB=84=98=EA=B9=80=EC=9D=B4=20=EC=83=88=20=EC=9E=90=EB=A6=AC?= =?UTF-8?q?=20=EC=9E=90=EB=A6=AC=20=EC=B9=B8=EB=A7=8C=20=EA=B1=B7=EC=9D=80?= =?UTF-8?q?=20=EA=B5=B0=EB=8D=94=EB=8D=94=EA=B8=B0=20cross=20=EC=B0=A8?= =?UTF-8?q?=EB=A1=80=20=EC=97=86=EC=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 맞춤(reconcile) 다시 먹일 입력 판정을 design_pipeline_cross.ts staleInputs 로 · cross 는 crossAtSeat 뒤 견줌 - 끌기 cross 차례 · 횡단 보기 진입 cross 차례 사라짐(cross 셈 2 → 0) · 값(누가토량 · 횡단 · 측점 · 칸) 그대로 - design_pipeline_page.ts 699 → 697 줄 - test_349_0o xfail 둘 걷음 · 쓰지 않는 DRIFT 표지 지움 Co-Authored-By: Claude Opus 5.5 --- A00_Common/design/design_pipeline_cross.ts | 34 ++++++++++++++++++---- A00_Common/design/design_pipeline_page.ts | 6 ++-- resources/tester/helper_verify_project.py | 9 ------ resources/tester/test_349_0o_redo_same.py | 4 +-- 4 files changed, 32 insertions(+), 21 deletions(-) diff --git a/A00_Common/design/design_pipeline_cross.ts b/A00_Common/design/design_pipeline_cross.ts index 4382d9be0..cd22d4114 100644 --- a/A00_Common/design/design_pipeline_cross.ts +++ b/A00_Common/design/design_pipeline_cross.ts @@ -22,13 +22,20 @@ import { effectiveStandardCross, readRockBoundaryDefault, } from "../../B06_Section/B06_Section_UI_Standard_Panel"; -import type { - CrossInput, - SeatedFields, - SectionSpecs, +import { + crossAtSeat, + type CrossInput, + type SeatedFields, + type SectionSpecs, } from "../../B05_Profile/logic/B05_Profile_Pipeline_Detail"; import { SEAT_HINT } from "../../B06_Section/logic/B06_Section_Route_Carry"; -import type { PipelineStation } from "./logic/design_pipeline_types"; +import { + SOURCE_INPUT, + type ChangeSource, + type PipelineInputs, + type PipelineStation, +} from "./logic/design_pipeline_types"; +import type { DesignPipeline } from "./logic/design_pipeline"; import type { StationSeat } from "../../common_util/common_util_station_key"; export interface PipelineCrossSource { @@ -252,3 +259,20 @@ export function pipelineCrossSource( specs, }; } + +/** 맞춤이 다시 먹일 입력 — 지금 입력이 묶음 입력 · 넘긴 입력(follow 전) 둘 다와 다른 것. cross 는 지금 측점 자리로 + * 자리 칸을 가른 뒤 견줌(`crossAtSeat` — 노선 초안 넘김이 새 자리 자리 칸만 걷은 것 = 같은 값 · 508-1c). */ +export function staleInputs( + now: PipelineInputs, + given: Partial, + pipe: DesignPipeline, +): Array<[ChangeSource, unknown]> { + const stations = pipe.output("ground")?.stations ?? []; + const seen = (key: keyof PipelineInputs, value: unknown): string => + JSON.stringify(key === "cross" ? crossAtSeat(value as CrossInput | null, stations) : value); + return (Object.entries(SOURCE_INPUT) as Array<[ChangeSource, keyof PipelineInputs]>) + .filter(([, key]) => + [pipe.input(key), given[key]].every((was) => seen(key, was) !== seen(key, now[key])), + ) + .map(([source, key]) => [source, now[key]]); +} diff --git a/A00_Common/design/design_pipeline_page.ts b/A00_Common/design/design_pipeline_page.ts index 5aacf0107..8be170cbc 100644 --- a/A00_Common/design/design_pipeline_page.ts +++ b/A00_Common/design/design_pipeline_page.ts @@ -70,6 +70,7 @@ import { fetchStructureTypes } from "../../B05_Profile/B05_Profile_Api_Structure import { forgetPageData, pagePipes, pageStructures, trackPageBusy } from "./design_page_data"; import type { PagePipelineOptions, ProfileLink } from "./design_pipeline_page_types"; import { createPageSaved } from "./design_pipeline_page_saved"; +import { staleInputs } from "./design_pipeline_cross"; import { draftDetailOf, heldSectionDetail, @@ -563,10 +564,7 @@ function createPagePipeline( await pipe.settled(); if (disposed || pageSaved.canonRouteId() !== savedSource().routeId) return round; // 정본 바뀜 = restart 몫 if (facilities.saving()) return (facilities.afterSave(), round); // 저장 도중 = 옛 저장분 · 끝나면 다시 - const now = inputsNow(); - const stale = (Object.entries(SOURCE_INPUT) as Array<[ChangeSource, keyof PipelineInputs]>) - .filter(([, key]) => !same(now[key], pipe.input(key)) && !same(now[key], given[key])) - .map(([source, key]): [ChangeSource, unknown] => [source, now[key]]); + const stale = staleInputs(inputsNow(), given, pipe); // cross = 지금 측점 자리로 견줌(508-1c) if (!stale.length) return round; if (round >= RECONCILE_ROUNDS) { options.onUnstable?.(); diff --git a/resources/tester/helper_verify_project.py b/resources/tester/helper_verify_project.py index e2998d0ee..97c768b7d 100644 --- a/resources/tester/helper_verify_project.py +++ b/resources/tester/helper_verify_project.py @@ -9,8 +9,6 @@ from __future__ import annotations import os import re - -import pytest from pathlib import Path ROOT = Path(__file__).resolve().parents[2] @@ -55,10 +53,3 @@ def find_project(*rels: str) -> Path: and (project / "B06_Section" / "cross_sections").is_dir() ] return min(found, key=lambda project: _touched(project, rels), default=NONE) - - -#: 제품 흠으로 보고한 시험만(508-1a) — 고치면 그 시험에서 걷음 -DRIFT = pytest.mark.xfail( - strict=False, - reason="508-1a 제품 흠: 노선 초안 자리 칸 넘김이 cross 입력을 바꿔 cross 군더더기 차례(a02367e61)", -) diff --git a/resources/tester/test_349_0o_redo_same.py b/resources/tester/test_349_0o_redo_same.py index 6b3afb5c3..d3c40c79a 100644 --- a/resources/tester/test_349_0o_redo_same.py +++ b/resources/tester/test_349_0o_redo_same.py @@ -25,7 +25,7 @@ ROOT = HERE.parents[1] if str(ROOT) not in sys.path: sys.path.insert(0, str(ROOT)) -from helper_verify_project import DRIFT, ROUTE_SET, SKIP_REASON, find_project # noqa: E402 +from helper_verify_project import ROUTE_SET, SKIP_REASON, find_project # noqa: E402 from B05_Profile.B05_Profile_Structures_Schema import structure_type_map # noqa: E402 from common_util.common_util_surface_sampler import dtm_grid_bytes # noqa: E402 @@ -149,7 +149,6 @@ def test_열기(r): assert o["final"] is not None -@DRIFT def test_끌기_새_노선(r): d = r["first"] assert [x["sources"] for x in d["runs"]] == [["route"]], d["runs"] @@ -188,7 +187,6 @@ def test_다시_하기_첫_끌기_값(r): assert d["stations"] == f["stations"] -@DRIFT def test_횡단_보기_다녀와도_열기_값(r): o = r["open"] for step in (r["cross"], r["back"]): From 1ec0f12fa17f8583711aaa438189700bf0f743d5 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Wed, 21 Oct 2026 17:22:42 +0900 Subject: [PATCH 3/3] =?UTF-8?q?chore(=EC=9A=94=EA=B5=AC):=20=EC=9A=94?= =?UTF-8?q?=EA=B5=AC=20315=20=E2=80=94=20=EC=9E=90=EB=8F=99=20=EA=B4=80=20?= =?UTF-8?q?=ED=91=9C=EC=8B=9C=20=C2=B7=20=EC=B6=94=EC=B2=9C=20=EB=8B=A4?= =?UTF-8?q?=EC=8B=9C=20=EC=85=88=20=EC=95=88=20=ED=95=A8=20=C2=B7=20?= =?UTF-8?q?=EA=B3=84=EC=82=B0=2045=20=EC=A0=84=EB=B6=80=20=EA=B3=A0?= =?UTF-8?q?=EB=A5=B4=EA=B8=B0=20=EB=B0=A9=EB=B2=95=20=EB=AC=BC=EC=9D=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- docs/raw/요구_1021/사용자요구_1021.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/raw/요구_1021/사용자요구_1021.md b/docs/raw/요구_1021/사용자요구_1021.md index 52877baaf..9f0676f3f 100644 --- a/docs/raw/요구_1021/사용자요구_1021.md +++ b/docs/raw/요구_1021/사용자요구_1021.md @@ -37,3 +37,8 @@ 2. b05 b06의 자동관표시는 추천 노천의 미리뵉 모달창에 띄운다는 말인지? 추천 다시셈이라는게 전처리에 저장된값을 다시걔산한다는 말임? 3. b07의 횡단도가 도각이나 칸 밖으로나가는건 사용자가 조절해야하는 부분임. 4. 계산자리의 제어페이지를 만드는 이유는 프론트와 백엔드에서 일원화된 동작을 하게하고 테스트하면서 사용자편의성을 보고 선택하기 위함임. 이해함? + +## 요구 315 (10-21 · 사용자) + +1. 2번 필요없음. +2. 4번 45개를 전체다 선택 가능하게 하려면? 어떻게 해야함? 일단 방법만 공유