-
- {label}
- 맞춤 데이트 코스를 생성하고 있어요
+
+
+
+
+
+
+ {label}의 장소들로
+ 데이트 코스를 생성하고 있어요
-
);
diff --git a/src/components/course-planner/CoursePlaceAddSheet.tsx b/src/components/course-planner/CoursePlaceAddSheet.tsx
index c2d3336..90def9e 100644
--- a/src/components/course-planner/CoursePlaceAddSheet.tsx
+++ b/src/components/course-planner/CoursePlaceAddSheet.tsx
@@ -1,68 +1,96 @@
import { useMemo, useState } from "react";
import { createPortal } from "react-dom";
-import { EditPlaceResultCard } from "@/components/link-place/EditPlaceResultCard";
-import { PlaceFlowSearchEmptyRow } from "@/components/place-flow/PlaceFlowSearchEmptyRow";
-import { PlaceFlowSearchFieldRow } from "@/components/place-flow/PlaceFlowSearchFieldRow";
-import { BottomSheet } from "@/components/ui/BottomSheet";
-import { PillButton } from "@/components/ui/PillButton";
-import { PLACE_FLOW_COPY } from "@/features/place-flow/place-flow-copy";
-import { PROMPT_FLOW_LIST_TOP_BORDER_CLASS } from "@/features/place-flow/prompt-flow-layout";
-import { SAVED_PLACE_MOCKS } from "@/shared/mocks/place-mocks";
+import { PlaceSearchMapSheet } from "@/components/place-flow/PlaceSearchMapSheet";
+import type { PlaceCandidate } from "@/features/place-candidates";
+import {
+ canSubmitPlaceCandidate,
+ placeCandidateToSavedPlace,
+ usePlaceCandidates,
+} from "@/features/place-candidates";
+import { cn } from "@/lib/utils";
import type { SavedPlace } from "@/shared/types/map-home";
+import {
+ FULLSCREEN_FLOW_PANEL_CLASSES,
+ FULLSCREEN_FLOW_ROUTE_OUTER_CLASSES,
+} from "@/shared/ui/fullscreen-flow-layout";
type CoursePlaceAddSheetProps = {
open: boolean;
+ roomId?: string | null;
excludedPlaceIds: string[];
onClose: () => void;
onConfirm: (place: SavedPlace) => void;
};
-function findPlaceMatches(keyword: string): SavedPlace[] {
- const trimmedKeyword = keyword.trim();
- if (!trimmedKeyword) {
- return [];
- }
-
- return SAVED_PLACE_MOCKS.filter(
- (place) => place.name.includes(trimmedKeyword) || place.address.includes(trimmedKeyword),
- );
-}
+const EMPTY_PLACE_CANDIDATES: PlaceCandidate[] = [];
export function CoursePlaceAddSheet({
open,
+ roomId = null,
excludedPlaceIds,
onClose,
onConfirm,
}: CoursePlaceAddSheetProps) {
const [keyword, setKeyword] = useState("");
+ const [submittedKeyword, setSubmittedKeyword] = useState("");
const [selectedPlaceId, setSelectedPlaceId] = useState(null);
const excludedPlaceIdSet = useMemo(() => new Set(excludedPlaceIds), [excludedPlaceIds]);
-
- const searchResults = useMemo(() => findPlaceMatches(keyword), [keyword]);
- const availableResults = useMemo(
- () => searchResults.filter((place) => !excludedPlaceIdSet.has(place.id)),
- [excludedPlaceIdSet, searchResults],
- );
- const selectedPlace = availableResults.find((place) => place.id === selectedPlaceId) ?? null;
const trimmedKeyword = keyword.trim();
- const canSearch = trimmedKeyword.length > 0;
- const canConfirm = selectedPlace != null;
+ const submittedTrimmedKeyword = submittedKeyword.trim();
+ const isSubmittedKeywordCurrent = submittedTrimmedKeyword === trimmedKeyword;
+
+ const placeCandidatesQuery = usePlaceCandidates({
+ roomId: roomId ?? null,
+ params: {
+ keyword: submittedTrimmedKeyword,
+ limit: 10,
+ },
+ enabled: open && Boolean(roomId) && submittedTrimmedKeyword.length > 0,
+ });
+ const placeCandidates = placeCandidatesQuery.data ?? EMPTY_PLACE_CANDIDATES;
+
+ const availableResults = useMemo(() => {
+ if (!isSubmittedKeywordCurrent || submittedTrimmedKeyword.length === 0) {
+ return [];
+ }
+
+ return placeCandidates
+ .map((place, index) => ({
+ candidate: place,
+ savedPlace: placeCandidateToSavedPlace(place, index),
+ }))
+ .filter(({ candidate, savedPlace }) => {
+ if (!canSubmitPlaceCandidate(candidate)) {
+ return false;
+ }
+
+ return (
+ !excludedPlaceIdSet.has(savedPlace.id) &&
+ !(savedPlace.kakaoPlaceId && excludedPlaceIdSet.has(savedPlace.kakaoPlaceId))
+ );
+ });
+ }, [excludedPlaceIdSet, isSubmittedKeywordCurrent, placeCandidates, submittedTrimmedKeyword]);
+
+ const selectedPlace =
+ availableResults.find(({ savedPlace }) => savedPlace.id === selectedPlaceId)?.savedPlace ??
+ null;
+ const searchResults = availableResults.map(({ savedPlace }) => savedPlace);
const resetAndClose = () => {
setKeyword("");
+ setSubmittedKeyword("");
setSelectedPlaceId(null);
onClose();
};
const handleSubmitSearch = () => {
- if (!canSearch) {
+ if (trimmedKeyword.length === 0) {
return;
}
- if (availableResults.length === 1) {
- setSelectedPlaceId(availableResults[0].id);
- }
+ setSubmittedKeyword(trimmedKeyword);
+ setSelectedPlaceId(null);
};
const handleConfirm = () => {
@@ -72,86 +100,40 @@ export function CoursePlaceAddSheet({
onConfirm(selectedPlace);
setKeyword("");
+ setSubmittedKeyword("");
setSelectedPlaceId(null);
};
- const hasOnlyDuplicateMatches =
- trimmedKeyword.length > 0 && searchResults.length > 0 && availableResults.length === 0;
+ if (!open) {
+ return null;
+ }
return createPortal(
-
-
-
-
- 장소 추가하기
-
-
코스에 추가할 장소를 검색해 주세요
-
-
-
-
{
- setKeyword(next);
- setSelectedPlaceId(null);
- }}
- placeholder={PLACE_FLOW_COPY.searchPlaceholder}
- searchButtonLabel={PLACE_FLOW_COPY.searchButton}
- onSubmitSearch={handleSubmitSearch}
- searchButtonDisabled={!canSearch}
- />
-
-
-
- {trimmedKeyword ? (
-
- {availableResults.length === 0 ? (
-
- ) : (
- availableResults.map((place) => (
- setSelectedPlaceId(place.id)}
- />
- ))
- )}
-
- ) : null}
-
-
-
+
+
+ 0 && isSubmittedKeywordCurrent}
+ canConfirm={selectedPlace != null}
+ onKeywordChange={(next) => {
+ setKeyword(next);
+ setSelectedPlaceId(null);
+ }}
+ onSubmitSearch={handleSubmitSearch}
+ onSelectPlace={setSelectedPlaceId}
+ onClearSelectedPlace={() => setSelectedPlaceId(null)}
+ onCancel={resetAndClose}
+ onConfirm={handleConfirm}
+ />
- ,
+
,
document.body,
);
}
diff --git a/src/components/course-planner/CoursePlaceInfoPanel.tsx b/src/components/course-planner/CoursePlaceInfoPanel.tsx
index b1136fd..bd2c956 100644
--- a/src/components/course-planner/CoursePlaceInfoPanel.tsx
+++ b/src/components/course-planner/CoursePlaceInfoPanel.tsx
@@ -34,18 +34,22 @@ type SaveConfirmKind = "create" | "edit";
type CoursePlaceInfoPanelProps = {
courseTitle: string;
stops: CourseStop[];
+ roomId?: string | null;
onBack: () => void;
onSave: (payload: CourseSavePayload) => void;
/** true면 조회 모드에서 「데이트 코스 저장하기」 버튼을 숨김 — 이미 저장된 코스(마이 페이지 등) */
hideNewCourseSaveButton?: boolean;
+ className?: string;
};
export function CoursePlaceInfoPanel({
courseTitle,
stops,
+ roomId = null,
onBack,
onSave,
hideNewCourseSaveButton = false,
+ className,
}: CoursePlaceInfoPanelProps) {
const openDetail = usePlaceDetailStore((s) => s.openDetail);
@@ -135,6 +139,7 @@ export function CoursePlaceInfoPanel({
hideNewCourseSaveButton && !isEditing
? "pb-[max(1.25rem,calc(env(safe-area-inset-bottom)+1rem))]"
: "pb-0",
+ className,
)}
>
{isEditing ? (
@@ -309,6 +314,7 @@ export function CoursePlaceInfoPanel({
stop.placeId)}
onClose={() => setIsAddPlaceOpen(false)}
onConfirm={handleAddPlace}
diff --git a/src/components/course-planner/CoursePlannerActions.tsx b/src/components/course-planner/CoursePlannerActions.tsx
index 2b5a78b..d1fbfd0 100644
--- a/src/components/course-planner/CoursePlannerActions.tsx
+++ b/src/components/course-planner/CoursePlannerActions.tsx
@@ -6,15 +6,17 @@ type CoursePlannerActionsProps = {
canGenerate: boolean;
onGenerate: () => void;
onReset: () => void;
+ className?: string;
};
export function CoursePlannerActions({
canGenerate,
onGenerate,
onReset,
+ className,
}: CoursePlannerActionsProps) {
return (
-
+