feat(509-5 · 509-2 B05 추천 지형): 분석 지형 고르기 = 추천 노선 창 안 · 지형마다 추천 미리 셈 · [적용] = 창 지형이 설계 지형 · 평면은 지형 이름만 · 설계 지형 격자 하나(수치지형도 적용이면 그 격자)
- 전처리 미리 셈이 고를 수 있는 지형마다(설계 지형 먼저 · 수치지형도는 DB 모델 있을 때만) 다섯 갈래 + 초기 종단 - 창 지형 바꿈 = 그 지형 저장본만 · 없으면 409 안내(+[재계산]) · 기본값 판정에서 지형 뺌 - POST /route/initial terrain = 창 지형 · [적용] draft.terrain = 창 지형 · 손 [저장]은 지형 안 바꿈(서버도 as_initial 일 때만) - 평면 분석 지형 고르기 · 화면 취향(routeedit-terrain) 걷음 · 지반고 · 묶음 초안 = 설계 지형(null) · 적용 뒤 받아 둔 격자 버림 - 시험 test_509_5 · test_509_2 · 걷은 고르기를 보던 옛 시험 새 동작으로(352_3 건너뜀) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,51 +6,24 @@
|
||||
* POST /projects/{id}/route/replan → 노선 초안 저장(346-3 · draft = 계약 RouteDraft · as_initial)
|
||||
* GET /projects/{id}/route/terrain-sources → 고를 수 있는 분석 지형(라이다 · 수치지형도)
|
||||
*
|
||||
* 편집 창 분석(지반고 · 횡단 미리보기 · 추천)은 **고른 분석 지형**(`terrain`)을 실어
|
||||
* 보낸다(PLAN 133-1) — [확인] 재계산은 확정 지표면 그대로라 안 실음.
|
||||
* 편집 창 분석(지반고 · 횡단 미리보기)은 설계 지형 그대로 — 지형 고르기는 추천 창 안 · [적용]이
|
||||
* 그 지형을 설계 지형으로(509-5).
|
||||
*
|
||||
* 재계산은 배수유역부터 전 단계를 다시 도는 무거운 작업이라(용화 67측점 기준 3분대)
|
||||
* 타임아웃을 길게 잡는다 — 기본값으로 두면 중간에 끊긴다.
|
||||
* ========================================================================== */
|
||||
|
||||
import { API_BASE_URL } from "@config/config_frontend";
|
||||
import { readState, writeState } from "../A00_Common/b_page_state";
|
||||
import { heightAt } from "../common_util/common_util_surface_sampler";
|
||||
import { loadDesignTerrainGrid } from "./B05_Profile_Api_TerrainGrid";
|
||||
import type { RouteDraft } from "../A00_Common/design/logic/design_pipeline_types";
|
||||
import type { RouteNodeEdit } from "./logic/B05_Profile_UI_RouteEdit_Edits";
|
||||
import type { RouteReset } from "./logic/B05_Profile_Route_Reset";
|
||||
|
||||
/** 편집 창 분석 지형 — las = 확정 지표면(라이다) · sheet = 수치지형도 서피스. */
|
||||
/** 분석 지형 — las = 확정 지표면(라이다) · sheet = 수치지형도 서피스. 고르기는 추천 노선 창 안(509-5). */
|
||||
export type AnalysisTerrain = "las" | "sheet";
|
||||
|
||||
/** 편집기가 열 때 알리는 지형 사정(352-3) — 저장하지 않음. 고를 수 있나(라이다 · 수치지형도 둘) · 설계 지형. */
|
||||
const terrainContext = new Map<string, { choosable: boolean; design: AnalysisTerrain }>();
|
||||
|
||||
export const setTerrainContext = (
|
||||
projectId: string,
|
||||
choosable: boolean,
|
||||
design: AnalysisTerrain,
|
||||
): void => void terrainContext.set(projectId, { choosable, design });
|
||||
|
||||
/** 지금 쓰는 분석 지형 — 하나뿐인 프로젝트는 그것 · 둘이면 사용자가 고른 값(화면 상태 보관소 pref 통 ·
|
||||
* 노선 편집 고정과 같은 자리), 고른 적 없으면 데이터대로(= 설계 지형 · 라이다가 있으면 라이다).
|
||||
* 고른 값은 [저장] 때 설계 지형이 됨(PLAN 133-2). */
|
||||
export const analysisTerrain = (
|
||||
projectId: string,
|
||||
fallback: AnalysisTerrain = "las",
|
||||
): AnalysisTerrain => {
|
||||
const context = terrainContext.get(projectId);
|
||||
if (context && !context.choosable) return context.design;
|
||||
const saved = readState<string>("routeedit-terrain", projectId);
|
||||
return saved === "sheet" || saved === "las" ? saved : (context?.design ?? fallback);
|
||||
};
|
||||
|
||||
/** 사용자가 고를 때만 기억 — 열 때 기본값은 쓰지 않음. */
|
||||
export const setAnalysisTerrain = (projectId: string, terrain: AnalysisTerrain): void =>
|
||||
writeState("routeedit-terrain", terrain, projectId);
|
||||
|
||||
/** 이 프로젝트가 고를 수 있는 분석 지형(둘이면 고르기를 띄운다)과 지금 설계 지형. */
|
||||
/** 이 프로젝트가 고를 수 있는 분석 지형(둘이면 추천 창에 고르기)과 지금 설계 지형. */
|
||||
export async function fetchTerrainSources(
|
||||
projectId: string,
|
||||
): Promise<{ sources: AnalysisTerrain[]; design: AnalysisTerrain }> {
|
||||
@@ -216,14 +189,14 @@ export async function replanRoute(
|
||||
*
|
||||
* 새 계산이 아니라 확정된 지표면을 **읽기만** 하므로 편집 중에 불러도 된다 — 다만 끄는 동안
|
||||
* 프레임마다 부르지는 않는다(찍는 순간에만). 지표면 밖은 `null` 로 온다.
|
||||
* 설계 지형 격자를 받아 두었고 분석 지형이 그것과 같으면 **브라우저 표본**(서버와 같은 값 ·
|
||||
* PLAN 344-1) — 아니면 서버에 물음. 자릿수는 서버 응답과 같게 mm. */
|
||||
* 설계 지형 격자를 받아 두었으면 **브라우저 표본**(서버와 같은 값 · PLAN 344-1) — 아니면
|
||||
* 서버에 물음. 자릿수는 서버 응답과 같게 mm. */
|
||||
export async function fetchRouteElevations(
|
||||
projectId: string,
|
||||
points: Array<[number, number]>,
|
||||
): Promise<Array<number | null>> {
|
||||
const local = await loadDesignTerrainGrid(projectId);
|
||||
if (local && analysisTerrain(projectId) === local.design) {
|
||||
if (local) {
|
||||
return points.map(([x, y]) => {
|
||||
const z = heightAt(local.grid, x, y);
|
||||
return Number.isNaN(z) ? null : Math.round(z * 1000) / 1000;
|
||||
@@ -231,7 +204,7 @@ export async function fetchRouteElevations(
|
||||
}
|
||||
const payload = await requestJson<{ z: Array<number | null> }>(
|
||||
`/projects/${projectId}/route/elevations`,
|
||||
{ method: "POST", body: JSON.stringify({ points, terrain: analysisTerrain(projectId) }) },
|
||||
{ method: "POST", body: JSON.stringify({ points }) },
|
||||
60000,
|
||||
);
|
||||
return payload.z;
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
* B05_Profile_Api_RouteInitial.ts
|
||||
* 초기노선 갈래 요청(계획서 20) — 로직 하나를 골라 서버가 새 계획노선 노드를 낸다.
|
||||
*
|
||||
* POST /projects/{id}/route/initial { mode, weights?, criteria? } → 노드 · 비교값 · 공통 제약 기준값
|
||||
* POST /projects/{id}/route/initial { mode, weights?, criteria?, terrain? } → 노드 · 비교값 · 공통 제약 기준값
|
||||
*
|
||||
* 계산 자리는 **서버 단독**(CLAUDE.md 5장 ③) — 화면은 부르고, 받은 노드를 편집 캐시에 싣고,
|
||||
* 받은 비교값·기준값을 보이기만 한다. 모양은 계획서 20 「API」 줄(서버 `initial_route`) —
|
||||
* 답을 펴는 자리는 `normalizeRouteInitial`(`_RouteEdit_Initial_Model`) 한 곳이다.
|
||||
* ========================================================================== */
|
||||
|
||||
import { analysisTerrain, requestJson } from "./B05_Profile_Api_Replan";
|
||||
import { requestJson, type AnalysisTerrain } from "./B05_Profile_Api_Replan";
|
||||
|
||||
/** 갈래 셈 대기 상한 — 노선 여러 벌을 셈해 견주므로 넉넉히 잡는다. */
|
||||
const INITIAL_TIMEOUT_MS = 5 * 60 * 1000;
|
||||
@@ -44,6 +44,8 @@ export interface RouteInitialRequest {
|
||||
with_profile?: boolean;
|
||||
/** [재계산] — 이 입력의 저장본을 지우고 새로 셈(421 · 서버 420). */
|
||||
recompute?: boolean;
|
||||
/** 창에서 고른 지형(509-5) — 지형마다 미리 셈 저장본 · 안 보내면 설계 지형. */
|
||||
terrain?: AnalysisTerrain;
|
||||
}
|
||||
|
||||
/** 서버 길이 아직 없다(404·405) — 화면은 이 글로 알린다. */
|
||||
@@ -60,7 +62,7 @@ export async function fetchRouteInitial(
|
||||
`/projects/${projectId}/route/initial`,
|
||||
{
|
||||
method: "POST",
|
||||
body: JSON.stringify({ ...request, terrain: analysisTerrain(projectId) }),
|
||||
body: JSON.stringify(request),
|
||||
},
|
||||
INITIAL_TIMEOUT_MS,
|
||||
);
|
||||
|
||||
@@ -17,7 +17,7 @@ import { type DtmGrid, parseDtmGrid } from "../common_util/common_util_surface_s
|
||||
import type { GroundStageDeps } from "./logic/B05_Profile_Pipeline_RouteGround";
|
||||
|
||||
export interface DesignTerrainGrid {
|
||||
/** 이 격자의 지형 — 편집 창 분석 지형이 이것과 같을 때만 씀. */
|
||||
/** 이 격자의 지형 — 계산 묶음 노선 초안 지형(null = 설계)이 이것과 같을 때만 씀. */
|
||||
design: "las" | "sheet";
|
||||
grid: DtmGrid;
|
||||
}
|
||||
@@ -65,6 +65,12 @@ export function loadDesignTerrainGrid(projectId: string): Promise<DesignTerrainG
|
||||
return pending;
|
||||
}
|
||||
|
||||
/** 설계 지형이 바뀜(추천 [적용] · 509-5) — 받아 둔 격자를 버림 · 다음 부름에 새 지형 격자를 받음. */
|
||||
export function forgetDesignTerrainGrid(projectId: string): void {
|
||||
loaded.delete(projectId);
|
||||
ready.delete(projectId);
|
||||
}
|
||||
|
||||
/** 지금 쓸 수 있는 격자 — 받는 중이면 null 을 돌려주고 받기를 띄움(기다리지 않음). */
|
||||
export function designTerrainGridNow(projectId: string): DesignTerrainGrid | null {
|
||||
if (!ready.has(projectId)) void loadDesignTerrainGrid(projectId);
|
||||
|
||||
@@ -127,12 +127,11 @@ def is_default(
|
||||
terrain: Any,
|
||||
saved: dict[str, Any] | None = None,
|
||||
) -> bool:
|
||||
"""지킬 결과 — 기본값(빈 제약 · 50/25/25 · 설계 지형) 또는 저장된 제약(`initial_choice`) 그대로.
|
||||
"""지킬 결과 — 기본값(빈 제약 · 50/25/25) 또는 저장된 제약(`initial_choice`) 그대로 · 지형은
|
||||
안 따짐(지형마다 미리 셈 · 509-5 · `terrain` 은 부르는 쪽 꼴 맞춤).
|
||||
|
||||
모달이 여는 값이라 「최근 2벌」 정리에서 따로 센다(351-1 · 사용자 시도에 안 밀림).
|
||||
"""
|
||||
if terrain is not None:
|
||||
return False
|
||||
filled = {key: value for key, value in (criteria or {}).items() if value is not None}
|
||||
used = resolve_weights(mode, weights)
|
||||
if not filled and (mode != "weighted" or used == DEFAULT_WEIGHTS):
|
||||
|
||||
@@ -637,7 +637,7 @@ async def _save_route(project_id: UUID, body: RouteSaveRequest) -> dict[str, Any
|
||||
if request.balanced_profile and as_initial
|
||||
else None
|
||||
),
|
||||
request.terrain,
|
||||
request.terrain if as_initial else None, # 지형 바꿈 = 추천 [적용]만(509-5)
|
||||
(old_line, await asyncio.to_thread(_vertices_of, working_path)),
|
||||
as_initial,
|
||||
)
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
import asyncio
|
||||
import functools
|
||||
import itertools
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
@@ -26,7 +27,7 @@ logger = logging.getLogger(__name__)
|
||||
# 미리 셈 진행(364-4) — 프로젝트마다 하나만 돎 · 도는 동안 또 오면 끝난 뒤 한 번 더(입력이 바뀜)
|
||||
_RUNNING: dict[str, dict[str, Any]] = {}
|
||||
# 미리 셈 갈래인데 저장본이 없을 때(505-1) — 프로젝트 접근은 셈하지 않음
|
||||
NOT_PREPARED = "추천 저장본이 없습니다 — 전처리를 다시 하면 생깁니다."
|
||||
NOT_PREPARED = "추천 저장본이 없습니다 — 전처리를 다시 하면 생깁니다 · [재계산]이면 지금 셉니다."
|
||||
|
||||
|
||||
@replan.router.post("/{project_id}/route/initial", response_model=None)
|
||||
@@ -249,7 +250,8 @@ async def project_standard(project_id: UUID) -> dict[str, Any] | None:
|
||||
|
||||
|
||||
async def precompute_defaults(project_id: UUID) -> None:
|
||||
"""체인 끝 예약(PLAN 327-2 · 351-1) — 기본 다섯 알약 + 저장된 제약(initial_choice)의 다섯.
|
||||
"""체인 끝 예약(PLAN 327-2 · 351-1) — 기본 다섯 알약 + 저장된 제약(initial_choice)의 다섯 ×
|
||||
고를 수 있는 지형(509-5 · 설계 지형 먼저 · 수치지형도는 DB 모델 있을 때만).
|
||||
|
||||
모달은 저장된 제약 · 비중으로 열리므로 그 값으로도 미리 셈한다. 있으면 꺼내기만.
|
||||
다섯 갈래 모두 종단 켬(420) · 첫머리에 30일 지난 다른 판 · 소스 지문 저장본을 지운다(420-2).
|
||||
@@ -260,11 +262,13 @@ async def precompute_defaults(project_id: UUID) -> None:
|
||||
started = time.perf_counter()
|
||||
variants: list[tuple[Any, Any]] = [(None, None)]
|
||||
modes = list(DEFAULT_MODES)
|
||||
terrains: list[Any] = [None]
|
||||
try:
|
||||
paths = await replan._project_paths(project_id)
|
||||
except LookupError:
|
||||
paths = None
|
||||
if paths is not None:
|
||||
terrains = await precompute_terrains(project_id, paths[0])
|
||||
removed = await asyncio.to_thread(purge_outdated, paths[0])
|
||||
if removed:
|
||||
logger.info("추천 옛 판 저장본 지움: project_id=%s %d", project_id, removed)
|
||||
@@ -276,29 +280,51 @@ async def precompute_defaults(project_id: UUID) -> None:
|
||||
modes.sort(key=lambda mode: mode != saved["logic"] or mode == "follow")
|
||||
progress = _RUNNING.get(str(project_id))
|
||||
if progress is not None:
|
||||
progress.update(total=len(variants) * len(modes))
|
||||
for criteria, weights in variants:
|
||||
for mode in modes:
|
||||
if progress is not None:
|
||||
progress["mode"] = mode
|
||||
# 화면과 같은 몸통 — 설계 지형 · 비중은 비중 갈래만 · 「종단 반영」 기본 켬(420)
|
||||
request = replan.RouteInitialRequest(
|
||||
mode=mode,
|
||||
with_profile=True,
|
||||
criteria=criteria,
|
||||
weights=weights if mode == "weighted" else None,
|
||||
progress.update(total=len(terrains) * len(variants) * len(modes))
|
||||
for terrain, (criteria, weights), mode in itertools.product(terrains, variants, modes):
|
||||
if progress is not None:
|
||||
progress["mode"] = mode
|
||||
# 화면과 같은 몸통 — 창 지형 · 비중은 비중 갈래만 · 「종단 반영」 기본 켬(420)
|
||||
request = replan.RouteInitialRequest(
|
||||
mode=mode,
|
||||
with_profile=True,
|
||||
criteria=criteria,
|
||||
weights=weights if mode == "weighted" else None,
|
||||
terrain=terrain,
|
||||
)
|
||||
outcome = await compute_initial(project_id, request, precompute=True)
|
||||
if isinstance(outcome, JSONResponse):
|
||||
logger.warning(
|
||||
"추천 미리 셈 멈춤(%s %s %s): project_id=%s",
|
||||
terrain,
|
||||
mode,
|
||||
outcome.status_code,
|
||||
project_id,
|
||||
)
|
||||
outcome = await compute_initial(project_id, request, precompute=True)
|
||||
if isinstance(outcome, JSONResponse):
|
||||
logger.warning(
|
||||
"추천 미리 셈 멈춤(%s %s): project_id=%s", mode, outcome.status_code, project_id
|
||||
)
|
||||
return
|
||||
if progress is not None:
|
||||
progress["done"] += 1
|
||||
return
|
||||
if progress is not None:
|
||||
progress["done"] += 1
|
||||
logger.info("추천 미리 셈 끝: project_id=%s %.1fs", project_id, time.perf_counter() - started)
|
||||
|
||||
|
||||
async def precompute_terrains(project_id: UUID, project_root: Path) -> list[Any]:
|
||||
"""미리 셈 지형(509-5) — 설계 지형 먼저 · 추천 창이 고를 수 있는 다른 지형."""
|
||||
from B05_Profile.B05_Profile_Router_Terrain import _MODELS_SUBDIR, choosable_terrains
|
||||
from common_util.common_util_surface_confirmation import (
|
||||
design_terrain,
|
||||
get_surface_confirmation_params,
|
||||
)
|
||||
|
||||
pool = get_db_pool()
|
||||
async with pool.acquire() as connection:
|
||||
params = await get_surface_confirmation_params(connection, str(project_id))
|
||||
sources = await choosable_terrains(
|
||||
connection, project_id, params, Path(project_root) / _MODELS_SUBDIR
|
||||
)
|
||||
design = design_terrain(params)
|
||||
return [design, *[source for source in sources if source != design]]
|
||||
|
||||
|
||||
async def run_precompute(project_id: UUID) -> None:
|
||||
"""미리 셈 한 줄(364-4) — 이미 돌면 「한 번 더」만 적고 돌아감(겹쳐 셈 막음)."""
|
||||
key = str(project_id)
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
GET /api/projects/{id}/surface/models/{mid}/grid → DTM 격자 원시 이진(브라우저 표본기 · ETag)
|
||||
|
||||
분석 지형(`terrain`, PLAN 133-1) — las = 라이다 · sheet = 수치지형도 · None = 지금 설계 지형.
|
||||
모달 분석만 고른 지형을 쓴다 — 설계 지형(확정)은 [확인] 때만 바뀜(133-2 · `/route/replan`).
|
||||
고르기는 추천 노선 창 안(509-5) — 설계 지형(확정)은 추천 [적용] 때만 바뀜(`/route/replan`).
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
@@ -116,11 +116,7 @@ async def read_route_elevations(project_id: UUID, request: ElevationRequest) ->
|
||||
|
||||
@router.get("/{project_id}/route/terrain-sources", response_model=None)
|
||||
async def read_terrain_sources(project_id: UUID) -> dict | JSONResponse:
|
||||
"""편집 모달이 고를 수 있는 분석 지형 — 둘이면 고르기를 띄움 · 하나면 숨김.
|
||||
|
||||
설계 지형 아닌 쪽은 DB 완료 모델이 있어야 둠 — [확인]이 그 모델로 바꾸므로(505-2 5d)."""
|
||||
from B04_PreProcess.B04_PreProcess_Service import find_surface_model_for_selection
|
||||
|
||||
"""추천 창이 고를 수 있는 분석 지형(509-5 · 둘이면 고르기) · 설계 지형 · 설계 지형 격자 자리."""
|
||||
pool = get_db_pool()
|
||||
async with pool.acquire() as connection:
|
||||
stored = await get_project_storage_relative_path(connection, project_id)
|
||||
@@ -132,14 +128,7 @@ async def read_terrain_sources(project_id: UUID) -> dict | JSONResponse:
|
||||
params = await get_surface_confirmation_params(connection, str(project_id))
|
||||
confirmed = await _confirmed_model(connection, project_id)
|
||||
models_dir = Path(resolve_stored_project_path(stored)) / _MODELS_SUBDIR
|
||||
sources = await asyncio.to_thread(analysis_terrain_sources, params, models_dir)
|
||||
for source in [s for s in sources if s != design_terrain(params)]:
|
||||
try:
|
||||
await find_surface_model_for_selection(
|
||||
connection, project_id, analysis_terrain_params(params, source)
|
||||
)
|
||||
except LookupError:
|
||||
sources.remove(source)
|
||||
sources = await choosable_terrains(connection, project_id, params, models_dir)
|
||||
return {
|
||||
"status": "success",
|
||||
"project_id": str(project_id),
|
||||
@@ -149,6 +138,24 @@ async def read_terrain_sources(project_id: UUID) -> dict | JSONResponse:
|
||||
}
|
||||
|
||||
|
||||
async def choosable_terrains(
|
||||
connection: aiomysql.Connection, project_id: UUID, params: dict, models_dir: Path
|
||||
) -> list[str]:
|
||||
"""고를 수 있는 분석 지형 — 설계 지형 아닌 쪽은 DB 완료 모델이 있어야 둠(505-2 5d) · 추천 창
|
||||
고르기 · 지형마다 추천 미리 셈(509-5)이 같이 씀."""
|
||||
from B04_PreProcess.B04_PreProcess_Service import find_surface_model_for_selection
|
||||
|
||||
sources = await asyncio.to_thread(analysis_terrain_sources, params, models_dir)
|
||||
for source in [s for s in sources if s != design_terrain(params)]:
|
||||
try:
|
||||
await find_surface_model_for_selection(
|
||||
connection, project_id, analysis_terrain_params(params, source)
|
||||
)
|
||||
except LookupError:
|
||||
sources.remove(source)
|
||||
return sources
|
||||
|
||||
|
||||
async def _confirmed_model(
|
||||
connection: aiomysql.Connection, project_id: UUID
|
||||
) -> tuple[int, str, str] | None:
|
||||
|
||||
@@ -31,11 +31,8 @@ import {
|
||||
import { createContourLock } from "./B05_Profile_UI_RouteEdit_ContourLock";
|
||||
import { fetchDrainageLayers } from "./B05_Profile_UI_Drainage_Parts";
|
||||
import {
|
||||
analysisTerrain,
|
||||
fetchRoutePlan,
|
||||
fetchTerrainSources,
|
||||
setAnalysisTerrain,
|
||||
setTerrainContext,
|
||||
type AnalysisTerrain,
|
||||
} from "./B05_Profile_Api_Replan";
|
||||
import { bindRouteApply } from "./B05_Profile_UI_RouteEdit_Apply";
|
||||
@@ -116,7 +113,7 @@ export async function mountRouteEditor(
|
||||
const stationIntervalM = options.stationIntervalM ?? 20;
|
||||
const chrome = createRouteEditChrome(parent, options.tools);
|
||||
const { overlay, canvas, status, measureBox, measureText, measureButton } = chrome;
|
||||
const { minorContourButton, terrainSelect, stationSnapButton } = chrome;
|
||||
const { minorContourButton, terrainName, stationSnapButton } = chrome;
|
||||
const context = canvas.getContext("2d")!;
|
||||
|
||||
let expected: Vertex[] = [];
|
||||
@@ -161,11 +158,10 @@ export async function mountRouteEditor(
|
||||
let meta: VWorldMeta | null = null;
|
||||
/** 바탕 등고선 한 벌 — LAS 것이거나 도엽 것. 고르기는 `_Contour` 몫. */
|
||||
let contours: RouteEditContours | null = null;
|
||||
/** 지금 설계 지형(PLAN 133-2) — 고른 분석 지형이 이와 다르면 [확인]이 설계 지형을 바꾼다. */
|
||||
/** 지금 설계 지형 — 편집 분석이 쓰는 지형 · 바꾸기는 추천 창 [적용](509-5). */
|
||||
let designTerrain: AnalysisTerrain = "las";
|
||||
/** [확인]에 실을 분석 지형 — 고르기가 안 보이면 null(설계 지형 그대로). */
|
||||
const pickedTerrain = (): AnalysisTerrain | null =>
|
||||
terrainSelect.closest("label")!.hidden ? null : analysisTerrain(projectId);
|
||||
/** 추천 창이 고를 지형 — `/route/terrain-sources` 를 읽은 뒤. */
|
||||
let terrainSources: { sources: AnalysisTerrain[]; design: AnalysisTerrain } | null = null;
|
||||
/** 등고선 말고 함께 깔 도엽 레이어(하천중심선). */
|
||||
let otherSheets: PreparedLayer[] = [];
|
||||
/** 고른 등고선 가닥 — 없으면 -1(계획서 0-9 ⑦). */
|
||||
@@ -267,6 +263,7 @@ export async function mountRouteEditor(
|
||||
planned: () => planned,
|
||||
planMinRadiusM: () => minRadiusM,
|
||||
apply: (snapshot, message) => restoreSnapshot(snapshot, message, true),
|
||||
terrains: () => terrainSources,
|
||||
});
|
||||
|
||||
function resize(): void {
|
||||
@@ -447,8 +444,8 @@ export async function mountRouteEditor(
|
||||
historyControls.sync();
|
||||
}
|
||||
|
||||
/** 계산 묶음에 알림 — 노선 · 분석 지형 · 스냅이 저장분과 다르면 초안 · 같으면 null(352-3 · 430 · 433-14 ②). */
|
||||
const otherChanged = (): boolean => pickedTerrain() !== designTerrain || snap.changed();
|
||||
/** 계산 묶음에 알림 — 노선 · 스냅이 저장분과 다르면 초안 · 같으면 null(430 · 433-14 ②). */
|
||||
const otherChanged = (): boolean => snap.changed();
|
||||
function tellDraft(): void {
|
||||
if (!history) return;
|
||||
const dirty = lineChanged(history) || otherChanged();
|
||||
@@ -587,9 +584,7 @@ export async function mountRouteEditor(
|
||||
const unbindApply = bindRouteApply({
|
||||
projectId,
|
||||
nodes: () => ({ planned, curveOn, curveRadius, curveLock, curveArc }),
|
||||
// 분석 지형만 바꿔도 [확인]이 설계 지형을 바꿔 다시 셈한다(PLAN 133-2).
|
||||
changed: () => (!!history && lineChanged(history)) || otherChanged(),
|
||||
terrain: pickedTerrain,
|
||||
snap: snap.draft,
|
||||
initial,
|
||||
onApplied,
|
||||
@@ -633,21 +628,16 @@ export async function mountRouteEditor(
|
||||
syncCurveBar();
|
||||
if (!planned.length) planned = plannedLine.map((vertex) => [vertex[0], vertex[1]]);
|
||||
meta = drainage.meta;
|
||||
// 분석 지형 고르기(PLAN 133-1) — 라이다 · 수치지형도 둘 다 있을 때만 보임 · 열 때 안 씀(352-3).
|
||||
// 고른 적 없으면 데이터대로(설계 지형 · 라이다가 있으면 라이다) · 사용자가 바꿀 때만 기억.
|
||||
// 설계 지형 이름만(509-5) — 고르기는 추천 창 안 · 둘 다 있을 때만 이름을 보임.
|
||||
designTerrain = sources?.design ?? "las";
|
||||
const choosable = !!sources?.sources.includes("las") && !!sources?.sources.includes("sheet");
|
||||
terrainSelect.closest("label")!.hidden = !choosable;
|
||||
setTerrainContext(projectId, choosable, designTerrain);
|
||||
terrainSelect.value = analysisTerrain(projectId);
|
||||
terrainSources = sources;
|
||||
terrainName.hidden = !(sources?.sources.includes("las") && sources.sources.includes("sheet"));
|
||||
terrainName.textContent = `지형 ${designTerrain === "sheet" ? "수치지형도" : "라이다"}`;
|
||||
// 등고선은 따로 고른다(LAS 우선 · 수치지형도를 고르면 도엽). 나머지 도엽 레이어(하천중심선)만 배경으로 깐다.
|
||||
// 확정 모델 등고선은 설계 지형이 라이다일 때만 라이다 것 — 수치지형도 설계면 도엽 선(글도 도엽).
|
||||
const loadBackground = () =>
|
||||
loadRouteEditBackground(projectId, drainage, {
|
||||
surfaceModelId:
|
||||
analysisTerrain(projectId) === "las" && designTerrain === "las"
|
||||
? (options.surfaceModelId ?? null)
|
||||
: null,
|
||||
surfaceModelId: designTerrain === "las" ? (options.surfaceModelId ?? null) : null,
|
||||
intervalM: options.contourIntervalM ?? 1,
|
||||
smooth: options.smooth ?? false,
|
||||
});
|
||||
@@ -670,21 +660,6 @@ export async function mountRouteEditor(
|
||||
};
|
||||
useBackground(await loadBackground());
|
||||
if (closed) return;
|
||||
// 고르면 모달 안 분석을 고른 지형으로 다시 그림 — 등고선 · 기울기 · 종단 띠 · 횡단 · 잰 값.
|
||||
let terrainTicket = 0;
|
||||
terrainSelect.addEventListener("change", () => {
|
||||
setAnalysisTerrain(projectId, terrainSelect.value === "sheet" ? "sheet" : "las");
|
||||
tellDraft(); // 노선이 그대로여도 지형이 설계와 다르면 초안으로 다시 셈(352-3).
|
||||
const mine = ++terrainTicket;
|
||||
void loadBackground().then((background) => {
|
||||
if (closed || mine !== terrainTicket) return;
|
||||
useBackground(background);
|
||||
measure.clear();
|
||||
slope.refresh();
|
||||
showStatus();
|
||||
draw();
|
||||
});
|
||||
});
|
||||
viewWanted = true;
|
||||
resize(); // 칸 크기가 서 있으면 여기서 보던 화면을 연다(아니면 처음 보일 때).
|
||||
showStatus();
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
* `route` 칸 = `RouteSaveBody`)에 싣는다 — 노선이 마지막 저장 그대로면 null(안 보냄 · 472-6 6-B).
|
||||
* · 서버가 노선 저장 → 관 · 구조물 다시 얹기 → 세션 횡단 고침 넘김(`routeCarryPart` 가 낸 옛 노선 칸 · 같은 TS)까지
|
||||
* 한 번에 · 다 저장한 뒤 `routeSaved` · 페이지는 새로고침 없이 다시 세운다(`rebuildAfterRouteSave`).
|
||||
* · 추천 [적용] = `saveRecommendedRoute` — `as_initial: true` 로 바로 저장(초기값 = 추천 노선 · 사용자 확정).
|
||||
* · 추천 [적용] = `saveRecommendedRoute` — `as_initial: true` 로 바로 저장(초기값 = 추천 노선 · 사용자 확정) ·
|
||||
* 창에서 고른 지형이 설계 지형(509-5 · 손 [저장]은 지형 안 바꿈).
|
||||
* 편집기가 살아 있는 동안 여기 등록부에 「지금 초안」 · 「다시 세우기」를 건다(프로젝트 하나에 하나).
|
||||
* 실패하면 서버가 옛 노선으로 되살리고 화면 초안은 그대로 남는다(오류 한 줄은 부르는 쪽).
|
||||
* ========================================================================== */
|
||||
@@ -30,6 +31,7 @@ import {
|
||||
import { balloonOffsetsPayload } from "@util/common_util_mass_haul_balance_view";
|
||||
import type { SavePart } from "../A00_Common/design/design_save_api";
|
||||
import { clearRouteLatestCache } from "./B05_Profile_Api_Fetch";
|
||||
import { forgetDesignTerrainGrid } from "./B05_Profile_Api_TerrainGrid";
|
||||
import { reloadSavedPlusStations } from "./B05_Profile_Api_PlusStations";
|
||||
import {
|
||||
replanRoute,
|
||||
@@ -55,8 +57,6 @@ export interface RouteApplyParams {
|
||||
nodes: () => RouteNodes;
|
||||
/** 마지막 저장 뒤 노선이 바뀌었나 — 안 바뀌었으면 보내지 않는다. */
|
||||
changed: () => boolean;
|
||||
/** 설계 지형으로 보낼 분석 지형(PLAN 133-2) — 고르기가 안 보이면 null(지금 설계 지형 그대로). */
|
||||
terrain: () => string | null;
|
||||
/** [측점 스냅] 켬끔(요구 224-2) · 간격(390-5) — 곡선표에 적혀 측점 셈이 읽음. */
|
||||
snap: () => SnapSetting;
|
||||
/** 초기노선 갈래 — 저장에 지금 추천을 싣고, 결과 캐시를 비운 뒤 다시 적는다. */
|
||||
@@ -95,7 +95,8 @@ export function routeDraftOf(nodes: RouteNodes, settings: RouteSettings): RouteD
|
||||
/** 편집기를 등록부에 건다 — 같은 프로젝트에 새 편집기가 서면 그쪽으로 바뀐다. 돌려준 함수 = 편집기가 떠날 때 풂
|
||||
* (안 풀면 노선 저장 뒤 다시 선 페이지에서 옛 편집기 「고친 것 있음」이 [저장] · [확정]마다 노선 저장을 다시 돌림 · 445-2). */
|
||||
export function bindRouteApply(params: RouteApplyParams): () => void {
|
||||
const settings = () => ({ ...params.snap(), terrain: params.terrain() });
|
||||
// 손으로 고친 노선 = 설계 지형 그대로 — 지형 바꿈은 추천 [적용]만(509-5)
|
||||
const settings = () => ({ ...params.snap(), terrain: null });
|
||||
const source: RouteSource = {
|
||||
pending: async () => {
|
||||
const nodes = params.nodes();
|
||||
@@ -191,18 +192,26 @@ export async function saveRecommendedRoute(
|
||||
nodes: RouteNodes,
|
||||
initial: unknown,
|
||||
balancedProfile: boolean,
|
||||
terrain: string | null = null,
|
||||
): Promise<boolean> {
|
||||
const settings = sources.get(projectId)?.settings() ?? { stationSnap: false, terrain: null };
|
||||
showLoadingOverlay();
|
||||
try {
|
||||
const saved = await saveRouteDraft(projectId, {
|
||||
// 383-1 — 추천 선은 곡선 MC 를 일반측점에 맞춘 선 → [측점 스냅] 켬 · 측점 No.(390-5)로 저장
|
||||
draft: routeDraftOf(nodes, { ...settings, stationSnap: true, stationSnapStep: null }),
|
||||
// 창에서 고른 지형 = 설계 지형(509-5 · 서버 `switch_design_terrain`)
|
||||
draft: routeDraftOf(nodes, {
|
||||
...settings,
|
||||
stationSnap: true,
|
||||
stationSnapStep: null,
|
||||
terrain,
|
||||
}),
|
||||
initial,
|
||||
balanced_profile: balancedProfile,
|
||||
as_initial: true,
|
||||
});
|
||||
clearDrafts(projectId);
|
||||
forgetDesignTerrainGrid(projectId); // 지형이 바뀌었으면 새 지형 격자로(509-2)
|
||||
await reloadSavedPlusStations(projectId); // 페이지를 다시 세워도 남는 옛 저장분(493-5)
|
||||
sources.get(projectId)?.saved();
|
||||
const reset = routeResetNotice(saved.route_reset, saved.pipes?.crossing ?? 0);
|
||||
|
||||
@@ -58,7 +58,8 @@ export interface RouteEditChrome {
|
||||
/** [측점 스냅] 켬끔(요구 224-2) — `aria-pressed` · `is-active` 는 본체가 맞춤. */
|
||||
stationSnapButton: HTMLButtonElement;
|
||||
/** 분석 지형 고르기(PLAN 133-1) — 라이다 있는 프로젝트만 보임(감싼 label 의 hidden). */
|
||||
terrainSelect: HTMLSelectElement;
|
||||
/** 설계 지형 이름(509-5) — 고르기는 추천 창 안. */
|
||||
terrainName: HTMLElement;
|
||||
}
|
||||
|
||||
/** 손잡이 자리(24) + 틈(8) — 접혀도 남는 폭(`.ui-side-panels` margin-left). */
|
||||
@@ -119,13 +120,8 @@ export function createRouteEditChrome(
|
||||
</select>
|
||||
</label>
|
||||
<i class="b05-route__view-separator" aria-hidden="true"></i>
|
||||
<label class="b05-routeedit__terrain" hidden
|
||||
title="노선 편집 분석(등고선 · 지반고 · 기울기 · 횡단 미리보기 · 추천)에 쓸 지형 — 확정 지표면 · 종횡단은 안 바뀝니다">분석 지형
|
||||
<select class="ui-select ui-select--sm" data-act="terrain">
|
||||
<option value="las">라이다</option>
|
||||
<option value="sheet">수치지형도</option>
|
||||
</select>
|
||||
</label>
|
||||
<span class="b05-routeedit__terrain" data-slot="terrain-name" hidden
|
||||
title="설계 지형 — 바꾸기는 [노선 추천] 창에서 지형을 골라 [적용]"></span>
|
||||
</div>
|
||||
<div class="b05-routeedit__hint">
|
||||
<div class="b05-routeedit__hint-body">
|
||||
@@ -253,7 +249,7 @@ export function createRouteEditChrome(
|
||||
const minorContourButton = overlay.querySelector<HTMLButtonElement>(
|
||||
'[data-act="contour-minor"]',
|
||||
)!;
|
||||
const terrainSelect = overlay.querySelector<HTMLSelectElement>('[data-act="terrain"]')!;
|
||||
const terrainName = overlay.querySelector<HTMLElement>('[data-slot="terrain-name"]')!;
|
||||
const stationSnapButton = overlay.querySelector<HTMLButtonElement>('[data-act="station-snap"]')!;
|
||||
bindFootprintToggle(overlay.querySelector<HTMLButtonElement>('[data-act="footprint"]')!);
|
||||
bindMarkToggle(overlay.querySelector<HTMLButtonElement>('[data-act="stations"]')!, "stations");
|
||||
@@ -269,6 +265,6 @@ export function createRouteEditChrome(
|
||||
measureButton,
|
||||
minorContourButton,
|
||||
stationSnapButton,
|
||||
terrainSelect,
|
||||
terrainName,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,7 +17,11 @@
|
||||
* 요청했을 때 서버가 답으로 준 값(진짜 기본값)을 기억해 둔 것이다.
|
||||
* ========================================================================== */
|
||||
|
||||
import { fetchRoutePlan, type RoutePlanResponse } from "./B05_Profile_Api_Replan";
|
||||
import {
|
||||
fetchRoutePlan,
|
||||
type AnalysisTerrain,
|
||||
type RoutePlanResponse,
|
||||
} from "./B05_Profile_Api_Replan";
|
||||
import {
|
||||
fetchPrecomputeProgress,
|
||||
fetchRouteInitial,
|
||||
@@ -57,6 +61,8 @@ export interface InitialPanelParams {
|
||||
planMinRadiusM: () => number;
|
||||
/** 받은 노선을 편집값으로 싣는다 — 되돌리기 한 걸음으로 쌓인다. */
|
||||
apply: (snapshot: RouteEditSnapshot, message: string) => void;
|
||||
/** 추천 창이 고를 지형 · 지금 설계 지형(509-5) — 못 읽었으면 null(설계 지형 하나). */
|
||||
terrains: () => { sources: AnalysisTerrain[]; design: AnalysisTerrain } | null;
|
||||
}
|
||||
|
||||
export interface InitialPanel {
|
||||
@@ -126,6 +132,7 @@ export function createInitialPanel(params: InitialPanelParams): InitialPanel {
|
||||
criteria: RouteInitialCriteria | null,
|
||||
withProfile: boolean,
|
||||
recompute: boolean,
|
||||
terrain: AnalysisTerrain,
|
||||
): Promise<InitialChoice> {
|
||||
const response = await fetchRouteInitial(projectId, {
|
||||
mode: logic,
|
||||
@@ -133,8 +140,9 @@ export function createInitialPanel(params: InitialPanelParams): InitialPanel {
|
||||
...(criteria ? { criteria } : {}),
|
||||
with_profile: withProfile,
|
||||
...(recompute ? { recompute: true } : {}),
|
||||
terrain,
|
||||
});
|
||||
return normalizeRouteInitial(response, logic, weights, criteria, withProfile);
|
||||
return { ...normalizeRouteInitial(response, logic, weights, criteria, withProfile), terrain };
|
||||
}
|
||||
|
||||
recommendButton.addEventListener("click", () => {
|
||||
@@ -158,6 +166,8 @@ export function createInitialPanel(params: InitialPanelParams): InitialPanel {
|
||||
expected: (plan?.expected ?? []).map(([x, y]) => [x, y]),
|
||||
current: params.planned().map(([x, y]) => [x, y]),
|
||||
request,
|
||||
terrains: params.terrains()?.sources ?? [],
|
||||
terrain: params.terrains()?.design ?? "las",
|
||||
progress: () => fetchPrecomputeProgress(projectId),
|
||||
confirmText: () => applyResetText(projectId),
|
||||
onApply: (picked) => {
|
||||
@@ -179,6 +189,7 @@ export function createInitialPanel(params: InitialPanelParams): InitialPanel {
|
||||
with_profile: choiceWithProfile(picked), // initial_choice 에 남김(420)
|
||||
},
|
||||
choiceWithProfile(picked),
|
||||
picked.terrain ?? null,
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -215,6 +215,8 @@ export interface InitialChoice {
|
||||
/** 시종점 높이 차 ÷ 연장(%) · 상한을 넘나(종단으로 못 풂) — 서버 `ends_grade_pct` · `ends_over_limit`(419). */
|
||||
endsGradePct?: number | null;
|
||||
endsOverLimit?: boolean;
|
||||
/** 추천 창에서 고른 지형(509-5) — [적용]이 설계 지형으로. 옛 캐시면 없음(설계 지형 그대로). */
|
||||
terrain?: "las" | "sheet";
|
||||
}
|
||||
|
||||
/** 거의 같은 노선인 다른 갈래 하나 — 두 노선 사이 가장 먼 거리(m). */
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
* 서버 부르기 · 캐시 · 「현재 추천」은 `_Initial` 몫이고 여기는 화면만 만든다.
|
||||
* 균형 종단(계획서 63-4) — 후보마다 서버가 세운 종단의 한 줄 · 그림 · 셈한 후보 표는 이 모달
|
||||
* 안에서만 보인다(`_Recommend_Profile`) · 닫으면 버린다.
|
||||
* 지형 고르기(509-5 · 라이다 · 수치지형도 둘일 때) — 바꾸면 그 지형 미리 셈 저장본 · 없으면 안내 + [재계산] ·
|
||||
* [적용]하면 그 지형이 설계 지형(초기값도 그 지형으로).
|
||||
* 적용될 초기 종단(476) — 서버가 [적용]과 같은 코드로 세운 종단을 같은 그림에 선 · 한 줄로 더한다
|
||||
* (끔이어도) · [적용]은 「종단 · 횡단 손 입력이 모두 지워짐」 확인 뒤(요구 277).
|
||||
* ========================================================================== */
|
||||
@@ -25,6 +27,7 @@ import {
|
||||
showToast,
|
||||
} from "@ui/ui_template_elements";
|
||||
import { toggleButton } from "./B05_Profile_UI_Panel_Fields";
|
||||
import type { AnalysisTerrain } from "./B05_Profile_Api_Replan";
|
||||
import type {
|
||||
PrecomputeProgress,
|
||||
RouteInitialCriteria,
|
||||
@@ -125,6 +128,8 @@ const CRITERIA_FIELD: Record<
|
||||
},
|
||||
};
|
||||
|
||||
const TERRAIN_NAME: Record<AnalysisTerrain, string> = { las: "라이다", sheet: "수치지형도" };
|
||||
|
||||
export interface RecommendParams {
|
||||
/** 칸에 처음 채울 제약 겹침(직접 넣은 값만). */
|
||||
criteria: RouteInitialCriteria | null;
|
||||
@@ -148,7 +153,11 @@ export interface RecommendParams {
|
||||
criteria: RouteInitialCriteria | null,
|
||||
withProfile: boolean,
|
||||
recompute: boolean,
|
||||
terrain: AnalysisTerrain,
|
||||
) => Promise<InitialChoice>;
|
||||
/** 고를 수 있는 지형(509-5) — 둘이면 창에 고르기 · 처음 값 = 지금 설계 지형. */
|
||||
terrains: AnalysisTerrain[];
|
||||
terrain: AnalysisTerrain;
|
||||
/** [적용] — 미리 본 노선을 바로 저장한다(새 초기값). */
|
||||
onApply: (choice: InitialChoice) => void;
|
||||
/** [적용] 확인 창 글 — 초기화로 지워질 수(492-2 · [저장]과 같은 글). */
|
||||
@@ -169,6 +178,9 @@ export function openRecommendModal(params: RecommendParams): void {
|
||||
let preview: InitialChoice | null = null;
|
||||
/** 종단 반영(421) — 켬 = 후보마다 균형 종단 · 끔 = 계획고 = 지반고. */
|
||||
let profileOn = params.withProfile;
|
||||
/** 창 지형(509-5) — 바꾸면 그 지형 저장본 · [적용]이 설계 지형으로. */
|
||||
let terrain = params.terrain;
|
||||
const choosable = params.terrains.includes("las") && params.terrains.includes("sheet");
|
||||
let busy = false;
|
||||
/** 마지막 미리 보기 뒤 슬라이더 · 제약 칸을 만졌다 — 미리 본 노선이 칸 값과 다르다. */
|
||||
let dirty = false;
|
||||
@@ -211,6 +223,12 @@ export function openRecommendModal(params: RecommendParams): void {
|
||||
<div class="b05-routerec__bars" data-slot="bars"></div>
|
||||
<span class="b05-routerec__sum" data-weight-sum></span>
|
||||
</div>
|
||||
<label class="b05-routerec__terrain" data-terrain-row hidden
|
||||
title="추천 · 초기 종단을 셀 지형 — [적용]하면 이 지형이 설계 지형(평면 · 종단 · 횡단)">지형
|
||||
<select class="ui-select ui-select--sm" data-act="terrain">
|
||||
<option value="las">라이다</option>
|
||||
<option value="sheet">수치지형도</option>
|
||||
</select></label>
|
||||
<div class="b05-routerec__criteria" data-slot="criteria"></div>
|
||||
<label class="b05-routerec__target" data-target-row hidden
|
||||
title="영선 노선이 따라갈 종단기울기(%) — 비우면 위 종단기울기 상한">목표 기울기
|
||||
@@ -336,6 +354,9 @@ export function openRecommendModal(params: RecommendParams): void {
|
||||
rerun.title = "지금 칸 값으로 서버 저장본을 지우고 새로 셈";
|
||||
rerun.before(profileToggle);
|
||||
const stateEl = q<HTMLElement>("[data-state]");
|
||||
const terrainSelect = q<HTMLSelectElement>('[data-act="terrain"]');
|
||||
q<HTMLElement>("[data-terrain-row]").hidden = !choosable;
|
||||
terrainSelect.value = terrain;
|
||||
const line = (name: string): HTMLElement => q<HTMLElement>(`[data-line="${name}"]`);
|
||||
const criteriaInputs = body.querySelectorAll<HTMLInputElement>("[data-criteria]");
|
||||
bindRecommendHelp(body);
|
||||
@@ -420,6 +441,7 @@ export function openRecommendModal(params: RecommendParams): void {
|
||||
q<HTMLElement>("[data-target-row]").hidden = logic !== "gradeline";
|
||||
// 종단 반영 오른쪽 · 늘 켜짐(입력 오류 · 셈 중 빼고 · 421)
|
||||
rerun.disabled = busy || invalid.size > 0;
|
||||
terrainSelect.disabled = busy;
|
||||
profileToggle.disabled = busy;
|
||||
profileToggle.setAttribute("aria-pressed", String(profileOn));
|
||||
profileToggle.classList.toggle("is-active", profileOn);
|
||||
@@ -498,7 +520,9 @@ export function openRecommendModal(params: RecommendParams): void {
|
||||
preview = null;
|
||||
// 후보마다 균형 종단을 셈해 처음 한 번은 수십 초 — 지난 초를 보인다(계획서 63-4).
|
||||
const started = Date.now();
|
||||
const label = `${choiceLabel(logic, sentWeights)}${withProfile ? "" : " · 종단 끔"}`;
|
||||
const label = `${choiceLabel(logic, sentWeights)}${withProfile ? "" : " · 종단 끔"}${
|
||||
choosable ? ` · ${TERRAIN_NAME[terrain]}` : ""
|
||||
}`;
|
||||
/** 서버가 뒤에서 미리 셈 중이면 그 진행(364-4) — 2초마다 읽음. */
|
||||
let behind = "";
|
||||
const tick = (): void => {
|
||||
@@ -529,6 +553,7 @@ export function openRecommendModal(params: RecommendParams): void {
|
||||
Object.keys(criteria).length ? criteria : null,
|
||||
withProfile,
|
||||
recompute,
|
||||
terrain,
|
||||
);
|
||||
if (closed || mine !== seq) return;
|
||||
// 비운 칸을 물었을 때 서버가 준 값 — 그 칸의 진짜 기본값으로 기억해 둔다.
|
||||
@@ -617,6 +642,10 @@ export function openRecommendModal(params: RecommendParams): void {
|
||||
});
|
||||
});
|
||||
rerun.addEventListener("click", () => void run(true));
|
||||
terrainSelect.addEventListener("change", () => {
|
||||
terrain = terrainSelect.value === "sheet" ? "sheet" : "las";
|
||||
void run(); // 그 지형 저장본 — 없으면 안내 · [재계산](509-5)
|
||||
});
|
||||
applyButton.addEventListener("click", () => {
|
||||
const picked = preview;
|
||||
if (!picked) return;
|
||||
|
||||
@@ -372,6 +372,19 @@
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 지형 고르기(509-5) — 이름 · 고르기 한 줄. */
|
||||
.b05-routerec__terrain {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 7em;
|
||||
align-items: center;
|
||||
gap: var(--spacing-8);
|
||||
color: var(--color-text-body);
|
||||
}
|
||||
|
||||
.b05-routerec__terrain[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 공통 제약 네 칸 — 한 줄에 하나(이름 · 입력 · 단위). 비우면 placeholder 가 기본값. */
|
||||
.b05-routerec__criteria {
|
||||
display: grid;
|
||||
|
||||
Reference in New Issue
Block a user