From 9ed6a23d030fb8adc57b85161f9002161dc4aead Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 22 Oct 2026 14:27:06 +0900 Subject: [PATCH] =?UTF-8?q?feat(511-B=20B-2=20stations=20=EC=A4=84=20?= =?UTF-8?q?=EC=97=B6):=20=EA=B3=84=EC=82=B0=20=EC=9E=90=EB=A6=AC=20?= =?UTF-8?q?=ED=8C=90=20stations(R5=20R6=20R7=20F3)=20=EB=B0=94=EA=BF=80=20?= =?UTF-8?q?=EC=88=98=20=EC=9E=88=EC=9D=8C=20=C2=B7=20=EA=B8=B0=EB=B3=B8=20?= =?UTF-8?q?both=20=EA=B7=B8=EB=8C=80=EB=A1=9C=20=C2=B7=20=E3=80=8C?= =?UTF-8?q?=EC=84=9C=EB=B2=84=E3=80=8D=20=3D=20=EB=AC=B6=EC=9D=8C=20statio?= =?UTF-8?q?ns=20=EB=8B=A8=EA=B3=84=EA=B0=80=20=EC=9E=A0=EC=9E=A0=20?= =?UTF-8?q?=EB=92=A4=20=EC=84=9C=EB=B2=84=20Node=20stations.list(=EA=B0=99?= =?UTF-8?q?=EC=9D=80=20buildStations=20=C2=B7=20=EB=AA=BB=20=EB=82=B4?= =?UTF-8?q?=EB=A9=B4=20=ED=99=94=EB=A9=B4=20=EC=85=88=20=C2=B7=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20=EC=85=88=EC=9D=80=20=EB=8F=99=EA=B8=B0=20=EA=B7=B8?= =?UTF-8?q?=EB=8C=80=EB=A1=9C)=20=C2=B7=20=EC=B0=BD=EA=B5=AC=EA=B0=80=20st?= =?UTF-8?q?ations.list=20=EC=97=90=20=EC=84=9C=EB=B2=84=20=EA=B5=AC?= =?UTF-8?q?=EC=A1=B0=EB=AC=BC=20=EC=A0=95=EC=9D=98=EB=A5=BC=20=EB=B6=99?= =?UTF-8?q?=EC=9E=84(=EB=B8=8C=EB=9D=BC=EC=9A=B0=EC=A0=80=20=EC=A0=95?= =?UTF-8?q?=EC=9D=98=20=EC=95=88=20=EC=94=80)=20=C2=B7=20=EA=B9=8C?= =?UTF-8?q?=EB=8B=AD=20=EA=B8=80=20stations=20=C2=B7=20309-2=20=C2=B7=2051?= =?UTF-8?q?1-B1=20=EC=8B=9C=ED=97=98=20=3D=20=EC=97=B4=EB=A6=B0=20?= =?UTF-8?q?=EC=A4=84=20=E2=80=94=20=EC=9A=94=EA=B5=AC=20320-4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- A00_Common/design/design_pipeline_page.ts | 2 + A00_Common/design/design_stations_server.ts | 38 +++++++++++++++++++ .../design/logic/design_pipeline_stations.ts | 22 ++++++++--- B06_Section/B06_Section_Router_Calc.py | 5 +++ common_util/common_util_calc_sites.py | 9 +++-- resources/master_template/calc_sites.json | 1 + resources/tester/test_309_2_calc_sites.py | 3 +- .../tester/test_511_b1_calc_node_stages.py | 6 +-- resources/tester/test_511_b2_stations_node.py | 29 ++++++++++++++ ui_template/ui_template_locale_m1.ts | 5 ++- 10 files changed, 106 insertions(+), 14 deletions(-) create mode 100644 A00_Common/design/design_stations_server.ts diff --git a/A00_Common/design/design_pipeline_page.ts b/A00_Common/design/design_pipeline_page.ts index 7812afd9b..bf82e8c2f 100644 --- a/A00_Common/design/design_pipeline_page.ts +++ b/A00_Common/design/design_pipeline_page.ts @@ -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({ diff --git a/A00_Common/design/design_stations_server.ts b/A00_Common/design/design_stations_server.ts new file mode 100644 index 000000000..03ca39ab0 --- /dev/null +++ b/A00_Common/design/design_stations_server.ts @@ -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 | 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( + "stations", + "stations.list", + { run: ["stations.list"], inputs: { stations }, pick: (results) => results["stations.list"] }, + signal, + ); +} diff --git a/A00_Common/design/logic/design_pipeline_stations.ts b/A00_Common/design/logic/design_pipeline_stations.ts index 053a4d96b..873d1d92a 100644 --- a/A00_Common/design/logic/design_pipeline_stations.ts +++ b/A00_Common/design/logic/design_pipeline_stations.ts @@ -61,18 +61,27 @@ export function snapOptions( }; } -/** stations 단계 — 입력 관 · 구조물(follow 뒤 누가거리) · 그린 노선 · 곡선표 · 스냅(초안 · 없으면 저장 노선). */ +/** `buildStations` 인자 한 벌 — 화면 셈 · 서버 셈(계산 자리 「서버」)이 같은 값을 받음. */ +export type StationsArgs = Parameters; + +/** stations 단계 — 입력 관 · 구조물(follow 뒤 누가거리) · 그린 노선 · 곡선표 · 스냅(초안 · 없으면 저장 노선). + * `remote` = 계산 자리 「서버」면 서버 Node 값(같은 `buildStations` · 511-B B-2) · null = 화면 셈 · 서버가 + * 못 내면(끊김 · 실패) 화면 셈. */ export function createStationsStage( options: Omit & { types: () => ReadonlyArray; saved?: () => SavedSnap | null; + remote?: ( + args: StationsArgs, + signal: AbortSignal, + ) => Promise | 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); // 화면 셈 = 동기 그대로 }; } diff --git a/B06_Section/B06_Section_Router_Calc.py b/B06_Section/B06_Section_Router_Calc.py index b367ef28c..91e88a4ef 100644 --- a/B06_Section/B06_Section_Router_Calc.py +++ b/B06_Section/B06_Section_Router_Calc.py @@ -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: diff --git a/common_util/common_util_calc_sites.py b/common_util/common_util_calc_sites.py index 789e460df..d9f416d4a 100644 --- a/common_util/common_util_calc_sites.py +++ b/common_util/common_util_calc_sites.py @@ -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", diff --git a/resources/master_template/calc_sites.json b/resources/master_template/calc_sites.json index c0ad2f7a9..29f2137ed 100644 --- a/resources/master_template/calc_sites.json +++ b/resources/master_template/calc_sites.json @@ -5,6 +5,7 @@ "corridor_mesh": "both", "quantity": "both", "carry": "both", + "stations": "both", "fixed_browser": "both" } } diff --git a/resources/tester/test_309_2_calc_sites.py b/resources/tester/test_309_2_calc_sites.py index 1e9b60dcd..f809e25cf 100644 --- a/resources/tester/test_309_2_calc_sites.py +++ b/resources/tester/test_309_2_calc_sites.py @@ -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": diff --git a/resources/tester/test_511_b1_calc_node_stages.py b/resources/tester/test_511_b1_calc_node_stages.py index 619e3556c..8608cf8eb 100644 --- a/resources/tester/test_511_b1_calc_node_stages.py +++ b/resources/tester/test_511_b1_calc_node_stages.py @@ -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 diff --git a/resources/tester/test_511_b2_stations_node.py b/resources/tester/test_511_b2_stations_node.py index e0dd427b0..c8b593096 100644 --- a/resources/tester/test_511_b2_stations_node.py +++ b/resources/tester/test_511_b2_stations_node.py @@ -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 diff --git a/ui_template/ui_template_locale_m1.ts b/ui_template/ui_template_locale_m1.ts index cb45089fc..9d9856e13 100644 --- a/ui_template/ui_template_locale_m1.ts +++ b/ui_template/ui_template_locale_m1.ts @@ -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"],