Spaces:
Running
Running
Sync DemoService 65a98143cc0c4d48a5221f2d90552f7d3a4bc364
Browse files
api/ai-service/app/api/v1/endpoints/route_generation.py
CHANGED
|
@@ -47,6 +47,7 @@ class RouteGenerateRequest(BaseModel):
|
|
| 47 |
gatv2Weight: float | None = Field(default=None, ge=0.0, le=1.0)
|
| 48 |
routeCount: int = Field(default=8, ge=1, le=100)
|
| 49 |
targetDistanceKm: float = Field(default=6.0, ge=0.5, le=30.0)
|
|
|
|
| 50 |
labels: list[str] | None = None
|
| 51 |
selectedLabel: str | None = None
|
| 52 |
selectedTopK: int = Field(default=0, ge=0, le=20)
|
|
@@ -402,6 +403,7 @@ def _run_fast_road_pipeline(
|
|
| 402 |
route_count: int,
|
| 403 |
target_distance_km: float,
|
| 404 |
selected_label: str | None,
|
|
|
|
| 405 |
) -> dict[str, Any]:
|
| 406 |
"""Create real walkable-road routes without per-route CLIP inference.
|
| 407 |
|
|
@@ -425,12 +427,24 @@ def _run_fast_road_pipeline(
|
|
| 425 |
# failed request before it can return any usable road loop.
|
| 426 |
examples = getattr(module, "GPX_SHAPE_TEMPLATES", {}).get(selected_label, [])
|
| 427 |
candidate_pool = min(16 if examples else 8, max(route_count, route_count + (11 if examples else 1)))
|
| 428 |
-
|
| 429 |
-
graph
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 434 |
# A selected shape is a preference, not a reason to leave the runner with
|
| 435 |
# no route. Sparse neighbourhoods and long distances do not always have
|
| 436 |
# enough roads to satisfy the strict shape-quality gate. In that case
|
|
@@ -458,7 +472,12 @@ def _run_fast_road_pipeline(
|
|
| 458 |
raise RuntimeError("No walkable road routes were generated near this location.")
|
| 459 |
|
| 460 |
routes: list[dict[str, Any]] = []
|
|
|
|
| 461 |
for route in candidates:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 462 |
# Simplified OSM edges can bend between intersections. Preserve those
|
| 463 |
# bends rather than drawing straight lines across blocks or water.
|
| 464 |
coords = _route_nodes_to_coords(graph, route["nodes"])
|
|
@@ -499,7 +518,7 @@ def _run_fast_road_pipeline(
|
|
| 499 |
if examples:
|
| 500 |
routes.sort(key=lambda route: (
|
| 501 |
-route["route_quality"]["gpx_similarity"],
|
| 502 |
-
abs(route["length_km"] - target_distance_km),
|
| 503 |
))
|
| 504 |
routes = routes[:route_count]
|
| 505 |
if not routes:
|
|
@@ -511,7 +530,7 @@ def _run_fast_road_pipeline(
|
|
| 511 |
"candidate_count": len(candidates),
|
| 512 |
"requested_count": route_count,
|
| 513 |
"generated_count": len(routes),
|
| 514 |
-
"target_distance_km": target_distance_km,
|
| 515 |
"radius_km": radius_km,
|
| 516 |
"labels": [selected_label] if selected_label else [],
|
| 517 |
"selected_label": selected_label,
|
|
@@ -674,11 +693,12 @@ def generate_routes(payload: RouteGenerateRequest) -> dict[str, Any]:
|
|
| 674 |
route_count_cap = 20 if use_current else 100
|
| 675 |
route_count = min(route_count_cap, max(1, payload.routeCount))
|
| 676 |
target_distance_km = max(0.5, payload.targetDistanceKm)
|
|
|
|
| 677 |
# A 1.5 km graph is enough for ordinary short courses, but it cannot make
|
| 678 |
# a truthful 16~30 km route. Grow the road graph only as much as the
|
| 679 |
# requested distance needs, up to the request model's safe 10 km limit.
|
| 680 |
required_radius_km = target_distance_km / 5.5
|
| 681 |
-
radius_km = min(10.0, max(1.5, min(10.0, payload.radiusKm), required_radius_km))
|
| 682 |
|
| 683 |
if payload.fastRoads:
|
| 684 |
module = None
|
|
@@ -693,6 +713,7 @@ def generate_routes(payload: RouteGenerateRequest) -> dict[str, Any]:
|
|
| 693 |
route_count=route_count,
|
| 694 |
target_distance_km=target_distance_km,
|
| 695 |
selected_label=selected_label,
|
|
|
|
| 696 |
)
|
| 697 |
except Exception as primary_exc:
|
| 698 |
logger.exception("fast walk-road route generation failed")
|
|
@@ -722,6 +743,7 @@ def generate_routes(payload: RouteGenerateRequest) -> dict[str, Any]:
|
|
| 722 |
route_count=route_count,
|
| 723 |
target_distance_km=target_distance_km,
|
| 724 |
selected_label=selected_label,
|
|
|
|
| 725 |
)
|
| 726 |
except Exception:
|
| 727 |
logger.exception("fallback-centre walk-road generation failed")
|
|
|
|
| 47 |
gatv2Weight: float | None = Field(default=None, ge=0.0, le=1.0)
|
| 48 |
routeCount: int = Field(default=8, ge=1, le=100)
|
| 49 |
targetDistanceKm: float = Field(default=6.0, ge=0.5, le=30.0)
|
| 50 |
+
shapePriority: bool = False
|
| 51 |
labels: list[str] | None = None
|
| 52 |
selectedLabel: str | None = None
|
| 53 |
selectedTopK: int = Field(default=0, ge=0, le=20)
|
|
|
|
| 403 |
route_count: int,
|
| 404 |
target_distance_km: float,
|
| 405 |
selected_label: str | None,
|
| 406 |
+
shape_priority: bool = False,
|
| 407 |
) -> dict[str, Any]:
|
| 408 |
"""Create real walkable-road routes without per-route CLIP inference.
|
| 409 |
|
|
|
|
| 427 |
# failed request before it can return any usable road loop.
|
| 428 |
examples = getattr(module, "GPX_SHAPE_TEMPLATES", {}).get(selected_label, [])
|
| 429 |
candidate_pool = min(16 if examples else 8, max(route_count, route_count + (11 if examples else 1)))
|
| 430 |
+
if shape_priority and examples:
|
| 431 |
+
# Explore multiple physical sizes on the same road graph. The UI did
|
| 432 |
+
# not request a distance; these are search scales, not length targets.
|
| 433 |
+
candidates = []
|
| 434 |
+
for search_distance_km in (6.0, 11.0, 18.0):
|
| 435 |
+
candidates.extend(build_candidates(
|
| 436 |
+
graph=graph,
|
| 437 |
+
route_count=min(6, candidate_pool),
|
| 438 |
+
target_distance_m=search_distance_km * 1000.0,
|
| 439 |
+
selected_label=selected_label,
|
| 440 |
+
))
|
| 441 |
+
else:
|
| 442 |
+
candidates = build_candidates(
|
| 443 |
+
graph=graph,
|
| 444 |
+
route_count=candidate_pool,
|
| 445 |
+
target_distance_m=target_distance_km * 1000.0,
|
| 446 |
+
selected_label=selected_label,
|
| 447 |
+
)
|
| 448 |
# A selected shape is a preference, not a reason to leave the runner with
|
| 449 |
# no route. Sparse neighbourhoods and long distances do not always have
|
| 450 |
# enough roads to satisfy the strict shape-quality gate. In that case
|
|
|
|
| 472 |
raise RuntimeError("No walkable road routes were generated near this location.")
|
| 473 |
|
| 474 |
routes: list[dict[str, Any]] = []
|
| 475 |
+
seen_nodes: set[tuple[Any, ...]] = set()
|
| 476 |
for route in candidates:
|
| 477 |
+
node_signature = tuple(route["nodes"])
|
| 478 |
+
if node_signature in seen_nodes:
|
| 479 |
+
continue
|
| 480 |
+
seen_nodes.add(node_signature)
|
| 481 |
# Simplified OSM edges can bend between intersections. Preserve those
|
| 482 |
# bends rather than drawing straight lines across blocks or water.
|
| 483 |
coords = _route_nodes_to_coords(graph, route["nodes"])
|
|
|
|
| 518 |
if examples:
|
| 519 |
routes.sort(key=lambda route: (
|
| 520 |
-route["route_quality"]["gpx_similarity"],
|
| 521 |
+
abs(route["length_km"] - target_distance_km) if not shape_priority else 0,
|
| 522 |
))
|
| 523 |
routes = routes[:route_count]
|
| 524 |
if not routes:
|
|
|
|
| 530 |
"candidate_count": len(candidates),
|
| 531 |
"requested_count": route_count,
|
| 532 |
"generated_count": len(routes),
|
| 533 |
+
"target_distance_km": 0.0 if shape_priority else target_distance_km,
|
| 534 |
"radius_km": radius_km,
|
| 535 |
"labels": [selected_label] if selected_label else [],
|
| 536 |
"selected_label": selected_label,
|
|
|
|
| 693 |
route_count_cap = 20 if use_current else 100
|
| 694 |
route_count = min(route_count_cap, max(1, payload.routeCount))
|
| 695 |
target_distance_km = max(0.5, payload.targetDistanceKm)
|
| 696 |
+
shape_priority = payload.shapePriority and selected_label in {"dog", "symbol_star"}
|
| 697 |
# A 1.5 km graph is enough for ordinary short courses, but it cannot make
|
| 698 |
# a truthful 16~30 km route. Grow the road graph only as much as the
|
| 699 |
# requested distance needs, up to the request model's safe 10 km limit.
|
| 700 |
required_radius_km = target_distance_km / 5.5
|
| 701 |
+
radius_km = min(10.0, max(5.0 if shape_priority else 1.5, min(10.0, payload.radiusKm), required_radius_km))
|
| 702 |
|
| 703 |
if payload.fastRoads:
|
| 704 |
module = None
|
|
|
|
| 713 |
route_count=route_count,
|
| 714 |
target_distance_km=target_distance_km,
|
| 715 |
selected_label=selected_label,
|
| 716 |
+
shape_priority=shape_priority,
|
| 717 |
)
|
| 718 |
except Exception as primary_exc:
|
| 719 |
logger.exception("fast walk-road route generation failed")
|
|
|
|
| 743 |
route_count=route_count,
|
| 744 |
target_distance_km=target_distance_km,
|
| 745 |
selected_label=selected_label,
|
| 746 |
+
shape_priority=shape_priority,
|
| 747 |
)
|
| 748 |
except Exception:
|
| 749 |
logger.exception("fallback-centre walk-road generation failed")
|
api/ai-service/tests/test_route_generation_recovery.py
CHANGED
|
@@ -133,6 +133,30 @@ def test_gpx_guided_fast_routes_are_ranked_by_actual_road_similarity(monkeypatch
|
|
| 133 |
assert result["best_route"] == result["routes"][0]
|
| 134 |
|
| 135 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
def test_place_request_returns_all_100_routes(pipeline):
|
| 137 |
pipeline.setattr(routes, "_run_fast_road_pipeline", Mock(side_effect=RuntimeError("All Overpass endpoints failed")))
|
| 138 |
result = routes.generate_routes(routes.RouteGenerateRequest(place="์๊ทํฌ์", fallbackLatitude=33.25, fallbackLongitude=126.56, routeCount=100))
|
|
|
|
| 133 |
assert result["best_route"] == result["routes"][0]
|
| 134 |
|
| 135 |
|
| 136 |
+
def test_shape_priority_searches_multiple_lengths_and_ranks_by_shape(monkeypatch):
|
| 137 |
+
searched_distances = []
|
| 138 |
+
|
| 139 |
+
def candidates_for_distance(**kwargs):
|
| 140 |
+
distance = kwargs["target_distance_m"]
|
| 141 |
+
searched_distances.append(distance)
|
| 142 |
+
return [{"nodes": [distance, distance + 1], "length_m": distance,
|
| 143 |
+
"shape_quality": 0.6, "is_loop": True}]
|
| 144 |
+
|
| 145 |
+
module = SimpleNamespace(
|
| 146 |
+
GPX_SHAPE_TEMPLATES={"dog": [[(0, 0), (1, 0), (0, 1)]]},
|
| 147 |
+
_build_or_get_walk_graph=lambda *_: (object(), "test"),
|
| 148 |
+
generate_candidate_routes=candidates_for_distance,
|
| 149 |
+
route_points=lambda *_: [], simplify_coords=lambda points: points,
|
| 150 |
+
)
|
| 151 |
+
monkeypatch.setattr(routes, "_route_nodes_to_coords", lambda _graph, nodes: [[nodes[0], 0], [nodes[1], 1]])
|
| 152 |
+
monkeypatch.setattr(routes, "best_gpx_shape_match", lambda coords, _examples: (coords[0][0] / 18000, 0))
|
| 153 |
+
result = routes._run_fast_road_pipeline(module, place=None, center_lat=37, center_lon=127,
|
| 154 |
+
radius_km=5, route_count=2, target_distance_km=6, selected_label="dog", shape_priority=True)
|
| 155 |
+
assert searched_distances == [6000, 11000, 18000]
|
| 156 |
+
assert [route["length_km"] for route in result["routes"]] == [18, 11]
|
| 157 |
+
assert result["target_distance_km"] == 0
|
| 158 |
+
|
| 159 |
+
|
| 160 |
def test_place_request_returns_all_100_routes(pipeline):
|
| 161 |
pipeline.setattr(routes, "_run_fast_road_pipeline", Mock(side_effect=RuntimeError("All Overpass endpoints failed")))
|
| 162 |
result = routes.generate_routes(routes.RouteGenerateRequest(place="์๊ทํฌ์", fallbackLatitude=33.25, fallbackLongitude=126.56, routeCount=100))
|
main/app/route-results/page.tsx
CHANGED
|
@@ -162,6 +162,7 @@ function RouteResultsPageContent() {
|
|
| 162 |
const longitude = Number.isFinite(initialLongitude) ? initialLongitude : null;
|
| 163 |
const radiusKm = Number.isFinite(initialRadiusKm) ? initialRadiusKm : 5;
|
| 164 |
const selectedTopK = Number.isFinite(initialSelectedTopK) ? initialSelectedTopK : 3;
|
|
|
|
| 165 |
const routeFallbackLatitude = Number.isFinite(fallbackLatitude) ? fallbackLatitude : null;
|
| 166 |
const routeFallbackLongitude = Number.isFinite(fallbackLongitude) ? fallbackLongitude : null;
|
| 167 |
|
|
@@ -170,7 +171,7 @@ function RouteResultsPageContent() {
|
|
| 170 |
const count = Number(routeCountInput);
|
| 171 |
const distance = Number(targetDistanceInput);
|
| 172 |
if (!requestPlace || !Number.isFinite(count) || count < 5 || count > 100 ||
|
| 173 |
-
!Number.isFinite(distance) || distance < 1 || distance > 30) {
|
| 174 |
void fetchRoutes(); // Keep the same field validation messages.
|
| 175 |
return;
|
| 176 |
}
|
|
@@ -205,7 +206,7 @@ function RouteResultsPageContent() {
|
|
| 205 |
setError("๊ฒฝ๋ก ์๋ 5~100 ์ฌ์ด๋ก ์
๋ ฅํด์ฃผ์ธ์.");
|
| 206 |
return;
|
| 207 |
}
|
| 208 |
-
if (!Number.isFinite(targetDistanceKm) || targetDistanceKm < 1 || targetDistanceKm > 30) {
|
| 209 |
setError("๋ชฉํ ๊ฑฐ๋ฆฌ๋ 1~30km ์ฌ์ด๋ก ์
๋ ฅํด์ฃผ์ธ์.");
|
| 210 |
return;
|
| 211 |
}
|
|
@@ -251,6 +252,7 @@ function RouteResultsPageContent() {
|
|
| 251 |
radiusKm,
|
| 252 |
routeCount,
|
| 253 |
targetDistanceKm,
|
|
|
|
| 254 |
labels: AVAILABLE_LABELS,
|
| 255 |
selectedLabel,
|
| 256 |
selectedTopK,
|
|
@@ -480,7 +482,7 @@ function RouteResultsPageContent() {
|
|
| 480 |
/>
|
| 481 |
|
| 482 |
<SectionCard>
|
| 483 |
-
<div className=
|
| 484 |
<label className="grid gap-2">
|
| 485 |
<span className="rts-label">์ง์ญ</span>
|
| 486 |
<input value={place} onChange={(event) => setPlace(event.target.value)} className="rts-input" />
|
|
@@ -489,10 +491,10 @@ function RouteResultsPageContent() {
|
|
| 489 |
<span className="rts-label">๊ฒฝ๋ก ์</span>
|
| 490 |
<input type="number" min={5} max={100} value={routeCountInput} onChange={(event) => setRouteCountInput(event.target.value)} className="rts-input" />
|
| 491 |
</label>
|
| 492 |
-
<label className="grid gap-2">
|
| 493 |
<span className="rts-label">๋ชฉํ ๊ฑฐ๋ฆฌ</span>
|
| 494 |
<input type="number" min={1} max={30} step={0.5} value={targetDistanceInput} onChange={(event) => setTargetDistanceInput(event.target.value)} className="rts-input" />
|
| 495 |
-
</label>
|
| 496 |
</div>
|
| 497 |
|
| 498 |
{error ? <p className="mt-3 text-sm text-red-600">{error}</p> : null}
|
|
@@ -517,8 +519,8 @@ function RouteResultsPageContent() {
|
|
| 517 |
<span>์ถ์ฒ ๊ฒฝ๋ก</span>
|
| 518 |
</div>
|
| 519 |
<div>
|
| 520 |
-
<strong>{formatDistance(result.target_distance_km)}</strong>
|
| 521 |
-
<span>๋ชฉํ ๊ฑฐ๋ฆฌ</span>
|
| 522 |
</div>
|
| 523 |
<div>
|
| 524 |
<strong>{formatGeneratedRouteLabel(selectedLabel)}</strong>
|
|
|
|
| 162 |
const longitude = Number.isFinite(initialLongitude) ? initialLongitude : null;
|
| 163 |
const radiusKm = Number.isFinite(initialRadiusKm) ? initialRadiusKm : 5;
|
| 164 |
const selectedTopK = Number.isFinite(initialSelectedTopK) ? initialSelectedTopK : 3;
|
| 165 |
+
const shapePriority = searchParams.get("shapePriority") === "true" && (selectedLabel === "dog" || selectedLabel === "symbol_star");
|
| 166 |
const routeFallbackLatitude = Number.isFinite(fallbackLatitude) ? fallbackLatitude : null;
|
| 167 |
const routeFallbackLongitude = Number.isFinite(fallbackLongitude) ? fallbackLongitude : null;
|
| 168 |
|
|
|
|
| 171 |
const count = Number(routeCountInput);
|
| 172 |
const distance = Number(targetDistanceInput);
|
| 173 |
if (!requestPlace || !Number.isFinite(count) || count < 5 || count > 100 ||
|
| 174 |
+
(!shapePriority && (!Number.isFinite(distance) || distance < 1 || distance > 30))) {
|
| 175 |
void fetchRoutes(); // Keep the same field validation messages.
|
| 176 |
return;
|
| 177 |
}
|
|
|
|
| 206 |
setError("๊ฒฝ๋ก ์๋ 5~100 ์ฌ์ด๋ก ์
๋ ฅํด์ฃผ์ธ์.");
|
| 207 |
return;
|
| 208 |
}
|
| 209 |
+
if (!shapePriority && (!Number.isFinite(targetDistanceKm) || targetDistanceKm < 1 || targetDistanceKm > 30)) {
|
| 210 |
setError("๋ชฉํ ๊ฑฐ๋ฆฌ๋ 1~30km ์ฌ์ด๋ก ์
๋ ฅํด์ฃผ์ธ์.");
|
| 211 |
return;
|
| 212 |
}
|
|
|
|
| 252 |
radiusKm,
|
| 253 |
routeCount,
|
| 254 |
targetDistanceKm,
|
| 255 |
+
shapePriority,
|
| 256 |
labels: AVAILABLE_LABELS,
|
| 257 |
selectedLabel,
|
| 258 |
selectedTopK,
|
|
|
|
| 482 |
/>
|
| 483 |
|
| 484 |
<SectionCard>
|
| 485 |
+
<div className={`grid gap-3 sm:items-end ${shapePriority ? "sm:grid-cols-[1fr_120px]" : "sm:grid-cols-[1fr_120px_120px]"}`}>
|
| 486 |
<label className="grid gap-2">
|
| 487 |
<span className="rts-label">์ง์ญ</span>
|
| 488 |
<input value={place} onChange={(event) => setPlace(event.target.value)} className="rts-input" />
|
|
|
|
| 491 |
<span className="rts-label">๊ฒฝ๋ก ์</span>
|
| 492 |
<input type="number" min={5} max={100} value={routeCountInput} onChange={(event) => setRouteCountInput(event.target.value)} className="rts-input" />
|
| 493 |
</label>
|
| 494 |
+
{!shapePriority ? <label className="grid gap-2">
|
| 495 |
<span className="rts-label">๋ชฉํ ๊ฑฐ๋ฆฌ</span>
|
| 496 |
<input type="number" min={1} max={30} step={0.5} value={targetDistanceInput} onChange={(event) => setTargetDistanceInput(event.target.value)} className="rts-input" />
|
| 497 |
+
</label> : null}
|
| 498 |
</div>
|
| 499 |
|
| 500 |
{error ? <p className="mt-3 text-sm text-red-600">{error}</p> : null}
|
|
|
|
| 519 |
<span>์ถ์ฒ ๊ฒฝ๋ก</span>
|
| 520 |
</div>
|
| 521 |
<div>
|
| 522 |
+
<strong>{shapePriority ? "๋ชจ์ ์ฐ์ " : formatDistance(result.target_distance_km)}</strong>
|
| 523 |
+
<span>{shapePriority ? "์ถ์ฒ ๊ธฐ์ค" : "๋ชฉํ ๊ฑฐ๋ฆฌ"}</span>
|
| 524 |
</div>
|
| 525 |
<div>
|
| 526 |
<strong>{formatGeneratedRouteLabel(selectedLabel)}</strong>
|
main/app/route-search/page.tsx
CHANGED
|
@@ -650,8 +650,11 @@ function RouteSearchPageContent() {
|
|
| 650 |
: [];
|
| 651 |
const isWaitingForCurrentLocation = isLocating;
|
| 652 |
const summaryPlace = source === "current" ? "ํ์ฌ ์์น" : selectedDistrictLabel;
|
| 653 |
-
const
|
| 654 |
-
const
|
|
|
|
|
|
|
|
|
|
| 655 |
|
| 656 |
useEffect(() => {
|
| 657 |
const isValid = isUiFlowPermitValid(FLOW_KEYS.routeSearch, flowToken);
|
|
@@ -837,11 +840,11 @@ function RouteSearchPageContent() {
|
|
| 837 |
setFormError("์ถ์ฒ ๊ฒฝ๋ก ์๋ 5~100 ์ฌ์ด ์ซ์๋ก ์
๋ ฅํด ์ฃผ์ธ์.");
|
| 838 |
return;
|
| 839 |
}
|
| 840 |
-
if (
|
| 841 |
!Number.isFinite(targetDistanceKm) ||
|
| 842 |
targetDistanceKm < 1 ||
|
| 843 |
targetDistanceKm > 30
|
| 844 |
-
) {
|
| 845 |
setFormError("๋ชฉํ ๊ฑฐ๋ฆฌ๋ 1~30km ์ฌ์ด ์ซ์๋ก ์
๋ ฅํด ์ฃผ์ธ์.");
|
| 846 |
return;
|
| 847 |
}
|
|
@@ -852,6 +855,7 @@ function RouteSearchPageContent() {
|
|
| 852 |
radiusKm: String(radiusKm),
|
| 853 |
routeCount: String(routeCount),
|
| 854 |
targetDistanceKm: String(targetDistanceKm),
|
|
|
|
| 855 |
label: selectedLabel,
|
| 856 |
selectedTopK: "3",
|
| 857 |
// Supply the visible map centre for a server-side recovery attempt when
|
|
@@ -1280,7 +1284,7 @@ function RouteSearchPageContent() {
|
|
| 1280 |
</div>
|
| 1281 |
</label>
|
| 1282 |
|
| 1283 |
-
<label className="grid gap-2">
|
| 1284 |
<span className="text-sm font-bold">๋ชฉํ ๊ฑฐ๋ฆฌ</span>
|
| 1285 |
<div className="relative">
|
| 1286 |
<button
|
|
@@ -1328,7 +1332,7 @@ function RouteSearchPageContent() {
|
|
| 1328 |
})}
|
| 1329 |
</MotionPopover>
|
| 1330 |
</div>
|
| 1331 |
-
</label>
|
| 1332 |
|
| 1333 |
<details className="rounded-2xl bg-[#f7f7f7] p-4">
|
| 1334 |
<summary className="cursor-pointer text-sm font-bold">์ธ๋ถ ์ค์ </summary>
|
|
|
|
| 650 |
: [];
|
| 651 |
const isWaitingForCurrentLocation = isLocating;
|
| 652 |
const summaryPlace = source === "current" ? "ํ์ฌ ์์น" : selectedDistrictLabel;
|
| 653 |
+
const shapePriority = selectedLabel === "dog" || selectedLabel === "symbol_star";
|
| 654 |
+
const selectedSummary = `${summaryPlace} ยท ${selectedShape.label}${shapePriority ? " ยท ๋ชจ์ ์ฐ์ " : ` ยท ${targetDistanceInput || "-"}km`}`;
|
| 655 |
+
const routePlanText = shapePriority
|
| 656 |
+
? `${summaryPlace}์ ๋๋ก์์ ${selectedShape.label} ๋ชจ์๊ณผ ๊ฐ์ฅ ๋ฎ์ ๊ฒฝ๋ก๋ฅผ ์ฐพ์ ${routeCountInput || "-"}๊ฐ ์ถ์ฒํฉ๋๋ค. ๊ฑฐ๋ฆฌ๋ ์์ฑ ๊ฒฐ๊ณผ์์ ํ์ธํ ์ ์์ด์.`
|
| 657 |
+
: `${summaryPlace}์์ ${selectedShape.label} ๋ชจ์์ ๊ฐ๊น์ด ${targetDistanceInput || "-"}km ๊ฒฝ๋ก๋ฅผ ${routeCountInput || "-"}๊ฐ ์ถ์ฒํฉ๋๋ค.`;
|
| 658 |
|
| 659 |
useEffect(() => {
|
| 660 |
const isValid = isUiFlowPermitValid(FLOW_KEYS.routeSearch, flowToken);
|
|
|
|
| 840 |
setFormError("์ถ์ฒ ๊ฒฝ๋ก ์๋ 5~100 ์ฌ์ด ์ซ์๋ก ์
๋ ฅํด ์ฃผ์ธ์.");
|
| 841 |
return;
|
| 842 |
}
|
| 843 |
+
if (!shapePriority && (
|
| 844 |
!Number.isFinite(targetDistanceKm) ||
|
| 845 |
targetDistanceKm < 1 ||
|
| 846 |
targetDistanceKm > 30
|
| 847 |
+
)) {
|
| 848 |
setFormError("๋ชฉํ ๊ฑฐ๋ฆฌ๋ 1~30km ์ฌ์ด ์ซ์๋ก ์
๋ ฅํด ์ฃผ์ธ์.");
|
| 849 |
return;
|
| 850 |
}
|
|
|
|
| 855 |
radiusKm: String(radiusKm),
|
| 856 |
routeCount: String(routeCount),
|
| 857 |
targetDistanceKm: String(targetDistanceKm),
|
| 858 |
+
shapePriority: String(shapePriority),
|
| 859 |
label: selectedLabel,
|
| 860 |
selectedTopK: "3",
|
| 861 |
// Supply the visible map centre for a server-side recovery attempt when
|
|
|
|
| 1284 |
</div>
|
| 1285 |
</label>
|
| 1286 |
|
| 1287 |
+
{!shapePriority ? <label className="grid gap-2">
|
| 1288 |
<span className="text-sm font-bold">๋ชฉํ ๊ฑฐ๋ฆฌ</span>
|
| 1289 |
<div className="relative">
|
| 1290 |
<button
|
|
|
|
| 1332 |
})}
|
| 1333 |
</MotionPopover>
|
| 1334 |
</div>
|
| 1335 |
+
</label> : null}
|
| 1336 |
|
| 1337 |
<details className="rounded-2xl bg-[#f7f7f7] p-4">
|
| 1338 |
<summary className="cursor-pointer text-sm font-bold">์ธ๋ถ ์ค์ </summary>
|