diff --git a/src/api/shop/index.ts b/src/api/shop/index.ts index f3016317..f38de607 100644 --- a/src/api/shop/index.ts +++ b/src/api/shop/index.ts @@ -1,4 +1,6 @@ -import { MyShopListRes, MyShopInfoRes, MyShopParam } from 'model/shopInfo/myShopInfo'; +import { + MyShopListRes, MyShopInfoRes, MyShopParam, EventListParam, StoreEventResponse, +} from 'model/shopInfo/myShopInfo'; import { MonoMenu, MenuInfoRes } from 'model/shopInfo/menuCategory'; import { ShopListRes } from 'model/shopInfo/allShopInfo'; import { accessClient, client } from 'api'; @@ -41,4 +43,10 @@ export const putShop = (id: number, data: OwnerShop) => accessClient.put(`/owner export const deleteMenu = (menuId:number) => accessClient.delete(`/owner/shops/menus/${menuId}`); +export const getStoreEventList = async (param : EventListParam) => { + const { data } = await accessClient.get(`/owner/shops/${param.id}/event`); + return StoreEventResponse.parse(data); +}; export const addEvent = (id: string, eventInfo: EventInfo) => accessClient.post(`owner/shops/${id}/event`, eventInfo); + +export const deleteEvent = (shopId: number, eventId:number) => accessClient.delete(`owner/shops/${shopId}/events/${eventId}`); diff --git a/src/assets/svg/mystore/add-event-icon.svg b/src/assets/svg/mystore/add-event-icon.svg new file mode 100644 index 00000000..10474b0f --- /dev/null +++ b/src/assets/svg/mystore/add-event-icon.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/src/assets/svg/mystore/check.svg b/src/assets/svg/mystore/check.svg new file mode 100644 index 00000000..2859e568 --- /dev/null +++ b/src/assets/svg/mystore/check.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/assets/svg/mystore/complete-icon.svg b/src/assets/svg/mystore/complete-icon.svg new file mode 100644 index 00000000..35064db5 --- /dev/null +++ b/src/assets/svg/mystore/complete-icon.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/assets/svg/mystore/delete-icon.svg b/src/assets/svg/mystore/delete-icon.svg new file mode 100644 index 00000000..3647042f --- /dev/null +++ b/src/assets/svg/mystore/delete-icon.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/assets/svg/mystore/edit-event-icon.svg b/src/assets/svg/mystore/edit-event-icon.svg new file mode 100644 index 00000000..9ec58a53 --- /dev/null +++ b/src/assets/svg/mystore/edit-event-icon.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/src/assets/svg/mystore/hidden-info-arrow.svg b/src/assets/svg/mystore/hidden-info-arrow.svg new file mode 100644 index 00000000..6b3cc1e5 --- /dev/null +++ b/src/assets/svg/mystore/hidden-info-arrow.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/assets/svg/mystore/non-check-circle.svg b/src/assets/svg/mystore/non-check-circle.svg new file mode 100644 index 00000000..d9f58a7c --- /dev/null +++ b/src/assets/svg/mystore/non-check-circle.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/assets/svg/mystore/non-check.svg b/src/assets/svg/mystore/non-check.svg new file mode 100644 index 00000000..859ef856 --- /dev/null +++ b/src/assets/svg/mystore/non-check.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/assets/svg/mystore/see-info-arrow.svg b/src/assets/svg/mystore/see-info-arrow.svg new file mode 100644 index 00000000..5826e6b6 --- /dev/null +++ b/src/assets/svg/mystore/see-info-arrow.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/component/common/Modal/ImageModal/ImageModal.module.scss b/src/component/common/Modal/ImageModal/ImageModal.module.scss new file mode 100644 index 00000000..bde5f372 --- /dev/null +++ b/src/component/common/Modal/ImageModal/ImageModal.module.scss @@ -0,0 +1,108 @@ +@use "src/utils/styles/mediaQuery" as media; + +.background { + position: fixed; + display: flex; + justify-content: center; + align-items: center; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0 0 0 / 70%); + z-index: 21; +} + +.image { + max-width: 574px; + max-height: 100%; + position: fixed; + margin: auto; + + @include media.media-breakpoint-down(mobile) { + & { + max-width: calc(100% - 68px); + max-height: calc(100% - 100px); + } + } +} + +.arrow-button { + width: 60px; + height: 60px; + outline: none; + border: 0; + z-index: 23; + background-size: 24px 24px; + background-position: 50% 50%; + background-repeat: no-repeat; + background-color: #252525; + border-radius: 50%; + box-sizing: border-box; + position: fixed; + top: calc((100vh - 60px) / 2); + cursor: pointer; + + &:hover { + background-color: #f7941e; + } + + @include media.media-breakpoint-down(mobile) { + & { + width: 24px; + height: 24px; + background-color: transparent; + top: calc((100vh - 24px) / 2); + } + + &:hover { + background-color: transparent; + } + } + + &--next { + background-image: url("https://static.koreatech.in/assets/img/next-arrow.png"); + right: 50px; + + @include media.media-breakpoint-down(mobile) { + right: 10px; + } + } + + &--prev { + background-image: url("https://static.koreatech.in/assets/img/prev-arrow.png"); + left: 50px; + + @include media.media-breakpoint-down(mobile) { + left: 10px; + } + } +} + +.close { + border: 0; + outline: 0; + position: fixed; + background-color: rgba(0 0 0 / 0%); + top: 33px; + right: 62px; + width: 33px; + height: 41px; + cursor: pointer; + background-image: url("https://static.koreatech.in/assets/img/close.png"); + + @include media.media-breakpoint-down(mobile) { + & { + top: 20px; + right: 20px; + width: 24px; + height: 24px; + background-size: 24px; + background-color: transparent; + } + + &:hover { + background-color: transparent; + } + } +} diff --git a/src/component/common/Modal/ImageModal/hooks/useModalKeyboardEvent.ts b/src/component/common/Modal/ImageModal/hooks/useModalKeyboardEvent.ts new file mode 100644 index 00000000..3070f309 --- /dev/null +++ b/src/component/common/Modal/ImageModal/hooks/useModalKeyboardEvent.ts @@ -0,0 +1,26 @@ +import React from 'react'; + +interface KeyboardEventProps { + onClose: () => void + onChangeImageIndex: (move: number) => void +} + +function useModalKeyboardEvent({ onClose, onChangeImageIndex }: KeyboardEventProps) { + React.useEffect(() => { + function pressKey(event: KeyboardEvent) { + if (event.code === 'Escape') { + onClose(); + } else if (event.code === 'ArrowLeft') { + onChangeImageIndex(-1); + } else if (event.code === 'ArrowRight') { + onChangeImageIndex(1); + } + } + window.addEventListener('keydown', pressKey, true); + return () => { + window.removeEventListener('keydown', pressKey, true); + }; + }, [onClose, onChangeImageIndex]); +} + +export default useModalKeyboardEvent; diff --git a/src/component/common/Modal/ImageModal/index.tsx b/src/component/common/Modal/ImageModal/index.tsx new file mode 100644 index 00000000..2eeefae3 --- /dev/null +++ b/src/component/common/Modal/ImageModal/index.tsx @@ -0,0 +1,69 @@ +import React from 'react'; +import cn from 'utils/ts/className'; +import styles from './ImageModal.module.scss'; +import useModalKeyboardEvent from './hooks/useModalKeyboardEvent'; + +export interface ImageModalProps { + imageList: string[] + imageIndex: number + onClose: () => void +} + +function ImageModal({ + imageList, + imageIndex, + onClose, +}: ImageModalProps) { + const [selectedIndex, setSelectedIndex] = React.useState(imageIndex); + const onChangeImageIndex = React.useCallback((move: number) => { + if (move < 0) { + return (selectedIndex !== 0 && ( + setSelectedIndex(selectedIndex + move) + )); + } + return (selectedIndex !== imageList.length - 1 && ( + setSelectedIndex(selectedIndex + move) + )); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedIndex]); // imageList 의존성 불필요 + + useModalKeyboardEvent({ onClose, onChangeImageIndex }); + + React.useEffect(() => { + const body = document.querySelector('body'); + body!.style.overflow = 'hidden'; + + return () => { body!.style.overflow = 'auto'; }; + }, []); + + return ( +
+ {selectedIndex !== 0 && ( +
+ ); +} + +export default ImageModal; diff --git a/src/component/common/Modal/PortalProvider.tsx b/src/component/common/Modal/PortalProvider.tsx new file mode 100644 index 00000000..3ec5a9c5 --- /dev/null +++ b/src/component/common/Modal/PortalProvider.tsx @@ -0,0 +1,73 @@ +import React, { ReactNode } from 'react'; +import ReactDOM from 'react-dom'; + +export interface Portal { + close: () => void; +} + +interface OpenOptions { + appendTo?: Element; + onClose?: () => void; +} + +type OpenFunc = ( + element: ((portal: Portal) => React.ReactElement) | React.ReactElement, + options?: OpenOptions +) => void; + +type CloseFunc = () => void; + +export interface PortalManager { + open: OpenFunc; + close: CloseFunc; +} + +export const PortalContext = React.createContext( + undefined, +); + +interface ProviderProps { + children: ReactNode +} + +const PortalProvider = function PortalProvider({ children }: ProviderProps) { + const [modalPortal, setModalPortal] = React.useState(); + + const open: OpenFunc = React.useCallback((element, options = {}) => { + const { + onClose, + } = options; + + const close: CloseFunc = () => setModalPortal(undefined); + + const portal: Portal = { + close: () => { + close(); + onClose?.(); + }, + }; + + const portalElement = ( + typeof element === 'function' ? element(portal) : element); + + const privatePortal: ReactNode = portalElement; + + setModalPortal(privatePortal); + }, []); + + const portalOption = React.useMemo(() => ({ + open, + close: () => setModalPortal(undefined), + }), [open]); + + return ( + + { children } + <> + { ReactDOM.createPortal(modalPortal, document.body) } + + + ); +}; + +export default PortalProvider; diff --git a/src/index.scss b/src/index.scss index 6a51a47a..fe5382b1 100644 --- a/src/index.scss +++ b/src/index.scss @@ -32,3 +32,7 @@ button { -webkit-tap-highlight-color: rgb(0 0 0 / 0%); -webkit-tap-highlight-color: transparent; } + +ul { + list-style: none; +} diff --git a/src/index.tsx b/src/index.tsx index c59f20c5..72ca962a 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -3,6 +3,7 @@ import ReactDOM from 'react-dom/client'; import './index.scss'; import { BrowserRouter } from 'react-router-dom'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import PortalProvider from 'component/common/Modal/PortalProvider'; import { ZodError } from 'zod'; import ErrorBoundary from 'component/common/ErrorBoundary'; import App from './App'; @@ -30,9 +31,11 @@ const queryClient = new QueryClient({ root.render( - - - + + + + + , ); diff --git a/src/model/shopInfo/myShopInfo.ts b/src/model/shopInfo/myShopInfo.ts index f48d1cee..f69068ab 100644 --- a/src/model/shopInfo/myShopInfo.ts +++ b/src/model/shopInfo/myShopInfo.ts @@ -58,3 +58,24 @@ export type MyShopInfoRes = z.infer; export interface MyShopParam { id: number; } + +export interface EventListParam { + id : number; +} + +export const StoreEvent = z.object({ + shop_id: z.number(), + shop_name: z.string(), + event_id: z.number(), + title: z.string(), + content: z.string(), + thumbnail_images: z.array(z.string()), + start_date: z.string(), + end_date: z.string(), +}); +export type StoreEvent = z.infer; +export const StoreEventResponse = z.object({ + events: z.array(StoreEvent), +}); + +export type StoreEventResponse = z.infer; diff --git a/src/page/AddMenu/components/MenuPrice/index.tsx b/src/page/AddMenu/components/MenuPrice/index.tsx index 545f5b7d..8e71c139 100644 --- a/src/page/AddMenu/components/MenuPrice/index.tsx +++ b/src/page/AddMenu/components/MenuPrice/index.tsx @@ -40,13 +40,12 @@ export default function MenuPrice({ isComplete }:MenuPriceProps) {
-
setSinglePrice(Number(e.target.value))} + value={singlePrice === 0 || singlePrice === null ? '' : singlePrice} + onChange={(e) => setSinglePrice(e.target.value === '' ? 0 : Number(e.target.value))} />

diff --git a/src/page/MyShopPage/MyShopPage.module.scss b/src/page/MyShopPage/MyShopPage.module.scss index 7a7134fe..999e713b 100644 --- a/src/page/MyShopPage/MyShopPage.module.scss +++ b/src/page/MyShopPage/MyShopPage.module.scss @@ -1,3 +1,5 @@ +@use "src/utils/styles/mediaQuery" as media; + .container { width: 1131px; margin: 0 auto; @@ -86,3 +88,28 @@ a { text-decoration: none; } + +.tap { + display: grid; + grid-template-columns: 1fr 1fr; + border-bottom: 1.5px solid #eee; + + &__type { + font-size: 20px; + font-weight: 600; + border: none; + background: none; + padding: 10px 0; + color: #8e8e8e; + cursor: pointer; + + &--active { + color: #175c8e; + border-bottom: 1.5px solid #175c8e; + } + + @include media.media-breakpoint-down(mobile) { + font-size: 16px; + } + } +} diff --git a/src/page/MyShopPage/components/EventTable/EventTable.module.scss b/src/page/MyShopPage/components/EventTable/EventTable.module.scss new file mode 100644 index 00000000..afce81be --- /dev/null +++ b/src/page/MyShopPage/components/EventTable/EventTable.module.scss @@ -0,0 +1,65 @@ +.eventContainer { + padding: 5px 24px; +} + +.manage-event-button-container { + padding: 16px 0 18px; + display: flex; + gap: 9px; +} + +.manage-event-button { + display: flex; + justify-content: center; + align-items: center; + gap: 8px; + height: 32px; + width: 100%; + border-radius: 5px; + background-color: #f5f5f5; + color: #4b4b4b; + font-size: 14px; + + &:hover { + background-color: #cacaca; + } +} + +.edit-menubar { + display: flex; + justify-content: space-between; + background-color: #fafafa; + padding: 11px 15px; + border-bottom: 1px solid #eee; +} + +.select-all-button { + display: flex; + flex-direction: column; + align-items: center; + gap: 3px; + background-color: #fafafa; + color: #8e8e8e; + font-size: 12px; +} + +.edit-menubar--button { + display: flex; + gap: 16px; +} + +.edit-menubar--button button { + display: flex; + justify-content: center; + align-items: center; + gap: 5px; + padding: 6px 8px; + border-radius: 5px; + background: #fafafa; + color: #8e8e8e; + font-size: 14px; + + &:hover { + background: #eee; + } +} diff --git a/src/page/MyShopPage/components/EventTable/components/EventCard.module.scss b/src/page/MyShopPage/components/EventTable/components/EventCard.module.scss new file mode 100644 index 00000000..da0e454d --- /dev/null +++ b/src/page/MyShopPage/components/EventTable/components/EventCard.module.scss @@ -0,0 +1,92 @@ +.eventCard { + position: relative; + padding: 10px 0; + display: flex; + gap: 16px; + width: 100%; + + &--nonHidden { + padding: 10px 0; + display: flex; + gap: 16px; + flex-direction: column; + } +} + +.eventThumbail { + width: 72px; + height: 80px; + border-radius: 5px; + + &--nonHidden { + width: 100%; + height: auto; + } +} + +.event-info { + width: 100%; + + &__header { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + gap: 5px; + } +} + +.arrow-button { + border: 0; + background-color: transparent; + padding-bottom: 5px; + white-space: nowrap; + display: flex; + align-items: center; + gap: 4px; + color: #8e8e8e; + font-size: 12px; +} + +.title { + color: #000; + font-size: 16px; + font-weight: 500; + line-height: normal; +} + +/* stylelint-disable value-no-vendor-prefix */ +.eventContent { + max-height: 28px; + max-width: 239px; + margin-top: 8px; + overflow: hidden; + color: #8e8e8e; + text-overflow: ellipsis; + font-size: 12px; + line-height: normal; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + + &--nonHidden { + display: block; + max-height: none; + } +} +/* stylelint-enable value-no-vendor-prefix */ + +.eventUpdatedAt { + margin-top: 6px; + color: #8e8e8e; + font-size: 12px; + font-style: normal; + line-height: 15.3px; +} + +.select-button { + position: absolute; + top: 2px; + left: -7px; + background-color: transparent; +} diff --git a/src/page/MyShopPage/components/EventTable/components/index.tsx b/src/page/MyShopPage/components/EventTable/components/index.tsx new file mode 100644 index 00000000..f1a7c910 --- /dev/null +++ b/src/page/MyShopPage/components/EventTable/components/index.tsx @@ -0,0 +1,88 @@ +import { useState } from 'react'; +import { ReactComponent as SeeInfoArrow } from 'assets/svg/mystore/see-info-arrow.svg'; +import { ReactComponent as HiddenInfoArrow } from 'assets/svg/mystore/hidden-info-arrow.svg'; +import { ReactComponent as NonCheck } from 'assets/svg/mystore/non-check.svg'; +import { ReactComponent as Check } from 'assets/svg/mystore/check.svg'; +import cn from 'utils/ts/className'; +import { StoreEvent } from 'model/shopInfo/myShopInfo'; +import styles from './EventCard.module.scss'; + +interface EventCardprops { + event : StoreEvent, + editState : boolean, + selectedEventIds : number[], + toggleSelect : (id: number) => void, +} +export default function EventCard({ + event, editState, selectedEventIds, toggleSelect, +}: EventCardprops) { + const [hiddenInfo, setHiddenInfo] = useState(true); + + const toggleHiddenInfo = (state:boolean) => { + if (state) { + setHiddenInfo(false); + } else setHiddenInfo(true); + }; + return ( +
+ {editState + && ( + + )} + {event.thumbnail_images ? ( + {event.title} + ) : ( + KOIN service logo + )} +
+
+
{event.title}
+ +
+
+ {event.content} +
+
{event.start_date.replace(/-/g, '.')}
+
+
+ ); +} diff --git a/src/page/MyShopPage/components/EventTable/index.tsx b/src/page/MyShopPage/components/EventTable/index.tsx new file mode 100644 index 00000000..32c877e3 --- /dev/null +++ b/src/page/MyShopPage/components/EventTable/index.tsx @@ -0,0 +1,120 @@ +import { StoreEvent } from 'model/shopInfo/myShopInfo'; +import { useState, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import useMyShop from 'query/shop'; +import { ReactComponent as EditEventIcon } from 'assets/svg/mystore/edit-event-icon.svg'; +import { ReactComponent as AddEventIcon } from 'assets/svg/mystore/add-event-icon.svg'; +import { ReactComponent as NonCheckCircle } from 'assets/svg/mystore/non-check-circle.svg'; +import { ReactComponent as DeleteIcon } from 'assets/svg/mystore/delete-icon.svg'; +import { ReactComponent as Check } from 'assets/svg/mystore/check.svg'; +import { ReactComponent as CompleteIcon } from 'assets/svg/mystore/complete-icon.svg'; +import { useDeleteEvent } from 'query/event'; +import showToast from 'utils/ts/showToast'; +import EventCard from './components'; +import styles from './EventTable.module.scss'; + +export default function EventTable() { + const { shopData, eventList } = useMyShop(); + const [editMenu, setEditMenu] = useState(false); + const [selectAll, setSelectAll] = useState(false); + const [selectedEventIds, setSelectedEventIds] = useState([]); + const { mutate: deleteEvent } = useDeleteEvent(shopData!.id, selectedEventIds); + + const toggleSelectEvent = (id: number): void => { + setSelectedEventIds((prev : number[]) => ( + prev.includes(id) ? prev.filter((item) => item !== id) : [...prev, id] + )); + }; + const navigate = useNavigate(); + useEffect(() => { + if (selectAll && eventList) { + setSelectedEventIds(eventList.events.map((event : StoreEvent) => event.event_id)); + } else { + setSelectedEventIds([]); + } + }, [selectAll, eventList]); + + return ( + <> +
+ {editMenu ? ( +
+
+ +
+
+ + + +
+
+ ) : ( +
+ + +
+ )} +
+
+ {eventList && eventList.events.map((event: StoreEvent) => ( + toggleSelectEvent(event.event_id)} + /> + ))} +
+ + ); +} diff --git a/src/page/MyShopPage/components/MenuTable/MenuTable.module.scss b/src/page/MyShopPage/components/MenuTable/MenuTable.module.scss new file mode 100644 index 00000000..64375680 --- /dev/null +++ b/src/page/MyShopPage/components/MenuTable/MenuTable.module.scss @@ -0,0 +1,125 @@ +@use "src/utils/styles/mediaQuery" as media; + +.categories { + display: flex; + gap: 13px; + margin: 16px 0; + + @include media.media-breakpoint-down(mobile) { + margin: 16px 0 0 10px; + } + + &__tag { + padding: 8px 12px; + background-color: transparent; + border: 1px solid #cacaca; + border-radius: 4px; + color: #cacaca; + cursor: pointer; + + &--active { + background-color: #175c8e; + border: 1px solid #175c8e; + color: #ffff; + } + } +} + +.menu { + @include media.media-breakpoint-down(mobile) { + padding: 10px; + border-bottom: 6px solid #f5f5f5; + + &:last-child { + border-bottom: none; + } + } + + &__title { + display: flex; + align-items: center; + gap: 8px; + font-size: 18px; + font-weight: 600; + color: #4590bb; + padding: 10px 0; + } +} + +.menu-info { + display: grid; + grid-template-columns: repeat(2, 1fr); + padding: 20px; + border-bottom: 1px solid #eee; + + &:last-child { + border-bottom: none; + } + + &:hover { + background-color: #eee; + } + + &__card { + display: flex; + flex-direction: column; + justify-content: center; + + &:nth-child(2) { + justify-content: center; + } + + span:nth-child(1) { + font-size: 16px; + font-weight: 600; + padding-bottom: 20px; + + @include media.media-breakpoint-down(mobile) { + padding-bottom: 12px; + width: 250px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + word-break: break-all; + } + } + + span:nth-child(2) { + color: #175c8e; + + @include media.media-breakpoint-down(mobile) { + font-size: 14px; + } + } + } +} + +.image { + display: flex; + justify-content: center; + + &:not(:first-child) { + display: none; + } + + &__button { + border: none; + background-color: transparent; + cursor: pointer; + + img { + width: 69px; + height: 69px; + } + } +} + +.empty-image { + display: flex; + justify-content: center; + align-items: center; + + img { + width: 69px; + } +} diff --git a/src/page/MyShopPage/components/MenuTable/index.tsx b/src/page/MyShopPage/components/MenuTable/index.tsx new file mode 100644 index 00000000..1e4f192c --- /dev/null +++ b/src/page/MyShopPage/components/MenuTable/index.tsx @@ -0,0 +1,125 @@ +import { useState } from 'react'; +import { MenuCategory } from 'model/shopInfo/menuCategory'; +import cn from 'utils/ts/className'; +import useMoveScroll from 'utils/hooks/useMoveScroll'; +import MENU_CATEGORY from 'utils/constant/menu'; +import styles from './MenuTable.module.scss'; + +interface MenuTableProps { + storeMenuCategories: MenuCategory[]; + onClickImage: (img: string[], index: number) => void; +} + +function MenuTable({ storeMenuCategories, onClickImage }: MenuTableProps) { + const [categoryType, setCateogoryType] = useState(storeMenuCategories[0].name); + const { elementsRef, onMoveToElement } = useMoveScroll(); + + return ( + <> +
    + {storeMenuCategories.map((menuCategories, index) => ( +
  • + +
  • + ))} +
+
+ {storeMenuCategories.map((menuCategories, index) => ( +
{ elementsRef.current[index] = element; }} + > + {MENU_CATEGORY.map((category) => ( + category.name === menuCategories.name && ( +
+ {category.name} + {menuCategories.name} +
+ ) + ))} + {menuCategories.menus.map((menu) => ( + menu.option_prices === null ? ( +
+ {menu.image_urls.length > 0 ? ( + menu.image_urls.map((img, idx) => ( +
+ +
+ ))) : ( +
+ KOIN service logo +
+ )} +
+ {menu.name} + + {!!menu.single_price && ( + menu.single_price.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') + )} + 원 + +
+
+ ) : ( + menu.option_prices.map((item) => ( +
+ {menu.image_urls.length > 0 ? ( + menu.image_urls.map((img, idx) => ( +
+ +
+ ))) : ( +
+ KOIN service logo +
+ )} +
+ {`${menu.name} - ${item.option}`} + + {item.price.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')} + 원 + +
+
+ )) + ) + ))} +
+ ))} +
+ + ); +} + +export default MenuTable; diff --git a/src/page/MyShopPage/index.tsx b/src/page/MyShopPage/index.tsx index 977f284b..9a0ff6e1 100644 --- a/src/page/MyShopPage/index.tsx +++ b/src/page/MyShopPage/index.tsx @@ -4,11 +4,17 @@ import useMediaQuery from 'utils/hooks/useMediaQuery'; import { Link, useNavigate } from 'react-router-dom'; import useBooleanState from 'utils/hooks/useBooleanState'; import { useEffect, useState } from 'react'; +import cn from 'utils/ts/className'; +import { Portal } from 'component/common/Modal/PortalProvider'; +import useModalPortal from 'utils/hooks/useModalPortal'; import showToast from 'utils/ts/showToast'; +import ImageModal from 'component/common/Modal/ImageModal'; import CatagoryMenuList from './components/CatagoryMenuList'; import StoreInfo from './components/ShopInfo'; import styles from './MyShopPage.module.scss'; import EditShopInfoModal from './components/EditShopInfoModal'; +import MenuTable from './components/MenuTable'; +import EventTable from './components/EventTable'; export default function MyShopPage() { const { isMobile } = useMediaQuery(); @@ -32,6 +38,9 @@ export default function MyShopPage() { setIsSuccess(false); } + const [tapType, setTapType] = useState('메뉴'); + const portalManager = useModalPortal(); + useEffect(() => { refetchShopData(); }, [refetchShopData, isEditShopInfoModalOpen]); @@ -42,6 +51,12 @@ export default function MyShopPage() { } }, [shopData, navigate, isLoading]); + const onClickImage = (img: string[], index: number) => { + portalManager.open((portalOption: Portal) => ( + + )); + }; + if (isMobile && shopData && isEditShopInfoModalOpen) { return ( <> @@ -81,12 +96,39 @@ export default function MyShopPage() { setIsSuccess={setIsSuccess} /> )} - {menusData && menusData.menu_categories.map((category) => ( - + + +
+ {tapType === '메뉴' ? ( + menusData && menusData.menu_categories.length > 0 && ( + - ))} + ) + ) + : ( + + )} ) : (
diff --git a/src/query/KeyFactory/shopKeys.ts b/src/query/KeyFactory/shopKeys.ts index 76599bca..0b652b8c 100644 --- a/src/query/KeyFactory/shopKeys.ts +++ b/src/query/KeyFactory/shopKeys.ts @@ -4,4 +4,5 @@ export const shopKeys = { myShopList: (myShopQueryKey: string | undefined) => [...shopKeys.all, 'myShop', myShopQueryKey] as const, myShopInfo: (shopId: number) => [...shopKeys.all, 'myShopInfo', shopId] as const, myMenuInfo: (shopId: number) => [...shopKeys.all, 'myMenuInfo', shopId] as const, + eventList: (shopId: number) => [...shopKeys.all, 'eventList', shopId] as const, }; diff --git a/src/query/event.ts b/src/query/event.ts index 0a4cf317..1293e1f7 100644 --- a/src/query/event.ts +++ b/src/query/event.ts @@ -1,13 +1,40 @@ -import { useMutation } from '@tanstack/react-query'; -import { addEvent } from 'api/shop'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useNavigate } from 'react-router-dom'; +import { addEvent, deleteEvent } from 'api/shop'; import { EventInfo } from 'model/shopInfo/event'; import { isKoinError } from '@bcsdlab/koin'; import showToast from 'utils/ts/showToast'; +import { shopKeys } from './KeyFactory/shopKeys'; export const useAddEvent = (id: string) => { + const queryClient = useQueryClient(); + const navigate = useNavigate(); const { mutate, isPending } = useMutation({ mutationFn: (data: EventInfo) => addEvent(id, data), - onSuccess: () => showToast('success', '이벤트 추가에 성공했습니다.'), + onSuccess: () => { + showToast('success', '이벤트 추가에 성공했습니다.'); + queryClient.invalidateQueries({ queryKey: shopKeys.eventList(Number(id)) }); + navigate('/owner'); + }, + onError: (e) => { + if (isKoinError(e)) showToast('error', e.message); + }, + }); + + return { mutate, isPending }; +}; + +export const useDeleteEvent = (shopId: number, eventIds: number[]) => { + const queryClient = useQueryClient(); + const { mutate, isPending } = useMutation({ + mutationFn: async () => { + const deletePromises = eventIds.map((eventId : number) => deleteEvent(shopId, eventId)); + await Promise.all(deletePromises); + }, + onSuccess: () => { + showToast('success', '이벤트 삭제에 성공했습니다.'); + queryClient.invalidateQueries({ queryKey: shopKeys.eventList(shopId) }); + }, onError: (e) => { if (isKoinError(e)) showToast('error', e.message); }, diff --git a/src/query/shop.ts b/src/query/shop.ts index 504477e4..b3828831 100644 --- a/src/query/shop.ts +++ b/src/query/shop.ts @@ -2,7 +2,7 @@ import { useMutation, useQuery, useQueryClient, useSuspenseQuery, } from '@tanstack/react-query'; import { - getMyShopList, getShopInfo, getMenuInfoList, addMenu, + getMyShopList, getShopInfo, getMenuInfoList, addMenu, getStoreEventList, } from 'api/shop'; import useUserStore from 'store/user'; import useAddMenuStore from 'store/addMenu'; @@ -51,8 +51,19 @@ const useMyShop = () => { }, }); + const { data: eventList } = useQuery({ + queryKey: shopKeys.eventList(shopId), + queryFn: () => getStoreEventList({ id: shopId }), + }); return { - shopData, menusData, addMenuMutation, addMenuError, refetchShopData, isLoading, categoryList, + shopData, + menusData, + addMenuMutation, + addMenuError, + refetchShopData, + isLoading, + categoryList, + eventList, }; }; diff --git a/src/utils/constant/menu.ts b/src/utils/constant/menu.ts new file mode 100644 index 00000000..d824d465 --- /dev/null +++ b/src/utils/constant/menu.ts @@ -0,0 +1,25 @@ +// 추후 추천, 메인, 세트, 사이드로 변경 예정 +const MENU_CATEGORY = [ + { + id: 1, + name: '추천 메뉴', + img: 'https://static.koreatech.in/assets/img/event-menu.png', + }, + { + id: 2, + name: '메인 메뉴', + img: 'https://static.koreatech.in/assets/img/representative-menu.png', + }, + { + id: 3, + name: '사이드 메뉴', + img: 'https://static.koreatech.in/assets/img/side-menu.png', + }, + { + id: 4, + name: '세트 메뉴', + img: 'https://static.koreatech.in/assets/img/set-menu.png', + }, +]; + +export default MENU_CATEGORY; diff --git a/src/utils/hooks/useModalPortal.ts b/src/utils/hooks/useModalPortal.ts new file mode 100644 index 00000000..e1abfe69 --- /dev/null +++ b/src/utils/hooks/useModalPortal.ts @@ -0,0 +1,14 @@ +import React from 'react'; +import { PortalContext } from 'component/common/Modal/PortalProvider'; + +const useModalPortal = () => { + const context = React.useContext(PortalContext); + + if (!context) { + throw new Error('usePortals must be used within a PortalProvider'); + } + + return context; +}; + +export default useModalPortal; diff --git a/src/utils/hooks/useMoveScroll.ts b/src/utils/hooks/useMoveScroll.ts new file mode 100644 index 00000000..693a68cc --- /dev/null +++ b/src/utils/hooks/useMoveScroll.ts @@ -0,0 +1,19 @@ +import { useRef } from 'react'; + +/** + * 페이지 내에서 특정 요소로 스크롤 이동 + * @return {elements, onMoveToElement} + * - elements: 이동할 요소들의 배열 + * - onMoveToElement: 인덱스에 해당하는 요소로 이동 + */ +export default function useMoveScroll() { + const elementsRef = useRef>([]); + + const onMoveToElement = (index: number) => { + if (elementsRef.current[index]) { + elementsRef.current[index]?.scrollIntoView({ behavior: 'smooth', block: 'start' }); + } + }; + + return { elementsRef, onMoveToElement }; +}