feat(511-B B-2 stations 줄 엶): 계산 자리 판 stations(R5 R6 R7 F3) 바꿀 수 있음 · 기본 both 그대로 · 「서버」 = 묶음 stations 단계가 잠잠 뒤 서버 Node stations.list(같은 buildStations · 못 내면 화면 셈 · 화면 셈은 동기 그대로) · 창구가 stations.list 에 서버 구조물 정의를 붙임(브라우저 정의 안 씀) · 까닭 글 stations · 309-2 · 511-B1 시험 = 열린 줄 — 요구 320-4

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-22 14:27:06 +09:00
co-authored by Claude Opus 5.5
parent 32b841dda3
commit 9ed6a23d03
10 changed files with 106 additions and 14 deletions
@@ -27,6 +27,7 @@ import { publishPagePipeline } from "./design_pipeline_page_publish";
import { SOURCE_INPUT } from "./logic/design_pipeline_types";
export { pipelineWork } from "./logic/design_pipeline";
import { createStationsStage, snapOptions, stationDiff } from "./logic/design_pipeline_stations";
import { stationsOnServer } from "./design_stations_server";
import type {
ChangeSource,
FollowPatch,
@@ -293,6 +294,7 @@ function createPagePipeline(
intervalM: options.stationIntervalM,
types: () => enabled,
saved: () => plan,
remote: stationsOnServer, // 계산 자리 stations 「서버」(511-B B-2)
}),
ground: createGroundStage(groundDeps),
planline: createPlanlineStage({
@@ -0,0 +1,38 @@
/* =============================================================================
* A00_Common/design/design_stations_server.ts
* 계산 자리 stations 「서버」(PLAN 511-B B-2) — 묶음 stations 단계 측점 목록을 서버 Node
* `stations.list`(같은 `buildStations`)로. 판이 both 면 null(화면 셈) · 서버 값만 · 잠잠 뒤 ·
* 끊김 · 실패 = undefined(단계가 화면 셈으로). 구조물 정의는 서버 창구가 붙임.
* ========================================================================== */
import { plusStations } from "@util/common_util_structure_station";
import { calcOnServerNow } from "./design_calc_sites";
import { calcOnServerOnly } from "./design_calc_server";
import type { StationsArgs } from "./logic/design_pipeline_stations";
import type { PipelineStation } from "./logic/design_pipeline_types";
export function stationsOnServer(
args: StationsArgs,
signal: AbortSignal,
): Promise<PipelineStation[] | undefined> | null {
if (!calcOnServerNow("stations")) return null;
const [line, curves, pipes, structures, options] = args;
const stations = {
line,
curves,
pipes,
structures,
types: options.types,
plus: [...(options.plus ?? plusStations())],
interval_m: options.intervalM,
include_endpoint: options.includeEndpoint,
station_snap: options.stationSnap ?? false,
station_snap_step: options.stationSnapStep ?? null,
};
return calcOnServerOnly<PipelineStation[]>(
"stations",
"stations.list",
{ run: ["stations.list"], inputs: { stations }, pick: (results) => results["stations.list"] },
signal,
);
}
@@ -61,18 +61,27 @@ export function snapOptions(
};
}
/** stations 단계 — 입력 관 · 구조물(follow 뒤 누가거리) · 그린 노선 · 곡선표 · 스냅(초안 · 없으면 저장 노선). */
/** `buildStations` 인자 한 벌 — 화면 셈 · 서버 셈(계산 자리 「서버」)이 같은 값을 받음. */
export type StationsArgs = Parameters<typeof buildStations>;
/** stations 단계 — 입력 관 · 구조물(follow 뒤 누가거리) · 그린 노선 · 곡선표 · 스냅(초안 · 없으면 저장 노선).
* `remote` = 계산 자리 「서버」면 서버 Node 값(같은 `buildStations` · 511-B B-2) · null = 화면 셈 · 서버가
* 못 내면(끊김 · 실패) 화면 셈. */
export function createStationsStage(
options: Omit<StationsOptions, "stationSnap" | "stationSnapStep" | "types"> & {
types: () => ReadonlyArray<PlanType>;
saved?: () => SavedSnap | null;
remote?: (
args: StationsArgs,
signal: AbortSignal,
) => Promise<PipelineStation[] | undefined> | null;
},
): StageFn<"stations"> {
const { saved, ...rest } = options;
const { saved, remote, ...rest } = options;
return (ctx: StageContext<"stations">) => {
const route = ctx.out.route;
if (!route) return [];
const next = buildStations(
const args: StationsArgs = [
route.line,
route.curves,
(ctx.inputs.pipes as PlanPipe[] | null) ?? [],
@@ -83,8 +92,11 @@ export function createStationsStage(
...snapOptions(ctx.inputs.route, saved?.()),
plus: ctx.inputs.plusStations ?? undefined, // follow 가 옮긴 추가 측점(459 · 가게보다 먼저)
},
);
return reuseOutside(next, ctx.prev, ctx.range);
];
const done = (next: PipelineStation[] | undefined) =>
reuseOutside(next ?? buildStations(...args), ctx.prev, ctx.range);
const asked = remote?.(args, ctx.signal);
return asked ? asked.then(done) : done(undefined); // 화면 셈 = 동기 그대로
};
}
+5
View File
@@ -50,6 +50,11 @@ async def run_calc_endpoint(project_id: UUID, request: Request) -> dict | JSONRe
return _error(400, "inputs = 객체")
if SERVER_ONLY_IDS.intersection(run):
return _error(400, "서버 측점 생성은 창구 밖")
if "stations.list" in run and isinstance(inputs.get("stations"), dict):
# 심는 측점 · 카드 이름 표 = 서버 구조물 정의(서버 측점 생성과 같은 표 · 511-B B-2)
from B05_Profile.B05_Profile_Engine_Sections_Node import structure_table
inputs["stations"]["definition"] = (await asyncio.to_thread(structure_table))[1]
try:
answer = await asyncio.to_thread(run_calc_served, run, inputs)
except ValueError as exc:
+5 -4
View File
@@ -30,7 +30,7 @@ GROUPS: tuple[dict[str, Any], ...] = (
"why": "quantity",
},
{"key": "carry", "nos": ["R9", "F10"], "switchable": True, "fixed": None, "why": "carry"},
# 묶음 단계 다섯(511-B · 단계째 서버 Node `stage.*`) — 짝 모으기 B-2 ~ B-5 끝난 줄부터 엶
# 묶음 단계(511-B · 단계째 서버 Node `stage.*`) — 짝 모으기 B-3 ~ B-5 끝난 줄부터 엶
{"key": "route", "nos": ["R1", "R3"], "switchable": False, "fixed": "both", "why": "pair_B5"},
{
"key": "follow",
@@ -39,12 +39,13 @@ GROUPS: tuple[dict[str, Any], ...] = (
"fixed": "both",
"why": "pair_B5",
},
# 측점(511-B B-2 · 서버 측점 생성 = Node `stations.sections` · 조작 중 「서버」 = `stations.list`)
{
"key": "stations",
"nos": ["R5", "R6", "R7", "F3"],
"switchable": False,
"fixed": "both",
"why": "pair_B2",
"switchable": True,
"fixed": None,
"why": None,
},
{
"key": "profile",
@@ -5,6 +5,7 @@
"corridor_mesh": "both",
"quantity": "both",
"carry": "both",
"stations": "both",
"fixed_browser": "both"
}
}
+2 -1
View File
@@ -63,8 +63,9 @@ def test_묶음_여덟_줄_등록부_전부():
"corridor_mesh",
"quantity",
"carry",
"stations",
"fixed_browser",
} # 511-A 셋 더
} # 511-A 셋 더 · 511-B B-2 stations
for group in calc_sites.GROUPS:
got = {sites[no] for no in group["nos"]}
if group["fixed"] == "browser":
@@ -3,7 +3,7 @@
못박는 것
- 단계 id 다섯(`stage.route` · `follow` · `stations` · `profile` · `cross`) = TS 표 =
파이썬 `STAGE_IDS` · `available`(등록부 `node:`) 밖 · 창구(`run_calc_served`)가 받음.
- 판 = 단계 다섯 줄(잠금 · 기본 both · 까닭 「짝 모으는 중 · B-n」) · F9 = 폼 기본값 ·
- 판 = 단계 줄(잠금 · 기본 both · 까닭 「짝 모으는 중 · B-n」 · stations = B-2 가 엶) · F9 = 폼 기본값 ·
커밋한 판 그대로.
- 고정 자료(`fixtures/golden_472`) — Node 단계가 저장 노선 한 바퀴를 돎
(측점 = 저장 상세 측점 0.01 m).
@@ -78,7 +78,6 @@ def test_판_다섯_줄_잠금_기본값_그대로():
expected = {
"route": (["R1", "R3"], "pair_B5"),
"follow": (["P5", "P6", "F1", "F2"], "pair_B5"),
"stations": (["R5", "R6", "R7", "F3"], "pair_B2"),
"profile": (["P1", "P2", "P3", "P4", "P8"], "pair_B3"),
"cross": (["G2", "G4", "C1", "C2", "C3", "C5", "C6", "F4", "F5", "F6"], "pair_B4"),
}
@@ -93,6 +92,7 @@ def test_판_다섯_줄_잠금_기본값_그대로():
"corridor_mesh",
"quantity",
"carry",
"stations", # B-2 끝 = 엶(511-B B-2)
"fixed_browser",
}
committed = subprocess.run( # noqa: S603 — 고정 실행 파일
@@ -103,7 +103,7 @@ def test_판_다섯_줄_잠금_기본값_그대로():
).stdout
assert calc_sites.PATH.read_bytes().replace(b"\r\n", b"\n") == committed.replace(b"\r\n", b"\n")
locale = (ROOT / "ui_template" / "ui_template_locale_m1.ts").read_text(encoding="utf-8")
for n in (2, 3, 4, 5):
for n in (3, 4, 5):
assert f'B01_CalcSites_Why_pair_B{n}: ["짝 모으는 중 · B-{n}"' in locale
@@ -213,3 +213,32 @@ def test_이월_열쇠_Node_출력_없으면_파이썬(project):
assert keyed_stations([*stations, {"chainage_m": 1.0}]) is None # 섞이면(구조물 측점 덧) 파이썬
route_save = (ROOT / "B05_Profile/B05_Profile_Route_Save.py").read_text(encoding="utf-8")
assert route_save.count("keyed_stations(") == 2
def test_창구_측점_목록에_서버_정의_붙임(monkeypatch):
from fastapi import FastAPI
from fastapi.testclient import TestClient
from B06_Section import B06_Section_Router_Calc as calc_router
seen = []
monkeypatch.setattr(
calc_router, "run_calc_served", lambda run, inputs: seen.append(inputs) or {"results": {}}
)
app = FastAPI()
app.include_router(calc_router.router)
url = "/api/projects/11111111-2222-3333-4444-555555555555/calc"
body = {"run": ["stations.list"], "inputs": {"stations": {"line": [], "definition": {"x": 1}}}}
assert TestClient(app).post(url, json=body).status_code == 200
definition = seen[0]["stations"]["definition"]
assert definition != {"x": 1} and definition["types"] # 브라우저가 보낸 정의 안 씀
def test_판_stations_줄_열림_기본_both():
from common_util import common_util_calc_sites as calc_sites
group = next(g for g in calc_sites.GROUPS if g["key"] == "stations")
assert group["switchable"] and group["fixed"] is None
assert calc_sites.read_sites()["sites"]["stations"] == "both"
page = (ROOT / "A00_Common/design/design_pipeline_page.ts").read_text(encoding="utf-8")
assert "remote: stationsOnServer" in page
+4 -1
View File
@@ -115,7 +115,10 @@ export const ui_locales_m1 = {
"끄면 잠잠 뒤 서버 셈 · F10 = 브라우저 부름 없음(판만)",
"Off = server calc after a pause · F10 = no browser call",
],
B01_CalcSites_Why_pair_B2: ["짝 모으는 중 · B-2", "Merging pairs · B-2"],
B01_CalcSites_Why_stations: [
"끄면 잠잠 뒤 서버 측점(같은 TS) · [저장] 측점 = 서버 Node · DTM 아닌 지표면은 파이썬",
"Off = server stations after a pause (same TS) · [Save] stations = server Node · non-DTM surface = Python",
],
B01_CalcSites_Why_pair_B3: ["짝 모으는 중 · B-3", "Merging pairs · B-3"],
B01_CalcSites_Why_pair_B4: ["짝 모으는 중 · B-4", "Merging pairs · B-4"],
B01_CalcSites_Why_pair_B5: ["짝 모으는 중 · B-5", "Merging pairs · B-5"],