diff --git a/src/components/Common/ShowAllButton/ShowAllButton.tsx b/src/components/Common/ShowAllButton/ShowAllButton.tsx new file mode 100644 index 00000000..b82b31dc --- /dev/null +++ b/src/components/Common/ShowAllButton/ShowAllButton.tsx @@ -0,0 +1,25 @@ +import { Link } from 'react-router-dom'; + +import { moreIconWrapper, linkWrapper } from './showAllButton.css'; + +import { SvgIcon, Text } from '@/components/Common'; +import { vars } from '@/styles/theme.css'; + +interface ShowAllButtonProps { + link: string; +} + +const ShowAllButton = ({ link }: ShowAllButtonProps) => { + return ( + +
+ +
+ + 전체보기 + + + ); +}; + +export default ShowAllButton; diff --git a/src/components/Common/ShowAllButton/showAllButton.css.ts b/src/components/Common/ShowAllButton/showAllButton.css.ts new file mode 100644 index 00000000..200e2c5d --- /dev/null +++ b/src/components/Common/ShowAllButton/showAllButton.css.ts @@ -0,0 +1,20 @@ +import { vars } from '@/styles/theme.css'; +import { style } from '@vanilla-extract/css'; + +export const linkWrapper = style({ + display: 'flex', + flexDirection: 'column', + gap: 12, + alignItems: 'center', + width: 45, +}); + +export const moreIconWrapper = style({ + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + width: 40, + height: 40, + borderRadius: '50%', + background: vars.colors.secondary1, +}); diff --git a/src/components/Common/TopBar/TopBar.stories.tsx b/src/components/Common/TopBar/TopBar.stories.tsx index 93b27658..c67c30c8 100644 --- a/src/components/Common/TopBar/TopBar.stories.tsx +++ b/src/components/Common/TopBar/TopBar.stories.tsx @@ -71,7 +71,7 @@ export const LeftTitleAndRegister: Story = { return ( - + ); }, diff --git a/src/components/Common/TopBar/TopBar.tsx b/src/components/Common/TopBar/TopBar.tsx index 5197fa3b..a9ba61dd 100644 --- a/src/components/Common/TopBar/TopBar.tsx +++ b/src/components/Common/TopBar/TopBar.tsx @@ -1,6 +1,7 @@ +import type { ComponentPropsWithoutRef } from 'react'; import { Link } from 'react-router-dom'; -import { LeftNavigationWrapper, container, headerTitle, leftTitle } from './topBar.css'; +import { LeftNavigationWrapper, container, headerTitle, leftTitle, register } from './topBar.css'; import SvgIcon from '../Svg/SvgIcon'; import Text from '../Text/Text'; @@ -8,7 +9,6 @@ import LogoImage from '@/assets/logo.svg'; import { PATH } from '@/constants/path'; import { vars } from '@/styles/theme.css'; - interface TopBarProps { children?: React.ReactNode; title?: string; @@ -16,6 +16,8 @@ interface TopBarProps { state?: unknown; } +type RegisterButtonProps = ComponentPropsWithoutRef<'button'>; + const TopBar = ({ children }: TopBarProps) => { return
{children}
; }; @@ -58,13 +60,13 @@ const SearchLink = () => { ); }; -const RegisterLink = ({ link = '' }: TopBarProps) => { +const RegisterButton = ({ ...props }: RegisterButtonProps) => { return ( - - + ); }; @@ -85,7 +87,7 @@ TopBar.BackLink = BackLink; TopBar.LeftNavigationGroup = LeftNavigationGroup; TopBar.Title = Title; TopBar.SearchLink = SearchLink; -TopBar.RegisterLink = RegisterLink; +TopBar.RegisterButton = RegisterButton; TopBar.CloseButton = CloseButton; TopBar.Spacer = Spacer; diff --git a/src/components/Common/TopBar/topBar.css.ts b/src/components/Common/TopBar/topBar.css.ts index a59f6f60..c743e336 100644 --- a/src/components/Common/TopBar/topBar.css.ts +++ b/src/components/Common/TopBar/topBar.css.ts @@ -34,3 +34,11 @@ export const headerTitle = style({ fontSize: 18, fontWeight: 600, }); + +export const register = style({ + selectors: { + 'button:disabled > &': { + color: vars.colors.text.disabled, + }, + }, +}); diff --git a/src/components/Common/index.ts b/src/components/Common/index.ts index c37aad25..6c66972c 100644 --- a/src/components/Common/index.ts +++ b/src/components/Common/index.ts @@ -26,3 +26,5 @@ export { default as Badge } from './Badge/Badge'; export { default as WriteButton } from './WriteButton/WriteButton'; export { default as Text } from './Text/Text'; export { default as Indicator } from './Indicator/Indicator'; +export { default as TopBar } from './TopBar/TopBar'; +export { default as ShowAllButton } from './ShowAllButton/ShowAllButton'; diff --git a/src/components/Product/ProductRecipeList/ProductRecipeList.tsx b/src/components/Product/ProductRecipeList/ProductRecipeList.tsx index 8c5f0f24..3c901d91 100644 --- a/src/components/Product/ProductRecipeList/ProductRecipeList.tsx +++ b/src/components/Product/ProductRecipeList/ProductRecipeList.tsx @@ -1,18 +1,20 @@ import { Link } from 'react-router-dom'; -import { container, moreIcon, moreIconWrapper, moreItem, moreLink } from './productRecipeList.css'; +import { container, moreItem, notFound, recipeLink } from './productRecipeList.css'; -import { SvgIcon, Text } from '@/components/Common'; +import SearchNotFoundImage from '@/assets/search-notfound.png'; +import { Text, ShowAllButton } from '@/components/Common'; import { DefaultRecipeItem } from '@/components/Recipe'; +import { PATH } from '@/constants/path'; import { useInfiniteProductRecipesQuery } from '@/hooks/queries/product'; -import { vars } from '@/styles/theme.css'; import displaySlice from '@/utils/displaySlice'; interface ProductRecipeListProps { productId: number; + productName: string; } -const ProductRecipeList = ({ productId }: ProductRecipeListProps) => { +const ProductRecipeList = ({ productId, productName }: ProductRecipeListProps) => { // 상품에서 보여줄 꿀조합 정렬 조건 const { data } = useInfiniteProductRecipesQuery(productId, 'favoriteCount,desc'); @@ -20,7 +22,20 @@ const ProductRecipeList = ({ productId }: ProductRecipeListProps) => { const recipeToDisplay = displaySlice(true, recipes, 3); if (recipes.length === 0) { - return null; + return ( +
+ 검색 결과 없음 + + 아직 작성된 꿀조합이 없어요 + +
+ + + 꿀조합 작성하러 가기 + + +
+ ); } return ( @@ -32,15 +47,7 @@ const ProductRecipeList = ({ productId }: ProductRecipeListProps) => { ))} {recipeToDisplay.length < recipes.length && (
  • - {/*링크는 상품이 포함된 꿀조합 검색결과로 가는 것이 맞을듯?*/} - -
    - -
    - - 전체보기 - - +
  • )} diff --git a/src/components/Product/ProductRecipeList/productRecipeList.css.ts b/src/components/Product/ProductRecipeList/productRecipeList.css.ts index 5c7824ba..dcb4e844 100644 --- a/src/components/Product/ProductRecipeList/productRecipeList.css.ts +++ b/src/components/Product/ProductRecipeList/productRecipeList.css.ts @@ -15,24 +15,20 @@ export const moreItem = style({ minWidth: 108, }); -export const moreLink = style({ +export const notFound = style({ display: 'flex', flexDirection: 'column', - justifyContent: 'center', alignItems: 'center', + gap: 6, + textAlign: 'center', }); -export const moreIconWrapper = style({ +export const recipeLink = style({ display: 'flex', justifyContent: 'center', alignItems: 'center', - width: 40, - height: 40, - marginBottom: 12, - borderRadius: '50%', - background: vars.colors.secondary1, -}); - -export const moreIcon = style({ - transform: 'rotate(180deg)', + height: 34, + padding: '0 16px', + backgroundColor: vars.colors.gray2, + borderRadius: 44, }); diff --git a/src/components/Recipe/RecipeItem/RecipeItem.tsx b/src/components/Recipe/RecipeItem/RecipeItem.tsx index e94712db..6bbf7de6 100644 --- a/src/components/Recipe/RecipeItem/RecipeItem.tsx +++ b/src/components/Recipe/RecipeItem/RecipeItem.tsx @@ -26,6 +26,7 @@ import { RECIPE_CARD_DEFAULT_IMAGE_URL_4, RECIPE_CARD_DEFAULT_IMAGE_URL_5, } from '@/constants/image'; +import { PATH } from '@/constants/path'; import RecipeItemProvider from '@/contexts/RecipeItemContext'; import { useRecipeItemValueContext } from '@/hooks/context'; import type { Recipe } from '@/types/recipe'; @@ -50,7 +51,7 @@ const RecipeItem = ({ recipe, children }: RecipeItemProps) => { return ( - {children} + {children} ); }; diff --git a/src/components/Review/ReviewRegisterForm/ReviewRegisterForm.tsx b/src/components/Review/ReviewRegisterForm/ReviewRegisterForm.tsx index dbbc0455..99d0dd85 100644 --- a/src/components/Review/ReviewRegisterForm/ReviewRegisterForm.tsx +++ b/src/components/Review/ReviewRegisterForm/ReviewRegisterForm.tsx @@ -8,7 +8,6 @@ import StarRate from './StarRate/StarRate'; import RebuyCheckbox from '../RebuyCheckbox/RebuyCheckbox'; import { ImageUploader, SvgIcon, Text } from '@/components/Common'; -import { MAX_DISPLAYED_TAGS_LENGTH, MIN_DISPLAYED_TAGS_LENGTH } from '@/constants'; import type { TagValue } from '@/contexts/ReviewFormContext'; import { useFormData, useImageUploader } from '@/hooks/common'; import { useReviewFormActionContext, useReviewFormValueContext } from '@/hooks/context'; @@ -16,31 +15,21 @@ import { useReviewRegisterFormMutation } from '@/hooks/queries/review'; import { vars } from '@/styles/theme.css'; import type { ReviewRequest } from '@/types/review'; -const MIN_RATING_SCORE = 0; -const MIN_CONTENT_LENGTH = 0; - interface ReviewRegisterFormProps { productId: number; openBottomSheet: () => void; } const ReviewRegisterForm = ({ productId, openBottomSheet }: ReviewRegisterFormProps) => { - const { isImageUploading, previewImage, imageFile, uploadImage, deleteImage } = useImageUploader(); + const { previewImage, imageFile, uploadImage, deleteImage } = useImageUploader(); const navigate = useNavigate(); - const reviewFormValue = useReviewFormValueContext(); + const { formValue: reviewFormValue } = useReviewFormValueContext(); const { handleReviewFormValue, resetReviewFormValue } = useReviewFormActionContext(); const { toast } = useToastActionContext(); const { mutate } = useReviewRegisterFormMutation(productId); - const isValid = - reviewFormValue.rating > MIN_RATING_SCORE && - reviewFormValue.tags.length >= MIN_DISPLAYED_TAGS_LENGTH && - reviewFormValue.tags.length <= MAX_DISPLAYED_TAGS_LENGTH && - reviewFormValue.content.length > MIN_CONTENT_LENGTH && - !isImageUploading; - const formValue: ReviewRequest = { ...reviewFormValue, tagIds: reviewFormValue.tags.map(({ id }) => id) }; const formData = useFormData({ @@ -81,7 +70,7 @@ const ReviewRegisterForm = ({ productId, openBottomSheet }: ReviewRegisterFormPr }; return ( -
    +

    사진 등록 @@ -105,7 +94,9 @@ const ReviewRegisterForm = ({ productId, openBottomSheet }: ReviewRegisterFormPr {reviewFormValue.tags.map((tag) => (
  • diff --git a/src/components/Review/ReviewRegisterForm/reviewRegisterForm.css.ts b/src/components/Review/ReviewRegisterForm/reviewRegisterForm.css.ts index f9e43fc2..d947c84f 100644 --- a/src/components/Review/ReviewRegisterForm/reviewRegisterForm.css.ts +++ b/src/components/Review/ReviewRegisterForm/reviewRegisterForm.css.ts @@ -37,8 +37,6 @@ export const tagButton = style({ gap: 4, height: 28, padding: '0 6px', - fontSize: '1.3rem', - fontWeight: 500, borderRadius: 4, backgroundColor: vars.colors.gray2, }); diff --git a/src/components/Review/ReviewTagList/ReviewTagList.tsx b/src/components/Review/ReviewTagList/ReviewTagList.tsx index 46363462..126adb76 100644 --- a/src/components/Review/ReviewTagList/ReviewTagList.tsx +++ b/src/components/Review/ReviewTagList/ReviewTagList.tsx @@ -10,7 +10,9 @@ import { useReviewTagsQuery } from '@/hooks/queries/review'; const ReviewTagList = () => { const { data: tagsData } = useReviewTagsQuery(); - const { tags: selectedTags } = useReviewFormValueContext(); + const { + formValue: { tags: selectedTags }, + } = useReviewFormValueContext(); const { handleReviewFormValue } = useReviewFormActionContext(); const isChecked = (tag: TagValue) => selectedTags.some(({ id }) => id === tag.id); diff --git a/src/components/Review/ReviewTagSheet/ReviewTagSheet.tsx b/src/components/Review/ReviewTagSheet/ReviewTagSheet.tsx index 10d5b0d8..3364f52a 100644 --- a/src/components/Review/ReviewTagSheet/ReviewTagSheet.tsx +++ b/src/components/Review/ReviewTagSheet/ReviewTagSheet.tsx @@ -1,20 +1,29 @@ -import { container, registerButton, registerButtonWrapper, section } from './reviewTagSheet.css'; +import { closeWrapper, container, registerButton, registerButtonWrapper, section } from './reviewTagSheet.css'; import ReviewTagList from '../ReviewTagList/ReviewTagList'; +import { SvgIcon } from '@/components/Common'; import { MAX_DISPLAYED_TAGS_LENGTH, MIN_DISPLAYED_TAGS_LENGTH } from '@/constants'; import { useReviewFormValueContext } from '@/hooks/context'; +import { vars } from '@/styles/theme.css'; interface ReviewTagSheetProps { close: () => void; } const ReviewTagSheet = ({ close }: ReviewTagSheetProps) => { - const { tags } = useReviewFormValueContext(); + const { + formValue: { tags }, + } = useReviewFormValueContext(); const isValid = tags.length >= MIN_DISPLAYED_TAGS_LENGTH && tags.length <= MAX_DISPLAYED_TAGS_LENGTH; return (
    +
    + +
    diff --git a/src/components/Review/ReviewTagSheet/reviewTagSheet.css.ts b/src/components/Review/ReviewTagSheet/reviewTagSheet.css.ts index f1d64a15..bce3cf0c 100644 --- a/src/components/Review/ReviewTagSheet/reviewTagSheet.css.ts +++ b/src/components/Review/ReviewTagSheet/reviewTagSheet.css.ts @@ -5,9 +5,17 @@ export const container = style({ height: '100vh', }); +export const closeWrapper = style({ + padding: '0 20px', + display: 'flex', + height: 50, + alignItems: 'center', + justifyContent: 'flex-end', +}); + export const section = style({ - padding: '50px 20px 70px', - marginBottom: 32, + padding: '0 20px', + margin: '16px 0 32px', }); export const registerButtonWrapper = style({ diff --git a/src/components/Search/ProductSearchResultList/ProductSearchResultPreviewList.stories.tsx b/src/components/Search/ProductSearchResultPreviewList/ProductSearchResultPreviewList.stories.tsx similarity index 100% rename from src/components/Search/ProductSearchResultList/ProductSearchResultPreviewList.stories.tsx rename to src/components/Search/ProductSearchResultPreviewList/ProductSearchResultPreviewList.stories.tsx diff --git a/src/components/Search/ProductSearchResultList/ProductSearchResultPreviewList.tsx b/src/components/Search/ProductSearchResultPreviewList/ProductSearchResultPreviewList.tsx similarity index 100% rename from src/components/Search/ProductSearchResultList/ProductSearchResultPreviewList.tsx rename to src/components/Search/ProductSearchResultPreviewList/ProductSearchResultPreviewList.tsx diff --git a/src/components/Search/ProductSearchResultList/productSearchResultPreivewList.css.ts b/src/components/Search/ProductSearchResultPreviewList/productSearchResultPreivewList.css.ts similarity index 100% rename from src/components/Search/ProductSearchResultList/productSearchResultPreivewList.css.ts rename to src/components/Search/ProductSearchResultPreviewList/productSearchResultPreivewList.css.ts diff --git a/src/components/Search/RecipeSearchResultPreviewList/RecipeSearchResultPreviewList.tsx b/src/components/Search/RecipeSearchResultPreviewList/RecipeSearchResultPreviewList.tsx new file mode 100644 index 00000000..b188fc6d --- /dev/null +++ b/src/components/Search/RecipeSearchResultPreviewList/RecipeSearchResultPreviewList.tsx @@ -0,0 +1,43 @@ +import { useRef } from 'react'; + +import { listWrapper } from './recipeSearchResultPreviewList.css'; +import SearchNotFound from '../SearchNotFound/SearchNotFound'; + +import { ShowAllButton } from '@/components/Common'; +import { DefaultRecipeItem } from '@/components/Recipe'; +import { PATH } from '@/constants/path'; +import { useIntersectionObserver } from '@/hooks/common'; +import { useInfiniteRecipeSearchResultsQuery } from '@/hooks/queries/search'; +import displaySlice from '@/utils/displaySlice'; + +interface RecipeSearchResultPreviewListProps { + searchQuery: string; +} + +const RecipeSearchResultPreviewList = ({ searchQuery }: RecipeSearchResultPreviewListProps) => { + const { data: searchResponse, fetchNextPage, hasNextPage } = useInfiniteRecipeSearchResultsQuery(searchQuery); + const scrollRef = useRef(null); + useIntersectionObserver(fetchNextPage, scrollRef, hasNextPage); + + const recipes = searchResponse.pages.flatMap((page) => page.recipes); + + if (recipes.length === 0) { + return ; + } + + return ( +
      + {displaySlice(false, recipes, 4).map((recipe, idx) => ( +
    • + {idx < 4 ? ( + + ) : ( + + )} +
    • + ))} +
    + ); +}; + +export default RecipeSearchResultPreviewList; diff --git a/src/components/Search/RecipeSearchResultPreviewList/recipeSearchResultPreviewList.css.ts b/src/components/Search/RecipeSearchResultPreviewList/recipeSearchResultPreviewList.css.ts new file mode 100644 index 00000000..df2e9d1a --- /dev/null +++ b/src/components/Search/RecipeSearchResultPreviewList/recipeSearchResultPreviewList.css.ts @@ -0,0 +1,8 @@ +import { style } from '@vanilla-extract/css'; + +export const listWrapper = style({ + display: 'flex', + gap: 10, + alignItems: 'center', + overflowY: 'scroll', +}); diff --git a/src/components/Search/SearchNotFound/SearchNotFound.tsx b/src/components/Search/SearchNotFound/SearchNotFound.tsx index 764f3d96..f1c1479a 100644 --- a/src/components/Search/SearchNotFound/SearchNotFound.tsx +++ b/src/components/Search/SearchNotFound/SearchNotFound.tsx @@ -3,21 +3,18 @@ import { container } from './searchNotFound.css'; import SearchNotFoundImage from '@/assets/search-notfound.png'; import { Text } from '@/components/Common'; - const SearchNotFound = () => { return ( - <> -
    - 검색 결과 없음 - - 검색 결과가 없어요 - -
    - - 다른 키워드로 검색해보세요! - -
    - +
    + 검색 결과 없음 + + 검색 결과가 없어요 + +
    + + 다른 키워드로 검색해보세요! + +
    ); }; diff --git a/src/components/Search/index.ts b/src/components/Search/index.ts index 2c312d80..0c32f1b8 100644 --- a/src/components/Search/index.ts +++ b/src/components/Search/index.ts @@ -1,5 +1,5 @@ -export { default as ProductSearchResultPreviewList } from './ProductSearchResultList/ProductSearchResultPreviewList'; +export { default as ProductSearchResultPreviewList } from './ProductSearchResultPreviewList/ProductSearchResultPreviewList'; export { default as RecommendList } from './RecommendList/RecommendList'; -export { default as RecipeSearchResultList } from './RecipeSearchResultList/RecipeSearchResultList'; +export { default as RecipeSearchResultPreviewList } from './RecipeSearchResultPreviewList/RecipeSearchResultPreviewList'; export { default as TagSearchResultList } from './TagSearchResultList/TagSearchResultList'; export { default as SearchInput } from './SearchInput/SearchInput'; diff --git a/src/contexts/ReviewFormContext.tsx b/src/contexts/ReviewFormContext.tsx index 0cc00f28..65e2f18c 100644 --- a/src/contexts/ReviewFormContext.tsx +++ b/src/contexts/ReviewFormContext.tsx @@ -2,7 +2,7 @@ import { useToastActionContext } from '@fun-eat/design-system'; import type { PropsWithChildren } from 'react'; import { createContext, useState } from 'react'; -import { MAX_DISPLAYED_TAGS_LENGTH } from '@/constants'; +import { MAX_DISPLAYED_TAGS_LENGTH, MIN_DISPLAYED_TAGS_LENGTH } from '@/constants'; import type { ReviewRequest } from '@/types/review'; export interface TagValue { @@ -10,12 +10,17 @@ export interface TagValue { name: string; } -type ReviewFormValue = Omit & { tags: TagValue[] }; -type ReviewFormValues = Exclude | TagValue; +type FormValue = Omit & { tags: TagValue[] }; +type FormValues = Exclude | TagValue; interface ReviewFormActionParams { - target: keyof ReviewFormValue; - value: ReviewFormValues; + target: keyof FormValue; + value: FormValues; +} + +interface ReviewFormValue { + isValid: boolean; + formValue: FormValue; } interface ReviewFormAction { @@ -23,7 +28,7 @@ interface ReviewFormAction { resetReviewFormValue: () => void; } -const initialReviewFormValue: ReviewFormValue = { +const initialFormValue: FormValue = { rating: 0, tags: [], content: '', @@ -31,10 +36,9 @@ const initialReviewFormValue: ReviewFormValue = { }; const MIN_RATING_SCORE = 0; -const MIN_SELECTED_TAGS_COUNT = 1; -const MIN_CONTENT_LENGTH = 0; +const MIN_CONTENT_LENGTH = 10; -const isTagValue = (value: ReviewFormValues): value is TagValue => +const isTagValue = (value: FormValues): value is TagValue => typeof value === 'object' && 'id' in value && 'name' in value; const isSelectedTag = (tags: TagValue[], selectedTag: TagValue) => tags.some(({ id }) => id === selectedTag.id); @@ -42,17 +46,17 @@ export const ReviewFormValueContext = createContext(null export const ReviewFormActionContext = createContext(null); const ReviewFormProvider = ({ children }: PropsWithChildren) => { - const [reviewFormValue, setReviewFormValue] = useState(initialReviewFormValue); + const [formValue, setFormValue] = useState(initialFormValue); const { toast } = useToastActionContext(); const isValid = - reviewFormValue.rating > MIN_RATING_SCORE && - reviewFormValue.tags.length >= MIN_SELECTED_TAGS_COUNT && - reviewFormValue.tags.length <= MAX_DISPLAYED_TAGS_LENGTH && - reviewFormValue.content.length > MIN_CONTENT_LENGTH; + formValue.rating > MIN_RATING_SCORE && + formValue.tags.length >= MIN_DISPLAYED_TAGS_LENGTH && + formValue.tags.length <= MAX_DISPLAYED_TAGS_LENGTH && + formValue.content.length >= MIN_CONTENT_LENGTH; const handleReviewFormValue = ({ target, value }: ReviewFormActionParams) => { - setReviewFormValue((prev) => { + setFormValue((prev) => { const targetValue = prev[target]; if (isTagValue(value) && Array.isArray(targetValue)) { @@ -73,7 +77,12 @@ const ReviewFormProvider = ({ children }: PropsWithChildren) => { }; const resetReviewFormValue = () => { - setReviewFormValue(initialReviewFormValue); + setFormValue(initialFormValue); + }; + + const value = { + isValid, + formValue, }; const reviewFormAction = { @@ -83,7 +92,7 @@ const ReviewFormProvider = ({ children }: PropsWithChildren) => { return ( - {children} + {children} ); }; diff --git a/src/hooks/queries/product/useInfiniteProductReviewsQuery.ts b/src/hooks/queries/product/useInfiniteProductReviewsQuery.ts index 8142c77e..8f778994 100644 --- a/src/hooks/queries/product/useInfiniteProductReviewsQuery.ts +++ b/src/hooks/queries/product/useInfiniteProductReviewsQuery.ts @@ -7,7 +7,6 @@ const fetchProductReviews = async (pageParam: number, productId: number, sort: s const res = await productApi.get({ params: `/${productId}/reviews`, queries: `?sort=${sort}&lastReviewId=${pageParam}`, - credentials: true, }); const data: ProductReviewResponse = await res.json(); diff --git a/src/mocks/handlers/reviewHandlers.ts b/src/mocks/handlers/reviewHandlers.ts index 656891e5..5aef00fb 100644 --- a/src/mocks/handlers/reviewHandlers.ts +++ b/src/mocks/handlers/reviewHandlers.ts @@ -10,13 +10,8 @@ import type { ReviewFavoriteRequestBody } from '@/types/review'; export const reviewHandlers = [ rest.get('/api/products/:productId/reviews', (req, res, ctx) => { - const { mockSessionId } = req.cookies; const sortOptions = req.url.searchParams.get('sort'); - if (!mockSessionId) { - return res(ctx.status(401)); - } - if (sortOptions === null) { return res(ctx.status(400)); } diff --git a/src/pages/ProductDetailPage/ProductDetailPage.tsx b/src/pages/ProductDetailPage/ProductDetailPage.tsx index 0367084c..04fd5d1b 100644 --- a/src/pages/ProductDetailPage/ProductDetailPage.tsx +++ b/src/pages/ProductDetailPage/ProductDetailPage.tsx @@ -1,9 +1,9 @@ import { BottomSheet, useBottomSheet } from '@fun-eat/design-system'; import { useQueryErrorResetBoundary } from '@tanstack/react-query'; import { Suspense } from 'react'; -import { useParams, useLocation, useNavigate } from 'react-router-dom'; +import { useParams, useLocation, useNavigate, Link } from 'react-router-dom'; -import { main, registerButton, registerButtonWrapper, section, sortWrapper } from './productDetailPage.css'; +import { link, linkWrapper, main, section, sortWrapper } from './productDetailPage.css'; import NotFoundPage from '../NotFoundPage'; import { @@ -66,7 +66,7 @@ export const ProductDetailPage = () => {
    }> - + @@ -86,11 +86,16 @@ export const ProductDetailPage = () => { - {/*로그인 여부에 따라 링크 경로*/} -
    - +
    + {member ? ( + + 리뷰 작성하기 + + ) : ( + + )}
    diff --git a/src/pages/ProductDetailPage/productDetailPage.css.ts b/src/pages/ProductDetailPage/productDetailPage.css.ts index 2291818f..02971751 100644 --- a/src/pages/ProductDetailPage/productDetailPage.css.ts +++ b/src/pages/ProductDetailPage/productDetailPage.css.ts @@ -1,5 +1,5 @@ import { vars } from '@/styles/theme.css'; -import { style, styleVariants } from '@vanilla-extract/css'; +import { style } from '@vanilla-extract/css'; export const main = style({ paddingBottom: 70, @@ -16,7 +16,7 @@ export const sortWrapper = style({ right: 20, }); -export const registerButtonWrapper = style({ +export const linkWrapper = style({ position: 'fixed', left: '50%', bottom: 0, @@ -32,7 +32,10 @@ export const registerButtonWrapper = style({ transform: 'translateX(-50%)', }); -const registerButtonBase = style({ +export const link = style({ + display: 'flex', + justifyContent: 'center', + alignItems: 'center', width: '100%', height: 56, backgroundColor: vars.colors.primary, @@ -40,8 +43,3 @@ const registerButtonBase = style({ borderRadius: 6, fontWeight: 700, }); - -export const registerButton = styleVariants({ - active: [registerButtonBase, { backgroundColor: vars.colors.primary }], - disabled: [registerButtonBase, { backgroundColor: vars.colors.background.tag }], -}); diff --git a/src/pages/ProductSearchListPage/ProductSearchListPage.tsx b/src/pages/ProductSearchListPage/ProductSearchListPage.tsx index 828710b0..fa9f1b8b 100644 --- a/src/pages/ProductSearchListPage/ProductSearchListPage.tsx +++ b/src/pages/ProductSearchListPage/ProductSearchListPage.tsx @@ -3,7 +3,7 @@ import { useSearchParams } from 'react-router-dom'; import { container } from './productSearchListPage.css'; -import { PageHeader } from '@/components/Common'; +import { TopBar } from '@/components/Common'; import { ProductOverviewList } from '@/components/Product'; import { useIntersectionObserver } from '@/hooks/common'; import { useInfiniteProductSearchResultsQuery } from '@/hooks/queries/search'; @@ -24,7 +24,11 @@ export const ProductSearchListPage = () => { return ( <> - + + + + +
    diff --git a/src/components/Search/RecipeSearchResultList/RecipeSearchResultList.tsx b/src/pages/RecipeSearchListPage/RecipeSearchListPage.tsx similarity index 50% rename from src/components/Search/RecipeSearchResultList/RecipeSearchResultList.tsx rename to src/pages/RecipeSearchListPage/RecipeSearchListPage.tsx index bdc5134b..8c0b0e70 100644 --- a/src/components/Search/RecipeSearchResultList/RecipeSearchResultList.tsx +++ b/src/pages/RecipeSearchListPage/RecipeSearchListPage.tsx @@ -1,19 +1,18 @@ import { useRef } from 'react'; -import { Link } from 'react-router-dom'; -import { styled } from 'styled-components'; +import { useSearchParams } from 'react-router-dom'; -import SearchNotFound from '../SearchNotFound/SearchNotFound'; +import { listWrapper } from './recipeSearchListPage.css'; -import { RecipeItem } from '@/components/Recipe'; -import { PATH } from '@/constants/path'; +import { TopBar } from '@/components/Common'; +import { DefaultRecipeItem } from '@/components/Recipe'; +import SearchNotFound from '@/components/Search/SearchNotFound/SearchNotFound'; import { useIntersectionObserver } from '@/hooks/common'; import { useInfiniteRecipeSearchResultsQuery } from '@/hooks/queries/search'; -interface RecipeSearchResultListProps { - searchQuery: string; -} +export const RecipeSearchListPage = () => { + const [searchParams, setSearchParams] = useSearchParams(); + const searchQuery = searchParams.get('query') || ''; -const RecipeSearchResultList = ({ searchQuery }: RecipeSearchResultListProps) => { const { data: searchResponse, fetchNextPage, hasNextPage } = useInfiniteRecipeSearchResultsQuery(searchQuery); const scrollRef = useRef(null); useIntersectionObserver(fetchNextPage, scrollRef, hasNextPage); @@ -26,24 +25,19 @@ const RecipeSearchResultList = ({ searchQuery }: RecipeSearchResultListProps) => return ( <> - + + + + + +
      {recipes.map((recipe) => (
    • - - - +
    • ))} - +
    ); }; - -export default RecipeSearchResultList; - -const RecipeSearchResultListContainer = styled.ul` - & > li + li { - margin-top: 40px; - } -`; diff --git a/src/pages/RecipeSearchListPage/recipeSearchListPage.css.ts b/src/pages/RecipeSearchListPage/recipeSearchListPage.css.ts new file mode 100644 index 00000000..5b47dfb5 --- /dev/null +++ b/src/pages/RecipeSearchListPage/recipeSearchListPage.css.ts @@ -0,0 +1,8 @@ +import { style } from '@vanilla-extract/css'; + +export const listWrapper = style({ + display: 'grid', + gridTemplateColumns: '1fr 1fr', + gap: '16px 10px', + padding: '0 20px', +}); diff --git a/src/pages/ReviewRegisterPage/ReviewRegisterPage.tsx b/src/pages/ReviewRegisterPage/ReviewRegisterPage.tsx index 06ca594d..b9251483 100644 --- a/src/pages/ReviewRegisterPage/ReviewRegisterPage.tsx +++ b/src/pages/ReviewRegisterPage/ReviewRegisterPage.tsx @@ -4,11 +4,13 @@ import { useParams } from 'react-router-dom'; import { section } from './reviewRegisterPage.css'; import NotFoundPage from '../NotFoundPage'; +import { TopBar } from '@/components/Common'; import { ReviewRegisterForm, ReviewTagSheet } from '@/components/Review'; -import ReviewFormProvider from '@/contexts/ReviewFormContext'; +import { useReviewFormValueContext } from '@/hooks/context'; export const ReviewRegisterPage = () => { const { productId } = useParams<{ productId: string }>(); + const { isValid } = useReviewFormValueContext(); const { isOpen, isClosing, handleOpenBottomSheet, handleCloseBottomSheet } = useBottomSheet(); if (!productId || isNaN(Number(productId))) { @@ -16,7 +18,11 @@ export const ReviewRegisterPage = () => { } return ( - + <> + + + +
    @@ -25,6 +31,6 @@ export const ReviewRegisterPage = () => {
    -
    + ); }; diff --git a/src/pages/ReviewRegisterPage/reviewRegisterPage.css.ts b/src/pages/ReviewRegisterPage/reviewRegisterPage.css.ts index 93a5f54f..408efe7b 100644 --- a/src/pages/ReviewRegisterPage/reviewRegisterPage.css.ts +++ b/src/pages/ReviewRegisterPage/reviewRegisterPage.css.ts @@ -2,5 +2,5 @@ import { style } from '@vanilla-extract/css'; export const section = style({ padding: '0 20px', - marginBottom: 32, + margin: '16px 0 32px', }); diff --git a/src/pages/SearchPage/SearchPage.tsx b/src/pages/SearchPage/SearchPage.tsx index 9f8562f7..d3b3a001 100644 --- a/src/pages/SearchPage/SearchPage.tsx +++ b/src/pages/SearchPage/SearchPage.tsx @@ -6,7 +6,7 @@ import { badgeContainer, searchWrapper, searchResultTitle, searchSection, subTit import { Text, Badge, ErrorBoundary, ErrorComponent, Loading, PageHeader } from '@/components/Common'; import { ProductSearchResultPreviewList, - RecipeSearchResultList, + RecipeSearchResultPreviewList, RecommendList, SearchInput, } from '@/components/Search'; @@ -87,7 +87,7 @@ export const SearchPage = () => { }> - +
    diff --git a/src/router/index.tsx b/src/router/index.tsx index 1ace78ba..acd76684 100644 --- a/src/router/index.tsx +++ b/src/router/index.tsx @@ -4,6 +4,7 @@ import App from './App'; import { AuthLayout } from '@/components/Layout'; import { PATH } from '@/constants/path'; +import ReviewFormProvider from '@/contexts/ReviewFormContext'; import NotFoundPage from '@/pages/NotFoundPage'; const router = createBrowserRouter([ @@ -146,15 +147,6 @@ const router = createBrowserRouter([ return { Component: ProductDetailPage }; }, }, - { - path: `${PATH.PRODUCT_LIST}/detail/:productId/review-register`, - async lazy() { - const { ReviewRegisterPage } = await import( - /* webpackChunkName: "ReviewRegisterPage" */ '@/pages/ReviewRegisterPage/ReviewRegisterPage' - ); - return { Component: ReviewRegisterPage }; - }, - }, ], }, /** 검색 페이지 */ @@ -188,6 +180,36 @@ const router = createBrowserRouter([ return { Component: ProductSearchListPage }; }, }, + { + path: `${PATH.SEARCH}/recipes`, + async lazy() { + const { RecipeSearchListPage } = await import( + /* webpackChunkName: "RecipeSearchListPage" */ '@/pages/RecipeSearchListPage/RecipeSearchListPage' + ); + return { Component: RecipeSearchListPage }; + }, + }, + ], + }, + /** 상품 리뷰 페이지 */ + { + path: '/', + element: ( + + + + ), + errorElement: , + children: [ + { + path: `${PATH.PRODUCT_LIST}/detail/:productId/review-register`, + async lazy() { + const { ReviewRegisterPage } = await import( + /* webpackChunkName: "ReviewRegisterPage" */ '@/pages/ReviewRegisterPage/ReviewRegisterPage' + ); + return { Component: ReviewRegisterPage }; + }, + }, ], }, ]);