diff --git a/src/App.error.tsx b/src/App.error.tsx new file mode 100644 index 00000000..a292457a --- /dev/null +++ b/src/App.error.tsx @@ -0,0 +1,26 @@ +import { FallbackProps } from 'react-error-boundary'; +import styled from '@emotion/styled'; + +const ErrorFallback = ({ resetErrorBoundary }: FallbackProps) => { + return ( + + App 전체 에러 발생 + + + ); +}; + +export default ErrorFallback; + +const Container = styled.div` + width: 100vw; + min-width: 100vw; + height: 100vh; + max-height: 100vh; + + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 10px; +`; diff --git a/src/App.loading.tsx b/src/App.loading.tsx new file mode 100644 index 00000000..e66043d1 --- /dev/null +++ b/src/App.loading.tsx @@ -0,0 +1,112 @@ +import styled from '@emotion/styled'; +import Skeleton from 'react-loading-skeleton'; +import 'react-loading-skeleton/dist/skeleton.css'; + +import theme from '@styles/theme'; + +const Loading = () => { + return ( + + +
+
+ +
+
+ ); +}; + +export default Loading; + +const BaseSkeleton = styled(Skeleton)` + width: 100%; + + border-radius: 16px; + + background-color: #f2f4f5; +`; + +const Container = styled.div` + position: relative; + + width: 100vw; + min-width: 100vw; + height: 100vh; + max-height: 100vh; + + display: flex; + + background-color: white; + overflow: hidden; +`; + +const Sidebar = styled(BaseSkeleton)` + position: fixed; + + width: 100px; + min-height: 100%; + + overflow: hidden; + + ${theme.response.tablet} { + display: none; + } +`; + +const Section = styled.div` + --sidebar-width: 100px; + + width: 100%; + min-width: calc(100vh - var(--sidebar-width)); + height: 100vh; + + margin-left: var(--sidebar-width); + padding: 0 22px; + + display: flex; + flex-direction: column; + + ${theme.response.tablet} { + min-width: 0; + + margin-left: 0; + padding: 0 15px; + } +`; + +const Header = styled(BaseSkeleton)` + position: sticky; + top: 0; + left: 0; + + width: 100%; + height: 85px; + + border-radius: 20px; + + display: flex; + justify-content: space-between; + align-items: center; + + ${theme.response.tablet} { + height: 65px; + + border-radius: 10px; + } +`; + +const OutletLayout = styled(BaseSkeleton)` + width: 100%; + height: 85vh; + max-height: 85vh; + + margin-top: 16px; + border-radius: 20px; + + ${theme.response.tablet} { + margin-top: 15px; + + margin-top: 0; + border-radius: 10px; + } +`; diff --git a/src/App.tsx b/src/App.tsx index 423e2815..c6cf25a7 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -13,8 +13,8 @@ import { Suspense } from 'react'; import { MainRouter } from './routes'; import GlobalStyles from '@styles/GlobalStyles'; import theme from '@styles/theme'; -import { ErrorApp } from '@components/ErrorFallback'; -import { LoadingApp } from '@components/Loading'; +import ErrorFallback from './App.error'; +import Loading from './App.loading'; const queryClient = new QueryClient(); @@ -28,9 +28,9 @@ const App = () => { - }> + }> diff --git a/src/api/index.ts b/src/api/index.ts index 7060ca5f..8c830a83 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -1,9 +1,14 @@ export { default as instance } from './lib/instance'; export { default as postLogin } from './lib/postLogin'; export { default as postRefreshToken } from './lib/postRefreshToken'; +export { + getCouponList, + couponUpdateApi, + couponDeleteApi, + couponToggleApi +} from './lib/getCouponList'; export { default as getTotalReport } from './lib/getTotalReport'; export { default as getYearReport } from './lib/getYearReport'; -export { default as getCouponList } from './lib/getCouponList'; export { default as getHeaderAccommodation } from './lib/getHeaderAccommodation'; export { default as getMonthReports } from './lib/getMonthReports'; export { default as getDailyReport } from './lib/getDailyReport'; diff --git a/src/api/lib/getCouponList.ts b/src/api/lib/getCouponList.ts index b2697f6b..1629e85a 100644 --- a/src/api/lib/getCouponList.ts +++ b/src/api/lib/getCouponList.ts @@ -1,8 +1,13 @@ -import { CouponListResponse } from '@/types/couponList'; +import { + CouponDeleteCredential, + CouponListResponse, + CouponToggleCredential, + CouponUpdateCredential +} from '@/types/couponList'; import { instance } from '..'; // 쿠폰 정보 가져오는 api -const getCouponList = async ( +export const getCouponList = async ( accommodationId: number, date?: string, status?: string, @@ -22,4 +27,31 @@ const getCouponList = async ( return response.data; }; -export default getCouponList; +// 쿠폰 수정 api +export const couponUpdateApi = async (credential: CouponUpdateCredential) => { + const couponNumber = credential.coupon_number; + const response = await instance.put( + `/v1/coupons/${couponNumber}`, + credential + ); + return response.data; +}; + +// 쿠폰 삭제 api +export const couponDeleteApi = async ( + credential: CouponDeleteCredential +): Promise => { + const couponNumber = credential.coupon_number; + const response = await instance.delete(`/v1/coupons/${couponNumber}`); + return response.data; +}; + +// 토클 on/off api +export const couponToggleApi = async (credential: CouponToggleCredential) => { + const couponNumber = credential.coupon_number; + const response = await instance.put( + `/v1/coupons/${couponNumber}/expose`, + credential + ); + return response.data; +}; diff --git a/src/api/lib/getHeaderAccommodation.ts b/src/api/lib/getHeaderAccommodation.ts index d37c4783..26c577ef 100644 --- a/src/api/lib/getHeaderAccommodation.ts +++ b/src/api/lib/getHeaderAccommodation.ts @@ -1,13 +1,16 @@ import { AxiosResponse } from 'axios'; import { instance } from '..'; -import { HeaderAccommodationResult } from '@/types/layout'; +import { + HeaderAccommodationData, + HeaderAccommodationResult +} from '@/types/layout'; -const getHeaderAccommodation = async (): Promise => { +const getHeaderAccommodation = async (): Promise => { const response: AxiosResponse = await instance.get(`/v1/accommodation`); - return response.data; + return response.data.accommodation_responses; }; export default getHeaderAccommodation; diff --git a/src/api/lib/getLocalCouponUsage.ts b/src/api/lib/getLocalCouponUsage.ts index ed6e0904..e7f29d01 100644 --- a/src/api/lib/getLocalCouponUsage.ts +++ b/src/api/lib/getLocalCouponUsage.ts @@ -1,14 +1,13 @@ import { instance } from '..'; +import { LocalCouponUsageResult } from '@/types/dashboard'; -const getLocalCouponUsage = async (id: number) => { - try { - const response = await instance.get( - `v1/dashboards/${id}/coupons/local/count` - ); - return response.data; - } catch (err) { - console.error(err); - } +const getLocalCouponUsage = async ( + id: number +): Promise => { + const response = await instance.get( + `v1/dashboards/${id}/coupons/local/count` + ); + return response.data; }; export default getLocalCouponUsage; diff --git a/src/api/lib/getMonthReports.ts b/src/api/lib/getMonthReports.ts index 225c1d03..4f771efc 100644 --- a/src/api/lib/getMonthReports.ts +++ b/src/api/lib/getMonthReports.ts @@ -1,12 +1,10 @@ import { instance } from '..'; -const getMonthReports = async (id: number) => { - try { - const response = await instance.get(`v1/dashboards/${id}/reports/month`); - return response.data; - } catch (err) { - console.error(err); - } +import { MonthReportsResults } from '@/types/dashboard'; + +const getMonthReports = async (id: number): Promise => { + const response = await instance.get(`v1/dashboards/${id}/reports/month`); + return response.data.monthly_data_responses; }; export default getMonthReports; diff --git a/src/components/CouponList/CouponBanner/index.tsx b/src/components/CouponList/CouponBanner/index.tsx index 63a59bde..b4e5ed1f 100644 --- a/src/components/CouponList/CouponBanner/index.tsx +++ b/src/components/CouponList/CouponBanner/index.tsx @@ -2,8 +2,15 @@ import styled from '@emotion/styled'; import theme from '@styles/theme'; import bannerIcon from '@assets/icons/ic-couponlist-speaker.svg'; +import { useRecoilValue } from 'recoil'; +import { headerAccommodationState } from '@recoil/index'; +// import { useGetCouponRanking } from '@hooks/queries/useGetCouponRanking'; const CouponBanner = () => { + const headerAccommodation = useRecoilValue(headerAccommodationState); + const sigunguData = headerAccommodation.sigungu; + // const { data } = useGetCouponRanking(headerAccommodation.id); + return ( @@ -14,7 +21,8 @@ const CouponBanner = () => {
이번 달 우리 지역 인기 쿠폰 - OO구에서 가장 많이 사용된 쿠폰은? 재방문 고객 20% 할인쿠폰 이에요! + {sigunguData}에서 가장 많이 사용된 쿠폰은? + {/* {data.first_coupon_title}쿠폰이에요! */}
@@ -46,9 +54,16 @@ const TabBanner = styled.div` const TabBannerTitle = styled.div` font-size: 12px; font-style: normal; + font-weight: 500; margin-bottom: 6px; `; const TabBannerContent = styled.div` font-size: 17px; font-style: normal; + font-weight: 500; + + span { + margin: 0px 5px; + border-bottom: 1px solid; + } `; diff --git a/src/components/CouponList/CouponHeader/index.tsx b/src/components/CouponList/CouponHeader/index.tsx index 4d632a24..4fd530b4 100644 --- a/src/components/CouponList/CouponHeader/index.tsx +++ b/src/components/CouponList/CouponHeader/index.tsx @@ -69,6 +69,10 @@ const CouponRegisterButton = styled.div` font-size: 17px; color: ${theme.colors.white}; - background: #ff3478; + background: linear-gradient(91deg, #ff3478 1.39%, #ff83ad 98.63%); cursor: pointer; + + &:hover { + background: #b22655; + } `; diff --git a/src/components/CouponList/CouponItem/CouponExpired/index.tsx b/src/components/CouponList/CouponItem/CouponExpired/index.tsx index b9deeb60..4a7e9ee1 100644 --- a/src/components/CouponList/CouponItem/CouponExpired/index.tsx +++ b/src/components/CouponList/CouponItem/CouponExpired/index.tsx @@ -4,18 +4,37 @@ import { useRef, useState } from 'react'; import theme from '@styles/theme'; import rightIcon from '@assets/icons/ic-couponlist-right.svg'; import deleteIcon from '@assets/icons/ic-couponlist-delete.svg'; -import { useOutsideClick } from '@hooks/index'; +import { useCouponDelete, useOutsideClick } from '@hooks/index'; import { CouponListProps } from '@/types/couponList'; +import Modal from '@components/modal'; +import CouponCondition from '@utils/lib/couponCondition'; const CouponExpired = ({ couponInfo }: CouponListProps) => { const [isShowRoomList, setIsShowRoomList] = useState(false); const roomListRef = useRef(null); + const [isShowModal, setIsShowModal] = useState(false); + const { mutateAsync } = useCouponDelete(); + + useOutsideClick(roomListRef, () => setIsShowRoomList(false)); const handleRoomList = () => { setIsShowRoomList(!isShowRoomList); }; - useOutsideClick(roomListRef, () => setIsShowRoomList(false)); + const handleDeleteClick = () => { + setIsShowModal(true); + }; + + // 모달 확인 버튼에 대한 동작 + const handleModalConfirm = () => { + mutateAsync({ coupon_number: couponInfo.coupon_number }); + setIsShowModal(false); + }; + + // 모달 취소 버튼에 대한 동작 + const handleModalClose = () => { + setIsShowModal(false); + }; return ( @@ -44,7 +63,12 @@ const CouponExpired = ({ couponInfo }: CouponListProps) => { 일정 - {couponInfo.coupon_room_type} + + {couponInfo.coupon_room_type}, + + {CouponCondition(couponInfo.coupon_use_condition_days)} + + 객실 @@ -95,7 +119,15 @@ const CouponExpired = ({ couponInfo }: CouponListProps) => { {couponInfo.created_date} - 삭제 + 삭제 + {isShowModal && ( + + )} ); }; @@ -217,6 +249,10 @@ const ContentValue = styled.div` font-size: 11px; font-style: normal; font-weight: 400; + + span { + margin-left: 3px; + } `; const DateContainer = styled.div` diff --git a/src/components/CouponList/CouponItem/CouponExpose/index.tsx b/src/components/CouponList/CouponItem/CouponExpose/index.tsx index bfcb5222..30c73a95 100644 --- a/src/components/CouponList/CouponItem/CouponExpose/index.tsx +++ b/src/components/CouponList/CouponItem/CouponExpose/index.tsx @@ -7,15 +7,25 @@ import toggleOffIcon from '@assets/icons/ic-couponlist-toggleOff.svg'; import rightIcon from '@assets/icons/ic-couponlist-right.svg'; import deleteIcon from '@assets/icons/ic-couponlist-delete.svg'; import { CouponListProps, ToggleStyleProps } from '@/types/couponList'; -import { useOutsideClick } from '@hooks/index'; +import { useOutsideClick, useToggleChange } from '@hooks/index'; +import { CouponCondition } from '@utils/lib/couponCondition'; const CouponExpose = ({ couponInfo }: CouponListProps) => { const [isToggle, setIsToggle] = useState(true); const [isShowRoomList, setIsShowRoomList] = useState(false); const roomListRef = useRef(null); + const { mutateAsync } = useToggleChange(); const handleToggle = () => { setIsToggle(!isToggle); + toggleUpdate(); + }; + + const toggleUpdate = async () => { + await mutateAsync({ + coupon_number: couponInfo.coupon_number, + coupon_status: '노출 OFF' + }); }; const handleRoomList = () => { @@ -72,7 +82,12 @@ const CouponExpose = ({ couponInfo }: CouponListProps) => { 일정 - {couponInfo.coupon_room_type} + + {couponInfo.coupon_room_type}, + + {CouponCondition(couponInfo.coupon_use_condition_days)} + + 객실 @@ -363,6 +378,10 @@ const ContentValue = styled.div` font-size: 11px; font-style: normal; font-weight: 400; + + span { + margin-left: 3px; + } `; const DateContainer = styled.div` diff --git a/src/components/CouponList/CouponItem/CouponStop/index.tsx b/src/components/CouponList/CouponItem/CouponStop/index.tsx index 2d310c06..de09f238 100644 --- a/src/components/CouponList/CouponItem/CouponStop/index.tsx +++ b/src/components/CouponList/CouponItem/CouponStop/index.tsx @@ -7,13 +7,14 @@ import toggleOffIcon from '@assets/icons/ic-couponlist-toggleOff.svg'; import rightIcon from '@assets/icons/ic-couponlist-right.svg'; import deleteIcon from '@assets/icons/ic-couponlist-delete.svg'; import { CouponListProps, ToggleStyleProps } from '@/types/couponList'; -import { useOutsideClick } from '@hooks/index'; +import { useOutsideClick, useToggleChange } from '@hooks/index'; +import CouponCondition from '@utils/lib/couponCondition'; const CouponStop = ({ couponInfo }: CouponListProps) => { const [isToggle, setIsToggle] = useState(false); const [isShowRoomList, setIsShowRoomList] = useState(false); const roomListRef = useRef(null); - + const { mutateAsync } = useToggleChange(); useOutsideClick(roomListRef, () => setIsShowRoomList(false)); const handleRoomList = () => { @@ -22,6 +23,10 @@ const CouponStop = ({ couponInfo }: CouponListProps) => { const handleToggle = () => { setIsToggle(!isToggle); + mutateAsync({ + coupon_number: couponInfo.coupon_number, + coupon_status: '노출 ON' + }); }; return ( @@ -72,7 +77,12 @@ const CouponStop = ({ couponInfo }: CouponListProps) => { 일정 - {couponInfo.coupon_room_type} + + {couponInfo.coupon_room_type}, + + {CouponCondition(couponInfo.coupon_use_condition_days)} + + 객실 @@ -269,6 +279,10 @@ const ContentValue = styled.div` font-size: 11px; font-style: normal; font-weight: 400; + + span { + margin-left: 3px; + } `; const DateContainer = styled.div` diff --git a/src/components/CouponList/CouponItem/CouponWait/index.tsx b/src/components/CouponList/CouponItem/CouponWait/index.tsx index bae274eb..95dcecea 100644 --- a/src/components/CouponList/CouponItem/CouponWait/index.tsx +++ b/src/components/CouponList/CouponItem/CouponWait/index.tsx @@ -1,22 +1,66 @@ import styled from '@emotion/styled'; import { useRef, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; import theme from '@styles/theme'; import centerIcon from '@assets/icons/ic-couponlist-center.svg'; import rightIcon from '@assets/icons/ic-couponlist-right.svg'; import deleteIcon from '@assets/icons/ic-couponlist-delete.svg'; -import { useOutsideClick } from '@hooks/index'; +import { useCouponDelete, useOutsideClick } from '@hooks/index'; import { CouponListProps } from '@/types/couponList'; +import Modal from '@components/modal'; +import CouponCondition from '@utils/lib/couponCondition'; const CouponWait = ({ couponInfo }: CouponListProps) => { const [isShowRoomList, setIsShowRoomList] = useState(false); + const [isShowModal, setIsShowModal] = useState(false); const roomListRef = useRef(null); + const [modalType, setModalType] = useState(''); + const navigate = useNavigate(); + const [modalContent, setModalContent] = useState({ + modalText: '', + subText: false + }); + const { mutateAsync } = useCouponDelete(); + + useOutsideClick(roomListRef, () => setIsShowRoomList(false)); const handleRoomList = () => { setIsShowRoomList(!isShowRoomList); }; - useOutsideClick(roomListRef, () => setIsShowRoomList(false)); + const handleUpdateClick = () => { + setIsShowModal(true); + setModalType('update'); + setModalContent({ + modalText: `"${couponInfo.title}"을 수정하시겠습니까?`, + subText: false + }); + }; + + const handleDeleteClick = () => { + setIsShowModal(true); + setModalType('delete'); + setModalContent({ + modalText: `"${couponInfo.title}"을 삭제하시겠습니까?`, + subText: true + }); + }; + + // 모달 확인 버튼에 대한 동작 + const handleModalConfirm = () => { + setIsShowModal(false); + if (modalType === 'delete') { + mutateAsync({ coupon_number: couponInfo.coupon_number }); + } else if (modalType === 'update') { + navigate(`/coupons/register/?couponNumber=${couponInfo.coupon_number}`); + } + }; + + // 모달 취소 버튼에 대한 동작 + const handleModalClose = () => { + setIsShowModal(false); + }; return ( @@ -45,7 +89,12 @@ const CouponWait = ({ couponInfo }: CouponListProps) => { 일정 - {couponInfo.coupon_room_type} + + {couponInfo.coupon_room_type}, + + {CouponCondition(couponInfo.coupon_use_condition_days)} + + 객실 @@ -97,13 +146,21 @@ const CouponWait = ({ couponInfo }: CouponListProps) => { -
수정
+ 수정 분리 선 이미지 -
삭제
+ 삭제
+ {isShowModal && ( + + )} ); }; @@ -223,6 +280,10 @@ const ContentValue = styled.div` font-size: 11px; font-style: normal; font-weight: 400; + + span { + margin-left: 3px; + } `; const DateContainer = styled.div` @@ -285,11 +346,19 @@ const CouponModifiedWrap = styled.div` color: #757676; font-size: 11px; - div { - cursor: pointer; + img { + margin-top: 2px; } `; +const UpdateButton = styled.div` + cursor: pointer; +`; + +const DeleteButton = styled.div` + cursor: pointer; +`; + const ContentRoom = styled.div` display: flex; align-items: center; diff --git a/src/components/CouponList/CouponMain/index.tsx b/src/components/CouponList/CouponMain/index.tsx index f5f71575..de9308ad 100644 --- a/src/components/CouponList/CouponMain/index.tsx +++ b/src/components/CouponList/CouponMain/index.tsx @@ -11,37 +11,46 @@ import couponListState from '@recoil/atoms/couponListState'; const CouponMain = () => { const coupons = useRecoilValue(couponListState); - console.log('recoil로 관리되는 쿠폰 리스트 ', coupons); + + // // 최근 등록일 기준으로 나열 + // const sortedCoupons = coupons?.content + // ? [...coupons.content].sort((a, b) => { + // const dateA = new Date(a.created_date).getTime(); + // const dateB = new Date(b.created_date).getTime(); + // return dateB - dateA; + // }) + // : []; + // console.log('recoil로 관리되는 쿠폰 리스트 ', coupons); return ( - {coupons?.content.map((coupon, index) => { + {coupons?.content?.map(coupon => { switch (coupon.coupon_status) { case '노출 ON': return ( ); case '노출 OFF': return ( ); case '노출 대기중': return ( ); case '노출 기간 만료': return ( ); diff --git a/src/components/CouponList/CouponNav/index.tsx b/src/components/CouponList/CouponNav/index.tsx index 6b37259c..930f522c 100644 --- a/src/components/CouponList/CouponNav/index.tsx +++ b/src/components/CouponList/CouponNav/index.tsx @@ -6,11 +6,11 @@ import theme from '@styles/theme'; import searchIcon from '@assets/icons/ic-couponlist-search.svg'; import centerIcon from '@assets/icons/ic-couponlist-period-center.svg'; import { couponListState, headerAccommodationState } from '@recoil/index'; -import { getCouponList } from 'src/api'; import { CategoryTabStyleProps, ResisterDateStyleProps } from '@/types/couponList'; +import { useGetCouponList } from '@hooks/queries/useCouponList'; const CouponNav = () => { const [resisterDateClick, setResisterDateClick] = useState('1년'); @@ -18,14 +18,16 @@ const CouponNav = () => { const [searchText, setSearchText] = useState(''); const headerAccommodation = useRecoilValue(headerAccommodationState); const setGlobalCoupons = useSetRecoilState(couponListState); - const coupons = useRecoilValue(couponListState); + const [searchAPI, setSearchAPI] = useState(''); const handleDateClick = (period: string) => { setResisterDateClick(period); + setSearchAPI(''); }; const handleCategoryTab = (tab: string) => { setCategoryTab(tab); + setSearchAPI(''); }; const handleSearchChange = (e: React.ChangeEvent) => { @@ -34,35 +36,26 @@ const CouponNav = () => { const handleSearch = (e: React.FormEvent) => { e.preventDefault(); + setSearchAPI(searchText); setSearchText(''); - fetchCoupons(); }; - // recoil 숙소 ID 가져오기 - const fetchCoupons = async () => { - try { - const couponData = await getCouponList( - headerAccommodation.id, - resisterDateClick !== '1년' ? resisterDateClick : undefined, - categoryTab !== '전체' ? categoryTab : undefined, - searchText - ); - setGlobalCoupons(couponData); - - console.log( - '검색어, 등록일, 카테고리:', - searchText, - resisterDateClick, - categoryTab - ); - } catch (error) { - console.log('쿠폰 조회 api 에러 ', error); - } - }; + const { data: coupons } = useGetCouponList( + headerAccommodation.id, + resisterDateClick !== '1년' ? resisterDateClick : undefined, + categoryTab !== '전체' ? categoryTab : undefined, + searchAPI + ); useEffect(() => { - fetchCoupons(); - }, [headerAccommodation.id, categoryTab, resisterDateClick]); + setGlobalCoupons(coupons); + }, [ + headerAccommodation.id, + resisterDateClick, + categoryTab, + searchAPI, + coupons + ]); return ( @@ -71,7 +64,7 @@ const CouponNav = () => { handleCategoryTab('전체')}> 전체 - {coupons?.category.all} + {coupons.category.all} handleCategoryTab('노출 ON')}> diff --git a/src/components/Dashboard/DashboardLeftSection/GraphSection/DownloadReport/CouponCounter/index.tsx b/src/components/Dashboard/DashboardLeftSection/GraphSection/DownloadReport/CouponCounter/index.tsx index a3165679..c3cbf4c7 100644 --- a/src/components/Dashboard/DashboardLeftSection/GraphSection/DownloadReport/CouponCounter/index.tsx +++ b/src/components/Dashboard/DashboardLeftSection/GraphSection/DownloadReport/CouponCounter/index.tsx @@ -1,13 +1,16 @@ import styled from '@emotion/styled'; import { CouponCounterProps, CouponCounterStyleProps } from '@/types/dashboard'; +import { getStatusToLocaleString } from '@utils/index'; import theme from '@styles/theme'; const CouponCounter = ({ type, result }: CouponCounterProps) => { return (
{type === 'download' ? '|다운로드 수' : '|사용완료 수'}
- {result}장 + + {getStatusToLocaleString(result)}장 +
); }; @@ -37,7 +40,7 @@ const Header = styled.div` align-self: flex-start; - font-size: 13.005px; + font-size: 13px; font-weight: 700; white-space: nowrap; @@ -56,7 +59,7 @@ const ResultContainer = styled.div` background-color: ${props => props.$type === 'download' ? '#F7F8FC' : '#ffffff4d'}; - font-size: 19px; + font-size: 18px; font-weight: 700; box-shadow: ${theme.shadow.medium}; diff --git a/src/components/Dashboard/DashboardLeftSection/GraphSection/DownloadReport/index.error.tsx b/src/components/Dashboard/DashboardLeftSection/GraphSection/DownloadReport/index.error.tsx new file mode 100644 index 00000000..c4486d14 --- /dev/null +++ b/src/components/Dashboard/DashboardLeftSection/GraphSection/DownloadReport/index.error.tsx @@ -0,0 +1,37 @@ +import { FallbackProps } from 'react-error-boundary'; +import styled from '@emotion/styled'; + +const ErrorFallback = ({ resetErrorBoundary }: FallbackProps) => { + return ( + +
다운로드 리포트 에러 발생
+ 다시시도 +
+ ); +}; + +export default ErrorFallback; + +const Container = styled.div` + width: 100%; + height: 100%; + + border-radius: 16px; + + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 30px; + + background-color: #fafafb; +`; + +const Header = styled.div` + font-size: 20px; + font-weight: 700; +`; + +const ResetButton = styled.button` + width: 50%; +`; diff --git a/src/components/Dashboard/DashboardLeftSection/GraphSection/DownloadReport/index.loading.tsx b/src/components/Dashboard/DashboardLeftSection/GraphSection/DownloadReport/index.loading.tsx new file mode 100644 index 00000000..33f7c646 --- /dev/null +++ b/src/components/Dashboard/DashboardLeftSection/GraphSection/DownloadReport/index.loading.tsx @@ -0,0 +1,80 @@ +import styled from '@emotion/styled'; +import Skeleton from 'react-loading-skeleton'; +import 'react-loading-skeleton/dist/skeleton.css'; + +const Loading = () => { + return ( + + + + + + + + + + + ); +}; + +export default Loading; + +const Container = styled.div` + width: 100%; + height: 100%; + + padding: 20px; + + display: flex; + flex-direction: column; + gap: 20px; +`; + +const BaseSkeleton = styled(Skeleton)` + width: 100%; + + margin: none; + padding: none; + border-radius: 12px; + + background-color: #f2f4f5; +`; + +const TitleLoading = styled(BaseSkeleton)` + width: 40%; + height: 30px; +`; + +const InnerContainer = styled.div` + width: 100%; + height: 300px; + + padding: 10px; + + display: flex; + flex-direction: column; + gap: 15px; +`; + +const StatusLoadingWrapper = styled.div` + width: 100%; + + flex: 1; + display: flex; + justify-content: center; + gap: 15px; +`; + +const StatusItemLoading = styled(BaseSkeleton)` + width: 130px; + height: 130px; + + flex: 1; +`; + +const MainContentLoading = styled(BaseSkeleton)` + width: 100%; + height: 150px; + + flex: 1.5; +`; diff --git a/src/components/Dashboard/DashboardLeftSection/GraphSection/DownloadReport/index.tsx b/src/components/Dashboard/DashboardLeftSection/GraphSection/DownloadReport/index.tsx index 5b6266b2..d292146d 100644 --- a/src/components/Dashboard/DashboardLeftSection/GraphSection/DownloadReport/index.tsx +++ b/src/components/Dashboard/DashboardLeftSection/GraphSection/DownloadReport/index.tsx @@ -1,12 +1,18 @@ import styled from '@emotion/styled'; +import { useRecoilValue } from 'recoil'; import CouponCounter from './CouponCounter'; import CouponRate from './CouponRate'; import titleIcon from '@assets/icons/ic-dashboard-downloadReport.svg'; import reloadIcon from '@assets/icons/ic-dashboard-reload.svg'; +import { headerAccommodationState } from '@recoil/index'; +import { useGetMonthReports } from '@hooks/index'; -//HACK : 해당 컴포넌트에서 서버상태값 전달 const DownloadReport = () => { + const headerSelectedState = useRecoilValue(headerAccommodationState); + const { data } = useGetMonthReports(headerSelectedState.id); + const lastestData = data[data.length - 1]; + return (
@@ -23,15 +29,15 @@ const DownloadReport = () => { - + diff --git a/src/components/Dashboard/DashboardLeftSection/GraphSection/GraphContainer/graphOptions.ts b/src/components/Dashboard/DashboardLeftSection/GraphSection/GraphContainer/graphOptions.ts index f55155c0..86874d13 100644 --- a/src/components/Dashboard/DashboardLeftSection/GraphSection/GraphContainer/graphOptions.ts +++ b/src/components/Dashboard/DashboardLeftSection/GraphSection/GraphContainer/graphOptions.ts @@ -27,7 +27,7 @@ const graphOptions: any = { y1: { position: 'left', beginAtZero: true, - max: 1200, + grid: { display: false } diff --git a/src/components/Dashboard/DashboardLeftSection/GraphSection/GraphContainer/index.error.tsx b/src/components/Dashboard/DashboardLeftSection/GraphSection/GraphContainer/index.error.tsx new file mode 100644 index 00000000..66e81d6f --- /dev/null +++ b/src/components/Dashboard/DashboardLeftSection/GraphSection/GraphContainer/index.error.tsx @@ -0,0 +1,38 @@ +import styled from '@emotion/styled'; +import { FallbackProps } from 'react-error-boundary'; + +const ErrorFallback = ({ resetErrorBoundary }: FallbackProps) => { + return ( + +
월별 차트 에러 발생
+ 다시시도 +
+ ); +}; + +export default ErrorFallback; + +const Container = styled.div` + width: 100%; + height: 100%; + + border-radius: 16px; + + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 30px; + align-self: center; + + background-color: #fafafb; +`; + +const Header = styled.div` + font-size: 20px; + font-weight: 700; +`; + +const ResetButton = styled.button` + width: 50%; +`; diff --git a/src/components/Dashboard/DashboardLeftSection/GraphSection/GraphContainer/index.loading.tsx b/src/components/Dashboard/DashboardLeftSection/GraphSection/GraphContainer/index.loading.tsx new file mode 100644 index 00000000..0bb2f265 --- /dev/null +++ b/src/components/Dashboard/DashboardLeftSection/GraphSection/GraphContainer/index.loading.tsx @@ -0,0 +1,45 @@ +import styled from '@emotion/styled'; +import Skeleton from 'react-loading-skeleton'; +import 'react-loading-skeleton/dist/skeleton.css'; + +const Loading = () => { + return ( + + + + + ); +}; + +export default Loading; + +const Container = styled.div` + width: 100%; + height: 100%; + + padding: 20px; + + display: flex; + flex-direction: column; + gap: 20px; +`; + +const BaseSkeleton = styled(Skeleton)` + width: 100%; + + margin: none; + padding: none; + border-radius: 12px; + + background-color: #f2f4f5; +`; + +const TitleLoading = styled(BaseSkeleton)` + width: 40%; + height: 30px; +`; + +const GraphLoading = styled(BaseSkeleton)` + width: 100%; + height: 340px; +`; diff --git a/src/components/Dashboard/DashboardLeftSection/GraphSection/GraphContainer/index.tsx b/src/components/Dashboard/DashboardLeftSection/GraphSection/GraphContainer/index.tsx index 739e1714..fb270dde 100644 --- a/src/components/Dashboard/DashboardLeftSection/GraphSection/GraphContainer/index.tsx +++ b/src/components/Dashboard/DashboardLeftSection/GraphSection/GraphContainer/index.tsx @@ -14,10 +14,13 @@ import { BarController } from 'chart.js'; import { Chart } from 'react-chartjs-2'; +import { useRecoilValue } from 'recoil'; import { GraphHeaderTag } from '@/types/dashboard'; import graphOptions from './graphOptions'; import { getUpdatedDate } from '@utils/index'; +import { headerAccommodationState } from '@recoil/index'; +import { useGetMonthReports } from '@hooks/index'; ChartJS.register( LinearScale, @@ -34,70 +37,72 @@ ChartJS.register( //HACK: 그래프 데이터 렌더링에 대한 테스트파일입니다. 실제 기능 구현에서는 해당 파일 다소 변경될 것 같습니다. -const labels = ['1월', '2월', '3월', '4월', '5월', '6월']; - -export const barGraphData = { - labels, - datasets: [ - { - type: 'line' as const, - label: '전환율(%)', - borderColor: '#FFADC8', - backgroundColor: '#FFADC8', - borderWidth: 2, - data: [10, 20, 10, 40, 30, 60, 10], - yAxisID: 'y2' - }, - { - type: 'bar' as const, - label: '쿠폰 다운로드', - data: [600, 500, 400, 500, 600, 800, 800], - backgroundColor: '#3182F6', - borderRadius: 5, - yAxisID: 'y1' - }, - - { - type: 'bar' as const, - label: '쿠폰 사용완료', - data: [300, 400, 200, 200, 400, 600, 700], - backgroundColor: '#FF3478', - borderRadius: 5, - yAxisID: 'y1' - } - ] -}; - -export const lineGraphData = { - labels, - datasets: [ - { - fill: true, - label: '쿠폰매출', - data: [300, 400, 200, 200, 400, 600, 700], - borderWidth: 1, - borderColor: '#3182F6', - backgroundColor: '#A7CBFF', - yAxisID: 'y1' - }, - - { - fill: true, - label: '전체매출', - data: [600, 500, 400, 500, 600, 800, 800], - borderWidth: 1, - borderColor: '#FF3478', - backgroundColor: '#FFC1D6', - yAxisID: 'y1' - } - ] -}; - //HACK 추후 utils에 적절한 폴더 생기면 옮길 예정 const GraphContainer = () => { + const headerSelectedState = useRecoilValue(headerAccommodationState); + const { data } = useGetMonthReports(headerSelectedState.id); const [isIncomeGraph, setisIncomeGraph] = useState(true); + const barGraphData = { + labels: data.map(data => `${data.statistics_month}월`), + datasets: [ + { + type: 'line' as const, + label: '전환율(%)', + borderColor: '#FFADC8', + backgroundColor: '#FFADC8', + borderWidth: 2, + data: data.map(data => data.conversion_rate), + yAxisID: 'y2' + }, + { + type: 'bar' as const, + label: '쿠폰 다운로드', + data: data.map(data => data.download_count), + backgroundColor: '#3182F6', + borderRadius: 7, + yAxisID: 'y1', + barPercentage: 0.8 + }, + + { + type: 'bar' as const, + label: '쿠폰 사용완료', + data: data.map(data => data.used_count), + backgroundColor: '#FF3478', + borderRadius: 7, + yAxisID: 'y1', + barPercentage: 0.8 + } + ] + }; + + const lineGraphData = { + labels: data.map(data => `${data.statistics_month}월`), + datasets: [ + { + fill: true, + label: '쿠폰매출', + data: data.map(data => data.coupon_total_sales), + borderWidth: 1, + borderColor: '#3182F6', + backgroundColor: '#A7CBFF', + yAxisID: 'y1' + }, + + { + fill: true, + label: '전체매출', + data: data.map(data => data.total_sales), + borderWidth: 1, + borderColor: '#FF3478', + backgroundColor: '#FFC1D6', + yAxisID: 'y1' + } + ] + }; + return (
diff --git a/src/components/Dashboard/DashboardLeftSection/GraphSection/index.tsx b/src/components/Dashboard/DashboardLeftSection/GraphSection/index.tsx index a5075a23..9e33bd98 100644 --- a/src/components/Dashboard/DashboardLeftSection/GraphSection/index.tsx +++ b/src/components/Dashboard/DashboardLeftSection/GraphSection/index.tsx @@ -1,19 +1,42 @@ import styled from '@emotion/styled'; +import React, { Suspense } from 'react'; +import { ErrorBoundary } from 'react-error-boundary'; +import { useQueryErrorResetBoundary } from '@tanstack/react-query'; import { DashboardHeader } from '@components/common'; -import GraphContainer from './GraphContainer'; -import DownloadReport from './DownloadReport'; +const GraphContainer = React.lazy(() => import('./GraphContainer')); +import GraphContainerErrorFallback from './GraphContainer/index.error'; +import GraphContainerLoading from './GraphContainer/index.loading'; +const DownloadReport = React.lazy(() => import('./DownloadReport')); +import DownloadReportErrorFallback from './DownloadReport/index.error'; +import DownloadReportLoading from './DownloadReport/index.loading'; const GraphSection = () => { + const { reset } = useQueryErrorResetBoundary(); + return ( - + + }> + + + - + + }> + + + diff --git a/src/components/Dashboard/DashboardLeftSection/LocalInformationSection/LocalCouponUsage/index.error.tsx b/src/components/Dashboard/DashboardLeftSection/LocalInformationSection/LocalCouponUsage/index.error.tsx new file mode 100644 index 00000000..9ff9163c --- /dev/null +++ b/src/components/Dashboard/DashboardLeftSection/LocalInformationSection/LocalCouponUsage/index.error.tsx @@ -0,0 +1,41 @@ +import { FallbackProps } from 'react-error-boundary'; +import styled from '@emotion/styled'; + +const ErrorFallback = ({ resetErrorBoundary }: FallbackProps) => { + return ( + + 지역 쿠폰 현황 에러 발생 + 다시 시도 + + ); +}; + +export default ErrorFallback; + +const Container = styled.div` + width: 100%; + height: 100%; + + border-radius: 16px; + + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 30px; + + color: #404446; + + background-color: #fafafb; +`; + +const ErrorMessage = styled.span` + text-align: center; + font-size: 18px; + font-weight: 700; + line-height: 1.4; +`; + +const RetryButton = styled.button` + width: 50%; +`; diff --git a/src/components/Dashboard/DashboardLeftSection/LocalInformationSection/LocalCouponUsage/index.loading.tsx b/src/components/Dashboard/DashboardLeftSection/LocalInformationSection/LocalCouponUsage/index.loading.tsx new file mode 100644 index 00000000..993866dc --- /dev/null +++ b/src/components/Dashboard/DashboardLeftSection/LocalInformationSection/LocalCouponUsage/index.loading.tsx @@ -0,0 +1,71 @@ +import styled from '@emotion/styled'; +import Skeleton from 'react-loading-skeleton'; +import 'react-loading-skeleton/dist/skeleton.css'; + +const Loading = () => { + return ( + +
+ + +
+ +
+ ); +}; + +export default Loading; + +const Container = styled.div` + width: 100%; + height: 100%; + + display: flex; + flex-direction: column; + justify-content: center; + + gap: 10px; +`; + +const BaseSkeleton = styled(Skeleton)` + width: 100%; + + border-radius: 16px; + + background-color: #f2f4f5; +`; + +const Header = styled.div` + width: 100%; + height: 30%; + + margin: 0; + padding: 0; + + display: flex; + flex-direction: column; + gap: 10px; +`; + +const TitleLoading = styled(BaseSkeleton)` + width: 80%; + height: 30px; + + margin: 0; + padding: 0; +`; + +const SubTitleLoading = styled(BaseSkeleton)` + width: 40%; + height: 20px; + + margin: 0; + padding: 0; + + display: flex; + align-items: center; +`; + +const InnerContainer = styled(BaseSkeleton)` + height: 180px; +`; diff --git a/src/components/Dashboard/DashboardLeftSection/LocalInformationSection/LocalCouponUsage/index.tsx b/src/components/Dashboard/DashboardLeftSection/LocalInformationSection/LocalCouponUsage/index.tsx index e33949fe..965cbaf7 100644 --- a/src/components/Dashboard/DashboardLeftSection/LocalInformationSection/LocalCouponUsage/index.tsx +++ b/src/components/Dashboard/DashboardLeftSection/LocalInformationSection/LocalCouponUsage/index.tsx @@ -1,5 +1,8 @@ import styled from '@emotion/styled'; +import { useRecoilValue } from 'recoil'; +import { useGetLocalCouponUsage } from '@hooks/index'; +import { headerAccommodationState } from '@recoil/index'; import reloadIcon from '@assets/icons/ic-dashboard-reload.svg'; import locationIcon from '@assets/icons/ic-dashboard-location.svg'; import bigLocationIcon from '@assets/icons/ic-dashboard-bigLocation.svg'; @@ -7,6 +10,8 @@ import gpsIcon from '@assets/icons/ic-dashboard-gps.svg'; import '@components/Dashboard/dashboardKeyframes.css'; const LocalCouponUsage = () => { + const headerSelectedState = useRecoilValue(headerAccommodationState); + const { data } = useGetLocalCouponUsage(headerSelectedState.id); return (
@@ -17,7 +22,7 @@ const LocalCouponUsage = () => { src={locationIcon} alt="장소" /> - 종로구 기준 + {data.address} 기준
@@ -38,7 +43,7 @@ const LocalCouponUsage = () => { 현재 내 숙소 주위의 사장님들이 - 평균 {8}종 이상의 쿠폰을 사용하고 있어요! + 평균 {data.coupon_avg}종 이상의 쿠폰을 사용하고 있어요! { diff --git a/src/components/Dashboard/DashboardLeftSection/LocalInformationSection/index.tsx b/src/components/Dashboard/DashboardLeftSection/LocalInformationSection/index.tsx index 4c829557..412bd973 100644 --- a/src/components/Dashboard/DashboardLeftSection/LocalInformationSection/index.tsx +++ b/src/components/Dashboard/DashboardLeftSection/LocalInformationSection/index.tsx @@ -3,10 +3,12 @@ import styled from '@emotion/styled'; import { useQueryErrorResetBoundary } from '@tanstack/react-query'; import { ErrorBoundary } from 'react-error-boundary'; -import LocalCouponUsage from './LocalCouponUsage'; const LocalTop3Coupons = React.lazy(() => import('./Top3Coupons')); import Top3CouponErrorFallback from './Top3Coupons/index.error'; -import Top3CouponRanking from './Top3Coupons/index.loading'; +import Top3CouponRankingLoading from './Top3Coupons/index.loading'; +const LocalCouponUsage = React.lazy(() => import('./LocalCouponUsage')); +import LocalUsageErrorFallback from './LocalCouponUsage/index.error'; +import LocalUsageLoading from './LocalCouponUsage/index.loading'; const LocalInformationSection = () => { const { reset } = useQueryErrorResetBoundary(); @@ -14,14 +16,21 @@ const LocalInformationSection = () => { return ( - + + }> + + + - }> + }> diff --git a/src/components/Dashboard/DashboardRightSection/CouponStatusSection/index.tsx b/src/components/Dashboard/DashboardRightSection/CouponStatusSection/index.tsx index e41138a1..cf0a5bb4 100644 --- a/src/components/Dashboard/DashboardRightSection/CouponStatusSection/index.tsx +++ b/src/components/Dashboard/DashboardRightSection/CouponStatusSection/index.tsx @@ -2,7 +2,7 @@ import styled from '@emotion/styled'; import { useRecoilValue } from 'recoil'; import StatusItem from './StatusItem'; -import { useGetMonthStatus } from '@hooks/queries/useGetMonthStatus'; +import { useGetMonthStatus } from '@hooks/index'; import { getStatusToLocaleString } from '@utils/index'; import { headerAccommodationState } from '@recoil/index'; diff --git a/src/components/Dashboard/DashboardRightSection/DailyReportSection/index.tsx b/src/components/Dashboard/DashboardRightSection/DailyReportSection/index.tsx index 60f51220..74c3db67 100644 --- a/src/components/Dashboard/DashboardRightSection/DailyReportSection/index.tsx +++ b/src/components/Dashboard/DashboardRightSection/DailyReportSection/index.tsx @@ -2,7 +2,7 @@ import styled from '@emotion/styled'; import { useRecoilValue } from 'recoil'; import GetMatchedReport from './GetMatchedReport'; -import { useGetDailyReport } from '@hooks/queries/useGetDailyReport'; +import { useGetDailyReport } from '@hooks/index'; import { headerAccommodationState } from '@recoil/index'; const DailyReportSection = () => { diff --git a/src/components/ErrorFallback/ErrorApp/index.tsx b/src/components/ErrorFallback/ErrorApp/index.tsx deleted file mode 100644 index af782ea1..00000000 --- a/src/components/ErrorFallback/ErrorApp/index.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { FallbackProps } from 'react-error-boundary'; - -const ErrorApp = ({ error, resetErrorBoundary }: FallbackProps) => { - return ( -
- App 전체 에러 발생 -
{error.message}
- -
- ); -}; - -export default ErrorApp; diff --git a/src/components/ErrorFallback/index.tsx b/src/components/ErrorFallback/index.tsx deleted file mode 100644 index b673b5ba..00000000 --- a/src/components/ErrorFallback/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { default as ErrorApp } from './ErrorApp'; diff --git a/src/components/Loading/LoadingApp/index.tsx b/src/components/Loading/LoadingApp/index.tsx deleted file mode 100644 index 28a38a3f..00000000 --- a/src/components/Loading/LoadingApp/index.tsx +++ /dev/null @@ -1,5 +0,0 @@ -const LoadingApp = () => { - return

로딩중...

; -}; - -export default LoadingApp; diff --git a/src/components/Loading/index.tsx b/src/components/Loading/index.tsx deleted file mode 100644 index 6cdde82e..00000000 --- a/src/components/Loading/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { default as LoadingApp } from './LoadingApp'; diff --git a/src/components/Report/LeftSection/index.error.tsx b/src/components/Report/LeftSection/index.error.tsx index 294c1262..0f30bfaa 100644 --- a/src/components/Report/LeftSection/index.error.tsx +++ b/src/components/Report/LeftSection/index.error.tsx @@ -2,9 +2,7 @@ import { FallbackProps } from 'react-error-boundary'; import styled from '@emotion/styled'; import theme from '@styles/theme'; -const ErrorFallback = ({ error, resetErrorBoundary }: FallbackProps) => { - console.log(error); - +const ErrorFallback = ({ resetErrorBoundary }: FallbackProps) => { return ( 누적 리포트 차트 에러 발생 diff --git a/src/components/Report/RightSection/TotalReport/index.error.tsx b/src/components/Report/RightSection/TotalReport/index.error.tsx index e4b324f8..8c7752c4 100644 --- a/src/components/Report/RightSection/TotalReport/index.error.tsx +++ b/src/components/Report/RightSection/TotalReport/index.error.tsx @@ -3,9 +3,7 @@ import styled from '@emotion/styled'; import theme from '@styles/theme'; -const ErrorFallback = ({ error, resetErrorBoundary }: FallbackProps) => { - console.log(error); - +const ErrorFallback = ({ resetErrorBoundary }: FallbackProps) => { return ( 누적 똑똑 현황 에러 발생 diff --git a/src/components/Report/RightSection/TotalReport/index.loading.tsx b/src/components/Report/RightSection/TotalReport/index.loading.tsx index 46017814..3a422ad2 100644 --- a/src/components/Report/RightSection/TotalReport/index.loading.tsx +++ b/src/components/Report/RightSection/TotalReport/index.loading.tsx @@ -32,10 +32,10 @@ const Container = styled.div` ${theme.response.tablet} { width: 90%; - max-height: 250px; + max-height: 220px; margin: 0; - padding: 20px 10px; + padding: 10px 10px; overflow: hidden; } @@ -59,7 +59,10 @@ const Title = styled(BaseSkeleton)` ${theme.response.tablet} { width: 30%; + + margin-bottom: 15px; border-radius: 7px; + padding-bottom: 5px; } `; @@ -74,6 +77,8 @@ const ContentsWrapper = styled.div` ${theme.response.tablet} { border-radius: 10px; + + gap: 5px; } `; @@ -88,7 +93,7 @@ const ContentsTop = styled(BaseSkeleton)` ${theme.response.tablet} { width: 100%; - height: 65px; + height: 50px; } `; diff --git a/src/components/Report/RightSection/index.tsx b/src/components/Report/RightSection/index.tsx index 051ddd88..918247af 100644 --- a/src/components/Report/RightSection/index.tsx +++ b/src/components/Report/RightSection/index.tsx @@ -3,17 +3,20 @@ import { useQueryErrorResetBoundary } from '@tanstack/react-query'; import { ErrorBoundary } from 'react-error-boundary'; import styled from '@emotion/styled'; -const TotalReport = React.lazy(() => import('./TotalReport')); +import { MobileDashboardHeader } from '@components/common'; import Catchphrase from './Catchphrase'; import Loading from './TotalReport/index.loading'; import ErrorFallback from './TotalReport/index.error'; import theme from '@styles/theme'; +const TotalReport = React.lazy(() => import('./TotalReport')); + const RightSection = () => { const { reset } = useQueryErrorResetBoundary(); return (
+ { + const navigate = useNavigate(); + const location = useLocation(); + + return ( + + + { + navigate('/'); + }} + > + 대시보드 + + { + navigate('/coupons/report'); + }} + > + 누적 리포트 + + + + + ); +}; + +export default MobileDashboardHeader; + +const Container = styled.div` + display: none; + + ${theme.response.tablet} { + width: 100%; + + border-bottom: 2px solid #c5c5c57f; + padding: 0 15px; + + display: flex; + justify-content: space-between; + align-items: flex-end; + + font-size: 11px; + font-weight: 700; + } +`; + +const MenuContainer = styled.div` + ${theme.response.tablet} { + display: flex; + } +`; + +const DashboardNavigation = styled.div` + ${theme.response.tablet} { + margin-right: 30px; + padding: 10px 0; + border-bottom: ${props => + props.$pathname === '/' ? '3px solid #001d6c' : 'none'}; + + color: ${props => (props.$pathname === '/' ? '#001d6c' : '#757676')}; + white-space: nowrap; + cursor: pointer; + } +`; + +const ReportPageNavigation = styled(DashboardNavigation)` + ${theme.response.tablet} { + border-bottom: ${props => + props.$pathname === '/coupons/report' ? '3px solid #001d6c' : 'none'}; + + color: ${props => + props.$pathname === '/coupons/report' ? '#001d6c' : '#757676'}; + } +`; + +const Button = styled.button` + ${theme.response.tablet} { + width: 90px; + height: 25px; + + margin-bottom: 5px; + border: none; + border-radius: 6px; + + display: flex; + justify-content: center; + align-items: center; + + background: linear-gradient(91deg, #ff3478 1.39%, #ff83ad 98.63%); + color: white; + font-size: 11px; + font-weight: 700; + cursor: pointer; + + &:hover { + background: #b22655; + } + } +`; diff --git a/src/components/common/index.tsx b/src/components/common/index.tsx index d80616f2..72b83916 100644 --- a/src/components/common/index.tsx +++ b/src/components/common/index.tsx @@ -1,3 +1,4 @@ export { default as Layout } from './Layout'; export { default as DashboardHeader } from './DashboardHeader'; +export { default as MobileDashboardHeader } from './MobileDashboardHeader'; export { default as Footer } from './Footer'; diff --git a/src/components/modal/index.tsx b/src/components/modal/index.tsx index 75dcd309..dd7717ad 100644 --- a/src/components/modal/index.tsx +++ b/src/components/modal/index.tsx @@ -1,38 +1,41 @@ import styled from '@emotion/styled'; import theme from '@styles/theme'; -import { useState } from 'react'; export interface ModalProps { modalText: string; subText: boolean; onConfirmClick(): void; + onCloseClick(): void; } -const Modal = ({ modalText, subText, onConfirmClick }: ModalProps) => { - const [isShowModal, setIsShowModal] = useState(true); - const [isShowSubText] = useState(true); - - const handleModalClose = () => { - setIsShowModal(false); +const Modal = ({ + modalText, + subText, + onConfirmClick, + onCloseClick +}: ModalProps) => { + const handleConfirmClick = () => { + onConfirmClick(); }; - const handleConfirmClick = () => { - onConfirmClick; - handleModalClose(); + const handleModalClose = () => { + onCloseClick(); }; - return isShowModal ? ( + return ( {modalText} - {isShowSubText && {subText}} + {subText && ( + 삭제한 쿠폰은 복구할 수 없습니다. + )} 확인 취소 - ) : null; + ); }; export default Modal; @@ -103,6 +106,10 @@ const ConfirmButton = styled.button` color: ${theme.colors.white}; background: #1a2849; cursor: pointer; + + &:hover { + background: #5f6980; + } `; const CancelButton = styled.button` width: 158px; @@ -114,4 +121,8 @@ const CancelButton = styled.button` color: ${theme.colors.white}; background: #b1b1b1; cursor: pointer; + + &:hover { + background: #404446; + } `; diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 8da9dfdf..6d71fb99 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -2,5 +2,15 @@ export { default as useOutsideClick } from './lib/useOutsideClick'; /* quries hooks */ +export { + useCouponUpdate, + useCouponDelete, + useToggleChange +} from './queries/useCouponList'; export { default as useGetTotalReport } from './queries/useGetTotalReport'; export { default as useGetYearReport } from './queries/useGetYearReport'; +export { default as useGetCouponRanking } from './queries/useGetCouponRanking'; +export { default as useGetDailyReport } from './queries/useGetDailyReport'; +export { default as useGetLocalCouponUsage } from './queries/useGetLocalCouponUsage'; +export { default as useGetMonthReports } from './queries/useGetMonthReports'; +export { default as useGetMonthStatus } from './queries/useGetMonthStatus'; diff --git a/src/hooks/queries/useCouponList.ts b/src/hooks/queries/useCouponList.ts new file mode 100644 index 00000000..f2394b7f --- /dev/null +++ b/src/hooks/queries/useCouponList.ts @@ -0,0 +1,62 @@ +import { + CouponDeleteCredential, + CouponListResponse, + CouponToggleCredential, + CouponUpdateCredential +} from '@/types/couponList'; +import { + useMutation, + useQueryClient, + useSuspenseQuery +} from '@tanstack/react-query'; +import { + couponDeleteApi, + couponToggleApi, + couponUpdateApi, + getCouponList +} from 'src/api/lib/getCouponList'; + +// 쿠폰 조회 +export const useGetCouponList = ( + accommodationId: number, + date?: string, + status?: string, + title?: string +) => + useSuspenseQuery({ + queryKey: ['CouponList', accommodationId, status, date, title], + queryFn: () => getCouponList(accommodationId, date, status, title) + }); + +// 쿠폰 수정 +export const useCouponUpdate = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: couponUpdateApi, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['CouponList'] }); + } + }); +}; + +// 쿠폰 삭제 +export const useCouponDelete = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: couponDeleteApi, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['CouponList'] }); + } + }); +}; + +// 토글 +export const useToggleChange = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: couponToggleApi, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['CouponList'] }); + } + }); +}; diff --git a/src/hooks/queries/useGetCouponRanking.tsx b/src/hooks/queries/useGetCouponRanking.ts similarity index 71% rename from src/hooks/queries/useGetCouponRanking.tsx rename to src/hooks/queries/useGetCouponRanking.ts index d2439d75..99ea2e37 100644 --- a/src/hooks/queries/useGetCouponRanking.tsx +++ b/src/hooks/queries/useGetCouponRanking.ts @@ -2,9 +2,11 @@ import { useSuspenseQuery } from '@tanstack/react-query'; import { getCouponRanking } from 'src/api'; -export const useGetCouponRanking = (accommodation_id: number) => { +const useGetCouponRanking = (accommodation_id: number) => { return useSuspenseQuery({ queryKey: ['CouponRanking', accommodation_id], queryFn: () => getCouponRanking(accommodation_id) }); }; + +export default useGetCouponRanking; diff --git a/src/hooks/queries/useGetDailyReport.tsx b/src/hooks/queries/useGetDailyReport.ts similarity index 72% rename from src/hooks/queries/useGetDailyReport.tsx rename to src/hooks/queries/useGetDailyReport.ts index 952bab36..fe364e6f 100644 --- a/src/hooks/queries/useGetDailyReport.tsx +++ b/src/hooks/queries/useGetDailyReport.ts @@ -2,9 +2,11 @@ import { useSuspenseQuery } from '@tanstack/react-query'; import { getDailyReport } from 'src/api'; -export const useGetDailyReport = (accommodation_id: number) => { +const useGetDailyReport = (accommodation_id: number) => { return useSuspenseQuery({ queryKey: ['DailyReport', accommodation_id], queryFn: () => getDailyReport(accommodation_id) }); }; + +export default useGetDailyReport; diff --git a/src/hooks/queries/useGetHeaderAccommodation.ts b/src/hooks/queries/useGetHeaderAccommodation.ts index 34b11b10..4f7a77af 100644 --- a/src/hooks/queries/useGetHeaderAccommodation.ts +++ b/src/hooks/queries/useGetHeaderAccommodation.ts @@ -1,10 +1,10 @@ import { useSuspenseQuery } from '@tanstack/react-query'; import { getHeaderAccommodation } from 'src/api'; -import { HeaderAccommodationResult } from '@/types/layout'; +import { HeaderAccommodationData } from '@/types/layout'; const useGetHeaderAccommodation = () => - useSuspenseQuery({ + useSuspenseQuery({ queryKey: ['Accommodation'], queryFn: () => getHeaderAccommodation() }); diff --git a/src/hooks/queries/useGetLocalCouponUsage.ts b/src/hooks/queries/useGetLocalCouponUsage.ts new file mode 100644 index 00000000..14d75f70 --- /dev/null +++ b/src/hooks/queries/useGetLocalCouponUsage.ts @@ -0,0 +1,12 @@ +import { useSuspenseQuery } from '@tanstack/react-query'; + +import { getLocalCouponUsage } from 'src/api'; + +const useGetLocalCouponUsage = (accommodation_id: number) => { + return useSuspenseQuery({ + queryKey: ['LocalCouponUsage', accommodation_id], + queryFn: () => getLocalCouponUsage(accommodation_id) + }); +}; + +export default useGetLocalCouponUsage; diff --git a/src/hooks/queries/useGetMonthReports.ts b/src/hooks/queries/useGetMonthReports.ts new file mode 100644 index 00000000..0fdb9b74 --- /dev/null +++ b/src/hooks/queries/useGetMonthReports.ts @@ -0,0 +1,12 @@ +import { useSuspenseQuery } from '@tanstack/react-query'; + +import { getMonthReports } from 'src/api'; + +const useGetMonthReports = (accommodation_id: number) => { + return useSuspenseQuery({ + queryKey: ['MonthReports', accommodation_id], + queryFn: () => getMonthReports(accommodation_id) + }); +}; + +export default useGetMonthReports; diff --git a/src/hooks/queries/useGetMonthStatus.ts b/src/hooks/queries/useGetMonthStatus.ts index 474a9a47..ee8d07f5 100644 --- a/src/hooks/queries/useGetMonthStatus.ts +++ b/src/hooks/queries/useGetMonthStatus.ts @@ -3,9 +3,11 @@ import { useSuspenseQuery } from '@tanstack/react-query'; import { getMonthStatus } from 'src/api'; import { CouponStatusResults } from '@/types/dashboard'; -export const useGetMonthStatus = (accommodation_id: number) => { +const useGetMonthStatus = (accommodation_id: number) => { return useSuspenseQuery({ queryKey: ['MonthStatus', accommodation_id], queryFn: () => getMonthStatus(accommodation_id) }); }; + +export default useGetMonthStatus; diff --git a/src/routes/MainRouter/index.tsx b/src/routes/MainRouter/index.tsx index 7f4df96e..2ca4edeb 100644 --- a/src/routes/MainRouter/index.tsx +++ b/src/routes/MainRouter/index.tsx @@ -7,7 +7,7 @@ import Dashboard from '@pages/Dashboard'; import Report from '@pages/Report'; import CouponList from '@pages/CouponList'; import Register from '@pages/Register'; -import Settlements from '@pages/Settlements'; +import Settlements from '@pages/Settlements'; const MainRouter = () => { return ( diff --git a/src/types/couponList.ts b/src/types/couponList.ts index d870d494..72d2a864 100644 --- a/src/types/couponList.ts +++ b/src/types/couponList.ts @@ -40,7 +40,7 @@ export interface CouponInformationResponse { customer_type: string; coupon_room_type: string; minimum_reservation_price: number; - coupon_use_condition_days: string[]; + coupon_use_condition_days: string; exposure_start_date: string; exposure_end_date: string; coupon_expiration: number; @@ -56,6 +56,31 @@ export interface CouponListProps { couponInfo: CouponInformationResponse; } +export interface CouponUpdateCredential { + coupon_number: string | undefined; + accommodation_id: number; + customer_type: string; + discount_type: string; + discount_value: number; + coupon_room_type: string; + register_all_room: false; + register_rooms: string[]; + minimum_reservation_price: number; + coupon_use_condition_days: string[]; + exposure_start_date: string; + exposure_end_date: string; +} + +export interface CouponDeleteCredential { + coupon_number: string | undefined; +} + +// 토글 api 요청 타입 +export interface CouponToggleCredential { + coupon_number: string | undefined; + coupon_status: string; +} + // HACK : 쿠폰 요청 타입 // export interface GetCouponListCredential { // accommodationId: number; diff --git a/src/types/dashboard.ts b/src/types/dashboard.ts index 5cebb62d..58318567 100644 --- a/src/types/dashboard.ts +++ b/src/types/dashboard.ts @@ -9,6 +9,7 @@ export type GraphHeaderTag = Pick; export type MonthReportsResults = { statistics_year: number; statistics_month: number; + total_sales: number; coupon_total_sales: number; download_count: number; used_count: number; @@ -67,6 +68,11 @@ export type CouponRankingResult = { third_coupon_title: string; }; +export type LocalCouponUsageResult = { + address: string; + coupon_avg: string; +}; + // DailyReport export type DailyReportResult = { diff --git a/src/types/layout.ts b/src/types/layout.ts index 2884b841..aa24c354 100644 --- a/src/types/layout.ts +++ b/src/types/layout.ts @@ -13,7 +13,11 @@ export type HeaderAccommodation = { address: string; }; -export type HeaderAccommodationResult = HeaderAccommodation[]; +export type HeaderAccommodationResult = { + accommodation_responses: HeaderAccommodation[]; +}; + +export type HeaderAccommodationData = HeaderAccommodation[]; // Sidebar export type SidebarHeader = { diff --git a/src/utils/lib/couponCondition.ts b/src/utils/lib/couponCondition.ts new file mode 100644 index 00000000..f625776b --- /dev/null +++ b/src/utils/lib/couponCondition.ts @@ -0,0 +1,12 @@ +export const CouponCondition = (conditionDays: string): string => { + if (conditionDays.length === 1) { + return `${conditionDays}요일`; + } else if (conditionDays === '평일') { + return '일~목'; + } else if (conditionDays === '주말') { + return '금~토'; + } + return conditionDays; +}; + +export default CouponCondition;