diff --git a/app.json b/app.json
index b181a66..aaef54e 100644
--- a/app.json
+++ b/app.json
@@ -19,12 +19,7 @@
"NSMicrophoneUsageDescription": "영상 녹화를 위해 마이크에 접근합니다.",
"ITSAppUsesNonExemptEncryption": false
},
- "buildNumber": "12",
- "splash": {
- "image": "./assets/splash-large.png",
- "resizeMode": "contain",
- "backgroundColor": "#DC6E3F"
- }
+ "buildNumber": "12"
},
"plugins": ["expo-apple-authentication", "expo-video"],
"android": {
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index 230dd55..4423929 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -282,6 +282,13 @@ PODS:
- ExpoModulesCore
- ExpoFont (13.3.2):
- ExpoModulesCore
+ - ExpoImage (2.4.1):
+ - ExpoModulesCore
+ - libavif/libdav1d
+ - SDWebImage (~> 5.21.0)
+ - SDWebImageAVIFCoder (~> 0.11.0)
+ - SDWebImageSVGCoder (~> 1.7.0)
+ - SDWebImageWebPCoder (~> 0.14.6)
- ExpoImagePicker (16.1.4):
- ExpoModulesCore
- ExpoKeepAwake (14.1.4):
@@ -340,6 +347,23 @@ PODS:
- KakaoSDKCommon/Common (= 2.22.0)
- KakaoSDKUser (2.22.0):
- KakaoSDKAuth (= 2.22.0)
+ - libavif/core (0.11.1)
+ - libavif/libdav1d (0.11.1):
+ - libavif/core
+ - libdav1d (>= 0.6.0)
+ - libdav1d (1.2.0)
+ - libwebp (1.5.0):
+ - libwebp/demux (= 1.5.0)
+ - libwebp/mux (= 1.5.0)
+ - libwebp/sharpyuv (= 1.5.0)
+ - libwebp/webp (= 1.5.0)
+ - libwebp/demux (1.5.0):
+ - libwebp/webp
+ - libwebp/mux (1.5.0):
+ - libwebp/demux
+ - libwebp/sharpyuv (1.5.0)
+ - libwebp/webp (1.5.0):
+ - libwebp/sharpyuv
- RCT-Folly (2024.11.18.00):
- boost
- DoubleConversion
@@ -2457,6 +2481,17 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
+ - SDWebImage (5.21.3):
+ - SDWebImage/Core (= 5.21.3)
+ - SDWebImage/Core (5.21.3)
+ - SDWebImageAVIFCoder (0.11.1):
+ - libavif/core (>= 0.11.0)
+ - SDWebImage (~> 5.10)
+ - SDWebImageSVGCoder (1.7.0):
+ - SDWebImage/Core (~> 5.6)
+ - SDWebImageWebPCoder (0.14.6):
+ - libwebp (~> 1.0)
+ - SDWebImage/Core (~> 5.17)
- SocketRocket (0.7.1)
- Yoga (0.0.0)
@@ -2477,6 +2512,7 @@ DEPENDENCIES:
- ExpoAsset (from `../node_modules/expo-asset/ios`)
- ExpoFileSystem (from `../node_modules/expo-file-system/ios`)
- ExpoFont (from `../node_modules/expo-font/ios`)
+ - ExpoImage (from `../node_modules/expo-image/ios`)
- ExpoImagePicker (from `../node_modules/expo-image-picker/ios`)
- ExpoKeepAwake (from `../node_modules/expo-keep-awake/ios`)
- ExpoModulesCore (from `../node_modules/expo-modules-core`)
@@ -2571,6 +2607,13 @@ SPEC REPOS:
- KakaoSDKAuth
- KakaoSDKCommon
- KakaoSDKUser
+ - libavif
+ - libdav1d
+ - libwebp
+ - SDWebImage
+ - SDWebImageAVIFCoder
+ - SDWebImageSVGCoder
+ - SDWebImageWebPCoder
- SocketRocket
EXTERNAL SOURCES:
@@ -2606,6 +2649,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/expo-file-system/ios"
ExpoFont:
:path: "../node_modules/expo-font/ios"
+ ExpoImage:
+ :path: "../node_modules/expo-image/ios"
ExpoImagePicker:
:path: "../node_modules/expo-image-picker/ios"
ExpoKeepAwake:
@@ -2796,6 +2841,7 @@ SPEC CHECKSUMS:
ExpoAsset: ef06e880126c375f580d4923fdd1cdf4ee6ee7d6
ExpoFileSystem: 7f92f7be2f5c5ed40a7c9efc8fa30821181d9d63
ExpoFont: cf508bc2e6b70871e05386d71cab927c8524cc8e
+ ExpoImage: b0b4a838c62bb9d5438bb475cccc85619a5f59dc
ExpoImagePicker: 0963da31800c906e01c03e25d7c849f16ebf02a2
ExpoKeepAwake: bf0811570c8da182bfb879169437d4de298376e7
ExpoModulesCore: 00a1b5c73248465bd0b93f59f8538c4573dac579
@@ -2811,6 +2857,9 @@ SPEC CHECKSUMS:
KakaoSDKAuth: 569b377eda622d93d4575240b8031cd658163eef
KakaoSDKCommon: d57127c339fc79e73aa8b236a4c77211c29924f1
KakaoSDKUser: 043bcd7e91454ebf3bf64f150c430e6f65f0a08d
+ libavif: 84bbb62fb232c3018d6f1bab79beea87e35de7b7
+ libdav1d: 23581a4d8ec811ff171ed5e2e05cd27bad64c39f
+ libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
RCT-Folly: e78785aa9ba2ed998ea4151e314036f6c49e6d82
RCTDeprecation: 5f638f65935e273753b1f31a365db6a8d6dc53b5
RCTRequired: 8b46a520ea9071e2bc47d474aa9ca31b4a935bd8
@@ -2883,6 +2932,10 @@ SPEC CHECKSUMS:
RNScreens: f0678748c5310b49a3f920f1485f5ec477afd345
RNSVG: 794f269526df9ddc1f79b3d1a202b619df0368e3
RNWorklets: c8214ac73c6bc6181f4564a9bcafba1db7ed0c44
+ SDWebImage: 16309af6d214ba3f77a7c6f6fdda888cb313a50a
+ SDWebImageAVIFCoder: afe194a084e851f70228e4be35ef651df0fc5c57
+ SDWebImageSVGCoder: 15a300a97ec1c8ac958f009c02220ac0402e936c
+ SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
Yoga: adb397651e1c00672c12e9495babca70777e411e
diff --git a/ios/zipbap.xcodeproj/project.pbxproj b/ios/zipbap.xcodeproj/project.pbxproj
index f27bd9a..f1ed4ab 100644
--- a/ios/zipbap.xcodeproj/project.pbxproj
+++ b/ios/zipbap.xcodeproj/project.pbxproj
@@ -275,6 +275,7 @@
"${PODS_CONFIGURATION_BUILD_DIR}/RNSVG/RNSVGFilters.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/React-Core/React-Core_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/React-cxxreact/React-cxxreact_privacy.bundle",
+ "${PODS_CONFIGURATION_BUILD_DIR}/SDWebImage/SDWebImage.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/boost/boost_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/expo-dev-launcher/EXDevLauncher.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/expo-dev-menu/EXDevMenu.bundle",
@@ -292,6 +293,7 @@
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNSVGFilters.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-Core_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-cxxreact_privacy.bundle",
+ "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/SDWebImage.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/boost_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXDevLauncher.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXDevMenu.bundle",
diff --git a/ios/zipbap/Info.plist b/ios/zipbap/Info.plist
index 19b22fc..0949aac 100644
--- a/ios/zipbap/Info.plist
+++ b/ios/zipbap/Info.plist
@@ -38,7 +38,7 @@
CFBundleVersion
- 15
+ 19
ITSAppUsesNonExemptEncryption
KAKAO_APP_KEY
diff --git a/package.json b/package.json
index e66db12..942d32f 100644
--- a/package.json
+++ b/package.json
@@ -38,6 +38,7 @@
"expo": "~53.0.22",
"expo-apple-authentication": "~7.2.4",
"expo-dev-client": "~5.2.4",
+ "expo-image": "~2.4.1",
"expo-image-picker": "^16.1.4",
"expo-network": "~7.1.5",
"expo-status-bar": "~2.2.3",
diff --git a/src/app/AndroidSplashScreen.tsx b/src/app/AndroidSplashScreen.tsx
index 705ef38..8ac8bbb 100644
--- a/src/app/AndroidSplashScreen.tsx
+++ b/src/app/AndroidSplashScreen.tsx
@@ -1,5 +1,6 @@
+import { Image } from 'expo-image';
import React from 'react';
-import { View, Image, Dimensions } from 'react-native';
+import { View, Dimensions } from 'react-native';
import SplashImg from '@/assets/img/splash-large.png';
const { width } = Dimensions.get('window');
@@ -15,7 +16,8 @@ const AndroidSplashScreen = () => {
position: 'relative',
top: -20,
}}
- resizeMode="contain"
+ contentFit="contain"
+ cachePolicy="memory-disk"
/>
);
diff --git a/src/app/Navigation.tsx b/src/app/Navigation.tsx
index d07a0e1..fda6f76 100644
--- a/src/app/Navigation.tsx
+++ b/src/app/Navigation.tsx
@@ -43,11 +43,7 @@ export function Navigation() {
component={RecipeCreateForm}
options={{ headerShown: false }}
/>
- }}
- />
+
{/* NOTE: 모달 페이지 관리 */}
+ }}
+ />
diff --git a/src/entities/recipe/api/useRecipeDelete.ts b/src/entities/recipe/api/useRecipeDelete.ts
index 277dc84..be0dbc2 100644
--- a/src/entities/recipe/api/useRecipeDelete.ts
+++ b/src/entities/recipe/api/useRecipeDelete.ts
@@ -9,6 +9,7 @@ export const useRecipeDelete = () => {
queryClient.invalidateQueries({ queryKey: queryKeys.recipeTemp.all });
queryClient.invalidateQueries({ queryKey: queryKeys.recipeFinal.all });
queryClient.invalidateQueries({ queryKey: queryKeys.recipes.all });
+ queryClient.invalidateQueries({ queryKey: queryKeys.feed.all });
};
return useMutation({
mutationFn: async (recipeId: string) => {
diff --git a/src/entities/recipe/ui/ArticleView.tsx b/src/entities/recipe/ui/ArticleView.tsx
index c7b794e..68b8b21 100644
--- a/src/entities/recipe/ui/ArticleView.tsx
+++ b/src/entities/recipe/ui/ArticleView.tsx
@@ -1,4 +1,5 @@
-import { View, Image, Text, Pressable } from 'react-native';
+import { Image } from 'expo-image';
+import { View, Text, Pressable } from 'react-native';
import TimerIcon from '@/assets/img/recipe/timeer.svg';
@@ -18,7 +19,11 @@ const ArticleView = ({ item, navigate }: Props) => {
onPress={() => navigate(item.id)}
className="mb-[33px] flex-row gap-4 rounded-xl bg-white"
>
-
+
{/* 서브타이틀, 요리시간 */}
diff --git a/src/entities/recipe/ui/DetailDeleteComponent.tsx b/src/entities/recipe/ui/DetailDeleteComponent.tsx
index 6441b04..632d259 100644
--- a/src/entities/recipe/ui/DetailDeleteComponent.tsx
+++ b/src/entities/recipe/ui/DetailDeleteComponent.tsx
@@ -1,12 +1,27 @@
-import { Pressable } from 'react-native';
-
+import { Pressable, Alert } from 'react-native';
import TrashIcon from '@/assets/img/recipe/trash-slide.svg';
import { useRecipeDelete } from '@entities/recipe';
const DetailDeleteComponent = ({ targetId }: { targetId: string }) => {
const { mutate: deleteRecipe } = useRecipeDelete();
+
const handleDelete = () => {
- deleteRecipe(targetId);
+ Alert.alert(
+ '레시피 삭제',
+ '정말 삭제하시겠습니까?',
+ [
+ {
+ text: '취소',
+ style: 'cancel',
+ },
+ {
+ text: '삭제',
+ style: 'destructive',
+ onPress: () => deleteRecipe(targetId),
+ },
+ ],
+ { cancelable: true },
+ );
};
return (
diff --git a/src/entities/recipe/ui/FeedView.tsx b/src/entities/recipe/ui/FeedView.tsx
index 5cb816b..0fff071 100644
--- a/src/entities/recipe/ui/FeedView.tsx
+++ b/src/entities/recipe/ui/FeedView.tsx
@@ -1,4 +1,5 @@
-import { Image, Text, Pressable } from 'react-native';
+import { Image } from 'expo-image';
+import { Text, Pressable } from 'react-native';
import { Recipe } from '../model';
@@ -10,7 +11,11 @@ interface Props {
const FeedView = ({ item, navigate }: Props) => {
return (
navigate(item.id)} className="mb-[33px] flex-col">
-
+
{item.title}
diff --git a/src/entities/recipe/ui/ImageView.tsx b/src/entities/recipe/ui/ImageView.tsx
index 5f0164d..e967f82 100644
--- a/src/entities/recipe/ui/ImageView.tsx
+++ b/src/entities/recipe/ui/ImageView.tsx
@@ -1,4 +1,5 @@
-import { View, Image, Text, Pressable } from 'react-native';
+import { Image } from 'expo-image';
+import { View, Text, Pressable } from 'react-native';
import { Recipe } from '../model';
@@ -13,7 +14,11 @@ const ImageView = ({ item, navigate }: Props) => {
className="mb-[12px] flex-1 flex-col items-start gap-2"
onPress={() => navigate(item.id)}
>
-
+
{item.title}
diff --git a/src/entities/user/ui/AnotherUserHeader.tsx b/src/entities/user/ui/AnotherUserHeader.tsx
index 95f061c..b9a1aac 100644
--- a/src/entities/user/ui/AnotherUserHeader.tsx
+++ b/src/entities/user/ui/AnotherUserHeader.tsx
@@ -1,12 +1,16 @@
+import { useNavigation } from '@react-navigation/native';
import React, { useState } from 'react';
import { TouchableOpacity } from 'react-native';
-
import AlarmOffSvgIcon from '@/assets/img/alarm-off.svg';
import AlarmOnSvgIcon from '@/assets/img/alarm-on.svg';
+import BackIcon from '@/assets/img/back-icon.svg';
import HeaderLogo from '@/assets/img/header-logo.svg';
+import { RootNavigationProp } from '@shared/types';
import { Header } from '@shared/ui';
const AnotherUserHeader = () => {
+ const navigation = useNavigation>();
+
const [alarmOn, setAlarmOn] = useState(false); // false면 Off, true면 On
const toggleAlarm = () => {
@@ -16,6 +20,15 @@ const AnotherUserHeader = () => {
return (
{
+ navigation.goBack();
+ }}
+ >
+
+
+ }
center={}
right={
diff --git a/src/features/chat/ui/ChatInput.tsx b/src/features/chat/ui/ChatInput.tsx
index 9e93084..4bad1b9 100644
--- a/src/features/chat/ui/ChatInput.tsx
+++ b/src/features/chat/ui/ChatInput.tsx
@@ -1,5 +1,6 @@
+import { Image } from 'expo-image';
import React, { forwardRef, useState } from 'react';
-import { View, TextInput, Pressable, Image, Text, TouchableWithoutFeedback } from 'react-native';
+import { View, TextInput, Pressable, Text, TouchableWithoutFeedback } from 'react-native';
import NoneProfileImgSvg from '@/assets/img/none-profile-img.svg';
import SendButtonIconSvg from '@/assets/img/send-button-icon.svg';
import { useUserStore } from '@shared/store';
@@ -20,7 +21,11 @@ const ChatInput = forwardRef(
{userProfile ? (
-
+
) : (
)}
diff --git a/src/features/chat/ui/CommentItem.tsx b/src/features/chat/ui/CommentItem.tsx
index b1e564b..afb7655 100644
--- a/src/features/chat/ui/CommentItem.tsx
+++ b/src/features/chat/ui/CommentItem.tsx
@@ -1,5 +1,6 @@
+import { Image } from 'expo-image';
import React, { useState } from 'react';
-import { View, Text, Image, Pressable } from 'react-native';
+import { View, Text, Pressable } from 'react-native';
import NoneProfileImgSvg from '@/assets/img/none-profile-img.svg';
import { Comment } from '@entities/comment';
import { getTimeAgo } from '@shared/lib/getTimeAgo';
@@ -19,7 +20,8 @@ const CommentItem = ({ comment, onReplyPress, depth = 1 }: Props) => {
{comment.profileImage ? (
) : (
diff --git a/src/features/feed/ui/FeedCard.tsx b/src/features/feed/ui/FeedCard.tsx
index 340d3c7..e89ce45 100644
--- a/src/features/feed/ui/FeedCard.tsx
+++ b/src/features/feed/ui/FeedCard.tsx
@@ -1,5 +1,6 @@
+import { Image } from 'expo-image';
import React from 'react';
-import { View, Text, Image, Pressable, TouchableOpacity } from 'react-native';
+import { View, Text, Pressable, TouchableOpacity } from 'react-native';
import ClockSvg from '@/assets/img/feed/clock-icon.svg';
import StarSvg from '@/assets/img/feed/star-icon.svg';
import NoneUserSvg from '@/assets/img/none-profile-img.svg';
@@ -33,7 +34,8 @@ const FeedCard = ({ feed, navigation }: Props) => {
{feed.profileImage ? (
) : (
@@ -50,7 +52,8 @@ const FeedCard = ({ feed, navigation }: Props) => {
{/* 대표 사진 */}
{/* 요리 소개 */}
diff --git a/src/features/feed/ui/FeedModalImageViewer.tsx b/src/features/feed/ui/FeedModalImageViewer.tsx
index 084b637..c8fa193 100644
--- a/src/features/feed/ui/FeedModalImageViewer.tsx
+++ b/src/features/feed/ui/FeedModalImageViewer.tsx
@@ -1,5 +1,6 @@
+import { Image } from 'expo-image';
import React from 'react';
-import { View, Image, Text, Pressable } from 'react-native';
+import { View, Text, Pressable } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import CloseIconSvg from '@/assets/img/close-icon.svg';
import { CookingOrder } from '@entities/recipe';
@@ -21,7 +22,12 @@ const FeedModalImageViewer = ({ visible, onClose, item }: Props) => {
{/* 이미지 */}
{item?.image && (
-
+
)}
{/* 하단 설명 바 */}
diff --git a/src/features/feed/ui/RecipeStepsArticleViewType.tsx b/src/features/feed/ui/RecipeStepsArticleViewType.tsx
index f0adb82..8a9a267 100644
--- a/src/features/feed/ui/RecipeStepsArticleViewType.tsx
+++ b/src/features/feed/ui/RecipeStepsArticleViewType.tsx
@@ -1,5 +1,6 @@
+import { Image } from 'expo-image';
import React, { useState } from 'react';
-import { View, Text, Image, Pressable } from 'react-native';
+import { View, Text, Pressable } from 'react-native';
import { CookingOrder } from '@entities/recipe';
import FeedModalImageViewer from './FeedModalImageViewer';
@@ -34,8 +35,9 @@ const RecipeStepsArticleViewType = ({ steps }: Props) => {
openModal(item)} className="active:opacity-80">
{item.description}
diff --git a/src/features/feed/ui/RecipeStepsFeedViewType.tsx b/src/features/feed/ui/RecipeStepsFeedViewType.tsx
index 078d250..82c347e 100644
--- a/src/features/feed/ui/RecipeStepsFeedViewType.tsx
+++ b/src/features/feed/ui/RecipeStepsFeedViewType.tsx
@@ -1,8 +1,8 @@
+import { Image } from 'expo-image';
import React, { useRef, useState } from 'react';
import {
View,
Text,
- Image,
FlatList,
Dimensions,
NativeScrollEvent,
@@ -42,7 +42,9 @@ const RecipeStepsFeedViewType = ({ steps }: Props) => {
step {item.turn.toString().padStart(2, '0')}
diff --git a/src/features/feed/ui/header/MyFeedCatagory.tsx b/src/features/feed/ui/header/MyFeedCatagory.tsx
index 3e90b8b..5373dcb 100644
--- a/src/features/feed/ui/header/MyFeedCatagory.tsx
+++ b/src/features/feed/ui/header/MyFeedCatagory.tsx
@@ -7,7 +7,6 @@ const MyFeedCategory = () => {
const categories = [
{ label: '전체', value: 'ALL' },
- { label: '오늘', value: 'TODAY' },
{ label: '인기', value: 'HOT' },
{ label: '추천', value: 'RECOMMEND' },
{ label: '팔로잉', value: 'FOLLOWING' },
diff --git a/src/features/feed/ui/skeleton/FeedDetailSkeleton.tsx b/src/features/feed/ui/skeleton/FeedDetailSkeleton.tsx
index c9419db..f6f436b 100644
--- a/src/features/feed/ui/skeleton/FeedDetailSkeleton.tsx
+++ b/src/features/feed/ui/skeleton/FeedDetailSkeleton.tsx
@@ -7,8 +7,9 @@ const FeedDetailSkeleton = () => {
{/* 대표 이미지 */}
-
-
+
+
+
{/* 본문 영역 */}
diff --git a/src/features/recipe/lib/validateRecipeForm.ts b/src/features/recipe/lib/validateRecipeForm.ts
index 0f4fddf..36aa3c0 100644
--- a/src/features/recipe/lib/validateRecipeForm.ts
+++ b/src/features/recipe/lib/validateRecipeForm.ts
@@ -9,7 +9,7 @@ export const validateRecipeForm = (recipe: RecipeDetail): boolean => {
if (!recipe.subtitle?.trim()) missingFields.push('레시피 소제목');
if (!recipe.introduction?.trim()) missingFields.push('레시피 소개');
- if (!recipe.myCategoryId) missingFields.push('내 카테고리');
+ // if (!recipe.myCategoryId) missingFields.push('내 카테고리');
if (!recipe.cookingTypeId) missingFields.push('종류');
if (!recipe.situationId) missingFields.push('상황');
if (!recipe.mainIngredientId) missingFields.push('주재료');
diff --git a/src/features/recipe/model/useRecipeCreateForm.ts b/src/features/recipe/model/useRecipeCreateForm.ts
index 6a5c11c..80680ac 100644
--- a/src/features/recipe/model/useRecipeCreateForm.ts
+++ b/src/features/recipe/model/useRecipeCreateForm.ts
@@ -110,7 +110,10 @@ export const useRecipeCreateForm = () => {
mutationFn: async (recipe: RecipeDetail) => {
return await apiInstance.put(`/recipes/${recipe.id}/finalize`, recipe);
},
- onSuccess: () => invalidateAll(),
+ onSuccess: () => {
+ invalidateAll();
+ queryClient.invalidateQueries({ queryKey: queryKeys.feed.all });
+ },
onError: error => console.error('❌ 최종 저장 실패:', error),
});
// 레시피 삭제
@@ -118,7 +121,10 @@ export const useRecipeCreateForm = () => {
mutationFn: async (recipeId: string) => {
return await apiInstance.delete(`/recipes/${recipeId}`);
},
- onSuccess: () => invalidateAll(),
+ onSuccess: () => {
+ invalidateAll();
+ queryClient.invalidateQueries({ queryKey: queryKeys.feed.all });
+ },
onError: error => console.error('❌ 삭제 실패:', error),
});
diff --git a/src/features/recipe/ui/FormMediaUpload.tsx b/src/features/recipe/ui/FormMediaUpload.tsx
index 9554913..73b6449 100644
--- a/src/features/recipe/ui/FormMediaUpload.tsx
+++ b/src/features/recipe/ui/FormMediaUpload.tsx
@@ -1,5 +1,6 @@
+import { Image } from 'expo-image';
import React, { useEffect, useState } from 'react';
-import { Text, TouchableOpacity, View, Image } from 'react-native';
+import { Alert, Text, TouchableOpacity, View } from 'react-native';
import VideoPlayer from '@shared/ui/VideoPlayer';
import { useMediaUpload } from '../lib/useMediaUpload';
@@ -34,6 +35,20 @@ const FormMediaUpload = ({
}
}, [value]);
+ const handleClickImage = () => {
+ Alert.alert('이미지 옵션', '선택해주세요.', [
+ {
+ text: '수정하기',
+ onPress: () => handleUpload(),
+ },
+
+ {
+ text: '취소',
+ style: 'cancel',
+ },
+ ]);
+ };
+
const isValidSource = value && value !== null;
return (
@@ -45,11 +60,14 @@ const FormMediaUpload = ({
{isValidSource ? (
uploadType === 'image' ? (
-
+
+
+
) : (
{
recipeId?: string;
from: RecipeCreateFormFrom;
};
+ const { setRecipeTypeFinal } = useRecipeTypeStore();
const {
recipe,
@@ -68,6 +70,7 @@ const RecipeCreateForm = () => {
if (!validateRecipeForm(recipe)) return;
await recipeMutation.finalizeSave(recipe);
+ setRecipeTypeFinal();
navigation.goBack();
};
diff --git a/src/features/user/ui/AnotherUserFeedGrid.tsx b/src/features/user/ui/AnotherUserFeedGrid.tsx
index cd1410a..e4be0d1 100644
--- a/src/features/user/ui/AnotherUserFeedGrid.tsx
+++ b/src/features/user/ui/AnotherUserFeedGrid.tsx
@@ -1,6 +1,6 @@
+import { Image } from 'expo-image';
import React from 'react';
-import { FlatList, Image, View, TouchableOpacity } from 'react-native';
-
+import { FlatList, View, TouchableOpacity } from 'react-native';
import FeedsSvg from '@/assets/img/feeds-icon.svg';
import loginVideo from '@/assets/video/emptyScreenVideo.mp4';
import { RecipeCard } from '@entities/user';
@@ -40,7 +40,11 @@ const AnotherUserFeedGrid = ({ data, navigation }: Props) => {
-
+
)}
keyExtractor={item => item.id}
diff --git a/src/features/user/ui/FeedGrid.tsx b/src/features/user/ui/FeedGrid.tsx
index e58f162..e3d9833 100644
--- a/src/features/user/ui/FeedGrid.tsx
+++ b/src/features/user/ui/FeedGrid.tsx
@@ -1,6 +1,6 @@
+import { Image } from 'expo-image';
import React from 'react';
-import { FlatList, Image, View, TouchableOpacity } from 'react-native';
-
+import { FlatList, View, TouchableOpacity } from 'react-native';
import FeedsSvg from '@/assets/img/feeds-icon.svg';
import loginVideo from '@/assets/video/emptyScreenVideo.mp4';
import { MyPageTabType, RecipeCard } from '@entities/user';
@@ -45,7 +45,11 @@ const FeedGrid = ({ data, type, navigation }: Props) => {
-
+
)}
keyExtractor={item => item.id}
diff --git a/src/features/user/ui/FollowItem.tsx b/src/features/user/ui/FollowItem.tsx
index 127acff..6ad7147 100644
--- a/src/features/user/ui/FollowItem.tsx
+++ b/src/features/user/ui/FollowItem.tsx
@@ -39,7 +39,7 @@ const FollowItem = ({ user, navigation }: Props) => {
className="flex-row items-center justify-between bg-white px-4 py-3"
onPress={() =>
navigation.navigate('AnotherUserPage', {
- userId: user.userId ?? '0',
+ userId: user.userId,
})
}
>
diff --git a/src/pages/feed/ui/Feed.tsx b/src/pages/feed/ui/Feed.tsx
index 157fd0c..6bd5ad7 100644
--- a/src/pages/feed/ui/Feed.tsx
+++ b/src/pages/feed/ui/Feed.tsx
@@ -1,6 +1,7 @@
import { useFocusEffect } from '@react-navigation/native';
-import React, { useCallback } from 'react';
-import { View, FlatList, RefreshControl } from 'react-native';
+import { Image } from 'expo-image';
+import React, { useCallback, useRef } from 'react';
+import { View, FlatList, RefreshControl, ViewToken } from 'react-native';
import { Portal } from 'react-native-portalize';
import { FeedCard, useFeedInfiniteQuery, FeedCardSkeleton } from '@features/feed';
import { Feed as FeedItem } from '@entities/feed';
@@ -13,6 +14,10 @@ interface FeedPageProps {
navigation: RootNavigationProp<'Main'>;
}
+const viewabilityConfig = {
+ itemVisiblePercentThreshold: 1,
+};
+
const Feed: React.FC = ({ navigation }) => {
const { filter, condition } = useFeedFilterStore();
@@ -31,6 +36,31 @@ const Feed: React.FC = ({ navigation }) => {
);
+ const onViewableItemsChanged = useRef(
+ async ({ viewableItems }: { viewableItems: Array> }) => {
+ for (const viewToken of viewableItems) {
+ const { item } = viewToken;
+ const { thumbnail, profileImage } = item;
+
+ if (thumbnail) {
+ const cachePath = await Image.getCachePathAsync(thumbnail);
+
+ if (!cachePath) {
+ await Image.prefetch(thumbnail);
+ }
+ }
+
+ if (profileImage) {
+ const cachePath = await Image.getCachePathAsync(profileImage);
+
+ if (!cachePath) {
+ await Image.prefetch(profileImage);
+ }
+ }
+ }
+ },
+ ).current;
+
if (isInitialLoading) {
return ;
}
@@ -46,6 +76,8 @@ const Feed: React.FC = ({ navigation }) => {
onEndReached={onEndReached}
onEndReachedThreshold={0.6}
refreshControl={}
+ viewabilityConfig={viewabilityConfig}
+ onViewableItemsChanged={onViewableItemsChanged}
/>
diff --git a/src/pages/feed/ui/FeedDetail.tsx b/src/pages/feed/ui/FeedDetail.tsx
index bafee73..5944e4e 100644
--- a/src/pages/feed/ui/FeedDetail.tsx
+++ b/src/pages/feed/ui/FeedDetail.tsx
@@ -1,5 +1,6 @@
+import { Image } from 'expo-image';
import React, { useState, useEffect } from 'react';
-import { Text, View, Image, Pressable, ScrollView, TouchableOpacity } from 'react-native';
+import { Text, View, Pressable, ScrollView, TouchableOpacity } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import BookmarkOffSvg from '@/assets/img/feed/bookmark-off-icon.svg';
import BookmarkOnSvg from '@/assets/img/feed/bookmark-on-icon.svg';
@@ -33,7 +34,7 @@ const FeedDetail = ({ navigation, route }: FeedDetailProps) => {
const insets = useSafeAreaInsets();
const { feedId } = route.params;
- const { data: feedDetail } = useFeedDetailQuery(feedId);
+ const { data: feedDetail, isLoading } = useFeedDetailQuery(feedId);
// TODO: refactoring
const [bookmarked, setBookmarked] = useState(feedDetail?.isBookmarked);
@@ -71,6 +72,7 @@ const FeedDetail = ({ navigation, route }: FeedDetailProps) => {
);
}
};
+
useEffect(() => {
if (feedDetail) {
setBookmarked(feedDetail.isBookmarked);
@@ -81,9 +83,27 @@ const FeedDetail = ({ navigation, route }: FeedDetailProps) => {
}
}, [feedDetail]);
- if (!feedId) return null;
- if (!feedDetail) return ;
+ // prefetch recipe orders image
+ useEffect(() => {
+ if (!feedDetail) return;
+
+ const prefetchStepImages = async () => {
+ for (const order of feedDetail.cookingOrders) {
+ if (order.image) {
+ const cachePath = await Image.getCachePathAsync(order.image);
+
+ if (!cachePath) await Image.prefetch(order.image);
+ }
+ }
+ };
+
+ prefetchStepImages();
+ }, [feedDetail]);
+
+ // Skeleton ui
+ if (!feedDetail || !feedId || isLoading) return ;
+ // categories
const categories = [
feedDetail.myCategory,
feedDetail.cookingType,
@@ -100,10 +120,15 @@ const FeedDetail = ({ navigation, route }: FeedDetailProps) => {
onBackPress={navigation.goBack}
rightContent={}
/>
-
+ {/* TODO: image prefetch*/}
{/* 스크롤 영역 */}
-
+ {/* TODO: 스켈레톤 */}
+
{
{feedDetail.profileImage ? (
) : (
diff --git a/src/pages/recipe/ui/MyRecipe.tsx b/src/pages/recipe/ui/MyRecipe.tsx
index 55d734b..7088c3e 100644
--- a/src/pages/recipe/ui/MyRecipe.tsx
+++ b/src/pages/recipe/ui/MyRecipe.tsx
@@ -1,5 +1,6 @@
import { useQuery } from '@tanstack/react-query';
-import React from 'react';
+import { Image } from 'expo-image';
+import React, { useEffect, useMemo } from 'react';
import { View, FlatList } from 'react-native';
import { Portal } from 'react-native-portalize';
import loginVideo from '@/assets/video/emptyScreenVideo.mp4';
@@ -33,8 +34,29 @@ const MyRecipe: React.FC = ({ navigation }) => {
},
});
- const recipeList: Recipe[] = recipes?.result || [];
+ // recipes
+ const recipeList: Recipe[] = useMemo(() => recipes?.result || [], [recipes]);
+ // prefetch image
+ useEffect(() => {
+ if (!recipeList) return;
+ const prefetchMyrecipeImage = async () => {
+ for (const recipe of recipeList) {
+ if (!recipe.thumbnail) return;
+
+ const cachePath = await Image.getCachePathAsync(recipe.thumbnail);
+
+ // cache miss
+ if (!cachePath) {
+ await Image.prefetch(recipe.thumbnail);
+ }
+ }
+ };
+
+ prefetchMyrecipeImage();
+ }, [recipeList]);
+
+ // filtered recipes
const filteredRecipes = recipeList.filter(recipe => {
const matchText =
text.trim().length === 0 || recipe.title.toLowerCase().includes(text.toLowerCase());
diff --git a/src/pages/recipe/ui/RecipeCreate.tsx b/src/pages/recipe/ui/RecipeCreate.tsx
index d1b4c29..fbdf333 100644
--- a/src/pages/recipe/ui/RecipeCreate.tsx
+++ b/src/pages/recipe/ui/RecipeCreate.tsx
@@ -1,4 +1,5 @@
-import React from 'react';
+import { Image } from 'expo-image';
+import React, { useEffect, useMemo } from 'react';
import { View, FlatList, TouchableOpacity } from 'react-native';
import Swipeable from 'react-native-gesture-handler/ReanimatedSwipeable';
import PlusIcon from '@/assets/img/recipe/plus-float.svg';
@@ -18,9 +19,26 @@ const RecipeCreate: React.FC = ({ navigation }) => {
// recipe list
const { data } = useRecipeListQuery(recipeType);
- const recipeList = (data || []) as Recipe[];
+ const recipeList = useMemo(() => (data || []) as Recipe[], [data]);
const isRecipeListEmpty = recipeList.length === 0;
+ // prefetch image
+ useEffect(() => {
+ if (isRecipeListEmpty) return;
+ const perfetchImage = async () => {
+ for (const recipe of recipeList) {
+ if (!recipe.thumbnail) return;
+
+ const cachePath = await Image.getCachePathAsync(recipe.thumbnail);
+
+ if (!cachePath) {
+ await Image.prefetch(recipe.thumbnail);
+ }
+ }
+ };
+ perfetchImage();
+ }, [isRecipeListEmpty, recipeList]);
+
// navigate
const navigateToRecipeCreateForm = () => {
navigation.navigate('RecipeCreateForm', { recipeId: '', from: 'RecipeCreate' });
diff --git a/src/pages/recipe/ui/RecipeDetail.tsx b/src/pages/recipe/ui/RecipeDetail.tsx
index 905d23d..0859d4c 100644
--- a/src/pages/recipe/ui/RecipeDetail.tsx
+++ b/src/pages/recipe/ui/RecipeDetail.tsx
@@ -1,5 +1,6 @@
-import React from 'react';
-import { Text, View, Image, ScrollView } from 'react-native';
+import { Image } from 'expo-image';
+import React, { useEffect } from 'react';
+import { Text, View, ScrollView } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import {
RecipeDetailSection,
@@ -25,9 +26,11 @@ import Shared from '@shared/ui/Shared';
import VideoPlayer from '@shared/ui/VideoPlayer';
const RecipeDetail = ({ navigation, route }: RecipeDetailProps) => {
+ // ui settings
const insets = useSafeAreaInsets();
const { viewType, setViewType } = useTwoViewTypeStore();
+ // params
const { recipeId } = route.params;
// delete recipe
@@ -39,9 +42,28 @@ const RecipeDetail = ({ navigation, route }: RecipeDetailProps) => {
// recipe list
const { data: detailRecipe, isLoading } = useRecipeDetailQuery(recipeId);
- const isRecipeDetail = detailRecipe !== null;
+
+ // prefetch: recipe orders image
+ useEffect(() => {
+ if (!detailRecipe) return;
+
+ const prefetchStepImages = async () => {
+ for (const order of detailRecipe.cookingOrders) {
+ if (order.image) {
+ const cachePath = await Image.getCachePathAsync(order.image);
+
+ if (!cachePath) await Image.prefetch(order.image);
+ }
+ }
+ };
+
+ prefetchStepImages();
+ }, [detailRecipe]);
+
+ // category
const { categoryValue } = useCategories();
if (!recipeId) return null;
+ const isRecipeDetail = detailRecipe !== null;
if (isLoading || !isRecipeDetail) return ;
if (!recipeId || !detailRecipe) return null;
@@ -53,6 +75,7 @@ const RecipeDetail = ({ navigation, route }: RecipeDetailProps) => {
categoryValue?.getMethod(detailRecipe),
].filter(isValidString);
+ // navigation
const navigateToRecipeCreateForm = async () => {
await navigation.goBack();
await navigation.navigate('RecipeCreateForm', {
@@ -60,6 +83,7 @@ const RecipeDetail = ({ navigation, route }: RecipeDetailProps) => {
from: 'RecipeDetail',
});
};
+
return (
{/* 헤더 */}
@@ -67,7 +91,11 @@ const RecipeDetail = ({ navigation, route }: RecipeDetailProps) => {
{/* 스크롤 영역 */}
-
+
{
subTitle="레시피 소개"
/>
{/* 레시피 영상 */}
-
- }
- />
+ {detailRecipe?.video && (
+
+ }
+ />
+ )}
{/* 레시피 순서 */}
diff --git a/src/pages/user/ui/AnotherUserPage.tsx b/src/pages/user/ui/AnotherUserPage.tsx
index a55162a..e317f0d 100644
--- a/src/pages/user/ui/AnotherUserPage.tsx
+++ b/src/pages/user/ui/AnotherUserPage.tsx
@@ -1,14 +1,13 @@
import React from 'react';
import { ActivityIndicator, View } from 'react-native';
+import { AnotherUserHeader } from '@/src/entities/user';
import { AnotherUserHeaderSection, AnotherUserFeedGrid, useFeedQuery } from '@features/user';
import { AnotherUserPageProps } from '@shared/types';
const AnotherUserPage = ({ navigation, route }: AnotherUserPageProps) => {
const { userId } = route.params;
- console.log(userId);
const { profile, feeds, isLoading: isLoadingRecipe } = useFeedQuery(userId!);
-
if (!userId) return null;
else if (isLoadingRecipe || !profile) {
return (
@@ -19,6 +18,7 @@ const AnotherUserPage = ({ navigation, route }: AnotherUserPageProps) => {
}
return (
+
{/*유저 헤더 섹션*/}
{
bottomSheetClose();
navigation.navigate('Secession', { userId: userId ?? '1' });
- console.log(1);
};
const handleCatagorySave = () => {
diff --git a/src/shared/ui/UserProfileImage.tsx b/src/shared/ui/UserProfileImage.tsx
index 394b358..3f01f95 100644
--- a/src/shared/ui/UserProfileImage.tsx
+++ b/src/shared/ui/UserProfileImage.tsx
@@ -1,5 +1,5 @@
+import { Image } from 'expo-image';
import React from 'react';
-import { Image } from 'react-native';
import NoneProfileImgSvg from '@/assets/img/none-profile-img.svg';
interface Props {
@@ -9,7 +9,11 @@ interface Props {
const UserProfileImage: React.FC = ({ uri, size = 110 }) => {
return uri ? (
-
+
) : (
);
diff --git a/yarn.lock b/yarn.lock
index a45aa34..09c8bad 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3820,6 +3820,11 @@ expo-image-picker@^16.1.4:
dependencies:
expo-image-loader "~5.1.0"
+expo-image@~2.4.1:
+ version "2.4.1"
+ resolved "https://registry.yarnpkg.com/expo-image/-/expo-image-2.4.1.tgz#c3f84795e33ea98d833fc4dad11ad750ea290b3e"
+ integrity sha512-yHp0Cy4ylOYyLR21CcH6i70DeRyLRPc0yAIPFPn4BT/BpkJNaX5QMXDppcHa58t4WI3Bb8QRJRLuAQaeCtDF8A==
+
expo-json-utils@~0.15.0:
version "0.15.0"
resolved "https://registry.yarnpkg.com/expo-json-utils/-/expo-json-utils-0.15.0.tgz#6723574814b9e6b0a90e4e23662be76123ab6ae9"