diff --git a/src/component/common/Auth/Complete/Complete.module.scss b/src/component/Auth/Complete/Complete.module.scss similarity index 100% rename from src/component/common/Auth/Complete/Complete.module.scss rename to src/component/Auth/Complete/Complete.module.scss diff --git a/src/component/common/Auth/Complete/index.tsx b/src/component/Auth/Complete/index.tsx similarity index 100% rename from src/component/common/Auth/Complete/index.tsx rename to src/component/Auth/Complete/index.tsx diff --git a/src/component/common/Auth/PreviousStep/PreviousStep.module.scss b/src/component/Auth/PreviousStep/PreviousStep.module.scss similarity index 100% rename from src/component/common/Auth/PreviousStep/PreviousStep.module.scss rename to src/component/Auth/PreviousStep/PreviousStep.module.scss diff --git a/src/component/common/Auth/PreviousStep/index.tsx b/src/component/Auth/PreviousStep/index.tsx similarity index 100% rename from src/component/common/Auth/PreviousStep/index.tsx rename to src/component/Auth/PreviousStep/index.tsx diff --git a/src/component/common/Auth/ProgressBar/ProgressBar.module.scss b/src/component/Auth/ProgressBar/ProgressBar.module.scss similarity index 100% rename from src/component/common/Auth/ProgressBar/ProgressBar.module.scss rename to src/component/Auth/ProgressBar/ProgressBar.module.scss diff --git a/src/component/common/Auth/ProgressBar/index.tsx b/src/component/Auth/ProgressBar/index.tsx similarity index 54% rename from src/component/common/Auth/ProgressBar/index.tsx rename to src/component/Auth/ProgressBar/index.tsx index f1d07ab4..c4a6b6ef 100644 --- a/src/component/common/Auth/ProgressBar/index.tsx +++ b/src/component/Auth/ProgressBar/index.tsx @@ -3,17 +3,17 @@ import styles from './ProgressBar.module.scss'; interface ProgressBarProps { step: number; total: number; - progressTitle: { step: number; title: string }[]; + progressTitle: string; } export default function ProgressBar({ step, total, progressTitle }: ProgressBarProps) { return (
- {`${progressTitle[step].step}. ${progressTitle[step].title}`} - {`${progressTitle[step].step} / ${total}`} + {`${step}. ${progressTitle}`} + {`${step} / ${total}`}
- +
); } diff --git a/src/component/common/Auth/SubTitle/SubTitle.module.scss b/src/component/Auth/SubTitle/SubTitle.module.scss similarity index 100% rename from src/component/common/Auth/SubTitle/SubTitle.module.scss rename to src/component/Auth/SubTitle/SubTitle.module.scss diff --git a/src/component/common/Auth/SubTitle/index.tsx b/src/component/Auth/SubTitle/index.tsx similarity index 100% rename from src/component/common/Auth/SubTitle/index.tsx rename to src/component/Auth/SubTitle/index.tsx diff --git a/src/page/Auth/Signup/component/ErrorMessage/ErrorMessage.module.scss b/src/component/common/ErrorMessage/ErrorMessage.module.scss similarity index 100% rename from src/page/Auth/Signup/component/ErrorMessage/ErrorMessage.module.scss rename to src/component/common/ErrorMessage/ErrorMessage.module.scss diff --git a/src/page/Auth/Signup/component/ErrorMessage/index.tsx b/src/component/common/ErrorMessage/index.tsx similarity index 100% rename from src/page/Auth/Signup/component/ErrorMessage/index.tsx rename to src/component/common/ErrorMessage/index.tsx diff --git a/src/page/AddMenu/components/AddMenuImgModal/index.tsx b/src/page/AddMenu/components/AddMenuImgModal/index.tsx index 463313b1..d6cb6b99 100644 --- a/src/page/AddMenu/components/AddMenuImgModal/index.tsx +++ b/src/page/AddMenu/components/AddMenuImgModal/index.tsx @@ -2,7 +2,7 @@ import React, { useEffect } from 'react'; import { createPortal } from 'react-dom'; import { ReactComponent as CancelIcon } from 'assets/svg/addmenu/mobile-cancle-icon.svg'; import useAddMenuStore from 'store/addMenu'; -import ErrorMessage from 'page/Auth/Signup/component/ErrorMessage'; +import ErrorMessage from 'component/common/ErrorMessage'; import { ERRORMESSAGE } from 'page/ShopRegistration/constant/errorMessage'; import { UploadError } from 'utils/hooks/useImagesUpload'; import styles from './AddMenuImgModal.module.scss'; diff --git a/src/page/AddMenu/components/MenuImage/index.tsx b/src/page/AddMenu/components/MenuImage/index.tsx index 238e597e..76466f1e 100644 --- a/src/page/AddMenu/components/MenuImage/index.tsx +++ b/src/page/AddMenu/components/MenuImage/index.tsx @@ -5,7 +5,7 @@ import useMediaQuery from 'utils/hooks/useMediaQuery'; import useBooleanState from 'utils/hooks/useBooleanState'; import AddMenuImgModal from 'page/AddMenu/components/AddMenuImgModal'; import useAddMenuStore from 'store/addMenu'; -import ErrorMessage from 'page/Auth/Signup/component/ErrorMessage'; +import ErrorMessage from 'component/common/ErrorMessage'; import { ERRORMESSAGE } from 'page/ShopRegistration/constant/errorMessage'; import useImagesUpload from 'utils/hooks/useImagesUpload'; import styles from './MenuImage.module.scss'; diff --git a/src/page/Auth/Signup/component/UserEmail/index.tsx b/src/page/Auth/Signup/component/UserEmail/index.tsx index 138614e1..d162be14 100644 --- a/src/page/Auth/Signup/component/UserEmail/index.tsx +++ b/src/page/Auth/Signup/component/UserEmail/index.tsx @@ -3,7 +3,7 @@ import CustomButton from 'page/Auth/Signup/component/CustomButton'; import useValidateEmail from 'page/Auth/Signup/hooks/useValidateEmail'; import useAuthCheck from 'page/Auth/Signup/hooks/useAuthCheck'; import useVerification from 'page/Auth/Signup/hooks/useVerification'; -import ErrorMessage from 'page/Auth/Signup/component/ErrorMessage'; +import ErrorMessage from 'component/common/ErrorMessage'; import useRegisterInfo from 'store/registerStore'; import useTimer from 'page/Auth/Signup/hooks/useTimer'; import { useEffect } from 'react'; diff --git a/src/page/Auth/Signup/component/UserId/index.tsx b/src/page/Auth/Signup/component/UserId/index.tsx index 718a2352..b35dd568 100644 --- a/src/page/Auth/Signup/component/UserId/index.tsx +++ b/src/page/Auth/Signup/component/UserId/index.tsx @@ -2,7 +2,7 @@ import useMediaQuery from 'utils/hooks/useMediaQuery'; import CustomButton from 'page/Auth/Signup/component/CustomButton'; import useValidateEmail from 'page/Auth/Signup/hooks/useValidateEmail'; import useCheckEmailDuplicate from 'page/Auth/Signup/hooks/useCheckEmailDuplicate'; -import ErrorMessage from 'page/Auth/Signup/component/ErrorMessage'; +import ErrorMessage from 'component/common/ErrorMessage'; import styles from './UserId.module.scss'; export default function UserId() { diff --git a/src/page/Auth/Signup/component/UserPassword/index.tsx b/src/page/Auth/Signup/component/UserPassword/index.tsx index 61f34256..1405445e 100644 --- a/src/page/Auth/Signup/component/UserPassword/index.tsx +++ b/src/page/Auth/Signup/component/UserPassword/index.tsx @@ -6,7 +6,7 @@ import usePasswordConfirm from 'page/Auth/Signup/hooks/usePasswordConfirm'; import { User } from 'page/Auth/Signup/types/User'; import { SubmitHandler } from 'react-hook-form'; -import ErrorMessage from 'page/Auth/Signup/component/ErrorMessage'; +import ErrorMessage from 'component/common/ErrorMessage'; import useRegisterInfo from 'store/registerStore'; import styles from './UserPassword.module.scss'; diff --git a/src/page/Auth/Signup/index.tsx b/src/page/Auth/Signup/index.tsx index 699dd463..e4e97004 100644 --- a/src/page/Auth/Signup/index.tsx +++ b/src/page/Auth/Signup/index.tsx @@ -3,7 +3,7 @@ import { ReactComponent as Logo } from 'assets/svg/auth/koin-logo.svg'; import { ReactComponent as Back } from 'assets/svg/common/back-arrow.svg'; import { Link } from 'react-router-dom'; import ProgressBar from 'component/common/ProgressBar'; -import PreviousStep from 'component/common/Auth/PreviousStep'; +import PreviousStep from 'component/Auth/PreviousStep'; import { useRef } from 'react'; import OwnerData from './view/OwnerDataPage'; import TermsOfService from './view/TermsOfServicePage'; diff --git a/src/page/Auth/Signup/view/OwnerDataPage/index.tsx b/src/page/Auth/Signup/view/OwnerDataPage/index.tsx index 1984d8be..02ce1395 100644 --- a/src/page/Auth/Signup/view/OwnerDataPage/index.tsx +++ b/src/page/Auth/Signup/view/OwnerDataPage/index.tsx @@ -6,7 +6,7 @@ import SearchShop from 'page/ShopRegistration/component/Modal/SearchShop'; import { ReactComponent as FileImage } from 'assets/svg/auth/default-file.svg'; import CustomModal from 'component/common/CustomModal'; import useCheckOwnerData from 'page/Auth/Signup/hooks/useOwnerData'; -import ErrorMessage from 'page/Auth/Signup/component/ErrorMessage'; +import ErrorMessage from 'component/common/ErrorMessage'; import useFileController from 'page/Auth/Signup/hooks/useFileController'; import useCheckNext from 'page/Auth/Signup/hooks/useCheckNext'; import { useEffect } from 'react'; diff --git a/src/page/MyShopPage/components/EditShopInfoModal/EditShopInfoModal.module.scss b/src/page/MyShopPage/components/EditShopInfoModal/EditShopInfoModal.module.scss index 20bd0081..7b8a138a 100644 --- a/src/page/MyShopPage/components/EditShopInfoModal/EditShopInfoModal.module.scss +++ b/src/page/MyShopPage/components/EditShopInfoModal/EditShopInfoModal.module.scss @@ -138,6 +138,10 @@ margin-bottom: 24px; } + &__error-message { + margin-bottom: 5px; + } + &__header { font-size: 18px; color: #17518e; @@ -316,7 +320,15 @@ &__label { display: flex; - margin-bottom: 16px; + margin-bottom: 24px; + + &--error { + margin-bottom: 0; + } + } + + &__error-message { + margin-bottom: 5px; } &__header { diff --git a/src/page/MyShopPage/components/EditShopInfoModal/index.tsx b/src/page/MyShopPage/components/EditShopInfoModal/index.tsx index add1cb16..dc3ce700 100644 --- a/src/page/MyShopPage/components/EditShopInfoModal/index.tsx +++ b/src/page/MyShopPage/components/EditShopInfoModal/index.tsx @@ -1,13 +1,9 @@ -/* eslint-disable react-hooks/exhaustive-deps */ -import { - Dispatch, SetStateAction, useEffect, -} from 'react'; +import { Dispatch, SetStateAction, useEffect } from 'react'; import { ReactComponent as DeleteImgIcon } from 'assets/svg/addmenu/mobile-delete-new-image.svg'; import { MyShopInfoRes } from 'model/shopInfo/myShopInfo'; import { ReactComponent as ImgPlusIcon } from 'assets/svg/myshop/imgplus.svg'; import { DAY_OF_WEEK, WEEK } from 'utils/constant/week'; -import useShopRegistrationStore from 'store/shopRegistration'; -import { SubmitHandler, useForm } from 'react-hook-form'; +import { SubmitHandler, useForm, useWatch } from 'react-hook-form'; import { OwnerShop } from 'model/shopInfo/ownerShop'; import { zodResolver } from '@hookform/resolvers/zod'; import { useMutation } from '@tanstack/react-query'; @@ -21,10 +17,12 @@ import CheckSameTime from 'page/ShopRegistration/hooks/CheckSameTime'; import useModalStore from 'store/modalStore'; import useMediaQuery from 'utils/hooks/useMediaQuery'; import OperateTimeMobile from 'page/ShopRegistration/component/Modal/OperateTimeMobile'; -import { TOTAL_CATEGORY } from 'utils/constant/category'; import useImagesUpload from 'utils/hooks/useImagesUpload'; import { isKoinError, sendClientError } from '@bcsdlab/koin'; import showToast from 'utils/ts/showToast'; +import cn from 'utils/ts/className'; +import { ERRORMESSAGE } from 'page/ShopRegistration/constant/errorMessage'; +import ErrorMessage from 'component/common/ErrorMessage'; import styles from './EditShopInfoModal.module.scss'; interface EditShopInfoModalProps { @@ -44,31 +42,16 @@ export default function EditShopInfoModal({ setFalse: closeOperateTimeModal, value: isOperateTimeModalOpen, } = useBooleanState(false); - const { - imageFile, imgRef, saveImgFile, uploadError, setImageFile, - } = useImagesUpload(); const { - setName, setAddress, setPhone, setDeliveryPrice, setDescription, - setImageUrls, setDelivery, setPayBank, setPayCard, setCategoryId, - } = useShopRegistrationStore(); - const { - name, address, phone, deliveryPrice, description, imageUrls, - delivery, payBank, payCard, categoryId, removeImageUrl, - } = useShopRegistrationStore(); + imageFile, imgRef, saveImgFile, setImageFile, + } = useImagesUpload(); const { categoryList } = useShopCategory(); - const { - openTimeState, - closeTimeState, - shopClosedState, + openTimeState, closeTimeState, shopClosedState, resetOperatingTime, } = useModalStore(); - const openTimeArray = Object.values(openTimeState); - const closeTimeArray = Object.values(closeTimeState); - const shopClosedArray = Object.values(shopClosedState); - const { isAllSameTime, hasClosedDay, @@ -76,27 +59,67 @@ export default function EditShopInfoModal({ isAllClosed, } = CheckSameTime(); - const handleCategoryIdChange = (e: React.ChangeEvent) => { - setCategoryId(Number(e.target.value)); - }; - const { - handleSubmit, setValue, + register, control, handleSubmit, setValue, formState: { errors }, } = useForm({ resolver: zodResolver(OwnerShop), + defaultValues: { + ...shopInfo, + category_ids: shopInfo.shop_categories.map((category) => category.id), + open: shopInfo.open.map((day) => ({ + day_of_week: day.day_of_week, + closed: day.closed, + open_time: day.open_time, + close_time: day.close_time, + })), + }, }); + const imageUrls = useWatch({ control, name: 'image_urls' }); + const name = useWatch({ control, name: 'name' }); + const categoryId = useWatch({ control, name: 'category_ids' }); + const phone = useWatch({ control, name: 'phone' }); + const address = useWatch({ control, name: 'address' }); + const deliveryPrice = useWatch({ control, name: 'delivery_price' }); + const description = useWatch({ control, name: 'description' }); + const delivery = useWatch({ control, name: 'delivery' }); + const payCard = useWatch({ control, name: 'pay_card' }); + const payBank = useWatch({ control, name: 'pay_bank' }); + + const handleCategoryIdChange = (e: React.ChangeEvent) => { + setValue('category_ids', [Number(e.target.value), 0]); + }; + + const handleDeleteImage = (image: string) => { + setImageFile(imageFile.filter((img) => img !== image)); + }; + + const formatPhoneNumber = (inputNumber:string) => { + const phoneNumber = inputNumber.replace(/\D/g, ''); + const formattedPhoneNumber = phoneNumber.replace(/^(\d{3})(\d{4})(\d{4})$/, '$1-$2-$3'); + return formattedPhoneNumber; + }; + + const handlePhoneChange = (event:React.ChangeEvent) => { + const formattedValue = formatPhoneNumber(event.target.value); + setValue('phone', formattedValue); + }; + useEffect(() => { - if (imageFile && !uploadError) { // 초기에 이 값이 true기 때문에 imageUrls가 빈 배열로 초기화되고 있었음 - setImageUrls(imageFile); + if (imageFile.length > 0) { + setValue('image_urls', imageFile); + } else if (imageFile.length !== imageUrls.length) { + setImageFile(imageUrls); } - }, [imageFile, setImageUrls]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [imageFile]); const mutation = useMutation({ mutationFn: (form: OwnerShop) => putShop(shopInfo.id, form), onSuccess: () => { closeModal(); setIsSuccess(true); + resetOperatingTime(); }, onError: (e) => { if (isKoinError(e)) { @@ -106,20 +129,13 @@ export default function EditShopInfoModal({ sendClientError(e); }, }); + + const operateTimeState = useOperateTimeState(); + const openTimeArray = Object.values(openTimeState); + const closeTimeArray = Object.values(closeTimeState); + const shopClosedArray = Object.values(shopClosedState); + useEffect(() => { - setImageUrls(shopInfo.image_urls); - setImageFile(shopInfo.image_urls); - setName(shopInfo.name); - setAddress(shopInfo.address); - setPhone(shopInfo.phone); - setDeliveryPrice(shopInfo.delivery_price); - setDescription(shopInfo.description); - setDelivery(shopInfo.delivery); - setPayBank(shopInfo.pay_bank); - setPayCard(shopInfo.pay_card); - setCategoryId(shopInfo.shop_categories[1] - ? shopInfo.shop_categories[1].id - : TOTAL_CATEGORY); shopInfo.open.forEach((day, index) => { useModalStore.setState((prev) => ({ ...prev, @@ -137,38 +153,21 @@ export default function EditShopInfoModal({ }, })); }); - }, []); - const operateTimeState = useOperateTimeState(); - const holiday = WEEK.filter((day) => shopClosedState[day]).length > 0 - ? `매주 ${WEEK.filter((day) => shopClosedState[day]).join('요일, ')}요일` - : '휴무일 없음'; + }, [shopInfo.open]); useEffect(() => { - setValue('image_urls', imageUrls); const openValue = DAY_OF_WEEK.map((day, index) => ({ close_time: closeTimeArray[index], closed: shopClosedArray[index], day_of_week: day, open_time: openTimeArray[index], })); - // shop_categories[0]은 전체보기이므로 따로 처리 - if (shopInfo.shop_categories.length === 1) { - setValue('category_ids', [shopInfo.shop_categories[0].id]); - } else { - const categoryIds = shopInfo.shop_categories.map((category) => category.id); - setValue('category_ids', categoryIds); - } setValue('open', openValue); - setValue('delivery_price', Number(deliveryPrice)); - setValue('description', description); - setValue('delivery', delivery); - setValue('pay_bank', payBank); - setValue('pay_card', payCard); - setValue('name', name); - setValue('phone', phone); - setValue('address', address); - }, [imageUrls, openTimeState, closeTimeState, shopClosedState, deliveryPrice, - description, delivery, payBank, payCard, name, phone, address, categoryId]); + }, [closeTimeArray, openTimeArray, setValue, shopClosedArray]); + + const holiday = WEEK.filter((day) => shopClosedState[day]).length > 0 + ? `매주 ${WEEK.filter((day) => shopClosedState[day]).join('요일, ')}요일` + : '휴무일 없음'; const onSubmit: SubmitHandler = (data) => { mutation.mutate(data); @@ -193,10 +192,7 @@ export default function EditShopInfoModal({ {`Selected diff --git a/src/page/ShopRegistration/component/Modal/Category/index.tsx b/src/page/ShopRegistration/component/Modal/Category/index.tsx index 7b09a54d..d7bb374d 100644 --- a/src/page/ShopRegistration/component/Modal/Category/index.tsx +++ b/src/page/ShopRegistration/component/Modal/Category/index.tsx @@ -1,18 +1,16 @@ import useMyShop from 'query/shop'; import cn from 'utils/ts/className'; import { Category as CategoryProps } from 'model/category/shopCategory'; -import useShopRegistrationStore from 'store/shopRegistration'; +import { useFormContext, useWatch } from 'react-hook-form'; import styles from './Category.module.scss'; export default function Category() { const { categoryList } = useMyShop(); - const { - category, setCategory, setCategoryId, - } = useShopRegistrationStore(); + const { control, setValue } = useFormContext(); + const categoryId = useWatch({ control, name: 'category_ids' }); const handleCategoryClick = (categoryInfo: CategoryProps) => { - setCategory(categoryInfo.name); - setCategoryId(categoryInfo.id); + setValue('category_ids', [categoryInfo.id, 0]); }; return ( @@ -21,10 +19,10 @@ export default function Category() { ))} - + {values.id && } ); } diff --git a/src/page/ShopRegistration/constant/errorMessage.ts b/src/page/ShopRegistration/constant/errorMessage.ts index 4f261776..475caf31 100644 --- a/src/page/ShopRegistration/constant/errorMessage.ts +++ b/src/page/ShopRegistration/constant/errorMessage.ts @@ -5,6 +5,7 @@ export const ERRORMESSAGE: { [key: string]: string } = { address: '주소를 입력해주세요.', phone: '전화번호를 입력해주세요.', invalidPhone: '전화번호 형식이 올바르지 않습니다.', + invalidPrice: '배달금액 형식이 올바르지 않습니다', networkError: '네트워크 오류가 발생했습니다. 다시 시도해주세요.', 401: '이미지 등록에 실패했습니다. 다시 시도해주세요.', 404: '존재하지 않는 도메인입니다.', diff --git a/src/page/ShopRegistration/hooks/useStoreTimeSetUp.ts b/src/page/ShopRegistration/hooks/useStoreTimeSetUp.ts new file mode 100644 index 00000000..e28ed3b4 --- /dev/null +++ b/src/page/ShopRegistration/hooks/useStoreTimeSetUp.ts @@ -0,0 +1,26 @@ +import { OwnerShop } from 'model/shopInfo/ownerShop'; +import { useEffect } from 'react'; +import { UseFormSetValue } from 'react-hook-form'; +import useModalStore from 'store/modalStore'; +import { DAY_OF_WEEK } from 'utils/constant/week'; + +interface StoreTimeSetUpProps { + setValue : UseFormSetValue; +} + +export default function useStoreTimeSetUp({ setValue }: StoreTimeSetUpProps) { + const { openTimeState, closeTimeState, shopClosedState } = useModalStore(); + const openTimeArray = Object.values(openTimeState); + const closeTimeArray = Object.values(closeTimeState); + const shopClosedArray = Object.values(shopClosedState); + + useEffect(() => { + const openValue = DAY_OF_WEEK.map((day, index) => ({ + close_time: closeTimeArray[index], + closed: shopClosedArray[index], + day_of_week: day, + open_time: openTimeArray[index], + })); + setValue('open', openValue); + }, [closeTimeArray, openTimeArray, setValue, shopClosedArray]); +} diff --git a/src/page/ShopRegistration/view/Mobile/Main/Main.module.scss b/src/page/ShopRegistration/view/Mobile/Main/Main.module.scss index ce0cd881..67fd63f9 100644 --- a/src/page/ShopRegistration/view/Mobile/Main/Main.module.scss +++ b/src/page/ShopRegistration/view/Mobile/Main/Main.module.scss @@ -48,10 +48,40 @@ } &__main-menu { + display: flex; + overflow-y: hidden; + flex-direction: row; + white-space: nowrap; max-width: 295px; max-height: 200px; } + &__main-menu-image { + object-fit: contain; + max-width: 295px; + height: 200px; + } + + &__delete-img-button { + position: relative; + right: 15px; + display: flex; + justify-content: center; + align-items: center; + background-color: #f7941e; + border-radius: 50%; + height: 24px; + + svg { + width: 24px; + height: 10px; + } + + &:hover { + cursor: pointer; + } + } + &__text { margin-top: 8px; font-size: 14px; diff --git a/src/page/ShopRegistration/view/Mobile/Main/index.tsx b/src/page/ShopRegistration/view/Mobile/Main/index.tsx index 73b5d608..5b454d85 100644 --- a/src/page/ShopRegistration/view/Mobile/Main/index.tsx +++ b/src/page/ShopRegistration/view/Mobile/Main/index.tsx @@ -1,43 +1,50 @@ -/* eslint-disable react-hooks/exhaustive-deps */ import { ReactComponent as EmptyImgIcon } from 'assets/svg/shopRegistration/mobile-empty-img.svg'; -import useStepStore from 'store/useStepStore'; -import useShopRegistrationStore from 'store/shopRegistration'; -import { useEffect, useState } from 'react'; -import ErrorMessage from 'page/Auth/Signup/component/ErrorMessage'; +import { ReactComponent as MobileDeleteImgIcon } from 'assets/svg/addmenu/mobile-delete-new-image.svg'; +import { useEffect } from 'react'; import { ERRORMESSAGE } from 'page/ShopRegistration/constant/errorMessage'; +import ErrorMessage from 'component/common/ErrorMessage'; import cn from 'utils/ts/className'; import useImagesUpload from 'utils/hooks/useImagesUpload'; +import { useFormContext, useWatch } from 'react-hook-form'; import styles from './Main.module.scss'; -export default function Main() { - const [isError, setIsError] = useState(false); - const { increaseStep } = useStepStore(); +export default function Main({ onNext }:{ onNext: () => void }) { const { - imageFile, imgRef, saveImgFile, uploadError, - } = useImagesUpload(); + register, control, setValue, trigger, formState: { errors }, + } = useFormContext(); + + const name = useWatch({ control, name: 'name' }); + const address = useWatch({ control, name: 'address' }); + const imageUrls = useWatch({ control, name: 'image_urls' }); + const { - name, setName, address, setAddress, imageUrls, setImageUrls, - } = useShopRegistrationStore(); + imageFile, imgRef, saveImgFile, uploadError, setImageFile, + } = useImagesUpload(); - const handleNextClick = () => { - if (name === '' || address === '' || imageUrls.length === 0 || uploadError !== '') { - setIsError(true); - } else { - setIsError(false); - increaseStep(); - } + const handleDeleteImage = (url: string) => { + setImageFile(imageFile.filter((img) => img !== url)); }; useEffect(() => { - if (imageFile.length > 0 || uploadError !== '') setImageUrls(imageFile); - }, [imageFile]); + if (imageFile.length > 0) { + setValue('image_urls', imageFile); + } + }, [imageFile, setValue]); + + const handleNextClick = async () => { + const isValid = await trigger(['image_urls', 'name', 'address']); + if (!isValid) { + return; + } + onNext(); + }; return (
- {uploadError === '' && imageUrls.length === 0 && isError && } + {errors.image_urls && } {uploadError !== '' && }
- {name === '' && isError && } + {errors.name && }
- {address === '' && isError && } + {errors.address && }
diff --git a/src/page/ShopRegistration/view/Mobile/ShopCategory/index.tsx b/src/page/ShopRegistration/view/Mobile/ShopCategory/index.tsx index ddb93bee..252bdfe3 100644 --- a/src/page/ShopRegistration/view/Mobile/ShopCategory/index.tsx +++ b/src/page/ShopRegistration/view/Mobile/ShopCategory/index.tsx @@ -1,32 +1,28 @@ -import useStepStore from 'store/useStepStore'; import useMyShop from 'query/shop'; import cn from 'utils/ts/className'; import { Category as CategoryProps } from 'model/category/shopCategory'; -import useShopRegistrationStore from 'store/shopRegistration'; import { useState } from 'react'; -import ErrorMessage from 'page/Auth/Signup/component/ErrorMessage'; +import ErrorMessage from 'component/common/ErrorMessage'; import { ERRORMESSAGE } from 'page/ShopRegistration/constant/errorMessage'; +import { useFormContext, useWatch } from 'react-hook-form'; import styles from './ShopCategory.module.scss'; -export default function ShopCategory() { +export default function ShopCategory({ onNext }:{ onNext: () => void }) { const [isError, setIsError] = useState(false); const { categoryList } = useMyShop(); - const { increaseStep } = useStepStore(); - const { - category, setCategory, setCategoryId, - } = useShopRegistrationStore(); + const { control, setValue } = useFormContext(); + const categoryId = useWatch({ control, name: 'category_ids' }); const handleCategoryClick = (categoryInfo: CategoryProps) => { - setCategory(categoryInfo.name); - setCategoryId(categoryInfo.id); + setValue('category_ids', [categoryInfo.id, 0]); }; const handleNextClick = () => { - if (category.length === 0) { + if (!categoryId) { setIsError(true); } else { setIsError(false); - increaseStep(); + onNext(); } }; @@ -38,7 +34,7 @@ export default function ShopCategory() { ))}
- {category.length === 0 && isError && } + {isError && }
diff --git a/src/page/ShopRegistration/view/Mobile/ShopConfirmation/index.tsx b/src/page/ShopRegistration/view/Mobile/ShopConfirmation/index.tsx index ba4d2f84..67255b5a 100644 --- a/src/page/ShopRegistration/view/Mobile/ShopConfirmation/index.tsx +++ b/src/page/ShopRegistration/view/Mobile/ShopConfirmation/index.tsx @@ -1,27 +1,31 @@ -/* eslint-disable react-hooks/exhaustive-deps */ -import useStepStore from 'store/useStepStore'; -import useShopRegistrationStore from 'store/shopRegistration'; import useOperateTimeState from 'page/ShopRegistration/hooks/useOperateTimeState'; -import { SubmitHandler, useForm } from 'react-hook-form'; +import { SubmitHandler, useFormContext } from 'react-hook-form'; import { OwnerShop } from 'model/shopInfo/ownerShop'; -import { zodResolver } from '@hookform/resolvers/zod'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { postShop } from 'api/shop'; -import { useEffect } from 'react'; -import { DAY_OF_WEEK, WEEK } from 'utils/constant/week'; +import { WEEK } from 'utils/constant/week'; import useModalStore from 'store/modalStore'; import CheckSameTime from 'page/ShopRegistration/hooks/CheckSameTime'; import { isKoinError } from '@bcsdlab/koin'; import showToast from 'utils/ts/showToast'; +import useMyShop from 'query/shop'; import styles from './ShopConfirmation.module.scss'; -export const usePostData = (setStep: (step: number) => void) => { +interface UsePostDataProps { + onNext?:() => void +} + +export const usePostData = ({ onNext } : UsePostDataProps) => { const queryClient = useQueryClient(); + const { resetOperatingTime } = useModalStore(); const mutation = useMutation({ mutationFn: (form: OwnerShop) => postShop(form), onSuccess: () => { - setStep(5); + if (onNext) { + onNext(); + } queryClient.refetchQueries(); + resetOperatingTime(); }, onError: (e) => { if (isKoinError(e)) { @@ -32,134 +36,86 @@ export const usePostData = (setStep: (step: number) => void) => { return mutation; }; -export default function ShopConfirmation() { - const { setStep } = useStepStore(); - const { - category, - categoryId, - imageUrls, - name, - address, - phone, - deliveryPrice, - description, - delivery, - payBank, - payCard, - } = useShopRegistrationStore(); - +export default function ShopConfirmation({ onNext }:{ onNext: () => void }) { + const { categoryList } = useMyShop(); const operateTimeState = useOperateTimeState(); - const { handleSubmit, setValue } = useForm({ - resolver: zodResolver(OwnerShop), - }); - - const mutation = usePostData(setStep); - + const { handleSubmit, getValues } = useFormContext(); + const values = getValues(); + const categoryId = categoryList?.shop_categories[values.category_ids[0] - 1].name; + const mutation = usePostData({ onNext }); const onSubmit: SubmitHandler = (data) => { mutation.mutate(data); }; - const { openTimeState, closeTimeState, shopClosedState } = useModalStore(); + const { shopClosedState } = useModalStore(); const { isAllSameTime, hasClosedDay, isSpecificDayClosedAndAllSameTime } = CheckSameTime(); - const openTimeArray = Object.values(openTimeState); - const closeTimeArray = Object.values(closeTimeState); - const shopClosedArray = Object.values(shopClosedState); - - useEffect(() => { - const openValue = DAY_OF_WEEK.map((day, index) => ({ - close_time: closeTimeArray[index], - closed: shopClosedArray[index], - day_of_week: day, - open_time: openTimeArray[index], - })); - setValue('image_urls', imageUrls); - setValue('category_ids', [categoryId]); - setValue('name', name); - setValue('address', address); - setValue('phone', phone); - setValue('delivery_price', Number(deliveryPrice)); - setValue('description', description); - setValue('delivery', delivery); - setValue('pay_bank', payBank); - setValue('pay_card', payCard); - setValue('open', openValue); - }, [openTimeArray, closeTimeArray, shopClosedArray, categoryId, name, - address, phone, deliveryPrice, description, delivery, payBank, payCard, imageUrls]); - return (
카테고리 - {category} + {categoryId}
가게명 - {name} + {values.name}
주소정보 - {address} + {values.address}
전화번호 - {phone} + {values.phone}
배달금액 - {deliveryPrice === 0 ? '무료' : `${deliveryPrice}원`} + {values.delivery_price === 0 ? '무료' : `${values.delivery_price}원`}
운영시간 - { - isAllSameTime && !hasClosedDay ? ( -
- {operateTimeState.time} -
- ) - : null - } - { - isSpecificDayClosedAndAllSameTime ? ( -
-
{operateTimeState.time}
-
{operateTimeState.holiday}
-
- ) : null - } - { - !isAllSameTime && !isSpecificDayClosedAndAllSameTime ? ( - <> - {WEEK.map((day) => ( -
- {shopClosedState[day] ? `${operateTimeState[day]}` : `${day} : ${operateTimeState[day]}`} -
- ))} - - ) : null - } + {isAllSameTime && !hasClosedDay && ( +
+ {operateTimeState.time} +
+ )} + {isSpecificDayClosedAndAllSameTime && ( +
+
{operateTimeState.time}
+
{operateTimeState.holiday}
+
+ )} + {!isAllSameTime && !isSpecificDayClosedAndAllSameTime && ( + <> + {WEEK.map((day) => ( +
+ {shopClosedState[day] ? `${operateTimeState[day]}` : `${day} : ${operateTimeState[day]}`} +
+ ))} + + )}
기타정보 - {description} + {values.description}
diff --git a/src/page/ShopRegistration/view/Mobile/ShopEntry/index.tsx b/src/page/ShopRegistration/view/Mobile/ShopEntry/index.tsx index 2dab6b92..7818ac21 100644 --- a/src/page/ShopRegistration/view/Mobile/ShopEntry/index.tsx +++ b/src/page/ShopRegistration/view/Mobile/ShopEntry/index.tsx @@ -1,9 +1,7 @@ import { ReactComponent as Memo } from 'assets/svg/shopRegistration/memo.svg'; -import useStepStore from 'store/useStepStore'; import styles from './ShopEntry.module.scss'; -export default function ShopEntry() { - const { increaseStep } = useStepStore(); +export default function ShopEntry({ onNext }:{ onNext: () => void }) { return (
@@ -15,7 +13,7 @@ export default function ShopEntry() { 학생들에게 최신 가게 정보를 알려주세요. - +
); diff --git a/src/page/ShopRegistration/view/Mobile/Sub/index.tsx b/src/page/ShopRegistration/view/Mobile/Sub/index.tsx index 3f855e71..d5690671 100644 --- a/src/page/ShopRegistration/view/Mobile/Sub/index.tsx +++ b/src/page/ShopRegistration/view/Mobile/Sub/index.tsx @@ -1,37 +1,23 @@ -/* eslint-disable jsx-a11y/label-has-associated-control */ import OperateTimeMobile from 'page/ShopRegistration/component/Modal/OperateTimeMobile'; import useBooleanState from 'utils/hooks/useBooleanState'; -import useStepStore from 'store/useStepStore'; -import useShopRegistrationStore from 'store/shopRegistration'; import useOperateTimeState from 'page/ShopRegistration/hooks/useOperateTimeState'; import CheckSameTime from 'page/ShopRegistration/hooks/CheckSameTime'; import { WEEK } from 'utils/constant/week'; import useModalStore from 'store/modalStore'; -import { useState } from 'react'; -import ErrorMessage from 'page/Auth/Signup/component/ErrorMessage'; +import ErrorMessage from 'component/common/ErrorMessage'; import { ERRORMESSAGE } from 'page/ShopRegistration/constant/errorMessage'; import cn from 'utils/ts/className'; +import { useFormContext, useWatch } from 'react-hook-form'; +import useStoreTimeSetUp from 'page/ShopRegistration/hooks/useStoreTimeSetUp'; +import { OwnerShop } from 'model/shopInfo/ownerShop'; import styles from './Sub.module.scss'; -export default function Sub() { - const { increaseStep } = useStepStore(); +export default function Sub({ onNext }:{ onNext: () => void }) { const { value: showOperateTime, setTrue: openOperateTime, setFalse: closeOperateTime, } = useBooleanState(false); - const { - setPhone, setDeliveryPrice, setDescription, setDelivery, setPayBank, setPayCard, - } = useShopRegistrationStore(); - - const { - phone, - deliveryPrice, - description, - delivery, - payBank, - payCard, - } = useShopRegistrationStore(); const operateTimeState = useOperateTimeState(); const { @@ -40,25 +26,39 @@ export default function Sub() { isSpecificDayClosedAndAllSameTime, isAllClosed, } = CheckSameTime(); + const { shopClosedState } = useModalStore(); - const [isError, setIsError] = useState(false); - const formatPhoneNumber = (inputNumber: string) => { + const { + register, control, trigger, setValue, formState: { errors }, + } = useFormContext(); + + const phone = useWatch({ control, name: 'phone' }); + const deliveryPrice = useWatch({ control, name: 'delivery_price' }); + const description = useWatch({ control, name: 'description' }); + const delivery = useWatch({ control, name: 'delivery' }); + const payBank = useWatch({ control, name: 'pay_bank' }); + const payCard = useWatch({ control, name: 'pay_card' }); + + useStoreTimeSetUp({ setValue }); + + const formatPhoneNumber = (inputNumber:string) => { const phoneNumber = inputNumber.replace(/\D/g, ''); - const formattedPhoneNumber = phoneNumber.replace(/(\d{3})(\d{4})(\d{4})/, '$1-$2-$3'); - if (formattedPhoneNumber.length > 13) return formattedPhoneNumber.slice(0, 13); + const formattedPhoneNumber = phoneNumber.replace(/^(\d{3})(\d{4})(\d{4})$/, '$1-$2-$3'); return formattedPhoneNumber; }; - const phoneNumberPattern = /^\d{3}-\d{4}-\d{4}$/; - const isValidPhoneNumber = phoneNumberPattern.test(phone); - const handleNextClick = () => { - if (phone === '' || Number.isNaN(deliveryPrice) || !isValidPhoneNumber) { - setIsError(true); - } else { - setIsError(false); - increaseStep(); + const handlePhoneChange = (event:React.ChangeEvent) => { + const formattedValue = formatPhoneNumber(event.target.value); + setValue('phone', formattedValue); + }; + + const handleNextClick = async () => { + const isValid = await trigger(['phone', 'delivery_price']); + if (!isValid) { + return; } + onNext(); }; if (showOperateTime) { @@ -73,21 +73,28 @@ export default function Sub() { htmlFor="phone" className={cn({ [styles.form__label]: true, - [styles['form__label--error']]: (phone === '' || !isValidPhoneNumber) && isError, + [styles['form__label--error']]: errors.phone !== undefined, })} > 전화번호 setPhone(formatPhoneNumber(e.target.value))} value={phone} className={styles.form__input} + {...register('phone', { + required: true, + pattern: { + value: /^\d{3}-\d{3,4}-\d{4}$/, + message: ERRORMESSAGE.invalidPhone, + }, + onChange: handlePhoneChange, + })} />
- {phone === '' && isError && } - {(!isValidPhoneNumber && phone !== '' && isError) && } + {errors.phone && }
@@ -152,8 +161,8 @@ export default function Sub() { type="text" id="extra-info" className={styles.form__input} - onChange={(e) => setDescription(e.target.value)} value={description} + {...register('description')} />
@@ -161,9 +170,9 @@ export default function Sub() { setDelivery(e.target.checked)} className={styles['form__checkbox-input']} checked={delivery} + {...register('delivery')} /> 배달 가능 @@ -171,9 +180,9 @@ export default function Sub() { setPayCard(e.target.checked)} className={styles['form__checkbox-input']} checked={payCard} + {...register('pay_card')} /> 카드 가능 @@ -181,9 +190,9 @@ export default function Sub() { setPayBank(e.target.checked)} className={styles['form__checkbox-input']} checked={payBank} + {...register('pay_bank')} /> 계좌이체 가능 diff --git a/src/page/ShopRegistration/view/Mobile/index.tsx b/src/page/ShopRegistration/view/Mobile/index.tsx index aed91226..00be4503 100644 --- a/src/page/ShopRegistration/view/Mobile/index.tsx +++ b/src/page/ShopRegistration/view/Mobile/index.tsx @@ -1,58 +1,149 @@ -/* eslint-disable jsx-a11y/label-has-associated-control */ -import PreviousStep from 'component/common/Auth/PreviousStep'; -import ProgressBar from 'component/common/Auth/ProgressBar'; -import Complete from 'component/common/Auth/Complete'; -import SubTitle from 'component/common/Auth/SubTitle'; -import useStepStore from 'store/useStepStore'; +import PreviousStep from 'component/Auth/PreviousStep'; +import ProgressBar from 'component/Auth/ProgressBar'; +import Complete from 'component/Auth/Complete'; +import SubTitle from 'component/Auth/SubTitle'; import PROGRESS_TITLE from 'utils/constant/progress'; import ShopEntry from 'page/ShopRegistration/view/Mobile/ShopEntry'; import ShopCategory from 'page/ShopRegistration/view/Mobile/ShopCategory'; import Main from 'page/ShopRegistration/view/Mobile/Main'; import Sub from 'page/ShopRegistration/view/Mobile/Sub'; import ShopConfirmation from 'page/ShopRegistration/view/Mobile/ShopConfirmation'; +import { useFunnel } from 'utils/hooks/useFunnel'; +import { FormProvider, useForm } from 'react-hook-form'; import styles from './ShopRegistrationMobile.module.scss'; +const OPEN_DEFAULT_VALUES = [ + { + day_of_week: 'MONDAY', closed: false, open_time: '00:00', close_time: '00:00', + }, + { + day_of_week: 'TUESDAY', closed: false, open_time: '00:00', close_time: '00:00', + }, + { + day_of_week: 'WEDNESDAY', closed: false, open_time: '00:00', close_time: '00:00', + }, + { + day_of_week: 'THURSDAY', closed: false, open_time: '00:00', close_time: '00:00', + }, + { + day_of_week: 'FRIDAY', closed: false, open_time: '00:00', close_time: '00:00', + }, + { + day_of_week: 'SATURDAY', closed: false, open_time: '00:00', close_time: '00:00', + }, + { + day_of_week: 'SUNDAY', closed: false, open_time: '00:00', close_time: '00:00', + }, +]; + export default function ShopRegistrationMobile() { - const { TOTAL_STEP, step, decreaseStep } = useStepStore(); - // 임시로 step 0 일때 뒤로가기 버튼 삭제 + const { + Funnel, Step, setStep, currentStep, + } = useFunnel('가게 등록'); + + const currentIndex = PROGRESS_TITLE.findIndex((step) => step.title === currentStep); + + const decreaseStep = () => { + if (currentIndex > 0) { + setStep(PROGRESS_TITLE[currentIndex - 1].title); + } + }; + + const methods = useForm({ + defaultValues: { + category_ids: [], + delivery_price: 0, + description: '', + image_urls: [], + name: '', + phone: '', + address: '', + delivery: false, + pay_bank: false, + pay_card: false, + open: OPEN_DEFAULT_VALUES, + }, + }); + return ( -
- {step !== 0 && } + + {currentStep !== '가게 등록' && }
- {step === 0 && } - {step === 1 && ( - <> - - - - - )} - {step === 2 && ( - <> - - -
- - )} - {step === 3 && ( - <> - - - - - )} - {step === 4 && ( - <> - -
- - - - )} - {step === 5 && ( - - )} + + + setStep('가게 카테고리 설정')} /> + + + <> + + + setStep('메인 정보 입력')} /> + + + + <> + + +
setStep('세부 정보 입력')} /> + + + + <> + + + setStep('가게 정보 확인')} /> + + + + <> + +
+ + setStep('가게 등록 완료')} /> + + + + + +
-
+ ); } diff --git a/src/page/ShopRegistration/view/PC/ShopConfirmation/ShopConfirmation.module.scss b/src/page/ShopRegistration/view/PC/ShopConfirmation/ShopConfirmation.module.scss new file mode 100644 index 00000000..9a4972c4 --- /dev/null +++ b/src/page/ShopRegistration/view/PC/ShopConfirmation/ShopConfirmation.module.scss @@ -0,0 +1,177 @@ +.wrapper { + position: relative; +} + +.container { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + padding: 80px 0 94px; + + &__koin-logo { + width: 368px; + position: relative; + margin-bottom: 56px; + background-color: #ffffff; + } +} + +.form { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + gap: 16px; + + &__title { + display: block; + font-size: 18px; + margin-bottom: 8px; + } + + &__image-upload { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + width: 210px; + height: 93px; + border: 1px solid #d2dae2; + padding: 53px 80px 54px; + cursor: pointer; + + &--active { + display: flex; + flex-direction: column; + padding: 10px 20px; + width: 340px; + height: 180px; + } + } + + &__upload-file { + display: none; + } + + &__main-menu { + max-width: 370px; + max-height: 200px; + } + + &__main-item { + max-width: 370px; + display: flex; + } + + &__main-text { + width: 90%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + word-break: break-all; + color: #858585; + } + + &__cutlery-cross { + width: 64px; + height: 64px; + margin: 0 auto; + display: flex; + justify-content: center; + align-items: center; + position: relative; + } + + &__text { + text-align: center; + font-size: 14px; + display: block; + color: #858585; + margin-top: 8px; + } + + &__section { + display: flex; + justify-content: space-between; + gap: 16px; + margin-top: 8px; + } + + &__input { + width: 240px; + border: 1px solid #d2dae2; + height: 22px; + padding: 13px 16px; + + &:focus { + border-bottom: 1px solid black; + } + } + + &__input-large { + width: 336px; + border: 1px solid #d2dae2; + height: 22px; + padding: 13px 16px; + + &:focus { + border-bottom: 1px solid black; + } + } + + &__operate-time { + display: flex; + align-items: center; + width: 272px; + height: auto; + font-size: 16px; + color: #858585; + } + + &__checkbox { + display: flex; + flex-direction: row; + justify-content: flex-start; + gap: 24px; + width: 368px; + } + + &__checkbox-label { + display: flex; + align-items: center; + font-weight: 500; + font-size: 14px; + color: #858585; + box-sizing: content-box; + cursor: pointer; + + input[type="checkbox"]:checked + span { + color: #f7941e; + } + } + + &__checkbox-input { + appearance: none; + width: 14px; + height: 14px; + border-radius: 100%; + box-sizing: border-box; + border: 1px solid #858585; + margin-right: 8px; + background-size: cover; + cursor: pointer; + + &:checked { + border: 2px solid #f7941e; + padding: 1px; + background-clip: content-box; + background-color: #f7941e; + } + } + + &__next-button { + width: 368px; + margin-top: 56px; + } +} diff --git a/src/page/ShopRegistration/view/PC/ShopConfirmation/index.tsx b/src/page/ShopRegistration/view/PC/ShopConfirmation/index.tsx new file mode 100644 index 00000000..2efe31d6 --- /dev/null +++ b/src/page/ShopRegistration/view/PC/ShopConfirmation/index.tsx @@ -0,0 +1,370 @@ +import { ReactComponent as Logo } from 'assets/svg/auth/koin-logo.svg'; +import { ReactComponent as Cutlery } from 'assets/svg/shopRegistration/cutlery.svg'; +import Copyright from 'component/common/Copyright'; +import CustomButton from 'page/Auth/Signup/component/CustomButton'; +import Category from 'page/ShopRegistration/component/Modal/Category'; +import SearchShop from 'page/ShopRegistration/component/Modal/SearchShop'; +import OperateTimePC from 'page/ShopRegistration/component/Modal/OperateTimePC'; +import ConfirmPopup from 'page/ShopRegistration/component/ConfirmPopup'; +import CustomModal from 'component/common/CustomModal'; +import cn from 'utils/ts/className'; +import useModalStore from 'store/modalStore'; +import { WEEK } from 'utils/constant/week'; +import { SubmitHandler, useFormContext, useWatch } from 'react-hook-form'; +import { OwnerShop } from 'model/shopInfo/ownerShop'; +import useImagesUpload from 'utils/hooks/useImagesUpload'; +import CheckSameTime from 'page/ShopRegistration/hooks/CheckSameTime'; +import useOperateTimeState from 'page/ShopRegistration/hooks/useOperateTimeState'; +import ErrorMessage from 'component/common/ErrorMessage'; +import { ERRORMESSAGE } from 'page/ShopRegistration/constant/errorMessage'; +import { usePostData } from 'page/ShopRegistration/view/Mobile/ShopConfirmation/index'; +import { ReactComponent as FileImage } from 'assets/svg/auth/default-file.svg'; +import useMyShop from 'query/shop'; +import { useEffect, useState } from 'react'; +import useBooleanState from 'utils/hooks/useBooleanState'; +import useStoreTimeSetUp from 'page/ShopRegistration/hooks/useStoreTimeSetUp'; +import styles from './ShopConfirmation.module.scss'; + +export default function ShopConfirmation({ onNext }:{ onNext: () => void }) { + const { + value: showCategory, + setTrue: openCategory, + setFalse: closeCategory, + } = useBooleanState(false); + const { + value: showOperateTime, + setTrue: openOperateTime, + setFalse: closeOperateTime, + } = useBooleanState(false); + const { + value: showSearchShop, + setTrue: openSearchShop, + setFalse: closeSearchShop, + } = useBooleanState(false); + const { + value: showConfirmPopup, + setTrue: openConfirmPopup, + setFalse: closeConfirmPopup, + } = useBooleanState(false); + + const { shopClosedState } = useModalStore(); + + const { + isAllSameTime, + hasClosedDay, + isSpecificDayClosedAndAllSameTime, + isAllClosed, + } = CheckSameTime(); + + const { categoryList } = useMyShop(); + + const { + register, control, setValue, handleSubmit, formState: { errors }, + } = useFormContext(); + + const { + imageFile, imgRef, saveImgFile, uploadError, setImageFile, + } = useImagesUpload(); + + const [isError, setIsError] = useState(false); + + const operateTimeState = useOperateTimeState(); + + const imageUrls = useWatch({ control, name: 'image_urls' }); + const name = useWatch({ control, name: 'name' }); + const categoryId = useWatch({ control, name: 'category_ids' }); + const phone = useWatch({ control, name: 'phone' }); + const address = useWatch({ control, name: 'address' }); + const deliveryPrice = useWatch({ control, name: 'delivery_price' }); + const description = useWatch({ control, name: 'description' }); + const delivery = useWatch({ control, name: 'delivery' }); + const payCard = useWatch({ control, name: 'pay_card' }); + const payBank = useWatch({ control, name: 'pay_bank' }); + const selectedId = categoryList?.shop_categories[categoryId[0] - 1]?.name; + + useStoreTimeSetUp({ setValue }); + + const formatPhoneNumber = (inputNumber: string) => { + const phoneNumber = inputNumber.replace(/\D/g, ''); + const formattedPhoneNumber = phoneNumber.replace(/^(\d{3})(\d{4})(\d{4})$/, '$1-$2-$3'); + return formattedPhoneNumber; + }; + + const handlePhoneChange = (e: React.ChangeEvent) => { + const formattedValue = formatPhoneNumber(e.target.value); + setValue('phone', formattedValue); + }; + + const handleNextClick = () => { + if (imageUrls.length === 0 || name === '' || categoryId.length === 0 + || address === '' || phone === '') { + setIsError(true); + } else { + setIsError(false); + openConfirmPopup(); + } + }; + + const handleDeleteImage = (e: React.MouseEvent, imageUrl: string) => { + e.preventDefault(); + setImageFile(imageFile.filter((img) => img !== imageUrl)); + }; + + useEffect(() => { + if (imageFile.length > 0) { + setValue('image_urls', imageFile); + } + }, [imageFile, setValue]); + + const mutation = usePostData({ onNext }); + + const onSubmit: SubmitHandler = (data) => { + mutation.mutate(data); + }; + + return ( +
+
+ + +
+ 대표 이미지 + + {uploadError === '' && isError && imageUrls.length === 0 + && } + {uploadError !== '' && } +
+
+ 카테고리 +
+ + + + +
+ {isError && categoryId.length === 0 && } +
+ + + +
+ 가게명 +
+ + +
+ {isError && name === '' && } +
+ + + +
+ 주소정보 +
+ +
+ {isError && address === '' && } +
+
+ 전화번호 +
+ +
+ {isError && phone === '' && } +
+
+ 배달금액 +
+ (e.target as HTMLElement).blur()} + /> +
+
+
+ 운영시간 +
+
+
+ {isAllSameTime && !hasClosedDay && ( +
+ {operateTimeState.time} +
+ )} + {isSpecificDayClosedAndAllSameTime && ( +
+
{operateTimeState.time}
+
{operateTimeState.holiday}
+
+ )} + {!isAllSameTime && !isSpecificDayClosedAndAllSameTime && !isAllClosed && ( + <> + {WEEK.map((day) => ( +
+ {shopClosedState[day] ? `${operateTimeState[day]}` : `${day} : ${operateTimeState[day]}`} +
+ ))} + + )} + {isAllClosed && ( + 매일 휴무 + )} +
+
+ +
+
+ + + +
+ 기타사항 +
+ +
+
+
+ + + +
+
+ +
+ + +
+ +
+ ); +} diff --git a/src/page/ShopRegistration/view/PC/ShopEntry/ShopEntry.module.scss b/src/page/ShopRegistration/view/PC/ShopEntry/ShopEntry.module.scss new file mode 100644 index 00000000..a2f11362 --- /dev/null +++ b/src/page/ShopRegistration/view/PC/ShopEntry/ShopEntry.module.scss @@ -0,0 +1,56 @@ +.wrapper { + position: relative; +} + +.block { + height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + + &__writing-icon { + margin-bottom: 72px; + } + + &__title { + display: block; + text-align: center; + font-size: 36px; + font-weight: 700; + color: #175c8e; + margin-bottom: 24px; + height: 53px; + } + + &__text { + display: flex; + flex-direction: column; + gap: 5px; + text-align: center; + font-size: 16px; + font-weight: 400; + margin-bottom: 80px; + color: #858585; + + span { + line-height: 180%; + } + } + + &__next-button { + display: flex; + justify-content: center; + align-items: center; + width: 368px; + height: 48px; + font-weight: 500; + color: #ffffff; + background-color: #175c8e; + text-decoration: none; + + &:hover { + cursor: pointer; + } + } +} diff --git a/src/page/ShopRegistration/view/PC/ShopEntry/index.tsx b/src/page/ShopRegistration/view/PC/ShopEntry/index.tsx new file mode 100644 index 00000000..fe3a9dd3 --- /dev/null +++ b/src/page/ShopRegistration/view/PC/ShopEntry/index.tsx @@ -0,0 +1,29 @@ +import { ReactComponent as Memo } from 'assets/svg/shopRegistration/memo.svg'; +import Copyright from 'component/common/Copyright'; +import styles from './ShopEntry.module.scss'; + +export default function ShopEntry({ onNext }:{ onNext: () => void }) { + return ( +
+
+ + 가게 정보 기입 +
+ + 가게의 다양한 정보를 입력 및 수정하여 +
+ 학생들에게 최신 가게 정보를 알려주세요 +
+
+ +
+ +
+ ); +} diff --git a/src/page/ShopRegistration/view/PC/ShopRegistrationPC.module.scss b/src/page/ShopRegistration/view/PC/ShopRegistrationPC.module.scss index a5f3393c..d5b080d2 100644 --- a/src/page/ShopRegistration/view/PC/ShopRegistrationPC.module.scss +++ b/src/page/ShopRegistration/view/PC/ShopRegistrationPC.module.scss @@ -1,238 +1,3 @@ -input::-webkit-outer-spin-button, -input::-webkit-inner-spin-button { - appearance: none; - margin: 0; -} - .wrapper { position: relative; } - -.block { - height: 100vh; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - - &__writing-icon { - margin-bottom: 72px; - } - - &__title { - display: block; - text-align: center; - font-size: 36px; - font-weight: 700; - color: #175c8e; - margin-bottom: 24px; - height: 53px; - } - - &__text { - display: flex; - flex-direction: column; - gap: 5px; - text-align: center; - font-size: 16px; - font-weight: 400; - margin-bottom: 80px; - color: #858585; - - span { - line-height: 180%; - } - } - - &__next-button { - display: flex; - justify-content: center; - align-items: center; - width: 368px; - height: 48px; - font-weight: 500; - color: #ffffff; - background-color: #175c8e; - text-decoration: none; - - &:hover { - cursor: pointer; - } - } -} - -.container { - position: relative; - display: flex; - flex-direction: column; - align-items: center; - padding: 80px 0 94px; - - &__koin-logo { - width: 368px; - position: relative; - margin-bottom: 56px; - background-color: #ffffff; - } -} - -.form { - position: relative; - display: flex; - flex-direction: column; - align-items: center; - gap: 16px; - - &__title { - display: block; - font-size: 18px; - margin-bottom: 8px; - } - - &__image-upload { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - width: 210px; - height: 93px; - border: 1px solid #d2dae2; - padding: 53px 80px 54px; - cursor: pointer; - - &--active { - display: flex; - flex-direction: column; - padding: 10px 20px; - width: 340px; - height: 180px; - justify-content: flex-start; - } - } - - &__upload-file { - display: none; - } - - &__main-menu { - max-width: 370px; - max-height: 200px; - } - - &__main-item { - max-width: 370px; - display: flex; - z-index: 99; - } - - &__main-text { - width: 90%; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - word-break: break-all; - color: #858585; - } - - &__cutlery-cross { - width: 64px; - height: 64px; - margin: 0 auto; - display: flex; - justify-content: center; - align-items: center; - position: relative; - } - - &__text { - text-align: center; - font-size: 14px; - display: block; - color: #858585; - margin-top: 8px; - } - - &__section { - display: flex; - justify-content: space-between; - gap: 16px; - margin-top: 8px; - } - - &__input { - width: 240px; - border: 1px solid #d2dae2; - height: 22px; - padding: 13px 16px; - - &:focus { - border-bottom: 1px solid black; - } - } - - &__input-large { - width: 336px; - border: 1px solid #d2dae2; - height: 22px; - padding: 13px 16px; - - &:focus { - border-bottom: 1px solid black; - } - } - - &__operate-time { - display: flex; - align-items: center; - width: 272px; - height: auto; - font-size: 16px; - color: #858585; - } - - &__checkbox { - display: flex; - flex-direction: row; - justify-content: flex-start; - gap: 24px; - width: 368px; - } - - &__checkbox-label { - display: flex; - align-items: center; - font-weight: 500; - font-size: 14px; - color: #858585; - box-sizing: content-box; - cursor: pointer; - - input[type="checkbox"]:checked + span { - color: #f7941e; - } - } - - &__checkbox-input { - appearance: none; - width: 14px; - height: 14px; - border-radius: 100%; - box-sizing: border-box; - border: 1px solid #858585; - margin-right: 8px; - background-size: cover; - cursor: pointer; - - &:checked { - border: 2px solid #f7941e; - padding: 1px; - background-clip: content-box; - background-color: #f7941e; - } - } - - &__next-button { - width: 368px; - margin-top: 56px; - } -} diff --git a/src/page/ShopRegistration/view/PC/index.tsx b/src/page/ShopRegistration/view/PC/index.tsx index cee89e98..243c92c7 100644 --- a/src/page/ShopRegistration/view/PC/index.tsx +++ b/src/page/ShopRegistration/view/PC/index.tsx @@ -1,458 +1,71 @@ -/* eslint-disable react-hooks/exhaustive-deps */ -import { ReactComponent as Memo } from 'assets/svg/shopRegistration/memo.svg'; -import { ReactComponent as Logo } from 'assets/svg/auth/koin-logo.svg'; -import { ReactComponent as Cutlery } from 'assets/svg/shopRegistration/cutlery.svg'; -import { useEffect, useState } from 'react'; -import useStepStore from 'store/useStepStore'; import Copyright from 'component/common/Copyright'; -import CustomButton from 'page/Auth/Signup/component/CustomButton'; -import Complete from 'component/common/Auth/Complete'; -import Category from 'page/ShopRegistration/component/Modal/Category'; -import SearchShop from 'page/ShopRegistration/component/Modal/SearchShop'; -import OperateTimePC from 'page/ShopRegistration/component/Modal/OperateTimePC'; -import ConfirmPopup from 'page/ShopRegistration/component/ConfirmPopup'; -import useMediaQuery from 'utils/hooks/useMediaQuery'; -import useBooleanState from 'utils/hooks/useBooleanState'; -import CustomModal from 'component/common/CustomModal'; -import cn from 'utils/ts/className'; -import useModalStore from 'store/modalStore'; -import { WEEK, DAY_OF_WEEK } from 'utils/constant/week'; -import { SubmitHandler, useForm } from 'react-hook-form'; -import { zodResolver } from '@hookform/resolvers/zod'; +import Complete from 'component/Auth/Complete'; +import { FormProvider, useForm } from 'react-hook-form'; import { OwnerShop } from 'model/shopInfo/ownerShop'; -import useImagesUpload from 'utils/hooks/useImagesUpload'; -import CheckSameTime from 'page/ShopRegistration/hooks/CheckSameTime'; -import useOperateTimeState from 'page/ShopRegistration/hooks/useOperateTimeState'; -import useShopRegistrationStore from 'store/shopRegistration'; -import ErrorMessage from 'page/Auth/Signup/component/ErrorMessage'; -import { ERRORMESSAGE } from 'page/ShopRegistration/constant/errorMessage'; -import { usePostData } from 'page/ShopRegistration/view/Mobile/ShopConfirmation/index'; -import { ReactComponent as FileImage } from 'assets/svg/auth/default-file.svg'; +import { useFunnel } from 'utils/hooks/useFunnel'; import styles from './ShopRegistrationPC.module.scss'; +import ShopEntry from './ShopEntry'; +import ShopConfirmation from './ShopConfirmation'; -export default function ShopRegistrationPC() { - const { isMobile } = useMediaQuery(); - const { step, setStep } = useStepStore(); - const { - value: showCategory, - setTrue: openCategory, - setFalse: closeCategory, - changeValue: toggleCategory, - } = useBooleanState(false); - const { - value: showOperateTime, - setTrue: openOperateTime, - setFalse: closeOperateTime, - } = useBooleanState(false); - const { - value: showSearchShop, - setTrue: openSearchShop, - setFalse: closeSearchShop, - } = useBooleanState(false); - const { - value: showConfirmPopup, - setTrue: openConfirmPopup, - setFalse: closeConfirmPopup, - } = useBooleanState(false); - const { - imageFile, imgRef, saveImgFile, uploadError, setImageFile, - } = useImagesUpload(); - const [isError, setIsError] = useState(false); - - const { - openTimeState, - closeTimeState, - shopClosedState, - } = useModalStore(); - - const { - setImageUrls, - setName, - setDelivery, - setPayCard, - setPayBank, - setAddress, - setPhone, - setDeliveryPrice, - setDescription, - removeImageUrl, - } = useShopRegistrationStore(); - - const { - imageUrls, - categoryId, - category, - name, - delivery, - payCard, - payBank, - address, - phone, - deliveryPrice, - description, - } = useShopRegistrationStore(); - const operateTimeState = useOperateTimeState(); - - const { - isAllSameTime, - hasClosedDay, - isSpecificDayClosedAndAllSameTime, - isAllClosed, - } = CheckSameTime(); +const OPEN_DEFAULT_VALUES = [ + { + day_of_week: 'MONDAY', closed: false, open_time: '00:00', close_time: '00:00', + }, + { + day_of_week: 'TUESDAY', closed: false, open_time: '00:00', close_time: '00:00', + }, + { + day_of_week: 'WEDNESDAY', closed: false, open_time: '00:00', close_time: '00:00', + }, + { + day_of_week: 'THURSDAY', closed: false, open_time: '00:00', close_time: '00:00', + }, + { + day_of_week: 'FRIDAY', closed: false, open_time: '00:00', close_time: '00:00', + }, + { + day_of_week: 'SATURDAY', closed: false, open_time: '00:00', close_time: '00:00', + }, + { + day_of_week: 'SUNDAY', closed: false, open_time: '00:00', close_time: '00:00', + }, +]; - const mutation = usePostData(setStep); - - const { - register, handleSubmit, setValue, formState: { errors }, - } = useForm({ - resolver: zodResolver(OwnerShop), +export default function ShopRegistrationPC() { + const methods = useForm({ + defaultValues: { + category_ids: [], + delivery_price: 0, + description: '', + image_urls: [], + name: '', + phone: '', + address: '', + delivery: false, + pay_bank: false, + pay_card: false, + open: OPEN_DEFAULT_VALUES, + }, }); - const formatPhoneNumber = (inputNumber: string) => { - const phoneNumber = inputNumber.replace(/\D/g, ''); - const formattedPhoneNumber = phoneNumber.replace(/(\d{3})(\d{4})(\d{4})/, '$1-$2-$3'); - if (formattedPhoneNumber.length > 13) return formattedPhoneNumber.slice(0, 13); - return formattedPhoneNumber; - }; - const phoneNumberPattern = /^\d{3}-\d{4}-\d{4}$/; - const isValidPhoneNumber = phoneNumberPattern.test(phone); - const handleNextClick = () => { - if (imageUrls.length === 0 || name === '' || category.length === 0 - || address === '' || phone === '' || !isValidPhoneNumber) { - setIsError(true); - } else { - setIsError(false); - openConfirmPopup(); - } - }; - const openTimeArray = Object.values(openTimeState); - const closeTimeArray = Object.values(closeTimeState); - const shopClosedArray = Object.values(shopClosedState); - - const onClickRemoveImageUrl = (e: React.MouseEvent, imageUrl: string) => { - e.preventDefault(); - setImageFile(imageFile.filter((img) => img !== imageUrl)); - removeImageUrl(imageUrl); - }; + const { Funnel, Step, setStep } = useFunnel('가게 등록'); - useEffect(() => { - if (imageFile.length > 0 || uploadError !== '') setImageUrls(imageFile); - const openValue = DAY_OF_WEEK.map((day, index) => ({ - close_time: closeTimeArray[index], - closed: shopClosedArray[index], - day_of_week: day, - open_time: openTimeArray[index], - })); - setValue('open', openValue); - setValue('category_ids', [categoryId]); - setValue('delivery_price', Number(deliveryPrice)); - setValue('name', name); - setValue('image_urls', imageUrls); - }, [openTimeState, closeTimeState, shopClosedState, imageUrls, - imageFile, categoryId, deliveryPrice, uploadError, name]); - const onSubmit: SubmitHandler = (data) => { - mutation.mutate(data); - }; - - // step 1일 때 그리고 모바일에서 PC로 변경 될 때 카테고리 모달을 자동으로 켜줌 - useEffect(() => { - if (!isMobile && step === 1) { - toggleCategory(); - } - }, [isMobile]); return ( - <> - {step === 0 && ( -
-
- - 가게 정보 기입 -
- - 가게의 다양한 정보를 입력 및 수정하여 -
- 학생들에게 최신 가게 정보를 알려주세요 -
-
- -
- -
- )} - {step >= 1 && step <= 4 && ( -
-
- -
-
- 대표 이미지 - - {uploadError === '' && imageUrls.length === 0 && isError - && } - {uploadError !== '' && } -
-
- 카테고리 -
- - -
- {category.length === 0 - && isError - && } -
- - - -
- 가게명 -
- { - setName(e.target.value); - }} - /> - -
- {name === '' && isError && } -
- - - -
- 주소정보 -
- { - setAddress(e.target.value); - }} - /> -
- {address === '' && isError && } -
-
- 전화번호 -
- { - setPhone(formatPhoneNumber(e.target.value)); - }} - /> -
- {phone === '' && isError && } - {phone !== '' && !isValidPhoneNumber && isError && } -
-
- 배달금액 -
- { - setDeliveryPrice(Number(e.target.value)); - }} - /> -
-
-
- 운영시간 -
-
-
- { - isAllSameTime && !hasClosedDay ? ( -
- {operateTimeState.time} -
- ) - : null - } - { - isSpecificDayClosedAndAllSameTime ? ( -
-
{operateTimeState.time}
-
{operateTimeState.holiday}
-
- ) : null - } - { - !isAllSameTime && !isSpecificDayClosedAndAllSameTime && !isAllClosed ? ( - <> - {WEEK.map((day) => ( -
- {shopClosedState[day] ? `${operateTimeState[day]}` : `${day} : ${operateTimeState[day]}`} -
- ))} - - ) : null - } - { - isAllClosed ? ( - 매일 휴무 - ) : null - } -
-
- -
-
- - - -
- 기타사항 -
- { - setDescription(e.target.value); - }} - /> -
-
-
- - - -
-
- -
- - + + + + setStep('가게 정보 입력')} /> + + + setStep('가게 등록 완료')} /> + + +
+ +
- -
- )} - {step === 5 && ( -
- - -
- )} - + + + ); } diff --git a/src/store/modalStore.ts b/src/store/modalStore.ts index b2bcc5f5..54520f83 100644 --- a/src/store/modalStore.ts +++ b/src/store/modalStore.ts @@ -12,8 +12,9 @@ interface ModalStore { setOpenTimeState: (state: OperatingTime) => void; setCloseTimeState: (state: OperatingTime) => void; setShopClosedState: (state: { [key: string]: boolean }) => void; - setSearchShopState: (state: string) => void; // 수정 요망 - setSelectedShopId:(state:string) => void; // 수정 요망 + setSearchShopState: (state: string) => void; + setSelectedShopId:(state:string) => void; + resetOperatingTime: ()=> void; } const initialOperatingTime: OperatingTime = { @@ -40,13 +41,20 @@ const useModalStore = create((set) => ({ openTimeState: initialOperatingTime, closeTimeState: initialOperatingTime, shopClosedState: initialShopClosed, - searchShopState: '', // 수정 요망 - selectedShopId: '', // 수정 요망 + searchShopState: '', + selectedShopId: '', setOpenTimeState: (state) => set(() => ({ openTimeState: state })), setCloseTimeState: (state) => set(() => ({ closeTimeState: state })), setShopClosedState: (state) => set({ shopClosedState: state }), - setSearchShopState: (state) => set({ searchShopState: state }), // 수정 요망 - setSelectedShopId: (state) => set({ selectedShopId: state }), // 수정 요망 + setSearchShopState: (state) => set({ searchShopState: state }), + setSelectedShopId: (state) => set({ selectedShopId: state }), + resetOperatingTime: () => { + set(() => ({ + openTimeState: initialOperatingTime, + closeTimeState: initialOperatingTime, + shopClosedState: initialShopClosed, + })); + }, })); export default useModalStore; diff --git a/src/store/shopRegistration.ts b/src/store/shopRegistration.ts index 0b478eaf..ebb9dda4 100644 --- a/src/store/shopRegistration.ts +++ b/src/store/shopRegistration.ts @@ -39,7 +39,7 @@ const useShopRegistrationStore = create((set) => ({ deliveryPrice: 0, description: '', imageUrl: '', - imageUrls: ['aa'], + imageUrls: [], owner: '', name: '', phone: '', diff --git a/src/utils/constant/progress.ts b/src/utils/constant/progress.ts index 78a43bc4..29bc9dd9 100644 --- a/src/utils/constant/progress.ts +++ b/src/utils/constant/progress.ts @@ -1,4 +1,8 @@ const PROGRESS_TITLE = [ + { + step: 0, + title: '가게 등록', + }, { step: 1, title: '가게 카테고리 설정', diff --git a/src/utils/hooks/useFunnel.ts b/src/utils/hooks/useFunnel.ts new file mode 100644 index 00000000..cf3675c8 --- /dev/null +++ b/src/utils/hooks/useFunnel.ts @@ -0,0 +1,31 @@ +import { ReactElement, useState } from 'react'; + +export interface StepProps { + name: string; + children: ReactElement; +} + +export interface FunnelProps { + children: ReactElement[]; +} + +/** +* 단계별 입력 폼을 진행할 시 사용 권장 +* @param { string } defaultStep 시작할 초기 단계 +* @returns { Funnel, Step, setStep, currentStep } 단계별 입력 폼을 관리하는데 사용하는 유틸리티 +*/ +export const useFunnel = (defaultStep: string) => { + const [step, setStep] = useState(defaultStep); + + const Step = (props: StepProps) => props.children; + + function Funnel({ children }: FunnelProps): ReactElement | null { + const targetStep = children.find((childStep) => childStep.props.name === step); + + return targetStep ?? null; + } + + return { + Funnel, Step, setStep, currentStep: step, + } as const; +}; diff --git a/src/utils/hooks/useImagesUpload.ts b/src/utils/hooks/useImagesUpload.ts index 9eed66ac..bcd076c2 100644 --- a/src/utils/hooks/useImagesUpload.ts +++ b/src/utils/hooks/useImagesUpload.ts @@ -15,7 +15,6 @@ export default function useImagesUpload() { const saveImgFile = async () => { const files = imgRef.current?.files; - console.log(files?.length) // imageFile.length + files.length을 통해 저장된 이미지 + 새로 추가할 이미지의 개수를 파악함 if (files && (files.length > 3 || imageFile.length >= 3 || imageFile.length + files.length > 3)) { showToast('error', '파일은 3개까지 등록할 수 있습니다.')