From 05488297fb2bcd9d77d6398b3761989e16cc3671 Mon Sep 17 00:00:00 2001 From: kimbosung521 Date: Fri, 3 Apr 2026 00:31:25 +0900 Subject: [PATCH 1/8] =?UTF-8?q?fix:=20PLI-19(=EC=9C=84=EC=8B=9C=EB=A6=AC?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=ED=94=BC=EB=93=9C=EB=B0=B1=20=EC=88=98?= =?UTF-8?q?=EC=A0=95)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 실시간 추천 시 바텀시트 높이 조절 - 모달 수정 - 위시 리시트 하트 기본 값 수정 - 검색 일회용 수정 --- src/components/ui/CustomBottomSheet.tsx | 13 +- src/screens/WishlistScreen.tsx | 696 ++++++++---------- src/screens/wishList/components/PlaceCard.tsx | 2 +- .../components/WishlistBottomSheet.tsx | 66 ++ src/screens/wishList/components/index.ts | 10 +- .../wishList/components/tab/WishTabSave.tsx | 56 ++ .../components/tab/WishTabTrending.tsx | 49 ++ .../components/tab/WishTabWishlist.tsx | 77 ++ 8 files changed, 576 insertions(+), 393 deletions(-) create mode 100644 src/screens/wishList/components/WishlistBottomSheet.tsx create mode 100644 src/screens/wishList/components/tab/WishTabSave.tsx create mode 100644 src/screens/wishList/components/tab/WishTabTrending.tsx create mode 100644 src/screens/wishList/components/tab/WishTabWishlist.tsx diff --git a/src/components/ui/CustomBottomSheet.tsx b/src/components/ui/CustomBottomSheet.tsx index 7e3f482..1fd5464 100644 --- a/src/components/ui/CustomBottomSheet.tsx +++ b/src/components/ui/CustomBottomSheet.tsx @@ -18,6 +18,7 @@ interface CustomBottomSheetProps { translateY: SharedValue; height?: number; collapsedVisibleHeight?: number; + maxTopSnap?: number; cornerRadius?: number; backgroundColor?: string; showIndicator?: boolean; @@ -29,6 +30,7 @@ export const CustomBottomSheet = ({ translateY, height, collapsedVisibleHeight = 28, + maxTopSnap, cornerRadius, backgroundColor, showIndicator = true, @@ -41,6 +43,7 @@ export const CustomBottomSheet = ({ const SNAP_MIN = SHEET_HEIGHT - collapsedVisibleHeight; const SNAP_MID = SHEET_HEIGHT - 310; const SNAP_HIGH = 0; + const SNAP_TOP_LIMIT = Math.max(SNAP_HIGH, Math.min(SNAP_MID, maxTopSnap ?? SNAP_HIGH)); const gesture = Gesture.Pan() .onStart(() => { @@ -49,7 +52,7 @@ export const CustomBottomSheet = ({ .onUpdate((e) => { const nextY = startY.value + e.translationY; // 이동 범위 제한 (620 ~ 66 사이에서만 움직임) - translateY.value = Math.max(SNAP_HIGH, Math.min(SNAP_MIN, nextY)); + translateY.value = Math.max(SNAP_TOP_LIMIT, Math.min(SNAP_MIN, nextY)); }) .onEnd((e) => { const currentY = translateY.value; @@ -59,7 +62,7 @@ export const CustomBottomSheet = ({ // 2. 속도가 빠를 때 방향에 따라 스냅 if (velocityY < -500) { // 위로 휙 올릴 때: 현재 위치보다 한 단계 위로 - targetY = currentY > SNAP_MID ? SNAP_MID : SNAP_HIGH; + targetY = currentY > SNAP_MID ? SNAP_MID : SNAP_TOP_LIMIT; } else if (velocityY > 500) { // 아래로 휙 내릴 때: 현재 위치보다 한 단계 아래로 targetY = currentY < SNAP_MID ? SNAP_MID : SNAP_MIN; @@ -67,13 +70,13 @@ export const CustomBottomSheet = ({ // 3. 속도가 느릴 때 가장 가까운 지점으로 자석처럼 붙기 const distanceToMin = Math.abs(currentY - SNAP_MIN); const distanceToMid = Math.abs(currentY - SNAP_MID); - const distanceToHigh = Math.abs(currentY - SNAP_HIGH); + const distanceToTop = Math.abs(currentY - SNAP_TOP_LIMIT); - const minDistance = Math.min(distanceToMin, distanceToMid, distanceToHigh); + const minDistance = Math.min(distanceToMin, distanceToMid, distanceToTop); if (minDistance === distanceToMin) targetY = SNAP_MIN; else if (minDistance === distanceToMid) targetY = SNAP_MID; - else targetY = SNAP_HIGH; + else targetY = SNAP_TOP_LIMIT; } // 부드럽게 이동 diff --git a/src/screens/WishlistScreen.tsx b/src/screens/WishlistScreen.tsx index 69a1458..96b0683 100644 --- a/src/screens/WishlistScreen.tsx +++ b/src/screens/WishlistScreen.tsx @@ -1,11 +1,9 @@ import React, { useCallback, useRef, useEffect } from 'react'; -import { View, TextInput, TouchableOpacity, Text, Dimensions } from 'react-native'; +import { View, TextInput, TouchableOpacity, Text, Pressable, Keyboard } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; -import { useNavigation } from '@react-navigation/native'; +import { useNavigation, useFocusEffect } from '@react-navigation/native'; import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; -import { SearchArrowIcon, SearchingIcon, MyLocation, EmptyLocation, EmptyWish, WishStar } from '@/assets/icons'; -import BottomSheet from '@gorhom/bottom-sheet'; -import CustomBottomSheet from '@/components/ui/CustomBottomSheet'; +import { SearchArrowIcon, SearchingIcon, MyLocation, WishStar } from '@/assets/icons'; import { SearchContainer } from '@/components/ui'; import { WishModal } from './wishList/components/WishModal'; import type { RootStackParamList } from '@/navigation/types'; @@ -14,354 +12,336 @@ import { useState } from "react"; import { RouteIcon, AlertIcon } from '@/assets/icons'; import { COLORS } from '@/constants'; import { BackHandler } from 'react-native'; -import { CategoryChip, PlaceCard, PlaceCardProps } from '@/screens/wishList/components'; +import { CategoryChip, PlaceCard, PlaceCardProps, WishTabSave, WishTabTrending, WishTabWishlist, WishlistBottomSheet } from '@/screens/wishList/components'; +import type { WishlistBottomSheetTabId } from '@/screens/wishList/components'; import Animated, { useSharedValue, useAnimatedStyle, + useAnimatedReaction, interpolate, withTiming, Easing, } from 'react-native-reanimated'; -import { ScrollView } from 'react-native-gesture-handler'; import { Shadow } from 'react-native-shadow-2'; + // ============ Types ============ type NavigationProp = NativeStackNavigationProp; - -// ============ Component ============ +type TabId = WishlistBottomSheetTabId; +type LikeTabId = Exclude; +//더미 데이터 - 실제 API 연동 시 제거 예정 +const TRENDING_PLACES: PlaceCardProps['place'][] = [ + { + id: 'place_1', + title: '센소지 아사쿠사', + location: '도쿄, 일본', + description: '도쿄는 일본의 수도이자 전통과 현대가 조화를 이루는 매력적인 도시입니다.', + categories: ['관광지', '문화', '역사'], + image: require('@/assets/images/thumnail.png'), + }, +]; + +const SAVED_PLACES: PlaceCardProps['place'][] = [ + { + id: 'placeS_1', + title: '센소지 아사쿠사', + location: '도쿄, 일본', + description: '도쿄는 일본의 수도이자 전통과 현대가 조화를 이루는 매력적인 도시입니다.', + categories: ['관광지', '문화', '역사'], + image: require('@/assets/images/thumnail.png'), + }, + { + id: 'placeS_2', + title: '센소지 아사쿠사', + location: '도쿄, 일본', + description: '도쿄는 일본의 수도이자 전통과 현대가 조화를 이루는 매력적인 도시입니다.', + categories: ['관광지', '문화', '역사'], + image: require('@/assets/images/thumnail.png'), + }, +]; + +const WISHLIST_PLACES: PlaceCardProps['place'][] = [ + { + id: 'placeW_1', + title: '센소지 아사쿠사', + location: '도쿄, 일본', + description: '도쿄는 일본의 수도이자 전통과 현대가 조화를 이루는 매력적인 도시입니다.', + categories: ['관광지', '문화', '역사'], + image: require('@/assets/images/thumnail.png'), + }, + { + id: 'placeW_2', + title: '센소지 아사쿠사', + location: '도쿄, 일본', + description: '도쿄는 일본의 수도이자 전통과 현대가 조화를 이루는 매력적인 도시입니다.', + categories: ['관광지', '문화', '역사'], + image: require('@/assets/images/thumnail.png'), + }, + { + id: 'placeW_3', + title: '센소지 아사쿠사', + location: '도쿄, 일본', + description: '도쿄는 일본의 수도이자 전통과 현대가 조화를 이루는 매력적인 도시입니다.', + categories: ['관광지', '문화', '역사'], + image: require('@/assets/images/thumnail.png'), + }, + { + id: 'placeW_4', + title: '센소지 아사쿠사', + location: '도쿄, 일본', + description: '도쿄는 일본의 수도이자 전통과 현대가 조화를 이루는 매력적인 도시입니다.', + categories: ['관광지', '문화', '역사'], + image: require('@/assets/images/thumnail.png'), + }, + { + id: 'placeW_5', + title: '센소지 아사쿠사', + location: '도쿄, 일본', + description: '도쿄는 일본의 수도이자 전통과 현대가 조화를 이루는 매력적인 도시입니다.', + categories: ['관광지', '문화', '역사'], + image: require('@/assets/images/thumnail.png'), + }, + { + id: 'placeW_6', + title: '센소지 아사쿠사', + location: '도쿄, 일본', + description: '도쿄는 일본의 수도이자 전통과 현대가 조화를 이루는 매력적인 도시입니다.', + categories: ['관광지', '문화', '역사'], + image: require('@/assets/images/thumnail.png'), + }, +]; + +const SEARCH_PLACES: PlaceCardProps['place'][] = [ + { + id: 'place_6', + title: '센소지 아사쿠사', + location: '도쿄, 일본', + description: '도쿄는 일본의 수도이자 전통과 현대가 조화를 이루는 매력적인 도시입니다.', + categories: ['관광지', '문화', '역사'], + image: require('@/assets/images/thumnail.png'), + }, + { + id: 'place_1', + title: '센소지 아사쿠사', + location: '도쿄, 일본', + description: '도쿄는 일본의 수도이자 전통과 현대가 조화를 이루는 매력적인 도시입니다.', + categories: ['관광지', '문화', '역사'], + image: require('@/assets/images/thumnail.png'), + }, +]; + +const TABS: { id: TabId; label: string }[] = [ + { id: 'trending', label: '실시간 추천' }, + { id: 'saved', label: '저장된 장소' }, + { id: 'wishlist', label: '위시 리스트' }, +]; + +// 바텀시트 스냅 포인트 const WishlistScreen: React.FC = () => { - const BOTTOM_SHEET_MIN_HEIGHT = 28; // 바텀시트 기본 높이 + const BOTTOM_SHEET_MIN_HEIGHT = 28; const SHEET_HEIGHT = 654; + const SECOND_SNAP_VISIBLE_HEIGHT = 310; + const INITIAL_CATEGORY: TabId = 'trending'; const SNAP_LOW = SHEET_HEIGHT - 28; - const translateY = useSharedValue(SHEET_HEIGHT - BOTTOM_SHEET_MIN_HEIGHT); - const SEARCH_BUTTON_BOTTOM = BOTTOM_SHEET_MIN_HEIGHT + 10; // 바텀시트 위에 10px 여유 - // 파생 값 - const tabs = React.useMemo( - () => [ - { id: 'trending', label: '실시간 추천' }, - { id: 'saved', label: '저장된 장소' }, - { id: 'wishlist', label: '위시 리스트' }, - ], - [], - ); - const TRENDING_PLACES: PlaceCardProps['place'][] = [ - { - id: 'place_1', - title: '센소지 아사쿠사', - location: '도쿄, 일본', - description: '도쿄는 일본의 수도이자 전통과 현대가 조화를 이루는 매력적인 도시입니다.', - categories: ['관광지', '문화', '역사'], - image: require('@/assets/images/thumnail.png'), - }, - ] - const SAVED_PLACES: PlaceCardProps['place'][] = [ - { - id: 'place_1', - title: '센소지 아사쿠사', - location: '도쿄, 일본', - description: '도쿄는 일본의 수도이자 전통과 현대가 조화를 이루는 매력적인 도시입니다.', - categories: ['관광지', '문화', '역사'], - image: require('@/assets/images/thumnail.png'), - }, - { - id: 'place_2', - title: '센소지 아사쿠사', - location: '도쿄, 일본', - description: '도쿄는 일본의 수도이자 전통과 현대가 조화를 이루는 매력적인 도시입니다.', - categories: ['관광지', '문화', '역사'], - image: require('@/assets/images/thumnail.png'), - }, - ] - const WISHLIST_PLACES: PlaceCardProps['place'][] = [ - { - id: 'place_1', - title: '센소지 아사쿠사', - location: '도쿄, 일본', - description: '도쿄는 일본의 수도이자 전통과 현대가 조화를 이루는 매력적인 도시입니다.', - categories: ['관광지', '문화', '역사'], - image: require('@/assets/images/thumnail.png'), - }, - { - id: 'place_2', - title: '센소지 아사쿠사', - location: '도쿄, 일본', - description: '도쿄는 일본의 수도이자 전통과 현대가 조화를 이루는 매력적인 도시입니다.', - categories: ['관광지', '문화', '역사'], - image: require('@/assets/images/thumnail.png'), - }, - - { - id: 'place_3', - title: '센소지 아사쿠사', - location: '도쿄, 일본', - description: '도쿄는 일본의 수도이자 전통과 현대가 조화를 이루는 매력적인 도시입니다.', - categories: ['관광지', '문화', '역사'], - image: require('@/assets/images/thumnail.png'), - }, - { - id: 'place_4', - title: '센소지 아사쿠사', - location: '도쿄, 일본', - description: '도쿄는 일본의 수도이자 전통과 현대가 조화를 이루는 매력적인 도시입니다.', - categories: ['관광지', '문화', '역사'], - image: require('@/assets/images/thumnail.png'), - }, - { - id: 'place_5', - title: '센소지 아사쿠사', - location: '도쿄, 일본', - description: '도쿄는 일본의 수도이자 전통과 현대가 조화를 이루는 매력적인 도시입니다.', - categories: ['관광지', '문화', '역사'], - image: require('@/assets/images/thumnail.png'), - }, - - - { - id: 'place_6', - title: '센소지 아사쿠사', - location: '도쿄, 일본', - description: '도쿄는 일본의 수도이자 전통과 현대가 조화를 이루는 매력적인 도시입니다.', - categories: ['관광지', '문화', '역사'], - image: require('@/assets/images/thumnail.png'), - }, - ] - const search_places: PlaceCardProps['place'][] = [ - { - id: 'place_6', - title: '센소지 아사쿠사', - location: '도쿄, 일본', - description: '도쿄는 일본의 수도이자 전통과 현대가 조화를 이루는 매력적인 도시입니다.', - categories: ['관광지', '문화', '역사'], - image: require('@/assets/images/thumnail.png'), - }, - { - id: 'place_1', - title: '센소지 아사쿠사', - location: '도쿄, 일본', - description: '도쿄는 일본의 수도이자 전통과 현대가 조화를 이루는 매력적인 도시입니다.', - categories: ['관광지', '문화', '역사'], - image: require('@/assets/images/thumnail.png'), - }, - ] - - - // 하트 다중 선택용 상태 관리 - const [likedItemIds, setLikedItemIds] = useState>(new Set()); - - const toggleLike = useCallback((id: string) => { - setLikedItemIds((prevIds) => { - const newIds = new Set(prevIds); - if (newIds.has(id)) { - newIds.delete(id); - } else { - newIds.add(id); - } - return newIds; + const SNAP_FULL = 0; + const SNAP_TRENDING = SHEET_HEIGHT - SECOND_SNAP_VISIBLE_HEIGHT; + const SEARCH_BUTTON_BOTTOM = BOTTOM_SHEET_MIN_HEIGHT + 10; + + const translateY = useSharedValue(SNAP_LOW); + const isTrendingTabSV = useSharedValue(INITIAL_CATEGORY === 'trending'); + + const [likedIdsByTab, setLikedIdsByTab] = useState>>(() => ({ + saved: new Set(), + wishlist: new Set(WISHLIST_PLACES.map((place) => place.id)), + })); + + const toggleLike = useCallback((tab: LikeTabId, id: string) => { + setLikedIdsByTab((prev) => { + const next = new Set(prev[tab]); + next.has(id) ? next.delete(id) : next.add(id); + return { ...prev, [tab]: next }; }); }, []); - const [isLiked, setIsLiked] = useState(false); + + const isLikedInTab = useCallback( + (tab: LikeTabId, id: string) => likedIdsByTab[tab].has(id), + [likedIdsByTab], + ); + const navigation = useNavigation(); - const bottomSheetRef = useRef(null); - const [selectedCategory, setSelectedCategory] = useState('trending'); + const [selectedCategory, setSelectedCategory] = useState(INITIAL_CATEGORY); const [isSearchFocused, setIsSearchFocused] = useState(false); const [showAddModal, setShowAddModal] = useState(false); const [showExitModal, setShowExitModal] = useState(false); - const [activeTab, setActiveTab] = React.useState('info'); const [isSheetExpanded, setIsSheetExpanded] = useState(false); + const [searchQuery, setSearchQuery] = useState(''); const searchInputRef = useRef(null); + const isInitialTabEffect = useRef(true); + const showAddModalRef = useRef(false); + const showExitModalRef = useRef(false); + + useEffect(() => { showAddModalRef.current = showAddModal; }, [showAddModal]); + useEffect(() => { showExitModalRef.current = showExitModal; }, [showExitModal]); + useEffect(() => { + isTrendingTabSV.value = selectedCategory === 'trending'; + }, [isTrendingTabSV, selectedCategory]); + // 바텀시트 애니메이션 함수 + const animateSheetTo = useCallback((targetY: number) => { + const currentY = translateY.value; + const distance = Math.abs(targetY - currentY); + const isMovingDown = targetY > currentY; + + if (isMovingDown) { + const duration = Math.max(260, Math.min(620, distance * 0.95)); + translateY.value = withTiming(targetY, { duration, easing: Easing.out(Easing.cubic) }); + } else { + const duration = Math.max(240, Math.min(700, distance * 1.02)); + translateY.value = withTiming(targetY, { duration, easing: Easing.out(Easing.cubic) }); + } + setIsSheetExpanded(targetY !== SNAP_LOW); + }, [SNAP_LOW, translateY]); + // 바텀시트 상태 변화 핸들러 const handleSheetChange = useCallback((expanded: boolean) => { setIsSheetExpanded(expanded); }, []); - - const handleTabChange = useCallback((tabId: string) => { - setActiveTab(tabId); + // 검색 입력창에 포커스 주기 + const focusSearchInput = useCallback(() => { + searchInputRef.current?.focus(); }, []); - - const handleGoBack = useCallback(() => { - - setShowExitModal(true); - }, []); - - const handleExpand = useCallback(() => { - bottomSheetRef.current?.expand(); - }, []); - - - const handleCollapse = useCallback(() => { - bottomSheetRef.current?.collapse(); + // 검색 입력창에 포커스 될 때 → 검색어 상태 업데이트 + 키보드 올리기 + const handleSearchFocus = useCallback(() => setIsSearchFocused(true), []); + // 검색 입력창에서 포커스 벗어날 때 → 검색어 초기화 + 키보드 내리기 + const handleSearchBlur = useCallback(() => { + setIsSearchFocused(false); + Keyboard.dismiss(); }, []); + // 뒤로가기 버튼 핸들러 → 모달 열기 + const handleGoBack = useCallback(() => setShowExitModal(true), []); + // AI 추천 일정짜기 버튼 핸들러 → TripDetail로 이동 + 모달 닫기 const handleAiPlan = useCallback(() => { - navigation.navigate('TripDetail'); + navigation.navigate('TripDetail'); setShowAddModal(false); - // 여기에 AI 일정 생성 페이지로 이동하는 로직 추가 - }, []); - + }, [navigation]); + // 직접 일정짜기 버튼 핸들러 → TripDetail로 이동 + 모달 닫기 const handleManualPlan = useCallback(() => { - navigation.navigate('TripDetail'); + navigation.navigate('TripDetail'); setShowAddModal(false); - // 여기에 직접 작성 페이지로 이동하는 로직 추가 - }, []); - - const handleComplete = useCallback(() => { - setShowAddModal(true); - }, []); - + }, [navigation]); + // 완료 버튼 핸들러 → 모달 열기 + const handleComplete = useCallback(() => setShowAddModal(true), []); + // 지도 영역 누르면 바텀시트 내려가기 const handleMapPress = useCallback(() => { - if (isSheetExpanded) { - translateY.value = withTiming(SHEET_HEIGHT - 28, { - duration: 400, - easing: Easing.out(Easing.exp), - }); - setIsSheetExpanded(false); - } - }, [isSheetExpanded, translateY]); + if (isSheetExpanded) animateSheetTo(SNAP_LOW); + }, [animateSheetTo, isSheetExpanded, SNAP_LOW]); + // 좋아요 토글 핸들러 + 상태 조회 함수 (탭별) + const handleToggleSaved = useCallback( + (id: string) => toggleLike('saved', id), + [toggleLike], + ); + const handleToggleWishlist = useCallback( + (id: string) => toggleLike('wishlist', id), + [toggleLike], + ); + const isSavedLiked = useCallback( + (id: string) => isLikedInTab('saved', id), + [isLikedInTab], + ); + const isWishlistLiked = useCallback( + (id: string) => isLikedInTab('wishlist', id), + [isLikedInTab], + ); + // 탭 변경 시 바텀시트 애니메이션 + useEffect(() => { + if (isInitialTabEffect.current) { + isInitialTabEffect.current = false; + return; + } + const targetY = selectedCategory === 'trending' ? SNAP_TRENDING : SNAP_FULL; + animateSheetTo(targetY); + }, [selectedCategory, animateSheetTo, SNAP_TRENDING, SNAP_FULL]); + + useAnimatedReaction( + () => ({ y: translateY.value, isTrending: isTrendingTabSV.value }), + ({ y, isTrending }, prev) => { + const isMovingUp = y < (prev?.y ?? y); + if (isTrending && isMovingUp && y < SNAP_TRENDING) { + translateY.value = SNAP_TRENDING; + } + }, + ); + // 바텀시트 애니메이션 스타일 const mapUIAnimatedStyle = useAnimatedStyle(() => { - 'worklet' + 'worklet'; const opacity = interpolate( translateY.value, - [SNAP_LOW - 5, SNAP_LOW], // SNAP_LOW 지점에 가까워질 때만 나타남 + [SNAP_LOW - 5, SNAP_LOW], [0, 1], - 'clamp' + 'clamp', ); - - return { - opacity, - // 버튼의 기본 위치를 70 지점보다 살짝 위로 설정 - pointerEvents: opacity < 0.1 ? 'none' : 'auto', - }; + return { opacity, pointerEvents: opacity < 0.1 ? 'none' : 'auto' }; }); - useEffect(() => { - const backAction = () => { - //모달이 떠 있으면 동작 막기 - if (showExitModal || showAddModal) return true; - - // 모달 띄우기 - setShowExitModal(true); + // 뒤로가기 버튼 핸들링 + 모달 상태 초기화 + useFocusEffect( + useCallback(() => { + setShowAddModal(false); + setShowExitModal(false); + + if (selectedCategory === 'trending' && translateY.value < SNAP_TRENDING) { + animateSheetTo(SNAP_TRENDING); + } - return true; - }; + const backAction = () => { + if (showExitModalRef.current || showAddModalRef.current) return true; + setShowExitModal(true); + return true; + }; - // 핸들러 등록 - const backHandler = BackHandler.addEventListener( - 'hardwareBackPress', - backAction, - ); - - return () => backHandler.remove(); - }, [showExitModal, showAddModal]); - //바텀시트 안에 콘텐츠 - // 1. 데이터가 있을 때 나오는 화면 (리스트) - const renderContent = (): React.ReactElement | null => { - switch (selectedCategory) { - case 'trending': - return ( - <> - 현재 핫한 장소 - - {TRENDING_PLACES.map((place) => ( - - ))} - - - ); - case 'saved': - return ( - - {SAVED_PLACES.map((place) => ( - - ))} - - ); - case 'wishlist': - return ( - - {WISHLIST_PLACES.map((place) => ( - - ))} - - ); - default: - return null; - } - }; + const backHandler = BackHandler.addEventListener('hardwareBackPress', backAction); + return () => backHandler.remove(); + }, [selectedCategory, SNAP_TRENDING, animateSheetTo, translateY]), + ); - // 데이터가 없을 때 나오는 화면 - const renderEmptyContent = (): React.ReactElement | null => { + //탭 컨텐츠 + const renderTabContent = () => { switch (selectedCategory) { case 'trending': - - return ( - <> - 현재 핫한 장소 - - {TRENDING_PLACES.map((place) => ( - - ))} - - - ); - //저장된 장소 데이터 없을 경우 + return ; case 'saved': return ( - - - 저장한 장소가 없어요 - 마음에 드는 장소를 저장해주세요. - + ); - //위시리스트 데이터 없을 경우 case 'wishlist': return ( - - - 위시리스트에 장소가 없어요 - 가고싶은 장소를 위시리스트에 추가해주세요. - + ); default: return null; } }; - // 2. 렌더링 return ( + /> - {/* 지도 영역만 누르면 바텀시트 내려가기 */} + {/* 지도 영역 누르면 바텀시트 내려가기 */} { onPress={handleMapPress} /> - + - {/* 왼쪽: 뒤로가기 버튼 */} - {/* 2. 검색 입력창 */} - setIsSearchFocused(true)} - onBlur={() => setIsSearchFocused(false)} + value={searchQuery} + onChangeText={setSearchQuery} + onFocus={handleSearchFocus} + onBlur={handleSearchBlur} /> - - {/* 3. 검색 아이콘 */} - + - {/* 3. 지도 위 UI를 Animated.View로 감싸고 mapUIAnimatedStyle 적용 */} - + { /> - + - - - - {/* 왼쪽: 탭 메뉴들 */} - - {tabs.map((tab) => ( - setSelectedCategory(tab.id)} - isSelected={selectedCategory === tab.id} - className={`mr-2 px-4 py-2 rounded-2xl ${selectedCategory === tab.id ? "bg-main" : "bg-chip" - }`} - /> - ))} - - - {/* 오른쪽: 완료 버튼 (혼자 우측 정렬) */} - - + - - - {(() => { - if (selectedCategory === 'trending') { - return renderContent(); - } - - - const activeData = selectedCategory === 'saved' ? SAVED_PLACES : WISHLIST_PLACES; - - return activeData.length === 0 - ? renderEmptyContent() - : renderContent(); - })()} - - - - - {/* 검색창이 포커스 되었을 때 */} + {/* 검색 포커스 오버레이 */} {isSearchFocused && ( - { - setIsSearchFocused(false); - - - }} - onBlur={() => setIsSearchFocused(false)} + onPress={handleSearchBlur} style={{ position: 'absolute', - top: 0, - left: 0, - right: 0, - bottom: 0, + top: 0, left: 0, right: 0, bottom: 0, backgroundColor: 'white', }} /> - {/**검색바 공간 차지 */} - - - {search_places.map((place) => ( + + + {SEARCH_PLACES.map((place) => ( ))} + isLiked={selectedCategory === 'trending' ? false : isLikedInTab(selectedCategory, place.id)} + onToggleLike={(id) => { + if (selectedCategory !== 'trending') toggleLike(selectedCategory, id); + }} + /> + ))} )} - {/* 1. 완료 모 */} + + {/* 완료 모달 */} setShowAddModal(false)} @@ -532,13 +456,13 @@ const WishlistScreen: React.FC = () => { primaryBtnClass="w-full py-3 bg-main" primaryTextClass="text-white" secondaryLabel="직접 일정짜기" - secondaryBtnClass="w-full py-3 border border-main " + secondaryBtnClass="w-full py-3 border border-main" secondaryTextClass="text-main" onPrimaryPress={handleAiPlan} onSecondaryPress={handleManualPlan} /> - {/* 2. 뒤로가기 모달*/} + {/* 뒤로가기 모달 */} setShowExitModal(false)} @@ -553,7 +477,7 @@ const WishlistScreen: React.FC = () => { primaryBtnClass="flex-1 bg-main py-3" primaryTextClass="text-white" secondaryLabel="나가기" - secondaryBtnClass=" flex-1 bg-chip py-3" + secondaryBtnClass="flex-1 bg-chip py-3" secondaryTextClass="text-gray" onPrimaryPress={() => setShowExitModal(false)} onSecondaryPress={() => navigation.goBack()} @@ -566,4 +490,4 @@ const WishlistScreen: React.FC = () => { WishlistScreen.displayName = 'WishlistScreen'; export default WishlistScreen; -export { WishlistScreen }; +export { WishlistScreen }; \ No newline at end of file diff --git a/src/screens/wishList/components/PlaceCard.tsx b/src/screens/wishList/components/PlaceCard.tsx index f32f2b0..16aab54 100644 --- a/src/screens/wishList/components/PlaceCard.tsx +++ b/src/screens/wishList/components/PlaceCard.tsx @@ -77,7 +77,7 @@ export const PlaceCard = React.memo( {place.title} - + {place.location} diff --git a/src/screens/wishList/components/WishlistBottomSheet.tsx b/src/screens/wishList/components/WishlistBottomSheet.tsx new file mode 100644 index 0000000..2e288d0 --- /dev/null +++ b/src/screens/wishList/components/WishlistBottomSheet.tsx @@ -0,0 +1,66 @@ +import React, { memo } from 'react'; +import { ScrollView, View } from 'react-native'; +import type { SharedValue } from 'react-native-reanimated'; +import CustomBottomSheet from '@/components/ui/CustomBottomSheet'; +import { CategoryChip } from '@/screens/wishList/components'; + +export type WishlistBottomSheetTabId = 'trending' | 'saved' | 'wishlist'; + +interface WishlistBottomSheetProps { + translateY: SharedValue; + onStateChange: (expanded: boolean) => void; + maxTopSnap?: number; + tabs: Array<{ id: WishlistBottomSheetTabId; label: string }>; + selectedCategory: WishlistBottomSheetTabId; + onSelectCategory: (tabId: WishlistBottomSheetTabId) => void; + onPressComplete: () => void; + renderTabContent: () => React.ReactNode; +} + +const WishlistBottomSheetComponent: React.FC = ({ + translateY, + onStateChange, + maxTopSnap, + tabs, + selectedCategory, + onSelectCategory, + onPressComplete, + renderTabContent, +}) => { + return ( + + + + {tabs.map(tab => ( + onSelectCategory(tab.id)} + isSelected={selectedCategory === tab.id} + className={`mr-2 px-4 py-2 rounded-2xl ${selectedCategory === tab.id ? 'bg-main' : 'bg-chip'}`} + /> + ))} + + + + + + + {renderTabContent()} + + + + ); +}; + +WishlistBottomSheetComponent.displayName = 'WishlistBottomSheet'; + +export const WishlistBottomSheet = memo(WishlistBottomSheetComponent); diff --git a/src/screens/wishList/components/index.ts b/src/screens/wishList/components/index.ts index 6fb3124..b4b6a5c 100644 --- a/src/screens/wishList/components/index.ts +++ b/src/screens/wishList/components/index.ts @@ -4,4 +4,12 @@ export type { CategoryChipProps } from './CategoryChip'; export { WishContentContainer } from './WishContentContainer'; export type { WishContentContainerProps } from './WishContentContainer'; export { default as PlaceCard } from './PlaceCard'; -export type { PlaceCardProps } from './PlaceCard' +export type { PlaceCardProps } from './PlaceCard'; +export { WishTabTrending } from './tab/WishTabTrending'; +export type { WishTabTrendingProps } from './tab/WishTabTrending'; +export { WishTabSave } from './tab/WishTabSave'; +export type { WishTabSaveProps } from './tab/WishTabSave'; +export { WishTabWishlist } from './tab/WishTabWishlist'; +export type { WishTabWishlistProps } from './tab/WishTabWishlist'; +export { WishlistBottomSheet } from './WishlistBottomSheet'; +export type { WishlistBottomSheetTabId } from './WishlistBottomSheet'; diff --git a/src/screens/wishList/components/tab/WishTabSave.tsx b/src/screens/wishList/components/tab/WishTabSave.tsx new file mode 100644 index 0000000..35a97d9 --- /dev/null +++ b/src/screens/wishList/components/tab/WishTabSave.tsx @@ -0,0 +1,56 @@ +import React, { useCallback } from 'react'; +import { View, Text, FlatList } from 'react-native'; +import { EmptyLocation } from '@/assets/icons'; +import { PlaceCard, PlaceCardProps } from '@/screens/wishList/components'; + +export interface WishTabSaveProps { + places: PlaceCardProps['place'][]; + isLiked: (id: string) => boolean; + onToggleLike: (id: string) => void; +} + +const SavePlaceItem = React.memo<{ + item: PlaceCardProps['place']; + isLiked: boolean; + onToggleLike: (id: string) => void; +}>(({ item, isLiked, onToggleLike }) => ( + +)); +SavePlaceItem.displayName = 'SavePlaceItem'; +// 빈 상태 — 변하지 않으므로 memo로 완전히 고정 +const WishSaveEmptyState = React.memo(() => ( + + + 저장한 장소가 없어요 + 마음에 드는 장소를 저장해주세요. + +)); +WishSaveEmptyState.displayName = 'WishSaveEmptyState'; + + +export const WishTabSave = React.memo(({ places, isLiked, onToggleLike }) => { + const renderItem = useCallback( + ({ item }: { item: PlaceCardProps['place'] }) => ( + + ), + [isLiked, onToggleLike], + ); + + const keyExtractor = useCallback((item: PlaceCardProps['place']) => `saved-${item.id}`, []); + + if (places.length === 0) { + return ; + } + return ( + + ); +}); + +WishTabSave.displayName = 'WishTabSave'; diff --git a/src/screens/wishList/components/tab/WishTabTrending.tsx b/src/screens/wishList/components/tab/WishTabTrending.tsx new file mode 100644 index 0000000..e55abae --- /dev/null +++ b/src/screens/wishList/components/tab/WishTabTrending.tsx @@ -0,0 +1,49 @@ +import React, { useCallback } from 'react'; +import { View, Text, FlatList } from 'react-native'; +import { PlaceCard, PlaceCardProps } from '@/screens/wishList/components'; + +export interface WishTabTrendingProps { + places: PlaceCardProps['place'][]; +} + +// FlatList renderItem → 반드시 별도 컴포넌트 + memo (스크롤 성능) +const TrendingPlaceItem = React.memo<{ item: PlaceCardProps['place'] }>(({ item }) => ( + { }} + isTrending={true} + /> +)); +TrendingPlaceItem.displayName = 'TrendingPlaceItem'; + +export const WishTabTrending = React.memo(({ places }) => { + + const renderItem = useCallback( + ({ item }: { item: PlaceCardProps['place'] }) => , + [], + ); + + const keyExtractor = useCallback( + (item: PlaceCardProps['place']) => `trending-${item.id}`, + [], + ); + + return ( + <> + + 현재 핫한 장소 + + + + ); +}); + +WishTabTrending.displayName = 'WishTabTrending'; diff --git a/src/screens/wishList/components/tab/WishTabWishlist.tsx b/src/screens/wishList/components/tab/WishTabWishlist.tsx new file mode 100644 index 0000000..0e9dc71 --- /dev/null +++ b/src/screens/wishList/components/tab/WishTabWishlist.tsx @@ -0,0 +1,77 @@ +import React, { useCallback } from 'react'; +import { View, Text, FlatList } from 'react-native'; +import { EmptyWish } from '@/assets/icons'; +import { PlaceCard, PlaceCardProps } from '@/screens/wishList/components'; + +export interface WishTabWishlistProps { + places: PlaceCardProps['place'][]; + isLiked: (id: string) => boolean; + onToggleLike: (id: string) => void; +} + +// FlatList 아이템 → 반드시 별도 컴포넌트 + memo +const WishlistPlaceItem = React.memo<{ + item: PlaceCardProps['place']; + isLiked: boolean; + onToggleLike: (id: string) => void; +}>(({ item, isLiked, onToggleLike }) => ( + +)); +WishlistPlaceItem.displayName = 'WishlistPlaceItem'; + +// 빈 상태 — 변하지 않으므로 memo로 완전히 고정 +const WishlistEmptyState = React.memo(() => ( + + + + + 위시리스트에 장소가 없어요 + + 가고싶은 장소를 위시리스트에 추가해주세요. + + +)); +WishlistEmptyState.displayName = 'WishlistEmptyState'; + +export const WishTabWishlist = React.memo(({ + places, + isLiked, + onToggleLike, +}) => { + const renderItem = useCallback( + ({ item }: { item: PlaceCardProps['place'] }) => ( + + ), + [isLiked, onToggleLike], + ); + + const keyExtractor = useCallback( + (item: PlaceCardProps['place']) => `wishlist-${item.id}`, + [], + ); + + if (places.length === 0) { + return ; + } + + return ( + + ); +}); + +WishTabWishlist.displayName = 'WishTabWishlist'; \ No newline at end of file From 08f6e4c607b126a1291f6af7b3f147f1b4c6a4fb Mon Sep 17 00:00:00 2001 From: Kimbosung521 Date: Tue, 7 Apr 2026 23:42:35 +0900 Subject: [PATCH 2/8] =?UTF-8?q?fix:PLI-19(=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=20=EB=B6=84=EB=A6=AC,types=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC,=EA=B2=80=EC=83=89=EC=B0=BD=20=ED=94=BC?= =?UTF-8?q?=EB=93=9C=EB=B0=B1=20=EC=88=98=EC=A0=95)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/screens/WishlistScreen.tsx | 181 +++++++++--------- .../wishList/components/CategoryChip.tsx | 79 ++++---- src/screens/wishList/components/PlaceCard.tsx | 161 +++++++--------- .../components/WishContentContainer.tsx | 80 ++++---- src/screens/wishList/components/WishModal.tsx | 165 +++++++--------- .../components/WishlistBottomSheet.tsx | 96 +++++----- .../wishList/components/WishlistSearchBar.tsx | 35 ++++ .../components/WishlistSearchOverlay.tsx | 50 +++++ src/screens/wishList/components/index.ts | 23 ++- .../wishList/components/tab/WishTabSave.tsx | 74 +++---- .../components/tab/WishTabTrending.tsx | 60 +++--- .../components/tab/WishTabWishlist.tsx | 84 ++++---- src/screens/wishList/index.ts | 1 + src/screens/wishList/types.ts | 97 ++++++++++ 14 files changed, 634 insertions(+), 552 deletions(-) create mode 100644 src/screens/wishList/components/WishlistSearchBar.tsx create mode 100644 src/screens/wishList/components/WishlistSearchOverlay.tsx create mode 100644 src/screens/wishList/types.ts diff --git a/src/screens/WishlistScreen.tsx b/src/screens/WishlistScreen.tsx index 96b0683..27c957a 100644 --- a/src/screens/WishlistScreen.tsx +++ b/src/screens/WishlistScreen.tsx @@ -1,18 +1,26 @@ import React, { useCallback, useRef, useEffect } from 'react'; -import { View, TextInput, TouchableOpacity, Text, Pressable, Keyboard } from 'react-native'; +import { View, TextInput, TouchableOpacity, Text, Keyboard } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { useNavigation, useFocusEffect } from '@react-navigation/native'; import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; -import { SearchArrowIcon, SearchingIcon, MyLocation, WishStar } from '@/assets/icons'; -import { SearchContainer } from '@/components/ui'; +import { MyLocation, WishStar } from '@/assets/icons'; import { WishModal } from './wishList/components/WishModal'; import type { RootStackParamList } from '@/navigation/types'; import MapView, { PROVIDER_GOOGLE } from 'react-native-maps'; -import { useState } from "react"; +import { useState } from 'react'; import { RouteIcon, AlertIcon } from '@/assets/icons'; -import { COLORS } from '@/constants'; import { BackHandler } from 'react-native'; -import { CategoryChip, PlaceCard, PlaceCardProps, WishTabSave, WishTabTrending, WishTabWishlist, WishlistBottomSheet } from '@/screens/wishList/components'; +import { + CategoryChip, + PlaceCard, + PlaceCardProps, + WishTabSave, + WishTabTrending, + WishTabWishlist, + WishlistBottomSheet, + WishlistSearchBar, + WishlistSearchOverlay, +} from '@/screens/wishList/components'; import type { WishlistBottomSheetTabId } from '@/screens/wishList/components'; import Animated, { useSharedValue, @@ -179,26 +187,33 @@ const WishlistScreen: React.FC = () => { const showAddModalRef = useRef(false); const showExitModalRef = useRef(false); - useEffect(() => { showAddModalRef.current = showAddModal; }, [showAddModal]); - useEffect(() => { showExitModalRef.current = showExitModal; }, [showExitModal]); + useEffect(() => { + showAddModalRef.current = showAddModal; + }, [showAddModal]); + useEffect(() => { + showExitModalRef.current = showExitModal; + }, [showExitModal]); useEffect(() => { isTrendingTabSV.value = selectedCategory === 'trending'; }, [isTrendingTabSV, selectedCategory]); // 바텀시트 애니메이션 함수 - const animateSheetTo = useCallback((targetY: number) => { - const currentY = translateY.value; - const distance = Math.abs(targetY - currentY); - const isMovingDown = targetY > currentY; + const animateSheetTo = useCallback( + (targetY: number) => { + const currentY = translateY.value; + const distance = Math.abs(targetY - currentY); + const isMovingDown = targetY > currentY; - if (isMovingDown) { - const duration = Math.max(260, Math.min(620, distance * 0.95)); - translateY.value = withTiming(targetY, { duration, easing: Easing.out(Easing.cubic) }); - } else { - const duration = Math.max(240, Math.min(700, distance * 1.02)); - translateY.value = withTiming(targetY, { duration, easing: Easing.out(Easing.cubic) }); - } - setIsSheetExpanded(targetY !== SNAP_LOW); - }, [SNAP_LOW, translateY]); + if (isMovingDown) { + const duration = Math.max(260, Math.min(620, distance * 0.95)); + translateY.value = withTiming(targetY, { duration, easing: Easing.out(Easing.cubic) }); + } else { + const duration = Math.max(240, Math.min(700, distance * 1.02)); + translateY.value = withTiming(targetY, { duration, easing: Easing.out(Easing.cubic) }); + } + setIsSheetExpanded(targetY !== SNAP_LOW); + }, + [SNAP_LOW, translateY], + ); // 바텀시트 상태 변화 핸들러 const handleSheetChange = useCallback((expanded: boolean) => { setIsSheetExpanded(expanded); @@ -214,8 +229,17 @@ const WishlistScreen: React.FC = () => { setIsSearchFocused(false); Keyboard.dismiss(); }, []); - // 뒤로가기 버튼 핸들러 → 모달 열기 - const handleGoBack = useCallback(() => setShowExitModal(true), []); + const handleSearchInputBlur = useCallback(() => { + Keyboard.dismiss(); + }, []); + // 뒤로가기 버튼 핸들러: 검색 중이면 검색 종료, 그 외에는 모달 열기 + const handleGoBack = useCallback(() => { + if (isSearchFocused) { + handleSearchBlur(); + return; + } + setShowExitModal(true); + }, [isSearchFocused, handleSearchBlur]); // AI 추천 일정짜기 버튼 핸들러 → TripDetail로 이동 + 모달 닫기 const handleAiPlan = useCallback(() => { navigation.navigate('TripDetail'); @@ -234,22 +258,13 @@ const WishlistScreen: React.FC = () => { }, [animateSheetTo, isSheetExpanded, SNAP_LOW]); // 좋아요 토글 핸들러 + 상태 조회 함수 (탭별) - const handleToggleSaved = useCallback( - (id: string) => toggleLike('saved', id), - [toggleLike], - ); + const handleToggleSaved = useCallback((id: string) => toggleLike('saved', id), [toggleLike]); const handleToggleWishlist = useCallback( (id: string) => toggleLike('wishlist', id), [toggleLike], ); - const isSavedLiked = useCallback( - (id: string) => isLikedInTab('saved', id), - [isLikedInTab], - ); - const isWishlistLiked = useCallback( - (id: string) => isLikedInTab('wishlist', id), - [isLikedInTab], - ); + const isSavedLiked = useCallback((id: string) => isLikedInTab('saved', id), [isLikedInTab]); + const isWishlistLiked = useCallback((id: string) => isLikedInTab('wishlist', id), [isLikedInTab]); // 탭 변경 시 바텀시트 애니메이션 useEffect(() => { if (isInitialTabEffect.current) { @@ -272,12 +287,7 @@ const WishlistScreen: React.FC = () => { // 바텀시트 애니메이션 스타일 const mapUIAnimatedStyle = useAnimatedStyle(() => { 'worklet'; - const opacity = interpolate( - translateY.value, - [SNAP_LOW - 5, SNAP_LOW], - [0, 1], - 'clamp', - ); + const opacity = interpolate(translateY.value, [SNAP_LOW - 5, SNAP_LOW], [0, 1], 'clamp'); return { opacity, pointerEvents: opacity < 0.1 ? 'none' : 'auto' }; }); // 뒤로가기 버튼 핸들링 + 모달 상태 초기화 @@ -292,13 +302,24 @@ const WishlistScreen: React.FC = () => { const backAction = () => { if (showExitModalRef.current || showAddModalRef.current) return true; + if (isSearchFocused) { + handleSearchBlur(); + return true; + } setShowExitModal(true); return true; }; const backHandler = BackHandler.addEventListener('hardwareBackPress', backAction); return () => backHandler.remove(); - }, [selectedCategory, SNAP_TRENDING, animateSheetTo, translateY]), + }, [ + selectedCategory, + SNAP_TRENDING, + animateSheetTo, + translateY, + isSearchFocused, + handleSearchBlur, + ]), ); //탭 컨텐츠 @@ -355,33 +376,21 @@ const WishlistScreen: React.FC = () => { onPress={handleMapPress} /> - - - - - - - - - - - + + ]}> { /> - + @@ -410,35 +423,13 @@ const WishlistScreen: React.FC = () => { renderTabContent={renderTabContent} /> - {/* 검색 포커스 오버레이 */} - {isSearchFocused && ( - - - - - {SEARCH_PLACES.map((place) => ( - { - if (selectedCategory !== 'trending') toggleLike(selectedCategory, id); - }} - /> - ))} - - - )} + isLikedInTab('wishlist', id)} + onToggleLike={(id) => toggleLike('wishlist', id)} + /> {/* 완료 모달 */} { WishlistScreen.displayName = 'WishlistScreen'; export default WishlistScreen; -export { WishlistScreen }; \ No newline at end of file +export { WishlistScreen }; diff --git a/src/screens/wishList/components/CategoryChip.tsx b/src/screens/wishList/components/CategoryChip.tsx index dde6411..a9a618d 100644 --- a/src/screens/wishList/components/CategoryChip.tsx +++ b/src/screens/wishList/components/CategoryChip.tsx @@ -1,52 +1,43 @@ import React, { useCallback } from 'react'; import { View, Text, Pressable } from 'react-native'; - -// ============ Types ============ -export interface CategoryChipProps { - label: string; - onPress?: () => void; - isSelected?: boolean; - className?: string; - textClassName?: string; -} +import type { CategoryChipProps } from '@/screens/wishList/types'; // ============ Component ============ export const CategoryChip = React.memo( - ({ label, onPress, isSelected = false, className, textClassName }) => { - const handlePress = useCallback(() => { - onPress?.(); - }, [onPress]); - - - const defaultContainerStyle = 'items-center justify-center'; - - // 선택 여부에 따른 색상만 자동으로 처리 - const bgStyle = isSelected ? 'bg-main' : 'bg-chip'; - const textStyle = isSelected ? 'text-white' : 'text-gray'; - - // 렌더링 함수 - const renderContent = ( - {label} - ); - - if (onPress) { - return ( - - {renderContent} - - ); - } - - return ( - - {renderContent} - - ); - }, + ({ label, onPress, isSelected = false, className, textClassName }) => { + const handlePress = useCallback(() => { + onPress?.(); + }, [onPress]); + + const defaultContainerStyle = 'items-center justify-center'; + + // 선택 여부에 따른 색상만 자동으로 처리 + const bgStyle = isSelected ? 'bg-main' : 'bg-chip'; + const textStyle = isSelected ? 'text-white' : 'text-gray'; + + // 렌더링 함수 + const renderContent = ( + {label} + ); + + if (onPress) { + return ( + + {renderContent} + + ); + } + + return ( + + {renderContent} + + ); + }, ); CategoryChip.displayName = 'CategoryChip'; -export default CategoryChip; \ No newline at end of file +export default CategoryChip; diff --git a/src/screens/wishList/components/PlaceCard.tsx b/src/screens/wishList/components/PlaceCard.tsx index 16aab54..d02457e 100644 --- a/src/screens/wishList/components/PlaceCard.tsx +++ b/src/screens/wishList/components/PlaceCard.tsx @@ -1,103 +1,80 @@ import React from 'react'; -import { View, Text, Image, TouchableOpacity, ImageSourcePropType } from 'react-native'; +import { View, Text, Image, TouchableOpacity } from 'react-native'; import { ContentContainer } from '@/components/ui'; import { CategoryChip, WishContentContainer } from '@/screens/wishList/components'; import { PlaceIcon, HeartIcon, ActiveHeartIcon, VectorIcon } from '@/assets/icons'; - - -export interface PlaceCardProps { - place: { - id: string; - title: string; - location?: string; - description: string; - image: ImageSourcePropType; - categories?: string[]; - }; - - isLiked: boolean; - onToggleLike: (id: string) => void; - isTrending?: boolean; -} +import type { PlaceCardProps } from '@/screens/wishList/types'; export const PlaceCard = React.memo( - ({ - place, - isLiked, - onToggleLike, - isTrending = false, - }) => { - - - if (isTrending) { - return ( - - - - - - - - {place.title} - - {place.description} - - {/* 카테고리 칩 */} - {place.categories && ( - - {place.categories.map((cat, idx) => ( - - ))} - - )} - - onToggleLike(place.id)}> - - - - + ({ place, isLiked, onToggleLike, isTrending = false }) => { + if (isTrending) { + return ( + + + + + + + + {place.title} + + + {place.description} + + + {/* 카테고리 칩 */} + {place.categories && ( + + {place.categories.map((cat, idx) => ( + + ))} + + )} + + onToggleLike(place.id)}> + + + + + + ); + } + return ( + + + + + + + + {place.title} + + + - ); - } - return ( - - - - - - - - {place.title} - - - - - {place.location} - - - {place.description} - - - onToggleLike(place.id)} - > - {isLiked ? : } - - - + {place.location} + + + + {place.description} + + - ); - }, + onToggleLike(place.id)}> + {isLiked ? : } + + + + + ); + }, ); PlaceCard.displayName = 'PlaceCard'; -export default PlaceCard; \ No newline at end of file +export default PlaceCard; diff --git a/src/screens/wishList/components/WishContentContainer.tsx b/src/screens/wishList/components/WishContentContainer.tsx index 8d6c47e..3ab730a 100644 --- a/src/screens/wishList/components/WishContentContainer.tsx +++ b/src/screens/wishList/components/WishContentContainer.tsx @@ -1,51 +1,47 @@ import React, { useMemo } from 'react'; import { View } from 'react-native'; import { Shadow } from 'react-native-shadow-2'; - -// ============ Types ============ -export interface WishContentContainerProps { - children: React.ReactNode; - className?: string; -} +import type { WishContentContainerProps } from '@/screens/wishList/types'; // ============ Component ============ export const WishContentContainer = React.memo( - ({ children, className }) => { - // 파생 값 - const { widthClass, otherClasses } = useMemo(() => { - if (!className) return { widthClass: null, otherClasses: undefined }; - - // tokens 기반으로 width 클래스(w-*, w-[*])만 분리해 안전하게 제거 - const tokens = className.split(/\s+/).filter(Boolean); - const widthTokenIndex = tokens.findIndex((t) => /^w-(?:\[[^\]]+\]|\d+)$/.test(t)); - - if (widthTokenIndex === -1) { - return { widthClass: null, otherClasses: className }; - } - - const widthClass = tokens[widthTokenIndex] ?? null; - const otherClasses = tokens.filter((_, idx) => idx !== widthTokenIndex).join(' ').trim(); - return { widthClass, otherClasses }; - }, [className]); - - // 렌더링 - return ( - - - - {children} - - - - ); - }, + ({ children, className }) => { + // 파생 값 + const { widthClass, otherClasses } = useMemo(() => { + if (!className) return { widthClass: null, otherClasses: undefined }; + + // tokens 기반으로 width 클래스(w-*, w-[*])만 분리해 안전하게 제거 + const tokens = className.split(/\s+/).filter(Boolean); + const widthTokenIndex = tokens.findIndex((t) => /^w-(?:\[[^\]]+\]|\d+)$/.test(t)); + + if (widthTokenIndex === -1) { + return { widthClass: null, otherClasses: className }; + } + + const widthClass = tokens[widthTokenIndex] ?? null; + const otherClasses = tokens + .filter((_, idx) => idx !== widthTokenIndex) + .join(' ') + .trim(); + return { widthClass, otherClasses }; + }, [className]); + + // 렌더링 + return ( + + + {children} + + + ); + }, ); WishContentContainer.displayName = 'WishContentContainer'; diff --git a/src/screens/wishList/components/WishModal.tsx b/src/screens/wishList/components/WishModal.tsx index 3ae19b4..8ce715f 100644 --- a/src/screens/wishList/components/WishModal.tsx +++ b/src/screens/wishList/components/WishModal.tsx @@ -1,109 +1,82 @@ import { X } from '@/assets/icons'; import React from 'react'; import { View, Text, Pressable, Modal } from 'react-native'; - -// ============ Types ============ -export interface WishModalProps { - isVisible: boolean; - onClose: () => void; - icon?: React.ReactNode; - ModalIcon?: string; - title: string; - showCloseButton: boolean; - // 버튼 레이아웃 스타일 - buttonContainerClass?: string; - - // 메인 버튼 스타일 - primaryLabel: string; - ModalContainer: string; - onPrimaryPress: () => void; - primaryBtnClass?: string; - primaryIcon?: React.ReactNode; - primaryTextClass?: string; - primaryTitleTextClass?: string; - // 서브 버튼 스타일 - secondaryLabel: string; - onSecondaryPress: () => void; - secondaryBtnClass?: string; - secondaryTextClass?: string; -} +import type { WishModalProps } from '@/screens/wishList/types'; // ============ Component ============ export const WishModal = React.memo( - ({ - isVisible, - onClose, - showCloseButton, - icon, - ModalIcon, - ModalContainer, - title, - buttonContainerClass, - primaryLabel, - onPrimaryPress, - primaryIcon, - primaryBtnClass, - primaryTextClass, - secondaryLabel, - onSecondaryPress, - secondaryBtnClass, - secondaryTextClass, - primaryTitleTextClass - }) => { - return ( - - - e.stopPropagation()} className="w-full"> - - - {/* 아이콘 영역 */} - {icon && {icon}} - - - {/* X 버튼 */} - {showCloseButton ? - - - : null - } - {/* 타이틀 영역 */} - - {title} - + ({ + isVisible, + onClose, + showCloseButton, + icon, + ModalIcon, + ModalContainer, + title, + buttonContainerClass, + primaryLabel, + onPrimaryPress, + primaryIcon, + primaryBtnClass, + primaryTextClass, + secondaryLabel, + onSecondaryPress, + secondaryBtnClass, + secondaryTextClass, + primaryTitleTextClass, + }) => { + return ( + + + e.stopPropagation()} className="w-full"> + + {/* 아이콘 영역 */} + {icon && {icon}} + {/* X 버튼 */} + {showCloseButton ? ( + + + + ) : null} + {/* 타이틀 영역 */} + + {title} + - {/* 버튼 영역: 외부에서 주입한 클래스에 따라 가로/세로 결정 */} - - - {/* Primary 버튼 */} - {primaryIcon && {primaryIcon}} - - {primaryLabel} - - - - {/* Secondary 버튼 */} - - - {secondaryLabel} - - - + {/* 버튼 영역: 외부에서 주입한 클래스에 따라 가로/세로 결정 */} + + {/* Primary 버튼 */} + + {primaryIcon && {primaryIcon}} + + {primaryLabel} + + - - - + {/* Secondary 버튼 */} + + + {secondaryLabel} + - - ); - } + + + + + + ); + }, ); WishModal.displayName = 'WishModal'; -export default WishModal; \ No newline at end of file +export default WishModal; diff --git a/src/screens/wishList/components/WishlistBottomSheet.tsx b/src/screens/wishList/components/WishlistBottomSheet.tsx index 2e288d0..879fc76 100644 --- a/src/screens/wishList/components/WishlistBottomSheet.tsx +++ b/src/screens/wishList/components/WishlistBottomSheet.tsx @@ -1,64 +1,54 @@ import React, { memo } from 'react'; import { ScrollView, View } from 'react-native'; -import type { SharedValue } from 'react-native-reanimated'; import CustomBottomSheet from '@/components/ui/CustomBottomSheet'; import { CategoryChip } from '@/screens/wishList/components'; - -export type WishlistBottomSheetTabId = 'trending' | 'saved' | 'wishlist'; - -interface WishlistBottomSheetProps { - translateY: SharedValue; - onStateChange: (expanded: boolean) => void; - maxTopSnap?: number; - tabs: Array<{ id: WishlistBottomSheetTabId; label: string }>; - selectedCategory: WishlistBottomSheetTabId; - onSelectCategory: (tabId: WishlistBottomSheetTabId) => void; - onPressComplete: () => void; - renderTabContent: () => React.ReactNode; -} +import type { WishlistBottomSheetProps } from '@/screens/wishList/types'; const WishlistBottomSheetComponent: React.FC = ({ - translateY, - onStateChange, - maxTopSnap, - tabs, - selectedCategory, - onSelectCategory, - onPressComplete, - renderTabContent, + translateY, + onStateChange, + maxTopSnap, + tabs, + selectedCategory, + onSelectCategory, + onPressComplete, + renderTabContent, }) => { - return ( - - - - {tabs.map(tab => ( - onSelectCategory(tab.id)} - isSelected={selectedCategory === tab.id} - className={`mr-2 px-4 py-2 rounded-2xl ${selectedCategory === tab.id ? 'bg-main' : 'bg-chip'}`} - /> - ))} - - - + return ( + + + + {tabs.map((tab) => ( + onSelectCategory(tab.id)} + isSelected={selectedCategory === tab.id} + className={`mr-2 px-4 py-2 rounded-2xl ${selectedCategory === tab.id ? 'bg-main' : 'bg-chip'}`} + /> + ))} + + + - - - {renderTabContent()} - - - - ); + + + {renderTabContent()} + + + + ); }; WishlistBottomSheetComponent.displayName = 'WishlistBottomSheet'; diff --git a/src/screens/wishList/components/WishlistSearchBar.tsx b/src/screens/wishList/components/WishlistSearchBar.tsx new file mode 100644 index 0000000..cc6ff5e --- /dev/null +++ b/src/screens/wishList/components/WishlistSearchBar.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { Pressable, TextInput, TouchableOpacity } from 'react-native'; +import { SearchArrowIcon, SearchingIcon } from '@/assets/icons'; +import { SearchContainer } from '@/components/ui'; +import { COLORS } from '@/constants'; +import type { WishlistSearchBarProps } from '@/screens/wishList/types'; + +export const WishlistSearchBar = React.memo( + ({ searchInputRef, searchQuery, onChangeText, onFocus, onBlur, onFocusInput, onPressBack }) => { + return ( + + + + + + + + + + + + ); + }, +); + +WishlistSearchBar.displayName = 'WishlistSearchBar'; diff --git a/src/screens/wishList/components/WishlistSearchOverlay.tsx b/src/screens/wishList/components/WishlistSearchOverlay.tsx new file mode 100644 index 0000000..1f0a5d1 --- /dev/null +++ b/src/screens/wishList/components/WishlistSearchOverlay.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { View } from 'react-native'; +import PlaceCard from './PlaceCard'; +import type { PlaceCardProps, WishlistBottomSheetTabId } from '@/screens/wishList/components'; +import { ScrollView } from 'react-native-gesture-handler'; + +interface WishlistSearchOverlayProps { + isVisible: boolean; + selectedCategory: WishlistBottomSheetTabId; + places: PlaceCardProps['place'][]; + isLiked: (id: string) => boolean; + onToggleLike: (id: string) => void; +} + +export const WishlistSearchOverlay = React.memo( + ({ isVisible, selectedCategory, places, isLiked, onToggleLike }) => { + if (!isVisible) { + return null; + } + + return ( + + + + + {places.map((place) => ( + + ))} + + + ); + }, +); + +WishlistSearchOverlay.displayName = 'WishlistSearchOverlay'; diff --git a/src/screens/wishList/components/index.ts b/src/screens/wishList/components/index.ts index b4b6a5c..2124b45 100644 --- a/src/screens/wishList/components/index.ts +++ b/src/screens/wishList/components/index.ts @@ -1,15 +1,24 @@ // destinationDetail 스크린 전용 서브컴포넌트 barrel export export { CategoryChip } from './CategoryChip'; -export type { CategoryChipProps } from './CategoryChip'; export { WishContentContainer } from './WishContentContainer'; -export type { WishContentContainerProps } from './WishContentContainer'; export { default as PlaceCard } from './PlaceCard'; -export type { PlaceCardProps } from './PlaceCard'; export { WishTabTrending } from './tab/WishTabTrending'; -export type { WishTabTrendingProps } from './tab/WishTabTrending'; export { WishTabSave } from './tab/WishTabSave'; -export type { WishTabSaveProps } from './tab/WishTabSave'; export { WishTabWishlist } from './tab/WishTabWishlist'; -export type { WishTabWishlistProps } from './tab/WishTabWishlist'; export { WishlistBottomSheet } from './WishlistBottomSheet'; -export type { WishlistBottomSheetTabId } from './WishlistBottomSheet'; +export { WishlistSearchBar } from './WishlistSearchBar'; +export { WishlistSearchOverlay } from './WishlistSearchOverlay'; +export type { + CategoryChipProps, + PlaceCardProps, + WishContentContainerProps, + WishModalProps, + WishTabSaveProps, + WishTabTrendingProps, + WishTabWishlistProps, + WishPlace, + WishlistBottomSheetProps, + WishlistBottomSheetTab, + WishlistBottomSheetTabId, + WishlistSearchBarProps, +} from '../types'; diff --git a/src/screens/wishList/components/tab/WishTabSave.tsx b/src/screens/wishList/components/tab/WishTabSave.tsx index 35a97d9..5be8de1 100644 --- a/src/screens/wishList/components/tab/WishTabSave.tsx +++ b/src/screens/wishList/components/tab/WishTabSave.tsx @@ -1,56 +1,58 @@ import React, { useCallback } from 'react'; import { View, Text, FlatList } from 'react-native'; import { EmptyLocation } from '@/assets/icons'; -import { PlaceCard, PlaceCardProps } from '@/screens/wishList/components'; - -export interface WishTabSaveProps { - places: PlaceCardProps['place'][]; - isLiked: (id: string) => boolean; - onToggleLike: (id: string) => void; -} +import { PlaceCard } from '@/screens/wishList/components'; +import type { WishPlace, WishTabSaveProps } from '@/screens/wishList/types'; const SavePlaceItem = React.memo<{ - item: PlaceCardProps['place']; - isLiked: boolean; - onToggleLike: (id: string) => void; + item: WishPlace; + isLiked: boolean; + onToggleLike: (id: string) => void; }>(({ item, isLiked, onToggleLike }) => ( - + )); SavePlaceItem.displayName = 'SavePlaceItem'; // 빈 상태 — 변하지 않으므로 memo로 완전히 고정 const WishSaveEmptyState = React.memo(() => ( - - - 저장한 장소가 없어요 - 마음에 드는 장소를 저장해주세요. + + + + + + 저장한 장소가 없어요 + + + 마음에 드는 장소를 저장해주세요. + + + )); WishSaveEmptyState.displayName = 'WishSaveEmptyState'; - export const WishTabSave = React.memo(({ places, isLiked, onToggleLike }) => { - const renderItem = useCallback( - ({ item }: { item: PlaceCardProps['place'] }) => ( - - ), - [isLiked, onToggleLike], - ); + const renderItem = useCallback( + ({ item }: { item: WishPlace }) => ( + + ), + [isLiked, onToggleLike], + ); - const keyExtractor = useCallback((item: PlaceCardProps['place']) => `saved-${item.id}`, []); + const keyExtractor = useCallback((item: WishPlace) => `saved-${item.id}`, []); - if (places.length === 0) { - return ; - } - return ( - - ); + if (places.length === 0) { + return ; + } + return ( + + ); }); WishTabSave.displayName = 'WishTabSave'; diff --git a/src/screens/wishList/components/tab/WishTabTrending.tsx b/src/screens/wishList/components/tab/WishTabTrending.tsx index e55abae..d01a92f 100644 --- a/src/screens/wishList/components/tab/WishTabTrending.tsx +++ b/src/screens/wishList/components/tab/WishTabTrending.tsx @@ -1,49 +1,37 @@ import React, { useCallback } from 'react'; import { View, Text, FlatList } from 'react-native'; -import { PlaceCard, PlaceCardProps } from '@/screens/wishList/components'; - -export interface WishTabTrendingProps { - places: PlaceCardProps['place'][]; -} +import { PlaceCard } from '@/screens/wishList/components'; +import type { WishPlace, WishTabTrendingProps } from '@/screens/wishList/types'; // FlatList renderItem → 반드시 별도 컴포넌트 + memo (스크롤 성능) -const TrendingPlaceItem = React.memo<{ item: PlaceCardProps['place'] }>(({ item }) => ( - { }} - isTrending={true} - /> +const TrendingPlaceItem = React.memo<{ item: WishPlace }>(({ item }) => ( + {}} isTrending={true} /> )); TrendingPlaceItem.displayName = 'TrendingPlaceItem'; export const WishTabTrending = React.memo(({ places }) => { + const renderItem = useCallback( + ({ item }: { item: WishPlace }) => , + [], + ); - const renderItem = useCallback( - ({ item }: { item: PlaceCardProps['place'] }) => , - [], - ); - - const keyExtractor = useCallback( - (item: PlaceCardProps['place']) => `trending-${item.id}`, - [], - ); + const keyExtractor = useCallback((item: WishPlace) => `trending-${item.id}`, []); - return ( - <> - - 현재 핫한 장소 - - - - ); + return ( + <> + + 현재 핫한 장소 + + + + ); }); WishTabTrending.displayName = 'WishTabTrending'; diff --git a/src/screens/wishList/components/tab/WishTabWishlist.tsx b/src/screens/wishList/components/tab/WishTabWishlist.tsx index 0e9dc71..f04b88e 100644 --- a/src/screens/wishList/components/tab/WishTabWishlist.tsx +++ b/src/screens/wishList/components/tab/WishTabWishlist.tsx @@ -1,77 +1,59 @@ import React, { useCallback } from 'react'; import { View, Text, FlatList } from 'react-native'; import { EmptyWish } from '@/assets/icons'; -import { PlaceCard, PlaceCardProps } from '@/screens/wishList/components'; - -export interface WishTabWishlistProps { - places: PlaceCardProps['place'][]; - isLiked: (id: string) => boolean; - onToggleLike: (id: string) => void; -} +import { PlaceCard } from '@/screens/wishList/components'; +import type { WishPlace, WishTabWishlistProps } from '@/screens/wishList/types'; // FlatList 아이템 → 반드시 별도 컴포넌트 + memo const WishlistPlaceItem = React.memo<{ - item: PlaceCardProps['place']; - isLiked: boolean; - onToggleLike: (id: string) => void; + item: WishPlace; + isLiked: boolean; + onToggleLike: (id: string) => void; }>(({ item, isLiked, onToggleLike }) => ( - + )); WishlistPlaceItem.displayName = 'WishlistPlaceItem'; // 빈 상태 — 변하지 않으므로 memo로 완전히 고정 const WishlistEmptyState = React.memo(() => ( - - - - - 위시리스트에 장소가 없어요 - - 가고싶은 장소를 위시리스트에 추가해주세요. - + + + + 위시리스트에 장소가 없어요 + + 가고싶은 장소를 위시리스트에 추가해주세요. + + )); WishlistEmptyState.displayName = 'WishlistEmptyState'; -export const WishTabWishlist = React.memo(({ - places, - isLiked, - onToggleLike, -}) => { +export const WishTabWishlist = React.memo( + ({ places, isLiked, onToggleLike }) => { const renderItem = useCallback( - ({ item }: { item: PlaceCardProps['place'] }) => ( - - ), - [isLiked, onToggleLike], + ({ item }: { item: WishPlace }) => ( + + ), + [isLiked, onToggleLike], ); - const keyExtractor = useCallback( - (item: PlaceCardProps['place']) => `wishlist-${item.id}`, - [], - ); + const keyExtractor = useCallback((item: WishPlace) => `wishlist-${item.id}`, []); if (places.length === 0) { - return ; + return ; } return ( - + ); -}); + }, +); -WishTabWishlist.displayName = 'WishTabWishlist'; \ No newline at end of file +WishTabWishlist.displayName = 'WishTabWishlist'; diff --git a/src/screens/wishList/index.ts b/src/screens/wishList/index.ts index 14f01d0..7c9af15 100644 --- a/src/screens/wishList/index.ts +++ b/src/screens/wishList/index.ts @@ -1,2 +1,3 @@ // DestinationDetail 스크린 전용 서브컴포넌트를 여기에 export 합니다. export * from './components'; +export type * from './types'; diff --git a/src/screens/wishList/types.ts b/src/screens/wishList/types.ts new file mode 100644 index 0000000..b63466f --- /dev/null +++ b/src/screens/wishList/types.ts @@ -0,0 +1,97 @@ +import type { ImageSourcePropType } from 'react-native'; +import type { TextInput } from 'react-native'; +import type { SharedValue } from 'react-native-reanimated'; + +export type WishlistBottomSheetTabId = 'trending' | 'saved' | 'wishlist'; + +export interface WishPlace { + id: string; + title: string; + location?: string; + description: string; + image: ImageSourcePropType; + categories?: string[]; +} + +export interface CategoryChipProps { + label: string; + onPress?: () => void; + isSelected?: boolean; + className?: string; + textClassName?: string; +} + +export interface WishContentContainerProps { + children: React.ReactNode; + className?: string; +} + +export interface PlaceCardProps { + place: WishPlace; + isLiked: boolean; + onToggleLike: (id: string) => void; + isTrending?: boolean; +} + +export interface WishTabTrendingProps { + places: WishPlace[]; +} + +export interface WishTabSaveProps { + places: WishPlace[]; + isLiked: (id: string) => boolean; + onToggleLike: (id: string) => void; +} + +export interface WishTabWishlistProps { + places: WishPlace[]; + isLiked: (id: string) => boolean; + onToggleLike: (id: string) => void; +} + +export interface WishModalProps { + isVisible: boolean; + onClose: () => void; + icon?: React.ReactNode; + ModalIcon?: string; + title: string; + showCloseButton: boolean; + buttonContainerClass?: string; + primaryLabel: string; + ModalContainer: string; + onPrimaryPress: () => void; + primaryBtnClass?: string; + primaryIcon?: React.ReactNode; + primaryTextClass?: string; + primaryTitleTextClass?: string; + secondaryLabel: string; + onSecondaryPress: () => void; + secondaryBtnClass?: string; + secondaryTextClass?: string; +} + +export interface WishlistBottomSheetTab { + id: WishlistBottomSheetTabId; + label: string; +} + +export interface WishlistBottomSheetProps { + translateY: SharedValue; + onStateChange: (expanded: boolean) => void; + maxTopSnap?: number; + tabs: WishlistBottomSheetTab[]; + selectedCategory: WishlistBottomSheetTabId; + onSelectCategory: (tabId: WishlistBottomSheetTabId) => void; + onPressComplete: () => void; + renderTabContent: () => React.ReactNode; +} + +export interface WishlistSearchBarProps { + searchInputRef: React.RefObject; + searchQuery: string; + onChangeText: (text: string) => void; + onFocus: () => void; + onBlur: () => void; + onFocusInput: () => void; + onPressBack: () => void; +} From 0842eb98e36bbd48d51f839fbc26b68a7384ebf5 Mon Sep 17 00:00:00 2001 From: Kimbosung521 Date: Wed, 8 Apr 2026 00:42:55 +0900 Subject: [PATCH 3/8] =?UTF-8?q?fix:PLI-19(=EA=B2=80=EC=83=89=EC=B0=BD=20?= =?UTF-8?q?=EB=B0=B0=EA=B2=BD=20css=EC=88=98=EC=A0=95)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/WishlistSearchOverlay.tsx | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/src/screens/wishList/components/WishlistSearchOverlay.tsx b/src/screens/wishList/components/WishlistSearchOverlay.tsx index 1f0a5d1..5a71a74 100644 --- a/src/screens/wishList/components/WishlistSearchOverlay.tsx +++ b/src/screens/wishList/components/WishlistSearchOverlay.tsx @@ -20,17 +20,15 @@ export const WishlistSearchOverlay = React.memo( return ( - + style={{ + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + zIndex: 40, + backgroundColor: 'white', + }}> {places.map((place) => ( From 192156460469631dfa000c3e9ccc9486800546a0 Mon Sep 17 00:00:00 2001 From: Kimbosung521 Date: Thu, 9 Apr 2026 00:41:50 +0900 Subject: [PATCH 4/8] =?UTF-8?q?fix:PLI-19(=EA=B2=80=EC=83=89=EC=B0=BD=20cs?= =?UTF-8?q?s=20=EC=88=98=EC=A0=95)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/WishlistSearchOverlay.tsx | 44 ++++++++++++------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/src/screens/wishList/components/WishlistSearchOverlay.tsx b/src/screens/wishList/components/WishlistSearchOverlay.tsx index 5a71a74..e7cf8da 100644 --- a/src/screens/wishList/components/WishlistSearchOverlay.tsx +++ b/src/screens/wishList/components/WishlistSearchOverlay.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { View } from 'react-native'; +import { Animated, View } from 'react-native'; import PlaceCard from './PlaceCard'; import type { PlaceCardProps, WishlistBottomSheetTabId } from '@/screens/wishList/components'; import { ScrollView } from 'react-native-gesture-handler'; @@ -14,12 +14,19 @@ interface WishlistSearchOverlayProps { export const WishlistSearchOverlay = React.memo( ({ isVisible, selectedCategory, places, isLiked, onToggleLike }) => { - if (!isVisible) { - return null; - } + const animatedOpacity = React.useRef(new Animated.Value(isVisible ? 1 : 0)).current; + + React.useEffect(() => { + Animated.timing(animatedOpacity, { + toValue: isVisible ? 1 : 0, + duration: 180, + useNativeDriver: true, + }).start(); + }, [animatedOpacity, isVisible]); return ( - ( bottom: 0, zIndex: 40, backgroundColor: 'white', + opacity: animatedOpacity, }}> - - - {places.map((place) => ( - - ))} - - + + + + {places.map((place) => ( + + ))} + + + ); }, ); From 79bd07819fe4c2252be6cef42fdb3c6404dc9539 Mon Sep 17 00:00:00 2001 From: Kimbosung521 Date: Thu, 9 Apr 2026 11:59:10 +0900 Subject: [PATCH 5/8] =?UTF-8?q?fix:PLI-19(=EC=A0=9C=EB=AF=B8=EB=82=98?= =?UTF-8?q?=EC=9D=B4=20=ED=94=BC=EB=93=9C=EB=B0=B1=20=EC=88=98=EC=A0=95)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/screens/WishlistScreen.tsx | 38 ++++++++---------- src/screens/wishList/components/WishModal.tsx | 14 +++---- .../components/WishlistBottomSheet.tsx | 15 ++++--- .../wishList/components/WishlistSearchBar.tsx | 20 ++++++---- .../components/WishlistSearchOverlay.tsx | 39 ++++++++++--------- .../wishList/components/tab/WishTabSave.tsx | 38 ++++++++---------- .../components/tab/WishTabTrending.tsx | 38 +++++++----------- .../components/tab/WishTabWishlist.tsx | 39 ++++++++----------- src/screens/wishList/types.ts | 1 + 9 files changed, 112 insertions(+), 130 deletions(-) diff --git a/src/screens/WishlistScreen.tsx b/src/screens/WishlistScreen.tsx index 27c957a..17a5a12 100644 --- a/src/screens/WishlistScreen.tsx +++ b/src/screens/WishlistScreen.tsx @@ -25,7 +25,6 @@ import type { WishlistBottomSheetTabId } from '@/screens/wishList/components'; import Animated, { useSharedValue, useAnimatedStyle, - useAnimatedReaction, interpolate, withTiming, Easing, @@ -150,12 +149,11 @@ const WishlistScreen: React.FC = () => { const SECOND_SNAP_VISIBLE_HEIGHT = 310; const INITIAL_CATEGORY: TabId = 'trending'; const SNAP_LOW = SHEET_HEIGHT - 28; - const SNAP_FULL = 0; + const SNAP_FULL = 15; const SNAP_TRENDING = SHEET_HEIGHT - SECOND_SNAP_VISIBLE_HEIGHT; const SEARCH_BUTTON_BOTTOM = BOTTOM_SHEET_MIN_HEIGHT + 10; const translateY = useSharedValue(SNAP_LOW); - const isTrendingTabSV = useSharedValue(INITIAL_CATEGORY === 'trending'); const [likedIdsByTab, setLikedIdsByTab] = useState>>(() => ({ saved: new Set(), @@ -193,9 +191,6 @@ const WishlistScreen: React.FC = () => { useEffect(() => { showExitModalRef.current = showExitModal; }, [showExitModal]); - useEffect(() => { - isTrendingTabSV.value = selectedCategory === 'trending'; - }, [isTrendingTabSV, selectedCategory]); // 바텀시트 애니메이션 함수 const animateSheetTo = useCallback( (targetY: number) => { @@ -274,16 +269,6 @@ const WishlistScreen: React.FC = () => { const targetY = selectedCategory === 'trending' ? SNAP_TRENDING : SNAP_FULL; animateSheetTo(targetY); }, [selectedCategory, animateSheetTo, SNAP_TRENDING, SNAP_FULL]); - - useAnimatedReaction( - () => ({ y: translateY.value, isTrending: isTrendingTabSV.value }), - ({ y, isTrending }, prev) => { - const isMovingUp = y < (prev?.y ?? y); - if (isTrending && isMovingUp && y < SNAP_TRENDING) { - translateY.value = SNAP_TRENDING; - } - }, - ); // 바텀시트 애니메이션 스타일 const mapUIAnimatedStyle = useAnimatedStyle(() => { 'worklet'; @@ -326,7 +311,12 @@ const WishlistScreen: React.FC = () => { const renderTabContent = () => { switch (selectedCategory) { case 'trending': - return ; + return ( + toggleLike('wishlist', id)} + /> + ); case 'saved': return ( { label="현 지도에서 검색" isSelected={true} textClassName="text-p1" - className="px-[29px] py-[10px] rounded-full" + className="rounded-full px-[29px] py-[10px]" /> - + - + @@ -427,8 +417,12 @@ const WishlistScreen: React.FC = () => { isVisible={isSearchFocused} selectedCategory={selectedCategory} places={SEARCH_PLACES} - isLiked={(id) => isLikedInTab('wishlist', id)} - onToggleLike={(id) => toggleLike('wishlist', id)} + isLiked={(id) => + isLikedInTab(selectedCategory === 'trending' ? 'wishlist' : selectedCategory, id) + } + onToggleLike={(id) => + toggleLike(selectedCategory === 'trending' ? 'wishlist' : selectedCategory, id) + } /> {/* 완료 모달 */} diff --git a/src/screens/wishList/components/WishModal.tsx b/src/screens/wishList/components/WishModal.tsx index 8ce715f..822469a 100644 --- a/src/screens/wishList/components/WishModal.tsx +++ b/src/screens/wishList/components/WishModal.tsx @@ -28,22 +28,22 @@ export const WishModal = React.memo( return ( e.stopPropagation()} className="w-full"> - + {/* 아이콘 영역 */} {icon && {icon}} {/* X 버튼 */} {showCloseButton ? ( - + ) : null} {/* 타이틀 영역 */} + className={`text-center font-pretendardSemiBold text-h2 text-black ${primaryTitleTextClass}`}> {title} @@ -52,7 +52,7 @@ export const WishModal = React.memo( {/* Primary 버튼 */} + className={`flex-row items-center justify-center rounded-lg ${primaryBtnClass}`}> {primaryIcon && {primaryIcon}} @@ -61,9 +61,7 @@ export const WishModal = React.memo( {/* Secondary 버튼 */} - + {secondaryLabel} diff --git a/src/screens/wishList/components/WishlistBottomSheet.tsx b/src/screens/wishList/components/WishlistBottomSheet.tsx index 879fc76..3cc016d 100644 --- a/src/screens/wishList/components/WishlistBottomSheet.tsx +++ b/src/screens/wishList/components/WishlistBottomSheet.tsx @@ -3,7 +3,7 @@ import { ScrollView, View } from 'react-native'; import CustomBottomSheet from '@/components/ui/CustomBottomSheet'; import { CategoryChip } from '@/screens/wishList/components'; import type { WishlistBottomSheetProps } from '@/screens/wishList/types'; - +import { useSafeAreaInsets } from 'react-native-safe-area-context'; const WishlistBottomSheetComponent: React.FC = ({ translateY, onStateChange, @@ -13,13 +13,16 @@ const WishlistBottomSheetComponent: React.FC = ({ onSelectCategory, onPressComplete, renderTabContent, + sheetHeight, }) => { + const insets = useSafeAreaInsets(); return ( - + maxTopSnap={maxTopSnap} + height={sheetHeight}> + {tabs.map((tab) => ( = ({ label={tab.label} onPress={() => onSelectCategory(tab.id)} isSelected={selectedCategory === tab.id} - className={`mr-2 px-4 py-2 rounded-2xl ${selectedCategory === tab.id ? 'bg-main' : 'bg-chip'}`} + className={`mr-2 rounded-2xl px-4 py-2 ${selectedCategory === tab.id ? 'bg-main' : 'bg-chip'}`} /> ))} @@ -35,11 +38,11 @@ const WishlistBottomSheetComponent: React.FC = ({ label="완료" onPress={onPressComplete} isSelected={true} - className="px-4 py-2 bg-main rounded-2xl" + className="rounded-2xl bg-main px-4 py-2" /> - + ( ({ searchInputRef, searchQuery, onChangeText, onFocus, onBlur, onFocusInput, onPressBack }) => { return ( - - - + + + ( onFocus={onFocus} onBlur={onBlur} /> - + - + - + ); }, ); diff --git a/src/screens/wishList/components/WishlistSearchOverlay.tsx b/src/screens/wishList/components/WishlistSearchOverlay.tsx index e7cf8da..39bd505 100644 --- a/src/screens/wishList/components/WishlistSearchOverlay.tsx +++ b/src/screens/wishList/components/WishlistSearchOverlay.tsx @@ -1,5 +1,6 @@ import React from 'react'; -import { Animated, View } from 'react-native'; +import { View } from 'react-native'; +import Animated, { useSharedValue, useAnimatedStyle, withTiming } from 'react-native-reanimated'; import PlaceCard from './PlaceCard'; import type { PlaceCardProps, WishlistBottomSheetTabId } from '@/screens/wishList/components'; import { ScrollView } from 'react-native-gesture-handler'; @@ -14,32 +15,34 @@ interface WishlistSearchOverlayProps { export const WishlistSearchOverlay = React.memo( ({ isVisible, selectedCategory, places, isLiked, onToggleLike }) => { - const animatedOpacity = React.useRef(new Animated.Value(isVisible ? 1 : 0)).current; + const animatedOpacity = useSharedValue(isVisible ? 1 : 0); React.useEffect(() => { - Animated.timing(animatedOpacity, { - toValue: isVisible ? 1 : 0, - duration: 180, - useNativeDriver: true, - }).start(); + animatedOpacity.value = withTiming(isVisible ? 1 : 0, { duration: 180 }); }, [animatedOpacity, isVisible]); + const animatedStyle = useAnimatedStyle(() => ({ + opacity: animatedOpacity.value, + })); + return ( + style={[ + { + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + zIndex: 40, + backgroundColor: 'white', + }, + animatedStyle, + ]}> - + {places.map((place) => ( ( - + - 저장한 장소가 없어요 + 저장한 장소가 없어요 - + 마음에 드는 장소를 저장해주세요. @@ -31,27 +31,21 @@ const WishSaveEmptyState = React.memo(() => ( WishSaveEmptyState.displayName = 'WishSaveEmptyState'; export const WishTabSave = React.memo(({ places, isLiked, onToggleLike }) => { - const renderItem = useCallback( - ({ item }: { item: WishPlace }) => ( - - ), - [isLiked, onToggleLike], - ); - - const keyExtractor = useCallback((item: WishPlace) => `saved-${item.id}`, []); - if (places.length === 0) { return ; } + return ( - + + {places.map((item) => ( + + ))} + ); }); diff --git a/src/screens/wishList/components/tab/WishTabTrending.tsx b/src/screens/wishList/components/tab/WishTabTrending.tsx index d01a92f..1ebe9f7 100644 --- a/src/screens/wishList/components/tab/WishTabTrending.tsx +++ b/src/screens/wishList/components/tab/WishTabTrending.tsx @@ -1,35 +1,27 @@ -import React, { useCallback } from 'react'; -import { View, Text, FlatList } from 'react-native'; +import React from 'react'; +import { View, Text } from 'react-native'; import { PlaceCard } from '@/screens/wishList/components'; import type { WishPlace, WishTabTrendingProps } from '@/screens/wishList/types'; -// FlatList renderItem → 반드시 별도 컴포넌트 + memo (스크롤 성능) -const TrendingPlaceItem = React.memo<{ item: WishPlace }>(({ item }) => ( - {}} isTrending={true} /> -)); +// .map() 사용 — 아이템 수가 적으므로 가상화 불필요 +const TrendingPlaceItem = React.memo<{ item: WishPlace; onToggleLike: (id: string) => void }>( + ({ item, onToggleLike }) => ( + + ), +); TrendingPlaceItem.displayName = 'TrendingPlaceItem'; -export const WishTabTrending = React.memo(({ places }) => { - const renderItem = useCallback( - ({ item }: { item: WishPlace }) => , - [], - ); - - const keyExtractor = useCallback((item: WishPlace) => `trending-${item.id}`, []); - +export const WishTabTrending = React.memo(({ places, onToggleLike }) => { return ( <> - 현재 핫한 장소 + 현재 핫한 장소 + + + {places.map((item) => ( + + ))} - ); }); diff --git a/src/screens/wishList/components/tab/WishTabWishlist.tsx b/src/screens/wishList/components/tab/WishTabWishlist.tsx index f04b88e..8f89b6f 100644 --- a/src/screens/wishList/components/tab/WishTabWishlist.tsx +++ b/src/screens/wishList/components/tab/WishTabWishlist.tsx @@ -1,10 +1,10 @@ -import React, { useCallback } from 'react'; -import { View, Text, FlatList } from 'react-native'; +import React from 'react'; +import { View, Text } from 'react-native'; import { EmptyWish } from '@/assets/icons'; import { PlaceCard } from '@/screens/wishList/components'; import type { WishPlace, WishTabWishlistProps } from '@/screens/wishList/types'; -// FlatList 아이템 → 반드시 별도 컴포넌트 + memo +// FlatList 아이템 → .map()으로 간단히 렌더링 const WishlistPlaceItem = React.memo<{ item: WishPlace; isLiked: boolean; @@ -16,12 +16,12 @@ WishlistPlaceItem.displayName = 'WishlistPlaceItem'; // 빈 상태 — 변하지 않으므로 memo로 완전히 고정 const WishlistEmptyState = React.memo(() => ( - + - 위시리스트에 장소가 없어요 - + 위시리스트에 장소가 없어요 + 가고싶은 장소를 위시리스트에 추가해주세요. @@ -30,28 +30,21 @@ WishlistEmptyState.displayName = 'WishlistEmptyState'; export const WishTabWishlist = React.memo( ({ places, isLiked, onToggleLike }) => { - const renderItem = useCallback( - ({ item }: { item: WishPlace }) => ( - - ), - [isLiked, onToggleLike], - ); - - const keyExtractor = useCallback((item: WishPlace) => `wishlist-${item.id}`, []); - if (places.length === 0) { return ; } return ( - + + {places.map((item) => ( + + ))} + ); }, ); diff --git a/src/screens/wishList/types.ts b/src/screens/wishList/types.ts index b63466f..1184201 100644 --- a/src/screens/wishList/types.ts +++ b/src/screens/wishList/types.ts @@ -35,6 +35,7 @@ export interface PlaceCardProps { export interface WishTabTrendingProps { places: WishPlace[]; + onToggleLike: (id: string) => void; } export interface WishTabSaveProps { From b3778f14e5e2855c23212c18da7f3272e7bf20e6 Mon Sep 17 00:00:00 2001 From: Kimbosung521 Date: Thu, 9 Apr 2026 15:31:33 +0900 Subject: [PATCH 6/8] =?UTF-8?q?fix:=20PLI-19(=EA=B2=80=EC=83=89=EC=B0=BD?= =?UTF-8?q?=20=ED=82=A4=EB=B3=B4=EB=93=9C=20=EC=88=98=EC=A0=95)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- android/app/src/main/AndroidManifest.xml | 2 +- src/screens/WishlistScreen.tsx | 67 +++++++++++++++++-- .../wishList/components/WishlistSearchBar.tsx | 47 +++++++------ .../components/WishlistSearchOverlay.tsx | 52 ++++++++++---- 4 files changed, 123 insertions(+), 45 deletions(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 935a500..cec492c 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -22,7 +22,7 @@ android:label="@string/app_name" android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode" android:launchMode="singleTask" - android:windowSoftInputMode="adjustResize" + android:windowSoftInputMode="adjustPan" android:exported="true"> diff --git a/src/screens/WishlistScreen.tsx b/src/screens/WishlistScreen.tsx index 17a5a12..47a9531 100644 --- a/src/screens/WishlistScreen.tsx +++ b/src/screens/WishlistScreen.tsx @@ -1,5 +1,5 @@ import React, { useCallback, useRef, useEffect } from 'react'; -import { View, TextInput, TouchableOpacity, Text, Keyboard } from 'react-native'; +import { View, TextInput, TouchableOpacity, Text, Keyboard, Platform } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { useNavigation, useFocusEffect } from '@react-navigation/native'; import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; @@ -181,6 +181,9 @@ const WishlistScreen: React.FC = () => { const [isSheetExpanded, setIsSheetExpanded] = useState(false); const [searchQuery, setSearchQuery] = useState(''); const searchInputRef = useRef(null); + const refocusRafRef = useRef(null); + const isSearchFocusedRef = useRef(false); + const isKeyboardVisibleRef = useRef(false); const isInitialTabEffect = useRef(true); const showAddModalRef = useRef(false); const showExitModalRef = useRef(false); @@ -191,6 +194,35 @@ const WishlistScreen: React.FC = () => { useEffect(() => { showExitModalRef.current = showExitModal; }, [showExitModal]); + + useEffect(() => { + isSearchFocusedRef.current = isSearchFocused; + }, [isSearchFocused]); + + useEffect(() => { + const showSub = Keyboard.addListener('keyboardDidShow', () => { + isKeyboardVisibleRef.current = true; + }); + const hideSub = Keyboard.addListener('keyboardDidHide', () => { + isKeyboardVisibleRef.current = false; + }); + + return () => { + showSub.remove(); + hideSub.remove(); + }; + }, []); + + const clearPendingRefocus = useCallback(() => { + if (refocusRafRef.current !== null) { + cancelAnimationFrame(refocusRafRef.current); + refocusRafRef.current = null; + } + }, []); + + useEffect(() => { + return () => clearPendingRefocus(); + }, [clearPendingRefocus]); // 바텀시트 애니메이션 함수 const animateSheetTo = useCallback( (targetY: number) => { @@ -215,17 +247,40 @@ const WishlistScreen: React.FC = () => { }, []); // 검색 입력창에 포커스 주기 const focusSearchInput = useCallback(() => { - searchInputRef.current?.focus(); - }, []); + const input = searchInputRef.current; + if (!input) return; + + clearPendingRefocus(); + + if (Platform.OS === 'android' && input.isFocused()) { + if (isKeyboardVisibleRef.current) return; + input.blur(); + refocusRafRef.current = requestAnimationFrame(() => { + refocusRafRef.current = null; + if (!isSearchFocusedRef.current) return; + input.focus(); + }); + return; + } + + input.focus(); + }, [clearPendingRefocus]); // 검색 입력창에 포커스 될 때 → 검색어 상태 업데이트 + 키보드 올리기 - const handleSearchFocus = useCallback(() => setIsSearchFocused(true), []); + const handleSearchFocus = useCallback(() => { + isSearchFocusedRef.current = true; + setIsSearchFocused(true); + }, []); // 검색 입력창에서 포커스 벗어날 때 → 검색어 초기화 + 키보드 내리기 const handleSearchBlur = useCallback(() => { + clearPendingRefocus(); + isSearchFocusedRef.current = false; + isKeyboardVisibleRef.current = false; + searchInputRef.current?.blur(); setIsSearchFocused(false); Keyboard.dismiss(); - }, []); + }, [clearPendingRefocus]); const handleSearchInputBlur = useCallback(() => { - Keyboard.dismiss(); + // Intentionally keep search mode active; only hide keyboard on blur. }, []); // 뒤로가기 버튼 핸들러: 검색 중이면 검색 종료, 그 외에는 모달 열기 const handleGoBack = useCallback(() => { diff --git a/src/screens/wishList/components/WishlistSearchBar.tsx b/src/screens/wishList/components/WishlistSearchBar.tsx index 6ce25d1..d577493 100644 --- a/src/screens/wishList/components/WishlistSearchBar.tsx +++ b/src/screens/wishList/components/WishlistSearchBar.tsx @@ -8,30 +8,29 @@ import type { WishlistSearchBarProps } from '@/screens/wishList/types'; export const WishlistSearchBar = React.memo( ({ searchInputRef, searchQuery, onChangeText, onFocus, onBlur, onFocusInput, onPressBack }) => { return ( - - - - - - - - - - - + + + + + + + + + ); }, ); diff --git a/src/screens/wishList/components/WishlistSearchOverlay.tsx b/src/screens/wishList/components/WishlistSearchOverlay.tsx index 39bd505..a3bb629 100644 --- a/src/screens/wishList/components/WishlistSearchOverlay.tsx +++ b/src/screens/wishList/components/WishlistSearchOverlay.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { View } from 'react-native'; +import { Keyboard, KeyboardAvoidingView, Platform, View } from 'react-native'; import Animated, { useSharedValue, useAnimatedStyle, withTiming } from 'react-native-reanimated'; import PlaceCard from './PlaceCard'; import type { PlaceCardProps, WishlistBottomSheetTabId } from '@/screens/wishList/components'; @@ -16,11 +16,29 @@ interface WishlistSearchOverlayProps { export const WishlistSearchOverlay = React.memo( ({ isVisible, selectedCategory, places, isLiked, onToggleLike }) => { const animatedOpacity = useSharedValue(isVisible ? 1 : 0); + const [keyboardHeight, setKeyboardHeight] = React.useState(0); React.useEffect(() => { animatedOpacity.value = withTiming(isVisible ? 1 : 0, { duration: 180 }); }, [animatedOpacity, isVisible]); + React.useEffect(() => { + const showEvent = Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow'; + const hideEvent = Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide'; + + const showSub = Keyboard.addListener(showEvent, (event) => { + setKeyboardHeight(event.endCoordinates.height); + }); + const hideSub = Keyboard.addListener(hideEvent, () => { + setKeyboardHeight(0); + }); + + return () => { + showSub.remove(); + hideSub.remove(); + }; + }, []); + const animatedStyle = useAnimatedStyle(() => ({ opacity: animatedOpacity.value, })); @@ -40,19 +58,25 @@ export const WishlistSearchOverlay = React.memo( }, animatedStyle, ]}> - - - - {places.map((place) => ( - - ))} - - + + + + + {places.map((place) => ( + + ))} + + + ); }, From b17a73147e873321d2d60fb6937ba2595be22ce5 Mon Sep 17 00:00:00 2001 From: kimbosung521 Date: Fri, 10 Apr 2026 00:26:47 +0900 Subject: [PATCH 7/8] =?UTF-8?q?fix:PLI-19(=EB=B0=94=ED=85=80=EC=8B=9C?= =?UTF-8?q?=ED=8A=B8=20=EB=86=92=EC=9D=B4=20=EC=A1=B0=EC=A0=95)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/screens/WishlistScreen.tsx | 4 ++-- src/screens/wishList/components/WishlistBottomSheet.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/screens/WishlistScreen.tsx b/src/screens/WishlistScreen.tsx index 47a9531..2219603 100644 --- a/src/screens/WishlistScreen.tsx +++ b/src/screens/WishlistScreen.tsx @@ -149,7 +149,7 @@ const WishlistScreen: React.FC = () => { const SECOND_SNAP_VISIBLE_HEIGHT = 310; const INITIAL_CATEGORY: TabId = 'trending'; const SNAP_LOW = SHEET_HEIGHT - 28; - const SNAP_FULL = 15; + const SNAP_FULL = 35; const SNAP_TRENDING = SHEET_HEIGHT - SECOND_SNAP_VISIBLE_HEIGHT; const SEARCH_BUTTON_BOTTOM = BOTTOM_SHEET_MIN_HEIGHT + 10; @@ -460,7 +460,7 @@ const WishlistScreen: React.FC = () => { = ({ onSelectCategory, onPressComplete, renderTabContent, - sheetHeight, + }) => { const insets = useSafeAreaInsets(); return ( @@ -21,7 +21,7 @@ const WishlistBottomSheetComponent: React.FC = ({ translateY={translateY} onStateChange={onStateChange} maxTopSnap={maxTopSnap} - height={sheetHeight}> + > {tabs.map((tab) => ( From 6052fcfc76b1c6e0083913f5a1f23f5373ca9fcd Mon Sep 17 00:00:00 2001 From: kimbosung521 Date: Fri, 10 Apr 2026 02:51:07 +0900 Subject: [PATCH 8/8] =?UTF-8?q?fix:=20PLI-19(=EB=B0=94=ED=85=80=EC=8B=9C?= =?UTF-8?q?=ED=8A=B8=EC=99=80=20=EA=B2=80=EC=83=89=20input=20=EC=B0=BD=20?= =?UTF-8?q?=EA=B2=B9=EC=B9=98=EB=8A=94=20=EA=B2=83=20=EC=88=98=EC=A0=95)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/screens/WishlistScreen.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/screens/WishlistScreen.tsx b/src/screens/WishlistScreen.tsx index 2219603..4ab8f40 100644 --- a/src/screens/WishlistScreen.tsx +++ b/src/screens/WishlistScreen.tsx @@ -456,7 +456,7 @@ const WishlistScreen: React.FC = () => { - + {!isSearchFocused && ( { onPressComplete={handleComplete} renderTabContent={renderTabContent} /> - + )} + {isSearchFocused && ( { toggleLike(selectedCategory === 'trending' ? 'wishlist' : selectedCategory, id) } /> - + )} {/* 완료 모달 */}