diff --git a/.github/workflows/CHECK_PR_MERGED.yml b/.github/workflows/CHECK_PR_MERGED.yml new file mode 100644 index 00000000..acb38a07 --- /dev/null +++ b/.github/workflows/CHECK_PR_MERGED.yml @@ -0,0 +1,26 @@ +on: + pull_request: + types: closed + +jobs: + check_pr_merged: + if: github.event.pull_request.merged == true + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v2 + - name: Check PR Merged + id: check_pr_merged + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const prUrl = context.payload.pull_request.html_url ?? context.payload.pull_request._links.html.href; + core.setOutput('pullRequestLink', JSON.stringify(prUrl)); + - name: Send Slack Trigger + run: | + curl -X POST https://api-slack.internal.bcsdlab.com/api/pr-merged/frontend \ + -H 'Content-Type: application/json' \ + -d '{ + "pullRequestLink": ${{ steps.check_pr_merged.outputs.pullRequestLink }} + }' \ No newline at end of file diff --git a/.github/workflows/PICK_REVIEWER.yml b/.github/workflows/PICK_REVIEWER.yml index 1165ab1a..37e867e5 100644 --- a/.github/workflows/PICK_REVIEWER.yml +++ b/.github/workflows/PICK_REVIEWER.yml @@ -24,30 +24,32 @@ jobs: const prUrl = context.payload.pull_request.html_url; const prCreatorJson = developers.reviewers.find(person => person.githubName === prCreator); - if (developers.length <= 1) { - core.setOutput('reviewers', developers.reviewers[0]); - core.setOutput('writer', prCreatorJson.name); - core.setOutput('pullRequestLink', prUrl); + //PrCreator가 reviewer에 등록되지 않은 사람인 경우 + if (!prCreatorJson) { + const reviewerArr = developers.reviewers; + const randomReviewer1 = getRandomReviewer(reviewerArr); + const randomReviewer2 = getRandomReviewer(reviewerArr.filter(reviewer => reviewer.name !== randomReviewer1.name)); + setOutput(prCreator, prUrl, randomReviewer1, randomReviewer2); } else { const candidateInternalReviewers = developers.reviewers.filter(person => person.team === prCreatorJson.team && person.githubName !== prCreator); const candidateExternalReviewers = developers.reviewers.filter(person => person.team !== prCreatorJson.team); + const randomReviewer1 = getRandomReviewer(candidateInternalReviewers); + const randomReviewer2 = getRandomReviewer(candidateExternalReviewers); + setOutput(prCreatorJson.name, prUrl, randomReviewer1, randomReviewer2); + } - const randomReviewer1 = candidateInternalReviewers[Math.floor(Math.random() * candidateInternalReviewers.length)]; - const randomReviewer2 = candidateExternalReviewers[Math.floor(Math.random() * candidateExternalReviewers.length)]; - - core.setOutput('reviewer1Name', JSON.stringify(randomReviewer1.name)); - core.setOutput('reviewer2Name', JSON.stringify(randomReviewer2.name)); - core.setOutput('reviewer1GithubName', randomReviewer1.githubName); - core.setOutput('reviewer2GithubName', randomReviewer2.githubName); - core.setOutput('writer', JSON.stringify(prCreatorJson.name)); - core.setOutput('pullRequestLink', JSON.stringify(prUrl)); + function getRandomReviewer(reviewers) { + return reviewers[Math.floor(Math.random() * reviewers.length)]; } - - name: test valiable - uses: actions/github-script@v7 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: console.log(${{ steps.pick_random_reviewer.outputs.reviewer1Name }}) + function setOutput(prCreator, prUrl, reviewer1, reviewer2) { + core.setOutput('writer', JSON.stringify(prCreator)); + core.setOutput('pullRequestLink', JSON.stringify(prUrl)); + core.setOutput('reviewer1Name', JSON.stringify(reviewer1.name)); + core.setOutput('reviewer2Name', JSON.stringify(reviewer2.name)); + core.setOutput('reviewer1GithubName', reviewer1.githubName); + core.setOutput('reviewer2GithubName', reviewer2.githubName); + } - name: Add Reviewers uses: madrapps/add-reviewers@v1 diff --git a/public/index.html b/public/index.html index 35a5fcbc..70a9baed 100644 --- a/public/index.html +++ b/public/index.html @@ -8,6 +8,7 @@ name="description" content="backoffice for koin's store owner" /> + diff --git a/src/App.tsx b/src/App.tsx index 38170254..54b1cebd 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -13,6 +13,7 @@ import PageNotFound from 'page/Error/PageNotFound'; import ModifyMenu from 'page/ModifyMenu'; import { Suspense } from 'react'; import Toast from 'component/common/Toast'; +import Coop from 'page/Coop'; function App() { return ( @@ -28,6 +29,7 @@ function App() { } /> } /> } /> + } /> }> } /> diff --git a/src/api/coop/index.ts b/src/api/coop/index.ts new file mode 100644 index 00000000..4791562f --- /dev/null +++ b/src/api/coop/index.ts @@ -0,0 +1,15 @@ +import { accessClient } from 'api'; +import { DiningImages, SoldOut } from 'model/Coop'; + +export const getDining = async () => { + const { data } = await accessClient.get('/dinings'); + return data; +}; + +export const uploadDiningImage = async (data: DiningImages) => { + await accessClient.patch('/coop/dining/image', data); +}; + +export const updateSoldOut = async (data: SoldOut) => { + await accessClient.patch('/coop/dining/soldout', data); +}; diff --git a/src/api/index.ts b/src/api/index.ts index e11e0cc6..4bc1c746 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -1,8 +1,10 @@ +/* eslint-disable @typescript-eslint/no-throw-literal */ /* eslint-disable no-param-reassign */ /* eslint-disable no-underscore-dangle */ -import axios, { AxiosResponse, InternalAxiosRequestConfig } from 'axios'; +import axios, { AxiosError, AxiosResponse, InternalAxiosRequestConfig } from 'axios'; import API_PATH from 'config/constants'; import { RefreshParams, RefreshResponse } from 'model/auth'; +import { CustomAxiosError, KoinError } from 'model/error'; const client = axios.create({ baseURL: `${API_PATH}`, @@ -51,20 +53,46 @@ accessClient.interceptors.request.use( }, ); +function isAxiosErrorWithResponseData(error: AxiosError) { + const { response } = error; + return response?.status !== undefined + && response.data.code !== undefined + && response.data.message !== undefined; +} + +function createKoinErrorFromAxiosError(error: AxiosError): KoinError | CustomAxiosError { + if (isAxiosErrorWithResponseData(error)) { + const koinError = error.response!; + return { + type: 'KOIN_ERROR', + status: koinError.status, + code: koinError.data.code, + message: koinError.data.message, + }; + } + return { + type: 'AXIOS_ERROR', + ...error, + }; +} + +client.interceptors.response.use( + (response) => response, + async (error) => { throw createKoinErrorFromAxiosError(error); }, +); + accessClient.interceptors.response.use( (response) => response, async (error) => { - if (error.message) return Promise.reject(error.message); - const originalRequest = error.config; - // accessToken만료시 새로운 accessToken으로 재요청 + // accessToken 만료시 새로운 accessToken으로 재요청 if (error.response?.status === 401 && !originalRequest._retry) { originalRequest._retry = true; - return refresh(originalRequest); } - return Promise.reject(error); + + throw createKoinErrorFromAxiosError(error); }, ); @@ -74,4 +102,10 @@ multipartClient.interceptors.request.use( return config; }, ); + +multipartClient.interceptors.response.use( + (response) => response, + async (error) => { throw createKoinErrorFromAxiosError(error); }, +); + export { client, accessClient, multipartClient }; diff --git a/src/assets/svg/coop/photo.svg b/src/assets/svg/coop/photo.svg new file mode 100644 index 00000000..072b2982 --- /dev/null +++ b/src/assets/svg/coop/photo.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/src/component/common/Header/index.tsx b/src/component/common/Header/index.tsx index 99601611..5b3173af 100644 --- a/src/component/common/Header/index.tsx +++ b/src/component/common/Header/index.tsx @@ -9,7 +9,6 @@ import useMediaQuery from 'utils/hooks/useMediaQuery'; import { createPortal } from 'react-dom'; import { postLogout } from 'api/auth'; import useUserStore from 'store/user'; -import useStepStore from 'store/useStepStore'; import styles from './Header.module.scss'; import useMobileSidebar from './hooks/useMobileSidebar'; import useMegaMenu from './hooks/useMegaMenu'; @@ -37,7 +36,6 @@ function Header() { } = useMobileSidebar(pathname, isMobile); const isMain = true; const { user, removeUser } = useUserStore(); - const setStep = useStepStore((state) => state.setStep); const logout = () => { postLogout() @@ -45,7 +43,6 @@ function Header() { sessionStorage.removeItem('access_token'); localStorage.removeItem('refresh_token'); removeUser(); - setStep(0); }); }; @@ -89,7 +86,7 @@ function Header() { )} - {pathname === '/' ? ( + {pathname === '/' || pathname === '/coop' ? ( ) : (CATEGORY .flatMap((categoryValue) => categoryValue.submenu) @@ -155,7 +152,7 @@ function Header() { key={subMenu.title} > - {subMenu.title} + {subMenu.title === '가게정보' && subMenu.title} ))} @@ -238,7 +235,7 @@ function Header() { {panelMenuList?.map((menu) => (
  • - {menu.title} + {menu.title === '가게정보' && menu.title}
  • ))} diff --git a/src/model/Coop/index.ts b/src/model/Coop/index.ts new file mode 100644 index 00000000..cab6b90d --- /dev/null +++ b/src/model/Coop/index.ts @@ -0,0 +1,41 @@ +import z from 'zod'; + +export type Menus = '아침' | '점심' | '저녁'; + +export type DiningTypes = 'BREAKFAST' | 'LUNCH' | 'DINNER'; + +export const DINING_TYPES: Record = { + 아침: 'BREAKFAST', + 점심: 'LUNCH', + 저녁: 'DINNER', +}; + +export const Dinings = z.object({ + date: z.string(), + id: z.number(), + kcal: z.number(), + menu: z.array(z.string()), + place: z.string(), + price_card: z.number(), + price_cash: z.number(), + type: z.string(), + updated_at: z.string(), + sold_out: z.boolean(), + is_changed: z.boolean(), +}); + +export type Dinings = z.infer; + +export const DiningImages = z.object({ + menuId: z.number(), + imageUrl: z.string(), +}); + +export type DiningImages = z.infer; + +export const SoldOut = z.object({ + menuId: z.number(), + soldOut: z.boolean(), +}); + +export type SoldOut = z.infer; diff --git a/src/model/error/index.ts b/src/model/error/index.ts new file mode 100644 index 00000000..ecf34591 --- /dev/null +++ b/src/model/error/index.ts @@ -0,0 +1,12 @@ +import { AxiosError } from 'axios'; + +export interface KoinError { + type: 'KOIN_ERROR'; + status: number; + code: number; + message: string; +} + +export interface CustomAxiosError extends AxiosError { + type: 'AXIOS_ERROR'; +} diff --git a/src/page/AddMenu/components/AddMenuImgModal/index.tsx b/src/page/AddMenu/components/AddMenuImgModal/index.tsx index 31b5bb1c..0de66a5d 100644 --- a/src/page/AddMenu/components/AddMenuImgModal/index.tsx +++ b/src/page/AddMenu/components/AddMenuImgModal/index.tsx @@ -2,9 +2,9 @@ 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 useImageUpload from 'utils/hooks/useImageUpload'; import ErrorMessage from 'page/Auth/Signup/component/ErrorMessage'; -import { ERRORMESSAGE } from 'page/ShopRegistration/constant/errorMessage'; import styles from './AddMenuImgModal.module.scss'; +import { ERRORMESSAGE } from 'page/ShopRegistration/constant/errorMessage'; import useImagesUpload from 'utils/hooks/useImagesUpload'; +import styles from './AddMenuImgModal.module.scss'; interface AddMenuImgModalProps { isOpen: boolean; @@ -12,10 +12,11 @@ interface AddMenuImgModalProps { } export default function AddMenuImgModal({ isOpen, closeModal }: AddMenuImgModalProps) { - const { setImageUrl } = useAddMenuStore(); + const { setImageUrls } = useAddMenuStore(); + const { imageFile, imgRef, saveImgFile, uploadError, - } = useImageUpload(); + } = useImagesUpload(); const triggerFileInput = () => { imgRef.current?.click(); @@ -28,10 +29,10 @@ export default function AddMenuImgModal({ isOpen, closeModal }: AddMenuImgModalP }; useEffect(() => { if (imageFile && !uploadError) { - setImageUrl(imageFile); + setImageUrls(imageFile); closeModal(); } - }, [imageFile, uploadError, setImageUrl, closeModal]); + }, [imageFile, uploadError, setImageUrls, closeModal]); if (!isOpen) return null; @@ -44,7 +45,7 @@ export default function AddMenuImgModal({ isOpen, closeModal }: AddMenuImgModalP 이미지 추가 메뉴 사진을 추가할 수 있습니다.
    - +
    diff --git a/src/page/AddMenu/components/MenuImage/index.tsx b/src/page/AddMenu/components/MenuImage/index.tsx index 91a83cf3..0edb4ba3 100644 --- a/src/page/AddMenu/components/MenuImage/index.tsx +++ b/src/page/AddMenu/components/MenuImage/index.tsx @@ -5,9 +5,9 @@ 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 useImageUpload from 'utils/hooks/useImageUpload'; import ErrorMessage from 'page/Auth/Signup/component/ErrorMessage'; import { ERRORMESSAGE } from 'page/ShopRegistration/constant/errorMessage'; +import useImagesUpload from 'utils/hooks/useImagesUpload'; import styles from './MenuImage.module.scss'; interface MenuImageProps { @@ -16,15 +16,16 @@ interface MenuImageProps { export default function MenuImage({ isComplete }: MenuImageProps) { const { isMobile } = useMediaQuery(); - const { imageUrl, setImageUrl, removeImageUrl } = useAddMenuStore(); + const { imageUrl, setImageUrls, removeImageUrl } = useAddMenuStore(); const { value: isAddMenuImgModal, setTrue: openAddMenuImgModal, setFalse: closeAddMenuImgModal, } = useBooleanState(false); + const { imageFile, imgRef, saveImgFile, uploadError, - } = useImageUpload(); + } = useImagesUpload(); const handleAddImage = () => { imgRef.current?.click(); }; @@ -36,9 +37,9 @@ export default function MenuImage({ isComplete }: MenuImageProps) { }; useEffect(() => { if (imageFile) { - setImageUrl(imageFile); + setImageUrls(imageFile); } - }, [imageFile, setImageUrl]); + }, [imageFile, setImageUrls]); return (
    {isMobile ? ( @@ -48,21 +49,6 @@ export default function MenuImage({ isComplete }: MenuImageProps) {
    (최대 이미지 3장)
    - {imageUrl.map((image, index) => ( -
    - {`Selected - {!isComplete && ( - - )} -
    - ))} + {imageUrl.map((image, index) => ( +
    + {`Selected + {!isComplete && ( + + )} +
    + ))}
    {`Selected {!isComplete && ( - + )} ))} @@ -114,6 +115,7 @@ export default function MenuImage({ isComplete }: MenuImageProps) { style={{ display: 'none' }} onChange={handleImageChange} ref={imgRef} + multiple />
    diff --git a/src/page/AddMenu/components/MenuName/MenuName.module.scss b/src/page/AddMenu/components/MenuName/MenuName.module.scss index 5fb1b4f7..8916328d 100644 --- a/src/page/AddMenu/components/MenuName/MenuName.module.scss +++ b/src/page/AddMenu/components/MenuName/MenuName.module.scss @@ -2,6 +2,7 @@ &__container { margin-top: 5px; width: 100%; + margin-bottom: 24px; } &__caption { @@ -18,13 +19,16 @@ border: 0.5px solid #898a8d; padding: 8px; box-sizing: border-box; - margin-bottom: 24px; &::placeholder { color: #a1a1a1; font-size: 13px; font-weight: 400; } + + &--error { + border: 0.5px #f7941e solid; + } } &__name-text { @@ -67,6 +71,10 @@ font-size: 20px; font-weight: 400; } + + &--error { + border: 0.5px #f7941e solid; + } } .name-text { diff --git a/src/page/AddMenu/components/MenuName/index.tsx b/src/page/AddMenu/components/MenuName/index.tsx index 43dc7633..56fa9fd5 100644 --- a/src/page/AddMenu/components/MenuName/index.tsx +++ b/src/page/AddMenu/components/MenuName/index.tsx @@ -1,6 +1,7 @@ import useMediaQuery from 'utils/hooks/useMediaQuery'; import useAddMenuStore from 'store/addMenu'; import { useErrorMessageStore } from 'store/errorMessageStore'; +import cn from 'utils/ts/className'; import styles from './MenuName.module.scss'; interface MenuNameProps { @@ -27,12 +28,16 @@ export default function MenuName({ isComplete }: MenuNameProps) { {name} ) : ( )} + {menuError && {menuError}}
    ) : (
    @@ -43,7 +48,10 @@ export default function MenuName({ isComplete }: MenuNameProps) { {name} ) : ( { - const updatedOptionPrices = (optionPrices || []).map( - (price, idx) => (index === idx ? { ...price, [field]: newValue } : price), - ); - setOptionPrices(updatedOptionPrices); - }; - - const addPriceInput = () => { - const newId = (optionPrices || []).length; - if (!isSingle) { - setOptionPrices([...(optionPrices || []), { id: newId, option: '', price: 0 }]); - } else { - setIsSingle(false); - } - }; - - const deletePriceInput = (index: number) => { - setOptionPrices((optionPrices || []).filter((_, idx) => idx !== index)); - }; - const handleIsSingleMenu = () => { - setIsSingle(!isSingle); - resetOptionPrice(); - }; return (
    {isMobile ? ( @@ -58,116 +24,34 @@ export default function MenuPrice({ isComplete }:MenuPriceProps) {
    가격
    - {isSingle ? ( -
    -
    -
    - {singlePrice} - 원 -
    +
    +
    +
    + {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', e.target.value)} - /> -

    -
    -
    - +
    +
    + +
    + setSinglePrice(Number(e.target.value))} + /> +

    - ))} - +
    +
    )}
    @@ -209,85 +93,21 @@ export default function MenuPrice({ isComplete }:MenuPriceProps) { <>
    가격
    -
    -
    단일메뉴
    - -
    - {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', e.target.value)} - /> -

    -
    -
    - +
    +
    +
    + setSinglePrice(e.target.value === '' ? 0 : Number(e.target.value))} + /> +

    - ))} - +
    + +
    )}
    diff --git a/src/page/AddMenu/hook/useFormValidation.ts b/src/page/AddMenu/hook/useFormValidation.ts index 6afc46b4..f1331a19 100644 --- a/src/page/AddMenu/hook/useFormValidation.ts +++ b/src/page/AddMenu/hook/useFormValidation.ts @@ -1,5 +1,6 @@ import useAddMenuStore from 'store/addMenu'; import { useErrorMessageStore } from 'store/errorMessageStore'; +import showToast from 'utils/ts/showToast'; const useFormValidation = () => { const { setMenuError, setCategoryError } = useErrorMessageStore(); @@ -10,6 +11,7 @@ const useFormValidation = () => { if (name.length === 0) { setMenuError('메뉴명을 입력해주세요.'); + showToast('error', '메뉴명을 입력해주세요.'); isValid = false; } else { setMenuError(''); @@ -17,6 +19,7 @@ const useFormValidation = () => { if (categoryIds.length === 0) { setCategoryError('카테고리를 1개 이상 선택해주세요.'); + showToast('error', '카테고리를 1개 이상 선택해주세요.'); isValid = false; } else { setCategoryError(''); diff --git a/src/page/Auth/Login/ApprovalModal/ApprovalModal.module.scss b/src/page/Auth/Login/ApprovalModal/ApprovalModal.module.scss new file mode 100644 index 00000000..4655fca4 --- /dev/null +++ b/src/page/Auth/Login/ApprovalModal/ApprovalModal.module.scss @@ -0,0 +1,72 @@ +.background { + background-color: rgb(0 0 0 / 70%); + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; +} + +.container { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + color: #000000; + opacity: 1; + background-color: #ffffff; + width: 300px; + height: 200px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 8px; + + &__title { + font-size: 18px; + font-weight: 500; + + span { + color: #175c8e; + } + } + + &__description { + display: flex; + flex-direction: column; + align-items: center; + font-size: 14px; + color: #8e8e8e; + } + + &__phone { + font-size: 16px; + color: #175c8e; + font-weight: 500; + } +} + +.button { + display: flex; + gap: 8px; + margin-top: 8px; + + &__confirm { + width: 115px; + height: 40px; + border-radius: 4px; + border: 1px solid #8e8e8e; + box-sizing: border-box; + cursor: pointer; + } + + &__clipboard { + width: 115px; + height: 40px; + border-radius: 4px; + background-color: #175c8e; + color: #ffffff; + cursor: pointer; + } +} diff --git a/src/page/Auth/Login/ApprovalModal/index.tsx b/src/page/Auth/Login/ApprovalModal/index.tsx new file mode 100644 index 00000000..02e68a65 --- /dev/null +++ b/src/page/Auth/Login/ApprovalModal/index.tsx @@ -0,0 +1,37 @@ +import { createPortal } from 'react-dom'; +import showToast from 'utils/ts/showToast'; +import styles from './ApprovalModal.module.scss'; + +const PHONE_NUMBER = '010-7724-5536'; + +export default function ApprovalModal({ toggle }:{ toggle: () => void }) { + const copyPhone = () => { + navigator.clipboard.writeText(PHONE_NUMBER).then(() => { + showToast('success', '전화번호를 클립보드에 복사하였습니다.'); + }).catch(() => { + showToast('error', '전화번호를 복사하는데 실패했습니다.'); + }); + }; + + return createPortal( +
    +
    +
    + 관리자의 승인 + 이 진행 중입니다. +
    +
    + 해당 화면이 지속해서 보일 시 + 아래 연락처로 문의하시기 바랍니다. +
    +
    {PHONE_NUMBER}
    +
    + + +
    +
    + +
    , + document.body, + ); +} diff --git a/src/page/Auth/Login/Login.module.scss b/src/page/Auth/Login/Login.module.scss index 380534ec..9ad18983 100644 --- a/src/page/Auth/Login/Login.module.scss +++ b/src/page/Auth/Login/Login.module.scss @@ -71,7 +71,7 @@ @include media.media-breakpoint-down(mobile) { border: none; - border-bottom: 1px solid #d2dae2; + border-bottom: 1px solid #f7941e; } } @@ -98,10 +98,6 @@ &__error-message { font-size: 12px; color: #f7941e; - - @include media.media-breakpoint-down(mobile) { - display: none; - } } &__icon { @@ -136,7 +132,7 @@ &--login { @include media.media-breakpoint-down(mobile) { background: #f7941e; - margin-top: 48px; + margin-top: 38px; } } } @@ -190,6 +186,10 @@ } } } + + &__error { + height: 10px; + } } .option { diff --git a/src/page/Auth/Login/index.tsx b/src/page/Auth/Login/index.tsx index 3d07c2cd..668242a5 100644 --- a/src/page/Auth/Login/index.tsx +++ b/src/page/Auth/Login/index.tsx @@ -15,6 +15,7 @@ import sha256 from 'utils/ts/SHA-256'; import { useErrorMessageStore } from 'store/errorMessageStore'; import styles from './Login.module.scss'; import OPTION from './static/option'; +import ApprovalModal from './ApprovalModal'; export default function Login() { const { value: isBlind, changeValue: changeIsBlind } = useBooleanState(); @@ -23,8 +24,9 @@ export default function Login() { const { login, isError: isServerError } = useLogin(); const [isFormError, setIsFormError] = useState(false); const navigate = useNavigate(); - const { loginError } = useErrorMessageStore(); + const { loginError, loginErrorCode } = useErrorMessageStore(); const [emailError, setEmailError] = useState(''); + const { value: isModalOpen, changeValue: toggle } = useBooleanState(false); const isError = isServerError || isFormError; @@ -83,7 +85,7 @@ export default function Login() {
    {(isError || !!isFormError) && ( -
    {loginError || emailError}
    +
    {loginError || emailError}
    )}
    +
    + {isMobile && (isError || !!isFormError) && ( +
    {loginError || emailError}
    + )} +
    @@ -127,6 +135,7 @@ export default function Login() {
    + {loginErrorCode === 100005 && isModalOpen && }
    ); } diff --git a/src/page/Auth/Signup/hooks/useOwnerData.ts b/src/page/Auth/Signup/hooks/useOwnerData.ts index 9be27c6b..fb705c90 100644 --- a/src/page/Auth/Signup/hooks/useOwnerData.ts +++ b/src/page/Auth/Signup/hooks/useOwnerData.ts @@ -171,8 +171,8 @@ export default function useCheckOwnerData(isMobile:boolean) { addFiles(watch('registerFiles')); }, validate: () => { - if (ownerData.registerFiles && ownerData.registerFiles.length < 3) { - return '파일을 3개 이상 첨부해주세요'; + if (!ownerData.registerFiles) { + return '파일을 1개 이상 첨부해주세요'; } return true; }, diff --git a/src/page/Auth/Signup/utils/parseRegisterData.ts b/src/page/Auth/Signup/utils/parseRegisterData.ts index 805d3e26..4592827e 100644 --- a/src/page/Auth/Signup/utils/parseRegisterData.ts +++ b/src/page/Auth/Signup/utils/parseRegisterData.ts @@ -11,7 +11,7 @@ const parseRegisterData = async ( ) => { const companyNumber = ownerInfo.registrationNumberMobile ? ownerInfo.registrationNumberMobile.replace(/^(\d{3})(\d{2})(\d+)/, '$1-$2-$3') : `${ownerInfo.registrationNumberFront}-${ownerInfo.registrationNumberMiddle}-${ownerInfo.registrationNumberEnd}`; const phoneNumber = ownerInfo.phoneMobile ? ownerInfo.phoneMobile.replace(/^(\d{3})(\d{4})(\d+)/, '$1-$2-$3') : `${ownerInfo.phoneFront}-${ownerInfo.phoneMiddle}-${ownerInfo.phoneEnd}`; - const attachmentUrls = fileUrls!.map((file) => ({ file_url: `https://${file}` })); + const attachmentUrls = fileUrls!.map((file) => ({ file_url: file })); const shopId = ownerInfo.shopName === searchShopState ? Number(selectedShopId) : null; const hashedPassword = await sha256(userInfo.password!); diff --git a/src/page/Coop/Coop.module.scss b/src/page/Coop/Coop.module.scss new file mode 100644 index 00000000..32e9f901 --- /dev/null +++ b/src/page/Coop/Coop.module.scss @@ -0,0 +1,49 @@ +.container { + display: flex; + flex-direction: column; + background-color: #f5f5f5; + width: 390px; + min-height: 100vh; + height: 100%; +} + +.container-wrapper { + display: flex; + justify-content: center; + align-items: center; +} + +.place { + &__container { + display: flex; + align-items: center; + gap: 8px; + margin-top: 24px; + margin-left: 24px; + } + + &__button--selected { + width: 60px; + height: 30px; + background-color: #175c8e; + color: #fff; + display: flex; + align-items: center; + justify-content: center; + border-radius: 999px; + cursor: pointer; + } + + &__button--unselected { + width: 60px; + height: 30px; + background-color: #fff; + color: #175c8e; + display: flex; + align-items: center; + justify-content: center; + border: solid 1px #175c8e; + border-radius: 999px; + cursor: pointer; + } +} diff --git a/src/page/Coop/components/MenuCard/MenuCard.module.scss b/src/page/Coop/components/MenuCard/MenuCard.module.scss new file mode 100644 index 00000000..561bd48c --- /dev/null +++ b/src/page/Coop/components/MenuCard/MenuCard.module.scss @@ -0,0 +1,69 @@ +.container { + display: flex; + flex-direction: column; +} + +.card { + display: flex; + flex-direction: column; + background-color: #fff; + margin: 10px 20px; + border-radius: 4px; + box-shadow: 0 1px 9px 1px rgb(0 0 0 / 6%); + + &__header { + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid #e0e0e0; + padding: 10px; + } + + &__title { + font-size: 18px; + font-weight: 500; + } + + &__content { + display: flex; + flex-direction: column; + font-size: 12px; + font-weight: 400; + line-height: 15px; + width: 150px; + } + + &__image { + width: 150px; + height: 100px; + flex-shrink: 0; + border-radius: 4px; + border: 1px solid #cacaca; + background: #fafafa; + display: flex; + align-items: center; + justify-content: center; + object-fit: scale-down; + } + + &__soldout-wrapper { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + } + + &__wrapper { + display: flex; + align-items: center; + justify-content: center; + padding: 12px; + gap: 40px; + } + + &__soldout { + color: #8e8e8e; + font-size: 13px; + font-weight: 400; + } +} diff --git a/src/page/Coop/components/MenuCard/index.tsx b/src/page/Coop/components/MenuCard/index.tsx new file mode 100644 index 00000000..19a4a118 --- /dev/null +++ b/src/page/Coop/components/MenuCard/index.tsx @@ -0,0 +1,87 @@ +import { useGetDining } from 'query/coop'; +import { Dinings, Menus, DINING_TYPES } from 'model/Coop'; +import SoldoutToggle from 'page/Coop/components/SoldoutToggle'; +import { ReactComponent as Photo } from 'assets/svg/coop/photo.svg'; +import { useRef, useState } from 'react'; +import styles from './MenuCard.module.scss'; + +interface MenuCardProps { + selectedMenuType: Menus; +} + +export default function MenuCard({ selectedMenuType }: MenuCardProps) { + const { data } = useGetDining(); + const [selectedImages, setSelectedImages] = useState<{ [key: number]: string }>({}); + const fileInputRefs = useRef<{ [key: number]: HTMLInputElement | null }>({}); + + const handleImageChange = (menuId: number) => (event: React.ChangeEvent) => { + if (event.target.files && event.target.files[0]) { + const fileReader = new FileReader(); + fileReader.onload = (e) => { + setSelectedImages((prevImages) => ({ + ...prevImages, + [menuId]: e.target?.result as string, + })); + }; + fileReader.readAsDataURL(event.target.files[0]); + } + }; + + const handleImageClick = (menuId: number) => () => { + fileInputRefs.current[menuId]?.click(); + }; + + const getDiningType = (menuType: Menus) => DINING_TYPES[menuType]; + + const filteredData = data?.filter((menu:Dinings) => { + const diningType = getDiningType(selectedMenuType); + return menu.type === diningType && ['A코너', 'B코너', 'C코너'].includes(menu.place); + }); + + return ( +
    + {filteredData?.map((menu: Dinings) => ( +
    +
    + {menu.place} +
    + 품절 + +
    +
    +
    +
    { + if (event.key === 'Enter') handleImageClick(menu.id)(); + }} + role="button" + tabIndex={0} + > + {selectedImages[menu.id] ? ( + + ) : ( + + )} +
    +
    + {menu.menu.map((item) => ( +
    {item}
    + ))} +
    +
    + { + fileInputRefs.current[menu.id] = el; + }} + /> +
    + ))} +
    + ); +} diff --git a/src/page/Coop/components/MenuType/MenuType.module.scss b/src/page/Coop/components/MenuType/MenuType.module.scss new file mode 100644 index 00000000..b9c5006c --- /dev/null +++ b/src/page/Coop/components/MenuType/MenuType.module.scss @@ -0,0 +1,34 @@ +.place { + &__container { + display: flex; + align-items: center; + gap: 8px; + margin-top: 24px; + margin-left: 24px; + } + + &__button { + width: 60px; + height: 33px; + background-color: #fff; + color: #175c8e; + display: flex; + align-items: center; + justify-content: center; + border: solid 1px #175c8e; + border-radius: 999px; + cursor: pointer; + } + + &__button--selected { + width: 60px; + height: 33px; + background-color: #175c8e; + color: #fff; + display: flex; + align-items: center; + justify-content: center; + border-radius: 999px; + cursor: pointer; + } +} diff --git a/src/page/Coop/components/MenuType/index.tsx b/src/page/Coop/components/MenuType/index.tsx new file mode 100644 index 00000000..92b07fff --- /dev/null +++ b/src/page/Coop/components/MenuType/index.tsx @@ -0,0 +1,51 @@ +import cn from 'utils/ts/className'; +import { Menus } from 'model/Coop'; +import styles from './MenuType.module.scss'; + +interface MenuTypeProps { + selectedMenuType: Menus; + setSelectedMenuType: (menuType: Menus) => void; +} + +export default function MenuType({ selectedMenuType, setSelectedMenuType }: MenuTypeProps) { + return ( +
    + + + +
    + ); +} diff --git a/src/page/Coop/components/SoldoutToggle/SoldoutToggle.module.scss b/src/page/Coop/components/SoldoutToggle/SoldoutToggle.module.scss new file mode 100644 index 00000000..345e1d9b --- /dev/null +++ b/src/page/Coop/components/SoldoutToggle/SoldoutToggle.module.scss @@ -0,0 +1,15 @@ +.toggle-button { + width: 46px; + height: 23px; + cursor: pointer; + + .background { + rx: 9.5; + stroke-width: 3; + } + + .circle { + r: 8; + fill: white; + } +} diff --git a/src/page/Coop/components/SoldoutToggle/index.tsx b/src/page/Coop/components/SoldoutToggle/index.tsx new file mode 100644 index 00000000..65ff7277 --- /dev/null +++ b/src/page/Coop/components/SoldoutToggle/index.tsx @@ -0,0 +1,17 @@ +import { useState } from 'react'; +import styles from './SoldoutToggle.module.scss'; + +export default function SoldoutToggle() { + const [isActive, setIsActive] = useState(true); + + const handleToggle = () => { + setIsActive(!isActive); + }; + + return ( + + + + + ); +} diff --git a/src/page/Coop/index.tsx b/src/page/Coop/index.tsx new file mode 100644 index 00000000..88a0e2e2 --- /dev/null +++ b/src/page/Coop/index.tsx @@ -0,0 +1,17 @@ +import { useState } from 'react'; +import { Menus } from 'model/Coop'; +import MenuCard from './components/MenuCard'; +import MenuType from './components/MenuType'; +import styles from './Coop.module.scss'; + +export default function Coop() { + const [selectedMenuType, setSelectedMenuType] = useState('아침'); + return ( +
    +
    + + +
    +
    + ); +} diff --git a/src/page/ModifyMenu/ModifyMenu.module.scss b/src/page/ModifyMenu/ModifyMenu.module.scss index 2ac079a7..3f64e51c 100644 --- a/src/page/ModifyMenu/ModifyMenu.module.scss +++ b/src/page/ModifyMenu/ModifyMenu.module.scss @@ -53,7 +53,7 @@ } &__button-check { - width: 226px; + width: 180px; height: 43px; flex-shrink: 0; background: #175c8e; diff --git a/src/page/MyShopPage/components/EditShopInfoModal/EditShopInfoModal.module.scss b/src/page/MyShopPage/components/EditShopInfoModal/EditShopInfoModal.module.scss index 73681394..95e39e66 100644 --- a/src/page/MyShopPage/components/EditShopInfoModal/EditShopInfoModal.module.scss +++ b/src/page/MyShopPage/components/EditShopInfoModal/EditShopInfoModal.module.scss @@ -126,17 +126,17 @@ } } -.content { +.main-info { display: grid; grid-template-columns: 1fr 1fr; margin-top: 22px; -} -.main-info { - display: flex; - flex-direction: column; - margin-right: 24px; - margin-bottom: 24px; + &__label { + display: flex; + flex-direction: column; + margin-right: 24px; + margin-bottom: 24px; + } &__header { font-size: 18px; @@ -151,6 +151,12 @@ padding: 8px 0 8px 12px; } + &__select { + width: 345px; + height: 40px; + padding-left: 12px; + } + &__operate-time { display: flex; @@ -171,6 +177,7 @@ &__checkboxes { display: grid; grid-template-columns: 1fr 1fr; + gap: 16px; } &__checkbox { @@ -248,13 +255,6 @@ display: none; } - &__main-content { - display: flex; - flex-direction: column; - align-items: center; - margin-top: 22px; - } - &__confirm-button-wrapper { display: flex; justify-content: end; @@ -277,9 +277,16 @@ .mobile-main-info { display: flex; - margin-bottom: 16px; + flex-direction: column; + align-items: center; + margin-top: 22px; + + &__label { + display: flex; + margin-bottom: 16px; + } - &--header { + &__header { display: flex; align-items: center; width: 68px; @@ -288,13 +295,19 @@ color: #17518e; } - &--input { + &__input { width: 252px; height: 21px; padding: 8px 0 8px 8px; border: 1px solid #898a8d; } + &__select { + width: 262px; + height: 37px; + padding: 0 8px; + } + &__checkboxes { display: flex; justify-content: space-between; @@ -342,16 +355,16 @@ .mobile-operate-time { display: flex; justify-content: space-between; + align-items: center; width: 252px; height: auto; - margin-left: 8px; - &--content { + &__content { color: #858585; font-size: 14px; } - &--button { + &__button { width: 58px; height: 29px; background-color: #17518e; diff --git a/src/page/MyShopPage/components/EditShopInfoModal/index.tsx b/src/page/MyShopPage/components/EditShopInfoModal/index.tsx index 041bfa06..43960c3e 100644 --- a/src/page/MyShopPage/components/EditShopInfoModal/index.tsx +++ b/src/page/MyShopPage/components/EditShopInfoModal/index.tsx @@ -13,6 +13,7 @@ import { OwnerShop } from 'model/shopInfo/ownerShop'; import { zodResolver } from '@hookform/resolvers/zod'; import { useMutation } from '@tanstack/react-query'; import { putShop } from 'api/shop'; +import useShopCategory from 'query/shopCategory'; import useBooleanState from 'utils/hooks/useBooleanState'; import CustomModal from 'component/common/CustomModal'; import OperateTimePC from 'page/ShopRegistration/component/Modal/OperateTimePC'; @@ -21,6 +22,7 @@ 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 styles from './EditShopInfoModal.module.scss'; interface EditShopInfoModalProps { @@ -41,12 +43,14 @@ EditShopInfoModalProps) { const { imageFile, saveImgFile, imgRef } = useImageUpload(); const { setName, setAddress, setPhone, setDeliveryPrice, setDescription, setDelivery, setPayBank, - setPayCard, + setPayCard, setCategoryId, } = useShopRegistrationStore(); const { - name, address, phone, deliveryPrice, description, delivery, payBank, payCard, + name, address, phone, deliveryPrice, description, delivery, payBank, payCard, categoryId, } = useShopRegistrationStore(); + const { categoryList } = useShopCategory(); + const { openTimeState, closeTimeState, @@ -64,6 +68,10 @@ EditShopInfoModalProps) { isAllClosed, } = CheckSameTime(); + const handleCategoryIdChange = (e: React.ChangeEvent) => { + setCategoryId(Number(e.target.value)); + }; + const { handleSubmit, setValue, } = useForm({ @@ -91,6 +99,7 @@ EditShopInfoModalProps) { 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, @@ -133,12 +142,9 @@ EditShopInfoModalProps) { 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); - } + const categoryIds = categoryId === TOTAL_CATEGORY + ? [TOTAL_CATEGORY] : [TOTAL_CATEGORY, categoryId]; + setValue('category_ids', categoryIds); setValue('open', openValue); setValue('delivery_price', Number(deliveryPrice)); setValue('description', description); @@ -149,7 +155,7 @@ EditShopInfoModalProps) { setValue('phone', phone); setValue('address', address); }, [imageUrlList, openTimeState, closeTimeState, shopClosedState, deliveryPrice, - description, delivery, payBank, payCard, name, phone, address]); + description, delivery, payBank, payCard, name, phone, address, categoryId]); const onSubmit: SubmitHandler = (data) => { mutation.mutate(data); @@ -186,31 +192,41 @@ EditShopInfoModalProps) { 사진변경 -
    -