From 4e7e9a6ac291ada7639056d47901b011d97b7c69 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 29 Sep 2026 11:41:35 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat(B05):=20=EC=B4=88=EA=B8=B0=EB=85=B8?= =?UTF-8?q?=EC=84=A0=20=EA=B0=88=EB=9E=98=20=EA=B3=B5=ED=86=B5=20=EC=A0=9C?= =?UTF-8?q?=EC=95=BD=20=EC=B9=B8=20=E2=80=94=20=EC=A2=85=EB=8B=A8=EA=B8=B0?= =?UTF-8?q?=EC=9A=B8=EA=B8=B0=20=C2=B7=20=EC=B5=9C=EC=86=8C=EB=B0=98?= =?UTF-8?q?=EC=A7=80=EB=A6=84=20=C2=B7=20=EA=B3=84=EB=A5=98=EC=9D=B4?= =?UTF-8?q?=EA=B2=A9=20=C2=B7=20=EC=A7=80=EB=B0=98=EA=B3=A0=EB=A6=84=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=EC=97=90=EC=84=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 공통 제약 기준값 확정(9-29 사용자 결정) — 후보 표 임시값 그대로 기본값, 화면에서 프로젝트마다 바꿀 수 있음 - 로직 단추 옆 제약 네 칸(비우면 placeholder 로 기본값) · 채운 칸만 POST /route/initial 몸통 criteria 로 실어 보냄 - [확인]의 initial.criteria 에도 실어 성공하면 initial_choice.json 에 겹친 칸만 남김(빈 칸은 기본값이라 안 담김) - 다시 열 때 current_initial.criteria 로 그 칸만 복원 · 캐시가 서버 갈래·비중·제약과 다르면 버림(sameBase) - 서버: generate_initial_route/_SearchGrid 가 stream_offset_m·grade_smooth_m 겹침을 받아 탐색·비교값에 반영 - 응답 criteria.pending 은 이제 늘 빈 목록 — 화면의 「후보 · 사용자 확정 대기」 글 삭제 시험: test_route_initial(+2) · test_b05_routeedit_initial(criteria 겹침·확정 표시 삭제) · test_b05_routeedit_initial_roundtrip(+1, 사본에서 계류 이격 30m 겹침 왕복) Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_015sCeZz8z8YnbovsKgn7HK7 --- B05_Profile/B05_Profile_Api_RouteInitial.ts | 12 +- .../B05_Profile_Engine_RouteInitial.py | 82 ++++++--- ...B05_Profile_Engine_RouteInitial_Metrics.py | 11 +- B05_Profile/B05_Profile_Router_Replan.py | 25 +++ .../B05_Profile_UI_RouteEdit_Initial.ts | 115 ++++++++++++- .../B05_Profile_UI_RouteEdit_Initial_Model.ts | 69 ++++++-- .../B05_Profile_UI_Style_RouteEdit.css | 29 ++++ .../tester/helper_b05_routeedit_initial.cjs | 155 +++++++++--------- .../helper_b05_routeedit_initial_apply.cjs | 18 +- .../tester/test_b05_routeedit_initial.py | 34 ++-- .../test_b05_routeedit_initial_roundtrip.py | 40 ++++- resources/tester/test_route_initial.py | 72 +++++++- 12 files changed, 512 insertions(+), 150 deletions(-) diff --git a/B05_Profile/B05_Profile_Api_RouteInitial.ts b/B05_Profile/B05_Profile_Api_RouteInitial.ts index be108a83..7f8b6653 100644 --- a/B05_Profile/B05_Profile_Api_RouteInitial.ts +++ b/B05_Profile/B05_Profile_Api_RouteInitial.ts @@ -2,7 +2,7 @@ * B05_Profile_Api_RouteInitial.ts * 초기노선 갈래 요청(계획서 20) — 로직 하나를 골라 서버가 새 계획노선 노드를 낸다. * - * POST /projects/{id}/route/initial { mode, weights? } → 노드 · 비교값 · 공통 제약 기준값 + * POST /projects/{id}/route/initial { mode, weights?, criteria? } → 노드 · 비교값 · 공통 제약 기준값 * * 계산 자리는 **서버 단독**(CLAUDE.md 5장 ③) — 화면은 부르고, 받은 노드를 편집 캐시에 싣고, * 받은 비교값·기준값을 보이기만 한다. 모양은 계획서 20 「API」 줄(서버 `initial_route`) — @@ -24,9 +24,19 @@ export interface RouteInitialWeights { grade: number; } +/** 공통 제약 겹침 값 — 화면 네 칸(계획서 20 확정). 비우면 서버가 프로젝트 기본값을 쓴다. + * 이름은 서버 몸통과 그대로(snake_case) — 변환 없이 그대로 실어 보낸다. */ +export interface RouteInitialCriteria { + max_grade_pct?: number; + min_radius_m?: number; + stream_offset_m?: number; + grade_smooth_m?: number; +} + export interface RouteInitialRequest { mode: RouteInitialLogic; weights?: RouteInitialWeights; + criteria?: RouteInitialCriteria; } /** 서버 길이 아직 없다(404·405) — 화면은 이 글로 알린다. */ diff --git a/B05_Profile/B05_Profile_Engine_RouteInitial.py b/B05_Profile/B05_Profile_Engine_RouteInitial.py index 5def0cb3..d6340207 100644 --- a/B05_Profile/B05_Profile_Engine_RouteInitial.py +++ b/B05_Profile/B05_Profile_Engine_RouteInitial.py @@ -23,9 +23,12 @@ · 그래도 길이 없을 때만(지형상 불가능) 벌점 탐색으로 가장 적게 넘긴 길을 내고, 넘긴 곳을 `metrics.violations`(측점 · 값 · 사유)에 적음. `over_limit` 수 = 그 목록 수. -⚠ 사용자 확정 대기 — 아래 임시값과 호출부가 넘기는 종단기울기 상한 · 최소 곡선반지름은 지식DB -(`resources/knowledge/technical_info/01_임도`) 후보일 뿐이다(PLAN 20장 기준값 후보 표). -확정되면 이 머리의 상수만 고친다. +기준값 확정(2026-09-29 사용자 결정, PLAN 20장) — 아래 임시값이 그대로 **기본값**이 됐다 +(지식DB `resources/knowledge/technical_info/01_임도` 후보 표 그대로). 종단기울기 상한 · +최소 곡선반지름은 여전히 호출부(`_road_settings`·`_plan_criteria`)가 프로젝트별로 넘기고, +계류 이격 · 기울기 잴 지반 폭은 이 머리의 상수가 기본값이다 — 넷 모두 화면(`RouteInitialCriteria`) +에서 프로젝트마다 겹쳐 쓸 수 있다(`generate_initial_route` 의 `stream_offset_m`·`grade_smooth_m` +인자, 라우터의 `max_grade`·`min_radius_m` 대입). 응답 `criteria.pending` 은 이제 늘 빈 목록이다. """ from __future__ import annotations @@ -58,14 +61,10 @@ STREAM_CLASSES = ("소하천", "지방하천", "국가하천") ROAD_WIDTH_M = 4.0 # 탐색 복도 반폭(m) — 예상노선에서 이보다 멀리는 가지 않음(법령 값 아님 · 알고리즘 값). CORRIDOR_M = 100.0 -PENDING = ( - "max_grade_pct", - "min_radius_m", - "stream_offset_m", - "road_width_m", - "corridor_m", - "grade_smooth_m", -) +# 기준값 확정(2026-09-29 사용자 결정) — 예전엔 여기 여섯 칸이 「사용자 확정 대기」였다. +# 이제 후보 표 값 그대로 기본값으로 굳었고(화면에서 프로젝트마다 바꿀 수 있음), 빈 목록이 곧 +# 「확정됨」이다(`criteria.pending`). +PENDING: tuple[str, ...] = () # ── 탐색 · 측정 값 ─────────────────────────────────────────────────────────────── MODES = ("follow", "earthwork", "gentle", "weighted") @@ -148,20 +147,40 @@ def _choice_path(project_root: Path) -> Path: def read_initial_choice(project_root: Path) -> dict[str, Any]: - """현재 초기값(갈래 · 비중) — [확인] 때 적어 둔 것. 없거나 못 읽으면 추종.""" + """현재 초기값(갈래 · 비중 · 제약 겹침) — [확인] 때 적어 둔 것. 없거나 못 읽으면 추종. + + `criteria` 는 **사용자가 겹쳐 쓴 값만**(비운 칸은 안 담김) — 나머지는 프로젝트 기본값이라 + 화면이 다시 열 때 겹친 칸만 채우고 나머지는 placeholder(기본값)로 비워 둔다. + """ try: data = json.loads(_choice_path(project_root).read_text(encoding="utf-8")) logic = data["logic"] if data.get("logic") in MODES else "follow" - return {"logic": logic, "weights": resolve_weights(logic, data.get("weights"))} + criteria = data.get("criteria") + return { + "logic": logic, + "weights": resolve_weights(logic, data.get("weights")), + "criteria": criteria if isinstance(criteria, dict) and criteria else None, + } except (OSError, ValueError, KeyError, TypeError, AttributeError): - return {"logic": "follow", "weights": resolve_weights("follow", None)} + return {"logic": "follow", "weights": resolve_weights("follow", None), "criteria": None} def write_initial_choice( - project_root: Path, logic: str, weights: dict[str, float] | None + project_root: Path, + logic: str, + weights: dict[str, float] | None, + criteria: dict[str, float] | None = None, ) -> dict[str, Any]: - """[확인] 때만 부름 — 작업본 한 칸. 다시 열기 · 다른 PC 에서도 같은 초기값.""" - choice = {"logic": logic, "weights": resolve_weights(logic, weights)} + """[확인] 때만 부름 — 작업본 한 칸. 다시 열기 · 다른 PC 에서도 같은 초기값. + + `criteria` 는 사용자가 화면 칸에 직접 넣은 겹침 값만(빈 칸은 담지 않는다) — 프로젝트 + 기본값이 나중에 바뀌어도 사용자가 안 만진 칸은 그 새 기본값을 그대로 받는다. + """ + choice = { + "logic": logic, + "weights": resolve_weights(logic, weights), + "criteria": criteria or None, + } path = _choice_path(project_root) path.parent.mkdir(parents=True, exist_ok=True) path.write_text(json.dumps(choice, ensure_ascii=False), encoding="utf-8") @@ -208,18 +227,27 @@ def generate_initial_route( weights: dict[str, float] | None, max_grade: float, min_radius_m: float, + stream_offset_m: float | None = None, + grade_smooth_m: float | None = None, ) -> dict[str, Any]: """갈래 하나로 초기 계획노선을 만들고 비교값을 붙여 돌려준다. `max_grade` 는 비율(0.14). 공통 제약을 넘지 않는 길을 먼저 찾고(탐색 상한을 `GRADE_CAP_STEPS` 차례로 낮춰 곡선 맞춤 뒤 지반 차까지 없앰), 그런 길이 없을 때만 벌점 탐색으로 가장 적게 넘긴 길을 낸다. + + `stream_offset_m`·`grade_smooth_m` 을 안 주면(None) 이 머리의 기본값을 쓴다 — 화면에서 + 프로젝트마다 겹쳐 쓸 수 있다(PLAN 20장 확정). """ if mode not in MODES: raise ValueError(f"모르는 갈래: {mode}") if len(expected) < 2: raise ValueError("예상노선 정점이 2개 미만입니다.") used = resolve_weights(mode, weights) - grid = _SearchGrid(expected, terrain, streams) + stream_offset = STREAM_OFFSET_M if stream_offset_m is None else float(stream_offset_m) + grade_smooth = GRADE_SMOOTH_M if grade_smooth_m is None else float(grade_smooth_m) + grid = _SearchGrid( + expected, terrain, streams, stream_offset_m=stream_offset, grade_smooth_m=grade_smooth + ) def attempt(hard_cap: float, bound: float | None) -> tuple[Any, dict[str, Any]]: nodes = _search_route( @@ -243,9 +271,10 @@ def generate_initial_route( outline.curves, max_grade=max_grade, min_radius_m=min_radius_m, - stream_offset_m=STREAM_OFFSET_M, + stream_offset_m=stream_offset, road_width_m=ROAD_WIDTH_M, bound_pct=None if bound is None else bound * 100.0, + grade_smooth_m=grade_smooth, ) return outline, metrics @@ -280,10 +309,11 @@ def generate_initial_route( "criteria": { "max_grade_pct": round(max_grade * 100.0, 2), "min_radius_m": round(min_radius_m, 2), - "stream_offset_m": STREAM_OFFSET_M, + "stream_offset_m": round(stream_offset, 2), "road_width_m": ROAD_WIDTH_M, "corridor_m": CORRIDOR_M, - "grade_smooth_m": GRADE_SMOOTH_M, + "grade_smooth_m": round(grade_smooth, 2), + # 기준값 확정(PLAN 20장, 2026-09-29) — 더는 후보가 아니다. 항상 빈 목록. "pending": list(PENDING), }, } @@ -323,11 +353,15 @@ class _SearchGrid: expected: list[tuple[float, float]], terrain: Terrain, streams: list[list[tuple[float, float]]], + *, + stream_offset_m: float = STREAM_OFFSET_M, + grade_smooth_m: float = GRADE_SMOOTH_M, ) -> None: step = max(1, int(round(SEARCH_CELL_M / _cell_size(terrain.x)))) self.xs, self.ys = terrain.x[::step], terrain.y[::step] # 기울기 · 옆경사는 비교값과 같은 고른 지반에서 잰다(`smoothed_ground`) - self.z = smoothed_ground(terrain)[::step, ::step] + self.z = smoothed_ground(terrain, grade_smooth_m)[::step, ::step] + self.stream_offset_m = stream_offset_m self.cell = _cell_size(self.xs) self.d_expected = _distance_grid([expected], self.xs, self.ys, self.cell) self.d_stream = _distance_grid(streams, self.xs, self.ys, self.cell) @@ -372,8 +406,8 @@ class _SearchGrid: norm = math.hypot(dr, dc) grade = np.abs(self.z[second] - self.z[first]) / (self.cell * norm) # 계류 버퍼 안에서는 계류 쪽 · 반대쪽 ±45° 로만(건너기 · 빠져나가기) - in_buffer = (self.d_stream[first] < STREAM_OFFSET_M) | ( - self.d_stream[second] < STREAM_OFFSET_M + in_buffer = (self.d_stream[first] < self.stream_offset_m) | ( + self.d_stream[second] < self.stream_offset_m ) ax = self.away_x[first] + self.away_x[second] ay = self.away_y[first] + self.away_y[second] diff --git a/B05_Profile/B05_Profile_Engine_RouteInitial_Metrics.py b/B05_Profile/B05_Profile_Engine_RouteInitial_Metrics.py index 518774a9..ddeb1fa2 100644 --- a/B05_Profile/B05_Profile_Engine_RouteInitial_Metrics.py +++ b/B05_Profile/B05_Profile_Engine_RouteInitial_Metrics.py @@ -33,12 +33,14 @@ REASON_STREAM = "계류 버퍼 안 — 건너기 · 기점 · 종점 봐줌을 REASON_STREAM_TERRAIN = "지형상 불가능 — 기울기 · 반지름을 지키며 계류 버퍼를 비켜 잇는 길 없음" -def smoothed_ground(terrain: Terrain) -> np.ndarray: - """종단기울기를 잴 지반(`GRADE_SMOOTH_M` 로 고른 DEM) — 탐색과 비교값이 같은 것을 본다.""" +def smoothed_ground(terrain: Terrain, grade_smooth_m: float = GRADE_SMOOTH_M) -> np.ndarray: + """종단기울기를 잴 지반(`grade_smooth_m` 로 고른 DEM) — 탐색과 비교값이 같은 것을 본다. + + 폭은 화면에서 바꿀 수 있다(PLAN 20장 확정 · 기본값 `GRADE_SMOOTH_M`).""" from scipy.ndimage import gaussian_filter spacing = 0.5 * (_spacing(terrain.x) + _spacing(terrain.y)) - return gaussian_filter(np.asarray(terrain.z, dtype=np.float64), GRADE_SMOOTH_M / spacing) + return gaussian_filter(np.asarray(terrain.z, dtype=np.float64), grade_smooth_m / spacing) def station_label(chainage_m: float) -> str: @@ -107,6 +109,7 @@ def route_metrics( stream_offset_m: float, road_width_m: float, bound_pct: float | None, + grade_smooth_m: float = GRADE_SMOOTH_M, ) -> dict[str, Any]: """비교값과 제약 못 지킨 구간 목록. 종단기울기 = 측점(20 m) 사이 고른 지반 기울기. @@ -115,7 +118,7 @@ def route_metrics( grid = (terrain.y, terrain.x) sampler = RegularGridInterpolator(grid, terrain.z, bounds_error=False, fill_value=None) smooth = RegularGridInterpolator( - grid, smoothed_ground(terrain), bounds_error=False, fill_value=None + grid, smoothed_ground(terrain, grade_smooth_m), bounds_error=False, fill_value=None ) points, chain = _resample(vertices, SAMPLE_M) length = float(chain[-1]) diff --git a/B05_Profile/B05_Profile_Router_Replan.py b/B05_Profile/B05_Profile_Router_Replan.py index 4ad1d17e..42edf351 100644 --- a/B05_Profile/B05_Profile_Router_Replan.py +++ b/B05_Profile/B05_Profile_Router_Replan.py @@ -94,6 +94,19 @@ class RouteInitialWeights(BaseModel): grade: float = Field(25.0, ge=0, le=100) +class RouteInitialCriteria(BaseModel): + """공통 제약 겹침 값 — 화면 네 칸(PLAN 20장 확정). 비우면(None) 프로젝트 기본값을 쓴다. + + 기본값(2026-09-29 사용자 결정 — 지식DB 후보 표 그대로 확정) — 종단기울기 상한 · 최소 + 곡선반지름은 프로젝트 설계속도 · 지형에 따른 표값, 계류 이격 100 m, 기울기 잴 지반 5 m 고름. + """ + + max_grade_pct: float | None = Field(None, ge=0, le=100) + min_radius_m: float | None = Field(None, ge=0) + stream_offset_m: float | None = Field(None, ge=0) + grade_smooth_m: float | None = Field(None, ge=0) + + class RouteInitialRequest(BaseModel): """초기 계획노선 갈래 — follow(예상노선 추종) · earthwork · gentle · weighted.""" @@ -101,6 +114,7 @@ class RouteInitialRequest(BaseModel): "follow", validation_alias=AliasChoices("logic", "mode") ) weights: RouteInitialWeights | None = None + criteria: RouteInitialCriteria | None = None class RouteReplanRequest(BaseModel): @@ -574,6 +588,7 @@ async def replan_route( project_root, choice.mode, choice.weights.model_dump() if choice.weights else None, + (choice.criteria.model_dump(exclude_none=True) or None) if choice.criteria else None, ) if choice else await asyncio.to_thread(read_initial_choice, project_root) @@ -648,6 +663,14 @@ async def initial_route( grade_class, design_speed, terrain_type = await _road_settings(project_id) # 비포장 기준 — 포장 예외(18 %)는 포장 여부가 정해진 뒤 종단 설계가 따진다(사용자 확정 대기). max_grade = legal_grade_limit_pct(grade_class, terrain_type, False, design_speed) / 100.0 + # 화면 제약 칸(PLAN 20장 확정) — 비운 칸은 위 프로젝트 기본값 그대로, 채운 칸만 겹쳐 쓴다. + criteria = request.criteria + if criteria and criteria.max_grade_pct is not None: + max_grade = criteria.max_grade_pct / 100.0 + if criteria and criteria.min_radius_m is not None: + radius_m = criteria.min_radius_m + stream_offset_m = criteria.stream_offset_m if criteria else None + grade_smooth_m = criteria.grade_smooth_m if criteria else None pool = get_db_pool() async with pool.acquire() as connection: selection = await get_surface_confirmation_params(connection, str(project_id)) @@ -666,6 +689,8 @@ async def initial_route( weights=weights, max_grade=max_grade, min_radius_m=radius_m, + stream_offset_m=stream_offset_m, + grade_smooth_m=grade_smooth_m, ) ) except (FileNotFoundError, ValueError) as error: diff --git a/B05_Profile/B05_Profile_UI_RouteEdit_Initial.ts b/B05_Profile/B05_Profile_UI_RouteEdit_Initial.ts index 7affdc8a..04dd4c01 100644 --- a/B05_Profile/B05_Profile_UI_RouteEdit_Initial.ts +++ b/B05_Profile/B05_Profile_UI_RouteEdit_Initial.ts @@ -1,19 +1,25 @@ /* ============================================================================= * B05_Profile_UI_RouteEdit_Initial.ts - * 계획노선 편집 창의 **초기노선 갈래**(계획서 20) — 로직 단추 셋 · 비중 슬라이더 · [초기화] - * · 「현재 초기값」 · 결과 비교값 · 공통 제약 기준값. + * 계획노선 편집 창의 **초기노선 갈래**(계획서 20) — 로직 단추 셋 · 공통 제약 겹침 네 칸 · + * 비중 슬라이더 · [초기화] · 「현재 초기값」 · 결과 비교값 · 공통 제약 기준값. * * 로직 단추를 누르면 서버가 노선을 새로 셈하고(`/route/initial`), 받은 노드를 **편집값에 * 싣는다**(되돌리기 한 걸음). 그 결과가 새 초기값이라 캐시(`routeedit-initial`)에 적고, * [초기화]는 서버를 다시 부르지 않고 그 초기값으로 돌아간다. 종단 · 배수유역 · 횡단 다시 * 계산은 여기서 안 돈다 — 노선이 바뀐 뒤 [확인]에서만(`_Apply`). * 비교값 · 기준값은 서버가 준 것을 보이기만 한다(계산 자리 = 서버 단독). + * + * 공통 제약 네 칸(종단기울기 % · 최소 반지름 m · 계류 이격 m · 지반 고름 m, 계획서 20 확정) + * — 비우면 서버 기본값을 쓴다. 채운 칸만 요청에 실어 보내고(비운 칸은 서버가 알아서), 그 + * 「채운 칸」만 캐시·`initial_choice.json`에 남아 다시 열 때 되살아난다. 빈 칸의 placeholder는 + * 그 칸을 처음 비운 채 요청했을 때 서버가 답으로 준 값(진짜 기본값)을 기억해 둔 것이다. * ========================================================================== */ import { showToast } from "@ui/ui_template_elements"; import { fetchRouteInitial, ROUTE_INITIAL_MISSING, + type RouteInitialCriteria, type RouteInitialLogic, type RouteInitialWeights, } from "./B05_Profile_Api_RouteInitial"; @@ -21,6 +27,7 @@ import type { RouteEditSnapshot } from "./B05_Profile_UI_RouteEdit_History"; import { choiceLabel, criteriaLine, + CRITERIA_KEYS, DEFAULT_WEIGHTS, metricsLine, violationsLine, @@ -33,8 +40,10 @@ import { serverBase, WEIGHT_KEYS, writeInitialChoice, + type CriteriaKey, type InitialBase, type InitialChoice, + type InitialCriteria, type WeightKey, } from "./B05_Profile_UI_RouteEdit_Initial_Model"; @@ -44,6 +53,37 @@ const WEIGHT_NAME: Record = { grade: "기울기", }; +/** 공통 제약 칸 — 표시 이름 · 단위 · 서버가 준 값(`InitialCriteria`)에서 그 칸을 꺼내는 법. */ +const CRITERIA_FIELD: Record< + CriteriaKey, + { label: string; unit: string; title: string; of: (c: InitialCriteria) => number | null } +> = { + max_grade_pct: { + label: "기울기", + unit: "%", + title: "종단기울기 상한(%) — 비우면 프로젝트 기본값(설계속도 · 지형 표값)", + of: (c) => c.maxGradePct, + }, + min_radius_m: { + label: "최소R", + unit: "m", + title: "최소 곡선반지름(m) — 비우면 프로젝트 기본값", + of: (c) => c.minRadiusM, + }, + stream_offset_m: { + label: "계류", + unit: "m", + title: "계류 이격(m) — 비우면 기본값 100m", + of: (c) => c.streamOffsetM, + }, + grade_smooth_m: { + label: "지반", + unit: "m", + title: "기울기 잴 지반 고름 폭(m) — 비우면 기본값 5m", + of: (c) => c.gradeSmoothM, + }, +}; + export interface InitialPanelParams { overlay: HTMLElement; projectId: string; @@ -57,7 +97,11 @@ export interface InitialPanel { /** 노선을 읽었다 — 창을 연 편집값 · 「고친 노선인가」 · 서버가 적어 둔 현재 초기값(`current_initial`). */ ready: (opening: RouteEditSnapshot, edited: boolean, server?: unknown) => void; /** [확인]에 실어 보낼 지금 초기값. */ - current: () => { mode: RouteInitialLogic; weights: RouteInitialWeights | null }; + current: () => { + mode: RouteInitialLogic; + weights: RouteInitialWeights | null; + criteria: RouteInitialCriteria | null; + }; /** [취소] — 이 창에서 바꾼 초기값을 창을 열 때 것으로 되돌린다. */ cancel: () => void; /** [확인] 뒤 결과 캐시를 비웠다 — 초기값은 새 노선의 출발점이라 다시 적는다. */ @@ -80,6 +124,13 @@ export function createInitialPanel(params: InitialPanelParams): InitialPanel { +
+ ${CRITERIA_KEYS.map( + (key) => ``, + ).join("")} +