{stop.name}
+{stop.address}
+ + + {!isLast ? ( +
{message}
diff --git a/src/components/course-planner/AmPmTimeWheelGroup.tsx b/src/components/course-planner/AmPmTimeWheelGroup.tsx
new file mode 100644
index 0000000..f1133ad
--- /dev/null
+++ b/src/components/course-planner/AmPmTimeWheelGroup.tsx
@@ -0,0 +1,374 @@
+import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
+
+import { isHmString } from "@/components/course-planner/course-date-time";
+import { cn } from "@/lib/utils";
+
+const ITEM_HEIGHT_PX = 44;
+const WHEEL_HEIGHT_PX = 216;
+const EDGE_PADDING_PX = (WHEEL_HEIGHT_PX - ITEM_HEIGHT_PX) / 2;
+
+type Period = "AM" | "PM";
+type Minute = "00" | "30";
+
+type CompleteTriplet = {
+ period: Period;
+ hour12: string;
+ minute: Minute;
+};
+
+const DEFAULT_TRIPLET: CompleteTriplet = {
+ period: "AM",
+ hour12: "12",
+ minute: "00",
+};
+
+const PERIOD_ITEMS: { value: Period; label: string }[] = [
+ { value: "AM", label: "오전" },
+ { value: "PM", label: "오후" },
+];
+
+const HOUR_ORDER = ["12", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11"] as const;
+
+const MINUTE_ITEMS: { value: Minute; label: string }[] = [
+ { value: "00", label: "00" },
+ { value: "30", label: "30" },
+];
+
+function hmToTriplet(hm: string): CompleteTriplet {
+ const [hs, ms] = hm.split(":");
+ const h24 = Number(hs);
+ const minute: Minute = ms === "30" ? "30" : "00";
+ const period: Period = h24 >= 12 ? "PM" : "AM";
+ let hour12 = h24 % 12;
+ if (hour12 === 0) hour12 = 12;
+ return { period, hour12: String(hour12), minute };
+}
+
+function tripletToHm(t: CompleteTriplet): string {
+ const hour12 = Number(t.hour12);
+ let h24 = hour12;
+ if (t.period === "AM") {
+ if (hour12 === 12) h24 = 0;
+ } else if (hour12 !== 12) {
+ h24 = hour12 + 12;
+ }
+
+ const mm = t.minute === "30" ? "30" : "00";
+ return `${String(h24).padStart(2, "0")}:${mm}`;
+}
+
+function parseTripletFromValue(value: string | null): CompleteTriplet {
+ if (!value || !isHmString(value)) return { ...DEFAULT_TRIPLET };
+ return hmToTriplet(value);
+}
+
+function scrollTopForIndex(index: number) {
+ return index * ITEM_HEIGHT_PX;
+}
+
+type WheelColumnProps {description}
+ {label}
+ 맞춤 데이트코스를 생성하고 있어요
+ {stop.address}
+ 마음에 드는 코스를 선택해서 장소 정보를 확인해보세요.
+
+ {formatSelectedDateLine(selectedDate)}
+ 영업시간 정보 없음 영업시간 정보 없음 영업시간 {buildStatusSummary(businessHours)} 영업시간 {buildStatusSummary(businessHours)} {businessHours.holidayNotice}
+ {businessHours.holidayNotice}
+
+
{todayHours.label} {todayHours.hours}
{title}
+
+ {courseTitle}
+
+
+ >
+ )}
+ {stop.name}
+
+ {Array.from({ length: CATEGORY_CHIP_SKELETON_COUNT }, (_, index) => (
+
+ );
+}
+
+export function CoursePlaceTagSelector({
+ categories,
+ categoryNameByCode,
+ filterCategories,
+ isCategoryLoading,
+ isCategoryError,
+ onRetryLoadCategories,
+ activeCategories,
+ focusedCategory,
+ onToggleCategory,
+ isTagPanelOpen,
+ selectedTagKeysByCategory,
+ selectedTagCountByCategory,
+ onToggleTagInCategory,
+}: MapFilterBarProps) {
+ const highlightCtx = { activeCategories, focusedCategory };
+ const focusedSection =
+ isTagPanelOpen && focusedCategory
+ ? (filterCategories.find((category) => category.code === focusedCategory) ?? null)
+ : null;
+ const selectedKeys = focusedSection ? (selectedTagKeysByCategory[focusedSection.code] ?? []) : [];
+ const focusedTags =
+ focusedSection?.tagGroups.flatMap((group) => group.tags).filter((tag) => tag.name.trim()) ?? [];
+
+ return (
+
+ {categories.map((category) => (
+
+ )}
+
+ {isCategoryError ? (
+
+ 맞춤 데이트코스 설정하기
+
+
+
+ 맞춤 데이트코스 확인하기
+
+ {headerTitle}
+
+
+ 지역설정
+
+