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/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/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..08fe7355 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -2,5 +2,10 @@ 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'; 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/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/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;