From b659fa25ebb0ff85aac1e62984bdd7bd403ddcb6 Mon Sep 17 00:00:00 2001 From: designsoo Date: Thu, 21 Mar 2024 05:01:45 +0900 Subject: [PATCH 1/7] =?UTF-8?q?feat:=20=EB=B3=84=EC=A0=90=20=EC=84=A0?= =?UTF-8?q?=ED=83=9D=20=EC=95=88=ED=96=88=EC=9D=84=20=EA=B2=BD=EC=9A=B0=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=20=EC=B2=98=EB=A6=AC=ED=95=A0=20=EB=B3=84=20?= =?UTF-8?q?=EC=95=84=EC=9D=B4=EC=BD=98=20=EC=97=85=EB=A1=9C=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/svgs/ic-star-error.svg | 3 +++ src/constants/images.ts | 4 ++++ 2 files changed, 7 insertions(+) create mode 100644 public/svgs/ic-star-error.svg diff --git a/public/svgs/ic-star-error.svg b/public/svgs/ic-star-error.svg new file mode 100644 index 00000000..a8a73465 --- /dev/null +++ b/public/svgs/ic-star-error.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/constants/images.ts b/src/constants/images.ts index 5294e81e..93864eca 100644 --- a/src/constants/images.ts +++ b/src/constants/images.ts @@ -21,6 +21,10 @@ export const SVGS = { url: '/svgs/ic-star-filled.svg', alt: '채워진 별 아이콘', }, + error: { + url: '/svgs/ic-star-error.svg', + alt: '에러 별 아이콘', + }, }, arrow: { From 2e5e9411f95c2ce473400e19d7159e708c134707 Mon Sep 17 00:00:00 2001 From: designsoo Date: Thu, 21 Mar 2024 05:04:47 +0900 Subject: [PATCH 2/7] =?UTF-8?q?feat:=20review-form=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=EB=A9=94=EC=8B=9C=EC=A7=80=20=EB=B0=8F=20regex=20=EC=83=81?= =?UTF-8?q?=EC=88=98=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/constants/inputValidation.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/constants/inputValidation.ts b/src/constants/inputValidation.ts index 1681ddc7..a805a2e6 100644 --- a/src/constants/inputValidation.ts +++ b/src/constants/inputValidation.ts @@ -11,8 +11,16 @@ export const ERROR_MESSAGE = { nickname: { min: '10자 이내로 입력해주세요', }, + review: { + min: '최소 5자 이상 입력해 주세요', + placeholder: '리뷰를 입력해 주세요', + }, + rating: { + min: '별점 1점 이상 등록해 주세요', + }, }; export const REGEX = { password: /^(?=.*[a-zA-Z])(?=.*\d)[a-zA-Z\d]{8,15}$/, + textarea: /\n/g, }; From d55b050b6d72d5653c493eaba8ae7339f79585ab Mon Sep 17 00:00:00 2001 From: designsoo Date: Thu, 21 Mar 2024 05:06:43 +0900 Subject: [PATCH 3/7] =?UTF-8?q?feat:=20=EB=A6=AC=EB=B7=B0=20=EB=AA=A8?= =?UTF-8?q?=EB=8B=AC=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ReviewModalContent.module.scss | 36 ++++++ .../ReviewModalContent/index.tsx | 108 ++++++++++++++++++ 2 files changed, 144 insertions(+) create mode 100644 src/components/mypage/MyReservations/ReviewModalContent/ReviewModalContent.module.scss create mode 100644 src/components/mypage/MyReservations/ReviewModalContent/index.tsx diff --git a/src/components/mypage/MyReservations/ReviewModalContent/ReviewModalContent.module.scss b/src/components/mypage/MyReservations/ReviewModalContent/ReviewModalContent.module.scss new file mode 100644 index 00000000..0eb02643 --- /dev/null +++ b/src/components/mypage/MyReservations/ReviewModalContent/ReviewModalContent.module.scss @@ -0,0 +1,36 @@ +.review-form { + &-header { + @include column-flexbox(start, start, 0.4rem); + } + + &-title { + @include text-style(16, $gray10, bold); + } + + &-date { + @include flexbox($gap: 0.4rem); + + &-text { + @include text-style(14, $gray40); + } + } + + &-rating { + padding: 1.6rem 0; + border-bottom: 0.1rem solid $black60; + } + + &-content { + padding: 1.6rem 0; + } + + &-buttons { + @include flexbox($gap: 1.2rem); + + button { + @include responsive(M) { + flex-grow: 1; + } + } + } +} diff --git a/src/components/mypage/MyReservations/ReviewModalContent/index.tsx b/src/components/mypage/MyReservations/ReviewModalContent/index.tsx new file mode 100644 index 00000000..282fba9b --- /dev/null +++ b/src/components/mypage/MyReservations/ReviewModalContent/index.tsx @@ -0,0 +1,108 @@ +import Image from 'next/image'; + +import { useState } from 'react'; + +import { zodResolver } from '@hookform/resolvers/zod'; +// import { useMutation, useQueryClient } from '@tanstack/react-query'; +import classNames from 'classnames/bind'; +import { FormProvider, useForm } from 'react-hook-form'; +import { z } from 'zod'; + +// import { MyReservations } from '@/apis/myReservations'; +import { SVGS, ERROR_MESSAGE, REGEX } from '@/constants'; + +import { BaseButton } from '@/components/commons/buttons'; +import { TextField } from '@/components/commons/inputs'; +import StarRating from '@/components/commons/StarRating'; + +import styles from './ReviewModalContent.module.scss'; + +const cx = classNames.bind(styles); + +const { url, alt } = SVGS.calendar.default; +const { review, rating } = ERROR_MESSAGE; +const MIN_LENGTH_TEXTAREA = 5; + +type ReviewModalContentProps = { + reservationId: number; + title: string; + date: string; + handleModalClose: (arg: string) => void; +}; + +const ReviewModalContent = ({ reservationId, title, date, handleModalClose }: ReviewModalContentProps) => { + const ReviewModalSchema = z.object({ + rating: z.number().min(1, { message: rating.min }), + content: z + .string() + .refine((content) => content.replace(REGEX.textarea, '').length >= MIN_LENGTH_TEXTAREA, { message: review.min }), + }); + + const methods = useForm({ + mode: 'all', + resolver: zodResolver(ReviewModalSchema), + }); + + const { handleSubmit, setValue, watch } = methods; + const selectedRating = watch('rating'); + const [currentRating, setCurrentRating] = useState(0); + + /* + const queryClient = useQueryClient(); + const { mutate: createReviewMutation } = useMutation({ + mutationFn: MyReservations.createReview, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['myReservations', reservationId, 'reviews'] }); + }, + }); + */ + + const handleCurrentRating = () => setCurrentRating(selectedRating); + + const handleStarClick = (rating: number) => { + setValue('rating', rating); + setCurrentRating(rating); + }; + + const onSubmit = (data: object) => { + console.log(data); + console.log(reservationId); + handleModalClose('submitReviewModal'); + // createReviewMutation(reservationId, data); + }; + + return ( + +
+
+

{title}

+
+ {alt} + {date} +
+
+ +
+ 별점 및 리뷰 등록 +
+ +
+
+ +
+
+ +
+ handleModalClose('submitReviewModal')}> + 취소 + + + 등록 + +
+
+
+ ); +}; + +export default ReviewModalContent; From e2abee3442ae566ff061436fd31774e21accb7c1 Mon Sep 17 00:00:00 2001 From: designsoo Date: Thu, 21 Mar 2024 05:08:39 +0900 Subject: [PATCH 4/7] =?UTF-8?q?refactor:=20=EB=B3=84=EC=A0=90=20=EC=84=A0?= =?UTF-8?q?=ED=83=9D=20=EC=95=88=ED=96=88=EC=9D=84=20=EA=B2=BD=EC=9A=B0=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=EC=B2=98=EB=A6=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/commons/StarRating/index.tsx | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/components/commons/StarRating/index.tsx b/src/components/commons/StarRating/index.tsx index 156612bf..f678d6ee 100644 --- a/src/components/commons/StarRating/index.tsx +++ b/src/components/commons/StarRating/index.tsx @@ -12,17 +12,20 @@ const { star } = SVGS; type StarRatingProps = { size: 'small' | 'medium' | 'large'; rating: number; + currentRating?: number; readonly?: boolean; onChange?: (arg: number) => void | undefined; }; -const StarRating = ({ size, onChange, rating = 0, readonly = false }: StarRatingProps) => { +const StarRating = ({ size, currentRating, onChange, rating = 0, readonly = false }: StarRatingProps) => { const TOTAL_RATING = 5; const OFFSET = 1; + const isEmptyPickStar = currentRating === undefined; const handleStarClick = (starId: number) => { if (!onChange) return; - onChange(starId + OFFSET); + const newRating = starId + OFFSET; + onChange(newRating); }; return ( @@ -41,7 +44,12 @@ const StarRating = ({ size, onChange, rating = 0, readonly = false }: StarRating ) : ( )} From f05939d64002b92827e06ace267aecb4b2a0ca17 Mon Sep 17 00:00:00 2001 From: designsoo Date: Thu, 21 Mar 2024 05:18:07 +0900 Subject: [PATCH 5/7] =?UTF-8?q?feat:=20=EC=B5=9C=EC=86=8C=20=EC=9E=85?= =?UTF-8?q?=EB=A0=A5=ED=95=B4=EC=95=BC=20=EB=90=A0=20=EA=B8=80=EC=9E=90=20?= =?UTF-8?q?=EC=88=98=20=EC=A7=80=EC=A0=95=20=EB=B0=8F=20=EC=97=90=EB=9F=AC?= =?UTF-8?q?=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../commons/inputs/TextField.module.scss | 8 +++++++- src/components/commons/inputs/TextField.tsx | 19 ++++++++++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/components/commons/inputs/TextField.module.scss b/src/components/commons/inputs/TextField.module.scss index 63ff22e9..9869439a 100644 --- a/src/components/commons/inputs/TextField.module.scss +++ b/src/components/commons/inputs/TextField.module.scss @@ -58,7 +58,13 @@ @include text-style(12, $gray50); } - .active { + &-current-num { + &.text-length-error { + color: $red; + } + } + + .active:not(.text-length-error) { color: $gray10; } } diff --git a/src/components/commons/inputs/TextField.tsx b/src/components/commons/inputs/TextField.tsx index 70f775d2..17bc3471 100644 --- a/src/components/commons/inputs/TextField.tsx +++ b/src/components/commons/inputs/TextField.tsx @@ -4,10 +4,14 @@ import { ChangeEvent, useState } from 'react'; import classNames from 'classnames/bind'; import { useFormContext } from 'react-hook-form'; +import { REGEX } from '@/constants'; + import styles from './TextField.module.scss'; const cx = classNames.bind(styles); +const MIN_LENGTH_TEXTAREA = 5; + type TextFieldProps = { name: string; label?: string; @@ -19,10 +23,14 @@ export const TextField = ({ name, label, maxLength = 700, ...props }: TextFieldP const { register, formState: { errors }, + watch, } = useFormContext(); const [textCount, setTextCount] = useState(0); const [isFocused, setIsFocused] = useState(false); + const contentValue = watch('content'); + const textLength = contentValue ? contentValue.replace(REGEX.textarea, '').length : 0; + const minLengthError = textLength < MIN_LENGTH_TEXTAREA; const isError = !!errors[name]?.message; const handleClick = () => { @@ -52,10 +60,19 @@ export const TextField = ({ name, label, maxLength = 700, ...props }: TextFieldP {...register(name, { onChange: (event) => handleChange(event), })} + maxLength={maxLength} {...props} />
- 0 })}>{textCount} + 4 }, + { 'text-length-error': minLengthError }, + )} + > + {textLength} + /{maxLength}
From a1c9372e58808e858aa0bc551fd225420adc4c49 Mon Sep 17 00:00:00 2001 From: designsoo Date: Thu, 21 Mar 2024 05:30:59 +0900 Subject: [PATCH 6/7] =?UTF-8?q?refactor:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20onChange,=20useState=20=EC=82=AD=EC=A0=9C=20?= =?UTF-8?q?=EB=B0=8F=20=EB=B6=88=EB=A6=AC=EC=96=B8=20=EB=B3=80=EC=88=98=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/commons/inputs/TextField.tsx | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/src/components/commons/inputs/TextField.tsx b/src/components/commons/inputs/TextField.tsx index 17bc3471..afab398e 100644 --- a/src/components/commons/inputs/TextField.tsx +++ b/src/components/commons/inputs/TextField.tsx @@ -1,5 +1,5 @@ /* eslint-disable jsx-a11y/click-events-have-key-events */ -import { ChangeEvent, useState } from 'react'; +import { useState } from 'react'; import classNames from 'classnames/bind'; import { useFormContext } from 'react-hook-form'; @@ -25,14 +25,15 @@ export const TextField = ({ name, label, maxLength = 700, ...props }: TextFieldP formState: { errors }, watch, } = useFormContext(); - const [textCount, setTextCount] = useState(0); - const [isFocused, setIsFocused] = useState(false); const contentValue = watch('content'); const textLength = contentValue ? contentValue.replace(REGEX.textarea, '').length : 0; - const minLengthError = textLength < MIN_LENGTH_TEXTAREA; + const isBelowMinLength = textLength < MIN_LENGTH_TEXTAREA; + const isValidLength = textLength >= MIN_LENGTH_TEXTAREA; const isError = !!errors[name]?.message; + const [isFocused, setIsFocused] = useState(false); + const handleClick = () => { setIsFocused(true); }; @@ -41,10 +42,6 @@ export const TextField = ({ name, label, maxLength = 700, ...props }: TextFieldP setIsFocused(false); }; - const handleChange = (event: ChangeEvent) => { - setTextCount(event.target.textLength); - }; - return (
@@ -57,9 +54,7 @@ export const TextField = ({ name, label, maxLength = 700, ...props }: TextFieldP >