diff --git a/.github/workflows/reviewer.json b/.github/workflows/reviewer.json index c364bc4a..6bf97d8a 100644 --- a/.github/workflows/reviewer.json +++ b/.github/workflows/reviewer.json @@ -21,8 +21,8 @@ "team" : "business" }, { - "name": "김하나", - "githubName": "hanagertrudeKim", + "name": "윤해진", + "githubName": "haejinyun", "team" : "user" }, { @@ -51,4 +51,4 @@ "team" : "campus" } ] -} \ No newline at end of file +} diff --git a/.gitignore b/.gitignore index c79844cf..dd0eb50c 100644 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,7 @@ lerna-debug.log* .env.development.local .env.test.local .env.production.local +.prettierrc npm-debug.log* yarn-debug.log* diff --git a/.yarnrc.yml b/.yarnrc.yml index 27b57c1e..68700796 100644 --- a/.yarnrc.yml +++ b/.yarnrc.yml @@ -1 +1,2 @@ yarnPath: .yarn/releases/yarn-3.8.0.cjs +nodeLinker: pnp 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/component/common/ErrorMessage/ErrorMessage.module.scss b/src/component/common/ErrorMessage/ErrorMessage.module.scss new file mode 100644 index 00000000..2b2c4a31 --- /dev/null +++ b/src/component/common/ErrorMessage/ErrorMessage.module.scss @@ -0,0 +1,12 @@ +.warn { + display: flex; + align-items: center; + gap: 7px; + margin-top: 3px; + + &--phrase { + display: block; + color: #f05d3d; + font-size: 11px; + } +} diff --git a/src/component/common/ErrorMessage/index.tsx b/src/component/common/ErrorMessage/index.tsx new file mode 100644 index 00000000..d72c1842 --- /dev/null +++ b/src/component/common/ErrorMessage/index.tsx @@ -0,0 +1,21 @@ +import { ReactComponent as Warn } from 'assets/svg/auth/warning.svg'; +import styles from './ErrorMessage.module.scss'; + +interface ErrorMessageProps { + message: string | (string | undefined | null)[] +} + +export default function ErrorMessage({ message }: ErrorMessageProps) { + if (message.length > 0) { + return ( +
+ + + {typeof message === 'string' ? message : message[0]} + +
+ ); + } + + return null; +} diff --git a/src/page/AddMenu/components/AddMenuImgModal/index.tsx b/src/page/AddMenu/components/AddMenuImgModal/index.tsx index 9e326ee1..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/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 4e78cce5..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/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/AddMenu/components/MenuName/index.tsx b/src/page/AddMenu/components/MenuName/index.tsx index 56fa9fd5..9cb8404f 100644 --- a/src/page/AddMenu/components/MenuName/index.tsx +++ b/src/page/AddMenu/components/MenuName/index.tsx @@ -13,7 +13,8 @@ export default function MenuName({ isComplete }: MenuNameProps) { const { name, setName } = useAddMenuStore(); const { menuError } = useErrorMessageStore(); - const handleNameChange = (e : React.ChangeEvent) => { + const handleNameChange = (e: React.ChangeEvent) => { + e.preventDefault(); setName(e.target.value); }; @@ -21,9 +22,7 @@ export default function MenuName({ isComplete }: MenuNameProps) {
{isMobile ? (
-
- 메뉴명 -
+
메뉴명
{isComplete ? ( {name} ) : ( @@ -33,17 +32,17 @@ export default function MenuName({ isComplete }: MenuNameProps) { [styles['mobile__name-input--error']]: menuError, })} placeholder="예) 불족발 + 막국수 저녁 SET" - onChange={handleNameChange} + onChange={(e) => handleNameChange(e)} value={name} /> )} - {menuError && {menuError}} + {menuError && ( + {menuError} + )}
) : (
-
- 메뉴명 -
+
메뉴명
{isComplete ? ( {name} ) : ( @@ -53,11 +52,13 @@ export default function MenuName({ isComplete }: MenuNameProps) { [styles['name-input--error']]: menuError, })} placeholder="예) 불족발 + 막국수 저녁 SET" - onChange={handleNameChange} + onChange={(e) => handleNameChange(e)} value={name} /> )} - {menuError && {menuError}} + {menuError && ( + {menuError} + )}
)}
diff --git a/src/page/AddMenu/components/MenuPrice/MenuPrice.module.scss b/src/page/AddMenu/components/MenuPrice/MenuPrice.module.scss index 5be7d192..0a2c2b88 100644 --- a/src/page/AddMenu/components/MenuPrice/MenuPrice.module.scss +++ b/src/page/AddMenu/components/MenuPrice/MenuPrice.module.scss @@ -4,328 +4,8 @@ margin-top: 16px; margin-bottom: 24px; } - - &__header { - width: 100%; - display: flex; - justify-content: space-between; - align-items: center; - } - - &__header-title { - color: #175c8e; - font-size: 15px; - font-weight: 500; - - &--complete { - color: #175c8e; - font-size: 15px; - font-weight: 500; - margin-bottom: 12px; - } - } - - &__header-condition { - display: flex; - justify-content: center; - align-items: center; - gap: 9px; - - &__text { - color: #a1a1a1; - text-align: right; - font-size: 14px; - font-weight: 400; - } - - &__button { - background-color: white; - display: flex; - justify-content: center; - align-items: center; - } - - &__icon { - width: 16px; - height: 16px; - - &:hover { - cursor: pointer; - } - } - } - - &__price-info-input-box { - margin-top: 10px; - display: flex; - justify-content: space-between; - align-items: center; - gap: 4px; - } - - &__price-info-inputs { - display: flex; - width: 100%; - align-items: center; - gap: 8px; - - &__size-input { - width: 100%; - height: 35px; - border: 0.5px solid #898a8d; - padding: 8px; - box-sizing: border-box; - - &--disabled { - width: 100%; - height: 35px; - border: 0.5px solid #898a8d; - background-color: #898a8d; - padding: 8px; - box-sizing: border-box; - } - - &::placeholder { - color: #a1a1a1; - font-size: 13px; - font-weight: 400; - } - } - - &__price-input-box { - position: relative; - width: 100%; - } - - &__price-input { - width: 100%; - height: 35px; - border: 0.5px solid #898a8d; - padding: 8px; - box-sizing: border-box; - } - - &__price-input-won { - box-sizing: border-box; - position: absolute; - right: 8px; - top: 32%; - color: #a1a1a1; - font-size: 13px; - font-weight: 400; - } - } - - &__cancle-button { - background-color: white; - } - - &__cancle-icon { - width: 16px; - height: 16px; - margin-left: 4px; - - &:hover { - cursor: pointer; - } - } - - &__add-price-button { - display: flex; - margin-top: 16px; - gap: 8px; - background-color: white; - - &__icon { - width: 12px; - height: 12px; - } - - &__text { - color: #f7941e; - font-size: 13px; - font-weight: 700; - } - - &:hover { - cursor: pointer; - } - } - - &__price-info-text { - display: flex; - align-items: center; - gap: 10px; - font-size: 15px; - font-weight: 500; - } } .container { margin-top: 32px; } - -.header { - display: flex; - justify-content: space-between; - align-items: center; - height: 36px; - - &__title { - color: #175c8e; - font-size: 24px; - font-weight: 500; - } - - &__condition { - display: flex; - justify-content: center; - align-items: center; - gap: 17.5px; - } - - &__condition-button { - background-color: white; - } - - &__condition-text { - color: #a1a1a1; - text-align: right; - font-size: 20px; - font-weight: 400; - - &:hover { - cursor: pointer; - } - } - - &__condition-icon { - width: 21px; - height: 21px; - - &:hover { - cursor: pointer; - } - } -} - -.price-info-input-box { - margin-top: 24px; - display: flex; - gap: 16px; - align-items: center; -} - -.price-info-inputs { - display: flex; - gap: 16px; - align-items: center; - width: 100%; - - &__size-input { - box-sizing: border-box; - width: 335px; - height: 62px; - border: 0.5px solid #858585; - padding: 16px; - color: #252525; - font-size: 20px; - font-weight: 500; - outline: none; - - &--disabled { - box-sizing: border-box; - width: 335px; - height: 62px; - border: 0.5px solid #858585; - padding: 16px; - background-color: #898a8d; - outline: none; - } - - &::placeholder { - color: #a1a1a1; - font-size: 20px; - font-weight: 400; - } - } - - &__price-input-box { - position: relative; - box-sizing: border-box; - width: 100%; - height: 62px; - border: 0.5px solid #858585; - } - - &__price-input { - text-align: right; - box-sizing: border-box; - width: 95%; - height: 60px; - padding: 16px; - color: #252525; - font-size: 20px; - font-weight: 500; - border: 0; - outline: none; - } - - &__price-input-won { - position: absolute; - top: 32%; - right: 10px; - color: #a1a1a1; - font-size: 20px; - font-weight: 400; - } -} - -.cancle-button { - width: 24px; - height: 24px; - background-color: white; - - &__icon { - width: 24px; - height: 24px; - - &:hover { - cursor: pointer; - } - } -} - -.add-price-button { - display: flex; - align-items: center; - margin-top: 16px; - gap: 8px; - height: 28px; - background-color: white; - - &__icon { - width: 24px; - height: 24px; - } - - &__text { - display: flex; - align-items: center; - color: #f7941e; - font-size: 20px; - font-weight: 700; - } - - &:hover { - cursor: pointer; - } -} - -.price-info-text { - display: flex; - align-items: center; - gap: 10px; - font-size: 20px; - font-weight: 500; -} diff --git a/src/page/AddMenu/components/MenuPrice/components/PriceDisplay/PriceDisplay.module.scss b/src/page/AddMenu/components/MenuPrice/components/PriceDisplay/PriceDisplay.module.scss new file mode 100644 index 00000000..93e64b8e --- /dev/null +++ b/src/page/AddMenu/components/MenuPrice/components/PriceDisplay/PriceDisplay.module.scss @@ -0,0 +1,48 @@ +@use "src/utils/styles/mediaQuery" as media; + +.header { + display: flex; + align-items: center; + height: 117px; + border-bottom: 2px solid #175c8e; + + @include media.media-breakpoint-down(mobile) { + width: 100%; + height: auto; + display: flex; + justify-content: space-between; + align-items: center; + border: none; + } + + &__title { + margin-right: auto; + height: 47px; + font-size: 36px; + font-weight: 500; + color: #175c8e; + font-family: "Noto Sans CJK KR", sans-serif; + + @include media.media-breakpoint-down(mobile) { + height: auto; + font-size: 15px; + margin-bottom: 12px; + } + } +} + +.price { + display: flex; + align-items: center; + gap: 10px; + font-size: 20px; + font-weight: 500; + + @include media.media-breakpoint-down(mobile) { + display: flex; + align-items: center; + gap: 10px; + font-size: 15px; + font-weight: 500; + } +} diff --git a/src/page/AddMenu/components/MenuPrice/components/PriceDisplay/index.tsx b/src/page/AddMenu/components/MenuPrice/components/PriceDisplay/index.tsx new file mode 100644 index 00000000..a8cc399f --- /dev/null +++ b/src/page/AddMenu/components/MenuPrice/components/PriceDisplay/index.tsx @@ -0,0 +1,47 @@ +import { OptionPrices } from 'store/addMenu'; +import styles from './PriceDisplay.module.scss'; + +interface PriceDisplayProps { + singlePrice: number; + isSingle: boolean; + optionPrices: OptionPrices[] | null +} + +export default function PriceDisplay({ + singlePrice, + isSingle, + optionPrices, +}: PriceDisplayProps) { + return ( + <> +
+
가격
+
+ {isSingle ? ( +
+
+
+ {singlePrice} + 원 +
+
+
+ ) : ( + (optionPrices || []).map((input) => ( +
+
+
+ {input.option} +
+
/
+
+ {input.price} + 원 +
+
+
+ )) + )} + + ); +} diff --git a/src/page/AddMenu/components/MenuPrice/components/PriceInput/PriceInput.module.scss b/src/page/AddMenu/components/MenuPrice/components/PriceInput/PriceInput.module.scss new file mode 100644 index 00000000..881d4e31 --- /dev/null +++ b/src/page/AddMenu/components/MenuPrice/components/PriceInput/PriceInput.module.scss @@ -0,0 +1,232 @@ +@use "src/utils/styles/mediaQuery" as media; + +.header { + display: flex; + justify-content: space-between; + align-items: center; + height: 36px; + + @include media.media-breakpoint-down(mobile) { + width: 100%; + display: flex; + justify-content: space-between; + align-items: center; + } + + &__title { + color: #175c8e; + font-size: 24px; + font-weight: 500; + + @include media.media-breakpoint-down(mobile) { + color: #175c8e; + font-size: 15px; + font-weight: 500; + } + } +} + +.price-box { + margin-top: 24px; + display: flex; + gap: 16px; + align-items: center; + + @include media.media-breakpoint-down(mobile) { + margin-top: 10px; + display: flex; + justify-content: space-between; + align-items: center; + gap: 4px; + } +} + +.inputs { + display: flex; + gap: 16px; + align-items: center; + width: 100%; + + @include media.media-breakpoint-down(mobile) { + display: flex; + width: 100%; + align-items: center; + gap: 8px; + } + + &__size-input { + box-sizing: border-box; + width: 335px; + height: 62px; + border: 0.5px solid #858585; + padding: 16px; + color: #252525; + font-size: 20px; + font-weight: 500; + outline: none; + + @include media.media-breakpoint-down(mobile) { + width: 100%; + height: 35px; + border: 0.5px solid #898a8d; + padding: 8px; + box-sizing: border-box; + } + + &--disabled { + box-sizing: border-box; + width: 335px; + height: 62px; + border: 0.5px solid #858585; + padding: 16px; + background-color: #898a8d; + outline: none; + + @include media.media-breakpoint-down(mobile) { + width: 100%; + height: 35px; + border: 0.5px solid #898a8d; + background-color: #898a8d; + padding: 8px; + box-sizing: border-box; + } + } + + &::placeholder { + color: #a1a1a1; + font-size: 20px; + font-weight: 400; + + @include media.media-breakpoint-down(mobile) { + color: #a1a1a1; + font-size: 13px; + font-weight: 400; + } + } + } + + &__price-input-box { + position: relative; + box-sizing: border-box; + width: 100%; + height: 62px; + border: 0.5px solid #858585; + + @include media.media-breakpoint-down(mobile) { + position: relative; + height: auto; + width: 100%; + border: none; + } + } + + &__price-input { + text-align: right; + box-sizing: border-box; + width: 95%; + height: 60px; + padding: 16px; + color: #252525; + font-size: 20px; + font-weight: 500; + border: 0; + outline: none; + + @include media.media-breakpoint-down(mobile) { + width: 100%; + height: 35px; + border: 0.5px solid #898a8d; + padding: 8px; + text-align: left; + box-sizing: border-box; + } + } + + &__price-input-won { + position: absolute; + top: 32%; + right: 10px; + color: #a1a1a1; + font-size: 20px; + font-weight: 400; + + @include media.media-breakpoint-down(mobile) { + box-sizing: border-box; + position: absolute; + right: 8px; + top: 32%; + color: #a1a1a1; + font-size: 13px; + font-weight: 400; + } + } +} + +.cancle { + width: 24px; + height: 24px; + background-color: white; + + @include media.media-breakpoint-down(mobile) { + width: auto; + height: auto; + background-color: white; + } + + &__icon { + width: 24px; + height: 24px; + + @include media.media-breakpoint-down(mobile) { + width: 16px; + height: 16px; + margin-left: 4px; + } + + &:hover { + cursor: pointer; + } + } +} + +.add-price-button { + display: flex; + align-items: center; + margin-top: 16px; + gap: 8px; + height: 28px; + background-color: white; + + @include media.media-breakpoint-down(mobile) { + display: flex; + margin-top: 16px; + gap: 8px; + background-color: white; + } + + &__icon { + width: 24px; + height: 24px; + + @include media.media-breakpoint-down(mobile) { + width: 12px; + height: 12px; + } + } + + &__text { + display: flex; + align-items: center; + color: #f7941e; + font-size: 20px; + font-weight: 700; + + @include media.media-breakpoint-down(mobile) { + font-size: 13px; + } + } + + &:hover { + cursor: pointer; + } +} diff --git a/src/page/AddMenu/components/MenuPrice/components/PriceInput/index.tsx b/src/page/AddMenu/components/MenuPrice/components/PriceInput/index.tsx new file mode 100644 index 00000000..147212ed --- /dev/null +++ b/src/page/AddMenu/components/MenuPrice/components/PriceInput/index.tsx @@ -0,0 +1,98 @@ +import { OptionPrices } from 'store/addMenu'; +import { ReactComponent as MobileDeleteIcon } from 'assets/svg/addmenu/mobile-delete-icon.svg'; +import { ReactComponent as MobilePlusIcon } from 'assets/svg/addmenu/mobile-plus-icon.svg'; +import styles from './PriceInput.module.scss'; + +interface PriceInputProps { + singlePrice: number; + isSingle: boolean; + optionPrices: OptionPrices[] | null + setSinglePrice: (price: number) => void; + updatePriceInput: (inputId: number, field: string, newValue: string | number) => void; + deletePriceInput: (price: number) => void; + addPriceInput: () => void; +} + +export default function PriceInput({ + singlePrice, + isSingle, + optionPrices, + setSinglePrice, + updatePriceInput, + deletePriceInput, + addPriceInput, +}: PriceInputProps) { + return ( + <> +
+
가격
+
+ {isSingle ? ( +
+
+
+ setSinglePrice(e.target.value === '' ? 0 : Number(e.target.value))} + /> +

+ 원 +

+
+
+
+ ) : ( + (optionPrices || []).map((input) => ( +
+
+ updatePriceInput(input.id, 'option', e.target.value)} + disabled={isSingle} + /> +
+ updatePriceInput(input.id, 'price', Number(e.target.value))} + /> +

+ 원 +

+
+
+ +
+ )) + )} + + + ); +} diff --git a/src/page/AddMenu/components/MenuPrice/index.tsx b/src/page/AddMenu/components/MenuPrice/index.tsx index 60f5917a..44e0e600 100644 --- a/src/page/AddMenu/components/MenuPrice/index.tsx +++ b/src/page/AddMenu/components/MenuPrice/index.tsx @@ -1,9 +1,7 @@ import useMediaQuery from 'utils/hooks/useMediaQuery'; import useAddMenuStore from 'store/addMenu'; -import { ReactComponent as PlusIcon } from 'assets/svg/main/plus.svg'; -import { ReactComponent as DeleteIcon } from 'assets/svg/addmenu/delete-icon.svg'; -import { ReactComponent as MobileDeleteIcon } from 'assets/svg/addmenu/mobile-delete-icon.svg'; -import { ReactComponent as MobilePlusIcon } from 'assets/svg/addmenu/mobile-plus-icon.svg'; +import PriceDisplay from './components/PriceDisplay'; +import PriceInput from './components/PriceInput'; import styles from './MenuPrice.module.scss'; interface MenuPriceProps { @@ -57,194 +55,44 @@ export default function MenuPrice({ isComplete }:MenuPriceProps) { {isMobile ? (
{isComplete ? ( - <> -
-
가격
-
- {isSingle ? ( -
-
-
- {singlePrice} - 원 -
-
-
- ) - : (optionPrices || []).map((input) => ( -
-
-
- {input.option} -
-
- / -
-
- {input.price} - 원 -
-
-
- ))} - + ) : ( - <> -
-
가격
-
- {isSingle - ? ( -
-
-
- setSinglePrice(e.target.value === '' ? 0 : Number(e.target.value))} - /> -

-
-
-
- ) - : (optionPrices || []).map((input) => ( -
-
- updatePriceInput(input.id, 'option', e.target.value)} - disabled={isSingle} - /> -
- updatePriceInput(input.id, 'price', Number(e.target.value))} - /> -

-
-
- -
- ))} - - + )}
) : (
- {isComplete ? ( - <> -
-
가격
-
- {isSingle ? ( -
-
-
- {singlePrice} - 원 -
-
-
- ) - : (optionPrices || []).map((input) => ( -
-
-
- {input.option} -
-
- / -
-
- {input.price} - 원 -
-
-
- ))} - - ) : ( - <> -
-
가격
-
- {isSingle - ? ( -
-
-
- setSinglePrice(e.target.value === '' ? 0 : Number(e.target.value))} - /> -

-
-
-
- ) - : (optionPrices || []).map((input) => ( -
-
- updatePriceInput(input.id, 'option', e.target.value)} - disabled={isSingle} - /> -
- updatePriceInput(input.id, 'price', Number(e.target.value))} - /> -

-
-
- -
- ))} - - - )} + { + isComplete ? ( + + ) : ( + + ) + }
)} diff --git a/src/page/AddMenu/components/MobileDivide/index.tsx b/src/page/AddMenu/components/MobileDivide/index.tsx index 65db9423..6ec5c3ce 100644 --- a/src/page/AddMenu/components/MobileDivide/index.tsx +++ b/src/page/AddMenu/components/MobileDivide/index.tsx @@ -1,7 +1,5 @@ import styles from './MobileDivide.module.scss'; export default function MobileDivide() { - return ( -
- ); + return
; } diff --git a/src/page/AddMenu/index.tsx b/src/page/AddMenu/index.tsx index 31f5866b..f46d2549 100644 --- a/src/page/AddMenu/index.tsx +++ b/src/page/AddMenu/index.tsx @@ -8,13 +8,13 @@ import { useErrorMessageStore } from 'store/errorMessageStore'; import useScrollToTop from 'utils/hooks/useScrollToTop'; import MenuImage from './components/MenuImage'; import MenuName from './components/MenuName'; -import styles from './AddMenu.module.scss'; import MenuPrice from './components/MenuPrice'; import MenuCategory from './components/MenuCategory'; import MenuDetail from './components/MenuDetail'; import GoMyShopModal from './components/GoMyShop'; import MobileDivide from './components/MobileDivide'; import useFormValidation from './hook/useFormValidation'; +import styles from './AddMenu.module.scss'; export default function AddMenu() { useScrollToTop(); @@ -23,9 +23,6 @@ export default function AddMenu() { const navigate = useNavigate(); const { resetMenuName, resetCategoryIds } = useAddMenuStore(); const { setMenuError, setCategoryError } = useErrorMessageStore(); - const goMyShop = () => { - navigate('/owner'); - }; const { value: isGoMyShopModal, setTrue: openGoMyShopModal, @@ -47,7 +44,8 @@ export default function AddMenu() { setIsComplete((prevState) => !prevState); } }; - const addMenu = () => { + + const addMenuMutationEvent = () => { const newMenuData = { category_ids: categoryIds, description, @@ -62,19 +60,34 @@ export default function AddMenu() { }; addMenuMutation(newMenuData); }; - const confirmAddMenu = () => { - addMenu(); - goMyShop(); + + const onClickMenuAddCancelHandler = () => { + if (isComplete) { + toggleConfirmClick(); + } else { + navigate('/owners'); + } }; - useEffect( - () => { - resetMenuName(); - resetCategoryIds(); - setMenuError(''); - setCategoryError(''); - }, - [resetMenuName, setMenuError, resetCategoryIds, setCategoryError], - ); + + const onClickMenuAddConfirmHandler = () => { + if (isComplete) { + if (isMobile) { + openGoMyShopModal(); + return; + } + addMenuMutationEvent(); + } else { + toggleConfirmClick(); + } + }; + + useEffect(() => { + resetMenuName(); + resetCategoryIds(); + setMenuError(''); + setCategoryError(''); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); return (
@@ -103,41 +116,20 @@ export default function AddMenu() {
- {isComplete ? ( - <> - - - - ) : ( - <> - - - - )} + +
) : ( @@ -145,41 +137,20 @@ export default function AddMenu() {

메뉴 추가

- {isComplete ? ( - <> - - - - ) : ( - <> - - - - )} + +
@@ -196,7 +167,7 @@ export default function AddMenu() { diff --git a/src/page/Auth/Signup/SignUp.module.scss b/src/page/Auth/Signup/SignUp.module.scss index 454b1a04..89657e19 100644 --- a/src/page/Auth/Signup/SignUp.module.scss +++ b/src/page/Auth/Signup/SignUp.module.scss @@ -21,6 +21,10 @@ &:focus { outline: 1px solid #4590bb; + + @include media.media-breakpoint-down(mobile) { + align-items: flex-start; + } } } diff --git a/src/page/Auth/SignupTmp/component/UserId/UserId.module.scss b/src/page/Auth/SignupTmp/component/UserId/UserId.module.scss index d31250e2..2d42970c 100644 --- a/src/page/Auth/SignupTmp/component/UserId/UserId.module.scss +++ b/src/page/Auth/SignupTmp/component/UserId/UserId.module.scss @@ -43,7 +43,7 @@ margin-top: 8px; @include media.media-breakpoint-down(mobile) { - align-items: end; + align-items: flex-end; } &__input { diff --git a/src/page/Auth/SignupTmp/component/UserId/index.tsx b/src/page/Auth/SignupTmp/component/UserId/index.tsx index be5e0910..16d997d3 100644 --- a/src/page/Auth/SignupTmp/component/UserId/index.tsx +++ b/src/page/Auth/SignupTmp/component/UserId/index.tsx @@ -1,8 +1,8 @@ import useMediaQuery from 'utils/hooks/useMediaQuery'; +import ErrorMessage from 'component/common/ErrorMessage'; import CustomButton from 'page/Auth/Signup/CustomButton'; import useValidateEmail from 'page/Auth/SignupTmp/hooks/useValidateEmail'; import useCheckEmailDuplicate from 'page/Auth/SignupTmp/hooks/useCheckEmailDuplicate'; -import ErrorMessage from 'page/Auth/Signup/ErrorMessage'; import styles from './UserId.module.scss'; export default function UserId() { diff --git a/src/page/Auth/SignupTmp/component/UserPassword/index.tsx b/src/page/Auth/SignupTmp/component/UserPassword/index.tsx index a47c5dc7..da5bde0e 100644 --- a/src/page/Auth/SignupTmp/component/UserPassword/index.tsx +++ b/src/page/Auth/SignupTmp/component/UserPassword/index.tsx @@ -6,7 +6,7 @@ import usePasswordConfirm from 'page/Auth/SignupTmp/hooks/usePasswordConfirm'; import { User } from 'page/Auth/SignupTmp/types/User'; import { SubmitHandler } from 'react-hook-form'; -import ErrorMessage from 'page/Auth/Signup/ErrorMessage'; +import ErrorMessage from 'component/common/ErrorMessage'; import useRegisterInfo from 'store/registerStore'; import styles from './UserPassword.module.scss'; diff --git a/src/page/Auth/SignupTmp/view/OwnerDataPage/OwnerData.module.scss b/src/page/Auth/SignupTmp/view/OwnerDataPage/OwnerData.module.scss index 47f2a1b1..5e7072c0 100644 --- a/src/page/Auth/SignupTmp/view/OwnerDataPage/OwnerData.module.scss +++ b/src/page/Auth/SignupTmp/view/OwnerDataPage/OwnerData.module.scss @@ -138,7 +138,7 @@ &__shop-name { &--wrapper { display: flex; - align-items: end; + align-items: flex-end; } width: calc(272px - 34px); @@ -218,7 +218,7 @@ display: flex; gap: 8px; align-items: center; - justify-content: start; + justify-content: flex-start; &--name { width: 90%; @@ -297,7 +297,7 @@ .button { margin-top: 16px; display: flex; - justify-content: end; + justify-content: flex-end; &--upload { display: flex; diff --git a/src/page/MyShopPage/components/EditShopInfoModal/EditShopInfoModal.module.scss b/src/page/MyShopPage/components/EditShopInfoModal/EditShopInfoModal.module.scss index 20bd0081..eb591186 100644 --- a/src/page/MyShopPage/components/EditShopInfoModal/EditShopInfoModal.module.scss +++ b/src/page/MyShopPage/components/EditShopInfoModal/EditShopInfoModal.module.scss @@ -1,7 +1,7 @@ .container { display: flex; flex-direction: column; - align-items: start; + align-items: flex-end; width: 752px; height: calc(80vh - 78px); margin-left: 40px; @@ -30,7 +30,7 @@ &__modify-main-image { display: flex; - justify-content: start; + justify-content: flex-start; width: 704px; height: 136px; margin-top: 24px; @@ -138,6 +138,10 @@ margin-bottom: 24px; } + &__error-message { + margin-bottom: 5px; + } + &__header { font-size: 18px; color: #17518e; @@ -290,7 +294,7 @@ &__confirm-button-wrapper { display: flex; - justify-content: end; + justify-content: flex-end; max-width: 328px; width: 100%; } @@ -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 2b3c538b..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/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 deab8a5d..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/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 988373ae..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/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 55c57315..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/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/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/query/shop.ts b/src/query/shop.ts index 9ffa59e9..1a3c3c56 100644 --- a/src/query/shop.ts +++ b/src/query/shop.ts @@ -4,14 +4,18 @@ import { import { getMyShopList, getShopInfo, getMenuInfoList, addMenu, getShopEventList, } from 'api/shop'; +import { isKoinError } from '@bcsdlab/koin'; +import showToast from 'utils/ts/showToast'; import useAddMenuStore from 'store/addMenu'; import { NewMenu } from 'model/shopInfo/newMenu'; +import { useNavigate } from 'react-router-dom'; import getShopCategory from 'api/category'; import useSuspenseUser from 'utils/hooks/useSuspenseUser'; import { shopKeys } from './KeyFactory/shopKeys'; const useMyShop = () => { let myShopQueryKey = ''; + const navigate = useNavigate(); const { data: user } = useSuspenseUser(); if (user && 'company_number' in user) { myShopQueryKey = user.company_number; @@ -57,6 +61,12 @@ const useMyShop = () => { onSuccess: () => { resetAddMenuStore(); queryClient.invalidateQueries({ queryKey: shopKeys.myMenuInfo(shopId) }); + navigate('/owner'); + }, + onError: (e) => { + if (isKoinError(e)) { + showToast('error', e.message); + } }, }); diff --git a/src/store/addMenu.ts b/src/store/addMenu.ts index f166b53e..ac8d3fc4 100644 --- a/src/store/addMenu.ts +++ b/src/store/addMenu.ts @@ -1,11 +1,11 @@ import { create } from 'zustand'; import { MonoMenu } from 'model/shopInfo/menuCategory'; -interface OptionPrices { +export type OptionPrices = { id: number; option: string; price: number; -} +}; interface AddMenuStore { menuId: number; 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개까지 등록할 수 있습니다.')