From e2d6f335cb52d146f6c622f674c3716d835aac90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=85=B8=20=EA=B8=B0=20=ED=9B=88?= Date: Fri, 14 Nov 2025 22:40:05 +0900 Subject: [PATCH 01/19] chore: install expo image --- ios/Podfile.lock | 53 ++++++++++++++++++++++++++++ ios/zipbap.xcodeproj/project.pbxproj | 2 ++ package.json | 1 + yarn.lock | 5 +++ 4 files changed, 61 insertions(+) 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/package.json b/package.json index f83b739..12cffbd 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/yarn.lock b/yarn.lock index 528034c..7b1fb1c 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" From 2d2aee6774f52a7348112b4b3a95ebcc9754c238 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=85=B8=20=EA=B8=B0=20=ED=9B=88?= Date: Fri, 14 Nov 2025 22:40:35 +0900 Subject: [PATCH 02/19] chore: CFBundleVersion version up --- ios/zipbap/Info.plist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios/zipbap/Info.plist b/ios/zipbap/Info.plist index 19b22fc..41054b6 100644 --- a/ios/zipbap/Info.plist +++ b/ios/zipbap/Info.plist @@ -38,7 +38,7 @@ CFBundleVersion - 15 + 16 ITSAppUsesNonExemptEncryption KAKAO_APP_KEY From 2be53646e867717ae08b45118f1d197a4d91e2a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=85=B8=20=EA=B8=B0=20=ED=9B=88?= Date: Fri, 14 Nov 2025 22:41:56 +0900 Subject: [PATCH 03/19] =?UTF-8?q?fix:=20Feed=20Image=20cache,=20prefetch?= =?UTF-8?q?=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/feed/ui/FeedCard.tsx | 9 +++++--- src/pages/feed/ui/Feed.tsx | 36 +++++++++++++++++++++++++++++-- src/pages/feed/ui/FeedDetail.tsx | 33 ++++++++++++++++++++++++---- 3 files changed, 69 insertions(+), 9 deletions(-) diff --git a/src/features/feed/ui/FeedCard.tsx b/src/features/feed/ui/FeedCard.tsx index cd976e5..21bb58d 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 } from 'react-native'; +import { View, Text, Pressable } 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'; @@ -26,7 +27,8 @@ const FeedCard = ({ feed, navigation }: Props) => { {feed.profileImage ? ( ) : ( @@ -42,7 +44,8 @@ const FeedCard = ({ feed, navigation }: Props) => { {/* 대표 사진 */} {/* 요리 소개 */} 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 0836596..fba045b 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 } from 'react-native'; +import { Text, View, Pressable, ScrollView } 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'; @@ -71,6 +72,7 @@ const FeedDetail = ({ navigation, route }: FeedDetailProps) => { ); } }; + useEffect(() => { if (feedDetail) { setBookmarked(feedDetail.isBookmarked); @@ -81,6 +83,23 @@ const FeedDetail = ({ navigation, route }: FeedDetailProps) => { } }, [feedDetail]); + // 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]); + if (!feedId) return null; if (!feedDetail) return ; @@ -100,10 +119,15 @@ const FeedDetail = ({ navigation, route }: FeedDetailProps) => { onBackPress={navigation.goBack} rightContent={} /> - + {/* TODO: image prefetch*/} {/* 스크롤 영역 */} - + {/* TODO: 스켈레톤 */} + { {feedDetail.profileImage ? ( ) : ( From 119efd16c843e4b10b135e80ce47917f576dc6d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=85=B8=20=EA=B8=B0=20=ED=9B=88?= Date: Fri, 14 Nov 2025 22:43:11 +0900 Subject: [PATCH 04/19] =?UTF-8?q?fix:=20Recipe=20Image=20cache,=20prefetch?= =?UTF-8?q?=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/entities/recipe/ui/ArticleView.tsx | 9 +++-- src/entities/recipe/ui/FeedView.tsx | 9 +++-- src/entities/recipe/ui/ImageView.tsx | 9 +++-- .../feed/ui/RecipeStepsArticleViewType.tsx | 8 +++-- .../feed/ui/RecipeStepsFeedViewType.tsx | 6 ++-- src/pages/recipe/ui/MyRecipe.tsx | 26 ++++++++++++-- src/pages/recipe/ui/RecipeDetail.tsx | 36 ++++++++++++++++--- 7 files changed, 86 insertions(+), 17 deletions(-) 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/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/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/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/RecipeDetail.tsx b/src/pages/recipe/ui/RecipeDetail.tsx index 905d23d..91fc99b 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) => { {/* 스크롤 영역 */} - + Date: Fri, 14 Nov 2025 22:44:03 +0900 Subject: [PATCH 05/19] =?UTF-8?q?fix:=20androidSplash=20image=20cache=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/AndroidSplashScreen.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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" /> ); From a1faa95b28a9c2745cea75137c7da0a603f09ecc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=85=B8=20=EA=B8=B0=20=ED=9B=88?= Date: Fri, 14 Nov 2025 22:44:31 +0900 Subject: [PATCH 06/19] =?UTF-8?q?fix:=20Swipable=20=EC=9A=94=EC=86=8C?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EC=82=AD=EC=A0=9C=EC=8B=9C=20=ED=99=95?= =?UTF-8?q?=EC=9D=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../recipe/ui/DetailDeleteComponent.tsx | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) 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 ( From 6ce056d48a18236175214201d18a3b6c4d968bce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=85=B8=20=EA=B8=B0=20=ED=9B=88?= Date: Fri, 14 Nov 2025 23:05:27 +0900 Subject: [PATCH 07/19] feat: recipe temp,image prefetch --- src/pages/recipe/ui/RecipeCreate.tsx | 22 ++++++++++++++++++-- src/pages/user/ui/UserSettingBottomSheet.tsx | 1 - 2 files changed, 20 insertions(+), 3 deletions(-) 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/user/ui/UserSettingBottomSheet.tsx b/src/pages/user/ui/UserSettingBottomSheet.tsx index fdb083a..de6b3a2 100644 --- a/src/pages/user/ui/UserSettingBottomSheet.tsx +++ b/src/pages/user/ui/UserSettingBottomSheet.tsx @@ -26,7 +26,6 @@ const UserSettingBottomSheet = ({ const handleCategorySecession = () => { bottomSheetClose(); navigation.navigate('Secession', { userId: userId ?? '1' }); - console.log(1); }; const handleCatagorySave = () => { From 8e4e7b7c397d79ef49e091bbfd155bf8fb699784 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=85=B8=20=EA=B8=B0=20=ED=9B=88?= Date: Fri, 14 Nov 2025 23:33:43 +0900 Subject: [PATCH 08/19] fix: react-native Image -> expo-image --- src/features/chat/ui/ChatInput.tsx | 9 +++++++-- src/features/chat/ui/CommentItem.tsx | 6 ++++-- src/features/feed/ui/FeedModalImageViewer.tsx | 10 ++++++++-- src/features/recipe/ui/FormMediaUpload.tsx | 8 +++++--- src/features/user/ui/AnotherUserFeedGrid.tsx | 10 +++++++--- src/features/user/ui/FeedGrid.tsx | 10 +++++++--- src/shared/ui/UserProfileImage.tsx | 8 ++++++-- 7 files changed, 44 insertions(+), 17 deletions(-) diff --git a/src/features/chat/ui/ChatInput.tsx b/src/features/chat/ui/ChatInput.tsx index 5fc5574..cc1182a 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/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/recipe/ui/FormMediaUpload.tsx b/src/features/recipe/ui/FormMediaUpload.tsx index 9554913..e3d6ca6 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 { Text, TouchableOpacity, View } from 'react-native'; import VideoPlayer from '@shared/ui/VideoPlayer'; import { useMediaUpload } from '../lib/useMediaUpload'; @@ -47,8 +48,9 @@ const FormMediaUpload = ({ uploadType === 'image' ? ( ) : ( { - + )} 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/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 ? ( - + ) : ( ); From cb74e5bc4766e6a799bbdd01d7bb4b14683f1498 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=85=B8=20=EA=B8=B0=20=ED=9B=88?= Date: Fri, 14 Nov 2025 23:35:40 +0900 Subject: [PATCH 09/19] chore: CFBundleVersion version up --- ios/zipbap/Info.plist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios/zipbap/Info.plist b/ios/zipbap/Info.plist index 41054b6..216c4ab 100644 --- a/ios/zipbap/Info.plist +++ b/ios/zipbap/Info.plist @@ -38,7 +38,7 @@ CFBundleVersion - 16 + 17 ITSAppUsesNonExemptEncryption KAKAO_APP_KEY From 4ea3492eadc0375fa52f9636c5cebfdb8cc746c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=85=B8=20=EA=B8=B0=20=ED=9B=88?= Date: Sat, 15 Nov 2025 00:00:33 +0900 Subject: [PATCH 10/19] =?UTF-8?q?style:=20FeedDetail=20Skeleton=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/feed/ui/skeleton/FeedDetailSkeleton.tsx | 5 +++-- src/pages/feed/ui/FeedDetail.tsx | 7 ++++--- 2 files changed, 7 insertions(+), 5 deletions(-) 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/pages/feed/ui/FeedDetail.tsx b/src/pages/feed/ui/FeedDetail.tsx index fba045b..05c06ae 100644 --- a/src/pages/feed/ui/FeedDetail.tsx +++ b/src/pages/feed/ui/FeedDetail.tsx @@ -34,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); @@ -100,9 +100,10 @@ const FeedDetail = ({ navigation, route }: FeedDetailProps) => { prefetchStepImages(); }, [feedDetail]); - if (!feedId) return null; - if (!feedDetail) return ; + // Skeleton ui + if (!feedDetail || !feedId || isLoading) return ; + // categories const categories = [ feedDetail.myCategory, feedDetail.cookingType, From cc860c4b198941c440d3307214ae66d8d3a39a46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=85=B8=20=EA=B8=B0=20=ED=9B=88?= Date: Sat, 15 Nov 2025 02:22:27 +0900 Subject: [PATCH 11/19] =?UTF-8?q?fix:=20recipe=20mutation=EC=9D=98=20inval?= =?UTF-8?q?idQueries=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/entities/recipe/api/useRecipeDelete.ts | 1 + 1 file changed, 1 insertion(+) 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) => { From 4ce0e4873a474abd20dc11126b0a8e94aea2ab4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=85=B8=20=EA=B8=B0=20=ED=9B=88?= Date: Sat, 15 Nov 2025 02:22:50 +0900 Subject: [PATCH 12/19] =?UTF-8?q?fix:=20feed=20filter=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/feed/ui/header/MyFeedCatagory.tsx | 1 - 1 file changed, 1 deletion(-) 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' }, From f81be08695d73f990ebd5ae9fb9724f09b737741 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=85=B8=20=EA=B8=B0=20=ED=9B=88?= Date: Sat, 15 Nov 2025 02:43:38 +0900 Subject: [PATCH 13/19] feat: recipe detail --- .../recipe/model/useRecipeCreateForm.ts | 10 ++++++++-- src/features/recipe/ui/RecipeCreateForm.tsx | 3 +++ src/pages/recipe/ui/RecipeDetail.tsx | 20 ++++++++++--------- 3 files changed, 22 insertions(+), 11 deletions(-) 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/RecipeCreateForm.tsx b/src/features/recipe/ui/RecipeCreateForm.tsx index 46f40aa..795eabc 100644 --- a/src/features/recipe/ui/RecipeCreateForm.tsx +++ b/src/features/recipe/ui/RecipeCreateForm.tsx @@ -4,6 +4,7 @@ import React from 'react'; import { View, Text, Alert } from 'react-native'; import { KeyboardAwareScrollView } from 'react-native-keyboard-controller'; import { useCategories } from '@entities/category'; +import { useRecipeTypeStore } from '@shared/store'; import { RootStackParamList } from '@shared/types'; import { RecipeCreateFormFrom, RootNavigationProp } from '@shared/types/navigation'; import { FullWidthButton } from '@shared/ui'; @@ -32,6 +33,7 @@ const RecipeCreateForm = () => { 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/pages/recipe/ui/RecipeDetail.tsx b/src/pages/recipe/ui/RecipeDetail.tsx index 91fc99b..0859d4c 100644 --- a/src/pages/recipe/ui/RecipeDetail.tsx +++ b/src/pages/recipe/ui/RecipeDetail.tsx @@ -145,15 +145,17 @@ const RecipeDetail = ({ navigation, route }: RecipeDetailProps) => { subTitle="레시피 소개" /> {/* 레시피 영상 */} - - } - /> + {detailRecipe?.video && ( + + } + /> + )} {/* 레시피 순서 */} From 7a8e524e18e7620667f38c7a711f58b354cf52f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=85=B8=20=EA=B8=B0=20=ED=9B=88?= Date: Sat, 15 Nov 2025 02:50:30 +0900 Subject: [PATCH 14/19] =?UTF-8?q?fix:=20merge=20confilt=20=ED=95=B4?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/feed/ui/FeedCard.tsx | 5 +++-- src/pages/feed/ui/FeedDetail.tsx | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/features/feed/ui/FeedCard.tsx b/src/features/feed/ui/FeedCard.tsx index 6157e5f..e89ce45 100644 --- a/src/features/feed/ui/FeedCard.tsx +++ b/src/features/feed/ui/FeedCard.tsx @@ -1,6 +1,6 @@ import { Image } from 'expo-image'; import React from 'react'; -import { View, Text, Image, Pressable } 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'; @@ -34,7 +34,8 @@ const FeedCard = ({ feed, navigation }: Props) => { {feed.profileImage ? ( ) : ( diff --git a/src/pages/feed/ui/FeedDetail.tsx b/src/pages/feed/ui/FeedDetail.tsx index 059b7a7..5944e4e 100644 --- a/src/pages/feed/ui/FeedDetail.tsx +++ b/src/pages/feed/ui/FeedDetail.tsx @@ -1,6 +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'; @@ -154,7 +154,8 @@ const FeedDetail = ({ navigation, route }: FeedDetailProps) => { {feedDetail.profileImage ? ( ) : ( From 8ef5aa1065b1eb33e235d374ac75fc9dd308ecd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=85=B8=20=EA=B8=B0=20=ED=9B=88?= Date: Sat, 15 Nov 2025 10:31:07 +0900 Subject: [PATCH 15/19] =?UTF-8?q?fix:=20validRecipe=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/recipe/lib/validateRecipeForm.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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('주재료'); From e9a16026a470a58772bfd9cb9cecaeaa9cc3ced9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=85=B8=20=EA=B8=B0=20=ED=9B=88?= Date: Sat, 15 Nov 2025 10:31:42 +0900 Subject: [PATCH 16/19] =?UTF-8?q?fix:=20FormMedia=20=ED=81=B4=EB=A6=AD?= =?UTF-8?q?=EC=8B=9C=20=EC=88=98=EC=A0=95=20=EA=B0=80=EB=8A=A5=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/recipe/ui/FormMediaUpload.tsx | 30 +++++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/src/features/recipe/ui/FormMediaUpload.tsx b/src/features/recipe/ui/FormMediaUpload.tsx index e3d6ca6..73b6449 100644 --- a/src/features/recipe/ui/FormMediaUpload.tsx +++ b/src/features/recipe/ui/FormMediaUpload.tsx @@ -1,6 +1,6 @@ import { Image } from 'expo-image'; import React, { useEffect, useState } from 'react'; -import { Text, TouchableOpacity, View } from 'react-native'; +import { Alert, Text, TouchableOpacity, View } from 'react-native'; import VideoPlayer from '@shared/ui/VideoPlayer'; import { useMediaUpload } from '../lib/useMediaUpload'; @@ -35,6 +35,20 @@ const FormMediaUpload = ({ } }, [value]); + const handleClickImage = () => { + Alert.alert('이미지 옵션', '선택해주세요.', [ + { + text: '수정하기', + onPress: () => handleUpload(), + }, + + { + text: '취소', + style: 'cancel', + }, + ]); + }; + const isValidSource = value && value !== null; return ( @@ -46,12 +60,14 @@ const FormMediaUpload = ({ {isValidSource ? ( uploadType === 'image' ? ( - + + + ) : ( Date: Sat, 15 Nov 2025 12:27:02 +0900 Subject: [PATCH 17/19] chore: CFBundleVersion version up --- ios/zipbap/Info.plist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios/zipbap/Info.plist b/ios/zipbap/Info.plist index 216c4ab..0949aac 100644 --- a/ios/zipbap/Info.plist +++ b/ios/zipbap/Info.plist @@ -38,7 +38,7 @@ CFBundleVersion - 17 + 19 ITSAppUsesNonExemptEncryption KAKAO_APP_KEY From 4678d856a00f61aa449275e96ee9dbb515d41ac1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=85=B8=20=EA=B8=B0=20=ED=9B=88?= Date: Sat, 15 Nov 2025 12:27:55 +0900 Subject: [PATCH 18/19] fix: another user profile navigation --- src/app/Navigation.tsx | 11 ++++++----- src/entities/user/ui/AnotherUserHeader.tsx | 15 ++++++++++++++- src/features/user/ui/FollowItem.tsx | 2 +- src/pages/user/ui/AnotherUserPage.tsx | 4 ++-- 4 files changed, 23 insertions(+), 9 deletions(-) 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/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/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/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 ( + {/*유저 헤더 섹션*/} Date: Sat, 15 Nov 2025 12:46:40 +0900 Subject: [PATCH 19/19] =?UTF-8?q?chore:=20app.json=20ios=20splash=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.json | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) 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": {