tkadkghdlf commited on
Commit
dc53196
ยท
verified ยท
1 Parent(s): a23c4a8

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
- candidates = build_candidates(
429
- graph=graph,
430
- route_count=candidate_pool,
431
- target_distance_m=target_distance_km * 1000.0,
432
- selected_label=selected_label,
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="grid gap-3 sm:grid-cols-[1fr_120px_120px] sm:items-end">
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 selectedSummary = `${summaryPlace} ยท ${selectedShape.label} ยท ${targetDistanceInput || "-"}km`;
654
- const routePlanText = `${summaryPlace}์—์„œ ${selectedShape.label} ๋ชจ์–‘์— ๊ฐ€๊นŒ์šด ${targetDistanceInput || "-"}km ๊ฒฝ๋กœ๋ฅผ ${routeCountInput || "-"}๊ฐœ ์ถ”์ฒœํ•ฉ๋‹ˆ๋‹ค.`;
 
 
 
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>