+
{suggestions.length > 0 ? (
);
diff --git a/src/components/map/chip-style.ts b/src/components/map/chip-style.ts
index f9f15c3..ad45fdb 100644
--- a/src/components/map/chip-style.ts
+++ b/src/components/map/chip-style.ts
@@ -3,12 +3,12 @@ export const MAP_CHIP_BASE_CLASS =
export const MAP_CHIP_SELECTED_CLASS = "border-primary bg-primary text-primary-foreground";
-export const MAP_CHIP_UNSELECTED_CLASS =
- "bg-background/92 text-muted-foreground/85 hover:bg-background border-border/80 backdrop-blur-sm";
+/** 지도 검색 입력·카테고리 칩(비선택·패널 포커스) 공통 글래스 — 배경 불투명도 85% 통일 */
+export const MAP_OVERLAY_GLASS_SURFACE_CLASS = "border-border/35 bg-background/85 backdrop-blur-md";
-/** 태그 패널에서 이 카테고리를 보고 있을 때 — 회색 톤(active 코랄 실색과 구분) */
-export const MAP_CHIP_PANEL_FOCUS_CLASS =
- "border-border/90 bg-muted/85 text-foreground/88 backdrop-blur-sm hover:bg-muted";
+export const MAP_CHIP_UNSELECTED_CLASS = `${MAP_OVERLAY_GLASS_SURFACE_CLASS} text-muted-foreground/85 hover:bg-background/90`;
+
+export const MAP_CHIP_PANEL_FOCUS_CLASS = `${MAP_OVERLAY_GLASS_SURFACE_CLASS} text-foreground/85 ring-1 ring-inset ring-foreground/12 hover:bg-background/90 dark:ring-white/15`;
/** active 칩이면서 태그 패널 포커스일 때 — 코랄 위 대비용 얕은 링 */
export const MAP_CHIP_PANEL_FOCUS_ON_ACTIVE_CLASS = "ring-2 ring-inset ring-primary-foreground/45";
@@ -26,17 +26,14 @@ export const MAP_CHIP_BADGE_SELECTED_MOBILE_CLASS =
export const MAP_CHIP_BADGE_UNSELECTED_MOBILE_CLASS =
"max-md:absolute max-md:-top-1 max-md:-right-0.5 max-md:z-1 max-md:flex max-md:h-4 max-md:min-w-4 max-md:items-center max-md:justify-center max-md:rounded-full max-md:px-1 max-md:tabular-nums max-md:font-semibold max-md:shadow-sm max-md:border max-md:border-primary/30 max-md:bg-primary max-md:text-primary-foreground";
-/** 검색창·검색 제안 패널 공통 글래스 (입력창은 아래 클래스에서 border-width는 기본 input과 함께 사용) */
-export const MAP_SEARCH_GLASS_SURFACE_CLASS = "border-border/55 bg-background/85 backdrop-blur-md";
+/** 검색 입력(`SearchField`) — `MAP_OVERLAY_GLASS_SURFACE_CLASS` 별칭 */
+export const MAP_SEARCH_GLASS_SURFACE_CLASS = MAP_OVERLAY_GLASS_SURFACE_CLASS;
/** 검색창 한 줄만 — 넓은 글래스 패널 없이 얇게 */
export const MAP_SEARCH_INPUT_GLASS_CLASS = `${MAP_SEARCH_GLASS_SURFACE_CLASS} placeholder:text-muted-foreground/90`;
-/** 검색 제안 드롭다운 패널 (div 래퍼용 — `border` 명시) */
-export const MAP_SEARCH_SUGGESTIONS_PANEL_CLASS = `border ${MAP_SEARCH_GLASS_SURFACE_CLASS}`;
-
-export const MAP_FILTER_PANEL_BASE_CLASS =
- "mt-2 overflow-hidden rounded-2xl border border-border/35 bg-background/88 shadow-filter-panel backdrop-blur-md transition-all duration-200 ease-out";
+/** 태그 필터 패널·검색 제안 드롭다운 공통 래퍼 */
+export const MAP_FILTER_PANEL_BASE_CLASS = `mt-2 overflow-hidden rounded-2xl border ${MAP_OVERLAY_GLASS_SURFACE_CLASS} shadow-filter-panel transition-all duration-200 ease-out`;
export const MAP_FILTER_PANEL_HEADER_ACTION_CLASS =
"text-xs font-medium text-muted-foreground/90 hover:text-muted-foreground";
diff --git a/src/components/mypage/SavedPlaceItem.tsx b/src/components/mypage/SavedPlaceItem.tsx
index 5bcc872..c121c5c 100644
--- a/src/components/mypage/SavedPlaceItem.tsx
+++ b/src/components/mypage/SavedPlaceItem.tsx
@@ -1,4 +1,4 @@
-import { MoreVertical } from "lucide-react";
+import { MapPin, MoreVertical } from "lucide-react";
import { useCallback, useRef } from "react";
import { usePointerDownOutside } from "@/hooks/use-pointer-down-outside";
@@ -47,7 +47,10 @@ export function SavedPlaceItem({
{!readOnly ? (
diff --git a/src/components/reels/EditPlaceResultCard.tsx b/src/components/reels/EditPlaceResultCard.tsx
new file mode 100644
index 0000000..983d948
--- /dev/null
+++ b/src/components/reels/EditPlaceResultCard.tsx
@@ -0,0 +1,34 @@
+import { RoundSelectionCheck } from "@/components/ui/RoundSelectionCheck";
+import type { SavedPlace } from "@/shared/types/map-home";
+
+type EditPlaceResultCardProps = {
+ place: SavedPlace;
+ selected: boolean;
+ onSelect: () => void;
+};
+
+export function EditPlaceResultCard({ place, selected, onSelect }: EditPlaceResultCardProps) {
+ return (
+
+
+
+ );
+}
diff --git a/src/components/reels/PlaceSelectCard.tsx b/src/components/reels/PlaceSelectCard.tsx
new file mode 100644
index 0000000..20af514
--- /dev/null
+++ b/src/components/reels/PlaceSelectCard.tsx
@@ -0,0 +1,81 @@
+import { MapPin, Pencil } from "lucide-react";
+
+import { RoundSelectionCheck } from "@/components/ui/RoundSelectionCheck";
+import { cn } from "@/lib/utils";
+import type { SavedPlace } from "@/shared/types/map-home";
+
+type PlaceSelectCardProps = {
+ place: SavedPlace;
+ selected: boolean;
+ disabled: boolean;
+ onSelect: () => void;
+ onEdit: () => void;
+};
+
+export function PlaceSelectCard({
+ place,
+ selected,
+ disabled,
+ onSelect,
+ onEdit,
+}: PlaceSelectCardProps) {
+ const handleSelect = () => {
+ if (!disabled) {
+ onSelect();
+ }
+ };
+
+ return (
+
+
+
+
+
{place.name}
+
+
+
+
+ {place.address}
+
+
+
+ {disabled ? (
+
+ 이미 저장된 장소입니다
+
+ ) : (
+
+ )}
+
+
+ );
+}
diff --git a/src/components/room/FriendRoomItemView.tsx b/src/components/room/FriendRoomItemView.tsx
index 642e65f..535cafb 100644
--- a/src/components/room/FriendRoomItemView.tsx
+++ b/src/components/room/FriendRoomItemView.tsx
@@ -29,6 +29,7 @@ export const FriendRoomItemView = memo(function FriendRoomItemView({
onKeyDown,
}: FriendRoomItemViewProps) {
const pinned = Boolean(row.isPinned);
+ const placeCountText = row.placeCount >= 1000 ? "1k+개 장소" : `${row.placeCount}개 장소`;
return (
- {row.placeCount}개 장소
+ {placeCountText}
diff --git a/src/components/room/RoomConfirmModal.tsx b/src/components/room/RoomConfirmModal.tsx
new file mode 100644
index 0000000..846e7aa
--- /dev/null
+++ b/src/components/room/RoomConfirmModal.tsx
@@ -0,0 +1,85 @@
+import { PillButton } from "@/components/ui/PillButton";
+import { cn } from "@/lib/utils";
+
+import { RoomModalShell } from "./RoomModalShell";
+
+type RoomConfirmModalProps = {
+ open: boolean;
+ message: string;
+ cancelLabel?: string;
+ confirmLabel: string;
+ onCancel?: () => void;
+ onConfirm: () => void;
+};
+
+export function RoomConfirmModal({
+ open,
+ message,
+ cancelLabel,
+ confirmLabel,
+ onCancel,
+ onConfirm,
+}: RoomConfirmModalProps) {
+ if (!open) {
+ return null;
+ }
+
+ const isSingleAction = !cancelLabel;
+
+ return (
+
{
+ if (!isSingleAction) {
+ onCancel?.();
+ }
+ }}
+ className="z-60"
+ >
+ {isSingleAction ? (
+
+
+ {message}
+
+
+ {confirmLabel}
+
+
+ ) : (
+ <>
+
+
+
+
+
+
+ >
+ )}
+
+ );
+}
diff --git a/src/components/room/link-add/PlaceSelectionScreen.tsx b/src/components/room/link-add/PlaceSelectionScreen.tsx
index 52eb6d3..a820fdc 100644
--- a/src/components/room/link-add/PlaceSelectionScreen.tsx
+++ b/src/components/room/link-add/PlaceSelectionScreen.tsx
@@ -1,4 +1,5 @@
import { PillButton } from "@/components/ui/PillButton";
+import { RoundSelectionCheck } from "@/components/ui/RoundSelectionCheck";
import type { MockPlaceCandidate } from "@/features/room/link-add";
import { cn } from "@/lib/utils";
@@ -52,12 +53,7 @@ export function PlaceSelectionScreen({
onClick={() => onSelectPlace(place.id)}
>
{place.name}
-
+
);
diff --git a/src/components/ui/RoundSelectionCheck.tsx b/src/components/ui/RoundSelectionCheck.tsx
new file mode 100644
index 0000000..f9c6e74
--- /dev/null
+++ b/src/components/ui/RoundSelectionCheck.tsx
@@ -0,0 +1,23 @@
+import { Check } from "lucide-react";
+
+import { cn } from "@/lib/utils";
+
+export type RoundSelectionCheckProps = {
+ selected: boolean;
+ className?: string;
+};
+
+export function RoundSelectionCheck({ selected, className }: RoundSelectionCheckProps) {
+ return (
+
+ {selected ? : null}
+
+ );
+}
diff --git a/src/features/onboarding/components/AgreementItem.tsx b/src/features/onboarding/components/AgreementItem.tsx
index fce2a3b..c229377 100644
--- a/src/features/onboarding/components/AgreementItem.tsx
+++ b/src/features/onboarding/components/AgreementItem.tsx
@@ -1,5 +1,4 @@
-import { Check } from "lucide-react";
-
+import { RoundSelectionCheck } from "@/components/ui/RoundSelectionCheck";
import { cn } from "@/lib/utils";
type AgreementItemProps = {
@@ -39,15 +38,7 @@ export function AgreementItem({
onClick={onToggle}
className="text-foreground active:bg-tap-highlight flex w-full cursor-pointer items-start gap-3 rounded-xl py-2.5 text-left text-[1rem] leading-snug"
>
-
- {checked ? : null}
-
+
{label}
{showBadge ? (
diff --git a/src/pages/EditPlacePage.tsx b/src/pages/EditPlacePage.tsx
new file mode 100644
index 0000000..0247e53
--- /dev/null
+++ b/src/pages/EditPlacePage.tsx
@@ -0,0 +1,168 @@
+import { useCallback, useEffect, useMemo, useState } from "react";
+import { useLocation, useNavigate } from "react-router-dom";
+
+import { SearchField } from "@/components/common/SearchField";
+import { EditPlaceResultCard } from "@/components/reels/EditPlaceResultCard";
+import { PillButton } from "@/components/ui/PillButton";
+import { SAVED_PLACE_MOCKS } from "@/shared/mocks/place-mocks";
+import { useEditPlaceStore } from "@/store/editPlaceStore";
+
+const REELS_LINK_MOCK = "https://www.instagram.com/reel/DNp9tqSz6rT/?igsh=MW4yOGd6aGNzMmRsYw==";
+
+type EditPlaceLocationState = {
+ placeId?: string;
+ placeName?: string;
+};
+
+export default function EditPlacePage() {
+ const navigate = useNavigate();
+ const location = useLocation();
+ const routeState = (location.state ?? {}) as EditPlaceLocationState;
+ const editingPlaceId = useEditPlaceStore((state) => state.editingPlaceId);
+ const searchKeyword = useEditPlaceStore((state) => state.searchKeyword);
+ const selectedResultId = useEditPlaceStore((state) => state.selectedResultId);
+ const setEditingPlace = useEditPlaceStore((state) => state.setEditingPlace);
+ const setKeyword = useEditPlaceStore((state) => state.setKeyword);
+ const setSelectedResult = useEditPlaceStore((state) => state.setSelectedResult);
+ const reset = useEditPlaceStore((state) => state.reset);
+ const [copyLabel, setCopyLabel] = useState("복사");
+
+ useEffect(() => {
+ const nextPlaceId = routeState.placeId ?? editingPlaceId;
+ const nextKeyword = routeState.placeName ?? searchKeyword;
+
+ setEditingPlace(nextPlaceId ?? null);
+ setKeyword(nextKeyword);
+ setSelectedResult(null);
+ // This page is entered with router state, so initialization should only run on entry.
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ const trimmedKeyword = searchKeyword.trim();
+ const canSearch = trimmedKeyword.length > 0;
+ const canConfirm = selectedResultId != null;
+
+ const searchResults = useMemo(() => {
+ if (!trimmedKeyword) {
+ return [];
+ }
+
+ return SAVED_PLACE_MOCKS.filter(
+ (place) => place.name.includes(trimmedKeyword) || place.address.includes(trimmedKeyword),
+ );
+ }, [trimmedKeyword]);
+
+ const handleCopy = useCallback(async () => {
+ try {
+ await navigator.clipboard.writeText(REELS_LINK_MOCK);
+ setCopyLabel("복사됨");
+ window.setTimeout(() => setCopyLabel("복사"), 1500);
+ } catch {
+ setCopyLabel("실패");
+ window.setTimeout(() => setCopyLabel("복사"), 1500);
+ }
+ }, []);
+
+ const handleBack = () => {
+ reset();
+ navigate(-1);
+ };
+
+ const handleConfirm = () => {
+ if (!selectedResultId) {
+ return;
+ }
+
+ navigate("/dev/register_place");
+ };
+
+ return (
+
+
+
+
+
+ 장소 위치 정보를 변경하시겠습니까?
+
+
+ 해당 장소를 직접 입력해주세요
+
+
+
+
+
{REELS_LINK_MOCK}
+
+
+
+
+
+ {trimmedKeyword ? (
+
+ {searchResults.length === 0 ? (
+ -
+ 검색 결과가 없습니다
+
+ ) : (
+ searchResults.map((place) => (
+ setSelectedResult(place.id)}
+ />
+ ))
+ )}
+
+ ) : null}
+
+
+
+
+
+ );
+}
diff --git a/src/pages/ReelsPlaceSelectPage.tsx b/src/pages/ReelsPlaceSelectPage.tsx
new file mode 100644
index 0000000..c1e45da
--- /dev/null
+++ b/src/pages/ReelsPlaceSelectPage.tsx
@@ -0,0 +1,146 @@
+import { useCallback, useMemo, useState } from "react";
+import { useNavigate } from "react-router-dom";
+
+import { PlaceSelectCard } from "@/components/reels/PlaceSelectCard";
+import { PillButton } from "@/components/ui/PillButton";
+import { SAVED_PLACE_MOCKS } from "@/shared/mocks/place-mocks";
+import { useEditPlaceStore } from "@/store/editPlaceStore";
+import { useReelsPlaceSelectStore } from "@/store/reelsPlaceSelectStore";
+import { useRegisterRoomStore } from "@/store/registerRoomStore";
+
+const REELS_LINK_MOCK = "https://www.instagram.com/reel/DNp9tqSz6rT/?igsh=MW4yOGd6aGNzMmRsYw==";
+const PLACE_RENDER_ORDER = [
+ "restaurant-1",
+ "restaurant-2",
+ "restaurant-3",
+ "restaurant-4",
+ "restaurant-5",
+ "cafe-1",
+];
+const SAVED_PLACE_ID = "restaurant-3";
+
+export default function ReelsPlaceSelectPage() {
+ const navigate = useNavigate();
+ const selectedPlaceIds = useReelsPlaceSelectStore((state) => state.selectedPlaceIds);
+ const togglePlace = useReelsPlaceSelectStore((state) => state.togglePlace);
+ const clearSelection = useReelsPlaceSelectStore((state) => state.clearSelection);
+ const editingPlaceId = useEditPlaceStore((state) => state.editingPlaceId);
+ const selectedResultId = useEditPlaceStore((state) => state.selectedResultId);
+ const setSelectedPlacesForRegister = useRegisterRoomStore((state) => state.setSelectedPlaces);
+ const [copyLabel, setCopyLabel] = useState("복사");
+
+ const placeRows = useMemo(
+ () =>
+ PLACE_RENDER_ORDER.map((placeId) => {
+ const originalPlace = SAVED_PLACE_MOCKS.find((place) => place.id === placeId);
+ const editedPlace =
+ editingPlaceId === placeId && selectedResultId
+ ? SAVED_PLACE_MOCKS.find((place) => place.id === selectedResultId)
+ : null;
+
+ const place = editedPlace ?? originalPlace;
+
+ return place ? { slotId: placeId, place } : null;
+ }).filter((row) => row != null),
+ [editingPlaceId, selectedResultId],
+ );
+ const canConfirm = selectedPlaceIds.length > 0;
+
+ const handleCopy = useCallback(async () => {
+ try {
+ await navigator.clipboard.writeText(REELS_LINK_MOCK);
+ setCopyLabel("복사됨");
+ window.setTimeout(() => setCopyLabel("복사"), 1500);
+ } catch {
+ setCopyLabel("실패");
+ window.setTimeout(() => setCopyLabel("복사"), 1500);
+ }
+ }, []);
+
+ return (
+
+
+
+
+
+ 장소가 인식되었습니다.
+
+
+ 어느 장소를 등록하시겠습니까?
+
+
+
+
+
{REELS_LINK_MOCK}
+
+
+
+
+
+
+
+ {placeRows.map(({ slotId, place }) => {
+ const disabled = slotId === SAVED_PLACE_ID;
+ return (
+ togglePlace(slotId)}
+ onEdit={() =>
+ navigate("/edit_place", {
+ state: {
+ placeId: slotId,
+ placeName: place.name,
+ },
+ })
+ }
+ />
+ );
+ })}
+
+
+
+
+
+
+ 취소
+
+
{
+ setSelectedPlacesForRegister(selectedPlaceIds);
+ navigate("/register-select-room", {
+ state: {
+ selectedPlaceIds,
+ selectedPlaceCount: selectedPlaceIds.length,
+ },
+ });
+ }}
+ >
+ 확인
+
+
+
+
+ );
+}
diff --git a/src/pages/RegisterSelectRoomPage.tsx b/src/pages/RegisterSelectRoomPage.tsx
new file mode 100644
index 0000000..ac5968f
--- /dev/null
+++ b/src/pages/RegisterSelectRoomPage.tsx
@@ -0,0 +1,87 @@
+import { useEffect, useMemo } from "react";
+import { useLocation, useNavigate } from "react-router-dom";
+
+import { BottomNavigationBar } from "@/components/common/BottomNavigationBar";
+import { FloatingActionButton } from "@/components/common/FloatingActionButton";
+import { FriendRoomList } from "@/components/room/FriendRoomList";
+import { RoomConfirmModal } from "@/components/room/RoomConfirmModal";
+import { RoomMainHeader } from "@/components/room/RoomMainHeader";
+import { RoomMainShell } from "@/components/room/RoomMainShell";
+import { FRIEND_ROOM_MOCK_ROWS } from "@/pages/room/friend-room-mock";
+import type { FriendRoomRow } from "@/shared/types/room";
+import { useRegisterRoomStore } from "@/store/registerRoomStore";
+
+type RegisterSelectRoomLocationState = {
+ selectedPlaceIds?: string[];
+ selectedPlaceCount?: number;
+};
+
+export default function RegisterSelectRoomPage() {
+ const navigate = useNavigate();
+ const location = useLocation();
+ const routeState = (location.state ?? {}) as RegisterSelectRoomLocationState;
+ const selectedRoomId = useRegisterRoomStore((state) => state.selectedRoomId);
+ const selectedPlaceCount = useRegisterRoomStore((state) => state.selectedPlaceCount);
+ const confirmModalOpen = useRegisterRoomStore((state) => state.confirmModalOpen);
+ const roomPlaceCountDeltas = useRegisterRoomStore((state) => state.roomPlaceCountDeltas);
+ const setSelectedPlaces = useRegisterRoomStore((state) => state.setSelectedPlaces);
+ const setSelectedRoom = useRegisterRoomStore((state) => state.setSelectedRoom);
+ const openConfirm = useRegisterRoomStore((state) => state.openConfirm);
+ const closeConfirm = useRegisterRoomStore((state) => state.closeConfirm);
+ const completeRegister = useRegisterRoomStore((state) => state.completeRegister);
+
+ useEffect(() => {
+ if (routeState.selectedPlaceIds && selectedPlaceCount === 0) {
+ setSelectedPlaces(routeState.selectedPlaceIds);
+ }
+ // Router state is only used as an entry payload.
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ const rows = useMemo(
+ () =>
+ FRIEND_ROOM_MOCK_ROWS.map((row) => ({
+ ...row,
+ placeCount: row.placeCount + (roomPlaceCountDeltas[row.id] ?? 0),
+ })),
+ [roomPlaceCountDeltas],
+ );
+ const selectedRoom = useMemo(
+ () => rows.find((row) => row.id === selectedRoomId) ?? null,
+ [rows, selectedRoomId],
+ );
+
+ const handleSelectRoom = (row: FriendRoomRow) => {
+ setSelectedRoom(row.id);
+ openConfirm();
+ };
+
+ return (
+
+ }
+ fab={}
+ bottomNav={ undefined} />}
+ >
+
+
+ {
+ if (completeRegister()) {
+ navigate("/room", { state: { showPlacesRegisteredToast: true } });
+ }
+ }}
+ />
+
+ );
+}
diff --git a/src/pages/room/RoomMainPage.tsx b/src/pages/room/RoomMainPage.tsx
index 712aa21..46caa1f 100644
--- a/src/pages/room/RoomMainPage.tsx
+++ b/src/pages/room/RoomMainPage.tsx
@@ -1,5 +1,5 @@
-import { lazy, Suspense, useCallback, useState } from "react";
-import { useNavigate } from "react-router-dom";
+import { lazy, Suspense, useCallback, useEffect, useState } from "react";
+import { useLocation, useNavigate } from "react-router-dom";
import { BottomNavigationBar } from "@/components/common/BottomNavigationBar";
import { BottomNavToast } from "@/components/common/BottomNavToast";
@@ -11,8 +11,10 @@ import { useRoomActionModalHistory, useRoomMainModals } from "@/features/room";
import type { RoomActionType } from "@/features/room/roomActionTypes";
import { useBottomNavController } from "@/hooks/use-bottom-nav-controller";
import { APP_ROUTES } from "@/shared/config/routes";
+import { REGISTER_SELECT_ROOM_TEXT } from "@/shared/config/text";
import type { FriendRoomRow } from "@/shared/types/room";
import { useAuthStore } from "@/store/auth-store";
+import { useRegisterRoomStore } from "@/store/registerRoomStore";
import { useRoomSelectionStore } from "@/store/room-selection-store";
const RoomActionModal = lazy(() =>
@@ -42,9 +44,15 @@ const EditRoomNameModal = lazy(() =>
})),
);
+type RoomMainLocationState = {
+ showPlacesRegisteredToast?: boolean;
+};
+
export default function RoomMainPage() {
const navigate = useNavigate();
+ const location = useLocation();
const selectRoom = useRoomSelectionStore((s) => s.selectRoom);
+ const roomPlaceCountDeltas = useRegisterRoomStore((state) => state.roomPlaceCountDeltas);
const nickname = useAuthStore((s) => s.nickname);
const roomMainHeaderTitle =
nickname != null && nickname.trim().length > 0
@@ -79,6 +87,21 @@ export default function RoomMainPage() {
const [isLinkAddModalLoaded, setIsLinkAddModalLoaded] = useState(linkAddRoom != null);
const [isRoomAddModalLoaded, setIsRoomAddModalLoaded] = useState(isAddRoomOpen);
+ useEffect(() => {
+ const state = (location.state ?? null) as RoomMainLocationState | null;
+ if (!state?.showPlacesRegisteredToast) {
+ return;
+ }
+
+ showToast(REGISTER_SELECT_ROOM_TEXT.placesRegisteredToast, 2000);
+ navigate(location.pathname, { replace: true, state: {} });
+ }, [location.pathname, location.state, navigate, showToast]);
+
+ const displayRows = sortedRows.map((row) => ({
+ ...row,
+ placeCount: row.placeCount + (roomPlaceCountDeltas[row.id] ?? 0),
+ }));
+
const handleRoomNavigate = useCallback(
(row: FriendRoomRow) => {
selectRoom({ id: row.id, name: row.displayName, memberCount: row.memberCount });
@@ -125,7 +148,7 @@ export default function RoomMainPage() {
bottomNav={}
>
diff --git a/src/pages/room/friend-room-mock.ts b/src/pages/room/friend-room-mock.ts
index ba4fd45..0060afc 100644
--- a/src/pages/room/friend-room-mock.ts
+++ b/src/pages/room/friend-room-mock.ts
@@ -5,7 +5,7 @@ export type { FriendRoomRow } from "@/shared/types/room";
export const FRIEND_ROOM_MOCK_ROWS: FriendRoomRow[] = [
{
id: "1",
- displayName: "내꺼♥️",
+ displayName: "내꺼♥",
memberCount: 2,
placeCount: 5,
isPinned: true,
diff --git a/src/shared/config/text.ts b/src/shared/config/text.ts
index 14c02a9..51796a6 100644
--- a/src/shared/config/text.ts
+++ b/src/shared/config/text.ts
@@ -6,3 +6,7 @@ export const PLACE_LIST_TEXT = {
export const BOTTOM_NAV_TEXT = {
roomRequiredToast: "방을 먼저 선택해주세요.",
} as const;
+
+export const REGISTER_SELECT_ROOM_TEXT = {
+ placesRegisteredToast: "등록되었습니다",
+} as const;
diff --git a/src/store/editPlaceStore.ts b/src/store/editPlaceStore.ts
new file mode 100644
index 0000000..d7ae683
--- /dev/null
+++ b/src/store/editPlaceStore.ts
@@ -0,0 +1,26 @@
+import { create } from "zustand";
+
+type EditPlaceState = {
+ editingPlaceId: string | null;
+ searchKeyword: string;
+ selectedResultId: string | null;
+ setEditingPlace: (placeId: string | null) => void;
+ setKeyword: (keyword: string) => void;
+ setSelectedResult: (resultId: string | null) => void;
+ reset: () => void;
+};
+
+export const useEditPlaceStore = create((set) => ({
+ editingPlaceId: null,
+ searchKeyword: "",
+ selectedResultId: null,
+ setEditingPlace: (placeId) => set({ editingPlaceId: placeId }),
+ setKeyword: (keyword) => set({ searchKeyword: keyword }),
+ setSelectedResult: (resultId) => set({ selectedResultId: resultId }),
+ reset: () =>
+ set({
+ editingPlaceId: null,
+ searchKeyword: "",
+ selectedResultId: null,
+ }),
+}));
diff --git a/src/store/reelsPlaceSelectStore.ts b/src/store/reelsPlaceSelectStore.ts
new file mode 100644
index 0000000..a7233d7
--- /dev/null
+++ b/src/store/reelsPlaceSelectStore.ts
@@ -0,0 +1,18 @@
+import { create } from "zustand";
+
+type ReelsPlaceSelectState = {
+ selectedPlaceIds: string[];
+ togglePlace: (id: string) => void;
+ clearSelection: () => void;
+};
+
+export const useReelsPlaceSelectStore = create((set) => ({
+ selectedPlaceIds: [],
+ togglePlace: (id) =>
+ set((state) => ({
+ selectedPlaceIds: state.selectedPlaceIds.includes(id)
+ ? state.selectedPlaceIds.filter((placeId) => placeId !== id)
+ : [...state.selectedPlaceIds, id],
+ })),
+ clearSelection: () => set({ selectedPlaceIds: [] }),
+}));
diff --git a/src/store/registerRoomStore.ts b/src/store/registerRoomStore.ts
new file mode 100644
index 0000000..ae18cdd
--- /dev/null
+++ b/src/store/registerRoomStore.ts
@@ -0,0 +1,57 @@
+import { create } from "zustand";
+
+type RegisterRoomState = {
+ selectedPlaceIds: string[];
+ selectedPlaceCount: number;
+ selectedRoomId: string | null;
+ confirmModalOpen: boolean;
+ roomPlaceCountDeltas: Record;
+ setSelectedPlaces: (placeIds: string[]) => void;
+ setSelectedRoom: (roomId: string | null) => void;
+ openConfirm: () => void;
+ closeConfirm: () => void;
+ completeRegister: () => boolean;
+ resetFlow: () => void;
+};
+
+export const useRegisterRoomStore = create((set, get) => ({
+ selectedPlaceIds: [],
+ selectedPlaceCount: 0,
+ selectedRoomId: null,
+ confirmModalOpen: false,
+ roomPlaceCountDeltas: {},
+ setSelectedPlaces: (placeIds) =>
+ set({
+ selectedPlaceIds: placeIds,
+ selectedPlaceCount: placeIds.length,
+ }),
+ setSelectedRoom: (roomId) => set({ selectedRoomId: roomId }),
+ openConfirm: () => set({ confirmModalOpen: true }),
+ closeConfirm: () => set({ confirmModalOpen: false }),
+ completeRegister: () => {
+ const { selectedRoomId, selectedPlaceCount, roomPlaceCountDeltas } = get();
+
+ if (!selectedRoomId || selectedPlaceCount <= 0) {
+ return false;
+ }
+
+ set({
+ roomPlaceCountDeltas: {
+ ...roomPlaceCountDeltas,
+ [selectedRoomId]: (roomPlaceCountDeltas[selectedRoomId] ?? 0) + selectedPlaceCount,
+ },
+ selectedPlaceIds: [],
+ selectedPlaceCount: 0,
+ selectedRoomId: null,
+ confirmModalOpen: false,
+ });
+ return true;
+ },
+ resetFlow: () =>
+ set({
+ selectedPlaceIds: [],
+ selectedPlaceCount: 0,
+ selectedRoomId: null,
+ confirmModalOpen: false,
+ }),
+}));