Skip to content

Commit

Permalink
feat: 상품 리뷰 작성 구현 (#89)
Browse files Browse the repository at this point in the history
* feat: 리뷰 요청에서 세션아이디 제거

* feat: 리뷰 및 로그인 링크 연결

* feat: 탑바 등록 버튼 수정

* feat: 리뷰 작성 폼 유효성 검사

* feat: 태그 스타일 수정

* feat: 리뷰 태그 선택 바텀시트 닫기 버튼 추가

* feat: 꿀조합 전체보기 버튼 스타일 수정

* feat: 상품 관련 꿀조합 없을시 처리

* refactor: 콘솔로그 삭제

* fix: 스토리북 에러 수정

* style: 리뷰 폼 변수명 수정
  • Loading branch information
Leejin-Yang authored Apr 20, 2024
1 parent f751e9e commit 4685b8b
Show file tree
Hide file tree
Showing 19 changed files with 171 additions and 97 deletions.
2 changes: 1 addition & 1 deletion src/components/Common/TopBar/TopBar.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export const LeftTitleAndRegister: Story = {
return (
<TopBar {...args}>
<TopBar.LeftNavigationGroup title="타이틀" />
<TopBar.RegisterLink />
<TopBar.RegisterButton />
</TopBar>
);
},
Expand Down
16 changes: 9 additions & 7 deletions src/components/Common/TopBar/TopBar.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
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';

import LogoImage from '@/assets/logo.svg';
import { PATH } from '@/constants/path';
import { vars } from '@/styles/theme.css';


interface TopBarProps {
children?: React.ReactNode;
title?: string;
link?: string;
state?: unknown;
}

type RegisterButtonProps = ComponentPropsWithoutRef<'button'>;

const TopBar = ({ children }: TopBarProps) => {
return <header className={container}>{children}</header>;
};
Expand Down Expand Up @@ -58,13 +60,13 @@ const SearchLink = () => {
);
};

const RegisterLink = ({ link = '' }: TopBarProps) => {
const RegisterButton = ({ ...props }: RegisterButtonProps) => {
return (
<Link to={link}>
<Text size="caption1" color="disabled" weight="semiBold">
<button {...props}>
<Text as="span" size="caption1" weight="semiBold" className={register}>
등록
</Text>
</Link>
</button>
);
};

Expand All @@ -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;

Expand Down
8 changes: 8 additions & 0 deletions src/components/Common/TopBar/topBar.css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,11 @@ export const headerTitle = style({
fontSize: 18,
fontWeight: 600,
});

export const register = style({
selectors: {
'button:disabled > &': {
color: vars.colors.text.disabled,
},
},
});
22 changes: 19 additions & 3 deletions src/components/Product/ProductRecipeList/ProductRecipeList.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -19,7 +22,20 @@ const ProductRecipeList = ({ productId, productName }: ProductRecipeListProps) =
const recipeToDisplay = displaySlice(true, recipes, 3);

if (recipes.length === 0) {
return null;
return (
<div className={notFound}>
<img src={SearchNotFoundImage} width={335} alt="검색 결과 없음" />
<Text color="disabled" size="caption4">
아직 작성된 꿀조합이 없어요
</Text>
<div style={{ height: '6px' }} />
<Link to={PATH.RECIPE} className={recipeLink}>
<Text as="span" color="sub" weight="semiBold" size="caption2">
꿀조합 작성하러 가기
</Text>
</Link>
</div>
);
}

return (
Expand Down
19 changes: 19 additions & 0 deletions src/components/Product/ProductRecipeList/productRecipeList.css.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { vars } from '@/styles/theme.css';
import { style } from '@vanilla-extract/css';

export const container = style({
Expand All @@ -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,
});
21 changes: 6 additions & 15 deletions src/components/Review/ReviewRegisterForm/ReviewRegisterForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,39 +8,28 @@ 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';
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<ReviewRequest>({
Expand Down Expand Up @@ -81,7 +70,7 @@ const ReviewRegisterForm = ({ productId, openBottomSheet }: ReviewRegisterFormPr
};

return (
<form onSubmit={handleSubmit}>
<form id="review-form" onSubmit={handleSubmit}>
<div>
<h2 className={itemTitle} tabIndex={0}>
사진 등록
Expand All @@ -105,7 +94,9 @@ const ReviewRegisterForm = ({ productId, openBottomSheet }: ReviewRegisterFormPr
{reviewFormValue.tags.map((tag) => (
<li key={tag.id}>
<button type="button" onClick={handleTagSelect(tag)} className={tagButton}>
<Text as="span">{tag.name}</Text>
<Text as="span" size="caption2" weight="medium">
{tag.name}
</Text>
<SvgIcon variant="close2" width={8} height={8} fill="none" stroke={vars.colors.gray4} />
</button>
</li>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
4 changes: 3 additions & 1 deletion src/components/Review/ReviewTagList/ReviewTagList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
13 changes: 11 additions & 2 deletions src/components/Review/ReviewTagSheet/ReviewTagSheet.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className={container}>
<div className={closeWrapper}>
<button type="button" onClick={close}>
<SvgIcon variant="close2" stroke={vars.colors.black} width={20} height={20} />
</button>
</div>
<section className={section}>
<ReviewTagList />
</section>
Expand Down
12 changes: 10 additions & 2 deletions src/components/Review/ReviewTagSheet/reviewTagSheet.css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
23 changes: 10 additions & 13 deletions src/components/Search/SearchNotFound/SearchNotFound.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,18 @@ import { container } from './searchNotFound.css';
import SearchNotFoundImage from '@/assets/search-notfound.png';
import { Text } from '@/components/Common';


const SearchNotFound = () => {
return (
<>
<div className={container}>
<img src={SearchNotFoundImage} width={335} alt="검색 결과 없음" />
<Text color="sub" size="headline" weight="semiBold">
검색 결과가 없어요
</Text>
<div style={{ height: '6px' }} />
<Text color="disabled" size="caption4">
다른 키워드로 검색해보세요!
</Text>
</div>
</>
<div className={container}>
<img src={SearchNotFoundImage} width={335} alt="검색 결과 없음" />
<Text color="sub" size="headline" weight="semiBold">
검색 결과가 없어요
</Text>
<div style={{ height: '6px' }} />
<Text color="disabled" size="caption4">
다른 키워드로 검색해보세요!
</Text>
</div>
);
};

Expand Down
Loading

0 comments on commit 4685b8b

Please sign in to comment.