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 fcca96df..4101c69b 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/Product/ProductRecipeList/ProductRecipeList.tsx b/src/components/Product/ProductRecipeList/ProductRecipeList.tsx index 385d8428..3c901d91 100644 --- a/src/components/Product/ProductRecipeList/ProductRecipeList.tsx +++ b/src/components/Product/ProductRecipeList/ProductRecipeList.tsx @@ -1,6 +1,9 @@ -import { container, moreItem } from './productRecipeList.css'; +import { Link } from 'react-router-dom'; -import { ShowAllButton } from '@/components/Common'; +import { container, moreItem, notFound, recipeLink } from './productRecipeList.css'; + +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'; @@ -19,7 +22,20 @@ const ProductRecipeList = ({ productId, productName }: ProductRecipeListProps) = const recipeToDisplay = displaySlice(true, recipes, 3); if (recipes.length === 0) { - return null; + return ( +
+ 검색 결과 없음 + + 아직 작성된 꿀조합이 없어요 + +
+ + + 꿀조합 작성하러 가기 + + +
+ ); } return ( diff --git a/src/components/Product/ProductRecipeList/productRecipeList.css.ts b/src/components/Product/ProductRecipeList/productRecipeList.css.ts index a6930606..dcb4e844 100644 --- a/src/components/Product/ProductRecipeList/productRecipeList.css.ts +++ b/src/components/Product/ProductRecipeList/productRecipeList.css.ts @@ -1,3 +1,4 @@ +import { vars } from '@/styles/theme.css'; import { style } from '@vanilla-extract/css'; export const container = style({ @@ -13,3 +14,21 @@ export const moreItem = style({ alignItems: 'center', minWidth: 108, }); + +export const notFound = style({ + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + gap: 6, + textAlign: 'center', +}); + +export const recipeLink = style({ + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + height: 34, + padding: '0 16px', + backgroundColor: vars.colors.gray2, + borderRadius: 44, +}); 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 8d689d3e..c069c38c 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/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/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 889e1abd..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 { @@ -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 78cc32f3..7e8701b7 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/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/router/index.tsx b/src/router/index.tsx index 843fabb2..d09f5b14 100644 --- a/src/router/index.tsx +++ b/src/router/index.tsx @@ -5,6 +5,7 @@ import App from './App'; import { AuthLayout } from '@/components/Layout'; import { PATH } from '@/constants/path'; import CategoryProvider from '@/contexts/CategoryContext'; +import ReviewFormProvider from '@/contexts/ReviewFormContext'; import NotFoundPage from '@/pages/NotFoundPage'; const router = createBrowserRouter([ @@ -179,15 +180,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 }; - }, - }, { path: PATH.SEARCH, async lazy() { @@ -224,6 +216,26 @@ const router = createBrowserRouter([ }, ], }, + { + 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 }; + }, + }, + ], + }, ]); export default router;